---
title: Shell scripts
author: Franklin Bristow
---
Shell scripts
=============
::: outcomes
* [X] Write and run shell scripts to repeat complex tasks.
:::
::: {style='float: left;width: 300px'}
![A script.](script.png)
:::
At this point you're pretty familiar with using the command line; you should be
able to do a variety of things:
* Navigate and create folder structures.
* Edit text files.
* Convert files between various formats.
* Filter lines from text files.
* Read and change permissions on files.
* Use version control software.
* Find files by name or properties.
* Redirect standard input, output, and error.
* Construct pipelines of commands.
While there's a whole world of things you can still learn about using the
command line, and more command line tools than you can imagine to learn about
(which sometimes themselves contain entire programming languages!), you've
learned and demonstrated a lot! :tada:
As you're getting more used to using the command line, you may find yourself
repeating similar, complex commands over and over again. Or maybe you find
yourself doing the same kinds of things over and over again, maybe running
multiple commands repeatedly. Or maybe you've built a complex pipeline that
you want to keep for use later because you'll need to use it again.
In their very simplest form, shell scripts are plain text files that contain a
sequence of commands or statements, separated by newlines or semi-colons. As you
grow shell scripts, they can contain things like conditional statements, loops,
functions, and... uh, hey, [wait a minute]. That, uh, that sounds an awful lot
like programming.
Most of the time we spend interacting with shells is interactive: the shell is
waiting for us to enter a command, and when we press Enter, the shell
runs the command, waits for it to finish (it actually `wait(2)`s, it's a C
function), then patiently waits for us to enter another command.
Most shells can also run non-interactively: you give the shell the name of a
file that contains a sequence of commands and the shell will just interpret,
then run the sequence of commands like it's a program.
[wait a minute]: https://youtu.be/g3YiPC91QUk?t=58
Shells and languages
--------------------
Similar to different programming languages, different shells use different
syntax to express similar ideas. Deciding which shell program to use defines
what syntax you're going to use and the kinds of keywords you should be using
when you're looking for help (in the manual page for the shell, on the shell's
website, or generally online).
While your default shell on Aviary is `tcsh`, we're going to be using `bash` as
a shell interpreter for scripting. This is an opinionated choice, just as
using `vim` instead of `emacs` is an opinionated choice. As you get more
comfortable using the command line, you may want to choose a different shell
(like `fish`) and thus a different shell language, but `bash` and its syntax are
common enough that we'll treat it like a "[lingua franca]".
[lingua franca]: https://en.wikipedia.org/wiki/Lingua_franca
Basic scripting
---------------
Let's start with the basics: the general structure of a shell script and some
very simple shell scripts.
### General structure
Shell scripts all start with a "[shebang]" line --- a line that starts with the
symbols `#!`. The first line indicates which program is going to be run to
interpret the rest of the file.
```bash
#!/usr/bin/env bash # this is a bash script
```
Everything after the first line is the actual contents of the script, the
sequence of commands to be executed.
[shebang]: https://en.wikipedia.org/wiki/Shebang_(Unix)
### Simple scripts
Here's an example of a very simple script:
```bash
#!/usr/bin/env bash
ls -la
```
Copy and paste this into a new plain text file on Aviary; give it a file name
that represents what this does (I recommend `la`, "list all").
::: aside
Copying and pasting into `vim` is tedious, having to enter and exit paste mode
is a real bother. Another way to quickly copy and paste into your terminal to
create a new file is using the program `cat`. As above, `cat` is a program that
will read standard input and write to standard output. But we can redirect
standard output to a file!
```bash
cat > la
```
Once you enter that, press Enter, then paste, then press
Control+D.
That's it!
:::
Once you've written the file, exit your editor. Before we can run a shell
script, we need to mark it as executable using `chmod`:
```bash
chmod a+x la
```
Then we can run the script:
```bash
./la
```
:tada:, your first shell script!
Shell scripts consist of one or more lines of commands to run. Add another line
to your script:
```bash
find . -name "*.md"
```
Then run it again (you don't need to `chmod` again). Now *two* commands worth of
output appear.
One command that you might find helpful when you're scripting is the `echo`
command, this is a "print" command for shells:
```bash
echo "Hello, world!"
```
You can also write comments using the `#` symbol, anything following is a
comment:
```bash
echo "Hello, world" # a friendly message
find . -name "*.docx" -delete # clean up Word files
```
::: example
Here's a more complete (and completely contrived) example of a script:
```bash
#!/usr/bin/env bash
echo "Here's what's in the current directory:"
# list all with long listings
ls -al
echo "Here are all the Markdown files:"
# find all files with the `.md` extension
find . -name "*.md"
```
:::
Environment variables and the `$PATH`
------------------------------------
Shell languages, like other programming languages, support variables.
Almost all shells follow the same convention for naming variables:
* Variable names start with a `$`,
* Variable names are all `UPPERCASE`,
* Variable names are [`SNAKE_CASE`] (they use underscores to separate words).
[`SNAKE_CASE`]: https://en.wikipedia.org/wiki/Snake_case
All shells have special variables, and those special variables help your shell
make decisions, or help define the behaviour that your shell has. These special
variables are called "environment variables".
::: example
Try running this in your shell to find out what shell you're using:
```bash
echo $SHELL
```
`$SHELL` is an environment variable that contains the name of the running shell.
:::
When you enter the names of programs on the command line, your shell has to
figure out where that program actually is in a folder structure.
::: aside
This is might seem obvious when you think about it, but think about it: the
programs that you're running on the command line are just files with bits in
them. They were written in a programming language (often C), then compiled and
put in a folder somewhere.
A lot of programs on Linux and UNIX systems live in the folders `/bin` and
`/usr/bin`. You can learn a bit more about where files live on a Linux system by
reading a manual page:
```bash
man hier
### OR
man 7 hier # hier is in section 7 for miscellaneous
```
:::
Your shell uses a special environment variable called `$PATH` (often called
*the* `$PATH`) to find where the command you just entered exists as a file. The
`$PATH` contains a list of directories that your shell will look in to find the
file representing the command you asked it to run. Different shells use
different separators for directory entries in the path. Both `tcsh` and `bash`
use a colon `:` to separate directories.
::: example
As above, use the `echo` command to print out what the `$PATH` is right now in
your shell:
```bash
echo $PATH
```
:::
Your `$PATH` will contain many folders on Aviary, but it importantly includes
two directories:
1. `.`: the current directory, and this is why you don't have to type `./` in
front of programs you've written and compiled yourself.
2. `~/bin`: a directory named `bin` relative to your home directory. When you
`echo $PATH`, the `~` will be listed as an absolute path (e.g.,
`/home/student/you/bin`).
What this means is that we can put scripts we write into the directory `~/bin`,
then we can run them *anywhere*.
::: example
The directory `~/bin` *may not* exist in your user directory on Aviary. Create
the directory, then move the `la` script you wrote above into this directory.
This applies **only** to `tcsh`: `tcsh` [caches] the names of commands that are
in folders on the `$PATH`. After you add something to `~/bin` (which is on your
`$PATH`), you've got to get `tcsh` to regenerate this cache. You can regenerate
the cache in `tcsh` by running the command:
```bash
rehash
```
You must do this any time you add programs or commands to folders on the `$PATH`
that you want to work in other places, but this only applies to `tcsh`. If
you're using a different shell like `bash` or `fish`, then your shell will
almost certainly do this for you.
Now change back to your user directory:
```bash
cd ~
### Or just `cd` with no arguments
cd
```
And run `la`. :tada:, now you can run `la` in *any* directory.
:::
You can find out which environment variables are currently set and what their
values are using the `env` command.
[caches]: https://en.wikipedia.org/wiki/Cache_(computing)
Arguments
---------
Shell scripts that you write can accept arguments, just like programs you write
in other programming languages. In both C and Java (and Python, technically),
you can access arguments passed on the command line to your program as arrays of
strings.
Shell scripts can access command line arguments using variables, but you can
directly access arguments on the command line as numbered variables like `$1`.
::: example
Here's a small program that will print out the values passed to it as arguments
on the command line:
```bash
#!/usr/bin/env bash
echo "The first argument is $1"
echo "The second argument is $2"
echo "The third argument is $3"
echo "All arguments are $*"
```
Write this script and try running it with different arguments to see how the
output changes. Don't forget to use `chmod` to set execute permissions for your
script!
:::
A common use of arguments on the command line for shell scripts is to pass the
name of a file or directory you want to operate on.
::: example
Let's upgrade our `la` script a little bit.
Remember that `ls` can run with no arguments, and when run with no arguments
it's defaulting to printing out the contents of the directory `.`. But `ls`
*can* accept arguments. Our `la` script doesn't right now.
Change your `la` script to accept an argument and pass it to `ls`:
```bash
#!/usr/bin/env bash
ls -al "$1" # quotes in case of spaces!
```
Now run `la` again, but pass it an argument:
```bash
la .
```
Neat.
::: aside
Why "quotes in case of spaces"? Try this:
1. Remove the quotes around `$1` in your script.
2. Create a directory that has a space in its name (`mkdir "space dir"`).
3. Try running `la "space dir"`.
4. Put back the quotes around `$1` in your script.
When the shell "expands" the variable `$1`, it's replacing the value of that
variable into the command *literally*. If the variable contains spaces, it will
be replaced in the command spaces and all. In other words,
```bash
ls -al $1 # becomes:
ls -al space dir
```
If you remember *way back a long time ago*, we had to put quotes around names
with spaces when using `mkdir` because `mkdir` would turn `space dir` into two
directories. Similarly, `ls` is looking for two separate directories.
Including the quotes around `$1` makes sure that even if the variable contains
spaces, it's going to be quoted when it's passed to the command:
```bash
ls -al "$1" # becomes
ls -al "space dir"
```
:::
:::
While we've improved `la` slightly here, we've also broken it. Try running `la`
by itself with no arguments.
...
Oops. Now we need to test for the special case of no arguments being passed.
We're going to need some more tools for that: conditional statements.
Structures: conditional statements and loops
--------------------------------------------
Shell scripting languages are fully featured programming languages and include
structures like conditional statements and loops. They contain other structures,
too, but let's stick to the basics.
### Conditional statements
Conditional statements in `bash` use the familiar `if` keyword and *resemble*
the expressions you've seen in other languages.
One of the major differences in `bash` are expressions themselves: most of the
questions you're going to be asking about a variable use unary operators.
Here's what a `bash` conditional statement looks like:
```bash
if [[ -a hello.c ]]; then
echo "hello.c exists"
else
echo "hello.c does not exist"
fi
```
The `-a` is a unary operator on file names. `-a` returns true if the file
exists, and returns false if the file does not exist.
Spacing is important here! `bash` is not a very smart language. You might be
tempted to leave out spaces between `[[` and `-a` or between `hello.c` and `]]`,
but you **must** have spaces between these symbols.
::: aside
WHY?!
`bash` is, uh, weird. `[[` is technically a command that takes arguments. The
arguments the `[[` command is getting in the above example are `-a`, `hello.c`,
and `]]`. The `;` is a line separator (like in Python it's optional, but can be
used).
Yeah, weird.
:::
`bash` has many unary operators that you can use to test files or variables. The
one we care about right now is the `-n` operator, asking if a string is non-zero
in length.
::: example
Let's add a conditional statement to `la` to test for the presence of arguments:
```bash
#!/usr/bin/env bash
if [[ -n "$1" ]] ; then
ls -al "$1"
else
ls -al
fi
```
:::
You can find more unary operators in `bash` by reading the `CONDITIONAL
EXPRESSIONS` section of its manual page, but here are some examples:
+----------------------+----------------------------------------------------+
| Operator | Meaning |
+======================+====================================================+
| `-a file` | True if file exists. |
+----------------------+----------------------------------------------------+
| `-d file` | True if file exists and is a directory. |
+----------------------+----------------------------------------------------+
| `-r file` | True if file exists and is readable. |
+----------------------+----------------------------------------------------+
| `-s file` | True if file exists and has a size greater than 0. |
+----------------------+----------------------------------------------------+
| `string1 == string2` | True if the strings are equal. |
+----------------------+----------------------------------------------------+
### Loops
We can't talk about conditional statements without at least saying something
about loops!
Similar to conditional statements, loops use the familiar `for` keyword. Bash
also supports `while` and `until` loops, but most of the time you're using a
loop in Bash, you're operating on some sequence of file names rather than until
some event happens.
The structure of a `for` loop is frustratingly different from conditional
statements in a way it's not in other programming languages --- the conditional
statements you saw above use the `[[` and `]]` brackets for wrapping the
expression, but `for` loops generally do not use brackets or parenthesis in
Bash.
Here's what a `for` loop looks like in Bash:
```bash
for f in * ; do
echo $f
done
```
* The `for` is ... `for`, it's the start of the loop.
* The `f` is the name of the variable you want to use as the name for the value
in the current iteration of the loop over the sequence.
* The `in` is a separator between the variable name and the sequence.
* In this case `*` *is* the sequence. This is a "glob" or a pattern, and this
glob in the shell means "all files in the current directory".
* The semi-colon `;`, like in conditional statements above, ends the current
statement.
* `do`, then is the beginning of the body of the loop.
* `echo $f` is one command you want run on the variable. This will print out
the variable's name.
* `done` ends the body of the loop.
::: aside
Maybe this looks sort of familiar. Maybe this looks like what we were doing with
`find` and `-exec`. They do accomplish similar results!
Both work, and both are effective. One way to think about this matching of ideas
is that `find` and `-exec` are more of a functional programming paradigm (this
is a `map` operation), `for` loops are more of a procedural paradigm.
:::
When you're writing `for` loops, the sequence can either use the patterns you've
seen before (like `*.md`), or can be the result of a *command*.
In fact, we can rewrite the `for` loop from above using `find`!
```bash
for f in $(find . -maxdepth 1) ; do
echo $f
done
```
The output looks a little bit different, but the result is the same.
Another common kind of loop you may want to write is one that iterates over a
sequence of numbers (like the traditional `for` loop you've seen in languages
like Java). To do that you can use the `seq` command:
```bash
for num in $(seq 1 10) ; do
echo $num
done
```
Further reading
---------------
Just like programming, shell scripting goes way beyond what you've been
introduced to here. You've got a good start, but as you keep working with shell
scripting, you'll find yourself running into situations where you need to get
some more help.
* You can read the manual page for your shell to learn more about its scripting
language (e.g., `man bash` or `man tcsh`).
* Sections of interest in the manual page for `bash` include the
`CONDITIONAL EXPRESSIONS` section and the `Compound Commands` subsection.
* Joshua Levy's "[The Art of Command Line]" is a very good resource that's been
translated into many languages. It's a good reference to keep in your
bookmarks.
* [ShellCheck] is a tool for identifying and then helping you fix possible bugs
in your shell scripts.
* The [Advanced Bash-Scripting Guide] is a comprehensive guide for shell
scripting with Bash.
[The Art of Command Line]: https://github.com/jlevy/the-art-of-command-line
[ShellCheck]: https://www.shellcheck.net/
[Advanced Bash-Scripting Guide]: https://tldp.org/LDP/abs/html/