Skip to content

Commit

Permalink
env-vars: improvements
Browse files Browse the repository at this point in the history
* Use ui::table for better formatting
* Allow viewing a single env var with `mise ev FOO`
* Added --global flag

See #1432
  • Loading branch information
jdx committed Jan 11, 2024
1 parent 3d12e5a commit 9843db1
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 77 deletions.
3 changes: 2 additions & 1 deletion completions/_mise
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ _mise() {
(direnv) __mise_direnv_cmd && ret=0 ;;
(doctor) __mise_doctor_cmd && ret=0 ;;
(e|env) __mise_env_cmd && ret=0 ;;
(ev|env-vars) __mise_env_vars_cmd && ret=0 ;;
(ev|set|env-vars) __mise_env_vars_cmd && ret=0 ;;
(x|exec) __mise_exec_cmd && ret=0 ;;
(g|global) __mise_global_cmd && ret=0 ;;
(hook-env) __mise_hook_env_cmd && ret=0 ;;
Expand Down Expand Up @@ -325,6 +325,7 @@ __mise_env_cmd() {
__mise_env_vars_cmd() {
_arguments -s -S \
'--file=[The TOML file to update]:file:_files' \
'(-g --global)'{-g,--global}'[Set the environment variable in the global config file]' \
'*--remove=[Remove the environment variable from config file]:remove:' \
'*::env_vars:' \
'(-C --cd)'{-C,--cd}'=[Change directory before running command]:cd:_directories' \
Expand Down
2 changes: 1 addition & 1 deletion completions/mise.bash
Original file line number Diff line number Diff line change
Expand Up @@ -1590,7 +1590,7 @@ _mise() {
return 0
;;
mise__env__vars)
opts="-C -q -v -y -h --file --remove --debug --log-level --trace --cd --quiet --verbose --yes --help [ENV_VARS]..."
opts="-g -C -q -v -y -h --file --global --remove --debug --log-level --trace --cd --quiet --verbose --yes --help [ENV_VARS]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down
1 change: 1 addition & 0 deletions completions/mise.fish
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ complete -kxc mise -n "$fssf env" -a "(__mise_tool_versions)" -d 'Tool(s) to use
# env-vars
complete -kxc mise -n "$fssf env-vars" -d 'Environment variable(s) to set'
complete -kxc mise -n "$fssf env-vars" -l file -a "(__fish_complete_path)" -d 'The TOML file to update'
complete -kxc mise -n "$fssf env-vars" -s g -l global -d 'Set the environment variable in the global config file'
complete -kxc mise -n "$fssf env-vars" -l remove -d 'Remove the environment variable from config file'
# exec
Expand Down
14 changes: 13 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,6 @@ Examples:
Manage environment variables
By default this command modifies ".mise.toml" in the current directory.
You can specify the file name by either setting the MISE_DEFAULT_CONFIG_FILENAME environment variable, or by using the --file option.
Usage: env-vars [OPTIONS] [ENV_VARS]...
Expand All @@ -349,10 +348,23 @@ Options:
Defaults to MISE_DEFAULT_CONFIG_FILENAME environment variable, or ".mise.toml".
-g, --global
Set the environment variable in the global config file
--remove <ENV_VAR>
Remove the environment variable from config file
Can be used multiple times.
Examples:
$ mise env-vars NODE_ENV=production
$ mise env-vars NODE_ENV
production
$ mise env-vars
key value source
NODE_ENV production ~/.config/mise/config.toml
```

## `mise exec [OPTIONS] [TOOL@VERSION]... [-- <COMMAND>...]`
Expand Down
61 changes: 22 additions & 39 deletions src/cli/args/env_var.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
use std::ffi::OsStr;

use clap::{
error::{ContextKind, ContextValue, ErrorKind},
Arg, Command, Error,
};
use clap::{Arg, Command, Error};

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct EnvVarArg {
pub key: String,
pub value: String,
pub value: Option<String>,
}

impl EnvVarArg {
pub fn parse(input: &str) -> Option<Self> {
input.split_once('=').map(|(k, v)| Self {
key: k.to_string(),
value: v.to_string(),
})
pub fn parse(input: &str) -> Self {
input
.split_once('=')
.map(|(k, v)| Self {
key: k.to_string(),
value: Some(v.to_string()),
})
.unwrap_or_else(|| Self {
key: input.to_string(),
value: None,
})
}
}

Expand All @@ -28,56 +31,36 @@ impl clap::builder::TypedValueParser for EnvVarArgParser {

fn parse_ref(
&self,
cmd: &Command,
arg: Option<&Arg>,
_cmd: &Command,
_arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, Error> {
if let Some(parsed) = EnvVarArg::parse(&value.to_string_lossy()) {
return Ok(parsed);
}

let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(
ContextKind::InvalidValue,
ContextValue::String(value.to_string_lossy().into()),
);
Err(err)
Ok(EnvVarArg::parse(&value.to_string_lossy()))
}
}

#[cfg(test)]
mod tests {
use super::EnvVarArg;

#[test]
fn invalid_value() {
let res = EnvVarArg::parse("NO_EQUAL_SIGN");
assert!(res.is_none());
}

#[test]
fn valid_values() {
let values = [
("FOO=", new_arg("FOO", "")),
("FOO=bar", new_arg("FOO", "bar")),
("FOO", new_arg("FOO", None)),
("FOO=", new_arg("FOO", Some(""))),
("FOO=bar", new_arg("FOO", Some("bar"))),
];

for (input, want) in values {
let got = EnvVarArg::parse(input);
assert_eq!(got, Some(want));
assert_eq!(got, want);
}
}

fn new_arg(key: &str, value: &str) -> EnvVarArg {
fn new_arg(key: &str, value: Option<&str>) -> EnvVarArg {
EnvVarArg {
key: key.to_string(),
value: value.to_string(),
value: value.map(|s| s.to_string()),
}
}
}
6 changes: 3 additions & 3 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ mod deactivate;
mod direnv;
mod doctor;
mod env;
mod env_vars;
pub mod exec;
mod external;
mod global;
Expand All @@ -42,6 +41,7 @@ mod render_mangen;
mod reshim;
mod run;
mod self_update;
mod set;
mod settings;
mod shell;
mod sync;
Expand Down Expand Up @@ -71,7 +71,6 @@ pub enum Commands {
Direnv(direnv::Direnv),
Doctor(doctor::Doctor),
Env(env::Env),
EnvVars(env_vars::EnvVars),
Exec(exec::Exec),
Global(global::Global),
HookEnv(hook_env::HookEnv),
Expand All @@ -89,6 +88,7 @@ pub enum Commands {
Reshim(reshim::Reshim),
Run(run::Run),
SelfUpdate(self_update::SelfUpdate),
Set(set::Set),
Settings(settings::Settings),
Shell(shell::Shell),
Sync(sync::Sync),
Expand Down Expand Up @@ -127,7 +127,6 @@ impl Commands {
Self::Direnv(cmd) => cmd.run(),
Self::Doctor(cmd) => cmd.run(),
Self::Env(cmd) => cmd.run(),
Self::EnvVars(cmd) => cmd.run(),
Self::Exec(cmd) => cmd.run(),
Self::Global(cmd) => cmd.run(),
Self::HookEnv(cmd) => cmd.run(),
Expand All @@ -145,6 +144,7 @@ impl Commands {
Self::Reshim(cmd) => cmd.run(),
Self::Run(cmd) => cmd.run(),
Self::SelfUpdate(cmd) => cmd.run(),
Self::Set(cmd) => cmd.run(),
Self::Settings(cmd) => cmd.run(),
Self::Shell(cmd) => cmd.run(),
Self::Sync(cmd) => cmd.run(),
Expand Down
94 changes: 71 additions & 23 deletions src/cli/env_vars.rs → src/cli/set.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
use std::path::{Path, PathBuf};

use miette::{IntoDiagnostic, Result};
use tabled::Tabled;

use crate::config::config_file::mise_toml::MiseToml;
use crate::config::config_file::ConfigFile;
use crate::config::Config;
use crate::env;
use crate::env::MISE_DEFAULT_CONFIG_FILENAME;
use crate::file::display_path;
use crate::ui::table;

use super::args::env_var::{EnvVarArg, EnvVarArgParser};

/// Manage environment variables
///
/// By default this command modifies ".mise.toml" in the current directory.
/// You can specify the file name by either setting the MISE_DEFAULT_CONFIG_FILENAME environment variable, or by using the --file option.
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "ev", verbatim_doc_comment)]
pub struct EnvVars {
#[clap(aliases = ["ev", "env-vars"], verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Set {
/// The TOML file to update
///
/// Defaults to MISE_DEFAULT_CONFIG_FILENAME environment variable, or ".mise.toml".
#[clap(long, verbatim_doc_comment, required = false, value_hint = clap::ValueHint::FilePath)]
file: Option<String>,
file: Option<PathBuf>,

/// Set the environment variable in the global config file
#[clap(short, long, verbatim_doc_comment, overrides_with = "file")]
global: bool,

/// Remove the environment variable from config file
///
Expand All @@ -34,22 +40,31 @@ pub struct EnvVars {
env_vars: Option<Vec<EnvVarArg>>,
}

impl EnvVars {
impl Set {
pub fn run(self) -> Result<()> {
let config = Config::try_get()?;
if self.remove.is_none() && self.env_vars.is_none() {
for (key, value) in &config.env {
let source = config.env_sources.get(key).unwrap();
miseprintln!("{key}={value} {}", display_path(source));
}
let rows = config
.env
.iter()
.map(|(key, value)| Row {
key: key.clone(),
value: value.clone(),
source: display_path(&config.env_sources.get(key).unwrap().clone()),
})
.collect::<Vec<_>>();
let mut table = tabled::Table::new(rows);
table::default_style(&mut table, false);
miseprintln!("{table}");
return Ok(());
}

let filename = self
.file
.unwrap_or_else(|| MISE_DEFAULT_CONFIG_FILENAME.to_string());
let filename = self.file.unwrap_or_else(|| match self.global {
true => env::MISE_GLOBAL_CONFIG_FILE.clone(),
false => env::MISE_DEFAULT_CONFIG_FILENAME.clone().into(),
});

let mut mise_toml = get_mise_toml(filename.as_str())?;
let mut mise_toml = get_mise_toml(&filename)?;

if let Some(env_names) = &self.remove {
for name in env_names {
Expand All @@ -58,15 +73,26 @@ impl EnvVars {
}

if let Some(env_vars) = self.env_vars {
if env_vars.len() == 1 && env_vars[0].value.is_none() {
let key = &env_vars[0].key;
match config.env.get(key) {
Some(value) => miseprintln!("{value}"),
None => bail!("Environment variable {key} not found"),
}
return Ok(());
}
for ev in env_vars {
mise_toml.update_env(&ev.key, ev.value);
match ev.value {
Some(value) => mise_toml.update_env(&ev.key, value),
None => bail!("{} has no value", ev.key),
}
}
}
mise_toml.save()
}
}

fn get_mise_toml(filename: &str) -> Result<MiseToml> {
fn get_mise_toml(filename: &Path) -> Result<MiseToml> {
let path = env::current_dir().into_diagnostic()?.join(filename);
let mise_toml = if path.exists() {
MiseToml::from_file(&path)?
Expand All @@ -77,11 +103,32 @@ fn get_mise_toml(filename: &str) -> Result<MiseToml> {
Ok(mise_toml)
}

#[derive(Tabled)]
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</bold>
key value source
NODE_ENV production ~/.config/mise/config.toml
"#
);

#[cfg(test)]
mod tests {
use crate::{env, file};
use std::path::PathBuf;

use crate::{env, file};

fn remove_config_file(filename: &str) -> PathBuf {
let cf_path = env::current_dir().unwrap().join(filename);
let _ = file::remove_file(&cf_path);
Expand All @@ -98,14 +145,15 @@ mod tests {
// Using the default file
let filename = ".test.mise.toml";
let cf_path = remove_config_file(filename);
assert_cli!("env-vars", "FOO=bar");
assert_cli_snapshot!("env-vars", "FOO=bar", @"");
assert_cli_snapshot!("env-vars", "FOO", @"bar");
assert_snapshot!(file::read_to_string(cf_path).unwrap());
remove_config_file(filename);

// Using a custom file
let filename = ".test-custom.mise.toml";
let cf_path = remove_config_file(filename);
assert_cli!("env-vars", "--file", filename, "FOO=bar");
assert_cli_snapshot!("env-vars", "--file", filename, "FOO=bar", @"");
assert_snapshot!(file::read_to_string(cf_path).unwrap());
remove_config_file(filename);
}
Expand All @@ -115,16 +163,16 @@ mod tests {
// Using the default file
let filename = ".test.mise.toml";
let cf_path = remove_config_file(filename);
assert_cli!("env-vars", "BAZ=quux");
assert_cli!("env-vars", "--remove", "BAZ");
assert_cli_snapshot!("env-vars", "BAZ=quux", @"");
assert_cli_snapshot!("env-vars", "--remove", "BAZ", @"");
assert_snapshot!(file::read_to_string(cf_path).unwrap());
remove_config_file(filename);

// Using a custom file
let filename = ".test-custom.mise.toml";
let cf_path = remove_config_file(filename);
assert_cli!("env-vars", "--file", filename, "BAZ=quux");
assert_cli!("env-vars", "--file", filename, "--remove", "BAZ");
assert_cli_snapshot!("env-vars", "--file", filename, "BAZ=quux", @"");
assert_cli_snapshot!("env-vars", "--file", filename, "--remove", "BAZ", @"");
assert_snapshot!(file::read_to_string(cf_path).unwrap());
remove_config_file(filename);
}
Expand Down
Loading

0 comments on commit 9843db1

Please sign in to comment.