MoreBash:Control flow

From Juneday education
Jump to: navigation, search

Introduction

This chapter introduces the most commonly used control flow constructs such as if, for and while.

Basic syntax examples

if and tests

The basic if-statement in Bash has the following structure:

if <command>
then
  <command>
  [<command>]*
else
  <command>
  [<command>]*
fi

The argument to if is a command (or a list of commands). The last command's exit status decides whether to enter the then-branch. The then branch can have one or more commands. The if-statement ends with fi.

Examples:

$ if ls test.sql
> then
>  echo "Found test.sql"
> fi
test.sql
Found test.sql

If we don't want the output of the test command to be shown, we can do this:

$ if ls test.sql &> /dev/null
> then
>  echo "Found test.sql"
> fi
Found test.sql

Using grep -q (quiet):

$ if grep -q Karlsson test.sql; then echo "Found Karlsson"; else echo "Didn't find Karlsson"; fi
Found Karlsson

You can write an if-statement on one line, using semicolons to separate the parts:

$ if ls test.sql &> /dev/null; then  echo "Found test.sql"; fi
Found test.sql

You can use the command [ which performs a test as if it were a boolean value. The [ command takes several arguments and the last argument must be ]. It has options to perform various comparisons and tests. For instance, -lt (less than), -gt (greater than).

If-statement with test, using [:

i=5
if [ $i -lt 10 ]
then
  echo small
else
  echo large
fi
small

Note that in the example above, [ $i -lt 10 ] is not part of the if syntax. It is an argument of type command, just like any other command. This is also why you must put spaces before and after each argument to [.

Since Bash 2.02, you should use the newer [[ keyword. It is safer and more capable. Examples:

$ myhost=$(hostname)
$ if [[ $myhost == dellasoul ]] ; then echo "I'm on dellasoul" ; else echo "I'm on some other computer"; fi
I'm on dellasoul

For arithmetic expressions and comparisons, you can use (()) in bash:

$ age=20; if ((age > 17)) ; then echo "adult"; else echo "youth"; fi
adult
$ age=17; if ((age > 17)) ; then echo "adult"; else echo "youth"; fi
youth

For-each-like for loop

Basic for-each:

for str in aa bb cc dd ee
do
  echo $str
done
aa
bb
cc
dd
ee

for num in $(seq 1 10)
do
  echo $num
done
1
2
3
4
5
6
7
8
9
10

while

Basic while-loop:

i=0
while [[ $i -lt 10 ]]
do
  echo Hello $i
  let i=i+1
done
Hello 0
Hello 1
Hello 2
Hello 3
Hello 4
Hello 5
Hello 6
Hello 7
Hello 8
Hello 9

Bonus - Alternative syntax for for and while

Did you know that you can use C-style syntax for loops? Examples:

for ((i=0;i<10;i++)){
  echo $i;
}
0
1
2
3
4
5
6
7
8
9

for ((i=0;i<10;i++))
do
  echo $i;
done
0
1
2
3
4
5
6
7
8
9

i=0
while ((i<10))
do
  echo $i
  ((i++))
done
0
1
2
3
4
5
6
7
8
9

The traditional for loop syntax in bash is actually more of a for-each loop. It typically iterates over a list of items (typically text strings):

for str in first second third fourth fifth
do
  echo $str
done
first
second
third
fourth
fifth

Even the if-statement has a C-style-ish version:

i=10
if (( i<11 ))
then
  echo hello
fi
hello

The use of double parentheses is quite convenient if you know some programming in a real programming language. You can even use the ternary operator (for artithmetic expressions):

i=4
((val = i<5 ? 1 : 0))
echo $val
1
i=10
((val = i<5 ? 1 : 0))
echo $val
0

You can use double parentheses for the decrement -- and increment ++ operator on numeric variables as well:

i=10
echo $((i--))
10
echo $((i--))
9
echo $((i--))
8

If you don't want to use the value directly, you can leave it without the dollar sign:

i=20
((i++))
echo $i
21

Of course, you can use the prefix version of the operators to get the change before the value is used:

i=1
echo $((++i))
2

Combining the above:

i=0
while ((i++ < 10))
do
  echo $i
done 
1
2
3
4
5
6
7
8
9
10

Bonus - Select loop for a simple menu system

There is a special kind of loop for presenting a menu and reading a choice from the menu from the user.

The select loop presents the user with a list of alternatives in a menu prefixed with the menu item number. It may look like this:

$ ./select.sh 
Which is your favorite color?
1) blue
2) green
3) red
4) purple
5) quit
#? 3
you like red
#? 5

The select loop is created with a header which declares a select variable and a list of menu item texts:

select color in "blue" "green" "red" "purple" "quit"
do
  ...
done

In the do ... done block, you typically use a case to investigate what option the user chose:

select color in "blue" "green" "red" "purple" "quit"
do
    case $color in
        blue)
            echo you like blue;;
        green)
            echo you like green;;
        red)
            echo you like red;;
        purple)
            echo you like purple;;
        quit)
            break;;
        *)
            echo "Error select option 1..3";;
    esac
done

Don't forget the quit option (to get out of the loop!) and the default case, to catch wrong answers.

The complete script is shown below:

$ cat select.sh
#!/bin/bash

echo Which is your favorite color?
select color in "blue" "green" "red" "purple" "quit"
do
    case $color in
        blue)
            echo you like blue;;
        green)
            echo you like green;;
        red)
            echo you like red;;
        purple)
            echo you like purple;;
        quit)
            break;;
        *)
            echo "Error select option 1..3";;
    esac
done

An even more complete version of this script with a custom prompt and a better error message:

#!/bin/bash

echo Which is your favorite color?
PS3="Your choice: "
select color in "blue" "green" "red" "purple" "quit"
do
    case $color in
        blue)
            echo you like blue;;
        green)
            echo you like green;;
        red)
            echo you like red;;
        purple)
            echo you like purple;;
        quit)
            echo "Bye."
            break;;
        *)
            echo "$REPLY wasn't an option. Please answer with a number between 1 and 5";;
    esac
done

Sample run:

$ ./select.sh
Which is your favorite color?
1) blue
2) green
3) red
4) purple
5) quit
Your choice: 6
6 wasn't an option. Please answer with a number between 1 and 5
Your choice: 2
you like green
Your choice: 5
Bye.

Eliminating IF statements

Intro to || and &&

With Bash, you don't have to use IF statements in certain situations. Remember that the IF statement in bash takes the following form:

if <command>
then
 <commands>
fi

The reason is that if the first command succeeds (has an exit status of 0), we enter the then branch.

Another way to take advantage of the exit status in bash, is to use the || and && operators.

You can use it like this:

<command> || <command-if-previous-command-fails>

<command> && <command-if-previous-command-succeeds>

For instance:

$ ./hal_open.sh pod_doors || echo "I'm sorry, Dave. I'm afraid I can't do that."
I'm sorry, Dave. I'm afraid I can't do that.

$ ./hal_open.sh a_bottle && echo "No problem, Dave."
No problem, Dave.

A more realistic example is to verify that a required command is installed and accessible:

$ which hal_open.sh &> /dev/null || echo "You need to install hal_open.sh"
You need to install hal_open.sh

$ which curl &> /dev/null && echo "You do have curl installed, so we're using that..."
You do have curl installed, so we're using that...

In the first example, which can't find the command hal_open.sh so the || makes the next command execute, a printout of an error message.

In the second example, which could indeed find the curl command, so the && makes the verification message be printed.

If you want to do more than one thing after || or &&, then you can group commands using curly braces:

verify(){
    which "$1" &> /dev/null || { missing="$missing $1"; return 1; }
    return 0;
}

The function verify uses which to verify that a required command (given as argument) is installed. If it isn't, it does two things:

  1. appends the command to a variable with a list of missing commands
  2. returns 1, indicating that the verification failed

This is an example of how to use it:

$ verify ls || echo "Can't find ls";
$ verify hal_open.sh || echo "Can't find hal_open.sh"
Can't find hal_open.sh

$ echo "$missing"
 hal_open.sh

Conditional printing

Another example for eliminating the need for IF, is to do some conditional printing. Imagine that you want a debug function which prints its argument to standard out, if a DEBUG variable is set to true, and does nothing otherwise.

#!/bin/bash
DEBUG=true

debug(){
    if $DEBUG
    then
        echo "$1"
    fi
}

debug "Here's a message..."
DEBUG=false
debug "Whatabout this?"

When run, the script prints only the first message.

Instead of the IF statement, we could do this:

debug(){
    $DEBUG && echo "$1"
}

But, now we don't even need the debug function! We can simply do this:

$DEBUG && echo "This will print if DEBUG is true"

The syntax:

if $DEBUG
then
  some-command
fi

works, because true and false are commands. Remember, the syntax for the IF statement in Bash is:

if <command>
then
  <commands>
fi

Function for asking a question

Here's another example. Let's say we have a function ask_question() which returns 0 if the user answers yes to a question and something other than 0 if the user doesn't answer yes:

ask_question()
{
    Q=$1
    
    RET=1

    echo "$Q [Y/n]"
    read ANSWER
    if [ "$ANSWER" = "Y" ] || [ "$ANSWER" = "y" ]
    then
        RET=0
    fi
    return $RET
}

The function is used like this in code:

ask_question   "Are you over 18?"
if [[ $? == 0 ]]
then
    echo "You can vote."
else
    echo "You cannot vote"
fi

Or, now that we know how to eliminate IF statements, perhaps:

ask_question "are you over 18?" && echo you can vote || echo "you cannot vote"

Let's fix the ask_question() function in a few ways. First, let's make it accept anything starting with a Y or y, or even accepting an empty string (the user hits RETURN since we ask [Y/n] which is the idiom for showing that Y is the default answer). But we'll also change the function so that it doesn't need the IF statements:

ask_question()
{
    echo "$Q [Y/n]"
    read ANSWER
    return $([[ $ANSWER == @(Y*|y*|"") ]])
}

Explanation:

The test expression [[ $ANSWER == @(Y*|y*|"") ]] checks if the variable ANSWER matches any of the glob expressions Y* or y* or "" (empty string).

Now you can answer any of the following (and more) as "yes":

  • Y
  • y
  • Yes
  • yes
  • etc

If you accept the default "Y", you can also just hit enter. That is the meaning of [Y/n] - if you accept "Y" for an answer, just hit ENTER.

The script evaluates the test expression using command substitution, $(some_command) and returns the result. There is no need for the RET variable, since the evaluation of the test expression will be 0 if the test succeeds, and something else if it doesn't.

Here's a small script you can test yourself:

#!/bin/bash

ask_question()
{
    Q=$1
    echo "$Q [Y/n]"
    read ANSWER
    return $([[ $ANSWER == @(Y*|y*|"") ]])
}

if ask_question "Are you sure?"
then
    ask_question "are you really sure?" &&
        echo "OK, let's go." ||
            echo "OK, bye"
else
    echo "Ok, thanks anyway"
fi

ask_question "are you over 18?" && echo you can vote || echo "you cannot vote"

Note: If the test doesn't work in your bash, you can add the following to the first line of the script: shopt -s extglob

An alternative to the extended globbing expression @(Y*|y*|"") is to use a switch-case:

    Q=$1
    echo "$Q [Y/n]"
    read ANSWER
    return $(case $ANSWER in Y*|y*|"") echo 0;; *) echo 1;; esac)

You can format the code for better readability:

    return $(case $ANSWER in
                 Y*|y*|"")
                     echo 0
                     ;;
                 *)
                     echo 1
                     ;;
             esac)

Videos

Links

Source code

External links (further reading)

Chapter links

previous (Output and return) | next (Exercises Control flow)