tokenizeSchedulerInput splits a /schedule, /wait, or /jobs argument string into tokens while respecting single- and double-quoted spans so that flag values like --do "/run X Y" or --tag 'k=v with space' arrive as a single token. Backslash escapes the next character inside a quoted span; outside quot
(input string)
| 820 | // Returns an error only when a quote is left unterminated, so callers |
| 821 | // can surface a precise message to the user. |
| 822 | func tokenizeSchedulerInput(input string) ([]string, error) { |
| 823 | var ( |
| 824 | out []string |
| 825 | buf strings.Builder |
| 826 | inQuote rune // 0 = none, '"' or '\'' |
| 827 | escaped bool |
| 828 | hasTok bool |
| 829 | ) |
| 830 | flush := func() { |
| 831 | if hasTok { |
| 832 | out = append(out, buf.String()) |
| 833 | buf.Reset() |
| 834 | hasTok = false |
| 835 | } |
| 836 | } |
| 837 | for _, r := range input { |
| 838 | if escaped { |
| 839 | buf.WriteRune(r) |
| 840 | hasTok = true |
| 841 | escaped = false |
| 842 | continue |
| 843 | } |
| 844 | if inQuote != 0 { |
| 845 | if r == '\\' && inQuote == '"' { |
| 846 | // Backslash only escapes within double quotes; inside |
| 847 | // single quotes it is literal (POSIX-ish). |
| 848 | escaped = true |
| 849 | continue |
| 850 | } |
| 851 | if r == inQuote { |
| 852 | inQuote = 0 |
| 853 | // An empty "" or '' still counts as a token. |
| 854 | hasTok = true |
| 855 | continue |
| 856 | } |
| 857 | buf.WriteRune(r) |
| 858 | hasTok = true |
| 859 | continue |
| 860 | } |
| 861 | switch r { |
| 862 | case '"', '\'': |
| 863 | inQuote = r |
| 864 | hasTok = true |
| 865 | case '\\': |
| 866 | escaped = true |
| 867 | case ' ', '\t', '\n', '\r': |
| 868 | flush() |
| 869 | default: |
| 870 | buf.WriteRune(r) |
| 871 | hasTok = true |
| 872 | } |
| 873 | } |
| 874 | if inQuote != 0 { |
| 875 | return nil, fmt.Errorf("unterminated %c-quoted argument", inQuote) |
| 876 | } |
| 877 | if escaped { |
| 878 | return nil, fmt.Errorf("trailing backslash with nothing to escape") |
| 879 | } |