1. Overview

In most programming and scripting languages, we have flow control statements that allow us to move execution to somewhere else in the script. Often, these include the goto statement.

Bash does not support goto but provides a few other mechanisms for flow control.

In this tutorial, we’ll look at how to simulate goto in Bash, as well as how to use the built-in flow control capabilities to jump over code, jump out of loops, or execute alternative actions.

2. Jumping Over Code in a Bash Script

Let’s start by simulating the jumping behavior of goto.

2.1. Basic Syntax of goto

A goto statement usually transfers control to the line identified by a label:

    statement1

    goto labe11
    statement2 #skipped
    #... more lines skipped
    statement9 #skipped

label1:
    statement10  #executed

2.2. Reading Multiple Lines From the Bash Script

While we cannot do this natively in Bash, the operator <<HERE redirects lines of the script to stdin until the string HERE is encountered. This is often called ‘heredoc‘.

2.3. Illustrative Example

Let’s try this out by using cat <<label to simulate a goto:

#!/bin/bash

echo "Starting the script"
cat >/dev/null <<LABEL_1
echo "This command is not run"
LABEL_1
echo "And this command is run"
cat >/dev/null <<LABEL_EXIT
echo "This command is not run as the first one"
LABEL_EXIT
echo "The script is done"

Next, let’s run it and take a look at the output:

./cat_goto_example
Starting the script

And this command is run
The script is done

As we can see, the echo statement between the cat and the LABEL_1 was not executed.

2.4. What Happens to the Code in cat

Let’s see what cat is doing. So, let’s omit the /dev/null redirection:

#!/bin/bash

echo "Starting the script"
cat <<LABEL_1
echo "This command is not run"
LABEL_1
echo "And this command is run"
cat <<LABEL_EXIT
echo "This command is not run as the first one"
LABEL_EXIT
echo "The script is done"

Now, we can see the cat output appears on our console:

Starting the script

echo "This command is not run"

And this command is run

echo "This command is not run as the first one"
The script is done

So, the cat and heredoc approach has allowed us to read but not execute the lines in the script. We should note that this workaround to simulate goto only works going forwards through the script.

3. Conditionally Jumping Over Code in a Bash Script

Most commonly, we’d use a goto in a script in conjunction with some condition, perhaps jumping over steps we did not want to perform.

3.1. Creating a Conditional goto with cat <<HERE

We’re trying to achieve logic similar to this pseudocode:

    if condition 
    goto label1
        
    statement1 #skipped if condition is met
    # ...
label1
    statement10 #always executed

With the above technique, we can achieve this using a little extra trickery:

if [ -n "$GOTO_CONDITION" ] ; then
    cat >/dev/null
else
    eval "$(cat -)"
fi <<'LABEL'

Here, we’re using both branches of the if statement to create a cat expression that does what we need. The part that executes when GOTO_CONDITION is true gets rid of the code lines down to label1 as before.

On the contrary, the ‘else‘ part reconstructs the script from the lines provided by the << operator and executes it as a whole.

In detail, the “$(cat -)” command concatenates the lines of scripts and presents the result as a bash variable to eval. As a result, we avoid executing each line of the script separately from each other.

Furthermore, we should use the heredoc construction with the label name put into quotation marks to avoid variable expansion.

3.2. Example of Conditional goto

Let’s create a script to use our construction:

#!/bin/bash

n=2
echo "Starting the script with n = $n"
foo="FOO"

if [ $n -eq 2 ] ; then
    cat >/dev/null
else
    eval "$(cat -)"
fi <<'LABEL_1'

echo "This is the foo variable: $foo"
echo "This command is not run when jumped over with goto"
bar="BAR"
echo "This is the bar variable: $bar"
LABEL_1
echo "After LABEL_1"
echo "This is the foo variable: $foo"
echo "This is the bar variable: $bar"
echo "And this command is run"
echo "The script is done"

We check the arithmetic expression to emphasize that the condition of goto may be evaluated during the execution of the script. Moreover, we use two variables, foo, and bar, to study their visibility.

3.3. Testing the Conditional goto

Let’s check the result when goto is active – with n=2:

Starting the script with n = 2
After LABEL_1
This is the foo variable: FOO
This is the bar variable: 
And this command is run
The script is done

This time, we jumped over the code. In addition, the variable foo is defined while bar is not.

Now, let’s start our script when goto is not performed, with n = 10:

Starting the script with n = 10
This is the foo variable: FOO
This command is not run when jumped over with goto
This is the bar variable: BAR
After LABEL_1
This is the foo variable: FOO
This is the bar variable: BAR
And this command is run
The script is done

Here, we find that all the script was executed. Furthermore, both variables are properly defined and visible.

Finally, the correct use of local variables, foo, and bar, proves that the script was executed as one integral whole.

4. Bash Alternatives for Enabling or Disabling a Part of Code

During developing or debugging a script, we often need to skip some lines in the code. We don’t need to use our goto technique for this, as there are some common alternatives.

4.1. Commenting Out

By far, the simplest solution for ignoring some code is to comment it out.

Bash uses the hash sign ‘#’ to ignore all characters from it to the end of the line:

#!/bin/bash

echo "This shows up in terminal"
#echo "but this not"

Unfortunately, there is no block comment available in bash.

4.2. Using the false Function in the if Statement

We can use false in an if statement:

#!/bin/bash

if false
then
    echo "Disabled code here"
fi

echo "Not disabled code here"

In order to re-enable the code, we can replace the false with true.

4.3. Setting a Bash Variable

Alternatively, we could use an environment variable or a Bash variable that takes its value from a command-line argument to make some code conditions.

Let’s imagine there’s a DEBUG environment variable:

#!/bin/bash

if [ -n "$DEBUG" ]
then
    echo "Disabled code here"
fi

echo "Not disabled code here"    

The -n switch tests if the variable isn’t an empty string. Besides, DEBUG may be set to any value.

The advantage of this version is that we don’t need to modify the script to change its behavior.

Instead, we should only set and export the variable:

export DEBUG=true
./debug_test

#result:
Disabled code here
Not disabled code here

or unset it before starting the script:

unset DEBUG
./debug_test

#result:
Not disabled code here

5. Jumping Around in Multi-Level Loops

Most programming languages deliver break and continue statements, but often their effect reaches only the directly enclosing loop**.**

In contrast, Bash provides break n and continue n.

5.1. Breaking Multi-Level Loops

The break n skips n levels of loops.
Let’s try jumping out of a nested loop, effectively stopping all iterations, when a condition is met:

#!/bin/bash

for x in {1..10}
do
    echo "x = $x"
    for y in {1..5}
    do
        echo "    y = $y"
    if [ $x -ge 2 ] && [ $y -eq 3 ]
    then
        echo "Breaking"
        break 2
    fi    
    done
done
echo "Now all loops are done"

Let’s examine the result:

x = 1
    y = 1
    y = 2
    y = 3
    y = 4
    y = 5
x = 2
    y = 1
    y = 2
    y = 3
Breaking
Now all loops are done

We should note that once the condition [ $x -ge 2 ] && [ $y -eq 3 ] is fulfilled, both loops terminate and the control passes to the first statement after them.

5.2. Continuing Multi-Level Loops

The continue n command omits operations within the loop and allows jumping over n levels of nested loops.

Not only is the rest of the current loop omitted, but also bodies of all enclosing loops, through n levels:

#!/bin/bash

for x in {1..5}
do
    for y in {1..3}
    do
        if [ $x -gt 2 ] && [ $y -eq 2 ]
    then
        echo "Continue"
        continue 2
    fi
    echo "    y = $y"    
    done
    echo "x = $x"    
done
echo "Now all loops are done"

Now, let’s examine the result:

    y = 1
    y = 2
    y = 3
x = 1
    y = 1
    y = 2
    y = 3
x = 2
    y = 1
Continue
    y = 1
Continue
    y = 1
Continue
Now all loops are done

Once the loop variables meet the condition [ $x -gt 2 ] && [ $y -eq 2 ], no operation after the continue statement is performed.

Moreover, it concerns also the outer loop with variable x.

6. Error Handling

Error handling is another concern that we can address by flow control.

6.1. The Model Control Flow

This pseudocode illustrates how we might take some action specific to an error, or resume the program:

    if ( !functionA() ) goto errorA
    if ( !functionB() ) goto errorB
    #...

    exit(0) # everything is OK

errorA:
    concludeA() #some specific A fallback
    exit(codeA)
errorB:
    concludeB() #some specific B fallback
    exit(codeB)

6.2. The Case Study

Let’s write a script that changes the directory to a given one and subsequently writes a new line to a file inside this directory.

Both these operations may issue their own error – for example, when the target folder does not exist or the user doesn’t have permission to write to the file.

6.3. The some_action || handle_error Construct

In Bash, the statement separator|| causes the right command to execute only if the left one fails.

Consequently, let’s assume that the some_action part is an error-prone code and handle_error is a suitable fallback function:

#!/bin/bash

target_folder="test"
target_file="foo"

cd "$target_folder" || { echo "Can not cd to $target_folder" && exit 1; }

echo -n > "$target_file" || { echo "Can not write to file $target_file in $target_folder" && exit 1; }

echo "Successfully wrote new line to file $target_file in $target_folder"

6.4. Testing the Script

Let’s suppose that, at the very beginning, the ‘test’ folder does not exist:

./error_example
./error_example: line 6: cd: test: No such file or directory
Can not cd to test

Now, let’s create the folder:

mkdir test #create a folder
./error_example Successfully wrote new line to file foo in test

And finally, take away the write permission from the user:

chmod u-w test/foo #take away the write permission from the user
./error_example
./error_example: line 8: foo: Permission denied
Can not write to file foo in test

Let’s catch that we obtain not only the regular Bash error messages but also our notifications as expected.

6.5. The Script Highlights

Let’s notice the important features of the script error_example:

  • We should put error handling commands into a block.
  • We should use curly braces {} on the error handling side to exit the script on an error.
  • In the righthand part, we use another separator && to force the execution of both commands, echo, and exit.

7. Conclusion

In this article, we learned how to use Bash’s capabilities to change the flow of control.

First, we imitated a forward goto by means of the cat <<HERE construction. Then, we looked through other ways to bypass a part of the script.

We moved onto Bash’s break and continue to handle multi-level nested loops. Finally, we learned how to chain bash commands with help of the || and && operators.