Rewrite a fragile shell script in Python

applied · 45 min · Objective 4.3

Task

Take a shell script that parses structured data and calls external commands, and rewrite it in Python at the point where shell stops being the right tool -- around JSON, error handling and anything past a hundred lines. Do it safely, with a venv, argparse and subprocess used correctly.

Steps

  1. Start from a shell script that queries something returning JSON -- e.g. podman inspect or a REST endpoint -- and parses it with grep and cut. Note where it is fragile: a field containing a space, a missing key, a value with a slash.
  2. Rewrite it in Python: parse the JSON with the json module rather than text tools, and handle the missing-key case with .get() and a default.
  3. Call external commands with subprocess.run passing a LIST, not a string, and never shell=True. Demonstrate why: show that a value containing ; rm -rf x is harmless in the list form and dangerous with shell=True (test the dangerous form only with a harmless payload like ; echo pwned).
  4. Add argument parsing with argparse so the script gets --help, type checking and required arguments for free.
  5. Check exit codes: inspect subprocess.run(...).returncode and fail the script clearly when a called command fails.
  6. Add logging with the logging module rather than print, so output can go to stderr with timestamps.
  7. Record dependencies with pip freeze and confirm the script runs from a freshly rebuilt venv.

Verify

source ~/.venvs/rewrite/bin/activate
python tool.py --help | grep -qi usage && echo "argparse wired"
python tool.py --target demo | head
# injection: the same hostile value, both ways
python - <<'PY'
import subprocess
payload = "; echo pwned"
as_list = subprocess.run(["echo", payload], capture_output=True, text=True).stdout
as_shell = subprocess.run("echo " + payload, shell=True, capture_output=True, text=True).stdout
print("list form printed:", repr(as_list.strip()))            # '; echo pwned' -- data
print("shell=True ran it:", "pwned" in as_shell.splitlines())  # True -- executed
PY

The injection test is the point, and it only teaches if you see both halves. Passing a list hands the arguments straight to exec with no shell to interpret the semicolon, so the hostile value comes back as the text it is. The same value with shell=True goes to /bin/sh -c, which reads the semicolon as "and then run this", and pwned appears on a line of its own. This is the quoting lesson from bash, with sharper teeth.

An earlier version of this check ran only the safe list form, so it could only ever print "safe" -- it never showed the dangerous case, and it never touched your tool. A check whose outcome is fixed in advance demonstrates nothing.

Notes

The honest trigger for the rewrite is a length: past roughly a hundred lines, or the moment a shell script starts parsing JSON, Python earns its place. Below that, shell is fine and the rewrite is overhead.

This is an independent study companion for CompTIA Linux+ and is not produced by or endorsed by CompTIA.