Jaki jest zalecany styl kodowania dla programu PowerShell?


79

Czy jest jakiś zalecany styl kodowania, jak pisać skrypty PowerShell?

To nie o tym, jak struktura kodu (jak wiele funkcji, jeśli użyć modułu, ...). Chodzi o „ jak napisać kod, aby był czytelny ”.

W językach programowania jest kilka zalecanych stylów kodowania (co wciskać , jak wciskać - spacje / tabulatory, gdzie zrobić nowy wiersz , gdzie umieścić nawiasy klamrowe , ...), ale nie widziałem żadnych sugestii dotyczących PowerShell.

Szczególnie interesuje mnie:


Jak pisać parametry

function New-XYZItem
  ( [string] $ItemName
  , [scriptblock] $definition
  ) { ...

(Widzę, że bardziej przypomina składnię „V1”)

lub

function New-PSClass  {
  param([string] $ClassName
       ,[scriptblock] $definition
  )...

lub (dlaczego dodać pusty atrybut?)

function New-PSClass  {
  param([Parameter()][string] $ClassName
       ,[Parameter()][scriptblock] $definition
  )...

lub (inne formatowanie, które widziałem być może w kodzie Jaykula)

function New-PSClass {
  param(
        [Parameter()]
        [string]
        $ClassName
        ,
        [Parameter()]
        [scriptblock]
        $definition
  )...

czy ...?


Jak napisać złożony potok

Get-SomeData -param1 abc -param2 xyz | % {
    $temp1 = $_
    1..100 | % {
      Process-somehow $temp1 $_
    }
  } | % {
    Process-Again $_
  } |
  Sort-Object -desc

lub (nazwa polecenia cmdlet w nowej linii)

Get-SomeData -param1 abc -param2 xyz |
  % {
    $temp1 = $_
    1..100 |
      % {
        Process-somehow $temp1 $_
      }
  } |
  % {
    Process-Again $_
  } |
  Sort-Object -desc |

A co, jeśli istnieją -begin, -processoraz -endparametry? Jak sprawić, by był jak najbardziej czytelny?

Get-SomeData -param1 abc -param2 xyz |
  % -begin {
     init
  } -process {
     Process-somehow2 ...
  } -end {
     Process-somehow3 ...
  } |
  % -begin {
  } ....

lub

Get-SomeData -param1 abc -param2 xyz |
  %  `
    -begin {
      init
    } `
    -process {
      Process-somehow2 ...
    } `
    -end {
      Process-somehow3 ...
    } |
  % -begin {
  } ....

Wcięcie jest tutaj ważne, a także jaki element zostanie umieszczony w nowej linii.


Omówiłem tylko pytania, które przychodzą mi do głowy bardzo często. Jest kilka innych, ale chciałbym, aby to pytanie dotyczące przepełnienia stosu było „krótkie”.

Wszelkie inne sugestie są mile widziane.


2
Myślę, że brak wspólnego stylu kodowania dla skryptów PowerShell wynika z faktu, że jest on bardziej związany z używaniem przez administratora, a nie z „prawdziwym” kodowaniem.
Filburt

5
Masz rację. Jednak administratorzy imho potrzebują skryptów, które są łatwe do odczytania. Na przykład nie lubię grawerunków, więc staram się ich unikać.
stej

2
To świetne pytanie.
Steve Rathbone

To dziwne, że pomimo stosów pogardy, nadal jest to kod; a zatem można je uczynić bardziej czytelnymi dzięki standardowemu i znanemu schematowi wcięć.
user2066657

Odpowiedzi:


89

Po kilku latach nurkowania w PowerShell v2.0, oto, na czym się zdecydowałem:

<#
.SYNOPSIS
Cmdlet help is awesome.  Autogenerate via a template so I never forget.

.DESCRIPTION
.PARAMETER
.PARAMETER
.INPUTS
.OUTPUTS
.EXAMPLE
.EXAMPLE
.LINK
#>
function Get-Widget
{
    [CmdletBinding()]
    param (
        # Think about which parameters users might loop over.  If there is a clear
        # favorite (80/20 rule), make it ValueFromPipeline and name it InputObject.
        [parameter(ValueFromPipeline=$True)]
        [alias("Server")]
        [string]$InputObject,

        # All other loop candidates are marked pipeline-able by property name.  Use Aliases to ensure the most 
        # common objects users want to feed in will "just work".
        [parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$True)]
        [alias("FullName")]
        [alias("Path")]
        [string[]]$Name,

        # Provide and document defaults for optional parameters whenever possible.
        [parameter(Position=1)]
        [int]$Minimum = 0,

        [parameter(Position=2)]
        [int]$ComputerName = "localhost",

        # Stick to standardized parameter names when possible.  *Especially* with switches.  Use Aliases to support 
        # domain-specific terminology and/or when you want to expose the parameter name of the .Net API you're wrapping.
        [parameter()]
        [Alias("IncludeFlibbles")]
        [switch]$All,
    )

    # The three main function blocks use this format if & only if they are short one-liners    
    begin { $buf = new-list string }

    # Otherwise they use spacing comparable to a C# method
    process    
    {
        # Likewise, control flow statements have a special style for one-liners
        try
        {
            # Side Note: internal variables (which may be inherited from a parent scope)  
            # are lowerCamelCase.  Direct parameters are UpperCamelCase.
            if ($All)
                { $flibbles = $Name | Get-Flibble }   
            elseif ($Minimum -eq 0)          
                { $flibbles = @() }
            else
                { return }                       

            $path = $Name |
                ? { $_.Length -gt $Minimum } |
                % { $InputObject.InvokeGetAPI($_, $flibbles) } |
                ConvertTo-FullPath
        }
        finally { Cleanup }

        # In general, though, control flow statements also stick to the C# style guidelines
        while($true)
        {
            Do-Something
            if ($true)
            {
                try
                {
                    Do-Something
                    Do-Something
                    $buf.Add("abc")
                }
                catch
                {
                    Do-Something
                    Do-Something
                }
            }            
        }    
    }    
}

<# 
Pipelines are a form of control flow, of course, and in my opinion the most important.  Let's go 
into more detail.

I find my code looks more consistent when I use the pipeline to nudge all of PowerShell's supported 
language constructs (within reason) toward an "infix" style, regardless of their legacy origin.  At the 
same time, I get really strict about avoiding complexity within each line.  My style encourages a long,
consistent "flow" of command-to-command-to-command, so we can ensure ample whitespace while remaining
quite compact for a .NET language. 

Note - from here on out I use aliases for the most common pipeline-aware cmdlets in my stable of 
tools.  Quick extract from my "meta-script" module definition:
sal ?? Invoke-Coalescing
sal ?: Invoke-Ternary
sal im Invoke-Method
sal gpv Get-PropertyValue
sal spv Set-PropertyValue
sal tp Test-Path2
sal so Select-Object2        
sal eo Expand-Object        

% and ? are your familiar friends.
Anything else that begins with a ? is a pseudo-infix operator autogenerated from the Posh syntax reference.
#>        
function PipelineExamples
{
    # Only the very simplest pipes get to be one-liners:
    $profileInfo = dir $profile | so @{Path="fullname"; KBs={$_.length/1kb}}
    $notNull = $someString | ?? ""        
    $type = $InputObject -is [Type] | ?: $InputObject $InputObject.GetType()        
    $ComObject | spv Enabled $true
    $foo | im PrivateAPI($param1, $param2)
    if ($path | tp -Unc)
        { Do-Something }

    # Any time the LHS is a collection (i.e. we're going to loop), the pipe character ends the line, even 
    # when the expression looks simple.
    $verySlowConcat = ""            
    $buf |
        % { $verySlowConcat += $_ }
    # Always put a comment on pipelines that have uncaptured output [destined for the caller's pipeline]
    $buf |
        ? { $_ -like "*a*" }


    # Multi-line blocks inside a pipeline:
    $orders |
        ? { 
            $_.SaleDate -gt $thisQuarter -and
            ($_ | Get-Customer | Test-Profitable) -and
            $_.TastesGreat -and
            $_.LessFilling
        } |
        so Widgets |        
        % {                
            if ($ReviewCompetition)
            {
                $otherFirms |
                    Get-Factory |
                    Get-ManufactureHistory -Filter $_ |
                    so HistoryEntry.Items.Widgets                     
            }
            else
            {
                $_
            }
        } |            
        Publish-WidgetReport -Format HTML


    # Mix COM, reflection, native commands, etc. seamlessly
    $flibble = Get-WmiObject SomethingReallyOpaque |
        spv AuthFlags 0xf -PassThru |
        im Put() -PassThru |
        gpv Flibbles |
        select -first 1

    # The coalescing operator is particularly well suited to this sort of thing
    $initializeMe = $OptionalParam |
        ?? $MandatoryParam.PropertyThatMightBeNullOrEmpty |
        ?? { pwd | Get-Something -Mode Expensive } |
        ?? { throw "Unable to determine your blahblah" }           
    $uncFolderPath = $someInput |
        Convert-Path -ea 0 |
        ?? $fallback { tp -Unc -Folder }

    # String manipulation        
    $myName = "First{0}   Last{1}   " |
        ?+ "Suffix{2}" |
        ?replace "{", ": {" |
        ?f {eo richard berg jr | im ToUpper}            

    # Math algorithms written in this style start to approach the elegance of functional languages
    $weightedAvg = $values |
        Linq-Zip $weights {$args[0] * $args[1]} |
        Linq-Sum |
        ?/ ($weights | Linq-Sum)
}

# Don't be afraid to define helper functions.  Thanks to the script:Name syntax, you don't have to cram them into 
# the begin{} block or anything like that.  Name, parameters, etc don't always need to follow the cmdlet guidelines.
# Note that variables from outer scopes are automatically available.  (even if we're in another file!)
function script:Cleanup { $buf.Clear() }

# In these small helpers where the logic is straightforward and the correct behavior well known, I occasionally 
# condense the indentation to something in between the "one liner" and "Microsoft C# guideline" styles
filter script:FixComputerName
{
    if ($ComputerName -and $_) {            
        # Handle UNC paths 
        if ($_[1] -eq "\") {   
            $uncHost = ($_ -split "\\")[2]
            $_.Replace($uncHost, $ComputerName)
        } else {
            $drive = $_[0]
            $pathUnderDrive = $_.Remove(0,3)            
            "\\$ComputerName\$drive`$\$pathUnderDrive"
        }
    } else {
        $_
    }
}

Podświetlacz składni Stack Overflow całkowicie mnie rezygnuje. Wklej go do ISE.


1
Dzięki za wyczerpującą odpowiedź; Prawdopodobnie oznaczę to jako zaakceptowaną odpowiedź, wydaje się, że nikt inny nie jest zainteresowany eleganckim stylem kodowania: | Czy opublikowałeś gdzieś swoje funkcje pomocnicze (??,?:,? +, Im, ...)? - myślę, że dla wielu byłoby to wartościowe;)
stej

Nie, nie ... tak, powinienem ... kiedyś ...!
Richard Berg,

3
Ok, popełnił v0.1 gdzieś publicznie. Wejdź na tfstoys.codeplex.com/SourceControl/changeset/view/33350#605701 i przejdź do Modules \ RichardBerg-Misc
Richard Berg

Punkt do dodania do tego wspaniałego przewodnika: używaj walidatorów tam, gdzie to konieczne! Oszczędzają na kodzie i poprawiają użyteczność.
JasonMArcher

Skrypt wdrożeniowy kolegi kiedyś się zepsuł, ponieważ w profilu miałem niestandardowy alias „ls”. Od tamtej pory moją praktyką jest „nie używaj aliasów w skryptach”
John Fouhy

15

Uważam, że najbardziej wszechstronnym zasobem dotyczącym stylu kodowania dla programu PowerShell nadal jest Przewodnik po najlepszych praktykach i stylu programu PowerShell .

Od ich wprowadzenia:

Podobnie jak angielskie reguły pisowni i gramatyki, najlepsze praktyki programowania PowerShell i reguły stylów prawie zawsze mają wyjątki, ale dokumentujemy podstawy struktury kodu, projektowania poleceń, programowania, formatowania, a nawet stylu, które pomogą uniknąć typowych problemów i pomogą piszesz kod nadający się do wielokrotnego użytku i czytelny - ponieważ kod wielokrotnego użytku nie musi być przepisywany, a czytelny kod można zachować.

Udostępnili również następujące linki do GitBooka :


404: Linki są zepsute.
Ashish Singh

Naprawiony. Ten nowy przewodnik został utworzony przez połączenie starego przewodnika po stylu programu PowerShell, autorstwa Carlosa Pereza (z którym pierwotnie się łączyłem) i The Community Book of PowerShell Practices , autorstwa Dona Jonesa i Matta Penny'ego.
rsenna

4
Ta odpowiedź naprawdę powinna być teraz wyższa.
Bacon Bits

8

Niedawno natrafiłem na doskonały punkt dotyczący stylu wcięć w PowerShell . Jak stwierdza linkowany komentarz, zwróć uwagę na różnicę między tymi samymi składniami:

1..10 | Sort-Object
{
    -$_
}

i

1..10 | Sort-Object {
    -$_
}

Chociaż mam skłonność do „robienia tego, co robią Rzymianie” i używania standardowego stylu wcięć C # ( mniej więcej Allman ), nie zgadzam się z tym wyjątkiem i innymi podobnymi.

To skłania mnie osobiście do korzystania z mojego ulubionego 1TBS , ale mogę być przekonany, że jest inaczej. Jak się ustatkowałeś z ciekawości?


2
Jestem całkiem nowy w szykownym. Dziękuję za ostrzeżenie! Na początku podobała mi się osobna linia, ale teraz lubię otwierać kręcone na linii konfiguracji.
AnneTheAgile

Możesz napotkać niechęć ze strony programistów .NET używających standardu C #, ale kiedy wcięcie zmienia funkcję, idę z tym, co będzie konsekwentnie robić to, czego oczekuje się od wszelkich preferencji religijnych, w dowolnym momencie. Zwykle wolę 1 TB na wszystko, ale jeśli powyższy przykład wykazywałby odwrotne zachowanie, cały mój PoSh zostałby Allmanizowany w mgnieniu oka. :)
Tohuw

1
@KeithSGarner raczej sugeruję, że zachowanie powinno dyktować styl. Albo lepiej, język powinien być agnostykiem stylu.
Tohuw

1
W przypadku przykładu if (& lt; test & gt;) {StatementBlock} język dopuszcza dowolny styl (1TBS lub Allman), nie jest to problem z zachowaniem. (Wolę Allman siebie, ale „Gdy w Rzymie ...”) Jak na przykład Sort-Object powyżej, to nie to kwestia stylu, jest tylko jedna poprawna odpowiedź w zależności od wymaganego zachowania. Styl! = Zachowanie
Keith S Garner

1
„istnieje tylko jedna poprawna odpowiedź w zależności od wymaganego zachowania”. Być może czegoś mi brakuje, ponieważ dokładnie to mam na myśli: jest tylko jeden właściwy sposób, aby to zrobić, a zatem sposób, w jaki tworzysz polecenie, jest podyktowany przez dane wyjściowe, których potrzebujesz. Ponieważ ma to wpływ na to, na co możesz wpłynąć przy wyborze stylu (tj. Gdzie są podziały linii), moim preferowanym wyborem jest taki, który nie ma szans wywołać nieoczekiwanych zachowań. Uważam, że nie różni się to od jakiegokolwiek innego stylu, który ma na celu uniknięcie niepożądanych zachowań językowych.
Tohuw
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.