4

I find . (alt. for many others) to be a very useful when working in a terminal, inserting the last argument from the last line at the current prompt. However, it does not work when the last line was commented (assuming set -o interactivecomments).

I can't think of an exact use case, but it has happened several times that I've commented out a command that I realized before execution to not be what I needed at that point, and then wanted the final argument of it in a subsequent command.

I realize that there is a logic to ignoring everything commented out, since it is never interpreted and therefore none of it is tokenized into arguments, but is there any way at all to do what I want?

It would not be ideal—but would be usable—if I had to use a different keystroke, bound to an alternate command that would do what I want.

I'm using Zsh, but a Bash solution might work or be close to what I'd need to do in Zsh.

2
  • 2
    Often you can use : instead of commenting it out, and then it's still a command, but that won't work if you have command substitution or something else side-effecting in the command. Does that matter for your use case? Apr 14, 2018 at 23:09
  • in my initial experiments it is perfect.... now I just need to change ⌥⇧3 to use : instead of # 😃
    – iconoclast
    Apr 14, 2018 at 23:28

4 Answers 4

4

Don't comment the command out.

Instead, put a : at the front rather than a #. That will make your command text arguments to the null utility :, which does nothing when run. The arguments are tokenised and parsed, because it's still a command, but nothing else is done with them.


There are cases where this isn't suitable: in particular, when figuring out the command itself has side effects. A simple example is command substitution:

$ : log $(find -print -delete) --target /var/run/cache

In this case, the command inside $(...) would be run and substituted into the arguments to :, even though the original command itself would never run. A less-destructive case would be ${foo:=default} parameter expansion, which has the side effect of assigning a value to $foo.

Finally, redirections or pipes will still take place:

$ : foo > outfile
$ : bar | grep x | ...

in both cases will send empty output into the destination, which will truncate or create the file or run the subsequent commands, which may not like the empty input they get.


If you don't have any of those special concerns for your command, this is entirely safe and works in zsh, Bash, and any POSIX-compatible shell (though the alt-. behaviour afterwards won't work everywhere).

If it's specifically the last word you want, zsh provides customisable key bindings and commands that you can use to manipulate the command line. If you bindkey '^K' kill-region then you can move back to before the word, Ctrl-K to erase to the start of the line, :, and then have a safe command with just the one word you care about left.

1
  • Interesting. It doesn't do anything by default (for me), and it's to start-of-line if I bindkey it with that command. The mark is at the start of the line by default. End-of-line is more consistent with emacs default behaviour. Regardless, that might actually be more useful - you can also paste out of the kill buffer if you bind yank or yank-pop to something. Apr 14, 2018 at 23:53
3

You can do this with comments, too; you do not have to switch to : if you are willing to use a different shortcut:

  1. Define a function which outputs its last positional parameter:

    lastarg () { test $# -eq 0 && return 1; while [ $# -gt 1 ]; do shift; done; printf '%s' $1; }
    
  2. Create a key binding which calls:

    lastarg $(history 2 | head -1)
    

    and inserts the output.

This may lead to wrong results due to word splitting in case the last argument was quoted.

dangerous version for quoting

If you are sure enough that nothing bad can happen (no unquoted <, >, &, $(), \``,;,#, and no0 single"or'` then you can do this in order to get a quoted last "argument" from a comment line:

# dummy foo\ \ \ bar
eval : $(history 2 | awk 'NR==1 { sub(" *[0-9]* *#",""); print; }') \; echo '"$_"'
    + eval : dummy 'foo\' '\' '\' bar ';' echo '"$_"'
    ++ : dummy 'foo   bar'
    ++ echo 'foo   bar'
    foo   bar
2
  • Is there any way around the quoting problem?
    – iconoclast
    Apr 15, 2018 at 2:35
  • @iconoclast I did not try to make this safe but at least it works. See my edit. Apr 15, 2018 at 14:44
2

Use the shell builtin !$ which corresponds to the last argument.

I'm using zsh as well and this works to access the last argument of a commented command for me in the shell.

4
  • 1
    This doesn't work for me. I get # in place of !$. Any idea why it would not be consistent? Is there an option that would affect this?
    – iconoclast
    Apr 15, 2018 at 2:37
  • It works for me with no zshrc. Perhaps I'm reading the question wrong. Are we talking about a commented line like #this entire line is commented or do you mean ls -l # end of line comment? In the latter scenario, it's still possible to specify the argument by counting which argument it was and using !:n where n is the argument index. In that example, -l would be !:1
    – arcsin
    Apr 15, 2018 at 2:53
  • Either scenario, where I'm trying to get commented in the first or comment in the second.
    – iconoclast
    Apr 15, 2018 at 3:30
  • Okay then my original answer is still the correct way to do that then. I have no idea why it wouldn't work.
    – arcsin
    Apr 15, 2018 at 3:36
1

You could add to your ~/.zshrc:

zshaddhistory() {
  local histchars=
  print -Sr -- ${1%$'\n'}
  fc -p
}

That is, in the zshaddhistory special hook function called before the current command line is added to the history, add it ourselves with an empty histchars, which would disable comment processing for print -S. fc -p makes sure the command line is not added yet another time to the history.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .