warg

package module
v0.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 4, 2022 License: MIT Imports: 10 Imported by: 0

README

warg

Build heirarchical CLI applications with warg!

  • warg uses funcopt style APIs to keep CLI declaration readable and terse. It does not require code generation. Nested CLI command are indented, which makes apps easy to debug.
  • warg is extremely interested in getting information into your app. Ensure a flag can be set from an environmental variable, configuration file, or default value by adding a single line to the flag (configuration files also take some app-level config).
  • warg is customizable. Add new types of flag values, config file formats, or --help outputs using the public API.
  • warg is easy to add to, maintain, and remove from your project (if necessary). This follows mostly from warg being terse and declarative. If you decide to remove warg, simply remove the app declaration and turn the passed flags into other types of function arguments for your command handlers. Done!

Hello World

Also see the examples in the docs.

Code

package main

import (
	"fmt"
	"os"

	"github.com/bbkane/warg"
	"github.com/bbkane/warg/command"
	"github.com/bbkane/warg/flag"
	"github.com/bbkane/warg/section"
	"github.com/bbkane/warg/value"
)

func hello(pf flag.PassedFlags) error {
	// this is a required flag, so we know it exists
	name := pf["--name"].(string)
	fmt.Printf("Hello %s!\n", name)
	return nil
}

func main() {
	app := warg.New(
		"say",
		section.New(
			"Make the terminal say things!!",
			section.WithCommand(
				"hello",
				"Say hello",
				hello,
				command.WithFlag(
					"--name",
					"Person we're talking to",
					value.String,
					flag.Alias("-n"),
					flag.EnvVars("SAY_NAME"),
					flag.Required(),
				),
			),
		),
	)
	app.MustRun(os.Args, os.LookupEnv)
}

Run

By default, these help messages are in color. You'll have to imagine that within this README :)

$ ./say -h
Make the terminal say things!!

Commands

  hello : Say hello

The default help for a command dynamically includes each flag's current value and how it was was set (passed flag, config, envvar, app default).

$ ./say hello --name World -h
Say hello

Command Flags:

  --name , -n : Person we're talking to
    type : string
    envvars : [SAY_NAME]
    required : true
    value (set by passedflag) : World

Inherited Section Flags:

  --help , -h : Print help
    type : stringenum with choices: [default]
    default : default
    value (set by appdefault) : default
$ SAY_NAME=Bob ./say hello -h
Say hello

Command Flags:

  --name , -n : Person we're talking to
    type : string
    envvars : [SAY_NAME]
    required : true
    value (set by envvar) : Bob

Inherited Section Flags:

  --help , -h : Print help
    type : stringenum with choices: [default]
    default : default
    value (set by appdefault) : default
$ ./say hello --name World
Hello World!

Should You Use warg?

I'm using warg for my personal projects, but the API is not finalized and there are some known issues (see below). I will eventually improve warg, but I'm currently ( 2021-11-19 ) taking a break from developing on warg to develop some CLIs with warg.

Known Issues

  • warg does not warn you if a child section/command has a flag with the same name as a parent. The child flag essentially overwrites the parent flag. I'd like to check this at test time.
  • lists containing aggregate values ( values in list objects from configs ) should be checked to have the same size and source but that must currently be done by the application ( see grabbit )
  • Many more types of values need to implemented. Especially StringEnumSlice, StringMap and Duration

Alternatives

  • cobra is by far the most popular CLI framework for Go. It relies on codegen.
  • cli is also very popular.
  • I've used the now unmaintained kingpin fairly successfully.

Concepts

Sections, Commands, and Flags

warg is designed to create heirarchical CLI applications similar to azure-cli (just to be clear, azure-cli is not built with warg, but it was my inspiration for warg). These apps use sections to group subcommands, and pass information via flags, not positional arguments. A few examples:

azure-cli
az keyvault certificate show --name <name> --vault-name <vault-name>

If we try to dissect the parts of this command, we see that it:

  • Starts with the app name (az).
  • Narrows down intent with a section (keyvault). Sections are usually nouns and function similarly to a directory heirarchy on a computer - used to group related sections and commands so they're easy to find and use together.
  • Narrows down intent further with another section (certificate).
  • Ends with a command (show). Commands are usually verbs and specify a single action to take within that section.
  • Passes information to the command with flags (--name, --vault-name).

This structure is both readable and scalable. az makes hundreds of commands browsable with this strategy!

grabbit

grabbit is a much smaller app to download wallpapers from Reddit that IS built with warg. It still benefits from the sections/commands/flags structure. Let's organize some of grabbit's components into a tree diagram:

grabbit                   # app name
├── --color               # section flag
├── --config-path         # section flag
├── --help                # section flag
├── config                # section
│   └── edit              # command
│       └── --editor      # command flag
├── grab                  # command
│   └── --subreddit-name  # command flag
└── version               # command

Similar to az, grabbit organizes its capabilities with sections, commands and flags. Sections are used to group commands. Flags defined in a "parent" section are available to child commands. for example, the config edit command has access to the parent --config-path flag, as does the grab command.

Special Flags

TODO

--config

--help + --color

Unsupported CLI Patterns

One of warg's tradeoffs is that it insists on only using sections, commands and flags. This means it is not possible (by design) to build some styles of CLI apps. warg does not support positional arguments. Instead, use a required flag: git clone <url> is spelled git clone --url <url>.

All warg apps must have at least one nested command. It is not possible to design a warg app such that calling <appname> --flag <value> does useful work. Instead, <appname> <command> --flag <value> must be used.

TODO

  • use https://stackoverflow.com/a/16946478/2958070 for better number handling?
  • put the : back at the end of headers in teh default help functions.
  • add a bool value
  • add screenshots for --help - colors look way better
  • zsh completion with https://www.dolthub.com/blog/2021-11-15-zsh-completions-with-subcommands/
  • Should I make commands not return an error? Maybe that should be handled by the app author?
  • Ensure a flag created with flag.New can be used in multiple places! Probably with lots of tests...
  • make help less verbose...
  • make an app.Test() method folks can add to their apps - should test for unique flag names between parent and child sections/commands for one thing
  • go through TODOs in code
  • --help ideas: man, json, web, form, term, lsp, bash-completion, zsh-completion, outline, compact

Documentation

Overview

Declaratively create heirarchical command line apps.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

An App contains your defined sections, commands, and flags Create a new App with New()

func New

func New(name string, rootSection s.SectionT, opts ...AppOpt) App

New builds a new App!

Example
package main

import (
	"fmt"
	"os"

	"github.com/bbkane/warg"
	"github.com/bbkane/warg/command"
	"github.com/bbkane/warg/flag"
	"github.com/bbkane/warg/section"
	"github.com/bbkane/warg/value"
)

func login(pf flag.PassedFlags) error {
	url := pf["--url"].(string)

	// timeout doesn't have a default value,
	// so we can't rely on it being passed.
	timeout, exists := pf["--timeout"]
	if exists {
		timeout := timeout.(int)
		fmt.Printf("Logging into %s with timeout %d\n", url, timeout)
		return nil
	}

	fmt.Printf("Logging into %s\n", url)
	return nil
}

func main() {
	app := warg.New(
		"blog",
		section.New(
			"work with a fictional blog platform",
			section.Command(
				"login",
				"Login to the platform",
				login,
			),
			section.Flag(
				"--timeout",
				"Optional timeout. Defaults to no timeout",
				value.Int,
			),
			section.Flag(
				"--url",
				"URL of the blog",
				value.String,
				flag.Default("https://www.myblog.com"),
				flag.EnvVars("BLOG_URL"),
			),
			section.Section(
				"comments",
				"Deal with comments",
				section.Command(
					"list",
					"List all comments",
					// still prototyping how we want this
					// command to look,
					// so use a provided stub action
					command.DoNothing,
				),
			),
		),
	)

	// normally we would rely on the user to set the environment variable,
	// bu this is an example
	err := os.Setenv("BLOG_URL", "https://envvar.com")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	app.MustRun([]string{"blog.exe", "login"}, os.LookupEnv)
}
Output:
Logging into https://envvar.com

func (*App) MustRun

func (app *App) MustRun(osArgs []string, osLookupEnv LookupFunc)

MustRun runs the app. Any errors will be printed to stderr and os.Exit(64) (EX_USAGE) will be called. If there are no errors, os.Exit(0) is called. For more control, check out app.Parse().

func (*App) Parse

func (app *App) Parse(osArgs []string, osLookupEnv LookupFunc) (*ParseResult, error)

Parse parses the args, but does not execute anything.

type AppOpt

type AppOpt = func(*App)

AppOpt let's you customize the app. It panics if there is an error

func ConfigFlag

func ConfigFlag(

	configFlagName string,
	newConfigReader config.NewReader,
	helpShort string,
	flagOpts ...f.FlagOpt,
) AppOpt

ConfigFlag lets you customize your config flag. Especially useful for changing the config reader (for example to choose whether to use a JSON or YAML structured config)

func OverrideHelpFlag added in v0.0.3

func OverrideHelpFlag(
	mappings []HelpFlagMapping,
	helpFile *os.File,
	flagName string,
	flagHelp string,
	flagOpts ...f.FlagOpt,
) AppOpt

OverrideHelpFlag customizes your --help. If you write a custom --help function, you'll want to add it to your app here!

Example
package main

import (
	"fmt"
	"os"

	"github.com/bbkane/warg"
	"github.com/bbkane/warg/command"
	"github.com/bbkane/warg/flag"
	"github.com/bbkane/warg/help"
	"github.com/bbkane/warg/section"
)

func exampleOverrideHelpFlaglogin(pf flag.PassedFlags) error {
	fmt.Println("Logging in")
	return nil
}

func exampleOverrideHelpFlagCustomCommandHelp(file *os.File, _ command.Command, _ help.HelpInfo) command.Action {
	return func(_ flag.PassedFlags) error {
		fmt.Fprintln(file, "Custom command help")
		return nil
	}
}

func exampleOverrideHelpFlagCustomSectionHelp(file *os.File, _ section.SectionT, _ help.HelpInfo) command.Action {
	return func(_ flag.PassedFlags) error {
		fmt.Fprintln(file, "Custom section help")
		return nil
	}
}

func main() {
	app := warg.New(
		"blog",
		section.New(
			"work with a fictional blog platform",
			section.Command(
				"login",
				"Login to the platform",
				exampleOverrideHelpFlaglogin,
			),
		),
		warg.OverrideHelpFlag(
			[]warg.HelpFlagMapping{
				{
					Name:        "default",
					CommandHelp: help.DefaultCommandHelp,
					SectionHelp: help.DefaultSectionHelp,
				},
				{
					Name:        "custom",
					CommandHelp: exampleOverrideHelpFlagCustomCommandHelp,
					SectionHelp: exampleOverrideHelpFlagCustomSectionHelp,
				},
			},
			os.Stdout,
			"--help",
			"Print help",
			flag.Alias("-h"),
			// the flag default should match a name in the HelpFlagMapping
			flag.Default("default"),
		),
	)

	app.MustRun([]string{"blog.exe", "-h", "custom"}, os.LookupEnv)
}
Output:
Custom section help

type HelpFlagMapping added in v0.0.3

type HelpFlagMapping struct {
	Name        string
	CommandHelp help.CommandHelp
	SectionHelp help.SectionHelp
}

HelpFlagMapping adds a new option to your --help flag

type LookupFunc added in v0.0.2

type LookupFunc = func(key string) (string, bool)

Look up keys (meant for environment variable parsing) - fulfillable with os.LookupEnv or warg.LookupMap(map)

func LookupMap added in v0.0.3

func LookupMap(m map[string]string) LookupFunc

LookupMap loooks up keys from a provided map. Useful to mock os.LookupEnv when parsing

type ParseResult

type ParseResult struct {
	// Path to the command invoked. Does not include executable name (os.Args[0])
	Path []string
	// PassedFlags holds the set flags!
	PassedFlags f.PassedFlags
	// Action holds the passed command's action to execute.
	Action c.Action
}

ParseResult holds the result of parsing the command line.

Directories

Path Synopsis
examples
say command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL