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

发布于 2024-12-21 10:18:30 字数 828 浏览 1 评论 0 原文

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

使用 Path.GetFullPath(string) 不起作用。它认为我目前处于〜,但我不是。如果我将属性从字符串更改为 FileInfo,也会出现同样的问题。

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

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

    LiteralPath = Path.GetFullPath(LiteralPath);

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

EDIT2:这更好,这样你就不会弄乱用户当前目录,你应该将其设置回来。

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

What is the proper way to receive a file as a parameter when writing a C# cmdlet? So far I just have a property LiteralPath (aligning with their parameter naming convention) that is a string. 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. It thinks I'm currently at ~, I'm not. Same problem occurs if I change the property from a string to a 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. 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.

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

浅暮の光 2024-12-28 10:18:30

这是一个令人惊讶的复杂领域,但我在这里有丰富的经验。简而言之,有一些 cmdlet 直接从 System.IO API 接受 win32 路径,并且这些 cmdlet 通常使用 -FilePath 参数。如果您想编写一个行为良好的“powershelly”cmdlet,则需要 -Path 和 -LiteralPath,以接受管道输入并使用相对和绝对提供程序路径。以下是我不久前写的博客文章的摘录:

PowerShell 中的路径最初很难理解。PowerShell 路径 - 或 PSPaths,不要与 Win32 路径混淆 - 以其绝对形式,它们有两种不同的风格:

  • 提供者限定:FileSystem::c:\temp\foo.txt
  • 限定:c:\temp\foo.txt

PSDrive 很容易对提供程序内部感到困惑(已解析的 System.Management.Automation.PathInfoProviderPath 属性 - ::< 右侧的部分/code> 上面的提供程序限定路径)和驱动器限定路径,因为如果您查看默认的文件系统提供程序驱动器,它们看起来是相同的。也就是说,PSDrive 与本机后备存储 Windows 文件系统 (C) 具有相同的名称 (C)。因此,为了让您自己更容易理解差异,请为自己创建一个新的 PSDrive:

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

现在,让我们再看一下:

  • Provider-qualified: FileSystem::c:\temp\foo.txt
  • Drive -qualified: temp:\foo.txt

这次更容易看出这次有什么不同。提供程序名称右侧的粗体文本是提供程序路径。

因此,编写接受路径的通用提供者友好型 Cmdlet(或高级函数)的目标是:

  • 定义别名为 PSPathLiteralPath 路径参数
  • 定义一个 Path 参数(将解析通配符/全局变量)
  • 始终假设您正在接收 PSPath,而不是本机提供程序路径(例如 Win32 路径)

第三点尤其重要。此外,显然 LiteralPathPath 应该属于互斥的参数集。

相对路径

一个很好的问题是:我们如何处理传递给 Cmdlet 的相对路径。由于您应该假设提供给您的所有路径都是 PSPaths,所以让我们看看下面的 Cmdlet 的作用:

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

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

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

现在您已拥有重新创建 PSPath 的两种绝对形式所需的一切,并且您还拥有本机绝对 ProviderPath。要为 foo.txt 创建提供者限定的 PSPath,请使用 $provider.Name + “::” + $providerPath。如果 $drive 不是 $null(您当前的位置可能是提供商限定的,在这种情况下 $drive 将为 $null< /code>) 那么您应该使用 $drive.name + ":\" + $drive.CurrentLocation + "\" + "foo.txt" 来获取驱动器限定的 PSPath。

快速入门 C# 骨架

下面是一个 C# 提供程序感知 cmdlet 的骨架,可帮助您入门。它内置了检查,以确保它已获得文件系统提供程序路径。我正在将其打包为 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 开发指南 (Microsoft)

以下是一些更通用的建议,可以帮助您解决以下问题:长远来看:
https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/strongly-encouraging-development-guidelines?view=powershell-7.4#support-windows-powershell-paths

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. 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. 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:

  • Provider-qualified: FileSystem::c:\temp\foo.txt
  • PSDrive-qualified: 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. That is to say, the PSDrive has the same name (C) as the native backing store, the windows filesystem (C). So, to make it easier for yourself to understand the differences, create yourself a new 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
  • Drive-qualified: 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.

So, your goals for writing a generalized provider-friendly Cmdlet (or advanced function) that accepts paths are:

  • Define a LiteralPath path parameter aliased to PSPath
  • Define a Path parameter (which will resolve wildcards / glob)
  • Always assume you are receiving PSPaths, NOT native provider-paths (e.g. Win32 paths)

Point number three is especially important. Also, obviously LiteralPath and Path should belong in mutually exclusive parameter sets.

Relative Paths

A good question is: how do we deal with relative paths being passed to a Cmdlet. As you should assume all paths being given to you are PSPaths, let’s look at what the Cmdlet below does:

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):

$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. To create a provider-qualified PSPath for foo.txt, use $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.

Quickstart C# Skeleton

Here's a skeleton of a C# provider-aware cmdlet to get you going. 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:

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)

Here is some more generalized advice that should help you out in the long run:
https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/strongly-encouraged-development-guidelines?view=powershell-7.4#support-windows-powershell-paths

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文