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

[玩转系统] 解决 PowerShell 对象时代挑战 - 第 1 部分

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

解决 PowerShell 对象时代挑战 - 第 1 部分


几周前,Iron Scripter 网站发布了一个有趣的挑战,关于编写一个通用函数来获取对象的年龄。我们在 PowerShell 中处理的许多事物都有“年龄”,例如文件、进程甚至 AD 组和用户。我认为这是一个特别有用的练习,因为它迫使您理解“管道中的对象”的 PowerShell 范例。

方法

我总是发现尝试可视化管道过程很有帮助。如果没有别的事,你需要能够口头描述你的命令将要做什么。如果你不能,你将很难编写代码。我知道在检索创建时间时,我必须至少确定对象的一个属性以供使用。因为挑战的一部分是获得所有已处理对象的平均年龄,所以我知道我必须在 End 脚本块中完成大部分工作。以下是我的口述:

初始化一个数组来保存传入的对象。当对象通过管道传入函数时,将每个函数添加到临时数组中。处理完所有内容后,计算平均年龄。对于临时数组中的每个对象,创建一个具有计算时间跨度的自定义结果。

这是一个不错的起点。

概述代码

在代码中勾画出这个我想出了这个。

Begin {
  #initialize holding array
}
Process {
    foreach ($item in $input) {
    #add each item to the array
    }
}
End {
    #calculate average age for all items in holding array
    $Age = #code
    foreach ($object in $array) {
      #create custom object 
      [pscustomobject]@{
        Created  = $object.$CreationProperty
        Modified = $object.$ChangeProperty
        Age      = $Age
        Average  = If ($Age -gt $Avg) {"Above"} else {"Below"}
      }
    }
}

我希望这种情况能够改变,但这给了我一个很好的起点。对于参数,我可以获取任何类型对象的输入。但我必须依靠用户来了解创建属性的用途,以及指示对象上次更改时间的可选属性。对象可能没有“已更改”属性,因此我将默认使用创建属性。我还希望用户能够在输出中指定他们想要的其他参数。

Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullorEmpty()]
        [object[]]$InputObject,

        [Parameter(Mandatory, Position = 0)]
        [string]$CreationProperty,

        [Parameter(Position = 1)]
        [string]$ChangeProperty,

        [object[]]$Properties = @("Name")
    )

这似乎是可以管理的。

通常,我会简单地初始化一个空数组。但为了让事情变得更有趣,我将使用通用列表对象。

$all = [System.Collections.Generic.List[object]]::new()

这种类型的对象应该具有更好的性能。作为快速测试,这段代码

measure-command {
 $all = [System.Collections.Generic.List[object]]::new()
 dir c:\scripts -file -Recurse | foreach-object { $all.Add($_) }
}

添加近 7900 个文件花了 2.95 秒。与“传统”方法相比。

measure-command {
 $all = @()
 dir c:\scripts -file -Recurse | foreach-object { $all+=$_ }
}

耗时 5.5 秒。我不想分心讨论这两种方法的优点,但对于这个解决方案,我将使用通用列表。我基本上将使用相同的模式:对于每个通过管道传入的对象,将其添加到列表中。

在“结束”块中,我现在可以处理所有对象。首先,我需要为每个对象构建一个创建年龄时间跨度的数组。

$allTimeSpans = $all | ForEach-Object {
            Try {
                $X = $_
                New-TimeSpan -start $_.$creationProperty -end $Now -ErrorAction Stop
            }
            Catch {
                Write-Warning "Failed to get $CreationProperty value. $($_.exception.message)"
                Write-Warning ($X | Out-String)
            }
        }  #foreach

然后我可以测量该集合并生成平均时间跨度。我将使用 TotalSeconds。

$avg = New-TimeSpan -seconds ($allTimeSpans | Measure-Object -Property TotalSeconds -average).Average

我将使用该值作为每个对象的创建年龄的比较,以便我可以指示它是高于还是低于平均水平。

对于列表中的每个文件,我将创建一个 [ordered] 哈希表。我这样做是因为我想要至少有一些输出结构。

foreach ($item in $all) {
   $Age = New-TimeSpan -end $Now -Start ($item.$creationProperty)
   $tmpHash = [ordered]@{
       Created  = $item.$CreationProperty
       Modified = $item.$ChangeProperty
       Age      = $Age
       Average  = If ($Age -gt $Avg) {"Above"} else {"Below"}
    }

接下来,我需要考虑额外的属性。我还决定支持哈希表,就像您在 Select-Object 中使用的那样。

foreach ($prop in $Properties) {
    if ($prop.gettype().name -eq 'hashtable') {
        Write-Verbose "[END    ] Expanding hashtable"
        $custom = $item | Select-object -property $prop
        $prop = $custom.psobject.properties.Name
        $val = $custom.psobject.properties.Value
    }
    else {
        $val = $item.$prop
    }
    #shorten the value for the verbose message
    if ($VerbosePreference -eq "Continue") {
        $valtest = ($val | Out-String).trim()
        if ($valtest.length -ge 19) {
            $valstring = "{0}.." -f $valtest.Substring(0, 19)
        }
        else {
            $valstring = $valtest
        }
    }
    Write-Verbose "[END    ] Adding Property: $prop Value: $valstring"
    $tmpHash.Add($prop, $val)
} #foreach property

其要点是获取每个对象的属性值并将其作为新元素添加到临时哈希表中。我也喜欢使用详细消息,在测试中我决定缩短值文本以防止详细消息换行。纯粹是化妆品。

每个对象的最后一步是创建自定义对象。

New-Object -TypeName PSObject -property $tmpHash

附加功能

我决定用这个功能来一点额外的乐趣。首先,显示结果后,我使用 Write-Host 显示平均总体创建年龄的彩色摘要。

Write-Host "`n$([char]0x1b)[1;38;5;154mAverage Creation Age Overall : $avg"

我什至使用 ANSI 转义序列以自定义颜色对输出进行着色。为什么要写主机?函数应该只将一种类型的对象写入管道,我的已经是了。我不想让它也写一个字符串。我本可以将此信息作为属性添加到每个对象,但决定简单地将信息写入控制台而不是管道。此转义序列适用于 Windows PowerShell 和 PwoerShell 7。

我想尝试的另一件事是控制台风格的进度指示器。我想要有一些东西来表明项目正在处理并添加到列表中。再次,一点 ANSI 的魔力可以拯救你。

在 Begin 块中,我定义了一个字符数组和一个计数器。

$ch = @("|", "/", "-", "\", "|", "/", "-")
$c = 0

在处理每个项目时,我使用 Write-Host 和 ANSI 序列来确保每行都写入相同的位置。

if (-Not ($VerbosePreference -eq "Continue")) {
    if ($c -ge $ch.count) {
        $c = 0
    }
    Write-Host "$([char]0x1b)[1EProcessing $($ch[$c])$([char]0x1b)[0m" -ForegroundColor cyan -NoNewline
    $c++
    #adding an artificial sleep so the progress wheel looks like it is spinning
    Start-Sleep -Milliseconds 10
}

最终结果是一个小旋转器。我添加了人工睡眠以使其看起来更好。我只在不使用 -Verbose 时使用进度微调器,因为详细消息列表会将微调器很快移出屏幕。

功能齐全

这是完整的功能。我不会在 PowerShell ISE 中很好地显示 ANSI 序列。

Function Get-ObjectAge {
    [cmdletbinding()]
    [alias('goa')]

    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullorEmpty()]
        [object[]]$InputObject,

        [Parameter(Mandatory, Position = 0)]
        [string]$CreationProperty,

        [Parameter(Position = 1)]
        [string]$ChangeProperty,

        [object[]]$Properties = @("Name")
    )

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

        #create a list object to hold all incoming objects.
        #this should perform slightly better than a traditional array
        $all = [System.Collections.Generic.List[object]]::new()

        #use the same date time for all timespan calculations
        $Now = Get-Date
        Write-Verbose "[BEGIN  ] Using $NOW for timespan calculations"
        Write-Verbose "[BEGIN  ] Using $CreationProperty for CreationProperty"

        #use $CreationProperty parameter value for Change property if latter is not specified
        if (-Not $ChangeProperty) {
            Write-Verbose "[BEGIN  ] Using $CreationProperty for ChangeProperty"
            $changeProperty = $CreationProperty
        }
        else {
            Write-Verbose "[BEGIN  ] Using $ChangeProperty for ChangeProperty"
        }

        #initialize counters and an array of characters to be using in a progress wheel
        $i = 0
        $ch = @("|", "/", "-", "\", "|", "/", "-")
        $c = 0

    } #begin

    Process {
        foreach ($object in $InputObject) {
            $i++
            #display a progress wheel if NOT using -Verbose
            if (-Not ($VerbosePreference -eq "Continue")) {
                if ($c -ge $ch.count) {
                    $c = 0
                }
                Write-Host "$([char]0x1b)[1EProcessing $($ch[$c])$([char]0x1b)[0m" -ForegroundColor cyan -NoNewline
                $c++
                #adding an artificial sleep so the progress wheel looks like it is spinning
                Start-Sleep -Milliseconds 10
            }
            #add the incoming object to the list
            $all.Add($object)
        }
    } #process

    End {
        Write-Verbose "[END    ] Calculating average creation age for $($all.count) objects"
        $allTimeSpans = $all | ForEach-Object {
            Try {
                $X = $_
                New-TimeSpan -start $_.$creationProperty -end $Now -ErrorAction Stop
            }
            Catch {
                Write-Warning "Failed to get $CreationProperty value. $($_.exception.message)"
                Write-Warning ($X | Out-String)
            }
        }  #foreach

        #get the average creation age timespan
        $avg = New-TimeSpan -seconds ($allTimeSpans | Measure-Object -Property TotalSeconds -average).Average

        #create a result object for each processed object and save to an array
        $results = foreach ($item in $all) {
            $Age = New-TimeSpan -end $Now -Start ($item.$creationProperty)
            $tmpHash = [ordered]@{
                Created  = $item.$CreationProperty
                Modified = $item.$ChangeProperty
                Age      = $Age
                Average  = If ($Age -gt $Avg) {"Above"} else {"Below"}
            }

            #add user specified properties
            foreach ($prop in $Properties) {
               if ($prop.gettype().name -eq 'hashtable') {
                    Write-Verbose "[END    ] Expanding hashtable"
                    $custom = $item | Select-object -property $prop
                    $prop = $custom.psobject.properties.Name
                    $val = $custom.psobject.properties.Value
                }
                else {
                    $val = $item.$prop
                }
                #shorten the value for the verbose message
                if ($VerbosePreference -eq "Continue") {
                    $valtest = ($val | Out-String).trim()
                    if ($valtest.length -ge 19) {
                        $valstring = "{0}.." -f $valtest.Substring(0, 19)
                    }
                    else {
                        $valstring = $valtest
                    }
                }
                Write-Verbose "[END    ] Adding Property: $prop Value: $valstring"
                $tmpHash.Add($prop, $val)
            } #foreach property

            #create the object
            New-Object -TypeName PSObject -property $tmpHash

        } #foreach item

        #display all the results at once
        $results

        #display a message about the average creation age
        Write-Host "`n$([char]0x1b)[1;38;5;154mAverage Creation Age Overall : $avg"
        
        Write-Verbose "[END    ] Ending: $($MyInvocation.Mycommand)"
    } #end
} #end function

想看看它的效果吗?该函数适用于文件。

[玩转系统] 解决 PowerShell 对象时代挑战 - 第 1 部分

[玩转系统] 解决 PowerShell 对象时代挑战 - 第 1 部分

让它变得更好

我有一个足够通用的函数,可以处理任何对象类型,假设它有一个反映它创建时间的属性。尽管这很好,但我知道它还可以更好。但我想我已经给了你足够玩一天的内容,所以我很快就会把增强版保存到另一天。

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

取消回复欢迎 发表评论:

关灯