Hacker News new | past | comments | ask | show | jobs | submit login
Testing Bash with BATS (opensource.com)
51 points by samebreath on Feb 21, 2019 | hide | past | favorite | 33 comments



My experience doing integration testing with BATS was that it was complete misery (not the BATS project’s fault, I think it’s just inherent in the nature of the thing).

You REALLY want a real programming language for any kind of non trivial integration testing. Things like set up and tear down, checking outputs, and testing many-similar-but-not-identical cases gets hairy fast. Sure you CAN do it in bash but just because you can doesn't mean you should. When we rewrote our BATS tests in Go was a huge step forward for the project.


One time I had to write a Json marshaller in bash (don't ask), and bats was invaluable. BATS is great when you need to unit test bash. And for the most basic integration tests on something that is meant to be a CLI.


Outside of the scope of elaborate CI pipelines, I wonder how useful this can really be.

Big CI pipelines are one of the few instances where I can think of Bash being both an appropriate choice AND the resulting product being large, elaborate, and sensitive to failure - which would benefit from being tested. Most other applications of Bash are generally just so simple that fundamentally altering how you write scripts ("Bash scripts must also be broken down into multiple functions, which the main part of the script should call when the script is executed.") for the sake of testing them seems like it could easily fall into the category of over engineering.

Beyond the CI pipeline use case, wouldn't the tools in which this would actually be properly useful be better off written in a proper programming language?


Whenever someone says "most people don't write large Bash scripts", I have to chime in and say that I would consider most GNU/Linux distros to be giant piles of shell scripts.

A year or two ago, Arch Linux migrated the tests for dbscripts (the server-side of how package releases happen) from shUnit2 to BATS. https://git.archlinux.org/dbscripts.git/


I would consider most GNU/Linux distros to be giant piles of shell scripts.

Yes! That was one of the primary motivations for my Oil project [1]. I was building containers from scratch with shell scripts (in 2012 or so, pre-Docker), and I was horrified when I discovered how Debian actually works.

Why Create a New Unix Shell? http://www.oilshell.org/blog/2018/01/28.html

And of course it's not just Debian. Red Hat, Fedora, Alpine, etc. are all big packages of shell scripts, often mixed with other ad hoc macro processing or Makefiles. Alpine does this funny hack where their metadata is in APKBUILD shell scripts, which is limiting when you want to read metadata without executing shell.

I also point out in that post that Kubernetes is pretty new (2014) and it has 48,000 lines of shell in its repo.

That's true of most cloud infrastructure. If you deal with Heroku, OpenStack, Cloud Foundry, etc. there is a ton of shell all over the place. With buildpacks, Travis CI, etc.

And that's obviously not because the authors of those projects don't know what they're doing. Shell is still the best tool for that job (bringing up Unix systems), despite all its flaws.

The world now runs on clusters of Unix machines, and in turn big piles of shell scripts :)

-----

New release here if anyone wants to help me test: OSH 0.6.pre15 http://www.oilshell.org/blog/2019/02/18.html

Caveat: it's still too slow; I'm mainly looking for people to help test it.

Running bats tests with it would be great. I don't know how bats works with the @test annotation? That doesn't look like valid shell syntax.

OSH also opens a lot of new opportunities for people interested in testing / static analysis and shell.

There was some discussion here, even related to bats, but I'm not sure where it went: https://github.com/oilshell/oil/issues/200

[1] The other being how hard it is to write an autocompletion script!


> I don't know how bats works with the @test annotation? That doesn't look like valid shell syntax.

Bats runs the test files through a preprocessor[1] that, among other things, converts each `@test` case into a shell function. E.g. `@test "the doohickey should frob" { ... }` becomes `test_the_doohickey_should_frob() { ... }`.

[1] https://github.com/bats-core/bats-core/blob/master/libexec/b...


> If you deal with Heroku, OpenStack, Cloud Foundry, etc. there is a ton of shell all over the place. With buildpacks, Travis CI, etc.

Cloud Foundry Buildpacks were rewritten in Go and the (quite elaborate) automation for manufacturing them[0] is mostly test-driven Ruby these days.

I can't claim credit for the former but I had a hand in the latter. Bash is not a sane production language.

[0] https://github.com/cloudfoundry/buildpacks-ci (though I note someone snuck in some Crystal and I suspect it was Dave).


Also, I looked at the buildpacks-ci repo you linked, and it still has ~2000 lines of shell in ~80 files.

For comparison, there's ~12,500 lines of Ruby.

Another issue I have is that if Kubernetes replaces its 44,000 lines of shell with 100,000 lines of Go (or probably more), that's a mixed result at best. There's just a lot of logic to express and shell does it concisely.

Of course, without a better shell, I don't blame them if they switch, but it's still a suboptimal state of affairs.


I agree bash is not a sane production language, and apparently so does Greg Wooledge, somebody who has maintained a popular bash FAQ for a very long time. I quoted him in my latest release announcment:

OSH 0.6.pre15 Does Not Crave Chaos and Destruction http://www.oilshell.org/blog/2019/02/18.html

I did hear about Cloud Foundry moving from shell to Go for some things, and I also heard that Kubernetes was rewriting a lot of its shell. [1]

(1) If you had to choose between Go and bash, I can understand choosing Go. I saw some blog posts along those lines [2].

Although I don't think it's optimal. I'd be interested in seeing some of those "shell scripts in Go" if you have a link. There should be a better language for sane shell scripting (hence Oil).

(2) I imagine it's not fun to port thousands of lines of shell to Go (or Ruby) by hand. Oil is supposed to help with that via an approximate translation and good errors, but that part isn't done yet.

What IS close to done is a dialect of bash that is sane -- or CAN BE MADE sane with user feedback.

For example, in the latest release I added set -o strict-argv, and there's also set -o strict-control-flow and strict-word-eval. I will add a strict-ALL to opt into all at once, as mentioned in the release notes.

Also, OSH gives you all your parse errors at once, rather than having them blow up at runtime later:

How to Parse Shell Like a Programming Language http://www.oilshell.org/blog/2019/02/07.html

[1] I just pulled the Kubernetes and there's 44K lines in *.sh files, as opposed to 48K lines a couple years ago. If it were proportional to the project's growth, I would have expected it to be 100K lines by now, so it seems they are indeed getting rid of shell!

However this only removes ONE LAYER of shell. Any cloud service that uses a Linux distro (which is all of them) is papering over all the nasty layers of shell underneath! I hope that Linux distros will gradually move to Oil to get rid of this legacy.

[2] https://jvns.ca/blog/2017/07/30/a-couple-useful-ideas-from-g...


That is actually a really solid point. I still don't know that it's necessarily the right way, but I'd certainly rather those scripts be tested!


I've found shell scripting to be superior to other programming languages when the job calls for performing some combination of the following:

1) calling lots of commands

2) using lots of pipelines

3) handling exit codes from 1 and 2 for error handling and conditional execution control

4) doing job control for processes and process groups (backgrounding, pausing, foregrounding, sending signals, etc.)


Have you looked at Plumbumm for Python? It handles everything you just mentioned:

https://plumbum.readthedocs.io/en/latest/

There's no reason not to use Python to achieve the same thing, and you get all the benefits of using Python instead of Bash.

I'm sure other languages have similar libraries.


Bash is proper programming language, though not necessarily modern nor ergonomic.

The problem is:

1. Bash is assumed to be everywhere, so people use it for maximum portability or bootstrapping.

2. It started as a 10-100 line "quick" script, but then grew into a monster and nobody wanted to take the hit to rewrite it in a modern programming language.

I've found when you enforce the same software best practice requirements regardless of language, people start choosing not-Bash since "they have to do it right anyway". Many devs see Bash as a shortcut to avoiding the extra work.


Tangential question, but are there any efforts to treat shell scripts as a compilation target? It seems like such an insane language, I don't know why another nicer language hasn't emerged on top of it.


> It seems like such an insane language

I can understand that because I thought that way too before I really learned it.

Bash is a language like any other: it takes reading and practice to get good at it. Also most of the WTFs in Bash actually make decent sense when you consider that whitespace is the delimiter in bash (for example setting a variable is VAR='value' instead of VAR = 'value'. That trips newer people up pretty badly at first).

Most people also abandon good software practices when they write bash for some reason. For example, you'd never write 100 lines of ruby or javascript without declaring functions, yet people do that all the time in bash (they shouldn't). If people followed good practice, I think bash would seem a lot less insane.


I think lack of proper arrays and structured data types is a big issue with bash. Basically everything is a string, with all the issues around escaping and corner cases that that implies


Bash (4 at least) has proper arrays and hash maps, but granted, the syntax is hideous. The arrays are also limited, but hash maps are somewhat sane.

> declare -a ARRAY > declare -A MAP


Yeah very fair. I actually thought I included a line like this: "If you need arrays, hashes, or floating point math then bash is a poor choice" but I must have deleted it (I've been trying to use more brevity when I write).


> It seems like such an insane language

I wouldn’t say insane, just limited.

But the main point of the shell and of scripting the shell is to execute programs and creating pipelines.

If you need to do complex things, or you need special data structures or you need to process the output directly yourself in any significant way, then shell scripting is simply not the right tool for the job.

And in the case where you want to use another language why do you want to transpile to shell script? Better to just install Python 3 or something on the server. (It might even be installed there by default already.)


Yes! So much yes. Many developers write crappy Bash scripts because they don't see it as a real language, but it's an interpreted language just like Ruby, Python, Perl, etc.

If you're going to design modular libraries and scripts with best practices, unit tests, etc, for those languages, then you should be doing the same with Bash.

If you're writing production scripts using Bash, you should be following software best practices - don't give me any "it's just Bash" excuses.

Mocking In addition to bats-mock (part of the bats library in the article), there is another mocking approach: https://pbrisbin.com/posts/mocking_bash/ They both have their uses.

Documentation NaturalDocs ( https://www.naturaldocs.org/ ) can happily parse & extract doc comments from your bash scripts & libraries: https://www.naturaldocs.org/ Just define your own language:

https://www.naturaldocs.org/reference/languages.txt/#adding_...

  Format: 1.4
  
  Language: bash
  Shebang Strings: bash
  Extensions: bash sh
  Ignore Extensions: bats
  Line Comments: #
Linting Use shellcheck, it's amazing: https://www.shellcheck.net/ If you use Vim, w0rp/ale knows how to integrate with it for real-time feedback: https://github.com/w0rp/ale/tree/master/ale_linters/sh

Argument Parsing Use Docopt ( http://docopt.org/ ) to get modern arg-parsing in your Bash scripts. Don't waste your time trying to roll your own or use the built-in anemic arg-parsing. Shells implementation: https://github.com/docopt/docopts

Use Bash built-ins Stop invoking external processes for features already available in Bash: https://github.com/dylanaraps/pure-bash-bible

More good resources:

* http://www.tldp.org/LDP/abs/html/

* https://www.gnu.org/software/bash/manual/bashref.html

* http://mywiki.wooledge.org/BashGuide

BUT why are you using Bash? Go use a modern language and don't suffer the idiosyncrasies of Bash where you have to re-invent many common libraries already available like JSON parsers, etc.

My favorite Bash replacement is Python + Plumbum: https://plumbum.readthedocs.io/en/latest/ All the convenience of Bash when invoking commands (no more subprocess()!) with the joy of Python.


> Linting Use shellcheck

Shellcheck has saved me countless times, but I noticed it's hard to get people to adopt it into their workflow.

I've introduced probably a dozen people to shellcheck and warn them ahead of time that their scripts are broken. They try it, see a flood of errors and warnings, close the window, and never use it again.

The logic surrounding their undefined variables and improper conditions happen to work as hoped, by luck, combined with their strict adherence to "it's best practice to not use spaces" both keep their scripts crawling along "just fine".


I just went to install it with macports, it says:

"The following dependencies will be installed: autoconf automake bzip2 clang-3.3 clang-4.0 clang_select cmake curl curl-ca-bundle db48 expat gcc6 gdbm ghc ghc-bootstrap glib2 gmake gmp help2man hs-json hs-mtl hs-parsec hs-primitive hs-quickcheck-devel hs-random hs-regex-base hs-regex-tdfa hs-syb hs-text hs-tf-random icu isl ld64 ld64-97 legacy-support libarchive libcxx libedit libffi libgcc libgcc6 libgcc7 libidn2 libmpc libomp libpsl libtool libunistring libuv libxml2 libxslt llvm-3.3 llvm-3.5 llvm-4.0 llvm_select lz4 lzo2 m4 mpfr openssl p5.28-locale-gettext pcre perl5 perl5.26 perl5.28 pkgconfig python27 python2_select python_select readline sqlite3 texinfo xar xattr zlib zstd Continue [Y/n]:" ..uh maybe not right now.


It looks like the MacPorts version of shellcheck is substantially out-of-date[1].

`brew info shellcheck` shows just three (build-time) dependencies: cabal-install, ghc, and pandoc.

[1]: https://github.com/macports/macports-ports/commits/master/de...


Yes, it hasn't been updated because dependencies have to be updated first. See recent discussion on updating ghc: https://trac.macports.org/ticket/48899#comment:43.


Oh thanks! That's a fascinating discussion.


Yeah, probably is, I only use macports as my computer is substantially out of date (2006)! But even my 'port info shellcheck' (0.3.8) only says:

Build Dependencies: apple-gcc42

Library Dependencies: ghc, hs-json, hs-mtl, hs-parsec, hs-quickcheck-devel, hs-regex-tdfa


I like how there's GCC, GCC6, GCC7, LLVM-3.3, LLVM-3.5, and LLVM-4.0...


> If you're going to design modular libraries and scripts with best practices, unit tests, etc, for those languages, then you should be doing the same with Bash.

Or: not use Bash at all.


Which is exactly my closing argument :)


Well now I just feel dumb.


Im probably in minority but I use powershell on my arch and love it. Finding duplicates has never been so easy-

`Get-childitem -Recurse |% {[System.Tuple]::Create( $_.FullName, (md5sum $_.FullName).split( " " )[ 0 ] ) }) | Group-Object Item2 | Where-Object { $_.Count -ge 1}`


You win for enthusiasm when it comes to that syntax. I appreciate I pipe output to uniq (MacOS) to get uniques, proving that easy is an extremely relative value :)


Use powershell ;)

It's wonderful.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: