Contents of this directory is archived and no longer updated.

Продолжаю начатую в прошлый раз цикл постов про гæдлайны оформления скриптов в PowerShell или «учимся писать скрипты». Сегодня мы поговорим о таких вещах, как оформление скриптов, правила именования скриптов/функций и поговорим о типах скриптов и функций — простых и расширенных (advanced).

Оформление скриптов — шляпа

Правильно написанный скрипт, который даже в теории может быть использован кем-то другим (или прочитан), должен содержать шапку. В шапке указывается название скрипта, краткое описание его работы (функций), версия скрипта, дата последнего изменения, автор скрипта и какие-то контактные данные, если потребуется связаться с автором кода. Вот примерный шаблон этой самой шапки:

<#
Project Name
Version 0.95c

Description: the script sends you too far.

Vadims Podans (c) 2011
http://www.sysadmins.lv/
#>

Формат даты зависит от того, как часто изменяется файл. Дата должна отражать наибольшую величину, в пределах которой может быть только одна ревизия скрипта. Т.е. если он меняется чаще, чем раз в год, следует добавлять месяц. Если чаще, чем раз в месяц, лучше указать конкретную дату ревизии и т.д. Контактные данные должны быть: веб-сайт и/или почтовый адрес автора (если скрипт предназначен для незнакомых лиц, например, для публикации в блоге). Если скрипт пишется команды людей занятых в одном общем проекте, можно указать и номера телефонов. Вобщем, всё зависит от ситуации. Главное — чтобы автора кода можно было бы как-то найти, если возникнут вопросы.

Именование функций или скриптов

Одна из особенностей PowerShell — унифицированный синтаксис. Как минимум все встроенные командлеты обзываются по простому принципу: Действие-Объект. Действие определяет что будет происходить с объектом и объект — это объект, над которым выполняется действие. Очевидных примеров примерно овер9000 — Copy-Item, Get-WMIObject и т.д. Можно даже не смотреть в справку, чтобы понять хотя бы примерно, что делает тот или иной командлет. Правильно написанный скрипт должен обзываться по такому же принципу. PowerShell определяет целый ряд разрешённых действий, которые следует применять. Например, ваш скрипт что-то удаляет — действие должно называться Remove, а не Delete или что-то ещё. Если вы не уверены в том, подпадает ли ваше название действия под разрешённое, вы всегда можете себя проверить выполнив команду Get-Verb. Если вы в списке не нашли такого, какой вы хотите — старайтесь использовать стандартное действие, которое максимально приближено по смыслу к вашему. Возвращаясь к нашему примеру, максимально приближенное действие к Delete является Remove.

Вот пример плохого названия скрипта/функции, написанного пкитимом (это который Windows PKI team): http://technet.microsoft.com/en-us/library/ff961506(WS.10).aspx. Я буду неоднократно ссылаться на него, как годный экземпляр несоблюдения гайдлайнов оформления скрипта (хотя, свою задачу он выполняет как положено :-))

Т.е. вместо PKISync.ps1, скрипт следовало бы назвать Sync-PKI.ps1. Стандартов по описанию объектов почти нету, кроме одного — название объекта, над которым совершается действие должно быть в единственном числе. Не Remove-Users, а Remove-User, даже если ваш скрипт предполагает удаление множества пользователей. Всегда и во всех случаях, название объекта должно быть в единственном числе. Стандартизация именования всего и вся — это уже очень много и даёт как минимум +3 поинта вашим скриптинг скиллзам.

Функции — простые и расширенные функции

В предыдущий раз я говорил исключительно про расширенные или advanced функции, хотя и упоминал, что функционал возможно реализовать силами простых функций. Почему так? Простые функции изжили себя? Они не подпадают под бест-практисы? Вовсе нет. Дело в том, что простые функции (единственный тип функций, который был доступен в PowerShell 1.0) обладают достаточно небольшим функционалом, не позволяют создавать cmdlet-стайл справку и возлагают на пользователя всю работу по обработке параметров — взаимодействия с пользователем. С появлением расширенных функций, солидную часть этой работы мы можем обратно свалить на эти самые функции. При этом работа с такими функциями внешне неотличима от работы с родными командлетами.

Проблема появилась, когда пользователи стали писать свои скрипты и засорять имивыкладывать их в интернеты, чтобы другие могли воспользоваться. Функционально многие были очень полезными, а какие-то не очень. Но оформление скриптов, определение и парсинг параметров в большинстве случаев был просто ужасен. Каждый писал так, как он мог или умел. Думаете, что только рядовые пользователи грешили этим? Отнюдь! Практически каждая продакт-группа Microsoft писала свои скрипты так, что лучше бы не писали. В качестве примера можете посмотреть пример уже упомянутого скрипта PKISync.ps1. Посмотрите первые 70 строк и вы всё поймёте. Поэтому не обязательно считать себя самым главным неудачником в этой жизни. Вы не одни :-)

Кстати говоря: ряд продуктовых команд Microsoft (в особенности Windows PKI team) люто-бешено не переваривают PowerShell и применяют его только потому что их заставляют. Будь их воля, они бы так и жили в мире cmd и VBS. Я очень надеюсь, что они не читают мой русскоязычный бложек, иначе доступ к ТЗ мне будет резко закрыт :-)

Итак, простые и расширенные функции — что и когда применяем. Ту часть скрипта, которая отвечает за взаимодействие с пользователем (это определение параметров, их возможности и встроенная справка) необходимо оформлять только и только в виде расширенных функций. Рассмотрим простой пример. У нас есть функция, которая принимает 2 аргумента, один из них должен быть обязательно указан. Вот как примерно выглядит код простой функции:

function Test-Me {
    param(
        [string]$arg1 = $(throw "The parameter must not be empty"),
        [string]$arg2
    )
    # some stuff
}

Заметка: это ещё хороший вариант кода. Вышеупомянутый скрипт PKISync.ps1 вообще явно не определяет аргументы а парсит переменную $args, куда все аргументы падают.

При этом, весьма неочевидно, какой из них не может быть пустым (это было в PS 1.0). Или его имя явно указывать в скриптоблоке throw.

[↓] [vPodans] Test-Me
The parameter must not be empty
At line:3 char:32
+         [string]$arg1 = $(throw <<<<  "The parameter must not be empty"),
    + CategoryInfo          : OperationStopped: (The parameter must not be empty:String) [], RuntimeException
    + FullyQualifiedErrorId : The parameter must not be empty

[↓] [vPodans]

Мы бы хотели более дружелюбное поведение, как у родных командлетов — любезно попросить пользователя указать обязательный параметр. Как пример, выполните Remove-Variable без указания какого-либо аргумента. При этом, все аргументы определённые в секции param() не могут принимать данные из конвейера. Это всё потому, что данные из конвейера приходили в динамическую (которую нельзя чётко определить) переменную — $_. Плюс, полное отсутствие возможности встроить справку, чтобы она была доступна при вызове команды "Get-Help Test-Me". Список недостатков простых функций бесконечный.

Следовательно, основную функцию надо оформлять только в виде расширенной функции. Что делает функцию расширенной?

  • [CmdletBinding()] в самом начале скрипта;
  • Применение параметра аргумента.

изменим наш код до вида расширенной функции:

function Test-Me {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$arg1,
        [string]$arg2
    )
    Write-Host My arg1 is "'$arg1'"
}
[↓] [vPodans] Test-Me

cmdlet Test-Me at command pipeline position 1
Supply values for the following parameters:
arg1: мимими
My arg1 is 'мимими'
[↓] [vPodans]

Эпик! Вы не отличите внешнее поведение нашей функции от того же Remove-Variable. А что мы сделали? Мы преобразовали простую функцию в расширенную (путём добавления [CmdletBinding()] или конструкции в квадартных скобках в секции param(), которая определяет дополнительные требования к переменной.

Причём, мы можем сказать, что переменная $arg1 должна принимать значения из конвейера:

function Test-Me {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$arg1,
        [string]$arg2
    )
    Write-Host My arg1 is "'$arg1'"
}
[↓] [vPodans] "няяяяя!!!" | Test-Me
My arg1 is 'няяяяя!!!'
[↓] [vPodans]

Как видите, в коде изменилось только то, что мы явно разрешили параметру arg1 принимать аргументы из конвейера. Так же, я выкинул [CmdletBinding()], чтобы продемонстрировать, что простая функция превращается в advanced при применении конструкций в квадратных скобках. О конструкциях в квадратных скобках, известных как параметры аргументов, смотрите встроенную справку:

Get-Help about_Functions_Advanced
Get-Help about_Functions_Advanced_CmdletBindingAttribute
Get-Help about_Functions_Advanced_Methods
Get-Help about_Functions_Advanced_Parameters

Примечание: мой совет, всегда указывайте [CmdletBinding()] в начале скрипта (должен идти до секции param()), чтобы можно было сразу сказать, что это расширенная функция. Так же, учтите, что если эта конструкция указана, но ваша функция никаких аргументов не принимает, вы должны прописать секцию param(). Хотя бы пустую.

Надо ли понимать, что простые функции изжили себя? Отнюдь! Простые функции до сих пор используются как минимум в качестве вспомогательных функций. Если посмотрите мои скрипты, вспомогательные функции используются достаточно активно. Суть вспомогательной функции — повсеместное использование определённого кода несколько раз в теле основной функции. Например, реальный пример из моей жизни. Я использую какие-то API, которые возвращают Unicode строку в виде байтового массива. Причём эти API используются несколько раз. Чтобы каждый раз не писать конвертер, который преобразует этот байтовый массив в нормальную строку (плюс, сначала, преобразует little-endian последовательность в big-endian):

function Backup-CertificationAuthority {
[CmdletBinding()]
    param (
        # тут разные аргументы
    )
    function Split-DataPath ([Byte[]]$Bytes) {
        $SB = New-Object System.Text.StringBuilder
        $bytes1 = $bytes | %{"{0:X2}" -f $_}
        for ($n = 0; $n -lt $bytes1.count; $n = $n + 2) {
            [void]$SB.Append([char](Invoke-Expression 0x$(($bytes1[$n+1]) + ($bytes1[$n]))))
        }
        $SB.ToString().Split("`0",[StringSplitOptions]::RemoveEmptyEntries)
    }
    # тут всяко-разный код
    $Bytes = # тут получаем наши байтики из API
    $FileName = Split-BackupPath $Bytes
}

в данном случае я точно знаю, что при вызове вспомогательной функции, в переменной $Bytes всегда будут байты. Это будет реализовано на уровне кода (здесь не показано). Поэтому я использую расширенный тип функции для основной (которая взаимодействует с пользователем), а вспомогательные функции делаю простыми.

На сегодня всё. Я буду стараться писать по немножку, чтобы вы могли спокойно обдумать и переварить материал. В следующий раз продолжим.


Share this article:

Comments:

rublog.alex-trofimov.com

>>Контактные данные должны быть: веб-сайт и/или почтовый адрес автора (если... Тогда уже и дисклеймер включай. А то будут потом претензии предъявлять ;)

www.google.com/accounts/o8/id?id=AItOawmbY8T_dnZKI2pfn5ud4iJuhkHHzk8vcBo

"в данном случа " Мне кажется лучше было бы всё таки разделять различные темы по разным постам. Так простые vs. advanced - первый, именование функций и параметров - второй, встроенная справка - третий.

Vadims Podāns

Я заранее предупреждал, что особой последовательности в темах постов не будет.

anon

Вадим, у Вас случайно на бложике не стоит чудо поганенький плагин supercache или тому подобное? Почему-то мои браузеры намертво забивают в кеш страницу и календарь за последние месяца отказываются обновлять (ИЕ, ФФ). Такой ерундой страдали как раз такие плагины.

Vadims Podāns

вы не поверите, в этом блоге нету вообще никаких плагинов, кроме postrank (и тот встроен вручную). Но он на кеш не влияет. Я не могу сказать, в чём проблема.

Comments are closed.