1. 64
  1.  

    1. 15

      Having spent a significant portion of the last decade thinking too much about CLI arguments I have a bunch of thoughts here, perhaps too many for comment.

      (I’ll preface this by saying my comments really only apply when your program accepts more than 1-2 simple arguments, or if the only user is yourself.)

      It’s really cool that Raku builds this in to the handling of main, but I also feel like building a general purpose command line argument parser is such a non-trivial amount of work (deceptively so!) that you either end up lowering the bar for the majority CLI programs in your language, or you end up with arcane syntax and non-trivial runtime performance costs to accommodate the more complex scenarios. The reason only implementing absolute minimum can be an issue is because it leaves the other 80% that people come to expect to be implemented (or not!) in 100 different ways all slightly incompatible with eachother. As a user of a program I personally hate that I have to even think about which language or CLI parser library was used to implement some tool because it informs my usage of that tool. But that’s just the state of things I guess.

      CLI arguments are one of those strange areas of development that seem like such a non-issue at first glance. “Just match a few strings, and boom you’re done!” But there are so many edge cases and unstated expectations! For example how do you (or do you even) handle stacking of short arguments (-abc being equivalent to -a -b -c)? If an argument takes a value (-d <str>); can you do any or, or all of -d val, -d=val, -dval. How does that interact with stacking, can we do -abcd val, or -abcd=val or -abcdval? How do you model mutual exclusion (--foo cannot be used with --bar)? Or do you instead model mutual exclusiveness to that of an override (--foo --bar translates to just --bar)? Or inversely, how do you model requirements (--foo requires use of --bar; although this typically only makes sense if --bar also requires a value such as --bar=baz). What about the simple case of wanting to take a negative number, or a value that starts with - (as many programs do use - to represent STDIN as an input)? For example, say you have an argument which accepts an optional value -e[=str] and another bare argument <str> and the program receives program -e -20; what should happen?

      The list goes on and on. It gets even longer if you start talking about trying to model complex interactions (conditional defaults, exclusions, requirements, or combine this with environment variables, global arguments, grouping of arguments, multiple values, minimums and maximums, etc.). Subcommands definitely complicate the topic, especially if you start needing “global” arguments that should work with all subcommands (think things like --verbose|-v, etc.). Error messages are another huge area. Could your error message help you solve the error? i.e. print an example usage, using the exact command line you already used but with the error corrected? Could it suggest what it thinks you may have meant? .e.g “You used --deltee, did you mean --delete?”

      As luck would have it, it turns out everyone has unstated expectations that all differ slightly from everyone else. I’m almost willing to be that many people here have different opinions about even just the few examples I listed above.

      Additionally, it turns out that designing a good CLI itself is hard much like UX/DX is dedicated topic of study.

      1. 5

        True to Raku’s heritage, there is more than one way to do it!

        [There are multiple points of flexibility](https://docs.raku.org/language/create-cli#Intercepting_CLI_argument_parsing_(2018.10,_v6.d_and_later\)) available for customizing the whole process of argument parsing (and then of course, if you don’t need a reusable approach, writing a normal script to read and handle @*ARGS is always an option)

        1. 2

          (you don’t need to escape that closing paren, CommonMark detects this nesting correctly.)

          [There are multiple points of flexibility](https://docs.raku.org/language/create-cli#Intercepting_CLI_argument_parsing_(2018.10,_v6.d_and_later))
          

          There are multiple points of flexibility

      2. 2

        I usually try to aim for “something that would not surprise people who are familiar with getopt/getopt_long”. Even for Windows utilities.

      3. 1

        Tbh, between Powershell, POSIX, and nushell all having slight differences in “correct” CLI flag, outside of /?,-help, -?,--help and /help, (or just printing usage details on arg parsing failure), I think the thing to aim for with a good CLI is to have your flags documented, ideally with examples, and in sync with your commands. The precise details of a given CLI are unlikely to be consistent with other CLIs at this point.

    2. 21

      There is one problem:

      $ which raku
      /usr/bin/which: no raku in ...
      
      1. 10

        I mean sure, for practical reasons it is “better” to use Ruby or Python or Perl or Awk or Bourne shell, or a compiled language (and not in the namby-pamby, “Python is a Compiled Language” sort of way).

        But Lobsters is a safe space to discuss weird and obscure PLs without prejudice! I don’t think anyone was talking about taking Raku to production here. Let a man enjoy inbuilt argument parsing (and, more broadly, Larry Wall’s decades-long acid trip) in peace!

    3. 3
    4. 2

      Seems the help output could be improved to group sub-command specific flags after the command

      1. 4

        Yeah it’s weird that’s not the default, but you get it if you set my %*SUB-MAIN-OPTS = :named-anywhere;.

    5. 2

      Not a big deal at all. The python sample is straightforward enough.

      For quick scripts I don’t even thing argparse is justified. Just access the cli parameters manually.

      1. 12

        To me argparse is always justified, because I absolutely love having generated help messages. Maintaining them by hand is a pain in the ass because they inevitably drift out of sync, and every time I see Unknown option: --help my soul dies a little inside.

      2. 6

        The time I forgot to add safeguards, and then forgot that I forgot to write safeguards before I used the script the next time, and so I just ran the script with no args, which then nuked 100+GB of data, has made me feel like argument parsing is almost always worth the effort, even with it being an uncomfortable amount of annoying boilerplate in some languages (e.g. bash)

        1. 1

          I have no idea how this is specific to argument parsing and not to any programming task at hand taking user input.

      3. 5

        argparse is great and I use it all the time for my Python scripts. But while it’s pretty easy to understand, I find it hard to write and I constantly have to look up how to do things like subcommands. The raku syntax is a lot easier for me to remember!

      4. 4

        My trick is to have a simple template script that already has argparse with some dummy arguments and a few other basic things I always end up using. I copy/paste the template and modify for my current needs.

        Maybe it’s over kill, but I’ve never regretted the extra functionality, and it makes sharing scripts easier, too.

        1. 1

          This is what I do for Go as well. I even wrote a Cookiecutter clone basically just to use for my CLI template.

      5. 3

        You could access directly from any scripting language, no? I thought Raku shines here as it gives a nice usage message with no effort

        1. 1

          You could access directly from any scripting language, no?

          That’s my point. This is not a big deal.

          1. 2

            But my point is different. The big deal is that Raku does print good usage message, unlike all other languages.

    6. 1

      One thing I’ve not be able to figure out is how to make variadic options, like having --src and --dest both take multiple paths.

      I think the ideal sig for doing --src 1 2, would be :( :$src ($,$) ), but I don’t know if it would be accepted since it would minorly conflict with the current --src= style of flags that raku currently expects. That all being said, someone could make a revised ARGS-TO-CAPTURE that ditches = and adds varags

    7. 1

      Function signature-based CLI approach reminds me of my favorite (at least in concept) Python CLI library, typer. However in practice argparse is usually good enough and adding an external dependency does not always seem worth it for better ergonomics, reflecting some of kbknapp’s arguments above. On top of that, there are a few differences on the front end, like list-valued options where -l a b c -- <positional_arg> is valid with argparse but typer/click require using -l a -l b -l c <positional_arg>.

      1. 1

        I had run across typer before, but at the time I was spending my mental energy learning about fastapi, and I forgot to come back to it. That’s really nice. I hope I remember it next time I want a quick cli gadget. I usually use click, and I always find myself needing to re-learn those decorators. Typer looks like what I wished click would grow into.

        Thanks for mentioning that!

        1. 2

          AFAICT, plac was the original gangsta of all this “infer from the signature” jazz argh was fast on its heels, though. Earlier on (for like a decade), before Guido had blessed the token region in the function signature for type hints in Py3, you could instead stick a string there next to parameters “repurposing” the syntax for per-parameter documentation. This was “easier to remember” enough that the plac author himself stopped using it when it became unavailable.

          In contrast, the Nim cligen also mentioned above, just takes a little help list like dispatch myCmd, help={"param1": "what does param1 do", ..}. While unfortunately lexically remote from its point of declaration (since Nim does not have a prevailing convention of doc comments near decls in parameter lists), this does at least have the charm of not being hard to remember.