Computing and the Command Line

Your First Script

A script is a file of commands. The shebang line, making it executable, and where to keep it so it runs by name. Variables, constants, quoting, printf, reading input, sending errors to standard error, and exit codes. set -euo pipefail and what it does and doesn't catch. Checking scripts with ShellCheck and tracing them with bash -x, and when a script has outgrown bash.

  • 7 min
  • 8 steps
  • 3 questions
  • Lesson 21 of 80

In this lesson

  1. A file of commands
  2. Variables and constants
  3. Talking to the user
  4. Safety settings
  5. Checking and debugging
  6. When to stop using bash
  7. Your turn
  8. So

A file of commands

A shell script is just a text file of commands, run one after another, as if you’d typed them 1. Anything you can type at the prompt can go in a script, and a script can use variables, decisions, and loops like any programming language 2.

Make a folder for practice and create hello:

me@linuxbox:~$ mkdir -p ~/shell-course/scripts && cd ~/shell-course/scripts
me@linuxbox:~/shell-course/scripts$ nano hello
#!/bin/bash
# hello: greet the user and show some facts about this machine.

echo "Hello, $USER."
echo "Today is $(date +%A), and this is $(hostname)."
echo "You have $(ls ~ | wc -l) items in your home folder."

Three steps make it a program 1:

  1. The shebang. The first line, #!/bin/bash, tells the system which program runs the file. Google’s style guide asks every executable script to start with exactly this line 3. (You’ll also see #!/usr/bin/env bash, which finds bash on PATH; it’s common for portability.)
  2. Make it executable: chmod +x hello (module 3).
  3. Run it with a path: ./hello. Or move it to ~/.local/bin and run it from anywhere by name (module 5).
me@linuxbox:~/shell-course/scripts$ chmod +x hello
me@linuxbox:~/shell-course/scripts$ ./hello
Hello, me.
Today is Monday, and this is linuxbox.
You have 9 items in your home folder.

Lines starting with # are comments. Start every script with a comment saying what it does and how to use it 3. Scripts that you run by name don’t need a .sh extension; libraries you source do 3.

A script called cleanup-downloads in a terminal, color-coded: the shebang #!/bin/bash; a comment saying it deletes downloads older than 30 days; set -euo pipefail; readonly DAYS=30 and readonly DIR="$HOME/Downloads"; a die function that echoes a message to standard error with >&2 and exits 1; then [[ -d "$DIR" ]] || die, find "$DIR" -type f -mtime +"$DAYS" -delete, and exit 0. A side panel names the parts: shebang, which program runs it; what it does and how to use it; stop on errors, unset variables, and pipe failures; constants, quoted; a function whose errors go to stderr; the work, every variable quoted. A second panel shows how to run it: chmod +x, ./cleanup-downloads or put it in ~/.local/bin, shellcheck to lint it, bash -x to trace it.
Every script you write can follow this shape. Credit: StudyCorner diagram · CC BY 4.0 · Source

Quick check

What does the first line #!/bin/bash do?

Variables and constants

Variables work exactly as at the prompt (module 5): name=value, no spaces, and quote every expansion: "$name" 3 1. Use "${name}" with braces when the name runs into other text.

Values that shouldn’t change are constants; mark them readonly and write them in capitals 3:

readonly BACKUP_DIR="$HOME/backups"
readonly KEEP_DAYS=30

Inside functions, declare variables local so they don’t leak into the rest of the script (lesson 3) 3.

Talking to the user

Printing. echo is fine for plain text. For anything with formatting, use printf, which takes a format string like other languages 4:

printf '%-12s %6.1f GB\n' "photos" 41.27

Reading input. read puts a line from standard input into a variable; -p shows a prompt, and -r keeps backslashes as typed 1:

read -rp "Delete old downloads? [y/N] " answer

Errors go to standard error, not standard output 3. That way they show up on screen even when the script’s output is redirected to a file, and they don’t pollute data piped onward. A small function keeps it tidy:

die() {
    echo "$(basename "$0"): $*" >&2
    exit 1
}

>&2 sends echo’s output to stream 2 (module 4). $0 is the script’s own name.

Exit status. exit 0 for success, exit 1 (or another nonzero number) for failure, so &&, ||, and if work on your script like any command (module 6) 1. A script that reaches its end exits with the status of its last command.

Quick check

Where should a script print its error messages?

Safety settings

By default, bash keeps going after a command fails, happily expands misspelled variables to nothing, and ignores failures in the middle of pipelines. In a script that deletes or moves files, that’s dangerous. Put this right after the comment block 4:

set -euo pipefail
  • -e: exit as soon as a command fails.
  • -u: treat an unset variable as an error instead of expanding it to nothing. A typo like rm -rf "$BACKUPDIR/"* (for $BACKUP_DIR) would otherwise become rm -rf /*.
  • -o pipefail: a pipeline fails if any part of it fails (module 6).

-e has blind spots by design: a failing command doesn’t stop the script if it’s the condition of an if or while, part of an && or || list (except the last command), or inverted with ! 4. That’s what lets you write [[ -d "$dir" ]] || die "...". Treat set -e as a safety net, not a substitute for checking the things that matter.

Quick check

Why put set -euo pipefail near the top of a script?

Checking and debugging

ShellCheck reads a script and points out bugs: unquoted variables, wrong test syntax, misspellings, commands that won’t do what you think, each with a code like SC2086 you can look up 5. Install it with sudo apt install shellcheck, run shellcheck hello, and fix what it reports. Make it a habit before you run anything that changes files.

To watch a script run, trace it 4 1:

me@linuxbox:~/shell-course/scripts$ bash -x ./hello
+ echo 'Hello, me.'
Hello, me.
++ date +%A
...

Each + line is a command after expansion. You can also put set -x and set +x around just the part you’re debugging.

When to stop using bash

Shell scripts are perfect for gluing commands together. They get awkward as they grow: Google’s rule is that once a script passes about 100 lines, or its logic gets complicated, it should be rewritten in a more structured language, such as Python 3. Keep scripts short and focused.

Your turn

Exercises

  1. Write hello as above, make it executable, run it, then move it into ~/.local/bin and run it by name.
  2. Write diskcheck: print how full your root file system is (df -h /) and the five biggest folders in your home (du -sh ~/* | sort -rh | head -5), with a heading before each.
  3. Add set -euo pipefail to diskcheck, then add a line echo "$UNDEFINED". What happens? Remove it.
  4. Write ask that reads your name with read -rp, then greets you; if the name is empty, print an error to stderr and exit 1. Test with ./ask; echo $?.
  5. Install ShellCheck and run it on all three scripts. Fix anything it reports.
Answers
  1. mv hello ~/.local/bin/ then hello. (Log in again if ~/.local/bin didn’t exist before.)
  2. For example:

    #!/bin/bash
    # diskcheck: show disk use and the biggest folders in my home.
    set -euo pipefail
    
    echo "== Root file system"
    df -h /
    echo
    echo "== Biggest folders in $HOME"
    du -sh "$HOME"/* 2> /dev/null | sort -rh | head -5
    
  3. The script stops with UNDEFINED: unbound variable and a nonzero exit status. That’s -u doing its job.

  4. For example:

    #!/bin/bash
    # ask: greet someone by name.
    set -euo pipefail
    
    read -rp "Your name? " name
    if [[ -z "$name" ]]; then
        echo "ask: no name given" >&2
        exit 1
    fi
    echo "Hello, $name!"
    
  5. Common finds: missing quotes (SC2086), $( ) output that will be split (SC2046), and cd without a check (SC2164: write cd dir || exit).

So

A script is a file of commands with a #!/bin/bash shebang, made executable with chmod +x and kept in ~/.local/bin. Quote variables, mark constants readonly, send errors to stderr with >&2, and exit nonzero on failure. Start with set -euo pipefail, check with ShellCheck, trace with bash -x, and switch languages when a script outgrows about 100 lines.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 7 min

Decisions and Loops

Next lesson
Sources for this lesson
  1. 1
    William Shotts. The Linux Command Line, Seventh Internet Edition (25.12A). LinuxCommand.org (print edition by No Starch Press). 2026. verifiedFree CC BY-NC-ND 3.0 book, release 25.12A of July 18, 2026. Part 1, Learning the Shell: the shell and terminal emulators, prompts ($ vs. # for the superuser), command history (most distributions keep the last 1,000 commands), Shift-Ctrl-C/V for copy and paste; navigation and the directory tree; exploring the system (ls options and the long listing, file, less, the guided tour of /, symbolic links); manipulating files (wildcards and character classes, mkdir, cp, mv, rm, ln; no undelete, test wildcards with ls first); working with commands (four kinds of commands, type, which, help, --help, man and its sections, apropos, whatis, info, alias); redirection; expansion and quoting; Readline keyboard tricks, completion, history search; permissions; processes. Later parts cover the environment, vi, packages, storage, networking, find, archiving, regular expressions, text processing, and shell scripting.
  2. 2
    Anish Athalye, Jon Gjengset, Jose Javier Gonzalez Ortiz. Course Overview + Introduction to the Shell (The Missing Semester of Your CS Education, 2026). MIT CSAIL. 2026. verifiedCC BY-NC-SA lecture notes. The shell is a textual interface for running programs and wiring them together; a terminal is the visual interface to it. Bash is the most widely used shell (zsh and fish are popular alternatives); on Windows use WSL or a Linux VM. The shell splits a command at whitespace: first word the program, the rest arguments; quote or backslash-escape spaces. man and --help, plus tldr for examples; cd is a shell builtin; Tab completion; pwd and $PWD; absolute vs. relative paths, . and ..; $PATH lists the directories searched for programs, which shows the one found. Basic tools cat, sort, uniq, head, tail, grep.
  3. 3
    Shell Style Guide. Google. verifiedBash is the only shell allowed for executables, which start with #!/bin/bash; executables on PATH need no extension, libraries take .sh and aren't executable. Every file starts with a comment describing it; functions get header comments. Error messages go to STDERR. Quote variables and prefer "${var}"; use "$@" for arguments; prefer [[ ]] over [ ] and $( ) over backticks; use local in functions; constants readonly and capitalized; put the program in a main function called last with main "$@". Scripts over about 100 lines, or with non-straightforward control flow, should be rewritten in a more structured language.
  4. 4
    Chet Ramey, Brian Fox. Bash Reference Manual, Edition 5.3. GNU Project, Free Software Foundation. 2025. verifiedThe reference for Bash 5.3 (May 18, 2025). Redirections (3.6) are processed left to right and order matters: ls > dirlist 2>&1 sends both streams to dirlist, while ls 2>&1 > dirlist sends only standard output there. With set -o noclobber, > fails on an existing regular file and >| overrides it. &> word is equivalent to > word 2>&1 and &>> word to >> word 2>&1. Here documents (<<word, with <<- stripping leading tabs; quoting word disables expansion) and here strings (<<<). Expansions (3.5) happen in a fixed order: brace; tilde, parameter, arithmetic, and command substitution left to right; word splitting; filename expansion; quote removal last. Startup files (6.2): an interactive login shell reads /etc/profile then the first of ~/.bash_profile, ~/.bash_login, ~/.profile; an interactive non-login shell reads ~/.bashrc. HISTCONTROL (ignorespace, ignoredups, ignoreboth), HISTSIZE, HISTFILESIZE; set -x traces expanded commands; shell functions and variables, export.
  5. 5
    Vidar Holen. ShellCheck: finds bugs in your shell scripts. shellcheck.net. verifiedA static analysis tool for sh and bash scripts that flags quoting problems, misused tests, unreachable or wrong logic, and portability issues, each with a numbered explanation (for example SC2086, double-quote to prevent word splitting). Installable with apt, dnf, brew, or pip (shellcheck-py), or usable in the browser. Version 0.11.0 checked the course's backup script clean on 2026-10-05.