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:
parent
22f3358d47
commit
c60203ddd3
3 changed files with 440 additions and 231 deletions
|
@ -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, "ing_style);
|
||||||
|
match fs::read_link(file) {
|
||||||
|
Ok(dst) => {
|
||||||
|
let quoted_dst = quote_file_name(&dst.to_string_lossy(), "ing_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;
|
||||||
Ok(Token::Directive {
|
|
||||||
width,
|
// 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,
|
flag,
|
||||||
|
width,
|
||||||
|
precision,
|
||||||
|
format: specifier.chars().next().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Token::Directive {
|
||||||
|
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
|
||||||
|
'b' => Token::Byte(0x08), // Backspace
|
||||||
|
'f' => Token::Byte(0x0C), // Form feed
|
||||||
|
'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) {
|
if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) {
|
||||||
*i += offset;
|
*i += offset;
|
||||||
Token::Char(c)
|
Token::Byte(c as u8)
|
||||||
} else {
|
} else {
|
||||||
show_warning!("unrecognized escape '\\x'");
|
show_warning!("unrecognized escape '\\x'");
|
||||||
Token::Char('x')
|
Token::Byte(b'x')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
show_warning!("incomplete hex escape '\\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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue