Red Hat Certified System Administrator (RHCSA) #3 Shell scripting: conditionals, loops, arguments, exit codes
If #2 Essential tools built your shell foundation with redirection, pipes, find, and grep, this post ties those commands together into a shell script that automates them as a single task. The RHCSA exam regularly includes a task that says “write a simple shell script.” It isn’t grand programming — it’s a script of around 30 lines that takes arguments, branches on conditions, processes multiple targets in a loop, and returns the right exit code.
So the goal of this post isn’t to survey all of bash syntax, but to firmly lock in just enough of the core skeleton that your hands won’t freeze on the exam. From shebang to functions, we’ll pick out only the elements that actually appear on the exam and type them out ourselves.
shebang and execution #
The first line of a script specifies which interpreter runs it: the shebang. On RHCSA you write bash scripts, so you start with this one line.
#!/usr/bin/bash
echo "Hello, RHCSA"The file you write needs execute permission to run directly. The flow for granting permission and running it is as follows.
chmod +x hello.sh
./hello.shIf you haven’t granted permission, you can also run it by passing it directly to the interpreter, like bash hello.sh. That said, when the exam asks you to “make an executable script,” it’s safer to set execute permission with chmod +x.
While you’re writing a script, running it with bash -x script.sh shows how each line expands, so you can quickly find the cause when the behavior differs from what you intended.
Variables and quoting #
Assign to a variable with no spaces on either side of the equals sign, and prefix it with $ when you use it. Wrapping the name in braces when you read the value makes the variable name’s boundary clear.
name="rhcsa"
count=3
echo "$name exam is post number ${count}"Quoting is the core of script reliability. Double quotes expand variables; single quotes leave their contents literal.
dir="/etc/my dir"
ls "$dir" # passed as a single argument even with a space
ls $dir # split into two arguments, behaving against your intent
echo '$name' # prints $name itselfIf you build the habit of always wrapping a variable in double quotes when you use it, you can prevent errors that arise when a path contains spaces or when the value is empty.
Positional arguments #
Arguments passed to a script are received in order as $1, $2. Let’s also learn the variables that handle all the arguments and their count.
| Variable | Meaning |
|---|---|
$0 | Script name |
$1, $2 | First and second positional arguments |
$# | Number of arguments passed |
"$@" | List preserving each argument with its own quoting |
$* | All arguments joined into a single string |
The difference between "$@" and $* matters when you do loop processing on the exam. To iterate over each argument safely, use "$@" wrapped in double quotes.
#!/usr/bin/bash
echo "Script: $0"
echo "Argument count: $#"
echo "First argument: $1"
for arg in "$@"; do
echo "Processing target: $arg"
doneExit codes #
Every command leaves an exit code when it finishes. 0 means success, and a non-zero value means failure. You check the exit code of the most recent command with $?.
ls /etc/passwd
echo "$?" # 0 (success)
ls /no/such/path
echo "$?" # 2 (failure)A script returns its own exit code explicitly with exit. A grading script or another command judges success or failure by this value, so the habit of exiting with a non-zero value on failure is important.
if [[ -z "$1" ]]; then
echo "an argument is required" >&2
exit 1
fi
exit 0As in the example above, the convention is to print error messages to standard error with >&2.
test and condition checks #
You check conditions with the test command or the equivalent brackets. In RHEL’s bash, using [[ ]] is safer for quote handling.
The file-test operators you’ll use often are as follows.
| Operator | True when |
|---|---|
-e file | The file exists |
-f file | It is a regular file |
-d file | It is a directory |
-r / -w / -x | It has read / write / execute permission |
-z string | The string is empty |
-n string | The string is not empty |
String and numeric comparisons use different operators. Strings use = and !=; numbers use -eq, -ne, -lt, -le, -gt, -ge.
[[ "$user" = "root" ]] # is the string equal
[[ "$count" -gt 5 ]] # is the number greater than 5
[[ -f /etc/fstab ]] # does the file existif/elif/else #
You write conditional branches with if. Let’s memorize exactly the form that closes the block with then and fi.
#!/usr/bin/bash
file="/etc/hosts"
if [[ -f "$file" ]]; then
echo "$file is a regular file"
elif [[ -d "$file" ]]; then
echo "$file is a directory"
else
echo "$file not found"
fiThe condition slot can hold not only brackets but a command itself. If the command’s exit code is 0 it’s treated as true, so you can branch on whether a command succeeded, like this.
if grep -q "^myuser:" /etc/passwd; then
echo "the user exists"
ficase #
When a value splits into several branches, case reads more easily than a chain of if and elif. Each branch writes its pattern with ) and closes with ;;.
#!/usr/bin/bash
case "$1" in
start)
echo "starting the service" ;;
stop)
echo "stopping the service" ;;
restart)
echo "restarting the service" ;;
*)
echo "usage: $0 {start|stop|restart}" >&2
exit 1 ;;
esacThe final *) is the default branch that catches anything matching no pattern. It’s useful when the argument must be one of a fixed set of values.
for/while/until loops #
Loops connect directly to exam tasks like “create several users at once” or “process each item in a list.”
for iterates over each item in a list.
for user in alice bob carol; do
echo "creating user: $user"
doneYou make a numeric range with brace expansion or seq.
for n in {1..5}; do
echo "disk $n"
donewhile loops while the condition is true, and until loops until the condition becomes true. while read is often used to read a file line by line.
while read -r line; do
echo "line: $line"
done < /etc/hostnamecount=1
until [[ "$count" -gt 3 ]]; do
echo "attempt $count"
count=$((count + 1))
doneCommand substitution and arithmetic #
Use $(...) to capture a command’s output into a variable. It’s better than backticks for nesting and readability, so let’s standardize on $(...).
today=$(date +%F)
lines=$(wc -l < /etc/passwd)
echo "user line count as of $today: $lines"Integer arithmetic is performed inside $(( )). You can also write comparison conditions with (( )).
a=7
b=3
echo "$(( a + b ))" # 10
echo "$(( a * b ))" # 21
if (( a > b )); then
echo "a is larger"
fiTaking input with read #
When a script needs to ask the user for a value, you use read. -p prints a prompt alongside it.
#!/usr/bin/bash
read -p "Enter a username: " username
read -sp "Enter a password: " password
echo
echo "entered user: $username"-s does not echo the input to the screen, so it’s suited to password entry. That said, on the exam the argument-passing approach is more common, so it’s enough to know read as a secondary option.
Functions #
You group repeated work into a function. Inside a function, you receive the arguments passed to it as $1 and $2.
#!/usr/bin/bash
log() {
echo "[$(date +%T)] $1"
}
create_user() {
local name="$1"
if id "$name" &>/dev/null; then
log "$name already exists"
return 1
fi
useradd "$name" && log "$name created"
}
log "script start"
create_user aliceIt’s safer to declare variables inside a function with local so they don’t collide with what’s outside the function. A function returns an exit code with return.
&& and || #
The && and || that chain commands express a short branch in one line. && runs the next command when the previous one succeeded; || when it failed.
mkdir -p /data && echo "directory ready"
id myuser &>/dev/null || useradd myuserThe second line above expresses the idempotent task “create the user if it doesn’t exist” in one line, a form used often in scripts.
Hands-on: an argument-validating, loop-processing script #
Tying together the elements so far, let’s write a script that validates the users passed as arguments and then creates them all at once. It’s an example that captures the typical “write a script” task on the exam.
#!/usr/bin/bash
#
# Usage: ./mkusers.sh user1 user2 ...
# Creates the users passed in and reports the result via the exit code.
# 1) Validate arguments: if there are none, print usage and exit with failure
if [[ "$#" -eq 0 ]]; then
echo "usage: $0 username [username ...]" >&2
exit 1
fi
# 2) Check root privileges: useradd requires them
if [[ "$(id -u)" -ne 0 ]]; then
echo "this script must be run as root" >&2
exit 1
fi
fail=0
# 3) Iterate over all passed arguments and process them
for user in "$@"; do
if id "$user" &>/dev/null; then
echo "skipping: $user already exists"
continue
fi
if useradd "$user"; then
echo "created: $user"
else
echo "failed: error while creating $user" >&2
fail=$(( fail + 1 ))
fi
done
# 4) If there was even one failure, exit with a non-zero code
if (( fail > 0 )); then
echo "failed to create $fail user(s)" >&2
exit 1
fi
echo "all users processed"
exit 0This one script contains all the core of this post. It validates the argument count with $#, iterates over arguments safely with "$@", branches with if, moves to the next item with continue, counts failures with arithmetic (( )), and returns the exit code clearly with exit. The scripts the exam asks for are mostly variations on this skeleton.
Exam points #
- Start with the shebang
#!/usr/bin/bash, and leave execute permission withchmod +x. Don’t miss the requirement to make an executable script. - Always wrap variables in quotes as
"$var". This prevents malfunctions caused by spaces or empty values. - Distinguish the positional arguments
$1,"$@", and$#precisely. For loop processing, thefor x in "$@"form is safe. - For exit codes,
exitwith 0 on success and a non-zero value on failure. Grading sometimes looks at the exit code. - Memorize the pairs
if/then/fi,case/esac, andfor/do/done. Forgetting a closing keyword is a common mistake. - When you get stuck on syntax, read the
/CONDITIONALsection ofman bash. It’s the way to check conditional operators without the internet. - While writing, debug by watching the expansion with
bash -x script.sh.
Wrap-up #
What this post locked in:
- shebang, variable quoting, positional arguments. The foundation that builds a script’s input and skeleton
- Exit codes (
$?,exit). The convention that signals success and failure with 0 and non-zero values - Conditionals (
test,[[ ]],if,case) and loops (for,while,until). The core syntax for branching and iteration - Command substitution
$(), arithmetic(( )),read, functions,&&,||. The tools that make a script practical - Hands-on script. The archetype that validates arguments, processes multiple targets in a loop, and reports the result via the exit code
RHCSA’s scripting task is solved not with fancy syntax but with the fundamentals of taking arguments, processing them by condition, and returning the result correctly. Drill the skeleton above into your hands and you won’t freeze on the exam.
Next: booting and the system #
You’ve laid the foundation of the shell and scripting. Now we go into how the system itself powers on and runs.
In #4 Booting and the system: systemd, target, GRUB2, password recovery, we’ll follow along firsthand through the flow by which systemd drives booting, target switching, GRUB2 bootloader configuration, and the exam-favorite root password recovery procedure.