Thuta Learning
ExercisesDevOps & Toolsbeginner

Exercise: Debug a Deployment Script

What you'll walk away with

  • Explain the core ideas behind Exercise: Debug a Deployment Script
  • Run the sample code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

This exercise digs into a specific, extremely common class of bash bug: silent word-splitting caused by storing a list in a plain string instead of an array, then iterating over that string with an unquoted expansion. The bug class matters more than the specific symptom here — write for f in $FILES anywhere in a script and bash will always split that value on IFS (default: space, tab, newline), no matter what's inside it. What makes this bug class especially dangerous is that it produces no error message at all in the failing case; cp simply gets called with arguments that don't correspond to any real file, fails on those specific calls, and the loop moves on to the next word without anyone noticing that "release" and "notes.txt" were never actually one file. Compounding that, the script's own success message — a file count pulled from ls | wc -l — reports on whatever ended up sitting in the release directory rather than on what was actually supposed to be there, so a partial deploy still prints "complete." The mental habit that catches this before it ships is treating every bare $variable expansion as a compile error waiting to happen: ask "could this value ever contain a space, glob character, or be empty?" before you write it unquoted, and treat any list of more than one thing as an array from the first line, not a convenience string that happens to work while your test data is boring. Running scripts through shellcheck, which flags unquoted expansions by default, is the automatable version of that same habit.

Connect it to a real scenario

deploy.sh keeps its list of files to copy in a single string, FILES="app.conf release notes.txt server.py", and then loops with for f in $FILES — no quotes around $FILES. Walk through what that line actually does: bash expands $FILES to the raw text app.conf release notes.txt server.py, and because the expansion is unquoted, splits that text on whitespace before handing it to the for loop, so the loop doesn't see three files, it sees four words: app.conf, release, notes.txt, and server.py. The first and last of those happen to be real filenames, so cp "$SOURCE_DIR/app.conf" and cp "$SOURCE_DIR/server.py" succeed normally. The middle two don't: cp tries to stat ./build/release and then ./build/notes.txt, both of which fail with "No such file or directory," while the actual file, release notes.txt, is never touched by any cp call at all — it's simply absent from every command bash generates. Because the script never checks cp's exit status, those two failures print to stderr and the loop just continues to the next word as if nothing happened. Then look at the last line: it counts whatever is already sitting in $RELEASE_DIR with ls | wc -l, which reports 2 (the two files that actually got copied) — not 3, not an error, just a number that looks plausible enough that nobody skimming the log would think to question it. Reproducing this means running the script as-is, catching the two "cannot stat" lines buried in otherwise normal-looking output, and tracing them back to the single unquoted $FILES expansion in the for line.

Try the working example

bash
#!/usr/bin/env bash
# deploy.sh - copies build artifacts into the release directory and restarts the app

SOURCE_DIR="./build"
RELEASE_DIR="./releases/current"
FILES="app.conf release notes.txt server.py"

mkdir -p "$SOURCE_DIR" "$RELEASE_DIR"

# Set up fake build artifacts so this demo is self-contained
touch "$SOURCE_DIR/app.conf"
touch "$SOURCE_DIR/release notes.txt"
touch "$SOURCE_DIR/server.py"

echo "Deploying files to $RELEASE_DIR..."

for f in $FILES; do
  cp "$SOURCE_DIR/$f" "$RELEASE_DIR/"
  echo "  copied: $f"
done

echo "Restarting app service..."
echo "Deployment complete. $(ls "$RELEASE_DIR" | wc -l) file(s) now in the release directory."
You should see
Deploying files to ./releases/current...
  copied: app.conf
cp: cannot stat './build/release': No such file or directory
  copied: release
cp: cannot stat './build/notes.txt': No such file or directory
  copied: notes.txt
  copied: server.py
Restarting app service...
Deployment complete. 2 file(s) now in the release directory.

5-minute try-it

Run the script and compare its final "file(s) now in the release directory" count to the three files it was supposed to copy. Then look closely at the "copied:" lines from the middle two loop iterations — neither of those names matches any real file in ./build, yet the script cheerfully claims to have copied them while the actual file, "release notes.txt", never makes it across. Find the line where the space-separated file list gets expanded without protection from word splitting, and restructure how that file list is stored and iterated so a filename containing a space survives as a single item end to end.

One important caution

Storing a list of items in a plain space-separated string variable instead of a bash array — there's no way to safely iterate over it once any item can contain whitespace

Assuming a script that prints a "Deployment complete" message and exits 0 actually did what it claims — always back a success message with a real count or check, not just an optimistic echo statement

Greg's Wiki — BashPitfallsBash / Shell Scripting

Easy traps

  • Storing a list of items in a plain space-separated string variable instead of a bash array — there's no way to safely iterate over it once any item can contain whitespace
  • Assuming a script that prints a "Deployment complete" message and exits 0 actually did what it claims — always back a success message with a real count or check, not just an optimistic echo statement
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Run the script and compare its final "file(s) now in the release directory" count to the three files it was supposed to copy. Then look closely at the "copied:" lines from the middle two loop iterations — neither of those names matches any real file in ./build, yet the script cheerfully claims to have copied them while the actual file, "release notes.txt", never makes it across. Find the line where the space-separated file list gets expanded without protection from word splitting, and restructure how that file list is stored and iterated so a filename containing a space survives as a single item end to end.

You'll know it worked when: Deploying files to ./releases/current... copied: app.conf cp: cannot stat './build/release': No such file or directory copied: release cp: cannot stat './build/notes.txt': No such file or directory copied: notes.txt copied: server.py Restarting app service... Deployment complete. 2 file(s) now in the release directory.

Exercise: Debug a Deployment Script | Thuta Learning