-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(wrangler): add shell completions (bash, zsh, fish) #11637
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
Closed
dmmulroy
wants to merge
18
commits into
cloudflare:main
from
dmmulroy:dmmulroy/wrangler-shell-completions
Closed
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
635fd18
Add wrangler shell completions
dmmulroy f352e34
PR Feedback
dmmulroy babc9cd
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy cc7844a
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy 5b6f040
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy 9976a2c
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev bd1ab58
Update packages/wrangler/src/completions/commands.ts
dmmulroy 7901bef
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy d357b41
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy 7a43633
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev 65d952a
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev d41bb93
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev b282b89
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev 9dd84f8
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy 244d2b2
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy 0dd63bf
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev 490c6d7
Merge branch 'main' into dmmulroy/wrangler-shell-completions
NuroDev 9bb2d3e
Merge branch 'main' into dmmulroy/wrangler-shell-completions
dmmulroy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| "wrangler": minor | ||
| --- | ||
|
|
||
| Add `wrangler completions` command for shell completion scripts (bash, zsh, fish) | ||
|
|
||
| Usage: | ||
|
|
||
| ```bash | ||
| # Bash | ||
| wrangler completions bash >> ~/.bashrc | ||
|
|
||
| # Zsh | ||
| wrangler completions zsh >> ~/.zshrc | ||
|
|
||
| # Fish | ||
| wrangler completions fish > ~/.config/fish/completions/wrangler.fish | ||
| ``` |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| import { execSync } from "node:child_process"; | ||
| import { describe, test } from "vitest"; | ||
| import { mockConsoleMethods } from "./helpers/mock-console"; | ||
| import { runInTempDir } from "./helpers/run-in-tmp"; | ||
| import { runWrangler } from "./helpers/run-wrangler"; | ||
|
|
||
| function shellAvailable(shell: string): boolean { | ||
| try { | ||
| execSync(`which ${shell}`, { stdio: "ignore" }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| describe("wrangler", () => { | ||
| describe("completions", () => { | ||
| const std = mockConsoleMethods(); | ||
| runInTempDir(); | ||
|
|
||
| test("should show available shells in help", async ({ expect }) => { | ||
| const result = runWrangler("completions --help"); | ||
|
|
||
| await expect(result).resolves.toBeUndefined(); | ||
| expect(std.out).toContain("Generate shell completion scripts"); | ||
| expect(std.out).toContain("wrangler completions bash"); | ||
| expect(std.out).toContain("wrangler completions zsh"); | ||
| expect(std.out).toContain("wrangler completions fish"); | ||
| }); | ||
|
|
||
| // ========================================================================= | ||
| // __complete command tests | ||
| // ========================================================================= | ||
| describe("__complete", () => { | ||
| test("should return top-level commands", async ({ expect }) => { | ||
| await runWrangler("__complete wrangler"); | ||
|
|
||
| expect(std.out).toContain("deploy\t"); | ||
| expect(std.out).toContain("dev\t"); | ||
| expect(std.out).toContain("kv\t"); | ||
| }); | ||
|
|
||
| test("should return subcommands for namespace", async ({ expect }) => { | ||
| // Empty string signals "complete after kv" | ||
| await runWrangler('__complete wrangler kv ""'); | ||
|
|
||
| expect(std.out).toContain("namespace\t"); | ||
| expect(std.out).toContain("key\t"); | ||
| }); | ||
|
|
||
| test("should filter by prefix", async ({ expect }) => { | ||
| await runWrangler("__complete wrangler kv na"); | ||
|
|
||
| expect(std.out).toContain("namespace\t"); | ||
| expect(std.out).not.toContain("key\t"); | ||
| }); | ||
|
|
||
| test("should return flags when prefix is --", async ({ expect }) => { | ||
| // Empty string after dev, then filter by -- | ||
| await runWrangler('__complete wrangler dev "" --'); | ||
|
|
||
| expect(std.out).toContain("--port\t"); | ||
| expect(std.out).toContain("--config\t"); | ||
| }); | ||
|
|
||
| test("should exclude hidden commands", async ({ expect }) => { | ||
| await runWrangler("__complete wrangler"); | ||
|
|
||
| // "check" namespace is hidden | ||
| expect(std.out).not.toMatch(/^check\t/m); | ||
| }); | ||
|
|
||
| test("should handle deeply nested commands", async ({ expect }) => { | ||
| // Empty string signals "complete after http" | ||
| await runWrangler('__complete wrangler queues consumer http ""'); | ||
|
|
||
| expect(std.out).toContain("add\t"); | ||
| expect(std.out).toContain("remove\t"); | ||
| }); | ||
|
|
||
| test("should output tab-separated format", async ({ expect }) => { | ||
| await runWrangler("__complete wrangler"); | ||
|
|
||
| // Each line should be "value\tdescription" | ||
| const lines = std.out.trim().split("\n"); | ||
| for (const line of lines) { | ||
| expect(line).toMatch(/^[^\t]+\t.*$/); | ||
| } | ||
| }); | ||
|
|
||
| test("should include global flags", async ({ expect }) => { | ||
| await runWrangler("__complete wrangler --"); | ||
|
|
||
| expect(std.out).toContain("--help\t"); | ||
| expect(std.out).toContain("--config\t"); | ||
| }); | ||
|
|
||
| test("should skip flags when building command path", async ({ | ||
| expect, | ||
| }) => { | ||
| // --binding should be skipped, so we're completing flags for "d1 create" | ||
| // Use -- to prevent yargs from parsing --binding as a flag for __complete | ||
| await runWrangler('__complete -- wrangler d1 create --binding foo ""'); | ||
|
|
||
| expect(std.out).toContain("--name\t"); | ||
| // Should not re-suggest d1 or create | ||
| expect(std.out).not.toMatch(/^d1\t/m); | ||
| expect(std.out).not.toMatch(/^create\t/m); | ||
| }); | ||
|
|
||
| test("should skip flag values when building command path", async ({ | ||
| expect, | ||
| }) => { | ||
| // Both --config and its value should be skipped | ||
| // Use -- to prevent yargs from parsing --config as a flag for __complete | ||
| await runWrangler( | ||
| '__complete -- wrangler --config wrangler.toml kv ""' | ||
| ); | ||
|
|
||
| expect(std.out).toContain("namespace\t"); | ||
| expect(std.out).toContain("key\t"); | ||
| }); | ||
| }); | ||
|
|
||
| // ========================================================================= | ||
| // Shell script generation tests | ||
| // ========================================================================= | ||
| const shells = ["bash", "zsh", "fish"] as const; | ||
|
|
||
| describe.each(shells)("%s", (shell) => { | ||
| test("should output script with begin/end markers", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler(`completions ${shell}`); | ||
|
|
||
| expect(std.out).toContain("###-begin-wrangler-completions-###"); | ||
| expect(std.out).toContain("###-end-wrangler-completions-###"); | ||
| }); | ||
|
|
||
| test("should show shell-specific description in help", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler(`completions ${shell} --help`); | ||
|
|
||
| expect(std.out).toContain(`Generate ${shell} completion script`); | ||
| }); | ||
|
|
||
| test("should reference wrangler __complete", async ({ expect }) => { | ||
| await runWrangler(`completions ${shell}`); | ||
|
|
||
| expect(std.out).toContain("wrangler __complete"); | ||
| }); | ||
| }); | ||
|
|
||
| // ========================================================================= | ||
| // Shell syntax validation tests | ||
| // ========================================================================= | ||
| describe("bash", () => { | ||
| test.skipIf(!shellAvailable("bash"))( | ||
| "should generate valid bash syntax", | ||
| async ({ expect }) => { | ||
| await runWrangler("completions bash"); | ||
|
|
||
| // bash -n checks syntax without executing | ||
| expect(() => { | ||
| execSync("bash -n", { input: std.out }); | ||
| }).not.toThrow(); | ||
| } | ||
| ); | ||
|
|
||
| test("should define _wrangler_completions function", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("completions bash"); | ||
|
|
||
| expect(std.out).toContain("_wrangler_completions()"); | ||
| }); | ||
|
|
||
| test("should register completion with complete builtin", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("completions bash"); | ||
|
|
||
| expect(std.out).toContain( | ||
| "complete -o default -F _wrangler_completions wrangler" | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("zsh", () => { | ||
| test.skipIf(!shellAvailable("zsh"))( | ||
| "should generate valid zsh syntax", | ||
| async ({ expect }) => { | ||
| await runWrangler("completions zsh"); | ||
|
|
||
| // zsh -n checks syntax without executing | ||
| expect(() => { | ||
| execSync("zsh -n", { input: std.out }); | ||
| }).not.toThrow(); | ||
| } | ||
| ); | ||
|
|
||
| test("should start with #compdef directive", async ({ expect }) => { | ||
| await runWrangler("completions zsh"); | ||
|
|
||
| expect(std.out).toContain("#compdef wrangler"); | ||
| }); | ||
|
|
||
| test("should use _describe for completions", async ({ expect }) => { | ||
| await runWrangler("completions zsh"); | ||
|
|
||
| expect(std.out).toContain("_describe 'wrangler' completions"); | ||
| }); | ||
|
|
||
| test("should register with compdef", async ({ expect }) => { | ||
| await runWrangler("completions zsh"); | ||
|
|
||
| expect(std.out).toContain("compdef _wrangler wrangler"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("fish", () => { | ||
| test.skipIf(!shellAvailable("fish"))( | ||
| "should generate valid fish syntax", | ||
| async ({ expect }) => { | ||
| await runWrangler("completions fish"); | ||
|
|
||
| // fish -n checks syntax without executing | ||
| expect(() => { | ||
| execSync("fish -n", { input: std.out }); | ||
| }).not.toThrow(); | ||
| } | ||
| ); | ||
|
|
||
| test("should define __wrangler_prepare_completions function", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("completions fish"); | ||
|
|
||
| expect(std.out).toContain("function __wrangler_prepare_completions"); | ||
| }); | ||
|
|
||
| test("should register completion with complete builtin", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("completions fish"); | ||
|
|
||
| expect(std.out).toContain("complete -c wrangler -f -n"); | ||
| expect(std.out).toContain("$__wrangler_comp_results"); | ||
| }); | ||
|
|
||
| test("should capture both completed tokens and current token", async ({ | ||
| expect, | ||
| }) => { | ||
| await runWrangler("completions fish"); | ||
|
|
||
| // commandline -opc gets completed tokens | ||
| expect(std.out).toContain("commandline -opc"); | ||
| // commandline -ct gets current token being typed | ||
| expect(std.out).toContain("commandline -ct"); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { createCommand, createNamespace } from "../core/create-command"; | ||
| import { logger } from "../logger"; | ||
| import { handleComplete } from "./complete-handler"; | ||
| import { getBashScript, getFishScript, getZshScript } from "./scripts"; | ||
|
|
||
| export const completionsNamespace = createNamespace({ | ||
| metadata: { | ||
| description: "⌨️ Generate shell completion scripts", | ||
| owner: "Workers: Authoring and Testing", | ||
| status: "stable", | ||
| epilogue: `Installation: | ||
| bash: wrangler completions bash >> ~/.bashrc | ||
| zsh: wrangler completions zsh >> ~/.zshrc | ||
| fish: wrangler completions fish > ~/.config/fish/completions/wrangler.fish`, | ||
| }, | ||
| }); | ||
|
|
||
| export const completionsBashCommand = createCommand({ | ||
| metadata: { | ||
| description: "Generate bash completion script", | ||
| owner: "Workers: Authoring and Testing", | ||
| status: "stable", | ||
| }, | ||
| behaviour: { | ||
| printBanner: false, | ||
| provideConfig: false, | ||
| }, | ||
| handler() { | ||
| logger.log(getBashScript()); | ||
| }, | ||
| }); | ||
|
|
||
| export const completionsZshCommand = createCommand({ | ||
| metadata: { | ||
| description: "Generate zsh completion script", | ||
| owner: "Workers: Authoring and Testing", | ||
| status: "stable", | ||
| }, | ||
| behaviour: { | ||
| printBanner: false, | ||
| provideConfig: false, | ||
| }, | ||
| handler() { | ||
| logger.log(getZshScript()); | ||
| }, | ||
| }); | ||
|
|
||
| export const completionsFishCommand = createCommand({ | ||
| metadata: { | ||
| description: "Generate fish completion script", | ||
| owner: "Workers: Authoring and Testing", | ||
| status: "stable", | ||
| }, | ||
| behaviour: { | ||
| printBanner: false, | ||
| provideConfig: false, | ||
| }, | ||
| handler() { | ||
| logger.log(getFishScript()); | ||
| }, | ||
| }); | ||
|
|
||
| export const completeCommand = createCommand({ | ||
| metadata: { | ||
| description: "Output completions for shell integration", | ||
| owner: "Workers: Authoring and Testing", | ||
| status: "stable", | ||
| hidden: true, // Not shown in --help | ||
| }, | ||
| behaviour: { | ||
| printBanner: false, | ||
| provideConfig: false, | ||
| }, | ||
| positionalArgs: ["args"], | ||
| args: { | ||
| args: { | ||
| type: "string", | ||
| array: true, | ||
| description: "Command line words to complete", | ||
| }, | ||
| }, | ||
| handler(args) { | ||
| // When -- is used, yargs puts args in _ instead of the positional array | ||
| // and includes the command name "__complete" as first element | ||
| let completionArgs = | ||
| args.args && args.args.length > 0 | ||
| ? args.args | ||
| : (args as unknown as { _: string[] })._ ?? []; | ||
| // Filter out __complete if it's the first arg (happens with --) | ||
| if (completionArgs[0] === "__complete") { | ||
| completionArgs = completionArgs.slice(1); | ||
| } | ||
| handleComplete(completionArgs); | ||
| }, | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.