PBS 144 of X — Basic Shell Script IO
In the previous instalment we learned how to collect terminal commands into reusable shell scripts. Those scripts did exactly the same thing every time they were run because they had no way of accepting any input. The obvious next step is to learn how to send information into your scripts.
There are actually three ways of passing information into scripts, and arguably three forms of output too. You can pass command line arguments to scripts, you can prompt the user for input, your script will always finish with an exit code, and you can use unix-style pipes to redirect input to your scripts, and your scripts will produce both output and error output streams which can also be redirected. For a detailed look at what I call terminal plumbing, check out Taming the Terminal Instalment 15.
For reasons that will become obvious, we need to learn about flow of control (conditionals and loops) before we can fully use the streams, so we’ll actually put a pin in those until the end of our short excursion into shell scripting. In this instalment we’ll learn about the other three forms of IO, specifically, we’ll learn to pass arguments to our scripts, to ask the user for input, and how exit codes get generated. Exit codes are vital to understanding conditionals and loops in shell scripts, so that will tee us up perfectly for the next instalment.
Matching Podcast Episode
Listen along to this instalment on episode 758 of the Chit Chat Across the Pond Podcast.
You can also Download the MP3
Read an unedited, auto-generated transcript: CCATP_2023_02_04
- The instalment ZIP file — pbs144.zip
Command line Arguments
By far the simplest way to get some data in is to accept command line arguments, i.e. space-separated strings after the name of your script.
Bash makes command line arguments available to us in our scripts through special variables,
$1 is the first argument,
$2 the second, and so on. The command used to invoke the script is available as
$0 in case you’re wondering why computer scientists were counting from 1 like regular humans for once 😉.
pbs144a-args.sh shows this in action:
#!/usr/bin/env bash echo "I am '$0', my first arg is '$1', and my second is '$2'"
If we run the script with the command
./pbs144a-args.sh pancakes waffles we get:
bart@bartsmacstudio pbs144 % ./pbs144a-args.sh pancakes waffles I am './pbs144a-args.sh', my first arg is 'pancakes', and my second is 'waffles' bart@bartsmacstudio pbs144 %
Prompting the User
read command can read input from any stream, and saves it to a variable. By default, it reads a single line from the standard input (
STDIN) which is generally the keyboard (unless you’re dealing with pipes, but we’re ignoring that for now). Read requires one argument, the name of the variable the read text should be saved to.
We could just use
read in conjunction with
echo to ask the user for input:
echo "What's your name?" read name
This would print a line of text, then read a line of input, and save it to a variable named
name, which we would access as
echo "Hi $name"'!' (note that we double-quote the bit of the string with the variable, then single-quote the bit with the trailing exclamation point which would be a special character within double quotes).
Since asking users for input is a very common use-case for
read, there’s no need to use two commands, instead, we can specify a prompt for the user with the
-p flag. As an example, here’s
#!/usr/bin/env bash read -p "What's your name? " name read -p "What's your favourite fruit? " fruit echo "Hi $name, have some $fruit"'s 🙂'
When we run it and enter ‘Bart’ and ‘apple’ we get the following:
bart@bartsmacstudio pbs144 % ./pbs144b-prompt.sh What's your name? Bart What's your favourite friut? apple Hi Bart, have some apples 🙂 bart@bartsmacstudio pbs144 %
Again, note the mix of quoting to allow the trailing s after the variable to be added without Bash wrongly assuming you meant to print the variable
$fruits instead of
Every terminal command you run finishes by returning an exit code to the operating shell to signal its success or failure. Zero means no error, and any other number means something went wrong, and each command/app gets to choose a meaning to assign to each code it returns. You’ll often find a section at the bottom on the manual pages for terminal commands titled something like EXIT STATUS or EXIT VALUES describing all the error codes the app could return, and what they mean. For example, in the man page for
man rsync) you’ll find:
EXIT VALUES 0 Success 1 Syntax or usage error 2 Protocol incompatibility 3 Errors selecting input/output files, dirs 4 Requested action not supported: an attempt was made to manipulate 64-bit files on a platform that cannot support them; or an option was specified that is supported by the client and not by the server. 5 Error starting client-server protocol 6 Daemon unable to append to log-file 10 Error in socket I/O 11 Error in file I/O 12 Error in rsync protocol data stream 13 Errors with program diagnostics 14 Error in IPC code 20 Received SIGUSR1 or SIGINT 21 Some error returned by waitpid() 22 Error allocating core memory buffers 23 Partial transfer due to error 24 Partial transfer due to vanished source files 25 The --max-delete limit stopped deletions 30 Timeout in data send/receive
In most of computer science,
error or something bad, and
true or something good, but POSIX-style shells like
zsh are different! To help keep this straight in my head, I mentally refer to exit codes as error codes, so
0 means no error. By all means do this in your own head, but don’t write it down that way or people will assume you’re some kind of terminal amateur 🙂
Anyway, these exit codes are returned to the shell, and they’re not generally shown to the user in any way. However, we can use the special
$? variable to read the exit code returned by the previous command. For example, if we successfully list the contents the root directory one item per line we can see that the
ls command returned an exit code of
bart@bartsmacstudio pbs144 % ls -1 / Applications Library System Users Volumes bin cores dev etc home opt private sbin tmp usr var bart@bartsmacstudio pbs144 % echo $? 0 bart@bartsmacstudio pbs144 %
Exit Codes in Scripts
When working with shell scripts, the exit code returned by the last command executed within a script becomes the script’s exit code.
We can also use the
exit command to end our script with an explicit exit code (passed as the first argument). To end our script with an exit code of 101, simple use:
Exit Codes as Pseudo-booleans
These exit cods are the closest Bash gets to traditional booleans, and they are the key to all control of flow in shell scripts — conditional statements branch based on the most recent exit code, and loops continue until some exit code becomes zero. Even the boolean operators
&& for a logical and and
|| for a logical or work with exit codes. In Bash the exit code from an
&& operator is very weird when you peep under the hood, but it works as expected when you say it out loud.
|Exit code 1||Exit code 2||Result||Description|
||Success and success is success|
||Success and error is error|
||Error and success is error|
||Error and error is error|
The same is true for the
|| logical or:
|Exit code 1||Exit code 2||Result||Description|
||Success or success is success|
||Success or error is success|
||Error or success is success|
||Error or error is error|
When you combine the above tables with the fact that Bash uses so-called lazy evaluation for both
||, you can use the exit code from one command to conditionally run another. Lazy evaluation is a method for evaluating boolean operations where you stop the moment you know what the answer will be. For example, if you’re doing an or, and you run the first command and the result is success, then the result of the second command could not change the outcome (both success or success and success or error evaluate to success), so the second command is not executed!
This gives us two very common patterns:
# only run the second command if the first fails cmd1 || cmd2 # only run the second command if the first succeeds cmd1 && cmd2
Those make more sense with real-world examples:
# exit with exit code 404 if nginx fails to restart systemctl restart nginx.service || exit 404 # only restart nginx if the config is valid nginx -t && systemctl restart nginx.service
This gives us some of the power of true conditionals like
elif, but only some.
Our exploration of basic shell scripting is progressing nicely, we can now write and run scripts, work with variables, take input from the user, and even do some basic conditional behaviour with exit codes and logical operators. We’re not ready to move into the most powerful thing we’ll learn in this mini-series, flow of control, specifically, true conditional statements, and loops. That will then set us up to properly process the standard POSIX streams used by the pipe operator.
Join the Community
Find us in the PBS channel on the Podfeet Slack.