mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-02-12 00:06:15 +08:00
225 lines
6.6 KiB
Rust
225 lines
6.6 KiB
Rust
//! Implementation of the echo builtin.
|
|
|
|
use super::prelude::*;
|
|
use crate::wchar::encode_byte_to_char;
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct Options {
|
|
print_newline: bool,
|
|
print_spaces: bool,
|
|
interpret_special_chars: bool,
|
|
}
|
|
|
|
impl Default for Options {
|
|
fn default() -> Self {
|
|
Self {
|
|
print_newline: true,
|
|
print_spaces: true,
|
|
interpret_special_chars: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_options(
|
|
args: &mut [&wstr],
|
|
parser: &Parser,
|
|
streams: &mut IoStreams,
|
|
) -> Result<(Options, usize), Option<c_int>> {
|
|
let cmd = args[0];
|
|
|
|
const SHORT_OPTS: &wstr = L!("+:Eens");
|
|
const LONG_OPTS: &[WOption] = &[];
|
|
|
|
let mut opts = Options::default();
|
|
|
|
let mut oldopts = opts;
|
|
let mut oldoptind = 0;
|
|
|
|
let mut w = WGetopter::new(SHORT_OPTS, LONG_OPTS, args);
|
|
while let Some(c) = w.next_opt() {
|
|
match c {
|
|
'n' => opts.print_newline = false,
|
|
'e' => opts.interpret_special_chars = true,
|
|
's' => opts.print_spaces = false,
|
|
'E' => opts.interpret_special_chars = false,
|
|
':' => {
|
|
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], true);
|
|
return Err(STATUS_INVALID_ARGS);
|
|
}
|
|
'?' => {
|
|
return Ok((oldopts, w.wopt_index - 1));
|
|
}
|
|
_ => {
|
|
panic!("unexpected retval from WGetopter");
|
|
}
|
|
}
|
|
|
|
// Super cheesy: We keep an old copy of the option state around,
|
|
// so we can revert it in case we get an argument like
|
|
// "-n foo".
|
|
// We need to keep it one out-of-date so we can ignore the *last* option.
|
|
// (this might be an issue in wgetopt, but that's a whole other can of worms
|
|
// and really only occurs with our weird "put it back" option parsing)
|
|
if w.wopt_index == oldoptind + 2 {
|
|
oldopts = opts;
|
|
oldoptind = w.wopt_index;
|
|
}
|
|
}
|
|
|
|
Ok((opts, w.wopt_index))
|
|
}
|
|
|
|
/// Parse a numeric escape sequence in `s`, returning the number of characters consumed and the
|
|
/// resulting value. Supported escape sequences:
|
|
///
|
|
/// - `0nnn`: octal value, zero to three digits
|
|
/// - `nnn`: octal value, one to three digits
|
|
/// - `xhh`: hex value, one to two digits
|
|
fn parse_numeric_sequence<I>(chars: I) -> Option<(usize, u8)>
|
|
where
|
|
I: IntoIterator<Item = char>,
|
|
{
|
|
let mut chars = chars.into_iter().peekable();
|
|
|
|
// the first character of the numeric part of the sequence
|
|
let mut start = 0;
|
|
|
|
let mut base: u8 = 0;
|
|
let mut max_digits = 0;
|
|
|
|
let first = *chars.peek()?;
|
|
if first.is_digit(8) {
|
|
// Octal escape
|
|
base = 8;
|
|
|
|
// If the first digit is a 0, we allow four digits (including that zero); otherwise, we
|
|
// allow 3.
|
|
max_digits = if first == '0' { 4 } else { 3 };
|
|
} else if first == 'x' {
|
|
// Hex escape
|
|
base = 16;
|
|
max_digits = 2;
|
|
|
|
// Skip the x
|
|
start = 1;
|
|
};
|
|
|
|
if base == 0 {
|
|
return None;
|
|
}
|
|
|
|
let mut val = 0;
|
|
let mut consumed = start;
|
|
for digit in chars
|
|
.skip(start)
|
|
.take(max_digits)
|
|
.map_while(|c| c.to_digit(base.into()))
|
|
{
|
|
// base is either 8 or 16, so digit can never be >255
|
|
let digit = u8::try_from(digit).unwrap();
|
|
|
|
val = val * base + digit;
|
|
|
|
consumed += 1;
|
|
}
|
|
|
|
// We succeeded if we consumed at least one digit.
|
|
if consumed > 0 {
|
|
Some((consumed, val))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// The echo builtin.
|
|
///
|
|
/// Bash only respects `-n` if it's the first argument. We'll do the same. We also support a new,
|
|
/// fish specific, option `-s` to mean "no spaces".
|
|
pub fn echo(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
|
|
let (opts, optind) = match parse_options(args, parser, streams) {
|
|
Ok((opts, optind)) => (opts, optind),
|
|
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
|
|
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
|
|
};
|
|
|
|
// The special character \c can be used to indicate no more output.
|
|
let mut output_stopped = false;
|
|
|
|
// We buffer output so we can write in one go,
|
|
// this matters when writing to an fd.
|
|
let mut out = WString::new();
|
|
let args_to_echo = &args[optind..];
|
|
'outer: for (idx, arg) in args_to_echo.iter().enumerate() {
|
|
if opts.print_spaces && idx > 0 {
|
|
out.push(' ');
|
|
}
|
|
|
|
let mut chars = arg.chars().peekable();
|
|
while let Some(c) = chars.next() {
|
|
if !opts.interpret_special_chars || c != '\\' {
|
|
// Not an escape.
|
|
out.push(c);
|
|
continue;
|
|
}
|
|
|
|
let Some(next_char) = chars.peek() else {
|
|
// Incomplete escape sequence is echoed verbatim
|
|
out.push('\\');
|
|
break;
|
|
};
|
|
|
|
// Most escapes consume one character in addition to the backslash; the numeric
|
|
// sequences may consume more, while an unrecognized escape sequence consumes none.
|
|
let mut consumed = 1;
|
|
|
|
let escaped = match next_char {
|
|
'a' => '\x07',
|
|
'b' => '\x08',
|
|
'e' => '\x1B',
|
|
'f' => '\x0C',
|
|
'n' => '\n',
|
|
'r' => '\r',
|
|
't' => '\t',
|
|
'v' => '\x0B',
|
|
'\\' => '\\',
|
|
'c' => {
|
|
output_stopped = true;
|
|
break 'outer;
|
|
}
|
|
_ => {
|
|
// Octal and hex escape sequences.
|
|
if let Some((digits_consumed, narrow_val)) =
|
|
parse_numeric_sequence(chars.clone())
|
|
{
|
|
consumed = digits_consumed;
|
|
// The narrow_val is a literal byte that we want to output (#1894).
|
|
encode_byte_to_char(narrow_val)
|
|
} else {
|
|
consumed = 0;
|
|
'\\'
|
|
}
|
|
}
|
|
};
|
|
|
|
// Skip over characters that were part of this escape sequence (after the backslash
|
|
// that was consumed by the `while` loop).
|
|
// TODO: `Iterator::advance_by()`: https://github.com/rust-lang/rust/issues/77404
|
|
for _ in 0..consumed {
|
|
let _ = chars.next();
|
|
}
|
|
|
|
out.push(escaped);
|
|
}
|
|
}
|
|
|
|
if opts.print_newline && !output_stopped {
|
|
out.push('\n');
|
|
}
|
|
|
|
if !out.is_empty() {
|
|
streams.out.append(out);
|
|
}
|
|
|
|
STATUS_CMD_OK
|
|
}
|