├── .editorconfig ├── .golangci.yml ├── LICENSE.md ├── README.md ├── command.go ├── command_test.go ├── examples ├── echo │ ├── .gitignore │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── github │ ├── .gitignore │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ └── repos-list.go │ └── main.go └── log-level │ ├── .gitignore │ ├── README.md │ ├── go.mod │ └── main.go ├── flag.go ├── flag_test.go ├── go.mod ├── go.work └── go.work.sum /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = unset 14 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | presets: 3 | - bugs 4 | - error 5 | - format 6 | - import 7 | - metalinter 8 | - module 9 | - performance 10 | - test 11 | - unused 12 | 13 | disable: 14 | - depguard 15 | - exhaustruct 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Thibaut Rousseau 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-command 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/Thiht/go-command.svg)](https://pkg.go.dev/github.com/Thiht/go-command) 4 | 5 | **go-command** is a **lightweight** and **easy to use** library for creating **command lines with commands and subcommands**. 6 | 7 | This library is built upon the [`flag`](https://pkg.go.dev/flag) package from the standard library. 8 | The declaration of subcommands is inspired by HTTP routers, and go-command encourages you to define the routing of commands in a single file. 9 | 10 | Here's what go-command **will** help you with: 11 | 12 | - declaring commands and subcommands 13 | - basic documentation on each command and subcommand (`-h`/`-help`) 14 | - coming later: shell completions 15 | 16 | go-command is **not** a framework, so here's where it **won't** help you: 17 | 18 | - flag validation beyond what's supported by `flag` 19 | - note that you can do more than you probably expect if you create custom flag types implementing [`flag.Getter`](https://pkg.go.dev/flag#Getter) 20 | - positional arguments validation, you can do it yourself in your handlers 21 | - no specific support for environment variables, you can manage it with `os.Getenv` 22 | - error handling or logging 23 | 24 | ## Why this library? 25 | 26 | 1. I wanted to do subcommands with just the standard library but found it hard to do; this is an attempt at making it easier with minimal abstractions 27 | 2. I wanted to declare my subcommands in the same way as [net/http.HandleFunc](https://pkg.go.dev/net/http#HandleFunc) 28 | 3. I wanted a simpler alternative to [spf13/cobra](https://github.com/spf13/cobra) and [urfave/cli](https://github.com/urfave/cli) 29 | - go-command doesn't aim at being as full-featured as these 30 | 31 | ## How to use? 32 | 33 | Almost everything that go-command can do is defined by the `Command` interface in [`command.go`](./command.go). 34 | 35 | ### Define a root command 36 | 37 | You can create a new root command with `command.Root()`. This returns a command on which you can bind an action or flags, or create a new subcommand. 38 | 39 | ```go 40 | root := command.Root() 41 | 42 | // Bind an action 43 | root = root.Action(rootHandler) 44 | 45 | // Add global flags 46 | root = root.Flags(func(flagSet *flag.FlagSet) { 47 | flagSet.Bool("verbose", false, "Enable verbose output") 48 | }) 49 | 50 | // Set a help text 51 | root = root.Help("Example command") 52 | 53 | // Or, defined fluently 54 | root := command.Root().Action(rootHandler).Flags(func(flagSet *flag.FlagSet) { 55 | flagSet.Bool("verbose", false, "Enable verbose output") 56 | }).Help("Example command") 57 | ``` 58 | 59 | ### (Optional) Create a middleware 60 | 61 | If you want to share behaviours between a command and its subcommands, you can define add middlewares. 62 | Middlewares are simply wrapper functions around action handlers. They can be used for setup and teardown. 63 | As an example, here's a middleware making use of a root `log-level` flag used to set the display level of the logger for the root command and all of its subcommands: 64 | 65 | ```go 66 | // Define a root command 67 | root := command.Root() 68 | 69 | // Add a global log-level flag 70 | root = root.Flags(func(flagSet *flag.FlagSet) { 71 | flagSet.String("log-level", "info", "Log level (debug, info, warn, error)") 72 | }) 73 | 74 | // Add a middleware to the root command 75 | root = root.Middlewares(logLevelMiddleware) 76 | 77 | // A middleware accepts a command handler and returns a new handler. 78 | // The handler argument needs to be called to execute the following middlewares or the action. 79 | // This lets you add behaviours before or after executing the command actions, and can be used to inject data to the context. 80 | func logLevelMiddleware(next command.Handler) command.Handler { 81 | return func(ctx context.Context, flagSet *flag.FlagSet, args []string) int { 82 | switch level := command.Lookup[string](flagSet, "level"); level { 83 | case "debug": 84 | slog.SetLogLoggerLevel(slog.LevelDebug) 85 | 86 | case "info": 87 | slog.SetLogLoggerLevel(slog.LevelInfo) 88 | 89 | case "warn": 90 | slog.SetLogLoggerLevel(slog.LevelWarn) 91 | 92 | case "error": 93 | slog.SetLogLoggerLevel(slog.LevelError) 94 | 95 | default: 96 | fmt.Println("Unknown level") 97 | return 1 98 | } 99 | 100 | return next(ctx, flagSet, args) 101 | } 102 | } 103 | ``` 104 | 105 | ### Add some subcommands 106 | 107 | You can then add subcommands with `SubCommand`. The root command and subcommands all share the same `Command` interface. 108 | 109 | ```go 110 | subCommand := root.SubCommand("my-subcommand") 111 | 112 | // Bind an action 113 | subCommand = subCommand.Action(subCommandHandler) 114 | 115 | // Add global flags 116 | subCommand = subCommand.Flags(func(flagSet *flag.FlagSet) { 117 | flagSet.String("input-file", "", "Input file location") 118 | }) 119 | 120 | // Set a help text 121 | subCommand = subCommand.Help("Example subcommand") 122 | 123 | // Or, defined fluently 124 | subCommand := root.SubCommand("my-subcommand").Action(subCommandHandler).Flags(func(flagSet *flag.FlagSet) { 125 | flagSet.String("input-file", "", "Input file location") 126 | }).Help("Example subcommand") 127 | ``` 128 | 129 | Handlers have to satisfy the `Handler` interface. They receive a context, a flag set, and positional arguments. 130 | 131 | go-command provides the `Lookup[T]` helper to easily get a flag value, but you can use [`flag.FlagSet.Lookup`](https://pkg.go.dev/flag#FlagSet.Lookup) directly if you prefer to stick with the standard library. 132 | 133 | ```go 134 | func rootHandler(ctx context.Context, fs *flag.FlagSet, args []string) int { 135 | verbose := command.Lookup[bool](flagSet, "verbose") 136 | 137 | if err := doStuff(); err != nil { 138 | if verbose { 139 | fmt.Println("something went wrong", err) 140 | } 141 | 142 | return 1 143 | } 144 | 145 | return 0 146 | } 147 | ``` 148 | 149 | ### Execute your command 150 | 151 | When your commands are defined, you can call `Execute()` to parse the execution arguments and call the correct subcommand. This function will call `os.Exit()` with the value returned by the handler. 152 | 153 | ```go 154 | root := command.Root() 155 | 156 | // ... 157 | 158 | root.Execute(context.Background()) 159 | ``` 160 | 161 | ## Examples 162 | 163 | Full examples are available in [./examples](./examples): 164 | 165 | - [echo](./examples/echo): a basic command showing the usage of a single command with a **handler**, **flags**, and **positional arguments**. 166 | - [github](./examples/github): a command showing the usage subcommands with **dependency injection**. 167 | - [log level middleware](./examples/log-level): a command defining a **middleware** to set the log level for all subcommands. 168 | 169 | ## License 170 | 171 | See [LICENSE](./LICENSE.md) 172 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Handler represents a command function called by [Command.Execute]. 12 | // The command flags can be accessed from the FlagSet parameter using [Lookup] or [flag.Lookup]. 13 | type Handler func(context.Context, *flag.FlagSet, []string) int 14 | 15 | // Middleware represents a function used to wrap a [Handler]. They can be used to make actions that will execute before or after the command. 16 | // They are also inherited by subcommands, unlike command actions. 17 | type Middleware func(Handler) Handler 18 | 19 | // Command represents any command or subcommand of the application. 20 | type Command interface { 21 | // SubCommand adds a new subcommand to an existing command. 22 | SubCommand(string) Command 23 | 24 | // Middlewares adds a list of middlewares to the command and its subcommands. 25 | Middlewares(...Middleware) Command 26 | 27 | // Action sets the action to execute when calling the command. 28 | Action(Handler) Command 29 | 30 | // Execute runs the command using [os.Args]. It should normally be called on the root command. 31 | Execute(context.Context) 32 | 33 | // Help sets the help message of a command. 34 | Help(string) Command 35 | 36 | // Flags is used to declare the flags of a command. 37 | Flags(func(*flag.FlagSet)) Command 38 | } 39 | 40 | type command struct { 41 | name string 42 | help string 43 | middlewares []Middleware 44 | handler Handler 45 | subCommands map[string]*command 46 | flagSet *flag.FlagSet 47 | parent *command 48 | } 49 | 50 | // Root creates a new root command. 51 | func Root() Command { 52 | command := command{ 53 | name: os.Args[0], 54 | subCommands: map[string]*command{}, 55 | flagSet: flag.CommandLine, 56 | } 57 | 58 | flag.CommandLine.Usage = command.usage 59 | 60 | return &command 61 | } 62 | 63 | func (c *command) SubCommand(name string) Command { 64 | c.subCommands[name] = &command{ 65 | name: name, 66 | subCommands: map[string]*command{}, 67 | flagSet: flag.NewFlagSet(name, flag.ExitOnError), 68 | parent: c, 69 | } 70 | 71 | c.subCommands[name].flagSet.Usage = c.subCommands[name].usage 72 | 73 | return c.subCommands[name] 74 | } 75 | 76 | func (c *command) Middlewares(middlewares ...Middleware) Command { 77 | c.middlewares = append(c.middlewares, middlewares...) 78 | return c 79 | } 80 | 81 | func (c *command) Action(handler Handler) Command { 82 | c.handler = handler 83 | return c 84 | } 85 | 86 | func (c *command) Execute(ctx context.Context) { 87 | command, args := c, os.Args[1:] 88 | middlewares := append([]Middleware{}, c.middlewares...) 89 | for { 90 | if err := command.flagSet.Parse(args); err != nil { 91 | // This should never occur because the flag sets use flag.ExitOnError 92 | os.Exit(2) // Use 2 to mimick the behavior of flag.ExitOnError 93 | } 94 | 95 | args = command.flagSet.Args() 96 | if len(args) == 0 { 97 | break 98 | } 99 | 100 | subCommand, ok := command.subCommands[args[0]] 101 | if !ok { 102 | break 103 | } 104 | 105 | command.flagSet.VisitAll(func(f *flag.Flag) { 106 | subCommand.flagSet.Var(f.Value, f.Name, f.Usage) 107 | }) 108 | 109 | command = subCommand 110 | args = args[1:] 111 | middlewares = append(middlewares, subCommand.middlewares...) 112 | } 113 | 114 | if command.handler == nil { 115 | if len(args) > 0 { 116 | command.flagSet.SetOutput(os.Stderr) 117 | fmt.Fprintf(command.flagSet.Output(), "command provided but not defined: %s\n", args[0]) 118 | command.usage() 119 | os.Exit(2) // Use 2 to mimick the behavior of flag.ExitOnError 120 | } 121 | 122 | command.usage() 123 | os.Exit(0) 124 | } 125 | 126 | handler := command.handler 127 | for i := len(middlewares) - 1; i >= 0; i-- { 128 | handler = middlewares[i](handler) 129 | } 130 | 131 | os.Exit(handler(ctx, command.flagSet, args)) 132 | } 133 | 134 | func (c *command) Help(help string) Command { 135 | c.help = help 136 | return c 137 | } 138 | 139 | func (c *command) Flags(flags func(*flag.FlagSet)) Command { 140 | flags(c.flagSet) 141 | return c 142 | } 143 | 144 | func (c *command) usage() { 145 | var builder strings.Builder 146 | output := c.flagSet.Output() 147 | c.flagSet.SetOutput(&builder) 148 | 149 | fullCommand := []string{c.name} 150 | for command := c.parent; command != nil; command = command.parent { 151 | fullCommand = append([]string{command.name}, fullCommand...) 152 | } 153 | 154 | var nbFlags int 155 | c.flagSet.VisitAll(func(*flag.Flag) { 156 | nbFlags++ 157 | }) 158 | 159 | optionsHint := "" 160 | if nbFlags > 0 { 161 | optionsHint = " [OPTIONS]" 162 | } 163 | 164 | subCommandHint := "" 165 | if len(c.subCommands) > 0 { 166 | subCommandHint = " [COMMAND]" 167 | if c.handler == nil { 168 | subCommandHint = " COMMAND" 169 | } 170 | } 171 | 172 | builder.WriteString("Usage: ") 173 | builder.WriteString(strings.Join(fullCommand, " ")) 174 | builder.WriteString(optionsHint) 175 | builder.WriteString(subCommandHint) 176 | builder.WriteString("\n") 177 | 178 | if c.help != "" { 179 | builder.WriteString("\n") 180 | builder.WriteString(c.help) 181 | builder.WriteString("\n") 182 | } 183 | 184 | if nbFlags > 0 { 185 | builder.WriteString("\n") 186 | builder.WriteString("Options:\n") 187 | c.flagSet.PrintDefaults() 188 | } 189 | 190 | if len(c.subCommands) > 0 { 191 | builder.WriteString("\n") 192 | builder.WriteString("Subcommands:") 193 | 194 | for name, subCommand := range c.subCommands { 195 | builder.WriteString("\n ") 196 | builder.WriteString(name) 197 | if subCommand.help != "" { 198 | builder.WriteString("\n\t") 199 | builder.WriteString(subCommand.help) 200 | } 201 | } 202 | } 203 | 204 | fmt.Fprintln(output, builder.String()) 205 | } 206 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | -------------------------------------------------------------------------------- /examples/echo/.gitignore: -------------------------------------------------------------------------------- 1 | echo 2 | -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | # echo 2 | 3 | **echo** is a basic command showing the usage of a single command with a handler, flags, and positional arguments. 4 | 5 | ## Build & Run 6 | 7 | ```sh 8 | go build 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | $ ./echo 15 | Usage: ./echo [OPTIONS] COMMAND 16 | 17 | Example command 18 | 19 | Options: 20 | -verbose 21 | Enable verbose output 22 | 23 | Subcommands: 24 | echo 25 | ``` 26 | 27 | ``` 28 | $ ./echo echo -h 29 | Usage: ./echo echo [OPTIONS] 30 | 31 | Options: 32 | -case string 33 | Case to use (upper, lower) 34 | -verbose 35 | Enable verbose output 36 | ``` 37 | 38 | ``` 39 | $ ./echo echo -verbose -case upper Hello, World! 40 | command echo called with case: upper 41 | HELLO, WORLD! 42 | ``` 43 | -------------------------------------------------------------------------------- /examples/echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Thiht/go-command/examples/echo 2 | 3 | go 1.21.3 4 | 5 | require github.com/Thiht/go-command v0.0.0 6 | 7 | replace github.com/Thiht/go-command => ../.. 8 | -------------------------------------------------------------------------------- /examples/echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Thiht/go-command v0.0.0-20231029145014-3c359652cae7 h1:8pv0V32NFXqN1/tceObXaVouhjCgOOTrYNaT7xkP+W8= 2 | github.com/Thiht/go-command v0.0.0-20231029145014-3c359652cae7/go.mod h1:FUs6mjIaE59iG0yeWrZyPOWZ4Dzt/ejODE3xQ6eSzfo= 3 | -------------------------------------------------------------------------------- /examples/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Thiht/go-command" 10 | ) 11 | 12 | func main() { 13 | root := command.Root().Flags(func(flagSet *flag.FlagSet) { 14 | flagSet.Bool("verbose", false, "Enable verbose output") 15 | }).Help("Example command") 16 | 17 | root.SubCommand("echo").Action(EchoHandler).Flags(func(flagSet *flag.FlagSet) { 18 | flagSet.String("case", "", "Case to use (upper, lower)") 19 | }) 20 | 21 | root.Execute(context.Background()) 22 | } 23 | 24 | func EchoHandler(ctx context.Context, flagSet *flag.FlagSet, args []string) int { 25 | verbose := command.Lookup[bool](flagSet, "verbose") 26 | textCase := command.Lookup[string](flagSet, "case") 27 | 28 | if verbose { 29 | fmt.Println("command echo called with case: " + textCase) 30 | } 31 | 32 | switch textCase { 33 | case "upper": 34 | fmt.Println(strings.ToUpper(strings.Join(args, " "))) 35 | 36 | case "lower": 37 | fmt.Println(strings.ToLower(strings.Join(args, " "))) 38 | 39 | default: 40 | fmt.Println(strings.Join(args, " ")) 41 | } 42 | 43 | return 0 44 | } 45 | -------------------------------------------------------------------------------- /examples/github/.gitignore: -------------------------------------------------------------------------------- 1 | github 2 | -------------------------------------------------------------------------------- /examples/github/README.md: -------------------------------------------------------------------------------- 1 | # github 2 | 3 | **github** is a command showing the usage subcommands with dependency injection. 4 | 5 | ## Build & Run 6 | 7 | ```sh 8 | go build 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | $ ./github 15 | Usage: ./github [OPTIONS] COMMAND 16 | 17 | Example command 18 | 19 | Options: 20 | -verbose 21 | Enable verbose output 22 | 23 | Subcommands: 24 | repos 25 | Manage GitHub repositories 26 | ``` 27 | 28 | ``` 29 | $ ./github repos list -user octocat 30 | Repositories of octocat: 31 | - boysenberry-repo-1 32 | - git-consortium 33 | - hello-worId 34 | - Hello-World 35 | - linguist 36 | - octocat.github.io 37 | - Spoon-Knife 38 | - test-repo1 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/github/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Thiht/go-command/examples/github 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/Thiht/go-command v0.0.0 7 | github.com/google/go-github/v56 v56.0.0 8 | ) 9 | 10 | require github.com/google/go-querystring v1.1.0 // indirect 11 | 12 | replace github.com/Thiht/go-command => ../.. 13 | -------------------------------------------------------------------------------- /examples/github/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 3 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= 5 | github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= 6 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 7 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 8 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /examples/github/handlers/repos-list.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/Thiht/go-command" 10 | "github.com/google/go-github/v56/github" 11 | ) 12 | 13 | func ReposListHandler(ghClient *github.Client) command.Handler { 14 | return func(ctx context.Context, flagSet *flag.FlagSet, _ []string) int { 15 | user := command.Lookup[string](flagSet, "user") 16 | if user == "" { 17 | log.Printf("missing required flag: user") 18 | return 1 19 | } 20 | 21 | repos, _, err := ghClient.Repositories.List(ctx, user, nil) 22 | if err != nil { 23 | log.Printf("failed to list repositories: %v", err) 24 | return 1 25 | } 26 | 27 | fmt.Printf("Repositories of %s:\n", user) 28 | for _, repo := range repos { 29 | fmt.Printf("- %s\n", *repo.Name) 30 | } 31 | 32 | return 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/github/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "github.com/Thiht/go-command" 8 | "github.com/Thiht/go-command/examples/github/handlers" 9 | "github.com/google/go-github/v56/github" 10 | ) 11 | 12 | func main() { 13 | client := github.NewClient(nil) 14 | 15 | root := command.Root().Flags(func(flagSet *flag.FlagSet) { 16 | flagSet.Bool("verbose", false, "Enable verbose output") 17 | }).Help("Example command") 18 | 19 | reposCommand := root.SubCommand("repos").Help("Manage GitHub repositories") 20 | { 21 | reposCommand.SubCommand("list").Action(handlers.ReposListHandler(client)).Flags(func(flagSet *flag.FlagSet) { 22 | flagSet.String("user", "", "GitHub user") 23 | }).Help("List repositories of a GitHub user") 24 | } 25 | 26 | root.Execute(context.Background()) 27 | } 28 | -------------------------------------------------------------------------------- /examples/log-level/.gitignore: -------------------------------------------------------------------------------- 1 | log-level 2 | -------------------------------------------------------------------------------- /examples/log-level/README.md: -------------------------------------------------------------------------------- 1 | # log-level 2 | 3 | **log-level** is a basic command showing how to make use of go-command's middlewares. 4 | 5 | ## Build & Run 6 | 7 | ```sh 8 | go build 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | $ ./log-level info Hello, world! 15 | 2024/12/26 22:57:13 INFO Hello, world! 16 | ``` 17 | 18 | ``` 19 | $ ./log-level -level=warn info Hello, world! 20 | 21 | ``` 22 | 23 | ``` 24 | $ ./log-level -level=warn error Hello, world! 25 | 2024/12/26 22:58:07 ERROR Hello, world! 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/log-level/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Thiht/go-command/examples/log-level 2 | 3 | go 1.23.4 4 | 5 | require github.com/Thiht/go-command v0.0.0 6 | 7 | replace github.com/Thiht/go-command => ../.. 8 | -------------------------------------------------------------------------------- /examples/log-level/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log/slog" 8 | "strings" 9 | 10 | "github.com/Thiht/go-command" 11 | ) 12 | 13 | func main() { 14 | root := command.Root().Flags(func(flagSet *flag.FlagSet) { 15 | flagSet.String("level", "info", "Minimum level of logs to display") 16 | }).Middlewares(LevelMiddleware) 17 | 18 | root.SubCommand("info").Action(InfoHandler) 19 | root.SubCommand("error").Action(ErrorHandler) 20 | 21 | root.Execute(context.Background()) 22 | } 23 | 24 | func LevelMiddleware(next command.Handler) command.Handler { 25 | return func(ctx context.Context, flagSet *flag.FlagSet, args []string) int { 26 | switch level := command.Lookup[string](flagSet, "level"); level { 27 | case "debug": 28 | slog.SetLogLoggerLevel(slog.LevelDebug) 29 | 30 | case "info": 31 | slog.SetLogLoggerLevel(slog.LevelInfo) 32 | 33 | case "warn": 34 | slog.SetLogLoggerLevel(slog.LevelWarn) 35 | 36 | case "error": 37 | slog.SetLogLoggerLevel(slog.LevelError) 38 | 39 | default: 40 | fmt.Println("Unknown level") 41 | return 1 42 | } 43 | 44 | return next(ctx, flagSet, args) 45 | } 46 | } 47 | 48 | func InfoHandler(ctx context.Context, _ *flag.FlagSet, args []string) int { 49 | slog.InfoContext(ctx, strings.Join(args, " ")) 50 | return 0 51 | } 52 | 53 | func ErrorHandler(ctx context.Context, _ *flag.FlagSet, args []string) int { 54 | slog.ErrorContext(ctx, strings.Join(args, " ")) 55 | return 0 56 | } 57 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | // Lookup returns the value of a flag as a T, or its zero value if the flag doesn't exist. 8 | func Lookup[T any](flagSet *flag.FlagSet, name string) T { 9 | return flagSet.Lookup(name).Value.(flag.Getter).Get().(T) 10 | } 11 | -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "flag" 5 | "net" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/Thiht/go-command" 11 | ) 12 | 13 | type stringList []string 14 | 15 | var _ flag.Getter = (*stringList)(nil) 16 | 17 | func (sl *stringList) Set(value string) error { 18 | *sl = strings.Split(value, ",") 19 | return nil 20 | } 21 | 22 | func (sl *stringList) String() string { 23 | return strings.Join(*sl, ",") 24 | } 25 | 26 | func (sl *stringList) Get() any { 27 | return *sl 28 | } 29 | 30 | type ipVar struct { 31 | IP net.IP 32 | } 33 | 34 | var _ flag.Getter = &ipVar{} 35 | 36 | func (v *ipVar) Get() any { 37 | return v.IP 38 | } 39 | 40 | func (v *ipVar) String() string { 41 | return v.IP.String() 42 | } 43 | 44 | func (v *ipVar) Set(s string) error { 45 | v.IP = net.ParseIP(s) 46 | return nil 47 | } 48 | 49 | func TestLookup(t *testing.T) { 50 | t.Parallel() 51 | 52 | fs := flag.NewFlagSet("test", flag.ExitOnError) 53 | fs.Bool("bool", false, "") 54 | fs.Int("int", 0, "") 55 | fs.Int64("int64", 0, "") 56 | fs.Uint("uint", 0, "") 57 | fs.Uint64("uint64", 0, "") 58 | fs.String("string", "info", "") 59 | fs.Float64("float64", 0, "") 60 | fs.Duration("duration", 0, "") 61 | 62 | var strings stringList 63 | fs.Var(&strings, "strings", "") 64 | 65 | fs.Var(&ipVar{}, "ip", "") 66 | 67 | if err := fs.Parse([]string{ 68 | "-bool", 69 | "-int=1", 70 | "-int64=1", 71 | "-uint=1", 72 | "-uint64=1", 73 | "-string=debug", 74 | "-float64=3.14", 75 | "-duration=1s", 76 | "-strings=foo,bar,baz", 77 | "-ip=127.0.0.1", 78 | }); err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | t.Run("lookup bool", func(t *testing.T) { 83 | t.Parallel() 84 | 85 | if boolValue := command.Lookup[bool](fs, "bool"); !boolValue { 86 | t.Errorf("boolValue = %v, want %v", boolValue, true) 87 | } 88 | }) 89 | 90 | t.Run("lookup int", func(t *testing.T) { 91 | t.Parallel() 92 | 93 | if intValue := command.Lookup[int](fs, "int"); intValue != 1 { 94 | t.Errorf("intValue = %v, want %v", intValue, 1) 95 | } 96 | }) 97 | 98 | t.Run("lookup int64", func(t *testing.T) { 99 | t.Parallel() 100 | 101 | if int64Value := command.Lookup[int64](fs, "int64"); int64Value != 1 { 102 | t.Errorf("int64Value = %v, want %v", int64Value, 1) 103 | } 104 | }) 105 | 106 | t.Run("lookup uint", func(t *testing.T) { 107 | t.Parallel() 108 | 109 | if uintValue := command.Lookup[uint](fs, "uint"); uintValue != 1 { 110 | t.Errorf("uintValue = %v, want %v", uintValue, 1) 111 | } 112 | }) 113 | 114 | t.Run("lookup uint64", func(t *testing.T) { 115 | t.Parallel() 116 | 117 | if uint64Value := command.Lookup[uint64](fs, "uint64"); uint64Value != 1 { 118 | t.Errorf("uint64Value = %v, want %v", uint64Value, 1) 119 | } 120 | }) 121 | 122 | t.Run("lookup string", func(t *testing.T) { 123 | t.Parallel() 124 | 125 | if stringValue := command.Lookup[string](fs, "string"); stringValue != "debug" { 126 | t.Errorf("stringValue = %v, want %v", stringValue, "debug") 127 | } 128 | }) 129 | 130 | t.Run("lookup float64", func(t *testing.T) { 131 | t.Parallel() 132 | 133 | if float64Value := command.Lookup[float64](fs, "float64"); float64Value != 3.14 { 134 | t.Errorf("float64Value = %v, want %v", float64Value, 3.14) 135 | } 136 | }) 137 | 138 | t.Run("lookup duration", func(t *testing.T) { 139 | t.Parallel() 140 | 141 | if durationValue := command.Lookup[time.Duration](fs, "duration"); durationValue != time.Second { 142 | t.Errorf("durationValue = %v, want %v", durationValue, time.Second) 143 | } 144 | }) 145 | 146 | t.Run("lookup custom type stringList", func(t *testing.T) { 147 | t.Parallel() 148 | 149 | if stringsValue := command.Lookup[stringList](fs, "strings"); len(stringsValue) != 3 || stringsValue[0] != "foo" || stringsValue[1] != "bar" || stringsValue[2] != "baz" { 150 | t.Errorf("stringsValue = %v, want %v", stringsValue, []string{"foo", "bar", "baz"}) 151 | } 152 | }) 153 | 154 | t.Run("lookup ip", func(t *testing.T) { 155 | t.Parallel() 156 | 157 | if ipValue := command.Lookup[net.IP](fs, "ip"); ipValue.String() != "127.0.0.1" { 158 | t.Errorf("ipValue = %v, want %v", ipValue.String(), "127.0.0.1") 159 | } 160 | }) 161 | 162 | t.Run("lookup non-existent flag", func(t *testing.T) { 163 | t.Parallel() 164 | 165 | defer func() { 166 | if recover() == nil { 167 | t.Error("lookup of a non-existent flag should panic") 168 | } 169 | }() 170 | 171 | if stringValue := command.Lookup[string](fs, "non-existent"); stringValue != "" { 172 | t.Errorf("stringValue = %v, want %v", stringValue, "") 173 | } 174 | }) 175 | 176 | t.Run("lookup flag with wrong type", func(t *testing.T) { 177 | t.Parallel() 178 | 179 | defer func() { 180 | if recover() == nil { 181 | t.Error("lookup of a flag with wrong type should panic") 182 | } 183 | }() 184 | 185 | if stringValue := command.Lookup[string](fs, "bool"); stringValue != "" { 186 | t.Errorf("stringValue = %v, want %v", stringValue, "") 187 | } 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Thiht/go-command 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.4 2 | 3 | use ( 4 | . 5 | ./examples/echo 6 | ./examples/github 7 | ./examples/log-level 8 | ) 9 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 2 | --------------------------------------------------------------------------------