1use std::cmp;
9use std::cmp::PartialEq;
10use std::collections::{BTreeSet, HashMap, HashSet};
11use std::ffi::{OsStr, OsString};
12use std::fmt::Write as FmtWrite;
13use std::fs::File;
14use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout};
15use std::num::ParseIntError;
16use std::path::Path;
17
18use clap::{Arg, ArgAction, Command};
19use regex::Regex;
20use thiserror::Error;
21use uucore::display::Quotable;
22use uucore::error::{FromIo, UError, UResult, UUsageError};
23use uucore::format_usage;
24use uucore::translate;
25
26#[derive(Debug, PartialEq)]
27enum OutFormat {
28 Dumb,
29 Roff,
30 Tex,
31}
32
33#[derive(Debug)]
34struct Config {
35 format: OutFormat,
36 gnu_ext: bool,
37 auto_ref: bool,
38 input_ref: bool,
39 right_ref: bool,
40 ignore_case: bool,
41 macro_name: String,
42 trunc_str: String,
43 context_regex: String,
44 line_width: usize,
45 gap_size: usize,
46}
47
48impl Default for Config {
49 fn default() -> Self {
50 Self {
51 format: OutFormat::Dumb,
52 gnu_ext: true,
53 auto_ref: false,
54 input_ref: false,
55 right_ref: false,
56 ignore_case: false,
57 macro_name: "xx".to_owned(),
58 trunc_str: "/".to_owned(),
59 context_regex: "\\w+".to_owned(),
60 line_width: 72,
61 gap_size: 3,
62 }
63 }
64}
65
66fn read_word_filter_file(
67 matches: &clap::ArgMatches,
68 option: &str,
69) -> std::io::Result<HashSet<String>> {
70 let filename = matches
71 .get_one::<OsString>(option)
72 .expect("parsing options failed!");
73 let reader: BufReader<Box<dyn Read>> = BufReader::new(if filename == "-" {
74 Box::new(stdin())
75 } else {
76 let file = File::open(Path::new(filename))?;
77 Box::new(file)
78 });
79 let mut words: HashSet<String> = HashSet::new();
80 for word in reader.lines() {
81 words.insert(word?);
82 }
83 Ok(words)
84}
85
86fn read_char_filter_file(
88 matches: &clap::ArgMatches,
89 option: &str,
90) -> std::io::Result<HashSet<char>> {
91 let filename = matches
92 .get_one::<OsString>(option)
93 .expect("parsing options failed!");
94 let mut reader: Box<dyn Read> = if filename == "-" {
95 Box::new(stdin())
96 } else {
97 let file = File::open(Path::new(filename))?;
98 Box::new(file)
99 };
100 let mut buffer = String::new();
101 reader.read_to_string(&mut buffer)?;
102 Ok(buffer.chars().collect())
103}
104
105#[derive(Debug)]
106struct WordFilter {
107 only_specified: bool,
108 ignore_specified: bool,
109 only_set: HashSet<String>,
110 ignore_set: HashSet<String>,
111 word_regex: String,
112}
113
114impl WordFilter {
115 #[allow(clippy::cognitive_complexity)]
116 fn new(matches: &clap::ArgMatches, config: &Config) -> UResult<Self> {
117 let (o, oset): (bool, HashSet<String>) = if matches.contains_id(options::ONLY_FILE) {
118 let words =
119 read_word_filter_file(matches, options::ONLY_FILE).map_err_context(String::new)?;
120 (true, words)
121 } else {
122 (false, HashSet::new())
123 };
124 let (i, iset): (bool, HashSet<String>) = if matches.contains_id(options::IGNORE_FILE) {
125 let words = read_word_filter_file(matches, options::IGNORE_FILE)
126 .map_err_context(String::new)?;
127 (true, words)
128 } else {
129 (false, HashSet::new())
130 };
131 let break_set: Option<HashSet<char>> = if matches.contains_id(options::BREAK_FILE)
132 && !matches.contains_id(options::WORD_REGEXP)
133 {
134 let chars =
135 read_char_filter_file(matches, options::BREAK_FILE).map_err_context(String::new)?;
136 let mut hs: HashSet<char> = if config.gnu_ext {
137 HashSet::new() } else {
139 [' ', '\t', '\n'].iter().copied().collect()
141 };
142 hs.extend(chars);
143 Some(hs)
144 } else {
145 None
147 };
148 let arg_reg: Option<String> = if matches.contains_id(options::WORD_REGEXP) {
150 match matches.get_one::<String>(options::WORD_REGEXP) {
151 Some(v) => {
152 if v.is_empty() {
153 None
154 } else {
155 Some(v.to_owned())
156 }
157 }
158 None => None,
159 }
160 } else {
161 None
162 };
163 let reg = match arg_reg {
164 Some(arg_reg) => arg_reg,
165 None => {
166 if let Some(break_set) = break_set {
167 format!(
168 "[^{}]+",
169 regex::escape(&break_set.into_iter().collect::<String>())
170 )
171 } else if config.gnu_ext {
172 "\\w+".to_owned()
173 } else {
174 "[^ \t\n]+".to_owned()
175 }
176 }
177 };
178 Ok(Self {
179 only_specified: o,
180 ignore_specified: i,
181 only_set: oset,
182 ignore_set: iset,
183 word_regex: reg,
184 })
185 }
186}
187
188#[derive(Debug, PartialOrd, PartialEq, Eq, Ord)]
189struct WordRef {
190 word: String,
191 global_line_nr: usize,
192 local_line_nr: usize,
193 position: usize,
194 position_end: usize,
195 filename: OsString,
196}
197
198#[derive(Debug, Error)]
199enum PtxError {
200 #[error("{}", translate!("ptx-error-dumb-format"))]
201 DumbFormat,
202
203 #[error("{}", translate!("ptx-error-not-implemented", "feature" => (*.0)))]
204 NotImplemented(&'static str),
205
206 #[error("{0}")]
207 ParseError(ParseIntError),
208}
209
210impl UError for PtxError {}
211
212fn get_config(matches: &clap::ArgMatches) -> UResult<Config> {
213 let mut config = Config::default();
214 let err_msg = "parsing options failed";
215 if matches.get_flag(options::TRADITIONAL) {
216 config.gnu_ext = false;
217 config.format = OutFormat::Roff;
218 "[^ \t\n]+".clone_into(&mut config.context_regex);
219 } else {
220 return Err(PtxError::NotImplemented("GNU extensions").into());
221 }
222 if matches.contains_id(options::SENTENCE_REGEXP) {
223 return Err(PtxError::NotImplemented("-S").into());
224 }
225 config.auto_ref = matches.get_flag(options::AUTO_REFERENCE);
226 config.input_ref = matches.get_flag(options::REFERENCES);
227 config.right_ref = matches.get_flag(options::RIGHT_SIDE_REFS);
228 config.ignore_case = matches.get_flag(options::IGNORE_CASE);
229 if matches.contains_id(options::MACRO_NAME) {
230 matches
231 .get_one::<String>(options::MACRO_NAME)
232 .expect(err_msg)
233 .clone_into(&mut config.macro_name);
234 }
235 if matches.contains_id(options::FLAG_TRUNCATION) {
236 matches
237 .get_one::<String>(options::FLAG_TRUNCATION)
238 .expect(err_msg)
239 .clone_into(&mut config.trunc_str);
240 }
241 if matches.contains_id(options::WIDTH) {
242 config.line_width = matches
243 .get_one::<String>(options::WIDTH)
244 .expect(err_msg)
245 .parse()
246 .map_err(PtxError::ParseError)?;
247 }
248 if matches.contains_id(options::GAP_SIZE) {
249 config.gap_size = matches
250 .get_one::<String>(options::GAP_SIZE)
251 .expect(err_msg)
252 .parse()
253 .map_err(PtxError::ParseError)?;
254 }
255 if let Some(format) = matches.get_one::<String>(options::FORMAT) {
256 config.format = match format.as_str() {
257 "roff" => OutFormat::Roff,
258 "tex" => OutFormat::Tex,
259 _ => unreachable!("should be caught by clap"),
260 };
261 }
262 if matches.get_flag(options::format::ROFF) {
263 config.format = OutFormat::Roff;
264 }
265 if matches.get_flag(options::format::TEX) {
266 config.format = OutFormat::Tex;
267 }
268 Ok(config)
269}
270
271struct FileContent {
272 lines: Vec<String>,
273 chars_lines: Vec<Vec<char>>,
274 offset: usize,
275}
276
277type FileMap = HashMap<OsString, FileContent>;
278
279fn read_input(input_files: &[OsString]) -> std::io::Result<FileMap> {
280 let mut file_map: FileMap = HashMap::new();
281 let mut offset: usize = 0;
282 for filename in input_files {
283 let reader: BufReader<Box<dyn Read>> = BufReader::new(if filename == "-" {
284 Box::new(stdin())
285 } else {
286 let file = File::open(Path::new(filename))?;
287 Box::new(file)
288 });
289 let lines: Vec<String> = reader.lines().collect::<std::io::Result<Vec<String>>>()?;
290
291 let chars_lines: Vec<Vec<char>> = lines.iter().map(|x| x.chars().collect()).collect();
294 let size = lines.len();
295 file_map.insert(
296 filename.clone(),
297 FileContent {
298 lines,
299 chars_lines,
300 offset,
301 },
302 );
303 offset += size;
304 }
305 Ok(file_map)
306}
307
308fn create_word_set(config: &Config, filter: &WordFilter, file_map: &FileMap) -> BTreeSet<WordRef> {
310 let reg = Regex::new(&filter.word_regex).unwrap();
311 let ref_reg = Regex::new(&config.context_regex).unwrap();
312 let mut word_set: BTreeSet<WordRef> = BTreeSet::new();
313 for (file, lines) in file_map {
314 let mut count: usize = 0;
315 let offs = lines.offset;
316 for line in &lines.lines {
317 let (ref_beg, ref_end) = match ref_reg.find(line) {
319 Some(x) => (x.start(), x.end()),
320 None => (0, 0),
321 };
322 for mat in reg.find_iter(line) {
324 let (beg, end) = (mat.start(), mat.end());
325 if config.input_ref && ((beg, end) == (ref_beg, ref_end)) {
326 continue;
327 }
328 let mut word = line[beg..end].to_owned();
329 if filter.only_specified && !filter.only_set.contains(&word) {
330 continue;
331 }
332 if filter.ignore_specified && filter.ignore_set.contains(&word) {
333 continue;
334 }
335 if config.ignore_case {
336 word = word.to_uppercase();
337 }
338 word_set.insert(WordRef {
339 word,
340 filename: file.clone(),
341 global_line_nr: offs + count,
342 local_line_nr: count,
343 position: beg,
344 position_end: end,
345 });
346 }
347 count += 1;
348 }
349 }
350 word_set
351}
352
353fn get_reference(config: &Config, word_ref: &WordRef, line: &str, context_reg: &Regex) -> String {
354 if config.auto_ref {
355 if word_ref.filename == "-" {
356 format!(":{}", word_ref.local_line_nr + 1)
357 } else {
358 format!(
359 "{}:{}",
360 word_ref.filename.maybe_quote(),
361 word_ref.local_line_nr + 1
362 )
363 }
364 } else if config.input_ref {
365 let (beg, end) = match context_reg.find(line) {
366 Some(x) => (x.start(), x.end()),
367 None => (0, 0),
368 };
369 line[beg..end].to_string()
370 } else {
371 String::new()
372 }
373}
374
375fn assert_str_integrity(s: &[char], beg: usize, end: usize) {
376 assert!(beg <= end);
377 assert!(end <= s.len());
378}
379
380fn trim_broken_word_left(s: &[char], beg: usize, end: usize) -> usize {
381 assert_str_integrity(s, beg, end);
382 if beg == end || beg == 0 || s[beg].is_whitespace() || s[beg - 1].is_whitespace() {
383 return beg;
384 }
385 let mut b = beg;
386 while b < end && !s[b].is_whitespace() {
387 b += 1;
388 }
389 b
390}
391
392fn trim_broken_word_right(s: &[char], beg: usize, end: usize) -> usize {
393 assert_str_integrity(s, beg, end);
394 if beg == end || end == s.len() || s[end - 1].is_whitespace() || s[end].is_whitespace() {
395 return end;
396 }
397 let mut e = end;
398 while beg < e && !s[e - 1].is_whitespace() {
399 e -= 1;
400 }
401 e
402}
403
404fn trim_idx(s: &[char], beg: usize, end: usize) -> (usize, usize) {
405 assert_str_integrity(s, beg, end);
406 let mut b = beg;
407 let mut e = end;
408 while b < e && s[b].is_whitespace() {
409 b += 1;
410 }
411 while beg < e && s[e - 1].is_whitespace() {
412 e -= 1;
413 }
414 (b, e)
415}
416
417fn get_output_chunks(
418 all_before: &[char],
419 keyword: &str,
420 all_after: &[char],
421 config: &Config,
422) -> (String, String, String, String) {
423 let half_line_size = config.line_width / 2;
426 let max_before_size = cmp::max(half_line_size as isize - config.gap_size as isize, 0) as usize;
427 let max_after_size = cmp::max(
428 half_line_size as isize
429 - (2 * config.trunc_str.len()) as isize
430 - keyword.len() as isize
431 - 1,
432 0,
433 ) as usize;
434
435 let mut head = String::with_capacity(half_line_size);
437 let mut before = String::with_capacity(half_line_size);
438 let mut after = String::with_capacity(half_line_size);
439 let mut tail = String::with_capacity(half_line_size);
440
441 let (_, before_end) = trim_idx(all_before, 0, all_before.len());
445
446 let before_beg = cmp::max(before_end as isize - max_before_size as isize, 0) as usize;
448 let before_beg = trim_broken_word_left(all_before, before_beg, before_end);
450
451 let (before_beg, before_end) = trim_idx(all_before, before_beg, before_end);
453
454 let before_str: String = all_before[before_beg..before_end].iter().collect();
456 before.push_str(&before_str);
457 assert!(max_before_size >= before.len());
458
459 let after_end = cmp::min(max_after_size, all_after.len());
463 let after_end = trim_broken_word_right(all_after, 0, after_end);
465
466 let (_, after_end) = trim_idx(all_after, 0, after_end);
468
469 let after_str: String = all_after[0..after_end].iter().collect();
471 after.push_str(&after_str);
472 assert!(max_after_size >= after.len());
473
474 let max_tail_size = cmp::max(
478 max_before_size as isize - before.len() as isize - config.gap_size as isize,
479 0,
480 ) as usize;
481
482 let (tail_beg, _) = trim_idx(all_after, after_end, all_after.len());
484
485 let tail_end = cmp::min(all_after.len(), tail_beg + max_tail_size);
487 let tail_end = trim_broken_word_right(all_after, tail_beg, tail_end);
489
490 let (tail_beg, mut tail_end) = trim_idx(all_after, tail_beg, tail_end);
492 if tail_end >= 2
496 && (tail_end - 2) > tail_beg
497 && all_after[tail_end - 2].is_whitespace()
498 && !all_after[tail_end - 1].is_whitespace()
499 {
500 tail_end -= 1;
501 (_, tail_end) = trim_idx(all_after, tail_beg, tail_end);
502 }
503
504 let tail_str: String = all_after[tail_beg..tail_end].iter().collect();
506 tail.push_str(&tail_str);
507
508 let max_head_size = cmp::max(
512 max_after_size as isize - after.len() as isize - config.gap_size as isize,
513 0,
514 ) as usize;
515
516 let (_, head_end) = trim_idx(all_before, 0, before_beg);
518
519 let head_beg = cmp::max(head_end as isize - max_head_size as isize, 0) as usize;
521 let head_beg = trim_broken_word_left(all_before, head_beg, head_end);
523
524 let (head_beg, head_end) = trim_idx(all_before, head_beg, head_end);
526
527 let head_str: String = all_before[head_beg..head_end].iter().collect();
529 head.push_str(&head_str);
530 if config.format != OutFormat::Tex {
532 if after_end != all_after.len() && tail_beg == tail_end {
534 after.push_str(&config.trunc_str);
535 } else if after_end != all_after.len() && tail_end != all_after.len() {
536 tail.push_str(&config.trunc_str);
537 }
538
539 if before_beg != 0 && head_beg == head_end {
541 before = format!("{}{before}", config.trunc_str);
542 } else if before_beg != 0 && head_beg != 0 {
543 head = format!("{}{head}", config.trunc_str);
544 }
545 }
546
547 (tail, before, after, head)
548}
549
550fn tex_mapper(x: char) -> String {
551 match x {
552 '\\' => "\\backslash{}".to_owned(),
553 '$' | '%' | '#' | '&' | '_' => format!("\\{x}"),
554 '}' | '{' => format!("$\\{x}$"),
555 _ => x.to_string(),
556 }
557}
558
559fn format_tex_field(s: &str) -> String {
561 let mapped_chunks: Vec<String> = s.chars().map(tex_mapper).collect();
562 mapped_chunks.join("")
563}
564
565fn format_tex_line(
566 config: &Config,
567 word_ref: &WordRef,
568 line: &str,
569 chars_line: &[char],
570 reference: &str,
571) -> String {
572 let mut output = String::new();
573 write!(output, "\\{} ", config.macro_name).unwrap();
574 let (tail, before, keyword, after, head) =
575 prepare_line_chunks(config, word_ref, line, chars_line, reference);
576 write!(
577 output,
578 "{{{0}}}{{{1}}}{{{2}}}{{{3}}}{{{4}}}",
579 format_tex_field(&tail),
580 format_tex_field(&before),
581 format_tex_field(&keyword),
582 format_tex_field(&after),
583 format_tex_field(&head),
584 )
585 .unwrap();
586 if config.auto_ref || config.input_ref {
587 write!(output, "{{{}}}", format_tex_field(reference)).unwrap();
588 }
589 output
590}
591
592fn format_roff_field(s: &str) -> String {
593 s.replace('\"', "\"\"")
594}
595
596fn format_roff_line(
597 config: &Config,
598 word_ref: &WordRef,
599 line: &str,
600 chars_line: &[char],
601 reference: &str,
602) -> String {
603 let mut output = String::new();
604 write!(output, ".{}", config.macro_name).unwrap();
605 let (tail, before, keyword, after, head) =
606 prepare_line_chunks(config, word_ref, line, chars_line, reference);
607 write!(
608 output,
609 " \"{}\" \"{}\" \"{}{}\" \"{}\"",
610 format_roff_field(&tail),
611 format_roff_field(&before),
612 format_roff_field(&keyword),
613 format_roff_field(&after),
614 format_roff_field(&head)
615 )
616 .unwrap();
617 if config.auto_ref || config.input_ref {
618 write!(output, " \"{}\"", format_roff_field(reference)).unwrap();
619 }
620 output
621}
622
623fn prepare_line_chunks(
625 config: &Config,
626 word_ref: &WordRef,
627 line: &str,
628 chars_line: &[char],
629 reference: &str,
630) -> (String, String, String, String, String) {
631 let ref_char_position = line[..word_ref.position].chars().count();
633 let char_position_end = ref_char_position
634 + line[word_ref.position..word_ref.position_end]
635 .chars()
636 .count();
637
638 let all_before = if config.input_ref {
640 let before = &line[..word_ref.position];
641 let before_char_count = before.chars().count();
642 let trimmed_char_count = before
643 .trim_start_matches(reference)
644 .trim_start()
645 .chars()
646 .count();
647 let trim_offset = before_char_count - trimmed_char_count;
648 &chars_line[trim_offset..before_char_count]
649 } else {
650 &chars_line[..ref_char_position]
651 };
652
653 let keyword = line[word_ref.position..word_ref.position_end].to_string();
655 let all_after = &chars_line[char_position_end..];
656
657 let (tail, before, after, head) = get_output_chunks(all_before, &keyword, all_after, config);
659
660 (tail, before, keyword, after, head)
661}
662
663fn write_traditional_output(
664 config: &mut Config,
665 file_map: &FileMap,
666 words: &BTreeSet<WordRef>,
667 output_filename: &OsStr,
668) -> UResult<()> {
669 let mut writer: BufWriter<Box<dyn Write>> =
670 BufWriter::new(if output_filename == OsStr::new("-") {
671 Box::new(stdout())
672 } else {
673 let file = File::create(output_filename)
674 .map_err_context(|| output_filename.to_string_lossy().quote().to_string())?;
675 Box::new(file)
676 });
677
678 let context_reg = Regex::new(&config.context_regex).unwrap();
679
680 if !config.right_ref {
681 let max_ref_len = if config.auto_ref {
682 get_auto_max_reference_len(words)
683 } else {
684 0
685 };
686 config.line_width -= max_ref_len;
687 }
688
689 for word_ref in words {
690 let file_map_value: &FileContent = file_map
691 .get(&word_ref.filename)
692 .expect("Missing file in file map");
693 let FileContent {
694 ref lines,
695 ref chars_lines,
696 offset: _,
697 } = *(file_map_value);
698 let reference = get_reference(
699 config,
700 word_ref,
701 &lines[word_ref.local_line_nr],
702 &context_reg,
703 );
704 let output_line: String = match config.format {
705 OutFormat::Tex => format_tex_line(
706 config,
707 word_ref,
708 &lines[word_ref.local_line_nr],
709 &chars_lines[word_ref.local_line_nr],
710 &reference,
711 ),
712 OutFormat::Roff => format_roff_line(
713 config,
714 word_ref,
715 &lines[word_ref.local_line_nr],
716 &chars_lines[word_ref.local_line_nr],
717 &reference,
718 ),
719 OutFormat::Dumb => {
720 return Err(PtxError::DumbFormat.into());
721 }
722 };
723 writeln!(writer, "{output_line}")
724 .map_err_context(|| translate!("ptx-error-write-failed"))?;
725 }
726
727 writer
728 .flush()
729 .map_err_context(|| translate!("ptx-error-write-failed"))?;
730
731 Ok(())
732}
733
734fn get_auto_max_reference_len(words: &BTreeSet<WordRef>) -> usize {
735 let line_num = words
737 .iter()
738 .map(|w| {
739 if w.local_line_nr == 0 {
740 1
741 } else {
742 (w.local_line_nr as f64).log10() as usize + 1
743 }
744 })
745 .max()
746 .unwrap_or(0);
747
748 let filename_len = words
749 .iter()
750 .filter(|w| w.filename != "-")
751 .map(|w| w.filename.maybe_quote().to_string().len())
752 .max()
753 .unwrap_or(0);
754
755 line_num + filename_len + 1
757}
758
759mod options {
760 pub mod format {
761 pub static ROFF: &str = "roff";
762 pub static TEX: &str = "tex";
763 }
764
765 pub static FILE: &str = "file";
766 pub static AUTO_REFERENCE: &str = "auto-reference";
767 pub static TRADITIONAL: &str = "traditional";
768 pub static FLAG_TRUNCATION: &str = "flag-truncation";
769 pub static MACRO_NAME: &str = "macro-name";
770 pub static FORMAT: &str = "format";
771 pub static RIGHT_SIDE_REFS: &str = "right-side-refs";
772 pub static SENTENCE_REGEXP: &str = "sentence-regexp";
773 pub static WORD_REGEXP: &str = "word-regexp";
774 pub static BREAK_FILE: &str = "break-file";
775 pub static IGNORE_CASE: &str = "ignore-case";
776 pub static GAP_SIZE: &str = "gap-size";
777 pub static IGNORE_FILE: &str = "ignore-file";
778 pub static ONLY_FILE: &str = "only-file";
779 pub static REFERENCES: &str = "references";
780 pub static WIDTH: &str = "width";
781}
782
783#[uucore::main]
784pub fn uumain(args: impl uucore::Args) -> UResult<()> {
785 let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
786 let mut config = get_config(&matches)?;
787
788 let input_files;
789 let output_file: OsString;
790
791 let mut files = matches
792 .get_many::<OsString>(options::FILE)
793 .into_iter()
794 .flatten()
795 .cloned();
796
797 if config.gnu_ext {
798 input_files = {
799 let mut files = files.collect::<Vec<_>>();
800 if files.is_empty() {
801 files.push(OsString::from("-"));
802 }
803 files
804 };
805 output_file = OsString::from("-");
806 } else {
807 input_files = vec![files.next().unwrap_or(OsString::from("-"))];
808 output_file = files.next().unwrap_or(OsString::from("-"));
809 if let Some(file) = files.next() {
810 return Err(UUsageError::new(
811 1,
812 translate!("ptx-error-extra-operand", "operand" => file.to_string_lossy().quote()),
813 ));
814 }
815 }
816
817 let word_filter = WordFilter::new(&matches, &config)?;
818 let file_map = read_input(&input_files).map_err_context(String::new)?;
819 let word_set = create_word_set(&config, &word_filter, &file_map);
820 write_traditional_output(&mut config, &file_map, &word_set, &output_file)
821}
822
823pub fn uu_app() -> Command {
824 Command::new(uucore::util_name())
825 .about(translate!("ptx-about"))
826 .version(uucore::crate_version!())
827 .help_template(uucore::localized_help_template(uucore::util_name()))
828 .override_usage(format_usage(&translate!("ptx-usage")))
829 .infer_long_args(true)
830 .arg(
831 Arg::new(options::FILE)
832 .hide(true)
833 .action(ArgAction::Append)
834 .value_hint(clap::ValueHint::FilePath)
835 .value_parser(clap::value_parser!(OsString)),
836 )
837 .arg(
838 Arg::new(options::AUTO_REFERENCE)
839 .short('A')
840 .long(options::AUTO_REFERENCE)
841 .help(translate!("ptx-help-auto-reference"))
842 .action(ArgAction::SetTrue),
843 )
844 .arg(
845 Arg::new(options::TRADITIONAL)
846 .short('G')
847 .long(options::TRADITIONAL)
848 .help(translate!("ptx-help-traditional"))
849 .action(ArgAction::SetTrue),
850 )
851 .arg(
852 Arg::new(options::FLAG_TRUNCATION)
853 .short('F')
854 .long(options::FLAG_TRUNCATION)
855 .help(translate!("ptx-help-flag-truncation"))
856 .value_name("STRING"),
857 )
858 .arg(
859 Arg::new(options::MACRO_NAME)
860 .short('M')
861 .long(options::MACRO_NAME)
862 .help(translate!("ptx-help-macro-name"))
863 .value_name("STRING"),
864 )
865 .arg(
866 Arg::new(options::FORMAT)
867 .long(options::FORMAT)
868 .hide(true)
869 .value_parser(["roff", "tex"])
870 .overrides_with_all([options::FORMAT, options::format::ROFF, options::format::TEX]),
871 )
872 .arg(
873 Arg::new(options::format::ROFF)
874 .short('O')
875 .help(translate!("ptx-help-roff"))
876 .overrides_with_all([options::FORMAT, options::format::ROFF, options::format::TEX])
877 .action(ArgAction::SetTrue),
878 )
879 .arg(
880 Arg::new(options::format::TEX)
881 .short('T')
882 .help(translate!("ptx-help-tex"))
883 .overrides_with_all([options::FORMAT, options::format::ROFF, options::format::TEX])
884 .action(ArgAction::SetTrue),
885 )
886 .arg(
887 Arg::new(options::RIGHT_SIDE_REFS)
888 .short('R')
889 .long(options::RIGHT_SIDE_REFS)
890 .help(translate!("ptx-help-right-side-refs"))
891 .action(ArgAction::SetTrue),
892 )
893 .arg(
894 Arg::new(options::SENTENCE_REGEXP)
895 .short('S')
896 .long(options::SENTENCE_REGEXP)
897 .help(translate!("ptx-help-sentence-regexp"))
898 .value_name("REGEXP"),
899 )
900 .arg(
901 Arg::new(options::WORD_REGEXP)
902 .short('W')
903 .long(options::WORD_REGEXP)
904 .help(translate!("ptx-help-word-regexp"))
905 .value_name("REGEXP"),
906 )
907 .arg(
908 Arg::new(options::BREAK_FILE)
909 .short('b')
910 .long(options::BREAK_FILE)
911 .help(translate!("ptx-help-break-file"))
912 .value_name("FILE")
913 .value_hint(clap::ValueHint::FilePath)
914 .value_parser(clap::value_parser!(OsString)),
915 )
916 .arg(
917 Arg::new(options::IGNORE_CASE)
918 .short('f')
919 .long(options::IGNORE_CASE)
920 .help(translate!("ptx-help-ignore-case"))
921 .action(ArgAction::SetTrue),
922 )
923 .arg(
924 Arg::new(options::GAP_SIZE)
925 .short('g')
926 .long(options::GAP_SIZE)
927 .help(translate!("ptx-help-gap-size"))
928 .value_name("NUMBER"),
929 )
930 .arg(
931 Arg::new(options::IGNORE_FILE)
932 .short('i')
933 .long(options::IGNORE_FILE)
934 .help(translate!("ptx-help-ignore-file"))
935 .value_name("FILE")
936 .value_hint(clap::ValueHint::FilePath)
937 .value_parser(clap::value_parser!(OsString)),
938 )
939 .arg(
940 Arg::new(options::ONLY_FILE)
941 .short('o')
942 .long(options::ONLY_FILE)
943 .help(translate!("ptx-help-only-file"))
944 .value_name("FILE")
945 .value_hint(clap::ValueHint::FilePath)
946 .value_parser(clap::value_parser!(OsString)),
947 )
948 .arg(
949 Arg::new(options::REFERENCES)
950 .short('r')
951 .long(options::REFERENCES)
952 .help(translate!("ptx-help-references"))
953 .value_name("FILE")
954 .action(ArgAction::SetTrue),
955 )
956 .arg(
957 Arg::new(options::WIDTH)
958 .short('w')
959 .long(options::WIDTH)
960 .help(translate!("ptx-help-width"))
961 .value_name("NUMBER"),
962 )
963}