Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ bump. Currently experimental: project bundling, project dependencies

# Unreleased

* feat: `icp completions <SHELL>` prints a shell completion script for `bash`, `zsh`, `fish`, `powershell`, or `elvish` to stdout. See the [installation guide](docs/guides/installation.md#shell-completions) for where to put it.

# v1.3.0

* feat: a canister environment variable's value can now be read from a file, by writing `var: { path: <file> }` in place of `var: value`. The path resolves against the canister's directory — including in an environment override, matching `init_args` — and surrounding whitespace is trimmed off the file's contents. The file is read when the project is loaded, so a missing file fails before anything is deployed. `icp project bundle` writes the value into the bundled manifest inline, rejecting a file outside the project as it does for other manifest file references.
Expand Down
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ candid = "0.10.19"
candid_parser = "0.3.0"
clap = { version = "4.5.3", features = ["derive", "env"] }
clap-markdown = "0.1.5"
clap_complete = "4.5.3"
cryptoki = "0.12.0"
console = "0.16.3"
dialoguer = "0.12.0"
Expand Down
1 change: 1 addition & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ candid.workspace = true
cargo-generate.workspace = true
clap-markdown.workspace = true
clap.workspace = true
clap_complete.workspace = true
dialoguer.workspace = true
dunce.workspace = true
elliptic-curve.workspace = true
Expand Down
36 changes: 36 additions & 0 deletions crates/icp-cli/src/commands/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::io;

use clap::{Args, CommandFactory};
use clap_complete::{Shell, generate};

use crate::Cli;

/// Generate a shell completion script
///
/// The script is written to stdout; redirect it to the location your shell
/// loads completions from, or source it directly from your shell profile.
#[derive(Debug, Args)]
#[command(after_long_help = "\
Examples:

# Bash
icp completions bash > /etc/bash_completion.d/icp

# Zsh, into a directory on your $fpath
icp completions zsh > ~/.zfunc/_icp

# Fish
icp completions fish > ~/.config/fish/completions/icp.fish

# PowerShell, appended to your profile
icp completions powershell >> $PROFILE
")]
pub(crate) struct CompletionsArgs {
/// The shell to generate a completion script for
shell: Shell,
}

pub(crate) fn exec(args: &CompletionsArgs) {
let mut command = Cli::command();
generate(args.shell, &mut command, "icp", &mut io::stdout());
Comment on lines +33 to +35
}
2 changes: 2 additions & 0 deletions crates/icp-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::Subcommand;
pub(crate) mod args;
pub(crate) mod build;
pub(crate) mod canister;
pub(crate) mod completions;
pub(crate) mod cycles;
pub(crate) mod deploy;
pub(crate) mod environment;
Expand All @@ -21,6 +22,7 @@ pub(crate) enum Command {
Build(build::BuildArgs),
#[command(subcommand)]
Canister(canister::Command),
Completions(completions::CompletionsArgs),
#[command(subcommand)]
Cycles(cycles::Command),
Deploy(deploy::DeployArgs),
Expand Down
11 changes: 11 additions & 0 deletions crates/icp-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ async fn main() -> Result<(), Error> {
}
};

// Completion scripts are derived from the clap definition alone. Handle them
// before any other setup: they are generated at package-install time, where
// the telemetry notice and the update check have no business firing.
if let Command::Completions(args) = &command {
commands::completions::exec(args);
return Ok(());
}

// Logging: --debug gets the detailed tracing layer; otherwise plain user-facing output
let debug = cli.debug;
let reg = Registry::default()
Expand Down Expand Up @@ -305,6 +313,9 @@ async fn dispatch(ctx: &icp::context::Context, command: Command) -> Result<(), E
}
},

// Completions: handled in `main` before the context exists
Command::Completions(_) => unreachable!(),

// Cycles
Command::Cycles(cmd) => match cmd {
commands::cycles::Command::Balance(args) => {
Expand Down
10 changes: 10 additions & 0 deletions docs/guides/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
```

## Shell Completions

The npm and Homebrew distributions install shell completions automatically. Otherwise, `icp completions <SHELL>` writes a completion script to stdout — `bash`, `zsh`, `fish`, `powershell`, and `elvish` are supported. Redirect it to wherever your shell loads completions from:
Comment thread
adamspofford-dfinity marked this conversation as resolved.

```bash
mkdir -p ~/.local/share/bash-completion/completions
icp completions bash > ~/.local/share/bash-completion/completions/icp
```

Restart your shell afterwards, and regenerate the script after upgrading `icp` so completions cover newly added commands and flags.

## Troubleshooting

Expand Down
34 changes: 34 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This document contains the help content for the `icp` command-line program.
* [`icp canister status`↴](#icp-canister-status)
* [`icp canister stop`↴](#icp-canister-stop)
* [`icp canister top-up`↴](#icp-canister-top-up)
* [`icp completions`↴](#icp-completions)
* [`icp cycles`↴](#icp-cycles)
* [`icp cycles balance`↴](#icp-cycles-balance)
* [`icp cycles mint`↴](#icp-cycles-mint)
Expand Down Expand Up @@ -89,6 +90,7 @@ This document contains the help content for the `icp` command-line program.

* `build` — Build canisters
* `canister` — Perform canister operations against a network
* `completions` — Generate a shell completion script
* `cycles` — Mint and manage cycles
* `deploy` — Deploy a project to an environment
* `environment` — Show information about the current project environments
Expand Down Expand Up @@ -802,6 +804,38 @@ Top up a canister with cycles



## `icp completions`

Generate a shell completion script

The script is written to stdout; redirect it to the location your shell loads completions from, or source it directly from your shell profile.

**Usage:** `icp completions <SHELL>`

Examples:

# Bash
icp completions bash > /etc/bash_completion.d/icp

# Zsh, into a directory on your $fpath
icp completions zsh > ~/.zfunc/_icp

# Fish
icp completions fish > ~/.config/fish/completions/icp.fish

# PowerShell, appended to your profile
icp completions powershell >> $PROFILE


###### **Arguments:**

* `<SHELL>` — The shell to generate a completion script for

Possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh`




## `icp cycles`

Mint and manage cycles
Expand Down
127 changes: 127 additions & 0 deletions npm/icp-cli/completions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Shell completion installation, run from postinstall.js.
*
* Only directories that a shell reads on its own are written to; hooking up a
* shell that needs a profile edit is left to the user, who can generate the
* script with `icp completions <shell>`.
*/

const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');

/**
* The user location bash-completion v2 loads from. `BASH_COMPLETION_USER_DIR`
* is a colon-separated list searched in order, so its first entry is the one to
* write to.
*/
function bashCompletionUserDir(home) {
const configured = (process.env.BASH_COMPLETION_USER_DIR || '')
.split(':')
.find((dir) => dir !== '');
if (configured) {
return configured;
}
const dataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share');
return path.join(dataHome, 'bash-completion');
}

function completionTargets(home) {
const fishConfig = path.join(
process.env.XDG_CONFIG_HOME || path.join(home, '.config'),
'fish'
);

return [
{
shell: 'bash',
dir: path.join(bashCompletionUserDir(home), 'completions'),
file: 'icp'
},
{
shell: 'fish',
dir: path.join(fishConfig, 'completions'),
file: 'icp.fish',
// Only if fish is actually configured; fish creates this itself otherwise.
requires: fishConfig
},
{
shell: 'zsh',
// Not on zsh's default $fpath, so only useful if the user set it up.
dir: path.join(home, '.zfunc'),
file: '_icp',
requires: path.join(home, '.zfunc')
}
];
}

/** Generate a completion script, or throw with what the binary reported. */
function generate(binaryPath, shell) {
const result = spawnSync(binaryPath, ['completions', shell], {
encoding: 'utf8',
maxBuffer: 8 * 1024 * 1024
});

if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const detail = (result.stderr || '').trim();
throw new Error(
`\`icp completions ${shell}\` exited with status ${result.status}` +
(detail ? `: ${detail}` : '')
);
}
if (!result.stdout) {
throw new Error(`\`icp completions ${shell}\` produced no output`);
}
return result.stdout;
}

/**
* Install completion scripts for the shells that can pick them up automatically.
*
* A failure is reported but does not fail the install: completions are a
* convenience, and an unwritable home directory is not an installation error.
*
* @returns {string[]} the shells whose completions were installed
*/
function installCompletions(binaryPath) {
if (process.env.ICP_CLI_SKIP_COMPLETIONS || process.platform === 'win32') {
return [];
}

const home = os.homedir();
if (!home) {
console.error(
'WARNING: skipping shell completions: no home directory found\n' +
' Once that is resolved, run `icp completions <SHELL>` and save the output ' +
'where your shell loads completions from.'
);
return [];
}

const installed = [];
for (const target of completionTargets(home)) {
if (target.requires && !fs.existsSync(target.requires)) {
continue;
}
const destination = path.join(target.dir, target.file);
try {
const script = generate(binaryPath, target.shell);
fs.mkdirSync(target.dir, { recursive: true });
fs.writeFileSync(destination, script, { mode: 0o644 });
installed.push(target.shell);
} catch (err) {
console.error(
`WARNING: could not install ${target.shell} completions at ${destination}: ${err.message}\n` +
` Once that is resolved, run: icp completions ${target.shell} > ${destination}`
);
}
}

return installed;
}

module.exports = { installCompletions };
11 changes: 11 additions & 0 deletions npm/icp-cli/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
const fs = require('fs');
const path = require('path');

const { installCompletions } = require('./completions');

const platform = process.platform;
const arch = process.arch;

Expand Down Expand Up @@ -52,6 +54,10 @@ try {
// Ignore permission errors - might not have rights to chmod
}

const completions = process.env.npm_config_global === 'true'
? installCompletions(binaryPath)
: [];

console.log(`
╔═══════════════════════════════════════════════════════════╗
║ ║
Expand All @@ -64,6 +70,11 @@ try {
║ ║
╚═══════════════════════════════════════════════════════════╝
`);

if (completions.length > 0) {
console.log(`Installed shell completions for: ${completions.join(', ')}. Restart your shell to use them.`);
}
console.log('Completions for other shells: see `icp completions --help`.');
} else {
console.log(`
╔═══════════════════════════════════════════════════════════╗
Expand Down
Loading