Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.1.0 #26

Merged
merged 15 commits into from
May 6, 2024
22 changes: 22 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: build

on:
workflow_dispatch:
push:
branches: [ master ]
paths:
- 'alias-tips/**/*.ps1'
pull_request:
branches: [ master ]

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Run Build Script
run: .\scripts\build.ps1
shell: pwsh
34 changes: 34 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: lint

on:
workflow_dispatch:
push:
branches: [ master ]
paths:
- 'alias-tips/**/*.ps1'
pull_request:
branches: [ master ]

defaults:
run:
working-directory: windows

jobs:
lint:
permissions:
contents: read
security-events: write
actions: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run PSScriptAnalyzer
uses: microsoft/[email protected]
with:
path: .\
recurse: true
output: results.sarif
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Cody Duong, and contributers
Copyright (c) 2023-2024 Cody Duong, and contributers

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ Install the module from the [PowerShell Gallery](https://www.powershellgallery.c
Install-Module alias-tips -AllowClobber
```

Inside your PowerShell profile
Inside your PowerShell profile, import alias-tips.

```powershell
Import-Module alias-tips
```

> [!IMPORTANT]
> alias-tips should be imported after all aliases declared

Everytime your aliases are updated run

```powershell
Expand All @@ -69,14 +72,17 @@ This will store a hash of all aliased commands to: `$HOME/.alias_tips.hash` . It
| ALIASTIPS_DEBUG | `$false` | Enable to show debug messages when processing commands |
| ALIASTIPS_HASH_PATH | `[System.IO.Path]::Combine("$HOME", '.alias_tips.hash')` | File Path to store results from `Find-AliasTips` |
| ALIASTIPS_MSG | `"Alias tip: {0}"` | Alias hint message for non-virtual terminals |
| ALIASTIPS_MSG_VT | `` `e[033mAlias tip: {0}`em" `` | Alias hint message for virtual terminals |
| ALIASTIPS_MSG_VT | `` `e[033mAlias tip: {0}`em" `` | Alias hint message for virtual terminals* |
| ALIASTIPS_FUNCTION_INTROSPECTION | `$false` | **POTENTIALLY DESTRUCTIVE** [Function Alias Introspection](#function-alias-introspection) |

\* This uses [ANSI Escape Codes](https://en.wikipedia.org/wiki/ANSI_escape_code), for a [table of colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors).
This also means alias-tips supports any other virtual terminal features: blinking/bold/underline/italics.

## How Does This Work

It will attempt to read all functions/aliases set in the current context.

### Example Interactions
### Example Interactions/Limitations

#### Alias
```powershell
Expand All @@ -90,6 +96,8 @@ function grbi {
git rebase -i $args
}
```
> [!NOTE]
> This has a limitation in that alias-tips does not know in this case that -i is equivalent to the --interactive flag.

#### Function Alias Introspection

Expand All @@ -101,12 +109,15 @@ function gcm {
}
```

This is potentially destructive behavior, as it requires running `Get-Git-MainBranch` (in this example)
to attempt to parse `$MainBranch` and is disabled by default. It is also currently in a limited parsing stage.
It does not attempt to parse line-by-line, instead performing a backwards search, and is naive in its
implementation.
> [!WARNING]
> This is potentially destructive behavior

This requires backparsing as it requires running `Get-Git-MainBranch` (in this example) to get the value of `$MainBranch`.
This backparsing **could be** a destructive command is not known to alias-tips, and it will run anyways. (ie. rm, git add, etc.)

As a result this behavior is disabled by default.

Set `$env:ALIASTIPS_FUNCTION_INTROSPECTION` to `$true` to enable it
Set `$env:ALIASTIPS_FUNCTION_INTROSPECTION` to `$true` to enable it.

## License

Expand Down
35 changes: 25 additions & 10 deletions alias-tips/Private/Find-AliasCommand.ps1
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
# Attempts to find an alias for a singular command
function Find-AliasCommand {
param(
[Parameter(ValueFromPipeline = $true)]
[Parameter(Mandatory, ValueFromPipeline = $true)]
[string]$Command
)

process {
begin {
if ($AliasTipsHash -and $AliasTipsHash.Count -eq 0) {
$AliasTipsHash = ConvertFrom-StringData -StringData $([System.IO.File]::ReadAllText($AliasTipsHashFile)) -Delimiter "|"
}
}

process {
# If we can find the alias quickly, do so
$Alias = $AliasTipsHash[$Command.Trim()]
if ($Alias) {
if (-not [string]::IsNullOrEmpty($Alias)) {
Write-Verbose "Quickly found alias inside of AliasTipsHash"
return $Alias
return $Alias | Format-Command
}

# TODO check if it is an alias, expand it back out to check if there is a better alias

# We failed to find the alias in the hash, instead get the executed command, and attempt to generate a regex for it.

# First we need to ensure we have generated required regexes
Find-RegexThreadJob
# Generate a regex that searches through our alias hash, and checks if it matches as an alias for our command
$Regex = Get-CommandRegex $Command
# Write-Host $Regex
if ([string]::IsNullOrEmpty($Regex)) {
return ""
}
Expand All @@ -34,24 +41,32 @@
$AliasTipsHash.GetEnumerator() | ForEach-Object {
# Only reasonably evaluate any commands that match the one we are searching for
if ($_.key -match $Regex) {
$Aliases += $_.key
$Aliases += ,($_.Key, $_.Value)
}

# Substitute commands using ExecutionContext if possible
# Check if we have anything that has a $(...)
if ($_.key -match $SimpleSubRegex -and ([boolean](Initialize-EnvVariable "ALIASTIPS_FUNCTION_INTROSPECTION" $false)) -eq $true) {
if ($_.key -match $SimpleSubRegex -and ((Initialize-EnvVariable "ALIASTIPS_FUNCTION_INTROSPECTION" $false) -eq $true)) {
$NewKey = Format-CommandFromExecutionContext($_.value)
if (-not [string]::IsNullOrEmpty($NewKey) -and $($NewKey -replace '\$args', '') -match $Regex) {
$Aliases += $($NewKey -replace '\$args', '').Trim()
$Aliases += ,($($NewKey -replace '\$args', '').Trim(), $_.Value)
$AliasTipsHashEvaluated[$NewKey] = $_.value
}
}
}
Clear-AliasTipsInternalASTResults

Write-Verbose $($Aliases -Join ",")
# Use the longest candiate
$AliasCandidate = ($Aliases | Sort-Object -Descending -Property Length)[0]
# Sort by which alias removes the most, then if they both shorten by same amount, choose the shorter alias
$Aliases = @(@($Aliases
Fixed Show fixed Hide fixed
| Where-Object { $null -ne $_[0] -and $null -ne $_[1] })
| Sort-Object -Property @{Expression = { - ($_[0]).Length } }, @{Expression = { ($_[1]).Length} })
# foreach ($pair in $Aliases) {
# Write-Host "($($pair[0]), $($pair[1]))"
# }
# Use the longest candiate, if tied use shorter alias
Fixed Show fixed Hide fixed
# -- TODO? this is my opinionated way since it results in most coverage (one long alias is better than two combined shorter aliases),
Fixed Show fixed Hide fixed
$AliasCandidate = ($Aliases)[0][0]
Write-Verbose "Alias Candidate Chosen: $AliasCandidate"
$Alias = ""
if (-not [string]::IsNullOrEmpty($AliasCandidate)) {
$Remaining = "$($Command)"
Expand Down
18 changes: 18 additions & 0 deletions alias-tips/Private/Find-RegexThreadJob.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function Find-RegexThreadJob {
if ($null -ne $global:AliasTipsProxyFunctionRegex -and $null -ne $global:AliasTipsProxyFunctionRegexNoArgs) {
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
return
}

$existingJob = Get-Job -Name "FindAliasTipsJob" -ErrorAction SilentlyContinue | Select-Object -Last 1
if ($null -ne $existingJob) {
$existingJob = Wait-Job -Job $existingJob
}
else {
$job = Start-RegexThreadJob

$existingJob = Wait-Job -Job $job
}
$result = Receive-Job -Job $existingJob -Wait -AutoRemoveJob

$global:AliasTipsProxyFunctionRegex, $global:AliasTipsProxyFunctionRegexNoArgs = $result
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
}
23 changes: 21 additions & 2 deletions alias-tips/Private/Get-Aliases.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Return a hashtable of possible aliases
function Get-Aliases {
$Hash = @{}
Find-RegexThreadJob

# generate aliases for commands aliases created via native PowerShell functions
$proxyAliases = Get-Item -Path Function:\
Expand All @@ -11,12 +12,30 @@
# validate there is a command
if ($ProxyDef -match $AliasTipsProxyFunctionRegex) {
$CleanedCommand = ("$($matches['cmd'].TrimStart()) $($matches['params'])") | Format-Command

Fixed Show fixed Hide fixed
if ($ProxyDef -match '\$args') {
$Hash[$CleanedCommand + ' $args'] = $ProxyName
# Use the shorter of two if we already have hashed this command
if ($Hash.ContainsKey($CleanedCommand + ' $args')) {
if ($ProxyName.Length -lt $Hash[$CleanedCommand + ' $args'].Length) {
$Hash[$CleanedCommand + ' $args'] = $ProxyName
}
}
else {
$Hash[$CleanedCommand + ' $args'] = $ProxyName
}

Fixed Show fixed Hide fixed
}

# quick alias
$Hash[$CleanedCommand] = $ProxyName
# use the shorter of two if we already have hashed this command
if ($Hash.ContainsKey($CleanedCommand)) {
if ($ProxyName.Length -lt $Hash[$CleanedCommand].Length) {
$Hash[$CleanedCommand] = $ProxyName
}
}
else {
$Hash[$CleanedCommand] = $ProxyName
}
}
}

Expand Down
15 changes: 4 additions & 11 deletions alias-tips/Private/Get-CommandRegex.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,19 @@
[OutputType([System.String])]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]${Command},

[Parameter()]
[switch]${Simple}
[string]${Command}
)

process {
if ($Simple) {
$CleanCommand = $Command | Format-Command
return "(" + ([Regex]::Escape($CleanCommand) -split " " -join "|") + ")"
}

# The parse is a bit naive...
if ($Command -match $AliasTipsProxyFunctionRegexNoArgs) {
if ($Command -match $global:AliasTipsProxyFunctionRegexNoArgs) {
Fixed Show fixed Hide fixed
# Clean up the command by removing extra delimiting whitespace and backtick preceding newlines
$CommandString = ("$($matches['cmd'].TrimStart())") | Format-Command
$CommandString = ("$($matches['cmd'].TrimStart())")

if ([string]::IsNullOrEmpty($CommandString)) {
return ""
}
$CommandString = $CommandString | Format-Command

$ReqParams = $($matches['params']) -split " "
$ReqParamRegex = "(" + ($ReqParams.ForEach({
Expand Down
14 changes: 0 additions & 14 deletions alias-tips/Private/Get-CommandsRegex.ps1

This file was deleted.

24 changes: 0 additions & 24 deletions alias-tips/Private/Get-ProxyFunctionRegex.ps1

This file was deleted.

53 changes: 53 additions & 0 deletions alias-tips/Private/Start-RegexThreadJob.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
function Start-RegexThreadJob {
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
$existingJob = Get-Job -Name "FindAliasTipsJob" -ErrorAction SilentlyContinue | Select-Object -Last 1
if ($null -ne $existingJob) {
$existingJob = Wait-Job -Job $existingJob
}

return Start-ThreadJob -Name "FindAliasTipsJob" -ScriptBlock {
function Get-CommandsRegex {
(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 '|'
}

# The regular expression here 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-ProxyFunctionRegexes {
Fixed Show fixed Hide fixed
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline = $true)][regex]${CommandPattern}
)

process {
[regex]"(^|[;`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])",
[regex]"(^|[;`n])(\s*)(?<cmd>($CommandPattern))(?<params>(([^\S\r\n]|[^\S\r\n]``\r?\n)+\S+)*)(\s|``\r?\n)*($|[|;`n])"
}
}


Get-CommandsRegex | Get-ProxyFunctionRegexes
}
}

Start-RegexThreadJob | Out-Null
Loading
Loading