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.