diff --git a/CMakeLists.txt b/CMakeLists.txt index c5504a2e2..6a776ebd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,8 +106,7 @@ set(FISH_BUILTIN_SRCS src/builtins/eval.cpp src/builtins/fg.cpp src/builtins/function.cpp src/builtins/functions.cpp src/builtins/history.cpp src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtins/path.cpp - src/builtins/read.cpp - src/builtins/realpath.cpp src/builtins/set.cpp + src/builtins/read.cpp src/builtins/set.cpp src/builtins/set_color.cpp src/builtins/source.cpp src/builtins/status.cpp src/builtins/string.cpp src/builtins/test.cpp src/builtins/type.cpp src/builtins/ulimit.cpp ) diff --git a/fish-rust/src/builtins/mod.rs b/fish-rust/src/builtins/mod.rs index fc889cfee..171493f1e 100644 --- a/fish-rust/src/builtins/mod.rs +++ b/fish-rust/src/builtins/mod.rs @@ -9,5 +9,6 @@ pub mod exit; pub mod pwd; pub mod random; +pub mod realpath; pub mod r#return; pub mod wait; diff --git a/fish-rust/src/builtins/realpath.rs b/fish-rust/src/builtins/realpath.rs new file mode 100644 index 000000000..401dd49fe --- /dev/null +++ b/fish-rust/src/builtins/realpath.rs @@ -0,0 +1,129 @@ +//! Implementation of the realpath builtin. + +use libc::c_int; + +use crate::{ + ffi::parser_t, + path::path_apply_working_directory, + wchar::{wstr, WExt, L}, + wchar_ffi::WCharFromFFI, + wgetopt::{wgetopter_t, wopt, woption, woption_argument_t::no_argument}, + wutil::{normalize_path, wgettext_fmt, wrealpath}, +}; + +use super::shared::{ + builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t, + BUILTIN_ERR_ARG_COUNT1, STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS, +}; + +#[derive(Default)] +struct Options { + print_help: bool, + no_symlinks: bool, +} + +const short_options: &wstr = L!("+:hs"); +const long_options: &[woption] = &[ + wopt(L!("no-symlinks"), no_argument, 's'), + wopt(L!("help"), no_argument, 'h'), +]; + +fn parse_options( + args: &mut [&wstr], + parser: &mut parser_t, + streams: &mut io_streams_t, +) -> Result<(Options, usize), Option> { + let cmd = args[0]; + + let mut opts = Options::default(); + + let mut w = wgetopter_t::new(short_options, long_options, args); + + while let Some(c) = w.wgetopt_long() { + match c { + 's' => opts.no_symlinks = true, + 'h' => opts.print_help = true, + ':' => { + builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], false); + return Err(STATUS_INVALID_ARGS); + } + '?' => { + builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false); + return Err(STATUS_INVALID_ARGS); + } + _ => panic!("unexpected retval from wgetopt_long"), + } + } + + Ok((opts, w.woptind)) +} + +/// An implementation of the external realpath command. Doesn't support any options. +/// In general scripts shouldn't invoke this directly. They should just use `realpath` which +/// will fallback to this builtin if an external command cannot be found. +pub fn realpath( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let cmd = args[0]; + let (opts, optind) = match parse_options(args, parser, streams) { + Ok((opts, optind)) => (opts, optind), + Err(err @ Some(_)) if err != STATUS_CMD_OK => return err, + Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"), + }; + + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + // TODO: allow arbitrary args. `realpath *` should print many paths + if optind + 1 != args.len() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT1, + cmd, + 0, + args.len() - 1 + )); + return STATUS_INVALID_ARGS; + } + + let arg = args[optind]; + + if !opts.no_symlinks { + if let Some(real_path) = wrealpath(arg) { + streams.out.append(real_path); + } else { + // TODO: get error from errno + // Report the error and make it clear this is an error + // from our builtin, not the system's realpath. + streams + .err + .append(wgettext_fmt!("builtin %ls: %ls\n", cmd, arg)); + return STATUS_CMD_ERROR; + } + } else { + // We need to get the *physical* pwd here. + let realpwd = wrealpath(&parser.vars1().get_pwd_slash().from_ffi()); + + if let Some(realpwd) = realpwd { + let absolute_arg = if arg.starts_with(L!("/")) { + arg.to_owned() + } else { + path_apply_working_directory(arg, &realpwd) + }; + streams.out.append(normalize_path(&absolute_arg, false)); + } else { + // TODO: get error from errno + streams + .err + .append(wgettext_fmt!("builtin %ls: realpath failed\n", cmd)); + return STATUS_CMD_ERROR; + } + } + + streams.out.append(L!("\n")); + + STATUS_CMD_OK +} diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index ec2ac45a9..f7163dab3 100644 --- a/fish-rust/src/builtins/shared.rs +++ b/fish-rust/src/builtins/shared.rs @@ -129,6 +129,7 @@ pub fn run_builtin( RustBuiltin::Exit => super::exit::exit(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), RustBuiltin::Return => super::r#return::r#return(parser, streams, args), RustBuiltin::Wait => wait::wait(parser, streams, args), } diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index f1ea2b5e5..dd50f7fc5 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -46,5 +46,7 @@ mod env; mod re; +mod path; + // Don't use `#[cfg(test)]` here to make sure ffi tests are built and tested mod tests; diff --git a/fish-rust/src/path.rs b/fish-rust/src/path.rs new file mode 100644 index 000000000..417be5272 --- /dev/null +++ b/fish-rust/src/path.rs @@ -0,0 +1,52 @@ +use crate::wchar::{wstr, WExt, WString, L}; + +/// If the given path looks like it's relative to the working directory, then prepend that working +/// directory. This operates on unescaped paths only (so a ~ means a literal ~). +pub fn path_apply_working_directory(path: &wstr, working_directory: &wstr) -> WString { + if path.is_empty() || working_directory.is_empty() { + return path.to_owned(); + } + + // We're going to make sure that if we want to prepend the wd, that the string has no leading + // "/". + let prepend_wd = path.as_char_slice()[0] != '/' && path.as_char_slice()[0] != '\u{FDD0}'; + + if !prepend_wd { + // No need to prepend the wd, so just return the path we were given. + return path.to_owned(); + } + + // Remove up to one "./". + let mut path_component = path.to_owned(); + if path_component.starts_with("./") { + path_component.replace_range(0..2, L!("")); + } + + // Removing leading /s. + while path_component.starts_with("/") { + path_component.replace_range(0..1, L!("")); + } + + // Construct and return a new path. + let mut new_path = working_directory.to_owned(); + append_path_component(&mut new_path, &path_component); + new_path +} + +pub fn append_path_component(path: &mut WString, component: &wstr) { + if path.is_empty() || component.is_empty() { + path.push_utfstr(component); + } else { + let path_len = path.len(); + let path_slash = path.as_char_slice()[path_len - 1] == '/'; + let comp_slash = component.as_char_slice()[0] == '/'; + if !path_slash && !comp_slash { + // Need a slash + path.push('/'); + } else if path_slash && comp_slash { + // Too many slashes. + path.pop(); + } + path.push_utfstr(component); + } +} diff --git a/fish-rust/src/wutil/mod.rs b/fish-rust/src/wutil/mod.rs index 7f30eca98..40962adfc 100644 --- a/fish-rust/src/wutil/mod.rs +++ b/fish-rust/src/wutil/mod.rs @@ -1,5 +1,6 @@ pub mod format; pub mod gettext; +mod normalize_path; mod wcstoi; mod wrealpath; @@ -7,6 +8,7 @@ pub(crate) use format::printf::sprintf; pub(crate) use gettext::{wgettext, wgettext_fmt}; +pub use normalize_path::*; pub use wcstoi::*; pub use wrealpath::*; diff --git a/fish-rust/src/wutil/normalize_path.rs b/fish-rust/src/wutil/normalize_path.rs new file mode 100644 index 000000000..728613352 --- /dev/null +++ b/fish-rust/src/wutil/normalize_path.rs @@ -0,0 +1,54 @@ +use std::iter::repeat; + +use crate::wchar::{wstr, WString, L}; + +pub fn normalize_path(path: &wstr, allow_leading_double_slashes: bool) -> WString { + // Count the leading slashes. + let sep = '/'; + let mut leading_slashes: usize = 0; + for (i, &c) in path.as_char_slice().iter().enumerate() { + if c != sep { + leading_slashes = i; + break; + } + } + + let comps = path + .as_char_slice() + .split(|&c| c == sep) + .map(wstr::from_char_slice) + .collect::>(); + let mut new_comps = Vec::new(); + for comp in comps { + if comp.is_empty() || comp == L!(".") { + continue; + } else if comp != L!("..") { + new_comps.push(comp); + } else if !new_comps.is_empty() && new_comps.last().map_or(L!(""), |&s| s) != L!("..") { + // '..' with a real path component, drop that path component. + new_comps.pop(); + } else if leading_slashes == 0 { + // We underflowed the .. and are a relative (not absolute) path. + new_comps.push(L!("..")); + } + } + let mut result = new_comps.into_iter().fold(Vec::new(), |mut acc, x| { + acc.extend_from_slice(x.as_char_slice()); + acc.push('/'); + acc + }); + result.pop(); + // If we don't allow leading double slashes, collapse them to 1 if there are any. + let mut numslashes = if leading_slashes > 0 { 1 } else { 0 }; + // If we do, prepend one or two leading slashes. + // Yes, three+ slashes are collapsed to one. (!) + if allow_leading_double_slashes && leading_slashes == 2 { + numslashes = 2; + } + result.splice(0..0, repeat(sep).take(numslashes)); + // Ensure ./ normalizes to . and not empty. + if result.is_empty() { + result.push('.'); + } + WString::from_chars(result) +} diff --git a/src/builtin.cpp b/src/builtin.cpp index 9673912db..7731a9488 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -46,7 +46,6 @@ #include "builtins/path.h" #include "builtins/printf.h" #include "builtins/read.h" -#include "builtins/realpath.h" #include "builtins/set.h" #include "builtins/set_color.h" #include "builtins/shared.rs.h" @@ -397,7 +396,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L"pwd", &implemented_in_rust, N_(L"Print the working directory")}, {L"random", &implemented_in_rust, N_(L"Generate random number")}, {L"read", &builtin_read, N_(L"Read a line of input into variables")}, - {L"realpath", &builtin_realpath, N_(L"Show absolute path sans symlinks")}, + {L"realpath", &implemented_in_rust, N_(L"Show absolute path sans symlinks")}, {L"return", &implemented_in_rust, N_(L"Stop the currently evaluated function")}, {L"set", &builtin_set, N_(L"Handle environment variables")}, {L"set_color", &builtin_set_color, N_(L"Set the terminal color")}, @@ -546,6 +545,9 @@ static maybe_t try_get_rust_builtin(const wcstring &cmd) { if (cmd == L"random") { return RustBuiltin::Random; } + if (cmd == L"realpath") { + return RustBuiltin::Realpath; + } if (cmd == L"wait") { return RustBuiltin::Wait; } diff --git a/src/builtin.h b/src/builtin.h index 11a987412..40774d4b8 100644 --- a/src/builtin.h +++ b/src/builtin.h @@ -118,6 +118,7 @@ enum RustBuiltin : int32_t { Exit, Pwd, Random, + Realpath, Return, Wait, }; diff --git a/src/builtins/realpath.cpp b/src/builtins/realpath.cpp deleted file mode 100644 index 32d32e7f4..000000000 --- a/src/builtins/realpath.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// Implementation of the realpath builtin. -#include "config.h" // IWYU pragma: keep - -#include "realpath.h" - -#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 "../wcstringutil.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep - -struct realpath_cmd_opts_t { - bool print_help = false; - bool no_symlinks = false; -}; - -static const wchar_t *const short_options = L"+:hs"; -static const struct woption long_options[] = { - {L"no-symlinks", no_argument, 's'}, {L"help", no_argument, 'h'}, {}}; - -static int parse_cmd_opts(realpath_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) - int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case 's': { - opts.no_symlinks = true; - break; - } - case 'h': { - opts.print_help = true; - break; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - *optind = w.woptind; - return STATUS_CMD_OK; -} -/// An implementation of the external realpath command. Doesn't support any options. -/// In general scripts shouldn't invoke this directly. They should just use `realpath` which -/// will fallback to this builtin if an external command cannot be found. -maybe_t builtin_realpath(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - realpath_cmd_opts_t opts; - int argc = builtin_count_args(argv); - int optind; - - int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - if (optind + 1 != argc) { // TODO: allow arbitrary args. `realpath *` should print many paths - streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc - optind); - builtin_print_help(parser, streams, cmd); - return STATUS_INVALID_ARGS; - } - - const wchar_t *arg = argv[optind]; - - if (!opts.no_symlinks) { - if (auto real_path = wrealpath(arg)) { - streams.out.append(*real_path); - } else { - if (errno) { - // realpath() just couldn't do it. Report the error and make it clear - // this is an error from our builtin, not the system's realpath. - streams.err.append_format(L"builtin %ls: %ls: %s\n", cmd, arg, - std::strerror(errno)); - } else { - // Who knows. Probably a bug in our wrealpath() implementation. - streams.err.append_format(_(L"builtin %ls: Invalid arg: %ls\n"), cmd, arg); - } - - return STATUS_CMD_ERROR; - } - } else { - // We need to get the *physical* pwd here. - auto realpwd = wrealpath(parser.vars().get_pwd_slash()); - if (!realpwd) { - streams.err.append_format(L"builtin %ls: realpath failed: %s\n", cmd, - std::strerror(errno)); - return STATUS_CMD_ERROR; - } - wcstring absolute_arg = - string_prefixes_string(L"/", arg) ? arg : path_apply_working_directory(arg, *realpwd); - streams.out.append(normalize_path(absolute_arg, /* allow leading double slashes */ false)); - } - - streams.out.append(L"\n"); - - return STATUS_CMD_OK; -} diff --git a/src/builtins/realpath.h b/src/builtins/realpath.h deleted file mode 100644 index 54cd960d9..000000000 --- a/src/builtins/realpath.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_realpath function. -#ifndef FISH_BUILTIN_REALPATH_H -#define FISH_BUILTIN_REALPATH_H - -#include "../maybe.h" - -class parser_t; -struct io_streams_t; - -maybe_t builtin_realpath(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif