Не так давно знакомая, работающая в одном из книжных издательств, обратилась ко мне с предложением написать книгу о PowerShell. «Только с подробным описанием что да как, а не так как на Хабре», прозвучал ее приятный голос по ту сторону телефона причем без всякого укора, хотя последнее замечание вполне справедливо и могло быть интонационно подчеркнуто. Если честно, я не доверяю людям, тем более женщинам, а потому попросил дать время на размышление.

Игра не стоит свеч?


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

Подробно — это как?


При написании книги непременно должны существовать рамки, в противном случае можно получить не научную монографию, а что-то из разряда занимательных сведений, которые на Западе обычно принято именовать cookbook. Последний вид книг исключительно вреден, так как, во-первых, формирует шаблонность мышления, во-вторых, ничего кроме фрагментарности в знаниях читающий не получит. Если рассматривать это на каких-то конкретных примерах, то давайте представим себе ситуацию: вы работаете над главой, посвященной сети и все что с ней связано, и хотите продемонстрировать читателю, скажем, как получить свой MAC-адрес, — в зависимости от контекста, решений у задачи очень много, но станете ли вы описывать их все, попутно разъясняя чем один способ лучше другого, а третий — предыдущего? Ответ, полагаю, очевиден. Писатель должен уметь отделять второстепенное от действительно полезных и значимых фактов, при этом давая понять, что приводимый пример кода являет собой лишь один из частных случаев, тем самым стимулируя интерес читателя к собственным экспериментам. Единственное, пожалуй, чем не должен поступаться писатель — упомянания о совместимости версий и ошибках, допущенных самими разработчиками .NET платформы.

Примеры получения MAC-адреса в PowerShell
function Get-MacAddress {
  <#
    .NOTES
        Альтернативные способы получения MAC-адреса.
        
        Пример 1:
        $asm = Add-Type -MemberDefinition @'
          [DllImport("rpcrt4.dll")]
          public static extern Int32 UuidCreateSequential(
              out Guid guid
          );
        '@ -Name Uuid -NameSpace MacAddress -PassThru
        $guid = New-Object Guid
        
        if (($res = $asm::UuidCreateSequential([ref]$guid)) -ne 0) {
          (New-Object ComponentModel.Win32Exception($res)).Message
          break
        }
        [Regex]::Replace(
          $guid.Guid.Split('-')[-1], '.{2}', '$0-'
        ).TrimEnd('-')
        
        Пример 2:
        Get-WmiObject Win32_NetworkAdapter | Where-Object {
          $_.MacAddress
        } | ForEach-Object {
          New-Object PSObject -Property @{
            Description = $_.Description
            Service     = $_.ServiceName
            MACAddress  = $_.MACAddress
          }
        } | Select-Object Description, Service, MACAddress
        
        Пример 3:
        Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object {
          $_.MacAddress
        } | ForEach-Object {
          New-Object PSObject -Property @{
            Description = $_.Description
            Id          = $_.SettingID
            MACAddress  = $_.MACAddress
          }
        } | Select-Object Description, Id, MACAddress
        
        Пример 4:
        $mac, $nic = (getmac /fo csv | Where-Object {
          ![String]::IsNullOrEmpty($_) -and $_ -match '\w{2}\-'
        }).Split(',') | ForEach-Object {$_.Trim('"')}
        New-Object PSObject -Property @{
          Interface  = $nic.Substring(($$ = $nic.IndexOf('{')), $nic.Length - $$)
          MACAddress = $mac
        }
        
        Пример 5:
        $key = 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\*'
        Get-ItemProperty $key | Where-Object {
          $_.DhcpIpAddress -and $_.DhcpIpAddress -ne '0.0.0.0'
        } | Select-Object DhcpIpAddress | ForEach-Object {
          New-Object PSObject -Property @{
            IPAddress  = $_.DhcpIpAddress
            MACAddress = ([Regex]'(\w{2}\-){5}\w{2}').Match((
              nbtstat -a $_.DhcpIpAddress
            )).Value
          }
        }
        
        Пример 6:
        ([Regex]'(\w{2}\-){5}\w{2}').Match((
            ipconfig /all
        )).Value
        
        Пример 7:
        ([Regex]'(\w{2}\s){6}').Match((
            route print
        )).Value.Trim().Replace([Char]32, '-')
  #>
  
  #в CLR v4 нет ошибки доступа по пути Global\.net clr networking
  #для учетной записи с ограниченными правами
  if (($clr = $PSVersionTable.CLRVersion.Major) -ge 4) {
    [Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() |
    Where-Object {
      $_.OperationalStatus -eq [Net.NetworkInformation.OperationalStatus]::Up
    } | ForEach-Object {
      if (![String]::IsNullOrEmpty((
        $$ = $_.GetPhysicalAddress().ToString()
      ))) {
        New-Object PSObject -Property @{
          Id         = $_.Id
          MACAddress = [Regex]::Replace($$, '.{2}', '$0-').TrimEnd('-')
        }
      }
    } | Select-Object Description, Id, MACAddress | Format-List
  }
  elseif ($clr -eq 2) {
    @(
      [Runtime.InteropServices.CallingConvention],
      [Runtime.InteropServices.Marshal],
      [Reflection.BindingFlags],
      [Reflection.Emit.OpCodes]
    ) | ForEach-Object {
      $keys = ($ta = [PSObject].Assembly.GetType(
        'System.Management.Automation.TypeAccelerators'
      ))::Get.Keys
      $collect = @()
    }{
      if ($keys -notcontains $_.Name) {
        $ta::Add($_.Name, $_)
      }
      $collect += $_.Name
    }
    
    function Get-LastError {
      param(
        [Int32]$ErrorCode = [Marshal]::GetLastWin32Error()
      )
      
      [PSObject].Assembly.GetType(
        'Microsoft.PowerShell.Commands.Internal.Win32Native'
      ).GetMethod(
        'GetMessage', [BindingFlags]40
      ).Invoke(
        $null, @($ErrorCode)
      )
    }
    
    function private:Invoke-FreeLibrary {
      param(
        [Parameter(Mandatory=$true)]
        [IntPtr]$ModuleHandle
      )
      
      [void][Linq.Enumerable].Assembly.GetType(
        'Microsoft.Win32.UnsafeNativeMethods'
      ).GetMethod(
        'FreeLibrary', [BindingFlags]40
      ).Invoke($null, @($ModuleHandle))
    }
    
    function private:Get-ProcAddress {
      param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String]$Module,
        
        [Parameter(Mandatory=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [String]$Function
      )
      
      [Data.Rule].Assembly.GetType(
        'System.Data.Common.SafeNativeMethods'
      ).GetMethods(
        [BindingFlags]40
      ) | Where-Object {
        $_.Name -cmatch '\AGet(ProcA|ModuleH)'
      } | ForEach-Object {
        Set-Variable $_.Name $_
      }
      
      if (($ptr = $GetModuleHandle.Invoke(
        $null, @($Module)
      )) -eq [IntPtr]::Zero) {
        if (($mod = [Regex].Assembly.GetType(
          'Microsoft.Win32.SafeNativeMethods'
        ).GetMethod('LoadLibrary').Invoke(
          $null, @($Module)
        )) -eq [IntPtr]::Zero) {
          Write-Warning "$(Get-LastError)"
          break
        }
        $ptr = $GetModuleHandle.Invoke($null, @($Module))
      }
      
      $GetProcAddress.Invoke($null, @($ptr, $Function)), $mod
    }
    
    function private:Set-Delegate {
      param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateScript({$_ -ne [IntPtr]::Zero})]
        [IntPtr]$ProcAddress,
        
        [Parameter(Mandatory=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [String]$Delegate
      )
      
      $proto = Invoke-Expression $Delegate
      $method = $proto.GetMethod('Invoke')
      
      $returntype = $method.ReturnType
      $paramtypes = $method.GetParameters() |
                                    Select-Object -ExpandProperty ParameterType
      
      $holder = New-Object Reflection.Emit.DynamicMethod(
        'Invoke', $returntype, $paramtypes, $proto
      )
      $il = $holder.GetILGenerator()
      0..($paramtypes.Length - 1) | ForEach-Object {
        $il.Emit([OpCodes]::Ldarg, $_)
      }
      
      switch ([IntPtr]::Size) {
        4 { $il.Emit([OpCodes]::Ldc_I4, $ProcAddress.ToInt32()) }
        8 { $il.Emit([OpCodes]::Ldc_I8, $ProcAddress.ToInt64()) }
      }
      $il.EmitCalli(
        [OpCodes]::Calli, [CallingConvention]::StdCall, $returntype, $paramtypes
      )
      $il.Emit([OpCodes]::Ret)
      
      $holder.CreateDelegate($proto)
    }
    
    $ptr, $mod = Get-ProcAddress iphlpapi SendARP
    $SendARP = Set-Delegate $ptr `
                              '[Func[UInt32, UInt32, [Byte[]], [Byte[]], Int32]]'
    
    $key = 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\*'
    Get-ItemProperty $key | Where-Object {
      $_.DhcpIpAddress -and $_.DhcpIpAddress -ne '0.0.0.0'
    } | ForEach-Object {
      $inet_addr = [Regex].Assembly.GetType(
        'System.Net.UnsafeNclNativeMethods+OSSOCK'
      ).GetMethod('inet_addr', [BindingFlags]40)
    }{
      $adr = [BitConverter]::ToUInt32(
        [BitConverter]::GetBytes(
          $inet_addr.Invoke($null, @($_.DhcpIpAddress)
        )), 0
      )
      $mac = New-Object Byte[] 6
      $len = [BitConverter]::GetBytes($mac.Length)
      
      if (($ret = $SendARP.Invoke($adr, 0, $mac, $len)) -ne 0) {
        Write-Warning "$(Get-LastError $ret)"
        break
      }
      
      New-Object PSObject -Property @{
        Id         = $_.PSChildName
        MACAddress = ($mac | ForEach-Object {'{0:X2}' -f $_}) -join '-'
      }
    }
    
    if ($mod) { Invoke-FreeLibrary $mod }
    $collect | ForEach-Object { [void]$ta::Remove($_) }
  }
}


Материальная сторона вопроса


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

Вместо послесловия


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

Комментарии (11)


  1. gotch
    28.07.2016 15:56
    +5

    Вот что скажу вам, Григорий. )
    Читаю ваши посты, и честно говоря, PowerShell-то в них и нет. Net Framework есть, а PowerShell для вас просто обертка. Понимаете, о чем я?
    Эту деформацию как раз можно использовать в вашей книге.

    Г.Захаров — «Эффективное использование .Net Framework в PowerShell».

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

    А если просто «за жизнь», то можно и Пайетта почитать.


  1. GrigoriZakharov
    28.07.2016 21:19

    Прежде всего благодарю Вас за комментарий. Я понял Вашу позицию, но согласиться с Вами все же не могу. Shell с английского оболочка, что уже наводит на мысль о «оберточности», а Power только подчеркивает абстрактность последней. В основе командлетов лежат типы .NET, то есть PowerShell отдельно от .NET существовать не может, именно поэтому «Эффективное использование .NET Framework в PowerShell» звучит практически как «хлеб с булкой» или «булка с хлебом». Что касается передачи книги в издательство… если книга и будет написана, то платной будет только ее печатная версия. По крайней мере это единственное требование с моей стороны к издателю, ибо интерес на постсоветском пространстве к PowerShell не столь велик, как на Западе. Также я планирую покинуть Хабр, так как в планах пара собственных проектов, но сколько бы я ни сигналил адмнистраторам Хабра, блокировать мой аккаунт не спешат.


    1. rbobot
      29.07.2016 09:09

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


      1. GrigoriZakharov
        29.07.2016 12:12

        Все черновики (исключая некоторые PoC'ы) кодов есть на моем github'е.


    1. gotch
      29.07.2016 11:36

      PowerShell задуман так, что в классах .NET обычно делать нечего — на 60% случаев есть командлеты. Еще в 30% случаев можно работать через WMI. И только в хардкорных случаях надо браться за MSDN.

      In computing, a shell is a user interface for access to an operating system's services.


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


      1. GrigoriZakharov
        29.07.2016 12:14

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


    1. strangewalker
      29.07.2016 16:07

      На собственном примере сталкивался с ситуациями, когда было необходимо автоматизировать те или иные программы, а из доступных средств было только API и кривое-косое описание public классов, так что поддержу gotch, такой вариант книги был бы очень полезен, хоть и для узкого круга.

      PS А как найти Ваш гитхаб?


  1. klerik
    29.07.2016 15:23

    пример получения mac на cmd

    Пуск-Выполнить cmd написать getmac
    Все. Если нужно больше, то getmac -?


    1. GrigoriZakharov
      29.07.2016 19:12

      Не совсем понятен Ваш комментарий: желаете ли Вы показать им, что так и не удосужились прочитать пост или это просто желание вставить свои пять копеек? getmac также фигурирует среди примеров в посте.


      1. klerik
        01.08.2016 12:20

        просто я не до конца понял — зачем тут powershell.


      1. gotch
        03.08.2016 12:07

        — Доброе утро!
        — Что вы хотите этим сказать? Просто желаете мне доброго утра? Или утверждаете, что утро сегодня доброе — неважно, что я о нём думаю? Или имеете в виду, что нынешним утром все должны быть добрыми?
        — И то, и другое, и третье И ещё — что в такое дивное утро отлично выкурить трубочку на воздухе. Если у вас есть трубка, присаживайтесь, отведайте моего табачку! Торопиться некуда, целый день впереди!