Skip to content

Commit b987db6

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 b987db6

File tree

5 files changed

+191
-252
lines changed

5 files changed

+191
-252
lines changed

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

Lines changed: 14 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,14 @@ 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+
// printf %q uses committed dollar mode (entire string in $'...' when control chars present)
408+
let printf_style = QuotingStyle::Shell {
409+
escape: true,
410+
always_quote: false,
411+
show_control: false,
412+
commit_dollar_mode: true, // printf %q style
413+
};
414+
let s = escape_name(args.next_string(position), printf_style, UEncoding::Utf8);
403415
let bytes = os_str_as_bytes(&s)?;
404416
writer.write_all(bytes).map_err(FormatError::IoError)
405417
}

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

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@ pub enum EscapeState {
2626
}
2727

2828
/// Bytes we need to present as escaped octal, in the form of `\nnn` per byte.
29-
/// Only supports characters up to 2 bytes long in UTF-8.
29+
/// Supports characters up to 4 bytes long in UTF-8.
3030
pub struct EscapeOctal {
31-
c: [u8; 2],
31+
bytes: [u8; 4],
32+
num_bytes: usize,
33+
byte_idx: usize,
34+
digit_idx: u8,
3235
state: EscapeOctalState,
33-
idx: u8,
3436
}
3537

3638
enum EscapeOctalState {
3739
Done,
38-
FirstBackslash,
39-
FirstValue,
40-
LastBackslash,
41-
LastValue,
40+
Backslash,
41+
Value,
4242
}
4343

4444
fn byte_to_octal_digit(byte: u8, idx: u8) -> u8 {
@@ -51,30 +51,23 @@ impl Iterator for EscapeOctal {
5151
fn next(&mut self) -> Option<char> {
5252
match self.state {
5353
EscapeOctalState::Done => None,
54-
EscapeOctalState::FirstBackslash => {
55-
self.state = EscapeOctalState::FirstValue;
54+
EscapeOctalState::Backslash => {
55+
self.state = EscapeOctalState::Value;
5656
Some('\\')
5757
}
58-
EscapeOctalState::LastBackslash => {
59-
self.state = EscapeOctalState::LastValue;
60-
Some('\\')
61-
}
62-
EscapeOctalState::FirstValue => {
63-
let octal_digit = byte_to_octal_digit(self.c[0], self.idx);
64-
if self.idx == 0 {
65-
self.state = EscapeOctalState::LastBackslash;
66-
self.idx = 2;
67-
} else {
68-
self.idx -= 1;
69-
}
70-
Some(from_digit(octal_digit.into(), 8).unwrap())
71-
}
72-
EscapeOctalState::LastValue => {
73-
let octal_digit = byte_to_octal_digit(self.c[1], self.idx);
74-
if self.idx == 0 {
75-
self.state = EscapeOctalState::Done;
58+
EscapeOctalState::Value => {
59+
let octal_digit = byte_to_octal_digit(self.bytes[self.byte_idx], self.digit_idx);
60+
if self.digit_idx == 0 {
61+
// Move to next byte
62+
self.byte_idx += 1;
63+
if self.byte_idx >= self.num_bytes {
64+
self.state = EscapeOctalState::Done;
65+
} else {
66+
self.state = EscapeOctalState::Backslash;
67+
self.digit_idx = 2;
68+
}
7669
} else {
77-
self.idx -= 1;
70+
self.digit_idx -= 1;
7871
}
7972
Some(from_digit(octal_digit.into(), 8).unwrap())
8073
}
@@ -88,20 +81,24 @@ impl EscapeOctal {
8881
return Self::from_byte(c as u8);
8982
}
9083

91-
let mut buf = [0; 2];
92-
let _s = c.encode_utf8(&mut buf);
84+
let mut bytes = [0; 4];
85+
let len = c.encode_utf8(&mut bytes).len();
9386
Self {
94-
c: buf,
95-
idx: 2,
96-
state: EscapeOctalState::FirstBackslash,
87+
bytes,
88+
num_bytes: len,
89+
byte_idx: 0,
90+
digit_idx: 2,
91+
state: EscapeOctalState::Backslash,
9792
}
9893
}
9994

10095
fn from_byte(b: u8) -> Self {
10196
Self {
102-
c: [0, b],
103-
idx: 2,
104-
state: EscapeOctalState::LastBackslash,
97+
bytes: [b, 0, 0, 0],
98+
num_bytes: 1,
99+
byte_idx: 0,
100+
digit_idx: 2,
101+
state: EscapeOctalState::Backslash,
105102
}
106103
}
107104
}

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

Lines changed: 15 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.
@@ -36,6 +35,11 @@ pub enum QuotingStyle {
3635

3736
/// Whether to show control and non-unicode characters, or replace them with `?`.
3837
show_control: bool,
38+
39+
/// Whether to commit to dollar quoting for the entire string (printf %q style).
40+
/// true: committed mode - wrap entire string in $'...' when control chars present
41+
/// false: selective mode (ls style) - only wrap individual control chars in $'...'
42+
commit_dollar_mode: bool,
3943
},
4044

4145
/// Escape the name as a C string.
@@ -59,24 +63,28 @@ impl QuotingStyle {
5963
escape: false,
6064
always_quote: false,
6165
show_control: false,
66+
commit_dollar_mode: false, // ls style - selective dollar mode
6267
};
6368

6469
pub const SHELL_ESCAPE: Self = Self::Shell {
6570
escape: true,
6671
always_quote: false,
6772
show_control: false,
73+
commit_dollar_mode: false, // ls style - selective dollar mode
6874
};
6975

7076
pub const SHELL_QUOTE: Self = Self::Shell {
7177
escape: false,
7278
always_quote: true,
7379
show_control: false,
80+
commit_dollar_mode: false, // ls style - selective dollar mode
7481
};
7582

7683
pub const SHELL_ESCAPE_QUOTE: Self = Self::Shell {
7784
escape: true,
7885
always_quote: true,
7986
show_control: false,
87+
commit_dollar_mode: false, // ls style - selective dollar mode
8088
};
8189

8290
pub const C_NO_QUOTES: Self = Self::C {
@@ -95,11 +103,13 @@ impl QuotingStyle {
95103
Shell {
96104
escape,
97105
always_quote,
106+
commit_dollar_mode,
98107
..
99108
} => Shell {
100109
escape,
101110
always_quote,
102111
show_control,
112+
commit_dollar_mode,
103113
},
104114
Literal { .. } => Literal { show_control },
105115
C { .. } => self,
@@ -162,17 +172,20 @@ fn escape_name_inner(
162172
QuotingStyle::Shell {
163173
escape: true,
164174
always_quote,
175+
commit_dollar_mode,
165176
..
166177
} => Box::new(EscapedShellQuoter::new(
167178
name,
168179
always_quote,
169180
dirname,
181+
commit_dollar_mode,
170182
name.len(),
171183
)),
172184
QuotingStyle::Shell {
173185
escape: false,
174186
always_quote,
175187
show_control,
188+
..
176189
} => Box::new(NonEscapedShellQuoter::new(
177190
name,
178191
show_control,
@@ -229,25 +242,14 @@ pub fn locale_aware_escape_dir_name(name: &OsStr, style: QuotingStyle) -> OsStri
229242
escape_dir_name(name, style, i18n::get_locale_encoding())
230243
}
231244

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-
244245
impl fmt::Display for QuotingStyle {
245246
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246247
match *self {
247248
Self::Shell {
248249
escape,
249250
always_quote,
250251
show_control,
252+
..
251253
} => {
252254
let mut style = "shell".to_string();
253255
if escape {

0 commit comments

Comments
 (0)