@@ -100,6 +100,9 @@ pub(super) struct EscapedShellQuoter<'a> {
100100 /// Whether the name should be quoted.
101101 must_quote : bool ,
102102
103+ /// Whether we committed to dollar quoting for the entire string.
104+ committed_dollar : bool ,
105+
103106 /// Whether we are currently in a dollar escaped environment.
104107 in_dollar : bool ,
105108
@@ -109,24 +112,35 @@ pub(super) struct EscapedShellQuoter<'a> {
109112impl < ' a > EscapedShellQuoter < ' a > {
110113 pub fn new ( reference : & ' a [ u8 ] , always_quote : bool , dirname : bool , size_hint : usize ) -> Self {
111114 let ( quotes, must_quote) = initial_quoting ( reference, dirname, always_quote) ;
115+
116+ // Pre-scan to check if we have any control characters
117+ // If so, commit to $'...' quoting for the entire string
118+ let has_control_chars = reference. iter ( ) . any ( |& b| b. is_ascii_control ( ) ) ;
119+
120+ let mut buffer = Vec :: with_capacity ( size_hint) ;
121+ if has_control_chars {
122+ buffer. extend ( b"$'" ) ;
123+ }
124+
112125 Self {
113126 reference,
114127 quotes,
115128 must_quote,
116- in_dollar : false ,
117- buffer : Vec :: with_capacity ( size_hint) ,
129+ committed_dollar : has_control_chars,
130+ in_dollar : has_control_chars,
131+ buffer,
118132 }
119133 }
120134
121135 fn enter_dollar ( & mut self ) {
122- if !self . in_dollar {
136+ if !self . committed_dollar && ! self . in_dollar {
123137 self . buffer . extend ( b"'$'" ) ;
124138 self . in_dollar = true ;
125139 }
126140 }
127141
128142 fn exit_dollar ( & mut self ) {
129- if self . in_dollar {
143+ if ! self . committed_dollar && self . in_dollar {
130144 self . buffer . extend ( b"''" ) ;
131145 self . in_dollar = false ;
132146 }
@@ -137,22 +151,52 @@ impl Quoter for EscapedShellQuoter<'_> {
137151 fn push_char ( & mut self , input : char ) {
138152 let escaped = EscapedChar :: new_shell ( input, true , self . quotes ) ;
139153 match escaped. state {
154+ // Single quotes need escaping - check BEFORE general Char(x)
155+ EscapeState :: Backslash ( '\'' ) | EscapeState :: Char ( '\'' ) => {
156+ if self . committed_dollar {
157+ // In committed dollar mode, escape as \'
158+ self . must_quote = true ;
159+ self . buffer . extend ( b"\\ '" ) ;
160+ } else {
161+ // In backslash mode (no control chars), escape as \'
162+ self . exit_dollar ( ) ;
163+ self . must_quote = true ;
164+ self . buffer . push ( b'\\' ) ;
165+ self . buffer . push ( b'\'' ) ;
166+ }
167+ }
168+ EscapeState :: Backslash ( '\\' ) => {
169+ if self . committed_dollar {
170+ // In committed dollar mode, escape as \\
171+ self . must_quote = true ;
172+ self . buffer . extend ( b"\\ \\ " ) ;
173+ } else {
174+ self . enter_dollar ( ) ;
175+ self . must_quote = true ;
176+ self . buffer . extend ( b"\\ \\ " ) ;
177+ }
178+ }
140179 EscapeState :: Char ( x) => {
141- self . exit_dollar ( ) ;
142- self . buffer . extend ( x. to_string ( ) . as_bytes ( ) ) ;
180+ if self . committed_dollar {
181+ // In committed dollar mode, regular chars are literal
182+ self . buffer . extend ( x. to_string ( ) . as_bytes ( ) ) ;
183+ } else {
184+ self . exit_dollar ( ) ;
185+ self . buffer . extend ( x. to_string ( ) . as_bytes ( ) ) ;
186+ }
143187 }
144188 EscapeState :: ForceQuote ( x) => {
145- self . exit_dollar ( ) ;
146- self . must_quote = true ;
147- self . buffer . extend ( x . to_string ( ) . as_bytes ( ) ) ;
148- }
149- // Single quotes are not put in dollar expressions, but are escaped
150- // if the string also contains double quotes. In that case, they
151- // must be handled separately.
152- EscapeState :: Backslash ( '\'' ) => {
153- self . exit_dollar ( ) ;
154- self . must_quote = true ;
155- self . buffer . extend ( b"' \\ ''" ) ;
189+ if self . committed_dollar {
190+ // In committed dollar mode, shell metacharacters are literal (no escaping needed)
191+ self . must_quote = true ;
192+ self . buffer . extend ( x . to_string ( ) . as_bytes ( ) ) ;
193+ } else {
194+ // Outside committed dollar mode, use backslash escaping
195+ self . exit_dollar ( ) ;
196+ self . must_quote = true ;
197+ self . buffer . push ( b'\\' ) ;
198+ self . buffer . extend ( x . to_string ( ) . as_bytes ( ) ) ;
199+ }
156200 }
157201 _ => {
158202 self . enter_dollar ( ) ;
@@ -179,8 +223,21 @@ impl Quoter for EscapedShellQuoter<'_> {
179223 ) ;
180224 }
181225
182- fn finalize ( self : Box < Self > ) -> Vec < u8 > {
183- finalize_shell_quoter ( self . buffer , self . reference , self . must_quote , self . quotes )
226+ fn finalize ( mut self : Box < Self > ) -> Vec < u8 > {
227+ // Close the committed dollar quote if needed
228+ if self . committed_dollar {
229+ self . buffer . push ( b'\'' ) ;
230+ return self . buffer ;
231+ }
232+
233+ // Empty string special case - return ''
234+ if self . reference . is_empty ( ) {
235+ return b"''" . to_vec ( ) ;
236+ }
237+
238+ // For backslash escaping mode (no control chars), don't add outer quotes
239+ // This matches bash printf %q behavior
240+ self . buffer
184241 }
185242}
186243
0 commit comments