loading...
Cover image for Shellscripting: Conditional Execution

Shellscripting: Conditional Execution

puritanic profile image Darkø Tasevski Updated on ・6 min read

Alt text of image

This is the second part of the series on Shellscripting. In case you've missed it, you can find the first part here.

Also, note that the most of the code is tested only with the bash and zsh shells, it may not work with other shells.

Conditional execution means that you can choose to execute code only if certain conditions are met. Without this capability, all you would be able to do is execute one command after another after another. The ability to test a variety of things about the state of the system, and of the environment variables of the process, means that a shell script can do far more powerful things that would otherwise be possible. In this post, we are going to explore test operators, if/then/elseconditionals, and case statements.

test aka [

With tests we can check for example: if the file exists, if a number is greater than another, compare if strings are equal...

Syntax:

[ condition-to-test-for ]

Example:

[ -e /etc/passwd ]

As heading above says, another name for test is [. It is also a shell builtin (which means that the shell itself will interpret [ as test, even if your Unix environment is set up differently). When [ is called, it requires a ] around its arguments, but otherwise, it does the same work.

test  -e /etc/passwd
# as above so below
[ -e /etc/passwd ]

This tests if etc/passwd exists, and if it does this returns true - command exit status of 0. If it doesn't exist the command exits with the exit status of 1 (more on exit statuses in next post).

Gotcha: The spaces around the [ and ] symbols are required! For example:
[-e /etc/passwd ] will not work; it is interpreted as test-e /etc/passwd ] which errors because ] doesn't have a beginning [. [ is actually a program, and just like ls and other programs, it must be surrounded by spaces.
Moral of the story: Put spaces around all your operators.

Note: You can reverse the results of the test with !:

if [ ! -r$1]; then echo "File $1 is not readable – skipping."; fi

As you can see test is a simple but powerful comparison utility. For full details, run man test on your system, but here are some usages and typical examples:

File test operators:

-d FILE #True if the file is a directory
-e FILE #True if the file exists
-f FILE #True if the file exists and it's regular file
-r FILE #True if the file is readable by you
-s FILE #True if the file exists and it's not empty
-w FILE #True if the file is writable by you
-x FILE #True if the file is executable by you

String test operators:

-z STRING #True if the string is empty
-n STRING #True if the string is not empty
STRING1 = STRING2 #True if the strings are equal
STRING1 != STRING2 #True if the strings are not equal

Arithmetic tests:

arg1 -eq arg2 #True if the arguments are equal
arg1 -ne arg2 #True if the arguments are not equal
arg1 -lt arg2 #True if the arg1 is less than arg2
arg1 -le arg2 #True if arg1 is less than or equal to arg2
arg1 -gt arg2 #True if arg1 is greater than arg2
arg1 -ge arg2 #True if arg1 is greater than or equal to arg2

&& and ||

It is possible to combine tests, and/or chain multiple commands by using the && and || operators. These perform a Logical AND and Logical OR, respectively.

  • && = AND
    mkdir /tmp/bak && cp test.txt /tmp/bak

    The command that follows && will be executed if and only the previous command succeeds (aka exits with 0 exit status).

  • || = OR
    cp test.txt /tmp/bak || cp test.txt /tmp

    The || operator performs a Logical OR, so when it only matters that one of the conditions is met, but not which one, this is the feature to use.

#! /bin/bash
HOST="google.com"
ping -c 1 $HOST && echo "$HOST reachable."

IF/THEN

Almost every programming language has an if/then/else construct, and the shell is no exception. The syntax uses square brackets to perform a test, and the then and fi statements are required, acting just like the { and } curly brackets in C and some other languages.

Syntax:

if [ condition ]
then
    statements for when the condition is true
fi

Other than the line break after the then, all these line breaks are required or can be replaced with semicolons. To remind: the spaces around the [ and ] symbols are also required, so this can be reduced (pls don't) at best to:
if [ condition ];then statements;fi

It is quite common to use the semicolon to put the then on the same line as the if.

Example:

MY_SHELL="zsh"

if [ "$MY_SHELL" = "zsh" ]
    then
        echo "You are the zsh shell user!"
fi

ELSE

It may be that you want to run the command if possible, but if it can’t be done, then continue execution of the script. One (simpler and the most common) way to do this would be to use ELSE statement:

if [ condition ]; then
    statements for when the condition is true
else
    statements for when the condition is false
fi
#!/bin/bash

# Check for likely causes of failure
if [ -r "$1" ]; then
    cat "$1"
else
    echo "Error: $1 is not a readable file."
fi

This snippet tries to cat the file passed to it as its first parameter ("$1" putting double quotes around it to allow for filenames including spaces) and spits out an error message if it failed to do so.

ELIF

elif is a construct that allows you to add conditions to the else part of an if statement. It is short for "else if" so that a long string of possible actions can be written more concisely. This makes it easier to write, easier to read, and most importantly, easier to debug.

#!/bin/bash 
OS=`uname -s`
if [ "$OS" = "FreeBSD" ]; then
    echo "This Is FreeBSD"
elif [ "$OS" = "CYGWIN_NT-5.1" ]; then
    echo "This is Cygwin"
elif [ "$OS" = "SunOS" ]; then
    echo "This is Solaris"
elif [ "$OS" = "Darwin" ]; then
    echo "This is Mac OSX"
elif [ "$OS" = "Linux" ]; then
    echo "This is Linux"
else
    echo "Failed to identify this OS"
fi

This is much, much more readable than the nested else code hell this could turn into.


case statement

case provides a much cleaner, easier-to-write, and far more readable alternative to the if/then/else construct, particularly when there are a lot of possible values to test for. With case, you list the values you want to identify and act upon, and then provide a block of code for each one.
One common place for case statements use are system startup scripts.
Syntax:

case "$VAR" in
    pattern_1)
        # Some commands here.
        ;; # Execution will stop when the double semicolon is reached 
    pattern_n)
        # Some commands here.
        ;;
esac

Example:

#!/bin/bash
OS=`uname -s`

case "$OS" in
    FreeBSD) echo "This is FreeBSD" ;;
    CYGWIN_NT-5.1) echo "This is Cygwin" ;;
    SunOS) echo "This is Solaris" ;;
    Darwin) echo "This is Mac OSX" ;;
    Linux) echo "This is Linux" ;;
    *) echo "Failed to identify this OS" ;;
esac

Although it looks like a special directive, the * is simply the most generic wildcard possible, as it will match absolutely any string. This also suggests that we are able to do more advanced pattern matching like RegEx, for example.

Note that the patterns are case sensitive.

A less well-known feature of the bash implementation of case is that you can end the statement with ;;& or ;& instead of only ;;. While ;; means that none of the other statements will be executed, if you end a statement with ;;& all subsequent cases will still be evaluated. If you end a statement with ;&, the case will be treated as having matched.

#!/bin/bash

read -p "Give me a word: " input
echo -en "That's "
case $input in
  *[[:digit:]]*) echo -en "numerical " ;;&
  *[[:lower:]]*) echo -en "lowercase " ;;&
  *[[:upper:]]*) echo -en "uppercase " ;;&
  *) echo "input." ;;
esac

$ ./case1.sh
Give me a word: Hello 123
That's numerical lowercase uppercase input.

This feature is specific to the bash shell; it is not a standard feature of the Bourne shell, so if you need to write a portable script, do not expect this to work. It will cause a syntax error message on other shells.


This post has covered the various ways of controlling conditional execution — from the simple if/then/else construct, through the different things that can be done with test, through to the more flexible case statement for matching against different sets of input.

I must admit that writing about shell programming seemed like two or three posts tops in the beginning, but there is a lot to be covered, and even this and previous posts are not even the half of everything that can be learned about topics I've written about. I really recommend Advanced Bash-Scripting Guide and Classic Shell Scripting if you want to learn shell programming in more depth. I'll write a bit about Positional parameters, exit codes and (hopefully) functions in the next one. Thanks for reading!

Posted on by:

Discussion

markdown guide
 

I really enjoy using conditions in one-liners with shell:

# if file exists, source it
[ -f $HOME/.env-local ] && source $HOME/.env-local

# if directory doesn't exist, create it
[ -d ./test ] || mkdir ./test
 

I really like exit codes
var = $(echo "$((2+2))") & export haspid=$!
echo "$var" | grep 4
if [ $? -eq 0 ] ; then
echo "has num 4 and pid was $haspid"
else
echo "bash can't add"
fi

BASH ASH or SH
Bourne Again SH
Almquist SH
or just Shell
are pretty much the standard as I see it.

I have had to use ksh and csh, just diff formatting...

Plus another vote to bring back t9word and Remove autocorrect

 

You should use exit statuses directly, not via $?. Like

if command ; then stuff ; fi

Shorter, and save from accidentally picking up the wrong exit code.

Also var = $(echo "$((2+2))") spawns two sub shells, where you only need one: var=$((2+2))

 

I make it a practice to always use echo and double quotes so that everything inside is resolved like the variables and maybe it is overkill but it is due to habit and also errors that I have seen that come up not using that way in various enviornments

And again putting the
if command;

May lead to again problems with variables and escaping of escapable characters.

Escaping and variables plus nested commands is why I do both practices,.

It's not just overkill, it's useless, and it might have some nasty side effects, too. Here's some more explanation: github.com/koalaman/shellcheck/wik...

If you're having trouble with your variables and escaping characters, you could always fun it in a sub shell and use that error code in the if construct. You're making it overly complicated, and prone to errors.

Idk why everyone has to argue shit on the internet, your own citations says that using echo has its places, and I use what works for me... Not making it complicated just making something that I know works... Just because there is something that also works doesn't make how mine worked wrong...

 

As far as I remember, case does not use regex, but globbing. Similar, but different enough to make you scratch your head when something is not working.
If you need regex, nested if with =~ is the way to go.

 

Much of this will behave unexpectedly on the C shell which is the standard shell on BSD.

 

I guess that I need to state that the most of this stuff is intended for the bash shell. I don't know many people that use csh/tcsh anyway :/

 

That wasn't clear, especially after the first part which recommended to use #!/bin/sh...

Gonna address that later, thanks for the advice! I've never used BSD nor csh before, so I'm not that familiar with differences between distros and shells, as I'm almost exclusively using bash/zsh for my job.

This table might help. The ksh (AIX/OpenBSD default shell) does not differ that much from POSIX, the csh does.

  • sh: the basics, POSIX standard
  • ash: reimplementation of sh
  • dash: port of ash
  • ksh: extensions on top of sh
  • bash: massive extensions on top of sh (mostly a superset of ksh's)
  • zsh: massive extensions on top of sh but different ones to bash

  • csh: a completely different shell to sh, contemporary in origin, mostly different syntax

  • tcsh: extensions on top of csh

  • fish: a completely different shell again, cut-down syntax and features mostly targeted at interactive users

So, yeah, it's important to specify which shell you're targeting.

Thanks for the info. I've updated the posts with shell reqs. 😊

 

These one-liners are great for early exits (like in assertions), but I wouldn't use 'em in the normal program flow. So my ping script would look like this:

#!/bin/bash

[ -z "$1" ] && echo "host required" && exit 1

ping -c 1
if [ $? -eq 0 ]; then
    echo "$1 reachable."
fi

exit 0

I think it's easier to see what part of the code belongs to error handling and what's the business logic.

 

Thanks for you post. It's very clear and usefully.
I think that here you have a bit error:

MY_SHELL="zsh"

if ["$MY_SHELL" = "zsh"]
    then
        echo "You are the zsh shell user!"
fi

You forget the space after an before [].

Sorry by my english.

 

Thanks for noticing, I'll fix it right away!