Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic parameter types for functions working with files and directories

Tags:

powershell

If I were writing "idiomatic" PowerShell, what parameter types would I use for functions that work with files or directories?

For example, I made a module that contains this function:

$bookmarks = @{}
$bookmarkKeys = @{}

function Set-Bookmark {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Id,
        [System.Management.Automation.DirectoryInfo] $Path = (Get-Location))
    $script:bookmarks[$Id] = $Path
    $script:bookmarkKeys[$Path.Path] = $Id
}

The trouble is, the following doesn't work:

PS>Set-Bookmark -Id Code -Path C:\Code
Set-Bookmark : Cannot process argument transformation on parameter 'Path'.
Cannot convert the "C:\Code" value of type "System.String" to type
"System.Management.Automation.PathInfo".
At line:1 char:29
+ Set-Bookmark -Id Code -Path C:\Code
+                             ~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Set-Bookmark], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Set-Bookmark

Weirdly, neither does this (but for a slightly different reason):

PS>Set-Bookmark -Id Code -Path (gi C:\Code)
Set-Bookmark : Cannot process argument transformation on parameter 'Path'.
Cannot convert the "C:\Code" value of type "System.IO.DirectoryInfo" to type
"System.Management.Automation.PathInfo".
At line:1 char:29
+ Set-Bookmark -Id Code -Path (gi C:\Code)
+                             ~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Set-Bookmark], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Set-Bookmark

So should I actually define my parameters as strings (i.e. the lowest common denominator) and then try to cast/parse them into the more meaningful types? This doesn't sound right, because it's a shame if I'm working with the results of things like Get-Item and they get brought down to string for passing-in, only for that function to parse back into the higher-level type again.

like image 327
Neil Barnwell Avatar asked Mar 18 '16 15:03

Neil Barnwell


3 Answers

I think you are wanting to use the type [IO.DirectoryInfo]. This works fine for me:

function t {
  param(
    [IO.DirectoryInfo] $d
  )
  "You passed $d"
}

t (Get-Item "C:\Windows")

For files, you can use [IO.FileInfo], although this won't support wildcards.

like image 129
Bill_Stewart Avatar answered Nov 19 '22 06:11

Bill_Stewart


From a usability perspective, I have always found it easier to declare file and directory parameters as type string. What makes PowerShell so flexible is the ability to combine functionality from vastly different sources. I don't know who your intended audience is, but suppose a user wanted to call Set-Bookmark for entries in a text file. It would be much simpler to call the function if it accepted a string:

$id = 0
Get-Content 'bookmarks.txt' | For-EachObject { Set-Bookmark -id $i++ -Path $_ } 

There's also the fact that you might not always be using the file provider, so assuming that your Path always represents a filepath will limit the usefulness of this function. If, say, you were using the Microsoft SQL provider, you might want your function to provide bookmarks to paths in the SQL provider. (Your second solution using Test-Path would work.) Again, I don't know your target audience, but it's worth considering.

like image 40
JamesQMurphy Avatar answered Nov 19 '22 07:11

JamesQMurphy


You used the type System.Management.Automation.PathInfo in your actual code, not System.Management.Automation.DirectoryInfo, otherwise you'd be getting a TypeNotFound exception. There is no DirectoryInfo class in the System.Management.Automation namespace. However, as @Bill_Stewart already mentioned, folder objects are of the type System.IO.DirectoryInfo anyway. The PathInfo class is for PowerShell paths, which aren't necessarily filesystem paths (think for instance cert or registry provider).

If you want a parameter that can take both files (System.IO.FileInfo) and folders (System.IO.DirectoryInfo), you need to make the parameter type either a common base class of both types (e.g. System.IO.FileSystemInfo):

function Set-Bookmark {
    Param(
        [Parameter(Mandatory=$true)]
        [string] $Id,
        [Parameter(Mandatory=$false)]
        [IO.FileSystemInfo]$Path = (Get-Location)
    )
    ...
}

or System.String (because both file and folder objects can be cast to string and vice versa):

function Set-Bookmark {
    Param(
        [Parameter(Mandatory=$true)]
        [string] $Id,
        [Parameter(Mandatory=$false)]
        [ValidateScript({Test-Path -LiteralPath $_})]
        [string]$Path = (Get-Location)
    )
    ...
}

Note that when using String as the type of the -Path parameter you need to validate the parameter yourself to prevent passing arguments that aren't valid paths. Also note that the parameter won't accept path strings when using IO.FileSystemInfo as the parameter type (i.e. Set-Bookmark -Id 42 -Path "C:\some\path" would fail).

Bottom line:

The idiomatic parameter type depends on what input you want the function to accept. If it's file and folder objects use System.IO.FileSystemInfo. If it's paths use use System.String, validate the input, and convert the path string (back) to a file/folder object as required.

like image 2
Ansgar Wiechers Avatar answered Nov 19 '22 07:11

Ansgar Wiechers