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;
static DEFAULT_CONFIG_PATHS: LazyLock<Result<Vec<PathBuf>, CfgPathError>> =
LazyLock::new(default_config_paths);
#[derive(Clone, Debug, Parser)]
#[command(author = "The Tor Project Developers")]
#[command(version)]
#[command(defer = cli_cmd_post_processing)]
pub(crate) struct Cli {
#[command(subcommand)]
pub(crate) command: Commands,
#[clap(flatten)]
pub(crate) global: GlobalArgs,
}
fn cli_cmd_post_processing(cli: Command) -> Command {
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(|path| {
let mut anon = path.anonymize_home().to_string();
if path.to_string_lossy().ends_with('/') && !anon.ends_with('/') {
anon.push('/');
}
anon
})
.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}")
}
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,
}
}
#[derive(Clone, Debug, Subcommand)]
pub(crate) enum Commands {
Run(RunArgs),
BuildInfo,
}
#[derive(Clone, Debug, Args)]
pub(crate) struct GlobalArgs {
#[arg(long, short, global = true)]
#[arg(value_name = "LEVEL")]
pub(crate) log_level: Option<LogLevel>,
#[arg(long, global = true)]
pub(crate) disable_fs_permission_checks: bool,
#[arg(long = "option", short, global = true)]
#[arg(value_name = "KEY=VALUE")]
pub(crate) options: Vec<String>,
#[arg(long, short, global = true)]
#[arg(value_name = "PATH")]
config: Vec<OsString>,
}
impl GlobalArgs {
pub(crate) fn config(&self) -> Result<ConfigurationSources, CfgPathError> {
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,
)?;
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)
}
}
#[derive(Clone, Debug, Args)]
pub(crate) struct RunArgs {}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum LogLevel {
#[value(help = None)]
Error,
#[value(help = None)]
Warn,
#[value(help = None)]
Info,
#[value(help = None)]
Debug,
#[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<LogLevel> 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 {
#![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_time_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"]);
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);
for arg in cmd.get_arguments() {
assert!(
arg.is_global_set(),
"'global' must be set for {:?}",
arg.get_long()
);
}
}
}