Pipe to subprocess stdin for JXA

1k Views Asked by At

I would like to start a subprocess in JavaScript for Automation (JXA) and send a string to that subprocess's stdin which might include newlines, shell metas, etc. Previous AppleScript approaches for this used bash's <<< operator, string concatenation, and quoted form of the string. If there was a JavaScript equivalent of quoted form of that I could trust to get all of the edge cases, I could use the same approach; I'm investigating regex methods toward that end.

However, I thought since we have access to unistd.h from JXA, why not try to just call $.pipe, $.fork, and $.execlp directly? $.pipe looks like it should take an array of 2 integers as its parameter, but none of the things that I have tried worked:

ObjC.import('unistd')
$.pipe() // Error: incorrect number of arguments
$.pipe([]) // segfault
$.pipe([3,4]) // segfault
$.pipe([$(), $()]) // segfault
var a = $(), b=$()
$.pipe([a,b]) // segfault
$.pipe($([a,b])) // NSException without a terribly helpful backtrace
$.pipe($([$(3), $(4)])) // segfault
var ref = Ref('int[2]')
$.pipe(ref)
ref[0] // 4, which is close!

Any suggestions?

3

There are 3 best solutions below

2
Joe Hildebrand On

I found an approach that works, using Cocoa instead of stdio:

ObjC.import('Cocoa')
var stdin = $.NSPipe.pipe
var stdout = $.NSPipe.pipe
var task = $.NSTask.alloc.init
task.launchPath = "/bin/cat"
task.standardInput = stdin
task.standardOutput = stdout

task.launch
var dataIn = $("foo$HOME'|\"").dataUsingEncoding($.NSUTF8StringEncoding)
stdin.fileHandleForWriting.writeData(dataIn)
stdin.fileHandleForWriting.closeFile
var dataOut = stdout.fileHandleForReading.readDataToEndOfFile
var stringOut = $.NSString.alloc.initWithDataEncoding(dataOut, $.NSUTF8StringEncoding).js
console.log(stringOut)
0
mklement0 On

It is indeed curious that there appears to be no JXA equivalent of AppleScript's quoted form of for safely passing script literals to shell commands.

However, it is fairly easy to implement:

// JXA implementation of AppleScript's `quoted form of`
function quotedForm(s) { return "'" + s.replace(/'/g, "'\\''") + "'" }

// Example
app = Application.currentApplication();
app.includeStandardAdditions = true;

console.log(app.doShellScript('cat <<<' + quotedForm("foo$HOME'|\"")))

Credit for quotedForm() goes to this comment.

As far as I can tell, this implementation does the same as quoted form of does:

  • In the simplest form, if the string contains no embedded single-quotes, it single-quotes the entire string; since POSIX-like shells perform no interpolation whatsoever on a single-quoted string, it is preserved as-is.

  • If the string does contain embedded single-quotes, it is effectively broken into multiple single-quoted strings, with each embedded single-quote spliced in as \' (backslash-escaped) - this is necessary, because it is not possible to embed single-quotes in single-quoted literal in POSIX-compatible shells.

In a POSIX-compatible shell, this should work for all strings.

1
UKenGB On

The quotedForm function above (below?) is lacking one very important feature, it only quotes/escapes the first in-line apostrophe whereas it needs to deal with however many exist in the string.

I changed it to this which seems to work:-

// JXA implementation of AppleScript's `quoted form of`
function quotedFormOf(s) { return "'" + s.replace(/'/g, "'\\''") + "'" }