Bash vs Python for Small Linux Automation Tasks
Implement the same file-byte report in Bash and Python, then compare argument handling, empty inputs, failure behavior, and the dependencies each version needs.

For a small Linux automation task, choose Bash or Python after defining the input and failure behavior. “Count bytes in text files” sounds trivial until a directory is missing, a filename contains a newline, or an empty directory must produce a nonzero result.
The two implementations below use the same contract. Bash stays close to a command-line loop; Python keeps path handling and error reporting in one program. Neither choice makes the contract unnecessary.
Agree on the task before comparing languages
The report reads regular files whose names match *.txt in one directory, without recursion. It reports each filename and byte count. A missing directory, no matching regular files, or a failed read must return nonzero. Names are displayed in an escaped form so a newline does not silently become a second report row.
The report is intended for a person. It is not a machine-readable interchange format, and a failure after earlier files were read can leave partial output. Both versions follow regular-file symlinks; neither is a sandbox for untrusted directories or a snapshot protected against concurrent changes.
Create an isolated fixture with Python 3:
practice_dir=$(mktemp -d)
cd "$practice_dir"
python3 - <<'PY'
from pathlib import Path
root = Path('inputs')
root.mkdir()
for name, content in {
'plain.txt': b'a\n',
'release notes.txt': b'alpha\n',
'report*.txt': b'beta\n',
'-draft.txt': b'gamma\n',
'two\nlines.txt': b'z\n',
}.items():
(root / name).write_bytes(content)
Path('empty').mkdir()
PY
The expected byte counts are 2, 6, 5, 6, and 2 for those named files. These examples were checked with Bash 3.2.57 and Python 3.14.7 locally, then with Bash and Python in Linux containers. The code uses established Python 3 path APIs rather than a version-specific new feature.
Bash keeps the job in a short command loop
Save report.sh:
#!/usr/bin/env bash
set -eu
if [ "$#" -ne 1 ] || [ ! -d "$1" ]; then
printf 'Usage: bash report.sh EXISTING_DIRECTORY\n' >&2
exit 2
fi
count=0
for file in "$1"/*.txt; do
[ -f "$file" ] || continue
if ! bytes=$(wc -c < "$file"); then
printf 'Cannot read <%s>\n' "$file" >&2
exit 1
fi
printf '%q\t%s\n' "$file" "$bytes"
count=$((count + 1))
done
if [ "$count" -eq 0 ]; then
printf 'No matching regular files\n' >&2
exit 1
fi
Run bash report.sh inputs. The quoted directory forms the fixed part of the pattern, while *.txt remains a glob. Each resulting path is quoted again when opened. The input redirection keeps a leading dash in the filename away from wc’s option parser.
Bash’s %q prints a shell-escaped representation. It makes unusual characters visible, but it is Bash-specific and does not define a universal report format. wc may pad the count with spaces; compare numeric values rather than byte-identical rows.
The file test handles the default unmatched-glob case by skipping the literal pattern. The counter then enforces the agreed empty-input failure. Read the filename-expansion rules when changing shell options such as nullglob or failglob; those options change the loop’s behavior.
Python keeps the paths out of shell expansion
Save report.py:
from pathlib import Path
import sys
if len(sys.argv) != 2 or not Path(sys.argv[1]).is_dir():
print('Usage: python3 report.py EXISTING_DIRECTORY', file=sys.stderr)
raise SystemExit(2)
root = Path(sys.argv[1])
count = 0
try:
for path in sorted(root.glob('*.txt')):
if not path.is_file():
continue
size = 0
with path.open('rb') as stream:
for block in iter(lambda: stream.read(65536), b''):
size += len(block)
print(f'{str(path)!r}\t{size}')
count += 1
except OSError as error:
print(f'Cannot complete report: {error}', file=sys.stderr)
raise SystemExit(1)
if count == 0:
print('No matching regular files', file=sys.stderr)
raise SystemExit(1)
Run python3 report.py inputs. Pathlib finds paths and opens files without asking a shell to split a constructed command. The code reads in blocks rather than loading a whole file merely to count it. The !r representation exposes newline characters; its quoting is different from Bash %q.
Python still needs validation. A path object does not make a missing directory valid or guarantee that a file remains unchanged between discovery and reading. Pathlib globbing can suppress some scanning errors, so this small report does not promise a complete inventory of an unreadable tree. If completeness matters, choose a traversal API with explicit scan-error handling and define that stronger contract first.
Test the failures as well as the normal directory
bash report.sh inputs
python3 report.py inputs
bash report.sh empty
python3 report.py empty
bash report.sh missing
python3 report.py missing
Run the commands individually in an interactive shell and inspect $? immediately after each one. Both normal runs should return 0, both empty runs 1, and both missing-directory runs 2. Check each named file’s count; row order and escaping are not required to be identical.
The filename argument walkthrough explains why a fix that works for plain.txt can still process the wrong files. Neither language gets credit merely because its simplest input passes.
Choose by the next change you expect
| Next requirement | What it changes |
|---|---|
| Chain a few installed command-line tools | Bash keeps their invocation visible |
| Parse JSON or produce a structured report | Python can represent fields and validation directly |
| Preserve exact argument boundaries when launching a tool | Bash needs quoted expansions and arrays; Python needs an argument list |
| Run where Python is not installed | The Python version adds a runtime prerequisite |
Run as portable /bin/sh |
This Bash script needs changes; %q is not portable shell syntax |
| Guarantee a complete inventory or atomic report | Both examples need a stronger design |
When Python calls an external tool, prefer an argument list such as subprocess.run(['tool', str(path)], check=True) to a constructed shell string. The subprocess documentation explains execution and failure handling. Avoiding a shell does not remove the called tool’s own option syntax.
Choose Bash for a small, readable command pipeline whose edge cases you can still inspect. Choose Python when structured data, branching, reusable functions, or error policy make the shell version harder to review. Carry the same unusual-name and failure fixtures across the change; translating the syntax without preserving the contract is not a successful migration.
References
- GNU Bash filename expansion — pattern and empty-match behavior.
- GNU Bash printf — Bash-specific escaped output with
%q. - Python pathlib — path iteration, file access, and glob limitations.
- Python subprocess — argument lists and subprocess failure handling.