Skip to content

Commit

Permalink
Add ShellCommand newtype
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasPickering committed Aug 15, 2023
1 parent d8c6b0b commit 933c9c4
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 27 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ anyhow = {version = "1.0.65", features = ["backtrace"]}
atty = "0.2.14"
clap = {version = "4.0.17", features = ["derive"]}
ctrlc = "3.2.3"
derive_more = "0.99.17"
dialoguer = {version = "0.10.2", default-features = false}
dotenv-parser = {version = "0.1.3", default-features = false}
env_logger = {version = "0.9.1", default-features = false, features = ["atty", "termcolor"]}
Expand Down
38 changes: 28 additions & 10 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod qualify;
mod tests;

use anyhow::{anyhow, bail, Context};
use derive_more::{Deref, Display, From};
use indexmap::{IndexMap, IndexSet};
use log::{debug, error, info, trace};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -44,7 +45,7 @@ pub struct Application {

/// An application or profile name. Newtype allows us to apply validation during
/// deserialization.
#[derive(Clone, Debug, Default, Serialize, Hash, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Display, Serialize, Hash, Eq, PartialEq)]
pub struct Name(String);

/// A profile is a set of fixed variable mappings, i.e. each variable maps to
Expand Down Expand Up @@ -128,7 +129,7 @@ pub enum ValueSourceKind {
/// A command that will be executed via the shell. Allows access to
/// aliases, pipes, etc.
#[serde(rename = "shell")]
ShellCommand { command: String },
ShellCommand { command: ShellCommand },

/// Run a command in a kubernetes pod.
#[serde(rename = "kubernetes")]
Expand Down Expand Up @@ -168,7 +169,7 @@ pub struct SideEffect {
#[serde(untagged)]
pub enum SideEffectCommand {
Native(NativeCommand),
Shell(String),
Shell(ShellCommand),
}

/// A native command is a program name/path, with zero or more arguments. This
Expand All @@ -182,6 +183,23 @@ pub struct NativeCommand {
pub arguments: Vec<String>,
}

/// A shell command is just a string, which will be parsed by the shell
#[derive(
Clone,
Debug,
Display,
Deref,
Eq,
From,
Hash,
PartialEq,
Serialize,
Deserialize,
)]
#[display(fmt = "`{}`", "0")]
pub struct ShellCommand(String);

impl Config {
/// Load config from the current directory and all parents. Any config
/// file in any directory in the hierarchy will be loaded and merged into
Expand Down Expand Up @@ -240,12 +258,6 @@ impl Config {
}
}

impl Display for Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

// Validate application/profile name. We do a bit of sanity checking here to
// prevent stuff that might be confusing, or collide with env-select features
impl FromStr for Name {
Expand Down Expand Up @@ -353,7 +365,7 @@ impl Display for ValueSource {
write!(f, "{command} (native)")
}
ValueSourceKind::ShellCommand { command } => {
write!(f, "`{command}` (shell)")
write!(f, "{command} (shell)")
}
ValueSourceKind::KubernetesCommand {
command,
Expand Down Expand Up @@ -396,6 +408,12 @@ impl From<NativeCommand> for Vec<String> {
}
}

impl From<&str> for ShellCommand {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}

// For deserialization
impl TryFrom<Vec<String>> for NativeCommand {
type Error = anyhow::Error;
Expand Down
17 changes: 16 additions & 1 deletion src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pub fn native<const N: usize>(
/// Helper to create a shell command
pub fn shell(command: &str) -> ValueSource {
ValueSourceKind::ShellCommand {
command: command.to_owned(),
command: command.into(),
}
.into()
}
Expand Down Expand Up @@ -434,18 +434,21 @@ fn test_parse_side_effects() {
name: "SideEffect",
len: 2,
},
//
Token::Str("setup"),
Token::Some,
Token::Seq { len: Some(2) },
Token::Str("echo"),
Token::Str("setup"),
Token::SeqEnd,
//
Token::Str("teardown"),
Token::Some,
Token::Seq { len: Some(2) },
Token::Str("echo"),
Token::Str("teardown"),
Token::SeqEnd,
//
Token::StructEnd,
],
);
Expand All @@ -458,12 +461,21 @@ fn test_parse_side_effects() {
name: "SideEffect",
len: 2,
},
//
Token::Str("setup"),
Token::Some,
Token::NewtypeStruct {
name: "ShellCommand",
},
Token::Str("echo setup"),
//
Token::Str("teardown"),
Token::Some,
Token::NewtypeStruct {
name: "ShellCommand",
},
Token::Str("echo teardown"),
//
Token::StructEnd,
],
);
Expand Down Expand Up @@ -516,6 +528,9 @@ fn test_parse_shell_command() {
Token::Str("type"),
Token::Str("shell"),
Token::Str("command"),
Token::NewtypeStruct {
name: "ShellCommand",
},
Token::Str("echo test"),
Token::StructEnd,
],
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ impl Executor {

let mut executable: Executable = if run_in_shell {
// Undo the tokenization from clap
self.shell.executable(&command.join(" "))
self.shell.executable(&command.join(" ").into())
} else {
// This *shouldn't* fail because we marked the argument as required,
// meaning clap will reject an empty command
Expand Down
27 changes: 12 additions & 15 deletions src/shell.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::{environment::Environment, execute::Executable};
use crate::{
config::ShellCommand, environment::Environment, execute::Executable,
};
use anyhow::anyhow;
use clap::ValueEnum;
use derive_more::Display;
use log::{debug, info};
use std::{
env,
Expand All @@ -27,11 +30,15 @@ pub struct Shell {
pub path: Option<String>,
}

/// A supported kind of shell
#[derive(Copy, Clone, Debug, ValueEnum)]
/// A supported kind of shell. The display implementation here defines the
/// binary name that we'll use to invoke it
#[derive(Copy, Clone, Debug, Display, ValueEnum)]
pub enum ShellKind {
#[display(fmt = "bash")]
Bash,
#[display(fmt = "zsh")]
Zsh,
#[display(fmt = "fish")]
Fish,
}

Expand Down Expand Up @@ -103,7 +110,7 @@ impl Shell {
}

/// Get an [Executable] command to run in this shell
pub fn executable(&self, command: &str) -> Executable {
pub fn executable(&self, command: &ShellCommand) -> Executable {
// Use the full shell path if we have it. Otherwise, just pass
// the shell name and hope it's in PATH
let shell_program =
Expand All @@ -116,17 +123,7 @@ impl Display for Shell {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.path {
Some(path) => write!(f, "{} (from $SHELL)", path),
None => write!(f, "{} (assumed to be in $PATH)", self.kind),
}
}
}

impl Display for ShellKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bash => write!(f, "bash"),
Self::Zsh => write!(f, "zsh"),
Self::Fish => write!(f, "fish"),
None => write!(f, "{} (from $PATH)", self.kind),
}
}
}

0 comments on commit 933c9c4

Please sign in to comment.