uu_tr/
tr.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6mod operation;
7mod simd;
8mod unicode_table;
9
10use clap::{Arg, ArgAction, Command, value_parser};
11use operation::{
12    DeleteOperation, Sequence, SqueezeOperation, SymbolTranslator, TranslateOperation,
13    flush_output, translate_input,
14};
15use simd::process_input;
16use std::ffi::OsString;
17use std::io::{stdin, stdout};
18use uucore::display::Quotable;
19use uucore::error::{UResult, USimpleError, UUsageError};
20use uucore::fs::is_stdin_directory;
21#[cfg(not(target_os = "windows"))]
22use uucore::libc;
23use uucore::translate;
24use uucore::{format_usage, os_str_as_bytes, show};
25
26mod options {
27    pub const COMPLEMENT: &str = "complement";
28    pub const DELETE: &str = "delete";
29    pub const SQUEEZE: &str = "squeeze-repeats";
30    pub const TRUNCATE_SET1: &str = "truncate-set1";
31    pub const SETS: &str = "sets";
32}
33
34#[uucore::main]
35pub fn uumain(args: impl uucore::Args) -> UResult<()> {
36    // When we receive a SIGPIPE signal, we want to terminate the process so
37    // that we don't print any error messages to stderr. Rust ignores SIGPIPE
38    // (see https://github.com/rust-lang/rust/issues/62569), so we restore it's
39    // default action here.
40    #[cfg(not(target_os = "windows"))]
41    unsafe {
42        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
43    }
44
45    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
46
47    let delete_flag = matches.get_flag(options::DELETE);
48    let complement_flag = matches.get_flag(options::COMPLEMENT);
49    let squeeze_flag = matches.get_flag(options::SQUEEZE);
50    let truncate_set1_flag = matches.get_flag(options::TRUNCATE_SET1);
51
52    // Ultimately this should be OsString, but we might want to wait for the
53    // pattern API on OsStr
54    let sets: Vec<_> = matches
55        .get_many::<OsString>(options::SETS)
56        .into_iter()
57        .flatten()
58        .map(ToOwned::to_owned)
59        .collect();
60
61    if sets.is_empty() {
62        return Err(UUsageError::new(1, translate!("tr-error-missing-operand")));
63    }
64
65    let sets_len = sets.len();
66
67    if !(delete_flag || squeeze_flag) && sets_len == 1 {
68        return Err(UUsageError::new(
69            1,
70            translate!("tr-error-missing-operand-translating", "set" => sets[0].quote()),
71        ));
72    }
73
74    if delete_flag && squeeze_flag && sets_len == 1 {
75        return Err(UUsageError::new(
76            1,
77            translate!("tr-error-missing-operand-deleting-squeezing", "set" => sets[0].quote()),
78        ));
79    }
80
81    if sets_len > 1 {
82        if delete_flag && !squeeze_flag {
83            let op = sets[1].quote();
84            let msg = if sets_len == 2 {
85                translate!("tr-error-extra-operand-deleting-without-squeezing", "operand" => op)
86            } else {
87                translate!("tr-error-extra-operand-simple", "operand" => op)
88            };
89            return Err(UUsageError::new(1, msg));
90        }
91        if sets_len > 2 {
92            let op = sets[2].quote();
93            let msg = translate!("tr-error-extra-operand-simple", "operand" => op);
94            return Err(UUsageError::new(1, msg));
95        }
96    }
97
98    if let Some(first) = sets.first() {
99        let slice = os_str_as_bytes(first)?;
100        let trailing_backslashes = slice.iter().rev().take_while(|&&c| c == b'\\').count();
101        if trailing_backslashes % 2 == 1 {
102            // The trailing backslash has a non-backslash character before it.
103            show!(USimpleError::new(
104                0,
105                translate!("tr-warning-unescaped-backslash")
106            ));
107        }
108    }
109
110    let stdin = stdin();
111    let mut locked_stdin = stdin.lock();
112    let mut locked_stdout = stdout().lock();
113
114    // According to the man page: translating only happens if deleting or if a second set is given
115    let translating = !delete_flag && sets.len() > 1;
116    let mut sets_iter = sets.iter().map(|c| c.as_os_str());
117    let (set1, set2) = Sequence::solve_set_characters(
118        os_str_as_bytes(sets_iter.next().unwrap_or_default())?,
119        os_str_as_bytes(sets_iter.next().unwrap_or_default())?,
120        complement_flag,
121        // if we are not translating then we don't truncate set1
122        truncate_set1_flag && translating,
123        translating,
124    )?;
125
126    if is_stdin_directory(&stdin) {
127        return Err(USimpleError::new(1, translate!("tr-error-read-directory")));
128    }
129
130    // '*_op' are the operations that need to be applied, in order.
131    if delete_flag {
132        if squeeze_flag {
133            let delete_op = DeleteOperation::new(set1);
134            let squeeze_op = SqueezeOperation::new(set2);
135            let op = delete_op.chain(squeeze_op);
136            translate_input(&mut locked_stdin, &mut locked_stdout, op)?;
137        } else {
138            let op = DeleteOperation::new(set1);
139            process_input(&mut locked_stdin, &mut locked_stdout, &op)?;
140        }
141    } else if squeeze_flag {
142        if sets_len == 1 {
143            let op = SqueezeOperation::new(set1);
144            translate_input(&mut locked_stdin, &mut locked_stdout, op)?;
145        } else {
146            let translate_op = TranslateOperation::new(set1, set2.clone())?;
147            let squeeze_op = SqueezeOperation::new(set2);
148            let op = translate_op.chain(squeeze_op);
149            translate_input(&mut locked_stdin, &mut locked_stdout, op)?;
150        }
151    } else {
152        let op = TranslateOperation::new(set1, set2)?;
153        process_input(&mut locked_stdin, &mut locked_stdout, &op)?;
154    }
155
156    flush_output(&mut locked_stdout)?;
157
158    Ok(())
159}
160
161pub fn uu_app() -> Command {
162    Command::new(uucore::util_name())
163        .version(uucore::crate_version!())
164        .help_template(uucore::localized_help_template(uucore::util_name()))
165        .about(translate!("tr-about"))
166        .override_usage(format_usage(&translate!("tr-usage")))
167        .after_help(translate!("tr-after-help"))
168        .infer_long_args(true)
169        .trailing_var_arg(true)
170        .arg(
171            Arg::new(options::COMPLEMENT)
172                .visible_short_alias('C')
173                .short('c')
174                .long(options::COMPLEMENT)
175                .help(translate!("tr-help-complement"))
176                .action(ArgAction::SetTrue)
177                .overrides_with(options::COMPLEMENT),
178        )
179        .arg(
180            Arg::new(options::DELETE)
181                .short('d')
182                .long(options::DELETE)
183                .help(translate!("tr-help-delete"))
184                .action(ArgAction::SetTrue)
185                .overrides_with(options::DELETE),
186        )
187        .arg(
188            Arg::new(options::SQUEEZE)
189                .long(options::SQUEEZE)
190                .short('s')
191                .help(translate!("tr-help-squeeze"))
192                .action(ArgAction::SetTrue)
193                .overrides_with(options::SQUEEZE),
194        )
195        .arg(
196            Arg::new(options::TRUNCATE_SET1)
197                .long(options::TRUNCATE_SET1)
198                .short('t')
199                .help(translate!("tr-help-truncate-set1"))
200                .action(ArgAction::SetTrue)
201                .overrides_with(options::TRUNCATE_SET1),
202        )
203        .arg(
204            Arg::new(options::SETS)
205                .num_args(1..)
206                .value_parser(value_parser!(OsString)),
207        )
208}