repo_description: The user-friendly command line shell.
repo_topics: fish rust shell terminal
code: //! Implementation of the read builtin.
  
  use super::prelude::*;
  use crate::{
      builtins::error::Error,
      common::valid_var_name,
      env::{EnvMode, EnvVar, EnvVarFlags, Environment as _, READ_BYTE_LIMIT},
      err_fmt, err_str,
      input_common::{DecodeState, InvalidPolicy, decode_utf8},
      nix::isatty,
      parse_execution::varname_error,
      parser::ParserEnvSetMode,
      reader::{
          ReaderConfig, commandline_set_buffer, reader_pop, reader_push, reader_readline,
          set_shell_modes_temporarily,
      },
      tokenizer::{TOK_ACCEPT_UNFINISHED, TOK_ARGUMENT_LIST, Tok, Tokenizer},
      wutil,
  };
  use fish_common::{UnescapeStringStyle, escape, read_blocked, unescape_string};
  use fish_util::perror;
  use fish_wcstringutil::{split_about, split_string_tok};
  use fish_widestring::bytes2wcstring;
  use libc::SEEK_CUR;
  use std::{num::NonZeroUsize, os::fd::RawFd, sync::atomic::Ordering};
  
  #[derive(Clone, Copy, Eq, PartialEq)]
  pub(crate) enum TokenOutputMode {
      Expanded,
      Raw,
      Unescaped,
  }
  
  #[derive(Default)]
  struct Options {
      print_help: bool,
      place: ParserEnvSetMode,
      prompt: Option<WString>,
      prompt_str: Option<WString>,
      right_prompt: WString,
      commandline: Option<WString>,
      // If a delimiter was given. Used to distinguish between the default
      // empty string and a given empty delimiter.
      delimiter: Option<WString>,
      token_mode: Option<TokenOutputMode>, // never expanded
      shell: bool,
      array: bool,
      silent: bool,
      split_null: bool,
      nchars: Option<NonZeroUsize>,
      one_line: bool,
  }
  
  impl Options {
      fn new() -> Self {
          Options {
              place: ParserEnvSetMode::user(EnvMode::empty()),
              ..Default::default()
          }
      }
  }
  
  const SHORT_OPTIONS: &wstr = L!("ac:d:fghLln:p:sStuxzP:UR:L");
  const LONG_OPTIONS: &[WOption] = &[
      wopt(L!("array"), ArgType::NoArgument, 'a'),
      wopt(L!("command"), ArgType::RequiredArgument, 'c'),
      wopt(L!("delimiter"), ArgType::RequiredArgument, 'd'),
      wopt(L!("export"), ArgType::NoArgument, 'x'),
      wopt(L!("function"), ArgType::NoArgument, 'f'),
      wopt(L!("global"), ArgType::NoArgument, 'g'),
      wopt(L!("help"), ArgType::NoArgument, 'h'),
      wopt(L!("line"), ArgType::NoArgument, 'L'),
      wopt(L!("list"), ArgType::NoArgument, 'a'),
      wopt(L!("local"), ArgType::NoArgument, 'l'),
      wopt(L!("nchars"), ArgType::RequiredArgument, 'n'),
      wopt(L!("null"), ArgType::NoArgument, 'z'),
      wopt(L!("prompt"), ArgType::RequiredArgument, 'p'),
      wopt(L!("prompt-str"), ArgType::RequiredArgument, 'P'),
      wopt(L!("right-prompt"), ArgType::RequiredArgument, 'R'),
      wopt(L!("shell"), ArgType::NoArgument, 'S'),
      wopt(L!("silent"), ArgType::NoArgument, 's'),
      wopt(L!("tokenize"), ArgType::NoArgument, 't'),
      wopt(L!("tokenize-raw"), ArgType::NoArgument, '\x01'),
      wopt(L!("unexport"), ArgType::NoArgument, 'u'),
      wopt(L!("universal"), ArgType::NoArgument, 'U'),
  ];
  
  fn tokenize_flag(token_mode: TokenOutputMode) -> &'static wstr {
      match token_mode {
          TokenOutputMode::Expanded => panic!(),
          TokenOutputMode::Raw => L!("--tokenize-raw"),
          TokenOutputMode::Unescaped => L!("--tokenize"),
      }
  }
  
  fn parse_cmd_opts(
      args: &mut [&wstr],
      parser: &Parser,
      streams: &mut IoStreams,
  ) -> Result<(Options, usize), ErrorCode> {
      let cmd = args[0];
      let mut opts = Options::new();
      let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, args);
      while let Some(opt) = w.next_opt() {
          match opt {
              'a' => {
                  opts.array = true;
              }
              'c' => {
                  opts.commandline = Some(w.woptarg.unwrap().to_owned());
              }
              'd' => {
                  opts.delimiter = Some(w.woptarg.unwrap().to_owned());
              }
              'f' => {
                  opts.place.mode |= EnvMode::FUNCTION;
              }
              'g' => {
                  opts.place.mode |= EnvMode::GLOBAL;
              }
              'h' => {
                  opts.print_help = true;
              }
              'L' => {
                  opts.one_line = true;
              }
              'l' => {
                  opts.place.mode |= EnvMode::LOCAL;
              }
              'n' => {
                  opts.nchars = match fish_wcstoi(w.woptarg.unwrap()) {
                      Ok(n) if n >= 0 => NonZeroUsize::new(n.try_into().unwrap()),
                      Err(wutil::Error::Overflow) => {
                          err_fmt!("Argument '%s' is out of range", w.woptarg.unwrap())
                              .cmd(cmd)
                              .full_trailer(parser)
                              .finish(streams);
                          return Err(STATUS_INVALID_ARGS);
                      }
                      _ => {
                          err_fmt!(Error::NOT_NUMBER, w.woptarg.unwrap())
                              .cmd(cmd)
                              .full_trailer(parser)
                              .finish(streams);
                          return Err(STATUS_INVALID_ARGS);
                      }
                  }
              }
              'P' => {
                  opts.prompt_str = Some(w.woptarg.unwrap().to_owned());
              }
              'p' => {
                  opts.prompt = Some(w.woptarg.unwrap().to_owned());
              }
              'R' => {
                  opts.right_prompt = w.woptarg.unwrap().to_owned();
              }
              's' => {
                  opts.silent = true;
              }
              'S' => {
                  opts.shell = true;
              }
              't' | '\x01' => {
                  let new_mode = match opt {
                      't' => TokenOutputMode::Unescaped,
                      '\x01' => TokenOutputMode::Raw,
                      _ => unreachable!(),
                  };
                  if let Some(old_mode) = opts.token_mode {
                      if old_mode != new_mode {
                          err_fmt!(
                              Error::INVALID_OPT_COMBO_WITH_CTX,
                              wgettext_fmt!(
                                  "%s and %s are mutually exclusive",
                                  tokenize_flag(old_mode),
                                  tokenize_flag(new_mode),
                              )
                          )
                          .cmd(cmd)
                          .full_trailer(parser)
                          .finish(streams);
                          return Err(STATUS_INVALID_ARGS);
                      }
                  }
                  opts.token_mode = Some(new_mode);
              }
              'U' => {
                  opts.place.mode |= EnvMode::UNIVERSAL;
              }
              'u' => {
                  opts.place.mode |= EnvMode::UNEXPORT;
              }
              'x' => {
                  opts.place.mode |= EnvMode::EXPORT;
              }
              'z' => {
                  opts.split_null = true;
              }
              ':' => {
                  builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], true);
                  return Err(STATUS_INVALID_ARGS);
              }
              ';' => {
                  builtin_unexpected_argument(parser, streams, cmd, args[w.wopt_index - 1], true);
                  return Err(STATUS_INVALID_ARGS);
              }
              '?' => {
                  builtin_unknown_option(parser, streams, cmd, args[w.wopt_index - 1], true);
                  return Err(STATUS_INVALID_ARGS);
              }
              _ => {
                  panic!("unexpected retval from WGetopter");
              }
          }
      }
  
      Ok((opts, w.wopt_index))
  }
  
  /// Read from the tty. This is only valid when the stream is stdin and it is attached to a tty and
  /// we weren't asked to split on null characters.
  #[allow(clippy::too_many_arguments)]
  fn read_interactive(
      parser: &Parser,
      buff: &mut WString,
      nchars: Option<NonZeroUsize>,
      shell: bool,
      silent: bool,
      prompt: &wstr,
      prompt_str_is_empty: bool,
      right_prompt: &wstr,
      commandline: Option<&WString>,
      inputfd: RawFd,
  ) -> BuiltinResult {
      let mut exit_res = Ok(SUCCESS);
  
      // Construct a configuration.
      let conf = ReaderConfig {
          complete_ok: shell,
          highlight_ok: shell,
          syntax_check_ok: shell,
  
          // No autosuggestions or abbreviations in builtin_read.
          autosuggest_ok: false,
          expand_abbrev_ok: false,
  
          exit_on_interrupt: true,
          read_prompt_str_is_empty: prompt_str_is_empty,
          in_silent_mode: silent,
  
          left_prompt_cmd: prompt.to_owned(),
          right_prompt_cmd: right_prompt.to_owned(),
          event: L!("fish_read"),
  
          inputfd,
  
          ..Default::default()
      };
  
      let old_modes = set_shell_modes_temporarily(inputfd);
  
      // Keep in-memory history only.
      reader_push(parser, L!(""), conf);
      let _modifiable_commandline = parser.scope().readonly_commandline.then(|| {
          parser.push_scope(|s| {
              s.readonly_commandline = false;
          })
      });
      if let Some(commandline) = commandline {
          commandline_set_buffer(parser, Some(commandline.clone()), None);
      }
  
      let mline = {
          let _interactive = parser.push_scope(|s| s.is_interactive = true);
          reader_readline(parser, old_modes, nchars)
      };
      if let Some(line) = mline {
          *buff = line;
          if let Some(nchars) = nchars.map(usize::from) {
              // Line may be longer than nchars if a keybinding used `commandline -i`
              // note: we're deliberately throwing away the tail of the commandline.
              // It shouldn't be unread because it was produced with `commandline -i`,
              // not typed.
              if nchars < buff.len() {
                  buff.truncate(nchars);
              }
          }
      } else {
          exit_res = Err(STATUS_CMD_ERROR);
      }
      reader_pop();
      exit_res
  }
  
  /// Bash uses 128 bytes for its chunk size. Very informal testing I did suggested that a smaller
  /// chunk size performed better. However, we're going to use the bash value under the assumption
  /// they've done more extensive testing.
  const READ_CHUNK_SIZE: usize = 128;
  
  /// Read from the fd in chunks until we see newline or null, as requested, is seen. This is only
  /// used when the fd is seekable (so not from a tty or pipe) and we're not reading a specific number
  /// of chars.
  ///
  /// Returns an exit status.
  fn read_in_chunks(fd: RawFd, buff: &mut WString, split_null: bool, do_seek: bool) -> BuiltinResult {
      let mut exit_res = Ok(SUCCESS);
      let mut narrow_buff = vec![];
      let mut eof = false;
      let mut finished = false;
  
      while !finished {
          let mut inbuf = [0_u8; READ_CHUNK_SIZE];
  
          let bytes_read = match read_blocked(fd, &mut inbuf) {
              Ok(0) | Err(_) => {
                  eof = true;
                  break;
              }
              Ok(read) => read,
          };
  
          let bytes_consumed = inbuf[..bytes_read]
              .iter()
              .position(|c| *c == if split_null { b'\0' } else { b'\n' })
              .unwrap_or(bytes_read);
          assert!(bytes_consumed <= bytes_read);
          narrow_buff.extend_from_slice(&inbuf[..bytes_consumed]);
          if bytes_consumed < bytes_read {
              // We found a splitter. The +1 because we need to treat the splitter as consumed, but
              // not append it to the string.
              if do_seek
                  && unsafe {
                      libc::lseek(
                          fd,
                          libc::off_t::try_from(
                              isize::try_from(bytes_consumed).unwrap() - (bytes_read as isize) + 1,
                          )
                          .unwrap(),
                          SEEK_CUR,
                      )
                  } == -1
              {
                  perror("lseek");
                  return Err(STATUS_CMD_ERROR);
              }
              finished = true;
          } else if narrow_buff.len() > READ_BYTE_LIMIT.load(Ordering::Relaxed) {
              exit_res = Err(STATUS_READ_TOO_MUCH);
              finished = true;
          }
      }
  
      *buff = bytes2wcstring(&narrow_buff);
      if buff.is_empty() && eof {
          exit_res = Err(STATUS_CMD_ERROR);
      }
  
      exit_res
  }
  
  /// Read from the fd on char at a time until we've read the requested number of characters or a
  /// newline or null, as appropriate, is seen. This is inefficient so should only be used when the
  /// fd is not seekable.
  fn read_one_char_at_a_time(
      fd: RawFd,
      buff: &mut WString,
      nchars: Option<NonZeroUsize>,
      split_null: bool,
  ) -> BuiltinResult {
      let mut exit_res = Ok(SUCCESS);
      let mut nbytes = 0;
  
      let mut unconsumed = vec![];
  
      loop {
          let chars_read = buff.len();
          let res = loop {
              let mut b = [0_u8; 1];
              match read_blocked(fd, &mut b) {
                  Ok(0) | Err(_) => {
                      break None;
                  }
                  _ => {}
              }
              unconsumed.push(b[0]);
              nbytes += 1;
              match decode_utf8(buff, InvalidPolicy::Passthrough, &unconsumed) {
                  DecodeState::Incomplete => continue,
                  DecodeState::Complete => {
                      unconsumed.clear();
                      break Some(buff.as_char_slice().last().unwrap());
                  }
                  DecodeState::Error => unreachable!(),
              }
          };
  
          if nbytes > READ_BYTE_LIMIT.load(Ordering::Relaxed) {
              // Historical behavior: do not include the codepoint that made us overflow.
              buff.truncate(chars_read);
              exit_res = Err(STATUS_READ_TOO_MUCH);
              break;
          }
          let Some(&res) = res else {
              // EOF
              if buff.is_empty() {
                  exit_res = Err(STATUS_CMD_ERROR);
              }
              break;
          };
          if res == if split_null { '\0' } else { '\n' } {
              buff.pop();
              break;
          }
          if let Some(nchars) = nchars.map(usize::from) {
              if nchars <= buff.len() {
                  break;
              }
          }
      }
  
      exit_res
  }
  
  /// Validate the arguments given to `read` and provide defaults where needed.
  fn validate_read_args(
      cmd: &wstr,
      opts: &mut Options,
      argv: &[&wstr],
      parser: &Parser,
      streams: &mut IoStreams,
  ) -> BuiltinResult {
      localizable_consts! {
          OPTIONS_CANNOT_BE_COMBINED
          "Options %s and %s cannot be used together"
      }
      if opts.prompt.is_some() && opts.prompt_str.is_some() {
          err_fmt!(OPTIONS_CANNOT_BE_COMBINED, "-p", "-P")
              .cmd(cmd)
              .full_trailer(parser)
              .finish(streams);
          return Err(STATUS_INVALID_ARGS);
      }
  
      if opts.delimiter.is_some() && opts.one_line {
          err_fmt!(OPTIONS_CANNOT_BE_COMBINED, "--delimiter", "--line")
              .cmd(cmd)
              .finish(streams);
          return Err(STATUS_INVALID_ARGS);
      }
      if opts.one_line && opts.split_null {
          err_fmt!(OPTIONS_CANNOT_BE_COMBINED, "-z", "--line")
              .cmd(cmd)
              .finish(streams);
          return Err(STATUS_INVALID_ARGS);
      }
  
      if let Some(prompt_str) = opts.prompt_str.as_ref() {
          opts.prompt = Some(L!("echo ").to_owned() + &escape(prompt_str)[..]);
      } else if opts.prompt.is_none() {
          opts.prompt = Some(DEFAULT_READ_PROMPT.to_owned());
      }
  
      if opts.place.mode.contains(EnvMode::UNEXPORT) && opts.place.mode.contains(EnvMode::EXPORT) {
          err_str!(Error::EXPORT_UNEXPORT)
              .cmd(cmd)
              .full_trailer(parser)
              .finish(streams);
          return Err(STATUS_INVALID_ARGS);
      }
  
      if opts
          .place
          .mode
          .intersection(EnvMode::ANY_SCOPE)
          .iter()
          .count()
          > 1
      {
          err_str!(Error::MULTIPLE_SCOPES)
              .cmd(cmd)
              .full_trailer(parser)
              .finish(streams);
          return Err(STATUS_INVALID_ARGS);
      }
  
      if opts.array && argv.len() != 1 {
          err_fmt!(Error::UNEXP_ARG_COUNT, 1, argv.len())
              .cmd(cmd)
              .finish(streams);
          return Err(STATUS_INVALID_ARGS);
      }
  
      fn tokenize_flag(token_mode: TokenOutputMode) -> &'static wstr {
          match token_mode {
              TokenOutputMode::Expanded => panic!(),
              TokenOutputMode::Raw => L!("--tokenize-raw"),
              TokenOutputMode::Unescaped => L!("--tokenize"),
          }
      }
  
      if let Some(token_mode) = opts.token_mode {
          if opts.delimiter.is_some() {
              err_fmt!(
                  Error::COMBO_EXCLUSIVE,
                  "--delimiter",
                  tokenize_flag(token_mode)
              )
              .cmd(cmd)
              .finish(streams);
              return Err(STATUS_INVALID_ARGS);
          }
  
          if opts.one_line {
              err_fmt!(Error::COMBO_EXCLUSIVE, "--line", tokenize_flag(token_mode))
                  .cmd(cmd)
                  .finish(streams);
              return Err(STATUS_INVALID_ARGS);
          }
      }
  
      // Verify all variable names.
      for arg in argv {
          if !valid_var_name(arg) {
              varname_error(cmd, arg).full_trailer(parser).finish(streams);
              return Err(STATUS_INVALID_ARGS);
          }
          if EnvVar::flags_for(arg).contains(EnvVarFlags::READ_ONLY) {
              err_fmt!("%s: cannot overwrite read-only variable", arg)
                  .cmd(cmd)
                  .full_trailer(parser)
                  .finish(streams);
              return Err(STATUS_INVALID_ARGS);
          }
      }
  
      Ok(SUCCESS)
  }
  
  /// The read builtin. Reads from stdin and stores the values in environment variables.
  pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
      let mut buff = WString::new();
      let mut exit_res: BuiltinResult;
  
      let (mut opts, optind) = parse_cmd_opts(argv, parser, streams)?;
  
      let cmd = argv[0];
      let argv = &argv[optind..];
      let argc = argv.len();
  
      if opts.print_help {
          builtin_print_help(parser, streams, cmd);
          return Ok(SUCCESS);
      }
  
      validate_read_args(cmd, &mut opts, argv, parser, streams)?;
  
      // stdin may have been explicitly closed
      if streams.is_stdin_closed() {
          err_str!(Error::STDIN_CLOSED).cmd(cmd).finish(streams);
          return Err(STATUS_CMD_ERROR);
      }
  
      if opts.one_line {
          // --line is the same as read -d \n repeated N times
          opts.delimiter = Some(L!("\n").to_owned());
          opts.split_null = false;
          opts.shell = false;
      }
  
      let mut var_ptr = 0;
      let vars_left = |var_ptr: usize| argc - var_ptr;
      let clear_remaining_vars = |var_ptr: &mut usize| {
          while vars_left(*var_ptr) != 0 {
              parser.set_empty(argv[*var_ptr], opts.place);
              *var_ptr += 1;
          }
      };
  
      let stream_stdin_is_a_tty = streams.stdin_fd() >= 0 && isatty(streams.stdin_fd());
  
      // Normally, we either consume a line of input or all available input. But if we are reading a
      // line at a time, we need a middle ground where we only consume as many lines as we need to
      // fill the given vars.
      loop {
          buff.clear();
  
          if stream_stdin_is_a_tty && !opts.split_null {
              // Read interactively using reader_readline(). This does not support splitting on null.
              exit_res = read_interactive(
                  parser,
                  &mut buff,
                  opts.nchars,
                  opts.shell,
                  opts.silent,
                  opts.prompt.as_ref().unwrap(),
                  opts.prompt_str.as_ref().is_some_and(|ps| ps.is_empty()),
                  &opts.right_prompt,
                  opts.commandline.as_ref(),
                  streams.stdin_fd(),
              );
          } else if opts.nchars.is_none() && !stream_stdin_is_a_tty &&
              // "one_line" is implemented as reading n-times to a new line,
              // if we're chunking we could get multiple lines so we would have to advance
              // more than 1 per run through the loop. Let's skip that for now.
              !opts.one_line &&
              (streams.stdin_is_directly_redirected ||
                      unsafe {libc::lseek(streams.stdin_fd(), 0, SEEK_CUR)} != -1)
          {
              // We read in chunks when we either can seek (so we put the bytes back),
              // or we have the bytes to ourselves (because it's directly redirected).
              //
              // Note we skip seeking back even if we're directly redirected to a seekable stream,
              // under the assumption that the stream will be closed soon anyway.
              // You don't rewind VHS tapes before throwing them in the trash.
              // TODO: Do this when nchars is set by seeking back.
              exit_res = read_in_chunks(
                  streams.stdin_fd(),
                  &mut buff,
                  opts.split_null,
                  !streams.stdin_is_directly_redirected,
              );
          } else {
              exit_res = read_one_char_at_a_time(
                  streams.stdin_fd(),
                  &mut buff,
                  opts.nchars,
                  opts.split_null,
              );
          }
  
          if exit_res.is_err() {
              clear_remaining_vars(&mut var_ptr);
              return exit_res;
          }
  
          if argv.is_empty() {
              streams.out.append(&buff);
              return exit_res;
          }
  
          if let Some(token_mode) = opts.token_mode {
              let mut tok = Tokenizer::new(&buff, TOK_ACCEPT_UNFINISHED | TOK_ARGUMENT_LIST);
              let token_text = |tokenizer: &mut Tokenizer<'_>, token: &Tok| -> WString {
                  let mut text = Cow::Borrowed(tokenizer.text_of(token));
                  match token_mode {
                      TokenOutputMode::Expanded => panic!(),
                      TokenOutputMode::Raw => (),
                      TokenOutputMode::Unescaped => {
                          if let Some(unescaped) =
                              unescape_string(&text, UnescapeStringStyle::default())
                          {
                              text = Cow::Owned(unescaped);
                          }
                      }
                  }
                  text.into_owned()
              };
              if opts.array {
                  // Array mode: assign each token as a separate element of the sole var.
                  let mut tokens = vec![];
                  while let Some(t) = tok.next() {
                      tokens.push(token_text(&mut tok, &t));
                  }
  
                  parser.set_var_and_fire(argv[var_ptr], opts.place, tokens);
                  var_ptr += 1;
              } else {
                  while vars_left(var_ptr) - 1 > 0 {
                      let Some(t) = tok.next() else {
                          break;
                      };
                      let out = token_text(&mut tok, &t);
                      parser.set_var_and_fire(argv[var_ptr], opts.place, vec![out]);
                      var_ptr += 1;
                  }
  
                  // If we still have tokens, set the last variable to them.
                  if let Some(t) = tok.next() {
                      let rest = buff[t.offset()..].to_owned();
                      parser.set_var_and_fire(argv[var_ptr], opts.place, vec![rest]);
                      var_ptr += 1;
                  }
              }
              // The rest of the loop is other split-modes, we don't care about those.
              // Make sure to check the loop exit condition before continuing.
              if !opts.one_line || vars_left(var_ptr) == 0 {
                  break;
              }
              continue;
          }
  
          let mut ifs_delimiter = WString::new();
          let delimiter: &wstr = opts.delimiter.as_deref().unwrap_or_else(|| {
              ifs_delimiter = parser
                  .vars()
                  .get_unless_empty(L!("IFS"))
                  .map(|var| var.as_string())
                  .unwrap_or_default();
              &ifs_delimiter
          });
  
          if delimiter.is_empty() {
              // Every character is a separate token with one wrinkle involving non-array mode where
              // the final var gets the remaining characters as a single string.
              let x = 1.max(buff.len());
              let n_splits = if opts.array || vars_left(var_ptr) > x {
                  x
              } else {
                  vars_left(var_ptr)
              };
              let mut chars = Vec::with_capacity(n_splits);
  
              for (i, c) in buff.chars().enumerate() {
                  if opts.array || i + 1 < vars_left(var_ptr) {
                      chars.push(WString::from_chars([c]));
                  } else {
                      chars.push(buff[i..].to_owned());
                      break;
                  }
              }
  
              if opts.array {
                  // Array mode: assign each char as a separate element of the sole var.
                  parser.set_var_and_fire(argv[var_ptr], opts.place, chars);
                  var_ptr += 1;
              } else {
                  // Not array mode: assign each char to a separate var with the remainder being
                  // assigned to the last var.
                  for c in chars {
                      parser.set_var_and_fire(argv[var_ptr], opts.place, vec![c]);
                      var_ptr += 1;
                  }
              }
          } else if opts.array {
              // The user has requested the input be split into a sequence of tokens and all the
              // tokens assigned to a single var. How we do the tokenizing depends on whether the user
              // specified the delimiter string or we're using IFS.
              if opts.delimiter.is_none() {
                  // We're using IFS, so tokenize the buffer using each IFS char. This is for backward
                  // compatibility with old versions of fish.
                  let tokens = split_string_tok(&buff, delimiter, None)
                      .into_iter()
                      .map(|s| s.to_owned())
                      .collect();
                  parser.set_var_and_fire(argv[var_ptr], opts.place, tokens);
                  var_ptr += 1;
              } else {
                  // We're using a delimiter provided by the user so use the `string split` behavior.
                  let splits = split_about(&buff, delimiter, usize::MAX, false)
                      .into_iter()
                      .map(|s| s.to_owned())
                      .collect();
                  parser.set_var_and_fire(argv[var_ptr], opts.place, splits);
                  var_ptr += 1;
              }
          } else {
              // Not array mode. Split the input into tokens and assign each to the vars in sequence.
              if opts.delimiter.is_none() {
                  // We're using IFS, so tokenize the buffer using each IFS char. This is for backward
                  // compatibility with old versions of fish.
                  // Note the final variable gets any remaining text.
                  let mut var_vals: Vec<WString> =
                      split_string_tok(&buff, delimiter, Some(vars_left(var_ptr)))
                          .into_iter()
                          .map(|s| s.to_owned())
                          .collect();
                  let mut val_idx = 0;
                  while vars_left(var_ptr) != 0 {
                      let mut val = WString::new();
                      if val_idx < var_vals.len() {
                          std::mem::swap(&mut val, &mut var_vals[val_idx]);
                          val_idx += 1;
                      }
                      parser.set_var_and_fire(argv[var_ptr], opts.place, vec![val]);
                      var_ptr += 1;
                  }
              } else {
                  // We're using a delimiter provided by the user so use the `string split` behavior.
                  // We're making at most argc - 1 splits so the last variable
                  // is set to the remaining string.
                  let splits = split_about(&buff, delimiter, argc - 1, false);
                  assert!(splits.len() <= vars_left(var_ptr));
                  for split in splits {
                      parser.set_var_and_fire(argv[var_ptr], opts.place, vec![split.to_owned()]);
                      var_ptr += 1;
                  }
              }
          }
  
          if !opts.one_line || vars_left(var_ptr) == 0 {
              break;
          }
      }
  
      if !opts.array {
          // In case there were more args than splits
          clear_remaining_vars(&mut var_ptr);
      }
  
      exit_res
  }
  