You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Document deferred nushell raw strings and multi-rune escapes
Add detailed implementation plans for two deferred features that require
multi-rune lookahead in the tokenizer state machine: raw strings
(r#'...'#) and hex/unicode escapes (\xHH, \u{X...}). Each plan includes
current vs expected behavior, how nushell's own lexer handles it,
affected files, key challenges, recommended approach, and test cases.
Assisted-by: Crush:glm-5.2
Copy file name to clipboardExpand all lines: skills/shlex/references/format-nushell.md
+179-3Lines changed: 179 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -188,13 +188,189 @@ Nushell passes args **with quoting intact** (unlike bash which strips quotes). T
188
188
189
189
## Edge Cases
190
190
191
-
-**`r#'...'#` raw strings**: the `#`-not-a-comment issue. Needs multi-rune opener recognition. Deferred — the `r#` prefix is currently treated as word characters, and `#` inside `r#'...'` is a word character (not a comment) by coincidence because `#` only starts a comment from `START_STATE`.
192
191
-**Backtick as quote** (not escape): opposite of PowerShell. Don't confuse the two formats.
193
192
-**`$` prefix on quotes**: `$'...'` and `$"..."` — the `$` adjoins the quoted segment; `Words()` merges them.
194
193
-**No `COMP_WORDBREAKS`**: nushell has no equivalent env var.
195
194
-**C-style escapes produce real characters**: `\n` in `"..."` produces a newline character (0x0A), not the literal text `n`. Implemented via `EscapingQuoteUnescaper`.
196
-
-**Stream redirect operators**: `out>`, `err>`, `o+e>`, `e>|`, etc. are multi-rune operators. Implemented via `PostProcess`.
197
-
-**`\xHH` and `\u{X}` escapes**: deferred — require multi-rune lookahead in the escape state.
195
+
-**Stream redirect operators**: `out>`, `err>`, `o+e>`, `e>|`, etc. are multi-rune operators. Implemented via `PostProcess`. Only bare words are merged — quoted strings like `'out'` are not treated as stream operators.
196
+
-**Quoted words before `>`**: `'out'>bar` is a string literal `out` followed by a plain redirect `>`, not a stream redirect operator. The `PostProcess` check `t.Value == t.RawValue` prevents merging quoted words.
197
+
198
+
## Deferred Features
199
+
200
+
Two features require multi-rune lookahead in the tokenizer's core state machine and are documented here for future implementation.
201
+
202
+
### 1. Raw Strings `r#'...'#`
203
+
204
+
#### What they are
205
+
206
+
Nushell raw strings are delimited by `r#`...`'#` (or `r##`...`'##`, `r###`...`'###`, etc.). They behave like single-quoted strings (no escapes) but can contain single quotes. The number of `#` symbols in the opener and closer must match.
207
+
208
+
```nu
209
+
r#'Raw strings can contain 'quoted' text.'# # → Raw strings can contain 'quoted' text.
210
+
r##'I can use '#' in a raw string'## # → I can use '#' in a raw string
211
+
r#''# # → (empty string)
212
+
```
213
+
214
+
#### Current (wrong) behavior
215
+
216
+
The tokenizer treats `r` as a regular word character and `#` as either a word character (inside `IN_WORD_STATE`) or a comment (from `START_STATE`). So `r#'hello'#` is tokenized as:
217
+
218
+
-`r` → word char (IN_WORD_STATE)
219
+
-`#` → word char (not START_STATE, so not a comment — this is the coincidence)
220
+
-`'` → enters QUOTING_STATE
221
+
-`hello` → literal content
222
+
-`'` → exits QUOTING_STATE → IN_WORD_STATE
223
+
-`#` → word char
224
+
225
+
Result: one WORD_TOKEN with `Value="r#hello#"` and `RawValue="r#'hello'#"`. The `r#` prefix and `'#` suffix are included in the value.
`lex_raw_string` counts consecutive `#` chars after `r` to determine `prefix_sharp_cnt`, expects a `'` after them, then scans forward looking for a closing `'` followed by the same number of `#` chars. The entire `r#'...'#` sequence is consumed as a single `TokenContents::Item`.
241
+
242
+
#### Implementation plan
243
+
244
+
The tokenizer's `scanStream` state machine processes one rune at a time via `ReadRune`/`UnreadRune`. To support raw strings, we need multi-rune lookahead in the `START_STATE` and `IN_WORD_STATE` handlers:
245
+
246
+
1.**Detect the `r#` pattern**: When the current rune is `r` and the state is `START_STATE` or `IN_WORD_STATE`, peek ahead to check if the next rune is `#`. If so, count consecutive `#` runes to determine `prefix_sharp_cnt`.
247
+
248
+
2.**Validate the opener**: After the `#` sequence, expect a `'`. If not found, treat `r` as a regular word char and `#` as a comment/word char as appropriate.
249
+
250
+
3.**Scan to the closer**: Read forward until finding a `'` followed by exactly `prefix_sharp_cnt``#` runes. Everything between the opener `'` and the closer `'#` is the raw string content.
251
+
252
+
4.**Emit the token**: Set `Token.Value` to the raw content (between opener and closer), `Token.RawValue` to the full `r#'...'#` source text, and `Token.State` to `IN_WORD_STATE` (the state after closing).
253
+
254
+
5.**Handle EOF in raw string**: If the closer is not found, emit the token with whatever content was consumed and set state to a new `RAW_STRING_STATE` (or reuse `QUOTING_STATE` for completion purposes — an unclosed raw string behaves like an unclosed quote).
255
+
256
+
**Affected files**:
257
+
-`shlex.go` — `scanStream()``START_STATE` and `IN_WORD_STATE` handlers: add `r#` detection before the default/wordbreak cases
258
+
-`format.go` — possibly a new `Format` interface method like `RawStringOpeners() bool` or add raw-string support to the classifier
259
+
-`format_nushell.go` — enable raw string support
260
+
261
+
**Key challenge**: The state machine is shared across all formats. Raw-string detection must be opt-in per format (only nushell has raw strings). This could be:
262
+
- A new `Format` interface method (e.g. `SupportsRawStrings() bool`)
263
+
- A new rune class (e.g. `rawStringOpenerRuneClass`) triggered by a multi-rune classifier
264
+
- A `PostProcessor` approach — but this won't work because by the time PostProcess runs, the tokens are already split incorrectly (the `#` may have been classified as a comment)
265
+
266
+
The cleanest approach is likely a multi-rune lookahead in `scanStream`, gated by a format check, similar to how `NonEscapingQuoteEscapes()` gates the single-quote peek logic.
267
+
268
+
**The `#`-not-a-comment issue**: Currently `#` only starts a comment from `START_STATE` (word boundary). Inside `IN_WORD_STATE`, `#` falls through to the `default` case and is added as a word char. This means `r#'hello'#` does not accidentally trigger a comment. However, `r#` at a word boundary (e.g. after a space) would have `#` classified as `commentRuneClass` from `START_STATE`, which would start a comment instead of a raw string. The fix must check for `r#`**before** the `commentRuneClass` check in `START_STATE`.
269
+
270
+
**Test cases to add**:
271
+
-`r#'hello'#` → value `hello`
272
+
-`r#''#` → value `` (empty)
273
+
-`r##'contains '#'# here'##` → value `contains '#'# here`
274
+
-`r#'unclosed` → state indicates open raw string (for completion)
275
+
-`echo r#'hello'#` → words `[echo hello]`
276
+
-`r#'it's a test'#` → value `it's a test` (single quotes allowed inside)
277
+
278
+
### 2. Hex and Unicode Escapes `\xHH` and `\u{X...}`
279
+
280
+
#### What they are
281
+
282
+
Inside double-quoted strings (`"..."` and `$"..."`), nushell supports:
283
+
284
+
| Escape | Format | Result |
285
+
|--------|--------|--------|
286
+
|`\xHH`| exactly 2 hex digits | single byte (0x00–0xFF) |
287
+
|`\u{X...}`| 1-6 hex digits in braces, max 0x10FFFF | Unicode codepoint (UTF-8 encoded) |
288
+
289
+
```nu
290
+
"\x41\x42\x43" # → ABC
291
+
"\u{1F600}" # → 😀
292
+
"\u{0041}" # → A
293
+
$"hello\n($name)" # → hello\nworld (interpolated)
294
+
```
295
+
296
+
#### Current (wrong) behavior
297
+
298
+
The `EscapingQuoteUnescaper` interface handles single-rune escapes (`\n`, `\t`, etc.) but cannot handle multi-rune escapes like `\x41` or `\u{1F600}` because the `ESCAPING_QUOTED_STATE` handler only receives one rune after the backslash. When it sees `x` or `u`, the unescaper returns `handled=false`, so both `\` and `x` (or `u`) are kept literally.
299
+
300
+
Result: `"\x41"` produces value `\x41` instead of `A`.
301
+
302
+
#### How nushell's parser handles it
303
+
304
+
In `unescape_string` (`crates/nu-parser/src/parse_literals.rs`):
`parse_hex_escape` reads exactly 2 hex digits after `\x`. `parse_unicode_escape` reads `{`, then 1-6 hex digits, then `}`, validating the codepoint ≤ 0x10FFFF.
316
+
317
+
#### Implementation plan
318
+
319
+
The `EscapingQuoteUnescaper` interface is called from `ESCAPING_QUOTED_STATE` with a single rune. To support multi-rune escapes, we need the unescaper to be able to consume additional runes from the tokenizer. Two approaches:
320
+
321
+
**Option A: Multi-rune unescaper (preferred)**
322
+
323
+
Extend `EscapingQuoteUnescaper` with a method that takes a `runeReader` or similar interface, allowing it to consume additional runes:
1. Call `EscapingQuoteUnescape(nextRune)` first — handles single-rune escapes.
339
+
2. If not handled, call `EscapingQuoteUnescapeMulti(nextRune, t)` — handles `\xHH` by reading 2 more runes, `\u{...}` by reading until `}`.
340
+
3. If still not handled, keep both `\` and the rune literally (current behavior).
341
+
342
+
The `tokenizer` already has `ReadRune`/`UnreadRune` methods. The multi-rune unescaper would read additional runes directly from the tokenizer, updating `t.index` and `token.RawValue`.
343
+
344
+
**Option B: Peek-based approach**
345
+
346
+
Give the unescaper access to a `PeekRune(n int) (rune, bool)` method that peeks ahead without consuming. The unescaper returns how many additional runes to consume. This is cleaner but requires adding a peek buffer to the tokenizer (currently it only has read/unread).
347
+
348
+
**Option C: State-machine extension**
349
+
350
+
Add new states like `ESCAPING_HEX_STATE` and `ESCAPING_UNICODE_STATE` to the state machine, with format-gated transitions. This is more invasive but keeps the single-rune-at-a-time model.
-`format_nushell.go` — implement `\xHH` and `\u{X...}` in the unescaper
356
+
357
+
**Key challenges**:
358
+
- The tokenizer's `RawValue` must include all consumed runes (the full `\x41` or `\u{1F600}`), while `Value` gets only the replacement character.
359
+
- Invalid escapes (`\x4`, `\x4z`, `\u{110000}`, `\u{6e`) are parse errors in nushell. shlex should be lenient: if the hex digits are missing or invalid, keep the backslash and the escape letter literally.
360
+
-`EscapingQuoteEscapeChars` (used by fish) and `EscapingQuoteUnescaper` are mutually exclusive — the unescaper takes priority. The multi-rune extension only applies to formats implementing `EscapingQuoteUnescaper`.
361
+
362
+
**Test cases to add**:
363
+
-`"\x41\x42\x43"` → value `ABC`
364
+
-`"\x00"` → value `\x00` (NUL)
365
+
-`"\xFF"` → value `\xFF` (byte 255)
366
+
-`"\u{1F600}"` → value `😀`
367
+
-`"\u{0041}"` → value `A`
368
+
-`"\u{0}"` → value `\x00` (NUL via unicode)
369
+
-`"\x4"` → value `\x4` (incomplete — lenient, keep literal)
370
+
-`"\x4z"` → value `\x4z` (invalid hex — lenient, keep literal)
371
+
-`"\u{110000}"` → value `\u{110000}` (out of range — lenient, keep literal)
0 commit comments