diff --git a/CHANGELOG.md b/CHANGELOG.md index a4951a1..fb1cd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), it triggers when you cut fields on 1-byte characters) * fields cut is now done on bytes, not strings (as long as your delimiter is proper utf-8 you'll be fine) +- feat: display short help when run without arguments - feat: --characters now depends on the (default) regex feature +- feat: help and short help are colored, as long as output is a tty and + unless env var TERM=dumb or NO_COLOR (any value) is set - refactor: --json internally uses serde_json, faster and more precise ## [1.2.0] - 2024-01-01 diff --git a/README.md b/README.md index e2813ac..a03d329 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,12 @@ Memory consumption: the whole input in memory (it also happens when -p or -m are being used) --bytes allocate the whole input in memory + +Colors: + Help is displayed using colors. Colors will be suppressed in the + following circumstances: + - when the TERM environment variable is not set or set to "dumb" + - when the NO_COLOR environment variable is set (regardless of value) ``` ## Examples diff --git a/doc/tuc.1 b/doc/tuc.1 index 0601746..e5a33b1 100644 --- a/doc/tuc.1 +++ b/doc/tuc.1 @@ -186,6 +186,14 @@ allocates the whole input in memory (it also happens when -p or -m are being used) .PP --bytes allocate the whole input in memory +.SH COLORS +.PP +Help is displayed using colors. +Colors will be suppressed in the following circumstances: +.IP \[bu] 2 +when the TERM environment variable is not set or set to \[lq]dumb\[rq] +.IP \[bu] 2 +when the NO_COLOR environment variable is set (regardless of value) .SH BUGS .PP See GitHub Issues: diff --git a/doc/tuc.1.md b/doc/tuc.1.md index 1e6bdc7..f264785 100644 --- a/doc/tuc.1.md +++ b/doc/tuc.1.md @@ -116,6 +116,15 @@ MEMORY CONSUMPTION \--bytes allocate the whole input in memory +COLORS +====== + +Help is displayed using colors. Colors will be suppressed in the +following circumstances: + +- when the TERM environment variable is not set or set to "dumb" +- when the NO_COLOR environment variable is set (regardless of value) + BUGS ==== diff --git a/src/bin/tuc.rs b/src/bin/tuc.rs index d510265..21d2119 100644 --- a/src/bin/tuc.rs +++ b/src/bin/tuc.rs @@ -1,5 +1,6 @@ use anyhow::Result; use std::convert::TryFrom; +use std::env::args; use std::io::Write; use std::str::FromStr; use tuc::bounds::{BoundOrFiller, BoundsType, UserBoundsList}; @@ -7,6 +8,7 @@ use tuc::cut_bytes::read_and_cut_bytes; use tuc::cut_lines::read_and_cut_lines; use tuc::cut_str::read_and_cut_str; use tuc::fast_lane::{read_and_cut_text_as_bytes, FastOpt}; +use tuc::help::{get_help, get_short_help}; use tuc::options::{Opt, EOL}; #[cfg(feature = "regex")] @@ -15,82 +17,16 @@ use tuc::options::RegexBag; #[cfg(feature = "regex")] use regex::bytes::Regex; -const HELP: &str = concat!( - "tuc ", - env!("CARGO_PKG_VERSION"), - r#" -Cut text (or bytes) where a delimiter matches, then keep the desired parts. - -The data is read from standard input. - -USAGE: - tuc [FLAGS] [OPTIONS] - -FLAGS: - -g, --greedy-delimiter Match consecutive delimiters as if it was one - -p, --compress-delimiter Print only the first delimiter of a sequence - -s, --only-delimited Print only lines containing the delimiter - -V, --version Print version information - -z, --zero-terminated Line delimiter is NUL (\0), not LF (\n) - -h, --help Print this help and exit - -m, --complement Invert fields (e.g. '2' becomes '1,3:') - -j, --(no-)join Print selected parts with delimiter in between - --json Print fields as a JSON array of strings - -OPTIONS: - -f, --fields Fields to keep, 1-indexed, comma separated. - Use colon to include everything in a range. - Fields can be negative (-1 is the last field). - [default 1:] - - e.g. cutting the string 'a-b-c-d' on '-' - -f 1 => a - -f 1: => a-b-c-d - -f 1:3 => a-b-c - -f 3,2 => cb - -f 3,1:2 => ca-b - -f -3:-2 => b-c - - To re-apply the delimiter add -j, to replace - it add -r (followed by the new delimiter). - - You can also format the output using {} syntax - e.g. - -f '({1}, {2})' => (a, b) - - You can escape { and } using {{ and }}. - - -b, --bytes Same as --fields, but it keeps bytes - -c, --characters Same as --fields, but it keeps characters - -l, --lines Same as --fields, but it keeps lines - Implies --join. To merge lines, use --no-join - -d, --delimiter Delimiter used by --fields to cut the text - [default: \t] - -e, --regex Use a regular expression as delimiter - -r, --replace-delimiter Replace the delimiter with the provided text. - Implies --join - -t, --trim Trim the delimiter (greedy). Valid values are - (l|L)eft, (r|R)ight, (b|B)oth - -Options precedence: - --trim and --compress-delimiter are applied before --fields or similar - -Memory consumption: - --characters and --fields read and allocate memory one line at a time - - --lines allocate memory one line at a time as long as the requested fields - are ordered and non-negative (e.g. -l 1,3:4,4,7), otherwise it allocates - the whole input in memory (it also happens when -p or -m are being used) - - --bytes allocate the whole input in memory -"# -); - fn parse_args() -> Result { let mut pargs = pico_args::Arguments::from_env(); + if args().len() == 1 { + print!("{}", get_short_help()); + std::process::exit(0); + } + if pargs.contains(["-h", "--help"]) { - print!("{HELP}"); + print!("{}", get_help()); std::process::exit(0); } diff --git a/src/help.rs b/src/help.rs new file mode 100644 index 0000000..0135213 --- /dev/null +++ b/src/help.rs @@ -0,0 +1,230 @@ +#[cfg(feature = "regex")] +use regex::Regex; +#[cfg(feature = "regex")] +use std::borrow::Cow; +#[cfg(feature = "regex")] +use std::io::IsTerminal; + +const HELP: &str = concat!( + "tuc ", + env!("CARGO_PKG_VERSION"), + r#" +Cut text (or bytes) where a delimiter matches, then keep the desired parts. + +The data is read from standard input. + +USAGE: + tuc [FLAGS] [OPTIONS] + +FLAGS: + -g, --greedy-delimiter Match consecutive delimiters as if it was one + -p, --compress-delimiter Print only the first delimiter of a sequence + -s, --only-delimited Print only lines containing the delimiter + -V, --version Print version information + -z, --zero-terminated Line delimiter is NUL (\0), not LF (\n) + -h, --help Print this help and exit + -m, --complement Invert fields (e.g. '2' becomes '1,3:') + -j, --(no-)join Print selected parts with delimiter in between + --json Print fields as a JSON array of strings + +OPTIONS: + -f, --fields Fields to keep, 1-indexed, comma separated. + Use colon to include everything in a range. + Fields can be negative (-1 is the last field). + [default: 1:] + + e.g. cutting the string 'a-b-c-d' on '-' + -f 1 => a + -f 1: => a-b-c-d + -f 1:3 => a-b-c + -f 3,2 => cb + -f 3,1:2 => ca-b + -f -3:-2 => b-c + + To re-apply the delimiter add -j, to replace + it add -r (followed by the new delimiter). + + You can also format the output using {} syntax + e.g. + -f '({1}, {2})' => (a, b) + + You can escape { and } using {{ and }}. + + -b, --bytes Same as --fields, but it keeps bytes + -c, --characters Same as --fields, but it keeps characters + -l, --lines Same as --fields, but it keeps lines + Implies --join. To merge lines, use --no-join + -d, --delimiter Delimiter used by --fields to cut the text + [default: \t] + -e, --regex Use a regular expression as delimiter + -r, --replace-delimiter Replace the delimiter with the provided text. + Implies --join + -t, --trim Trim the delimiter (greedy). Valid values are + (l|L)eft, (r|R)ight, (b|B)oth + +Options precedence: + --trim and --compress-delimiter are applied before --fields or similar + +Memory consumption: + --characters and --fields read and allocate memory one line at a time + + --lines allocate memory one line at a time as long as the requested fields + are ordered and non-negative (e.g. -l 1,3:4,4,7), otherwise it allocates + the whole input in memory (it also happens when -p or -m are being used) + + --bytes allocate the whole input in memory + +Colors: + Help is displayed using colors. Colors will be suppressed in the + following circumstances: + - when the TERM environment variable is not set or set to "dumb" + - when the NO_COLOR environment variable is set (regardless of value) +"# +); + +pub const SHORT_HELP: &str = concat!( + "tuc ", + env!("CARGO_PKG_VERSION"), + r#" - Created by Riccardo Attilio Galli + +Cut text (or bytes) where a delimiter matches, then keep the desired parts. + +Some examples: + + $ echo "a/b/c" | tuc -d / -f 1,-1 + ac + + $ echo "a/b/c" | tuc -d / -f 2: + b/c + + $ echo "hello.bak" | tuc -d . -f 'mv {1:} {1}' + mv hello.bak hello + + $ printf "a\nb\nc\nd\ne" | tuc -l 2:-2 + b + c + d + +Run `tuc --help` for more detailed information. +Send bug reports to: https://github.com/riquito/tuc/issues +"# +); + +#[cfg(feature = "regex")] +fn get_colored_help(text: &str) -> String { + // This is very unprofessional but: + // - I'm playing around and there's no need to look for serious + // performance for the help + // - for getting the colours as I wanted, the alternative + // was to tag the original help, but I'm more afraid + // of desyncing readme/man/help than getting this wrong + // (which I will, no doubt about it) + + // optional parameters + let text = Regex::new(r#"<.*?>"#) + .unwrap() + .replace_all(text, "\x1b[33m$0\x1b[0m"); + + // any example using "-f something" + let text = Regex::new(r#"-(f|l) ('.+'|[0-9,:-]+)"#) + .unwrap() + .replace_all(&text, "-$1 \x1b[33m$2\x1b[0m"); + + // a few one-shot fields" + let text = Regex::new(r#"'2'|'1,3:'|-1 "#) + .unwrap() + .replace_all(&text, "\x1b[33m$0\x1b[0m"); + + // Main labels + let text = Regex::new(r#"(?m)^[^\s].+?:.*"#) + .unwrap() + .replace_all(&text, "\x1b[1;32m$0\x1b[0m"); + + // args (e.g. -j, --join) + let text = Regex::new(r#"\s-[^\s\d,]+"#) + .unwrap() + .replace_all(&text, "\x1b[1;36m$0\x1b[0m"); + + // first line + let text = Regex::new(r#"tuc.*"#) + .unwrap() + .replace_all(&text, "\x1b[1;35m$0\x1b[0m"); + + // trim examples: (l|L)eft, (r|R)ight, (b|B)oth + let text = Regex::new(r#"\((.)\|(.)\)(eft|ight|oth)"#) + .unwrap() + .replace_all(&text, "(\x1b[33m$1\x1b[0m|\x1b[33m$2\x1b[0m)$3"); + + // defaults + let text = Regex::new(r#"default: ([^\]]+)"#) + .unwrap() + .replace_all(&text, "\x1b[35mdefault\x1b[0m: \x1b[33m$1\x1b[0m"); + + text.into_owned() +} + +#[cfg(feature = "regex")] +fn get_colored_short_help(text: &str) -> String { + let text = Regex::new(r#"( tuc|echo|printf)"#) + .unwrap() + .replace_all(text, "\x1b[1;32m$1\x1b[0m"); + + let text = Regex::new(r#"(?ms)(\$) (.*?)\n(.*?)\n\n"#) + .unwrap() + .replace_all(&text, "\x1b[1;36m$1\x1b[0m $2\n\x1b[0m$3\x1b[0m\n\n"); + + let text = Regex::new(r#"\|"#) + .unwrap() + .replace_all(&text, "\x1b[1;35m|\x1b[0m"); + + let text = Regex::new(r#"(tuc --help)"#) + .unwrap() + .replace_all(&text, "\x1b[33m$1\x1b[0m"); + + let text = Regex::new(r#"(tuc [^\s]+)"#) + .unwrap() + .replace_all(&text, "\x1b[1;35m$1\x1b[0m"); + + text.into_owned() +} + +#[cfg(feature = "regex")] +fn can_use_color() -> bool { + let is_tty = std::io::stdout().is_terminal(); + let term = std::env::var("TERM"); + let no_color = std::env::var("NO_COLOR"); + + is_tty + && term.is_ok() + && term.as_deref() != Ok("dumb") + && term.as_deref() != Ok("") + && no_color.is_err() +} + +#[cfg(feature = "regex")] +pub fn get_help() -> Cow<'static, str> { + if can_use_color() { + Cow::Owned(get_colored_help(HELP)) + } else { + Cow::Borrowed(HELP) + } +} + +#[cfg(feature = "regex")] +pub fn get_short_help() -> Cow<'static, str> { + if can_use_color() { + Cow::Owned(get_colored_short_help(SHORT_HELP)) + } else { + Cow::Borrowed(SHORT_HELP) + } +} + +#[cfg(not(feature = "regex"))] +pub fn get_help() -> &'static str { + HELP +} + +#[cfg(not(feature = "regex"))] +pub fn get_short_help() -> &'static str { + SHORT_HELP +} diff --git a/src/lib.rs b/src/lib.rs index 48dbb6e..cf1c9e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,6 @@ pub mod cut_bytes; pub mod cut_lines; pub mod cut_str; pub mod fast_lane; +pub mod help; pub mod options; mod read_utils; diff --git a/tests/cli.rs b/tests/cli.rs index 0a172d0..1d9739c 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,10 +1,19 @@ use assert_cmd::Command; +#[test] +fn it_display_short_help_when_run_without_arguments() { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + let assert = cmd.assert(); + + assert.success().stdout(predicates::str::starts_with("tuc")); +} + #[test] fn it_echo_non_delimited_line() { let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.write_stdin("foobar").assert(); + let assert = cmd.args(["-d", "/"]).write_stdin("foobar").assert(); assert.success().stdout("foobar\n"); }