Bash 05 – Script Logic

Jarret B

Well-Known Member
Staff member
Joined
May 22, 2017
Messages
340
Reaction score
367
Credits
11,754
Probably the most important aspect of scripting is using logic operators to control the flow of the script. Sometimes a script needs to do more than run each command in order from the beginning to the end of the script.

There are many times when you need to control the flow of a script to allow different things to happen based on the parameters that you specify. For example, you may need to verify that a file or folder exists before you copy certain files into the folder.

We have basically four logic features that we need to cover in this article. There are more logic features we will cover later, but these features will aid you in script flow.

Chaining

Chaining is the ability to place multiple commands on a single line. Different connectors connect the multiple commands to evaluate how the multiple commands work together. There are four connectors used are:

  1. ;
  2. &
  3. &&
  4. ||

The semi-colon (;) is used to chain commands to run sequentially. This means that when command 1 has completed, then command 2 starts and so on. One command or program at a time.

For example, in a script, you can issue the command to start a program. Once the program completes, you may want temporary files created by the program to be deleted. The command would be:

Code:
program_name ; rm /tmp/program_folder/*

Once the program successfully completes, the files are deleted.

Instead of sequentially, you can run things asynchronously. This means that all commands run nearly at the same time. If we have three commands, the first command starts, then the second, and finally the third. Each command may complete at different times, but it will execute all three. For example, if you needed three folders deleted, you could issue a command to delete the first folder. When the first folder is deleted, you can issue the second command to delete the second folder, and so on for the third folder. By running asynchronously, you issue all three commands at once and the folders are deleted at the same time. An ampersand separates each command (&). For example, to delete folder1, folder2 and folder5 asynchronously from the HOME folder, the command would be:

Code:
rmdir folder1 & rmdir folder2 & rmdir folder5

Running the command on my system results in the following:

Code:
jarret@tuned:~$ rmdir folder1 & rmdir folder2 & rmdir folder5
[1] 4255
[2] 4256
[1]- Done rmdir folder1
[2]+ Done rmdir folder2

Command 1 and 2 are placed in the background, as shown by the lines ‘[1] 4255’ and ‘[2] 4256’. Command 3 is completed, even though it is not shown. Then command 1 and 2 are completed, as shown by the last two lines.

The next Chaining Command is the double ampersand (&&). The double ampersand is used to separate commands where you need the second command to run after the first command has completed successfully. Most Linux users will be familiar with this chaining command with the command:

Code:
sudo apt update && sudo apt upgrade -y

The first command will download package information for the system. If the package information downloads successfully, then you want the system to run the second command to download the updates on currently installed packages. There is no need to check the packages for upgrades if the system does not have an updated package list. Notice that the commands are sequentially executed.

The last chaining command is also run sequentially. It is like the double ampersand (&&), it is the double pipe (||). The only difference is that it only executes the second command if the first command fails. A basic example is to run a command, then the second command would be an echo statement with an error message, such as:

Code:
rmdir $HOME/folder1 || echo The folder does not exist or cannot be removed!

These are some very basic examples, but I hope they get the point across. Everyone will have varying needs for chaining commands. Keep in mind that they can be used together and in more than two commands.

One last example, we can issue a command an have another run if the first succeeds. A third command can be executed if the first fails. This is another basic example, but it should make sense:

Code:
sudo apt update && sudo apt upgrade -y || echo Package information cannot be updated!

The first two commands we covered previously. The last command with double pipe allows a third command to be run of the first one fails. You may want to think that the third command is executed only if the second fails. Keep this in mind: that the whole command is based only on the success or failure of the first command.

Test Commands

Sometimes you need to test items and make comparisons. If we are making numeric comparisons, we can use the following:

  • -eq - equal to
  • -ne - not equal to
  • -gt - greater than
  • -lt - less than
  • -geq - greater than or equal to
  • -leq - less than or equal to

Test commands are placed in brackets ([]) with a required space after the first bracket and a space before the last bracket. The values returned by a test command are a ‘0’ if the test is true and a ‘1’ if the test if false. Let’s look at a simple test:

Code:
[1 -eq 1]; echo $?

The return value will be a ‘0’ sine 1 does equal 1.

If you prompt a user for a value, which they entered, and the value is placed in the variable ‘a’, we could test it:

Code:
[ $a -eq 5 ];

If ‘a’ has a value of 5, then the result is ‘0’.

NOTE: The test operatives (-eq, -ne, etc.) only work with numeric values.

If we wish to do string comparisons, we use the following test operatives:

  • = - equal to
  • != - not equal to
  • > - greater than
  • < - less than
  • -z - string is empty (null)
  • -n - string is not empty (null)

If we had the following script portion, the output would be ‘1’:

Code:
a=A
b=a
[ $a = $b ]; echo $?

Here, we are comparing the contents of the variables ‘a’ and ‘b’. The values of ‘A’ and ‘a’ are different and not equal, so the return value is false or a ‘1’.

If we want to test the operatives ‘-z’ or ‘-n’, it works like the following:

Code:
a=“”
b=“not empty”
[ -z $a ]
[ -n $b ]

Both tests will return a ‘0’. The first one is testing the variable ‘a’ to see if it is empty. The second command tests if the variable ‘b’ is not empty. In both cases, the result is true and returns a value of ‘0’.

The next type of test is file and folder tests. The following are the operatives:

  • -e - file exists
  • -d - directory exists
  • -r - file allows for read
  • -w - file allows for writing
  • -x - file is executable
  • -s - file is larger than 0 bytes (not empty)
  • -nt - first file is newer than second
  • -ot - first file is older than second
To test a file if it exists, the command would be:

Code:
[ -e file1 ]; echo $?

If the file exists, the result is ‘0’. If not, it is a ‘1’.

Try the command ‘touch file1’. Make sure that ‘file1’ does not exist first. Now try the script:

Code:
#!/bin/bash
[ -x "file1" ]; echo Execute: $?
[ -r "file1" ]; echo Read: $?
[ -w "file1" ]; echo Write: $?

The result is the following:

Execute: 1
Read: 0
Write: 0


Initially, a file you create will have read and write permissions for your user account. If you perform ‘chmod 744 file1’ then the result is ‘0’ for all tests since it is now executable as well.

For the age comparison, you list two files to compare. Such as:

Code:
[ file1 -nt file2 ];

Try a few of these scripts to see how the test commands work. The file tests can be very handy at times.

If Statements

With the ‘if’ statement, we can provide more logic for the script. We can move script flow depending on specific information.

The ‘if’ statement basically works as an ‘if this is true then do that, otherwise do something else’. Of course we can expand it more, but that is the basics of it. Let’s look at a simple example.

We want to check the HOME folder and see if a folder exists called ‘folder1’. If the folder exists, then we can move on. If the folder does not exist, then we need to create the folder. The script is as follows:

Code:
#!/bin/bash
if [ -d $HOME/folder1 ]; then
  echo Folder exists!
else
  mkdir $HOME/folder1
  echo Folder created!
fi

The first time you run it, the folder is created and you receive the message ‘Folder created!’. Running the script a second time will give the message ‘Folder exists!’.

The way the statement works, we use the reserved word ‘if’ and follow that with a test of some type. There can be multiple tests. A semicolon (;) follows the last test and the word ‘then’. For best viewing, place all the statements to be performed and the following lines. Indent each line with a tab.

In the script example, there are only two outcomes to my test. The folder exists, or it doesn’t. I use the reserved word ‘else’ statement to separate the other statements for when the test is ‘false’. In this case, the folder is created.

To end the ‘if’ statement, use the reserved word ‘fi’ (‘if’ backwards).

Another reserved word that can be used is ‘elif’. This stands for ‘else if’ for when there may be more than two outcomes and more tests need to be run.

NOTE: The ‘if’ statements work top to bottom. If a statement passes the test, its statements are executed. Once a test is true, no other tests are performed for the ‘if’ statement.

Let’s say we ask a user to input a number 1 through 3. Here we will have three possibilities: 1, 2 or 3. We need to keep in mind that the user may enter an incorrect value, so there will be four possibilities we must deal with for this script.

Code:
#!/bin/bash
read -p “Enter a number 1-3: ” number
if [ $number -eq 1 ]; then
  echo You pressed 1!
elif [ $number -eq 2 ]; then
  echo You pressed 2!
elif [ $number -eq 3 ]; then
  echo You pressed 3!
else
  echo You entered an invalid choice!
fi

In this example, it placed the value entered by the user into the variable called ‘number’. The first ‘if’ statement checks if the number is equal to ‘1’. If it is, it processes the statement to echo ‘You pressed 1!’. The first ‘elif’ statement checks if the value of the number is ‘2’. If the result is ‘true’, then the following echo statement is executed. The next ‘elif’ statement compares the number to ‘3’ and, if true, the statement following the line is performed.

Now that we have checked over all the desired possibilities, we now need to add an ‘else’ statement to ‘catch’ any entries for ‘number’ that are not taken care of in the previous lines. Be aware to always use a fail-safe for when the test may fail in the ‘if’ and ‘elif’ statements. So, if any value other that ‘1’. ‘2’ or ‘3’ is entered, the user will get the message ‘You entered an invalid choice!’.

Statements can exist before and after the ‘if’ and ‘fi’ reserved words.

Let’s look at expanding the ‘if’ statement to test multiple items. Let’s check for the existence of a folder and a file within the folder. If the folder doesn’t exist, we can create it. If the folder exists, we need to check if the file exists. If the file does not exist, we can create the file. If both the folder and file exist, we do not need to do anything.

Code:
#!/bin/bash
if [ ! -d $HOME/folder1 ]; then
  mkdir $HOME/folder1
  touch $HOME/folder1/file1
elif [-d $HOME/folder1 ] && [ ! -e $HOME/folder1/file1 ]; then
  touch $HOME/folder1/file1
else
  echo Folder and file exists!
fi

The first ‘if’ statement checks if the folder exists. If not, we know the folder and file need to be created. The first statement ‘mkdir $HOME/folder1’ creates the folder. The next statement, ‘touch $HOME/folder1/file1’, creates the file.

The first ‘elif’ statement checks that the folder exists and that the file does not exist (! -e). An exclamation mark represents ‘NOT’. The double ampersand (&&) stands for ‘AND’. So, both statements must be true for the statement ‘touch $HOME/folder1/file1’ to be executed.

NOTE: If you want to use an ‘OR’ within the test, it is represented by the double pipe (||). This means one or both of the tests must be true. It only fails if both tests are false.

The last ‘else’ statement catches what remains, which is that the folder and file exist. Nothing needs to happen except maybe a message that both the folder and file exist.

Case Statements

The ‘Case’ statement allows you to perform something similar to the ‘If’ statement, but with multiple choices.

The structure starts with the word ‘case “$<variable>” in’. The variable is the container for the value that we will compare to multiple choices. For instance, if the value will be matched against a list of names or numbers. The next line will be the first value in the list to that we are comparing our variable followed by a closed parenthesis. The next line starts the statements that are executed if the match is true. Each statement line must end with two semicolons (;) Then another possible match value followed by a closed parenthesis, then the statements to execute. At the end is an asterisk (*) followed by a closed parenthesis and statements to run if no other match has occurred. The last line in the ‘Case’ Statetment is ‘esac’, or ‘case’ backwards. Let’s try a basic example. If we enter a value rolled on a dice (1-6):

Code:
#!/bin/bash
read -p “Enter the die value (1-6): ” number
case “$number” in
1)
echo You rolled a one!;;
2)
echo You rolled a two!;;
3)
echo You rolled a three!;;
4)
echo You rolled a four!;;
5)
echo You rolled a five!;;
6)
echo You rolled a six!;;
*)
echo That input doesn’t work!;;
esac

NOTE: If you copy and paste the example into an editor, change the curved quote marks to straight quote marks.

The example should be straightforward. Each choice has a single selection of 1 through 6. The last choice (*) allows for an entry that is invalid. Let’s look at the example again, but a little changed.

Code:
#!/bin/bash
read -p “Enter the die value (1-6): ” number
case “$number” in
1 | 3 | 5)
echo You rolled an odd number!;;
2 | 4 | 6)
echo You rolled an even number!;;
*)
echo That isn’t on a single die!;;
esac

Here, we compare the input to three values. Each value is separated by a pipe to represent ‘or’. The first case is either ‘1’ or ‘3’ or ‘5’. The second is either ‘2’ or ‘4’ or ‘6’. The last case statement is for anything that was an invalid entry.

The selection does not need to be numeric, you can use strings.

Conclusion

The logic operations can help improve a bash script to handle different situations and input as you need.

Understanding the logic operators and using them can make your scripts more powerful to automate tasks or handle user input in various ways so that the script can cause different things to occur based on circumstances. It may seem that your script is ‘thinking’ since the output will vary on the input.

I hope you are learning bash. We have about 4-5 articles to go to finish learning bash.
 


So if I understand correctly, ; and & basically serve the same purpose, sequential execution?
 
So if I understand correctly, ; and & basically serve the same purpose, sequential execution?

No, not quite.
& will run a command in the background, as a background job.

So, in Jarret’s example using the & operator, each of the rmdir operations are started more or less simultaneously as background jobs. So they are started sequentially, but will complete asynchronously, not sequentially. There is no guarantee in which order they’ll complete.

; separates commands that are to be ran sequentially. So rather than putting each command on a new line, you can put several commands on a single line and separate each of them using a ;.

The semicolon ; operator is extremely useful in creating bash one liners.
If you’re writing a script, typically you’ll put each command on a separate line instead of using ;. But for one liners, you need to use ; to separate different commands on the same line. One liners typically use a mixture of piped commands (using | to chain the output of one command to the input of another.) and commands separated by ;.

&& separates commands that are sequential, but the command to the right of the && only runs if the command to the left of it completes successfully. So with && the commands are sequential, but also conditional.

So to sum up:
& is asynchronous.
Commands separated by & are started sequentially, but run simultaneously as background processes.
The order of completion of commands separated by & is not guaranteed.

; is sequential and unconditional.
Anything after the ; will be ran after the previous command has finished, regardless of whether or not it completed successfully.

&& is sequential and conditional.
Anything after the && will only run if the command that comes before it completes successfully.
 
No, not quite.
& will run a command in the background, as a background job.

So, in Jarret’s example using the & operator, each of the rmdir operations are started more or less simultaneously as background jobs. So they are started sequentially, but will complete asynchronously, not sequentially. There is no guarantee in which order they’ll complete.

; separates commands that are to be ran sequentially. So rather than putting each command on a new line, you can put several commands on a single line and separate each of them using a ;.

The semicolon ; operator is extremely useful in creating bash one liners.
If you’re writing a script, typically you’ll put each command on a separate line instead of using ;. But for one liners, you need to use ; to separate different commands on the same line. One liners typically use a mixture of piped commands (using | to chain the output of one command to the input of another.) and commands separated by ;.

&& separates commands that are sequential, but the command to the right of the && only runs if the command to the left of it completes successfully. So with && the commands are sequential, but also conditional.

So to sum up:
& is asynchronous.
Commands separated by & are started sequentially, but run simultaneously as background processes.
The order of completion of commands separated by & is not guaranteed.

; is sequential and unconditional.
Anything after the ; will be ran after the previous command has finished, regardless of whether or not it completed successfully.

&& is sequential and conditional.
Anything after the && will only run if the command that comes before it completes successfully.
Referencing the post again, that pretty much sums it up: ; just signifies the end of a statement (it's pretty much universal for all programming languages...), & does the same thing but it's unclear how it executes (just not at the same time) and puts things in the background, && says "don't run this until the first command was completed", || means "or", "if the first command executes successfully, do not run the next one".
 
The 'or' using the double-pipe '||' runs the second command if the first one fails. It is a sort of backup command or a cleanup command to handle the first command failing.
 

Members online


Top