use crate::{err_fmt, err_raw, err_str, screen::escape_code_length};
use fish_wcstringutil::fish_wcwidth_visible;
use super::prelude::*;
mod collect;
mod escape;
mod join;
mod length;
mod r#match;
mod pad;
mod repeat;
mod replace;
mod shorten;
mod split;
mod sub;
mod transform;
mod trim;
mod unescape;
#[cfg(test)]
mod test_helpers;
trait StringSubCommand<'args> {
const SHORT_OPTIONS: &'static wstr;
const LONG_OPTIONS: &'static [WOption<'static>];
fn parse_opt(&mut self, c: char, arg: Option<&'args wstr>) -> Result<(), StringError<'_>>;
fn parse_opts(
&mut self,
args: &mut [&'args wstr],
parser: &Parser,
streams: &mut IoStreams,
) -> Result<usize, ErrorCode> {
let cmd = L!("string");
let subcmd = args[0];
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
let mut w = WGetopter::new(Self::SHORT_OPTIONS, Self::LONG_OPTIONS, args);
while let Some(c) = w.next_opt() {
match c {
':' => {
builtin_missing_argument(
parser,
streams,
cmd,
Some(subcmd),
args_read[w.wopt_index - 1],
false,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
err_fmt!(Error::UNEXP_OPT_ARG, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
err_fmt!(Error::UNKNOWN_OPT, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
c => {
let retval = self.parse_opt(c, w.woptarg);
if let Err(e) = retval {
e.print_error(&args_read, streams, w.wopt_index);
return Err(STATUS_INVALID_ARGS);
}
}
}
}
Ok(w.wopt_index)
}
fn parse_arg_number<'a, 'b>(arg: &'a wstr) -> Result<i64, StringError<'b>> {
let n = fish_wcstol(arg).map_err(|_| err_fmt!(Error::NOT_NUMBER, arg))?;
Ok(n)
}
#[allow(unused_variables)]
fn take_args(
&mut self,
optind: &mut usize,
args: &[&'args wstr],
streams: &mut IoStreams,
) -> Result<(), ErrorCode> {
Ok(())
}
fn handle(
&mut self,
parser: &Parser,
streams: &mut IoStreams,
optind: &mut usize,
args: &[&'args wstr],
) -> Result<(), ErrorCode>;
fn run(
&mut self,
parser: &Parser,
streams: &mut IoStreams,
args: &mut [&'args wstr],
) -> BuiltinResult {
match self.run_impl(parser, streams, args) {
Ok(()) => BuiltinResult::Ok(SUCCESS),
Err(err) => BuiltinResult::Err(err),
}
}
fn run_impl(
&mut self,
parser: &Parser,
streams: &mut IoStreams,
args: &mut [&'args wstr],
) -> Result<(), ErrorCode> {
if args.len() >= 3 && (args[2] == "-h" || args[2] == "--help") {
let string_dash_subcmd = WString::from(args[0]) + L!("-") + args[1];
builtin_print_help(parser, streams, &string_dash_subcmd);
return Ok(());
}
let args = &mut args[1..];
let mut optind = self.parse_opts(args, parser, streams)?;
self.take_args(&mut optind, args, streams)?;
if streams.stdin_is_directly_redirected && args.len() > optind {
err_str!(Error::TOO_MANY_ARGUMENTS)
.subcmd(L!("string"), args[0])
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
self.handle(parser, streams, &mut optind, args)
}
}
enum StringError<'a> {
InvalidArgs(Error<'a>),
UnknownOption,
}
enum RegexError {
Compile(WString, pcre2::Error),
InvalidCaptureGroupName(WString),
InvalidEscape(WString),
}
impl RegexError {
fn print_error(&self, args: &[&wstr], streams: &mut IoStreams) {
let cmd = L!("string");
let subcmd = args[0];
use RegexError::*;
match self {
Compile(pattern, e) => {
let mut marker: WString =
" ".repeat(e.offset().unwrap_or(0).saturating_sub(1)).into();
marker.push('^');
err_fmt!(Error::REGEX_COMPILE, e.error_message())
.append_to_msg('\n')
.append_to_msg(&err_raw!(pattern).subcmd(cmd, subcmd).to_string())
.append_to_msg('\n')
.append_to_msg(&err_raw!(marker).subcmd(cmd, subcmd).to_string())
.subcmd(cmd, subcmd)
.finish(streams);
}
InvalidCaptureGroupName(name) => {
err_fmt!(
"Modification of read-only variable \"%s\" is not allowed",
name
)
.finish(streams);
}
InvalidEscape(pattern) => {
err_fmt!("Invalid escape sequence in pattern \"%s\"", pattern)
.subcmd(cmd, subcmd)
.finish(streams);
}
}
}
}
impl<'a> From<error::Error<'a>> for StringError<'a> {
fn from(error: error::Error<'a>) -> Self {
StringError::InvalidArgs(error)
}
}
impl<'a> StringError<'a> {
fn print_error(self, args: &[&wstr], streams: &mut IoStreams, optind: usize) {
let cmd = L!("string");
let subcmd = args[0];
use StringError::*;
match self {
InvalidArgs(err) => {
err.subcmd(cmd, subcmd).finish(streams);
}
UnknownOption => {
unreachable!(
"unexpected option '{}' for 'string {subcmd}'",
args[optind - 1]
)
}
}
}
}
#[derive(Default, PartialEq, Clone, Copy)]
enum Direction {
#[default]
Left,
Right,
}
fn width_without_escapes(ins: &wstr, start_pos: usize) -> usize {
let mut width: isize = 0;
for c in ins[start_pos..].chars() {
let w = fish_wcwidth_visible(c);
if w > 0 || width > 0 {
width += w;
}
}
let mut pos = start_pos;
while let Some(ec_pos) = ins.slice_from(pos).find_char('\x1B') {
pos += ec_pos;
if let Some(len) = escape_code_length(ins.slice_from(pos)) {
let sub = &ins[pos..pos + len];
for c in sub.chars() {
width -= fish_wcwidth_visible(c);
}
pos += len - 1;
} else {
pos += 1;
}
}
usize::try_from(width).expect("line has negative width")
}
const STRING_CHUNK_SIZE: usize = 1024;
fn arguments<'iter, 'args>(
args: &'iter [&'args wstr],
argidx: &'iter mut usize,
streams: &mut IoStreams,
) -> Arguments<'args, 'iter> {
Arguments::new(args, argidx, streams, STRING_CHUNK_SIZE)
}
pub fn string(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
let cmd = args[0];
let argc = args.len();
if argc <= 1 {
err_str!(Error::MISSING_SUBCMD)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if args[1] == "-h" || args[1] == "--help" {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
}
let subcmd_name = args[1];
match subcmd_name.to_string().as_str() {
"collect" => collect::Collect::default().run(parser, streams, args),
"escape" => escape::Escape::default().run(parser, streams, args),
"join" => join::Join::default().run(parser, streams, args),
"join0" => {
let mut cmd = join::Join::default();
cmd.is_join0 = true;
cmd.run(parser, streams, args)
}
"length" => length::Length::default().run(parser, streams, args),
"lower" => transform::Transform {
quiet: false,
func: wstr::to_lowercase,
}
.run(parser, streams, args),
"match" => r#match::Match::default().run(parser, streams, args),
"pad" => pad::Pad::default().run(parser, streams, args),
"repeat" => repeat::Repeat::default().run(parser, streams, args),
"replace" => replace::Replace::default().run(parser, streams, args),
"shorten" => shorten::Shorten::default().run(parser, streams, args),
"split" => split::Split::default().run(parser, streams, args),
"split0" => {
let mut cmd = split::Split::default();
cmd.is_split0 = true;
cmd.run(parser, streams, args)
}
"sub" => sub::Sub::default().run(parser, streams, args),
"trim" => trim::Trim::default().run(parser, streams, args),
"unescape" => unescape::Unescape::default().run(parser, streams, args),
"upper" => transform::Transform {
quiet: false,
func: wstr::to_uppercase,
}
.run(parser, streams, args),
_ => {
err_fmt!(Error::INVALID_SUBCMD)
.subcmd(cmd, subcmd_name)
.full_trailer(parser)
.finish(streams);
Err(STATUS_INVALID_ARGS)
}
}
}