1use std::ffi::OsString;
6use std::path::PathBuf;
7
8use clap::{Args, Command, Parser, Subcommand, ValueEnum};
9use fs_mistrust::anon_home::PathExt as _;
10use std::sync::LazyLock;
11use tor_config::{ConfigurationSource, ConfigurationSources};
12use tor_config_path::CfgPathError;
13
14use crate::config::default_config_paths;
15
16static DEFAULT_CONFIG_PATHS: LazyLock<Result<Vec<PathBuf>, CfgPathError>> =
20 LazyLock::new(default_config_paths);
21
22#[derive(Clone, Debug, Parser)]
24#[command(author = "The Tor Project Developers")]
25#[command(version)]
26#[command(defer = cli_cmd_post_processing)]
27pub(crate) struct Cli {
28 #[command(subcommand)]
30 pub(crate) command: Commands,
31
32 #[clap(flatten)]
36 pub(crate) global: GlobalArgs,
37}
38
39fn cli_cmd_post_processing(cli: Command) -> Command {
43 fn fmt_help(help: Option<&str>, paths: &[PathBuf]) -> String {
45 let help = help.map(|x| format!("{x}\n\n")).unwrap_or("".to_string());
46 let paths: Vec<_> = paths
47 .iter()
48 .map(|x| x.anonymize_home().to_string())
49 .collect();
50 let paths = paths.join("\n");
51
52 const DESC: &str =
53 "If no paths are provided, the following config paths will be used if they exist:";
54 format!("{help}{DESC}\n\n{paths}")
55 }
56
57 match &*DEFAULT_CONFIG_PATHS {
59 Ok(paths) => cli.mut_arg("config", |arg| {
60 if let Some(help) = arg.get_long_help() {
61 let help = help.to_string();
62 arg.long_help(fmt_help(Some(&help), paths))
63 } else if let Some(help) = arg.get_help() {
64 let help = help.to_string();
65 arg.long_help(fmt_help(Some(&help), paths))
66 } else {
67 arg.long_help(fmt_help(None, paths))
68 }
69 }),
70 Err(_e) => cli,
71 }
72}
73
74#[derive(Clone, Debug, Subcommand)]
76pub(crate) enum Commands {
77 Run(RunArgs),
79 BuildInfo,
81}
82
83#[derive(Clone, Debug, Args)]
86pub(crate) struct GlobalArgs {
87 #[arg(long, short, global = true)]
89 #[arg(value_name = "LEVEL")]
90 pub(crate) log_level: Option<LogLevel>,
91
92 #[arg(long, global = true)]
94 pub(crate) disable_fs_permission_checks: bool,
95
96 #[arg(long = "option", short, global = true)]
98 #[arg(value_name = "KEY=VALUE")]
99 pub(crate) options: Vec<String>,
100
101 #[arg(long, short, global = true)]
106 #[arg(value_name = "PATH")]
107 config: Vec<OsString>,
108}
109
110impl GlobalArgs {
111 pub(crate) fn config(&self) -> Result<ConfigurationSources, CfgPathError> {
117 let mut cfg_sources = ConfigurationSources::try_from_cmdline(
119 || {
120 Ok(DEFAULT_CONFIG_PATHS
121 .as_ref()
122 .map_err(Clone::clone)?
123 .iter()
124 .map(ConfigurationSource::from_path))
125 },
126 &self.config,
127 &self.options,
128 )?;
129
130 if self.disable_fs_permission_checks {
135 cfg_sources.push_option("storage.permissions.dangerously_trust_everyone=true");
136 }
137
138 if let Some(log_level) = self.log_level {
139 cfg_sources.push_option(format!("logging.console={log_level}"));
140 }
141
142 Ok(cfg_sources)
143 }
144}
145
146#[derive(Clone, Debug, Args)]
148pub(crate) struct RunArgs {}
149
150#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
152pub(crate) enum LogLevel {
153 #[value(help = None)]
155 Error,
156 #[value(help = None)]
158 Warn,
159 #[value(help = None)]
161 Info,
162 #[value(help = None)]
164 Debug,
165 #[value(help = None)]
167 Trace,
168}
169
170impl std::fmt::Display for LogLevel {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 match self {
173 Self::Error => write!(f, "error"),
174 Self::Warn => write!(f, "warn"),
175 Self::Info => write!(f, "info"),
176 Self::Debug => write!(f, "debug"),
177 Self::Trace => write!(f, "trace"),
178 }
179 }
180}
181
182impl From<LogLevel> for tracing::metadata::Level {
183 fn from(x: LogLevel) -> Self {
184 match x {
185 LogLevel::Error => Self::ERROR,
186 LogLevel::Warn => Self::WARN,
187 LogLevel::Info => Self::INFO,
188 LogLevel::Debug => Self::DEBUG,
189 LogLevel::Trace => Self::TRACE,
190 }
191 }
192}
193
194#[cfg(test)]
195mod test {
196 #![allow(clippy::bool_assert_comparison)]
198 #![allow(clippy::clone_on_copy)]
199 #![allow(clippy::dbg_macro)]
200 #![allow(clippy::mixed_attributes_style)]
201 #![allow(clippy::print_stderr)]
202 #![allow(clippy::print_stdout)]
203 #![allow(clippy::single_char_pattern)]
204 #![allow(clippy::unwrap_used)]
205 #![allow(clippy::unchecked_duration_subtraction)]
206 #![allow(clippy::useless_vec)]
207 #![allow(clippy::needless_pass_by_value)]
208 use super::*;
211
212 #[test]
213 fn common_flags() {
214 Cli::parse_from(["arti-relay", "build-info"]);
215 Cli::parse_from(["arti-relay", "run"]);
216
217 let cli = Cli::parse_from(["arti-relay", "--log-level", "warn", "run"]);
218 assert_eq!(cli.global.log_level, Some(LogLevel::Warn));
219 let cli = Cli::parse_from(["arti-relay", "run", "--log-level", "warn"]);
220 assert_eq!(cli.global.log_level, Some(LogLevel::Warn));
221
222 let cli = Cli::parse_from(["arti-relay", "--disable-fs-permission-checks", "run"]);
223 assert!(cli.global.disable_fs_permission_checks);
224 let cli = Cli::parse_from(["arti-relay", "run", "--disable-fs-permission-checks"]);
225 assert!(cli.global.disable_fs_permission_checks);
226 }
227
228 #[test]
229 fn clap_bug() {
230 let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run"]);
231 assert_eq!(cli.global.options, vec!["foo=1"]);
232
233 let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "-o", "bar=2", "run"]);
234 assert_eq!(cli.global.options, vec!["foo=1", "bar=2"]);
235
236 let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run", "-o", "bar=2"]);
239 assert_eq!(cli.global.options, vec!["bar=2"]);
240 }
241
242 #[test]
243 fn global_args_are_global() {
244 let cmd = Command::new("test");
245 let cmd = GlobalArgs::augment_args(cmd);
246
247 for arg in cmd.get_arguments() {
249 assert!(
250 arg.is_global_set(),
251 "'global' must be set for {:?}",
252 arg.get_long()
253 );
254 }
255 }
256}