AzureCliExpansion.ps1

# This PS script replicates the functionality in https://github.com/Azure/azure-xplat-cli/blob/90a20ee00e0741a5ec8cece69bf5e18bf1e0ecda/lib/autocomplete.js
# An alternative approach would be to look at ways to directly invoke the functionality and capture the output :-)
# This has a dependency on plugins json files (Use "azure --gen" to create)
$installPath = Split-Path $MyInvocation.MyCommand.Path
. "$installPath\utils.ps1"

$global:_CLI_MODE=$null;
$global:_CLI_DATA=$null;

function AzureCliExpansion($line) {
    # TODO - error handling (e.g. azure.cmd not found, plugins.xxx.json not found)
    DebugMessage "autocomplete: $line"
    
    $azureCmdPath = GetAzureCmdPath
    $azurecliLibPath = GetAzureLibPath
    
    # Based on https://github.com/Azure/azure-xplat-cli/blob/90a20ee00e0741a5ec8cece69bf5e18bf1e0ecda/lib/util/utilsCore.js#L77
    $config = Get-Content "~/.azure/config.json" | ConvertFrom-Json
    $mode = $config.mode

    DebugMessage "\tmode: $mode"

    if ( ($global:_CLI_MODE -eq $mode) -and ($global:_CLI_DATA -ne $null))     {
        $plugins = $global:_CLI_DATA
    } else {
        # Based on https://github.com/Azure/azure-xplat-cli/blob/90a20ee00e0741a5ec8cece69bf5e18bf1e0ecda/lib/autocomplete.js#L26
        $datafile = "$azurecliLibPath/plugins.$mode.json"
        $plugins = Get-Content $datafile | ConvertFrom-Json

        $global:_CLI_MODE = $mode
        $global:_CLI_DATA = $plugins
    }

    # Based on https://github.com/Azure/azure-xplat-cli/blob/90a20ee00e0741a5ec8cece69bf5e18bf1e0ecda/lib/autocomplete.js#L49
    $args = $line.Split(' ') | ?{ $_ -ne ''} | %{ $_.Trim() }
    
    # start from 1, so to discard "azure" word
    $currentCategory = $plugins;
    for ($index = 1; $index -lt $args.Length; $index++){
        $arg = $args[$index]
        $parentCategory = $currentCategory
        if ( ($index -eq $args.Length -1) -and (-not $line.EndsWith(" "))    )    {
            # bail early if no space at the end of the line to avoid bogus completion.
            # i.e. don't match "vm" commands for "azure vm", only for "azure vm "
            # otherwise the vm command is replaced with the child commands!
            return $currentCategory.commands | where Name -eq $arg
        }
        $currentCategory = $currentCategory.categories.psobject.Properties[$arg].Value
        DebugMessage "\targ $($index): $arg"
        if( $currentCategory -eq $null){
            DebugMessage "\targ $($index): $arg - no match"
            break
        }
    }
    
    $tempCategory = Coalesce $currentCategory $parentCategory
    $allSubCategoriesAndCommands = @($tempCategory.categories.PSObject.Properties | select -ExpandProperty Name) `
                        + @($tempCategory.commands | select -ExpandProperty Name)
    
    $currentCommand = $tempCategory.commands | where Name -eq $arg | select -First 1
    
    
    # run out argument while have a valid category?
    if ($currentCategory -ne $null) {
        DebugMessage "\treturn all categories/commands (no category)"
        #return sub categories and command combind
        return $allSubCategoriesAndCommands | sort
    }
    
    if ($currentCommand -ne $null) {
        $allCommandOptions = @($currentCommand.options | select -ExpandProperty long) `
                                + @($currentCommand.options | select -ExpandProperty short -ErrorAction SilentlyContinue) # silent continue to ignore options without short property
    }
    
    # we are at the last arg, try match both categories and commands
    if ($index -eq $args.Length -1) {
        if ($currentCommand -ne $null) {
            DebugMessage "\treturn all commands"
            return $allCommandOptions | sort
        } else {
            DebugMessage "\treturn matches on prefix: $arg"
            return $allSubCategoriesAndCommands | ?{ $_.StartsWith($arg) } | sort
        }
    }
    
    # try to match a command's options
    $lastArg = $args[$args.Length - 1]
    if( ($currentCommand -ne $null) -and ($lastArg.StartsWith('-') ) ) {
        $option = $currentCommand.options | ?{ $_.fileRelatedOption -and ($_.short -eq $lastArg -or $_.long -eq $lastArg ) } | select -First 1
        
        if ($option -ne $null) {
            # return this.reply(fs.readdirSync(process.cwd()));
            # Default PS behaviour is to complete files :-)
            DebugMessage "\treturn to allow default PowerShell file matching"
            return
        } else {
            DebugMessage "\treturn matches on lastArg: $lastArg"
            return $allCommandOptions | ?{ $_.StartsWith($lastArg) } | sort
        }
    }
    
    DebugMessage "\treturn all categories/commands"
    return $allSubCategoriesAndCommands | sort
}


# TODO - look at posh-git/posh-hg to link with powertab
DebugMessage "Installing..."
if(-not (Test-Path Function:\AzureCliTabExpansionBackup)){

    if (Test-Path Function:\TabExpansion) {
        DebugMessage "\tbackup previous TabExpansion"
        Rename-Item Function:\TabExpansion AzureCliTabExpansionBackup
    }

    DebugMessage "\tInstalling TabExpansion hook"
    function TabExpansion($line, $lastWord) {
       $lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()

       switch -Regex ($lastBlock) {
            "^azure (.*)" { AzureCliExpansion $lastBlock }

            # Fall back on existing tab expansion
            default { if (Test-Path Function:\AzureCliTabExpansionBackup) { AzureCliTabExpansionBackup $line $lastWord } }
       }
    }
}