Learn › RHCSA (EX200) › Shell Scripts
test - a hands-on Linux lab on a real virtual machine.
Make a script decide with test and its [ ] twin. File tests -f -d -e -r -w -x, string compares = != -z -n, numeric compares -eq -ne -lt -le -gt -ge, the exit status read from $?, chaining with && and , and the full if / elif / else / fi block. Serves EX200 shell-scripts task 8: branch on a condition and set the exit code.
You have a script that copies a config file into place. It runs fine on the machine you wrote it on. Then it runs on a machine where the source file is missing, and it copies nothing, reports nothing, and exits as if all is well. The next command downstream trusts that lie and breaks something real.
The fix is not more commands. It is a decision. Before it acts, the script has to ask a yes/no question, does this file exist, and take a different path for yes than for no. That single skill, branching on a condition, is EX200 shell-scripts task 8: conditionally execute code with if, test, and [ ]. By the end of this lesson you will write the branch and set the exit code the grader reads.
The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The outputs shown are from a real RHEL 10 machine (AlmaLinux 10.2). The exact files and accounts on your machine may differ, but the exit codes and the branch logic are the same everywhere, and those are the lesson.
test is a real command whose only job is to answer one yes/no question and report the answer as an exit status. It prints nothing. Instead it exits 0 when the question is true and a non-zero value (usually 1) when it is false. That flipped convention is the heart of shell logic: in bash, exit 0 means success and therefore true; any non-zero exit means failure and therefore false.
The simplest form is test -f FILE, which asks is this a regular file that exists. You read the answer from the special variable $?, which always holds the exit status of the command you just ran. So test sets the status, and $? reads it back.
test is one of the oldest commands in the shell, older than most of the tools you know. Its authors gave it a second name that reads like punctuation: a single [. When you write [ -f FILE ], the shell runs the exact same test program. The opening bracket IS the command name, and the closing ] is just its final argument, there to make the line look balanced.
Because [ is a command and ] is an argument, the spaces around them are not optional. The shell splits a command line on spaces to find the program and its arguments. So [ -f FILE ] is four separate words it can read, while [-f FILE] looks like one unknown command named [-f. This one rule causes more exam-day syntax errors than any other. Spaces inside the brackets, always.
Start with the plainest question. Ask test whether /etc/passwd exists as a regular file, then read the answer out of $?. Before you run it, decide what you expect: /etc/passwd is on every Linux machine, so the answer should be true, which means exit 0.
Run the check and print its status:
test -f /etc/passwd; echo $?
prompt: student@servera:~$ answer: test -f /etc/passwd; echo $? output: 0 hint: Run test with -f and the path, then a semicolon, then echo the status variable: test -f /etc/passwd; echo $?
0. The file exists, so test succeeded, and success in the shell is exit 0, which is how bash spells true. Notice test printed nothing of its own; the only thing on screen is the 0 that echo $? pulled out of the status. That is the whole mechanic. test never talks; it sets a status, and $? is how you hear the answer.
Now aim test at a path that is not there. Ask whether /etc/nope exists as a regular file. You expect the opposite answer: false, which in the shell means a non-zero exit, and test uses 1.
test -f /etc/nope; echo $?
prompt: student@servera:~$ answer: test -f /etc/nope; echo $? output: 1 hint: Same shape, point it at a path that does not exist: test -f /etc/nope; echo $?
1. The file was not there, so test failed, and failure is any non-zero exit, which is how bash spells false. Put the two runs side by side and you have the entire idea: 0 for true, 1 for false. Every conditional you write from here rides on that one contrast.
Time to see that [ ] really is test wearing punctuation. There is a companion file test for directories: -d is true when the path exists and is a directory. Ask [ ] whether /etc is a directory, read the status, then ask about a directory that is missing.
You will run two checks. First the real directory:
[ -d /etc ]; echo $?
prompt: student@servera:~$ answer: [ -d /etc ]; echo $? output: 0 hint: The bracket IS test. Space after [ and before ], then read the status: [ -d /etc ]; echo $?
0. /etc is a directory, so the -d test is true, and [ -d /etc ] gave you the same exit 0 that test -d /etc would. The bracket form and the word test are interchangeable; pick whichever reads better. Try it against a missing directory, [ -d /etc/nope ]; echo $?, and you get 1, exactly as the missing-file test did. Same command, same rules.
/etc exists on every RHEL machine, so this pair is stable, but the LOGIC is what carries: -d for directories, -f for regular files, -e for either. Swap the path and the status follows the truth on your own machine.
test and [ ] understand three families of questions. You will reach for all three on the exam.
File tests ask about a path:
String compares ask whether two pieces of text match. Note the single =, not ==, is the portable form:
Numeric compares use letter flags, never the math symbols > and <, because inside [ ] those symbols mean something else entirely:
Always double-quote a variable inside a test: [ -z "$NAME" ], not [ -z $NAME ]. If $NAME is empty and unquoted, the shell erases the word entirely and the test becomes [ -z ], which is a syntax error, unary operator expected. The quotes hold an empty value in place as one argument.
Because test reports through the exit status, you can wire it directly to another command with the shell's logic operators. A && B runs B only if A succeeded (exit 0). A || B runs B only if A failed (non-zero). So [ ... ] && echo ... is a one-line if with no if keyword at all.
Ask whether two strings are equal, and print a message only when they are:
[ "abc" = "abc" ] && echo "strings equal"
prompt: student@servera:~$ answer: [ "abc" = "abc" ] && echo "strings equal" output: strings equal hint: Bracket test with a single =, then && to run echo only on success: [ "abc" = "abc" ] && echo "strings equal"
strings equal. The = compare was true, so the bracket exited 0, so && let the echo run. Flip one letter, [ "abc" = "xyz" ], and the compare fails, the bracket exits non-zero, and && swallows the echo so nothing prints. Two numeric versions read the same way: [ 5 -gt 3 ] && echo "5 is greater than 3" prints because five really is greater than three, and [ -z "" ] && echo "empty string is zero-length" prints because an empty string genuinely has zero length. Same pattern, three questions.
-gt is the numeric greater-than. Do not reach for > inside [ ]: there the > is a redirection, so [ 9 > 10 ] silently creates a file named 10 and reports success, which is never what you meant. Letters for numbers: -gt, -lt, -eq.
The one-liner is handy, but real branches want the readable block form. The keyword is if, it runs a test, and it opens with then, closes with fi, and can stack extra branches with elif and a final catch-all else.
Here is the shape, checking a number against two thresholds:
if [ "$n" -lt 10 ]; then
echo low
elif [ "$n" -lt 100 ]; then
echo medium
else
echo high
fi
Read it top to bottom. if tries the first test; if true, it runs that branch and skips the rest. If false, elif tries the next test. If every test fails, else runs. Exactly one branch fires. With n set to 42, the first test (-lt 10) is false, the second (-lt 100) is true, so it prints medium.
Run that exact block with n=42 as a single line:
n=42; if [ "$n" -lt 10 ]; then echo low; elif [ "$n" -lt 100 ]; then echo medium; else echo high; fi
prompt: student@servera:~$ answer: n=42; if [ "$n" -lt 10 ]; then echo low; elif [ "$n" -lt 100 ]; then echo medium; else echo high; fi output: medium hint: Set n=42, then the if/elif/else chain with semicolons before each then and a closing fi: ...elif [ "$n" -lt 100 ]; then echo medium; else echo high; fi
medium. The first test asked is 42 less than 10, which is false, so if skipped its branch. elif asked is 42 less than 100, which is true, so it ran echo medium and jumped straight to fi, never touching else. That is the guarantee of the block: the branches are tried in order and exactly one wins. Change n to 5 and you would see low; change it to 500 and you would see high.
Every if must close with fi, and every then must sit after a condition. On one line, that means a semicolon before each then and before fi. Forget the fi and bash waits forever for the block to end, then reports syntax error: unexpected end of file. In vim on the exam, this is the error you will fix most.
Scaffolding off. No command is shown this time. You have every piece you need.
A task wants a one-line branch that checks whether /etc is a directory. If it is, print found and let the line succeed. Think about which operator tests for a directory, and which logic operator runs the echo only when that test is true. Keep it to a single line using the bracket form.
prompt: student@servera:~$ answer: [ -d /etc ] && echo found output: found hint: Directory test is -d, and && runs the next command only on success. Build the bracket test for /etc, then && echo found.
found. The -d test confirmed /etc is a directory, so the bracket exited 0, and && let the echo run and print found. Because the last command in the line, the echo, succeeded, the whole line exits 0 too, which is the true status a script downstream would read. That is task 8 in miniature: a test decides, an action follows, and the exit code tells the truth about what happened. Swap -d /etc for -d /nope and the line prints nothing and exits non-zero, the honest report that the branch did not fire.
You earned this cheat sheet. Every row is a form you just ran or built:
The one convention to burn in for the exam: exit 0 is true, non-zero is false. test sets it, $? reads it, && and || act on it, and if branches on it.
[ ] is the portable POSIX form, and it is the one the exam expects because it works in any shell and in scripts that start #!/bin/bash. Bash also offers a richer [[ ]] with pattern matching and safer variable handling, but it is a bash extension, not portable. Learn [ ] cold for EX200; treat [[ ]] as a bonus for later.
The practice terminal has shown you the shape of a conditional: test and its [ ] twin, the file tests -f -d -e -r -w -x, string compares with = and -z and -n, numeric compares with -eq and friends, the exit status you read from $?, the && and || chains, and the full if / elif / else / fi block. Every one of those you typed yourself.
The shell-scripts module mission is where you run these against a real RHEL 10 machine. A full VM boots for you, with its own files to test and its own accounts to branch on. The mission hands you objectives that use exactly what you practiced here: check a condition, branch on the result, and set the exit code the grader reads. One difference from this lesson: the mission shows no commands. You read the objective, recall the if and the test, and type it. That recall is what makes it stick on exam day.
Finish the other shell-scripts lessons, then go make a real script decide for itself.
Practice Conditionals: if, test, [ ] in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.