Create and use annotations (similar to Java annotations or Python decorators) for Bash scripts.
bash-annotations was developed with Bash version 5.0.17.
No effort has been made to ensure the project is compatible with older versions of Bash (although more recent features, such as associative arrays, are not used by the project).
Install git hooks:
git config core.hooksPath .githooks && chmod +x .githooks/pre-commitAdd bash-annotations to your project as a submodule:
git add submodule https://github.com/david-luison-starkey/bash-annotationsAlternatively, add and update:
git submodule update --init --recursiveUpdate the submodule after adding it to your project:
git submodule update --remote bash-annotationsBegin by sourcing bash-annotations.bash into the desired script.
Once bash-annotations.bash is sourced the import function is available to concisely source files from within the /bash-annotations/src/ directory structure.
Then you can begin defining custom annotations that target variables and functions.
# Sourcing this file automatically performs the setup to enable annotation behaviour
source bash-annotations/src/bash-annotations.bash
# No leading forward slash is required for imports
# Multiple imports can be inlined...
import annotations/weave.bash annotations/observe.bash
# or imported separately
import annotations/weave.bash
import annotations/observe.bash
# Define an annotation that takes a function body and weaves that into that of the annotation target's.
@weave "PRE"
requires_number_of_arguments() {
local expected_number_args="${1}" # annotations argument
local annotated_function_number_args="${#@}" # annotation target's number of arguments
if [[ "${expected_number_args}" -ne "${annotated_function_number_args}" ]]; then
echo -n "[ERROR] @requires_number_of_arguments -> ${FUNCNAME[0]}(): ${BASH_LINENO[0]} - "
echo "expected ${expected_number_args} arguments but received ${annotated_function_number_args}"
return 1
fi
}
@requires_number_of_arguments 2
greeting() {
local greeting="${1}"
local recipient="${2}"
echo "${greeting} ${recipient}"
}
greeting # [ERROR] @requires_number_of_arguments -> greeting(): 29 - expected 2 arguments but received 0
greeting "Hello, " "World!" # Hello, World!bash-annotations provides two functions that act as annotation factories, abstracting complexity and affording the easy creation of custom annotations.
- @observe creates annotations that trigger before and/or after their target annotated type is called. Listeners persist for the lifetime of the script.
- @weave creates annotations that inject their body into their target annotated function. The listener is consumed on first invocation.
Once a function is annotated with either @observe or @weave, an annotation version of that function can then be used (which will be declared at runtime).
The format for annotations is:
- "@" + the function's namespace.
@observe FUNCTION PRE
func() {
:
}
@funcAnnotations can take positional parameters like regular Bash functions.
Annotation factories and the annotations they declare are designed to be placed above their target types. Comments and other annotations may exist between an annotation and its target without affecting functionality.
Empty lines and any other content interfere with an annotation's ability to locate its target, causing the annotation (either custom or factory) to do nothing.
Correct usage:
@one
# @two -- commented out, will not execute
@three
# This is a function.
# It takes no arguments.
# It does very little.
target_function() {
echo "Hello world"
}Incorrect usage:
@annotation
declare variable="100"Any number of @weave and @observe annotations can be used on the same function (see Gotchas for @weave/@observe VARIABLE incompatibility and annotation order of execution).
Functions used to create annotations remain usable in their non-annotation form.
Both @observe and @weave take arguments that determine the behaviour of the annotation they are declaring.
Arguments must be upper-case (to avoid namespace clashes with the function keyword, and for stylistic reasons, mimicking Java ENUMs).
All annotations evaluate lazily - annotation behaviour won't occur until a function or variable is called (not when declared/initialised).
Annotations created with @observe can target functions or variables, and be set to trigger before, after, or before and after the annotated type is called. Once triggered, the body of the annotation is executed.
@observe takes two arguments, target type and trigger condition.
Target type arguments:
- FUNCTION
- VARIABLE
Trigger condition argument:
- PRE
- POST
- PREPOST
@observe FUNCTION POST
cleanup() {
rm "temporary_file.txt"
ls
}
@cleanup
create_temp_file() {
touch "temporary_file.txt"
ls
}
create_type_filedeclare -xgi VARIABLE_COUNT=0
@observe VARIABLE POST
call_count() {
VARIABLE_COUNT=$((VARIABLE_COUNT + 1))
}
@call_count
declare variable="Counting"
echo "${variable}: "
echo "${VARIABLE_COUNT}"
echo "${variable}: "
echo "${VARIABLE_COUNT}"Output:
Counting:
1
Counting:
2
@weave creates an annotation that injects its body into the annotated function. Unlike @observe, the listener is consumed after the first invocation — the target function is permanently rewritten with the injected code.
Annotations created via @weave can only annotate functions.
Annotations created with @weave can inject their body before, after, or before and after the annotated function's body.
@weave takes one argument, the injection location.
Injection location argument:
- PRE
- POST
- PREPOST
@weave PRE
injection() {
cd "${HOME}"
}
@injection
target_function() {
pwd
}
target_function
declare -f target_functionOutput:
/home/${user}
target_function()
{
cd "${HOME}";
pwd
}
Annotations use the Bash DEBUG trap to intercept function and variable calls at runtime. @observe creates persistent listener functions registered in __BA_FUNCTION_ARRAY; @weave rewrites the target function body once and removes itself.
See docs/INTERNALS.md for a full explanation.
bash-annotations exposes special variables (e.g. annotated_function, inject_annotated_function, annotation_source_file) that are available inside annotation function bodies. @weave annotations also make their local variables and positional parameters accessible to the function they annotate.
See docs/API_REFERENCE.md for the full variable reference and examples.
There are several behavioural constraints to be aware of: DEBUG trap override on source, special variable line-placement rules, @weave/@observe VARIABLE incompatibility, and performance implications of @observe.
See docs/GOTCHAS.md for the complete list.