Author Posts

April 23, 2015 at 2:19 am

I would like to have some feedback on a new function I created. It works pretty well, but tips/tricks are always welcome!

function Change-DriveLetter {



#requires -Version 3

param (
    [parameter(Mandatory=$true)]
    [string]$OldDriveLetter,
    [parameter(Mandatory=$true)]
    [string]$NewDriveLetter
    )
 
    if ((Get-WmiObject -class Win32_Volume | select Name) | Where Name -Like "$NewDriveLetter`:\") {
        Write-Warning "Drive letter already exists, please enter a different drive letter."
        BREAK
    } # End of if block

    else {
        
        Try {
            $driveLetterToChange = Get-WmiObject -Class Win32_volume -Filter "DriveLetter LIKE '$OldDriveLetter%'" -ErrorAction Stop
            Set-WmiInstance -InputObject $driveLetterToChange -Arguments @{DriveLetter=$NewDriveLetter + ':'}
            Write-Verbose -Message "Changed the Driveletter from $OldDriveLetter to $NewDriveLetter"
        } # End of Try block

        Catch {
            Write-Warning "The following errors where found during the attempt to change the drive letter: `n$_"
            BREAK
        } # End of Catch Block
    } # End of else block

} # End of Function Block

April 23, 2015 at 4:44 am

You should be using the return keyword rather than Break, in both places. Break is for getting out of loops and switch statements, and if you use it when you're not in one of those constructs, you can actually abort someone's entire script that's calling your function.

I'd also recommend using Write-Error rather than Write-Warning. Both of those conditions are worth getting some red text.

April 23, 2015 at 6:42 am

A couple of suggestions. First try to use parameter validation, particularly ValidateScript.

function Set-Drive {
    [CmdletBinding()]
    param(
        [ValidateScript({ !(Test-Path $_ )})]
        [string]$NewDriveLetter="Y:\"
    )
    begin{}
    process{
        "Run command"
    }
    end{}
}

Set-Drive C:

Output:

Set-Drive : Cannot validate argument on parameter 'NewDriveLetter'. The " !(Test-Path $_ )" validation script for the argument with value "C:" did not return a result of True. Determine why the 
validation script failed, and then try the command again.
At line:15 char:11
+ Set-Drive C:
+           ~~
    + CategoryInfo          : InvalidData: (:) [Set-Drive], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Set-Drive

So, validate did occur, but the error message is a bit cryptic stating that what you validated wasn't true, so an error was thrown. Let's add a bit more logic into the parameter:

function Set-Drive {
    [CmdletBinding()]
    param(
        [ValidateScript({IF (-NOT (Test-Path $_)) {$true}else{ Throw "$($_) already exists, choose another drive"}})]
        [string]$NewDriveLetter="Y:\"
    )
    begin{}
    process{
        "Run command"
    }
    end{}
}

Set-Drive Y:

Output:

Set-Drive : Cannot validate argument on parameter 'NewDriveLetter'. C: already exists, choose another drive
At line:31 char:11
+ Set-Drive C:
+           ~~
    + CategoryInfo          : InvalidData: (:) [Set-Drive], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Set-Drive

Now we get a more useful custom error for troubleshooting. Your code would now work just like the try\catch around WMI:

PS C:\Windows\System32\WindowsPowerShell\v1.0> try{Set-Drive -NewDriveLetter C: -ErrorAction Stop}catch{$_.Exception.Message}
Cannot validate argument on parameter 'NewDriveLetter'. C: already exists, choose another drive

Second item, you are not validating that someone is actually providing what you want. You want a letter with no colon or backslash, what would happen if someone passed E:\. How is a user supposed to know what you want?

[attachment file="Set-Drive.txt"]

Output:

try{Set-Drive -NewDriveLetter A -ErrorAction Stop}catch{$_.Exception.Message}

Cannot validate argument on parameter 'NewDriveLetter'. The argument "A" does not match the "(?# Must be a letter D-Z )^[d-zD-Z]" pattern. Supply an argument that matches "(?# Must be a letter D-Z )^[d-
zD-Z]" and try the command again.

try{Set-Drive -NewDriveLetter A: -ErrorAction Stop}catch{$_.Exception.Message}

Cannot validate argument on parameter 'NewDriveLetter'. The character length of the 2 argument is too long. Shorten the character length of the argument so it is fewer than or equal to "1" characters, a
nd then try the command again.

try{Set-Drive -NewDriveLetter D -ErrorAction Stop}catch{$_.Exception.Message}

Cannot validate argument on parameter 'NewDriveLetter'. D:\ already exists, choose another drive

try{Set-Drive -NewDriveLetter E -ErrorAction Stop}catch{$_.Exception.Message}

Run command

try{Set-Drive -NewDriveLetter E:\ -ErrorAction Stop}catch{$_.Exception.Message}

Cannot validate argument on parameter 'NewDriveLetter'. The character length of the 3 argument is too long. Shorten the character length of the argument so it is fewer than or equal to "1" characters, a
nd then try the command again.

#Now we can get help on the function with Get-Help

PS C:\Windows\System32\WindowsPowerShell\v1.0> Get-Help Set-Drive -Detailed

NAME
    Set-Drive
    
SYNOPSIS
    Sets a new driver letter
    
    
SYNTAX
    Set-Drive [-NewDriveLetter]  []
    
    
DESCRIPTION
    Sets a new driver letter using WMI Class ....
    

PARAMETERS
    -NewDriveLetter 
        The letter of the drive with no colon or backslash (e.g. G)
        
    
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer, PipelineVariable, and OutVariable. For more information, see 
        about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). 
    
    -------------------------- EXAMPLE 1 --------------------------
    
    C:\PS>Set-Drive -NewDriveLetter E
    
    Set the new drive letter to E
    
    
    
    
REMARKS
    To see the examples, type: "get-help Set-Drive -examples".
    For more information, type: "get-help Set-Drive -detailed".
    For technical information, type: "get-help Set-Drive -full".

#Or

PS C:\Windows\System32\WindowsPowerShell\v1.0> Set-Drive
cmdlet Set-Drive at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
NewDriveLetter: !?
Provide a drive letter from D to Z with no colon or backslash
NewDriveLetter: 

Now, after you have done all this work to validate your parameters meet all of the criteria, does the WMI method already do all of this? What happens if you pass a blank value, a letter being used already, etc? Does the error tell you what is wrong? All I'm saying is you might be doing more work than is required and should test the method and see if it returns the proper errors before you do any validation in your script. Make sure you declare [CmdletBinding()] when using Write-Verbose or it will not work and set the parameter as Mandatory if it's required for the function to work.

function Set-Drive {
    [CmdletBinding()]
    param(
        [string]$NewDriveLetter
    )
    begin{}
    process{
        try{
            #WMI Method
        }
        catch {
            #any errors would be caught here that were handled by the
            #WMI method developer.  If the method tells you the drive is already used,
            #do you need handle it before then method is called?  You can catch the error,
            #log it and just do Throw $_ to throw the WMI error message to the calling logic
        }
    }
    end{}
}

Edit: Forum is stripping XML comments out of the content, adding as attachment

April 24, 2015 at 1:03 pm

There is a shorter way to perform your change, as such:

Get-WmiObject -Class Win32_volume -Filter "DriveLetter LIKE '$OldDriveLetter%'" -ErrorAction Stop | Set-WmiInstance -ArgumentList @{DriveLetter = "$NewDriveLetter`:"}

April 25, 2015 at 7:19 am

@dave, Rob & Fausto, thank you for the great feedback. I will improve my script this week and post it here again.