From 6b3020f4270bd070efd12225c0aa8c7b213be5dc Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 14:03:23 +0200 Subject: [PATCH 1/8] added injection command --- assert/assert.go | 8 +++++++- example/cmd/injection.go | 26 ++++++++++++++++++++++++++ example/cmd/root_test.go | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 example/cmd/injection.go diff --git a/assert/assert.go b/assert/assert.go index e493e0fe1..e67af5857 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -3,6 +3,7 @@ package assert import ( "bytes" "log" + "strings" "testing" "github.com/alecthomas/chroma/quick" @@ -23,6 +24,11 @@ func Equal(t *testing.T, expected string, actual string) { } else { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(expected, actual, false) - t.Errorf("\nexpected: %v\nactual : %v", expected, dmp.DiffPrettyText(diffs)) + + replacer := strings.NewReplacer( + ``, ``, + ) + + t.Errorf("\nexpected: %v\nactual : %v", expected, replacer.Replace(dmp.DiffPrettyText(diffs))) } } diff --git a/example/cmd/injection.go b/example/cmd/injection.go new file mode 100644 index 000000000..08288a592 --- /dev/null +++ b/example/cmd/injection.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "github.com/rsteube/carapace" + "github.com/spf13/cobra" +) + +var injectionCmd = &cobra.Command{ + Use: "injection", + Short: "just trying to break things", +} + +func init() { + rootCmd.AddCommand(injectionCmd) + + carapace.Gen(injectionCmd).PositionalCompletion( + carapace.ActionValues("$(echo fail)"), + carapace.ActionValues("`echo fail`"), + carapace.ActionValues(`"; echo fail #`), + carapace.ActionValues(`"| echo fail #`), + carapace.ActionValues(`"&& echo fail #`), + carapace.ActionValues(`\$(echo fail)`), + carapace.ActionValues(`\`), + carapace.ActionValues(`LAST POSITIONAL VALUE`), + ) +} diff --git a/example/cmd/root_test.go b/example/cmd/root_test.go index 874abaf47..8de97dc50 100644 --- a/example/cmd/root_test.go +++ b/example/cmd/root_test.go @@ -34,7 +34,7 @@ _example_completions() { *) - COMPREPLY=($(compgen -W "action alias callback condition" -- $last)) + COMPREPLY=($(compgen -W "action alias callback condition injection" -- $last)) ;; esac fi @@ -127,6 +127,20 @@ _example_completions() { fi ;; + + '_example__injection' ) + if [[ $last == -* ]]; then + COMPREPLY=($()) + else + case $previous in + + *) + COMPREPLY=($(eval $(_example_callback '_'))) + ;; + esac + fi + ;; + esac } @@ -159,6 +173,7 @@ complete -c example -f -n '_example_state _example' -l toggle -s t -d 'Help mess complete -c example -f -n '_example_state _example ' -a 'action alias' -d 'action example' complete -c example -f -n '_example_state _example ' -a 'callback ' -d 'callback example' complete -c example -f -n '_example_state _example ' -a 'condition ' -d 'condition example' +complete -c example -f -n '_example_state _example ' -a 'injection ' -d 'just trying to break things' complete -c example -f -n '_example_state _example__action' -l custom -s c -d 'custom flag' -a '()' -r @@ -180,6 +195,9 @@ complete -c example -f -n '_example_state _example__callback' -a '(_example_call complete -c example -f -n '_example_state _example__condition' -l required -s r -d 'required flag' -a '(echo -e valid\ninvalid)' -r complete -c example -f -n '_example_state _example__condition' -a '(_example_callback _)' + + +complete -c example -f -n '_example_state _example__injection' -a '(_example_callback _)' ` assert.Equal(t, expected, carapace.Gen(rootCmd).Fish()) } @@ -207,6 +225,7 @@ function _example { "alias:action example" "callback:callback example" "condition:condition example" + "injection:just trying to break things" ) _describe "command" commands ;; @@ -225,6 +244,9 @@ function _example { condition) _example__condition ;; + injection) + _example__injection + ;; esac } @@ -255,6 +277,18 @@ function _example__condition { "(-r --required)"{-r,--required}"[required flag]: :_values '' valid invalid" \ "1:: : eval \$(${os_args[1]} _carapace zsh '_example__condition#1' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" } + +function _example__injection { + _arguments -C \ + "1:: :_values '' $(echo\ fail)" \ + "2:: :_values '' ` + "`" + `echo\ fail` + "`" + `" \ + "3:: :_values '' ";\ echo\ fail\ #" \ + "4:: :_values '' "|\ echo\ fail\ #" \ + "5:: :_values '' "&&\ echo\ fail\ #" \ + "6:: :_values '' \$(echo\ fail)" \ + "7:: :_values '' \" \ + "8:: :_values '' LAST\ POSITIONAL\ VALUE" +} if compquote '' 2>/dev/null; then _example; else compdef _example example; fi ` assert.Equal(t, expected, carapace.Gen(rootCmd).Zsh()) From de090a841d989169379066dd7103f20a77ba9209 Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 14:07:26 +0200 Subject: [PATCH 2/8] disable shellcheck for now --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e4d8d2350..66a95d2fb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,8 +16,8 @@ references: go build . curl -Lso shellcheck https://github.com/caarlos0/shellcheck-docker/releases/download/v0.4.6/shellcheck chmod +x shellcheck - ./shellcheck -e SC2148,SC2154 <(./example _carapace zsh) - ./shellcheck -e SC1064,SC1072,SC1073 <(./example _carapace fish) + # ./shellcheck -e SC2148,SC2154 <(./example _carapace zsh) + #./shellcheck -e SC1064,SC1072,SC1073 <(./example _carapace fish) jobs: go-current: From dc85589deb0d071939baf68b9ed6313b4c0c3f0b Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 14:54:48 +0200 Subject: [PATCH 3/8] bash: sanitize values --- bash/action.go | 23 +++++++++++++++++++---- example/cmd/injection.go | 1 + example/cmd/root_test.go | 15 ++++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/bash/action.go b/bash/action.go index 5b2b7ece2..310aa9112 100644 --- a/bash/action.go +++ b/bash/action.go @@ -5,6 +5,21 @@ import ( "strings" ) +var replacer = strings.NewReplacer( + `$`, ``, + "`", ``, + `\`, ``, + `"`, `'`, +) + +func Sanitize(values ...string) []string { + sanitized := make([]string, len(values)) + for index, value := range values { + sanitized[index] = replacer.Replace(value) + } + return sanitized +} + func Callback(prefix string, uid string) string { return fmt.Sprintf(`eval $(_%v_callback '%v')`, prefix, uid) } @@ -18,7 +33,7 @@ func ActionFiles(suffix string) string { } func ActionNetInterfaces() string { - return ActionValues(ActionExecute(`ifconfig -a | grep -o '^[^ :]\+' | tr '\n' ' '`)) + return `compgen -W "$(ifconfig -a | grep -o '^[^ :]\+' | tr '\n' ' ')" -- $last` } func ActionUsers() string { @@ -30,7 +45,7 @@ func ActionGroups() string { } func ActionHosts() string { - return ActionValues(ActionExecute(`cat ~/.ssh/known_hosts | cut -d ' ' -f1 | cut -d ',' -f1`)) + return `compgen -W "$(cat ~/.ssh/known_hosts | cut -d ' ' -f1 | cut -d ',' -f1)" -- $last` } func ActionValues(values ...string) string { @@ -39,7 +54,7 @@ func ActionValues(values ...string) string { } vals := make([]string, len(values)) - for index, val := range values { + for index, val := range Sanitize(values...) { // TODO escape special characters //vals[index] = strings.Replace(val, " ", `\ `, -1) vals[index] = val @@ -59,7 +74,7 @@ func ActionValuesDescribed(values ...string) string { } func ActionMessage(msg string) string { - return ActionValues("ERR", strings.Replace(msg, " ", "_", -1)) // TODO escape characters + return ActionValues("ERR", strings.Replace(Sanitize(msg)[0], " ", "_", -1)) // TODO escape characters } func ActionMultiParts(separator rune, values ...string) string { diff --git a/example/cmd/injection.go b/example/cmd/injection.go index 08288a592..848e752c3 100644 --- a/example/cmd/injection.go +++ b/example/cmd/injection.go @@ -15,6 +15,7 @@ func init() { carapace.Gen(injectionCmd).PositionalCompletion( carapace.ActionValues("$(echo fail)"), + carapace.ActionValues(`\$(echo fail)`), carapace.ActionValues("`echo fail`"), carapace.ActionValues(`"; echo fail #`), carapace.ActionValues(`"| echo fail #`), diff --git a/example/cmd/root_test.go b/example/cmd/root_test.go index 8de97dc50..e2f60a32e 100644 --- a/example/cmd/root_test.go +++ b/example/cmd/root_test.go @@ -281,13 +281,14 @@ function _example__condition { function _example__injection { _arguments -C \ "1:: :_values '' $(echo\ fail)" \ - "2:: :_values '' ` + "`" + `echo\ fail` + "`" + `" \ - "3:: :_values '' ";\ echo\ fail\ #" \ - "4:: :_values '' "|\ echo\ fail\ #" \ - "5:: :_values '' "&&\ echo\ fail\ #" \ - "6:: :_values '' \$(echo\ fail)" \ - "7:: :_values '' \" \ - "8:: :_values '' LAST\ POSITIONAL\ VALUE" + "2:: :_values '' \$(echo\ fail)" \ + "3:: :_values '' ` + "`" + `echo\ fail` + "`" + `" \ + "4:: :_values '' ";\ echo\ fail\ #" \ + "5:: :_values '' "|\ echo\ fail\ #" \ + "6:: :_values '' "&&\ echo\ fail\ #" \ + "7:: :_values '' \$(echo\ fail)" \ + "8:: :_values '' \" \ + "9:: :_values '' LAST\ POSITIONAL\ VALUE" } if compquote '' 2>/dev/null; then _example; else compdef _example example; fi ` From 73a574303574965c1d32fe11ebb850be3404b36c Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 15:20:58 +0200 Subject: [PATCH 4/8] fish: sanitize values --- assert/assert.go | 1 + bash/action.go | 4 ++-- example/cmd/root_test.go | 12 ++++++------ fish/action.go | 27 ++++++++++++++++++++++----- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/assert/assert.go b/assert/assert.go index e67af5857..8e27af55b 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -27,6 +27,7 @@ func Equal(t *testing.T, expected string, actual string) { replacer := strings.NewReplacer( ``, ``, + ``, ``, ) t.Errorf("\nexpected: %v\nactual : %v", expected, replacer.Replace(dmp.DiffPrettyText(diffs))) diff --git a/bash/action.go b/bash/action.go index 310aa9112..c2ff6dbbe 100644 --- a/bash/action.go +++ b/bash/action.go @@ -5,7 +5,7 @@ import ( "strings" ) -var replacer = strings.NewReplacer( +var sanitizer = strings.NewReplacer( `$`, ``, "`", ``, `\`, ``, @@ -15,7 +15,7 @@ var replacer = strings.NewReplacer( func Sanitize(values ...string) []string { sanitized := make([]string, len(values)) for index, value := range values { - sanitized[index] = replacer.Replace(value) + sanitized[index] = sanitizer.Replace(value) } return sanitized } diff --git a/example/cmd/root_test.go b/example/cmd/root_test.go index e2f60a32e..fff3b97f8 100644 --- a/example/cmd/root_test.go +++ b/example/cmd/root_test.go @@ -169,7 +169,7 @@ complete -c example -f complete -c example -f -n '_example_state _example' -l array -s a -d 'multiflag' -r complete -c example -f -n '_example_state _example' -l persistentFlag -s p -d 'Help message for persistentFlag' -complete -c example -f -n '_example_state _example' -l toggle -s t -d 'Help message for toggle' -a '(echo -e true\nfalse)' -r +complete -c example -f -n '_example_state _example' -l toggle -s t -d 'Help message for toggle' -a '(echo -e "true\nfalse")' -r complete -c example -f -n '_example_state _example ' -a 'action alias' -d 'action example' complete -c example -f -n '_example_state _example ' -a 'callback ' -d 'callback example' complete -c example -f -n '_example_state _example ' -a 'condition ' -d 'condition example' @@ -180,12 +180,12 @@ complete -c example -f -n '_example_state _example__action' -l custom -s c -d 'c complete -c example -f -n '_example_state _example__action' -l files -s f -d 'files flag' -a '(__fish_complete_suffix ".go")' -r complete -c example -f -n '_example_state _example__action' -l groups -s g -d 'groups flag' -a '(__fish_complete_groups)' -r complete -c example -f -n '_example_state _example__action' -l hosts -d 'hosts flag' -a '(__fish_print_hostnames)' -r -complete -c example -f -n '_example_state _example__action' -l message -s m -d 'message flag' -a '(echo -e ERR\tmessage example\n_\t\n\n)' -r -complete -c example -f -n '_example_state _example__action' -l multi_parts -d 'multi_parts flag' -a '(echo -e multi/parts\nmulti/parts/example\nmulti/parts/test\nexample/parts)' -r +complete -c example -f -n '_example_state _example__action' -l message -s m -d 'message flag' -a '(echo -e "ERR\tmessage example\n_")' -r +complete -c example -f -n '_example_state _example__action' -l multi_parts -d 'multi_parts flag' -a '(echo -e "multi/parts\nmulti/parts/example\nmulti/parts/test\nexample/parts")' -r complete -c example -f -n '_example_state _example__action' -l net_interfaces -s n -d 'net_interfaces flag' -a '(__fish_print_interfaces)' -r complete -c example -f -n '_example_state _example__action' -l users -s u -d 'users flag' -a '(__fish_complete_users)' -r -complete -c example -f -n '_example_state _example__action' -l values -s v -d 'values flag' -a '(echo -e values\nexample)' -r -complete -c example -f -n '_example_state _example__action' -l values_described -s d -d 'values with description flag' -a '(echo -e values\tvalueDescription\nexample\texampleDescription\n\n)' -r +complete -c example -f -n '_example_state _example__action' -l values -s v -d 'values flag' -a '(echo -e "values\nexample")' -r +complete -c example -f -n '_example_state _example__action' -l values_described -s d -d 'values with description flag' -a '(echo -e "values\tvalueDescription\nexample\texampleDescription\n\n")' -r complete -c example -f -n '_example_state _example__action' -a '(_example_callback _)' @@ -193,7 +193,7 @@ complete -c example -f -n '_example_state _example__callback' -l callback -s c - complete -c example -f -n '_example_state _example__callback' -a '(_example_callback _)' -complete -c example -f -n '_example_state _example__condition' -l required -s r -d 'required flag' -a '(echo -e valid\ninvalid)' -r +complete -c example -f -n '_example_state _example__condition' -l required -s r -d 'required flag' -a '(echo -e "valid\ninvalid")' -r complete -c example -f -n '_example_state _example__condition' -a '(_example_callback _)' diff --git a/fish/action.go b/fish/action.go index 35a8cd1d4..ce77d7ede 100644 --- a/fish/action.go +++ b/fish/action.go @@ -5,6 +5,23 @@ import ( "strings" ) +var sanitizer = strings.NewReplacer( + `$`, ``, + "`", ``, + `\`, ``, + `"`, `'`, + `(`, `[`, + `)`, `]`, +) + +func Sanitize(values ...string) []string { + sanitized := make([]string, len(values)) + for index, value := range values { + sanitized[index] = sanitizer.Replace(value) + } + return sanitized +} + func Callback(prefix string, uid string) string { return ActionExecute(fmt.Sprintf(`_%v_callback %v`, prefix, uid)) } @@ -39,27 +56,27 @@ func ActionValues(values ...string) string { } vals := make([]string, len(values)) - for index, val := range values { + for index, val := range Sanitize(values...) { // TODO escape special characters //vals[index] = strings.Replace(val, " ", `\ `, -1) vals[index] = val } - return ActionExecute(fmt.Sprintf(`echo -e %v`, strings.Join(vals, `\n`))) + return ActionExecute(fmt.Sprintf(`echo -e "%v"`, strings.Join(vals, `\n`))) } func ActionValuesDescribed(values ...string) string { // TODO verify length (description always exists) vals := make([]string, len(values)) - for index, val := range values { + for index, val := range Sanitize(values...) { if index%2 == 0 { vals[index/2] = fmt.Sprintf(`%v\t%v`, val, values[index+1]) } } - return ActionValues(vals...) + return ActionExecute(fmt.Sprintf(`echo -e "%v"`, strings.Join(vals, `\n`))) } func ActionMessage(msg string) string { - return ActionValuesDescribed("ERR", msg, "_", "") + return ActionExecute(fmt.Sprintf(`echo -e "ERR\t%v\n_"`, Sanitize(msg)[0])) } func ActionMultiParts(separator rune, values ...string) string { From 054dcbb74199e2262c9f1d971a3393c5af4945a0 Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 21:04:45 +0200 Subject: [PATCH 5/8] zsh: sanitize values --- zsh/action.go | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/zsh/action.go b/zsh/action.go index bcebebeff..018f6091b 100644 --- a/zsh/action.go +++ b/zsh/action.go @@ -5,6 +5,30 @@ import ( "strings" ) +var sanitizer = strings.NewReplacer( + `$`, ``, + "`", ``, + `\`, ``, + `"`, ``, + `'`, ``, + `|`, ``, + `>`, ``, + `<`, ``, + `&`, ``, + `(`, ``, + `)`, ``, + `;`, ``, + `#`, ``, +) + +func Sanitize(values ...string) []string { + sanitized := make([]string, len(values)) + for index, value := range values { + sanitized[index] = sanitizer.Replace(value) + } + return sanitized +} + func Callback(uid string) string { return ActionExecute(fmt.Sprintf(`${os_args[1]} _carapace zsh '%v' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"}`, uid)) } @@ -46,12 +70,13 @@ func ActionHosts() string { // ActionValues completes arbitrary keywords (values) func ActionValues(values ...string) string { - if len(strings.TrimSpace(strings.Join(values, ""))) == 0 { + sanitized := Sanitize(values...) + if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { return ActionMessage("no values to complete") } - vals := make([]string, len(values)) - for index, val := range values { + vals := make([]string, len(sanitized)) + for index, val := range sanitized { // TODO escape special characters vals[index] = strings.Replace(val, " ", `\ `, -1) } @@ -60,14 +85,19 @@ func ActionValues(values ...string) string { // ActionValuesDescribed completes arbitrary key (values) with an additional description (value, description pairs) func ActionValuesDescribed(values ...string) string { + sanitized := Sanitize(values...) + if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { + return ActionMessage("no values to complete") + } + // TODO verify length (description always exists) - vals := make([]string, len(values)) - for index, val := range values { + vals := make([]string, len(sanitized)) + for index, val := range sanitized { if index%2 == 0 { - vals[index/2] = fmt.Sprintf("'%v[%v]'", val, values[index+1]) + vals[index/2] = fmt.Sprintf("'%v[%v]'", strings.Replace(val, " ", `\ `, -1), strings.Replace(values[index+1], " ", `\ `, -1)) } } - return ActionValues(vals...) + return fmt.Sprintf(`_values '' %v`, strings.Join(vals, " ")) } // ActionMessage displays a help messages in places where no completions can be generated From 2446b869ef7b4da549115a976ed6c821a65539ae Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 21:11:30 +0200 Subject: [PATCH 6/8] fish: fix no values to complete --- fish/action.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fish/action.go b/fish/action.go index ce77d7ede..0b213c473 100644 --- a/fish/action.go +++ b/fish/action.go @@ -51,12 +51,13 @@ func ActionHosts() string { } func ActionValues(values ...string) string { - if len(strings.TrimSpace(strings.Join(values, ""))) == 0 { + sanitized := Sanitize(values...) + if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { return ActionMessage("no values to complete") } - vals := make([]string, len(values)) - for index, val := range Sanitize(values...) { + vals := make([]string, len(sanitized)) + for index, val := range sanitized { // TODO escape special characters //vals[index] = strings.Replace(val, " ", `\ `, -1) vals[index] = val @@ -65,9 +66,10 @@ func ActionValues(values ...string) string { } func ActionValuesDescribed(values ...string) string { + sanitized := Sanitize(values...) // TODO verify length (description always exists) - vals := make([]string, len(values)) - for index, val := range Sanitize(values...) { + vals := make([]string, len(sanitized)) + for index, val := range sanitized { if index%2 == 0 { vals[index/2] = fmt.Sprintf(`%v\t%v`, val, values[index+1]) } From 6ea40651a2e1265a201e544b6bcc92cf9d895a1c Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 21:15:54 +0200 Subject: [PATCH 7/8] fix test --- example/cmd/root_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/example/cmd/root_test.go b/example/cmd/root_test.go index fff3b97f8..841126b9e 100644 --- a/example/cmd/root_test.go +++ b/example/cmd/root_test.go @@ -280,14 +280,14 @@ function _example__condition { function _example__injection { _arguments -C \ - "1:: :_values '' $(echo\ fail)" \ - "2:: :_values '' \$(echo\ fail)" \ - "3:: :_values '' ` + "`" + `echo\ fail` + "`" + `" \ - "4:: :_values '' ";\ echo\ fail\ #" \ - "5:: :_values '' "|\ echo\ fail\ #" \ - "6:: :_values '' "&&\ echo\ fail\ #" \ - "7:: :_values '' \$(echo\ fail)" \ - "8:: :_values '' \" \ + "1:: :_values '' echo\ fail" \ + "2:: :_values '' echo\ fail" \ + "3:: :_values '' echo\ fail" \ + "4:: :_values '' \ echo\ fail\ " \ + "5:: :_values '' \ echo\ fail\ " \ + "6:: :_values '' \ echo\ fail\ " \ + "7:: :_values '' echo\ fail" \ + "8:: : _message -r 'no values to complete'" \ "9:: :_values '' LAST\ POSITIONAL\ VALUE" } if compquote '' 2>/dev/null; then _example; else compdef _example example; fi From 7a3f622e2806c1f61131d485958d92dc7ca1466f Mon Sep 17 00:00:00 2001 From: rsteube Date: Sun, 29 Mar 2020 21:17:41 +0200 Subject: [PATCH 8/8] re-enable shellcheck --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 66a95d2fb..e4d8d2350 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,8 +16,8 @@ references: go build . curl -Lso shellcheck https://github.com/caarlos0/shellcheck-docker/releases/download/v0.4.6/shellcheck chmod +x shellcheck - # ./shellcheck -e SC2148,SC2154 <(./example _carapace zsh) - #./shellcheck -e SC1064,SC1072,SC1073 <(./example _carapace fish) + ./shellcheck -e SC2148,SC2154 <(./example _carapace zsh) + ./shellcheck -e SC1064,SC1072,SC1073 <(./example _carapace fish) jobs: go-current: