PowerShell,我的经验转储

引言


本文面向的读者是那些已经熟悉PowerShell基础知识,使用stackexchange运行某些脚本,并且可能拥有自己的文本文件以及各种摘要以方便日常工作的读者。 编写它的目的是为了减少熵,提高公司中使用的PowerShell代码的可读性和可维护性,从而提高与之合作的管理员的工作效率。


kdpv


在以前的工作场所中,由于任务的特殊性以及世界的不完善性,我极大地提高了使用PowerShell的技能。 换工作后,这种负载大大减少了,在用新技术解决新问题的经验下,指尖附近的一切都开始下沉。 据此,本文声称只是它自己声明的内容,它揭示了一个主题列表,我认为这对我自己大约7年前是有用的,那时我对这个工具的了解才刚刚开始。


如果您不了解PowerShell为什么是面向对象的外壳,为什么会从中获得收益,以及为什么有必要这样做,我会为您推荐一本不错的书,尽管激起了仇恨,但它还是迅速地介绍了这种环境的本质-Andrey Vladimirovich Popov,Windows PowerShell简介 。 是的,它是关于PS的旧版本的,是的,该语言获得了一些扩展和改进,但是这本书很好,因为当描述这种环境的开发的早期阶段时,它无意中只强调了基本的内容。 我认为,环境中长满的语法糖在您不了解该概念如何工作的情况下很快就意识到了。 读这本书只需要几个晚上,读完以后再回来。


波波夫


该书也可以在作者的网站上找到,尽管我不确定这种使用的许可方式: https//andpop.ru/courses/winscript/books/posh_popov.pdf


风格指南


在所有样式应用中,根据样式指南设计脚本都是一种好的做法,几乎不会有两种意见。 一些生态系统已经在本地调整级别上解决了这一问题,Python社区中的pep8和Golang中的go fmt显而易见。 这些都是节省时间的宝贵工具,不幸的是,标准的PowerShell软件包中缺少这些工具,因此,问题就已经引起了我们的注意。 当前,解决统一代码格式问题的唯一方法是通过反复编写满足样式指南的代码来发展反思(实际上,没有)。


由于缺少Microsoft的正式批准和详细描述而导致的样式指南在PowerShell v3时代诞生于社区中,自那时以来,它们一直以github: PowerShellPracticeAndStyle的开放形式进行开发。 对于曾经在PowerShell ise中使用“保存”按钮的任何人来说,这都是一个值得注意的存储库。


如果您尝试进行挤压,则可能会归结为以下几点:


  • PowerShell使用PascalCase来命名变量,cmdlet,模块名称以及除运算符外的几乎所有内容。
  • 语言运算符(如ifswitchbreakprocess-match用很小的字母表示;
  • 圆括号以唯一真实的方式放置,否则也称为Kernigan和Richie的风格,他的故事来自《 C编程语言 》一书;
  • 不要在交互式控制台会话以外的任何地方使用别名,不要ps | ? processname -eq firefox | %{$ws=0}{$ws+=$_.workingset}{$ws/1MB}脚本文件中写入任何ps | ? processname -eq firefox | %{$ws=0}{$ws+=$_.workingset}{$ws/1MB} ps | ? processname -eq firefox | %{$ws=0}{$ws+=$_.workingset}{$ws/1MB} ps | ? processname -eq firefox | %{$ws=0}{$ws+=$_.workingset}{$ws/1MB}
  • 指示显式参数名称, cmdlet行为及其签名可能会更改 ,此外,这还会为不熟悉特定cmdlet的人添加上下文;
  • 设计用于调用脚本的参数,不要在脚本内部编写函数,最后一行调用此函数时需要更改全局变量的值而不是指定参数;
  • 指定[CmdletBinding()]-这将为您的cmdlet提供-Verbose-Debug标志以及许多其他有用的功能 。 尽管一些纯粹主义者在社区中持坚定立场,但我不支持在简单的内联函数和由几行​​文字组成的过滤器中指出此属性;
  • 编写基于注释的帮助:一个句子,故障单链接,呼叫示例;
  • #requires部分中指定所需的PowerShell版本。
  • 使用Set-StrictMode -Version Latest这将帮助您避免以下问题
  • 处理错误;
  • 不要急于在PowerShell中重写所有内容。 PowerShell主要是shell,而调用二进制文件是其直接任务。 在脚本中使用robocopy并没有错,当然不是rsync,但这也非常好。

基于注释的帮助


以下是如何获取帮助脚本的示例。 该脚本将图像构图成一个正方形并进行调整大小,我认为您的任务是为用户制作化身(也许根据exif数据的旋转除外)。 .EXAMPLE部分中有一个用法示例,请尝试一下。 由于PowerShell由公共语言运行库运行,与其他dotnet语言相同,因此它具有使用dotnet库的全部功能:


 <# .SYNOPSIS Resize-Image resizes an image file .DESCRIPTION This function uses the native .NET API to crop a square and resize an image file .PARAMETER InputFile Specify the path to the image .PARAMETER OutputFile Specify the path to the resized image .PARAMETER SquareHeight Define the size of the side of the square of the cropped image. .PARAMETER Quality Jpeg compression ratio .EXAMPLE Resize the image to a specific size: .\Resize-Image.ps1 -InputFile "C:\userpic.jpg" -OutputFile "C:\userpic-400.jpg"-SquareHeight 400 #> # requires -version 3 [CmdletBinding()] Param( [Parameter( Mandatory )] [string]$InputFile, [Parameter( Mandatory )] [string]$OutputFile, [Parameter( Mandatory )] [int32]$SquareHeight, [ValidateRange( 1, 100 )] [int]$Quality = 85 ) # Add System.Drawing assembly Add-Type -AssemblyName System.Drawing # Open image file $Image = [System.Drawing.Image]::FromFile( $InputFile ) # Calculate the offset for centering the image $SquareSide = if ( $Image.Height -lt $Image.Width ) { $Image.Height $Offset = 0 } else { $Image.Width $Offset = ( $Image.Height - $Image.Width ) / 2 } # Create empty square canvas for the new image $SquareImage = New-Object System.Drawing.Bitmap( $SquareSide, $SquareSide ) $SquareImage.SetResolution( $Image.HorizontalResolution, $Image.VerticalResolution ) # Draw new image on the empty canvas $Canvas = [System.Drawing.Graphics]::FromImage( $SquareImage ) $Canvas.DrawImage( $Image, 0, -$Offset ) # Resize image $ResultImage = New-Object System.Drawing.Bitmap( $SquareHeight, $SquareHeight ) $Canvas = [System.Drawing.Graphics]::FromImage( $ResultImage ) $Canvas.DrawImage( $SquareImage, 0, 0, $SquareHeight, $SquareHeight ) $ImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object MimeType -eq 'image/jpeg' # https://msdn.microsoft.com/ru-ru/library/hwkztaft(v=vs.110).aspx $EncoderQuality = [System.Drawing.Imaging.Encoder]::Quality $EncoderParameters = New-Object System.Drawing.Imaging.EncoderParameters( 1 ) $EncoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter( $EncoderQuality, $Quality ) # Save the image $ResultImage.Save( $OutputFile, $ImageCodecInfo, $EncoderParameters ) 

上面的脚本以多行注释<# ... #>开头,如果该注释首先出现并包含某些关键字,那么PowerShell将自动为该脚本构建帮助。 这种帮助的字面意思是- 基于注释的帮助


帮忙


此外,调用脚本时,无论是PowerShell控制台还是代码编辑器,参数的工具提示都将起作用:


内联帮助


我再次提请注意一个事实,即她不应被忽略。 如果您不知道在此处写些什么,请写点东西,然后去凉爽的地方,一经返回,您肯定会了解需要更改的内容。 可以用 不必狂妄地填写所有关键字,PowerShell旨在自我记录,如果给参数赋予了有意义的全名,则.SYNOPIS部分中的.SYNOPIS和一个示例就足够了。


严格模式


与许多其他脚本语言一样,PowerShell具有动态类型。 这种类型的键入有很多支持者:编写简单但功能强大的高级逻辑仅需几分钟,但是当您的决定开始达到一千行时,您肯定会遇到这种方法的脆弱性。


在测试阶段自动不变地输出类型(在您总是收到一组元素的地方形成一个数组),肯定会放一个猪,以防万一您得到一个元素并且在以下条件下,而不是检查元素的数量,而是得到字符的数量或另一个属性,具体取决于项目类型。 脚本的逻辑将中断,运行时将假装一切正常。


设置严格模式有助于避免这类问题,但是还需要您多一点代码,例如初始化变量和显式转换。


通过Set-StrictMode -Version Latest cmdlet启用此模式,尽管“严格”还有其他选项,但我选择使用后者。


在下面的示例中,严格模式捕获对不存在的属性的调用。 由于文件夹内只有一个元素,执行后$Input变量的类型将为FileInfo ,而不是对应元素的预期数组:


严格的


为避免此类问题,应将cmdlet的结果显式转换为数组:


 $Items = @( Get-ChildItem C:\Users\snd3r\Nextcloud ) 

将其设置为始终设置严格模式的规则,这将使您避免运行脚本产生意外结果。


错误处理


ErrorActionPreference


例如,在github上查看其他人的脚本时,我经常看到要么完全忽略错误处理机制,要么在出现错误的情况下显式激活静默继续模式。 一般而言,错误处理问题当然不是最容易编程的,尤其是在脚本中,但绝对不容忽视。 默认情况下,如果发生错误,PowerShell会显示该错误并继续工作(我稍微简化了这个概念,但是下面是该主题的git书的链接)。 如果您迫切需要将在该域中广泛使用的程序更新分发给所有计算机,而不必等到该程序散布到sccm部署的所有点或以您使用的另一种方式散布,这将非常方便。 如果其中一台计算机已关闭,则中断并重新启动该过程是不愉快的,这是事实。


另一方面,如果您对一个系统进行复杂的备份,该系统由一个信息系统的一个以上的多个数据文件组成,则需要确保备份是一致的,并且所有必要的数据集都已复制且没有错误。


若要在发生错误时更改cmdlet的行为,有一个全局变量$ErrorActionPreference ,其中包含以下可能的值列表: Stop,Inquire,Continue,Suspend,SilentlyContinue


我建议您在脚本数量或它们的复杂性不再存储在您的头部的堆栈中时始终使用Stop值,最好确保在任何不可预见的情况下该脚本将停止其工作,并且不会``悄悄地''破坏柴火,``成功地''完成执行。


除了在发生错误的情况下停止脚本之外,使用脚本还有另一个先决条件-处理异常情况。 有一个try/catch构造,但是仅当错误导致执行停止时才起作用。 不必必须在整个脚本级别启用停止, ErrorAction可以在cmdlet级别使用以下参数设置ErrorAction


 Get-ChildItem 'C:\System Volume Information\' -ErrorAction 'Stop' 

实际上,这种可能性定义了两种逻辑策略:“默认情况下”解决所​​有错误,并仅针对处理错误的关键位置设置ErrorAction ; 通过设置全局变量的值并在非关键操作上设置-ErrorAction 'Continue' ,可以在整个脚本级别启用它们。 我总是选择第二个选项,我不着急将其强加给您,我只建议您一次了解该问题并使用此有用的工具。


尝试/抓住


在错误处理程序中,您可以根据异常的类型进行匹配并使用执行线程进行操作,例如,添加更多信息。 尽管使用try/catch/throw/trap运算符可以构建脚本执行的整个流程,但您应绝对避免这样做,因为这种执行执行的方法不仅被视为“ goto-noodle”类别中的一种极端反模式,因此也会极大地降低性能。


 #requires -version 3 $ErrorActionPreference = 'Stop' #   ,    , #          $Logger = Get-Logger "$PSScriptRoot\Log.txt" #    trap { $Logger.AddErrorRecord( $_ ) exit 1 } #    $count = 1; while ( $true ) { try { #   $StorageServers = @( Get-ADGroupMember -Identity StorageServers | Select-Object -Expand Name ) } catch [System.Management.Automation.CommandNotFoundException] { #      ,         throw " Get-ADGroupMember ,    Active Directory module for PowerShell; $( $_.Exception.Message )" } catch [System.TimeoutException] { #             if ( $count -le 3 ) { $count++; Start-Sleep -S 10; continue } #             throw "     -   ,   $count ; $( $_.Exception.Message )" } #         break } 

值得注意的是, trap操作符是全局错误陷阱。 它捕获所有未在较低级别进行处理的内容,或由于无法独立纠正情况而抛出异常处理程序的所有内容。


除了上述的面向对象的异常方法外,PowerShell还提供了与其他“经典” shell兼容的更熟悉的概念,例如,错误流,返回代码和错误累积变量。 所有这一切当然很方便,有时没有其他选择,但从总体上来说,这超出了本主题的范围。 幸运的是, 在github上有一关于该主题的好书


我无法确定系统将具有PowerShell 5时使用的记录器代码(您可以在其中更方便地描述记录器类),请尝试一下,由于它的简单性和简洁性,它可能对您很有用,请确保毫无困难地添加其他方法。 :


 #   " ",   PowerShell v3 function Get-Logger { [CmdletBinding()] param ( [Parameter( Mandatory = $true )] [string] $LogPath, [string] $TimeFormat = 'yyyy-MM-dd HH:mm:ss' ) $LogsDir = [System.IO.Path]::GetDirectoryName( $LogPath ) New-Item $LogsDir -ItemType Directory -Force | Out-Null New-Item $LogPath -ItemType File -Force | Out-Null $Logger = [PSCustomObject]@{ LogPath = $LogPath TimeFormat = $TimeFormat } Add-Member -InputObject $Logger -MemberType ScriptMethod AddErrorRecord -Value { param( [Parameter( Mandatory = $true )] [string]$String ) "$( Get-Date -Format 'yyyy-MM-dd HH:mm:ss' ) [Error] $String" | Out-File $this.LogPath -Append } return $Logger } 

我重复这个想法-不要忽略错误处理。 从长远来看,这将节省您的时间和精力。
不要以为运行脚本不管什么都好。 很好-是时候摔断柴火了。


工具


值得从控制台仿真器开始改进使用PowerShell的工具。 我经常听到替代性黄蜂的支持者说,Windows中的控制台很糟糕,根本不是控制台,而是dos等。 很少有人能就此问题提出充分的主张,但如果有人成功了,那么实际上事实证明所有问题都可以解决。 集线器窗口中已经有关于终端和新控制台的更多信息; 那里的一切都还可以


第一步是安装Conemu或其Cmder组件,这并不是特别重要,因为我认为无论如何都要进行设置。 我通常以最小的配置选择cmder,没有配置的gita和其他二进制文件,尽管几年来我为纯Conemu调整了配置。 这确实是Windows的最佳终端仿真器,它允许您分割屏幕(对于tmux /屏幕爱好者),创建选项卡并启用地震式控制台模式:


Conemu


cmder


下一步,我建议放置以下模块: oh-my-poshposh-gitPSReadLine 。 前两个将通过添加有关当前会话,上次执行的命令的状态,特权指示符和git存储库在当前位置的状态的信息,使舞会变得更加愉快。 PSReadLine大大提高了promt的功能,例如添加了对输入命令的历史记录(CRTL + R)的搜索以及在CRTL + Space上cmdlet的便捷提示:


阅读线


是的,现在可以使用CTRL + L清除控制台,而cls担心cls


Visual Studio代码


编辑。 关于PowerShell,我能说的最糟糕的是纯粹的PowerShell ISE,那些看到具有三个面板的第一个版本的人不太可能会忘记这种体验。 终端的编码方式不同,缺少基本的编辑器功能,例如自动缩进,自动关闭括号,代码格式以及由此生成的整套反模式(我以防万一),这全都是关于ISE的。


不要使用它,而是将Visual Studio Code与PowerShell扩展一起使用-有您不想要的所有内容(当然,在合理范围内)。 并且不要忘记,在PoweShell到第六版(PowerShell Core 6.0)中,脚本的编码是UTF8 BOM,否则俄语会中断。


vscode


除了语法高亮显示,工具提示和脚本调试功能之外,该插件还安装了一个linter,它将帮助您遵循社区中已建立的惯例,例如,一键式扩展快捷方式(在灯泡上)。 实际上,这是一个常规模块,可以独立安装 ,例如,将其添加到管道签名脚本中: PSScriptAnalyzer


PSScriptAnalyzer


您可以在扩展名设置中设置代码格式设置参数,对于所有设置(编辑器和扩展名)都可以进行搜索: File - Preferences - Settings


Otbs


为了获得一个新的conpty控制台,您应该在settings中设置该标志 ,稍后,此建议可能是无关紧要的。


值得记住的是,VS Code中的任何操作都可以在控制中心执行,由CTRL + Shift + P调用。格式化从聊天中插入的一段代码,按字母顺序排序,将缩进从空格更改为制表符,依此类推,所有这些都在控制中心中。


例如,启用全屏和居中编辑器位置:


布局图


源代码控制 Git,SVN


Windows系统管理员通常在版本控制系统中有解决冲突的恐惧症,这可能是因为如果该集合的代表使用git,则通常不会遇到任何此类问题。 使用vscode,解决冲突的方法实际上是用鼠标单击需要保留或替换的代码部分。


合并


303行和304行之间的这些标签是可单击的,如果发生冲突,则应单击文档中显示的所有标签,然后提交更改并提交到服务器。 U-方便。


有关如何使用版本控制系统的信息,以及有关图片的信息,均记录在vscode扩展坞中 ,在那里简要简要地介绍了您。


片段


代码段是一种宏/模板,可让您加快代码编写速度。 绝对是必看的。


快速创建对象:


自定义对象


寻求基于注释的帮助:


帮忙


如果cmdlet需要传递大量参数,则使用splatting是有意义的。
这是他的摘录:


喷溅


通过Ctrl + Alt + J查看所有可用的代码段:


片段


如果之后您希望继续改善周围环境,但还没有听说过黄蜂床单,那么您来了 。 另外,如果您在编写PowerShell脚本时拥有一组对您有用的扩展,我将很高兴在注释中看到它们的列表。


性能表现


性能主题并不像乍看起来那样简单。 一方面,过早的优化会大大降低代码的可读性和可维护性,从而节省300ms的脚本执行时间(通常运行时间为十分钟),在这种情况下,它们的应用肯定具有破坏性。 另一方面,有一些相当简单的技巧可以提高代码的可读性和速度,非常适合经常使用。 下面,我将讨论其中的一些内容,如果性能满足您的所有需求,并且由于维护期间服务停机的严格时间限制,可读性会被忽略,我建议您参考专业文献。


管道和foreach


提高生产率的最简单且始终可行的方法是避免使用管道。 由于出于安全性的考虑,类型安全和方便,PowerShell通过管道传递元素将每个元素包装在一个对象中。 在dotnet语言中,此行为称为boxing 。 拳击是好的,它可以保证安全性,但是它有自己的价格,有时候付不起钱。


第一步是通过删除Foreach-Object所有应用程序并将其替换为foreach语句来提高性能,并提高可读性。 您可能会因为它们实际上是两个不同的实体而感到困惑,因为foreachForeach-Object的别名-实际上,主要区别在于foreach不接受管道中的值,同时它的运行速度快了三倍。


: - , , :


 Get-Content D:\temp\SomeHeavy.log | Select-String '328117' 

— , . — , , Get-Content . string , , . — , :


When reading from and writing to binary files, use the AsByteStream parameter and a value of 0 for the ReadCount parameter. A ReadCount value of 0 reads the entire file in a single read operation. The default ReadCount value, 1, reads one byte in each read operation and converts each byte into a separate object, which causes errors when you use the Set-Content cmdlet to write the bytes to a file unless you use AsByteStream

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content

 Get-Content D:\temp\SomeHeavy.log -ReadCount 0 | Select-String '328117' 

:


readcount


, . Select-String — . , Select-String . , Select-String , , :


 foreach ( $line in ( Get-Content D:\temp\SomeHeavy.log -ReadCount 0 )) { if ( $line -match '328117' ) { $line } } 

30 , - 30%, , , , - , ( ;-). , . , -match ; — . , — — - , .


— - , " " :


 foreach ( $line in ( Get-Content D:\temp\SomeHeavy.log -ReadCount 0 )) { if ( $line -match '328117' ) { "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" | Out-File D:\temp\Result.log -Append } } 

Measure-Command :


 Hours : 2 Minutes : 20 Seconds : 9 Milliseconds : 101 

. , , , . , PowerShell , , — . , , — StringBuilder . , , . , .


 $StringBuilder = New-Object System.Text.StringBuilder foreach ( $line in ( Get-Content D:\temp\SomeHeavy.log -ReadCount 0 )) { if ( $line -match '328117' ) { $null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" ) } } Out-File -InputObject $StringBuilder.ToString() -FilePath D:\temp\Result.log -Append -Encoding UTF8 

5 , :


 Hours : 0 Minutes : 5 Seconds : 37 Milliseconds : 150 

Out-File -InputObject , , . — . — Get-Help -Full , Accept pipeline input? true (ByValue) :


 -InputObject <psobject> Required? false Position? Named Accept pipeline input? true (ByValue) Parameter set name (All) Aliases None Dynamic? false 

PowerShell :


taskmgr


StringBuilder :


stringbuilder alloc


, , 3 3. dotnet- — StreamReader .


 $StringBuilder = New-Object System.Text.StringBuilder $StreamReader = New-Object System.IO.StreamReader 'D:\temp\SomeHeavy.log' while ( $line = $StreamReader.ReadLine()) { if ( $line -match '328117' ) { $null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" ) } } $StreamReader.Dispose() Out-File -InputObject $StringBuilder.ToString() -FilePath C:\temp\Result.log -Append -Encoding UTF8 

 Hours : 0 Minutes : 5 Seconds : 33 Milliseconds : 657 

, . , , , , 2. , :


streamreader


— "", — StringBuilder — "" . , ( 100) . — 90% ( , ):


 $BufferSize = 104857600 $StringBuilder = New-Object System.Text.StringBuilder $BufferSize $StreamReader = New-Object System.IO.StreamReader 'C:\temp\SomeHeavy.log' while ( $line = $StreamReader.ReadLine()) { if ( $line -match '1443' ) { #      if ( $StringBuilder.Length -gt ( $BufferSize - ( $BufferSize * 0.1 ))) { Out-File -InputObject $StringBuilder.ToString() -FilePath C:\temp\Result.log -Append -Encoding UTF8 $StringBuilder.Clear() } $null = $StringBuilder.AppendLine( "$( Get-Date -UFormat '%d.%m.%Y %H:%M:%S') $line" ) } } Out-File -InputObject $StringBuilder.ToString() -FilePath C:\temp\Result.log -Append -Encoding UTF8 $StreamReader.Dispose() 

 Hours : 0 Minutes : 5 Seconds : 53 Milliseconds : 417 

1 :


streamreader with dump


, . , , StreamWriter , , ;-) , , .


- — , . , — . Select-String Out-File , OutOfMemoryException , — .



, PowerShell , — , : PowerShell — , .


, StringBuilder dir — ( ). :


 $CurrentPath = ( Get-Location ).Path + '\' $StringBuilder = New-Object System.Text.StringBuilder foreach ( $Line in ( &cmd /c dir /b /s /ad )) { $null = $StringBuilder.AppendLine( $Line.Replace( $CurrentPath, '.' )) } $StringBuilder.ToString() 

 Hours : 0 Minutes : 0 Seconds : 3 Milliseconds : 9 

 $StringBuilder = New-Object System.Text.StringBuilder foreach ( $Line in ( Get-ChildItem -File -Recurse | Resolve-Path -Relative )) { $null = $StringBuilder.AppendLine( $Line ) } $StringBuilder.ToString() 

 Hours : 0 Minutes : 0 Seconds : 16 Milliseconds : 337 

$null — . , — Out-Null ; , ( $null ) , .


 # : $null = $StringBuilder.AppendLine( $Line ) # : $StringBuilder.AppendLine( $Line ) | Out-Null 

, , . Compare-Object , , , . robocopy.exe, ( PowerShell 5), :


 class Robocopy { [String]$RobocopyPath Robocopy () { $this.RobocopyPath = Join-Path $env:SystemRoot 'System32\Robocopy.exe' if ( -not ( Test-Path $this.RobocopyPath -PathType Leaf )) { throw '    ' } } [void]CopyFile ( [String]$SourceFile, [String]$DestinationFolder ) { $this.CopyFile( $SourceFile, $DestinationFolder, $false ) } [void]CopyFile ( [String]$SourceFile, [String]$DestinationFolder, [bool]$Archive ) { $FileName = [IO.Path]::GetFileName( $SourceFile ) $FolderName = [IO.Path]::GetDirectoryName( $SourceFile ) $Arguments = @( '/R:0', '/NP', '/NC', '/NS', '/NJH', '/NJS', '/NDL' ) if ( $Archive ) { $Arguments += $( '/A+:a' ) } $ErrorFlag = $false &$this.RobocopyPath $FolderName $DestinationFolder $FileName $Arguments | Foreach-Object { if ( $ErrorFlag ) { $ErrorFlag = $false throw "$_ $ErrorString" } else { if ( $_ -match '(?<=\(0x[\da-f]{8}\))(?<text>(.+$))' ) { $ErrorFlag = $true $ErrorString = $matches.text } else { $Logger.AddRecord( $_.Trim()) } } } if ( $LASTEXITCODE -eq 8 ) { throw 'Some files or directories could not be copied' } if ( $LASTEXITCODE -eq 16 ) { throw 'Robocopy did not copy any files. Check the command line parameters and verify that Robocopy has enough rights to write to the destination folder.' } } [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder ) { $this.SyncFolders( $SourceFolder, $DestinationFolder, '*.*', '', $false ) } [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [Bool]$Archive ) { $this.SyncFolders( $SourceFolder, $DestinationFolder, '*.*', '', $Archive ) } [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include ) { $this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, '', $false ) } [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [Bool]$Archive ) { $this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, '', $Archive ) } [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [String]$Exclude ) { $this.SyncFolders( $SourceFolder, $DestinationFolder, $Include, $Exclude, $false ) } [void]SyncFolders ( [String]$SourceFolder, [String]$DestinationFolder, [String]$Include, [String]$Exclude, [Bool]$Archive ) { $Arguments = @( '/MIR', '/R:0', '/NP', '/NC', '/NS', '/NJH', '/NJS', '/NDL' ) if ( $Exclude ) { $Arguments += $( '/XF' ) $Arguments += $Exclude.Split(' ') } if ( $Archive ) { $Arguments += $( '/A+:a' ) } $ErrorFlag = $false &$this.RobocopyPath $SourceFolder $DestinationFolder $Include $Arguments | Foreach-Object { if ( $ErrorFlag ) { $ErrorFlag = $false throw "$_ $ErrorString" } else { if ( $_ -match '(?<=\(0x[\da-f]{8}\))(?<text>(.+$))' ) { $ErrorFlag = $true $ErrorString = $matches.text } else { $Logger.AddRecord( $_.Trim()) } } } if ( $LASTEXITCODE -eq 8 ) { throw 'Some files or directories could not be copied' } if ( $LASTEXITCODE -eq 16 ) { throw 'Robocopy did not copy any files. Check the command line parameters and verify that Robocopy has enough rights to write to the destination folder.' } } } 

, ( ), — .


, : Foreach-Object !? , : foreach , Foreach-Object — , , , , . .


, :


 $Robocopy = New-Object Robocopy #    $Robocopy.CopyFile( $Source, $Dest ) #   $Robocopy.SyncFolders( $SourceDir, $DestDir ) #    .xml     $Robocopy.SyncFolders( $SourceDir, $DestDir , '*.xml', $true ) #     *.zip *.tmp *.log     $Robocopy.SyncFolders( $SourceDir, $DestDir, '*.*', '*.zip *.tmp *.log', $true ) 


— , , ; , , , :


  • foreach Foreach-Object ;


  • ;


  • / , ;


  • StringBuilder ;


  • , - ;


  • ( "" );



: - , .


Jobs


, , , , , , . . , IO, .


ssd

Windows Server 2019 Hyper-V ssd ( hdd):


2019ssd


PowerShell ( Get-Command *-Job ), .


, , , :


 $Job = Start-Job -ScriptBlock { Write-Output 'Good night' Start-Sleep -S 10 Write-Output 'Good morning' } $Job | Wait-Job | Receive-Job Remove-Job $Job 

, — , . .


, :


jobs
https://xaegr.wordpress.com/2011/07/12/threadping/


, , — , . , , (50 — 50 ):


job dies


. , — , . — , .


, , , - .


Runspaces


Beginning Use of PowerShell Runspaces: Part 1 . , — PowerShell , . (, PowerShell ), : ( ) . , .


WPF , PowerShell, . — , . — , "" . .


, .


wpf


 #     $GUISyncHash = [hashtable]::Synchronized(@{}) <# WPF  #> $GUISyncHash.FormXAML = [xml](@" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Sample WPF Form" Height="510" Width="410" ResizeMode="NoResize"> <Grid> <Label Content=" " HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="37" Width="374" FontSize="18"/> <Label Content="" HorizontalAlignment="Left" Margin="16,64,0,0" VerticalAlignment="Top" Height="26" Width="48"/> <TextBox x:Name="BackupPath" HorizontalAlignment="Left" Height="23" Margin="69,68,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="300"/> <Label Content="" HorizontalAlignment="Left" Margin="16,103,0,0" VerticalAlignment="Top" Height="26" Width="35"/> <TextBox x:Name="RestorePath" HorizontalAlignment="Left" Height="23" Margin="69,107,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="300"/> <Button x:Name="FirstButton" Content="√" HorizontalAlignment="Left" Margin="357,68,0,0" VerticalAlignment="Top" Width="23" Height="23"/> <Button x:Name="SecondButton" Content="√" HorizontalAlignment="Left" Margin="357,107,0,0" VerticalAlignment="Top" Width="23" Height="23"/> <CheckBox x:Name="Check" Content="  " HorizontalAlignment="Left" Margin="16,146,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.113,-0.267" Width="172"/> <Button x:Name="Go" Content="Go" HorizontalAlignment="Left" Margin="298,173,0,0" VerticalAlignment="Top" Width="82" Height="26"/> <ComboBox x:Name="Droplist" HorizontalAlignment="Left" Margin="16,173,0,0" VerticalAlignment="Top" Width="172" Height="26"/> <ListBox x:Name="ListBox" HorizontalAlignment="Left" Height="250" Margin="16,210,0,0" VerticalAlignment="Top" Width="364"/> </Grid> </Window> "@) <#   #> $GUISyncHash.GUIThread = { $GUISyncHash.Window = [Windows.Markup.XamlReader]::Load(( New-Object System.Xml.XmlNodeReader $GUISyncHash.FormXAML )) $GUISyncHash.Check = $GUISyncHash.Window.FindName( "Check" ) $GUISyncHash.GO = $GUISyncHash.Window.FindName( "Go" ) $GUISyncHash.ListBox = $GUISyncHash.Window.FindName( "ListBox" ) $GUISyncHash.BackupPath = $GUISyncHash.Window.FindName( "BackupPath" ) $GUISyncHash.RestorePath = $GUISyncHash.Window.FindName( "RestorePath" ) $GUISyncHash.FirstButton = $GUISyncHash.Window.FindName( "FirstButton" ) $GUISyncHash.SecondButton = $GUISyncHash.Window.FindName( "SecondButton" ) $GUISyncHash.Droplist = $GUISyncHash.Window.FindName( "Droplist" ) $GUISyncHash.Window.Add_SourceInitialized({ $GUISyncHash.GO.IsEnabled = $true }) $GUISyncHash.FirstButton.Add_Click( { $GUISyncHash.ListBox.Items.Add( 'Click FirstButton' ) }) $GUISyncHash.SecondButton.Add_Click( { $GUISyncHash.ListBox.Items.Add( 'Click SecondButton' ) }) $GUISyncHash.GO.Add_Click( { $GUISyncHash.ListBox.Items.Add( 'Click GO' ) }) $GUISyncHash.Window.Add_Closed( { Stop-Process -Id $PID -Force }) $null = $GUISyncHash.Window.ShowDialog() } $Runspace = @{} $Runspace.Runspace = [RunspaceFactory]::CreateRunspace() $Runspace.Runspace.ApartmentState = "STA" $Runspace.Runspace.ThreadOptions = "ReuseThread" $Runspace.Runspace.Open() $Runspace.psCmd = { Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase }.GetPowerShell() $Runspace.Runspace.SessionStateProxy.SetVariable( 'GUISyncHash', $GUISyncHash ) $Runspace.psCmd.Runspace = $Runspace.Runspace $Runspace.Handle = $Runspace.psCmd.AddScript( $GUISyncHash.GUIThread ).BeginInvoke() Start-Sleep -S 1 $GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] { $GUISyncHash.ListBox.Items.Add( '' ) }) $GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] { $GUISyncHash.ListBox.Items.Add( '  ' ) }) foreach ( $item in 1..5 ) { $GUISyncHash.Droplist.Dispatcher.Invoke( "Normal", [action] { $GUISyncHash.Droplist.Items.Add( $item ) $GUISyncHash.Droplist.SelectedIndex = 0 }) } $GUISyncHash.ListBox.Dispatcher.Invoke( "Normal", [action] { $GUISyncHash.ListBox.Items.Add( 'While ( $true ) { Start-Sleep -S 10 }' ) }) while ( $true ) { Start-Sleep -S 10 } 

WPF github, , smart : https://github.com/snd3r/GetDiskSmart/ . , MVVM:


binging


Visual Studio, Community Edition , xaml- wpf — https://github.com/punker76/kaxaml


kaxaml


而不是结论


PowerShell — Windows-. , , , .


, , "PowerShell, ", . , — , . . , - , - .


— .


calm


PS Boomburum , 2019 powershell — .

Source: https://habr.com/ru/post/zh-CN443256/


All Articles