简体   繁体   English

编写 PowerShell Cmdlet 时如何处理路径?

[英]How do I deal with Paths when writing a PowerShell Cmdlet?

What is the proper way to receive a file as a parameter when writing a C# cmdlet?编写 C# cmdlet 时接收文件作为参数的正确方法是什么? So far I just have a property LiteralPath (aligning with their parameter naming convention) that is a string.到目前为止,我只有一个字符串属性 LiteralPath(与其参数命名约定一致)。 This is a problem because you just get whatever is typed into the console;这是一个问题,因为您只会获得输入到控制台中的任何内容; which could be the full path or could be a relative path.这可能是完整路径,也可能是相对路径。

Using Path.GetFullPath(string) doesn't work.使用 Path.GetFullPath(string) 不起作用。 It thinks I'm currently at ~, I'm not.它认为我目前在〜,我不是。 Same problem occurs if I change the property from a string to a FileInfo.如果我将属性从字符串更改为 FileInfo,则会出现同样的问题。

EDIT: For anyone interested, this workaround is working for me:编辑:对于任何感兴趣的人,这个解决方法对我有用:

    SessionState ss = new SessionState();
    Directory.SetCurrentDirectory(ss.Path.CurrentFileSystemLocation.Path);

    LiteralPath = Path.GetFullPath(LiteralPath);

LiteralPath is the string parameter. LiteralPath 是字符串参数。 I'm still interested in learning what is the recommended way to handle file paths that are passed as parameters.我仍然有兴趣了解处理作为参数传递的文件路径的推荐方法是什么。

EDIT2: This is better, so that you don't mess with the users current directory, you should set it back. EDIT2:这样更好,这样您就不会弄乱用户当前目录,您应该将其重新设置。

            string current = Directory.GetCurrentDirectory();
            Directory.SetCurrentDirectory(ss.Path.CurrentFileSystemLocation.Path);
            LiteralPath = Path.GetFullPath(LiteralPath);
            Directory.SetCurrentDirectory(current);

This is a surprisingly complex area, but I have a ton of experience here.这是一个令人惊讶的复杂领域,但我在这里拥有丰富的经验。 In short, there are some cmdlets that accept win32 paths straight from the System.IO APIs, and these typically use a -FilePath parameter.简而言之,有一些 cmdlet 直接从 System.IO API 接受 win32 路径,这些通常使用 -FilePath 参数。 If you want to write a well behaved "powershelly" cmdlet, you need -Path and -LiteralPath, to accept pipeline input and work with relative and absolute provider paths.如果您想编写一个表现良好的“powershelly” cmdlet,您需要 -Path 和 -LiteralPath,以接受管道输入并使用相对和绝对提供程序路径。 Here's an excerpt from a blog post I wrote a while ago:这是我前段时间写的一篇博文的摘录:

Paths in PowerShell are tough to understand [at first.] PowerShell Paths - or PSPaths , not to be confused with Win32 paths - in their absolute forms, they come in two distinct flavours: PowerShell 中的路径 [起初] 很难理解。PowerShell路径 - 或PSPaths ,不要与 Win32 路径混淆 - 在它们的绝对形式中,它们有两种不同的风格:

  • Provider-qualified: FileSystem::c:\\temp\\foo.txt提供者限定: FileSystem::c:\\temp\\foo.txt
  • PSDrive-qualified: c:\\temp\\foo.txt PSDrive 限定: c:\\temp\\foo.txt

It's very easy to get confused over provider-internal (The ProviderPath property of a resolved System.Management.Automation.PathInfo – the portion to the right of :: of the provider-qualified path above) and drive-qualified paths since they look the same if you look at the default FileSystem provider drives.很容易混淆 provider-internal(已解析的System.Management.Automation.PathInfoProviderPath属性——上面提供者限定路径的::右侧的部分)和驱动器限定路径,因为它们看起来像如果您查看默认的 FileSystem 提供程序驱动器,则相同。 That is to say, the PSDrive has the same name (C) as the native backing store, the windows filesystem (C).也就是说,PSDrive 与本机后备存储 Windows 文件系统 (C) 具有相同的名称 (C)。 So, to make it easier for yourself to understand the differences, create yourself a new PSDrive:因此,为了让自己更容易理解差异,请为自己创建一个新的 PSDrive:

ps c:\> new-psdrive temp filesystem c:\temp\
ps c:\> cd temp:
ps temp:\>

Now, let's look at this again:现在,让我们再看看这个:

  • Provider-qualified: FileSystem::c:\\temp\\foo.txt提供者限定: FileSystem::c:\\temp\\foo.txt
  • Drive-qualified: temp:\\foo.txt驱动器限定: temp:\\foo.txt

A bit easier this time to see what's different this time.这次稍微容易一点,看看这次有什么不同。 The bold text to the right of the provider name is the ProviderPath.提供者名称右侧的粗体文本是 ProviderPath。

So, your goals for writing a generalized provider-friendly Cmdlet (or advanced function) that accepts paths are:因此,编写接受路径的通用提供者友好 Cmdlet(或高级函数)的目标是:

  • Define a LiteralPath path parameter aliased to PSPath定义别名为PSPathLiteralPath路径参数
  • Define a Path parameter (which will resolve wildcards / glob)定义一个Path参数(它将解析通配符/glob)
  • Always assume you are receiving PSPaths, NOT native provider-paths (eg Win32 paths)始终假设您正在接收 PSPath,而不是本机提供程序路径(例如 Win32 路径)

Point number three is especially important.第三点尤为重要。 Also, obviously LiteralPath and Path should belong in mutually exclusive parameter sets.此外,显然LiteralPathPath应该属于互斥的参数集。

Relative Paths相对路径

A good question is: how do we deal with relative paths being passed to a Cmdlet.一个很好的问题是:我们如何处理传递给 Cmdlet 的相对路径。 As you should assume all paths being given to you are PSPaths, let's look at what the Cmdlet below does:由于您应该假设提供给您的所有路径都是 PSPath,让我们看看下面的 Cmdlet 做了什么:

ps temp:\> write-zip -literalpath foo.txt

The command should assume foo.txt is in the current drive, so this should be resolved immediately in the ProcessRecord or EndProcessing block like (using the scripting API here to demo):该命令应假定 foo.txt 在当前驱动器中,因此应立即在 ProcessRecord 或 EndProcessing 块中解决此问题(使用此处的脚本 API 进行演示):

$provider = $null;
$drive = $null
$pathHelper = $ExecutionContext.SessionState.Path
$providerPath = $pathHelper.GetUnresolvedProviderPathFromPSPath(
    "foo.txt", [ref]$provider, [ref]$drive)

Now you everything you need to recreate the two absolute forms of PSPaths, and you also have the native absolute ProviderPath.现在您拥有重新创建 PSPath 的两种绝对形式所需的一切,并且您还拥有本机绝对 ProviderPath。 To create a provider-qualified PSPath for foo.txt, use $provider.Name + “::” + $providerPath .要为 foo.txt 创建提供者限定的 PSPath,请使用$provider.Name + “::” + $providerPath If $drive is not $null (your current location might be provider-qualified in which case $drive will be $null ) then you should use $drive.name + ":\\" + $drive.CurrentLocation + "\\" + "foo.txt" to get a drive-qualified PSPath.如果$drive不是$null (您当前的位置可能是提供者限定的,在这种情况下$drive将是$null )那么您应该使用$drive.name + ":\\" + $drive.CurrentLocation + "\\" + "foo.txt"以获取驱动器限定的 PSPath。

Quickstart C# Skeleton快速入门 C# 骨架

Here's a skeleton of a C# provider-aware cmdlet to get you going.下面是一个 C# 提供者感知 cmdlet 的框架,可以让您继续前进。 It has built in checks to ensure it has been handed a FileSystem provider path.它内置了检查以确保它已获得文件系统提供程序路径。 I am in the process of packaging this up for NuGet to help others get writing well-behaved provider-aware Cmdlets:我正在为 NuGet 打包,以帮助其他人编写行为良好的提供者感知 Cmdlet:

using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using Microsoft.PowerShell.Commands;
namespace PSQuickStart
{
    [Cmdlet(VerbsCommon.Get, Noun,
        DefaultParameterSetName = ParamSetPath,
        SupportsShouldProcess = true)
    ]
    public class GetFileMetadataCommand : PSCmdlet
    {
        private const string Noun = "FileMetadata";
        private const string ParamSetLiteral = "Literal";
        private const string ParamSetPath = "Path";
        private string[] _paths;
        private bool _shouldExpandWildcards;
        [Parameter(
            Mandatory = true,
            ValueFromPipeline = false,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParamSetLiteral)
        ]
        [Alias("PSPath")]
        [ValidateNotNullOrEmpty]
        public string[] LiteralPath
        {
            get { return _paths; }
            set { _paths = value; }
        }
        [Parameter(
            Position = 0,
            Mandatory = true,
            ValueFromPipeline = true,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParamSetPath)
        ]
        [ValidateNotNullOrEmpty]
        public string[] Path
        {
            get { return _paths; }
            set
            {
                _shouldExpandWildcards = true;
                _paths = value;
            }
        }
        protected override void ProcessRecord()
        {
            foreach (string path in _paths)
            {
                // This will hold information about the provider containing
                // the items that this path string might resolve to.                
                ProviderInfo provider;
                // This will be used by the method that processes literal paths
                PSDriveInfo drive;
                // this contains the paths to process for this iteration of the
                // loop to resolve and optionally expand wildcards.
                List<string> filePaths = new List<string>();
                if (_shouldExpandWildcards)
                {
                    // Turn *.txt into foo.txt,foo2.txt etc.
                    // if path is just "foo.txt," it will return unchanged.
                    filePaths.AddRange(this.GetResolvedProviderPathFromPSPath(path, out provider));
                }
                else
                {
                    // no wildcards, so don't try to expand any * or ? symbols.                    
                    filePaths.Add(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
                        path, out provider, out drive));
                }
                // ensure that this path (or set of paths after wildcard expansion)
                // is on the filesystem. A wildcard can never expand to span multiple
                // providers.
                if (IsFileSystemPath(provider, path) == false)
                {
                    // no, so skip to next path in _paths.
                    continue;
                }
                // at this point, we have a list of paths on the filesystem.
                foreach (string filePath in filePaths)
                {
                    PSObject custom;
                    // If -whatif was supplied, do not perform the actions
                    // inside this "if" statement; only show the message.
                    //
                    // This block also supports the -confirm switch, where
                    // you will be asked if you want to perform the action
                    // "get metadata" on target: foo.txt
                    if (ShouldProcess(filePath, "Get Metadata"))
                    {
                        if (Directory.Exists(filePath))
                        {
                            custom = GetDirectoryCustomObject(new DirectoryInfo(filePath));
                        }
                        else
                        {
                            custom = GetFileCustomObject(new FileInfo(filePath));
                        }
                        WriteObject(custom);
                    }
                }
            }
        }
        private PSObject GetFileCustomObject(FileInfo file)
        {
            // this message will be shown if the -verbose switch is given
            WriteVerbose("GetFileCustomObject " + file);
            // create a custom object with a few properties
            PSObject custom = new PSObject();
            custom.Properties.Add(new PSNoteProperty("Size", file.Length));
            custom.Properties.Add(new PSNoteProperty("Name", file.Name));
            custom.Properties.Add(new PSNoteProperty("Extension", file.Extension));
            return custom;
        }
        private PSObject GetDirectoryCustomObject(DirectoryInfo dir)
        {
            // this message will be shown if the -verbose switch is given
            WriteVerbose("GetDirectoryCustomObject " + dir);
            // create a custom object with a few properties
            PSObject custom = new PSObject();
            int files = dir.GetFiles().Length;
            int subdirs = dir.GetDirectories().Length;
            custom.Properties.Add(new PSNoteProperty("Files", files));
            custom.Properties.Add(new PSNoteProperty("Subdirectories", subdirs));
            custom.Properties.Add(new PSNoteProperty("Name", dir.Name));
            return custom;
        }
        private bool IsFileSystemPath(ProviderInfo provider, string path)
        {
            bool isFileSystem = true;
            // check that this provider is the filesystem
            if (provider.ImplementingType != typeof(FileSystemProvider))
            {
                // create a .NET exception wrapping our error text
                ArgumentException ex = new ArgumentException(path +
                    " does not resolve to a path on the FileSystem provider.");
                // wrap this in a powershell errorrecord
                ErrorRecord error = new ErrorRecord(ex, "InvalidProvider",
                    ErrorCategory.InvalidArgument, path);
                // write a non-terminating error to pipeline
                this.WriteError(error);
                // tell our caller that the item was not on the filesystem
                isFileSystem = false;
            }
            return isFileSystem;
        }
    }
}

Cmdlet Development Guidelines (Microsoft) Cmdlet 开发指南 (Microsoft)

Here is some more generalized advice that should help you out in the long run: http://msdn.microsoft.com/en-us/library/ms714657%28VS.85%29.aspx以下是一些更广泛的建议,从长远来看应该可以帮助您: http : //msdn.microsoft.com/en-us/library/ms714657%28VS.85%29.aspx

This is how you can handle Path input in a PowerShell script cmdlet:这是在 PowerShell 脚本 cmdlet 中处理Path输入的方式:

function My-Cmdlet {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
    Param(
        # The path to the location of a file. You can also pipe a path to My-Cmdlet.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]] $Path
    )

    Begin {
        ...
    }

    Process {
        # ignore empty values
        # resolve the path
        # Convert it to remove provider path
        foreach($curPath in ($Path | Where-Object {$_} | Resolve-Path | Convert-Path)) {
            # test wether the input is a file
            if(Test-Path $curPath -PathType Leaf) {
                # now we have a valid path

                # confirm
                if ($PsCmdLet.ShouldProcess($curPath)) {
                    # for example
                    Write-Host $curPath
                }
            }
        }
    }

    End {
        ...
    }
}

You can invoke this method in the following ways:您可以通过以下方式调用此方法:

With a direct path:使用直接路径:

My-Cmdlet .

With a wildcard string:使用通配符字符串:

My-Cmdlet *.txt

With an actual file:使用实际文件:

My-Cmdlet .\PowerShell_transcript.20130714003415.txt

With a set of files in a variable:使用变量中的一组文件:

$x = Get-ChildItem *.txt
My-Cmdlet -Path $x

Or with the name only:或仅使用名称:

My-Cmdlet -Path $x.Name

Or by pasing the set of files via the pipeline:或者通过管道传递文件集:

$x | My-Cmdlet

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM