Function - Drive Letter Change

This topic contains 4 replies, has 4 voices, and was last updated by Profile photo of Richard Diphoorn Richard Diphoorn 1 year, 11 months ago.

  • Author
    Posts
  • #24581
    Profile photo of Richard Diphoorn
    Richard Diphoorn
    Participant

    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
    
  • #24589
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    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.

  • #24596
    Profile photo of Rob Simmers
    Rob Simmers
    Participant

    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

  • #24682
    Profile photo of Fausto Nascimento
    Fausto Nascimento
    Participant

    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`:"}
  • #24710
    Profile photo of Richard Diphoorn
    Richard Diphoorn
    Participant

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

You must be logged in to reply to this topic.