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

[玩转系统] 应对 CIM 目录挑战

作者:精品下载站 日期:2024-12-14 08:00:57 浏览:14 分类:玩电脑

应对 CIM 目录挑战


[玩转系统] 应对 CIM 目录挑战

2020 年的最后一项 Iron Scripter 挑战是一项重大挑战。如果你没有机会去做,看看你能想出什么,然后回来看看我的方法。与许多挑战一样,我们的目标不是生成可用于生产的 PowerShell 工具,而是突破 PowerShell 脚本编写和工具制作能力的极限。从这方面来说,这是一个巨大的挑战。

目标是使用 WMI/CIM 获取文件和文件夹的目录列表。这可能是传统文件系统列表的潜在替代方案。正如您将看到的,完成此任务是可能的。然而,并不是每个文件和/或文件夹都被注册,我还没有找到一致的原因。这意味着无法保证当我运行代码时我会得到明确的答案。但目标是学习,所以我对此表示同意。让我们深入探讨一下。

Win32_目录

解决方案的基础始于 Win32_Directory WMI 类。我将过滤单个文件夹以了解它的外观。

Get-CimInstance win32_directory -Filter "Name='C:\Work\samples'" | Tee-Object -Variable d

您还可以使用 Get-CimClass 检查类属性。请记住,对于 WMI/CIM 过滤器,要使用旧版运算符,例如=符号。\是一个特殊字符,需要转义。

[玩转系统] 应对 CIM 目录挑战

现在是有趣的部分。在 WMI 中,几乎所有内容都是相关的。使用 Get-CimAssociatedInstance 命令检索相关对象。您可以通过过滤类名来限制结果类型。

[玩转系统] 应对 CIM 目录挑战

出于我的目的,我只想查看直接子目录,因此我将修改代码以过滤掉父目录。

$d | Get-CimAssociatedInstance -ResultClassName Win32_Directory | 
Where-Object { (Split-Path $_.name) -eq $d.name } |
Select-Object Hidden, Archive, System, Compressed, Writeable, Encrypted, LastModified, Name, Path

CIM_数据文件

现在我有了获取文件夹的方法,我需要查找文件。这将使用 CIM_DataFile 类。我可以使用相同的技术。

$d | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile | Select-Object -First 1 -Property *

[玩转系统] 应对 CIM 目录挑战

同样,我也可以使用 Get-CimClass 来发现属性,但查看实际示例通常会有所帮助。

部分挑战是复制 Get-ChildItem 的输出,因此这样的内容是一个很好的概念验证。

$d | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile |
Format-Table -GroupBy @{Name = "Path"; Expression = { Resolve-Path (Join-Path -Path $_.drive -ChildPath $_.Path) } } -Property  Hidden, Archive, System, LastModified, FileSize, Name

[玩转系统] 应对 CIM 目录挑战

创建自定义类

现在我知道如何检索文件和文件夹,我想要一种更简单的方法来显示它们,而不必依赖繁琐的选择对象语法。我的答案是创建 PowerShell 类。

Class cimFolder {
    [string]$FullName
    [string]$Name
    [bool]$Archive
    [bool]$Compressed
    [DateTime]$CreationDate
    [string]$Computername
    [string]$Drive
    [bool]$Encrypted
    [bool]$Hidden
    [DateTime]$LastAccessed
    [DateTime]$LastModified
    [string]$Path
    [bool]$Readable
    [bool]$System
    [bool]$Writeable
    [string]$Mode
}

Class cimFile {
    [string]$FullName
    [string]$Name
    [bool]$Archive
    [bool]$Compressed
    [DateTime]$CreationDate
    [string]$Computername
    [string]$Drive
    [bool]$Encrypted
    [bool]$Hidden
    [int64]$FileSize
    [DateTime]$LastAccessed
    [DateTime]$LastModified
    [string]$Path
    [bool]$Readable
    [bool]$System
    [bool]$Writeable
    [string]$Mode
}

本质上,这些是原始 WMI 类的克隆。我将编写一个 PowerShell 函数,将原始 WMI 类“转换”为我的自定义类,而不是定义构造函数。

Function New-CimFile {
    [cmdletbinding()]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [object]$CimObject
    )
    Begin {
        $properties = [CimFile].DeclaredProperties.Name
    }
    Process {
        $file = [CimFile]::New()
        foreach ($item in $properties) {
            $file.$item = $CimObject.$item
        }

        $file.name = Split-Path -Path $CimObject.caption -Leaf
        $file.fullname = $CimObject.Caption
        $file.computername = $CimObject.CSName
        $file.mode = Get-Mode $CimObject

        $file
    }
    End {
        #not used
    }
}

我还编写了一个辅助函数来获取文件模式。

Function Get-Mode {
    [cmdletbinding()]
    param([object]$CimObject)

    # use the ternary operator to simplify the code,
    # although this will require PowerShell 7.x
    $dir = $CimObject.CimClass.Cimclassname -match 'Directory' ? "d" : "-"
    $archive = $CimObject.archive ? "a" : "-"
    $ro = $CimObject.writeable ? "-" : "r"
    $system = $CimObject.System ? "s" : "-"
    $hidden = $CimObject.Hidden ? "h" : "-"

    "{0}{1}{2}{3}{4}" -f $Dir, $Archive, $RO, $Hidden, $System
}

请注意,此函数使用三元运算符,因此它只能在 PowerShell 7 中运行。除了我想找个借口尝试一下之外,没有真正的理由使用该运算符。将这些元素加载到我的 PowerShell 会话中后,我可以验证这个概念。

$d | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile | New-CimFile

[玩转系统] 应对 CIM 目录挑战

我可以对目录对象执行类似的过程。最后,我可以将它们全部放在一个命令中。

Function Get-CimFolder {
    [cmdletbinding(DefaultParameterSetName = "computer")]
    [alias("cdir")]
    [OutputType("cimFolder", "cimFile")]
    Param(
        [Parameter(Position = 0, HelpMessage = "Enter the folder path. Don't include the trailing \.")]
        [ValidateNotNullorEmpty()]
        [string]$Path = ".",
        [switch]$Recurse,
        [Parameter(ParameterSetName = "computer")]
        [ValidateNotNullOrEmpty()]
        [alias("cn")]
        [string]$Computername = $ENV:Computername,
        [Parameter(ParameterSetName = "session")]
        [ValidateNotNullOrEmpty()]
        [Microsoft.Management.Infrastructure.CimSession]$CimSession
    )
    Begin {
        Write-Verbose "Starting $($myinvocation.MyCommand)"
        $cimParams = @{
            Classname  = "win32_directory"
            Filter     = ""
            CimSession = ""
        }

    } #begin
    Process {
        #convert Path to a file system path
        if ($path -match '\\$') {
            Write-Verbose "Stripping off a trailing slash"
            $path = $path -replace "\\$", ""
        }
        Try {
            $cpath = Convert-Path -Path $path -ErrorAction Stop
            #escape any \ in the path
            $rPath = $cpath.replace("\", "\\")
            $cimParams.Filter = "Name='$rpath'"
            Write-Verbose "Using query $($cimparams.filter)"
        }
        Catch {
            Write-Warning "Can't validate the path $path. $($_.Exception.Message)"
            #bail out
            return
        }
        if ($pscmdlet.ParameterSetName -eq 'computer') {
            Try {
                $cimSession = New-CimSession -ComputerName $computername -ErrorAction Stop
                $tmpSession = $True
            }
            Catch {
                Throw $_
            }
        }
        Else {
            Write-Verbose "Using an existing CIMSession to $($cimsession.computername)"
            $tmpSession = $False
        }
        $cimParams.Cimsession = $CimSession
        Write-Verbose "Getting $cpath on $($cimsession.computername)"
        $main = Get-CimInstance @cimParams

        #filter out the parent folder
        $main | Get-CimAssociatedInstance -ResultClassName Win32_Directory |
            Where-Object { (Split-Path $_.name) -eq $main.name } | New-CimFolder -outvariable cf

        $main | Get-CimAssociatedInstance -ResultClassName CIM_DATAFile | New-Cimfile

        if ($cf -AND $recurse) {
            Write-Verbose "Recursing..."
            foreach ($fldr in $cf.fullname) {
                Write-Verbose $fldr
                Get-CimFolder -path $Fldr -CimSession $cimSession
            }
        }

        if ($cimSession -AND $tmpSession) {
            Write-Verbose "Remove the temporary session"
            Remove-CimSession $cimSession
        }
    } #Process
    End {
        Write-Verbose "Ending $($myinvocation.MyCommand)"
    }
}

因为我使用的是 CIM cmdlet,所以我可以选择查询远程计算机上的文件夹,这也是挑战的一部分。我的函数使用参数集来支持通过计算机名称和 CIMSession 进行连接。

定制结果

如果您尝试该代码,您将看到结果是具有所有属性的完整对象。不一定容易阅读,也肯定达不到挑战目标。创建自己的独特类的原因之一是我可以定义自定义类型扩展,例如一组默认属性。

Update-TypeData -TypeName cimFolder -DefaultDisplayPropertySet Mode, LastModified, Size, Name -Force
Update-TypeData -TypeName cimFile -MemberType AliasProperty -MemberName Size -Value FileSize -Force
Update-TypeData -TypeName cimFile -DefaultDisplayPropertySet Mode, LastModified, Size, Name -Force

这大大清理了输出。

[玩转系统] 应对 CIM 目录挑战

但定义的对象也可以具有自定义格式文件。我使用值得信赖的 New-PSFormatXML 命令创建了一个。

<?xml version="1.0" encoding="UTF-8"?>
<!--
Format type data generated 10/21/2020 17:19:32 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools

This file will only work properly in PowerShell 7
-->
<Configuration>
  <SelectionSets>
    <SelectionSet>
      <Name>cimFileTypes</Name>
      <Types>
        <TypeName>cimFolder</TypeName>
        <TypeName>cimFile</TypeName>
      </Types>
    </SelectionSet>
  </SelectionSets>
  <ViewDefinitions>
    <View>
      <Name>default</Name>
      <ViewSelectedBy>
        <SelectionSetName>cimFileTypes</SelectionSetName>
      </ViewSelectedBy>
      <GroupBy>
        <!--
            You can also use a scriptblock to define a custom property name.
            You must have a Label tag.
            <ScriptBlock>$_.machinename.toUpper()</ScriptBlock>
            <Label>Computername</Label>

            Use <Label> to set the displayed value.
-->
        <Label>Path</Label>
        <ScriptBlock>Join-Path -path $_.drive -childpath $_.path  </ScriptBlock>
      </GroupBy>
      <TableControl>
        <!--Delete the AutoSize node if you want to use the defined widths.
        <AutoSize />-->
        <TableHeaders>
          <TableColumnHeader>
            <Label>Mode</Label>
            <Width>6</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>LastModified</Label>
            <Width>24</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Size</Label>
            <Width>14</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Name</Label>
            <!--  <Width>10</Width> -->
            <Alignment>left</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <!--
            By default the entries use property names, but you can replace them with scriptblocks.
            <ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
              <TableColumnItem>
                <PropertyName>Mode</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>LastModified</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Size</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>
                  <!-- show Directories, hidden, compressed or encrypted files in color using ANSI-->
                  if ($_.GetType().Name -eq 'cimFolder') {
                    "`e[38;5;228m$($_.name)`e[0m"
                  }
                  elseif ($_.Encrypted -OR $_.compressed) {
                      "`e[38;5;201m$($_.name)`e[0m"
                  }
                  elseif ($_.Hidden) {
                    "`e[38;5;105m$($_.name)`e[0m"
                    }
                  else {
                    $_.name
                  }
                </ScriptBlock>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

该文件使用脚本块以彩色显示某些文件,这是另一个挑战任务。我的代码使用 PowerShell 7 的 ANSI 转义序列。当我使用 Update-FormatData 加载文件后,请查看惊人的差异。

[玩转系统] 应对 CIM 目录挑战

创建模块

正如您所看到的,该解决方案有很多移动部件。我决定采取额外的步骤,将所有内容整合到一个名为 CimFolder 的模块中。您可以在 GitHub 上找到该模块:https://github.com/jdhisolutions/CimFolder。该模块是一个概念验证,不一定是生产就绪的代码,因此我不打算将其发布到 PowerShell Gallery。

也有可能出现不完整的结果。并非每个文件和文件夹都注册到 WMI。在我的测试中,我遇到了我的命令无法枚举的文件夹,但 Get-ChildItem 工作得很好。我还没有找到一致的解释或修复,这就是为什么我不会依赖我的代码进行生产。然而,我希望我如何以及为什么把这些东西放在一起是有教育意义的。

该模块具有本博客文章中代码示例的最新更新。但我不得不说,我对结果很满意。

[玩转系统] 应对 CIM 目录挑战

[玩转系统] 应对 CIM 目录挑战

我希望您能浏览代码以获取想法和灵感。

拓展你的技能

我希望您能关注 Iron Scripter 网站并尝试应对挑战。这些挑战是开放式的,因此我鼓励您浏览该网站并解决其中的一些挑战。

如果您喜欢从挑战中学习,我建议您获取一份《PowerShell 实践入门》的副本。本书包含 100 个练习题,从非常简单的水平开始,然后难度逐渐增加。答案应该不超过您将在控制台中运行的几个 PowerShell 表达式。这些不是脚本挑战。

或者,要将您的脚本编写技能提升到一个新的水平,请查看 LeanPub 上的 The PowerShell Scripting and Toolmaking Book。

无论如何,提高 PowerShell 技能的最佳方法就是继续每天使用它们。

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

取消回复欢迎 发表评论:

关灯