Bash_unit – bash unit testing framework
github.com
github.com
Bash is a lowest-common-denominator language, available on almost all platforms, with a very stable (old?) API, which makes it ideal for broad distribution & bootstrapping of systems.
When you use another other languages, like Python, suddenly you're not only worrying about whether Python is installed, but also which version is available, and you still can't use anything that's not part of the standard library (because then you enter package management/dependency hell).
I agree that even Python with stdlib-only is still better than Bash, but I guarantee that you'll find Bash >=3.0 on every host.
Turns out that bash does evolve...slowly. E.g. I found out that the default bash on MacOS 10.4 doesn't support the "=~" operator...oi...
Shouldn't you go for POSIX at that point? I'm not an expert, but I still have some idea how to test that my script is (somewhat) POSIX compliant (e.g. use shellcheck, run the script with dash or a shell in POSIX-compatible mode). I have Bash 5 on my Debian, how do I test that a script I tested with this version works on Bash 3 as well? Or have the changes between Bash 3 and Bash 5 been so minimal that I would have to do it on purpose to find an incompatibility?
I'm also curious about the number of machines where Bash scripts are run but don't have, say, Python 3.4 installed, and projects where it's a better trade-off to spend time developing and testing in Bash rather than have a Python 3 dependency. I'd say it's relatively rare.
But none of those 3 solutions really check for POSIX compliance (including -o posix, since of course you can use many non-POSIX features when it's on). I am not aware that ShellCheck actually checks POSIX compliance; I am pretty sure it just checks for common errors in your bash/sh scripts (which is what it says on the home page).
I think what you may mean is "portable shell", i.e. portable between shell X and Y and system X and Y. That is a decent goal but many people use bash just because it alone is portable enough! The system tools are often unportable though. Limiting yourself to POSIX in that case is pretty painful and also virtually untestable. It's better to say "portable between X and Y" because I think that's what you mean.
In other words, there is plenty of stuff that's portable but not POSIX that you probably want to use. local variables are probably the biggest example. Every shell I know of supports those, including dash, but it's not POSIX (though maybe they're thinking about adding it; the spec is pretty behind)
I addressed this somewhat here: http://www.oilshell.org/blog/2021/01/why-a-new-shell.html#li...
Bashisms are errors if you specify #!/bin/sh. If you want to try it out, you can load random examples on https://www.shellcheck.net/ until you get a #!/bin/sh. Then you'll get warnings such as "SC3010: In POSIX sh, [[ ]] is undefined."
To go back to the discussion: if you want to use Bash features, you're better off switching to Python.
But every shell I know of supports 'local'. It's not a "bash-ism' because it's universally supported.
I consider it essential; otherwise you might as well not use functions in shell.
This is a long argument (and is addressed in the same FAQ), but as someone who's written hundreds of thousands of lines of Python, I think shell is still better for a large set of tasks. A typical small project I write might have 300 lines of shell and 1000 lines of Python, rather than 5000 lines of Python.
But of course there are problems with using shell; if there weren't then Oil wouldn't exist :)
The installer would be ignored as much as my coworkers could manage, and as many customers as possible had to be able to just use it unmodified. Since our supported OSes were RHEL6, RHEL7, SLES12, and SLES15, the best option was bash. Also had to build it on the off chance one of our more restrictive customers was still on RHEL5.11
Pain in the butt, but it worked.
ShellSpec (https://shellspec.info/) is a POSIX compliant testing framework supports all POSIX shells (Bash >= 2.0, dash, ksh88, zsh >=3.1, etc). I'm the author of ShellSpec.
Python stdlib does way more than Bash, it's generally easier to just install Python and run a script that sticks with stdlib, than to install Bash and then figure out what subprocesses it invokes, what packages those map to in the OS package manager, what versions they are, if they're compatible with what you wrote, etc. And if you need more libs, Python also has a cross-platform package manager with a consistent interface. As an example, consider coreutils between macOS and Linux--macOS ships with dramatically different coreutils programs (such as "ls"). The only feasible way to get them consistent across OSes is to stuff everything in a Docker image, but that has its own problems and limitations. Or ask the user to figure it out for themselves, e.g. "use Homebrew".
Yes, when working with sub-processes you need to account for platform differences...which is also true when Python.
A Bash script "just works", while I have to direct customers to install the appropriate version of Python (or whatever else).
The cases in which you even need to use subprocesses is much smaller with Python, because the stdlib replaces a huge amount of external programs typically used with Bash (cat, grep, awk, sed, cut, paste, wc, ls, find, etc.). And so if you stick with stdlib without subprocesses, you don't need to account for platform differences. Python has already done that work for you.
In simple cases, or cases where portability doesn't really matter, then I agree with you. But once you start caring about portability, you have to be careful to consider the compatibility differences of external programs across distributions and OSs on which Bash runs, which vary dramatically among even the simplest or most common of programs such as "ls" or "grep".
Example:
# ubuntu 20.04
echo fooboo | grep -Eo '.+?oo'
> fooboo
# macos big sur
echo fooboo | grep -Eo '.+?oo'
> foo
> boo
Bash scripts being portable has little to do with Bash itself, it's either deliberate by the programmer, or a happy coincidence. IME it's almost always the latter, if it's portable at all (which isn't uncommon in my work).(Also wanted to add that this isn't a unique property to Python either. Any "real" language typically has these same properties.)
Thankfully, with the official deprecation of Python 2.7 and the slow march of progress I hope to be able to better standardize on Python 3 + stdlib (or similar).
/bin/sh is, bash isn't. Portability is a good excuse for using /bin/sh, bash is just a crappy middle ground, it's not as portable as the POSIX shell and it's still not a decent substitution for a proper scripting language.
However, having been down this path myself, i don’t think it’s a great idea.
Beyond the hello world examples you start to run into the need to defang commands being tested or getting better visibility of what they’re doing just so you can make a useful assert, you end up LD_PRELOAD’ing shims to intercept calls and it gets a bit horrifying, very quickly.
There’s so many flavours of make with annoyingly incompatible syntax and that’s before you get into the presence of GMSL or equiv on each platform.
There’s only one fitting answer to this and it’s the output of the following command:
make love
Except depending on your make, this joke doesn’t work :-(Cannot compare it to bash_unit, but I'm happy there's alternatives!
On the other hand, a lot of the stuff that gets written in bash is quite mission critical, so increasing test coverage is a good thing!
Regardless, as you pointed out, regardless of the language some sort of testing is still a good idea. Even small, supposedly 'trivial' scripts deserve some testing!
I get the POSIX argument, but I'm not sure I agree. In my experience at least, POSIX is less supported than bash.
I'm pretty confident that any random terminal I have to ssh into will have a bash shell. I ssh into a lot of terminals.
I know where you're coming from, it just doesn't chime with my personal experience.
I am serious.
I have decided for my own efforts to use python for any kind of command line script and I’m always glad I did.
I do agree though that if you need extensive massaging of output or arguments, python can help and make the whole thing easier.
I've been resisting learning (any more) bash for a while now, but I think I'm going to have to. This looks like it could reduce some of my terror at the insanities of shell scripts (why does one even need two quote characters that do different things?)
$ echo ‘pwd’
$ echo “pwd” $ echo $(pwd)
Incidentally: how does one type back quotes on an iPhone?!Edited to add: shellcheck [0] will flag the back quote usage if you're writing a bash script instead of a sh script.
`Thank you`!
A couple recommendations that make bash a much saner and avoid entire classes of problems:
First, whenever you are expanding a list of args, use "$@"! That exact four character sequence expands to the properly quoted positional params ("$1" "$2" ...) with any necessary escaping included so each param expands as a single word. Almost all of the problems you've probably heard about bash "not handling spaces properly" or otherwise having problems with whitespace or strange characters in filenames are fixed by using "$@". If you're using arrays/hashes, you can get the same effect using "${somearrayorhash[@]}" (quotes included, just like "$@"). Removing the quotes or using the tradition $* is almost always a bug.
Second, always use explicit quotes/brackets! Forget that they were ever optional. Using "$@" fixes most of the whitespace-in-filename problems; expanding your variables with explicit quotes fixes the rest. Assuming these:
showargs() {
echo "$# args"
for i in "$@" ; do
echo "arg[${i}]"
done
}
declare -- name="filename with spaces\\!.txt"
declare -A h='([a]="b c" [foo]="'\''bar'\'' \"baz\" qu*x" )'
Instead of using the traditional shortcuts (which cause problems): showargs $name
# 3 args
# arg[filename]
# arg[with]
# arg[spaces\!.txt]
showargs ${h[*]} # or ${h[@]}
# 7 args
# arg[b]
# arg[c]
# arg['bar']
# arg["baz"]
# arg[qu*x]
Always using quotes/brackets simply does the right thing: showargs "${name}"
# 1 args
# arg[filename with spaces\!.txt]
showargs "${h[@]}"
# 2 args
# arg[b c d e]
# arg['bar' "baz" qu*x]
Bash still has it quirks and strange historical baggage, but in my experi4nce, using these two rules (and actually taking the time to read the bash(1) manpage...) changed writings shell scripts from an annoying mess of buggy arcane incantations into an actually sane(-ish) programming language.Available as `python3-sh` in Ubuntu.
I start to get the feeling that if you feel the need for shell scripts, it might be wise to pause and wonder of this is really the right approach long-term. Especially if you feel the need to put it in git or something.
My experience is that there are often I need to put in some checks for safety and before I know it, you create a mess of grep awk cut sed and you wish you started out with python.
Are you really that much in a hurry or do you have the time to calmly spend a little bit more time to ‘do it right?’
Eventually I realised it was a lost cause and really you just shouldn't use shell scripts for anything that you want to be reliable above a trivial level of complexity.
That was with PowerShell, but I wouldn't be surprised if the same applies to bash as well.
Bash people are the worst when they switch, since they keep the same mentality and continue parsing strings or use other bashizms.
See my answer above also.
Powershell is on another plane, more like Python and less like bash shell scripting.
I used Powershell and Pester a lot and it worked great.
Bash becomes a mess of cat grep sed awk cut
Along similar lines bash's syntax is incredibly streamlined for composing standalone scripts and programs through pipes. A simple bash one-liner like the below would be much more awkward to write in python:
> diff <(netcat $server | grep town | sed 's/street/St' | cut -f 3 | head -n 5) <(cat ./$(psql $query).dat)
It is all about context to me. A shell oneliner is not something I would replace with python but as soon as you start up an editor, think again I would say
We also test REST backend in PowerShell using Pester and home made Posh rest client.
PowerShell is preferred in this house because
a) you can run it on any Windows OS on the spot and modify it in ad hoc manner, you can even debug it with breakpoints etc easily. Also our Linux machines have it so it is unifying admin interface.
b) its powerful, you can do anything in it with few lines of code (one case: we did 10 million SOAP requests using certificate per day for entire country)
c) many Windows tools use it like SqlServer, IIS etc. which makes management way easier - for example we use [1] to install sql server on all dev/prod machines or use [2] to monitor all our servers or use [3] to send CI metrics to influx (all those are just minor samples, we have bunch of stuff like that)
d) we find it way easier to keep CI/CD vars in PowerShell hashtables then in yaml, so our yaml fiels are one liners and everything works locally.
e) Python, ruby and friends are NOT designed for shell work. Its akward, unfriendly and most of all not there on Windows OTB.
---
[1] https://github.com/majkinetor/Install-SqlServer
> I am not that into the way MS is doing the winget thing,
That is years away IMO, no scripting there too, and it moves like a snail. I would really be embarrassed if I were leading that team.
> I see a lot of manual scripts for installing things on Windows CI systems
Yeah, most people suck, like their scripts :-) There is literary 0 chance for you to make reliable installation script in general that works in any context.
> I really like chocolatey but I am worried it will disappear soon.
Just use it. I don't work for them. I maintain core team repo [2]. Its great tool now. What will happen tomorrow nobody knows but like I said, you have escape plan and even if they go down your CI will still work for many years if you set it up properly.
---
[1] https://gist.github.com/majkinetor/a700c70b8847b29ebb1c918d4...
[2] https://github.com/chocolatey-community/chocolatey-coreteamp...
[3] https://github.com/majkinetor/au
[4] https://github.com/majkinetor/au-packages/tags
[5] https://github.com/chocolatey-community/chocolatey-coreteamp...
Powershell is so much powerfull, I’ve written a ton of code in that and also used Pester for unit testing.
Entirely different world compared to shell scripting.