@@ -12,6 +12,7 @@ import {
1212 firstPositional ,
1313 optionValue ,
1414 positionalArgs ,
15+ scanTopLevelChars ,
1516 splitShellWords ,
1617 splitTopLevelPipes ,
1718 splitTopLevelStages ,
@@ -299,6 +300,115 @@ function summarizePipeline(stage: string): string {
299300 return summarizeKnownExec ( trimLeadingEnv ( splitShellWords ( stage ) ) ) ;
300301}
301302
303+ type HeredocTerminator = {
304+ value : string ;
305+ stripLeadingTabs : boolean ;
306+ } ;
307+
308+ function collectHeredocTerminators ( commandLine : string ) : HeredocTerminator [ ] {
309+ const terminators : HeredocTerminator [ ] = [ ] ;
310+ scanTopLevelChars ( commandLine , ( char , index ) => {
311+ if ( char !== "<" || commandLine [ index + 1 ] !== "<" || commandLine [ index + 2 ] === "<" ) {
312+ return true ;
313+ }
314+
315+ const stripLeadingTabs = commandLine [ index + 2 ] === "-" ;
316+ const parsed = parseHeredocTerminator ( commandLine , index + ( stripLeadingTabs ? 3 : 2 ) ) ;
317+ if ( parsed ) {
318+ terminators . push ( { value : parsed , stripLeadingTabs } ) ;
319+ }
320+ return true ;
321+ } ) ;
322+ return terminators ;
323+ }
324+
325+ function parseHeredocTerminator ( commandLine : string , rawStart : number ) : string | undefined {
326+ let start = rawStart ;
327+ while ( / \s / u. test ( commandLine [ start ] ?? "" ) ) {
328+ start += 1 ;
329+ }
330+
331+ let value = "" ;
332+ let quote : '"' | "'" | undefined ;
333+
334+ for ( let index = start ; index < commandLine . length ; index += 1 ) {
335+ const char = commandLine [ index ] ?? "" ;
336+
337+ if ( quote ) {
338+ if ( char === quote ) {
339+ quote = undefined ;
340+ continue ;
341+ }
342+ if ( quote === '"' && char === "\\" && index + 1 < commandLine . length ) {
343+ index += 1 ;
344+ value += commandLine [ index ] ?? "" ;
345+ continue ;
346+ }
347+ value += char ;
348+ continue ;
349+ }
350+
351+ if ( / [ \s ; & | < > ] / u. test ( char ) ) {
352+ break ;
353+ }
354+ if ( char === "'" || char === '"' ) {
355+ quote = char ;
356+ continue ;
357+ }
358+ if ( char === "\\" && index + 1 < commandLine . length ) {
359+ index += 1 ;
360+ value += commandLine [ index ] ?? "" ;
361+ continue ;
362+ }
363+ value += char ;
364+ }
365+
366+ return value || undefined ;
367+ }
368+
369+ function commandWithoutHeredocBodies ( command : string ) : string | undefined {
370+ if ( ! command . includes ( "\n" ) ) {
371+ return undefined ;
372+ }
373+
374+ const lines = command . split ( / \r ? \n / u) ;
375+ const summaryLines : string [ ] = [ ] ;
376+ let foundHeredoc = false ;
377+
378+ for ( let index = 0 ; index < lines . length ; index += 1 ) {
379+ const line = lines [ index ] ?? "" ;
380+ summaryLines . push ( line ) ;
381+
382+ const terminators = collectHeredocTerminators ( line ) ;
383+ if ( terminators . length === 0 ) {
384+ continue ;
385+ }
386+ foundHeredoc = true ;
387+
388+ for ( const terminator of terminators ) {
389+ index += 1 ;
390+ while ( index < lines . length ) {
391+ const candidate = terminator . stripLeadingTabs
392+ ? ( lines [ index ] ?? "" ) . replace ( / ^ \t + / u, "" )
393+ : ( lines [ index ] ?? "" ) ;
394+ if ( candidate === terminator . value ) {
395+ break ;
396+ }
397+ index += 1 ;
398+ }
399+ }
400+ }
401+
402+ if ( ! foundHeredoc ) {
403+ return undefined ;
404+ }
405+
406+ return summaryLines
407+ . map ( ( line ) => line . trim ( ) )
408+ . filter ( Boolean )
409+ . join ( "; " ) ;
410+ }
411+
302412type ExecSummary = {
303413 text : string ;
304414 chdirPath ?: string ;
@@ -362,7 +472,8 @@ function summarizeExecCommand(command: string): ExecSummary | undefined {
362472 return chdirPath ? { text : "" , chdirPath } : undefined ;
363473 }
364474
365- const stages = splitTopLevelStages ( cleaned ) ;
475+ const summaryCommand = commandWithoutHeredocBodies ( cleaned ) ?? cleaned ;
476+ const stages = splitTopLevelStages ( summaryCommand ) ;
366477 if ( stages . length === 0 ) {
367478 return undefined ;
368479 }
0 commit comments