Posts on this page:
Навеяно множеством мотивов:
Как мне кажется, с Write-Progress вряд ли получится что-то хорошее, поэтому немного сориентировался и нашёл вот это: PowerShell Script: Copy-FilePlus. Именно этот скрипт взят за основу визуального окна. А за основу логики был взят мой простенький скрипт копирования файлов с сохранением структуры каталогов:Лёгкая разминка. Приведённый по ссылке на Copy-FilePlus вариант Хала Роттенберга является достаточно базовым, поскольку не позволяет копировать за раз множество файлов, плюс требуется вводить имя конечного файла. Я решил его немного расширить и добавить следующий функционал:
заранее оговорюсь, что скрипт обладает одним недостатком: не будет единого прогресс-бара для всех файлов, а только для каждого файла свой (издержки .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 в комментарии. :-)
Сегодня самый главный PowerShell Guy – Marc van Orsouw, он же MoW, он же /\/\o\/\/ выпустил очередную версию PowerTab, в которой исправлены баги, которые были замечены в работе с PowerShell V2 CTP3, плюс добавлена поддержка Windows 7 и Windows Server 2008 R2. Один из наиболее значимых багов – зависание окна автозавершения. Это выглядело вот так:
[vPodans] dir -include *.log
[vPodans] ╔═ - ══════════════╗
║ -Debug ║
║ -ErrorAction ║
║ -ErrorVariable ║
║ -Exclude ║
║ -Filter ║
║ -Force ║
║ -Include ║
║ -LiteralPath ║
║ -Name ║
║ -OutBuffer ║
║ -OutVariable ║
║ -Path ║
║ -Recurse ║
║ -UseTransaction ║
║ -Verbose ║
║ -WarningAction ║
║ -WarningVariable ║
╚═[4] 1-17 [17]════╝
т.е. по этому меню можно было перемещаться вниз. Но при любом нажатии стрелки вверх – оно намертво прилипало к экрану:
[vPodans] dir -include *.log
[vPodans] bla-bla-bla═════════════╗
The term 'bla-bla-bla' is not recognized as a cmdlet, function, operable program, or script file. Verify the term and t
ry again. ║ -ErrorAction ║
At line:1 char:12-ErrorVariable ║
+ bla-bla-bla <<<< xclude ║
+ CategoryInfo : ObjectNotFound: (bla-bla-bla:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
║ -Include ║
[vPodans] ║ -LiteralPath ║
║ -Name ║
║ -OutBuffer ║
║ -OutVariable ║
║ -Path ║
║ -Recurse ║
║ -UseTransaction ║
║ -Verbose ║
║ -WarningAction ║
║ -WarningVariable ║
╚═[4] 1-17 [17]════╝
а учитывая, что я частенько промахиваюсь со стрелками, то вот такую картину видел тоже часто. И убрать это можно было только через Clear-Host, он же CLS. Сейчас установил новую версию PowerTab – проблема исчезла :rock:
Ну и поддержка новых систем тоже будет многим по душе.
Собственно PowerTab взять можно тут (смотреть аттачменты посте):
http://thepowershellguy.com/blogs/posh/archive/2009/05/15/powertab-0-99b2-ctp3-fix.aspx
А знаете ли вы, что скрипты PowerShell можно очень легко и удобно использовать в качестве startup/shutdown и logon/logoff скриптов в GPO?
В обычной жизни при двойном клике на .ps1 файл – он откроется в редакторе, но не будет исполнен. Это было сделано в целях безопасности, что у PS1 файлов PerceivedType выставлен как Text и расширение PS1 отсутствует в переменной %pathext%. Но это совсем не значит, что мы не можем использовать эти скрипты в GPO или Task Sheduler. Ларчик открывается очень просто:
Суть сводится к тому, что в Script Name указывается путь к исполняемому модулю PowerShell. А вот уже в Script Parameters уже указываете путь к скрипту. Причём тут следует обратить внимание, что если путь задаёте через переменные (например, если у вас несколько контроллеров домена, то целесообразно запускать скрипт из папки NetLogon того контроллера, который вас аутентифицирует), то переменные нужно указывать в формате CMD, т.е. %variable%. В результате вы получите вот такой вид настроенного логон-скрипта:
Т.е. реализуется это всё очень просто. Однако, здесь есть одно большое “НО” – для реализации логон-скриптов в среде Windows Server 2003/2008 у вас должна быть реализована политика подписанных скриптов. В противном случае скрипт просто не исполнится, даже если у вас политика запуска скриптов выставлена в Unrestricted. Это обусловлено тем, что скрипт исполняется не с локального диска, а с сетевого. Это можно очень легко проверить:
[vPodans] Set-ExecutionPolicy unrestricted
[vPodans] Get-ExecutionPolicy
Unrestricted
[vPodans] & $env:logonserver\netlogon\get.ps1
Security Warning
Run only scripts that you trust. While scripts from the Internet can be useful, this script can
potentially harm your computer. Do you want to run \\DC1\netlogon\get.ps1?
[D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"):
У вас каждый раз будет спрашиваться разрешение на запуск скрипта. Т.к. логонные скрипты выполняются в фоне и не взаимодействуют с пользователем, то вы просто не сможете никак нажать кнопку R. Тем более, как видно из снимка, у нас нету возможности сделать Run always. Именно по этой причине требуются только подписанные скрипты. Но это не проблема, учитывая, что я у себя в блоге написал 2 поста по практической реализации инфраструктуры подписанных скриптов, тем более, это наоборот повышает безопасность запуска скриптов PowerShell.
Но если очень хочется выполнять логонные и стартапные скрипты без внедрения цифровых подписей, то есть один workaround - узел, на котором размещены скрипты в сети (обычно это сам контроллер домена) нужно добавить в интернет-зону Local Intranet. Тогда скрипты из сети будут равноценны локальным и политика RemoteSigned спокойно разрешит такой запуск.
Пока что Windows Server 2008 R2 и Windows 7 не RTM, но уже известно, что в них уже нативно поддерживаются скрипты PowerShell в качестве логонных и стартапных, поскольку в этих системах PowerShell установлен и включен по умолчанию. И выглядит это вот так:
В табе Scripts располагаются классические скрипты как .BAT, .CMD, .VBS, .JS и в комментарии не нуждаются. Но в новых системах добавлен ещё один таб PowerShell scripts. Вы можете прямо указывать на .PS1 файлы без указания программы, которая будет их отрабатывать. Иными словами эти скрипты теперь ничем не отличаются от тех же скриптов CMD/WSH. Но в проводнике PS1 файлы всё равно не будут исполняться, а открываться в редакторе (ISE/Notepad/PowerGUI). Ну и ещё одна заметка:
Вы можете выбирать в каком порядке эти скрипты будут исполняться – до или после классических скриптов в первом табе. На практике есть один небольшой недостаток – при логоне частенько проскакивает консоль PowerShell, что есть не очень хорошо. Правда, следует учитывать, что новые системы ещё только Release Candidate (скриншоты сделаны с беты), поэтому есть надежда, что это будет исправлено. В отличии от предыдущего варианта, когда скрипты PowerShell адаптируются под классические – здесь не обязательно подписывать скрипты, хотя для этого придётся выставить политику запуска в Unrestricted, что не есть безопасно. Поэтому я бы посоветовал везде, где это возможно – использовать цифровые подписи для скриптов. И ещё раз напомню, что данная вкладка в GPO доступна только в Windows Server 2008 R2/Windows 7 и, скорее всего, в последующих версиях и применяться будет тоже только к ним. Если у вас домен под управлением Windows Server 2008 R2, а клиенты – Windows Vista, то эти логонные скрипты работать не будут, даже если на последних всеми правдами и неправдами :-) установлен PowerShell.
As always enjoy the automation of tools within powershell.exe! © Flowering Weeds
А знаете ли вы, что объекты типа PsObject можно создавать очень легко?
Очень часто в скриптах мы создаём свои кастомайзенные объекты, наделяем их необходимыми свойствами, в которые уже записываем данные, полученные в процессе работы скрипта. Как это делается в классическом случае:
$Object = New-Object System.Management.Automation.PSObject $Object | Add-Member NoteProperty Computer ([PSObject]$null) $Object | Add-Member NoteProperty Name ([PSObject]$null) $Object | Add-Member NoteProperty Path ([PSObject]$null) $Object | Add-Member NoteProperty Description ([PSObject]$null) ...
и так для добавления каждого свойства делать новую строчку. В итоге мы получим вот такой объект:
[vPodans] $Object = New-Object System.Management.Automation.PSObject
[vPodans] $Object | Add-Member NoteProperty Computer ([PSObject]$null)
[vPodans] $Object | Add-Member NoteProperty Name ([PSObject]$null)
[vPodans] $Object | Add-Member NoteProperty Path ([PSObject]$null)
[vPodans] $Object | Add-Member NoteProperty Description ([PSObject]$null)
[vPodans] $object | gm
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method System.Boolean Equals(Object obj)
GetHashCode Method System.Int32 GetHashCode()
GetType Method System.Type GetType()
ToString Method System.String ToString()
Computer NoteProperty Computer=null
Description NoteProperty Description=null
Name NoteProperty Name=null
Path NoteProperty Path=null
[vPodans]
Но этот процесс можно сделать ещё более простым и коротким:
$Object = "" | Select Computer, Name, Path, Description
Смотрим:
[vPodans] $Object = "" | Select Computer, Name, Path, Description
[vPodans] $object | gm
TypeName: Selected.System.String
Name MemberType Definition
---- ---------- ----------
Equals Method System.Boolean Equals(Object obj)
GetHashCode Method System.Int32 GetHashCode()
GetType Method System.Type GetType()
ToString Method System.String ToString()
Computer NoteProperty Computer=null
Description NoteProperty Description=null
Name NoteProperty Name=null
Path NoteProperty Path=null
[vPodans]
Видите, в результате объект получился такой же, зато как эффективно! Но и это ещё не всё. Если с Add-Member мы могли сразу присваивать значения параметру через параметр –Value командлета Add-Member, то и здесь мы можем на стадии создания объекта ему что-то присвоить. Присвоение производится по схеме:
$var = "" | Select @{n = "PropertyName";e={"PropertyValue"}}
Для примера сделаем простой объект с несколькими свойствами и сразу при создании запишем в них значения:
$Object = "" | Select @{n='Computer';e={"компик"}},`@ {n=Name;e={"имя"}},@{n="Path";e={"вот тут путь какой-то"}}
проверяем:
[vPodans] $Object = "" | Select @{n='Computer';e={"компик"}},`
>> @{n='Name';e={"имя"}},@{n="Path";e={"вот тут путь какой-то"}}
>>
[vPodans] $Object | gm
TypeName: Selected.System.String
Name MemberType Definition
---- ---------- ----------
Equals Method System.Boolean Equals(Object obj)
GetHashCode Method System.Int32 GetHashCode()
GetType Method System.Type GetType()
ToString Method System.String ToString()
Computer NoteProperty System.String Computer=компик
Name NoteProperty System.String Name=имя
Path NoteProperty System.String Path=вот тут путь какой-то
[vPodans] $Object | fl *
Computer : компик
Name : имя
Path : вот тут путь какой-то
[vPodans]
Вот такими небольшими трюками мы позволяем делать PowerShell очень удобным и простым инструментом.
Вобщем, как говорится - As always enjoy the automation of tools within powershell.exe! © Flowering Weeds :-)
з.ы. По причине краха виртуальных машин, продолжение темы про OCSP откладывается на некоторое время.
На ньюсгруппах недавно попросили скрипт с использованием PowerShell, который бы рекурсивно определённую папку на предмет содержания определённых файлов и копировал эти файлы в другую папку. Причём файлы копироваться должны вместе с деревом папок, относительно корня. Корень в данном случае является папка, откуда начинается рекурсивный поиск. Действительно, такие задачи встречаются, поэтому мой вариант кому-то может оказаться полезным.
$RootPoint = "D:\Users\Shared Documents" $dest = 'E:\DestFolder' $rep = $RootPoint.Replace("\","\\") dir $RootPoint -Include *.msi -Recurse | %{ $suf = $_.directory.tostring() -replace $rep New-Item -itemtype d -path $($dest + $suf) -force -ea 0 copy $_ $("$dest" + "$suf") }
Этот скрипт будет искать все MSI файлы в папке D:\Users\Shared Documents и подпапках. Если файл будет найден, то он будет скопирован в E:\SomeFolder с повторением структуры папок. Для начала я заэскейпил слеши в исходной папке. И в цикле уже отрезаю путь до корня обычным оператором –replace. После чего хвост пристыковываю к пути назначения, чтобы получить новую структуру папок и создаю нужные папки в месте назначения. И последней строкой копирую сам файл в уже подготовленную папку.
Решений для этой задачи на самом деле очень много и я постарался сделать наиболее простой вариант, для разминки мозгов, так сказать :)