PowerShell / SHiPS

Simple Hierarchy in PowerShell - developing PowerShell provider got so much easier
MIT License
188 stars 32 forks source link

Support for additional APIs #66

Open mattmcnabb opened 6 years ago

mattmcnabb commented 6 years ago

Currently SHiPS only implements Get-ChildItem functionality. Please consider adding additional capabilities, particularly removing items.

stefanstranger commented 6 years ago

Already some more updates on now Additional APIs? I want to use remove-item and Get-ItemProperty in my VSTeam SHiPS provider.

jzabroski commented 6 years ago

@jianyunt I think we should split this out into smaller feature requests:

I believe this is the full set of possible PowerShell Provider functions.

What do you think? Is there a reason these all need to be tackled at once?

michaeltlombardi commented 6 years ago

Strong concur that tracking and adding support for these individually will lead to faster releases and more use for folks. Some of the above (New-Item, Remove-Item, and Copy-Item, for example) are much more valuable to a wider audience.

oising commented 6 years ago

Get-Content / Set-Content also.

jzabroski commented 6 years ago

I updated my post to include Get-Content and Set-Content. Good idea. Is there a master list somewhere of "cool commands" that proxy for the old PS Drives feature? I compiled mine through scouring the Internet.

jianyunt commented 6 years ago

@jzabroski we do not have a list. Your list listed above can be considered as master lit. Agreed we should create separate items so the community may have chance to grab a few too.

jzabroski commented 5 years ago

@jianyunt The full list is here: https://github.com/PowerShell/PowerShell/blob/1be3f4cc0e465ae11ad8e59e9060f5a59e4762eb/src/System.Management.Automation/utils/CoreProviderCmdlets.cs

rhysjtevans commented 5 years ago

Does anyone know if this is actively being worked on? Is it a case of adding in functions on the leaf and node classes, e.g. can it be done in the SHiPSBase so child classes inherit on the appropriate classes. Similar to https://github.com/PowerShell/SHiPS/blob/19b466bd0f000674e4ee787ad82d637b34360fd9/src/Microsoft.PowerShell.SHiPS/Node/SHiPSDirectory.cs#L35 ?

I'll have a play in the meantime and see how I get on.

SeeminglyScience commented 5 years ago

@rhysjtevans You'd need to add it there as well as add the actual implementation for it in SHiPSProvider.

For example, if you wanted to add support for *-Content you'd need to add (something like) these to SHiPSLeaf:

public virtual object[] GetContent() => null;

public virtual void SetContent(object[] value) { }

public virtual void ClearContent() { }

And also the provider side implementation that utilizes SHiPSLeaf:

public override IContentReader GetContentReader(string path)
{
    // Create a content reader that uses SHiPSLeaf.
}

public override IContentWriter GetContentWriter(string path)
{
    // Create a content writer that uses SHiPSLeaf.
}

public override void ClearContent(string path)
{
    // Call SHiPSLeaf.ClearContent
}

Also dynamic params.

rhysjtevans commented 5 years ago

awesome, Nice one @SeeminglyScience. That helps. I'm working on a couple of other projects so may not get round to this for a while yet.

oising commented 5 years ago

The approach I took when I wrote psproviderframework was to add two new cmdlets to encapsulate the idea of a content reader and a content writer, making it easier to abstract for differing backing stores - different nodes in the tree may require a different strategy for read/write. Each cmdlet took three scriptblocks as parameters representing read, seek and close operations respectively, just like a regular stream. Then, in the script-based provider definition, you provide functions for GetContentReader and GetContentWriter and construct readers and writers to return to the framework. Perhaps someone can take these ideas and work them into SHiPS. I wrote this about ten years ago, so powershell classes weren't around at the time, so it's all scriptblock based and relies on closures to capture state in the defining module.

Here's an example definition script of a simple provider that allows get-content and set-content against its nodes:

Import-Module c:\projects\PowerShell\PSProvider\PSProviderFramework\bin\Debug\PSProviderFramework.dll

if (Get-PSDrive data -ea 0) { remove-psdrive data }

# create new module representing our provider definition and pass to our hosting provider
New-PSDrive data ContainerScriptProvider -Root / -ModuleInfo $(New-Module -Name test -Args @{ 

        # our backing store
        a = "this is node a."
        b = "this is node b."
        c = "this is node c."
        d = "this is node d."

    } -ScriptBlock {

        param($data)

        function GetContentReader($path) {
            $psprovider.writeverbose("getcontentreader '$path'")

            # initialize for read operation
            $item = $data[$path]
            $content = [char[]]$item
            $position = 0

            # should close around our locals, esp. $position
            # to support concurrent content readers. 
            & {
                # create a new content reader and return it
                New-ContentReader -OnRead {
                    param([long]$count)

                    # this implementation returns a string where $count represents number of char to return
                    # at a time; you may choose to return whatever you like, and treat $count in any way you feel
                    # is appropriate. All that matters is that you return an array. Return an empty array to signify
                    # the end of the stream.

                    # yes, i could use stringbuilder here but i figure the algorithm is more general purpose for a sample
                    # as this could be easily adopted for byte arrays.
                    $remaining = $content.length - $position

                    if ($remaining -gt 0) {

                        if ($count -gt $remaining) {
                            $len = $remaining
                        }
                        else {
                            $len = $count
                        }
                        $output = new-object char[] $len

                        [array]::Copy($content, $position, $output, 0, $len)
                        $position += $len

                        @($output -join "")

                    }
                    else {

                        # end stream, return empty array
                        write-verbose "read: EOF" -verbose
                        @()
                    }

                } -OnSeek {                
                    param([long]$offset, [io.seekorigin]$origin)
                    write-verbose "seek: $offset origin: $origin" -verbose

                } -OnClose {
                    # perform any cleanup you like here.
                    write-verbose "read: close!" -verbose
                }
            }.getnewclosure() # capture state from module
        }

        function GetContentWriter($path) {
            $psprovider.writeverbose("getcontentwriter '$path'")

            # initialize for write operation
            $item = $data[$path]
            $position = 0

            & {
                New-ContentWriter -OnWrite {
                    param([collections.ilist]$content)

                    write-verbose "write: $($content.length) element(s)." -verbose

                    $content

                } -OnSeek {
                    # seek must be implemented to support -Append, Add-Content etc
                    param([long]$offset, [io.seekorigin]$origin)
                    write-verbose "seek: $offset origin: $origin" -verbose

                    switch ($origin) {
                        "end" {
                            $position = $item.length + $offset
                            write-verbose "seek: new position at $position" -verbose
                        }
                        default {
                            write-warning "unsupported seek."
                        }
                    }

                } -OnClose {
                    # perform any cleanup you like here.
                    write-verbose "write: close!" -verbose            
                }
            }.getnewclosure() # capture state from module
        }

        function GetItem($path) {
            $psprovider.writeverbose("getitem '$path'")

            if ($path) {

                if ($data[$path]) {
                    $psprovider.writeitemobject($data[$path], $path, $false)
                }

            }
            else {

                # root
                $psprovider.writeitemobject($data.values, "/", $true)
            }
        }

        function ItemExists($path) {

            if ($path) {

                $psprovider.writeverbose("item exists $path")
                $data.containskey($path)

            }
            else {

                # root always exists
                $true
            }
        }

        function GetChildNames($path, $returnContainers) {

            $psprovider.writeverbose("getchildnames '$path' $returnContainers")

            if ($path) {
                if ($data[$path]) {
                    $psprovider.writeitemobject($path, $path, $false)
                }
            }
            else {
                $data.keys | % { $psprovider.writeitemobject($_, [string]$_, $false) }
            }
        }

        function GetChildItems($path, $recurse) {

            $psprovider.writeverbose("getchildnames '$path' $returnContainers")

            if ($path) {
                $psprovider.writeitemobject($data[$path], $_, $false)
            }
            else {
                $data.keys | % {
                    $psprovider.writeitemobject($data[$_], $_, $false)
                }
            }
        }

        function ClearItem($path) {
            $psprovider.writeverbose("clearitem '$path'")
        }
    })

get-content data:\a -verbose -ReadCount 8

See: https://github.com/oising/psprovider/blob/master/Trunk/PSProviderFramework/Scripts/harness.ps1

oising commented 5 years ago

I even got as far as providing transaction support ;)

namespace PSProviderFramework
{
    [CmdletProvider("TransactedTreeScriptProvider", ProviderCapabilities.ShouldProcess | ProviderCapabilities.Transactions)]
    public class TransactedTreeScriptProvider : TreeScriptProvider {
        protected override TReturn InvokeFunction<TReturn>(string function, params object[] parameters) {
            TReturn returnValue;

            // push correct provider thread context for this call
            using (PSProviderContext<TransactedTreeScriptProvider>.Enter(this)) {
                returnValue = PSProviderContext<TransactedTreeScriptProvider>
                    .InvokeFunctionInternal<TReturn>(function, parameters);
            } // pop context

            return returnValue;
        }

        protected override void InvokeFunction(string function, params object[] parameters) {
            // push correct provider thread context for this call
            using (PSProviderContext<TransactedTreeScriptProvider>.Enter(this)) {
                PSProviderContext<TransactedTreeScriptProvider>
                    .InvokeFunctionInternal<object>(function, parameters);
            } // pop context
        }
    }

    [CmdletProvider("TreeScriptProvider", ProviderCapabilities.ShouldProcess)]
    public class TreeScriptProvider : NavigationCmdletProvider, IScriptProvider, IContentCmdletProvider, IPropertyCmdletProvider
    {
    // ...

... as well as properties read/write, container, tree etc. In fact, I think I had covered every aspect of providers. And they haven't changed since then.

jzabroski commented 5 years ago

@oising Thanks for sharing your knowledge, wisdom, and design tips!