mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
Merge remote-tracking branch 'upstream/master' into ls/fix_backslash_escape
This commit is contained in:
commit
ea10647a62
8 changed files with 394 additions and 165 deletions
3
.github/workflows/GNU.yml
vendored
3
.github/workflows/GNU.yml
vendored
|
@ -80,6 +80,9 @@ jobs:
|
|||
-e '/tests\/misc\/help-version-getopt.sh/ D' \
|
||||
Makefile
|
||||
|
||||
# printf doesn't limit the values used in its arg, so this produced ~2GB of output
|
||||
sed -i '/INT_OFLOW/ D' tests/misc/printf.sh
|
||||
|
||||
# Use the system coreutils where the test fails due to error in a util that is not the one being tested
|
||||
sed -i 's|stat|/usr/bin/stat|' tests/chgrp/basic.sh tests/cp/existing-perm-dir.sh tests/touch/60-seconds.sh tests/misc/sort-compress-proc.sh
|
||||
sed -i 's|ls -|/usr/bin/ls -|' tests/chgrp/posix-H.sh tests/chown/deref.sh tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh tests/du/8gb.sh
|
||||
|
|
|
@ -22,6 +22,12 @@ use std::io::{self, Read, Write};
|
|||
use thiserror::Error;
|
||||
use uucore::fs::is_stdin_interactive;
|
||||
|
||||
/// Linux splice support
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
mod splice;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
/// Unix domain socket support
|
||||
#[cfg(unix)]
|
||||
use std::net::Shutdown;
|
||||
|
@ -30,14 +36,6 @@ use std::os::unix::fs::FileTypeExt;
|
|||
#[cfg(unix)]
|
||||
use unix_socket::UnixStream;
|
||||
|
||||
/// Linux splice support
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::fcntl::{splice, SpliceFFlags};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use nix::unistd::pipe;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
static NAME: &str = "cat";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
static SYNTAX: &str = "[OPTION]... [FILE]...";
|
||||
|
@ -395,7 +393,7 @@ fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
|
|||
{
|
||||
// If we're on Linux or Android, try to use the splice() system call
|
||||
// for faster writing. If it works, we're done.
|
||||
if !write_fast_using_splice(handle, stdout_lock.as_raw_fd())? {
|
||||
if !splice::write_fast_using_splice(handle, stdout_lock.as_raw_fd())? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
@ -411,75 +409,6 @@ fn write_fast<R: Read>(handle: &mut InputHandle<R>) -> CatResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is called from `write_fast()` on Linux and Android. The
|
||||
/// function `splice()` is used to move data between two file descriptors
|
||||
/// without copying between kernel- and userspace. This results in a large
|
||||
/// speedup.
|
||||
///
|
||||
/// The `bool` in the result value indicates if we need to fall back to normal
|
||||
/// copying or not. False means we don't have to.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn write_fast_using_splice<R: Read>(handle: &mut InputHandle<R>, writer: RawFd) -> CatResult<bool> {
|
||||
const BUF_SIZE: usize = 1024 * 16;
|
||||
|
||||
let (pipe_rd, pipe_wr) = pipe()?;
|
||||
|
||||
// We only fall back if splice fails on the first call.
|
||||
match splice(
|
||||
handle.file_descriptor,
|
||||
None,
|
||||
pipe_wr,
|
||||
None,
|
||||
BUF_SIZE,
|
||||
SpliceFFlags::empty(),
|
||||
) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
splice_exact(pipe_rd, writer, n)?;
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let n = splice(
|
||||
handle.file_descriptor,
|
||||
None,
|
||||
pipe_wr,
|
||||
None,
|
||||
BUF_SIZE,
|
||||
SpliceFFlags::empty(),
|
||||
)?;
|
||||
if n == 0 {
|
||||
// We read 0 bytes from the input,
|
||||
// which means we're done copying.
|
||||
break;
|
||||
}
|
||||
splice_exact(pipe_rd, writer, n)?;
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Splice wrapper which handles short writes
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> {
|
||||
let mut left = num_bytes;
|
||||
loop {
|
||||
let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?;
|
||||
left -= written;
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Outputs file contents to stdout in a line-by-line fashion,
|
||||
/// propagating any errors that might occur.
|
||||
fn write_lines<R: Read>(
|
||||
|
|
91
src/uu/cat/src/splice.rs
Normal file
91
src/uu/cat/src/splice.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use super::{CatResult, InputHandle};
|
||||
|
||||
use nix::fcntl::{splice, SpliceFFlags};
|
||||
use nix::unistd::{self, pipe};
|
||||
use std::io::Read;
|
||||
use std::os::unix::io::RawFd;
|
||||
|
||||
const BUF_SIZE: usize = 1024 * 16;
|
||||
|
||||
/// This function is called from `write_fast()` on Linux and Android. The
|
||||
/// function `splice()` is used to move data between two file descriptors
|
||||
/// without copying between kernel- and userspace. This results in a large
|
||||
/// speedup.
|
||||
///
|
||||
/// The `bool` in the result value indicates if we need to fall back to normal
|
||||
/// copying or not. False means we don't have to.
|
||||
#[inline]
|
||||
pub(super) fn write_fast_using_splice<R: Read>(
|
||||
handle: &mut InputHandle<R>,
|
||||
write_fd: RawFd,
|
||||
) -> CatResult<bool> {
|
||||
let (pipe_rd, pipe_wr) = match pipe() {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
// It is very rare that creating a pipe fails, but it can happen.
|
||||
return Ok(true);
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match splice(
|
||||
handle.file_descriptor,
|
||||
None,
|
||||
pipe_wr,
|
||||
None,
|
||||
BUF_SIZE,
|
||||
SpliceFFlags::empty(),
|
||||
) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
if splice_exact(pipe_rd, write_fd, n).is_err() {
|
||||
// If the first splice manages to copy to the intermediate
|
||||
// pipe, but the second splice to stdout fails for some reason
|
||||
// we can recover by copying the data that we have from the
|
||||
// intermediate pipe to stdout using normal read/write. Then
|
||||
// we tell the caller to fall back.
|
||||
copy_exact(pipe_rd, write_fd, n)?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splice wrapper which handles short writes.
|
||||
#[inline]
|
||||
fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> {
|
||||
let mut left = num_bytes;
|
||||
loop {
|
||||
let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?;
|
||||
left -= written;
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Caller must ensure that `num_bytes <= BUF_SIZE`, otherwise this function
|
||||
/// will panic. The way we use this function in `write_fast_using_splice`
|
||||
/// above is safe because `splice` is set to write at most `BUF_SIZE` to the
|
||||
/// pipe.
|
||||
#[inline]
|
||||
fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> {
|
||||
let mut left = num_bytes;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
loop {
|
||||
let read = unistd::read(read_fd, &mut buf[..left])?;
|
||||
let written = unistd::write(write_fd, &mut buf[..read])?;
|
||||
left -= written;
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
34
src/uu/ls/BENCHMARKING.md
Normal file
34
src/uu/ls/BENCHMARKING.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Benchmarking ls
|
||||
|
||||
ls majorly involves fetching a lot of details (depending upon what details are requested, eg. time/date, inode details, etc) for each path using system calls. Ideally, any system call should be done only once for each of the paths - not adhering to this principle leads to a lot of system call overhead multiplying and bubbling up, especially for recursive ls, therefore it is important to always benchmark multiple scenarios.
|
||||
This is an overwiew over what was benchmarked, and if you make changes to `ls`, you are encouraged to check
|
||||
how performance was affected for the workloads listed below. Feel free to add other workloads to the
|
||||
list that we should improve / make sure not to regress.
|
||||
|
||||
Run `cargo build --release` before benchmarking after you make a change!
|
||||
|
||||
## Simple recursive ls
|
||||
|
||||
- Get a large tree, for example linux kernel source tree.
|
||||
- Benchmark simple recursive ls with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -R tree > /dev/null"`.
|
||||
|
||||
## Recursive ls with all and long options
|
||||
|
||||
- Same tree as above
|
||||
- Benchmark recursive ls with -al -R options with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"`.
|
||||
|
||||
## Comparing with GNU ls
|
||||
|
||||
Hyperfine accepts multiple commands to run and will compare them. To compare performance with GNU ls
|
||||
duplicate the string you passed to hyperfine but remove the `target/release/coreutils` bit from it.
|
||||
|
||||
Example: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"` becomes
|
||||
`hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null" "ls -al -R tree > /dev/null"`
|
||||
(This assumes GNU ls is installed as `ls`)
|
||||
|
||||
This can also be used to compare with version of ls built before your changes to ensure your change does not regress this
|
||||
|
||||
## Checking system call count
|
||||
|
||||
- Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems.
|
||||
- Example: `strace -c target/release/coreutils ls -al -R tree`
|
|
@ -1038,12 +1038,43 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
list(locs, Config::from(matches))
|
||||
}
|
||||
|
||||
/// Represents a Path along with it's associated data
|
||||
/// Any data that will be reused several times makes sense to be added to this structure
|
||||
/// Caching data here helps eliminate redundant syscalls to fetch same information
|
||||
struct PathData {
|
||||
// Result<MetaData> got from symlink_metadata() or metadata() based on config
|
||||
md: std::io::Result<Metadata>,
|
||||
// String formed from get_lossy_string() for the path
|
||||
lossy_string: String,
|
||||
// Name of the file - will be empty for . or ..
|
||||
file_name: String,
|
||||
// PathBuf that all above data corresponds to
|
||||
p_buf: PathBuf,
|
||||
}
|
||||
|
||||
impl PathData {
|
||||
fn new(p_buf: PathBuf, config: &Config, command_line: bool) -> Self {
|
||||
let md = get_metadata(&p_buf, config, command_line);
|
||||
let lossy_string = p_buf.to_string_lossy().into_owned();
|
||||
let name = p_buf
|
||||
.file_name()
|
||||
.map_or(String::new(), |s| s.to_string_lossy().into_owned());
|
||||
Self {
|
||||
md,
|
||||
lossy_string,
|
||||
file_name: name,
|
||||
p_buf,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list(locs: Vec<String>, config: Config) -> i32 {
|
||||
let number_of_locs = locs.len();
|
||||
|
||||
let mut files = Vec::<PathBuf>::new();
|
||||
let mut dirs = Vec::<PathBuf>::new();
|
||||
let mut files = Vec::<PathData>::new();
|
||||
let mut dirs = Vec::<PathData>::new();
|
||||
let mut has_failed = false;
|
||||
|
||||
for loc in locs {
|
||||
let p = PathBuf::from(&loc);
|
||||
if !p.exists() {
|
||||
|
@ -1054,36 +1085,28 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
|
|||
continue;
|
||||
}
|
||||
|
||||
let show_dir_contents = if !config.directory {
|
||||
match config.dereference {
|
||||
Dereference::None => {
|
||||
if let Ok(md) = p.symlink_metadata() {
|
||||
md.is_dir()
|
||||
} else {
|
||||
show_error!("'{}': {}", &loc, "No such file or directory");
|
||||
has_failed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => p.is_dir(),
|
||||
}
|
||||
let path_data = PathData::new(p, &config, true);
|
||||
|
||||
let show_dir_contents = if let Ok(md) = path_data.md.as_ref() {
|
||||
!config.directory && md.is_dir()
|
||||
} else {
|
||||
has_failed = true;
|
||||
false
|
||||
};
|
||||
|
||||
if show_dir_contents {
|
||||
dirs.push(p);
|
||||
dirs.push(path_data);
|
||||
} else {
|
||||
files.push(p);
|
||||
files.push(path_data);
|
||||
}
|
||||
}
|
||||
sort_entries(&mut files, &config);
|
||||
display_items(&files, None, &config, true);
|
||||
display_items(&files, None, &config);
|
||||
|
||||
sort_entries(&mut dirs, &config);
|
||||
for dir in dirs {
|
||||
if number_of_locs > 1 {
|
||||
println!("\n{}:", dir.to_string_lossy());
|
||||
println!("\n{}:", dir.lossy_string);
|
||||
}
|
||||
enter_directory(&dir, &config);
|
||||
}
|
||||
|
@ -1094,22 +1117,22 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
|
|||
}
|
||||
}
|
||||
|
||||
fn sort_entries(entries: &mut Vec<PathBuf>, config: &Config) {
|
||||
fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
|
||||
match config.sort {
|
||||
Sort::Time => entries.sort_by_key(|k| {
|
||||
Reverse(
|
||||
get_metadata(k, false)
|
||||
k.md.as_ref()
|
||||
.ok()
|
||||
.and_then(|md| get_system_time(&md, config))
|
||||
.unwrap_or(UNIX_EPOCH),
|
||||
)
|
||||
}),
|
||||
Sort::Size => {
|
||||
entries.sort_by_key(|k| Reverse(get_metadata(k, false).map(|md| md.len()).unwrap_or(0)))
|
||||
entries.sort_by_key(|k| Reverse(k.md.as_ref().map(|md| md.len()).unwrap_or(0)))
|
||||
}
|
||||
// The default sort in GNU ls is case insensitive
|
||||
Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()),
|
||||
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)),
|
||||
Sort::Name => entries.sort_by_key(|k| k.file_name.to_lowercase()),
|
||||
Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
|
||||
Sort::None => {}
|
||||
}
|
||||
|
||||
|
@ -1143,32 +1166,57 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
fn enter_directory(dir: &Path, config: &Config) {
|
||||
let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect));
|
||||
|
||||
entries.retain(|e| should_display(e, config));
|
||||
|
||||
let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect();
|
||||
sort_entries(&mut entries, config);
|
||||
|
||||
if config.files == Files::All {
|
||||
let mut display_entries = entries.clone();
|
||||
display_entries.insert(0, dir.join(".."));
|
||||
display_entries.insert(0, dir.join("."));
|
||||
display_items(&display_entries, Some(dir), config, false);
|
||||
fn enter_directory(dir: &PathData, config: &Config) {
|
||||
let mut entries: Vec<_> = if config.files == Files::All {
|
||||
vec![
|
||||
PathData::new(dir.p_buf.join("."), config, false),
|
||||
PathData::new(dir.p_buf.join(".."), config, false),
|
||||
]
|
||||
} else {
|
||||
display_items(&entries, Some(dir), config, false);
|
||||
}
|
||||
vec![]
|
||||
};
|
||||
|
||||
let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf))
|
||||
.map(|res| safe_unwrap!(res))
|
||||
.filter(|e| should_display(e, config))
|
||||
.map(|e| PathData::new(DirEntry::path(&e), config, false))
|
||||
.collect();
|
||||
|
||||
sort_entries(&mut temp, config);
|
||||
|
||||
entries.append(&mut temp);
|
||||
|
||||
display_items(&entries, Some(&dir.p_buf), config);
|
||||
|
||||
if config.recursive {
|
||||
for e in entries.iter().filter(|p| p.is_dir()) {
|
||||
println!("\n{}:", e.to_string_lossy());
|
||||
for e in entries
|
||||
.iter()
|
||||
.skip(if config.files == Files::All { 2 } else { 0 })
|
||||
.filter(|p| p.md.as_ref().map(|md| md.is_dir()).unwrap_or(false))
|
||||
{
|
||||
println!("\n{}:", e.lossy_string);
|
||||
enter_directory(&e, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result<Metadata> {
|
||||
fn get_metadata(entry: &Path, config: &Config, command_line: bool) -> std::io::Result<Metadata> {
|
||||
let dereference = match &config.dereference {
|
||||
Dereference::All => true,
|
||||
Dereference::Args => command_line,
|
||||
Dereference::DirArgs => {
|
||||
if command_line {
|
||||
if let Ok(md) = entry.metadata() {
|
||||
md.is_dir()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Dereference::None => false,
|
||||
};
|
||||
if dereference {
|
||||
entry.metadata().or_else(|_| entry.symlink_metadata())
|
||||
} else {
|
||||
|
@ -1176,8 +1224,8 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result<Metadata> {
|
|||
}
|
||||
}
|
||||
|
||||
fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) {
|
||||
if let Ok(md) = get_metadata(entry, false) {
|
||||
fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
|
||||
if let Ok(md) = entry.md.as_ref() {
|
||||
(
|
||||
display_symlink_count(&md).len(),
|
||||
display_file_size(&md, config).len(),
|
||||
|
@ -1191,7 +1239,7 @@ fn pad_left(string: String, count: usize) -> String {
|
|||
format!("{:>width$}", string, width = count)
|
||||
}
|
||||
|
||||
fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, command_line: bool) {
|
||||
fn display_items(items: &[PathData], strip: Option<&Path>, config: &Config) {
|
||||
if config.format == Format::Long {
|
||||
let (mut max_links, mut max_size) = (1, 1);
|
||||
for item in items {
|
||||
|
@ -1200,18 +1248,18 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, comma
|
|||
max_size = size.max(max_size);
|
||||
}
|
||||
for item in items {
|
||||
display_item_long(item, strip, max_links, max_size, config, command_line);
|
||||
display_item_long(item, strip, max_links, max_size, config);
|
||||
}
|
||||
} else {
|
||||
let names = items.iter().filter_map(|i| {
|
||||
let md = get_metadata(i, false);
|
||||
let md = i.md.as_ref();
|
||||
match md {
|
||||
Err(e) => {
|
||||
let filename = get_file_name(i, strip);
|
||||
let filename = get_file_name(&i.p_buf, strip);
|
||||
show_error!("'{}': {}", filename, e);
|
||||
None
|
||||
}
|
||||
Ok(md) => Some(display_file_name(&i, strip, &md, config)),
|
||||
Ok(md) => Some(display_file_name(&i.p_buf, strip, &md, config)),
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1271,33 +1319,15 @@ fn display_grid(names: impl Iterator<Item = Cell>, width: u16, direction: Direct
|
|||
use uucore::fs::display_permissions;
|
||||
|
||||
fn display_item_long(
|
||||
item: &Path,
|
||||
item: &PathData,
|
||||
strip: Option<&Path>,
|
||||
max_links: usize,
|
||||
max_size: usize,
|
||||
config: &Config,
|
||||
command_line: bool,
|
||||
) {
|
||||
let dereference = match &config.dereference {
|
||||
Dereference::All => true,
|
||||
Dereference::Args => command_line,
|
||||
Dereference::DirArgs => {
|
||||
if command_line {
|
||||
if let Ok(md) = item.metadata() {
|
||||
md.is_dir()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Dereference::None => false,
|
||||
};
|
||||
|
||||
let md = match get_metadata(item, dereference) {
|
||||
let md = match &item.md {
|
||||
Err(e) => {
|
||||
let filename = get_file_name(&item, strip);
|
||||
let filename = get_file_name(&item.p_buf, strip);
|
||||
show_error!("{}: {}", filename, e);
|
||||
return;
|
||||
}
|
||||
|
@ -1336,7 +1366,7 @@ fn display_item_long(
|
|||
" {} {} {}",
|
||||
pad_left(display_file_size(&md, config), max_size),
|
||||
display_date(&md, config),
|
||||
display_file_name(&item, strip, &md, config).contents,
|
||||
display_file_name(&item.p_buf, strip, &md, config).contents,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1348,14 +1378,50 @@ fn get_inode(metadata: &Metadata) -> String {
|
|||
// Currently getpwuid is `linux` target only. If it's broken out into
|
||||
// a posix-compliant attribute this can be updated...
|
||||
#[cfg(unix)]
|
||||
use std::sync::Mutex;
|
||||
#[cfg(unix)]
|
||||
use uucore::entries;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn cached_uid2usr(uid: u32) -> String {
|
||||
lazy_static! {
|
||||
static ref UID_CACHE: Mutex<HashMap<u32, String>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
let mut uid_cache = UID_CACHE.lock().unwrap();
|
||||
match uid_cache.get(&uid) {
|
||||
Some(usr) => usr.clone(),
|
||||
None => {
|
||||
let usr = entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string());
|
||||
uid_cache.insert(uid, usr.clone());
|
||||
usr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn display_uname(metadata: &Metadata, config: &Config) -> String {
|
||||
if config.long.numeric_uid_gid {
|
||||
metadata.uid().to_string()
|
||||
} else {
|
||||
entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string())
|
||||
cached_uid2usr(metadata.uid())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn cached_gid2grp(gid: u32) -> String {
|
||||
lazy_static! {
|
||||
static ref GID_CACHE: Mutex<HashMap<u32, String>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
let mut gid_cache = GID_CACHE.lock().unwrap();
|
||||
match gid_cache.get(&gid) {
|
||||
Some(grp) => grp.clone(),
|
||||
None => {
|
||||
let grp = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string());
|
||||
gid_cache.insert(gid, grp.clone());
|
||||
grp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1364,7 +1430,7 @@ fn display_group(metadata: &Metadata, config: &Config) -> String {
|
|||
if config.long.numeric_uid_gid {
|
||||
metadata.gid().to_string()
|
||||
} else {
|
||||
entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string())
|
||||
cached_gid2grp(metadata.gid())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::common::util::*;
|
||||
#[cfg(unix)]
|
||||
use std::fs::OpenOptions;
|
||||
#[cfg(unix)]
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
|
@ -54,7 +57,6 @@ fn test_no_options_big_input() {
|
|||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_fifo_symlink() {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::thread;
|
||||
|
||||
|
@ -85,6 +87,74 @@ fn test_fifo_symlink() {
|
|||
thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_piped_to_regular_file() {
|
||||
use std::fs::read_to_string;
|
||||
|
||||
for &append in &[true, false] {
|
||||
let s = TestScenario::new(util_name!());
|
||||
let file_path = s.fixtures.plus("file.txt");
|
||||
|
||||
{
|
||||
let file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.append(append)
|
||||
.open(&file_path)
|
||||
.unwrap();
|
||||
|
||||
s.ucmd()
|
||||
.set_stdout(file)
|
||||
.pipe_in_fixture("alpha.txt")
|
||||
.succeeds();
|
||||
}
|
||||
let contents = read_to_string(&file_path).unwrap();
|
||||
assert_eq!(contents, "abcde\nfghij\nklmno\npqrst\nuvwxyz\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_piped_to_dev_null() {
|
||||
for &append in &[true, false] {
|
||||
let s = TestScenario::new(util_name!());
|
||||
{
|
||||
let dev_null = OpenOptions::new()
|
||||
.write(true)
|
||||
.append(append)
|
||||
.open("/dev/null")
|
||||
.unwrap();
|
||||
|
||||
s.ucmd()
|
||||
.set_stdout(dev_null)
|
||||
.pipe_in_fixture("alpha.txt")
|
||||
.succeeds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
|
||||
fn test_piped_to_dev_full() {
|
||||
for &append in &[true, false] {
|
||||
let s = TestScenario::new(util_name!());
|
||||
{
|
||||
let dev_full = OpenOptions::new()
|
||||
.write(true)
|
||||
.append(append)
|
||||
.open("/dev/full")
|
||||
.unwrap();
|
||||
|
||||
s.ucmd()
|
||||
.set_stdout(dev_full)
|
||||
.pipe_in_fixture("alpha.txt")
|
||||
.fails()
|
||||
.stderr_contains(&"No space left on device".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_directory() {
|
||||
let s = TestScenario::new(util_name!());
|
||||
|
@ -330,22 +400,29 @@ fn test_domain_socket() {
|
|||
use std::thread;
|
||||
use tempdir::TempDir;
|
||||
use unix_socket::UnixListener;
|
||||
use std::sync::{Barrier, Arc};
|
||||
|
||||
let dir = TempDir::new("unix_socket").expect("failed to create dir");
|
||||
let socket_path = dir.path().join("sock");
|
||||
let listener = UnixListener::bind(&socket_path).expect("failed to create socket");
|
||||
|
||||
// use a barrier to ensure we don't run cat before the listener is setup
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
let barrier2 = Arc::clone(&barrier);
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
let mut stream = listener.accept().expect("failed to accept connection").0;
|
||||
barrier2.wait();
|
||||
stream
|
||||
.write_all(b"a\tb")
|
||||
.expect("failed to write test data");
|
||||
});
|
||||
|
||||
new_ucmd!()
|
||||
.args(&[socket_path])
|
||||
.succeeds()
|
||||
.stdout_only("a\tb");
|
||||
let child = new_ucmd!().args(&[socket_path]).run_no_wait();
|
||||
barrier.wait();
|
||||
let stdout = &child.wait_with_output().unwrap().stdout.clone();
|
||||
let output = String::from_utf8_lossy(&stdout);
|
||||
assert_eq!("a\tb", output);
|
||||
|
||||
thread.join().unwrap();
|
||||
}
|
||||
|
|
|
@ -109,6 +109,14 @@ fn test_ls_width() {
|
|||
.arg("-w=bad")
|
||||
.fails()
|
||||
.stderr_contains("invalid line width");
|
||||
|
||||
for option in &["-w 1a", "-w=1a", "--width=1a", "--width 1a"] {
|
||||
scene
|
||||
.ucmd()
|
||||
.args(&option.split(" ").collect::<Vec<_>>())
|
||||
.fails()
|
||||
.stderr_only("ls: error: invalid line width: ‘1a’");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -696,8 +696,11 @@ pub struct UCommand {
|
|||
comm_string: String,
|
||||
tmpd: Option<Rc<TempDir>>,
|
||||
has_run: bool,
|
||||
stdin: Option<Vec<u8>>,
|
||||
ignore_stdin_write_error: bool,
|
||||
stdin: Option<Stdio>,
|
||||
stdout: Option<Stdio>,
|
||||
stderr: Option<Stdio>,
|
||||
bytes_into_stdin: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl UCommand {
|
||||
|
@ -726,8 +729,11 @@ impl UCommand {
|
|||
cmd
|
||||
},
|
||||
comm_string: String::from(arg.as_ref().to_str().unwrap()),
|
||||
stdin: None,
|
||||
ignore_stdin_write_error: false,
|
||||
bytes_into_stdin: None,
|
||||
stdin: None,
|
||||
stdout: None,
|
||||
stderr: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -738,6 +744,21 @@ impl UCommand {
|
|||
ucmd
|
||||
}
|
||||
|
||||
pub fn set_stdin<T: Into<Stdio>>(&mut self, stdin: T) -> &mut UCommand {
|
||||
self.stdin = Some(stdin.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_stdout<T: Into<Stdio>>(&mut self, stdout: T) -> &mut UCommand {
|
||||
self.stdout = Some(stdout.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_stderr<T: Into<Stdio>>(&mut self, stderr: T) -> &mut UCommand {
|
||||
self.stderr = Some(stderr.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a parameter to the invocation. Path arguments are treated relative
|
||||
/// to the test environment directory.
|
||||
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut UCommand {
|
||||
|
@ -767,10 +788,10 @@ impl UCommand {
|
|||
|
||||
/// provides stdinput to feed in to the command when spawned
|
||||
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, input: T) -> &mut UCommand {
|
||||
if self.stdin.is_some() {
|
||||
if self.bytes_into_stdin.is_some() {
|
||||
panic!("{}", MULTIPLE_STDIN_MEANINGLESS);
|
||||
}
|
||||
self.stdin = Some(input.into());
|
||||
self.bytes_into_stdin = Some(input.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -784,7 +805,7 @@ impl UCommand {
|
|||
/// This is typically useful to test non-standard workflows
|
||||
/// like feeding something to a command that does not read it
|
||||
pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand {
|
||||
if self.stdin.is_none() {
|
||||
if self.bytes_into_stdin.is_none() {
|
||||
panic!("{}", NO_STDIN_MEANINGLESS);
|
||||
}
|
||||
self.ignore_stdin_write_error = true;
|
||||
|
@ -813,13 +834,13 @@ impl UCommand {
|
|||
log_info("run", &self.comm_string);
|
||||
let mut child = self
|
||||
.raw
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stdin(self.stdin.take().unwrap_or_else(|| Stdio::piped()))
|
||||
.stdout(self.stdout.take().unwrap_or_else(|| Stdio::piped()))
|
||||
.stderr(self.stderr.take().unwrap_or_else(|| Stdio::piped()))
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
if let Some(ref input) = self.stdin {
|
||||
if let Some(ref input) = self.bytes_into_stdin {
|
||||
let write_result = child
|
||||
.stdin
|
||||
.take()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue