Add semi-working implementation of fishshell completions
This commit is contained in:
		
							parent
							
								
									3f7f04e4f1
								
							
						
					
					
						commit
						7123217e58
					
				|  | @ -0,0 +1,39 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 
 | ||||||
|  | 	"go.fifitido.net/cmd" | ||||||
|  | 	"go.fifitido.net/cmd/opt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	so = opt.String("string", "s", "", "example string option") | ||||||
|  | 	io = opt.Int("int", "i", 0, "example int option") | ||||||
|  | 	fo = opt.Float("float", "f", 0, "example float option") | ||||||
|  | 	bo = opt.Bool("bool", "b", false, "example bool option") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var root = cmd.NewRoot( | ||||||
|  | 	"example-cmd", | ||||||
|  | 	cmd.WithShortDescription("Example command"), | ||||||
|  | 	cmd.WithLongDescription(`An example command to show how to use go.fifitido.net/cmd | ||||||
|  | 
 | ||||||
|  | this example is just a simple hello world program to show | ||||||
|  | the basics of the library.`), | ||||||
|  | 	cmd.WithArgument("name", false), | ||||||
|  | 	cmd.WithSubcommand(cmd.CompletionsSubcommand()), | ||||||
|  | 	cmd.WithOptions(so, io, fo, bo), | ||||||
|  | 	cmd.WithRunFunc(func(args []string) { | ||||||
|  | 		if len(args) == 0 { | ||||||
|  | 			fmt.Println("Hello World!") | ||||||
|  | 		} else { | ||||||
|  | 			fmt.Printf("Hello %s!\n", args[0]) | ||||||
|  | 		} | ||||||
|  | 	}), | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	root.Execute(os.Args) | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								command.go
								
								
								
								
							
							
						
						
									
										40
									
								
								command.go
								
								
								
								
							|  | @ -3,32 +3,31 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" |  | ||||||
| 
 | 
 | ||||||
| 	"go.fifitido.net/cmd/opt" | 	"go.fifitido.net/cmd/opt" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type Command struct { | type Command struct { | ||||||
| 	name             string | 	Name             string | ||||||
| 	shortDescription string | 	ShortDescription string | ||||||
| 	longDescription  string | 	LongDescription  string | ||||||
| 	aliases          []string | 	Aliases          []string | ||||||
| 	arguments        []*Argument | 	Args             []*Argument | ||||||
| 	opts             opt.Set | 	Opts             opt.Set | ||||||
| 	subcommands      Set | 	Subcommands      Set | ||||||
| 	parent           *Command | 	Parent           *Command | ||||||
| 	run              func(args []string) | 	run              func(args []string) | ||||||
| 	isRoot           bool | 	isRoot           bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewRoot(options ...Option) *Command { | func NewRoot(name string, options ...Option) *Command { | ||||||
| 	cmd := &Command{isRoot: true} | 	cmd := &Command{Name: name, isRoot: true} | ||||||
| 	cmd.ApplyOptions(options...) | 	cmd.ApplyOptions(options...) | ||||||
| 	return cmd | 	return cmd | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func New(name string, options ...Option) *Command { | func New(name string, options ...Option) *Command { | ||||||
| 	cmd := &Command{name: name} | 	cmd := &Command{Name: name} | ||||||
| 	cmd.ApplyOptions(options...) | 	cmd.ApplyOptions(options...) | ||||||
| 	return cmd | 	return cmd | ||||||
| } | } | ||||||
|  | @ -44,15 +43,20 @@ func (c *Command) Root() *Command { | ||||||
| 		return c | 		return c | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return c.parent.Root() | 	return c.Parent.Root() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *Command) CommandPath() string { | func (c *Command) CommandPath() string { | ||||||
| 	if c.parent == nil { | 	if c.Parent == nil { | ||||||
| 		return filepath.Base(os.Args[0]) | 		return "" | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return c.parent.CommandPath() + " " + c.name | 	parentPath := c.Parent.CommandPath() | ||||||
|  | 	if parentPath == "" { | ||||||
|  | 		return c.Name | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return c.Parent.CommandPath() + " " + c.Name | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *Command) CanRun() bool { | func (c *Command) CanRun() bool { | ||||||
|  | @ -68,7 +72,7 @@ func (c *Command) Execute(args []string) { | ||||||
| 		args = args[1:] | 		args = args[1:] | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	parser := opt.NewParser(args, c.opts, false) | 	parser := opt.NewParser(args, c.Opts, false) | ||||||
| 	restArgs, err := parser.Parse() | 	restArgs, err := parser.Parse() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Println(err.Error()) | 		fmt.Println(err.Error()) | ||||||
|  | @ -77,7 +81,7 @@ func (c *Command) Execute(args []string) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(restArgs) > 0 { | 	if len(restArgs) > 0 { | ||||||
| 		sc, ok := c.subcommands.Get(restArgs[0]) | 		sc, ok := c.Subcommands.Get(restArgs[0]) | ||||||
| 		if ok { | 		if ok { | ||||||
| 			sc.Execute(restArgs[1:]) | 			sc.Execute(restArgs[1:]) | ||||||
| 			return | 			return | ||||||
|  |  | ||||||
|  | @ -9,11 +9,65 @@ import ( | ||||||
| 
 | 
 | ||||||
| func WriteFishCompletions(out io.Writer, rootCmd *Command) error { | func WriteFishCompletions(out io.Writer, rootCmd *Command) error { | ||||||
| 	return fishTpl.Execute(out, map[string]any{ | 	return fishTpl.Execute(out, map[string]any{ | ||||||
| 		"rootCmd":    rootCmd, | 		"RootCmd":    rootCmd, | ||||||
| 		"globalOpts": opt.Globals(), | 		"GlobalOpts": opt.Globals(), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var fishTpl = template.Must(template.New("fish").Parse(` | var fishTpl = template.Must(template.New("fish").Funcs(tplFuncs).Parse(` | ||||||
|  | set -l progName {{ .RootCmd.Name }} | ||||||
|  | set -l commands {{- range .RootCmd.Subcommands }} {{ .Name }}{{ end }} | ||||||
| 
 | 
 | ||||||
|  | {{- /* Option template */ -}} | ||||||
|  | {{ define "opt" }} | ||||||
|  | complete -c $progName | ||||||
|  | {{- if ne .Opt.ShortName "" }} -s {{ .Opt.ShortName }} {{ end -}} | ||||||
|  | {{- if ne .Opt.Name "" }} -l {{ .Opt.Name }} {{ end -}}  | ||||||
|  | {{- if .Cond }} -n "{{ .Cond }}" {{ end -}} | ||||||
|  | -d '{{ .Opt.Description }}' | ||||||
|  | {{- end }} | ||||||
|  | 
 | ||||||
|  | {{- /* Command template */ -}} | ||||||
|  | {{ define "cmd" }} | ||||||
|  | {{ $parentVarPrefix := "" -}} | ||||||
|  | {{- $varPrefix := join .Cmd.Name "_" -}} | ||||||
|  | {{- if .VarPrefix -}} | ||||||
|  | {{- $varPrefix = join .VarPrefix $varPrefix -}} | ||||||
|  | {{- $parentVarPrefix = .VarPrefix -}} | ||||||
|  | {{- end -}} | ||||||
|  | 
 | ||||||
|  | {{ $parentCond := "" }} | ||||||
|  | {{- $cond := join "__fish_seen_subcommand_from " .Cmd.Name }} | ||||||
|  | {{- if .Prefix -}} | ||||||
|  | {{- $cond = join .Prefix $cond -}} | ||||||
|  | {{- $parentCond = .Prefix -}} | ||||||
|  | {{- end -}} | ||||||
|  | 
 | ||||||
|  | set -l {{ $varPrefix }}commands {{- range .Cmd.Subcommands }} {{ .Name }}{{ end }} | ||||||
|  | complete -f -c $progName -n "{{ $parentCond }}not __fish_seen_subcommand_from ${{ $parentVarPrefix }}commands" -a {{ .Cmd.Name }} -d "{{ .Cmd.ShortDescription }}" | ||||||
|  | 
 | ||||||
|  | {{- range .Cmd.Opts }} | ||||||
|  | {{- template "opt" (map "Opt" . "Cond" $cond) -}} | ||||||
|  | {{ end -}} | ||||||
|  | 
 | ||||||
|  | {{ $cmdName := .Cmd.Name }} | ||||||
|  | {{- range .Cmd.Subcommands }} | ||||||
|  | {{- template "cmd" (map "Cmd" . "Prefix" (join $cond "; ") "VarPrefix" $varPrefix) -}} | ||||||
|  | {{ end -}} | ||||||
|  | {{ end }} | ||||||
|  | 
 | ||||||
|  | {{- /* Top-level commands */ -}} | ||||||
|  | {{ range .RootCmd.Subcommands }} | ||||||
|  | {{- template "cmd" (map "Cmd" .) -}} | ||||||
|  | {{ end }} | ||||||
|  | 
 | ||||||
|  | {{- /* Root command options */ -}} | ||||||
|  | {{ range .RootCmd.Opts }} | ||||||
|  | {{- template "opt" (map "Opt" . "Cond" "not __fish_seen_subcommand_from $commands") -}} | ||||||
|  | {{ end }} | ||||||
|  | 
 | ||||||
|  | {{- /* Global options */ -}} | ||||||
|  | {{ range .GlobalOpts }} | ||||||
|  | {{- template "opt" (map "Opt" .) -}} | ||||||
|  | {{ end }} | ||||||
| `)) | `)) | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var root = cmd.NewRoot( | var root = cmd.NewRoot( | ||||||
|  | 	"hello-world", | ||||||
| 	cmd.WithShortDescription("Example command"), | 	cmd.WithShortDescription("Example command"), | ||||||
| 	cmd.WithLongDescription(`An example command to show how to use go.fifitido.net/cmd | 	cmd.WithLongDescription(`An example command to show how to use go.fifitido.net/cmd | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,4 +2,4 @@ module go.fifitido.net/cmd/examples/hello-world | ||||||
| 
 | 
 | ||||||
| go 1.21.3 | go 1.21.3 | ||||||
| 
 | 
 | ||||||
| require go.fifitido.net/cmd v0.0.0-20231110055906-31e40ecc826a | require go.fifitido.net/cmd v0.2.0 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,2 @@ | ||||||
| go.fifitido.net/cmd v0.0.0-20231110043437-c32ce2efcd5f h1:v+sjO+t6cGEtDhStOnuypYBrkOMO4suiDVxL93rOUCs= | go.fifitido.net/cmd v0.2.0 h1:QEq4Gbts4DjnzXLwxV4xaSI3kleVXmtvSFcuOGjBGqc= | ||||||
| go.fifitido.net/cmd v0.0.0-20231110043437-c32ce2efcd5f/go.mod h1:8SaDxCG1m6WwShUlZApSbNleCvV7oTfqZIyYu4aAqO0= | go.fifitido.net/cmd v0.2.0/go.mod h1:8SaDxCG1m6WwShUlZApSbNleCvV7oTfqZIyYu4aAqO0= | ||||||
| go.fifitido.net/cmd v0.0.0-20231110054944-def39983fdfa h1:Z62sZG2rnKMqa0jI+WD1t8vO9ESQsQa4j6ad9OFeREM= |  | ||||||
| go.fifitido.net/cmd v0.0.0-20231110054944-def39983fdfa/go.mod h1:8SaDxCG1m6WwShUlZApSbNleCvV7oTfqZIyYu4aAqO0= |  | ||||||
| go.fifitido.net/cmd v0.0.0-20231110055906-31e40ecc826a h1:qT/xBA3vcVJYgaPoei70yRlLoB1CdaL3FPzi0kZ+JF0= |  | ||||||
| go.fifitido.net/cmd v0.0.0-20231110055906-31e40ecc826a/go.mod h1:8SaDxCG1m6WwShUlZApSbNleCvV7oTfqZIyYu4aAqO0= |  | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								help.go
								
								
								
								
							
							
						
						
									
										27
									
								
								help.go
								
								
								
								
							|  | @ -8,13 +8,14 @@ import ( | ||||||
| 
 | 
 | ||||||
| func (c *Command) ShowHelp() { | func (c *Command) ShowHelp() { | ||||||
| 	cmdPath := c.CommandPath() | 	cmdPath := c.CommandPath() | ||||||
|  | 	binaryName := c.Root().Name | ||||||
| 
 | 
 | ||||||
| 	fmt.Println(c.longDescription) | 	fmt.Println(c.LongDescription) | ||||||
| 	fmt.Println() | 	fmt.Println() | ||||||
| 	fmt.Println("Usage: ") | 	fmt.Println("Usage: ") | ||||||
| 	fmt.Printf("  %s ", cmdPath) | 	fmt.Printf("  %s %s ", binaryName, cmdPath) | ||||||
| 
 | 
 | ||||||
| 	if len(c.subcommands) > 0 { | 	if len(c.Subcommands) > 0 { | ||||||
| 		if c.CanRun() { | 		if c.CanRun() { | ||||||
| 			fmt.Print("[command] ") | 			fmt.Print("[command] ") | ||||||
| 		} else { | 		} else { | ||||||
|  | @ -22,8 +23,8 @@ func (c *Command) ShowHelp() { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	fmt.Print("[options]") | 	fmt.Print("[options]") | ||||||
| 	if len(c.arguments) > 0 { | 	if len(c.Args) > 0 { | ||||||
| 		for _, a := range c.arguments { | 		for _, a := range c.Args { | ||||||
| 			if a.Required { | 			if a.Required { | ||||||
| 				fmt.Print(" <" + a.Name + ">") | 				fmt.Print(" <" + a.Name + ">") | ||||||
| 			} else { | 			} else { | ||||||
|  | @ -33,7 +34,7 @@ func (c *Command) ShowHelp() { | ||||||
| 	} | 	} | ||||||
| 	fmt.Println() | 	fmt.Println() | ||||||
| 
 | 
 | ||||||
| 	if len(c.subcommands) > 0 { | 	if len(c.Subcommands) > 0 { | ||||||
| 		fmt.Println() | 		fmt.Println() | ||||||
| 
 | 
 | ||||||
| 		if c.isRoot { | 		if c.isRoot { | ||||||
|  | @ -42,16 +43,16 @@ func (c *Command) ShowHelp() { | ||||||
| 			fmt.Println("Available subcommands:") | 			fmt.Println("Available subcommands:") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		for _, s := range c.subcommands { | 		for _, s := range c.Subcommands { | ||||||
| 			fmt.Println("  " + s.name + "    " + s.shortDescription) | 			fmt.Println("  " + s.Name + "    " + s.ShortDescription) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(c.opts) > 0 { | 	if len(c.Opts) > 0 { | ||||||
| 		fmt.Println() | 		fmt.Println() | ||||||
| 		fmt.Println("Available options:") | 		fmt.Println("Available options:") | ||||||
| 		paddedWidth := c.opts.MaxWidth() | 		paddedWidth := c.Opts.MaxWidth() | ||||||
| 		for _, f := range c.opts { | 		for _, f := range c.Opts { | ||||||
| 			fmt.Println("  " + opt.HelpLine(f, paddedWidth)) | 			fmt.Println("  " + opt.HelpLine(f, paddedWidth)) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -66,8 +67,8 @@ func (c *Command) ShowHelp() { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(c.subcommands) > 0 { | 	if len(c.Subcommands) > 0 { | ||||||
| 		fmt.Println() | 		fmt.Println() | ||||||
| 		fmt.Printf("Run '%s <command> --help' for more information about a command.\n", c.Root().CommandPath()) | 		fmt.Printf("Run '%s <command> --help' for more information about a command.\n", binaryName) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								option.go
								
								
								
								
							
							
						
						
									
										24
									
								
								option.go
								
								
								
								
							|  | @ -8,60 +8,60 @@ type Option func(*Command) | ||||||
| 
 | 
 | ||||||
| func WithShortDescription(s string) Option { | func WithShortDescription(s string) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.shortDescription = s | 		c.ShortDescription = s | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithLongDescription(s string) Option { | func WithLongDescription(s string) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.longDescription = s | 		c.LongDescription = s | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithArgument(name string, required bool) Option { | func WithArgument(name string, required bool) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.arguments = append(c.arguments, &Argument{name, required}) | 		c.Args = append(c.Args, &Argument{name, required}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithArguments(args ...*Argument) Option { | func WithArguments(args ...*Argument) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.arguments = append(c.arguments, args...) | 		c.Args = append(c.Args, args...) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithOption(f opt.Option) Option { | func WithOption(f opt.Option) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.opts = append(c.opts, f) | 		c.Opts = append(c.Opts, f) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithOptions(os ...opt.Option) Option { | func WithOptions(os ...opt.Option) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.opts = append(c.opts, os...) | 		c.Opts = append(c.Opts, os...) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithSubcommand(s *Command) Option { | func WithSubcommand(s *Command) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.subcommands.Add(c) | 		c.Subcommands.Add(s) | ||||||
| 		s.parent = c | 		s.Parent = c | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithSubcommands(ss ...*Command) Option { | func WithSubcommands(ss ...*Command) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.subcommands.Add(c) |  | ||||||
| 		for _, s := range ss { | 		for _, s := range ss { | ||||||
| 			s.parent = c | 			c.Subcommands.Add(s) | ||||||
|  | 			s.Parent = c | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WithParent(p *Command) Option { | func WithParent(p *Command) Option { | ||||||
| 	return func(c *Command) { | 	return func(c *Command) { | ||||||
| 		c.parent = p | 		c.Parent = p | ||||||
| 		p.subcommands.Add(c) | 		p.Subcommands.Add(c) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								set.go
								
								
								
								
							
							
						
						
									
										6
									
								
								set.go
								
								
								
								
							|  | @ -12,11 +12,11 @@ func (s *Set) Add(c *Command) { | ||||||
| 
 | 
 | ||||||
| func (s Set) Get(name string) (*Command, bool) { | func (s Set) Get(name string) (*Command, bool) { | ||||||
| 	for _, c := range s { | 	for _, c := range s { | ||||||
| 		if c.name == name { | 		if c.Name == name { | ||||||
| 			return c, true | 			return c, true | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		for _, alias := range c.aliases { | 		for _, alias := range c.Aliases { | ||||||
| 			if alias == name { | 			if alias == name { | ||||||
| 				return c, true | 				return c, true | ||||||
| 			} | 			} | ||||||
|  | @ -28,7 +28,7 @@ func (s Set) Get(name string) (*Command, bool) { | ||||||
| func (s Set) MaxNameWidth() int { | func (s Set) MaxNameWidth() int { | ||||||
| 	max := 0 | 	max := 0 | ||||||
| 	for _, f := range s { | 	for _, f := range s { | ||||||
| 		if w := len(f.name); w > max { | 		if w := len(f.Name); w > max { | ||||||
| 			max = w | 			max = w | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | package cmd | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"text/template" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var tplFuncs = template.FuncMap{ | ||||||
|  | 	"map":  tplMap, | ||||||
|  | 	"join": tplJoin, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func tplMap(vals ...any) (map[string]any, error) { | ||||||
|  | 	if len(vals)%2 != 0 { | ||||||
|  | 		return nil, fmt.Errorf("missing value, need one key and one value per kv pair") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	m := make(map[string]any) | ||||||
|  | 	for i := 0; i < len(vals); i += 2 { | ||||||
|  | 		m[vals[i].(string)] = vals[i+1] | ||||||
|  | 	} | ||||||
|  | 	return m, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func tplJoin(strs ...string) string { | ||||||
|  | 	return strings.Join(strs, "") | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue