Skip to content

Commit dd82476

Browse files
committed
fix(printf): use shared SHELL_ESCAPE for %q format
Fix EscapedShellQuoter to match bash printf %q behavior. Remove redundant PrintfQuoter implementation. Both ls and printf now share same quoting logic.
1 parent 78d0e23 commit dd82476

File tree

4 files changed

+87
-217
lines changed

4 files changed

+87
-217
lines changed

src/uucore/src/lib/features/format/spec.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ use super::{
1313
},
1414
parse_escape_only,
1515
};
16-
use crate::{format::FormatArguments, os_str_as_bytes};
16+
use crate::{
17+
format::FormatArguments,
18+
i18n::UEncoding,
19+
os_str_as_bytes,
20+
quoting_style::{QuotingStyle, escape_name},
21+
};
1722
use std::{io::Write, num::NonZero, ops::ControlFlow};
1823

1924
/// A parsed specification for formatting a value
@@ -399,7 +404,11 @@ impl Spec {
399404
writer.write_all(&parsed).map_err(FormatError::IoError)
400405
}
401406
Self::QuotedString { position } => {
402-
let s = crate::quoting_style::printf_quote(args.next_string(position));
407+
let s = escape_name(
408+
args.next_string(position),
409+
QuotingStyle::SHELL_ESCAPE,
410+
UEncoding::Utf8,
411+
);
403412
let bytes = os_str_as_bytes(&s)?;
404413
writer.write_all(bytes).map_err(FormatError::IoError)
405414
}

src/uucore/src/lib/features/quoting_style/mod.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ pub use escaped_char::{EscapeState, EscapedChar};
1818

1919
mod c_quoter;
2020
mod literal_quoter;
21-
mod printf_quoter;
2221
mod shell_quoter;
2322

2423
/// The quoting style to use when escaping a name.
@@ -229,18 +228,6 @@ pub fn locale_aware_escape_dir_name(name: &OsStr, style: QuotingStyle) -> OsStri
229228
escape_dir_name(name, style, i18n::get_locale_encoding())
230229
}
231230

232-
/// Escape a string for printf %q format specifier (bash-compatible shell quoting).
233-
/// This uses a simpler algorithm than SHELL_ESCAPE:
234-
/// - Empty strings become ''
235-
/// - Simple alphanumeric strings are unchanged
236-
/// - Strings with shell metacharacters but no control chars use backslash escaping
237-
/// - Strings with control characters use $'...' ANSI-C quoting
238-
pub fn printf_quote(name: &OsStr) -> OsString {
239-
let name = crate::os_str_as_bytes_lossy(name);
240-
crate::os_string_from_vec(printf_quoter::PrintfQuoter::quote(&name))
241-
.expect("all byte sequences should be valid for platform")
242-
}
243-
244231
impl fmt::Display for QuotingStyle {
245232
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246233
match *self {

src/uucore/src/lib/features/quoting_style/printf_quoter.rs

Lines changed: 0 additions & 183 deletions
This file was deleted.

src/uucore/src/lib/features/quoting_style/shell_quoter.rs

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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> {
109112
impl<'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

Comments
 (0)