azredwing has asked for the wisdom of the Perl Monks concerning the following question:

Hi all,

I wrote a program for work (I'm an undergraduate intern who taught himself Perl in about 3 weeks) that basically generates a giant report and dumps it to the terminal. It works great, but I'd like people to be able to pipe it to either dump to a file or use the "more" command, as in these examples:

$ my_program > w00t.txt $ my_program | more
I can easily implement either of these features from within the program, but I'd like to be able to this to make the program more "UNIX-like". I don't want to make people learn some new switch/flag/whatever when it's possible to use the UNIX pipes, especially if people want to use "cat", "lpr" or "grep", etc.

I'm not going to post the code here; it's pretty long, irrelevant to the question, and essentially the print to STDOUT happens within the last 5 lines.

So, how do I pipe my output to, say, "more"?

Thanks for all the help!

Replies are listed 'Best First'.
Re: How do I pipe the output of a Perl program to something else?
by kyle (Abbot) on Feb 14, 2008 at 03:57 UTC

    Have you tried your examples, as written? I'd expect them to work exactly like that. If they're not working, what happens when you try?

      Aha!: My program requests user input.

      If I replaced the <STDIN> with a typical user request, it works fine.

      So the problem is getting it to play with STDIN. Now what??

      EDIT: Here's a snippet of the code that's not working:

      while(1) { print "\nEnter the day number you are interested in: "; chomp($sol_request=<STDIN>); #append 0 to the front if($day_request < 100 && $day_request !~/0[0-9][0-9]/) { $sol_request = "0".$day_request; } if(not -d "/somedir/$day_request/") { print "\nSol $day_request does not exist! Please try again +\n"; } else { last; } }
      When I run the code with the pipe to more, the message "Enter day number" does not appear. Putting in a number works as usual, and the message does not print until after you hit <ENTER>. The number you enter also never appears on screen. The output correctly gets piped to more.

      EDIT2: I guess I didn't quite understand what was going on with the more command. It captures everything going to STDOUT and more's it, right? I don't want that line going to STDOUT. I printed to STDERR instead and now that message shows. The user cannot see what he is typing, though. I could always print out what he typed to STDERR as well, but is a way to show the user what he's typing?

        Rule Number One for writing "UNIX-style" command-line tools: Thou shalt not prompt the user for keyboard input. Whatever decisions the user needs to make in order for the tool to do his bidding on a given run, it should be possible to have those decisions made before hitting "Enter" on the command line to make it run -- and those decisions take the form of "arguments" (one or more space-separated "word" tokens that are typed directly after the name of the program).

        In other words, everything the program needs to know from the user needs to be provided via @ARGV (and/or, in more sophisticated/complicated settings, by %ENV). Refactor your script so that the stuff expected from the user via STDIN comes from @ARGV (or %ENV) instead.

        If the nature of the program involves showing the user some data or processing results before asking for a decision -- such that the user won't know what directives to supply to the script until after it starts running -- you are up a creek. That is not a unix-shell-style process, and you won't really be able to use it effectively in any sort of pipeline command.

        UPDATE: Looking again at what you said, it seems that you want the user to provide a list of items of interest, you process each list item as the user supplies it, and then the user has give a special signal when they are done.

        It would actually be easier for the user -- and provide many other benefits that you will discover over time -- if you ask the them to supply the name of a file that contains the list.

        Alternatively, the user could supply a list by running some other process and piping its output to your script (e.g.  ls | grep some_pattern | cut -f1 -d. | uniq | your_script or any variety of other methods).

        Either way, all your script needs to do is:

        my @input_list = <>; chomp @input_list; for my $item ( @input_list ) { ## instead of "while (1)" ... # do what needs to be done without further directions from u +ser }
        Of course, when people are used to running the script all by itself (not in a pipeline, and without any args), changing it to this sort of approach can be disorienting: now they run it and it just sits there doing nothing (waiting for input on STDIN), and the user doesn't know what to do (they're waiting for instructions). To cure that problem, put the following as the first thing (right after "use strict;" and any other modules/pragmas you are using):
        if ( @ARGV == 0 and -t ) { die "Usage: $0 list.file or some_command | $0\n";
        The "-t" is true when STDIN is known to be the tty (expecting input from the keyboard), and is false when STDIN is tied to a pipeline. So if there's no pipeline for input and no file name given on the command line, you have to stop and tell the user how to run the command.

        I'd recommend going with the excellent suggestions from graff.

        However, if you really are stuck prompting, you may be able to use IO::Prompt. I installed it and tried it out quickly.

        use IO::Prompt; prompt 'What say you? '; print "user say '$_'\n";

        If I run this with no arguments, it prompts and outputs the line as expected. If I run it and redirect to a file, it prompts, and the output goes to the file. I believe IO::Prompt achieves this by opening the user's tty directly.

Re: How do I pipe the output of a Perl program to something else?
by cdarke (Prior) on Feb 14, 2008 at 14:39 UTC
    A few other tips. When you take input from the keyboard you probably want to output a prompt to the user. This looks a bit dumb if the user redirected the input from a file (< in the shell) or another program using a pipe (prog|yours.pl). You can test if STDIN (or any other file handle) is connected to a terminal with:
    if (-t STDIN) { # output prompt }
    This is also useful before you pipe to more(1). You can pipe to more(1) from within your program automatically if connected to a terminal:
    my $old_handle; if (-t STDOUT) { open (MORE, "| more") or die "No more $!\n"; $old_handle = select MORE; }
    At the end of the program:
    END { if (defined $old_handle) { select $old_handle; close MORE; } }