Fang, the CLI Starter Kit

147 points by bewuethr 3 weeks ago | 32 comments
  • csomar 3 weeks ago
    I’ve been maintaining rust-starter[!] for quite sometime now. It is kind of the equivalent of fang but for Rust. It uses Clap which is the equivalent of Cobra; though I don’t think Clap has the same kind of fancy output?

    Throughout these years, I have found that cross-compiling is the most challenging part of creating a CLI. When you are building a web back-end, you control the execution environment (usually linux). For CLIs, your users could be on Linux, macOS or Windows. You need to get three x2 binaries (so a total of 6) to have a fair coverage.

    I’ve tried cross, but for Windows and macOS you need licenses. There is no straightforward way to give your users Docker images and have them running in a few commands. You can compile on GitHub action machines, but that’s a very slow feedback loop. I wonder if things are better in Go land.

    !: https://github.com/rust-starter/rust-starter

    • neomantra 3 weeks ago
      Golang itself bundles a toolchain and can cross compile to a many target OSes and architectures. I use Goreleaser [1] to create GitHub releases, Homebrew packages, Docker images, and Linux packages. Goreleaser Pro can also create MSI packages.

      ETA since I just saw Christian chime in: the Goreleaser author works at Charm.sh =)

      [1] https://goreleaser.com

      • christianrocha 3 weeks ago
        GoReleaser is indeed an awesome tool and we use it for every applicable project.

        And actually Carlos, who builds GoReleaser, is the true author of Fang and took it from concept to execution.

        • 3 weeks ago
        • christianrocha 3 weeks ago
          I feel your pain. That said, cross compiling from Go is pretty trivial, as long as everything is pure Go, which it most often is. That’s one if the reasons we invested in jt.

          (Hello from Charm! I’m one of the authors of this library.)

          • threemux 3 weeks ago
            Wow TIL cross compilation is a bit of a pain in Rust. I assumed it was as easy as Go. I can confirm as long as you're using pure Go (no cgo), it's as easy as setting $GOOS and $GOARCH appropriately.
            • paulddraper 3 weeks ago
              The difficulty is due to Rust dynamically linking and Go statically linking. The former requires the system libs.

              That said, I am unfamiliar with any licensing for Windows builds.

              MacOS has some niche (usually discarded) technicalities like you can only use the SDK on Apple hardware.

              • threemux 3 weeks ago
                I think that's a reference to the use of Docker in the "cross" tool that makes cross compilation easier but I'm not certain
            • rmac 2 weeks ago
              I looked into how rust does does it for rustup, and they have a pretty amazing set of gh actions to build for their architectures, I can't find the GH link now tho

              for speed you can look for vendors like https://depot.dev/

              • mootoday 3 weeks ago
                This is great, lots I learned by looking at your code and the dependencies you use!

                I started a similar thing, although not as feature-rich as yours. My goal was to follow CLI best practices and add all the boilerplate one needs to build a Rust CLI.

                https://github.com/mootoday/cli-template

                • masfuerte 2 weeks ago
                  I configure github actions to build each version in its natural habitat.

                  Though my motivation for this was getting the builds off my machine. I didn't want to be shipping binaries I had built locally.

                • johnisgood 3 weeks ago
                  I just ran across "gum"[1] from charmbracelet which I intend to use! I just want to replace dialog. I came across whiptail, too, but gum seems nicer.

                  They have a TUI framework, too, among a lot of other related things. Some of their projects are Go libraries, some are a CLI tool, such as gum.

                  [1] https://github.com/charmbracelet/gum

                  • nodesocket 3 weeks ago
                    Gum looks awesome. Gonna keep it in-mind next time I need to write a shell CLI.
                  • bbkane 3 weeks ago
                    It would be really cool if Fang could generate a TUI form for your app with https://github.com/charmbracelet/huh (by the same org). Is something like that on the roadmap?

                    Similar work: https://github.com/chriskiehl/Gooey and https://github.com/Sorcerio/Argparse-Interface

                    I've wanted to do TUI form generation for my own CLI framework since 2023 ( https://github.com/bbkane/warg/issues/71 ), but I still haven't gotten around to it

                    • jmsdnns 3 weeks ago
                      This is awesome! Love the work the charm bracelet team is doing.
                      • christianrocha 3 weeks ago
                        Thank you for the very kind words. Hello from Charm!
                      • jonpalmisc 3 weeks ago
                        Stubborn complaint (and maybe a hot take): I dislike CLIs that try to be overly pretty. I don't receive any tangible benefit as the user from the "fancy" (their words) help output. I'd much rather simple plain text output that looks like all the other tools I already use.
                        • arp242 3 weeks ago
                          Alignment (and maybe bold text for some things) is all you need in >95% of cases, IMHO.

                          One of the downsides of a lot of these tools is that's exactly what they don't do well: many things don't align or wrap nicely.

                          For example, here's a comparison of this fang library vs one where I just raw-dogged the usage text: https://imgur.com/a/QWh9TLD – the nice alignment does a lot more than colours. Especially for larger programs with a bunch of flags it can be such a pain. For example from an otherwise quite nice tool: https://imgur.com/a/RELL9Gk – you just completely lose any "overview".

                          I did spend some time on some better tooling to improve all of this, because manually writing these isn't super-fun either, but not so straight-forward to do well (or at least: not in a way that I like).

                          • quotemstr 3 weeks ago
                            > One of the downsides of a lot of these tools is that's exactly what they don't do well: many things don't align or wrap nicely.

                            Bling is easy. Unsexy usability details are hard.

                                Z$ time ./example/run
                                You ran the root command. Now try --help.
                                ./example/run  0.13s user 0.27s system 177% cpu 0.228 total
                            
                            Why would an example program take 228ms?

                                Z$ ./example/run --name='abc def'
                                
                                  Unknown command "def" for "example".                                                            
                            
                                  Try --help for usage.
                            
                            
                            Huh? 'abc def' is one shell word. --name=abc works fine.

                                 Z$ ./example/run --name ''
                                      
                                   ERROR  
                                     
                                   Flag needs an argument: --name.                                                                 
                            
                                   Try --help for usage.
                            
                            But I did give it an argument: the empty string.

                            And why is the output indented two columns from the left margin anyway?

                                Z$ ./example/run ''
                                You ran the root command. Now try --help.
                            
                                Z$ ./example/run 'x'
                                      
                                  ERROR  
                                      
                                  Unknown command "x" for "example".                                                              
                            
                                  Try --help for usage.
                            
                            Should have produced an error using '' for the subcommand name.

                                Z$ ./example/run sub "multi-word quoted string" --flag "another quoted string"
                                Ran the sub command!
                            
                                Z$ ./example/run --help
                            
                                  A little example program!                                                                         
                                                                                                                                    
                                  It doesn’t really do anything, but that’s the point.™                                             
                            
                                  USAGE  
                            
                            
                                    example [command] [args] [--flags]                                     
                            
                            
                                  EXAMPLES  
                            
                            
                                    # Run it:                                                              
                                    example                                                                
                                                                                                           
                                    # Run it with some arguments:                                          
                                    example --name=Carlos -a -s Becker -a                                  
                                                                                                           
                                    # Run a subcommand with an argument:                                   
                                    example sub --async --foo=xyz --async arguments                        
                                                                                                           
                                    # Run with a quoted string:                                            
                                    example sub "quoted string"                                            
                                                                                                           
                                    # Mix and match:                                                       
                                    example sub "multi-word quoted string" --flag "another quoted string"  
                            
                            
                                  COMMANDS  
                            
                                    help [command]                  Help about any command
                                    sub [command] [args] [--flags]  An example subcommand
                                    throw                           Throws an error
                            
                                  FLAGS  
                            
                                     -a --async                     Run async
                                     -h --help                      Help for example
                                     --name                         Your name (jane)
                                     -s --surname                   Your surname (doe)
                                     -v --version                   Version for example
                            
                            
                                Z$ ./example/run sub "multi-word quoted string" --flag "another quoted string"
                                zsh: command not found: example sub multi-word quoted string --flag another quoted string
                            
                            Huh? Why did the command work when I typed it myself but not when I pasted from the help output?

                            Oh. Because the help output is using nbsp, not regular spaces.

                            Anyway, command line interfaces have a surprising number of hairy corner cases. I'd rather have boring monochrome that gets them right than an all-colorful theme auto-shell-completion-generating system that doesn't.

                        • anitil 3 weeks ago
                          I like colour in a tool like 'bat' or 'jless' where it helps me visually scan, but other than that I'd agree with you
                          • joshka 2 weeks ago
                            As a counter argument, not putting color in help usage text leaves a large amount on the table for readability. The reasonable compromise for those that disagree with this is to set the NO_COLOR environment variable. This should be respected by most things which do use color (and if it's not, that's a bug).
                            • quotemstr 3 weeks ago
                              > overly pretty

                              "bling" is the word you're looking for. It's just a fashion. Fashion is famously cyclic, and we're just in a high-ornamentation part of the cycle. Eventually, restraint will become fashionable again.

                              • guappa 2 weeks ago
                                And they often don't check the terminal so if you pipe them or run on a serial terminal they break horribly.
                                • assbuttbuttass 3 weeks ago
                                  agreed, plain text is more scriptable too. Let me pipe it into awk!
                                  • caarlos0 3 weeks ago
                                    If you do it right, you can output plain text when stdout is not a tty - which is something fang does, fwiw :)
                                    • arp242 3 weeks ago
                                      It still tries to put the terminal in raw mode or something. "cmd | less" doesn't work and requires "stty sane". I didn't investigate, but FYI.
                                    • kitd 2 weeks ago
                                      A lot of the fancy CLIs I use have a `--json` option that gives the user the chance to pipe output to eg jq and process it there. I find that a good alternative to running stuff through cut, sed or awk before processing.
                                  • aranw 2 weeks ago
                                    It's probably more accurate to say it's a Go Cobra CLI Starter Kit rather just a generic CLI Starter kit. I personally find Cobra bit OTT and bloated and try to avoid it where I can but this could be a nice starter point for my own similar library to suit my needs
                                    • RSHEPP 3 weeks ago
                                      Just re-wrote an internal CLI to urfave from custom, but having issues with v3 autocomplete scripts. Might just take the time to switch over to cobra and use this.
                                      • syvolt 3 weeks ago
                                        I think kong[1] is better than both options although cobra is by far the most popular. Might be worth a look.

                                        Definitely won't look as pretty as the parent project though but I prefer kong in terms of the actual code you need to write.

                                        [1] https://github.com/alecthomas/kong

                                    • Cthulhu_ 2 weeks ago
                                      That's pretty neat, although it's little more than a coloring wrapper around Cobra. That said, I wrote a small tool for a hackathon and just dropped this in for a bit of whimsy.