Return values and how to read them
Return values are a big deal in Linux and other Unix and Unix-like systems. They are a big deal in C programming as well. Most functions in C return some value with return
. It's that same return
statement we use to return a value from main()
to the shell. The original Unix operating system and the C programming language came around at the same time and from the same place. As soon as the C language was completed in the early 1970s, Unix was rewritten in C. Previously, it was written in assembler only. And hence, C and Unix fit together tightly.
The reason why return values are so crucial in Linux is that we can build shell scripts. Those shell scripts use other programs and, hopefully, our programs, as its parts. For the shell script to be able to check whether a program has succeeded or not, it reads the return value of that program.
In this recipe, we will write a program that tells the user if a file or directory exists or not.
Getting ready
It's recommended that you use Bash for this recipe. I can't guarantee compatibility with other shells.
How to do it…
In this recipe, we will write a small shell script that demonstrates the purpose of the return values, how to read them, and how to interpret them. Let's get started:
- Before we write the code, we must investigate what return values the program uses that we will use in our script. Execute the following commands, and make a note of the return values we get. The
test
command is a small utility that tests certain conditions. In this example, we'll use it to determine if a file or directory exists. The-e
option stands for exists. Thetest
command doesn't give us any output; it just exits with a return value:$> test -e / $> echo $? 0 $> test -e /asdfasdf $> echo $? 1
- Now that we know what return values the
test
program gives us (0 when the file or directory exists, otherwise 1), we can move on and write our script. Write the following code in a file and save it asexist.sh
. You can also download it from https://github.com/PacktPublishing/Linux-System-Programming-Techniques/blob/master/ch2/exist.sh. The shell script uses thetest
command to determine whether the specified file or directory exists:#!/bin/bash # Check if the user supplied exactly one argument if [ "$#" -ne 1 ]; then echo "You must supply exactly one argument." echo "Example: $0 /etc" exit 1 # Return with value 1 fi # Check if the file/directory exists test -e "$1" # Perform the actual test if [ "$?" -eq 0 ]; then echo "File or directory exists" elif [ "$?" -eq 1 ]; then echo "File or directory does not exist" exit 3 # Return with a special code so other # programs can use the value to see if a # file dosen't exist else echo "Unknown return value from test..." exit 1 # Unknown error occured, so exit with 1 fi exit 0 # If the file or directory exists, we exit # with
- Then, you need to make it executable with the following command:
$> chmod +x exist.sh
- Now, it's time to try out our script. We try it with directories that do exist and with those that don't. We also check the exit code after each run:
$> ./exist.sh You must supply exactly one argument. Example: ./exist.sh /etc $> echo $? 1 $> ./exist.sh /etc File or directory exists $> echo $? 0 $> ./exist.sh /asdfasdf File or directory does not exist $> echo $? 3
- Now that we know that it's working and leaving the correct exit codes, we can write one-liners to use our script together with, for example,
echo
to print a text stating whether the file or directory exists:$> ./exist.sh / && echo "Nice, that one exists" File or directory exists Nice, that one exists $> ./exist.sh /asdf && echo "Nice, that one exists" File or directory does not exist
- We can also write a more complicated one-liner—one that takes advantage of the unique error code 3 we assigned to "file not found" in our script. Note that you shouldn't type
>
at the start of the second line. This character is automatically inserted by the shell when you end the first line with a backslash to indicate the continuation of a long line:$> ./exist.sh /asdf &> /dev/null; \ > if [ $? -eq 3 ]; then echo "That doesn't exist"; fi That doesn't exist
How it works…
The test
program is a small utility designed to test files and directories, compare
values, and so on. In our case, we used it to test if the specified file or directory exists (-e
for exist).
The test
program doesn't print anything; it just exits in silence. It does, however, leave a return value. It is that return value that we check with the $?
variable. It's also the very same variable we check in the script's if
statements.
There are some other special variables in the script that we used. The first one was $#
, which contains the number of arguments passed to the script. It works like argc
in C. At the very start of the script, we compared if $#
is not equal to 1 (-ne
stands for not equal). If $#
is not equal to 1, an error message is printed and the script aborts with code 1.
The reason for putting $#
inside quotes is just a safety mechanism. If, in some unforeseen event, $#
were to contain spaces, we still want the content to be evaluated as a single value, not two. The same thing goes for the quotes around the other variables in the script.
The next special variable is $0
. This variable contains argument 0, which is the name of the program, just as with argv[0]
in C, as we saw in Chapter 1, Getting the Necessary Tools and Writing Our First Linux Programs.
The first argument to the program is stored in $1
, as shown in the test
case. The first argument in our case is the supplied filename or directory that we want to test.
Like our C programs, we want our scripts to exit with a relevant return value (or exit code, as it is also called). We use exit
to leave the script and set a return value. In case the user doesn't supply precisely one argument, we exit with code 1, a general error code. And if the script is executed as it should, and the file or directory exists, we exit with code 0. If the script is executed as it should, but the file or directory doesn't exist, we exit with code 3, which isn't reserved for a particular use, but still indicates an error (all non-zero codes are error codes). This way, other scripts can fetch the return value of our script and act upon it.
In Step 5, we did just that—act upon the exit code from our script with the following command:
$> ./exist.sh / && echo "Nice, that one exists"
&&
means "and". We can read the whole line as an if
statement. If exist.sh
is true—that is, exit code 0—then execute the echo
command. If the exit
code is anything other than 0, then the echo
command is never executed.
In Step 6, we redirected all the output from the script to /dev/null
and then used a complete if
statement to check for error code 3. If error code 3 is encountered, we print a message with echo
.
There's more…
There are a lot more tests and comparisons we can do with the test
program. They are all listed in the manual; that is, man 1 test
.
If you are unfamiliar with Bash and shell scripting, there is a lot of useful information in the manual page, man 1 bash
.
The opposite of &&
is ||
and is pronounced "or." So, the opposite of what we did in this recipe would be as follows:
$> ./exist.sh / || echo "That doesn't exist" File or directory exists $> ./exist.sh /asdf || echo "That doesn't exist" File or directory does not exist That doesn't exist
See also
If you want to dig deep into the world of Bash and shell scripting, there is an excellent guide at The Linux Documentation Project: https://tldp.org/LDP/Bash-Beginners-Guide/html/index.html.