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/.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/.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 649bfa7e..ccf8cab0 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() { @@ -41,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) @@ -66,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() { @@ -89,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) } @@ -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, out 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(out) + 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,45 @@ 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 +250,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/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 4b1d6912..5ccac66c 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/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/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/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..9aefba1e 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: @@ -40,15 +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/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 } diff --git a/tests/zsh_completion.bats_ b/tests/zsh_completion.bats_ new file mode 100644 index 00000000..f5cade61 --- /dev/null +++ b/tests/zsh_completion.bats_ @@ -0,0 +1,42 @@ +load test_helpers +load "${BATS_UTILS_PATH}/bats-support/load.bash" +load "${BATS_UTILS_PATH}/bats-assert/load.bash" + +setup() { + cd ./tests/zsh_completion + cleanup +} + +@test "zsh_completion: should complete run command" { + run ./completion_helper.sh "lets r" + + assert_success + assert_output "run" +} + +@test "zsh_completion: should complete run command options" { + run ./completion_helper.sh "lets run --" + + assert_success + assert_output <] + 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