Rewrite cd builtin in Rust

Note this is slightly incomplete - the FD is not moved into the parser, and so
will be freed at the end of each directory change. The FD saved in the parser is
never actually used in existing code, so this doesn't break anything, but will
need to be corrected once the parser is ported.
This commit is contained in:
David Adam 2023-04-23 17:43:57 +08:00
parent e31c0ebb05
commit 289fbecaa9
8 changed files with 185 additions and 152 deletions

View file

@ -100,7 +100,6 @@ endif()
# List of sources for builtin functions.
set(FISH_BUILTIN_SRCS
src/builtin.cpp src/builtins/bind.cpp
src/builtins/cd.cpp
src/builtins/commandline.cpp src/builtins/complete.cpp
src/builtins/disown.cpp
src/builtins/eval.cpp src/builtins/fg.cpp

View file

@ -0,0 +1,178 @@
// Implementation of the cd builtin.
use super::shared::{builtin_print_help, io_streams_t, STATUS_CMD_ERROR};
use crate::{
builtins::shared::{HelpOnlyCmdOpts, STATUS_CMD_OK},
env::{EnvMode, Environment},
fds::{wopen_cloexec, AutoCloseFd},
ffi::{parser_t, Repin},
path::path_apply_cdpath,
wchar::{wstr, WString, L},
wchar_ffi::{WCharFromFFI, WCharToFFI},
wutil::{normalize_path, wgettext_fmt, wperror, wreadlink},
};
use errno::{self, Errno};
use libc::{c_int, fchdir, EACCES, ELOOP, ENOENT, ENOTDIR, EPERM, O_RDONLY};
// The cd builtin. Changes the current directory to the one specified or to $HOME if none is
// specified. The directory can be relative to any directory in the CDPATH variable.
pub fn cd(parser: &mut parser_t, streams: &mut io_streams_t, args: &mut [&wstr]) -> Option<c_int> {
let cmd = args[0];
let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) {
Ok(opts) => opts,
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;
}
let vars = parser.get_vars();
let tmpstr;
let dir_in: &wstr = if args.len() > opts.optind {
args[opts.optind]
} else {
match vars.get_unless_empty(L!("HOME")) {
Some(v) => {
tmpstr = v.as_string();
&tmpstr
}
None => {
streams
.err
.append(wgettext_fmt!("%ls: Could not find home directory\n", cmd));
return STATUS_CMD_ERROR;
}
}
};
// Stop `cd ""` from crashing
if dir_in.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: Empty directory '%ls' does not exist\n",
cmd,
dir_in
));
if !parser.is_interactive() {
streams.err.append(parser.pin().current_line().from_ffi());
};
return STATUS_CMD_ERROR;
}
let pwd = vars.get_pwd_slash();
let dirs = path_apply_cdpath(dir_in, &pwd, vars.as_ref());
if dirs.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: The directory '%ls' does not exist\n",
cmd,
dir_in
));
if !parser.is_interactive() {
streams.err.append(parser.pin().current_line().from_ffi());
}
return STATUS_CMD_ERROR;
}
let mut best_errno = 0;
let mut broken_symlink = WString::new();
let mut broken_symlink_target = WString::new();
for dir in dirs {
let norm_dir = normalize_path(&dir, true);
errno::set_errno(Errno(0));
// We need to keep around the fd for this directory, in the parser.
let dir_fd = AutoCloseFd::new(wopen_cloexec(&norm_dir, O_RDONLY, 0));
if !(dir_fd.is_valid() && unsafe { fchdir(dir_fd.fd()) } == 0) {
// Some errors we skip and only report if nothing worked.
// ENOENT in particular is very low priority
// - if in another directory there was a *file* by the correct name
// we prefer *that* error because it's more specific
if errno::errno().0 == ENOENT {
let tmp = wreadlink(&norm_dir);
// clippy doesn't like this is_some/unwrap pair, but using if let is harder to read IMO
#[allow(clippy::unnecessary_unwrap)]
if broken_symlink.is_empty() && tmp.is_some() {
broken_symlink = norm_dir;
broken_symlink_target = tmp.unwrap();
} else if best_errno == 0 {
best_errno = errno::errno().0;
}
continue;
} else if errno::errno().0 == ENOTDIR {
best_errno = errno::errno().0;
continue;
}
best_errno = errno::errno().0;
break;
}
// Port note: sending the AutocloseFd across the FFI interface requires additional work
// It's never actually used in the target parser object (perhaps will be after the port to Rust)
// Keep this commented until the parser is ported.
//parser.libdata().cwd_fd = std::make_shared<const autoclose_fd_t>(std::move(dir_fd));
parser.pin().set_var_and_fire(
&L!("PWD").to_ffi(),
EnvMode::EXPORT.bits() | EnvMode::GLOBAL.bits(),
norm_dir,
);
return STATUS_CMD_OK;
}
if best_errno == ENOTDIR {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a directory\n",
cmd,
dir_in
));
} else if !broken_symlink.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is a broken symbolic link to '%ls'\n",
cmd,
broken_symlink,
broken_symlink_target
));
} else if best_errno == ELOOP {
streams.err.append(wgettext_fmt!(
"%ls: Too many levels of symbolic links: '%ls'\n",
cmd,
dir_in
));
} else if best_errno == ENOENT {
streams.err.append(wgettext_fmt!(
"%ls: The directory '%ls' does not exist\n",
cmd,
dir_in
));
} else if best_errno == EACCES || best_errno == EPERM {
streams.err.append(wgettext_fmt!(
"%ls: Permission denied: '%ls'\n",
cmd,
dir_in
));
} else {
errno::set_errno(Errno(best_errno));
wperror(L!("cd"));
streams.err.append(wgettext_fmt!(
"%ls: Unknown error trying to locate directory '%ls'\n",
cmd,
dir_in
));
}
if !parser.is_interactive() {
streams.err.append(parser.pin().current_line().from_ffi());
}
return STATUS_CMD_ERROR;
}

View file

@ -5,6 +5,7 @@ pub mod argparse;
pub mod bg;
pub mod block;
pub mod builtin;
pub mod cd;
pub mod command;
pub mod contains;
pub mod echo;

View file

@ -190,6 +190,7 @@ pub fn run_builtin(
RustBuiltin::Bg => super::bg::bg(parser, streams, args),
RustBuiltin::Block => super::block::block(parser, streams, args),
RustBuiltin::Builtin => super::builtin::builtin(parser, streams, args),
RustBuiltin::Cd => super::cd::cd(parser, streams, args),
RustBuiltin::Contains => super::contains::contains(parser, streams, args),
RustBuiltin::Command => super::command::command(parser, streams, args),
RustBuiltin::Echo => super::echo::echo(parser, streams, args),

View file

@ -30,7 +30,6 @@
#include <string>
#include "builtins/bind.h"
#include "builtins/cd.h"
#include "builtins/commandline.h"
#include "builtins/complete.h"
#include "builtins/disown.h"
@ -357,7 +356,7 @@ static constexpr builtin_data_t builtin_datas[] = {
{L"breakpoint", &builtin_breakpoint, N_(L"Halt execution and start debug prompt")},
{L"builtin", &implemented_in_rust, N_(L"Run a builtin specifically")},
{L"case", &builtin_generic, N_(L"Block of code to run conditionally")},
{L"cd", &builtin_cd, N_(L"Change working directory")},
{L"cd", &implemented_in_rust, N_(L"Change working directory")},
{L"command", &implemented_in_rust, N_(L"Run a command specifically")},
{L"commandline", &builtin_commandline, N_(L"Set or get the commandline")},
{L"complete", &builtin_complete, N_(L"Edit command specific completions")},
@ -534,6 +533,9 @@ static maybe_t<RustBuiltin> try_get_rust_builtin(const wcstring &cmd) {
if (cmd == L"builtin") {
return RustBuiltin::Builtin;
}
if (cmd == L"cd") {
return RustBuiltin::Cd;
}
if (cmd == L"contains") {
return RustBuiltin::Contains;
}

View file

@ -117,6 +117,7 @@ enum class RustBuiltin : int32_t {
Bg,
Block,
Builtin,
Cd,
Contains,
Command,
Echo,

View file

@ -1,138 +0,0 @@
// Implementation of the cd builtin.
#include "config.h" // IWYU pragma: keep
#include "cd.h"
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "../builtin.h"
#include "../common.h"
#include "../env.h"
#include "../fallback.h" // IWYU pragma: keep
#include "../fds.h"
#include "../io.h"
#include "../maybe.h"
#include "../parser.h"
#include "../path.h"
#include "../wutil.h" // IWYU pragma: keep
/// The cd builtin. Changes the current directory to the one specified or to $HOME if none is
/// specified. The directory can be relative to any directory in the CDPATH variable.
maybe_t<int> builtin_cd(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
const wchar_t *cmd = argv[0];
int argc = builtin_count_args(argv);
help_only_cmd_opts_t opts;
int optind;
int retval = parse_help_only_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;
}
wcstring dir_in;
if (argv[optind]) {
dir_in = argv[optind];
} else {
auto maybe_dir_in = parser.vars().get_unless_empty(L"HOME");
if (!maybe_dir_in) {
streams.err.append_format(_(L"%ls: Could not find home directory\n"), cmd);
return STATUS_CMD_ERROR;
}
dir_in = maybe_dir_in->as_string();
}
if (dir_in.empty()) {
streams.err.append_format(_(L"%ls: Empty directory '%ls' does not exist\n"), cmd,
dir_in.c_str());
if (!parser.is_interactive()) streams.err.append(parser.current_line());
return STATUS_CMD_ERROR;
}
wcstring pwd = parser.vars().get_pwd_slash();
auto dirs = path_apply_cdpath(dir_in, pwd, parser.vars());
if (dirs.empty()) {
streams.err.append_format(_(L"%ls: The directory '%ls' does not exist\n"), cmd,
dir_in.c_str());
if (!parser.is_interactive()) streams.err.append(parser.current_line());
return STATUS_CMD_ERROR;
}
errno = 0;
auto best_errno = errno;
wcstring broken_symlink, broken_symlink_target;
for (const auto &dir : dirs) {
wcstring norm_dir = normalize_path(dir);
// We need to keep around the fd for this directory, in the parser.
errno = 0;
autoclose_fd_t dir_fd(wopen_cloexec(norm_dir, O_RDONLY));
bool success = dir_fd.valid() && fchdir(dir_fd.fd()) == 0;
if (!success) {
// Some errors we skip and only report if nothing worked.
// ENOENT in particular is very low priority
// - if in another directory there was a *file* by the correct name
// we prefer *that* error because it's more specific
if (errno == ENOENT) {
maybe_t<wcstring> tmp;
if (broken_symlink.empty() && (tmp = wreadlink(norm_dir))) {
broken_symlink = norm_dir;
broken_symlink_target = std::move(*tmp);
} else if (!best_errno)
best_errno = errno;
continue;
} else if (errno == ENOTDIR) {
best_errno = errno;
continue;
}
best_errno = errno;
break;
}
parser.libdata().cwd_fd = std::make_shared<const autoclose_fd_t>(std::move(dir_fd));
parser.set_var_and_fire(L"PWD", ENV_EXPORT | ENV_GLOBAL, std::move(norm_dir));
return STATUS_CMD_OK;
}
if (best_errno == ENOTDIR) {
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
} else if (!broken_symlink.empty()) {
streams.err.append_format(_(L"%ls: '%ls' is a broken symbolic link to '%ls'\n"), cmd,
broken_symlink.c_str(), broken_symlink_target.c_str());
} else if (best_errno == ELOOP) {
streams.err.append_format(_(L"%ls: Too many levels of symbolic links: '%ls'\n"), cmd,
dir_in.c_str());
} else if (best_errno == ENOENT) {
streams.err.append_format(_(L"%ls: The directory '%ls' does not exist\n"), cmd,
dir_in.c_str());
} else if (best_errno == EACCES || best_errno == EPERM) {
streams.err.append_format(_(L"%ls: Permission denied: '%ls'\n"), cmd, dir_in.c_str());
} else {
errno = best_errno;
wperror(L"cd");
streams.err.append_format(_(L"%ls: Unknown error trying to locate directory '%ls'\n"), cmd,
dir_in.c_str());
}
if (!parser.is_interactive()) {
streams.err.append(parser.current_line());
}
return STATUS_CMD_ERROR;
}

View file

@ -1,11 +0,0 @@
// Prototypes for executing builtin_cd function.
#ifndef FISH_BUILTIN_CD_H
#define FISH_BUILTIN_CD_H
#include "../maybe.h"
class parser_t;
struct io_streams_t;
maybe_t<int> builtin_cd(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
#endif