Which do you prefer to sling around, a bash command or a python script?
In my goal to write more python and less shell, for the benefit of my colleagues' sanity, I have been trying to favour Python - specifically, readable and maintainable python.
I have moaned before about how sysadmin scripts often look very messy and unmaintainable, and I have found a perfect example of how that arises.
I needed my team to send me the hostnames, IPs and MAC addresses of their VMs to send to IT for reserving in DHCP, and I started cooking up a python script. Just as I was finishing, I had a flash of inspiration and thought... can't I do just a one-liner in shell?
It's just a matter of getting the default IP and looking for the interface that uses it... I wrote the scirpt so that it could give result for a local host, or a series of specified remote hosts.
Which would you prefer to commit to a tooling repo? :-)
Here's the python script, idiomatic, and hopefully easy to read and maintain. I was going to tell my colleagues "Please clone (script repo) and run the script, supplying the hosts as arguments, like 'gather-ips.py jack@machine1 jack@machine2 ...' (yeah, I would need to send instructions too)
#!/usr/bin/env python
import re
import subprocess
import sys
class SubprocessError(Exception):
pass
def run(command:list[str]) -> str:
proc = subprocess.Popen(command, stdout=subprocess.PIPE, text=True)
stdout, _ = proc.communicate()
if proc.returncode > 0:
raise SubprocessError(f"Failed {command}, see stderr.")
return stdout
def read_ipa(ipa_lines):
ipa_entries = {}
current = None
for line in ipa_lines:
m = re.match(r"^[0-9]+:\s+(\S+?):.*", line)
if m:
current = m.group(1)
ipa_entries[current] = {}
continue
if current is None:
continue
current_entry = ipa_entries[current]
eth_m = re.match(r".+link/ether ([a-fA-F0-9:]+)", line)
if eth_m:
current_entry["mac"] = eth_m.group(1)
inet_m = re.match(r"\s+inet ([0-9.]+)/[0-9]+", line)
if inet_m:
current_entry["ipv4"] = inet_m.group(1)
return ipa_entries
def get_default_ip(ipr_lines):
default_line = [line for line in ipr_lines if 'default via' in line][0]
return re.findall(r"src [0-9.]+", default_line)[0][4:]
def get_ipra_lines(host):
if host:
lines = run(["ssh", host, "ip r; echo ===; ip a"]).split("\n")
idx = 0
for n in range(len(lines)):
if lines[n] == "===":
idx = n
break
ipr_lines = lines[:idx]
ipa_lines = lines[idx+1:]
else:
ipr_lines = run(["ip", "r"]).split("\n")
ipa_lines = run(["ip", "a"]).split("\n")
return ipr_lines,ipa_lines
def ublat(host_string:str):
try:
i = host_string.index("@")
return host_string[i+1:]
except ValueError:
return host_string
def get_target(host=None):
ipr_lines, ipa_lines = get_ipra_lines(host)
entries = read_ipa(ipa_lines)
def_ip = get_default_ip(ipr_lines)
hostname = ublat(host) if host else run(["hostname"]).strip()
for _, data in entries.items():
if def_ip in data.get("ipv4", ""):
print(f"{hostname}\t{data.get('ipv4')}\t{data.get('mac')}")
return
print(f"# Nothing in {entries}")
def main():
if not sys.argv[1:] or "get" in sys.argv[1:]:
get_target()
return
for host in sys.argv[1:]:
get_target(host)
main()
For sake of comparison, this is its bash equivalent - it does EXACTLY the same thing:
command="echo \"\$(hostname) \$(ip a|grep \"\$(ip r|grep 'default via'|grep -Po '(?<=src )[0-9.]+')\" -B 1|grep -Po '(?<=ether |inet )(\S+)')\"|xargs echo"
if [[ -z "$*" ]]; then
bash -c "$command"
exit
fi
for node in "$@"; do
ssh "$node" "$command"
done
In the end, I sent a message to the team and told them:
Hi all,
Please run this on each of your VMs and send me the output via email
echo "$(hostname) $(ip a|grep "$(ip r|grep 'default via'|grep -Po '(?<=src )[0-9.]+')\" -B 1|grep -Po '(?<=ether |inet )(\S+)')"|xargs echo
A one-liner in a single message. No sending scripts around. No instructing to clone and execute. Just "run this on each machine."
Which approach would you have taken
Top comments (4)
Personally I tend to stick with shell scripts, and make them POSIX if possible. Then they'll work anywhere (anywhere decent...)
If they're more than 50 lines or so, they probably need to be driven by something else, like a Makefile, or replaced with a Python (or similar) script, but that's a dodgy rule of thumb. Really it's down to what kind of stuff you need to do. Non-trivial string manipulation? API libraries? Probably Python.
In your examples, the Python script just ends up running system commands anyway for part of its job :)
The bash script however has reached the point of obscurity in terms of escaped characters and regex. I'd maybe go with
awkfor that sort of thing - I'm not an awk person so I'd have a bit of trial-and-error, but I imagine it'd be easier to maintain once it's made.I like that.
The bash script is a terse one-liner, but it is as impenetrable as a rock in a mud puddle... it's a counterpoint to the "more than 50 lines" heuristic, which I don't like because.... I could have done this thing in about 5 lines, assuming I forcibly broke at semi-colons... and it would be a most ... emetic piece of code.
The python script goes out of its way to be "readable" whereas the bash script makes no attempt to do so.
I could have added an example of a more readable bash script" which would have spanned quite a few more lines.... but then if the "obscurity" were neatly avoided, perhaps keeping to bash even with a few dozen lines would be acceptable...
Interesting. I posted this with the tags
bash, python, sysadmin, linuxA few minutes later I checked my profile posts... and its tags had been changed to
discuss, automation, python, linuxI suspect AI schenanigans trying to tell me what I meant.
WHAT THE ACTUAL.
there is rule that i like , it says , if the bash script is longer than 50 lines , rewrite in code
and because i used to do some web scrapping with sed awk rg grep and automated web app penetration testing for endpoit and information disclosure discovery and many linux utilities ill say that bash is horrible at syntaxt, like the difference between using "" and '' makes me lose my sanity especially when the script gets nested, so stick to python if your script is going to be longer than 50 lines