Last active
July 20, 2025 09:57
-
-
Save charlesroper/90b7b3f76951a7ac24e624378700679d to your computer and use it in GitHub Desktop.
Native tab-completion for eza on PowerShell 7+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
eza-completion.ps1 | |
────────────────── | |
Native tab-completion for eza on PowerShell 7+ | |
Drop this file in your $PROFILE directory and source it: | |
. (Join-Path (Split-Path $PROFILE -Parent) "eza-completion.ps1") | |
Make sure the following PSReadLine options are set: | |
Set-PSReadLineOption -ShowToolTips | |
Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete | |
#> | |
using namespace System.Management.Automation | |
using namespace System.Management.Automation.Language | |
# Register the completer for the eza command and its aliases. Note that you have | |
# to include aliases because PowerShell does not automatically register them. | |
Register-ArgumentCompleter -Native ` | |
-CommandName 'eza', 'eza.exe', 'ls', 'll' ` | |
-ScriptBlock { | |
param( | |
$wordToComplete, | |
$commandAst, | |
$cursorPosition | |
) | |
# ─── one-line descriptions taken from `eza --help` ──────────────────────── | |
$helpText = New-Object 'System.Collections.Hashtable' ([System.StringComparer]::Ordinal) | |
# Meta options | |
$helpText['-?'] = '-?, --help show list of command-line options' | |
$helpText['--help'] = '-?, --help show list of command-line options' | |
$helpText['-v'] = '-v, --version show version of eza' | |
$helpText['--version'] = '-v, --version show version of eza' | |
# Display options | |
$helpText['-1'] = '-1, --oneline display one entry per line' | |
$helpText['--oneline'] = '-1, --oneline display one entry per line' | |
$helpText['-l'] = '-l, --long display extended file metadata as a table' | |
$helpText['--long'] = '-l, --long display extended file metadata as a table' | |
$helpText['-G'] = '-G, --grid display entries as a grid (default)' | |
$helpText['--grid'] = '-G, --grid display entries as a grid (default)' | |
$helpText['-x'] = '-x, --across sort the grid across, rather than downwards' | |
$helpText['--across'] = '-x, --across sort the grid across, rather than downwards' | |
$helpText['-R'] = '-R, --recurse recurse into directories' | |
$helpText['--recurse'] = '-R, --recurse recurse into directories' | |
$helpText['-T'] = '-T, --tree recurse into directories as a tree' | |
$helpText['--tree'] = '-T, --tree recurse into directories as a tree' | |
$helpText['-X'] = '-X, --dereference dereference symbolic links when displaying information' | |
$helpText['--dereference'] = '-X, --dereference dereference symbolic links when displaying information' | |
$helpText['-F'] = '-F, --classify display type indicator by file names' | |
$helpText['--classify'] = '-F, --classify display type indicator by file names' | |
$helpText['--color'] = '--colo[u]r when to use terminal colours (always, auto, never)' | |
$helpText['--colour'] = '--colo[u]r when to use terminal colours (always, auto, never)' | |
$helpText['--color-scale'] = '--colo[u]r-scale highlight levels of ''field'' distinctly (all, age, size)' | |
$helpText['--colour-scale'] = '--colo[u]r-scale highlight levels of ''field'' distinctly (all, age, size)' | |
$helpText['--color-scale-mode'] = '--colo[u]r-scale-mode use gradient or fixed colors in --color-scale (fixed, gradient)' | |
$helpText['--colour-scale-mode'] = '--colo[u]r-scale-mode use gradient or fixed colors in --color-scale (fixed, gradient)' | |
$helpText['--icons'] = '--icons when to display icons (always, auto, never)' | |
$helpText['--no-quotes'] = '--no-quotes don''t quote file names with spaces' | |
$helpText['--hyperlink'] = '--hyperlink display entries as hyperlinks' | |
$helpText['--absolute'] = '--absolute display entries with their absolute path (on, follow, off)' | |
$helpText['--follow-symlinks'] = '--follow-symlinks drill down into symbolic links that point to directories' | |
$helpText['-w'] = '-w, --width set screen width in columns' | |
$helpText['--width'] = '-w, --width set screen width in columns' | |
# Filtering and sorting options | |
$helpText['-a'] = '-a, --all show hidden and ''dot'' files' | |
$helpText['--all'] = '-a, --all show hidden and ''dot'' files' | |
$helpText['-A'] = '-A, --almost-all equivalent to --all; included for compatibility with `ls -A`' | |
$helpText['--almost-all'] = '-A, --almost-all equivalent to --all; included for compatibility with `ls -A`' | |
$helpText['-d'] = '-d, --treat-dirs-as-files list directories as files; don''t list their contents' | |
$helpText['--treat-dirs-as-files'] = '-d, --treat-dirs-as-files list directories as files; don''t list their contents' | |
$helpText['-D'] = '-D, --only-dirs list only directories' | |
$helpText['--only-dirs'] = '-D, --only-dirs list only directories' | |
$helpText['-f'] = '-f, --only-files list only files' | |
$helpText['--only-files'] = '-f, --only-files list only files' | |
$helpText['--show-symlinks'] = '--show-symlinks explicitly show symbolic links' | |
$helpText['--no-symlinks'] = '--no-symlinks do not show symbolic links' | |
$helpText['-L'] = '-L, --level limit the depth of recursion' | |
$helpText['--level'] = '-L, --level limit the depth of recursion' | |
$helpText['-r'] = '-r, --reverse reverse the sort order' | |
$helpText['--reverse'] = '-r, --reverse reverse the sort order' | |
$helpText['-s'] = '-s, --sort which field to sort by' | |
$helpText['--sort'] = '-s, --sort which field to sort by' | |
$helpText['--group-directories-first'] = '--group-directories-first list directories before other files' | |
$helpText['--group-directories-last'] = '--group-directories-last list directories after other files' | |
$helpText['-I'] = '-I, --ignore-glob glob patterns (pipe-separated) of files to ignore' | |
$helpText['--ignore-glob'] = '-I, --ignore-glob glob patterns (pipe-separated) of files to ignore' | |
$helpText['--git-ignore'] = '--git-ignore ignore files mentioned in ''.gitignore''' | |
# Long view options | |
$helpText['-b'] = '-b, --binary list file sizes with binary prefixes' | |
$helpText['--binary'] = '-b, --binary list file sizes with binary prefixes' | |
$helpText['-B'] = '-B, --bytes list file sizes in bytes, without any prefixes' | |
$helpText['--bytes'] = '-B, --bytes list file sizes in bytes, without any prefixes' | |
$helpText['-g'] = '-g, --group list each file''s group' | |
$helpText['--group'] = '-g, --group list each file''s group' | |
$helpText['--smart-group'] = '--smart-group only show group if it has a different name from owner' | |
$helpText['-h'] = '-h, --header add a header row to each column' | |
$helpText['--header'] = '-h, --header add a header row to each column' | |
$helpText['-H'] = '-H, --links list each file''s number of hard links' | |
$helpText['--links'] = '-H, --links list each file''s number of hard links' | |
$helpText['-i'] = '-i, --inode list each file''s inode number' | |
$helpText['--inode'] = '-i, --inode list each file''s inode number' | |
$helpText['-M'] = '-M, --mounts show mount details (Linux and Mac only)' | |
$helpText['--mounts'] = '-M, --mounts show mount details (Linux and Mac only)' | |
$helpText['-n'] = '-n, --numeric list numeric user and group IDs' | |
$helpText['--numeric'] = '-n, --numeric list numeric user and group IDs' | |
$helpText['-O'] = '-O, --flags list file flags (Mac, BSD, and Windows only)' | |
$helpText['--flags'] = '-O, --flags list file flags (Mac, BSD, and Windows only)' | |
$helpText['-S'] = '-S, --blocksize show size of allocated file system blocks' | |
$helpText['--blocksize'] = '-S, --blocksize show size of allocated file system blocks' | |
$helpText['-t'] = '-t, --time which timestamp field to list (modified, accessed, created)' | |
$helpText['--time'] = '-t, --time which timestamp field to list (modified, accessed, created)' | |
$helpText['--time-style'] = '--time-style how to format timestamps (default, iso, long-iso, full-iso, relative, or custom)' | |
$helpText['-m'] = '-m, --modified use the modified timestamp field' | |
$helpText['--modified'] = '-m, --modified use the modified timestamp field' | |
$helpText['-u'] = '-u, --accessed use the accessed timestamp field' | |
$helpText['--accessed'] = '-u, --accessed use the accessed timestamp field' | |
$helpText['-U'] = '-U, --created use the created timestamp field' | |
$helpText['--created'] = '-U, --created use the created timestamp field' | |
$helpText['--changed'] = '--changed use the changed timestamp field' | |
$helpText['--total-size'] = '--total-size show the size of a directory as the size of all files and directories inside' | |
$helpText['--no-total-size'] = '--no-total-size do not show the total size of directories' | |
$helpText['--no-permissions'] = '--no-permissions suppress the permissions field' | |
$helpText['--no-filesize'] = '--no-filesize suppress the filesize field' | |
$helpText['--no-user'] = '--no-user suppress the user field' | |
$helpText['--no-time'] = '--no-time suppress the time field' | |
$helpText['--stdin'] = '--stdin read file names from stdin, one per line' | |
$helpText['--git'] = '--git list each file''s Git status, if tracked or ignored' | |
$helpText['--no-git'] = '--no-git suppress Git status (always overrides --git)' | |
$helpText['--git-repos'] = '--git-repos list root of git-tree status' | |
$helpText['--git-repos-no-status'] = '--git-repos-no-status list each git-repos branch name (much faster)' | |
# ─── Context-aware options ──────────────────────────────────────────────── | |
# 'Name' and 'Extension' are for case-sensitive sorting. | |
$fileSortOptions = @( | |
'name', 'Name', 'extension', 'Extension', 'size', 'type', | |
'created', 'modified', 'accessed', 'changed', 'inode', 'none', | |
'date', 'time', 'old', 'new' | |
) | |
$timeSortOptions = @( | |
'modified', 'accessed', 'created', 'changed') | |
$timeStyles = @( | |
'default', 'iso', 'long-iso', 'full-iso', 'relative', 'custom' | |
) | |
$colorAndIconOptions = @( | |
'always', 'auto', 'never' | |
) | |
$colorScaleOptions = @( | |
'all', 'age', 'size' | |
) | |
$colorScaleModeOptions = @( | |
'fixed', 'gradient' | |
) | |
# ─── Context-aware values ───────────────────────────────────────────────── | |
$valueTable = New-Object 'System.Collections.Hashtable' ([System.StringComparer]::Ordinal) | |
$valueTable['--absolute'] = @('on', 'follow', 'off') | |
$valueTable['--color'] = $colorAndIconOptions | |
$valueTable['--colour'] = $colorAndIconOptions | |
$valueTable['--icons'] = $colorAndIconOptions | |
$valueTable['--color-scale'] = $colorScaleOptions | |
$valueTable['--colour-scale'] = $colorScaleOptions | |
$valueTable['--color-scale-mode'] = $colorScaleModeOptions | |
$valueTable['--colour-scale-mode'] = $colorScaleModeOptions | |
$valueTable['--level'] = (1..9 | ForEach-Object { "$_" }) | |
$valueTable['--sort'] = $fileSortOptions | |
$valueTable['--time'] = $timeSortOptions | |
$valueTable['--time-style'] = $timeStyles | |
$valueTable['--width'] = @() # No specific values, but expects one. | |
$valueTable['--ignore-glob'] = @() # Expects a value. | |
$valueTable['-s'] = $fileSortOptions | |
$valueTable['-t'] = $timeSortOptions | |
$valueTable['-L'] = (1..9 | ForEach-Object { "$_" }) | |
$valueTable['-w'] = @() | |
$valueTable['-I'] = @() | |
# ─── every recognised switch ────────────────────────────────────────────── | |
$switches = @( | |
# Meta options | |
'-?', '--help', | |
'-v', '--version', | |
# Display options | |
'-1', '--oneline', | |
'-l', '--long', | |
'-G', '--grid', | |
'-x', '--across', | |
'-R', '--recurse', | |
'-T', '--tree', | |
'-X', '--dereference', | |
'-F', '--classify', | |
'--no-quotes', | |
'--hyperlink', | |
'--follow-symlinks', | |
# Filtering and sorting options | |
'-a', '--all', | |
'-A', '--almost-all', | |
'-d', '--treat-dirs-as-files', | |
'-D', '--only-dirs', | |
'-f', '--only-files', | |
'--show-symlinks', | |
'--no-symlinks', | |
'-r', '--reverse', | |
'--group-directories-first', | |
'--group-directories-last', | |
'--git-ignore', | |
# Long view options | |
'-b', '--binary', | |
'-B', '--bytes', | |
'-g', '--group', | |
'--smart-group', | |
'-h', '--header', | |
'-H', '--links', | |
'-i', '--inode', | |
'-M', '--mounts', | |
'-n', '--numeric', | |
'-O', '--flags', | |
'-S', '--blocksize', | |
'-m', '--modified', | |
'-u', '--accessed', | |
'-U', '--created', | |
'--changed', | |
'--total-size', | |
'--no-total-size', | |
'--no-permissions', | |
'--no-filesize', | |
'--no-user', | |
'--no-time', | |
'--stdin', | |
'--git', | |
'--no-git', | |
'--git-repos', | |
'--git-repos-no-status' | |
) | |
# ──── figure out what the previous token was ────────────────────────────── | |
$elements = $commandAst.CommandElements | |
$prevToken = $null | |
if ($elements.Count -gt 1) { | |
# Get the last two elements. | |
$lastElement = $elements[-1] | |
$penultimateElement = if ($elements.Count -gt 1) { $elements[-2] } else { $null } | |
function Get-ElementValue($element) { | |
if ($null -eq $element) { return $null } | |
switch ($element) { | |
{ $_ -is [StringConstantExpressionAst] } { return $_.Value } | |
{ $_ -is [CommandParameterAst] } { return "-" + $_.ParameterName } | |
default { return $_.ToString() } | |
} | |
} | |
# If the cursor is at the end of the line after one or more spaces when | |
# <tab> is hit, then PowerShell treats this trailing whitespace as a | |
# StringConstantExpressionAst which has a Value property of "", | |
# therefore $lastElement and $wordToComplete (the token currently under | |
# the cursor) represent empty strings. That means we need to grab the | |
# penultimate element instead of the last element when hitting <tab> | |
# after a space. | |
# | |
# E.g., "eza -s<space><tab>" -> $lastElement.Value = "" | |
# $wordToComplete = "" | |
# $penultimateElement = "-s" | |
# | |
# Otherwise, the last element would be "-s" and so we select that. | |
if ($lastElement -is [StringConstantExpressionAst] -and $lastElement.Value -eq $wordToComplete) { | |
$prevToken = Get-ElementValue $penultimateElement | |
} else { | |
# This handles completing a value when there's no space after the parameter, e.g., "eza -s<tab>" | |
$prevToken = Get-ElementValue $lastElement | |
} | |
} | |
# ──── value completion ──────────────────────────────────────────────────── | |
if ($prevToken -and $valueTable.ContainsKey($prevToken)) { | |
$valueMatches = $valueTable[$prevToken] | | |
Where-Object { $_ -like "$wordToComplete*" } | |
if ($valueMatches) { | |
$valueMatches | | |
ForEach-Object { [CompletionResult]::new($_, $_, 'ParameterValue', $_) } | |
return # only return when we have value completions | |
} | |
} | |
# ──── switch completion (plus fallback to files) ────────────────────────── | |
$switchMatches = $switches + $valueTable.Keys | | |
Where-Object { $_ -like "$wordToComplete*" } | | |
Sort-Object { | |
# Sort short options first (starting with single dash), then long options | |
if ($_ -match '^-[^-]') { "0$_" } else { "1$_" } | |
} | |
foreach ($s in $switchMatches) { | |
# Use help text as the tooltip if available | |
$tooltip = if ($helpText.ContainsKey($s)) { $helpText[$s] } else { $s } | |
[CompletionResult]::new($s, $s, 'ParameterName', $tooltip) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment