Commands#

Registering commands, subcommands, and dispatchers.

The experiment script skeleton#

The minimal top-and-tail every Knit experiment script needs.

APIs: knit, KNIT_SCRIPT_NAME

A Knit experiment is an ordinary Bash script that sources knit.sh near the top and hands control to Knit’s dispatcher at the very bottom:

source knit.sh

Everything between those two lines registers commands. The final line dispatches the script’s arguments to whichever command the user named:

knit "$@"

Inside any command you can read KNIT_SCRIPT_NAME — the base name of the script that sourced knit.sh — to build messages that name the experiment (for example "Run ./${KNIT_SCRIPT_NAME} bootstrap first.").

Register a command#

Declare a command with knit_register, a body function, and knit_done.

APIs: knit_register, knit_done, knit_empty

Every command is declared with knit_register <name> <function> <description>, followed by any parameter declarations, and closed with knit_done. The named function is the command’s body. In the @ shorthand shown below, @command takes only <name> <description> — it finds the body from the function defined just beneath the declaration — and @done closes it:

@command "hello" "Print a greeting."
_hello() {
    echo "Hello World"
}
@done

# @empty registers a command with no behavior --- handy for a stub or a
# grouping parent that only carries subcommands.
@command "todo" "Planned command, not implemented yet."
@empty
@done
knit_register "hello" _hello "Print a greeting."
_hello() {
    echo "Hello World"
}
knit_done

# @empty registers a command with no behavior --- handy for a stub or a
# grouping parent that only carries subcommands.
knit_register "todo" knit_empty "Planned command, not implemented yet."
knit_done

Use knit_empty as the body when a command does nothing yet (a stub) or when it exists only to group subcommands under it; the shorthand writes this as an @empty marker line.

Set the program description#

Give the experiment a one-line description shown in top-level –help.

APIs: knit_set_program_description

Call knit_set_program_description once, after sourcing knit.sh, to set the line shown at the top of the experiment’s --help:

knit_set_program_description "Demonstrate command registration and nesting."

Nest subcommands#

Group commands under a parent using colon-nested names.

APIs: knit_register, knit_empty, knit_with_subcommand_title

Colons in a command name nest it under a parent, so say:hello is invoked as say hello. Register the parent with knit_empty (it only groups its children) and, optionally, rename the section they appear under in --help with knit_with_subcommand_title:

# A parent command that only groups subcommands: register it with @empty and
# (optionally) rename the section its children appear under in --help.
@command "say" "Say something."
@empty
@with_subcommand_title "Greetings"
@done

# Subcommands: colons in the name nest them under the parent, so they are invoked
# as "say hello" and "say goodbye".
@command "say:hello" "Say hello."
_say_hello() {
    echo "Hello"
}
@done

@command "say:goodbye" "Say goodbye."
_say_goodbye() {
    echo "Goodbye"
}
@done
# A parent command that only groups subcommands: register it with @empty and
# (optionally) rename the section its children appear under in --help.
knit_register "say" knit_empty "Say something."
knit_with_subcommand_title "Greetings"
knit_done

# Subcommands: colons in the name nest them under the parent, so they are invoked
# as "say hello" and "say goodbye".
knit_register "say:hello" _say_hello "Say hello."
_say_hello() {
    echo "Hello"
}
knit_done

knit_register "say:goodbye" _say_goodbye "Say goodbye."
_say_goodbye() {
    echo "Goodbye"
}
knit_done

Nesting can go deeper (say:hello:loudlysay hello loudly); each parent level is registered the same way.

Add a dispatcher command#

Forward the trailing arguments to a target the command resolves itself.

APIs: knit_with_dispatch, knit_extra_index

A dispatcher takes a target after -- and forwards the remaining arguments to it. Knit’s own setup, submit, and run are dispatchers — each takes a setup, job, or app name after -- and runs it.

knit_with_dispatch marks the command as a dispatcher (which changes its --help usage line to cmd [OPTIONS] -- <target> [OPTIONS] and allows the trailing arguments), and the body reads them with knit_extra_index:

# A dispatcher forwards everything after "--" to a target it looks up itself.
# @with_dispatch changes the --help usage line to "tool [OPTIONS] -- <tool>"
# and allows the trailing arguments; the body reads them with knit_extra_index.
@command "tool" "Run a named tool with trailing arguments."
@with_dispatch "tool" "The tool name and its arguments (after --)."
_run_tool() {
    local args=("$@") extra_index extra
    extra_index=$(knit_extra_index "${args[@]}")
    extra=("${args[@]:extra_index}")
    printf 'running: %s\n' "${extra[*]}"
}
@done
# A dispatcher forwards everything after "--" to a target it looks up itself.
# @with_dispatch changes the --help usage line to "tool [OPTIONS] -- <tool>"
# and allows the trailing arguments; the body reads them with knit_extra_index.
knit_register "tool" _run_tool "Run a named tool with trailing arguments."
knit_with_dispatch "tool" "The tool name and its arguments (after --)."
_run_tool() {
    local args=("$@") extra_index extra
    extra_index=$(knit_extra_index "${args[@]}")
    extra=("${args[@]:extra_index}")
    printf 'running: %s\n' "${extra[*]}"
}
knit_done

knit_extra_index returns the index of the first argument after --, so "${args[@]:extra_index}" is the forwarded target and its arguments.

Hide or gate a command#

Control a command’s visibility in –help and whether it may run.

APIs: knit_hidden, knit_hidden_if, knit_usable_if, knit_usable_before_bootstrap

knit_hidden removes a command from --help while leaving it fully invokable; knit_usable_if blocks a command from running unless a predicate holds, showing its description as the error otherwise:

# Hide a command from --help. It stays fully invokable.
@command "internal" "Internal helper."
@hidden
_internal_cmd() {
    echo "internal"
}
@done

# Refuse to run a command unless a predicate holds. The predicate receives the
# command name and returns 0 (usable) or non-zero (blocked); its description is
# shown as the error when the command is invoked while blocked.
_danger_allowed() {
    [[ "${ALLOW_DANGER:-}" == "1" ]]
}
@command "danger" "Perform a dangerous operation."
@usable_if _danger_allowed "Set ALLOW_DANGER=1 to enable this command."
_danger_cmd() {
    echo "danger performed"
}
@done
# Hide a command from --help. It stays fully invokable.
knit_register "internal" _internal_cmd "Internal helper."
knit_hidden
_internal_cmd() {
    echo "internal"
}
knit_done

# Refuse to run a command unless a predicate holds. The predicate receives the
# command name and returns 0 (usable) or non-zero (blocked); its description is
# shown as the error when the command is invoked while blocked.
_danger_allowed() {
    [[ "${ALLOW_DANGER:-}" == "1" ]]
}
knit_register "danger" _danger_cmd "Perform a dangerous operation."
knit_usable_if _danger_allowed "Set ALLOW_DANGER=1 to enable this command."
_danger_cmd() {
    echo "danger performed"
}
knit_done

Related decorators, all applied between knit_register and knit_done:

  • knit_hidden_if <predicate> — hide from --help only when the predicate returns 0 (dynamic; the command stays invokable). Mutually exclusive with knit_hidden.

  • knit_usable_before_bootstrap — allow the command to run (and appear in --help) on a fresh checkout before bootstrap has created .knit/. Such a command may not declare a table or a --when constraint.

Each predicate is the name of a shell function that receives the command name and returns 0 or non-zero.

Highlight a command in –help#

Bold a command’s name in –help when a predicate holds.

APIs: knit_highlight_if

knit_highlight_if <predicate> bolds a command’s name in its parent’s --help whenever the predicate returns 0 and the output is a terminal. It is purely cosmetic: it never affects whether the command runs, its provenance, or how describe sees it.

# Bold a command's name in --help when a predicate holds and output is a
# terminal. Purely cosmetic: it never affects whether the command runs.
_is_featured() {
    true
}
@command "featured" "A command highlighted in --help."
@highlight_if _is_featured
_featured_cmd() {
    echo "featured"
}
@done
# Bold a command's name in --help when a predicate holds and output is a
# terminal. Purely cosmetic: it never affects whether the command runs.
_is_featured() {
    true
}
knit_register "featured" _featured_cmd "A command highlighted in --help."
knit_highlight_if _is_featured
_featured_cmd() {
    echo "featured"
}
knit_done

The predicate is the name of a shell function that receives the command name and returns 0 (highlight) or non-zero (plain). It is repeatable — the name is highlighted if any registered predicate returns 0.

This is the mechanism that bolds the builtin bootstrap command in the root --help while the experiment has not been bootstrapped yet (and leaves it plain afterwards). The aim is to visually steer the user toward the next relevant command.