man bash , procure a seção Parâmetros especiais . Por _ você encontrará:
expands to the last argument to the previous command, after expansion.
Assim, em um contexto de linha de comando, a diferença é a expansão. Por exemplo, se eu tiver uma variável FOO definida como 'bar' e executar echo $FOO , então $_ na próxima linha resultará em bar sendo enviado no próximo comando, enquanto !$ resultará em $FOO .
Esse comportamento pode não ser óbvio por padrão, mas com shopt -s histverify é um pouco mais evidente. (Com histverify habilitado, o Bash não enviará a expansão do histórico imediatamente após Enter , primeiro exibirá o comando, pós-expansão, e aguardará um segundo Enter ). Então, veríamos algo assim depois de FOO=bar :
$ echo $FOO
bar
$ ls !$ $_
# We hit Enter and then see
$ ls $FOO $_
$_ não é expandido até que o comando seja submetido durante a expansão do parâmetro (juntamente com $FOO ). É uma distinção sutil, mas lá vai você.
Provavelmente, a distinção mais interessante / útil é que $_ é uma variável . Portanto, por exemplo, se o último parâmetro for /some/file/path.txt , você poderá obter o comprimento ${#_} -> 19 , extrair apenas o nome do arquivo ${_##*/} -> path.txt , alterar a raiz ${_//some/other} -> /other/file/path.txt e assim por diante. Não é possível fazer tudo isso com !$ .
Observe que $_ tem significados diferentes em outros contextos:
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.
... e ...
Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command.
... e ...
When checking mail, this parameter holds the name of the mail file currently being checked.
ATUALIZAÇÃO: Embora seja mais preciso, minha resposta carece de algumas informações importantes e talvez até enganosas sobre como / quando $_ realmente obtém um valor. Essa primeira citação destina-se a ler assim:
expands to the last argument to the previous command as it was received by that command, i.e. after going through Shell Expansion
A melhor maneira de ilustrar é com alguns exemplos:
$ echo "$(date +%s)" # %s is seconds since epoch
1536984661
$ echo $_ "$(date +%s)" # don't hit enter until a few seconds pass
1536984661 1536984665
Portanto, $_ obtém os segundos reais desde a época calculados no momento em que o primeiro echo foi executado não perto da invocação 1536984665 do segundo date .
Outro:
$ foo='a b c'
$ echo "$foo"
a b c
$ echo "$_"
a b c
$ #---------
$ echo $foo
a b c
$ echo "$_"
c
No segundo caso, não citamos $foo . Antes de ser passado para echo , passou pela expansão de variável / parâmetro seguida de divisão de palavras. A falta de aspas resultou em "a", "b" e "c" sendo passados como três parâmetros separados e $_ obteve o último, ou seja, "c".