LESSON 3 OF 4
Build, trace and inspect the complete inventory
Prepare an isolated fixture
Create a new directory and use sibling input and output directories. Do not run the setup inside a directory containing real files with the same names.
mkdir -p -- practice/input practice/output
printf 'abc' > practice/input/alpha.txt
printf 'hello\n' > 'practice/input/two words.txt'
printf 'outside scope\n' > practice/input/notes.log
printf 'hidden\n' > practice/input/.hidden.txt
These setup commands intentionally create or overwrite the named fixture files. They are safe only in your new practice directory; they are not a source-preservation promise. The inventory itself has a different, read-only source contract. Separating setup from collection keeps that distinction clear.
Save the supplied script as inventory.sh. Run it with Bash rather than assuming the default shell is compatible.
bash inventory.sh practice/input practice/output/report.tsv
Expect the message Published 2 file rows; scope: visible top-level *.txt only. The report contains a header and two rows. Their byte counts are 3 and 6. The display name for the second row is two\ words.txt. It is escaped for display; never feed the report to eval to reconstruct filenames. Reversible transport of arbitrary pathnames belongs in a designed format and trusted parser, not an invitation to execute a field.
Complete program
#!/usr/bin/env bash
# Read visible top-level *.txt files; publish one new TSV without overwriting.
set -u
export LC_ALL=C
fail() { printf 'Inventory failed: %s\n' "$1" >&2; exit 2; }
[[ $# == 2 ]] || fail 'usage: bash inventory.sh INPUT_DIRECTORY NEW_REPORT.tsv'
for tool in wc sha256sum mktemp ln rm dirname basename; do
command -v "$tool" >/dev/null || fail "required command unavailable: $tool"
done
input=$(cd -- "$1" && pwd -P) || fail 'input directory unavailable'
parent=$(cd -- "$(dirname -- "$2")" && pwd -P) || fail 'output parent unavailable'
output="$parent/$(basename -- "$2")"
[[ $parent != "$input" && $parent != "$input/"* ]] || fail 'output must be outside input tree'
[[ ! -e $output && ! -L $output ]] || fail 'output already exists'
temp=$(mktemp -- "$parent/.ck-inventory.XXXXXXXX") || fail 'cannot create temporary file'
cleanup() { [[ -z ${temp:-} ]] || rm -f -- "$temp"; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
exec 3>"$temp" || fail 'cannot open temporary report'
printf 'filename_bash_quoted\tbytes\tsha256\n' >&3 || fail 'cannot write header'
shopt -s nullglob
files=("$input"/*.txt)
count=0
for path in "${files[@]}"; do
[[ ! -L $path && -f $path && -r $path ]] || fail 'matched path is not a readable non-symlink regular file'
size=$(wc -c < "$path") || fail 'byte count failed'
size=${size//[[:space:]]/}
[[ $size =~ ^[0-9]+$ ]] || fail 'unexpected byte count'
digest=$(sha256sum < "$path") || fail 'hash read failed'
hash=${digest%% *}
[[ $hash =~ ^[0-9a-f]{64}$ ]] || fail 'unexpected hash'
name=${path##*/}
printf '%q\t%s\t%s\n' "$name" "$size" "$hash" >&3 || fail 'cannot write row'
count=$((count + 1))
done
exec 3>&- || fail 'cannot close report'
# GNU ln -T refuses an existing target, including directories and symlinks.
ln -T -- "$temp" "$output" || fail 'exclusive report publication failed'
printf 'Published %s file rows; scope: visible top-level *.txt only\n' "$count"
Trace the boundaries in order
At startup, verify the argument count and required tools. Resolve the input directory and output parent physically so a symlinked parent does not disguise an output inside the source tree. Preserve the final destination name: the link operation must refuse an already existing target rather than follow it.
After creating the temporary file, install cleanup handlers. An ordinary error or exit removes only that known path; interrupt and termination handlers return explicit statuses. A forced kill or machine crash cannot execute cleanup, so a temporary file can remain. It is a leftover to inspect, not an excuse for recursively deleting broad directories.
Build the array once using the declared glob. Walk its elements, reject unacceptable matches, read bytes and hash, then write one row. Finally close the descriptor and publish. Use bash -n inventory.sh to check syntax before running. Syntax success does not exercise reads, permission decisions, empty matches or final publication; it is one layer of evidence.
Fault lab with visible evidence
Run the same output path twice. The second run must fail and leave the report bytes unchanged. Create a dangling symlink with a .txt name inside a separate input fixture: no report should be published. Create a directory ending in .txt: it should also fail. Try an output under input and confirm that no temporary report is left there.
Next create -n.txt, a tab-containing name and a newline-containing name. Use the supplied tests if your terminal makes those names difficult to create. The TSV should still have exactly one physical row per selected file and exactly three columns. %q makes control characters visible without putting an actual tab or newline into the filename field. This is a display contract specific to Bash, not generic CSV quoting.
List the chosen directory before and after collection and compare input bytes. Record the runtime, invocation, source scope, report, status and diagnostics. If a test fails, keep its actual result and isolate one hypothesis. Re-running until a green message appears without explaining the repair loses the debugging evidence the project is meant to produce.
Worked laboratory solution
Compare your reasoning with the worked solution
Command substitution and unquoted expansion split daily report.txt into separate words. The literal wildcard in notes*.txt can be expanded again when unquoted. A leading hyphen can be interpreted as an option by utilities unless protected using supported — syntax or an appropriate path prefix. Parsing ls is not a reliable filename interface. In a flat directory, a carefully quoted glob loop with explicit no-match handling can suffice; recursive traversal may need null-delimited find/read processing. Check each copy’s status and preserve a clear summary rather than printing unconditional success.
YOUR NEXT STEP
Keep building your skills
No directly related CogniSkill book is mapped to this course yet. Continue with the free learning, or compare the publishing catalogue by your next goal.
Explore books by topic and goal →Sign in to save lesson completion. Reading position is separate from completion.
Practice notes — your working space
Save predictions, outputs and explanations as you work. Notes stay on this browser, not your account; people sharing this browser can access them. Avoid passwords and sensitive data. Export to keep a portable copy, or clear your notes before leaving a shared device.
How was this lesson?
Help us improve this lesson.