Ultimate Bash
This is part of Underappreciated Bash idioms, which see.
ultimate /ˈʌltɪmət/
adj. 1: The last item of a series.
2: The utmost, the highest.
Highly enjoyable
To an outsider, it looks like a hack, at best. At worst, it's unintelligible syntax. Neither impression does much to rescue Bash from its reputation of not being a "real language", and might even contribute to a sudden outburst of "why am I even wasting my time with shell scripts no really this thing doesn't even have libraries seriously that's it I'm done with Bash I'll rewrite this in Python".
Not quite how I see it.
On its face, Ultimate Bash is just an idiom, or a small combination thereof.
Then you start using it, and it grows on you.
Then it becomes more like a style.
Then soon enough its gravitational force starts arranging other Bash things around it.
Ultimate Bash is a highly enjoyable central piece of my Bash experience.
The basics
You're in a terminal. You want a new directory.
mkdir -p ~/Documents/research/functional-programming/pdf
cd ~/Documents/research/functional-programming/pdf
You're not gonna type that twice, are you?
Recapturing the last argument
Since you never got around to adding this to your ~/.bash_aliases:
mcd() { mkdir -p "$1" && cd "$1" ;}
in which case this common situation could always be solved without repetition:
mcd ~/Documents/research/functional-programming/pdf
you'll need some other way out: how are you going to avoid typing all that again?
You have a few options.
Option 1: tab completion
It helps: ~/D<TAB>re<TAB>… — but not very good, and not general enough: many things don't tab-complete.
Option 2: history expansion
Those exclamation things you never got around to memorizing.
mkdir -p ~/Documents/research/functional-programming/pdf
cd !$
Press RET and the last argument of the previous command replaces the !$ and the line is immediately executed.
(or soon enough)
If you don't like the risk of these things immediately executing without you being sure that it got the intended string right, you can either:
- press Alt-^ to manually expand it each time, or
add this to your
~/.bashrc:
shopt -s histverifyso that whenever you press RET on history expansions it'll only expand it;
In either case, if the expansion looks ok, you press RET again to execute it.
Yet I never use !$, because...
Option 3: yank-last-arg
It can get better.
mkdir -p ~/Documents/research/functional-programming/pdf
cd
After entering the cd and a space, press Alt-. — and voilà, the last argument of the previous command is inserted at point. Just press RET to run it.
Go ahead. Open a Bash terminal right now.
Type echo then SPC then Alt-. repeatedly to see the last argument of each of your previous commands.
Enormously useful.
So to recap, you can save yourself from retyping by:
- tab-completing — when that's at all an option;
- using
!$, from history expansion; - using
Alt-., which isyank-last-arg.
Unfortunately, all of these are for interactive use.
Option 4: dollar-low-line
What if you want to do it in a script?
Then $_ is your friend:
mkdir -p ~/Documents/research/functional-programming/pdf cd "$_"
Unlike the others, which would save the expanded directory string itself in history, this one saves exactly what it shows, "$_".
Unlike the others, which are shortcuts for expansion, this is "just" a shell variable — one that, to quote man bash,
expands to the last argument to the previous simple command executed in the foreground, after expansion.
Low line is the official Unicode name of the character.
Underscore, underline, low line, low dash — same thing.
"So what?", you ask, "and why do you keep italicizing last argument as if it's important?"
Because it is important. The last argument (or "the ultimate argument") is highly recyclable, threadable, and enjoyable.
And you're gonna get familiar with the dollar-low-line guy — so you might as well start to affectionately call it the ultimate.
Its sidekick, the colon
Bash has a : command:
help :
:: :
Null command.
No effect; the command does nothing.
Exit Status:
Always succeeds.
which is morally equivalent to true:
help true
true: true
Return a successful result.
Exit Status:
Always succeeds.
with the only con that :'s meaning is less obvious. Yet this is compensated by two pros:
- it's shorter and unobtrusive
- it's semantically "less about declaring truth" and "more about doing nothing"
although that last part is not exactly true, or I wouldn't be bothering to write about it.
See, its declared doing-nothingness refers to the fact that it never outputs anything.
But the command is perfectly capable of enabling side effects — an observation that you can engineer to your advantage.
Behold:
x= #⇒ (no output) printf "$x" #⇒ (no output) : "${x:=foo}" #⇒ (no output) printf "${x^^}" #⇒ FOO
So it did something, didn't it? It allowed the assignment of the parameter expansion to happen. Because this:
x= #⇒ (no output) printf "$x" #⇒ (no output) "${x:=foo}" #→ error
would give you a line 3: foo: command not found, because that assigment outputs the quoted string "foo", which Bash tries to execute, whereas this:
: "foo"
is fine, because : always outputs nothing and succeeds.
Now look at this:
echo This is foo #⇒ This is foo echo "$_" #⇒ foo : This is bar #⇒ (no output) echo "$_" #⇒ bar
What was that again?
No effect; the Command does nothing.
Balderdash!
The Command does many things
when in Ultimate Bash.
The examples
Shall we?
Simplify conditionals
Let's start with conditionals.
Common Bash 1
foo() { local fruit_selection case "$foo" in a) fruit_selection=apples ;; b) fruit_selection=bananas ;; c) fruit_selection=cherries ;; d) fruit_selection=dates ;; *) echo "Wrong!" >&2 return 1 esac echo "$fruit_selection" }
Common Bash 2
foo() { case "$foo" in a) echo apples ;; b) echo bananas ;; c) echo cherries ;; d) echo dates ;; *) echo "Wrong!" >&2 return 1 esac }
Ultimate Bash
foo() { case "$foo" in a) : apples ;; b) : bananas ;; c) : cherries ;; d) : dates ;; *) echo "Wrong!" >&2; return 1 esac; echo "$_" ;}
Compose a string to build a message
You have a long error message that doesn't fit the line.
Common Bash
foo() { local number="$1" if ((number==42)) then echo "Good choice!" else echo "Unfortunately, the number you've chosen, " >&2 echo "$number, is not a really good one, " >&2 echo "so we'll have to leave this slightly " >&2 echo "contrived error message" >&2 FOO="Bad number"; return 1 fi ;}
Less common Bash
foo() { local number="$1" err="" if ((number==42)) then echo "Good choice!" else err="Unfortunately, the number you've chosen, " err+="$number, is not a really good one, " err+="so we'll have to leave this slightly " err+="contrived error message" echo "$err" >&2; FOO="Bad number"; return 1 fi ;}
Ultimate Bash
foo() { local number="$1" if ((number==42)) then echo "Good choice!" else : "Unfortunately, the number you've chosen," : "$_ $number, is not a really good one," : "$_ so we'll have to leave this slightly" : "$_ contrived error message" echo "$_" >&2; FOO="Bad number"; return 1 fi ;}
Compose a string to build command's expression
You have several sed substitutions to make, some of which big.
They wouldn't fit in one line.
Common Bash
foo() { sed -e 's/something-from-1/something-to-1/g' \ -e 's/something-from-2/something-to-2/g' \ -e 's/something-from-3/something-to-3/g' \ -e 's/something-from-4/something-to-4/g' \ -e 's/something-from-5/something-to-5/g' \ -e 's/something-from-6/something-to-6/g' "$1" | \ grep -oP '(?<=before)foo.*(?=after)' }
Pro: most standard format; aggressively aligns in Emacs.
Con: repetition of -e; can't add comments.
Less common Bash 1a
foo() { sed 's/something-from-1/something-to-1/g s/something-from-2/something-to-2/g s/something-from-3/something-to-3/g s/something-from-4/something-to-4/g s/something-from-5/something-to-5/g s/something-from-6/something-to-6/g' "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Less common Bash 1b
# Same, but with comments: foo() { sed 's/something-from-1/something-to-1/g # This will change something-2: s/something-from-2/something-to-2/g s/something-from-3/something-to-3/g # <-- solves stg3 s/something-from-4/something-to-4/g # Important because of something-5: s/something-from-5/something-to-5/g s/something-from-6/something-to-6/g' "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Pro: less noise: gets rid of quotes, of -e, and of line continuations; comments allowed
Con: multiline string won't autoalign in Emacs; comments don't syntax-highlight properly
I like this style better than Common Bash's.
Yes, you can add comments to the middle of sed strings if at end of line:
<<<foo sed '# Replace the f; s/f/g/; # Now the first o; s/o/e/; s/g/G/' #⇒ foo <<<foo sed '# Replace the f s/f/g/ # Now the first o s/o/e/; s/g/G/' #⇒ Geo
Ultimate Bash 1a
foo() { : 's/something-from-1/something-to-1/g s/something-from-2/something-to-2/g s/something-from-3/something-to-3/g s/something-from-4/something-to-4/g s/something-from-5/something-to-5/g s/something-from-6/something-to-6/g' sed "$_" "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Ultimate Bash 1b
# Same, but with comments: foo() { : 's/something-from-1/something-to-1/g # This will change something-2: s/something-from-2/something-to-2/g s/something-from-3/something-to-3/g # <-- solves stg3 s/something-from-4/something-to-4/g # Important because of something-5: s/something-from-5/something-to-5/g s/something-from-6/something-to-6/g' sed "$_" "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Pro: same as Less common; logic immediately clear since command now fits one line.
Con: same as Less common
Ultimate Bash 2
foo() { : "s/something-from-1/something-to-1/g" # This will change something-2 : "$_; s/something-from-2/something-to-2/g" : "$_; s/something-from-3/something-to-3/g" # <-- solves stg3 : "$_; s/something-from-4/something-to-4/g" # Important because of something-5 : "$_; s/something-from-5/something-to-5/g" : "$_; s/something-from-6/something-to-6/g" sed "$_" "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Pro: logic in one line; comments highlight as comments; aggressively aligns.
Con: noisier than the previous.
Ultimate Bash 3
foo() { local r; declare -a r r=("something-from-1" "something-to-1" # This will change something-2 "something-from-2" "something-to-2" "something-from-3" "something-to-3" # <-- solves stg3 "something-from-4" "something-to-4" # Important because of something-5 "something-from-5" "something-to-5" "something-from-6" "something-to-6") : "$(printf 's/%s/%s/g;' "${r[@]}")" sed "$_" "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Pro: same, but also gets rid of many s///g and $_;.
Con: using array adds other noises with declare and printf, plus the extra quotes.
Ultimate Bash 4
foo() { local r; declare -a r r=("something-from-1/something-to-1" # This will change something-2 "something-from-2/something-to-2" "something-from-3/something-to-3" # <-- solves stg3 "something-from-4/something-to-4" # Important because of something-5 "something-from-5/something-to-5" "something-from-6/something-to-6") : "$(printf 's/%s/g;' "${r[@]}")" sed "$_" "$1" | grep -oP '(?<=before)foo.*(?=after)' }
Same, but merges each pair into from/to, so less noise from quotes.
I like Ultimate Bash #1a better if no comments are needed; but #2 when heavy commenting is preferable.
Test a function's arguments
You have a function.
- If it receives three or more arguments, it's wrong.
- If 2nd arg is null or unset, make it 42.
- Assign 2nd arg to local variable
bar. - Unless 1st arg is null or unset:
- assume it's a file, and
- store its contents in the local variable
foo.
- assume it's a file, and
- Output to
stdout:Bar: $bar\nContents of foo:\n$foo
Common Bash
quux() { local foo bar if [ "$#" -ge 3 ]; then echo "Error: we need two or less args" >&2 return 1 fi if [ -z "$2" ]; then bar=42 else bar="$2" fi if [ -n "$1" ]; then foo=$(cat "$1") else foo="" fi echo "Bar: $bar" echo "Contents of foo:" echo "$foo" }
First impression: Very common and readable Bash, nothing wrong with it!
Second impression: Argh! Can't. Avoid. Tweaking it.
Less common Bash
quux() { local foo bar (("$#">2)) && echo >&2 "Error: we need two or less args" && return 1 [[ -z "$1" ]] && foo="" || foo=$(<"$1") [[ -n "$2" ]] && bar="$2" || bar=42 echo -e "Bar: $bar\nContents of foo:\n$foo" }
Much better. Sigh of relief.
Those first foo and bar assignments (after &&) always return true.
On the other hand, this wouldn't be nice:
[[ -n "$1" ]] && foo=$(<"$1") || foo=""
because if $1 isn't a file, it fails, then foo="" runs.
We could fix that with:
local foo="" [[ -n "$1" ]] && foo=$(<"$1")
or:
[[ -f "$1" ]] && foo=$(<"$1") || foo=""
but rather than analyzing the possibility of failure case by case, there's an alternative that makes short-circuit evaluations robust.
Ultimate Bash 1
quux() { : "Error: we need two or less args" (("$#">2)) && echo >&2 "$_" && return 1 [[ "$1" ]] && : "$(<"$1")" || : "" ; local foo="$_" [[ "$2" ]] && : "$2" || : 42 ; local bar="$_" echo -e "Bar: $bar\nContents of foo:\n$foo" ;}
Shorter maxwidth by detaching the error message.
Less repetitions of foo and bar. Count them.
Two lines fewer by not leaving braces in their own lines.
ShellCheck will complain that:
A && B || C is not if-then-else. C may run when A is true.
Wrong. No, it won't. Not here.
- When
Ais true, it'll runB. Bis always true, because:always succeeds.- Therefore,
Cwill never run.
With Ultimate Bash's colons, A && B || C is if-then-else.
Not convinced?
Try this:
true && : "$(<NonexistingFile)" || : Oops printf "\$_ is ->%s<- and return code is ->%s<-\n" "$_" "$?"
and you get:
bash: NonexistingFile: No such file or directory $_ is -><- and return code is ->0<-
The error turns "$(<NonexistingFile)" into the empty string.
But despite the error, it succeeds, so it doesn't reach Oops.
If anyone has a counter-example, I'm all ears.
Caveat: typing mistakes will make B fail — but then it's no longer the same thing:
true && :"foo bar" || : Oops echo "$_"
bash: :foo bar: command not found Oops
Yeah, sure. You didn't add a space after :, then it became the :foo bar command — which doesn't exist, so we reached Oops.
So just don't do that.
Ultimate Bash 2
quux() { : "Error: we need two or less args" (("$#">2)) && echo >&2 "$_" && return 1 local foo="${1:+$(<"$1")}" bar="${2:-42}" echo -e "Bar: $bar\nContents of foo:\n$foo" ;}
Here we test the arguments with parameter expansions.
Ultimate Bash 3
quux() { : "Error: we need two or less args" (("$#">2)) && echo >&2 "$_" && return 1 echo -e "Bar: ${2:-42}\nContents of foo:\n${1:+$(<"$1")}" ;}
Same, but ditching the local variables altogether.
Ultimate Bash 4
quux() { : "Error: we need two or less args" (("$#">2)) && echo >&2 "$_" && return 1 : "Contents of foo:\n${1:+$(<"$1")}" echo -e "Bar: ${2:-42}\n$_" ;}
Same, but you happen to prefer to see them separately.
Sequentially modify a string without sed
You want to:
- get only the characters 5 to 7
- downcase the first of these
- replace all "o" with "0"
- mark it as hex
- show conversion from hex to decimal
Like this:
foohex "Our Foe is finally defeated." foohex "The Foo won't fool you." foohex "The Doc has arrived."
0xf0e = 3854 0xf00 = 3840 0xd0c = 3340
Common Bash 1
foohex() { local hex hex=$(echo "$1" \ | sed -E \ -e "s/^....(...).*/\1/" \ -e "s/^./\L&/" \ -e "y/o/0/") echo -n "0x$hex = " echo "$hex" | sed "s/.*/\U&/; s/^/ibase=16; /" | bc -l }
I'd actually be surprised to see sed's seemingly little known \L instead of more complicated maneuvers to downcase a single letter.
The sed "s/.*/\U&/" would be simpler to replace — tr 'a-z' 'A-Z' would do it, and more commonly seen.
My point is that \L and \U are not that common — but the rest is.
Common Bash 2
foohex() { local hex hex=$(echo "$1" \ | sed -E \ -e "s/^....(...).*/\1/" \ -e "s/^./\L&/" \ -e "y/o/0/") echo "0x$hex = $((0x$hex))" }
Gets rid of bc, which cuts the size of the last line by 80%.
Less common Bash
foohex() { local hex hex=$(<<<"$1" \ sed -E "\ s/^....(...).*/\1/ s/^./0x\L&/; y/o/0/") echo "$hex = $((hex))" ;}
Closer to my taste: compact, prepended Here String, no -e for sed.
Ultimate Bash 1
foohex() { : "$(<<<"$1" \ sed -E "\ s/^....(...).*/\1/ s/^./0x\L&/; y/o/0/")" echo "$_ = $((_))" ;}
Local variable is gone. Hello : and $_ — we're in Ultimate Bash territory.
But this forces the huge command substitution to be quoted, which in turn messes up both with syntax highlighting and aggressive indentation. We can do better.
Ultimate Bash 2
foohex() { <<<"$1" \ sed -E "s/^....(...).*/\1/; s/^./0x\L&/ ; y/o/0/" | (: "$(cat)"; echo "$_ = $((_))") ;}
The huge quoted command substitution gets out of the subshell and becomes a regular command at the current shell. That's then piped to a subshell block and captured by the cat inside a tiny and pleasant-to-look-at command substitution, whose evaluation is then passed as the last argument of the : command, which "does nothing". This is, of course, recorded by $_, which is then used twice.
Better!
Ultimate Bash 3
foohex() { : "s/^....(...).*/\1/ ; s/^./0x\L&/ ; y/o/0/" <<<"$1" sed -E "$_" | (: "$(cat)" echo "$_ = $((_))") ;}
Oh, yes. By all means let's use a whole line for that large expression and pass that to next line's sed. That cleans up the sed line, whose logic you can now see more clearly, unencumbered by the details of the actual substitutions.
But wait a minute: do we even need that extra parenthesized subshell?
The $(cat) is being used exactly as captured. So we could assign-and-capture at once using a throwaway variable x (local not needed, as it'll disappear with the pipe's subshell):
Ultimate Bash 4
foohex() { : "s/^....(...).*/\1/ ; s/^./0x\L&/ ; y/o/0/" <<<"$1" sed -E "$_" | echo "${x=$(cat)} = $((x))" ;}
But wait a minute: now that the thing is so short, do we need that pipe? Do we need that cat?
Ultimate Bash 5
foohex() { : "s/^....(...).*/\1/ ; s/^./0x\L&/ ; y/o/0/" : "$(sed -E "$_" <<< "$1")" echo "$_ = $((_))" ;}
A huge thing in the first line is abstracted to $_.
This is used in the second line, all of which is abstracted to $_.
This is used twice (one of which modified) in the third line.
This is sent to stdout.
No pipe. Unless you squint.
Ultimate Bash 6
foohex() { : "${1:4:3}" ;: "0x${_,}" ;: "${_//o/0}" echo "$_ = $((_))" ;}
Shortest. Same idea, but no sed. Perfect.
Parameter expansions of ultimate variable chained through null commands:
- Faster. Tiny.
- At first, illegible.
- Then, you study it.
- Then, you get used to it.
- Then, you love it.
The formal definitions
We're ready for formal definitions.
ultimate /ˈʌltɪmət/
noun 1: (programming) In a shell, refers generically to the ultimate (i.e. last)
argument of a previous command.
2: (programming) In Bash, the ultimate (i.e. last) argument of the previous
simple foreground command of the current shell environment.
3: (programming) Metonymically, the variable "$_" that dynamically stores
the value of the ultimate argument.
verb 1: (programming) To practice Ultimate Bash.
and:
Ultimate Bash /ˌʌltɪmət ˈbæʃ/
noun 1: (programming) In Bash programming, the art and practice of employing
the ultimate (sense 3) in lieu of explicit repetitions
or new variable assignments, often with the support of
the null command and parameter expansions.
2: (programming) Code written in that style.
The laundry lists
But isn't that... unstable?
The idea of using in "serious code" something that changes all the time and you can't assign directly makes you uneasy.
I hear you. You don't want the $_ thing chameleonizing into something unexpected. Using hardcoded "stable" "normal" variables feels safer.
I once had this concern myself, and was likewise reluctant. All I can say is that I've been Ultimate Bashing for years now, and unpredictability has quickly fallen to about zero. I almost never see myself going:
Oh. This was not what I expected this thing to consider as previous command's ultimate argument.
Tell you what: let me try to exhaust the situations involving the last argument. This should assuage your concerns.
Where can you use the ultimate argument?
Will we get the $_'s value we expect?
You can use it more than once
: This is foo echo "This? It's a $_! A $_! ${_^}, nothing else."
This? It's a foo! A foo! Foo, nothing else.
You can sneak it into pipes
: This is foo echo "This?" | sed "s/$/ Why, it's a $_! ${_^}./"
This? Why, it's a foo! Foo.
You can sneak it into Here Strings
: What is this? sed "s/$/ Why, it's a foo!/" <<< "${_^}"
This? Why, it's a foo!
You can sneak it into Here Docs
: What is this? sed "s/$/ Why, it's a foo!/" <<EOF ${_^} EOF
This? Why, it's a foo!
You can sneak it into command substitutions
: This is foo echo "$(sed "s/$/ Why, it's a $_! ${_^}./" <<< "This?")"
This? Why, it's a foo! Foo.
You can sneak it into process substitutions
: This is foo cat < <(sed "s/$/ Why, it's a $_! ${_^}./" <<< "This?")
This? Why, it's a foo! Foo.
You can sneak it into arithmetic expansions
: The answer is 42 echo "The answer? $((_))."
The answer? 42.
: The answer is NOT 21 echo "The answer? $((_*2))."
The answer? 42.
You can sneak it into compound commands
Namely:
You can sneak it into braces (current shell)
: This is foo { x="A $_."; echo "This? $x Just ${x,}" ;}
This? A foo. Just a foo.
You can sneak it into parentheses (subshell)
: This is foo (x="A $_."; echo "This? $x Just ${x,}")
This? A foo. Just a foo.
You can sneak it anywhere inside tests and conditionals
short-circuit
: This is foo [[ "$_" == foo ]] && : "Ok, a $_." || : "That's a $_, not a foo!" echo "$_"
Ok, a foo.
: This is bar [[ "$_" == foo ]] && : "Ok, a $_." || : "That's a $_, not a foo!" echo "$_"
That's a bar, not a foo!
You can sneak it into arithmetic evaluations
: The answer is 42 ((_==6*7)) && : "The answer? $_." || : "$_ is not the answer." echo "$_"
The answer? 42.
: The answer is NOT 21 ((_==6*7)) && : "The answer? $_." || : "$_ is not the answer." echo "$_"
21 is not the answer.
What can provide the ultimate argument?
Ok, good. But we also need to look from the opposite direction.
Remember: $_ is assigned to "the last argument to the previous simple command executed in the foreground, after expansion".
Expansions happen first
Braces:
# This is bee boo gee goo fee foo : This is {b,g,f}{ee,oo} echo "$_"
foo
Global variable:
: This is "$BASH" echo "$_"
/usr/bin/bash
If no argument is passed, last argument is the command
: echo "$_"
:
false echo "$_"
false
ls >/dev/null echo "$_"
ls
Pipes
: foo echo b | sed "s/$/ar/" echo "$_"
bar foo
Surprised?
In multi-command pipelines, the commands execute in subshells.
The last simple foreground command is : foo.
Process substitution
: foo sed "s/$/ar/" < <(echo b) echo "$_"
bar s/$/ar/
But here that sed is running in the current shell environment — a simple command in the foreground.
Here String
sed "s/$/ar/" <<< b echo "$_"
bar s/$/ar/
and:
cat <<< foo echo "$_"
foo cat
and:
: a <<< b echo "$_"
a
You got the idea: the last argument is before the Here String.
Changing the order preserves that:
: <<<b a echo "$_"
a
Here Document
cat <<EOF foo EOF echo "$_"
foo cat
and:
: <<EOF foo EOF echo "$_"
:
Same idea: the last argument is before the Here Document.
Changing the order preserves that:
<<EOF cat foo EOF echo "$_"
foo cat
Command substitution
It's expanded, and then processed normally. Therefore, quoting matters:
: "$(seq -s', ' 3)" # : "1, 2, 3" echo "$_"
1, 2, 3
but:
: $(seq -s', ' 3) # : 1, 2, 3 echo "$_"
3
Conditional — bracket test
: foo [[ -n "bar" ]] && : is ok || : empty echo "$_" : foo [ -n "bar" ] && : is ok || : empty echo "$_" : foo test -n "bar" && : is ok || : empty echo "$_"
ok ok ok
Make sense?
If you don't use the test, it gets weird — avoid:
: foo [[ -n "bar" ]] echo "$_" : foo [ -n "bar" ] echo "$_"
foo ]
But when do you run a test that you don't use? So this shouldn't be an issue.
Conditional — if…then…else…fi
: foo if [[ -n "bar" ]]; then : is ok; else : empty; fi echo "$_" : foo if [ -n "bar" ] ; then : is ok; else : empty; fi echo "$_"
ok ok
As expected, and just like the short-circuits.
Even though if…then…else…fi is a compound command, it evaluates : is ok, which is a simple command executed in the foreground, so we get ok (is ok was unquoted).
You'd "capture the foo" if the : is ok were in a compound command subshell:
: foo if [[ -n "bar" ]]; then (: is ok); else : empty; fi echo "$_"
foo
Arithmetic evaluation is also compound, so if you put that there...
: foo if [[ -n "bar" ]]; then ((6*7)); else : empty; fi echo "$_" : foo if [ -n "bar" ] ; then ((6*7)); else : empty; fi echo "$_"
foo ]
But arithmetic expansion is another story:
: foo if [[ -n "bar" ]]; then : "$((6*7))"; else : empty; fi echo "$_" : foo if [ -n "bar" ] ; then : "$((6*7))"; else : empty; fi echo "$_"
42 42
The arithmetic expansion resolves itself to the string 42, and : 42 is a simple command.
Can you guess what happens if you put that in a subshell?
: foo if [[ -n "bar" ]]; then (: "$((6*7))"); else : empty; fi echo "$_" : foo if [ -n "bar" ] ; then (: "$((6*7))"); else : empty; fi echo "$_"
foo ]
"Hey, ok, stop! This already happened a few times: why does the first outputs foo but the second [?"
Here's a hint:
: "[" cat <<HINT $(type $_$_ ]] $_ ] 2>&1) And "help $_" will tell you that: $(help $_) $_ is this: $(type -P $_) HINT
[[ is a shell keyword
]] is a shell keyword
[ is a shell builtin
bash: line 2: type: ]: not found
And "help [" will tell you that:
[: [ arg... ]
Evaluate conditional expression.
This is a synonym for the "test" builtin, but the last argument must
be a literal `]', to match the opening `['.
[ is this: /usr/bin/[
Redirects
As with Here Strings and Here Documents, the last argument is the one before the redirect:
: null : foo > /dev/"$_" echo "$_"
foo
: null cat < /dev/"$_" echo "$_"
cat
Group commands count:
: null { echo -n; cat ;} < /dev/"$_" echo "$_"
cat
But here we have a pipeline, therefore subshells:
: urandom { tr -cd @ | head -c3 ;} < /dev/"$_" echo "$_"
@@@urandom
The last simple foreground command was : urandom.
Changing the order doesn't matter, still a pipeline:
: urandom tr -cd @ < /dev/"$_" | head -c3 echo "$_"
@@@urandom
No pipes, but also a subshell:
: null (echo -n; cat) < /dev/"$_" echo "$_"
null
Assignment and arithmetic
Remember: $_ is assigned to "the last argument to the previous simple command executed in the foreground, after expansion".
Assignment — in simple command
Current shell. Command is the last.
: quux x="This is foo" echo "$_"
(empty)
With stuff after it:
: quux x="This is foo" : Hello bar echo "$_"
bar
Assignment — in group command
Current shell. Command is the last.
: quux { : baz x="This is foo" ;} echo "$_"
(no output)
With stuff after it:
: quux { : baz x="This is foo" : Hello bar ;} echo "$_"
bar
Assignment — in subshell
Compound command, subshell. Command is not the previous. Skipped.
: quux (: baz; x="This is foo") echo "$_"
quux
With stuff after it:
: quux (: baz; x="This is foo" : Hello bar) echo "$_"
quux
Assignment — in Arithmetic evaluation
Compound command, so it's not the previous. Skipped.
: quux ((x=21, y=42)) echo "$_"
quux
Unless!
: quux ((x=21, _=42)) echo "$_"
42
: quux ((_=42, x=21)) echo "$_"
42
Since the assignment is to $_ itself, it changes that variable's value from quux to 42.
You can't directly assign
The _=42 in ((…)) above is an exception. Elsewhere it won't work:
echo This is foo #⇒ foo echo "->$_<-" #⇒ ->foo<- _=bar #⇒ (no output) echo "->$_<-" #⇒ -><-
nor this:
# WON'T WORK foo() { local number="$1" if ((number==42)) then echo "Good choice!" else : "Unfortunately, the number you've chosen," _+=" $number, is not a really good one," _+=" so we'll have to leave this slightly" _+=" contrived error message" echo "$_" >&2; FOO="Bad number"; return 1 fi ;}
This is because the plain assignment is a simple command composed of:
- an assignment (which is before the
$0) - the rest of the command, starting with
$0(which is empty, since it's just the assignment)
So $0, $1, $2 — all empty, so $_ is empty.
Compare:
x=A y=B z=C #⇒ (no output) printf "$_" #⇒ (no output) x=A y=B z=C true #⇒ (no output) printf "$_" #⇒ true
The cheat sheets
A small summary of assignment cases
: foo bar # The last argument is bar echo "$_" #⇒ bar : foo bar x=7 # This regular foreground assignment is the last simple command echo "$_" #⇒ (empty string...) : foo bar _=7 # Since the rest of the simple command is empty, no last argument echo "$_" #⇒ (empty string...) : foo bar { x=7 ;} # The same applies to group commands echo "$_" #⇒ (empty string...) : foo bar { _=7 ;} # So this reassignment will also be ignored echo "$_" #⇒ (empty string...) : foo bar x=7 & # But this is a background assignment, therefore skipped echo "$_" #⇒ bar : foo bar _=7 & # Same here: background, therefore skipped echo "$_" #⇒ bar : foo bar (x=7) # And this assignment is in a subshell compound command — skipped echo "$_" #⇒ bar : foo bar (_=7) # As is this. And subshell assignments don't survive outside them. echo "$_" #⇒ bar : foo bar ((x=7)) # Arithmetic evaluation is also compound command, so also skipped echo "$_" #⇒ bar : foo bar ((_=7)) # Skipped too — BUT! Assignment of _ survives the ((…))! echo "$_" #⇒ 7 : foo bar : "$((x=7, x))" # Here the simple command starts with a colon, ends with $x echo "$_" #⇒ 7
This takes some getting used to. But it's not a big deal.
A small summary of non-assignment cases
Since we're here, with non-assignments:
: foo bar # The last argument is bar echo "$_" #⇒ bar : foo bar : quux # The last one overrides it echo "$_" #⇒ quux : foo bar { : quux ;} # The same applies to group commands echo "$_" #⇒ quux : foo bar : quux & # But this ends in &, so a background command — skipped echo "$_" #⇒ bar : foo bar (: quux) # And this is in a subshell compound command — skipped echo "$_" #⇒ bar : foo bar echo | true # Same with pipes echo "$_" #⇒ bar : foo bar true < <(echo) # But here, true is in the current shell environment echo "$_" #⇒ true : foo bar sed 1d <<< quux # 2nd line has no output; 2nd line's 1d is last echo "$_" #⇒ 1d : foo bar sed 1d <<EOF # Same idea. quux EOF echo "$_" #⇒ 1d : foo bar : <<< quux # 2nd line has no output; 2nd line's colon is the last echo "$_" #⇒ : : foo bar : <<EOF # Same idea. quux EOF echo "$_" #⇒ : : foo bar : "$(echo quux)" # Command substitution: result becomes a regular string echo "$_" #⇒ quux : foo bar : "$((6*7))" # Arithmetic expansion: result becomes a regular string echo "$_" #⇒ 42 : foo bar [[ x ]] && : a || : b # Test is true, and : a is a simple command echo "$_" #⇒ a : foo bar if [[ x ]] then : a else : b fi # Same idea. echo "$_" #⇒ a : foo bar [[ x ]] && : ((6*7)) || # But ((6*7)) is not a simple command — skipped : b echo "$_" #⇒ bar : foo bar if [[ x ]] then ((6*7)) else : b fi # Same idea. echo "$_" #⇒ bar
The asymmetries
At first, you thought that "look from the opposite direction" seemed a bit redundant.
But then you noticed the asymmetries.
You can sneak the last argument into pipes...
: A CAT echo | sed "s/^/${_,,}/"
cat
...but you can't capture it from pipes:
: A foo echo -n | cat echo "$_"
foo
You can sneak it into background commands...
: A CAT echo "${_,,}" &
cat
...but you can't capture it from background commands:
: A foo : A cat & echo "$_"
foo
You can sneak it into parenthesized subshells...
: A CAT (echo "${_,,}")
cat
...but you can't capture it from parenthesized subshells:
: A foo (: A cat) echo "$_"
foo
Backgrounds, pipes, or parentheses can almost always be used to modify a command without any meaningful difference in the outcome:
echo foo (echo foo) # same result, delayed by a few microseconds that you won't notice
And this opens up a few possibilities.
Delayed ultimate capture
The concept
In another article, you have contemplated the possibility of delayed pipe captures:
echo foo | { printf "Just a "; cat ;} echo foo | { printf "Just a "; echo "$(</dev/stdin)." ;} echo foo | { printf "Just a "; : "$(cat)" ; echo "${_^^}!" ;}
Just a foo Just a foo. Just a FOO!
In Ultimate Bash, data flows less from pipes, and more through the dynamic $_ variable.
You may thus find it germane to ponder whether the idea of delayed capture translates to this style.
- Can we better control the flow?
- Can we do other things instead of rushing to use what the last command offers us?
- Can we also delay the capture of the last argument — aka the ultimate argument, aka the ultimate?
Why, but of course! Behold:
Options for delaying
We want to capture the bar.
: A foo : A bar echo "$_" #⇒ bar
We no longer want to capture the bar.
: A foo : A bar & echo "$_" #⇒ foo : A foo (: A bar) echo "$_" #⇒ foo : A foo : A bar | cat echo "$_" #⇒ foo : A foo : A bar |: echo "$_" #⇒ foo
The first one (&) sends it to the background — but parallelizing things might change the order of outputs, and you might not want that. So be careful.
The second and third are your go-to options for doing something while preserving the last argument.
Compare:
: Hello echo "$_ world!" echo "$_, once again."
Hello world! Hello world!, once again.
But:
: Hello (echo "$_ world!") echo "$_, once again."
Hello world! Hello, once again.
Likewise:
: Hello echo "$_ world!" | cat echo "$_, once again."
Hello world! Hello, once again.
Piping into the void
The fourth may look odd. After all, piping anything to : will make it successfully disappear:
echo foo |: #⇒ (empty, with exit status 0) false |: #⇒ (empty, with exit status 0)
It looks useless, then:
: Hello echo "$_, once again."
Hello, once again.
: Hello echo "$_ world!" |: echo "$_, once again."
Hello, once again.
But here's one use case: error messages.
: A foo echo quux >&2 echo "$_"
quux # to stderr quux # to stdout
: A foo echo quux >&2 |: echo "$_"
quux # to stderr foo # to stdout
The pipe only captures stdin, so the error message is sent intact to stderr, just as it would if |: weren't there.
Yet the pipe makes execution happen in subshells, so the line is skipped by $_.
And either of these two would do the same job as the |: solution above:
: A foo echo quux >&2 | cat echo "$_"
: A foo (echo quux >&2) echo "$_"
When integers are involved
If what you want to do in the middle involves integers, you can also use arithmetic evaluation to delay the capture.
You won't capture the foo here:
: A foo x=42 echo "$_" #⇒ (empty)
because x=42 is a valid foreground simple command with no last argument (prepended assignments don't count, remember?), so $_ becomes empty.
Nor here:
: A foo : "${x=42}" echo "$_" #⇒ 42
because "${x=42}" assigns x, then expands to 42, which becomes the last argument of the simple command : 42.
But this will work:
: A foo ((x=42)) echo "$_" #⇒ foo
The previous, more general options wouldn't quite work.
I mean, they would work for capturing the foo, but the variable assignment itself would fail.
Look:
: A foo # Subshell — skipped by $_, and assignments will be gone v=42 & (w=42) : "${x=42}" | cat : "${y=42}" | : # Compound — skipped by $_, but assignment will persist ((z=42)) cat <<EOF _ = $_ (we captured it!) v = $v │ w = $w │> gone with the subshell x = $x │ y = $y │ z = $z (persisted) EOF
_ = foo (we captured it!) v = │ w = │> gone with the subshell x = │ y = │ z = 42 (persisted)
The other shells
What, Bash no good? 😕
Z shell, aka Zsh
Z shell folks, rejoice: it seems that Zsh can potentially fulfill your ultimate needs:
touch foo1.zsh chmod +x "$_" # So far so good cat <<'FOO' >foo1.zsh # Why not "$_"? We'll see why... #!/usr/bin/env zsh : The world echo "Hello, $_!" FOO ./foo1.zsh
Hello, world!
Zsh has :, Zsh has $_, and they seem to work in the simplest case.
BUT!
# Bash would write it to ./bar # Zsh would write it to ./there : A foo bar echo Hello there > "$_"
And:
# Bash would output: bar\n # Zsh would output: \n : A foo bar (: Hello) echo "$_"
And:
# Bash would output: foobar\n # Zsh would output: foocat\n : A bar printf foo | cat echo "$_"
The art of Ultimate Shell Scripting is a different animal over there.
man zshall has this to say about it:
_ <S> The last argument of the previous command.
Also, this parameter is set in the environment of every
command executed to the full pathname of the command.
Zsh's "The last argument of the previous command."
sounds simpler than
Bash's "The last argument to the previous simple command executed in the foreground, after expansion."
but a quick look at the examples above suggests it's not so simple.
What counts as "previous command" in Zsh?
- When redirecting, does the first part already count as previous, as the
theresuggests? - Was
(: Hello)a previous command with no last argument, then? - Does the last command in a pipeline count as the previous, as the
catsuggests?
I'll leave these questions for Zsh folks to answer.
Will someone there feel inspired to flesh out the details of Ultimate Zsh?
Dash, aka sh
You're out of luck.
In Bash, when there's no previous command, $_ will output the path to the executable.
Zsh, on the other hand, outputs nothing in this case: $_ is empty when no previous command.
What about sh?
The sh executable is usually actually a symlink to dash, a POSIX-compliant implementation of /bin/sh:
stat -c%N /bin/sh | tr -d \'
/bin/sh -> dash
Over there, : works fine, but $_ seems to return path to the executable regardless of whether there was some previous command.
In other words, it seems you won't be able to practice your ultimate skills when in sh.
Here's the summary:
:; bash -c ': A foo; echo "$_"' #⇒ foo # Ultimate Bash! 🎉 :; bash -c ' echo "$_"' #⇒ /usr/bin/bash :; zsh -c ': A foo; echo "$_"' #⇒ foo # Ultimate Zsh! 🎊 :; zsh -c ' echo "$_"' #⇒ (empty string) :; sh -c ': A foo; echo "$_"' #⇒ /usr/bin/sh # Not over here. 😕 :; sh -c ' echo "$_"' #⇒ /usr/bin/sh
The wild
Most of the previous examples were "crafted for didactical purposes".
That's prefectly fine, but I'd also like to show a few instances of Ultimate Bash used in "actual code".
Function output
From Append2Org.
Uses conditionals to select the next sed expression to use.
Variable input is sequentially redefined with an alternation of parameter expansions (substring expansion plus case modification) and sed.
output() { local input="$1" # Let's further fix the input. # We want its first line to always start with "** ". # We also want to be able to batch-add entries. if [[ "${input:0:3}" == "** " ]]; then : "" elif [[ "${input:0:3}" == "***" ]]; then : "1 s/^/** (new entry)\n/" elif [[ "${input:0:2}" == "* " ]]; then : "/^[*]+ / s/^/*/" else : "1 s/^/** /" fi input="$(sed -E "$_" <<< "$input")" # Then we check if there's a "todo" followed or not by [abc]. # This is simply so we can feed clipboard with things like: # "todoa Wash cat" and have it sent to inbox as "** TODO [#A] Wash cat". : "${input:0:8}" : "${_,,}" if [[ "$_" == "** todoa" ]]; then : "1 s/...todoa/** TODO [#A]/I" elif [[ "$_" == "** todob" ]]; then : "1 s/...todob/** TODO [#B]/I" elif [[ "$_" == "** todoc" ]]; then : "1 s/...todoc/** TODO [#C]/I" elif [[ "$_" == "** todo " ]]; then : "1 s/...todo /** TODO /I" else : "" fi input="$(sed -E "$_" <<< "$input")" # We can be even lazier: # "t a Wash cat". : "${input:0:7}" : "${_,,}" if [[ "$_" == "** t a " ]]; then : "1 s/...t../** TODO [#A]/I" elif [[ "$_" == "** t b " ]]; then : "1 s/...t../** TODO [#B]/I" elif [[ "$_" == "** t c " ]]; then : "1 s/...t../** TODO [#C]/I" else : "" fi sed -E "$_" <<< "$input" }
Function loadvars
From Append2Org.
Sometimes the opposite makes sense: conditional clauses may want to repeatedly use the previous command's last argument.
loadvars() { : ~/.config/append2org.rc # USER: this file^ must exist if [[ -f "$_" ]] then . "$_" else echo "$_ is missing; create it" >&2 exit 1 fi ;}