]> git.proxmox.com Git - cargo.git/blob - src/bin/cargo/cli.rs
refactor(cli): Make help behave like other subcommands
[cargo.git] / src / bin / cargo / cli.rs
1 use anyhow::anyhow;
2 use cargo::core::{features, CliUnstable};
3 use cargo::{self, drop_print, drop_println, CliResult, Config};
4 use clap::{AppSettings, Arg, ArgMatches};
5 use itertools::Itertools;
6 use std::collections::HashMap;
7 use std::fmt::Write;
8
9 use super::commands;
10 use super::list_commands;
11 use crate::command_prelude::*;
12 use cargo::core::features::HIDDEN;
13
14 lazy_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).
18 static ref KNOWN_EXTERNAL_COMMAND_DESCRIPTIONS: HashMap<&'static str, &'static str> = HashMap::from([
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."),
21 ]);
22 }
23
24 pub fn main(config: &mut Config) -> CliResult {
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).
28
29 let args = cli().try_get_matches()?;
30
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).
34 let (expanded_args, global_args) = expand_aliases(config, args, vec![])?;
35
36 if expanded_args
37 .get_one::<String>("unstable-features")
38 .map(String::as_str)
39 == Some("help")
40 {
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("_", "-");
56 let padding = " ".repeat(longest_option - option_name.len()); // safe to subtract
57 format!(
58 " -Z {}{} -- {}",
59 option_name_kebab_case, padding, option_help_message
60 )
61 })
62 .collect();
63 let joined = help_lines.join("\n");
64 drop_println!(
65 config,
66 "
67 Available unstable (nightly-only) flags:
68
69 {}
70
71 Run with 'cargo -Z [FLAG] [SUBCOMMAND]'",
72 joined
73 );
74 if !config.nightly_features_allowed {
75 drop_println!(
76 config,
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 }
84 drop_println!(
85 config,
86 "\nSee https://doc.rust-lang.org/nightly/cargo/reference/unstable.html \
87 for more information about these flags."
88 );
89 return Ok(());
90 }
91
92 let is_verbose = expanded_args.verbose() > 0;
93 if expanded_args.flag("version") {
94 let version = get_version_string(is_verbose);
95 drop_print!(config, "{}", version);
96 return Ok(());
97 }
98
99 if let Some(code) = expanded_args.get_one::<String>("explain") {
100 let mut procss = config.load_global_rustc(None)?.process();
101 procss.arg("--explain").arg(code).exec()?;
102 return Ok(());
103 }
104
105 if expanded_args.flag("list") {
106 drop_println!(config, "Installed Commands:");
107 for (name, command) in list_commands(config) {
108 let known_external_desc = KNOWN_EXTERNAL_COMMAND_DESCRIPTIONS.get(name.as_str());
109 match command {
110 CommandInfo::BuiltIn { about } => {
111 assert!(
112 known_external_desc.is_none(),
113 "KNOWN_EXTERNAL_COMMANDS shouldn't contain builtin \"{}\"",
114 name
115 );
116 let summary = about.unwrap_or_default();
117 let summary = summary.lines().next().unwrap_or(&summary); // display only the first line
118 drop_println!(config, " {:<20} {}", name, summary);
119 }
120 CommandInfo::External { path } => {
121 if let Some(desc) = known_external_desc {
122 drop_println!(config, " {:<20} {}", name, desc);
123 } else if is_verbose {
124 drop_println!(config, " {:<20} {}", name, path.display());
125 } else {
126 drop_println!(config, " {}", name);
127 }
128 }
129 CommandInfo::Alias { target } => {
130 drop_println!(
131 config,
132 " {:<20} alias: {}",
133 name,
134 target.iter().join(" ")
135 );
136 }
137 }
138 }
139 return Ok(());
140 }
141
142 let (cmd, subcommand_args) = match expanded_args.subcommand() {
143 Some((cmd, args)) => (cmd, args),
144 _ => {
145 // No subcommand provided.
146 cli().print_help()?;
147 return Ok(());
148 }
149 };
150 config_configure(config, &expanded_args, subcommand_args, global_args)?;
151 super::init_git_transports(config);
152
153 execute_subcommand(config, cmd, subcommand_args)
154 }
155
156 pub fn get_version_string(is_verbose: bool) -> String {
157 let version = cargo::version();
158 let mut version_string = format!("cargo {}\n", version);
159 if is_verbose {
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));
164 }
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);
169 writeln!(version_string, "os: {}", os_info::get()).unwrap();
170 }
171 version_string
172 }
173
174 fn 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
194 fn 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
212 fn 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
223 fn expand_aliases(
224 config: &mut Config,
225 args: ArgMatches,
226 mut already_expanded: Vec<String>,
227 ) -> Result<(ArgMatches, GlobalArgs), CliError> {
228 if let Some((cmd, args)) = args.subcommand() {
229 match (
230 commands::builtin_exec(cmd),
231 super::aliased_command(config, cmd)?,
232 ) {
233 (Some(_), Some(_)) => {
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 }
240 (Some(_), None) => {
241 // Command is built-in and is not conflicting with alias, but contains ignored values.
242 if let Some(mut values) = args.get_many::<String>("") {
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) => {}
251 (_, Some(mut alias)) => {
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!(
258 "\
259 user-defined alias `{}` is shadowing an external subcommand found at: `{}`
260 This was previously accepted but is being phased out; it will become a hard error in a future release.
261 For more information, see issue #10049 <https://github.com/rust-lang/cargo/issues/10049>.",
262 cmd,
263 path.display(),
264 ))?;
265 }
266
267 alias.extend(args.get_many::<String>("").unwrap_or_default().cloned());
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.
272 let global_args = GlobalArgs::new(args);
273 let new_args = cli().no_binary_name(true).try_get_matches_from(alias)?;
274
275 let new_cmd = new_args.subcommand_name().expect("subcommand is required");
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)?;
289 return Ok((expanded_args, global_args));
290 }
291 }
292 };
293
294 Ok((args, GlobalArgs::default()))
295 }
296
297 fn config_configure(
298 config: &mut Config,
299 args: &ArgMatches,
300 subcommand_args: &ArgMatches,
301 global_args: GlobalArgs,
302 ) -> CliResult {
303 let arg_target_dir = &subcommand_args.value_of_path("target-dir", config);
304 let verbose = global_args.verbose + args.verbose();
305 // quiet is unusual because it is redefined in some subcommands in order
306 // to provide custom help text.
307 let quiet = args.flag("quiet") || subcommand_args.flag("quiet") || global_args.quiet;
308 let global_color = global_args.color; // Extract so it can take reference.
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;
316 let mut unstable_flags = global_args.unstable_flags;
317 if let Some(values) = args.get_many::<String>("unstable-features") {
318 unstable_flags.extend(values.cloned());
319 }
320 let mut config_args = global_args.config_args;
321 if let Some(values) = args.get_many::<String>("config") {
322 config_args.extend(values.cloned());
323 }
324 config.configure(
325 verbose,
326 quiet,
327 color,
328 frozen,
329 locked,
330 offline,
331 arg_target_dir,
332 &unstable_flags,
333 &config_args,
334 )?;
335 Ok(())
336 }
337
338 fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatches) -> CliResult {
339 if let Some(exec) = commands::builtin_exec(cmd) {
340 return exec(config, subcommand_args);
341 }
342
343 let mut ext_args: Vec<&str> = vec![cmd];
344 ext_args.extend(
345 subcommand_args
346 .get_many::<String>("")
347 .unwrap_or_default()
348 .map(String::as_str),
349 );
350 super::execute_external_subcommand(config, cmd, &ext_args)
351 }
352
353 #[derive(Default)]
354 struct 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
365 impl GlobalArgs {
366 fn new(args: &ArgMatches) -> GlobalArgs {
367 GlobalArgs {
368 verbose: args.verbose(),
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"),
374 unstable_flags: args
375 .get_many::<String>("unstable-features")
376 .unwrap_or_default()
377 .cloned()
378 .collect(),
379 config_args: args
380 .get_many::<String>("config")
381 .unwrap_or_default()
382 .cloned()
383 .collect(),
384 }
385 }
386 }
387
388 pub fn cli() -> App {
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 };
395 App::new("cargo")
396 .allow_external_subcommands(true)
397 .setting(AppSettings::DeriveDisplayOrder)
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
400 .disable_colored_help(true)
401 // Provide a custom help subcommand for calling into man pages
402 .disable_help_subcommand(true)
403 .override_usage(usage)
404 .help_template(
405 "\
406 Rust's package manager
407
408 USAGE:
409 {usage}
410
411 OPTIONS:
412 {options}
413
414 Some common cargo commands are (see all commands with --list):
415 build, b Compile the current package
416 check, c Analyze the current package and report errors, but don't build object files
417 clean Remove the target directory
418 doc, d Build this package's and its dependencies' documentation
419 new Create a new cargo package
420 init Create a new cargo package in an existing directory
421 add Add dependencies to a manifest file
422 run, r Run a binary or example of the local package
423 test, t Run the tests
424 bench Run the benchmarks
425 update Update dependencies listed in Cargo.lock
426 search Search registry for crates
427 publish Package and upload this package to the registry
428 install Install a Rust binary. Default location is $HOME/.cargo/bin
429 uninstall Uninstall a Rust binary
430
431 See 'cargo help <command>' for more information on a specific command.\n",
432 )
433 .arg(flag("version", "Print version info and exit").short('V'))
434 .arg(flag("list", "List installed commands"))
435 .arg(opt("explain", "Run `rustc --explain CODE`").value_name("CODE"))
436 .arg(
437 opt(
438 "verbose",
439 "Use verbose output (-vv very verbose/build.rs output)",
440 )
441 .short('v')
442 .action(ArgAction::Count)
443 .global(true),
444 )
445 .arg_quiet()
446 .arg(
447 opt("color", "Coloring: auto, always, never")
448 .value_name("WHEN")
449 .global(true),
450 )
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))
454 .arg(multi_opt("config", "KEY=VALUE", "Override a configuration value").global(true))
455 .arg(
456 Arg::new("unstable-features")
457 .help("Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details")
458 .short('Z')
459 .value_name("FLAG")
460 .action(ArgAction::Append)
461 .global(true),
462 )
463 .subcommands(commands::builtin())
464 }
465
466 #[test]
467 fn verify_cli() {
468 cli().debug_assert();
469 }