Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

Testing the current directory with Cwd and File::Spec

by xdg (Monsignor)
on Dec 03, 2005 at 15:58 UTC ( [id://513817]=perlmeditation: print w/replies, xml ) Need Help??

This meditation addresses how to test whether the current working directory is what you expect it to be. It describes some pitfalls I encountered using two common modules, Cwd and File::Spec, in the hopes that others may learn from my mistakes and avoid similar frustrations.

This meditation stems from my work on the test suite for File::pushd, which does a "local" directory change for a limited scope. The module itself works correctly on both Linux and Win32 ActiveState (the two platforms I have handy to test) -- but proving that it's working in the test suite and doing so in an easy and portable way turned out to be a greater challenge that I first anticipated.

Here is a simplified example of where my testing went awry:

use strict; use warnings; use Cwd; use File::Spec; use Test::More 'no_plan'; use File::pushd qw( pushd ); # Meditation benefits from short directory names, so save our initial # directory then switch to the tempdir and create a dummy directory my $initial_dir = cwd(); chdir File::Spec->tmpdir(); mkdir "t" unless -e "t"; # Record the directory before pushd() is called my $original_dir = cwd(); # Establish a limited scope for pushd() { # Change to target directory until $new_dir goes out of scope # Returns absolute path to new directory my $new_dir = pushd( 't' ); is( cwd(), $new_dir, "changed to new directory" ); } is( cwd(), $original_dir, "changed back to original directory" ); # Get back to the starting point chdir $initial_dir;

Result on Linux:

ok 1 - changed to new directory ok 2 - changed back to original directory 1..2

Result on Win32:

not ok 1 - changed to new directory # Failed test (cwd_post1.pl at line 26) # got: 'C:/Temp/t' # expected: 'C:\Temp\t' ok 2 - changed back to original directory 1..2 # Looks like you failed 1 test of 2.

Simple enough -- cwd provides forward slashes, whereas File::pushd uses File::Spec, which keeps things in the canonical form for the operating system. That can be cleaned up with canonpath.

- is( cwd(), $new_dir, "changed to new directory" ); + is( File::Spec->canonpath( cwd() ), $new_dir, "changed to new dir +ectory" );

That works on both Linux and Win32, hooray! But then I tried a different directory -- the root directory.

- my $new_dir = pushd( 't' ); + my $new_dir = pushd( File::Spec->rootdir );

Again, this works just fine on Linux, but not on Win32.

Result on Win32:

not ok 1 - changed to new directory # Failed test (cwd_post3.pl at line 26) # got: 'C:\' # expected: '\' ok 2 - changed back to original directory 1..2 # Looks like you failed 1 test of 2.

The problem is that rootdir doesn't provide a drive volume letter, whereas cwd does. This sent me off in several directions looking for a portable workaround. Before describing what I settled on, I'll show some things that didn't work, didn't work as expected, or just turned out too cumbersome as examples of things to avoid.

Option 1: see if the current directory relative to the expected new root directory ( built from rootdir ) gives a directory of "." (i.e. the relative form of the current directory).

- is( File::Spec->canonpath( cwd() ), $new_dir, "changed to new dir +ectory" ); + is( File::Spec->abs2rel( cwd(), $new_dir), File::Spec->curdir, + "changed to new directory" + );

Now things get interesting.

The result on Linux reveals a bug in File::Spec -- the result is the empty string rather than a proper directory:

not ok 1 - changed to new directory # Failed test (cwd_post4.pl at line 26) # got: '' # expected: '.' ok 2 - changed back to original directory

This may seem a minor issue, but it could lead to a very hard-to-detect (and potentially severe) error if such a blank result were ever used with catdir again, as these one-liners demonstrate:

$ perl -MFile::Spec -e 'print File::Spec->catdir(".","t")' t $ perl -MFile::Spec -e 'print File::Spec->catdir("","t")' /t

This bug has been fixed in version 3.13 of PathTools, but of course it breaks anything that depends on the older, broken behavior. For example, SVK is one of those tools that breaks. Whether/how this fix should be rolled into maint sparked some discussion on this perl5-porters thread.

The situation isn't much better on Win32. There, I don't even get the empty string, as File::Spec refuses to take a relative path if it can't be sure the base path is on the same volume.

not ok 1 - changed to new directory # Failed test (cwd_post4.pl at line 26) # got: 'C:\' # expected: '.' ok 2 - changed back to original directory 1..2 # Looks like you failed 1 test of 2.

Option 2: drop the volume using splitpath (being sure to include the "no file" option).

(For brevity, I use File::Spec::Functions selectively in the following examples.)

use File::Spec; +use File::Spec::Functions qw( canonpath splitpath );
- is( File::Spec->abs2rel( cwd(), $new_dir), q{.}, + is( canonpath( [ splitpath( cwd(), 1 ) ]->[1] ), + $new_dir, "changed to new directory" );

This works on both Linux and Win32 -- until I go back to trying it not with the root directory but with the first directory I tried.

- my $new_dir = pushd( File::Spec->rootdir ); + my $new_dir = pushd( "t" );

This fails on Win32 because (recalling above), both "got" and "expected" have a volume. So the volume must be stripped twice:

- is( canonpath( [ splitpath( cwd(), 1 ) ]->[1] ), - $new_dir, + is( canonpath( [ splitpath( cwd(), 1 ) ]->[1] ), + canonpath( [ splitpath( $new_dir, 1 ) ]->[1] ), "changed to new directory" );

This "works" on both systems, but only in the sense that the resulting strings match. With the volumes dropped, "D:\Temp" is the same as "C:\Temp". For the test suite I was running, that's probably OK, but I wouldn't recommend it as a general approach. It's also quite cumbersome, even more so without File::Spec::Functions.

Option 3: stick entirely to Cwd and use abs_path to convert the expected value to the same format as cwd. In hindsight, this should have been obvious much earlier, since abs_path is the algorithm underlying cwd, but I started out trying to use File::Spec to handle the absolute paths portably and got caught up in the bug in Option 1. This option is shorter, clearer and works on both Linux and Win32.

-use Cwd; +use Cwd qw( cwd abs_path ); use File::Spec; -use File::Spec::Functions qw( canonpath splitpath );
- is( canonpath( [ splitpath( cwd(), 1 ) ]->[1] ), - canonpath( [ splitpath( $new_dir, 1 ) ]->[1] ), - "changed to new directory" - ); + is( cwd(), abs_path( $new_dir ), "changed to new directory" );

This also works as desired on both operating systems when used with the root directory:

- my $new_dir = pushd( "t" ); + my $new_dir = pushd( File::Spec->rootdir );

One potential downside to abs_path is that the paths returned have forward-slashes. This might be an issue if comparing to filenames generated externally from a native OS source -- though the consistent use of abs_path on all such filenames would address that. Likewise, consistently applying canonpath after abs_path could be an approach.

abs_path has some side-effects, as it also resolves symbolic links and relative-path components like "." and "..", but for most ordinary needs it should work well. Given the volume-challenged behavior in File::Spec, abs_path is going to have a lasting place in my toolbox.

-xdg

Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

Replies are listed 'Best First'.
Re: Testing the current directory with Cwd and File::Spec
by Anonymous Monk on Dec 04, 2005 at 22:58 UTC

    If you want to save the current directory so that you may return to it later, the best way to do it is to open "." (current directory) and save the handle. Then you fchdir() to this handle in order to restore it.

    There are several problems with obtaining a text representation of the current directory instead. It may be longer than the system's maximum pathname length, it may change without you noticing (someone else renames a directory that is one of the parents of the current one), and it may even in some cases not be possible to determine what it is (missing permissions?). It is also expensive because you have to walk the chain of parent directories back to the root.

    But: How do you fchdir() in Perl?

    -Celada

      I disagree. The easiest way to save the current directory and return to it later is File::chdir, which lets you change directories through a localized variable. Example:

      use File::chdir; print $CWD; # now cd and show me a directory listing { local(@CWD,$CWD); push @CWD,'subdir'; print $CWD; print join("\n", glob '*'); } # now we're back where we started. We'll do a listing to show. print $CWD; print join("\n", glob '*');

      The imported variable $CWD always holds the full path, so you can also just store it to a variable and restore the value later, even mixing things with chdir.

      use File::chdir; my $wd = $CWD; chdir('subdir'); # do some work. $CWD = $wd; # return to saved dir.

      Now, that may not be the best way, but I'd vote for it as easiest. :-)

      <-radiant.matrix->
      A collection of thoughts and links from the minds of geeks
      The Code that can be seen is not the true Code
      "In any sufficiently large group of people, most are idiots" - Kaa's Law
        Now, that may not be the best way, but I'd vote for it as easiest. :-)

        Two comments.

        First, File::chdir uses Cwd and File::Spec internally, which means it's potentially just as subject to the issues raised in the meditation. The Anonymous Monk was suggesting opening and saving the filehandle, not the directory name. That's quite a bit different and would be more robust to name issues. However, the meditation was about comparing the current directory to a desired one -- which in a test suite is specified by name, not filehandle, so that doesn't really help either.

        Second, my hope is that people may find File::pushd to be even easier -- at least for the directory reverting part. Here's something like your first example with File::pushd -- no localization needed:

        use Cwd; use File::pushd; print cwd; { my $dir = pushd('subdir'); print join("\n", glob '*'); } # back where we started print cwd; print join("\n", glob '*');

        And a File::pushd version like your second example -- with no needing to manually change back to a saved directory:

        use File::pushd; { my $wd = pushd(); chdir('subdir'); # do some work. } # automatically back in original dir

        Where it really shines is with temporary directories that need to clean themselves up:

        use File::pushd; { my $wd = tempd(); # do work in the temporary directory } # back in the original directory and the temporary directory is delete +d

        -xdg

        Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

Re: Testing the current directory with Cwd and File::Spec
by 0xbeef (Hermit) on Dec 07, 2005 at 00:21 UTC
    Here is another Cwd consideration. If the directory is a mounted filesystem, getcwd() (as the owner of the mounted filesystem) may return undefined if the owner has insufficient access to the underlying mount-point even if he has full access to the mounted filesystem.

    For example:

    #get group $groups = `/usr/bin/id -G $username`; chomp $groups; # drop privileges to owner of filesystem $) = "$groups"; #setegid $> = $uid; #seteuid $currentdir = Cwd::getcwd(); my $lasterr = $!; if (not defined $currentdir) { print "getcwd() failed : @_\n"; } else { print "getcwd() for $over is OK."; }

    A command like tar will usually give an error like "tar: The getwd subroutine failed. Cannot open parent directory."

    This is useful to know in cases where the underlying stub directory's permissions affect programs relying on getcwd() e.g. /bin/pwd, tar etc.

    0xbeef

      That's an interesting little quirk! I guess that's why it's considered unwise to give less than 0555 permission to mountpoints. By the way I can't reproduce the behaviour under Linux but I can under Solaris.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://513817]
Approved by Ovid
Front-paged by Old_Gray_Bear
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others lurking in the Monastery: (8)
As of 2024-04-19 07:42 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found