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

Merge branch 'main' into feat-refactor-expr

This commit is contained in:
Arpit Bhadauria 2023-12-11 01:19:40 +00:00
commit 17f2b830d8
31 changed files with 1279 additions and 641 deletions

View file

@ -39,7 +39,7 @@ jobs:
- name: Run sccache-cache - name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3 uses: mozilla-actions/sccache-action@v0.0.3
- name: Prepare, build and test - name: Prepare, build and test
uses: vmactions/freebsd-vm@v1.0.2 uses: vmactions/freebsd-vm@v1.0.5
with: with:
usesh: true usesh: true
sync: rsync sync: rsync
@ -131,7 +131,7 @@ jobs:
- name: Run sccache-cache - name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3 uses: mozilla-actions/sccache-action@v0.0.3
- name: Prepare, build and test - name: Prepare, build and test
uses: vmactions/freebsd-vm@v1.0.2 uses: vmactions/freebsd-vm@v1.0.5
with: with:
usesh: true usesh: true
sync: rsync sync: rsync

View file

@ -46,6 +46,8 @@ jobs:
- { name: fuzz_date, should_pass: false } - { name: fuzz_date, should_pass: false }
- { name: fuzz_expr, should_pass: true } - { name: fuzz_expr, should_pass: true }
- { name: fuzz_printf, should_pass: false } - { name: fuzz_printf, should_pass: false }
- { name: fuzz_echo, should_pass: false }
- { name: fuzz_seq, should_pass: false }
- { name: fuzz_parse_glob, should_pass: true } - { name: fuzz_parse_glob, should_pass: true }
- { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_size, should_pass: true }
- { name: fuzz_parse_time, should_pass: true } - { name: fuzz_parse_time, should_pass: true }

9
Cargo.lock generated
View file

@ -1409,9 +1409,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.18.0" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "onig" name = "onig"
@ -2618,6 +2618,7 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"glob", "glob",
"hostname",
"lscolors", "lscolors",
"number_prefix", "number_prefix",
"once_cell", "once_cell",
@ -3518,9 +3519,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]] [[package]]
name = "xattr" name = "xattr"
version = "1.0.1" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" checksum = "fbc6ab6ec1907d1a901cdbcd2bd4cb9e7d64ce5c9739cbb97d3c391acd8c7fae"
dependencies = [ dependencies = [
"libc", "libc",
] ]

View file

@ -284,11 +284,12 @@ fundu = "2.0.0"
gcd = "2.3" gcd = "2.3"
glob = "0.3.1" glob = "0.3.1"
half = "2.3" half = "2.3"
hostname = "0.3"
indicatif = "0.17" indicatif = "0.17"
itertools = "0.12.0" itertools = "0.12.0"
libc = "0.2.150" libc = "0.2.150"
lscolors = { version = "0.16.0", default-features = false, features = [ lscolors = { version = "0.16.0", default-features = false, features = [
"nu-ansi-term", "gnu_legacy",
] } ] }
memchr = "2" memchr = "2"
memmap2 = "0.9" memmap2 = "0.9"
@ -298,7 +299,7 @@ notify = { version = "=6.0.1", features = ["macos_kqueue"] }
num-bigint = "0.4.4" num-bigint = "0.4.4"
num-traits = "0.2.17" num-traits = "0.2.17"
number_prefix = "0.4" number_prefix = "0.4"
once_cell = "1.18.0" once_cell = "1.19.0"
onig = { version = "~6.4", default-features = false } onig = { version = "~6.4", default-features = false }
parse_datetime = "0.5.0" parse_datetime = "0.5.0"
phf = "0.11.2" phf = "0.11.2"
@ -329,7 +330,7 @@ utf-8 = "0.7.6"
walkdir = "2.4" walkdir = "2.4"
winapi-util = "0.1.6" winapi-util = "0.1.6"
windows-sys = { version = "0.48.0", default-features = false } windows-sys = { version = "0.48.0", default-features = false }
xattr = "1.0.1" xattr = "1.1.1"
zip = { version = "0.6.6", default-features = false, features = ["deflate"] } zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
hex = "0.4.3" hex = "0.4.3"

View file

@ -17,7 +17,8 @@ uu_date = { path = "../src/uu/date/" }
uu_test = { path = "../src/uu/test/" } uu_test = { path = "../src/uu/test/" }
uu_expr = { path = "../src/uu/expr/" } uu_expr = { path = "../src/uu/expr/" }
uu_printf = { path = "../src/uu/printf/" } uu_printf = { path = "../src/uu/printf/" }
uu_echo = { path = "../src/uu/echo/" }
uu_seq = { path = "../src/uu/seq/" }
# Prevent this from interfering with workspaces # Prevent this from interfering with workspaces
[workspace] [workspace]
@ -35,6 +36,18 @@ path = "fuzz_targets/fuzz_printf.rs"
test = false test = false
doc = false doc = false
[[bin]]
name = "fuzz_echo"
path = "fuzz_targets/fuzz_echo.rs"
test = false
doc = false
[[bin]]
name = "fuzz_seq"
path = "fuzz_targets/fuzz_seq.rs"
test = false
doc = false
[[bin]] [[bin]]
name = "fuzz_expr" name = "fuzz_expr"
path = "fuzz_targets/fuzz_expr.rs" path = "fuzz_targets/fuzz_expr.rs"

View file

@ -0,0 +1,89 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use uu_echo::uumain;
use rand::prelude::SliceRandom;
use rand::Rng;
use std::ffi::OsString;
mod fuzz_common;
use crate::fuzz_common::CommandResult;
use crate::fuzz_common::{
compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd,
};
static CMD_PATH: &str = "echo";
fn generate_echo() -> String {
let mut rng = rand::thread_rng();
let mut echo_str = String::new();
// Randomly decide whether to include options
let include_n = rng.gen_bool(0.1); // 10% chance
let include_e = rng.gen_bool(0.1); // 10% chance
let include_E = rng.gen_bool(0.1); // 10% chance
if include_n {
echo_str.push_str("-n ");
}
if include_e {
echo_str.push_str("-e ");
}
if include_E {
echo_str.push_str("-E ");
}
// Add a random string
echo_str.push_str(&generate_random_string(rng.gen_range(1..=10)));
// Include escape sequences if -e is enabled
if include_e {
// Add a 10% chance of including an escape sequence
if rng.gen_bool(0.1) {
echo_str.push_str(&generate_escape_sequence(&mut rng));
}
}
echo_str
}
fn generate_escape_sequence(rng: &mut impl Rng) -> String {
let escape_sequences = [
"\\\\", "\\a", "\\b", "\\c", "\\e", "\\f", "\\n", "\\r", "\\t", "\\v", "\\0NNN", "\\xHH",
];
// \0NNN and \xHH need more work
escape_sequences.choose(rng).unwrap().to_string()
}
fuzz_target!(|_data: &[u8]| {
let echo_input = generate_echo();
let mut args = vec![OsString::from("echo")];
args.extend(echo_input.split_whitespace().map(OsString::from));
let rust_result = generate_and_run_uumain(&args, uumain);
let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false) {
Ok(result) => result,
Err(error_result) => {
eprintln!("Failed to run GNU command:");
eprintln!("Stderr: {}", error_result.stderr);
eprintln!("Exit Code: {}", error_result.exit_code);
CommandResult {
stdout: String::new(),
stderr: error_result.stderr,
exit_code: error_result.exit_code,
}
}
};
compare_result(
"echo",
&format!("{:?}", &args[1..]),
&rust_result.stdout,
&gnu_result.stdout,
&rust_result.stderr,
&gnu_result.stderr,
rust_result.exit_code,
gnu_result.exit_code,
true,
);
});

View file

@ -0,0 +1,78 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore parens
#![no_main]
use libfuzzer_sys::fuzz_target;
use uu_seq::uumain;
use rand::Rng;
use std::ffi::OsString;
mod fuzz_common;
use crate::fuzz_common::CommandResult;
use crate::fuzz_common::{
compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd,
};
static CMD_PATH: &str = "seq";
fn generate_seq() -> String {
let mut rng = rand::thread_rng();
// Generate 1 to 3 numbers for seq arguments
let arg_count = rng.gen_range(1..=3);
let mut args = Vec::new();
for _ in 0..arg_count {
if rng.gen_ratio(1, 100) {
// 1% chance to add a random string
args.push(generate_random_string(rng.gen_range(1..=10)));
} else {
// 99% chance to add a numeric value
match rng.gen_range(0..=3) {
0 => args.push(rng.gen_range(-10000..=10000).to_string()), // Large or small integers
1 => args.push(rng.gen_range(-100.0..100.0).to_string()), // Floating-point numbers
2 => args.push(rng.gen_range(-100..0).to_string()), // Negative integers
_ => args.push(rng.gen_range(1..=100).to_string()), // Regular integers
}
}
}
args.join(" ")
}
fuzz_target!(|_data: &[u8]| {
let seq = generate_seq();
let mut args = vec![OsString::from("seq")];
args.extend(seq.split_whitespace().map(OsString::from));
let rust_result = generate_and_run_uumain(&args, uumain);
let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false) {
Ok(result) => result,
Err(error_result) => {
eprintln!("Failed to run GNU command:");
eprintln!("Stderr: {}", error_result.stderr);
eprintln!("Exit Code: {}", error_result.exit_code);
CommandResult {
stdout: String::new(),
stderr: error_result.stderr,
exit_code: error_result.exit_code,
}
}
};
compare_result(
"seq",
&format!("{:?}", &args[1..]),
&rust_result.stdout,
&gnu_result.stdout,
&rust_result.stderr,
&gnu_result.stderr,
rust_result.exit_code,
gnu_result.exit_code,
false, // Set to true if you want to fail on stderr diff
);
});

View file

@ -16,7 +16,7 @@ path = "src/dircolors.rs"
[dependencies] [dependencies]
clap = { workspace = true } clap = { workspace = true }
uucore = { workspace = true } uucore = { workspace = true, features = ["colors"] }
[[bin]] [[bin]]
name = "dircolors" name = "dircolors"

View file

@ -1,225 +0,0 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) EIGHTBIT ETERM MULTIHARDLINK cpio dtterm jfbterm konsole kterm mlterm rmvb rxvt stat'able svgz tmux webm xspf COLORTERM tzst avif tzst mjpg mjpeg webp dpkg rpmnew rpmorig rpmsave
pub const INTERNAL_DB: &str = r#"# Configuration file for dircolors, a utility to help you set the
# LS_COLORS environment variable used by GNU ls with the --color option.
# Copyright (C) 1996-2022 Free Software Foundation, Inc.
# Copying and distribution of this file, with or without modification,
# are permitted provided the copyright notice and this notice are preserved.
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
# slackware version of dircolors) are recognized but ignored.
# Global config options can be specified before TERM or COLORTERM entries
# Below are TERM or COLORTERM entries, which can be glob patterns, which
# restrict following config to systems with matching environment variables.
COLORTERM ?*
TERM Eterm
TERM ansi
TERM *color*
TERM con[0-9]*x[0-9]*
TERM cons25
TERM console
TERM cygwin
TERM *direct*
TERM dtterm
TERM gnome
TERM hurd
TERM jfbterm
TERM konsole
TERM kterm
TERM linux
TERM linux-c
TERM mlterm
TERM putty
TERM rxvt*
TERM screen*
TERM st
TERM terminator
TERM tmux*
TERM vt100
TERM xterm*
# Below are the color init strings for the basic file types.
# One can use codes for 256 or more colors supported by modern terminals.
# The default color codes use the capabilities of an 8 color terminal
# with some additional attributes as per the following codes:
# Attribute codes:
# 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
# Text color codes:
# 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
# Background color codes:
# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
#NORMAL 00 # no color code at all
#FILE 00 # regular file: use no color at all
RESET 0 # reset to "normal" color
DIR 01;34 # directory
LINK 01;36 # symbolic link. (If you set this to 'target' instead of a
# numerical value, the color is as for the file pointed to.)
MULTIHARDLINK 00 # regular file with more than one link
FIFO 40;33 # pipe
SOCK 01;35 # socket
DOOR 01;35 # door
BLK 40;33;01 # block device driver
CHR 40;33;01 # character device driver
ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ...
MISSING 00 # ... and the files they point to
SETUID 37;41 # file that is setuid (u+s)
SETGID 30;43 # file that is setgid (g+s)
CAPABILITY 00 # file with capability (very expensive to lookup)
STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w)
OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky
STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable
# This is for files with execute permission:
EXEC 01;32
# List any file extensions like '.gz' or '.tar' that you would like ls
# to color below. Put the extension, a space, and the color init string.
# (and any comments you want to add after a '#')
# If you use DOS-style suffixes, you may want to uncomment the following:
#.cmd 01;32 # executables (bright green)
#.exe 01;32
#.com 01;32
#.btm 01;32
#.bat 01;32
# Or if you want to color scripts even if they do not have the
# executable bit actually set.
#.sh 01;32
#.csh 01;32
# archives or compressed (bright red)
.tar 01;31
.tgz 01;31
.arc 01;31
.arj 01;31
.taz 01;31
.lha 01;31
.lz4 01;31
.lzh 01;31
.lzma 01;31
.tlz 01;31
.txz 01;31
.tzo 01;31
.t7z 01;31
.zip 01;31
.z 01;31
.dz 01;31
.gz 01;31
.lrz 01;31
.lz 01;31
.lzo 01;31
.xz 01;31
.zst 01;31
.tzst 01;31
.bz2 01;31
.bz 01;31
.tbz 01;31
.tbz2 01;31
.tz 01;31
.deb 01;31
.rpm 01;31
.jar 01;31
.war 01;31
.ear 01;31
.sar 01;31
.rar 01;31
.alz 01;31
.ace 01;31
.zoo 01;31
.cpio 01;31
.7z 01;31
.rz 01;31
.cab 01;31
.wim 01;31
.swm 01;31
.dwm 01;31
.esd 01;31
# image formats
.avif 01;35
.jpg 01;35
.jpeg 01;35
.mjpg 01;35
.mjpeg 01;35
.gif 01;35
.bmp 01;35
.pbm 01;35
.pgm 01;35
.ppm 01;35
.tga 01;35
.xbm 01;35
.xpm 01;35
.tif 01;35
.tiff 01;35
.png 01;35
.svg 01;35
.svgz 01;35
.mng 01;35
.pcx 01;35
.mov 01;35
.mpg 01;35
.mpeg 01;35
.m2v 01;35
.mkv 01;35
.webm 01;35
.webp 01;35
.ogm 01;35
.mp4 01;35
.m4v 01;35
.mp4v 01;35
.vob 01;35
.qt 01;35
.nuv 01;35
.wmv 01;35
.asf 01;35
.rm 01;35
.rmvb 01;35
.flc 01;35
.avi 01;35
.fli 01;35
.flv 01;35
.gl 01;35
.dl 01;35
.xcf 01;35
.xwd 01;35
.yuv 01;35
.cgm 01;35
.emf 01;35
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.ogv 01;35
.ogx 01;35
# audio formats
.aac 00;36
.au 00;36
.flac 00;36
.m4a 00;36
.mid 00;36
.midi 00;36
.mka 00;36
.mp3 00;36
.mpc 00;36
.ogg 00;36
.ra 00;36
.wav 00;36
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.oga 00;36
.opus 00;36
.spx 00;36
.xspf 00;36
# backup files
*~ 00;90
*# 00;90
.bak 00;90
.old 00;90
.orig 00;90
.part 00;90
.rej 00;90
.swp 00;90
.tmp 00;90
.dpkg-dist 00;90
.dpkg-old 00;90
.ucf-dist 00;90
.ucf-new 00;90
.ucf-old 00;90
.rpmnew 00;90
.rpmorig 00;90
.rpmsave 00;90
# Subsequent TERM or COLORTERM entries, can be used to add / override
# config specific to those matching environment variables."#;

View file

@ -8,10 +8,12 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::env; use std::env;
use std::fs::File; use std::fs::File;
//use std::io::IsTerminal;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::path::Path; use std::path::Path;
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use uucore::colors::{FILE_ATTRIBUTE_CODES, FILE_COLORS, FILE_TYPES, TERMS};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::{help_about, help_section, help_usage}; use uucore::{help_about, help_section, help_usage};
@ -28,9 +30,6 @@ const USAGE: &str = help_usage!("dircolors.md");
const ABOUT: &str = help_about!("dircolors.md"); const ABOUT: &str = help_about!("dircolors.md");
const AFTER_HELP: &str = help_section!("after help", "dircolors.md"); const AFTER_HELP: &str = help_section!("after help", "dircolors.md");
mod colors;
use self::colors::INTERNAL_DB;
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub enum OutputFmt { pub enum OutputFmt {
Shell, Shell,
@ -57,6 +56,77 @@ pub fn guess_syntax() -> OutputFmt {
} }
} }
fn get_colors_format_strings(fmt: &OutputFmt) -> (String, String) {
let prefix = match fmt {
OutputFmt::Shell => "LS_COLORS='".to_string(),
OutputFmt::CShell => "setenv LS_COLORS '".to_string(),
OutputFmt::Display => String::new(),
OutputFmt::Unknown => unreachable!(),
};
let suffix = match fmt {
OutputFmt::Shell => "';\nexport LS_COLORS".to_string(),
OutputFmt::CShell => "'".to_string(),
OutputFmt::Display => String::new(),
OutputFmt::Unknown => unreachable!(),
};
(prefix, suffix)
}
pub fn generate_type_output(fmt: &OutputFmt) -> String {
match fmt {
OutputFmt::Display => FILE_TYPES
.iter()
.map(|&(_, key, val)| format!("\x1b[{}m{}\t{}\x1b[0m", val, key, val))
.collect::<Vec<String>>()
.join("\n"),
_ => {
// Existing logic for other formats
FILE_TYPES
.iter()
.map(|&(_, v1, v2)| format!("{}={}", v1, v2))
.collect::<Vec<String>>()
.join(":")
}
}
}
fn generate_ls_colors(fmt: &OutputFmt, sep: &str) -> String {
match fmt {
OutputFmt::Display => {
let mut display_parts = vec![];
let type_output = generate_type_output(fmt);
display_parts.push(type_output);
for &(extension, code) in FILE_COLORS {
let prefix = if extension.starts_with('*') { "" } else { "*" };
let formatted_extension =
format!("\x1b[{}m{}{}\t{}\x1b[0m", code, prefix, extension, code);
display_parts.push(formatted_extension);
}
display_parts.join("\n")
}
_ => {
// existing logic for other formats
let mut parts = vec![];
for &(extension, code) in FILE_COLORS {
let prefix = if extension.starts_with('*') { "" } else { "*" };
let formatted_extension = format!("{}{}", prefix, extension);
parts.push(format!("{}={}", formatted_extension, code));
}
let (prefix, suffix) = get_colors_format_strings(fmt);
let ls_colors = parts.join(sep);
format!(
"{}{}:{}:{}",
prefix,
generate_type_output(fmt),
ls_colors,
suffix
)
}
}
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_ignore(); let args = args.collect_ignore();
@ -97,7 +167,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
), ),
)); ));
} }
println!("{INTERNAL_DB}");
println!("{}", generate_dircolors_config());
return Ok(()); return Ok(());
} }
@ -125,7 +196,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let result; let result;
if files.is_empty() { if files.is_empty() {
result = parse(INTERNAL_DB.lines(), &out_format, ""); println!("{}", generate_ls_colors(&out_format, ":"));
return Ok(());
/*
// Check if data is being piped into the program
if std::io::stdin().is_terminal() {
// No data piped, use default behavior
println!("{}", generate_ls_colors(&out_format, ":"));
return Ok(());
} else {
// Data is piped, process the input from stdin
let fin = BufReader::new(std::io::stdin());
result = parse(fin.lines().map_while(Result::ok), &out_format, "-");
}
*/
} else if files.len() > 1 { } else if files.len() > 1 {
return Err(UUsageError::new( return Err(UUsageError::new(
1, 1,
@ -133,6 +217,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
)); ));
} else if files[0].eq("-") { } else if files[0].eq("-") {
let fin = BufReader::new(std::io::stdin()); let fin = BufReader::new(std::io::stdin());
// For example, for echo "owt 40;33"|dircolors -b -
result = parse(fin.lines().map_while(Result::ok), &out_format, files[0]); result = parse(fin.lines().map_while(Result::ok), &out_format, files[0]);
} else { } else {
let path = Path::new(files[0]); let path = Path::new(files[0]);
@ -276,69 +361,25 @@ enum ParseState {
Pass, Pass,
} }
use std::collections::HashMap;
use uucore::{format_usage, parse_glob}; use uucore::{format_usage, parse_glob};
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn parse<T>(lines: T, fmt: &OutputFmt, fp: &str) -> Result<String, String> fn parse<T>(user_input: T, fmt: &OutputFmt, fp: &str) -> Result<String, String>
where where
T: IntoIterator, T: IntoIterator,
T::Item: Borrow<str>, T::Item: Borrow<str>,
{ {
// 1790 > $(dircolors | wc -m)
let mut result = String::with_capacity(1790); let mut result = String::with_capacity(1790);
match fmt { let (prefix, suffix) = get_colors_format_strings(fmt);
OutputFmt::Shell => result.push_str("LS_COLORS='"),
OutputFmt::CShell => result.push_str("setenv LS_COLORS '"),
OutputFmt::Display => (),
OutputFmt::Unknown => unreachable!(),
}
let mut table: HashMap<&str, &str> = HashMap::with_capacity(48); result.push_str(&prefix);
table.insert("normal", "no");
table.insert("norm", "no");
table.insert("file", "fi");
table.insert("reset", "rs");
table.insert("dir", "di");
table.insert("lnk", "ln");
table.insert("link", "ln");
table.insert("symlink", "ln");
table.insert("orphan", "or");
table.insert("missing", "mi");
table.insert("fifo", "pi");
table.insert("pipe", "pi");
table.insert("sock", "so");
table.insert("blk", "bd");
table.insert("block", "bd");
table.insert("chr", "cd");
table.insert("char", "cd");
table.insert("door", "do");
table.insert("exec", "ex");
table.insert("left", "lc");
table.insert("leftcode", "lc");
table.insert("right", "rc");
table.insert("rightcode", "rc");
table.insert("end", "ec");
table.insert("endcode", "ec");
table.insert("suid", "su");
table.insert("setuid", "su");
table.insert("sgid", "sg");
table.insert("setgid", "sg");
table.insert("sticky", "st");
table.insert("other_writable", "ow");
table.insert("owr", "ow");
table.insert("sticky_other_writable", "tw");
table.insert("owt", "tw");
table.insert("capability", "ca");
table.insert("multihardlink", "mh");
table.insert("clrtoeol", "cl");
let term = env::var("TERM").unwrap_or_else(|_| "none".to_owned()); let term = env::var("TERM").unwrap_or_else(|_| "none".to_owned());
let term = term.as_str(); let term = term.as_str();
let mut state = ParseState::Global; let mut state = ParseState::Global;
for (num, line) in lines.into_iter().enumerate() { for (num, line) in user_input.into_iter().enumerate() {
let num = num + 1; let num = num + 1;
let line = line.borrow().purify(); let line = line.borrow().purify();
if line.is_empty() { if line.is_empty() {
@ -350,13 +391,13 @@ where
let (key, val) = line.split_two(); let (key, val) = line.split_two();
if val.is_empty() { if val.is_empty() {
return Err(format!( return Err(format!(
// The double space is what GNU is doing
"{}:{}: invalid line; missing second token", "{}:{}: invalid line; missing second token",
fp.maybe_quote(), fp.maybe_quote(),
num num
)); ));
} }
let lower = key.to_lowercase(); let lower = key.to_lowercase();
if lower == "term" || lower == "colorterm" { if lower == "term" || lower == "colorterm" {
if term.fnmatch(val) { if term.fnmatch(val) {
state = ParseState::Matched; state = ParseState::Matched;
@ -370,6 +411,8 @@ where
state = ParseState::Continue; state = ParseState::Continue;
} }
if state != ParseState::Pass { if state != ParseState::Pass {
let search_key = lower.as_str();
if key.starts_with('.') { if key.starts_with('.') {
if *fmt == OutputFmt::Display { if *fmt == OutputFmt::Display {
result.push_str(format!("\x1b[{val}m*{key}\t{val}\x1b[0m\n").as_str()); result.push_str(format!("\x1b[{val}m*{key}\t{val}\x1b[0m\n").as_str());
@ -384,7 +427,10 @@ where
} }
} else if lower == "options" || lower == "color" || lower == "eightbit" { } else if lower == "options" || lower == "color" || lower == "eightbit" {
// Slackware only. Ignore // Slackware only. Ignore
} else if let Some(s) = table.get(lower.as_str()) { } else if let Some((_, s)) = FILE_ATTRIBUTE_CODES
.iter()
.find(|&&(key, _)| key == search_key)
{
if *fmt == OutputFmt::Display { if *fmt == OutputFmt::Display {
result.push_str(format!("\x1b[{val}m{s}\t{val}\x1b[0m\n").as_str()); result.push_str(format!("\x1b[{val}m{s}\t{val}\x1b[0m\n").as_str());
} else { } else {
@ -402,15 +448,11 @@ where
} }
} }
match fmt { if fmt == &OutputFmt::Display {
OutputFmt::Shell => result.push_str("';\nexport LS_COLORS"), // remove latest "\n"
OutputFmt::CShell => result.push('\''), result.pop();
OutputFmt::Display => {
// remove latest "\n"
result.pop();
}
OutputFmt::Unknown => unreachable!(),
} }
result.push_str(&suffix);
Ok(result) Ok(result)
} }
@ -436,6 +478,58 @@ fn escape(s: &str) -> String {
result result
} }
pub fn generate_dircolors_config() -> String {
let mut config = String::new();
config.push_str(
"\
# Configuration file for dircolors, a utility to help you set the\n\
# LS_COLORS environment variable used by GNU ls with the --color option.\n\
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the\n\
# slackware version of dircolors) are recognized but ignored.\n\
# Global config options can be specified before TERM or COLORTERM entries\n\
# Below are TERM or COLORTERM entries, which can be glob patterns, which\n\
# restrict following config to systems with matching environment variables.\n\
",
);
config.push_str("COLORTERM ?*\n");
for term in TERMS {
config.push_str(&format!("TERM {}\n", term));
}
config.push_str(
"\
# Below are the color init strings for the basic file types.\n\
# One can use codes for 256 or more colors supported by modern terminals.\n\
# The default color codes use the capabilities of an 8 color terminal\n\
# with some additional attributes as per the following codes:\n\
# Attribute codes:\n\
# 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed\n\
# Text color codes:\n\
# 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white\n\
# Background color codes:\n\
# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white\n\
#NORMAL 00 # no color code at all\n\
#FILE 00 # regular file: use no color at all\n\
",
);
for (name, _, code) in FILE_TYPES {
config.push_str(&format!("{} {}\n", name, code));
}
config.push_str("# List any file extensions like '.gz' or '.tar' that you would like ls\n");
config.push_str("# to color below. Put the extension, a space, and the color init string.\n");
for (ext, color) in FILE_COLORS {
config.push_str(&format!("{} {}\n", ext, color));
}
config.push_str("# Subsequent TERM or COLORTERM entries, can be used to add / override\n");
config.push_str("# config specific to those matching environment variables.");
config
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::escape; use super::escape;

View file

@ -3,35 +3,30 @@
// 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.
use chrono::prelude::DateTime; use chrono::{DateTime, Local};
use chrono::Local; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use clap::ArgAction;
use clap::{crate_version, Arg, ArgMatches, Command};
use glob::Pattern; use glob::Pattern;
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
use std::fs; use std::error::Error;
use std::fs::File; use std::fmt::Display;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::fs::Metadata; use std::fs::Metadata;
use std::io::BufRead; use std::fs::{self, File};
use std::io::BufReader; use std::io::{BufRead, BufReader};
#[cfg(not(windows))] #[cfg(not(windows))]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
#[cfg(windows)] #[cfg(windows)]
use std::os::windows::fs::MetadataExt; use std::os::windows::fs::MetadataExt;
#[cfg(windows)] #[cfg(windows)]
use std::os::windows::io::AsRawHandle; use std::os::windows::io::AsRawHandle;
use std::path::Path; use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::{Duration, UNIX_EPOCH}; use std::time::{Duration, UNIX_EPOCH};
use std::{error::Error, fmt::Display};
use uucore::display::{print_verbatim, Quotable}; use uucore::display::{print_verbatim, Quotable};
use uucore::error::FromIo; use uucore::error::{FromIo, UError, UResult, USimpleError};
use uucore::error::{UError, UResult, USimpleError};
use uucore::line_ending::LineEnding; use uucore::line_ending::LineEnding;
use uucore::parse_glob; use uucore::parse_glob;
use uucore::parse_size::{parse_size_u64, ParseSizeError}; use uucore::parse_size::{parse_size_u64, ParseSizeError};
@ -81,17 +76,27 @@ const USAGE: &str = help_usage!("du.md");
// TODO: Support Z & Y (currently limited by size of u64) // TODO: Support Z & Y (currently limited by size of u64)
const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2), ('K', 1)]; const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2), ('K', 1)];
#[derive(Clone)] struct TraversalOptions {
struct Options {
all: bool, all: bool,
max_depth: Option<usize>,
total: bool,
separate_dirs: bool, separate_dirs: bool,
one_file_system: bool, one_file_system: bool,
dereference: Deref, dereference: Deref,
count_links: bool, count_links: bool,
inodes: bool,
verbose: bool, verbose: bool,
excludes: Vec<Pattern>,
}
struct StatPrinter {
total: bool,
inodes: bool,
max_depth: Option<usize>,
threshold: Option<Threshold>,
apparent_size: bool,
size_format: SizeFormat,
time: Option<Time>,
time_format: String,
line_ending: LineEnding,
summarize: bool,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
@ -101,6 +106,19 @@ enum Deref {
None, None,
} }
#[derive(Clone, Copy)]
enum Time {
Accessed,
Modified,
Created,
}
#[derive(Clone)]
enum SizeFormat {
Human(u64),
BlockSize(u64),
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)] #[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct FileInfo { struct FileInfo {
file_id: u128, file_id: u128,
@ -120,7 +138,7 @@ struct Stat {
} }
impl Stat { impl Stat {
fn new(path: &Path, options: &Options) -> std::io::Result<Self> { fn new(path: &Path, options: &TraversalOptions) -> std::io::Result<Self> {
// Determine whether to dereference (follow) the symbolic link // Determine whether to dereference (follow) the symbolic link
let should_dereference = match &options.dereference { let should_dereference = match &options.dereference {
Deref::All => true, Deref::All => true,
@ -278,26 +296,13 @@ fn read_block_size(s: Option<&str>) -> UResult<u64> {
} }
} }
fn choose_size(matches: &ArgMatches, stat: &Stat) -> u64 {
if matches.get_flag(options::INODES) {
stat.inodes
} else if matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES) {
stat.size
} else {
// The st_blocks field indicates the number of blocks allocated to the file, 512-byte units.
// See: http://linux.die.net/man/2/stat
stat.blocks * 512
}
}
// this takes `my_stat` to avoid having to stat files multiple times. // this takes `my_stat` to avoid having to stat files multiple times.
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn du( fn du(
mut my_stat: Stat, mut my_stat: Stat,
options: &Options, options: &TraversalOptions,
depth: usize, depth: usize,
seen_inodes: &mut HashSet<FileInfo>, seen_inodes: &mut HashSet<FileInfo>,
exclude: &[Pattern],
print_tx: &mpsc::Sender<UResult<StatPrintInfo>>, print_tx: &mpsc::Sender<UResult<StatPrintInfo>>,
) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> { ) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> {
if my_stat.is_dir { if my_stat.is_dir {
@ -317,7 +322,7 @@ fn du(
match Stat::new(&entry.path(), options) { match Stat::new(&entry.path(), options) {
Ok(this_stat) => { Ok(this_stat) => {
// We have an exclude list // We have an exclude list
for pattern in exclude { for pattern in &options.excludes {
// Look at all patterns with both short and long paths // Look at all patterns with both short and long paths
// if we have 'du foo' but search to exclude 'foo/bar' // if we have 'du foo' but search to exclude 'foo/bar'
// we need the full path // we need the full path
@ -353,14 +358,8 @@ fn du(
} }
} }
let this_stat = du( let this_stat =
this_stat, du(this_stat, options, depth + 1, seen_inodes, print_tx)?;
options,
depth + 1,
seen_inodes,
exclude,
print_tx,
)?;
if !options.separate_dirs { if !options.separate_dirs {
my_stat.size += this_stat.size; my_stat.size += this_stat.size;
@ -396,58 +395,12 @@ fn du(
Ok(my_stat) Ok(my_stat)
} }
fn convert_size_human(size: u64, multiplier: u64, _block_size: u64) -> String {
for &(unit, power) in &UNITS {
let limit = multiplier.pow(power);
if size >= limit {
return format!("{:.1}{}", (size as f64) / (limit as f64), unit);
}
}
if size == 0 {
return "0".to_string();
}
format!("{size}B")
}
fn convert_size_b(size: u64, _multiplier: u64, _block_size: u64) -> String {
format!("{}", ((size as f64) / (1_f64)).ceil())
}
fn convert_size_k(size: u64, multiplier: u64, _block_size: u64) -> String {
format!("{}", ((size as f64) / (multiplier as f64)).ceil())
}
fn convert_size_m(size: u64, multiplier: u64, _block_size: u64) -> String {
format!(
"{}",
((size as f64) / ((multiplier * multiplier) as f64)).ceil()
)
}
fn convert_size_other(size: u64, _multiplier: u64, block_size: u64) -> String {
format!("{}", ((size as f64) / (block_size as f64)).ceil())
}
fn get_convert_size_fn(matches: &ArgMatches) -> Box<dyn Fn(u64, u64, u64) -> String + Send> {
if matches.get_flag(options::HUMAN_READABLE) || matches.get_flag(options::SI) {
Box::new(convert_size_human)
} else if matches.get_flag(options::BYTES) {
Box::new(convert_size_b)
} else if matches.get_flag(options::BLOCK_SIZE_1K) {
Box::new(convert_size_k)
} else if matches.get_flag(options::BLOCK_SIZE_1M) {
Box::new(convert_size_m)
} else {
Box::new(convert_size_other)
}
}
#[derive(Debug)] #[derive(Debug)]
enum DuError { enum DuError {
InvalidMaxDepthArg(String), InvalidMaxDepthArg(String),
SummarizeDepthConflict(String), SummarizeDepthConflict(String),
InvalidTimeStyleArg(String), InvalidTimeStyleArg(String),
InvalidTimeArg(String), InvalidTimeArg,
InvalidGlob(String), InvalidGlob(String),
} }
@ -473,11 +426,9 @@ Try '{} --help' for more information.",
s.quote(), s.quote(),
uucore::execution_phrase() uucore::execution_phrase()
), ),
Self::InvalidTimeArg(s) => write!( Self::InvalidTimeArg => write!(
f, f,
"Invalid argument {} for --time. "'birth' and 'creation' arguments for --time are not supported on this platform.",
'birth' and 'creation' arguments are not supported on this platform.",
s.quote()
), ),
Self::InvalidGlob(s) => write!(f, "Invalid exclude syntax: {s}"), Self::InvalidGlob(s) => write!(f, "Invalid exclude syntax: {s}"),
} }
@ -492,7 +443,7 @@ impl UError for DuError {
Self::InvalidMaxDepthArg(_) Self::InvalidMaxDepthArg(_)
| Self::SummarizeDepthConflict(_) | Self::SummarizeDepthConflict(_)
| Self::InvalidTimeStyleArg(_) | Self::InvalidTimeStyleArg(_)
| Self::InvalidTimeArg(_) | Self::InvalidTimeArg
| Self::InvalidGlob(_) => 1, | Self::InvalidGlob(_) => 1,
} }
} }
@ -539,66 +490,17 @@ struct StatPrintInfo {
depth: usize, depth: usize,
} }
struct StatPrinter {
matches: ArgMatches,
threshold: Option<Threshold>,
summarize: bool,
time_format_str: String,
line_ending: LineEnding,
options: Options,
convert_size: Box<dyn Fn(u64) -> String + Send>,
}
impl StatPrinter { impl StatPrinter {
fn new(matches: ArgMatches, options: Options, summarize: bool) -> UResult<Self> { fn choose_size(&self, stat: &Stat) -> u64 {
let block_size = read_block_size( if self.inodes {
matches stat.inodes
.get_one::<String>(options::BLOCK_SIZE) } else if self.apparent_size {
.map(|s| s.as_str()), stat.size
)?;
let multiplier: u64 = if matches.get_flag(options::SI) {
1000
} else { } else {
1024 // The st_blocks field indicates the number of blocks allocated to the file, 512-byte units.
}; // See: http://linux.die.net/man/2/stat
stat.blocks * 512
let convert_size_fn = get_convert_size_fn(&matches); }
let convert_size: Box<dyn Fn(u64) -> String + Send> = if options.inodes {
Box::new(|size: u64| size.to_string())
} else {
Box::new(move |size: u64| convert_size_fn(size, multiplier, block_size))
};
let threshold = match matches.get_one::<String>(options::THRESHOLD) {
Some(s) => match Threshold::from_str(s) {
Ok(t) => Some(t),
Err(e) => {
return Err(USimpleError::new(
1,
format_error_message(&e, s, options::THRESHOLD),
))
}
},
None => None,
};
let time_format_str =
parse_time_style(matches.get_one::<String>("time-style").map(|s| s.as_str()))?
.to_string();
let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::NULL));
Ok(Self {
matches,
threshold,
summarize,
time_format_str,
line_ending,
options,
convert_size,
})
} }
fn print_stats(&self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> { fn print_stats(&self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> {
@ -609,7 +511,7 @@ impl StatPrinter {
match received { match received {
Ok(message) => match message { Ok(message) => match message {
Ok(stat_info) => { Ok(stat_info) => {
let size = choose_size(&self.matches, &stat_info.stat); let size = self.choose_size(&stat_info.stat);
if stat_info.depth == 0 { if stat_info.depth == 0 {
grand_total += size; grand_total += size;
@ -619,7 +521,6 @@ impl StatPrinter {
.threshold .threshold
.map_or(false, |threshold| threshold.should_exclude(size)) .map_or(false, |threshold| threshold.should_exclude(size))
&& self && self
.options
.max_depth .max_depth
.map_or(true, |max_depth| stat_info.depth <= max_depth) .map_or(true, |max_depth| stat_info.depth <= max_depth)
&& (!self.summarize || stat_info.depth == 0) && (!self.summarize || stat_info.depth == 0)
@ -633,29 +534,43 @@ impl StatPrinter {
} }
} }
if self.options.total { if self.total {
print!("{}\ttotal", (self.convert_size)(grand_total)); print!("{}\ttotal", self.convert_size(grand_total));
print!("{}", self.line_ending); print!("{}", self.line_ending);
} }
Ok(()) Ok(())
} }
fn convert_size(&self, size: u64) -> String {
if self.inodes {
return size.to_string();
}
match self.size_format {
SizeFormat::Human(multiplier) => {
if size == 0 {
return "0".to_string();
}
for &(unit, power) in &UNITS {
let limit = multiplier.pow(power);
if size >= limit {
return format!("{:.1}{}", (size as f64) / (limit as f64), unit);
}
}
format!("{size}B")
}
SizeFormat::BlockSize(block_size) => div_ceil(size, block_size).to_string(),
}
}
fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> { fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> {
if self.matches.contains_id(options::TIME) { if let Some(time) = self.time {
let tm = { let secs = get_time_secs(time, stat)?;
let secs = self let tm = DateTime::<Local>::from(UNIX_EPOCH + Duration::from_secs(secs));
.matches let time_str = tm.format(&self.time_format).to_string();
.get_one::<String>(options::TIME) print!("{}\t{}\t", self.convert_size(size), time_str);
.map(|s| get_time_secs(s, stat))
.transpose()?
.unwrap_or(stat.modified);
DateTime::<Local>::from(UNIX_EPOCH + Duration::from_secs(secs))
};
let time_str = tm.format(&self.time_format_str).to_string();
print!("{}\t{}\t", (self.convert_size)(size), time_str);
} else { } else {
print!("{}\t", (self.convert_size)(size)); print!("{}\t", self.convert_size(size));
} }
print_verbatim(&stat.path).unwrap(); print_verbatim(&stat.path).unwrap();
@ -665,6 +580,13 @@ impl StatPrinter {
} }
} }
// This can be replaced with u64::div_ceil once it is stabilized.
// This implementation approach is optimized for when `b` is a constant,
// particularly a power of two.
pub fn div_ceil(a: u64, b: u64) -> u64 {
(a + b - 1) / b
}
#[uucore::main] #[uucore::main]
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@ -690,10 +612,35 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
None => vec![PathBuf::from(".")], None => vec![PathBuf::from(".")],
}; };
let options = Options { let time = matches.contains_id(options::TIME).then(|| {
match matches.get_one::<String>(options::TIME).map(AsRef::as_ref) {
None | Some("ctime" | "status") => Time::Modified,
Some("access" | "atime" | "use") => Time::Accessed,
Some("birth" | "creation") => Time::Created,
_ => unreachable!("should be caught by clap"),
}
});
let size_format = if matches.get_flag(options::HUMAN_READABLE) {
SizeFormat::Human(1024)
} else if matches.get_flag(options::SI) {
SizeFormat::Human(1000)
} else if matches.get_flag(options::BYTES) {
SizeFormat::BlockSize(1)
} else if matches.get_flag(options::BLOCK_SIZE_1K) {
SizeFormat::BlockSize(1024)
} else if matches.get_flag(options::BLOCK_SIZE_1M) {
SizeFormat::BlockSize(1024 * 1024)
} else {
SizeFormat::BlockSize(read_block_size(
matches
.get_one::<String>(options::BLOCK_SIZE)
.map(AsRef::as_ref),
)?)
};
let traversal_options = TraversalOptions {
all: matches.get_flag(options::ALL), all: matches.get_flag(options::ALL),
max_depth,
total: matches.get_flag(options::TOTAL),
separate_dirs: matches.get_flag(options::SEPARATE_DIRS), separate_dirs: matches.get_flag(options::SEPARATE_DIRS),
one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM), one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
dereference: if matches.get_flag(options::DEREFERENCE) { dereference: if matches.get_flag(options::DEREFERENCE) {
@ -705,31 +652,49 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Deref::None Deref::None
}, },
count_links: matches.get_flag(options::COUNT_LINKS), count_links: matches.get_flag(options::COUNT_LINKS),
inodes: matches.get_flag(options::INODES),
verbose: matches.get_flag(options::VERBOSE), verbose: matches.get_flag(options::VERBOSE),
excludes: build_exclude_patterns(&matches)?,
}; };
if options.inodes let stat_printer = StatPrinter {
max_depth,
size_format,
summarize,
total: matches.get_flag(options::TOTAL),
inodes: matches.get_flag(options::INODES),
threshold: matches
.get_one::<String>(options::THRESHOLD)
.map(|s| {
Threshold::from_str(s).map_err(|e| {
USimpleError::new(1, format_error_message(&e, s, options::THRESHOLD))
})
})
.transpose()?,
apparent_size: matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES),
time,
time_format: parse_time_style(matches.get_one::<String>("time-style").map(|s| s.as_str()))?
.to_string(),
line_ending: LineEnding::from_zero_flag(matches.get_flag(options::NULL)),
};
if stat_printer.inodes
&& (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES)) && (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES))
{ {
show_warning!("options --apparent-size and -b are ineffective with --inodes"); show_warning!("options --apparent-size and -b are ineffective with --inodes");
} }
// Use separate thread to print output, so we can print finished results while computation is still running // Use separate thread to print output, so we can print finished results while computation is still running
let stat_printer = StatPrinter::new(matches.clone(), options.clone(), summarize)?;
let (print_tx, rx) = mpsc::channel::<UResult<StatPrintInfo>>(); let (print_tx, rx) = mpsc::channel::<UResult<StatPrintInfo>>();
let printing_thread = thread::spawn(move || stat_printer.print_stats(&rx)); let printing_thread = thread::spawn(move || stat_printer.print_stats(&rx));
let excludes = build_exclude_patterns(&matches)?;
'loop_file: for path in files { 'loop_file: for path in files {
// Skip if we don't want to ignore anything // Skip if we don't want to ignore anything
if !&excludes.is_empty() { if !&traversal_options.excludes.is_empty() {
let path_string = path.to_string_lossy(); let path_string = path.to_string_lossy();
for pattern in &excludes { for pattern in &traversal_options.excludes {
if pattern.matches(&path_string) { if pattern.matches(&path_string) {
// if the directory is ignored, leave early // if the directory is ignored, leave early
if options.verbose { if traversal_options.verbose {
println!("{} ignored", path_string.quote()); println!("{} ignored", path_string.quote());
} }
continue 'loop_file; continue 'loop_file;
@ -738,13 +703,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} }
// Check existence of path provided in argument // Check existence of path provided in argument
if let Ok(stat) = Stat::new(&path, &options) { if let Ok(stat) = Stat::new(&path, &traversal_options) {
// Kick off the computation of disk usage from the initial path // Kick off the computation of disk usage from the initial path
let mut seen_inodes: HashSet<FileInfo> = HashSet::new(); let mut seen_inodes: HashSet<FileInfo> = HashSet::new();
if let Some(inode) = stat.inode { if let Some(inode) = stat.inode {
seen_inodes.insert(inode); seen_inodes.insert(inode);
} }
let stat = du(stat, &options, 0, &mut seen_inodes, &excludes, &print_tx) let stat = du(stat, &traversal_options, 0, &mut seen_inodes, &print_tx)
.map_err(|e| USimpleError::new(1, e.to_string()))?; .map_err(|e| USimpleError::new(1, e.to_string()))?;
print_tx print_tx
@ -772,17 +737,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Ok(()) Ok(())
} }
fn get_time_secs(s: &str, stat: &Stat) -> Result<u64, DuError> { fn get_time_secs(time: Time, stat: &Stat) -> Result<u64, DuError> {
let secs = match s { match time {
"ctime" | "status" => stat.modified, Time::Modified => Ok(stat.modified),
"access" | "atime" | "use" => stat.accessed, Time::Accessed => Ok(stat.accessed),
"birth" | "creation" => stat Time::Created => stat.created.ok_or(DuError::InvalidTimeArg),
.created }
.ok_or_else(|| DuError::InvalidTimeArg(s.into()))?,
// below should never happen as clap already restricts the values.
_ => unreachable!("Invalid field for --time"),
};
Ok(secs)
} }
fn parse_time_style(s: Option<&str>) -> UResult<&str> { fn parse_time_style(s: Option<&str>) -> UResult<&str> {

View file

@ -16,7 +16,7 @@ path = "src/hostname.rs"
[dependencies] [dependencies]
clap = { workspace = true } clap = { workspace = true }
hostname = { version = "0.3", features = ["set"] } hostname = { workspace = true, features = ["set"] }
uucore = { workspace = true, features = ["wide"] } uucore = { workspace = true, features = ["wide"] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]

View file

@ -31,6 +31,7 @@ uucore = { workspace = true, features = [
] } ] }
once_cell = { workspace = true } once_cell = { workspace = true }
selinux = { workspace = true, optional = true } selinux = { workspace = true, optional = true }
hostname = { workspace = true }
[[bin]] [[bin]]
name = "ls" name = "ls"

View file

@ -3,14 +3,15 @@
// 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) cpio svgz webm somegroup nlink rmvb xspf tabsize dired subdired dtype // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype
use clap::{ use clap::{
builder::{NonEmptyStringValueParser, ValueParser}, builder::{NonEmptyStringValueParser, ValueParser},
crate_version, Arg, ArgAction, Command, crate_version, Arg, ArgAction, Command,
}; };
use glob::{MatchOptions, Pattern}; use glob::{MatchOptions, Pattern};
use lscolors::LsColors; use lscolors::{LsColors, Style};
use number_prefix::NumberPrefix; use number_prefix::NumberPrefix;
use std::{cell::OnceCell, num::IntErrorKind}; use std::{cell::OnceCell, num::IntErrorKind};
use std::{collections::HashSet, io::IsTerminal}; use std::{collections::HashSet, io::IsTerminal};
@ -20,7 +21,7 @@ use std::os::windows::fs::MetadataExt;
use std::{ use std::{
cmp::Reverse, cmp::Reverse,
error::Error, error::Error,
ffi::{OsStr, OsString}, ffi::OsString,
fmt::{Display, Write as FmtWrite}, fmt::{Display, Write as FmtWrite},
fs::{self, DirEntry, FileType, Metadata, ReadDir}, fs::{self, DirEntry, FileType, Metadata, ReadDir},
io::{stdout, BufWriter, ErrorKind, Stdout, Write}, io::{stdout, BufWriter, ErrorKind, Stdout, Write},
@ -155,6 +156,7 @@ pub mod options {
pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first";
pub static ZERO: &str = "zero"; pub static ZERO: &str = "zero";
pub static DIRED: &str = "dired"; pub static DIRED: &str = "dired";
pub static HYPERLINK: &str = "hyperlink";
} }
const DEFAULT_TERM_WIDTH: u16 = 80; const DEFAULT_TERM_WIDTH: u16 = 80;
@ -418,6 +420,7 @@ pub struct Config {
group_directories_first: bool, group_directories_first: bool,
line_ending: LineEnding, line_ending: LineEnding,
dired: bool, dired: bool,
hyperlink: bool,
} }
// Fields that can be removed or added to the long format // Fields that can be removed or added to the long format
@ -566,6 +569,25 @@ fn extract_color(options: &clap::ArgMatches) -> bool {
} }
} }
/// Extracts the hyperlink option to use based on the options provided.
///
/// # Returns
///
/// A boolean representing whether to hyperlink files.
fn extract_hyperlink(options: &clap::ArgMatches) -> bool {
let hyperlink = options
.get_one::<String>(options::HYPERLINK)
.unwrap()
.as_str();
match hyperlink {
"always" | "yes" | "force" => true,
"auto" | "tty" | "if-tty" => std::io::stdout().is_terminal(),
"never" | "no" | "none" => false,
_ => unreachable!("should be handled by clap"),
}
}
/// Extracts the quoting style to use based on the options provided. /// Extracts the quoting style to use based on the options provided.
/// ///
/// # Arguments /// # Arguments
@ -620,7 +642,9 @@ fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> Quot
QuotingStyle::C { QuotingStyle::C {
quotes: quoting_style::Quotes::Double, quotes: quoting_style::Quotes::Double,
} }
} else if options.get_flag(options::DIRED) { } else if options.get_flag(options::DIRED) || !std::io::stdout().is_terminal() {
// By default, `ls` uses Literal quoting when
// writing to a non-terminal file descriptor
QuotingStyle::Literal { show_control } QuotingStyle::Literal { show_control }
} else { } else {
// TODO: use environment variable if available // TODO: use environment variable if available
@ -736,19 +760,18 @@ impl Config {
} }
let sort = extract_sort(options); let sort = extract_sort(options);
let time = extract_time(options); let time = extract_time(options);
let mut needs_color = extract_color(options); let mut needs_color = extract_color(options);
let hyperlink = extract_hyperlink(options);
let cmd_line_bs = options.get_one::<String>(options::size::BLOCK_SIZE); let opt_block_size = options.get_one::<String>(options::size::BLOCK_SIZE);
let opt_si = cmd_line_bs.is_some() let opt_si = opt_block_size.is_some()
&& options && options
.get_one::<String>(options::size::BLOCK_SIZE) .get_one::<String>(options::size::BLOCK_SIZE)
.unwrap() .unwrap()
.eq("si") .eq("si")
|| options.get_flag(options::size::SI); || options.get_flag(options::size::SI);
let opt_hr = (cmd_line_bs.is_some() let opt_hr = (opt_block_size.is_some()
&& options && options
.get_one::<String>(options::size::BLOCK_SIZE) .get_one::<String>(options::size::BLOCK_SIZE)
.unwrap() .unwrap()
@ -756,9 +779,9 @@ impl Config {
|| options.get_flag(options::size::HUMAN_READABLE); || options.get_flag(options::size::HUMAN_READABLE);
let opt_kb = options.get_flag(options::size::KIBIBYTES); let opt_kb = options.get_flag(options::size::KIBIBYTES);
let bs_env_var = std::env::var_os("BLOCK_SIZE"); let env_var_block_size = std::env::var_os("BLOCK_SIZE");
let ls_bs_env_var = std::env::var_os("LS_BLOCK_SIZE"); let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE");
let pc_env_var = std::env::var_os("POSIXLY_CORRECT"); let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT");
let size_format = if opt_si { let size_format = if opt_si {
SizeFormat::Decimal SizeFormat::Decimal
@ -768,13 +791,13 @@ impl Config {
SizeFormat::Bytes SizeFormat::Bytes
}; };
let raw_bs = if let Some(cmd_line_bs) = cmd_line_bs { let raw_block_size = if let Some(opt_block_size) = opt_block_size {
OsString::from(cmd_line_bs) OsString::from(opt_block_size)
} else if !opt_kb { } else if !opt_kb {
if let Some(ls_bs_env_var) = ls_bs_env_var { if let Some(env_var_ls_block_size) = env_var_ls_block_size {
ls_bs_env_var env_var_ls_block_size
} else if let Some(bs_env_var) = bs_env_var { } else if let Some(env_var_block_size) = env_var_block_size {
bs_env_var env_var_block_size
} else { } else {
OsString::from("") OsString::from("")
} }
@ -782,20 +805,18 @@ impl Config {
OsString::from("") OsString::from("")
}; };
let block_size: Option<u64> = if !opt_si && !opt_hr && !raw_bs.is_empty() { let block_size: Option<u64> = if !opt_si && !opt_hr && !raw_block_size.is_empty() {
match parse_size_u64(&raw_bs.to_string_lossy()) { match parse_size_u64(&raw_block_size.to_string_lossy()) {
Ok(size) => Some(size), Ok(size) => Some(size),
Err(_) => { Err(_) => {
show!(LsError::BlockSizeParseError(cmd_line_bs.unwrap().clone())); show!(LsError::BlockSizeParseError(
opt_block_size.unwrap().clone()
));
None None
} }
} }
} else if let Some(pc) = pc_env_var { } else if env_var_posixly_correct.is_some() {
if pc.as_os_str() == OsStr::new("true") || pc == OsStr::new("1") { Some(POSIXLY_CORRECT_BLOCK_SIZE)
Some(POSIXLY_CORRECT_BLOCK_SIZE)
} else {
None
}
} else { } else {
None None
}; };
@ -1022,6 +1043,7 @@ impl Config {
group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST),
line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)),
dired, dired,
hyperlink,
}) })
} }
} }
@ -1156,6 +1178,19 @@ pub fn uu_app() -> Command {
.help("generate output designed for Emacs' dired (Directory Editor) mode") .help("generate output designed for Emacs' dired (Directory Editor) mode")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg(
Arg::new(options::HYPERLINK)
.long(options::HYPERLINK)
.help("hyperlink file names WHEN")
.value_parser([
"always", "yes", "force", "auto", "tty", "if-tty", "never", "no", "none",
])
.require_equals(true)
.num_args(0..=1)
.default_missing_value("always")
.default_value("never")
.value_name("WHEN"),
)
// The next four arguments do not override with the other format // The next four arguments do not override with the other format
// options, see the comment in Config::from for the reason. // options, see the comment in Config::from for the reason.
// Ideally, they would use Arg::override_with, with their own name // Ideally, they would use Arg::override_with, with their own name
@ -1868,6 +1903,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
let mut dirs = Vec::<PathData>::new(); let mut dirs = Vec::<PathData>::new();
let mut out = BufWriter::new(stdout()); let mut out = BufWriter::new(stdout());
let mut dired = DiredOutput::default(); let mut dired = DiredOutput::default();
let mut style_manager = StyleManager::new();
let initial_locs_len = locs.len(); let initial_locs_len = locs.len();
for loc in locs { for loc in locs {
@ -1901,7 +1937,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
sort_entries(&mut files, config, &mut out); sort_entries(&mut files, config, &mut out);
sort_entries(&mut dirs, config, &mut out); sort_entries(&mut dirs, config, &mut out);
display_items(&files, config, &mut out, &mut dired)?; display_items(&files, config, &mut out, &mut dired, &mut style_manager)?;
for (pos, path_data) in dirs.iter().enumerate() { for (pos, path_data) in dirs.iter().enumerate() {
// Do read_dir call here to match GNU semantics by printing // Do read_dir call here to match GNU semantics by printing
@ -1953,6 +1989,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
&mut out, &mut out,
&mut listed_ancestors, &mut listed_ancestors,
&mut dired, &mut dired,
&mut style_manager,
)?; )?;
} }
if config.dired { if config.dired {
@ -2069,6 +2106,7 @@ fn enter_directory(
out: &mut BufWriter<Stdout>, out: &mut BufWriter<Stdout>,
listed_ancestors: &mut HashSet<FileInformation>, listed_ancestors: &mut HashSet<FileInformation>,
dired: &mut DiredOutput, dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> { ) -> UResult<()> {
// Create vec of entries with initial dot files // Create vec of entries with initial dot files
let mut entries: Vec<PathData> = if config.files == Files::All { let mut entries: Vec<PathData> = if config.files == Files::All {
@ -2121,7 +2159,7 @@ fn enter_directory(
} }
} }
display_items(&entries, config, out, dired)?; display_items(&entries, config, out, dired, style_manager)?;
if config.recursive { if config.recursive {
for e in entries for e in entries
@ -2162,7 +2200,15 @@ fn enter_directory(
show_dir_name(&e.p_buf, out); show_dir_name(&e.p_buf, out);
writeln!(out)?; writeln!(out)?;
enter_directory(e, rd, config, out, listed_ancestors, dired)?; enter_directory(
e,
rd,
config,
out,
listed_ancestors,
dired,
style_manager,
)?;
listed_ancestors listed_ancestors
.remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?); .remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?);
} else { } else {
@ -2284,6 +2330,7 @@ fn display_items(
config: &Config, config: &Config,
out: &mut BufWriter<Stdout>, out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput, dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> { ) -> UResult<()> {
// `-Z`, `--context`: // `-Z`, `--context`:
// Display the SELinux security context or '?' if none is found. When used with the `-l` // Display the SELinux security context or '?' if none is found. When used with the `-l`
@ -2306,7 +2353,7 @@ fn display_items(
display_additional_leading_info(item, &padding_collection, config, out)?; display_additional_leading_info(item, &padding_collection, config, out)?;
write!(out, "{more_info}")?; write!(out, "{more_info}")?;
} }
display_item_long(item, &padding_collection, config, out, dired)?; display_item_long(item, &padding_collection, config, out, dired, style_manager)?;
} }
} else { } else {
let mut longest_context_len = 1; let mut longest_context_len = 1;
@ -2326,7 +2373,7 @@ fn display_items(
for i in items { for i in items {
let more_info = display_additional_leading_info(i, &padding, config, out)?; let more_info = display_additional_leading_info(i, &padding, config, out)?;
let cell = display_file_name(i, config, prefix_context, more_info, out); let cell = display_file_name(i, config, prefix_context, more_info, out, style_manager);
names_vec.push(cell); names_vec.push(cell);
} }
@ -2481,6 +2528,7 @@ fn display_item_long(
config: &Config, config: &Config,
out: &mut BufWriter<Stdout>, out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput, dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> { ) -> UResult<()> {
let mut output_display: String = String::new(); let mut output_display: String = String::new();
if config.dired { if config.dired {
@ -2573,7 +2621,8 @@ fn display_item_long(
write!(output_display, " {} ", display_date(md, config)).unwrap(); write!(output_display, " {} ", display_date(md, config)).unwrap();
let displayed_file = display_file_name(item, config, None, String::new(), out).contents; let displayed_file =
display_file_name(item, config, None, String::new(), out, style_manager).contents;
if config.dired { if config.dired {
let (start, end) = dired::calculate_dired( let (start, end) = dired::calculate_dired(
&dired.dired_positions, &dired.dired_positions,
@ -2655,7 +2704,8 @@ fn display_item_long(
write!(output_display, " {}", pad_right("?", padding.uname)).unwrap(); write!(output_display, " {}", pad_right("?", padding.uname)).unwrap();
} }
let displayed_file = display_file_name(item, config, None, String::new(), out).contents; let displayed_file =
display_file_name(item, config, None, String::new(), out, style_manager).contents;
let date_len = 12; let date_len = 12;
write!( write!(
@ -2946,7 +2996,6 @@ fn classify_file(path: &PathData, out: &mut BufWriter<Stdout>) -> Option<char> {
/// ///
/// Note that non-unicode sequences in symlink targets are dealt with using /// Note that non-unicode sequences in symlink targets are dealt with using
/// [`std::path::Path::to_string_lossy`]. /// [`std::path::Path::to_string_lossy`].
#[allow(unused_variables)]
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn display_file_name( fn display_file_name(
path: &PathData, path: &PathData,
@ -2954,6 +3003,7 @@ fn display_file_name(
prefix_context: Option<usize>, prefix_context: Option<usize>,
more_info: String, more_info: String,
out: &mut BufWriter<Stdout>, out: &mut BufWriter<Stdout>,
style_manager: &mut StyleManager,
) -> Cell { ) -> Cell {
// This is our return value. We start by `&path.display_name` and modify it along the way. // This is our return value. We start by `&path.display_name` and modify it along the way.
let mut name = escape_name(&path.display_name, &config.quoting_style); let mut name = escape_name(&path.display_name, &config.quoting_style);
@ -2962,16 +3012,29 @@ fn display_file_name(
// infer it because the color codes mess up term_grid's width calculation. // infer it because the color codes mess up term_grid's width calculation.
let mut width = name.width(); let mut width = name.width();
if config.hyperlink {
let hostname = hostname::get().unwrap_or(OsString::from(""));
let hostname = hostname.to_string_lossy();
let absolute_path = fs::canonicalize(&path.p_buf).unwrap_or_default();
let absolute_path = absolute_path.to_string_lossy();
// TODO encode path
// \x1b = ESC, \x07 = BEL
name = format!("\x1b]8;;file://{hostname}{absolute_path}\x07{name}\x1b]8;;\x07");
}
if let Some(ls_colors) = &config.color { if let Some(ls_colors) = &config.color {
let md = path.md(out); let md = path.md(out);
name = if md.is_some() { name = if md.is_some() {
color_name(name, &path.p_buf, md, ls_colors) color_name(name, &path.p_buf, md, ls_colors, style_manager)
} else { } else {
color_name( color_name(
name, name,
&path.p_buf, &path.p_buf,
path.p_buf.symlink_metadata().ok().as_ref(), path.p_buf.symlink_metadata().ok().as_ref(),
ls_colors, ls_colors,
style_manager,
) )
}; };
} }
@ -3060,6 +3123,7 @@ fn display_file_name(
&target_data.p_buf, &target_data.p_buf,
Some(&target_metadata), Some(&target_metadata),
ls_colors, ls_colors,
style_manager,
)); ));
} }
} else { } else {
@ -3094,11 +3158,50 @@ fn display_file_name(
} }
} }
fn color_name(name: String, path: &Path, md: Option<&Metadata>, ls_colors: &LsColors) -> String { /// We need this struct to be able to store the previous style.
match ls_colors.style_for_path_with_metadata(path, md) { /// This because we need to check the previous value in case we don't need
Some(style) => { /// the reset
return style.to_nu_ansi_term_style().paint(name).to_string(); struct StyleManager {
current_style: Option<Style>,
}
impl StyleManager {
fn new() -> Self {
Self {
current_style: None,
} }
}
fn apply_style(&mut self, new_style: &Style, name: &str) -> String {
if let Some(current) = &self.current_style {
if *current == *new_style {
// Current style is the same as new style, apply without reset.
let mut style = new_style.to_nu_ansi_term_style();
style.prefix_with_reset = false;
return style.paint(name).to_string();
}
}
// We are getting a new style, we need to reset it
self.current_style = Some(new_style.clone());
new_style
.to_nu_ansi_term_style()
.reset_before_style()
.paint(name)
.to_string()
}
}
/// Colors the provided name based on the style determined for the given path.
fn color_name(
name: String,
path: &Path,
md: Option<&Metadata>,
ls_colors: &LsColors,
style_manager: &mut StyleManager,
) -> String {
match ls_colors.style_for_path_with_metadata(path, md) {
Some(style) => style_manager.apply_style(style, &name),
None => name, None => name,
} }
} }

View file

@ -341,7 +341,7 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
let target_is_dir = target.is_dir(); let target_is_dir = target.is_dir();
if path_ends_with_terminator(target) && !target_is_dir { if path_ends_with_terminator(target) && !target_is_dir && !opts.no_target_dir {
return Err(MvError::FailedToAccessNotADirectory(target.quote().to_string()).into()); return Err(MvError::FailedToAccessNotADirectory(target.quote().to_string()).into());
} }

View file

@ -2,6 +2,8 @@
// //
// 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.
// cSpell:ignore sysconf
use crate::word_count::WordCount; use crate::word_count::WordCount;
use super::WordCountable; use super::WordCountable;
@ -11,11 +13,19 @@ use std::fs::OpenOptions;
use std::io::{self, ErrorKind, Read}; use std::io::{self, ErrorKind, Read};
#[cfg(unix)] #[cfg(unix)]
use libc::S_IFREG; use libc::{sysconf, S_IFREG, _SC_PAGESIZE};
#[cfg(unix)] #[cfg(unix)]
use nix::sys::stat; use nix::sys::stat;
#[cfg(unix)]
use std::io::{Seek, SeekFrom};
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
#[cfg(windows)]
const FILE_ATTRIBUTE_ARCHIVE: u32 = 32;
#[cfg(windows)]
const FILE_ATTRIBUTE_NORMAL: u32 = 128;
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
use libc::S_IFIFO; use libc::S_IFIFO;
@ -72,6 +82,8 @@ fn count_bytes_using_splice(fd: &impl AsRawFd) -> Result<usize, usize> {
/// 1. On Unix, we can simply `stat` the file if it is regular. /// 1. On Unix, we can simply `stat` the file if it is regular.
/// 2. On Linux -- if the above did not work -- we can use splice to count /// 2. On Linux -- if the above did not work -- we can use splice to count
/// the number of bytes if the file is a FIFO. /// the number of bytes if the file is a FIFO.
/// 3. On Windows we can use `std::os::windows::fs::MetadataExt` to get file size
/// for regular files
/// 3. Otherwise, we just read normally, but without the overhead of counting /// 3. Otherwise, we just read normally, but without the overhead of counting
/// other things such as lines and words. /// other things such as lines and words.
#[inline] #[inline]
@ -87,11 +99,60 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
// If stat.st_size = 0 then // If stat.st_size = 0 then
// - either the size is 0 // - either the size is 0
// - or the size is unknown. // - or the size is unknown.
// The second case happens for files in pseudo-filesystems. For // The second case happens for files in pseudo-filesystems.
// example with /proc/version and /sys/kernel/profiling. So, // For example with /proc/version.
// if it is 0 we don't report that and instead do a full read. // So, if it is 0 we don't report that and instead do a full read.
if (stat.st_mode as libc::mode_t & S_IFREG) != 0 && stat.st_size > 0 { //
return (stat.st_size as usize, None); // Another thing to consider for files in pseudo-filesystems like /proc, /sys
// and similar is that they could report `st_size` greater than actual content.
// For example /sys/kernel/profiling could report `st_size` equal to
// system page size (typically 4096 on 64bit system), while it's file content
// would count up only to a couple of bytes.
// This condition usually occurs for files in pseudo-filesystems like /proc, /sys
// that report `st_size` in the multiples of system page size.
// In such cases - attempt `seek()` almost to the end of the file
// and then fall back on read to count the rest.
//
// And finally a special case of input redirection in *nix shell:
// `( wc -c ; wc -c ) < file` should return
// ```
// size_of_file
// 0
// ```
// Similarly
// `( head -c1 ; wc -c ) < file` should return
// ```
// first_byte_of_file
// size_of_file - 1
// ```
// Since the input stream from file is treated as continuous across both commands inside ().
// In cases like this, due to `<` redirect, the `stat.st_mode` would report input as a regular file
// and `stat.st_size` would report the size of file on disk
// and NOT the remaining number of bytes in the input stream.
// However, the raw file descriptor in this situation would be equal to `0`
// for STDIN in both invocations.
// Therefore we cannot rely of `st_size` here and should fall back on full read.
if fd > 0 && (stat.st_mode as libc::mode_t & S_IFREG) != 0 && stat.st_size > 0 {
let sys_page_size = unsafe { sysconf(_SC_PAGESIZE) as usize };
if stat.st_size as usize % sys_page_size > 0 {
// regular file or file from /proc, /sys and similar pseudo-filesystems
// with size that is NOT a multiple of system page size
return (stat.st_size as usize, None);
} else if let Some(file) = handle.inner_file() {
// On some platforms `stat.st_blksize` and `stat.st_size`
// are of different types: i64 vs i32
// i.e. MacOS on Apple Silicon (aarch64-apple-darwin),
// Debian Linux on ARM (aarch64-unknown-linux-gnu),
// 32bit i686 targets, etc.
// While on the others they are of the same type.
#[allow(clippy::unnecessary_cast)]
let offset =
stat.st_size as i64 - stat.st_size as i64 % (stat.st_blksize as i64 + 1);
if let Ok(n) = file.seek(SeekFrom::Start(offset as u64)) {
byte_count = n as usize;
}
}
} }
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
{ {
@ -107,6 +168,21 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
} }
} }
#[cfg(windows)]
{
if let Some(file) = handle.inner_file() {
if let Ok(metadata) = file.metadata() {
let attributes = metadata.file_attributes();
if (attributes & FILE_ATTRIBUTE_ARCHIVE) != 0
|| (attributes & FILE_ATTRIBUTE_NORMAL) != 0
{
return (metadata.file_size() as usize, None);
}
}
}
}
// Fall back on `read`, but without the overhead of counting words and lines. // Fall back on `read`, but without the overhead of counting words and lines.
let mut buf = [0_u8; BUF_SIZE]; let mut buf = [0_u8; BUF_SIZE];
loop { loop {

View file

@ -17,12 +17,14 @@ use std::os::unix::io::AsRawFd;
pub trait WordCountable: AsRawFd + Read { pub trait WordCountable: AsRawFd + Read {
type Buffered: BufRead; type Buffered: BufRead;
fn buffered(self) -> Self::Buffered; fn buffered(self) -> Self::Buffered;
fn inner_file(&mut self) -> Option<&mut File>;
} }
#[cfg(not(unix))] #[cfg(not(unix))]
pub trait WordCountable: Read { pub trait WordCountable: Read {
type Buffered: BufRead; type Buffered: BufRead;
fn buffered(self) -> Self::Buffered; fn buffered(self) -> Self::Buffered;
fn inner_file(&mut self) -> Option<&mut File>;
} }
impl WordCountable for StdinLock<'_> { impl WordCountable for StdinLock<'_> {
@ -31,6 +33,9 @@ impl WordCountable for StdinLock<'_> {
fn buffered(self) -> Self::Buffered { fn buffered(self) -> Self::Buffered {
self self
} }
fn inner_file(&mut self) -> Option<&mut File> {
None
}
} }
impl WordCountable for File { impl WordCountable for File {
@ -39,4 +44,8 @@ impl WordCountable for File {
fn buffered(self) -> Self::Buffered { fn buffered(self) -> Self::Buffered {
BufReader::new(self) BufReader::new(self)
} }
fn inner_file(&mut self) -> Option<&mut File> {
Some(self)
}
} }

View file

@ -72,6 +72,7 @@ windows-sys = { workspace = true, optional = true, default-features = false, fea
default = [] default = []
# * non-default features # * non-default features
backup-control = [] backup-control = []
colors = []
encoding = ["data-encoding", "data-encoding-macro", "z85", "thiserror"] encoding = ["data-encoding", "data-encoding-macro", "z85", "thiserror"]
entries = ["libc"] entries = ["libc"]
fs = ["dunce", "libc", "winapi-util", "windows-sys"] fs = ["dunce", "libc", "winapi-util", "windows-sys"]

View file

@ -6,6 +6,8 @@
#[cfg(feature = "backup-control")] #[cfg(feature = "backup-control")]
pub mod backup_control; pub mod backup_control;
#[cfg(feature = "colors")]
pub mod colors;
#[cfg(feature = "encoding")] #[cfg(feature = "encoding")]
pub mod encoding; pub mod encoding;
#[cfg(feature = "format")] #[cfg(feature = "format")]

View file

@ -0,0 +1,264 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// cSpell:disable
/// The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
/// slackware version of dircolors) are recognized but ignored.
/// Global config options can be specified before TERM or COLORTERM entries
/// below are TERM or COLORTERM entries, which can be glob patterns, which
/// restrict following config to systems with matching environment variables.
pub static TERMS: &[&str] = &[
"Eterm",
"ansi",
"*color*",
"con[0-9]*x[0-9]*",
"cons25",
"console",
"cygwin",
"*direct*",
"dtterm",
"gnome",
"hurd",
"jfbterm",
"konsole",
"kterm",
"linux",
"linux-c",
"mlterm",
"putty",
"rxvt*",
"screen*",
"st",
"terminator",
"tmux*",
"vt100",
"xterm*",
];
/// Below are the color init strings for the basic file types.
/// One can use codes for 256 or more colors supported by modern terminals.
/// The default color codes use the capabilities of an 8 color terminal
/// with some additional attributes as per the following codes:
/// Attribute codes:
/// 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
/// Text color codes:
/// 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
/// Background color codes:
/// 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
/// #NORMAL 00 /// no color code at all
/// #FILE 00 /// regular file: use no color at all
pub static FILE_TYPES: &[(&str, &str, &str)] = &[
("RESET", "rs", "0"), // reset to "normal" color
("DIR", "di", "01;34"), // directory
("LINK", "ln", "01;36"), // symbolic link
("MULTIHARDLINK", "mh", "00"), // regular file with more than one link
("FIFO", "pi", "40;33"), // pipe
("SOCK", "so", "01;35"), // socket
("DOOR", "do", "01;35"), // door
("BLK", "bd", "40;33;01"), // block device driver
("CHR", "cd", "40;33;01"), // character device driver
("ORPHAN", "or", "40;31;01"), // symlink to nonexistent file, or non-stat'able file
("MISSING", "mi", "00"), // ... and the files they point to
("SETUID", "su", "37;41"), // file that is setuid (u+s)
("SETGID", "sg", "30;43"), // file that is setgid (g+s)
("CAPABILITY", "ca", "00"), // file with capability
("STICKY_OTHER_WRITABLE", "tw", "30;42"), // dir that is sticky and other-writable (+t,o+w)
("OTHER_WRITABLE", "ow", "34;42"), // dir that is other-writable (o+w) and not sticky
("STICKY", "st", "37;44"), // dir with the sticky bit set (+t) and not other-writable
("EXEC", "ex", "01;32"), // files with execute permission
];
/// Colors for file types
///
/// List any file extensions like '.gz' or '.tar' that you would like ls
/// to color below. Put the extension, a space, and the color init string.
/// (and any comments you want to add after a '#')
pub static FILE_COLORS: &[(&str, &str)] = &[
/*
// Executables (Windows)
(".cmd", "01;32"),
(".exe", "01;32"),
(".com", "01;32"),
(".btm", "01;32"),
(".bat", "01;32"),
(".sh", "01;32"),
(".csh", "01;32"),*/
// Archives or compressed
(".tar", "01;31"),
(".tgz", "01;31"),
(".arc", "01;31"),
(".arj", "01;31"),
(".taz", "01;31"),
(".lha", "01;31"),
(".lz4", "01;31"),
(".lzh", "01;31"),
(".lzma", "01;31"),
(".tlz", "01;31"),
(".txz", "01;31"),
(".tzo", "01;31"),
(".t7z", "01;31"),
(".zip", "01;31"),
(".z", "01;31"),
(".dz", "01;31"),
(".gz", "01;31"),
(".lrz", "01;31"),
(".lz", "01;31"),
(".lzo", "01;31"),
(".xz", "01;31"),
(".zst", "01;31"),
(".tzst", "01;31"),
(".bz2", "01;31"),
(".bz", "01;31"),
(".tbz", "01;31"),
(".tbz2", "01;31"),
(".tz", "01;31"),
(".deb", "01;31"),
(".rpm", "01;31"),
(".jar", "01;31"),
(".war", "01;31"),
(".ear", "01;31"),
(".sar", "01;31"),
(".rar", "01;31"),
(".alz", "01;31"),
(".ace", "01;31"),
(".zoo", "01;31"),
(".cpio", "01;31"),
(".7z", "01;31"),
(".rz", "01;31"),
(".cab", "01;31"),
(".wim", "01;31"),
(".swm", "01;31"),
(".dwm", "01;31"),
(".esd", "01;31"),
// Image formats
(".avif", "01;35"),
(".jpg", "01;35"),
(".jpeg", "01;35"),
(".mjpg", "01;35"),
(".mjpeg", "01;35"),
(".gif", "01;35"),
(".bmp", "01;35"),
(".pbm", "01;35"),
(".pgm", "01;35"),
(".ppm", "01;35"),
(".tga", "01;35"),
(".xbm", "01;35"),
(".xpm", "01;35"),
(".tif", "01;35"),
(".tiff", "01;35"),
(".png", "01;35"),
(".svg", "01;35"),
(".svgz", "01;35"),
(".mng", "01;35"),
(".pcx", "01;35"),
(".mov", "01;35"),
(".mpg", "01;35"),
(".mpeg", "01;35"),
(".m2v", "01;35"),
(".mkv", "01;35"),
(".webm", "01;35"),
(".webp", "01;35"),
(".ogm", "01;35"),
(".mp4", "01;35"),
(".m4v", "01;35"),
(".mp4v", "01;35"),
(".vob", "01;35"),
(".qt", "01;35"),
(".nuv", "01;35"),
(".wmv", "01;35"),
(".asf", "01;35"),
(".rm", "01;35"),
(".rmvb", "01;35"),
(".flc", "01;35"),
(".avi", "01;35"),
(".fli", "01;35"),
(".flv", "01;35"),
(".gl", "01;35"),
(".dl", "01;35"),
(".xcf", "01;35"),
(".xwd", "01;35"),
(".yuv", "01;35"),
(".cgm", "01;35"),
(".emf", "01;35"),
// https://wiki.xiph.org/MIME_Types_and_File_Extensions
(".ogv", "01;35"),
(".ogx", "01;35"),
// Audio formats
(".aac", "00;36"),
(".au", "00;36"),
(".flac", "00;36"),
(".m4a", "00;36"),
(".mid", "00;36"),
(".midi", "00;36"),
(".mka", "00;36"),
(".mp3", "00;36"),
(".mpc", "00;36"),
(".ogg", "00;36"),
(".ra", "00;36"),
(".wav", "00;36"),
// https://wiki.xiph.org/MIME_Types_and_File_Extensions
(".oga", "00;36"),
(".opus", "00;36"),
(".spx", "00;36"),
(".xspf", "00;36"),
// Backup files
("*~", "00;90"),
("*#", "00;90"),
(".bak", "00;90"),
(".old", "00;90"),
(".orig", "00;90"),
(".part", "00;90"),
(".rej", "00;90"),
(".swp", "00;90"),
(".tmp", "00;90"),
(".dpkg-dist", "00;90"),
(".dpkg-old", "00;90"),
(".ucf-dist", "00;90"),
(".ucf-new", "00;90"),
(".ucf-old", "00;90"),
(".rpmnew", "00;90"),
(".rpmorig", "00;90"),
(".rpmsave", "00;90"),
];
pub static FILE_ATTRIBUTE_CODES: &[(&str, &str)] = &[
("normal", "no"),
("norm", "no"),
("file", "fi"),
("reset", "rs"),
("dir", "di"),
("lnk", "ln"),
("link", "ln"),
("symlink", "ln"),
("orphan", "or"),
("missing", "mi"),
("fifo", "pi"),
("pipe", "pi"),
("sock", "so"),
("blk", "bd"),
("block", "bd"),
("chr", "cd"),
("char", "cd"),
("door", "do"),
("exec", "ex"),
("left", "lc"),
("leftcode", "lc"),
("right", "rc"),
("rightcode", "rc"),
("end", "ec"),
("endcode", "ec"),
("suid", "su"),
("setuid", "su"),
("sgid", "sg"),
("setgid", "sg"),
("sticky", "st"),
("other_writable", "ow"),
("owr", "ow"),
("sticky_other_writable", "tw"),
("owt", "tw"),
("capability", "ca"),
("multihardlink", "mh"),
("clrtoeol", "cl"),
];

View file

@ -115,6 +115,7 @@ impl FileInformation {
not(target_os = "android"), not(target_os = "android"),
not(target_os = "freebsd"), not(target_os = "freebsd"),
not(target_os = "netbsd"), not(target_os = "netbsd"),
not(target_os = "openbsd"),
not(target_os = "illumos"), not(target_os = "illumos"),
not(target_os = "solaris"), not(target_os = "solaris"),
not(target_arch = "aarch64"), not(target_arch = "aarch64"),
@ -130,6 +131,7 @@ impl FileInformation {
target_os = "android", target_os = "android",
target_os = "freebsd", target_os = "freebsd",
target_os = "netbsd", target_os = "netbsd",
target_os = "openbsd",
target_os = "illumos", target_os = "illumos",
target_os = "solaris", target_os = "solaris",
target_arch = "aarch64", target_arch = "aarch64",
@ -146,13 +148,14 @@ impl FileInformation {
#[cfg(unix)] #[cfg(unix)]
pub fn inode(&self) -> u64 { pub fn inode(&self) -> u64 {
#[cfg(all( #[cfg(all(
not(any(target_os = "freebsd", target_os = "netbsd")), not(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd")),
target_pointer_width = "64" target_pointer_width = "64"
))] ))]
return self.0.st_ino; return self.0.st_ino;
#[cfg(any( #[cfg(any(
target_os = "freebsd", target_os = "freebsd",
target_os = "netbsd", target_os = "netbsd",
target_os = "openbsd",
not(target_pointer_width = "64") not(target_pointer_width = "64")
))] ))]
return self.0.st_ino.into(); return self.0.st_ino.into();

View file

@ -497,7 +497,10 @@ impl FsUsage {
#[cfg(unix)] #[cfg(unix)]
pub fn new(statvfs: StatFs) -> Self { pub fn new(statvfs: StatFs) -> Self {
{ {
#[cfg(all(not(target_os = "freebsd"), target_pointer_width = "64"))] #[cfg(all(
not(any(target_os = "freebsd", target_os = "openbsd")),
target_pointer_width = "64"
))]
return Self { return Self {
blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ? blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ?
blocks: statvfs.f_blocks, blocks: statvfs.f_blocks,
@ -507,7 +510,10 @@ impl FsUsage {
files: statvfs.f_files, files: statvfs.f_files,
ffree: statvfs.f_ffree, ffree: statvfs.f_ffree,
}; };
#[cfg(all(not(target_os = "freebsd"), not(target_pointer_width = "64")))] #[cfg(all(
not(any(target_os = "freebsd", target_os = "openbsd")),
not(target_pointer_width = "64")
))]
return Self { return Self {
blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ? blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ?
blocks: statvfs.f_blocks.into(), blocks: statvfs.f_blocks.into(),
@ -530,6 +536,19 @@ impl FsUsage {
files: statvfs.f_files, files: statvfs.f_files,
ffree: statvfs.f_ffree.try_into().unwrap(), ffree: statvfs.f_ffree.try_into().unwrap(),
}; };
#[cfg(target_os = "openbsd")]
return Self {
blocksize: statvfs.f_bsize.into(),
blocks: statvfs.f_blocks,
bfree: statvfs.f_bfree,
bavail: statvfs.f_bavail.try_into().unwrap(),
bavail_top_bit_set: ((std::convert::TryInto::<u64>::try_into(statvfs.f_bavail)
.unwrap())
& (1u64.rotate_right(1)))
!= 0,
files: statvfs.f_files,
ffree: statvfs.f_ffree,
};
} }
} }
#[cfg(not(unix))] #[cfg(not(unix))]
@ -617,6 +636,7 @@ impl FsMeta for StatFs {
not(target_vendor = "apple"), not(target_vendor = "apple"),
not(target_os = "android"), not(target_os = "android"),
not(target_os = "freebsd"), not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(target_os = "illumos"), not(target_os = "illumos"),
not(target_os = "solaris"), not(target_os = "solaris"),
not(target_arch = "s390x"), not(target_arch = "s390x"),
@ -630,6 +650,7 @@ impl FsMeta for StatFs {
target_arch = "s390x", target_arch = "s390x",
target_vendor = "apple", target_vendor = "apple",
target_os = "android", target_os = "android",
target_os = "openbsd",
not(target_pointer_width = "64") not(target_pointer_width = "64")
) )
))] ))]
@ -655,11 +676,19 @@ impl FsMeta for StatFs {
return self.f_bfree.into(); return self.f_bfree.into();
} }
fn avail_blocks(&self) -> u64 { fn avail_blocks(&self) -> u64 {
#[cfg(all(not(target_os = "freebsd"), target_pointer_width = "64"))] #[cfg(all(
not(target_os = "freebsd"),
not(target_os = "openbsd"),
target_pointer_width = "64"
))]
return self.f_bavail; return self.f_bavail;
#[cfg(all(not(target_os = "freebsd"), not(target_pointer_width = "64")))] #[cfg(all(
not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(target_pointer_width = "64")
))]
return self.f_bavail.into(); return self.f_bavail.into();
#[cfg(target_os = "freebsd")] #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
return self.f_bavail.try_into().unwrap(); return self.f_bavail.try_into().unwrap();
} }
fn total_file_nodes(&self) -> u64 { fn total_file_nodes(&self) -> u64 {

View file

@ -35,6 +35,8 @@ pub use crate::parser::shortcut_value_parser;
// * feature-gated modules // * feature-gated modules
#[cfg(feature = "backup-control")] #[cfg(feature = "backup-control")]
pub use crate::features::backup_control; pub use crate::features::backup_control;
#[cfg(feature = "colors")]
pub use crate::features::colors;
#[cfg(feature = "encoding")] #[cfg(feature = "encoding")]
pub use crate::features::encoding; pub use crate::features::encoding;
#[cfg(feature = "format")] #[cfg(feature = "format")]

View file

@ -13,7 +13,7 @@ use std::os::unix::fs;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
#[cfg(all(unix, not(target_os = "freebsd")))] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
#[cfg(windows)] #[cfg(windows)]
use std::os::windows::fs::symlink_file; use std::os::windows::fs::symlink_file;
@ -2381,13 +2381,18 @@ fn test_copy_symlink_force() {
} }
#[test] #[test]
#[cfg(all(unix, not(target_os = "freebsd")))] #[cfg(unix)]
fn test_no_preserve_mode() { fn test_no_preserve_mode() {
use std::os::unix::prelude::MetadataExt; use std::os::unix::prelude::MetadataExt;
use uucore::mode::get_umask; use uucore::mode::get_umask;
const PERMS_ALL: u32 = 0o7777; const PERMS_ALL: u32 = if cfg!(target_os = "freebsd") {
// Only the superuser can set the sticky bit on a file.
0o6777
} else {
0o7777
};
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
at.touch("file"); at.touch("file");
@ -2407,11 +2412,16 @@ fn test_no_preserve_mode() {
} }
#[test] #[test]
#[cfg(all(unix, not(target_os = "freebsd")))] #[cfg(unix)]
fn test_preserve_mode() { fn test_preserve_mode() {
use std::os::unix::prelude::MetadataExt; use std::os::unix::prelude::MetadataExt;
const PERMS_ALL: u32 = 0o7777; const PERMS_ALL: u32 = if cfg!(target_os = "freebsd") {
// Only the superuser can set the sticky bit on a file.
0o6777
} else {
0o7777
};
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
at.touch("file"); at.touch("file");

View file

@ -159,6 +159,18 @@ fn test_quoting() {
.no_stderr(); .no_stderr();
} }
/*
#[test]
fn test_print_ls_colors() {
new_ucmd!()
.pipe_in("OWT 40;33\n")
.args(&["--print-ls-colors"])
.succeeds()
.stdout_is("\x1B[40;33mtw\t40;33\x1B[0m\n")
.no_stderr();
}
*/
#[test] #[test]
fn test_extra_operand() { fn test_extra_operand() {
new_ucmd!() new_ucmd!()

View file

@ -365,12 +365,19 @@ fn test_du_no_dereference() {
.stdout_does_not_contain(symlink); .stdout_does_not_contain(symlink);
// ensure dereference "wins" // ensure dereference "wins"
ts.ucmd() let result = ts.ucmd().arg(arg).arg("--dereference").succeeds();
.arg(arg)
.arg("--dereference") #[cfg(target_os = "linux")]
.succeeds() {
.stdout_contains(symlink) let result_reference = unwrap_or_return!(expected_result(&ts, &[arg, "--dereference"]));
.stdout_does_not_contain(dir);
if result_reference.succeeded() {
assert_eq!(result.stdout_str(), result_reference.stdout_str());
}
}
#[cfg(not(target_os = "linux"))]
result.stdout_contains(symlink).stdout_does_not_contain(dir);
} }
} }
@ -441,6 +448,7 @@ fn test_du_inodes() {
} }
} }
#[cfg(not(target_os = "android"))]
#[test] #[test]
fn test_du_inodes_with_count_links() { fn test_du_inodes_with_count_links() {
let ts = TestScenario::new(util_name!()); let ts = TestScenario::new(util_name!());

View file

@ -2,7 +2,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 (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs // spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir
#[cfg(any(unix, feature = "feat_selinux"))] #[cfg(any(unix, feature = "feat_selinux"))]
use crate::common::util::expected_result; use crate::common::util::expected_result;
@ -864,11 +864,11 @@ fn test_ls_zero() {
.succeeds() .succeeds()
.stdout_only("\"0-test-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00"); .stdout_only("\"0-test-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00");
scene let result = scene.ucmd().args(&["--zero", "--color=always"]).succeeds();
.ucmd() assert_eq!(
.args(&["--zero", "--color=always"]) result.stdout_str(),
.succeeds() "\u{1b}[0m\u{1b}[01;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00"
.stdout_only("\x1b[1;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00"); );
scene scene
.ucmd() .ucmd()
@ -921,12 +921,9 @@ fn test_ls_zero() {
"\"0-test-zero\"\x00\"1\\ntest-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00", "\"0-test-zero\"\x00\"1\\ntest-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00",
); );
scene let result = scene.ucmd().args(&["--zero", "--color=always"]).succeeds();
.ucmd() assert_eq!(result.stdout_str(),
.args(&["--zero", "--color=always"]) "\u{1b}[0m\u{1b}[01;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00",
.succeeds()
.stdout_only(
"\x1b[1;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00",
); );
scene scene
@ -1202,12 +1199,21 @@ fn test_ls_long_symlink_color() {
} }
fn capture_colored_string(input: &str) -> (Color, Name) { fn capture_colored_string(input: &str) -> (Color, Name) {
let colored_name = Regex::new(r"\x1b\[([0-9;]+)m(.+)\x1b\[0m").unwrap(); // Input can be:
// \u{1b}[0m\u{1b}[01;36mln-dir3\u{1b}[0m
// \u{1b}[0m\u{1b}[01;34m./dir1/dir2/dir3\u{1b}[0m
// \u{1b}[0m\u{1b}[01;36mln-file-invalid\u{1b}[0m
// \u{1b}[01;36mdir1/invalid-target\u{1b}[0m
let colored_name = Regex::new(r"(?:\x1b\[0m\x1b)?\[([0-9;]+)m(.+)\x1b\[0m").unwrap();
match colored_name.captures(input) { match colored_name.captures(input) {
Some(captures) => ( Some(captures) => {
captures.get(1).unwrap().as_str().to_string(), dbg!(captures.get(1).unwrap().as_str().to_string());
captures.get(2).unwrap().as_str().to_string(), dbg!(captures.get(2).unwrap().as_str().to_string());
), return (
captures.get(1).unwrap().as_str().to_string(),
captures.get(2).unwrap().as_str().to_string(),
);
}
None => (String::new(), input.to_string()), None => (String::new(), input.to_string()),
} }
} }
@ -1995,9 +2001,9 @@ fn test_ls_color() {
at.touch(nested_file); at.touch(nested_file);
at.touch("test-color"); at.touch("test-color");
let a_with_colors = "\x1b[1;34ma\x1b[0m"; let a_with_colors = "\x1b[0m\x1b[01;34ma\x1b[0m";
let z_with_colors = "\x1b[1;34mz\x1b[0m"; let z_with_colors = "\x1b[01;34mz\x1b[0m\n";
let nested_dir_with_colors = "\x1b[1;34mnested_dir\x1b[0m"; // spell-checker:disable-line let nested_dir_with_colors = "\x1b[0m\x1b[01;34mnested_dir\x1b[0m\x0anested_file"; // spell-checker:disable-line
// Color is disabled by default // Color is disabled by default
let result = scene.ucmd().succeeds(); let result = scene.ucmd().succeeds();
@ -2006,12 +2012,9 @@ fn test_ls_color() {
// Color should be enabled // Color should be enabled
for param in ["--color", "--col", "--color=always", "--col=always"] { for param in ["--color", "--col", "--color=always", "--col=always"] {
scene let result = scene.ucmd().arg(param).succeeds();
.ucmd() assert!(result.stdout_str().contains(a_with_colors));
.arg(param) assert!(result.stdout_str().contains(z_with_colors));
.succeeds()
.stdout_contains(a_with_colors)
.stdout_contains(z_with_colors);
} }
// Color should be disabled // Color should be disabled
@ -2020,12 +2023,8 @@ fn test_ls_color() {
assert!(!result.stdout_str().contains(z_with_colors)); assert!(!result.stdout_str().contains(z_with_colors));
// Nested dir should be shown and colored // Nested dir should be shown and colored
scene let result = scene.ucmd().arg("--color").arg("a").succeeds();
.ucmd() assert!(result.stdout_str().contains(nested_dir_with_colors));
.arg("--color")
.arg("a")
.succeeds()
.stdout_contains(nested_dir_with_colors);
// No output // No output
scene scene
@ -2037,13 +2036,18 @@ fn test_ls_color() {
// The colors must not mess up the grid layout // The colors must not mess up the grid layout
at.touch("b"); at.touch("b");
scene let result = scene
.ucmd() .ucmd()
.arg("--color") .arg("--color")
.arg("-w=15") .arg("-w=15")
.arg("-C") .arg("-C")
.succeeds() .succeeds();
.stdout_only(format!("{a_with_colors} test-color\nb {z_with_colors}\n")); let expected = format!("{} test-color\x0ab {}", a_with_colors, z_with_colors);
assert_eq!(
result.stdout_str().escape_default().to_string(),
expected.escape_default().to_string()
);
assert_eq!(result.stdout_str(), expected);
} }
#[cfg(unix)] #[cfg(unix)]
@ -2468,13 +2472,16 @@ fn test_ls_quoting_style() {
{ {
at.touch("one\ntwo"); at.touch("one\ntwo");
at.touch("one\\two"); at.touch("one\\two");
// Default is shell-escape // Default is literal, when stdout is not a TTY.
// Otherwise, it is shell-escape
scene scene
.ucmd() .ucmd()
.arg("--hide-control-chars") .arg("--hide-control-chars")
.arg("one\ntwo") .arg("one\ntwo")
.succeeds() .succeeds()
.stdout_only("'one'$'\\n''two'\n"); .stdout_only("one?two\n");
// TODO: TTY-expected output, find a way to check this as well
// .stdout_only("'one'$'\\n''two'\n");
for (arg, correct) in [ for (arg, correct) in [
("--quoting-style=literal", "one?two"), ("--quoting-style=literal", "one?two"),
@ -2561,7 +2568,9 @@ fn test_ls_quoting_style() {
.ucmd() .ucmd()
.arg("one two") .arg("one two")
.succeeds() .succeeds()
.stdout_only("'one two'\n"); .stdout_only("one two\n");
// TODO: TTY-expected output
// .stdout_only("'one two'\n");
for (arg, correct) in [ for (arg, correct) in [
("--quoting-style=literal", "one two"), ("--quoting-style=literal", "one two"),
@ -2624,7 +2633,9 @@ fn test_ls_quoting_and_color() {
.arg("--color") .arg("--color")
.arg("one two") .arg("one two")
.succeeds() .succeeds()
.stdout_only("'one two'\n"); .stdout_only("one two\n");
// TODO: TTY-expected output
// .stdout_only("'one two'\n");
} }
#[test] #[test]
@ -3156,11 +3167,8 @@ fn test_ls_path() {
.stdout_is(expected_stdout); .stdout_is(expected_stdout);
let abs_path = format!("{}/{}", at.as_string(), path); let abs_path = format!("{}/{}", at.as_string(), path);
let expected_stdout = if cfg!(windows) { let expected_stdout = format!("{abs_path}\n");
format!("\'{abs_path}\'\n")
} else {
format!("{abs_path}\n")
};
scene.ucmd().arg(&abs_path).run().stdout_is(expected_stdout); scene.ucmd().arg(&abs_path).run().stdout_is(expected_stdout);
let expected_stdout = format!("{path}\n{file1}\n"); let expected_stdout = format!("{path}\n{file1}\n");
@ -3828,3 +3836,80 @@ fn test_ls_cf_output_should_be_delimited_by_tab() {
.succeeds() .succeeds()
.stdout_is("a2345/\tb/\n"); .stdout_is("a2345/\tb/\n");
} }
#[cfg(all(unix, feature = "dd"))]
#[test]
fn test_posixly_correct() {
let scene = TestScenario::new(util_name!());
scene
.ccmd("dd")
.arg("if=/dev/zero")
.arg("of=file")
.arg("bs=1024")
.arg("count=1")
.succeeds();
scene
.ucmd()
.arg("-s")
.succeeds()
.stdout_contains_line("total 4");
scene
.ucmd()
.arg("-s")
.env("POSIXLY_CORRECT", "some_value")
.succeeds()
.stdout_contains_line("total 8");
}
#[test]
fn test_ls_hyperlink() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let file = "a.txt";
at.touch(file);
let path = at.root_dir_resolved();
let separator = std::path::MAIN_SEPARATOR_STR;
let result = scene.ucmd().arg("--hyperlink").succeeds();
assert!(result.stdout_str().contains("\x1b]8;;file://"));
assert!(result
.stdout_str()
.contains(&format!("{path}{separator}{file}\x07{file}\x1b]8;;\x07")));
let result = scene.ucmd().arg("--hyperlink=always").succeeds();
assert!(result.stdout_str().contains("\x1b]8;;file://"));
assert!(result
.stdout_str()
.contains(&format!("{path}{separator}{file}\x07{file}\x1b]8;;\x07")));
scene
.ucmd()
.arg("--hyperlink=never")
.succeeds()
.stdout_is(format!("{file}\n"));
}
#[test]
fn test_ls_color_do_not_reset() {
let scene: TestScenario = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("example");
at.mkdir("example/a");
at.mkdir("example/b");
let result = scene
.ucmd()
.arg("--color=always")
.arg("example/")
.succeeds();
// the second color code should not have a reset
assert_eq!(
result.stdout_str().escape_default().to_string(),
"\\u{1b}[0m\\u{1b}[01;34ma\\u{1b}[0m\\n\\u{1b}[01;34mb\\u{1b}[0m\\n"
);
}

View file

@ -1158,6 +1158,32 @@ fn test_mv_overwrite_dir() {
assert!(at.dir_exists(dir_b)); assert!(at.dir_exists(dir_b));
} }
#[test]
fn test_mv_no_target_dir_with_dest_not_existing() {
let (at, mut ucmd) = at_and_ucmd!();
let dir_a = "a";
let dir_b = "b";
at.mkdir(dir_a);
ucmd.arg("-T").arg(dir_a).arg(dir_b).succeeds().no_output();
assert!(!at.dir_exists(dir_a));
assert!(at.dir_exists(dir_b));
}
#[test]
fn test_mv_no_target_dir_with_dest_not_existing_and_ending_with_slash() {
let (at, mut ucmd) = at_and_ucmd!();
let dir_a = "a";
let dir_b = "b/";
at.mkdir(dir_a);
ucmd.arg("-T").arg(dir_a).arg(dir_b).succeeds().no_output();
assert!(!at.dir_exists(dir_a));
assert!(at.dir_exists(dir_b));
}
#[test] #[test]
fn test_mv_overwrite_nonempty_dir() { fn test_mv_overwrite_nonempty_dir() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();

View file

@ -553,7 +553,7 @@ fn test_nonexistent_file_is_not_symlink() {
} }
#[test] #[test]
// FixME: freebsd fails with 'chmod: sticky_file: Inappropriate file type or format' // Only the superuser is allowed to set the sticky bit on files on FreeBSD.
// Windows has no concept of sticky bit // Windows has no concept of sticky bit
#[cfg(not(any(windows, target_os = "freebsd")))] #[cfg(not(any(windows, target_os = "freebsd")))]
fn test_file_is_sticky() { fn test_file_is_sticky() {

View file

@ -243,6 +243,14 @@ fn test_single_only_lines() {
.stdout_is("18 moby_dick.txt\n"); .stdout_is("18 moby_dick.txt\n");
} }
#[test]
fn test_single_only_bytes() {
new_ucmd!()
.args(&["-c", "lorem_ipsum.txt"])
.run()
.stdout_is("772 lorem_ipsum.txt\n");
}
#[test] #[test]
fn test_single_all_counts() { fn test_single_all_counts() {
new_ucmd!() new_ucmd!()
@ -419,6 +427,14 @@ fn test_files_from_pseudo_filesystem() {
use pretty_assertions::assert_ne; use pretty_assertions::assert_ne;
let result = new_ucmd!().arg("-c").arg("/proc/cpuinfo").succeeds(); let result = new_ucmd!().arg("-c").arg("/proc/cpuinfo").succeeds();
assert_ne!(result.stdout_str(), "0 /proc/cpuinfo\n"); assert_ne!(result.stdout_str(), "0 /proc/cpuinfo\n");
let (at, mut ucmd) = at_and_ucmd!();
let result = ucmd.arg("-c").arg("/sys/kernel/profiling").succeeds();
let actual = at.read("/sys/kernel/profiling").len();
assert_eq!(
result.stdout_str(),
format!("{} /sys/kernel/profiling\n", actual)
);
} }
#[test] #[test]

View file

@ -1,8 +1,5 @@
# Configuration file for dircolors, a utility to help you set the # Configuration file for dircolors, a utility to help you set the
# LS_COLORS environment variable used by GNU ls with the --color option. # LS_COLORS environment variable used by GNU ls with the --color option.
# Copyright (C) 1996-2022 Free Software Foundation, Inc.
# Copying and distribution of this file, with or without modification,
# are permitted provided the copyright notice and this notice are preserved.
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the # The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
# slackware version of dircolors) are recognized but ignored. # slackware version of dircolors) are recognized but ignored.
# Global config options can be specified before TERM or COLORTERM entries # Global config options can be specified before TERM or COLORTERM entries
@ -46,40 +43,26 @@ TERM xterm*
# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white # 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
#NORMAL 00 # no color code at all #NORMAL 00 # no color code at all
#FILE 00 # regular file: use no color at all #FILE 00 # regular file: use no color at all
RESET 0 # reset to "normal" color RESET 0
DIR 01;34 # directory DIR 01;34
LINK 01;36 # symbolic link. (If you set this to 'target' instead of a LINK 01;36
# numerical value, the color is as for the file pointed to.) MULTIHARDLINK 00
MULTIHARDLINK 00 # regular file with more than one link FIFO 40;33
FIFO 40;33 # pipe SOCK 01;35
SOCK 01;35 # socket DOOR 01;35
DOOR 01;35 # door BLK 40;33;01
BLK 40;33;01 # block device driver CHR 40;33;01
CHR 40;33;01 # character device driver ORPHAN 40;31;01
ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ... MISSING 00
MISSING 00 # ... and the files they point to SETUID 37;41
SETUID 37;41 # file that is setuid (u+s) SETGID 30;43
SETGID 30;43 # file that is setgid (g+s) CAPABILITY 00
CAPABILITY 00 # file with capability (very expensive to lookup) STICKY_OTHER_WRITABLE 30;42
STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w) OTHER_WRITABLE 34;42
OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky STICKY 37;44
STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable
# This is for files with execute permission:
EXEC 01;32 EXEC 01;32
# List any file extensions like '.gz' or '.tar' that you would like ls # List any file extensions like '.gz' or '.tar' that you would like ls
# to color below. Put the extension, a space, and the color init string. # to color below. Put the extension, a space, and the color init string.
# (and any comments you want to add after a '#')
# If you use DOS-style suffixes, you may want to uncomment the following:
#.cmd 01;32 # executables (bright green)
#.exe 01;32
#.com 01;32
#.btm 01;32
#.bat 01;32
# Or if you want to color scripts even if they do not have the
# executable bit actually set.
#.sh 01;32
#.csh 01;32
# archives or compressed (bright red)
.tar 01;31 .tar 01;31
.tgz 01;31 .tgz 01;31
.arc 01;31 .arc 01;31
@ -126,7 +109,6 @@ EXEC 01;32
.swm 01;31 .swm 01;31
.dwm 01;31 .dwm 01;31
.esd 01;31 .esd 01;31
# image formats
.avif 01;35 .avif 01;35
.jpg 01;35 .jpg 01;35
.jpeg 01;35 .jpeg 01;35
@ -176,10 +158,8 @@ EXEC 01;32
.yuv 01;35 .yuv 01;35
.cgm 01;35 .cgm 01;35
.emf 01;35 .emf 01;35
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.ogv 01;35 .ogv 01;35
.ogx 01;35 .ogx 01;35
# audio formats
.aac 00;36 .aac 00;36
.au 00;36 .au 00;36
.flac 00;36 .flac 00;36
@ -192,12 +172,10 @@ EXEC 01;32
.ogg 00;36 .ogg 00;36
.ra 00;36 .ra 00;36
.wav 00;36 .wav 00;36
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.oga 00;36 .oga 00;36
.opus 00;36 .opus 00;36
.spx 00;36 .spx 00;36
.xspf 00;36 .xspf 00;36
# backup files
*~ 00;90 *~ 00;90
*# 00;90 *# 00;90
.bak 00;90 .bak 00;90