shellSplit tokenizes a command line in a POSIX-ish way: whitespace separates tokens, double-quotes group, single-quotes group (no expansion inside), backslash escapes the next rune outside single quotes. Stops at the first unquoted pipeline boundary (`|`, `;`, `&`, `>`, `<`) so the validator only ch
(s string)
| 146 | // to be runnable invocations of one CLI command (possibly piped to |
| 147 | // another tool — that other tool is the user's shell, not pad's). |
| 148 | func shellSplit(s string) ([]string, error) { |
| 149 | var tokens []string |
| 150 | var cur strings.Builder |
| 151 | var inDQuote, inSQuote, escape bool |
| 152 | flush := func() { |
| 153 | if cur.Len() > 0 { |
| 154 | tokens = append(tokens, cur.String()) |
| 155 | cur.Reset() |
| 156 | } |
| 157 | } |
| 158 | for _, r := range s { |
| 159 | switch { |
| 160 | case escape: |
| 161 | cur.WriteRune(r) |
| 162 | escape = false |
| 163 | case r == '\\' && !inSQuote: |
| 164 | escape = true |
| 165 | case r == '"' && !inSQuote: |
| 166 | inDQuote = !inDQuote |
| 167 | case r == '\'' && !inDQuote: |
| 168 | inSQuote = !inSQuote |
| 169 | case (r == ' ' || r == '\t') && !inDQuote && !inSQuote: |
| 170 | flush() |
| 171 | case (r == '|' || r == ';' || r == '&' || r == '>' || r == '<') && !inDQuote && !inSQuote: |
| 172 | // Pipeline boundary — return what we have so far. The rest |
| 173 | // of the string belongs to a different command (or the |
| 174 | // shell), which is out of scope for cmdhelp validation. |
| 175 | flush() |
| 176 | return tokens, nil |
| 177 | default: |
| 178 | cur.WriteRune(r) |
| 179 | } |
| 180 | } |
| 181 | if inDQuote || inSQuote { |
| 182 | return nil, fmt.Errorf("unterminated quote") |
| 183 | } |
| 184 | if escape { |
| 185 | return nil, fmt.Errorf("trailing backslash") |
| 186 | } |
| 187 | flush() |
| 188 | return tokens, nil |
| 189 | } |
| 190 | |
| 191 | // ValidateBoolArity asserts that every bool-typed flag in doc obeys |
| 192 | // spec §5.3: bool flags MUST be presence switches. They MUST NOT |