Какой рекомендуемый стиль кодирования для PowerShell?

79

Есть ли рекомендуемый стиль написания сценариев PowerShell?

Дело не в том, как структурировать код (сколько функций, если использовать модуль, ...). Речь идет о том, « как написать код, чтобы он был удобочитаемым ».

В языках программирования есть несколько рекомендуемых стилей кодирования (что делать отступы , как делать отступы - пробелы / табуляторы, где делать новую строку , где ставить фигурные скобки , ...), но я не видел никаких предложений для PowerShell.

Меня особенно интересуют:


Как писать параметры

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

(Я вижу, что это больше похоже на синтаксис V1)

или же

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

или (зачем добавлять пустой атрибут?)

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

или (другое форматирование, которое я видел, возможно, в коде Джайкула)

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

или же ...?


Как написать сложный конвейер

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

или (имя командлета в новой строке)

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

А что , если есть -begin, -processи -endпараметры? Как сделать его максимально читаемым?

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

или же

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

Здесь важен отступ и то, какой элемент ставится на новую строку.


Я ответил только на вопросы, которые приходят мне в голову очень часто. Есть и другие, но я бы хотел, чтобы этот вопрос о переполнении стека был «коротким».

Любые другие предложения приветствуются.

Stej
источник
2
Я предполагаю, что отсутствие общего стиля кодирования для сценариев PowerShell связано с тем, что он больше связан с использованием администратора, а не с «настоящим» кодированием.
Filburt 08
5
Ты прав. Однако администраторам imho нужны легко читаемые сценарии. Например, мне не нравятся обратные кавычки, поэтому я стараюсь их избегать.
Stej 08
2
Это большой вопрос.
Стив Рэтбоун
Странно, что, несмотря на кучу насмешек, это все еще код; и, таким образом, его можно сделать более читабельным с помощью стандартной и знакомой схемы отступов.
user2066657

Ответы:

89

Потратив пару лет на глубокое погружение в PowerShell v2.0, я остановился на следующем:

<#
.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 {
        $_
    }
}

Подсветка синтаксиса Stack Overflow полностью меня разочаровала. Вставьте его в ISE.

Ричард Берг
источник
1
Спасибо за исчерпывающий ответ; Я, наверное, отмечу это как принятый ответ, похоже, никого больше не интересует стиль программирования Posh: | Вы где-нибудь публиковали свои вспомогательные функции (??,?:,? +, Im, ...)? - я думаю, это было бы
полезно
Нет, я не ... да, я должен ... на днях ...!
Ричард Берг
3
Хорошо, совершил v0.1 где-то публично. Перейдите на tfstoys.codeplex.com/SourceControl/changeset/view/33350#605701 и перейдите в Modules \ RichardBerg-Misc
Ричард Берг,
Стоит добавить к этому замечательному руководству: используйте валидаторы там, где это необходимо! Они экономят на коде и улучшают удобство использования.
JasonMArcher
Сценарий развертывания моего коллеги однажды сломался для меня, потому что в моем профиле был настраиваемый псевдоним ls. С тех пор моя практика заключалась в том, чтобы «не использовать псевдонимы в сценариях»
Джон Фухи
15

Я считаю, что наиболее исчерпывающим ресурсом по стилю кодирования для PowerShell по-прежнему является PowerShell Best Practices and Style Guide .

Из их введения:

Подобно правилам орфографии и грамматики английского языка, лучшие практики программирования PowerShell и правила стиля почти всегда имеют исключения, но мы документируем базовый уровень для структуры кода, разработки команд, программирования, форматирования и даже стиля, которые помогут вам избежать распространенных проблем и помогут вы пишете более пригодный для многократного использования, читаемый код, потому что многократно используемый код не нужно переписывать, а читаемый код можно сохранить.

Они также сделали доступными эти ссылки на GitBook :

Рсенна
источник
404: ссылки не работают.
Ашиш Сингх
Исправлена. Это новое руководство было создано путем объединения старого Руководства по стилю PowerShell от Карлоса Переса (с которым я изначально связал) и Книги сообщества по методам PowerShell. от Дона Джонса и Мэтта Пенни.
rsenna
4
Этот ответ действительно должен быть сейчас выше.
Bacon Bits
8

Недавно я наткнулся на отличную мысль о стиле отступа в PowerShell . Как говорится в связанном комментарии, обратите внимание на разницу между этими синтаксисами:

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

и

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

Хотя я склонен «поступать как римляне» и использовать стандартный стиль отступов C # ( более или менее Allman ), я не согласен с этим исключением и другими похожими на него исключениями.

Это побуждает меня лично использовать мои любимые 1TBS , но меня можно было убедить в обратном. Как вы устроились из любопытства?

Tohuw
источник
2
Я новичок в моде. Спасибо за внимание! Сначала мне нравилась отдельная строка, но теперь мне нравится открывать фигурные скобки на строке настройки.
AnneTheAgile
Вы можете столкнуться с враждебностью со стороны программистов .NET, использующих стандарт C #, но когда отступы меняют функцию, я иду с тем, что будет последовательно делать то, что ожидается по любым религиозным предпочтениям, в любое время. Я предпочитаю 1TBS для всего, но если бы в приведенном выше примере было бы обратное поведение, все мои PoSh были бы Allmanized в одно мгновение. :)
Tohuw
1
@KeithSGarner, скорее, я имею в виду, что поведение должно диктовать стиль. Или лучше, язык должен быть независимым от стиля.
Tohuw
1
При работе с примером if (& lt; test & gt;) {StatementBlock} язык допускает любой стиль (1TBS или Allman), это не проблема поведения. (Я сам предпочитаю Allman, но «Когда в Риме ...») Что касается приведенного выше примера Sort-Object, это не проблема стиля, есть только один правильный ответ в зависимости от требуемого поведения. Стиль! = Поведение
Кейт С. Гарнер,
1
«есть только один правильный ответ в зависимости от требуемого поведения». Возможно, мне что-то не хватает, потому что я имею в виду именно это: есть только один правильный способ сделать это, поэтому то, как вы структурируете команду, зависит от того, какой результат вам нужен. Поскольку это влияет на то, на что вы можете повлиять при выборе стиля (например, где находятся разрывы строк), я предпочитаю выбирать стиль, который не имеет шансов вызвать неожиданное поведение. Я считаю, что это ничем не отличается от любого другого выбора стиля, сделанного во избежание нежелательного языкового поведения.
Tohuw