github.com/ucarion/cli
is a Golang package for writing delightful, Unix-style
command-line tools in a type-safe way. With cli
, you can define:
- Commands and sub-commands (
git commit
,git remote
,git remote set-url
) - Short-style flags (
-f
,-o json
,-ojson
,-abc
) - Long-style flags (
--force
,--output json
,--output=json
) - "Positional" arguments (
mv <from> <to>
,cat <files...>
)
You will automatically get:
-h
and--help
usage messages- Man page generation (e.g. an automatically-generated
man my-cool-tool
) - Bash and Zsh tab autocompletion (e.g.
mytool --f<TAB>
expands intomytool --force
)
Best of all, github.com/ucarion/cli
gives you all of this while keeping a
dirt-simple interface. Here's an unabridged, working tool built with cli
:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
FirstName string `cli:"--first-name"`
LastName string `cli:"--last-name"`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Println("hello", args.FirstName, args.LastName)
return nil
})
}
This is examples/basic
in this repo, which you can run as:
$ go run ./examples/basic/... --first-name=john --last-name doe
hello john doe
To use cli
in your project, run:
go get github.com/ucarion/cli
As an end-to-end demonstration of how you can use cli
to build a tool with
subcommands, flags, arguments, --help
text, man
pages, and Bash/Zsh
completions, all with automated releases with GitHub actions and an
easy-to-install brew
formula for macOS users, check out:
https://github.com/ucarion/fakegit
You can use fakegit
to see what the most complex cli
applications look like,
and you use it as a starting point in your own applications.
For detailed, specific documentation on exactly what you can pass to cli.Run
,
see the godocs for
github.com/ucarion/cli
.
This section will work more as a cookbook, showing you working programs that you
can work off of.
At a high level, you use cli
by passing cli.Run
a context and a set of
functions. cli
requires that every function you pass to cli.Run
looks like:
func (context.Context, T) error
Where T
has to be a struct. cli
will use reflection to determine the options
and arguments your command or sub-command expects. The rest of this section will
show examples of how you can use all of cli'
s features.
To accept options (also called "flags"), mark a field in your struct with a tag
called cli
. You can give your option a "short" name (e.g. -f
), a "long" name
(e.g. --force
), or both.
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
Force bool `cli:"-f,--force"`
Output string `cli:"-o,--output"`
N int `cli:"-n"`
RFC3339 bool `cli:"--rfc3339"`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/options
in this repo, which you can run
as:
$ go run ./examples/options/...
main.args{Force:false, Output:"", N:0, RFC3339:false}
$ go run ./examples/options/... --force --output json --rfc3339 -n 5
main.args{Force:true, Output:"json", N:5, RFC3339:true}
cli
supports the full set of "standard" Unix command-line conventions, so this
also works, like it would with most tools in modern Linux distributions:
$ go run ./examples/options/... -fn5 --rfc3339 --output=json
main.args{Force:true, Output:"json", N:5, RFC3339:true}
To accept arguments that aren't options, like the pattern
and trailing list of
files
in grep pattern files...
, then tag your fields with cli
, but don't
include a leading -
or --
. If your tag's value ends with ...
, then all
"leftover" / "trailing" arguments will go into that field.
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
Foo string `cli:"foo"`
Bar string `cli:"bar"`
Baz []string `cli:"baz..."`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/posargs
in this repo, which you can run
as:
$ go run ./examples/posargs/... a b
main.args{Foo:"a", Bar:"b", Baz:[]string(nil)}
$ go run ./examples/posargs/... a b c d e
main.args{Foo:"a", Bar:"b", Baz:[]string{"c", "d", "e"}}
As a relatively straightforward extension of the previous two examples, you can accept both options ("flags") and "positional" arguments at once:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
Force bool `cli:"-f,--force"`
Output string `cli:"-o,--output"`
N int `cli:"-n"`
RFC3339 bool `cli:"--rfc3339"`
Foo string `cli:"foo"`
Bar string `cli:"bar"`
Baz []string `cli:"baz..."`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/argsandopts
in this repo, which you
can run as:
$ go run ./examples/argsandopts/... --force --output=json a b c d e --rfc3339
main.args{Force:true, Output:"json", N:0, RFC3339:true, Foo:"a", Bar:"b", Baz:[]string{"c", "d", "e"}}
As is standard with Unix tools, cli
will treat --
in the input args as a
"end of flags" indicator. So, for instance, if you wanted --rfc3339
above to
be treated as an argument instead of a flag, you could do:
$ go run ./examples/argsandopts/... --force --output=json a b c d e -- --rfc3339
main.args{Force:true, Output:"json", N:0, RFC3339:false, Foo:"a", Bar:"b", Baz:[]string{"c", "d", "e", "--rfc3339"}}
If you mark one of the fields of your struct like this:
type bazArgs struct {
ParentArgs parentArgs `cli:"baz,subcmd"`
}
That means that you're defining a sub-command called baz
, and it's a
sub-command of the parentArgs
type. When you pass a set of functions to
cli.Run
, cli
will use these cli:"xxx,subcmd"
tags to discover your "tree"
of commands.
So, for instance, if you want to have a CLI tool that has a get
and set
subcommands, you can do that like this:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type rootArgs struct {
Username string `cli:"--username"`
Password string `cli:"--password"`
}
func main() {
cli.Run(context.Background(), get, set)
}
type getArgs struct {
RootArgs rootArgs `cli:"get,subcmd"`
Key string `cli:"key"`
}
func get(ctx context.Context, args getArgs) error {
fmt.Printf("get %#v\n", args)
return nil
}
type setArgs struct {
RootArgs rootArgs `cli:"set,subcmd"`
Key string `cli:"key"`
Value string `cli:"value"`
}
func set(ctx context.Context, args setArgs) error {
fmt.Printf("set %#v\n", args)
return nil
}
This is examples/subcmds
in this repo, which you can run
as:
$ go run ./examples/subcmds/... --username foo --password bar get xxx
get main.getArgs{RootArgs:main.rootArgs{Username:"foo", Password:"bar"}, Key:"xxx"}
$ go run ./examples/subcmds/... --username foo --password bar set xxx yyy
set main.setArgs{RootArgs:main.rootArgs{Username:"foo", Password:"bar"}, Key:"xxx", Value:"yyy"}
The pattern above of pointing to your parent config type via cli:"xxx,subcmd"
tags can work recursively. For instance, if you wanted to add config get
and
config set
subcommands to the above example, you could do:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type rootArgs struct {
Username string `cli:"--username"`
Password string `cli:"--password"`
}
func main() {
cli.Run(context.Background(), get, set, getConfig, setConfig)
}
type getArgs struct {
RootArgs rootArgs `cli:"get,subcmd"`
Key string `cli:"key"`
}
func get(ctx context.Context, args getArgs) error {
fmt.Printf("get %#v\n", args)
return nil
}
type setArgs struct {
RootArgs rootArgs `cli:"set,subcmd"`
Key string `cli:"key"`
Value string `cli:"value"`
}
func set(ctx context.Context, args setArgs) error {
fmt.Printf("set %#v\n", args)
return nil
}
type configArgs struct {
RootArgs rootArgs `cli:"config,subcmd"`
ConfigFile string `cli:"--config-file"`
}
type getConfigArgs struct {
ConfigArgs configArgs `cli:"get,subcmd"`
Key string `cli:"key"`
}
func getConfig(ctx context.Context, args getConfigArgs) error {
fmt.Printf("get config %#v\n", args)
return nil
}
type setConfigArgs struct {
ConfigArgs configArgs `cli:"set,subcmd"`
Key string `cli:"key"`
Value string `cli:"value"`
}
func setConfig(ctx context.Context, args setConfigArgs) error {
fmt.Printf("set config %#v\n", args)
return nil
}
This is examples/nestedsubcmds
in this repo, which
you can run as:
$ go run ./examples/nestedsubcmds/... --username foo --password bar get xxx
get main.getArgs{RootArgs:main.rootArgs{Username:"foo", Password:"bar"}, Key:"xxx"}
$ go run ./examples/nestedsubcmds/... --username foo --password bar set xxx yyy
set main.setArgs{RootArgs:main.rootArgs{Username:"foo", Password:"bar"}, Key:"xxx", Value:"yyy"}
$ go run ./examples/nestedsubcmds/... config --config-file=config.txt get xxx
get config main.getConfigArgs{ConfigArgs:main.configArgs{RootArgs:main.rootArgs{Username:"", Password:""}, ConfigFile:"config.txt"}, Key:"xxx"}
$ go run ./examples/nestedsubcmds/... config --config-file=config.txt set xxx yyy
set config main.setConfigArgs{ConfigArgs:main.configArgs{RootArgs:main.rootArgs{Username:"", Password:""}, ConfigFile:"config.txt"}, Key:"xxx", Value:"yyy"}
You may notice that in the above example, configArgs
is used as the parent
type to both getConfigArgs
and putConfigArgs
, but is never directly used by
any function you pass to cli.Run
. When you do that, that indicates to cli
that you don't want the config
subcommand to really "run". So this:
$ go run ./examples/nestedsubcmds/... config
Just outputs help text, showing users that config
takes a --config-file
, and
that its subcommands are get
and set
:
usage: /var/folders/.../exe/nestedsubcmds config [<options>] get|set
--config-file <string>
-h, --help display this help and exit
By default, cli
will generate a help text for you, and it will be displayed if
the user passes -h
or --help
. By default, the help text looks like (see
examples/argsandopts
in this repo for where these
flags and args are from):
$ go run ./examples/argsandopts/... --help
usage: /var/folders/.../exe/argsandopts [<options>] foo bar baz...
-f, --force
-o, --output <string>
-n <int>
--rfc3339
-h, --help display this help and exit
The long /var/folders/...
stuff is an artifact of how go run
works, where it
first compiles the program into a temp directory with an esoteric name. cli
's
auto-generated help will figure out your program's name from os.Args[0]
, as is
convention in Unix tools.
To get a less weird-looking name after usage:
in the help text, try compiling
the program yourself first:
$ go build ./examples/argsandopts/...
$ ./argsandopts --help
usage: ./argsandopts [<options>] foo bar baz...
-f, --force
-o, --output <string>
-n <int>
--rfc3339
-h, --help display this help and exit
There are a couple of things you can do to customize the help text:
- If you set a
ExtendedDescription() string
method on your args struct, thencli
will call it, and use it as a description for your command. - If you set a
usage
tag on a field, that will be shown next to the flag. - If you set a
value
tag on a field, that will be shown instead of the<string>
or<int>
in the default output above.
Furthermore, if you define either -h
or --help
flag yourself, then cli
will leave it be. If you define both -h
and --help
, then cli
will not show
help for you at all. This is useful mostly if you're writing a tool like ls
or
du
, where -h
means "human-readable".
Putting all of that together, you can do:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
Human bool `cli:"-h" usage:"show human-readable output"`
Force bool `cli:"-f,--force" usage:"do the thing no matter what"`
Output string `cli:"-o,--output" value:"format" usage:"the format to output in"`
N int `cli:"-n" value:"times" usage:"how many times to do the thing"`
RFC3339 bool `cli:"--rfc3339" usage:"use rfc3339 timestamps"`
Foo string `cli:"foo"`
Bar string `cli:"bar"`
Baz []string `cli:"baz..."`
}
func (_ args) ExtendedDescription() string {
return "This is just a program that shows you how to customize help text."
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/customhelptext
in this repo,
which you can run as:
$ go run ./examples/customhelptext/... --help
usage: /var/folders/.../customhelptext [<options>] foo bar baz...
This is just a program that shows you how to customize help text.
-h show human-readable output
-f, --force do the thing no matter what
-o, --output <format> the format to output in
-n <times> how many times to do the thing
--rfc3339 use rfc3339 timestamps
--help display this help and exit
If you set an environment variable called UCARION_CLI_GENERATE_MAN
, then
cli.Run
will generate man
pages instead of running your program as usual.
The value of UCARION_CLI_GENERATE_MAN
is the directory where the man pages
will be generated; each sub-command will get its own man page.
Aside: it's called
UCARION_CLI_GENERATE_MAN
to make it more obvious what is reading the environment variable. The goal was to use a name that made it obvious that something called "ucarion cli" is generating a man page, which if you put in Google will hopefully lead you to the docs you are currently reading.
By default, the man pages look like (see examples/argsandopts
in this
repo for where these flags and args are from):
$ UCARION_CLI_GENERATE_MAN="." go run ./examples/argsandopts/...
$ man ./argsandopts.1
ARGSANDOPTS(1) ARGSANDOPTS(1)
NAME
argsandopts
SYNOPSIS
argsandopts [<options>] foo bar baz...
DESCRIPTION
OPTIONS
-f, --force
-o, --output <string>
-n <int>
--rfc3339
-h, --help
Display help message and exit.
ARGSANDOPTS(1)
There are a couple of things you can do to customize the help text:
-
If you set a
Description() string
method on your args struct, thencli
will call it, and the return value will appear after your program's name in the "Name" section.By convention, you should use a short, lower-case string for the description. For example,
ls
's description is:ls - list directory contents
-
If you set a
ExtendedDescription() string
method on your args struct, thencli
will call it, and use the return value as the "Description" for your command. This method is also used in help text, described in the previous section. -
If you set a
value
tag on a field, that will be shown instead of the<string>
or<int>
in the default output above. -
If you have a field called
XXX
in your struct (this is the "actual" name for the field not what you put in thecli
tag), and if you have a method calledExtendedUsage_XXX() string
, thencli
will call it, and use the return value as the usage for the flag in man pages. This only applies to flags; there is no corresponding conventional way to describe "positional" arguments in man pages.
Putting all of that together, you can do:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
Force bool `cli:"-f,--force"`
Output string `cli:"-o,--output" value:"format"`
N int `cli:"-n" value:"times"`
RFC3339 bool `cli:"--rfc3339"`
Foo string `cli:"foo"`
Bar string `cli:"bar"`
Baz []string `cli:"baz..."`
}
func (_ args) Description() string {
return "dummy command with custom man page"
}
func (_ args) ExtendedDescription() string {
return "This is just a program that shows you how to customize man pages."
}
func (_ args) ExtendedUsage_Force() string {
return "Do the thing no matter what."
}
func (_ args) ExtendedUsage_Output() string {
return "The format to output in."
}
func (_ args) ExtendedUsage_N() string {
return "How many times to do the thing."
}
func (_ args) ExtendedUsage_RFC3339() string {
return "Use RFC3339 timestamps."
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/custommanpage
in this repo, which
you can run as:
$ UCARION_CLI_GENERATE_MAN="." go run ./examples/custommanpage/...
$ man ./custommanpage.1
CUSTOMMANPAGE(1) CUSTOMMANPAGE(1)
NAME
custommanpage - dummy command with custom man page
SYNOPSIS
custommanpage [<options>] foo bar baz...
DESCRIPTION
This is just a program that shows you how to customize man pages.
OPTIONS
-f, --force
Do the thing no matter what.
-o, --output <format>
The format to output in.
-n <times>
How many times to do the thing.
--rfc3339
Use RFC3339 timestamps.
-h, --help
Display help message and exit.
CUSTOMMANPAGE(1)
The Bash and Zsh shells both support "completion" scripts that the shell will
run when you press "tab" (the Fish shell uses man
pages to populate
completions, so the above section covers that) If you're not familiar with how
Bash/Zsh completion works, here's a crash course:
-
You can register a completion script with Bash/Zsh using the builtin
complete
. In Bash, this builtin is available out of the box. In Zsh, you first have to run:autoload -U +X compinit && compinit autoload -U +X bashcompinit && bashcompinit
-
Once you've registered a completion script, then when Bash/Zsh needs to generate completions, it will call the relevant completion script with the environment vars
COMP_LINE
(containing the line typed so far) andCOMP_CWORD
(containing the index of the word to complete).
Typically, programs that support completion ship with a Bash or Zsh script alongside their main program, and they re-implement (a subset of) their flag parsing in a shell script in order to generate completions.
cli
takes a different approach. With cli
, every program is its own
completion script. If cli.Run
sees that the COMP_LINE
and COMP_CWORD
environment variables are present, then cli.Run
will output a set of
completions instead of running your program as usual.
For instance, to see what completions look like by default (see
examples/argsandopts
in this repo for where these
flags and args are from):
$ go build ./examples/argsandopts/...
$ complete -o bashdefault -o default -C ./argsandopts argsandopts
$ ./argsandopts -<TAB>
--force --output --rfc3339 -n
To emphasize how non-magic this is, you could also get those completions by
running ./argsandopts
yourself:
$ COMP_LINE="./argsandopts -" COMP_CWORD="1" ./argsandopts
--force
--output
--rfc3339
-n
If your program has sub-commands, cli.Run
will offer those sub-commands in its
completions. For instance (see examples/nestedsubcmds
in this
repo for where these commands and flags are from):
$ ./nestedsubcmds <TAB>
--password --username config get set
$ ./nestedsubcmds config <TAB>
--config-file get set
By default, cli
will not offer any autocompletions for the value of a flag or
a positional argument. As a result of -o
flags we passed to complete
in the
previous section, Bash/Zsh will fall back to its default behavior, which is to
list files in the current directory:
$ ./argsandopts --output <TAB>
README.md argsandopts* cli.go ... (etc)
If you have a field called XXX
in your struct (this is the "actual" name for
the field not what you put in the cli
tag), and if you have a method called
Autocomplete_XXX() []string
, then cli
will call it, and will use the return
value as the suggested values for the flag or argument.
Crucially, your Autocomplete_XXX
will be called after cli
tries to parse
the flags the user has provided. That means that if your completions for a flag
or argument are a function of other flags, you can read those flag values to
figure out what to complete. This is especially useful if you have some sort of
--config-file
or --username
/--password
flags that you need in order to
authenticate with a system, and then poll that system to figure out your
completions.
Putting all of that together, you can do:
package main
import (
"context"
"fmt"
"strings"
"github.com/ucarion/cli"
)
type args struct {
Foo string `cli:"--foo"`
Bar string `cli:"--bar"`
}
func (a args) Autocomplete_Bar() []string {
if a.Foo == "" {
return nil
}
return []string{strings.ToUpper(a.Foo), strings.ToLower(a.Foo)}
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/customcompletions
in this
repo, which you can run as:
$ go build ./examples/customcompletions/...
$ complete -o bashdefault -o default -C ./customcompletions customcompletions
$ ./customcompletions --foo hElLo --bar <TAB>
HELLO hello
This section will go through some more advanced use-cases for things you can do with flags or arguments.
If you mark a field as a flag, and that field's type is a slice (e.g.
[]string
, []int
, etc.), then cli
will let users pass that flag multiple
times. For example:
package main
import (
"context"
"fmt"
"github.com/ucarion/cli"
)
type args struct {
Names []string `cli:"--name"`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/repeatedflag
in this repo, which
you can run as:
$ go run ./examples/repeatedflag/...
main.args{Names:[]string(nil)}
$ go run ./examples/repeatedflag/... --name foo
main.args{Names:[]string{"foo"}}
$ go run ./examples/repeatedflag/... --name foo --name bar --name baz
main.args{Names:[]string{"foo", "bar", "baz"}}
Some tools support options that can be provided either in the "boolean" way
(e.g. mycmd --force
) or in the "takes-a-value" way (e.g. mycmd --output=json
). For instance, in git
the --color
flag, when it's supported,
can be provided with or without a value:
# These two do the same thing
git show HEAD --color
git show HEAD --color=auto
# This is different
git show HEAD --color=never
cli
supports this use-case. If you mark a field as a flag, and that field's
type is a pointer (e.g. *string
, *int
, etc.), then cli
will let users pass
that flag with or without a value.
If users don't pass the flag at all, the field will remain nil
when it's
provided to you. If the users set the flag, but don't provide a value, then the
field will be instantiated as a pointer to the zero value of the type (e.g. for
*string
, it would be a pointer to an empty string). If users set the flag and
provide a value, the field will be a pointer to that parsed value.
For example:
package main
import (
"context"
"encoding/json"
"os"
"github.com/ucarion/cli"
)
type args struct {
Color *string `cli:"--color"`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
// We display this as json to avoid just printing a pointer here.
return json.NewEncoder(os.Stdout).Encode(args)
})
}
This is examples/optionallytakingvalue
in this
repo, which you can run as:
$ go run ./examples/optionallytakingvalue/...
{"Color":null}
$ go run ./examples/optionallytakingvalue/... --color
{"Color":""}
$ go run ./examples/optionallytakingvalue/... --color=never
{"Color":"never"}
Optionally-taking-value options like this can be confusing to users. For instance, this is not a valid invocation, because you're not allowed to put a space between an optionally-taking-value option and its value:
$ go run ./examples/optionallytakingvalue/... --color never
unexpected argument: never
What's going on here is that cli
, in accordance with Unix convention, parses
--color
as not having a value passed, and assumes never
is a non-option
argument. But examples/optionallytakingvalue
doesn't define any non-option
arguments, so cli
reports an error to the user for the unexpected argument.
Out of the box, cli
supports all of Go's number types (including floats,
signed and unsigned ints, but not complex numbers), as well as bools and
strings, for any option or argument. If you'd like to parse options into a
different type, you can:
-
Just do that parsing yourself, from within the function you pass to
cli.Run
, or -
Make sure the type implements the standard libary's
encoding.TextUnmarshaler
interface, which looks like this:type TextUnmarshaler interface { UnmarshalText(text []byte) error }
For example, the standard library's net.IP
type implements TextUnmarshaler
,
so you can do this:
package main
import (
"context"
"fmt"
"net"
"github.com/ucarion/cli"
)
type args struct {
Foo net.IP `cli:"--foo"`
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/ipparam
in this repo, which you can run
as:
$ go run ./examples/ipparam/... --foo asdf
--foo: invalid IP address: asdf
exit status 1
go run ./examples/ipparam/... --foo 127.0.0.1
main.args{Foo:net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x1}}
Alternatively, you can implement TextUnmarshaler
on your own. For instance,
here's a basic TextUnmarshaler
implementation for a type that supports parsing
numbers with suffixes like "B", "KB", "GB", "TB":
package main
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/ucarion/cli"
)
type args struct {
Foo bytes `cli:"--foo"`
}
type bytes int
func (b *bytes) UnmarshalText(text []byte) error {
s := string(text)
var base string
var factor int
switch {
case strings.HasSuffix(s, "KB"):
base = s[:len(s)-2]
factor = 1024
case strings.HasSuffix(s, "MB"):
base = s[:len(s)-2]
factor = 1024 * 1024
case strings.HasSuffix(s, "GB"):
base = s[:len(s)-2]
factor = 1024 * 1024 * 1024
case strings.HasSuffix(s, "TB"):
base = s[:len(s)-2]
factor = 1024 * 1024 * 1024 * 1024
case strings.HasSuffix(s, "B"):
base = s[:len(s)-1]
factor = 1
default:
return fmt.Errorf("missing units suffix (must be one of B, KB, MB, GB, TB): %s", s)
}
n, err := strconv.ParseInt(base, 0, 0)
if err != nil {
return err
}
*b = bytes(int(n) * factor)
return nil
}
func main() {
cli.Run(context.Background(), func(ctx context.Context, args args) error {
fmt.Printf("%#v\n", args)
return nil
})
}
This is examples/customtype
in this repo, which you
can run as:
$ go run ./examples/customtype/... --foo asdf
--foo: missing units suffix (must be one of B, KB, MB, GB, TB): asdf
exit status 1
$ go run ./examples/customtype/... --foo 2B
main.args{Foo:2}
$ go run ./examples/customtype/... --foo 2KB
main.args{Foo:2048}
$ go run ./examples/customtype/... --foo 2GB
main.args{Foo:2147483648}
$ go run ./examples/customtype/... --foo 2TB
main.args{Foo:2199023255552}
With such a design, you could use this custom bytes
type for any CLI parameter
that you want formatted with B/KB/GB/TB suffixes. You could even publish a
package with human-friendly types that implement TextUnmarshaler
, like (for
example)
github.com/segmentio/cli/human
,
and then re-use those types across multiple projects.
Ultimately, cli
relies on TextUnmarshaler
in order to be broadly compatible
with the Golang ecosystem and standard library, and does not bundle
implementations of TextUnmarshaler
so as to avoid picking winners and losers
in the space of "human-friendly" string-parsing libraries.