Where is the Documents Folder?

In this article, our guest blogger—Brent Challis—provides a different approach to retrieving the correct Documents folder.


The Problem

One of the problems working with multiple computers is that the Documents folder path could be:

                C:\Users\<user>\OneDrive\Documents
or
                C:\Users\<user>\Documents

There are many other folders that could have a range of locations based on the operating system installation; this makes writing portable code difficult, as hard-coding paths will inevitably cause problems.

The Solution

There is a very straightforward way to retrieve the actual path by using the .NET Environment class:

[Environment]::GetFolderPath(“MyDocuments”) )

The Approach

I have written a PowerShell command to make working with these folders easier. I can use a command such as this to change to my Documents folder:

Set-Location -Path (Get-CCSpecialFolder -Name Documents -AsPath)

The Get-CCSpecialFolder command

The command uses an object to represent a special folder. I prefer to use classes rather than custom objects, so I defined this class:

class SpecialFolder
        {
            [String]$Name
            [String]$Path
            [String]$Type
        }

This is simplest form for a class, listing the variables needed to automatically create properties for them. In this case, I want the object to represent the name (or key) for the folder and it’s path. As I intend to represent paths relating to PowerShell locations, I wanted to identify if the path is:

  • One of the special folders from the operating system (BuiltIn).
  • A path relating to the PowerShell installation(s) (PowerShellDesktop or PowerShellCore).
  • An alias for an existing key (Alias – e.g., Documents for MyDocuments).
  • A path relating to how I use PowerShell (Custom for a path read from a file to identify a location such as the folder where my projects are stored).

I like to use classes as, unlike custom objects where you need to have individual lines of code to set the values for each data value (property), I can use a constructor. I can also easily add functionality. My final class definition is:

class SpecialFolder
        {
            [String]$Name
            [String]$Path
            [String]$Type
 
            SpecialFolder ([string]$Name,[String]$Path)
            {
                $this.Name = $Name   
                $this.Path = $Path  
                $this.Type = "BuiltIn"  
            }
            SpecialFolder ([string]$Name,[String]$Path,[String]$Type)
            {
                $this.Name = $Name   
                $this.Path = $Path  
                $this.Type = $Type  
            }
 
            [String]ToString()
            {
                return ($this.Path)
            }
        }

I decided not to implement the default constructor (this is a version of the constructor without arguments) as I want to always create an object with data. Instead, I overloaded the constructor to provide options for the object creation. Unfortunately, classes in PowerShell do not allow for cascading constructors. One way around this is to have the constructors simply call an initialization function as these can cascade.

This means that I can create a new object simply by calling the constructor:

[SpecialFolder]::new($Name,$Path,$Type)

Why an Alias?

One of the quirky parts of the system is that I have to use the ‘MyDocuments’ key as there are two Documents folders. One is under my user hive: C:\Users\Brent\OneDrive\Documents. The other is under the Public user hive: C:\Users\Public\Documents (although, oddly, it displays as Public Documents in File Explorer).

The Design

I create a collection of objects representing the special folders by using a HashTable and populating it with objects by iterating over the special folder keys:

$specialFolderNames = [Environment+SpecialFolder]::GetNames([Environment+SpecialFolder])
        [System.Collections.HashTable]$specialFolders = [System.Collections.HashTable]::new()
        [System.Collections.HashTable]$selectedSpecialFolders = [System.Collections.HashTable]::new()
 
        ### Create an entry for each of the special folders as defined by the operating system ###
        foreach ($specialFolderName in $SpecialFolderNames)
        {
            Add-SpecialFolder -Name $specialFolderName -Path ([Environment]::GetFolderPath($specialFolderName)) -Type "BuiltIn"
        }

Having set up the objects for the paths from the Environment class, I add folders for the aliases and the PowerShell installation(s).  For example:

####  Add special folder name alias #### 
Add-SpecialFolder -Name "Documents" -Path ($SpecialFolders["MyDocuments"].Path) -Type "Alias"
 
        ### Add special folder names for PowerShell Desktop Edition paths ###
        Add-SpecialFolder -Name "PSMyDesktopModules" -Path (Join-Path -Path $SpecialFolders["MyDocuments"].Path -ChildPath "WindowsPowerShell\Modules") -Type "PowerShellDesktop"
 
        ### Add special folder names for PowerShell Core Edition paths ###
        Add-SpecialFolder -Name "PSMyCorePowerShell" -Path (Join-Path -Path $SpecialFolders["MyDocuments"].Path -ChildPath "PowerShell") -Type "PowerShellCore"

I wrote the code to represent paths for the installation as these are reasonably clearly defined. But I also wanted the flexibility to add other paths to suit my work needs, so a file is read to add location-specific paths:

#### Custom paths as listed in the PowerShellCustomPaths.csv file ####
        [String]$customPath = Join-Path -Path $SpecialFolders["MyDocuments"].Path -Childpath "PowerShellCustomPaths.csv"
        if (Test-Path $customPath)
        {
            $customPaths = Import-Csv $customPath
            foreach($item in $customPaths)
            {
                Add-SpecialFolder -Name $item.Name -Path $item.Path -Type "Custom"
            }
        }

The Use

My primary use is to create a collection of all the special folders storing only the path so I can easily access and use it:

$PSSpecialFolders = Get-CCSpecialFolder -AsPath

When the AsPath switch is used, only the path is stored in the HashTable—not the SpecialFolder object. This makes it easier to use the collection, whereas having the complete object makes it easier to manage them.

To show the difference, if I do not use the -AsPath switch:

(Get-CCSpecialFolder -Include Alias).Values
returns:

Name      Path                                       Type 
----      ----                                       ---- 
Documents C:\Users\Brent\OneDrive\Documents          Alias
PSHome    C:\Windows\System32\WindowsPowerShell\v1.0 Alias 

Whereas using the -AsPath option:

(Get-CCSpecialFolder -Include Alias -AsPath).Values
returns:

C:\Users\Brent\OneDrive\Documents
C:\Windows\System32\WindowsPowerShell\v1.0

Now, if I want the path to my PowerShell Modules folder, I can use:

$PSSpecialFolders["PSMyDesktopModules"]

which returns:

C:\Users\Brent\OneDrive\Documents\WindowsPowerShell\Modules

Or, to repeat changing location to my documents folder:

Set-Location $PSSpecialFolders["Documents"]

The comment-based help with the command provides details for the parameters, which enables you to restrict which paths are returned; and whether to return the entire special folders object or just the path.

The Conclusion

I have been a developer for a long time, a .NET developer from its inception, and working with PowerShell from its initial development as Monad.

For me, PowerShell has two defining attributes:

  1. It lets you develop code to achieve the needed management outcomes, such as provisioning infrastructure or pre-processing data easily and repeatably. 
  2. It can change itself – you can use PowerShell to extend and enhance how your PowerShell environment/system works to make your work easier and more efficient.

Happy scripting.


We love to hear from you! Please comment below, or in our feedback forum and reference this post.