From f4a5de1fbf498e8fcfce015e253124849889a20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=B8rl=C3=BCck=20Berg?= <36937807+henrikhorluck@users.noreply.github.com> Date: Sat, 29 Jul 2023 13:28:02 +0200 Subject: [PATCH] Port builtins/path to Rust --- CMakeLists.txt | 2 +- fish-rust/src/builtins/mod.rs | 1 + fish-rust/src/builtins/path.rs | 1004 ++++++++++++++++++++ fish-rust/src/builtins/shared.rs | 149 ++- fish-rust/src/builtins/string.rs | 140 +-- fish-rust/src/builtins/string/collect.rs | 4 +- fish-rust/src/builtins/string/escape.rs | 2 +- fish-rust/src/builtins/string/join.rs | 8 +- fish-rust/src/builtins/string/length.rs | 2 +- fish-rust/src/builtins/string/match.rs | 2 +- fish-rust/src/builtins/string/pad.rs | 2 +- fish-rust/src/builtins/string/repeat.rs | 2 +- fish-rust/src/builtins/string/replace.rs | 2 +- fish-rust/src/builtins/string/shorten.rs | 2 +- fish-rust/src/builtins/string/split.rs | 8 +- fish-rust/src/builtins/string/sub.rs | 2 +- fish-rust/src/builtins/string/transform.rs | 2 +- fish-rust/src/builtins/string/trim.rs | 2 +- fish-rust/src/builtins/string/unescape.rs | 2 +- fish-rust/src/wutil/mod.rs | 14 +- src/builtin.cpp | 6 +- src/builtin.h | 1 + src/builtins/path.cpp | 952 ------------------- src/builtins/path.h | 10 - 24 files changed, 1200 insertions(+), 1121 deletions(-) create mode 100644 fish-rust/src/builtins/path.rs delete mode 100644 src/builtins/path.cpp delete mode 100644 src/builtins/path.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bc91c5003..3090676b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ set(FISH_BUILTIN_SRCS src/builtins/disown.cpp src/builtins/eval.cpp src/builtins/fg.cpp src/builtins/functions.cpp src/builtins/history.cpp - src/builtins/jobs.cpp src/builtins/path.cpp + src/builtins/jobs.cpp src/builtins/read.cpp src/builtins/set.cpp src/builtins/source.cpp src/builtins/ulimit.cpp diff --git a/fish-rust/src/builtins/mod.rs b/fish-rust/src/builtins/mod.rs index 37a29e33e..85432820a 100644 --- a/fish-rust/src/builtins/mod.rs +++ b/fish-rust/src/builtins/mod.rs @@ -13,6 +13,7 @@ pub mod emit; pub mod exit; pub mod function; pub mod math; +pub mod path; pub mod printf; pub mod pwd; pub mod random; diff --git a/fish-rust/src/builtins/path.rs b/fish-rust/src/builtins/path.rs new file mode 100644 index 000000000..8ea727f5d --- /dev/null +++ b/fish-rust/src/builtins/path.rs @@ -0,0 +1,1004 @@ +use crate::env::environment::Environment; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::os::unix::prelude::{FileTypeExt, MetadataExt}; +use std::time::SystemTime; + +use crate::path::path_apply_working_directory; +use crate::util::wcsfilecmp_glob; +use crate::wcstringutil::split_string_tok; +use crate::wutil::{ + file_id_for_path, lwstat, normalize_path, waccess, wbasename, wdirname, wrealpath, wstat, + INVALID_FILE_ID, +}; +use crate::{ + builtins::shared::{ + builtin_missing_argument, builtin_print_error_trailer, builtin_print_help, io_streams_t, + Arguments, SplitBehavior, BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_INVALID_SUBCMD, + BUILTIN_ERR_MISSING_SUBCMD, BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN, + STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS, + }, + ffi::{parser_t, separation_type_t}, + wchar::{wstr, WString, L}, + wchar_ext::{ToWString, WExt}, + wgetopt::{wgetopter_t, wopt, woption, woption_argument_t::*, NONOPTION_CHAR_CODE}, + wutil::wgettext_fmt, +}; +use bitflags::bitflags; +use libc::{ + c_int, getegid, geteuid, mode_t, uid_t, F_OK, PATH_MAX, R_OK, S_ISGID, S_ISUID, W_OK, X_OK, +}; + +use super::shared::BuiltinCmd; + +macro_rules! path_error { + ( + $streams:expr, + $string:expr + $(, $args:expr)+ + $(,)? + ) => { + $streams.err.append(L!("path ")); + $streams.err.append(wgettext_fmt!($string, $($args),*)); + }; +} + +fn path_unknown_option( + parser: &mut parser_t, + streams: &mut io_streams_t, + subcmd: &wstr, + opt: &wstr, +) { + path_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt); + builtin_print_error_trailer(parser, streams, L!("path")); +} + +// How many bytes we read() at once. +// We use PATH_MAX here so we always get at least one path, +// and so we can automatically detect NULL-separated input. +const PATH_CHUNK_SIZE: usize = PATH_MAX as usize; +#[inline] +fn arguments<'iter, 'args>( + args: &'iter [&'args wstr], + optind: &'iter mut usize, + streams: &mut io_streams_t, +) -> Arguments<'args, 'iter> { + Arguments::new(args, optind, streams, PATH_CHUNK_SIZE) +} + +bitflags! { + #[derive(Default)] + pub struct TypeFlags: u32 { + /// A block device + const BLOCK = 1 << 0; + /// A directory + const DIR = 1 << 1; + /// A regular file + const FILE = 1 << 2; + /// A link + const LINK = 1 << 3; + /// A character device + const CHAR = 1 << 4; + /// A fifo + const FIFO = 1 << 5; + /// A socket + const SOCK = 1 << 6; + } +} + +impl TryFrom<&wstr> for TypeFlags { + type Error = (); + + fn try_from(value: &wstr) -> Result { + let flag = match value { + t if t == "file" => Self::FILE, + t if t == "dir" => Self::DIR, + t if t == "block" => Self::BLOCK, + t if t == "char" => Self::CHAR, + t if t == "fifo" => Self::FIFO, + t if t == "socket" => Self::SOCK, + t if t == "link" => Self::LINK, + _ => return Err(()), + }; + + Ok(flag) + } +} + +bitflags! { + #[derive(Default)] + pub struct PermFlags: u32 { + const READ = 1 << 0; + const WRITE = 1 << 1; + const EXEC = 1 << 2; + const SUID = 1 << 3; + const SGID = 1 << 4; + const USER = 1 << 5; + const GROUP = 1 << 6; + } +} + +impl PermFlags { + fn is_special(self) -> bool { + self.intersects(Self::SUID | Self::SGID | Self::USER | Self::GROUP) + } +} + +impl TryFrom<&wstr> for PermFlags { + type Error = (); + + fn try_from(value: &wstr) -> Result { + let flag = match value { + t if t == "read" => Self::READ, + t if t == "write" => Self::WRITE, + t if t == "exec" => Self::EXEC, + t if t == "suid" => Self::SUID, + t if t == "sgid" => Self::SGID, + t if t == "user" => Self::USER, + t if t == "group" => Self::GROUP, + _ => return Err(()), + }; + + Ok(flag) + } +} + +/// This is used by the subcommands to communicate with the option parser which flags are +/// valid and get the result of parsing the command for flags. +#[derive(Default)] +struct Options<'args> { + null_in: bool, + null_out: bool, + quiet: bool, + + invert_valid: bool, + invert: bool, + + relative_valid: bool, + relative: bool, + + reverse_valid: bool, + reverse: bool, + + unique_valid: bool, + unique: bool, + + key: Option<&'args wstr>, + + types_valid: bool, + types: Option, + + perms_valid: bool, + perms: Option, + + arg1: Option<&'args wstr>, +} + +#[inline] +fn path_out(streams: &mut io_streams_t, opts: &Options<'_>, s: impl AsRef) { + let s = s.as_ref(); + if !opts.quiet { + if !opts.null_out { + streams + .out + .append_with_separation(s, separation_type_t::explicitly, true); + } else { + let mut output = WString::with_capacity(s.len() + 1); + output.push_utfstr(s); + output.push('\0'); + streams.out.append(output); + } + } +} + +fn construct_short_opts(opts: &Options) -> WString { + // All commands accept -z, -Z and -q + let mut short_opts = WString::from(":zZq"); + if opts.perms_valid { + short_opts += L!("p:"); + short_opts += L!("rwx"); + } + + if opts.types_valid { + short_opts += L!("t:"); + short_opts += L!("fld"); + } + + if opts.invert_valid { + short_opts.push('v'); + } + if opts.relative_valid { + short_opts.push('R'); + } + if opts.reverse_valid { + short_opts.push('r'); + } + if opts.unique_valid { + short_opts.push('u'); + } + + short_opts +} + +/// Note that several long flags share the same short flag. That is okay. The caller is expected +/// to indicate that a max of one of the long flags sharing a short flag is valid. +/// Remember: adjust the completions in share/completions/ when options change +const LONG_OPTIONS: [woption<'static>; 10] = [ + wopt(L!("quiet"), no_argument, 'q'), + wopt(L!("null-in"), no_argument, 'z'), + wopt(L!("null-out"), no_argument, 'Z'), + wopt(L!("perm"), required_argument, 'p'), + wopt(L!("type"), required_argument, 't'), + wopt(L!("invert"), no_argument, 'v'), + wopt(L!("relative"), no_argument, 'R'), + wopt(L!("reverse"), no_argument, 'r'), + wopt(L!("unique"), no_argument, 'u'), + wopt(L!("key"), required_argument, NONOPTION_CHAR_CODE), +]; + +fn parse_opts<'args>( + opts: &mut Options<'args>, + optind: &mut usize, + n_req_args: usize, + args: &mut [&'args wstr], + parser: &mut parser_t, + streams: &mut io_streams_t, +) -> Option { + let cmd = args[0]; + let mut args_read = Vec::with_capacity(args.len()); + args_read.extend_from_slice(args); + + let short_opts = construct_short_opts(opts); + + let mut w = wgetopter_t::new(&short_opts, &LONG_OPTIONS, args); + while let Some(c) = w.wgetopt_long() { + match c { + ':' => { + streams.err.append(L!("path ")); // clone of string_error + builtin_missing_argument(parser, streams, cmd, args_read[w.woptind - 1], false); + return STATUS_INVALID_ARGS; + } + '?' => { + path_unknown_option(parser, streams, cmd, args_read[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + 'q' => { + opts.quiet = true; + continue; + } + 'z' => { + opts.null_in = true; + continue; + } + 'Z' => { + opts.null_out = true; + continue; + } + 'v' if opts.invert_valid => { + opts.invert = true; + continue; + } + 't' if opts.types_valid => { + let types = opts.types.get_or_insert_with(TypeFlags::default); + let types_args = split_string_tok(w.woptarg.unwrap(), L!(","), None); + for t in types_args { + let Ok(r#type) = t.try_into() else { + path_error!(streams, "%ls: Invalid type '%ls'\n", "path", t); + return STATUS_INVALID_ARGS; + }; + *types |= r#type; + } + continue; + } + 'p' if opts.perms_valid => { + let perms = opts.perms.get_or_insert_with(PermFlags::default); + let perms_args = split_string_tok(w.woptarg.unwrap(), L!(","), None); + for p in perms_args { + let Ok(perm) = p.try_into() else { + path_error!(streams, "%ls: Invalid permission '%ls'\n", "path", p); + return STATUS_INVALID_ARGS; + }; + *perms |= perm; + } + continue; + } + 'r' if opts.reverse_valid => { + opts.reverse = true; + continue; + } + 'r' if opts.perms_valid => { + let perms = opts.perms.get_or_insert_with(PermFlags::default); + *perms |= PermFlags::READ; + continue; + } + 'w' if opts.perms_valid => { + let perms = opts.perms.get_or_insert_with(PermFlags::default); + *perms |= PermFlags::WRITE; + continue; + } + 'x' if opts.perms_valid => { + let perms = opts.perms.get_or_insert_with(PermFlags::default); + *perms |= PermFlags::EXEC; + continue; + } + 'f' if opts.types_valid => { + let types = opts.types.get_or_insert_with(TypeFlags::default); + *types |= TypeFlags::FILE; + continue; + } + 'l' if opts.types_valid => { + let types = opts.types.get_or_insert_with(TypeFlags::default); + *types |= TypeFlags::LINK; + continue; + } + 'd' if opts.types_valid => { + let types = opts.types.get_or_insert_with(TypeFlags::default); + *types |= TypeFlags::DIR; + continue; + } + 'u' if opts.unique_valid => { + opts.unique = true; + continue; + } + 'R' if opts.relative_valid => { + opts.relative = true; + continue; + } + NONOPTION_CHAR_CODE => { + assert!(w.woptarg.is_some()); + opts.key = w.woptarg; + continue; + } + _ => { + path_unknown_option(parser, streams, cmd, args_read[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + } + } + + *optind = w.woptind; + + if n_req_args != 0 { + assert!(n_req_args == 1); + opts.arg1 = args.get(*optind).copied(); + if opts.arg1.is_some() { + *optind += 1; + } + + if opts.arg1.is_none() && n_req_args == 1 { + path_error!(streams, BUILTIN_ERR_ARG_COUNT0, cmd); + return STATUS_INVALID_ARGS; + } + } + + // At this point we should not have optional args and be reading args from stdin. + if streams.stdin_is_directly_redirected() && args.len() > *optind { + path_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); + return STATUS_INVALID_ARGS; + } + + STATUS_CMD_OK +} + +fn path_transform( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], + func: impl Fn(&wstr) -> WString, +) -> Option { + let mut opts = Options::default(); + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let mut n_transformed = 0; + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + for (arg, _) in arguments { + // Empty paths make no sense, but e.g. wbasename returns true for them. + if arg.is_empty() { + continue; + } + let transformed = func(&arg); + if transformed != arg { + n_transformed += 1; + // Return okay if path wasn't already in this form + // TODO: Is that correct? + if opts.quiet { + return STATUS_CMD_OK; + }; + } + path_out(streams, &opts, transformed); + } + + if n_transformed > 0 { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} + +fn path_basename( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + path_transform(parser, streams, args, |s| wbasename(s).to_owned()) +} + +fn path_dirname( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + path_transform(parser, streams, args, |s| wdirname(s).to_owned()) +} + +fn normalize_help(path: &wstr) -> WString { + let mut np = normalize_path(path, false); + if !np.is_empty() && np.char_at(0) == '-' { + np = "./".chars().chain(np.chars()).collect(); + } + np +} + +fn path_normalize( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + path_transform(parser, streams, args, normalize_help) +} + +fn path_mtime( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let mut opts = Options::default(); + opts.relative_valid = true; + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let mut n_transformed = 0; + + let t = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => dur.as_secs() as i64, + Err(err) => -(err.duration().as_secs() as i64), + }; + + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + for (arg, _) in arguments { + let ret = file_id_for_path(&arg); + + if ret != INVALID_FILE_ID { + if opts.quiet { + return STATUS_CMD_OK; + } + n_transformed += 1; + if !opts.relative { + path_out(streams, &opts, (ret.mod_seconds).to_wstring()); + } else { + // note that the mod time can actually be before the system time + // so this can end up negative + #[allow(clippy::unnecessary_cast)] + path_out(streams, &opts, (t - ret.mod_seconds as i64).to_wstring()); + } + } + } + + if n_transformed > 0 { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} + +fn find_extension(path: &wstr) -> Option { + // The extension belongs to the basename, + // if there is a "." before the last component it doesn't matter. + // e.g. ~/.config/fish/conf.d/foo + // does not have an extension! The ".d" here is not a file extension for "foo". + // And "~/.config" doesn't have an extension either - the ".config" is the filename. + let filename = wbasename(path); + + // "." and ".." aren't really *files* and therefore don't have an extension. + if filename == "." || filename == ".." { + return None; + } + + // If we don't have a "." or the "." is the first in the filename, + // we do not have an extension + let pos = filename.chars().rposition(|c| c == '.'); + match pos { + None | Some(0) => None, + // Convert pos back to what it would be in the original path. + Some(pos) => Some(pos + path.len() - filename.len()), + } +} + +#[test] +fn test_find_extension() { + let cases = [ + (L!("foo.wmv"), Some(3)), + (L!("verylongfilename.wmv"), Some("verylongfilename".len())), + (L!("foo"), None), + (L!(".foo"), None), + (L!("./foo.wmv"), Some(5)), + ]; + + for (f, ext_idx) in cases { + assert_eq!(find_extension(f), ext_idx); + } +} + +fn path_extension( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let mut opts = Options::default(); + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let mut n_transformed = 0; + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + for (arg, _) in arguments { + let pos = find_extension(&arg); + let Some(pos) = pos else { + // If there is no extension the extension is empty. + // This is unambiguous because we include the ".". + path_out(streams, &opts, L!("")); + continue; + }; + + let ext = arg.slice_from(pos); + if opts.quiet && !ext.is_empty() { + return STATUS_CMD_OK; + } + path_out(streams, &opts, ext); + n_transformed += 1; + } + + if n_transformed > 0 { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} + +fn path_change_extension( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let mut opts = Options::default(); + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 1, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let mut n_transformed = 0usize; + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + for (mut arg, _) in arguments { + let pos = find_extension(&arg); + let mut ext = match pos { + Some(pos) => { + arg.to_mut().truncate(pos); + arg.into_owned() + } + None => arg.into_owned(), + }; + + // Only add on the extension "." if we have something. + // That way specifying an empty extension strips it. + if let Some(replacement) = opts.arg1 { + if !replacement.is_empty() { + if replacement.char_at(0) != '.' { + ext.push('.'); + } + ext.push_utfstr(replacement); + } + } + path_out(streams, &opts, ext); + + n_transformed += 1; + } + + if n_transformed > 0 { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} + +fn path_resolve( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let mut opts = Options::default(); + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let mut n_transformed = 0usize; + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + for (arg, _) in arguments { + let mut real = match wrealpath(&arg) { + Some(p) => p, + None => { + // The path doesn't exist, isn't readable or a symlink loop. + // We go up until we find something that works. + let mut next = arg.into_owned(); + // First add $PWD if we're relative + if !next.is_empty() && next.char_at(0) != '/' { + next = path_apply_working_directory(&next, &parser.get_vars().get_pwd_slash()); + } + let mut rest = wbasename(&next).to_owned(); + let mut real = None; + while !next.is_empty() && next != "/" { + next = wdirname(&next).to_owned(); + real = wrealpath(&next); + if let Some(ref mut real) = real { + real.push('/'); + real.push_utfstr(&rest); + *real = normalize_path(real, false); + break; + } + rest = (wbasename(&next).to_owned() + L!("/")) + rest.as_utfstr(); + } + + match real { + Some(p) => p, + None => continue, + } + } + }; + + // Normalize the path so "../" components are eliminated even after + // nonexistent or non-directory components. + // Otherwise `path resolve foo/../` will be `$PWD/foo/../` if foo is a file. + real = normalize_path(&real, false); + + // Return 0 if we found a realpath. + if opts.quiet { + return STATUS_CMD_OK; + } + path_out(streams, &opts, real); + n_transformed += 1; + } + + if n_transformed > 0 { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} + +fn path_sort( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let mut opts = Options::default(); + opts.reverse_valid = true; + opts.unique_valid = true; + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let keyfunc: &dyn Fn(&wstr) -> &wstr = match &opts.key { + Some(k) if k == "basename" => &wbasename as _, + Some(k) if k == "dirname" => &wdirname as _, + Some(k) if k == "path" => { + // Act as if --key hadn't been given. + opts.key = None; + &wbasename as _ + } + None => &wbasename as _, + Some(k) => { + path_error!(streams, "%ls: Invalid sort key '%ls'\n", args[0], k); + return STATUS_INVALID_ARGS; + } + }; + + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + + let owning_list: Vec<_> = arguments.map(|(f, _)| f).collect(); + let mut list: Vec<&wstr> = owning_list.iter().map(|f| f.as_ref()).collect(); + + if opts.key.is_some() { + // Keep a map to avoid repeated keyfunc calls + let key: HashMap<&wstr, &wstr> = list + .iter() + .map(|f| (<&wstr>::clone(f), keyfunc(f))) + .collect(); + + // We use a stable sort here + list.sort_by(|a, b| { + match wcsfilecmp_glob(key[a], key[b]) { + // to avoid changing the order so we can chain calls + Ordering::Equal => Ordering::Greater, + order if opts.reverse => order.reverse(), + order => order, + } + }); + + if opts.unique { + // we are sorted, dedup will remove all duplicates + list.dedup_by(|a, b| key[a] == key[b]); + } + } else { + // Without --key, we just sort by the entire path, + // so we have no need to transform and such. + list.sort_by(|a, b| { + match wcsfilecmp_glob(a, b) { + // to avoid changing the order so we can chain calls + Ordering::Equal => Ordering::Greater, + order if opts.reverse => order.reverse(), + order => order, + } + }); + + if opts.unique { + // we are sorted, dedup will remove all duplicates + list.dedup(); + } + } + + for entry in list { + path_out(streams, &opts, entry); + } + + /* TODO: Return true only if already sorted? */ + STATUS_CMD_OK +} + +fn filter_path(opts: &Options, path: &wstr) -> bool { + // TODO: Add moar stuff: + // fifos, sockets, size greater than zero, setuid, ... + // Nothing to check, file existence is checked elsewhere. + if opts.types.is_none() && opts.perms.is_none() { + return true; + } + + if let Some(t) = opts.types { + let mut type_ok = false; + if t.contains(TypeFlags::LINK) { + let md = lwstat(path); + type_ok = md.is_some() && md.unwrap().is_symlink(); + } + let Some(md) = wstat(path) else { + // Does not exist + return false; + }; + + let ft = md.file_type(); + type_ok = match type_ok { + true => true, + _ if t.contains(TypeFlags::FILE) && ft.is_file() => true, + _ if t.contains(TypeFlags::DIR) && ft.is_dir() => true, + _ if t.contains(TypeFlags::BLOCK) && ft.is_block_device() => true, + _ if t.contains(TypeFlags::CHAR) && ft.is_char_device() => true, + _ if t.contains(TypeFlags::FIFO) && ft.is_fifo() => true, + _ if t.contains(TypeFlags::SOCK) && ft.is_socket() => true, + _ => false, + }; + + if !type_ok { + return false; + } + } + + if let Some(perm) = opts.perms { + let mut amode = 0; + // TODO: Update bitflags so this works + /* + for f in perm { + amode |= match f { + PermFlags::READ => R_OK, + PermFlags::WRITE => W_OK, + PermFlags::EXEC => X_OK, + _ => PermFlags::empty(), + } + } + */ + if perm.contains(PermFlags::READ) { + amode |= R_OK; + } + if perm.contains(PermFlags::WRITE) { + amode |= W_OK; + } + if perm.contains(PermFlags::EXEC) { + amode |= X_OK; + } + // access returns 0 on success, + // -1 on failure. Yes, C can't even keep its bools straight. + if waccess(path, amode) != 0 { + return false; + } + + // Permissions that require special handling + + if perm.is_special() { + let Some(md) = wstat(path) else { + // Does not exist, even though we just checked we can access it + // likely some kind of race condition + // We might want to warn the user about this? + return false; + }; + + #[allow(clippy::if_same_then_else)] + if perm.contains(PermFlags::SUID) && (md.mode() as mode_t & S_ISUID) == 0 { + return false; + } else if perm.contains(PermFlags::SGID) && (md.mode() as mode_t & S_ISGID) == 0 { + return false; + } else if perm.contains(PermFlags::USER) && (unsafe { geteuid() } != md.uid() as uid_t) + { + return false; + } else if perm.contains(PermFlags::GROUP) && (unsafe { getegid() } != md.gid() as uid_t) + { + return false; + } + } + } + + // No filters failed. + true +} + +fn path_filter_maybe_is( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], + is_is: bool, +) -> Option { + let mut opts = Options::default(); + opts.types_valid = true; + opts.perms_valid = true; + opts.invert_valid = true; + let mut optind = 0; + let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + // If we have been invoked as "path is", which is "path filter -q". + if is_is { + opts.quiet = true; + } + + let mut n_transformed = 0; + let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in { + true => SplitBehavior::Null, + false => SplitBehavior::InferNull, + }); + for (arg, _) in arguments.filter(|(f, _)| { + (opts.perms.is_none() && opts.types.is_none()) || (filter_path(&opts, f) != opts.invert) + }) { + // If we don't have filters, check if it exists. + if opts.perms.is_none() && opts.types.is_none() { + let ok = waccess(&arg, F_OK) == 0; + if ok == opts.invert { + continue; + } + } + + // We *know* this is a filename, + // and so if it starts with a `-` we *know* it is relative + // to $PWD. So we can add `./`. + // Empty paths make no sense, but e.g. wbasename returns true for them. + if !arg.is_empty() && arg.starts_with('-') { + let out = WString::from("./") + arg.as_ref(); + path_out(streams, &opts, out); + } else { + path_out(streams, &opts, arg); + } + n_transformed += 1; + if opts.quiet { + return STATUS_CMD_OK; + }; + } + + if n_transformed > 0 { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} + +fn path_filter( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + path_filter_maybe_is(parser, streams, args, false) +} + +fn path_is(parser: &mut parser_t, streams: &mut io_streams_t, args: &mut [&wstr]) -> Option { + path_filter_maybe_is(parser, streams, args, true) +} + +/// The path builtin, for handling paths. +pub fn path( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let cmd = args[0]; + let argc = args.len(); + + if argc <= 1 { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd)); + builtin_print_error_trailer(parser, streams, cmd); + return STATUS_INVALID_ARGS; + } + + if args[1] == "-h" || args[1] == "--help" { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + let subcmd_name = args[1]; + + let subcmd: BuiltinCmd = match subcmd_name.to_string().as_str() { + "basename" => path_basename, + "change-extension" => path_change_extension, + "dirname" => path_dirname, + "extension" => path_extension, + "filter" => path_filter, + "is" => path_is, + "mtime" => path_mtime, + "normalize" => path_normalize, + "resolve" => path_resolve, + "sort" => path_sort, + _ => { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name)); + builtin_print_error_trailer(parser, streams, cmd); + return STATUS_INVALID_ARGS; + } + }; + + if argc >= 3 && (args[2] == "-h" || args[2] == "--help") { + // Unlike string, we don't have separate docs (yet) + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + let args = &mut args[1..]; + return subcmd(parser, streams, args); +} diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index 5fb7df123..3f4a7e50c 100644 --- a/fish-rust/src/builtins/shared.rs +++ b/fish-rust/src/builtins/shared.rs @@ -1,4 +1,5 @@ use crate::builtins::{printf, wait}; +use crate::common::str2wcstring; use crate::ffi::separation_type_t; use crate::ffi::{self, parser_t, wcstring_list_ffi_t, Repin, RustBuiltin}; use crate::wchar::{wstr, WString, L}; @@ -6,9 +7,14 @@ use crate::wchar_ffi::{c_str, empty_wstring, ToCppWString, WCharFromFFI}; use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; use cxx::{type_id, ExternType}; use libc::c_int; -use std::os::fd::RawFd; +use std::borrow::Cow; +use std::fs::File; +use std::io::{BufRead, BufReader, Read}; +use std::os::fd::{FromRawFd, RawFd}; use std::pin::Pin; +pub type BuiltinCmd = fn(&mut parser_t, &mut io_streams_t, &mut [&wstr]) -> Option; + #[cxx::bridge] mod builtins_ffi { extern "C++" { @@ -225,6 +231,7 @@ pub fn run_builtin( RustBuiltin::Emit => super::emit::emit(parser, streams, args), RustBuiltin::Exit => super::exit::exit(parser, streams, args), RustBuiltin::Math => super::math::math(parser, streams, args), + RustBuiltin::Path => super::path::path(parser, streams, args), RustBuiltin::Pwd => super::pwd::pwd(parser, streams, args), RustBuiltin::Random => super::random::random(parser, streams, args), RustBuiltin::Realpath => super::realpath::realpath(parser, streams, args), @@ -336,3 +343,143 @@ impl HelpOnlyCmdOpts { }) } } + +#[derive(PartialEq)] +pub enum SplitBehavior { + Newline, + /// The default behavior of the -z or --null-in switch, + /// Automatically start splitting on NULL if one appears in the first PATH_MAX bytes. + /// Otherwise on newline + InferNull, + Null, + Never, +} + +/// A helper type for extracting arguments from either argv or stdin. +pub struct Arguments<'args, 'iter> { + /// The list of arguments passed to the string builtin. + args: &'iter [&'args wstr], + /// If using argv, index of the next argument to return. + argidx: &'iter mut usize, + split_behavior: SplitBehavior, + /// Buffer to store what we read with the BufReader + /// Is only here to avoid allocating every time + buffer: Vec, + /// If not using argv, we read with a buffer + reader: Option>, +} + +impl Drop for Arguments<'_, '_> { + fn drop(&mut self) { + if let Some(r) = self.reader.take() { + // we should not close stdin + std::mem::forget(r.into_inner()); + } + } +} + +impl<'args, 'iter> Arguments<'args, 'iter> { + pub fn new( + args: &'iter [&'args wstr], + argidx: &'iter mut usize, + streams: &mut io_streams_t, + chunk_size: usize, + ) -> Self { + let reader = streams.stdin_is_directly_redirected().then(|| { + let stdin_fd = streams + .stdin_fd() + .filter(|&fd| fd >= 0) + .expect("should have a valid fd"); + // safety: this should be a valid fd, and already open + let fd = unsafe { File::from_raw_fd(stdin_fd) }; + BufReader::with_capacity(chunk_size, fd) + }); + + Arguments { + args, + argidx, + split_behavior: SplitBehavior::Newline, + buffer: Vec::new(), + reader, + } + } + + pub fn with_split_behavior(mut self, split_behavior: SplitBehavior) -> Self { + self.split_behavior = split_behavior; + self + } + + fn get_arg_stdin(&mut self) -> Option<(Cow<'args, wstr>, bool)> { + use SplitBehavior::*; + let reader = self.reader.as_mut().unwrap(); + + if self.split_behavior == InferNull { + // we must determine if the first `PATH_MAX` bytes contains a null. + // we intentionally do not consume the buffer here + // the contents will be returned again later + let b = reader.fill_buf().ok()?; + if b.contains(&b'\0') { + self.split_behavior = Null; + } else { + self.split_behavior = Newline; + } + } + + // NOTE: C++ wrongly commented that read_blocked retries for EAGAIN + let num_bytes: usize = match self.split_behavior { + Newline => reader.read_until(b'\n', &mut self.buffer), + Null => reader.read_until(b'\0', &mut self.buffer), + Never => reader.read_to_end(&mut self.buffer), + _ => unreachable!(), + } + .ok()?; + + // to match behaviour of earlier versions + if num_bytes == 0 { + return None; + } + + // assert!(num_bytes == self.buffer.len()); + let (end, want_newline) = match (&self.split_behavior, self.buffer.last().unwrap()) { + // remove the newline — consumers do not expect it + (Newline, b'\n') => (num_bytes - 1, true), + // we are missing a trailing newline! + (Newline, _) => (num_bytes, false), + // consumers do not expect to deal with the null + // "want_newline" is not currently relevant for Null + (Null, b'\0') => (num_bytes - 1, false), + // we are missing a null! + (Null, _) => (num_bytes, false), + (Never, _) => (num_bytes, false), + _ => unreachable!(), + }; + + let parsed = str2wcstring(&self.buffer[..end]); + + let retval = Some((Cow::Owned(parsed), want_newline)); + self.buffer.clear(); + retval + } +} + +impl<'args> Iterator for Arguments<'args, '_> { + // second is want_newline + // If not set, we have consumed all of stdin and its last line is missing a newline character. + // This is an edge case -- we expect text input, which is conventionally terminated by a + // newline character. But if it isn't, we use this to avoid creating one out of thin air, + // to not corrupt input data. + type Item = (Cow<'args, wstr>, bool); + + fn next(&mut self) -> Option { + if self.reader.is_some() { + return self.get_arg_stdin(); + } + + if *self.argidx >= self.args.len() { + return None; + } + let retval = (Cow::Borrowed(self.args[*self.argidx]), true); + *self.argidx += 1; + return Some(retval); + } +} diff --git a/fish-rust/src/builtins/string.rs b/fish-rust/src/builtins/string.rs index 3d90298c8..72f5fb768 100644 --- a/fish-rust/src/builtins/string.rs +++ b/fish-rust/src/builtins/string.rs @@ -1,18 +1,12 @@ -use std::borrow::Cow; -use std::fs::File; -use std::io::{BufRead, BufReader, Read}; -use std::os::fd::FromRawFd; - -use crate::common::str2wcstring; use crate::wcstringutil::fish_wcwidth_visible; // Forward some imports to make subcmd implementations easier use crate::{ builtins::shared::{ builtin_missing_argument, builtin_print_error_trailer, builtin_print_help, io_streams_t, - BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_ARG_COUNT1, BUILTIN_ERR_COMBO2, - BUILTIN_ERR_INVALID_SUBCMD, BUILTIN_ERR_MISSING_SUBCMD, BUILTIN_ERR_NOT_NUMBER, - BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN, STATUS_CMD_ERROR, STATUS_CMD_OK, - STATUS_INVALID_ARGS, + Arguments, SplitBehavior, BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_ARG_COUNT1, + BUILTIN_ERR_COMBO2, BUILTIN_ERR_INVALID_SUBCMD, BUILTIN_ERR_MISSING_SUBCMD, + BUILTIN_ERR_NOT_NUMBER, BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN, + STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS, }, ffi::{parser_t, separation_type_t}, wchar::{wstr, WString, L}, @@ -302,126 +296,16 @@ fn escape_code_length(code: &wstr) -> Option { } } -/// A helper type for extracting arguments from either argv or stdin. -struct Arguments<'args, 'iter> { - /// The list of arguments passed to the string builtin. +/// Empirically determined. +/// This is probably down to some pipe buffer or some such, +/// but too small means we need to call `read(2)` and str2wcstring a lot. +const STRING_CHUNK_SIZE: usize = 1024; +fn arguments<'iter, 'args>( args: &'iter [&'args wstr], - /// If using argv, index of the next argument to return. argidx: &'iter mut usize, - /// If set, when reading from a stream, split on newlines. - split_on_newline: bool, - /// Buffer to store what we read with the BufReader - /// Is only here to avoid allocating every time - buffer: Vec, - /// If not using argv, we read with a buffer - reader: Option>, -} - -impl Drop for Arguments<'_, '_> { - fn drop(&mut self) { - if let Some(r) = self.reader.take() { - // we should not close stdin - std::mem::forget(r.into_inner()); - } - } -} - -impl<'args, 'iter> Arguments<'args, 'iter> { - /// Empirically determined. - /// This is probably down to some pipe buffer or some such, - /// but too small means we need to call `read(2)` and str2wcstring a lot. - const STRING_CHUNK_SIZE: usize = 1024; - - fn new( - args: &'iter [&'args wstr], - argidx: &'iter mut usize, - streams: &mut io_streams_t, - ) -> Self { - let reader = streams.stdin_is_directly_redirected().then(|| { - let stdin_fd = streams - .stdin_fd() - .filter(|&fd| fd >= 0) - .expect("should have a valid fd"); - // safety: this should be a valid fd, and already open - let fd = unsafe { File::from_raw_fd(stdin_fd) }; - BufReader::with_capacity(Self::STRING_CHUNK_SIZE, fd) - }); - - Arguments { - args, - argidx, - split_on_newline: true, - buffer: Vec::new(), - reader, - } - } - - fn without_splitting_on_newline( - args: &'iter [&'args wstr], - argidx: &'iter mut usize, - streams: &mut io_streams_t, - ) -> Self { - let mut args = Self::new(args, argidx, streams); - args.split_on_newline = false; - args - } - - fn get_arg_stdin(&mut self) -> Option<(Cow<'args, wstr>, bool)> { - let reader = self.reader.as_mut().unwrap(); - - // NOTE: C++ wrongly commented that read_blocked retries for EAGAIN - let num_bytes = match self.split_on_newline { - true => reader.read_until(b'\n', &mut self.buffer), - false => reader.read_to_end(&mut self.buffer), - } - .ok()?; - - // to match behaviour of earlier versions - if num_bytes == 0 { - return None; - } - - let mut parsed = str2wcstring(&self.buffer); - - // If not set, we have consumed all of stdin and its last line is missing a newline character. - // This is an edge case -- we expect text input, which is conventionally terminated by a - // newline character. But if it isn't, we use this to avoid creating one out of thin air, - // to not corrupt input data. - let want_newline; - if self.split_on_newline { - if parsed.char_at(parsed.len() - 1) == '\n' { - // consumers do not expect to deal with the newline - parsed.pop(); - want_newline = true; - } else { - // we are missing a trailing newline - want_newline = false; - } - } else { - want_newline = false; - } - - let retval = Some((Cow::Owned(parsed), want_newline)); - self.buffer.clear(); - retval - } -} - -impl<'args> Iterator for Arguments<'args, '_> { - // second is want_newline - type Item = (Cow<'args, wstr>, bool); - - fn next(&mut self) -> Option { - if self.reader.is_some() { - return self.get_arg_stdin(); - } - - if *self.argidx >= self.args.len() { - return None; - } - *self.argidx += 1; - return Some((Cow::Borrowed(self.args[*self.argidx - 1]), true)); - } + streams: &mut io_streams_t, +) -> Arguments<'args, 'iter> { + Arguments::new(args, argidx, streams, STRING_CHUNK_SIZE) } /// The string builtin, for manipulating strings. diff --git a/fish-rust/src/builtins/string/collect.rs b/fish-rust/src/builtins/string/collect.rs index be4206299..9ddcecb94 100644 --- a/fish-rust/src/builtins/string/collect.rs +++ b/fish-rust/src/builtins/string/collect.rs @@ -31,7 +31,9 @@ impl StringSubCommand<'_> for Collect { ) -> Option { let mut appended = 0usize; - for (arg, want_newline) in Arguments::without_splitting_on_newline(args, optind, streams) { + for (arg, want_newline) in + arguments(args, optind, streams).with_split_behavior(SplitBehavior::Never) + { let arg = if !self.no_trim_newlines { let trim_len = arg.len() - arg.chars().rev().take_while(|&c| c == '\n').count(); &arg[..trim_len] diff --git a/fish-rust/src/builtins/string/escape.rs b/fish-rust/src/builtins/string/escape.rs index 405bfcfce..008e87236 100644 --- a/fish-rust/src/builtins/string/escape.rs +++ b/fish-rust/src/builtins/string/escape.rs @@ -45,7 +45,7 @@ impl StringSubCommand<'_> for Escape { }; let mut escaped_any = false; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { let mut escaped = escape_string(&arg, style); if want_newline { diff --git a/fish-rust/src/builtins/string/join.rs b/fish-rust/src/builtins/string/join.rs index 4d3b5d435..1bbff0131 100644 --- a/fish-rust/src/builtins/string/join.rs +++ b/fish-rust/src/builtins/string/join.rs @@ -45,9 +45,9 @@ impl<'args> StringSubCommand<'args> for Join<'args> { } let Some(arg) = args.get(*optind).copied() else { - string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]); - return STATUS_INVALID_ARGS; - }; + string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]); + return STATUS_INVALID_ARGS; + }; *optind += 1; self.sep = arg; @@ -64,7 +64,7 @@ impl<'args> StringSubCommand<'args> for Join<'args> { let sep = &self.sep; let mut nargs = 0usize; let mut print_trailing_newline = true; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { if !self.quiet { if self.no_empty && arg.is_empty() { continue; diff --git a/fish-rust/src/builtins/string/length.rs b/fish-rust/src/builtins/string/length.rs index 6c53a2e0d..b84fc30a7 100644 --- a/fish-rust/src/builtins/string/length.rs +++ b/fish-rust/src/builtins/string/length.rs @@ -33,7 +33,7 @@ impl StringSubCommand<'_> for Length { ) -> Option { let mut nnonempty = 0usize; - for (arg, _) in Arguments::new(args, optind, streams) { + for (arg, _) in arguments(args, optind, streams) { if self.visible { // Visible length only makes sense line-wise. for line in split_string(&arg, '\n') { diff --git a/fish-rust/src/builtins/string/match.rs b/fish-rust/src/builtins/string/match.rs index 2873beaa7..74936daf9 100644 --- a/fish-rust/src/builtins/string/match.rs +++ b/fish-rust/src/builtins/string/match.rs @@ -110,7 +110,7 @@ impl<'args> StringSubCommand<'args> for Match<'args> { } }; - for (arg, _) in Arguments::new(args, optind, streams) { + for (arg, _) in arguments(args, optind, streams) { if let Err(e) = matcher.report_matches(arg.as_ref(), streams) { FLOG!(error, "pcre2_match unexpected error:", e.error_message()) } diff --git a/fish-rust/src/builtins/string/pad.rs b/fish-rust/src/builtins/string/pad.rs index 996ab67cd..86c151b29 100644 --- a/fish-rust/src/builtins/string/pad.rs +++ b/fish-rust/src/builtins/string/pad.rs @@ -74,7 +74,7 @@ impl StringSubCommand<'_> for Pad { let mut inputs: Vec<(Cow<'args, wstr>, usize)> = Vec::new(); let mut print_trailing_newline = true; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { let width = width_without_escapes(&arg, 0); max_width = max_width.max(width); inputs.push((arg, width)); diff --git a/fish-rust/src/builtins/string/repeat.rs b/fish-rust/src/builtins/string/repeat.rs index 7859f6857..acc7f3023 100644 --- a/fish-rust/src/builtins/string/repeat.rs +++ b/fish-rust/src/builtins/string/repeat.rs @@ -55,7 +55,7 @@ impl StringSubCommand<'_> for Repeat { let mut first = true; let mut print_trailing_newline = true; - for (w, want_newline) in Arguments::new(args, optind, streams) { + for (w, want_newline) in arguments(args, optind, streams) { print_trailing_newline = want_newline; if w.is_empty() { continue; diff --git a/fish-rust/src/builtins/string/replace.rs b/fish-rust/src/builtins/string/replace.rs index dc936aaf0..a76f2b940 100644 --- a/fish-rust/src/builtins/string/replace.rs +++ b/fish-rust/src/builtins/string/replace.rs @@ -79,7 +79,7 @@ impl<'args> StringSubCommand<'args> for Replace<'args> { let mut replace_count = 0; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { let (replaced, result) = match replacer.replace(arg) { Ok(x) => x, Err(e) => { diff --git a/fish-rust/src/builtins/string/shorten.rs b/fish-rust/src/builtins/string/shorten.rs index 163b7383b..7e9f4c424 100644 --- a/fish-rust/src/builtins/string/shorten.rs +++ b/fish-rust/src/builtins/string/shorten.rs @@ -72,7 +72,7 @@ impl<'args> StringSubCommand<'args> for Shorten<'args> { let mut min_width = usize::MAX; let mut inputs = Vec::new(); - let iter = Arguments::new(args, optind, streams); + let iter = arguments(args, optind, streams); if self.max == Some(0) { // Special case: Max of 0 means no shortening. diff --git a/fish-rust/src/builtins/string/split.rs b/fish-rust/src/builtins/string/split.rs index 0dbb887f9..5438d0527 100644 --- a/fish-rust/src/builtins/string/split.rs +++ b/fish-rust/src/builtins/string/split.rs @@ -181,10 +181,10 @@ impl<'args> StringSubCommand<'args> for Split<'args> { let mut split_count = 0usize; let mut arg_count = 0usize; - let argiter = match self.is_split0 { - false => Arguments::new(args, optind, streams), - true => Arguments::without_splitting_on_newline(args, optind, streams), - }; + let argiter = arguments(args, optind, streams).with_split_behavior(match self.is_split0 { + false => SplitBehavior::Newline, + true => SplitBehavior::Never, + }); for (arg, _) in argiter { let splits: Vec> = match (self.split_from, arg) { (Direction::Right, arg) => { diff --git a/fish-rust/src/builtins/string/sub.rs b/fish-rust/src/builtins/string/sub.rs index bb9d92290..9ca20fd75 100644 --- a/fish-rust/src/builtins/string/sub.rs +++ b/fish-rust/src/builtins/string/sub.rs @@ -65,7 +65,7 @@ impl StringSubCommand<'_> for Sub { } let mut nsub = 0; - for (s, want_newline) in Arguments::new(args, optind, streams) { + for (s, want_newline) in arguments(args, optind, streams) { let start: usize = match self.start.map(i64::from).unwrap_or_default() { n @ 1.. => n as usize - 1, 0 => 0, diff --git a/fish-rust/src/builtins/string/transform.rs b/fish-rust/src/builtins/string/transform.rs index eee7576eb..0ed7e3e75 100644 --- a/fish-rust/src/builtins/string/transform.rs +++ b/fish-rust/src/builtins/string/transform.rs @@ -25,7 +25,7 @@ impl StringSubCommand<'_> for Transform { ) -> Option { let mut n_transformed = 0usize; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { let transformed = (self.func)(&arg); if transformed != arg { n_transformed += 1; diff --git a/fish-rust/src/builtins/string/trim.rs b/fish-rust/src/builtins/string/trim.rs index 2d05cbd06..f11a50a8f 100644 --- a/fish-rust/src/builtins/string/trim.rs +++ b/fish-rust/src/builtins/string/trim.rs @@ -72,7 +72,7 @@ impl<'args> StringSubCommand<'args> for Trim<'args> { .count() }; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { let trim_start = self.left.then(|| to_trim_start(&arg)).unwrap_or(0); // collision is only an issue if the whole string is getting trimmed let trim_end = (self.right && trim_start != arg.len()) diff --git a/fish-rust/src/builtins/string/unescape.rs b/fish-rust/src/builtins/string/unescape.rs index fb441a4c6..0f311ee54 100644 --- a/fish-rust/src/builtins/string/unescape.rs +++ b/fish-rust/src/builtins/string/unescape.rs @@ -38,7 +38,7 @@ impl StringSubCommand<'_> for Unescape { args: &[&wstr], ) -> Option { let mut nesc = 0; - for (arg, want_newline) in Arguments::new(args, optind, streams) { + for (arg, want_newline) in arguments(args, optind, streams) { if let Some(res) = unescape_string(&arg, self.style) { streams.out.append(res); if want_newline { diff --git a/fish-rust/src/wutil/mod.rs b/fish-rust/src/wutil/mod.rs index 9f118cdb6..6f07b9a5f 100644 --- a/fish-rust/src/wutil/mod.rs +++ b/fish-rust/src/wutil/mod.rs @@ -511,13 +511,13 @@ pub fn fish_wcswidth(s: &wstr) -> libc::c_int { /// problem). Therefore we include richer information. #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct FileId { - device: libc::dev_t, - inode: libc::ino_t, - size: u64, - change_seconds: libc::time_t, - change_nanoseconds: i64, - mod_seconds: libc::time_t, - mod_nanoseconds: i64, + pub device: libc::dev_t, + pub inode: libc::ino_t, + pub size: u64, + pub change_seconds: libc::time_t, + pub change_nanoseconds: i64, + pub mod_seconds: libc::time_t, + pub mod_nanoseconds: i64, } impl FileId { diff --git a/src/builtin.cpp b/src/builtin.cpp index 6c3a4fdcd..f07f41e95 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -38,7 +38,6 @@ #include "builtins/functions.h" #include "builtins/history.h" #include "builtins/jobs.h" -#include "builtins/path.h" #include "builtins/read.h" #include "builtins/set.h" #include "builtins/shared.rs.h" @@ -381,7 +380,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L"math", &implemented_in_rust, N_(L"Evaluate math expressions")}, {L"not", &builtin_generic, N_(L"Negate exit status of job")}, {L"or", &builtin_generic, N_(L"Execute command if previous command failed")}, - {L"path", &builtin_path, N_(L"Handle paths")}, + {L"path", &implemented_in_rust, N_(L"Handle paths")}, {L"printf", &implemented_in_rust, N_(L"Prints formatted text")}, {L"pwd", &implemented_in_rust, N_(L"Print the working directory")}, {L"random", &implemented_in_rust, N_(L"Generate random number")}, @@ -580,6 +579,9 @@ static maybe_t try_get_rust_builtin(const wcstring &cmd) { if (cmd == L"wait") { return RustBuiltin::Wait; } + if (cmd == L"path") { + return RustBuiltin::Path; + } if (cmd == L"printf") { return RustBuiltin::Printf; } diff --git a/src/builtin.h b/src/builtin.h index 22a8bba4f..7a0cc3208 100644 --- a/src/builtin.h +++ b/src/builtin.h @@ -124,6 +124,7 @@ enum class RustBuiltin : int32_t { Emit, Exit, Math, + Path, Printf, Pwd, Random, diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp deleted file mode 100644 index 4a59e4a58..000000000 --- a/src/builtins/path.cpp +++ /dev/null @@ -1,952 +0,0 @@ -// Implementation of the path builtin. -#include "config.h" // IWYU pragma: keep - -#include "path.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../env.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../path.h" -#include "../util.h" -#include "../wcstringutil.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep - -// How many bytes we read() at once. -// We use PATH_MAX here so we always get at least one path, -// and so we can automatically detect NULL-separated input. -#define PATH_CHUNK_SIZE PATH_MAX - -static void path_error(io_streams_t &streams, const wchar_t *fmt, ...) { - streams.err.append(L"path "); - std::va_list va; - va_start(va, fmt); - streams.err.append_formatv(fmt, va); - va_end(va); -} - -static void path_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *subcmd, - const wchar_t *opt) { - path_error(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt); - builtin_print_error_trailer(parser, streams.err, L"path"); -} - -// We read from stdin if we are the second or later process in a pipeline. -static bool path_args_from_stdin(const io_streams_t &streams) { - return streams.stdin_is_directly_redirected; -} - -static const wchar_t *path_get_arg_argv(int *argidx, const wchar_t *const *argv) { - return argv && argv[*argidx] ? argv[(*argidx)++] : nullptr; -} - -// A helper type for extracting arguments from either argv or stdin. -namespace { -class arg_iterator_t { - // The list of arguments passed to this builtin. - const wchar_t *const *argv_; - // If using argv, index of the next argument to return. - int argidx_; - // If not using argv, a string to store bytes that have been read but not yet returned. - std::string buffer_; - // Whether we have found a char to split on yet, when reading from stdin. - // If explicitly passed, we will always split on NULL, - // if not we will split on NULL if the first PATH_MAX chunk includes one, - // or '\n' otherwise. - bool have_split_; - // The char we have decided to split on when reading from stdin. - char split_{'\0'}; - // Backing storage for the next() string. - wcstring storage_; - const io_streams_t &streams_; - - /// Reads the next argument from stdin, returning true if an argument was produced and false if - /// not. On true, the string is stored in storage_. - bool get_arg_stdin() { - assert(path_args_from_stdin(streams_) && "should not be reading from stdin"); - assert(streams_.stdin_fd >= 0 && "should have a valid fd"); - // Read in chunks from fd until buffer has a line (or the end if split_ is unset). - size_t pos; - while (!have_split_ || (pos = buffer_.find(split_)) == std::string::npos) { - char buf[PATH_CHUNK_SIZE]; - long n = read_blocked(streams_.stdin_fd, buf, PATH_CHUNK_SIZE); - if (n == 0) { - // If we still have buffer contents, flush them, - // in case there was no trailing sep. - if (buffer_.empty()) return false; - storage_ = str2wcstring(buffer_); - buffer_.clear(); - return true; - } - if (n == -1) { - // Some error happened. We can't do anything about it, - // so ignore it. - // (read_blocked already retries for EAGAIN and EINTR) - storage_ = str2wcstring(buffer_); - buffer_.clear(); - return false; - } - buffer_.append(buf, n); - if (!have_split_) { - if (buffer_.find('\0') != std::string::npos) { - split_ = '\0'; - } else { - split_ = '\n'; - } - have_split_ = true; - } - } - - // Split the buffer on the sep and return the first part. - storage_ = str2wcstring(buffer_, pos); - buffer_.erase(0, pos + 1); - return true; - } - - public: - arg_iterator_t(const wchar_t *const *argv, int argidx, const io_streams_t &streams, - bool split_null) - : argv_(argv), argidx_(argidx), have_split_(split_null), streams_(streams) {} - - const wcstring *nextstr() { - if (path_args_from_stdin(streams_)) { - return get_arg_stdin() ? &storage_ : nullptr; - } - if (auto arg = path_get_arg_argv(&argidx_, argv_)) { - storage_ = arg; - return &storage_; - } else { - return nullptr; - } - } -}; -} // namespace - -enum { - TYPE_BLOCK = 1 << 0, /// A block device - TYPE_DIR = 1 << 1, /// A directory - TYPE_FILE = 1 << 2, /// A regular file - TYPE_LINK = 1 << 3, /// A link - TYPE_CHAR = 1 << 4, /// A character device - TYPE_FIFO = 1 << 5, /// A fifo - TYPE_SOCK = 1 << 6, /// A socket -}; -typedef uint32_t path_type_flags_t; - -enum { - PERM_READ = 1 << 0, - PERM_WRITE = 1 << 1, - PERM_EXEC = 1 << 2, - PERM_SUID = 1 << 3, - PERM_SGID = 1 << 4, - PERM_USER = 1 << 5, - PERM_GROUP = 1 << 6, -}; -typedef uint32_t path_perm_flags_t; - -// This is used by the subcommands to communicate with the option parser which flags are -// valid and get the result of parsing the command for flags. -struct options_t { //!OCLINT(too many fields) - bool perm_valid = false; - bool type_valid = false; - bool invert_valid = false; - bool relative_valid = false; - bool reverse_valid = false; - bool key_valid = false; - bool unique_valid = false; - bool unique = false; - bool have_key = false; - const wchar_t *key = nullptr; - - bool null_in = false; - bool null_out = false; - bool quiet = false; - - bool have_type = false; - path_type_flags_t type = 0; - - bool have_perm = false; - // Whether we need to check a special permission like suid. - bool have_special_perm = false; - path_perm_flags_t perm = 0; - - bool invert = false; - bool relative = false; - bool reverse = false; - - const wchar_t *arg1 = nullptr; -}; - -static void path_out(io_streams_t &streams, const options_t &opts, const wcstring &str) { - if (!opts.quiet) { - if (!opts.null_out) { - streams.out.append_with_separation(str, separation_type_t::explicitly); - } else { - // Note the char - if this was a string instead we'd add - // a string of length 0, i.e. nothing - streams.out.append(str + L'\0'); - } - } -} - -static int handle_flag_q(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - UNUSED(argv); - UNUSED(parser); - UNUSED(streams); - UNUSED(w); - opts->quiet = true; - return STATUS_CMD_OK; -} - -static int handle_flag_z(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - UNUSED(argv); - UNUSED(parser); - UNUSED(streams); - UNUSED(w); - opts->null_in = true; - return STATUS_CMD_OK; -} - -static int handle_flag_Z(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - UNUSED(argv); - UNUSED(parser); - UNUSED(streams); - UNUSED(w); - opts->null_out = true; - return STATUS_CMD_OK; -} - -static int handle_flag_t(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - if (opts->type_valid) { - if (!opts->have_type) opts->type = 0; - opts->have_type = true; - std::vector types = split_string_tok(w.woptarg, L","); - for (const auto &t : types) { - if (t == L"file") { - opts->type |= TYPE_FILE; - } else if (t == L"dir") { - opts->type |= TYPE_DIR; - } else if (t == L"block") { - opts->type |= TYPE_BLOCK; - } else if (t == L"char") { - opts->type |= TYPE_CHAR; - } else if (t == L"fifo") { - opts->type |= TYPE_FIFO; - } else if (t == L"socket") { - opts->type |= TYPE_SOCK; - } else if (t == L"link") { - opts->type |= TYPE_LINK; - } else { - path_error(streams, _(L"%ls: Invalid type '%ls'\n"), L"path", t.c_str()); - return STATUS_INVALID_ARGS; - } - } - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_p(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - if (opts->perm_valid) { - if (!opts->have_perm) opts->perm = 0; - opts->have_perm = true; - std::vector perms = split_string_tok(w.woptarg, L","); - for (const auto &p : perms) { - if (p == L"read") { - opts->perm |= PERM_READ; - } else if (p == L"write") { - opts->perm |= PERM_WRITE; - } else if (p == L"exec") { - opts->perm |= PERM_EXEC; - } else if (p == L"suid") { - opts->perm |= PERM_SUID; - opts->have_special_perm = true; - } else if (p == L"sgid") { - opts->perm |= PERM_SGID; - opts->have_special_perm = true; - } else if (p == L"user") { - opts->perm |= PERM_USER; - opts->have_special_perm = true; - } else if (p == L"group") { - opts->perm |= PERM_GROUP; - opts->have_special_perm = true; - } else { - path_error(streams, _(L"%ls: Invalid permission '%ls'\n"), L"path", p.c_str()); - return STATUS_INVALID_ARGS; - } - } - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_perms(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts, path_perm_flags_t perm) { - if (opts->perm_valid) { - if (!opts->have_perm) opts->perm = 0; - opts->have_perm = true; - opts->perm |= perm; - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_R(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - if (opts->relative_valid) { - opts->relative = true; - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_r(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - if (opts->reverse_valid) { - opts->reverse = true; - return STATUS_CMD_OK; - } else if (opts->perm_valid) { - return handle_flag_perms(argv, parser, streams, w, opts, PERM_READ); - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_w(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - return handle_flag_perms(argv, parser, streams, w, opts, PERM_WRITE); -} -static int handle_flag_x(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - return handle_flag_perms(argv, parser, streams, w, opts, PERM_EXEC); -} - -static int handle_flag_types(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts, path_type_flags_t type) { - if (opts->type_valid) { - if (!opts->have_type) opts->type = 0; - opts->have_type = true; - opts->type |= type; - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_f(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - return handle_flag_types(argv, parser, streams, w, opts, TYPE_FILE); -} -static int handle_flag_l(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - return handle_flag_types(argv, parser, streams, w, opts, TYPE_LINK); -} -static int handle_flag_d(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - return handle_flag_types(argv, parser, streams, w, opts, TYPE_DIR); -} - -static int handle_flag_v(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - if (opts->invert_valid) { - opts->invert = true; - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_u(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - if (opts->unique_valid) { - opts->unique = true; - return STATUS_CMD_OK; - } - path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; -} - -static int handle_flag_key(const wchar_t **argv, parser_t &parser, io_streams_t &streams, - const wgetopter_t &w, options_t *opts) { - UNUSED(argv); - UNUSED(parser); - UNUSED(streams); - opts->have_key = true; - opts->key = w.woptarg; - return STATUS_CMD_OK; -} - -/// This constructs the wgetopt() short options string based on which arguments are valid for the -/// subcommand. We have to do this because many short flags have multiple meanings and may or may -/// not require an argument depending on the meaning. -static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath complexity) - // All commands accept -z, -Z and -q - wcstring short_opts(L":zZq"); - if (opts->perm_valid) { - short_opts.append(L"p:"); - short_opts.append(L"rwx"); - } - if (opts->type_valid) { - short_opts.append(L"t:"); - short_opts.append(L"fld"); - } - if (opts->invert_valid) short_opts.append(L"v"); - if (opts->relative_valid) short_opts.append(L"R"); - if (opts->reverse_valid) short_opts.append(L"r"); - if (opts->unique_valid) short_opts.append(L"u"); - return short_opts; -} - -// Note that several long flags share the same short flag. That is okay. The caller is expected -// to indicate that a max of one of the long flags sharing a short flag is valid. -// Remember: adjust the completions in share/completions/ when options change -static const struct woption long_options[] = {{L"quiet", no_argument, 'q'}, - {L"null-in", no_argument, 'z'}, - {L"null-out", no_argument, 'Z'}, - {L"perm", required_argument, 'p'}, - {L"type", required_argument, 't'}, - {L"invert", no_argument, 'v'}, - {L"relative", no_argument, 'R'}, - {L"reverse", no_argument, 'r'}, - {L"unique", no_argument, 'u'}, - {L"key", required_argument, 1}, - {}}; - -static const std::unordered_map flag_to_function = { - {'q', handle_flag_q}, {'v', handle_flag_v}, {'z', handle_flag_z}, {'Z', handle_flag_Z}, - {'t', handle_flag_t}, {'p', handle_flag_p}, {'r', handle_flag_r}, {'w', handle_flag_w}, - {'x', handle_flag_x}, {'f', handle_flag_f}, {'l', handle_flag_l}, {'d', handle_flag_d}, - {'l', handle_flag_l}, {'d', handle_flag_d}, {'u', handle_flag_u}, {1, handle_flag_key}, - {'R', handle_flag_R}, -}; - -/// Parse the arguments for flags recognized by a specific string subcommand. -static int parse_opts(options_t *opts, int *optind, int n_req_args, int argc, const wchar_t **argv, - parser_t &parser, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - wcstring short_opts = construct_short_opts(opts); - const wchar_t *short_options = short_opts.c_str(); - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - auto fn = flag_to_function.find(opt); - if (fn != flag_to_function.end()) { - int retval = fn->second(argv, parser, streams, w, opts); - if (retval != STATUS_CMD_OK) return retval; - } else if (opt == ':') { - streams.err.append(L"path "); - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], - false /* print_hints */); - return STATUS_INVALID_ARGS; - } else if (opt == '?') { - path_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } else { - DIE("unexpected retval from wgetopt_long"); - } - } - - *optind = w.woptind; - - if (n_req_args) { - assert(n_req_args == 1); - opts->arg1 = path_get_arg_argv(optind, argv); - if (!opts->arg1 && n_req_args == 1) { - path_error(streams, BUILTIN_ERR_ARG_COUNT0, cmd); - return STATUS_INVALID_ARGS; - } - } - - // At this point we should not have optional args and be reading args from stdin. - if (path_args_from_stdin(streams) && argc > *optind) { - path_error(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); - return STATUS_INVALID_ARGS; - } - - return STATUS_CMD_OK; -} - -static int path_transform(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv, - wcstring (*func)(wcstring)) { - options_t opts; - int optind; - int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - // Empty paths make no sense, but e.g. wbasename returns true for them. - if (arg->empty()) continue; - wcstring transformed = func(*arg); - if (transformed != *arg) { - n_transformed++; - // Return okay if path wasn't already in this form - // TODO: Is that correct? - if (opts.quiet) return STATUS_CMD_OK; - } - path_out(streams, opts, transformed); - } - - return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} - -static int path_basename(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - return path_transform(parser, streams, argc, argv, wbasename); -} - -static int path_dirname(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - return path_transform(parser, streams, argc, argv, wdirname); -} - -// Not a constref because this must have the same type as wdirname. -// cppcheck-suppress passedByValue -static wcstring normalize_helper(wcstring path) { - wcstring np = normalize_path(path, false); - if (!np.empty() && np[0] == L'-') { - np = L"./" + np; - } - return np; -} - -static bool filter_path(options_t opts, const wcstring &path) { - // TODO: Add moar stuff: - // fifos, sockets, size greater than zero, setuid, ... - // Nothing to check, file existence is checked elsewhere. - if (!opts.have_type && !opts.have_perm) return true; - - if (opts.have_type) { - bool type_ok = false; - struct stat buf; - if (opts.type & TYPE_LINK) { - type_ok = !lwstat(path, &buf) && S_ISLNK(buf.st_mode); - } - - auto ret = !wstat(path, &buf); - if (!ret) { - // Does not exist - return false; - } - if (!type_ok && opts.type & TYPE_FILE && S_ISREG(buf.st_mode)) { - type_ok = true; - } - if (!type_ok && opts.type & TYPE_DIR && S_ISDIR(buf.st_mode)) { - type_ok = true; - } - if (!type_ok && opts.type & TYPE_BLOCK && S_ISBLK(buf.st_mode)) { - type_ok = true; - } - if (!type_ok && opts.type & TYPE_CHAR && S_ISCHR(buf.st_mode)) { - type_ok = true; - } - if (!type_ok && opts.type & TYPE_FIFO && S_ISFIFO(buf.st_mode)) { - type_ok = true; - } - if (!type_ok && opts.type & TYPE_SOCK && S_ISSOCK(buf.st_mode)) { - type_ok = true; - } - if (!type_ok) return false; - } - if (opts.have_perm) { - int amode = 0; - if (opts.perm & PERM_READ) amode |= R_OK; - if (opts.perm & PERM_WRITE) amode |= W_OK; - if (opts.perm & PERM_EXEC) amode |= X_OK; - // access returns 0 on success, - // -1 on failure. Yes, C can't even keep its bools straight. - if (waccess(path, amode)) return false; - - // Permissions that require special handling - if (opts.have_special_perm) { - struct stat buf; - auto ret = !wstat(path, &buf); - if (!ret) { - // Does not exist, WTF? - return false; - } - - if (opts.perm & PERM_SUID && !(S_ISUID & buf.st_mode)) return false; - if (opts.perm & PERM_SGID && !(S_ISGID & buf.st_mode)) return false; - if (opts.perm & PERM_USER && !(geteuid() == buf.st_uid)) return false; - if (opts.perm & PERM_GROUP && !(getegid() == buf.st_gid)) return false; - } - } - - // No filters failed. - return true; -} - -static int path_mtime(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - options_t opts; - opts.relative_valid = true; - int optind; - int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - int n_transformed = 0; - - time_t t = std::time(nullptr); - - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - auto ret = file_id_for_path(*arg); - - if (ret != kInvalidFileID) { - if (opts.quiet) return STATUS_CMD_OK; - n_transformed++; - - if (!opts.relative) { - path_out(streams, opts, to_string(ret.mod_seconds)); - } else { - path_out(streams, opts, to_string(t - ret.mod_seconds)); - } - } - } - - return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} - -static int path_normalize(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - return path_transform(parser, streams, argc, argv, normalize_helper); -} - -static maybe_t find_extension(const wcstring &path) { - // The extension belongs to the basename, - // if there is a "." before the last component it doesn't matter. - // e.g. ~/.config/fish/conf.d/foo - // does not have an extension! The ".d" here is not a file extension for "foo". - // And "~/.config" doesn't have an extension either - the ".config" is the filename. - wcstring filename = wbasename(path); - - // "." and ".." aren't really *files* and therefore don't have an extension. - if (filename == L"." || filename == L"..") return none(); - - // If we don't have a "." or the "." is the first in the filename, - // we do not have an extension - size_t pos = filename.find_last_of(L'.'); - if (pos == wcstring::npos || pos == 0) { - return none(); - } - - // Convert pos back to what it would be in the original path. - return pos + path.size() - filename.size(); -} - -static int path_extension(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - options_t opts; - int optind; - int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - auto pos = find_extension(*arg); - - if (!pos.has_value()) { - // If there is no extension the extension is empty. - // This is unambiguous because we include the ".". - path_out(streams, opts, L""); - continue; - } - - wcstring ext = arg->substr(*pos); - if (opts.quiet && !ext.empty()) { - return STATUS_CMD_OK; - } - path_out(streams, opts, ext); - n_transformed++; - } - - return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} - -static int path_change_extension(parser_t &parser, io_streams_t &streams, int argc, - const wchar_t **argv) { - options_t opts; - int optind; - int retval = parse_opts(&opts, &optind, 1, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - auto pos = find_extension(*arg); - - wcstring ext; - if (!pos.has_value()) { - ext = *arg; - } else { - ext = arg->substr(0, *pos); - } - - // Only add on the extension "." if we have something. - // That way specifying an empty extension strips it. - if (*opts.arg1) { - if (opts.arg1[0] != L'.') { - ext.push_back(L'.'); - } - ext.append(opts.arg1); - } - path_out(streams, opts, ext); - n_transformed++; - } - - return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} - -static int path_resolve(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - options_t opts; - int optind; - int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - auto real = wrealpath(*arg); - - if (!real) { - // The path doesn't exist, isn't readable or a symlink loop. - // We go up until we find something that works. - wcstring next = *arg; - // First add $PWD if we're relative - if (!next.empty() && next[0] != L'/') { - // Note pwd can have symlinks, but we are about to resolve it anyway. - next = path_apply_working_directory(*arg, parser.vars().get_pwd_slash()); - } - auto rest = wbasename(next); - while (!next.empty() && next != L"/") { - next = wdirname(next); - real = wrealpath(next); - if (real) { - real->push_back(L'/'); - real->append(rest); - real = normalize_path(*real, false); - break; - } - rest = wbasename(next) + L'/' + rest; - } - if (!real) { - continue; - } - } - - // Normalize the path so "../" components are eliminated even after - // nonexistent or non-directory components. - // Otherwise `path resolve foo/../` will be `$PWD/foo/../` if foo is a file. - real = normalize_path(*real, false); - - // Return 0 if we found a realpath. - if (opts.quiet) { - return STATUS_CMD_OK; - } - path_out(streams, opts, *real); - n_transformed++; - } - - return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} - -static int path_sort(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - options_t opts; - opts.reverse_valid = true; - opts.key_valid = true; - opts.unique_valid = true; - int optind; - int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - auto keyfunc = +[](const wcstring &x) { return wbasename(x); }; - if (opts.have_key) { - if (std::wcscmp(opts.key, L"basename") == 0) { - // Do nothing, this is the default - } else if (std::wcscmp(opts.key, L"dirname") == 0) { - keyfunc = +[](const wcstring &x) { return wdirname(x); }; - } else if (std::wcscmp(opts.key, L"path") == 0) { - // Act as if --key hadn't been given. - opts.have_key = false; - } else { - path_error(streams, _(L"%ls: Invalid sort key '%ls'\n"), argv[0], opts.key); - return STATUS_INVALID_ARGS; - } - } - - std::vector list; - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - list.push_back(*arg); - } - - if (opts.have_key) { - // Keep a map to avoid repeated keyfunc calls and to keep things alive. - std::map key; - for (const auto &arg : list) { - key[arg] = keyfunc(arg); - } - - // We use a stable sort here, and also explicit < and >, - // to avoid changing the order so you can chain calls. - std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - if (!opts.reverse) - return (wcsfilecmp_glob(key[a].c_str(), key[b].c_str()) < 0); - else - return (wcsfilecmp_glob(key[a].c_str(), key[b].c_str()) > 0); - }); - if (opts.unique) { - list.erase( - std::unique(list.begin(), list.end(), - [&](const wcstring &a, const wcstring &b) { return key[a] == key[b]; }), - list.end()); - } - } else { - // Without --key, we just sort by the entire path, - // so we have no need to transform and such. - std::stable_sort(list.begin(), list.end(), [&](const wcstring &a, const wcstring &b) { - if (!opts.reverse) - return (wcsfilecmp_glob(a.c_str(), b.c_str()) < 0); - else - return (wcsfilecmp_glob(a.c_str(), b.c_str()) > 0); - }); - if (opts.unique) { - list.erase(std::unique(list.begin(), list.end()), list.end()); - } - } - - for (const auto &entry : list) { - path_out(streams, opts, entry); - } - - /* TODO: Return true only if already sorted? */ - return STATUS_CMD_OK; -} - -// All strings are taken to be filenames, and if they match the type/perms/etc (and exist!) -// they are passed along. -static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv, - bool is_is) { - options_t opts; - opts.type_valid = true; - opts.perm_valid = true; - opts.invert_valid = true; - int optind; - int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - // If we have been invoked as "path is", which is "path filter -q". - if (is_is) opts.quiet = true; - - int n_transformed = 0; - arg_iterator_t aiter(argv, optind, streams, opts.null_in); - while (const wcstring *arg = aiter.nextstr()) { - if ((!opts.have_perm && !opts.have_type) || (filter_path(opts, *arg) != opts.invert)) { - // If we don't have filters, check if it exists. - if (!opts.have_type && !opts.have_perm) { - bool ok = !waccess(*arg, F_OK); - if (ok == opts.invert) continue; - } - - // We *know* this is a filename, - // and so if it starts with a `-` we *know* it is relative - // to $PWD. So we can add `./`. - if (!arg->empty() && arg->front() == L'-') { - wcstring out = L"./" + *arg; - path_out(streams, opts, out); - } else { - path_out(streams, opts, *arg); - } - n_transformed++; - if (opts.quiet) return STATUS_CMD_OK; - } - } - - return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} - -static int path_filter(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - return path_filter(parser, streams, argc, argv, false /* is_is */); -} - -static int path_is(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) { - return path_filter(parser, streams, argc, argv, true /* is_is */); -} - -// Keep sorted alphabetically -static constexpr const struct path_subcommand { - const wchar_t *name; - int (*handler)(parser_t &, io_streams_t &, int argc, //!OCLINT(unused param) - const wchar_t **argv); //!OCLINT(unused param) -} path_subcommands[] = { - // TODO: Which operations do we want? - {L"basename", &path_basename}, // - {L"change-extension", &path_change_extension}, - {L"dirname", &path_dirname}, - {L"extension", &path_extension}, - {L"filter", &path_filter}, - {L"is", &path_is}, - {L"mtime", &path_mtime}, - {L"normalize", &path_normalize}, - {L"resolve", &path_resolve}, - {L"sort", &path_sort}, -}; -ASSERT_SORTED_BY_NAME(path_subcommands); - -/// The path builtin, for handling paths. -maybe_t builtin_path(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - if (argc <= 1) { - streams.err.append_format(BUILTIN_ERR_MISSING_SUBCMD, cmd); - builtin_print_error_trailer(parser, streams.err, L"path"); - return STATUS_INVALID_ARGS; - } - - if (std::wcscmp(argv[1], L"-h") == 0 || std::wcscmp(argv[1], L"--help") == 0) { - builtin_print_help(parser, streams, L"path"); - return STATUS_CMD_OK; - } - - const wchar_t *subcmd_name = argv[1]; - const auto *subcmd = get_by_sorted_name(subcmd_name, path_subcommands); - if (!subcmd) { - streams.err.append_format(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name); - builtin_print_error_trailer(parser, streams.err, L"path"); - return STATUS_INVALID_ARGS; - } - - if (argc >= 3 && (std::wcscmp(argv[2], L"-h") == 0 || std::wcscmp(argv[2], L"--help") == 0)) { - // Unlike string, we don't have separate docs (yet) - builtin_print_help(parser, streams, L"path"); - return STATUS_CMD_OK; - } - argc--; - argv++; - return subcmd->handler(parser, streams, argc, argv); -} diff --git a/src/builtins/path.h b/src/builtins/path.h deleted file mode 100644 index 885b93de2..000000000 --- a/src/builtins/path.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef FISH_BUILTIN_PATH_H -#define FISH_BUILTIN_PATH_H - -#include "../maybe.h" - -class parser_t; -struct io_streams_t; - -maybe_t builtin_path(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif