PowerShell 技能连载 - 查找文件以及错误信息

适用于 PowerShell 所有版本

当您使用 Get-ChildItem 在目录中递归查找文件时,您有时候会遇到一些权限不足的文件夹。为了禁止错误信息,您可能会使用 -ErrorAction SilentlyContinue 的方法。

这是个不错的实践,但是您也许还希望得到一份权限不足的文件夹的清单。

以下是一段在 Windows 文件夹中搜索所有 PowerShell 脚本的脚本。它将这些文件保存在 $PSScripts 变量中。同时,它将所有的错误信息记录在 $ErrorList 变量中,并列出所有不可存取的文件夹。

$PSScripts = Get-ChildItem -Path c:\windows -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue -ErrorVariable ErrorList

$ErrorList | ForEach-Object {
  Write-Warning ('Access denied: ' + $_.CategoryInfo.TargetName)
}

PowerShell 技能连载 - 查找可改变的属性

适用于 PowerShell 所有版本

当您从 PowerShell cmdlet 中获取结果时,返回的结果都是包含属性的对象。有些属性是可改变的,另一些是只读的。

以下是一个获取可改变的属性的简单技巧。这段代码是以当前 PowerShell 宿主的进程对象为例,但您可以用任意的 cmdlet 结果。

$myProcess = Get-Process -Id $Pid

$myProcess |
  Get-Member -MemberType Properties |
  Out-String -Stream |
  Where-Object { $_ -like '*set;*' }

结果类似如下:

EnableRaisingEvents        Property       bool EnableRaisingEvents {get;set;}
MaxWorkingSet              Property       System.IntPtr MaxWorkingSet  {get;set;}
MinWorkingSet              Property       System.IntPtr MinWorkingSet  {get;set;}
PriorityBoostEnabled       Property       bool PriorityBoostEnabled  {get;set;}

PowerShell 技能连载 - 获取变量详细清单

适用于 PowerShell ISE 3 或更高版本

出于写文档等目的,您可能需要获得一份 PowerShell 脚本用到的所有变量的清单。

以下是一个名为 Get-Variable 的函数:

function Get-Variable
{

  $token = $null
  $errors = $null

  $ast = [System.Management.Automation.Language.Parser]::ParseInput($psise.CurrentFile.Editor.Text, [ref] $token, [ref] $errors)

  # not complete, add variables you want to exclude from the list:
  $systemVariables = '_', 'null', 'psitem', 'true', 'false', 'args', 'host'

  $null = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true)
  $token |
    Where-Object { $_.Kind -eq 'Variable'} |
    Select-Object -ExpandProperty Name |
    Where-Object { $systemVariables -notcontains $_ } |
    Sort-Object -Unique
}

只需要用系统自带的 ISE 编辑器打开这个脚本,然后在交互式控制台中运行 Get-Variable

您将会得到一个排序过的列表,内容是当前打开的脚本用到的所有变量。

如果您将“$psise.CurrentFile.Editor.Text”替换成一个包含脚本代码的变量,那么您可以在 ISE 编辑器之外运行这个函数。只需要用 Get-Content 将任意脚本的内容读取进一个变量,然后就可以在上述代码中使用这个变量。

PowerShell 技能连载 - 重命名变量

适用于 PowerShell ISE 3 或更高版本

以下是一个简单的变量重命名函数,您可以在 PowerShell 3.0 及以上版本的 ISE 编辑器中使用它

它将识别某个变量的所有实例,然后将它重命名为一个新的名字。

function Rename-Variable
{
  param
  (
    [Parameter(Mandatory=$true)]
    $OldName,

    [Parameter(Mandatory=$true)]
    $NewName
  )

  $InputText = $psise.CurrentFile.Editor.Text
  $token = $null
  $errors = $null

  $ast = [System.Management.Automation.Language.Parser]::ParseInput($InputText, [ref] $token, [ref] $errors)

  $token |
  Where-Object { $_.Kind -eq 'Variable'} |
  Where-Object { $_.Name -eq $OldName } |
  Sort-Object { $_.Extent.StartOffset } -Descending |
  ForEach-Object {
    $start = $_.Extent.StartOffset + 1
    $end = $_.Extent.EndOffset
    $InputText = $InputText.Remove($start, $end-$start).Insert($start, $NewName)
  }

  $psise.CurrentFile.Editor.Text = $InputText
}

运行这个函数之后,您将得到一个名为 Rename-Variable 的新命令。

下一步,在 ISE 编辑器中打开一个脚本,然后在控制台面板中,键入以下内容(当然,需要将旧的变量名“_oldVariableName_”改为您当前所打开的 ISE 脚本中实际存在的变量名)。

PS> Rename-Variable -OldName oldVariableName -NewName theNEWname

立刻,旧变量的所有出现的地方都被替换成新的变量名。

注意:这是一个非常简易的变量重命名函数。一定要记得备份您的脚本。它还不能算是一个能用在生产环境的重构方案。

当您重命名变量时,您脚本的许多别处地方也可能需要更新。例如,当一个变量是函数参数时,所有调用该函数的地方都得修改它们的参数名。

PowerShell 技能连载 - 格式化行尾符

适用于 PowerShell 所有版本

当您从 Internet 中下载了文件之后,您也许会遇到文件无法在编辑器中正常打开的情况。最常见的是,由于非正常行尾符导致的。

以下是这个问题的一个例子。在前一个技能里我们演示了如何下载一份 MAC 地址的厂家清单。当下载完成后用记事本打开它时,换行都消失了:

$url = 'http://standards.ieee.org/develop/regauth/oui/oui.txt'
$outfile = "$home\vendorlist.txt"

Invoke-WebRequest -Uri $url -OutFile $outfile

Invoke-Item -Path $outfile

要修复这个文件,只需要使用这段代码:

$OldFile = "$home\vendorlist.txt"
$NewFile = "$home\vendorlistGood.txt"

Get-Content $OldFile | Set-Content -Path $NewFile

notepad $NewFile

Get-Content 能够检测非标准的行尾符,所以结果是各行的字符串数组。当您将这些行写入一个新文件时,一切都会变正常,因为 Set-Content 会使用缺省的行尾符。

PowerShell 技能连载 - 通过 MAC 地址识别网卡厂家

适用于 PowerShell 所有版本

每个 MAC 地址唯一标识了一个网络设备。MAC 地址是由网络设备厂家分配的。所以您可以通过任何一个 MAC 地址反查出厂家信息。

您所需的只是一份大约 2MB 大小的 IEEE 厂家清单。以下是下载该清单的脚本:

$url = 'http://standards.ieee.org/develop/regauth/oui/oui.txt'
$outfile = "$home\vendorlist.txt"

Invoke-WebRequest -Uri $url -OutFile $outfile

下一步,您可以使用该清单来识别厂家信息。首先获取 MAC 地址,例如:

PS> getmac

Physical Address    Transport Name
=================== ==========================================================
5C-51-4F-62-F2-7D   \Device\Tcpip_{FF034A81-CBFE-4B11-9D81-FC8FC889A33C}
5C-51-4F-62-F2-81   Media disconnected

取 MAC 地址的前 3 个 8 进制字符,例如 _5c-51-4f_,然后用它在下载的文件中查询:

PS> Get-Content -Path $outfile | Select-String 5c-51-4f -Context 0,6

>   5C-51-4F   (hex)        Intel Corporate
    5C514F     (base 16)        Intel Corporate
                    Lot 8, Jalan Hi-Tech 2/3
                  Kulim Hi-Tech Park
                  Kulim Kedah 09000
                  MALAYSIA

您不仅可以获取厂家名称(这个例子中是 Intel),而且还可以获取厂家的地址和所在区域。

PowerShell 技能连载 - 获取 MAC 地址

适用于 PowerShell 所有版本

在 PowerShell 中获取网卡的 MAC 地址十分简单。以下是众多方法中的一个:

PS> getmac /FO CSV | ConvertFrom-Csv

Physical Address                        Transport Name
----------------                        --------------
5C-51-4F-62-F2-7D                       \Device\Tcpip_{FF034A81-CBFE-4B11-9D...
5C-51-4F-62-F2-81                       Media disconnected

有挑战性的地方在于实际的列名是本地化的,不同语言文化的值差异很大。由于原始信息是来自于 getmac.exe 生成的 CSV 数据,所以有一个简单的技巧:跳过首行(包含 CSV 头部),然后传入自定义的统一列名,以达到对列重命名的效果。

getmac.exe /FO CSV |
  Select-Object -Skip 1 |
  ConvertFrom-Csv -Header MAC, Transport

这将总是产生“_MAC_”和“_Transport_”的列。

当然,也有面向对象的解决方案,例如通过 WMI 查询或者使用 Windows 8.1 或 Server 2012/2012 R2。不过,我们认为所演示的方法是一个有趣的选择并且展示了如何将原始的 CSV 数据转换为真正有用的和语言文化无关的信息。

PowerShell 技能连载 - 高级文本分隔

适用于 PowerShell 所有版本

当您用 -split 操作符来分隔文本时,分隔符本身会被忽略掉:

PS> 'Hello, this is a text, and it has commas' -split ','
Hello
 this is a text
 and it has commas

如您所见,结果中的逗号被忽略掉了。

分隔符有可能多于一个字符。以下代码将以逗号 + 一个空格作为分隔符:

PS> 'Hello, this is a text, and it has commas' -split ', '
Hello
this is a text
and it has commas

由于 -split 接受的操作数是一个正则表达式,所以以下代码将以逗号 + 至少一个空格作为分隔符:

PS> 'Hello,    this is a    text, and it has commas' -split ',\s{1,}'
Hello
this is a    text
and it has commas

如果您需要的话,可以用 (?=…) 把分隔符包裹起来,以在结果中保留分隔符:

PS> 'Hello,    this is a    text, and it has commas' -split '(?=,\s{1,})'
Hello
,    this is a    text
, and it has commas

PowerShell 技能连载 - 分隔文本

适用于 PowerShell 所有版本

我们可以用 -split 操作符按指定的分隔符来分隔文本。这个操作符接受一个正则表达式作为操作数,所以如果您只是希望用纯文本的表达式来作为分隔的操作数,那么您需要将该纯文本转义一下。

以下是用反斜杠来分隔路径的例子:

$originalText = 'c:\windows\test\file.txt'
$splitText = [RegEx]::Escape('\')

$originalText -split $splitText

结果类似如下,并且它是一个数组:

PS> $originalText -split $splitText
c:
windows
test
file.txt

我们可以将它保存到一个变量中,然后存取单个的数组元素。

PS> $parts = $originalText -split $splitText

PS> $parts[0]
c:

PS> $parts[-1]
file.txt

用 PowerShell 下载 imooc.com 的视频教程

这是一个从 http://www.imooc.com 教学网站批量下载视频的 PowerShell 脚本。默认下载的是最高清晰度的视频。

按课程专辑 URL 下载

您可以传入课程专辑的 URL 作为下载参数:

.\Download-Imooc.ps1 http://www.imooc.com/learn/197

按课程专辑 ID 下载

可以一口气传入多个课程专辑的 ID 作为参数:

.\Download-Imooc.ps1 75,197

自动续传

如果不传任何参数的话,将在当前文件夹中搜索已下载的课程,并自动续传。

.\Download-Imooc.ps1

自动合并视频

如果希望自动合并所有视频,请使用 -Combine 参数。该参数可以和其它参数同时使用。

.\Download-Imooc.ps1 -Combine

关于

代码中用到了参数分组、-WhatIf 处理等技术,供参考。

以下是源代码:

# Require PowerShell 3.0 or higher.

[CmdletBinding(DefaultParameterSetName='URI', SupportsShouldProcess=$true, ConfirmImpact='Medium')]
Param
(
    [Parameter(ParameterSetName='URI',Position = 0)]
    [string]
    $Uri, # 'http://www.imooc.com/learn/197'

    [Parameter(ParameterSetName='ID', Position = 0)]
    [int[]]
    $ID, # @(75, 197)

    [Switch]
    $Combine, # = $true

    [Switch]
    $RemoveOriginal
)

# $DebugPreference = 'Continue' # Continue, SilentlyContinue
# $WhatIfPreference = $true # $true, $false

# 修正文件名,将文件系统不支持的字符替换成“.”
function Fix-FileName {
    Param (
        $FileName
    )

    [System.IO.Path]::GetInvalidFileNameChars() | ForEach-Object {
        $FileName = $FileName.Replace($_, '.')
    }

    return $FileName
}

# 修正目录名,将文件系统不支持的字符替换成“.”
function Fix-FolderName {
    Param (
        $FolderName
    )

    [System.IO.Path]::GetInvalidPathChars() | ForEach-Object {
        $FolderName = $FolderName.Replace($_, '.')
    }

    return $FolderName
}

# 从专辑页面中分析标题和视频页面的 ID。
function Get-ID {
    Param (
        $Uri
    )

    $Uri = $Uri.Replace('/view/', '/learn/')
    $Uri = $Uri.Replace('/qa/', '/learn/')
    $Uri = $Uri.Replace('/note/', '/learn/')
    $Uri = $Uri.Replace('/wiki/', '/learn/')
    $response = Invoke-WebRequest $Uri
    $title = $response.ParsedHtml.title

    echo $title
    $links = $response.Links
    $links | ForEach-Object {
        if ($_.href -cmatch '(?m)^/video/(\d+)$') {
            return [PSCustomObject][Ordered]@{
                Title = $_.InnerText;
                ID = $Matches[1]
            }
        }
    }
}

# 获取视频下载地址。
function Get-VideoUri {
    Param (
        [Parameter(ValueFromPipeline=$true)]
        $ID
    )

    $template = 'http://www.imooc.com/course/ajaxmediainfo/?mid={0}&mode=flash'
    $uri = $template -f $ID
    Write-Debug $uri
    $result = Invoke-RestMethod $uri
    if ($result.result -ne 0) {
        Write-Warning $result.result
    }

    $uri = $result.data.result.mpath.'0'

    # 取最高清晰度的版本。
    $uri = $uri.Replace('L.flv', 'H.flv').Replace('M.flv', 'H.flv')
    return $uri
}

# 创建“.url”快捷方式。
function New-ShortCut {
    Param (
        $Title,
        $Uri
    )

    $shell = New-Object -ComObject 'wscript.shell'
    $dir = pwd
    $path = Join-Path $dir "$Title\$Title.url"
    $lnk = $shell.CreateShortcut($path)
    $lnk.TargetPath = $Uri
    $lnk.Save()
}

function Assert-PSVersion {
    if (($PSVersionTable.PSCompatibleVersions | Where-Object Major -ge 3).Count -eq 0) {
        Write-Error '请安装 PowerShell 3.0 以上的版本。'
        exit
    }
}

function Get-ExistingCourses {
    Get-ChildItem -Directory | ForEach-Object {
        $folder = $_
        $expectedFilePath = (Join-Path $folder $folder.Name) + '.url'
        if (Test-Path -PathType Leaf $expectedFilePath) {
            $shell = New-Object -ComObject 'wscript.shell'
            $lnk = $shell.CreateShortcut($expectedFilePath)
            $targetPath = $lnk.TargetPath
            if ($targetPath -cmatch '(?m)\A^http://www\.imooc\.com/\w+/\d+$\z') {
                echo $targetPath
            }
        }
    }
}

# 下载课程。
function Download-Course {
    Param (
        [string]$Uri
    )

    Write-Progress -Activity '下载视频' -Status '分析视频 ID'
    $title, $ids = Get-ID -Uri $Uri
    Write-Output "课程名称:$title"
    Write-Debug $title
    $folderName = Fix-FolderName $title
    Write-Debug $folderName
    if (-not (Test-Path $folderName)) { $null = mkdir $folderName }
    New-ShortCut -Title $title -Uri $Uri

    $outputPathes = New-Object System.Collections.ArrayList
    $actualDownloadAny = $false
    #$ids = $ids | Select-Object -First 3
    $ids | ForEach-Object {
        if ($_.Title -cnotmatch '(?m)^\d') {
            return
        }

        $title = $_.Title
        Write-Progress -Activity '下载视频' -Status '获取视频地址'
        $videoUrl = Get-VideoUri $_.ID
        $extension = ($videoUrl -split '\.')[-1]

        $title = Fix-FileName $title
        $outputPath = "$folderName\$title.$extension"
        $null = $outputPathes.Add($outputPath)
        Write-Output $title
        Write-Debug $videoUrl
        Write-Debug $outputPath

        if (Test-Path $outputPath) {
            Write-Debug "目标文件 $outputPath 已存在,自动跳过"
        } else {
            Write-Progress -Activity '下载视频' -Status "下载《$title》视频文件"
            if ($PSCmdlet.ShouldProcess("$videoUrl", 'Invoke-WebRequest')) {
                Invoke-WebRequest -Uri $videoUrl -OutFile $outputPath
                $actualDownloadAny = $true
            }
        }
    }

    $targetFile = "$folderName\$folderName.flv"
    #if ($Combine -and ($actualDownloadAny -or -not (Test-Path $targetFile))) {
    if ($Combine) {
        # -and ($actualDownloadAny -or -not (Test-Path $targetFile))) {
        if ($actualDownloadAny -or -not (Test-Path $targetFile) -or (Test-Path $targetFile) -and $PSCmdlet.ShouldProcess('分段视频', '合并')) {
            Write-Progress -Activity '下载视频' -Status '合并视频'
            Write-Output ("合并视频(共 {0:N0} 个)" -f $outputPathes.Count)
            $outputPathes.Insert(0, $targetFile)

            $eap = $ErrorActionPreference
            $ErrorActionPreference = "SilentlyContinue"
            .\FlvBind.exe $outputPathes.ToArray()
            $ErrorActionPreference = $eap

            <#
            $outputPathes = $outputPathes | ForEach-Object {
                "`"$_`""
            }
            Start-Process `
                -WorkingDirectory (pwd) `
                -FilePath .\FlvBind.exe `
                -ArgumentList $outputPathes `
                -NoNewWindow `
                -Wait `
                -ErrorAction SilentlyContinue `
                -WindowStyle Hidden
            #>
            if ($?) {
                Write-Output '视频合并成功'
                if ($RemoveOriginal -and $PSCmdlet.ShouldProcess('分段视频', '删除')) {
                    $outputPathes.RemoveAt(0)
                    $outputPathes | ForEach-Object {
                        Remove-Item $_
                    }
                    Write-Output '原始视频删除完毕'
                }
            } else {
                Write-Warning '视频合并失败'
            }
        }
    }
}

Assert-PSVersion

# 判断参数集
$chosen= $PSCmdlet.ParameterSetName
if ($chosen -eq 'URI') {
    if ($Uri) {
        Download-Course $Uri
    } else {
        Get-ExistingCourses | ForEach-Object {
            Download-Course $_
        }
    }
}
if ($chosen -eq 'ID') {
    $template = 'http://www.imooc.com/learn/{0}'
    $ID | ForEach-Object {
        $Uri = $template -f $_
        Download-Course $Uri
    }
}

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