Module 5
Live-Coding Lab: Putting it all together
Writing your first script
We are finally ready to see what makes the shell such a powerful programming environment. We are going to take the commands we repeat frequently and save them in files so that we can re-run all those operations again later by typing a single command. For historical reasons, a bunch of commands saved in a file is usually called a shell script, but make no mistake: these are actually small programs.
Not only will writing shell scripts make your work faster– you won’t have to retype the same commands over and over again– it will also make it more accurate (fewer chances for typos) and more reproducible. If you come back to your work later (or if someone else finds your work and wants to build on it) you will be able to reproduce the same results simply by running your script, rather than having to remember or retype a long list of commands.
For this example, we will be working with one of our .gbff files. Let’s say that we want to know how many locus_tag identifiers in this file. To start, you can inspect the atlanta.gbff file with less to see if there is any text that can help us achieve this.
Remember that you can navigate the less interface with your arrow keys, and quit with the q key.
You may notice that our Locus Tags are always preceded by the text /locus_tag=. With this in mind, we now have something which we can count inside our .gbff file, and therefore we are ready to create our first script!
Let’s start by going back to data/genomes and creating a new file, count_tags.sh which will become our shell script:
The command nano count_tags.sh opens the file count_tags.sh within the text editor ‘nano’ (which runs within the shell). If the file does not exist, it will be created. We can use the text editor to directly edit the file – we’ll simply insert the following line:
Then we save the file (Ctrl-O in nano), and exit the text editor (Ctrl-X in nano). Check that the directory molecules now contains a file called count_tags.sh.
Once we have saved the file, we can ask the shell to execute the commands it contains. Our shell is called bash, so we run the following command:
atlanta.gbff 12237
Sure enough, our script’s output is exactly what we would get if we ran that pipeline directly. You can test this by pasting the line starting with grep into your shell to see what happens:
atlanta.gbff 12237
As expected, the result is the same. The benefit of a script is that you can repeat this operation without typing a new command each time.
Let’s inspect how our script is working under the hood. Instead of outputting the count of /locus_tag= matches, let’s see what we are finding with our grep command. We will use the head program to catch the output, since we can expect from our previous result that there are 12237 matches (which we DON’T want to print to our shell output):
/locus_tag="AYP09_RS32715"
/locus_tag="AYP09_RS32715"
/locus_tag="AYP09_RS32720"
/locus_tag="AYP09_RS32720"
/locus_tag="AYP09_RS00575"
/locus_tag="AYP09_RS00575"
/locus_tag="AYP09_RS00580"
/locus_tag="AYP09_RS00580"
/locus_tag="AYP09_RS00585"
/locus_tag="AYP09_RS00585"By visual inspection, we notice that some locus tags appear twice in the .gbff file so we’re double-counting a lot of tags (but not all, since our previous count appeared to be odd. Let’s modify our script to only count unique tags.
First, open the script with nano:
Now, using the arrow keys to navigate, modify our script with the sort and uniq programs before we count our resultant lines with wc -l:
Now when we run the script:
atlanta.gbff 6109
Now we can be confident that we are not double-counting our locus tags.
What if we want to count the number of locus tags in many files? Let’s introduce a new concept: looping!
Open up our script again:
We are going to give our script a “routine,” better known as a loop, to execute when we run it. We do this by using specific commands within the script, namely for and do. Let’s modify our script until it looks like the following:
for filename in atlanta.gbff london.gbff
do
echo -n "$filename "
grep "/locus_tag=" $filename | sort | uniq | wc -l
doneNote that while the indentations/spaces between do and done are not necessary in bash scripts, it is best practice to use them for keeping track of your loops.
Let’s explain what’s happening:
In lay terms, the first line is telling the shell that we want to do something (which has yet to be defined) for every element in an array (or “list”).
The loop executes all of the commands between do and done for each time the loop iterates, and each time, the variable $filename is replaced by either atlanta.gbff or london.gbff since these are the elements that were in our **array*.
What if we wanted to count the number of tags in other .gbff files?. At the moment, the filenames are hard-coded into our script. It only counts tags in atlanta.gbff and london.gbff. We can make the script a little bit more flexible by using the variable names $1 and $2 in our array.
Now, within nano, replace the text atlanta.gbff with the special variable called $1. Remember to save your script when you exit!
for filename in $1 $2
do
echo -n "$filename "
grep "/locus_tag=" $filename | sort | uniq | wc -l
doneInside a shell script, $1 means ‘the first filename (or other argument) on the command line’. Similarly, $2 is the second argument passed to the script. We can now run our script like this:
atlanta.gbff 6109
london.gbff 5831
or on a different file like this:
nevada.gbff 5691
texas.gbff 5424
This is better, but our script still isn’t quite as flexible as it could be. It can only operate on two files at a time. What if I wanted to count the tags in three or four files at a time?
There is a special variable $@ which holds all of the arguments passed to the script. In other words, $@ can represent all of $1, $2, $3, and so on. Let’s make another modification to our script:
The “$@” variable gets replaced with all of the arguments passed to our script. This allows us to run:
atlanta.gbff 6109
braunschweig.gbff 5248
lab-strain.gbff 4609
london.gbff 5831
muenster.gbff 5176
nevada.gbff 5691
texas.gbff 5424
This works, but it may take the next person who reads count_tags.sh a moment to figure out what it does. We can improve our script by adding some comments at the top:
# Counts the number of unique locus tags in one or more gbff files.
for filename in $@
do
echo -n "$filename "
grep "/locus_tag=" $filename | sort | uniq | wc -l
doneA comment starts with a # character and runs to the end of the line. The computer ignores comments, but they’re invaluable for helping people (including your future self) understand and use scripts. The only caveat is that each time you modify the script, you should check that the comment is still accurate: an explanation that sends the reader in the wrong direction is worse than none at all.
Lastly, let’s make our script executable. First we’ll modify the file permissions. This can be done using the chmod command:
We’ll also add a “shebang” line to our script which tells the shell what program should be used to run our script. Modify our script to include this first line:
#!/bin/bash
# Counts the number of unique locus tags in one or more gbff files.
for filename in $@
do
echo -n "$filename "
grep "/locus_tag=" $filename | sort | uniq | wc -l
doneExercise: Can you create a command that uses our count_tags.sh script to find the gbff file with the smallest number of locus tags?
Key Points
- Save commands in files (usually called shell scripts) for re-use.
bash [filename]runs the commands saved in a file.$@refers to all of a shell script’s command-line arguments.$1,$2, etc., refer to the first command-line argument, the second command-line argument, etc.- Place variables in quotes if the values might have spaces in them.
- Letting users decide what files to process is more flexible and more consistent with built-in Unix commands.