PowerShell 技能连载 - 简化命令提示符

适用于 PowerShell 3.0 及以上版本

缺省情况下,PowerShell 的命令提示符中包含了当前路径。当您以一个普通用户启动 PowerShell 时,当前路径就是您的用户路径。那是一个很长的路径,会占用命令行很多空间。

最有效最简单的方法是将当前目录改为根目录:

PS C:\Users\Tobias\Documents> cd \

PS C:\>

或者,可以调整 prompt 函数,使它在其它地方显示当前路径,例如在标题栏:

function prompt
{
  'PS> '
  $host.UI.RawUI.WindowTitle = Get-Location
}

PowerShell 技能连载 - 获取 DELL 保修信息(第二部分)

适用于 PowerShell 2.0 及以上版本

在前一个技巧中我们演示了如何用一个 Web Service 来获取 DELL 电脑的保修信息。我们收到了许多反馈,所以在我们介绍新内容之前,先展示这段可以获取保修信息的代码:

$serial = '36GPL41'

$service = New-WebServiceProxy -Uri http://143.166.84.118/services/assetservice.asmx?WSDL
$guid = [Guid]::NewGuid()

$info = $service.GetAssetInformation($guid,'warrantycheck',$serial)
$Entitlements = $info.Entitlements

$Entitlements

现在,如果您试着将 $Entitlements 加到一个用户界面的文本框中,或者将它输出为文本,结果可能不是您想要的:

PS> "Your Entitlements: $Entitlements"
Your Entitlements: Microsoft.PowerShell.Commands.NewWebserviceProxy.AutogeneratedTypes.WebServiceProxy1ervices_assetservice_asmx_WSDL.EntitlementData Microsoft.PowerShell.Commands.NewWebserviceProxy.AutogeneratedTypes.WebServiceProxy1ervices_assetservice_asmx_WSDL.EntitlementData Microsoft.PowerShell.Commands.NewWebserviceProxy.AutogeneratedTypes.WebServiceProxy1ervices_assetservice_asmx_WSDL.EntitlementData

PS>

这是因为 $Entitlements 是一个对象数组。当将它以文本方式显示时,我们希望 PowerShell 扩展类型系统能处理这些对象,所以将它们用 Out-String 处理一下:

PS> $EntitlementsText = $Entitlements | Out-String

PS> "Your Entitlements: $EntitlementsText"
Your Entitlements:

ServiceLevelCode        : TS
ServiceLevelDescription : P, ProSupport
Provider                : DELL
StartDate               : 23.03.2004 00:00:00
EndDate                 : 23.03.2007 00:00:00
DaysLeft                : 0
EntitlementType         : Expired

ServiceLevelCode        : ND
ServiceLevelDescription : C, NBD ONSITE
Provider                : UNY
StartDate               : 23.03.2005 00:00:00
EndDate                 : 23.03.2007 00:00:00
DaysLeft                : 0
EntitlementType         : Expired

ServiceLevelCode        : ND
ServiceLevelDescription : C, NBD ONSITE
Provider                : UNY
StartDate               : 23.03.2004 00:00:00
EndDate                 : 24.03.2005 00:00:00
DaysLeft                : 0
EntitlementType         : Expired

PowerShell 技能连载 - 将文件的扩展名正常化

适用于 PowerShell 2.0 及以上版本

假设您希望用户提交一个文件扩展名的列表,或者是从某些其它来源获取这个列表。

文件扩展名是模糊标准的绝好例子。您要如何指定一个文本文件的扩展名呢?是用 “.txt” 还是 “*.txt”?

以下是一个将文件的扩展名正常化的简单技巧,无论它们如何拼写都有效:

$extensions = '*.ps1', '.txt'
$cleanExtensions = $extensions -replace '^\.', '*.'

$extensions
$cleanExtensions

PowerShell 技能连载 - 使用 PowerShell ISE 调试器

适用于 PowerShell 3.0 及以上版本

有些时候,很难找出 PowerShell 脚本为什么不按期望工作的原因。要更好地了解脚本工作的过程,请使用 PowerShell ISE 内置的调试器。

在您开始调试一个脚本之前请先保存它。无标题的脚本实际上还不算脚本,所以 PowerShell ISE 还无法调试它。

以下是调试一个脚本的简单步骤:

  1. 设置断点。断点是您希望调试器在代码中停下的位置,这样您可以检查当前变量的状态。要设置一个断点,请单击某一行,然后按下 F9 键。该行代码将会变成红色。如果该行未变红,说明您还没有保存该脚本,或是该行代码不包含可执行的代码。

  2. 运行脚本:脚本将会正常运行,但当运行到一个断点时,PowerShell ISE 将会暂停。当前行会标记成黄色。您可以将光标悬停到代码中的变量上来查看它们的值,或是在交互式窗口中执行任意代码,例如导出变量的值,甚至改变变量的值。

  3. 继续:要继续执行下一条指令,请按 F10F11F10 将执行当前作用域内的下一条指令。F11 将执行下一条指令,无论是哪个作用域。所以如果当前行将要执行一个函数,而您按下 F10,那么将执行整个函数。如果您按下 F11,那么您将执行到该函数的第一行。这有点像“一小步”的概念。

  4. 按下 F5 继续执行整个脚本。它将运行到整个脚本结束,或是遇到下一个断点。

  5. 按下 SHIFT+F5 退出执行过程并且停止调试器。

一旦您掌握了这些步骤,调试过程会变得十分容易。并且它可以给您带来许多领悟和帮助。如果不采用调试方法的话将很难调查脚本错误。

PowerShell 技能连载 - 在 ISE 中使用代码区域

适用于 PowerShell 3.0 及以上版本

PowerShell ISE 已经支持了可折叠的代码区域。当您编写函数、循环,或是条件时,您也许会注意到在左边距的上方有一个“减号”符号。点击它可实现折叠该区域。

如果看不到区域特性,您可以这样启用它们:视图/显示大纲(区域)。

您也可以在脚本的其它部分使用区域和代码折叠。要将代码的任意部分包括在一个可折叠的区域内,请在代码中加入一些特殊的注释:

#region Variable Declarations
$a = $b = $c = 1
$d, $e, $f = 2,3,4
#endregion

请注意这些特殊的注释是大小写敏感的。在将区域折叠之后,”#region“ 后的文本将变为折叠区域的标题。

PowerShell 技能连载 - 进程终结器(和一些陷阱)

适用于 PowerShell 3.0 及以上版本

在前一个技能中我们介绍了如何利用 Out-GridView 做一个选择对话框,并且提供了一些建议。一个点子是列出所有桌面应用,并且允许用户选择一个进程并终结它。

要列出所有桌面应用,请试试这段代码:

PS> Get-Process | Where-Object MainWindowTitle | Select-Object -Property Name, Description, MainWindowTitle, StartTime

这行代码对进程列表进行过滤并只列出设置了 MainWindowTitle 的进程。实际上,它返回了一个包含窗体的列表,忽略了所有不可见的后台进程。

接下来,将结果通过管道输出到 Out-GridView 并允许单选:

PS> Get-Process | Where-Object MainWindowTitle | Select-Object -Property Name, Description, MainWindowTitle, StartTime | Out-GridView -Title 'Kill Application' -OutputMode Single | Stop-Process -WhatIf

这行代码将打开一个网格视图窗口,显示所有运行中的进程。当您选中一个进程并点击“确定”按钮,将会杀掉该进程。不过,还差一点:这段示例代码包含了 -WhatIf 开关,所以只是 Stop-Process 只是模拟操作。

所以这是个好东西,因为您可能会注意到选择一个进程将会导致杀掉所有同名的进程。

这是由于 Stop-Process 可以接受两个不同的信息:名字(字符串),或是进程 ID (int)。由于这行代码使用了 Select-Object 来筛选属性,并且不包含进程 ID,所以 Stop-Process 将会使用进程名字,并杀掉所有同名的进程。

要实现杀除更具体的进程,请确保包含了进程的 ID:

PS> Get-Process | Where-Object MainWindowTitle | Select-Object -Property Name, Id, Description, MainWindowTitle, StartTime | Out-GridView -Title 'Kill Application' -OutputMode Single | Stop-Process -WhatIf

PowerShell 技能连载 - 将结果复制到剪贴板

适用于 PowerShell 3.0 及以上版本

在前一个技能中我们介绍了如何简单地从 Out-GridView 的网格视图窗口中复制粘贴信息。不过这并不会复制列头。

您可以将这行代码加到任意命令中,并将它的结果复制到剪贴板中(包括列头):

PS> Get-Service | Format-Table -AutoSize -Wrap | Out-String -Width 200 | clip.exe

当您运行完这行代码后,所有服务的清单就保存到剪贴板中了,接下来可以将内容粘贴到 Word 或其它接受文本输入的应用程序中。

请注意 Format-TableOut-String 的用法:它们确保数据不会按照 PowerShell 控制台的边界来格式化。相反地,可用的宽度被设为设为 200 字符,如果结果仍比这个长,那么将会折行。

如果忽略掉这两个 cmdlet,然后查看一下结果:如果没有它们,文本将会输出到 PowerShell 控制台。过长的结果将会被截断。

为了简化操作,您可以将这行代码封装为一个简单的函数,例如:

PS> function Out-Clipboard { $input | Format-Table -AutoSize -Wrap | Out-String -Width 1000 | clip.exe }

现在,当您想将结果复制到剪贴板时,可以使用 Out-Clipboard

PS> Get-Process | Out-Clipboard

PowerShell 技能连载 - Out-GridView:通用对话框

适用于 PowerShell 3.0 及以上版本

默认情况下,Out-GridView 是一条单行道:您可以将数据用管道输出到该命令,将结果显示在一个网格视图窗口中,但是您无法将数据再往下传递。

当您添加了 -PassThru 开关参数时,情况就变了。这时 Out-GridView 的右下角显示了两个新按钮:“确定”和“取消”。它将自己变为一个通用的对话框。

试试这行代码:

PS> Get-Service | Where-Object CanStop | Out-GridView -Title 'Stoppable Services' -PassThru

这将打开一个标题为 “Stoppable Services” 的网格视图窗口,并列出所有可停止的服务(您可能还需要管理员权限才可以停止它们)。

您现在可以选择一个或多个项目(按住 CTRL 键多选),然后点击网格视图窗口右下角的“确定”按钮。

如您所见,返回了选中的对象。

要将这行代码变为一个有用的工具,您可以将 Out-GridView 的结果输出到 cmdlet,来执行具体的操作。这行代码将试图停止所有选中的服务:

PS> Get-Service | Where-Object CanStop | Out-GridView -Title 'Stoppable Services' -PassThru | Stop-Service -WhatIf

请注意,出于安全考虑,我们对 Stop-Service 命令增加了 -WhatIf 参数,所以该 cmdlet 只会模拟停止服务。当您移除了这个参数,该行代码就不是模拟执行,而是真实停止服务了。

只需要坐下喝杯咖啡,然后思考一下它的原理:Out-GridView 接受任何类型的数据,所以您可以用它创建任何工具。例如,使用 Active Directory cmdlet Get-ADUser 来查找当前禁用的用户,然后让 PowerShell 为您启用所有选中的用户。

或者显示一个有主窗口的进程(桌面应用),并且杀掉所有选中的进程。

如果想达到这个目的,您可能会期望 Out-GridView 禁止多选。要想只允许选择单条记录,请试试以下代码:

PS> 1..10 | Out-GridView -Title 'Pick favorite number' -OutputMode Single

PowerShell 技能连载 - 查找所有可停止的服务

适用于 PowerShell 3.0 及以上版本

Get-Service 可以列出您计算机上所有已安装的服务。不过它没有可以选择仅包含运行或停止的服务的参数。

用一个简单的 Where-Object 从句,您可以实现这个目的。最常见的是,您会见到类似如下的用法:

PS> Get-Service | Where-Object Status -eq Running

基本上,Where-Object 可以指定对象拥有的任意属性并且允许您定义需要的条件。

如果您打算获得一个可停止的服务的列表,那么上述代码不能达到您所要的目的。一些服务可能正在运行但是不能被停止。稍微调整一下过滤条件,您就可以达到所要的目的了。这段代码列出所有运行中并且可以停止的服务:

PS> Get-Service | Where-Object CanStop

并且这种写法缩短了代码量:由于 “CanStop“ 属性本身是个布尔值(truefalse),所以无需使用比较运算符。

要查看这个列表的补集,即所有不可停止的服务,可使用比较运算符:

PS> Get-Service | Where-Object CanStop -eq $false

请注意用 Where-Object 的简化语法,您无法取得相反的结果。以下代码并不会生效:

PS> Get-Service | Where-Object !CanStop

PS> Get-Service | Where-Object -not CanStop

要使用这些条件,或者要合并比较条件,请使用完整语法:

PS> Get-Service | Where-Object { !$_.CanStop -and $_.Status -eq 'Running' }

随机抽奖中的粘连现象

现象

公司年会中的抽奖环节,是用一个计算机程序来随机抽取中奖姓名,嘉宾喊一声停,屏幕上就出现五个姓名。不过每抽取一次,大屏幕上显示的姓名往往是按部门粘连在一块的。也就是同一个部门连续出现 3-4 个中奖人。

重现

这种现象是怎么产生的呢?从程序上,可以想象到一种可能性。我们用 C# 写一个随机数生成器,并假设录入数据的时候,是按部门录入的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
internal class Generator
{
private readonly IList<string> candidateList;
private Random random = new Random();

internal Generator(string[] candidates)
{
candidateList = new List<string>(candidates);
}

internal string Pick()
{
var random = new Random(); // 会导致粘连
//var random = this.random; // 不会粘连

int index = random.Next(candidateList.Count);
Debug.Write(index + ", ");
Thread.Sleep(5); // 延时,模拟计算机性能差的情况。
// 设为 5 时,有很多“粘连”的数据。
// 设为 15 时,“粘连”现象就消失了!

string choosen = candidateList[index];
candidateList.Remove(choosen);
return choosen;
}
}

Pick() 方法中,采用 var random = new Random();,则生成的中奖名单如下:

从 Debug 窗口中可以观察到,每一轮(5 个)抽取的序号中实际上有许多是重复的或者是相邻的。而这一轮和下一轮之间的序号并不相邻。

3, 3, 3, 3, 3, 35, 35, 35, 34, 34, 14, 14, 13, 13, 13, 16, 15, 15, 15, 15, 60, 59, 58, 57, 57, 42, 41, 41, 40, 40, 8, 8, 7, 7, 7, 32, 32, 31, 31, 30, 12, 12, 11, 11, 11,

而且,调节 Thread.Sleep(x); 中的延时值,模拟计算机性能的快慢,可以改变结果粘连的程度!当延时小于 5 毫秒时,粘连现象十分明显当延时大于 15 时,粘连现象基本消失。

消除粘连

计算机性能是不可控制的,那么应当如何产生正确的随机数呢?正确的做法是,在一系列随机数生成的过程中,应该自始至终用同一个随机数发生器,而不是每生成一个随机数就临时创建一个随机数发生器。

所以在 Pick() 方法里 var random = new Random(); 的写法是不正确的。应该采用生存周期更长的 var random = this.random; 写法。代码修改后粘连现象消失了:

结论

造成粘连现象的本质原因如下:

随机数的生成是从种子值开始。 如果反复使用同一个种子,就会生成相同的数字系列。 产生不同序列的一种方法是使种子值与时间相关,从而对于 Random 的每个新实例,都会产生不同的系列。 默认情况下,Random 类的无参数构造函数使用系统时钟生成其种子值,而参数化构造函数可根据当前时间的计时周期数采用 Int32 值。 但是,因为时钟的分辨率有限,所以,如果使用无参数构造函数连续创建不同的 Random 对象,就会创建生成相同随机数序列的随机数生成器。

在一轮中,每次抽号抽取的是相同的随机数序列的第一个元素,所以结果很有可能是相同的。由于中奖的号码从列表中移走,所以很可能连续抽到相邻部门的姓名。

注意,虽然结果有些不符合常理,但是对于个人来说,中奖概率还是均等的。

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;

namespace Lottery
{
internal class Generator
{
private readonly IList<string> candidateList;
private Random random = new Random();

internal Generator(string[] candidates)
{
candidateList = new List<string>(candidates);
}

internal string Pick()
{
var random = new Random(); // 会导致粘连
//var random = this.random; // 不会粘连

int index = random.Next(candidateList.Count);
Debug.Write(index + ", ");
Thread.Sleep(5); // 延时,模拟电脑性能差的情况。
// 设为 5 时,有很多“粘连”的数据。
// 设为 15 时,“粘连”现象就消失了!

string choosen = candidateList[index];
candidateList.Remove(choosen);
return choosen;
}
}

internal class Program
{
private static void Main(string[] args)
{
string[] candidates =
{
"公司领导-宋江", "公司领导-卢俊义", "公司领导-吴用", "公司领导-公孙胜", "公司领导-关胜", "公司领导-林冲", "公司领导-秦明",
"公司领导-呼延灼", "市场部-花荣", "市场部-柴进", "市场部-李应", "市场部-朱仝", "市场部-鲁智深", "测试中心-武松", "测试中心-董平", "测试中心-张清",
"测试中心-杨志", "测试中心-徐宁", "测试中心-索超", "测试中心-戴宗", "测试中心-刘唐", "财务部-李逵", "财务部-史进", "财务部-穆弘", "财务部-雷横", "财务部-李俊",
"财务部-阮小二", "财务部-张横", "品质管理部-阮小五", "品质管理部-张顺", "品质管理部-阮小七", "品质管理部-杨雄", "品质管理部-石秀", "品质管理部-解珍",
"人力资源部-解宝", "人力资源部-燕青", "人力资源部-朱武", "人力资源部-黄信", "人力资源部-孙立", "人力资源部-宣赞", "人力资源部-郝思文", "系统支持部-韩滔",
"系统支持部-彭玘", "系统支持部-单廷珪", "系统支持部-魏定国", "系统支持部-萧让", "信息中心-裴宣", "信息中心-欧鹏", "信息中心-邓飞", "信息中心-燕顺", "信息中心-杨林",
"信息中心-凌振", "信息中心-蒋敬", "研发一部-吕方", "研发一部-郭盛", "研发一部-安道全", "研发一部-皇甫端", "研发一部-王英", "研发一部-扈三娘", "研发一部-鲍旭",
"研发一部-樊瑞", "研发二部-孔明", "研发二部-孔亮", "研发二部-项充", "研发二部-李衮", "研发二部-金大坚", "研发二部-马麟", "研发二部-童威", "研发三部-童猛",
"研发三部-孟康", "研发三部-侯健", "研发三部-陈达", "研发三部-杨春", "研发三部-郑天寿", "研发三部-陶宗旺", "研发三部-宋清", "研发三部-乐和", "研发三部-龚旺",
"研发三部-丁得孙", "研发四部-穆春", "研发四部-曹正", "研发四部-宋万", "研发四部-杜迁", "研发四部-薛永", "研发四部-施恩", "研发四部-李忠", "研发四部-周通",
"研发四部-汤隆", "研发四部-杜兴", "研发四部-邹渊", "研发五部-邹润", "研发五部-朱贵", "研发五部-朱富", "研发五部-蔡福", "研发五部-蔡庆", "研发五部-李立",
"研发五部-李云", "研发五部-焦挺", "研发六部-石勇", "研发六部-孙新", "研发六部-顾大嫂", "研发六部-张青", "研发六部-孙二娘", "研发六部-王定六", "研发六部-郁保四",
"研发六部-白胜", "研发六部-时迁", "研发六部-段景住"
};
var generator = new Generator(candidates);

Console.WindowWidth = 100;
while (Console.ReadKey().Key != ConsoleKey.Escape)
{
var sb = new StringBuilder();
for (int i = 0; i < 5; i++)
{
var choosen = generator.Pick();
sb.Append(choosen);
sb.Append('\t');
}
Console.WriteLine(sb.ToString().TrimEnd());
}
}
}
}

您也可以在这里下载完整的代码。