Динамическое значение параметра в зависимости от другого динамического значения параметра
Исходная предпосылка: очень ограниченная среда, Windows 7 SP1, Powershell 3.0. Ограничено или нет возможности использования внешних библиотек.
Я пытаюсь переписать инструмент bash, который я создал ранее, на этот раз с помощью PowerShell. В bash я реализовал автозаполнение, чтобы сделать инструмент более удобным для пользователя, и я хочу сделать то же самое для версии PowerShell.
Версия bash работает так:
./launcher <Tab> => ./launcher test (or dev, prod, etc.)
./launcher test <Tab> => ./launcher test app1 (or app2, app3, etc.)
./launcher test app1 <Tab> => ./launcher test app1 command1 (or command2, command3, etc.).
Как видите, все было динамично. Список сред был динамическим, список приложений был динамичным, в зависимости от выбранной среды, список команд также был динамическим.
Проблема с тестом → подключение приложения. Я хочу показать правильное приложение на основе среды, уже выбранной пользователем.
Используя PowerShell DynamicParam, я могу получить динамический список сред на основе списка папок. Однако я не могу (или, по крайней мере, я не узнал, как это сделать) сделать листинг другой папки, но на этот раз, используя переменную, основанную на существующем выборе пользователя.
Текущий код:
function ParameterCompletion {
$RuntimeParameterDictionary = New-Object Management.Automation.RuntimeDefinedParameterDictionary
# Block 1.
$AttributeCollection = New-Object Collections.ObjectModel.Collection[System.Attribute]
$ParameterName = "Environment1"
$ParameterAttribute = New-Object Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
$AttributeCollection.Add($ParameterAttribute)
# End of block 1.
$parameterValues = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute = New-Object Management.Automation.ValidateSetAttribute($parameterValues)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
# Block 2: same thing as in block 1 just with 2 at the end of variables.
# Problem section: how can I change this line to include ".\configurations\${myVar}"?
# And what's the magic incantation to fill $myVar with the info I need?
$parameterValues2 = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute2 = New-Object Management.Automation.ValidateSetAttribute($parameterValues2)
$AttributeCollection2.Add($ValidateSetAttribute2)
$RuntimeParameter2 = New-Object
Management.Automation.RuntimeDefinedParameter($ParameterName2, [string], $AttributeCollection2)
$RuntimeParameterDictionary.Add($ParameterName2, $RuntimeParameter2)
return $RuntimeParameterDictionary
}
function App {
[CmdletBinding()]
Param()
DynamicParam {
return ParameterCompletion "Environment1"
}
Begin {
$Environment = $PsBoundParameters["Environment1"]
}
Process {
}
}
1 ответ
Я бы порекомендовал использовать завершители аргументов, которые частично представлены в PowerShell 3 и 4 и полностью доступны в версии 5.0 и выше. Для v3 и v4 базовая функциональность есть, но вы должны переопределить встроенную функцию TabExpansion2, чтобы использовать их. Это нормально для вашего собственного сеанса, но обычно не одобряется распространение инструментов, которые делают это, на сеансы других людей (представьте, если бы каждый пытался переопределить эту функцию). У члена команды PowerShell есть модуль, который делает это для вас и называется https://github.com/lzybkr/TabExpansionPlusPlus. Я знаю, я сказал, что переопределение TabExpansion2 было плохо, но это нормально, если этот модуль делает это:)
Когда мне нужно было поддерживать версии 3 и 4, я распределял свои команды по модулям, а модули проверяли наличие команды 'Register-ArgumentCompleter', которая является командлетом в v5+ и функцией, если у вас есть TE++ модуль. Если модуль найдет его, он зарегистрирует любой завершитель (и), а если нет, то уведомит пользователя, что завершение аргумента не будет работать, если он не получит модуль TabExpansionPlusPlus.
Предполагая, что у вас есть модуль TE++ или PSv5+, я думаю, что это должно привести вас на правильный путь:
function launcher {
[CmdletBinding()]
param(
[string] $Environment1,
[string] $Environment2,
[string] $Environment3
)
$PSBoundParameters
}
1..3 | ForEach-Object {
Register-ArgumentCompleter -CommandName launcher -ParameterName "Environment${_}" -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$PathParts = $fakeBoundParameter.Keys | where { $_ -like 'Environment*' } | sort | ForEach-Object {
$fakeBoundParameter[$_]
}
Get-ChildItem -Path ".\configurations\$($PathParts -join '\')" -Directory -ErrorAction SilentlyContinue | select -ExpandProperty Name | where { $_ -like "${wordToComplete}*" } | ForEach-Object {
New-Object System.Management.Automation.CompletionResult (
$_,
$_,
'ParameterValue',
$_
)
}
}
}
Чтобы это работало, вашему текущему рабочему каталогу понадобится каталог "configs", содержащийся в нем, и вам понадобится как минимум три уровня подкаталогов (читая ваш пример, выглядело, как будто вы собирались перечислить каталог, и вы будет углубляться в эту структуру при добавлении параметров). Перечисление каталога сейчас не очень разумно, и вы можете легко его обмануть, если просто пропустите параметр, например, launcher -Environment3 <TAB>
постараюсь дать вам дополнения к первому подкаталогу.
Это работает, если у вас всегда будет три доступных параметра. Если вам нужна переменная # параметров, вы все равно можете использовать завершители, но это может оказаться немного сложнее.
Самым большим недостатком будет то, что вам все равно придется проверять вводимые пользователем данные, так как завершители - это всего лишь предложения, и пользователям не нужно использовать эти предложения.
Если вы хотите использовать динамические параметры, это становится довольно сумасшедшим. Возможно, есть лучший способ, но я никогда не мог видеть значение динамических параметров в командной строке без отражения, и в этот момент вы используете функциональность, которая может измениться в следующем выпуске (члены обычно не ' т общественность по причине). Заманчиво попытаться использовать $MyInvocation внутри блока DynamicParam {}, но он не заполняется в тот момент, когда пользователь вводит команду в командной строке, и в любом случае он показывает только одну строку команды без использования отражения.
Нижеследующее было протестировано на PowerShell 5.1, поэтому я не могу гарантировать, что в любой другой версии есть те же самые ученики (она основана на том, что я впервые увидел, как Гаррет Серак). Как и в предыдущем примере, это зависит от папки.\ Configurations в текущем рабочем каталоге (если ее нет, вы не увидите никаких параметров -Environment).
function badlauncher {
[CmdletBinding()]
param()
DynamicParam {
#region Get the arguments
# In it's current form, this will ignore parameter names, e.g., '-ParameterName ParameterValue' would ignore '-ParameterName',
# and only 'ParameterValue' would be in $UnboundArgs
$BindingFlags = [System.Reflection.BindingFlags] 'Instance, NonPublic, Public'
$Context = $PSCmdlet.GetType().GetProperty('Context', $BindingFlags).GetValue($PSCmdlet)
$CurrentCommandProcessor = $Context.GetType().GetProperty('CurrentCommandProcessor', $BindingFlags).GetValue($Context)
$ParameterBinder = $CurrentCommandProcessor.GetType().GetProperty('CmdletParameterBinderController', $BindingFlags).GetValue($CurrentCommandProcessor)
$UnboundArgs = @($ParameterBinder.GetType().GetProperty('UnboundArguments', $BindingFlags).GetValue($ParameterBinder) | where { $_ } | ForEach-Object {
try {
if (-not $_.GetType().GetProperty('ParameterNameSpecified', $BindingFlags).GetValue($_)) {
$_.GetType().GetProperty('ArgumentValue', $BindingFlags).GetValue($_)
}
}
catch {
# Don't do anything??
}
})
#endregion
$ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create an Environment parameter for each argument specified, plus one extra as long as there
# are valid subfolders under .\configurations
for ($i = 0; $i -le $UnboundArgs.Count; $i++) {
$ParameterName = "Environment$($i + 1)"
$ParamAttributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttributes.Add((New-Object Parameter))
$ParamAttributes[0].Position = $i
# Build the path that will be enumerated based on previous arguments
$PathSb = New-Object System.Text.StringBuilder
$PathSb.Append('.\configurations\') | Out-Null
for ($j = 0; $j -lt $i; $j++) {
$PathSb.AppendFormat('{0}\', $UnboundArgs[$j]) | Out-Null
}
$ValidParameterValues = Get-ChildItem -Path $PathSb.ToString() -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name
if ($ValidParameterValues) {
$ParamAttributes.Add((New-Object ValidateSet $ValidParameterValues))
$ParamDictionary[$ParameterName] = New-Object System.Management.Automation.RuntimeDefinedParameter (
$ParameterName,
[string[]],
$ParamAttributes
)
}
}
return $ParamDictionary
}
process {
$PSBoundParameters
}
}
Крутая вещь об этом состоит в том, что он может продолжать работать, пока есть папки, и он автоматически выполняет проверку параметров. Конечно, вы нарушаете законы.NET, используя рефлексию, чтобы привлечь всех этих частных пользователей, поэтому я бы посчитал это ужасным и хрупким решением, независимо от того, как это было весело.