@@ -11,6 +11,7 @@ use std::io::{self, StdoutLock, Write};
1111use uucore:: error:: { UResult , USimpleError } ;
1212use uucore:: format:: { FormatChar , OctalParsing , parse_escape_only} ;
1313use uucore:: format_usage;
14+ use uucore:: os_str_as_bytes;
1415
1516use 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]
100126pub 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- }
0 commit comments