PowerShell笔记 - 8.函数

本系列是一个重新学习PowerShell的笔记,内容引用自PowerShell中文博客

处理函数的参数

Powershell函数可以接受参数,并对参数进行处理。函数的参数有3个特性:

  1. 任意参数:内部变量\(args 接受函数调用时接受的参数,\)args是一个数组类型。
  2. 命名参数:函数的每一个参数可以分配一个名称,在调用时通过名称指定对应的参数。
  3. 预定义参数:函数在定义参数时可以指定默认值,如果调用时没有专门指定参数的值,就会保持默认值。

万能参数$args

\(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

Switch 参数

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数组。可以通过索引访问数组。

Return语句

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

自定义Prompt

每次成功执行完一条命令,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

Clear-Host:删除屏幕缓存

很可能,你已经注意到了,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

盘符名预定义函数C:,D:,E:

这些盘符名称可以作为单独的一个函数,是怎么做到的呢?

PS C:\PowerShell> $function:C:                              
Set-Location $MyInvocation.MyCommand.Name

函数过滤器、管道

一个函数能够访问和进一步处理另外一条命令的结果吗?答案是肯定的,这被称为管道。管道有两种模式,一种是顺序处理模式,一种是流处理模式。

低效率的顺序模式:$input

在最简单的情况下,你的函数不是真正支持管道。只能对前一个命令执行后的结果处理。前一个命令执行的结果通过被自动保存在$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
上一篇:PowerShell笔记 - 14.xml基本操作


下一篇:Powershell操作wmi