none
Tab-completion of command based upon a folderlist? RRS feed

  • Question

  • Hi,

    Please see this post:

    https://social.technet.microsoft.com/Forums/en-US/a42527e9-7a0f-42e2-8536-a2a0ca16136a/generate-
    folderlist-of-sublevels?forum=ITCG

    $form_Load = {

         $root = '\\share\packages\'
         $f = Get-ChildItem $root -Directory | Sort-Object
         $combobox1.DataSource = [system.collections.arraylist]@($f)
         $combobox1.DisplayMember = 'Basename'
         $combobox1.SelectedIndex=10
     }

     $combobox1_SelectedIndexChanged = {
         if ($f = Get-ChildItem $combobox1.SelectedItem.FullName -Directory) {
             $combobox2.DataSource = [collections.arraylist]@($f)
             $combobox2.DisplayMember = 'Basename'
         } else {
             $combobox2.DataSource=$null   
         }
     }


    Can this command also achieved by commandline? So what I would like to do, is have a command

    Deploy-AppVPackage -Publisher Microsoft -Name Office -Version 2010
    in which Microsoft, Office and 2010 can be filled in via tab-completion.

    So tab completion of -Publisher (in my example Microsoft) should come from:
    $f = Get-ChildItem $root -Directory | Sort-Object

    tab comletion of -Name (in my example Office) should come from what is selected in -Publisher (in my example Microsoft) = folders underneath Microsoft etc

    Folderstructure share =

    *Microsoft
    **Office
    ***2010
    ***VisualBasic
    *Mindjet
    **MindManager
    ***11.0

    Please advise.
    J.


    Jan Hoedt

    Friday, September 25, 2015 11:27 AM

Answers

  • Most of what you see with the module's required files are extra completers for other commands. I'd just copy the folder from ~\Documents\WindowsPowerShell\Modules\TabExpansion++ and use that to deploy the module. It's up to you whether or not you want to do a machine install of the module for the other computers, or if you want each user to install the module for themselves...

    About your existing functions: try my example code above without providing one of the parameters I've marked as 'Mandatory'. You should be prompted for that value (without having to use 'Read-Host'). That's because the 'advanced' function uses the [Parameter()] attribute. For more info about what makes a function 'advanced' and what the [Parameter()] attribute can do, check out the help topics for 'about_Functions_Advanced' and 'about_Functions_Advanced_Parameters'...

    To change the order of the completions, add a call to Sort-Object in the $Completer scriptblock:
    if (Test-Path $FolderPath -PathType Container) {
        Get-ChildItem $FolderPath -Directory | Sort-Object Name | ForEach-Object {
            New-CompletionResult $_.Name $_.FullName
        }
    }
    

    • Marked as answer by janhoedt Tuesday, September 29, 2015 12:52 PM
    Tuesday, September 29, 2015 12:01 PM
  • I've got a better way to do this, and it's not flaky anymore :)

    If you have PowerShell version 3 or higher, go get the TabExpansion module (download the ZIP file, unblock it, extract it, then run 'install.ps1'). The code below can be modified slightly to not use this module, but it complicates things if you have another command that needs to do something like this. The module was created by the same Microsoft employee that created the PSReadLine module, which is now included with PowerShell v5.

    Anyway, if you can get the module, try this:

    function Deploy-AppVPackage {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, Position=0)]
            [string] $Publisher,
            [Parameter(Mandatory, Position=1)]
            [string] $Name,
            [Parameter(Mandatory, Position=2)]
            [string] $Version,
            [string] $PackageFolder = "\\share\packages"
        )
    
        $FolderPath = $PackageFolder
        foreach ($Node in echo Publisher, Name, Version) {
            $FolderPath = "{0}\{1}" -f $FolderPath, $PSBoundParameters[$Node]
            if (-not (Test-Path $FolderPath -PathType Container)) {
                throw ("Invalid {0}: {1}" -f $Node, $PSBoundParameters[$Node])
            }
        }
    
        $FolderPath
    }
    
    if (Get-Module TabExpansion++ -ListAvailable) {
    
        $Completer = {
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
            $FolderPath = $fakeBoundParameter['PackageFolder']
            if ($FolderPath -eq $null) {
                # Look at the command's definition to find the default folder
                $FolderPath = (Get-Command $commandName -ErrorAction SilentlyContinue).ScriptBlock.Ast.Find(
                    { $args[0] -is [System.Management.Automation.Language.ParameterAst] -and $args[0].Name.VariablePath.UserPath -eq "PackageFolder" },
                    $true
                ) | select -ExpandProperty DefaultValue | select -ExpandProperty Value
            }
    
            $SubFolders = @(switch ($parameterName) {
                { $_ -in (echo Name, Version) } {
                    $fakeBoundParameter['Publisher']
                }
    
                Version {
                    $fakeBoundParameter['Name']
                }
            })
    
            if (($SubFolders -eq $null).Count -gt 0) {
                # This means one of the $fakeBoundParameters didn't exist, so assume that
                # parameter wasn't specified
                return
            }
    
            $FolderPath = "$FolderPath\$($SubFolders -join '\')"
            if (Test-Path -LiteralPath $FolderPath -PathType Container) {
                Get-ChildItem -LiteralPath $FolderPath -Directory | ForEach-Object {
                    New-CompletionResult $_.Name $_.FullName
                }
            }
        }
    
        echo Publisher, Name, Version | ForEach-Object { Register-ArgumentCompleter -CommandName Deploy-AppVPackage -ParameterName $_ -ScriptBlock $Completer }
    }
    else {
        Write-Warning "Unable to find TabExpansion++, so argument completer won't work"
    }
    
    That makes the actual function so much simpler. The argument completer stuff is the most complicated part, but I think it's easier to follow than doing this with dynamic parameters. If you want me to explain about anything going on, just let me know.

    • Marked as answer by janhoedt Tuesday, September 29, 2015 8:17 AM
    Monday, September 28, 2015 11:49 PM
  • Thanks! The only thing that's left for me now is to figure out where I have to put my code.
    What I do not get is why you have the "if (Get-Module TabExpansion++ -ListAvailable" not included in the function. So do I need to include my functions 

    New-AppVApplicationUsers

    New-AppVApplicationComputers

    in the fucntion or in the "if (Get-Module TabExpansion++ -ListAvailable" part(?)



    Jan Hoedt

    The 'if (Get-Module...)' part is optional. That's what registers the 'Argument Completers' that the TabExpansion++ module uses to know that we want it to help with the -Publisher, -Name, and -Version parameters. To do that, we require a command (Register-ArgumentCompleter) that only exists if you have the module. So, the 'if' statement checks to see if the module is installed on the computer. If it is, it runs the Register-ArgumentCompleter command (3 times in this case). If it's not, it displays a warning (you could change that to tell your users that they need to get the module if they want the function to have tab completion).

    If the module isn't available, though, the function will still work if you know the folder structure (you just don't get the convenient tab completion)

    If you include other function definitions inside of the Deploy-AppVPacakge function, you will only be able to use them inside that function. If these are commands that you want to use by themselves (like from the command line, or from other functions), you need to define them 'beside' the Deploy-AppVPackage function. Assuming you have multiple functions you want defined, you might structure everything like this:
    function Deploy-AppVPackage {
    <#
        CODE GOES HERE, including calls to other functions like 'New-AppVApplicationUsers'
    #> 
    }
    
    function New-AppVApplicationUsers {
    <#
        CODE GOES HERE
    #> 
    }
    
    function New-AppVApplicationComputers {
    <#
        CODE GOES HERE
    #> 
    }
    
    if (Get-Module TabExpansion++ -ListAvailable) {
        # This part is optional, and just helps make the functions
        # easier to use.
    
        <REGISTER COMPLETERS HERE>
    
    }
    
    If you're going to deploy these functions to other users, you might consider saving them inside of a custom module. For more info, see the 'about_Modules' help topic.


    • Marked as answer by janhoedt Wednesday, September 30, 2015 1:18 PM
    Tuesday, September 29, 2015 4:12 PM
  • http://blogs.technet.com/b/pstips/archive/2014/06/10/dynamic-validateset-in-a-dynamic-parameter.aspx

    Jan Hoedt

    • Marked as answer by janhoedt Saturday, September 26, 2015 7:51 AM
    Saturday, September 26, 2015 7:51 AM

All replies

  • Are you asking how to use tab completion in a custom function?   Any parameter defined as an enumeration or a validation list ill support ab completion.

    Search for examples of "ValidateSet"


    \_(ツ)_/

    Friday, September 25, 2015 4:45 PM
  • Thanks!

    Ok, looked that up and put things together.
    To claify, this is more or less what I 'm looking for. That's the right direction, correct?


    Function New-AppVApplication
    {}
    Function New-AppVCollections
    {}
    
    Function Deploy-AppVPackage
    { 
         #Get folders in sharename with sources, f.e. Microsoft
         $root = '\\share\packages\'
         $f = Get-ChildItem $root -Directory | Sort-Object
    
        param
         
         # validation should occur to $f with tab-completion, if no value given, it should ask for a value
        ([ValidateSet $f]
         [string]$Publisher = (Read-Host "Please enter Publisher:"),
         [string]$AppName = (Read-Host "Please enter AppName:"),
         $AppVersion = (Read-Host "Please enter AppVersion:"),
         $PackageVersion = (Read-Host "Please enter PackageVersion:")
        )
        LoadVariables
        New-AppVApplication
        New-AppVCollections
        ...
    }



    Jan Hoedt

    Friday, September 25, 2015 5:55 PM
  • Please look at how to use validation and dynamic parameters.  We don't have enough room on this thread to teach you how to use PowerShell.  I recommend getting one of the many new and excellent books too learn how to use PowerShell.  Asking 20 questions a day will not help you learn PowerShell.


    \_(ツ)_/

    Friday, September 25, 2015 6:03 PM
  • http://blogs.technet.com/b/pstips/archive/2014/06/10/dynamic-validateset-in-a-dynamic-parameter.aspx

    Jan Hoedt

    • Marked as answer by janhoedt Saturday, September 26, 2015 7:51 AM
    Saturday, September 26, 2015 7:51 AM
  • Sorry but I do not see your solution.  Dynamic parameters are useful but not for what you asked.

    \_(ツ)_/

    Saturday, September 26, 2015 9:34 AM
  • They give tab-completion for a directory and that's what I need.

    Jan Hoedt

    Saturday, September 26, 2015 9:35 AM
  • Haven't it figured out fully but since you keep referring to help files and "read books" so the post is as close to a solution as I'm getting now.

    Jan Hoedt

    Saturday, September 26, 2015 9:38 AM
  • The goal of asking questions is to learn.  You can't play the violin from day one. You watch and learn from others.

    This is what I needed:

     function Get-FolderNames {
        [CmdletBinding()]
        Param( $Path
            # Any other parameters can go here
        )
     
        DynamicParam {
                # Set the dynamic parameters' name
                $ParameterName = 'Folder'
                
                # Create the dictionary 
                $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    
                # Create the collection of attributes
                $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                
                # Create and set the parameters' attributes
                $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
                $ParameterAttribute.Mandatory = $true
                $ParameterAttribute.Position = 1
    
                # Add the attributes to the attributes collection
                $AttributeCollection.Add($ParameterAttribute)
    
                # Generate and set the ValidateSet 
                $arrSet = Get-ChildItem -Path $Path -Directory | Select-Object Name -ExpandProperty Name
                $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
                write-host "ValidateSetAttribute $ValidateSetAttribute"
    
                # Add the ValidateSet to the attributes collection
                $AttributeCollection.Add($ValidateSetAttribute)
    
                # Create and return the dynamic parameter
                $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
                $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
                return $RuntimeParameterDictionary
        }
    
        begin {
            # Bind the parameter to a friendly variable
            $Path = $PsBoundParameters[$ParameterName]
        }
    
        process {
            # Your code goes here
            dir -Path $Path
       
        }
    
    }
    
    Get-FolderNames -Path \\share\folder -Folder [Here The Magic Happens]
    


    Jan Hoedt

    Saturday, September 26, 2015 1:18 PM
  • Having a dynamic parameter that depends on the value of another dynamic parameter can get pretty complicated pretty quickly. I could be wrong, but I don't think doing it is something the designers took into account. It can be done, though. Below is the simplest way I could think of to do it. It has one major downside: it creates a variable in the function's parent scope when you type the command that stays around until the command successfully runs. What does that mean? Type this on the screen, then press Esc:

    Deploy-AppVPackage -Publisher Microsoft -Name Office
    

    Then, check the $__DeployAppVPackageDynamicParams variable. If you run the function in the ISE (or dot source it), the parent scope will be the global scope, which means you'll see it on the command line. If you put the function in a module, the parent scope will be the module's scope, which means you won't find the variable unless you go looking for it. That's better, but you'll still have a bug that is caused by saving the parameter dictionary either way: if the command is cancelled, like the example above, the next time you run the command it will act like whatever was defined in the previous command is still defined. Type that command above, and then backspace until -Publisher is gone, then try to type -Name. Notice that it still acts like the publisher is defined.

    I'm pretty sure those problems can be overcome, but it would require a much nastier command, and it would more than likely violate more best practices than this already is. Anyway, here's the code to try:

    $__DeployAppVPackageFolder = "C:\path\to\folder\structure"
    $__TempGlobalParamDictionaryName = "__DeployAppVPackageDynamicParams"
    
    function Deploy-AppVPackage {
    
        [CmdletBinding()]
        param(
        )
    
        DynamicParam {
    
            # Try to get the previously defined dynamic parameter dictionary that's in parent scope
            $ParamDictionary = Get-Variable -Name $__TempGlobalParamDictionaryName -Scope 1 -ValueOnly -ErrorAction SilentlyContinue
    
            if ($ParamDictionary -eq $null) {
                # It didn't exist, so create it:
                $ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    
                $i = 0
                foreach ($ParamName in echo Publisher, Name, Version) {
                    $Attributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    $Attributes.Add( (New-Object System.Management.Automation.ParameterAttribute) )
    
                    # If you don't want the params to be mandatory, you can change this
                    $Attributes[0].Mandatory = $true
    
                    $ParamDictionary[$ParamName] = New-Object System.Management.Automation.RuntimeDefinedParameter (
                        $ParamName,
                        [string],
                        $Attributes
                    )
                }
    
                # This will populate the 'Publisher' property:
                $ValidateSet = $ParamDictionary["Publisher"].Attributes | ? { $_ -is [ValidateSet] }
                [void] $ParamDictionary["Publisher"].Attributes.Remove($ValidateSet)
                $ParamDictionary["Publisher"].Attributes.Add((New-Object ValidateSet (Get-ChildItem $__DeployAppVPackageFolder -Directory | select -exp Name)))
            }
    
            # Populate Name property:
            if ($ParamDictionary["Publisher"].IsSet) {
                $Attributes = $ParamDictionary["Name"].Attributes
                $FolderPath = "{0}\{1}" -f $__DeployAppVPackageFolder, $ParamDictionary["Publisher"].Value
                $Results = Get-ChildItem $FolderPath -Directory -ErrorAction SilentlyContinue | select -exp Name
                [void] $Attributes.Remove(($Attributes | ? { $_ -is [ValidateSet] }))
                if ($Results) {
                    $Attributes.Add( (New-Object ValidateSet $Results) )
                }
            }
    
            # Populate Version property:
            if ($ParamDictionary["Name"].IsSet) {
                $Attributes = $ParamDictionary["Version"].Attributes
                $FolderPath = "{0}\{1}\{2}" -f $__DeployAppVPackageFolder, $ParamDictionary["Publisher"].Value, $ParamDictionary["Name"].Value
                $Results = Get-ChildItem $FolderPath -Directory -ErrorAction SilentlyContinue | select -exp Name
                [void] $Attributes.Remove(($Attributes | ? { $_ -is [ValidateSet] }))
                if ($Results) {
                    $Attributes.Add( (New-Object ValidateSet $Results) )
                }
            }
    
            # Save the param dictionary in the parent scope
            Set-Variable -Name $__TempGlobalParamDictionaryName -Scope 1 -Value $ParamDictionary
    
            # Return the parameter dictionary (that's what DynamicParam {} is supposed to do)
            return $ParamDictionary
        }
    
        end {
            # Clean up the param dictionary in the parent scope
            Remove-Variable -Name $__TempGlobalParamDictionaryName -Scope 1
            
            $FolderPath = "{0}\{1}\{2}\{3}" -f $__DeployAppVPackageFolder, $PSBoundParameters["Publisher"], $PSBoundParameters["Name"], $PSBoundParameters["Version"]
    
            if (-not (Test-Path $FolderPath -PathType Container)) {
                throw "Unable to find install folder!"
            }
    
            $FolderPath
        }
    }

    Try it out and let me know if you have any questions. Someone else may be able to improve on this code a little more and get rid of some of the issues...

    Saturday, September 26, 2015 1:28 PM
  • The goal of asking questions is to learn.  You can't play the violin from day one. You watch and learn from others.



    Jan Hoedt

    You cannot learn a complex technology by monkey-see-monkey-do methods. Technology, like brain surgery, requires a structured approach to learning.  By skipping the fundamentals you will be in a position to learn the wrong things and to not understand what is shown to you. 

    To work with a technology one must take a proactive approach to learning and actually study something.


    \_(ツ)_/

    Saturday, September 26, 2015 2:34 PM
  • Thanks, I will asap and let you know!

    Jan Hoedt

    Sunday, September 27, 2015 7:13 PM
  • Powershell is no brain surgery. It's a language in which you can do complicated things and for that you can read tons of books or just throw yourself in the country of the people who speak it and learn.
    Both approaches are good though a basic understanding is preferrable indeed.
    The most motivating method for me is the first one + having a goal = script to build to start with. Looking for the building blocks, analyze them and understand them. I don't see any monkey-see monkey do, that's impossible without understanding what you are doing. Even then trial and error do learn me a lot what I'm doing: howto debug etc.
    Anyhow, I understand your point of view but I do see it differently. I start reading about Powershell basics about a month ago (not intensively) but most I learned is of building scripts, debugging where I went wrong. That's my learning method. Just asking for some guidelines here and there, don't see why that is wrong. There is just a sripting forum, not one for beginners, medium and advanced scripters.


    Jan Hoedt


    • Edited by janhoedt Sunday, September 27, 2015 7:22 PM Update
    Sunday, September 27, 2015 7:20 PM
  • That is the hardest way to learn anything.  Guessing leads to incorrect understanding.  That leads to confusion.

    Usually people who insist on learning by guesswork are just to lazy to study so they rely on others to tell them the answers.  Not a very professional approach and it will not get you the good jobs.

    In nearly all of the companies I have worked for we always sent techs out to a class for new technology.  They were expected to learn the technology in the class to a specific proficiency level.  Failure to do this meant loss of promotions and loss of bonuses.  This who insisted on using your method were usually not hired or did not last.

    If you are or want to be a professional technician then I highly recommend looking into how to learn effectively.  Once you see how easy it is you will see why I suggest that you are wasting a lot of your time.  I have seen mthis revelation in many techs who finally decided to get serious.


    \_(ツ)_/

    Sunday, September 27, 2015 7:34 PM
  • . Just asking for some guidelines here and there, don't see why that is wrong. There is just a sripting forum, not one for beginners, medium and advanced scripters.


    Jan Hoedt


    My criticism is not intended to chase you away. It seems that you keep asking the same questions or variations on the same question. Most of the answer to your questions are in the  help.  If you learn to use the help and other tools you will then start asking better questions.

    Just a suggestion but one that could help you.


    \_(ツ)_/

    Sunday, September 27, 2015 7:37 PM
  • Thanks, but this is a bridge to far for me right now :-).

    Can't make it work right now with default values ( Deploy-AppVPackage -publisher doesn't return any values) will have to dig deeper into it.


    Jan Hoedt

    Sunday, September 27, 2015 7:52 PM
  • Thanks, but this is a bridge to far for me right now :-).

    Can't make it work right now with default values ( Deploy-AppVPackage -publisher doesn't return any values) will have to dig deeper into it.


    Jan Hoedt

    Hmm. It's not working at all? I can see it being a little flaky, but -Publisher should definitely be populated. I can think of two things off the top of my head that might cause issues:

    1. What version of PowerShell are you running? This almost certainly won't work with version 2 (some changes to the code would probably make it work), but should work with version 3. It's only been tested on version 5...

    2. Did you change the $__DeployAppVPackageFolder variable to point to the root of your folder structure?

    Sunday, September 27, 2015 11:24 PM
  • I've got a better way to do this, and it's not flaky anymore :)

    If you have PowerShell version 3 or higher, go get the TabExpansion module (download the ZIP file, unblock it, extract it, then run 'install.ps1'). The code below can be modified slightly to not use this module, but it complicates things if you have another command that needs to do something like this. The module was created by the same Microsoft employee that created the PSReadLine module, which is now included with PowerShell v5.

    Anyway, if you can get the module, try this:

    function Deploy-AppVPackage {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, Position=0)]
            [string] $Publisher,
            [Parameter(Mandatory, Position=1)]
            [string] $Name,
            [Parameter(Mandatory, Position=2)]
            [string] $Version,
            [string] $PackageFolder = "\\share\packages"
        )
    
        $FolderPath = $PackageFolder
        foreach ($Node in echo Publisher, Name, Version) {
            $FolderPath = "{0}\{1}" -f $FolderPath, $PSBoundParameters[$Node]
            if (-not (Test-Path $FolderPath -PathType Container)) {
                throw ("Invalid {0}: {1}" -f $Node, $PSBoundParameters[$Node])
            }
        }
    
        $FolderPath
    }
    
    if (Get-Module TabExpansion++ -ListAvailable) {
    
        $Completer = {
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
            $FolderPath = $fakeBoundParameter['PackageFolder']
            if ($FolderPath -eq $null) {
                # Look at the command's definition to find the default folder
                $FolderPath = (Get-Command $commandName -ErrorAction SilentlyContinue).ScriptBlock.Ast.Find(
                    { $args[0] -is [System.Management.Automation.Language.ParameterAst] -and $args[0].Name.VariablePath.UserPath -eq "PackageFolder" },
                    $true
                ) | select -ExpandProperty DefaultValue | select -ExpandProperty Value
            }
    
            $SubFolders = @(switch ($parameterName) {
                { $_ -in (echo Name, Version) } {
                    $fakeBoundParameter['Publisher']
                }
    
                Version {
                    $fakeBoundParameter['Name']
                }
            })
    
            if (($SubFolders -eq $null).Count -gt 0) {
                # This means one of the $fakeBoundParameters didn't exist, so assume that
                # parameter wasn't specified
                return
            }
    
            $FolderPath = "$FolderPath\$($SubFolders -join '\')"
            if (Test-Path -LiteralPath $FolderPath -PathType Container) {
                Get-ChildItem -LiteralPath $FolderPath -Directory | ForEach-Object {
                    New-CompletionResult $_.Name $_.FullName
                }
            }
        }
    
        echo Publisher, Name, Version | ForEach-Object { Register-ArgumentCompleter -CommandName Deploy-AppVPackage -ParameterName $_ -ScriptBlock $Completer }
    }
    else {
        Write-Warning "Unable to find TabExpansion++, so argument completer won't work"
    }
    
    That makes the actual function so much simpler. The argument completer stuff is the most complicated part, but I think it's easier to follow than doing this with dynamic parameters. If you want me to explain about anything going on, just let me know.

    • Marked as answer by janhoedt Tuesday, September 29, 2015 8:17 AM
    Monday, September 28, 2015 11:49 PM
  • Built into V4 and later.  Upgrade to V5. You get that and "ReadLine" built in along with oodles more.


    \_(ツ)_/

    Monday, September 28, 2015 11:59 PM
  • Wow, this is awesome! It works. Thanks!!!
    It is actually far more difficult then I ever imagined but I'll analyze it step by step.

    What I only need from the zip-file are the TabExpansion++ ....ps1, psd1 and psm1, right? I did an install of the full TabExpansion++ but I guess the other modules are not needed(?)

    Next thing I'd need to do is figure out howto implement this into  my functions:

    Function MyOwnDeploy-AppVPackage
    { param
        ([string]$Publisher = (Read-Host "Please enter Publisher:"),
         [string]$AppName = (Read-Host "Please enter AppName:"),
         $AppVersion = (Read-Host "Please enter AppVersion:"),
         $PackageVersion = (Read-Host "Please enter PackageVersion:")
        )
    	#Load variables f.e. $AppVPackagename = "$Publisher - AppName - $AppVersion - 						$PackageVersion"
    	LoadVariables
    	#Load Functions
    	New-AppVApplicationUsers
    	New-AppVApplicationComputers
    	New-AppVApplicationCollections
    	New-AppVApplicationCollectionQueries
    	New-AppVApplicationDeployment
    	New-AppVApplicationADGroups	
    }
    
    The same for 
    MyOwnDeploy-Application, which has a different share and other functions




    Note: completion isn't sorting the names alphabetically, that's also something I need to figure out.

    Jan Hoedt


    • Edited by janhoedt Tuesday, September 29, 2015 8:29 AM Update
    Tuesday, September 29, 2015 8:17 AM
  • Most of what you see with the module's required files are extra completers for other commands. I'd just copy the folder from ~\Documents\WindowsPowerShell\Modules\TabExpansion++ and use that to deploy the module. It's up to you whether or not you want to do a machine install of the module for the other computers, or if you want each user to install the module for themselves...

    About your existing functions: try my example code above without providing one of the parameters I've marked as 'Mandatory'. You should be prompted for that value (without having to use 'Read-Host'). That's because the 'advanced' function uses the [Parameter()] attribute. For more info about what makes a function 'advanced' and what the [Parameter()] attribute can do, check out the help topics for 'about_Functions_Advanced' and 'about_Functions_Advanced_Parameters'...

    To change the order of the completions, add a call to Sort-Object in the $Completer scriptblock:
    if (Test-Path $FolderPath -PathType Container) {
        Get-ChildItem $FolderPath -Directory | Sort-Object Name | ForEach-Object {
            New-CompletionResult $_.Name $_.FullName
        }
    }
    

    • Marked as answer by janhoedt Tuesday, September 29, 2015 12:52 PM
    Tuesday, September 29, 2015 12:01 PM
  • Thanks! The only thing that's left for me now is to figure out where I have to put my code.
    What I do not get is why you have the "if (Get-Module TabExpansion++ -ListAvailable" not included in the function. So do I need to include my functions 

    New-AppVApplicationUsers

    New-AppVApplicationComputers

    in the fucntion or in the "if (Get-Module TabExpansion++ -ListAvailable" part(?)



    Jan Hoedt

    Tuesday, September 29, 2015 12:52 PM
  • Thanks! The only thing that's left for me now is to figure out where I have to put my code.
    What I do not get is why you have the "if (Get-Module TabExpansion++ -ListAvailable" not included in the function. So do I need to include my functions 

    New-AppVApplicationUsers

    New-AppVApplicationComputers

    in the fucntion or in the "if (Get-Module TabExpansion++ -ListAvailable" part(?)



    Jan Hoedt

    The 'if (Get-Module...)' part is optional. That's what registers the 'Argument Completers' that the TabExpansion++ module uses to know that we want it to help with the -Publisher, -Name, and -Version parameters. To do that, we require a command (Register-ArgumentCompleter) that only exists if you have the module. So, the 'if' statement checks to see if the module is installed on the computer. If it is, it runs the Register-ArgumentCompleter command (3 times in this case). If it's not, it displays a warning (you could change that to tell your users that they need to get the module if they want the function to have tab completion).

    If the module isn't available, though, the function will still work if you know the folder structure (you just don't get the convenient tab completion)

    If you include other function definitions inside of the Deploy-AppVPacakge function, you will only be able to use them inside that function. If these are commands that you want to use by themselves (like from the command line, or from other functions), you need to define them 'beside' the Deploy-AppVPackage function. Assuming you have multiple functions you want defined, you might structure everything like this:
    function Deploy-AppVPackage {
    <#
        CODE GOES HERE, including calls to other functions like 'New-AppVApplicationUsers'
    #> 
    }
    
    function New-AppVApplicationUsers {
    <#
        CODE GOES HERE
    #> 
    }
    
    function New-AppVApplicationComputers {
    <#
        CODE GOES HERE
    #> 
    }
    
    if (Get-Module TabExpansion++ -ListAvailable) {
        # This part is optional, and just helps make the functions
        # easier to use.
    
        <REGISTER COMPLETERS HERE>
    
    }
    
    If you're going to deploy these functions to other users, you might consider saving them inside of a custom module. For more info, see the 'about_Modules' help topic.


    • Marked as answer by janhoedt Wednesday, September 30, 2015 1:18 PM
    Tuesday, September 29, 2015 4:12 PM
  • Hi,

    Thanks so much for all your effor/input!
    In the script, I have added one extra parameter $Packageversion which it doesn't autocomplete yet. Probably some config I'm overlooking.

    Then there is the issue with my Variables which I'm loading them as a function. I think I'm missing somewhere a part of the function-concept as it does not load my variables. Probably I have to load (all of) them one by one (isn't there an easier way)?
    F.e.
    load $PackageName
    load $owner
    load $Description
    etc

    This is what I load:

    Function LoadVariables
    {
      
            $AvailableTime = $Time #$Time
            $DeadlineDate = $Date
            $Owner = "myuser"
            $Description = "Created by $Owner on $Date $Time"
            $FullAppVNameComputers = "$Publisher - $AppName - $AppVersion"
    }

    This is the Function with tab completion

    function Deploy-CMAppVPackageTabCompletion {
        
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, Position=0)]
            [string] $Publisher,
            [Parameter(Mandatory, Position=1)]
            [string] $AppName,
            [Parameter(Mandatory, Position=2)]
            [string] $AppVersion,
            [Parameter(Mandatory, Position=3)]
            [string] $PackageVersion,
            [string] $PackageFolder = "\\Servername\PackageSources\AppV\X64"
        )
     
        LoadVariables
        New-CMAppVUsers
    
        $FolderPath = $PackageFolder
        foreach ($Node in echo Publisher, Name, Version) {
            $FolderPath = "{0}\{1}" -f $FolderPath, $PSBoundParameters[$Node]
            if (-not (Test-Path $FolderPath -PathType Container)) {
                throw ("Invalid {0}: {1}" -f $Node, $PSBoundParameters[$Node])
            }
        }
    
        $FolderPath
    }
    
    if (Get-Module TabExpansion++ -ListAvailable) {
    
        $Completer = {
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
            $FolderPath = $fakeBoundParameter['PackageFolder']
            if ($FolderPath -eq $null) {
                # Look at the command's definition to find the default folder
                $FolderPath = (Get-Command $commandName -ErrorAction SilentlyContinue).ScriptBlock.Ast.Find(
                    { $args[0] -is [System.Management.Automation.Language.ParameterAst] -and $args[0].Name.VariablePath.UserPath -eq "PackageFolder" },
                    $true
                ) | select -ExpandProperty DefaultValue | select -ExpandProperty Value
            }
    
            $SubFolders = @(switch ($parameterName) {
                { $_ -in (echo AppName, AppVersion) } {
                    $fakeBoundParameter['Publisher']
                }
    
                AppVersion {
                    $fakeBoundParameter['AppName']
                }
            })
    
            if (($SubFolders -eq $null).Count -gt 0) {
                # This means one of the $fakeBoundParameters didn't exist, so assume that
                # parameter wasn't specified
                return
            }
    
            $FolderPath = "$FolderPath\$($SubFolders -join '\')"
            if (Test-Path -LiteralPath $FolderPath -PathType Container) {
                Get-ChildItem -LiteralPath $FolderPath -Directory | Sort-Object Name | ForEach-Object {
                    New-CompletionResult $_.Name $_.FullName
                }
            }
        }
    
        echo Publisher, AppName, AppVersion, PackageVersion | ForEach-Object { Register-ArgumentCompleter -CommandName Deploy-CMAppVPackageTabCompletion -ParameterName $_ -ScriptBlock $Completer }
    }
    else {
        Write-Warning "Unable to find TabExpansion++, so argument completer won't work"
    }



    Jan Hoedt

    The error I have is

    Creating


    " New-CMApplication : Cannot bind argument to parameter 'Name' because it is null. "

    So it didn't load the variables ....

    • Edited by janhoedt Wednesday, September 30, 2015 2:59 PM Update
    Wednesday, September 30, 2015 1:18 PM
  • I'll post this particular question seperately since this post actually is answered.

    Jan Hoedt

    Wednesday, September 30, 2015 7:58 PM
  • What you're seeing is related to scope (see the 'about_Scopes' help topic for more info). I'm not sure how often you plan to create those variables, but I would stick to just defining them locally inside the function where you will use them for now. You can get around it by using Set-Variable with the -Scope parameter (see the help), but I think that's just going to complicate things and make it harder to follow the code.

    Creating a new parameter (or changing parameter names) means that you need to change the code in the $Completer scriptblock if you want the parameters to tab complete. Also, the test function I provided was doing a test where it walked through each element in the final $FolderPath that used $PSBoundParameters to validate the user input. If you're having trouble following that, you can take it out, but I'd make sure you at least put some sort of test in place to make sure you were given valid data before you try to continue. If you want to leave it, you need to make sure to change the strings in the foreach() block to match the new parameter names.

    Wednesday, September 30, 2015 9:43 PM
  • Thanks! Scope is not an option for me then.
    I defined the variables in my script and it is working now!

    The functions theirselves (like New-CMAppVUsers) however don't because it won't accept the $PackageVersion. Can't figure out howto add it (it is in the $SubFolders part, I know but can't make it work, could you advise on that?)

    Note: I could upgrade to Powershell 5 so I don't need the  TabExtension++ but is that advisable (it's no final release and not sure how I should modify this code then).

    This is what I have now:

    function Deploy-CMApplicationTabCompletion {
        
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, Position=0)]
            [string] $Publisher,
            [Parameter(Mandatory, Position=1)]
            [string] $AppName,
            [Parameter(Mandatory, Position=2)]
            [string] $AppVersion,
            #Completion not working yet for $PackageVersion
            [Parameter(Mandatory, Position=3)]
            [string] $PackageVersion,
            [string] $PackageFolder = "\\ourshare\AppV\X64"
            # To add: if variable $PackageType = AppV, Application or Package, if AppV then run AppV functions, if Application then run Application functions etc
        )
    
    
        #VARIABLES: howto collapse this section since I have about 20 more#
            $Environment = "RDS"
            $DeployPurpose="Required"
            $Date = Get-Date -format 
            $Time = Get-Date -Format "h:mm"
            $AvailableDateTime = $Date
            $AvailableTime = $Time
            $DeadlineDate = $Date
            $DeadlineTime = "{0:HH:mm}" -f (Get-Date).AddMinutes(1)
            $Owner = "myuser"   
            $FullAppVNameComputers = "Computers - $Publisher - $AppName - $AppVersion - $PackageVersion"
            $FullAppVNameUsers = "Users - $Publisher - $AppName - $AppVersion - $PackageVersion"     
            $DistributionPointGroupName = "OURDG"
         #... 30 other variables that do not matter here#
            
        New-CMAppVUsers #Other functions to add, I test with this one first
        
       
        $FolderPath = $PackageFolder
        foreach ($Node in echo Publisher, Name, Version) {
            $FolderPath = "{0}\{1}" -f $FolderPath, $PSBoundParameters[$Node]
            if (-not (Test-Path $FolderPath -PathType Container)) {
                throw ("Invalid {0}: {1}" -f $Node, $PSBoundParameters[$Node])
            }
        }
    
        $FolderPath
    }
    
    if (Get-Module TabExpansion++ -ListAvailable) {
    
        $Completer = {
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
            $FolderPath = $fakeBoundParameter['PackageFolder']
            if ($FolderPath -eq $null) {
                # Look at the command's definition to find the default folder
                $FolderPath = (Get-Command $commandName -ErrorAction SilentlyContinue).ScriptBlock.Ast.Find(
                    { $args[0] -is [System.Management.Automation.Language.ParameterAst] -and $args[0].Name.VariablePath.UserPath -eq "PackageFolder" },
                    $true
                ) | select -ExpandProperty DefaultValue | select -ExpandProperty Value
            }
    
            $SubFolders = @(switch ($parameterName) {
                { $_ -in (echo AppName, AppVersion) } {
                    $fakeBoundParameter['Publisher']
                }
    
                AppVersion {
                    $fakeBoundParameter['AppName']
                }
            })
    
            if (($SubFolders -eq $null).Count -gt 0) {
                # This means one of the $fakeBoundParameters didn't exist, so assume that
                # parameter wasn't specified
                return
            }
    
            $FolderPath = "$FolderPath\$($SubFolders -join '\')"
            if (Test-Path -LiteralPath $FolderPath -PathType Container) {
                Get-ChildItem -LiteralPath $FolderPath -Directory | Sort-Object Name | ForEach-Object {
                    New-CompletionResult $_.Name $_.FullName
                }
            }
        }
    
        echo Publisher, AppName, AppVersion, PackageVersion | ForEach-Object { Register-ArgumentCompleter -CommandName Deploy-CMApplicationTabCompletion -ParameterName $_ -ScriptBlock $Completer }
    }
    else {
        Write-Warning "Unable to find TabExpansion++, so argument completer won't work"
    }
    



    Jan Hoedt

    Thursday, October 1, 2015 10:34 AM
  • Found out that I had to put the $PackageVersion between double quotes in the command but still then there is an error

    test-path : A parameter cannot be found that matches parameter name 'PathType'.

    Jan Hoedt

    Friday, October 2, 2015 4:29 PM
  • I left this post some months ago but would love to pick it up/make it work. Could you advise on my latest update?


    Jan Hoedt

    Thursday, January 28, 2016 9:57 AM
  • Hi Jan,

    Better start a new post, which is not marked as answer to start. Post link to this one there.


    Regards,

    Satyajit

    Please “Vote As Helpful” if you find my contribution useful or “Mark As Answer” if it does answer your question. That will encourage me - and others - to take time out to help you.

    Thursday, January 28, 2016 10:06 AM
  • Found out that I had to put the $PackageVersion between double quotes in the command but still then there is an error

    test-path : A parameter cannot be found that matches parameter name 'PathType'.

    Jan Hoedt

    Q.#VARIABLES: howto collapse this section since I have about 20 more#
            $Environment
    = "RDS"; $DeployPurpose="Required" ;

    A. Use semi-colon as line terminator and put all the variables in a single line. If you are looking for collapsible section within ISE use statement block { } without any if or for.

    You should check   $FolderPath generated is the correct one.


    Regards,

    Satyajit

    Please “Vote As Helpful” if you find my contribution useful or “Mark As Answer” if it does answer your question. That will encourage me - and others - to take time out to help you.

    Thursday, January 28, 2016 10:21 AM