]> git.proxmox.com Git - cargo.git/blame - src/bin/cargo/cli.rs
refactor(cli): Make help behave like other subcommands
[cargo.git] / src / bin / cargo / cli.rs
CommitLineData
28e1289e 1use anyhow::anyhow;
05d37ae2 2use cargo::core::{features, CliUnstable};
7274307a 3use cargo::{self, drop_print, drop_println, CliResult, Config};
d674c229 4use clap::{AppSettings, Arg, ArgMatches};
e5d10f97 5use itertools::Itertools;
1edd8630 6use std::collections::HashMap;
c5318a17 7use std::fmt::Write;
38f81e05 8
e8f37dae 9use super::commands;
e68c682a 10use super::list_commands;
04ddd4d0 11use crate::command_prelude::*;
05d37ae2 12use cargo::core::features::HIDDEN;
38f81e05 13
1edd8630
NK
14lazy_static::lazy_static! {
15 // Maps from commonly known external commands (not builtin to cargo) to their
16 // description, for the help page. Reserved for external subcommands that are
17 // core within the rust ecosystem (esp ones that might become internal in the future).
144d9417 18 static ref KNOWN_EXTERNAL_COMMAND_DESCRIPTIONS: HashMap<&'static str, &'static str> = HashMap::from([
1edd8630
NK
19 ("clippy", "Checks a package to catch common mistakes and improve your Rust code."),
20 ("fmt", "Formats all bin and lib files of the current crate using rustfmt."),
144d9417 21 ]);
1edd8630
NK
22}
23
e68c682a 24pub fn main(config: &mut Config) -> CliResult {
91015d52
EH
25 // CAUTION: Be careful with using `config` until it is configured below.
26 // In general, try to avoid loading config values unless necessary (like
27 // the [alias] table).
0e26eae5 28
d674c229 29 let args = cli().try_get_matches()?;
7bf9a502 30
e8519ec0 31 // Global args need to be extracted before expanding aliases because the
32 // clap code for extracting a subcommand discards global options
33 // (appearing before the subcommand).
7bf9a502 34 let (expanded_args, global_args) = expand_aliases(config, args, vec![])?;
6363aeca 35
fc0ca1e1
EP
36 if expanded_args
37 .get_one::<String>("unstable-features")
38 .map(String::as_str)
39 == Some("help")
40 {
05d37ae2 41 let options = CliUnstable::help();
42 let non_hidden_options: Vec<(String, String)> = options
43 .iter()
44 .filter(|(_, help_message)| *help_message != HIDDEN)
45 .map(|(name, help)| (name.to_string(), help.to_string()))
46 .collect();
47 let longest_option = non_hidden_options
48 .iter()
49 .map(|(option_name, _)| option_name.len())
50 .max()
51 .unwrap_or(0);
52 let help_lines: Vec<String> = non_hidden_options
53 .iter()
54 .map(|(option_name, option_help_message)| {
55 let option_name_kebab_case = option_name.replace("_", "-");
64439ce3 56 let padding = " ".repeat(longest_option - option_name.len()); // safe to subtract
05d37ae2 57 format!(
58 " -Z {}{} -- {}",
59 option_name_kebab_case, padding, option_help_message
60 )
61 })
62 .collect();
63 let joined = help_lines.join("\n");
7274307a
EH
64 drop_println!(
65 config,
6363aeca
KP
66 "
67Available unstable (nightly-only) flags:
68
05d37ae2 69{}
6363aeca 70
05d37ae2 71Run with 'cargo -Z [FLAG] [SUBCOMMAND]'",
72 joined
6363aeca 73 );
a5720117 74 if !config.nightly_features_allowed {
7274307a
EH
75 drop_println!(
76 config,
35b843ef
EH
77 "\nUnstable flags are only available on the nightly channel \
78 of Cargo, but this is the `{}` channel.\n\
79 {}",
80 features::channel(),
81 features::SEE_CHANNELS
82 );
83 }
7274307a
EH
84 drop_println!(
85 config,
35b843ef
EH
86 "\nSee https://doc.rust-lang.org/nightly/cargo/reference/unstable.html \
87 for more information about these flags."
88 );
6363aeca
KP
89 return Ok(());
90 }
91
55f8a260 92 let is_verbose = expanded_args.verbose() > 0;
fc0ca1e1 93 if expanded_args.flag("version") {
93095a84 94 let version = get_version_string(is_verbose);
7274307a 95 drop_print!(config, "{}", version);
38f81e05
AK
96 return Ok(());
97 }
98
fc0ca1e1 99 if let Some(code) = expanded_args.get_one::<String>("explain") {
0389edc6 100 let mut procss = config.load_global_rustc(None)?.process();
cc3ce000
AK
101 procss.arg("--explain").arg(code).exec()?;
102 return Ok(());
103 }
104
fc0ca1e1 105 if expanded_args.flag("list") {
7274307a 106 drop_println!(config, "Installed Commands:");
0ab79d7a 107 for (name, command) in list_commands(config) {
1edd8630 108 let known_external_desc = KNOWN_EXTERNAL_COMMAND_DESCRIPTIONS.get(name.as_str());
c3422cfc 109 match command {
0ab79d7a 110 CommandInfo::BuiltIn { about } => {
1edd8630
NK
111 assert!(
112 known_external_desc.is_none(),
113 "KNOWN_EXTERNAL_COMMANDS shouldn't contain builtin \"{}\"",
114 name
115 );
af2c3555 116 let summary = about.unwrap_or_default();
a838f8fa 117 let summary = summary.lines().next().unwrap_or(&summary); // display only the first line
7274307a 118 drop_println!(config, " {:<20} {}", name, summary);
c3422cfc 119 }
0ab79d7a 120 CommandInfo::External { path } => {
1edd8630
NK
121 if let Some(desc) = known_external_desc {
122 drop_println!(config, " {:<20} {}", name, desc);
123 } else if is_verbose {
7274307a 124 drop_println!(config, " {:<20} {}", name, path.display());
c3422cfc 125 } else {
7274307a 126 drop_println!(config, " {}", name);
c3422cfc 127 }
0470d775 128 }
0ab79d7a 129 CommandInfo::Alias { target } => {
910569aa
EH
130 drop_println!(
131 config,
132 " {:<20} alias: {}",
133 name,
134 target.iter().join(" ")
135 );
1c5e68b4 136 }
0470d775
AK
137 }
138 }
139 return Ok(());
140 }
141
0279e8e6 142 let (cmd, subcommand_args) = match expanded_args.subcommand() {
f17ecafc 143 Some((cmd, args)) => (cmd, args),
91015d52
EH
144 _ => {
145 // No subcommand provided.
146 cli().print_help()?;
147 return Ok(());
148 }
149 };
0279e8e6 150 config_configure(config, &expanded_args, subcommand_args, global_args)?;
239ebf4f 151 super::init_git_transports(config);
d49910fd 152
91015d52 153 execute_subcommand(config, cmd, subcommand_args)
93875b88
AK
154}
155
93095a84
MK
156pub fn get_version_string(is_verbose: bool) -> String {
157 let version = cargo::version();
fdc398e7 158 let mut version_string = format!("cargo {}\n", version);
93095a84 159 if is_verbose {
295ea6d9
EH
160 version_string.push_str(&format!("release: {}\n", version.version));
161 if let Some(ref ci) = version.commit_info {
162 version_string.push_str(&format!("commit-hash: {}\n", ci.commit_hash));
163 version_string.push_str(&format!("commit-date: {}\n", ci.commit_date));
93095a84 164 }
c5318a17
EH
165 writeln!(version_string, "host: {}", env!("RUST_HOST_TARGET")).unwrap();
166 add_libgit2(&mut version_string);
167 add_curl(&mut version_string);
168 add_ssl(&mut version_string);
5a8be7ba 169 writeln!(version_string, "os: {}", os_info::get()).unwrap();
93095a84
MK
170 }
171 version_string
172}
173
c5318a17
EH
174fn add_libgit2(version_string: &mut String) {
175 let git2_v = git2::Version::get();
176 let lib_v = git2_v.libgit2_version();
177 let vendored = if git2_v.vendored() {
178 format!("vendored")
179 } else {
180 format!("system")
181 };
182 writeln!(
183 version_string,
184 "libgit2: {}.{}.{} (sys:{} {})",
185 lib_v.0,
186 lib_v.1,
187 lib_v.2,
188 git2_v.crate_version(),
189 vendored
190 )
191 .unwrap();
192}
193
194fn add_curl(version_string: &mut String) {
195 let curl_v = curl::Version::get();
196 let vendored = if curl_v.vendored() {
197 format!("vendored")
198 } else {
199 format!("system")
200 };
201 writeln!(
202 version_string,
203 "libcurl: {} (sys:{} {} ssl:{})",
204 curl_v.version(),
205 curl_sys::rust_crate_version(),
206 vendored,
207 curl_v.ssl_version().unwrap_or("none")
208 )
209 .unwrap();
210}
211
212fn add_ssl(version_string: &mut String) {
213 #[cfg(feature = "openssl")]
214 {
215 writeln!(version_string, "ssl: {}", openssl::version::version()).unwrap();
216 }
217 #[cfg(not(feature = "openssl"))]
218 {
219 let _ = version_string; // Silence unused warning.
220 }
221}
222
d49910fd
AK
223fn expand_aliases(
224 config: &mut Config,
f17ecafc 225 args: ArgMatches,
28e1289e 226 mut already_expanded: Vec<String>,
f17ecafc
EP
227) -> Result<(ArgMatches, GlobalArgs), CliError> {
228 if let Some((cmd, args)) = args.subcommand() {
d49910fd
AK
229 match (
230 commands::builtin_exec(cmd),
231 super::aliased_command(config, cmd)?,
232 ) {
e8f37dae 233 (Some(_), Some(_)) => {
ea1f525c
HY
234 // User alias conflicts with a built-in subcommand
235 config.shell().warn(format!(
236 "user-defined alias `{}` is ignored, because it is shadowed by a built-in command",
237 cmd,
238 ))?;
239 }
e5d10f97
AA
240 (Some(_), None) => {
241 // Command is built-in and is not conflicting with alias, but contains ignored values.
fc0ca1e1 242 if let Some(mut values) = args.get_many::<String>("") {
e5d10f97
AA
243 config.shell().warn(format!(
244 "trailing arguments after built-in command `{}` are ignored: `{}`",
245 cmd,
246 values.join(" "),
247 ))?;
248 }
249 }
250 (None, None) => {}
e8f37dae 251 (_, Some(mut alias)) => {
5bfd345e
BH
252 // Check if this alias is shadowing an external subcommand
253 // (binary of the form `cargo-<subcommand>`)
254 // Currently this is only a warning, but after a transition period this will become
255 // a hard error.
256 if let Some(path) = super::find_external_subcommand(config, cmd) {
257 config.shell().warn(format!(
cba8503e
BH
258 "\
259user-defined alias `{}` is shadowing an external subcommand found at: `{}`
260This was previously accepted but is being phased out; it will become a hard error in a future release.
261For more information, see issue #10049 <https://github.com/rust-lang/cargo/issues/10049>.",
5bfd345e
BH
262 cmd,
263 path.display(),
264 ))?;
265 }
266
fc0ca1e1 267 alias.extend(args.get_many::<String>("").unwrap_or_default().cloned());
0279e8e6
EH
268 // new_args strips out everything before the subcommand, so
269 // capture those global options now.
270 // Note that an alias to an external command will not receive
271 // these arguments. That may be confusing, but such is life.
2b1dc3e5 272 let global_args = GlobalArgs::new(args);
f78ffb9f 273 let new_args = cli().no_binary_name(true).try_get_matches_from(alias)?;
28e1289e 274
f17ecafc 275 let new_cmd = new_args.subcommand_name().expect("subcommand is required");
28e1289e
NK
276 already_expanded.push(cmd.to_string());
277 if already_expanded.contains(&new_cmd.to_string()) {
278 // Crash if the aliases are corecursive / unresolvable
279 return Err(anyhow!(
280 "alias {} has unresolvable recursive definition: {} -> {}",
281 already_expanded[0],
282 already_expanded.join(" -> "),
283 new_cmd,
284 )
285 .into());
286 }
287
288 let (expanded_args, _) = expand_aliases(config, new_args, already_expanded)?;
0279e8e6 289 return Ok((expanded_args, global_args));
d49910fd 290 }
d49910fd
AK
291 }
292 };
ea1f525c 293
0279e8e6 294 Ok((args, GlobalArgs::default()))
d49910fd
AK
295}
296
91015d52
EH
297fn config_configure(
298 config: &mut Config,
f17ecafc
EP
299 args: &ArgMatches,
300 subcommand_args: &ArgMatches,
0279e8e6 301 global_args: GlobalArgs,
91015d52 302) -> CliResult {
fc0ca1e1 303 let arg_target_dir = &subcommand_args.value_of_path("target-dir", config);
55f8a260 304 let verbose = global_args.verbose + args.verbose();
0279e8e6
EH
305 // quiet is unusual because it is redefined in some subcommands in order
306 // to provide custom help text.
fc0ca1e1 307 let quiet = args.flag("quiet") || subcommand_args.flag("quiet") || global_args.quiet;
0279e8e6 308 let global_color = global_args.color; // Extract so it can take reference.
fc0ca1e1
EP
309 let color = args
310 .get_one::<String>("color")
311 .map(String::as_str)
312 .or_else(|| global_color.as_deref());
313 let frozen = args.flag("frozen") || global_args.frozen;
314 let locked = args.flag("locked") || global_args.locked;
315 let offline = args.flag("offline") || global_args.offline;
0279e8e6 316 let mut unstable_flags = global_args.unstable_flags;
fc0ca1e1
EP
317 if let Some(values) = args.get_many::<String>("unstable-features") {
318 unstable_flags.extend(values.cloned());
0279e8e6
EH
319 }
320 let mut config_args = global_args.config_args;
fc0ca1e1
EP
321 if let Some(values) = args.get_many::<String>("config") {
322 config_args.extend(values.cloned());
0279e8e6 323 }
7dcc030e 324 config.configure(
0279e8e6 325 verbose,
91015d52 326 quiet,
0279e8e6
EH
327 color,
328 frozen,
329 locked,
330 offline,
dd0b7a2c 331 arg_target_dir,
0279e8e6 332 &unstable_flags,
91015d52 333 &config_args,
7dcc030e 334 )?;
91015d52
EH
335 Ok(())
336}
93875b88 337
f17ecafc 338fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatches) -> CliResult {
e8f37dae 339 if let Some(exec) = commands::builtin_exec(cmd) {
dd0b7a2c 340 return exec(config, subcommand_args);
6b9c063b 341 }
666e232b 342
6b9c063b 343 let mut ext_args: Vec<&str> = vec![cmd];
fc0ca1e1
EP
344 ext_args.extend(
345 subcommand_args
346 .get_many::<String>("")
347 .unwrap_or_default()
348 .map(String::as_str),
349 );
6b9c063b 350 super::execute_external_subcommand(config, cmd, &ext_args)
38f81e05
AK
351}
352
0279e8e6
EH
353#[derive(Default)]
354struct GlobalArgs {
355 verbose: u32,
356 quiet: bool,
357 color: Option<String>,
358 frozen: bool,
359 locked: bool,
360 offline: bool,
361 unstable_flags: Vec<String>,
362 config_args: Vec<String>,
363}
364
365impl GlobalArgs {
f17ecafc 366 fn new(args: &ArgMatches) -> GlobalArgs {
0279e8e6 367 GlobalArgs {
55f8a260 368 verbose: args.verbose(),
fc0ca1e1
EP
369 quiet: args.flag("quiet"),
370 color: args.get_one::<String>("color").cloned(),
371 frozen: args.flag("frozen"),
372 locked: args.flag("locked"),
373 offline: args.flag("offline"),
0279e8e6 374 unstable_flags: args
fc0ca1e1
EP
375 .get_many::<String>("unstable-features")
376 .unwrap_or_default()
377 .cloned()
378 .collect(),
0279e8e6 379 config_args: args
fc0ca1e1 380 .get_many::<String>("config")
0279e8e6 381 .unwrap_or_default()
fc0ca1e1 382 .cloned()
0279e8e6
EH
383 .collect(),
384 }
385 }
386}
387
d674c229 388pub fn cli() -> App {
337fd9f0
EH
389 let is_rustup = std::env::var_os("RUSTUP_HOME").is_some();
390 let usage = if is_rustup {
391 "cargo [+toolchain] [OPTIONS] [SUBCOMMAND]"
392 } else {
393 "cargo [OPTIONS] [SUBCOMMAND]"
394 };
d085b5e7 395 App::new("cargo")
f78ffb9f 396 .allow_external_subcommands(true)
fc0ca1e1 397 .setting(AppSettings::DeriveDisplayOrder)
f17ecafc
EP
398 // Doesn't mix well with our list of common cargo commands. See clap-rs/clap#3108 for
399 // opening clap up to allow us to style our help template
f78ffb9f 400 .disable_colored_help(true)
d674c229
EP
401 // Provide a custom help subcommand for calling into man pages
402 .disable_help_subcommand(true)
aed03125 403 .override_usage(usage)
5cd062e4 404 .help_template(
1e682848 405 "\
c180e176
AK
406Rust's package manager
407
408USAGE:
409 {usage}
410
411OPTIONS:
f17ecafc 412{options}
c180e176
AK
413
414Some common cargo commands are (see all commands with --list):
a3118e1c
IT
415 build, b Compile the current package
416 check, c Analyze the current package and report errors, but don't build object files
c180e176 417 clean Remove the target directory
82636e86 418 doc, d Build this package's and its dependencies' documentation
3492a390
ZL
419 new Create a new cargo package
420 init Create a new cargo package in an existing directory
f0ede38f 421 add Add dependencies to a manifest file
a3118e1c
IT
422 run, r Run a binary or example of the local package
423 test, t Run the tests
c180e176
AK
424 bench Run the benchmarks
425 update Update dependencies listed in Cargo.lock
426 search Search registry for crates
3492a390 427 publish Package and upload this package to the registry
b738f486 428 install Install a Rust binary. Default location is $HOME/.cargo/bin
c180e176
AK
429 uninstall Uninstall a Rust binary
430
7b36e963 431See 'cargo help <command>' for more information on a specific command.\n",
38f81e05 432 )
fc0ca1e1
EP
433 .arg(flag("version", "Print version info and exit").short('V'))
434 .arg(flag("list", "List installed commands"))
1e682848 435 .arg(opt("explain", "Run `rustc --explain CODE`").value_name("CODE"))
38f81e05 436 .arg(
1e682848
AC
437 opt(
438 "verbose",
439 "Use verbose output (-vv very verbose/build.rs output)",
e5a11190 440 )
f17ecafc 441 .short('v')
fc0ca1e1 442 .action(ArgAction::Count)
e5a11190 443 .global(true),
38f81e05 444 )
a3e3ff99 445 .arg_quiet()
38f81e05
AK
446 .arg(
447 opt("color", "Coloring: auto, always, never")
1e682848
AC
448 .value_name("WHEN")
449 .global(true),
8518be6c 450 )
fc0ca1e1
EP
451 .arg(flag("frozen", "Require Cargo.lock and cache are up to date").global(true))
452 .arg(flag("locked", "Require Cargo.lock is up to date").global(true))
453 .arg(flag("offline", "Run without accessing the network").global(true))
0ecdde53 454 .arg(multi_opt("config", "KEY=VALUE", "Override a configuration value").global(true))
38f81e05 455 .arg(
88a122c4 456 Arg::new("unstable-features")
6363aeca 457 .help("Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details")
f17ecafc 458 .short('Z')
1e682848 459 .value_name("FLAG")
fc0ca1e1 460 .action(ArgAction::Append)
1e682848 461 .global(true),
38f81e05 462 )
d085b5e7 463 .subcommands(commands::builtin())
38f81e05 464}
f17ecafc
EP
465
466#[test]
467fn verify_cli() {
468 cli().debug_assert();
469}