You duct tape a flashlight to the bulkhead so you can work hands free and actually see what you are doing. All you have on you is a broken pocket knife but it’ll do because all you need to accomplish right now is to tighten the housing screws enough. You know this for a fact because you’ve done it three times already in the last 24 hours.
It’s not even a documented procedure — you’ll replace the housing mounts entirely when you’re back at port in three days’ time. You guarantee it — this is the first thing you’ll do even, when you get back to shore. You have my word on that, captain!
The duct tape came unstuck. It was damp and doesn’t work so well (at all) when it’s wet. The flashlight survived the fall. More tape this time should do the job. Tape mount version 2 will still unstick of course, eventually. Nothing stops the damp at sea, but if you use enough tape then you’ll have fixed the power by the time the tape fails. That’s your plan B and you’re sticking to it.
Sure, you could do this job better if you had an impact driver with an automatically illuminated bit chuck, but buying one of those is further down the todo list than fixing the power on the boat, making it back to port, and ensuring the power doesn’t fail this way again, as promised. Or at least won’t fail for the next few shifts.
On your days off you relax by programming in Bash.
"Necessity is the mother of invention" must have been coined by a sailor.
help() {
cat <<'EOH'
my-script — does one thing well
Usage:
my-script <input> <output>
Options:
<input> Input file to read.
<output> Output file to write. Use '-' for stdout.
-h Show this message.
EOH
}
If the indentation bugs you, you can use a simpler sed trick to remove leading space so that you can indent it as desired: help() {
sed -e 's/ //' <<'EOH'
my-script — does one thing well
Usage:
my-script <input> <output>
Options:
<input> Input file to read.
<output> Output file to write. Use '-' for stdout.
-h Show this message.
EOH
} #!/bin/bash
USAGE="my-script — does one thing well
Usage:
my-script <input> <output>
Options:
<input> Input file to read.
<output> Output file to write. Use '-' for stdout.
-h Show this message.
"
help() {
echo "$USAGE"
}
This is my standard approach which is cleaner for putting the documentation at the very top of the file like the linked article.Do you know if these are portable?
Also, putting the help text in code like this instead of a comment allows one to expand $0 so that the command name in the help text always matches the filename and path used in the invocation.
https://dev.to/thiht/shell-scripts-matter
So I took some of the advice and tips offered in there, and wrote a template file to be used as a baseline when writing scripts for any project that might need one:
https://github.com/j1elo/shell-snippets/blob/master/template...
Other resources that I link in the readme of that repo, because they were a great guide to write better and more robust scripts, are:
- Writing Robust Bash Shell Scripts: https://www.davidpashley.com/articles/writing-robust-shell-s...
- Common shell script mistakes: http://www.pixelbeat.org/programming/shell_script_mistakes.h...
- Bash Pitfalls: http://mywiki.wooledge.org/BashPitfalls
- The Bash Hackers Wiki: https://wiki.bash-hackers.org/
EDIT: -for anyone who would like to read some actual examples- I have to manage a bunch of scripts so actually a slightly more up to date version of the template is put into practice by means of a common bash.conf file that then gets sourced by all scripts: https://github.com/Kurento/adm-scripts/blob/master/bash.conf...
People say that for complex things it's better to write Python, but that doesn't fly in embedded or Docker environments. Python is not even present in the default Ubuntu Docker images. Also if all you want to do is really write glue code between CLI programs, shell scripting is the way to go.
Happy coding!
https://twitter.com/hellsmaddy/status/1273744824835796993?s=...
Tl;dr use `shopt -s inherit_errexit`
# Cause command substitution to inherit the value of the `errexit` option.
# Introduced in Bash 4.4
if [ "${BASH_VERSINFO[0]}" -gt 4 ] ||
{ [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 4 ]; }; then
shopt -s inherit_errexit
fihttps://docs.python.org/3/library/argparse.html is great.
Powershell has the Param keyword that functions like argparse in Python
https://docs.microsoft.com/en-us/powershell/module/microsoft...
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
do_help
exit
;;
-v|--version)
do_version
exit
;;
-d|--debug)
debug=true
shift
;;
-a|--arg)
arg_value=$2
shift 2
;;
esac
done import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-v','--version',action='version', version='demo',help='Print version information')
parser.add_argument('-d','--debug', help='Enable Debug Mode')
parser.add_argument('a','arg', help="Argument Documentation")
args = parser.parse_args()
Personally I feel like this is more readable code, gets me better validation, and help docs for "free". That's the attraction. while (( $# )); do
case "$1" in
-h|--help)
usage
exit
;;
-v|--version)
do_version
exit
;;
-d|--debug)
debug=true
;;
-a|--arg)
arg_value="$2"
shift
;;
*)
if [[ ! -v pos1 ]]; then
pos1="$1"
elif [[ ! -v pos2 ]]; then
pos2="$1"
else
>&2 printf "%s: unrecognized argument\n" "$1"
>&2 usage
exit 1
fi
esac
shift
doneArgparse is okay (and being in stdlib makes it always-available), but it's no click. https://click.palletsprojects.com/en/7.x/
A great option when you're stuck with an old crusty script that you don't want to completely rewrite in python, but do want to clean up enough so that you can call it with `-h` and remember how to use it a few months in the future.
Unfortunately, this won't help you if you're on embedded where python isn't in the base system.
https://docs.microsoft.com/en-us/powershell/module/microsoft...
When I write a shell script, I often write a help function if it's not a totally trivial script, but there's no need for this cryptic sed expression, right? You can just call `echo` a few times and do it the obvious way. That works better for maintainability and if you put it at the top of the file then it's immediately visible when opening or using `head` on it.
Neat trick though - sed is super useful for all kinds of things. I had a co-worker who bound a keybinding to a sed one-liner that would automagically reconfigure some files that needed to be changed regularly. I ended up using that trick to allow for changing my terminal console colorscheme with a single command.
But at any rate my typical usage() is generally along these lines (warning watch out for expansions):
usage()
{
cat - <<-EOF
`basename ${0}`: demonstrate here docs
Usage:
`basename ${0}` <required argument> [-o|--optional-param]
Etc. Possibly referencing
default value: ${DEFAULT_VALUE}
EOF
} while read -r line; do
case "$line" in
"###"*)
echo "${line#\###}" ;;
esac
done <"$0" ## help: prints this help message
help:
@echo "Usage: \n"
@egrep -h "^## [a-zA-Z0-9\-]*:" ${MAKEFILE_LIST} | sed -e 's/##//' | column -t -s ':' | sed -e 's/^/ /'
## build: builds JAR with dependencies
build:
mvn compileawk '/^###/' "$0"
if [ $# -eq 0 ] || [ "$1" = "-h" ]; then help exit 1 fi
Relevant spec bits:
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t...
awk 'sub("^### ?","")' "$0"This is only a convention and is entirely up to the calling program.
For example in bash scripts you can use `exec -a name ...` to pass "name" as the 0th argument.
If you are already using #!/bin/bash you might as well use ${BASH_SOURCE[0]} to get the path to the current script.
Do you ever collect families of functions in your shell scripts under different sections? Here's a nice way of printing out all the functions under a given section:
funs(){ sed -n '/^## /h;x;/'"$1"'/{x;s/^\(\w\+\)().*/\1/p;x};x' "$0";}
Where "sections" are delimited by comments of the form "## Section Name" at the beginning of a line. A particularly nice use case is when you write scripts that expect "subcommand" arguments, like $ foo.sh bar baz
and wish to keep track of the available subcommands in the help documentation. Simply collect all your subcommands under the heading "## Subcommands" and stick a funs call in your documentation: usage=$(cat <<USAGE
Usage: foo <subcommand>
Subcommands: $(funs Subcommands)
USAGE
)
The sed one-liner above uses the oft-ignored "hold space" which lets you store data that persists between lines. Here's the same sed but expanded with comments: funs(){ sed -n '/^## /h # Store header line in hold space
x # Swap out current line with header in hold space.
/'"$1"'/{ # Run block if last encountered header matches $1
x # Return to processing current line (instead of header)
s/^\(\w\+\)().*/\1/p # Print function names
x # Whether or not this block runs, we want to return to
# processing the current line. If the block does not
# run, then the hold space contains our current line
# with the active line being our header. So we must
} # return to that state as whell when the block is run.
x # Restore current line from hold space' "$0"
}I also like to feed a heredoc directly into man, which allows you to achieve nicer formatting for the help documentation. Something like this...
man -l - << EOF
.\" Manpage for encpass.sh.
.\" Email contact@plyint.com to correct errors or typos.
.TH man 8 "06 March 2020" "1.0" "encpass.sh man page"
.SH NAME
encpass.sh \- Use encrypted passwords in shell scripts
...
EOF
See encpass.sh for a working example of this -> https://github.com/plyint/encpass.sh/blob/master/encpass.shThat is a very strange argument to me. You find that more cumbersome than jumping around to random functions?
By having the help documentation in a function in the middle or towards the end of the file, I don't have to page down through the help documentation to get to the code that is actually doing things. If I'm really interested in the help documentation, then I'd prefer to look at the nicely formatted version output by the script (<script> --help or whatever) rather than looking in the actual script code anyway.
Admittedly, this may be more of a subjective personal preference item.
from sh import ifconfig print(ifconfig("wlan0"))
% sed -rn 's/^### ?//;T;p' testfile
sed: 1: "s/^### ?//;T;p": invalid command code T
Looks like it might need GNU Sed or something. But honestly if I want to read the top of the file less works just as well.
You could do the same thing with awk instead:
awk '{ if (sub("^### ?", "")) { print; } else { exit; } }'Perl for example was made for problems like this.
perl -ne 'print if ( s/^### ?// )' help() { cat $0 }
"May the source be with you." : )* NIX makes people do creative things
All that Bell Labs stuff still works the way it always did.
* People are still reinventing this particular wheel, and
* This embedded help stuff still somehow hasn't made it into the infrastructure along with autocomplete.
https://gist.github.com/ivanistheone/0454191800c9caad77a52e0...
x=$(command -v $0 2>/dev/null)
sed -rn 's/^### ?//;T;p' $x
Personally I would not use the author's chosen sed commands. exec sed -n '/^###/p' $x
would work fine.https://github.com/zsh-users/zsh-completions/blob/master/zsh...
ARGUMENTS+=(
"a,arch,Target operating system architecture (amd64)"
"b,build,Suppress building application"
"o,os,Target operating system (linux, windows, mac)"
"u,update,Java update version number (${ARG_JRE_UPDATE})"
"v,version,Full Java version (${ARG_JRE_VERSION})"
)
The lines are machine-readable and alignment is computed by the template:https://github.com/DaveJarvis/scrivenvar/blob/master/build-t...
When install script[0] help is requested, the following is produced:
$ ./installer -h
Usage: installer [OPTIONS...]
-V, --verbose Log messages while processing
-h, --help Show this help message then exit
-a, --arch Target operating system architecture (amd64)
-b, --build Suppress building application
-o, --os Target operating system (linux, windows, mac)
-u, --update Java update version number (8)
-v, --version Full Java version (14.0.1)
Using an array reduces some duplication, though more can be eliminated. Scripts typically have two places where the arguments are referenced: help and switch statements. The switch statements resemble:https://github.com/DaveJarvis/scrivenvar/blob/master/install...
Usually parsing arguments entails either assigning a variable or (not) performing an action later. Introducing another convention would allow hoisting the switch statement out of the installer script and into the template. Off the cuff, this could resemble:
ARGUMENTS+=(
"ARG_ARCH,a,arch,Target operating system architecture (amd64)"
"do_build=noop,b,build,Suppress building application"
"ARG_JRE_OS,o,os,Target operating system (linux, windows, mac)"
"ARG_JRE_UPDATE,u,update,Java update version number (${ARG_JRE_UPDATE})"
"ARG_JRE_VERSION,v,version,Full Java version (${ARG_JRE_VERSION})"
)
The instructions to execute when arguments are parsed are thus associated with the arguments themselves, in a quasi-FP style. This approach, not including the FP convention, is discussed at length in my Typesetting Markdown series[1].[0]: https://github.com/DaveJarvis/scrivenvar/blob/master/install...
[1]: https://dave.autonoma.ca/blog/2019/05/22/typesetting-markdow...