Skip to main content
    Courses/Command Line/Shell Scripting Basics

    Lesson 7 • Intermediate

    Shell Scripting Basics

    By the end of this lesson you'll be able to turn a list of terminal commands into a reusable bash script — with variables, input, decisions, loops, and functions — so the computer does the repetitive work for you.

    What You'll Learn

    • Write the shebang #!/bin/bash and make a script executable with chmod +x
    • Store and reuse values with variables and $VAR / ${VAR}
    • Capture command output with command substitution $(...)
    • Read user input and accept positional arguments $1, $@, $#
    • Make decisions with if / elif / else and test / [ ] / [[ ]]
    • Repeat work with for and while loops, package it in functions, and check exit codes with $?

    1. The Shebang & Variables

    A script is just a text file full of commands. The first line is the shebang#!/bin/bash — which tells the system to run the file with the bash interpreter. After that, you store values in variables: write name=value with no spaces around the =, and read it back with $name. Use ${name} braces when the variable name sits right next to other text.

    Worked example: shebang and variables
    #!/bin/bash
    # The shebang (line 1) tells the system: run this file with /bin/bash.
    # It MUST be the very first line, starting with #!  (no space before #).
    
    # A variable = a named label for a value. Assign with name=value.
    # CRITICAL: no spaces around the = sign.
    name="Ada"
    language="bash"
    year=2026
    
    # Read a variable back by putting $ in front of its name.
    echo "Hello, $name!"
    
    # Use ${VAR} braces when the name touches other text, so bash knows
    # where the variable name ends.
    echo "You are learning ${language} scripting."
    echo "It is the year ${year}."
    Output
    Hello, Ada!
    You are learning bash scripting.
    It is the year 2026.
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    Running a script

    There are two ways to run a saved script. Either make it executable once with chmod +x and run it directly, or hand the file to bash each time:

    $ chmod +x script.sh    # make it executable (only needed once)
    $ ./script.sh           # run it directly
    
    # OR, with no chmod needed:
    $ bash script.sh        # run it by handing the file to bash

    Forgetting chmod +x is the classic "Permission denied" trip-up — see Common Errors below.

    2. Command Substitution & Input

    Command substitution$(command) — runs a command and drops its output straight into your line. So today=$(date +%A) stores today's weekday in a variable. To get input from the person running the script, use read: it pauses, waits for them to type, and stores the result in a variable you name.

    Worked example: $(...) and read
    #!/bin/bash
    # Command substitution: $(command) runs the command and drops its
    # OUTPUT right into your line. The script below uses fixed values so
    # the output is predictable, but the comments show the real commands.
    
    today="Monday"                 # real version: today=$(date +%A)
    file_count=3                   # real version: file_count=$(ls | wc -l)
    
    echo "Today is ${today}."
    echo "There are ${file_count} files here."
    
    # 'read' pauses and stores what the user types into a variable.
    # In an interactive shell this would prompt you; here we show the idea:
    #   read -p "What is your name? " user_name
    #   echo "Welcome, ${user_name}!"
    user_name="Grace"              # pretend the user typed this
    echo "Welcome, ${user_name}!"
    Output
    Today is Monday.
    There are 3 files here.
    Welcome, Grace!
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    3. Positional Arguments

    Scripts become powerful when you pass them arguments — the extra words you type after the script name. Bash hands them to you as $1 (first), $2 (second), and so on. $# is the count of arguments, $@ is all of them as a list, and $0 is the script's own name. This is how one script can act on whatever you give it.

    Worked example: $1, $@ and $#
    #!/bin/bash
    # Positional arguments are the words you type AFTER the script name.
    # Save this as greet.sh and run:   bash greet.sh Alice Bob Carol
    #
    #   $0  -> the script's own name        (greet.sh)
    #   $1  -> the first argument           (Alice)
    #   $2  -> the second argument          (Bob)
    #   $#  -> how many arguments there are (3)
    #   $@  -> ALL the arguments, as a list
    
    echo "Script name: $0"
    echo "First arg:   $1"
    echo "Arg count:   $#"
    
    # Loop over every argument with $@:
    for person in "$@"; do
      echo "Hello, ${person}!"
    done
    Output
    Script name: greet.sh
    First arg:   Alice
    Arg count:   3
    Hello, Alice!
    Hello, Bob!
    Hello, Carol!
    Save as greet.sh and run bash greet.sh Alice Bob Carol in your own terminal — the arguments fill $1, $@ and $#.

    4. Conditionals: if, test, [ ] and [[ ]]

    Conditionals run code only when a test passes. The shape is if [ test ]; then ... fi — and you must close it with fi. Numbers use -gt -ge -lt -le -eq -ne; strings use = and !=; files use checks like -f (exists). [ ] is the classic test command; [[ ]] is the modern bash version that's safer with strings. Watch the spaces — you need one inside the brackets.

    Worked example: numeric, string and file tests
    #!/bin/bash
    # Conditionals run code only when a test is true.
    # [ ] is the classic test command;  [[ ]] is the modern bash version
    # (safer with strings and supports && and ||). Mind the spaces:
    # you need a space INSIDE the brackets:  [ "$x" -gt 0 ]
    
    score=85
    
    # Numeric comparisons:  -gt -ge -lt -le -eq -ne
    if [ "$score" -ge 90 ]; then
      echo "Grade: A"
    elif [ "$score" -ge 80 ]; then
      echo "Grade: B"          # 85 is >= 80, so this branch runs
    else
      echo "Grade: C"
    fi
    
    # String comparison uses = (equal) and != (not equal).
    name="admin"
    if [[ "$name" == "admin" ]]; then
      echo "Access granted"
    fi
    
    # File test: -f is true if the file exists and is a regular file.
    if [ -f "/etc/hostname" ]; then
      echo "Host file found"
    fi
    Output
    Grade: B
    Access granted
    Host file found
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    5. Loops, Functions & Exit Codes

    A for loop walks through a list; a while loop repeats as long as its test stays true. A function wraps commands under a name so you can reuse them — inside it, $1 is the first argument and local keeps a variable private. Finally, every command sets $?, its exit code: 0 means success and anything else means a failure.

    Worked example: for, while, a function, and $?
    #!/bin/bash
    # Loops repeat work; functions package work so you can reuse it.
    
    # for over an explicit list
    echo "--- for loop ---"
    for i in 1 2 3; do
      echo "Step $i"
    done
    
    # while repeats WHILE its test stays true
    echo "--- while loop ---"
    count=3
    while [ "$count" -gt 0 ]; do
      echo "${count}..."
      count=$((count - 1))     # $(( )) does the arithmetic
    done
    echo "Liftoff!"
    
    # A function. $1 is its first argument; 'local' keeps the var private.
    greet() {
      local who=$1
      echo "Hi, ${who}!"
    }
    echo "--- function ---"
    greet "Linus"
    
    # Every command sets $? — its exit code. 0 means success, non-zero a failure.
    ls /etc/hostname > /dev/null   # this succeeds
    echo "Exit code: $?"          # 0
    Output
    --- for loop ---
    Step 1
    Step 2
    Step 3
    --- while loop ---
    3...
    2...
    1...
    Liftoff!
    --- function ---
    Hi, Linus!
    Exit code: 0
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    🎯 Your Turn: complete the for-loop

    The script is almost done — fill in the two blanks marked ___ using the # 👉 hints, then run it and check the Output panel matches.

    Fill in the blanks: loop over a list
    #!/bin/bash
    # 🎯 YOUR TURN — finish the for-loop, then run it.
    
    fruits="apple banana cherry"
    
    # 1) Loop over each fruit in the list.
    for fruit in ___; do          # 👉 replace ___ with  $fruits
      # 2) Print one line per fruit using the loop variable.
      echo "I like ___"           # 👉 replace ___ with  ${fruit}
    done
    
    # ✅ Expected output:
    #    I like apple
    #    I like banana
    #    I like cherry
    Output
    I like apple
    I like banana
    I like cherry
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    🎯 Your Turn: complete the if-test

    Two blanks again: pick the right comparison operator, and the keyword that closes an if block. Then run it.

    Fill in the blanks: an if-test
    #!/bin/bash
    # 🎯 YOUR TURN — fill in the if-test, then run it.
    
    age=20
    
    # 1) Test whether age is 18 OR MORE.  (hint: -ge means ">=")
    if [ "$age" ___ 18 ]; then    # 👉 replace ___ with  -ge
      echo "You can vote"
    # 2) Close the if block with the right keyword.
    ___                            # 👉 replace ___ with  fi
    
    # ✅ Expected output:
    #    You can vote
    Output
    You can vote
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    Pro Tips

    • 💡 Quote your variables: write "$file", not $file. If the value has spaces, quoting keeps it as one piece instead of splitting it.
    • 💡 Use local inside functions so a function's variables don't leak into the rest of the script — the same idea as let/const scope in JavaScript.
    • 💡 $((...)) does maths: count=$((count + 1)). Plain $( ) runs a command; the double parentheses do arithmetic.
    • 💡 Fail loudly: start scripts with set -euo pipefail once you're comfortable — it stops the script on the first error instead of charging on.

    Common Errors (and the fix)

    • "command not found" after an assignment — you put spaces around =. name = "Alice" is wrong; write name="Alice" with no spaces.
    • "syntax error: unexpected end of file" — you forgot to close a block. Every if needs fi, every for/while needs done, and every case branch needs ;;.
    • "Permission denied" when running ./script.sh — the file isn't executable. Run chmod +x script.sh first, or just run it with bash script.sh.
    • "too many arguments" / "unary operator expected" in a test — an unquoted variable that's empty or has spaces broke the [ ] test. Always quote it: [ "$x" -gt 0 ].
    • "[: missing `]'" — you left out the space inside the brackets. It must be [ "$x" = "y" ], with spaces just inside both brackets.

    📋 Quick Reference

    ConceptSyntaxNotes
    Shebang#!/bin/bashMust be line 1
    Variablename="value"No spaces around =
    Read it$name / ${name}Braces near other text
    Substitutionx=$(date)Captures command output
    Inputread -p "Q? " vStores typed text in v
    Arguments$1 $@ $#First, all, count
    Ifif [ cond ]; then … fiClose with fi
    For loopfor x in list; do … doneClose with done
    While loopwhile [ cond ]; do … doneRepeats while true
    Functionf() { … }$1 = first arg
    Arithmeticn=$((n + 1))Double parentheses
    Exit code$?0 = success

    Frequently Asked Questions

    Q: Why does name = "Alice" fail but name="Alice" work?

    Bash treats a space as the separator between a command and its arguments. With spaces, bash thinks 'name' is a command and tries to run it with the arguments '=' and 'Alice'. Variable assignment must have NO spaces around the equals sign: name="Alice".

    Q: What is the difference between $VAR and ${VAR}?

    They do the same thing — read the variable's value. You need the braces ${VAR} only when the name touches other characters, so bash knows where the name ends. For example ${file}_backup works, but $file_backup looks for a variable called 'file_backup'.

    Q: When should I use [ ] versus [[ ]]?

    [ ] (the test command) is POSIX and works in any shell. [[ ]] is a bash/zsh feature that is safer with strings, lets you use && and || inside, and supports pattern matching. In bash scripts, prefer [[ ]] for string and file tests; use [ ] when you need maximum portability.

    Q: What does $? mean, and what is an exit code?

    $? holds the exit code of the command that just ran. By convention 0 means success and any non-zero value means a failure (1 is a generic error). Scripts use exit codes so other programs — and the shell itself — can tell whether a command worked.

    Q: Why do people say to always quote variables, like "$file"?

    If a variable's value contains spaces (a filename like 'my notes.txt'), an unquoted $file splits into separate words and breaks your command. Wrapping it in double quotes — "$file" — keeps the whole value as one piece. Quoting variables prevents a whole class of subtle bugs.

    Mini-Challenge: file-counter report

    No blanks this time — just a brief and an outline. Write a small, genuinely useful script that takes a folder name as an argument, guards against being called with none, and reports how many items it holds. Check your output against the example in the comments.

    🎯 Mini-Challenge: build it yourself
    #!/bin/bash
    # 🎯 MINI-CHALLENGE: a tiny file-counter report
    # Save as report.sh and run:  bash report.sh Documents
    #
    # 1. Read the folder name from the first argument ($1) into a variable.
    # 2. If no argument was given ($# is 0), print "Usage: report.sh <folder>"
    #    and stop the script with  exit 1  (a non-zero exit code = error).
    # 3. Otherwise, use a for-loop over the items and a counter variable to
    #    count them, then print:  "<folder> contains N item(s)".
    #
    # Hints:  count=$((count + 1))   |   for item in "$folder"/*; do ... done
    #
    # ✅ Example (folder has 4 items):  Documents contains 4 item(s)
    
    # your code here
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    🎉 Lesson Complete!

    • ✅ A script starts with the shebang #!/bin/bash; make it runnable with chmod +x
    • ✅ Variables use name=value (no spaces) and read back as $name / ${name}
    • $(...) captures command output; read gets input; $1/$@/$# are arguments
    • ✅ Decide with if … fi and [ ]/[[ ]]; repeat with for and while … done
    • ✅ Package work in functions; check success with the exit code $?
    • Next lesson: Process Management — list, background, and stop running programs with ps, kill, and jobs

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service