mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-26 21:03:12 +00:00
Port builtins/path to Rust
This commit is contained in:
parent
6d4916a77c
commit
f4a5de1fbf
24 changed files with 1200 additions and 1121 deletions
|
@ -104,7 +104,7 @@ set(FISH_BUILTIN_SRCS
|
|||
src/builtins/disown.cpp
|
||||
src/builtins/eval.cpp src/builtins/fg.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/source.cpp
|
||||
src/builtins/ulimit.cpp
|
||||
|
|
|
@ -13,6 +13,7 @@ pub mod emit;
|
|||
pub mod exit;
|
||||
pub mod function;
|
||||
pub mod math;
|
||||
pub mod path;
|
||||
pub mod printf;
|
||||
pub mod pwd;
|
||||
pub mod random;
|
||||
|
|
1004
fish-rust/src/builtins/path.rs
Normal file
1004
fish-rust/src/builtins/path.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
|||
use crate::builtins::{printf, wait};
|
||||
use crate::common::str2wcstring;
|
||||
use crate::ffi::separation_type_t;
|
||||
use crate::ffi::{self, parser_t, wcstring_list_ffi_t, Repin, RustBuiltin};
|
||||
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 cxx::{type_id, ExternType};
|
||||
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;
|
||||
|
||||
pub type BuiltinCmd = fn(&mut parser_t, &mut io_streams_t, &mut [&wstr]) -> Option<c_int>;
|
||||
|
||||
#[cxx::bridge]
|
||||
mod builtins_ffi {
|
||||
extern "C++" {
|
||||
|
@ -225,6 +231,7 @@ pub fn run_builtin(
|
|||
RustBuiltin::Emit => super::emit::emit(parser, streams, args),
|
||||
RustBuiltin::Exit => super::exit::exit(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::Random => super::random::random(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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
// Forward some imports to make subcmd implementations easier
|
||||
use crate::{
|
||||
builtins::shared::{
|
||||
builtin_missing_argument, builtin_print_error_trailer, builtin_print_help, io_streams_t,
|
||||
BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_ARG_COUNT1, BUILTIN_ERR_COMBO2,
|
||||
BUILTIN_ERR_INVALID_SUBCMD, BUILTIN_ERR_MISSING_SUBCMD, BUILTIN_ERR_NOT_NUMBER,
|
||||
BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN, STATUS_CMD_ERROR, STATUS_CMD_OK,
|
||||
STATUS_INVALID_ARGS,
|
||||
Arguments, SplitBehavior, BUILTIN_ERR_ARG_COUNT0, BUILTIN_ERR_ARG_COUNT1,
|
||||
BUILTIN_ERR_COMBO2, BUILTIN_ERR_INVALID_SUBCMD, BUILTIN_ERR_MISSING_SUBCMD,
|
||||
BUILTIN_ERR_NOT_NUMBER, BUILTIN_ERR_TOO_MANY_ARGUMENTS, BUILTIN_ERR_UNKNOWN,
|
||||
STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS,
|
||||
},
|
||||
ffi::{parser_t, separation_type_t},
|
||||
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.
|
||||
struct Arguments<'args, 'iter> {
|
||||
/// The list of arguments passed to the string builtin.
|
||||
/// 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 arguments<'iter, 'args>(
|
||||
args: &'iter [&'args wstr],
|
||||
/// If using argv, index of the next argument to return.
|
||||
argidx: &'iter mut usize,
|
||||
/// If set, when reading from a stream, split on newlines.
|
||||
split_on_newline: bool,
|
||||
/// 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> {
|
||||
/// 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));
|
||||
}
|
||||
streams: &mut io_streams_t,
|
||||
) -> Arguments<'args, 'iter> {
|
||||
Arguments::new(args, argidx, streams, STRING_CHUNK_SIZE)
|
||||
}
|
||||
|
||||
/// The string builtin, for manipulating strings.
|
||||
|
|
|
@ -31,7 +31,9 @@ impl StringSubCommand<'_> for Collect {
|
|||
) -> Option<libc::c_int> {
|
||||
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 trim_len = arg.len() - arg.chars().rev().take_while(|&c| c == '\n').count();
|
||||
&arg[..trim_len]
|
||||
|
|
|
@ -45,7 +45,7 @@ impl StringSubCommand<'_> for Escape {
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
if want_newline {
|
||||
|
|
|
@ -45,9 +45,9 @@ impl<'args> StringSubCommand<'args> for Join<'args> {
|
|||
}
|
||||
|
||||
let Some(arg) = args.get(*optind).copied() else {
|
||||
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
};
|
||||
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
};
|
||||
*optind += 1;
|
||||
self.sep = arg;
|
||||
|
||||
|
@ -64,7 +64,7 @@ impl<'args> StringSubCommand<'args> for Join<'args> {
|
|||
let sep = &self.sep;
|
||||
let mut nargs = 0usize;
|
||||
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.no_empty && arg.is_empty() {
|
||||
continue;
|
||||
|
|
|
@ -33,7 +33,7 @@ impl StringSubCommand<'_> for Length {
|
|||
) -> Option<libc::c_int> {
|
||||
let mut nnonempty = 0usize;
|
||||
|
||||
for (arg, _) in Arguments::new(args, optind, streams) {
|
||||
for (arg, _) in arguments(args, optind, streams) {
|
||||
if self.visible {
|
||||
// Visible length only makes sense line-wise.
|
||||
for line in split_string(&arg, '\n') {
|
||||
|
|
|
@ -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) {
|
||||
FLOG!(error, "pcre2_match unexpected error:", e.error_message())
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ impl StringSubCommand<'_> for Pad {
|
|||
let mut inputs: Vec<(Cow<'args, wstr>, usize)> = Vec::new();
|
||||
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);
|
||||
max_width = max_width.max(width);
|
||||
inputs.push((arg, width));
|
||||
|
|
|
@ -55,7 +55,7 @@ impl StringSubCommand<'_> for Repeat {
|
|||
let mut first = 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;
|
||||
if w.is_empty() {
|
||||
continue;
|
||||
|
|
|
@ -79,7 +79,7 @@ impl<'args> StringSubCommand<'args> for Replace<'args> {
|
|||
|
||||
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) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
|
|
|
@ -72,7 +72,7 @@ impl<'args> StringSubCommand<'args> for Shorten<'args> {
|
|||
let mut min_width = usize::MAX;
|
||||
let mut inputs = Vec::new();
|
||||
|
||||
let iter = Arguments::new(args, optind, streams);
|
||||
let iter = arguments(args, optind, streams);
|
||||
|
||||
if self.max == Some(0) {
|
||||
// Special case: Max of 0 means no shortening.
|
||||
|
|
|
@ -181,10 +181,10 @@ impl<'args> StringSubCommand<'args> for Split<'args> {
|
|||
let mut split_count = 0usize;
|
||||
let mut arg_count = 0usize;
|
||||
|
||||
let argiter = match self.is_split0 {
|
||||
false => Arguments::new(args, optind, streams),
|
||||
true => Arguments::without_splitting_on_newline(args, optind, streams),
|
||||
};
|
||||
let argiter = arguments(args, optind, streams).with_split_behavior(match self.is_split0 {
|
||||
false => SplitBehavior::Newline,
|
||||
true => SplitBehavior::Never,
|
||||
});
|
||||
for (arg, _) in argiter {
|
||||
let splits: Vec<Cow<'args, wstr>> = match (self.split_from, arg) {
|
||||
(Direction::Right, arg) => {
|
||||
|
|
|
@ -65,7 +65,7 @@ impl StringSubCommand<'_> for Sub {
|
|||
}
|
||||
|
||||
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() {
|
||||
n @ 1.. => n as usize - 1,
|
||||
0 => 0,
|
||||
|
|
|
@ -25,7 +25,7 @@ impl StringSubCommand<'_> for Transform {
|
|||
) -> Option<libc::c_int> {
|
||||
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);
|
||||
if transformed != arg {
|
||||
n_transformed += 1;
|
||||
|
|
|
@ -72,7 +72,7 @@ impl<'args> StringSubCommand<'args> for Trim<'args> {
|
|||
.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);
|
||||
// collision is only an issue if the whole string is getting trimmed
|
||||
let trim_end = (self.right && trim_start != arg.len())
|
||||
|
|
|
@ -38,7 +38,7 @@ impl StringSubCommand<'_> for Unescape {
|
|||
args: &[&wstr],
|
||||
) -> Option<libc::c_int> {
|
||||
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) {
|
||||
streams.out.append(res);
|
||||
if want_newline {
|
||||
|
|
|
@ -511,13 +511,13 @@ pub fn fish_wcswidth(s: &wstr) -> libc::c_int {
|
|||
/// problem). Therefore we include richer information.
|
||||
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct FileId {
|
||||
device: libc::dev_t,
|
||||
inode: libc::ino_t,
|
||||
size: u64,
|
||||
change_seconds: libc::time_t,
|
||||
change_nanoseconds: i64,
|
||||
mod_seconds: libc::time_t,
|
||||
mod_nanoseconds: i64,
|
||||
pub device: libc::dev_t,
|
||||
pub inode: libc::ino_t,
|
||||
pub size: u64,
|
||||
pub change_seconds: libc::time_t,
|
||||
pub change_nanoseconds: i64,
|
||||
pub mod_seconds: libc::time_t,
|
||||
pub mod_nanoseconds: i64,
|
||||
}
|
||||
|
||||
impl FileId {
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
#include "builtins/functions.h"
|
||||
#include "builtins/history.h"
|
||||
#include "builtins/jobs.h"
|
||||
#include "builtins/path.h"
|
||||
#include "builtins/read.h"
|
||||
#include "builtins/set.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"not", &builtin_generic, N_(L"Negate exit status of job")},
|
||||
{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"pwd", &implemented_in_rust, N_(L"Print the working directory")},
|
||||
{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") {
|
||||
return RustBuiltin::Wait;
|
||||
}
|
||||
if (cmd == L"path") {
|
||||
return RustBuiltin::Path;
|
||||
}
|
||||
if (cmd == L"printf") {
|
||||
return RustBuiltin::Printf;
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ enum class RustBuiltin : int32_t {
|
|||
Emit,
|
||||
Exit,
|
||||
Math,
|
||||
Path,
|
||||
Printf,
|
||||
Pwd,
|
||||
Random,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
Loading…
Reference in a new issue