Skip to content

Commit afbe90f

Browse files
authored
Merge pull request #8310 from RGBCube/echo-help
echo: print help if not posixly correct and only argument is --help
2 parents dddcf2a + 3ea679a commit afbe90f

File tree

2 files changed

+139
-106
lines changed

2 files changed

+139
-106
lines changed

src/uu/echo/src/echo.rs

Lines changed: 133 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::io::{self, StdoutLock, Write};
1111
use uucore::error::{UResult, USimpleError};
1212
use uucore::format::{FormatChar, OctalParsing, parse_escape_only};
1313
use uucore::format_usage;
14+
use uucore::os_str_as_bytes;
1415

1516
use uucore::locale::get_message;
1617

@@ -21,109 +22,157 @@ mod options {
2122
pub const DISABLE_BACKSLASH_ESCAPE: &str = "disable_backslash_escape";
2223
}
2324

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
25+
/// Options for the echo command.
26+
#[derive(Debug, Clone, Copy)]
27+
struct Options {
28+
/// Whether the output should have a trailing newline.
29+
///
30+
/// True by default. `-n` disables it.
3031
pub trailing_newline: bool,
3132

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

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-
}
41+
impl Default for Options {
42+
fn default() -> Self {
43+
Self {
44+
trailing_newline: true,
45+
escape: false,
46+
}
47+
}
48+
}
49+
50+
impl Options {
51+
fn posixly_correct_default() -> Self {
52+
Self {
53+
trailing_newline: true,
54+
escape: true,
5755
}
56+
}
57+
}
58+
59+
/// Checks if an argument is a valid echo flag, and if
60+
/// it is records the changes in [`Options`].
61+
fn is_flag(arg: &OsStr, options: &mut Options) -> bool {
62+
let arg = arg.as_encoded_bytes();
63+
64+
if arg.first() != Some(&b'-') || arg == b"-" {
65+
// Argument doesn't start with '-' or is '-' => not a flag.
66+
return false;
67+
}
5868

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;
69+
// We don't modify the given options until after
70+
// the loop because there is a chance the flag isn't
71+
// valid after all & shouldn't affect the options.
72+
let mut options_: Options = *options;
73+
74+
// Skip the '-' when processing characters.
75+
for c in &arg[1..] {
76+
match c {
77+
b'e' => options_.escape = true,
78+
b'E' => options_.escape = false,
79+
b'n' => options_.trailing_newline = false,
80+
81+
// If there is any character in an supposed flag
82+
// that is not a valid flag character, it is not
83+
// a flag.
84+
//
85+
// "-eeEnEe" => is a flag.
86+
// "-eeBne" => not a flag, short circuit at the B.
87+
_ => return false,
88+
}
6389
}
6490

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

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);
97+
/// Processes command line arguments, separating flags from normal arguments.
98+
///
99+
/// # Returns
100+
///
101+
/// - Vector of non-flag arguments.
102+
/// - [`Options`], describing how teh arguments should be interpreted.
103+
fn filter_flags(mut args: impl Iterator<Item = OsString>) -> (Vec<OsString>, Options) {
104+
let mut arguments = Vec::with_capacity(args.size_hint().0);
105+
let mut options = Options::default();
106+
107+
// Process arguments until first non-flag is found.
108+
for arg in &mut args {
109+
// We parse flags and aggregate the options in `options`.
110+
// First call to `is_echo_flag` to return false will break the loop.
111+
if !is_flag(&arg, &mut options) {
112+
// Not a flag. Can break out of flag-processing loop.
113+
// Don't forget to push it to the arguments too.
114+
arguments.push(arg);
89115
break;
90116
}
91117
}
92-
// Collect remaining arguments
93-
for arg in args_iter {
94-
result.push(arg);
95-
}
96-
(result, echo_options.trailing_newline, echo_options.escape)
118+
119+
// Collect remaining non-flag arguments.
120+
arguments.extend(args);
121+
122+
(arguments, options)
97123
}
98124

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

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();
107-
108-
if args_iter.peek() == Some(&OsString::from("-n")) {
143+
let (args, options) = if is_posixly_correct {
144+
if args.first().is_some_and(|arg| arg == "-n") {
109145
// 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)
146+
// we filter flags normally but 'escaped' is activated nonetheless.
147+
let (args, _) = filter_flags(args.into_iter());
148+
(
149+
args,
150+
Options {
151+
trailing_newline: false,
152+
..Options::posixly_correct_default()
153+
},
154+
)
113155
} else {
114156
// 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)
157+
// we just collect all arguments as no arguments are interpreted as flags.
158+
(args, Options::posixly_correct_default())
118159
}
160+
} else if args.len() == 1 && args[0] == "--help" {
161+
// If POSIXLY_CORRECT is not set and the first argument
162+
// is `--help`, GNU coreutils prints the help message.
163+
//
164+
// Verify this using:
165+
//
166+
// POSIXLY_CORRECT=1 echo --help
167+
// echo --help
168+
uu_app().print_help()?;
169+
return Ok(());
119170
} else {
120171
// 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)
172+
filter_flags(args.into_iter())
123173
};
124174

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

128177
Ok(())
129178
}
@@ -169,51 +218,29 @@ pub fn uu_app() -> Command {
169218
)
170219
}
171220

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 {
180-
return Err(USimpleError::new(1, get_message("echo-error-non-utf8")));
181-
};
221+
fn execute(stdout: &mut StdoutLock, args: Vec<OsString>, options: Options) -> UResult<()> {
222+
for (i, arg) in args.into_iter().enumerate() {
223+
let bytes = os_str_as_bytes(arg.as_os_str())
224+
.map_err(|_| USimpleError::new(1, get_message("echo-error-non-utf8")))?;
182225

183226
if i > 0 {
184-
stdout_lock.write_all(b" ")?;
227+
stdout.write_all(b" ")?;
185228
}
186229

187-
if escaped {
230+
if options.escape {
188231
for item in parse_escape_only(bytes, OctalParsing::ThreeDigits) {
189-
if item.write(&mut *stdout_lock)?.is_break() {
232+
if item.write(&mut *stdout)?.is_break() {
190233
return Ok(());
191234
}
192235
}
193236
} else {
194-
stdout_lock.write_all(bytes)?;
237+
stdout.write_all(bytes)?;
195238
}
196239
}
197240

198-
if trailing_newline {
199-
stdout_lock.write_all(b"\n")?;
241+
if options.trailing_newline {
242+
stdout.write_all(b"\n")?;
200243
}
201244

202245
Ok(())
203246
}
204-
205-
fn bytes_from_os_string(input: &OsStr) -> Option<&[u8]> {
206-
#[cfg(target_family = "unix")]
207-
{
208-
use std::os::unix::ffi::OsStrExt;
209-
210-
Some(input.as_bytes())
211-
}
212-
213-
#[cfg(not(target_family = "unix"))]
214-
{
215-
// TODO
216-
// Verify that this works correctly on these platforms
217-
input.to_str().map(|st| st.as_bytes())
218-
}
219-
}

tests/by-util/test_echo.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,12 @@ fn partial_help_argument() {
514514
new_ucmd!().arg("--he").succeeds().stdout_is("--he\n");
515515
}
516516

517+
#[test]
518+
fn only_help_argument_prints_help() {
519+
assert_ne!(new_ucmd!().arg("--help").succeeds().stdout(), b"--help\n");
520+
assert_ne!(new_ucmd!().arg("--help").succeeds().stdout(), b"--help"); // This one is just in case.
521+
}
522+
517523
#[test]
518524
fn multibyte_escape_unicode() {
519525
// spell-checker:disable-next-line

0 commit comments

Comments
 (0)