argument_completer.ps1
|
if ($PSEdition -eq "Desktop") { try { $IsWindows = $true } catch {} } # 获取具有补全脚本的 Part 列表。 function Get-FountPartListWithCompleter { param([string]$Username, [string]$PartPath) # 获取指定用户或所有用户的可用 Part 列表。 Get-FountPartList -PartPath $PartPath -Username $Username | # 筛选出存在 argument_completer.ps1 脚本的 Part。 Where-Object { $partDir = Get-FountPartDirectory -Username $Username -PartPath "$PartPath/$_" Test-Path (Join-Path -Path $partDir -ChildPath "argument_completer.ps1") -PathType Leaf } } # `run` 不带用户名时服务器会挑最后活跃的用户,补全得算出同一个人才能找到他独占的 part。 # 判定与 fount 的 getLastActiveUsername 一致:lastActiveAt 优先,回退 createdAt,全无记录时按用户目录 mtime 兜底。 function Get-FountLastActiveUsername { $fountDir = Get-FountDirectory try { $users = (Get-Content -LiteralPath "$fountDir/data/config.json" -Raw | ConvertFrom-Json).data.users.PSObject.Properties $latestName = $null $latestAt = 0 foreach ($user in $users) { $activityAt = if ($user.Value.lastActiveAt) { [double]$user.Value.lastActiveAt } else { [double]$user.Value.createdAt } if ($activityAt -gt $latestAt) { $latestAt = $activityAt $latestName = $user.Name } } if ($latestName) { return $latestName } } catch { } Get-ChildItem -LiteralPath "$fountDir/data/users" -Directory | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 -ExpandProperty Name } # 补全 partpath:逐级合并用户 part 与公共 part 里同名的子目录。 function Get-FountPartPathCompletion { param([string]$Username, [string]$WordToComplete) $slash = $WordToComplete.LastIndexOf('/') $parentPath = if ($slash -ge 0) { $WordToComplete.Substring(0, $slash) } else { '' } $prefix = $WordToComplete.Substring($slash + 1) $fountDir = Get-FountDirectory $searchRoots = @("$fountDir/src/public/parts/$parentPath") if ($Username) { $searchRoots = @("$fountDir/data/users/$Username/$parentPath") + $searchRoots } $candidates = $searchRoots | ForEach-Object { Get-ChildItem -LiteralPath $_ -Directory -ErrorAction SilentlyContinue } | Where-Object { $_.Name.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase) } | Group-Object Name | Sort-Object Name foreach ($candidate in $candidates) { $path = if ($parentPath) { "$parentPath/$($candidate.Name)" } else { $candidate.Name } # 目录自己是 part 就补完,只在后代里还有 part 时补成能继续输入的路径。 $isPart = $false $hasPartBelow = $false foreach ($directory in $candidate.Group) { if (Test-Path -LiteralPath (Join-Path $directory.FullName 'fount.json') -PathType Leaf) { $isPart = $true; break } if (Get-ChildItem -LiteralPath $directory.FullName -Filter fount.json -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1) { $hasPartBelow = $true } } if ($isPart) { [System.Management.Automation.CompletionResult]::new($path, $candidate.Name, 'ParameterValue', $path) } elseif ($hasPartBelow) { [System.Management.Automation.CompletionResult]::new("$path/", "$($candidate.Name)/", 'ParameterValue', "$path/") } } } # 移除哈希表中的指定键及其子键(递归)。 function Remove-ArgumentNode { param([hashtable]$Node, [string]$Key) # 如果哈希表中存在指定的键,则移除该键。 if ($Node.ContainsKey($Key)) { $Node.Remove($Key) } # 遍历哈希表中的所有键。 foreach ($mapperkey in $Node.Keys) { # 如果当前键对应的值是哈希表,则递归调用 Remove-ArgumentNode 函数。 if ($Node[$mapperkey] -is [hashtable]) { Remove-ArgumentNode -Node $Node[$mapperkey] -Key $Key } } } # 处理 'run' 和 'runas' 命令的参数补全。 function Invoke-RunshellCompletion { param( [string]$WordToComplete, [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition, [int]$RunIndex, # 'run' 或 'runas' 在 CommandAst 中的索引。 [int]$ArgIndex # 光标所在参数在 CommandAst 中的索引。 ) $commandElements = $CommandAst.CommandElements # 相对 run/runas 的参数序号:runas 的 1 是用户名、2 是 partpath,run 的 1 就是 partpath。 $argumentOffset = $ArgIndex - $RunIndex if ($argumentOffset -lt 1) { return } $hasExplicitUsername = $commandElements[$RunIndex].Value -eq 'runas' if ($hasExplicitUsername -and $argumentOffset -eq 1) { return Get-FountUserList | Where-Object { $_.StartsWith($WordToComplete) } } $partPathOffset = if ($hasExplicitUsername) { 2 } else { 1 } # `run` 的用户名由服务器挑,这里替他挑同一个,省得后面再分 run 和 runas。 $username = if ($hasExplicitUsername) { $commandElements[$RunIndex + 1].Value } else { Get-FountLastActiveUsername } if ($argumentOffset -eq $partPathOffset) { return Get-FountPartPathCompletion -Username $username -WordToComplete $WordToComplete } # 更靠后的参数委托给 part 自己的 argument_completer.ps1。 $partPath = $commandElements[$RunIndex + $partPathOffset].Value if (-not $partPath) { return } $partDir = Get-FountPartDirectory -Username $username -PartPath $partPath if ($partDir -and (Test-Path (Join-Path -Path $partDir -ChildPath "argument_completer.ps1") -PathType Leaf)) { & (Join-Path -Path $partDir -ChildPath "argument_completer.ps1") $username $WordToComplete $CommandAst $CursorPosition $RunIndex $ArgIndex } } # fount 的命令表:path/src/cmd 下的 CLI 子命令(default 与 protocolhandle 不用手输),加上由服务器解析的 run/runas。 $FountCommands = 'background', 'clean', 'debug', 'eval', 'geneexe', 'init', 'keepalive', 'log', 'logo', 'nop', 'open', 'reboot', 'remove', 'run', 'runas', 'server', 'shutdown', 'test', 'update', 'version' # geneexe 补全当前目录下的文件(只有 Windows 有 geneexe 命令)。 $GeneexeCompleter = { Get-ChildItem -Path "$($WordToComplete)*" -File | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'File', $_.FullName) } } # keepalive 与 debug 都只是把后面的参数转给服务器,run/runas 之后再交给 part 的补全脚本。 $DebugNode = @{ Parameters = 'run', 'runas' run = ${function:Invoke-RunshellCompletion} runas = ${function:Invoke-RunshellCompletion} } $KeepaliveNode = @{ Parameters = 'debug', 'run', 'runas' debug = $DebugNode run = ${function:Invoke-RunshellCompletion} runas = ${function:Invoke-RunshellCompletion} } # 定义 fount 工具的参数结构。 $ArgumentStructure = @{ Root = @{ # 根节点包含的参数。 Parameters = $FountCommands # 'background' 把剩下的参数交给新的 fount 进程,但不能再套一层 background。 background = @{ Parameters = $FountCommands -ne 'background' geneexe = $GeneexeCompleter init = $null # 没有补全逻辑。 keepalive = $KeepaliveNode debug = $DebugNode run = ${function:Invoke-RunshellCompletion} runas = ${function:Invoke-RunshellCompletion} } geneexe = $GeneexeCompleter init = $null keepalive = $KeepaliveNode debug = $DebugNode run = ${function:Invoke-RunshellCompletion} runas = ${function:Invoke-RunshellCompletion} } } # 如果不是 Windows 系统,则移除 'geneexe' 参数。 if (-not $IsWindows) { Remove-ArgumentNode -Node $ArgumentStructure.Root -Key 'geneexe' } # fount 工具的主参数补全函数。 function Get-FountArgumentCompletion { param( [string]$WordToComplete, [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) try { # 用 AST 元素范围算出光标落在第几个参数上,引号、空格、光标停在参数中间都算得对。 $commandElements = $CommandAst.CommandElements $ArgIndex = $commandElements.Count foreach ($index in 0..($commandElements.Count - 1)) { if ($CursorPosition -le $commandElements[$index].Extent.EndOffset) { $ArgIndex = $index break } if ($index + 1 -lt $commandElements.Count -and $CursorPosition -lt $commandElements[$index + 1].Extent.StartOffset) { $ArgIndex = $index + 1 break } } # 从参数结构的根节点开始,只走光标之前的元素,免得光标后面的参数改了当前层级。 $currentLevel = $ArgumentStructure.Root $runIndex = -1 for ($elementIndex = 1; $elementIndex -lt $ArgIndex; $elementIndex++) { $element = $commandElements[$elementIndex] if ($element -isnot [System.Management.Automation.Language.StringConstantExpressionAst]) { break } if ($currentLevel -isnot [hashtable]) { break } if ($currentLevel.ContainsKey($element.Value)) { $currentLevel = $currentLevel[$element.Value] # 走到处理脚本就把它后面的参数交给脚本自己补。 if ($currentLevel -is [scriptblock]) { $runIndex = $elementIndex break } } elseif ($currentLevel.Parameters -notcontains $element.Value) { break } } # 处理脚本负责自己的参数补齐。 if ($currentLevel -is [scriptblock]) { return & $currentLevel $WordToComplete $CommandAst $CursorPosition $runIndex $ArgIndex } # 还停在某个命令节点上时,补全它接受的参数;叶子命令(节点值是 $null)没有可补的。 if ($currentLevel -is [hashtable] -and $currentLevel.Parameters) { $currentLevel.Parameters | Where-Object { $_.StartsWith($WordToComplete) } } } catch { Write-Host "Error in Get-FountArgumentCompletion: $_" -ForegroundColor Red } } # 注册参数补全函数。 # 为 'fount'、'fount.ps1'、'fount.cmd' 和 'fount.exe' 命令注册 Get-FountArgumentCompletion 函数。 Register-ArgumentCompleter -CommandName fount -ScriptBlock ${function:Get-FountArgumentCompletion} -Native Register-ArgumentCompleter -CommandName fount.ps1 -ScriptBlock ${function:Get-FountArgumentCompletion} -Native Register-ArgumentCompleter -CommandName fount.cmd -ScriptBlock ${function:Get-FountArgumentCompletion} -Native Register-ArgumentCompleter -CommandName fount.exe -ScriptBlock ${function:Get-FountArgumentCompletion} -Native |