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.