PowerShell 技能连载 - 产生多个返回值

适用于所有 PowerShell 版本

如果一个 PowerShell 函数需要产生多个返回信息,最佳的实践方式是返回多个对象,然后将信息分别存储在对象的各个属性中。

以下是一个有趣的例外情况,它在某些场景中较为适用。尽管返回多个信息就可以了,并且要确保将结果赋值给多个变量:

function Get-MultipleData
{
  Get-Date
  'Hello'
  1+4
}

$date, $text, $result = Get-MultipleData

"The date is $date"
"The text was $text"
"The result is $result"

这个测试函数产生 3 段信息,然后将结果存储在 3 个不同的变量中。

Excel 列号和数字互相转换

Excel 的列号是采用“A”、“B”……“Z”、“AA”、“AB”……的方式编号。但是我们在自动化操作中,往往希望用数字作为列号。我们可以用 PowerShell 来实现 Excel 的列号和数字之间的互相转换。

需求归纳

Excel 列号 -> 数字

A   1
AB  28
AC  29

数字 -> Excel 列号

1   A
2   B
24  Y
26  Z
27  AA
28  AB

算法分析

  • Excel 列号 -> 数字
    • 用 ASCII 编码对输入的字符串解码,得到一个数字型数组。
    • 用 26 进制对数组进行处理(逐位 *= 26,然后累加)。
  • 数字 -> Excel 列号
    • 用 26 进制对数字进行处理(不断地 /= 26,取余数),得到数字型数组。
    • 将数字型数组顺序颠倒。
    • 用 ASCII 编码对数字型数组编码,得到 Excel 风格的列号。

源代码

转换函数:

function ConvertFrom-ExcelColumn ($column) {
    $result = 0
    $ids = [System.Text.Encoding]::ASCII.GetBytes($column) | foreach {
        $result = $result * 26 + $_ - 64
    }
    return $result
}

function ConvertTo-ExcelColumn ($number) {
    $ids = while ($number -gt 0) {
        ($number - 1) % 26 + 1 + 64
        $number = [math]::Truncate(($number - 1) / 26)
    }

    [array]::Reverse($ids)
    return [System.Text.Encoding]::ASCII.GetString([array]$ids)
}

测试代码:

echo "A`t$(ConvertFrom-ExcelColumn A)"
echo "AB`t$(ConvertFrom-ExcelColumn AB)"
echo "AC`t$(ConvertFrom-ExcelColumn AC)"

echo ''

@(1..2) + @(25..28) | foreach {
    echo "$_`t$(ConvertTo-ExcelColumn $_)"
}

执行结果:

A   1
AB  28
AC  29

1   A
2   B
25  Y
26  Z
27  AA
28  AB

您也可以在这里下载完整的脚本。

用 PowerShell 快速查看 PATH 环境变量

我们常常需要查看 PATH 环境变量里是否有我们需要的路径。通常的做法是:

  1. 依次打开 系统属性 / 高级 / 环境变量。
  2. 分别在“用户变量”和“系统变量”列表框中双击 PATH 条目。
  3. 在“变量值”窄小的文本框中检视 PATH 变量的值。
  4. 往往不得不把变量值复制粘贴到记事本中,再利用搜索功能来查找。

利用 PowerShell,可以告别以上笨拙的步骤:

PS > (type env:path) -split ';'

这样就可以看到一个完美分割过的列表了。当然,利用 PowerShell 强大的查询功能,还可以进一步节省眼力。例如我们要查询所有包含“_bin_”的路径:

PS > (type env:path) -split ';' | sls bin

C:\PROGRAM FILES (X86)\JAVA\JDK1.7.0_45\JRE\BIN
C:\PROGRAM FILES (X86)\INTEL\OPENCL SDK\2.0\BIN\X86
C:\PROGRAM FILES (X86)\INTEL\OPENCL SDK\2.0\BIN\X64
C:\PROGRAM FILES\MICROSOFT SQL SERVER\110\TOOLS\BINN\
D:\greensoft\UnxUtils\usr\local\wbin\
C:\Program Files\Microsoft SQL Server\120\Tools\Binn\
C:\Program Files\TortoiseGit\bin
C:\Chocolatey\bin
c:\Program Files\MongoDB 2.6 Standard\bin

PowerShell 技能连载 - 编辑“hosts”文件

适用于所有 PowerShell 版本

如果您常常需要修改“hosts”文件,那么手工用提升权限的记事本实例来打开文件是相当乏味的事情。这是因为该文件只能被 Administrators 用户修改,所以普通的记事本实例无法修改它。

以下是一段您可以直接使用,或者调整一下用来打开任何需要提升权限的程序的脚本。

function Show-HostsFile
{
  $Path = "$env:windir\system32\drivers\etc\hosts"
  Start-Process -FilePath notepad -ArgumentList $Path -Verb runas
}

PowerShell 技能连载 - 请注意 UNC 路径!

适用于所有 PowerShell 版本

许多 cmdlet 可以处理 UNC 路径,但是使用 UNC 路径会导致很多古怪的情况。请看以下:

PS> Test-Path -Path \\127.0.0.1\c$
True

这段代码返回了 true,该 UNC 路径存在。现在将当前驱动器变为一个非文件系统驱动器,然后再次实验:

PS> cd hkcu:\

PS> Test-Path -Path \\127.0.0.1\c$
False

同样的路径现在返回了 false。这是因为 UNC 路径并不包含驱动器号,而 PowerShell 需要驱动器号来指定正确的提供器。如果一个路径不包含驱动器号,那么 PowerShell 假设使用当前驱动器的提供器。所以如果您将当前的目录改为注册表,PowerShell 尝试在那儿查找 UNC 路径,那么就会失败。

更糟糕的是,出于某些未知的原因,但您用 net use 来映射驱动器时,PowerShell 在使用 cmdlet 来访问驱动器时可能会也可能不会产生混淆。

解决方案十分简单:当您用 cmdlet 访问 UNC 时,始终在 UNC 路径前面加上正确的提供器名称。这将消除该问题:

PS> Test-Path -Path FileSystem::\\127.0.0.1\c$
True

PS> cd hkcu:\

PS> Test-Path -Path \\127.0.0.1\c$
False

PS> Test-Path -Path FileSystem::\\127.0.0.1\c$
True

如果您遇到了 net use 产生的问题,也可以使用同样的办法,在路径前面加上 “FileSystem::。该问题可以立刻得到解决。

从命令行运行 PowerShell

从命令行运行 PowerShell 命令最精炼的代码:

@powershell -nop -ex unrestricted -c "Get-ChildItem"

最后一个参数可以换成别的命令。另外,从命令行运行 .ps1 脚本的方式是:

@powershell -nop -ex unrestricted .\something.ps1

用 PowerShell 批量分割 QQ 聊天记录

纯文本文件有诸多的好处:

  • 通用
  • 易于管理
  • 易于搜索
  • 易于迁移

接下来我们用 PoewrShell 来处理 QQ 的聊天记录。目的是将所有的聊天记录按照“组名/对象名.txt”来分别保存每个好友、每个 QQ 群等的聊天记录。

我现在用的是 QQ 6.1 (11905) 版本。依次打开 QQ / 工具 / 消息管理器,点击右上角的倒三角按钮可以看到“导出全部消息记录”菜单项。我们在接下来的对话框里的保存类型中选择“文本文件(*.txt,不支持导入)”,并用默认的“全部消息记录.txt”文件名保存。保存之后的文件内容大概是如下格式:

消息记录(此消息记录为文本格式,不支持重新导入)

================================================================
消息分组:我的好友
================================================================
消息对象:Victor.Woo
================================================================

2010-01-06 16:57:28 Victor.Woo
http://pic4.nipic.com/20090728/1684061_175750076_2.jpg

2010-05-27 12:29:35 Victor.Woo
6块钱包月55
8000/月
中心端,用户端

================================================================
消息分组:技术.关注
================================================================
消息对象:*PowerShell技术交流
================================================================

2013-06-23 15:52:32 此消彼长,云过有痕<qq_g@163.com>
http://yun.baidu.com/buy/center?tag=4#FAQ02

百度亮了,自己找亮点

2013-06-23 18:42:35 Victor.Woo<victorwoo@gmail.com>
[表情]

观察它的规律:

  • ================================================================ 作为每一段的元数据开始。
  • 接下来依次是消息分组、分隔符、消息对象。
  • ================================================================ 作为元数据的结束。
  • 元数据之后,是正文部分,直到下一个元数据开始。
  • 文件头部还有两行无关内容。
  • 文件尺寸巨大,不适合整体用正则表达式来提取,只能一行一行解析。

我们的目标是生成 我的好友/Victor.Woo.txt技术.关注/.PowerShell技术交流.txt

根据这个规律,我们可以用类似“状态机”的思想来设计 PowerShell 脚本。在遍历源文件的所有行时,用一个 $status 变量来表示当前的状态,各个状态的含义如下:

状态 含义
INIT 初始状态
ENTER_BLOCK 进入一个元数据块
ENTER_GROUP “消息分组”解析完成
ENTER_SPLITTER 元数据中间的分隔符解析完成
ENTER_TARGET “消息对象”解析完成
LEAVE_BLOCK 元数据块解析完成
CONTENT 当前行是正文内容

然后用一个 switch 语句让 $status 变量在这些状态之间来回跳转,就能解析出一个一个独立的消息文件了。完整代码如下:

function Get-Status($status, $textLine, $lineNumber, $block) {
    $splitter = '================================================================'
    switch ($status) {
        'INIT' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_BLOCK'
            }
        }
        'ENTER_BLOCK' {
            if ($textLine -cmatch '消息分组:(.*)') {
              $block.Group = $matches[1]
                $block.Target = $null
                $status = 'ENTER_GROUP'
                break
            } else {
              Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'ENTER_GROUP' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_SPLITTER'
                break
            } else {
                Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'ENTER_SPLITTER' {
            if ($textLine -cmatch '消息对象:(.*)') {
              $block.Target = $matches[1]
                $status = 'ENTER_TARGET'
                break
            } else {
              Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'ENTER_TARGET' {
            if ($textLine -eq $splitter) {
                $status = 'LEAVE_BLOCK'
                break
            } else {
                Write-Error "[$lineNumber] [$status] $textLine"
                exit
            }
        }
        'LEAVE_BLOCK' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_BLOCK'
                break
            } else {
                $status = 'CONTENT'
            }
        }
        'CONTENT' {
            if ($textLine -eq $splitter) {
                $status = 'ENTER_BLOCK'
                break
            } else {
                $status = 'CONTENT'
            }
        }
    }

    return $status
}

$status = 'INIT'
$lineNumber = 0
$block = @{}
$targetPath = $null
cat 全部消息记录.txt -Encoding UTF8 | foreach {
    $textLine = $_
    $lineNumber++
    $status = Get-Status $status $textLine $lineNumber $block
    switch ($status) {
        'LEAVE_BLOCK' {
            if ($block.Target -eq '最近联系人') {
                break
            }
            $dirName = $block.Group.Replace('*', '.')
            if (!(Test-Path $dirName)) {
                md $dirName | Out-Null
            }

            $fileName = $block.Target.Replace('*', '.')

            $targetPath = (Join-Path $dirName $fileName) + '.txt'
            if (Test-Path $targetPath) {
                del $targetPath
            }

            echo $targetPath
        }
        'CONTENT' {
            #echo $textLine
            if ($block.Target -eq '最近联系人') {
                break
            }
            Out-File -InputObject $textLine -Encoding utf8 -LiteralPath $targetPath -Append
        }
    }
}

您也可以在这里下载完成后的版本。

PowerShell 技能连载 - 查找 AD 用户

适用于所有 PowerShell 版本

假如您已登录到了一个活动目录域中,那么只需要执行一些简单的命令就可以搜索活动目录。在前一个技巧中我们演示了最基本的脚本。以下是一个扩展,它能够定义一个搜索的根(搜索的起点),就像一个扁平的搜索一样(相对于在容器中递归而言)。

它也演示了如何将活动目录的搜索结果转换成实际的用户对象:

$SAMAccountName = 'tobias'
$SearchRoot = 'LDAP://OU=customer,DC=company,DC=com'
$SearchScope = 'OneLevel'

$ldap = "(&(objectClass=user)(samAccountName=*$SAMAccountName*))"
$searcher = [adsisearcher]$ldap
$searcher.SearchRoot = $SearchRoot
$searcher.PageSize = 999
$searcher.SearchScope = $SearchScope

$searcher.FindAll() |
  ForEach-Object { $_.GetDirectoryEntry()  } |
  Select-Object -Property *

PowerShell 技能连载 - 查找并提取注册表键的路径

适用于所有 PowerShell 版本

在前一个技巧中,我们演示了如何将一个 PowerShell 内部的路径格式转换为一个真实的路径。以下是一个用力。这段代码递归地搜索 HKEY_CURRENT_USER 键,并且找出所有包含单词“_powershell_”的注册表键(您可以将搜索关键字换成任何别的):

Get-ChildItem -Path HKCU:\ -Include *PowerShell* -Recurse -ErrorAction SilentlyContinue |
  Select-Object -Property *Path* |
  Out-GridView

这段代码输出所有名称中包含“Path_”的属性。如您所见,注册表键中有两个属性包含该关键字:_PSPath 和 _PSParentPath_。两者都是 PowerShell 内置的路径格式。

要提取所有满足搜索条件的注册表键的路径,请使用以下代码:

Get-ChildItem -Path HKCU:\ -Include *PowerShell* -Recurse -ErrorAction SilentlyContinue |
  ForEach-Object {
    Convert-Path -Path $_.PSPath
  }

PowerShell 技能连载 - 修正 PowerShell 中的路径

适用于所有 PowerShell 版本

有些时候,您会为某些奇怪的路径格式感到困惑,比如这个:

Microsoft.PowerShell.Core\FileSystem::C:\windows\explorer.exe

这是一个完整的 PowerShell 路径名,路径中包含了了模块名和提供器名。要得到一个纯的路径名,请使用以下代码:

Convert-Path -Path Microsoft.PowerShell.Core\FileSystem::C:\windows\explorer.exe