Skip to content

Commit acb7c12

Browse files
committed
Generalize subcommands: rg, git-show (etc.), diff
The possible command line now is: delta <delta-args> [SUBCMD <subcmd-args>] If the entire command line fails to parse because SUBCMD is unknown, then try (until the next arg fails) parsing <delta-args> only, and then parse and call SUBCMD.., then pipe input to delta. The generic subcommands also take precedence over the diff/git-diff (delta a b, where e.g. a=show and b=HEAD), and any diff call gets converted into a subcommand first. Available are: delta rg .. => rg --json .. | delta delta show .. => git <color-on> show .. | delta delta a b .. => git diff a b .. | delta And various other git-CMDS: log grep blame. The piping is not done by the shell, but delta, so the subcommands now are child processes of delta.
1 parent 4e3702c commit acb7c12

File tree

6 files changed

+369
-162
lines changed

6 files changed

+369
-162
lines changed

src/cli.rs

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::ffi::OsString;
33
use std::path::{Path, PathBuf};
44

55
use bat::assets::HighlightingAssets;
6+
use clap::error::Error;
67
use clap::{ArgMatches, ColorChoice, CommandFactory, FromArgMatches, Parser, ValueEnum, ValueHint};
78
use clap_complete::Shell;
89
use console::Term;
@@ -16,6 +17,7 @@ use crate::config::delta_unreachable;
1617
use crate::env::DeltaEnv;
1718
use crate::git_config::GitConfig;
1819
use crate::options;
20+
use crate::subcommands;
1921
use crate::utils;
2022
use crate::utils::bat::output::PagingMode;
2123

@@ -1217,23 +1219,12 @@ pub enum DetectDarkLight {
12171219
#[derive(Debug)]
12181220
pub enum Call<T> {
12191221
Delta(T),
1222+
DeltaDiff(T, PathBuf, PathBuf),
1223+
SubCommand(T, subcommands::SubCommand),
12201224
Help(String),
12211225
Version(String),
12221226
}
12231227

1224-
// Custom conversion because a) generic TryFrom<A,B> is not possible and
1225-
// b) the Delta(T) variant can't be converted.
1226-
impl<A> Call<A> {
1227-
fn try_convert<B>(self) -> Option<Call<B>> {
1228-
use Call::*;
1229-
match self {
1230-
Delta(_) => None,
1231-
Help(help) => Some(Help(help)),
1232-
Version(ver) => Some(Version(ver)),
1233-
}
1234-
}
1235-
}
1236-
12371228
impl Opt {
12381229
fn handle_help_and_version(args: &[OsString]) -> Call<ArgMatches> {
12391230
match Self::command().try_get_matches_from(args) {
@@ -1283,31 +1274,55 @@ impl Opt {
12831274
Call::Help(help)
12841275
}
12851276
Err(e) => {
1286-
e.exit();
1277+
// Calls `e.exit()` if error persists.
1278+
let (matches, subcmd) = subcommands::extract(args, e);
1279+
Call::SubCommand(matches, subcmd)
1280+
}
1281+
Ok(matches) => {
1282+
// subcommands take precedence over diffs
1283+
let minus_file = matches.get_one::<PathBuf>("minus_file").map(PathBuf::from);
1284+
if let Some(subcmd) = &minus_file {
1285+
if let Some(arg) = subcmd.to_str() {
1286+
if subcommands::SUBCOMMANDS.contains(&arg) {
1287+
let unreachable_error =
1288+
Error::new(clap::error::ErrorKind::InvalidSubcommand);
1289+
let (matches, subcmd) = subcommands::extract(args, unreachable_error);
1290+
return Call::SubCommand(matches, subcmd);
1291+
}
1292+
}
1293+
}
1294+
1295+
match (
1296+
minus_file,
1297+
matches.get_one::<PathBuf>("plus_file").map(PathBuf::from),
1298+
) {
1299+
(Some(minus_file), Some(plus_file)) => {
1300+
Call::DeltaDiff(matches, minus_file, plus_file)
1301+
}
1302+
_ => Call::Delta(matches),
1303+
}
12871304
}
1288-
Ok(matches) => Call::Delta(matches),
12891305
}
12901306
}
12911307

12921308
pub fn from_args_and_git_config(
12931309
args: Vec<OsString>,
12941310
env: &DeltaEnv,
12951311
assets: HighlightingAssets,
1296-
) -> Call<Self> {
1312+
) -> (Call<()>, Option<Opt>) {
12971313
#[cfg(test)]
12981314
// Set argv[0] when called in tests:
12991315
let args = {
13001316
let mut args = args;
13011317
args.insert(0, OsString::from("delta"));
13021318
args
13031319
};
1304-
let matches = match Self::handle_help_and_version(&args) {
1305-
Call::Delta(t) => t,
1306-
msg => {
1307-
return msg
1308-
.try_convert()
1309-
.unwrap_or_else(|| panic!("Call<_> conversion failed"))
1310-
}
1320+
let (matches, call) = match Self::handle_help_and_version(&args) {
1321+
Call::Delta(t) => (t, Call::Delta(())),
1322+
Call::DeltaDiff(t, a, b) => (t, Call::DeltaDiff((), a, b)),
1323+
Call::SubCommand(t, cmd) => (t, Call::SubCommand((), cmd)),
1324+
Call::Help(help) => return (Call::Help(help), None),
1325+
Call::Version(ver) => return (Call::Version(ver), None),
13111326
};
13121327

13131328
let mut final_config = if *matches.get_one::<bool>("no_gitconfig").unwrap_or(&false) {
@@ -1323,12 +1338,8 @@ impl Opt {
13231338
}
13241339
}
13251340

1326-
Call::Delta(Self::from_clap_and_git_config(
1327-
env,
1328-
matches,
1329-
final_config,
1330-
assets,
1331-
))
1341+
let opt = Self::from_clap_and_git_config(env, matches, final_config, assets);
1342+
(call, Some(opt))
13321343
}
13331344

13341345
pub fn from_iter_and_git_config<I>(
@@ -1399,9 +1410,3 @@ lazy_static! {
13991410
.into_iter()
14001411
.collect();
14011412
}
1402-
1403-
// Call::Help(format!(
1404-
// "foo\nbar\nbatz\n{}\n{}",
1405-
// help.replace("Options:", "well well\n\nOptions:"),
1406-
// h2
1407-
// ))

src/main.rs

Lines changed: 141 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ mod subcommands;
2525
mod tests;
2626

2727
use std::ffi::OsString;
28-
use std::io::{self, Cursor, ErrorKind, IsTerminal, Write};
29-
use std::process;
28+
use std::io::{self, BufRead, Cursor, ErrorKind, IsTerminal, Write};
29+
use std::process::{self, Command, Stdio};
3030

3131
use bytelines::ByteLinesReader;
3232

3333
use crate::cli::Call;
34+
use crate::config::delta_unreachable;
3435
use crate::delta::delta;
36+
use crate::subcommands::{SubCmdKind, SubCommand};
3537
use crate::utils::bat::assets::list_languages;
3638
use crate::utils::bat::output::{OutputType, PagingMode};
3739

@@ -67,7 +69,7 @@ fn main() -> std::io::Result<()> {
6769
ctrlc::set_handler(|| {})
6870
.unwrap_or_else(|err| eprintln!("Failed to set ctrl-c handler: {err}"));
6971
let exit_code = run_app(std::env::args_os().collect::<Vec<_>>(), None)?;
70-
// when you call process::exit, no destructors are called, so we want to do it only once, here
72+
// when you call process::exit, no drop impls are called, so we want to do it only once, here
7173
process::exit(exit_code);
7274
}
7375

@@ -81,19 +83,16 @@ pub fn run_app(
8183
) -> std::io::Result<i32> {
8284
let env = env::DeltaEnv::init();
8385
let assets = utils::bat::assets::load_highlighting_assets();
84-
let opt = cli::Opt::from_args_and_git_config(args, &env, assets);
86+
let (call, opt) = cli::Opt::from_args_and_git_config(args, &env, assets);
8587

86-
let opt = match opt {
87-
Call::Version(msg) => {
88-
writeln!(std::io::stdout(), "{}", msg.trim_end())?;
89-
return Ok(0);
90-
}
91-
Call::Help(msg) => {
92-
OutputType::oneshot_write(msg)?;
93-
return Ok(0);
94-
}
95-
Call::Delta(opt) => opt,
96-
};
88+
if let Call::Version(msg) = call {
89+
writeln!(std::io::stdout(), "{}", msg.trim_end())?;
90+
return Ok(0);
91+
} else if let Call::Help(msg) = call {
92+
OutputType::oneshot_write(msg)?;
93+
return Ok(0);
94+
}
95+
let opt = opt.expect("Opt is set");
9796

9897
let subcommand_result = if let Some(shell) = opt.generate_completion {
9998
Some(subcommands::generate_completion::generate_completion_file(
@@ -153,26 +152,134 @@ pub fn run_app(
153152
output_type.handle().unwrap()
154153
};
155154

156-
if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) {
157-
let exit_code = subcommands::diff::diff(minus_file, plus_file, &config, &mut writer);
158-
return Ok(exit_code);
159-
}
155+
let subcmd = match call {
156+
Call::DeltaDiff(_, minus, plus) => {
157+
match subcommands::diff::build_diff_cmd(&minus, &plus, &config) {
158+
Err(code) => return Ok(code),
159+
Ok(val) => val,
160+
}
161+
}
162+
Call::SubCommand(_, subcmd) => subcmd,
163+
Call::Delta(_) => SubCommand::none(),
164+
Call::Help(_) | Call::Version(_) => delta_unreachable("help/version handled earlier"),
165+
};
160166

161-
if io::stdin().is_terminal() {
162-
eprintln!(
163-
"\
164-
The main way to use delta is to configure it as the pager for git: \
165-
see https://github.com/dandavison/delta#get-started. \
166-
You can also use delta to diff two files: `delta file_A file_B`."
167-
);
168-
return Ok(config.error_exit_code);
169-
}
167+
if subcmd.is_none() {
168+
// Default delta run: read input from stdin, write to stdout or pager (pager started already^).
169+
if io::stdin().is_terminal() {
170+
eprintln!(
171+
"\
172+
The main way to use delta is to configure it as the pager for git: \
173+
see https://github.com/dandavison/delta#get-started. \
174+
You can also use delta to diff two files: `delta file_A file_B`."
175+
);
176+
return Ok(config.error_exit_code);
177+
}
178+
179+
let res = delta(io::stdin().lock().byte_lines(), &mut writer, &config);
170180

171-
if let Err(error) = delta(io::stdin().lock().byte_lines(), &mut writer, &config) {
172-
match error.kind() {
173-
ErrorKind::BrokenPipe => return Ok(0),
174-
_ => eprintln!("{error}"),
181+
if let Err(error) = res {
182+
match error.kind() {
183+
ErrorKind::BrokenPipe => return Ok(0),
184+
_ => {
185+
eprintln!("{error}");
186+
return Ok(config.error_exit_code);
187+
}
188+
}
175189
}
176-
};
177-
Ok(0)
190+
191+
Ok(0)
192+
} else {
193+
// First start a subcommand, and process input from it to delta(). Also handle
194+
// subcommand stderr and exit codes, e.g. for git and diff logic.
195+
196+
let (subcmd_bin, subcmd_args) = subcmd.args.split_first().unwrap();
197+
let subcmd_kind = subcmd.kind; // for easier {} formatting
198+
199+
let subcmd_bin_path = match grep_cli::resolve_binary(std::path::PathBuf::from(subcmd_bin)) {
200+
Ok(path) => path,
201+
Err(err) => {
202+
eprintln!("Failed to resolve command {subcmd_bin:?}: {err}");
203+
return Ok(config.error_exit_code);
204+
}
205+
};
206+
207+
let cmd = Command::new(subcmd_bin)
208+
.args(subcmd_args.iter())
209+
.stdout(Stdio::piped())
210+
.stderr(Stdio::piped())
211+
.spawn();
212+
213+
if let Err(err) = cmd {
214+
eprintln!("Failed to execute the command {subcmd_bin:?}: {err}");
215+
return Ok(config.error_exit_code);
216+
}
217+
let mut cmd = cmd.unwrap();
218+
219+
let cmd_stdout = cmd.stdout.as_mut().expect("Failed to open stdout");
220+
let cmd_stdout_buf = io::BufReader::new(cmd_stdout);
221+
222+
let res = delta(cmd_stdout_buf.byte_lines(), &mut writer, &config);
223+
224+
if let Err(error) = res {
225+
match error.kind() {
226+
ErrorKind::BrokenPipe => return Ok(0),
227+
_ => {
228+
eprintln!("{error}");
229+
return Ok(config.error_exit_code);
230+
}
231+
}
232+
};
233+
234+
let subcmd_status = cmd
235+
.wait()
236+
.unwrap_or_else(|_| {
237+
delta_unreachable(&format!("{subcmd_kind} process not running."));
238+
})
239+
.code()
240+
.unwrap_or_else(|| {
241+
eprintln!("{subcmd_kind} process terminated without exit status.");
242+
config.error_exit_code
243+
});
244+
245+
let mut stderr_lines =
246+
io::BufReader::new(cmd.stderr.expect("Failed to open stderr")).lines();
247+
if let Some(line1) = stderr_lines.next() {
248+
// prefix the first error line prefixed with the called subcommand
249+
eprintln!(
250+
"{}: {}",
251+
subcmd_kind,
252+
line1.unwrap_or("<delta: could not parse line>".into())
253+
);
254+
}
255+
256+
// On `git diff` unknown option: print first line (which is an error message) but not
257+
// the remainder (which is the entire --help text).
258+
if !(subcmd_status == 129
259+
&& matches!(subcmd_kind, SubCmdKind::GitDiff | SubCmdKind::Git(_)))
260+
{
261+
for line in stderr_lines {
262+
eprintln!("{}", line.unwrap_or("<delta: could not parse line>".into()));
263+
}
264+
}
265+
266+
if matches!(subcmd_kind, SubCmdKind::GitDiff | SubCmdKind::Diff) && subcmd_status >= 2 {
267+
eprintln!(
268+
"{subcmd_kind} process failed with exit status {subcmd_status}. Command was: {}",
269+
format_args!(
270+
"{} {}",
271+
subcmd_bin_path.display(),
272+
shell_words::join(
273+
subcmd_args
274+
.iter()
275+
.map(|arg0: &OsString| std::ffi::OsStr::to_string_lossy(arg0))
276+
),
277+
)
278+
);
279+
}
280+
281+
Ok(subcmd_status)
282+
}
283+
284+
// `output_type` drop impl runs here
178285
}

0 commit comments

Comments
 (0)