Introduction to Bash Scripting
- 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
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.shand type this in; thecat > hello.sh <<'EOF' … EOFform writes the same two lines in one step so you can run it right away, thencat hello.shshows what landed in the file. Two lines, two essentials. The first line#!/bin/bashis 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:echoprints text. Every script you ever write is this skeleton — a shebang, then commands — grown taller.chmod +x hello.sh && ./hello.shHello, 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 +xadds it (that's thexbit from the Permissions lesson). Then./hello.shruns 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.shruns it without needing the execute bit at all.)Check yourself
What is the first line #!/bin/bash called, and what does it do?
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=valueand 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 $nameit expands it — swaps in the stored value — beforeechoever runs, soechoreceivesName: Alice, Age: 25. One subtlety worth knowing early: Bash has no real number type. That25is 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?
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" varprints the prompt, pauses for the person to type a line, and stores it invar. Here they typedAlice, so$usernamebecomesAlice. 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:$1is the first argument (foo),$#is how many there were (2),$@is all of them, and$0is 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?
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)runsdate, grabs its output, and stores it intoday;$(ls | wc -l)counts the entries here and stores that infiles. Then theechoreads 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 `cmdfor the same thing; prefer$( )` — it reads better and nests cleanly.)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.
iflets 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"; fiadult
- -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 thethenbranch runs and printsadult. 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. Changeageto15and the same script printsminor.Check yourself
In
if [ -f config.txt ]; then …, what does the -f test check?Repeating work: the for loop
Do the same thing to each item in a list.
The other half of automation is repetition. A
forloop 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.doopens the body,donecloses it — the loop equivalent ofthen/fi.for i in 1 2 3; do echo "backup pass $i"; donebackup pass 1 backup pass 2 backup pass 3
The loop runs its body three times, with $itaking 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')?
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.shThis writes a real script to /tmp/report.sh using a here-document ( <<'EOF' … EOFfeeds 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.shRun it, piping Alicein 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.
- 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.
| Block | Syntax |
|---|---|
| Shebang | #!/bin/bash (first line) |
| Make runnable | chmod +x script.sh then ./script.sh |
| Variable | name="Alice" → "$name" |
| User input | read -p "prompt" var |
| Arguments | $1 $2 · $# count · $@ all · $? last status |
| Capture output | x=$(date +%F) |
| Decision | if [ "$n" -ge 5 ]; then … else … fi |
| Loop | for x in *.txt; do … done |
