Skip to content
Merged
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
10 changes: 10 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ clap = { version = "4.5.8", features = ["string"] }
clap_lex = { version = "0.7" }
anstyle = "1"
serde = { version = "1", optional = true }
clap_complete = {version = "4.5.61", optional = true}

[features]
default = ["serde"]
completion = ["dep:clap_complete"]

[dev-dependencies]
assert_matches = "1.5"
Expand Down
92 changes: 92 additions & 0 deletions examples/completion_example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! See completion_example.sh for a test script for this example.
//! This requires the Cargo "completion" feature to be enabled.

use conf::{Conf, Subcommands, completion::Shell};
use http::Uri as Url;
use std::net::SocketAddr;
use std::path::PathBuf;

/// Top-level CLI wrapper
#[derive(Conf, Debug)]
#[conf(name = "completion_example")]
pub struct Cli {
#[conf(subcommands)]
pub cmd: CliCommand,
}

#[derive(Subcommands, Debug)]
pub enum CliCommand {
/// Run the service
Run(ModelServiceConfig),

/// Print a shell completion script to stdout
Completion(CompletionArgs),
}

#[derive(Conf, Debug)]
pub struct CompletionArgs {
/// Shell to generate completions for (bash|elvish|fish|powershell|zsh)
#[conf(pos)]
pub shell: Shell,
}

/// Configuration for an http client
#[derive(Conf, Debug)]
pub struct HttpClientConfig {
/// Base URL
#[conf(long, env)]
pub url: Url,

/// Number of retries
#[conf(long, env)]
pub retries: u32,
}

/// Configuration for model service
#[derive(Conf, Debug)]
pub struct ModelServiceConfig {
/// Listen address to bind to
#[conf(long, env, default_value = "127.0.0.1:9090")]
pub listen_addr: SocketAddr,

/// Auth service:
#[conf(flatten, prefix, help_prefix)]
pub auth: Option<HttpClientConfig>,

/// Database:
#[conf(flatten, prefix, help_prefix)]
pub db: HttpClientConfig,

/// Optional subcommands
#[conf(subcommands)]
pub command: Option<Command>,
}

/// Subcommands that can be used with this service
#[derive(Subcommands, Debug)]
pub enum Command {
/// Run the migrations
RunMigrations(MigrationConfig),
/// Show the pending migrations
ShowPendingMigrations(MigrationConfig),
}

#[derive(Conf, Debug)]
pub struct MigrationConfig {
/// Path to migrations file (instead of embedded migrations)
#[conf(long, env)]
pub migrations: Option<PathBuf>,
}

fn main() {
match Cli::parse().cmd {
CliCommand::Completion(args) => {
conf::completion::write_completion::<Cli, _>(args.shell, None, &mut std::io::stdout())
.expect("Expected to output shell script");
}
CliCommand::Run(_cfg) => {
// Your normal execution path goes here
// (start server, run migrations, etc.)
}
}
}
23 changes: 23 additions & 0 deletions examples/completion_example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash

## Bash script to start an interactive shell where you can
## test the TAB completion for the `completion_example` command.
## This requires that you have already setup bash-completion on your host:
## https://wiki.debian.org/Add%20Bash%20Completion

set -euo pipefail

exec bash --noprofile --rcfile <(cat <<'EOF'
PS1="## $ "
source <(cargo run --features completion --example completion_example completion bash)
alias completion_example='cargo run --features completion --example completion_example'

echo "## Entering sub-shell."
echo "## Test 'completion_example' and its TAB completion."
echo "## When done, press Ctrl-D, or type 'exit'."
echo
echo "## $ completion_example help"
(set -x; completion_example help)
echo
EOF
) -i
126 changes: 126 additions & 0 deletions src/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Shell completion via clap_complete.
//! Requires the Cargo "completion" feature to be enabled.

use crate::{Conf, ParsedEnv};
use clap::Command as ClapCommand;
use clap_complete::{aot::Shell as ClapShell, generate};
use std::{fmt, io, str::FromStr};

/// Shell names that provide autocompletion support
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Shell {
/// Bourne Again shell (bash)
Bash,
/// Elvish shell
Elvish,
/// Friendly Interactive Shell (fish)
Fish,
/// PowerShell
PowerShell,
/// Z shell (zsh)
Zsh,
}

impl Shell {
#[inline]
fn to_clap(self) -> ClapShell {
match self {
Shell::Bash => ClapShell::Bash,
Shell::Elvish => ClapShell::Elvish,
Shell::Fish => ClapShell::Fish,
Shell::PowerShell => ClapShell::PowerShell,
Shell::Zsh => ClapShell::Zsh,
}
}
}

#[derive(Debug, Clone)]
/// Error in case the specified shell name is not parsed correctly
pub struct ParseShellError {
input: String,
}

impl fmt::Display for ParseShellError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid shell {:?} (expected: bash, elvish, fish, powershell, zsh)",
self.input
)
}
}

impl std::error::Error for ParseShellError {}

impl FromStr for Shell {
type Err = ParseShellError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let v = s.trim().to_ascii_lowercase();
match v.as_str() {
"bash" => Ok(Shell::Bash),
"elvish" => Ok(Shell::Elvish),
"fish" => Ok(Shell::Fish),
"powershell" => Ok(Shell::PowerShell),
"zsh" => Ok(Shell::Zsh),
_ => Err(ParseShellError {
input: s.to_string(),
}),
}
}
}

impl fmt::Display for Shell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Shell::Bash => "bash",
Shell::Elvish => "elvish",
Shell::Fish => "fish",
Shell::PowerShell => "powershell",
Shell::Zsh => "zsh",
})
}
}

/// Internal: retrieve the clap::Command to give to clap_complete.
fn get_clap_command<C: Conf>() -> ClapCommand {
let parsed_env = ParsedEnv::default();
let program_options = <C as Conf>::PROGRAM_OPTIONS.iter().collect::<Vec<_>>();

let parser =
<C as Conf>::get_parser(&parsed_env, program_options).expect("failed to build conf parser");

parser.into_command()
}

/// Write completion script for `C` into `out`.
///
/// `bin_name` is the name used in the generated script; if `None`, we use the clap command name.
pub fn write_completion<C: Conf, W: std::io::Write>(
shell: Shell,
bin_name: Option<&str>,
out: &mut W,
) -> std::io::Result<()> {
let mut cmd = get_clap_command::<C>();

let name: String = match bin_name {
Some(s) => s.to_string(),
None => cmd.get_name().to_string(),
};

generate(shell.to_clap(), &mut cmd, name, out);
Ok(())
}

/// Generate completion script and return as bytes.
pub fn completion_bytes<C: Conf>(shell: Shell, bin_name: Option<&str>) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
write_completion::<C, _>(shell, bin_name, &mut buf)?;
Ok(buf)
}

/// Generate completion script and return as a UTF-8 `String`.
pub fn completion_string<C: Conf>(shell: Shell, bin_name: Option<&str>) -> io::Result<String> {
let bytes = completion_bytes::<C>(shell, bin_name)?;
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
pub mod readme;

mod builder;
#[cfg(feature = "completion")]
pub mod completion;
mod conf_context;
mod error;
mod find_parameter;
Expand Down
Loading