$_ vs !$. Last argument of the preceding command and output redirection

38,610

Solution 1

!$ is a word designator of history expansion; it expands to the last word of the previous command in history. In other words, the last word of the previous entry in history. This word is usually the last argument to the command, but not in case of redirection. In:

echo "hello" > /tmp/a.txt

the whole command 'echo "hello" > /tmp/a.txt' appeared in history, and /tmp/a.txt is the last word of that command.

_ is a shell parameter; it expands to the last argument of the previous command. Here, the redirection is not a part of arguments passed to the command, so only hello is the argument passed to echo. That's why $_ expanded to hello.

_ is no longer one of shell standard special parameters. It works in bash, zsh, mksh and dash only when interactive, ksh93 only when two commands are on separated lines:

$ echo 1 && echo $_
1
/usr/bin/ksh

$ echo 1
1
$ echo $_
1

Solution 2

I found it difficult if not impossible to use parameter substitution directly with !$.

For example if I could wget http://foo.com/my.tar then tar xf ${_##*/} but with !$ it is not possible without defining an intermediary variable.

Share:
38,610

Related videos on Youtube

Loom
Author by

Loom

Updated on September 18, 2022

Comments

  • Loom
    Loom almost 2 years

    The question is about special variables. Documentation says:

    !!:$

    designates the last argument of the preceding command. This may be shortened to !$.


    ($_, an underscore.) At shell startup, set to the absolute pathname used to invoke the shell or shell script being executed as passed in the environment or argument list. Subsequently, expands to the last argument to the previous command after expansion. Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command.

    There must be some difference I cannot catch, because:

    $ echo "hello" > /tmp/a.txt
    $ echo "!$"
    echo "/tmp/a.txt"
    /tmp/a.txt
    
    $ echo "hello" > /tmp/a.txt
    $ echo $_
    hello
    

    What is the difference?

    • Jeff Schaller
      Jeff Schaller over 8 years
      !$ is history expansion, not a special variable, so I'd assume that the bash code handles them differently.
    • zwol
      zwol over 8 years
      I don't have a definitive answer to this question, but: ① The Bourne shell language is one of, perhaps the, least internally consistent programming language still widely used today. Attempting to deduce general principles will get you nowhere. You just have to memorize all the special rules. Sorry. ② Note the words "after expansion", immediately after the phrase you highlighted, in the definition of $_. That's crucial. It might not explain the difference that puzzles you but it does explain a bunch of other ways $_ and !$ are not the same.
  • BlueJ
    BlueJ over 4 years
    One effect of !$ expansion vs $_ parameter is that echo $_ always shows in history as echo $_, while echo !$ shows the value that it was expanded to. E.g. echo one two then echo !$, then pressing up shows echo two.
  • Fons MA
    Fons MA almost 4 years
    Not sure I understand what "only when interactive" applies to. I was just able to do something along the lines of mkdir -p foo && for f in ${array_of_filenames[@]}; do mv $f $_; done in a bash script
  • Admin
    Admin about 2 years
    This is really about the documentation overloading the word "argument" with two different meanings. $_ gets the last command line argument that actually went to the executed command though the execve() call. That happens after the command line is fully parsed, after all expansions etc. But !$ looks at words at the command line really early, before much parsing has happened. It does some things like detecting quotes there, but not much else.
  • Admin
    Admin about 2 years
    You can see a difference too with e.g. echo $RANDOM, where $_ would contain the actual value passed, and hence echo $_ would print the same random number again. But !$ would turn into $RANDOM, which would then be expanded as usual, giving a different number. Or with false && echo bar where $_ would give false (the last argument to the last executed command), but !$ would give bar (the last word on the last command line).