We can write complex computations using command line tools by creating shell scripts. Shell scripts are nothing more than simple files containing shell commands grouped together. They are called ‘shell scripts‘ because all the complex computations that would need to be typed out to the terminal are located in one file – the script. The shell can read that script and carry out the commands as though they were typed directly on the command line. Shell scripts are used to circumvent and automate repetitive tasks, or tasks that require a multiple series of commands. An additional benefit of shell scripts is that long commands can be more readable by indentation if put in a script instead of being written directly in the command line.
Simple shell script
#!/bin/sh echo 'Hello!'
Although the first line looks like a comment (the hashtag seems like a giveaway) it is actually a special construct called a shebang. A shebang is a character sequence consisting of the hashtag and exclamation mark. It always appears at the beginning and tells the interpreter that it should interpret the file as a script. We can save this file as just hello
for now. In order to make our script executable, we should change its permissions. We can use the chmod
command that changes the access permissions.
Now, our file is ready to be executed. By running ./hello
(or alternatively sh hello
or bash hello
) we can run our script. The terminal says ‘Hello!’ back to us. Amazing!
We can also create a variable in the script:
#!/bin/sh greeting="Hello!" echo "$greeting"
Variables usually follow the same rules as in regular programming languages, but there is one important thing to remember – spaces and punctuation symbols are not allowed. Additionally, the shell does not know about the type of data assigned to the variable – it treats them all as strings. A space between greeting
and the equal sign would throw an error in the console. We used our variable as a constant though. We never redeclared it. To the shell, there is no difference between a variable and a constant, but the common practice is to use uppercase letters to denote constants and lowercase to denote variables.
#!/bin/sh GREETING="Hello!" echo "$GREETING"
There is a way to enforce the immutability of constants. We can use the declare
buildint with the read-only option (-r
) like this:
#!/bin/sh declare -r GREETING="Hello!" echo "$GREETING"
The shell would prevent any subsequent assignments to the GREETING
variable. All variables are treated as strings in the shell. If we want to restrict the assignment of variables to integers we can use the declare
builtin again by using -i
.
#!/bin/sh declare -i NUMBER=5 NUMBER="Hello!" echo "$NUMBER"
Shell arrays
The array variable in shell, similar to other programming/scripting languages can hold multiple values each assigned a numeric index. Compared to different languages, the syntax is a bit different. For example, we create arrays using plain old parenthesis without comma separators:
arr=(Orange Banana Apple) arr[4]=Cherry echo "Element with index #1 ${arr[1]}" echo "Element with index #4 ${arr[4]}" echo "All elements: ${arr[*]}" echo "All indices: ${!arr[*]}" echo "Total number of elements in the array: ${#arr[*]}"
The first line creates the array by enclosing the contents in parenthesis: arr=(Orange Banana Apple)
. Then we set the fourth item in the array to be ‘Cherry’ with arr[4]=Cherry
. With ${arr[1]}
and ${arr[4]}
we retrieved the first and fourth elements in the array. With ${arr[*]}
we printed the list of all elements (an alternative way to print all elements is to use the ${arr[*]}
, those two commands are basically interchangeable), and with ${!arr[*]}
the list of all indices (notice how there is no element indexed with 3). With the last command (${#arr[*]}
) we print out the total length of the array.
Element with index #1 Banana Element with index #4 Cherry All elements: Orange Banana Apple Cherry All indices: 0 1 2 4 Total number of elements in the array: 4
We can read elements sequentially too, using loops (we will come back to loops in future articles):
for fruit in ${arr[*]} do echo $i done
You might have noticed above that we’ve used ${arr[*]}
to print all elements in the array, but also we used this symbol in order to iterate through the array itself.
Shell functions
We can create functions the same way as in other programming languages. Shell functions are considered ‘mini-scripts’ and are located inside other scripts and they act as an autonomous program.
#!/bin/sh function hello() { echo "Hello!" return } hello2() { echo "Hello to you too!" return } hello hello2
There are two ways of declaring functions in shell, both of which are syntactically valid. In the snippet above, the shell will ignore the function blocks (hello
and hello2
), and it will start the execution at the point where hello
and hello2
are actually invoked. The shell moves the control to the functions, executing them and moving back to the main script. You also might notice the return command that is used to exit from a shell function. The return command is optional. We’ll mention it more in a couple of paragraphs down the line.
We can also create local variables, whose lifecycle depends on the shell function in which they’re defined. Once the shell function terminates all the local variables cease to exist.
#!/bin/sh test="Hello" function hello() { test="Hello from the function hello()" echo $test return } hello
We can use the local variables in order to reuse the names that might already exist either globally or in other shell functions. Additionally, using local variables means that we don’t need to worry about naming conflicts anymore.
Branching
We can also write branching statements in shell, by using the if
command and altering the program flow with the command’s exit status. The command’s exit status is a value issued by commands and provided to the subsystem when the commands are terminated. With branching instead of executing commands sequentially, we can control the flow of the script using the decision control instructions.
#!/bin/sh greeting="Hello" if [ $greeting = "Hello" ]; then echo "Greeting is 'Hello'" else echo "Greeting is something else than 'Hello'" fi
The exit status value is represented by an integer in the range from 0 to 255. Any other values apart from 0 (indicating success) indicate failure. We can examine the shell status by using the $?
command. Some exit codes can have special meanings (1 is commonly used for general errors, 2 for misusing the shell built-in, 127 for ‘command not found‘). Others you can check here. Below you can see some examples:
Optionally we can provide the exit status ourselves by writing the exit
command which accepts a single argument. The argument becomes the script’s exit status. When no arguments are passed the exit status defaults to zero. Using exit
gives us more control over scripts. If the file runs over to the end of the file, it will close with an exit status of zero by default. We can also use return
(mentioned above) as an exit status. The difference between exit
and return
is that return
exits a Bash function, and exit
exits a bash script. Now, branching in shell is not used to compare numbers, but during file, integer, and string expressions, which we will cover in the next article.