Did you know that Bash scripts run sequentially from disk?[1]
This means that if the script changes on disk during execution, Bash will blindly resume at the current offset. Depending on how the script changed, that can be disastrous.
Here’s a contrived example:
#!/bin/bash
echo hi
sleep 20
#rm -rf *
If this script is updated on disk during that twenty second pause, then Bash
will run whatever it finds at byte offset 30, previously the commented line #rm -rf *
.
Let’s say you decide the sleep should be shorter, so you tweak it:
#!/bin/bash
echo hi
sleep 5
#rm -rf *
If the script was in the midst of its 20-second pause when you wrote this to
disk, Bash would execute the rm -rf *
because the removal of a digit from the
sleep causes the byte offset 30 to skip over the comment character. Yikes!
Now that your imagination is running scared, here’s one way to fix it:
#!/bin/bash
main() {
echo hi
sleep 20
echo there
# Explicit exit to avoid execution continuing dangerously at byte offset.
# See https://arongriffis.com/2023-11-18-bash-main
exit
}
main "$@"
This works because Bash loads the function into memory before running it, and the function exits instead of returning.
As a pleasant side effect, this structure lets you define your other functions
after main
, so that you can jump directly into the main flow in your editor:
#!/bin/bash
main() {
build && test && install
# Explicit exit to avoid execution continuing dangerously at byte offset.
# See https://arongriffis.com/2023-11-18-bash-main
exit
}
build() {
run the compiler or something
}
test() {
because you always write tests
}
install() {
life is good
}
main "$@"
Seriously, though, have you considered Python?
[1] Technically, the granularity is compound
command or line, whichever is larger. For example, while true; do ... done
is
read as a unit from disk, even if it spans multiple lines, and echo hi; echo hello
is read as a unit from disk even though it is multiple commands, because
they’re on the same line.