Lesson 4 of 13 · ⏱ 50 min · ✓ Free

Variables & Loops

So far every command you have run processes one thing at a time. Variables and loops change that entirely. With a single loop you can process 100 FASTQ files, rename an entire folder of samples, or run the same quality check on every chromosome — in the same time it takes to type one command. This is where Bash starts to feel like real programming.

01 Variables — storing values for later

📦 Why variables exist

A variable is a named container that holds a value — a piece of text, a number, a file path, or a command's output. Instead of typing the same long path or value repeatedly throughout your script, you store it once in a variable and reuse the name.

In bioinformatics this is essential. Imagine you have a pipeline that references the same reference genome path twenty times. If you store it as GENOME="/data/references/GRCh38.fa", changing the genome later means editing one line. Without a variable you would need to find and replace it twenty times — and inevitably miss one.

Variable names are written in UPPERCASE by convention (though lowercase works too). To set a variable you write NAME=value with no spaces around the = sign — spaces will cause an error. To use a variable's value you prefix the name with a $ sign: $NAME or ${NAME}. The curly braces form is safer when the variable name is followed immediately by other characters.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# Setting variables — no spaces around =
SAMPLE="SRR123456"
GENOME="/data/references/GRCh38.fa"
THREADS=8
OUTDIR="~/bash-linux-bioinformatics/results"

# Using variables with $
echo "Sample ID: $SAMPLE"
Sample ID: SRR123456

echo "Using genome: $GENOME"
Using genome: /data/references/GRCh38.fa

# Use ${} when the variable is followed immediately by more text
echo "Output file: ${SAMPLE}_aligned.bam"
Output file: SRR123456_aligned.bam

# THIS FAILS — Bash looks for variable named SAMPLE_aligned, not SAMPLE
echo "$SAMPLE_aligned.bam"
.bam   # wrong! Variable not found, so empty string

🔤 Single quotes vs double quotes — a critical difference

Quoting in Bash controls whether variables are expanded (replaced with their value) or treated as plain text.

Double quotes "..." — variables inside are expanded. "Hello $NAME" becomes "Hello Shajedur". This is what you want almost all the time.

Single quotes '...' — everything inside is treated as literal text. Variables are NOT expanded. 'Hello $NAME' prints exactly Hello $NAME with no substitution.

Use double quotes around any variable that might contain spaces — for example a file path with spaces in folder names. Without quotes, Bash splits on spaces and passes each word as a separate argument, which breaks things silently.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
NAME="Shajedur"

echo "Hello $NAME"     # double quotes — variable IS expanded
Hello Shajedur

echo 'Hello $NAME'     # single quotes — variable is NOT expanded
Hello $NAME

Never put spaces around = when setting a variable. SAMPLE = "SRR123456" causes an error — Bash thinks SAMPLE is a command, not a variable assignment.

02 Special variables Bash sets automatically

⚙️ Variables Bash creates for you

Bash automatically creates several special variables that are extremely useful in scripts. You do not set these — they are always available.

$? holds the exit status of the last command. A value of 0 means the command succeeded. Any non-zero value means it failed. This is how scripts check whether the previous step worked before continuing — if an alignment fails, you do not want the next step to run on a broken BAM file.

$$ holds the process ID (PID) of the current shell. Useful for creating unique temporary file names when running multiple jobs in parallel.

$0, $1, $2... are the arguments passed to a script. $0 is the script's own name. $1 is the first argument you pass, $2 the second, and so on. This is how you make scripts reusable — instead of hardcoding a sample name, you pass it in as an argument.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# $? — exit status of the last command
ls species.txt
echo "Exit status: $?"
Exit status: 0           # 0 = success

ls nonexistent.txt
echo "Exit status: $?"
Exit status: 2           # non-zero = failure

# $$ — current process ID (useful for unique temp files)
echo "My PID is: $$"
My PID is: 12345         # number varies every time

# Creating a unique temp file using $$
TMPFILE="/tmp/bioinfo_temp_$$.txt"
echo "Temp file: $TMPFILE"
Temp file: /tmp/bioinfo_temp_12345.txt
💡

After every important command in a script, check $?. If it is not 0, something went wrong. In Lesson 6 you will learn to use if [ $? -ne 0 ] to automatically stop a pipeline when a step fails.

03 Arithmetic — doing maths in Bash

🔢 Why arithmetic in Bash matters for bioinformatics

Bash is not a calculator — by default everything is treated as text. If you write RESULT=2+2, the variable holds the string "2+2", not the number 4.

To do arithmetic you wrap the expression in $(( )) — the double parentheses tell Bash to evaluate it as a mathematical expression. This is used constantly in bioinformatics scripts: calculating how many reads are in a file, working out memory requirements based on file size, computing batch sizes for parallel jobs, or converting line counts to read counts (dividing by 4 for FASTQ).

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# Basic arithmetic with $(( ))
echo $((2 + 2))
4

echo $((10 - 3))
7

echo $((6 * 7))
42

echo $((17 / 4))
4                  # integer division only — remainder is dropped

echo $((17 % 4))
1                  # % = modulo (remainder)

# Store result in a variable
LINES=$(wc -l < sample.fastq)
READS=$(( LINES / 4 ))
echo "Lines: $LINES | Reads: $READS"
Lines: 12 | Reads: 3

# Increment a counter (add 1)
COUNT=0
COUNT=$(( COUNT + 1 ))
echo "$COUNT"
1

💾 Command substitution — capturing command output into a variable

The $( ) syntax (single parentheses, different from $(( ))) runs a command and captures its output as a value. This is called command substitution.

For example, LINES=$(wc -l < sample.fastq) runs the wc -l command and stores the number it prints into the variable LINES. Without this, you would have to run the command, write down the number, and type it manually — useless in a script that processes hundreds of files.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# Capture command output into a variable
TODAY=$(date +%Y-%m-%d)
echo "Today is: $TODAY"
Today is: 2026-06-17

FILE_COUNT=$(ls ~/bash-linux-bioinformatics/module-1-foundations/ | wc -l)
echo "Files in folder: $FILE_COUNT"

# Capture wc output cleanly (< sends file as stdin, avoids filename in output)
LINES=$(wc -l < sample.fastq)
echo "Line count: $LINES"

04 Arrays — storing lists of values

📋 Why arrays are useful in bioinformatics

A variable holds one value. An array holds a list of values — all under the same name, accessed by position number (index). In bioinformatics, arrays are perfect for storing lists of sample names, chromosome names, or file paths that you want to loop over.

Array indexing in Bash starts at 0 — the first item is at index 0, the second at 1, and so on. To get all items, use ${ARRAY[@]}. To get the count of items, use ${#ARRAY[@]}.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# Define an array of sample names
SAMPLES=("SRR001" "SRR002" "SRR003" "SRR004")

# Access individual items — index starts at 0
echo "${SAMPLES[0]}"
SRR001

echo "${SAMPLES[2]}"
SRR003

# Access all items
echo "${SAMPLES[@]}"
SRR001 SRR002 SRR003 SRR004

# Count items in the array
echo "Number of samples: ${#SAMPLES[@]}"
Number of samples: 4

# Array of chromosome names — real bioinformatics use case
CHROMOSOMES=("chr1" "chr2" "chr3" "chrX" "chrY")
echo "Processing ${#CHROMOSOMES[@]} chromosomes"
Processing 5 chromosomes

05 The for loop — repeat for every item in a list

🔁 Why for loops are the most important thing in this lesson

A for loop runs a block of commands once for each item in a list. The loop variable takes the value of each item in turn, and you use that variable inside the loop body to do something with it.

This is the single most powerful tool for automating bioinformatics work. Without loops, running FastQC on 50 samples means typing 50 commands. With a loop, you type one. Running STAR alignment on 100 FASTQ files? One loop. Renaming all your output files to include the date? One loop.

The structure is always: for VARIABLE in LIST; do ... done. The do marks the start of the commands to repeat, and done marks the end.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# Basic for loop — iterate over a list of values
for SPECIES in maize rice sorghum wheat; do
    echo "Processing: $SPECIES"
done
Processing: maize
Processing: rice
Processing: sorghum
Processing: wheat

# Loop over an array of sample names
SAMPLES=("SRR001" "SRR002" "SRR003")
for SAMPLE in "${SAMPLES[@]}"; do
    echo "Running FastQC on: ${SAMPLE}.fastq"
done
Running FastQC on: SRR001.fastq
Running FastQC on: SRR002.fastq
Running FastQC on: SRR003.fastq

# Loop over a numeric range with {start..end}
for I in {1..5}; do
    echo "Iteration $I"
done
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5

# Loop over all .txt files in a folder
for FILE in ~/bash-linux-bioinformatics/module-1-foundations/*.txt; do
    echo "Found: $FILE"
    wc -l "$FILE"
done
💡

Always wrap file path variables in double quotes inside loops: "$FILE" not $FILE. If the file path contains a space, unquoted variables will break the command silently.

06 The while loop — repeat until a condition is false

⏳ for vs while — when to use each

A for loop runs a fixed number of times — once per item in a known list. A while loop runs as long as a condition remains true — you do not need to know in advance how many times it will run.

In bioinformatics, while loops are most useful for: reading a file line by line (processing each sample from a sample sheet), waiting for a job to finish before starting the next step, or retrying a failed download until it succeeds.

The most common bioinformatics use of while is reading a file line by line with while read LINE; do ... done < file.txt. This is how you process a sample sheet — a tab-separated file listing sample names, file paths, and conditions — without hardcoding anything.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# Basic while loop — count from 1 to 3
COUNT=1
while [ $COUNT -le 3 ]; do
    echo "Count: $COUNT"
    COUNT=$(( COUNT + 1 ))
done
Count: 1
Count: 2
Count: 3

# Read a file line by line — essential for sample sheets
while read LINE; do
    echo "Processing sample: $LINE"
done < species.txt
Processing sample: Sorghum bicolor
Processing sample: Arabidopsis thaliana
Processing sample: Oryza sativa

# Read multiple columns from a tab-separated file
# Imagine sample_sheet.txt has: SAMPLE_ID  FASTQ_PATH  CONDITION
while read SAMPLE FASTQ CONDITION; do
    echo "Sample: $SAMPLE | File: $FASTQ | Group: $CONDITION"
done < sample_sheet.txt
💡

sample_sheet.txt does not exist yet — you need to create it first before the loop can run. In real projects this file comes from your experiment design. For practice, create one now:

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash — create a practice sample sheet
# Create a tab-separated sample sheet (use $'\t' for a real tab character)
printf "control_1\tdata/raw/control_1.fastq\tcontrol\n" >  sample_sheet.txt
printf "control_2\tdata/raw/control_2.fastq\tcontrol\n" >> sample_sheet.txt
printf "treated_1\tdata/raw/treated_1.fastq\tcontreated\n" >> sample_sheet.txt
printf "treated_2\tdata/raw/treated_2.fastq\ttreated\n"  >> sample_sheet.txt

# Now run the while read loop
while read SAMPLE FASTQ CONDITION; do
    echo "Sample: $SAMPLE | File: $FASTQ | Group: $CONDITION"
done < sample_sheet.txt
Sample: control_1 | File: data/raw/control_1.fastq | Group: control
Sample: control_2 | File: data/raw/control_2.fastq | Group: control
Sample: treated_1 | File: data/raw/treated_1.fastq | Group: treated
Sample: treated_2 | File: data/raw/treated_2.fastq | Group: treated

Comparison operators inside [ ]: -le means "less than or equal to". Others: -lt (less than), -ge (greater than or equal), -gt (greater than), -eq (equal), -ne (not equal). We cover these fully in Lesson 6 — Conditionals.

07 Loops in bioinformatics — real patterns

Here are the exact patterns you will use in every real pipeline. Study these — they will appear throughout the rest of this course.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash — real bioinformatics loop patterns
# ── Pattern 1: Run a tool on every FASTQ file in a folder ──
for FASTQ in ~/data/raw/*.fastq.gz; do
    SAMPLE=$(basename "$FASTQ" .fastq.gz)   # strip folder path and extension
    echo "Running FastQC on: $SAMPLE"
    # fastqc "$FASTQ" --outdir ~/results/fastqc/  (real command when FastQC installed)
done

# ── Pattern 2: Create output folders for each sample ──
SAMPLES=("control_1" "control_2" "treated_1" "treated_2")
for SAMPLE in "${SAMPLES[@]}"; do
    mkdir -p "~/bash-linux-bioinformatics/results/${SAMPLE}"
    echo "Created folder for: $SAMPLE"
done

# ── Pattern 3: Count reads in every FASTQ file ──
for FILE in ~/bash-linux-bioinformatics/module-1-foundations/*.fastq; do
    LINES=$(wc -l < "$FILE")
    READS=$(( LINES / 4 ))
    NAME=$(basename "$FILE")
    echo "$NAME: $READS reads"
done

# ── Pattern 4: Add a prefix to every filename ──
for FILE in ~/bash-linux-bioinformatics/module-1-foundations/*.txt; do
    BASENAME=$(basename "$FILE")
    DIR=$(dirname "$FILE")
    mv "$FILE" "${DIR}/backup_${BASENAME}"
    echo "Renamed: $BASENAME → backup_$BASENAME"
done

🛠 basename and dirname — essential loop helpers

basename strips the directory path from a file path, leaving just the filename. With a second argument, it also strips the extension. For example, basename "/data/raw/SRR001.fastq.gz" ".fastq.gz" returns SRR001. This is how you extract a clean sample name from a full file path inside a loop.

dirname does the opposite — it strips the filename and returns just the directory path. So dirname "/data/raw/SRR001.fastq.gz" returns /data/raw. Together, basename and dirname let you manipulate file paths precisely inside loops.

Run from: ~/bash-linux-bioinformatics/module-1-foundations
bash
# basename — get just the filename
basename "/home/shajedur/data/SRR001.fastq.gz"
SRR001.fastq.gz

# basename — strip extension too
basename "/home/shajedur/data/SRR001.fastq.gz" ".fastq.gz"
SRR001

# dirname — get just the folder path
dirname "/home/shajedur/data/SRR001.fastq.gz"
/home/shajedur/data

08 Quick reference

SyntaxWhat it doesNotes
VAR=valueSet a variable — no spaces around =Use UPPERCASE by convention
$VAR or ${VAR}Use a variable's valueUse ${} when followed by more text
$(command)Capture command output into a variableCommand substitution
$(( expr ))Evaluate arithmetic expressionInteger maths only
$?Exit status of last command (0 = success)Check after every important step
ARRAY=(a b c)Define an arrayItems separated by spaces
${ARRAY[@]}All array items${#ARRAY[@]} gives the count
for X in LIST; do ... doneLoop over a listUse *.ext to loop over files
while [ condition ]; do ... doneLoop while condition is trueUse with read for line-by-line
basename path [ext]Strip directory (and extension) from pathEssential in file loops
dirname pathGet directory portion of a path

09 Exercises

Work through all five exercises in your Ubuntu terminal. Type every command yourself — do not copy-paste.

Exercise 1Variables and command substitution

Navigate to ~/bash-linux-bioinformatics/module-1-foundations. Create three variables: COURSE set to "Bash and Linux", LESSON set to 4, and FILE_COUNT set to the output of ls *.txt | wc -l (using command substitution). Then print a sentence using all three: "Course: Bash and Linux | Lesson: 4 | Text files found: N".

💬 Hint: use $( ) for command substitution. Use double quotes in your echo so all three variables expand.

Show answer
cd ~/bash-linux-bioinformatics/module-1-foundations
COURSE="Bash and Linux"
LESSON=4
FILE_COUNT=$(ls *.txt 2>/dev/null | wc -l)
echo "Course: $COURSE | Lesson: $LESSON | Text files found: $FILE_COUNT"
Course: Bash and Linux | Lesson: 4 | Text files found: 5
Exercise 2Arithmetic — FASTQ read counter

From ~/bash-linux-bioinformatics/module-1-foundations, use command substitution to store the line count of sample.fastq in a variable called LINES. Then use arithmetic to calculate the number of reads and store it in READS. Print both values.

💬 Hint: LINES=$(wc -l < sample.fastq) gives a clean number. Then READS=$(( LINES / 4 )).

Show answer
cd ~/bash-linux-bioinformatics/module-1-foundations
LINES=$(wc -l < sample.fastq)
READS=$(( LINES / 4 ))
echo "Lines: $LINES"
Lines: 12
echo "Reads: $READS"
Reads: 3
Exercise 3for loop — process a list of species

From ~/bash-linux-bioinformatics/module-1-foundations, write a for loop that iterates over the array ("Sorghum" "Arabidopsis" "Rice" "Maize"). For each species, create an empty file called SPECIES_data.txt (e.g. Sorghum_data.txt) in the results/ folder. After the loop, list the results folder to confirm all four files were created.

💬 Hint: define the array first, then loop with for SPECIES in "${ARRAY[@]}". Use touch to create each file.

Show answer
cd ~/bash-linux-bioinformatics/module-1-foundations
SPECIES_LIST=("Sorghum" "Arabidopsis" "Rice" "Maize")
for SPECIES in "${SPECIES_LIST[@]}"; do
    touch "../results/${SPECIES}_data.txt"
    echo "Created: ${SPECIES}_data.txt"
done
ls ../results/
Arabidopsis_data.txt  Maize_data.txt  Rice_data.txt  Sorghum_data.txt
Exercise 4while read — process a file line by line

From ~/bash-linux-bioinformatics/module-1-foundations, use a while read loop to read species.txt line by line. For each line, print: "Analysing genome of: [species name]".

💬 Hint: while read LINE; do ... done < species.txt. The < redirects the file as input to the while loop.

Show answer
cd ~/bash-linux-bioinformatics/module-1-foundations
while read LINE; do
    echo "Analysing genome of: $LINE"
done < species.txt
Analysing genome of: Sorghum bicolor
Analysing genome of: Arabidopsis thaliana
Analysing genome of: Oryza sativa
Exercise 5 · ChallengeLoop over files with basename

From ~/bash-linux-bioinformatics/module-1-foundations, write a for loop that iterates over every .txt file in that folder. For each file, use basename to extract just the filename (without the path), count its lines with wc -l, and print: "[filename]: N lines".

💬 Hint: loop with for FILE in *.txt. Then NAME=$(basename "$FILE") and LINES=$(wc -l < "$FILE").

Show answer
cd ~/bash-linux-bioinformatics/module-1-foundations
for FILE in *.txt; do
    NAME=$(basename "$FILE")
    LINES=$(wc -l < "$FILE")
    echo "${NAME}: $LINES lines"
done
duplicates.txt: 7 lines
file_list.txt: 8 lines
genomes.txt: 5 lines
species.txt: 3 lines
# Your output may differ depending on which files you have