35479106It’s time for another entry in the trying-to-keep-myself-from-making-this-mistake-all-the-time series.

PowerShell has a notion of your current location.  You can see this using the $pwd automatic variable or the get-location cmdlet:

PS C:\Users\Jim\Documents> $pwd


PS C:\Users\Jim\Documents> get-location


This location is also displayed in the default prompt.  This path is used by PowerShell to resolve relative paths at the level of the PowerShell API.

Applications have a notion of the current directory.  This is the directory used to resolve relative paths at the level of the Windows API.

How you Get Burned

Your current location may or may not be the same as your current directory.

Allow me to elaborate…

If you write a file using a PowerShell cmdlet like out-file, the relative path you specify will be resolved using the current location:

PS C:\Users\Jim\Documents> 'hello world' | out-file foo.txt
PS C:\Users\Jim\Documents> ls foo.txt

    Directory: C:\Users\Jim\Documents

Mode                LastWriteTime     Length Name                                                                      
----                -------------     ------ ----                                                                      
-a---         2/27/2013  10:50 AM         28 foo.txt                                                                   

I told the out-file cmdlet to write the file foo.txt, and it create that file in my current location – c:\users\jim\documents.  Make sense? 

Now, let’s try the same thing using the .NET Framework directly:

PS C:\Users\Jim\Documents> [io.file]::writealltext( 'bar.txt', 'hello world' )
PS C:\Users\Jim\Documents> dir bar*
PS C:\Users\Jim\Documents> dir ../bar*

    Directory: C:\Users\Jim

Mode                LastWriteTime     Length Name                                                                      
----                -------------     ------ ----                                                                      
-a---         2/27/2013  11:04 AM         11 bar.txt                                                                   

This time I used the System.IO.File.WriteAllText static method, specifying the relative path bar.txt.  But the file was written into another directory! 

Why?  Because the WriteAllText method uses paths relative to my current directory, not my current location.  In this case, they’re different:

PS C:\Users\Jim\Documents> [environment]::currentdirectory
PS C:\Users\Jim\Documents> get-location


The fact is, PowerShell doesn’t actively move the [environment]::currentdirectory as you navigate around the drive:

PS C:\Users\Jim\Documents> cd .\project
PS C:\Users\Jim\Documents\project> [environment]::currentdirectory
PS C:\Users\Jim\Documents\project> cd /
PS C:\> [environment]::currentdirectory

This becomes a problem when you use PowerShell to run an application or invoke parts of the .NET framework – the output doesn’t go where you’d expect it to go.  Generally speaking, these things will work off of the current directory and have no concept of your current location in PowerShell.

Things You Can Do

First, you can force PowerShell to update the current directory each time the current location changes.  Just customize your prompt function so it sets [environment]::currentDirectory to the current filesystem location:

   1:  function prompt {
   2:      $p = get-location -PSProvider filesystem | select -exp path;
   3:      [environment]::CurrentDirectory = $p;
   4:      return "Jim is Awesome!!!1 $pwd> ";
   5:  }

Line 2 gets the current location for the FileSystem PowerShell Provider.  We have to specify the FileSystem provider because PowerShell lets you do crazy things like set the current location to the registry, and we can’t very well set the current directory to such a path.  Line 3 updates the current directory so it matches the current location.  This happens each time each time the prompt is written by the current host, so the current directory will be updated after each command.  That means when you change location, the current directory will immediately follow suit.

Second, if you author cmdlets that rely on paths, and you end up using these paths in .NET framework calls, I would strongly recommend that you resolve the PowerShell paths into full file system paths before passing them on to the framework:

// ...
[Parameter(Mandatory = true)]
public string FilePath { get; set; }

protected override void ProcessRecord()
    var fullPath = this.GetUnresolvedProviderPathFromPSPath(FilePath);
    Bitmap bitmap = CreateBitmap();
    bitmap.Save( fullPath );
// ...

Finally, if you find your self expecting a file to appear in your current location and it doesn’t, check the current directory before you start throwing things:

PS C:\Users\Jim\Documents> test-path bar.txt
PS C:\Users\Jim\Documents> test-path ( join-path ([environment]::currentdirectory) bar.txt)