2017-03-13 4 views
2

Ausgangspunkt: sehr restriktive Umgebung, Windows 7 SP1, Powershell 3.0. Begrenzte oder keine Möglichkeit, externe Bibliotheken zu verwenden.Dynamischer Parameterwert in Abhängigkeit von einem anderen dynamischen Parameterwert

Ich versuche, ein Bash-Tool, das ich zuvor erstellt habe, neu zu schreiben, diesmal mit PowerShell. In bash habe ich Autocompletion implementiert, um das Tool nutzerfreundlicher zu machen, und ich möchte das Gleiche für die PowerShell-Version tun.

Die Bash-Version wie folgt gearbeitet:

./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.). 

Wie Sie sehen können, ist alles dynamisch war. Die Liste der Umgebungen war dynamisch, die Liste der Anwendungen war dynamisch, abhängig von der ausgewählten Umgebung, die Liste der Befehle war ebenfalls dynamisch.

Das Problem ist mit dem Test → Anwendungsverbindung. Ich möchte die korrekte Anwendung basierend auf der bereits vom Benutzer ausgewählten Umgebung anzeigen.

Mit PowerShell DynamicParam kann ich eine dynamische Liste von Umgebungen basierend auf einer Ordnerliste erhalten. Ich kann jedoch nicht (oder zumindest habe ich nicht herausgefunden, wie ich das machen soll) eine andere Ordnerliste machen, aber dieses Mal benutze ich eine Variable basierend auf der vorhandenen Benutzerauswahl.

Aktuelle Code:

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 { 
    } 
} 
+0

Wissen Sie, welche Version von PowerShell-Benutzern der Funktion haben wird? Ich würde die Verwendung von Argument-Completers empfehlen, die mit Version 5+ geliefert werden und für 3 und 4 mit dem TabExpansionPlusPlus-Modul verfügbar sind. –

+0

Powershell v3.0 und ich haben keinen administrativen Zugriff. Außerdem würde ich lieber genau das umsetzen, was ich von irgendwelchen Bibliotheken benötige, da die Erlaubnis, etwas Äußeres zu verwenden, ziemlich begrenzt ist. – oblio

Antwort

2

Ich würde empfehlen Argument Beender verwenden, die in Powershell halb ausgesetzt sind 3 und 4, und voll in der Version 5.0 und höher ausgesetzt. Für v3 und v4 ist die zugrunde liegende Funktionalität vorhanden, Sie müssen jedoch die integrierte TabExpansion2-Funktion überschreiben, um sie zu verwenden. Das ist in Ordnung für Ihre eigene Sitzung, aber es ist in der Regel verpönt, Tools zu verteilen, die das in Sitzungen anderer Leute tun (stellen Sie sich vor, wenn jeder versucht, diese Funktion zu überschreiben). Ein PowerShell-Teammitglied verfügt über ein Modul, das dies für Sie erledigt: TabExpansionPlusPlus. Ich weiß, ich sagte, TabExpansion2 war schlecht, aber es ist OK, wenn dieses Modul es tut :)

Wenn ich die Versionen 3 und 4 unterstützen musste, würde ich meine Befehle in Modulen verteilen und die Module auf die Existenz von überprüfen lassen der Befehl 'Register-ArgumentCompleter', der ein Cmdlet in Version 5 + ist und eine Funktion für das TE ++ - Modul ist. Wenn das Modul es gefunden hat, würde es alle Completer registrieren, und wenn dies nicht der Fall wäre, würde es den Benutzer darüber informieren, dass die Argumentvervollständigung nicht funktionieren würde, wenn sie nicht das Modul TabExpansionPlusPlus erhalten hätten.

Vorausgesetzt, dass Sie die TE ++ Modul oder pSV5 + haben, ich denke, das sollten Sie sich auf dem richtigen Weg:

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', 
       $_ 
      ) 
     } 
    } 
} 

Damit dies funktioniert, Ihr aktuelles Arbeitsverzeichnis ein Verzeichnis darin enthaltenen ‚Konfigurationen‘ müssen , und Sie werden mindestens drei Ebenen von Unterverzeichnissen benötigen (wenn Sie Ihr Beispiel durchlesen, sah es so aus, als ob Sie ein Verzeichnis aufzählen würden, und Sie würden tiefer in diese Struktur einsteigen, wenn Parameter hinzugefügt wurden). Die Aufzählung des Verzeichnisses ist im Moment nicht sehr intelligent, und Sie können es ziemlich einfach täuschen, wenn Sie einfach einen Parameter überspringen, z. B. launcher -Environment3 <TAB> würde versuchen, Ihnen Komplettierungen für das erste Unterverzeichnis zu geben.

Dies funktioniert, wenn Sie immer drei Parameter zur Verfügung haben. Wenn Sie eine variable Anzahl von Parametern benötigen, können Sie noch Completers verwenden, aber es könnte etwas komplizierter werden.

Der größte Nachteil wäre, dass Sie die Eingaben der Benutzer noch validieren müssen, da es sich bei den Completers im Grunde nur um Vorschläge handelt und die Benutzer diese Vorschläge nicht verwenden müssen.

Wenn Sie dynamische Parameter verwenden möchten, wird es ziemlich verrückt. Es mag einen besseren Weg geben, aber ich war noch nie in der Lage, den Wert von dynamischen Parametern in der Befehlszeile zu sehen, ohne Reflektion zu verwenden, und an diesem Punkt verwenden Sie Funktionen, die sich bei der nächsten Version ändern könnten (die Mitglieder sind normalerweise t öffentlich aus einem Grund). Es ist verlockend zu versuchen, $ MyInvocation im Block DynamicParam {} zu verwenden, aber zu dem Zeitpunkt, zu dem der Benutzer den Befehl in die Befehlszeile eingibt, wird er nicht ausgefüllt, und es wird nur eine Zeile des Befehls angezeigt, ohne dass eine Spiegelung verwendet wird.

Der untenstehende wurde auf PowerShell 5.1 getestet, so kann ich nicht garantieren, dass jede andere Version genau dieselben Klassenmitglieder hat (es basiert auf etwas, das ich zuerst sah Garrett Serack tun). Wie im vorherigen Beispiel hängt es von einem. \ Configurations-Ordner im aktuellen Arbeitsverzeichnis ab (wenn es keins gibt, werden Sie keine -Environment-Parameter sehen).

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 
    } 
} 

Die kühle Sache über dieses ist, dass es so lange weitermachen können, da es Ordner sind, und es funktioniert automatisch Parametervalidierung. Natürlich brechen Sie die .NET-Gesetze, indem Sie Reflektionen nutzen, um all diese privaten Mitglieder zu erreichen, also würde ich das als eine schreckliche und zerbrechliche Lösung betrachten, egal wie lustig es war, sich zu entschließen.

+0

Whoa! Eine wirklich gründliche Antwort. Ja, leider werde ich ziemlich viele Räder neu erfinden müssen, wahrscheinlich sehr schlecht. Ich werde es testen und ich werde es akzeptieren, sobald ich eine funktionierende Lösung habe :) Ich weiß, dass die resultierende Lösung ziemlich spröde sein wird, aber ich habe keine wirklich Angst davor, der Wechsel der Technik passiert hier in einem glazialen Tempo, sehe ich nicht, dass sie in naher Zukunft zu Powershell> 5 gehen werden. – oblio

+0

Ich habe mich herausgepickt. Ich habe die Parameterabhängigkeit dadurch gehackt, dass die Umgebungsliste basierend auf dem Startordner einen dynamischen Validierungssatz enthält. Die App-Liste verwendet einen fest codierten Umgebungswert test, da keine Anwendung eine andere Umgebung erreichen kann, ohne in prod zu sein (mal sehen, wie gut die Annahme hält!). Der letzte Parameter hängt nicht wirklich von den ersten ab. Die endgültige Version des Codes verwendet 3 dynamische Parameter basierend auf: https://blogs.technet.microsoft.com/pstips/2014/06/09/dynamic-validateset-in-a-dynamic-parameter/, nur mit 1 Funktion hinzugefügt, um die Dinge trocken zu halten. – oblio

Verwandte Themen