PowerShell: за гранью

от автора

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

Предел — это более психологический барьер, нежели факт. Когда кто-то говорит, что достиг предела в некотором из своих начинаний, можно с уверенностью утверждать, что человек не добился ровным счетом ничего, а некоторый результат — всего лишь промежуточное состояние предмета. Возможно, кто-то припомнит избитую поговорку «нет предела совершенству», на что можно парировать «ничто не совершенно»; совершенство по сути — недостижимая цель, которую человек себе ставит очевидно лишь для того, чтобы наполнить жизнь смыслом. Впрочем, это все риторика, имеющая к делу лишь посредственное отношение.

Развитие PowerShell планомерно, в смысле разработчики изначально заложили в него прочный фундамент, возводя с каждой последующей версией не менее твердую конструкцию. И все же лично мне кажется, что некоторые вещи в PowerShell развиваются несколько не в том направлении. Например, если с отсутствием возможности создавать перечисления мириться можно (в виду наличия хэштаблиц), то как быть со структурами? Разумеется и перечисления и структуры могут быть созданы посредством командлета Add-Type, но лично мне этот способ кажется топорным из-за его расхода времени на компиляцию кода. Найдется еще с десяток прочих аргументов не в пользу использования данного командлета, но соль не в этом. Создатели PowerShell весьма дальновидно предусмотрели расширяемость последнего за счет модулей (если мне не изменяет память, эта возможность появилась во второй версии), тем самым стимулируя разработчиков на различного рода эксперименты как с функциональностью, так и синтаксисом.

Мои эксперименты с PowerShell начинались в пору первой версии последнего. Тогда мной было предпринято несколько попыток расширения возможностей PowerShell за счет компиляции C#-кода, так как командлета Add-Type еще не было; чуть позже возникло желание расширить синтаксис самого PowerShell, но эта затея стала принимать вполне осязаемые черты лишь с переходом на вторую версию, — ключевую роль здесь сыграли именно модули. Впрочем, на все имеющиеся на данный момент наработки повляла одна специфическая черта PowerShell — диски.

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

В языках программирования под словом функция разумеется блок инструкций, в то время как в PowerShell функция — это диск, хранящий определение функции в виде пары имя-скрипт-блок. Это проще продемонстрировать на примере.

PS C:\> function add($a, $b) { $a + $b } PS C:\> dir function: PS C:\> #или чтобы отсеять ненужное PS C:\> dir function:add 

Чтобы посмотреть содержимое функции, используем командлет Get-Content.

PS C:\> gc function:add param($a, $b) $a + $b 

Убеждаемся, что содержимое является скрипт-блоком.

PS C:\> (gc function:add).GetType()  IsPublic IsSerial Name                                     BaseType -------- -------- ----                                     -------- True     False    ScriptBlock                              System.Object 

Иными словами объявление функции в PowerShell является своего рода иллюзией функции в привычной для последней трактовке, а смысловую нагрузку на себя принимает именно скрипт-блок.

PS C:\> add 10 20 30 PS C:\> (gc function:add).Invoke(10, 20) 30 

Так как функция — это диск, следовательно объявление функции в PowerShell в сущности является записью данных на этот диск.

PS C:\> sc function:add { param($a, $b) $a + $b } 

Такая запись избыточна, так как все накладные расходы при традиционном объявлении функции хост берет на себя, здесь эта запись приводится для понимания сути (лично я использую подобную запись, чтобы отделить функции с составным именем от простых).

PS C:\> function Add-Something { ... } #составное имя PS C:\> sc function:done { ... } #простое имя 

Все это занимательно, но какое отношение это имеет к расширению синтаксиса PowerShell? Как я уже говорил, за основу была взята концепция, о которой только что было рассказано, реализация же строится относительно понятия динамической сборки в текущем домене приложений. Давайте посмотрим на следующий код.

Set-Content function:dynmod {   $name = -join (0..7 | % {$rnd = New-Object Random}{     [Char]$rnd.Next(97, 122)   })      if (!($asm = ($cd = [AppDomain]::CurrentDomain).GetAssemblies() | ? {     $_.ManifestModule.ScopeName.Equals(($mem = 'RefEmit_InMemoryManifestModule'))   })) {     ($cd.DefineDynamicAssembly(       (New-Object Reflection.AssemblyName($name)), 'Run'     )).DefineDynamicModule($name, $false)   }   else { $asm.GetModules() | ? {$_.FullyQualifiedName -ne $mem} } } 

Функция (мы то знаем, что на самом деле скрипт-блок) создает или обращается к уже созданному модулю в динамической сборке в текщем домене приложений. Сама по себе она мало что значит и в сущности является связующим звеном между хостом и прочими функциями, которые мы в дальнейшем определим. Например, давайте упростим себе вызов API’шных функций за счет подобия C#-делегатов.

#обертка над инкапсулированными функциями GetModuleHandle и GetProcAddress function Get-ProcAddress {   [OutputType([IntPtr])]   param(     [Parameter(Mandatory=$true, Position=0)]     [String]$Dll,          [Parameter(Mandatory=$true, Position=1)]     [String]$Function   )      $href = New-Object Runtime.InteropServices.HandleRef(     (New-Object IntPtr),     [IntPtr]($$ = [Regex].Assembly.GetType(       'Microsoft.Win32.UnsafeNativeMethods'     ).GetMethods() | ? {       $_.Name -match '\AGet(ModuleH|ProcA).*\Z'     })[0].Invoke(       $null, @($Dll)   ))      if (($ptr = [IntPtr]$$[1].Invoke($null,     @([Runtime.InteropServices.HandleRef]$href, $Function)   )) -eq [IntPtr]::Zero) {     throw (New-Object Exception("Could not find $Function entry point in $Dll library."))   }      return $ptr }  #какбы новое ключевое слово - delegate Set-Content function:delegate {   [OutputType([Type])]   param(     [Parameter(Mandatory=$true, Position=0)]     [String]$Dll,          [Parameter(Mandatory=$true, Position=1)]     [String]$Function,          [Parameter(Mandatory=$true, Position=2)]     [Type]$ReturnType,          [Parameter(Mandatory=$true, Position=3)]     [Type[]]$Parameters   )      $ptr = Get-ProcAddress $Dll $Function   $Delegate = $Function + 'Delegate'      if (!(($mb = dynmod).GetTypes() | ? {$_.Name -eq $Delegate})) {     $type = $mb.DefineType(       $Delegate, 'AnsiClass, Class, Public, Sealed', [MulticastDelegate]     )     $ctor = $type.DefineConstructor(       'HideBySig, Public, RTSpecialName', 'Standard', $Parameters     )     $ctor.SetImplementationFlags('Managed, Runtime')     $meth = $type.DefineMethod(       'Invoke', 'HideBySig, NewSlot, Public, Virtual', $ReturnType, $Parameters     )     $Parameters | % {$i = 1}{       if ($_.IsByRef) { [void]$meth.DefineParameter($i, 'Out', $null) }       $i++     }     $meth.SetImplementationFlags('Managed, Runtime')          [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(       $ptr, ($type.CreateType())     )   }   else {     [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(       $ptr, $mb.GetType($Delegate)     )   } } 

Теперь вызвать некоторую API-функцию стало проще (эдакий clockres).

[Int32]$max = $min = $cur = 0  if ((delegate ntdll NtQueryTimerResolution Int32 @(   [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType() )).Invoke([ref]$max, [ref]$min. [ref]$cur) -eq 0) {   'Maximum timer resolution: {0:3f}' -f ($max / 10000)   'Minimum timer resolution: {0:3f}' -f {$min / 10000)   'Current timer resolution: {0:3f}' -f ($cur / 10000) } 

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

#какбы новое ключевое слово - struct Set-Content function:struct {   [OutputType([Type])]   param(     [Parameter(Mandatory=$true, Position=0)]     [String]$StructName,          [Parameter(Mandatory=$true, Position=1)]     [ScriptBlock]$Definition,          [Parameter(Position=2)]     [Reflection.Emit.PackingSize]$PackingSize = 'Unspecified',          [Parameter(Position=3)]     [Switch]$Explicit   )      if (!(($mb = dynmod).GetTypes() | ? {$_.Name -eq $StructName})) {     [Reflection.TypeAttributes]$attr = 'AnsiClass, BeforeFieldInit, Class, Public, Sealed'     $attr = switch ($Explicit) {       $true  { $attr -bor [Reflection.TypeAttributes]::ExplicitLayout }       $false { $attr -bor [Reflection.TypeAttributes]::SequentialLayout }     }     $type = $mb.DefineType($StructName, $attr, [ValueType], $PackingSize)     $ctor = [Runtime.InteropServices.MarshalAsAttribute].GetConstructor(       [Reflection.BindingFlags]20, $null, [Type[]]@([Runtime.InteropServices.UnmanagedType]), $null     )     $cnst = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst'))          $ret = $null          [Management.Automation.PSParser]::Tokenize($Definition, [ref]$ret) | ? {       $_.Type -match '\A(Command|String)\Z'     } | % {       if ($_.Type -eq 'Command') {         $token = $_.Content #тип поля         $ft = switch (($def = $mb.GetType($token)) -eq $null) {           $true  { [Type]$token }           $false { $def } #поиск типа в динамической сборке         } #switch       }       else {         $token = @($_.Content -split '\s') #имя поля, смещение, атрибуты и размер         switch ($token.Length) {           1 { [void]$type.DefineField($token[0], $ft, 'Public') } #пример: UInt32 'e_lfanew';           2 { #структура помечена как Explicit: Int64 'QuadPart 0'; иначе String 'Buffer LPWStr';             switch ($Explicit) {               $true  { [void]$type.DefineField($token[0], $ft, 'Public').SetOffset([Int32]($token[1])) }               $false {                 $unm = [Runtime.InteropServices.UnmanagedType]($token[1])                 [void]$type.DefineField($token[0], $ft, 'Public, HasFieldMarshal').SetCustomAttribute(                   (New-Object Reflection.Emit.CustomAttributeBuilder($ctor, [Object[]]@($unm)))                 )               }             } #switch           }           3 { #пример: UInt16[] 'e_res ByValArray 10';             $unm = [Runtime.InteropServices.UnmanagedType]$token[1]             [void]$type.DefineField($token[0], $ft, 'Public, HasFieldMarshal').SetCustomAttribute(               (New-Object Reflection.Emit.CustomAttributeBuilder($ctor, $unm, $cnst, @([Int32]$token[2])))             )           }         } #switch       }     } #foreach     #пара полезных методов для создаваемой структуры     $OpCodes = [Reflection.Emit.OpCodes]     $Marshal = [Runtime.InteropServices.Marshal]     $GetSize = $type.DefineMethod('GetSize', 'Public, Static', [Int32], [Type[]]@())     $IL = $GetSize.GetILGenerator()     $IL.Emit($OpCodes::Ldtoken, $type)     $IL.Emit($OpCodes::Call, [Type].GetMethod('GetTypeFromHandle'))     $IL.Emit($OpCodes::Call, $Marshal.GetMethod('SizeOf', [Type[]]@([Type])))     $IL.Emit($OpCodes::Ret)     $Implicit = $type.DefineMethod(       'op_Implicit', 'PrivateScope, Public, Static, HideBySig, SpecialName', $type, [Type[]]@([IntPtr])     )     $IL = $Implicit.GetILGenerator()     $IL.Emit($OpCodes::Ldarg_0)     $IL.Emit($OpCodes::Ldtoken, $type)     $IL.Emit($OpCodes::Call, [Type].GetMethod('GetTypeFromHandle'))     $IL.Emit($OpCodes::Call, $Marshal.GetMethod('PtrToStructure', [Type[]]@([IntPtr], [Type])))     $IL.Emit($OpCodes::Unbox_Any, $type)     $IL.Emit($OpCodes::Ret)     $type.CreateType()   }   else { $mb.GetType($StructName) } } 

Пример (uptime).

$sti = struct SYSTEM_TIMEOFDAY_INFORMATION {   Int64  'BootTime';   Int64  'CurrentTime';   Int64  'TimeZoneBias';   UInt32 'TimeZoneId';   UInt32 'Reserved';   UInt64 'BootTimeBias';   UInt64 'SleepTimeBias'; }  $sti = NtQuerySystemInformation $sti SystemTimeOfDayInformation  '{0:D2}:{1:D2}:{2:D2} up {3} day{4}' -f (   $u = (Get-Date) - [DateTime]::FromFileTime($sti.BootTime) ).Hours, $u.Minutes, $u.Seconds, $u.Days, $(if($u.Days -gt 1){'s'}else{''}) 

Где NtQuerySystemInformation:

$SYSTEM_INFORMATION_CLASS = @{   ...   SystemTimeOfDayInformation = 3   ... }  Set-Content function:NtQuerySystemInformation {   param(     [Parameter(Mandatory=$true, Position=0)]     [Type]$Struct,          [Parameter(Mandatory=$true, Position=1)]     [String]$Class   )      $len = $Struct::GetSize()   $ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($len)   $cls = $SYSTEM_INFORMATION_CLASS[$Class]      if ([Regex].Assembly.GetType('Microsoft.Win32.NativeMethods').GetMethod(     'NtQuerySystemInformation'   ).Invoke($null, @($cls, $ptr, $len, $ref)) -eq 0) {     $str = $ptr -as $Struct   }   [Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)      return $str } 

Вроде бы начиналось что-то о функциях, а заканчивается кучей кода, — позвольте кое-что прояснить. Будь то делегат или структура, которую мы объявляем, все это заносится в одну единственную сборку (принцип диска); прочий код — попытка автоматизировать\упростить создание\вызов структур\API-функций. При этом структуры дополняются парой полезных методов (получения размера структуры и конвертации указателя в структуру с помощью оператора as, например, $ptr -as $struc).

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

ссылка на оригинал статьи http://habrahabr.ru/post/254081/