Skip to content
VLSI Mentor

Perl Part II · Module 17 · Engineering Files and Directory Automation

Anchoring a Script to the Project Root

A flow script is called from at least three places: by an engineer standing in a run directory, by a Makefile rule from the project root, and by a job scheduler from a working directory nobody chose. If the script resolves its paths relative to wherever it happens to be, it does three different things — and the one that breaks is always the one nobody tested, which is the scheduler. This chapter measures that failure by running one script from three directories and comparing what it can see about itself, then gives the four-line recipe that makes the behaviour identical in all of them. It is the first chapter of Part II because every tool that follows depends on it.

Intermediate12 min readPerlPart IIProject Structure

Part II · Module 17 · Chapter 17.1 · Anchoring a Script to the Project Root

1. The Same Script, Three Directories

Azvya Education Pvt. Ltd.VLSI Mentor
where-am-i.pl
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
use File::Path qw(make_path);

# a small project tree: bin/, rtl/, and a sibling directory to stand in
my $root = File::Spec->catdir(tempdir(CLEANUP => 1), "proj");
make_path(File::Spec->catdir($root, $_)) for qw(bin rtl elsewhere);

my $probe = File::Spec->catfile($root, "bin", "show.pl");
open my $w, ">", $probe or die "cannot write: $!\n";
print $w <<'PROBE';
use strict;
use warnings;
use Cwd qw(getcwd abs_path);
use FindBin;
use File::Basename qw(dirname);
# every path is masked back to <proj> so the output is reproducible
my $mask = sub { my $p = shift // "undef"; $p =~ s{.*/proj}{<proj>}; return $p };
printf "   cwd                          = %s\n", $mask->(getcwd());
printf "   \$0                           = %s\n", $mask->($0);
printf "   dirname(\$0)                  = %s\n", $mask->(dirname($0));
printf "   abs_path(\$0)                 = %s\n", $mask->(abs_path($0));
printf "   FindBin::Bin                 = %s\n", $mask->($FindBin::Bin);
printf "   abs_path(FindBin::Bin/'..')  = %s\n", $mask->(abs_path("$FindBin::Bin/.."));
PROBE
close $w;

my @how = (
  [ "from the project root",      $root,                                      "perl bin/show.pl"      ],
  [ "from a sibling, relative",   File::Spec->catdir($root, "elsewhere"),     "perl ../bin/show.pl"   ],
  [ "from /, absolute path",      File::Spec->rootdir(),                      "perl '$probe'"         ],
);
for my $h (@how) {
    my ($label, $where, $cmd) = @$h;
    # the label must not leak the temp path, or the output is not reproducible
    (my $shown = $cmd) =~ s{\Q$root\E}{<proj>}g;
    print "$label  ($shown)\n";
    print `cd '$where' && $cmd 2>&1`;
    print "\n";
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
from the project root  (perl bin/show.pl)
   cwd                          = <proj>
   $0                           = bin/show.pl
   dirname($0)                  = bin
   abs_path($0)                 = <proj>/bin/show.pl
   FindBin::Bin                 = <proj>/bin
   abs_path(FindBin::Bin/'..')  = <proj>

from a sibling, relative  (perl ../bin/show.pl)
   cwd                          = <proj>/elsewhere
   $0                           = ../bin/show.pl
   dirname($0)                  = ../bin
   abs_path($0)                 = <proj>/bin/show.pl
   FindBin::Bin                 = <proj>/bin
   abs_path(FindBin::Bin/'..')  = <proj>

from /, absolute path  (perl '<proj>/bin/show.pl')
   cwd                          = /
   $0                           = <proj>/bin/show.pl
   dirname($0)                  = <proj>/bin
   abs_path($0)                 = <proj>/bin/show.pl
   FindBin::Bin                 = <proj>/bin
   abs_path(FindBin::Bin/'..')  = <proj>

Read down the three blocks and compare each row against itself.

cwd differs, obviously — that is what we changed.

$0 differs in all three. It is whatever was typed: bin/show.pl, then ../bin/show.pl, then the full absolute path. Perl does not normalise it.

dirname($0) differs in all three — bin, ../bin, and an absolute directory. This is the one that catches people, because dirname($0) . "/../rtl" looks like it anchors to the project and does not.

FindBin::Bin is identical in all three. Always the directory the script lives in, as an absolute path.

abs_path($0) is also identical, and so is abs_path("$FindBin::Bin/.."), which is the project root.

So the rule is: the script's own location is stable; the caller's location is not. Anchor to the former.

2. What Is and Is Not Stable

A short table of what you can rely on:

ThingStable across callers?What it is
getcwd()nowhere the caller happened to be
$0nothe text used to invoke the script
dirname($0)noa directory relative to the caller
$FindBin::Binyesthe script's directory, absolute
$FindBin::Scriptyesthe script's filename
abs_path($0)yesthe script's full path
$ENV{PWD}nothe shell's idea of cwd, and may be stale

Two notes on the table.

$ENV{PWD} is worse than getcwd(), not better. It is set by the shell and is not updated by a chdir inside your script, so it can disagree with reality. Chapter 15.5 covered reading the environment; this is one of the variables not to trust.

FindBin is a core module, so it costs nothing to use — Chapter 12.9's list. It works out the script's directory at compile time from $0, the current directory and PATH.

3. The Recipe

Four lines at the top of every flow script:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
use FindBin;
use Cwd qw(abs_path);
use File::Spec;

my $ROOT = abs_path("$FindBin::Bin/..");    # bin/ is one level under the root

Then every path in the script is built from $ROOT:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my $rtl     = File::Spec->catdir($ROOT, "rtl");
my $runs    = File::Spec->catdir($ROOT, "runs");
my $filelist= File::Spec->catfile($ROOT, "rtl", "files.f");

Three things that recipe is doing:

abs_path collapses the .. so the root is a clean absolute path rather than /proj/bin/... That matters when the path appears in a log or a generated filelist that somebody has to read.

File::Spec->catdir and catfile join the parts instead of interpolating slashes. On Unix the difference is invisible; the habit is worth having because it also handles a trailing slash in $ROOT correctly.

Nothing reads the current directory. The script works identically from anywhere.

4. The Cases That Break It

Four situations where anchoring needs more thought.

The script is a symbolic link. FindBin::Bin resolves to the directory of the link, not the target, which is usually what you want for a shared bin linked into several projects — each project's link gives that project's root. If you want the target instead, FindBin offers $FindBin::RealBin. Knowing which you want is the whole decision.

abs_path resolves symlinks, and that can surprise you. On this machine /var is a link to /private/var, so a path under /var comes back rewritten. The consequence is practical: never compare a resolved path against an unresolved one. If you are going to compare, resolve both sides — and Chapter 17.6 depends on exactly this, because a cleanup check that compares a resolved target against an unresolved root will reject paths it should accept.

perl -e has no script. $FindBin::Bin falls back to the current directory, because there is no file to locate. One-liners cannot anchor, which is one more reason Chapter 15.6 said to promote a one-liner to a file as soon as it matters.

A Makefile rule that changes directory. cd runs/$@ && $(PERL) ../../bin/run.pl leaves the script anchored correctly — that is the point — but anything the rule does with relative paths is now relative to the run directory. Mixing the two is where confusion comes from. The cleaner pattern is to pass the directory in and let the script chdir itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my $rundir = $opt{rundir} or die usage();
chdir $rundir or die "cannot chdir to $rundir: $!\n";

Note the or die with $! on the chdir. An unchecked chdir that fails leaves the script running in the wrong directory and doing real work there, which is Chapter 13.5's unchecked-operation problem with a much larger blast radius.

5. What To Remember

  • A flow script is called from at least three places: a shell, a Makefile, and a scheduler.
  • Measured: $0 and dirname($0) differed in all three runs; FindBin::Bin and abs_path($0) were identical in all three.
  • dirname($0) . "/../rtl" looks like anchoring and is not.
  • The script's own location is stable; the caller's is not. Anchor to the former.
  • Do not trust $ENV{PWD} — the shell sets it and a chdir does not update it.
  • FindBin is core, so using it costs nothing.
  • The recipe is four lines: use FindBin, use Cwd qw(abs_path), use File::Spec, then my $ROOT = abs_path("$FindBin::Bin/..").
  • Build every path from $ROOT with catdir and catfile, and read the current directory never.
  • Allow an override with $ENV{PROJ_ROOT} // — // not || — and validate it with -d before use.
  • A symlinked script gives the link's directory; $FindBin::RealBin gives the target's. Choose deliberately.
  • abs_path resolves symlinks, so never compare a resolved path against an unresolved one — 17.6 depends on this.
  • perl -e cannot anchor — there is no script to locate.
  • Check every chdir with or die "...: $!", or the script does real work in the wrong place.
  • Do not assume bin/ is one level below the root. Pass the depth with a comment, or walk upward looking for a marker file.
  • A marker search needs a termination check — at /, .. stops changing.

Chapter 17.2 names the directories a script will be anchoring to. A real project has a small number of recognisable tree shapes, and knowing them is what lets one script work on several projects without a hard-coded path for each.

Where this fits

Part of the Perl curriculum.