Який рекомендований стиль кодування для 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 {
  } ....

Тут важливий відступ, а також те, який елемент буде розміщено на новому рядку.


Я висвітлював лише ті питання, які дуже часто виникають у мене в голові. Є й деякі інші, але я хотів би, щоб це запитання щодо переповнення стека було коротким.

Будь-які інші пропозиції вітаються.


1
Я думаю, відсутність загального стилю кодування для скриптів PowerShell пов'язана з тим фактом, що він більше пов'язаний із використанням адміністратора замість "реального" кодування.
Filburt

4
Ти правий. Однак адміністраторам imho потрібні скрипти, які легко читати. Наприклад, мені не подобаються зворотні позначки, тому я намагаюся їх уникати.
stej

2
Це чудове питання.
Steve Rathbone 02

Дивно, що, незважаючи на купи презирства, це все одно код; і, отже, можна зробити більш читабельним за допомогою стандартної та звичної схеми відступу.
user2066657

Відповіді:


88

Провівши пару років, занурившись досить глибоко в 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.


Дякуємо за всебічну відповідь; Я, мабуть, позначу це як прийняту відповідь, схоже, ніхто інший не цікавиться стилем кодування Posh: | Ви опублікували десь свої допоміжні функції (??,?:,? +, Im, ...)? - це було б цінним для багатьох людей;)
stej

Ні, я не ... так, я повинен ... одного дня ...!
Richard Berg

3
Добре, здійснено v0.1 десь загальнодоступно. Перейдіть на сторінку tfstoys.codeplex.com/SourceControl/changeset/view/33350#605701 та перейдіть до Modules \ RichardBerg-Misc
Richard Berg

Слід додати до цього чудового посібника: використовуйте валідатори, де це потрібно! Вони економить на коді та покращують зручність використання.
JasonMArcher

Одного разу сценарій розгортання колеги зламався для мене, оскільки в моєму профілі був спеціальний псевдонім ls. З тих пір у моїй практиці було "не використовувати псевдоніми в сценаріях"
Джон Фухі,

14

Я вважаю, що найповнішим ресурсом стилю кодування для PowerShell все ще є Посібник із найкращих практик і стилів PowerShell .

З моменту їх введення:

Як і англійські правила орфографії та граматики, найкращі практики програмування та правила стилів PowerShell майже завжди мають винятки, але ми документуємо базову структуру коду, дизайн команд, програмування, форматування та навіть стиль, що допоможе вам уникнути поширених проблем та допоможе ви пишете більш багаторазовий, читабельний код - тому що код багаторазового використання не потрібно переписувати, і читабельний код можна підтримувати.

Вони також зробили доступними ці посилання на GitBook :


404: Посилання порушено.
Ashish Singh

Виправлено. Цей новий посібник був створений шляхом об’єднання старого Посібника стилю PowerShell від Карлоса Переса (до якого я спочатку посилався) та Книги спільнот практик PowerShell від Дона Джонса та Метта Пенні.
rsenna

4
Зараз ця відповідь справді повинна бути вищою.
Bacon Bits

8

Нещодавно я натрапив на чудову думку про стиль відступу в PowerShell . Оскільки зв’язаний коментар зазначає, спостерігайте різницю між цими самими синтаксисами:

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

і

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

Хоча я схильний "робити так, як роблять римляни", і використовувати стандартний стиль відступу C # ( Allman , більш-менш), я сприймаю це виняток та інші подібні до нього.

Це схиляє мене особисто використовувати мою улюблену 1TBS , але я міг би переконатися в протилежному. Як ти влаштувався, з цікавості?


2
Я досить новачок у шикарному. Дякую за голови! Спочатку мені сподобався окремий рядок, але зараз мені подобається відкривати фігурне на рядку налаштування.
AnneTheAgile

Ви можете зіткнутися з ворожнечею у кодерів .NET, що використовують стандарт C #, але коли зміни відступу змінюються, я погоджуюся з тим, що буде послідовно робити те, що очікується від будь-яких релігійних уподобань, у будь-який час. Я, як правило, віддаю перевагу 1TBS для всього, але якби вищенаведений приклад мав зворотну поведінку, усі мої PoSh були б олманізовані в серцебитті. :)
Тоху

Обережно, ви поєднуєте стиль із поведінкою.
Keith S Garner

@KeithSGarner, я маю на увазі, що поведінка повинна диктувати стиль. Або краще, мова повинна бути стилістичною.
Тоху

1
Коли мова йде про приклад if (& lt; test & gt;) {StatementBlock} , мова допускає будь-який стиль (1TBS або Allman), це не проблема поведінки. (Я віддаю перевагу самому Алману, але "Коли в Римі ...") Що стосується прикладу Sort-Object вище, це не проблема стилю, тут є лише одна правильна відповідь залежно від необхідної поведінки. Стиль! = Поведінка
Кіт Гарнер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.