Skip to content

Commit

Permalink
feat: Add native shell completion (#47)
Browse files Browse the repository at this point in the history
- Closes #24
  • Loading branch information
Ash258 committed Jun 28, 2020
1 parent c7ea0f3 commit 3ccb82d
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 13 deletions.
14 changes: 14 additions & 0 deletions lib/Diagnostic.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,17 @@ function Test-Config {

return $result
}

function Test-CompletionRegistered {
$result = (Get-Content $PROFILE) -like '*\supporting\completion*'
if (!$result) {
$path = Join-Path $PSScriptRoot '..\supporting\completion\Scoop-Completion.psd1' -Resolve
Write-UserMessage -Message 'Automatic completion module is not imported in $PROFILE' -Warning
Write-UserMessage -Message @(
' Consider importing module for automatic commands/parameters completion'
" Add-Content `$PROFILE 'Import-Module ''$path'' -ErrorAction SilentlyContinue'"
)
}

return $result
}
2 changes: 1 addition & 1 deletion libexec/scoop-alias.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Usage: scoop alias add|list|rm [<args>]
# Usage: scoop alias [add|list|rm] [<args>]
# Summary: Manage scoop aliases
# Help: Add, remove or list Scoop aliases
#
Expand Down
2 changes: 1 addition & 1 deletion libexec/scoop-bucket.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Usage: scoop bucket add|list|known|rm [<args>]
# Usage: scoop bucket [add|list|known|rm] [<args>]
# Summary: Manage Scoop buckets
# Help: Add, list or remove buckets.
#
Expand Down
2 changes: 1 addition & 1 deletion libexec/scoop-cache.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Usage: scoop cache show|rm [app]
# Usage: scoop cache [rm|show] [app]
# Summary: Show or clear the download cache
# Help: Scoop caches downloads so you don't need to download the same files
# when you uninstall and re-install the same version of an app.
Expand Down
1 change: 1 addition & 0 deletions libexec/scoop-checkup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ $issues += !(Test-EnvironmentVariable)
$issues += !(Test-HelpersInstalled)
$issues += !(Test-Drive)
$issues += !(Test-Config)
$issues += !(Test-CompletionRegistered)

if ($issues -gt 0) {
Write-UserMessage -Message "Found $issues potential $(pluralize $issues 'problem' 'problems')." -Warning
Expand Down
1 change: 0 additions & 1 deletion libexec/scoop-export.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Usage: scoop export > filename
# Summary: Exports (an importable) list of installed apps
# Help: Lists all installed apps.

'core', 'Versions', 'manifest', 'buckets' | ForEach-Object {
. (Join-Path $PSScriptRoot "..\lib\$_.ps1")
Expand Down
9 changes: 9 additions & 0 deletions supporting/completion/Scoop-Completion.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@{
'ModuleVersion' = '1.0.0'
'Description' = 'A Scoop tab completion module for PowerShell'
'GUID' = '915c2217-407e-430a-b34b-2e1d82dc6511'
'PowerShellVersion' = '5.0'
'Author' = 'Jakub Čábera <cabera.jakub@gmail.com>'
'RootModule' = 'Scoop-Completion.psm1'
'FunctionsToExport' = @('TabExpansion')
}
253 changes: 253 additions & 0 deletions supporting/completion/Scoop-Completion.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
if (!((Get-Command 'scoop' -ErrorAction SilentlyContinue) -or (Get-Command 'shovel' -ErrorAction SilentlyContinue))) {
Write-Error 'Scoop is not installed'
exit 1
}

$script:SCOOP_CONFIG = scoop config show | ConvertFrom-Json
$script:SCOOP_ALL_ALIASES = $SCOOP_CONFIG.'alias'
$script:SCOOP_DIRECTORY = $env:SCOOP, $SCOOP_CONFIG.'rootPath', "$env:USERPROFILE\scoop" | Where-Object { ![String]::IsNullOrEmpty($_) } | Select-Object -First 1
$script:SCOOP_COMMANDS = @(
'alias'
'bucket'
'cache'
'cat'
'checkup'
'cleanup'
'config'
'depends'
'download'
'export'
'help'
'hold'
'home'
'info'
'install'
'list'
'prefix'
'reset'
'search'
'status'
'unhold'
'uninstall'
'update'
'virustotal'
'which'
)
$script:SCOOP_SUB_COMMANDS = @{
'alias' = 'add list rm'
'bucket' = 'add known list rm'
'cache' = 'rm show'
'config' = 'rm show'
}
$script:SCOOP_SHORT_PARAMETERS = @{
'cleanup' = 'g k'
'download' = 's u a b'
'hold' = 'g'
'install' = 'g i k s a'
'list' = 'i u r'
'unhold' = 'g'
'uninstall' = 'g p'
'update' = 'f g i k s q'
'virustotal' = 'a s n'
}
$script:SCOOP_LONG_PARAMETERS = @{
'cleanup' = 'global cache'
'download' = 'skip utility arch all-architectures'
'hold' = 'global'
'install' = 'global independent no-cache skip arch'
'list' = 'installed updated reverse'
'unhold' = 'global'
'uninstall' = 'global purge'
'update' = 'force global independent no-cache skip quiet'
'virustotal' = 'arch scan no-depends'
}
$script:SCOOP_PARAMETER_VALUES = @{
'install' = @{
'a' = '32bit 64bit'
'arch' = '32bit 64bit'
}
'download' = @{
'a' = '32bit 64bit'
'arch' = '32bit 64bit'
'u' = 'native aria2'
'utility' = 'native aria2'
}
'virustotal' = @{
'a' = '32bit 64bit'
'arch' = '32bit 64bit'
}
}

$script:REGEX_SHORT_PARAMETERS = $SCOOP_SHORT_PARAMETERS.Keys -join '|'
$script:REGEX_LONG_PARAMETERS = $SCOOP_LONG_PARAMETERS.Keys -join '|'
$script:REGEX_PARAMETERS_VALUES = $SCOOP_PARAMETER_VALUES.Keys -join '|'

#region Helpers
function script:Expand-ScoopLongParameter($Cmd, $Filter) {
return ($SCOOP_LONG_PARAMETERS[$Cmd] -split ' ') -like "$Filter*" | Sort-Object | ForEach-Object { "--$_" }
}

function script:Expand-ScoopShortParameter($Cmd, $Filter) {
return ($SCOOP_SHORT_PARAMETERS[$Cmd] -split ' ') -like "$Filter*" | Sort-Object | ForEach-Object { "-$_" }
}

function script:Expand-ScoopParametersValue($Cmd, $Param, $Filter) {
return ($SCOOP_PARAMETER_VALUES[$Cmd][$Param] -split ' ') -like "$Filter*" | Sort-Object
}

function script:Get-ScoopAlias($Filter) {
$res = $()
if ($null -ne $SCOOP_ALL_ALIASES) {
$res = @(($SCOOP_ALL_ALIASES.PSObject.Properties.Name) -like "$Filter*")
}

return $res
}

function script:New-AllScoopAlias {
$al = @()

'scoop', 'shovel' | ForEach-Object {
$al += $_, "$_\.ps1", "$_\.cmd"
$al += @(Get-Alias | Where-Object -Property Definition -EQ -Value $_ | Select-Object -ExpandProperty Name)
}

return $al -join '|'
}

function script:Expand-ScoopCommandParameter($Commands, $Command, $Filter) {
return ($Commands.$Command -split ' ') -like "$Filter*"
}

function script:Expand-ScoopCommand($Filter, [Switch] $IncludeAlias) {
$cmdList = $SCOOP_COMMANDS
if ($IncludeAlias) { $cmdList += Get-ScoopAlias($Filter) }

return $cmdList -like "$Filter*" | Sort-Object
}

function script:Get-LocallyInstalledApplicationsByScoop($Filter) {
return (Get-ChildItem $SCOOP_DIRECTORY 'apps\*' -Exclude 'scoop' -Directory -Name) -like "$Filter*"
}

function script:Get-LocallyAvaialableApplicationsByScoop($Filter) {
$buckets = Get-ChildItem $SCOOP_DIRECTORY 'buckets\*' -Directory

$manifests = @()
foreach ($buc in $buckets) {
$manifests += Get-ChildItem $buc.FullName 'bucket\*' -File | Select-Object -ExpandProperty BaseName
}

return ($manifests | Select-Object -Unique) -like "$Filter*"
}

function script:Get-ScoopCachedFile($Filter) {
$files = Get-ChildItem $SCOOP_DIRECTORY 'cache\*' -File -Name

$res = @()
foreach ($f in $files) { $res += ($f -split '#')[0] }

return ($res | Select-Object -Unique) -like "$Filter*"
}

function script:Get-LocallyAddedBucket($Filter) {
return (Get-ChildItem $SCOOP_DIRECTORY 'buckets\*' -Directory -Name) -like "$Filter*"
}

function script:Get-AvailableBucket($Filter) {
return @((scoop bucket known) -like "$Filter*")
}
#endregion Helpers

function script:ScoopTabExpansion($LastBlock) {
switch -Regex ($LastBlock) {
# Handles Scoop <cmd> --<param> <value>
"^(?<cmd>$REGEX_PARAMETERS_VALUES).* --(?<param>.+) (?<value>\w*)$" {
if ($SCOOP_PARAMETER_VALUES[$Matches['cmd']][$Matches['param']]) {
return Expand-ScoopParametersValue $Matches['cmd'] $Matches['param'] $Matches['value']
}
}

# Handles Scoop <cmd> -<shortparam> <value>
"^(?<cmd>$REGEX_PARAMETERS_VALUES).* -(?<param>.+) (?<value>\w*)$" {
if ($SCOOP_PARAMETER_VALUES[$Matches['cmd']][$Matches['param']]) {
return Expand-ScoopParametersValue $Matches['cmd'] $Matches['param'] $Matches['value']
}
}

# Handles uninstall package names
'^(cleanup|hold|prefix|reset|uninstall|update|virustotal|unhold)\s+(?:.+\s+)?(?<package>[\w][\-.\w]*)?$' {
return Get-LocallyInstalledApplicationsByScoop $Matches['package']
}

# Handles install package names
'^(cat|depends|download|info|install|home)\s+(?:.+\s+)?(?<package>[\w][\-.\w]*)?$' {
return Get-LocallyAvaialableApplicationsByScoop $Matches['package']
}

# Handles cache (rm/show) cache names
'^cache (rm|show)\s+(?:.+\s+)?(?<cache>[\w][\-.\w]*)?$' {
return Get-ScoopCachedFile $Matches['cache']
}

# Handles bucket rm bucket names
'^bucket rm\s+(?:.+\s+)?(?<bucket>[\w][\-.\w]*)?$' {
return Get-LocallyAddedBucket $Matches['bucket']
}

# Handles bucket add bucket names
'^bucket add\s+(?:.+\s+)?(?<bucket>[\w][\-.\w]*)?$' {
return Get-AvailableBucket $Matches['bucket']
}

# Handles alias rm alias names
'^alias rm\s+(?:.+\s+)?(?<alias>[\w][\-\.\w]*)?$' {
return Get-ScoopAlias $Matches['alias']
}

# Handles Scoop help <cmd>
'^help (?<cmd>\S*)$' {
return Expand-ScoopCommand $Matches['cmd']
}

# Handles Scoop <cmd> <subcmd>
"^(?<cmd>$($SCOOP_SUB_COMMANDS.Keys -join '|'))\s+(?<op>\S*)$" {
return Expand-ScoopCommandParameter $SCOOP_SUB_COMMANDS $Matches['cmd'] $Matches['op']
}

# Handles Scoop <cmd>
'^(?<cmd>\S*)$' {
return Expand-ScoopCommand $Matches['cmd'] -IncludeAlias
}

# Handles Scoop <cmd> --<param>
"^(?<cmd>$REGEX_LONG_PARAMETERS).* --(?<param>\S*)$" {
return Expand-ScoopLongParameter $Matches['cmd'] $Matches['param']
}

# Handles Scoop <cmd> -<shortparam>
"^(?<cmd>$REGEX_SHORT_PARAMETERS).* -(?<shortparam>\S*)$" {
return Expand-ScoopShortParameter $Matches['cmd'] $Matches['shortparam']
}
}
}

# Rename already hooked TabExpansion
if (Test-Path Function:\TabExpansion) { Rename-Item Function:\TabExpansion TabExpansion_Scoop_Backup }

function TabExpansion($Line, $LastWord) {
<#
.SYNOPSIS
Handle tab completion of all scoop|shovel commands
#>
$lastBlock = [Regex]::Split($Line, '[|;]')[-1].TrimStart()

switch -Regex ($lastBlock) {
# https://regex101.com/r/COrwSO
"^(sudo\s+)?((\.[\\\/])?bin[\\\/])?(($(New-AllScoopAlias)))\s+(?<rest>.*)$" { ScoopTabExpansion $Matches['rest'] }

default { if (Test-Path Function:\TabExpansion_Scoop_Backup) { TabExpansion_Scoop_Backup $Line $LastWord } }
}
}

Export-ModuleMember -Function 'TabExpansion'
9 changes: 5 additions & 4 deletions test/00-Project.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ $repo_dir = (Get-Item $MyInvocation.MyCommand.Path).Directory.Parent.FullName
$repo_files = @( Get-ChildItem $repo_dir -file -recurse -force )

$project_file_exclusions = @(
$([regex]::Escape($repo_dir) + '(\\|/).git(\\|/).*$'),
'.sublime-workspace$',
'.DS_Store$',
'supporting(\\|/)validator(\\|/)packages(\\|/)*',
([Regex]::Escape($repo_dir) + '(\\|/).git(\\|/).*$')
'.sublime-workspace$'
'.DS_Store$'
'supporting(\\|/)validator(\\|/)packages(\\|/)*'
'supporting(\\|/)shimexe(\\|/)packages(\\|/)*'
'supporting(\\|/)yaml*'
)

Describe 'Project code' {
Expand Down
6 changes: 3 additions & 3 deletions test/Import-File-Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Describe 'Style constraints for non-binary project files' {
$files = @(
# gather all files except '*.exe', '*.zip', or any .git repository files
$repo_files |
Where-Object { $_.fullname -inotmatch $($project_file_exclusions -join '|') } |
Where-Object { $_.fullname -inotmatch '(.exe|.zip|.dll)$' } |
Where-Object { $_.fullname -inotmatch '(unformated)' }
Where-Object { $_.FullName -inotmatch $($project_file_exclusions -join '|') } |
Where-Object { $_.FullName -inotmatch '(.exe|.zip|.dll)$' } |
Where-Object { $_.FullName -inotmatch '(unformated)' }
)

$files_exist = ($files.Count -gt 0)
Expand Down
2 changes: 1 addition & 1 deletion test/Scoop-Config.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Describe "config" -Tag 'Scoop' {
}

It "get_config should return exactly the same values" {
$scoopConfig = ConvertFrom-Json $json
$SCOOP_CONFIGURATION = ConvertFrom-Json $json
get_config 'does_not_exist' 'default' | Should -Be 'default'

get_config 'one' | Should -BeExactly 1
Expand Down
2 changes: 1 addition & 1 deletion test/Scoop-Core.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Describe "shim" -Tag 'Scoop' {
}

AfterEach {
rm_shim "shim-test" $shimdir
rm_shim "shim-test" $shimdir 6>&1 | Out-Null
}
}

Expand Down

0 comments on commit 3ccb82d

Please sign in to comment.