Skip to content

Commit ef1ed93

Browse files
Added PowerShell command completion solving #261
1 parent e36f9b6 commit ef1ed93

12 files changed

Lines changed: 868 additions & 6 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# cli completion pwsh setup
2+
3+
Sets up command completion for PowerShell
4+
5+
## Usage
6+
7+
```sh
8+
cli completion pwsh setup [options]
9+
```
10+
11+
## Options
12+
13+
Option|Description
14+
------|-----------
15+
`--help`|output usage information
16+
`-p, --profile <profile>`|Path to the PowerShell profile file
17+
`--query [query]`|JMESPath query string. See [http://jmespath.org/](http://jmespath.org/) for more information and examples
18+
`-o, --output [output]`|Output type. `json|text`. Default `text`
19+
`--verbose`|Runs command with verbose logging
20+
`--debug`|Runs command with debug logging
21+
22+
## Remarks
23+
24+
This commands sets up command completion for the Office 365 CLI in PowerShell by registering a custom PowerShell argument completer in the specified profile. Because Office 365 CLI is not a native PowerShell module, it requires a custom completer to provide completion when used in non-immersive mode.
25+
26+
If the specified profile path doesn't exist, the CLI will try to create it.
27+
28+
## Examples
29+
30+
Set up command completion for PowerShell using the profile from the profile
31+
variable
32+
33+
```powershell
34+
cli completion pwsh setup --profile $profile
35+
```
36+
37+
## More information
38+
39+
- Command completion: [https://pnp.github.io/office365-cli/concepts/completion/](https://pnp.github.io/office365-cli/concepts/completion/)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# cli completion pwsh update
2+
3+
Updates command completion for PowerShell
4+
5+
## Usage
6+
7+
```sh
8+
cli completion pwsh update [options]
9+
```
10+
11+
## Options
12+
13+
Option|Description
14+
------|-----------
15+
`--help`|output usage information
16+
`--query [query]`|JMESPath query string. See [http://jmespath.org/](http://jmespath.org/) for more information and examples
17+
`-o, --output [output]`|Output type. `json|text`. Default `text`
18+
`--verbose`|Runs command with verbose logging
19+
`--debug`|Runs command with debug logging
20+
21+
## Remarks
22+
23+
This commands updates the list of commands and their options used by command completion in PowerShell. You should run this command each time after upgrading the Office 365 CLI.
24+
25+
## Examples
26+
27+
Update list of commands for PowerShell command completion
28+
29+
```powershell
30+
cli completion pwsh update
31+
```
32+
33+
## More information
34+
35+
- Command completion: [https://pnp.github.io/office365-cli/concepts/completion/](https://pnp.github.io/office365-cli/concepts/completion/)

docs/manual/docs/concepts/completion.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ To enable completion:
2525

2626
You should now be able to complete your input, eg. typing `o365 s<tab>` will complete it to `o365 spo` and typing `o365 spo <tab><tab>` will list all SharePoint Online commands available in Office 365 CLI. To see the options available for the current command, type `-<tab><tab>`, for example `o365 spo app list -<tab><tab>` will list all options available for the `o365 spo app list` command.
2727

28-
#### Disable Clink completion
29-
30-
To disable completion, delete the `o365.lua` file you generated previously and restart your shell.
31-
3228
#### Update Clink completion
3329

3430
Command completion is based on a static file. After updating the Office 365 CLI, you should update the completion file as described in the [Enable completion](#enable-clink-completion) section so that the completion file reflects the latest commands in the Office 365 CLI.
3531

32+
#### Disable Clink completion
33+
34+
To disable completion, delete the `o365.lua` file you generated previously and restart your shell.
35+
3636
### Zsh, Bash and Fish
3737

3838
If you're using Zsh, Bash or Fish as your shell, you can benefit of Office 365 CLI command completion as well, when typing commands directly in the shell. The completion is based on the [Omelette](https://www.npmjs.com/package/omelette) package.
@@ -49,6 +49,10 @@ To enable completion:
4949

5050
You should now be able to complete your input, eg. typing `o365 s<tab>` will complete it to `o365 spo` and typing `o365 spo <tab><tab>` will list all SharePoint Online commands available in Office 365 CLI. To see the options available for the command, type `-<tab><tab>`, for example `o365 spo app list -<tab><tab>` will list all options available for the `o365 spo app list` command. If the command is completed, the completion will automatically start suggestions with a `-` indicating that you have matched a command and can now specify its options. Command options you've already used are removed from the suggestions list, but the completion doesn't take into account short and long variant of the same option. If you specified the `--output` option in your command, `--option` will not be displayed in the list of suggestions, but `-o` will.
5151

52+
#### Update sh completion
53+
54+
Command completion is based on the static `commands.json` file located in the folder where the Office 365 CLI is installed. After updating the Office 365 CLI, you should update the completion file by executing `o365 --completion:sh:generate` in the command line. After running this command, it's not necessary to restart the shell to see the latest changes.
55+
5256
#### Disable sh completion
5357

5458
To disable completion, edit your shell's profile file (for Zsh `~/.zshrc`) and remove the following lines:
@@ -61,6 +65,24 @@ To disable completion, edit your shell's profile file (for Zsh `~/.zshrc`) and r
6165

6266
Save the profile file and restart the shell for the changes to take effect.
6367

64-
#### Update sh completion
68+
### PowerShell
6569

66-
Command completion is based on the static `commands.json` file located in the folder where the Office 365 CLI is installed. After updating the Office 365 CLI, you should update the completion file by executing `o365 --completion:sh:generate` in the command line. After running this command, it's not necessary to restart the shell to see the latest changes.
70+
If you use Office 365 CLI in PowerShell you can use the custom argument completer provided with the Office 365 CLI to get command completion when typing commands directly in the shell.
71+
72+
#### Enable PowerShell completion
73+
74+
To enable completion in your current PowerShell profile:
75+
76+
1. Start PowerShell
77+
1. Execute `o365 cli completion pwsh setup --profile $profile`. This will generate the `commands.json` file in the same folder where the Office 365 CLI is installed, listing all available commands and their options. Additionally, it will register completion in your PowerShell profile
78+
1. Restart PowerShell
79+
80+
You should now be able to complete your input, eg. typing `o365 s<tab>` will complete it to `o365 spo` and typing `o365 spo <tab><tab>` will list all SharePoint Online commands available in Office 365 CLI. To see the options available for the command, type `-<tab><tab>`, for example `o365 spo app list -<tab><tab>` will list all options available for the `o365 spo app list` command. If the command is completed, the completion will automatically start suggestions with a `-` indicating that you have matched a command and can now specify its options. Command options you've already used are removed from the suggestions list, but the completion doesn't take into account short and long variant of the same option. If you specified the `--output` option in your command, `--option` will not be displayed in the list of suggestions, but `-o` will.
81+
82+
#### Update PowerShell completion
83+
84+
Command completion is based on the static `commands.json` file located in the folder where the Office 365 CLI is installed. After updating the Office 365 CLI, you should update the completion file by executing `o365 cli completion pwsh update` in the command line. After running this command, it's not necessary to restart PowerShell to see the latest changes.
85+
86+
#### Disable PowerShell completion
87+
88+
To disable Office 365 CLI command completion in your PowerShell profile, open the profile file in a code editor, and remove the reference to the `Register-O365CLICompletion.ps1` script. Restart PowerShell for the changes to take effect.

docs/manual/mkdocs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ nav:
5353
- user:
5454
- user get: 'cmd/aad/user/user-get.md'
5555
- user list: 'cmd/aad/user/user-list.md'
56+
- CLI (cli):
57+
- completion:
58+
- completion pwsh setup: 'cmd/cli/completion/completion-pwsh-setup.md'
59+
- completion pwsh update: 'cmd/cli/completion/completion-pwsh-update.md'
5660
- Microsoft Flow (flow):
5761
- disable: 'cmd/flow/disable.md'
5862
- enable: 'cmd/flow/enable.md'
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
function Office365Completion {
2+
param($commandName, $wordToComplete, $cursorPosition)
3+
4+
$commands = Get-Content $(Join-Path $PSScriptRoot ".." "commands.json" -Resolve) | ConvertFrom-Json
5+
$command = $commands
6+
$parent = $commands
7+
$replies = @{ }
8+
9+
# split what's been typed by the user into words. First word is o365|office365
10+
# which we can skip
11+
[string[]]$allWords = $wordToComplete.ToString().Split(" ", [StringSplitOptions]::RemoveEmptyEntries) | Select-Object -Skip 1
12+
13+
# if nothing was typed yet, return all top-level commands
14+
if ($null -eq $allWords -or $allWords.Count -eq 0) {
15+
$replies = $parent.psobject.properties | ForEach-Object { $_.Name }
16+
$replies | sort
17+
return
18+
}
19+
20+
# number of the first word in the array that hasn't been matched with a command
21+
$wordNotMatched = 0
22+
do {
23+
$word = $allWords[$wordNotMatched]
24+
if ($word.StartsWith("-") -eq $true) {
25+
break
26+
}
27+
28+
$parentCollection = if ($parent.Value) { $parent.Value.psobject.properties } else { $parent.psobject.properties }
29+
$parent = $parentCollection | Where-Object { $_.Name -eq $word }
30+
if ($null -ne $parent) {
31+
$command = $parent
32+
$wordNotMatched++
33+
}
34+
else {
35+
break
36+
}
37+
} until ($wordNotMatched -gt $allWords.Count - 1)
38+
39+
$collection = if ($command.Value) { $command.Value.psobject.properties } else { $command.psobject.properties }
40+
$replies = $collection | ForEach-Object {
41+
$_.Name
42+
}
43+
44+
# check if we matched the whole string or not, if we haven't we need to filter
45+
# the list of suggestions with only those that match the last partial string
46+
if ($wordNotMatched -lt $allWords.Length) {
47+
# filter the list of suggested commands only if the first unmatched word is
48+
# not an option
49+
$notMatchedWord = $allWords[$wordNotMatched]
50+
if ($notMatchedWord.StartsWith("-") -ne $true) {
51+
# since we didn't match the whole string, let's trim suggestions to match
52+
# the partial string, eg. `spo site l` > list
53+
$replies = $replies | Where-Object { $_ -Like "$($allWords[$wordNotMatched])*" }
54+
}
55+
}
56+
57+
# check if the last word is an option
58+
$lastWord = $allWords[$allWords.Count - 1]
59+
if ($lastWord.StartsWith("-") -eq $true) {
60+
$option = $command.Value.psobject.properties | Where-Object { $_.Name -eq $lastWord }
61+
if ($null -ne $option) {
62+
# the option was matched. if the option is an enum, replace replies with
63+
# enum values
64+
if ($option.Value -is [System.Array]) {
65+
$replies = $option.Value | ForEach-Object { $_ }
66+
}
67+
}
68+
else {
69+
# check if there is a partial match
70+
$replies = $replies | Where-Object { $_ -Like "$($lastWord)*" }
71+
}
72+
}
73+
else {
74+
# check if the next to last word is an option
75+
$nextToLastWord = $allWords[$allWords.Count - 2]
76+
if ($nextToLastWord.StartsWith("-") -eq $true) {
77+
$option = $command.Value.psobject.properties | Where-Object { $_.Name -eq $nextToLastWord }
78+
if ($null -ne $option) {
79+
# the option was matched. if the option is an enum, check if the last word
80+
# fully matches one of the values. If it doesn't replace replies with
81+
# enum values
82+
if ($option.Value.Contains($lastWord) -ne $true) {
83+
$replies = $option.Value | Where-Object { $_ -Like "$($lastWord)*" }
84+
}
85+
}
86+
}
87+
}
88+
89+
# remove used options
90+
$replies = $replies | Where-Object { $allWords.Contains($_) -ne $true }
91+
92+
$replies | sort
93+
}
94+
95+
Register-ArgumentCompleter -Native -CommandName @("o365", "office365") -ScriptBlock $function:Office365Completion

scripts/Test-O365CLICompletion.ps1

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
. $(Join-Path . Register-O365CLICompletion.ps1)
2+
3+
$tests = @{
4+
"o365" = @("aad","accesstoken","consent","flow","graph","help","login","logout","onedrive","outlook","pa","planner","spfx","spo","status","teams","tenant","yammer");
5+
"o365 " = @("aad","accesstoken","consent","flow","graph","help","login","logout","onedrive","outlook","pa","planner","spfx","spo","status","teams","tenant","yammer");
6+
"o365 s" = @("spfx","spo","status");
7+
"o365 spo" = @("app","apppage","cdn","contenttype","contenttypehub","customaction","externaluser","feature","field","file","folder","get","hidedefaultthemes","homesite","hubsite","list","listitem","mail","navigation","orgassetslibrary","orgnewssite","page","propertybag","report","search","serviceprincipal","set","site","sitedesign","sitescript","sp","storageentity","tenant","term","theme","web");
8+
"o365 spo site" = @("add","appcatalog","classic","commsite","get","groupify","inplacerecordsmanagement","list","rename","set");
9+
"o365 spo site list" = @("--debug","--filter","--help","--output","--type","--verbose","-f","-o");
10+
"o365 b" = $null
11+
"o365 spo site list -" = @("--debug","--filter","--help","--output","--type","--verbose","-f","-o");
12+
"o365 spo site list -b" = $null;
13+
"o365 spo site list -o" = @("json","text");
14+
"o365 spo site list -o j" = @("json");
15+
"o365 spo site list --o" = @("--output");
16+
"o365 spo site list -o json" = @("--debug","--filter","--help","--output","--type","--verbose","-f");
17+
"o365 spo site list --debug" = @("--filter","--help","--output","--type","--verbose","-f","-o");
18+
}
19+
20+
$tests.Keys | ForEach-Object {
21+
Write-Host "$($_)..." -NoNewLine
22+
$completion = Office365Completion "" $_ 6
23+
if ($null -eq $completion -and $null -eq $tests.Item($_)) {
24+
Write-Host "PASSED" -ForegroundColor Green
25+
}
26+
elseif ([String]::Join(",", $completion) -eq [String]::Join(",", $tests.Item($_))) {
27+
Write-Host "PASSED" -ForegroundColor Green
28+
}
29+
else {
30+
Write-Host "FAILED" -ForegroundColor Red
31+
Write-Host " Expected: $([String]::Join(",",$tests.Item($_)))"
32+
Write-Host " Actual: $([String]::Join(",", $completion))"
33+
}
34+
}

src/o365/base/AnonymousCommand.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Command, { CommandAction, CommandArgs } from '../../Command';
2+
3+
export default abstract class AnonymousCommand extends Command {
4+
public action(): CommandAction {
5+
const cmd: Command = this;
6+
return function (this: CommandInstance, args: CommandArgs, cb: (err?: any) => void) {
7+
args = (cmd as any).processArgs(args);
8+
(cmd as any).initAction(args, this);
9+
cmd.commandAction(this, args, cb);
10+
}
11+
}
12+
}

src/o365/cli/commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const prefix: string = 'cli';
2+
3+
export default {
4+
COMPLETION_PWSH_SETUP: `${prefix} completion pwsh setup`,
5+
COMPLETION_PWSH_UPDATE: `${prefix} completion pwsh update`
6+
};

0 commit comments

Comments
 (0)