1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-29 20:17:45 +00:00

ptx: move from getopts to clap (#1893)

* ptx: move from getopts to clap

* chore: delete comment

* chore: fix some clippy warnings
This commit is contained in:
Yagiz Degirmenci 2021-03-24 23:46:17 +03:00 committed by GitHub
parent ffcfcfeef7
commit 63317b3529
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 206 additions and 130 deletions

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/ptx.rs" path = "src/ptx.rs"
[dependencies] [dependencies]
clap = "2.33"
aho-corasick = "0.7.3" aho-corasick = "0.7.3"
getopts = "0.2.18" getopts = "0.2.18"
libc = "0.2.42" libc = "0.2.42"

View file

@ -10,7 +10,7 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use getopts::{Matches, Options}; use clap::{App, Arg};
use regex::Regex; use regex::Regex;
use std::cmp; use std::cmp;
use std::collections::{BTreeSet, HashMap, HashSet}; use std::collections::{BTreeSet, HashMap, HashSet};
@ -20,6 +20,12 @@ use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
static NAME: &str = "ptx"; static NAME: &str = "ptx";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static BRIEF: &str = "Usage: ptx [OPTION]... [INPUT]... (without -G) or: \
ptx -G [OPTION]... [INPUT [OUTPUT]] \n Output a permuted index, \
including context, of the words in the input files. \n\n Mandatory \
arguments to long options are mandatory for short options too.\n
With no FILE, or when FILE is -, read standard input. \
Default is '-F /'.";
#[derive(Debug)] #[derive(Debug)]
enum OutFormat { enum OutFormat {
@ -61,8 +67,11 @@ impl Default for Config {
} }
} }
fn read_word_filter_file(matches: &Matches, option: &str) -> HashSet<String> { fn read_word_filter_file(matches: &clap::ArgMatches, option: &str) -> HashSet<String> {
let filename = matches.opt_str(option).expect("parsing options failed!"); let filename = matches
.value_of(option)
.expect("parsing options failed!")
.to_string();
let reader = BufReader::new(crash_if_err!(1, File::open(filename))); let reader = BufReader::new(crash_if_err!(1, File::open(filename)));
let mut words: HashSet<String> = HashSet::new(); let mut words: HashSet<String> = HashSet::new();
for word in reader.lines() { for word in reader.lines() {
@ -81,23 +90,29 @@ struct WordFilter {
} }
impl WordFilter { impl WordFilter {
fn new(matches: &Matches, config: &Config) -> WordFilter { fn new(matches: &clap::ArgMatches, config: &Config) -> WordFilter {
let (o, oset): (bool, HashSet<String>) = if matches.opt_present("o") { let (o, oset): (bool, HashSet<String>) = if matches.is_present(options::ONLY_FILE) {
(true, read_word_filter_file(matches, "o")) (true, read_word_filter_file(matches, options::ONLY_FILE))
} else { } else {
(false, HashSet::new()) (false, HashSet::new())
}; };
let (i, iset): (bool, HashSet<String>) = if matches.opt_present("i") { let (i, iset): (bool, HashSet<String>) = if matches.is_present(options::IGNORE_FILE) {
(true, read_word_filter_file(matches, "i")) (true, read_word_filter_file(matches, options::IGNORE_FILE))
} else { } else {
(false, HashSet::new()) (false, HashSet::new())
}; };
if matches.opt_present("b") { if matches.is_present(options::BREAK_FILE) {
crash!(1, "-b not implemented yet"); crash!(1, "-b not implemented yet");
} }
// Ignore empty string regex from cmd-line-args // Ignore empty string regex from cmd-line-args
let arg_reg: Option<String> = if matches.opt_present("W") { let arg_reg: Option<String> = if matches.is_present(options::WORD_REGEXP) {
matches.opt_str("W").filter(|reg| !reg.is_empty()) match matches.value_of(options::WORD_REGEXP) {
Some(v) => match v.is_empty() {
true => None,
false => Some(v.to_string()),
},
None => None,
}
} else { } else {
None None
}; };
@ -131,55 +146,50 @@ struct WordRef {
filename: String, filename: String,
} }
fn print_version() { fn get_config(matches: &clap::ArgMatches) -> Config {
println!("{} {}", NAME, VERSION);
}
fn print_usage(opts: &Options) {
let brief = "Usage: ptx [OPTION]... [INPUT]... (without -G) or: \
ptx -G [OPTION]... [INPUT [OUTPUT]] \n Output a permuted index, \
including context, of the words in the input files. \n\n Mandatory \
arguments to long options are mandatory for short options too.";
let explanation = "With no FILE, or when FILE is -, read standard input. \
Default is '-F /'.";
println!("{}\n{}", opts.usage(&brief), explanation);
}
fn get_config(matches: &Matches) -> Config {
let mut config: Config = Default::default(); let mut config: Config = Default::default();
let err_msg = "parsing options failed"; let err_msg = "parsing options failed";
if matches.opt_present("G") { if matches.is_present(options::TRADITIONAL) {
config.gnu_ext = false; config.gnu_ext = false;
config.format = OutFormat::Roff; config.format = OutFormat::Roff;
config.context_regex = "[^ \t\n]+".to_owned(); config.context_regex = "[^ \t\n]+".to_owned();
} else { } else {
crash!(1, "GNU extensions not implemented yet"); crash!(1, "GNU extensions not implemented yet");
} }
if matches.opt_present("S") { if matches.is_present(options::SENTENCE_REGEXP) {
crash!(1, "-S not implemented yet"); crash!(1, "-S not implemented yet");
} }
config.auto_ref = matches.opt_present("A"); config.auto_ref = matches.is_present(options::AUTO_REFERENCE);
config.input_ref = matches.opt_present("r"); config.input_ref = matches.is_present(options::REFERENCES);
config.right_ref &= matches.opt_present("R"); config.right_ref &= matches.is_present(options::RIGHT_SIDE_REFS);
config.ignore_case = matches.opt_present("f"); config.ignore_case = matches.is_present(options::IGNORE_CASE);
if matches.opt_present("M") { if matches.is_present(options::MACRO_NAME) {
config.macro_name = matches.opt_str("M").expect(err_msg); config.macro_name = matches
.value_of(options::MACRO_NAME)
.expect(err_msg)
.to_string();
} }
if matches.opt_present("F") { if matches.is_present(options::IGNORE_CASE) {
config.trunc_str = matches.opt_str("F").expect(err_msg); config.trunc_str = matches
.value_of(options::IGNORE_CASE)
.expect(err_msg)
.to_string();
} }
if matches.opt_present("w") { if matches.is_present(options::WIDTH) {
let width_str = matches.opt_str("w").expect(err_msg); let width_str = matches.value_of(options::WIDTH).expect(err_msg).to_string();
config.line_width = crash_if_err!(1, usize::from_str_radix(&width_str, 10)); config.line_width = crash_if_err!(1, usize::from_str_radix(&width_str, 10));
} }
if matches.opt_present("g") { if matches.is_present(options::GAP_SIZE) {
let gap_str = matches.opt_str("g").expect(err_msg); let gap_str = matches
.value_of(options::GAP_SIZE)
.expect(err_msg)
.to_string();
config.gap_size = crash_if_err!(1, usize::from_str_radix(&gap_str, 10)); config.gap_size = crash_if_err!(1, usize::from_str_radix(&gap_str, 10));
} }
if matches.opt_present("O") { if matches.is_present(options::FORMAT_ROFF) {
config.format = OutFormat::Roff; config.format = OutFormat::Roff;
} }
if matches.opt_present("T") { if matches.is_present(options::FORMAT_TEX) {
config.format = OutFormat::Tex; config.format = OutFormat::Tex;
} }
config config
@ -494,102 +504,167 @@ fn write_traditional_output(
} }
} }
mod options {
pub static FILE: &str = "file";
pub static AUTO_REFERENCE: &str = "auto-reference";
pub static TRADITIONAL: &str = "traditional";
pub static FLAG_TRUNCATION: &str = "flag-truncation";
pub static MACRO_NAME: &str = "macro-name";
pub static FORMAT_ROFF: &str = "format=roff";
pub static RIGHT_SIDE_REFS: &str = "right-side-refs";
pub static SENTENCE_REGEXP: &str = "sentence-regexp";
pub static FORMAT_TEX: &str = "format=tex";
pub static WORD_REGEXP: &str = "word-regexp";
pub static BREAK_FILE: &str = "break-file";
pub static IGNORE_CASE: &str = "ignore-case";
pub static GAP_SIZE: &str = "gap-size";
pub static IGNORE_FILE: &str = "ignore-file";
pub static ONLY_FILE: &str = "only-file";
pub static REFERENCES: &str = "references";
pub static WIDTH: &str = "width";
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args.collect_str(); let args = args.collect_str();
let mut opts = Options::new(); // let mut opts = Options::new();
opts.optflag( let matches = App::new(executable!())
"A", .name(NAME)
"auto-reference", .version(VERSION)
"output automatically generated references", .usage(BRIEF)
); .arg(Arg::with_name(options::FILE).hidden(true).multiple(true))
opts.optflag("G", "traditional", "behave more like System V 'ptx'"); .arg(
opts.optopt( Arg::with_name(options::AUTO_REFERENCE)
"F", .short("A")
"flag-truncation", .long(options::AUTO_REFERENCE)
"use STRING for flagging line truncations", .help("output automatically generated references")
"STRING", .takes_value(false),
); )
opts.optopt( .arg(
"M", Arg::with_name(options::TRADITIONAL)
"macro-name", .short("G")
"macro name to use instead of 'xx'", .long(options::TRADITIONAL)
"STRING", .help("behave more like System V 'ptx'"),
); )
opts.optflag("O", "format=roff", "generate output as roff directives"); .arg(
opts.optflag( Arg::with_name(options::FLAG_TRUNCATION)
"R", .short("F")
"right-side-refs", .long(options::FLAG_TRUNCATION)
"put references at right, not counted in -w", .help("use STRING for flagging line truncations")
); .value_name("STRING")
opts.optopt( .takes_value(true),
"S", )
"sentence-regexp", .arg(
"for end of lines or end of sentences", Arg::with_name(options::MACRO_NAME)
"REGEXP", .short("M")
); .long(options::MACRO_NAME)
opts.optflag("T", "format=tex", "generate output as TeX directives"); .help("macro name to use instead of 'xx'")
opts.optopt( .value_name("STRING")
"W", .takes_value(true),
"word-regexp", )
"use REGEXP to match each keyword", .arg(
"REGEXP", Arg::with_name(options::FORMAT_ROFF)
); .short("O")
opts.optopt( .long(options::FORMAT_ROFF)
"b", .help("generate output as roff directives"),
"break-file", )
"word break characters in this FILE", .arg(
"FILE", Arg::with_name(options::RIGHT_SIDE_REFS)
); .short("R")
opts.optflag( .long(options::RIGHT_SIDE_REFS)
"f", .help("put references at right, not counted in -w")
"ignore-case", .takes_value(false),
"fold lower case to upper case for sorting", )
); .arg(
opts.optopt( Arg::with_name(options::SENTENCE_REGEXP)
"g", .short("S")
"gap-size", .long(options::SENTENCE_REGEXP)
"gap size in columns between output fields", .help("for end of lines or end of sentences")
"NUMBER", .value_name("REGEXP")
); .takes_value(true),
opts.optopt( )
"i", .arg(
"ignore-file", Arg::with_name(options::FORMAT_TEX)
"read ignore word list from FILE", .short("T")
"FILE", .long(options::FORMAT_TEX)
); .help("generate output as TeX directives"),
opts.optopt( )
"o", .arg(
"only-file", Arg::with_name(options::WORD_REGEXP)
"read only word list from this FILE", .short("W")
"FILE", .long(options::WORD_REGEXP)
); .help("use REGEXP to match each keyword")
opts.optflag("r", "references", "first field of each line is a reference"); .value_name("REGEXP")
opts.optopt( .takes_value(true),
"w", )
"width", .arg(
"output width in columns, reference excluded", Arg::with_name(options::BREAK_FILE)
"NUMBER", .short("b")
); .long(options::BREAK_FILE)
opts.optflag("", "help", "display this help and exit"); .help("word break characters in this FILE")
opts.optflag("", "version", "output version information and exit"); .value_name("FILE")
.takes_value(true),
)
.arg(
Arg::with_name(options::IGNORE_CASE)
.short("f")
.long(options::IGNORE_CASE)
.help("fold lower case to upper case for sorting")
.takes_value(false),
)
.arg(
Arg::with_name(options::GAP_SIZE)
.short("g")
.long(options::GAP_SIZE)
.help("gap size in columns between output fields")
.value_name("NUMBER")
.takes_value(true),
)
.arg(
Arg::with_name(options::IGNORE_FILE)
.short("i")
.long(options::IGNORE_FILE)
.help("read ignore word list from FILE")
.value_name("FILE")
.takes_value(true),
)
.arg(
Arg::with_name(options::ONLY_FILE)
.short("o")
.long(options::ONLY_FILE)
.help("read only word list from this FILE")
.value_name("FILE")
.takes_value(true),
)
.arg(
Arg::with_name(options::REFERENCES)
.short("r")
.long(options::REFERENCES)
.help("first field of each line is a reference")
.value_name("FILE")
.takes_value(false),
)
.arg(
Arg::with_name(options::WIDTH)
.short("w")
.long(options::WIDTH)
.help("output width in columns, reference excluded")
.value_name("NUMBER")
.takes_value(true),
)
.get_matches_from(args);
let matches = return_if_err!(1, opts.parse(&args[1..])); let input_files: Vec<String> = match &matches.values_of(options::FILE) {
Some(v) => v.clone().map(|v| v.to_owned()).collect(),
None => vec!["-".to_string()],
};
if matches.opt_present("help") {
print_usage(&opts);
return 0;
}
if matches.opt_present("version") {
print_version();
return 0;
}
let config = get_config(&matches); let config = get_config(&matches);
let word_filter = WordFilter::new(&matches, &config); let word_filter = WordFilter::new(&matches, &config);
let file_map = read_input(&matches.free, &config); let file_map = read_input(&input_files, &config);
let word_set = create_word_set(&config, &word_filter, &file_map); let word_set = create_word_set(&config, &word_filter, &file_map);
let output_file = if !config.gnu_ext && matches.free.len() == 2 { let output_file = if !config.gnu_ext && matches.args.len() == 2 {
matches.free[1].clone() matches.value_of(options::FILE).unwrap_or("-").to_string()
} else { } else {
"-".to_owned() "-".to_owned()
}; };