ariketa yo

in order to facilitate my giving talks on bash, i cobbled together some key bindings and functions, improving a bit on this each time. after each talk, somebody would usually find me to ask what i was using to advance through my examples during the talk. i've finally cleaned it up, made it more general, and have released it here: ariketa

the basic premise is to load an array with the examples you'll be presenting, then use the key bindings to advance through the examples, editing, executing, highlighting, or diffing at will. the key bindings were chosen to avoid overwriting defaults, so all the goodies you expect to find in bind -P like shell-expand-line and edit-and-execute-command will keep working.

a simple, ad hoc array of tuples using expansion

while bash does not have nested data structures directly, there are various approaches which can offer approximate funcitonality in particular contexts. one of the issues i began having after having given variants of my bash talk several times was remembering how many (more) examples i had related to a particular slide or topic. i would find myself advancing too far for a split second - not the end of the world, but can interrupt your canter. to counter this, i took my array of examples and prepended a tag to each element, effectively approximating an array of tuples. the following is an example i used in my recent talk at Linuxfest Northwest, tagged "while and until" to indicate the relevant section of slides the example is associated with:

    'while and until:
    until (( ++count > 3 ))
        echo $count

now that i'd imposed a simple, string-delineator based structure on my data, i could use parameter expansion modifiers as tuple handlers. to unpack an example, we need to remove everything from the beginning of my the string up to my delineator. i chose a colon followed by a newline (:$'\n' [1]); we can use the following expansion:


that leaves us with just the example code. similarly, we can expand just the tag by removing everything from the end of the string up to the first :\n with:


since i didn't want untagged examples to necessarily break everything, as well as for ease of reuse, i made functions to unpack examples and tags. of note is a deficiency in my current system - if there are any untagged examples, they will be incorrectly treated if they happen to contain :\n in their text, to which there are several alternative solutions i may eventually adopt in ariketa. for now, we get a simple function for unpacking examples, since we can't reliably detect a false positive, but the tag unpacking function will return a failure code if nothing looked like a tag [2], because we don't want to shove a whole example into the tag area in the ariketa status header:

_example.unpack() {
    printf "%s" "${1#*:$'\n'}"

_tag.unpack() {
    local _tag="${1%%:$'\n'*}"
    (( ${#_tag} < ${#1} )) || return 1
    printf "%s" "$_tag"

the current limitations of this implimentation notwithstanding, all that was left was to make tag adjacency indicator functions which determine if there are examples of the same tag before or after the current example, along with a display function to pull it all together into a concise format for use in the example header, if a tag is found [3]:

_tag.same_as_next? () {
    [[ "$(_tag.unpack "${_examples[$_I]}")" \
    == "$(_tag.unpack "${_examples[$((_I+1))]}")" ]] \
        || return 1
    printf "%s" "${1:-+}"

_tag.same_as_prev? () {
    [[ "$(_tag.unpack "${_examples[$_I]}")" \
    == "$(_tag.unpack "${_examples[$((_I-1))]}")" ]] \
        || return 1
    printf "%s" "${1:-+}"

_tag.display () {
    local _tag=$(_tag.unpack "${_examples[$_I]}") \
        && printf "[$(_tag.same_as_prev?) %s $(_tag.same_as_next?)]" "${_tag}"

with this, we get a concise header showing the current tag, along with + to either side indicating whether there are more examples with that tag before or after the current one, respectively:

> ## example 54 / 91  [+ while and until +]
[1]$' quoting is like double quoting ("..."), but with backslash-escaped characters being replaced as specified by the ANSI C standard (\n, for example). when used in a quoted expansion context, e.g. "${var#*$'\n'}", the extquote shell option must be set; lucky us, it's set by default. to be clear, the string :$'\n' would be identical to the string $':\n' -- directly adjacent strings, whether using different quoting styles or none at all, form one string ultimately, and non-special characters are always the same.
[2]the check here verifies that there are fewer characters in the string resulting from our conditional expansion than there were in the input; if the :$'\n'* pattern is not found in the string, nothing will be removed at expansion.
[3]bonus points if you can spot the feature improvement i had in mind writing some of the _tag. functions.