Skip to content

Commit b7fc62e

Browse files
committed
stat: handle byte as a format for better display
1 parent a945717 commit b7fc62e

File tree

3 files changed

+91
-24
lines changed

3 files changed

+91
-24
lines changed

src/uu/stat/src/stat.rs

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
2222
use std::borrow::Cow;
2323
use std::ffi::{OsStr, OsString};
2424
use std::fs::{FileType, Metadata};
25+
use std::io::Write;
2526
use std::os::unix::fs::{FileTypeExt, MetadataExt};
2627
use std::os::unix::prelude::OsStrExt;
2728
use std::path::Path;
@@ -119,6 +120,7 @@ impl std::str::FromStr for QuotingStyle {
119120
#[derive(Debug, PartialEq, Eq)]
120121
enum Token {
121122
Char(char),
123+
Byte(u8),
122124
Directive {
123125
flag: Flags,
124126
width: usize,
@@ -362,6 +364,7 @@ fn get_quoted_file_name(
362364

363365
fn process_token_fs(t: &Token, meta: StatFs, display_name: &str) {
364366
match *t {
367+
Token::Byte(byte) => write_raw_byte(byte),
365368
Token::Char(c) => print!("{c}"),
366369
Token::Directive {
367370
flag,
@@ -512,6 +515,10 @@ fn print_unsigned_hex(
512515
pad_and_print(&s, flags.left, width, padding_char);
513516
}
514517

518+
fn write_raw_byte(byte: u8) {
519+
std::io::stdout().write_all(&[byte]).unwrap();
520+
}
521+
515522
impl Stater {
516523
fn process_flags(chars: &[char], i: &mut usize, bound: usize, flag: &mut Flags) {
517524
while *i < bound {
@@ -614,33 +621,49 @@ impl Stater {
614621
return Token::Char('\\');
615622
}
616623
match chars[*i] {
617-
'x' if *i + 1 < bound => {
618-
if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) {
619-
*i += offset;
620-
Token::Char(c)
621-
} else {
622-
show_warning!("unrecognized escape '\\x'");
623-
Token::Char('x')
624+
'a' => Token::Byte(0x07), // BEL
625+
'b' => Token::Byte(0x08), // Backspace
626+
'f' => Token::Byte(0x0C), // Form feed
627+
'n' => Token::Byte(0x0A), // Line feed
628+
'r' => Token::Byte(0x0D), // Carriage return
629+
't' => Token::Byte(0x09), // Horizontal tab
630+
'\\' => Token::Byte(b'\\'), // Backslash
631+
'\'' => Token::Byte(b'\''), // Single quote
632+
'"' => Token::Byte(b'"'), // Double quote
633+
'0'..='7' => {
634+
// Parse octal escape sequence (up to 3 digits)
635+
let mut value = 0u8;
636+
let mut count = 0;
637+
while *i < bound && count < 3 {
638+
if let Some(digit) = chars[*i].to_digit(8) {
639+
value = value * 8 + digit as u8;
640+
*i += 1;
641+
count += 1;
642+
} else {
643+
break;
644+
}
624645
}
646+
*i -= 1; // Adjust index to account for the outer loop increment
647+
Token::Byte(value)
625648
}
626-
'0'..='7' => {
627-
let (c, offset) = format_str[*i..].scan_char(8).unwrap();
628-
*i += offset - 1;
629-
Token::Char(c)
649+
'x' => {
650+
// Parse hexadecimal escape sequence
651+
if *i + 1 < bound {
652+
if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) {
653+
*i += offset;
654+
Token::Byte(c as u8)
655+
} else {
656+
show_warning!("unrecognized escape '\\x'");
657+
Token::Byte(b'x')
658+
}
659+
} else {
660+
show_warning!("incomplete hex escape '\\x'");
661+
Token::Byte(b'x')
662+
}
630663
}
631-
'"' => Token::Char('"'),
632-
'\\' => Token::Char('\\'),
633-
'a' => Token::Char('\x07'),
634-
'b' => Token::Char('\x08'),
635-
'e' => Token::Char('\x1B'),
636-
'f' => Token::Char('\x0C'),
637-
'n' => Token::Char('\n'),
638-
'r' => Token::Char('\r'),
639-
't' => Token::Char('\t'),
640-
'v' => Token::Char('\x0B'),
641-
c => {
642-
show_warning!("unrecognized escape '\\{}'", c);
643-
Token::Char(c)
664+
other => {
665+
show_warning!("unrecognized escape '\\{}'", other);
666+
Token::Byte(other as u8)
644667
}
645668
}
646669
}
@@ -773,7 +796,9 @@ impl Stater {
773796
from_user: bool,
774797
) -> Result<(), i32> {
775798
match *t {
799+
Token::Byte(byte) => write_raw_byte(byte),
776800
Token::Char(c) => print!("{c}"),
801+
777802
Token::Directive {
778803
flag,
779804
width,

tests/by-util/test_stat.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,42 @@ fn test_quoting_style_locale() {
373373
.stdout_is("\"'\"\n")
374374
.succeeded();
375375
}
376+
377+
#[test]
378+
fn test_printf_octal_1() {
379+
let ts = TestScenario::new(util_name!());
380+
let expected_stdout = vec![0x0A, 0xFF]; // Newline + byte 255
381+
ts.ucmd()
382+
.args(&["--printf=\\012\\377", "."])
383+
.succeeds()
384+
.stdout_is_bytes(expected_stdout);
385+
}
386+
387+
#[test]
388+
fn test_printf_octal_2() {
389+
let ts = TestScenario::new(util_name!());
390+
let expected_stdout = vec![b'.', 0x0A, b'a', 0xFF, b'b']; // ".\naxÿb"
391+
ts.ucmd()
392+
.args(&["--printf=.\\012a\\377b", "."])
393+
.succeeds()
394+
.stdout_is_bytes(expected_stdout);
395+
}
396+
397+
#[test]
398+
fn test_printf_hex_3() {
399+
let ts = TestScenario::new(util_name!());
400+
ts.ucmd()
401+
.args(&["--printf=\\x", "."])
402+
.run()
403+
.stderr_contains("warning: incomplete hex escape");
404+
}
405+
406+
#[test]
407+
fn test_printf_bel_etc() {
408+
let ts = TestScenario::new(util_name!());
409+
let expected_stdout = vec![0x07, 0x08, 0x0C, 0x0A, 0x0D, 0x09]; // BEL, BS, FF, LF, CR, TAB
410+
ts.ucmd()
411+
.args(&["--printf=\\a\\b\\f\\n\\r\\t", "."])
412+
.succeeds()
413+
.stdout_is_bytes(expected_stdout);
414+
}

util/build-gnu.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not
204204
# Our message is a bit better
205205
sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh
206206

207+
# Our message is better
208+
sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl
209+
207210
sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh
208211
sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh
209212
sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh

0 commit comments

Comments
 (0)