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

Merge branch 'fix_getgrouplist' into id_zero_2351

This commit is contained in:
Jan Scheer 2021-06-16 19:20:34 +02:00
commit 88367c6fb4
18 changed files with 701 additions and 647 deletions

1
Cargo.lock generated
View file

@ -2597,6 +2597,7 @@ version = "0.0.6"
dependencies = [ dependencies = [
"clap", "clap",
"libc", "libc",
"nix 0.20.0",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]

View file

@ -28,6 +28,7 @@ use std::os::windows::io::AsRawHandle;
#[cfg(windows)] #[cfg(windows)]
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH}; use std::time::{Duration, UNIX_EPOCH};
use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
@ -56,6 +57,7 @@ mod options {
pub const BLOCK_SIZE_1M: &str = "m"; pub const BLOCK_SIZE_1M: &str = "m";
pub const SEPARATE_DIRS: &str = "S"; pub const SEPARATE_DIRS: &str = "S";
pub const SUMMARIZE: &str = "s"; pub const SUMMARIZE: &str = "s";
pub const THRESHOLD: &str = "threshold";
pub const SI: &str = "si"; pub const SI: &str = "si";
pub const TIME: &str = "time"; pub const TIME: &str = "time";
pub const TIME_STYLE: &str = "time-style"; pub const TIME_STYLE: &str = "time-style";
@ -510,6 +512,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(options::ONE_FILE_SYSTEM) .long(options::ONE_FILE_SYSTEM)
.help("skip directories on different file systems") .help("skip directories on different file systems")
) )
.arg(
Arg::with_name(options::THRESHOLD)
.short("t")
.long(options::THRESHOLD)
.alias("th")
.value_name("SIZE")
.number_of_values(1)
.allow_hyphen_values(true)
.help("exclude entries smaller than SIZE if positive, \
or entries greater than SIZE if negative")
)
// .arg( // .arg(
// Arg::with_name("") // Arg::with_name("")
// .short("x") // .short("x")
@ -586,6 +599,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let block_size = u64::try_from(read_block_size(matches.value_of(options::BLOCK_SIZE))).unwrap(); let block_size = u64::try_from(read_block_size(matches.value_of(options::BLOCK_SIZE))).unwrap();
let threshold = matches.value_of(options::THRESHOLD).map(|s| {
Threshold::from_str(s)
.unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::THRESHOLD)))
});
let multiplier: u64 = if matches.is_present(options::SI) { let multiplier: u64 = if matches.is_present(options::SI) {
1000 1000
} else { } else {
@ -654,6 +672,11 @@ Try '{} --help' for more information.",
// See: http://linux.die.net/man/2/stat // See: http://linux.die.net/man/2/stat
stat.blocks * 512 stat.blocks * 512
}; };
if threshold.map_or(false, |threshold| threshold.should_exclude(size)) {
continue;
}
if matches.is_present(options::TIME) { if matches.is_present(options::TIME) {
let tm = { let tm = {
let secs = { let secs = {
@ -720,6 +743,37 @@ Try '{} --help' for more information.",
0 0
} }
#[derive(Clone, Copy)]
enum Threshold {
Lower(u64),
Upper(u64),
}
impl FromStr for Threshold {
type Err = ParseSizeError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let offset = if s.starts_with(&['-', '+'][..]) { 1 } else { 0 };
let size = u64::try_from(parse_size(&s[offset..])?).unwrap();
if s.starts_with('-') {
Ok(Threshold::Upper(size))
} else {
Ok(Threshold::Lower(size))
}
}
}
impl Threshold {
fn should_exclude(&self, size: u64) -> bool {
match *self {
Threshold::Upper(threshold) => size > threshold,
Threshold::Lower(threshold) => size < threshold,
}
}
}
fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String {
// NOTE: // NOTE:
// GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection // GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection

View file

@ -129,33 +129,24 @@ fn handle_obsolete(mut args: Vec<String>) -> (Vec<String>, Option<String>) {
} }
fn table() { fn table() {
let mut name_width = 0; let name_width = ALL_SIGNALS.iter().map(|n| n.len()).max().unwrap();
/* Compute the maximum width of a signal name. */
for s in &ALL_SIGNALS {
if s.name.len() > name_width {
name_width = s.name.len()
}
}
for (idx, signal) in ALL_SIGNALS.iter().enumerate() { for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
print!("{0: >#2} {1: <#8}", idx + 1, signal.name); print!("{0: >#2} {1: <#2$}", idx, signal, name_width + 2);
//TODO: obtain max signal width here
if (idx + 1) % 7 == 0 { if (idx + 1) % 7 == 0 {
println!(); println!();
} }
} }
println!()
} }
fn print_signal(signal_name_or_value: &str) { fn print_signal(signal_name_or_value: &str) {
for signal in &ALL_SIGNALS { for (value, &signal) in ALL_SIGNALS.iter().enumerate() {
if signal.name == signal_name_or_value if signal == signal_name_or_value || (format!("SIG{}", signal)) == signal_name_or_value {
|| (format!("SIG{}", signal.name)) == signal_name_or_value println!("{}", value);
{
println!("{}", signal.value);
exit!(EXIT_OK as i32) exit!(EXIT_OK as i32)
} else if signal_name_or_value == signal.value.to_string() { } else if signal_name_or_value == value.to_string() {
println!("{}", signal.name); println!("{}", signal);
exit!(EXIT_OK as i32) exit!(EXIT_OK as i32)
} }
} }
@ -165,8 +156,8 @@ fn print_signal(signal_name_or_value: &str) {
fn print_signals() { fn print_signals() {
let mut pos = 0; let mut pos = 0;
for (idx, signal) in ALL_SIGNALS.iter().enumerate() { for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
pos += signal.name.len(); pos += signal.len();
print!("{}", signal.name); print!("{}", signal);
if idx > 0 && pos > 73 { if idx > 0 && pos > 73 {
println!(); println!();
pos = 0; pos = 0;

View file

@ -46,8 +46,8 @@ use unicode_width::UnicodeWidthStr;
use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static NAME: &str = "sort"; const NAME: &str = "sort";
static ABOUT: &str = "Display sorted concatenation of all FILE(s)."; const ABOUT: &str = "Display sorted concatenation of all FILE(s).";
const LONG_HELP_KEYS: &str = "The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]. const LONG_HELP_KEYS: &str = "The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS].
@ -59,49 +59,59 @@ If CHAR is set 0, it means the end of the field. CHAR defaults to 1 for the star
Valid options are: MbdfhnRrV. They override the global options for this key."; Valid options are: MbdfhnRrV. They override the global options for this key.";
static OPT_HUMAN_NUMERIC_SORT: &str = "human-numeric-sort"; mod options {
static OPT_MONTH_SORT: &str = "month-sort"; pub mod modes {
static OPT_NUMERIC_SORT: &str = "numeric-sort"; pub const SORT: &str = "sort";
static OPT_GENERAL_NUMERIC_SORT: &str = "general-numeric-sort";
static OPT_VERSION_SORT: &str = "version-sort";
static OPT_SORT: &str = "sort"; pub const HUMAN_NUMERIC: &str = "human-numeric-sort";
pub const MONTH: &str = "month-sort";
pub const NUMERIC: &str = "numeric-sort";
pub const GENERAL_NUMERIC: &str = "general-numeric-sort";
pub const VERSION: &str = "version-sort";
pub const RANDOM: &str = "random-sort";
static ALL_SORT_MODES: &[&str] = &[ pub const ALL_SORT_MODES: [&str; 6] = [
OPT_GENERAL_NUMERIC_SORT, GENERAL_NUMERIC,
OPT_HUMAN_NUMERIC_SORT, HUMAN_NUMERIC,
OPT_MONTH_SORT, MONTH,
OPT_NUMERIC_SORT, NUMERIC,
OPT_VERSION_SORT, VERSION,
OPT_RANDOM, RANDOM,
]; ];
}
static OPT_DICTIONARY_ORDER: &str = "dictionary-order"; pub mod check {
static OPT_MERGE: &str = "merge"; pub const CHECK: &str = "check";
static OPT_CHECK: &str = "check"; pub const CHECK_SILENT: &str = "check-silent";
static OPT_CHECK_SILENT: &str = "check-silent"; pub const SILENT: &str = "silent";
static OPT_DEBUG: &str = "debug"; pub const QUIET: &str = "quiet";
static OPT_IGNORE_CASE: &str = "ignore-case"; pub const DIAGNOSE_FIRST: &str = "diagnose-first";
static OPT_IGNORE_BLANKS: &str = "ignore-blanks"; }
static OPT_IGNORE_NONPRINTING: &str = "ignore-nonprinting";
static OPT_OUTPUT: &str = "output";
static OPT_REVERSE: &str = "reverse";
static OPT_STABLE: &str = "stable";
static OPT_UNIQUE: &str = "unique";
static OPT_KEY: &str = "key";
static OPT_SEPARATOR: &str = "field-separator";
static OPT_RANDOM: &str = "random-sort";
static OPT_ZERO_TERMINATED: &str = "zero-terminated";
static OPT_PARALLEL: &str = "parallel";
static OPT_FILES0_FROM: &str = "files0-from";
static OPT_BUF_SIZE: &str = "buffer-size";
static OPT_TMP_DIR: &str = "temporary-directory";
static OPT_COMPRESS_PROG: &str = "compress-program";
static OPT_BATCH_SIZE: &str = "batch-size";
static ARG_FILES: &str = "files"; pub const DICTIONARY_ORDER: &str = "dictionary-order";
pub const MERGE: &str = "merge";
pub const DEBUG: &str = "debug";
pub const IGNORE_CASE: &str = "ignore-case";
pub const IGNORE_LEADING_BLANKS: &str = "ignore-leading-blanks";
pub const IGNORE_NONPRINTING: &str = "ignore-nonprinting";
pub const OUTPUT: &str = "output";
pub const REVERSE: &str = "reverse";
pub const STABLE: &str = "stable";
pub const UNIQUE: &str = "unique";
pub const KEY: &str = "key";
pub const SEPARATOR: &str = "field-separator";
pub const ZERO_TERMINATED: &str = "zero-terminated";
pub const PARALLEL: &str = "parallel";
pub const FILES0_FROM: &str = "files0-from";
pub const BUF_SIZE: &str = "buffer-size";
pub const TMP_DIR: &str = "temporary-directory";
pub const COMPRESS_PROG: &str = "compress-program";
pub const BATCH_SIZE: &str = "batch-size";
static DECIMAL_PT: char = '.'; pub const FILES: &str = "files";
}
const DECIMAL_PT: char = '.';
const NEGATIVE: char = '-'; const NEGATIVE: char = '-';
const POSITIVE: char = '+'; const POSITIVE: char = '+';
@ -109,7 +119,7 @@ const POSITIVE: char = '+';
// Choosing a higher buffer size does not result in performance improvements // Choosing a higher buffer size does not result in performance improvements
// (at least not on my machine). TODO: In the future, we should also take the amount of // (at least not on my machine). TODO: In the future, we should also take the amount of
// available memory into consideration, instead of relying on this constant only. // available memory into consideration, instead of relying on this constant only.
static DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)] #[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)]
enum SortMode { enum SortMode {
@ -140,7 +150,7 @@ impl SortMode {
pub struct GlobalSettings { pub struct GlobalSettings {
mode: SortMode, mode: SortMode,
debug: bool, debug: bool,
ignore_blanks: bool, ignore_leading_blanks: bool,
ignore_case: bool, ignore_case: bool,
dictionary_order: bool, dictionary_order: bool,
ignore_non_printing: bool, ignore_non_printing: bool,
@ -207,7 +217,7 @@ impl Default for GlobalSettings {
GlobalSettings { GlobalSettings {
mode: SortMode::Default, mode: SortMode::Default,
debug: false, debug: false,
ignore_blanks: false, ignore_leading_blanks: false,
ignore_case: false, ignore_case: false,
dictionary_order: false, dictionary_order: false,
ignore_non_printing: false, ignore_non_printing: false,
@ -298,7 +308,7 @@ impl From<&GlobalSettings> for KeySettings {
fn from(settings: &GlobalSettings) -> Self { fn from(settings: &GlobalSettings) -> Self {
Self { Self {
mode: settings.mode, mode: settings.mode,
ignore_blanks: settings.ignore_blanks, ignore_blanks: settings.ignore_leading_blanks,
ignore_case: settings.ignore_case, ignore_case: settings.ignore_case,
ignore_non_printing: settings.ignore_non_printing, ignore_non_printing: settings.ignore_non_printing,
reverse: settings.reverse, reverse: settings.reverse,
@ -505,7 +515,7 @@ impl<'a> Line<'a> {
&& !settings.stable && !settings.stable
&& !settings.unique && !settings.unique
&& (settings.dictionary_order && (settings.dictionary_order
|| settings.ignore_blanks || settings.ignore_leading_blanks
|| settings.ignore_case || settings.ignore_case
|| settings.ignore_non_printing || settings.ignore_non_printing
|| settings.mode != SortMode::Default || settings.mode != SortMode::Default
@ -669,9 +679,11 @@ impl FieldSelector {
// This would be ideal for a try block, I think. In the meantime this closure allows // This would be ideal for a try block, I think. In the meantime this closure allows
// to use the `?` operator here. // to use the `?` operator here.
Self::new( Self::new(
KeyPosition::new(from, 1, global_settings.ignore_blanks)?, KeyPosition::new(from, 1, global_settings.ignore_leading_blanks)?,
to.map(|(to, _)| KeyPosition::new(to, 0, global_settings.ignore_blanks)) to.map(|(to, _)| {
.transpose()?, KeyPosition::new(to, 0, global_settings.ignore_leading_blanks)
})
.transpose()?,
KeySettings::from(global_settings), KeySettings::from(global_settings),
) )
})() })()
@ -887,9 +899,10 @@ With no FILE, or when FILE is -, read standard input.",
) )
} }
/// Creates an `Arg` that conflicts with all other sort modes.
fn make_sort_mode_arg<'a, 'b>(mode: &'a str, short: &'b str, help: &'b str) -> Arg<'a, 'b> { fn make_sort_mode_arg<'a, 'b>(mode: &'a str, short: &'b str, help: &'b str) -> Arg<'a, 'b> {
let mut arg = Arg::with_name(mode).short(short).long(mode).help(help); let mut arg = Arg::with_name(mode).short(short).long(mode).help(help);
for possible_mode in ALL_SORT_MODES { for possible_mode in &options::modes::ALL_SORT_MODES {
if *possible_mode != mode { if *possible_mode != mode {
arg = arg.conflicts_with(possible_mode); arg = arg.conflicts_with(possible_mode);
} }
@ -909,8 +922,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.about(ABOUT) .about(ABOUT)
.usage(&usage[..]) .usage(&usage[..])
.arg( .arg(
Arg::with_name(OPT_SORT) Arg::with_name(options::modes::SORT)
.long(OPT_SORT) .long(options::modes::SORT)
.takes_value(true) .takes_value(true)
.possible_values( .possible_values(
&[ &[
@ -922,199 +935,221 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
"random", "random",
] ]
) )
.conflicts_with_all(ALL_SORT_MODES) .conflicts_with_all(&options::modes::ALL_SORT_MODES)
) )
.arg( .arg(
make_sort_mode_arg( make_sort_mode_arg(
OPT_HUMAN_NUMERIC_SORT, options::modes::HUMAN_NUMERIC,
"h", "h",
"compare according to human readable sizes, eg 1M > 100k" "compare according to human readable sizes, eg 1M > 100k"
), ),
) )
.arg( .arg(
make_sort_mode_arg( make_sort_mode_arg(
OPT_MONTH_SORT, options::modes::MONTH,
"M", "M",
"compare according to month name abbreviation" "compare according to month name abbreviation"
), ),
) )
.arg( .arg(
make_sort_mode_arg( make_sort_mode_arg(
OPT_NUMERIC_SORT, options::modes::NUMERIC,
"n", "n",
"compare according to string numerical value" "compare according to string numerical value"
), ),
) )
.arg( .arg(
make_sort_mode_arg( make_sort_mode_arg(
OPT_GENERAL_NUMERIC_SORT, options::modes::GENERAL_NUMERIC,
"g", "g",
"compare according to string general numerical value" "compare according to string general numerical value"
), ),
) )
.arg( .arg(
make_sort_mode_arg( make_sort_mode_arg(
OPT_VERSION_SORT, options::modes::VERSION,
"V", "V",
"Sort by SemVer version number, eg 1.12.2 > 1.1.2", "Sort by SemVer version number, eg 1.12.2 > 1.1.2",
), ),
) )
.arg( .arg(
make_sort_mode_arg( make_sort_mode_arg(
OPT_RANDOM, options::modes::RANDOM,
"R", "R",
"shuffle in random order", "shuffle in random order",
), ),
) )
.arg( .arg(
Arg::with_name(OPT_DICTIONARY_ORDER) Arg::with_name(options::DICTIONARY_ORDER)
.short("d") .short("d")
.long(OPT_DICTIONARY_ORDER) .long(options::DICTIONARY_ORDER)
.help("consider only blanks and alphanumeric characters") .help("consider only blanks and alphanumeric characters")
.conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]), .conflicts_with_all(
&[
options::modes::NUMERIC,
options::modes::GENERAL_NUMERIC,
options::modes::HUMAN_NUMERIC,
options::modes::MONTH,
]
),
) )
.arg( .arg(
Arg::with_name(OPT_MERGE) Arg::with_name(options::MERGE)
.short("m") .short("m")
.long(OPT_MERGE) .long(options::MERGE)
.help("merge already sorted files; do not sort"), .help("merge already sorted files; do not sort"),
) )
.arg( .arg(
Arg::with_name(OPT_CHECK) Arg::with_name(options::check::CHECK)
.short("c") .short("c")
.long(OPT_CHECK) .long(options::check::CHECK)
.takes_value(true)
.require_equals(true)
.min_values(0)
.possible_values(&[
options::check::SILENT,
options::check::QUIET,
options::check::DIAGNOSE_FIRST,
])
.help("check for sorted input; do not sort"), .help("check for sorted input; do not sort"),
) )
.arg( .arg(
Arg::with_name(OPT_CHECK_SILENT) Arg::with_name(options::check::CHECK_SILENT)
.short("C") .short("C")
.long(OPT_CHECK_SILENT) .long(options::check::CHECK_SILENT)
.help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."), .help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."),
) )
.arg( .arg(
Arg::with_name(OPT_IGNORE_CASE) Arg::with_name(options::IGNORE_CASE)
.short("f") .short("f")
.long(OPT_IGNORE_CASE) .long(options::IGNORE_CASE)
.help("fold lower case to upper case characters"), .help("fold lower case to upper case characters"),
) )
.arg( .arg(
Arg::with_name(OPT_IGNORE_NONPRINTING) Arg::with_name(options::IGNORE_NONPRINTING)
.short("i") .short("i")
.long(OPT_IGNORE_NONPRINTING) .long(options::IGNORE_NONPRINTING)
.help("ignore nonprinting characters") .help("ignore nonprinting characters")
.conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]), .conflicts_with_all(
&[
options::modes::NUMERIC,
options::modes::GENERAL_NUMERIC,
options::modes::HUMAN_NUMERIC,
options::modes::MONTH
]
),
) )
.arg( .arg(
Arg::with_name(OPT_IGNORE_BLANKS) Arg::with_name(options::IGNORE_LEADING_BLANKS)
.short("b") .short("b")
.long(OPT_IGNORE_BLANKS) .long(options::IGNORE_LEADING_BLANKS)
.help("ignore leading blanks when finding sort keys in each line"), .help("ignore leading blanks when finding sort keys in each line"),
) )
.arg( .arg(
Arg::with_name(OPT_OUTPUT) Arg::with_name(options::OUTPUT)
.short("o") .short("o")
.long(OPT_OUTPUT) .long(options::OUTPUT)
.help("write output to FILENAME instead of stdout") .help("write output to FILENAME instead of stdout")
.takes_value(true) .takes_value(true)
.value_name("FILENAME"), .value_name("FILENAME"),
) )
.arg( .arg(
Arg::with_name(OPT_REVERSE) Arg::with_name(options::REVERSE)
.short("r") .short("r")
.long(OPT_REVERSE) .long(options::REVERSE)
.help("reverse the output"), .help("reverse the output"),
) )
.arg( .arg(
Arg::with_name(OPT_STABLE) Arg::with_name(options::STABLE)
.short("s") .short("s")
.long(OPT_STABLE) .long(options::STABLE)
.help("stabilize sort by disabling last-resort comparison"), .help("stabilize sort by disabling last-resort comparison"),
) )
.arg( .arg(
Arg::with_name(OPT_UNIQUE) Arg::with_name(options::UNIQUE)
.short("u") .short("u")
.long(OPT_UNIQUE) .long(options::UNIQUE)
.help("output only the first of an equal run"), .help("output only the first of an equal run"),
) )
.arg( .arg(
Arg::with_name(OPT_KEY) Arg::with_name(options::KEY)
.short("k") .short("k")
.long(OPT_KEY) .long(options::KEY)
.help("sort by a key") .help("sort by a key")
.long_help(LONG_HELP_KEYS) .long_help(LONG_HELP_KEYS)
.multiple(true) .multiple(true)
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name(OPT_SEPARATOR) Arg::with_name(options::SEPARATOR)
.short("t") .short("t")
.long(OPT_SEPARATOR) .long(options::SEPARATOR)
.help("custom separator for -k") .help("custom separator for -k")
.takes_value(true)) .takes_value(true))
.arg( .arg(
Arg::with_name(OPT_ZERO_TERMINATED) Arg::with_name(options::ZERO_TERMINATED)
.short("z") .short("z")
.long(OPT_ZERO_TERMINATED) .long(options::ZERO_TERMINATED)
.help("line delimiter is NUL, not newline"), .help("line delimiter is NUL, not newline"),
) )
.arg( .arg(
Arg::with_name(OPT_PARALLEL) Arg::with_name(options::PARALLEL)
.long(OPT_PARALLEL) .long(options::PARALLEL)
.help("change the number of threads running concurrently to NUM_THREADS") .help("change the number of threads running concurrently to NUM_THREADS")
.takes_value(true) .takes_value(true)
.value_name("NUM_THREADS"), .value_name("NUM_THREADS"),
) )
.arg( .arg(
Arg::with_name(OPT_BUF_SIZE) Arg::with_name(options::BUF_SIZE)
.short("S") .short("S")
.long(OPT_BUF_SIZE) .long(options::BUF_SIZE)
.help("sets the maximum SIZE of each segment in number of sorted items") .help("sets the maximum SIZE of each segment in number of sorted items")
.takes_value(true) .takes_value(true)
.value_name("SIZE"), .value_name("SIZE"),
) )
.arg( .arg(
Arg::with_name(OPT_TMP_DIR) Arg::with_name(options::TMP_DIR)
.short("T") .short("T")
.long(OPT_TMP_DIR) .long(options::TMP_DIR)
.help("use DIR for temporaries, not $TMPDIR or /tmp") .help("use DIR for temporaries, not $TMPDIR or /tmp")
.takes_value(true) .takes_value(true)
.value_name("DIR"), .value_name("DIR"),
) )
.arg( .arg(
Arg::with_name(OPT_COMPRESS_PROG) Arg::with_name(options::COMPRESS_PROG)
.long(OPT_COMPRESS_PROG) .long(options::COMPRESS_PROG)
.help("compress temporary files with PROG, decompress with PROG -d") .help("compress temporary files with PROG, decompress with PROG -d")
.long_help("PROG has to take input from stdin and output to stdout") .long_help("PROG has to take input from stdin and output to stdout")
.value_name("PROG") .value_name("PROG")
) )
.arg( .arg(
Arg::with_name(OPT_BATCH_SIZE) Arg::with_name(options::BATCH_SIZE)
.long(OPT_BATCH_SIZE) .long(options::BATCH_SIZE)
.help("Merge at most N_MERGE inputs at once.") .help("Merge at most N_MERGE inputs at once.")
.value_name("N_MERGE") .value_name("N_MERGE")
) )
.arg( .arg(
Arg::with_name(OPT_FILES0_FROM) Arg::with_name(options::FILES0_FROM)
.long(OPT_FILES0_FROM) .long(options::FILES0_FROM)
.help("read input from the files specified by NUL-terminated NUL_FILES") .help("read input from the files specified by NUL-terminated NUL_FILES")
.takes_value(true) .takes_value(true)
.value_name("NUL_FILES") .value_name("NUL_FILES")
.multiple(true), .multiple(true),
) )
.arg( .arg(
Arg::with_name(OPT_DEBUG) Arg::with_name(options::DEBUG)
.long(OPT_DEBUG) .long(options::DEBUG)
.help("underline the parts of the line that are actually used for sorting"), .help("underline the parts of the line that are actually used for sorting"),
) )
.arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) .arg(Arg::with_name(options::FILES).multiple(true).takes_value(true))
.get_matches_from(args); .get_matches_from(args);
settings.debug = matches.is_present(OPT_DEBUG); settings.debug = matches.is_present(options::DEBUG);
// check whether user specified a zero terminated list of files for input, otherwise read files from args // check whether user specified a zero terminated list of files for input, otherwise read files from args
let mut files: Vec<String> = if matches.is_present(OPT_FILES0_FROM) { let mut files: Vec<String> = if matches.is_present(options::FILES0_FROM) {
let files0_from: Vec<String> = matches let files0_from: Vec<String> = matches
.values_of(OPT_FILES0_FROM) .values_of(options::FILES0_FROM)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default(); .unwrap_or_default();
@ -1133,82 +1168,93 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
files files
} else { } else {
matches matches
.values_of(ARG_FILES) .values_of(options::FILES)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default() .unwrap_or_default()
}; };
settings.mode = if matches.is_present(OPT_HUMAN_NUMERIC_SORT) settings.mode = if matches.is_present(options::modes::HUMAN_NUMERIC)
|| matches.value_of(OPT_SORT) == Some("human-numeric") || matches.value_of(options::modes::SORT) == Some("human-numeric")
{ {
SortMode::HumanNumeric SortMode::HumanNumeric
} else if matches.is_present(OPT_MONTH_SORT) || matches.value_of(OPT_SORT) == Some("month") { } else if matches.is_present(options::modes::MONTH)
|| matches.value_of(options::modes::SORT) == Some("month")
{
SortMode::Month SortMode::Month
} else if matches.is_present(OPT_GENERAL_NUMERIC_SORT) } else if matches.is_present(options::modes::GENERAL_NUMERIC)
|| matches.value_of(OPT_SORT) == Some("general-numeric") || matches.value_of(options::modes::SORT) == Some("general-numeric")
{ {
SortMode::GeneralNumeric SortMode::GeneralNumeric
} else if matches.is_present(OPT_NUMERIC_SORT) || matches.value_of(OPT_SORT) == Some("numeric") } else if matches.is_present(options::modes::NUMERIC)
|| matches.value_of(options::modes::SORT) == Some("numeric")
{ {
SortMode::Numeric SortMode::Numeric
} else if matches.is_present(OPT_VERSION_SORT) || matches.value_of(OPT_SORT) == Some("version") } else if matches.is_present(options::modes::VERSION)
|| matches.value_of(options::modes::SORT) == Some("version")
{ {
SortMode::Version SortMode::Version
} else if matches.is_present(OPT_RANDOM) || matches.value_of(OPT_SORT) == Some("random") { } else if matches.is_present(options::modes::RANDOM)
|| matches.value_of(options::modes::SORT) == Some("random")
{
settings.salt = get_rand_string(); settings.salt = get_rand_string();
SortMode::Random SortMode::Random
} else { } else {
SortMode::Default SortMode::Default
}; };
settings.dictionary_order = matches.is_present(OPT_DICTIONARY_ORDER); settings.dictionary_order = matches.is_present(options::DICTIONARY_ORDER);
settings.ignore_non_printing = matches.is_present(OPT_IGNORE_NONPRINTING); settings.ignore_non_printing = matches.is_present(options::IGNORE_NONPRINTING);
if matches.is_present(OPT_PARALLEL) { if matches.is_present(options::PARALLEL) {
// "0" is default - threads = num of cores // "0" is default - threads = num of cores
settings.threads = matches settings.threads = matches
.value_of(OPT_PARALLEL) .value_of(options::PARALLEL)
.map(String::from) .map(String::from)
.unwrap_or_else(|| "0".to_string()); .unwrap_or_else(|| "0".to_string());
env::set_var("RAYON_NUM_THREADS", &settings.threads); env::set_var("RAYON_NUM_THREADS", &settings.threads);
} }
settings.buffer_size = matches settings.buffer_size = matches
.value_of(OPT_BUF_SIZE) .value_of(options::BUF_SIZE)
.map_or(DEFAULT_BUF_SIZE, |s| { .map_or(DEFAULT_BUF_SIZE, |s| {
GlobalSettings::parse_byte_count(s) GlobalSettings::parse_byte_count(s)
.unwrap_or_else(|e| crash!(2, "{}", format_error_message(e, s, OPT_BUF_SIZE))) .unwrap_or_else(|e| crash!(2, "{}", format_error_message(e, s, options::BUF_SIZE)))
}); });
settings.tmp_dir = matches settings.tmp_dir = matches
.value_of(OPT_TMP_DIR) .value_of(options::TMP_DIR)
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(env::temp_dir); .unwrap_or_else(env::temp_dir);
settings.compress_prog = matches.value_of(OPT_COMPRESS_PROG).map(String::from); settings.compress_prog = matches.value_of(options::COMPRESS_PROG).map(String::from);
if let Some(n_merge) = matches.value_of(OPT_BATCH_SIZE) { if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) {
settings.merge_batch_size = n_merge settings.merge_batch_size = n_merge
.parse() .parse()
.unwrap_or_else(|_| crash!(2, "invalid --batch-size argument '{}'", n_merge)); .unwrap_or_else(|_| crash!(2, "invalid --batch-size argument '{}'", n_merge));
} }
settings.zero_terminated = matches.is_present(OPT_ZERO_TERMINATED); settings.zero_terminated = matches.is_present(options::ZERO_TERMINATED);
settings.merge = matches.is_present(OPT_MERGE); settings.merge = matches.is_present(options::MERGE);
settings.check = matches.is_present(OPT_CHECK); settings.check = matches.is_present(options::check::CHECK);
if matches.is_present(OPT_CHECK_SILENT) { if matches.is_present(options::check::CHECK_SILENT)
settings.check_silent = matches.is_present(OPT_CHECK_SILENT); || matches!(
matches.value_of(options::check::CHECK),
Some(options::check::SILENT) | Some(options::check::QUIET)
)
{
settings.check_silent = true;
settings.check = true; settings.check = true;
}; };
settings.ignore_case = matches.is_present(OPT_IGNORE_CASE); settings.ignore_case = matches.is_present(options::IGNORE_CASE);
settings.ignore_blanks = matches.is_present(OPT_IGNORE_BLANKS); settings.ignore_leading_blanks = matches.is_present(options::IGNORE_LEADING_BLANKS);
settings.output_file = matches.value_of(OPT_OUTPUT).map(String::from); settings.output_file = matches.value_of(options::OUTPUT).map(String::from);
settings.reverse = matches.is_present(OPT_REVERSE); settings.reverse = matches.is_present(options::REVERSE);
settings.stable = matches.is_present(OPT_STABLE); settings.stable = matches.is_present(options::STABLE);
settings.unique = matches.is_present(OPT_UNIQUE); settings.unique = matches.is_present(options::UNIQUE);
if files.is_empty() { if files.is_empty() {
/* if no file, default to stdin */ /* if no file, default to stdin */
@ -1217,7 +1263,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
crash!(1, "extra operand `{}' not allowed with -c", files[1]) crash!(1, "extra operand `{}' not allowed with -c", files[1])
} }
if let Some(arg) = matches.args.get(OPT_SEPARATOR) { if let Some(arg) = matches.args.get(options::SEPARATOR) {
let separator = arg.vals[0].to_string_lossy(); let separator = arg.vals[0].to_string_lossy();
let separator = separator; let separator = separator;
if separator.len() != 1 { if separator.len() != 1 {
@ -1226,15 +1272,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
settings.separator = Some(separator.chars().next().unwrap()) settings.separator = Some(separator.chars().next().unwrap())
} }
if matches.is_present(OPT_KEY) { if let Some(values) = matches.values_of(options::KEY) {
for key in &matches.args[OPT_KEY].vals { for value in values {
settings settings
.selectors .selectors
.push(FieldSelector::parse(&key.to_string_lossy(), &settings)); .push(FieldSelector::parse(value, &settings));
} }
} }
if !matches.is_present(OPT_KEY) { if !matches.is_present(options::KEY) {
// add a default selector matching the whole line // add a default selector matching the whole line
let key_settings = KeySettings::from(&settings); let key_settings = KeySettings::from(&settings);
settings.selectors.push( settings.selectors.push(

View file

@ -6,7 +6,7 @@
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore (vars) FiletestOp StrlenOp // spell-checker:ignore (vars) egid euid FiletestOp StrlenOp
mod parser; mod parser;
@ -96,8 +96,10 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
"-e" => path(&f, PathCondition::Exists), "-e" => path(&f, PathCondition::Exists),
"-f" => path(&f, PathCondition::Regular), "-f" => path(&f, PathCondition::Regular),
"-g" => path(&f, PathCondition::GroupIdFlag), "-g" => path(&f, PathCondition::GroupIdFlag),
"-G" => path(&f, PathCondition::GroupOwns),
"-h" => path(&f, PathCondition::SymLink), "-h" => path(&f, PathCondition::SymLink),
"-L" => path(&f, PathCondition::SymLink), "-L" => path(&f, PathCondition::SymLink),
"-O" => path(&f, PathCondition::UserOwns),
"-p" => path(&f, PathCondition::Fifo), "-p" => path(&f, PathCondition::Fifo),
"-r" => path(&f, PathCondition::Readable), "-r" => path(&f, PathCondition::Readable),
"-S" => path(&f, PathCondition::Socket), "-S" => path(&f, PathCondition::Socket),
@ -166,7 +168,9 @@ enum PathCondition {
Exists, Exists,
Regular, Regular,
GroupIdFlag, GroupIdFlag,
GroupOwns,
SymLink, SymLink,
UserOwns,
Fifo, Fifo,
Readable, Readable,
Socket, Socket,
@ -190,18 +194,28 @@ fn path(path: &OsStr, condition: PathCondition) -> bool {
Execute = 0o1, Execute = 0o1,
} }
let perm = |metadata: Metadata, p: Permission| { let geteuid = || {
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) }; let euid = unsafe { libc::geteuid() };
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
let (uid, gid) = ( let euid = syscall::geteuid().unwrap() as u32;
syscall::getuid().unwrap() as u32,
syscall::getgid().unwrap() as u32,
);
if uid == metadata.uid() { euid
};
let getegid = || {
#[cfg(not(target_os = "redox"))]
let egid = unsafe { libc::getegid() };
#[cfg(target_os = "redox")]
let egid = syscall::getegid().unwrap() as u32;
egid
};
let perm = |metadata: Metadata, p: Permission| {
if geteuid() == metadata.uid() {
metadata.mode() & ((p as u32) << 6) != 0 metadata.mode() & ((p as u32) << 6) != 0
} else if gid == metadata.gid() { } else if getegid() == metadata.gid() {
metadata.mode() & ((p as u32) << 3) != 0 metadata.mode() & ((p as u32) << 3) != 0
} else { } else {
metadata.mode() & (p as u32) != 0 metadata.mode() & (p as u32) != 0
@ -230,7 +244,9 @@ fn path(path: &OsStr, condition: PathCondition) -> bool {
PathCondition::Exists => true, PathCondition::Exists => true,
PathCondition::Regular => file_type.is_file(), PathCondition::Regular => file_type.is_file(),
PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0, PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0,
PathCondition::GroupOwns => metadata.gid() == getegid(),
PathCondition::SymLink => metadata.file_type().is_symlink(), PathCondition::SymLink => metadata.file_type().is_symlink(),
PathCondition::UserOwns => metadata.uid() == geteuid(),
PathCondition::Fifo => file_type.is_fifo(), PathCondition::Fifo => file_type.is_fifo(),
PathCondition::Readable => perm(metadata, Permission::Read), PathCondition::Readable => perm(metadata, Permission::Read),
PathCondition::Socket => file_type.is_socket(), PathCondition::Socket => file_type.is_socket(),
@ -257,7 +273,9 @@ fn path(path: &OsStr, condition: PathCondition) -> bool {
PathCondition::Exists => true, PathCondition::Exists => true,
PathCondition::Regular => stat.is_file(), PathCondition::Regular => stat.is_file(),
PathCondition::GroupIdFlag => false, PathCondition::GroupIdFlag => false,
PathCondition::GroupOwns => unimplemented!(),
PathCondition::SymLink => false, PathCondition::SymLink => false,
PathCondition::UserOwns => unimplemented!(),
PathCondition::Fifo => false, PathCondition::Fifo => false,
PathCondition::Readable => false, // TODO PathCondition::Readable => false, // TODO
PathCondition::Socket => false, PathCondition::Socket => false,

View file

@ -17,6 +17,7 @@ path = "src/timeout.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
libc = "0.2.42" libc = "0.2.42"
nix = "0.20.0"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -5,7 +5,7 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid // spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
@ -17,13 +17,13 @@ use std::io::ErrorKind;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::time::Duration; use std::time::Duration;
use uucore::process::ChildExt; use uucore::process::ChildExt;
use uucore::signals::signal_by_name_or_value; use uucore::signals::{signal_by_name_or_value, signal_name_by_value};
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION."; static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION.";
fn get_usage() -> String { fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!()) format!("{0} [OPTION] DURATION COMMAND...", executable!())
} }
const ERR_EXIT_STATUS: i32 = 125; const ERR_EXIT_STATUS: i32 = 125;
@ -33,22 +33,22 @@ pub mod options {
pub static KILL_AFTER: &str = "kill-after"; pub static KILL_AFTER: &str = "kill-after";
pub static SIGNAL: &str = "signal"; pub static SIGNAL: &str = "signal";
pub static PRESERVE_STATUS: &str = "preserve-status"; pub static PRESERVE_STATUS: &str = "preserve-status";
pub static VERBOSE: &str = "verbose";
// Positional args. // Positional args.
pub static DURATION: &str = "duration"; pub static DURATION: &str = "duration";
pub static COMMAND: &str = "command"; pub static COMMAND: &str = "command";
pub static ARGS: &str = "args";
} }
struct Config { struct Config {
foreground: bool, foreground: bool,
kill_after: Duration, kill_after: Option<Duration>,
signal: usize, signal: usize,
duration: Duration, duration: Duration,
preserve_status: bool, preserve_status: bool,
verbose: bool,
command: String, command: Vec<String>,
command_args: Vec<String>,
} }
impl Config { impl Config {
@ -66,23 +66,22 @@ impl Config {
_ => uucore::signals::signal_by_name_or_value("TERM").unwrap(), _ => uucore::signals::signal_by_name_or_value("TERM").unwrap(),
}; };
let kill_after: Duration = match options.value_of(options::KILL_AFTER) { let kill_after = options
Some(time) => uucore::parse_time::from_str(time).unwrap(), .value_of(options::KILL_AFTER)
None => Duration::new(0, 0), .map(|time| uucore::parse_time::from_str(time).unwrap());
};
let duration: Duration = let duration: Duration =
uucore::parse_time::from_str(options.value_of(options::DURATION).unwrap()).unwrap(); uucore::parse_time::from_str(options.value_of(options::DURATION).unwrap()).unwrap();
let preserve_status: bool = options.is_present(options::PRESERVE_STATUS); let preserve_status: bool = options.is_present(options::PRESERVE_STATUS);
let foreground = options.is_present(options::FOREGROUND); let foreground = options.is_present(options::FOREGROUND);
let verbose = options.is_present(options::VERBOSE);
let command: String = options.value_of(options::COMMAND).unwrap().to_string(); let command = options
.values_of(options::COMMAND)
let command_args: Vec<String> = match options.values_of(options::ARGS) { .unwrap()
Some(values) => values.map(|x| x.to_owned()).collect(), .map(String::from)
None => vec![], .collect::<Vec<_>>();
};
Config { Config {
foreground, foreground,
@ -91,7 +90,7 @@ impl Config {
duration, duration,
preserve_status, preserve_status,
command, command,
command_args, verbose,
} }
} }
} }
@ -128,6 +127,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.help("specify the signal to be sent on timeout; SIGNAL may be a name like 'HUP' or a number; see 'kill -l' for a list of signals") .help("specify the signal to be sent on timeout; SIGNAL may be a name like 'HUP' or a number; see 'kill -l' for a list of signals")
.takes_value(true) .takes_value(true)
) )
.arg(
Arg::with_name(options::VERBOSE)
.short("v")
.long(options::VERBOSE)
.help("diagnose to stderr any signal sent upon timeout")
)
.arg( .arg(
Arg::with_name(options::DURATION) Arg::with_name(options::DURATION)
.index(1) .index(1)
@ -137,9 +142,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Arg::with_name(options::COMMAND) Arg::with_name(options::COMMAND)
.index(2) .index(2)
.required(true) .required(true)
) .multiple(true)
.arg(
Arg::with_name(options::ARGS).multiple(true)
) )
.setting(AppSettings::TrailingVarArg); .setting(AppSettings::TrailingVarArg);
@ -148,31 +151,42 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let config = Config::from(matches); let config = Config::from(matches);
timeout( timeout(
&config.command, &config.command,
&config.command_args,
config.duration, config.duration,
config.signal, config.signal,
config.kill_after, config.kill_after,
config.foreground, config.foreground,
config.preserve_status, config.preserve_status,
config.verbose,
) )
} }
/// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail.
fn unblock_sigchld() {
unsafe {
nix::sys::signal::signal(
nix::sys::signal::Signal::SIGCHLD,
nix::sys::signal::SigHandler::SigDfl,
)
.unwrap();
}
}
/// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes. /// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes.
fn timeout( fn timeout(
cmdname: &str, cmd: &[String],
args: &[String],
duration: Duration, duration: Duration,
signal: usize, signal: usize,
kill_after: Duration, kill_after: Option<Duration>,
foreground: bool, foreground: bool,
preserve_status: bool, preserve_status: bool,
verbose: bool,
) -> i32 { ) -> i32 {
if !foreground { if !foreground {
unsafe { libc::setpgid(0, 0) }; unsafe { libc::setpgid(0, 0) };
} }
let mut process = match Command::new(cmdname) let mut process = match Command::new(&cmd[0])
.args(args) .args(&cmd[1..])
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
@ -190,32 +204,44 @@ fn timeout(
} }
} }
}; };
unblock_sigchld();
match process.wait_or_timeout(duration) { match process.wait_or_timeout(duration) {
Ok(Some(status)) => status.code().unwrap_or_else(|| status.signal().unwrap()), Ok(Some(status)) => status.code().unwrap_or_else(|| status.signal().unwrap()),
Ok(None) => { Ok(None) => {
if verbose {
show_error!(
"sending signal {} to command '{}'",
signal_name_by_value(signal).unwrap(),
cmd[0]
);
}
return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal)); return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal));
match process.wait_or_timeout(kill_after) { if let Some(kill_after) = kill_after {
Ok(Some(status)) => { match process.wait_or_timeout(kill_after) {
if preserve_status { Ok(Some(status)) => {
status.code().unwrap_or_else(|| status.signal().unwrap()) if preserve_status {
} else { status.code().unwrap_or_else(|| status.signal().unwrap())
124 } else {
124
}
} }
} Ok(None) => {
Ok(None) => { if verbose {
if kill_after == Duration::new(0, 0) { show_error!("sending signal KILL to command '{}'", cmd[0]);
// XXX: this may not be right }
return 124; return_if_err!(
ERR_EXIT_STATUS,
process.send_signal(
uucore::signals::signal_by_name_or_value("KILL").unwrap()
)
);
return_if_err!(ERR_EXIT_STATUS, process.wait());
137
} }
return_if_err!( Err(_) => 124,
ERR_EXIT_STATUS,
process
.send_signal(uucore::signals::signal_by_name_or_value("KILL").unwrap())
);
return_if_err!(ERR_EXIT_STATUS, process.wait());
137
} }
Err(_) => 124, } else {
124
} }
} }
Err(_) => { Err(_) => {

View file

@ -166,7 +166,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
if let Err(e) = File::create(path) { if let Err(e) = File::create(path) {
show_warning!("cannot touch '{}': {}", path, e); match e.kind() {
std::io::ErrorKind::NotFound => {
show_error!("cannot touch '{}': {}", path, "No such file or directory")
}
std::io::ErrorKind::PermissionDenied => {
show_error!("cannot touch '{}': {}", path, "Permission denied")
}
_ => show_error!("cannot touch '{}': {}", path, e),
}
error_code = 1; error_code = 1;
continue; continue;
}; };

View file

@ -47,6 +47,9 @@ use std::io::Result as IOResult;
use std::ptr; use std::ptr;
extern "C" { extern "C" {
/// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html
/// > The getgrouplist() function scans the group database to obtain
/// > the list of groups that user belongs to.
fn getgrouplist( fn getgrouplist(
name: *const c_char, name: *const c_char,
gid: gid_t, gid: gid_t,
@ -55,6 +58,13 @@ extern "C" {
) -> c_int; ) -> c_int;
} }
/// From: https://man7.org/linux/man-pages/man2/getgroups.2.html
/// > getgroups() returns the supplementary group IDs of the calling
/// > process in list.
/// > If size is zero, list is not modified, but the total number of
/// > supplementary group IDs for the process is returned. This allows
/// > the caller to determine the size of a dynamically allocated list
/// > to be used in a further call to getgroups().
pub fn get_groups() -> IOResult<Vec<gid_t>> { pub fn get_groups() -> IOResult<Vec<gid_t>> {
let ngroups = unsafe { getgroups(0, ptr::null_mut()) }; let ngroups = unsafe { getgroups(0, ptr::null_mut()) };
if ngroups == -1 { if ngroups == -1 {
@ -83,17 +93,17 @@ pub fn get_groups() -> IOResult<Vec<gid_t>> {
/// for `id --groups --real` if `gid` and `egid` are not equal. /// for `id --groups --real` if `gid` and `egid` are not equal.
/// ///
/// From: https://www.man7.org/linux/man-pages/man3/getgroups.3p.html /// From: https://www.man7.org/linux/man-pages/man3/getgroups.3p.html
/// As implied by the definition of supplementary groups, the /// > As implied by the definition of supplementary groups, the
/// effective group ID may appear in the array returned by /// > effective group ID may appear in the array returned by
/// getgroups() or it may be returned only by getegid(). Duplication /// > getgroups() or it may be returned only by getegid(). Duplication
/// may exist, but the application needs to call getegid() to be sure /// > may exist, but the application needs to call getegid() to be sure
/// of getting all of the information. Various implementation /// > of getting all of the information. Various implementation
/// variations and administrative sequences cause the set of groups /// > variations and administrative sequences cause the set of groups
/// appearing in the result of getgroups() to vary in order and as to /// > appearing in the result of getgroups() to vary in order and as to
/// whether the effective group ID is included, even when the set of /// > whether the effective group ID is included, even when the set of
/// groups is the same (in the mathematical sense of ``set''). (The /// > groups is the same (in the mathematical sense of ``set''). (The
/// history of a process and its parents could affect the details of /// > history of a process and its parents could affect the details of
/// the result.) /// > the result.)
#[cfg(all(unix, feature = "process"))] #[cfg(all(unix, feature = "process"))]
pub fn get_groups_gnu(arg_id: Option<u32>) -> IOResult<Vec<gid_t>> { pub fn get_groups_gnu(arg_id: Option<u32>) -> IOResult<Vec<gid_t>> {
let groups = get_groups()?; let groups = get_groups()?;
@ -187,20 +197,35 @@ impl Passwd {
/// This is a wrapper function for `libc::getgrouplist`. /// This is a wrapper function for `libc::getgrouplist`.
/// ///
/// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html /// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html
/// If the user is a member of more than *ngroups groups, then /// > If the number of groups of which user is a member is less than or
/// getgrouplist() returns -1. In this case, the value returned in /// > equal to *ngroups, then the value *ngroups is returned.
/// *ngroups can be used to resize the buffer passed to a further /// > If the user is a member of more than *ngroups groups, then
/// call getgrouplist(). /// > getgrouplist() returns -1. In this case, the value returned in
/// > *ngroups can be used to resize the buffer passed to a further
/// > call getgrouplist().
///
/// However, on macOS/darwin (and maybe others?) `getgrouplist` does
/// not update `ngroups` if `ngroups` is too small. Therefore, if not
/// updated by `getgrouplist`, `ngroups` needs to be increased in a
/// loop until `getgrouplist` stops returning -1.
pub fn belongs_to(&self) -> Vec<gid_t> { pub fn belongs_to(&self) -> Vec<gid_t> {
let mut ngroups: c_int = 8; let mut ngroups: c_int = 8;
let mut ngroups_old: c_int;
let mut groups = Vec::with_capacity(ngroups as usize); let mut groups = Vec::with_capacity(ngroups as usize);
let gid = self.inner.pw_gid; let gid = self.inner.pw_gid;
let name = self.inner.pw_name; let name = self.inner.pw_name;
unsafe { loop {
if getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) == -1 { ngroups_old = ngroups;
if unsafe { getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) } == -1 {
if ngroups == ngroups_old {
ngroups *= 2;
}
groups.resize(ngroups as usize, 0); groups.resize(ngroups as usize, 0);
getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups); } else {
break;
} }
}
unsafe {
groups.set_len(ngroups as usize); groups.set_len(ngroups as usize);
} }
groups.truncate(ngroups as usize); groups.truncate(ngroups as usize);

View file

@ -17,18 +17,22 @@ use std::process::ExitStatus as StdExitStatus;
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
/// `geteuid()` returns the effective user ID of the calling process.
pub fn geteuid() -> uid_t { pub fn geteuid() -> uid_t {
unsafe { libc::geteuid() } unsafe { libc::geteuid() }
} }
/// `getegid()` returns the effective group ID of the calling process.
pub fn getegid() -> gid_t { pub fn getegid() -> gid_t {
unsafe { libc::getegid() } unsafe { libc::getegid() }
} }
/// `getgid()` returns the real group ID of the calling process.
pub fn getgid() -> gid_t { pub fn getgid() -> gid_t {
unsafe { libc::getgid() } unsafe { libc::getgid() }
} }
/// `getuid()` returns the real user ID of the calling process.
pub fn getuid() -> uid_t { pub fn getuid() -> uid_t {
unsafe { libc::getuid() } unsafe { libc::getuid() }
} }
@ -93,6 +97,7 @@ pub trait ChildExt {
fn send_signal(&mut self, signal: usize) -> io::Result<()>; fn send_signal(&mut self, signal: usize) -> io::Result<()>;
/// Wait for a process to finish or return after the specified duration. /// Wait for a process to finish or return after the specified duration.
/// A `timeout` of zero disables the timeout.
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>; fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>;
} }
@ -106,6 +111,11 @@ impl ChildExt for Child {
} }
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> { fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
if timeout == Duration::from_micros(0) {
return self
.wait()
.map(|status| Some(ExitStatus::from_std_status(status)));
}
// .try_wait() doesn't drop stdin, so we do it manually // .try_wait() doesn't drop stdin, so we do it manually
drop(self.stdin.take()); drop(self.stdin.take());

View file

@ -10,11 +10,6 @@
pub static DEFAULT_SIGNAL: usize = 15; pub static DEFAULT_SIGNAL: usize = 15;
pub struct Signal<'a> {
pub name: &'a str,
pub value: usize,
}
/* /*
Linux Programmer's Manual Linux Programmer's Manual
@ -29,131 +24,10 @@ Linux Programmer's Manual
*/ */
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub static ALL_SIGNALS: [Signal<'static>; 31] = [ pub static ALL_SIGNALS: [&str; 32] = [
Signal { "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
name: "HUP", "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU",
value: 1, "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "POLL", "PWR", "SYS",
},
Signal {
name: "INT",
value: 2,
},
Signal {
name: "QUIT",
value: 3,
},
Signal {
name: "ILL",
value: 4,
},
Signal {
name: "TRAP",
value: 5,
},
Signal {
name: "ABRT",
value: 6,
},
Signal {
name: "BUS",
value: 7,
},
Signal {
name: "FPE",
value: 8,
},
Signal {
name: "KILL",
value: 9,
},
Signal {
name: "USR1",
value: 10,
},
Signal {
name: "SEGV",
value: 11,
},
Signal {
name: "USR2",
value: 12,
},
Signal {
name: "PIPE",
value: 13,
},
Signal {
name: "ALRM",
value: 14,
},
Signal {
name: "TERM",
value: 15,
},
Signal {
name: "STKFLT",
value: 16,
},
Signal {
name: "CHLD",
value: 17,
},
Signal {
name: "CONT",
value: 18,
},
Signal {
name: "STOP",
value: 19,
},
Signal {
name: "TSTP",
value: 20,
},
Signal {
name: "TTIN",
value: 21,
},
Signal {
name: "TTOU",
value: 22,
},
Signal {
name: "URG",
value: 23,
},
Signal {
name: "XCPU",
value: 24,
},
Signal {
name: "XFSZ",
value: 25,
},
Signal {
name: "VTALRM",
value: 26,
},
Signal {
name: "PROF",
value: 27,
},
Signal {
name: "WINCH",
value: 28,
},
Signal {
name: "POLL",
value: 29,
},
Signal {
name: "PWR",
value: 30,
},
Signal {
name: "SYS",
value: 31,
},
]; ];
/* /*
@ -198,131 +72,10 @@ No Name Default Action Description
*/ */
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] #[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
pub static ALL_SIGNALS: [Signal<'static>; 31] = [ pub static ALL_SIGNALS: [&str; 32] = [
Signal { "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
name: "HUP", "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
value: 1, "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2",
},
Signal {
name: "INT",
value: 2,
},
Signal {
name: "QUIT",
value: 3,
},
Signal {
name: "ILL",
value: 4,
},
Signal {
name: "TRAP",
value: 5,
},
Signal {
name: "ABRT",
value: 6,
},
Signal {
name: "EMT",
value: 7,
},
Signal {
name: "FPE",
value: 8,
},
Signal {
name: "KILL",
value: 9,
},
Signal {
name: "BUS",
value: 10,
},
Signal {
name: "SEGV",
value: 11,
},
Signal {
name: "SYS",
value: 12,
},
Signal {
name: "PIPE",
value: 13,
},
Signal {
name: "ALRM",
value: 14,
},
Signal {
name: "TERM",
value: 15,
},
Signal {
name: "URG",
value: 16,
},
Signal {
name: "STOP",
value: 17,
},
Signal {
name: "TSTP",
value: 18,
},
Signal {
name: "CONT",
value: 19,
},
Signal {
name: "CHLD",
value: 20,
},
Signal {
name: "TTIN",
value: 21,
},
Signal {
name: "TTOU",
value: 22,
},
Signal {
name: "IO",
value: 23,
},
Signal {
name: "XCPU",
value: 24,
},
Signal {
name: "XFSZ",
value: 25,
},
Signal {
name: "VTALRM",
value: 26,
},
Signal {
name: "PROF",
value: 27,
},
Signal {
name: "WINCH",
value: 28,
},
Signal {
name: "INFO",
value: 29,
},
Signal {
name: "USR1",
value: 30,
},
Signal {
name: "USR2",
value: 31,
},
]; ];
pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> { pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
@ -335,56 +88,45 @@ pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
} }
let signal_name = signal_name_or_value.trim_start_matches("SIG"); let signal_name = signal_name_or_value.trim_start_matches("SIG");
ALL_SIGNALS ALL_SIGNALS.iter().position(|&s| s == signal_name)
.iter()
.find(|s| s.name == signal_name)
.map(|s| s.value)
} }
#[inline(always)]
pub fn is_signal(num: usize) -> bool { pub fn is_signal(num: usize) -> bool {
// Named signals start at 1 num < ALL_SIGNALS.len()
num <= ALL_SIGNALS.len()
} }
#[test] pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> {
fn signals_all_contiguous() { ALL_SIGNALS.get(signal_value).copied()
for (i, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal.value, i + 1);
}
}
#[test]
fn signals_all_are_signal() {
for signal in &ALL_SIGNALS {
assert!(is_signal(signal.value));
}
} }
#[test] #[test]
fn signal_by_value() { fn signal_by_value() {
assert_eq!(signal_by_name_or_value("0"), Some(0)); assert_eq!(signal_by_name_or_value("0"), Some(0));
for signal in &ALL_SIGNALS { for (value, _signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!( assert_eq!(signal_by_name_or_value(&value.to_string()), Some(value));
signal_by_name_or_value(&signal.value.to_string()),
Some(signal.value)
);
} }
} }
#[test] #[test]
fn signal_by_short_name() { fn signal_by_short_name() {
for signal in &ALL_SIGNALS { for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_by_name_or_value(signal.name), Some(signal.value)); assert_eq!(signal_by_name_or_value(signal), Some(value));
} }
} }
#[test] #[test]
fn signal_by_long_name() { fn signal_by_long_name() {
for signal in &ALL_SIGNALS { for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!( assert_eq!(
signal_by_name_or_value(&format!("SIG{}", signal.name)), signal_by_name_or_value(&format!("SIG{}", signal)),
Some(signal.value) Some(value)
); );
} }
} }
#[test]
fn name() {
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_name_by_value(value), Some(*signal));
}
}

View file

@ -80,19 +80,22 @@ fn _du_basics_subdir(s: &str) {
#[test] #[test]
fn test_du_invalid_size() { fn test_du_invalid_size() {
new_ucmd!() let args = &["block-size", "threshold"];
.arg("--block-size=1fb4t") for s in args {
.arg("/tmp") new_ucmd!()
.fails() .arg(format!("--{}=1fb4t", s))
.code_is(1) .arg("/tmp")
.stderr_only("du: invalid --block-size argument '1fb4t'"); .fails()
#[cfg(not(target_pointer_width = "128"))] .code_is(1)
new_ucmd!() .stderr_only(format!("du: invalid --{} argument '1fb4t'", s));
.arg("--block-size=1Y") #[cfg(not(target_pointer_width = "128"))]
.arg("/tmp") new_ucmd!()
.fails() .arg(format!("--{}=1Y", s))
.code_is(1) .arg("/tmp")
.stderr_only("du: --block-size argument '1Y' too large"); .fails()
.code_is(1)
.stderr_only(format!("du: --{} argument '1Y' too large", s));
}
} }
#[test] #[test]
@ -351,3 +354,24 @@ fn test_du_one_file_system() {
} }
_du_basics_subdir(result.stdout_str()); _du_basics_subdir(result.stdout_str());
} }
#[test]
fn test_du_threshold() {
let scene = TestScenario::new(util_name!());
let threshold = if cfg!(windows) { "7K" } else { "10K" };
scene
.ucmd()
.arg(format!("--threshold={}", threshold))
.succeeds()
.stdout_contains("links")
.stdout_does_not_contain("deeper");
scene
.ucmd()
.arg(format!("--threshold=-{}", threshold))
.succeeds()
.stdout_does_not_contain("links")
.stdout_contains("deeper");
}

View file

@ -3,6 +3,7 @@
use crate::common::util::*; use crate::common::util::*;
use chrono::offset::Local; use chrono::offset::Local;
use chrono::DateTime; use chrono::DateTime;
use chrono::Duration;
use std::fs::metadata; use std::fs::metadata;
fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String { fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
@ -20,8 +21,22 @@ fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
.unwrap_or_default() .unwrap_or_default()
} }
fn now_time() -> String { fn all_minutes(from: DateTime<Local>, to: DateTime<Local>) -> Vec<String> {
Local::now().format("%b %d %H:%M %Y").to_string() const FORMAT: &str = "%b %d %H:%M %Y";
let mut vec = vec![];
let mut current = from;
while current < to {
vec.push(current.format(FORMAT).to_string());
current = current + Duration::minutes(1);
}
vec
}
fn valid_last_modified_template_vars(from: DateTime<Local>) -> Vec<Vec<(String, String)>> {
all_minutes(from, Local::now())
.into_iter()
.map(|time| vec![("{last_modified_time}".to_string(), time)])
.collect()
} }
#[test] #[test]
@ -33,10 +48,7 @@ fn test_without_any_options() {
scenario scenario
.args(&[test_file_path]) .args(&[test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -48,10 +60,7 @@ fn test_with_numbering_option_with_number_width() {
scenario scenario
.args(&["-n", "2", test_file_path]) .args(&["-n", "2", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -66,10 +75,7 @@ fn test_with_long_header_option() {
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(
expected_test_file_path, expected_test_file_path,
vec![ &[("{last_modified_time}", &value), ("{header}", header)],
(&"{last_modified_time}".to_string(), &value),
(&"{header}".to_string(), &header.to_string()),
],
); );
new_ucmd!() new_ucmd!()
@ -77,10 +83,7 @@ fn test_with_long_header_option() {
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(
expected_test_file_path, expected_test_file_path,
vec![ &[("{last_modified_time}", &value), ("{header}", header)],
(&"{last_modified_time}".to_string(), &value),
(&"{header}".to_string(), &header.to_string()),
],
); );
} }
@ -93,18 +96,12 @@ fn test_with_double_space_option() {
scenario scenario
.args(&["-d", test_file_path]) .args(&["-d", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
new_ucmd!() new_ucmd!()
.args(&["--double-space", test_file_path]) .args(&["--double-space", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -116,10 +113,7 @@ fn test_with_first_line_number_option() {
scenario scenario
.args(&["-N", "5", "-n", test_file_path]) .args(&["-N", "5", "-n", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -131,10 +125,7 @@ fn test_with_first_line_number_long_option() {
scenario scenario
.args(&["--first-line-number=5", "-n", test_file_path]) .args(&["--first-line-number=5", "-n", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -146,10 +137,7 @@ fn test_with_number_option_with_custom_separator_char() {
scenario scenario
.args(&["-nc", test_file_path]) .args(&["-nc", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -161,10 +149,7 @@ fn test_with_number_option_with_custom_separator_char_and_width() {
scenario scenario
.args(&["-nc1", test_file_path]) .args(&["-nc1", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -207,25 +192,19 @@ fn test_with_page_range() {
scenario scenario
.args(&["--pages=15", test_file_path]) .args(&["--pages=15", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
new_ucmd!() new_ucmd!()
.args(&["+15", test_file_path]) .args(&["+15", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
new_ucmd!() new_ucmd!()
.args(&["--pages=15:17", test_file_path]) .args(&["--pages=15:17", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(
expected_test_file_path1, expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &value)], &[("{last_modified_time}", &value)],
); );
new_ucmd!() new_ucmd!()
@ -233,7 +212,7 @@ fn test_with_page_range() {
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(
expected_test_file_path1, expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &value)], &[("{last_modified_time}", &value)],
); );
} }
@ -246,10 +225,7 @@ fn test_with_no_header_trailer_option() {
scenario scenario
.args(&["-t", test_file_path]) .args(&["-t", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -262,10 +238,7 @@ fn test_with_page_length_option() {
scenario scenario
.args(&["--pages=2:3", "-l", "100", "-n", test_file_path]) .args(&["--pages=2:3", "-l", "100", "-n", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
new_ucmd!() new_ucmd!()
.args(&["--pages=2:3", "-l", "5", "-n", test_file_path]) .args(&["--pages=2:3", "-l", "5", "-n", test_file_path])
@ -288,14 +261,14 @@ fn test_with_suppress_error_option() {
fn test_with_stdin() { fn test_with_stdin() {
let expected_file_path = "stdin.log.expected"; let expected_file_path = "stdin.log.expected";
let mut scenario = new_ucmd!(); let mut scenario = new_ucmd!();
let now = now_time(); let start = Local::now();
scenario scenario
.pipe_in_fixture("stdin.log") .pipe_in_fixture("stdin.log")
.args(&["--pages=1:2", "-n", "-"]) .args(&["--pages=1:2", "-n", "-"])
.run() .run()
.stdout_is_templated_fixture( .stdout_is_templated_fixture_any(
expected_file_path, expected_file_path,
vec![(&"{last_modified_time}".to_string(), &now)], &valid_last_modified_template_vars(start),
); );
} }
@ -308,18 +281,12 @@ fn test_with_column() {
scenario scenario
.args(&["--pages=3:5", "--column=3", "-n", test_file_path]) .args(&["--pages=3:5", "--column=3", "-n", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
new_ucmd!() new_ucmd!()
.args(&["--pages=3:5", "-3", "-n", test_file_path]) .args(&["--pages=3:5", "-3", "-n", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -331,10 +298,7 @@ fn test_with_column_across_option() {
scenario scenario
.args(&["--pages=3:5", "--column=3", "-a", "-n", test_file_path]) .args(&["--pages=3:5", "--column=3", "-a", "-n", test_file_path])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -354,10 +318,7 @@ fn test_with_column_across_option_and_column_separator() {
test_file_path, test_file_path,
]) ])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
new_ucmd!() new_ucmd!()
.args(&[ .args(&[
@ -371,7 +332,7 @@ fn test_with_column_across_option_and_column_separator() {
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(
expected_test_file_path1, expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &value)], &[("{last_modified_time}", &value)],
); );
} }
@ -382,25 +343,25 @@ fn test_with_mpr() {
let expected_test_file_path = "mpr.log.expected"; let expected_test_file_path = "mpr.log.expected";
let expected_test_file_path1 = "mpr1.log.expected"; let expected_test_file_path1 = "mpr1.log.expected";
let expected_test_file_path2 = "mpr2.log.expected"; let expected_test_file_path2 = "mpr2.log.expected";
let now = now_time(); let start = Local::now();
new_ucmd!() new_ucmd!()
.args(&["--pages=1:2", "-m", "-n", test_file_path, test_file_path1]) .args(&["--pages=1:2", "-m", "-n", test_file_path, test_file_path1])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture_any(
expected_test_file_path, expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &now)], &valid_last_modified_template_vars(start),
); );
let now = now_time(); let start = Local::now();
new_ucmd!() new_ucmd!()
.args(&["--pages=2:4", "-m", "-n", test_file_path, test_file_path1]) .args(&["--pages=2:4", "-m", "-n", test_file_path, test_file_path1])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture_any(
expected_test_file_path1, expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &now)], &valid_last_modified_template_vars(start),
); );
let now = now_time(); let start = Local::now();
new_ucmd!() new_ucmd!()
.args(&[ .args(&[
"--pages=1:2", "--pages=1:2",
@ -413,9 +374,9 @@ fn test_with_mpr() {
test_file_path, test_file_path,
]) ])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture_any(
expected_test_file_path2, expected_test_file_path2,
vec![(&"{last_modified_time}".to_string(), &now)], &valid_last_modified_template_vars(start),
); );
} }
@ -452,10 +413,7 @@ fn test_with_offset_space_option() {
test_file_path, test_file_path,
]) ])
.succeeds() .succeeds()
.stdout_is_templated_fixture( .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
} }
#[test] #[test]
@ -497,9 +455,9 @@ fn test_with_pr_core_utils_tests() {
scenario_with_expected_status.stdout_is_templated_fixture( scenario_with_expected_status.stdout_is_templated_fixture(
test_file_path, test_file_path,
vec![ &[
(&"{last_modified_time}".to_string(), &value), ("{last_modified_time}", &value),
(&"{file_name}".to_string(), &input_file_path.to_string()), ("{file_name}", input_file_path),
], ],
); );
} }
@ -511,12 +469,12 @@ fn test_with_join_lines_option() {
let test_file_2 = "test.log"; let test_file_2 = "test.log";
let expected_file_path = "joined.log.expected"; let expected_file_path = "joined.log.expected";
let mut scenario = new_ucmd!(); let mut scenario = new_ucmd!();
let now = now_time(); let start = Local::now();
scenario scenario
.args(&["+1:2", "-J", "-m", test_file_1, test_file_2]) .args(&["+1:2", "-J", "-m", test_file_1, test_file_2])
.run() .run()
.stdout_is_templated_fixture( .stdout_is_templated_fixture_any(
expected_file_path, expected_file_path,
vec![(&"{last_modified_time}".to_string(), &now)], &valid_last_modified_template_vars(start),
); );
} }

View file

@ -762,26 +762,30 @@ fn test_pipe() {
#[test] #[test]
fn test_check() { fn test_check() {
new_ucmd!() for diagnose_arg in &["-c", "--check", "--check=diagnose-first"] {
.arg("-c") new_ucmd!()
.arg("check_fail.txt") .arg(diagnose_arg)
.fails() .arg("check_fail.txt")
.stdout_is("sort: check_fail.txt:6: disorder: 5\n"); .fails()
.stdout_is("sort: check_fail.txt:6: disorder: 5\n");
new_ucmd!() new_ucmd!()
.arg("-c") .arg(diagnose_arg)
.arg("multiple_files.expected") .arg("multiple_files.expected")
.succeeds() .succeeds()
.stdout_is(""); .stdout_is("");
}
} }
#[test] #[test]
fn test_check_silent() { fn test_check_silent() {
new_ucmd!() for silent_arg in &["-C", "--check=silent", "--check=quiet"] {
.arg("-C") new_ucmd!()
.arg("check_fail.txt") .arg(silent_arg)
.fails() .arg("check_fail.txt")
.stdout_is(""); .fails()
.stdout_is("");
}
} }
#[test] #[test]
@ -835,7 +839,7 @@ fn test_nonexistent_file() {
#[test] #[test]
fn test_blanks() { fn test_blanks() {
test_helper("blanks", &["-b", "--ignore-blanks"]); test_helper("blanks", &["-b", "--ignore-leading-blanks"]);
} }
#[test] #[test]

View file

@ -8,7 +8,7 @@
// file that was distributed with this source code. // file that was distributed with this source code.
// //
// spell-checker:ignore (words) pseudofloat // spell-checker:ignore (words) egid euid pseudofloat
use crate::common::util::*; use crate::common::util::*;
@ -476,6 +476,52 @@ fn test_nonexistent_file_is_not_symlink() {
.succeeds(); .succeeds();
} }
#[test]
#[cfg(not(windows))]
fn test_file_owned_by_euid() {
new_ucmd!().args(&["-O", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-O", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-f", "/bin/sh", "-a", "!", "-O", "/bin/sh"])
.succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_file_owned_by_egid() {
new_ucmd!().args(&["-G", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-G", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-f", "/bin/sh", "-a", "!", "-G", "/bin/sh"])
.succeeds();
}
#[test] #[test]
fn test_op_precedence_and_or_1() { fn test_op_precedence_and_or_1() {
new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds(); new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds();

View file

@ -9,3 +9,39 @@ fn test_subcommand_return_code() {
new_ucmd!().arg("1").arg("false").run().status_code(1); new_ucmd!().arg("1").arg("false").run().status_code(1);
} }
#[test]
fn test_command_with_args() {
new_ucmd!()
.args(&["1700", "echo", "-n", "abcd"])
.succeeds()
.stdout_only("abcd");
}
#[test]
fn test_verbose() {
for &verbose_flag in &["-v", "--verbose"] {
new_ucmd!()
.args(&[verbose_flag, ".1", "sleep", "10"])
.fails()
.stderr_only("timeout: sending signal TERM to command 'sleep'");
new_ucmd!()
.args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "10"])
.fails()
.stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'");
}
}
#[test]
fn test_zero_timeout() {
new_ucmd!()
.args(&["-v", "0", "sleep", ".1"])
.succeeds()
.no_stderr()
.no_stdout();
new_ucmd!()
.args(&["-v", "0", "-s0", "-k0", "sleep", ".1"])
.succeeds()
.no_stderr()
.no_stdout();
}

View file

@ -6,6 +6,7 @@ use self::touch::filetime::{self, FileTime};
extern crate time; extern crate time;
use crate::common::util::*; use crate::common::util::*;
use std::path::PathBuf;
fn get_file_times(at: &AtPath, path: &str) -> (FileTime, FileTime) { fn get_file_times(at: &AtPath, path: &str) -> (FileTime, FileTime) {
let m = at.metadata(path); let m = at.metadata(path);
@ -466,3 +467,37 @@ fn test_touch_trailing_slash() {
let file = "no-file/"; let file = "no-file/";
ucmd.args(&[file]).fails(); ucmd.args(&[file]).fails();
} }
#[test]
fn test_touch_no_such_file_error_msg() {
let dirname = "nonexistent";
let filename = "file";
let path = PathBuf::from(dirname).join(filename);
let path_str = path.to_str().unwrap();
new_ucmd!().arg(&path).fails().stderr_only(format!(
"touch: cannot touch '{}': No such file or directory",
path_str
));
}
#[test]
#[cfg(unix)]
fn test_touch_permission_denied_error_msg() {
let (at, mut ucmd) = at_and_ucmd!();
let dirname = "dir_with_read_only_access";
let filename = "file";
let path = PathBuf::from(dirname).join(filename);
let path_str = path.to_str().unwrap();
// create dest without write permissions
at.mkdir(dirname);
at.set_readonly(dirname);
let full_path = at.plus_as_string(path_str);
ucmd.arg(&full_path).fails().stderr_only(format!(
"touch: cannot touch '{}': Permission denied",
&full_path
));
}

View file

@ -223,6 +223,18 @@ impl CmdResult {
self self
} }
/// like `stdout_is`, but succeeds if any elements of `expected` matches stdout.
pub fn stdout_is_any<T: AsRef<str> + std::fmt::Debug>(&self, expected: Vec<T>) -> &CmdResult {
if !expected.iter().any(|msg| self.stdout_str() == msg.as_ref()) {
panic!(
"stdout was {}\nExpected any of {:#?}",
self.stdout_str(),
expected
)
}
self
}
/// Like `stdout_is` but newlines are normalized to `\n`. /// Like `stdout_is` but newlines are normalized to `\n`.
pub fn normalized_newlines_stdout_is<T: AsRef<str>>(&self, msg: T) -> &CmdResult { pub fn normalized_newlines_stdout_is<T: AsRef<str>>(&self, msg: T) -> &CmdResult {
let msg = msg.as_ref().replace("\r\n", "\n"); let msg = msg.as_ref().replace("\r\n", "\n");
@ -247,7 +259,7 @@ impl CmdResult {
pub fn stdout_is_templated_fixture<T: AsRef<OsStr>>( pub fn stdout_is_templated_fixture<T: AsRef<OsStr>>(
&self, &self,
file_rel_path: T, file_rel_path: T,
template_vars: Vec<(&String, &String)>, template_vars: &[(&str, &str)],
) -> &CmdResult { ) -> &CmdResult {
let mut contents = let mut contents =
String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap();
@ -257,6 +269,23 @@ impl CmdResult {
self.stdout_is(contents) self.stdout_is(contents)
} }
/// like `stdout_is_templated_fixture`, but succeeds if any replacement by `template_vars` results in the actual stdout.
pub fn stdout_is_templated_fixture_any<T: AsRef<OsStr>>(
&self,
file_rel_path: T,
template_vars: &[Vec<(String, String)>],
) {
let contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap();
let possible_values = template_vars.iter().map(|vars| {
let mut contents = contents.clone();
for kv in vars.iter() {
contents = contents.replace(&kv.0, &kv.1);
}
contents
});
self.stdout_is_any(possible_values.collect());
}
/// asserts that the command resulted in stderr stream output that equals the /// asserts that the command resulted in stderr stream output that equals the
/// passed in value, when both are trimmed of trailing whitespace /// passed in value, when both are trimmed of trailing whitespace
/// stderr_only is a better choice unless stdout may or will be non-empty /// stderr_only is a better choice unless stdout may or will be non-empty