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
, orex_script.bash
if the script is available via thePATH
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
.
andsource
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'slastpipe
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 internalcomplete
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:
- 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.
- 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:
- Update the
menu
function to present and address another scripting language. - Create a new front matter function for the new scripting language.
- 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:
name
email
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 inexit 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.