Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 11 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,21 @@ jobs:
# timeout: with one, apt gives up on the stalled mirror in seconds and moves to the
# next entry in `/etc/apt/apt-mirrors.txt`, which is the archive.ubuntu.com that was
# answering fine throughout.
- name: Install shells for completion integration tests
#
# `bash-completion` is in the list for the same reason: usage no longer embeds a copy of
# it, so the generated bash completion needs the system one loaded, and
# `bash_completion_or_skip` panics under CI rather than skipping without it. The image
# already ships it; naming it here is what keeps that true if a future one stops.
#
# Deliberately the distro package rather than a newer build from source: `ubuntu-latest`
# is 24.04, whose 2.11 is the oldest version the generated script supports. Testing
# against the floor is the point — 26.04 ships 2.16, which would pass either way.
- name: Install shells and bash-completion for completion integration tests
timeout-minutes: 5
run: |
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish bash-completion
# pwsh is pre-installed on GitHub ubuntu-latest images. Self-heal
# if a future image drops it so the integration test still runs
# (rather than panicking under CI=1).
Expand Down
20 changes: 0 additions & 20 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,6 @@ SOFTWARE.
The Apache-2.0 option is available upstream at
<https://github.com/clap-rs/clap/blob/master/LICENSE-APACHE>.

## bash-completion

[`lib/bash-completion/bash_completion`](lib/bash-completion/bash_completion) is a
verbatim copy of the `bash_completion` script from
[scop/bash-completion](https://github.com/scop/bash-completion) (version 2.15.0).
`lib/src/complete/bash.rs` embeds it with `include_str!` and emits it when
`usage generate completion bash --include-bash-completion-lib` is used, so the
script is redistributed both in the `usage-lib` crate and in any binary built
from it.

**bash-completion is licensed under the GNU General Public License, version 2 or
later** — not MIT. The full license text ships alongside the script in
[lib/bash-completion/COPYING](lib/bash-completion/COPYING), and the upstream
copyright header is preserved at the top of the script itself:

```
Copyright © 2006-2008, Ian Macdonald <ian@caliban.org>
© 2009-2020, Bash Completion Maintainers
```

## clap adopter probes

`benches/shadows/external-*` and `conformance/tests/external_clap_adopters.rs`
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,4 @@ license is reproduced in [NOTICE.md](NOTICE.md).

## License

[MIT](LICENSE). Third-party notices — including the GPL-2.0-or-later
[bash-completion](https://github.com/scop/bash-completion) script that
`usage generate completion bash --include-bash-completion-lib` emits — are in
[NOTICE.md](NOTICE.md).
[MIT](LICENSE). Third-party notices are in [NOTICE.md](NOTICE.md).
16 changes: 12 additions & 4 deletions cli/assets/completions/usage.bash
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
# @generated by usage-cli from usage spec
# Requires bash-completion (https://github.com/scop/bash-completion), which must be
# sourced before this script.
_usage() {
if ! type -P usage &> /dev/null; then
echo >&2
echo "Error: usage CLI not found. This is required for completions to work in usage." >&2
echo "See https://usage.jdx.dev for more information." >&2
return 1
fi
if ! declare -F _init_completion > /dev/null 2>&1; then
echo >&2
echo "Error: bash-completion is required for usage completions but was not loaded." >&2
echo "Install it from your package manager and source it before this script." >&2
return 1
fi

local cur prev words cword was_split comp_args
_comp_initialize -n : -- "$@" || return
local cur prev words cword split
_init_completion -n : || return
local spec_dir="${XDG_CACHE_HOME:-$HOME/.cache}/usage"
[[ -d "$spec_dir" ]] || mkdir -p -m 700 "$spec_dir"
local spec_file="$spec_dir/usage__usage_spec_usage.spec"
command usage --usage-spec >| "$spec_file"
# shellcheck disable=SC2207
_comp_compgen -- -W "$(command usage complete-word --shell bash -f "$spec_file" --cword="$cword" -- "${words[@]}")"
_comp_ltrim_colon_completions "$cur"
COMPREPLY=($(compgen -W "$(command usage complete-word --shell bash -f "$spec_file" --cword="$cword" -- "${words[@]}")" -- "$cur"))
__ltrim_colon_completions "$cur"
# shellcheck disable=SC2181
if [[ $? -ne 0 ]]; then
unset COMPREPLY
Expand Down
5 changes: 0 additions & 5 deletions cli/assets/fig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,6 @@ const completionSpec: Fig.Spec = {
name: "cache_key",
},
},
{
name: "--include-bash-completion-lib",
description: "Include https://github.com/scop/bash-completion",
isRepeatable: false,
},
{
name: "--usage-bin",
description:
Expand Down
5 changes: 0 additions & 5 deletions cli/assets/usage.1
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,6 @@ A .usage.kdl spec file to use for generating completions, use "\-" to read from
\fB\-\-cache\-key\fR \fI<CACHE_KEY>\fR
A cache key to use for storing the results of calling the CLI with \-\-usage\-cmd
.TP
\fB\-\-include\-bash\-completion\-lib\fR
Include https://github.com/scop/bash\-completion

This is required for usage completions to work in bash, but the user may already provide it
.TP
\fB\-\-usage\-bin\fR \fI<USAGE_BIN>\fR
Override the bin used for calling back to usage\-cli

Expand Down
7 changes: 0 additions & 7 deletions cli/src/cli/generate/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ pub struct Completion {
#[usage(long, requires = "--usage-cmd")]
cache_key: Option<String>,

/// Include https://github.com/scop/bash-completion
///
/// This is required for usage completions to work in bash, but the user may already provide it
#[usage(long, verbatim_doc_comment)]
include_bash_completion_lib: bool,

/// Override the bin used for calling back to usage-cli
///
/// You may need to set this if you have a different bin named "usage"
Expand Down Expand Up @@ -61,7 +55,6 @@ impl Completion {
cache_key: self.cache_key.clone(),
spec,
usage_cmd: self.usage_cmd.clone(),
include_bash_completion_lib: self.include_bash_completion_lib,
source_file: self.file.as_ref().map(|f| {
if f.as_os_str() == "-" {
"stdin".to_string()
Expand Down
150 changes: 143 additions & 7 deletions cli/tests/shell_completions_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,82 @@ fn skip_if_shell_missing(shell: &str) -> bool {
true
}

/// The system bash-completion library, or `None` if it isn't installed.
///
/// usage no longer embeds a copy of bash-completion, so the generated bash completion needs
/// the real one loaded before it — `_init_completion` and `__ltrim_colon_completions` come from
/// there.
///
/// Candidates are the library itself, never the `profile.d/bash_completion.sh` snippet that
/// Homebrew and others also install: that one is guarded on `$PS1` and so does nothing at all
/// in a non-interactive shell, which is the only kind these tests run. Sourcing it would look
/// like success and then fail the completion assertions. `BASH_COMPLETION_USER_DIR` is not
/// consulted either — it holds per-command completions, not the library.
///
/// `USAGE_TEST_BASH_COMPLETION` names a library directly, for testing against a copy that is
/// not installed system-wide.
fn bash_completion_candidates() -> Vec<PathBuf> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(path) = env::var_os("USAGE_TEST_BASH_COMPLETION") {
candidates.push(PathBuf::from(path));
}
// Whichever Homebrew is installed: `/opt/homebrew` on Apple Silicon, `/usr/local` on Intel,
// and anywhere else if the user relocated it.
if let Some(prefix) = env::var_os("HOMEBREW_PREFIX") {
candidates.push(PathBuf::from(prefix).join("share/bash-completion/bash_completion"));
}
candidates.extend(
[
"/usr/share/bash-completion/bash_completion",
"/usr/local/share/bash-completion/bash_completion",
"/opt/homebrew/share/bash-completion/bash_completion",
]
.into_iter()
.map(PathBuf::from),
);
candidates
}

/// Returns the first candidate that a non-interactive bash can actually load `_init_completion`
/// from.
///
/// Existing on disk is not the property the test needs — being sourceable into the shell the
/// test spawns is. Probing for it means a wrapper, a version too old to define the function, or
/// a path guess that turns out wrong is rejected here rather than surfacing as a puzzling
/// completion assertion failure further down.
fn system_bash_completion() -> Option<PathBuf> {
bash_completion_candidates().into_iter().find(|candidate| {
candidate.is_file()
&& Command::new(shell_program("bash"))
.args(["-c", "source \"$1\" && declare -F _init_completion", "--"])
.arg(sh_path(candidate))
.output()
.is_ok_and(|out| out.status.success())
})
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// Returns `Some(path)` to the system bash-completion, or `None` if the test should be skipped.
///
/// Panics under `CI` for the same reason [`skip_if_shell_missing`] does: bash-completion is
/// installed by the workflow, so a run that quietly skipped this test would be reporting a
/// green bash suite that never exercised a completion.
///
/// What CI finds is `ubuntu-latest`'s package, which is 2.11 — deliberately the oldest version
/// the generated script claims to support, so the claim is tested rather than asserted.
fn bash_completion_or_skip() -> Option<PathBuf> {
if let Some(path) = system_bash_completion() {
return Some(path);
}
if env::var("CI").is_ok_and(|v| !v.is_empty()) {
panic!(
"no usable bash-completion but CI is set — refusing to skip. Tried: {:?}",
bash_completion_candidates()
);
}
eprintln!("Skipping bash completion test - no usable bash-completion library found");
None
}

fn shell_can_run_a_script(shell: &str) -> bool {
static NEXT: AtomicUsize = AtomicUsize::new(0);
let dir = env::temp_dir().join(format!(
Expand Down Expand Up @@ -369,6 +445,9 @@ fn test_bash_completion_integration() {
if skip_if_shell_missing("bash") {
return;
}
let Some(bash_completion) = bash_completion_or_skip() else {
return;
};

// Build the usage binary
let usage_bin = build_usage_binary();
Expand All @@ -389,12 +468,10 @@ cmd "sub" help="Subcommand" {
let spec_kdl_file = temp_dir.join("testcli.kdl");
fs::write(&spec_kdl_file, spec).unwrap();

// Generate the completion with bash-completion library included
let output = Command::new(&usage_bin)
.args(["generate", "completion", "bash", "testcli"])
.arg("-f")
.arg(spec_kdl_file.to_str().unwrap())
.arg("--include-bash-completion-lib")
.output()
.expect("Failed to generate bash completion");

Expand Down Expand Up @@ -424,7 +501,8 @@ set +e
# Add usage binary to PATH
export PATH="{}:$PATH"

# Source our completion (which includes bash-completion library)
# bash-completion first: the generated completion calls its functions.
source {}
source {}

echo "LOAD_SUCCESS"
Expand Down Expand Up @@ -519,6 +597,7 @@ fi
echo "COMPLETION_TEST_DONE"
"#,
path_var_entry("bash", usage_bin.parent().unwrap()),
sh_path(&bash_completion),
sh_path(&comp_file),
sh_path(&temp_dir)
);
Expand Down Expand Up @@ -1576,6 +1655,64 @@ echo "GUARD_EXIT=$?"
let _ = fs::remove_dir_all(&temp_dir);
}

#[test]
fn test_bash_guard_reports_missing_bash_completion() {
if skip_if_shell_missing("bash") {
return;
}

// usage no longer ships a copy of bash-completion, so "it isn't installed" is a
// reachable state for anyone sourcing a generated completion. It has to say so
// rather than fail as `_init_completion: command not found`.
let usage_bin = build_usage_binary();
let temp_dir = env::temp_dir().join(format!("usage_bash_nobc_test_{}", std::process::id()));
fs::create_dir_all(&temp_dir).unwrap();

let output = Command::new(&usage_bin)
.args(["generate", "completion", "bash", "testcli"])
.args(["--usage-cmd", "command usage --usage-spec"])
.output()
.expect("Failed to generate bash completion");
let comp_file = temp_dir.join("testcli.bash");
fs::write(&comp_file, &output.stdout).unwrap();

// A non-interactive bash loads no bash-completion of its own, so sourcing only the
// generated script is exactly the state being tested.
let test_script = format!(
r#"#!/usr/bin/env bash
export PATH="{path}:$PATH"
source "{comp}"
_testcli testcli "" testcli 1
echo "GUARD_EXIT=$?"
"#,
path = path_var_entry("bash", usage_bin.parent().unwrap()),
comp = sh_path(&comp_file),
);
let script_file = temp_dir.join("test.sh");
fs::write(&script_file, &test_script).unwrap();

let result = script_command("bash", &script_file)
.output()
.expect("Failed to run bash test");
let stdout = String::from_utf8_lossy(&result.stdout);
let stderr = String::from_utf8_lossy(&result.stderr);

assert!(
stdout.contains("GUARD_EXIT=1"),
"guard should return 1 when bash-completion is not loaded.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.contains("bash-completion is required"),
"guard should name bash-completion.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
!stderr.contains("_init_completion: command not found"),
"the guard should fire before bash reports a missing function.\nstderr:\n{stderr}"
);

let _ = fs::remove_dir_all(&temp_dir);
}

#[test]
fn test_bash_self_completion_uses_executable_not_shadowing_function() {
if skip_if_shell_missing("bash") {
Expand All @@ -1588,16 +1725,15 @@ fn test_bash_self_completion_uses_executable_not_shadowing_function() {

// The real usage binary is on $PATH *and* a `usage` function is defined,
// so the guard passes legitimately — only the spec call can go wrong.
// bash-completion isn't loaded here; stub the three helpers so the spec
// bash-completion isn't loaded here; stub the helpers it provides so the spec
// write is the only thing under test.
let test_script = format!(
r#"#!/usr/bin/env bash
export PATH="{usage_dir}:$PATH"
export XDG_CACHE_HOME="{cache}"
usage() {{ echo "FUNCTION_MARKER"; }}
_comp_initialize() {{ return 0; }}
_comp_compgen() {{ return 0; }}
_comp_ltrim_colon_completions() {{ return 0; }}
_init_completion() {{ return 0; }}
__ltrim_colon_completions() {{ return 0; }}
source "{asset}"
_usage usage "" usage 1
echo "SPEC_BEGIN"
Expand Down
3 changes: 0 additions & 3 deletions cli/usage.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,6 @@ cmd generate help="Generate completions, documentation, and other artifacts from
flag --cache-key help="A cache key to use for storing the results of calling the CLI with --usage-cmd" requires=--usage-cmd {
arg <CACHE_KEY>
}
flag --include-bash-completion-lib help="Include https://github.com/scop/bash-completion" {
long_help "Include https://github.com/scop/bash-completion\n\nThis is required for usage completions to work in bash, but the user may already provide it"
}
flag --usage-bin help="Override the bin used for calling back to usage-cli" env=JDX_USAGE_BIN default=usage {
long_help "Override the bin used for calling back to usage-cli\n\nYou may need to set this if you have a different bin named \"usage\""
arg <USAGE_BIN>
Expand Down
8 changes: 8 additions & 0 deletions docs/cli/completions.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ source ~/.bash_completions/mycli.bash
mycli --<TAB>
```

Generated bash completions call
[bash-completion](https://github.com/scop/bash-completion)'s helper functions, so it
must be installed and sourced before the generated script. Install it from your
package manager (`apt install bash-completion`, `brew install bash-completion@2`, …);
a generated completion that cannot find it says so instead of failing silently.
Version 2.11 and newer are supported, which covers every current distribution
release.

zsh:

```bash
Expand Down
11 changes: 0 additions & 11 deletions docs/cli/reference/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,17 +354,6 @@
},
"requires": ["--usage-cmd"]
},
{
"name": "include-bash-completion-lib",
"usage": "--include-bash-completion-lib",
"help": "Include https://github.com/scop/bash-completion",
"help_long": "Include https://github.com/scop/bash-completion\n\nThis is required for usage completions to work in bash, but the user may already provide it",
"help_first_line": "Include https://github.com/scop/bash-completion",
"short": [],
"long": ["include-bash-completion-lib"],
"hide": false,
"global": false
},
{
"name": "usage-bin",
"usage": "--usage-bin <USAGE_BIN>",
Expand Down
Loading
Loading