Shell Scripting Basics - Variables, Conditionals, Loops, test
What You Will Achieve
- Explain the role of the shebang (
#!/bin/bash) and make a script executable - Use variables, positional parameters, special parameters, and command substitution correctly
- Test strings, numbers, and files with
test/[ ]/[[ ]] - Write control structures with
if/case/for/while/until - Choose between exit status and
exit/&&/||, and shell functions
This is the core of LPIC-1 objective 105.2 "Customize or write simple scripts" (LPIC 102). It is the smallest unit of technique for automating daily operations.
What Is the Basic Structure of a Shell Script?
A shell script consists of "shebang -> variable definitions -> processing body". The first-line shebang determines the interpreter, and once you grant execute permission with chmod +x, you can launch it with ./script.sh.
| Element | Syntax | Role |
|---|---|---|
| Shebang | #!/bin/bash |
Specify the interpreter to run |
| Variable | name=value |
Hold a value (no spaces around =) |
| Reference | $name / ${name} |
Expand the variable's value |
| Command substitution | $(command) |
Get command output as a value |
| Exit | exit N |
Exit with status N |
The kernel interprets the shebang when the first two bytes of a script are #!. #!/bin/bash runs under bash, and #!/bin/sh runs under a POSIX shell. This is the standard invocation method defined in the "INVOCATION" section of man bash.
The shebang must be at the very start of the first line. If a blank line or space precedes it, it is treated as an ordinary comment and has no effect.
Steps
Step 1: Write the shebang and make it executable
cat > hello.sh <<'EOF' #!/bin/bash echo "Hello, LPIC" EOF chmod +x hello.sh ./hello.sh
Hello, LPIC
The first line #!/bin/bash specifies the interpreter. Grant execute permission with chmod +x and launch with ./hello.sh. Passing the script explicitly to the interpreter as bash hello.sh requires neither execute permission nor a shebang, but in practice it is conventional to set both.
Step 2: Use variables and quoting
#!/bin/bash
name="Penguin Gym"
count=3
echo "${name} : ${count}"
echo "${name}_log"Penguin Gym : 3 Penguin Gym_log
Assignment uses the form name="value", and you must not put spaces around =. When referencing, wrapping it as ${name} in braces makes the variable-name boundary clear even when characters follow immediately, as in ${name}_log. Note the "..." quoting because the value contains a space.
Step 3: Read positional and special parameters
#!/bin/bash echo "Script name: $0" echo "First argument: $1" echo "Argument count: $#" echo "All arguments: $@"
Script name: ./args.sh First argument: alpha Argument count: 2 All arguments: alpha beta
$0 is the script name, and $1 through $9 are the arguments in order. $# is the number of arguments, and $@ and $* represent all arguments. The difference is that "$@" expands each argument as a separate string, while "$*" expands them as a single string (man bash "Special Parameters"). To loop over arguments, use "$@".
| Variable | Meaning |
|---|---|
$0 |
Script name |
$1 to $9 |
Positional parameters (nth argument) |
$# |
Number of arguments |
$@ / $* |
All arguments |
$? |
Exit status of the previous command |
$$ |
Process ID of the current shell |
$! |
Process ID of the most recent background command |
Step 4: Capture output into a variable with command substitution
#!/bin/bash
today=$(date +%F)
files=$(ls /etc | wc -l)
echo "${today} : ${files} items in /etc"2026-05-30 : 152 items in /etc
$(command) expands a command's standard output as a string. This is command substitution. It is equivalent to the older `command` (backtick) syntax, but $(...) is recommended for nesting and readability (man bash "Command Substitution").
Step 5: Test conditions with test / [ ] / [[]]
#!/bin/bash a="abc"; n=5 [ "$a" = "abc" ] && echo "string match" [ "$n" -gt 3 ] && echo "number: greater than 3" [ -f /etc/passwd ] && echo "file exists"
string match number: greater than 3 file exists
[ ... ] is an alias for the test command, and a space is required right after [ and right before ]. Use = and != for string comparison, -eq, -ne, -lt, -gt for numeric comparison, and -f (regular file), -d (directory), -e (exists), -r, -w, -x (read, write, execute permission) for file tests (man test). The bash extension [[ ... ]] suppresses word splitting and pathname expansion and lets you write &&, ||, <, > directly, so it is easier to handle when you are limited to bash.
| Kind | Operators | Example |
|---|---|---|
| String | = / != |
[ "$a" = "$b" ] |
| Numeric | -eq -ne -lt -gt -le -ge |
[ "$n" -eq 0 ] |
| File | -f -d -e -r -w -x |
[ -d /tmp ] |
Using > or < for numeric comparison inside [ ] is interpreted as "redirection" or "lexicographic string comparison" and produces unintended results. Always use -gt / -lt and the like for numeric comparison.
Step 6: Branch with if / case
#!/bin/bash
score=$1
if [ "$score" -ge 80 ]; then
echo "pass"
elif [ "$score" -ge 60 ]; then
echo "review"
else
echo "fail"
fipass
The basic form is if condition; then ... elif condition; then ... else ... fi. Crucially, if branches on whether the condition command's exit status is 0 (true). To branch on a set of values, case is more readable.
#!/bin/bash
case "$1" in
start) echo "starting" ;;
stop) echo "stopping" ;;
*) echo "usage: $0 {start|stop}" ;;
esacstarting
The form is case value in pattern) action ;; esac. Each branch ends with ;;, and *) catches "none of the above". This is the classic pattern for service startup scripts.
Step 7: Repeat with for / while / until
#!/bin/bash
for f in *.log; do
echo "processing: $f"
done
n=1
while [ "$n" -le 3 ]; do
echo "count: $n"
n=$((n + 1))
doneprocessing: access.log processing: error.log count: 1 count: 2 count: 3
for variable in value-list; do ... done processes a list in order. while condition; do ... done repeats while the condition is true, and until condition; do ... done repeats conversely until the condition becomes true. $((...)) is arithmetic expansion, used for integer calculation (man bash "Arithmetic Expansion").
Step 8: Receive input with read and exit with exit
#!/bin/bash
read -p "Enter your name: " who
if [ -z "$who" ]; then
echo "name is empty" >&2
exit 1
fi
echo "Welcome ${who}"
exit 0Enter your name: rina Welcome rina
read variable reads one line from standard input into a variable (-p shows a prompt). exit N ends the script with exit status N. By convention, 0 is success and 1 or more is failure. [ -z "$who" ] tests whether the string is empty.
How to Use Exit Status and && / ||
The exit status of the previous command is stored in $?. 0 is success and non-zero is failure. Using this value, you can chain commands with && (run the next if the previous succeeded) and || (run the next if the previous failed).
mkdir -p /tmp/work && echo "created" || echo "failed" echo "$?"
created 0
A && B runs B only when A's exit status is 0. A || B runs B only when A is non-zero. The if test is also based on this exit status, and once you understand the shell-specific definition of truth where "true = exit status 0", the behavior of conditional branching looks consistent.
You can also group processing in a shell function. Inside a function, return N sets the function's exit status, while exit N ends the entire script.
#!/bin/bash
greet() {
echo "Hello, $1"
return 0
}
greet "world"
echo "function return value: $?"Hello, world function return value: 0
Define a function with name() { ... } and call it with name arguments. Inside the function, arguments are also referenced with $1, $2, $#, because the positional parameters switch per call. When return is omitted, the exit status of the last command executed in the function is returned.
Common Mistakes and Fixes
| Mistake | Symptom | Correct form |
|---|---|---|
Spaces around = in assignment |
var: command not found |
var=value (no spaces) |
| Missing quotes | Arguments split on values with spaces | Wrap in double quotes, as [ "$var" = "x" ] |
Missing spaces in [ ] |
[: missing ']' |
[ "$a" = "$b" ] (space after [ and before ]) |
Using = / > for numeric comparison |
Misread as string comparison or redirection | Use -eq / -gt for numbers |
| Misreading exit status | if behaves backwards |
Understand that true means "exit status 0" |
A particularly common one is a space in assignment. If you write var = value, the shell interprets var as the command name and = and value as arguments, tries to run it, and reports command not found.
Troubleshooting
Symptom: The script will not start with "Permission denied"
Cause: No execute permission, or an invalid shebang
Check:
ls -l script.sh head -n 1 script.sh
Fix: Grant execute permission with chmod +x script.sh. Also confirm that the shebang is on the first line with no blank line before it. As a workaround, you can run it directly with bash script.sh.
Symptom: Variable assignment reports "command not found"
Cause: There are spaces around =
Check:
bash -n script.sh
Fix: Remove the spaces, as in var=value. bash -n can be used for a syntax check (it checks grammar only, without executing).
Symptom: Conditional branching breaks when a value contains spaces
Cause: The variable reference is not quoted, so word splitting occurs
Check:
bash -x script.sh
Fix: Wrap the variable in double quotes, as [ "$var" = "value" ]. bash -x displays the expanded result of each command, so you can pinpoint where the splitting occurs.
Completion Checklist
- [ ] Wrote the shebang (
#!/bin/bash) on the first line - [ ] Granted execute permission with
chmod +x - [ ] Confirmed there are no spaces around
=in assignments - [ ] Quoted variable references as
"$var" - [ ] Checked the spaces inside
[ ]and the comparison operators (string vs numeric) - [ ] Returned an exit status with
exit 0/exit 1
Summary
| Scenario | Syntax | Purpose |
|---|---|---|
| First line | #!/bin/bash |
Specify the interpreter |
| Get a value | $(command) |
Turn command output into a variable |
| String test | [ "$a" = "$b" ] |
Equal / not equal |
| Numeric test | [ "$n" -gt 3 ] |
Magnitude comparison |
| Branch | if / case |
Branch by condition or value |
| Repeat | for / while / until |
Loop processing |
| Chain | A && B / A || B |
Chain by exit status |
Shell scripting is the basis of Linux operations automation. After covering 105.2, combine it with environment variables (105.1) and text-processing commands and regular expressions to write practical automation scripts.