Everything you have typed at the prompt so far — commands, pipes, redirects, variable assignments — can be written into a file and run as a program. That is a shell script. It is not a different language. It is the same shell, reading from a file instead of your keyboard.
The shebang
#!/bin/bashThe first line of every shell script is the shebang[1] (also called
hashbang). It tells the kernel which interpreter to use when the file
is executed. /bin/bash is the path to Bash.
Without it, the shell falls back to running the file with /bin/sh —
the system's default shell, which might be dash or another minimal
POSIX shell, not Bash.
If your script uses any Bash-specific syntax, it will behave differently
or fail outright. The shebang is how you guarantee which interpreter runs
the file.
Create a script:
cat > greet.sh << 'EOF'
#!/bin/bash
echo "hello from a script"
EOFMake it executable and run it:
chmod +x greet.sh
./greet.shhello from a scriptVariables
#!/bin/bash
NAME="terminal"
echo "welcome to the $NAME"Variables have no spaces around =. $NAME expands to the value.
Double quotes allow expansion; single quotes suppress it:
echo "$NAME" # prints: terminal
echo '$NAME' # prints: $NAMEArguments
When you call a script with arguments, Bash populates special variables:
#!/bin/bash
echo "script name: $0"
echo "first arg: $1"
echo "second arg: $2"
echo "all args: $@"
echo "arg count: $#"./greet.sh foo barscript name: ./greet.sh
first arg: foo
second arg: bar
all args: foo bar
arg count: 2Conditionals
#!/bin/bash
if [[ -f "$1" ]]; then
echo "$1 is a regular file"
elif [[ -d "$1" ]]; then
echo "$1 is a directory"
else
echo "$1 does not exist"
fi[[ ... ]] is the Bash compound test. Common tests:
[[ -f file ]] # file exists and is a regular file
[[ -d dir ]] # dir exists and is a directory
[[ -z "$var" ]] # var is empty
[[ -n "$var" ]] # var is non-empty
[[ "$a" = "$b" ]] # strings are equal
[[ "$a" -eq "$b" ]] # integers are equal
[[ "$a" -gt "$b" ]] # integer a > bAlways quote variables inside [[ ]] to avoid errors when they are
empty — including integer tests.
In Bash scripts, use [[ ]] for all tests. It avoids word-splitting
and supports pattern matching and regex. [ ] is the POSIX test
command — it works in any sh script, but Bash scripts should use
[[ ]] consistently.
Loops
for iterates over a list. Each item is assigned to the loop variable
in turn, and the body runs once per item:
#!/bin/bash
for file in *.c; do
echo "found: $file"
doneThe glob *.c expands to all .c files in the current directory
before the loop starts. The loop runs once per file. If there are no
.c files, it does not run at all.
To iterate over a range of numbers, use brace expansion:
for i in {1..5}; do
echo "step $i"
done{1..5} expands to 1 2 3 4 5 before the loop runs. The shell
handles the expansion — for just sees a list of five items.
while runs as long as a condition is true. Use it when you do not
know the number of iterations in advance, or when you are counting
yourself:
count=0
while [[ "$count" -lt 10 ]]; do
echo "$count"
count=$((count + 1))
donecount=0 initialises the variable before the loop. [ "$count" -lt 10 ]
is the condition — less than 10. $(( ... )) performs integer
arithmetic: $((count + 1)) adds one to count and assigns it back.
Without that line, the condition never becomes false and the loop runs
forever.
Exit codes
#!/bin/bash
set -o pipefail
if [[ "$#" -eq 0 ]]; then
echo "usage: $0 <directory>" >&2
exit 1
fi
count=$(find "$1" -name '*.c' -type f | wc -l)
echo "$count C files in $1"
exit 0exit 0 means success. exit 1 (or any non-zero) means failure.
The >&2 redirects the error message to stderr — the convention for
usage errors and diagnostics.
Save this as count_c_files.sh, chmod +x it, and run it against
the directories you will create in the C chapters:
./count_c_files.sh ~/DevelopmentThis is the shape of every tester in the companion repo. A script that takes arguments, checks preconditions, runs tests, and exits with a code that reflects the result.