mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-27 21:33:09 +00:00
builtins: Port realpath
to Rust
This commit is contained in:
parent
47b4e3d067
commit
ca494778e4
12 changed files with 247 additions and 134 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
|
129
fish-rust/src/builtins/realpath.rs
Normal file
129
fish-rust/src/builtins/realpath.rs
Normal 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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
52
fish-rust/src/path.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
||||
|
|
54
fish-rust/src/wutil/normalize_path.rs
Normal file
54
fish-rust/src/wutil/normalize_path.rs
Normal 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)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@ enum RustBuiltin : int32_t {
|
|||
Exit,
|
||||
Pwd,
|
||||
Random,
|
||||
Realpath,
|
||||
Return,
|
||||
Wait,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
Loading…
Reference in a new issue