💾 Archived View for going-flying.com › ~mernisse › 32.gmi captured on 2024-05-10 at 10:42:48. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-03-20)
-=-=-=-=-=-=-
I stumbled upon some musing[1] about different form simple shell scripts can take and had some thoughts. Now my first rule of programming is to follow the style of the team you're working with. If you think something should be different, you must convince the team instead of doing your own thing in the corner. It's more important that the team be able to survive without you than it is to be "right". If you are working alone then as a team of 1, you just need to convince yourself. Once you have satisfied the above, the next rule is to keep it as simple and straightforward as possible. To paraphrase something I heard many years ago, you have to be smarter than the author to debug a thing so try not to be your cleverest when you are writing something else you may not be qualified to maintain it. For some final context before I begin my more specific thoughts I'm used to working in mixed environments so my goal with shell scripts specifically is maximal portability. I prefer POSIX shell to the point that I'd rather inline Python or Perl than resort to bash-isms. My targets are typically MacOS/bash, Linux/bash, Linux/dash, and BSD/pdksh.
These were the examples given in the original article.
ping -f "$@"
#!/bin/sh ping -f "$@"
#!/bin/sh /sbin/ping -f "$@"
#!/bin/sh exec /sbin/ping -f "$@"
Now in these specific examples you would need super-user privileges to run any of these commands (I tested on MacOS, Debian Linux and OpenBSD) but I'm going to assume the original author didn't intend to limit the scope of their comments to scripts requiring elevated privileges to run.
The first example (pa) is not even a shell script.
Of the remaining, I'd prefer the second form (re) the most. It is simple, lets the shell do most of the work and doesn't impose unnecessary portability problems. The author argues that hard-coding absolute path names provides some additional security but for maintainability's sake I'd rather choose to control $PATH in the script if it was critical and still allow the shell to resolve locations itself. It is certainly easier to read and maintain than having /full/path/to/file everywhere. More critically I think the security argument is a bit of a fallacy. Defense is depth is a good strategy but there's no point in digging a moat inside the vault. If an attacker has gained control of your system sufficiently to modify the environment of your running shell (to change your $PATH) and inject an executable binary on the filesystem then the game was already over. Now there may be some cases where you absolutely want a specifically installed version of a program to be run, and then you may find yourself hard-coding a full path to it (perhaps it is located in a directory not in your shell's $PATH and you don't want to accidentally execute other binaries later in the script from that directory) but for system utilities letting the shell do the lookup for you is not unreasonable. As to the complaint of extra work, Most shells cache the lookups so the penalty is very low and the portability gains are very high. You certainly don't want to find yourself doing this.
case $(uname -s) in Darwin) /usr/bin/foo ;; OpenBSD) /bin/foo ;; *) /usr/local/bin/foo ;; esac
Towards the final form (vo), and generally as far as using exec goes, the author elucidates a frustration with an untidy pstree(1) output and frankly I find that argument unpersuasive. More important than an aesthetic situation that might be encountered rarely (are you often looking at the process table in another window while running a script?) is being able to figure out where a hung program came from. I've had many situations where a remote filesystem hangs and a bunch of programs back up blocked waiting on IO and if they all were just exec'd binaries over shell scripts I'd never be able to say track down the cron(8) job that fired them off. Exec also obviates the ability for any sort of cleanup or trap handler functions from running leading to the question, why is this a shell script? If you truly are making a script that is literally as shown in vo above, you should probably just use an alias or a function in your .profile instead. You save yourself the performance and security concerns of executing a new copy of the shell entirely there.
Hopefully my thoughts make it clear that I value simplicity, portability and maintainability in most things, and doubly so in shell scripts. I have scripts that have roots over 20 years ago and they generally get polished down to be as short, and simple as possible. If you don't want to find yourself mad at it at some point in the future, leave as few traps for yourself as you can.
For further reading one might find an annotated view of my .profile[2] interesting.
🚀 © MMXX-MMXXIII matt@going-flying.com