当前位置:网站首页 > 更多 > 玩电脑 > 正文

[玩转系统] 构建 PowerShell 库存

作者:精品下载站 日期:2024-12-14 07:59:01 浏览:15 分类:玩电脑

构建 PowerShell 库存


几周前,发布了新的 Iron Scripter PowerShell 脚本挑战赛。对于这个挑战,我们被要求编写一些 PowerShell 代码,我们可以使用它们来清点我们的 PowerShell 脚本库。这是我解决这个问题的方法,这绝不是唯一的方法。

代码行数

挑战的第一部分是计算 PowerShell 脚本文件中的代码行数。具有 PowerShell 相关扩展名的任何文件。这需要几个步骤。

  1. 获取 PowerShell 文件
  2. 获取每个文件的内容
  3. 计算行数。

Get-ChildItem 可能是列出所有文件的命令。 Get-Content 是读取每个文件的明显选择。为了进行计数,Measure-Object 具有用于测量文件中文本行的参数。但除非您有阅读帮助文件的习惯,否则您不会知道这一点。这是一个可能的解决方案。

$Path = "C:\scripts"
Get-ChildItem -Path $Path -file -Recurse -Filter "*.ps*" |
Where-Object {$_.Extension -match "\.ps(m)?1$"} -outvariable o |
Get-Content | Measure-Object -line |
Select-Object -Property @{Name="Path";Expression={$Path}},
@{Name="TotalFiles";Expression = {$o.count}},Lines,
@{Name="Date";Expression = {Get-Date}}

[玩转系统] 构建 PowerShell 库存

运行大约需要 35 秒。我可能可以通过直接使用 .NET 类来提高性能,但我想尽可能坚持使用 cmdlet。顺便说一句,你会注意到我基本上过滤了两次。您总是希望在表达式中尽早进行过滤。我使用 -Filter 参数来获取 PowerShell 文件。这完成了大部分过滤工作。 Where-Object 使用正则表达式模式过滤掉像 foo.psx 这样的奇怪文件。

这是一个多步骤等效项。

$Path = "C:\scripts"

$files = Get-ChildItem -Path $Path -file -Recurse -Filter "*.ps*" |
Where-Object {$_.Extension -match "\.ps(m)?1$"}

$measure = $files | Get-Content | Measure-Object -line
[pscustomobject]@{
    Computername = $env:COMPUTERNAME
    Date = (Get-Date)
    Path = $Path
    TotalFiles = $files.count
    TotalLines = $measure.lines
}

相同的信息以不同的方式构建。

命令库存

所以我有超过 4700 个文件。这些文件可以追溯到 2006 年。我使用过哪些命令?这是挑战的第二部分。

我最初的想法是遵循这样的过程:

  1. 获取所有可能的命令的列表
  2. 获取所有PowerShell文件
  3. 遍历每个文件的内容
  4. 使用 Select-String 将内容与命令名称匹配

这种蛮力方法是这样的:

#brute force approach
#filter out location changing functions
$cmds = (Get-Command -CommandType Cmdlet,Function).Name.where({$_ -notmatch ":|\|\.\."}) | Get-Unique
$cmds| foreach-object -Begin {$cmdHash = @{}} -process {
  if (-Not $cmdhash.ContainsKey($_)) {$cmdHash.Add($_,0) }
}

$Path = "C:\scripts"
$files = (Get-ChildItem -Path $Path -file -Recurse -Filter "*.ps*").Where({$_.Extension -match "\.ps(m)?1$"})

[string[]]$keys =  $cmdHash.Keys

foreach ($file in $files) {
  foreach ($key in $keys) {
    if (Select-String -path $file.fullname -pattern $key -Quiet) {
     $cmdhash.item($key)++
    }
  } #foreach key

} #foreach file

($cmdhash.GetEnumerator()).WHERE({$_.value -gt 0}) | sort-object -Property Value -Descending | select-object -first 100

但这根本无法扩展,并且运行时间太长而无法实用。然后我想,为什么不使用一个巨大的正则表达式模式呢?

$r = @{}
$pester = (get-command -module pester).name

$cmds2 = $cmds.where({$pester -notcontains $_})
$pattern = $cmds2 -join "|"
[System.Text.RegularExpressions.Regex]::Matches((Get-Content $files[1].FullName),"$pattern","IgnoreCase") |
Foreach-object {
 if ($r.ContainsKey($_.value)) {
    $r.item($($_.value))++
 }
 else {
    $r.add($($_.value),1)
 }
}
$r.GetEnumerator() | Sort-Object Value -descending | Select-Object -first 25

这是使用上一个示例中的 $cmds。我还意识到我需要过滤掉 Pester 模块,因为像 It 和 Should 这样的命令被错误地检测到。此方法适用于 10 个文件。大约9秒就完成了。做完1000个文件大约需要2分钟。但全部 4800 个文件花了 2 个多小时。当然,我可以把跑步当成一份工作,稍后再得到结果,但我想要更好。

使用 AST

所以我转向了 AST。这就是 PowerShell 在底层处理代码的方式。我首先使用单个文件进行概念验证。

New-Variable astTokens -force
New-Variable astErr -force
$path = 'C:\scripts\JDH-Functions.ps1'
$AST = [System.Management.Automation.Language.Parser]::ParseFile($Path,[ref]$astTokens,[ref]$astErr)

$found = $AST.FindAll(
    {$args[0] -is [System.Management.Automation.Language.CommandAst]},
    $true
)

$asttokens 变量包含检测到的不同命令和语言元素的对象。我所需要做的就是进行一些过滤、分组和排序。

($asttokens).where({$_.tokenFlags -eq "commandname" -AND (-Not $_.nestedtokens)}) | 
Select-Object -property text | 
Group-Object -property text -NoElement |
Sort-Object -Property count -Descending

[玩转系统] 构建 PowerShell 库存

Function Measure-ScriptFile {
    [cmdletbinding()]
    [alias("msf")]

    Param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [alias("fullname")]
        [string]$Path
    )

    Begin {
        Write-Verbose "[BEGIN  ] Starting: $($MyInvocation.Mycommand)"
        $countHash = @{}

        #get all commands
        #Write-Verbose "[BEGIN  ] Building command inventory"
        #$cmds = (Get-Command -commandtype Filter, Function, Cmdlet).Name
        #Write-Verbose "[BEGIN  ] Using $($cmds.count) command names"

        New-Variable astTokens -force
        New-Variable astErr -force
    } #begin
    Process {
        $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)
        [void]$AST.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)

        #filter for commands but not file system commands like notepad.exe
        ($asttokens).where( {$_.tokenFlags -eq "commandname" -AND (-Not $_.nestedtokens) -AND ($_.text -notmatch "\.")}) |
        ForEach-Object {
            #resolve alias
            $token = $_
            if ($_.kind -eq 'identifier') {
                Try {
                    $value = (Get-Command -Name $_.text -ErrorAction stop).ResolvedCommandName
                }
                Catch {
                    #ignore the error
                    $msg = "Unresolved: text:{1} in $filename" -f $filename, $token.text
                    Write-Verbose $msg
                    $value = $null
                }
            }   #if identifier
            elseif ($token.text -eq '?') {
                $Value = 'Where-Object'
            }
            elseif ($token.text -eq '%') {
                $value = 'ForEach-Object'
            }
            #test if the text looks like a command name
            elseif ($token.text -match "\w+-\w+") {
                $value = $token.text
            }
            <#
            Use if testing for actual commands
            elseif ($cmds -contains $token.text) {
                $value = $token.text
            }
            #>
            #add the value to the counting hashtable
            if ($value) {
                if ($countHash.ContainsKey($value)) {
                    $countHash.item($value)++
                }
                else {
                    $countHash.add($value, 1)
                }
            } #if $value
        } #foreach
    } #process
    End {
        $countHash
        Write-Verbose "[END    ] Ending: $($MyInvocation.Mycommand)"
    } #end
}

如果文本看起来像命令,该函数将解析别名并构建命令表。也就是说,采用动词-名词命名约定。我有代码根据 Get-Command 构建的命令名称列表测试每个可能的命令。但这至少会增加 30 秒以上的处理时间。我显然有超过 16K 个命令,这需要时间来计算。

我可以这样运行它:

$Path = "C:\scripts"
(Get-ChildItem -Path $Path -file -Recurse -Filter "*.ps*").Where({$_.Extension -match "\.ps(m)?1$"}) | Measure-ScriptFile

这还需要时间。超过 45 分钟,但绝对更快。每个文件的平均时间不到 2 秒,这还不错。

平行线

现在最大的问题是,在这种情况下,PowerShell 7 中的新并行功能是否可以发挥作用?答案是谨慎的“也许”。请记住,设置和拆除并行处理会产生开销。您需要确保您的并行操作是“具有成本效益的”。

并行处理单个文件是没有意义的。至少需要一两秒的时间来计算开销,这与我的函数处理平均文件所需的时间一样长。我的解决方案是将我的文件分成 500 个文件组,然后并行处理每个文件组

#requires -version 7.0

[cmdletbinding()]
Param([string]$Path = "C:\scripts",[int]$ThrottleLimit = 5)

Write-Host "Getting PowerShell files from $Path" -ForegroundColor Green
$files = (Get-ChildItem -Path $Path -file -Recurse -Filter "*.ps*").Where({$_.Extension -match "\.ps(m)?1$"})
Write-Host "Processing $($files.count) files" -ForegroundColor Green

#divide the files into sets
$sets = @{}
$c=0
for ($i = 0 ; $i -lt $files.count; $i+=500) {
$c++
 $start = $i
 $end = $i+499
 $sets.Add("Set$C",@($files[$start..$end]))
 }

 #process the file sets in parallel
 $results = $sets.GetEnumerator() | 
ForEach-Object -throttlelimit $ThrottleLimit -parallel {
  . C:\scripts\Measure-ScriptFile.ps1
  Write-Host "Processing $($_.name)" -fore cyan
  $_.value | Measure-ScriptFile
 }

 #assemble the results into a single data set.
 $data = @{}
 foreach ($result in $results) {
   $result.getenumerator().foreach({
     if ($data.containskey($_.name)) {
        $data.item($_.name)+=$_.value
     }
     else {
        $data.add($_.name,$_.value)
     }
   })
 }

 #the unified results
 $data

[玩转系统] 构建 PowerShell 库存

我的函数包括传递给 ForEach-Object 的 ThrottleLimit 参数。我用不同的油门值尝试了我的脚本,但它对于这项任务并没有产生太大的影响。但速度明显更快!近 5000 个文件的处理时间约为 10 分钟!

这显然是我在 PowerShell 工作中使用最多的。以下是前 25 个命令。

[玩转系统] 构建 PowerShell 库存

全部来自 2422 个命令的总体结果。

当然,这不是一个绝对的列表。它没有考虑来自我的计算机上不再存在的模块的命令或来自我可能使用过的非标准命令名称的命令,例如私有函数。但它应该足够接近。

我不认为我解决了挑战中的所有问题,但这是一个很好的起点。我将让您尝试一下代码,看看会得到什么样的结果。

随时欢迎提出意见和问题。

更新

查看此代码的扩展版本,现已打包为 PowerShell 模块,网址为 https://jdhisolutions.com/blog/powershell/7559/an-expanded-powershell-scripting-inventory-tool/

您需要 登录账户 后才能发表评论

取消回复欢迎 发表评论:

关灯