Contents of this directory is archived and no longer updated.

Posts on this page:

Beginner Division

Summer Scripting Games 2009 задача на работу с реестром.

Задача:

  • Найти текущее значение максимального количества закачек в Internet Explorer
  • Увеличить это число на другое значение
  • при чтении этого значения предусмотреть проверку существования этого параметра

Решение:

Ключи и значения реестра для этой задачи можно найти тут: http://support.microsoft.com/kb/282402. В принципе, очень просто тут:

$path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
if (!(Test-Path $path)) {New-Item -ItemType Registry -Path $path -Force}
if (!(Get-ItemProperty $path).MaxConnectionsPer1_0Server) {
    Write-Warning "MaxConnectionsPer1_0Server property doesn't exist"
    [void](New-ItemProperty -Path $path -Name 'MaxConnectionsPer1_0Server' -Value 10 -PropertyType 'DWord')
} else {"MaxConnectionsPer1_0Server is: " + (Get-ItemProperty $path).MaxConnectionsPer1_0Server}
if (!(Get-ItemProperty $path).MaxConnectionsPerServer) {
    Write-Warning "MaxConnectionsPerServer property doesn't exist"
    [void](New-ItemProperty -Path $path -Name 'MaxConnectionsPerServer' -Value 10 -PropertyType 'DWord')
} else {"MaxConnectionsPerServer is: " + (Get-ItemProperty $path).MaxConnectionsPerServer}

и вот вывод:

[↓] [vPodans] $path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
[↓] [vPodans] if (!(Test-Path $path)) {New-Item -ItemType Registry -Path $path -Force}
[↓] [vPodans] if (!(Get-ItemProperty $path).MaxConnectionsPer1_0Server) {
>>     Write-Warning "MaxConnectionsPer1_0Server property doesn't exist"
>>     [void](New-ItemProperty -Path $path -Name 'MaxConnectionsPer1_0Server' -Value 10 -PropertyType 'DWord')
>> } else {"MaxConnectionsPer1_0Server is: " + (Get-ItemProperty $path).MaxConnectionsPer1_0Server}
>> if (!(Get-ItemProperty $path).MaxConnectionsPerServer) {
>>     Write-Warning "MaxConnectionsPerServer property doesn't exist"
>>     [void](New-ItemProperty -Path $path -Name 'MaxConnectionsPerServer' -Value 10 -PropertyType 'DWord')
>> } else {"MaxConnectionsPerServer is: " + (Get-ItemProperty $path).MaxConnectionsPerServer}
>>
WARNING: MaxConnectionsPer1_0Server property doesn't exist
WARNING: MaxConnectionsPerServer property doesn't exist
[↓] [vPodans] $path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
[↓] [vPodans] if (!(Test-Path $path)) {New-Item -ItemType Registry -Path $path -Force}
[↓] [vPodans] if (!(Get-ItemProperty $path).MaxConnectionsPer1_0Server) {
>>     Write-Warning "MaxConnectionsPer1_0Server property doesn't exist"
>>     [void](New-ItemProperty -Path $path -Name 'MaxConnectionsPer1_0Server' -Value 10 -PropertyType 'DWord')
>> } else {"MaxConnectionsPer1_0Server is: " + (Get-ItemProperty $path).MaxConnectionsPer1_0Server}
>> if (!(Get-ItemProperty $path).MaxConnectionsPerServer) {
>>     Write-Warning "MaxConnectionsPerServer property doesn't exist"
>>     [void](New-ItemProperty -Path $path -Name 'MaxConnectionsPerServer' -Value 10 -PropertyType 'DWord')
>> } else {"MaxConnectionsPerServer is: " + (Get-ItemProperty $path).MaxConnectionsPerServer}
>>
MaxConnectionsPer1_0Server is: 10
MaxConnectionsPerServer is: 10
[↓] [vPodans]

как бы ничего сложного.

Advanced Division

работа с тэгами файлов. Потребуются файлы TechEd1.JPG, TechEd2.JPG и TechEd3.JPG из Competitors Pack.

Задача:

  • прочитать следующие EXIF свойства файлов:
  • Имя
  • когда снимок был сделан
  • свойство Make
  • и модель камеры, которой был сделан снимок

Решение:

Задача не такая и сложная, на самом деле, как кажется. Для начала нам потребуется класс System.Drawing.Bitmap и его метод GetPropertyItem. Для работы с этим классом нужно подключить библиотеку System.Drawing.dll:

[void][reflection.assembly]::loadfile("C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll")

и создать объект System.Drawing.Bitmap:

$drawing = New-Object system.drawing.bitmap -ArgumentList .\TechEd1.jpg

Этот объект будет содержать всё нам необходимое. Чтобы посмотреть нужные нам свойства, нужно получить где-то список всех ID, которым соответствуют эти свойства. К сожалению я не нашёл такого списка на MSDN, а только отдельными частями в интернете:
http://www.exif.org/specifications.html

там в конце PDF файла есть Appendix с таблицой ID на эту тему. Если посмотреть в таблицу, то увидим, что Model находится под ID = 272. Глянем, что там есть:

[↓] [vPodans] $drawing = New-Object system.drawing.bitmap -ArgumentList .\TechEd1.jpg
[↓] [vPodans] $drawing.GetPropertyItem(271).value
67
97
110
111
110
0
[↓] [vPodans]

Как-то не очень радует такой вывод. Но если внимательно посмотреть на эти цифры, то можно заметить, что они не превышают число 256, что подсказывает, что это массив ASCII байтов. Эти ASCII байты можно сконвертировать в элемент вот так: [char]ASCII_Number. А т.к. это массив, то его нужно собрать либо методом ToString() или оператором –Join, который есть в PowerShell V2:

[↓] [vPodans] $drawing = New-Object system.drawing.bitmap -ArgumentList .\TechEd1.jpg
[↓] [vPodans] $drawing.GetPropertyItem(271).value
67
97
110
111
110
0
[↓] [vPodans] -join ($drawing.GetPropertyItem(271).value | %{[char]$_})
Canon
[↓] [vPodans]

Вот так оно всяко лучше стало. По такой же схеме выбираем и собираем все остальные свойства:

[void][reflection.assembly]::loadfile("C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll")
dir *.jpg | %{
    $drawing = New-Object system.drawing.bitmap -ArgumentList $_
    $photo = "" | Select Name, Author, Make, Model, DateTaken
    $photo.Name = $_.Name
    $photo.Author = -join ($drawing.GetPropertyItem(315).value | %{[char]$_})
    $photo.Make = -join ($drawing.GetPropertyItem(271).value | %{[char]$_})
    $photo.Model = -join ($drawing.GetPropertyItem(272).value | %{[char]$_})
    $photo.DateTaken = -join ($drawing.GetPropertyItem(36867).value | %{[char]$_})
    $photo
} | ft -AutoSize
[↓] [vPodans] [void][reflection.assembly]::loadfile("C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll")
[↓] [vPodans] dir *.jpg | %{
>>     $drawing = New-Object system.drawing.bitmap -ArgumentList $_
>>     $photo = "" | Select Name, Author, Make, Model, DateTaken
>>     $photo.Name = $_.Name
>>     $photo.Author = -join ($drawing.GetPropertyItem(315).value | %{[char]$_})
>>     $photo.Make = -join ($drawing.GetPropertyItem(271).value | %{[char]$_})
>>     $photo.Model = -join ($drawing.GetPropertyItem(272).value | %{[char]$_})
>>     $photo.DateTaken = -join ($drawing.GetPropertyItem(36867).value | %{[char]$_})
>>     $photo
>> } | ft -AutoSize
>>

Name        Author     Make   Model               DateTaken
----        ------     ----   -----               ---------
TechEd1.JPG Ed Wilson  Canon  Canon PowerShot G9  2009:05:12 10:41:03
TechEd2.JPG Ed Wilson  Canon  Canon PowerShot G7  2009:05:12 10:25:59
TechEd3.JPG Ed Wilson  Sony   A-9                 2009:05:12 10:26:48


[↓] [vPodans]

Вобщем, как видите, тут тоже ничего сверхсложного нету.

Работая с функциями в PowerShell можно столкнуться с одной особенностью – в качестве передачи аргументов в функции или скрипты вы можете использовать почти всё, кроме ключей (данных типа Switch). Их передавать можно, но тут есть одна особенность. Обычно это ощущается, когда вы работаете с командлетами. Возьмём простой пример:

function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
Write-Host '$Path is:' $Path
Write-Host '$Filter is:' $Filter
Write-Host '$Force is:' $Force
}

и вызовем эту функцию:

[↓] [vPodans] function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
>> Write-Host '$Path is:' $Path
>> Write-Host '$Filter is:' $Filter
>> Write-Host '$Force is:' $Force
>> }
>>
[↓] [vPodans] Test C:\ * -force
$Path is: C:\
$Filter is: *
$Force is: True
[↓] [vPodans]

В принципе, всё как и ожидалось. Но если внимательно посмотреть на последний аргумент, то мы увидим лишь True, т.е. увидим факт, что ключ Force был передан. Однако, PowerShell не умеет подставять (биндить) переменную $Force (равно как и другие переменные) как именованный параметр в другую команду. Чтобы в этом убедиться, мы попробуем сымитировать нашу функцию как командлет Get-ChildItem:

[↓] [vPodans] function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
>> Get-ChildItem $Path $Filter $Force
>> }
>>
[↓] [vPodans] Test C:\ * -Force
Get-ChildItem : A positional parameter cannot be found that accepts argument 'True'.
At line:2 char:14
+ Get-ChildItem <<<<  $Path $Filter $Force
    + CategoryInfo          : InvalidArgument: (:) [Get-ChildItem], ParameterBindingException
    + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

[↓] [vPodans]

Как видно, командлет Get-ChildItem не смог сопоставить последний аргумент ни с одним из своих параметров. Т.е. по факту выполнилась следующая строка:

Get-ChildItem -Path C:\ -Filter * True

PowerShell не смог сказать командлету, что мы указали ключ Force и хотим именно его передать в командлет. По факту в этом коде Get-ChildItem не знал, какие параметры ему были переданы и подставлял их на основе номера позиции параметра. А т.к. у Get-ChildItem нету параметра с порядковым номером 3 (с номером 1 идёт Path или LiteralPath, а с номером 2 идёт Filter. Остальные параметры именованные), то мы получили ошибку. Но всё же, как выкручиваться из этой ситуации? Вы можете как угодно пытаться подставить аргументы, но ничего не выйдет. Для этих целей в PowerShell V2 появилась специальная переменная - $PSBoundParameters. Эта переменная по сути представляет собой хэш-таблицу:

[↓] [vPodans] function Test ([string]$Path, [String]$Filter, [switch]$Force) {
>> $PSBoundParameters
>> }
>>
[↓] [vPodans] Test C:\ * -Force

Key                                                         Value
---                                                         -----
Force                                                       True
Path                                                        C:\
Filter                                                      *


[↓] [vPodans]

В отличии от первого примера переменная $PSBoundParameters содержит не только значения переменных, но и их имена (в перовм примере я вручную дописывал имена переменных), которые используются в качестве именованных параметров. Т.е. при подстановке аргументов в команду, она сначала выбирает имя переменной в качестве именованного параметра и значение переменной подставляет в качестве аргумента этого параметра. Синтаксис использования этой переменной очень прост:

function Name ($arg1, $arg2, $arg3 ... $argN) {
Command @PSBoundParameters
}

и в результате будет исполняться вот такая команда:

Command –arg1 <значение $arg1> –arg2 <значение $arg2> –arg3 <значение $arg3> … –argN <значение $argN>

Т.е. будут подставляться именованные параметры и значения переменных соответствующих аргументов. Давайте проверим, как это подействует на наш пример с Get-ChildItem:

function Test ([string]$Path, [String]$Filter, [switch]$Force) {
Get-ChildItem @PSBoundParameters
}

[↓] [vPodans] function Test ([string]$Path, [String]$Filter, [switch]$Force) {
>> Get-ChildItem @PSBoundParameters
>> }
>>
[↓] [vPodans] Test C:\ * -Force


    Directory: C:\


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d--hs       10.07.2007.     19:44            $Recycle.Bin
d--hs       09.03.2008.     17:53            Boot
d----       23.11.2008.     19:41            inetpub
d-rh-       26.06.2007.     22:27            MSOCache
d----       09.03.2008.     17:41            PerfLogs
<...>

И вы можете видеть, что у нас всё получилось в наилучшем виде! В сравнении с предыдущим примером фактически выполнилась следующая команда:

Get-ChildItem -Path C:\ -Filter * -Force

Для формирования аргументов для командлетов в функциях (так называемые wrapped cmdlets) это самый красивый и идеальный вариант. Если вы захотите изменить логику стандартных командлетов или добавить в них свой функционал, то $PSBoundParamters сделает за вас очень много лишней работы. Но это не единственное полезное применение для этой переменной. Она так же позволяет сократить возможность ошибки при вызове функции внутри скрипта или другой функции, которая принимает те же аргументы. Давайте посмотрим ещё один пример:

function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
    Write-Host '$Path in Test is:' $Path
    Write-Host '$Filter in Test is:' $Filter
    Write-Host '$Force in Test is:' $Force
    Write-Host --------------------------------
    function Test2 ([switch]$Force, [string[]]$Path, [String]$Filter) {
Write-Host '$Path in Test2 is:' $Path
Write-Host '$Filter in Test2 is:' $Filter Write-Host '$Force in Test2 is:' $Force } Test2 @PSBoundParameters }

Что мы делаем: мы создали функцию Test, которая принимает набор аргументов. Внутри этой функции есть другая функция Test2, которая принимает те же аргументы. Далее из функции Test вызываем функцию Test2 и с помощью $PSBoundParameters передаём в неё аргументы. Обратите внимание, что в функции Test2 я изменил порядок аргументов. Это сделано для того, чтобы показать, что $PSBoundParamters отсортирует наши аргументы. А теперь внимание на экран:

[↓] [vPodans] function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
>>     Write-Host '$Path in Test is:' $Path
>>     Write-Host '$Filter in Test is:' $Filter
>>     Write-Host '$Force in Test is:' $Force
>>     Write-Host --------------------------------
>>     function Test2 ([switch]$Force, [string[]]$Path, [String]$Filter) {
>>         Write-Host '$Path in Test2 is:' $Path
>>         Write-Host '$Filter in Test2 is:' $Filter
>>         Write-Host '$Force in Test2 is:' $Force
>>     }
>>     Test2 @PSBoundParameters
>> }
>>
[↓] [vPodans] Test C:\ * -Force
$Path in Test is: C:\
$Filter in Test is: *
$Force in Test is: True
--------------------------------
$Path in Test2 is: C:\
$Filter in Test2 is: *
$Force in Test2 is: True
[↓] [vPodans]

И смотрите, что у нас получилось. А у нас получилось, что функция Test2 получила тот же набор аргументов, причём они были подставлены в правильном соответствии. Фактически строка Test2 @PSBoundParameters была преобразована в:

Test2 -Path $Path -Filter $Filter -Force $true

Т.е. в вариант, который мы вынуждены использовать в PowerShell 1.0. Плюс, мы имеем возможность подставлять позиционно ключи (объекты типа Switch), что есть очень позитивно.

Навеяно множеством мотивов:

Как мне кажется, с Write-Progress вряд ли получится что-то хорошее, поэтому немного сориентировался и нашёл вот это: PowerShell Script: Copy-FilePlus. Именно этот скрипт взят за основу визуального окна. А за основу логики был взят мой простенький скрипт копирования файлов с сохранением структуры каталогов:Лёгкая разминка. Приведённый по ссылке на Copy-FilePlus вариант Хала Роттенберга является достаточно базовым, поскольку не позволяет копировать за раз множество файлов, плюс требуется вводить имя конечного файла. Я решил его немного расширить и добавить следующий функционал:

  1. при вызове функции достаточно указать конечную директорию, т.е. имя конечного файла вводить не надо
  2. поддержка массового копирования файлов как через конвейер, так и через передачу исходных файлов в аргументы
  3. поддержка копирования с сохранением дерева каталогов относительно определённой точки монтирования дерева

заранее оговорюсь, что скрипт обладает одним недостатком: не будет единого прогресс-бара для всех файлов, а только для каждого файла свой (издержки .NET). Зато хоть что-то и весьма актуально при копировании больших файлов. Код получился несколько солидным, но я его постарался снабдить комментариями о коде и о логике, на которой он работает. Причём следуя примеру Роттенберга я скрипт тоже оформил в Advanced Function. По advanced functions я в скрипте вложил ссылки, как почитать о них во встроенной справке PowerShell. И, собственно, сам код:

########################################################
# Copy files with GUI.ps1
# Version 1.3
#
# Copies single or couple files with GUI progressbar
# 
# Original idea: Oisín Grehan
# First edition: Hal Rottenberg
# Second edition: Vadims Podans
#
# Vadims Podans (c) 2009
# http://www.sysadmins.lv/
########################################################

# сразу после названия идёт описание к функции. После загрузки функции в консоль
# её справка будет доступна в консоли. Достаточно будет набрать:
# Get-Help Copy-FilesPlus
# вобщем, как в настоящих командлетах
function Copy-FilesPlus {
<#
.Synopsis
    Copies files and folders displaying GUI progress bar.
.Description
    This is a script, that demonstrates how PowerShell can use
    useful .NET types and PowerShell V2 capabilities.
.Parameter Path
    Specifies the filename or FileInfo object representing file to be copied.
    Objects can be passed through a pipeline.
.Parameter Destination
    Specifies the path for resulting copy operation
.Parameter Recurse
    Gets the items in the specified locations and in all child items of the locations.
    Used only when source directory passed through argument list. 
.Parameter Force
    Creates directory structure in destination folder and copies files to
    their source respective folders (Tree copy).
.EXAMPLE
    PS > Copy-FilesPlus -Path C:\tmp -Destination e:\Users
    
    This will copy only files from C:\tmp to E:\Users
.EXAMPLE
    PS > Get-Item C:\tmp\windows7.iso | Copy-FilesPlus -Destination E:\Users
    
    This will copy specified file from C:\tmp folder to e:\Users
.EXAMPLE
    PS > Get-Childitem D:\Shared | Copy-FilesPlus -Destination E:\ 
    
    This will copy all files from Shared folders to E: drive root directory
.EXAMPLE
    PS > Get-Childitem D:\Shared -Recurse | Copy-FilesPlus -Destination E:\ -Force
    
    This will copy all files in Shared folder and subfolders. Shared folder will be a
    tree root point. All directory structure will be copied with files to destination folder.
.EXAMPLE
    PS > Copy-FilesPlus C:\Users\User E:\ -Recurse
    
    This will copy all files from User folder and subfolders to destination directory without
    copying source folders tree
.EXAMPLE
    PS > Copy-FilesPlus C:\Users\User E:\ -Recurse -Force
    
    This will copy all files in User folder and subfolders. User folder will be a
    tree root point. All directory structure will be copied with files to destination folder.
.ReturnValue
    Genrally, script don't return anything, except errors!
.Link
    about_functions
    about_functions_advanced
    about_functions_advanced_methods
    about_functions_advanced_parameters
#Requires -Version 2.0
#>

# ну и теперь фишки от advanced functions в V2. CmdletBinding делает
# подстановку передаваемых аргументов в функцию. Если аргумент не передан
# то PowerShell попросит его ввести, а не вывалится с ошибкой
[CmdletBinding()]
    param (
        # первый аргумент. Он является обязательным и он может принимать значения
        # из конвейера. Причём, внутри блока Process {} для обозначения текущего элемента
        # можно использовать, как переменную $path, так и $_.
        [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
        $Path,
        [Parameter(Mandatory = $true)]
        [string]$Destination,
        [switch]$Recurse,
        [switch]$Force
    )
    begin {
        # пробуем создать папку назначения, куда будут копироваться файлы.
        [void](md $Destination -Force -ea 0)
        # вот тут я сделал переменную для счётчика. Счётчик мне потребуется для того,
        # чтобы при использовании ключа -Force и если файлы передаются по конвейеру
        # можно было брать точку начала дерева структуры, которая будет копироваться.
        $n = 0
        # временная функция, которая выполняет само копирование. Тут нужно учесть то,
        # что путь назначения должен указываться в полном формате с указанием имени
        # конечного файла. Относительные пути тут не поддерживаются. Поэтому дальше
        # в коде я буду сохранять имя оригинального файла. т.е. переименовывание файлов
        # на лету не поддерживается
        function _routinecopy_ ([string]$Destination) {
            process {
                Add-Type -AssemblyName microsoft.visualbasic
                [Microsoft.VisualBasic.FileIO.FileSystem]::CopyFile($_, $Destination, 
                [Microsoft.VisualBasic.FileIO.UIOption]::AllDialogs,
                [Microsoft.VisualBasic.FileIO.UICancelOption]::ThrowException)
            }
        }
    }
    process {
        try {
            # вот здесь я проверяю, откуда пришли файлы - через аргументы или через конвейер
            # если данные пришли из обоих путей, то приоритет за конвейером
            if ($_) {
                # проверяем, что объект существует и что это объект файловой системы
                $File = gi $Path.FullName -ea stop | ?{$_.PsProvider -match "FileSystem$"}
                if ($File) {
                    # если объект в порядке и выполняется только первая итерация конвейера
                    # то мы задаём точку начала дерева. Весь путь от этой точки до имени файла
                    # будет копироваться в папку назначения
                    if ($n -eq 0) {
                        # здесь отрезаем от файла структуру папок, которая будет являться границей дерева
                        $RootPoint = $Path.FullName -replace $([regex]::Escape($Path.Name))
                        # заодно на основе этой структуры делаем регулярное выражение. Этим регулярным
                        # выражением будем у всех последующих файлов отрезать начало и оставлять необходимую
                        # часть дерева
                        $RootRegEx = [regex]::Escape($RootPoint.Substring(0,$RootPoint.Length -1))
                        # важно, что эту операцию нужно проделать единожды, чтобы точка монтирования дерева
                        # больше не менялась в процессе. Поэтому увеличиваем счётчик и тогда в течении текущего
                        # процесса копирования код сюда не вернётся
                        $n++
                    }
                    # проверяем, что нужно ли копировать дерево или нет.
                    if ($Force) {
                        # если копируем дерево, то выбрасываем папки и работаем только с файлами
                        $File = $File | ?{!$_.PsIsContainer}
                        if ($File) {
                            # если есть файлы для копирования, то выбираем весь путь папок до текущего файла
                            # и заранее приготовленным регэкспом отрезаем начало. В переменную $rep мы запишем
                            # дерево папок от точки монтирования дерева до имени файла
                            $rep = $Path.Directory.ToString() -replace $RootRegEx
                            # а теперь к папке назначения пристыковываем дерево папок от точки монтирования до имени файла
                            $DestFolder = Join-Path $Destination $rep
                            # заранее создаём начальный хвостик папок в целевой папке и подавляем вывод на экран
                            [void](md $DestFolder -Force -ea 0)
                            # а теперь к новому конечному пути пристыковываем имя файла
                            $Dest = Join-Path $DestFolder $Path.Name
                            # и теперь подаём текущий файл в функцию копирования. Вот тут мы и увидим прогресс-бар.
                            $Path.FullName | _routinecopy_ $Dest
                        }
                    } else {
                        # если структуру папок копировать не надо, то все файлы, что пришли с конвейера будут копироваться
                        # в папку назначения без создания структуры папок.
                        if (!$_.PsIsContainer) {
                            $Dest = Join-Path $Destination $File.Name
                            $File | _routinecopy_ $Dest
                        }
                    }
                # если объект не существует или это не объект файловой системы (защита от дураков, да-да :)), то ругаемся
                } else {throw "Input object does not represent any applicable FileSystem object"}
            } else {
                # если данные об источнике копирования переданы через аргументы, то проверяем, что заданный путь допустим
                # и объект, который мы получим после Get-Item является объектом файловой системы
                $File = gi $Path -ea stop | ?{$_.PsProvider -match "FileSystem$"}
                if ($File) {
                    # если всё хорошо, то указанный файл или папка становятся точкой монтирования дерева
                    $RootPoint = Resolve-Path $Path
                    # если ключи -Force -Recurse не указаны, то копируется либо указанный файл или все файлы в указанной
                    # папке в папку назначения
                    if (!$Recurse) {
                        dir $RootPoint | ?{!$_.PsIsContainer} | %{$Dest = Join-Path $Destination $_.Name
                            $_.FullName | _routinecopy_ $Dest}
                    # хитрый режим, когда рекурсивно выбираются все файлы в указанной папке и всех подпапках и копируются
                    # в папку назначения без сохранения структуры (т.е. в папке назначения будет большая куча файлов).
                    } elseif ($Recurse -and !$Force) {
                        dir $RootPoint -Recurse | ?{!$_.PsIsContainer} | %{$Dest = Join-Path $Destination $_.Name
                            $_.FullName | _routinecopy_ $Dest}
                    # если указаны оба ключа, то мы повторяем структуру исходных папок относительно точки монтирования дерева
                    # в папке назначения
                    } elseif ($Recurse -and $Force) {
                        dir $RootPoint -Recurse | ?{!$_.PsIsContainer} | %{
                            # тут мы делаем то же самое, что и в случае, когда файлы пришли с конвейера
                            $RootRegEx = [regex]::Escape($RootPoint)
                            $rep = $_.Directory.ToString() -replace $RootRegEx
                            $DestFolder = Join-Path $Destination $rep
                            [void](md $DestFolder -Force -ea 0)
                            $Dest = Join-Path $DestFolder $_.Name
                            $_.FullName | _routinecopy_ $Dest
                        }
                    }
                } else {throw "Input object does not represent any applicable FileSystem object"}
            }
        } catch {$_}
    }
}
Вот такой скриптик у меня получился. Местами даже очень умный, почти как я :-). Как мне кажется, на этом коде тоже можно чему-то поучиться. Если будут вопросы – то welcome в комментарии. :-)

Тут обнаружился один интересный командлет – ConvertFrom-StringData, который позволяет преобразовывать строку в хэш-таблицы. Скажем, есть файл вида:

ключ1 = значение1
ключ2 = значение2
ключ3 = значение3
ключ4 = значение4

при этом иногда очень хочется работать с этими строками как с объектами. Т.е. при указании объекта и его ключа, например, $a.key1 получить его значение. Как это делается в хэш-таблицах. Вот пример:

[vPodans] $a = @{"key1"="value1";"key2"="value2"} [vPodans] $a Name Value ---- ----- key2 value2 key1 value1 [vPodans] $a.key1 value1 [vPodans] $a.key2 value2 [vPodans]

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

[vPodans] $a = "key1 = value1" [vPodans] $a key1 = value1 [vPodans] $a.GetType().FullName System.String [vPodans] $b = ConvertFrom-StringData -StringData $a [vPodans] $b Name Value ---- ----- key1 value1 [vPodans] $b.GetType().FullName System.Collections.Hashtable [vPodans] $b.key1 value1 [vPodans]

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

[vPodans] $a = gc keys.txt [vPodans] $a key1 = value1 key2 = value2 key3 = value3 key4 = value4 [vPodans] $a[0] key1 = value1 [vPodans] ConvertFrom-StringData -StringData $a ConvertFrom-StringData : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'StringData '. Specified method is not supported. At line:1 char:35 + ConvertFrom-StringData -StringData <<<< $a + CategoryInfo : InvalidArgument: (:) [ConvertFrom-StringData], ParameterBindingException + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.ConvertFromStringDataCommand [vPodans]

командлет Get-Content (или его алиас GC, не путать с глобальным каталогом) читает файл построчно в виде массива. Следовательно каждая строка является отдельным элементом массива. Как быть в такой ситуации? На первый взгляд может показаться, что можно Get-Content разобрать через Foreach-Object и уже отдельные элементы массива подавать конвертеру в качестве строк. Смотрим:

[vPodans] $a = gc keys.txt | %{ConvertFrom-StringData -StringData $_} [vPodans] $a Name Value ---- ----- key1 value1 key2 value2 key3 value3 key4 value4 [vPodans] $a.key1 [vPodans] $a.GetType().FullName System.Object[] [vPodans] $a[0].key1 value1 [vPodans]

Как видите, мы на выходе получили не одну хэш-таблицу, а массив одиночных хэш-таблиц и для доступа к его ключам и значениям нужно ещё указывать номер элемента в массиве. Чтобы решить данную проблему нужно каким-то образом прочитать файл не построчно, а в виде целой строки. Для чтения файла целиком можно воспользоваться методом ReadAllText класса File Class. Как видно из описания метода, он читает текст в единую строку. Давайте посмотрим, что у нас получится:

[vPodans] $a = [io.file]::ReadAllText("keys.txt") [vPodans] $a key1 = value1 key2 = value2 key3 = value3 key4 = value4 [vPodans] $a.GetType().FullName System.String [vPodans] $b = ConvertFrom-StringData -StringData $a [vPodans] $b Name Value ---- ----- key2 value2 key4 value4 key1 value1 key3 value3 [vPodans] $b.key1 value1 [vPodans] $b.key2 value2 [vPodans] $b.key3 value3 [vPodans] $b.key4 value4 [vPodans]

или просто в одну строчку:

ConvertFrom-StringData -StringData ([io.file]::ReadAllText("keys.txt"))

Вот так просто в стиле The PowerShell Way (в одну строчку) можно текстовые файлы сконвертировать в хэш-таблицы и работать с ними как с объектами, что есть удобно и полезно.

Продолжая цикл постов о новых командлетах в PowerShell V2 CTP3 хочу рассказать про несколько командлетов для управлением компьютера как клиента в домене Active Directory или рабочей группе. Отмечу сразу, что эти командлеты не управляют доменом Active Directory, а только компьютером-клиентом домена (не знаю, как по-русски правильно сформулировать мысль). По сути эти командлеты повторяют аналог утилиты netdom.exe и вот их список:

  • Add-Computer
  • Remove-Computer
  • Rename-Computer
  • Reset-ComputerMachinePassword
  • Test-ComputerSecureChannel

и ещё 2 на закуску:

  • Restart-Computer
  • Stop-Computer

Ну и как обычно - рассмотрим каждый из них:

1) Add-Computer - добавляет компьютер к домену Active Directory или перемещает компьютер между рабочими группами (Workgroup). Данный командлет содержит следующие параметры и ключи:

  • -ComputerName - параметр. Имя компьютера, который нужно включить в домен или рабочую группу. Можно указывать как NetBIOS имена, так и FQDN (Fully Qualified Domain Name) и IP адреса компьютеров. Для локального компьютера можно использовать точку ( . ) или localhost или не указывать этот параметр совсем, поскольку по умолчанию будет использовать именно локальная машина. Так же можно указывать несколько компьютеров перечисляя их через запятую. Имена можно передавать в командлет по конвейеру по свойству ComputerName.
  • -Credential - параметр. Учётные данные для аутентификации в домене или на удалённых компьютерах рабочей группы. Формат учётных данных такой же, как и в подключениях WMI. Для этого можно использовать командлет Get-Credential. Данный параметр следует использовать только при необходимости использования альтернативных учётных данных. Если не указан, то будут использоваться учётные данные текущего пользователя.
  • -DomainName - параметр. DNS имя домена, куда будет подключаться компьютер. Этот параметр является обязательным при подключении к домену.
  • -WorkgroupName - параметр. Название рабочей группы, в которую перемещается компьютер. Обязателен при переводе компьютера из одной рабочей группы в другую.
  • -OUPath - параметр. Если заранее учётная запись компьютера заранее не создана в AD, то указание этого параметра позволяет добавить компьютер в нужный OU с созданием учётной записи в нём. Если этот параметр не указан, то компьютер будет помещён в контейнер по умолчанию - CN Computers. Так же этот параметр не нужно указывать при перемещении компьютера между рабочими группами.
  • -Server - параметр. Используется только при подключении к домену. Позволяет выбирать контроллер домена, который будет производить ввод компьютера в домен и установку пароля компьютера в домене. Параметр опциональный и может иметь смысл при добавлении машин в домен в удалённых сайтах. Указывается в формате DomainName\DCName.
  • -Unsecure - ключ. Операция по вводу компьютера в домен или рабочую группу будет производиться без установки защищённого канала между клиентом и сервером (контроллером домена). В домене с жёсткими политиками безопасности данный ключ может дать сбой.
  • -Reboot - ключ. Как известно любое изменение местоположения компьютера в сети (ввод в домен, вывод из домена, смена рабочей группы) требуют перезагрузки машины.

Примечание: может показаться, что ключ -Reboot очень полезный, но мне так не кажется. И вот почему. Если командлет сам не перезагружает машину после завершения операции, то это нужно сделать вручную. Но перезагружать машину можно только в случае если всё прошло успешно. Но если по каким-то причинам ожидаемый результат не был достигнут, то компьютер будет перезагружен в пустую. Я на этом не настаиваю, но мне это видится именно так.

Hint: не забывайте, что если указываются позиционные параметры, то их имена указывать не обязательно. Например, ComputerName позиционно является первым параметром и если указывать за командлетом сразу имена компьютеров, то название параметра -ComputerName можно и не указывать.

По умолчанию данный командлет не выводит результат на экран, поэтому для вывода результата исполнения можно использовать ключи -PassThru или -Verbose. Так же командлет имеет такие полезные ключи как -Confirm для ручного подтверждения выполнения операции и -WhatIf для моделирования работы командлета. И несколько примеров использования командлета:

Add-Computer -DomainName contoso.com -Credential (Get-Credential) -OUPath OU=Test,OU="3th floor",DC=Contoso,DC=com -Reboot
Add-Computer -ComputerName (Get-Content Comps.txt) -WorkgroupName MSHome -PassThru

2) Remove-Computer - удаляет компьютер из домена или рабочей группы. Содержит параметры -ComputerName, -Credential (учётные данные пользователя, который имеет право на вывод машины из домена или локального администратора, если компьютер перемещается в рабочих группах), -Reboot, -Confirm, -PassThru и -WhatIf. Правила использования этих параметров и ключей такие же, что и для Add-Computer, но только в контексте удаления, а не ввода машины во что-то. Пример:

Remove-Computer -ComputerName computer1, computer2 -Credential contoso.com\Administrator -PassThru -Reboot - выводит компьютер из домена. Следует учесть, что при выполнении команды будет запрошен пароль учётной записи, указанной в Credential.
Remove-Computer MyComputer
- просто удаляет компьютер из рабочей группы (интересно, куда? :-D )

Примечание: ни в коем случае не удаляйте так контроллеры домена. Их сначала нужно понизить до роли рядового сервера командой dcpromo и только потом с помощью UI, netdom или командлета Remove-Computer выводить из домена.

3) Rename-Computer - переименовывает компьютер или компьютеры в рабочей группе или домене Active Directory. Обычно использует следующие параметры:

  • -ComputerName - имя компьютера, которые нужно переименовать. Можно не указывать, если переименовывается локальный компьютер. Если компьютер находится в домене, то следует указывать его полное FQDN имя. Не поддерживает указание нескольких компьютеров, только одного.
  • -NewComputerName - новое имя компьютера. Если машина находится в домене, то переименовывается и её учётная запись.
  • -Credential - учётные данные. Данный параметр является обязательным при переименовании удалённых компьютеров-членов домена (кроме переименовывания локального компьютера).

так же командлет содержит такие ключи как -Confirm, -Reboot и -WhatIf.

Примечание: ни в коем случае не пытайтесь этим командлетом переименовать контроллер домена!

4) Reset-ComputerMachinePassword - сбрасывает пароль учётной записи компьютера в базе Active Directory. Используется только для доменных компьютеров. Данный командлет можно использовать при трудностях аутентификации компьютера в домене или при устаревании пароля. Несколько типичных случаев, когда требуется сброс пароля компьютера - компьютер не аутентифицировался в домене более 30 или 60 дней, в зависимости от настроек домена; компьютер был восстановлен из бэкапа (SystemState), срок которого старше 30 или 60 дней или компьютер был восстановлен из образа без дополнительного восстановления актуального SystemState и другие случаи.. Подробности этой темы выходят за рамки этого поста.

Из актуальных параметров содержит -ComputerName (можно указывать несколько компьютеров. Допускаются FQDN, NetBIOS имена или IP адреса), -Server - имя контроллера домена, который будет производить сброс пароля (не обязательный параметр) и -Credental - имя пользователя, который имеет права сброса паролей указанных компьютеров. Из ключей можно выделить такие как -Confirm и -WhatIf. Общий синтаксис такой:

Reset-ComputerMachinePassword (get-content comp.txt) - переустановит пароль всех компьютеров из списка comp.txt
Reset-ComputerMachinePassword - переустановит пароль текущего компьютера
Reset-ComputerMachinePassword -Server dc1.contoso.com -Credential (Get-Credential) -Confirm - переустановит пароль локального компьютера на контроллере домена с именем DC1 и с вводом альтернативных учётных записей. После запуска потребует подтверждения операции.

5) Test-ComputerSecureChannel - проверяет возможность установки безопасного канала между клиентом и сервером. Работает только в домене и возвращает True или False. Имеет один параметр и ключ:

  • -Server - параметр. Имя компьютера в домене (допускается использовать FQDN, NetBIOS имена или IP адрес). Допускается указание только одного компьютера, с которым хотите проверить работоспособность безопасного канала. Параметр обязательный, поскольку нету смысла проверять защищённый канал с самим собой :-)
  • -Repair - ключ. Если команда выводит False, то при указании этого ключа команда будет пытаться восстановить его.

И содержит дополнительные ключи как -Confirm и -WhatIf.

Я не придумал к какой категории присвоить эти 2 командлета и решил их описать здесь. Кстати, очень удобные командлеты:

6) Restart-Computer и Stop-Computer. Первый командлет перезагружает локальный или удалённый компьютер (или несколько), а второй выключает их совсем. Это очень полезно, поскольку я видел как минимум 5 различных решений перезагрузки/выключения компьютеров в скриптах PowerShell. Как правило это либо WMI, либо использование штатного shutdown.exe (кстати говоря, я им пользуюсь всегда) либо ещё что-нибудь. Теперь можно будет этот момент стандартизировать. Много говорить про них не буду, а скажу только, что эти командлеты используют метод Win32Shutdown WMI класса Win32_OperatingSystem. Следовательно эти командлеты обладают всеми новыми возможностями, которые описаны в предыдущей статье: Обзор новых командлетов PowerShell V2 CTP3 - WMI. Одно из преимуществ - можно использовать фоновую работу. Скажем, отправить в ребут сотню компьютеров и пока это всё происходит спокойно работать в консоли (шутка :-) ). Поэтому в этих командлетах можно использовать такие параметры и ключи как -AsJob, Impersonate, Authentication, ComputerName и другие. Из уникальных отмечу один параметр:

  • -ThrottleLimit - указывает количество одновременных удалённых подключений для конкретно этой команды. Если компьютеров будет много в этой команде, то чтобы не загружать сеть можно ограничить, скажем, по 10-20 одновременных подключений. По умолчанию максимум одновременно установлено 32 подключения.

и ключ:

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

На сегодня вроде всё. Вроде ничего не пропустил.