Command.ps1
function Get-CommandsPattern { (Get-Command * | ForEach-Object { $CommandUnsafe = $_ | Select-Object -ExpandProperty 'Name' $Command = [Regex]::Escape($CommandUnsafe) # check if it has a file extensions if ($CommandUnsafe -match "(?<cmd>[^.\s]+)\.(?<ext>[^.\s]+)$") { $CommandWithoutExtension = [Regex]::Escape($matches['cmd']) return $Command, $CommandWithoutExtension } else { return $Command } }) -Join '|' } $global:AliasTipCommandsPattern = Get-CommandsPattern # THANKS TO csc027 for the original code https://github.com/csc027 # Taken from: https://github.com/dahlbyk/posh-git/blob/ad8278e90ad8180c18e336676e490d921615e506/src/GitTabExpansion.ps1#L73-L87 # # The regular expression here is roughly follows this pattern: # # <begin anchor><whitespace>*<command>(<whitespace><parameter>)*<whitespace>+<$args><whitespace>*<end anchor> # # The delimiters inside the parameter list and between some of the elements are non-newline whitespace characters ([^\S\r\n]). # In those instances, newlines are only allowed if they preceded by a non-newline whitespace character. # # Begin anchor (^|[;`n]) # Whitespace (\s*) # Any Command (?<cmd>) # Parameters (?<params>(([^\S\r\n]|[^\S\r\n]``\r?\n)+\S+)*) # $args Anchor (([^\S\r\n]|[^\S\r\n]``\r?\n)+\`$args) # Whitespace (\s|``\r?\n)* # End Anchor ($|[|;`n]) function Get-ProxyFunctionRegex { param ( [Parameter(Mandatory, ValueFromPipeline = $true)][string]${CommandPattern} ) "(^|[;`n])(\s*)(?<cmd>($CommandPattern))(?<params>(([^\S\r\n]|[^\S\r\n]``\r?\n)+\S+)*)(([^\S\r\n]|[^\S\r\n]``\r?\n)+\`$args)(\s|``\r?\n)*($|[|;`n])" } function Get-ProxyFunctionRegexNoArgs { param ( [Parameter(Mandatory, ValueFromPipeline = $true)][string]${CommandPattern} ) "(^|[;`n])(\s*)(?<cmd>($CommandPattern))(?<params>(([^\S\r\n]|[^\S\r\n]``\r?\n)+\S+)*)(\s|``\r?\n)*($|[|;`n])" } function Format-CleanCommand { param( [Parameter(Mandatory, ValueFromPipeline = $true)][string]${Command} ) return ($Command -replace '`\r?\n', ' ' -replace '\s+', ' ').Trim() } # Returns a regex to match with function Get-CommandMatcher { param( [Parameter(Mandatory, ValueFromPipeline = $true)][string]${Command}, [Parameter()][switch]${Simple} ) if ($Simple) { $CleanCommand = $Command | Format-CleanCommand return "(" + ([Regex]::Escape($CleanCommand) -split " " -join "|") + ")" } # The parse is a bit naive... if ($Command -match $AliasTipsProxyFunctionRegexNoArgs) { # Clean up the command by removing extra delimiting whitespace and backtick preceding newlines $CommandString = ("$($matches['cmd'].TrimStart())") | Format-CleanCommand $ReqParams = $($matches['params']) -split " " # $ReqParamLength = $ReqParams.Count $ReqParamRegex = "(" + ($ReqParams.ForEach({ "$([Regex]::Escape($_.Trim()))(\s|``\r?\n)*" }) -join '|') + ")*" # Enable sensitive case (?-i) # Begin anchor (^|[;`n]) # Whitespace (\s*) # Any Command (?<cmd>$CommandString) # Whitespace (\s|``\r?\n)* # Req Parameters (?<params>$ReqParamRegex) # Req Param Length {$ReqParamLength,} # Whitespace (\s|``\r?\n)* # End Anchor ($|[|;`n]) #$Regex = "(^|[;`n])(\s*)(?<cmd>$CommandString)(?<params>$ReqParamRegex{$ReqParamLength,})(\s|``\r?\n)*($|[|;`n])" $Regex = "(?-i)(^|[;`n])(\s*)(?<cmd>$CommandString)(\s|``\r?\n)*(?<params>$ReqParamRegex)(\s|``\r?\n)*($|[|;`n])" return $Regex } return "" } $script:AUTOMATIC_VARIBLES_TO_SUPRESS = @( '\$', '\?', '\^', '_', 'args', 'ConsoleFileName', 'EnabledExperimentalFeatures', 'Error', 'Event(Args|Subscriber)?', 'ExecutionContext', 'false', 'HOME', 'Host', 'input', 'Is(CoreCLR|Linux|MacOS|Windows){1}', 'LASTEXITCODE', 'Matches', # TODO? 'MyInvocation', 'NestedPromptLevel', 'null', 'PID', 'PROFILE', 'PSBoundParameters', # TODO? 'PSCmdlet', 'PSCommandPath', # TODO? 'PSCulture', 'PSDebugContext', 'PSEdition', 'PSHOME', 'PSItem', 'PSScriptRoot', 'PSSenderInfo', 'PSUICulture', 'PSVersionTable', 'PWD', 'Sender', 'ShellId', 'StackTrace', 'switch', 'this', 'true' ) -Join '|' # Finds the command based on the alias and replaces $(...) if possible function Format-CommandAST { param( [Parameter(Mandatory)][string]${Alias} ) # Get the original definition $Def = Get-Item -Path Function:\$Alias | Select-Object -ExpandProperty 'Definition' # Find variables we need to resolve, ie $MainBranch $VarsToResolve = @("") $ReconstructedCommand = "" if ($Def -match $AliasTipsProxyFunctionRegexNoArgs) { $ReconstructedCommand = ("$($matches['cmd'].TrimStart()) $($matches['params'])") | Format-CleanCommand if ($args -match '\$args') { $ReconstructedCommand += ' $args' } $($matches['params'] | Format-CleanCommand) -split " " | ForEach-Object { if ($_ -match '\$') { # Make sure it is not an automatic variable if ($_ -match "(\`$)($AUTOMATIC_VARIBLES_TO_SUPRESS)") { } else { $VarsToResolve += $_ -replace "[^$`n]*(?=\$)", "" } } } } else { return "" } $VarsReplaceHash = @{} Get-Variable AliasTipsInternalASTResults_* | ForEach-Object { if ($_.Value) { $VarsReplaceHash[$($_.Name -replace "AliasTipsInternalASTResults_","")] = $_.Value } } # If there are vars to resolve, attempt to find them. if ($VarsToResolve) { $DefScriptBlock = [scriptblock]::Create($Def) $DefAst = $DefScriptBlock.Ast foreach ($Var in $VarsToResolve) { # Attempt to find the definition based on the ast # TODO: handle nested script blocks $FoundAssignment = $DefAst.Find({ $args[0] -is [System.Management.Automation.Language.VariableExpressionAst] -and $("$($args[0].Extent)" -eq "$Var") }, $false) if ($FoundAssignment -and -not $VarsReplaceHash[$Var]) { $CommandToEval = $($FoundAssignment.Parent -replace "[^=`n]*=","").Trim() # Super naive LOL! Hopefully the command isn't destructive! $Evaluated = Invoke-Command -ScriptBlock $([scriptblock]::Create("$CommandToEval -ErrorAction SilentlyContinue")) if ($Evaluated) { $VarsReplaceHash[$Var] = $Evaluated Set-Variable -Name "AliasTipsInternalASTResults_$Var" -Value $Evaluated -Scope Global } } } $VarsReplaceHash.GetEnumerator() | ForEach-Object { if ($_.Value) { $ReconstructedCommand = $ReconstructedCommand -replace $([regex]::Escape($_.key)),$_.Value } } return $($ReconstructedCommand | Format-CleanCommand) } } function Clear-AliasTipsInternalASTResults { Clear-Variable AliasTipsInternalASTResults_* -Scope Global } |