Shell scripting I – variables and arguments
Week 7 – Lecture A
1 Introduction
1.1 Learning goals
This week, you will learn how to write shell scripts to run programs with command-line interfaces (CLIs), like various bioinformatics tools. In this session, we will talk about:
- What shell scripts are
- Writing a shell script that runs FastQC
- Boilerplate shell script header lines: shebang and safe settings
- Shell variables
- Command-line arguments to scripts
- “Logging” statements that report what a script is doing
1.2 Getting ready
- At https://ondemand.osc.edu, start a VS Code session in
/fs/ess/PAS3493/people/$USER. - Open a terminal and create and then navigate to a
week07dir. - Within the
week07dir, create a dirscripts(but don’t navigate there).
2 Introduction to shell scripts
Running command-line programs
Last week, we saw a first example of running a bioinformatics tool (FastQC) from the command line. This was quite similar to how we ran basic shell commands in the previous weeks, both in terms of the structure of the commands (commands, options, and arguments), and how we ran them interactively in the terminal.
And while many bioinformatics tools are run with similarly structured commands, it’s best not to run them interactively in the terminal for real analyses. Instead, it’s better to run them via shell scripts: plain-text files that contain shell code and can be executed as a single command, and be submitted as batch jobs that will run even if you log out of the system.
Why use shell scripts
Some general reasons why it can be beneficial to use shell scripts instead of running code interactively line-by-line:
- It is a good way to save and organize your code.
- You can easily rerun scripts and re-use them in similar contexts.
And importantly for our purposes at OSC, we can submit scripts as “batch jobs”, which allows us to:
- Easily perform long-running analyses (taking many hours or even multiple days)
- Run analyses many times simultaneously, such as for different files/samples
- Run analyses while being able to disconnect from the system. We can submit a script, log out from OSC and shut down our computer, and it will still run.
Most of these reasons don’t apply (as much) to quick shell commands, but they are very important when running bioinformatics tools. We’ll talk about batch jobs in two weeks, and will start learning about shell scripts today.
So far, we’ve mostly talked about the Unix shell and shell scripts. From today onwards, we’ll also see the term “Bash” more.
Recall the difference we talked about in the Unix shell intro session: there are multiple Unix shell language variants and the specific one we’ve been using is Bash (which is by far the most common).
Our shell scripts are therefore in the Bash language, can be specifically called Bash scripts, and can be run with the bash command.
3 Your first shell scripts
Let’s start with the simplest possible kind of script: one that contains a single command. Create a script hello.sh (shell scripts usually have the extension .sh) as follows:
# Create an empty file
touch scripts/hello.shOpen the file in your VS Code editor pane, and type or paste the following line inside the script, and save it:
scripts/hello.sh
echo "Hello world"That’s it — this one-line file is a functional shell script! One way of running a script is by typing bash followed by the path to the script:
bash scripts/hello.shHello world
Running the script has the exact same effect as typing echo "Hello world" directly in the terminal.
Let’s make one more very basic script and run it:
touch scripts/info.shscripts/info.sh
date
pwdbash scripts/info.shMon Oct 5 14:13:31 EDT 2026
/fs/ess/PAS3493/people/jelmer/week07
This shows the basic idea of a shell script: when you run it, its commands are executed one by one, from top to bottom, just as if you had typed them in the terminal. (Also note that pwd printed week07, not week07/scripts: commands in a script run in your working dir, not in the dir where the script is stored.)
In practice, you wouldn’t bother writing a script for simple commands like these. Scripts do pay off for commands that you want to save and rerun, and/or that take a long time to run, such as those that run bioinformatics tools. So let’s move on to a more useful example.
4 A script to run FastQC
4.1 The initial script
In the previous lecture, you ran FastQC for a FASTQ file by executing the following commands directly in the terminal:
mkdir -p results/fastqc
apptainer exec oras://community.wave.seqera.io/library/fastqc:0.12.1--104d26ddd9519960 \
fastqc --outdir results/fastqc ../garrigos/fastq/S63_R1.fastq.gzLet’s put these commands in a shell script instead. Create a script fastqc.sh:
touch scripts/fastqc.shOpen the file and type or paste the following inside the script:
scripts/fastqc.sh
# Create the output dir if needed
mkdir -p results/fastqc
# Run FastQC with its container
apptainer exec oras://community.wave.seqera.io/library/fastqc:0.12.1--104d26ddd9519960 \
fastqc --outdir results/fastqc ../garrigos/fastq/S63_R1.fastq.gz
Like your first two scripts, this script simply contains shell commands — a couple of notes:
The output dir is created with
mkdir -p
Recall that FastQC won’t create the output dir for you. Usingmkdir -pinside the script ensures that the output dir exists, whether or not it existed before running the script (see the box below for details).TipMore about the-poption tomkdir(Click to expand)Using
mkdir’s-poption does two things at once, both of which are very useful when including this command in a script:It will enable
mkdirto create multiple levels of directories at once (i.e., to act recursively): by default,mkdirerrors out if the parent directory/ies of the specified directory don’t yet exist.mkdir newdir1/newdir2mkdir: cannot create directory ‘newdir1/newdir2’: No such file or directory# This successfully creates both directories: mkdir -p newdir1/newdir2If the directory already exists, with
-p,mkdirwon’t do anything and won’t return an error. Without this option,mkdirwould return an error, which would in turn make the script stop (given the settings you’ll learn about below):mkdir newdir1/newdir2mkdir: cannot create directory ‘newdir1/newdir2’: File exists# This does nothing since the dirs already exist mkdir -p newdir1/newdir2
The file paths are relative to your working dir, not to the script
As you saw withinfo.sh, relative paths in a script, likeresults/fastqc, are interpreted relative to your working dir at the time you run the script, not relative to where the script itself is stored. This means that this script only works as intended if you run it from yourweek07dir: from any other dir,../garrigos/fastq/S63_R1.fastq.gzwould point to a non-existent file.
Let’s run the script:
bash scripts/fastqc.shINFO: Using cached SIF image
INFO: gocryptfs not found, will not be able to use gocryptfs
application/gzip
Started analysis of S63_R1.fastq.gz
Approx 5% complete for S63_R1.fastq.gz
Approx 10% complete for S63_R1.fastq.gz
Approx 15% complete for S63_R1.fastq.gz
# [...output truncated...]
Analysis complete for S63_R1.fastq.gz
Nice! Next, you’ll learn about two header lines that are good practice to add to every shell script. But first, let’s install a VS Code extension that will alert you to the first of these.
4.2 Install the Shellcheck extension
Shellcheck is a really useful VS Code extension that checks your shell scripts for errors and bad practices. Let’s install it:
- In the narrow side bar, click on the Extensions icon.
- In the wide side bar, search for shellcheck (by timonwong) and install it.
Bizarrely, some changes to Shellcheck appear to have been made in the first days of October that are currently breaking it in Code Server, but that I didn’t experience as I installed this extension a little earlier.
Switching the “Shellcheck runtime” setting from native to wasm seems to fix it. Open the VS Code settings: → Settings, and search for “shellcheck runtime”:
Change this setting to from native to wasm:
Now, hopefully, Shellcheck will work!
4.3 Shebang line
If Shellcheck is working, you should now see a red squiggly line on the first line of your fastqc.sh script:
Hover over it to see the warning: Shellcheck wants you to add a “shebang” line. (More generally, keep an eye out for these squiggly lines — Shellcheck will also flag things like typos in variable names.)
A so-called “shebang” line is used as the very first line of a script to indicate which computer language the script uses. Add it to the top of your fastqc.sh script:
It starts with #! (hash-bang), followed by the path to the program that should run the script: here, Bash, whose executable is located at /bin/bash. Adding a shebang line to every shell script is good practice, especially when you submit your script to OSC’s Slurm queue, as we’ll do later.
4.4 Safe shell settings
Some default settings of the Bash shell are bad ideas inside scripts:
- A script keeps running after a command fails, executing all remaining lines anyway. At best, this wastes time, but it can also produce results downstream of the error that are wrong — and if your script prints a lot of output, you may not even notice the error message.
- When you reference a non-existent (“unset”) variable, the shell silently replaces it with nothing (you’ll learn more about variables below).
To make shell scripts safer and error out when such things happen, we can apply a few “safe” settings by adding a single line of code to the script. Add this line to the top of your fastqc.sh script, right after the shebang line:
set command near the top of the script?
The safe shell settings will apply to the lines of code in the script that come after the set command, which is why it’s important to place the set command near the top of the script. This way, the settings will be in effect for the entire script.
With these settings, the script stops with an error message if:
-e— a command fails.-u— a non-existent variable is referenced.-o pipefail— any command in a shell “pipeline” (e.g.,sort | uniq) fails. (Without this option,-eonly notices failures of the last command in a pipeline.)
4.5 Rerunning the script with the new header lines
Your fastqc.sh script should now contain the following:
scripts/fastqc.sh
#!/bin/bash
set -euo pipefail
# Create the output dir if needed
mkdir -p results/fastqc
# Run FastQC with its container
apptainer exec oras://community.wave.seqera.io/library/fastqc:0.12.1--104d26ddd9519960 \
fastqc --outdir results/fastqc ../garrigos/fastq/S63_R1.fastq.gzRun the script again (this will overwrite the output files from the previous run, which is fine):
bash scripts/fastqc.shINFO: Using cached SIF image
INFO: gocryptfs not found, will not be able to use gocryptfs
application/gzip
Started analysis of S63_R1.fastq.gz
Approx 5% complete for S63_R1.fastq.gz
# [...output truncated...]
Analysis complete for S63_R1.fastq.gz
That didn’t change the output, but we confirmed that the script still works.
bash command? (Click to expand)
By default, you need to run a script with the bash command, like bash scripts/fastqc.sh. When doing so, you are telling the computer to run the script with Bash (and the shebang line is actually ignored; Bash will be used to run the script regardless of what the shebang line says).
It’s possible to execute scripts by only typing their path, using syntax like:
# If the script is in a different dir:
scripts/fastqc.sh
# If the script is in your working dir:
./fastqc.shTo be able to do this, the script:
Must have a shebang line so the computer knows which language to execute the script with
Must be “executable”. You can make it executable by running:
# Add 'execute permissions' for fastqc.sh: chmod +x scripts/fastqc.sh
Finally, why do you need ./ in the second example above (./fastqc.sh), where the script is in your working dir? The ./ is necessary to make explicit that you’re referring to a file name and not a command. With yet another step, it is possible to add your script to the computer’s registry of commands/programs, but we won’t cover that here (see this optional self-study page if you’re curious).
4.6 The problem with “hard-coded” file paths
Your script works, but it can only do one thing: run FastQC for the file S63_R1.fastq.gz, with output going to results/fastqc. These file paths are “hard-coded” in the script: to run FastQC for the R2 file of the same sample, or for any other sample, you would have to create a new script or edit the script every time.
It would be better to make the script flexible so that it can run FastQC for any FASTQ file, with output going to any output dir. We’ll do this in two steps:
- Using shell variables to define the file paths only once, at the top of the script
- Using command-line arguments to pass the file paths to the script when you run it, so you won’t have to edit the script at all
5 Shell variables
5.1 Variables
In week 3, you learned about environment variables like $USER and $HOME, which are pre-set by the system. But you can also define your own variables in the shell and in scripts, which is especially useful for items that:
- Are referred to repeatedly and/or
- Are subject to change
For example, variables may be useful for settings like the paths to input and output files and parameter values for programs. When using variables, these can be changed in one place (the variable definition) instead of having to change them in multiple places throughout your code.
Assigning and referencing variables
To assign a value to a variable in the shell, use the syntax variable_name=value:
# Assign a FASTQ file path to a variable with the name "fastq":
fastq=../garrigos/fastq/S63_R1.fastq.gz
# Assign an output dir path to a variable with the name "outdir":
outdir=results/fastqcA variable can contain any value: besides file paths, it could for example be any word (e.g., treatment=inoculated) or number (e.g., nr_samples=200).
No spaces are allowed around the equals sign: outdir = results/fastqc fails, because the shell would try to run a command called outdir.
To reference a variable (i.e., to access its value) by its name:
- Always put a dollar sign
$in front of it:outdirbecomes$outdir. - To be safe, also double-quote it:
$outdirbecomes"$outdir".
As before with the environment variables $USER and $HOME, use echo to see what values your variables contain:
echo "$fastq"../garrigos/fastq/S63_R1.fastq.gz
echo "$outdir"results/fastqc
More usefully, you can use a variable anywhere you would otherwise type its value. For example:
ls -lh "$fastq"-rw-rw----+ 1 jelmer PAS0471 21M Sep 9 13:46 ../garrigos/fastq/S63_R1.fastq.gz
mkdir "$outdir"mkdir: cannot create directory ‘results/fastqc’: File exists
(We get an error here because the dir already exists, and we didn’t use the -p option, but that proves that the command was executed as intended.)
Some rules for variable names in the shell — they:
- Can contain letters, numbers, and underscores
- Cannot contain spaces, periods (
.), dashes (-), or other special symbols1 - Cannot start with a number
Ideally, variable names are descriptive, like $fastq and $outdir above and not cryptic like $x.
Like with file names, you can distinguish words using capitalization (“camelCase”, $inputFile) or underscores (“snake case”, $input_file). In this course, we’ll mostly use lowercase variable names, except for “constants” in scripts, which we’ll write in uppercase — see the box at the end of the arguments section.
5.2 Quoting variables
I have mentioned that it is good practice to quote variables (i.e. to use "$outdir" instead of $outdir). One reason is that quoting variables prevents “word splitting”, which is when the shell interprets spaces in a variable’s value as separators between multiple words. Let’s look at an example:
Say that you have a variable
today, and its value contains spaces:today="Mon, Oct 5"Printing the value of the variable with or without quotes doesn’t seem to make a difference:
echo "$today"Mon, Oct 5echo $todayMon, Oct 5Next, you try to create a file with a name that includes this variable, and you don’t quote it:
touch README_$today.txt ls5.txt Oct README_Mon, results scriptsOops! The shell performed “word splitting” to split the value into three separate units. As a result, three files were created:
5.txt,Oct, andREADME_Mon,.This can be avoided by quoting the variable:
touch README_"$today".txt # The 4th file listed below is the one we intended to create all along: ls5.txt Oct README_Mon, 'README_Mon, Oct 5.txt' results scripts
This is the same behavior you saw with spaces in file names in week 4, where rm -r extra data removed the data dir instead of the extra data dir: the shell treats spaces as separators between arguments.
The difference is that with a variable, the space is hidden inside its value, so you can’t see it when you look at the command — which is why it’s a good habit to always quote variables. And just like "$today" above, quoting a file name (rm -r "extra data") is one way to prevent the problem.
Not quoting a variable can also cause confusion about where the variable name ends (see the exercise below).
Self-study exercise 1: Where does a variable name end?
First, assign a sample ID to a variable:
sample_id="A1"For each command below, first predict what it will print, and then run it to check your prediction.
echo $sample_id.fastq.gzClick for the solution
This works as intended, because a period can’t be part of a variable name, so the shell knows that the variable name is
sample_id:A1.fastq.gzecho $sample_id_R1.fastq.gz(the intent is to printA1_R1.fastq.gz)Click for the solution
This prints only
.fastq.gz!Underscores and letters can be part of a variable name, so the shell looked for a variable called
sample_id_R1. That variable doesn’t exist, and the shell silently replaced it with nothing (!):.fastq.gzecho "Processing sample: $sample_ID"Click for the solution
Variable names are case-sensitive, so
$sample_IDis a different, non-existent variable. Again, the shell doesn’t complain, it just replaces it with nothing:Processing sample:This is why the
set -usetting you added to your script is so useful: it makes a script fail with an error message instead.
Finally:
Fix the command from item 2 so that it prints
A1_R1.fastq.gz.Click for the solution
Quote the variable, so that the shell knows where the variable name ends:
echo "$sample_id"_R1.fastq.gzA1_R1.fastq.gz
By double-quoting a variable, you essentially escape (“turn off”) the default special meaning of the space as a word separator, asking the shell to interpret it instead as a literal space.
Similarly, double quotes will escape other special characters in the shell, like shell wildcards. Compare:
Due to shell expansion, this will show all files in the current working dir:
echo *5.txt Oct README_Mon, README_Mon, Oct 5.txt results scriptsBut with the
*enclosed in double quotes, it will simply print the literal “*” character:echo "*"*
However, double quotes do not turn off the special meaning of $ (which is to denote a string as a variable):
With double quotes, the shell will still interpret
$todayas a variable:echo "$today"Mon, Oct 5With single quotes, the shell will interpret
$todayliterally as a string, and not as a variable:echo '$today'$today
${myvar} (Click to expand)
The $var notation to refer to a variable in the shell is actually an abbreviation of the full notation, which includes curly braces:
echo ${today}Mon, Oct 5
Putting variable names between curly braces will also make it clear where the variable name begins and ends (so echo ${sample_id}_R1.fastq.gz would also have solved item 4 of the exercise above), although it does not prevent word splitting:
touch README_${today}_final.txt
ls -1 *final*5_final.txt
But you can combine curly braces and quoting:
touch README_"${today}"_final.txt
ls -1 *final* 5_final.txt
'README_Mon, Oct 5_final.txt'
5.3 Using variables in your FastQC script
Now, let’s change the FastQC script to use variables for the container URI, the input file, and the output dir:
scripts/fastqc.sh
#!/bin/bash
set -euo pipefail
# FastQC container
CONTAINER=oras://community.wave.seqera.io/library/fastqc:0.12.1--104d26ddd9519960
# Define in- and outputs
fastq=../garrigos/fastq/S63_R1.fastq.gz
outdir=results/fastqc
# Create the output dir if needed
mkdir -p "$outdir"
# Run FastQC with its container
apptainer exec "$CONTAINER" fastqc --outdir "$outdir" "$fastq"This is an improvement: the container URI and file paths are now clearly defined in one place at the top of the script, and the output dir, which is used twice, would only need to be changed once.
(Also, the apptainer exec command has become short enough to fit on a single line. But why is CONTAINER in uppercase? See the box on “constants” below.)
However, you’d still need to edit the script to run it for a different FASTQ file. Let’s see how we can make this more flexible using command-line arguments.
While editing the script, you may have noticed orange squiggly lines under variable names, for example:
Above, it’s warning that the script has defined variables that are not used (yet). That’s a good thing to know about, because it may indicate a typo in the variable name, or that you forgot to use the variable in your code. (Conversely, Shellcheck also warns you when you reference a variable that hasn’t been assigned.)
If you finished the script as shown above, the squiggly lines should disappear.
6 Command-line arguments for scripts
When you run a script, you can pass arguments to it, such as an input file and an output dir. That way, one script can process any file without being edited. And you can run the same script for many files at once (in parallel), which can dramatically speed up your analysis. All shell scripts we’ll write in this course will accept arguments.
6.1 Executing a script with arguments
Executing a script with arguments is just like providing a command like ls with arguments:
Pass two file names as arguments to
ls:ls data/sampleA.fastq.gz data/sampleB.fastq.gzPass two arguments, an input file and an output dir, to a script:
bash scripts/fastqc.sh data/sampleA.fastq.gz results/fastqc
6.2 Accessing arguments inside the script
Inside the script, any arguments you pass are automatically assigned to variables named after their position:
- Any first argument will be assigned to the variable
$1(above:data/sampleA.fastq.gz). - Any second argument will be assigned to
$2(above:results/fastqc). - Any third argument will be assigned to
$3— and so on.
However, the script won’t use these “placeholder” variables unless you reference them in your code. Let’s see how to do that.
$#contains the number of command-line arguments passed to the script (this can be useful for validation — see this section of the optional self-study page).$0contains the script’s path (as typed when running it, e.g.scripts/fastqc.sh)2.$@contains all command-line arguments passed to the script. This can be useful when you want a script to accept any number of input files, which you could then process with a loop likefor file in "$@"; do ...(loops are covered in the next session). Always put$@in double quotes ("$@"), so that each argument is kept as a separate item.
6.3 Using arguments in your FastQC script
In your FastQC script, let’s replace the hard-coded file paths with $1 and $2:
scripts/fastqc.sh
#!/bin/bash
set -euo pipefail
# FastQC container
CONTAINER=oras://community.wave.seqera.io/library/fastqc:0.12.1--104d26ddd9519960
# Copy the placeholder variables
fastq="$1"
outdir="$2"
# Create the output dir if needed
mkdir -p "$outdir"
# Run FastQC with its container
apptainer exec "$CONTAINER" fastqc --outdir "$outdir" "$fastq"Only the two lines that define $fastq and $outdir changed: their values now come from the command line instead of being hard-coded.
You could simply use the $1-style variables directly throughout your script, like so:
fastqc.sh (alternative - don't use)
apptainer exec "$CONTAINER" fastqc --outdir "$2" "$1"But I recommend always copying them to descriptively named variables like we did here, because:
- It will make your script easier to understand for others and for your future self
- It will make it less likely that you accidentally mix up the variables
Now, you can run the script for any FASTQ file without editing it — for example, for both FASTQ files of sample S63:
bash scripts/fastqc.sh ../garrigos/fastq/S63_R1.fastq.gz results/fastqcStarted analysis of S63_R1.fastq.gz
Approx 5% complete for S63_R1.fastq.gz
# [...output truncated...]
Analysis complete for S63_R1.fastq.gz
bash scripts/fastqc.sh ../garrigos/fastq/S63_R2.fastq.gz results/fastqcStarted analysis of S63_R2.fastq.gz
Approx 5% complete for S63_R2.fastq.gz
# [...output truncated...]
Analysis complete for S63_R2.fastq.gz
6.4 Safe settings in action
Our script currently requires two arguments: the input FASTQ file and the output dir. No default values are set for these variables, so if you forget to pass them to the script, it won’t be able to run successfully. Let’s see what happens exactly if you don’t pass arguments to the script:
bash scripts/fastqc.shscripts/fastqc.sh: line 8: $1: unbound variable
Thanks to our safe settings with set, the script stops right away, with an error message that tells you exactly what went wrong: the variable $1 doesn’t exist (“is unbound”).
Without this setting, the script would have continued with empty values for $fastq and $outdir, leading to much more confusing error messages downstream.
In-class exercise 1: A script with two arguments
Create a new shell script scripts/printname.sh, with the following contents:
scripts/printname.sh
#!/bin/bash
set -euo pipefail
first_name="$1"
last_name="$2"
echo "First name: $first_name, last name: $last_name"In each scenario described below, think about what might happen, then run the code to test your prediction.
With the arguments
JohnandDoe:bash scripts/printname.sh John DoeClick for the solution
This is the intended use of the script, and it works as expected:
bash scripts/printname.sh John DoeFirst name: John, last name: DoeDouble-quoting the entire name:
bash scripts/printname.sh "John Doe"Click for the solution
Because we are quoting
"John Doe", both names are passed as a single argument, and both names end up in$1, the “first name”.Then, because no second argument was passed, the script stops when trying to copy
$2thanks to thesetsettings:bash scripts/printname.sh "John Doe"scripts/printname.sh: line 5: $2: unbound variableIf we had not used the
setsettings, the script would have continued with an empty value for$last_name:First name: John Doe, last name:Passing three names:
bash scripts/printname.sh John Michael DoeClick for the solution
The script runs without errors, but prints the wrong last name: the third argument,
Doe, ends up in$3, which the script never uses. Note thatset -udoesn’t help here: it only catches references to non-existent variables, not unused arguments.bash scripts/printname.sh John Michael DoeFirst name: John, last name: MichaelRemoving the line with
setsettings from the script, and then running it without passing arguments to it:bash scripts/printname.shClick for the solution
The script will run in its entirety and not throw any errors, because we are now using default Bash settings such that referencing non-existent variables does not throw an error. Of course, no names are printed either, since we didn’t specify any:
bash scripts/printname.shFirst name: , last name:
Self-study exercise 2: Quoting arguments
What do you expect the output to be when you run your printname.sh script from In-class exercise 1 as follows? Make a prediction, then run it to check.
bash scripts/printname.sh "$USER" '$USER'Hint: see the box on double vs. single quotes above.
Click for the solution
The shell expands variables before passing arguments to the script, so the first argument is your username. The single quotes prevent that expansion, so the second argument is the literal string $USER:
First name: jelmer
Last name: $USER
The shell’s default behavior of ignoring the referencing of unset variables can lead to accidental file removal in a couple of ways. Here are two examples:
Using a variable, you try to remove temporary files whose names start with
temp_:temp_prefix="temp_" rm "$tmp_prefix"*Using a variable, you try to remove a temporary directory:
tempdir=output/tmp rm -rf $tmpdir/*
In both examples, there is a similar typo: temp vs. tmp, which means that we are referencing a (likely) non-existent variable.
In the first example,
rm "$tmp_prefix"*would have been interpreted asrm *, because the non-existent variable is simply ignored. Therefore, we would have removed all files in the current working directory.In the second example, along similar lines,
rm -rf $tmpdir/*would have been interpreted asrm -rf /*. This would attempt to remove the computer’s entire filesystem: recall that a leading/in a path is a computer’s root directory (-rmakes the removal recursive and-fforces removal).
These kinds of accidents are especially likely to happen inside scripts, where it is common to use variables and to work non-interactively.
Luckily, you can’t remove system files this way, neither at OSC nor (without “admin” rights) on your own computer. But rm -rf /* would still remove every file that you do have permission to remove. At OSC, that includes your Home dir and project dirs — and since project files are often group-writable (see the rw-rw---- in ls -l output), possibly also your labmates’ files! This is one more reason to always use set -u in your scripts.
Our FastQC script gets the input file and output dir from arguments, but the container URI is still hard-coded in the script. That’s fine: you could also use arguments for settings like the container or program options (e.g., a quality threshold), but a long list of arguments makes a script confusing to use.
When you do hard-code settings that may change, store them in variables that you clearly define at the top of your script, like we did with CONTAINER. Such variables are often called “constants”: they make the script easier to maintain, but unlike variables that get their values from arguments, they can’t be changed from outside the script.
In this course, we’ll name constants in uppercase (e.g., CONTAINER) and most other variables in lowercase, so the two are easy to tell apart. (Conventions vary, though: some people use uppercase for all shell variables, and environment variables like $USER and $HOME are always uppercase.)
7 “Logging” statements
7.1 Add logging statements to your script
It is often useful to have your shell script report or “log” what it is doing. For instance:
- The date and time at which the script was run
- The arguments that were passed to the script
- The designated output dirs/files
- Perhaps even summaries of the output (we won’t do this here)
This information can help with troubleshooting and record-keeping.
Let’s add some code to the FastQC script to make it produce logging information:
scripts/fastqc.sh
#!/bin/bash
set -euo pipefail
# FastQC container
CONTAINER=oras://community.wave.seqera.io/library/fastqc:0.12.1--104d26ddd9519960
# Copy the placeholder variables
fastq="$1"
outdir="$2"
# Initial logging
echo "Starting script fastqc.sh"
date
echo "Input FASTQ file: $fastq"
echo "Output dir: $outdir"
echo
# Create the output dir if needed
mkdir -p "$outdir"
# Run FastQC with its container
apptainer exec "$CONTAINER" fastqc --outdir "$outdir" "$fastq"
# Final logging
echo
echo "Used FastQC version:"
apptainer exec "$CONTAINER" fastqc --version
echo
echo "Successfully finished script fastqc.sh"
dateA couple of notes about the lines that were added to the script above:
A “marker line”
Successfully finished scriptindicates that the end of the script was reached. This is useful because of yoursetsettings: seeing this line printed means that no errors were encountered.Running
dateat the beginning and end of the script is one way to be able to see how long the script took to run, and to keep a record of when it was run.Printing the input file names can be particularly useful for troubleshooting.
The lines that only have
echowill simply print a blank line, basically as a separator between sections.For scripts that run bioinformatics tools, it’s a good idea to include a line that prints the program version.
7.2 Run your final FastQC script
Make sure you’ve updated your scripts/fastqc.sh script with the code from the box above, and then run your final FastQC script again for one FASTQ file:
bash scripts/fastqc.sh ../garrigos/fastq/S63_R1.fastq.gz results/fastqcStarting script fastqc.sh
Mon Oct 5 14:23:13 EDT 2026
Input FASTQ file: ../garrigos/fastq/S63_R1.fastq.gz
Output dir: results/fastqc
INFO: Using cached SIF image
INFO: gocryptfs not found, will not be able to use gocryptfs
application/gzip
Started analysis of S63_R1.fastq.gz
Approx 5% complete for S63_R1.fastq.gz
# [...output truncated...]
Analysis complete for S63_R1.fastq.gz
Used FastQC version:
INFO: Using cached SIF image
INFO: gocryptfs not found, will not be able to use gocryptfs
FastQC v0.12.1
Successfully finished script fastqc.sh
Mon Oct 5 14:23:19 EDT 2026
Great! This logging information is useful as a record of what was done, and for example allows you to see that the script took only 6 seconds to run for this small FASTQ file. Though currently, the logging information is only printed to the screen, which is not ideal for record-keeping. In two weeks, we’ll run scripts as batch jobs, and the logging information will be printed to a text file that you can keep along with the output files.
8 Recap and next steps
In this session, you’ve learned the basics of shell scripts, using a script that runs FastQC as an example. As you’ve seen, shell scripts mostly consist of “regular” Unix shell code, but with some added bells and whistles like:
- Boilerplate shell script header lines: shebang and safe settings
- Variables, to clearly define settings like file paths in one place
- Command-line arguments, which are available in variables in the script and make the script flexible
- Logging statements, which report what the script is doing
In the next class, you will learn how to write loops to easily run a script many times, such as for every FASTQ file or every sample in a dataset.