//! Complete pass-compatible command request contract. use std::{error::Error, ffi::OsString, fmt, num::NonZeroUsize, path::PathBuf}; use clap::{Args, CommandFactory, Parser, Subcommand, error::ErrorKind}; use clap_complete::{Shell, generate}; use crate::PRODUCT_NAME; pub const EXIT_SUCCESS: u8 = 0; pub const EXIT_FAILURE: u8 = 1; pub const EXIT_USAGE: u8 = EXIT_FAILURE; pub const EXIT_UNAVAILABLE: u8 = 69; pub const EXIT_CONFIG: u8 = 78; #[derive(Clone, Debug, Eq, PartialEq)] pub enum CliAction { Run(CliInvocation), Display(String), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct CliInvocation { config: Option, request: CommandRequest, } impl CliInvocation { pub fn config(&self) -> Option<&std::path::Path> { self.config.as_deref() } pub fn request(&self) -> &CommandRequest { &self.request } pub fn into_parts(self) -> (Option, CommandRequest) { (self.config, self.request) } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum CommandRequest { Init(InitRequest), List(ListRequest), Show(ShowRequest), Find(FindRequest), Grep(GrepRequest), Insert(InsertRequest), Edit(EditRequest), Generate(GenerateRequest), Remove(RemoveRequest), Move(MoveRequest), Copy(CopyRequest), Git(GitRequest), Otp(OtpRequest), Completion { shell: CompletionShell }, Help { topic: Option }, Version, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CompletionShell { Bash, Elvish, Fish, PowerShell, Zsh, } impl CompletionShell { fn generator(self) -> Shell { match self { Self::Bash => Shell::Bash, Self::Elvish => Shell::Elvish, Self::Fish => Shell::Fish, Self::PowerShell => Shell::PowerShell, Self::Zsh => Shell::Zsh, } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InitRequest { pub path: Option, pub key_identities: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ListRequest { pub path: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ShowRequest { pub entry: Option, pub presentation: Presentation, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Presentation { Terminal, Clipboard { line: NonZeroUsize }, QrCode { line: NonZeroUsize }, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct FindRequest { pub terms: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct GrepRequest { pub pattern: String, pub ignore_case: bool, pub invert_match: bool, pub line_number: bool, pub fixed_strings: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InsertRequest { pub entry: String, pub input: InsertInput, pub force: bool, } impl InsertRequest { pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan { if !standard_input_is_terminal { return if self.input == InsertInput::Multiline { InputPlan::StandardInputToEnd } else { InputPlan::StandardInputLine }; } match self.input { InsertInput::HiddenConfirmed => InputPlan::HiddenConfirmed, InsertInput::EchoedLine => InputPlan::EchoedLine, InsertInput::Multiline => InputPlan::StandardInputToEnd, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum InsertInput { HiddenConfirmed, EchoedLine, Multiline, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum InputPlan { HiddenConfirmed, EchoedLine, StandardInputLine, StandardInputToEnd, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EditRequest { pub entry: String, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct GenerateRequest { pub entry: String, pub length: Option, pub no_symbols: bool, pub force: bool, pub in_place: bool, pub presentation: GeneratedPresentation, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum GeneratedPresentation { Terminal, Clipboard, QrCode, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct RemoveRequest { pub entry: String, pub recursive: bool, pub force: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MoveRequest { pub source: String, pub destination: String, pub force: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct CopyRequest { pub source: String, pub destination: String, pub force: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum GitRequest { Init, Status, Log { maximum: Option, }, Diff { paths: Vec, }, Add { paths: Vec, }, Commit { message: String, }, Remote(GitRemoteRequest), Config(GitConfigRequest), Fetch { remote: Option, }, Pull { remote: Option, branch: Option, }, Push { remote: Option, branch: Option, }, Sync { remote: Option, }, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum GitRemoteRequest { List, GetUrl { name: String }, Add { name: String, url: String }, SetUrl { name: String, url: String }, Remove { name: String }, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum GitConfigRequest { Get { key: String }, Set { key: String, value: String }, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum OtpRequest { Code(OtpCodeRequest), Insert(OtpInsertRequest), Append(OtpAppendRequest), Uri(OtpUriRequest), Validate { uri: String }, Help, Version, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct OtpCodeRequest { pub entry: String, pub clipboard: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct OtpInsertRequest { pub entry: Option, pub force: bool, pub echo: bool, pub source: OtpInputSource, } impl OtpInsertRequest { pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan { line_input_plan(self.echo, standard_input_is_terminal) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct OtpAppendRequest { pub entry: String, pub force: bool, pub echo: bool, pub source: OtpInputSource, } impl OtpAppendRequest { pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan { line_input_plan(self.echo, standard_input_is_terminal) } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum OtpInputSource { Uri, Secret { issuer: Option, account: Option, }, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct OtpUriRequest { pub entry: String, pub presentation: OtpUriPresentation, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum OtpUriPresentation { Terminal, Clipboard, QrCode, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum HelpTopic { Init, List, Show, Find, Grep, Insert, Edit, Generate, Remove, Move, Copy, Git, Otp, } impl HelpTopic { fn parse(value: &str) -> Option { match value { "init" => Some(Self::Init), "ls" | "list" => Some(Self::List), "show" => Some(Self::Show), "find" | "search" => Some(Self::Find), "grep" => Some(Self::Grep), "insert" | "add" => Some(Self::Insert), "edit" => Some(Self::Edit), "generate" => Some(Self::Generate), "rm" | "remove" | "delete" => Some(Self::Remove), "mv" | "rename" => Some(Self::Move), "cp" | "copy" => Some(Self::Copy), "git" => Some(Self::Git), "otp" => Some(Self::Otp), _ => None, } } fn command_name(self) -> &'static str { match self { Self::Init => "init", Self::List => "ls", Self::Show => "show", Self::Find => "find", Self::Grep => "grep", Self::Insert => "insert", Self::Edit => "edit", Self::Generate => "generate", Self::Remove => "rm", Self::Move => "mv", Self::Copy => "cp", Self::Git => "git", Self::Otp => "otp", } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct CliParseError { message: String, exit_code: u8, } impl CliParseError { pub fn exit_code(&self) -> u8 { self.exit_code } } impl fmt::Display for CliParseError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.message) } } impl Error for CliParseError {} pub fn parse_env() -> Result { parse_from(std::env::args_os()) } pub fn parse_from(arguments: I) -> Result where I: IntoIterator, T: Into + Clone, { let arguments = arguments.into_iter().map(Into::into).collect::>(); let arguments = normalize_dispatch(arguments); match CliArguments::try_parse_from(arguments) { Ok(arguments) => convert_arguments(arguments).map(CliAction::Run), Err(error) if matches!( error.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion ) => { Ok(CliAction::Display(error.to_string())) } Err(error) => Err(CliParseError { message: error.to_string(), exit_code: EXIT_USAGE, }), } } pub fn help_text(topic: Option) -> String { let mut command = CliArguments::command(); if let Some(topic) = topic { let name = topic.command_name(); if let Some(subcommand) = command.find_subcommand_mut(name) { return subcommand.render_long_help().to_string(); } } command.render_long_help().to_string() } pub fn version_text() -> String { format!("{PRODUCT_NAME} {}\n", env!("CARGO_PKG_VERSION")) } /// The observable version string from the pinned pass-otp v1.2.0 source. pub fn otp_version_text() -> &'static str { "1.1.1\n" } pub fn completion_script(shell: CompletionShell) -> Vec { let mut command = CliArguments::command(); let mut output = Vec::new(); generate(shell.generator(), &mut command, "ironstorage", &mut output); output } #[derive(Parser)] #[command( name = "ironstorage", about = "A pass-compatible password-store client", disable_help_subcommand = true, version )] struct CliArguments { #[arg(long, global = true, value_name = "PATH")] config: Option, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum CommandArguments { Init(InitArguments), #[command(name = "ls", visible_alias = "list")] List(ListArguments), Show(ShowArguments), #[command(visible_alias = "search")] Find(FindArguments), Grep(GrepArguments), #[command(visible_alias = "add")] Insert(InsertArguments), Edit(EditArguments), Generate(GenerateArguments), #[command(name = "rm", visible_aliases = ["remove", "delete"])] Remove(RemoveArguments), #[command(name = "mv", visible_alias = "rename")] Move(MoveArguments), #[command(name = "cp", visible_alias = "copy")] Copy(CopyArguments), Git(GitArguments), Otp(OtpArguments), Completion(CompletionArguments), Help(HelpArguments), Version, } #[derive(Args)] struct CompletionArguments { #[arg(value_enum)] shell: CompletionShellArgument, } #[derive(Clone, Copy, clap::ValueEnum)] enum CompletionShellArgument { Bash, Elvish, Fish, Powershell, Zsh, } #[derive(Args)] struct InitArguments { #[arg(short = 'p', long = "path")] path: Option, #[arg(required = true, num_args = 1.., value_name = "GPG-ID")] key_identities: Vec, } #[derive(Args)] struct ListArguments { path: Option, } #[derive(Args)] struct ShowArguments { #[arg( short = 'c', long = "clip", num_args = 0..=1, default_missing_value = "1", require_equals = true, conflicts_with = "qrcode", value_name = "LINE" )] clip: Option, #[arg( short = 'q', long = "qrcode", num_args = 0..=1, default_missing_value = "1", require_equals = true, conflicts_with = "clip", value_name = "LINE" )] qrcode: Option, entry: Option, } #[derive(Args)] struct FindArguments { #[arg(required = true, num_args = 1..)] terms: Vec, } #[derive(Args)] struct GrepArguments { #[arg(short = 'i', long = "ignore-case")] ignore_case: bool, #[arg(short = 'v', long = "invert-match")] invert_match: bool, #[arg(short = 'n', long = "line-number")] line_number: bool, #[arg(short = 'F', long = "fixed-strings")] fixed_strings: bool, pattern: String, } #[derive(Args)] struct InsertArguments { #[arg(short = 'e', long = "echo", conflicts_with = "multiline")] echo: bool, #[arg(short = 'm', long = "multiline", conflicts_with = "echo")] multiline: bool, #[arg(short = 'f', long = "force")] force: bool, entry: String, } #[derive(Args)] struct EditArguments { entry: String, } #[derive(Args)] struct GenerateArguments { #[arg(short = 'n', long = "no-symbols")] no_symbols: bool, #[arg(short = 'c', long = "clip", conflicts_with = "qrcode")] clip: bool, #[arg(short = 'q', long = "qrcode", conflicts_with = "clip")] qrcode: bool, #[arg(short = 'i', long = "in-place", conflicts_with = "force")] in_place: bool, #[arg(short = 'f', long = "force", conflicts_with = "in_place")] force: bool, entry: String, length: Option, } #[derive(Args)] struct RemoveArguments { #[arg(short = 'r', long = "recursive")] recursive: bool, #[arg(short = 'f', long = "force")] force: bool, entry: String, } #[derive(Args)] struct MoveArguments { #[arg(short = 'f', long = "force")] force: bool, source: String, destination: String, } #[derive(Args)] struct CopyArguments { #[arg(short = 'f', long = "force")] force: bool, source: String, destination: String, } #[derive(Args)] struct GitArguments { #[command(subcommand)] command: GitCommandArguments, } #[derive(Subcommand)] enum GitCommandArguments { Init, Status, Log { #[arg(short = 'n', long = "max-count")] maximum: Option, }, Diff { paths: Vec, }, Add { #[arg(required = true, num_args = 1..)] paths: Vec, }, Commit { #[arg(short = 'm', long = "message")] message: String, }, Remote { #[command(subcommand)] command: Option, }, Config(GitConfigArguments), Fetch { remote: Option, }, Pull { remote: Option, branch: Option, }, Push { remote: Option, branch: Option, }, Sync { remote: Option, }, } #[derive(Subcommand)] enum GitRemoteArguments { GetUrl { name: String }, Add { name: String, url: String }, SetUrl { name: String, url: String }, Remove { name: String }, } #[derive(Args)] struct GitConfigArguments { #[arg(long, conflicts_with = "values")] get: Option, #[arg(num_args = 2, value_names = ["KEY", "VALUE"])] values: Vec, } #[derive(Args)] struct OtpArguments { #[command(subcommand)] command: OtpCommandArguments, } #[derive(Subcommand)] enum OtpCommandArguments { #[command(visible_alias = "show")] Code(OtpCodeArguments), #[command(visible_alias = "add")] Insert(OtpInsertArguments), Append(OtpAppendArguments), Uri(OtpUriArguments), Validate { uri: String, }, Version, } #[derive(Args)] struct OtpCodeArguments { #[arg(short = 'c', long = "clip")] clipboard: bool, entry: String, } #[derive(Args)] struct OtpInsertArguments { #[arg(short = 'f', long = "force")] force: bool, #[arg(short = 'e', long = "echo")] echo: bool, #[arg(short = 's', long = "secret")] secret: bool, #[arg(short = 'i', long = "issuer")] issuer: Option, #[arg(short = 'a', long = "account")] account: Option, entry: Option, } #[derive(Args)] struct OtpAppendArguments { #[arg(short = 'f', long = "force")] force: bool, #[arg(short = 'e', long = "echo")] echo: bool, #[arg(short = 's', long = "secret")] secret: bool, #[arg(short = 'i', long = "issuer")] issuer: Option, #[arg(short = 'a', long = "account")] account: Option, entry: String, } #[derive(Args)] struct OtpUriArguments { #[arg(short = 'c', long = "clip", conflicts_with = "qrcode")] clipboard: bool, #[arg(short = 'q', long = "qrcode", conflicts_with = "clipboard")] qrcode: bool, entry: String, } #[derive(Args)] struct HelpArguments { topic: Option, } fn convert_arguments(arguments: CliArguments) -> Result { let request = match arguments.command { None => CommandRequest::Show(ShowRequest { entry: None, presentation: Presentation::Terminal, }), Some(CommandArguments::Init(arguments)) => CommandRequest::Init(InitRequest { path: arguments.path, key_identities: arguments.key_identities, }), Some(CommandArguments::List(arguments)) => CommandRequest::List(ListRequest { path: arguments.path, }), Some(CommandArguments::Show(arguments)) => CommandRequest::Show(ShowRequest { entry: arguments.entry, presentation: match (arguments.clip, arguments.qrcode) { (Some(line), None) => Presentation::Clipboard { line }, (None, Some(line)) => Presentation::QrCode { line }, (None, None) => Presentation::Terminal, (Some(_), Some(_)) => unreachable!("clap rejects conflicting presentation flags"), }, }), Some(CommandArguments::Find(arguments)) => CommandRequest::Find(FindRequest { terms: arguments.terms, }), Some(CommandArguments::Grep(arguments)) => CommandRequest::Grep(GrepRequest { pattern: arguments.pattern, ignore_case: arguments.ignore_case, invert_match: arguments.invert_match, line_number: arguments.line_number, fixed_strings: arguments.fixed_strings, }), Some(CommandArguments::Insert(arguments)) => CommandRequest::Insert(InsertRequest { entry: arguments.entry, input: if arguments.multiline { InsertInput::Multiline } else if arguments.echo { InsertInput::EchoedLine } else { InsertInput::HiddenConfirmed }, force: arguments.force, }), Some(CommandArguments::Edit(arguments)) => CommandRequest::Edit(EditRequest { entry: arguments.entry, }), Some(CommandArguments::Generate(arguments)) => CommandRequest::Generate(GenerateRequest { entry: arguments.entry, length: arguments.length, no_symbols: arguments.no_symbols, force: arguments.force, in_place: arguments.in_place, presentation: if arguments.clip { GeneratedPresentation::Clipboard } else if arguments.qrcode { GeneratedPresentation::QrCode } else { GeneratedPresentation::Terminal }, }), Some(CommandArguments::Remove(arguments)) => CommandRequest::Remove(RemoveRequest { entry: arguments.entry, recursive: arguments.recursive, force: arguments.force, }), Some(CommandArguments::Move(arguments)) => CommandRequest::Move(MoveRequest { source: arguments.source, destination: arguments.destination, force: arguments.force, }), Some(CommandArguments::Copy(arguments)) => CommandRequest::Copy(CopyRequest { source: arguments.source, destination: arguments.destination, force: arguments.force, }), Some(CommandArguments::Git(arguments)) => { CommandRequest::Git(convert_git(arguments.command)?) } Some(CommandArguments::Otp(arguments)) => { CommandRequest::Otp(convert_otp(arguments.command)?) } Some(CommandArguments::Completion(arguments)) => CommandRequest::Completion { shell: match arguments.shell { CompletionShellArgument::Bash => CompletionShell::Bash, CompletionShellArgument::Elvish => CompletionShell::Elvish, CompletionShellArgument::Fish => CompletionShell::Fish, CompletionShellArgument::Powershell => CompletionShell::PowerShell, CompletionShellArgument::Zsh => CompletionShell::Zsh, }, }, Some(CommandArguments::Help(arguments)) => { let topic = arguments .topic .map(|topic| { HelpTopic::parse(&topic).ok_or_else(|| usage_error("unknown help topic")) }) .transpose()?; CommandRequest::Help { topic } } Some(CommandArguments::Version) => CommandRequest::Version, }; Ok(CliInvocation { config: arguments.config, request, }) } fn convert_git(arguments: GitCommandArguments) -> Result { Ok(match arguments { GitCommandArguments::Init => GitRequest::Init, GitCommandArguments::Status => GitRequest::Status, GitCommandArguments::Log { maximum } => GitRequest::Log { maximum }, GitCommandArguments::Diff { paths } => GitRequest::Diff { paths }, GitCommandArguments::Add { paths } => GitRequest::Add { paths }, GitCommandArguments::Commit { message } => GitRequest::Commit { message }, GitCommandArguments::Remote { command } => GitRequest::Remote(match command { None => GitRemoteRequest::List, Some(GitRemoteArguments::GetUrl { name }) => GitRemoteRequest::GetUrl { name }, Some(GitRemoteArguments::Add { name, url }) => GitRemoteRequest::Add { name, url }, Some(GitRemoteArguments::SetUrl { name, url }) => { GitRemoteRequest::SetUrl { name, url } } Some(GitRemoteArguments::Remove { name }) => GitRemoteRequest::Remove { name }, }), GitCommandArguments::Config(arguments) => { let request = match (arguments.get, arguments.values.as_slice()) { (Some(key), []) => GitConfigRequest::Get { key }, (None, [key, value]) => GitConfigRequest::Set { key: key.clone(), value: value.clone(), }, _ => return Err(usage_error("git config requires --get KEY or KEY VALUE")), }; GitRequest::Config(request) } GitCommandArguments::Fetch { remote } => GitRequest::Fetch { remote }, GitCommandArguments::Pull { remote, branch } => GitRequest::Pull { remote, branch }, GitCommandArguments::Push { remote, branch } => GitRequest::Push { remote, branch }, GitCommandArguments::Sync { remote } => GitRequest::Sync { remote }, }) } fn convert_otp(arguments: OtpCommandArguments) -> Result { Ok(match arguments { OtpCommandArguments::Code(arguments) => OtpRequest::Code(OtpCodeRequest { entry: arguments.entry, clipboard: arguments.clipboard, }), OtpCommandArguments::Insert(arguments) => OtpRequest::Insert(OtpInsertRequest { entry: arguments.entry, force: arguments.force, echo: arguments.echo, source: otp_source(arguments.secret, arguments.issuer, arguments.account)?, }), OtpCommandArguments::Append(arguments) => OtpRequest::Append(OtpAppendRequest { entry: arguments.entry, force: arguments.force, echo: arguments.echo, source: otp_source(arguments.secret, arguments.issuer, arguments.account)?, }), OtpCommandArguments::Uri(arguments) => OtpRequest::Uri(OtpUriRequest { entry: arguments.entry, presentation: if arguments.clipboard { OtpUriPresentation::Clipboard } else if arguments.qrcode { OtpUriPresentation::QrCode } else { OtpUriPresentation::Terminal }, }), OtpCommandArguments::Validate { uri } => OtpRequest::Validate { uri }, OtpCommandArguments::Version => OtpRequest::Version, }) } fn otp_source( secret: bool, issuer: Option, account: Option, ) -> Result { if secret { if issuer.is_none() && account.is_none() { return Err(usage_error( "--secret requires at least one of --issuer or --account", )); } Ok(OtpInputSource::Secret { issuer, account }) } else if issuer.is_some() || account.is_some() { Err(usage_error("--issuer and --account require --secret")) } else { Ok(OtpInputSource::Uri) } } fn line_input_plan(echo: bool, standard_input_is_terminal: bool) -> InputPlan { if !standard_input_is_terminal { InputPlan::StandardInputLine } else if echo { InputPlan::EchoedLine } else { InputPlan::HiddenConfirmed } } fn usage_error(message: &str) -> CliParseError { CliParseError { message: format!("error: {message}\n"), exit_code: EXIT_USAGE, } } fn normalize_dispatch(mut arguments: Vec) -> Vec { if arguments.is_empty() { arguments.push(OsString::from("ironstorage")); } let Some(command_index) = first_command_index(&arguments) else { return arguments; }; let command = arguments[command_index].to_string_lossy(); const ROOT_COMMANDS: &[&str] = &[ "init", "ls", "list", "show", "find", "search", "grep", "insert", "add", "edit", "generate", "rm", "remove", "delete", "mv", "rename", "cp", "copy", "git", "otp", "help", "version", "completion", ]; if !ROOT_COMMANDS.contains(&command.as_ref()) && !command.starts_with('-') { arguments.insert(command_index, OsString::from("show")); normalize_show_presentation(&mut arguments, command_index + 1); return arguments; } if command == "show" { normalize_show_presentation(&mut arguments, command_index + 1); } else if command == "otp" { normalize_otp_dispatch(&mut arguments, command_index + 1); } arguments } fn normalize_show_presentation(arguments: &mut Vec, mut index: usize) { while index < arguments.len() { let argument = arguments[index].to_string_lossy(); let long_name = match argument.as_ref() { "-c" | "--clip" => Some("--clip"), "-q" | "--qrcode" => Some("--qrcode"), _ => None, }; if let Some(long_name) = long_name { let numeric_value = arguments .get(index + 1) .and_then(|value| value.to_str()) .filter(|value| { !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) }) .map(str::to_owned); if let Some(value) = numeric_value { arguments[index] = OsString::from(format!("{long_name}={value}")); arguments.remove(index + 1); } } else if let Some(value) = argument .strip_prefix("-c") .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())) { arguments[index] = OsString::from(format!("--clip={value}")); } else if let Some(value) = argument .strip_prefix("-q") .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())) { arguments[index] = OsString::from(format!("--qrcode={value}")); } index += 1; } } fn first_command_index(arguments: &[OsString]) -> Option { let mut index = 1; while index < arguments.len() { let argument = arguments[index].to_string_lossy(); if argument == "--config" { index += 2; } else if argument.starts_with("--config=") { index += 1; } else { return Some(index); } } None } fn normalize_otp_dispatch(arguments: &mut Vec, mut index: usize) { while index < arguments.len() { let argument = arguments[index].to_string_lossy(); if argument == "--config" { index += 2; } else if argument.starts_with("--config=") { index += 1; } else { break; } } let Some(argument) = arguments.get(index).map(|value| value.to_string_lossy()) else { return; }; const OTP_COMMANDS: &[&str] = &[ "code", "show", "insert", "add", "append", "uri", "validate", "help", "version", ]; if argument == "help" { arguments[index] = OsString::from("--help"); } else if argument == "--version" { arguments[index] = OsString::from("version"); } else if !OTP_COMMANDS.contains(&argument.as_ref()) && argument != "--help" && argument != "-h" { arguments.insert(index, OsString::from("code")); } }