Exam weight: 4 โ LPIC-1 v5, Exam 102
What You Need to Know
From the official LPIC-1 objectives:
- Apply standard
shsyntax: loops and conditionals. - Use command substitution.
- Test command return values to determine success or failure.
- Chain commands together.
- Send conditional notifications to the root administrator.
- Select the correct script interpreter via the shebang line
#!. - Manage script location, ownership, execute permissions, and SUID.
Key files, terms, and utilities: for, while, test, if, read, seq, exec, ||, &&.
Shebang and Interpreter Selection
The first line of a script begins with #! (shebang) followed by the absolute path to the interpreter. The kernel reads this line when executing the file and passes it to the named program. Without a shebang the script still runs, but through the current shell โ which may not be the intended behavior.
#!/bin/bash
echo "Hello, world"
Common options:
#!/bin/sh: POSIX compatibility, minimal feature set#!/bin/bash: Bash extensions, arrays,[[ ]]#!/usr/bin/env python3: find the interpreter viaenvusing$PATH
There are two ways to run a script. By filename (./script.sh) โ the kernel reads the shebang and starts the interpreter. By explicit interpreter call (bash script.sh) โ the shebang is ignored and the named program runs.
Running a Script, Comments, and Subshell
Lines inside a script that begin with # are comments and are ignored by the interpreter. Exception: the first shebang line #!. Empty lines are also skipped. Multiple commands can go on one line separated by semicolons, but in Bash a newline is equivalent to a semicolon, so each command on its own line is more readable.
The .sh suffix has no special meaning to the interpreter โ filenames are free-form. The extension is useful only for quickly identifying scripts when browsing directories.
The echo command adds a newline at the end of its output by default. The -n option suppresses the trailing newline so the next output appears on the same line:
echo -n "Now: "
date +%H:%M
For other users to run the script, they need read permission. chmod o+r script.sh grants read access to everyone. Read permission is needed when the script is run via bash script.sh. Running by filename additionally requires the x bit (see Script Location and Permissions).
When run normally, the script executes in a new child shell (subshell). This protects the current session: variables and directory changes made inside the script do not affect the parent shell. This behavior is desirable for most tasks.
To execute a script’s instructions directly in the current session โ without a subshell โ use:
source script.sh
. script.sh
A space between the dot and the filename is required. This is useful when a script sets environment variables or functions that should persist in the current shell (typical use: profile files like .bashrc, .profile).
The exec command before a script or another command completely replaces the current shell process. After the script finishes, the session itself closes:
exec ./long_task.sh
# execution never reaches this line
After a normal script run, the script’s exit code is available in $?.
Basic sh Syntax
Variables
Assignment โ no spaces around the equals sign; access via $:
NAME="Maks"
echo "Hello, $NAME"
By convention variable names are uppercase, but this is not required. A name cannot begin with a digit or other non-alphanumeric character.
Variable value length (number of characters) is returned by prefixing # inside curly braces:
OS=$(uname -o)
echo $OS # GNU/Linux
echo ${#OS} # 9
Double quotes expand variables and command substitutions; single quotes pass everything literally.
Script Parameters
Inside a script, positional parameters are available as variables:
$0: the script name itself$1, $2, โฆ, $9: first, second, and so on arguments$#: number of arguments passed$@: all arguments as a list, each as a separate word with "$@"$*: all arguments as a single string$?: exit code of the last executed command$$: PID of the current script$!: PID of the last background process
#!/bin/bash
echo "Script: $0"
echo "First argument: $1"
echo "Total arguments: $#"
Parameters with numbers above nine require curly braces: ${10}, ${11}, and so on. Writing $10 is interpreted as $1 followed by the character 0.
Bash Arrays
Bash supports one-dimensional arrays. An array can be declared explicitly or initialized immediately:
declare -a SIZES
SIZES=( 1048576 1073741824 )
Indexing starts at zero. Reading an element requires curly braces and square-bracket indices:
echo ${SIZES[0]} # 1048576
echo ${SIZES[1]} # 1073741824
Writing an element does not require curly braces:
SIZES[0]=2097152
The length of an individual element is returned by ${#ARR[N]}. The total number of elements in the array: ${#ARR[@]} or ${#ARR[*]}:
echo ${#SIZES[0]} # 7 (length of string "2097152")
echo ${#SIZES[@]} # 2 (total elements)
An array is conveniently created from command output โ each word separated by space, tab, or newline becomes an element:
FS=( $(cut -f 2 < /proc/filesystems) )
echo ${FS[0]} ${FS[1]} ${FS[2]}
The field separator is controlled by the IFS variable (Input Field Separator). Default: space, tab, newline. To split only on newline:
IFS=$'\n'
Command Substitution
Command substitution takes a program’s standard output and substitutes it at the call site. Two forms are supported. Backticks come from POSIX; the $(...) form is modern, more readable, and supports nesting.
TODAY=$(date +%Y-%m-%d)
echo "Today: $TODAY"
# Equivalent with backticks
TODAY=`date +%Y-%m-%d`
# Nested substitutions
KERNEL_DIR=/lib/modules/$(uname -r)
The substitution result is usually stored in a variable or passed directly to another command:
mkdir backup-$(date +%F)
USER_COUNT=$(wc -l < /etc/passwd)
Arithmetic
Bash handles integers two ways. The old POSIX approach via expr; the modern built-in $(( )).
SUM=`expr $VAL1 + $VAL2`
SUM=$(( $VAL1 + $VAL2 ))
Spaces between numbers and the operator are required in expr. Inside $(( )) spaces are optional and the operators +, -, *, /, %, ** (power) are supported:
echo $(( 1024 ** 2 )) # 1048576
echo $(( 1024 ** 3 )) # 1073741824
SIZES=( $((1024**2)) $((1024**3)) )
Command substitution can be combined with arithmetic โ for example, get free RAM bytes from the second line of /proc/meminfo:
FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` ))
echo "Free: $(( $FREE / 1024**2 )) MB"
Inside $(( )), variable names can be written without $: $(( VAL1 + VAL2 )) works the same.
Script Output: echo and printf
echo and Escape Sequences
The echo command prints a string and adds a newline at the end. The -n option suppresses the trailing newline. The -e option enables interpretation of backslash escape sequences:
\n: newline\t: tab\\: backslash itself\": double quote
echo -e "Name:\tMaks\nCity:\tSydney"
When using -e always wrap the string in quotes โ otherwise the shell may consume the backslashes before echo sees them.
printf
The printf command formats output according to a template, like the C function of the same name. The first argument is the format string; the rest are values to substitute into the markers.
%s: string%d: integer%f: floating-point number%x: hexadecimal%%: literal percent sign
OS=$(uname -o)
FREE_MB=1491
printf "OS:\t%s\nMemory:\t%d MB\n" "$OS" "$FREE_MB"
Key differences between printf and echo:
- No automatic trailing newline โ
\nin the template is required - Markers are replaced in order from the arguments
- If there are more values than markers, the template repeats
- The template can be saved in a variable to switch output formats
FMT_TXT='Name: %s, age: %d\n'
FMT_CSV='%s,%d\n'
printf "$FMT_TXT" "Maks" 30
printf "$FMT_CSV" "Maks" 30
Full format documentation: man 3 printf.
Return Values and the test Command
Exit Code
Every command returns an integer from 0 to 255 when it finishes. Zero means success; anything else is an error. The number is in $? immediately after:
ls /etc > /dev/null
echo $? # 0
ls /no_such_dir 2>/dev/null
echo $? # non-zero, typically 2
A script sets its exit code with exit N (0โ255).
File Checks
The test command evaluates a condition and returns 0 for true, 1 for false. The equivalent square-bracket form [ ... ] is used more often. Spaces around every element inside the brackets are mandatory.
-e FILE: file or directory exists-f FILE: regular file-d FILE: directory-L FILE: symbolic link-r FILE: readable-w FILE: writable-x FILE: executable-s FILE: non-zero sizeFILE1 -nt FILE2: FILE1 is newer than FILE2FILE1 -ot FILE2: FILE1 is older than FILE2
test -f /etc/passwd
[ -f /etc/passwd ]
[ -d /etc ] && echo "Directory found"
String Comparisons
STR1 = STR2: strings are equalSTR1 != STR2: strings differ-z STR: string is empty-n STR: string is non-empty
Numeric Comparisons
-eq: equal-ne: not equal-lt: less than-le: less or equal-gt: greater than-ge: greater or equal
[ "$AGE" -ge 18 ] && echo "Adult"
In Bash the extended form [[ ... ]] is safer: variables don’t need quoting, glob patterns and regex are supported, and && and || can be used inside. For arithmetic the (( ... )) form is convenient.
if [[ "$FILE" == *.log ]]; then echo "log file"; fi
if (( COUNT > 10 )); then echo "many"; fi
Conditional Constructs
if / then / elif / else
if [ -f /etc/passwd ]; then
echo "File found"
elif [ -d /etc/passwd ]; then
echo "It's a directory"
else
echo "Not found"
fi
The if condition is any command โ its exit code is used, not its output. So if grep -q root /etc/passwd; then ... fi works correctly.
case
case "$1" in
start)
echo "Starting service"
;;
stop)
echo "Stopping service"
;;
restart|reload)
echo "Reloading"
;;
*)
echo "Usage: $0 {start|stop|restart|reload}"
exit 1
;;
esac
Matching in case uses glob patterns, not regular expressions. Each branch ends with ;;. Closed with esac.
Loops: for, while, until
The for loop iterates over a list of elements:
for i in 1 2 3 4 5; do
echo "Number $i"
done
for f in *.txt; do
echo "File: $f"
done
for i in $(seq 1 10); do
echo $i
done
Bash also supports C-style syntax:
for ((i=0; i<10; i++)); do
echo $i
done
The while loop repeats a block as long as the condition is true:
COUNT=0
while [ $COUNT -lt 5 ]; do
echo $COUNT
COUNT=$((COUNT + 1))
done
# Read a file line by line
while read LINE; do
echo "$LINE"
done < input.txt
The until loop works as the mirror image โ it repeats while the condition is false:
until ping -c1 -W1 host >/dev/null 2>&1; do
sleep 5
done
echo "Host is up"
read, seq, exec
read
Reads a line from standard input and stores it in one or more variables. If multiple variables are given, words are split on IFS.
read -p "Enter name: " NAME
echo "Hello, $NAME"
read FIRST LAST <<< "John Smith"
echo "$LAST, $FIRST"
If no variable name is given, the input is stored in REPLY:
echo "Continue (y/n)?"
read
echo "Answer: $REPLY"
When there are more words than variable names, the extra words are appended to the last variable.
Useful options:
-p TEXT: show a prompt-s: silent input (for passwords)-t SEC: input timeout-n N: read exactly N characters-r: do not interpret backslashes-a ARRAY: read words into a Bash array
seq
Prints a numeric sequence.
seq 5 # 1 2 3 4 5
seq 2 8 # 2 3 4 5 6 7 8
seq 1 2 9 # 1 3 5 7 9 (step 2)
seq -w 1 10 # zero-padded: 01 02 ... 10
seq -s, 1 5 # 1,2,3,4,5 (custom separator)
In a for loop:
for i in $(seq 1 100); do
curl -s "https://example.com/page$i" -o "page$i.html"
done
exec
exec has two modes. With an argument it replaces the current process with the named program โ the script does not continue:
exec ls /tmp
echo "this line never runs"
Without an argument (with redirection only), exec changes the file descriptors of the current shell. This is handy when all subsequent output should go to a log:
#!/bin/bash
exec >> /var/log/myscript.log 2>&1
echo "This and everything after goes to the log"
date
Command Chains
Multiple commands can be joined on one line. The separator determines execution logic.
cmd1 ; cmd2: run both sequentially, ignoring the resultcmd1 && cmd2: run cmd2 only if cmd1 succeeded (exit 0)cmd1 || cmd2: run cmd2 only if cmd1 failed (non-zero exit)
mkdir backup && cp *.conf backup/
ping -c1 host >/dev/null || echo "Host unreachable"
[ -f config ] && source config || echo "config missing"
These operators replace short if statements. Long chains are better rewritten as if blocks.
Conditional Mail to the Administrator
Standard practice: notify root when something goes wrong. The message is piped to mail.
backup.sh || echo "Backup failed on $(hostname)" | mail -s "Backup error" root
Checking a service:
if ! systemctl is-active --quiet sshd; then
echo "SSH stopped on $(hostname) at $(date)" | mail -s "ALERT: sshd" root
fi
Disk usage threshold:
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d %)
if [ "$USAGE" -gt 90 ]; then
df -h | mail -s "Root nearly full on $(hostname)" root
fi
The mail command delivers to the local root mailbox when an MTA (postfix, sendmail, exim) or local delivery agent is configured.
Script Location and Permissions
Common places to store scripts:
~/bin or ~/.local/bin: personal user scripts; many distros add these to $PATH automatically/usr/local/bin: shared local scripts for all users/usr/local/sbin: administrative scripts accessible only to root/etc/cron.daily, /etc/cron.hourly: scripts run periodically/etc/init.d: legacy SysV service init scripts
Make a script executable:
chmod +x myscript.sh
chmod 755 myscript.sh # rwxr-xr-x
chmod 750 myscript.sh # rwxr-x--- (owner and group only)
For a script to be callable by short name, its directory must be in $PATH. Otherwise specify the full or relative path.
SUID on Scripts
The SUID bit (octal 4000) normally gives the process the owner’s privileges at runtime. Set it with:
chmod u+s script.sh
chmod 4755 script.sh
The Linux kernel ignores SUID on interpreted scripts for security reasons (race-condition vulnerability during shebang interpretation). The bit can be set but has no effect. SUID only works on compiled binaries. If a script needs root privileges, configure sudo via /etc/sudoers:
# /etc/sudoers
maks ALL=(root) NOPASSWD: /usr/local/sbin/backup.sh
Quick Reference
| Construct | Purpose |
|---|---|
#!/bin/bash | Shebang for bash |
#!/bin/sh | Shebang for POSIX shell |
# comment | Ignored by the interpreter |
$0, $1โฆ$9 | Script name, positional arguments |
${10}, ${11} | Parameters with index above 9 |
$# | Number of arguments |
$@, $* | All arguments |
$? | Exit code of last command |
$$ | PID of current process |
$! | PID of last background process |
${#VAR} | Length of variable value |
$(cmd) or `cmd` | Command substitution |
$(( EXPR )) | Bash integer arithmetic |
expr A + B | POSIX arithmetic |
declare -a ARR | Declare an array |
ARR=( a b c ) | Populate an array |
${ARR[0]} | Read an element |
${#ARR[@]} | Number of elements in array |
IFS=$'\n' | Field separator: newline only |
source FILE or . FILE | Run script in current session |
exec CMD | Replace current process |
exec >> FILE | Redirect script output |
echo -n | Output without trailing newline |
echo -e "\t\n" | Interpret escape sequences |
printf "%s\n" "$VAR" | Formatted output |
test COND, [ COND ] | Evaluate condition |
[[ COND ]] | Extended Bash test |
(( EXPR )) | Bash arithmetic test |
if ... then ... elif ... else ... fi | Conditional block |
case ... in ... esac | Multiple branch select |
for VAR in LIST; do ... done | Iterate over a list |
while COND; do ... done | Loop with precondition |
until COND; do ... done | Loop with inverted condition |
read VAR | Read a line from stdin |
read (no name) | Store in REPLY |
read -p "?" -s VAR | Read password with prompt |
seq START STEP END | Numeric sequence |
cmd1 && cmd2 | Run on success |
cmd1 || cmd2 | Run on failure |
chmod +x FILE | Make executable |
chmod 4755 FILE | SUID + rwxr-xr-x |
mail -s "subject" root | Send mail to the administrator |
Exam Questions
- What is the shebang and where does it go? โ
#!interpreteron the very first line. - Practical difference between
#!/bin/shand#!/bin/bash? โshis POSIX-only;bashadds arrays,[[ ]], and$(( )). - How do you get the exit code of the last command? โ
$?. - What does exit code 0 mean? โ Success.
- Which command substitution form is preferred โ backticks or
$()? โ$(): supports nesting, more readable. - Difference between
&&,||, and;? โ&&on success;||on failure;;always. - How do you send mail to root if a command failed? โ
cmd || echo "msg" | mail -s "subject" root - Which command reads a line from stdin into a variable? โ
read. - Into which variable does
readstore input when no name is given? โREPLY. - Which
readoption hides input for passwords? โ-s. - What does
seq 1 2 7print? โ1 3 5 7. - What does
execdo with an argument vs without one? โ With: replaces the current process. Without (with redirect): redirects the shell’s file descriptors. - Difference between
bash script.sh,source script.sh, and. script.sh? โbashruns in a subshell;source/.run in the current session. - How do you make a script executable in octal notation? โ
chmod 755 script.sh. - Where are personal, shared, and administrative scripts stored? โ
~/bin,/usr/local/bin,/usr/local/sbin. - Does SUID work on shell scripts in modern Linux? โ No โ the kernel ignores it.
- Which
testoperators check file existence, directory, and executability? โ-e,-d,-x. - Difference between
[ ]and[[ ]]in Bash? โ[[ ]]is a Bash extension: no quoting required, supports patterns and regex, allows&&/||inside. - How do you loop over all files in a directory with
for? โfor f in /dir/*; do ...; done. - How do you get the length of a variable’s value? โ
${#VAR}. - How do you access the tenth positional parameter? โ
${10}. - Difference between
echo -eand plainecho? โ-einterprets\n,\t, etc. - Difference between
echoandprintfregarding line endings? โechoalways adds a newline;printfonly if the template contains\n. - How do you declare an array and access its elements? โ
ARR=( a b c ); access with${ARR[0]}. - How do you get the total number of elements in an array? โ
${#ARR[@]}. - What does
IFSstore and why change it? โ The field separator for word splitting; set to$'\n'to prevent splitting on spaces. - How do you calculate an arithmetic expression with the Bash built-in? โ
$(( EXPR )). - What does
execdo before a script? โ Replaces the current shell with the script; the session ends when the script finishes.
Exercises
Guided Exercise 1 โ Reading a Password without Echo
The -s option of read is useful for password input: the typed text is not shown on screen. How do you store the user’s input in the variable PASSWORD?
Answer
read -s PASSWORD
-sis often combined with-pfor a prompt and-tfor a timeout. Full interactive form:read -s -p "Password: " PASSWORD. Addechoimmediately after to print a newline โ Enter with suppressed echo does not emit one.
Guided Exercise 2 โ Saving whoami Output to a Variable
How do you save the output of whoami into the variable WHO inside a Bash script?
Answer
WHO=`whoami`
or
WHO=$(whoami)
The
$()form is preferred: clearer and supports nesting (e.g.,LOG="$(whoami)-$(date +%F).log"). Backticks are retained for compatibility with older POSIX scripts.
Guided Exercise 3 โ Conditional Execution with an Operator
Which Bash operator should appear between apt-get dist-upgrade and systemctl reboot if root wants to reboot only when the upgrade completes successfully?
Answer
The && operator:
apt-get dist-upgrade && systemctl reboot
&&runs the right-hand command only if the left-hand command returned exit code 0. The symmetric||runs the right-hand command on a non-zero code. Use;or a newline if the reboot should happen regardless.
Explorational Exercise 1 โ Permission Denied When Running a Script
When trying to run a freshly created Bash script the user gets:
bash: ./script.sh: Permission denied
The file was created by the same user. What is the likely cause?
Answer
The file does not have the execute bit set.
Running via
./script.shrequires read and execute permissions. A newly created file has noxbit by default. Fix withchmod +x script.sh. Alternative without thexbit: call the interpreter explicitly โbash script.sh. That requires only read permission because the file is read by the interpreter, not executed by the kernel.
Explorational Exercise 2 โ Identifying the Call Name via a Symbolic Link
Let do.sh be an executable script and undo.sh a symbolic link to it. How can the script determine from within itself which name was used to call it?
Answer
The special variable $0 contains the filename under which the script was invoked.
#!/bin/bash
case "$(basename "$0")" in
do.sh) echo "Mode: apply" ;;
undo.sh) echo "Mode: undo" ;;
*) echo "Unknown call mode: $0" ;;
esac
This technique (one script, multiple symbolic links) is widely used in Unix. Classic example: BusyBox โ a single binary that behaves like
ls,cat,grep, and dozens of other commands depending on$0.basenamestrips the path and leaves only the filename.
Explorational Exercise 3 โ Conditional Mail to Root on Non-Zero Exit Code
On a system with a configured mail service the command
mail -s "Maintenance Error" root <<<"Scheduled task error"
sends a notification to root. Write an if construct that executes this mail command if the exit code of the previous command is non-zero.
Answer
if [ "$?" -ne 0 ]; then mail -s "Maintenance Error" root <<<"Scheduled task error"; fi
[ "$?" -ne 0 ]compares$?with zero. Quotes around"$?"guard against empty-value errors โ though$?always holds a number. Alternative compact form via||:previous_command || mail -s "Maintenance Error" root <<<"Scheduled task error". Drawback of||: it must be chained to the previous command on the same line.
LPIC-1 Study Notes | Topic 105: Shells and Shell Scripting