Named arguments in CLI application (original) (raw)
I’m writing a command line application, and have been reading up on best practices. In particular, I’ve come across these two recommendations, which are giving me some trouble:
- “If you’ve got two or more arguments for different things, you’re probably doing something wrong.”[1]
- “abusing flags to implement named arguments is the wrong thing to do unless all of the arguments are optional.”[2]
My application flashes firmware to embedded devices over a serial connection. It necessarily requires three parameters, which cannot be guessed and have no sane defaults:
- A path to a firmware image
- A serial port connected to the target device
- The baud rate of the serial interface
Currently, the application takes the firmware image path as an argument, while the port and baud rate are set via mandatory flags. For example:
$ myapp --port=/dev/ttyUSB0 --baudrate=115200 firmware.hex
This goes against recommendation #2 above. Changing it so that all three are positional arguments instead goes against recommendation #1.
I would like to take all three as named arguments, i.e. the interface would look like this:
$ myapp port=/dev/ttyUSB0 baudrate=115200 image=firmware.hex
However, neither argparse
nor click
support this, as far as I can tell. The closes thing is setting the parameters via environment variables, which click
supports:
PORT=/dev/ttyUSB0 BAUDRATE=115200 myapp firmware.hex
But I can’t say that I like this. Requiring mandatory ENV vars seems at least as bad as mandatory flags.
Questions:
- Is there a way to do this with either
argparse
orclick
that I’ve overlooked? - Is there another CLI library that supports this?
- Should I ignore one or both of the above recommendations?
Basically, how would you implement this interface?
Rosuav (Chris Angelico) June 10, 2024, 9:17am 2
One possible UI would be to combine the port and baud rate into a single argument, and have two mandatory arguments whose order doesn’t matter:
$ myapp /dev/ttyUSB0:115200 firmware.hex
$ myapp firmware.hex /dev/ttyUSB0:115200
It’s reminiscent of how you might combine an IP address and port together (eg openssl s_client -connect 127.0.0.1:443
), so it would make some sense written like this. It’s technically ambiguous (your firmware COULD be stored in a file whose name ends with a colon and some digits, but that seems unlikely), but more convenient than forcing the extra keyword arguments.
JamesParrott (James Parrott) June 10, 2024, 9:57am 3
Can you really not pick a default baud rate? Normally there’s only a handful of possible values.
Anyway, I don’t know where they came from, but those recommendations are more like guidelines really. Only the hobgoblins see them as rules.
I’d ignore recommendation 2 and use your first example with argparse.
It’s all a question of taste of course, but I don’t find any of the decorator-heavy frameworks add value, personally.
abessman (Alexander Bessman) June 10, 2024, 10:38am 4
Is mandatory arguments with arbitrary order something that either argparse
or click
supports? I don’t think so.
I guess I could use a single variadic argument with nargs=2
and parse them myself, but that circumvents some nice features of the parsing libraries, e.g. I couldn’t use click.Path
to ensure that the firmware image is a file that exists. Yes, I can easily do that myself, but it’s nice to avoid the extra complexity in my own code if I can rely on a library to do it for me.
Well, it’s probably one of (460800, 230400, 115200, 57600, 38400, 19200, 9600)
. It could be something more exotic, but even if it’s one of these I have no way of knowing which, short of trying each one and seeing if I get a response.
This being my first foray into the realm of CLI design, I tend to err on the side of foolish consistency
Rosuav (Chris Angelico) June 10, 2024, 10:50am 5
Hmm, actually, come to think of it, I’m not sure I’ve done that with argparse/click. What you could do is simply define two different versions and try one, except try the other, which isn’t too bad with two options but would be a pain with more.
I’ve normally done this by bypassing those modules and directly pulling from sys.argv, but if you have other flags you want to process, it’d be easier to use the library.
JamesParrott (James Parrott) June 10, 2024, 10:58am 6
Is mandatory arguments with arbitrary order something that either
argparse
orclick
supports? I don’t think so.
Argparse doesn’t. Args are either positional, i.e. ordered, or optional (denoted by a -
flag or --
name). I don’t know click, but at the end of the day both libraries are just processing sys.argv
on your behalf. The main advantages are maintainability, and the automatic help documentation for the user.
Rolling your own based on the types of the args, e.g. assuming one that’s all digits is a baud rate, is straightforward (with a regex or try int() except), if you wish to support arbitrarily ordered args. With argparse you can do a partial parse to filter out optional args, and do what you like with the remaining list of strings, whether the application interprets them traditionally as positional args, feeds them into a sub parser, or treats them some other way.