Skip to content

Commit

Permalink
Merge pull request #74 from hmrc/autocomplete
Browse files Browse the repository at this point in the history
Improve autocompletion
  • Loading branch information
RikSherman authored Jul 21, 2023
2 parents cee0a65 + 6c6e177 commit f3ad40d
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 65 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
20 changes: 0 additions & 20 deletions cli/autocomplete.go

This file was deleted.

128 changes: 86 additions & 42 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,59 @@ import (
"flag"
"fmt"
"os"
"reflect"
"regexp"
"strconv"
"strings"
)

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
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
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 {
Expand Down Expand Up @@ -153,20 +157,23 @@ 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)

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, "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.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")
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.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")
Expand Down Expand Up @@ -194,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")
})
}
}
2 changes: 1 addition & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions servicemanager/autocomplete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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("# 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 --comp-pword \"$prev\" )
COMPREPLY=( $(compgen -W "$words" -- $cur) )
return 0
}
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)
flagSet := cli.BuildFlagSet(opts)
flagSet.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()
}

// 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
}
5 changes: 3 additions & 2 deletions servicemanager/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"os"
"regexp"
"sm2/cli"
"sm2/version"
)

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit f3ad40d

Please sign in to comment.