http://www.perlmonks.org?node_id=1186687

A small meditation started by Ssh and qx


Intro

Let's face it: qx is evil, as soon as you want to reliably pass arguments to a program. And it's not necessarily perl's fault. Blame the default shell (Update: see The problem of "the" default shell). Luckily, perl has multi-argument pipe open since 5.8.0:

open(my $pipe,'-|','/usr/local/bin/foo','bar','baz','1&2>3') or die "C +an't start foo: $!"; my @output=<$pipe>; close $pipe or die "Broken pipe: $!";

It's so easy. Granted, it takes two more lines than qx, but we got rid of the default shell. And that two extra lines could easily be wrapped in a function:

my @output=safe_qx('/usr/local/bin/foo','bar','baz','1&2>3');

But, of course, that would be too easy to be true. Why can't we have nice things?

Three-argument pipe open gets the nasty default shell back into play:

> perl -E 'open my $pipe,"-|","pstree --ascii --arguments --long $$ 1> +&2" or die $!;' perl -E open my $pipe,"-|","pstree --ascii --arguments --long $$ 1>&2" + or die $!; `-sh -c pstree --ascii --arguments --long 22176 1>&2 `-pstree --ascii --arguments --long 22176 >

Now what? We could resort to my favorite part of perlipc, "Safe pipe opens". 15 to 28 lines of code just to safely start an external program, and all of that only because perl wants to be clever instead of being safe.


How to fix it

Let's make multi-argument pipe open clever.

system and exec have the indirect-object-as-executable-name hack to prevent the default shell mess. Applying that to open might be possible, but still looks quite hacky:

open $list[0] my $pipe,'-|',@list or die "Can't open pipe: $!";

No! Just no!

So, do we really need to specify the executable twice? We usually don't want to lie to the target program about it's name. It might be useful to make a shell think that it's a login shell, but then again, that can also be done by passing an extra argument. No, we don't want to lie to our child process. If backwards compatibility was not a problem, we could simply disable the shell logic for any pipe open with more than two arguments. But for backwards compatibility, we can't do that. We need is a flag to disable the shell logic.

My first idea was to just double the dash in the MODE argument:

ModeActionUsage of default shell
-|Read from child's STDOUTenabled for three argument open,
disabled for more than three arguments passed to open
(legacy mode)
|-Write to child's STDIN
--|Read from child's STDOUTdisabled
|--Write to child's STDIN

But we still can do better: A single bit is sufficient for a flag. + is already used in MODE, but not in combination with the pipe symbol. So let's use + instead of - to disable the default shell:

ModeActionUsage of default shell
-|Read from child's STDOUTenabled for three argument open,
disabled for more than three arguments passed to open
(legacy mode)
|-Write to child's STDIN
+|Read from child's STDOUTdisabled
|+Write to child's STDIN

Yes, I'm aware that the difference between "+" and "-" in ASCII is two bits.


Update:

For a better mnemonic (ls -f uses "*" to indicate an executable file), we could use "*" instead of "-" to disable the default shell and specify the executable file in the third argument:

ModeActionUsage of default shell
-|Read from child's STDOUTenabled for three argument open,
disabled for more than three arguments passed to open
(legacy mode)
|-Write to child's STDIN
*|Read from child's STDOUTdisabled
|*Write to child's STDIN

Thanks to huck and hippo for finding two missing quotes.

Alexander

--
Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)