use anyhow::{Context, Result, anyhow};
use serde_json::Value;
mod hooks;
mod opt;
mod settings;
use clap::Parser;
use std::path::{Path, PathBuf};
use std::process;
use std::{env::current_dir, fs};
use directories::BaseDirs;
use hooks::zsh::Zsh;
use opt::{Command, Envy};
use settings::Settings;
fn config_path() -> Result<PathBuf> {
let base_dirs = BaseDirs::new().context("Cannot get base directories")?;
Ok(base_dirs.config_dir().join("envy").join("Config.toml"))
}
fn main() -> Result<()> {
let opt = Envy::parse();
match opt.cmd {
Command::Allow { env_file } => allow(env_file),
Command::Deny { env_file } => deny(env_file),
Command::Edit => edit(),
Command::Export { shell } => export(shell),
Command::Find { variable } => find(variable),
Command::Hook { shell } => hook(shell),
Command::Load { env_file } => load(env_file),
Command::Path => print_config_path(),
Command::Show => show(),
}
}
fn load(env_file: PathBuf) -> Result<()> {
anyhow::ensure!(
env_file.exists(),
"File does not exist: {}",
env_file.display()
);
source(env_file)
}
fn find(variable: String) -> Result<()> {
match std::env::var(&variable) {
Ok(value) => println!("{value}"),
Err(_) => println!("Variable {variable} not found"),
}
Ok(())
}
fn deny(env_file: PathBuf) -> Result<()> {
anyhow::ensure!(
env_file.exists(),
"File does not exist: {}",
env_file.display()
);
let mut settings = Settings::load(config_path()?)?;
let env_file = env_file.canonicalize()?;
settings.remove_env(env_file);
Settings::save(config_path()?, settings)
}
fn allow(env_file: PathBuf) -> Result<()> {
anyhow::ensure!(
env_file.exists(),
"File does not exist: {}",
env_file.display()
);
let mut settings = Settings::load(config_path()?)?;
let env_file = env_file.canonicalize()?;
settings.add_env(env_file);
Settings::save(config_path()?, settings)
}
pub fn open_editor(filename: &str) -> Result<std::process::ExitStatus> {
let editor_name = std::env::var("EDITOR")?;
let mut editor = process::Command::new(editor_name).arg(filename).spawn()?;
Ok(editor.wait()?)
}
fn edit() -> Result<()> {
let config = config_path()?;
open_editor(&config.to_string_lossy())?;
Ok(())
}
fn hook(shell: String) -> Result<()> {
let hook = match shell.as_ref() {
"bash" => hooks::bash::Bash::hook()?,
"fish" => hooks::fish::Fish::hook()?,
"zsh" => Zsh::hook()?,
_ => return Err(anyhow!("{} is currently not supported", shell)),
};
println!("{hook}");
Ok(())
}
fn get_env_vars_from_file(env: &Path) -> Result<Vec<String>> {
let env = fs::read_to_string(env).context("Cannot read env file")?;
Ok(env
.lines()
.filter(|line| !line.starts_with('#'))
.map(String::from)
.collect())
}
fn split_env_var(var: &str) -> Option<(String, String)> {
let trimmed = var.trim();
let var = trimmed.strip_prefix("export ").unwrap_or(trimmed);
var.split_once('=')
.map(|(key, value)| (key.trim().to_string(), value.trim().to_string()))
}
fn show() -> Result<()> {
let settings = Settings::load(config_path()?)?;
let dir = current_dir()?;
let env_files = settings.matching_env_files(&dir);
for file in &env_files {
println!("Loaded from `{}`:", file.display());
let vars = get_env_vars_from_file(file).context("Cannot read env file")?;
for var in vars {
println!("{var}");
}
println!();
}
match settings.matching_patterns(&dir) {
Some(env) => println!("{}", env.join("\n")),
None => {
if env_files.is_empty() {
println!(
"{}: envy found no pattern matches for this directory",
dir.display()
);
}
}
};
Ok(())
}
fn print_config_path() -> Result<()> {
let path = config_path().context("Cannot read config path")?;
println!("{}", path.display());
Ok(())
}
fn source(env_file: PathBuf) -> Result<()> {
for var in get_env_vars_from_file(&env_file)? {
match var {
var if var.starts_with("export ") => {
println!("{var}");
}
var => {
println!("export {var}");
}
}
}
Ok(())
}
fn export(shell: String) -> Result<()> {
let settings = Settings::load(config_path()?)?;
let current_dir = current_dir()?;
let mut all_env_vars = Vec::new();
if let Some(patterns) = settings.matching_patterns(¤t_dir) {
all_env_vars.extend(patterns);
}
for env_file in settings.matching_env_files(¤t_dir) {
let file_env_vars = get_env_vars_from_file(&env_file)?;
all_env_vars.extend(file_env_vars);
}
match shell.as_ref() {
"bash" | "zsh" => export_bash_zsh(&all_env_vars),
"fish" => export_fish(&all_env_vars),
"json" => export_json(&all_env_vars),
_ => Err(anyhow!("{} is currently not supported", shell)),
}
}
fn export_bash_zsh(env_vars: &[String]) -> Result<()> {
for env_var in env_vars {
if env_var.starts_with("export ") {
println!("{env_var}");
} else {
println!("export {env_var}");
}
}
Ok(())
}
fn export_fish(env_vars: &[String]) -> Result<()> {
for env_var in env_vars {
if let Some((key, value)) = split_env_var(env_var) {
println!("set -gx {key} {value}");
}
}
Ok(())
}
fn export_json(env_vars: &[String]) -> Result<()> {
let env_vars: serde_json::Map<String, Value> = env_vars
.iter()
.filter_map(|var| split_env_var(var).map(|(key, value)| (key, Value::String(value))))
.collect();
let json = serde_json::to_string(&env_vars).context("Failed to serialize to JSON")?;
println!("{json}");
Ok(())
}