本系列是一个重新学习PowerShell的笔记,内容引用自
PowerShell中文博客
Powershell函数可以接受参数,并对参数进行处理。函数的参数有3个特性:
\(args 万能参数
给一个函数定义参数最简单的是使用\)args这个内置的参数。
它可以识别任意个参数。尤其适用哪些参数可有可无的函数。
function sayHello { if($args.Count -eq 0) { "No argument!" } else { $args | foreach {"Hello,$($_)"} } } Write-Host -ForegroundColor "Red" "无参数调用" sayHello Write-Host -ForegroundColor "Red" "一个或多个参数调用" sayHello hua sayHello hua hua PS C:\PowerShell>test.ps1 无参数调用 No argument! 一个或多个参数调用 Hello,hua Hello,hua Hello,hua
function Test($Str1 = "Hua",$str2 = "Hua") { $Str1 + $str2 } Test Test -Str1 "h" -str2 "ua" PS C:\PowerShell>test.ps1 HuaHua hua
function SetDate([DateTime]$Date,[int]$Days = 0,[string]$Description = "无") { Write-Host -ForegroundColor "Green" $Date.Day $Date.AddDays($Days) Write-Host -ForegroundColor "Red" $Date.Day Write-Host -ForegroundColor "Red" $Description } SetDate '2020-01-01' SetDate '2020-11-01' -Days 6 -Description "HuaHua" #输入错误的日期 SetDate '2020-15-01' -Days 6 -Description "HuaHua" PS C:\PowerShell>test.ps1 1 2020年1月1日 0:00:00 1 无 1 2020年11月7日 0:00:00 1 HuaHua SetDate : Cannot process argument transformation on parameter 'Date'. Cannot convert value "2020-15-01" to type "System.DateTime". Error: "String was not recognized as a valid DateTime." At test.ps1:11 char:9
Powershell函数最简单的参数类型为布尔类型,除了使用Bool类型,也可以使用Switch关键字。
下面的函数逆转字符串,但是可以通过\(try 参数进行控制,如果没有指定\)try的值,默认值为$false
function tryReverse( [switch]$try , [string]$source ) { [string]$target="" if($try) { for( [int]$i = $source.length -1; $i -ge 0 ;$i--) { $target += $source[$i] } return $target } return $source } tryReverse -source www.mossfly.com tryReverse -try $true -source www.mossfly.com PS C:\PowerShell> test.ps1 www.mossfly.com moc.ylfssom.www
Powershell不像它编程语言,它的函数可以有多个返回值。如果你直接调用函数,返回值会在控制台输出。当然你也可以将结果存储在一个变量中进一步处理。
下面的例子演示返回一个值:
function GetDate() { return Get-Date } GetDate $d = GetDate $d.GetType().FullName PS C:\PowerShell> test.ps1 2021年9月14日 15:27:28 System.DateTime
下面的例子演示返回多个值
function GetDate() { $v = Get-Date $v.Year $v.Month $v.Day } GetDate $d = GetDate $d.GetType().FullName $d.Count $d[0] PS C:\PowerShell> test.ps1 2021 9 14 System.Object[] 3 2021
总结一下,如果一个函数返回一个值,像其它编程语言一样,这个值包括她的类型信息会直接返回。但是如果遇到多个返回值,Powershell会将所有的返回值自动构造成一个Object数组。可以通过索引访问数组。
Powershell会将函数中所有的输出作为返回值,但是也可以通过return语句指定具体的我返回值。
Return 语句会将指定的值返回,同时也会中断函数的执行,return后面的语句会被忽略。
function GetDate() { $v = Get-Date $v.Year return $v.Month $v.Day } GetDate $d = GetDate $d.GetType().FullName $d.Count #return 语句之后的Day没有返回被截断 PS C:\PowerShell> test.ps1 2021 9 System.Object[]
一个函数返回了一个值还是多个值,是可以验证的。下面的例子会产生随机数,如果没有指定个数,默认会返回一个随机数,否则会返回指定个数的随机数。
Function lottery([int]$number=1) { $rand = New-Object system.random For ($i=1; $i -le $number; $i++) { $rand.next(1,50) } } # 参数为空时,返回值不是数组: $result = lottery $result -is [array] # False # 如果指定多个随机数是,返回值是数组类型: $result = lottery 10 $result -is [array] PS C:\PowerShell> test.ps1 False True
函数默认会将函数中的所有输出作为函数的返回值返回,这样很方便。但有时可能会将不必要的输出误以为返回值。写脚本程序时,可能需要自定义一些函数,这个函数可能只需要一个返回值,但是为了提高函数的可读性,可能会在函数增加一些注释输出行。
Function Test() { "Try to calculate." "3.1415926" "Done." } #保存在变量中输出, $value=Test $value # Try to calculate. # 3.1415926 # Done. #如果要过滤注释,只输出,不作为返回值, #可以使用Write-Host命令 Function Test() { Write-Host "Try to calculate." "3.1415926" Write-Host "Done." } # 在变量值中保存返回值,在控制台输出注释行 $value=Test # Try to calculate. # Done. # 测试返回值 $value # 3.1415926
可能输出这些函数中临时提示信息,给函数的返回值造成干扰。要解决这个问题,除了上述的Write-Host,也可以使用Write-Debug命令。
Function Test() { Write-Debug "Try to calculate." "3.1415926" Write-Debug "Done." } # Debug调试信息只会在调试模式下被输出 $value=Test # 3.1415926 #如果你想通过显示调试信息调试函数,可以开启调试模式 $DebugPreference="Continue" $value=Test # 调试: Try to calculate. # 调试: Done. # 测试返回值 $value # 3.1415926 #如果关闭调试模式,这些调试信息自然不会输出 $DebugPreference="SilentlyContinue" $value=Test
使用Write-Debug有两个优势,首先调试信息会自动高亮显示,便于分析。其次,这些调试信息只会在调试模式开启时输出,控制起来更加方便。当然最重要的是这些临时信息无论什么时候也不会混淆在返回值。
函数中的错误信息,也有可能作为返回值的一部分,因为默认这些错误信息会直接输出。
Function ErrorTest() { #该进程不存在 Stop-Process -Name "www.mossfly.com" } ErrorTest Stop-Process : 找不到名为“www.mossfly.com”的进程。请验证该进程名称,然后再次调用 cmdlet。 所在位置 C:UsersbaozhenDesktoptest.ps1:6 字符: 17 + Stop-Process <<<< -Name "www.mossfly.com" + CategoryInfo : ObjectNotFound: (www.mossfly.com:String) [Stop-P rocess], ProcessCommandException + FullyQualifiedErrorId : NoProcessFoundForGivenName,Microsoft.PowerShell. Commands.StopProcessCommand 很明显,类似这样的错误提示信息,对调试程序很重要,但如果你觉得它不重要,特意要隐藏,可以使用$ErrorActionPreference进行设置。 Function ErrorTest() { #从这里开始隐藏所有的错误信息 $ErrorActionPreference="SilentlyContinue" Stop-Process -Name "www.mossfly.com" #该进程不存在 } #错误信息不会输出 ErrorTest
但是上面的做法并不明智,因为这样可能错过其它错误提示。所以最好的方式是处理完后,对$ErrorActionPreference进行复位。
Function ErrorTest() { #从这里开始隐藏所有的错误信息 $ErrorActionPreference="SilentlyContinue" Stop-Process -Name "www.mossfly.com" #该进程不存在 #恢复$ErrorActionPreference,错误开始输出 $ErrorActionPreference="Continue" 2/0 } ErrorTest 试图除以零。 所在位置 行:9 字符: 7 + 2/ <<<< 0 + CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException + FullyQualifiedErrorId : RuntimeException
Powershell已经提供了许多用户能够使用的预定义函数,这些函数可以通过Function:PSDrive虚拟驱动器查看。
PS C:\PowerShell> Get-ChildItem function: | Format-Table Name,Definition Name Definition ---- ---------- A: Set-Location $MyInvocation.MyCommand.Name B: Set-Location $MyInvocation.MyCommand.Name C: Set-Location $MyInvocation.MyCommand.Name cd.. Set-Location .. cd\ Set-Location \ Clear-Host ... ConvertFrom-SddlString ... D: Set-Location $MyInvocation.MyCommand.Name E: Set-Location $MyInvocation.MyCommand.Name F: Set-Location $MyInvocation.MyCommand.Name Format-Hex ... G: Set-Location $MyInvocation.MyCommand.Name Get-FileHash ... Get-Verb ... H: Set-Location $MyInvocation.MyCommand.Name help ... I: Set-Location $MyInvocation.MyCommand.Name Import-PowerShellDataFile ... ImportSystemModules J: Set-Location $MyInvocation.MyCommand.Name K: Set-Location $MyInvocation.MyCommand.Name L: Set-Location $MyInvocation.MyCommand.Name M: Set-Location $MyInvocation.MyCommand.Name mkdir ... more ... N: Set-Location $MyInvocation.MyCommand.Name New-Guid ... New-TemporaryFile ... O: Set-Location $MyInvocation.MyCommand.Name oss ... P: Set-Location $MyInvocation.MyCommand.Name Pause $null = Read-Host 'Press Enter to continue...' prompt ... PSConsoleHostReadLine ... Q: Set-Location $MyInvocation.MyCommand.Name R: Set-Location $MyInvocation.MyCommand.Name S: Set-Location $MyInvocation.MyCommand.Name T: Set-Location $MyInvocation.MyCommand.Name TabExpansion2 ... U: Set-Location $MyInvocation.MyCommand.Name V: Set-Location $MyInvocation.MyCommand.Name W: Set-Location $MyInvocation.MyCommand.Name X: Set-Location $MyInvocation.MyCommand.Name Y: Set-Location $MyInvocation.MyCommand.Name Z: Set-Location $MyInvocation.MyCommand.Name
从这些结果不但能够看出函数的名称,还能通过Definition列查看函数的内容。如果你想深入查看函数的内部定义可以直接访问Function:
PS C:\PowerShell> $function:prompt "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "; # .Link # https://go.microsoft.com/fwlink/?LinkID=225750 # .ExternalHelp System.Management.Automation.dll-help.xml
Powershell中的这些预定义的函数可以做很多重要的工作。
Clear-Host 清除屏幕的缓存
help,man 查看命令的帮助文档
mkdir,md 通过new-Item创建子目录
more 分屏输出管道结果
prompt 返回提示文本
TabExpansion Tab键的自动完成提示
X: 调用Set-Location定位到指定的驱动器根目录
如果你想查看当前Powershell环境中定义了多少个函数可以通过:
PS C:\PowerShell> (Dir function:).Count 45
每次成功执行完一条命令,Powershell就会执行Prompt函数,提示用户进行下一步输入。
默认设置中,prompt显示“PS” 和当前的工作目录。
再接着是”>”或”>>”,具体情况要看当前Powershell控制台的的层数。
当然你可以自定义prompt的,那就得覆盖prompt函数:
PS C:\PowerShell> pwd Path ---- C:\PowerShell PS C:\PowerShell> Function prompt{"Hua Hua"} Hua Hua Hua Huapwd Path ---- C:\PowerShell Hua Hua
这样的覆盖安全吗,显然安全,对预定义函数的重写,只会在当前控制台会话中有效,当你重新启动控制台时,自然会恢复如初。
在控制台的任何位置输出文本(自定义光标的位置)
因为控制台的内容存放在控制台屏幕的缓存中,因此你可以逐个访问内容的每一行或每一个字符。
你甚至可以在控制台的屏幕的任何位置输出你想要输出的信息,接下来的函数会演示这个功能。
要完成这个功能,需要使用$Host.UI.Rawui ,光标的位置通过屏幕的横坐标(X)和纵坐标(Y)确定,下面的函数会首先记住当前光标的位置,然后在横坐标上增加60个占位符,然后重置光标的位置至当前位置,最后通过prompt函数回复光标的原始位置。
Hua H> function prompt >> { >> $curPos = $host.ui.rawui.CursorPosition >> $newPos = $curPos >> $newPos.X+=60 >> $host.ui.rawui.CursorPosition = $newPos >> Write-Host ("{0:D} {0:T}" -f (Get-Date)) -foregroundcolor Yellow >> $host.ui.rawui.CursorPosition = $curPos >> Write-Host ("PS " + $(get-location) +">") -nonewline -foregroundcolor Green >> " " >> } PS C:\PowerShell> 2021年9月14日 16:57:04
在Windows控制台的标题栏有一部分空间,可以放置一些有用的信息,比如当前哪个用户登录在控制台,可以通过设置$host.UI.RawUI.WindowTitle
来自定义控制台标题栏的文本。
下面的例子就会演示设置标题栏文本,通过.NET方法获取当前用户信息,由于该方法会有几秒钟执行时间,为了效率考虑首先将用户信息保存在全局变量中,然后在Prompt函数中调用。
$global:CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() function prompt { $host.ui.rawui.WindowTitle = "Line: " + $host.UI.RawUI.CursorPosition.Y + " " + $CurrentUser.Name + " " + $Host.Name + " " + $Host.Version Write-Host ("PS " + $(get-location) +">") -nonewline -foregroundcolor Green return " " }
执行以后在标题栏会显示:Line: 72 ComputerNameuser ConsoleHost 2.0
如果你使用管理员权限运行控制台时,Prompt函数还可以给出警告。使用WindowsPrincipal 辨别当前用户是否使用了管理员权限,你不需要了解下面的.NET代码,它会在全局变量中将布尔值赋值给$Admin。
$CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = new-object System.Security.principal.windowsprincipal($CurrentUser) $global:Admin = $principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::Administrator) Function prompt { # 输出标准的提示信息: Write-Host ("PS " + $(get-location)) -nonewline # The rest depends on whether you have admin rights or not: If ($admin) { $oldtitle = $host.ui.rawui.WindowTitle # 将"Administrator: " 显示在标题栏 If (!$oldtitle.StartsWith("Administrator: ")) { $host.ui.rawui.WindowTitle ="Administrator: " + $oldtitle } # Prompt结尾显示红色的尖括号 Write-Host ">" -nonewline -foregroundcolor Red } Else { Write-Host ">" -nonewline } return " " }
没有管理员权限时,标题栏文本:Windows Powershell
有管理员权限时,标题栏文本:Administrator :管理员 : Windows Powershell
很可能,你已经注意到了,cls可以删除屏幕的缓存。
事实上,cls只是Clear-Host函数的别名,但是却看不到这个函数的内容。
PS C:\PowerShell> $function:Clear-Host At line:1 char:16 + $function:Clear-Host + ~~~~~ Unexpected token '-Host' in expression or statement. + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException + FullyQualifiedErrorId : UnexpectedToken
在Powershell中短斜杠是个特殊字符,如果一个函数名中包含了特殊字符就应当把它放在花括号中。
PS C:\PowerShell> ${function:Clear-Host} $RawUI = $Host.UI.RawUI $RawUI.CursorPosition = @{X=0;Y=0} $RawUI.SetBufferContents( @{Top = -1; Bottom = -1; Right = -1; Left = -1}, @{Character = ' '; ForegroundColor = $rawui.ForegroundColor; BackgroundColor = $rawui.BackgroundColor}) # .Link # https://go.microsoft.com/fwlink/?LinkID=225747 # .ExternalHelp System.Management.Automation.dll-help.xml
这些盘符名称可以作为单独的一个函数,是怎么做到的呢?
PS C:\PowerShell> $function:C: Set-Location $MyInvocation.MyCommand.Name
一个函数能够访问和进一步处理另外一条命令的结果吗?答案是肯定的,这被称为管道。管道有两种模式,一种是顺序处理模式,一种是流处理模式。
在最简单的情况下,你的函数不是真正支持管道。只能对前一个命令执行后的结果处理。前一个命令执行的结果通过被自动保存在$input
变量中,$input
是一个数组,它可以包含许多元素,一个元素,甚至一个元素都没有,这取决于具体的环境。
下面的例子,是一个函数,仅仅输出$input
的内容。
PS C:\PowerShell> function OutPut { >> $input >> } PS C:\PowerShell> 1,2,3 | OutPut 1 2 3 PS C:\PowerShell> "222",1 | OutPut 222 1 PS C:\PowerShell> dir | OutPut Directory: C:\PowerShell Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2021/9/14 13:51 58 test.txt
到目前为止,这个函数只是仅仅输出了管道的结果,并没有其它比较强大的功能。
在接下来的例子中,函数将会对管道的结果做进一步处理。函数名MarkEXE,将会检查Dir的结果,并高亮标记后缀名为EXE的文件名为红色。
Function MarkEXE { # 保存控制台当前的前景色 $oldcolor = $host.ui.rawui.ForegroundColor # 通过循环逐条检查管道的结果 Foreach ($element in $input) { # 如果后缀名为.exe,设置为前景色为红色 If ($element.name.toLower().endsWith(".exe")) { $host.ui.Rawui.ForegroundColor = "red" } Else { # 否则恢复默认的前景色 $host.ui.Rawui.ForegroundColor = $oldcolor } # 输出数组元素 $element } # 最后,重置控制台的前景色: $host.ui.Rawui.ForegroundColor = $oldcolor } Dir | MarkEXE PS C:\PowerShell> New-Item test.exe Directory: C:\PowerShell Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2021/9/15 10:46 0 test.exe PS C:\PowerShell> ls Directory: C:\PowerShell Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2021/9/15 10:46 0 test.exe -a---- 2021/9/14 13:51 58 test.txt PS C:\PowerShell> test.ps1 Directory: C:\PowerShell Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2021/9/15 10:46 0 test.exe -a---- 2021/9/14 13:51 58 test.txt
管道的低效率顺序模式在处理大容量数据时很容易出现问题,其结果是巨大的内存占用和进程等待。
如果你的函数支持高效率的流模式,在处理管道结果时仅占用很小的内存。
事实上,针对之前MarkEXE函数,你只需要替换”function” 关键字 为 “filter”它就会开始流模式处理,这样你再也不用过分的担心忍受程序的无休止的响应和崩溃的危险。
你也可以递归处理全盘目录,甚至处理极其庞大的数据。例如:
PS C:\PowerShell> Dir C:\PowerShell\ -recurse | MarkEXE
当MarkEXE每次被调用时,它只会对当前目录下的每个单独的元素进行处理。
对于过滤器filters
来说,$input
一直都是一个独立的元素。
这也就是为什么在过滤器中$input
一点用也没有的道理。
此时,最好使用$_
变量,因为它代表了当前处理的数据。
这样还可以简化MarkExe,因为过滤器自身已经扮演了循环的角色了,你没有必要再写专门的循环处理了。
Filter MarkEXE { # 记录当前控制台的背景色 $oldcolor = $host.ui.rawui.ForegroundColor # 当前的管道元素保存在 $_ 变量中 # 如果后缀名为 ".exe", # 改变背景色为红色: If ($_.name.toLower().endsWith(".exe")) { $host.ui.Rawui.ForegroundColor = "red" } Else { # 否则使用之前的背景色 $host.ui.Rawui.ForegroundColor = $oldcolor } # 输出当前元素 $_ # 最后恢复控制台颜色: $host.ui.Rawui.ForegroundColor = $oldcolor } Dir | MarkEXE PS C:\PowerShell> test.ps1 Directory: C:\PowerShell Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2021/9/15 10:46 0 test.exe -a---- 2021/9/14 13:51 58 test.txt
过滤器在函数中属于高级应用,因为它可以立即处理管道结果的每一个元素。但是过滤器必须每次重复执行预定义命令的结果。
对于MarkEXE 函数,每次执行的过程中要记录和更新控制台的背景颜色,也要花费资源和时间。
事实上,过滤器只是特殊的函数。如果一个函数内部使用了管道,你就可以定义三个基础的任务区了:第一步,完成函数的初始化,完成函数执行的预备步骤;第二步处理递归调用所得的结果;最后进行收尾工作。
这三个任务区分别可以使用begin
,process
,end
语句块。
function MarkEXE { begin { # 记录当前控制台的背景色 $oldcolor = $host.ui.rawui.ForegroundColor } process { # 当前的管道元素保存在 $_ 变量中 # 如果后缀名为 ".exe", # 改变背景色为红色: If ($_.name.toLower().endsWith(".exe")) { $host.ui.Rawui.ForegroundColor = "red" } Else { # 否则使用之前的背景色 $host.ui.Rawui.ForegroundColor = $oldcolor } # 输出当前元素 $_ } end { # 最后恢复控制台颜色: $host.ui.Rawui.ForegroundColor = $oldcolor } } Dir | MarkEXE PS C:\PowerShell> test.ps1 Directory: C:\PowerShell Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2021/9/15 10:46 0 test.exe -a---- 2021/9/14 13:51 58 test.txt