1 package cobra |
1 package cobra |
2 |
2 |
3 import ( |
3 import ( |
4 "encoding/json" |
4 "bytes" |
5 "fmt" |
5 "fmt" |
6 "io" |
6 "io" |
7 "os" |
7 "os" |
8 "sort" |
|
9 "strings" |
|
10 "text/template" |
|
11 |
|
12 "github.com/spf13/pflag" |
|
13 ) |
8 ) |
14 |
9 |
15 const ( |
10 // GenZshCompletionFile generates zsh completion file including descriptions. |
16 zshCompArgumentAnnotation = "cobra_annotations_zsh_completion_argument_annotation" |
|
17 zshCompArgumentFilenameComp = "cobra_annotations_zsh_completion_argument_file_completion" |
|
18 zshCompArgumentWordComp = "cobra_annotations_zsh_completion_argument_word_completion" |
|
19 zshCompDirname = "cobra_annotations_zsh_dirname" |
|
20 ) |
|
21 |
|
22 var ( |
|
23 zshCompFuncMap = template.FuncMap{ |
|
24 "genZshFuncName": zshCompGenFuncName, |
|
25 "extractFlags": zshCompExtractFlag, |
|
26 "genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments, |
|
27 "extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering, |
|
28 } |
|
29 zshCompletionText = ` |
|
30 {{/* should accept Command (that contains subcommands) as parameter */}} |
|
31 {{define "argumentsC" -}} |
|
32 {{ $cmdPath := genZshFuncName .}} |
|
33 function {{$cmdPath}} { |
|
34 local -a commands |
|
35 |
|
36 _arguments -C \{{- range extractFlags .}} |
|
37 {{genFlagEntryForZshArguments .}} \{{- end}} |
|
38 "1: :->cmnds" \ |
|
39 "*::arg:->args" |
|
40 |
|
41 case $state in |
|
42 cmnds) |
|
43 commands=({{range .Commands}}{{if not .Hidden}} |
|
44 "{{.Name}}:{{.Short}}"{{end}}{{end}} |
|
45 ) |
|
46 _describe "command" commands |
|
47 ;; |
|
48 esac |
|
49 |
|
50 case "$words[1]" in {{- range .Commands}}{{if not .Hidden}} |
|
51 {{.Name}}) |
|
52 {{$cmdPath}}_{{.Name}} |
|
53 ;;{{end}}{{end}} |
|
54 esac |
|
55 } |
|
56 {{range .Commands}}{{if not .Hidden}} |
|
57 {{template "selectCmdTemplate" .}} |
|
58 {{- end}}{{end}} |
|
59 {{- end}} |
|
60 |
|
61 {{/* should accept Command without subcommands as parameter */}} |
|
62 {{define "arguments" -}} |
|
63 function {{genZshFuncName .}} { |
|
64 {{" _arguments"}}{{range extractFlags .}} \ |
|
65 {{genFlagEntryForZshArguments . -}} |
|
66 {{end}}{{range extractArgsCompletions .}} \ |
|
67 {{.}}{{end}} |
|
68 } |
|
69 {{end}} |
|
70 |
|
71 {{/* dispatcher for commands with or without subcommands */}} |
|
72 {{define "selectCmdTemplate" -}} |
|
73 {{if .Hidden}}{{/* ignore hidden*/}}{{else -}} |
|
74 {{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} |
|
75 {{- end}} |
|
76 {{- end}} |
|
77 |
|
78 {{/* template entry point */}} |
|
79 {{define "Main" -}} |
|
80 #compdef _{{.Name}} {{.Name}} |
|
81 |
|
82 {{template "selectCmdTemplate" .}} |
|
83 {{end}} |
|
84 ` |
|
85 ) |
|
86 |
|
87 // zshCompArgsAnnotation is used to encode/decode zsh completion for |
|
88 // arguments to/from Command.Annotations. |
|
89 type zshCompArgsAnnotation map[int]zshCompArgHint |
|
90 |
|
91 type zshCompArgHint struct { |
|
92 // Indicates the type of the completion to use. One of: |
|
93 // zshCompArgumentFilenameComp or zshCompArgumentWordComp |
|
94 Tipe string `json:"type"` |
|
95 |
|
96 // A value for the type above (globs for file completion or words) |
|
97 Options []string `json:"options"` |
|
98 } |
|
99 |
|
100 // GenZshCompletionFile generates zsh completion file. |
|
101 func (c *Command) GenZshCompletionFile(filename string) error { |
11 func (c *Command) GenZshCompletionFile(filename string) error { |
|
12 return c.genZshCompletionFile(filename, true) |
|
13 } |
|
14 |
|
15 // GenZshCompletion generates zsh completion file including descriptions |
|
16 // and writes it to the passed writer. |
|
17 func (c *Command) GenZshCompletion(w io.Writer) error { |
|
18 return c.genZshCompletion(w, true) |
|
19 } |
|
20 |
|
21 // GenZshCompletionFileNoDesc generates zsh completion file without descriptions. |
|
22 func (c *Command) GenZshCompletionFileNoDesc(filename string) error { |
|
23 return c.genZshCompletionFile(filename, false) |
|
24 } |
|
25 |
|
26 // GenZshCompletionNoDesc generates zsh completion file without descriptions |
|
27 // and writes it to the passed writer. |
|
28 func (c *Command) GenZshCompletionNoDesc(w io.Writer) error { |
|
29 return c.genZshCompletion(w, false) |
|
30 } |
|
31 |
|
32 // MarkZshCompPositionalArgumentFile only worked for zsh and its behavior was |
|
33 // not consistent with Bash completion. It has therefore been disabled. |
|
34 // Instead, when no other completion is specified, file completion is done by |
|
35 // default for every argument. One can disable file completion on a per-argument |
|
36 // basis by using ValidArgsFunction and ShellCompDirectiveNoFileComp. |
|
37 // To achieve file extension filtering, one can use ValidArgsFunction and |
|
38 // ShellCompDirectiveFilterFileExt. |
|
39 // |
|
40 // Deprecated |
|
41 func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { |
|
42 return nil |
|
43 } |
|
44 |
|
45 // MarkZshCompPositionalArgumentWords only worked for zsh. It has therefore |
|
46 // been disabled. |
|
47 // To achieve the same behavior across all shells, one can use |
|
48 // ValidArgs (for the first argument only) or ValidArgsFunction for |
|
49 // any argument (can include the first one also). |
|
50 // |
|
51 // Deprecated |
|
52 func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { |
|
53 return nil |
|
54 } |
|
55 |
|
56 func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error { |
102 outFile, err := os.Create(filename) |
57 outFile, err := os.Create(filename) |
103 if err != nil { |
58 if err != nil { |
104 return err |
59 return err |
105 } |
60 } |
106 defer outFile.Close() |
61 defer outFile.Close() |
107 |
62 |
108 return c.GenZshCompletion(outFile) |
63 return c.genZshCompletion(outFile, includeDesc) |
109 } |
64 } |
110 |
65 |
111 // GenZshCompletion generates a zsh completion file and writes to the passed |
66 func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { |
112 // writer. The completion always run on the root command regardless of the |
67 buf := new(bytes.Buffer) |
113 // command it was called from. |
68 genZshComp(buf, c.Name(), includeDesc) |
114 func (c *Command) GenZshCompletion(w io.Writer) error { |
69 _, err := buf.WriteTo(w) |
115 tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText) |
70 return err |
116 if err != nil { |
71 } |
117 return fmt.Errorf("error creating zsh completion template: %v", err) |
72 |
|
73 func genZshComp(buf io.StringWriter, name string, includeDesc bool) { |
|
74 compCmd := ShellCompRequestCmd |
|
75 if !includeDesc { |
|
76 compCmd = ShellCompNoDescRequestCmd |
118 } |
77 } |
119 return tmpl.Execute(w, c.Root()) |
78 WriteStringAndCheck(buf, fmt.Sprintf(`#compdef _%[1]s %[1]s |
120 } |
79 |
121 |
80 # zsh completion for %-36[1]s -*- shell-script -*- |
122 // MarkZshCompPositionalArgumentFile marks the specified argument (first |
81 |
123 // argument is 1) as completed by file selection. patterns (e.g. "*.txt") are |
82 __%[1]s_debug() |
124 // optional - if not provided the completion will search for all files. |
83 { |
125 func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { |
84 local file="$BASH_COMP_DEBUG_FILE" |
126 if argPosition < 1 { |
85 if [[ -n ${file} ]]; then |
127 return fmt.Errorf("Invalid argument position (%d)", argPosition) |
86 echo "$*" >> "${file}" |
128 } |
87 fi |
129 annotation, err := c.zshCompGetArgsAnnotations() |
88 } |
130 if err != nil { |
89 |
131 return err |
90 _%[1]s() |
132 } |
91 { |
133 if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) { |
92 local shellCompDirectiveError=%[3]d |
134 return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition) |
93 local shellCompDirectiveNoSpace=%[4]d |
135 } |
94 local shellCompDirectiveNoFileComp=%[5]d |
136 annotation[argPosition] = zshCompArgHint{ |
95 local shellCompDirectiveFilterFileExt=%[6]d |
137 Tipe: zshCompArgumentFilenameComp, |
96 local shellCompDirectiveFilterDirs=%[7]d |
138 Options: patterns, |
97 |
139 } |
98 local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace |
140 return c.zshCompSetArgsAnnotations(annotation) |
99 local -a completions |
141 } |
100 |
142 |
101 __%[1]s_debug "\n========= starting completion logic ==========" |
143 // MarkZshCompPositionalArgumentWords marks the specified positional argument |
102 __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" |
144 // (first argument is 1) as completed by the provided words. At east one word |
103 |
145 // must be provided, spaces within words will be offered completion with |
104 # The user could have moved the cursor backwards on the command-line. |
146 // "word\ word". |
105 # We need to trigger completion from the $CURRENT location, so we need |
147 func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { |
106 # to truncate the command-line ($words) up to the $CURRENT location. |
148 if argPosition < 1 { |
107 # (We cannot use $CURSOR as its value does not work when a command is an alias.) |
149 return fmt.Errorf("Invalid argument position (%d)", argPosition) |
108 words=("${=words[1,CURRENT]}") |
150 } |
109 __%[1]s_debug "Truncated words[*]: ${words[*]}," |
151 if len(words) == 0 { |
110 |
152 return fmt.Errorf("Trying to set empty word list for positional argument %d", argPosition) |
111 lastParam=${words[-1]} |
153 } |
112 lastChar=${lastParam[-1]} |
154 annotation, err := c.zshCompGetArgsAnnotations() |
113 __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" |
155 if err != nil { |
114 |
156 return err |
115 # For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>) |
157 } |
116 # completions must be prefixed with the flag |
158 if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) { |
117 setopt local_options BASH_REMATCH |
159 return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition) |
118 if [[ "${lastParam}" =~ '-.*=' ]]; then |
160 } |
119 # We are dealing with a flag with an = |
161 annotation[argPosition] = zshCompArgHint{ |
120 flagPrefix="-P ${BASH_REMATCH}" |
162 Tipe: zshCompArgumentWordComp, |
121 fi |
163 Options: words, |
122 |
164 } |
123 # Prepare the command to obtain completions |
165 return c.zshCompSetArgsAnnotations(annotation) |
124 requestComp="${words[1]} %[2]s ${words[2,-1]}" |
166 } |
125 if [ "${lastChar}" = "" ]; then |
167 |
126 # If the last parameter is complete (there is a space following it) |
168 func zshCompExtractArgumentCompletionHintsForRendering(c *Command) ([]string, error) { |
127 # We add an extra empty parameter so we can indicate this to the go completion code. |
169 var result []string |
128 __%[1]s_debug "Adding extra empty parameter" |
170 annotation, err := c.zshCompGetArgsAnnotations() |
129 requestComp="${requestComp} \"\"" |
171 if err != nil { |
130 fi |
172 return nil, err |
131 |
173 } |
132 __%[1]s_debug "About to call: eval ${requestComp}" |
174 for k, v := range annotation { |
133 |
175 s, err := zshCompRenderZshCompArgHint(k, v) |
134 # Use eval to handle any environment variables and such |
176 if err != nil { |
135 out=$(eval ${requestComp} 2>/dev/null) |
177 return nil, err |
136 __%[1]s_debug "completion output: ${out}" |
178 } |
137 |
179 result = append(result, s) |
138 # Extract the directive integer following a : from the last line |
180 } |
139 local lastLine |
181 if len(c.ValidArgs) > 0 { |
140 while IFS='\n' read -r line; do |
182 if _, positionOneExists := annotation[1]; !positionOneExists { |
141 lastLine=${line} |
183 s, err := zshCompRenderZshCompArgHint(1, zshCompArgHint{ |
142 done < <(printf "%%s\n" "${out[@]}") |
184 Tipe: zshCompArgumentWordComp, |
143 __%[1]s_debug "last line: ${lastLine}" |
185 Options: c.ValidArgs, |
144 |
186 }) |
145 if [ "${lastLine[1]}" = : ]; then |
187 if err != nil { |
146 directive=${lastLine[2,-1]} |
188 return nil, err |
147 # Remove the directive including the : and the newline |
189 } |
148 local suffix |
190 result = append(result, s) |
149 (( suffix=${#lastLine}+2)) |
191 } |
150 out=${out[1,-$suffix]} |
192 } |
151 else |
193 sort.Strings(result) |
152 # There is no directive specified. Leave $out as is. |
194 return result, nil |
153 __%[1]s_debug "No directive found. Setting do default" |
195 } |
154 directive=0 |
196 |
155 fi |
197 func zshCompRenderZshCompArgHint(i int, z zshCompArgHint) (string, error) { |
156 |
198 switch t := z.Tipe; t { |
157 __%[1]s_debug "directive: ${directive}" |
199 case zshCompArgumentFilenameComp: |
158 __%[1]s_debug "completions: ${out}" |
200 var globs []string |
159 __%[1]s_debug "flagPrefix: ${flagPrefix}" |
201 for _, g := range z.Options { |
160 |
202 globs = append(globs, fmt.Sprintf(`-g "%s"`, g)) |
161 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then |
203 } |
162 __%[1]s_debug "Completion received error. Ignoring completions." |
204 return fmt.Sprintf(`'%d: :_files %s'`, i, strings.Join(globs, " ")), nil |
163 return |
205 case zshCompArgumentWordComp: |
164 fi |
206 var words []string |
165 |
207 for _, w := range z.Options { |
166 while IFS='\n' read -r comp; do |
208 words = append(words, fmt.Sprintf("%q", w)) |
167 if [ -n "$comp" ]; then |
209 } |
168 # If requested, completions are returned with a description. |
210 return fmt.Sprintf(`'%d: :(%s)'`, i, strings.Join(words, " ")), nil |
169 # The description is preceded by a TAB character. |
211 default: |
170 # For zsh's _describe, we need to use a : instead of a TAB. |
212 return "", fmt.Errorf("Invalid zsh argument completion annotation: %s", t) |
171 # We first need to escape any : as part of the completion itself. |
213 } |
172 comp=${comp//:/\\:} |
214 } |
173 |
215 |
174 local tab=$(printf '\t') |
216 func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool { |
175 comp=${comp//$tab/:} |
217 _, dup := annotation[position] |
176 |
218 return dup |
177 __%[1]s_debug "Adding completion: ${comp}" |
219 } |
178 completions+=${comp} |
220 |
179 lastComp=$comp |
221 func (c *Command) zshCompGetArgsAnnotations() (zshCompArgsAnnotation, error) { |
180 fi |
222 annotation := make(zshCompArgsAnnotation) |
181 done < <(printf "%%s\n" "${out[@]}") |
223 annotationString, ok := c.Annotations[zshCompArgumentAnnotation] |
182 |
224 if !ok { |
183 if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then |
225 return annotation, nil |
184 __%[1]s_debug "Activating nospace." |
226 } |
185 noSpace="-S ''" |
227 err := json.Unmarshal([]byte(annotationString), &annotation) |
186 fi |
228 if err != nil { |
187 |
229 return annotation, fmt.Errorf("Error unmarshaling zsh argument annotation: %v", err) |
188 if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then |
230 } |
189 # File extension filtering |
231 return annotation, nil |
190 local filteringCmd |
232 } |
191 filteringCmd='_files' |
233 |
192 for filter in ${completions[@]}; do |
234 func (c *Command) zshCompSetArgsAnnotations(annotation zshCompArgsAnnotation) error { |
193 if [ ${filter[1]} != '*' ]; then |
235 jsn, err := json.Marshal(annotation) |
194 # zsh requires a glob pattern to do file filtering |
236 if err != nil { |
195 filter="\*.$filter" |
237 return fmt.Errorf("Error marshaling zsh argument annotation: %v", err) |
196 fi |
238 } |
197 filteringCmd+=" -g $filter" |
239 if c.Annotations == nil { |
198 done |
240 c.Annotations = make(map[string]string) |
199 filteringCmd+=" ${flagPrefix}" |
241 } |
200 |
242 c.Annotations[zshCompArgumentAnnotation] = string(jsn) |
201 __%[1]s_debug "File filtering command: $filteringCmd" |
243 return nil |
202 _arguments '*:filename:'"$filteringCmd" |
244 } |
203 elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then |
245 |
204 # File completion for directories only |
246 func zshCompGenFuncName(c *Command) string { |
205 local subDir |
247 if c.HasParent() { |
206 subdir="${completions[1]}" |
248 return zshCompGenFuncName(c.Parent()) + "_" + c.Name() |
207 if [ -n "$subdir" ]; then |
249 } |
208 __%[1]s_debug "Listing directories in $subdir" |
250 return "_" + c.Name() |
209 pushd "${subdir}" >/dev/null 2>&1 |
251 } |
210 else |
252 |
211 __%[1]s_debug "Listing directories in ." |
253 func zshCompExtractFlag(c *Command) []*pflag.Flag { |
212 fi |
254 var flags []*pflag.Flag |
213 |
255 c.LocalFlags().VisitAll(func(f *pflag.Flag) { |
214 local result |
256 if !f.Hidden { |
215 _arguments '*:dirname:_files -/'" ${flagPrefix}" |
257 flags = append(flags, f) |
216 result=$? |
258 } |
217 if [ -n "$subdir" ]; then |
259 }) |
218 popd >/dev/null 2>&1 |
260 c.InheritedFlags().VisitAll(func(f *pflag.Flag) { |
219 fi |
261 if !f.Hidden { |
220 return $result |
262 flags = append(flags, f) |
221 else |
263 } |
222 __%[1]s_debug "Calling _describe" |
264 }) |
223 if eval _describe "completions" completions $flagPrefix $noSpace; then |
265 return flags |
224 __%[1]s_debug "_describe found some completions" |
266 } |
225 |
267 |
226 # Return the success of having called _describe |
268 // zshCompGenFlagEntryForArguments returns an entry that matches _arguments |
227 return 0 |
269 // zsh-completion parameters. It's too complicated to generate in a template. |
228 else |
270 func zshCompGenFlagEntryForArguments(f *pflag.Flag) string { |
229 __%[1]s_debug "_describe did not find completions." |
271 if f.Name == "" || f.Shorthand == "" { |
230 __%[1]s_debug "Checking if we should do file completion." |
272 return zshCompGenFlagEntryForSingleOptionFlag(f) |
231 if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then |
273 } |
232 __%[1]s_debug "deactivating file completion" |
274 return zshCompGenFlagEntryForMultiOptionFlag(f) |
233 |
275 } |
234 # We must return an error code here to let zsh know that there were no |
276 |
235 # completions found by _describe; this is what will trigger other |
277 func zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string { |
236 # matching algorithms to attempt to find completions. |
278 var option, multiMark, extras string |
237 # For example zsh can match letters in the middle of words. |
279 |
238 return 1 |
280 if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
239 else |
281 multiMark = "*" |
240 # Perform file completion |
282 } |
241 __%[1]s_debug "Activating file completion" |
283 |
242 |
284 option = "--" + f.Name |
243 # We must return the result of this command, so it must be the |
285 if option == "--" { |
244 # last command, or else we must store its result to return it. |
286 option = "-" + f.Shorthand |
245 _arguments '*:filename:_files'" ${flagPrefix}" |
287 } |
246 fi |
288 extras = zshCompGenFlagEntryExtras(f) |
247 fi |
289 |
248 fi |
290 return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras) |
249 } |
291 } |
250 |
292 |
251 # don't run the completion function when being source-ed or eval-ed |
293 func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string { |
252 if [ "$funcstack[1]" = "_%[1]s" ]; then |
294 var options, parenMultiMark, curlyMultiMark, extras string |
253 _%[1]s |
295 |
254 fi |
296 if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
255 `, name, compCmd, |
297 parenMultiMark = "*" |
256 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
298 curlyMultiMark = "\\*" |
257 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) |
299 } |
258 } |
300 |
|
301 options = fmt.Sprintf(`'(%s-%s %s--%s)'{%s-%s,%s--%s}`, |
|
302 parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name) |
|
303 extras = zshCompGenFlagEntryExtras(f) |
|
304 |
|
305 return fmt.Sprintf(`%s'[%s]%s'`, options, zshCompQuoteFlagDescription(f.Usage), extras) |
|
306 } |
|
307 |
|
308 func zshCompGenFlagEntryExtras(f *pflag.Flag) string { |
|
309 if f.NoOptDefVal != "" { |
|
310 return "" |
|
311 } |
|
312 |
|
313 extras := ":" // allow options for flag (even without assistance) |
|
314 for key, values := range f.Annotations { |
|
315 switch key { |
|
316 case zshCompDirname: |
|
317 extras = fmt.Sprintf(":filename:_files -g %q", values[0]) |
|
318 case BashCompFilenameExt: |
|
319 extras = ":filename:_files" |
|
320 for _, pattern := range values { |
|
321 extras = extras + fmt.Sprintf(` -g "%s"`, pattern) |
|
322 } |
|
323 } |
|
324 } |
|
325 |
|
326 return extras |
|
327 } |
|
328 |
|
329 func zshCompFlagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { |
|
330 return strings.Contains(f.Value.Type(), "Slice") || |
|
331 strings.Contains(f.Value.Type(), "Array") |
|
332 } |
|
333 |
|
334 func zshCompQuoteFlagDescription(s string) string { |
|
335 return strings.Replace(s, "'", `'\''`, -1) |
|
336 } |
|