@@ -126,55 +126,47 @@ impl<'a> EscapedShellQuoter<'a> {
126126 let ( quotes, must_quote) = initial_quoting ( reference, dirname, always_quote) ;
127127
128128 // commit_dollar_mode controls quoting strategy:
129- // true (printf %q): committed dollar mode - wrap entire string in $'...' when control chars present
130- // false (ls): selective dollar mode - only wrap individual control chars in $'...'
129+ // true (printf %q): use selective dollar-quoting (same as ls)
130+ // false (ls): use selective dollar-quoting
131+ // Both modes use selective quoting: enter $'...' only for control chars
131132 let commit_dollar = commit_dollar_mode;
132133
133- // Pre-scan for control chars or non-ASCII if we're in committed mode
134- let has_control_chars = commit_dollar
135- && reference
136- . iter ( )
137- . any ( |& b| b. is_ascii_control ( ) || !b. is_ascii ( ) ) ;
138-
139- let mut buffer = Vec :: with_capacity ( size_hint) ;
140- if has_control_chars {
141- buffer. extend ( b"$'" ) ;
142- }
143-
144134 Self {
145135 reference,
146136 quotes,
147137 always_quote,
148138 commit_dollar,
149139 must_quote,
150- in_dollar : has_control_chars ,
140+ in_dollar : false , // Never start in dollar mode - enter it dynamically
151141 in_quote_section : false ,
152- buffer,
142+ buffer : Vec :: with_capacity ( size_hint ) ,
153143 }
154144 }
155145
156146 fn enter_dollar ( & mut self ) {
157147 if !self . in_dollar {
158- if self . buffer . is_empty ( ) {
159- // Starting with dollar quote - prepend empty quotes to indicate no prefix
160- // GNU coreutils does this for strings that start with only invalid bytes
161- self . buffer . extend ( b"''$'" ) ;
162- } else {
163- // Had previous content
164- if self . in_quote_section {
165- // Close the existing quote section
166- self . buffer . push ( b'\'' ) ;
167- self . in_quote_section = false ;
148+ if self . in_quote_section {
149+ // Close any existing quote section first
150+ self . buffer . push ( b'\'' ) ;
151+ self . in_quote_section = false ;
152+ } else if self . buffer . is_empty ( ) {
153+ // Both ls and printf %q modes: add empty quotes when buffer is empty
154+ // This indicates the string starts with something needing quotes
155+ self . buffer . extend ( b"''" ) ;
156+ } else if !self . commit_dollar {
157+ // ls mode with existing content: wrap it in quotes
158+ let quote = if self . quotes == Quotes :: Single {
159+ b'\''
168160 } else {
169- // We have unquoted content - need to quote it first
170- let mut temp = Vec :: with_capacity ( self . buffer . len ( ) + 2 ) ;
171- temp. push ( b'\'' ) ;
172- temp. extend ( & self . buffer ) ;
173- temp. push ( b'\'' ) ;
174- self . buffer = temp;
175- }
176- self . buffer . extend ( b"$'" ) ;
161+ b'"'
162+ } ;
163+ let mut quoted = Vec :: with_capacity ( self . buffer . len ( ) + 2 ) ;
164+ quoted. push ( quote) ;
165+ quoted. extend_from_slice ( & self . buffer ) ;
166+ quoted. push ( quote) ;
167+ self . buffer = quoted;
177168 }
169+ self . buffer . extend ( b"$'" ) ;
178170 self . in_dollar = true ;
179171 }
180172 }
@@ -196,18 +188,20 @@ impl Quoter for EscapedShellQuoter<'_> {
196188 // Single quotes need escaping - check BEFORE general Char(x)
197189 EscapeState :: Backslash ( '\'' ) | EscapeState :: Char ( '\'' ) => {
198190 if self . in_dollar {
199- // Inside an existing $'...' section, escape as \'
191+ // Inside $'...' section - need to exit, then handle apostrophe
192+ self . exit_dollar ( ) ;
200193 self . must_quote = true ;
194+ // Backslash-escape the apostrophe
201195 self . buffer . extend ( b"\\ '" ) ;
202196 } else if self . commit_dollar {
203- // printf %q mode (commit_dollar) , not in dollar section:
204- // Check if this is a standalone single quote (buffer still empty = first char)
197+ // printf %q mode, not in dollar section
198+ // Check if this is a standalone single quote
205199 if self . buffer . is_empty ( ) && self . reference . len ( ) == 1 {
206200 // Standalone single quote uses double quotes: "'"
207201 self . must_quote = true ;
208202 self . buffer . extend ( b"\" '\" " ) ;
209203 } else {
210- // Embedded quote (like in "it's") - backslash escape
204+ // Embedded quote - backslash escape
211205 self . must_quote = true ;
212206 self . buffer . extend ( b"\\ '" ) ;
213207 }
@@ -237,7 +231,8 @@ impl Quoter for EscapedShellQuoter<'_> {
237231 EscapeState :: Char ( x) => {
238232 if self . in_dollar {
239233 if self . commit_dollar {
240- // In committed dollar mode (printf), regular chars are literal
234+ // In committed dollar mode (printf), exit and write regular char as-is
235+ self . exit_dollar ( ) ;
241236 self . buffer . extend ( x. to_string ( ) . as_bytes ( ) ) ;
242237 } else {
243238 // In selective dollar mode (ls), exit dollar and start new quoted section
@@ -253,7 +248,7 @@ impl Quoter for EscapedShellQuoter<'_> {
253248 }
254249 } else {
255250 // Not in dollar mode - just add the character
256- // Outer quoting will be handled in finalize if needed
251+ // Quoting will be handled by enter_dollar (when control chars appear) or finalize
257252 self . buffer . extend ( x. to_string ( ) . as_bytes ( ) ) ;
258253 }
259254 }
@@ -273,8 +268,7 @@ impl Quoter for EscapedShellQuoter<'_> {
273268 } else {
274269 // Not in dollar mode
275270 if self . commit_dollar {
276- // printf %q: backslash-escape
277- self . buffer . push ( b'\\' ) ;
271+ // printf %q: just add the character, will be quoted in finalize
278272 self . buffer . extend ( x. to_string ( ) . as_bytes ( ) ) ;
279273 } else {
280274 // ls: will be wrapped in outer quotes, no escaping needed
@@ -286,6 +280,7 @@ impl Quoter for EscapedShellQuoter<'_> {
286280 self . enter_dollar ( ) ;
287281 self . must_quote = true ;
288282 self . buffer . extend ( escaped. collect :: < String > ( ) . as_bytes ( ) ) ;
283+ // Don't exit - let regular chars exit when needed for selective quoting
289284 }
290285 }
291286 }
@@ -337,10 +332,16 @@ impl Quoter for EscapedShellQuoter<'_> {
337332 }
338333
339334 // For strings without dollar quotes, add outer quotes if needed
340- // printf %q (commit_dollar): no outer quotes needed
341- // ls (selective): add outer quotes when must_quote OR always_quote OR starts with special chars
342335 let contains_quote_chars = bytes_start_with ( self . reference , SPECIAL_SHELL_CHARS_START ) ;
343- if !self . commit_dollar && ( self . must_quote || self . always_quote || contains_quote_chars) {
336+ let should_quote = self . must_quote || self . always_quote || contains_quote_chars;
337+
338+ // For printf %q (commit_dollar=true), if the buffer already contains quotes (e.g., "'"
339+ // for a standalone single quote), don't add outer quotes
340+ if self . commit_dollar && ( self . buffer . starts_with ( b"\" '\" " ) || self . buffer . starts_with ( b"'" ) || self . buffer . starts_with ( b"\" " ) ) {
341+ return self . buffer ;
342+ }
343+
344+ if should_quote {
344345 let mut quoted = Vec :: with_capacity ( self . buffer . len ( ) + 2 ) ;
345346 let quote = if self . quotes == Quotes :: Single {
346347 b'\''
@@ -397,7 +398,7 @@ fn finalize_shell_quoter(
397398) -> Vec < u8 > {
398399 let contains_quote_chars = must_quote || bytes_start_with ( reference, SPECIAL_SHELL_CHARS_START ) ;
399400
400- if must_quote | contains_quote_chars && quotes != Quotes :: None {
401+ if ( must_quote || contains_quote_chars) && quotes != Quotes :: None {
401402 let mut quoted = Vec :: < u8 > :: with_capacity ( buffer. len ( ) + 2 ) ;
402403 let quote = if quotes == Quotes :: Single {
403404 b'\''
0 commit comments