Port builtins/path to Rust

This commit is contained in:
Henrik Hørlück Berg 2023-07-29 13:28:02 +02:00 committed by Peter Ammon
parent 6d4916a77c
commit f4a5de1fbf
24 changed files with 1200 additions and 1121 deletions

View file

@ -104,7 +104,7 @@ set(FISH_BUILTIN_SRCS
src/builtins/disown.cpp src/builtins/disown.cpp
src/builtins/eval.cpp src/builtins/fg.cpp src/builtins/eval.cpp src/builtins/fg.cpp
src/builtins/functions.cpp src/builtins/history.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/read.cpp src/builtins/set.cpp
src/builtins/source.cpp src/builtins/source.cpp
src/builtins/ulimit.cpp src/builtins/ulimit.cpp

View file

@ -13,6 +13,7 @@ pub mod emit;
pub mod exit; pub mod exit;
pub mod function; pub mod function;
pub mod math; pub mod math;
pub mod path;
pub mod printf; pub mod printf;
pub mod pwd; pub mod pwd;
pub mod random; pub mod random;

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
use crate::builtins::{printf, wait}; use crate::builtins::{printf, wait};
use crate::common::str2wcstring;
use crate::ffi::separation_type_t; use crate::ffi::separation_type_t;
use crate::ffi::{self, parser_t, wcstring_list_ffi_t, Repin, RustBuiltin}; use crate::ffi::{self, parser_t, wcstring_list_ffi_t, Repin, RustBuiltin};
use crate::wchar::{wstr, WString, L}; 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 crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use cxx::{type_id, ExternType}; use cxx::{type_id, ExternType};
use libc::c_int; 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; use std::pin::Pin;
pub type BuiltinCmd = fn(&mut parser_t, &mut io_streams_t, &mut [&wstr]) -> Option<c_int>;
#[cxx::bridge] #[cxx::bridge]
mod builtins_ffi { mod builtins_ffi {
extern "C++" { extern "C++" {
@ -225,6 +231,7 @@ pub fn run_builtin(
RustBuiltin::Emit => super::emit::emit(parser, streams, args), RustBuiltin::Emit => super::emit::emit(parser, streams, args),
RustBuiltin::Exit => super::exit::exit(parser, streams, args), RustBuiltin::Exit => super::exit::exit(parser, streams, args),
RustBuiltin::Math => super::math::math(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::Pwd => super::pwd::pwd(parser, streams, args),
RustBuiltin::Random => super::random::random(parser, streams, args), RustBuiltin::Random => super::random::random(parser, streams, args),
RustBuiltin::Realpath => super::realpath::realpath(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<u8>,
/// If not using argv, we read with a buffer
reader: Option<BufReader<File>>,
}
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<Self::Item> {
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);
}
}

View file

@ -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; use crate::wcstringutil::fish_wcwidth_visible;
// Forward some imports to make subcmd implementations easier // Forward some imports to make subcmd implementations easier
use crate::{ use crate::{
builtins::shared::{ builtins::shared::{
builtin_missing_argument, builtin_print_error_trailer, builtin_print_help, io_streams_t, builtin_missing_argument, builtin_print_error_trailer, builtin_print_help, io_streams_t,
BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_ARG_COUNT1, BUILTIN_ERR_COMBO2, Arguments, SplitBehavior, BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_ARG_COUNT1,
BUILTIN_ERR_INVALID_SUBCMD, BUILTIN_ERR_MISSING_SUBCMD, BUILTIN_ERR_NOT_NUMBER, BUILTIN_ERR_COMBO2, BUILTIN_ERR_INVALID_SUBCMD, BUILTIN_ERR_MISSING_SUBCMD,
BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN, STATUS_CMD_ERROR, STATUS_CMD_OK, BUILTIN_ERR_NOT_NUMBER, BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN,
STATUS_INVALID_ARGS, STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS,
}, },
ffi::{parser_t, separation_type_t}, ffi::{parser_t, separation_type_t},
wchar::{wstr, WString, L}, wchar::{wstr, WString, L},
@ -302,126 +296,16 @@ fn escape_code_length(code: &wstr) -> Option<usize> {
} }
} }
/// A helper type for extracting arguments from either argv or stdin. /// Empirically determined.
struct Arguments<'args, 'iter> { /// This is probably down to some pipe buffer or some such,
/// The list of arguments passed to the string builtin. /// 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], args: &'iter [&'args wstr],
/// If using argv, index of the next argument to return.
argidx: &'iter mut usize, argidx: &'iter mut usize,
/// If set, when reading from a stream, split on newlines. streams: &mut io_streams_t,
split_on_newline: bool, ) -> Arguments<'args, 'iter> {
/// Buffer to store what we read with the BufReader Arguments::new(args, argidx, streams, STRING_CHUNK_SIZE)
/// Is only here to avoid allocating every time
buffer: Vec<u8>,
/// If not using argv, we read with a buffer
reader: Option<BufReader<File>>,
}
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<Self::Item> {
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));
}
} }
/// The string builtin, for manipulating strings. /// The string builtin, for manipulating strings.

View file

@ -31,7 +31,9 @@ impl StringSubCommand<'_> for Collect {
) -> Option<libc::c_int> { ) -> Option<libc::c_int> {
let mut appended = 0usize; 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 arg = if !self.no_trim_newlines {
let trim_len = arg.len() - arg.chars().rev().take_while(|&c| c == '\n').count(); let trim_len = arg.len() - arg.chars().rev().take_while(|&c| c == '\n').count();
&arg[..trim_len] &arg[..trim_len]

View file

@ -45,7 +45,7 @@ impl StringSubCommand<'_> for Escape {
}; };
let mut escaped_any = false; 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); let mut escaped = escape_string(&arg, style);
if want_newline { if want_newline {

View file

@ -45,9 +45,9 @@ impl<'args> StringSubCommand<'args> for Join<'args> {
} }
let Some(arg) = args.get(*optind).copied() else { let Some(arg) = args.get(*optind).copied() else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]); string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]);
return STATUS_INVALID_ARGS; return STATUS_INVALID_ARGS;
}; };
*optind += 1; *optind += 1;
self.sep = arg; self.sep = arg;
@ -64,7 +64,7 @@ impl<'args> StringSubCommand<'args> for Join<'args> {
let sep = &self.sep; let sep = &self.sep;
let mut nargs = 0usize; let mut nargs = 0usize;
let mut print_trailing_newline = true; 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.quiet {
if self.no_empty && arg.is_empty() { if self.no_empty && arg.is_empty() {
continue; continue;

View file

@ -33,7 +33,7 @@ impl StringSubCommand<'_> for Length {
) -> Option<libc::c_int> { ) -> Option<libc::c_int> {
let mut nnonempty = 0usize; let mut nnonempty = 0usize;
for (arg, _) in Arguments::new(args, optind, streams) { for (arg, _) in arguments(args, optind, streams) {
if self.visible { if self.visible {
// Visible length only makes sense line-wise. // Visible length only makes sense line-wise.
for line in split_string(&arg, '\n') { for line in split_string(&arg, '\n') {

View file

@ -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) { if let Err(e) = matcher.report_matches(arg.as_ref(), streams) {
FLOG!(error, "pcre2_match unexpected error:", e.error_message()) FLOG!(error, "pcre2_match unexpected error:", e.error_message())
} }

View file

@ -74,7 +74,7 @@ impl StringSubCommand<'_> for Pad {
let mut inputs: Vec<(Cow<'args, wstr>, usize)> = Vec::new(); let mut inputs: Vec<(Cow<'args, wstr>, usize)> = Vec::new();
let mut print_trailing_newline = true; 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); let width = width_without_escapes(&arg, 0);
max_width = max_width.max(width); max_width = max_width.max(width);
inputs.push((arg, width)); inputs.push((arg, width));

View file

@ -55,7 +55,7 @@ impl StringSubCommand<'_> for Repeat {
let mut first = true; let mut first = true;
let mut print_trailing_newline = 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; print_trailing_newline = want_newline;
if w.is_empty() { if w.is_empty() {
continue; continue;

View file

@ -79,7 +79,7 @@ impl<'args> StringSubCommand<'args> for Replace<'args> {
let mut replace_count = 0; 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) { let (replaced, result) = match replacer.replace(arg) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {

View file

@ -72,7 +72,7 @@ impl<'args> StringSubCommand<'args> for Shorten<'args> {
let mut min_width = usize::MAX; let mut min_width = usize::MAX;
let mut inputs = Vec::new(); let mut inputs = Vec::new();
let iter = Arguments::new(args, optind, streams); let iter = arguments(args, optind, streams);
if self.max == Some(0) { if self.max == Some(0) {
// Special case: Max of 0 means no shortening. // Special case: Max of 0 means no shortening.

View file

@ -181,10 +181,10 @@ impl<'args> StringSubCommand<'args> for Split<'args> {
let mut split_count = 0usize; let mut split_count = 0usize;
let mut arg_count = 0usize; let mut arg_count = 0usize;
let argiter = match self.is_split0 { let argiter = arguments(args, optind, streams).with_split_behavior(match self.is_split0 {
false => Arguments::new(args, optind, streams), false => SplitBehavior::Newline,
true => Arguments::without_splitting_on_newline(args, optind, streams), true => SplitBehavior::Never,
}; });
for (arg, _) in argiter { for (arg, _) in argiter {
let splits: Vec<Cow<'args, wstr>> = match (self.split_from, arg) { let splits: Vec<Cow<'args, wstr>> = match (self.split_from, arg) {
(Direction::Right, arg) => { (Direction::Right, arg) => {

View file

@ -65,7 +65,7 @@ impl StringSubCommand<'_> for Sub {
} }
let mut nsub = 0; 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() { let start: usize = match self.start.map(i64::from).unwrap_or_default() {
n @ 1.. => n as usize - 1, n @ 1.. => n as usize - 1,
0 => 0, 0 => 0,

View file

@ -25,7 +25,7 @@ impl StringSubCommand<'_> for Transform {
) -> Option<libc::c_int> { ) -> Option<libc::c_int> {
let mut n_transformed = 0usize; 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); let transformed = (self.func)(&arg);
if transformed != arg { if transformed != arg {
n_transformed += 1; n_transformed += 1;

View file

@ -72,7 +72,7 @@ impl<'args> StringSubCommand<'args> for Trim<'args> {
.count() .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); 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 // collision is only an issue if the whole string is getting trimmed
let trim_end = (self.right && trim_start != arg.len()) let trim_end = (self.right && trim_start != arg.len())

View file

@ -38,7 +38,7 @@ impl StringSubCommand<'_> for Unescape {
args: &[&wstr], args: &[&wstr],
) -> Option<libc::c_int> { ) -> Option<libc::c_int> {
let mut nesc = 0; 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) { if let Some(res) = unescape_string(&arg, self.style) {
streams.out.append(res); streams.out.append(res);
if want_newline { if want_newline {

View file

@ -511,13 +511,13 @@ pub fn fish_wcswidth(s: &wstr) -> libc::c_int {
/// problem). Therefore we include richer information. /// problem). Therefore we include richer information.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct FileId { pub struct FileId {
device: libc::dev_t, pub device: libc::dev_t,
inode: libc::ino_t, pub inode: libc::ino_t,
size: u64, pub size: u64,
change_seconds: libc::time_t, pub change_seconds: libc::time_t,
change_nanoseconds: i64, pub change_nanoseconds: i64,
mod_seconds: libc::time_t, pub mod_seconds: libc::time_t,
mod_nanoseconds: i64, pub mod_nanoseconds: i64,
} }
impl FileId { impl FileId {

View file

@ -38,7 +38,6 @@
#include "builtins/functions.h" #include "builtins/functions.h"
#include "builtins/history.h" #include "builtins/history.h"
#include "builtins/jobs.h" #include "builtins/jobs.h"
#include "builtins/path.h"
#include "builtins/read.h" #include "builtins/read.h"
#include "builtins/set.h" #include "builtins/set.h"
#include "builtins/shared.rs.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"math", &implemented_in_rust, N_(L"Evaluate math expressions")},
{L"not", &builtin_generic, N_(L"Negate exit status of job")}, {L"not", &builtin_generic, N_(L"Negate exit status of job")},
{L"or", &builtin_generic, N_(L"Execute command if previous command failed")}, {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"printf", &implemented_in_rust, N_(L"Prints formatted text")},
{L"pwd", &implemented_in_rust, N_(L"Print the working directory")}, {L"pwd", &implemented_in_rust, N_(L"Print the working directory")},
{L"random", &implemented_in_rust, N_(L"Generate random number")}, {L"random", &implemented_in_rust, N_(L"Generate random number")},
@ -580,6 +579,9 @@ static maybe_t<RustBuiltin> try_get_rust_builtin(const wcstring &cmd) {
if (cmd == L"wait") { if (cmd == L"wait") {
return RustBuiltin::Wait; return RustBuiltin::Wait;
} }
if (cmd == L"path") {
return RustBuiltin::Path;
}
if (cmd == L"printf") { if (cmd == L"printf") {
return RustBuiltin::Printf; return RustBuiltin::Printf;
} }

View file

@ -124,6 +124,7 @@ enum class RustBuiltin : int32_t {
Emit, Emit,
Exit, Exit,
Math, Math,
Path,
Printf, Printf,
Pwd, Pwd,
Random, Random,

View file

@ -1,952 +0,0 @@
// Implementation of the path builtin.
#include "config.h" // IWYU pragma: keep
#include "path.h"
#include <stdint.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <climits>
#include <cstdarg>
#include <ctime>
#include <cwchar>
#include <map>
#include <string>
#include <unordered_map>
#include <utility>
#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<wcstring> 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<wcstring> 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<char, decltype(*handle_flag_q)> 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<size_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<wcstring> 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<wcstring, wcstring> 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<int> 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);
}

View file

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