PowerShell 技能连载 - 复制命令行历史记录
如果您在使用 PowerShell 的过程中突然发现刚才键入的某些代码起作用了,您接下来会想把这些代码复制并粘贴到一个脚本编辑器中,将它们保存起来,然后分享给朋友。
以下是操作方法:
Get-History -Count 5 | Select-Object -ExpandProperty CommandLine | clip.exe
这将会把您最后键入的 5 条命令复制到剪贴板中。
如果您在使用 PowerShell 的过程中突然发现刚才键入的某些代码起作用了,您接下来会想把这些代码复制并粘贴到一个脚本编辑器中,将它们保存起来,然后分享给朋友。
以下是操作方法:
Get-History -Count 5 | Select-Object -ExpandProperty CommandLine | clip.exe
这将会把您最后键入的 5 条命令复制到剪贴板中。
如果您需要知道两个日期之间的间隔天数,那么可以用 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 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()
我们的脚本常常需要向已有的文本中添加新的文本。以下是一段您可能很熟悉的代码:
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()
}
以下单行代码可以列出指定月份的所有工作日:
$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
你也许偶然注意到,文件夹的 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 个元素。
后台任务可以大大提速脚本的执行,因为它们可以并行执行。然而,后台任务只适用于执行的代码不会产生大量的数据——因为通过 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
关键字。它的工作方式也许和您想象的很不一样。
我们假设有这样一个函数:
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
。
使用 continue
,循环继续执行,但是跳过剩下的代码。当您执行 break
时,循环提前结束并返回所有的结果。
另外,还有一个关键字 return
。它将导致立即退出当前的作用域。所以当您在一个函数中执行 return
,那么该函数将会退出而如果您在一个脚本中执行 return
,那么整个脚本将退出。
要在 PowerShell 中读取 Windows 环境变量,只需要使用“env:”前缀:
PS> $env:windir
C:\Windows
PS> $env:USERNAME
Tobias
实际上,“env:”是一个虚拟驱动器,所以您可以用它来查找所有(或一部分)环境变量。这段代码将列出所有名字中含有“user”的环境变量:
PS> dir env:\*user*
Name Value
---- -----
USERPROFILE C:\Users\Tobias
USERNAME Tobias
ALLUSERSPROFILE C:\ProgramData
USERDOMAIN TobiasAir1