Last week, a blog post Hints for writing Unix tools by Marius Eriksen made the rounds. It presented nine suggestions on what makes a command a good citizen of the Unix command-line ecosystem, especially for fitting into pipelines and filters.
This reminded me of a longer list of guidelines I recently gathered as part of our efforts to train new hires in Solaris engineering. I polled long time engineers, trawled the Best Practices documents of our Architecture Review Committee, cross referenced to the WCAG2ICT accessibility guidelines for non-web applications recommended by Oracle’s accessibility group, and linked to our online documentation, to come up with our suggestions on writing new CLI tools for Solaris.
Since these may be useful to others writing commands, I figured I’d share some of them. I’ve left out the bits specific to complying with our internal policies or using private interfaces that aren’t documented for external use, but many of these are generally applicable. Do note that these are based in part on lessons learned from 40+ years of Unix history, and that history means that many existing commands do not follow these suggestions, and in some cases, can’t follow them without breaking backwards compatibility, so please don’t start calling tech support to complain about every case our old code isn’t doing one of these things.
One of the key points of our best practices is that many commands belong to part of a larger family, and it’s best to fit in with that existing family. If you’re writing a Solaris-specific command, it should follow the Solaris Command Line Interface Paradigm guidelines (as listed in the Solaris Intro(1) man page), but GNU commands should instead follow the GNU Coding Standards, classic X11 commands should use the options described in the OPTIONS section of the X(7) man page, and so on.
Command names & paths
- Most new commands should have names 3-9 letters long. Command names longer than 9 letters should be commands users rarely have to type in.
- Follow common naming patterns, such as:
||Command to change current state & administer a subsystem
||Command to make permanent configuration changes to a subsystem
||Command to print information about objects managed by a subsystem
||Command to print properties of objects managed by a subsystem
||Command to print/monitor statistics on a subsystem
- Commands run by normal users should be delivered in /usr/bin/. Commands normally only run by sysadmins should be delivered in /usr/sbin/. Commands only run by other programs, not humans, should be in an appropriate subdirectory under /usr/lib/. (Commands not delivered with the OS should instead use the appropriate subdirectory under /opt instead of /usr in the above paths.)
- Never provide an option to take a password or other sensitive data on the command line or environment variables, as ps and the proc tools can show those to other users. (see Passing secrets to subprocesses).
- All commands should have a --help and -? option to print recognized options/arguments/subcommands.
- Option parsing should use one of the standard getopt() routines if at all possible. If you don’t use one, your custom parser will need to replicate a lot of things the standard routines provide for error checking & handling.
- When reporting errors, be specific about which argument/option failed, don’t just dump usage output and make the user guess which part of the command line was wrong. (See WCAG2ICT #3.3.1. Examples of fixing this in X11 programs: bitmap, fslsfonts, mkfontscale, xgamma, xpr, xsetroot.)
- If possible, provide suggestions to correct - if option is invalid, list options that would be valid. Same for subcommands, arguments, etc. (See WCAG2ICT #3.3.3.)
- Option flags should be similar across commands when possible.
If you are writing a command that uses subcommands, then being careful in your work can make your command much easier to use.
Good examples to follow: hg, zfs, dladm
- The help subcommand should list the other subcommands, but not overwhelm the user with pages of details on all of them. (Remember, the Solaris kernel text console has no scrollback and users with text-to-speech don’t want 10 minutes of output from it.)
Good examples: hg, svccfg
- The help foo or foo --help subcommands should list the options specific to that subcommand.
Good examples: hg
- Look at existing commands with similar subcommands and use similar names for your subcommands
- All functionality should be available when TERM=dumb. Use of color output, bold text, terminal positioning codes, etc. can be used to enhance output on more capable terminals, but users need to be able to use the system without it. Users may need to run different commands to get plain text interface instead of curses/terminal mode, such as ed instead of vi, or mailx vs. mutt, as long as it’s clearly documented what they need to run instead, but they must be able to get their work done in some way. (See WCAG2ICT #1.3.2, WCAG2ICT #1.4.1, & WCAG2ICT #1.4.3)
- Text output is generally composed of messages and data. Messages are the text included in the program, such as status descriptions, error messages, and output headers; while data comes from the subsystem the command interacts with, and depends on the system in question.
- Messages displayed to users should use gettext(3C) to allow translation & localization.
- Errors should be printed to stderr, other output to stdout, unless specific output redirection options (such as logging errors to a file) are given.
- Users should be able to disable any use of ASCII art, line drawing characters, figlet-style text and any other output other than plain text which a text-to-speech screen reader cannot figure out how to read, while not losing information, only formatting of it. (See WCAG2ICT #1.1.1 & WCAG2ICT #1.4.5)
- Error messages should include the program name to help track down which program produced an error in a shell script, SMF method, etc. This is automatically done if you use the standard libc functions err, verr, errx, verrx, warn, vwarn, warnx, vwarnx (3c).
- Parsable output should follow the design outlined in Creating Shell-Friendly Parsable Output:
- Parsable output should require the user to specify the fields to output, via a -o or similar flag, so that new fields can be added to the command without breaking existing parsers.
- Headers should be omitted in parsable output mode or when a flag such as -H is specified to omit them.
- Parsable output should use a non-whitespace delimiter, such as “:” between fields.
Privileges on Solaris
- If you offer an interactive command prompt mode, such as svccfg does for executing subcommands, consider using libtecla or similar support for command line editing in this mode.
- Any operation that may permanently alter or destroy data should either have an “undo” option (such as rollback to prior snapshot) or have a mode offering the user a chance to confirm (such as the -i option to rm). (See WCAG2ICT #3.3.4)
- Users should be able to configure timeout lengths for any operation that expects user interaction before a timeout expires. (See WCAG2ICT #2.2.1)
- Commands are expected to return 0 on success, 1 upon a fatal error, and 2 when invoked incorrectly (usage error). Standard C also provides EXIT_SUCCESS and EXIT_FAILURE in stdio.h for the first two cases. If other exit status values can be returned, document them in your man page.
- All software should be 64-bit clean now. If your command will only be run on Solaris 11 or later, consider making it 64-bit-only and skip providing a 32-bit version if you don’t need one.
- The Solaris libc library has picked up a number of routines over the years to make it easier to implement common command functionality:
- Command line option handling: getopt(3c), getopt_clip, getopt_long, getopt_long_only (3c), getsubopt(3c)
- Error reporting: err, verr, errx, verrx, warn, vwarn, warnx, vwarnx (3c)
- Input reading: getline, getdelim(3C)
- Internationalization: i18n Programming Interfaces in the libc Library