From 51604d51f94ba58d4bb8994861511bfe08e30437 Mon Sep 17 00:00:00 2001 From: Ethan Kinnear <51250849+superatomic@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:07:17 -0500 Subject: [PATCH] Create convert.rs and implement unset logic (#31) Move shell script conversion to a separate file, `convert.rs`. They were previously in `main.rs`. Additionally, implement unsetting environment variables as described in #31. Note: until https://github.com/rust-lang/rust-clippy/pull/8271 is added to Rust in 1.61, `#![allow(clippy::ptr_arg)]` has been added to the top of the file. --- src/convert.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 92 ++---------------------------- 2 files changed, 152 insertions(+), 88 deletions(-) create mode 100644 src/convert.rs diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..1134c16 --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,148 @@ +// Copyright 2022 Ethan Kinnear +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Convert a mapping representation of toml-formatted data into an `eval`able shell script. + +// Ignore ptr_arg Clippy warnings, as they are false positives. +// This will be fixed in 1.61. +// https://github.com/rust-lang/rust-clippy/pull/8271 +#![allow(clippy::ptr_arg)] + +use clap::ArgEnum; +use indexmap::IndexMap; +use std::string::String; + +use crate::cli::Shell; +use crate::config_file::{EnvVariableOption, EnvVariableValue, EnvironmentVariables}; + +pub(crate) fn to_shell_source(vars: &EnvironmentVariables, shell: &Shell) -> String { + //! Converts the hash table of `vars` into a script for the given `shell`. + + let mut output = String::new(); + for (name, variable_option) in vars { + // Check whether the current item is a single environment var or a table of specific shells. + let raw_value = match variable_option { + EnvVariableOption::General(v) => v, + + // If it is a shell specific choice, get the correct value for `shell`, and then... + EnvVariableOption::Specific(map) => match value_for_specific(shell, map) { + Some(v) => v, // ...extract the `EnvVariableValue` if it exists + None => continue, // ...and skip the value if it does not. + }, + }; + + // Convert an array to a string, but log if it was an array. + // Any arrays are treated as a path. + let (value, is_path) = &match raw_value { + // If the value of the environment variable is `false`, + // then add the "unset" script line to the String and skip the rest of this function. + EnvVariableValue::Set(false) => { + add_script_line::unset_variable(&mut output, shell, name); + continue; + } + + EnvVariableValue::String(string) => (expand_value(string.as_str()), false), + EnvVariableValue::Set(true) => ("1".to_string(), false), + EnvVariableValue::Array(array) => { + let v_expanded: Vec = + array.iter().map(|value| expand_value(value)).collect(); + (v_expanded.join(":"), true) + } + }; + + add_script_line::set_variable(&mut output, shell, name, value, is_path); + } + output +} + +// Module for adding a line to the script that will be sourced by the shell. +// Defines methods for adding the different types of lines. +mod add_script_line { + use super::Shell; + + pub fn set_variable( + output: &mut String, + shell: &Shell, + name: &str, + value: &str, + is_path: &bool, + ) { + // Log each processed variable + if log_enabled!(log::Level::Trace) { + let variable_log_header = match is_path { + true => "[Set]", + false => "'Set'", + }; + trace!("{}: {} -> {}", variable_log_header, name, value); + }; + + // Select the correct form for the chosen shell. + *output += &match shell { + Shell::Bash | Shell::Zsh => { + format!("export {}=\"{}\";\n", name, value) + } + Shell::Fish => { + // Add `--path` to the variable if the variable is represented as a list. + let path = match is_path { + true => " --path", + false => "", + }; + format!("set -gx{path} {} \"{}\";\n", name, value, path = path) + } + }; + } + + pub fn unset_variable(output: &mut String, shell: &Shell, name: &str) { + // Log each processed variable + trace!("Unset: {}", name); + + // Select the correct form for the chosen shell. + *output += &match shell { + Shell::Bash | Shell::Zsh => { + format!("unset {};\n", name) + } + Shell::Fish => { + format!("set -e {};\n", name) + } + }; + } +} + +fn value_for_specific<'a>( + shell: &Shell, + map: &'a IndexMap, +) -> Option<&'a EnvVariableValue> { + //! Given a `shell` and a `map` of all specific shell options, get the correct shell `EnvVariableValue`. + //! Used by `to_shell_source` to filter the right `EnvVariableOption::Specific` for the current shell. + let shell_name = shell.to_possible_value()?.get_name(); + map.get(shell_name).or_else(|| map.get("_")) +} + +fn expand_value(value: &str) -> String { + //! Expand the literal representation of a string in the toml + //! to a value with escape characters escaped and shell variables expanded. + + // Replace TOML escape codes with the literal representation so that they are correctly used. + // We need to do this for every code listed here: https://toml.io/en/v1.0.0#string + let value = value + .replace('\\', r"\\") // Backslash - Must be first! + .replace('\x08', r"\b") // Backspace + .replace('\t', r"\t") // Tab + .replace('\n', r"\n") // Newline + .replace('\x0C', r"\f") // Form Feed + .replace('\r', r"\r") // Carriage Return + .replace('\"', "\\\""); // Double Quote + + // Expand tildes + shellexpand::tilde(&value).to_string() +} diff --git a/src/main.rs b/src/main.rs index f48ac69..a2f1f34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ mod cli; mod config_file; +mod convert; #[macro_use] extern crate log; @@ -40,7 +41,7 @@ use std::path::Path; use std::{fs, path::PathBuf, process::exit, string::String}; use crate::cli::{Cli, Shell}; -use crate::config_file::{ConfigFile, EnvVariableOption, EnvVariableValue, EnvironmentVariables}; +use crate::config_file::{ConfigFile, EnvVariableOption, EnvVariableValue}; fn main() { //! Main function. @@ -110,7 +111,7 @@ fn main() { } // Output the file data converted to the correct shell format to the standard output. - let output = to_shell_source(&file_data.vars, &shell); + let output = convert::to_shell_source(&file_data.vars, &shell); print!("{}", output); // Retain compatibility with deprecated https://github.com/superatomic/xshe/issues/30 @@ -128,7 +129,7 @@ fn deprecated_to_specific_shell_source(file_data: &ConfigFile, shell: &Shell) { .map(|(key, value)| (key.to_owned(), EnvVariableOption::General(value.to_owned()))) .collect(); - let shell_specific_output = to_shell_source(&wrap_specific_vars, shell); + let shell_specific_output = convert::to_shell_source(&wrap_specific_vars, shell); print!("{}", shell_specific_output); }; @@ -213,91 +214,6 @@ fn get_specific_shell<'a>( file_data.shell.as_ref()?.get(field_name) } -fn to_shell_source(vars: &EnvironmentVariables, shell: &Shell) -> String { - //! Converts the hash table of `vars` into a script for the given `shell`. - - let mut output = String::new(); - for (name, variable_option) in vars { - // Check whether the current item is a single environment var or a table of specific shells. - let raw_value = match variable_option { - EnvVariableOption::General(v) => v, - - // If it is a shell specific choice, get the correct value for `shell`, and then... - EnvVariableOption::Specific(map) => match value_for_specific(shell, map) { - Some(v) => v, // ...extract the `EnvVariableValue` if it exists - None => continue, // ...and skip the value if it does not. - }, - }; - - // Convert an array to a string, but log if it was an array. - // Any arrays are treated as a path. - let (value, is_path) = match raw_value { - EnvVariableValue::String(string) => (expand_value(string.as_str()), false), - EnvVariableValue::Set(true) => ("1".to_string(), false), - EnvVariableValue::Set(false) => continue, // todo! - EnvVariableValue::Array(array) => { - let v_expanded: Vec = - array.iter().map(|value| expand_value(value)).collect(); - (v_expanded.join(":"), true) - } - }; - - // Log each processed variable - if log_enabled!(log::Level::Trace) { - let variable_log_header = match is_path { - true => "PATH EnvVar", - false => "EnvVariable", - }; - trace!("{}: {} -> {}", variable_log_header, name, value); - }; - - // Select the correct form for the chosen shell. - output += &match shell { - Shell::Bash | Shell::Zsh => { - format!("export {}=\"{}\";\n", name, value) - } - Shell::Fish => { - // Add `--path` to the variable if the variable was represented as a list in the TOML. - let path = match is_path { - true => " --path", - false => "", - }; - format!("set -gx{path} {} \"{}\";\n", name, value, path = path) - } - }; - } - output -} - -fn expand_value(value: &str) -> String { - //! Expand the literal representation of a string in the toml - //! to a value with escape characters escaped and shell variables expanded. - - // Replace TOML escape codes with the literal representation so that they are correctly used. - // We need to do this for every code listed here: https://toml.io/en/v1.0.0#string - let value = value - .replace('\\', r"\\") // Backslash - Must be first! - .replace('\x08', r"\b") // Backspace - .replace('\t', r"\t") // Tab - .replace('\n', r"\n") // Newline - .replace('\x0C', r"\f") // Form Feed - .replace('\r', r"\r") // Carriage Return - .replace('\"', "\\\""); // Double Quote - - // Expand tildes - shellexpand::tilde(&value).to_string() -} - -fn value_for_specific<'a>( - shell: &Shell, - map: &'a IndexMap, -) -> Option<&'a EnvVariableValue> { - //! Given a `shell` and a `map` of all specific shell options, get the correct shell `EnvVariableValue`. - //! Used by `to_shell_source` to filter the right `EnvVariableOption::Specific` for the current shell. - let shell_name = shell.to_possible_value()?.get_name(); - map.get(shell_name).or_else(|| map.get("_")) -} - fn get_file_path_default() -> PathBuf { //! Gets the default file path for `xshe.toml` if the `-f`/`--file` option is not set.