use std::io::Read as _;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::args::EnvVarArg;
use crate::agecrypt;
use crate::config::config_file::ConfigFile;
use crate::config::config_file::mise_toml::MiseToml;
use crate::config::env_directive::EnvDirective;
use crate::config::{Config, ConfigPathOptions, Settings, resolve_target_config_path};
use crate::env::{self};
use crate::file::display_path;
use crate::ui::table;
use demand::Input;
use eyre::{Result, bail, eyre};
use tabled::Tabled;
#[derive(Debug, clap::Args)]
#[clap(aliases = ["ev", "env-vars"], verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Set {
#[clap(value_name = "ENV_VAR", verbatim_doc_comment)]
env_vars: Option<Vec<EnvVarArg>>,
#[clap(short = 'E', long, overrides_with_all = &["global", "file"])]
env: Option<String>,
#[clap(short, long, verbatim_doc_comment, overrides_with_all = &["file", "env"])]
global: bool,
#[clap(long, requires = "env_vars")]
age_encrypt: bool,
#[clap(long, value_name = "PATH", requires = "age_encrypt", value_hint = clap::ValueHint::FilePath)]
age_key_file: Option<PathBuf>,
#[clap(long, value_name = "RECIPIENT", requires = "age_encrypt")]
age_recipient: Vec<String>,
#[clap(long, value_name = "PATH_OR_PUBKEY", requires = "age_encrypt")]
age_ssh_recipient: Vec<String>,
#[clap(long, hide = true)]
complete: bool,
#[clap(long, verbatim_doc_comment, required = false, value_hint = clap::ValueHint::AnyPath)]
file: Option<PathBuf>,
#[clap(long)]
no_redact: bool,
#[clap(long)]
prompt: bool,
#[clap(long, value_name = "ENV_KEY", verbatim_doc_comment, visible_aliases = ["rm", "unset"], hide = true)]
remove: Option<Vec<String>>,
#[clap(long, conflicts_with = "prompt", requires = "env_vars")]
stdin: bool,
}
impl Set {
async fn decrypt_value_if_needed(
key: &str,
value: &str,
directive: Option<&EnvDirective>,
) -> Result<String> {
if let Some(EnvDirective::Age { .. }) = directive {
agecrypt::decrypt_age_directive(directive.unwrap())
.await
.map_err(|e| eyre!("[experimental] Failed to decrypt {}: {}", key, e))
}
else {
Ok(value.to_string())
}
}
pub async fn run(mut self) -> Result<()> {
if self.complete {
return self.complete().await;
}
match (&self.remove, &self.env_vars) {
(None, None) => {
return self.list_all().await;
}
(None, Some(env_vars))
if env_vars.iter().all(|ev| ev.value.is_none()) && !self.prompt && !self.stdin =>
{
return self.get().await;
}
_ => {}
}
let filename = self.filename()?;
let mut mise_toml = get_mise_toml(&filename)?;
if let Some(env_names) = &self.remove {
for name in env_names {
mise_toml.remove_env(name)?;
}
}
if let Some(env_vars) = &self.env_vars
&& env_vars.len() == 1
&& env_vars[0].value.is_none()
&& !self.prompt
&& !self.stdin
{
let key = &env_vars[0].key;
let full_config = Config::get().await?;
let env = full_config.env().await?;
match env.get(key) {
Some(value) => {
miseprintln!("{value}");
}
None => bail!("Environment variable {key} not found"),
}
return Ok(());
}
if let Some(mut env_vars) = self.env_vars.take() {
if self.prompt {
let theme = crate::ui::theme::get_theme();
for ev in &mut env_vars {
if ev.value.is_none() {
let prompt_msg = format!("Enter value for {}", ev.key);
let value = Input::new(&prompt_msg)
.password(self.age_encrypt) .theme(&theme)
.run()?;
ev.value = Some(value);
}
}
}
if self.stdin {
if env_vars.len() != 1 {
bail!("--stdin requires exactly one environment variable key");
}
let ev = &mut env_vars[0];
if ev.value.is_some() {
bail!(
"--stdin reads the value from stdin; do not provide a value with KEY=VALUE syntax"
);
}
let mut value = String::new();
std::io::stdin().read_to_string(&mut value)?;
if value.ends_with("\r\n") {
value.truncate(value.len() - 2);
} else if value.ends_with('\n') {
value.truncate(value.len() - 1);
}
ev.value = Some(value);
}
if self.age_encrypt {
Settings::get().ensure_experimental("age encryption")?;
let recipients = self.collect_age_recipients().await?;
for ev in env_vars {
match ev.value {
Some(value) => {
let age_directive =
agecrypt::create_age_directive(ev.key.clone(), &value, &recipients)
.await?;
if let crate::config::env_directive::EnvDirective::Age {
value: encrypted_value,
format,
..
} = age_directive
{
mise_toml.update_env_age(&ev.key, &encrypted_value, format)?;
}
}
None => bail!("{} has no value", ev.key),
}
}
} else {
for ev in env_vars {
match ev.value {
Some(value) => mise_toml.update_env(&ev.key, value)?,
None => bail!("{} has no value", ev.key),
}
}
}
}
mise_toml.save()
}
async fn complete(&self) -> Result<()> {
let config = Config::get().await?;
for ev in self.cur_env(&config).await? {
println!("{}", ev.key);
}
Ok(())
}
async fn list_all(self) -> Result<()> {
let config = Config::get().await?;
if !self.no_redact {
let _ = config.env_results().await?;
}
let mut env = self.cur_env(&config).await?;
if !self.no_redact {
for row in &mut env {
row.value = config.redact(&row.value);
}
}
let mut table = tabled::Table::new(env);
table::default_style(&mut table, false);
miseprintln!("{table}");
Ok(())
}
async fn get(self) -> Result<()> {
let config_path = if let Some(file) = &self.file {
Some(file.clone())
} else if self.env.is_some() {
Some(self.filename()?)
} else if !self.global {
let cwd = env::current_dir()?;
let mise_toml = cwd.join("mise.toml");
if mise_toml.exists() {
Some(mise_toml)
} else {
let dot_mise_toml = cwd.join(".mise.toml");
if dot_mise_toml.exists() {
Some(dot_mise_toml)
} else {
None }
}
} else {
None
};
let filter = self.env_vars.unwrap();
if config_path.is_none() {
let config = Config::get().await?;
let env_with_sources = config.env_with_sources().await?;
for eva in filter {
if let Some((value, _source)) = env_with_sources.get(&eva.key) {
miseprintln!("{value}");
} else {
bail!("Environment variable {} not found", eva.key);
}
}
return Ok(());
}
let config = MiseToml::from_file(&config_path.unwrap()).unwrap_or_default();
let env_entries = config.env_entries()?;
for eva in filter {
match env_entries.iter().find_map(|ev| match ev {
EnvDirective::Val(k, v, _) if k == &eva.key => Some((v.clone(), Some(ev))),
EnvDirective::Age {
key: k, value: v, ..
} if k == &eva.key => Some((v.clone(), Some(ev))),
_ => None,
}) {
Some((value, directive)) => {
let decrypted =
Self::decrypt_value_if_needed(&eva.key, &value, directive).await?;
miseprintln!("{decrypted}");
}
None => bail!("Environment variable {} not found", eva.key),
}
}
Ok(())
}
async fn cur_env(&self, config: &Arc<Config>) -> Result<Vec<Row>> {
let redact = !self.no_redact;
let rows = if let Some(file) = &self.file {
let mise_toml = MiseToml::from_file(file).unwrap_or_default();
Self::rows_from_directives(mise_toml.env_entries()?, file, redact)
} else if self.env.is_some() {
let filename = self.filename()?;
let mise_toml = MiseToml::from_file(&filename).unwrap_or_default();
Self::rows_from_directives(mise_toml.env_entries()?, &filename, redact)
} else {
config
.env_with_sources()
.await?
.iter()
.map(|(key, (value, source))| Row {
key: key.clone(),
value: value.clone(),
source: display_path(source),
})
.collect()
};
Ok(rows)
}
fn rows_from_directives(
directives: Vec<EnvDirective>,
source: &Path,
redact: bool,
) -> Vec<Row> {
directives
.into_iter()
.filter_map(|ed| match ed {
EnvDirective::Val(key, value, opts) => Some(Row {
key,
value: if redact && opts.redact.unwrap_or(false) {
"[redacted]".to_string()
} else {
value
},
source: display_path(source),
}),
EnvDirective::Age {
key,
value,
options,
..
} => Some(Row {
key,
value: if redact && options.redact != Some(false) {
"[redacted]".to_string()
} else {
value
},
source: display_path(source),
}),
_ => None,
})
.collect()
}
fn filename(&self) -> Result<PathBuf> {
let opts = ConfigPathOptions {
global: self.global,
path: self.file.clone(),
env: self.env.clone(),
cwd: None, prefer_toml: true, prevent_home_local: true, };
resolve_target_config_path(opts)
}
async fn collect_age_recipients(&self) -> Result<Vec<Box<dyn age::Recipient + Send>>> {
use age::Recipient;
let mut recipients: Vec<Box<dyn Recipient + Send>> = Vec::new();
for recipient_str in &self.age_recipient {
if let Some(recipient) = agecrypt::parse_recipient(recipient_str)? {
recipients.push(recipient);
}
}
for ssh_arg in &self.age_ssh_recipient {
let path = Path::new(ssh_arg);
if path.exists() {
recipients.push(agecrypt::load_ssh_recipient_from_path(path).await?);
} else {
if let Some(recipient) = agecrypt::parse_recipient(ssh_arg)? {
recipients.push(recipient);
}
}
}
if recipients.is_empty()
&& (self.age_recipient.is_empty()
&& self.age_ssh_recipient.is_empty()
&& self.age_key_file.is_none())
{
recipients = agecrypt::load_recipients_from_defaults().await?;
}
if let Some(key_file) = &self.age_key_file {
let key_file_recipients = agecrypt::load_recipients_from_key_file(key_file).await?;
recipients.extend(key_file_recipients);
}
if recipients.is_empty() {
bail!(
"[experimental] No age recipients provided. Use --age-recipient, --age-ssh-recipient, or --age-key-file"
);
}
Ok(recipients)
}
}
fn get_mise_toml(filename: &Path) -> Result<MiseToml> {
let path = env::current_dir()?.join(filename);
let mise_toml = if path.exists() {
MiseToml::from_file(&path)?
} else {
MiseToml::init(&path)
};
Ok(mise_toml)
}
#[derive(Tabled, Debug, Clone)]
struct Row {
key: String,
value: String,
source: String,
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise set NODE_ENV=production</bold>
$ <bold>mise set NODE_ENV</bold>
production
$ <bold>mise set -E staging NODE_ENV=staging</bold>
# creates or modifies mise.staging.toml
$ <bold>mise set</bold>
key value source
NODE_ENV production ~/.config/mise/config.toml
$ <bold>mise set --prompt PASSWORD</bold>
Enter value for PASSWORD: [hidden input]
<bold><underline>Multiline Values (--stdin):</underline></bold>
$ <bold>cat private.key | mise set --stdin MY_KEY</bold>
$ <bold>printf "line1\nline2" | mise set --stdin MY_KEY</bold>
<bold><underline>[experimental] Age Encryption:</underline></bold>
$ <bold>mise set --age-encrypt API_KEY=secret</bold>
$ <bold>mise set --age-encrypt --prompt API_KEY</bold>
Enter value for API_KEY: [hidden input]
"#
);