1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37: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
uses: mozilla-actions/sccache-action@v0.0.3
- name: Prepare, build and test
uses: vmactions/freebsd-vm@v1.0.2
uses: vmactions/freebsd-vm@v1.0.5
with:
usesh: true
sync: rsync
@ -131,7 +131,7 @@ jobs:
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3
- name: Prepare, build and test
uses: vmactions/freebsd-vm@v1.0.2
uses: vmactions/freebsd-vm@v1.0.5
with:
usesh: true
sync: rsync

View file

@ -46,6 +46,8 @@ jobs:
- { name: fuzz_date, should_pass: false }
- { name: fuzz_expr, should_pass: true }
- { 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_size, should_pass: true }
- { name: fuzz_parse_time, should_pass: true }

9
Cargo.lock generated
View file

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

View file

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

View file

@ -17,7 +17,8 @@ uu_date = { path = "../src/uu/date/" }
uu_test = { path = "../src/uu/test/" }
uu_expr = { path = "../src/uu/expr/" }
uu_printf = { path = "../src/uu/printf/" }
uu_echo = { path = "../src/uu/echo/" }
uu_seq = { path = "../src/uu/seq/" }
# Prevent this from interfering with workspaces
[workspace]
@ -35,6 +36,18 @@ path = "fuzz_targets/fuzz_printf.rs"
test = 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]]
name = "fuzz_expr"
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]
clap = { workspace = true }
uucore = { workspace = true }
uucore = { workspace = true, features = ["colors"] }
[[bin]]
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::env;
use std::fs::File;
//use std::io::IsTerminal;
use std::io::{BufRead, BufReader};
use std::path::Path;
use clap::{crate_version, Arg, ArgAction, Command};
use uucore::colors::{FILE_ATTRIBUTE_CODES, FILE_COLORS, FILE_TYPES, TERMS};
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError, UUsageError};
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 AFTER_HELP: &str = help_section!("after help", "dircolors.md");
mod colors;
use self::colors::INTERNAL_DB;
#[derive(PartialEq, Eq, Debug)]
pub enum OutputFmt {
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
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(());
}
@ -125,7 +196,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let result;
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 {
return Err(UUsageError::new(
1,
@ -133,6 +217,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
));
} else if files[0].eq("-") {
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]);
} else {
let path = Path::new(files[0]);
@ -276,69 +361,25 @@ enum ParseState {
Pass,
}
use std::collections::HashMap;
use uucore::{format_usage, parse_glob};
#[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
T: IntoIterator,
T::Item: Borrow<str>,
{
// 1790 > $(dircolors | wc -m)
let mut result = String::with_capacity(1790);
match fmt {
OutputFmt::Shell => result.push_str("LS_COLORS='"),
OutputFmt::CShell => result.push_str("setenv LS_COLORS '"),
OutputFmt::Display => (),
OutputFmt::Unknown => unreachable!(),
}
let (prefix, suffix) = get_colors_format_strings(fmt);
let mut table: HashMap<&str, &str> = HashMap::with_capacity(48);
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");
result.push_str(&prefix);
let term = env::var("TERM").unwrap_or_else(|_| "none".to_owned());
let term = term.as_str();
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 line = line.borrow().purify();
if line.is_empty() {
@ -350,13 +391,13 @@ where
let (key, val) = line.split_two();
if val.is_empty() {
return Err(format!(
// The double space is what GNU is doing
"{}:{}: invalid line; missing second token",
fp.maybe_quote(),
num
));
}
let lower = key.to_lowercase();
if lower == "term" || lower == "colorterm" {
if term.fnmatch(val) {
state = ParseState::Matched;
@ -370,6 +411,8 @@ where
state = ParseState::Continue;
}
if state != ParseState::Pass {
let search_key = lower.as_str();
if key.starts_with('.') {
if *fmt == OutputFmt::Display {
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" {
// 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 {
result.push_str(format!("\x1b[{val}m{s}\t{val}\x1b[0m\n").as_str());
} else {
@ -402,15 +448,11 @@ where
}
}
match fmt {
OutputFmt::Shell => result.push_str("';\nexport LS_COLORS"),
OutputFmt::CShell => result.push('\''),
OutputFmt::Display => {
// remove latest "\n"
result.pop();
}
OutputFmt::Unknown => unreachable!(),
if fmt == &OutputFmt::Display {
// remove latest "\n"
result.pop();
}
result.push_str(&suffix);
Ok(result)
}
@ -436,6 +478,58 @@ fn escape(s: &str) -> String {
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)]
mod tests {
use super::escape;

View file

@ -3,35 +3,30 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::prelude::DateTime;
use chrono::Local;
use clap::ArgAction;
use clap::{crate_version, Arg, ArgMatches, Command};
use chrono::{DateTime, Local};
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use glob::Pattern;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::fs::File;
use std::error::Error;
use std::fmt::Display;
#[cfg(not(windows))]
use std::fs::Metadata;
use std::io::BufRead;
use std::io::BufReader;
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
#[cfg(not(windows))]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
use std::path::Path;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, UNIX_EPOCH};
use std::{error::Error, fmt::Display};
use uucore::display::{print_verbatim, Quotable};
use uucore::error::FromIo;
use uucore::error::{UError, UResult, USimpleError};
use uucore::error::{FromIo, UError, UResult, USimpleError};
use uucore::line_ending::LineEnding;
use uucore::parse_glob;
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)
const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2), ('K', 1)];
#[derive(Clone)]
struct Options {
struct TraversalOptions {
all: bool,
max_depth: Option<usize>,
total: bool,
separate_dirs: bool,
one_file_system: bool,
dereference: Deref,
count_links: bool,
inodes: 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)]
@ -101,6 +106,19 @@ enum Deref {
None,
}
#[derive(Clone, Copy)]
enum Time {
Accessed,
Modified,
Created,
}
#[derive(Clone)]
enum SizeFormat {
Human(u64),
BlockSize(u64),
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct FileInfo {
file_id: u128,
@ -120,7 +138,7 @@ struct 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
let should_dereference = match &options.dereference {
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.
#[allow(clippy::cognitive_complexity)]
fn du(
mut my_stat: Stat,
options: &Options,
options: &TraversalOptions,
depth: usize,
seen_inodes: &mut HashSet<FileInfo>,
exclude: &[Pattern],
print_tx: &mpsc::Sender<UResult<StatPrintInfo>>,
) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> {
if my_stat.is_dir {
@ -317,7 +322,7 @@ fn du(
match Stat::new(&entry.path(), options) {
Ok(this_stat) => {
// We have an exclude list
for pattern in exclude {
for pattern in &options.excludes {
// Look at all patterns with both short and long paths
// if we have 'du foo' but search to exclude 'foo/bar'
// we need the full path
@ -353,14 +358,8 @@ fn du(
}
}
let this_stat = du(
this_stat,
options,
depth + 1,
seen_inodes,
exclude,
print_tx,
)?;
let this_stat =
du(this_stat, options, depth + 1, seen_inodes, print_tx)?;
if !options.separate_dirs {
my_stat.size += this_stat.size;
@ -396,58 +395,12 @@ fn du(
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)]
enum DuError {
InvalidMaxDepthArg(String),
SummarizeDepthConflict(String),
InvalidTimeStyleArg(String),
InvalidTimeArg(String),
InvalidTimeArg,
InvalidGlob(String),
}
@ -473,11 +426,9 @@ Try '{} --help' for more information.",
s.quote(),
uucore::execution_phrase()
),
Self::InvalidTimeArg(s) => write!(
Self::InvalidTimeArg => write!(
f,
"Invalid argument {} for --time.
'birth' and 'creation' arguments are not supported on this platform.",
s.quote()
"'birth' and 'creation' arguments for --time are not supported on this platform.",
),
Self::InvalidGlob(s) => write!(f, "Invalid exclude syntax: {s}"),
}
@ -492,7 +443,7 @@ impl UError for DuError {
Self::InvalidMaxDepthArg(_)
| Self::SummarizeDepthConflict(_)
| Self::InvalidTimeStyleArg(_)
| Self::InvalidTimeArg(_)
| Self::InvalidTimeArg
| Self::InvalidGlob(_) => 1,
}
}
@ -539,66 +490,17 @@ struct StatPrintInfo {
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 {
fn new(matches: ArgMatches, options: Options, summarize: bool) -> UResult<Self> {
let block_size = read_block_size(
matches
.get_one::<String>(options::BLOCK_SIZE)
.map(|s| s.as_str()),
)?;
let multiplier: u64 = if matches.get_flag(options::SI) {
1000
fn choose_size(&self, stat: &Stat) -> u64 {
if self.inodes {
stat.inodes
} else if self.apparent_size {
stat.size
} else {
1024
};
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,
})
// 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
}
}
fn print_stats(&self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> {
@ -609,7 +511,7 @@ impl StatPrinter {
match received {
Ok(message) => match message {
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 {
grand_total += size;
@ -619,7 +521,6 @@ impl StatPrinter {
.threshold
.map_or(false, |threshold| threshold.should_exclude(size))
&& self
.options
.max_depth
.map_or(true, |max_depth| stat_info.depth <= max_depth)
&& (!self.summarize || stat_info.depth == 0)
@ -633,29 +534,43 @@ impl StatPrinter {
}
}
if self.options.total {
print!("{}\ttotal", (self.convert_size)(grand_total));
if self.total {
print!("{}\ttotal", self.convert_size(grand_total));
print!("{}", self.line_ending);
}
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<()> {
if self.matches.contains_id(options::TIME) {
let tm = {
let secs = self
.matches
.get_one::<String>(options::TIME)
.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);
if let Some(time) = self.time {
let secs = get_time_secs(time, stat)?;
let tm = DateTime::<Local>::from(UNIX_EPOCH + Duration::from_secs(secs));
let time_str = tm.format(&self.time_format).to_string();
print!("{}\t{}\t", self.convert_size(size), time_str);
} else {
print!("{}\t", (self.convert_size)(size));
print!("{}\t", self.convert_size(size));
}
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]
#[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@ -690,10 +612,35 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
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),
max_depth,
total: matches.get_flag(options::TOTAL),
separate_dirs: matches.get_flag(options::SEPARATE_DIRS),
one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
dereference: if matches.get_flag(options::DEREFERENCE) {
@ -705,31 +652,49 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Deref::None
},
count_links: matches.get_flag(options::COUNT_LINKS),
inodes: matches.get_flag(options::INODES),
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))
{
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
let stat_printer = StatPrinter::new(matches.clone(), options.clone(), summarize)?;
let (print_tx, rx) = mpsc::channel::<UResult<StatPrintInfo>>();
let printing_thread = thread::spawn(move || stat_printer.print_stats(&rx));
let excludes = build_exclude_patterns(&matches)?;
'loop_file: for path in files {
// 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();
for pattern in &excludes {
for pattern in &traversal_options.excludes {
if pattern.matches(&path_string) {
// if the directory is ignored, leave early
if options.verbose {
if traversal_options.verbose {
println!("{} ignored", path_string.quote());
}
continue 'loop_file;
@ -738,13 +703,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
// 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
let mut seen_inodes: HashSet<FileInfo> = HashSet::new();
if let Some(inode) = stat.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()))?;
print_tx
@ -772,17 +737,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Ok(())
}
fn get_time_secs(s: &str, stat: &Stat) -> Result<u64, DuError> {
let secs = match s {
"ctime" | "status" => stat.modified,
"access" | "atime" | "use" => stat.accessed,
"birth" | "creation" => stat
.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 get_time_secs(time: Time, stat: &Stat) -> Result<u64, DuError> {
match time {
Time::Modified => Ok(stat.modified),
Time::Accessed => Ok(stat.accessed),
Time::Created => stat.created.ok_or(DuError::InvalidTimeArg),
}
}
fn parse_time_style(s: Option<&str>) -> UResult<&str> {

View file

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

View file

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

View file

@ -3,14 +3,15 @@
// For the full copyright and license information, please view the LICENSE
// 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::{
builder::{NonEmptyStringValueParser, ValueParser},
crate_version, Arg, ArgAction, Command,
};
use glob::{MatchOptions, Pattern};
use lscolors::LsColors;
use lscolors::{LsColors, Style};
use number_prefix::NumberPrefix;
use std::{cell::OnceCell, num::IntErrorKind};
use std::{collections::HashSet, io::IsTerminal};
@ -20,7 +21,7 @@ use std::os::windows::fs::MetadataExt;
use std::{
cmp::Reverse,
error::Error,
ffi::{OsStr, OsString},
ffi::OsString,
fmt::{Display, Write as FmtWrite},
fs::{self, DirEntry, FileType, Metadata, ReadDir},
io::{stdout, BufWriter, ErrorKind, Stdout, Write},
@ -155,6 +156,7 @@ pub mod options {
pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first";
pub static ZERO: &str = "zero";
pub static DIRED: &str = "dired";
pub static HYPERLINK: &str = "hyperlink";
}
const DEFAULT_TERM_WIDTH: u16 = 80;
@ -418,6 +420,7 @@ pub struct Config {
group_directories_first: bool,
line_ending: LineEnding,
dired: bool,
hyperlink: bool,
}
// 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.
///
/// # Arguments
@ -620,7 +642,9 @@ fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> Quot
QuotingStyle::C {
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 }
} else {
// TODO: use environment variable if available
@ -736,19 +760,18 @@ impl Config {
}
let sort = extract_sort(options);
let time = extract_time(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_si = cmd_line_bs.is_some()
let opt_block_size = options.get_one::<String>(options::size::BLOCK_SIZE);
let opt_si = opt_block_size.is_some()
&& options
.get_one::<String>(options::size::BLOCK_SIZE)
.unwrap()
.eq("si")
|| options.get_flag(options::size::SI);
let opt_hr = (cmd_line_bs.is_some()
let opt_hr = (opt_block_size.is_some()
&& options
.get_one::<String>(options::size::BLOCK_SIZE)
.unwrap()
@ -756,9 +779,9 @@ impl Config {
|| options.get_flag(options::size::HUMAN_READABLE);
let opt_kb = options.get_flag(options::size::KIBIBYTES);
let bs_env_var = std::env::var_os("BLOCK_SIZE");
let ls_bs_env_var = std::env::var_os("LS_BLOCK_SIZE");
let pc_env_var = std::env::var_os("POSIXLY_CORRECT");
let env_var_block_size = std::env::var_os("BLOCK_SIZE");
let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE");
let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT");
let size_format = if opt_si {
SizeFormat::Decimal
@ -768,13 +791,13 @@ impl Config {
SizeFormat::Bytes
};
let raw_bs = if let Some(cmd_line_bs) = cmd_line_bs {
OsString::from(cmd_line_bs)
let raw_block_size = if let Some(opt_block_size) = opt_block_size {
OsString::from(opt_block_size)
} else if !opt_kb {
if let Some(ls_bs_env_var) = ls_bs_env_var {
ls_bs_env_var
} else if let Some(bs_env_var) = bs_env_var {
bs_env_var
if let Some(env_var_ls_block_size) = env_var_ls_block_size {
env_var_ls_block_size
} else if let Some(env_var_block_size) = env_var_block_size {
env_var_block_size
} else {
OsString::from("")
}
@ -782,20 +805,18 @@ impl Config {
OsString::from("")
};
let block_size: Option<u64> = if !opt_si && !opt_hr && !raw_bs.is_empty() {
match parse_size_u64(&raw_bs.to_string_lossy()) {
let block_size: Option<u64> = if !opt_si && !opt_hr && !raw_block_size.is_empty() {
match parse_size_u64(&raw_block_size.to_string_lossy()) {
Ok(size) => Some(size),
Err(_) => {
show!(LsError::BlockSizeParseError(cmd_line_bs.unwrap().clone()));
show!(LsError::BlockSizeParseError(
opt_block_size.unwrap().clone()
));
None
}
}
} else if let Some(pc) = pc_env_var {
if pc.as_os_str() == OsStr::new("true") || pc == OsStr::new("1") {
Some(POSIXLY_CORRECT_BLOCK_SIZE)
} else {
None
}
} else if env_var_posixly_correct.is_some() {
Some(POSIXLY_CORRECT_BLOCK_SIZE)
} else {
None
};
@ -1022,6 +1043,7 @@ impl Config {
group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST),
line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)),
dired,
hyperlink,
})
}
}
@ -1156,6 +1178,19 @@ pub fn uu_app() -> Command {
.help("generate output designed for Emacs' dired (Directory Editor) mode")
.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
// options, see the comment in Config::from for the reason.
// 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 out = BufWriter::new(stdout());
let mut dired = DiredOutput::default();
let mut style_manager = StyleManager::new();
let initial_locs_len = locs.len();
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 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() {
// 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 listed_ancestors,
&mut dired,
&mut style_manager,
)?;
}
if config.dired {
@ -2069,6 +2106,7 @@ fn enter_directory(
out: &mut BufWriter<Stdout>,
listed_ancestors: &mut HashSet<FileInformation>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> {
// Create vec of entries with initial dot files
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 {
for e in entries
@ -2162,7 +2200,15 @@ fn enter_directory(
show_dir_name(&e.p_buf, 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
.remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?);
} else {
@ -2284,6 +2330,7 @@ fn display_items(
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> {
// `-Z`, `--context`:
// 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)?;
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 {
let mut longest_context_len = 1;
@ -2326,7 +2373,7 @@ fn display_items(
for i in items {
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);
}
@ -2481,6 +2528,7 @@ fn display_item_long(
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> {
let mut output_display: String = String::new();
if config.dired {
@ -2573,7 +2621,8 @@ fn display_item_long(
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 {
let (start, end) = dired::calculate_dired(
&dired.dired_positions,
@ -2655,7 +2704,8 @@ fn display_item_long(
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;
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
/// [`std::path::Path::to_string_lossy`].
#[allow(unused_variables)]
#[allow(clippy::cognitive_complexity)]
fn display_file_name(
path: &PathData,
@ -2954,6 +3003,7 @@ fn display_file_name(
prefix_context: Option<usize>,
more_info: String,
out: &mut BufWriter<Stdout>,
style_manager: &mut StyleManager,
) -> Cell {
// 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);
@ -2962,16 +3012,29 @@ fn display_file_name(
// infer it because the color codes mess up term_grid's width calculation.
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 {
let md = path.md(out);
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 {
color_name(
name,
&path.p_buf,
path.p_buf.symlink_metadata().ok().as_ref(),
ls_colors,
style_manager,
)
};
}
@ -3060,6 +3123,7 @@ fn display_file_name(
&target_data.p_buf,
Some(&target_metadata),
ls_colors,
style_manager,
));
}
} else {
@ -3094,11 +3158,50 @@ fn display_file_name(
}
}
fn color_name(name: String, path: &Path, md: Option<&Metadata>, ls_colors: &LsColors) -> String {
match ls_colors.style_for_path_with_metadata(path, md) {
Some(style) => {
return style.to_nu_ansi_term_style().paint(name).to_string();
/// We need this struct to be able to store the previous style.
/// This because we need to check the previous value in case we don't need
/// the reset
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,
}
}

View file

@ -341,7 +341,7 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
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());
}

View file

@ -2,6 +2,8 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// cSpell:ignore sysconf
use crate::word_count::WordCount;
use super::WordCountable;
@ -11,11 +13,19 @@ use std::fs::OpenOptions;
use std::io::{self, ErrorKind, Read};
#[cfg(unix)]
use libc::S_IFREG;
use libc::{sysconf, S_IFREG, _SC_PAGESIZE};
#[cfg(unix)]
use nix::sys::stat;
#[cfg(unix)]
use std::io::{Seek, SeekFrom};
#[cfg(any(target_os = "linux", target_os = "android"))]
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"))]
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.
/// 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.
/// 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
/// other things such as lines and words.
#[inline]
@ -87,11 +99,60 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
// If stat.st_size = 0 then
// - either the size is 0
// - or the size is unknown.
// The second case happens for files in pseudo-filesystems. For
// example with /proc/version and /sys/kernel/profiling. 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);
// The second case happens for files in pseudo-filesystems.
// For example with /proc/version.
// So, if it is 0 we don't report that and instead do a full read.
//
// 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"))]
{
@ -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.
let mut buf = [0_u8; BUF_SIZE];
loop {

View file

@ -17,12 +17,14 @@ use std::os::unix::io::AsRawFd;
pub trait WordCountable: AsRawFd + Read {
type Buffered: BufRead;
fn buffered(self) -> Self::Buffered;
fn inner_file(&mut self) -> Option<&mut File>;
}
#[cfg(not(unix))]
pub trait WordCountable: Read {
type Buffered: BufRead;
fn buffered(self) -> Self::Buffered;
fn inner_file(&mut self) -> Option<&mut File>;
}
impl WordCountable for StdinLock<'_> {
@ -31,6 +33,9 @@ impl WordCountable for StdinLock<'_> {
fn buffered(self) -> Self::Buffered {
self
}
fn inner_file(&mut self) -> Option<&mut File> {
None
}
}
impl WordCountable for File {
@ -39,4 +44,8 @@ impl WordCountable for File {
fn buffered(self) -> Self::Buffered {
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 = []
# * non-default features
backup-control = []
colors = []
encoding = ["data-encoding", "data-encoding-macro", "z85", "thiserror"]
entries = ["libc"]
fs = ["dunce", "libc", "winapi-util", "windows-sys"]

View file

@ -6,6 +6,8 @@
#[cfg(feature = "backup-control")]
pub mod backup_control;
#[cfg(feature = "colors")]
pub mod colors;
#[cfg(feature = "encoding")]
pub mod encoding;
#[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 = "freebsd"),
not(target_os = "netbsd"),
not(target_os = "openbsd"),
not(target_os = "illumos"),
not(target_os = "solaris"),
not(target_arch = "aarch64"),
@ -130,6 +131,7 @@ impl FileInformation {
target_os = "android",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "illumos",
target_os = "solaris",
target_arch = "aarch64",
@ -146,13 +148,14 @@ impl FileInformation {
#[cfg(unix)]
pub fn inode(&self) -> u64 {
#[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"
))]
return self.0.st_ino;
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
not(target_pointer_width = "64")
))]
return self.0.st_ino.into();

View file

@ -497,7 +497,10 @@ impl FsUsage {
#[cfg(unix)]
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 {
blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ?
blocks: statvfs.f_blocks,
@ -507,7 +510,10 @@ impl FsUsage {
files: statvfs.f_files,
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 {
blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ?
blocks: statvfs.f_blocks.into(),
@ -530,6 +536,19 @@ impl FsUsage {
files: statvfs.f_files,
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))]
@ -617,6 +636,7 @@ impl FsMeta for StatFs {
not(target_vendor = "apple"),
not(target_os = "android"),
not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(target_os = "illumos"),
not(target_os = "solaris"),
not(target_arch = "s390x"),
@ -630,6 +650,7 @@ impl FsMeta for StatFs {
target_arch = "s390x",
target_vendor = "apple",
target_os = "android",
target_os = "openbsd",
not(target_pointer_width = "64")
)
))]
@ -655,11 +676,19 @@ impl FsMeta for StatFs {
return self.f_bfree.into();
}
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;
#[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();
#[cfg(target_os = "freebsd")]
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
return self.f_bavail.try_into().unwrap();
}
fn total_file_nodes(&self) -> u64 {

View file

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

View file

@ -13,7 +13,7 @@ use std::os::unix::fs;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(all(unix, not(target_os = "freebsd")))]
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(windows)]
use std::os::windows::fs::symlink_file;
@ -2381,13 +2381,18 @@ fn test_copy_symlink_force() {
}
#[test]
#[cfg(all(unix, not(target_os = "freebsd")))]
#[cfg(unix)]
fn test_no_preserve_mode() {
use std::os::unix::prelude::MetadataExt;
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!();
at.touch("file");
@ -2407,11 +2412,16 @@ fn test_no_preserve_mode() {
}
#[test]
#[cfg(all(unix, not(target_os = "freebsd")))]
#[cfg(unix)]
fn test_preserve_mode() {
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!();
at.touch("file");

View file

@ -159,6 +159,18 @@ fn test_quoting() {
.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]
fn test_extra_operand() {
new_ucmd!()

View file

@ -365,12 +365,19 @@ fn test_du_no_dereference() {
.stdout_does_not_contain(symlink);
// ensure dereference "wins"
ts.ucmd()
.arg(arg)
.arg("--dereference")
.succeeds()
.stdout_contains(symlink)
.stdout_does_not_contain(dir);
let result = ts.ucmd().arg(arg).arg("--dereference").succeeds();
#[cfg(target_os = "linux")]
{
let result_reference = unwrap_or_return!(expected_result(&ts, &[arg, "--dereference"]));
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]
fn test_du_inodes_with_count_links() {
let ts = TestScenario::new(util_name!());

View file

@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// 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"))]
use crate::common::util::expected_result;
@ -864,11 +864,11 @@ fn test_ls_zero() {
.succeeds()
.stdout_only("\"0-test-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00");
scene
.ucmd()
.args(&["--zero", "--color=always"])
.succeeds()
.stdout_only("\x1b[1;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00");
let result = scene.ucmd().args(&["--zero", "--color=always"]).succeeds();
assert_eq!(
result.stdout_str(),
"\u{1b}[0m\u{1b}[01;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00"
);
scene
.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",
);
scene
.ucmd()
.args(&["--zero", "--color=always"])
.succeeds()
.stdout_only(
"\x1b[1;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00",
let result = scene.ucmd().args(&["--zero", "--color=always"]).succeeds();
assert_eq!(result.stdout_str(),
"\u{1b}[0m\u{1b}[01;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00",
);
scene
@ -1202,12 +1199,21 @@ fn test_ls_long_symlink_color() {
}
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) {
Some(captures) => (
captures.get(1).unwrap().as_str().to_string(),
captures.get(2).unwrap().as_str().to_string(),
),
Some(captures) => {
dbg!(captures.get(1).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()),
}
}
@ -1995,9 +2001,9 @@ fn test_ls_color() {
at.touch(nested_file);
at.touch("test-color");
let a_with_colors = "\x1b[1;34ma\x1b[0m";
let z_with_colors = "\x1b[1;34mz\x1b[0m";
let nested_dir_with_colors = "\x1b[1;34mnested_dir\x1b[0m"; // spell-checker:disable-line
let a_with_colors = "\x1b[0m\x1b[01;34ma\x1b[0m";
let z_with_colors = "\x1b[01;34mz\x1b[0m\n";
let nested_dir_with_colors = "\x1b[0m\x1b[01;34mnested_dir\x1b[0m\x0anested_file"; // spell-checker:disable-line
// Color is disabled by default
let result = scene.ucmd().succeeds();
@ -2006,12 +2012,9 @@ fn test_ls_color() {
// Color should be enabled
for param in ["--color", "--col", "--color=always", "--col=always"] {
scene
.ucmd()
.arg(param)
.succeeds()
.stdout_contains(a_with_colors)
.stdout_contains(z_with_colors);
let result = scene.ucmd().arg(param).succeeds();
assert!(result.stdout_str().contains(a_with_colors));
assert!(result.stdout_str().contains(z_with_colors));
}
// Color should be disabled
@ -2020,12 +2023,8 @@ fn test_ls_color() {
assert!(!result.stdout_str().contains(z_with_colors));
// Nested dir should be shown and colored
scene
.ucmd()
.arg("--color")
.arg("a")
.succeeds()
.stdout_contains(nested_dir_with_colors);
let result = scene.ucmd().arg("--color").arg("a").succeeds();
assert!(result.stdout_str().contains(nested_dir_with_colors));
// No output
scene
@ -2037,13 +2036,18 @@ fn test_ls_color() {
// The colors must not mess up the grid layout
at.touch("b");
scene
let result = scene
.ucmd()
.arg("--color")
.arg("-w=15")
.arg("-C")
.succeeds()
.stdout_only(format!("{a_with_colors} test-color\nb {z_with_colors}\n"));
.succeeds();
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)]
@ -2468,13 +2472,16 @@ fn test_ls_quoting_style() {
{
at.touch("one\ntwo");
at.touch("one\\two");
// Default is shell-escape
// Default is literal, when stdout is not a TTY.
// Otherwise, it is shell-escape
scene
.ucmd()
.arg("--hide-control-chars")
.arg("one\ntwo")
.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 [
("--quoting-style=literal", "one?two"),
@ -2561,7 +2568,9 @@ fn test_ls_quoting_style() {
.ucmd()
.arg("one two")
.succeeds()
.stdout_only("'one two'\n");
.stdout_only("one two\n");
// TODO: TTY-expected output
// .stdout_only("'one two'\n");
for (arg, correct) in [
("--quoting-style=literal", "one two"),
@ -2624,7 +2633,9 @@ fn test_ls_quoting_and_color() {
.arg("--color")
.arg("one two")
.succeeds()
.stdout_only("'one two'\n");
.stdout_only("one two\n");
// TODO: TTY-expected output
// .stdout_only("'one two'\n");
}
#[test]
@ -3156,11 +3167,8 @@ fn test_ls_path() {
.stdout_is(expected_stdout);
let abs_path = format!("{}/{}", at.as_string(), path);
let expected_stdout = if cfg!(windows) {
format!("\'{abs_path}\'\n")
} else {
format!("{abs_path}\n")
};
let expected_stdout = format!("{abs_path}\n");
scene.ucmd().arg(&abs_path).run().stdout_is(expected_stdout);
let expected_stdout = format!("{path}\n{file1}\n");
@ -3828,3 +3836,80 @@ fn test_ls_cf_output_should_be_delimited_by_tab() {
.succeeds()
.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));
}
#[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]
fn test_mv_overwrite_nonempty_dir() {
let (at, mut ucmd) = at_and_ucmd!();

View file

@ -553,7 +553,7 @@ fn test_nonexistent_file_is_not_symlink() {
}
#[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
#[cfg(not(any(windows, target_os = "freebsd")))]
fn test_file_is_sticky() {

View file

@ -243,6 +243,14 @@ fn test_single_only_lines() {
.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]
fn test_single_all_counts() {
new_ucmd!()
@ -419,6 +427,14 @@ fn test_files_from_pseudo_filesystem() {
use pretty_assertions::assert_ne;
let result = new_ucmd!().arg("-c").arg("/proc/cpuinfo").succeeds();
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]

View file

@ -1,8 +1,5 @@
# 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
@ -46,40 +43,26 @@ TERM xterm*
# 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:
RESET 0
DIR 01;34
LINK 01;36
MULTIHARDLINK 00
FIFO 40;33
SOCK 01;35
DOOR 01;35
BLK 40;33;01
CHR 40;33;01
ORPHAN 40;31;01
MISSING 00
SETUID 37;41
SETGID 30;43
CAPABILITY 00
STICKY_OTHER_WRITABLE 30;42
OTHER_WRITABLE 34;42
STICKY 37;44
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
@ -126,7 +109,6 @@ EXEC 01;32
.swm 01;31
.dwm 01;31
.esd 01;31
# image formats
.avif 01;35
.jpg 01;35
.jpeg 01;35
@ -176,10 +158,8 @@ EXEC 01;32
.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
@ -192,12 +172,10 @@ EXEC 01;32
.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