Shell scripting II – rerunning scripts with loops

Week 7 – Lecture B

Presented on

October 7, 2026

Page last modified

October 17, 2026




1 Introduction

1.1 Learning goals

In the previous session, you wrote a shell script to run FastQC for a single FASTQ file. Here, we will focus on running that script many times with loops. Specifically, you will learn:

  • How for loops work
  • How to use a for loop to loop over files and run a shell script many times
  • How to keep a record of your analysis steps in a Markdown file
  • Self-study: How to use parameter expansion to search-and-replace text in variables
  • Self-study: How to loop over samples (e.g., pairs of FASTQ files) rather than individual files

1.2 The big picture

The strategy you’ve learned so far, and that we will stick to, is writing scripts that run a single program a single time. We do this even though you commonly need to run a CLI tool many times, e.g. separately for each file or sample. That’s because at OSC and other HPC systems, you can save a lot of time by submitting a separate batch job for each file or sample, since these jobs can all run simultaneously1. To accommodate this, you will:

  • Make your scripts flexible by passing arguments to them (as seen in the previous session)
  • Loop over files/samples outside of the script, and run/submit the script many times (as you’ll see today)

As a result, you’ll also type important code outside of your scripts, and that code orchestrates your workflow. You will store that code in Markdown files, which then provide a record of what you did.

1.3 Getting ready

  1. At https://ondemand.osc.edu, start a VS Code session in /fs/ess/PAS3493/people/$USER.
  2. In the terminal, navigate to your week07 dir.

2 Markdown setup

Today, and more commonly from now on, we will store some of our shell code in Markdown files rather than typing it directly in the terminal, for the reasons explained above.

I’ll demonstrate a “visual” Markdown editing approach in VS Code that is more similar to that of a word processor. You can then decide whether you want to switch to that approach from now on, or to keep using the “raw” Markdown editor.

  1. Create a new Markdown file in your week07 dir, which you’ll use as a scratch file to practice with loops — run the below command and open the file in the VS Code editor:

    touch loops-practice.md
  2. Copy-paste the following into your loops-practice.md — a skeleton for what we’ll do in this session:

    loops-practice.md
    # For loop practice
    
    - **Author**: <your-name>
    - **Date**: 2026-10-07
    - **Working dir**: `/fs/ess/PAS3493/people/<user>/week07`
    
    In this file, we'll practice writing **`for` loops** in the shell.
    
    ## A first shell loop
    
    ```{bash}
    
    ```
    
    ## Multiple actions per iteration
    
    ## Combining loops and globbing
    
    ## Exercise 1
    
    ## Exercise 2
    
    ## Self-study: Parameter expansion
    
    ## Self-study exercise 1
    
    ## Self-study: A per-sample loop
    
    ## Self-study exercise 2
  1. Find and install the Quarto extension in VS Code (click “Trust Publisher and Install” when prompted).



  1. Find the ... button in the top-right of the editor, and in the dropdown, select “Edit in Visual Mode”:

A screenshot showing the Quarto extension in VS Code.

A screenshot showing the editor's '...' dropdown menu, with 'Edit in Visual Mode' at the bottom.

“Edit in Visual Mode” is at the bottom of the dropdown menu.

            You may get a pop-up like this — click “Use Visual Mode” (the reformatting it warns about is harmless):

A screenshot showing the Quarto extension in VS Code.

  1. Then, the editor will look like in the screenshot below — similar to the Markdown preview you’ve seen before, but editable, and with formatting buttons across the top:

A screenshot showing the VS Code editor in Visual Mode.

  1. The main wrinkle with this setup is that in order to easily send code from the editor to the terminal (so you don’t have to copy-and-paste), we need to make code blocks with a ```{bash} instead of ```bash opening line2.

    Practice with creating a new code block below the “Multiple actions per iteration” header — type ```{bash} and then press Enter to turn it into a code block (you won’t have to type the closing ``` line!):

A screenshot showing the VS Code editor in Visual Mode with a new code block being created.

A screenshot showing the VS Code editor in Visual Mode with a new code block being created.

  1. To send the code in a code block to the terminal, you can click the Play button at the right end of the code block, or press Shift+Enter when your cursor is in the code block. We’ll practice this in a bit.

None of this is required, but it will make your life easier. From now on, we’ll write some of our code in Markdown files, and then want to send that code to the terminal without having to copy-and-paste. The visual editor has a pre-set keyboard shortcut for that, but the raw Markdown editor does not.

Set a keyboard shortcut to send code from the editor to the terminal.
This shortcut will send selected code in the editor to the terminal. When no text is selected, the shortcut sends the line your cursor is on (in VS Code, your entire line is implicitly “selected” when your cursor is on it). To run a multi-line command like a for loop, first select all of its lines.

  1. Click (bottom-left) > Keyboard Shortcuts.
  2. Type “Run Selected Text” in the search box, and you should see the option “Terminal: Run Selected Text in Active Terminal”.
  3. Click on that line, then add the shortcut Ctrl+Enter (don’t worry about any warnings that other keybindings exist for this action; also, the keyboard shortcut is up to you, but I’d recommend this one).

A screenshot showing the VS Code keyboard shortcuts settings with the 'Run Selected Text in Active Terminal' option selected.

Set a keyboard shortcut to create a new code block.
Unlike in Visual Mode, you can continue to work with the “regular” ```bash code blocks. But it’s tedious to type the opening and closing lines each time — and in this mode, it’s straightforward to set a keyboard shortcut to create a new code block.

  1. Open the Command Palette with Ctrl/Cmd+Shift+P, and look for “Preferences: Open Keyboard Shortcuts (JSON)” — you’ll want the JSON file option.

    A screenshot showing the VS Code keyboard shortcuts settings with the 'Insert Snippet' option selected.

  2. Copy-paste the following into the JSON file, and save it (select the correct block for your operating system). This sets a keyboard shortcut for the insertion of a so-called “code snippet” that creates a new code block:

[
  {
    "key": "ctrl+shift+i",
    "command": "editor.action.insertSnippet",
    "when": "editorTextFocus && editorLangId == markdown",
    "args": {
      "snippet": "```bash\n$0\n```\n"
    }
  }
]
[
  {
    "key": "cmd+shift+i",
    "command": "editor.action.insertSnippet",
    "when": "editorTextFocus && editorLangId == markdown",
    "args": {
      "snippet": "```bash\n$0\n```\n"
    }
  }
]

If your file already contains other shortcuts, add only the { ... } part (preceded by a comma) inside the existing [ ], rather than pasting a second set of square brackets.

  1. Now, you can press Ctrl/Cmd+Shift+I to create a new code block in your Markdown file.

3 For loops

Loops are used to repeat operations. Here, we’ll cover the most common type of loop, the for loop3, which:

  1. Iterates over (i.e., goes through, one by one) a pre-defined collection, such as a list of files.
  2. Uses one element of the collection in each iteration (or “round”) of the loop, allowing you to perform one or more actions for that element.

3.1 A non-coding example

In a way, you’re likely to already think in for loops regularly in your daily life. Say you have a rack with three tubes, and your protocol says:

For each tube in the rack: add 10 µL of buffer to the tube, vortex it, and spin it down.

Here’s what that instruction consists of:

  • A collection of items to go through: the tubes in the rack.
  • A set of actions that are repeated for every item: add buffer, vortex, spin down.
  • A placeholder that stands in for “the item I’m currently working on”: “the tube” or “it”. In the first round, “the tube” means tube 1; in the second round, tube 2; and so on.

You’d go through the tubes one at a time, doing all three actions for a tube before moving on to the next one. And you’re done when you run out of tubes: with three tubes, you’d do this three times. If we wrote that protocol step in for loop syntax, it would look like this (this is not real code!):

for tube in tube1 tube2 tube3; do
    add 10 µL buffer to $tube
    vortex $tube
    spin down $tube
done

In shell code, a for loop has the same three components:

  • The collection comes after in: here, tube1 tube2 tube3.
  • The actions go between do and done, in indented lines so it’s clear what is being repeated.
  • The placeholder is a variable, whose name you choose and put after for: here, tube. Inside the loop, you refer to it with $tube, and in each round of the loop, it contains a different item from the collection.

3.2 A first shell loop

Let’s type out the example below in your loops-practice.md and then run it in the terminal. Here, the collection is a short list of three fruits, and the only action is to print the current fruit.

In your code block below the “A first shell loop” header, type the following:

loops-practice.md (excerpt)
for fruit in apple banana plum; do
    echo "Fruit: $fruit"
done

Then, send the code to the terminal: either press the Play button at the right end of the code block, or press Shift+Enter with your cursor in the code block (this assumes your in visual editing mode).

You should see your code appear in the terminal along with the following output, which shows that the echo line ran three times, and that each time, the variable $fruit contained a different fruit:

for fruit in apple banana plum; do
    echo "Fruit: $fruit"
done
Fruit: apple
Fruit: banana
Fruit: plum

Under the hood, the following was actually run by the shell — three times, a value was assigned to $fruit, and then, the exact same echo command was run every time:

fruit=apple
echo "Fruit: $fruit"

fruit=banana
echo "Fruit: $fruit"

fruit=plum
echo "Fruit: $fruit"

A photo of a bowl of Froot Loops cereal.

© Kellogg’s

3.3 Multiple actions per iteration

Just like in the tube example, you can perform multiple actions in each round of the loop: every line between do and done is run for every item.

For the next example, we will count the number of characters in each fruit’s name. Here’s how to do that for a single word, where we add the -n option to echo to prevent it from adding a newline (which would otherwise be counted as a character too), and use the -c option of wc to count the number of characters rather than lines:

echo -n "apple" | wc -c
5

Below, for each fruit, we print its name and then count the number of characters in its name:

loops-practice.md (excerpt)
for fruit in apple banana plum; do
    echo "Fruit: $fruit"
    echo "Number of characters:"
    echo -n "$fruit" | wc -c
    echo
done
Fruit: apple
Number of characters:
5

Fruit: banana
Number of characters:
6

Fruit: plum
Number of characters:
4

Notice that all four lines ran for apple before any of them ran for banana: the loop completes all actions for one item before moving on to the next item.

3.4 Loop recap so far

To recap some key things you should understand about for loops:

  • In each iteration of the loop, one element in the collection is being assigned to the variable specified after for.
  • The loop will run exactly as many times as there are items in the collection.
  • The indented lines’ code is being repeated: all lines are run for one item before the loop moves on to the next4.
  • The variable name is up to you. Like with other variables, try to choose a name that is descriptive.
  • Like with other variables, use a $ when you reference (echo "$fruit") but not when you assign (for fruit).

On the first and last, unindented lines, for loops contain the following mandatory keywords:

Keyword Purpose
for After for, we set the variable name (an arbitrary name; above we used fruit)
in After in, we specify the collection (list of items) we are looping over
do After do, we have one or more lines specifying what to do with each item
done Tells the shell we are done with the loop

You may find the ; do at the end of the first line of the for loop to be odd. What is that all about?

First, let’s see what a semicolon does in the first place. It’s a way to enter multiple commands on a single line — for example, the line below would first run ls and then echo "Hello":

ls; echo "Hello"

(So, ; can be thought of as a counterpart to \, which allows you to spread a single command across multiple lines: with a ;, you can have multiple commands on a single line.)

Alternatively, you could write for loops like so:

for fruit in apple banana plum
do
    echo "Fruit: $fruit"
done

Some people prefer that variant. You can do it either way, but we will continue to demonstrate the ; do variant, which is more symmetric, and saves a line of code.

3.5 Combining loops and globbing

In an omics data analysis, you will often have many files that need to be processed in the same way. For example, we wrote a fastqc.sh script that runs FastQC for a single FASTQ file, and we want to run that script for all FASTQ files in a directory. We can loop over file names just like we looped over fruits above:

loops-practice.md (excerpt)
for fastq_file in ../garrigos/fastq/S63_R1.fastq.gz ../garrigos/fastq/S63_R2.fastq.gz; do
    echo "Now processing file: $fastq_file"
done
Now processing file: ../garrigos/fastq/S63_R1.fastq.gz
Now processing file: ../garrigos/fastq/S63_R2.fastq.gz

However, typing out all file names is tedious and error-prone. Instead, we can use globbing to automatically generate a list of file names, and then loop over that list, all in one line of code:

loops-practice.md (excerpt)
for fastq_file in ../garrigos/fastq/*fastq.gz; do
    echo "Running an analysis on $fastq_file"
    # [This is where you would put additional commands to process each FASTQ file]
done
Running an analysis on ../garrigos/fastq/S63_R1.fastq.gz
Running an analysis on ../garrigos/fastq/S63_R2.fastq.gz
Running an analysis on ../garrigos/fastq/S64_R1.fastq.gz
Running an analysis on ../garrigos/fastq/S64_R2.fastq.gz
# [...output truncated...]

The shell expands the glob into a list of matching file names before the loop starts (recall from week 4 that expansion is done by the shell itself), so to the loop, this is no different from the typed-out list of file names above.


In-class exercise 1: What will these loops print?

Create these files:

mkdir -p sandbox
touch sandbox/file1.txt sandbox/S1_R1.fastq.gz sandbox/S1_R2.fastq.gz

Now, what do you expect the output of each of the following loops to be? Test your predictions by running the loops (either directly in the terminal or by putting them in your Markdown file loops-practice.md and sending them to the terminal):

for file in sandbox/*fastq.gz; do
    echo "I have found file $file"
done
Click for the solution

The glob only matches the two FASTQ files, so the loop runs twice:

I have found file sandbox/S1_R1.fastq.gz
I have found file sandbox/S1_R2.fastq.gz
for file in sandbox/f*; do
    echo "$f"
done
Click for the solution

Look closely: the loop variable is file, but the echo line uses $f — a variable that was never assigned. Because we’re at the interactive prompt rather than in a script with set -u, the shell doesn’t complain about the unset variable, instead treating it as an empty string.

So the loop prints one empty line for each matching file: here, that is just file1.txt. This is exactly the kind of silent failure that set -u protects you from: in a script with strict Bash settings, this typo would cause an immediate error instead.

for file in sandbox/*.bam; do
    echo "I have found file $file"
done
Click for the solution

You might expect no output at all, since no files match the pattern. But when a glob pattern doesn’t match any files, the shell leaves the pattern as is, so the loop runs once, with the literal string sandbox/*.bam:

I have found file sandbox/*.bam

So if you make a typo in a glob pattern in a loop that runs a script, the script will be run once for a non-existent “file”. This is one more reason to print input file names in your scripts’ logging output!

In-class exercise 2: Another loop

Modify the globbing loop above (the one printing “Running an analysis on …”) to print the following for each FASTQ file (output for 2 files shown):

Running an analysis on ../garrigos/fastq/S63_R1.fastq.gz
File size:
-rw-rw----+ 1 jelmer PAS0471 21M Sep  9 13:46 ../garrigos/fastq/S63_R1.fastq.gz
Number of lines:
2000000

Running an analysis on ../garrigos/fastq/S63_R2.fastq.gz
File size:
-rw-rw----+ 1 jelmer PAS0471 22M Sep  9 13:46 ../garrigos/fastq/S63_R2.fastq.gz
Number of lines:
2000000
Click for a hint

To count the lines in a gzip-compressed file, recall from the week 4 exercises that zcat prints the decompressed contents of a file, which you can then pipe into another command.

Click for the solution
for fastq_file in ../garrigos/fastq/*fastq.gz; do
    echo "Running an analysis on $fastq_file"
    echo "File size:"
    ls -lh "$fastq_file"
    echo "Number of lines:"
    zcat "$fastq_file" | wc -l
    echo
done

Each FASTQ file has 2,000,000 lines, and since each read takes up 4 lines, it contains 500,000 reads. R1 and R2 files of a sample always have the same number of reads, since they contain the forward and reverse reads of the same DNA fragments: each read in the R1 file has a “mate” in the R2 file, in the same order.

4 Running a script many times with a loop

4.1 Your FastQC script

In the previous session, you wrote a script to run FastQC (saved at scripts/fastqc.sh). Recall that the script accepts one FASTQ file as an argument, so it can only be run for one file at a time. For example:

bash scripts/fastqc.sh ../garrigos/fastq/S63_R1.fastq.gz results/fastqc
Click to see the contents of the fastqc.sh script
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"
date

4.2 A “protocol” Markdown file

To run the script repeatedly, for all FASTQ files, we’ll use a loop.

While this is still practice, it does resemble a real analysis step unlike the loops in your loops-practice.md file. So, let’s use this opportunity to see what a proper record of such a step would look like: we’ll store the loop in a “protocol” section of a new README.md file, which also describes the project as a whole (for small projects, such a simple protocol in the main README.md can suffice).

  1. Create a README.md file in your week07 dir and open it in the editor:

    touch README.md
  2. Copy-paste the following into your README.md:

    README.md
    # RNA-seq data analysis
    
    ## General information
    
    - **Author**: <your-name>
    - **Date**: 2026-10-07
    - **Environment**: Pitzer cluster at OSC via VS Code
    - **Working dir**: `/fs/ess/PAS3493/people/<user>/week07`
    
    ## Project background
    
    This project is ...
    
    ## Protocol

In a few weeks, we’ll discuss how you can organize this kind of information for larger projects.

4.3 Looping over FASTQ files

You will now loop over all FASTQ files in the ../garrigos/fastq dir, and run your fastqc.sh script for each file. Add the following under ## Protocol in your README.md, and then run the code inside the code block (this will take a few minutes to run for all 44 files):

README.md (excerpt)
### Step 1 - Run FastQC for each FASTQ file:

```{bash}
for fastq in ../garrigos/fastq/*fastq.gz; do
    bash scripts/fastqc.sh "$fastq" results/fastqc
done
```
Starting script fastqc.sh
Wed Oct  7 13:58:21 EDT 2026
Input FASTQ file:   ../garrigos/fastq/S63_R1.fastq.gz
Output dir:         results/fastqc

Started analysis of S63_R1.fastq.gz
Approx 5% complete for S63_R1.fastq.gz
# [...output truncated...]

Successfully finished script fastqc.sh
Wed Oct  7 13:58:26 EDT 2026

Starting script fastqc.sh
Wed Oct  7 13:58:26 EDT 2026
Input FASTQ file:   ../garrigos/fastq/S63_R2.fastq.gz
Output dir:         results/fastqc

Started analysis of S63_R2.fastq.gz
Approx 5% complete for S63_R2.fastq.gz
# [...output truncated...]

That produces a lot of output! You can hopefully see that this looping approach is quite powerful, allowing you to easily run a script many times.

NoteTwo different variables called fastq

The loop variable fastq and the variable fastq inside the fastqc.sh script share a name, but they are separate variables: a script runs in its own, new shell, which can’t see the variables you assigned in your terminal. The only way the file name gets into the script is as an argument: the loop passes "$fastq" as the first argument, which the script receives as $1 and then copies to its own fastq variable.

TipThis loop runs the script sequentially, not in parallel

The loop above runs the fastqc.sh script sequentially: only after it has finished for the first FASTQ file, it will run for the second file, and so on. If these had been full-size FASTQ files, it may have taken hours for this to finish.

So, while you have now learned an elegant way to run a script many times, you have not yet learned how to run it simultaneously in parallel. In week 10, you’ll learn about doing this by submitting scripts as batch jobs.

5 Self-study: Looping over samples

FastQC is run separately for each FASTQ file, but many tools that process paired-end data need the R1 and R2 files of a sample together. Such tools should be run once per sample, i.e. once per pair of FASTQ files.

How can you loop over pairs of FASTQ files? There are two main ways:

  • Create a list of sample IDs, loop over these IDs, and find the pair of FASTQ files with matching names.
  • Loop over the R1 files only and then deduce the name of the corresponding R2 file inside the loop. This works because R1 and R2 file names are identical except for the read-direction identifier (typically _R1/_R2).

We will use the second method. To deduce the R2 file name, you’ll first need to learn about parameter expansion.

Type and run the code in this self-study section from your loops-practice.md file.

5.1 Parameter expansion

“Parameter expansion” (“parameter” is another word for variable) is the shell’s term for retrieving a variable’s value, as in $fastq. Some special forms of parameter expansion modify the value as it is retrieved — for example, to search-and-replace text:

  • Start by assigning a short DNA sequence to a variable:

    dna_seq="AAGTTCAT"
    echo "$dna_seq"
    AAGTTCAT
  • Now, use parameter expansion to replace all Ts with Us:

    echo "${dna_seq//T/U}"
    AAGUUCAU
  • You can also assign the result of the parameter expansion back to a variable:

    rna_seq="${dna_seq//T/U}"
    echo "$rna_seq"
    AAGUUCAU

So, the syntax for this type of parameter expansion is ${var_name//<search>/<replace>} — let’s deconstruct that:

  1. You reference the variable using the “full notation” with braces: here, ${dna_seq}.
  2. You add two forward slashes after the variable name, and then the search pattern: here, //T.
  3. After another forward slash, enter the replacement: here, /U.

If you only want to replace the first match, use a single forward slash after the variable name: ${var_name/<search>/<replace>}. Compare:

echo "${dna_seq/T/U}"
AAGUTCAT

Self-study exercise 1: Get the R2 file name with parameter expansion

After assigning the file name ../garrigos/fastq/S63_R1.fastq.gz to a variable, as shown below, add the parameter expansion to obtain the name of the corresponding R2 file to the second line:

fastq_R1=../garrigos/fastq/S63_R1.fastq.gz
fastq_R2=
Click for the solution
fastq_R1=../garrigos/fastq/S63_R1.fastq.gz
fastq_R2="${fastq_R1/_R1/_R2}"

Test that it worked:

echo "$fastq_R2"
../garrigos/fastq/S63_R2.fastq.gz

Next, a labmate suggests the shorter fastq_R2="${fastq_R1/1/2}". Does that give the correct result for this file? Would it still work if the sample were called S61 instead of S63?

Click for the solution

It works for S63, since the first 1 in the path happens to be the one in R1. But for ../garrigos/fastq/S61_R1.fastq.gz, the 1 in the sample ID would be replaced instead, giving ../garrigos/fastq/S62_R1.fastq.gz — the R1 file of a different sample! So, make your search pattern as specific as possible, like _R1.

5.2 A per-sample loop

Using parameter expansion, you can create a loop that iterates over R1 FASTQ files only, and then deduces the corresponding R2 file name. Here is a loop that runs once per sample, and prints the two file names for each:

for R1 in ../garrigos/fastq/*_R1.fastq.gz; do
    R2="${R1/_R1/_R2}"
    echo "R1 file: $R1"
    echo "R2 file: $R2"
    echo
done
R1 file: ../garrigos/fastq/S63_R1.fastq.gz
R2 file: ../garrigos/fastq/S63_R2.fastq.gz

R1 file: ../garrigos/fastq/S64_R1.fastq.gz
R2 file: ../garrigos/fastq/S64_R2.fastq.gz

# [...output truncated...]

Search for _R1 and not just R1: in other datasets, the string “R1” may also occur in the sample ID (e.g. ERR1234)!

To run a script for each sample, you would replace the echo lines with a call to a script that accepts both files as arguments. You’ll practice that in the exercise below with a “mock” script, and in a later week, you’ll write such a script for the program TrimGalore.


Self-study exercise 2: Run a script with a per-sample loop

  • Write a script scripts/trim_mock.sh that accepts three arguments: an R1 FASTQ file (assigned to $R1), an R2 FASTQ file (assigned to $R2), and an output dir (assigned to $outdir).

    You can use your fastqc.sh script as a template, but instead of running FastQC, this script should “mock-run” the fictional trimmer program as follows:

    # Mock-run the tool: by prefacing the command with 'echo',
    # the command will merely be printed, not run.
    echo trimmer --in1 "$R1" --in2 "$R2" --outdir "$outdir"

    Click for the solution

    scripts/trim_mock.sh
    #!/bin/bash
    set -euo pipefail
    
    # Copy the placeholder variables
    R1="$1"
    R2="$2"
    outdir="$3"
    
    # Initial logging
    echo "Starting script trim_mock.sh"
    date
    echo "Input R1 file:       $R1"
    echo "Input R2 file:       $R2"
    echo "Output dir:          $outdir"
    echo
    
    # Create the output dir if needed
    mkdir -p "$outdir"
    
    # Mock-run the tool
    echo trimmer --in1 "$R1" --in2 "$R2" --outdir "$outdir"
    
    # Final logging
    echo
    echo "Successfully finished script trim_mock.sh"
    date
  • In your loops-practice.md file, write a loop over all R1 FASTQ files that runs the trim_mock.sh script for each sample.

    Click for the solution

    loops-practice.md (excerpt)
    ## Self-study exercise 2
    
    We will loop over R1 files only, and deduce the R2 file name:
    
    ```bash
    for R1 in ../garrigos/fastq/*_R1.fastq.gz; do
        R2="${R1/_R1/_R2}"
        bash scripts/trim_mock.sh "$R1" "$R2" results/trim_mock
    done
    ```
    Starting script trim_mock.sh
    Wed Oct  7 14:05:10 EDT 2026
    Input R1 file:       ../garrigos/fastq/S63_R1.fastq.gz
    Input R2 file:       ../garrigos/fastq/S63_R2.fastq.gz
    Output dir:          results/trim_mock
    
    trimmer --in1 ../garrigos/fastq/S63_R1.fastq.gz --in2 ../garrigos/fastq/S63_R2.fastq.gz --outdir results/trim_mock
    
    Successfully finished script trim_mock.sh
    Wed Oct  7 14:05:10 EDT 2026
    
    Starting script trim_mock.sh
    Wed Oct  7 14:05:10 EDT 2026
    Input R1 file:       ../garrigos/fastq/S64_R1.fastq.gz
    # [...output truncated...]

6 Recap and next steps

In this lecture, you’ve learned how to write and use for loops, and how to use them to run a shell script many times: both for each FASTQ file and, using parameter expansion, for each sample. You’ve also started recording your analysis steps in a “protocol” Markdown file.

The optional shell scripting self-study page covers command substitution, basename and dirname, renaming files with loops, and more. Next week, we’ll look at using generative AI when coding. And in week 10, we’ll move on to submitting shell scripts as batch jobs at OSC.


Back to top

Footnotes

  1. It is possible to use another strategy that parallelizes program runs within a script, but we will use this strategy instead.↩︎

  2. This makes the code blocks “executable” — you’ll learn much more about that when we start using Quarto with R code in week 12.↩︎

  3. You can learn about while loops in the optional shell scripting self-study page.↩︎

  4. The indentation itself is optional for the shell, but makes loops much easier to read.↩︎