Skip to content

Commit

Permalink
Update README and argument names (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikogs authored Dec 31, 2024
1 parent 94ae7ee commit b49c328
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 185 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BSD 2-Clause License

Copyright (c) 2023, 2024, Mikolaj Gasior
Copyright (c) 2023, 2024, Mikolaj Gasior <[email protected]>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down
158 changes: 100 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,124 @@
# broccli

[![Go Reference](https://pkg.go.dev/badge/github.com/mikolajgs/broccli.svg)](https://pkg.go.dev/github.com/mikolajgs/broccli) [![Go Report Card](https://goreportcard.com/badge/github.com/mikolajgs/broccli)](https://goreportcard.com/report/github.com/mikolajgs/broccli) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/mikolajgs/broccli?sort=semver)
[![Go Reference](https://pkg.go.dev/badge/github.com/go-phings/broccli.svg)](https://pkg.go.dev/github.com/go-phings/broccli) [![Go Report Card](https://goreportcard.com/badge/github.com/go-phings/broccli)](https://goreportcard.com/report/github.com/go-phings/broccli)

----

The `mikolajgs/broccli` package simplifies command line interface management. It allows you to define commands complete with arguments and flags, and attach handlers to them. The package handles all the parsing automatically.
The `go-phings/broccli` package simplifies command line interface management. It allows you to define commands complete with arguments and flags, and attach handlers to them. The package handles all the parsing automatically.

----

### Install

Ensure you have your
[workspace directory](https://golang.org/doc/code.html#Workspaces) created and
run the following:
## Table of Contents

* [Sample code](#sample-code)
* [Structs explained](#structs-explained)
* [CLI](#cli)
* [Commands](#commands)
* [Flags and Arguments](#flags-and-arguments)
* [Environment variables to check](#environment-variables-to-check)
* [Accessing flag and arg values](#accessing-flag-and-arg-values)
* [Features + Roadmap](#features)

## Sample code
The following code snippet from another tiny project shows how the module can be used.

```go
// create new CLI object
cli := broccli.NewCLI("snakey-letters", "Classic snake but with letters and words!", "")

// add a command and attach a function to it
cmd := cli.AddCmd("start", "Starts the game", startHandler)
// add a flag to the command
cmd.AddFlag("words", "f", "",
"Text file with wordlist",
// should be a path to a file
broccli.TypePathFile,
// flag is required (cannot be empty) and path must exist
broccli.IsExistent|broccli.IsRequired,
)

// add another command to print version number
_ = cli.AddCmd("version", "Shows version", versionHandler)

// run cli
os.Exit(cli.Run())

// handlers for each command
func startHandler(c *broccli.CLI) int {
fmt.Fprint(os.Stdout, "Starting with file %s...", c.Flag("words"))
return 0
}

```
go get -u github.com/mikolajgs/broccli
func versionHandler(c *broccli.CLI) int {
fmt.Fprintf(os.Stdout, VERSION+"\n")
return 0
}
```

### Example
## Structs explained
### CLI
The main `CLI` object has three arguments such as name, description and author. These guys are displayed when syntax is printed out.

Let's start with an example covering everything. First, let's create main
`CLI` instance and commands:
### Commands
Method `AddCmd` creates a new command which has the following properties.

```
func main() {
myCLI := broccli.NewCLI("Example", "App", "Author <[email protected]>")
print := myCLI.AddCmd("print", "Prints out flags", func(c *CLI) int {
fmt.Fprintf(os.Stdout, "Printing on the screen:\n%s\n%s\n\n", c.Flag("text1"), c.Arg("text2"))
return 2
})
template := myCLI.AddCmd("template", "Process template", func(c *CLI) int {
fmt.Fprintf(os.Stdout, "Do something here")
return 0
})
start := myCLI.AddCmd("start", "Start the game", func(c *CLI) int {
fmt.Fprintf(os.Stdout, "Do something here")
return 0
})
}
```
* a `name`, used to call it
* short `description` - few words to display what the command does on the syntax screen
* `handler` function that is executed when the command is called and all its flags and argument are valid

Next, let's add flags, args and required environments variables to our commands:
#### Command Options
Optionally, after command flags and arguments are successfully validated, and just before the execution of `handler`, additional code (func) can be executed. This can be passed as a last argument.

```go
cmd := cli.AddCmd("start", "Starts the game", startHandler,
broccli.OnPostValidation(func(c *broccli.Cmd) {
// do something, even with the command
}),
)
```
print.AddFlag("text1", "a", "Text", "Text to print", TypeString, IsRequired)
print.AddFlag("text2", "b", "Alphanum with dots", "Can have dots", TypeAlphanumeric, AllowDots)
// If '-r' is passed, the '--text2'/'-b' flag is required
print.AddFlag("make-text2-required", "r", "", "Make alphanumdots required", TypeBool, 0, OnTrue(func(c *Cmd) {
c.flags["text2"].flags = c.flags["text2"].flags | IsRequired
}))
template.AddFlag("template", "t", "filepath", "Path to template file", TypePathFile, IsExistent|IsRequired)
template.AddFlag("file-output", "o", "filepath", "Output to a specific file instead of stdout", TypePathFile, 0)
template.AddFlag("number", "n", "int", "Number necessary for initialisation", TypeInt, IsRequired)
start.AddFlag("verbose", "v", "", "Verbose mode", TypeBool, 0)
start.AddFlag("username", "u", "username", "Username", TypeAlphanumeric, AllowDots|AllowUnderscore|IsRequired)
start.AddFlag("threshold", "", "1.5", "Threshold, default 1.5", TypeFloat, 0)
start.AddArg("input", "FILE", "Path to a file", TypePathFile, IRequired)
start.AddArg("difficulty", "DIFFICULTY", "Level of difficulty (1-5), default 3", TypeInt, 0)

See `cmd_options.go` for all available options.

### Flags and Arguments
Each command can have arguments and flags, as shown below.

```txt
program some-command -f flag1 -g flag2 ARGUMENT1 ARGUMENT2
```

One of the arguments to AddFlag, AddArg or AddEnvVar is type of the value. It can be one of the Type* consts, eg.
TypeInt, TypeBool, TypeString, TypePathFile etc.
To setup a flag in a command, method `AddFlag` is used. It takes the following arguments:

Just next to the type, there is an argument that can contain additional validation flags, such as:
* `name` and `alias` that are used to call the flag (eg. `--help` and `-h`, without the hyphens in the func args)
* `valuePlaceholder`, a placeholder that is printed out on the syntax screen, eg. in `-f PATH_TO_FILE` it is the `PATH_TO_FILE`
* `description` - few words telling what the command does (syntax screen again)
* `types`, an int64 value that defines the value type, currently one of `TypeString`, `TypeBool`, `TypeInt`, `TypeFloat`, `TypeAlphanumeric` or `TypePathFile` (see `flags.go` for more information)
* `flags`, an int64 value containing validation requirement, eg. `IsRequired|IsExistent|IsDirectory` could be used with `TypePathFile` to require the flag to be a non-empty path to an existing directory (again, navigate to `flags.go` for more detailed information)

* IsRequired when flag/arg is required;
* AllowMultipleValues if flag/arg can have have multiple values, eg. 1,2,3, separated by comma or another character (there are flags for that such as SeparatorSemiColon etc.);
* IsExistent which will cause a flag/arg to be checked if it exists;
* IsRegularFile, IsDirectory, IsValidJSON and so on...
Optionally, a function can be attached to a boolean flag that is triggered when a flag is true. The motivation behind that was a use case when setting a certain flag to true would make another string flag required. However, it's not recommended to be used.

Check flags.go for more information on flag types.
To add an argument for a command, method `AddArg` shall be used. It has almost the same arguments, apart from the fact that `alias` is not there.

And in the end of `main()` func:
### Environment variables to check
Command may require environment variables. `AddEnvVar` can be called to setup environment variables that should be verified before running the command. For example, a variable might need to contain a path to an existing regular file.

### Accessing flag and arg values
See sample code that does that below.

```go
func startHandler(c *broccli.CLI) int {
fmt.Fprint(os.Stdout, "Starting with level %s...", c.Arg("level"))
fmt.Fprint(os.Stdout, "Writing moves to file %s...", c.Flag("somefile"))
return 0
}
```
os.Exit(myCLI.Run())
```

`level` and `somefile` are `name`s of the argument (sometimes they are uppercase) and flag.

## Features
- [X] Flags and arguments support
- [X] Validation for basic value types such as integer, float, string, bool
- [X] Additional value types of alpha-numeric and file path
- [X] Validation for multiple values, separated with colon or semicolon, eg. `-t val1,val2,val3`
- [X] Check for file existence and its type (can be directory, regular file or other)
- [X] Post validation hook
- [X] Boolean flag on-true hook before validation
43 changes: 21 additions & 22 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ type CLI struct {
parsedArgs map[string]string
}

// NewCLI returns pointer to a new CLI instance with specified name, description and author. All these are used when
// displaying syntax help.
func NewCLI(n string, d string, a string) *CLI {
// NewCLI returns pointer to a new CLI instance. Name, description and author are displayed on the syntax screen.
func NewCLI(name string, description string, author string) *CLI {
c := &CLI{
name: n,
desc: d,
author: a,
name: name,
desc: description,
author: author,
cmds: map[string]*Cmd{},
envVars: map[string]*param{},
parsedFlags: map[string]string{},
Expand All @@ -43,41 +42,41 @@ func NewCLI(n string, d string, a string) *CLI {
// AddCmd returns pointer to a new command with specified name, description and handler. Handler is a function that
// gets called when command is executed.
// Additionally, there is a set of options that can be passed as arguments. Search for cmdOption for more info.
func (c *CLI) AddCmd(n string, d string, h func(cli *CLI) int, opts ...cmdOption) *Cmd {
c.cmds[n] = &Cmd{
name: n,
desc: d,
func (c *CLI) AddCmd(name string, description string, handler func(cli *CLI) int, opts ...cmdOption) *Cmd {
c.cmds[name] = &Cmd{
name: name,
desc: description,
flags: map[string]*param{},
args: map[string]*param{},
envVars: map[string]*param{},
handler: h,
handler: handler,
options: cmdOptions{},
}
for _, o := range opts {
o(&(c.cmds[n].options))
o(&(c.cmds[name].options))
}
return c.cmds[n]
return c.cmds[name]
}

// AddEnvVar returns pointer to a new environment variable that is required to run every command.
// Method requires name, eg. MY_VAR, and description.
func (c *CLI) AddEnvVar(n string, d string) {
c.envVars[n] = &param{
name: n,
desc: d,
func (c *CLI) AddEnvVar(name string, description string) {
c.envVars[name] = &param{
name: name,
desc: description,
flags: IsRequired,
options: paramOptions{},
}
}

// Flag returns value of flag.
func (c *CLI) Flag(n string) string {
return c.parsedFlags[n]
func (c *CLI) Flag(name string) string {
return c.parsedFlags[name]
}

// Arg returns value of arg.
func (c *CLI) Arg(n string) string {
return c.parsedArgs[n]
func (c *CLI) Arg(name string) string {
return c.parsedArgs[name]
}

// Run parses the arguments, validates them and executes command handler.
Expand Down Expand Up @@ -282,7 +281,7 @@ func (c *CLI) processArgs(cmd *Cmd, as []string, args []string) int {

err := cmd.args[n].validateValue(v)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s %s: %s\n", c.getParamTypeName(ParamArg), cmd.args[n].helpValue, err.Error())
fmt.Fprintf(os.Stderr, "ERROR: %s %s: %s\n", c.getParamTypeName(ParamArg), cmd.args[n].valuePlaceholder, err.Error())
cmd.printHelp()
return 1
}
Expand Down
58 changes: 29 additions & 29 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,62 +28,62 @@ type Cmd struct {
// Method requires name (eg. 'data' for '--data', alias (eg. 'd' for '-d'), placeholder for the value displayed on the
// 'help' screen, description, type of the value and additional validation that is set up with bit flags, eg. IsRequired
// or AllowMultipleValues. If no additional flags are required, 0 should be used.
func (c *Cmd) AddFlag(n string, a string, hv string, d string, t int64, f int64, opts ...paramOption) {
func (c *Cmd) AddFlag(name string, alias string, valuePlaceholder string, description string, types int64, flags int64, opts ...paramOption) {
if c.flags == nil {
c.flags = map[string]*param{}
}
c.flags[n] = &param{
name: n,
alias: a,
desc: d,
helpValue: hv,
valueType: t,
flags: f,
c.flags[name] = &param{
name: name,
alias: alias,
desc: description,
valuePlaceholder: valuePlaceholder,
valueType: types,
flags: flags,
options: paramOptions{},
}
for _, o := range opts {
o(&(c.flags[n].options))
o(&(c.flags[name].options))
}
}

// AddArg adds an argument to a command and returns a pointer to Param instance. It is the same as adding flag except
// it does not have an alias.
func (c *Cmd) AddArg(n string, hv string, d string, t int64, f int64, opts ...paramOption) {
func (c *Cmd) AddArg(name string, valuePlaceholder string, description string, types int64, flags int64, opts ...paramOption) {
if c.argsIdx > 9 {
log.Fatal("Only 10 arguments are allowed")
}
if c.args == nil {
c.args = map[string]*param{}
}
c.args[n] = &param{
name: n,
desc: d,
helpValue: hv,
valueType: t,
flags: f,
c.args[name] = &param{
name: name,
desc: description,
valuePlaceholder: valuePlaceholder,
valueType: types,
flags: flags,
options: paramOptions{},
}
if c.argsOrder == nil {
c.argsOrder = make([]string, 10)
}
c.argsOrder[c.argsIdx] = n
c.argsOrder[c.argsIdx] = name
c.argsIdx++
for _, o := range opts {
o(&(c.args[n].options))
o(&(c.args[name].options))
}
}

// AddEnvVar adds a required environment variable to a command and returns a pointer to Param. It's arguments are very
// similar to ones in previous AddArg and AddFlag methods.
func (c *Cmd) AddEnvVar(n string, d string, t int64, f int64, opts ...paramOption) {
func (c *Cmd) AddEnvVar(name string, description string, types int64, flags int64, opts ...paramOption) {
if c.envVars == nil {
c.envVars = map[string]*param{}
}
c.envVars[n] = &param{
name: n,
desc: d,
valueType: t,
flags: f,
c.envVars[name] = &param{
name: name,
desc: description,
valueType: types,
flags: flags,
options: paramOptions{},
}
}
Expand Down Expand Up @@ -132,9 +132,9 @@ func (c *Cmd) sortedEnvVars() []string {

// PrintHelp prints command usage information to stdout file.
func (c *Cmd) printHelp() {
fmt.Fprintf(os.Stdout, fmt.Sprintf("\nUsage: %s %s [FLAGS]%s\n\n", path.Base(os.Args[0]), c.name,
c.argsHelpLine()))
fmt.Fprintf(os.Stdout, fmt.Sprintf("%s\n", c.desc))
fmt.Fprintf(os.Stdout, "\nUsage: %s %s [FLAGS]%s\n\n", path.Base(os.Args[0]), c.name,
c.argsHelpLine())
fmt.Fprintf(os.Stdout, "%s\n", c.desc)

if len(c.envVars) > 0 {
fmt.Fprintf(os.Stdout, "\nRequired environment variables:\n")
Expand Down Expand Up @@ -182,9 +182,9 @@ func (c *Cmd) argsHelpLine() string {
n := c.argsOrder[i]
arg := c.args[n]
if arg.flags&IsRequired > 0 {
sr += " " + arg.helpValue
sr += " " + arg.valuePlaceholder
} else {
so += " [" + arg.helpValue + "]"
so += " [" + arg.valuePlaceholder + "]"
}
}
}
Expand Down
Loading

0 comments on commit b49c328

Please sign in to comment.