3rd January 2019 | 6 minutes
When it comes to shell scripting there are some odd quirks and behaviours to the syntax that may not be immediately obvious. This post aims to collect my learnings of general shell weirdness, and maybe it will help you as well!
When using a variable, you will always want to use double quotes when getting it's value. This is because otherwise your terminal of choice might decide to interpret a number of characters in it's own, delightfully destructive way.
An easy example can be given with looping over a variable's contents without using quotes.
list="one two three"
for word in $list
do
echo "$word"
done
# Contrary to what you might expect this will output
# three variables in sequence:
## => one
## => two
## => three
And just like that you're in for a nasty surprise
Woops! Looks like your terminal just turned your nice string into a whitespace-delimited array. Which is clearly what you wanted all along, no?
To prevent this, you need only wrap the $list
variable in quotes, like so
list="one two three"
for word in "$list"
do
echo "$word"
done
# This will now output the expected string
## => one two three
Quotes are magic. Never forget that
So as an easy rule to remember: Unless you're on a treasure hunt for pain, just always wrap your variables in quotes.
Conditionals in shell scripting is super fun, because wrapping things in various bracket formats is totally what you expect to do. But those brackets hold deep dark mysteries that can easily trip you up when you want to do simple conditionals.
Let's do a simple number comparison. For our intents and purposes let's agree that 10
is bigger than 5
.
Now consider the following example
# conditionals.sh
ten=10
five=5
if [ "$ten" > "$five" ]
then
echo "ten is bigger than five"
else
echo "ten is smaller than five"
fi
I'll let you guess what the outcome will be
The outcome should be quite obvious, no? Run the script and we should see ten is bigger than five
. Easy!
And that's where you're wrong. Here's what the actual output is.
./conditionals.sh: line 6: 10: No such file or directory
ten is smaller than five
It's like some kind of alternate reality. Spooky
Wait, what? We're getting a file not found error and according to the shell 10
is smaller than 5
. I don't know what
math class you took, but in my world that's just all kinds of wrong.
What happened?
The first thing that went wrong is that we used a single pair of brackets. When we do that, the shell will automatically
try to execute and compare to a file glob pattern and trips up because it can't find a directory named >
. The easy way
to fix this is either to use double braces like so: [[ "$ten" > "$five" ]]
or just escape the greater-than symbol
with a backward slash like so \>
.
This will get rid of the first error, however you will still receive output telling you that ten is smaller than five
.
The problem here is that by using square brackets, the shell will automatically do a lexical comparison. And in the alphabet one
and zero generally come before five.
Now there's two ways we can correct this, and I'll show you both.
# conditionals.sh
ten=10
five=5
# Solution 1
# This version uses the -gt switch to force
# arithmetic comparison.
if [ "$ten" -gt "$five" ]
then
echo "ten is bigger than five"
else
echo "ten is smaller than five"
fi
# Solution 2
# This version uses a pair of parentheses (( )) to force
# arithmetic comparison.
if (( "$ten" > "$five" ))
then
echo "ten is bigger than five"
else
echo "ten is smaller than five"
fi
It just all makes sense. All of it
There are lots of other different comparison operators like -gt
and you can find an extensive guide over at
tldp.org along with the exact comparison syntax.
For the last chapter of this wild joy ride through shell behavior, we will take a look at functions. They too behave in ways that might be confusing if you come from other programming languages.
Contrary to basically every other programming language out there (save for some exceptions), bash does not use parentheses for a function call. You simply write the name. If you want to pass in some arguments, just add them with a space after the function call. It looks like this:
function foobar {
echo "Foo to the bar"
}
foobar
foobar "argument1" "argument2"
This feels so.. empty
Both calls will do the exact same thing. One thing to note is that you can pass any number of arguments to a shell function and it will not care. If you don't use arguments, they will simply be ignored. Another thing to be aware of is that you do not need to write parentheses after the function name. Your terminal simply doesn't care.
Unlike other programming languages, arguments in shell scripting are not defined in the function signature, but are rather
used by using special $
variables, like so.
function foobar {
argument_1=$1
argument_2=$2
argument_list=$@
echo "First argument: $argument_1"
echo "Second argument $argument_2"
echo "List of arguments: $argument_list"
}
foobar "argument1" "argument2"
# Generated output
## => First argument: argument1
## => Second argument argument2
## => List of arguments: argument1 argument2
I think this is where Larry Wall got inspired for Perl
This also means that it's completely up to you to check whether an argument is actually set and throw an error if that is not the case. If you want to output nice error messages I highly recommend my post on coloring your terminal output. Ahem.
Another fun thing about shell scripts. If you define variables in your function like I did above, they are by default in global scope and can be accessed by anyone after the first time the function was called. Check this out
function foobar {
argument_1=$1
argument_2=$2
argument_list=$@
echo "First argument: $argument_1"
echo "Second argument $argument_2"
echo "List of arguments: $argument_list"
}
foobar "argument1" "argument2"
echo "Outside the function: $argument_1"
# Generated output
## => First argument: argument1
## => Second argument argument2
## => List of arguments: argument1 argument2
## => Outside the function: argument1
A whole new level of "Hello World"
To prevent this behavior, you simply need to prefix your variables with the local
keyword to prevent the function scope
from spilling into the outside world. It looks like so
function foobar {
local argument_1=$1
local argument_2=$2
local argument_list=$@
echo "First argument: $argument_1"
echo "Second argument $argument_2"
echo "List of arguments: $argument_list"
}
foobar "argument1" "argument2"
echo "Outside the function: $argument_1"
# Generated output
## => First argument: argument1
## => Second argument argument2
## => List of arguments: argument1 argument2
## => Outside the function: <nothing>
Now the variable is contained as it should be
And as grand finale, let's talk about return values!
You'd think that using a simple return
statement would be enough, yes? I hate to break it to you, but no. No, of course
not. Because doing so would be entirely too easy. Instead what you do is to echo
your return value and then capture the
output into a new variable. This looks somewhat like this
function foobar {
local argument_1=$1
local argument_2=$2
local argument_3=$3
echo "$argument_1 $argument_2 $argument_3"
}
foobar_returned=$(foobar "foo" "bar" "baz")
echo "Foobar return value: $foobar_returned"
# Generated output
## => Foobar return value: foo bar baz
If you've gotten this far without giving up, I applaud you
Phew. Makes you feel like somebody repairing their boat by putting duct tape over all the leaky holes.
As you can see the world of shell scripting has lots of traps for unsuspecting people strolling past. I hope this post helps you avoid some of these traps in your own scripts. For a concise reference on most shell syntax you can check out learnxinyminutes.com. It's my go-to reference for odd quirks and things I just cannot seem to remember.
Enjoy, and good luck out there!~