105.2 Lesson 1
Certificate: |
LPIC-1 |
---|---|
Version: |
5.0 |
Topic: |
105 Shells and Shell Scripting |
Objective: |
105.2 Customize or write simple scripts |
Lesson: |
1 of 2 |
Introduction
The Linux shell environment allows the use of files — called scripts — containing commands from any available program in the system combined with shell builtin commands to automate a user’s and/or a system’s custom tasks. Indeed, many of the operating system maintenance tasks are performed by scripts consisting of sequences of commands, decision structures and conditional loops. Although scripts are most of the time intended for tasks related to the operating system itself, they are also useful for user oriented tasks, like mass renaming of files, data collecting and parsing, or any otherwise repetitive command line activities.
Scripts are nothing more than text files that behave like programs. An actual program — the interpreter — reads and executes the instructions listed in the script file. The interpreter can also start an interactive session where commands — including scripts — are read and executed as they are entered, as is the case with Linux shell sessions. Script files can group those instructions and commands when they grow too complex to be implemented as an alias or a custom shell function. Furthermore, script files can be maintained like conventional programs and, being just text files, they can be created and modified with any simple text editor.
Script Structure and Execution
Basically, a script file is an ordered sequence of commands that must be executed by a corresponding command interpreter. How an interpreter reads a script file varies and there are distinct ways of doing so in a Bash shell session, but the default interpreter for a script file will be the one indicated in the first line of the script, just after the characters #!
(known as shebang). In a script with instructions for the Bash shell, the first line should be #!/bin/bash
. By indicating this line, the interpreter for all the instructions in the file will be /bin/bash
. Except for the first line, all other lines starting with the hash character #
will be ignored, so they can be used to place reminders and comments. Blank lines are also ignored. A very concise shell script file can therefore be written as follows:
#!/bin/bash # A very simple script echo "Cheers from the script file! Current time is: " date +%H:%M
This script has only two instructions for the /bin/bash
interpreter: the builtin command echo
and the command date
. The most basic way to run a script file is to execute the interpreter with the script path as the argument. So, assuming the previous example was saved in a script file named script.sh
in the current directory, it will be read and interpreted by Bash with the following command:
$ bash script.sh Cheers from the script file! Current time is: 10:57
Command echo
will automatically add a new line after displaying the content, but the option -n
will suppress this behaviour. Thus, using echo -n
in the script will make the output of both commands to appear in the same line:
$ bash script.sh Cheers from the script file! Current time is: 10:57
Although not required, the suffix .sh
helps to identify shell scripts when listing and searching for files.
Tip
|
Bash will call whatever command is indicated after the |
If the script file is intended to be executed by other users in the system, it is important to check if the proper reading permissions are set. The command chmod o+r script.sh
will give reading permission to all users in the system, allowing them to execute script.sh
by placing the path to the script file as the argument of command bash
. Alternatively, the script file may have the execution bit permission set so the file can be executed as a conventional command. The execution bit is activated on the script file with the command chmod
:
$ chmod +x script.sh
With the execution bit enabled, the script file named script.sh
in the current directory can be executed directly with the command ./script.sh
. Scripts placed in a directory listed in the PATH
environment variable will also be accessible without their complete path.
Warning
|
A script performing restricted actions may have its SUID permission activated, so ordinary users can also run the script with root privileges. In this case, it is very important to ensure that no user other than root has the permission to write in the file. Otherwise, an ordinary user could modify the file to perform arbitrary and potentially harmful operations. |
The placement and indentation of commands in script files are not too rigid. Every line in a shell script will be executed as an ordinary shell command, in the same sequence as the line appears in the script file, and the same rules that apply to the shell prompt also apply to each script line individually. It is possible to place two or more commands in the same line, separated by semicolons:
echo "Cheers from the script file! Current time is:" ; date +%H:%M
Although this format might be convenient at times, its usage is optional, as sequential commands can be placed one command per line and they will be executed just as they were separated by semicolons. In other words, the semicolon can be replaced by a new line character in Bash script files.
When a script is executed, the commands contained therein are not executed directly in the current session, but instead they are executed by a new Bash process, called a sub-shell. It prevents the script from overwriting the current session’s environment variables and from leaving unattended modifications in the current session. If the goal is to run the script’s contents in the current shell session, then it should be executed with source script.sh
or . script.sh
(note that there is a space between the dot and the script name).
As it happens with the execution of any other command, the shell prompt will only be available again when the script ends its execution and its exit status code will be available in the $?
variable. To change this behaviour, so the current shell also ends when the script ends, the script — or any other command — can be preceded by the exec
command. This command will also replace the exit status code of the current shell session with its own.
Variables
Variables in shell scripts behave in the same way as in interactive sessions, given that the interpreter is the same. For example, the format SOLUTION=42
(without spaces around the equal sign) will assign the value 42
to the variable named SOLUTION
. By convention, uppercase letters are used for variable names, but it’s not mandatory. Variable names can not, however, start with non alphabetical characters.
In addition to the ordinary variables created by the user, Bash scripts also have a set of special variables called parameters. Unlike ordinary variables, parameter names start with a non-alphabetical character that designates its function. Arguments passed to a script and other useful information are stored in parameters like $0
, $*
, $?
, etc, where the character following the dollar sign indicates the information to be fetched:
$*
-
All the arguments passed to the script.
$@
-
All the arguments passed to the script. If used with double quotes, as in
"$@"
, every argument will be enclosed by double quotes. $#
-
The number of arguments.
$0
-
The name of the script file.
$!
-
PID of the last executed program.
$$
-
PID of the current shell.
$?
-
Numerical exit status code of the last finished command. For POSIX standard processes, a numerical value of
0
means that the last command was successfully executed, which also applies to shell scripts.
A positional parameter is a parameter denoted by one or more digits, other than the single digit 0
. For example, the variable $1
corresponds to the first argument given to the script (positional parameter one), $2
corresponds to the second argument, and so on. If the position of a parameter is greater than nine, it must be referenced with curly braces, as in ${10}
, ${11}
, etc.
Ordinary variables, on the other hand, are intended to store manually inserted values or the output generated by other commands. Command read
, for example, can be used inside the script to ask the user for input during script execution:
echo "Do you want to continue (y/n)?" read ANSWER
The returned value will be stored in the ANSWER
variable. If the name of the variable is not supplied, the variable name REPLY
will be used by default. It is also possible to use command read
to read more than one variable simultaneously:
echo "Type your first name and last name:" read NAME SURNAME
In this case, each space separated term will be assigned to the variables NAME
and SURNAME
respectively. If the number of given terms is greater than the number of variables, the exceeding terms will be stored in the last variable. read
itself can display the message to the user with option -p
, making the echo
command redundant in this case:
read -p "Type your first name and last name:" NAME SURNAME
Scripts performing system tasks will often require information provided by other programs. The backtick notation can be used to store the output of a command in a variable:
$ OS=`uname -o`
In the example, the output of command uname -o
will be stored in the OS
variable. An identical result will be produced with $()
:
$ OS=$(uname -o)
The length of a variable, that is, the quantity of characters it contains, is returned by prepending a hash #
before the name of the variable. This feature, however, requires the use of the curly braces syntax to indicate the variable:
$ OS=$(uname -o) $ echo $OS GNU/Linux $ echo ${#OS} 9
Bash also features one-dimensional array variables, so a set of related elements can be stored with a single variable name. Every element in an array has a numerical index, which must be used to write and read values in the corresponding element. Different from ordinary variables, arrays must be declared with the Bash builtin command declare
. For example, to declare a variable named SIZES
as an array:
$ declare -a SIZES
Arrays can also be implicitly declared when populated from a predefined list of items, using the parenthesis notation:
$ SIZES=( 1048576 1073741824 )
In the example, the two large integer values were stored in the SIZES
array. Array elements must be referenced using curly braces and square brackets, otherwise Bash will not change or display the element correctly. As array indexes start at 0, the content of the first element is in ${SIZES[0]}
, the second element is in ${SIZES[1]}
, and so on:
$ echo ${SIZES[0]} 1048576 $ echo ${SIZES[1]} 1073741824
Unlike reading, changing an array element’s content is performed without the curly braces (e.g., SIZES[0]=1048576
). As with ordinary variables, the length of an element in an array is returned with the hash character (e.g., ${#SIZES[0]}
for the length of the first element in SIZES
array). The total number of elements in an array is returned if @
or *
are used as the index:
$ echo ${#SIZES[@]} 2 $ echo ${#SIZES[*]} 2
Arrays can also be declared using the output of a command as the initial elements through command substitution. The following example shows how to create a Bash array whose elements are the current system’s supported filesystems:
$ FS=( $(cut -f 2 < /proc/filesystems) )
The command cut -f 2 < /proc/filesystems
will display all the filesystems currently supported by the running kernel (as listed in the second column of the file /proc/filesystems
), so the array FS
now contains one element for each supported filesystem. Any text content can be used to initialize an array as, by default, any terms delimited by space, tab or newline characters will become an array element.
Tip
|
Bash treats each character of an environment variable’s |
Arithmetic Expressions
Bash provides a practical method to perform integer arithmetic operations with the builtin command expr
. Two numerical variables, $VAL1
and $VAL2
for example, can be added together with the following command:
$ SUM=`expr $VAL1 + $VAL2`
The resulting value of the example will be available in the $SUM
variable. Command expr
can be replaced by $(())
, so the previous example can be rewritten as SUM=$(( $VAL1 + $VAL2 ))
. Power expressions are also allowed with the double asterisks operator, so the previous array declaration SIZES=( 1048576 1073741824)
could be rewritten as SIZES=( $((1024**2)) $((1024**3)) )
.
Command substitution can also be used in arithmetic expressions. For example, the file /proc/meminfo
has detailed information about the system memory, including the number of free bytes in RAM:
$ FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` ))
The example shows how the command sed
can be used to parse the contents of /proc/meminfo
inside the arithmetic expression. The second line of the /proc/meminfo
file contains the amount of free memory in thousands of bytes, so the arithmetic expression multiplies it by 1000 to obtain the number of free bytes in RAM.
Conditional Execution
Some scripts usually are not intended to execute all the commands in the script file, but just those commands that match a predefined criteria. For example, a maintenance script may send a warning message to the administrator’s email only if the execution of a command fails. Bash provides specific methods of assessing the success of command execution and general conditional structures, more similar to those found in popular programming languages.
By separating commands with &&
, the command to the right will be executed only if the command to the left did not encounter an error, that is, if its exit status was equal to 0
:
COMMAND A && COMMAND B && COMMAND C
The opposite behaviour occurs if commands are separated with ||
. In this case, the following command will be executed only if the previous command did encounter an error, that is, if its returning status code differs from 0.
One of the most important features of all programming languages is the ability to execute commands depending on previously defined conditions. The most straightforward way to conditionally execute commands is to use the Bash builtin command if
, which executes one or more commands only if the command given as argument returns a 0 (success) status code. Another command, test
, can be used to assess many different special criteria, so it is mostly used in conjunction with if
. In the following example, the message Confirmed: /bin/bash is executable.
will be displayed if the file /bin/bash
exists and it is executable:
if test -x /bin/bash ; then echo "Confirmed: /bin/bash is executable." fi
Option -x
makes command test
return a status code 0 only if the given path is an executable file. The following example shows another way to achieve the exact same result, as the square brackets can be used as a replacement for test
:
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." fi
The else
instruction is optional to the if
structure and can, if present, define a command or sequence of commands to execute if the conditional expression is not true:
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." else echo "No, /bin/bash is not executable." fi
if
structures must always end with fi
, so the Bash interpreter knows were the conditional commands end.
Script Output
Even when the purpose of a script only involves file-oriented operations, it is important to display progress related messages in the standard output, so the user is kept informed of any issues and can eventually use those messages to generate operation logs.
The Bash builtin command echo
is commonly used to display simple strings of text, but it also provides some extended features. With option -e
, command echo
is able to display special characters using escaped sequences (a backslash sequence designating a special character). For example:
#!/bin/bash # Get the operating system's generic name OS=$(uname -o) # Get the amount of free memory in bytes FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` )) echo -e "Operating system:\t$OS" echo -e "Unallocated RAM:\t$(( $FREE / 1024**2 )) MB"
Whilst the use of quotes is optional when using echo
with no options, it is necessary to add them when using option -e
, otherwise the special characters may not render correctly. In the previous script, both echo
commands use the tabulation character \t
to align the text, resulting in the following output:
Operating system: GNU/Linux Unallocated RAM: 1491 MB
The newline character \n
can be used to separate the output lines, so the exact same output is obtained by combining the two echo
commands into only one:
echo -e "Operating system:\t$OS\nUnallocated RAM:\t$(( $FREE / 1024**2 )) MB"
Although fit to display most text messages, command echo
may not be well suited to display more specific text patterns. Bash builtin command printf
gives more control over how to display the variables. Command printf
uses the first argument as the format of the output, where placeholders will be replaced by the following arguments in the order they appear in the command line. For example, the message of the previous example could be generated with the following printf
command:
printf "Operating system:\t%s\nUnallocated RAM:\t%d MB\n" $OS $(( $FREE / 1024**2 ))
The placeholder %s
is intended for text content (it will be replaced by the $OS
variable) and the %d
placeholder is intended to integer numbers (it will be replaced by the resulting number of free megabytes in RAM). printf
does not append a newline character at the end of the text, so the newline character \n
should be placed at the end of the pattern if needed. The entire pattern should be interpreted as a single argument, so it must be enclosed in quotes.
Tip
|
The format of placeholder substitution performed by |
With printf
, the variables are placed outside the text pattern, which makes it possible to store the text pattern in a separate variable:
MSG='Operating system:\t%s\nUnallocated RAM:\t%d MB\n' printf "$MSG" $OS $(( $FREE / 1024**2 ))
This method is particularly useful to display distinct output formats, depending on the user’s requirements. It makes it easier, for example, to write a script that uses a distinct text pattern if the user requires a CSV (Comma Separated Values) list rather than a default output message.
Guided Exercises
-
The
-s
option for theread
command is useful for entering passwords, as it will not show the content being typed on the screen. How could theread
command be used to store the user’s input in the variablePASSWORD
while hiding the typed content? -
The only purpose of the command
whoami
is to display the username of the user who called it, so it is mostly used inside scripts to identify the user who is running it. Inside a Bash script, how could the output of thewhoami
command be stored in the variable namedWHO
? -
What Bash operator should be between the commands
apt-get dist-upgrade
andsystemctl reboot
if the root user wants to executesystemctl reboot
only ifapt-get dist-upgrade
finished successfully?
Explorational Exercises
-
After trying to run a newly created Bash script, a user receives the following error message:
bash: ./script.sh: Permission denied
Considering that the file
./script.sh
was created by the same user, what would be the probable cause of this error? -
Suppose a script file named
do.sh
is executable and the symbolic link namedundo.sh
points to it. From within the script, how could you identify if the calling filename wasdo.sh
orundo.sh
? -
In a system with a properly configured email service, the command
mail -s "Maintenance Error" root <<<"Scheduled task error"
sends the notice email message to the root user. Such a command could be used in unattended tasks, like cronjobs, to inform the system administrator about an unexpected issue. Write an if construct that will execute the aforementionedmail
command if the exit status of the previous command — whatever it was — is unsuccessful.
Summary
This lesson covers the basic concepts for understanding and writing Bash shell scripts. Shell scripts are a core part of any Linux distribution as they offer a very flexible way to automate user and system tasks performed in the shell environment. The lesson goes through the following steps:
-
Shell script structure and correct script file permissions
-
Script parameters
-
Using variables to read user input and to store the output of commands
-
Bash arrays
-
Simple tests and conditional execution
-
Output formatting
The commands and procedures addressed were:
-
Bash builtin notation for command substitution, array expansion and arithmetic expressions
-
Conditional command execution with the
||
and&&
operators -
echo
-
chmod
-
exec
-
read
-
declare
-
test
-
if
-
printf
Answers to Guided Exercises
-
The
-s
option for theread
command is useful for entering passwords, as it will not show the content being typed on the screen. How could theread
command be used to store the user’s input in the variablePASSWORD
while hiding the typed content?read -s PASSWORD
-
The only purpose of the command
whoami
is to display the username of the user who called it, so it is mostly used inside scripts to identify the user who is running it. Inside a Bash script, how could the output of thewhoami
command be stored in the variable namedWHO
?WHO=`whoami`
orWHO=$(whoami)
-
What Bash operator should be between the commands
apt-get dist-upgrade
andsystemctl reboot
if the root user wants to executesystemctl reboot
only ifapt-get dist-upgrade
finished successfully?The operator
&&
, as inapt-get dist-upgrade && systemctl reboot
.
Answers to Explorational Exercises
-
After trying to run a newly created Bash script, a user receives the following error message:
bash: ./script.sh: Permission denied
Considering that the file
./script.sh
was created by the same user, what would be the probable cause of this error?The
./script.sh
file does not have the execution permission enabled. -
Suppose a script file named
do.sh
is executable and the symbolic link namedundo.sh
points to it. From within the script, how could you identify if the calling filename wasdo.sh
orundo.sh
?The special variable
$0
contains the filename used to call the script. -
In a system with a properly configured email service, the command
mail -s "Maintenance Error" root <<<"Scheduled task error"
sends the notice email message to the root user. Such a command could be used in unattended tasks, like cronjobs, to inform the system administrator about an unexpected issue. Write an if construct that will execute the aforementionedmail
command if the exit status of the previous command — whatever it was — is unsuccessful.if [ "$?" -ne 0 ]; then mail -s "Maintenance Error" root <<<"Scheduled task error"; fi