Skip to content

Instantly share code, notes, and snippets.

@fillest
Last active October 18, 2023 19:11
Show Gist options
  • Save fillest/8d64f8fa0cdb1745bfc9c683cf39d453 to your computer and use it in GitHub Desktop.
Save fillest/8d64f8fa0cdb1745bfc9c683cf39d453 to your computer and use it in GitHub Desktop.
No more unmaintainable error-prone Bash scripts - use this instead
# Bash really should be avoided as much as possible (within reasonable limits, of course) even for one-liners which *seem* trivial.
# Bash is very error-prone by design. It's hard to comprehend all the pitfalls (e.g. https://mywiki.wooledge.org/BashFAQ/105)
# and it's a regrettable time-waste anyway.
#
# Modern Python is good for scripting the logic - keep Bash only for launching executables and most primitive
# pipes and redirections (avoid subshells, substitutions and so on). No need to install anything -
# just start your script with the following small self-contained helper function (check the examples for usage). Its features:
# * terminates on non-zero exit status by default
# * returns the output (combined - which usually should not be a problem - use e.g. '2>/dev/null' when it is)
# * prints commands and combined output
# * prepends 'set -o pipefail'
#
import subprocess, sys
def run (cmd: str, *args, check=True, pipefail=True, **kwargs):
print('$', cmd, flush=True)
if pipefail:
cmd = 'set -o pipefail && ' + cmd
#we want to be able to stream the output (use `echo test1; sleep 1; echo test2` to test)
with subprocess.Popen(cmd, *args, shell=True, executable='/bin/bash', stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
bufsize=1, universal_newlines=True, #'text' was added in 3.7
**kwargs) as process:
output = []
for line in process.stdout: #should not deadlock because stderr=subprocess.STDOUT
print(line, end="")
output.append(line)
exit_status = process.wait()
output = "".join(output).strip()
if check and exit_status:
# sys.exit(f"\n-------\n[ERROR] command failed: {cmd}")
raise Exception(f"Command failed (see above): {cmd}")
elif check:
return output
return exit_status, output
################## Usage examples:
var = "world"
run(f'echo "Hello, {var.capitalize()}!"')
try:
run('cat non-existing-file')
except SystemExit:
"expected"
else:
raise Exception("will not happen")
result = run('echo $0')
assert result == '/bin/bash'
exit_status, result = run('whatever', check=False)
#^ alternatively a name like "err"/"error" may be more readable, e.g. "if err".
#When you don't care about any output, use it like this: "if run(...)[0]"
exit_status, _ = run('{ echo "auto-pipefail test"; false; } | grep test', check=False)
assert exit_status != 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment