Как создать функцию, которая принимает несколько типов аргументов из конвейера и командной строки?

Я пытаюсь написать функцию, которая принимает несколько аргументов, которые могут поступать либо из командной строки, либо из конвейера. Аргументами могут быть строки или объекты каталога. Идея состоит в том, что любая из следующих вызовов должна работать:

Test-VEnv '.\MyPath', '.\AnotherPath'
Test-VEnv (dir)
'MyPath', 'AnotherPath' | Test-VEnv
dir | Test-VEnv

Следующий код почти работает:

function Test-VEnv {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$true)]
        [Alias('FullName')]
        [String[]]$Path
    )

    process {
        foreach ($P in $Path) {
            ...
        }
    }
}

Он обрабатывает строки как из конвейера, так и из аргумента команды и обрабатывает объекты каталога из конвейера (через ValueFromPipelineByPropertyName и псевдоним FullName). Но он не обрабатывает объекты каталога в командной строке, поэтому

dir | Where-Object { Test-VEnv $_ }

терпит неудачу, поскольку он преобразует объекты каталога в строки, который использует свойство Name, а не FullName, и последующий код не работает.

Может ли кто-нибудь сказать мне, как добиться того, чего я хочу?

Я знаю, что даже если я смогу заставить это работать, это может быть не очень хороший дизайн. Но насколько я могу судить, как работает встроенный тестовый путь, поэтому я хочу попробовать следовать стандартным поведением, прежде чем придумывать свои собственные...

Ответ 1

Так как ваш тип параметра string, он принуждает объект info файловой системы к строке, когда вы не используете конвейер { Test-VEnv $_ }. Если вы вызовете метод ToString() объекта System.IO.FileInfo или System.IO.DirectoryInfo, вы увидите это. Когда вы используете конвейер, он связывает псевдоним fullname, предоставляя вам полный путь.

Вы можете видеть, что делает PowerShell для привязки входного объекта с помощью Trace-Command. Вот пример того, как его использовать:

trace-command -name parameterbinding -expression {(dir C:\)[0] | ? {Test-VEnv $_}} -pshost

Вот важная часть вывода:

BIND arg [PerfLogs] to parameter [Path]
    Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
        result returned from DATA GENERATION: System.String[]
    COERCE arg to [System.String[]]
        Parameter and arg types the same, no coercion is needed.
    BIND arg [System.String[]] to param [Path] SUCCESSFUL

Test-Path делает то же самое. Взгляните на эти три примера:

PS C:\Users\Andy> Test-Path (dir C:\)[0]
False
PS C:\Users\Andy> (dir C:\)[0] | Test-Path
True
PS C:\> Test-Path (dir C:\)[0]
True
  • Поскольку мой PWD не является C:\, я получаю FALSE, потому что объект DirectoryInfo преобразуется в строку (ToString()), которая дает имя папки. Это связано с тем, что конвейер не использовался.

  • Поскольку используется конвейер, он работает, потому что он привязан к PsPath с этим параметром:

    [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string[]]
    ${LiteralPath},
    
  • Поскольку каталог содержит папку, имя папки существует.

Вы можете попробовать псевдоним PsPath для вашей привязки. Это то, что использует Test-Path:

param (
    [Parameter(Mandatory=$true, Position=0,
        ValueFromPipeline=$True,
        ValueFromPipelineByPropertyName=$true)]
    [Alias('PsPath')]
    [String[]] $Path
)

process {
    foreach ($P in $Path) {
        Get-Item $p
    }
}

Некоторые тесты:

Set-Location C:\
Write-Host 1
    Test-VEnv '.\Windows', '.\Program Files'
Write-Host 2
    Test-VEnv (dir)
Write-Host 3
    'Windows', 'Program Files' | Test-VEnv
Write-Host 4
    dir | Test-VEnv

Вывод:

1
    Directory: C:\
Mode                LastWriteTime     Length Name                                                       
----                -------------     ------ ----                                                       
d----         3/14/2012   3:41 AM            Windows                                                    
d-r--         3/24/2012   7:46 PM            Program Files                                              

2
d----         2/18/2012   4:32 AM            PerfLogs                                                   
d-r--         3/24/2012   7:46 PM            Program Files                                              
d-r--         3/25/2012   4:49 PM            Program Files (x86)                                        
d----          3/9/2012   9:57 PM            Python27                                                   
d-r--          3/4/2012   8:11 PM            Users                                                      
d----         3/14/2012   3:41 AM            Windows                                                    
-a---          3/4/2012   8:45 PM       1024 .rnd                                                       

3
d----         3/14/2012   3:41 AM            Windows                                                    
d-r--         3/24/2012   7:46 PM            Program Files                                              

4
d----         2/18/2012   4:32 AM            PerfLogs                                                   
d-r--         3/24/2012   7:46 PM            Program Files                                              
d-r--         3/25/2012   4:49 PM            Program Files (x86)                                        
d----          3/9/2012   9:57 PM            Python27                                                   
d-r--          3/4/2012   8:11 PM            Users                                                      
d----         3/14/2012   3:41 AM            Windows                                                    
-a---          3/4/2012   8:45 PM       1024 .rnd  

Ответ 2

@Энди дает отличную информацию, конкретно касающуюся точек в вашем вопросе. Мой ответ здесь скорее дополняет рассмотрение более широких последствий. Вероятно, он заслуживает того, чтобы быть комментарием, но длина и мое включенное изображение помешают мне опубликовать это как просто комментарий...

Недавно я рассмотрел вопрос о конвейере и непосредственном вводе в Powershell с конкретной целью сделать эти входные потоки симметричными по отношению ко всем классам входных данных и по отношению к тем, какие применяются по умолчанию. По моим подсчетам, шесть классов эквивалентности ввода для рассмотрения:

  • нет ввода
  • NULL
  • пустой
  • скалярная
  • список нормальных значений
  • список смешанных значений (т.е. некоторый пустой или пустой)

То, что обычно ожидалось бы, когда каждый из этих входов отправляется функции, будет таким соответствующим списком:

  • значение по умолчанию
  • NULL
  • пустой
  • скалярная
  • список нормальных значений
  • список смешанных значений (т.е. некоторый пустой или пустой)

То есть без ввода данных используется значение по умолчанию; в противном случае используется данное значение. Это звучит почти тривиально, практически тавтология, но есть некоторые тонкости. Рассмотрим, например, то, что означает отсутствие ввода по трубопроводу? Это пустая или пустая коллекция? Я утверждаю, что последний, среди других причин, позволяет использовать симметрию между потоками, упомянутыми выше. Кроме того, то, как вы пишете как свою сигнатуру, так и тело своей функции, иногда вызывает неожиданное воздействие на некоторые или все эти входные классы с одним или другим входным потоком. Таким образом, я далее утверждаю, что есть намного больше этого "тривиального" рассмотрения, чем кажется на первый взгляд. Настолько, что я много писал об этом в статье Вниз Rabbit Hole - исследование в PowerShell Pipelines, Functions и Parameters, опубликован на сайте Simple-Talk.com. В комплекте с этой статьей находится настенная диаграмма, в которой показана таблица из шести классов ввода эквивалентности и то, что вы получаете для каждого с различными шаблонами функций. Вот миниатюра настенной диаграммы:

enter image description here

Ответ 3

Работает ли он, если вы измените тип $path из String [] на [System.IO.DirectoryInfo []]?

Ответ 4

function Install-PathTransformation 
{
    [CmdletBinding()]
    param()

    if (-not $script:my_pathtransformation_types) {
      $script:my_pathtransformation_types = Add-Type -TypeDefinition @"
        using System;
        using System.IO;
        using System.Management.Automation;

        public class ValidPathTransformationAttribute : ArgumentTransformationAttribute {
            public bool Resolve {
                get;
                set;
            }

            public override Object Transform(EngineIntrinsics engineIntrinsics, Object inputObject) {
                PSObject psobj = inputObject as PSObject;
                if (psobj != null)
                    inputObject = psobj.BaseObject;
                if (inputObject == null)
                    return inputObject;

                FileSystemInfo test1 = inputObject as FileSystemInfo;
                if (test1 != null)
                    return test1.FullName; // no need for further checks, path shoul de qualified!

                PathInfo test2 = inputObject as PathInfo;
                if (test2 != null)
                    return test2.Path;     // no need for further checks, path shoul de qualified!

                string test3 = inputObject as string;
                if (test3 == null)
                    test3 = (string)LanguagePrimitives.ConvertTo(inputObject, typeof(string));
                if (Resolve)
                    test3 = engineIntrinsics.SessionState.Path.GetUnresolvedProviderPathFromPSPath(test3);
                else if (!engineIntrinsics.SessionState.Path.IsValid(test3))
                    throw new ArgumentTransformationMetadataException("Invalid path value: " + test3);
                return test3;
            }
        }
"@
    }
    return $script:my_pathtransformation_types
}


Install-PathTransformation

function A(
    [parameter(Mandatory=$false, ValueFromPipeline=$true)]
    [ValidPathTransformation(Resolve=$true)]
    [string] # optional, transformation returns always string
    $z) { 
  Process {
    Write-Host $("{0}: {1}" -f $z.GetType().FullName, $z)
  }
}

& {
    'mumu', 10, 10.5, ""
    dir $env:Temp | select -First 5
} | A

Как это работает:
1) Создайте атрибут преобразования для обработки значения параметра.
2) Во время преобразования, если Value является FileSystemInfo или PathInfo, мы принимаем значение внутри, если не преобразуем значение в строку и убедитесь, что "путь" действителен (и, если необходимо, разрешить путь).
3) При применении результат Transformation всегда является строкой.