Friday Fun: Get Next Available Drive Letter

A few days ago I saw on question, I think on Facebook, about using PowerShell to find the next available drive letter that could be used for mapping a network drive. Before I show you my approach, let me state that if you need to map a drive in PowerShell for use only within your PowerShell solution, you don’t need a drive letter. You can name the drive anything you want when using New-PSDrive.


new-psdrive Backup -PSProvider Filesystem -Root "\\NAS01\backup"

But of course that won’t work if you plan on using the NET USE command. So let’s figure out how to determine the next available drive letter. First, let’s build an array of possible drive letters. I’m too lazy to type the letters C-Z (skipping A and B for the sake of nostalgia). So I’ll “create” them like this:


$letters=[char[]](67..90)

This creates an array of letters C-Z, albeit technically [CHAR] objects, but we’re good. Next we need a list of currently used drive letters which we can retrieve from WMI.


$devices=get-wmiobject win32_logicaldisk | select -expand DeviceID

I’m expanding the DeviceID property so $devices is a simple array and not a collection of objects with a DeviceID property. I did that so that I could use the -Contains operator.


$letters | where {$devices -notcontains "$($_):"} | Select -first 1

With this simple command I’m piping the collection of characters to Where-Object, testing if each letter (with the appended :) is NOT found in the array $devices. The letters are already sorted so all I need to do is select the first 1 and that will be the next available drive letter in my current PowerShell session.

Before I let you go today, let me point out one thing: I could combine these two lines into a single PowerShell one-liner.


[char[]](67..90) | Where {(get-wmiobject win32_logicaldisk | select -expand DeviceID) -notcontains "$($_):"} | Select -first 1

I’ll get the same result…BUT…just because you can do something in PowerShell doesn’t mean you should. This one liner takes almost 2400MS to execute. But when I break it into two lines, as I showed you, I get the end result in 110MS which is a dramatic and noticeable difference. You can test for yourself using Measure-Command. The take-away is to not be afraid to use multiple commands, especially in a script. It may be faster and it will certainly be easier to understand.

6 thoughts on “Friday Fun: Get Next Available Drive Letter”

  1. Where-Object, testing if each letter

    you can break pipeline like this:


    filter BreakWhere {
    param(
    [ScriptBlock]$FilterScript
    )
    if(&$FilterScript) {
    $_
    } else {
    break
    }
    }

    and this un test:

    PS> 1..10000000 | BreakWhere -FilterScript { $_ -le 5 }
    PS> 1..10000000 | Where -FilterScript { $_ -le 5 }

  2. also you can create a proxy function


    function Select-Object {

    [CmdletBinding(DefaultParameterSetName='DefaultParameter')]
    param(
    [Parameter(ValueFromPipeline=$true)]
    [System.Management.Automation.PSObject]
    ${InputObject},

    [Parameter(ParameterSetName='DefaultParameter', Position=0)]
    [System.Object[]]
    ${Property},

    [Parameter(ParameterSetName='DefaultParameter')]
    [System.String[]]
    ${ExcludeProperty},

    [Parameter(ParameterSetName='DefaultParameter')]
    [System.String]
    ${ExpandProperty},

    [Switch]
    ${Unique},

    [Parameter(ParameterSetName='DefaultParameter')]
    [ValidateRange(0, 2147483647)]
    [System.Int32]
    ${Last},

    [Parameter(ParameterSetName='DefaultParameter')]
    [ValidateRange(0, 2147483647)]
    [System.Int32]
    ${First},

    [Parameter(ParameterSetName='DefaultParameter')]
    [ValidateRange(0, 2147483647)]
    [System.Int32]
    ${Skip},

    [Parameter(ParameterSetName='IndexParameter')]
    [ValidateRange(0, 2147483647)]
    [System.Int32[]]
    ${Index})

    begin
    {
    try {
    $outBuffer = $null
    if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
    {
    $PSBoundParameters['OutBuffer'] = 1
    }
    $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Select-Object', [System.Management.Automation.CommandTypes]::Cmdlet)
    $scriptCmd = {& $wrappedCmd @PSBoundParameters }
    $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
    $steppablePipeline.Begin($PSCmdlet)
    } catch {
    throw
    }
    }

    process
    {
    try {
    if($First -and $_ -le $First) {
    $steppablePipeline.Process($_)
    } else {
    break
    }
    } catch {
    throw
    }
    }

    end
    {
    try {
    $steppablePipeline.End()
    } catch {
    throw
    }
    }
    < #

    .ForwardHelpTargetName Select-Object
    .ForwardHelpCategory Cmdlet

    #>

    }

    test:


    1..1mb | Select-Object -First 1
    1..1mb | Microsoft.PowerShell.Utility\Select-Object -First 1

  3. Even slower one-liner, but with mapped drives too…

    [char[]](65..90) | % { If ((gwmi Win32_LogicalDisk).DeviceID -contains “${_}:” -or (gwmi Win32_MappedLogicalDisk).DeviceID -contains “${_}:”) { “${_}: YUP!” } Else { “${_}: NOPE!” } }

    1. Now that I go back and run the code it won’t work because WMI is going to return a collection of objects. Even on Windows 7 I get all “Nope”.

  4. @Chris Savard,

    don’t work in my XP:

    A: NOPE!
    B: NOPE!
    C: NOPE!
    D: NOPE!
    E: NOPE!
    F: NOPE!
    G: NOPE!
    H: NOPE!
    I: NOPE!
    J: NOPE!
    K: NOPE!
    L: NOPE!
    M: NOPE!
    N: NOPE!
    O: NOPE!
    P: NOPE!
    Q: NOPE!
    R: NOPE!
    S: NOPE!
    T: NOPE!
    U: NOPE!
    V: NOPE!
    W: NOPE!
    X: NOPE!
    Y: NOPE!
    Z: NOPE!

Comments are closed.