blob: 2277bd6f8364f9c5c7a435178e4ac1c488c0a474 [file] [log] [blame]
//! Implementation of `cargo config` subcommand.
use crate::util::config::{Config, ConfigKey, ConfigValue as CV, Definition};
use crate::util::errors::CargoResult;
use crate::{drop_eprintln, drop_println};
use anyhow::{bail, format_err, Error};
use serde_json::json;
use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
pub enum ConfigFormat {
Toml,
Json,
JsonValue,
}
impl ConfigFormat {
/// For clap.
pub const POSSIBLE_VALUES: [&'static str; 3] = ["toml", "json", "json-value"];
}
impl FromStr for ConfigFormat {
type Err = Error;
fn from_str(s: &str) -> CargoResult<Self> {
match s {
"toml" => Ok(ConfigFormat::Toml),
"json" => Ok(ConfigFormat::Json),
"json-value" => Ok(ConfigFormat::JsonValue),
f => bail!("unknown config format `{}`", f),
}
}
}
impl fmt::Display for ConfigFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ConfigFormat::Toml => write!(f, "toml"),
ConfigFormat::Json => write!(f, "json"),
ConfigFormat::JsonValue => write!(f, "json-value"),
}
}
}
/// Options for `cargo config get`.
pub struct GetOptions<'a> {
pub key: Option<&'a str>,
pub format: ConfigFormat,
pub show_origin: bool,
pub merged: bool,
}
pub fn get(config: &Config, opts: &GetOptions<'_>) -> CargoResult<()> {
if opts.show_origin && !matches!(opts.format, ConfigFormat::Toml) {
bail!(
"the `{}` format does not support --show-origin, try the `toml` format instead",
opts.format
);
}
let key = match opts.key {
Some(key) => ConfigKey::from_str(key),
None => ConfigKey::new(),
};
if opts.merged {
let cv = config
.get_cv_with_env(&key)?
.ok_or_else(|| format_err!("config value `{}` is not set", key))?;
match opts.format {
ConfigFormat::Toml => print_toml(config, opts, &key, &cv),
ConfigFormat::Json => print_json(config, &key, &cv, true),
ConfigFormat::JsonValue => print_json(config, &key, &cv, false),
}
if let Some(env) = maybe_env(config, &key, &cv) {
match opts.format {
ConfigFormat::Toml => print_toml_env(config, &env),
ConfigFormat::Json | ConfigFormat::JsonValue => print_json_env(config, &env),
}
}
} else {
match &opts.format {
ConfigFormat::Toml => print_toml_unmerged(config, opts, &key)?,
format => bail!(
"the `{}` format does not support --merged=no, try the `toml` format instead",
format
),
}
}
Ok(())
}
/// Checks for environment variables that might be used.
fn maybe_env<'config>(
config: &'config Config,
key: &ConfigKey,
cv: &CV,
) -> Option<Vec<(&'config str, &'config str)>> {
// Only fetching a table is unable to load env values. Leaf entries should
// work properly.
match cv {
CV::Table(_map, _def) => {}
_ => return None,
}
let mut env: Vec<_> = config
.env()
.filter(|(env_key, _val)| env_key.starts_with(&format!("{}_", key.as_env_key())))
.collect();
env.sort_by_key(|x| x.0);
if env.is_empty() {
None
} else {
Some(env)
}
}
fn print_toml(config: &Config, opts: &GetOptions<'_>, key: &ConfigKey, cv: &CV) {
let origin = |def: &Definition| -> String {
if !opts.show_origin {
return "".to_string();
}
format!(" # {}", def)
};
match cv {
CV::Boolean(val, def) => drop_println!(config, "{} = {}{}", key, val, origin(def)),
CV::Integer(val, def) => drop_println!(config, "{} = {}{}", key, val, origin(def)),
CV::String(val, def) => drop_println!(
config,
"{} = {}{}",
key,
toml_edit::Value::from(val),
origin(def)
),
CV::List(vals, _def) => {
if opts.show_origin {
drop_println!(config, "{} = [", key);
for (val, def) in vals {
drop_println!(
config,
" {}, # {}",
serde::Serialize::serialize(val, toml_edit::ser::ValueSerializer::new())
.unwrap(),
def
);
}
drop_println!(config, "]");
} else {
let vals: toml_edit::Array = vals.iter().map(|x| &x.0).collect();
drop_println!(config, "{} = {}", key, vals);
}
}
CV::Table(table, _def) => {
let mut key_vals: Vec<_> = table.iter().collect();
key_vals.sort_by(|a, b| a.0.cmp(b.0));
for (table_key, val) in key_vals {
let mut subkey = key.clone();
// push or push_sensitive shouldn't matter here, since this is
// not dealing with environment variables.
subkey.push(table_key);
print_toml(config, opts, &subkey, val);
}
}
}
}
fn print_toml_env(config: &Config, env: &[(&str, &str)]) {
drop_println!(
config,
"# The following environment variables may affect the loaded values."
);
for (env_key, env_value) in env {
let val = shell_escape::escape(Cow::Borrowed(env_value));
drop_println!(config, "# {}={}", env_key, val);
}
}
fn print_json_env(config: &Config, env: &[(&str, &str)]) {
drop_eprintln!(
config,
"note: The following environment variables may affect the loaded values."
);
for (env_key, env_value) in env {
let val = shell_escape::escape(Cow::Borrowed(env_value));
drop_eprintln!(config, "{}={}", env_key, val);
}
}
fn print_json(config: &Config, key: &ConfigKey, cv: &CV, include_key: bool) {
let json_value = if key.is_root() || !include_key {
cv_to_json(cv)
} else {
let mut parts: Vec<_> = key.parts().collect();
let last_part = parts.pop().unwrap();
let mut root_table = json!({});
// Create a JSON object with nested keys up to the value being displayed.
let mut table = &mut root_table;
for part in parts {
table[part] = json!({});
table = table.get_mut(part).unwrap();
}
table[last_part] = cv_to_json(cv);
root_table
};
drop_println!(config, "{}", serde_json::to_string(&json_value).unwrap());
// Helper for recursively converting a CV to JSON.
fn cv_to_json(cv: &CV) -> serde_json::Value {
match cv {
CV::Boolean(val, _def) => json!(val),
CV::Integer(val, _def) => json!(val),
CV::String(val, _def) => json!(val),
CV::List(vals, _def) => {
let jvals: Vec<_> = vals.iter().map(|(val, _def)| json!(val)).collect();
json!(jvals)
}
CV::Table(map, _def) => {
let mut table = json!({});
for (key, val) in map {
table[key] = cv_to_json(val);
}
table
}
}
}
}
fn print_toml_unmerged(config: &Config, opts: &GetOptions<'_>, key: &ConfigKey) -> CargoResult<()> {
let print_table = |cv: &CV| {
drop_println!(config, "# {}", cv.definition());
print_toml(config, opts, &ConfigKey::new(), cv);
drop_println!(config, "");
};
// This removes entries from the given CV so that all that remains is the
// given key. Returns false if no entries were found.
fn trim_cv(mut cv: &mut CV, key: &ConfigKey) -> CargoResult<bool> {
for (i, part) in key.parts().enumerate() {
match cv {
CV::Table(map, _def) => {
map.retain(|key, _value| key == part);
match map.get_mut(part) {
Some(val) => cv = val,
None => return Ok(false),
}
}
_ => {
let mut key_so_far = ConfigKey::new();
for part in key.parts().take(i) {
key_so_far.push(part);
}
bail!(
"expected table for configuration key `{}`, \
but found {} in {}",
key_so_far,
cv.desc(),
cv.definition()
)
}
}
}
Ok(match cv {
CV::Table(map, _def) => !map.is_empty(),
_ => true,
})
}
let mut cli_args = config.cli_args_as_table()?;
if trim_cv(&mut cli_args, key)? {
print_table(&cli_args);
}
// This slurps up some extra env vars that aren't technically part of the
// "config" (or are special-cased). I'm personally fine with just keeping
// them here, though it might be confusing. The vars I'm aware of:
//
// * CARGO
// * CARGO_HOME
// * CARGO_NAME
// * CARGO_EMAIL
// * CARGO_INCREMENTAL
// * CARGO_TARGET_DIR
// * CARGO_CACHE_RUSTC_INFO
//
// All of these except CARGO, CARGO_HOME, and CARGO_CACHE_RUSTC_INFO are
// actually part of the config, but they are special-cased in the code.
//
// TODO: It might be a good idea to teach the Config loader to support
// environment variable aliases so that these special cases are less
// special, and will just naturally get loaded as part of the config.
let mut env: Vec<_> = config
.env()
.filter(|(env_key, _val)| env_key.starts_with(key.as_env_key()))
.collect();
if !env.is_empty() {
env.sort_by_key(|x| x.0);
drop_println!(config, "# Environment variables");
for (key, value) in env {
// Displaying this in "shell" syntax instead of TOML, since that
// somehow makes more sense to me.
let val = shell_escape::escape(Cow::Borrowed(value));
drop_println!(config, "# {}={}", key, val);
}
drop_println!(config, "");
}
let unmerged = config.load_values_unmerged()?;
for mut cv in unmerged {
if trim_cv(&mut cv, key)? {
print_table(&cv);
}
}
Ok(())
}