1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 11:07:44 +00:00

stat: improve GNU compatibility (#6933)

* stat: fix the quotes when dealing with %N

and other formats
should fix
tests/stat/stat-fmt.sh

* stats: use an enum instead of a string

* stats: split the functions into smaller functions

* stat: handle byte as a format for better display

* stat: handle error better. should make tests/stat/stat-printf.pl pass

* stat: Some escape sequences are non-standard

* Fix tests

* Take comments into account
This commit is contained in:
Sylvestre Ledru 2024-12-10 08:48:52 +01:00 committed by GitHub
parent 22f3358d47
commit c60203ddd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 440 additions and 231 deletions

View file

@ -9,7 +9,9 @@ use uucore::error::{UResult, USimpleError};
use clap::builder::ValueParser; use clap::builder::ValueParser;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::fs::display_permissions; use uucore::fs::display_permissions;
use uucore::fsext::{pretty_filetype, pretty_fstype, read_fs_list, statfs, BirthTime, FsMeta}; use uucore::fsext::{
pretty_filetype, pretty_fstype, read_fs_list, statfs, BirthTime, FsMeta, StatFs,
};
use uucore::libc::mode_t; use uucore::libc::mode_t;
use uucore::{ use uucore::{
entries, format_usage, help_about, help_section, help_usage, show_error, show_warning, entries, format_usage, help_about, help_section, help_usage, show_error, show_warning,
@ -19,10 +21,12 @@ use chrono::{DateTime, Local};
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use std::borrow::Cow; use std::borrow::Cow;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::fs; use std::fs::{FileType, Metadata};
use std::io::Write;
use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::os::unix::prelude::OsStrExt; use std::os::unix::prelude::OsStrExt;
use std::path::Path; use std::path::Path;
use std::{env, fs};
const ABOUT: &str = help_about!("stat.md"); const ABOUT: &str = help_about!("stat.md");
const USAGE: &str = help_usage!("stat.md"); const USAGE: &str = help_usage!("stat.md");
@ -93,9 +97,33 @@ pub enum OutputType {
Unknown, Unknown,
} }
#[derive(Default)]
enum QuotingStyle {
Locale,
Shell,
#[default]
ShellEscapeAlways,
Quote,
}
impl std::str::FromStr for QuotingStyle {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"locale" => Ok(QuotingStyle::Locale),
"shell" => Ok(QuotingStyle::Shell),
"shell-escape-always" => Ok(QuotingStyle::ShellEscapeAlways),
// The others aren't exposed to the user
_ => Err(format!("Invalid quoting style: {}", s)),
}
}
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
enum Token { enum Token {
Char(char), Char(char),
Byte(u8),
Directive { Directive {
flag: Flags, flag: Flags,
width: usize, width: usize,
@ -293,6 +321,93 @@ fn print_str(s: &str, flags: &Flags, width: usize, precision: Option<usize>) {
pad_and_print(s, flags.left, width, Padding::Space); pad_and_print(s, flags.left, width, Padding::Space);
} }
fn quote_file_name(file_name: &str, quoting_style: &QuotingStyle) -> String {
match quoting_style {
QuotingStyle::Locale | QuotingStyle::Shell => {
let escaped = file_name.replace('\'', r"\'");
format!("'{}'", escaped)
}
QuotingStyle::ShellEscapeAlways => format!("\"{}\"", file_name),
QuotingStyle::Quote => file_name.to_string(),
}
}
fn get_quoted_file_name(
display_name: &str,
file: &OsString,
file_type: &FileType,
from_user: bool,
) -> Result<String, i32> {
let quoting_style = env::var("QUOTING_STYLE")
.ok()
.and_then(|style| style.parse().ok())
.unwrap_or_default();
if file_type.is_symlink() {
let quoted_display_name = quote_file_name(display_name, &quoting_style);
match fs::read_link(file) {
Ok(dst) => {
let quoted_dst = quote_file_name(&dst.to_string_lossy(), &quoting_style);
Ok(format!("{quoted_display_name} -> {quoted_dst}"))
}
Err(e) => {
show_error!("{e}");
Err(1)
}
}
} else {
let style = if from_user {
quoting_style
} else {
QuotingStyle::Quote
};
Ok(quote_file_name(display_name, &style))
}
}
fn process_token_filesystem(t: &Token, meta: StatFs, display_name: &str) {
match *t {
Token::Byte(byte) => write_raw_byte(byte),
Token::Char(c) => print!("{c}"),
Token::Directive {
flag,
width,
precision,
format,
} => {
let output = match format {
// free blocks available to non-superuser
'a' => OutputType::Unsigned(meta.avail_blocks()),
// total data blocks in file system
'b' => OutputType::Unsigned(meta.total_blocks()),
// total file nodes in file system
'c' => OutputType::Unsigned(meta.total_file_nodes()),
// free file nodes in file system
'd' => OutputType::Unsigned(meta.free_file_nodes()),
// free blocks in file system
'f' => OutputType::Unsigned(meta.free_blocks()),
// file system ID in hex
'i' => OutputType::UnsignedHex(meta.fsid()),
// maximum length of filenames
'l' => OutputType::Unsigned(meta.namelen()),
// file name
'n' => OutputType::Str(display_name.to_string()),
// block size (for faster transfers)
's' => OutputType::Unsigned(meta.io_size()),
// fundamental block size (for block counts)
'S' => OutputType::Integer(meta.block_size()),
// file system type in hex
't' => OutputType::UnsignedHex(meta.fs_type() as u64),
// file system type in human readable form
'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()),
_ => OutputType::Unknown,
};
print_it(&output, flag, width, precision);
}
}
}
/// Prints an integer value based on the provided flags, width, and precision. /// Prints an integer value based on the provided flags, width, and precision.
/// ///
/// # Arguments /// # Arguments
@ -403,7 +518,26 @@ fn print_unsigned_hex(
pad_and_print(&s, flags.left, width, padding_char); pad_and_print(&s, flags.left, width, padding_char);
} }
fn write_raw_byte(byte: u8) {
std::io::stdout().write_all(&[byte]).unwrap();
}
impl Stater { impl Stater {
fn process_flags(chars: &[char], i: &mut usize, bound: usize, flag: &mut Flags) {
while *i < bound {
match chars[*i] {
'#' => flag.alter = true,
'0' => flag.zero = true,
'-' => flag.left = true,
' ' => flag.space = true,
'+' => flag.sign = true,
'\'' => flag.group = true,
_ => break,
}
*i += 1;
}
}
fn handle_percent_case( fn handle_percent_case(
chars: &[char], chars: &[char],
i: &mut usize, i: &mut usize,
@ -423,20 +557,7 @@ impl Stater {
let mut flag = Flags::default(); let mut flag = Flags::default();
while *i < bound { Self::process_flags(chars, i, bound, &mut flag);
match chars[*i] {
'#' => flag.alter = true,
'0' => flag.zero = true,
'-' => flag.left = true,
' ' => flag.space = true,
'+' => flag.sign = true,
'\'' => flag.group = true,
'I' => unimplemented!(),
_ => break,
}
*i += 1;
}
check_bound(format_str, bound, old, *i)?;
let mut width = 0; let mut width = 0;
let mut precision = None; let mut precision = None;
@ -445,6 +566,15 @@ impl Stater {
if let Some((field_width, offset)) = format_str[j..].scan_num::<usize>() { if let Some((field_width, offset)) = format_str[j..].scan_num::<usize>() {
width = field_width; width = field_width;
j += offset; j += offset;
// Reject directives like `%<NUMBER>` by checking if width has been parsed.
if j >= bound || chars[j] == '%' {
let invalid_directive: String = chars[old..=j.min(bound - 1)].iter().collect();
return Err(USimpleError::new(
1,
format!("{}: invalid directive", invalid_directive.quote()),
));
}
} }
check_bound(format_str, bound, old, j)?; check_bound(format_str, bound, old, j)?;
@ -465,9 +595,27 @@ impl Stater {
} }
*i = j; *i = j;
// Check for multi-character specifiers (e.g., `%Hd`, `%Lr`)
if *i + 1 < bound {
if let Some(&next_char) = chars.get(*i + 1) {
if (chars[*i] == 'H' || chars[*i] == 'L') && (next_char == 'd' || next_char == 'r')
{
let specifier = format!("{}{}", chars[*i], next_char);
*i += 1;
return Ok(Token::Directive {
flag,
width,
precision,
format: specifier.chars().next().unwrap(),
});
}
}
}
Ok(Token::Directive { Ok(Token::Directive {
width,
flag, flag,
width,
precision, precision,
format: chars[*i], format: chars[*i],
}) })
@ -485,33 +633,49 @@ impl Stater {
return Token::Char('\\'); return Token::Char('\\');
} }
match chars[*i] { match chars[*i] {
'x' if *i + 1 < bound => { 'a' => Token::Byte(0x07), // BEL
if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) { 'b' => Token::Byte(0x08), // Backspace
*i += offset; 'f' => Token::Byte(0x0C), // Form feed
Token::Char(c) 'n' => Token::Byte(0x0A), // Line feed
'r' => Token::Byte(0x0D), // Carriage return
't' => Token::Byte(0x09), // Horizontal tab
'\\' => Token::Byte(b'\\'), // Backslash
'\'' => Token::Byte(b'\''), // Single quote
'"' => Token::Byte(b'"'), // Double quote
'0'..='7' => {
// Parse octal escape sequence (up to 3 digits)
let mut value = 0u8;
let mut count = 0;
while *i < bound && count < 3 {
if let Some(digit) = chars[*i].to_digit(8) {
value = value * 8 + digit as u8;
*i += 1;
count += 1;
} else {
break;
}
}
*i -= 1; // Adjust index to account for the outer loop increment
Token::Byte(value)
}
'x' => {
// Parse hexadecimal escape sequence
if *i + 1 < bound {
if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) {
*i += offset;
Token::Byte(c as u8)
} else {
show_warning!("unrecognized escape '\\x'");
Token::Byte(b'x')
}
} else { } else {
show_warning!("unrecognized escape '\\x'"); show_warning!("incomplete hex escape '\\x'");
Token::Char('x') Token::Byte(b'x')
} }
} }
'0'..='7' => { other => {
let (c, offset) = format_str[*i..].scan_char(8).unwrap(); show_warning!("unrecognized escape '\\{}'", other);
*i += offset - 1; Token::Byte(other as u8)
Token::Char(c)
}
'"' => Token::Char('"'),
'\\' => Token::Char('\\'),
'a' => Token::Char('\x07'),
'b' => Token::Char('\x08'),
'e' => Token::Char('\x1B'),
'f' => Token::Char('\x0C'),
'n' => Token::Char('\n'),
'r' => Token::Char('\r'),
't' => Token::Char('\t'),
'v' => Token::Char('\x0B'),
c => {
show_warning!("unrecognized escape '\\{}'", c);
Token::Char(c)
} }
} }
} }
@ -634,7 +798,128 @@ impl Stater {
ret ret
} }
#[allow(clippy::cognitive_complexity)] fn process_token_files(
&self,
t: &Token,
meta: &Metadata,
display_name: &str,
file: &OsString,
file_type: &FileType,
from_user: bool,
) -> Result<(), i32> {
match *t {
Token::Byte(byte) => write_raw_byte(byte),
Token::Char(c) => print!("{c}"),
Token::Directive {
flag,
width,
precision,
format,
} => {
let output = match format {
// access rights in octal
'a' => OutputType::UnsignedOct(0o7777 & meta.mode()),
// access rights in human readable form
'A' => OutputType::Str(display_permissions(meta, true)),
// number of blocks allocated (see %B)
'b' => OutputType::Unsigned(meta.blocks()),
// the size in bytes of each block reported by %b
// FIXME: blocksize differs on various platform
// See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line
'B' => OutputType::Unsigned(512),
// device number in decimal
'd' => OutputType::Unsigned(meta.dev()),
// device number in hex
'D' => OutputType::UnsignedHex(meta.dev()),
// raw mode in hex
'f' => OutputType::UnsignedHex(meta.mode() as u64),
// file type
'F' => OutputType::Str(
pretty_filetype(meta.mode() as mode_t, meta.len()).to_owned(),
),
// group ID of owner
'g' => OutputType::Unsigned(meta.gid() as u64),
// group name of owner
'G' => {
let group_name =
entries::gid2grp(meta.gid()).unwrap_or_else(|_| "UNKNOWN".to_owned());
OutputType::Str(group_name)
}
// number of hard links
'h' => OutputType::Unsigned(meta.nlink()),
// inode number
'i' => OutputType::Unsigned(meta.ino()),
// mount point
'm' => OutputType::Str(self.find_mount_point(file).unwrap()),
// file name
'n' => OutputType::Str(display_name.to_string()),
// quoted file name with dereference if symbolic link
'N' => {
let file_name =
get_quoted_file_name(display_name, file, file_type, from_user)?;
OutputType::Str(file_name)
}
// optimal I/O transfer size hint
'o' => OutputType::Unsigned(meta.blksize()),
// total size, in bytes
's' => OutputType::Integer(meta.len() as i64),
// major device type in hex, for character/block device special
// files
't' => OutputType::UnsignedHex(meta.rdev() >> 8),
// minor device type in hex, for character/block device special
// files
'T' => OutputType::UnsignedHex(meta.rdev() & 0xff),
// user ID of owner
'u' => OutputType::Unsigned(meta.uid() as u64),
// user name of owner
'U' => {
let user_name =
entries::uid2usr(meta.uid()).unwrap_or_else(|_| "UNKNOWN".to_owned());
OutputType::Str(user_name)
}
// time of file birth, human-readable; - if unknown
'w' => OutputType::Str(
meta.birth()
.map(|(sec, nsec)| pretty_time(sec as i64, nsec as i64))
.unwrap_or(String::from("-")),
),
// time of file birth, seconds since Epoch; 0 if unknown
'W' => OutputType::Unsigned(meta.birth().unwrap_or_default().0),
// time of last access, human-readable
'x' => OutputType::Str(pretty_time(meta.atime(), meta.atime_nsec())),
// time of last access, seconds since Epoch
'X' => OutputType::Integer(meta.atime()),
// time of last data modification, human-readable
'y' => OutputType::Str(pretty_time(meta.mtime(), meta.mtime_nsec())),
// time of last data modification, seconds since Epoch
'Y' => OutputType::Integer(meta.mtime()),
// time of last status change, human-readable
'z' => OutputType::Str(pretty_time(meta.ctime(), meta.ctime_nsec())),
// time of last status change, seconds since Epoch
'Z' => OutputType::Integer(meta.ctime()),
'R' => {
let major = meta.rdev() >> 8;
let minor = meta.rdev() & 0xff;
OutputType::Str(format!("{},{}", major, minor))
}
'r' => OutputType::Unsigned(meta.rdev()),
'H' => OutputType::Unsigned(meta.rdev() >> 8), // Major in decimal
'L' => OutputType::Unsigned(meta.rdev() & 0xff), // Minor in decimal
_ => OutputType::Unknown,
};
print_it(&output, flag, width, precision);
}
}
Ok(())
}
fn do_stat(&self, file: &OsStr, stdin_is_fifo: bool) -> i32 { fn do_stat(&self, file: &OsStr, stdin_is_fifo: bool) -> i32 {
let display_name = file.to_string_lossy(); let display_name = file.to_string_lossy();
let file = if cfg!(unix) && display_name == "-" { let file = if cfg!(unix) && display_name == "-" {
@ -659,46 +944,9 @@ impl Stater {
Ok(meta) => { Ok(meta) => {
let tokens = &self.default_tokens; let tokens = &self.default_tokens;
// Usage
for t in tokens { for t in tokens {
match *t { process_token_filesystem(t, meta, &display_name);
Token::Char(c) => print!("{c}"),
Token::Directive {
flag,
width,
precision,
format,
} => {
let output = match format {
// free blocks available to non-superuser
'a' => OutputType::Unsigned(meta.avail_blocks()),
// total data blocks in file system
'b' => OutputType::Unsigned(meta.total_blocks()),
// total file nodes in file system
'c' => OutputType::Unsigned(meta.total_file_nodes()),
// free file nodes in file system
'd' => OutputType::Unsigned(meta.free_file_nodes()),
// free blocks in file system
'f' => OutputType::Unsigned(meta.free_blocks()),
// file system ID in hex
'i' => OutputType::UnsignedHex(meta.fsid()),
// maximum length of filenames
'l' => OutputType::Unsigned(meta.namelen()),
// file name
'n' => OutputType::Str(display_name.to_string()),
// block size (for faster transfers)
's' => OutputType::Unsigned(meta.io_size()),
// fundamental block size (for block counts)
'S' => OutputType::Integer(meta.block_size()),
// file system type in hex
't' => OutputType::UnsignedHex(meta.fs_type() as u64),
// file system type in human readable form
'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()),
_ => OutputType::Unknown,
};
print_it(&output, flag, width, precision);
}
}
} }
} }
Err(e) => { Err(e) => {
@ -728,125 +976,15 @@ impl Stater {
}; };
for t in tokens { for t in tokens {
match *t { if let Err(code) = self.process_token_files(
Token::Char(c) => print!("{c}"), t,
Token::Directive { &meta,
flag, &display_name,
width, &file,
precision, &file_type,
format, self.from_user,
} => { ) {
let output = match format { return code;
// access rights in octal
'a' => OutputType::UnsignedOct(0o7777 & meta.mode()),
// access rights in human readable form
'A' => OutputType::Str(display_permissions(&meta, true)),
// number of blocks allocated (see %B)
'b' => OutputType::Unsigned(meta.blocks()),
// the size in bytes of each block reported by %b
// FIXME: blocksize differs on various platform
// See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line
'B' => OutputType::Unsigned(512),
// device number in decimal
'd' => OutputType::Unsigned(meta.dev()),
// device number in hex
'D' => OutputType::UnsignedHex(meta.dev()),
// raw mode in hex
'f' => OutputType::UnsignedHex(meta.mode() as u64),
// file type
'F' => OutputType::Str(
pretty_filetype(meta.mode() as mode_t, meta.len())
.to_owned(),
),
// group ID of owner
'g' => OutputType::Unsigned(meta.gid() as u64),
// group name of owner
'G' => {
let group_name = entries::gid2grp(meta.gid())
.unwrap_or_else(|_| "UNKNOWN".to_owned());
OutputType::Str(group_name)
}
// number of hard links
'h' => OutputType::Unsigned(meta.nlink()),
// inode number
'i' => OutputType::Unsigned(meta.ino()),
// mount point
'm' => OutputType::Str(self.find_mount_point(&file).unwrap()),
// file name
'n' => OutputType::Str(display_name.to_string()),
// quoted file name with dereference if symbolic link
'N' => {
let file_name = if file_type.is_symlink() {
let dst = match fs::read_link(&file) {
Ok(path) => path,
Err(e) => {
println!("{e}");
return 1;
}
};
format!("{} -> {}", display_name.quote(), dst.quote())
} else {
display_name.to_string()
};
OutputType::Str(file_name)
}
// optimal I/O transfer size hint
'o' => OutputType::Unsigned(meta.blksize()),
// total size, in bytes
's' => OutputType::Integer(meta.len() as i64),
// major device type in hex, for character/block device special
// files
't' => OutputType::UnsignedHex(meta.rdev() >> 8),
// minor device type in hex, for character/block device special
// files
'T' => OutputType::UnsignedHex(meta.rdev() & 0xff),
// user ID of owner
'u' => OutputType::Unsigned(meta.uid() as u64),
// user name of owner
'U' => {
let user_name = entries::uid2usr(meta.uid())
.unwrap_or_else(|_| "UNKNOWN".to_owned());
OutputType::Str(user_name)
}
// time of file birth, human-readable; - if unknown
'w' => OutputType::Str(
meta.birth()
.map(|(sec, nsec)| pretty_time(sec as i64, nsec as i64))
.unwrap_or(String::from("-")),
),
// time of file birth, seconds since Epoch; 0 if unknown
'W' => OutputType::Unsigned(meta.birth().unwrap_or_default().0),
// time of last access, human-readable
'x' => OutputType::Str(pretty_time(
meta.atime(),
meta.atime_nsec(),
)),
// time of last access, seconds since Epoch
'X' => OutputType::Integer(meta.atime()),
// time of last data modification, human-readable
'y' => OutputType::Str(pretty_time(
meta.mtime(),
meta.mtime_nsec(),
)),
// time of last data modification, seconds since Epoch
'Y' => OutputType::Integer(meta.mtime()),
// time of last status change, human-readable
'z' => OutputType::Str(pretty_time(
meta.ctime(),
meta.ctime_nsec(),
)),
// time of last status change, seconds since Epoch
'Z' => OutputType::Integer(meta.ctime()),
_ => OutputType::Unknown,
};
print_it(&output, flag, width, precision);
}
} }
} }
} }
@ -1038,7 +1176,7 @@ mod tests {
#[test] #[test]
fn printf_format() { fn printf_format() {
let s = r#"%-# 15a\t\r\"\\\a\b\e\f\v%+020.-23w\x12\167\132\112\n"#; let s = r#"%-# 15a\t\r\"\\\a\b\x1B\f\x0B%+020.-23w\x12\167\132\112\n"#;
let expected = vec![ let expected = vec![
Token::Directive { Token::Directive {
flag: Flags { flag: Flags {
@ -1051,15 +1189,15 @@ mod tests {
precision: None, precision: None,
format: 'a', format: 'a',
}, },
Token::Char('\t'), Token::Byte(b'\t'),
Token::Char('\r'), Token::Byte(b'\r'),
Token::Char('"'), Token::Byte(b'"'),
Token::Char('\\'), Token::Byte(b'\\'),
Token::Char('\x07'), Token::Byte(b'\x07'),
Token::Char('\x08'), Token::Byte(b'\x08'),
Token::Char('\x1B'), Token::Byte(b'\x1B'),
Token::Char('\x0C'), Token::Byte(b'\x0C'),
Token::Char('\x0B'), Token::Byte(b'\x0B'),
Token::Directive { Token::Directive {
flag: Flags { flag: Flags {
sign: true, sign: true,
@ -1070,11 +1208,11 @@ mod tests {
precision: None, precision: None,
format: 'w', format: 'w',
}, },
Token::Char('\x12'), Token::Byte(b'\x12'),
Token::Char('w'), Token::Byte(b'w'),
Token::Char('Z'), Token::Byte(b'Z'),
Token::Char('J'), Token::Byte(b'J'),
Token::Char('\n'), Token::Byte(b'\n'),
]; ];
assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap()); assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap());
} }

View file

@ -242,7 +242,7 @@ fn test_multi_files() {
#[test] #[test]
fn test_printf() { fn test_printf() {
let args = [ let args = [
"--printf=123%-# 15q\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.23m\\x12\\167\\132\\112\\n", "--printf=123%-# 15q\\r\\\"\\\\\\a\\b\\x1B\\f\\x0B%+020.23m\\x12\\167\\132\\112\\n",
"/", "/",
]; ];
let ts = TestScenario::new(util_name!()); let ts = TestScenario::new(util_name!());
@ -256,11 +256,10 @@ fn test_pipe_fifo() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
at.mkfifo("FIFO"); at.mkfifo("FIFO");
ucmd.arg("FIFO") ucmd.arg("FIFO")
.run() .succeeds()
.no_stderr() .no_stderr()
.stdout_contains("fifo") .stdout_contains("fifo")
.stdout_contains("File: FIFO") .stdout_contains("File: FIFO");
.succeeded();
} }
#[test] #[test]
@ -275,19 +274,17 @@ fn test_stdin_pipe_fifo1() {
new_ucmd!() new_ucmd!()
.arg("-") .arg("-")
.set_stdin(std::process::Stdio::piped()) .set_stdin(std::process::Stdio::piped())
.run() .succeeds()
.no_stderr() .no_stderr()
.stdout_contains("fifo") .stdout_contains("fifo")
.stdout_contains("File: -") .stdout_contains("File: -");
.succeeded();
new_ucmd!() new_ucmd!()
.args(&["-L", "-"]) .args(&["-L", "-"])
.set_stdin(std::process::Stdio::piped()) .set_stdin(std::process::Stdio::piped())
.run() .succeeds()
.no_stderr() .no_stderr()
.stdout_contains("fifo") .stdout_contains("fifo")
.stdout_contains("File: -") .stdout_contains("File: -");
.succeeded();
} }
#[test] #[test]
@ -299,11 +296,10 @@ fn test_stdin_pipe_fifo2() {
new_ucmd!() new_ucmd!()
.arg("-") .arg("-")
.set_stdin(std::process::Stdio::null()) .set_stdin(std::process::Stdio::null())
.run() .succeeds()
.no_stderr() .no_stderr()
.stdout_contains("character special file") .stdout_contains("character special file")
.stdout_contains("File: -") .stdout_contains("File: -");
.succeeded();
} }
#[test] #[test]
@ -339,11 +335,10 @@ fn test_stdin_redirect() {
ts.ucmd() ts.ucmd()
.arg("-") .arg("-")
.set_stdin(std::fs::File::open(at.plus("f")).unwrap()) .set_stdin(std::fs::File::open(at.plus("f")).unwrap())
.run() .succeeds()
.no_stderr() .no_stderr()
.stdout_contains("regular empty file") .stdout_contains("regular empty file")
.stdout_contains("File: -") .stdout_contains("File: -");
.succeeded();
} }
#[test] #[test]
@ -352,3 +347,76 @@ fn test_without_argument() {
.fails() .fails()
.stderr_contains("missing operand\nTry 'stat --help' for more information."); .stderr_contains("missing operand\nTry 'stat --help' for more information.");
} }
#[test]
fn test_quoting_style_locale() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("'");
ts.ucmd()
.env("QUOTING_STYLE", "locale")
.args(&["-c", "%N", "'"])
.succeeds()
.stdout_only("'\\''\n");
ts.ucmd()
.args(&["-c", "%N", "'"])
.succeeds()
.stdout_only("\"'\"\n");
}
#[test]
fn test_printf_octal_1() {
let ts = TestScenario::new(util_name!());
let expected_stdout = vec![0x0A, 0xFF]; // Newline + byte 255
ts.ucmd()
.args(&["--printf=\\012\\377", "."])
.succeeds()
.stdout_is_bytes(expected_stdout);
}
#[test]
fn test_printf_octal_2() {
let ts = TestScenario::new(util_name!());
let expected_stdout = vec![b'.', 0x0A, b'a', 0xFF, b'b'];
ts.ucmd()
.args(&["--printf=.\\012a\\377b", "."])
.succeeds()
.stdout_is_bytes(expected_stdout);
}
#[test]
fn test_printf_incomplete_hex() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["--printf=\\x", "."])
.succeeds()
.stderr_contains("warning: incomplete hex escape");
}
#[test]
fn test_printf_bel_etc() {
let ts = TestScenario::new(util_name!());
let expected_stdout = vec![0x07, 0x08, 0x0C, 0x0A, 0x0D, 0x09]; // BEL, BS, FF, LF, CR, TAB
ts.ucmd()
.args(&["--printf=\\a\\b\\f\\n\\r\\t", "."])
.succeeds()
.stdout_is_bytes(expected_stdout);
}
#[test]
fn test_printf_invalid_directive() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["--printf=%9", "."])
.fails()
.code_is(1)
.stderr_contains("'%9': invalid directive");
ts.ucmd()
.args(&["--printf=%9%", "."])
.fails()
.code_is(1)
.stderr_contains("'%9%': invalid directive");
}

View file

@ -204,6 +204,9 @@ sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not
# Our message is a bit better # Our message is a bit better
sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh
# Our message is better
sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl
sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh
sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh
sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh