Page Body

Shell Scripting With Bash

Bash is more than a shell. It can be used to program your GNU/Linux system, from creating simple scripts that perform a series of commands, to more complicated command line applications with text-based user interfaces (TUIs).

Learning how to utilize Bash programming will greatly enhance the utility of your GNU/Linux system.

Note: If you are not familiar with the GNU/Linux command line interface, review the Conventions page before proceeding.

Shell Scripts

Shell scripts have certain advantages and disadvantages compared to programs created with compiled languages, like C.

Generally, shell scripts are:

  • Quicker to develop
  • Independent of computer architecture
  • Shorter

Usually, shell programming is easier to learn than programming in languages like C, as well.

Disadvantages of shell scripts are that they:

  • Are less efficient than compiled programs
  • Have a limited number of available data structures
  • Do not lend themselves to the implementation of software that needs to fulfill strong security requirements

Also, shell scripts have important differences compared to shell aliases and functions:

  • They are not kept in memory by the shell (i.e., they are read from files as they are needed).
  • They can be invoked in myriad ways. For example, shell scripts can be passed as arguments to an interpreter (e.g., bash ex_script.bash) or directly invoked as a command/program (e.g., ./ex_script.bash, or ex_script.bash if the script is available via the PATH shell variable), where the interpreter denoted in the script's shebang interprets it.

    Shell scripts invoked in these ways appear to behave as ordinary executables, so they can be used by any program.

  • They are run in a separate process with their own environment, which cannot affect the invoking shell's environment.

  • They can be read and executed by the current shell using the . and source built-in commands.

Shells, Child Shells, and Subshells

Shell scripts are child shells (i.e., child processes) that are run in non-interactive shells. Like all child processes, they inherit a copy of their parent's environment (i.e., copies of their parent's environment variables).

A subshell is a copy of a parent shell. It has access to all of the available resources that the parent shell has access to (i.e., shell variables, aliases, functions).

Subshells are created when you:

  • Execute a command in the background with an ampersand (&).
  • Create a pipeline with a pipe (|). The pipe creates two subshells, one for the left side of the pipe and one for the right side of the pipe, and waits for both sides to terminate. If you set Bash's lastpipe option, the right side of the pipe will run in the original shell (i.e., the pipeline will create only one subshell).
  • Use process substitution (<(), >()). <() creates a subshell with its standard output set to a pipe and expands to the name of the pipe. The parent (or another process) may open the pipe to communicate with the subshell. >() accomplishes the same, but with the pipe on the standard input.
  • Create a coprocess with the coproc internal command. coproc creates a subshell and does not wait for it to terminate. The subshell's standard input and standard output are set to a pipe with the parent being connected to the other end of each pipe.
  • Use the -C ex_command option with the internal complete command.
  • Use command substitution ($()).

The shell variables SHLVL and BASH_SUBSHELL can be used to demonstrate the distinction between child shells and subshells. SHLVL starts at 1 and is incremented each time you move into a child shell. BASH_SUBSHELL starts at 0 and is incremented each time you move into a subshell.

$ echo "${SHLVL}"
1
$ echo "${BASH_SUBSHELL}"
0
$ bash
$ echo "${SHLVL}"
2
$ echo "${BASH_SUBSHELL}"
0
$ exit
exit
$ echo "${SHLVL}"
1
$ echo "${BASH_SUBSHELL}"
0
$ echo "$(echo "${SHLVL}")"
1
$ echo "$(echo "${BASH_SUBSHELL}")"
1
$ 

Keep in mind, shell internal commands (e.g., alias, cd, echo) are not run as separate child processes, as they are part of the shell itself. Therefore, they have access to all of the resources of the shell they are run from.

Best Practices

A common approach is to first implement a program as a shell script and later replace performance-critical parts by C programs, or programs in more full-featured languages. It may be wise to restrict your scripts to the functionality mandated by POSIX, rather than all of the convenient extensions that may be available for your system's shell.

Many of the same command line interface (CLI) behaviors hold for shell scripts. For example, line breaks in shell scripts serve as command separators and you can increase the readability of your scripts through the judicious use of blank lines.

Scripts should start with a comment block containing useful information, e.g., the name of the script, its purpose, how it works, and how to invoke it (this is commonly referred to as the script's header or front matter). Also, the author's name and a version history can be placed here.

When writing your shell scripts, write all of the commands to a file that you would have entered on the command line and connect them in a sensible way. Put the names of frequently-used files in variables and reference them when you need them.

Command line parameters can be inspected and used to insert elements that change between invocations. If necessary, you should check the completeness and plausibility of these parameters, and output applicable warnings or error messages.

When creating a script, it often helps to begin with a rough outline containing all of the probable steps in the script (i.e., pseudocode). This makes it easier to determine if your conception of the script contains logical errors.

Commenting every single command should not be necessary. It is preferable to provide a higher-level view. Concentrate on the data flow and format of the script's input and output. Creating external documentation (e.g., manual pages) for larger programs may be warranted.

A good script has structure (e.g., good use of indentation). Commands on the same logical level should have the same distance from the left edge of the screen (or window). You can use tabs or space characters to indent, but you should try to be consistent.

Try to keep your script lines from becoming too long, which may make your code less legible. A long line can be broken up with the backslash character (\):

ex_code\
    ex_code_continues

Above, the \ character escapes the invisible line return character at the end of the line. You do not need to escape the line return character after you break a line following the && or || logical operators, or after a pipe (|). There, line continuation is implicit.

Errors

There are two kinds of script errors:

  1. Conceptual Errors These are mistakes concerning the logical structure of the program. It can be costly to recognize and repair these errors. They are best avoided by careful planning.
  2. Syntax Errors These errors frequently occur. Many syntax errors can be avoided if you proceed from the simple to the specialized when writing the script. Since there is no compilation (i.e., no checking of your program), you should systematically test your script to ensure that, if possible, all script lines (e.g., all branches of conditional code) are taken. If a script goes to hundreds or thousands of lines, consider a script language with better syntax, like Python.

Shellcheck

A linter is a software tool that analyzes your code. Linters are useful for identifying potential errors and for ensuring that you adhere to best practices. For shell scripts, ShellCheck is one of these tools. Before you start testing your scripts, try running them through shellcheck first.

By default, ShellCheck may not be installed on your GNU/Linux distribution, but it is likely available in your distribution's repository (e.g., # apt install shellcheck, # dnf install shellcheck). Also, there is an online version.

Testing

During development, try to test your scripts as frequently as possible. Try using a testing environment, especially if the script changes existing files.

The set command is useful for examining the shell's command execution. With set -o xtrace (set -x), you can see the steps that the shell takes to do its job:

$ set -o xtrace
$ echo "My ${HOME} is my castle."
+ echo 'My /home/enterprise is my castle.'
My /home/enterprise is my castle.

This is called tracing. The disadvantage is that the command is still executed. In the case of substitutions, it may be better if the commands are just displayed, instead of being display and executed. You can do this with set's noexec option (set -o noexec, set -n), but it only works for shell scripts, not interactive shells.

Another useful set option is verbose (set -o verbose, set -v), which executes the commands and prints all input lines as they are read. For a shell script, you will obtain not just its output, but also everything contained within it (e.g., if you are providing a running script with input to operate on, you will see all the input lines as they are read in).

When developing scripts, add any desired options for the CLI to the script's shebang (e.g., #!/bin/bash -x). Here, the -x option for bash prints commands and their arguments as they are executed, which enables you to run a Bash script in debug mode. If you only want to debug specific parts of a Bash script, bracket the parts of the script in question with set -o xtrace and set +o xtrace, instead.

Before executing a command involving potentially problematic substitutions, put an echo command in front of it. This causes the whole command (including all of its expansions and substitutions) to be output without being executed. This is adequate for a quick test to see if the substitution resolves to the desired value.

Shell Script Format

Essentially, a shell script is a text file, but there are conventions that either must be followed or are considered best practice. For example, the first line in a Bash script needs to be the shebang.

This is an example shebang for a Bash script:

#!/usr/bin/bash

This line specifies the absolute path of the program that should be used to interpret the script (in this case, the absolute path for bash). The program executing the script does not need to be a shell, as every binary executable is eligible for inclusion (e.g., you can write awk scripts). On GNU/Linux, the shebang line may be at most 127 bytes long and may also contain parameters in addition to the program name (e.g., #!/bin/bash -x).

In the example above, we are writing a Bash script, so we specify the absolute path to the Bash shell. On Debian, this is /usr/bin/bash. However, not every system may have Bash installed at the same location, or a person may want their shell to use a specific version of Bash that is installed at a different location. To increase the portability of our Bash scripts, we can use the following shebang instead:

#!/usr/bin/env bash

This line uses the env command to have the shell use the PATH variable to determine where Bash is located in the system's environment.

After the shebang line, any text entered after a sharp (#) is considered a comment (i.e., not executable code). It is prudent to add header lines as comments, which include information like the script's author and purpose:

#!/usr/bin/env bash
# Author: Kathryn Janeway
# Purpose: Initialize Voyager

Creating and Running Shell Scripts

If your system user has read and execute permissions on a script and the script is in your current directory, you can run it like so:

./ex_script

The ./ represents a null directory, which indicates the current directory at any given time.

An alternative approach is to pass the script as an argument to the shell that you want to interpret the script (with this approach, the script does not need to marked as executable). For Bash, that looks like:

bash ex_script

In regard to naming and running Bash scripts in a more convenient, sustainable fashion, refer to the Running Bash Scripts instructions on the Conventions page.

Creating a new script and configuring it to run involves several steps and is likely a task that you will need to repeat. This makes script creation an excellent candidate for automation with Bash scripting.

The following script has options for creating Bash and Python scripts. The script is easily extensible and configurable to allow the addition of new scripting languages and default selections.

The full script is available below:

If you ever want to add options for more scripting languages (e.g., ruby, perl), you can do the following to incorporate them into the script:

  1. Update the menu function to present and address another scripting language.
  2. Create a new front matter function for the new scripting language.
  3. Add another elif block to the script's program to address the new scripting language extension (these are the lines following the # Call appropriate front matter function based on script extension comment).

New default values can be set by updating the variable values for the lines following the # Program comment.

Collecting Input With the read Command

Arguments can be passed to a script on the command line when a script is called via positional parameters. However, you sometimes need to interactively collect information from a user after a script is started, like in the script creation script above.

The read command reads a line from the standard input and splits it into fields, as with word splitting. This command can be used to collect information from users and save it into shell variables, so it is often used in shell scripts.

One or more variables can be provided to read:

read ex_variable...

After word splitting, the first variable is assigned the first word and the second variable is assigned the second word:

$ read -r h t <<< 'Hi there'
$ echo "${t} ${h}"
there Hi

Above, the <<< operator denotes a here string, which specifies a string value to use as input for the read command. read's -r option prevents the use of backslashes to escape characters. h and t are variable names to store the text from the specified here string.

Superfluous variables sent to read remain empty:

$ read -r d e  <<< '4 5 6'
$ echo "${e}"
5 6

Above, 4 is placed into the d variable, while both 5 and 6 are placed into the e variable.

The shell uses the contents of the Internal Field Separator (IFS) variable, viewed per character, as the separators. The default value of IFS consists of the space character, the tab character, and the newline separator, but you can specify a different value (e.g., IFS=',').

read's -p option can be used to output a prompt string (without a trailing newline) for the user before attempting to read input (e.g., read -p 'Please enter your name: ' user_name).

When using the read command to collect information from users, it is important to allow the user a way to abort the script. In the script creation script above, this was accomplished via the q option in the menu() function:

q)
    :
    ;;

An alternative approach can use a function, a flag, and a loop (e.g., a while or until loop):

function end() {
    done=0 # Set flag

    while [[ "${done}" -ne 1 ]]; do
        read -p 'Continue? (yes or no): ' answer

        case "${answer}" in
            'yes')
                echo -e '\nThe loop continues!\n'
                ;;
            'no')
                echo -e '\nThe loop ends.'
                done=1 # Increment flag to break out of loop
                ;;
            *)
                echo -e '\nPlease enter a valid menu selection.\n' 1>&2
                ;;
        esac
    done
}

Above, the flag enables you to set a test condition that can be used to end the loop.

Arrays

An array is a variable that contains multiple elements that are selected by indices. Bash supports both indexed and associative arrays.

A variable can be marked as an array by setting the appropriate attribute for that data type on the variable using the declare command.

Each element in an array can be accessed via its index (key). In Bash, you can output these values like this:

echo "${ex_array[ex_element_index]}"

Values can be assigned or re-assigned to specific indices in an array as well:

ex_array[ex_element_index]=ex_value

To add elements to an array, you can use the += operator:

ex_array+=(ex_new_element_1 ex_new_element_2)

To remove an element from an array, use the unset command:

unset ex_array[ex_index]

unset can also be used to delete an entire array (or any variable):

unset ex_array

Additional constructs are available to help you gauge information about an array:

${ex_array[*]}
Obtains all element values of ex_array and return as a single word. The separator for each element is determined by the IFS variable. If IFS is not set, the separator is a space.
${ex_array[@]}
Obtains all element values of ex_array and return as separate words. The separator for each element is determined by the IFS variable. If IFS is not set, the separator is a space.
${!ex_array[*]}
Obtains all element indices of ex_array and return as a single word. The separator for each index is determined by the IFS variable. If IFS is not set, the separator is a space.
${!ex_array[@]}
Obtains all element indices of ex_array and return as separate words. The separator for each index is determined by the IFS variable. If IFS is not set, the separator is a space.
${#ex_array[*]}
Obtains the number of elements in ex_array, i.e., determine its length.
${#ex_array[ex_element_index]}
Obtains the length of the element at ex_element_index.

Indexed Arrays

An indexed array can be explicitly created like so:

declare -a ex_array

Also, an indexed array can be implicitly created by simply assigning values to it:

ex_array=(ex_element_1 ex_element_2 ex_element_3)

In an indexed array, the element indices are numbers, starting at 0. Index creation is implicit (i.e., it is automatically assumed and indices do not need to be explicitly defined). For example, the star_trek array below has four elements:

star_trek=('The_Original_Series' 'The_Next_Generation' 'Deep_Space_Nine' 'Voyager')

We can confirm the length of the array:

$ echo "${#star_trek[*]}"
4

We can determine the values stored at both the first and last index in the array:

$ echo "${star_trek[0]}" ; echo "${star_trek[3]}"
The_Original_Series
Voyager

We can add a new element to the end of the array:

$ star_trek+=('Enterprise')
$ echo "${star_trek[*]}"
The_Original_Series The_Next_Generation Deep_Space_Nine Voyager Enterprise

We can overwrite the current value of an array element:

$ star_trek[4]='Discovery'
$ echo "${star_trek[*]}"
The_Original_Series The_Next_Generation Deep_Space_Nine Voyager Discovery

Finally, we can remove an element from the array:

$ unset "star_trek[4]"
$ echo "${star_trek[*]}"
The_Original_Series The_Next_Generation Deep_Space_Nine Voyager

Associative Arrays

An associative array can be explicitly created like so:

declare -A ex_array

In an associative array, the element indices (referred to as keys), are defined by the user. For example, this is the star_trek array re-imagined as an associative array:

$ declare -A star_trek
$ star_trek=(
    ['The_Original_Series']='1966'
    ['The_Next_Generation']='1987'
    ['Deep_Space_Nine']='1993'
    ['Voyager']='1995'
)

The star_trek associative array has the name of each series as each element's key and the broadcast year of the show as the element's value.

We can confirm the length of the array:

$ echo "${#star_trek[*]}"
4

We can determine the values stored in the array for specific series:

$ echo "${star_trek['The_Original_Series']}" ; echo "${star_trek['Voyager']}"
1966
1995

We can add a new element to the end of the array:

$ star_trek+=(['Enterprise']='2001')
$ echo "${star_trek[*]}"
1995 1993 1966 2001 1987

We can view display the keys used in the array:

$ echo "${!star_trek[*]}"
Voyager Deep_Space_Nine The_Original_Series Enterprise The_Next_Generation

We can overwrite the current value of an array element:

$ star_trek['Enterprise']='2001-2005'
$ echo "${star_trek[*]}"
1995 1993 1966 2001-2005 1987

Finally, we can remove an element from the array:

$ unset star_trek['Enterprise']
$ echo "${star_trek[*]}"
1995 1993 1966 1987

Conditionals

Conditionals enable you to test for a condition and to make the execution of code contingent upon that condition.

if Statement

The basic format of an if statement is:

if [[ ex_test_case ]]; then
    ex_code
fi

If ex_test_case is true, then ex_code executes.

You can create a negative if statement by using the logical NOT operator (!):

if ! [[ ex_test_case ]]; then
    ex_code
fi

Above, ex_code will only execute if ex_test_case is not true, i.e., it is false.

The prior examples make use of the test command's abbreviated notation to test the condition we specify ([[ ex_test_case ]]). However, there may be times where the test condition you are interested in is whether or not a command successfully ran. Such an if statement is constructed like so:

if ex_command > '/dev/null' 2>&1; then
    ex_code
fi

Above, if ex_command successfully executes, i.e., its exit status returns a 0, ex_code will run. > '/dev/null' ensures that any output from the command that would normally appear on the standard output is sent to the null device (/dev/null), and 2>&1 redirects the standard error to the standard output. > '/dev/null' 2>&1 ensures that we do not actually see output from ex_command in our script, as this may be unwanted.

elif Statement

If you have separate sets of code that should only be run under distinct conditions, the elif clause can help:

if [[ ex_test_case_1]]; then
    ex_code_1
elif [[ ex_test_case_2 ]]; then
    ex_code_2
fi

Only ex_code_1 or ex_code_2 will run. Not both, i.e., as soon as a test condition is met, its associated code will run and no subsequent code in the if statement will be executed.

else Statement

The else clause is used to specify default code that you want to run if none of the conditional tests that you specify are met:

if [[ ex_test_case ]]; then
    ex_code_1
else
    ex_code_2
fi

Regular Expressions

An if statement can be used to evaluate the content of a variable using a regular expression:

if [[ ex_variable =~ ex_regular_expression ]]; then
    ex_code
fi

ex_regular_expression is evaluated as a POSIX extended regular expression.

The following example determines if a url variable contains an IPv4 address value by comparing it to a regular expression stored in the ipv4_regex variable:

# Regex to match IPv4 addresses
ipv4_regex='(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]'
ipv4_regex+='?[0-9][0-9]?)){3}'

if [[ "${url}" =~ ${ipv4_regex} ]]; then
    ex_code
fi

The case Statement

A case statement is used to create several cases, i.e., distinct blocks of code. Instead of defining a test condition, as with if statements, the value of a variable is evaluated and each case is tied to a specific variable value.

case ex_variable in
    ex_case_1)
        ex_code
        ;;
    ex_case_2)
        ex_code
        ;;
    ex_case_3)
        ex_code
        ;;
    *)
        ex_code
        ;;
esac

Above, ex_variable is the variable to be evaluated. ex_case_1, ex_case_2, and so on represent different values of ex_variable. * is the shell wildcard and is used to define the case statement's default case (i.e., the case when the value in the evaluated variable does not match any of the defined cases). Each case is ended with two semicolons (;;).

Case statements are great for evaluating user input from menus.

Loops

Loops are common constructs used in Bash scripts. They can be used to iterate through the individual characters in a string, the elements in arrays, or simply to keep repeating a set of code until a condition is met.

for Loops

The basic format of the for loop is:

for ex_variable in ex_list; do
    ex_code
done

Above, ex_list can be a space-separated list of items. If you omit the in keyword and ex_list, the for loop will iterate over the arguments (i.e., positional parameters) passed to the script. This makes for i equivalent to for i in "${@}".

Keep in mind, you can pipe the output of a loop using a pipeline. Loops and conditionals (like functions and normal commands) have standard input and standard output channels.

A for loop with C-style syntax (i.e., an arithmetic for loop) is constructed like so:

for ((  ex_counter=0; ex_counter < 5; ex_counter++ )); do
    ex_code
done

Notice that when you refer to the ex_counter variable using C-style syntax in (( ex_counter=0; ex_counter < 5; ex_counter++ )), you need to leave off the $.

Looping Through a Range

The for loop can loop through (inclusively) a range of values (e.g., letters, numbers) using extended brace expansion:

for ex_variable in {ex_start_value..ex_end_value}; do
    ex_code
done

Looping Through a Range With an Increment

A for loop with a range can include an increment, as well:

for ex_variable in {ex_start_value..ex_end_value..ex_increment}; do
    ex_code
done

Looping Through a String

You can combine an arithmetic for loop and the Bash length operator to loop through each character of a string:

for (( ex_counter=0; ex_counter < "${#ex_string}"; ex_counter++ )); do
    ex_code
done

To reference each character in the string, use the Bash substitution operator :, e.g., "${ex_string:$ex_counter:1}". In "${ex_string:$ex_counter:1}", $ex_counter is the index number of the character string you want to start slicing at, i.e., your offset, and 1 is the length of the string you want to slice starting at your offset.

Looping Through an Array

The for loop can be used to loop through the contents of both indexed and associative arrays.

The format for looping through an array's values is:

for ex_variable in "${ex_array[@]}"; do
    ex_code
done

The format for looping through an array's indices (keys) is:

for ex_variable in "${!ex_array[@]}"; do
    ex_code
done

The keys and values can be simultaneously obtained like so:

for ex_variable in "${!ex_array[@]}"; do
    echo -e "\nKey:   ex_variable"
    echo "Value: ${ex_array[ex_variable]}"
done

For example:

for key in "${!star_trek[@]}"; do
    echo -e "\nKey:   ${key}"
    echo "Value: ${star_trek[${key}]}"
done

Key:   Voyager
Value: 1995

Key:   Deep_Space_Nine
Value: 1993

Key:   The_Original_Series
Value: 1966

Key:   Enterprise
Value: 2001

Key:   The_Next_Generation
Value: 1987

Looping Through a Set of Files

A for loop can be used to loop through a directory's files:

for ex_variable in ex_directory/*; do
    ex_code
done

Above, ex_directory/* will expand to a list of filenames from ex_directory via filename expansion.

while Loops

The while loop repeats a set of code while a condition remains true.

while ex_condition; do
    ex_code
done

Looping With a Counter

We can redo a for loop as a while loop like so:

ex_counter=0

while [[ "${ex_counter}" -lt ex_number ]]; do
    ex_code
    (( ex_counter++ ))
done

The above loop defines an ex_counter variable and sets it to 0. Then, while the ex_counter variable is less than ex_number, ex_code will run and the ex_counter variable will be incremented.

Looping Through the Lines of a File

Here is a script called birthday.bash:

#!/usr/bin/env bash
# Author: siliconsoul.org
# Purpose: Generate custom birthday thank you messages.

IFS=':'

while read name email present; do
    {
        echo -e "${name},\n"
        echo "Thank you for the ${present}!"
        echo -e 'I enjoyed it very much.\n'
        echo 'Best wishes,'
        echo 'Jean-Luc'
    } | mail -s "Many thanks! ${email}"
done < "${1}"

First, the IFS variable is set to a colon (:). Then, a file that is passed to the script on the command line as the first positional parameter (${1}) is sent to the loop via redirection. The passed in file contains colon-delimited lines that consist of a name, email address, and present.

The script reads the file and constructs a thank you email message from each line. Specifically, the read command reads the input, line by line, and splits each line at the colons (determined by the variable IFS) into three fields:

  1. name
  2. email
  3. present

These fields are made available as variables inside of the loop. The custom birthday message is constructed from each line of the passed in file and mailed to the appropriate recipient with the subject line Many thanks! using a pipe.

Looping Through Interactive Input

The following while loop interactively collects information from a user and continues until the user enters a specific flag to break the loop:

while read ex_variable; do
    if [[ "${ex_variable}" == 'ex_flag' ]]; then
        break
    else
        ex_code
    fi
done

Above, the while loop collects input from a user via the terminal's standard input. If the user does not enter the ex_flag value, the loop executes ex_code in the conditional's else block and the loop repeats. When the user enters the ex_flag value, the break keyword breaks the loop.

Bash can use both the break and continue keywords. The break command aborts the loop and continues execution of the code after the loop. The continue command does not abort the complete loop, but just aborts the current iteration of the loop.

until Loops

An until loop repeats a set of code until a condition is true.

until ex_condition; do
    ex_code
done

Looping With a Counter

We can redo a for loop as an until loop like so:

ex_counter=0

until [[ "${ex_counter}" -gt ex_number ]]; do
    ex_code
    (( ex_counter++ ))
done

The above loop defines an ex_counter variable and sets it to 0. Then, until the ex_counter variable is greater than ex_number, ex_code will run and the ex_counter variable will be incremented.

The select Loop

The select loop is an alternative to using the echo command and case statement for creating menus and evaluating user responses.

ex_options=(ex_option_1 ex_option_2 ex_option_3)

PS3='Make a selection: '

select ex_variable in "${ex_options[@]}"; do
    ex_code
done

The shell presents the entries in the ex_options indexed array with preceding numbers and the user makes a choice by means of one of the numbers. select uses the IFS variable to separate the various menu entries when constructing the menu.

The PS3 (Prompt Statement 3) variable is used to contain a prompt message to the user (i.e., Make a selection:).

ex_variable is the variable that contains the user's menu selection and is what the select statement evaluates. select behaves like a for loop, i.e., if a list is omitted, it presents the script's positional parameters for selection. The select loop, like all other Bash loops, can be aborted using the break or continue keywords.

To have different code execute for different user selections, the loops code could contain a series of if..elif..else conditionals, like so:

ex_options=(ex_option_1 ex_option_2 ex_option_3)

PS3='Make a selection: '

select ex_variable in "${ex_options[@]}"; do
    if [[ ex_variable == ex_option_1 ]]; then
        {
            ex_code_1
        }
    elif [[ ex_variable == ex_option_2 ]]; then
        {
            ex_code_2
        }
    elif [[ ex_variable == ex_option_3 ]]; then
        {
            ex_code_3
        }
    else
        break
    fi
done

For the example above, if a user does not enter one of the options specified in the ex_options indexed array, the select loop will end via the break keyword. Also, a user can press Ctrl+d to exit a select loop at any time.

Functions

Shell functions are blocks of code that you can repeatedly call by invoking the name of the function. In Bash, you can create a function as follows:

ex_function_name() {
    ex_commands
}

For example, this simple function outputs a greeting:

hello_world() {
    echo 'Hello, world!'
}

You call the function by using its name, hello_world.

Unlike aliases, functions must appear at the beginning of any command. Anything else on the line, up until redirection or semicolons, will be considered an argument for the function.

From the point-of-view of the invoking code, shell functions behave like normal commands. They have standard input and output channels and can take command-line arguments. Within a shell function, the positional parameters correspond to the shell function's arguments, not to those of the actual shell process.

Inside of a function, ${0} is still the name of the shell script, not that of the shell function. If you want to know the name of the function, you can use the FUNCNAME shell variable.

If you need to exit a function at a specific point and ensure that no code in the function after this point is executed, use the return keyword.

The exit status of a shell function is the exit status of the last command executed by it (the return keyword does not count) and is considered the exit status of the function as a whole.

The names and definitions of the functions currently defined within your shell can be inspected using the declare -f command. declare -F gives you just the function names.

The shift Command

Sometimes, it may be advantageous to manipulate the positional parameters that were provided to a script when it was called. The shift command shifts the positional parameters down by one.

#!/usr/bin/env bash
# Author: siliconsoul.org
# Purpose: Demonstrate shift command.

# Program

shift

# Loop through Bash positional parameters and display each value
for ex_variable in "${@}"; do
    echo "${ex_variable}"
done

exit 0 # Exit script with successful exit status
$ shift.bash 1 2 3 4
2
3
4

If you supply shift with a numeric value (e.g., shift 2), it will shift values down by that number, instead of the default of one.

The seq Command

The seq command prints a sequence of numbers. The sequence starts at 1 and goes to the integer provided to the command as an argument.

$ seq 6
1
2
3
4
5
6

If two arguments are provided to seq, it will consider them the range of values to print:

$ seq 2 4
2
3
4

If three arguments are provided to seq, the second value will be considered an increment:

$ seq 1 2 10
1
3
5
7
9

Here is a function that is part of a script that is meant to rotate old log files. It supports ten old versions as follows: When rotating, ${file_basename} is renamed to ${file_basename}.0, a possibly existing file ${file_basename}.0 is renamed to ${file_basename}.1, and so on. A possibly existing file ${file_basename}.9 is deleted.

#!/usr/bin/env bash
# Author: siliconsoul.org
# Purpose: Rotate log.

# Functions

function rotate() {
rm -f "${1}.9"
    for i in $(seq 9 -1 1); do
        mv -f "${1}.$(( i-1 ))" "${1}.${i}"
    done

    mv "${1}" "${1}.0"
}

# Program

rotate "${1}"

exit 0 # Exit script with successful exit status

rotate.bash ex_file_basename

The script is sent an argument that represents the log file basename (e.g., app_log). app_log.9 is removed, then all the other log files in the directory where the script is running are incremented up by one (e.g., app_log.8 is renamed app_log.9, app_log.0 is renamed app_log.1). Finally, the current log file, app_log, is renamed app_log.0.

Exit Statuses

Many programming languages use Boolean values (e.g., TRUE or FALSE) to govern conditionals or loops. Bash does not. It uses the exit status of a program for control.

On GNU/Linux, every process tells its parent process upon termination whether it was successfully executed or not. The exit status in Bash is an integer between 0 and 255. The value of 0 always implies success. Every other value (up to and including 255) implies failure. This makes it possible for a process to give more detail as to what went wrong.

Reserved exit statuses for Bash include:

0
Success
1
General error
2
Misuse of built-in shell functions (rarely used)
126
Command was not executable (no permission, or not a binary)
127
Command to be executed was not found
128
Invalid argument on exit, as in exit 1.5
129-165
Program terminated by signal

The exit() system call does accept values up to and including 255 (larger values will be passed on modulo 255), but Bash uses exit statuses from 129 onward to denote that the child process was terminated by a signal. You can determine which signal was used by subtracting 128 from the exit status.

If you place a ! in front of a command, that command's exit status is then logically negated, i.e., success becomes failure, and failure becomes success.

$ true; echo "${?}"
0
$ ! true; echo "${?}"
1
$ ! false; echo "${?}"
0

The exit Command

The exit command causes normal process termination. An integer can be passed to the command as an argument to specify a specific exit status value for the process:

exit ex_integer

In general, an exit status of 0 is a success and an exit status of 1 is an error. One exception is the grep command, where 0 means that there is a hit, 1 means that there are no hits, and anything else is an error.

If you invoke exit without an argument or simply reach the end of a shell script, the shell terminates, as well. In this case, the shell's exit status is that of the last command that was executed (exit does not count).

$ bash -c 'true ; exit'; echo "${?}"
0
$ bash -c 'false ; exit'; echo "${?}"
1

Documentation

You can find more information on the commands discussed above by examining the Linux User's Manual, either at the command line or online.

Enjoyed this post?

Subscribe to the feed for the latest updates.