Skip to content

Commit e0da98f

Browse files
committed
Add substitution support for fish, nushell, powershell, and xonsh
Add ( and ) as wordbreaks to fish, nushell, powershell, and xonsh classifiers. Each format's PostProcess reclassifies parens as substitution delimiters. PowerShell and xonsh detect $ + ( (and @ + ( for xonsh) adjacency to merge into a single opener token. Assisted-by: Crush:glm-5.2
1 parent c8fa4e9 commit e0da98f

4 files changed

Lines changed: 102 additions & 6 deletions

File tree

format_fish.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ func (fishFormat) Classifier() tokenClassifier {
1818
// Fish operators: |, ;, <, >, &, ?
1919
// & is included for &&, ||, &, |&, &|, &>, &>>, &>?, >&, <>&
2020
// ? is part of redirect operators (>? >>? <?) — deprecated glob char
21-
t.addWordbreaks("|;<>&?")
21+
// ( and ) are command substitution delimiters
22+
t.addWordbreaks("|;<>&?()")
2223
return t
2324
}
2425

@@ -84,3 +85,20 @@ var fishDoubleQuoteEscapes = map[rune]bool{
8485
func (fishFormat) QuoteWord(s string) string { return fishQuoteWord(s) }
8586
func (fishFormat) TripleQuoteSupport() bool { return false }
8687
func (fishFormat) RawPrefixSupport() bool { return false }
88+
89+
// PostProcess reclassifies ( and ) as substitution delimiters for fish
90+
// command substitution. Fish uses bare () for command substitution
91+
// (no $ prefix).
92+
func (fishFormat) PostProcess(tokens TokenSlice) TokenSlice {
93+
result := make(TokenSlice, 0, len(tokens))
94+
for _, t := range tokens {
95+
if t.Type == WORDBREAK_TOKEN && t.Value == "(" {
96+
t.WordbreakType = WORDBREAK_SUBSTITUTION_OPEN
97+
}
98+
if t.Type == WORDBREAK_TOKEN && t.Value == ")" {
99+
t.WordbreakType = WORDBREAK_SUBSTITUTION_CLOSE
100+
}
101+
result = append(result, t)
102+
}
103+
return result
104+
}

format_nushell.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ func (nushellFormat) Classifier() tokenClassifier {
3333

3434
// Nushell operators: |, ;, >, <, >>
3535
// No &&, ||, & — no POSIX list operators
36-
t.addWordbreaks("|;<>")
36+
// ( and ) are subexpression delimiters
37+
t.addWordbreaks("|;<>()")
3738
return t
3839
}
3940

@@ -172,5 +173,16 @@ func (nushellFormat) PostProcess(tokens TokenSlice) TokenSlice {
172173

173174
result = append(result, t)
174175
}
176+
177+
// Reclassify ( and ) as substitution delimiters
178+
for i := range result {
179+
t := &result[i]
180+
if t.Type == WORDBREAK_TOKEN && t.Value == "(" {
181+
t.WordbreakType = WORDBREAK_SUBSTITUTION_OPEN
182+
}
183+
if t.Type == WORDBREAK_TOKEN && t.Value == ")" {
184+
t.WordbreakType = WORDBREAK_SUBSTITUTION_CLOSE
185+
}
186+
}
175187
return result
176188
}

format_powershell.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ func (powershellFormat) Classifier() tokenClassifier {
2828

2929
// PowerShell operators: |, ;, >, >>, &&, ||, &
3030
// Note: & is the call operator, not a background operator
31-
t.addWordbreaks("|;&><")
31+
// ( and ) are subexpression/substitution delimiters
32+
t.addWordbreaks("|;&><()")
3233
return t
3334
}
3435

@@ -151,5 +152,37 @@ func (powershellFormat) PostProcess(tokens TokenSlice) TokenSlice {
151152

152153
result = append(result, t)
153154
}
154-
return result
155+
156+
// Second pass: merge $ + ( into substitution opener and reclassify ) as closer
157+
final := make(TokenSlice, 0, len(result))
158+
for i := 0; i < len(result); i++ {
159+
t := result[i]
160+
// Detect $ + ( adjacency → merge into WORDBREAK_SUBSTITUTION_OPEN
161+
if t.Type == WORD_TOKEN && t.Value == "$" && i+1 < len(result) {
162+
next := result[i+1]
163+
if next.Type == WORDBREAK_TOKEN && next.Value == "(" && t.adjoins(next) {
164+
merged := Token{
165+
Type: WORDBREAK_TOKEN,
166+
Value: "$(",
167+
RawValue: t.RawValue + next.RawValue,
168+
Span: Span{Start: t.Span.Start, End: next.Span.End},
169+
State: next.State,
170+
WordbreakType: WORDBREAK_SUBSTITUTION_OPEN,
171+
}
172+
final = append(final, merged)
173+
i++
174+
continue
175+
}
176+
}
177+
// Reclassify standalone ( as substitution opener
178+
if t.Type == WORDBREAK_TOKEN && t.Value == "(" {
179+
t.WordbreakType = WORDBREAK_SUBSTITUTION_OPEN
180+
}
181+
// Reclassify ) as substitution closer
182+
if t.Type == WORDBREAK_TOKEN && t.Value == ")" {
183+
t.WordbreakType = WORDBREAK_SUBSTITUTION_CLOSE
184+
}
185+
final = append(final, t)
186+
}
187+
return final
155188
}

format_xonsh.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ func XonshFormat() Format { return xonshFormat{} }
2222
func (xonshFormat) Classifier() tokenClassifier {
2323
t := newBaseClassifier(escapeRunes)
2424
// Xonsh operators: |, >, >>, <, ;, &&, ||, &
25-
t.addWordbreaks("|<>&;")
25+
// ( and ) are subprocess/eval delimiters
26+
t.addWordbreaks("|<>&;()")
2627
return t
2728
}
2829

@@ -111,5 +112,37 @@ func (xonshFormat) PostProcess(tokens TokenSlice) TokenSlice {
111112

112113
result = append(result, t)
113114
}
114-
return result
115+
116+
// Second pass: merge $/@ + ( into substitution opener and reclassify ) as closer
117+
final := make(TokenSlice, 0, len(result))
118+
for i := 0; i < len(result); i++ {
119+
t := result[i]
120+
// Detect $ + ( or @ + ( adjacency → merge into WORDBREAK_SUBSTITUTION_OPEN
121+
if t.Type == WORD_TOKEN && (t.Value == "$" || t.Value == "@") && i+1 < len(result) {
122+
next := result[i+1]
123+
if next.Type == WORDBREAK_TOKEN && next.Value == "(" && t.adjoins(next) {
124+
merged := Token{
125+
Type: WORDBREAK_TOKEN,
126+
Value: t.Value + "(",
127+
RawValue: t.RawValue + next.RawValue,
128+
Span: Span{Start: t.Span.Start, End: next.Span.End},
129+
State: next.State,
130+
WordbreakType: WORDBREAK_SUBSTITUTION_OPEN,
131+
}
132+
final = append(final, merged)
133+
i++
134+
continue
135+
}
136+
}
137+
// Reclassify standalone ( as substitution opener
138+
if t.Type == WORDBREAK_TOKEN && t.Value == "(" {
139+
t.WordbreakType = WORDBREAK_SUBSTITUTION_OPEN
140+
}
141+
// Reclassify ) as substitution closer
142+
if t.Type == WORDBREAK_TOKEN && t.Value == ")" {
143+
t.WordbreakType = WORDBREAK_SUBSTITUTION_CLOSE
144+
}
145+
final = append(final, t)
146+
}
147+
return final
115148
}

0 commit comments

Comments
 (0)