The 2003 Perl Advent Calendar
[about] | [archives] | [contact] | [home]

On the 11th day of Advent my True Language brought to me..
File::chdir

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.

Enter File::chdir

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.

Cross platform directories

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";

  • chdir
  • FindBin
  • File::Spec::Functions
  • File::Basename