Какие бы хвалебные оды не пелись в адрес 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).
Все эти изыскания не появились в одночасье, а являются результатом многих экспериментов и простым желанием упростить написание модулей. Возможно кто-то найдет для себя все это полезным и интересным, а этот пост станет своего рода отправной точкой в дальнейших исследованиях или даже поможет сократить количество набираемого кода.
- О дисках PowerShell
- Встроенная справка — man Get-PSDrive
- О домене приложений
ссылка на оригинал статьи http://habrahabr.ru/post/254081/
Добавить комментарий