Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

25 changed files with 165 additions and 813 deletions

View File

@ -1,39 +0,0 @@
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)
}

View File

@ -3,32 +3,32 @@ package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"go.fifitido.net/cmd/opt" "go.fifitido.net/cmd/opts"
) )
type Command struct { type Command struct {
Name string name string
version string shortDescription string
ShortDescription string longDescription string
LongDescription string aliases []string
Aliases []string arguments []*Argument
Args []*Argument opts opts.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(name string, version string, options ...Option) *Command { func NewRoot(options ...Option) *Command {
cmd := &Command{Name: name, version: version, isRoot: true} cmd := &Command{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,24 +44,15 @@ func (c *Command) Root() *Command {
return c return c
} }
return c.Parent.Root() return c.parent.Root()
}
func (c *Command) IsRoot() bool {
return c.isRoot
} }
func (c *Command) CommandPath() string { func (c *Command) CommandPath() string {
if c.Parent == nil { if c.parent == nil {
return "" return filepath.Base(os.Args[0])
} }
parentPath := c.Parent.CommandPath() return c.parent.CommandPath() + " " + c.name
if parentPath == "" {
return c.Name
}
return c.Parent.CommandPath() + " " + c.Name
} }
func (c *Command) CanRun() bool { func (c *Command) CanRun() bool {
@ -76,15 +67,8 @@ func (c *Command) Execute(args []string) {
if c.isRoot { if c.isRoot {
args = args[1:] args = args[1:]
} }
if len(args) > 0 {
sc, ok := c.Subcommands.Get(args[0])
if ok {
sc.Execute(args[1:])
return
}
}
parser := opt.NewParser(args, c.Opts, false) parser := opts.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())
@ -92,13 +76,15 @@ func (c *Command) Execute(args []string) {
os.Exit(1) os.Exit(1)
} }
versionOpt, ok := opt.Globals().GetBool("version") if len(restArgs) > 0 {
if ok && versionOpt.Value() { sc, ok := c.subcommands.Get(restArgs[0])
c.ShowVersion() if ok {
return sc.Execute(restArgs[1:])
return
}
} }
helpOpt, ok := opt.Globals().GetBool("help") helpOpt, ok := opts.Globals().GetBool("help")
if ok && helpOpt.Value() { if ok && helpOpt.Value() {
c.ShowHelp() c.ShowHelp()
return return
@ -111,8 +97,3 @@ func (c *Command) Execute(args []string) {
c.ShowHelp() c.ShowHelp()
} }
func (c *Command) ShowVersion() {
rootName := c.Root().Name
fmt.Printf("%s %s\n", rootName, c.version)
}

View File

@ -1,124 +0,0 @@
package cmd
import (
"io"
"os"
"go.fifitido.net/cmd/opt"
)
var outputFileOption = opt.String("output", "o", "", "The file to output the completions to")
func CompletionsSubcommand() *Command {
cmd := New(
"completions",
WithShortDescription("Generate shell completions"),
WithLongDescription("Generate shell completions"),
)
registerBashCompletions(cmd)
registerFishCompletions(cmd)
registerPowerShellCompletions(cmd)
registerZsgCompletions(cmd)
return cmd
}
func getCompletionsOut() io.Writer {
outputFile := outputFileOption.Value()
if outputFile != "" {
var err error
out, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
return out
}
return os.Stdout
}
func registerBashCompletions(parent *Command) *Command {
return New(
"bash",
WithShortDescription("Generate bash completions"),
WithLongDescription("Generate bash completions"),
WithOptions(outputFileOption),
WithParent(parent),
WithRunFunc(func(args []string) {
out := getCompletionsOut()
if fc, ok := out.(io.Closer); ok {
defer fc.Close()
}
if err := WriteBashCompletions(out, parent.Root()); err != nil {
panic(err)
}
}),
)
}
func registerFishCompletions(parent *Command) *Command {
return New(
"fish",
WithShortDescription("Generate fish completions"),
WithLongDescription("Generate fish completions"),
WithOptions(outputFileOption),
WithParent(parent),
WithRunFunc(func(args []string) {
out := getCompletionsOut()
if fc, ok := out.(io.Closer); ok {
defer fc.Close()
}
if err := WriteFishCompletions(out, parent.Root()); err != nil {
panic(err)
}
}),
)
}
func registerPowerShellCompletions(parent *Command) *Command {
return New(
"powershell",
WithShortDescription("Generate powershell completions"),
WithLongDescription("Generate powershell completions"),
WithOptions(outputFileOption),
WithParent(parent),
WithRunFunc(func(args []string) {
out := getCompletionsOut()
if fc, ok := out.(io.Closer); ok {
defer fc.Close()
}
if err := WritePowerShellCompletions(out, parent.Root()); err != nil {
panic(err)
}
}),
)
}
func registerZsgCompletions(parent *Command) *Command {
return New(
"zsh",
WithShortDescription("Generate zsh completions"),
WithLongDescription("Generate zsh completions"),
WithOptions(outputFileOption),
WithParent(parent),
WithRunFunc(func(args []string) {
out := getCompletionsOut()
if fc, ok := out.(io.Closer); ok {
defer fc.Close()
}
if err := WriteZshCompletions(out, parent.Root()); err != nil {
panic(err)
}
}),
)
}

View File

@ -1,58 +0,0 @@
package cmd
import (
"io"
"text/template"
"go.fifitido.net/cmd/opt"
)
func WriteBashCompletions(out io.Writer, rootCmd *Command) error {
return bashTpl.Execute(out, map[string]any{
"RootCmd": rootCmd,
"GlobalOpts": opt.Globals(),
})
}
// TODO: Add --option|--other-option...) to return nothing for options that require a value
// TODO: Add descriptions to completions using https://stackoverflow.com/a/10130007
var bashTpl = template.Must(template.New("bash").Funcs(tplFuncs).Parse(`
{{- $rootCmd := .RootCmd -}}
{{- $progName := $rootCmd.Name -}}
{{- $varName := under $rootCmd.Name -}}
{{- define "cmd" }}
{{ $currIdx := .Index -}}
{{- $nextIdx := inc .Index -}}
{{- $indentSize := mult $currIdx 2 -}}
{{ indent $indentSize }}"{{ .Cmd.Name }}")
{{ indent $indentSize }} case ${COMP_WORDS[{{ $currIdx }}]} in
{{ indent $indentSize }} {{ range .Cmd.Subcommands -}}
{{ indent $indentSize }} {{ template "cmd" (map "Cmd" . "Index" $nextIdx) -}}
{{ indent $indentSize }} {{ end }}
{{ indent $indentSize }} *)
{{ indent $indentSize }} COMPREPLY=($(compgen -W "{{ join .Cmd.Subcommands.Names " " }} {{ join .Cmd.Opts.Names " " }}" -- $curr))
{{ indent $indentSize }} ;;
{{ indent $indentSize }} esac
{{ indent $indentSize }};; # {{ .Cmd.Name }}
{{- end -}}
_{{$varName}}_completions()
{
COMPREPLY=()
case ${COMP_WORDS[1]} in
{{- range .RootCmd.Subcommands -}}
{{ template "cmd" (map "Cmd" . "Index" 2) -}}
{{ end }}
*)
COMPREPLY=($(compgen -W "{{ join .RootCmd.Subcommands.Names " " }} {{ join .RootCmd.Opts.Names " " }}" -- $curr))
;;
esac
# Global Options
COMPREPLY+=($(compgen -W "{{ join .GlobalOpts.Names " " }}" -- $curr))
}
complete -F _{{$varName}}_completions {{$progName}}
`))

View File

@ -1,100 +0,0 @@
package cmd
import (
"io"
"text/template"
"go.fifitido.net/cmd/opt"
)
func WriteFishCompletions(out io.Writer, rootCmd *Command) error {
return fishTpl.Execute(out, map[string]any{
"RootCmd": rootCmd,
"GlobalOpts": opt.Globals(),
})
}
// TODO: Fix fish infinitely completing last command at the end of the command chain
var fishTpl = template.Must(template.New("fish").Funcs(tplFuncs).Parse(`
{{- $rootCmd := .RootCmd -}}
{{- $progName := $rootCmd.Name -}}
{{- $varName := under $rootCmd.Name -}}
set -l commands {{ join $rootCmd.Subcommands.Names " " }}
function __fish_{{ $varName }}_needs_command
set -l cmd (commandline -opc)
if test (count $cmd) -eq 1
return 0
end
return 1
end
function __fish_{{ $varName }}_using_command
set -l cmd (commandline -opc)
set -l cnt (count $argv)
if test (count $cmd) -gt $cnt
for i in (seq 1 $cnt);
if test $argv[$i] != $cmd[(math $i + 1)]
return 1
end
end
return 0
end
return 1
end
{{/* Option template */ -}}
{{ define "opt" }}
{{ $progName := .ProgName -}}
{{- $varName := .VarName -}}
complete -c {{ $progName }}
{{- if ne .Opt.ShortName "" }} -s {{ .Opt.ShortName }} {{ end -}}
{{- if ne .Opt.Name "" }} -l {{ .Opt.Name }} {{ end -}}
{{- if .Cmd }} {{- if ne .Cmd.CommandPath "" -}}
-n "__fish_{{ $varName }}_using_command {{.Cmd.CommandPath}}" {{ else -}}
-n "__fish_{{ $varName }}_needs_command" {{ end -}}{{- end -}}
-d '{{ .Opt.Description }}'
{{- end }}
{{- /* Command template */ -}}
{{ define "cmd" }}
{{ $progName := .ProgName -}}
{{- $varName := .VarName -}}
{{- $cmd := .Cmd -}}
{{- $parentVarPrefix := varPrefix .Cmd.Parent.CommandPath -}}
{{- $varPrefix := varPrefix .Cmd.CommandPath -}}
{{- if eq .Cmd.Parent.CommandPath "" -}}
complete -f -c {{ $progName }} -n "__fish_{{ $varName }}_needs_command" -a {{ .Cmd.Name }} -d "{{ .Cmd.ShortDescription }}"
{{- else -}}
complete -f -c {{ $progName }} -n "__fish_{{ $varName }}_using_command {{.Cmd.Parent.CommandPath}}" -a {{ .Cmd.Name }} -d "{{ .Cmd.ShortDescription }}"
{{- end -}}
{{- range .Cmd.Opts }}
{{- template "opt" (map "Opt" . "ProgName" $progName "VarName" $varName "Cmd" $cmd) -}}
{{ end -}}
{{ if gt (len .Cmd.Subcommands) 0 }}
set -l {{ $varPrefix }}commands {{ join .Cmd.Subcommands.Names " " }}
{{ $cmdName := .Cmd.Name }}
{{- range .Cmd.Subcommands }}
{{- template "cmd" (map "Cmd" . "ProgName" $progName "VarName" $varName) -}}
{{ end -}}
{{- end -}}
{{ end }}
{{- /* Top-level commands */ -}}
{{ range $rootCmd.Subcommands }}
{{- template "cmd" (map "Cmd" . "ProgName" $progName "VarName" $varName) -}}
{{ end }}
{{- /* Root command options */ -}}
{{ range $rootCmd.Opts }}
{{- template "opt" (map "Opt" . "ProgName" $progName "VarName" $varName "Cmd" $rootCmd) -}}
{{ end }}
{{- /* Global options */ -}}
{{ range .GlobalOpts }}
{{- template "opt" (map "Opt" . "ProgName" $progName "VarName" $varName) -}}
{{ end }}
`))

View File

@ -1,90 +0,0 @@
package cmd
import (
"io"
"text/template"
"go.fifitido.net/cmd/opt"
)
func WritePowerShellCompletions(out io.Writer, rootCmd *Command) error {
return powerShellTpl.Execute(out, map[string]any{
"RootCmd": rootCmd,
"GlobalOpts": opt.Globals(),
})
}
var powerShellTpl = template.Must(template.New("PowerShell").Funcs(tplFuncs).Parse(`
{{- $rootCmd := .RootCmd -}}
{{- $progName := $rootCmd.Name -}}
{{- $varName := $rootCmd.Name | camel -}}
{{- define "cmd" -}}
{{- $progName := .ProgName -}}
{{- if .Cmd.IsRoot }}
{{- /**/}} '{{ $progName }}' {
{{- else }}
{{- /**/}} '{{ $progName }};{{ join (split .Cmd.CommandPath " ") ";" }}' {
{{- end -}}
{{- range .Cmd.Opts -}}
{{ if ne .Name "" }}
[CompletionResult]::new('--{{ .Name }}', '{{ .Name }}', [CompletionResultType]::ParameterName, '{{ .Description }}')
{{- end -}}
{{- if ne .ShortName "" }}
[CompletionResult]::new('-{{ .ShortName }}', '{{ .ShortName }}', [CompletionResultType]::ParameterName, '{{ .Description }}')
{{- end -}}
{{- end }}
{{- range .Cmd.Subcommands }}
[CompletionResult]::new('{{ .Name }}', '{{ .Name }}', [CompletionResultType]::ParameterValue, '{{ .ShortDescription }}')
{{- end }}
break;
}
{{- range .Cmd.Subcommands }}
{{ template "cmd" (map "Cmd" . "ProgName" $progName) }}
{{- end -}}
{{- end -}}
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
[scriptblock]$__{{ $varName }}CompleterBlock = {
param(
$wordToComplete,
$commandAst,
$cursorPosition
)
$commandElements
$command = @(
'{{ $progName }}'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-') -or
$element.Value -eq $wordToComplete) {
break
}
$element.Value
}
) -join ';'
$completions = @(
switch ($command) {
{{ template "cmd" (map "Cmd" $rootCmd "ProgName" $progName) }}
}
{{- range .GlobalOpts -}}
{{- if ne .Name "" }}
[CompletionResult]::new('--{{ .Name }}', '{{ .Name }}', [CompletionResultType]::ParameterName, '{{ .Description }}')
{{- end -}}
{{- if ne .ShortName "" }}
[CompletionResult]::new('-{{ .ShortName }}', '{{ .ShortName }}', [CompletionResultType]::ParameterName, '{{ .Description }}')
{{- end -}}
{{- end -}}
)
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}
Register-ArgumentCompleter -CommandName {{ $progName }} -ScriptBlock $__{{ $varName }}CompleterBlock
`))

View File

@ -1,85 +0,0 @@
package cmd
import (
"io"
"text/template"
"go.fifitido.net/cmd/opt"
)
func WriteZshCompletions(out io.Writer, rootCmd *Command) error {
return zshTpl.Execute(out, map[string]any{
"RootCmd": rootCmd,
"GlobalOpts": opt.Globals(),
})
}
// TODO: Fix indentation and other spacing
var zshTpl = template.Must(template.New("zsh").Funcs(tplFuncs).Parse(`
{{- $rootCmd := .RootCmd -}}
{{- $progName := $rootCmd.Name -}}
{{- $varName := under $rootCmd.Name -}}
{{- define "cmd" }}
{{- $varPrefix := varPrefix .Cmd.CommandPath -}}
{{- if not .Cmd.IsRoot}}
{{ .Cmd.Name }})
{{ end -}}
{{ if gt (len .Cmd.Subcommands) 0 -}}
local -a {{ $varPrefix }}commands
{{ $varPrefix }}commands=(
{{- range .Cmd.Subcommands}}
'{{ .Name }}:{{ .ShortDescription }}'
{{- end }}
)
{{ end }}
{{- if or (gt (len .Cmd.Opts) 0) (gt (len .Cmd.Subcommands) 0) -}}
_arguments
{{- end -}}
{{- range .Cmd.Opts -}}
{{- if ne .Name ""}} \
'--{{ .Name }}[{{ .Description }}]'
{{- end }}
{{- if ne .ShortName ""}} \
'-{{ .ShortName }}[{{ .Description }}]'
{{- end }}
{{- end }}
{{- if gt (len .Cmd.Subcommands) 0 -}}
{{ " " }}\
'1: :{_describe 'command' {{ $varPrefix }}commands}' \
'*:: :->args'
case $state in
args)
case $words[1] in
{{- range .Cmd.Subcommands }}
{{ template "cmd" (map "Cmd" .) }}
{{- end }}
esac
;;
esac
{{- end }}
{{- if not .Cmd.IsRoot}}
;;
{{ end }}
{{ end -}}
function _{{ $varName }} {
{{ template "cmd" (map "Cmd" .RootCmd) -}}
{{- if gt (len .GlobalOpts) 0 }}
_arguments
{{- range .GlobalOpts -}}
{{- if ne .Name ""}} \
'--{{ .Name }}[{{ .Description }}]'
{{- end }}
{{- if ne .ShortName ""}} \
'-{{ .ShortName }}[{{ .Description }}]'
{{- end }}
{{- end }}
{{- end }}
}
compdef _{{ $varName }} {{ $progName }}
`))

View File

@ -8,7 +8,6 @@ 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

View File

@ -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.2.0 require go.fifitido.net/cmd v0.0.0-20231110055906-31e40ecc826a

View File

@ -1,2 +1,6 @@
go.fifitido.net/cmd v0.2.0 h1:QEq4Gbts4DjnzXLwxV4xaSI3kleVXmtvSFcuOGjBGqc= go.fifitido.net/cmd v0.0.0-20231110043437-c32ce2efcd5f h1:v+sjO+t6cGEtDhStOnuypYBrkOMO4suiDVxL93rOUCs=
go.fifitido.net/cmd v0.2.0/go.mod h1:8SaDxCG1m6WwShUlZApSbNleCvV7oTfqZIyYu4aAqO0= go.fifitido.net/cmd v0.0.0-20231110043437-c32ce2efcd5f/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=

2
go.mod
View File

@ -1,5 +1,3 @@
module go.fifitido.net/cmd module go.fifitido.net/cmd
go 1.21.3 go 1.21.3
require github.com/iancoleman/strcase v0.3.0

2
go.sum
View File

@ -1,2 +0,0 @@
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=

54
help.go
View File

@ -2,40 +2,29 @@ package cmd
import ( import (
"fmt" "fmt"
"strings"
"go.fifitido.net/cmd/opt" "go.fifitido.net/cmd/opts"
) )
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 %s ", binaryName, cmdPath) fmt.Printf(" %s ", 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 {
fmt.Print("<command> ") fmt.Print("<command> ")
} }
} }
fmt.Print("[options]")
if len(c.Args) > 0 {
for _, a := range c.Args {
if a.Required {
fmt.Print(" <" + a.Name + ">")
} else {
fmt.Print(" [" + a.Name + "]")
}
}
}
fmt.Println()
if len(c.Subcommands) > 0 { fmt.Println("[options]")
if len(c.subcommands) > 0 {
fmt.Println() fmt.Println()
if c.isRoot { if c.isRoot {
@ -44,34 +33,31 @@ func (c *Command) ShowHelp() {
fmt.Println("Available subcommands:") fmt.Println("Available subcommands:")
} }
paddedWidth := c.Subcommands.MaxWidth() for _, s := range c.subcommands {
for _, s := range c.Subcommands { fmt.Println(" " + s.name + " " + s.shortDescription)
spaceSize := 2 + paddedWidth - len(s.Name)
fmt.Println(" " + s.Name + strings.Repeat(" ", spaceSize) + s.ShortDescription)
} }
} }
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(" " + opts.HelpLine(f, paddedWidth))
}
} }
globalOpts := opt.Globals() globalOpts := opts.Globals()
if len(globalOpts) > 0 { if len(globalOpts) > 0 {
paddedWidth = globalOpts.MaxWidth()
fmt.Println() fmt.Println()
fmt.Println("Global options:") fmt.Println("Global options:")
paddedWidth := globalOpts.MaxWidth()
for _, f := range globalOpts { for _, f := range globalOpts {
fmt.Println(" " + opt.HelpLine(f, paddedWidth)) fmt.Println(" " + opts.HelpLine(f, paddedWidth))
} }
} }
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", binaryName) fmt.Printf("Run '%s <command> --help' for more information about a command.\n", c.Root().CommandPath())
} }
} }

View File

@ -1,67 +1,67 @@
package cmd package cmd
import ( import (
"go.fifitido.net/cmd/opt" "go.fifitido.net/cmd/opts"
) )
type Option func(*Command) 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.Args = append(c.Args, &Argument{name, required}) c.arguments = append(c.arguments, &Argument{name, required})
} }
} }
func WithArguments(args ...*Argument) Option { func WithArguments(args ...*Argument) Option {
return func(c *Command) { return func(c *Command) {
c.Args = append(c.Args, args...) c.arguments = append(c.arguments, args...)
} }
} }
func WithOption(f opt.Option) Option { func WithOption(f opts.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 ...opts.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(s) c.subcommands.Add(c)
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 {
c.Subcommands.Add(s) s.parent = c
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)
} }
} }

View File

@ -1,4 +1,4 @@
package opt package opts
import ( import (
"strconv" "strconv"

View File

@ -1,4 +1,4 @@
package opt package opts
import "strconv" import "strconv"

View File

@ -1,4 +1,4 @@
package opt package opts
var globalOpts = Set{ var globalOpts = Set{
Bool("help", "h", false, "Show the help menu"), Bool("help", "h", false, "Show the help menu"),

View File

@ -1,4 +1,4 @@
package opt package opts
import "strconv" import "strconv"

View File

@ -1,4 +1,4 @@
package opt package opts
import "fmt" import "fmt"

View File

@ -1,4 +1,4 @@
package opt package opts
import ( import (
"errors" "errors"

View File

@ -1,15 +1,15 @@
package opt_test package opts_test
import ( import (
"testing" "testing"
"go.fifitido.net/cmd/opt" "go.fifitido.net/cmd/opts"
) )
func TestParseUnknownLongOption(t *testing.T) { func TestParseUnknownLongOption(t *testing.T) {
set := opt.Set{} set := opts.Set{}
args := []string{"--unknown"} args := []string{"--unknown"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -19,9 +19,9 @@ func TestParseUnknownLongOption(t *testing.T) {
} }
func TestParseUnknownShortOption(t *testing.T) { func TestParseUnknownShortOption(t *testing.T) {
set := opt.Set{} set := opts.Set{}
args := []string{"-u"} args := []string{"-u"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -31,12 +31,12 @@ func TestParseUnknownShortOption(t *testing.T) {
} }
func TestParseUnknownShortChainedOption1(t *testing.T) { func TestParseUnknownShortChainedOption1(t *testing.T) {
set := opt.Set{ set := opts.Set{
opt.Bool("banana", "b", false, ""), opts.Bool("banana", "b", false, ""),
opt.Bool("cucumber", "c", false, ""), opts.Bool("cucumber", "c", false, ""),
} }
args := []string{"-abc"} args := []string{"-abc"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -46,12 +46,12 @@ func TestParseUnknownShortChainedOption1(t *testing.T) {
} }
func TestParseUnknownShortChainedOption2(t *testing.T) { func TestParseUnknownShortChainedOption2(t *testing.T) {
set := opt.Set{ set := opts.Set{
opt.Bool("apple", "a", false, ""), opts.Bool("apple", "a", false, ""),
opt.Bool("cucumber", "c", false, ""), opts.Bool("cucumber", "c", false, ""),
} }
args := []string{"-abc"} args := []string{"-abc"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -61,12 +61,12 @@ func TestParseUnknownShortChainedOption2(t *testing.T) {
} }
func TestParseUnknownShortChainedOption3(t *testing.T) { func TestParseUnknownShortChainedOption3(t *testing.T) {
set := opt.Set{ set := opts.Set{
opt.Bool("apple", "a", false, ""), opts.Bool("apple", "a", false, ""),
opt.Bool("banana", "b", false, ""), opts.Bool("banana", "b", false, ""),
} }
args := []string{"-abc"} args := []string{"-abc"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -76,12 +76,12 @@ func TestParseUnknownShortChainedOption3(t *testing.T) {
} }
func TestParseUnchaninableOption(t *testing.T) { func TestParseUnchaninableOption(t *testing.T) {
set := opt.Set{ set := opts.Set{
opt.Bool("apple", "a", false, ""), opts.Bool("apple", "a", false, ""),
opt.String("banana", "b", "", ""), opts.String("banana", "b", "", ""),
} }
args := []string{"-ab"} args := []string{"-ab"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -91,92 +91,92 @@ func TestParseUnchaninableOption(t *testing.T) {
} }
func TestParseShortOptionWithValueAndNoSpace(t *testing.T) { func TestParseShortOptionWithValueAndNoSpace(t *testing.T) {
o := opt.String("fruit", "f", "", "") opt := opts.String("fruit", "f", "", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"-fapple"} args := []string{"-fapple"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "apple" { if opt.Value() != "apple" {
t.Errorf("Expected fruit to be 'apple', got: %s", o.Value()) t.Errorf("Expected fruit to be 'apple', got: %s", opt.Value())
} }
} }
func TestParseShortOptionWithValueAndSpace(t *testing.T) { func TestParseShortOptionWithValueAndSpace(t *testing.T) {
o := opt.String("fruit", "f", "", "") opt := opts.String("fruit", "f", "", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"-f", "apple"} args := []string{"-f", "apple"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "apple" { if opt.Value() != "apple" {
t.Errorf("Expected fruit to be 'apple', got: %s", o.Value()) t.Errorf("Expected fruit to be 'apple', got: %s", opt.Value())
} }
} }
func TestParseShortOptionWithValueAndEqual(t *testing.T) { func TestParseShortOptionWithValueAndEqual(t *testing.T) {
o := opt.String("fruit", "f", "", "") opt := opts.String("fruit", "f", "", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"-f=apple"} args := []string{"-f=apple"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "apple" { if opt.Value() != "apple" {
t.Errorf("Expected fruit to be 'apple', got: %s", o.Value()) t.Errorf("Expected fruit to be 'apple', got: %s", opt.Value())
} }
} }
func TestParseLongOptionWithValueAndEqual(t *testing.T) { func TestParseLongOptionWithValueAndEqual(t *testing.T) {
o := opt.String("fruit", "f", "", "") opt := opts.String("fruit", "f", "", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"--fruit=apple"} args := []string{"--fruit=apple"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "apple" { if opt.Value() != "apple" {
t.Errorf("Expected fruit to be 'apple', got: %s", o.Value()) t.Errorf("Expected fruit to be 'apple', got: %s", opt.Value())
} }
} }
func TestParseLongOptionWithValueAndSpace(t *testing.T) { func TestParseLongOptionWithValueAndSpace(t *testing.T) {
o := opt.String("fruit", "f", "", "") opt := opts.String("fruit", "f", "", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"--fruit", "apple"} args := []string{"--fruit", "apple"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "apple" { if opt.Value() != "apple" {
t.Errorf("Expected fruit to be 'apple', got: %s", o.Value()) t.Errorf("Expected fruit to be 'apple', got: %s", opt.Value())
} }
} }
func TestParseOptionTerminator(t *testing.T) { func TestParseOptionTerminator(t *testing.T) {
o := opt.String("fruit", "f", "banana", "") opt := opts.String("fruit", "f", "banana", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"--fruit", "apple", "--", "hello", "world"} args := []string{"--fruit", "apple", "--", "hello", "world"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
restArgs, err := parser.Parse() restArgs, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "apple" { if opt.Value() != "apple" {
t.Errorf("Expected fruit to be 'apple', got: %s", o.Value()) t.Errorf("Expected fruit to be 'apple', got: %s", opt.Value())
} }
if len(restArgs) != 2 { if len(restArgs) != 2 {
@ -193,9 +193,9 @@ func TestParseOptionTerminator(t *testing.T) {
} }
func TestParseLongOptionIgnoreUnknown(t *testing.T) { func TestParseLongOptionIgnoreUnknown(t *testing.T) {
set := opt.Set{} set := opts.Set{}
args := []string{"--unknown", "hello", "world"} args := []string{"--unknown", "hello", "world"}
parser := opt.NewParser(args, set, true) parser := opts.NewParser(args, set, true)
restArgs, err := parser.Parse() restArgs, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
@ -215,9 +215,9 @@ func TestParseLongOptionIgnoreUnknown(t *testing.T) {
} }
func TestParseShortOptionIgnoreUnknown(t *testing.T) { func TestParseShortOptionIgnoreUnknown(t *testing.T) {
set := opt.Set{} set := opts.Set{}
args := []string{"-q"} args := []string{"-q"}
parser := opt.NewParser(args, set, true) parser := opts.NewParser(args, set, true)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
@ -226,12 +226,12 @@ func TestParseShortOptionIgnoreUnknown(t *testing.T) {
} }
func TestParseChainedShortOptionIgnoreUnknown(t *testing.T) { func TestParseChainedShortOptionIgnoreUnknown(t *testing.T) {
apple := opt.Bool("apple", "a", false, "") apple := opts.Bool("apple", "a", false, "")
banana := opt.Bool("banana", "b", false, "") banana := opts.Bool("banana", "b", false, "")
cucumber := opt.Bool("cucumber", "c", false, "") cucumber := opts.Bool("cucumber", "c", false, "")
set := opt.Set{apple, banana, cucumber} set := opts.Set{apple, banana, cucumber}
args := []string{"-adc"} args := []string{"-adc"}
parser := opt.NewParser(args, set, true) parser := opts.NewParser(args, set, true)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
@ -252,25 +252,25 @@ func TestParseChainedShortOptionIgnoreUnknown(t *testing.T) {
} }
func TestParseSingleDashValue(t *testing.T) { func TestParseSingleDashValue(t *testing.T) {
o := opt.String("fruit", "f", "", "") opt := opts.String("fruit", "f", "", "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"-f-"} args := []string{"-f-"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err != nil { if err != nil {
t.Errorf("Expected no error, got: %s", err.Error()) t.Errorf("Expected no error, got: %s", err.Error())
} }
if o.Value() != "-" { if opt.Value() != "-" {
t.Errorf("Expected fruit to be '-', got: %s", o.Value()) t.Errorf("Expected fruit to be '-', got: %s", opt.Value())
} }
} }
func TestParseLongOptionBadValue(t *testing.T) { func TestParseLongOptionBadValue(t *testing.T) {
o := opt.Int("fruit", "f", 0, "") opt := opts.Int("fruit", "f", 0, "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"--fruit=five"} args := []string{"--fruit=five"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -278,10 +278,10 @@ func TestParseLongOptionBadValue(t *testing.T) {
} }
func TestParseShortOptionWithSpaceBadValue(t *testing.T) { func TestParseShortOptionWithSpaceBadValue(t *testing.T) {
o := opt.Int("fruit", "f", 0, "") opt := opts.Int("fruit", "f", 0, "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"-f", "five"} args := []string{"-f", "five"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -289,11 +289,11 @@ func TestParseShortOptionWithSpaceBadValue(t *testing.T) {
} }
func TestParseShortOptionWithoutEqualBadValue(t *testing.T) { func TestParseShortOptionWithoutEqualBadValue(t *testing.T) {
og := opt.Bool("green", "g", false, "") optG := opts.Bool("green", "g", false, "")
of := opt.Int("fruit", "f", 0, "") optF := opts.Int("fruit", "f", 0, "")
set := opt.Set{og, of} set := opts.Set{optG, optF}
args := []string{"-ffive"} args := []string{"-ffive"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -301,11 +301,11 @@ func TestParseShortOptionWithoutEqualBadValue(t *testing.T) {
} }
func TestParseShortOptionWithEqualBadValue(t *testing.T) { func TestParseShortOptionWithEqualBadValue(t *testing.T) {
og := opt.Bool("green", "g", false, "") optG := opts.Bool("green", "g", false, "")
of := opt.Int("fruit", "f", 0, "") optF := opts.Int("fruit", "f", 0, "")
set := opt.Set{og, of} set := opts.Set{optG, optF}
args := []string{"-f=five"} args := []string{"-f=five"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -313,10 +313,10 @@ func TestParseShortOptionWithEqualBadValue(t *testing.T) {
} }
func TestParseLongOptionMissingValue(t *testing.T) { func TestParseLongOptionMissingValue(t *testing.T) {
o := opt.Float("fruit-price", "f", 0, "") opt := opts.Float("fruit-price", "f", 0, "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"--fruit-price"} args := []string{"--fruit-price"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")
@ -324,10 +324,10 @@ func TestParseLongOptionMissingValue(t *testing.T) {
} }
func TestParseShortOptionMissingValue(t *testing.T) { func TestParseShortOptionMissingValue(t *testing.T) {
o := opt.Float("fruit-price", "f", 0, "") opt := opts.Float("fruit-price", "f", 0, "")
set := opt.Set{o} set := opts.Set{opt}
args := []string{"-f"} args := []string{"-f"}
parser := opt.NewParser(args, set, false) parser := opts.NewParser(args, set, false)
_, err := parser.Parse() _, err := parser.Parse()
if err == nil { if err == nil {
t.Error("Expected error") t.Error("Expected error")

View File

@ -1,4 +1,4 @@
package opt package opts
type Set []Option type Set []Option
@ -33,16 +33,6 @@ func (s Set) Get(name string) (Option, bool) {
return nil, false return nil, false
} }
func (s Set) Names() []string {
names := []string{}
for _, o := range s {
names = append(names, "--"+o.Name())
names = append(names, "-"+o.ShortName())
}
return names
}
func (s Set) GetByLongName(longName string) (Option, bool) { func (s Set) GetByLongName(longName string) (Option, bool) {
for _, o := range s { for _, o := range s {
if o.Name() == longName { if o.Name() == longName {

View File

@ -1,4 +1,4 @@
package opt package opts
type StringOption struct { type StringOption struct {
name string name string

17
set.go
View File

@ -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
} }
@ -25,25 +25,16 @@ func (s Set) Get(name string) (*Command, bool) {
return nil, false return nil, false
} }
func (s Set) MaxWidth() 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
} }
} }
return max return max
} }
func (s Set) Names() []string {
names := make([]string, 0, len(s))
for _, c := range s {
names = append(names, c.Name)
names = append(names, c.Aliases...)
}
return names
}
func (s Set) Len() int { func (s Set) Len() int {
return len(s) return len(s)
} }

View File

@ -1,99 +0,0 @@
package cmd
import (
"fmt"
"strings"
"text/template"
"github.com/iancoleman/strcase"
)
var tplFuncs = template.FuncMap{
"map": tplMap,
"cat": tplCat,
"split": tplSplit,
"join": tplJoin,
"under": tplUnder,
"varPrefix": tplVarPrefix,
"repeat": tplRepeat,
"indent": tplIndent,
"add": tplAdd,
"inc": tplInc,
"sub": tplSub,
"dec": tplDec,
"mult": tplMult,
"pascal": tplPascal,
"camel": tplCamel,
}
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 tplCat(strs ...string) string {
return strings.Join(strs, "")
}
func tplSplit(s string, sep string) []string {
return strings.Split(s, sep)
}
func tplJoin(strs []string, sep string) string {
return strings.Join(strs, sep)
}
func tplUnder(s string) string {
return strcase.ToSnake(s)
}
func tplVarPrefix(s string) string {
if s == "" {
return ""
}
return tplUnder(s) + "_"
}
func tplRepeat(s string, n int) string {
return strings.Repeat(s, n)
}
func tplIndent(n int) string {
return tplRepeat(" ", n)
}
func tplAdd(a, b int) int {
return a + b
}
func tplInc(i int) int {
return tplAdd(i, 1)
}
func tplSub(a, b int) int {
return a - b
}
func tplDec(i int) int {
return tplSub(i, 1)
}
func tplMult(a, b int) int {
return a * b
}
func tplPascal(s string) string {
return strcase.ToCamel(s)
}
func tplCamel(s string) string {
return strcase.ToLowerCamel(s)
}