====== 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 ]] }