Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help by default #134

Merged
merged 8 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions doc/tuc.1
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://github.com/riquito/tuc/issues>
Expand Down
9 changes: 9 additions & 0 deletions doc/tuc.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
====

Expand Down
80 changes: 8 additions & 72 deletions src/bin/tuc.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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};
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")]
Expand All @@ -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 <bounds> 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 <bounds> Same as --fields, but it keeps bytes
-c, --characters <bounds> Same as --fields, but it keeps characters
-l, --lines <bounds> Same as --fields, but it keeps lines
Implies --join. To merge lines, use --no-join
-d, --delimiter <delimiter> Delimiter used by --fields to cut the text
[default: \t]
-e, --regex <some regex> Use a regular expression as delimiter
-r, --replace-delimiter <new> Replace the delimiter with the provided text.
Implies --join
-t, --trim <type> 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<Opt, pico_args::Error> {
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);
}

Expand Down
230 changes: 230 additions & 0 deletions src/help.rs
Original file line number Diff line number Diff line change
@@ -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 <bounds> 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 <bounds> Same as --fields, but it keeps bytes
-c, --characters <bounds> Same as --fields, but it keeps characters
-l, --lines <bounds> Same as --fields, but it keeps lines
Implies --join. To merge lines, use --no-join
-d, --delimiter <delimiter> Delimiter used by --fields to cut the text
[default: \t]
-e, --regex <some regex> Use a regular expression as delimiter
-r, --replace-delimiter <new> Replace the delimiter with the provided text.
Implies --join
-t, --trim <type> 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
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading
Loading