How do I replace a specific argument of a previous command in zsh?

Let's say I have the following command:

/long/path/to/bin -a foo -b 2 -c 3 -d 4 foo | gunzip -c | less

And now I want to replace foo behind 4 with bar, so it becomes:

/long/path/to/bin -a foo -b 2 -c 3 -d 4 bar | gunzip -c | less

I also know that I could do:

!:0-8 bar !:10-$

Is there also a way where I can simply substitute one argument? Something like:

!:9:bar

Obviously this won't work, because it will only expand to the 9th argument, complaining about an unknown modifier.


Solution 1:

You can use a zle widget to do that.

Define the widget

Enter this on the command line (or insert it in your ~/.zshrc, then it's permanent):

accept-line-with-histmod() {
  if [[ $BUFFER[1,3] == "?\!:" ]]; then
    local loc mod
    loc=${${(s.:.)BUFFER}[2]}
    mod=${${(s.:.)BUFFER}[3]}
    zle up-history
    BUFFER="${${(z)BUFFER}[1,$((loc))]} $mod ${${(z)BUFFER}[$((loc+2)),-1]}"
    CURSOR=$#BUFFER
  else
    zle accept-line
  fi
}
zle -N accept-line-with-histmod
bindkey "^M" accept-line-with-histmod

How to use it?

The syntax is slightly diffent to that you proposed, but not much (I added a question mark in front, so that the parsing is more easily). Demo ($ is the prompt):

$ /long/path/to/bin -a foo -b 2 -c 3 -d 4 foo | gunzip -c | less
[...]
$ ?!:9:bar
$ /long/path/to/bin -a foo -b 2 -c 3 -d 4 bar | gunzip -c | less

How it works!

This widget is bound to the ENTER key, replacing the default key binding of accept-line, therefore it's executed every time you hit enter.

First the widget checks, if the command line starts with ?!:; the variable $BUFFER holds the complete command line. If not, the usual accept-line widget is executed resulting in the default behavior.

But if the line start with ?!:, then the arguments (?!:POSITION:REPLACEMENT TEXT) are stored in variables $loc and $mod, resp. [${${(s.:.)BUFFER}[2]} splits the command line (BUFFER) at colons ((s.:.)) and takes the second word ([2]) ]

Now, it recalls the last line from history (zle up-history) which now resides in $BUFFER. (If you want to handle older events, then you must expand the widget here!)

Then it basically does the same you have done in you manual example, building the current command line out of the old one. (The offset of 1 is because the index of arrays start at 1, but the index in the history expansion at 0).

Finally the cursor is placed at the end of the new built command line, waiting for you to acknowledge the execution.

Remarks: Obviously there is no error handling, so be careful! I also didn't test it with very complex command lines. It may or may not work with those.


Alternatively you might be interested in the search & replace syntax of the history expansion:

$ echo foo
$ !:s/foo/bar

Or the r builtin

$ echo foo
$ r foo=bar

However, this is not possible with you example in the question, as foo there is not unique.