Home > Software design >  Is there a way to salvage these broken bash escape sequence artifacts from my Node.js spawned shell
Is there a way to salvage these broken bash escape sequence artifacts from my Node.js spawned shell

Time:10-19

I wrote a class for spawning and interacting with a shell (within an Electron app) like so:

const { spawn } = require('child_process')
class Shell {
    constructor() {
        this.process = spawn('/bin/bash', []);
        this.process.stdout.on('data', (data) => {
            const out = data.toString()
            console.log('stdout:', out)
            if (this.res) {
                this.res(out)
            }
        });
        this.process.stderr.on('data', function (data) {
            const err = data.toString()
            console.log('stderr:', err)
            if (this.rej) this.rej(err)
        });
    }
    send(command, throwErr = false) {
        return new Promise((resolve, reject) => {
            this.res = resolve
            if (throwErr) this.rej = reject
            else this.rej = resolve
            this.process.stdin.write(command   '\n')
        })
    }
}

And the output I'm getting is like:

stdout: ]0;student@linux-opstation-jyrf: ~[01;32mstudent@linux-opstation-jyrf[00m:[01;34m~[00m$

Here's a version where I stringify the outout with JSON to see the escape chars: stdout: "\u001b]0;student@linux-opstation-jyrf: ~\u0007\u001b[01;32mstudent@linux-opstation-jyrf\u001b[00m:\u001b[01;34m~\u001b[00m$ ssh -t -t -oStrictHostKeyChecking=no [email protected]\r50.30.231\r\n"

I realize these are artifacts from bash escape sequences for formatting and I'm having trouble figuring how to get rid of it, especially since the escaped characters aren't printed.

Edit: So I wrote the raw stdout Buffer (data in the code) to a binary file:

fs.createWriteStream(path, { encoding: 'binary'}).write(data);

and found that there seems to be no loss happening in the .toString() method (I think?) so I'm left scratching my head where the rest of the stdout markup is getting truncated.

00000000: 1b5d 303b 7374 7564 656e 7440 6c69 6e75  .]0;student@linu
00000010: 782d 6f70 7374 6174 696f 6e2d 6a79 7266  x-opstation-jyrf
00000020: 3a20 7e07 1b5b 3031 3b33 326d 7374 7564  : ~..[01;32mstud
00000030: 656e 7440 6c69 6e75 782d 6f70 7374 6174  ent@linux-opstat
00000040: 696f 6e2d 6a79 7266 1b5b 3030 6d3a 1b5b  ion-jyrf.[00m:.[
00000050: 3031 3b33 346d 7e1b 5b30 306d 2420 7373  01;34m~.[00m$ ss
00000060: 6820 2d74 202d 7420 2d6f 5374 7269 6374  h -t -t -oStrict
00000070: 486f 7374 4b65 7943 6865 636b 696e 673d  HostKeyChecking=
00000080: 6e6f 2073 7475 6465 6e74 4031 302e 350d  no [email protected].
00000090: 3530 2e33 302e 3233 310d 0a              50.30.231..

But maybe I'm not getting the encoding right when I save the file because I think the raw buffer should(?) output text like this \u001b[00m:\u001b[01;34m and the \uFFFF hex chars aren't there. Edit: Ah, \uFFFF is unicode apparently. Still figuring out how to save that buffer properly as binary, I think the unicode is being lost with {encoding: 'binary'} set as the option. Or else the hex dump only shows utf8, that sounds more likely.

CodePudding user response:

Supposing that these artifacts are indeed coming from a customized prompt string, the easiest thing to do would be to change the prompt string. There is a variety of alternatives for that, somewhat dependent on where the prompt string is being set.

If you are getting a prompt at all, then bash is running as an interactive shell. It will not be running as a login shell with the command you are using to launch it. The relevant initialization steps are thus these:

When an interactive shell that is not a login shell is started, bash reads and executes commands from ~/.bashrc, if that file exists. This may be inhibited by using the --norc option. The --rcfile file option will force bash to read and execute commands from file instead of ~/.bashrc.

(Bash manual page)

So, you could pass the --norc option to bash to suppress reading any shell initialization file, thereby getting the default prompt (and also the default everything else). But that environment might be too sparse for your needs, so as an alternative, you could create a for-purpose shell configuration file that sets up the exact configuration you want, and use the --rcfile option to make the bash instance started by your program use that. This is probably your best option, but it might take some work to get set up as you need.

As a quicker and dirtier alternative, you could modify the relevant ~/.bashrc to change the prompt strings (remove definitions of PS1 and PS2, or else redefine them to defaults: PS1='\s-\v\$ ' PS2='> '). You could also combine the previous two with a custom config file that reads the default one and overrides the prompt strings:

electron.bashrc

. ~/.bashrc
PS1='\s-\v\$ '
PS2='> '

CodePudding user response:

This is a hacky, ugly, quick-n-dirty solution. I welcome more elegant approaches:

const { spawn } = require('child_process')
class Shell {
    constructor() {
        // I just threw a bunch of output into a regex tester 
        // and wrote `|` joined matches until all markup in my
        // sample input was detected. That's all there is to it.
        // This will almost certainly not work across various
        // machines depending on how the markup is structured.
        const re = /\\u[0-9 a-f]{4}\[\d\d;\d\dm|\\u[0-9 a-f]{4}]0;|\\u[0-9 a-f]{4}\[\d\dm:\\u[0-9 a-f]{4}\[\d\d;\d\dm|\\u[0-9 a-f]{4}\[\d\dm|\\u[0-9 a-f]{4}/g
        this.process = spawn('/bin/bash', []);

        this.process.stdout.on('data', (data) => {
            const out = data.toString()
            const stringified = JSON.stringify(out)
            console.log('stdout:', stringified)
            const trimmed = stringified.replace(re, "")
            .split('"').join('')
            .split('\\r').join('')
            .split('\\n').join('')
            console.log('parsed stdout:', trimmed)
            if (this.res) {
                this.res(out)
            }
        });
        this.process.stderr.on('data', function (data) {
            const err = data.toString()
            console.log('stderr:', err)
            if (this.rej) this.rej(err)
        });
    }
    send(command, throwErr = false) {
        return new Promise((resolve, reject) => {
            this.res = resolve
            if (throwErr) this.rej = reject
            else this.rej = resolve
            this.process.stdin.write(command   '\n')
        })
    }
}
  • Related