PowerShell命令与脚本(9)——函数

PowerShell定义函数

函数是自定义的Powershell代码,有三个原则:
简短:函数名简短,并且显而易见。
聚合:函数可以完成多个操作。
封装和扩展:将一批Powershell语句进行封装,实现全新的功能需求。
函数的结构由三部分组成:函数名,参数,函数体
Function FuncName (args[])
{
code;
}
 
使用函数作为别名
假如Powershell不支持copy con命令,你可以通过定义函数实现这个功能:
控制台上多行输入定义函数
 
把函数精简成一行
可以将一个函数定义在一行上,但是这样阅读和理解起来就不方便,可以在每条命令后加分号进行分割。
使用文本编辑器
函数可以在文本编辑器上编写,写完以后复制进Powershell控制台即可。如果控制台设置为快速编辑模式,从记事本复制后,直接在控制台鼠标右键即可完成黏贴。
 
更新函数
如果要更新已经定义好的函数,简单的方法是重新定义,这样新的定义会覆盖旧的定义。但是如果函数代码没有保存副本,可以先将函数定义导出到ps1文件,然后就可以编辑了。
 
删除函数
控制台定义的函数只会在当前会话生效,一旦控制台退出,会自动消失。在不关闭控制台的条件下删除一个已经定义好的函数,可是使用虚拟驱动器的方法:
 
 

PowerShell处理函数的参数

Powershell函数可以接受参数,并对参数进行处理。函数的参数有3个特性:
  1. 任意参数:内部变量$args 接受函数调用时接受的参数,$args是一个数组类型。
  2. 命名参数:函数的每一个参数可以分配一个名称,在调用时通过名称指定对应的参数。
  3. 预定义参数:函数在定义参数时可以指定默认值,如果调用时没有专门指定参数的值,就会保持默认值。
1、$args 万能参数
给一个函数定义参数最简单的是使用$args这个内置的参数。它可以识别任意个参数。尤其适用哪些参数可有可无的函数。
function sayHello
{
    if($args.Count -eq 0)
    {
        "No argument!"
    }
    else
    {
        $args | foreach {"Hello,$($_)"}
    }
}
因为$arg是一个数组,可以用它求和。
 
2、设置参数名称
 
3、给参数定义默认值
 
4、使用强类型参数
通过之前的例子发现将用户的参数传递给函数显得比较混乱。罪魁祸首就是Powershell的参数解释器,它可以自动处理和分配参数给函数。
函数的参数解释器比较傲慢,它对你提供的参数的信息完全不关心。它只会粗略地将参数进行分割,并且最大限度的进行自动类型转换。事实上,这种类型转换很多时候并不完美。所以最好提前能够对参数进行强类型限制。
限制数字类型
上面的函数执行后,会抛出异常。
因为subtract参数定义了强类型,参数的类型可能引起函数的处理结果。例如调用上面的函数。
结果为0
但是如果将上面的函数的参数定义为Double型,
 
限制日期类型
函数的参数解释器会自动尝试将字符串转换成日期类型,如果转换失败就是抛出异常。
 
5、Switch 参数
Powershell函数最简单的参数类型为布尔类型,除了使用Bool类型,也可以使用Switch关键字。
下面的函数逆转字符串,但是可以通过$try 参数进行控制,如果没有指定$try的值,默认值为$false
 

PowerShell指定函数的返回值

1、一个或多个返回值
Powershell不像它编程语言,它的函数可以有多个返回值。如果你直接调用函数,返回值会在控制台输出。当然你也可以将结果存储在一个变量中进一步处理。
下面的例子演示返回一个值:
function Square([double]$num)
{
    return $num*$num
}
#在控制台输出结果
Square 9.87
#97.4169
 
#将结果赋值给变量
$value=Square 9.87
$value
#97.4169
 
#返回值为Double类型
$value.GetType().FullName
#System.Double
下面的例子演示返回多个值
function gbMeasure($amount)
{
    "$amount GB=$($amount) GB"
    "$amount GB=$($amount*1gb/1mb) MB"
    "$amount GB=$($amount*1gb/1kb) KB"
    "$amount GB=$($amount*1gb) B"
}
 
#函数返回4个值
gbMeasure 1
# 1 GB=1 GB
# 1 GB=1024 MB
# 1 GB=1048576 KB
# 1 GB=1073741824 B
 
#将所有的返回值存储在一个变量中
$result=gbMeasure 1
$result

# 1 GB=1 GB
# 1 GB=1024 MB
# 1 GB=1048576 KB
# 1 GB=1073741824 B
 
#所有的返回值会自动存储在一个数组中
$result=gbMeasure 1
$result.GetType().Name
 
# Object[]
 
#通过索引访问每个返回值
$result=gbMeasure 1
$result[3]
# 1 GB=1073741824 B
总结一下,如果一个函数返回一个值,像其它编程语言一样,这个值包括她的类型信息会直接返回。但是如果遇到多个返回值,Powershell会将所有的返回值自动构造成一个Object数组。可以通过索引访问数组。
 
2、Return语句
Powershell会将函数中所有的输出作为返回值,但是也可以通过return语句指定具体的返回值。
Return 语句会将指定的值返回,同时也会中断函数的执行,return后面的语句会被忽略。
function test($num)
{
    1
    9
    return 10
    4
    6
}
test
# 1 和 9 作为输出会返回
# return语句中的10 也会返回
# return 语句后的4和6会被忽略
 
#1
#9
#10
 
访问返回值
一个函数返回了一个值还是多个值,是可以验证的。下面的例子会产生随机数,如果没有指定个数,默认会返回一个随机数,否则会返回指定个数的随机数。
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]
# 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查看支持的函数

Powershell已经提供了许多用户能够使用的预定义函数,这些函数可以通过Function:PSDrive虚拟驱动器查看。
从这些结果不但能够看出函数的名称,还能通过Definition列查看函数的内容。如果你想深入查看函数的内部定义可以直接访问Function:
Powershell中的这些预定义的函数可以做很多重要的工作。
 
Clear-Host 清除屏幕的缓存
help,man 查看命令的帮助文档
mkdir,md 通过new-Item创建子目录
more 分屏输出管道结果
prompt 返回提示文本
TabExpansion Tab键的自动完成提示
X: 调用Set-Location定位到指定的驱动器根目录
 
如果你想查看当前Powershell环境中定义了多少个函数可以通过
 
自定义Prompt
每次成功执行完一条命令,Powershell就会执行Prompt函数,提示用户进行下一步输入。默认设置中,prompt显示“PS” 和当前的工作目录。
再接着是”>”或”>>”,具体情况要看当前Powershell控制台的的层数。当然你可以自定义prompt的,那就得覆盖prompt函数:
这样的覆盖安全吗,显然安全,对预定义函数的重写,只会在当前控制台会话中有效,当你重新启动控制台时,自然会恢复如初。
在控制台的任何位置输出文本(自定义光标的位置)
因为控制台的内容存放在控制台屏幕的缓存中,因此你可以逐个访问内容的每一行或每一个字符。你甚至可以在控制台的屏幕的任何位置输出你想要输出的信息,接下来的函数会演示这个功能。要完成这个功能,需要使用$Host.UI.Rawui ,光标的位置通过屏幕的横坐标(X)和纵坐标(Y)确定,下面的函数会首先记住当前光标的位置,然后在横坐标上增加60个占位符,然后重置光标的位置至当前位置,最后通过prompt函数回复光标的原始位置。
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
" "
}
运行结果
 
使用窗口标题栏
在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 " "
}
执行以后在标题栏会显示:
如果你使用管理员权限运行控制台时,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函数的别名,但是却看不到这个函数的内容。
在Powershell中短斜杠是个特殊字符,如果一个函数名中包含了特殊字符就应当把它放在花括号中。
 
盘符名预定义函数C:,D:,E:
这些盘符名称可以作为单独的一个函数,是怎么做到的呢?
 

PowerShell函数 过滤器 管道

一个函数能够访问和进一步处理另外一条命令的结果吗?答案是肯定的,这被称为管道。管道有两种模式,一种是顺序处理模式,一种是流处理模式。
 
低效率的顺序模式:$input
在最简单的情况下,你的函数不是真正支持管道。只能对前一个命令执行后的结果处理。前一个命令执行的结果通过被自动保存在$input变量中,$input是一个数组,它可以包含许多元素,一个元素,甚至一个元素都没有,这取决于具体的环境。
到目前为止,这个函数只是仅仅输出了管道的结果,并没有其它比较强大的功能。在接下来的例子中,函数将会对管道的结果做进一步处理。函数名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
}
 
过滤器:高效率 流模式
管道的低效率顺序模式在处理大容量数据时很容易出现问题,其结果是巨大的内存占用和进程等待。如果你的函数支持高效率的流模式,在处理管道结果时仅占用很小的内存。
事实上,针对之前MarkEXE函数,你只需要替换”function” 关键字 为 “filter”,它就会开始流模式处理,这样你再也不用过分的担心忍受程序的无休止的响应和崩溃的危险。你也可以递归处理全盘目录,甚至处理极其庞大的数据。例如:
Dir c: -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
}
 
开发真正的管道函数
过滤器在函数中属于高级应用,因为它可以立即处理管道结果的每一个元素。但是过滤器必须每次重复执行预定义命令的结果。对于MarkEXE 函数,每次执行的过程中要记录和更新控制台的背景颜色,也要花费资源和时间。
事实上,过滤器只是特殊的函数。如果一个函数内部使用了管道,你就可以定义三个基础的任务区了:第一步,完成函数的初始化,完成函数执行的预备步骤;第二步处理递归调用所得的结果;最后进行收尾工作。 这三个任务区分别可以使用begin,process,end 语句块。
接下来把MarkEXE安装上面的模式进行改装:
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
     }
}
 
posted @ 2021-06-25 23:15  Ulysses~  阅读(473)  评论(0编辑  收藏  举报