//! The command-line interface. //! //! See [`Cli`]. use std::ffi::OsString; use std::path::PathBuf; use clap::{Args, Command, Parser, Subcommand, ValueEnum}; use fs_mistrust::anon_home::PathExt as _; use std::sync::LazyLock; use tor_config::{ConfigurationSource, ConfigurationSources}; use tor_config_path::CfgPathError; use crate::config::default_config_paths; /// A cached copy of the default config paths. /// /// We cache the values to ensure they are consistent between the help text and the values used. static DEFAULT_CONFIG_PATHS: LazyLock, CfgPathError>> = LazyLock::new(default_config_paths); /// A Rust Tor relay implementation. #[derive(Clone, Debug, Parser)] #[command(author = "The Tor Project Developers")] #[command(version)] #[command(defer = cli_cmd_post_processing)] pub(crate) struct Cli { /// Sub-commands. #[command(subcommand)] pub(crate) command: Commands, /// Global arguments available for all sub-commands. /// /// These arguments may be specified before or after the subcommand argument. #[clap(flatten)] pub(crate) global: GlobalArgs, } /// Perform post-processing on the [`Command`] generated by clap for [`Cli`]. /// /// We use this to append the default config paths to the help text. fn cli_cmd_post_processing(cli: Command) -> Command { /// Append the paths to the help text. fn fmt_help(help: Option<&str>, paths: &[PathBuf]) -> String { let help = help.map(|x| format!("{x}\n\n")).unwrap_or("".to_string()); let paths: Vec<_> = paths .iter() .map(|x| x.anonymize_home().to_string()) .collect(); let paths = paths.join("\n"); const DESC: &str = "If no paths are provided, the following config paths will be used if they exist:"; format!("{help}{DESC}\n\n{paths}") } // Show the default paths in the "--help" text. match &*DEFAULT_CONFIG_PATHS { Ok(paths) => cli.mut_arg("config", |arg| { if let Some(help) = arg.get_long_help() { let help = help.to_string(); arg.long_help(fmt_help(Some(&help), paths)) } else if let Some(help) = arg.get_help() { let help = help.to_string(); arg.long_help(fmt_help(Some(&help), paths)) } else { arg.long_help(fmt_help(None, paths)) } }), Err(_e) => cli, } } /// Main subcommands. #[derive(Clone, Debug, Subcommand)] pub(crate) enum Commands { /// Run the relay. Run(RunArgs), /// Print build information. BuildInfo, } /// Global arguments for all commands. // NOTE: `global = true` should be set for each field #[derive(Clone, Debug, Args)] pub(crate) struct GlobalArgs { /// Override the log level from the configuration. #[arg(long, short, global = true)] #[arg(value_name = "LEVEL")] pub(crate) log_level: Option, /// Don't check permissions on the files we use. #[arg(long, global = true)] pub(crate) disable_fs_permission_checks: bool, /// Override config file parameters, using TOML-like syntax. #[arg(long = "option", short, global = true)] #[arg(value_name = "KEY=VALUE")] pub(crate) options: Vec, /// Config files and directories to read. // NOTE: We append the default config paths to the help text in `cli_cmd_post_processing`. // NOTE: This value does not take into account the default config paths, // so this is private while the `GlobalArgs::config()` method is public instead. #[arg(long, short, global = true)] #[arg(value_name = "PATH")] config: Vec, } impl GlobalArgs { /// Get the configuration sources. /// /// You may also want to set a [`Mistrust`](fs_mistrust::Mistrust) /// and any additional configuration option overrides /// using [`push_option`](ConfigurationSources::push_option). pub(crate) fn config(&self) -> Result { // Use `try_from_cmdline` to be consistent with Arti. let mut cfg_sources = ConfigurationSources::try_from_cmdline( || { Ok(DEFAULT_CONFIG_PATHS .as_ref() .map_err(Clone::clone)? .iter() .map(ConfigurationSource::from_path)) }, &self.config, &self.options, )?; // TODO: These text strings may become stale if the configuration structure changes, // and they're not checked at compile time. // Can we change `ConfigurationSources` in some way to allow overrides from an existing // builder? if self.disable_fs_permission_checks { cfg_sources.push_option("storage.permissions.dangerously_trust_everyone=true"); } if let Some(log_level) = self.log_level { cfg_sources.push_option(format!("logging.console={log_level}")); } Ok(cfg_sources) } } /// Arguments when running an Arti relay. #[derive(Clone, Debug, Args)] pub(crate) struct RunArgs {} /// Log levels allowed by the cli. #[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] pub(crate) enum LogLevel { /// See [`tracing::Level::ERROR`]. #[value(help = None)] Error, /// See [`tracing::Level::WARN`]. #[value(help = None)] Warn, /// See [`tracing::Level::INFO`]. #[value(help = None)] Info, /// See [`tracing::Level::DEBUG`]. #[value(help = None)] Debug, /// See [`tracing::Level::TRACE`]. #[value(help = None)] Trace, } impl std::fmt::Display for LogLevel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Error => write!(f, "error"), Self::Warn => write!(f, "warn"), Self::Info => write!(f, "info"), Self::Debug => write!(f, "debug"), Self::Trace => write!(f, "trace"), } } } impl From for tracing::metadata::Level { fn from(x: LogLevel) -> Self { match x { LogLevel::Error => Self::ERROR, LogLevel::Warn => Self::WARN, LogLevel::Info => Self::INFO, LogLevel::Debug => Self::DEBUG, LogLevel::Trace => Self::TRACE, } } } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! use super::*; #[test] fn common_flags() { Cli::parse_from(["arti-relay", "build-info"]); Cli::parse_from(["arti-relay", "run"]); let cli = Cli::parse_from(["arti-relay", "--log-level", "warn", "run"]); assert_eq!(cli.global.log_level, Some(LogLevel::Warn)); let cli = Cli::parse_from(["arti-relay", "run", "--log-level", "warn"]); assert_eq!(cli.global.log_level, Some(LogLevel::Warn)); let cli = Cli::parse_from(["arti-relay", "--disable-fs-permission-checks", "run"]); assert!(cli.global.disable_fs_permission_checks); let cli = Cli::parse_from(["arti-relay", "run", "--disable-fs-permission-checks"]); assert!(cli.global.disable_fs_permission_checks); } #[test] fn clap_bug() { let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run"]); assert_eq!(cli.global.options, vec!["foo=1"]); let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "-o", "bar=2", "run"]); assert_eq!(cli.global.options, vec!["foo=1", "bar=2"]); // this is https://github.com/clap-rs/clap/issues/3938 // TODO: this is a footgun, and we should consider alternatives to clap's 'global' args let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run", "-o", "bar=2"]); assert_eq!(cli.global.options, vec!["bar=2"]); } #[test] fn global_args_are_global() { let cmd = Command::new("test"); let cmd = GlobalArgs::augment_args(cmd); // check that each argument in `GlobalArgs` has "global" set for arg in cmd.get_arguments() { assert!( arg.is_global_set(), "'global' must be set for {:?}", arg.get_long() ); } } }