|
1 | 1 | package shlex |
2 | 2 |
|
| 3 | +import "strings" |
| 4 | + |
3 | 5 | // nushellFormat implements Format for nushell lexing. |
4 | 6 | // Key differences from bash: |
5 | 7 | // - Backtick (`) is a quote character (not an escape like PowerShell) |
6 | 8 | // - $'...' and $"..." are interpolated strings ($ prefix + standard quote) |
7 | | -// - r#'...'# raw strings need multi-rune opener support (deferred) |
| 9 | +// - C-style escapes in double quotes with a richer set than bash: |
| 10 | +// \" \' \\ \/ \b \f \r \n \t \0 \a \e \( \) \{ \} \$ \^ \# \| \~ \xHH \u{X} |
8 | 11 | // - No POSIX list operators (no &&, ||, &) |
9 | | -// - C-style escapes in double quotes (same as bash) |
| 12 | +// - Stream redirect operators: out>, err>, out+err>, o>, e>, o+e> |
| 13 | +// and pipe variants: e>|, err>|, o+e>|, out+err>| |
| 14 | +// - r#'...'# raw strings need multi-rune opener support (deferred) |
10 | 15 | type nushellFormat struct{} |
11 | 16 |
|
12 | 17 | // NushellFormat returns the nushell lexical format. |
@@ -59,3 +64,115 @@ func (nushellFormat) NonEscapingQuoteBackslashEscapes() bool { return false } |
59 | 64 | func (nushellFormat) EscapeNotBareword() bool { return true } |
60 | 65 | func (nushellFormat) EscapingQuoteEscapeChars() map[rune]bool { return nil } |
61 | 66 | func (nushellFormat) QuoteWord(s string) string { return nushellQuoteWord(s) } |
| 67 | + |
| 68 | +// EscapingQuoteUnescape implements the EscapingQuoteUnescaper interface. |
| 69 | +// Nushell double-quoted strings support C-style escapes with a richer set |
| 70 | +// than bash. Recognized escapes produce the corresponding character(s); |
| 71 | +// unrecognized escapes keep both the backslash and the rune literally |
| 72 | +// (nushell itself errors on unrecognized escapes, but for completion being |
| 73 | +// lenient is better than failing). |
| 74 | +func (nushellFormat) EscapingQuoteUnescape(r rune) (string, bool) { |
| 75 | + switch r { |
| 76 | + case '"': |
| 77 | + return "\"", true |
| 78 | + case '\'': |
| 79 | + return "'", true |
| 80 | + case '\\': |
| 81 | + return "\\", true |
| 82 | + case '/': |
| 83 | + return "/", true |
| 84 | + case 'b': |
| 85 | + return "\b", true |
| 86 | + case 'f': |
| 87 | + return "\f", true |
| 88 | + case 'r': |
| 89 | + return "\r", true |
| 90 | + case 'n': |
| 91 | + return "\n", true |
| 92 | + case 't': |
| 93 | + return "\t", true |
| 94 | + case '0': |
| 95 | + return "\x00", true |
| 96 | + case 'a': |
| 97 | + return "\a", true |
| 98 | + case 'e': |
| 99 | + return "\x1b", true |
| 100 | + case '(': |
| 101 | + return "(", true |
| 102 | + case ')': |
| 103 | + return ")", true |
| 104 | + case '{': |
| 105 | + return "{", true |
| 106 | + case '}': |
| 107 | + return "}", true |
| 108 | + case '$': |
| 109 | + return "$", true |
| 110 | + case '^': |
| 111 | + return "^", true |
| 112 | + case '#': |
| 113 | + return "#", true |
| 114 | + case '|': |
| 115 | + return "|", true |
| 116 | + case '~': |
| 117 | + return "~", true |
| 118 | + default: |
| 119 | + return "", false |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// nushellStreamRedirects maps the word portion of stream-redirect operators |
| 124 | +// (the part before >) to their WordbreakType. The PostProcess step merges |
| 125 | +// these with a following > or >| wordbreak token. |
| 126 | +var nushellStreamRedirects = map[string]WordbreakType{ |
| 127 | + "out": WORDBREAK_REDIRECT_OUTPUT, |
| 128 | + "err": WORDBREAK_REDIRECT_OUTPUT, |
| 129 | + "o": WORDBREAK_REDIRECT_OUTPUT, |
| 130 | + "e": WORDBREAK_REDIRECT_OUTPUT, |
| 131 | + "out+err": WORDBREAK_REDIRECT_OUTPUT_BOTH, |
| 132 | + "o+e": WORDBREAK_REDIRECT_OUTPUT_BOTH, |
| 133 | +} |
| 134 | + |
| 135 | +// PostProcess merges nushell stream-redirect operators. The tokenizer |
| 136 | +// produces e.g. `out` as a WORD_TOKEN and `>` (or `>|`, `>>`) as a |
| 137 | +// WORDBREAK_TOKEN because the rune-classifier only handles single-rune |
| 138 | +// word breaks. This step detects adjacent word+wordbreak sequences like |
| 139 | +// `out>`, `err>`, `o+e>`, `e>|`, `o+e>|` and reclassifies them as single |
| 140 | +// WORDBREAK_TOKENs with the appropriate WordbreakType. |
| 141 | +func (nushellFormat) PostProcess(tokens TokenSlice) TokenSlice { |
| 142 | + result := make(TokenSlice, 0, len(tokens)) |
| 143 | + for i := 0; i < len(tokens); i++ { |
| 144 | + t := tokens[i] |
| 145 | + |
| 146 | + // Look for bare WORD_TOKEN immediately followed by WORDBREAK_TOKEN starting with '>' |
| 147 | + // Only merge bare words (Value == RawValue) — quoted words like 'out' or "out" |
| 148 | + // are string literals, not stream redirect operators. |
| 149 | + if t.Type == WORD_TOKEN && t.Value == t.RawValue && i+1 < len(tokens) { |
| 150 | + next := tokens[i+1] |
| 151 | + if next.Type == WORDBREAK_TOKEN && next.adjoins(t) && len(next.RawValue) > 0 && next.RawValue[0] == '>' { |
| 152 | + if wbType, ok := nushellStreamRedirects[t.Value]; ok { |
| 153 | + // Check if the wordbreak token includes a pipe suffix (e.g. >|) |
| 154 | + // which makes it a pipe-with-stderr variant |
| 155 | + if strings.Contains(next.RawValue, "|") { |
| 156 | + wbType = WORDBREAK_PIPE_WITH_STDERR |
| 157 | + } |
| 158 | + |
| 159 | + merged := Token{ |
| 160 | + Type: WORDBREAK_TOKEN, |
| 161 | + Value: t.Value + next.Value, |
| 162 | + RawValue: t.RawValue + next.RawValue, |
| 163 | + Span: Span{Start: t.Span.Start, End: next.Span.End}, |
| 164 | + State: next.State, |
| 165 | + WordbreakType: wbType, |
| 166 | + } |
| 167 | + |
| 168 | + result = append(result, merged) |
| 169 | + i += 1 |
| 170 | + continue |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + result = append(result, t) |
| 176 | + } |
| 177 | + return result |
| 178 | +} |
0 commit comments