Parameters#

Declaring parameters, flags, extras, and reusable parameter sets.

Add a required parameter#

Declare a parameter the caller must supply and read it in the body.

APIs: knit_with_required, knit_get_parameter

knit_with_required <name:type> <description>, placed between knit_register and knit_done, declares a parameter the caller must supply as --name value. The body reads it back with knit_get_parameter:

# A required parameter must be supplied; the body reads it with knit_get_parameter.
@command "greet" "Greet someone."
@with_required "name:string" "Who to greet."
_greet() {
    local name
    name=$(knit_get_parameter "name" "$@")
    echo "Hello ${name}"
}
@done
# A required parameter must be supplied; the body reads it with knit_get_parameter.
knit_register "greet" _greet "Greet someone."
knit_with_required "name:string" "Who to greet."
_greet() {
    local name
    name=$(knit_get_parameter "name" "$@")
    echo "Hello ${name}"
}
knit_done

The type annotation is required — there is no default type, so name alone is rejected and you must write name:string, width:integer, and so on (see the Types category for the full list). knit_get_parameter <name> "$@" prints the value, accepting both --name value and --name=value.

Positional arguments#

Why Knit has no positional parameters, and what to write instead.

APIs: knit_with_required

Knit deliberately has no positional parameters: every value is passed by name (--width 3), never by position (box 3 4). This is a design choice, not a limitation — forcing callers to name every argument keeps experiment scripts explicit and self-documenting, and rules out the classic positional mistakes of swapping two arguments or silently dropping one.

A bare token where a command name would go is treated as a subcommand, so box 3 4 fails with an “Unknown command” error rather than being quietly misread as --width 3 --height 4. Write box --width 3 --height 4 instead: the order no longer matters, and a forgotten argument is reported as a missing required parameter rather than shifting every value by one.

Optional parameters and flags#

Give a parameter a default, or declare a boolean flag.

APIs: knit_with_optional, knit_with_flag, knit_get_parameter

knit_with_optional <name:type> <default> <description> declares a parameter that falls back to a default when omitted. knit_with_flag <name> <description> declares a boolean flag that is either present or not:

# An optional parameter has a default; a flag is either present or not and reads
# back as "true"/"false".
@command "shout" "Greet someone, optionally louder."
@with_optional "name:string" "World" "Who to greet."
@with_flag "excited" "Add an exclamation mark."
_shout() {
    local name excited greeting
    name=$(knit_get_parameter "name" "$@")
    excited=$(knit_get_parameter "excited" "$@")
    greeting="Hello ${name}"
    [[ "${excited}" == "true" ]] && greeting="${greeting}!"
    echo "${greeting}"
}
@done
# An optional parameter has a default; a flag is either present or not and reads
# back as "true"/"false".
knit_register "shout" _shout "Greet someone, optionally louder."
knit_with_optional "name:string" "World" "Who to greet."
knit_with_flag "excited" "Add an exclamation mark."
_shout() {
    local name excited greeting
    name=$(knit_get_parameter "name" "$@")
    excited=$(knit_get_parameter "excited" "$@")
    greeting="Hello ${name}"
    [[ "${excited}" == "true" ]] && greeting="${greeting}!"
    echo "${greeting}"
}
knit_done

As with knit_with_required, the type annotation is mandatory (name:string, count:integer, …). A flag is implicitly boolean, so it takes no type; it reads back through knit_get_parameter as the string true (when the flag was passed) or false (when it was not), so test it with [[ "${flag}" == "true" ]].

Default a parameter from the environment#

Fall back to an environment variable when a parameter is omitted.

APIs: knit_with_optional

Write an optional parameter’s default as ENV[NAME] to fall back to the NAME environment variable when the caller does not pass the parameter (empty when NAME is unset):

# An "ENV[NAME]" default falls back to the NAME environment variable when the
# parameter is not passed (empty if NAME is unset). Resolved when the parameter
# is filled in, so a job picks up a value exported by its setup.
@command "roll" "Print the random seed in use."
@with_optional "seed:integer" "ENV[SEED]" "Random seed."
_roll() {
    echo "seed=$(knit_get_parameter "seed" "$@")"
}
@done
# An "ENV[NAME]" default falls back to the NAME environment variable when the
# parameter is not passed (empty if NAME is unset). Resolved when the parameter
# is filled in, so a job picks up a value exported by its setup.
knit_register "roll" _roll "Print the random seed in use."
knit_with_optional "seed:integer" "ENV[SEED]" "Random seed."
_roll() {
    echo "seed=$(knit_get_parameter "seed" "$@")"
}
knit_done

The fallback is resolved when the parameter is filled in, not at registration, so a job picks up a value exported by its setup’s environment. An explicit --seed on the command line always wins over the environment, so ./exp.sh roll --seed 42 and SEED=42 ./exp.sh roll are equivalent — the flag is just a way to set the same value inline.

Reuse parameters with a parameter set#

Declare a group of parameters once and import it into many commands.

APIs: knit_parameter_set, knit_with_parameter_set, knit_with_required

When several commands share the same parameters, declare them once in a named parameter set and import the set wherever you need it. A set is opened with knit_parameter_set, populated with the usual knit_with_* calls, and closed with knit_done:

# Define a reusable set of parameters once...
@parameter_set "grid"
@with_required "width:integer" "Grid width."
@with_required "height:integer" "Grid height."
@done

# ...then import it into any command with @with_parameter_set.
@command "area" "Compute a grid area."
@with_parameter_set "grid"
_area() {
    local width height
    width=$(knit_get_parameter "width" "$@")
    height=$(knit_get_parameter "height" "$@")
    echo $(( width * height ))
}
@done
# Define a reusable set of parameters once...
knit_parameter_set "grid"
knit_with_required "width:integer" "Grid width."
knit_with_required "height:integer" "Grid height."
knit_done

# ...then import it into any command with @with_parameter_set.
knit_register "area" _area "Compute a grid area."
knit_with_parameter_set "grid"
_area() {
    local width height
    width=$(knit_get_parameter "width" "$@")
    height=$(knit_get_parameter "height" "$@")
    echo $(( width * height ))
}
knit_done

knit_with_parameter_set <name> copies the set’s parameters into the command being registered; call it as many times as you like. A parameter from the set that collides with one already declared on the command is a fatal error.

Import only some parameters of a set#

Import just a few of a parameter set’s parameters with –only.

APIs: knit_with_parameter_set, knit_parameter_set

By default knit_with_parameter_set imports every parameter a set declares. Pass --only with a comma-separated allow-list to import just those parameters and leave the rest behind:

# --only imports just the named parameters of a set (an allow-list); the rest are
# left out.
@command "width-only" "Report just the grid width."
@with_parameter_set "grid" --only "width"
_width_only() {
    echo "$(knit_get_parameter "width" "$@")"
}
@done
# --only imports just the named parameters of a set (an allow-list); the rest are
# left out.
knit_register "width-only" _width_only "Report just the grid width."
knit_with_parameter_set "grid" --only "width"
_width_only() {
    echo "$(knit_get_parameter "width" "$@")"
}
knit_done

Every name in the list must exist in the set, else the call is fatal — this catches a typo that would otherwise silently import the whole set.

Exclude parameters when importing a set#

Import a parameter set minus a few parameters, freeing those names to re-declare.

APIs: knit_with_parameter_set, knit_parameter_set, knit_with_optional

Pass --exclude with a comma-separated deny-list to import every parameter of a set but the named ones. Excluding a name also frees it, so the command can declare it itself with a different kind or default — here a parameter that is required in the set becomes optional on this command:

# --exclude imports every parameter but the named ones, freeing a name so the
# command can re-declare it differently (here "height" becomes optional).
@command "flat-area" "Compute a grid area with an optional height."
@with_parameter_set "grid" --exclude "height"
@with_optional "height:integer" "1" "Grid height (defaults to 1)."
_flat_area() {
    local width height
    width=$(knit_get_parameter "width" "$@")
    height=$(knit_get_parameter "height" "$@")
    echo $(( width * height ))
}
@done
# --exclude imports every parameter but the named ones, freeing a name so the
# command can re-declare it differently (here "height" becomes optional).
knit_register "flat-area" _flat_area "Compute a grid area with an optional height."
knit_with_parameter_set "grid" --exclude "height"
knit_with_optional "height:integer" "1" "Grid height (defaults to 1)."
_flat_area() {
    local width height
    width=$(knit_get_parameter "width" "$@")
    height=$(knit_get_parameter "height" "$@")
    echo $(( width * height ))
}
knit_done

Every excluded name must exist in the set, else the call is fatal. --exclude and --only are mutually exclusive.

Pass opaque trailing arguments#

Accept arbitrary arguments after – and read them in the body.

APIs: knit_with_extra, knit_extra_index

To let a command accept arbitrary arguments after -- (for example to forward them to another program), document them with knit_with_extra and read them in the body starting at knit_extra_index:

# @with_extra documents the arguments accepted after "--"; the body reads them
# starting at knit_extra_index.
@command "forward" "Echo the arguments given after --."
@with_extra "The arguments to echo."
_forward() {
    local args=("$@") extra_index extra
    extra_index=$(knit_extra_index "${args[@]}")
    extra=("${args[@]:extra_index}")
    printf '%s\n' "${extra[*]}"
}
@done
# @with_extra documents the arguments accepted after "--"; the body reads them
# starting at knit_extra_index.
knit_register "forward" _forward "Echo the arguments given after --."
knit_with_extra "The arguments to echo."
_forward() {
    local args=("$@") extra_index extra
    extra_index=$(knit_extra_index "${args[@]}")
    extra=("${args[@]:extra_index}")
    printf '%s\n' "${extra[*]}"
}
knit_done

knit_extra_index returns the index of the first argument after -- (or the argument count when there is no --), so "${args[@]:extra_index}" is exactly the trailing arguments. They are passed through verbatim and are not validated against the command’s declared parameters.

Warning

Use trailing arguments sparingly. Unlike declared parameters, they are not broken out into their own database columns — they are recorded as one opaque blob — so you cannot cleanly filter or group runs by them afterwards. When a value has a known meaning, prefer a named parameter with knit_with_required / knit_with_optional so it lands in its own column.

Validate args in a plain function#

Reject unexpected arguments in a helper that is not a registered command.

APIs: knit_check_arguments, knit_get_parameter

Registered commands validate their arguments automatically, but a plain helper that parses its own "$@" does not. knit_check_arguments <options> <flags> "$@" gives such a helper the same check: the first list names options that take a value, the second names flags, and it returns 1 on the first unexpected argument (logging an error attributed to the caller):

# A plain helper (not registered) can validate its own "$@" with
# knit_check_arguments: the first list names options that take a value, the second
# names flags. It errors on the first unexpected argument and returns 1.
_render() {
    local args=("$@")
    knit_check_arguments "size" "verbose" "${args[@]}" || return 1
    echo "size=$(knit_get_parameter "size" "${args[@]}")"
}
@command "render" "Render at a given size."
@with_optional "size:integer" "8" "Image size."
_render_cmd() {
    _render --size "$(knit_get_parameter "size" "$@")"
}
@done
# A plain helper (not registered) can validate its own "$@" with
# knit_check_arguments: the first list names options that take a value, the second
# names flags. It errors on the first unexpected argument and returns 1.
_render() {
    local args=("$@")
    knit_check_arguments "size" "verbose" "${args[@]}" || return 1
    echo "size=$(knit_get_parameter "size" "${args[@]}")"
}
knit_register "render" _render_cmd "Render at a given size."
knit_with_optional "size:integer" "8" "Image size."
_render_cmd() {
    _render --size "$(knit_get_parameter "size" "$@")"
}
knit_done

Names use hyphens or underscores interchangeably, and everything from a literal -- onwards is treated as extra and left unchecked.