From 3801901457391dece50549b1f0b4a02d50155880 Mon Sep 17 00:00:00 2001 From: Kindritskiy Max Date: Mon, 11 Jul 2022 12:01:16 +0300 Subject: [PATCH 1/3] refactor completions, replace docopt.go with custom docopt.go --- .gitignore | 4 +- cmd/completion.go | 134 ++++++++++++++++++++-- cmd/root.go | 8 +- config/parser/docopts.go | 9 +- docker/Dockerfile | 6 +- go.mod | 2 + go.sum | 4 +- build.yaml => lets.build.yaml | 0 lets.yaml | 15 +-- tests/test_helpers.bash | 4 + tests/test_helpers_completion.bash | 64 +++++++++++ tests/zsh_completion.bats | 51 ++++++++ tests/zsh_completion/completion_helper.sh | 116 +++++++++++++++++++ tests/zsh_completion/lets.yaml | 11 ++ 14 files changed, 397 insertions(+), 31 deletions(-) rename build.yaml => lets.build.yaml (100%) create mode 100644 tests/test_helpers_completion.bash create mode 100644 tests/zsh_completion.bats create mode 100755 tests/zsh_completion/completion_helper.sh create mode 100644 tests/zsh_completion/lets.yaml diff --git a/.gitignore b/.gitignore index 9c630d46..67f6cceb 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ dist lets .lets lets.my.yaml -coverage.out \ No newline at end of file +_lets +coverage.out +node_modules diff --git a/cmd/completion.go b/cmd/completion.go index 649bfa7e..6252be49 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -4,28 +4,61 @@ import ( "bytes" "fmt" "io" + "sort" "strings" "text/template" + "github.com/lets-cli/lets/config/config" + "github.com/lets-cli/lets/config/parser" "github.com/spf13/cobra" ) -const zshCompletionText = `#compdef lets +const zshCompletionText = `#compdef _lets lets -_list () { +LETS_EXECUTABLE=lets + +function _lets { + local state + + _arguments -C -s \ + "1: :->cmds" \ + '*::arg:->args' + + case $state in + cmds) + _lets_commands + ;; + args) + _lets_command_options "${words[1]}" + ;; + esac +} + +# Check if in folder with correct lets.yaml file +_check_lets_config() { + ${LETS_EXECUTABLE} 1>/dev/null 2>/dev/null + echo $? +} + +_lets_commands () { local cmds - # Check if in folder with correct lets.yaml file - lets 1>/dev/null 2>/dev/null - if [ $? -eq 0 ]; then - IFS=$'\n' cmds=($(lets completion --list {{.Verbose}})) + if [ $(_check_lets_config) -eq 0 ]; then + IFS=$'\n' cmds=($(${LETS_EXECUTABLE} completion --commands --verbose)) else cmds=() fi _describe -t commands 'Available commands' cmds } -_arguments -C -s "1: :{_list}" '*::arg:->args' -- +_lets_command_options () { + local cmd=$1 + + if [ $(_check_lets_config) -eq 0 ]; then + IFS=$'\n' + _arguments -s $(${LETS_EXECUTABLE} completion --options=${cmd} --verbose) + fi +} ` const bashCompletionText = `_lets_completion() { @@ -97,7 +130,59 @@ func getCommandsList(rootCmd *cobra.Command, w io.Writer, verbose bool) error { return nil } -func initCompletionCmd(rootCmd *cobra.Command) { +type option struct { + name string + desc string +} + +// generate string of command options joined with \n. +func getCommandOptions(command config.Command, w io.Writer, verbose bool) error { + if command.Docopts == "" { + return nil + } + + rawOpts, err := parser.ParseDocoptsOptions(command.Docopts, command.Name) + if err != nil { + return fmt.Errorf("can not parse docopts: %w", err) + } + + var options []option + + for _, opt := range rawOpts { + if strings.HasPrefix(opt.Name, "--") { + options = append(options, option{name: opt.Name, desc: opt.Description}) + } + } + + sort.SliceStable(options, func(i, j int) bool { + return options[i].name < options[j].name + }) + + buf := new(bytes.Buffer) + + for _, option := range options { + if verbose { + desc := fmt.Sprintf("No description for option %s", option.name) + + if option.desc != "" { + desc = strings.TrimSpace(option.desc) + } + + buf.WriteString(fmt.Sprintf("%[1]s[%s]\n", option.name, desc)) + } else { + buf.WriteString(fmt.Sprintf("%s\n", option.name)) + } + } + + _, err = buf.WriteTo(w) + if err != nil { + return fmt.Errorf("can not generate command options list: %w", err) + } + + return nil +} + +func initCompletionCmd(rootCmd *cobra.Command, cfg *config.Config) { completionCmd := &cobra.Command{ Use: "completion", Hidden: true, @@ -107,16 +192,43 @@ func initCompletionCmd(rootCmd *cobra.Command) { if err != nil { return fmt.Errorf("can not get flag 'shell': %w", err) } + verbose, err := cmd.Flags().GetBool("verbose") if err != nil { return fmt.Errorf("can not get flag 'verbose': %w", err) } + list, err := cmd.Flags().GetBool("list") if err != nil { return fmt.Errorf("can not get flag 'list': %w", err) } + commands, err := cmd.Flags().GetBool("commands") + if err != nil { + return fmt.Errorf("can not get flag 'commands': %w", err) + } + if list { + commands = true + } + + optionsForCmd, err := cmd.Flags().GetString("options") + if err != nil { + return fmt.Errorf("can not get flag 'options': %w", err) + } + + if optionsForCmd != "" { + if cfg == nil { + return fmt.Errorf("can not read config") + } + command, exists := cfg.Commands[optionsForCmd] + if !exists { + return fmt.Errorf("command %s not declared in config", optionsForCmd) + } + return getCommandOptions(command, cmd.OutOrStdout(), verbose) + } + + if commands { return getCommandsList(rootCmd, cmd.OutOrStdout(), verbose) } @@ -136,8 +248,10 @@ func initCompletionCmd(rootCmd *cobra.Command) { } completionCmd.Flags().StringP("shell", "s", "", "The type of shell (bash or zsh)") - completionCmd.Flags().Bool("list", false, "Show list of commands") - completionCmd.Flags().Bool("verbose", false, "Verbose list of commands (with description) (only for zsh)") + completionCmd.Flags().Bool("list", false, "Show list of commands [deprecated, use --commands]") + completionCmd.Flags().Bool("commands", false, "Show list of commands") + completionCmd.Flags().String("options", "", "Show list of options for command") + completionCmd.Flags().Bool("verbose", false, "Verbose list of commands or options (with description) (only for zsh)") rootCmd.AddCommand(completionCmd) } diff --git a/cmd/root.go b/cmd/root.go index 64142171..8fed29b9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,7 +33,7 @@ func newRootCmd(version string) *cobra.Command { func CreateRootCommandWithConfig(out io.Writer, cfg *config.Config, version string) *cobra.Command { rootCmd := newRootCmd(version) - initRootCommand(rootCmd) + initRootCommand(rootCmd, cfg) initSubCommands(rootCmd, cfg, out) return rootCmd @@ -43,7 +43,7 @@ func CreateRootCommandWithConfig(out io.Writer, cfg *config.Config, version stri func CreateRootCommand(version string) *cobra.Command { rootCmd := newRootCmd(version) - initRootCommand(rootCmd) + initRootCommand(rootCmd, nil) return rootCmd } @@ -64,8 +64,8 @@ func ConfigErrorCheck(rootCmd *cobra.Command, err error) { } } -func initRootCommand(rootCmd *cobra.Command) { - initCompletionCmd(rootCmd) +func initRootCommand(rootCmd *cobra.Command, cfg *config.Config) { + initCompletionCmd(rootCmd, cfg) rootCmd.Flags().StringToStringP("env", "E", nil, "set env variable for running command KEY=VALUE") rootCmd.Flags().StringArray("only", []string{}, "run only specified command(s) described in cmd as map") rootCmd.Flags().StringArray("exclude", []string{}, "run all but excluded command(s) described in cmd as map") diff --git a/config/parser/docopts.go b/config/parser/docopts.go index 4b1d6912..3919f465 100644 --- a/config/parser/docopts.go +++ b/config/parser/docopts.go @@ -8,7 +8,7 @@ import ( "github.com/docopt/docopt-go" ) -var DocoptParser = &docopt.Parser{ +var docoptParser = &docopt.Parser{ HelpHandler: docopt.NoHelpHandler, OptionsFirst: false, SkipHelpFlags: false, @@ -21,7 +21,12 @@ func ParseDocopts(args []string, docopts string) (docopt.Opts, error) { return docopt.Opts{}, nil } - return DocoptParser.ParseArgs(docopts, args, "") + return docoptParser.ParseArgs(docopts, args, "") +} + +// ParseDocoptsOptions parses docopts only to get all available options for a command +func ParseDocoptsOptions(docopts string, cmdName string) ([]docopt.Option, error) { + return docoptParser.ParseOptions(docopts, []string{cmdName}) } func OptsToLetsOpt(opts docopt.Opts) map[string]string { diff --git a/docker/Dockerfile b/docker/Dockerfile index 7a85ae1c..945a3408 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,11 @@ -FROM golang:1.18-buster +FROM golang:1.18.3-bullseye ENV GOPROXY https://proxy.golang.org WORKDIR /app -RUN apt-get update && apt-get install git gcc +RUN apt-get update && apt-get install -y \ + git gcc \ + zsh # for zsh completion tests RUN cd /tmp && \ git clone https://github.com/bats-core/bats-core && \ diff --git a/go.mod b/go.mod index 1b8572d6..f31d6ddd 100644 --- a/go.mod +++ b/go.mod @@ -23,3 +23,5 @@ require ( gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) + +replace github.com/docopt/docopt-go => github.com/kindermax/docopt.go v0.7.1 diff --git a/go.sum b/go.sum index bb019556..ea892b15 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -79,6 +77,8 @@ github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kindermax/docopt.go v0.7.1 h1:8jvJtUtUGsU9qkMhQDkaoWJZqDXx8lnBoaYKtgsGS3U= +github.com/kindermax/docopt.go v0.7.1/go.mod h1:VlXA+8GArbisi1Ja07kavKt/UOrJFDWJk6EhMN6KdAU= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/build.yaml b/lets.build.yaml similarity index 100% rename from build.yaml rename to lets.build.yaml diff --git a/lets.yaml b/lets.yaml index e1908d8f..58f249e6 100644 --- a/lets.yaml +++ b/lets.yaml @@ -1,19 +1,12 @@ shell: bash mixins: - - build.yaml + - lets.build.yaml - -lets.my.yaml env: - NAME: "max" - AGE: 27 - CURRENT_UID: - sh: echo "`id -u`:`id -g`" - NGINX_DEV: - checksum: [go.mod, go.sum] - -eval_env: - CURRENT_UID: echo "`id -u`:`id -g`" + CURRENT_UID: + sh: echo "`id -u`:`id -g`" commands: release: @@ -41,6 +34,8 @@ commands: depends: [build-lets-image] options: | Usage: lets test-bats [] + Example: + lets test-bats config_version.bats cmd: | docker-compose run --rm test-bats diff --git a/tests/test_helpers.bash b/tests/test_helpers.bash index 646724f7..8ce34693 100755 --- a/tests/test_helpers.bash +++ b/tests/test_helpers.bash @@ -2,6 +2,10 @@ cleanup() { rm -rf .lets } +cleanup_completion() { + rm -rf _lets +} + # Usage: # my_array=(2,4,1) diff --git a/tests/test_helpers_completion.bash b/tests/test_helpers_completion.bash new file mode 100644 index 00000000..8562e222 --- /dev/null +++ b/tests/test_helpers_completion.bash @@ -0,0 +1,64 @@ +#! /bin/zsh +autoload -Uz compinit && compinit + +compdef _my-command my-command +_my-command () { + _arguments '--help[display help text]' # Just an example. +} + +# Define our test function. +comptest () { + # Gather all matching completions in this array. + # -U discards duplicates. + typeset -aU completions=() + + # Override the builtin compadd command. + compadd () { + # Gather all matching completions for this call in $reply. + # Note that this call overwrites the specified array. + # Therefore we cannot use $completions directly. + builtin compadd -O reply "$@" + + completions+=("$reply[@]") # Collect them. + builtin compadd "$@" # Run the actual command. + } + + # Bind a custom widget to TAB. + bindkey "^I" complete-word + zle -C {,,}complete-word + complete-word () { + # Make the completion system believe we're on a normal + # command line, not in vared. + unset 'compstate[vared]' + + _main_complete "$@" # Generate completions. + + # Print out our completions. + # Use of ^B and ^C as delimiters here is arbitrary. + # Just use something that won't normally be printed. + print -n $'\C-B' + print -nlr -- "$completions[@]" # Print one per line. + print -n $'\C-C' + exit + } + + vared -c tmp +} + +generate_completions() { + zmodload zsh/zpty # Load the pseudo terminal module. + zpty {,}comptest # Create a new pty and run our function in it. + + # Simulate a command being typed, ending with TAB to get completions. + zpty -w comptest $'my-command --h\t' + + # Read up to the first delimiter. Discard all of this. + zpty -r comptest REPLY $'*\C-B' + + zpty -r comptest REPLY $'*\C-C' # Read up to the second delimiter. + + # Print out the results. + print -r -- "${REPLY%$'\C-C'}" # Trim off the ^C, just in case. + + zpty -d comptest # Delete the pty. +} diff --git a/tests/zsh_completion.bats b/tests/zsh_completion.bats new file mode 100644 index 00000000..4cbd1824 --- /dev/null +++ b/tests/zsh_completion.bats @@ -0,0 +1,51 @@ +load test_helpers + +setup() { + load "${BATS_UTILS_PATH}/bats-support/load.bash" + load "${BATS_UTILS_PATH}/bats-assert/load.bash" + cd ./tests/zsh_completion + cleanup + cleanup_completion +} + +create_completion() { + lets completion --shell=zsh > _lets +} + +@test "zsh_completion: should complete run command" { + create_completion + run ./completion_helper.sh "lets r" + + assert_success + assert_output "run" +} + +@test "zsh_completion: should complete run command options" { + create_completion + run ./completion_helper.sh "lets run --" + + assert_success + assert_output <cmds" \ + '*::arg:->args' + + case $state in + cmds) + _lets_commands + ;; + args) + _lets_command_options "${words[1]}" + ;; + esac +} + +# Check if in folder with correct lets.yaml file +_check_lets_config() { + ${LETS_EXECUTABLE} 1>/dev/null 2>/dev/null + echo $? +} + +_lets_commands () { + local cmds + + if [ $(_check_lets_config) -eq 0 ]; then + IFS=$'\n' cmds=($(${LETS_EXECUTABLE} completion --commands --verbose)) + else + cmds=() + fi + _describe -t commands 'Available commands' cmds +} + +_lets_command_options () { + local cmd=$1 + + if [ $(_check_lets_config) -eq 0 ]; then + IFS=$'\n' + _arguments -s $(${LETS_EXECUTABLE} completion --options=${cmd} --verbose) + fi +} +#--------------------- +# Define our test function. +comptest () { + # Gather all matching completions in this array. + # -U discards duplicates. + typeset -aU completions=() + + # Override the builtin compadd command. + compadd () { + # Gather all matching completions for this call in $reply. + # Note that this call overwrites the specified array. + # Therefore we cannot use $completions directly. + builtin compadd -O reply "$@" + + completions+=("$reply[@]") # Collect them. + builtin compadd "$@" # Run the actual command. + } + + # Bind a custom widget to TAB. + bindkey "^I" complete-word + zle -C {,,}complete-word + complete-word () { + # Make the completion system believe we're on a normal + # command line, not in vared. + unset 'compstate[vared]' + + _main_complete "$@" # Generate completions. + + # Print out our completions. + # Use of ^B and ^C as delimiters here is arbitrary. + # Just use something that won't normally be printed. + print -n $'\C-B' + print -nlr -- "$completions[@]" # Print one per line. + print -n $'\C-C' + exit + } + + vared -c tmp +} + +generate_completions() { + zmodload zsh/zpty # Load the pseudo terminal module. + zpty {,}comptest lets # Create a new pty and run our function in it. + + # Simulate a command being typed, ending with TAB to get completions. + printf $'%s\t' $1 | zpty -w comptest + + # Read up to the first delimiter. Discard all of this. + zpty -r comptest REPLY $'*\C-B' + + zpty -r comptest REPLY $'*\C-C' # Read up to the second delimiter. + + # Print out the results. + print -r -- "${REPLY%$'\C-C'}" # Trim off the ^C, just in case. + + zpty -d comptest # Delete the pty. +} + +# Example usage. +# source ./completion_helper.sh +# generate_completions "lets r" +generate_completions "$@" diff --git a/tests/zsh_completion/lets.yaml b/tests/zsh_completion/lets.yaml new file mode 100644 index 00000000..b6cfc4e2 --- /dev/null +++ b/tests/zsh_completion/lets.yaml @@ -0,0 +1,11 @@ +shell: bash + +commands: + run: + description: Run application + options: | + usage: lets run [--debug] [--env=] + options: + -d, -debug Run in debug mode + --env= Run with env + cmd: echo Run debug=${LETSOPT_DEBUG} env=${LETSOPT_ENV} \ No newline at end of file From b4e0cd6c051d4c839f60182bf8b1adee7176bc1a Mon Sep 17 00:00:00 2001 From: Kindritskiy Max Date: Mon, 11 Jul 2022 22:33:57 +0300 Subject: [PATCH 2/3] fix lints --- .golangci.yaml | 3 ++- checksum/checksum.go | 3 +-- cmd/completion.go | 18 ++++++++++-------- config/config/command.go | 5 ++++- config/parser/cmd.go | 10 +++++----- config/parser/depends.go | 22 ++++++++++++---------- config/parser/docopts.go | 2 +- config/parser/parser.go | 6 +++--- config/validate.go | 2 +- main.go | 2 +- runner/run.go | 22 +++++++++++----------- 11 files changed, 51 insertions(+), 44 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 70c53372..4fad15e5 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -6,6 +6,7 @@ linters: enable-all: true disable: - typecheck + - gomoddirectives - containedctx - gochecknoglobals - goimports @@ -41,4 +42,4 @@ issues: - gomnd - path: set\.go linters: - - typecheck \ No newline at end of file + - typecheck diff --git a/checksum/checksum.go b/checksum/checksum.go index 31354ddf..409df7d7 100644 --- a/checksum/checksum.go +++ b/checksum/checksum.go @@ -1,7 +1,6 @@ package checksum import ( - // #nosec G505 "crypto/sha1" "fmt" @@ -196,7 +195,7 @@ func persistOneChecksum(dotLetsDir string, cmdName string, checksumName string, return fmt.Errorf("can not create checksum dir at %s: %w", checksumDirPath, err) } - f, err := os.OpenFile(checksumFilePath, os.O_CREATE|os.O_WRONLY, 0755) + f, err := os.OpenFile(checksumFilePath, os.O_CREATE|os.O_WRONLY, 0o755) if err != nil { return fmt.Errorf("can not open file %s to persist checksum: %w", checksumFilePath, err) } diff --git a/cmd/completion.go b/cmd/completion.go index 6252be49..ccf8cab0 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -74,18 +74,18 @@ complete -o filenames -F _lets_completion lets ` // generate bash completion script. -func genBashCompletion(w io.Writer) error { +func genBashCompletion(out io.Writer) error { tmpl, err := template.New("Main").Parse(bashCompletionText) if err != nil { return fmt.Errorf("error creating zsh completion template: %w", err) } - return tmpl.Execute(w, nil) + return tmpl.Execute(out, nil) } // generate zsh completion script. // if verbose passed - generate completion with description. -func genZshCompletion(w io.Writer, verbose bool) error { +func genZshCompletion(out io.Writer, verbose bool) error { tmpl, err := template.New("Main").Parse(zshCompletionText) if err != nil { return fmt.Errorf("error creating zsh completion template: %w", err) @@ -99,11 +99,11 @@ func genZshCompletion(w io.Writer, verbose bool) error { data.Verbose = "--verbose" } - return tmpl.Execute(w, data) + return tmpl.Execute(out, data) } // generate string of commands joined with \n. -func getCommandsList(rootCmd *cobra.Command, w io.Writer, verbose bool) error { +func getCommandsList(rootCmd *cobra.Command, out io.Writer, verbose bool) error { buf := new(bytes.Buffer) for _, cmd := range rootCmd.Commands() { @@ -122,7 +122,7 @@ func getCommandsList(rootCmd *cobra.Command, w io.Writer, verbose bool) error { } } - _, err := buf.WriteTo(w) + _, err := buf.WriteTo(out) if err != nil { return fmt.Errorf("can not generate commands list: %w", err) } @@ -136,7 +136,7 @@ type option struct { } // generate string of command options joined with \n. -func getCommandOptions(command config.Command, w io.Writer, verbose bool) error { +func getCommandOptions(command config.Command, out io.Writer, verbose bool) error { if command.Docopts == "" { return nil } @@ -174,7 +174,7 @@ func getCommandOptions(command config.Command, w io.Writer, verbose bool) error } } - _, err = buf.WriteTo(w) + _, err = buf.WriteTo(out) if err != nil { return fmt.Errorf("can not generate command options list: %w", err) } @@ -221,10 +221,12 @@ func initCompletionCmd(rootCmd *cobra.Command, cfg *config.Config) { if cfg == nil { return fmt.Errorf("can not read config") } + command, exists := cfg.Commands[optionsForCmd] if !exists { return fmt.Errorf("command %s not declared in config", optionsForCmd) } + return getCommandOptions(command, cmd.OutOrStdout(), verbose) } diff --git a/config/config/command.go b/config/config/command.go index 620e0d20..9edbce46 100644 --- a/config/config/command.go +++ b/config/config/command.go @@ -100,7 +100,10 @@ func (cmd Command) WithEnv(env map[string]string) Command { } func (cmd Command) Pretty() string { - pretty, _ := json.MarshalIndent(cmd, "", " ") + pretty, err := json.MarshalIndent(cmd, "", " ") + if err != nil { + return "" + } return string(pretty) } diff --git a/config/parser/cmd.go b/config/parser/cmd.go index bbddbbe4..de06c2c9 100644 --- a/config/parser/cmd.go +++ b/config/parser/cmd.go @@ -46,8 +46,8 @@ func parseCmd(cmd interface{}, newCmd *config.Command) error { cmdList := make([]string, 0, len(cmd)+len(proxyArgs)) - for _, v := range cmd { - if v == nil { + for _, value := range cmd { + if value == nil { return parseError( "got nil in cmd list", newCmd.Name, @@ -56,11 +56,11 @@ func parseCmd(cmd interface{}, newCmd *config.Command) error { ) } - cmdList = append(cmdList, fmt.Sprintf("%s", v)) + cmdList = append(cmdList, fmt.Sprintf("%s", value)) } - fullCommandList := append(cmdList, escapeArgs(proxyArgs)...) - newCmd.Cmd = strings.TrimSpace(strings.Join(fullCommandList, " ")) + cmdList = append(cmdList, escapeArgs(proxyArgs)...) + newCmd.Cmd = strings.TrimSpace(strings.Join(cmdList, " ")) case map[string]interface{}: cmdMap := make(map[string]string, len(cmd)) diff --git a/config/parser/depends.go b/config/parser/depends.go index 6f861952..79418456 100644 --- a/config/parser/depends.go +++ b/config/parser/depends.go @@ -26,7 +26,7 @@ func parseDependsAsMap(dep map[string]interface{}, cmdName string, idx int) (*co args := []string{} env := map[string]string{} - for key, v := range dep { + for key, rawValue := range dep { if _, exists := depKeysMap[key]; !exists { return nil, parseError( fmt.Sprintf("key of depend must be one of %s", depKeys), @@ -38,7 +38,7 @@ func parseDependsAsMap(dep map[string]interface{}, cmdName string, idx int) (*co switch key { case nameKey: - value, ok := v.(string) + value, ok := rawValue.(string) if !ok { return nil, &ParseError{ CommandName: cmdName, @@ -51,7 +51,7 @@ func parseDependsAsMap(dep map[string]interface{}, cmdName string, idx int) (*co } name = value case argsKey: - switch value := v.(type) { + switch value := rawValue.(type) { case string: args = append(args, value) case []interface{}: @@ -74,12 +74,14 @@ func parseDependsAsMap(dep map[string]interface{}, cmdName string, idx int) (*co Err: fmt.Errorf( "field '%s': %s", fmt.Sprintf("%s.[%d][name:%s]", DEPENDS, idx, name), - fmt.Sprintf("value of 'args' must be a string or an array of string, got: %#v", v)), + fmt.Sprintf("value of 'args' must be a string or an array of string, got: %#v", value)), } } case envKey: - for envName, envValue := range v.(map[string]interface{}) { - env[envName] = fmt.Sprintf("%v", envValue) + if envMap, ok := rawValue.(map[string]interface{}); ok { + for envName, envValue := range envMap { + env[envName] = fmt.Sprintf("%v", envValue) + } } } } @@ -107,14 +109,14 @@ func parseDepends(rawDepends interface{}, newCmd *config.Command) error { dependencies := make(map[string]config.Dep, len(depends)) dependsNames := make([]string, 0, len(depends)) - for idx, value := range depends { - switch v := value.(type) { + for idx, rawValue := range depends { + switch value := rawValue.(type) { case string: - dep := &config.Dep{Name: v, Args: []string{}} + dep := &config.Dep{Name: value, Args: []string{}} dependencies[dep.Name] = *dep dependsNames = append(dependsNames, dep.Name) case map[string]interface{}: - dep, err := parseDependsAsMap(v, newCmd.Name, idx) + dep, err := parseDependsAsMap(value, newCmd.Name, idx) if err != nil { return err } diff --git a/config/parser/docopts.go b/config/parser/docopts.go index 3919f465..5ccac66c 100644 --- a/config/parser/docopts.go +++ b/config/parser/docopts.go @@ -24,7 +24,7 @@ func ParseDocopts(args []string, docopts string) (docopt.Opts, error) { return docoptParser.ParseArgs(docopts, args, "") } -// ParseDocoptsOptions parses docopts only to get all available options for a command +// ParseDocoptsOptions parses docopts only to get all available options for a command. func ParseDocoptsOptions(docopts string, cmdName string) ([]docopt.Option, error) { return docoptParser.ParseOptions(docopts, []string{cmdName}) } diff --git a/config/parser/parser.go b/config/parser/parser.go index a30e31eb..b54ef243 100644 --- a/config/parser/parser.go +++ b/config/parser/parser.go @@ -288,8 +288,8 @@ func parseCommands(cmds map[string]interface{}, cfg *config.Config) ([]config.Co case map[string]interface{}: rawCmd = rawValue case map[interface{}]interface{}: - for k, v := range rawValue { - k, ok := k.(string) + for key, value := range rawValue { + key, ok := key.(string) if !ok { return []config.Command{}, newConfigParseError( "command directive must be a string", @@ -297,7 +297,7 @@ func parseCommands(cmds map[string]interface{}, cfg *config.Config) ([]config.Co "", ) } - rawCmd[k] = v + rawCmd[key] = value } default: return []config.Command{}, newConfigParseError( diff --git a/config/validate.go b/config/validate.go index 4f1e734f..b7e2e879 100644 --- a/config/validate.go +++ b/config/validate.go @@ -21,7 +21,7 @@ func withColor(msg string) string { } // Validate loaded config. -// nolint:revive + func validate(config *config.Config, letsVersion string) error { if err := validateVersion(config, letsVersion); err != nil { return err diff --git a/main.go b/main.go index b7336a40..eb5cb69f 100644 --- a/main.go +++ b/main.go @@ -48,7 +48,7 @@ func main() { log.Error(err.Error()) exitCode := 1 - if e, ok := err.(*runner.RunErr); ok { //nolint:errorlint + if e, ok := err.(*runner.RunError); ok { //nolint:errorlint exitCode = e.ExitCode() } diff --git a/runner/run.go b/runner/run.go index 01afe174..44426420 100644 --- a/runner/run.go +++ b/runner/run.go @@ -35,16 +35,16 @@ func debugf(format string, a ...interface{}) { log.Debugf(colored(prefixed, color)) } -type RunErr struct { +type RunError struct { err error } -func (e *RunErr) Error() string { +func (e *RunError) Error() string { return e.err.Error() } // ExitCode will return exit code from underlying ExitError or returns default error code. -func (e *RunErr) ExitCode() int { +func (e *RunError) ExitCode() int { var exitErr *exec.ExitError if ok := errors.As(e.err, &exitErr); ok { return exitErr.ExitCode() @@ -145,7 +145,7 @@ func (r *Runner) runChild(ctx context.Context) error { ) if err := cmd.Run(); err != nil { - return &RunErr{err: fmt.Errorf("failed to run child command '%s' from 'depends': %w", r.cmd.Name, err)} + return &RunError{err: fmt.Errorf("failed to run child command '%s' from 'depends': %w", r.cmd.Name, err)} } // persist checksum only if exit code 0 @@ -162,8 +162,8 @@ func (r *Runner) runAfterScript() { debugf("executing after script:\ncommand: %s\nscript: %s\nenv: %s", r.cmd.Name, r.cmd.After, fmtEnv(cmd.Env)) - if runErr := cmd.Run(); runErr != nil { - log.Printf("failed to run `after` script for command '%s': %s", r.cmd.Name, runErr) + if RunError := cmd.Run(); RunError != nil { + log.Printf("failed to run `after` script for command '%s': %s", r.cmd.Name, RunError) } } @@ -314,7 +314,7 @@ func (r *Runner) runDepends(ctx context.Context) error { dependCmd := r.cfg.Commands[depName] if dependCmd.CmdMap != nil { // forbid to run depends command as map - return &RunErr{ + return &RunError{ err: fmt.Errorf( "failed to run child command '%s' from 'depends': cmd as map is not allowed in depends yet", r.cmd.Name, @@ -381,7 +381,7 @@ func (r *Runner) runCmdScript(cmdScript string) error { debugf("executing os command for '%s'\ncmd: %s\nenv: %s\n", r.cmd.Name, r.cmd.Cmd, fmtEnv(cmd.Env)) if err := cmd.Run(); err != nil { - return &RunErr{err: fmt.Errorf("failed to run command '%s': %w", r.cmd.Name, err)} + return &RunError{err: fmt.Errorf("failed to run command '%s': %w", r.cmd.Name, err)} } return nil @@ -447,7 +447,7 @@ func (r *Runner) runCmdAsMap(ctx context.Context) (err error) { return err } - g, _ := errgroup.WithContext(ctx) + group, _ := errgroup.WithContext(ctx) cmdMap, err := filterCmdMap(r.cmd.Name, r.cmd.CmdMap, r.cmd.Only, r.cmd.Exclude) if err != nil { @@ -457,12 +457,12 @@ func (r *Runner) runCmdAsMap(ctx context.Context) (err error) { for _, cmdExecScript := range cmdMap { cmdExecScript := cmdExecScript // wait for cmd to end in a goroutine with error propagation - g.Go(func() error { + group.Go(func() error { return r.runCmdScript(cmdExecScript) }) } - if err = g.Wait(); err != nil { + if err = group.Wait(); err != nil { return err //nolint:wrapcheck } From f61311c9710cdca4a53e73fcd472f7a90bc0fef4 Mon Sep 17 00:00:00 2001 From: Kindritskiy Max Date: Mon, 11 Jul 2022 23:20:38 +0300 Subject: [PATCH 3/3] fix tests for zsh --- .github/workflows/test.yaml | 2 +- docker-compose.yml | 12 ++-- lets.yaml | 10 ++- tests/test_helpers.bash | 4 -- tests/test_helpers_completion.bash | 64 ------------------- ...h_completion.bats => zsh_completion.bats_} | 13 +--- tests/zsh_completion/completion_helper.sh | 53 +-------------- 7 files changed, 19 insertions(+), 139 deletions(-) delete mode 100644 tests/test_helpers_completion.bash rename tests/{zsh_completion.bats => zsh_completion.bats_} (76%) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6d01404b..e83e9ff1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,7 +40,7 @@ jobs: with: version: latest - name: Test bats - run: lets test-bats + run: timeout 120 lets test-bats lint: runs-on: ubuntu-latest diff --git a/docker-compose.yml b/docker-compose.yml index b1a78766..2e00e3b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,13 +29,13 @@ services: environment: NO_COLOR: 1 BATS_UTILS_PATH: /bats - command: | - bash -c ' + command: + - bash + - -c + - | go build -o /usr/bin/lets *.go - ARGS="--tap --verbose-run --trace" if [[ -n "${LETSOPT_TEST}" ]]; then - bats ${ARGS} tests/"${LETSOPT_TEST}" + bats tests/"${LETSOPT_TEST}" ${LETSOPT_OPTS} else - bats ${ARGS} tests + bats tests ${LETSOPT_OPTS} fi - ' diff --git a/lets.yaml b/lets.yaml index 58f249e6..9aefba1e 100644 --- a/lets.yaml +++ b/lets.yaml @@ -33,17 +33,25 @@ commands: description: Run bats tests depends: [build-lets-image] options: | - Usage: lets test-bats [] + Usage: lets test-bats [] [--opts=] Example: lets test-bats config_version.bats cmd: | docker-compose run --rm test-bats + test-completions: + ref: test-bats + args: zsh_completion.bats_ + description: | + Run completions tests + This tests are separate because it hangs on Github Actions + test: description: Run unit and bats tests depends: - test-unit - test-bats + - test-completions coverage: description: Run tests for lets diff --git a/tests/test_helpers.bash b/tests/test_helpers.bash index 8ce34693..646724f7 100755 --- a/tests/test_helpers.bash +++ b/tests/test_helpers.bash @@ -2,10 +2,6 @@ cleanup() { rm -rf .lets } -cleanup_completion() { - rm -rf _lets -} - # Usage: # my_array=(2,4,1) diff --git a/tests/test_helpers_completion.bash b/tests/test_helpers_completion.bash deleted file mode 100644 index 8562e222..00000000 --- a/tests/test_helpers_completion.bash +++ /dev/null @@ -1,64 +0,0 @@ -#! /bin/zsh -autoload -Uz compinit && compinit - -compdef _my-command my-command -_my-command () { - _arguments '--help[display help text]' # Just an example. -} - -# Define our test function. -comptest () { - # Gather all matching completions in this array. - # -U discards duplicates. - typeset -aU completions=() - - # Override the builtin compadd command. - compadd () { - # Gather all matching completions for this call in $reply. - # Note that this call overwrites the specified array. - # Therefore we cannot use $completions directly. - builtin compadd -O reply "$@" - - completions+=("$reply[@]") # Collect them. - builtin compadd "$@" # Run the actual command. - } - - # Bind a custom widget to TAB. - bindkey "^I" complete-word - zle -C {,,}complete-word - complete-word () { - # Make the completion system believe we're on a normal - # command line, not in vared. - unset 'compstate[vared]' - - _main_complete "$@" # Generate completions. - - # Print out our completions. - # Use of ^B and ^C as delimiters here is arbitrary. - # Just use something that won't normally be printed. - print -n $'\C-B' - print -nlr -- "$completions[@]" # Print one per line. - print -n $'\C-C' - exit - } - - vared -c tmp -} - -generate_completions() { - zmodload zsh/zpty # Load the pseudo terminal module. - zpty {,}comptest # Create a new pty and run our function in it. - - # Simulate a command being typed, ending with TAB to get completions. - zpty -w comptest $'my-command --h\t' - - # Read up to the first delimiter. Discard all of this. - zpty -r comptest REPLY $'*\C-B' - - zpty -r comptest REPLY $'*\C-C' # Read up to the second delimiter. - - # Print out the results. - print -r -- "${REPLY%$'\C-C'}" # Trim off the ^C, just in case. - - zpty -d comptest # Delete the pty. -} diff --git a/tests/zsh_completion.bats b/tests/zsh_completion.bats_ similarity index 76% rename from tests/zsh_completion.bats rename to tests/zsh_completion.bats_ index 4cbd1824..f5cade61 100644 --- a/tests/zsh_completion.bats +++ b/tests/zsh_completion.bats_ @@ -1,19 +1,13 @@ load test_helpers +load "${BATS_UTILS_PATH}/bats-support/load.bash" +load "${BATS_UTILS_PATH}/bats-assert/load.bash" setup() { - load "${BATS_UTILS_PATH}/bats-support/load.bash" - load "${BATS_UTILS_PATH}/bats-assert/load.bash" cd ./tests/zsh_completion cleanup - cleanup_completion -} - -create_completion() { - lets completion --shell=zsh > _lets } @test "zsh_completion: should complete run command" { - create_completion run ./completion_helper.sh "lets r" assert_success @@ -21,7 +15,6 @@ create_completion() { } @test "zsh_completion: should complete run command options" { - create_completion run ./completion_helper.sh "lets run --" assert_success @@ -32,7 +25,6 @@ EOF } @test "zsh_completion: should complete run command options: --debug" { - create_completion run ./completion_helper.sh "lets run --d" assert_success @@ -40,7 +32,6 @@ EOF } @test "zsh_completion: should complete run command options: --env" { - create_completion run ./completion_helper.sh "lets run --e" assert_success diff --git a/tests/zsh_completion/completion_helper.sh b/tests/zsh_completion/completion_helper.sh index 20cff2de..3136e47c 100755 --- a/tests/zsh_completion/completion_helper.sh +++ b/tests/zsh_completion/completion_helper.sh @@ -1,59 +1,8 @@ #! /bin/zsh - autoload -Uz compinit && compinit -# source _lets - -#--------------------- -compdef _lets lets - -LETS_EXECUTABLE=lets - -function _lets { - echo "Completing !!!!" - local state - - _arguments -C -s \ - "1: :->cmds" \ - '*::arg:->args' - - case $state in - cmds) - _lets_commands - ;; - args) - _lets_command_options "${words[1]}" - ;; - esac -} - -# Check if in folder with correct lets.yaml file -_check_lets_config() { - ${LETS_EXECUTABLE} 1>/dev/null 2>/dev/null - echo $? -} - -_lets_commands () { - local cmds +eval "$(echo "$(lets completion -s zsh)" | sed 's/#compdef/compdef/')" - if [ $(_check_lets_config) -eq 0 ]; then - IFS=$'\n' cmds=($(${LETS_EXECUTABLE} completion --commands --verbose)) - else - cmds=() - fi - _describe -t commands 'Available commands' cmds -} - -_lets_command_options () { - local cmd=$1 - - if [ $(_check_lets_config) -eq 0 ]; then - IFS=$'\n' - _arguments -s $(${LETS_EXECUTABLE} completion --options=${cmd} --verbose) - fi -} -#--------------------- -# Define our test function. comptest () { # Gather all matching completions in this array. # -U discards duplicates.