@@ -108,15 +108,22 @@ func TestCmdFormat_DoubleAnd(t *testing.T) {
108108}
109109
110110func TestCmdFormat_CaretInQuotes (t * testing.T ) {
111- // Cmd: ^ escapes inside double quotes (^" → literal ")
111+ // Cmd: ^ is LITERAL inside double quotes — it does not escape.
112+ // "say ^" → ^ is literal, " closes the quote.
113+ // Outside quotes, ^" → literal " (caret escapes).
112114 tokens , err := SplitWith (`echo "say ^"hello^""` , CmdFormat ())
113115 if err != nil {
114116 t .Fatal (err )
115117 }
116118 words := tokens .Words ()
117119 last := words [len (words )- 1 ]
118- if last .Value != `say "hello"` {
119- t .Errorf ("cmd caret in quotes: Value = %q, want %q" , last .Value , `say "hello"` )
120+ // "say ^" → quote contains "say ^", then " closes quote
121+ // hello → bareword outside quotes
122+ // ^" → caret escapes the quote → literal "
123+ // " → this final quote opens a new quoted region (unterminated)
124+ // Words() merges adjacent tokens, so the whole thing is one word.
125+ if last .Value != `say ^hello"` {
126+ t .Errorf ("cmd caret in quotes: Value = %q, want %q" , last .Value , `say ^hello"` )
120127 }
121128}
122129
@@ -176,3 +183,159 @@ func TestCmdFormat_CaretAtEOF(t *testing.T) {
176183 t .Errorf ("cmd caret EOF: Value = %q, want %q" , last .Value , "foo" )
177184 }
178185}
186+
187+ func TestCmdFormat_CaretLiteralInQuotes (t * testing.T ) {
188+ // Cmd: ^ is literal inside double quotes — does not escape the next char.
189+ // "hello^world" should produce hello^world, not helloworld.
190+ tokens , err := SplitWith (`echo "hello^world"` , CmdFormat ())
191+ if err != nil {
192+ t .Fatal (err )
193+ }
194+ words := tokens .Words ().Strings ()
195+ if len (words ) != 2 || words [0 ] != "echo" || words [1 ] != "hello^world" {
196+ t .Errorf ("cmd caret literal in quotes: Words = %v, want [echo hello^world]" , words )
197+ }
198+ }
199+
200+ func TestCmdFormat_DoubleCaretLiteralInQuotes (t * testing.T ) {
201+ // Cmd: ^^ inside quotes is literal ^^ (both carets), not a single ^.
202+ tokens , err := SplitWith (`echo "hello^^world"` , CmdFormat ())
203+ if err != nil {
204+ t .Fatal (err )
205+ }
206+ words := tokens .Words ().Strings ()
207+ if len (words ) != 2 || words [0 ] != "echo" || words [1 ] != "hello^^world" {
208+ t .Errorf ("cmd double caret in quotes: Words = %v, want [echo hello^^world]" , words )
209+ }
210+ }
211+
212+ func TestCmdFormat_LineContinuation (t * testing.T ) {
213+ // Cmd: ^ at end of line is a line continuation — ^\n is consumed
214+ tokens , err := SplitWith ("echo foo^\n bar" , CmdFormat ())
215+ if err != nil {
216+ t .Fatal (err )
217+ }
218+ words := tokens .Words ().Strings ()
219+ if len (words ) != 2 || words [0 ] != "echo" || words [1 ] != "foobar" {
220+ t .Errorf ("cmd line continuation: Words = %v, want [echo foobar]" , words )
221+ }
222+ }
223+
224+ func TestCmdFormat_LineContinuationCRLF (t * testing.T ) {
225+ // Cmd: ^ at end of line with CRLF is a line continuation
226+ tokens , err := SplitWith ("echo foo^\r \n bar" , CmdFormat ())
227+ if err != nil {
228+ t .Fatal (err )
229+ }
230+ words := tokens .Words ().Strings ()
231+ if len (words ) != 2 || words [0 ] != "echo" || words [1 ] != "foobar" {
232+ t .Errorf ("cmd line continuation CRLF: Words = %v, want [echo foobar]" , words )
233+ }
234+ }
235+
236+ func TestCmdFormat_ParenGrouping (t * testing.T ) {
237+ // Cmd: ( and ) are grouping operators
238+ tokens , err := SplitWith ("(echo foo) & echo bar" , CmdFormat ())
239+ if err != nil {
240+ t .Fatal (err )
241+ }
242+ pipelines := tokens .Pipelines ()
243+ if len (pipelines ) != 2 {
244+ t .Errorf ("cmd parens: %d pipelines, want 2" , len (pipelines ))
245+ }
246+ }
247+
248+ func TestCmdFormat_ParenBeforeCommand (t * testing.T ) {
249+ // Cmd: ( and ) are wordbreak operators; with spaces they separate from words
250+ // They are not redirect operators, so FilterRedirects keeps them.
251+ // Words() does not merge non-adjacent tokens.
252+ tokens , err := SplitWith ("( echo hello )" , CmdFormat ())
253+ if err != nil {
254+ t .Fatal (err )
255+ }
256+ pipeline := tokens .CurrentPipeline ()
257+ words := pipeline .Words ().Strings ()
258+ if len (words ) != 4 || words [0 ] != "(" || words [1 ] != "echo" || words [2 ] != "hello" || words [3 ] != ")" {
259+ t .Errorf ("cmd paren before cmd: Words = %v, want [( echo hello )]" , words )
260+ }
261+ }
262+
263+ func TestCmdFormat_CommaDelimiter (t * testing.T ) {
264+ // Cmd: comma is a word delimiter (like space)
265+ tokens , err := SplitWith ("echo hello,world" , CmdFormat ())
266+ if err != nil {
267+ t .Fatal (err )
268+ }
269+ words := tokens .Words ().Strings ()
270+ if len (words ) != 3 || words [0 ] != "echo" || words [1 ] != "hello" || words [2 ] != "world" {
271+ t .Errorf ("cmd comma: Words = %v, want [echo hello world]" , words )
272+ }
273+ }
274+
275+ func TestCmdFormat_CommaInQuotes (t * testing.T ) {
276+ // Cmd: comma inside double quotes is literal
277+ tokens , err := SplitWith (`echo "hello,world"` , CmdFormat ())
278+ if err != nil {
279+ t .Fatal (err )
280+ }
281+ words := tokens .Words ().Strings ()
282+ if len (words ) != 2 || words [0 ] != "echo" || words [1 ] != "hello,world" {
283+ t .Errorf ("cmd comma in quotes: Words = %v, want [echo hello,world]" , words )
284+ }
285+ }
286+
287+ func TestCmdFormat_StreamRedirect2 (t * testing.T ) {
288+ // Cmd: 2> should be recognized as a stream redirect (stderr)
289+ tokens , err := SplitWith ("echo foo 2> bar" , CmdFormat ())
290+ if err != nil {
291+ t .Fatal (err )
292+ }
293+ pipelines := tokens .Pipelines ()
294+ if len (pipelines ) != 1 {
295+ t .Errorf ("cmd 2>: %d pipelines, want 1" , len (pipelines ))
296+ }
297+ // The 2> should be a redirect, so filtered words should not include "2"
298+ filtered := pipelines [0 ].FilterRedirects ().Words ().Strings ()
299+ if len (filtered ) != 2 || filtered [0 ] != "echo" || filtered [1 ] != "foo" {
300+ t .Errorf ("cmd 2> filtered: Words = %v, want [echo foo]" , filtered )
301+ }
302+ }
303+
304+ func TestCmdFormat_StreamRedirectMerge (t * testing.T ) {
305+ // Cmd: 2>&1 should be recognized as a stream merge redirect
306+ tokens , err := SplitWith ("echo foo 2>&1 bar" , CmdFormat ())
307+ if err != nil {
308+ t .Fatal (err )
309+ }
310+ pipelines := tokens .Pipelines ()
311+ if len (pipelines ) != 1 {
312+ t .Errorf ("cmd 2>&1: %d pipelines, want 1" , len (pipelines ))
313+ }
314+ filtered := pipelines [0 ].FilterRedirects ().Words ().Strings ()
315+ if len (filtered ) != 2 || filtered [0 ] != "echo" || filtered [1 ] != "foo" {
316+ t .Errorf ("cmd 2>&1 filtered: Words = %v, want [echo foo]" , filtered )
317+ }
318+ }
319+
320+ func TestCmdFormat_StreamRedirectCompletion (t * testing.T ) {
321+ // Cmd: completing after 2> should detect redirect
322+ ctx := SplitForCompletion ("echo foo 2> bar" , CmdFormat ())
323+ if ! ctx .IsRedirect {
324+ t .Errorf ("cmd 2> completion: IsRedirect = false, want true" )
325+ }
326+ if ctx .CurrentWord != "bar" {
327+ t .Errorf ("cmd 2> completion: CurrentWord = %q, want %q" , ctx .CurrentWord , "bar" )
328+ }
329+ }
330+
331+ func TestCmdFormat_CaretLineContinuationAtEOF (t * testing.T ) {
332+ // Cmd: ^ at EOF (no newline) should enter ESCAPING_STATE, not line continuation
333+ tokens , err := SplitWith ("echo foo^" , CmdFormat ())
334+ if err != nil {
335+ t .Fatal (err )
336+ }
337+ last := tokens .Words ().CurrentToken ()
338+ if last .State != ESCAPING_STATE {
339+ t .Errorf ("cmd caret EOF: State = %v, want ESCAPING_STATE" , last .State )
340+ }
341+ }
0 commit comments