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.
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.
# 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
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.
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.
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.
# $? — 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.
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).
# 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
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.
# 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"
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[@]}.
# 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
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.
# 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.
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.
# 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:
# 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.
Here are the exact patterns you will use in every real pipeline. Study these — they will appear throughout the rest of this course.
# ── 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 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.
# 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
| Syntax | What it does | Notes |
|---|---|---|
| VAR=value | Set a variable — no spaces around = | Use UPPERCASE by convention |
| $VAR or ${VAR} | Use a variable's value | Use ${} when followed by more text |
| $(command) | Capture command output into a variable | Command substitution |
| $(( expr )) | Evaluate arithmetic expression | Integer maths only |
| $? | Exit status of last command (0 = success) | Check after every important step |
| ARRAY=(a b c) | Define an array | Items separated by spaces |
| ${ARRAY[@]} | All array items | ${#ARRAY[@]} gives the count |
| for X in LIST; do ... done | Loop over a list | Use *.ext to loop over files |
| while [ condition ]; do ... done | Loop while condition is true | Use with read for line-by-line |
| basename path [ext] | Strip directory (and extension) from path | Essential in file loops |
| dirname path | Get directory portion of a path | — |
Work through all five exercises in your Ubuntu terminal. Type every command yourself — do not copy-paste.
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.
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
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 )).
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
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.
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
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.
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
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").
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