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