Shell Scripting Basics - Variables, Conditionals, Loops, test

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"
fi
pass

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}" ;;
esac
starting

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))
done
processing: 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 0
Enter 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.

Next Reading

Continue Your LPIC-1 Journey

LPIC-1 Hub

  • LPIC-1 Learning Hub — Full LPIC-1 article map, progress tracking, and exam objective coverage

Practice