mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-02-09 15:16:16 +08:00
954 lines
31 KiB
Rust
954 lines
31 KiB
Rust
use super::prelude::*;
|
|
use crate::builtins::*;
|
|
use crate::common::{escape, get_by_sorted_name, str2wcstring, Named};
|
|
use crate::io::{IoFd, OutputStream};
|
|
use crate::parse_constants::UNKNOWN_BUILTIN_ERR_MSG;
|
|
use crate::parse_util::parse_util_argument_is_help;
|
|
use crate::parser::{Block, BlockType, LoopStatus};
|
|
use crate::proc::{no_exec, ProcStatus};
|
|
use crate::reader::reader_read;
|
|
use crate::wchar::{wstr, WString, L};
|
|
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
|
|
use errno::errno;
|
|
use libc::{c_int, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
|
|
|
|
use std::borrow::Cow;
|
|
use std::fs::File;
|
|
use std::io::{BufRead, BufReader, Read};
|
|
use std::os::fd::FromRawFd;
|
|
use std::sync::Arc;
|
|
|
|
pub type BuiltinCmd = fn(&Parser, &mut IoStreams, &mut [&wstr]) -> Option<c_int>;
|
|
|
|
/// The default prompt for the read command.
|
|
pub const DEFAULT_READ_PROMPT: &wstr =
|
|
L!("set_color green; echo -n read; set_color normal; echo -n \"> \"");
|
|
|
|
/// Error message on missing argument.
|
|
pub const BUILTIN_ERR_MISSING: &str = "%ls: %ls: option requires an argument\n";
|
|
|
|
/// Error message on missing man page.
|
|
pub const BUILTIN_ERR_MISSING_HELP: &str = concat!(
|
|
"fish: %ls: missing man page\nDocumentation may not be installed.\n`help %ls` will ",
|
|
"show an online version\n"
|
|
);
|
|
|
|
/// Error message on multiple scope levels for variables.
|
|
pub const BUILTIN_ERR_GLOCAL: &str =
|
|
"%ls: scope can be only one of: universal function global local\n";
|
|
|
|
/// Error message for specifying both export and unexport to set/read.
|
|
pub const BUILTIN_ERR_EXPUNEXP: &str = "%ls: cannot both export and unexport\n";
|
|
|
|
/// Error message for specifying both path and unpath to set/read.
|
|
pub const BUILTIN_ERR_PATHUNPATH: &str = "%ls: cannot both path and unpath\n";
|
|
|
|
/// Error message for unknown switch.
|
|
pub const BUILTIN_ERR_UNKNOWN: &str = "%ls: %ls: unknown option\n";
|
|
|
|
/// Error message for invalid bind mode name.
|
|
pub const BUILTIN_ERR_BIND_MODE: &str = "%ls: %ls: invalid mode name. See `help identifiers`\n";
|
|
|
|
/// Error message when too many arguments are supplied to a builtin.
|
|
pub const BUILTIN_ERR_TOO_MANY_ARGUMENTS: &str = "%ls: too many arguments\n";
|
|
|
|
/// Error message when integer expected
|
|
pub const BUILTIN_ERR_NOT_NUMBER: &str = "%ls: %ls: invalid integer\n";
|
|
|
|
/// Command that requires a subcommand was invoked without a recognized subcommand.
|
|
pub const BUILTIN_ERR_MISSING_SUBCMD: &str = "%ls: missing subcommand\n";
|
|
pub const BUILTIN_ERR_INVALID_SUBCMD: &str = "%ls: %ls: invalid subcommand\n";
|
|
|
|
/// Error messages for unexpected args.
|
|
pub const BUILTIN_ERR_ARG_COUNT0: &str = "%ls: missing argument\n";
|
|
pub const BUILTIN_ERR_ARG_COUNT1: &str = "%ls: expected %d arguments; got %d\n";
|
|
pub const BUILTIN_ERR_ARG_COUNT2: &str = "%ls: %ls: expected %d arguments; got %d\n";
|
|
pub const BUILTIN_ERR_MIN_ARG_COUNT1: &str = "%ls: expected >= %d arguments; got %d\n";
|
|
pub const BUILTIN_ERR_MAX_ARG_COUNT1: &str = "%ls: expected <= %d arguments; got %d\n";
|
|
|
|
/// Error message for invalid variable name.
|
|
pub const BUILTIN_ERR_VARNAME: &str = "%ls: %ls: invalid variable name. See `help identifiers`\n";
|
|
|
|
/// Error message on invalid combination of options.
|
|
pub const BUILTIN_ERR_COMBO: &str = "%ls: invalid option combination\n";
|
|
pub const BUILTIN_ERR_COMBO2: &str = "%ls: invalid option combination, %ls\n";
|
|
pub const BUILTIN_ERR_COMBO2_EXCLUSIVE: &str = "%ls: %ls %ls: options cannot be used together\n";
|
|
|
|
/// The send stuff to foreground message.
|
|
pub const FG_MSG: &str = "Send job %d (%ls) to foreground\n";
|
|
|
|
// Return values (`$status` values for fish scripts) for various situations.
|
|
|
|
/// The status code used for normal exit in a command.
|
|
pub const STATUS_CMD_OK: Option<c_int> = Some(0);
|
|
/// The status code used for failure exit in a command (but not if the args were invalid).
|
|
pub const STATUS_CMD_ERROR: Option<c_int> = Some(1);
|
|
/// The status code used for invalid arguments given to a command. This is distinct from valid
|
|
/// arguments that might result in a command failure. An invalid args condition is something
|
|
/// like an unrecognized flag, missing or too many arguments, an invalid integer, etc.
|
|
pub const STATUS_INVALID_ARGS: Option<c_int> = Some(2);
|
|
|
|
/// The status code used when a command was not found.
|
|
pub const STATUS_CMD_UNKNOWN: Option<c_int> = Some(127);
|
|
|
|
/// The status code used when an external command can not be run.
|
|
pub const STATUS_NOT_EXECUTABLE: Option<c_int> = Some(126);
|
|
|
|
/// The status code used when a wildcard had no matches.
|
|
pub const STATUS_UNMATCHED_WILDCARD: Option<c_int> = Some(124);
|
|
/// The status code used when illegal command name is encountered.
|
|
pub const STATUS_ILLEGAL_CMD: Option<c_int> = Some(123);
|
|
/// The status code used when `read` is asked to consume too much data.
|
|
pub const STATUS_READ_TOO_MUCH: Option<c_int> = Some(122);
|
|
/// The status code when an expansion fails, for example, "$foo["
|
|
pub const STATUS_EXPAND_ERROR: Option<c_int> = Some(121);
|
|
|
|
/// Data structure to describe a builtin.
|
|
struct BuiltinData {
|
|
// Name of the builtin.
|
|
name: &'static wstr,
|
|
// Function pointer to the builtin implementation.
|
|
func: BuiltinCmd,
|
|
}
|
|
|
|
// Data about all the builtin commands in fish.
|
|
// Functions that are bound to builtin_generic are handled directly by the parser.
|
|
// NOTE: These must be kept in sorted order!
|
|
const BUILTIN_DATAS: &[BuiltinData] = &[
|
|
BuiltinData {
|
|
name: L!("."),
|
|
func: source::source,
|
|
},
|
|
BuiltinData {
|
|
name: L!(":"),
|
|
func: builtin_true,
|
|
},
|
|
BuiltinData {
|
|
name: L!("["), // ]
|
|
func: test::test,
|
|
},
|
|
BuiltinData {
|
|
name: L!("_"),
|
|
func: builtin_gettext,
|
|
},
|
|
BuiltinData {
|
|
name: L!("abbr"),
|
|
func: abbr::abbr,
|
|
},
|
|
BuiltinData {
|
|
name: L!("and"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("argparse"),
|
|
func: argparse::argparse,
|
|
},
|
|
BuiltinData {
|
|
name: L!("begin"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("bg"),
|
|
func: bg::bg,
|
|
},
|
|
BuiltinData {
|
|
name: L!("bind"),
|
|
func: bind::bind,
|
|
},
|
|
BuiltinData {
|
|
name: L!("block"),
|
|
func: block::block,
|
|
},
|
|
BuiltinData {
|
|
name: L!("break"),
|
|
func: builtin_break_continue,
|
|
},
|
|
BuiltinData {
|
|
name: L!("breakpoint"),
|
|
func: builtin_breakpoint,
|
|
},
|
|
BuiltinData {
|
|
name: L!("builtin"),
|
|
func: builtin::builtin,
|
|
},
|
|
BuiltinData {
|
|
name: L!("case"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("cd"),
|
|
func: cd::cd,
|
|
},
|
|
BuiltinData {
|
|
name: L!("command"),
|
|
func: command::command,
|
|
},
|
|
BuiltinData {
|
|
name: L!("commandline"),
|
|
func: commandline::commandline,
|
|
},
|
|
BuiltinData {
|
|
name: L!("complete"),
|
|
func: complete::complete,
|
|
},
|
|
BuiltinData {
|
|
name: L!("contains"),
|
|
func: contains::contains,
|
|
},
|
|
BuiltinData {
|
|
name: L!("continue"),
|
|
func: builtin_break_continue,
|
|
},
|
|
BuiltinData {
|
|
name: L!("count"),
|
|
func: count::count,
|
|
},
|
|
BuiltinData {
|
|
name: L!("disown"),
|
|
func: disown::disown,
|
|
},
|
|
BuiltinData {
|
|
name: L!("echo"),
|
|
func: echo::echo,
|
|
},
|
|
BuiltinData {
|
|
name: L!("else"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("emit"),
|
|
func: emit::emit,
|
|
},
|
|
BuiltinData {
|
|
name: L!("end"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("eval"),
|
|
func: eval::eval,
|
|
},
|
|
BuiltinData {
|
|
name: L!("exec"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("exit"),
|
|
func: exit::exit,
|
|
},
|
|
BuiltinData {
|
|
name: L!("false"),
|
|
func: builtin_false,
|
|
},
|
|
BuiltinData {
|
|
name: L!("fg"),
|
|
func: fg::fg,
|
|
},
|
|
BuiltinData {
|
|
name: L!("for"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("function"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("functions"),
|
|
func: functions::functions,
|
|
},
|
|
BuiltinData {
|
|
name: L!("history"),
|
|
func: history::history,
|
|
},
|
|
BuiltinData {
|
|
name: L!("if"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("jobs"),
|
|
func: jobs::jobs,
|
|
},
|
|
BuiltinData {
|
|
name: L!("math"),
|
|
func: math::math,
|
|
},
|
|
BuiltinData {
|
|
name: L!("not"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("or"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("path"),
|
|
func: path::path,
|
|
},
|
|
BuiltinData {
|
|
name: L!("printf"),
|
|
func: printf::printf,
|
|
},
|
|
BuiltinData {
|
|
name: L!("pwd"),
|
|
func: pwd::pwd,
|
|
},
|
|
BuiltinData {
|
|
name: L!("random"),
|
|
func: random::random,
|
|
},
|
|
BuiltinData {
|
|
name: L!("read"),
|
|
func: read::read,
|
|
},
|
|
BuiltinData {
|
|
name: L!("realpath"),
|
|
func: realpath::realpath,
|
|
},
|
|
BuiltinData {
|
|
name: L!("return"),
|
|
func: r#return::r#return,
|
|
},
|
|
BuiltinData {
|
|
name: L!("set"),
|
|
func: set::set,
|
|
},
|
|
BuiltinData {
|
|
name: L!("set_color"),
|
|
func: set_color::set_color,
|
|
},
|
|
BuiltinData {
|
|
name: L!("source"),
|
|
func: source::source,
|
|
},
|
|
BuiltinData {
|
|
name: L!("status"),
|
|
func: status::status,
|
|
},
|
|
BuiltinData {
|
|
name: L!("string"),
|
|
func: string::string,
|
|
},
|
|
BuiltinData {
|
|
name: L!("switch"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("test"),
|
|
func: test::test,
|
|
},
|
|
BuiltinData {
|
|
name: L!("time"),
|
|
func: builtin_generic,
|
|
},
|
|
BuiltinData {
|
|
name: L!("true"),
|
|
func: builtin_true,
|
|
},
|
|
BuiltinData {
|
|
name: L!("type"),
|
|
func: r#type::r#type,
|
|
},
|
|
BuiltinData {
|
|
name: L!("ulimit"),
|
|
func: ulimit::ulimit,
|
|
},
|
|
BuiltinData {
|
|
name: L!("wait"),
|
|
func: wait::wait,
|
|
},
|
|
BuiltinData {
|
|
name: L!("while"),
|
|
func: builtin_generic,
|
|
},
|
|
];
|
|
assert_sorted_by_name!(BUILTIN_DATAS);
|
|
|
|
impl Named for BuiltinData {
|
|
fn name(&self) -> &'static wstr {
|
|
self.name
|
|
}
|
|
}
|
|
|
|
fn builtin_lookup(name: &wstr) -> Option<&'static BuiltinData> {
|
|
get_by_sorted_name(name, BUILTIN_DATAS)
|
|
}
|
|
|
|
/// Is there a builtin command with the given name?
|
|
pub fn builtin_exists(name: &wstr) -> bool {
|
|
builtin_lookup(name).is_some()
|
|
}
|
|
|
|
/// Is the command a keyword we need to special-case the handling of `-h` and `--help`.
|
|
fn cmd_needs_help(cmd: &wstr) -> bool {
|
|
[
|
|
L!("for"),
|
|
L!("while"),
|
|
L!("function"),
|
|
L!("if"),
|
|
L!("end"),
|
|
L!("switch"),
|
|
L!("case"),
|
|
]
|
|
.contains(&cmd)
|
|
}
|
|
|
|
/// Execute a builtin command
|
|
pub fn builtin_run(parser: &Parser, argv: &mut [&wstr], streams: &mut IoStreams) -> ProcStatus {
|
|
if argv.is_empty() {
|
|
return ProcStatus::from_exit_code(STATUS_INVALID_ARGS.unwrap());
|
|
}
|
|
|
|
// We can be handed a keyword by the parser as if it was a command. This happens when the user
|
|
// follows the keyword by `-h` or `--help`. Since it isn't really a builtin command we need to
|
|
// handle displaying help for it here.
|
|
if argv.len() == 2 && parse_util_argument_is_help(argv[1]) && cmd_needs_help(argv[0]) {
|
|
builtin_print_help(parser, streams, argv[0]);
|
|
return ProcStatus::from_exit_code(STATUS_CMD_OK.unwrap());
|
|
}
|
|
|
|
let Some(builtin) = builtin_lookup(argv[0]) else {
|
|
FLOGF!(error, "%s", wgettext_fmt!(UNKNOWN_BUILTIN_ERR_MSG, argv[0]));
|
|
return ProcStatus::from_exit_code(STATUS_CMD_ERROR.unwrap());
|
|
};
|
|
|
|
let builtin_ret = (builtin.func)(parser, streams, argv);
|
|
|
|
// Flush our out and error streams, and check for their errors.
|
|
let out_ret = streams.out.flush_and_check_error();
|
|
let err_ret = streams.err.flush_and_check_error();
|
|
|
|
// Resolve our status code.
|
|
// If the builtin itself produced an error, use that error.
|
|
// Otherwise use any errors from writing to out and writing to err, in that order.
|
|
let mut code = builtin_ret.unwrap_or(0);
|
|
if code == 0 {
|
|
code = out_ret;
|
|
}
|
|
if code == 0 {
|
|
code = err_ret;
|
|
}
|
|
|
|
// The exit code is cast to an 8-bit unsigned integer, so saturate to 255. Otherwise,
|
|
// multiples of 256 are reported as 0.
|
|
if code > 255 {
|
|
code = 255;
|
|
}
|
|
|
|
// Handle the case of an empty status.
|
|
if code == 0 && builtin_ret.is_none() {
|
|
return ProcStatus::empty();
|
|
}
|
|
if code < 0 {
|
|
// If the code is below 0, constructing a proc_status_t
|
|
// would assert() out, which is a terrible failure mode
|
|
// So instead, what we do is we get a positive code,
|
|
// and we avoid 0.
|
|
FLOGF!(
|
|
warning,
|
|
"builtin %ls returned invalid exit code %d",
|
|
argv[0],
|
|
code
|
|
);
|
|
code = ((256 + code) % 256).abs();
|
|
if code == 0 {
|
|
code = 255;
|
|
}
|
|
}
|
|
|
|
ProcStatus::from_exit_code(code)
|
|
}
|
|
|
|
/// Returns a list of all builtin names.
|
|
pub fn builtin_get_names() -> impl Iterator<Item = &'static wstr> {
|
|
BUILTIN_DATAS.iter().map(|builtin| builtin.name)
|
|
}
|
|
|
|
/// Return a one-line description of the specified builtin.
|
|
pub fn builtin_get_desc(name: &wstr) -> Option<&'static wstr> {
|
|
let desc = match name {
|
|
_ if name == "." => wgettext!("Evaluate contents of file"),
|
|
_ if name == ":" => wgettext!("Return a successful result"),
|
|
_ if name == "[" => wgettext!("Test a condition"), // ]
|
|
_ if name == "_" => wgettext!("Translate a string"),
|
|
_ if name == "abbr" => wgettext!("Manage abbreviations"),
|
|
_ if name == "and" => wgettext!("Run command if last command succeeded"),
|
|
_ if name == "argparse" => wgettext!("Parse options in fish script"),
|
|
_ if name == "begin" => wgettext!("Create a block of code"),
|
|
_ if name == "bg" => wgettext!("Send job to background"),
|
|
_ if name == "bind" => wgettext!("Handle fish key bindings"),
|
|
_ if name == "block" => wgettext!("Temporarily block delivery of events"),
|
|
_ if name == "break" => wgettext!("Stop the innermost loop"),
|
|
_ if name == "breakpoint" => wgettext!("Halt execution and start debug prompt"),
|
|
_ if name == "builtin" => wgettext!("Run a builtin specifically"),
|
|
_ if name == "case" => wgettext!("Block of code to run conditionally"),
|
|
_ if name == "cd" => wgettext!("Change working directory"),
|
|
_ if name == "command" => wgettext!("Run a command specifically"),
|
|
_ if name == "commandline" => wgettext!("Set or get the commandline"),
|
|
_ if name == "complete" => wgettext!("Edit command specific completions"),
|
|
_ if name == "contains" => wgettext!("Search for a specified string in a list"),
|
|
_ if name == "continue" => wgettext!("Skip over remaining innermost loop"),
|
|
_ if name == "count" => wgettext!("Count the number of arguments"),
|
|
_ if name == "disown" => wgettext!("Remove job from job list"),
|
|
_ if name == "echo" => wgettext!("Print arguments"),
|
|
_ if name == "else" => wgettext!("Evaluate block if condition is false"),
|
|
_ if name == "emit" => wgettext!("Emit an event"),
|
|
_ if name == "end" => wgettext!("End a block of commands"),
|
|
_ if name == "eval" => wgettext!("Evaluate a string as a statement"),
|
|
_ if name == "exec" => wgettext!("Run command in current process"),
|
|
_ if name == "exit" => wgettext!("Exit the shell"),
|
|
_ if name == "false" => wgettext!("Return an unsuccessful result"),
|
|
_ if name == "fg" => wgettext!("Send job to foreground"),
|
|
_ if name == "for" => wgettext!("Perform a set of commands multiple times"),
|
|
_ if name == "function" => wgettext!("Define a new function"),
|
|
_ if name == "functions" => wgettext!("List or remove functions"),
|
|
_ if name == "history" => wgettext!("History of commands executed by user"),
|
|
_ if name == "if" => wgettext!("Evaluate block if condition is true"),
|
|
_ if name == "jobs" => wgettext!("Print currently running jobs"),
|
|
_ if name == "math" => wgettext!("Evaluate math expressions"),
|
|
_ if name == "not" => wgettext!("Negate exit status of job"),
|
|
_ if name == "or" => wgettext!("Execute command if previous command failed"),
|
|
_ if name == "path" => wgettext!("Handle paths"),
|
|
_ if name == "printf" => wgettext!("Prints formatted text"),
|
|
_ if name == "pwd" => wgettext!("Print the working directory"),
|
|
_ if name == "random" => wgettext!("Generate random number"),
|
|
_ if name == "read" => wgettext!("Read a line of input into variables"),
|
|
_ if name == "realpath" => wgettext!("Show absolute path sans symlinks"),
|
|
_ if name == "return" => wgettext!("Stop the currently evaluated function"),
|
|
_ if name == "set" => wgettext!("Handle environment variables"),
|
|
_ if name == "set_color" => wgettext!("Set the terminal color"),
|
|
_ if name == "source" => wgettext!("Evaluate contents of file"),
|
|
_ if name == "status" => wgettext!("Return status information about fish"),
|
|
_ if name == "string" => wgettext!("Manipulate strings"),
|
|
_ if name == "switch" => wgettext!("Conditionally run blocks of code"),
|
|
_ if name == "test" => wgettext!("Test a condition"),
|
|
_ if name == "time" => wgettext!("Measure how long a command or block takes"),
|
|
_ if name == "true" => wgettext!("Return a successful result"),
|
|
_ if name == "type" => wgettext!("Check if a thing is a thing"),
|
|
_ if name == "ulimit" => wgettext!("Get/set resource usage limits"),
|
|
_ if name == "wait" => wgettext!("Wait for background processes completed"),
|
|
_ if name == "while" => wgettext!("Perform a command multiple times"),
|
|
_ => return None,
|
|
};
|
|
Some(desc)
|
|
}
|
|
|
|
/// Display help/usage information for the specified builtin or function from manpage
|
|
///
|
|
/// @param name
|
|
/// builtin or function name to get up help for
|
|
///
|
|
/// Process and print help for the specified builtin or function.
|
|
pub fn builtin_print_help(parser: &Parser, streams: &mut IoStreams, cmd: &wstr) {
|
|
builtin_print_help_error(parser, streams, cmd, L!(""))
|
|
}
|
|
|
|
pub fn builtin_print_help_error(
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
cmd: &wstr,
|
|
error_message: &wstr,
|
|
) {
|
|
// This won't ever work if no_exec is set.
|
|
if no_exec() {
|
|
return;
|
|
}
|
|
let name_esc = escape(cmd);
|
|
let mut cmd = sprintf!("__fish_print_help %ls ", &name_esc);
|
|
let mut ios = streams.io_chain.clone();
|
|
if !error_message.is_empty() {
|
|
cmd.push_utfstr(&escape(error_message));
|
|
// If it's an error, redirect the output of __fish_print_help to stderr
|
|
ios.push(Arc::new(IoFd::new(STDOUT_FILENO, STDERR_FILENO)));
|
|
}
|
|
let res = parser.eval(&cmd, &ios);
|
|
if res.status.normal_exited() && res.status.exit_code() == 2 {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!(BUILTIN_ERR_MISSING_HELP, name_esc, name_esc));
|
|
}
|
|
}
|
|
|
|
/// Perform error reporting for encounter with unknown option.
|
|
pub fn builtin_unknown_option(
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
cmd: &wstr,
|
|
opt: &wstr,
|
|
print_hints: bool, /*=true*/
|
|
) {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!(BUILTIN_ERR_UNKNOWN, cmd, opt));
|
|
if print_hints {
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
|
}
|
|
}
|
|
|
|
/// Perform error reporting for encounter with missing argument.
|
|
pub fn builtin_missing_argument(
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
cmd: &wstr,
|
|
mut opt: &wstr,
|
|
print_hints: bool, /*=true*/
|
|
) {
|
|
if opt.char_at(0) == '-' && opt.char_at(1) != '-' {
|
|
// if c in -qc '-qc' is missing the argument, now opt is just 'c'
|
|
opt = &opt[opt.len() - 1..];
|
|
streams.err.append(wgettext_fmt!(
|
|
BUILTIN_ERR_MISSING,
|
|
cmd,
|
|
L!("-").to_owned() + opt
|
|
));
|
|
} else {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!(BUILTIN_ERR_MISSING, cmd, opt));
|
|
}
|
|
if print_hints {
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
|
}
|
|
}
|
|
|
|
/// Print the backtrace and call for help that we use at the end of error messages.
|
|
pub fn builtin_print_error_trailer(parser: &Parser, b: &mut OutputStream, cmd: &wstr) {
|
|
b.push('\n');
|
|
let stacktrace = parser.current_line();
|
|
// Don't print two empty lines if we don't have a stacktrace.
|
|
if !stacktrace.is_empty() {
|
|
b.append(stacktrace);
|
|
b.push('\n');
|
|
}
|
|
b.append(wgettext_fmt!(
|
|
"(Type 'help %ls' for related documentation)\n",
|
|
cmd
|
|
));
|
|
}
|
|
|
|
/// This function works like perror, but it prints its result into the streams.err string instead
|
|
/// to stderr. Used by the builtin commands.
|
|
pub fn builtin_wperror(program_name: &wstr, streams: &mut IoStreams) {
|
|
let err = errno();
|
|
streams.err.append(program_name);
|
|
streams.err.append(L!(": "));
|
|
if err.0 != 0 {
|
|
let werr = WString::from_str(&err.to_string());
|
|
streams.err.append(werr);
|
|
streams.err.push('\n');
|
|
}
|
|
}
|
|
|
|
pub struct HelpOnlyCmdOpts {
|
|
pub print_help: bool,
|
|
pub optind: usize,
|
|
}
|
|
|
|
impl HelpOnlyCmdOpts {
|
|
pub fn parse(
|
|
args: &mut [&wstr],
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
) -> Result<Self, Option<c_int>> {
|
|
let cmd = args[0];
|
|
let print_hints = true;
|
|
|
|
const shortopts: &wstr = L!("+:h");
|
|
const longopts: &[woption] = &[wopt(L!("help"), woption_argument_t::no_argument, 'h')];
|
|
|
|
let mut print_help = false;
|
|
let mut w = wgetopter_t::new(shortopts, longopts, args);
|
|
while let Some(c) = w.wgetopt_long() {
|
|
match c {
|
|
'h' => {
|
|
print_help = true;
|
|
}
|
|
':' => {
|
|
builtin_missing_argument(
|
|
parser,
|
|
streams,
|
|
cmd,
|
|
args[w.woptind - 1],
|
|
print_hints,
|
|
);
|
|
return Err(STATUS_INVALID_ARGS);
|
|
}
|
|
'?' => {
|
|
builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], print_hints);
|
|
return Err(STATUS_INVALID_ARGS);
|
|
}
|
|
_ => {
|
|
panic!("unexpected retval from wgetopter::wgetopt_long()");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(HelpOnlyCmdOpts {
|
|
print_help,
|
|
optind: w.woptind,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[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 IoStreams,
|
|
chunk_size: usize,
|
|
) -> Self {
|
|
let reader = streams.stdin_is_directly_redirected.then(|| {
|
|
let stdin_fd = streams.stdin_fd;
|
|
assert!(stdin_fd >= 0, "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);
|
|
}
|
|
}
|
|
|
|
/// A generic builtin that only supports showing a help message. This is only a placeholder that
|
|
/// prints the help message. Useful for commands that live in the parser.
|
|
fn builtin_generic(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option<c_int> {
|
|
let argc = argv.len();
|
|
let opts = match HelpOnlyCmdOpts::parse(argv, parser, streams) {
|
|
Ok(opts) => opts,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
if opts.print_help {
|
|
builtin_print_help(parser, streams, argv[0]);
|
|
return STATUS_CMD_OK;
|
|
}
|
|
|
|
// Hackish - if we have no arguments other than the command, we are a "naked invocation" and we
|
|
// just print help.
|
|
if argc == 1 || argv[0] == "time" {
|
|
builtin_print_help(parser, streams, argv[0]);
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
|
|
STATUS_CMD_ERROR
|
|
}
|
|
|
|
/// This function handles both the 'continue' and the 'break' builtins that are used for loop
|
|
/// control.
|
|
fn builtin_break_continue(
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
argv: &mut [&wstr],
|
|
) -> Option<c_int> {
|
|
let is_break = argv[0] == "break";
|
|
let argc = argv.len();
|
|
|
|
if argc != 1 {
|
|
let error_message = wgettext_fmt!(BUILTIN_ERR_UNKNOWN, argv[0], argv[1]);
|
|
builtin_print_help_error(parser, streams, argv[0], &error_message);
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
|
|
// Paranoia: ensure we have a real loop.
|
|
// This is checked in the AST but we may be invoked dynamically, e.g. just via "eval break".
|
|
let mut has_loop = false;
|
|
for b in parser.blocks().iter().rev() {
|
|
if [BlockType::while_block, BlockType::for_block].contains(&b.typ()) {
|
|
has_loop = true;
|
|
break;
|
|
}
|
|
if b.is_function_call() {
|
|
break;
|
|
}
|
|
}
|
|
if !has_loop {
|
|
let error_message = wgettext_fmt!("%ls: Not inside of loop\n", argv[0]);
|
|
builtin_print_help_error(parser, streams, argv[0], &error_message);
|
|
return STATUS_CMD_ERROR;
|
|
}
|
|
|
|
// Mark the status in the libdata.
|
|
parser.libdata_mut().pods.loop_status = if is_break {
|
|
LoopStatus::breaks
|
|
} else {
|
|
LoopStatus::continues
|
|
};
|
|
STATUS_CMD_OK
|
|
}
|
|
|
|
/// Implementation of the builtin breakpoint command, used to launch the interactive debugger.
|
|
fn builtin_breakpoint(
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
argv: &mut [&wstr],
|
|
) -> Option<c_int> {
|
|
let cmd = argv[0];
|
|
if argv.len() != 1 {
|
|
streams.err.append(wgettext_fmt!(
|
|
BUILTIN_ERR_ARG_COUNT1,
|
|
cmd,
|
|
0,
|
|
argv.len() - 1
|
|
));
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
|
|
// If we're not interactive then we can't enter the debugger. So treat this command as a no-op.
|
|
if !parser.is_interactive() {
|
|
return STATUS_CMD_ERROR;
|
|
}
|
|
|
|
// Ensure we don't allow creating a breakpoint at an interactive prompt. There may be a simpler
|
|
// or clearer way to do this but this works.
|
|
{
|
|
if parser
|
|
.block_at_index(1)
|
|
.map_or(true, |b| b.typ() == BlockType::breakpoint)
|
|
{
|
|
streams.err.append(wgettext_fmt!(
|
|
"%ls: Command not valid at and interactive prompt\n",
|
|
cmd,
|
|
));
|
|
return STATUS_ILLEGAL_CMD;
|
|
}
|
|
}
|
|
|
|
let bpb = parser.push_block(Block::breakpoint_block());
|
|
let io_chain = &streams.io_chain;
|
|
reader_read(parser, STDIN_FILENO, io_chain);
|
|
parser.pop_block(bpb);
|
|
Some(parser.get_last_status())
|
|
}
|
|
|
|
fn builtin_true(_parser: &Parser, _streams: &mut IoStreams, _argv: &mut [&wstr]) -> Option<c_int> {
|
|
STATUS_CMD_OK
|
|
}
|
|
|
|
fn builtin_false(_parser: &Parser, _streams: &mut IoStreams, _argv: &mut [&wstr]) -> Option<c_int> {
|
|
STATUS_CMD_ERROR
|
|
}
|
|
|
|
fn builtin_gettext(_parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option<c_int> {
|
|
for arg in &argv[1..] {
|
|
streams.out.append(wgettext_str(arg));
|
|
}
|
|
STATUS_CMD_OK
|
|
}
|