gosif is a tool that helps you to create a simple CLI for your Go app.
gosif is a simple and lite tool that generates CLI for Go apps. More precisely, it generates all the necessary functions to pass arguments to your app via a CLI and to check their correctness.
Go is a great language. Having a codebase written entirely in Go it's a shame to have to use bash, or python, or whatnot for scripting. Initially gosif was designed to allow developers to write scripts without wasting time for typing in a lot of bolierplate for parsing, checking and passing arguments to the scripts functions.
At the same time, we wanted to create a very light tool, without tons of dependencies to maintain. As a result gosif (as well as the code that it generates) depends only on the standard library.
gosif is an opinionated tool that does not allow to build a rich CLI out of the box. We wanted to keep gosif simple: there is no configuration whatsoever. That being said, you can tweak the generated files as you want, but the interface that gosif generates is rather succinct.
- Quick start
- How to use gosif
- How gosif processes your application
- Generated help messages
- Argument-types
- License
Let's see gosif in action. Here is a simple, "Hello, World!" example to outline what gosif does. Let's say you want to write an app that joins the passed arguments into a string and optionally converts the result to uppercase. Such a function may look like:
func PrintStringMaybeUpper(parts []string, upper bool) {
str := strings.Join(parts, " ")
if upper {
str = strings.ToUpper(str)
}
fmt.Println(str)
}To generate an interface for it we pass the directory that contains this file to gosif:
gosif maybe_upper_stringgosif generates a file main.gen.go with a main function that parses the CLI arguments for your application and passes it to PrintStringMaybeUpper. Running the app prints:
go run . PrintStringMaybeUpper --parts Hello gosif ! --upper
> HELLO GOSIF !gosif also generates validators and help messages for your functions. So, if you forget to pass the expected argument --parts you get a reminder:
go run . PrintStringMaybeUpper --upper
> [ERR]: a required flag "-parts" was not passed
> Function PrintStringMaybeUpper
> Required options:
> --parts []string
> Available options:
> --parts []string
> --upper boolThe code for this example is in examples/readme/maybe_upper_string/myscript.go
To use gosif for your application you need to:
- install
gosif:
go get github.com/SergeyShpak/gosif- pass the name of the folder that contains your application to
gosif:
gosif my-app/- build and run your application or use
go run:
cd my-app/ && go build . && ./my-app <args>
# or
cd my-app && go run . <args>If you run gosif on the directory that already contains a file main.gen.go, gosif will scan this file. If it contains only functions main(), gosif() and functions prefixed with gosif_, gosif rewrites it. If gosif finds other functions inside the file it shows the error message and cancels the code generation.
gosif generates CLI for executables. It scans through the package main, finds all the exportable functions and tries to generate interfaces for them. It skips functions that it cannot process.
A function that gosif can process:
- has arguments of types that are listed in the Argument types section only
- does not return anything
- is exportable (its name starts with a capital letter)
- are located in the
mainpackage
If the main package does not contain the main() function yet, gosif generates it. Otherwise, gosif generates a function gosif() that should be manually added to main().
gosif generates help messages for your application functions that indicate names of the available functions, names of the function flags and types of the expected arguments.
To see the list of executable functions of your app run:
go run . help
> The following functions are available:
> ...This message is also shown when trying to run a function that does not exist:
go run . InexistentFunc
> [ERR]: unknown script InexistentFunc
> The following functions are available:
> ...To see the help message of a function, run the function with the argument help:
go run . MyFunc help
> Function MyFunc:
> Required options:
> ...
> Available options:
> ...This message is also shown when passing a bad argument to the function:
go run . OnlyInts --n 3.14
> [ERR]: cast failed: failed to cast 12.34 to int: strconv.ParseInt: parsing "3.14": invalid syntax
> Function OnlyInts:
> ...gosif can generate interfaces for functions with arguments of the following types:
- string
- byte
- rune
- bool
- int
- int8
- int16
- int32
- int64
- uint
- uint8
- uint16
- uint32
- uint64
- float32
- float64
- complex64
- complex128
- error
You can find the code from this section in examples/readme/types_demo/myscript.go
We use this function in the example:
func StringType(s string) {
fmt.Println(s)
}Passing a string argument results in:
go run . StringType --s hello!
> hello!gosif functions pass the string argument as is, ignoring escape sequences.
go run . StringType --s 'Hello!\n'
> Hello!\nThe only exception to this are double quotes. A double-quoted chain of characters is treated as a single argument.
go run . StringType --s "Hello, gosif!"
> Hello, gosif!We use this function in the example:
func ByteType(b byte) {
fmt.Println(b)
}A byte argument should be passed as a decimal, 8-bit unsigned integer number:
go run . ByteType --b 42
> 42We use this function in the example:
func RuneType(r rune) {
fmt.Println(string(r))
}A rune argument should be passed as a utf8 character:
go run . RuneType --r 😀
> 😀We use this function in the example:
func BoolType(bt bool, bf bool) {
fmt.Println(bt, bf)
}There are three formats for boolean arguments:
- You can pass the boolean flag without arguments for a
truevalue, or omit it for afalsevalue:
go run . BoolType --bt
> true false- You can pass a single letter
tfor atruevalue, orffor afalsevalue:
go run . BoolType --bt t --bf f
> true false- You can pass a word
truefor atruevalue, orfalsefor afalsevalue:
go run . BoolType --bt true --bf false
> true falseThe passed arguments are case-insensitive:
go run . BoolType --bt T --bf False
> true falseUsing arguments that are pointers to bools is slightly different, see pointers section for details.
We use this function in the example:
func SignedIntType(n int, n8 int8, n16 int16, n32 int32, n64 int64) {
fmt.Printf("n: %d, n8: %d, n16: %d, n32: %d, n64: %d\n", n, n8, n16, n32, n64)
}A passed string argument is converted to a signed integer by strconv.ParseInt (the base of the integer is implied by the passed string prefix), see its documentation to learn about accepted string formats:
go run . SignedIntType --n 0b101010 --n8 -8 --n16 +16 --n32 040 --n64 -0x40
> n: 42, n8: -8, n16: 16, n32: 32, n64: -64We use this function in the example:
func UnsignedIntType(n uint, n8 uint8, n16 uint16, n32 uint32, n64 uint64) {
fmt.Printf("n: %d, n8: %d, n16: %d, n32: %d, n64: %d\n", n, n8, n16, n32, n64)
}A passed string argument is converted to an unsigned integer by strconv.ParseUint (the base of the integer is implied by the passed string prefix), see its documentation to learn about accepted string formats:
go run . UnsignedIntType --n 42 --n8 0b1000 --n16 020 --n32 0o40 --n64 0x40
> n: 42, n8: 8, n16: 16, n32: 32, n64: 64We use this function in the example:
func FloatType(f32 float32, f64 float64) {
fmt.Printf("f32: %.3f, f64: %.3f\n")
}A passed string argument is converted to a floating-point number by strconv.ParseFloat (the base of the integer is implied by the passed string prefix), see its documentation to learn about accepted string formats:
go run . FloatType --f32 3.14159 --f64 -6.02E+23
> f32: 3.142, f64: -601999999999999995805696.000We use this function in the example:
func ComplexType(c64 complex64, c128 complex128) {
fmt.Printf("c64: (%.3f, %.3fi), c128: (%.3f, %.3fi)\n", real(c64), imag(c64), real(c128), imag(c128)
}There are two valid ways to represent a complex number:
- If one of the complex number parts (real or imaginary) is zero, then you can pass the other part directly as the function argument. The imaginary part should have the suffix
i.
go run . ComplexType --c64 42 --c128 -42i
> c64: (42.000, 0.000i), c128: (0.000, -42.000i)- A standard way of representing a complex number is putting the real and imaginary parts between parentheses and separating them with a comma. Parts can be placed in an arbitrary order, but the imaginary part should have the suffix
i. Please note that some shells only accept quoted arguments with parentheses.
go run . ComplexType --c64 "(3.14159, 0i)" --c128 "(-i, 42.123)"
> c64: (3.142, 0.000i), c128: (42.123, -1.000i)We use strconv.ParseFloat to parse the real and imaginary parts, so, to represent them, you can use any float-point numbers syntax valid for this function (see its documentation for details):
go run . ComplexType --c64 "(+Inf, NaNi)" --c128 -6.02E+23i
> c64: (+Inf, NaNi), c128: (0.000, -601999999999999995805696.000i)We use this function in the example:
func ErrorType(e error) {
fmt.Println(e)
}Passing a string as an error argument makes the string the error message:
go run . ErrorType --e "this is an error message"
> this is an error messageYou can use slices and arrays of the available types in your functions definitions:
func MySliceFunc(nums []int) { fmt.Println(nums) }
func MyArrFunc(nums [3]int) { fmt.Println(nums) }In case of a slice, gosif functions treat the passed arguments separated by spaces as the elements of the slice.
go run . MySliceFunc --nums 1 2 3
> [1 2 3]
go run . MySliceFunc --nums
> []The same is valid for arrays arguments:
go run . MyArrFunc --nums 1 2 3
> [1 2 3]However, gosif treats slices and arrays semantically different: gosif functions check that a correct number of arguments were passed for an array argument:
go run . MyArrFunc --nums 1 2
> [ERR]: flag --nums: expected 3 arguments, but got 2As multi-dimensional slices and arrays are currently not supported, gosif skips functions with arguments of such a type. Running gosif on
func MyMultiSliceFunc(nums [][][]int) { fmt.Println(nums) }
func MyMultiArrFunc(nums [2][3]int) { fmt.Println(nums) }results is a warning:
[WARN]: skipping the function MyMultiSliceFunc in examples/readme/slices_and_arrays/myscript.go: failed to parse the parameter "nums": multidimensional parameters are not yet supported
...
[WARN]: skipping the function MyMultiArrFunc in examples/readme/slices_and_arrays/myscript.go: failed to parse the parameter "nums": multidimensional parameters are not yet supportedYou can find the code used in this section in examples/readme/slices_and_arrays/myscript.go
You can use pointers of the available types in your functions definitions:
func MyPointerFunc(num *int) {
if num == nil {
fmt.Println("nil")
} else {
fmt.Println(*num)
}
}gosif functions treat pointer arguments as optional:
go run . MyPointerFunc --num 42
> 42
go run . MyPointerFunc
> nilPassing no arguments to the optional flag, however, results in an error:
go run . MyPointerFunc --num
> [ERR]: could not get the argument passed to the flag "--num": no arguments passed
> ...gosif correctly treats multiple levels of indirection (pointers to pointers to pointers...):
func MyManyPointersFunc(num ***int) {
if num == nil {
fmt.Println("nil")
} else {
fmt.Println(***num)
}
}As gosif does not treat the passed arguments differently, we may check only if the first pointer is nil in MyManyPointersFunc. With gosif you cannot pass a pointer to a nil pointer.
Running the example with multiple pointers gives the same result as for the one with the single pointer:
go run . MyManyPointersFunc --num 42
> 42
go run . MyManyPointersFunc
> nil
go run . MyManyPointersFunc --num
> [ERR]: could not get the argument passed to the flag "--num": no arguments passed
> ...Working with pointers to booleans is different then working with direct boolean values: you may no longer omit the boolean flag to get a false value, you need to specify that the argument is false explicitly. Running the function
func BoolPointerFunc(b *bool) {
if b == nil {
fmt.Println("nil")
} else {
fmt.Println(*b)
}
}outputs
go run . BoolPointerFunc --b
> true
go run . BoolPointerFunc
> nil
go run . BoolPointerFunc --b f
> falseYou can find the code used in this section in examples/readme/pointers/myscript.go
gosif treats arguments that are pointer to slices of some type as optional. Arguments that are slices of pointers are treated the same way as slices of direct values, i.e. with gosif it is not possible to pass a slice that contains nils to the function.
Running the function
func MySlicePointerFunc(ps *[]int, sp []*int, psp *[]*int) {
if ps == nil {
fmt.Println("ps: nil")
} else {
fmt.Println("ps: ", *ps)
}
spElems := make([]int, len(sp))
for i, el := range sp {
spElems[i] = *el
}
fmt.Println("sp: ", spElems)
if psp == nil {
fmt.Println("psp: nil")
} else {
pspElems := make([]int, len(*psp))
for i, el := range *psp {
pspElems[i] = *el
}
fmt.Println("psp: ", pspElems)
}
}outputs
go run . MySlicePointerFunc --ps 1 2 3 --sp 1 2 3 --psp 1 2 3
> ps: [1 2 3]
> sp: [1 2 3]
> psp: [1 2 3]Multiple levels of indirection (pointers to pointers) are allowed, you can find more information about them in the section pointers.
You can find the code used in this section in examples/readme/slices_arrays_pointers/slices_pointers.go
gosif treats arguments that are pointer to arrays of some type as optional. Arguments that are arrays of pointers are treated differently from slices of pointers: elements of such arrays are optional, so the missing elements are set to nil:
Running the function
func MyArrPointerFunc(pa *[3]int, ap [3]*int, pap *[3]*int) {
if pa == nil {
fmt.Println("pa: nil")
} else {
fmt.Println("pa: ", *pa)
}
apElems := make([]string, len(ap))
for i, el := range ap {
if el == nil {
apElems[i] = "nil"
} else {
apElems[i] = fmt.Sprintf("%d", *el)
}
}
fmt.Println("ap: ", apElems)
if pap == nil {
fmt.Println("pap: nil")
} else {
papElems := make([]string, len(*pap))
for i, el := range *pap {
if el == nil {
papElems[i] = "nil"
} else {
papElems[i] = fmt.Sprintf("%d", *el)
}
}
fmt.Println("pap: ", papElems)
}
}outputs
go run . MyArrPointerFunc --ap 1 2 --pap
> pa: nil
> ap: [1 2 nil]
> pap: [nil nil nil]Multiple levels of indirection (pointers to pointers) are allowed, you can find more information about them in the section pointers.
You can find the code used in this section in examples/readme/slices_arrays_pointers/arrays_pointers.go
See the LICENSE file for license rights and limitations (MIT).