[char[]] $taken = (Get-PSDrive -Name [A-Z]).Name
$nextAvailable = ([char[]] (65..90)).Where({ $_ -notin $taken }, 'First')[0]
if (-not $nextAvailable) { throw "No drive letters available." }
(Get-PSDrive -Name [A-Z]) uses a wildcard expression to get information about all currently defined single-letter-name drives.
.Name uses member-access enumeration to return an array of the names of all these drives, i.e. the taken letters as strings.
- The
[char[]] type constraint applied to variable $taken converts the array of single-letter strings to an array of [char] instances.
[char[]] (65..90) programmatically creates the array of (English) uppercase letters, 'A', 'B', ..., 'Z', using .., the range operator. In PowerShell (Core) 7+ you can use character endpoints directly: 'A'..'Z'. Note that PowerShell is case-insensitive in general, so the case of these letters is irrelevant.
The .Where() array method iterates over each letter and returns the first letter ('First') that satisfies the condition:
$_ -notin $taken returns $true if the input letter at hand ($_) is not an element of (-notin) the $taken array, i.e. it returns $true if the letter isn't currently taken.
Note that [0] is applied to the .Where() call so as to ensure that the (at most) one and only result is treated as a scalar, which is necessary, because the .Where() and .ForEach() methods always return a collection. That said, in PowerShell the distinction between a scalar and a collection with a single element often doesn't matter.
Note: The above starts looking for available drive letters with letter A (drive A:).
To start looking from a different letter - say E - do the following:
Adjust the starting character in the letter-array-creation operation, ..
[char[]] (69..90) # 'E', 'F', ..., 'Z'
69, i.e. the decimal form of the Unicode code point of the uppercase E letter, was obtained with [int] [char] 'E'
Again, in PowerShell (Core) 7+ you could simply use 'E'..'Z'
As an optional optimization you could additionally adjust the wildcard pattern in the Get-PSDrive call:
(Get-PSDrive -Name [E-Z]).Name
An alternative solution that avoids looping and linear searches in arrays:
Surprisingly, however, it is slower than the solution above, perhaps due to overhead of exception handling; in practice, both solutions will likely perform acceptably.
It takes advantage of the fact that you can pass multiple drive letters (names) to the -Name parameter; non-existent names trigger a non-terminating error that can be escalated to a terminating one with -ErrorAction Stop, which can then be trapped with try { ... } catch { ... }.
The offending name - the nonexistent drive letter - is reported in the error record's .TargetObject property (reported via the automatic $_ variable in the catch block, as an [ErrorRecord] instance).
$nextAvailable =
try {
$null = Get-PSDrive -ErrorAction Stop -Name ([char[]] (65..90))
} catch {
$_.TargetObject
}
if (-not $nextAvailable) { throw "No drive letters available." }