Linux Basic β€” sed & awk Practice Exercises

4 minute read

sed & awk in Scripts β€” Practice Exercises

Five exercises spanning capture, sed, awk, and Bash regex. Each one mirrors a real scripting task.


Exercise 1 β€” Safe Capture With Exit Code

Goal: Write a run function that takes a command, runs it, and captures both stdout+stderr and the exit code. If the command fails, print FAIL <name> (exit N) and the captured output on stderr, then return the same exit code. If it succeeds, print OK <name> and any output indented under it. Demonstrate with ls /tmp, ls /nope, and false.

πŸ’‘ Show Answer
run() {
    local name=$1
    shift
    local output rc
    output=$("$@" 2>&1)
    rc=$?
    if (( rc != 0 )); then
        printf '[FAIL] %s (exit %d)\n' "$name" "$rc" >&2
        [[ -n "$output" ]] && printf '  %s\n' "$output" >&2
        return "$rc"
    fi
    printf '[OK]   %s\n' "$name"
    [[ -n "$output" ]] && printf '  %s\n' "$output"
}

run "list tmp"  ls /tmp >/dev/null
run "list nope" ls /nope-does-not-exist
run "false"     false
echo "---"
echo "exit of last: $?"

The output=$(…) then rc=$? order is critical β€” anything between them overwrites $?.


Exercise 2 β€” Cross-Platform sed -i Wrapper

Goal: Write a sedi function that performs sed -i correctly on both GNU sed (Linux) and BSD sed (macOS). It should accept the same arguments as sed, minus the -i flag itself. Demonstrate by replacing every occurrence of old with new in a test file.

πŸ’‘ Show Answer
sedi() {
    case "$OSTYPE" in
        darwin*) sed -i '' "$@" ;;
        *)       sed -i      "$@" ;;
    esac
}

# Test
cat > /tmp/test.txt <<'EOF'
this is old text
old again
nothing here
EOF

sedi 's/old/new/g' /tmp/test.txt
cat /tmp/test.txt
# this is new text
# new again
# nothing here

Robust alternative β€” backup + replace + cleanup, no -i at all:

sedi_portable() {
    local file=${@: -1}                 # last arg
    local args=( "${@:1:$#-1}" )        # all but last
    cp -p "$file" "$file.tmp.$$"
    sed "${args[@]}" "$file.tmp.$$" > "$file"
    rm "$file.tmp.$$"
}

Exercise 3 β€” Validate Semver With =~

Goal: Write a function parse_semver that takes a string and:

  • returns 0 and prints major minor patch [prerelease] if it’s valid semver (v1.2.3 or 1.2.3-rc1, with optional leading v)
  • returns 1 silently otherwise

Test with: v1.2.3, 2.0.0, 1.2.3-beta.4, 1.2, v1.2.x.

πŸ’‘ Show Answer
parse_semver() {
    local s=$1
    local re='^v?([0-9]+)\.([0-9]+)\.([0-9]+)(-[A-Za-z0-9.]+)?$'
    if [[ "$s" =~ $re ]]; then
        local major=${BASH_REMATCH[1]}
        local minor=${BASH_REMATCH[2]}
        local patch=${BASH_REMATCH[3]}
        local pre=${BASH_REMATCH[4]#-}      # strip leading dash if present
        printf '%s %s %s' "$major" "$minor" "$patch"
        [[ -n "$pre" ]] && printf ' %s' "$pre"
        echo
        return 0
    fi
    return 1
}

for v in v1.2.3 2.0.0 1.2.3-beta.4 1.2 v1.2.x; do
    if out=$(parse_semver "$v"); then
        printf '%-15s β†’ %s\n' "$v" "$out"
    else
        printf '%-15s β†’ INVALID\n' "$v"
    fi
done
# v1.2.3          β†’ 1 2 3
# 2.0.0           β†’ 2 0 0
# 1.2.3-beta.4    β†’ 1 2 3 beta.4
# 1.2             β†’ INVALID
# v1.2.x          β†’ INVALID

Note the unquoted $re β€” quoting it would make it match literally.


Exercise 4 β€” Top 5 Source IPs From a Mini Access Log

Goal: Given the access-log-shaped data below, print the top 5 source IPs by request count, format <count> <ip>, descending.

Setup:

cat > /tmp/access.log <<'EOF'
10.0.0.1 - - [17/Jun/2026:09:00:01] "GET / HTTP/1.1" 200 1024
10.0.0.2 - - [17/Jun/2026:09:00:02] "GET /a HTTP/1.1" 200 512
10.0.0.1 - - [17/Jun/2026:09:00:03] "GET /b HTTP/1.1" 200 800
10.0.0.3 - - [17/Jun/2026:09:00:04] "GET / HTTP/1.1" 404 256
10.0.0.1 - - [17/Jun/2026:09:00:05] "POST /api HTTP/1.1" 200 600
10.0.0.2 - - [17/Jun/2026:09:00:06] "GET /a HTTP/1.1" 200 512
10.0.0.4 - - [17/Jun/2026:09:00:07] "GET / HTTP/1.1" 200 1024
10.0.0.1 - - [17/Jun/2026:09:00:08] "GET /c HTTP/1.1" 200 400
10.0.0.5 - - [17/Jun/2026:09:00:09] "GET / HTTP/1.1" 500 0
10.0.0.2 - - [17/Jun/2026:09:00:10] "GET /a HTTP/1.1" 200 512
EOF
πŸ’‘ Show Answer
awk '{ c[$1]++ } END { for (ip in c) print c[ip], ip }' /tmp/access.log \
    | sort -rn \
    | head -5
# 4 10.0.0.1
# 3 10.0.0.2
# 1 10.0.0.5
# 1 10.0.0.4
# 1 10.0.0.3

Three commands; no shell loop. Awk does the counting in one pass through the file.

Bonus β€” total bytes per IP too:

awk '{ c[$1]++; b[$1]+=$NF } END { for (ip in c) printf "%d %12d %s\n", c[ip], b[ip], ip }' \
    /tmp/access.log | sort -rn | head -5

Exercise 5 β€” Update or Insert a Config Setting

Goal: Write a function set_config FILE KEY VALUE that:

  • If KEY=… already exists in FILE, replaces the line
  • If not, appends KEY=VALUE at the end
  • Preserves a backup at FILE.bak
  • Works on both Linux and macOS

Demonstrate by setting timeout=60 (update an existing key) and retries=3 (insert new).

πŸ’‘ Show Answer
set_config() {
    local file=$1 key=$2 val=$3

    if grep -q "^${key}=" "$file"; then
        # Existing: update
        case "$OSTYPE" in
            darwin*) sed -i.bak "s|^${key}=.*|${key}=${val}|" "$file" ;;
            *)       sed -i.bak "s|^${key}=.*|${key}=${val}|" "$file" ;;
        esac
    else
        # Missing: backup + append
        cp -p "$file" "$file.bak"
        printf '%s=%s\n' "$key" "$val" >> "$file"
    fi
}

cat > /tmp/app.conf <<'EOF'
host=example.com
port=8080
timeout=30
EOF

set_config /tmp/app.conf timeout 60
set_config /tmp/app.conf retries 3

cat /tmp/app.conf
# host=example.com
# port=8080
# timeout=60
# retries=3

A pure-awk version that does it all in one atomic pass:

set_config_awk() {
    local file=$1 key=$2 val=$3
    awk -v k="$key" -v v="$val" '
        BEGIN { FS=OFS="=" }
        $1 == k { print k OFS v; found=1; next }
        { print }
        END { if (!found) print k OFS v }
    ' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
}

The awk version is atomic (no half-written file if interrupted) and platform-independent.


What’s Next?

You now have the full Bash text toolkit: parameter expansion for strings in memory, regex for in-process matching, sed/awk for streams and files. The next chapter pivots to data structures β€” how to slice, sort, copy, and otherwise manipulate arrays.

Continue β†’ Manipulating Arrays

Updated: