bhylu.com

Advertisements

Bash Report Project

Validate a small input format before publishing its summary.

How it works

The project reports sheet counts for a fictional map desk. Its input contract is exactly two comma-separated fields: a lowercase area name and one to three decimal digits. It rejects malformed rows before producing a result and refuses to replace an existing destination. This is a deliberately small plain-text format, not a general CSV implementation or a concurrency-safe publishing service.

Run the code

Reviewed with Git for Windows Bash 5.2.37. Open Git Bash, save the download with LF line endings in a separate learning folder, check it with bash -n lesson-name.sh, then run bash lesson-name.sh using the actual filename. Bash 4 or newer is required for the full course. Linux-only notes identify unavailable tools or POSIX behaviour. There is no Bash browser runner; Apache and MySQL are not required.

This complete example was executed in the stated local review environment.

The complete source creates a fresh private temporary workspace and checks its physical path before cleanup. All file fixtures belong to that workspace; do not remove the guards or point it at existing documents.

Download complete source

Example

report-project.sh

#!/usr/bin/env bash
set -u
set -o pipefail
export LC_ALL=C

# Own a new directory; verify physical paths before cleanup.
parent=$(cd -- "${TMPDIR:-/tmp}" && pwd -P) || exit 1
work=$(mktemp -d "$parent/bhylu-bash.XXXXXX") || exit 1
work=$(cd -- "$work" && pwd -P) || exit 1
case "$work" in "$parent"/bhylu-bash.??????) ;; *) exit 1 ;; esac
cleanup() {
    cd -- "$parent" || return 1
    [[ -d $work && ! -L $work ]] || return 1
    [[ $(cd -- "$work" && pwd -P) == "$work" ]] || return 1
    case "$work" in "$parent"/bhylu-bash.??????) rm -r -- "$work" ;; *) return 1 ;; esac
}
trap cleanup EXIT
cd -- "$work" || exit 1

build_report() {
    [[ $# -eq 2 ]] || return 2
    local input=$1 output=$2
    [[ -f $input && ! -e $output ]] || return 2
    awk -F, '{ sub(/\r$/, "") } NF != 2 || $1 !~ /^[a-z]+$/ || $2 !~ /^[0-9][0-9]?[0-9]?$/ { bad=1 } END { if(!NR)bad=1; exit bad }' < "$input" || return 2
    awk -F, '{ sub(/\r$/, ""); total += $2; rows++ } END { if (!rows) exit 2; printf "jobs=%d;sheets=%d\n",rows,total }' < "$input" > "$output" || return 2
}
printf "north,8\nsouth,5\nnorth,3\n" > jobs.txt
build_report jobs.txt report.txt || exit 1
[[ $(cat report.txt) == "jobs=3;sheets=16" ]] || exit 1
printf "north,not-a-count\n" > bad.txt
if build_report bad.txt rejected.txt; then exit 1; fi
[[ ! -e rejected.txt ]] || exit 1
if build_report jobs.txt report.txt; then exit 1; fi
printf "Report total 16; malformed input and replacement rejected\n"

Focus on the lesson commands

This excerpt omits temporary-workspace setup and cleanup. Use the complete source above to run the example.

Command excerpt

build_report() {
    [[ $# -eq 2 ]] || return 2
    local input=$1 output=$2
    [[ -f $input && ! -e $output ]] || return 2
    awk -F, '{ sub(/\r$/, "") } NF != 2 || $1 !~ /^[a-z]+$/ || $2 !~ /^[0-9][0-9]?[0-9]?$/ { bad=1 } END { if(!NR)bad=1; exit bad }' < "$input" || return 2
    awk -F, '{ sub(/\r$/, ""); total += $2; rows++ } END { if (!rows) exit 2; printf "jobs=%d;sheets=%d\n",rows,total }' < "$input" > "$output" || return 2
}
printf "north,8\nsouth,5\nnorth,3\n" > jobs.txt
build_report jobs.txt report.txt || exit 1
[[ $(cat report.txt) == "jobs=3;sheets=16" ]] || exit 1
printf "north,not-a-count\n" > bad.txt
if build_report bad.txt rejected.txt; then exit 1; fi
[[ ! -e rejected.txt ]] || exit 1
if build_report jobs.txt report.txt; then exit 1; fi
printf "Report total 16; malformed input and replacement rejected\n"

Expected result

Report total 16; malformed input and replacement rejected

The first awk pass validates the entire input before the second computes a report. The sample checks a correct total, refusal of malformed data and refusal to replace an existing report. A real concurrent workflow needs an atomic creation strategy, and write failure can leave a partial output; those limits are not hidden by the existence checks. Both passes explicitly normalise a trailing carriage return, so LF and CRLF record endings follow the same stated policy across environments.

Primary technical reference: Bash documentation.

Advertisements