Learning shell scripting may seem a bit esoteric. However, shell is an easy environment for learning important concepts in programming; concepts which are often overlooked in traditional programming language courses. This section will look at these considerations in two parts: Those items that apply to programming in general, and those that are specific to Linux shell scripting.
Tip
System variable $PWD contains a user’s Present Working Directory or path.
When a routine has the same effect if it is performed multiple times as when it is run once, the routine is said to be idempotent. The typical hello-world.sh program is idempotent, because the effect of the program does not change with multiple executions.
Now look at the sidebar code. The first time that code is executed, files will be copied to new directory ~/Projects/dummy. The next time the code is executed, files will be copied to $PWD. Ouch! If a script can be run once from the terminal, it may be run multiple times. Therefore every shell script must be tested and verified to be idempotent.
Programmers often overlook such problems, as the invoice import feature in QuickBooks Accounting demonstrates. QuckBooks creates additional postings for invoices with every repeated import, rather than posting only the first time. (And QuickBooks is an industry standard?!)
File paths can be problematic when writing shell scripts. A script can be copied from one directory to another, which may change its startup directory. Or, a script may be called at a directory other than the user’s present working directory ($PWD). A script can be called from some other function, and $PWD could be anything in that case. Unixes vary in their system directory layouts. Therefore any path or filename used in a script must be validated before file or directory operations.
Some popular test logic operators for files are shown below. See File test operators for a detailed list.
Test | File condition which returns true |
---|---|
-e | file exists |
-f | a regular file, not a directory or device |
-s | file is not zero size |
-d | file is a directory |
Warning
The consequence of not validating files and paths before performing file I/O might be a broken system.
A previous lesson presented a code snippet to obtain a user confirmation:
echo -e -n '\n\e[1;31m Say Hello'
read -n 1 -p ' (y/n)?' RESP
echo -e '\e[0m'
...
The phrase Say Hello makes this code specific to the Hello World example script. Placing the text Say Hello in a variable and assigning it in our variable declarations will generalize the snippet and allow it to be used in any script without further code changes. This will save programming and testing time down the road.
There are other reasons to assign strings to variables in declarations at the top of a program. Configuration information will be exposed in the variable declarations instead of being buried in the script, and changing the variable’s language will translate the script for the user.
Many variables have meaning only in the internal context of a program. Others may be displayed to the user, and so language could be a concern. Then, there are variables which represent configuration settings for a system. We would want to change program configuration items without re-writing code, so configuration settings should be stored in a text file, and not hard coded.
Tip
Unixes store most configuration files in directory /etc/, or as hidden files in the user’s home directory ~/.
A common form of configuration file is structured just like variable assignment declarations. In fact, a program usually contains exactly the same statements as default variable declarations, followed by a routine to override the defaults with configuration values: maybe from a global file, then from a user file.
So the contents of a simple configuration file would be lines like:
VARIABLE1=VALUE1
VARIABLE2=VALUE2
...
The sidebar shows a generic routine to convert such a text file into assigned variables.
Note
A comment test is missing from the do .. done loop. This would entail checking the first non-whitespace character on a line, to see if it is a #, or to check for blank lines. Those lines should be skipped.
Coloring terminal output makes messages stand out. This avoids the problem of users ignoring important errors, or wondering why a program doesn’t finish when prompted with Do this now? (Y/n).
Applying embedded color commands is a function of the echo command, provided the -e option is included in the command. Try out the following commands in the terminal to see how this works.
Terminal color settings documents the possible codes which are accepted at the terminal for changing the displayed colors.
Note
Two commands for displaying text on the terminal are printf and echo. While printf behaves more consistently across platforms, this guide will discuss echo.
Similarly, there are two Unix conventions for formatting terminal text: embedded VT100 Codes, and tput commands.
Code compaction is a major source of confusion and error in programming. Shell scripts are particularly vulnerable to this, as a routine that is compacted into a single line can be pasted into the terminal and run in one step. Building a whole script from one-liners will just lead to confusion, however. Take the following command:
if [[ $EUID -ne 0 ]] ; then echo -e "\e[1;31m Use sudo \e[0m" ; exit 1 ; fi
The expanded code,
if [[ $EUID -ne 0 ]]
then
echo -e "\e[1;31m Use sudo \e[0m"
exit 1
fi
is actually intelligible.
Recently Bash has added a debugger, not documented here. Instead, a generic echo command which will display the path, line number, and a message is shown following. Insert this message throughout a script, with appropriate messages, to see the results of an executed program.
echo "$PWD$(tput setaf 1) $LINENO: $MSG-COMMAND $(tput sgr0)"
Note that this command does not require the -e option. Partial quoting ("textstring") is required to evaluate embedded code in quoted text, so this command will not work with full quotes (').