Test Driven Bash
Historically, shell scripting is derided for thumbing it's nose at best practices. But with a little setup, bash is capable of embracing test driven development. Combined with linting and "strict mode", there are many fewer footguns.
There are two components to ergonomic testing with bash scripts: (1) everything in a function w/ a $(caller)
check (2) writing test functions for bats with #@test
functions. 1)
Function First
A bash file can check to see if it's being sourced (from another script or from bats
) or if it's executed directly . Think python's __name__ = "__main__"
idiom. Instead of __name__
, we have $(caller)
which starts with 0
when executed from a file.
We can make a single bash file both testable while still being able to call it directly to execute commands.
Here's an example file foobar.bash
# NOTE: not calling the function the same as the file # if the script is in $PATH, bash might endlessly recurse # calling the file instead of the function foobar_(){ echo hi; } # only hit when ./foobar.bash if [[ "$(caller)" ~= "0 "* ]]; then foobar_ fi
Testing
Writing tests is easy! We just need a function with # @test
on the same line as the function definition. 2).
test_foobar() { #@test run foobar_ [[ $output == "hi" ]] }
With that appended to foobar.bash
we can test it like
bats --verbose-run
and run it like
./foobar.bash
Integrating Workflow
Writing like this takes a bit of joy out of using bash. There's boiler plate. That's for java and python programming. To avoid the buzzkill, I use a snippet (yasnippet, ultrisnips) on the shebang of bash scripts to make setup painless. See mine for vim and emacs
Example
# spot the bug getid(){ grep -oP '\d{5}_\d{3}_\d{2}' <<< "$*" | sed 's/_/ /'; } function test_getid { #@test run getid fake/path/sub-98765_123_45.nii.gz [[ $output == "98765 123 45" ]] } [[ "$(caller)" != "0 "* ]] || getid "$@"
(missing “global” modifier in sed: s///g
)