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

45

在编写C# cmdlet时,接收文件作为参数的正确方式是什么?到目前为止,我只有一个名为LiteralPath的属性(与其参数命名约定相对应),它是一个字符串。这是一个问题,因为你只会得到在控制台中键入的内容;这可能是完整路径,也可能是相对路径。

使用Path.GetFullPath(string)不起作用。它认为我当前在~,但实际上并不是。如果将属性从字符串更改为FileInfo,则会出现相同的问题。

编辑:对于任何感兴趣的人,这个解决方法对我有用:

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

    LiteralPath = Path.GetFullPath(LiteralPath);

LiteralPath是字符串参数。我仍然想了解处理作为参数传递的文件路径的推荐方式。

编辑2:为了不干扰用户的当前目录,最好将其设置回来。

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

3
看起来你正在寻找PSCmdlet.GetUnresolvedProviderPathFromPSPath 方法 - Roman Kuzmin
提示:您应始终从Cmdlet的ExecutionContext获取SessionState。 - x0n
这是我的救命之恩。致敬! - Simon
1个回答

73
这是一个非常复杂的领域,但我在这方面有很丰富的经验。简单来说,一些cmdlet直接使用System.IO API中的win32路径,并且这些通常使用-FilePath参数。如果您想编写符合规范的 "powershelly" cmdlet,则需要-Path和-LiteralPath来接受管道输入并处理相对和绝对提供程序路径。以下是我之前写的博客文章摘录:
PowerShell中的路径很难理解。 PowerShell路径或称为PSPaths(不要与Win32路径混淆)在其绝对形式中有两种完全不同的风格:
- 提供程序限定:FileSystem :: c:\temp\foo.txt - PSDrive限定:c:\temp\foo.txt
从默认的FileSystem提供程序驱动器查看,很容易混淆提供程序内部(解析的System.Management.Automation.PathInfo的ProviderPath属性-提供程序限定路径上::右侧的部分)和驱动器限定路径,因为它们看起来相同。也就是说,PSDrive的名称(C)与本地后备存储(Windows文件系统(C))相同。因此,为了更容易地理解差异,请创建新的PSDrive。
ps c:\> new-psdrive temp filesystem c:\temp\
ps c:\> cd temp:
ps temp:\>

现在,让我们再来看一下这个问题:
提供程序限定:FileSystem::c:\temp\foo.txt 驱动器限定:temp:\foo.txt 这次更容易看出不同之处了。提供程序名称右侧的粗体文本是ProviderPath。
因此,编写一个通用的提供程序友好的Cmdlet(或高级函数),接受路径的目标是:
定义别名为PSPathLiteralPath路径参数
定义Path参数(将解析通配符/ glob)
始终假设您收到的是PSPaths,而不是本机提供程序路径(例如Win32路径)
第三点尤其重要。此外,明显的LiteralPathPath应该属于互斥的参数集。
相对路径
一个很好的问题是:我们如何处理传递给Cmdlet的相对路径。由于您应该假设所有给定的路径都是PSPaths,让我们看一下下面的Cmdlet所做的事情:
ps temp:\> write-zip -literalpath foo.txt

该命令应假定foo.txt在当前驱动器中,因此应立即在ProcessRecord或EndProcessing块中解决此问题(使用脚本API进行演示):

该命令应该默认foo.txt在当前驱动器中,所以在ProcessRecord或EndProcessing块中应立即解决这个问题(这里使用脚本API进行演示):

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

现在你已经掌握了重新创建两个PSPaths的绝对形式所需的一切,同时也有了本机的绝对ProviderPath。要为foo.txt创建一个提供程序限定的PSPath,请使用$provider.Name + "::" + $providerPath。如果$drive不是$null(您当前的位置可能是提供程序限定的,在这种情况下$drive将为$null),则应使用$drive.name + ":\\" + $drive.CurrentLocation + "\\" + "foo.txt"来获取驱动器限定的PSPath。 C#快速入门骨架 以下是一个C#提供程序感知Cmdlet的骨架,它内置了检查以确保已传递FileSystem提供程序路径。我正在将其打包为NuGet,以帮助其他人编写行为良好的提供程序感知Cmdlets:
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开发指南(Microsoft)

以下是一些更为通用的建议,这些建议将有助于您长期发展: http://msdn.microsoft.com/en-us/library/ms714657%28VS.85%29.aspx


3
这就是我为什么使用StackOverflow。谢谢你。你有博客文章的链接吗?我会订阅的。 - Matthew
搜索http://www.nivot.org--最近由于工作原因我有点安静了,但是随着PowerShell v3的发布,我正在努力重新振作。 - x0n
Oisin,感谢你在这里的工作。这是最简单的吗?就API的可用性而言,在.NET中它是我见过的最糟糕的之一 - 我可能会忽略这个废话并使用字符串。 - Luke Puplett
@LukePuplett 对于某些情况,您可能可以使用字符串来解决问题,但提供程序框架是一个高级抽象,旨在支持多种类型的数据存储,而不仅仅是文件系统。这种复杂性是意料之外的,但却是必要的。我同意,乍一看它几乎和橡胶叉子一样不直观。 - x0n

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接