Search code examples
powershellautocompletepowershell-core

I have a function that returns a PSCustomObject with specific named properties. How can I get these named properties to appear in autocomplete?


Here is a simple function that splits a file path into its individual components and assigns them to a handful of properties:

function Get-FilePathComponents {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,Position=0,ValueFromPipeline)]
        [String[]]
        $Path
    )

    begin {}

    process {

        foreach ($P in $Path) {
            [PSCustomObject]@{
                ContainingFolder     = Split-Path $P -Parent
                FileBaseName         = Split-Path $P -LeafBase
                FileFullName         = Split-Path $P -Leaf
                FileExtension        = Split-Path $P -Extension
                FullPathNoExtension  = [IO.Path]::Combine((Split-Path $P -Parent),(Split-Path $P -LeafBase))
                CompletePath         = $P
                ParentFolder         = Split-Path (Split-Path $P -Parent) -Parent
            }
        }
    }
}

I then use the function like this:

$DestFile = "C:\Images\Wallpapers\SomeCoolWallpaper.jpg"
$Components = Get-FilePathComponents $DestFile

$Components.FileFullName     # Outputs SomeCoolWallpaper.jpg
$Components.ContainingFolder # Outputs C:\Images\Wallpapers

What I really need to accomplish is to somehow enable autocompletion for the returned object and its important properties.

This image below is exactly what I want:

Autocomplete

I was able to get it to work by defining a custom types.ps1xml file and populating it with <ScriptProperty> members that correspond to my parameters:

<Types>
<Type>
<Name>VSYSFileOps.Object.FilePathComponents</Name>

<ScriptProperty>
   <Name>ContainingFolder</Name>
   <GetScriptBlock>
      $this.ContainingFolder
   </GetScriptBlock>
</ScriptProperty>

<ScriptProperty>
   <Name>FileBaseName</Name>
   <GetScriptBlock>
      $this.FileBaseName
   </GetScriptBlock>
</ScriptProperty>

<ScriptProperty>
   <Name>FileFullName</Name>
   <GetScriptBlock>
      $this.FileFullName
   </GetScriptBlock>
</ScriptProperty>

... Etc...

And then adding [OutputType('VSYSFileOps.Object.FilePathComponents')] to the beginning of my function. Surprisingly, while this seems to work, I've been informed by some very knowledgeable people that this is a very bad idea and not what ps1xml files are used for.

I really want to get auto-completion working for this. How can I make this happen?

Any help at all would be extremely welcomed.

Edit: I am using VSCode with the official PowerShell extension and Powershell Pro Tools.

Edit 2 (Working Solution):


Many thanks to @AdminOfThings. His advice led me to a great solution.

Here's the final working code:

$FilePathComponents = @'
public class FilePathComponents
{
    public string ContainingFolder { get;set; }
    public string FileBaseName { get;set; }
    public string FileFullName { get;set; }
    public string FileExtension { get;set; }
    public string FullPathNoExtension { get;set; }
    public string CompletePath { get;set; }
    public string ParentFolder { get;set; }
}
'@

Add-Type -TypeDefinition $FilePathComponents -Language CSharp

function Get-FilePathComponents {

    [OutputType("FilePathComponents")]

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,Position=0,ValueFromPipeline)]
        [String[]]
        $Path
    )

    begin {}

    process {
        foreach ($P in $Path) {
            $obj = [FilePathComponents]::new()
            $obj.ContainingFolder     = Split-Path $P -Parent
            $obj.FileBaseName         = Split-Path $P -LeafBase
            $obj.FileFullName         = Split-Path $P -Leaf
            $obj.FileExtension        = Split-Path $P -Extension
            $obj.FullPathNoExtension  = [IO.Path]::Combine((Split-Path $P -Parent),(Split-Path $P -LeafBase))
            $obj.CompletePath         = $P
            $obj.ParentFolder         = Split-Path (Split-Path $P -Parent) -Parent
            $obj
        }
    }
}

The key was adding [OutputType("FilePathComponents")] to the beginning of the function definition. Now, when I use the function outside of the module, Intellisense and TAB completion work like a charm, even without strictly typing the variable name:

Autocompletion


Solution

  • If you want to go the custom class route, you can add a custom class as a type definition or by loading the class code in a script file/console:

    # class definition
    $class = @'
    public class FilePathComponents
    {
        public string ContainingFolder { get;set; }
        public string FileBaseName { get;set; }
        public string FileFullName { get;set; }
        public string FileExtension { get;set; }
        public string FullPathNoExtension { get;set; }
        public string CompletePath { get;set; }
        public string ParentFolder { get;set; }
    }
    '@
    # add class to PowerShell session
    Add-Type -TypeDefinition $class
    # define function
    function Get-FilePathComponents {
    
        [CmdletBinding()]
        param (
            [Parameter(Mandatory,Position=0,ValueFromPipeline)]
            [String[]]
            $Path
        )
    
        begin {}
    
        process {
    
            foreach ($P in $Path) {
                $obj = [FilePathComponents]::new()
                $obj.ContainingFolder     = Split-Path $P -Parent
                $obj.FileBaseName         = Split-Path $P -LeafBase
                $obj.FileFullName         = Split-Path $P -Leaf
                $obj.FileExtension        = Split-Path $P -Extension
                $obj.FullPathNoExtension  = [IO.Path]::Combine((Split-Path $P -Parent),(Split-Path $P -LeafBase))
                $obj.CompletePath         = $P
                $obj.ParentFolder         = Split-Path (Split-Path $P -Parent) -Parent
                $obj
            }
        }
    }
    
    # call your function
    $file = Get-FilePathComponents C:\temp\test1\a.csv
    # use $file. to auto-populate the properties