builtins: Port realpath to Rust

This commit is contained in:
Victor Song 2023-03-05 21:38:41 -05:00 committed by ridiculousfish
parent 47b4e3d067
commit ca494778e4
12 changed files with 247 additions and 134 deletions

View file

@ -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
)

View file

@ -9,5 +9,6 @@ pub mod emit;
pub mod exit;
pub mod pwd;
pub mod random;
pub mod realpath;
pub mod r#return;
pub mod wait;

View file

@ -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<c_int>> {
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<c_int> {
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
}

View file

@ -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),
}

View file

@ -46,5 +46,7 @@ mod builtins;
mod env;
mod re;
mod path;
// Don't use `#[cfg(test)]` here to make sure ffi tests are built and tested
mod tests;

52
fish-rust/src/path.rs Normal file
View file

@ -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);
}
}

View file

@ -1,5 +1,6 @@
pub mod format;
pub mod gettext;
mod normalize_path;
mod wcstoi;
mod wrealpath;
@ -7,6 +8,7 @@ use std::io::Write;
pub(crate) use format::printf::sprintf;
pub(crate) use gettext::{wgettext, wgettext_fmt};
pub use normalize_path::*;
pub use wcstoi::*;
pub use wrealpath::*;

View file

@ -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::<Vec<_>>();
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)
}

View file

@ -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<RustBuiltin> 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;
}

View file

@ -118,6 +118,7 @@ enum RustBuiltin : int32_t {
Exit,
Pwd,
Random,
Realpath,
Return,
Wait,
};

View file

@ -1,119 +0,0 @@
// Implementation of the realpath builtin.
#include "config.h" // IWYU pragma: keep
#include "realpath.h"
#include <cerrno>
#include <cstring>
#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<int> 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;
}

View file

@ -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<int> builtin_realpath(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
#endif