-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathshell.go
More file actions
216 lines (193 loc) · 6.16 KB
/
Copy pathshell.go
File metadata and controls
216 lines (193 loc) · 6.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package climux
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"unicode"
"go.hotsrc.dev/climux/internal/argv"
"go.hotsrc.dev/climux/ir"
)
// completionEnvVar returns the name of the environment variable Run
// consults for shell completion, derived from rootName as EnableCompletion
// documents: uppercased, with every rune that is not a letter or digit
// mapped to '_', followed by "_COMPLETE".
func completionEnvVar(rootName string) string {
var b strings.Builder
for _, r := range rootName {
r = unicode.ToUpper(r)
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
r = '_'
}
b.WriteRune(r)
}
b.WriteString("_COMPLETE")
return b.String()
}
// shellFuncName returns prog reduced to characters legal in a bash or zsh
// function name, for the completion scripts to name their function after
// the program without risking a syntax error on an unusual program name.
func shellFuncName(prog string) string {
var b strings.Builder
for _, r := range prog {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
b.WriteRune(r)
} else {
b.WriteRune('_')
}
}
return b.String()
}
// completionHook answers cmd's shell completion environment variable when
// it is set to a value RunWithArgs recognizes, reporting handled as false
// when it is unset or holds anything else, so the caller falls through to
// parsing args as usual.
//
// Which variable to consult is a question about the tree, since it is
// named from the root's name, so cmd is the compiled tree that
// RunWithArgs already lowered rather than the configuration it was
// lowered from. A tree that does not compile never reaches here: its
// fault is reported first. See EnableCompletion and RunWithArgs.
func completionHook(cmd *ir.Command, stdout io.Writer) (code int, handled bool) {
rootName := cmd.Root.Name
varName := completionEnvVar(rootName)
val, ok := os.LookupEnv(varName)
if !ok {
return 0, false
}
switch val {
case "bash_source":
fmt.Fprint(stdout, bashSourceScript(rootName, varName))
return ExitCodeSuccess, true
case "zsh_source":
fmt.Fprint(stdout, zshSourceScript(rootName, varName))
return ExitCodeSuccess, true
case "bash_complete", "zsh_complete":
writeCompletionReply(cmd, stdout)
return ExitCodeSuccess, true
default:
return 0, false
}
}
// writeCompletionReply answers one completion request read from
// COMP_WORDS and COMP_CWORD, in the protocol completionEnvVar's value
// selects.
//
// # Shell completion protocol
//
// COMP_WORDS holds the command line's words, one per line, starting with
// the program name; COMP_CWORD holds the index into COMP_WORDS of the word
// under the cursor, which may be empty and may be past the end of a line
// that has trailing whitespace. From these, args is the words strictly
// between the program name and the cursor, and word is the word at the
// cursor, or "" past the end of the line.
//
// The reply is written to w as newline-terminated lines, each one of:
//
// plain,<candidate> one candidate for the shell to offer
// nofiles, present exactly when the directive is
// CompNoFileComp; its absence means CompDefault
//
// This always exits 0: a shell script evaluates the reply, and a nonzero
// exit or anything on stderr would surface as noise in the user's
// terminal rather than as a completion failure anyone can act on.
func writeCompletionReply(cmd *ir.Command, w io.Writer) {
var words []string
if s := os.Getenv("COMP_WORDS"); s != "" {
words = strings.Split(s, "\n")
}
cword, _ := strconv.Atoi(os.Getenv("COMP_CWORD"))
var args []string
if cword > 0 && cword <= len(words) {
args = words[1:cword]
}
var word string
if cword >= 0 && cword < len(words) {
word = words[cword]
}
cands, dir := argv.Complete(cmd, args, word)
for _, cand := range cands {
fmt.Fprintf(w, "plain,%s\n", cand)
}
if dir == ir.CompNoFileComp {
fmt.Fprint(w, "nofiles,\n")
}
}
// bashSourceScript returns the Bash completion script for prog, which
// reads envVar to tell the two ways it may be invoked apart: sourced, to
// print this script, or re-invoked by the script itself to answer one
// completion request.
func bashSourceScript(prog, envVar string) string {
return fmt.Sprintf(bashCompletionScriptTmpl, prog, envVar, shellFuncName(prog))
}
// zshSourceScript is bashSourceScript's counterpart for Zsh.
func zshSourceScript(prog, envVar string) string {
return fmt.Sprintf(zshCompletionScriptTmpl, prog, envVar, shellFuncName(prog))
}
// bashCompletionScriptTmpl is a fmt.Sprintf template taking, in order, the
// program name, the environment variable EnableCompletion derived for it,
// and a function-name-safe form of the program name.
const bashCompletionScriptTmpl = `# Bash completion for %[1]s. To enable it in the current shell, run:
#
# source <(%[2]s=bash_source %[1]s 2>/dev/null)
#
# or add that line to your ~/.bashrc to enable it in every new shell.
_%[3]s_complete() {
local response
response=$(
COMP_WORDS="$(printf '%%s\n' "${COMP_WORDS[@]}")" \
COMP_CWORD="$COMP_CWORD" \
%[2]s=bash_complete "${COMP_WORDS[0]}" 2>/dev/null
)
local nofiles=0
COMPREPLY=()
while IFS= read -r line; do
case "$line" in
plain,*)
COMPREPLY+=("${line#plain,}")
;;
nofiles,*)
nofiles=1
;;
esac
done <<<"$response"
if [ "$nofiles" -eq 0 ]; then
COMPREPLY+=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}"))
fi
}
complete -F _%[3]s_complete %[1]s
`
// zshCompletionScriptTmpl is bashCompletionScriptTmpl's counterpart for
// Zsh, taking the same three arguments in the same order.
const zshCompletionScriptTmpl = `#compdef %[1]s
# Zsh completion for %[1]s. To enable it in the current shell, run:
#
# source <(%[2]s=zsh_source %[1]s 2>/dev/null)
#
# or add that line to your ~/.zshrc to enable it in every new shell.
_%[3]s_complete() {
local -a plain
local response nofiles=0
response=$(
COMP_WORDS="$(printf '%%s\n' "${words[@]}")" \
COMP_CWORD=$((CURRENT - 1)) \
%[2]s=zsh_complete "${words[1]}" 2>/dev/null
)
while IFS= read -r line; do
case "$line" in
plain,*)
plain+=("${line#plain,}")
;;
nofiles,*)
nofiles=1
;;
esac
done <<<"$response"
_describe -V '%[1]s completions' plain
if [ "$nofiles" -eq 0 ]; then
_files
fi
}
compdef _%[3]s_complete %[1]s
`