Skip to content

The directory is the dispatch table

 3 min vps-lxc discovered its subcommands by listing a directory, with one sed filter as the entire registration policy.
Published

Feb 11, 2010

vps-lxc, my container manager, never declared its subcommand list anywhere. It computed it:

# COMMAND list
VPS_COMMAND_LIST="$(ls -1 ${VPS_USR}/lib/${VPS_NAME}/cmd | sed -e "/[^a-z]/d")"

One ls, one sed. Every directory entry in cmd/ whose name is pure lowercase letters is a candidate subcommand; ls does not distinguish a regular file from a directory or symlink, and the sed checks only the name. command.proto—the documented prototype sitting in the same directory—is excluded by the dot in its name. Drop a lowercase-named readable file in the directory and it enters the command surface; give an entry a dot, an underscore, or a capital and it is inert. The repository ships both halves of that: command.proto as the annotated template, and a live all-lowercase copy at cmd/proto, so the demo is itself a discovered, runnable command.

Dispatch tried to validate against the same listing before anything else happened:

VPS_COMMAND="$(echo "${VPS_COMMAND_LIST}" | tr " " "\n" | grep "^${1}\$")"

That is not exact literal membership. The command name is interpolated as a basic regular expression, so metacharacters can select another discovered name—pr.to, for example, matches proto. A literal whole-line check would have been grep -Fx -- "$1". The ordering still matters: an empty result dies before escalation, but the regex admitted more than the discovered command names.

Ordering detail worth stealing: the sudo re-exec (exec ${VPS_SUDO} ${0} "$@") sits after this check, so privileges escalate only after the directory-derived gate accepts the input.

The chosen path is then sourced—the first point that tests whether it is readable shell input; an executable bit never participates. A successful source must define a fixed lifecycle: vps_cmd_init, option handling, vps_cmd_param, vps_cmd_pre_main, vps_cmd_main, vps_cmd_post_main. The split of responsibilities is the part that keeps plugins small: the dispatcher runs getopts itself, but with the option string the command declared, handing each parsed option back to the command’s handler:

# send each opt to the COMMAND handler
while getopts "${VPS_COMMAND_OPTSTR}" OPT; do
    vps_cmd_opt "${OPT}" "${OPTIND}" "${OPTARG}"
done

A command that declines to declare an option string dies with “does not implement an OPTSTRING”. Parsing mechanics live once, in the framework; meaning lives in the plugin.

A later Bash completion rework reused the same directory-and-filter expression, keeping completion from maintaining a second command list:

COMPREPLY=( $(compgen -W "help $(ls -1 ${VPS_USR}/lib/${VPS_NAME}/cmd | sed -e "/[^a-z]/d")" -- ${cur}) )

The gotcha is that the filter is also the constraint, in both directions. Command names can only be lowercase letters—no make-template, no ls2—and any lowercase-named entry that lands in that directory becomes a candidate. The registry that requires no registration also has no type gate or review step; write access to the directory or any selected target is code-execution authority.