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