TODO.rst
Planned Constructs
==================
Bash Options
------------
This will be inserted at the top of every script (including the comments):
.. code:: bash
set -o errexit # exit immediately if a command fails
set -o pipefail # ...including commands in a pipeline
set -o nounset # use of undefined variables is an error
set -o noclobber # prevent redirection from overwriting files
shopt -s nullglob # expand an unmatching glob pattern to a null string
shopt -s dotglob # glob matches files starting with a dot
Safer Echo
----------
.. code:: bash
# print a string
@echo "--help"
# print an array
@echo @args
Generated bash:
.. code:: bash
# print a string
printf '%s\n' "--help"
# print an array
printf '%s\n' 'args=('
for i in "${!args[@]}"; do
printf ' [%q]=%q\n' "${i}" "${args[${i}]}"
done
printf ')\n'
Trap and Re-Raise Signal
------------------------
Inspired by `Proper handling of SIGINT/SIGQUIT <http://www.cons.org/cracauer/sigint.html>`_.
.. code:: bash
@trap INT QUIT {
...
}
A well-behaved script should re-raise every signal that it can.
To do this properly, the script must reset the signal and then kill itself
with that same signal. Since there's no way to determine which signal was
raised, there must be a separate handler for each so that the correct signal
is re-raised.
.. code:: bash
__INT_QUIT_handler() {
...
}
trap '__INT_QUIT_handler "$@"; trap INT; kill -INT $$' INT
trap '__INT_QUIT_handler "$@"; trap QUIT; kill -QUIT $$' QUIT
Note that for clean-up tasks, a context manager should generally be used instead.
Context Managers
----------------
Inspired by `Python's @contextmanager decorator <https://docs.python.org/3.4/library/contextlib.html#contextlib.contextmanager>`_.
A context manager is responsible for running some code when entering and
exiting a block. The exiting code is always run, even when the block is
terminated early. This is especially useful for clean-up tasks, like deleting
a temporary directory. For example, the following bashup code would define
just such a context manager:
.. code:: bash
@ctx mktemp {
local tmp=$(mktemp "$@")
@yield "${tmp}"
rm -rf "${tmp}"
}
The context manager could then be used as follows:
.. code:: bash
# multi-line version
@with(mktemp -d) as tmp {
...
}
# single-line version
... @with(mktemp -d) as tmp
The generated bash would look something like this:
.. code:: bash
with_mktemp() (
local body_fn=${1}; shift
local tmp=$(mktemp "$@")
exit_ctx() {
rm -f "${tmp}"
}
trap exit_ctx EXIT
"${body_fn}" "${tmp}"
)
...
ctx_0() {
local tmp=${1}
...
}
with_mktemp ctx_0 -d
Note that the body of the context ends up being evaluated in a subshell. If
this is unacceptable, consider using a *decorator* instead.
Decorators
----------
Like Python decorators, but evaluated every time the function is called.
.. code:: bash
# Decorator to temporarily toggle off exiting on non-zero exit statuses.
@fn ignore_failure {
set +e
"$@" || :
set -e
}
# Print a message with the name and arguments of the decorated fn.
@fn show_args {
echo ">>> $@"
"$@"
}
@ignore_failure
@show_args
@fn enable_ramdisk size, path='/ramdisk' {
...
}
Equivalent bash:
.. code:: bash
ignore_failure() {
set +e
"$@" || :
set -e
}
show_args() {
echo ">>> $@"
"$@"
}
enable_ramdisk() {
ignore_failure show_args enable_ramdisk_impl "$@"
}
enable_ramdisk_impl() {
...
}
Decorators can also be used to decorate a single line:
.. code:: bash
false @ignore_failure
Equivalent bash:
.. code:: bash
ignore_failure false
The bash is actually shorter (by one character), but I think the bashup reads better.
Aliases
-------
Aliases would be useful for keeping your bashup code as
`DRY <http://en.wikipedia.org/wiki/Don%27t_repeat_yourself>`_ as possible.
They'd have to be evaluated before any other constructs.
For example, let's say you've defined a context manager which creates a
temporary file with a longer-than-normal name:
.. code:: bash
@mytmp = @with(mktemp tmp.XXXXXXXXXXXXXXXXXXXXXXXXXX)
The alias can then be treated as a literal text substitution:
.. code:: bash
@mytmp as tmp {
...
}
Macros
------
Macros are aliases that can take options. Or, more accurately - aliases are
just a special case of macros that take no options.
Here's a similar example to above:
.. code:: bash
@mytmp(extra) = @with(mktemp @extra tmp.XXXXXXXXXXXXXXXXXXXXXXXXXX)
.. code:: bash
@mytmp(-d) as tmp_dir {
...
}
Insert External Text
--------------------
Again, in the spirit of DRY code, it may be useful to include a snippit of code
or plain text from an external source (either from a local file, an internal
network, or from the web).
.. code:: bash
# Insert a file from the web:
@insert https://acme.com/scripts/snippit.sh
# Insert a gist from GitHub:
@insert gist:5725550
# Insert a file from a GitHub repo:
@insert github:user/repo@revision
# Insert a file by relative path (and comment out each line!):
@insert LICENSE.txt --comment
Unlike other constructs, this does not compile into some equivalent bash code.
Instead, the text is inserted directly into the document before other
constructs are evaluated. (Aliases and macros would have to be evaluated both
before and after inserting snippits).
Script Directory
----------------
The ``@dir`` alias will allow concise access to directory from which the
script is running. It is (functionally) equivalent to this:
.. code:: bash
$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
`See this Stack Overflow discussion <http://stackoverflow.com/a/246128>`_ for
the pros and cons of this approach.
Sourced
-------
The ``@sourced`` alias will allow concise checking of whether or not the
script is being sourced or called directly. It is exactly equivalent to:
.. code:: bash
[ "${BASH_SOURCE[0]}" != "${0}" ]
It can be used to avoid side effects when the script is being sourced:
.. code:: bash
@sourced || main "$@"
Check if Unset
--------------
The ``@notset`` macro allows for checking whether or not a variable is set
without willing it into existence. For example, ``@notset(my_var)`` is exactly
equivalent to:
.. code:: bash
[ "_${my_var:-notset}" == "_notset" ]
Docopt
------
Docopt command-line builder:
.. code:: bash
# Naval Fate.
#
# Usage:
# naval_fate ship new <name>...
# naval_fate ship <name> move <x> <y> [--speed=<kn>]
# naval_fate ship shoot <x> <y>
# naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
# naval_fate -h | --help
# naval_fate --version
#
# Options:
# -h --help Show this screen.
# --version Show version.
# --speed=<kn> Speed in knots [default: 10].
# --moored Moored (anchored) mine.
# --drifting Drifting mine.
#
# Version:
# Naval Fate 2.0
@fn main {
@echo @args
}
@sourced || {
@docopt
main
}
The above bashup would generate something like the following bash:
.. code:: bash
#!/bin/bash
DOCOPT_DESC='Naval Fate.'
DOCOPT_USAGE='
naval_fate ship new <name>...
naval_fate ship <name> move <x> <y> [--speed=<kn>]
naval_fate ship shoot <x> <y>
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
naval_fate -h | --help
naval_fate --version'
DOCOPT_OPTIONS='
-h --help Show this screen.
--version Show version.
--speed=<kn> Speed in knots [default: 10].
--moored Moored (anchored) mine.
--drifting Drifting mine.'
DOCOPT_VERSION='Naval Fate 2.0'
main() {
printf '%s\n' 'args=('
for i in "${!args[@]}"; do
printf ' [%q]=%q\n' "${i}" "${args[${i}]}"
done
printf ')\n'
}
docopt_usage() {
printf 'Usage:\n%s\n\nOptions:\n%s' \
"${DOCOPT_USAGE}" \
"${DOCOPT_OPTIONS}"
exit 1
}
docopt_help() {
printf '%s\n\nUsage:\n%s\n\nOptions:\n%s\n\nVersion:\n %s' \
"${DOCOPT_DESC}" \
"${DOCOPT_USAGE}" \
"${DOCOPT_OPTIONS}" \
"${DOCOPT_VERSION}"
exit 0
}
docopt_version() {
printf '%s\n' "${DOCOPT_VERSION}"
exit 0
}
docopt_error() {
printf 'Unknown option "%s"\n' "${1}"
docopt_usage
}
docopt() {
args=()
while (( $# )); do
if [ "${1}" == "-h" ] || [ "${1}" == "--help" ]; then
docopt_help
elif [ "${1}" == "--version" ]; then
docopt_version
elif [ "${1}" == "ship" ]; then
shift
if [ "${1}" == "new" ]; then
shift
if [ $# -eq 0 ]; then
printf 'Failed to specify at least one <name>\n'
docopt_usage
fi
args["<name>"]=(${@})
shift $#
args["new"]=true
elif [ "${1}" == "shoot" ]; then
shift
if [ $# -ne 2 ]; then
printf 'Failed to specify arguments: <x> <y>\n'
docopt_usage
fi
args["<x>"]=${1}
args["<y>"]=${2}
shift 2
args["shoot"]=true
else
if [ $# -ne 1 ]; then
printf 'Failed to specify argument <name>\n'
docopt_usage
fi
args["<name>"]=${1}
shift
if [ "${1}" == "move" ]; then
shift
if [ $# -lt 2 ]; then
printf 'Failed to specify arguments: <x> <y>\n'
docopt_usage
fi
args["<x>"]=${1}
args["<y>"]=${2}
shift 2
while (( $# )); do
if [[ "${1}" == --speed=* ]]; then
args["--speed"]=${1#--speed=}
shift
else
docopt_error "${1}"
fi
done
args["move"]=true
else
docopt_error "${1}"
fi
fi
elif [ "${1}" == "mine" ]; then
shift
if [ "${1}" == "set" ] || [ "${1}" == "remove" ]; then
args["${1}"]=true
shift
else
docopt_error "${1}"
fi
if [ $# -lt 2 ]; then
printf 'Failed to specify arguments: <x> <y>\n'
docopt_usage
fi
args["<x>"]=${1}
args["<y>"]=${2}
shift 2
if [ $# -eq 0 ]; then
:
elif [ "${1}" == "--moored" ]; then
args["--moored"]=true
shift
elif [ "${1}" == "--drifting" ]; then
args["--drifting"]=true
shift
else
docopt_error "${1}"
fi
args["mine"]=true
else
docopt_error "${1}"
fi
shift
done
}
[ "${BASH_SOURCE[0]}" != "${0}" ] || {
docopt "$@" 1>&2
main
}
Note that the above code requires Bash >= 4.0 due to the use of associative
arrays.