Skip to content

Commit f1436f3

Browse files
RGBCubeRenjiSann
authored andcommitted
echo: cleanup
1 parent 68bc7bb commit f1436f3

File tree

2 files changed

+127
-92
lines changed

2 files changed

+127
-92
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ unused_qualifications = "warn"
626626
all = { level = "warn", priority = -1 }
627627
cargo = { level = "warn", priority = -1 }
628628
pedantic = { level = "warn", priority = -1 }
629+
match_bool = "allow" # 8310
629630
cargo_common_metadata = "allow" # 3240
630631
multiple_crate_versions = "allow" # 2314
631632
missing_errors_doc = "allow" # 1504

src/uu/echo/src/echo.rs

Lines changed: 126 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -21,109 +21,149 @@ mod options {
2121
pub const DISABLE_BACKSLASH_ESCAPE: &str = "disable_backslash_escape";
2222
}
2323

24-
/// Holds the options for echo command:
25-
/// -n (disable newline)
26-
/// -e/-E (escape handling),
27-
struct EchoOptions {
28-
/// -n flag option: if true, output a trailing newline (-n disables it)
29-
/// Default: true
24+
/// Options for the echo command.
25+
#[derive(Debug, Clone, Copy)]
26+
struct Options {
27+
/// Whether the output should have a trailing newline.
28+
///
29+
/// True by default. `-n` disables it.
3030
pub trailing_newline: bool,
3131

32-
/// -e enables escape interpretation, -E disables it
33-
/// Default: false (escape interpretation disabled)
32+
/// Whether given string literals should be parsed for
33+
/// escape characters.
34+
///
35+
/// False by default, can be enabled with `-e`. Always true if
36+
/// `POSIXLY_CORRECT` (cannot be disabled with `-E`).
3437
pub escape: bool,
3538
}
3639

37-
/// Checks if an argument is a valid echo flag
38-
/// Returns true if valid echo flag found
39-
fn is_echo_flag(arg: &OsString, echo_options: &mut EchoOptions) -> bool {
40-
let bytes = arg.as_encoded_bytes();
41-
if bytes.first() == Some(&b'-') && arg != "-" {
42-
// we initialize our local variables to the "current" options so we don't override
43-
// previous found flags
44-
let mut escape = echo_options.escape;
45-
let mut trailing_newline = echo_options.trailing_newline;
46-
47-
// Process characters after the '-'
48-
for c in &bytes[1..] {
49-
match c {
50-
b'e' => escape = true,
51-
b'E' => escape = false,
52-
b'n' => trailing_newline = false,
53-
// if there is any char in an argument starting with '-' that doesn't match e/E/n
54-
// present means that this argument is not a flag
55-
_ => return false,
56-
}
40+
impl Default for Options {
41+
fn default() -> Self {
42+
Self {
43+
trailing_newline: true,
44+
escape: false,
5745
}
46+
}
47+
}
5848

59-
// we only override the options with flags being found once we parsed the whole argument
60-
echo_options.escape = escape;
61-
echo_options.trailing_newline = trailing_newline;
62-
return true;
49+
impl Options {
50+
fn posixly_correct_default() -> Self {
51+
Self {
52+
trailing_newline: true,
53+
escape: true,
54+
}
55+
}
56+
}
57+
58+
/// Checks if an argument is a valid echo flag, and if
59+
/// it is records the changes in [`Options`].
60+
fn is_flag(arg: &OsStr, options: &mut Options) -> bool {
61+
let arg = arg.as_encoded_bytes();
62+
63+
if arg.first() != Some(&b'-') || arg == b"-" {
64+
// Argument doesn't start with '-' or is '-' => not a flag.
65+
return false;
66+
}
67+
68+
// We don't modify the given options until after
69+
// the loop because there is a chance the flag isn't
70+
// valid after all & shouldn't affect the options.
71+
let mut options_: Options = *options;
72+
73+
// Skip the '-' when processing characters.
74+
for c in &arg[1..] {
75+
match c {
76+
b'e' => options_.escape = true,
77+
b'E' => options_.escape = false,
78+
b'n' => options_.trailing_newline = false,
79+
80+
// If there is any character in an supposed flag
81+
// that is not a valid flag character, it is not
82+
// a flag.
83+
//
84+
// "-eeEnEe" => is a flag.
85+
// "-eeBne" => not a flag, short circuit at the B.
86+
_ => return false,
87+
}
6388
}
6489

65-
// argument doesn't start with '-' or is "-" => no flag
66-
false
90+
// We are now sure that the argument is a
91+
// flag, and can apply the modified options.
92+
*options = options_;
93+
true
6794
}
6895

69-
/// Processes command line arguments, separating flags from normal arguments
70-
/// Returns:
71-
/// - Vector of non-flag arguments
72-
/// - `trailing_newline`: whether to print a trailing newline
73-
/// - `escape`: whether to process escape sequences
74-
fn filter_echo_flags(args: impl uucore::Args) -> (Vec<OsString>, bool, bool) {
75-
let mut result = Vec::new();
76-
let mut echo_options = EchoOptions {
77-
trailing_newline: true,
78-
escape: false,
79-
};
80-
let mut args_iter = args.into_iter();
81-
82-
// Process arguments until first non-flag is found
83-
for arg in &mut args_iter {
84-
// we parse flags and store options found in "echo_option". First is_echo_flag
85-
// call to return false will break the loop and we will collect the remaining arguments
86-
if !is_echo_flag(&arg, &mut echo_options) {
87-
// First non-flag argument stops flag processing
88-
result.push(arg);
96+
/// Processes command line arguments, separating flags from normal arguments.
97+
///
98+
/// # Returns
99+
///
100+
/// - Vector of non-flag arguments.
101+
/// - [`Options`], describing how teh arguments should be interpreted.
102+
fn filter_flags(mut args: impl uucore::Args) -> (Vec<OsString>, Options) {
103+
let mut arguments = Vec::with_capacity(args.size_hint().0);
104+
let mut options = Options::default();
105+
106+
// Process arguments until first non-flag is found.
107+
for arg in &mut args {
108+
// We parse flags and aggregate the options in `options`.
109+
// First call to `is_echo_flag` to return false will break the loop.
110+
if !is_flag(&arg, &mut options) {
111+
// Not a flag. Can break out of flag-processing loop.
112+
// Don't forget to push it to the arguments too.
113+
arguments.push(arg);
89114
break;
90115
}
91116
}
92-
// Collect remaining arguments
93-
for arg in args_iter {
94-
result.push(arg);
95-
}
96-
(result, echo_options.trailing_newline, echo_options.escape)
117+
118+
// Collect remaining non-flag arguments.
119+
arguments.extend(args);
120+
121+
(arguments, options)
97122
}
98123

99124
#[uucore::main]
100125
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
126+
// args[0] is the name of the binary.
127+
let mut args = args.skip(1).peekable();
128+
101129
// Check POSIX compatibility mode
130+
//
131+
// From the GNU manual, on what it should do:
132+
//
133+
// > If the POSIXLY_CORRECT environment variable is set, then when
134+
// > echo’s first argument is not -n it outputs option-like arguments
135+
// > instead of treating them as options. For example, echo -ne hello
136+
// > outputs ‘-ne hello’ instead of plain ‘hello’. Also backslash
137+
// > escapes are always enabled. To echo the string ‘-n’, one of the
138+
// > characters can be escaped in either octal or hexadecimal
139+
// > representation. For example, echo -e '\x2dn'.
102140
let is_posixly_correct = env::var_os("POSIXLY_CORRECT").is_some();
103141

104-
let args_iter = args.skip(1);
105-
let (args, trailing_newline, escaped) = if is_posixly_correct {
106-
let mut args_iter = args_iter.peekable();
142+
let (args, options) = match is_posixly_correct {
143+
// if POSIXLY_CORRECT is not set we filter the flags normally
144+
false => filter_flags(args),
107145

108-
if args_iter.peek() == Some(&OsString::from("-n")) {
146+
true if args.peek().is_some_and(|arg| arg == "-n") => {
109147
// if POSIXLY_CORRECT is set and the first argument is the "-n" flag
110-
// we filter flags normally but 'escaped' is activated nonetheless
111-
let (args, _, _) = filter_echo_flags(args_iter);
112-
(args, false, true)
113-
} else {
148+
// we filter flags normally but 'escaped' is activated nonetheless.
149+
let (args, _) = filter_flags(args);
150+
(
151+
args,
152+
Options {
153+
trailing_newline: false,
154+
..Options::posixly_correct_default()
155+
},
156+
)
157+
}
158+
159+
true => {
114160
// if POSIXLY_CORRECT is set and the first argument is not the "-n" flag
115-
// we just collect all arguments as every argument is considered an argument
116-
let args: Vec<OsString> = args_iter.collect();
117-
(args, true, true)
161+
// we just collect all arguments as every argument is considered an argument.
162+
(args.collect(), Options::posixly_correct_default())
118163
}
119-
} else {
120-
// if POSIXLY_CORRECT is not set we filter the flags normally
121-
let (args, trailing_newline, escaped) = filter_echo_flags(args_iter);
122-
(args, trailing_newline, escaped)
123164
};
124165

125-
let mut stdout_lock = io::stdout().lock();
126-
execute(&mut stdout_lock, args, trailing_newline, escaped)?;
166+
execute(&mut io::stdout().lock(), args, options)?;
127167

128168
Ok(())
129169
}
@@ -169,34 +209,29 @@ pub fn uu_app() -> Command {
169209
)
170210
}
171211

172-
fn execute(
173-
stdout_lock: &mut StdoutLock,
174-
arguments_after_options: Vec<OsString>,
175-
trailing_newline: bool,
176-
escaped: bool,
177-
) -> UResult<()> {
178-
for (i, input) in arguments_after_options.into_iter().enumerate() {
179-
let Some(bytes) = bytes_from_os_string(input.as_os_str()) else {
212+
fn execute(stdout: &mut StdoutLock, args: Vec<OsString>, options: Options) -> UResult<()> {
213+
for (i, arg) in args.into_iter().enumerate() {
214+
let Some(bytes) = bytes_from_os_string(arg.as_os_str()) else {
180215
return Err(USimpleError::new(1, get_message("echo-error-non-utf8")));
181216
};
182217

183218
if i > 0 {
184-
stdout_lock.write_all(b" ")?;
219+
stdout.write_all(b" ")?;
185220
}
186221

187-
if escaped {
222+
if options.escape {
188223
for item in parse_escape_only(bytes, OctalParsing::ThreeDigits) {
189-
if item.write(&mut *stdout_lock)?.is_break() {
224+
if item.write(&mut *stdout)?.is_break() {
190225
return Ok(());
191226
}
192227
}
193228
} else {
194-
stdout_lock.write_all(bytes)?;
229+
stdout.write_all(bytes)?;
195230
}
196231
}
197232

198-
if trailing_newline {
199-
stdout_lock.write_all(b"\n")?;
233+
if options.trailing_newline {
234+
stdout.write_all(b"\n")?;
200235
}
201236

202237
Ok(())
@@ -212,8 +247,7 @@ fn bytes_from_os_string(input: &OsStr) -> Option<&[u8]> {
212247

213248
#[cfg(not(target_family = "unix"))]
214249
{
215-
// TODO
216-
// Verify that this works correctly on these platforms
250+
// TODO: Verify that this works correctly on these platforms
217251
input.to_str().map(|st| st.as_bytes())
218252
}
219253
}

0 commit comments

Comments
 (0)