Project 10. Write Shell ScriptsIn this project, you'll write a simple shell script using arguments, shell variables, and control constructs. In Project 9, we learned that a shell script is a sequence of commands, written in a shell scripting language and stored in a file the shell can read, and we wrote a simple script of the kind runs a series of normal Unix commands, just as you would issue them on the command line, such as cd, ls, mv, and so on. Learn More
Now we'll take a look at a more advanced kind of shell script, one that takes advantage of features of the shell scripting language itself. These advanced features include variables and a variety of control constructs, and they enable techniques like loops and conditional processing. In combination with standard Unix commands, these scripting features allow the creation of sophisticated scripts that can adapt their behavior to different system conditions. A script might test for the presence of necessary support files before installing softwareand install any missing files before continuing. We won't be that ambitious with our first conditional script; we'll just have it test to see whether a file exists, rename it if it does, and display an error message if it doesn't. Learn More
Once again, scripting requires the use of a text editor. If you're not familiar with any of the available Unix text editors, use Apple's TextEdit, but remember to save the file as plain text. In TextEdit, select menu Format and then item Make Plain Text. If you see the option Make Rich Text instead, your file is already in plain text. Shell ArgumentsMost Unix commands take arguments. In the command cp file1 file2, for example, cp is passed two arguments. Shell scripts, too, can take arguments, and this section shows how a script processes them. Looking at it from the inside (when writing a script as opposed to running it), arguments are often called parameters, so that's the term I'll use throughout this project. Learn More
Let's look at a ready-made script called rename that illustrates the basics. It doesn't exist on your Mac, so you're going to have to create it by typing it in a text editor and saving it. You can save it anywhereDocuments might be a good place to choose. This is how it looks displayed in cat. $ cat rename #!/bin/bash # Usage: extension filename mv $2 $2.$1 $ Learn More
This script expects to see two parameters: an extension and a filename. It adds the extension to the filename. A parameter is accessed using the notation $n, where n=1 for the first argument passed, 2 for the second, and so on. This is termed parameter expansion. The script is very primitive and doesn't do any more than command mv does, but we shall improve it over the course of the project. Let's give it execute permission and run it. $ chmod +x rename $ ls f1 rename $ ./rename txt f1 $ ls f1.txt rename $ The script does what it should but has a flaw: If it is passed a filename with spaces, it fails. In this example $ ./rename txt "my file" usage: mv [-f | -i | -n] [-v] source target mv [-f | -i | -n] [-v] source ... directory the line mv $2 $2.$1 expands to mv my file my file.txt To prevent this type of problem, it's prudent to enclose parameter expansion in double quotes. By doing so we preserve spaces in file names. Don't use single quotes, because single quotes stop parameters from being expanded. Here's the new version of the script. Enclosing "$1" in quotes ensures that whatever $1 is expanded to, it will be treated as a single item, never as two space-separated items. $ cat rename #!/bin/bash # Usage: extension filename mv "$2" "$2"."$1" $ ls my file rename $ ./rename txt "my file" $ ls my file.txt rename $ Learn More
Conditional ProcessingConditional processing says to do something only if particular conditions are met and possibly to do something else if they are not. One improvement to the rename script is to ensure that at least two arguments are passed. $ cat rename #!/bin/bash # Usage: extension filename if [ $# -ne 2 ]; then echo "Usage: $0: extension filename" else mv "$2" "$2"."$1" fi $ An if construct evaluates the condition that follows in [ square brackets ]. If the condition turns out to be true, the statements between then and else are executed; if it turns out to be false, the statements between else and fi are executed. Any number of statements is allowed in either part of the if statement, even other if-then-else-fi statements. Notice that within the if and else parts, other statements are indented to make the structure of the script easier to read. Note
The special parameter expansion $# expands to the number of arguments passed, and within a condition, -ne means not equal to. So the condition is testing "if the number of arguments is not equal to 2." The special parameter expansion $0 on the next line expands to the name of the shell script itself, as typed on the command line. Tip
The else part is optional, and this is perfectly legal. if [ condition ]; then statements fi Running the improved rename with too few parameters gives $ ./rename txt Usage: ./rename: extension filename $ Learn More
Multiple ConditionsAn if statement may have multiple conditions, each additional condition being introduced by elif. if [ "$1" = "positive" ]; then echo "Yes" elif [ "$1" = "negative" ]; then echo "No" else echo "Not sure" fi Alternatively, if all the conditions test for alternative values of a single variable, use a case statement. Each alternative should be terminated by ;;. Otherwise, processing falls through to the next alternative instead of going to the end of the case. The very last alternative should be *), which catches all other possibilities. case "$1" in "positive") echo "Yes" ;; "negative") echo "No" ;; *) echo "Not sure" ;; esac LoopsThe rename script would be actually useful if it could take an arbitrary number of filenames onto which the extension is added. Doing this requires some sort of loop that will process all the parameters. In shell speak, "all the parameters" is represented by the special parameter expansion $*. Bash provides several looping constructs, of which the for loop is the most appropriate here. $ cat rename #!/bin/bash # Usage: extension filename if [ $# -lt 2 ]; then echo "Usage: $0: extension filename" else # loop to process each parameter for file in $*; do # rename the file echo mv "$file" "$file"."$1" done fi Tip
The for loop repeats for each value given in a list of values and assigns the next value to variable file each time around the loop. You may use any variable name here instead of file, of course. In this example, the list is $*, which expands to all parameters (all arguments passed to the script). As a precaution, the line that does the actual moving is preceded by echo. echo mv "$file" "$file"."$1" The move command will not be executedjust displayed on the terminal. In this way, we can check that it will do as we expect, and if everything looks good, we remove echo. Let's run the script. $ ./rename txt "file 1" "file 2" mv txt txt.txt mv file file.txt mv 1 1.txt mv file file.txt mv 2 2.txt D'oh! You will notice two problems, or bugs, with the script, First, we did not want to process the extension name within the loop; second, parameters with spaces are being split. A useful command called shift drops the first parameter off the list, which we can use after first saving the extension name in a shell variable. A shell variable is assigned with the syntax variablename=value and the value is recalled by $variablename To solve the second bug, we need only surround $* in double quotes, as we did with $1. $ cat rename ... else # save the extension then drop it from the parameter list extension="$1" shift # loop to process each parameter for file in "$*"; do echo mv "$file" "$file"."$extension" done fi $ ./rename txt "file 1" "file 2" mv file 1 file 2 file 1 file 2.txt D'oh! Again! Quoting $* has created one big long parameter. Solving this problem would be tricky were it not for a neat feature of Bash. Enter "$@", which expands to "$1" "$2"... instead of "$1 $2...", which is what "$*" expands to. $ cat rename ... # loop to process each parameter for file in "$@"; do if [ -r "$file" ]; then mv "$file" "$file"."$extension" else echo "No such file: $file" fi done fi Now let's run the script. $ ls f2 file 1 file 3 rename $ ./rename txt file\ 1 f2 rubbish "file 3" No such file: rubbish $ ls f2.txt file 1.txt file 3.txt rename That looks better. Notice that an extra if condition has been added to check that the file we are trying to rename does exist and is readable. Check out the man page for test for a list of possible conditions.
A while loop loops continually while a specified condition is true. The condition is formed in exactly the same way as for an if statement. #!/bin/bash read -p "Give a filename: " fn while [ "$fn" != "" ]; do if [ -e "$fn" ]; then file "$fn" else echo "File $fn does not exist" fi read -p "Give a filename: " fn done This script prompts you to enter a filename, then displays the content type of the file you named (if it exists). It accepts filenames that contain spaces, without requiring that they be quoted. Tip
Bash also provides an until loop, which simply uses the keyword until instead of while. As you might guess from the linguistic sense of the construct, the condition for exiting the loop is reversed. |