Skip to content

Commit

Permalink
Generalize delta subcommands: rg, diff (implicit), show
Browse files Browse the repository at this point in the history
The possible command line now is this:

  delta <delta-args> [SUBCMD <subcmd-args>]

If the entire command line fails to parse because SUBCMD is
unknown, then try parsing <delta-args> only, and on success
call SUBCMD and pipe input to delta.

Available are:
   delta rg ..     => rg --json .. | delta
   delta show ..   => git show --color=always .. | delta
   delta a b ..    => diff a b .. | delta

The piping is not done by the shell, but delta, so the subcommands
now are child processes of delta
  • Loading branch information
th1000s committed Jul 22, 2024
1 parent 6208283 commit 3d93723
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 92 deletions.
62 changes: 51 additions & 11 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf};

use bat::assets::HighlightingAssets;
use clap::error::Error;
use clap::{ArgMatches, ColorChoice, CommandFactory, FromArgMatches, Parser, ValueEnum, ValueHint};
use clap_complete::Shell;
use console::Term;
Expand Down Expand Up @@ -974,6 +975,11 @@ pub struct Opt {

#[arg(skip)]
pub env: DeltaEnv,

#[arg(skip)]
// foreign subcommand which is unknown to this parser, only filled if
// normal parsing fails
pub subcmd: Vec<OsString>,
}

#[allow(non_snake_case)]
Expand Down Expand Up @@ -1209,6 +1215,7 @@ pub enum DetectDarkLight {
#[derive(Debug)]
pub enum Call<T> {
Delta(T),
SubCommand(T, Vec<OsString>),
Help(String),
Version(String),
}
Expand All @@ -1220,6 +1227,7 @@ impl<A> Call<A> {
use Call::*;
match self {
Delta(_) => None,
SubCommand(_, _) => None,
Help(help) => Some(Help(help)),
Version(ver) => Some(Version(ver)),
}
Expand Down Expand Up @@ -1251,18 +1259,50 @@ impl Opt {
Call::Help(format!("{}", help_text))
}
}
Err(e) => {
e.exit();
}
Err(e) => Opt::try_subcmds(args, e),
Ok(matches) => Call::Delta(matches),
}
}

pub(crate) fn try_subcmds(args: &[OsString], orig_error: Error) -> Call<ArgMatches> {
for (i, arg) in args.iter().enumerate() {
if arg == "rg" || arg == "show" {
// on help and version the subcommand could ALSO be called
match Self::command().try_get_matches_from(&args[..i]) {
Err(ref e) if e.kind() == clap::error::ErrorKind::DisplayVersion => {
unreachable!("handled by caller");
}
Err(ref e) if e.kind() == clap::error::ErrorKind::DisplayHelp => {
unreachable!("handled by caller");
}
Ok(matches) => {
let mut subcmd = vec![arg.into()];
if arg == "rg" {
subcmd.push("--json".into());
} else if arg == "show" {
subcmd.insert(0, "git".into());
subcmd.push("--color=always".into());
}
subcmd.extend(args[i + 1..].iter().map(|arg| arg.into()));
return Call::SubCommand(matches, subcmd);
}
Err(_) => {
// part before the subcommand failed to parse, report that error
orig_error.exit()
}
}
}
}
// no valid subcommand found, exit with the original error
orig_error.exit();
}

pub fn from_args_and_git_config(env: &DeltaEnv, assets: HighlightingAssets) -> Call<Self> {
let args = std::env::args_os().collect::<Vec<_>>();

let matches = match Self::handle_help_and_version(&args) {
Call::Delta(t) => t,
let (matches, subcmd) = match Self::handle_help_and_version(&args) {
Call::Delta(t) => (t, None),
Call::SubCommand(t, cmd) => (t, Some(cmd)),
msg => {
return msg
.try_convert()
Expand All @@ -1283,12 +1323,12 @@ impl Opt {
}
}

Call::Delta(Self::from_clap_and_git_config(
env,
matches,
final_config,
assets,
))
let opt = Self::from_clap_and_git_config(env, matches, final_config, assets);
if let Some(subcmd) = subcmd {
Call::SubCommand(opt, subcmd)
} else {
Call::Delta(opt)
}
}

pub fn from_iter_and_git_config<I>(
Expand Down
52 changes: 39 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ fn run_app() -> std::io::Result<i32> {
return Ok(0);
}
Call::Delta(opt) => opt,
Call::SubCommand(mut opt, subcmd) => {
opt.subcmd = subcmd;
opt
}
};

let subcommand_result = if let Some(shell) = opt.generate_completion {
Expand Down Expand Up @@ -121,7 +125,12 @@ fn run_app() -> std::io::Result<i32> {
};

let _show_config = opt.show_config;
let config = config::Config::from(opt);

let (config, subcmd) = {
let mut opt = opt;
let subcmd = std::mem::take(&mut opt.subcmd);
(config::Config::from(opt), subcmd)
};

if _show_config {
let stdout = io::stdout();
Expand All @@ -135,22 +144,39 @@ fn run_app() -> std::io::Result<i32> {
OutputType::from_mode(&env, config.paging_mode, config.pager.clone(), &pager_cfg).unwrap();
let mut writer = output_type.handle().unwrap();

if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) {
let exit_code = subcommands::diff::diff(minus_file, plus_file, &config, &mut writer);
return Ok(exit_code);
}

if io::stdin().is_terminal() {
eprintln!(
"\
let subcmd =
if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) {
subcommands::diff::diff(minus_file, plus_file, &config)
} else {
subcmd
};

let res = if subcmd.is_empty() {
if io::stdin().is_terminal() {
eprintln!(
"\
The main way to use delta is to configure it as the pager for git: \
see https://github.com/dandavison/delta#get-started. \
You can also use delta to diff two files: `delta file_A file_B`."
);
return Ok(config.error_exit_code);
}
);
return Ok(config.error_exit_code);
}
delta(io::stdin().lock().byte_lines(), &mut writer, &config)
} else {
use std::process::{Command, Stdio};

let cmd = Command::new(&subcmd[0])
.args(&subcmd[1..])
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start");

let cmd_stdout = cmd.stdout.expect("Failed to open stdout");
let cmd_stdout_buf = io::BufReader::new(cmd_stdout);
delta(cmd_stdout_buf.byte_lines(), &mut writer, &config)
};

if let Err(error) = delta(io::stdin().lock().byte_lines(), &mut writer, &config) {
if let Err(error) = res {
match error.kind() {
ErrorKind::BrokenPipe => return Ok(0),
_ => eprintln!("{error}"),
Expand Down
96 changes: 28 additions & 68 deletions src/subcommands/diff.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::ffi::OsString;
use std::path::Path;

use bytelines::ByteLinesReader;

use crate::config::{self, delta_unreachable};
use crate::delta;
use crate::cli::Call::SubCommand;
use crate::cli::Opt;
use crate::config::{self};

/// Run `git diff` on the files provided on the command line and display the output.
pub fn diff(
minus_file: &Path,
plus_file: &Path,
config: &config::Config,
writer: &mut dyn Write,
) -> i32 {
use std::io::BufReader;

pub fn diff(minus_file: &Path, plus_file: &Path, _config: &config::Config) -> Vec<OsString> {
// When called as `delta <(echo foo) <(echo bar)`, then git as of version 2.34 just prints the
// diff of the filenames which were created by the process substitution and does not read their
// content, so fall back to plain `diff` which simply opens the given input as files.
Expand All @@ -29,54 +20,23 @@ pub fn diff(
["git", "diff", "--no-index", "--color", "--"].as_slice()
};

let diff_bin = diff_cmd[0];
let diff_path = match grep_cli::resolve_binary(PathBuf::from(diff_bin)) {
Ok(path) => path,
Err(err) => {
eprintln!("Failed to resolve command '{diff_bin}': {err}");
return config.error_exit_code;
// TODO, check if paths exist
if minus_file == Path::new("rg") || minus_file == Path::new("show") {
let args = std::env::args_os().collect::<Vec<_>>();
if let SubCommand(_, subcommand) = Opt::try_subcmds(
&args,
clap::Error::new(clap::error::ErrorKind::InvalidSubcommand),
) {
return subcommand;
}
};

let diff_process = process::Command::new(diff_path)
.args(&diff_cmd[1..])
.args([minus_file, plus_file])
.stdout(process::Stdio::piped())
.spawn();

if let Err(err) = diff_process {
eprintln!("Failed to execute the command '{diff_bin}': {err}");
return config.error_exit_code;
unreachable!()
}
let mut diff_process = diff_process.unwrap();

if let Err(error) = delta::delta(
BufReader::new(diff_process.stdout.take().unwrap()).byte_lines(),
writer,
config,
) {
match error.kind() {
ErrorKind::BrokenPipe => return 0,
_ => {
eprintln!("{error}");
return config.error_exit_code;
}
}
};
let mut result: Vec<_> = diff_cmd.iter().map(|&arg| arg.into()).collect();
result.push(minus_file.into());
result.push(plus_file.into());

// Return the exit code from the diff process, so that the exit code
// contract of `delta file_A file_B` is the same as that of `diff file_A
// file_B` (i.e. 0 if same, 1 if different, 2 if error).
diff_process
.wait()
.unwrap_or_else(|_| {
delta_unreachable(&format!("'{diff_bin}' process not running."));
})
.code()
.unwrap_or_else(|| {
eprintln!("'{diff_bin}' process terminated without exit status.");
config.error_exit_code
})
result
}

#[cfg(test)]
Expand Down Expand Up @@ -112,15 +72,15 @@ mod main_tests {
}

fn _do_diff_test(file_a: &str, file_b: &str, expect_diff: bool) {
let config = integration_test_utils::make_config_from_args(&[]);
let mut writer = Cursor::new(vec![]);
let exit_code = diff(
&PathBuf::from(file_a),
&PathBuf::from(file_b),
&config,
&mut writer,
);
assert_eq!(exit_code, if expect_diff { 1 } else { 0 });
// let config = integration_test_utils::make_config_from_args(&[]);
// let mut writer = Cursor::new(vec![]);
// let exit_code = diff(
// &PathBuf::from(file_a),
// &PathBuf::from(file_b),
// &config,
// &mut writer,
// );
// assert_eq!(exit_code, if expect_diff { 1 } else { 0 });
}

fn _read_to_string(cursor: &mut Cursor<Vec<u8>>) -> String {
Expand Down

0 comments on commit 3d93723

Please sign in to comment.