├── .gitignore ├── example.gif ├── circle.yml ├── glide.yaml ├── examples ├── spiderpig.yaml ├── ping.yaml ├── mycliapp.yaml └── fileutil.yml ├── Makefile ├── funcs.go ├── cli.yml ├── glide.lock ├── command_test.go ├── flag_test.go ├── command.go ├── flag.go ├── main_test.go ├── main.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextrevision/fauxcli/HEAD/example.gif -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - go vet 4 | - go test -v 5 | - make build-ci 6 | post: 7 | - mv fauxcli_* $CIRCLE_ARTIFACTS/ 8 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/nextrevision/fauxcli 2 | import: 3 | - package: gopkg.in/yaml.v2 4 | - package: github.com/Pallinder/go-randomdata 5 | - package: github.com/leekchan/gtf 6 | -------------------------------------------------------------------------------- /examples/spiderpig.yaml: -------------------------------------------------------------------------------- 1 | name: spiderpig 2 | help: does whatever a spiderpig does 3 | commands: 4 | - name: swing 5 | help: swings from a web 6 | output: | 7 | I can't do that, I'm a pig! 8 | - name: plop 9 | help: super secret maneuver 10 | output: | 11 | Look out! 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST?= 2 | 3 | default: test 4 | 5 | test: 6 | go test -v $(TEST) $(TESTARGS) 7 | go vet -v 8 | 9 | cover: 10 | go test $(TEST) -covermode=count -coverprofile=coverage.out 11 | go tool cover -html=coverage.out 12 | rm coverage.out 13 | 14 | build: 15 | gox -osarch="darwin/386 darwin/amd64 linux/386 linux/amd64" 16 | 17 | build-ci: 18 | go get github.com/mitchellh/gox 19 | sudo chown -R ${USER}: /usr/local/go 20 | $(MAKE) build 21 | -------------------------------------------------------------------------------- /funcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/Pallinder/go-randomdata" 4 | 5 | func randomFullName() string { 6 | return randomdata.FullName(randomdata.RandomGender) 7 | } 8 | 9 | func toString(v interface{}) string { 10 | r := v.(*string) 11 | return *r 12 | } 13 | 14 | func toBool(v interface{}) bool { 15 | r := v.(*bool) 16 | return *r 17 | } 18 | 19 | func toInt(v interface{}) int { 20 | r := v.(*int) 21 | return *r 22 | } 23 | 24 | func toFloat(v interface{}) float64 { 25 | r := v.(*float64) 26 | return *r 27 | } 28 | 29 | func count(n int) []struct{} { 30 | return make([]struct{}, n) 31 | } 32 | -------------------------------------------------------------------------------- /cli.yml: -------------------------------------------------------------------------------- 1 | name: mycliapp 2 | aliases: 3 | - myapp 4 | - app 5 | help: does something cool 6 | output: | 7 | Hello world 8 | commands: 9 | - name: subcommand1 10 | aliases: [] 11 | help: a subcommand 12 | output: |- 13 | {{ if .Flags.upper.Bool -}} 14 | HELLO FROM SC1! 15 | {{ else -}}Hello from SC1!{{ end -}} 16 | commands: [] 17 | flags: 18 | - name: upper 19 | short: u 20 | help: converts output to uppercase 21 | global: false 22 | default: null 23 | type: bool 24 | flags: 25 | - name: debug 26 | short: d 27 | help: enables debugging 28 | global: true 29 | default: false 30 | type: bool 31 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 5633436e3c61d8edb01e1bc6c2a185be890b86d79517d4dc0ac4db6a8e7fab53 2 | updated: 2016-06-09T15:28:06.27176339-04:00 3 | imports: 4 | - name: github.com/inconshreveable/mousetrap 5 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 6 | - name: github.com/leekchan/gtf 7 | version: 882e96b937c8506c67b5adf98ff1f84cd7b7b93a 8 | - name: github.com/Pallinder/go-randomdata 9 | version: 104cc800bd6434e70cb0c3ad08c4541f4f2eb48b 10 | - name: github.com/spf13/cobra 11 | version: 1238ba19d24b0b9ceee2094e1cb31947d45c3e86 12 | - name: github.com/spf13/pflag 13 | version: cb88ea77998c3f024757528e3305022ab50b43be 14 | - name: gopkg.in/yaml.v2 15 | version: a83829b6f1293c91addabc89d0571c246397bbf4 16 | devImports: [] 17 | -------------------------------------------------------------------------------- /examples/ping.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ping 3 | help: sends an ICMP echo request to a host 4 | flags: 5 | - name: count 6 | help: number of pings to send 7 | short: c 8 | default: 5 9 | type: int 10 | output: | 11 | {{ if .Args | lengthis 0 -}} 12 | ERROR: must pass a host to ping 13 | {{ else -}} 14 | PING {{ .Args | first }} ({{ ipaddress }}): 56 data bytes 15 | {{ range $i, $_ := count .Flags.count.Int -}} 16 | 64 bytes from {{ ipaddress }}: icmp_seq={{ $i }} ttl=53 time=12.141 ms 17 | {{ end -}} 18 | --- {{ .Args | first }} ping statistics --- 19 | {{ .Flags.count.Int }} packets transmitted, {{ .Flags.count.Int }} packets received, 0.0% packet loss 20 | round-trip min/avg/max/stddev = 11.749/12.050/12.474/0.247 ms 21 | {{ end -}} 22 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateCommand(t *testing.T) { 10 | assert.NotNil(t, validateCommand(Command{})) 11 | assert.NotNil(t, validateCommand(Command{Name: "test"})) 12 | assert.NotNil(t, validateCommand(Command{Help: "test"})) 13 | assert.Nil(t, validateCommand(Command{Name: "test", Help: "test"})) 14 | } 15 | 16 | func TestSetCommand(t *testing.T) { 17 | c := setCommand(Command{ 18 | Name: "app", 19 | Help: "help text", 20 | Aliases: []string{"app1", "app2"}, 21 | }) 22 | 23 | assert.Equal(t, c.Use, "app", "Not equal") 24 | assert.Equal(t, c.Short, "help text", "Not equal") 25 | assert.Equal(t, c.Long, "help text", "Not equal") 26 | assert.Equal(t, c.Aliases, []string{"app1", "app2"}, "Not equal") 27 | } 28 | -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateFlag(t *testing.T) { 10 | flags := map[string]Flag{} 11 | flags["flag1"] = Flag{ 12 | Name: "flag1", 13 | Help: "help text", 14 | Short: "f", 15 | } 16 | 17 | assert.NotNil(t, validateFlag(Flag{}, flags)) 18 | assert.NotNil(t, validateFlag(Flag{Name: "test"}, flags)) 19 | assert.NotNil(t, validateFlag(Flag{Help: "test"}, flags)) 20 | assert.NotNil(t, validateFlag(Flag{Name: "flag1", Help: "h"}, flags)) 21 | assert.NotNil(t, validateFlag(Flag{Name: "flag2", Help: "h", Short: "f"}, flags)) 22 | assert.Nil(t, validateFlag(Flag{Name: "test", Help: "test"}, flags)) 23 | } 24 | 25 | func TestSetStringFlag(t *testing.T) { 26 | sVal := "val" 27 | bVal := true 28 | iVal := 1 29 | fVal := 1.2 30 | s := Flag{Name: "flag", Help: "h", Short: "f", value: &sVal, Type: "string"} 31 | b := Flag{Name: "flag", Help: "h", Short: "f", value: &bVal, Type: "bool"} 32 | i := Flag{Name: "flag", Help: "h", Short: "f", value: &iVal, Type: "int"} 33 | f := Flag{Name: "flag", Help: "h", Short: "f", value: &fVal, Type: "float"} 34 | 35 | assert.Equal(t, s.String(), "val", "Should be equal") 36 | assert.Equal(t, s.Value().(*string), &sVal, "Should be equal") 37 | assert.Equal(t, b.Bool(), true, "Should be equal") 38 | assert.Equal(t, b.Value().(*bool), &bVal, "Should be equal") 39 | assert.Equal(t, i.Int(), 1, "Should be equal") 40 | assert.Equal(t, i.Value().(*int), &iVal, "Should be equal") 41 | assert.Equal(t, f.Float(), 1.2, "Should be equal") 42 | assert.Equal(t, f.Value().(*float64), &fVal, "Should be equal") 43 | } 44 | -------------------------------------------------------------------------------- /examples/mycliapp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # (required) name of the command 3 | name: mycliapp 4 | 5 | # (required) the help text for the command (displayed with -h) 6 | help: does something cool 7 | 8 | # additional command aliases 9 | aliases: ["myapp", "app"] 10 | 11 | # output to print when the command is run 12 | # if this key is omitted, the command will act as a 13 | # parent to any subcommands, essentially doing nothing 14 | # but printing the help text 15 | output: | 16 | Hello, World! 17 | 18 | # flags available to the command 19 | flags: 20 | # (required) long name of the flag (--debug) 21 | - name: debug 22 | 23 | # (required) help text for the flag 24 | help: enables debugging 25 | 26 | # short name for the flag (-d) 27 | short: d 28 | 29 | # default value of the flag 30 | default: false 31 | 32 | # make the flag globally available 33 | global: true 34 | 35 | # the type of the value (default string) 36 | # available types: 37 | # - string 38 | # - bool 39 | type: bool 40 | 41 | # subcommands (nested from all the options above) 42 | commands: 43 | - name: subcommand1 44 | help: a subcommand 45 | flags: 46 | - name: upper 47 | help: converts output to uppercase 48 | short: u 49 | type: bool 50 | output: | 51 | {{ if .Flags.upper.Bool -}} 52 | HELLO FROM SC1! 53 | {{ else -}} 54 | Hello from SC1! 55 | {{ end -}} 56 | - name: subcommand2 57 | help: another subcommand with children 58 | commands: 59 | - name: child1 60 | help: the first child command 61 | output: | 62 | Hello from child1 63 | - name: child2 64 | help: the second child command 65 | output: | 66 | Hello from child2 67 | -------------------------------------------------------------------------------- /examples/fileutil.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: fileutil 3 | help: performs operations on files and directories 4 | flags: 5 | - name: verbose 6 | short: v 7 | type: bool 8 | help: enable verbose logging 9 | global: true 10 | commands: 11 | - name: list 12 | aliases: ["ls"] 13 | help: lists all files and folders in a directory 14 | flags: 15 | - name: dirs 16 | short: d 17 | type: bool 18 | help: list dirs 19 | - name: files 20 | short: f 21 | type: bool 22 | help: list files 23 | - name: all 24 | short: a 25 | type: bool 26 | help: include hidden files and folders 27 | output: | 28 | {{ if not .Flags.files.Bool -}} 29 | {{ if .Flags.all.Bool -}} 30 | d: . 31 | d: .. 32 | {{ end -}} 33 | {{ range $_, $_ := count 3 -}} 34 | d: {{ name | lower }} 35 | {{ end -}} 36 | {{ end -}} 37 | {{ if not .Flags.dirs.Bool -}} 38 | {{ range $_, $_ := count 3 -}} 39 | f: {{ name | lower }} 40 | {{ end -}} 41 | {{ end -}} 42 | - name: new 43 | aliases: ["create", "make"] 44 | help: create a new resource 45 | commands: 46 | - name: file 47 | help: creates a new file resource 48 | output: | 49 | {{ if .Args | lengthis 0 -}} 50 | ERROR: must pass the name of the resource to create 51 | {{ else -}} 52 | Created file: {{ .Args | first }} 53 | {{ end -}} 54 | - name: dir 55 | help: creates a new directory resource 56 | output: | 57 | {{ if .Args | lengthis 0 -}} 58 | ERROR: must pass the name of the resource to create 59 | {{ else -}} 60 | Created dir: {{ .Args | first }} 61 | {{ end -}} 62 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "log" 7 | "os" 8 | 9 | "github.com/Pallinder/go-randomdata" 10 | "github.com/leekchan/gtf" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // Command is the basic command structure 15 | type Command struct { 16 | Name string `yaml:"name"` 17 | Aliases []string `yaml:"aliases"` 18 | Help string `yaml:"help"` 19 | Output string `yaml:"output"` 20 | Commands []Command `yaml:"commands"` 21 | Flags []Flag `yaml:"flags"` 22 | } 23 | 24 | func validateCommand(command Command) error { 25 | if command.Name == "" { 26 | return fmt.Errorf("Missing name for command: %+v", command) 27 | } 28 | 29 | if command.Help == "" { 30 | return fmt.Errorf("Missing help for command: %s", command.Name) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func setCommand(command Command) *cobra.Command { 37 | c := &cobra.Command{Use: command.Name} 38 | if command.Help != "" { 39 | c.Short = command.Help 40 | c.Long = command.Help 41 | } 42 | 43 | if len(command.Aliases) > 0 { 44 | c.Aliases = command.Aliases 45 | } 46 | 47 | if command.Output != "" { 48 | c.Run = func(c *cobra.Command, args []string) { 49 | funcmap := template.FuncMap{ 50 | "name": randomdata.SillyName, 51 | "fullname": randomFullName, 52 | "email": randomdata.Email, 53 | "city": randomdata.City, 54 | "street": randomdata.Street, 55 | "address": randomdata.Address, 56 | "number": randomdata.Number, 57 | "paragraph": randomdata.Paragraph, 58 | "ipaddress": randomdata.IpV4Address, 59 | "day": randomdata.Day, 60 | "month": randomdata.Month, 61 | "date": randomdata.FullDate, 62 | "string": toString, 63 | "bool": toBool, 64 | "int": toInt, 65 | "float": toFloat, 66 | "count": count, 67 | } 68 | gtf.Inject(funcmap) 69 | 70 | data := struct { 71 | Flags map[string]Flag 72 | Args []string 73 | }{ 74 | Flags: flags, 75 | Args: args, 76 | } 77 | 78 | t, err := template.New("output").Funcs(funcmap).Parse(command.Output) 79 | if err != nil { 80 | log.Fatalf("Error parsing %s output: %s", command.Name, err.Error()) 81 | } 82 | 83 | err = template.Must(t, err).Execute(os.Stdout, data) 84 | if err != nil { 85 | log.Fatalf("Error parsing %s output: %s", command.Name, err.Error()) 86 | } 87 | } 88 | } 89 | 90 | return c 91 | } 92 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/pflag" 7 | ) 8 | 9 | // Flag is the basic flag structure 10 | type Flag struct { 11 | Name string `yaml:"name"` 12 | Short string `yaml:"short"` 13 | Help string `yaml:"help"` 14 | Global bool `yaml:"global"` 15 | Default interface{} `yaml:"default"` 16 | Type string `yaml:"type"` 17 | value interface{} 18 | } 19 | 20 | func validateFlag(flag Flag, flags map[string]Flag) error { 21 | if flag.Name == "" { 22 | return fmt.Errorf("Missing name for flag: %+v", flag) 23 | } 24 | 25 | if flag.Help == "" { 26 | return fmt.Errorf("Missing help for flag: %s", flag.Name) 27 | } 28 | 29 | for _, f := range flags { 30 | if f.Name == flag.Name { 31 | return fmt.Errorf("Duplicate flag found for %s", flag.Name) 32 | } 33 | 34 | if flag.Short != "" && f.Short == flag.Short { 35 | return fmt.Errorf("Duplicate flag short name found for %s", flag.Name) 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func setStringFlag(f Flag, fs *pflag.FlagSet) { 43 | f.value = new(string) 44 | flags[f.Name] = f 45 | 46 | d := "" 47 | if f.Default != nil { 48 | d = f.Default.(string) 49 | } 50 | 51 | fs.StringVarP( 52 | flags[f.Name].value.(*string), 53 | f.Name, 54 | f.Short, 55 | d, 56 | f.Help, 57 | ) 58 | } 59 | 60 | func setBoolFlag(f Flag, fs *pflag.FlagSet) { 61 | f.value = new(bool) 62 | flags[f.Name] = f 63 | 64 | d := false 65 | if f.Default != nil { 66 | d = f.Default.(bool) 67 | } 68 | 69 | fs.BoolVarP( 70 | flags[f.Name].value.(*bool), 71 | f.Name, 72 | f.Short, 73 | d, 74 | f.Help, 75 | ) 76 | } 77 | 78 | func setIntFlag(f Flag, fs *pflag.FlagSet) { 79 | f.value = new(int) 80 | flags[f.Name] = f 81 | 82 | d := 0 83 | if f.Default != nil { 84 | d = f.Default.(int) 85 | } 86 | 87 | fs.IntVarP( 88 | flags[f.Name].value.(*int), 89 | f.Name, 90 | f.Short, 91 | d, 92 | f.Help, 93 | ) 94 | } 95 | 96 | func setFloatFlag(f Flag, fs *pflag.FlagSet) { 97 | f.value = new(float64) 98 | flags[f.Name] = f 99 | 100 | d := 0.0 101 | if f.Default != nil { 102 | d = f.Default.(float64) 103 | } 104 | 105 | fs.Float64VarP( 106 | flags[f.Name].value.(*float64), 107 | f.Name, 108 | f.Short, 109 | d, 110 | f.Help, 111 | ) 112 | } 113 | 114 | // Value returns the generic interface value of a flag 115 | func (f Flag) Value() interface{} { 116 | return f.value 117 | } 118 | 119 | // String returns the string value of a flag 120 | func (f Flag) String() string { 121 | if f.Type == "string" || f.Type == "" { 122 | v := f.value.(*string) 123 | return *v 124 | } 125 | 126 | return "" 127 | } 128 | 129 | // Bool returns the string value of a flag 130 | func (f Flag) Bool() bool { 131 | if f.Type == "bool" { 132 | v := f.value.(*bool) 133 | return *v 134 | } 135 | 136 | return false 137 | } 138 | 139 | // Int returns the int value of a flag 140 | func (f Flag) Int() int { 141 | if f.Type == "int" { 142 | v := f.value.(*int) 143 | return *v 144 | } 145 | 146 | return 0 147 | } 148 | 149 | // Float returns the int value of a flag 150 | func (f Flag) Float() float64 { 151 | if f.Type == "float" { 152 | v := f.value.(*float64) 153 | return *v 154 | } 155 | 156 | return 0 157 | } 158 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestProcessCommands(t *testing.T) { 11 | c := processCommands(Command{ 12 | Name: "mycliapp", 13 | Help: "does something cool", 14 | Aliases: []string{"myapp", "app"}, 15 | Output: "Hello World!\n", 16 | Flags: []Flag{ 17 | Flag{ 18 | Name: "debug", 19 | Help: "enables debugging", 20 | Short: "d", 21 | Default: false, 22 | Global: true, 23 | Type: "bool", 24 | }, 25 | }, 26 | Commands: []Command{ 27 | Command{ 28 | Name: "subcommand1", 29 | Help: "a subcommand", 30 | Flags: []Flag{ 31 | Flag{ 32 | Name: "upper", 33 | Help: "converts output to uppercase", 34 | Short: "u", 35 | Type: "bool", 36 | }, 37 | }, 38 | Output: "{{ if .Flags.upper.Bool -}}\nHELLO FROM SC1!\n{{ else -}}\nHello from SC1!\n{{ end -}}\n", 39 | }, 40 | }, 41 | }) 42 | 43 | assert.Equal(t, c.Use, "mycliapp", "Use should be set to mycliapp") 44 | assert.Equal(t, c.Short, "does something cool", "Short is not equal") 45 | assert.Equal(t, c.Long, "does something cool", "Long is not equal") 46 | assert.Equal(t, c.Aliases, []string{"myapp", "app"}, "Aliases is not equal") 47 | assert.Equal(t, len(flags), 2, "Flags length is not equal") 48 | 49 | commands := c.Commands() 50 | assert.Equal(t, len(commands), 1, "Commands length is not equal") 51 | assert.Equal(t, commands[0].Use, "subcommand1", "Not equal") 52 | assert.Equal(t, commands[0].Short, "a subcommand", "Not equal") 53 | assert.Equal(t, commands[0].Long, "a subcommand", "Not equal") 54 | 55 | assert.Nil(t, c.Execute()) 56 | } 57 | 58 | func TestFileExists(t *testing.T) { 59 | if !fileExists("examples/fileutil.yml") { 60 | t.Fatal("should be true") 61 | } 62 | if fileExists("examples/doesnotexist.yml") { 63 | t.Fatal("should be false") 64 | } 65 | } 66 | 67 | func TestLoadCLIYAML(t *testing.T) { 68 | want := Command{ 69 | Name: "mycliapp", 70 | Help: "does something cool", 71 | Aliases: []string{"myapp", "app"}, 72 | Output: "Hello, World!\n", 73 | Flags: []Flag{ 74 | Flag{ 75 | Name: "debug", 76 | Help: "enables debugging", 77 | Short: "d", 78 | Default: false, 79 | Global: true, 80 | Type: "bool", 81 | }, 82 | }, 83 | Commands: []Command{ 84 | Command{ 85 | Name: "subcommand1", 86 | Help: "a subcommand", 87 | Flags: []Flag{ 88 | Flag{ 89 | Name: "upper", 90 | Help: "converts output to uppercase", 91 | Short: "u", 92 | Type: "bool", 93 | }, 94 | }, 95 | Output: "{{ if .Flags.upper.Bool -}}\nHELLO FROM SC1!\n{{ else -}}\nHello from SC1!\n{{ end -}}\n", 96 | }, 97 | Command{ 98 | Name: "subcommand2", 99 | Help: "another subcommand with children", 100 | Commands: []Command{ 101 | Command{ 102 | Name: "child1", 103 | Help: "the first child command", 104 | Output: "Hello from child1\n", 105 | }, 106 | Command{ 107 | Name: "child2", 108 | Help: "the second child command", 109 | Output: "Hello from child2\n", 110 | }, 111 | }, 112 | }, 113 | }, 114 | } 115 | 116 | c, err := loadCLIYAML("examples/mycliapp.yaml") 117 | 118 | assert.Nil(t, err) 119 | 120 | if !reflect.DeepEqual(c, want) { 121 | t.Errorf("Loaded data not equal, data returned %+v, want %+v", c, want) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | var flags = map[string]Flag{} 14 | 15 | func main() { 16 | filename := "cli.yml" 17 | if fileExists("cli.yaml") { 18 | filename = "cli.yaml" 19 | } else if os.Getenv("CLIMOCK_FILE") != "" { 20 | filename = os.Getenv("CLIMOCK_FILE") 21 | } else if os.Getenv("FAUXCLI_FILE") != "" { 22 | filename = os.Getenv("FAUXCLI_FILE") 23 | } 24 | 25 | if !fileExists(filename) && os.Getenv("FAUXCLI_INIT") == "1" { 26 | if err := initCLIYAML(filename); err != nil { 27 | log.Fatalf("Could not init fauxcli file %s: %s", filename, err) 28 | } 29 | } else if !fileExists(filename) { 30 | log.Fatalf("%s not found", filename) 31 | } 32 | 33 | cli, err := loadCLIYAML(filename) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | err = processCommands(cli).Execute() 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | func processCommands(command Command) *cobra.Command { 45 | cobraCommand := setCommand(command) 46 | if len(command.Commands) > 0 { 47 | for _, c := range command.Commands { 48 | err := validateCommand(c) 49 | if err != nil { 50 | log.Fatalln(err.Error()) 51 | } 52 | 53 | cobraCommand.AddCommand(processCommands(c)) 54 | } 55 | } 56 | 57 | if len(command.Flags) > 0 { 58 | for _, f := range command.Flags { 59 | err := validateFlag(f, flags) 60 | if err != nil { 61 | log.Fatalln(err.Error()) 62 | } 63 | 64 | flagSet := cobraCommand.Flags() 65 | 66 | if f.Global { 67 | flagSet = cobraCommand.PersistentFlags() 68 | } 69 | 70 | switch f.Type { 71 | case "string": 72 | setStringFlag(f, flagSet) 73 | case "bool": 74 | setBoolFlag(f, flagSet) 75 | case "int": 76 | setIntFlag(f, flagSet) 77 | case "float": 78 | setFloatFlag(f, flagSet) 79 | default: 80 | setStringFlag(f, flagSet) 81 | } 82 | } 83 | } 84 | 85 | return cobraCommand 86 | } 87 | 88 | func fileExists(filename string) bool { 89 | _, err := os.Stat(filename) 90 | if err == nil { 91 | return true 92 | } 93 | if os.IsNotExist(err) { 94 | return false 95 | } 96 | return true 97 | } 98 | 99 | func initCLIYAML(filename string) error { 100 | root := Command{ 101 | Name: "mycliapp", 102 | Help: "does something cool", 103 | Aliases: []string{"myapp", "app"}, 104 | Output: "Hello, World!\n", 105 | Flags: []Flag{ 106 | Flag{ 107 | Name: "debug", 108 | Short: "d", 109 | Help: "enables debugging", 110 | Default: false, 111 | Global: true, 112 | Type: "bool", 113 | }, 114 | }, 115 | Commands: []Command{ 116 | Command{ 117 | Name: "subcommand1", 118 | Help: "a subcommand", 119 | Output: "{{ if .Flags.upper.Bool -}}\nHELLO FROM SC1!\n{{ else -}}\nHello from SC1!\n{{ end -}}", 120 | Flags: []Flag{ 121 | Flag{ 122 | Name: "upper", 123 | Short: "u", 124 | Help: "converts output to uppercase", 125 | Type: "bool", 126 | }, 127 | }, 128 | }, 129 | }, 130 | } 131 | 132 | data, err := yaml.Marshal(&root) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | err = ioutil.WriteFile(filename, data, 0644) 138 | return err 139 | } 140 | 141 | func loadCLIYAML(filename string) (Command, error) { 142 | command := Command{} 143 | contents, err := ioutil.ReadFile(filename) 144 | if err != nil { 145 | return command, err 146 | } 147 | 148 | err = yaml.Unmarshal(contents, &command) 149 | if err != nil { 150 | return command, err 151 | } 152 | 153 | return command, nil 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fauxcli 2 | 3 | [![CircleCI](https://circleci.com/gh/nextrevision/fauxcli.svg?style=svg)](https://circleci.com/gh/nextrevision/fauxcli) 4 | 5 | FauxCLI (pronounced foak-ley) mocks a command line client from a given YAML file. 6 | 7 | ![FauxCLI Example](example.gif) 8 | 9 | ## Quickstart 10 | 11 | Install: 12 | 13 | ``` 14 | $ go get github.com/nextrevision/fauxcli 15 | ``` 16 | 17 | Create a `cli.yml` file in your current directory (or init one with `FAUXCLI_INIT=1 fauxcli`): 18 | 19 | ``` 20 | name: spiderpig 21 | help: does whatever a spiderpig does 22 | commands: 23 | - name: swing 24 | help: swings from a web 25 | output: | 26 | I can't do that, I'm a pig! 27 | - name: plop 28 | help: super secret maneuver 29 | output: | 30 | Look out! 31 | ``` 32 | 33 | Run `fauxcli`: 34 | 35 | ``` 36 | $ fauxcli 37 | does whatever a spiderpig does 38 | 39 | Usage: 40 | spiderpig [command] 41 | 42 | Available Commands: 43 | swing swings from a web 44 | plop super secret maneuver 45 | 46 | Flags: 47 | -h, --help help for spiderpig 48 | 49 | Use "spiderpig [command] --help" for more information about a command. 50 | ``` 51 | 52 | With a subcommand: 53 | 54 | ``` 55 | $ fauxcli swing 56 | I can't do that, I'm a pig! 57 | ``` 58 | 59 | Aliasing: 60 | 61 | ``` 62 | $ alias spiderpig='fauxcli' 63 | $ spiderpig plop 64 | Look out! 65 | ``` 66 | 67 | ## Installation 68 | 69 | With go: 70 | 71 | ``` 72 | go get github.com/nextrevision/fauxcli 73 | ``` 74 | 75 | Using GitHub releases: 76 | 77 | ``` 78 | # OSX 79 | curl -s -o /usr/local/bin/fauxcli https://github.com/nextrevision/fauxcli/releases/download/1.1.0/fauxcli_darwin_amd64 80 | 81 | # Linux 82 | curl -s -o /usr/local/bin/fauxcli https://github.com/nextrevision/fauxcli/releases/download/1.1.0/fauxcli_linux_amd64 83 | 84 | chmod +x /usr/local/bin/fauxcli 85 | ``` 86 | 87 | ## `cli.yml` 88 | 89 | The `cli.yml` file holds all the details required in order to mock a CLI application. By default, `fauxcli` will look in your current directory for this file. You can override this setting with the `FAUXCLI_FILE` environment variable pointing to a different YAML file of your choosing. 90 | 91 | ``` 92 | --- 93 | # (required) name of the command 94 | name: mycliapp 95 | 96 | # (required) the help text for the command (displayed with -h) 97 | help: does something cool 98 | 99 | # additional command aliases 100 | aliases: ["myapp", "app"] 101 | 102 | # output to print when the command is run 103 | # if this key is omitted, the command will act as a 104 | # parent to any subcommands, essentially doing nothing 105 | # but printing the help text 106 | output: | 107 | Hello, World! 108 | 109 | # flags available to the command 110 | flags: 111 | # (required) long name of the flag (--debug) 112 | - name: debug 113 | 114 | # (required) help text for the flag 115 | help: enables debugging 116 | 117 | # short name for the flag (-d) 118 | short: d 119 | 120 | # default value of the flag 121 | default: false 122 | 123 | # make the flag globally available 124 | global: true 125 | 126 | # the type of the value (default string) 127 | # available types: 128 | # - string 129 | # - bool 130 | # - int 131 | # - float 132 | type: bool 133 | 134 | # subcommands (nested from all the options above) 135 | commands: 136 | - name: subcommand1 137 | help: a subcommand 138 | flags: 139 | - name: upper 140 | help: converts output to uppercase 141 | short: u 142 | type: bool 143 | output: | 144 | {{ if .Flags.upper.Bool -}} 145 | HELLO FROM SC1! 146 | {{ else -}} 147 | Hello from SC1! 148 | {{ end -}} 149 | - name: subcommand2 150 | help: another subcommand with children 151 | commands: 152 | - name: child1 153 | help: the first child command 154 | output: | 155 | Hello from child1 156 | - name: child2 157 | help: the second child command 158 | output: | 159 | Hello from child2 160 | ``` 161 | 162 | ### Initializing a `cli.yml` 163 | 164 | Run fauxcli with the environment variable `FAUXCLI_INIT` set to `1` to create a sample cli.yml. 165 | 166 | ``` 167 | $ FAXUCLI_INIT=1 fauxcli 168 | Hello, World! 169 | ``` 170 | 171 | ## Output 172 | 173 | The `output` section of a command uses the Golang [template](https://golang.org/pkg/text/template/) syntax (also see [https://gohugo.io/templates/go-templates/](https://gohugo.io/templates/go-templates/)). There are two main variables exposed to you: 174 | 175 | 1. `Flags`, the list of flags defined in your `cli.yaml`. 176 | 2. `Args`, a list of additional string arguments passed to the command. 177 | 178 | ### Flags 179 | 180 | Flags can be accessed using the following syntax in the output section: `{{ .Flags.flagname }}`. This, however, is not very useful on its own. If you want to retrieve the genericly typed value of a flag, you can use `{{ .Flags.flagname.Value }}`. This is useful for just dumping the value, regardless of type. If you are doing a comparison, like checking if a string flag's value is empty, you need to retrieve the typed value instead, so for a string `{{ .Flags.flagname.String }}`, bools `{{ .Flags.flagname.Bool }}`, ints `{{ .Flags.flagname.Int }}`, and floats `{{ .Flags.flagname.Float }}`. 181 | 182 | ### Args 183 | 184 | Additional arguments passed to the command can be accessed via the `{{ .Args }}` variable. Useful functions related to args are: 185 | 186 | * Checking if args are passed (or empty): `{{ .Args | lengthis 0 }}` 187 | * Retrieving the first arg: `{{ .Args | first }}` 188 | * Looping over the args: 189 | 190 | ``` 191 | {{ range $i, $v := .Args -}} 192 | arg{{ $i }} value: {{ $v }} 193 | {{ end -}} 194 | ``` 195 | 196 | ### Pipelines/Filters (methods available in functions) 197 | 198 | A number of methods are made available to use in the functions, taken primarily from [Pallinder/go-randomdata](https://github.com/Pallinder/go-randomdata#usage) and [leekchan/gtf](https://github.com/leekchan/gtf#reference): 199 | 200 | #### Random Data Functions 201 | 202 | Sometimes is useful to mix up the output type you will receive. Also "foo" and "bar" get a little boring compared to "sargentbrindle" and "fishhail". Here are some functions to generate random values in your output templates (from [Pallinder/go-randomdata](https://github.com/Pallinder/go-randomdata#usage)): 203 | 204 | | Function | Description | Example | Usage | 205 | |----------|-------------|---------|-------| 206 | | name | returns a silly, random, one-word name | Apeflannel | `{{ name }}`| 207 | | fullname | returns a capitalized, first and last name | Natalie White | `{{ fullname }}` | 208 | | email | returns a random email address | jaydendavis@example.com | `{{ email }}` | 209 | | city | returns the name of a random city | Skidaway Island | `{{ city }}` | 210 | | street | returns the name of a random street | Wilson Ct | `{{ street }}` | 211 | | address | returns a random street address (two lines) | 48 Lincoln Circle,\nBurrton, MS, 85910 | `{{ address }}` | 212 | | number | returns a random number for a given range | 4 | `{{ number 1 10 }}` | 213 | | paragraph | returns a random paragraph | One dog rolled before him...snip | `{{ paragraph }}` | 214 | | ipaddress | returns a random IPv4 address | 32.254.96.245 | `{{ ipaddress }}` | 215 | | day | returns a random day | Sunday | `{{ day }}` | 216 | | month | returns a random month | November | `{{ month }}` | 217 | | date | returns a random date | Wednesday 8 Apr 2016 | `{{ date }}` | 218 | 219 | 220 | #### Imported from GTF 221 | 222 | Again, see the [reference here](https://github.com/leekchan/gtf#reference) for additional details. 223 | 224 | * replace 225 | * default 226 | * length 227 | * lower 228 | * upper 229 | * truncatechars 230 | * urlencode 231 | * wordcount 232 | * divisibleby 233 | * lengthis 234 | * trim 235 | * capfirst 236 | * pluralize 237 | * yesno 238 | * rjust 239 | * ljust 240 | * center 241 | * filesizeformat 242 | * apnumber 243 | * intcomma 244 | * ordinal 245 | * first 246 | * last 247 | * join 248 | * slice 249 | * random 250 | * striptags 251 | 252 | #### Custom 253 | 254 | | Function | Description | Usage | 255 | |----------|-------------|-------| 256 | | string | type casts value as a string | `{{ .Flags.flagname.Value | string }}` | 257 | | bool | type casts value as a bool | `{{ .Flags.flagname.Value | bool }}` | 258 | | int | type casts value as a int | `{{ .Flags.flagname.Value | int }}` | 259 | | float | type casts value as a float (float64) | `{{ .Flags.flagname.Value | float }}` | 260 | | count | returns an iterator with the specified size | `{{ range $i, $_ := count 3 }}` | 261 | 262 | ## Examples 263 | 264 | Check out the examples directory for some sample apps. 265 | --------------------------------------------------------------------------------