2023-07-29 19:28:02 +08:00
|
|
|
use crate::env::environment::Environment;
|
|
|
|
use std::cmp::Ordering;
|
2023-11-15 00:49:38 +08:00
|
|
|
use std::fs::Metadata;
|
2023-07-29 19:28:02 +08:00
|
|
|
use std::os::unix::prelude::{FileTypeExt, MetadataExt};
|
|
|
|
use std::time::SystemTime;
|
|
|
|
|
2023-08-09 06:17:10 +08:00
|
|
|
use super::prelude::*;
|
2023-11-20 03:07:24 +08:00
|
|
|
use crate::nix::{getegid, geteuid};
|
2023-07-29 19:28:02 +08:00
|
|
|
use crate::path::path_apply_working_directory;
|
|
|
|
use crate::util::wcsfilecmp_glob;
|
|
|
|
use crate::wcstringutil::split_string_tok;
|
|
|
|
use crate::wutil::{
|
|
|
|
file_id_for_path, lwstat, normalize_path, waccess, wbasename, wdirname, wrealpath, wstat,
|
|
|
|
INVALID_FILE_ID,
|
|
|
|
};
|
|
|
|
use bitflags::bitflags;
|
2023-11-20 03:07:24 +08:00
|
|
|
use libc::{mode_t, F_OK, PATH_MAX, R_OK, S_ISGID, S_ISUID, W_OK, X_OK};
|
2023-07-29 19:28:02 +08:00
|
|
|
|
|
|
|
use super::shared::BuiltinCmd;
|
|
|
|
|
|
|
|
macro_rules! path_error {
|
|
|
|
(
|
|
|
|
$streams:expr,
|
|
|
|
$string:expr
|
|
|
|
$(, $args:expr)+
|
|
|
|
$(,)?
|
|
|
|
) => {
|
|
|
|
$streams.err.append(L!("path "));
|
|
|
|
$streams.err.append(wgettext_fmt!($string, $($args),*));
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_unknown_option(parser: &Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt);
|
2023-10-09 05:22:27 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, L!("path"));
|
2023-07-29 19:28:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
const PATH_CHUNK_SIZE: usize = PATH_MAX as usize;
|
|
|
|
#[inline]
|
|
|
|
fn arguments<'iter, 'args>(
|
|
|
|
args: &'iter [&'args wstr],
|
|
|
|
optind: &'iter mut usize,
|
2023-10-09 05:10:05 +08:00
|
|
|
streams: &mut IoStreams,
|
2023-07-29 19:28:02 +08:00
|
|
|
) -> Arguments<'args, 'iter> {
|
|
|
|
Arguments::new(args, optind, streams, PATH_CHUNK_SIZE)
|
|
|
|
}
|
|
|
|
|
|
|
|
bitflags! {
|
2023-09-15 17:56:03 +08:00
|
|
|
#[derive(Copy, Clone, Default)]
|
2023-07-29 19:28:02 +08:00
|
|
|
pub struct TypeFlags: u32 {
|
|
|
|
/// A block device
|
|
|
|
const BLOCK = 1 << 0;
|
|
|
|
/// A directory
|
|
|
|
const DIR = 1 << 1;
|
|
|
|
/// A regular file
|
|
|
|
const FILE = 1 << 2;
|
|
|
|
/// A link
|
|
|
|
const LINK = 1 << 3;
|
|
|
|
/// A character device
|
|
|
|
const CHAR = 1 << 4;
|
|
|
|
/// A fifo
|
|
|
|
const FIFO = 1 << 5;
|
|
|
|
/// A socket
|
|
|
|
const SOCK = 1 << 6;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<&wstr> for TypeFlags {
|
|
|
|
type Error = ();
|
|
|
|
|
|
|
|
fn try_from(value: &wstr) -> Result<Self, Self::Error> {
|
|
|
|
let flag = match value {
|
|
|
|
t if t == "file" => Self::FILE,
|
|
|
|
t if t == "dir" => Self::DIR,
|
|
|
|
t if t == "block" => Self::BLOCK,
|
|
|
|
t if t == "char" => Self::CHAR,
|
|
|
|
t if t == "fifo" => Self::FIFO,
|
|
|
|
t if t == "socket" => Self::SOCK,
|
|
|
|
t if t == "link" => Self::LINK,
|
|
|
|
_ => return Err(()),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(flag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bitflags! {
|
2023-09-15 17:56:03 +08:00
|
|
|
#[derive(Copy, Clone, Default)]
|
2023-07-29 19:28:02 +08:00
|
|
|
pub struct PermFlags: u32 {
|
|
|
|
const READ = 1 << 0;
|
|
|
|
const WRITE = 1 << 1;
|
|
|
|
const EXEC = 1 << 2;
|
|
|
|
const SUID = 1 << 3;
|
|
|
|
const SGID = 1 << 4;
|
|
|
|
const USER = 1 << 5;
|
|
|
|
const GROUP = 1 << 6;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PermFlags {
|
|
|
|
fn is_special(self) -> bool {
|
|
|
|
self.intersects(Self::SUID | Self::SGID | Self::USER | Self::GROUP)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<&wstr> for PermFlags {
|
|
|
|
type Error = ();
|
|
|
|
|
|
|
|
fn try_from(value: &wstr) -> Result<Self, Self::Error> {
|
|
|
|
let flag = match value {
|
|
|
|
t if t == "read" => Self::READ,
|
|
|
|
t if t == "write" => Self::WRITE,
|
|
|
|
t if t == "exec" => Self::EXEC,
|
|
|
|
t if t == "suid" => Self::SUID,
|
|
|
|
t if t == "sgid" => Self::SGID,
|
|
|
|
t if t == "user" => Self::USER,
|
|
|
|
t if t == "group" => Self::GROUP,
|
|
|
|
_ => return Err(()),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(flag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
#[derive(Default)]
|
|
|
|
struct Options<'args> {
|
|
|
|
null_in: bool,
|
|
|
|
null_out: bool,
|
|
|
|
quiet: bool,
|
|
|
|
|
|
|
|
invert_valid: bool,
|
|
|
|
invert: bool,
|
|
|
|
|
|
|
|
relative_valid: bool,
|
|
|
|
relative: bool,
|
|
|
|
|
|
|
|
reverse_valid: bool,
|
|
|
|
reverse: bool,
|
|
|
|
|
|
|
|
unique_valid: bool,
|
|
|
|
unique: bool,
|
|
|
|
|
|
|
|
key: Option<&'args wstr>,
|
|
|
|
|
|
|
|
types_valid: bool,
|
|
|
|
types: Option<TypeFlags>,
|
|
|
|
|
|
|
|
perms_valid: bool,
|
|
|
|
perms: Option<PermFlags>,
|
|
|
|
|
|
|
|
arg1: Option<&'args wstr>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
2023-10-09 05:10:05 +08:00
|
|
|
fn path_out(streams: &mut IoStreams, opts: &Options<'_>, s: impl AsRef<wstr>) {
|
2023-07-29 19:28:02 +08:00
|
|
|
let s = s.as_ref();
|
|
|
|
if !opts.quiet {
|
|
|
|
if !opts.null_out {
|
|
|
|
streams
|
|
|
|
.out
|
2023-10-09 05:22:27 +08:00
|
|
|
.append_with_separation(s, SeparationType::explicitly, true);
|
2023-07-29 19:28:02 +08:00
|
|
|
} else {
|
|
|
|
let mut output = WString::with_capacity(s.len() + 1);
|
|
|
|
output.push_utfstr(s);
|
|
|
|
output.push('\0');
|
|
|
|
streams.out.append(output);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn construct_short_opts(opts: &Options) -> WString {
|
|
|
|
// All commands accept -z, -Z and -q
|
|
|
|
let mut short_opts = WString::from(":zZq");
|
|
|
|
if opts.perms_valid {
|
|
|
|
short_opts += L!("p:");
|
|
|
|
short_opts += L!("rwx");
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.types_valid {
|
|
|
|
short_opts += L!("t:");
|
|
|
|
short_opts += L!("fld");
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.invert_valid {
|
|
|
|
short_opts.push('v');
|
|
|
|
}
|
|
|
|
if opts.relative_valid {
|
|
|
|
short_opts.push('R');
|
|
|
|
}
|
|
|
|
if opts.reverse_valid {
|
|
|
|
short_opts.push('r');
|
|
|
|
}
|
|
|
|
if opts.unique_valid {
|
|
|
|
short_opts.push('u');
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
const LONG_OPTIONS: [woption<'static>; 10] = [
|
|
|
|
wopt(L!("quiet"), no_argument, 'q'),
|
|
|
|
wopt(L!("null-in"), no_argument, 'z'),
|
|
|
|
wopt(L!("null-out"), no_argument, 'Z'),
|
|
|
|
wopt(L!("perm"), required_argument, 'p'),
|
|
|
|
wopt(L!("type"), required_argument, 't'),
|
|
|
|
wopt(L!("invert"), no_argument, 'v'),
|
|
|
|
wopt(L!("relative"), no_argument, 'R'),
|
|
|
|
wopt(L!("reverse"), no_argument, 'r'),
|
|
|
|
wopt(L!("unique"), no_argument, 'u'),
|
|
|
|
wopt(L!("key"), required_argument, NONOPTION_CHAR_CODE),
|
|
|
|
];
|
|
|
|
|
|
|
|
fn parse_opts<'args>(
|
|
|
|
opts: &mut Options<'args>,
|
|
|
|
optind: &mut usize,
|
|
|
|
n_req_args: usize,
|
|
|
|
args: &mut [&'args wstr],
|
2023-10-09 05:22:27 +08:00
|
|
|
parser: &Parser,
|
2023-10-09 05:10:05 +08:00
|
|
|
streams: &mut IoStreams,
|
2023-07-29 19:28:02 +08:00
|
|
|
) -> Option<c_int> {
|
|
|
|
let cmd = args[0];
|
|
|
|
let mut args_read = Vec::with_capacity(args.len());
|
|
|
|
args_read.extend_from_slice(args);
|
|
|
|
|
|
|
|
let short_opts = construct_short_opts(opts);
|
|
|
|
|
|
|
|
let mut w = wgetopter_t::new(&short_opts, &LONG_OPTIONS, args);
|
|
|
|
while let Some(c) = w.wgetopt_long() {
|
|
|
|
match c {
|
|
|
|
':' => {
|
|
|
|
streams.err.append(L!("path ")); // clone of string_error
|
|
|
|
builtin_missing_argument(parser, streams, cmd, args_read[w.woptind - 1], false);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
'?' => {
|
|
|
|
path_unknown_option(parser, streams, cmd, args_read[w.woptind - 1]);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
'q' => {
|
|
|
|
opts.quiet = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'z' => {
|
|
|
|
opts.null_in = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'Z' => {
|
|
|
|
opts.null_out = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'v' if opts.invert_valid => {
|
|
|
|
opts.invert = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
't' if opts.types_valid => {
|
|
|
|
let types = opts.types.get_or_insert_with(TypeFlags::default);
|
|
|
|
let types_args = split_string_tok(w.woptarg.unwrap(), L!(","), None);
|
|
|
|
for t in types_args {
|
|
|
|
let Ok(r#type) = t.try_into() else {
|
|
|
|
path_error!(streams, "%ls: Invalid type '%ls'\n", "path", t);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
};
|
|
|
|
*types |= r#type;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'p' if opts.perms_valid => {
|
|
|
|
let perms = opts.perms.get_or_insert_with(PermFlags::default);
|
|
|
|
let perms_args = split_string_tok(w.woptarg.unwrap(), L!(","), None);
|
|
|
|
for p in perms_args {
|
|
|
|
let Ok(perm) = p.try_into() else {
|
|
|
|
path_error!(streams, "%ls: Invalid permission '%ls'\n", "path", p);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
};
|
|
|
|
*perms |= perm;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'r' if opts.reverse_valid => {
|
|
|
|
opts.reverse = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'r' if opts.perms_valid => {
|
|
|
|
let perms = opts.perms.get_or_insert_with(PermFlags::default);
|
|
|
|
*perms |= PermFlags::READ;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'w' if opts.perms_valid => {
|
|
|
|
let perms = opts.perms.get_or_insert_with(PermFlags::default);
|
|
|
|
*perms |= PermFlags::WRITE;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'x' if opts.perms_valid => {
|
|
|
|
let perms = opts.perms.get_or_insert_with(PermFlags::default);
|
|
|
|
*perms |= PermFlags::EXEC;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'f' if opts.types_valid => {
|
|
|
|
let types = opts.types.get_or_insert_with(TypeFlags::default);
|
|
|
|
*types |= TypeFlags::FILE;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'l' if opts.types_valid => {
|
|
|
|
let types = opts.types.get_or_insert_with(TypeFlags::default);
|
|
|
|
*types |= TypeFlags::LINK;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'd' if opts.types_valid => {
|
|
|
|
let types = opts.types.get_or_insert_with(TypeFlags::default);
|
|
|
|
*types |= TypeFlags::DIR;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'u' if opts.unique_valid => {
|
|
|
|
opts.unique = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
'R' if opts.relative_valid => {
|
|
|
|
opts.relative = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
NONOPTION_CHAR_CODE => {
|
|
|
|
assert!(w.woptarg.is_some());
|
|
|
|
opts.key = w.woptarg;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
path_unknown_option(parser, streams, cmd, args_read[w.woptind - 1]);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
*optind = w.woptind;
|
|
|
|
|
|
|
|
if n_req_args != 0 {
|
|
|
|
assert!(n_req_args == 1);
|
|
|
|
opts.arg1 = args.get(*optind).copied();
|
|
|
|
if opts.arg1.is_some() {
|
|
|
|
*optind += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.arg1.is_none() && 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.
|
2023-10-09 05:22:27 +08:00
|
|
|
if streams.stdin_is_directly_redirected && args.len() > *optind {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATUS_CMD_OK
|
|
|
|
}
|
|
|
|
|
|
|
|
fn path_transform(
|
2023-10-09 05:22:27 +08:00
|
|
|
parser: &Parser,
|
2023-10-09 05:10:05 +08:00
|
|
|
streams: &mut IoStreams,
|
2023-07-29 19:28:02 +08:00
|
|
|
args: &mut [&wstr],
|
|
|
|
func: impl Fn(&wstr) -> WString,
|
|
|
|
) -> Option<c_int> {
|
|
|
|
let mut opts = Options::default();
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
|
|
|
|
if retval != STATUS_CMD_OK {
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut n_transformed = 0;
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
|
|
|
for (arg, _) in arguments {
|
|
|
|
// Empty paths make no sense, but e.g. wbasename returns true for them.
|
|
|
|
if arg.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let transformed = func(&arg);
|
|
|
|
if transformed != arg {
|
|
|
|
n_transformed += 1;
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
if n_transformed > 0 {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_basename(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_transform(parser, streams, args, |s| wbasename(s).to_owned())
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_dirname(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_transform(parser, streams, args, |s| wdirname(s).to_owned())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn normalize_help(path: &wstr) -> WString {
|
|
|
|
let mut np = normalize_path(path, false);
|
|
|
|
if !np.is_empty() && np.char_at(0) == '-' {
|
|
|
|
np = "./".chars().chain(np.chars()).collect();
|
|
|
|
}
|
|
|
|
np
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_normalize(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_transform(parser, streams, args, normalize_help)
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_mtime(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
let mut opts = Options::default();
|
|
|
|
opts.relative_valid = true;
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
|
|
|
|
if retval != STATUS_CMD_OK {
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut n_transformed = 0;
|
|
|
|
|
|
|
|
let t = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
|
|
|
Ok(dur) => dur.as_secs() as i64,
|
|
|
|
Err(err) => -(err.duration().as_secs() as i64),
|
|
|
|
};
|
|
|
|
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
|
|
|
for (arg, _) in arguments {
|
|
|
|
let ret = file_id_for_path(&arg);
|
|
|
|
|
|
|
|
if ret != INVALID_FILE_ID {
|
|
|
|
if opts.quiet {
|
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
n_transformed += 1;
|
|
|
|
if !opts.relative {
|
|
|
|
path_out(streams, &opts, (ret.mod_seconds).to_wstring());
|
|
|
|
} else {
|
|
|
|
// note that the mod time can actually be before the system time
|
|
|
|
// so this can end up negative
|
|
|
|
#[allow(clippy::unnecessary_cast)]
|
|
|
|
path_out(streams, &opts, (t - ret.mod_seconds as i64).to_wstring());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if n_transformed > 0 {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn find_extension(path: &wstr) -> Option<usize> {
|
|
|
|
// 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.
|
|
|
|
let filename = wbasename(path);
|
|
|
|
|
|
|
|
// "." and ".." aren't really *files* and therefore don't have an extension.
|
|
|
|
if filename == "." || filename == ".." {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we don't have a "." or the "." is the first in the filename,
|
|
|
|
// we do not have an extension
|
|
|
|
let pos = filename.chars().rposition(|c| c == '.');
|
|
|
|
match pos {
|
|
|
|
None | Some(0) => None,
|
|
|
|
// Convert pos back to what it would be in the original path.
|
|
|
|
Some(pos) => Some(pos + path.len() - filename.len()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_find_extension() {
|
|
|
|
let cases = [
|
|
|
|
(L!("foo.wmv"), Some(3)),
|
|
|
|
(L!("verylongfilename.wmv"), Some("verylongfilename".len())),
|
|
|
|
(L!("foo"), None),
|
|
|
|
(L!(".foo"), None),
|
|
|
|
(L!("./foo.wmv"), Some(5)),
|
|
|
|
];
|
|
|
|
|
|
|
|
for (f, ext_idx) in cases {
|
|
|
|
assert_eq!(find_extension(f), ext_idx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_extension(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
let mut opts = Options::default();
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
|
|
|
|
if retval != STATUS_CMD_OK {
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut n_transformed = 0;
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
|
|
|
for (arg, _) in arguments {
|
|
|
|
let pos = find_extension(&arg);
|
|
|
|
let Some(pos) = pos else {
|
|
|
|
// If there is no extension the extension is empty.
|
|
|
|
// This is unambiguous because we include the ".".
|
|
|
|
path_out(streams, &opts, L!(""));
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
let ext = arg.slice_from(pos);
|
|
|
|
if opts.quiet && !ext.is_empty() {
|
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
path_out(streams, &opts, ext);
|
|
|
|
n_transformed += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if n_transformed > 0 {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn path_change_extension(
|
2023-10-09 05:22:27 +08:00
|
|
|
parser: &Parser,
|
2023-10-09 05:10:05 +08:00
|
|
|
streams: &mut IoStreams,
|
2023-07-29 19:28:02 +08:00
|
|
|
args: &mut [&wstr],
|
|
|
|
) -> Option<c_int> {
|
|
|
|
let mut opts = Options::default();
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 1, args, parser, streams);
|
|
|
|
if retval != STATUS_CMD_OK {
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut n_transformed = 0usize;
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
|
|
|
for (mut arg, _) in arguments {
|
|
|
|
let pos = find_extension(&arg);
|
|
|
|
let mut ext = match pos {
|
|
|
|
Some(pos) => {
|
|
|
|
arg.to_mut().truncate(pos);
|
|
|
|
arg.into_owned()
|
|
|
|
}
|
|
|
|
None => arg.into_owned(),
|
|
|
|
};
|
|
|
|
|
|
|
|
// Only add on the extension "." if we have something.
|
|
|
|
// That way specifying an empty extension strips it.
|
|
|
|
if let Some(replacement) = opts.arg1 {
|
|
|
|
if !replacement.is_empty() {
|
|
|
|
if replacement.char_at(0) != '.' {
|
|
|
|
ext.push('.');
|
|
|
|
}
|
|
|
|
ext.push_utfstr(replacement);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
path_out(streams, &opts, ext);
|
|
|
|
|
|
|
|
n_transformed += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if n_transformed > 0 {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_resolve(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
let mut opts = Options::default();
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
|
|
|
|
if retval != STATUS_CMD_OK {
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut n_transformed = 0usize;
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
|
|
|
for (arg, _) in arguments {
|
|
|
|
let mut real = match wrealpath(&arg) {
|
|
|
|
Some(p) => p,
|
|
|
|
None => {
|
|
|
|
// The path doesn't exist, isn't readable or a symlink loop.
|
|
|
|
// We go up until we find something that works.
|
|
|
|
let mut next = arg.into_owned();
|
|
|
|
// First add $PWD if we're relative
|
|
|
|
if !next.is_empty() && next.char_at(0) != '/' {
|
2023-10-09 05:22:27 +08:00
|
|
|
next = path_apply_working_directory(&next, &parser.vars().get_pwd_slash());
|
2023-07-29 19:28:02 +08:00
|
|
|
}
|
|
|
|
let mut rest = wbasename(&next).to_owned();
|
|
|
|
let mut real = None;
|
|
|
|
while !next.is_empty() && next != "/" {
|
|
|
|
next = wdirname(&next).to_owned();
|
|
|
|
real = wrealpath(&next);
|
|
|
|
if let Some(ref mut real) = real {
|
|
|
|
real.push('/');
|
|
|
|
real.push_utfstr(&rest);
|
|
|
|
*real = normalize_path(real, false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
rest = (wbasename(&next).to_owned() + L!("/")) + rest.as_utfstr();
|
|
|
|
}
|
|
|
|
|
|
|
|
match real {
|
|
|
|
Some(p) => p,
|
|
|
|
None => 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 += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if n_transformed > 0 {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_sort(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
let mut opts = Options::default();
|
|
|
|
opts.reverse_valid = true;
|
|
|
|
opts.unique_valid = true;
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams);
|
|
|
|
if retval != STATUS_CMD_OK {
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
let keyfunc: &dyn Fn(&wstr) -> &wstr = match &opts.key {
|
|
|
|
Some(k) if k == "basename" => &wbasename as _,
|
|
|
|
Some(k) if k == "dirname" => &wdirname as _,
|
|
|
|
Some(k) if k == "path" => {
|
|
|
|
// Act as if --key hadn't been given.
|
|
|
|
opts.key = None;
|
|
|
|
&wbasename as _
|
|
|
|
}
|
|
|
|
None => &wbasename as _,
|
|
|
|
Some(k) => {
|
|
|
|
path_error!(streams, "%ls: Invalid sort key '%ls'\n", args[0], k);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
|
|
|
|
2023-07-31 18:58:17 +08:00
|
|
|
let mut list: Vec<_> = arguments.map(|(f, _)| f).collect();
|
2023-07-29 19:28:02 +08:00
|
|
|
|
|
|
|
if opts.key.is_some() {
|
|
|
|
// We use a stable sort here
|
|
|
|
list.sort_by(|a, b| {
|
2023-07-31 18:58:17 +08:00
|
|
|
match wcsfilecmp_glob(keyfunc(a), keyfunc(b)) {
|
2023-07-29 19:28:02 +08:00
|
|
|
// to avoid changing the order so we can chain calls
|
|
|
|
Ordering::Equal => Ordering::Greater,
|
|
|
|
order if opts.reverse => order.reverse(),
|
|
|
|
order => order,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if opts.unique {
|
|
|
|
// we are sorted, dedup will remove all duplicates
|
2023-07-31 18:58:17 +08:00
|
|
|
list.dedup_by(|a, b| keyfunc(a) == keyfunc(b));
|
2023-07-29 19:28:02 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Without --key, we just sort by the entire path,
|
|
|
|
// so we have no need to transform and such.
|
|
|
|
list.sort_by(|a, b| {
|
|
|
|
match wcsfilecmp_glob(a, b) {
|
|
|
|
// to avoid changing the order so we can chain calls
|
|
|
|
Ordering::Equal => Ordering::Greater,
|
|
|
|
order if opts.reverse => order.reverse(),
|
|
|
|
order => order,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if opts.unique {
|
|
|
|
// we are sorted, dedup will remove all duplicates
|
|
|
|
list.dedup();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for entry in list {
|
2023-07-31 18:58:17 +08:00
|
|
|
path_out(streams, &opts, &entry);
|
2023-07-29 19:28:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/* TODO: Return true only if already sorted? */
|
|
|
|
STATUS_CMD_OK
|
|
|
|
}
|
|
|
|
|
2023-11-10 02:28:43 +08:00
|
|
|
fn filter_path(opts: &Options, path: &wstr, uid: Option<u32>, gid: Option<u32>) -> bool {
|
2023-07-29 19:28:02 +08:00
|
|
|
// TODO: Add moar stuff:
|
|
|
|
// fifos, sockets, size greater than zero, setuid, ...
|
|
|
|
// Nothing to check, file existence is checked elsewhere.
|
|
|
|
if opts.types.is_none() && opts.perms.is_none() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-11-15 00:49:38 +08:00
|
|
|
// We keep the metadata around for other checks if we have it.
|
|
|
|
let mut metadata: Option<Metadata> = None;
|
|
|
|
|
2023-07-29 19:28:02 +08:00
|
|
|
if let Some(t) = opts.types {
|
|
|
|
let mut type_ok = false;
|
|
|
|
if t.contains(TypeFlags::LINK) {
|
|
|
|
let md = lwstat(path);
|
2023-07-29 19:37:33 +08:00
|
|
|
type_ok = md.is_ok() && md.unwrap().is_symlink();
|
2023-07-29 19:28:02 +08:00
|
|
|
}
|
2023-07-29 19:37:33 +08:00
|
|
|
let Ok(md) = wstat(path) else {
|
2023-07-29 19:28:02 +08:00
|
|
|
// Does not exist
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
let ft = md.file_type();
|
|
|
|
type_ok = match type_ok {
|
|
|
|
true => true,
|
|
|
|
_ if t.contains(TypeFlags::FILE) && ft.is_file() => true,
|
|
|
|
_ if t.contains(TypeFlags::DIR) && ft.is_dir() => true,
|
|
|
|
_ if t.contains(TypeFlags::BLOCK) && ft.is_block_device() => true,
|
|
|
|
_ if t.contains(TypeFlags::CHAR) && ft.is_char_device() => true,
|
|
|
|
_ if t.contains(TypeFlags::FIFO) && ft.is_fifo() => true,
|
|
|
|
_ if t.contains(TypeFlags::SOCK) && ft.is_socket() => true,
|
|
|
|
_ => false,
|
|
|
|
};
|
|
|
|
|
|
|
|
if !type_ok {
|
|
|
|
return false;
|
|
|
|
}
|
2023-11-15 00:49:38 +08:00
|
|
|
metadata = Some(md);
|
2023-07-29 19:28:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(perm) = opts.perms {
|
|
|
|
let mut amode = 0;
|
|
|
|
// TODO: Update bitflags so this works
|
|
|
|
/*
|
|
|
|
for f in perm {
|
|
|
|
amode |= match f {
|
|
|
|
PermFlags::READ => R_OK,
|
|
|
|
PermFlags::WRITE => W_OK,
|
|
|
|
PermFlags::EXEC => X_OK,
|
|
|
|
_ => PermFlags::empty(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
if perm.contains(PermFlags::READ) {
|
|
|
|
amode |= R_OK;
|
|
|
|
}
|
|
|
|
if perm.contains(PermFlags::WRITE) {
|
|
|
|
amode |= W_OK;
|
|
|
|
}
|
|
|
|
if perm.contains(PermFlags::EXEC) {
|
|
|
|
amode |= X_OK;
|
|
|
|
}
|
|
|
|
// access returns 0 on success,
|
|
|
|
// -1 on failure. Yes, C can't even keep its bools straight.
|
2023-11-10 02:28:43 +08:00
|
|
|
// Skip this if we don't have a mode to check - the stat can do existence too.
|
2023-11-15 00:49:38 +08:00
|
|
|
// It's tempting to check metadata here if we have it,
|
|
|
|
// e.g. see if any read-bit is set for READ.
|
|
|
|
// That won't work for root.
|
2023-11-10 02:28:43 +08:00
|
|
|
if amode != 0 && waccess(path, amode) != 0 {
|
2023-07-29 19:28:02 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Permissions that require special handling
|
|
|
|
if perm.is_special() {
|
2023-11-15 00:49:38 +08:00
|
|
|
let md = match metadata {
|
|
|
|
Some(n) => n,
|
|
|
|
_ => {
|
|
|
|
let Ok(md) = wstat(path) else {
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
md
|
|
|
|
}
|
2023-07-29 19:28:02 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
#[allow(clippy::if_same_then_else)]
|
|
|
|
if perm.contains(PermFlags::SUID) && (md.mode() as mode_t & S_ISUID) == 0 {
|
|
|
|
return false;
|
|
|
|
} else if perm.contains(PermFlags::SGID) && (md.mode() as mode_t & S_ISGID) == 0 {
|
|
|
|
return false;
|
2023-11-10 02:28:43 +08:00
|
|
|
} else if perm.contains(PermFlags::USER) && uid != Some(md.uid()) {
|
2023-07-29 19:28:02 +08:00
|
|
|
return false;
|
2023-11-10 02:28:43 +08:00
|
|
|
} else if perm.contains(PermFlags::GROUP) && gid != Some(md.gid()) {
|
2023-07-29 19:28:02 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No filters failed.
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
fn path_filter_maybe_is(
|
2023-10-09 05:22:27 +08:00
|
|
|
parser: &Parser,
|
2023-10-09 05:10:05 +08:00
|
|
|
streams: &mut IoStreams,
|
2023-07-29 19:28:02 +08:00
|
|
|
args: &mut [&wstr],
|
|
|
|
is_is: bool,
|
|
|
|
) -> Option<c_int> {
|
|
|
|
let mut opts = Options::default();
|
|
|
|
opts.types_valid = true;
|
|
|
|
opts.perms_valid = true;
|
|
|
|
opts.invert_valid = true;
|
|
|
|
let mut optind = 0;
|
|
|
|
let retval = parse_opts(&mut opts, &mut optind, 0, args, 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut n_transformed = 0;
|
|
|
|
let arguments = arguments(args, &mut optind, streams).with_split_behavior(match opts.null_in {
|
|
|
|
true => SplitBehavior::Null,
|
|
|
|
false => SplitBehavior::InferNull,
|
|
|
|
});
|
2023-11-10 02:28:43 +08:00
|
|
|
|
|
|
|
// If we're looking for the owner/group, get our euid/egid here once.
|
|
|
|
let uid = if opts.perms.unwrap_or_default().contains(PermFlags::USER) {
|
|
|
|
Some(geteuid())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
let gid = if opts.perms.unwrap_or_default().contains(PermFlags::GROUP) {
|
|
|
|
Some(getegid())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
2023-07-29 19:28:02 +08:00
|
|
|
for (arg, _) in arguments.filter(|(f, _)| {
|
2023-11-10 02:28:43 +08:00
|
|
|
(opts.perms.is_none() && opts.types.is_none())
|
|
|
|
|| (filter_path(&opts, f, uid, gid) != opts.invert)
|
2023-07-29 19:28:02 +08:00
|
|
|
}) {
|
|
|
|
// If we don't have filters, check if it exists.
|
|
|
|
if opts.perms.is_none() && opts.types.is_none() {
|
|
|
|
let ok = waccess(&arg, F_OK) == 0;
|
|
|
|
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 `./`.
|
|
|
|
// Empty paths make no sense, but e.g. wbasename returns true for them.
|
|
|
|
if !arg.is_empty() && arg.starts_with('-') {
|
|
|
|
let out = WString::from("./") + arg.as_ref();
|
|
|
|
path_out(streams, &opts, out);
|
|
|
|
} else {
|
|
|
|
path_out(streams, &opts, arg);
|
|
|
|
}
|
|
|
|
n_transformed += 1;
|
|
|
|
if opts.quiet {
|
|
|
|
return STATUS_CMD_OK;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if n_transformed > 0 {
|
|
|
|
STATUS_CMD_OK
|
|
|
|
} else {
|
|
|
|
STATUS_CMD_ERROR
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_filter(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_filter_maybe_is(parser, streams, args, false)
|
|
|
|
}
|
|
|
|
|
2023-10-09 05:22:27 +08:00
|
|
|
fn path_is(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
path_filter_maybe_is(parser, streams, args, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The path builtin, for handling paths.
|
2023-10-09 05:22:27 +08:00
|
|
|
pub fn path(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
2023-07-29 19:28:02 +08:00
|
|
|
let cmd = args[0];
|
|
|
|
let argc = args.len();
|
|
|
|
|
|
|
|
if argc <= 1 {
|
|
|
|
streams
|
|
|
|
.err
|
|
|
|
.append(wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd));
|
2023-10-09 05:22:27 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2023-07-29 19:28:02 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if args[1] == "-h" || args[1] == "--help" {
|
|
|
|
builtin_print_help(parser, streams, cmd);
|
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
let subcmd_name = args[1];
|
|
|
|
|
|
|
|
let subcmd: BuiltinCmd = match subcmd_name.to_string().as_str() {
|
|
|
|
"basename" => path_basename,
|
|
|
|
"change-extension" => path_change_extension,
|
|
|
|
"dirname" => path_dirname,
|
|
|
|
"extension" => path_extension,
|
|
|
|
"filter" => path_filter,
|
|
|
|
"is" => path_is,
|
|
|
|
"mtime" => path_mtime,
|
|
|
|
"normalize" => path_normalize,
|
|
|
|
"resolve" => path_resolve,
|
|
|
|
"sort" => path_sort,
|
|
|
|
_ => {
|
|
|
|
streams
|
|
|
|
.err
|
|
|
|
.append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name));
|
2023-10-09 05:22:27 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2023-07-29 19:28:02 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if argc >= 3 && (args[2] == "-h" || args[2] == "--help") {
|
|
|
|
// Unlike string, we don't have separate docs (yet)
|
|
|
|
builtin_print_help(parser, streams, cmd);
|
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
let args = &mut args[1..];
|
|
|
|
return subcmd(parser, streams, args);
|
|
|
|
}
|