use std::io::IsTerminal;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(name = "sopsy")]
#[command(version)]
#[command(about = "The missing developer experience for SOPS")]
#[command(propagate_version = true)]
#[command(max_term_width = 80)]
pub struct Cli {
#[command(flatten)]
pub global: GlobalArgs,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Args, Clone)]
pub struct GlobalArgs {
#[arg(long, short = 'y', visible_alias = "yes", global = true)]
pub non_interactive: bool,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
}
impl GlobalArgs {
pub fn resolve_interactive(&self) -> bool {
!self.non_interactive && std::io::stdout().is_terminal()
}
pub fn resolve_color(&self) -> bool {
!self.no_color
}
}
#[derive(Debug, Subcommand)]
pub enum Command {
Init(InitArgs),
Doctor,
Edit(EditArgs),
#[command(subcommand)]
Recipient(RecipientCommand),
Check,
Deps(DepsArgs),
Completion(CompletionArgs),
}
#[derive(Debug, Args)]
pub struct InitArgs {
#[arg(long)]
pub recipient_name: Option<String>,
#[arg(long)]
pub public_key: Option<String>,
#[arg(long)]
pub no_generate: bool,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Args)]
pub struct EditArgs {
pub file: PathBuf,
#[arg(long)]
pub editor: Option<String>,
#[arg(last = true)]
pub sops_args: Vec<String>,
}
#[derive(Debug, Args)]
pub struct DepsArgs {
#[arg(long)]
pub check: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: clap_complete::Shell,
}
#[derive(Debug, Subcommand)]
pub enum RecipientCommand {
Add(RecipientAddArgs),
Remove(RecipientRemoveArgs),
List,
}
#[derive(Debug, Args)]
pub struct RecipientAddArgs {
pub name_pos: Option<String>,
#[arg(long = "name")]
pub name: Option<String>,
#[arg(long)]
pub public_key: Option<String>,
#[arg(long)]
pub break_glass: bool,
#[arg(long)]
pub no_updatekeys: bool,
}
impl RecipientAddArgs {
pub fn resolved_name(&self) -> Option<&str> {
self.name.as_deref().or(self.name_pos.as_deref())
}
}
#[derive(Debug, Args)]
pub struct RecipientRemoveArgs {
pub name_pos: Option<String>,
#[arg(long = "name")]
pub name: Option<String>,
#[arg(long)]
pub no_updatekeys: bool,
}
impl RecipientRemoveArgs {
pub fn resolved_name(&self) -> Option<&str> {
self.name.as_deref().or(self.name_pos.as_deref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_definition_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn parses_recipient_add_with_flags() {
let cli = Cli::try_parse_from([
"sopsy",
"--non-interactive",
"recipient",
"add",
"--name",
"alice",
"--public-key",
"age1alice",
])
.unwrap();
assert!(cli.global.non_interactive);
match cli.command {
Command::Recipient(RecipientCommand::Add(args)) => {
assert_eq!(args.resolved_name(), Some("alice"));
assert_eq!(args.public_key.as_deref(), Some("age1alice"));
}
_ => panic!("expected recipient add"),
}
}
#[test]
fn yes_alias_enables_non_interactive() {
let cli = Cli::try_parse_from(["sopsy", "-y", "doctor"]).unwrap();
assert!(cli.global.non_interactive);
}
}