Last active
October 18, 2023 19:11
-
-
Save fillest/8d64f8fa0cdb1745bfc9c683cf39d453 to your computer and use it in GitHub Desktop.
No more unmaintainable error-prone Bash scripts - use this instead
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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