company_banner

Притворяемся что пишем на C#, но только на Powershell


    Powershell — удобная API построенная на .net. Powershell позволяет пользователям писать скрипты, не упираясь в программирование, при этом получая схожие результаты. Что происходит на КДВП, автор объяснит позже по тексту. Сейчас нам срочно нужно притвориться, что мы программируем на C#.

    TL;DR: Postman не нужен, если есть Powershell. Но сперва нужно зайти издалека.

    Делаем простой класс


    Автор слышал, что крутые программисты делают все через классы и их методы.
    Так как PowerShell это позволяет, давайте автор покажет, как можно сложить 1 + 1 притворившись, что мы программируем.

    class ClassName {
     
        [string] Sum ($A, $B) {
           
            $Result = $A + $B
            return $Result
        }
    }
    

    Вот наш класс ClassName и его метод Sum. Экземпляр класса можно вызвать ровно так же, как в настоящих языках программирования.

    $NewClass = [ClassName]::new()
    $NewClass.Sum(1, 1)

    Создаем новый экземпляр класса и вызываем метод, всё просто.

    Есть ли Void в Powershell


    При написании сложных скриптов этот же вопрос вставал у автора. Как сделать функцию, которая будет Void?

    Говорят, что можно сделать так:

    Get-Date | Out-Null

    Однако, | Out-Null так же глушит весь Verbose, ErrorAction и не работает с Invoke-Command.

    Если вам нужна функция с [Void] – делайте новый класс, другого выхода нет.

    class ClassName {
     
        #Конструктов класса
        [void] Start () {
            #Создаем экземпляр класса прямо внутри этого же класса.
            $q = [ClassName]::new()
            $q.GetDate()
        }
     
        #Своего рода метод внутри класса
        [void] GetDate () {
            #А вот тут вызываем еще один метод из .Net
    	  #Просто так, потому что можем
            $Result = [DateTime]::UtcNow.ToString()
            Write-Host $Result
        }
    }
    

    Сделать метод приватным внутри одного класса, или вызвать один из методов класса внутри этого же класса в PowerShell невозможно, поэтому приходится лепить такие вот вызовы.
    Конструктор класса был добавлен в пример для понимания ограничений языка и такой код писать в целом не стоит.

    Вот так мы добились того, что не заглушили Verbose, при этом сделали функцию с Void.

    Список методов класса


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

    Перечислить все методы интересующего класса можно так:

    #Делаем любовь, а не класс
    $Love = [ClassName]::new()
     
    #Выбираем члены интересующего нас класса и записываем в массив.
    foreach ($i in $Love | Get-Member -MemberType Method | Select-Object name) {
        [array]$array += $i.Name
    }
     
    #Вызываем члены класса, если нужно.
    $Array | ForEach-Object {
        $Love.$_()
    }
    

    Отправляем HTTP запросы скриптом (оправдываем КДПВ)


    Классами мы можем представлять данные и эти данные конвертировать в разные форматы. К примеру, нам нужно отправить POST запрос на веб сайт в формате JSON.

    Сначала мы делаем модель данных и заполним данные в новый экземпляр.

    #В качестве модели данных делаем новый класс
    class DataModel {
        $Data
        $TimeStamp
    }
     
    #Создаем экземляр класса
    $i = [DataModel]::new()
     
    #Заполняем данные
    $i.Data = "My Message in string"
    $i.TimeStamp = Get-Date
    

    Так выглядит экземпляр класса после заполнения:

    PS C:\> $i
     
    Data                 TimeStamp
    ----                 ---------
    My Message in string 30.07.2020 5:51:56

    Потом этот экземпляр можно конвертировать в XML или JSON или даже SQL запрос. Остановимся на JSON:

    #Конвертируем данные в JSON
    $Request = $i | ConvertTo-Json

    Так выглядит JSON после его конвертации:

    PS C:\> $Request
    {
      "Data": "My Message in string",
      "TimeStamp": "2020-07-30T05:51:56.6588729+03:00"
    }
    

    И отправляем:

    #Отправляем JSON
    Invoke-WebRequest localhost -Body $Request -Method Post -UseBasicParsing

    В случае если нужно отправлять один и тот же JSON файл 24/7, можно сохранить его как файл и отправлять уже из файла. К примеру, возьмем этот же самый $Request.

    #Сохраняем данные конвертированные ранее в JSON в файл
    $Request | Set-Content C:\Users\User\Desktop\YourRequest.json
     
    #Отправляем ранее сохраненный в файл JSON
    Invoke-WebRequest localhost -Body (Get-Content C:\Users\User\Desktop\YourRequest.json) -Method Post -UseBasicParsing

    Получаем HTTP запросы скриптом (оправдываем КДПВ 2)


    Автор терпеть не может Postman, зачем кому-либо нужен Postman, когда есть руки и PowerShell? (Автор предвзято относится к этой программе и его нелюбовь ничем не обоснована.)
    Делать свою альтернативу мы будем это с помощью System.Net.HttpListener, то есть мы сейчас запустим настоящий веб сервер из скрипта.

    #Создаем новый экземпляр класса
    $http = [System.Net.HttpListener]::new()
     
    #Добавляем HTTP префиксы. Их может быть сколько угодно
    $http.Prefixes.Add("http:/localhost/")
    $http.Prefixes.Add("http://127.0.0.1/")
     
    #Запускаем хттп листенер
    $http.Start()
     
     
    $http.Close()
    

    Так проходит запуск класса.

    Экземпляр класса был создан и его процесс запустился, мы можем слушать от него вывод. Вывод представлен как System.Net.HttpListener.GetContext. В это примере мы принимаем и конвертируем только POST запрос.

    while ($http.IsListening) {
     
        #GetContext нужен для получения сырых данных из HttpListener
        $context = $http.GetContext()
     
        #Определяем тип запроса с помощью Request.HttpMethod 
        if ($context.Request.HttpMethod -eq 'POST') {
     
            #Читаем сырые данные из GetContext
            #Для каждого отдельного запроса создаем свой конвейер
            [System.IO.StreamReader]::new($context.Request.InputStream).ReadToEnd() | ForEach-Object {
                
                #С помощью System.Web.HttpUtility делаем urlDecore, иначе кириллица превращается в руны
                $DecodedContent = [System.Web.HttpUtility]::UrlDecode($_)
     
                #Конвертируем прилетевшие данные в нужный нам формат
                $ConvertedForm = $DecodedContent | ConvertFrom-Json -ErrorAction SilentlyContinue
     
                #Cконвертированные данные отображаем таблицой
                $ConvertedForm | Format-Table
               
            }
        }
    } 
    

    Готовый скрипт


    С помощью этого скрипта можно принимать запросы:

    #Создаем новый экземпляр класса
    $http = [System.Net.HttpListener]::new()
     
    #Добавляем HTTP префиксы. Их может быть сколько угодно
    $http.Prefixes.Add("http://localhost/")
    $http.Prefixes.Add("http://127.0.0.1/")
    
    
    
    #Запускаем веб листенер
    $http.Start()
     
    if ($http.IsListening) {
        Write-Host "Скрипт запущен"
    }
     
    while ($http.IsListening) {
     
        #GetContext нужен для получения сырых данных из HttpListener
        $context = $http.GetContext()
     
        #Определяем тип запроса с помощью Request.HttpMethod 
        if ($context.Request.HttpMethod -eq 'POST') {
     
            #Читаем сырые данные из GetContext
            #Для каждого отдельного запроса создаем свой конвейер
            [System.IO.StreamReader]::new($context.Request.InputStream).ReadToEnd() | ForEach-Object {
                
                #С помощью System.Web.HttpUtility делаем urlDecore, иначе кириллица превращается в руны
                $DecodedContent = [System.Web.HttpUtility]::UrlDecode($_)
     
                #Конвертируем прилетевшие данные в нужный нам формат
                $ConvertedForm = $DecodedContent | ConvertFrom-Json -ErrorAction SilentlyContinue
     
                #Cконвертированные данные отображаем таблицей
                $ConvertedForm | Format-Table
               
            }
     
            #Отвечаем клиенту 200 OK и закрываем стрим.
            $context.Response.Headers.Add("Content-Type", "text/plain")
            $context.Response.StatusCode = 200
            $ResponseBuffer = [System.Text.Encoding]::UTF8.GetBytes("")
            $context.Response.ContentLength64 = $ResponseBuffer.Length
            $context.Response.OutputStream.Write($ResponseBuffer, 0, $ResponseBuffer.Length)
            $context.Response.Close()
     
        }
        #Cконвертированные данные отображаем таблицей
        $http.Close()
        break
    }
    

    Данные будут автоматичеки конвертироваться из JSON и выводиться в терминал.

    Автор надеется, что вы выбросите Postman, так же, как и GIT с GUI.

    RUVDS.com
    VDS/VPS-хостинг. Скидка 10% по коду HABR

    Комментарии 18

      –1
      Автор надеется, что вы выбросите Postman

      Вот когда автор расскажет, как в его поделке авторизоваться в удаленном сервисе через OAuth 2, или, что веселее, послать запрос, подписанный по требованиям AWS, тогда и поговорим.

      +4

      Добавлю: вместо Invoke-WebRequest можно использовать Invoke-RestMethod, он за нас умеет делать сериализацию/десериализацию тела запроса/ответа в JSON/XML и обратно в PS-объекты, а еще имеет набор заготовленных параметров для типичных RESTful API.


      https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-restmethod?view=powershell-7

        +6

        Люблю и ненавижу PowerShell. С одной стороны позволяет с легкостью автоматизировать даже сложные процессы, с другой совершенно идиотский и неконсистентный синтаксис с динамической типизацией.
        Например, работа с коллекцией через foreach(obj in $list) {...} и через $list | Foreach-Object {...} разнится и зачастую непредсказуемо, если внутри блока использовать continue, break или return или код внутри блока выкидывает исключение.
        За двойные стандарты вызова функций авторов PowerShell нужно отправить в несгораемый котел: почему одним функциям нужно передавать параметр через Set-Value -Parameter "Hi", а другим через "string".Split(";")? Эта неконсистентость только усиливается классами, пример которых представлен в статье. Если я объявляю функцию модуля, то она будет вызваться как Get-Something, но если я создаю класс, то это уже ClassInstance.Get($something). Я кучу часов потратил на отладку неработающего кода, из-за того, что скопировал функцию из объявления Invoke-Something($a = "aaa", $b = "bbb) и забыл убрать скобки!
        Еще одна болезнь — толерантность к пустым переменным. Я плачу по strict режиму, чтобы нельзя было использовать необъявленные и неинициализированные переменные. Куча времени уходит на поимку фантомов, после того, как в одном месте изменяется название переменной, а ниже по пайпу она продолжает использоваться как ни в чем не бывало. Еще хуже с этим дела обстоят в PowerShell ISE, который будет хранить временное значение переменной до перезапуска, а вы будете себе рвать волосы на голове, не понимая почему значение переменной $MuVar не меняется, хотя выше по коду $MyVar давно имеет новое значение.

          +14
          После Bash, где написание скриптов подобно заплыву по бездонному океану граблей, PowerShell кажется потрясающе логичным и продуманным.
            +2

            Но таким скуучным… :)

            0
            В добавок к вашему списку еще хочется добавить то как он возвращает значения из функций.
            Заголовок спойлера
            function testSingleValue{
              "1"
              "2"
              "3"
              return "4"
            }
            testSingleValue
            


            This function returns the number 4 as a string, right? Wrong! It returns an array with the values “1”,”2”,”3”,”4”. The reason is that the strings “1”,”2” and “3” are not saved into a variable, so the output of those strings goes into the PowerShell pipeline, which collects the outputs of each line and returns all the values as an array.
              +1
              Потому что в powershell ключевое слово return выполняет другую функцию. Непривычно но не более.
              Из документации:

              The return keyword exits a function, script, or script block. It can be used to exit a scope at a specific point, to return a value, or to indicate that the end of the scope has been reached.

              Users who are familiar with languages like C or C# might want to use the return keyword to make the logic of leaving a scope explicit.

              In PowerShell, the results of each statement are returned as output, even without a statement that contains the Return keyword. Languages like C or C# return only the value or values that are specified by the return keyword.
              0
              Я плачу по strict режиму
              — не понял, есть же Set-StrictMode. Временное значение переменной до перезапуска зависит, скорее всего, не от ISE, а от сессии PS. От сессии PS же зависит загрузка psm-модулей — грузится однократно, а для перезагрузки только завершать сессию.
              0
              Первый же код такой вывод про $NewClass.Sum(1, 1):
              При создании конвейера произошла ошибка.
              + CategoryInfo: NotSpecified: (:) [], ParentContainsErrorRecordException
              + FullyQualifiedErrorId: RuntimeException
                +3
                То ли у меня лыжи не те, то ли ещё чего, но вообще не представляю как хотябы среднего размера запросы отправлять через командную строку.
                Вот нужно в середине JSON в 10 строк поменять пару строк. А после отправки поменять ещё раз. И ещё раз.

                Идти файл редактировать каждый раз?
                Или в командной строке ковырять мульти-строковую команду?
                А выравнивать JSON руками что ли тогда?

                Короче куча проблем, которые Postman и ему подобные решают легко.

                  +2

                  А зачем строки менять? PowerShell десериализует JSON в объект и достаточно будет поменять свойство нужных полей, а потом сериализовать и отправить ответ:


                  $Json = @"
                  {
                      "Param1": "",
                  
                      "Param2": {
                          "Param3": 1,
                          "Param4": ["1", "2","3"],
                      }
                  }
                  "@
                  
                  $psObject = $Json | ConvertFrom-Json 
                  $psObject.Param2.Param4[2] = "5"
                  
                  $psObject | ConvertTo-Json

                  Причем неважно откуда будет получен исходный объект — прочитан из файла, из запроса или сформирован через переменные PowerShell.

                    0
                    В смысле зачем строки менять? Затем чтобы новый запрос отправить.
                    Вот пусть я сначала создаю с JSON как у вас, затем второй объект у которого 10 значений в массиве Param4, затем у третьего объекта есть Param5 типа объект с несколькими полей. Который я вставлю из внутренностей ответа на другой запрос.

                    Причём это мне нужно только сегодня. Это задача на один раз, никакой автоматизации не нужно. Мутить пайплайн который всё это сделает не нужно.

                    Так вот, как вы будете это делать в командной строке?

                    Добавлено:
                    И вот то что вы породили через изменение параметра объекта это угар. Напишу я коллеге: «смотри, чо у нас происходит». А он мне: «интереснько. а ну-ка кинь мне курл». А я что ему?
                  • НЛО прилетело и опубликовало эту надпись здесь
                    +1

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

                      +2

                      Окей гугл: как пройтись по всем сайтовым коллекциям SharePoint и почистить в них корзину из LINQPad? Как в несколько шагов организовать миграцию для SQL Server по аналогии с dbatools?
                      Может для программиста и проще накидать скетч на C#, а для администратора проще написать скрипт на повершеле. Тем более, что управление инфраструктурой Microsoft построено вокруг PowerShell.

                        +1

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

                        +1
                        Спасибо, чудесная вещь! Впервые встретил.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое