Skip to content

Commit

Permalink
bash: use double quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
rsteube committed Aug 11, 2023
1 parent b374e9e commit 9ecbf6a
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 66 deletions.
21 changes: 16 additions & 5 deletions example/cmd/_test/bash-ble.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@ _example_completion() {
export COMP_WORDBREAKS
export COMP_LINE

local compline="${COMP_LINE:0:${COMP_POINT}}"
local nospace data compline="${COMP_LINE:0:${COMP_POINT}}"

if echo ${compline}"''" | xargs echo 2>/dev/null > /dev/null; then
data=$(echo ${compline}"''" | xargs example _carapace bash)
elif echo ${compline} | sed "s/\$/'/" | xargs echo 2>/dev/null > /dev/null; then
data=$(echo ${compline} | sed "s/\$/'/" | xargs example _carapace bash)
else
data=$(echo ${compline} | sed 's/$/"/' | xargs example _carapace bash)
fi

IFS=$'\001' read -r -d '' nospace data <<<"${data}"
mapfile -t COMPREPLY < <(echo "${data}")
unset COMPREPLY[-1]

[ "${nospace}" = true ] && compopt -o nospace
local IFS=$'\n'
mapfile -t COMPREPLY < <(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs example _carapace bash)
[[ "${COMPREPLY[*]}" == "" ]] && COMPREPLY=() # fix for mapfile creating a non-empty array from empty command output

compopt -o nospace
}


complete -o noquote -F _example_completion example

_example_completion_ble() {
if [[ ${BLE_ATTACHED-} ]]; then
Expand Down
21 changes: 16 additions & 5 deletions example/cmd/_test/bash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ _example_completion() {
export COMP_WORDBREAKS
export COMP_LINE

local compline="${COMP_LINE:0:${COMP_POINT}}"
local nospace data compline="${COMP_LINE:0:${COMP_POINT}}"

if echo ${compline}"''" | xargs echo 2>/dev/null > /dev/null; then
data=$(echo ${compline}"''" | xargs example _carapace bash)
elif echo ${compline} | sed "s/\$/'/" | xargs echo 2>/dev/null > /dev/null; then
data=$(echo ${compline} | sed "s/\$/'/" | xargs example _carapace bash)
else
data=$(echo ${compline} | sed 's/$/"/' | xargs example _carapace bash)
fi

IFS=$'\001' read -r -d '' nospace data <<<"${data}"
mapfile -t COMPREPLY < <(echo "${data}")
unset COMPREPLY[-1]

[ "${nospace}" = true ] && compopt -o nospace
local IFS=$'\n'
mapfile -t COMPREPLY < <(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs example _carapace bash)
[[ "${COMPREPLY[*]}" == "" ]] && COMPREPLY=() # fix for mapfile creating a non-empty array from empty command output

compopt -o nospace
}

complete -F _example_completion example
complete -o noquote -F _example_completion example

40 changes: 25 additions & 15 deletions example/cmd/_test_files/files_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,37 @@ package testfiles

//go:generate touch -- -_minus_prefix.txt
//go:generate touch -- ampersand_&.txt
//go:generate touch -- angle-brackets_<.txt
//go:generate touch -- angle-brackets_<>.txt
//go:generate touch -- backslash2_\n.txt
//go:generate touch -- backslash3_\t.txt
//go:generate touch -- backslash_\.txt
//go:generate touch -- backtick_`.txt
//go:generate touch -- both_quotes_'".txt
//go:generate touch -- angle-bracket_left_<.txt
//go:generate touch -- angle-bracket_right_>.txt
//go:generate touch -- angle-bracket_both_<>.txt
//go:generate touch -- backslash_linebreak_\n.txt
//go:generate touch -- backslash_tab_\t.txt
//go:generate touch -- backslash_single_\.txt
//go:generate touch -- backslash_double_\\.txt
//go:generate touch -- backtick_single_`.txt
//go:generate touch -- backtick_double_``.txt
//go:generate touch -- colon_:.txt
//go:generate touch -- curly-bracket_{.txt
//go:generate touch -- curly-brackets_{}.txt
//go:generate touch -- dollar_$.txt
//go:generate touch -- double-quote_".txt
//go:generate touch -- curly-bracket_left_{.txt
//go:generate touch -- curly-bracket_right_}.txt
//go:generate touch -- curly-bracket_both_{}.txt
//go:generate touch -- dollar_single_$.txt
//go:generate touch -- dollar_round_$().txt
//go:generate touch -- "dollar_curly_${}.txt"
//go:generate touch -- quote_single_'.txt
//go:generate touch -- quote_double_".txt
//go:generate touch -- quote_both_'".txt
//go:generate touch -- hash_#.txt
//go:generate touch -- pipe_|.txt
//go:generate touch -- question_?.txt
//go:generate touch -- round-bracket_(.txt
//go:generate touch -- round-brackets_().txt
//go:generate touch -- round-bracket_left_(.txt
//go:generate touch -- round-bracket_right_).txt
//go:generate touch -- round-bracket_both_().txt
//go:generate touch -- semicolon_;.txt
//go:generate touch -- single-quote_'.txt
//go:generate touch -- "space_ .txt"
//go:generate touch -- square-bracket_[.txt
//go:generate touch -- square-brackets_[].txt
//go:generate touch -- square-bracket_left_[.txt
//go:generate touch -- square-bracket_right_].txt
//go:generate touch -- square-bracket_both_[].txt
//go:generate touch -- "star_*.txt"
//go:generate touch -- "star_*_match.txt"
//go:generate touch -- ~_tilde_prefix.txt
7 changes: 7 additions & 0 deletions example/cmd/multiparts.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func init() {
multipartsCmd.Flags().String("none-two", "", "multiparts without divider limited to 2")
multipartsCmd.Flags().String("none-three", "", "multiparts without divider limited to 3")
multipartsCmd.Flags().String("slash", "", "multiparts with / as divider")
multipartsCmd.Flags().String("space", "", "multiparts with space as divider")

rootCmd.AddCommand(multipartsCmd)

Expand Down Expand Up @@ -84,6 +85,12 @@ func init() {
}
}),
"slash": actionMultipartsTest("/"),
"space": carapace.ActionValues(
"one",
"two",
"three",
"four",
).UniqueList(" "),
})

carapace.Gen(multipartsCmd).PositionalCompletion(
Expand Down
3 changes: 1 addition & 2 deletions example/cmd/special.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ func init() {
})

carapace.Gen(specialCmd).PositionalCompletion(
carapace.ActionValues("positional1", "p1", "positional1 with space"),
carapace.ActionValues("positional2", "p2", "positional2 with space"),
carapace.ActionValues(`p1 & < > ' " { } $ # | ? ( ) ; [ ] * \ `+"`", "positional1"),
)

}
74 changes: 41 additions & 33 deletions internal/shell/bash/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,15 @@ var sanitizer = strings.NewReplacer(
"\t", ``,
)

var quoter = strings.NewReplacer(
// seems readline provides quotation only for the filename completion (which would add suffixes) so do that here
`&`, `\&`,
`<`, `\<`,
`>`, `\>`,
"`", "\\`",
`'`, `\'`,
var valueReplacer = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`{`, `\{`,
`}`, `\}`,
`$`, `\$`,
`#`, `\#`,
`|`, `\|`,
`?`, `\?`,
`(`, `\(`,
`)`, `\)`,
`;`, `\;`,
` `, `\ `,
`[`, `\[`,
`]`, `\]`,
`*`, `\*`,
`\`, `\\`,
"`", "\\`",
)

var displayReplacer = strings.NewReplacer(
`${`, `\\\${`,
)

func commonPrefix(a, b string) string {
Expand Down Expand Up @@ -72,16 +59,17 @@ func commonValuePrefix(values ...common.RawValue) (prefix string) {
func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
lastSegment := currentWord // last segment of currentWord split by COMP_WORDBREAKS

for valueIndex, value := range values {
// TODO optimize
if wordbreaks, ok := os.LookupEnv("COMP_WORDBREAKS"); ok {
wordbreaks = strings.Replace(wordbreaks, " ", "", -1)
if index := strings.LastIndexAny(currentWord, wordbreaks); index != -1 {
values[valueIndex].Value = strings.TrimPrefix(value.Value, currentWord[:index+1])
lastSegment = currentWord[index+1:]
}
}
}
// TODO handle wordbreaks correctly with carapace-shlex
// for valueIndex, value := range values {
// TODO optimize
// if wordbreaks, ok := os.LookupEnv("COMP_WORDBREAKS"); ok {
// wordbreaks = strings.Replace(wordbreaks, " ", "", -1)
// if index := strings.LastIndexAny(currentWord, wordbreaks); index != -1 {
// values[valueIndex].Value = strings.TrimPrefix(value.Value, currentWord[:index+1])
// lastSegment = currentWord[index+1:]
// }
// }
// }

if len(values) > 1 && commonDisplayPrefix(values...) != "" {
// When all display values have the same prefix bash will insert is as partial completion (which skips prefixes/formatting).
Expand All @@ -95,21 +83,41 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu
meta.Nospace.Add('*')
}

nospace := true
vals := make([]string, len(values))
for index, val := range values {
if len(values) == 1 {
vals[index] = quoter.Replace(sanitizer.Replace(val.Value))
if !meta.Nospace.Matches(val.Value) {
vals[index] = val.Value + " "
nospace = false
}

vals[index] = sanitizer.Replace(val.Value)
if requiresQuoting(vals[index]) {
vals[index] = valueReplacer.Replace(vals[index])
switch {
case strings.HasPrefix(vals[index], "~"): // assume homedir expansion
vals[index] = fmt.Sprintf(`~"%v"`, strings.TrimPrefix(vals[index], "~"))
default:
vals[index] = fmt.Sprintf(`"%v"`, vals[index])
}
}
} else {
val.Display = displayReplacer.Replace(val.Display)
val.Description = displayReplacer.Replace(val.Description)
if val.Description != "" {
vals[index] = fmt.Sprintf("%v (%v)", val.Display, sanitizer.Replace(val.TrimmedDescription()))
} else {
vals[index] = val.Display
}
}
}
return strings.Join(vals, "\n")
return fmt.Sprintf("%v\001%v", nospace, strings.Join(vals, "\n"))
}

func requiresQuoting(s string) bool {
chars := " \t\r\n`" + `[]{}()<>;|$&:*#`
chars += os.Getenv("COMP_WORDBREAKS")
chars += `\`
return strings.ContainsAny(s, chars)

}
23 changes: 17 additions & 6 deletions internal/shell/bash/snippet.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,27 @@ _%v_completion() {
export COMP_WORDBREAKS
export COMP_LINE
local compline="${COMP_LINE:0:${COMP_POINT}}"
local nospace data compline="${COMP_LINE:0:${COMP_POINT}}"
if echo ${compline}"''" | xargs echo 2>/dev/null > /dev/null; then
data=$(echo ${compline}"''" | xargs %v _carapace bash)
elif echo ${compline} | sed "s/\$/'/" | xargs echo 2>/dev/null > /dev/null; then
data=$(echo ${compline} | sed "s/\$/'/" | xargs %v _carapace bash)
else
data=$(echo ${compline} | sed 's/$/"/' | xargs %v _carapace bash)
fi
IFS=$'\001' read -r -d '' nospace data <<<"${data}"
mapfile -t COMPREPLY < <(echo "${data}")
unset COMPREPLY[-1]
[ "${nospace}" = true ] && compopt -o nospace
local IFS=$'\n'
mapfile -t COMPREPLY < <(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs %v _carapace bash)
[[ "${COMPREPLY[*]}" == "" ]] && COMPREPLY=() # fix for mapfile creating a non-empty array from empty command output
compopt -o nospace
}
complete -F _%v_completion %v
`, cmd.Name(), uid.Executable(), cmd.Name(), cmd.Name())
complete -o noquote -F _%v_completion %v
`, cmd.Name(), uid.Executable(), uid.Executable(), uid.Executable(), cmd.Name(), cmd.Name())

return result
}

0 comments on commit 9ecbf6a

Please sign in to comment.