|
• The primary win for all of those utilities would be in separating their horribly-large arrays of top-level runtime switches into subcommands that each have a restricted, learnable set of runtime switches relevant only to that subcommand. (See: git, docker, lvm, ip, ufw). • As well, for top-level switches that are really preferences—that is, switches that don't apply to a use-case, but rather to a user—read those from ~/.config/foo/foorc with defaults in /etc/foo/foorc. Don't expose them at all as runtime switches. If there are features that would be customized by different wrapper-client UIs, provide a --config-file=[file] switch that takes a config-file that sets those. And for special drivers, like Makefiles or CI setups, that have a matrix of different UI options they might want to ask for, you can provide FOO_OPTION env-vars. (See: curl, compiler toolchains, ffmpeg, apt). In ffmpeg's case, there are even config-file presets—basically little plug-ins of config options you can enable as a group. You can dump new files into the /etc/ffmpeg/presets or ~/.config/ffmpeg/presets to add them as presets. • For switches that are exposed individually, but which are also aggregated into switch-group-setting switches, in most cases there's literally no use-case for setting the individual switches, only the switch-group-setting switch. Remove the individual switches. Just because the program could theoretically be configured to do X compatibility thing for old-arch Foo, but not Y other old compatibility thing for old-arch Foo (where any time you're talking to Foo you always need both), you don't have to expose both an X switch and a Y switch. Just expose a --foo-compat switch and be done with it. People trying to use your binary on other weird OSes shouldn't be doing so by combining tons of option-switches; they should sit down and port your program to that OS, by finding the place in your program where you've defined {arch -> shim flag set} mappings, adding a new mapping for their arch, and exposing it as a new external --bar-compat switch. • Speaking of compat flags, try just making such behaviors always-on when you build targeting the relevant arch, rather than needing to specify them at runtime. The only real use-case for runtime compat switches is as an IPC/RPC client talking to a server that expects the weird behaviour. And even then, your client should first try to auto-detect the server's expectations, and you should only add the runtime switch if that auto-detection turns out to be unreliable. (Meanwhile, If you're the IPC/RPC server, the compat settings belong in the config file. See: Samba, NFS, Netatalk, ...) • If your binary manipulates state (like, say, how git manipulates repos), and you change the way it does so in a backward-incompatible manner, you might be tempted to add a runtime switch to turn the old behaviour back on to allow collaboration with people using older versions. If your state-store is at all extensible, though, you should instead add a field for a schema-version and a flag for pinning the state-store to that schema-version, as well as a subcommand to pin/unpin a given state-store's schema-version. Now your binary will upgrade the schema of its state-store by default, but will respect "protected" state-stores and perform only backward-compatible operations on those. (Importantly, this guides the code architecture into failing by default instead of doing something backward-incompatible; whereas, with backcompat switches, you're always forcing a state-store schema-migration on people by default every time you add new code, unless/until you add a matching backcompat-switch.) • quiet/verbose and interactive/batch are silly "mode switches" to have in modern programs, just like forking is a silly way to do daemons when you've got a modern init(8). Put interactively-useful information on stdout, and do interactive prompting, if-and-only-if isatty(STDIN); put information of all levels of usefulness on syslog with appropriate error-level tags attached. It's up to the thing running your program to provide a PTY or not, and to filter your output or not. Be friendly to expect(1). These guidelines together should trim each subcommand to a reasonable "visible" option-set. Now just ensure that typing "man command subcommand" gets you a separate man-page written just for the subcommand, and add bash/zsh completion for each subcommand and for the subcommands list itself. |
In principle I agree. In practice, all of the commands you listed are multi-tool commands. For example, git overall can be described as a "content tracker"; only its subcommands can be described as doing one thing, and even then often they do multiple duties (to a newbie, git reset apparently does three different things). In contrast, two of the commands you listed (ps and rsync) do only one thing; list processes and copy files. The third, tar, does technically have multiple modes, but most of the switches apply to multiple modes (-f, compression settings, etc). How would you break these up into subcommands?
> • As well, for top-level switches that are really preferences—that is, switches that don't apply to a use-case, but rather to a user—read those from ~/.config/foo/foorc with defaults in /etc/foo/foorc. Don't expose them at all as runtime switches. If there are features that would be customized by different wrapper-client UIs, provide a --config-file=[file] switch that takes a config-file that sets those. And for special drivers, like Makefiles or CI setups, that have a matrix of different UI options they might want to ask for, you can provide FOO_OPTION env-vars. (See: curl, compiler toolchains, ffmpeg, apt). In ffmpeg's case, there are even config-file presets—basically little plug-ins of config options you can enable as a group. You can dump new files into the /etc/ffmpeg/presets or ~/.config/ffmpeg/presets to add them as presets.
Some would argue that this is an anti-pattern and that such configuration should reside in your shell as an alias or function. Regardless, with the possible exception of ps, for the commands you listed, which options exactly would you move into a configuration file? It would likely not be a good idea, for example, to say that all tar -c commands should imply -J, since that would break lots of scripts.
> • For options that are aggregated into option-set flags, where there's literally no use for the options outside of their use in the option-set flag, remove the individual options. Just because the program could theoretically be configured to do X compatibility thing for old-arch Foo, but not Y other old compatibility thing for old-arch Foo, you don't have to support both an X and Y option. Just expose a --foo-compat option and be done with it. People trying to use your binary on other weird OSes shouldn't be doing so by combining tons of option-switches; they should sit down and port your program to that OS, by finding the place in your program where you've defined {arch -> shim flag set} mappings, adding a new mapping for their arch, and exposing it as a new external --bar-compat switch.
I don't understand what this means. Can you give a concrete example?
> • Speaking of compat flags, try just making such behaviors always-on when you build targeting the relevant arch, rather than needing to specify them at runtime. The only real use-case for runtime compat switches is as an IPC/RPC client talking to a server that expects the weird behaviour. And even then, your client should first try to auto-detect the server's expectations, and you should only add the runtime switch if that auto-detection turns out to be unreliable. (Meanwhile, If you're the IPC/RPC server, the compat settings belong in the config file. See: Samba, NFS, Netatalk, ...)
Again, I don't understand what this means.
> Those four things together should trim each subcommand to a reasonable "visible" option-set. Now just ensure that typing "man command subcommand" gets you a separate man-page written just for the subcommand, and add bash/zsh completion for each subcommand and for the subcommands list itself.
I agree with this conclusion given the premises you provided, but the issue is that the premises themselves appear to be incorrect.
Broadly speaking, most complaints about the commands you listed originate in poor understanding of their interfaces; everybody thinks they're hard, so nobody ever reads the man page and learns how to use them correctly. For example, complaints about not knowing tar -z from -j are mostly a result of not knowing that GNU and at least one BSD tar know how to automatically select compression format in at least some cases. A similar situation is the cause of "ps -aux" and such monstrosities as "rsync -arlv -e ssh dir host:dir".