Skip to content

Commit

Permalink
Merge pull request #38 from yhakbar/feature/adding_from
Browse files Browse the repository at this point in the history
Adding `from`
  • Loading branch information
yhakbar committed Jul 20, 2023
2 parents 8c990aa + c00767a commit efb9308
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 31 deletions.
24 changes: 24 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ command_as_mapping:
post_msg: Optional String

chdir: Optional String
from: Optional String
```
- `command_as_string` is a string that will be run as a command. This is equivalent to `command_as_mapping` with `prog` set to `bash`, `args` set to `-c` and `cmd` set to the value.
Expand All @@ -29,6 +30,7 @@ command_as_mapping:
- `pre_msg` is a message to print before running the command.
- `post_msg` is a message to print after running the command.
- `chdir` is the directory to change to before running the command.
- `from` is a configuration file to use instead of the current one for a given command.

## Config File Precedence

Expand Down Expand Up @@ -174,3 +176,25 @@ install:
```

The value of `chdir` can be any valid path. It can be an absolute path, a relative path, or a path relative to the root of the git repository that the current directory is in. You can use the `$GIT_ROOT` variable to refer to the root of the git repository that the current directory is in and the `$HOME` variable to refer to the home directory of the current user.

### From

Using the `from` key, you can specify a configuration file to use for a command instead of the current one. For example, you might want to have a central config file that defines standard commands used throughout a monorepo and referenced in subdirectories. You can do that like so:

```yml
# .config/ya.yml
lint:
prog: cargo
args: ["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"]
```

```yml
# subdirectory/.config/ya.yml
lint:
from: $GIT_ROOT/.config/ya.yml
```

```bash
# Within the subdirectory
❯ ya lint
```
1 change: 1 addition & 0 deletions examples/from/.config/ya.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
run: echo "Hey, from the from directory!"
2 changes: 2 additions & 0 deletions examples/from/git/.config/ya.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run:
from: $GIT_ROOT/examples/from/.config/ya.yml
2 changes: 2 additions & 0 deletions examples/from/relative/.config/ya.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run:
from: ../.config/ya.yml
111 changes: 85 additions & 26 deletions src/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,98 @@
use colored::Colorize;
use serde_yaml::Value;
use std::process::Command;
use std::{process::Command, path::PathBuf};

use crate::config::{parse_cmd, resolve_chdir, FullCommand, ParsedConfig, CommandType};
use crate::config::{parse_cmd, resolve_chdir, FullCommand, ParsedConfig, CommandType, parse_config_from_file};

const FROM_RECURSION_LIMIT: u64 = 10;

pub fn run_command_from_config(
config: &Value,
command_name: String,
sd: &[String],
quiet: bool,
execution: bool,
no_color: bool,
run_command_flags: &RunCommandFlags,
extra_args: &[String],
) -> anyhow::Result<()> {
let command_name = command_name.as_str();
let cmd = config.get(command_name).ok_or(anyhow::anyhow!(
"Command {} not found in config",
command_name
))?;
run_command(config, cmd, sd, quiet, execution, no_color, extra_args)
run_command(config, command_name, cmd, run_command_flags, extra_args)
}


fn get_full_command_from_parsed_command(parsed_command: CommandType, from: Option<String>, command_name: &str, recursion_depth: u64) -> Result<FullCommand, anyhow::Error> {
match parsed_command {
CommandType::SimpleCommand(cmd) => Ok(FullCommand {
prog: "bash".to_string(),
args: vec!["-c".to_string()],
cmd: Some(cmd),
}),
CommandType::FullCommand(cmd) => Ok(cmd),
CommandType::None => {
if let Some(from) = from {
let from = resolve_chdir(from)?;
let from_path_buff = PathBuf::from(&from);
let from_config = parse_config_from_file(from_path_buff.as_path())?;

let parsed_cmd = from_config.get(command_name);

if let Some(parsed_cmd) = parsed_cmd {
let from_command = parse_cmd(parsed_cmd)?;

let ParsedConfig {
parsed_command,
pre_msg: _,
post_msg: _,
pre_cmds: _,
post_cmds: _,
chdir: _,
from: _,
} = from_command;

return match parsed_command {
CommandType::SimpleCommand(cmd) => Ok(FullCommand {
prog: "bash".to_string(),
args: vec!["-c".to_string()],
cmd: Some(cmd),
}),
CommandType::FullCommand(cmd) => Ok(cmd),
CommandType::None => {
if (recursion_depth) >= FROM_RECURSION_LIMIT {
return Err(anyhow::anyhow!(
"Recursion limit of `from` reached: {}",
FROM_RECURSION_LIMIT
))
}

return get_full_command_from_parsed_command(parsed_command, Some(from), command_name, recursion_depth + 1)
}
}
} else {
return Err(anyhow::anyhow!(
"Command `{}` not found in config specified by `from` field of file {}",
command_name,
&from,
))
}
}
Err(anyhow::anyhow!("You must provide one of: a string representing a command, a fully qualified command, or a `from` field"))
}
}
}

pub struct RunCommandFlags {
pub sd: Vec<String>,
pub quiet: bool,
pub execution: bool,
pub no_color: bool,
}

fn run_command(
config: &Value,
command_name: &str,
cmd: &Value,
sd: &[String],
quiet: bool,
execution: bool,
no_color: bool,
run_command_flags: &RunCommandFlags,
extra_args: &[String],
) -> anyhow::Result<()> {
let command = parse_cmd(cmd)?;
Expand All @@ -39,16 +104,10 @@ fn run_command(
pre_cmds,
post_cmds,
chdir,
from,
} = command;

let full_command = match parsed_command {
CommandType::SimpleCommand(cmd) => FullCommand {
prog: "bash".to_string(),
args: vec!["-c".to_string()],
cmd: Some(cmd),
},
CommandType::FullCommand(cmd) => cmd,
};
let full_command = get_full_command_from_parsed_command(parsed_command, from, command_name, 0)?;

let FullCommand {
ref prog,
Expand All @@ -60,19 +119,19 @@ fn run_command(

if let Some(pre_cmds) = pre_cmds {
for cmd in pre_cmds {
run_command_from_config(config, cmd, sd, quiet, execution, no_color, &[])?;
run_command_from_config(config, cmd, run_command_flags, &[])?;
}
}

if !quiet {
if !run_command_flags.quiet {
if let Some(msg) = pre_msg {
println!("{}", msg);
}
}

if execution {
if run_command_flags.execution {
let mut parsed_command = format!("$ {}", full_command);
if !no_color {
if !run_command_flags.no_color {
parsed_command = parsed_command.blue().bold().to_string();
}
println!("{}", parsed_command);
Expand All @@ -83,7 +142,7 @@ fn run_command(
command_builder.args(args);

if let Some(cmd) = cmd {
let cmd = sd.iter().fold(cmd, |cmd, s| {
let cmd = run_command_flags.sd.iter().fold(cmd, |cmd, s| {
let (key, value) = s.split_once('=').unwrap();
cmd.replace(key, value)
});
Expand All @@ -104,15 +163,15 @@ fn run_command(
return Err(anyhow::anyhow!("{}", msg));
}

if !quiet {
if !run_command_flags.quiet {
if let Some(msg) = post_msg {
println!("{}", msg);
}
}

if let Some(post_cmds) = post_cmds {
for cmd in post_cmds {
run_command_from_config(config, cmd, sd, quiet, execution, no_color, &[])?;
run_command_from_config(config, cmd, run_command_flags, &[])?;
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub struct ParsedConfig {
pub pre_cmds: Option<Vec<String>>,
pub post_cmds: Option<Vec<String>>,
pub chdir: Option<String>,
pub from: Option<String>,
}

pub struct FullCommand {
Expand All @@ -101,6 +102,7 @@ pub struct FullCommand {
pub enum CommandType {
SimpleCommand(String),
FullCommand(FullCommand),
None,
}

impl std::fmt::Display for FullCommand {
Expand Down Expand Up @@ -136,6 +138,7 @@ pub fn parse_cmd(cmd: &Value) -> anyhow::Result<ParsedConfig> {
pre_cmds: None,
post_cmds: None,
chdir: None,
from: None,
}),
Value::Mapping(m) => {
let config_prog = m.get("prog");
Expand Down Expand Up @@ -242,6 +245,26 @@ pub fn parse_cmd(cmd: &Value) -> anyhow::Result<ParsedConfig> {
})
.transpose()?;

let from = m
.get("from")
.map(|v| {
v.as_str()
.ok_or(anyhow::anyhow!("Invalid Config: `from` is not a string"))
})
.transpose()?;

if let Some(from) = from {
return Ok(ParsedConfig {
parsed_command: CommandType::None,
pre_msg: None,
post_msg: None,
pre_cmds: None,
post_cmds: None,
chdir: None,
from: Some(from.to_string()),
});
}

Ok(ParsedConfig {
parsed_command: CommandType::FullCommand(
FullCommand {
Expand All @@ -255,6 +278,7 @@ pub fn parse_cmd(cmd: &Value) -> anyhow::Result<ParsedConfig> {
pre_cmds,
post_cmds,
chdir: chdir.map(|s| s.to_string()),
from: from.map(|s| s.to_string()),
})
}
_ => Err(anyhow::anyhow!(
Expand Down
12 changes: 7 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod config;
mod git;
mod validate;

use cmd::run_command_from_config;
use cmd::{run_command_from_config, RunCommandFlags};
use config::{get_config_path, parse_config_from_file, print_config_from_file};
use validate::{validate_config_file, validate_sd};

Expand Down Expand Up @@ -68,10 +68,12 @@ fn main() -> anyhow::Result<()> {
run_command_from_config(
&config,
command_name,
args.sd.as_slice(),
args.quiet,
args.execution,
args.no_color,
&RunCommandFlags {
sd: args.sd,
quiet: args.quiet,
execution: args.execution,
no_color: args.no_color,
},
args.extra_args.as_slice(),
)?
}
Expand Down
39 changes: 39 additions & 0 deletions tests/from.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#[cfg(test)]
mod from {
use anyhow::Result;
use assert_cmd::Command;
fn ya() -> Command {
Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("Error invoking ya")
}

#[test]
fn from_noop() -> Result<()> {
ya().args(["run"])
.current_dir("examples/from")
.assert()
.success()
.stdout("Hey, from the from directory!\n");

Ok(())
}
#[test]
fn from_relative() -> Result<()> {
ya().args(["run"])
.current_dir("examples/from/relative")
.assert()
.success()
.stdout("Hey, from the from directory!\n");

Ok(())
}
#[test]
fn from_git() -> Result<()> {
ya().args(["run"])
.current_dir("examples/from/git")
.assert()
.success()
.stdout("Hey, from the from directory!\n");

Ok(())
}
}

0 comments on commit efb9308

Please sign in to comment.