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

[玩转系统] 应对 PowerShell 链接挑战

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

应对 PowerShell 链接挑战


几周前,Iron Scripter 面临的挑战是将符合某些标准的文件移动到新位置并留下链接。正如我之前所写,这些挑战是测试您的 PowerShell 技能和拓展自我的好方法。这项挑战有许多活动部分。

通常,使用 PowerShell 时最困难的工作是规划和组织。你将如何完成给定的任务?如果您正在编写其他人将使用的代码,他们会有什么期望或假设?他们如何使用你的命令?我还考虑灵活性和重用。我尽量避免编写一个可以做几件事的单一函数。相反,您应该专注于可以组合在管道表达式中的小型一次性任务。或者从控制器脚本进行编排,这就是我稍后将向您展示的内容。

测试核心命令

此任务中的核心命令是 New-Item。您可以使用此 cmdlet 创建文件、文件夹和链接。我假设您会查看完整的帮助和示例。我来试一下。

New-Item -ItemType SymbolicLink -Path . -Name procmon.exe -Value D:\OneDrive\tools\Procmon.exe

此命令将在当前目录中创建指向 D:\OneDrive\tools\Procmon.exe 的链接。

[玩转系统] 应对 PowerShell 链接挑战

我可以在 C:\Work 中运行 procmon.exe,而无需关心实际文件所在的位置。出色的。挑战的前提是浏览文件列表,将它们移动到目的地并留下链接。

移动和链接

这听起来像是我可能想重复使用的任务。我不想限制要处理的文件。我希望能够将任何文件传递给我的命令并让它移动和链接。从技术上讲,PowerShell 有一个 Move-Item 命令,但我将“移动和链接”过程视为单个操作,因此我将以这种方式对其进行编码。另外,我还想解决一些其他具有挑战性的功能。

这是我的“移动和链接”功能。

#requires -version 5.1
Function Move-FileWithLink {
    [CmdletBinding(SupportsShouldProcess)]
    Param(
        [Parameter(Position = 0, Mandatory, HelpMessage = "Specify the path of a file to move.", ValueFromPipelineByPropertyName)]
        [alias("fullname")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,

        [Parameter(HelpMessage = "Specify the top-level target path. It will be created if it doesn't exist.")]
        [ValidateNotNullOrEmpty()]
        [string]$Destination = "\172.16.10.100\backup\lts",

        [switch]$Passthru
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
        #define a timestamped logfile
        $logname = "{0}_MoveLink.log" -f (Get-Date -Format "yyyy_MM-dd-hhmm")
        #using .NET to support cross-platform
        $logfile = Join-Path -Path $([System.io.path]::GetTemppath()) -ChildPath $logname
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Logging activity to $logfile"

        #define a logging helper function
        function log {
            [cmdletbinding(SupportsShouldProcess)]
            param([string]$Message, [string]$LogFile)
            $text = "[{0}] {1}" -f (Get-Date -Format u), $Message
            $text | Out-File -FilePath $logFile -Force -Append
        }

        #set the default parameter value for the log function
        $PSDefaultParameterValues["log:logfile"] = $logfile

        log "Starting: $($myinvocation.MyCommand)"
        $who = "$([System.Environment]::UserDomainName)$([System.Environment]::UserName)"
        $where = [System.Environment]::MachineName
        log "User: $who"
        log "Computer: $where"

        if (-Not (Test-Path -Path $Destination)) {
            Try {
                log "Creating $destination"
                [void](New-Item -ItemType Directory -Path $Destination -Force -ErrorAction Stop)
            }
            Catch {
                Throw $_
            }
        }
    } #begin
    Process {
        $Path = Convert-Path $path
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Path"
        log "Processing $path"
        $parent = Split-Path -Path $path
        $name = Split-Path -Path $path -Leaf
        #get the relative path so it can be reconstructed in the destination
        $target = Join-Path -Path $Destination -ChildPath $parent.Substring(3)

        if ($pscmdlet.ShouldProcess($path, "Move file to $target")) {
            Try {
                #recreate file structure
                if (-Not (Test-Path -Path $Target)) {
                    Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating $Target"
                    [void](New-Item -ItemType Directory -Path $Target -Force -ErrorAction Stop)
                    log "Created target $target"
                }
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Moving file to $Target"
                $m = Move-Item -Path $path -Destination $target -PassThru -ErrorAction Stop
                log "Moved $path to $target"
            }
            Catch {
                $msg = "Failed to move $path to $target. $($_.Exception.message)"
                Write-Warning $msg
                log $msg
            }
        }

        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating link for $path"
        if ($pscmdlet.ShouldProcess($path, "Creating link")) {
            if (Test-Path $m.fullname) {
                Try {
                    $link = New-Item -ItemType SymbolicLink -Name $name -Path $parent -Value $m.FullName -Force -ErrorAction Stop
                    log "Created symboliclink from $($m.fullname)"
                    if ($passthru) {
                        $link
                    }
                }
                Catch {
                    $msg = "Failed to create link to $path from $($m.fullname). $($_.Exception.message)"
                    Write-Warning $msg
                    log $msg
                }
            }
        }

    } #process
    End {
        log "Ending: $($myinvocation.MyCommand)"
        if (Test-Path $logfile) {
            Write-Verbose "[$((Get-Date).TimeofDay) END    ] Activity logged at $logfile"
        }
        if ($PSDefaultParameterValues.ContainsKey("log:logfile")) {
            $PSDefaultParameterValues.Remove("log:logfile")
        }
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end
}

该函数将采用文件名作为参数,将其复制到目标,然后在原始位置创建链接。该函数具有硬编码的日志记录功能,它将在用户的 %TEMP% 文件夹中创建基于文本的日志文件。

$logname = "{0}_MoveLink.log" -f (Get-Date -Format "yyyy_MM-dd-hhmm")
#using .NET to support cross-platform
$logfile = Join-Path -Path $([System.io.path]::GetTemppath()) -ChildPath $logname

function log {
    [cmdletbinding(SupportsShouldProcess)]
    param([string]$Message, [string]$LogFile)
    $text = "[{0}] {1}" -f (Get-Date -Format u), $Message
    $text | Out-File -FilePath $logFile -Force -Append
}

我编写了一个简短的辅助函数,以便每个日志条目都包含 UTC 时间戳。我想让我的代码保持简单,所以我临时为该函数设置了一个 PSDefaultParameterValue。

#set the default parameter value for the log function
$PSDefaultParameterValues["log:logfile"] = $logfile

log "Starting: $($myinvocation.MyCommand)"

这使得在整个脚本中记录事件变得容易。在函数末尾,我删除了 PSDefaultParameterValue。

if ($PSDefaultParameterValues.ContainsKey("log:logfile")) {
    $PSDefaultParameterValues.Remove("log:logfile")
}

该函数使用 cmdletbinding 属性来启用 -WhatIf。尽管我使用的大多数命令都会自动检测 -WhatIf,但我选择创建自己的 -WhatIf 代码,以便对消息传递有更多控制。

if ($pscmdlet.ShouldProcess($path, "Move file to $target")) {
    Try {
        #recreate file structure
        if (-Not (Test-Path -Path $Target)) {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating $Target"
            [void](New-Item -ItemType Directory -Path $Target -Force -ErrorAction Stop)
            log "Created target $target"
        }
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Moving file to $Target"
        $m = Move-Item -Path $path -Destination $target -PassThru -ErrorAction Stop
        log "Moved $path to $target"
    }
    Catch {
        $msg = "Failed to move $path to $target. $($_.Exception.message)"
        Write-Warning $msg
        log $msg
    }
}

我可以像这样使用该函数:

[玩转系统] 应对 PowerShell 链接挑战

还可以检测日志记录功能的 WhatIf。我将移动并链接该文件。

[玩转系统] 应对 PowerShell 链接挑战

这就是我日志中的最终内容。

[2020-12-02 16:16:37Z] Starting: Move-FileWithLink
[2020-12-02 16:16:37Z] User: PROSPERO\Jeff
[2020-12-02 16:16:37Z] Computer: PROSPERO
[2020-12-02 16:16:37Z] Creating d:\archive
[2020-12-02 16:16:37Z] Processing C:\work\s.clixml
[2020-12-02 16:16:37Z] Created target d:\archive\work
[2020-12-02 16:16:37Z] Moved C:\work\s.clixml to d:\archive\work
[2020-12-02 16:16:37Z] Created symboliclink from D:\archive\work\s.clixml
[2020-12-02 16:16:37Z] Ending: Move-FileWithLink

使用控制器脚本

现在我有了一个可以移动和链接文件的工作工具,我需要一种简单的方法来使用它。这就是考虑将使用您的命令以及如何发挥作用的地方。移动和链接功能将接受任何输入。经验丰富的 PowerShell 用户可以简单地运行如下命令:

[玩转系统] 应对 PowerShell 链接挑战

但是,挑战在于寻找一种用户可以在指定位置运行的工具。我们面临的挑战还在于寻找一种工具来移动和链接在给定日期之前最后修改的文件。我编写了一个控制器脚本

此类脚本旨在包装一些核心命令并以结构化方式执行它们。您可以手动运行脚本中的命令并获得相同的结果。控制器脚本还可以使用参数以及函数中几乎所有的脚本功能。

#requires -version 5.1

<#
move specified files to an alternate location, leaving
linked copies behind

Specify the source folder
Specify the target folder
Specify the last modified age
Support recursion
Support an exclude filter

This could be turned into a function instead of a control script
#>

[CmdletBinding(SupportsShouldProcess)]
Param (
    [parameter(Position = 0, Mandatory, HelpMessage = "The folder to process for old files.", ValueFromPipeline)]
    [ValidateScript({Test-Path $_ })]
    [string]$Path,
    [parameter(Position = 1, Mandatory, HelpMessage = "The top-level destination folder.")]
    [string]$Destination,
    [Parameter(HelpMessage = "Specify the last modified cutoff date.")]
    [datetime]$LastModified = (Get-Date).AddDays(-180).Date,
    [parameter(HelpMessage = "Recurse for files from the given path.")]
    [switch]$Recurse,
    [parameter(HelpMessage = "Specify a file pattern to exclude. Wildcards are permitted.")]
    [string]$Exclude,
    [switch]$Passthru
)

Begin {
    Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"

    #dot source the function that moves files and creates links
    . $PSScriptRoot\Move-FileLink.ps1

    $getParams = @{
        ErrorAction = "Stop"
        File = $True
    }
    if ($Recurse) {
        $getParams.Add("Recurse",$True)
    }
    $moveParams = @{
        Destination = $Destination
        Passthru = $Passthru
    }
    Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Finding files last modified before $LastModified"
    if ($Exclude) {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Excluding files that match the pattern $Exclude"
    }
    Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Using a destination of $destination"

} #begin

Process {
    #convert to a complete file-system path
    $Path = Convert-Path $Path
    Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Path"
    $getParams.Path = $Path

    #build a filter for Where-Object. -Exclude only works with Get-Childitem when using
    #recurse, which the user might not want.
    if ($Exclude) {
        Get-ChildItem @getParams | Where-object {($_.LastWriteTime -lt $LastModified) -AND ($_.name -notlike $Exclude)} |
        Move-FileWithLink @moveParams
    }
    else {
        Get-ChildItem @getParams | Where-Object { $_.LastWriteTime -lt $LastModified } | Move-FileWithLink @moveParams
    }
} #process

End {
    Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
} #end

如果我愿意的话,将其转换为函数并不需要太多。事实上,我经常从独立脚本开始,因为我可以运行它们而无需额外的点源步骤。一旦我对代码感到满意,添加功能位就很简单了。

让我们看看这个脚本的实际效果。

[玩转系统] 应对 PowerShell 链接挑战

详细输出清楚地表明了该命令将要执行的操作。

[玩转系统] 应对 PowerShell 链接挑战

概括

核心命令灵活且可重用,超出了挑战要求。其余的挑战要求在控制脚本中得到满足。我想很多人都忽略了这个概念。并非所有内容都必须放入函数中。控制脚本是确保一致性的好方法。您可以对这些脚本进行签名,将它们签入源代码管理,并以与处理 PowerShell 模块相同的方式管理它们。

如果您觉得需要提高 PowerShell 脚本编写技能,请考虑获取一本《PowerShell 脚本编写和工具制作书籍》。

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

取消回复欢迎 发表评论:

关灯