From 58961fb39b31939a409e2a5b6f45e8b71afdfd9b Mon Sep 17 00:00:00 2001 From: Richard Sherman <23365258+RikSherman@users.noreply.github.com> Date: Tue, 11 Jul 2023 08:24:32 +0100 Subject: [PATCH 1/5] Improve autocompletion --- cli/autocomplete.go | 20 -------- cli/cli.go | 84 ++++++++++++++++++---------------- servicemanager/autocomplete.go | 59 ++++++++++++++++++++++++ servicemanager/commands.go | 5 +- 4 files changed, 106 insertions(+), 62 deletions(-) delete mode 100644 cli/autocomplete.go create mode 100644 servicemanager/autocomplete.go diff --git a/cli/autocomplete.go b/cli/autocomplete.go deleted file mode 100644 index 1f8c16f..0000000 --- a/cli/autocomplete.go +++ /dev/null @@ -1,20 +0,0 @@ -package cli - -import ( - "flag" - "fmt" -) - -// Print a valid bash autocomplete config to stdout -// intended use would be to pipe it into a file in the os's autocomplete folder -func GenerateAutoCompletions() { - opts := new(UserOption) - flagset := buildFlagSet(opts) - fmt.Println("# copy this content into a file and place it in your OS's autocomplete folder") - fmt.Printf("complete -W \"") - flagset.VisitAll(func(f *flag.Flag) { - fmt.Printf("-%s ", f.Name) - }) - - fmt.Print("\" sm2\n") -} diff --git a/cli/cli.go b/cli/cli.go index 5fe42e9..76b833c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -11,49 +11,51 @@ import ( ) type UserOption struct { - appendArgs string // not exported, content decoded into ExtraArgs - AutoComplete bool // generates an autocomplete script - CheckPorts bool // finds duplicate ports - Clean bool // used with --start to force re-downloading - Config string // uses a different service-manager-config folder - Debug string // debug info about a service, used to determine why it failed to start - Diagnostic bool // runs tests to determine if there are problems with the install - ExtraArgs map[string][]string // parsed from content of AppendArgs - ExtraServices []string // ids of services to start - FromSource bool // used with --start to run from source rather than bin - FormatPlain bool // flag for setting enabling machine friendly/undecorated output - Latest bool // used in conjunction with --restart to check for latest version of service(s) being restarted - List bool // lists all the services - Logs string // prints the logs of a service, running or otherwise - NoProgress bool // hides the animated download progress meter - NoVpnCheck bool // skips checking if vpn is connected before starting a service - Offline bool // prints downloaded services, used with --start bypasses download and uses local copy - Port int // overrides service port, only works with the first service when starting multiple - Ports bool // prints all the ports - Prune bool // deletes .state files of services with a status of FAIL - Release string // specify a version when starting one service. unlikely old sm, cannot be used without a version - Restart bool // restarts a service or profile - ReverseProxy bool // starts a reverse-proxy on 3000 (override with --port) - Search string // searches for services/profiles - Start bool // starts a service, multiple services or a profile(s) - Status bool // shows status of everything that's running - StatusShort bool // same as --status but is the -s short version of the cmd - StopAll bool // stops all the services that are running - Stop bool // stops a service, multiple services or profile(s) - Update bool // update sm2 if a newer version is available - UpdateConfig bool // pulls the latest copy of service-manager-config - Verbose bool // shows extra logging - Version bool // prints sm2 version number - Verify bool // checks if a given service or profile is running - Wait int // waits given number of secs after starting services for then to respond to pings - Workers int // sets the number of concurrent downloads/service starts - DelaySeconds int // sets the pause in seconds between starting services + appendArgs string // not exported, content decoded into ExtraArgs + AutoComplete bool // generates an autocomplete response + CheckPorts bool // finds duplicate ports + Clean bool // used with --start to force re-downloading + CompWordCount int // used with --autocomplete number of words in completion + Config string // uses a different service-manager-config folder + Debug string // debug info about a service, used to determine why it failed to start + Diagnostic bool // runs tests to determine if there are problems with the install + ExtraArgs map[string][]string // parsed from content of AppendArgs + ExtraServices []string // ids of services to start + FromSource bool // used with --start to run from source rather than bin + FormatPlain bool // flag for setting enabling machine friendly/undecorated output + GenerateAutoComplete bool // generates an autocomplete script + Latest bool // used in conjunction with --restart to check for latest version of service(s) being restarted + List bool // lists all the services + Logs string // prints the logs of a service, running or otherwise + NoProgress bool // hides the animated download progress meter + NoVpnCheck bool // skips checking if vpn is connected before starting a service + Offline bool // prints downloaded services, used with --start bypasses download and uses local copy + Port int // overrides service port, only works with the first service when starting multiple + Ports bool // prints all the ports + Prune bool // deletes .state files of services with a status of FAIL + Release string // specify a version when starting one service. unlikely old sm, cannot be used without a version + Restart bool // restarts a service or profile + ReverseProxy bool // starts a reverse-proxy on 3000 (override with --port) + Search string // searches for services/profiles + Start bool // starts a service, multiple services or a profile(s) + Status bool // shows status of everything that's running + StatusShort bool // same as --status but is the -s short version of the cmd + StopAll bool // stops all the services that are running + Stop bool // stops a service, multiple services or profile(s) + Update bool // update sm2 if a newer version is available + UpdateConfig bool // pulls the latest copy of service-manager-config + Verbose bool // shows extra logging + Version bool // prints sm2 version number + Verify bool // checks if a given service or profile is running + Wait int // waits given number of secs after starting services for then to respond to pings + Workers int // sets the number of concurrent downloads/service starts + DelaySeconds int // sets the pause in seconds between starting services } func Parse(args []string) (*UserOption, error) { opts := new(UserOption) - flagset := buildFlagSet(opts) + flagset := BuildFlagSet(opts) flagset.Parse(fixupInvalidFlags(args)) if opts.Workers <= 0 { @@ -153,18 +155,20 @@ func releaseIsValid(release string) bool { return rx.MatchString(release) } -func buildFlagSet(opts *UserOption) *flag.FlagSet { +func BuildFlagSet(opts *UserOption) *flag.FlagSet { flagset := flag.NewFlagSet("servicemanager", flag.ExitOnError) flagset.StringVar(&opts.appendArgs, "appendArgs", "", "A map of args to append for services you are starting. i.e. '{\"SERVICE_NAME\":[\"-DFoo=Bar\",\"SOMETHING\"],\"SERVICE_TWO\":[\"APPEND_THIS\"]}'") - flagset.BoolVar(&opts.AutoComplete, "generate-autocomplete", false, "generates bash completions script") + flagset.BoolVar(&opts.AutoComplete, "autocomplete", false, "generates bash completions response (used by bash-completions)") flagset.BoolVar(&opts.CheckPorts, "checkports", false, "finds services using the same port number") flagset.BoolVar(&opts.Clean, "clean", false, "forces reinstall of service (use with --start)") + flagset.IntVar(&opts.CompWordCount, "comp-cword", 1, "used with --autocomplete by script generated using --generate-autocomplete") flagset.StringVar(&opts.Config, "config", "", "sets an alternate directory for service-manager-config") flagset.StringVar(&opts.Debug, "debug", "", "infomation on why a given `service` may not have started") flagset.BoolVar(&opts.Diagnostic, "diagnostic", false, "a suite of checks to debug issues with service manager") flagset.BoolVar(&opts.FromSource, "src", false, "run service from source (use with --start)") flagset.BoolVar(&opts.FormatPlain, "format-plain", false, "list services without formatting") + flagset.BoolVar(&opts.GenerateAutoComplete, "generate-autocomplete", false, "generates bash completions script") flagset.BoolVar(&opts.Latest, "latest", false, "used in conjunction with -restart to check for latest version of service(s) being restarted") flagset.BoolVar(&opts.List, "list", false, "lists all available services") flagset.StringVar(&opts.Logs, "logs", "", "shows the stdout logs for a service") diff --git a/servicemanager/autocomplete.go b/servicemanager/autocomplete.go new file mode 100644 index 0000000..cc6ae3c --- /dev/null +++ b/servicemanager/autocomplete.go @@ -0,0 +1,59 @@ +package servicemanager + +import ( + "flag" + "fmt" + "sm2/cli" + "sort" + "strings" +) + +// Print a valid bash autocomplete config to stdout +// intended use would be to pipe it into a file in the os's autocomplete folder +func GenerateAutoCompletionScript() { + fmt.Println("# copy this content into a file and place it in your OS's autocomplete folder") + fmt.Println( + `_serv_words() +{ + local count cur + cur=${COMP_WORDS[COMP_CWORD]} + count=${COMP_CWORD} + words=$(sm2 --autocomplete --comp-cword $count) + COMPREPLY=( $(compgen -W "$words" -- $cur) ) + return 0 +} +complete -F _serv_words sm2 +`) +} + +func (sm *ServiceManager) GenerateAutocompleteResponse() string { + count := sm.Commands.CompWordCount + var words strings.Builder + opts := new(cli.UserOption) + cli.BuildFlagSet(opts).VisitAll(func(f *flag.Flag) { + if len(f.Name) == 1 { + words.WriteString(fmt.Sprintf("-%s ", f.Name)) + } else { + words.WriteString(fmt.Sprintf("--%s ", f.Name)) + } + }) + + if count >= 2 { + keys := make([]string, len(sm.Services)+len(sm.Profiles)) + i := 0 + for k := range sm.Services { + keys[i] = k + i++ + + } + + for k := range sm.Profiles { + keys[i] = k + i++ + + } + sort.Strings(keys) + words.WriteString(strings.Join(keys, " ")) + } + return words.String() +} diff --git a/servicemanager/commands.go b/servicemanager/commands.go index 83d3c44..087051a 100644 --- a/servicemanager/commands.go +++ b/servicemanager/commands.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "regexp" - "sm2/cli" "sm2/version" ) @@ -124,8 +123,10 @@ func (sm *ServiceManager) Run() { } } else if sm.Commands.Update { err = update(sm.Config.TmpDir) + } else if sm.Commands.GenerateAutoComplete { + GenerateAutoCompletionScript() } else if sm.Commands.AutoComplete { - cli.GenerateAutoCompletions() + fmt.Println(sm.GenerateAutocompleteResponse()) } else { // show help if they're not using --update-config with another command if !sm.Commands.UpdateConfig { From 603693982ee4bb1657838b297cea3d32c204d834 Mon Sep 17 00:00:00 2001 From: Richard Sherman <23365258+RikSherman@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:14:55 +0100 Subject: [PATCH 2/5] Improve autocompletion --- README.md | 15 ++++++++++++++ cli/cli.go | 2 ++ servicemanager/autocomplete.go | 36 +++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7579c0a..453abfc 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,21 @@ You can check everything is setup correctly by running: sm2 --diagnostic ``` +### Enabling tab completion +Run the commands below which will copy a completion script into a file called `sm2.bash` in the directory +`~/.local/share/bash-completion/completions`, if the directory doesn't exist then it will create it. +```shell +mkdir -p ~/.local/share/bash-completion/completions +sm2 --generate-autocomplete > ~/.local/share/bash-completion/completions/sm2.bash +``` +If you are using zsh as your shell and not using Oh-My-Zsh then you may need to enable bash-completion support by adding the following to your `~/.zshrc` file +```shell +# Load bash completion functions +autoload -Uz +X compinit && compinit +autoload -Uz +X bashcompinit && bashcompinit +``` + + ### Upgrading Service Manager 2 As of v1.0.9 `sm2` can update itself - simply run `sm2 -update`. You will need to ensure `sm2` is available on your `$PATH`. diff --git a/cli/cli.go b/cli/cli.go index 76b833c..1cc644b 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -16,6 +16,7 @@ type UserOption struct { CheckPorts bool // finds duplicate ports Clean bool // used with --start to force re-downloading CompWordCount int // used with --autocomplete number of words in completion + CompPreviousWord string // used with --autocomplete previous of word in completion Config string // uses a different service-manager-config folder Debug string // debug info about a service, used to determine why it failed to start Diagnostic bool // runs tests to determine if there are problems with the install @@ -162,6 +163,7 @@ func BuildFlagSet(opts *UserOption) *flag.FlagSet { flagset.BoolVar(&opts.AutoComplete, "autocomplete", false, "generates bash completions response (used by bash-completions)") flagset.BoolVar(&opts.CheckPorts, "checkports", false, "finds services using the same port number") flagset.BoolVar(&opts.Clean, "clean", false, "forces reinstall of service (use with --start)") + flagset.StringVar(&opts.CompPreviousWord, "comp-pword", "", "used with --autocomplete by script generated using --generate-autocomplete") flagset.IntVar(&opts.CompWordCount, "comp-cword", 1, "used with --autocomplete by script generated using --generate-autocomplete") flagset.StringVar(&opts.Config, "config", "", "sets an alternate directory for service-manager-config") flagset.StringVar(&opts.Debug, "debug", "", "infomation on why a given `service` may not have started") diff --git a/servicemanager/autocomplete.go b/servicemanager/autocomplete.go index cc6ae3c..e910770 100644 --- a/servicemanager/autocomplete.go +++ b/servicemanager/autocomplete.go @@ -11,26 +11,31 @@ import ( // Print a valid bash autocomplete config to stdout // intended use would be to pipe it into a file in the os's autocomplete folder func GenerateAutoCompletionScript() { - fmt.Println("# copy this content into a file and place it in your OS's autocomplete folder") + fmt.Println("# Below is a bash completion script for tab completion") fmt.Println( `_serv_words() { local count cur cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} count=${COMP_CWORD} - words=$(sm2 --autocomplete --comp-cword $count) + words=$(sm2 --autocomplete --comp-cword $count --comp-pword \"$prev\" ) COMPREPLY=( $(compgen -W "$words" -- $cur) ) return 0 } -complete -F _serv_words sm2 -`) +complete -F _serv_words sm2`) } func (sm *ServiceManager) GenerateAutocompleteResponse() string { count := sm.Commands.CompWordCount + prev := strings.ReplaceAll(sm.Commands.CompPreviousWord, "\"", "") + if dontComplete(prev) { + return "" + } var words strings.Builder opts := new(cli.UserOption) - cli.BuildFlagSet(opts).VisitAll(func(f *flag.Flag) { + flagSet := cli.BuildFlagSet(opts) + flagSet.VisitAll(func(f *flag.Flag) { if len(f.Name) == 1 { words.WriteString(fmt.Sprintf("-%s ", f.Name)) } else { @@ -57,3 +62,24 @@ func (sm *ServiceManager) GenerateAutocompleteResponse() string { } return words.String() } + +// Non boolean arguments can't be autocompleted +func dontComplete(previousWord string) bool { + switch strings.ReplaceAll(previousWord, "--", "-") { + case + "-appendArgs", + "-comp-cword", + "-comp-pword", + "-config", + "-debug", + "-logs", + "-port", + "-ports", + "-search", + "-wait", + "-workers", + "-delay-seconds": + return true + } + return false +} From d0183e64e30ed215e3138033eab8d508c484b985 Mon Sep 17 00:00:00 2001 From: Richard Sherman <23365258+RikSherman@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:17:22 +0100 Subject: [PATCH 3/5] Improve autocompletion --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index eaa71fe..98006c3 100644 --- a/makefile +++ b/makefile @@ -8,7 +8,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BINARY := sm2 -VERSION := 1.0.9 +VERSION := 1.0.10 BUILD := `git rev-parse HEAD` # Setup linker flags option for build that interoperate with variable names in src code From f00c063d1b50419e5351a9beec42cbe344e2ac44 Mon Sep 17 00:00:00 2001 From: Richard Sherman <23365258+RikSherman@users.noreply.github.com> Date: Thu, 20 Jul 2023 08:34:48 +0100 Subject: [PATCH 4/5] Improve description of list --- cli/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cli.go b/cli/cli.go index 1cc644b..2096a25 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -172,7 +172,7 @@ func BuildFlagSet(opts *UserOption) *flag.FlagSet { flagset.BoolVar(&opts.FormatPlain, "format-plain", false, "list services without formatting") flagset.BoolVar(&opts.GenerateAutoComplete, "generate-autocomplete", false, "generates bash completions script") flagset.BoolVar(&opts.Latest, "latest", false, "used in conjunction with -restart to check for latest version of service(s) being restarted") - flagset.BoolVar(&opts.List, "list", false, "lists all available services") + flagset.BoolVar(&opts.List, "list", false, "lists all available services and profiles") flagset.StringVar(&opts.Logs, "logs", "", "shows the stdout logs for a service") flagset.BoolVar(&opts.NoProgress, "noprogress", false, "prevents download progress being shown (use with --start)") flagset.BoolVar(&opts.NoVpnCheck, "no-vpn-check", defaultVpnCheck(), "disables checking if the vpn is connected") From 6c6e17721c1bcec0663722cbe3b733cf88feb3c2 Mon Sep 17 00:00:00 2001 From: Richard Sherman <23365258+RikSherman@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:29:06 +0100 Subject: [PATCH 5/5] Display -- for longform arguments --- cli/cli.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/cli/cli.go b/cli/cli.go index 2096a25..79d6f0f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "os" + "reflect" "regexp" "strconv" "strings" @@ -158,7 +159,7 @@ func releaseIsValid(release string) bool { func BuildFlagSet(opts *UserOption) *flag.FlagSet { flagset := flag.NewFlagSet("servicemanager", flag.ExitOnError) - + setUsage(flagset) flagset.StringVar(&opts.appendArgs, "appendArgs", "", "A map of args to append for services you are starting. i.e. '{\"SERVICE_NAME\":[\"-DFoo=Bar\",\"SOMETHING\"],\"SERVICE_TWO\":[\"APPEND_THIS\"]}'") flagset.BoolVar(&opts.AutoComplete, "autocomplete", false, "generates bash completions response (used by bash-completions)") flagset.BoolVar(&opts.CheckPorts, "checkports", false, "finds services using the same port number") @@ -200,3 +201,40 @@ func BuildFlagSet(opts *UserOption) *flag.FlagSet { return flagset } + +// Based on flag.DefaultUsage to use -- for long arguments +func setUsage(f *flag.FlagSet) { + f.Usage = func() { + f.VisitAll(func(flag *flag.Flag) { + var s string + if len(flag.Name) == 1 { + s = fmt.Sprintf(" -%s", flag.Name) + } else { + s = fmt.Sprintf(" --%s", flag.Name) + } + usage := flag.Usage + flagType := strings.TrimSuffix( + strings.TrimPrefix( + reflect.TypeOf(flag.Value).String(), + "*flag."), + "Value") + if flagType == "bool" { + flagType = "" + } + if len(flagType) > 0 { + s += " " + flagType + } + // Boolean flags of one ASCII letter are so common we + // treat them specially, putting their usage on the same line. + if len(s) <= 4 { // space, space, '-', 'x'. + s += "\t" + } else { + // Four spaces before the tab triggers good alignment + // for both 4- and 8-space tab stops. + s += "\n \t" + } + s += strings.ReplaceAll(usage, "\n", "\n \t") + fmt.Fprint(f.Output(), s, "\n") + }) + } +}