If there's one thing that we're taught as programmers it's the concept of encapsulation. Don't mess with other people's stuff, and they won't mess with yours and we can hopefully work out who's doing what and be able to guarantee our code does what it says it will.
As Perl programmers, we don't always follow the strict letter of the law when it comes to encapsulation and our language will let you break these rules (almost) whenever we want. Sometimes it just makes more sense to define a method in someone else's class or access their variables directly. However, no matter how we hate being forced into encapsulation when we program in languages like Java, there's one thing that we hate even more: being forced to give up encapsulation unnecessarily.
Take changing directory for example. When I change directory Perl it effects the directory for the whole program not just for the subroutine I'm in. This means when I leave the subroutine, unless I want to screw up the rest of the program, I have to change the directory back. If I forget, then all hell breaks loose.
What we need is a way of locally changing the directory just for
one subroutine or one block - just for the scope we're in. And that's
where File::chdir
comes in.
The standard way manipulate directories in Perl is to use the chdir
command. chdir
returns true if it changes directory and false if it
does not.
use Cwd; use IO::File;
sub get_config { # change to the home directory and remember where we are my $old_directory = cwd; chdir or die "Can't change directory: $!";
# read in the file $/ = undef; my $fh; unless ($fh = IO::File->new(".config")) { chdir($old_directory); die "Can't open config file: $!"; } my $data = <$fh>;
# change back to the original directory chdir($old_directory);
return $data; }
The chdir changes the working directory for the entire program, so we
make sure to be careful to set it back to whatever it was at the start
of the subroutine. To aid us in this we use the cwd
function
that's exported by the Cwd module to get the current directory we're
in when we start and store it in $old_directory
, and then we can
chdir
back again just before any point we exit the subroutine.
This is all very, very tiring. It's interfering with the flow of our program - we can't just return at any point we have to remember to clean up after ourselves first - So the simple task of throwing an exception when we can't access our file is much more complicated that I think it really should be.
File::chdir creates a variable in your namespace called $CWD
.
Instead of using chdir
and cwd
the working directory can be set
and got by writing to and reading from to this variable. So the code
above could be rewritten as:
use File::chdir; use IO::File;
sub get_config { # change to the home directory and remember where we are my $old_directory = $CWD; $CWD = $ENV{HOME};
# read in the file $/ = undef; my $fh; unless ($fh = IO::File->new(".config")) { $CWD = $old_directory; die "Can't open config file: $!"; } my $data = <$fh>;
# change back to the original directory $CWD = $old_directory;
return $data; }
This really isn't any better than before. But wait, Perl can use the
local
command. local
temporarily sets a variable to a new
value, and restores the value to it's old value when you exit the
current scope (in this case the end of the subroutine.) Unlike my
,
local
creates dynamically scoped variables, which means that for any
subroutines we call from within that scope will see the new value,
i.e. will be in the same directory.
This wors fine with $CWD
so we can use local
to reset the
variable, and hence the directory, back to it's original value when we
get out of the subroutine.
use File::chdir; use IO::File;
sub get_config { local $CWD = $ENV{HOME};
# read in the file $/ = undef; my $fh = IO::File->new(".config") or die "Can't open config file: $!"; my $data = <$fh>;
return $data; }
Hooray! Look at all that lovely code that we no longer have to write. We now automatically return to the right directory whenever we exit the subroutine however we do it.
Of course, we can just set $CWD
to a particular location in the
file system.
$CWD = "$CWD/bin";
This doesn't work too well if your platform doesn't use "/" for a path
separator. For example, if you're using Windows, you should use "\"
and mac os 9 and lower users should uses ":". The common way around
this on is to use the File::Spec::Functions module. This exports a
list of functions that do the right thing on the platform you're
using. One such function catdir
combines directories with the
correct path separator.
$CWD = catdir(rootdir, "home","mark", "perlmods");
The File::chdir makes your life easier than that however. It lets you assign to the @CWD variable that contains the broken up path.
# change to /home/mark/perlmods (or C:\home\mark\perlmods, etc) use File::chdir; @CWD = ("home", "mark", "perlmods");
The @CWD contains each of the 'bits' that make up your path and you can manipulate it like you can any other array. To move up a directory:
# remove the item at the end of the directory pop @CWD;
And to move down a directory you just need to tack a directory onto the end of the list:
push @CWD, "public_html";
This is a lot safer than using absolute paths (I mean, what's the chance that there's a "C:\home\mark\perlmods" directory on my Windows box?) Importantly, you can easily find files relative to your script or module:
# the "resources" directory in the same directory # as your script use FindBin; local $CWD = $FindBin::Bin; push @CWD, "resources";
# the "resources" directory in the same directory # as the Foo::Bar module that you've 'used' use File::Basename; local $CWD = dirname($INC{'Foo/Bar.pm'}); push @CWD, "resources";