9781118013847
plays_well_with_others.html

Chapter 17. Plays well with others

WHAT YOU WILL LEARN IN THIS CHAPTER

  • Reading user input from STDIN

  • Handling command line arguments

  • Reading from other programs

  • Writing to other programs

  • Understanding STDOUT, STDERR

Up to this point, we’ve learned quite a bit about how a Perl program works, but they’ve largely been stand-alone programs. We haven’t done much work reading information from the command line, reading from or writing to other programs, or even investigated deeply how output is handled. This chapter, while not the last, kind of “wraps ups” your beginning Perl knowledge and helps to put you on the path to being a well-rounded Perl developer.

The Command Line

If your experience with computers has previously been limited to GUIs (Graphical User Interfaces), you may have trouble understanding the full power of the command line. While a pretty graphic interface can make it very easy to see how things are organized, it can make some things harder. For example, moving a file to a different directory and renaming it are two steps requiring a few clicks. From the command line, to move foo.txt to backups/mydata.txt, you could do this in one command:

mv foo.txt backups/mydata.txt

Or on in Microsoft Windows cmd shell:

move foo.txt backups\mydata.txt

Or imagine that you have a file named events containing many lines of data like this:

ovid:2009-03-12:created:admin
bob:2012-03-12:updated:user

That’s a colon-delimited list of data. Let’s say that you want to extract all of the dates from that file and count the number of times each date occurred, and write that out to a file. You might think that you should write a program to do this, but you can do this from the a standard Unix-style command line:

cut -d: -f2 < events | sort | uniq -c > events.txt

We’re not going to explain what all of that means, but suffice it to say, working with the command line, while not as “pretty” as working with graphic interfaces, can put a huge amount of power at your fingertips. Most of this book is geared towards giving you the basic skills needed to write programs, but many of those programs may need to be run from the command line, so we’ll start showing you some of the basic techniques you’ll need to understand.

Reading user input

Perhaps the most basic thing you might wish to do from a program is to read what a user types in:

use strict;
use warnings;
print "Enter your name: ";
my $name = <STDIN>;
chomp($name);
print "Hello, $name!\n";

If you run this program, it will print Enter your name: and wait for you to enter something and hit enter. Then it will print that “something”. So if you entered Ovid for your name, you would see this:

Enter your name: Ovid
Hello, Ovid!

The <STDIN> syntax looks a bit odd. You already know the angle brackets are generally wrapped around a filehandle to read from that filehandle, but what’s STDIN? We need to give a bit of a long explanation for you to understand how this work.

For Perl, and in fact, for many modern programming languages, you have one input stream of data (STDIN) and two output streams of data, one for normal output (STDOUT) and one for error output (STDERR). STDIN, STDOUT, and STDERR are all special filehandles and a stream is the sequence of data made available over time. So when you type your name in the program above, you can take your time typing each character until you finally hit enter. When you hit enter is when Perl reads the data from the STDIN and returns it, in this case by assigning it to the $name variable. The STDIN filehandle also returns the newline, so we call chomp() to remove that newline.

When you print something in Perl, by default it prints to STDOUT. You may recall that you can print something to a filehandle like this:

open my $fh, '>', $filename
  or die "Cannot open $filename for writing: $!";
print $fh "Here is a line of text\n";

What happens when you omit the filehandle? Perl uses STDOUT by default. Thus, the following two lines mean the same thing:

print "Hello, $name!'n";
print STDOUT "Hello, $name!'n";

When you warn or die, the message is instead printed to STDERR. The following two lines are almost the same thing:

print STDERR "This is a warning!\n";
warn "This is a warning!\n";

Note

We’ve said that the following two are almost the same thing:

print STDERR "This is a warning!\n";
warn "This is a warning!\n";

They’re not quite the same because you can trap warnings with signal handlers, but you cannot use a signal handler to trap data printing directly to STDERR. Signal handlers tricky (and a frequent source of obscure bugs), but you can read perldoc perlipc to learn more about them.

When data sent to STDOUT and STDERR show up on your terminal, they look the same, but because they are sent to different streams, they won’t also show up in the same order your program output them. Normally this is not a problem, but it’s something you should be aware of.

The reason STDOUT and STDERR are sent to separate streams is because you can get more control over them. From the command line, you could redirect STDERR to a log file and have STDOUT show up in the terminal, and vice-versa. We won’t really cover much more of that, but it’s part of what’s tricky about understanding data streams when you’re programming. Just remember than when you print something, it usually does what you want it to do.

Since STDIN is a stream, you can read from it multiple times:

use strict;
use warnings;
print "Enter your name: ";
my $name = <STDIN>;
chomp($name);
print "Hello, $name!\n";
print "Anything else you want to bore me with? ";
my $inane_reply = <STDIN>;
chomp($inane_reply);
print "You bored me with: $inane_reply\n";

Any time you want to read from STDIN, it’s there, waiting for you patiently, like some creepy little stalker.

Handling command-line arguments

You’ve seen several examples in this book of reading from @ARGV. @ARGV, as you know, contains arguments supplied on the command line. So you could save this as dump_args.pl:

use strict;
use warnings;
use Data::Dumper;
print Dumper(\@ARGV);

And then run it with:

perl dump_args.pl this is a list of arguments

And you should get the following output:

$VAR1 = [
          'this',
          'is',
          'a',
          'list',
          'of',
          'arguments'
        ];

Many times, however, we want to pass arguments to our program and give them specific names to make it easier for our program to understand them. For example, you may recall that in Chapter 7, Subroutines we had a program called maze.pl. In that program, you needed to specify the height and width of the maze. A common way of doing this would be as follows:

perl maze.pl --height 20 --width 40

There are many ways you could handle that, including writing your own parser, but there are plenty of modules on the CPAN to do this for you. Most of these are in the Getopt:: namespace. We’ll use Getopt::Long, by Johan Vromans, as this is one of the most popular choices:

use strict;
use warnings;
use Getopt::Long;
my ( $height, $width );
GetOptions(
    'height=i' => \$height,
    'width=i'  => \$width,
) or die "Could not parse options";
# the rest of your program here

When you use Getopt::Long, it exports a GetOptions function. This function expects an even size list of name/variable pairs. The “name” of each option is actually a specification of how to read that option from the command line. In this example, the =i in height=i says “the --height option must have an integer value”. Of course, that integer value could be negative, so you may need to do some validation after.

The variable $height gets passed as a reference. Because we’re passing by reference instead of value, the Getopt::Long module is able to modify the variable’s value directly. Thus, with the above code, passing either of the following command lines will set $height and $width to 20 and 30, respectively (because the = sign is optional):

perl maze.pl --height 20 --width 30
perl maze.pl --height=20 --width=30

What’s going on is that when you run the program, @ARGV contains all of the values passed in the command line. For the two examples above, @ARGV will contain this:

# perl maze.pl --height 20 --width 30
@ARGV = ('--height', '20', '--width', '30');
# perl maze.pl --height=20 --width=30
@ARGV = ('--height=20', '--width=30');

Getopt::Long will remove all arguments from @ARGV that it can parse, leaving the remaining arguments in @ARGV. For example, if you have a program named print_it.pl:

use strict;
use warnings;
use Getopt::Long;
my $times = 1;
GetOptions(
    'times=i' => \$times,
) or die "Could not parse options";
if ( $times < 1 ) {
   die "The --times argument must be greater than zero";
}
my $args = join '-', @ARGV;
for ( 1 .. $times ) {
    print "$args\n";
}

And you run it with:

perl print_it.pl --times 3 bob dobbs

It will print out the following:

bob-dobbs
bob-dobbs
bob-dobbs

Getopt::Long will also take the minimum unique string for each option (the single dash is optional):

perl maze.pl -h 20 -w 30

Sometimes you don’t want to assign a value to an option, you just want to know if it’s present. For example, you program might print out additional information such as the directories it’s creating, the data it has read from a configuration file, and so on. But you might only want this information displayed if you request it with a --verbose switch:

perl some_program.pl --verbose

And in your program:

use Getopt::Long;
my $is_verbose;
GetOptions(
   'verbose' => \$is_verbose,
) or die "Could not parse options";

With this, $is_verbose will have a true value if you included --verbose (or -v) on the command line, or a false value if you omitted it.

If you include =s with the option name, it expects a string:

use Getopt::Long;
my $name = "Ovid";
GetOptions(
    'name=s' => \$name,
) or die "Could not parse options";
print "Hello, $name\n";

And run that with:

perl get_name.pl --name Bob

That will print Hello, Bob. If you do not supply an argument, the $name value will default to Ovid because GetOptions will not overwrite a value if there is no corresponding value on the command line.

If you string has whitespace in it, you’ll need to quote it:

perl get_name.pl --name 'Bob Dobbs'

That’s because when you run your program with the above arguments, @ARGV would be this without the quoting:

@ARGV = ('--name', 'Bob', 'Dobbs');

And this with the quoting:

@ARGV = ('--name', 'Bob Dobbs');

When using Getopt::Long, you’ll find that standalone arguments (like our --verbose example), string arguments (--name='Bob') and integer arguments (--height and --width) are the most common types of command line arguments, but there are many more ways of handling your parsing. See perldoc Getopt::Long for more details.

perlrun

While we’re going in-depth about how to run Perl programs from the command line, it would be a bad thing to forget to mention the oft-overlooked perlrun documentation. By reading perldoc perlrun, you will learn many things about how to create useful Perl one-liners to solve thorny problems, or perhaps interesting switches that can make your life easier. For example, the -I switch tells Perl which paths to include when searching for modules to use or require. For example, we often included this line in our sample programs:

use lib 'lib/';

That line tells Perl to also search for Perl modules in the lib/ directory. However, you can use the -I switch to do this from the command line:

perl -Ilib/ some_program.pl

You can specify this more than once:

perl -Ilib/ -I../mylib/ some_program.pl

The -e switch is for executing the text that follows, instead of assuming it’s a program. That’s sometimes used with the -l switch that automatically adds a newline after every print statement. Here’s one way of finding out which version of Moose you have installed:

perl -l -e 'use Moose; print Moose->VERSION'

At the present time on my system, that prints something like:

$ perl -l -e 'use Moose; print Moose->VERSION'
2.0402
$

Note that the $ prompt is on a line by itself. Without the -l switch, you get the following output:

$ perl -l -e 'use Moose; print Moose->VERSION'
2.0402$

That may be annoying to you, so the -l switch makes it nicer to read.

If the switches don’t take arguments, you can “bundle” them together. For example, instead of perl -l -e, you can use perl -le:

perl -le 'use Moose; print Moose->VERSION'

The -n switch wraps a while (<>) { ... } loop around your program. As you might recall, the while(<>) { ... } syntax will read each successive line from filenames found in @ARGV and assigns the line values to $_. Thus, to print out all of the comments in a few programs:

perl -ne 'print if /^\s*#/' program1.pl program2.pl program3.pl

That’s more or less equivalent to this:

use strict;
use warnings;
PROGRAM: foreach my $program (@ARGV) {
    if (open my $fh, '<', $program) {
        while (<$fh>) {
            print $_ if /^\s*#/;
        }
    }
    else {
        warn "Could not open $program for reading: $!";
        next PROGRAM;
    }
}

As you can see, the command line switches documented in perlrun give you a lot of power from the command line, but they can be daunting to learn. Hit your favorite search engine and search for “perl one liners” to see many more examples. They’re very popular.

And don’t forget to dive into perldoc perlrun.

Other Programs

Most of this book has covered Perl development geared towards writing usable, standalone software. We’ve also covered quite a few CPAN modules along the way. However, sometimes you’ll find software that does exactly what you want, but it’s not written in Perl. There are many techniques to handling this, but we’re going to show a couple of the easier ones involving reading another program’s output and writing output for another program to use.

Running an external program

Perl offers a variety of ways of running an external program. We’ll show you how to handle the most common ways of doing this. Table 17.1, “Table 17-1” shows the tools we will cover.

Table 17.1. Table 17-1

Command

Usage

system

Executing a command when you only care about its success or failure

exec

Ending the Perl program and passing control to another program

backticks and qx

Running an external program and capturing its output

open

Writing to or reading from another program

Capture::Tiny

Capture STDOUT and STDERR from external programs


Some of the commands listed in Table 17.1, “Table 17-1” overlap and the choice of each depends on your needs.

The most basic way of running another program is the system() command. This builtin is useful when you want to run an external program and only care about its success or failure, not its output. For example, if you are running a Perl program that has written several megabytes of output in separate files in a directory, you might wish to compress those files into a single file and remove the directory afterwards.

Without going into too much detail, one way to compress a directory of files is to use the tar command:

tar cjf archive.tgz mydir/

That will create a file named archive.tgz, containing the contents of the mydir/ directory. You can later extract (similar to “unzip”) this archive with the following command:

tar xjf archive.tgz

Note

This chapter is frustrating to write because many of the command line tools that we care about are written for Unix/Linux or Mac OS X, but not the Windows operating systems. Fortunately, Many Unix/Linux commands will run unchanged on an OS X system. You can run many of them on Windows using the GnuWin project that is freely downloadable from http://gnuwin32.sourceforge.net/.

Another option is to the Cygwin program mentioned in Chapter 1, What’s Perl?.

So to compress a directory’s contents into a single file and then delete that directory, we can do this:

use strict;
use warnings;
use DateTime;
# concatenating a string to the end of the DateTime object triggers
# its string overloading. This example will create a string
# representation of the current date and time
my $dir = DateTime->now . "";
# lots of code to write data to the $dir directory
my @command = ('tar', 'cjf', "$dir.tgz", $dir);
system(@command) == 0
  or die "Could not '@command': $?";
@command = ('rm', '-fr', $dir);
system(@command) == 0
  or die "Could not '@command': $?";

This looks a bit strange (and to be fair, system programming often does when you’re not used to it), but let’s look at our first system command and see what’s going on.

my @command = ('tar', 'cjf', "$dir.tgz", $dir);
system(@command) == 0
  or die "Could not '@command': $?";

For this, our @command array has the command as the first element, followed by the arguments to the command as successive elements. For this example, we only put the external tar command into a separate variable to make it easier to report the command failure in the die statement.

The system() command takes a string or a list of strings and executes them. You could pack everything into a single string:

my $command = "tar cjf $dir.tgz $dir";
system($command) == 0
  or die "Could not '$command': $?";

However, putting the command and its arguments into a single string is not recommended because that will call your operating system’s shell to execute the command instead of executing the command directly. We won’t go into detail, but suffice it to say that if you call your operating system’s shell to execute the command, not only is it slower, it also can open up serious security holes. Passing a list to system is faster and, while it can still be dangerous if you don’t know what you’re doing, it’s a touch safer than using a string.

So why do we test that it returns 0? Because on most operating systems, programs have return values. If that value is 0 (zero), the program completed successfully. Thus, what Perl considers to be a false value is returned upon success! If we didn’t test for system(@command) == 0, we’d have to write this:

system(@command)
  and die "Could not '@command': $?";

While that shows up in many programs that use the system command, it’s very confusing to read.

Note

Programs generally exit with 0 if the program completed successfully. If your program dies because of a failure, it will exit with some value other than zero. If you trap an error and want to handle it and then exit with a non-zero value, just pass that value as the argument to exit:

eval { some_func() };
if ( my $error = $@ ) {
    warn $error;
    # do cleanup
    exit 1;
}

The $? variable in the string we died with is the status returned by the system() command. Usually you hope that the status is zero. If not, you know the command failed. You can get a fair amount of data out of this by applying various bitwise operators, as described in the entry for $? in perldoc perlvar:

The status returned by the last pipe close, backtick (``) command,
successful call to wait() or waitpid(), or from the system()
operator. This is just the 16-bit status word returned by the
traditional Unix wait() system call (or else is made up to look
like it). Thus, the exit value of the subprocess is really
("$? >> 8"), and "$? & 127" gives which signal, if any, the
process died from, and "$? & 128" reports whether there was a core
dump.

However, we’ll just focus on success or failure rather than trying to parse the exit codes as different programs have different exit codes. Unfortunately, they tend to be poorly documented and given that programs are often not portable between Windows and other operating systems, it’s difficult to cover them here.

Reading another program’s output

Some times you will find yourself running another program and wanting to read its output. Both backticks (``) and the qx// operator help with this (they’re the same thing but with different syntax) and they’re documented in perldoc perlop. So in a Unix system, the uptime command tells you how long the computer has been running since its last reboot or powerup:

print `uptime`;
print qx(uptime);

On my system, that currently prints:

21:42  up 21 days,  4:41, 3 users, load averages: 0.96 0.92 0.91
21:42  up 21 days,  4:41, 3 users, load averages: 0.96 0.92 0.91

Note

The qx operator is a standard “quotelike” operator, meaning that you can use many punctuation characters as delimiters:

print qx(uptime);
print qx/uptime/;
print qx'uptime';
print qx"uptime";
print qx<uptime>;

This means you can use standard variable interpolation:

my $program = 'uptime';
print qx<$program>;

If you do not want variable interpolation, be sure to use single quotes or escape the variable sigils:

print qx'$uptime';
print qx<\$uptime>;

Be very careful when interpolating variables in code that executes other programs. It’s very easy to get something wrong. For example, if you’re reading commands to execute from a file and you want to do a quick qx($command), you’ll be very disappointed if the command is rm -fr *.

If you prefer you can also use what is known as a “piped open” to read program output. Just place the pipe, |, after the program name:

open my $read_fh, "$program |"
  or die "Cannot execute '$program |': $!";
while ( my $output = <$read_fh> ) {
    print $output;
}

This can be very useful if you want to pass a function a filehandle and not care if the output is coming from a file or another program.

If you don’t want shell metacharacters to be expanded when using a piped open, use -| instead of a bare |:

open my $read_fh, "$program -|"
  or die "Cannot execute '$program -|': $!";

These methods only capture a program’s STDOUT. To capture its STDERR, you will need to redirect it:

my $stderr = qx/program 2>&1/;

Unfortunately, the 2>&1 is a bit cryptic and it’s not portable. We’ll see Capture::Tiny later in this chapter. This module provides an easy, portable solution.

Note

The various solutions we’ve presented for reading output for a program are blocking solutions. That means that they will wait until the program called exits before the output is returned to you. See perldoc perlopentut for more information. The sysread and sysopen functions can help here.

Writing to another program’s input

Sometimes instead of reading from another program, you need to write to it. You can do this by placing the pipe in front of the command:

open my $fh, "| $command"
  or die "Could not open a pipe to $command: $!";

Better still is to use the three argument form of open, with |- to prevent shell expansion:

open my $fh, "|-", $command
  or die "Could not open a pipe to $command: $!";

For example, let’s say you have a block of text and you’d like to know how many lines, words and characters it has. You could use the Unix wc utility:

use strict;
use warnings;
my $text = <<'END';
I will not be pushed, filed, stamped, indexed,
briefed, debriefed, or numbered.
END
open my $fh, '|-', 'wc' or die $!;
print $fh $text;

And that will print out:

2      12      79

That output says we have two lines, twelve words, and seventy-nine characters. The reason why this text is printed out is because wc's default output goes to STDOUT. We effectively use wc to filter our output.

Note

The wc utility is used to count words, lines and characters in a block of text. It is not native to Windows, but you can download a set of core Unix utilities for Windows from:

http://sourceforge.net/projects/gnuwin32/files/coreutils/5.3.0/coreutils-5.3.0.exe/download

This can also be useful if you want to change how your own STDOUT behaves. For example, if you want your program’s output to go through a pager such as less, you could do this (we use |- to avoid calling the shell):

my $pager = $ENV{PAGER} || '/usr/bin/less';
open STDOUT, "|-", $pager
  or die "Could not open STDOUT to $pager: $!";

Now your program’s STDOUT output will be using the pager instead of spewing lots of information that might scroll past their terminal window.

If you need to read and write at the same time, see IPC::Open2 and IPC::Open3, both of which are core modules. Or you can install IPC::Run from the CPAN. It’s much more flexible.

Note

For more information about using pipes with open, see Pipe Opens in perldoc perlopentut and also read perldoc perlipc.

The topic has generated many questions over the years and perldoc perlfaq8 has more relevant information. Unfortunately, as of this writing, the perlipc information is a touch out of date.

STDERR

Earlier we showed you how to capture an external program’s STDERR instead of its STDOUT.

my $stderr = qx/program 2>&1/;

Even if you know you’re only going to be running your program on Linux, perhaps you want to capture both STDOUT and STDERR? That’s where Capture::Tiny comes in.

use Capture::Tiny 'capture';
my ($stdout, $stderr, @result) = capture {
    print "This goes to STDOUT\n";
    warn "This goes to STDERR\n";
    return qw(These are the results);
};
print "STDOUT: $stdout";
print "STDERR: $stderr";
print "Results: @result";

Running the code snippet will output:

STDOUT: This goes to STDOUT
STDERR: This goes to STDERR
Results: These are the results

Capture::Tiny can be used with capturing STDOUT, STDERR and return values from a regular chunk of code. In the case of an external program, it works just the same. Save the following program as example_17_1_poets.pl.

use strict;
use warnings;
my @favorite_poets = (
    'Publius Ovidius Naso',
    'John Davidson',
    'Alfred, Lord Tennyson',
    'Christina Rossetti',
);
foreach my $poet (@favorite_poets) {
    print "$poet\n";
}
warn "We're done here";

Note

example_17_1_poets.pl available for download at Wrox.com.

It should be pretty clear what happens if you run that, but let’s capture the output in a different program named example_17_2_capture.pl.

use strict;
use warnings;
use Data::Dumper;
use Capture::Tiny 'capture';
my $program = "$^X example_17_1_poets.pl";
my ( $stdout, $stderr, @result ) = capture { qx"$program" };
print Dumper $stdout, $stderr, \@result;

Note

example_17_2_capture.pl available for download at Wrox.com.

Note

$^X is the name of the current Perl executable.

Running that should produce the following output:

$VAR1 = '';
$VAR2 = 'We\'re done here at example_17_1_poets.pl line 14.
';
$VAR3 = [
          'Publius Ovidius Naso
',
          'John Davidson
',
          'Alfred, Lord Tennyson
',
          'Christina Rossetti
'
        ];

In this case, $VAR1 is $stdout, $VAR2 is $stderr, and $VAR3 is the @output. We actually didn’t have any STDOUT to capture because the qx operator captured that for us and returned it as the @output of the qx command. The warning from example_17_1_poets.pl was captured and returned as the $stderr.

Capture::Tiny tries very hard to be cross-platform friendly and generally works on Windows, OS X and Unix/Linux systems. However, the author, David Golden, states that portability is a goal, not a guarantee.

Note

To know if a given module will work on your system, a good place to start is the CPAN testers matrix, http://matrix.cpantesters.org/, provided by the Perl testing community. To see Capture::Tiny’s results:

http://matrix.cpantesters.org/?dist=Capture-Tiny

Summary

In this chapter, you’ve learned some of the basics of working with programs on the command line, reading user input and handling command line arguments. You’ve also learned about perlrun, the documentation that explains many of the switches that are available to control the behavior of the Perl interpreter.

You’ve also learned about running other programs from within your program. You can read their STDOUT and STDERR, and you can send information to them, as needed.

Exercises

1. Write a program called age.pl that prompts a user for their birth date in YYYY-MM-DD format and prints their age in years. You can use the following to parse a string in that format into a DateTime object. What happens if someone enters an invalid date?

use DateTime::Format::Strptime;
my $datetime_formatter = DateTime::Format::Strptime->new(
    pattern   => '%Y-%m-%d',
    time_zone => 'GMT',
);
my $string = '1967-33-33';
my $birthdate = $datetime_formatter->parse_datetime($string);

2. Modify your program from Exercise 1 to not prompt the user if the birth date has been supplied from the command line. Instead, use the birth date supplied on the command line. If they have supplied any extra arguments, assume that they’re the person’s name:

perl age.pl --birthdate=1955-04-08 Barbara Kingsolver

Allow an --age_at parameter to allow a person to specify what day you want to calculate their age at.

perl age.pl --birthdate 1964-10-18 --age_at 2007-10-02 Charles Stross

3. In Chapter 14, Testing we learned about writing tests. Use qx and Capture::Tiny to write some tests for age.pl from Exercise 2. Use the following to verify your program:

perl age.pl --birthdate 1964-10-18 --age_at 2007-10-02 Charles Stross
perl age.pl --birthday 1967-06-20
perl age.pl Ovid

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC

DESCRIPTION

STDIN

The filehandle that user input is read from

@ARGV

The built-in array containing command line arguments.

Getopt::Long

A standard module used to parse command line options.

Perlrun

The documentation explaining standard Perl command line switches.

exec

Used to terminate the current program and pass control to a new one.

system

Used to execute another program when you don’t care about its output.

Backticks and qx

Use to execute another program and capture its STDOUT.

Piped opens

Used to read and write to external programs.

Capture::Tiny

Used to capture the STDOUT, STDERR and output from a subroutine.

Answers to exercises

1. Write a program called age.pl that prompts a user for their birth date in YYYY-MM-DD format and prints their age in years. You can use the following to parse a string in that format into a DateTime object. What happens if someone enters an invalid date?

You will want to read the person’s birthday from STDIN. Then, you use DateTime::Format::Strptime to parse that date into a DateTime object. Then, you can subtract that date from DateTime->now to get a DateTime::Duration object and call the years() method on it to extract the number of years since the birthday.

use strict;
use warnings;
use DateTime;
use DateTime::Format::Strptime;
my $datetime_formatter = DateTime::Format::Strptime->new(
    pattern   => '%Y-%m-%d',
    time_zone => 'GMT',
);
print "Enter your birthday in YYYY-MM-DD format: ";
my $birthday = <STDIN>;
chomp($birthday);
my $birthday_date = $datetime_formatter->parse_datetime($birthday)
  or die "Could not parse birthday: $birthday";
my $duration = DateTime->now - $birthday_date;
printf "You are %d years old\n" => $duration->years;

2. Modify your program from Exercise 1 to not prompt the user if the birth date has been supplied from the command line. Instead, use the birth date supplied on the command line. If they have supplied any extra arguments, assume that they’re the person’s name:

perl age.pl --birthdate=1955-04-08 Barbara Kingsolver

Allow an --age_at parameter to allow a person to specify what day you want to calculate their age at.

perl age.pl --birthdate 1964-10-18 --age_at 2007-10-02 Charles Stross

We use Getopt::Long with --birthdate and --age_at command line switches. If --birthdate is not supplied, we will prompt the user from the command line. If --age_at is not supplied, we assume today as the end date. If the end date is before the starting date, we’ll die with a useful error message.

use strict;
use warnings;
use DateTime;
use Getopt::Long;
my ( $birthdate, $age_at );
GetOptions(
    'birthdate=s' => \$birthdate,
    'age_at=s'   => \$age_at,
) or die "Could not parse options";
my $name = join " " => @ARGV;
use DateTime::Format::Strptime;
my $datetime_formatter = DateTime::Format::Strptime->new(
    pattern   => '%Y-%m-%d',
    time_zone => 'GMT',
);
unless ($birthdate) {
    print "Enter your birthday in YYYY-MM-DD format: ";
    $birthdate = <STDIN>;
    chomp($birthdate);
}
my $birthday_date = $datetime_formatter->parse_datetime($birthdate)
  or die "Could not parse birthday: $birthdate";
my $end_date = DateTime->now;
if ($age_at) {    # overwrite $end_date if we have $age_at
    $end_date = $datetime_formatter->parse_datetime($age_at)
      or die "Could not parse birthday: $age_at";
}
if ( $end_date < $birthday_date ) {
    die "End date must be on or after the birthday";
}
my $duration = $end_date - $birthday_date;
if ($name) {
    printf "$name is %d years old\n" => $duration->years;
}
else {
    printf "You are %d years old\n" => $duration->years;
}

3. In Chapter 14, Testing we learned about writing tests. Use qx and Capture::Tiny to write some tests for age.pl from Exercise 2. Use the following to verify your program:

perl age.pl --birthdate 1964-10-18 --age_at 2007-10-02 Charles Stross
perl age.pl --birthday 1967-06-20
perl age.pl Ovid

This one is a bit trickier and shows us that Capture::Tiny does the right thing, but it might be a bit hard to figure out at first. First, here’s one way to write those tests:

use strict;
use warnings;
use Test::More;
use DateTime;
use Capture::Tiny 'capture';
my ( $stdout, $stderr, @output ) = capture {
    qx/perl age.pl --birthdate 1964-10-18 --age_at 2007-10-02 Charles Stross/;
};
is $output[0], "Charles Stross is 42 years old\n",
  'Charles Stross was 42 years old when he wrote Halting State';
( $stdout, $stderr, @output ) = capture {
    qx/perl age.pl --birthday 1967-06-20/;
};
like $stderr, qr/Unknown option: birthday/,
  'Passing an unknown option should cause the program to fail';
( $stdout, $stderr, @output ) = capture {
    open my $fh, '|-', 'perl age.pl Ovid';
    print $fh '1967-06-20';
};
like $stdout, qr/Enter your birthday in YYYY-MM-DD format:/,
  'Not entering a birthdate should prompt for our birthday';
my $today    = DateTime->now;
my $birthday = DateTime->new(
    year  => 1967,
    month => 6,
    day   => 20,
);
my $age = ( $today - $birthday )->years;
like $stdout, qr/Ovid is $age years old/,
  '... and the program should still tell use the correct age';
diag $stdout;
done_testing;

In our first test:

my ( $stdout, $stderr, @output ) = capture {
    qx/perl age.pl --birthdate 1964-10-18 --age_at 2007-10-02 Charles Stross/;
};
is $output[0], "Charles Stross is 42 years old\n",
  'Charles Stross was 42 years old when he wrote Halting State';

Because qx returns the program’s STDOUT, it populates the @output argument in our return values. In this example, our $stdout is always empty and our $stderr will remain empty if we have no errors.

Our second test shows that $stderr is gets a value when we pass a bad option:

( $stdout, $stderr, @output ) = capture {
    qx/perl age.pl --birthday 1967-06-20/;
};
like $stderr, qr/Unknown option: birthday/,
  'Passing an unknown option should cause the program to fail';

Our third test is the most interesting:

( $stdout, $stderr, @output ) = capture {
    open my $fh, '|-', 'perl age.pl Ovid';
    print $fh '1967-06-20';
};
like $stdout, qr/Enter your birthday in YYYY-MM-DD format:/,
  'Not entering a birthdate should prompt for our birthday';
my $today    = DateTime->now;
my $birthday = DateTime->new(
    year  => 1967,
    month => 6,
    day   => 20,
);
my $age = ( $today - $birthday )->years;
like $stdout, qr/Ovid is $age years old/,
  '... and the program should still tell use the correct age';

We used a piped open instead of the qx operator because we had to send some data to the program. We then construct our own DateTime object for today to make sure that we always have the correct age in years in our test.

The entire test output should look similar to this:

age.t ..
ok 1 - Charles Stross was 42 years old when he wrote Halting State
ok 2 - Passing an unknown option should cause the program to fail
ok 3 - Not entering a birthdate should prompt for our birthday
ok 4 - ... and the program should still tell use the correct age
1..4
# Enter your birthday in YYYY-MM-DD format: Ovid is 44 years old
ok
All tests successful.
Files=1, Tests=4,  0 wallclock secs
Result: PASS

You’ll note this diagnostic in the test output:

# Enter your birthday in YYYY-MM-DD format: Ovid is 44 years old

That comes from this line of code:

diag $stdout;

Though you could determine this by reading our tests carefully, the diag() statement makes it clear that Capture::Tiny is going to return all of the STDOUT into a single variable, but we see it’s on a single line without newlines. Why? If you run the age.pl program from the command line, you might see output like this:

$ perl age.pl Ovid
Enter your birthday in YYYY-MM-DD format: 1967-06-20
Ovid is 44 years old

So why doesn’t that show up in two lines in our $stdout variable?

The newline that you might expect after you enter your birthday in YYYY-MM-DD format isn’t present because that was actually read from the program’s STDIN! Just because you can see it on the console when you ran the program from the command line doesn’t mean that it’s coming from that program’s STDOUT. This behavior might seem confusing, but once you think about it, it’s quite clear.

Site last updated on: July 5, 2012 at 11:41:08 AM PDT
Cover for Beginning Perl (Wrox)

View 2 comments

  1. chrisjack1 – Posted June 28, 2012

    s/a Perl program/perl programs/

  2. Curtis Poe – Posted July 6, 2012

    Fixed. Thanks!

Add a comment

View 2 comments

  1. chrisjack1 – Posted June 28, 2012

    s/you/your/

  2. Curtis Poe – Posted July 6, 2012

    Fixed. Thanks!

Add a comment

View 2 comments

  1. chrisjack1 – Posted June 28, 2012

    I would suggest mentioning Capture::Tiny is probably not appropriate to use when output is likely to be huge.

  2. Curtis Poe – Posted July 6, 2012

    Added. Thanks.

Add a comment