Perl Part II · Module 28 · Legacy Perl and Python Migration
Reading a 2000-Line Legacy Perl Utility
Sooner or later every engineer on a chip team is handed a Perl script nobody else understands. It may be a timing summariser, a regression driver or a filelist generator. It has run every night for years, its author has left, and now it needs a change or a replacement. Reading it from line 1 to line 2,000 is the slowest way in. This chapter teaches a faster order: map the file without running it, then read the argument handling, the main loop, the outputs and the exit calls, in that order. Write what you learn down as predictions, and run the script once to find out which of your predictions were wrong.
Advanced29 min readPerlPart IIEDA Flows
Part II · Module 28 · Chapter 28.1 · Reading a 2000-Line Legacy Perl Utility
1. The Job: Four Questions in One Hour
Module 28 is about the most common Perl task on a modern semiconductor team. That task is not writing new Perl. It is dealing with old Perl: understanding a script someone else wrote, deciding whether to migrate it, and if so, migrating it without breaking the flow that depends on it. Every one of those steps starts with reading.
The goal of a first read is not to understand every line. It is to answer four questions well enough to act:
- Inputs. What does the script read: arguments, files, directories, environment variables, other programs' output?
- Outputs. What does it produce: text on STDOUT, messages on STDERR, files, and where those files land?
- Decisions. Where does it make a judgement, such as a pass/fail verdict, a threshold or a filter?
- Failure behaviour. What does it do when an input is missing, empty or malformed, and what exit status does the caller see?
Those four answers are what every later chapter in this module needs. Chapter 28.2 turns them into a written inventory. Chapter 28.4 uses them to pick a strategy. Chapter 28.8 uses them to decide what a side-by-side comparison has to cover.
Why reading from the top fails. A Perl file is not laid out in execution order. Subroutines can sit above or below the code that calls them. Documentation (POD) can fill hundreds of lines. Large blocks of text that the script prints can look exactly like code. You can spend forty minutes in a formatting subroutine before you find out which arguments the script accepts. So the method is: map first, then read the parts that answer the four questions, in a fixed order.
Figure 28.1.1 — The reading order
2. The Script for This Module
This module follows one script through the whole migration. sta_summary.pl reads a directory of per-corner timing reports and prints one summary line per corner. It is a sample written for this module, not a real artefact, but it is written the way scripts of its age usually are: no use strict, no use warnings, global variables, bareword filehandles, and an open ... || next that skips a file without a word. It is short, so that every behaviour in it can be shown on one page; the method is the same for one that is forty times longer, and section 4 applies it to one.
#!/usr/bin/perl
# sta_summary.pl - summarise per-corner timing reports
# usage: sta_summary.pl [-v] [report_dir]
# prints one line per corner: paths, violations, WNS and TNS
#
# (A SAMPLE legacy script written for this module, in the style of
# scripts that have run unchanged in a flow for a decade.)
use Getopt::Long;
$verbose = 0;
GetOptions("v" => \$verbose);
$dir = shift @ARGV;
$dir = "." unless $dir;
opendir(D, $dir) || die "cannot open $dir: $!\n";
@files = grep(/\.rpt$/, readdir(D));
closedir(D);
if (!@files) {
print "No reports found in $dir\n";
exit 0;
}
foreach $f (@files) {
($corner = $f) =~ s/\.rpt$//;
open(F, "$dir/$f") || next;
while (<F>) {
if (/^Endpoint\s*:\s*(\S+)/) {
$ep = $1;
}
if (/slack \((\w+)\)\s+(\S+)/) {
$paths{$corner}++;
if ($1 eq "VIOLATED") {
$viol{$corner}++;
$tns{$corner} += $2;
push(@{$eps{$corner}}, $ep);
}
$wns{$corner} = $2 if $2 < $wns{$corner};
}
}
close(F);
}
open(CSV, ">tmp.csv");
$bad = 0;
foreach $c (sort keys %paths) {
printf("%-16s paths=%-3d viol=%-3d wns=%.3f tns=%.3f\n",
$c, $paths{$c}, $viol{$c}, $wns{$c}, $tns{$c});
print CSV "$c,$viol{$c},$wns{$c}\n";
if ($verbose) {
foreach $e (@{$eps{$c}}) { print " $e\n"; }
}
$bad++ if $viol{$c};
}
close(CSV);
exit $bad;Its inputs are two reports in the synthetic teaching format this Part has used since Module 26: one block per path, an Endpoint line, and a slack line marked MET or VIOLATED.
Report : timing (synthetic teaching format)
Design : chip_top
Corner : ss_0p72v_125c
Startpoint: u_core/r_reg[3]
Endpoint : u_core/acc_reg[7]
slack (VIOLATED) -0.043
Startpoint: u_core/r_reg[1]
Endpoint : u_core/acc_reg[2]
slack (VIOLATED) -0.011
Startpoint: u_core/cnt_reg[0]
Endpoint : u_core/cnt_reg[5]
slack (MET) 0.120
End of reportReport : timing (synthetic teaching format)
Design : chip_top
Corner : tt_0p80v_25c
Startpoint: u_core/r_reg[3]
Endpoint : u_core/acc_reg[7]
slack (MET) 0.087
Startpoint: u_core/cnt_reg[0]
Endpoint : u_core/cnt_reg[5]
slack (MET) 0.240
End of reportResist the urge to run it yet. Running a script you do not understand is how a tmp.csv in somebody's working directory gets overwritten. Map it first.
3. Map Before You Read
survey.pl reads a Perl file as text and prints where to look. It never executes the script it surveys, so it is safe to point at anything. It classifies every line as code, POD, comment or blank, then lists the lines that matter for the four questions under six headings:
- ARGUMENTS: where options and positional arguments are read.
- INPUTS: files and directories opened for reading.
- OUTPUTS: files opened for writing.
- EXTERNAL: other programs and environment variables.
- EXITS: every
exitanddie. - SUBS: every subroutine definition.
use strict;
use warnings;
# survey.pl - a first-hour map of an unfamiliar Perl script.
# It reads the source as TEXT and never runs it. Every entry is a
# lexical match, so treat the output as a list of places to read,
# not as proof of what the script does.
my $counts_only = @ARGV && $ARGV[0] eq '--counts' ? shift : 0;
my $file = shift or die "usage: survey.pl [--counts] script\n";
open my $fh, '<', $file or die "cannot read $file: $!\n";
my @src = <$fh>;
close $fh;
my (%n, %env, %sect, $in_pod, $first_code, $after_end, $heredoc);
my @order = qw(ARGUMENTS INPUTS OUTPUTS EXTERNAL EXITS SUBS);
for my $i (0 .. $#src) {
my ($line, $ln) = ($src[$i], $i + 1);
if (defined $heredoc) { # text inside a here-document is DATA:
$n{heredoc}++; # an __END__ or =head1 in it is not real
undef $heredoc if $line =~ /^\s*\Q$heredoc\E\s*$/;
next;
}
if ($after_end) { $n{after_end}++; next }
if ($line =~ /^__(?:END|DATA)__\b/) { $after_end = 1; $n{after_end}++; next }
if ($line =~ /^=(\w+)/) { $in_pod = $1 ne 'cut'; $n{pod}++; next }
if ($in_pod) { $n{pod}++; next }
if ($line =~ /^\s*$/) { $n{blank}++; next }
if ($line =~ /^\s*#/) { $n{comment}++; next }
$n{code}++;
$first_code //= $ln;
(my $c = $line) =~ s/\s+#[^'"]*$//; # crude: drop a trailing comment
chomp $c;
my $at = sprintf "%5d %s", $ln, $c =~ s/^\s+//r;
push @{ $sect{ARGUMENTS} }, $at if $c =~ /\bGetOptions\b|\bgetopts?\b|\@ARGV|\bshift\s*(?:\(\s*\))?\s*;/;
push @{ $sect{INPUTS} }, $at if $c =~ /\bopen(?:dir)?\b/ && $c !~ /["']\s*>|,\s*['"]\+?>/;
push @{ $sect{OUTPUTS} }, $at if $c =~ /\bopen\b[^;]*(?:["']\s*>|,\s*['"]\+?>)/;
push @{ $sect{EXTERNAL} }, $at if $c =~ /\bsystem\b|\bexec\b|`|\bqx\b|\$ENV\{/;
push @{ $sect{EXITS} }, $at if $c =~ /\bexit\b|\bdie\b/;
push @{ $sect{SUBS} }, $at if $c =~ /^\s*sub\s+\w+/;
$env{$1}++ while $c =~ /\$ENV\{\s*['"]?(\w+)/g;
$heredoc = $1 // $2 if $c =~ /<<~?\s*(?:["'](\w+)["']|([A-Za-z_]\w*))/; # first one per line only
}
my $text = join '', @src;
my %pragma = (
strict => $text =~ /^\s*use\s+strict\b/m ? 'yes' : 'NO',
warnings => ($text =~ /^\s*use\s+warnings\b/m || $src[0] =~ /^#!.*\s-\w*w/) ? 'yes' : 'NO',
);
printf "%s: %d lines = %d code, %d here-doc text, %d comment, %d blank, %d POD, %d after __END__\n",
$file, scalar @src, map { $n{$_} // 0 } qw(code heredoc comment blank pod after_end);
printf "first line of code: %d use strict: %s warnings: %s\n",
$first_code // 0, $pragma{strict}, $pragma{warnings};
printf "%-10s %d\n", $_, scalar @{ $sect{$_} // [] } for @order;
printf "ENV read: %s\n", %env ? join(', ', sort keys %env) : '(none)';
exit 0 if $counts_only;
for my $s (@order) {
next unless $sect{$s};
print "\n-- $s\n";
print "$_\n" for @{ $sect{$s} };
}A few lines deserve a closer look.
$in_pod = $1 ne 'cut'. POD starts at any line beginning with=wordand ends at=cut. One comparison handles both: entering POD sets the flag true, and=cutsets it false.- The INPUTS and OUTPUTS patterns separate an
openthat writes (a>in the mode or filename) from one that reads. That is a heuristic. A two-argumentopen(F, $name)where$namehappens to start with>would be misfiled. The comment at the top says so: these are places to read, not facts. - The here-document handling (the
$heredoclines) is there because the first version of this tool did not have it, and a real script fooled it. Section 4 shows how.
Here is the map of the sample script:
#!/bin/bash
perl survey.pl sta_summary.plsta_summary.pl: 59 lines = 44 code, 0 here-doc text, 7 comment, 8 blank, 0 POD, 0 after __END__
first line of code: 9 use strict: NO warnings: NO
ARGUMENTS 2
INPUTS 2
OUTPUTS 1
EXTERNAL 0
EXITS 3
SUBS 0
ENV read: (none)
-- ARGUMENTS
12 GetOptions("v" => \$verbose);
14 $dir = shift @ARGV;
-- INPUTS
17 opendir(D, $dir) || die "cannot open $dir: $!\n";
28 open(F, "$dir/$f") || next;
-- OUTPUTS
46 open(CSV, ">tmp.csv");
-- EXITS
17 opendir(D, $dir) || die "cannot open $dir: $!\n";
23 exit 0;
59 exit $bad;In under a second you have nine line numbers that matter. There are no subroutines, so the whole script is the main program. It reads no environment variables and runs no external commands; Chapter 16.2 measured why those two would make it far harder to replace. It writes one file, tmp.csv, with a relative path, so the file lands in whatever directory the caller happens to be in, not next to the reports. And it has three ways to stop. Those nine lines are your reading list. Everything else is detail you can come back to.
4. The Map Is Fooled Once: A Real 2,207-Line Script
A sample script proves little about a method meant for big files. So here is a real one. h2xs is a utility that ships with Perl itself; it generates the skeleton of a Perl extension module. On this machine (Perl 5.34.1 on macOS) it lives at /usr/bin/h2xs and is 2,207 lines long, a fair stand-in for the inherited flow script in the chapter title.
The first version of survey.pl did not track here-documents. This is what it printed for h2xs:
/usr/bin/h2xs: 2207 lines = 617 code, 45 comment, 93 blank, 456 POD, 996 after __END__Nine hundred and ninety-six lines after __END__. In Perl, __END__ on a line of its own ends the program; nothing below it runs. If that number were true, almost half of the file would be dead text, and a reader trusting the map would skip it. That would mean skipping the part that writes all the output files.
The number is wrong, and the reason is worth knowing. Here are lines 1209 to 1213 of h2xs:
print PM <<"END";
1;
__END__
ENDThe __END__ on line 1212 is inside a here-document. It is text that h2xs prints into the .pm file it generates. Perl's parser knows that, because it reads the here-document as a string. A line-by-line text scan does not. The fix is the $heredoc logic now in survey.pl: after a line that opens a here-document, every line up to the terminator counts as text, not code. With it:
#!/bin/bash
# h2xs ships with Perl; this is Perl 5.34.1's copy on macOS. Another Perl
# version ships a different h2xs, so your numbers will differ.
if [ -r /usr/bin/h2xs ]; then
perl survey.pl --counts /usr/bin/h2xs
else
echo "no /usr/bin/h2xs here; try: perl survey.pl --counts \$(which h2xs)"
fi/usr/bin/h2xs: 2207 lines = 941 code, 579 here-doc text, 64 comment, 167 blank, 456 POD, 0 after __END__
first line of code: 2 use strict: yes warnings: yes
ARGUMENTS 9
INPUTS 4
OUTPUTS 8
EXTERNAL 4
EXITS 25
SUBS 11
ENV read: (none)Three lessons come out of that measurement, and all of them apply to any inherited script.
Less than half of a 2,207-line file is code. 941 lines are code. The rest is documentation (456 lines of POD), text the script prints (579 lines of here-documents), comments and blank lines. The "2000-line script" you were handed may be a 900-line script, and knowing that changes how long the job looks.
In a generator, the here-documents are the output specification. h2xs writes files, so its 579 lines of here-document text are a near-literal picture of what it produces. For a report generator in a flow, the printf formats play the same role. Read them as the outputs section of the four questions.
A map that surprises you is a map to check, not a fact to act on. "996 lines after __END__" was surprising. Opening line 1212 took ten seconds and showed the tool was wrong. Every number survey.pl prints is a lexical guess. The comment at the top of the tool says that, and this is the measurement behind the comment.
5. Step 1: Argument Handling
Go to the ARGUMENTS lines first, because they define the script's inputs.
$verbose = 0;
GetOptions("v" => \$verbose);
$dir = shift @ARGV;
$dir = "." unless $dir;Read each line with the four questions in mind.
- One option,
-v. It is a flag (no=s), so it takes no value. Chapter 15.2 covers the specification syntax. GetOptions's return value is ignored.GetOptionsreturns false when it meets an option it does not know. This script never checks, so an unknown option is reported and then ignored: the run continues. That is a failure behaviour, and it goes in your notes.- One positional argument, the directory. It is optional; with none, the script reads the current directory.
$dir = "." unless $dirreplaces the argument when it is false, not when it is missing. Chapter 2.8 lists Perl's false values, and the string"0"is one of them. So a directory literally named0is silently replaced by.. Nobody names a report directory0on purpose, but a generated name like a seed or a run index can be exactly that. Note it as a prediction to test.- No usage message and no validation. The script never checks that
$diris a directory. It letsopendirfind out.
That is ten minutes of reading at most, and the inputs question is answered: one flag, one optional directory, nothing from the environment.
6. Step 2: The Main Loop
The main loop is where the decisions live. Here it is lines 26 to 44: for every .rpt file, read it line by line and collect four per-corner numbers.
Read it slowly, and treat every global as something that might outlive the place you first see it. Without use strict (Chapter 1.10), every variable here is a package global unless declared otherwise, and nothing is declared. Three facts come out.
The corner name comes from the filename, not from the Corner : line inside the report. Rename a file and the corner changes.
open(F, "$dir/$f") || next skips an unreadable report silently. No message is printed and the exit status is not affected. Chapter 13.5 is about exactly this pattern. For your notes: a corner can vanish from the summary without anyone being told.
$ep is never reset. It is set by an Endpoint line and read by the next VIOLATED line. Because it is a global, a value from one file survives into the next. In well-formed reports every slack line follows its own endpoint, so this is harmless today. It goes in the notes as an assumption the code makes about its input.
One line deserves more than reading:
$wns{$corner} = $2 if $2 < $wns{$corner};When a line is dense, let Perl tell you how it parses it. B::Deparse is a core module that turns the compiled program back into source, and its -p option adds every parenthesis:
#!/bin/bash
perl -MO=Deparse,-p -e '$wns{$corner} = $2 if $2 < $wns{$corner};' 2>/dev/null(($2 < $wns{$corner}) and ($wns{$corner} = $2));So the line means: compare first, assign only if smaller. Now ask what $wns{$corner} is the first time a corner is seen. It has never been assigned, so it is undef (Chapter 2.7), and undef compares as 0. For a corner whose slacks are all positive, 0.087 < 0 is false, and so is 0.240 < 0. The assignment never happens. The worst slack of a corner that meets timing is never recorded. That is a decision the code makes, and almost certainly not the one its author intended. You found it by reading.
You can get Perl to point at the same spot without editing the file. Running the script under perl -w turns on warnings for that one run (Chapter 1.11):
#!/bin/bash
# -w on the command line enables warnings for this run only; the file is untouched.
work=$(mktemp -d)
cp -R sta_summary.pl reports "$work"/
cd "$work" || exit 1
perl -w sta_summary.pl reports 2>&1 >/dev/null | sed 's/, <F> line [0-9]*//' | sort | uniq -c 3 Use of uninitialized value $viol{"tt_0p80v_25c"} in printf at sta_summary.pl line 50.
2 Use of uninitialized value in concatenation (.) or string at sta_summary.pl line 51.
3 Use of uninitialized value in numeric lt (<) at sta_summary.pl line 40.Eight warnings, from three lines. Line 40 is the $wns comparison you just reasoned about, and its count tells the same story. It warns once for the violating corner, on that corner's first slack line, because the comparison then stores a value and the variable is defined from then on. It warns twice for the clean corner, once per slack line, because its value is never stored at all. Lines 50 and 51 are the output statements using values that were never set for the clean corner. Chapter 14.7 decodes these messages. Here they are a free list of the lines worth a second look. The sed strips the <F> line N suffix so that the counts do not depend on the order in which readdir returns the files.
One detail in that output is a trap. All three warnings at line 50 name $viol{"tt_0p80v_25c"}, but only one of the three undefined values is $viol; the other two are $wns and $tns. When a single printf has several undefined arguments, Perl names the first one in every warning. That was measured separately with three undefined hash elements: three warnings, all naming the first. Trust the line number a warning gives you, and treat the variable name as a hint.
7. Step 3: Outputs and the Verdict
The OUTPUTS line from the map was line 46. Read from there to the end:
- STDOUT: one
printfline per corner, insort keys %pathsorder. That is a string sort of corner names (Chapter 5.9), which is fine for these names but would putss_10beforess_9. tmp.csv: written with a relative path, so it lands in the caller's working directory. It is overwritten every run. Each row iscorner,violations,wns, written withprint, notprintf.- The verdict:
$badcounts corners with at least one violation. -v: after a corner's line, list its violating endpoints in the order they appeared in the file.
The difference between printf and print matters, and it is easy to read straight past it. Section 9 shows the result.
8. Step 4: Exits and Edges
The map listed three exits. Each is a sentence in the failure-behaviour notes.
| Line | Statement | When | What the caller sees |
|---|---|---|---|
| 17 | die "cannot open $dir: $!\n" | the directory cannot be opened | message on STDERR; exit status from $! (Chapter 15.4), not 255 |
| 23 | exit 0 | no .rpt files in the directory | message on STDOUT; status 0; tmp.csv never opened |
| 59 | exit $bad | normal end | status = number of violating corners |
Two of those rows contain the kind of fact that sinks a migration if nobody writes it down.
An empty directory exits 0, and leaves tmp.csv alone. The early exit happens before line 46, so the previous run's tmp.csv survives untouched. Anything reading tmp.csv after an empty run reads the last run's numbers.
The exit status is a count, not a flag. A caller testing for zero sees "pass" or "fail" correctly. A caller that compares the status with 1 is wrong whenever two corners fail. Chapter 15.4 measured what happens at 256.
9. The Reading Record, and Testing It
Write down what you learned before running anything. The record below took a few minutes. Every line in it is a prediction, and every prediction carries the line number it came from, so that a wrong one can be traced to a misreading.
INPUTS
-v flag; optional directory (default ".") l.12-15
a directory named "0" is replaced by "." l.15
every *.rpt in the directory; corner name = file name l.18,27
OUTPUTS
STDOUT: one line per corner, sorted by name l.48-50
tmp.csv in the CURRENT directory, overwritten every run l.46
tmp.csv: one row per corner with its violation count and WNS l.51
DECISIONS
a VIOLATED slack counts toward viol and tns l.35-38
WNS is recorded only when below 0: a clean corner shows 0.000 l.40
FAILURE BEHAVIOUR
unknown option: warned, then ignored l.12
unreadable report: skipped silently l.28
missing directory: STDERR, exit = errno l.17
empty directory: STDOUT message, exit 0, tmp.csv untouched l.21-24
exit status = number of violating corners l.55,59Now, and only now, run it once, in a scratch directory, against inputs chosen to test the record:
#!/bin/bash
# Test the reading record. Everything runs in a scratch copy.
work=$(mktemp -d)
cp -R sta_summary.pl reports "$work"/
cd "$work" || exit 1
echo "== normal run with -v"
perl sta_summary.pl -v reports; echo "exit=$?"
echo "== tmp.csv"
cat tmp.csv
echo "== unknown option"
perl sta_summary.pl -x reports 2>&1 >/dev/null; echo "exit=$?" # STDERR shown, STDOUT discarded
echo "== a directory named 0"
mkdir 0 && cp reports/*.rpt 0/
perl sta_summary.pl 0; echo "exit=$?"
echo "== missing directory"
perl sta_summary.pl nosuch 2>&1; echo "exit=$?"
echo "== empty directory: does the old tmp.csv survive?"
mkdir empty
perl sta_summary.pl empty; echo "exit=$?"
cat tmp.csv== normal run with -v
ss_0p72v_125c paths=3 viol=2 wns=-0.043 tns=-0.054
u_core/acc_reg[7]
u_core/acc_reg[2]
tt_0p80v_25c paths=2 viol=0 wns=0.000 tns=0.000
exit=1
== tmp.csv
ss_0p72v_125c,2,-0.043
tt_0p80v_25c,,
== unknown option
Unknown option: x
exit=1
== a directory named 0
No reports found in .
exit=0
== missing directory
cannot open nosuch: No such file or directory
exit=2
== empty directory: does the old tmp.csv survive?
No reports found in empty
exit=0
ss_0p72v_125c,2,-0.043
tt_0p80v_25c,,Go through it line by line against the record.
- Confirmed: the summary format and order, the 0.000 WNS for the clean corner, the unknown option warned about and ignored (the run still exits with its normal status of 1), the
0directory replaced by., the missing directory exiting 2 (the errno for "No such file or directory", exactly as Chapter 15.4 predicts), and the empty directory exiting 0 and leaving the previoustmp.csvin place. - Wrong: "
tmp.csv: one row per corner with its violation count and WNS." The row for the clean corner istt_0p80v_25c,,with both fields empty. The record was sloppy. The summary line printsviol=0andwns=0.000becauseprintfformats an undefined value as zero. The CSV line usesprint, andprintwrites an undefined value as nothing at all. The same missing numbers come out as zeros in one output and as empty fields in the other.
That second point is the reason to write predictions down. A reading that was "roughly right" would have missed it. A downstream script that reads tmp.csv and treats an empty field as "no data" will report the clean corner differently from the person reading the summary. Correct the record:
tmp.csv: viol and WNS fields are EMPTY for a corner with no violations l.51The four questions are now answered, with line numbers, and every answer has either been confirmed by a run or corrected by one. That is the output of the first hour, and Chapter 28.2 builds on it.
10. Mistakes That Waste the Hour
Reading top to bottom. On h2xs that means 456 lines of POD before the first option is parsed at line 615. Map first.
Trusting the comment. The usage comment says the script "prints one line per corner: paths, violations, WNS and TNS". For a clean corner the WNS it prints is not the worst slack at all. Comments record what the author meant at some point in the past. The code records what runs today.
Fixing while reading. It is tempting to add my and use strict as you go. Don't. Every edit is a behaviour change you have not recorded yet, and the next two chapters need the behaviour as it is. Fixes come after the inventory, on purpose, one at a time.
Running it before reading the outputs. This script overwrites tmp.csv in the current directory. Others delete scratch areas, submit farm jobs or mail people. Read the OUTPUTS and EXTERNAL sections of the map before the first run, and make the first run in a scratch copy.
Treating a lexical map as truth. The first version of the survey misjudged half of a real file. Use the map to decide where to read, and check any number that surprises you.
11. Exercises
1. Survey a real script. Run survey.pl on an inherited Perl script from your own flow (if you have none, use $(which prove) or $(which cpan), both shipped with Perl). Before opening the file, write down from the map alone what you expect its inputs and outputs to be. Then read the ARGUMENTS and OUTPUTS lines and score your guesses.
2. Break the survey. Find or write a five-line Perl script for which survey.pl gives a wrong answer in one of its six sections. (Hints: a two-argument open whose filename comes from a variable; two here-documents opened on the same line; exit inside a string.) Explain which heuristic failed and whether fixing it is worth the complexity.
3. The stale endpoint. Write a report file in which a slack (VIOLATED) line appears before any Endpoint line. Predict what -v prints for it when that report is read first, and when it is read second. Run both orders. (You can force the order by putting the reports in separate runs, since readdir order is not under your control.)
4. Find the WNS fix's blast radius. Suppose someone "fixes" line 40 so that a clean corner reports its true worst slack (0.087). List every output of the script that changes as a result, for the sample reports. Which of those changes could break a caller? (Chapter 28.2 asks this question systematically.)
5. An hour on something big. Take a script of more than 1,000 lines and spend exactly one hour on it with this chapter's order. Produce a reading record in the format of section 9, then test at least three of its predictions with runs in a scratch directory. Report how many were wrong, and what you misread.
12. What To Remember
- The first hour answers four questions: inputs, outputs, decisions and failure behaviour.
- Do not read top to bottom. Map the file, then read the arguments, the main loop, the outputs and the exits, in that order.
survey.plmaps a script as text without running it. On the sample it produced nine line numbers that mattered.- Measured on a real 2,207-line Perl program (
h2xs): only 941 lines are code. The rest is POD, here-document text, comments and blank lines. - The first survey reported 996 lines after
__END__. That__END__was inside a here-document. A lexical map is a guide to where to read, and a surprising number is a reason to check. - In a generator, the here-documents and formats are the output specification.
B::Deparse -pshows how Perl parses a dense line.perl -w scriptturns on warnings for one run without editing the file.undefcompares as 0: in the sample, a corner that meets timing never records its worst slack.$x = "." unless $xreplaces a value that is false, so a directory named0became..- An ignored
GetOptionsreturn value means an unknown option is reported and then ignored. - The same undefined value printed as
0.000in one output and as an empty field in the other, becauseprintfandprinttreat it differently. - Exit paths are part of the interface: here, errno 2 for a missing directory, 0 with yesterday's
tmp.csvfor an empty one, and a count of failing corners otherwise. - Write the reading down as predictions with line numbers, then run once in a scratch directory. One of this chapter's predictions was wrong, and the run is what found it.
Chapter 28.2 takes the corrected reading record and turns it into something a migration can be held to: a written inventory of everything the script actually does, built from probes rather than reading, including the behaviours nobody would choose on purpose.
Where this fits
Part of the Perl curriculum.
