diff --git a/index.ts b/index.ts index 9569354..cf6b9cf 100644 --- a/index.ts +++ b/index.ts @@ -66,6 +66,10 @@ export interface Options extends SpawnOptions { * arguments to your program */ args?: string[]; + /** + * if enabled, \r (carriage return) characters will trigger a 'crdata' event + */ + handleCarriageReturn?: boolean; } export class PythonShellError extends Error { @@ -83,12 +87,42 @@ export class PythonShellErrorWithLogs extends PythonShellError { export class NewlineTransformer extends Transform { // NewlineTransformer: Megatron's little known once-removed cousin private _lastLineData: string; + private _handleCr: boolean; + constructor(handleCr?: boolean) { + super(); + this._handleCr = !!handleCr; + } _transform(chunk: any, encoding: string, callback: TransformCallback) { let data: string = chunk.toString(); if (this._lastLineData) data = this._lastLineData + data; - const lines = data.split(newline); - this._lastLineData = lines.pop(); - lines.forEach(this.push.bind(this)); + if (this._handleCr) { + let current = ''; + for (let i = 0; i < data.length; i++) { + const ch = data[i]; + if (ch === '\r') { + if (i + 1 < data.length && data[i + 1] === '\n') { + // \r\n — Windows line ending: treat as regular newline + this.push(current); + i++; + } else { + // \r alone — progress update: emit as cr only + this.emit('cr', current); + } + current = ''; + } else if (ch === '\n') { + // \n alone — regular line ending: push as newline message + this.push(current); + current = ''; + } else { + current += ch; + } + } + this._lastLineData = current; + } else { + const lines = data.split(newline); + this._lastLineData = lines.pop(); + lines.forEach(this.push.bind(this)); + } callback(); } _flush(done: TransformCallback) { @@ -201,22 +235,32 @@ export class PythonShell extends EventEmitter { // for example JSON parsing breaks if it recieves partial JSON // so we use newlineTransformer to emit each batch seperated by newline if (this.parser && this.stdout) { - if (!stdoutSplitter) stdoutSplitter = new NewlineTransformer(); + if (!stdoutSplitter) stdoutSplitter = new NewlineTransformer(options.handleCarriageReturn); // note that setting the encoding turns the chunk into a string stdoutSplitter.setEncoding(options.encoding || 'utf8'); this.stdout.pipe(stdoutSplitter).on('data', (chunk: string) => { this.emit('message', self.parser(chunk)); }); + if (options.handleCarriageReturn) { + stdoutSplitter.on('cr', (chunk: string) => { + this.emit('crdata', self.parser(chunk)); + }); + } } // listen to stderr and emit errors for incoming data if (this.stderrParser && this.stderr) { - if (!stderrSplitter) stderrSplitter = new NewlineTransformer(); + if (!stderrSplitter) stderrSplitter = new NewlineTransformer(options.handleCarriageReturn); // note that setting the encoding turns the chunk into a string stderrSplitter.setEncoding(options.encoding || 'utf8'); this.stderr.pipe(stderrSplitter).on('data', (chunk: string) => { this.emit('stderr', self.stderrParser(chunk)); }); + if (options.handleCarriageReturn) { + stderrSplitter.on('cr', (chunk: string) => { + this.emit('crdata', self.stderrParser(chunk)); + }); + } } if (this.stderr) { @@ -494,6 +538,16 @@ export interface PythonShell { listener: (parsedChunk: any) => void, ): this; + addListener(event: 'crdata', listener: (parsedChunk: any) => void): this; + emit(event: 'crdata', parsedChunk: any): boolean; + on(event: 'crdata', listener: (parsedChunk: any) => void): this; + once(event: 'crdata', listener: (parsedChunk: any) => void): this; + prependListener(event: 'crdata', listener: (parsedChunk: any) => void): this; + prependOnceListener( + event: 'crdata', + listener: (parsedChunk: any) => void, + ): this; + addListener(event: 'close', listener: () => void): this; emit(event: 'close'): boolean; on(event: 'close', listener: () => void): this; diff --git a/test/python/echo_progress.py b/test/python/echo_progress.py new file mode 100644 index 0000000..c4770cd --- /dev/null +++ b/test/python/echo_progress.py @@ -0,0 +1,8 @@ +import sys + +sys.stdout.write('Progress: 50%\r') +sys.stdout.flush() +sys.stdout.write('Progress: 100%\r') +sys.stdout.flush() +sys.stdout.write('Done\n') +sys.stdout.flush() diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 3d5de81..ed1e706 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -467,6 +467,38 @@ describe('PythonShell', function () { .send('world!') .end(done); }); + it('should emit "crdata" events for carriage return terminated output', function (done) { + let pyshell = new PythonShell('echo_progress.py', { + mode: 'text', + handleCarriageReturn: true, + }); + let crdataMessages: string[] = []; + let messages: string[] = []; + pyshell + .on('crdata', (data) => { + crdataMessages.push(data); + }) + .on('message', (data) => { + messages.push(data); + }) + .on('close', () => { + crdataMessages.should.eql(['Progress: 50%', 'Progress: 100%']); + messages.should.eql(['Done']); + done(); + }); + }); + it('should not emit "crdata" events when handleCarriageReturn is disabled', function (done) { + let pyshell = new PythonShell('echo_progress.py', { + mode: 'text', + }); + pyshell + .on('crdata', () => { + done(new Error('should not emit crdata events when disabled')); + }) + .on('close', () => { + done(); + }); + }); }); describe('stderr', function () {