Linux Basic — Working with Strings Practice Exercises

4 minute read

Working with Strings — Practice Exercises

Five exercises that cover every operator in the chapter. Try each yourself before peeking.


Exercise 1 — Required Environment Variables

Goal: At the top of a script, enforce that API_TOKEN, REGION, and ENV are all set and non-empty. Print one error per missing var with a clear name, then exit. If all are present, print a confirmation.

💡 Show Answer
#!/usr/bin/env bash
set -u

missing=()
for v in API_TOKEN REGION ENV; do
    [[ -n "${!v:-}" ]] || missing+=("$v")
done

if ((${#missing[@]})); then
    printf 'Missing required env var: %s\n' "${missing[@]}" >&2
    exit 1
fi

echo "All required env vars are set."

${!v} is indirect expansion: it expands to the value of the variable whose name is $v. The :- keeps set -u happy.

A more compact one-liner per var:

: "${API_TOKEN:?API_TOKEN required}"
: "${REGION:?REGION required}"
: "${ENV:?ENV required}"

…but it exits on the first missing var rather than reporting all of them.


Exercise 2 — Native basename and dirname

Goal: Write two functions, bname and dname, that mimic basename and dirname using only parameter expansion — no forks. They must handle:

  • A normal path: /var/log/nginx/access.log
  • A trailing slash: /var/log/nginx/
  • A bare filename: hello.txt
  • An empty input
💡 Show Answer
bname() {
    local p=${1%/}                  # strip any trailing slash
    [[ -z "$p" ]] && { echo /; return; }   # input was just "/"
    echo "${p##*/}"
}

dname() {
    local p=${1%/}
    [[ -z "$p" ]] && { echo /; return; }
    local d=${p%/*}
    [[ "$d" == "$p" ]] && d=.      # no slash in input
    echo "$d"
}

for f in "/var/log/nginx/access.log" "/var/log/nginx/" "hello.txt" "/" ""; do
    printf '%-30s -> bname=%-15s dname=%s\n' "[$f]" "$(bname "$f")" "$(dname "$f")"
done
# [/var/log/nginx/access.log]    -> bname=access.log     dname=/var/log/nginx
# [/var/log/nginx/]              -> bname=nginx          dname=/var/log
# [hello.txt]                    -> bname=hello.txt      dname=.
# [/]                            -> bname=/              dname=/
# []                             -> bname=/              dname=/

Edge cases handled: trailing slash via ${1%/}, empty / bare slash, dirname of a single component returns ..


Exercise 3 — Slugify a Title

Goal: Convert any blog-post title into a URL-safe slug:

  • lowercase
  • spaces → -
  • strip all characters except a-z, 0-9, -
  • collapse multiple consecutive - into one
  • trim leading/trailing -

Input: " Hello, World!! -- 2026 Update " Expected: hello-world-2026-update

💡 Show Answer
slugify() {
    local s=${1,,}                  # lowercase
    s=${s// /-}                     # spaces to hyphens
    # Strip everything not a-z, 0-9, or -
    local out=""
    local i ch
    for ((i=0; i<${#s}; i++)); do
        ch=${s:i:1}
        [[ "$ch" =~ [a-z0-9-] ]] && out+="$ch"
    done
    # Collapse multiple hyphens
    while [[ "$out" == *--* ]]; do
        out=${out//--/-}
    done
    # Trim leading / trailing hyphens
    out=${out#-}
    out=${out%-}
    echo "$out"
}

slugify "  Hello, World!! -- 2026 Update  "
# hello-world-2026-update

slugify "Café & Cookies"
# caf-cookies     (multi-byte é dropped — for codepoint support you'd need a different tool)

Or the same with tr + sed (if you’re allowed):

slugify_fast() {
    echo "${1,,}" \
        | tr -c 'a-z0-9' '-' \
        | tr -s '-' \
        | sed 's/^-//;s/-$//'
}

Exercise 4 — Parse a key=value Config File

Goal: Read a config file with key=value lines (and # comments) into an associative array, then print every key/value pair. Handle:

  • Blank lines
  • Lines starting with # (comment)
  • Whitespace around =
  • Quoted values: name="Alice Smith" (strip the quotes)

Setup:

cat > /tmp/app.conf <<'EOF'
# Sample config
host = example.com
port=8080
name = "Alice Smith"

debug=true
EOF
💡 Show Answer
declare -A cfg

while IFS= read -r line; do
    # Skip blanks and comments
    [[ -z "${line// }"   ]] && continue
    [[ "$line" =~ ^[[:space:]]*# ]] && continue

    # Split on first =
    key=${line%%=*}
    val=${line#*=}

    # Trim whitespace
    key=${key#"${key%%[![:space:]]*}"}; key=${key%"${key##*[![:space:]]}"}
    val=${val#"${val%%[![:space:]]*}"}; val=${val%"${val##*[![:space:]]}"}

    # Strip surrounding quotes
    [[ "$val" == \"*\" ]] && val=${val:1:-1}

    cfg["$key"]="$val"
done < /tmp/app.conf

for k in "${!cfg[@]}"; do
    printf '%-8s = [%s]\n' "$k" "${cfg[$k]}"
done
# host     = [example.com]
# port     = [8080]
# name     = [Alice Smith]
# debug    = [true]

The two key=${key#"${key%%[![:space:]]*}"} lines are the cheap trim idiom — strip leading and trailing whitespace using pattern removal.


Exercise 5 — CSV Line → Pretty Table Row

Goal: Given a comma-separated string of fields, print them as a fixed-width table row. Trim whitespace around each field, title-case the first field, and pad all fields to 12 chars.

Input: " alice , developer , eu-west-1" Expected:

| Alice        | developer    | eu-west-1    |
💡 Show Answer
pretty_row() {
    local IFS=, fields
    read -ra fields <<< "$1"

    local out="|"
    for i in "${!fields[@]}"; do
        local f=${fields[i]}
        # Trim
        f=${f#"${f%%[![:space:]]*}"}
        f=${f%"${f##*[![:space:]]}"}
        # Title-case first field
        ((i == 0)) && f=${f^}
        printf -v cell ' %-12s |' "$f"
        out+=$cell
    done
    echo "$out"
}

pretty_row "  alice ,  developer , eu-west-1"
# | Alice        | developer    | eu-west-1    |

printf -v cell stores the formatted output into a variable instead of printing — much faster than cell=$(printf …) in a loop.


What’s Next?

You can manipulate any string natively. The next chapter graduates to the heavy lifters of UNIX text processing — sed and awk in scripts: when to reach for them, the patterns that come up in 90% of scripts, and Bash’s own regex =~ for the cases where you can keep it native.

Continue → sed & awk in Scripts

Updated: