Signal9Signal9 — home
online
all lessons

Introduction to Bash Scripting

Beginner35 min7 steps
What you'll learn
  • Explain what a script and a shebang are, and run one after making it executable
  • Store and read data in variables — and respect the no-spaces and quoting rules
  • Get data into a script from the user (read) and the command line ($1, $@, $#)
  • Capture a command's output into a variable with command substitution $( )
  • Make decisions with if / test, and repeat work with a for loop
  • Combine all of the above into a small, real script
  1. What Bash is, and your first script

    Understand what a script and a shebang are — then create one and run it.

    You already know how to type a command and press Enter. Bash scripting is just saving those commands in a file so the machine runs them for you — in order, the same way, every time, without you retyping a thing. That's the whole idea; everything else is detail.

    An analogy that holds up the whole lesson: typing one command is like asking the kitchen for a single dish. A script is handing over a recipe card — a list of steps the kitchen follows top to bottom, identically, whether it's the first time or the thousandth. Automation is really just "write the recipe once, run it forever."

    Why learn this? Because the moment you do something more than twice — back up a folder, check a service, process a batch of files — a script does it faster, without the typos, and while you're doing something else. It's the single biggest force-multiplier in Linux.

    cat > hello.sh <<'EOF'
    #!/bin/bash
    echo "Hello, World!"
    EOF
    cat hello.sh
    #!/bin/bash
    echo "Hello, World!"
    #!/bin/bash
    the shebang: run this file with the Bash interpreter
    Normally you'd open an editor like nano hello.sh and type this in; the cat > hello.sh <<'EOF' … EOF form writes the same two lines in one step so you can run it right away, then cat hello.sh shows what landed in the file. Two lines, two essentials. The first line #!/bin/bash is the shebang (#! + a path). It's not a comment — it tells the system which interpreter should run this file, here Bash. Leave it off and the system has to guess. The second line is an ordinary command: echo prints text. Every script you ever write is this skeleton — a shebang, then commands — grown taller.
    chmod +x hello.sh && ./hello.sh
    Hello, World!
    chmod +x
    add the execute permission so the file can be run as a program
    ./
    run the script from the current directory (which isn't on $PATH)
    A file of commands isn't runnable until it has the execute permission, so first chmod +x adds it (that's the x bit from the Permissions lesson). Then ./hello.sh runs it. The ./ is required and trips everyone up once: it means "the file here, in this directory," because for safety the current directory isn't on your $PATH. (Shortcut: bash hello.sh runs it without needing the execute bit at all.)

    Check yourself

    What is the first line #!/bin/bash called, and what does it do?

  2. Variables: labelled boxes for your data

    Store values and read them back — with the two rules that trip up every beginner.

    A variable is a labelled box that holds a value so you can reuse it and change it in one place. You assign with name=value and read with a $ in front: $name. That asymmetry — no $ when setting, a $ when getting — is worth saying out loud until it's automatic.

    name="Alice" && age=25 && echo "Name: $name, Age: $age"
    Name: Alice, Age: 25
    Set two boxes, then read them inside a string. When Bash sees $name it expands it — swaps in the stored value — before echo ever runs, so echo receives Name: Alice, Age: 25. One subtlety worth knowing early: Bash has no real number type. That 25 is the string "25"; it only behaves like a number in contexts that ask for one (like the comparison you'll meet in the conditionals step).

    Check yourself

    How do you read the value stored in a variable named count?

  3. Getting data in: input and arguments

    Feed a script values from the user or from the command line.

    A script that only ever works on hard-coded values is a script that does exactly one thing. Real scripts take input, and there are two ways it arrives: interactively (you ask the person running it) or as arguments (values handed over on the command line).

    read -p "What's your name? " username && echo "Hello, $username!"
    What's your name? Alice
    Hello, Alice!
    -p "…"
    show this prompt before reading (no newline needed)
    read -p "prompt" var prints the prompt, pauses for the person to type a line, and stores it in var. Here they typed Alice, so $username becomes Alice. This is interactive input — great for a question a human answers while watching.
    echo "script: $0  first: $1  count: $#  all: $@"
    script: ./greet.sh  first: foo  count: 2  all: foo bar
    $0
    the script's own name
    $1, $2 …
    the first, second, … positional argument
    $#
    how many arguments were passed
    $@
    all the arguments, as a list
    The other input source is command-line arguments, exposed through special variables. The output above is what you'd see after running ./greet.sh foo bar: $1 is the first argument (foo), $# is how many there were (2), $@ is all of them, and $0 is the script's own name. Arguments are what let one script act on whatever you point it at — a filename, a username, a number — instead of something baked in.

    Check yourself

    Which special variable holds the first argument passed to a script?

  4. Command substitution: capturing output

    Store a command's output in a variable and reuse it.

    So far your variables hold text you typed. But often the value you want is the result of running a command — today's date, a count of files, the current hostname. Command substitution captures it: wrap a command in $( ) and Bash replaces the whole thing with that command's output.

    today=$(date +%Y-%m-%d) && files=$(ls | wc -l) && echo "On $today this dir has $files files"
    On 2026-07-10 this dir has 7 files
    $(date +%Y-%m-%d) runs date, grabs its output, and stores it in today; $(ls | wc -l) counts the entries here and stores that in files. Then the echo reads both back. This is how a script uses live data instead of static text — the date and count are whatever they actually are at run time. (You may see the older backtick form ` cmd for the same thing; prefer $( )` — it reads better and nests cleanly.)
  5. Making decisions: if and test

    Have a script choose a path based on a condition.

    A recipe that always does the same thing is useful; one that reacts is powerful. if lets a script branch:

    `

    if CONDITION; then

    # runs when the condition is true

    else

    # runs otherwise

    fi

    `

    fi ("if" backwards) closes the block. The CONDITION is usually a test, written [ ... ]. A few you'll use daily: -f file (the file exists), -d dir (the directory exists), -z "$x" (the string is empty), and numeric comparisons -eq -ne -lt -le -gt -ge (equal, not-equal, less-than, …).

    age=25 && if [ "$age" -ge 18 ]; then echo "adult"; else echo "minor"; fi
    adult
    -ge
    numeric 'greater than or equal' (also -eq -ne -lt -le -gt)
    -f / -d
    true if a file / directory exists
    -z
    true if a string is empty
    The test [ "$age" -ge 18 ] asks "is age greater-than-or-equal-to 18?" — true for 25, so the then branch runs and prints adult. Note the quotes around "$age" (that habit again) and the spaces inside the brackets: [ is actually a command, so ["$age" without a space is an error. Change age to 15 and the same script prints minor.

    Check yourself

    In if [ -f config.txt ]; then …, what does the -f test check?

  6. Repeating work: the for loop

    Do the same thing to each item in a list.

    The other half of automation is repetition. A for loop walks a list and runs the same block once per item:

    `

    for item in list; do

    # use $item

    done

    `

    The list can be words you write, a glob like *.txt (every matching file), or the output of a command via substitution. do opens the body, done closes it — the loop equivalent of then/fi.

    for i in 1 2 3; do echo "backup pass $i"; done
    backup pass 1
    backup pass 2
    backup pass 3
    The loop runs its body three times, with $i taking each value in turn. Swap the list for a glob — for f in *.log; do echo "found $f"; done — and it runs once per matching file, which is how you process a whole directory in three lines. Same shape every time: for … in … ; do … done.

    Check yourself

    Which keyword closes the body of a for loop (the loop's equivalent of 'fi')?

  7. Now do it — put it all together

    Write and run a real script that combines variables, substitution, a test, and a loop.

    You have a real shell to the right. Let's assemble the pieces into one small script that actually does something — a tiny "report" that greets you, stamps the date, and reacts to how many files are around. Build it, make it executable, run it, and watch the concepts work together.

    cat > /tmp/report.sh <<'EOF'
    #!/bin/bash
    read -p "Your name: " who
    count=$(ls /tmp | wc -l)
    echo "Hi $who — $(date +%F), /tmp has $count entries."
    if [ "$count" -gt 5 ]; then echo "That's a busy /tmp."; else echo "/tmp is tidy."; fi
    for n in 1 2 3; do echo "  tick $n"; done
    EOF
    chmod +x /tmp/report.sh
    This writes a real script to /tmp/report.sh using a here-document (<<'EOF' … EOF feeds those lines in as the file's contents) and makes it executable. Read the script itself: it takes input (read), captures live data ($(ls…), $(date…)), makes a decision (if), and repeats (for) — every concept from this lesson in six lines.
    echo Alice | /tmp/report.sh
    Run it, piping Alice in as the answer to the prompt. You'll see the greeting with today's date, a line reacting to how many entries /tmp has, and three ticks from the loop. Change the name, run it again — the recipe stays the same, the output adapts. That's a script.
Recap
  • A script is saved commands with a shebang (#!/bin/bash) on line 1; chmod +x it, then run ./script.sh (or bash script.sh with no execute bit).
  • Variables: assign name=value (NO spaces), read "$name" (quote it, so spaces in the value stay one value).
  • Input arrives from read -p (interactive) or the command line via $1 $2 … with $# (count) and $@ (all); $? is the last command's exit status.
  • Command substitution $(command) captures a command's output into a variable — the glue for using live data like dates and counts.
  • Decide with if CONDITION; then … else … fi using tests [ -f file ], [ "$x" -ge N ] — mind the spaces inside [ ]; repeat with for x in list; do … done.
  • Real scripts combine all of these: input → capture → decide → repeat.
Building block → syntax
BlockSyntax
Shebang#!/bin/bash (first line)
Make runnablechmod +x script.sh then ./script.sh
Variablename="Alice" → "$name"
User inputread -p "prompt" var
Arguments$1 $2 · $# count · $@ all · $? last status
Capture outputx=$(date +%F)
Decisionif [ "$n" -ge 5 ]; then … else … fi
Loopfor x in *.txt; do … done