sysadmin · May 25, 2026 · 14 min read

Two real shell scripts.
variables, conditionals, exit codes, logging.

ops1 wants a disk usage checker that fails gracefully and an idempotent user-creation tool that logs everything. Real scripts with argument handling and proper exit codes.

// what we’re getting into
  1. The ticket
  2. Concept review. The bash you actually need.
  3. Script one. diskcheck.
  4. Script two. adduser_safe.
  5. For loops with real data
  6. While loop with a threshold
  7. Exam questions
  8. Final checklist: confirm everything works

The ticket: ops1 needs two scripts in /usr/local/bin/, both runnable by the automation group. A diskcheck that reports the usage of any mountpoint passed as an argument and fails cleanly with a non-zero exit code if the mountpoint does not exist. And an adduser_safe that creates a user if they do not already exist, skips them if they do, and appends every action to an audit log.

The real problem: this is where scripting stops being one-liners. Argument handling, conditionals, exit codes, and logging, done the way a real tool would.

What we are doing: writing two genuine shell scripts, testing every failure path, not just the happy one.

The ticket

bash
id ops1                  # UID 7003
getent group automation  # GID 6500

Concept review. The bash you actually need.

The bash man page is enormous. The sections that matter here are SPECIAL PARAMETERS, CONDITIONAL EXPRESSIONS, and Compound Commands.

bash
man bash    # search /SPECIAL PARAMETERS and /CONDITIONAL
the essentials
#!/bin/bash        shebang, tells the kernel which interpreter to use
NAME="value"       a variable. read it back with $NAME
$1 $2 $@ $#        positional parameters, all args, and the arg count
$?                 exit code of the last command
if [ cond ]; then ... fi     conditional
for i in list; do ... done   loop over a list
while [ cond ]; do ... done  loop while true
-f file  -d dir  -z empty-string  -eq numeric-equal   common tests

Script one. diskcheck.

It takes a mountpoint as $1. If none is given, or the path is not actually a mountpoint, it prints a clear error and exits non-zero. Different failures get different exit codes so a caller can tell them apart.

bash
sudo bash -c 'cat > /usr/local/bin/diskcheck.sh << "EOF"
#!/bin/bash
# diskcheck.sh -- report disk usage for a given mountpoint
# usage: diskcheck.sh /mountpoint

MOUNT="$1"

if [ -z "$MOUNT" ]; then
    echo "ERROR: usage: $0 <mountpoint>"
    exit 1
fi

if ! mountpoint -q "$MOUNT"; then
    echo "ERROR: $MOUNT is not a mounted filesystem"
    exit 2
fi

USAGE=$(df -h "$MOUNT" | awk "NR==2 {print \$5}")
echo "Disk usage at $MOUNT: $USAGE"
exit 0
EOF'

sudo chmod 750 /usr/local/bin/diskcheck.sh
sudo chgrp automation /usr/local/bin/diskcheck.sh

# test every path: valid, invalid, missing arg
/usr/local/bin/diskcheck.sh / ; echo "exit: $?"
/usr/local/bin/diskcheck.sh /fakepath ; echo "exit: $?"
/usr/local/bin/diskcheck.sh ; echo "exit: $?"

-z tests for an empty string, mountpoint -q checks the path is a real mount, and each exit code is distinct so failures are diagnosable.

Script two. adduser_safe.

Idempotent means running it twice is safe. If the user exists it logs a skip and exits 0. If a named group does not exist it errors. Every outcome is appended to the audit log with a timestamp.

bash
sudo bash -c 'cat > /usr/local/bin/adduser_safe.sh << "EOF"
#!/bin/bash
# adduser_safe.sh -- idempotent user creation with an audit trail
# usage: adduser_safe.sh <username> [group]

LOGFILE="/var/log/user_provisioning.log"
USERNAME="$1"
GROUP="$2"
TS=$(date "+%Y-%m-%d %H:%M:%S")

if [ -z "$USERNAME" ]; then
    echo "ERROR: usage: $0 <username> [group]"
    exit 1
fi

if id "$USERNAME" &>/dev/null; then
    echo "$TS SKIPPED: user $USERNAME already exists" >> "$LOGFILE"
    echo "User $USERNAME already exists. Skipping."
    exit 0
fi

if [ -n "$GROUP" ]; then
    if ! getent group "$GROUP" &>/dev/null; then
        echo "$TS ERROR: group $GROUP does not exist" >> "$LOGFILE"
        echo "ERROR: group $GROUP does not exist"
        exit 2
    fi
    useradd -m -G "$GROUP" "$USERNAME"
    echo "$TS CREATED: $USERNAME (added to $GROUP)" >> "$LOGFILE"
else
    useradd -m "$USERNAME"
    echo "$TS CREATED: $USERNAME" >> "$LOGFILE"
fi
exit 0
EOF'

sudo chmod +x /usr/local/bin/adduser_safe.sh
sudo touch /var/log/user_provisioning.log
sudo chgrp automation /var/log/user_provisioning.log
sudo chmod 660 /var/log/user_provisioning.log

# test: create, then re-run (skip), then missing arg, then bad group
sudo /usr/local/bin/adduser_safe.sh testuser_a developers
sudo /usr/local/bin/adduser_safe.sh testuser_a developers
sudo /usr/local/bin/adduser_safe.sh
sudo /usr/local/bin/adduser_safe.sh testuser_b nonexistent_group
sudo cat /var/log/user_provisioning.log
sudo userdel -r testuser_a 2>/dev/null

id “$USERNAME” &>/dev/null is the existence check, its output thrown away because you only care about the exit status. -n tests for a non-empty string, the mirror of -z.

For loops with real data

Reading /etc/passwd line by line and acting on a field is a pattern you will reuse constantly.

bash
cat > ~/list_real_users.sh << 'EOF'
#!/bin/bash
printf "%-20s %-6s %s\n" "USERNAME" "UID" "HOME"
while IFS=: read -r user _ uid _ _ home _; do
    if [ "$uid" -ge 1000 ] && [ "$uid" -lt 65000 ]; then
        printf "%-20s %-6s %s\n" "$user" "$uid" "$home"
    fi
done < /etc/passwd
EOF
chmod +x ~/list_real_users.sh
~/list_real_users.sh

While loop with a threshold

A default value with ${2:-80} means the second argument is optional and falls back to 80.

bash
cat > ~/disk_watch.sh << 'EOF'
#!/bin/bash
MOUNT="$1"
THRESHOLD="${2:-80}"
for i in 1 2 3; do
    USAGE=$(df --output=pcent "$MOUNT" 2>/dev/null | tail -1 | tr -d ' %')
    if [ "$USAGE" -ge "$THRESHOLD" ]; then
        echo "WARN: $MOUNT at ${USAGE}% (threshold $THRESHOLD%)"
    else
        echo "OK: $MOUNT at ${USAGE}%"
    fi
    sleep 1
done
EOF
chmod +x ~/disk_watch.sh
~/disk_watch.sh / 50

Exam questions

Write the script before you run it.

Q1. Write a script that takes a filename as $1 and prints whether it is a regular file, a directory, or does not exist.

Q2. Write a script that takes a username as $1, and if they do not exist, creates them, adds them to group staff, and logs the action with a timestamp to /tmp/audit.log.

Q3. Loop through all users in /etc/passwd with UID 1000 and up and print username, UID, and home in a formatted table.

Answers.

bash
# A1
#!/bin/bash
if [ -f "$1" ]; then echo "$1 is a regular file"
elif [ -d "$1" ]; then echo "$1 is a directory"
else echo "$1 does not exist"; fi

# A2
#!/bin/bash
USER="$1"
getent group staff || sudo groupadd staff
if ! id "$USER" &>/dev/null; then
    sudo useradd -G staff "$USER"
    echo "$(date): created $USER, added to staff" >> /tmp/audit.log
else echo "user $USER already exists"; fi

# A3
awk -F: '$3 >= 1000 {printf "%-20s %-6s %s\n", $1, $3, $6}' /etc/passwd

Final checklist: confirm everything works

If every check passes, the ticket is done.

bash
# 1. both scripts exist and are executable
ls -l /usr/local/bin/diskcheck.sh /usr/local/bin/adduser_safe.sh

# 2. diskcheck returns 0 on a real mount, non-zero on a fake one
/usr/local/bin/diskcheck.sh / ; echo $?
/usr/local/bin/diskcheck.sh /nope ; echo $?

# 3. the audit log has entries
sudo cat /var/log/user_provisioning.log

# 4. all three exam scripts written from scratch
# 5. tracker entry checked off

Reply to ops1: both scripts are in /usr/local/bin/, group-owned by automation. The audit log is at /var/log/user_provisioning.log, group-writable by automation. Every failure path returns a distinct non-zero exit code.

next post
Onboarding contractors and a service account that cannot log in