====== 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 [[https://github.com/koalaman/shellcheck|linting]] and [[https://steinbaugh.com/posts/shell-strict-mode.html|"strict mode"]], there are many fewer [[https://en.wiktionary.org/wiki/footgun|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 [[https://github.com/bats-core/bats-core|bats]] with ''#@test'' functions. ((Possible 3rd: identifying when a test is appropriate.))
===== 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 ''[[https://bats-core.readthedocs.io/en/stable/writing-tests.html#comment-syntax|# @test]]'' on the same line as the function definition. ((long from function definition syntax ''function foobar_ { #@test'' also works)).
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 ([[https://github.com/joaotavora/yasnippet|yasnippet]], [[https://github.com/SirVer/ultisnips|ultrisnips]]) on the shebang of bash scripts to make setup painless.
See mine for [[https://github.com/WillForan/dotconf/blob/master/vim/.vim/UltiSnips/sh.snippets|vim]] and [[
https://github.com/WillForan/dotconf/blob/master/emacs/snippets/sh-mode/shebang|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 "$@"
{{:public:pasted:20220819-162428.png}}
(missing "global" modifier in sed: ''%%s///g%%'')
====== Convenience Utilities ======
''[[https://github.com/LabNeuroCogDevel/lncdtools/blob/master/iffmain|iffmain]]'' from [[https://github.com/LabNeuroCogDevel/lncdtools|lncdtools]] also adds unoffical strict-mode (''set -euo pipefail'') and trap
mainfunc() { echo "hi"; }
eval "$(iffmain "mainfunc")"
mainfunc_test(){ #@test
run mainfunc
[[ $output =~ ^h ]]
}