Pipe to subprocess stdin for JXA
Asked Answered
R

3

1

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?

Robber answered 21/12, 2014 at 4:18 Comment(0)
R
2

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)
Robber answered 22/12, 2014 at 3:16 Comment(2)
This no longer works in Mojave due to the removal of NSTask.launch. Any workarounds?Nimesh
This might work: task.executableURL = $.NSURL.alloc.initFileURLWithPath("/bin/cat"); let er = $.NSError.alloc.initWithDomainCodeUserInfo("", 0, ""); let ret = task.launchAndReturnError(er); if (!ret) { console.log("ERROR", er.localizedDescription.UTF8String); }Robber
T
1

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.

Thebaine answered 10/10, 2015 at 6:14 Comment(0)
C
0

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, "'\\''") + "'" }
Carbonari answered 8/2, 2017 at 19:9 Comment(1)
Good point - yes, that's what my answer should always have done, and it's now updated. As for "above" vs. "below". You can't rely on a specific ordering of posts over time, so it's best to simply link to another answer you're referring to, as I've done in this comment; in Markdown, you can construct the link as follows [<link-text>](<link-url>) and even using raw URLs work (though that's less pretty).Thebaine

© 2022 - 2024 — McMap. All rights reserved.