Using Perl 6 on Windows I want to programatically call a .bat file. Both the path to the .bat file and one of its arguments have a space in it. I can not call the .bat file directly for reasons given at the end of the question. The call I want to make is:
"C:\data\p6 repos\rakudo\perl6-m.bat" --target=mbc "--output=C:/data/p6 repos/rakudo/lib/.precomp/EA6F1A9F9A17B17B899C4C7C4A775A883DAF537E/33/33A52796DB3EBB40BEF94B7696A1B0AB7A29B5C5.bc" "C:/data/p6 repos/rakudo/lib/CompUnit/Repository/Staging.pm6"
I need to capture its STDIN and STDOUT and retrieve the exit code.
How do I do this?
According to this superuser answer one has to use the following command to run this (notice the extra quotes at the very start and end of my command):
cmd.exe /C ""C:\data\p6 repos\rakudo\perl6-m.bat" --target=mbc "--output=C:/data/p6 repos/rakudo/lib/.precomp/EA6F1A9F9A17B17B899C4C7C4A775A883DAF537E/33/33A52796DB3EBB40BEF94B7696A1B0AB7A29B5C5.bc" "C:/data/p6 repos/rakudo/lib/CompUnit/Repository/Staging.pm6""
That works when pasted into a cmd command window.
Doing this in Perl 6 errors out. Doing the same in Python gives the same errors. This is my code.
my $perl6 = 'C:\\data\\p6 repos\\rakudo\\perl6-m.bat';
my $bc = 'C:/data/p6 repos/rakudo/lib/.precomp/EA6F1A9F9A17B17B899C4C7C4A775A883DAF537E/33/33A52796DB3EBB40BEF94B7696A1B0AB7A29B5C5.bc';
my $path = 'C:/data/p6 repos/rakudo/lib/CompUnit/Repository/Staging.pm6';
react {
my $proc = Proc::Async.new(
'C:\WINDOWS\system32\cmd.exe', '/C', "\"\"$perl6\" --target=mbc \"--output=$bc\" \"$path\"\""
);
whenever $proc.stdout {
say "Out: $_"
}
whenever $proc.stderr {
say "Err: $_"
}
whenever $proc.start(ENV => %(RAKUDO_MODULE_DEBUG => 1)) {
say "Status: {$_.exitcode}"
}
}
This results in:
Err: The network path was not found.
Using forward slashes in the $perl6 command results in:
Err: The filename, directory name, or volume label syntax is incorrect.
Leaving the surrounding quotation marks of the command off
'C:\WINDOWS\system32\cmd.exe', '/C', "\"$perl6\" --target=mbc \"--output=$bc\" \"$path\""
results in:
Err: '\"C:\data\p6 repos\rakudo\perl6-m.bat\"' is not recognized as an internal or external command,
operable program or batch file
Err: .
Passing the arguments separately in the command
'C:\WINDOWS\system32\cmd.exe', '/C', $perl6, "--target=mbc", "--output=$bc", $path
results in:
Err: 'C:\data\p6' is not recognized as an internal or external command,
operable program or batch file.
Quoting the executable but leaving the arguments separately
'C:\WINDOWS\system32\cmd.exe', '/C', "\"$perl6\"", "--target=mbc", "--output=$bc", $path
results in:
Err: The filename, directory name, or volume label syntax is incorrect.
Also quoting the other arguments separately
'C:\WINDOWS\system32\cmd.exe', '/C', "\"$perl6\"", "\"--target=mbc\"", "\"--output=$bc\"", "\"$path\""
results in:
Err: '\"C:\data\p6 repos\rakudo\perl6-m.bat\"" "\"--target=mbc\"" "\"--output=C:/data/p6 repos/rakudo/lib/.precomp/EA6F1A9F9A17B17B8
Err: 99C4C7C4A775A883DAF537E/33/33A52796DB3EBB40BEF94B7696A1B0AB7A29B5C5.bc\"" "\"C:/data/p6 repos/rakudo/lib/CompUnit/Repository/Staging.pm6\"' is not recognized as an internal or external command,
operable program or batch file.
Keeping the arguments separate but adding quotationmarks at the beginning and end
'C:\WINDOWS\system32\cmd.exe', '/C', "\"\"$perl6\"", "\"--target=mbc\"", "\"--output=$bc\"", "\"$path\"\""
results in:
Err: The network path was not found.
The errors I get are largely the same when using comparable code on Python 3 and nodejs. So I'm pretty sure this is a problem further down.
So I think I tried everything one can possibly think of. I start to suspect that's a bug in cmd.exe and it's impossible to do.
On node.js the following works:
complete = '""C:\\data\\p6 repos\\rakudo\\perl6-m.bat" --target=mbc "--output=C:/data/p6 repos/rakudo/lib/.precomp/EA6F1A9F9A17B17B899C4C7C4A775A883DAF537E/33/33A52796DB3EBB40BEF94B7696A1B0AB7A29B5C5.bc" "C:/data/p6 repos/rakudo/lib/CompUnit/Repository/Staging.pm6""'
const { spawn } = require('child_process');
const bat = spawn('cmd.exe', ['/c', complete], { shell: true });
bat.stdout.on('data', (data) => {
console.log(data.toString());
});
bat.stderr.on('data', (data) => {
console.log(data.toString());
});
bat.on('exit', (code) => {
console.log(`Child exited with code ${code}`);
});
The trick is, that the above command results in two wrapped cmd.exe calls. That in turn gets rid of the limitation described above.
Sadly just copying this trick in Perl 6 still doesn't succeed
'C:\WINDOWS\system32\cmd.exe', '/d', '/s', '/c', "\"cmd.exe /c \"\"$perl6\" --target=mbc \"--output=$bc\" \"$path\"\""
That results in:
Err: The network path was not found.
The reason is, that libuv messes with the arguments it gets passed and puts a backslash before every quotation mark. There is a flag to turn that off called UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS.
The reason this works on node.js is, that it has some special handling that is triggered when 1. on windows, 2. the shell argument is passed to spawn and 3. the command one executes is cmd.exe again. In this very specific situation node passes the options /d /s /c to its cmd.exe and activates the UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS flag.
When adding that flag to moarvms implementation of spawnprocasync it works in Perl 6 also. Though I have no idea what other side effects that will have.
When calling the .bat file directly the respective line in the Perl 6 code above becomes
my $proc = Proc::Async.new( $perl6, "--target=mbc", "--output=$bc", $path );
The code errors out with:
Err: 'C:/data/p6' is not recognized as an internal or external command, operable program or batch file.
Here is some similar code in Python 3.
import subprocess
perl6 = 'C:/data/p6 repos/rakudo/perl6-m.bat'
bc = 'C:/data/p6 repos/rakudo/lib/.precomp/EA6F1A9F9A17B17B899C4C7C4A775A883DAF537E/33/33A52796DB3EBB40BEF94B7696A1B0AB7A29B5C5.bc'
path = 'C:/data/p6 repos/rakudo/lib/CompUnit/Repository/Staging.pm6'
p = subprocess.Popen([perl6, "--target=mbc", f"--output={bc}", path], stdout=subprocess.PIPE)
while p.poll() is None:
l = p.stdout.readline()
print(l)
print(p.stdout.read())
On Python this works.
The reason is, that Python calls the Windows API function CreateProcessW without specifying the first argument lpApplicationName. This is the place the actual call happens. application_name is left empty as long as one doesn't pass the executable argument to the subprocesss.Popen call above. When one passes the executable argument to subprocess.Popen the error is the same I get using Perl 6.
p = subprocess.Popen([perl6, "--target=mbc", f"--output={bc}", path], executable="\"C:/data/p6 repos/rakudo/perl6-m.bat\"", stdout=subprocess.PIPE)
Perl 6 uses libuv under the hood. In libuv it is currently not possible to not pass the first argument to CreateProcessW. Thus I can not call the .bat file directly as is possible using Python. The node.js docs (node.js also uses libuv) tell the same story.
node exposes
windowsVerbatimArguments: truewhen spawning processes. It's certainly pragmatic, but how well does that map to future backends? To that end I wonder if a good (but complicated) solution is something likequotemeta(but for this specific purpose) living inside rakudo such that users themselves would call this on whatever argument instead of passing e.g.windowsVerbatimArguments: trueto run/shell. Of course from a backwards compatibility perspective that wouldn't work sinceUV_PROCESS_WINDOWS_VERBATIM_ARGUMENTSwould need to be disabled in MoarVM.