$_ is the last argument that actually works in a script

Bang-dollar and dollar-underscore give you the same thing at the prompt, so most people learn one and never meet the other. They are not the same mechanism, and only one of them survives being put in a file.

Short version: !$ is history expansion and $_ is an ordinary shell variable. At the prompt they hand you the same string, which is why almost nobody notices there are two of them. Put either in a script and the difference stops being trivia: !$ is passed through as literal text, $_ works exactly as it does interactively.

Everything below came off a real shell session, not from memory.

At the prompt they look identical

Here is !$, the history form.

root@ops-01:/srv/app# ls -l logs/app.log
-rw-r--r-- 1 root root 14 Aug  4 11:40 logs/app.log
root@ops-01:/srv/app# wc -l !$
wc -l logs/app.log
2 logs/app.log

Here is $_, doing the same job.

root@ops-01:/srv/app# ls -l logs/app.log
-rw-r--r-- 1 root root 14 Aug  4 11:40 logs/app.log
root@ops-01:/srv/app# wc -l $_
2 logs/app.log

Same result, and there is already a visible tell between them. The !$ run printed wc -l logs/app.log before its output. The $_ run did not.

That extra line is bash echoing the expansion. History expansion rewrites your command before running it, so bash shows you what it rewrote. $_ is a variable, and a shell does not narrate variable expansion any more than it announces $HOME. If you see the echo, you used history. If you do not, you used the variable.

In a script only one of them survives

Two scripts doing the same work, differing by two characters.

root@ops-01:/srv/app# cat withbang.sh
#!/bin/bash
ls -l logs/app.log
wc -l !$
root@ops-01:/srv/app# bash withbang.sh
-rw-r--r-- 1 root root 14 Aug  4 11:40 logs/app.log
wc: '!
#39;: No such file or directory

History expansion is an interactive feature. In a script it is switched off, so !$ is never rewritten and wc goes looking for a file with that literal name.

root@ops-01:/srv/app# cat withunder.sh
#!/bin/bash
ls -l logs/app.log
wc -l $_
root@ops-01:/srv/app# bash withunder.sh
-rw-r--r-- 1 root root 14 Aug  4 11:40 logs/app.log
2 logs/app.log

$_ is a variable. Variables do not care whether a shell is interactive, so this behaves in a file exactly as it did at the prompt.

Three things about $_ that surprise people

It follows every command, including the harmless ones. cd takes an argument like anything else, so $_ becomes the directory you just moved to.

root@ops-01:/srv/app# cd /tmp
root@ops-01:/tmp# echo $_
/tmp

On the first line of a script it is not what you would guess. Before any command has run there is no previous argument, so bash sets $_ to the path it was invoked with. Running bash atstart.sh gives you the interpreter, not the script.

root@ops-01:/srv/app# cat atstart.sh
#!/bin/bash
echo "first line sees: $_"
root@ops-01:/srv/app# bash atstart.sh
first line sees: /usr/bin/bash

It updates after the command that read it. $_ is the last argument of the previous command, and once echo $_ has expanded, its own last argument is the value that was just printed. So reading it twice gives the same answer for a reason that is not the one people assume.

root@ops-01:/srv/app# true one two three
root@ops-01:/srv/app# echo $_
three
root@ops-01:/srv/app# echo $_
three

The second three is not left over from true. It is the last argument of the first echo, which happened to be three.

Which to reach for

Use !$ at the prompt. It is fewer keystrokes, and the echo is a genuine safety feature: you see the rewritten command before it runs.

Use $_ when a command is going into a file, a cron entry, a systemd unit, or anywhere else that is not an interactive shell.

Use neither in a script you expect somebody to maintain. $_ works, but it points at something invisible from where it is written, and it changes meaning if anyone inserts a line above it. Name the value once and use the name.

Quick reference

FormWhat it isEchoes the expansionWorks in a script
!$history expansionYesNo
!^history expansion, first argumentYesNo
$_shell variableNoYes
$1 in a functionpositional parameterNoYes

Verified on GNU bash 5.2.15 on Debian 12. $_ is bash specific in this exact behaviour; dash does not provide it.

Common questions

Is $_ the same as !$? They usually produce the same string at an interactive prompt, but they are different mechanisms. !$ is history expansion, rewritten before the command runs and disabled in scripts. $_ is a variable that works everywhere.

Why does my command print itself when I use !$ but not $_? That line is bash showing you the result of history expansion before running it. Variables are not narrated, so $_ expands silently.

Why is $_ empty or wrong at the top of my script? Because no command has run yet. At that point bash has set it to the path used to invoke the shell, so bash script.sh gives you the interpreter path.

Can I use $_ inside a pipeline or a subshell? It exists there, but it refers to the last argument in that context rather than the one you were looking at, which is a common way to get a value you did not expect. If the value matters, assign it to a name.

More from the Field Manual, or work through the training tracks.