mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
completions: add clap_complete shell completion generator (#106)
Feature-gated binary (--features completions) that generates bash/zsh/fish/ elvish/powershell completions from each tool's uu_app() definition. Usage: shadow-rs-completions passwd --shell bash shadow-rs-completions --all --shell zsh --dir completions/ Also updates generate-completions.sh to use the new binary and adds windows-sys skip to deny.toml for transitive clap dep. Fixes #106
This commit is contained in:
+13
@@ -44,6 +44,7 @@ shadow-core = { path = "src/shadow-core" }
|
||||
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
clap_complete = "4"
|
||||
|
||||
# uutils integration
|
||||
uucore = "0.7"
|
||||
@@ -57,12 +58,16 @@ libc = "0.2"
|
||||
|
||||
# Security
|
||||
zeroize = "1"
|
||||
landlock = "0.4"
|
||||
|
||||
# Testing
|
||||
proptest = "1"
|
||||
tempfile = "3"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
clap_complete = { workspace = true, optional = true }
|
||||
|
||||
# Tool crates (optional, enabled by features)
|
||||
passwd = { optional = true, version = "0.0.1", package = "uu_passwd", path = "src/uu/passwd" }
|
||||
pwck = { optional = true, version = "0.0.1", package = "uu_pwck", path = "src/uu/pwck" }
|
||||
@@ -105,10 +110,18 @@ feat_phase2 = ["feat_useradd", "feat_userdel", "feat_usermod", "feat_chpasswd",
|
||||
feat_phase3 = ["feat_groupadd", "feat_groupdel", "feat_groupmod", "feat_grpck", "feat_chfn", "feat_chsh", "feat_newgrp"]
|
||||
feat_common = ["feat_phase1", "feat_phase2"]
|
||||
|
||||
# Shell completions generator
|
||||
completions = ["dep:clap_complete"]
|
||||
|
||||
[[bin]]
|
||||
name = "shadow-rs"
|
||||
path = "src/bin/shadow-rs.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "shadow-rs-completions"
|
||||
path = "src/bin/completions.rs"
|
||||
required-features = ["completions"]
|
||||
|
||||
[[test]]
|
||||
name = "test_chage"
|
||||
path = "tests/by-util/test_chage.rs"
|
||||
|
||||
@@ -26,9 +26,12 @@ multiple-versions = "deny"
|
||||
wildcards = "allow"
|
||||
highlight = "all"
|
||||
|
||||
# Known duplicate dependencies (uucore pulls in older versions of some crates).
|
||||
# Update this list when bumping uucore.
|
||||
skip = []
|
||||
# Known duplicate dependencies from transitive deps.
|
||||
# Update this list when bumping clap/uucore.
|
||||
skip = [
|
||||
# clap -> terminal_size (0.60) vs clap -> anstyle-query (0.61)
|
||||
{ crate = "windows-sys@0.60", reason = "transitive clap dep, Linux-only project" },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "warn"
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// This file is part of the shadow-rs package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore completions
|
||||
|
||||
//! Shell completion generator for all shadow-rs tools.
|
||||
//!
|
||||
//! Generates completions by calling each tool's `uu_app()` function,
|
||||
//! so completions always match the actual CLI definition.
|
||||
//!
|
||||
//! Usage:
|
||||
//! shadow-rs-completions passwd --shell bash
|
||||
//! shadow-rs-completions --all --shell zsh --dir completions/
|
||||
//!
|
||||
//! Supported shells: bash, zsh, fish, elvish, powershell
|
||||
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use clap_complete::generate;
|
||||
use clap_complete::Shell;
|
||||
use std::io;
|
||||
|
||||
fn get_tool_app(name: &str) -> Option<Command> {
|
||||
match name {
|
||||
#[cfg(feature = "chage")]
|
||||
"chage" => Some(chage::uu_app()),
|
||||
#[cfg(feature = "chfn")]
|
||||
"chfn" => Some(chfn::uu_app()),
|
||||
#[cfg(feature = "chpasswd")]
|
||||
"chpasswd" => Some(chpasswd::uu_app()),
|
||||
#[cfg(feature = "chsh")]
|
||||
"chsh" => Some(chsh::uu_app()),
|
||||
#[cfg(feature = "groupadd")]
|
||||
"groupadd" => Some(groupadd::uu_app()),
|
||||
#[cfg(feature = "groupdel")]
|
||||
"groupdel" => Some(groupdel::uu_app()),
|
||||
#[cfg(feature = "groupmod")]
|
||||
"groupmod" => Some(groupmod::uu_app()),
|
||||
#[cfg(feature = "grpck")]
|
||||
"grpck" => Some(grpck::uu_app()),
|
||||
#[cfg(feature = "newgrp")]
|
||||
"newgrp" => Some(newgrp::uu_app()),
|
||||
#[cfg(feature = "passwd")]
|
||||
"passwd" => Some(passwd::uu_app()),
|
||||
#[cfg(feature = "pwck")]
|
||||
"pwck" => Some(pwck::uu_app()),
|
||||
#[cfg(feature = "useradd")]
|
||||
"useradd" => Some(useradd::uu_app()),
|
||||
#[cfg(feature = "userdel")]
|
||||
"userdel" => Some(userdel::uu_app()),
|
||||
#[cfg(feature = "usermod")]
|
||||
"usermod" => Some(usermod::uu_app()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::vec_init_then_push)] // cfg attributes on each push prevent using vec![]
|
||||
fn all_tool_names() -> Vec<&'static str> {
|
||||
let mut names = Vec::new();
|
||||
#[cfg(feature = "chage")]
|
||||
names.push("chage");
|
||||
#[cfg(feature = "chfn")]
|
||||
names.push("chfn");
|
||||
#[cfg(feature = "chpasswd")]
|
||||
names.push("chpasswd");
|
||||
#[cfg(feature = "chsh")]
|
||||
names.push("chsh");
|
||||
#[cfg(feature = "groupadd")]
|
||||
names.push("groupadd");
|
||||
#[cfg(feature = "groupdel")]
|
||||
names.push("groupdel");
|
||||
#[cfg(feature = "groupmod")]
|
||||
names.push("groupmod");
|
||||
#[cfg(feature = "grpck")]
|
||||
names.push("grpck");
|
||||
#[cfg(feature = "newgrp")]
|
||||
names.push("newgrp");
|
||||
#[cfg(feature = "passwd")]
|
||||
names.push("passwd");
|
||||
#[cfg(feature = "pwck")]
|
||||
names.push("pwck");
|
||||
#[cfg(feature = "useradd")]
|
||||
names.push("useradd");
|
||||
#[cfg(feature = "userdel")]
|
||||
names.push("userdel");
|
||||
#[cfg(feature = "usermod")]
|
||||
names.push("usermod");
|
||||
names
|
||||
}
|
||||
|
||||
fn cli() -> Command {
|
||||
Command::new("shadow-rs-completions")
|
||||
.about("Generate shell completions for shadow-rs tools")
|
||||
.arg(
|
||||
Arg::new("tool")
|
||||
.help("Tool name (or use --all)")
|
||||
.required_unless_present("all"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("shell")
|
||||
.long("shell")
|
||||
.short('s')
|
||||
.help("Target shell: bash, zsh, fish, elvish, powershell")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("all")
|
||||
.long("all")
|
||||
.help("Generate completions for all tools")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("dir")
|
||||
.long("dir")
|
||||
.help("Output directory (one file per tool; default: stdout)")
|
||||
.requires("all"),
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_for_tool(name: &str, shell: Shell, out: &mut dyn io::Write) {
|
||||
if let Some(mut cmd) = get_tool_app(name) {
|
||||
generate(shell, &mut cmd, name, out);
|
||||
} else {
|
||||
eprintln!("unknown tool: {name}");
|
||||
eprintln!("available: {}", all_tool_names().join(", "));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_extension(shell: Shell) -> &'static str {
|
||||
match shell {
|
||||
Shell::Bash => "bash",
|
||||
Shell::Zsh => "zsh",
|
||||
Shell::Fish => "fish",
|
||||
Shell::Elvish => "elv",
|
||||
Shell::PowerShell => "ps1",
|
||||
_ => "txt",
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = cli().get_matches();
|
||||
let shell: Shell = matches
|
||||
.get_one::<String>("shell")
|
||||
.expect("shell is required")
|
||||
.parse()
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("invalid shell: {e}");
|
||||
eprintln!("supported: bash, zsh, fish, elvish, powershell");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if matches.get_flag("all") {
|
||||
let tools = all_tool_names();
|
||||
if let Some(dir) = matches.get_one::<String>("dir") {
|
||||
std::fs::create_dir_all(dir).unwrap_or_else(|e| {
|
||||
eprintln!("cannot create directory '{dir}': {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let ext = shell_extension(shell);
|
||||
for name in &tools {
|
||||
let path = format!("{dir}/{name}.{ext}");
|
||||
let mut file = std::fs::File::create(&path).unwrap_or_else(|e| {
|
||||
eprintln!("cannot create '{path}': {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
if let Some(mut cmd) = get_tool_app(name) {
|
||||
generate(shell, &mut cmd, *name, &mut file);
|
||||
}
|
||||
}
|
||||
eprintln!("generated {} completions in {dir}/", tools.len());
|
||||
} else {
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
for name in &tools {
|
||||
if let Some(mut cmd) = get_tool_app(name) {
|
||||
generate(shell, &mut cmd, *name, &mut out);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tool = matches.get_one::<String>("tool").expect("tool is required");
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
generate_for_tool(tool, shell, &mut out);
|
||||
}
|
||||
}
|
||||
@@ -2,37 +2,27 @@
|
||||
# Generate shell completions for all shadow-rs tools.
|
||||
#
|
||||
# Usage:
|
||||
# ./util/generate-completions.sh bash
|
||||
# ./util/generate-completions.sh zsh
|
||||
# ./util/generate-completions.sh fish
|
||||
# ./util/generate-completions.sh bash [output-dir]
|
||||
# ./util/generate-completions.sh zsh [output-dir]
|
||||
# ./util/generate-completions.sh fish [output-dir]
|
||||
#
|
||||
# For proper clap-based completions, build with clap_complete.
|
||||
# This is a minimal placeholder until clap_complete is integrated.
|
||||
# Requires: cargo build --features completions
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SHELL_TYPE="${1:-bash}"
|
||||
TOOLS="passwd pwck useradd userdel usermod chpasswd chage groupadd groupdel groupmod grpck chfn chsh newgrp"
|
||||
OUTPUT_DIR="${2:-}"
|
||||
|
||||
case "$SHELL_TYPE" in
|
||||
bash)
|
||||
for tool in $TOOLS; do
|
||||
echo "complete -W '--help' $tool"
|
||||
done
|
||||
;;
|
||||
zsh)
|
||||
echo "#compdef shadow-rs"
|
||||
for tool in $TOOLS; do
|
||||
echo "complete -c $tool -s h -l help -d 'Show help'"
|
||||
done
|
||||
;;
|
||||
fish)
|
||||
for tool in $TOOLS; do
|
||||
echo "complete -c $tool -s h -l help -d 'Show help'"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {bash|zsh|fish}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Build the completions binary
|
||||
cargo build --manifest-path "$PROJECT_DIR/Cargo.toml" --features completions --bin shadow-rs-completions
|
||||
|
||||
BINARY="$PROJECT_DIR/target/debug/shadow-rs-completions"
|
||||
|
||||
if [ -n "$OUTPUT_DIR" ]; then
|
||||
"$BINARY" --all --shell "$SHELL_TYPE" --dir "$OUTPUT_DIR"
|
||||
else
|
||||
"$BINARY" --all --shell "$SHELL_TYPE"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user