Rewrite a fragile shell script in Python
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
- Start from a shell script that queries something returning JSON -- e.g.
podman inspector 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. - Rewrite it in Python: parse the JSON with the
jsonmodule rather than text tools, and handle the missing-key case with.get()and a default. - Call external commands with
subprocess.runpassing a LIST, not a string, and nevershell=True. Demonstrate why: show that a value containing; rm -rf xis harmless in the list form and dangerous withshell=True(test the dangerous form only with a harmless payload like; echo pwned). - Add argument parsing with
argparseso the script gets--help, type checking and required arguments for free. - Check exit codes: inspect
subprocess.run(...).returncodeand fail the script clearly when a called command fails. - Add logging with the
loggingmodule rather than print, so output can go to stderr with timestamps. - Record dependencies with
pip freezeand 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.