Search code examples
perlscriptingexeccommand-line-argumentsstdin

How to exec self with command-line arguments added to stdin?


EDIT: just to clarify, I want to know how one would implement the piping of content to an exec'd process, putting aside the question of whether Perl offers better ways to achieve the same end-result that don't involve this technique.


It's easier to describe what I want to do with a toy zsh script example:

#!/usr/bin/env zsh

# -----------------------------------------------------------------------------
# handle command-line arguments if any
# nb: ${(%):-%x} is zsh-speak for "yours truly"

# (( $# % 2 )) && (print -rl -- "$@"; [[ -t 0 ]] || cat) | exec ${(%):-%x}
(( $# > 0 )) && ([[ -t 0 ]] || cat; print -rl -- "$@") | exec ${(%):-%x}

# -----------------------------------------------------------------------------
# standard operation below this line

nl

The script above is pretty much a pass-through wrapper for the nl ("number lines") utility, except that, if command-line arguments are present, it will append them to to its stdin. For example:

$ seq 3 | /tmp/nlwrapper.sh
     1      1
     2      2
     3      3
$ seq 3 | /tmp/nlwrapper.sh foo bar baz
     1      1
     2      2
     3      3
     4      foo
     5      bar
     6      baz

Note that

  1. the command-line arguments could have just as easily been prepended to stdin (in fact, if one uncomments the script's 7th line, the resulting script will prepend or append the command-line arguments to stdin depending on whether their number is odd or even, respectively); I am interested in both functionalities.

  2. the script consists of two entirely independent sections: a preamble that handles the command-line arguments (if any), and the body (in this toy example consisting of a single line) that takes care of the script's main functionality (numbering lines); this is an essential design feature.

To elaborate on these two points a bit further: the "body" section knows nothing about the business with the command-line arguments. The code that implements this handling of command-line arguments could be prepended pretty much "as-is" to any zsh script that processes stdin. Moreover, changes to how the command-line arguments are handled (prepend vs append, etc) leave everything below the comment # standard operation below this line untouched. The two sections are truly independent of each other.


What would be the equivalent of the preamble above in a Perl script?


I know that the Perl-equivalent of the script above would have the general form

#!/usr/bin/env perl

use strict;
use English;

# -----------------------------------------------------------------------------
# handle command-line arguments if any

if ( @ARGV > 0 ) {

   # (mumble, mumble)   ...   -t STDIN   ...   exec $PROGRAM_NAME;

}

# -----------------------------------------------------------------------------
# standard operation below this line

printf "%6d\t$_", $., $_ while <>;

My problem is implementing this bit:

([[ -t 0 ]] || cat; print -rl -- "$@") |

I do know that

  1. the [[ -t 0 ]] test in Perl is -t STDIN;
  2. the cat part could be implemented with print while <>; and
  3. the print -rl -- "$@" bit could be implemented with CORE::say for @ARGV

What I don't know is how to put these elements together to get the desired functionality.


Solution

  • Might be easier just using cat with some common shell features. (bash used here.)

    cat <( seq 3 ) <( printf 'foo\nbar\nbaz\n' ) | prog
    

    Solution:

    use POSIX qw( );
    
    sub munge_stdin {
       pipe(my $r, my $w) or die("pipe: $!");
       $w->autoflush();
    
       local $SIG{CHLD} = 'IGNORE';
       defined( my $pid = fork() ) or die("fork: $!");
       if (!$pid) {
          eval {
             close($r) or die("close pipe: $!");
    
             while (<STDIN>) {
                print($w $_) or die("print: $!");
             }
    
             for (@ARGV) {
                print($w "$_\n") or die("print: $!");
             }
    
             POSIX::_exit(0);
          };
    
          warn($@);
          POSIX::_exit(1);
       }
    
       close($w) or die("close pipe: $!");
       open(STDIN, '<&', $r) or die("dup: $!");
       @ARGV = ();
    }
    
    munge_stdin();
    print while <>;   # or: exec("cat") or die("exec: $!");
    
    • This solution supports exec() in the Perl script.
    • This solution won't deadlock no matter how large the inputs are.