PowerShell 技能连载 - 复制命令行历史的工具函数

在前一个技能中我们演示了如何将之前键入的交互式 PowerShell 命令复制到您喜欢的脚本编辑器中。以下是一个能让操作更加简化的函数。如果您喜欢它,您可以将它加入您的配置脚本,那么就可以随时调用它:

function Get-MyGeniusInput
{
  param
  (
    $Count,
    $Minute = 10000
  )

  $cutoff = (Get-Date).AddMinutes(-$Minute)

  $null = $PSBoundParameters.Remove('Minute')
  $result = Get-History @PSBoundParameters |
    Where-Object { $_.StartExecutionTime -gt $cutoff } |
    Select-Object -ExpandProperty CommandLine

  $count = $result.Count
  $result | clip.exe
  Write-Warning "Copied $count command lines to the clipboard!"
}

Get-MyGeniusInput 默认将所有命令行历史都复制到剪贴板。通过 -Count 参数,您可以指定复制的条数,例如最后 5 条命令。而通过 -Minute 参数,您可以指定复制多少分钟之内的历史记录。

PS> Get-MyGeniusInput -Minute 25
WARNING: Copied 32 command lines to the clipboard!

PS> Get-MyGeniusInput -Minute 25 -Count 5
WARNING: Copied 5 command lines to the clipboard!

PS>

PowerShell 技能连载 - 复制命令行历史记录

如果您在使用 PowerShell 的过程中突然发现刚才键入的某些代码起作用了,您接下来会想把这些代码复制并粘贴到一个脚本编辑器中,将它们保存起来,然后分享给朋友。

以下是操作方法:

Get-History -Count  5  |  Select-Object  -ExpandProperty  CommandLine  |  clip.exe

这将会把您最后键入的 5 条命令复制到剪贴板中。

PowerShell 技能连载 - 查找两个日期之间的所有日子

如果您需要知道两个日期之间的间隔天数,那么可以用 New-TimeSpan 轻松地获得:

$startdate = Get-Date
$enddate = Get-Date -Date '2014-09-12'

$difference = New-TimeSpan -Start $startdate -End $enddate
$difference.Days

然而,如果您不仅想知道两者之间的间隔天数,而且还希望精确地获取每一天的日期对象,那么可以用这个方法:

$startdate = Get-Date
$enddate = Get-Date -Date '2014-09-12'

$difference = New-TimeSpan -Start $startdate -End $enddate
$days = [Math]::Ceiling($difference.TotalDays)+1

1..$days | ForEach-Object {
  $startdate
  $startdate = $startdate.AddDays(1)
}

这一次,PowerShell 输出两个指定日期之间的所有日期对象。

当您了解了精确获取每个日期对象(而不仅是总天数)的方法之后,您可以过滤(例如以星期数),并查找距离您放假或退休之前还有多少个星期天或工作日。

以下代码是获取工作日用的:

$startdate = Get-Date
$enddate = Get-Date -Date '2014-09-12'

$difference = New-TimeSpan -Start $startdate -End $enddate
$difference.Days

$days = [Math]::Ceiling($difference.TotalDays)+1

1..$days | ForEach-Object {
  $startdate
  $startdate = $startdate.AddDays(1)
} |
  Where-Object { $_.DayOfWeek -gt 0 -and $_.DayOfWeek -lt 6}

这段代码时统计工作日天数用的:

$startdate = Get-Date
$enddate = Get-Date -Date '2014-09-12'

$difference = New-TimeSpan -Start $startdate -End $enddate
"Days in all: " + $difference.Days

$days = [Math]::Ceiling($difference.TotalDays)+1

$workdays = 1..$days | ForEach-Object {
  $startdate
  $startdate = $startdate.AddDays(1)
} |
  Where-Object { $_.DayOfWeek -gt 0 -and $_.DayOfWeek -lt 6} |
  Measure-Object |
  Select-Object -ExpandProperty Count

"Workdays: $workdays"

PowerShell 技能连载 - 使用缺省参数

在 PowerShell 3.0 中,任意的 cmdlet 参数都可以定义一个缺省值。

例如这行代码,将设置所有 cmdlet 的 -Path 参数的缺省值为一个指定的路径:

$PSDefaultParameterValues.Add('*:Path', 'c:\Windows')

所以当您运行 Get-ChildItem 或任何其它包含 -Path 参数的 cmdlet 时,看起来好像您已经指定了这个参数。

除了 * 之外,您当然也可以指定 cmdlet 的名称。所以如果您希望将 Get-WmiObject-ComputerName 参数设置为指定的远程主机,那么只需要这样做:

$PSDefaultParameterValues.Add('Get-WmiObject:ComputerName', 'server12')

所有这些缺省值只在当前的 PowerShell 会话中有效。如果您想使它们始终有效,那么只需要在您的配置脚本中定义缺省值即可。

要移除所有的缺省值,请使用这行代码:

$PSDefaultParameterValues.Clear()

PowerShell 技能连载 - 通过 StringBuilder 加速脚本

我们的脚本常常需要向已有的文本中添加新的文本。以下是一段您可能很熟悉的代码:

Measure-Command {
  $text = "Hello"
  for ($x=0; $x -lt 100000; $x++)
  {
    $text += "status $x"
  }
  $text
}

这段代码运行起来非常慢,因为当您向字符串中添加文本时,整个字符串都需要重新构造。然而,有一个专用的对象,叫做 StringBuilder。它可以做相同的事情,但是速度飞快:

Measure-Command {
  $sb = New-Object -TypeName System.Text.StringBuilder
  $null = $sb.Append("Hello")

  for ($x=0; $x -lt 100000; $x++)
  {
    $null = $sb.Append("status $x")
  }

  $sb.ToString()
}

PowerShell 技能连载 - 列出工作日

以下单行代码可以列出指定月份的所有工作日:

$month = 7
1..31 | ForEach-Object { Get-Date -Day $_ -Month $month } |
  Where-Object { $_.DayOfWeek -gt 0 -and $_.DayOfWeek -lt 6 }

只需要将月份赋值给 $month(例子中以 7 月为例)。

多添一些代码,就可以从管道中返回工作日的数量:

$month = 7
1..31 | ForEach-Object { Get-Date -Day $_ -Month $month } |
  Where-Object { $_.DayOfWeek -gt 0 -and $_.DayOfWeek -lt 6 } |
  Measure-Object |
  Select-Object -ExpandProperty Count

PowerShell 技能连载 - 为什么目录的大小为 1

你也许偶然注意到,文件夹的 Length 为 1 字节。这是从 PowerShell 3.0 开始的。在 PowerShell 2.0 中,Length 并没有任何值。

$folder = Get-Item c:\Windows
$folder.Length

这个结果是由另一个特性导致的。一个对象如果不包含“Count”或“Length”属性,它也会隐式地含有这两个属性,并且预设值为 1。

PS> $host.length
1

PS> $host.Count
1

PS> (Get-Date).Length
1

PS> (Get-Date).Count
1

PS> $ConfirmPreference.Length
1

PS> $ConfirmPreference.Count
1

在过去,通常只有数组才有 Length 或 Count 属性。所以当一个 cmdlet 或 函数仅返回 1 个对象(或 0 个对象)时,结果不会被包装成数组。而导致获取结果的数量时发生错误。

通过这个新“特性”,如果一个命令只返回一个对象,那么添加的“Count”属性总是返回“1”,意味着返回了 1 个元素。

PowerShell 技能连载 - 加速后台任务

后台任务可以大大提速脚本的执行,因为它们可以并行执行。然而,后台任务只适用于执行的代码不会产生大量的数据——因为通过 XML 序列化返回数据可能会消耗掉比串行操作节约出来的更多的时间。

幸运的是,您可以控制后台操作可以返回多少数据。让我们看看如何实现。

这段代码中有三个任务($code1-3)并发执行。两个为后台任务,另一个操作 PowerShell 前台。

$start = Get-Date

$code1 = { Get-Hotfix }
$code2 = { Get-ChildItem $env:windir\system32\*.dll }
$code3 = { Get-Content -Path C:\Windows\WindowsUpdate.log }

$job1 = Start-Job -ScriptBlock $code1
$job2 = Start-Job -ScriptBlock $code2
$result3 = & $code3

$alljobs = Wait-Job $job1, $job2

Remove-Job -Job $alljobs
$result1, $result2 = Receive-Job $alljobs

$end = Get-Date
$timespan = $end - $start
$seconds = $timespan.TotalSeconds
Write-Host "This took me $seconds seconds."

这将消耗大约半分钟时间。而当您顺序执行这三个任务,而不是用后台任务,它们只消耗 5 秒钟。

$start = Get-Date

$result1 = Get-Hotfix
$result2 = Get-ChildItem $env:windir\system32\*.dll
$result3 = Get-Content -Path C:\Windows\WindowsUpdate.log

$end = Get-Date
$timespan = $end - $start
$seconds = $timespan.TotalSeconds
Write-Host "This took me $seconds seconds."

所以后台任务不仅增加了代码的复杂度,并且还延长了脚本的执行时间。只有优化过的返回数据才有意义。传递越少数据越好。

$start = Get-Date

$code1 = { Get-Hotfix | Select-Object -ExpandProperty HotfixID }
$code2 = { Get-Content -Path C:\Windows\WindowsUpdate.log | Where-Object { $_ -like '*successfully installed*' }}
$code3 = { Get-ChildItem $env:windir\system32\*.dll | Select-Object -ExpandProperty Name }

$job1 = Start-Job -ScriptBlock $code1
$job2 = Start-Job -ScriptBlock $code2
$result3 = & $code3

$alljobs = Wait-Job $job1, $job2

Remove-Job -Job $alljobs
$result1, $result2 = Receive-Job $alljob

这一次,后台任务只返回必须的数据,大大提升了执行效率。

总的来说,后台任务适合于做一些简单的事情(例如配置)而不返回任何数据或只返回少量的数据。

PowerShell 技能连载 - 理解 exit 语句

PowerShell 支持基于作用域的 exit 关键字。它的工作方式也许和您想象的很不一样。

我们假设有这样一个函数:

function test
{
  'A'
  exit
  'B'
}

当您将这个函数保存为一个脚本文件,然后执行这个脚本,将得到以下结果:

PS> C:\Users\Tobias\Documents\PowerShell\test12343.ps1
A

这意味着 exit 使函数提前退出。然而,当您没有保存该脚本,或当您以交互式的方式调用该函数,您的整个 PowerShell 宿主将会关闭。

exit 将会导致调用者上下文退出,而不仅仅是所在的函数。所以如果您将脚本像这样保存,您也许会有意外发现:

function test
{
  'A'
  exit
  'B'
}

'Start'
test
'Stop'

现在的结果类似这样:

PS> C:\Users\Tobias\Documents\PowerShell\test12343.ps1
Start
A

如您所见,“Stop”语句不再能执行到。exit 使得函数调用者作用域都退出了。这是为什么您以交互式执行该函数会导致您的 PowerShell 被关闭的原因(因为此时您的调用者作用域是宿主本身)。

那么 exit 应该怎么使用?当脚本执行结束的时候您可以用它来设置错误等级(error level)。该错误等级可以被调用者读取,所以如果您通过计划任务启动一个 PowerShell 脚本,或者用批处理文件通过 powershell.exe 来启动它,那么您通过 exit 指定的数值将会作为脚本的返回值,并赋值给批处理文件的 %ERRORLEVEL% 变量。

PowerShell 技能连载 - 使用 break、continue 和 return 语句

在 PowerShell 的循环中,有两个特殊的关键字:breakcontinue

使用 continue,循环继续执行,但是跳过剩下的代码。当您执行 break 时,循环提前结束并返回所有的结果。

另外,还有一个关键字 return。它将导致立即退出当前的作用域。所以当您在一个函数中执行 return,那么该函数将会退出而如果您在一个脚本中执行 return,那么整个脚本将退出。