2017-06-13 12:34:24 +08:00
|
|
|
// Implementation of the read builtin.
|
|
|
|
#include "config.h" // IWYU pragma: keep
|
|
|
|
|
2019-10-14 06:50:48 +08:00
|
|
|
#include "builtin_read.h"
|
2019-12-30 06:25:42 +08:00
|
|
|
|
2017-06-13 12:34:24 +08:00
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
#include <algorithm>
|
2019-11-19 09:11:16 +08:00
|
|
|
#include <cerrno>
|
|
|
|
#include <climits>
|
|
|
|
#include <cstddef>
|
|
|
|
#include <cstdio>
|
|
|
|
#include <cstdlib>
|
2019-10-14 06:50:48 +08:00
|
|
|
#include <cstring>
|
|
|
|
#include <cwchar>
|
2017-06-13 12:34:24 +08:00
|
|
|
#include <memory>
|
2018-04-18 02:28:56 +08:00
|
|
|
#include <numeric>
|
2017-06-13 12:34:24 +08:00
|
|
|
#include <string>
|
2017-08-06 06:08:39 +08:00
|
|
|
#include <vector>
|
2017-06-13 12:34:24 +08:00
|
|
|
|
|
|
|
#include "builtin.h"
|
|
|
|
#include "common.h"
|
|
|
|
#include "complete.h"
|
|
|
|
#include "env.h"
|
|
|
|
#include "event.h"
|
|
|
|
#include "fallback.h" // IWYU pragma: keep
|
|
|
|
#include "highlight.h"
|
2017-07-01 12:03:05 +08:00
|
|
|
#include "history.h"
|
2017-06-13 12:34:24 +08:00
|
|
|
#include "io.h"
|
2018-09-11 12:27:25 +08:00
|
|
|
#include "parser.h"
|
2017-06-13 12:34:24 +08:00
|
|
|
#include "proc.h"
|
|
|
|
#include "reader.h"
|
|
|
|
#include "wcstringutil.h"
|
|
|
|
#include "wgetopt.h"
|
|
|
|
#include "wutil.h" // IWYU pragma: keep
|
|
|
|
|
2017-06-16 08:57:37 +08:00
|
|
|
struct read_cmd_opts_t {
|
2017-06-13 12:34:24 +08:00
|
|
|
bool print_help = false;
|
|
|
|
int place = ENV_USER;
|
|
|
|
wcstring prompt_cmd;
|
2019-11-19 10:34:50 +08:00
|
|
|
const wchar_t *prompt = nullptr;
|
|
|
|
const wchar_t *prompt_str = nullptr;
|
2017-06-13 12:34:24 +08:00
|
|
|
const wchar_t *right_prompt = L"";
|
|
|
|
const wchar_t *commandline = L"";
|
2017-07-27 21:06:01 +08:00
|
|
|
// If a delimiter was given. Used to distinguish between the default
|
|
|
|
// empty string and a given empty delimiter.
|
|
|
|
bool have_delimiter = false;
|
|
|
|
wcstring delimiter;
|
2019-11-30 03:05:31 +08:00
|
|
|
bool tokenize = false;
|
2017-06-13 12:34:24 +08:00
|
|
|
bool shell = false;
|
|
|
|
bool array = false;
|
|
|
|
bool silent = false;
|
|
|
|
bool split_null = false;
|
2017-10-13 00:44:14 +08:00
|
|
|
bool to_stdout = false;
|
2017-06-13 12:34:24 +08:00
|
|
|
int nchars = 0;
|
2018-04-17 19:57:33 +08:00
|
|
|
bool one_line = false;
|
2017-06-13 12:34:24 +08:00
|
|
|
};
|
|
|
|
|
2019-11-30 03:05:31 +08:00
|
|
|
static const wchar_t *const short_options = L":ac:d:ghiLlm:n:p:sStuxzP:UR:LB";
|
2019-11-19 10:34:50 +08:00
|
|
|
static const struct woption long_options[] = {{L"array", no_argument, nullptr, 'a'},
|
|
|
|
{L"command", required_argument, nullptr, 'c'},
|
|
|
|
{L"delimiter", required_argument, nullptr, 'd'},
|
|
|
|
{L"export", no_argument, nullptr, 'x'},
|
|
|
|
{L"global", no_argument, nullptr, 'g'},
|
|
|
|
{L"help", no_argument, nullptr, 'h'},
|
|
|
|
{L"line", no_argument, nullptr, 'L'},
|
|
|
|
{L"list", no_argument, nullptr, 'a'},
|
|
|
|
{L"local", no_argument, nullptr, 'l'},
|
|
|
|
{L"nchars", required_argument, nullptr, 'n'},
|
|
|
|
{L"null", no_argument, nullptr, 'z'},
|
|
|
|
{L"prompt", required_argument, nullptr, 'p'},
|
|
|
|
{L"prompt-str", required_argument, nullptr, 'P'},
|
|
|
|
{L"right-prompt", required_argument, nullptr, 'R'},
|
|
|
|
{L"shell", no_argument, nullptr, 'S'},
|
|
|
|
{L"silent", no_argument, nullptr, 's'},
|
2019-11-30 03:05:31 +08:00
|
|
|
{L"tokenize", no_argument, nullptr, 't'},
|
2019-11-19 10:34:50 +08:00
|
|
|
{L"unexport", no_argument, nullptr, 'u'},
|
|
|
|
{L"universal", no_argument, nullptr, 'U'},
|
|
|
|
{nullptr, 0, nullptr, 0}};
|
2017-06-13 12:34:24 +08:00
|
|
|
|
2017-06-16 08:57:37 +08:00
|
|
|
static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
|
2017-06-14 04:03:32 +08:00
|
|
|
int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
2017-06-13 12:34:24 +08:00
|
|
|
wchar_t *cmd = argv[0];
|
|
|
|
int opt;
|
|
|
|
wgetopter_t w;
|
2019-11-19 10:34:50 +08:00
|
|
|
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
|
2017-06-13 12:34:24 +08:00
|
|
|
switch (opt) {
|
2018-04-17 19:57:33 +08:00
|
|
|
case 'a': {
|
|
|
|
opts.array = true;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'c': {
|
|
|
|
opts.commandline = w.woptarg;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case 'd': {
|
|
|
|
opts.have_delimiter = true;
|
|
|
|
opts.delimiter = w.woptarg;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case 'i': {
|
2019-05-05 18:09:25 +08:00
|
|
|
streams.err.append_format(_(L"%ls: usage of -i for --silent is deprecated. Please "
|
|
|
|
L"use -s or --silent instead.\n"),
|
|
|
|
cmd);
|
2018-04-17 19:57:33 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
2017-06-13 12:34:24 +08:00
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'g': {
|
|
|
|
opts.place |= ENV_GLOBAL;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case 'h': {
|
|
|
|
opts.print_help = true;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'L': {
|
|
|
|
opts.one_line = true;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'l': {
|
|
|
|
opts.place |= ENV_LOCAL;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case L'n': {
|
2017-06-16 08:57:37 +08:00
|
|
|
opts.nchars = fish_wcstoi(w.woptarg);
|
2017-06-13 12:34:24 +08:00
|
|
|
if (errno) {
|
|
|
|
if (errno == ERANGE) {
|
|
|
|
streams.err.append_format(_(L"%ls: Argument '%ls' is out of range\n"), cmd,
|
|
|
|
w.woptarg);
|
2019-03-27 02:13:01 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2019-09-18 13:00:08 +08:00
|
|
|
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg);
|
2019-03-27 02:13:01 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'P': {
|
|
|
|
opts.prompt_str = w.woptarg;
|
2017-07-27 21:06:01 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'p': {
|
|
|
|
opts.prompt = w.woptarg;
|
|
|
|
break;
|
2018-03-10 01:48:51 +08:00
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'R': {
|
|
|
|
opts.right_prompt = w.woptarg;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case 's': {
|
|
|
|
opts.silent = true;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-03-10 01:48:09 +08:00
|
|
|
case L'S': {
|
|
|
|
opts.shell = true;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2019-11-30 03:05:31 +08:00
|
|
|
case L't': {
|
|
|
|
opts.tokenize = true;
|
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'U': {
|
|
|
|
opts.place |= ENV_UNIVERSAL;
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
2018-04-17 19:57:33 +08:00
|
|
|
case L'u': {
|
|
|
|
opts.place |= ENV_UNEXPORT;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case L'x': {
|
|
|
|
opts.place |= ENV_EXPORT;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case L'z': {
|
|
|
|
opts.split_null = true;
|
2017-06-15 13:12:29 +08:00
|
|
|
break;
|
2017-06-13 12:34:24 +08:00
|
|
|
}
|
|
|
|
case ':': {
|
2017-07-02 05:03:47 +08:00
|
|
|
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
case L'?': {
|
2017-06-15 03:26:05 +08:00
|
|
|
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
DIE("unexpected retval from wgetopt_long");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
*optind = w.woptind;
|
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
2019-03-25 17:26:50 +08:00
|
|
|
static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool shell, bool silent,
|
2017-07-01 12:03:05 +08:00
|
|
|
const wchar_t *prompt, const wchar_t *right_prompt,
|
|
|
|
const wchar_t *commandline) {
|
2017-06-13 12:34:24 +08:00
|
|
|
int exit_res = STATUS_CMD_OK;
|
|
|
|
|
2020-08-04 05:53:09 +08:00
|
|
|
// Construct a configuration.
|
|
|
|
reader_config_t conf;
|
|
|
|
conf.complete_ok = shell;
|
|
|
|
conf.highlight_ok = shell;
|
|
|
|
conf.syntax_check_ok = shell;
|
2017-07-02 04:23:24 +08:00
|
|
|
|
2017-06-13 12:34:24 +08:00
|
|
|
// No autosuggestions or abbreviations in builtin_read.
|
2020-08-04 05:53:09 +08:00
|
|
|
conf.autosuggest_ok = false;
|
|
|
|
conf.expand_abbrev_ok = false;
|
|
|
|
|
|
|
|
conf.exit_on_interrupt = true;
|
|
|
|
conf.in_silent_mode = silent;
|
|
|
|
|
|
|
|
conf.left_prompt_cmd = prompt;
|
|
|
|
conf.right_prompt_cmd = right_prompt;
|
|
|
|
|
|
|
|
// Don't keep history.
|
|
|
|
reader_push(parser, wcstring{}, std::move(conf));
|
|
|
|
reader_get_history()->resolve_pending();
|
2017-06-13 12:34:24 +08:00
|
|
|
|
2019-03-13 05:06:01 +08:00
|
|
|
reader_set_buffer(commandline, std::wcslen(commandline));
|
2019-05-28 05:52:48 +08:00
|
|
|
scoped_push<bool> interactive{&parser.libdata().is_interactive, true};
|
2017-06-13 12:34:24 +08:00
|
|
|
|
2020-05-27 04:24:31 +08:00
|
|
|
event_fire_generic(parser, L"fish_read");
|
2019-02-25 05:59:49 +08:00
|
|
|
auto mline = reader_readline(nchars);
|
2019-05-28 05:52:48 +08:00
|
|
|
interactive.restore();
|
2019-02-25 05:59:49 +08:00
|
|
|
if (mline) {
|
|
|
|
buff = mline.acquire();
|
2019-11-19 09:08:16 +08:00
|
|
|
if (nchars > 0 && static_cast<size_t>(nchars) < buff.size()) {
|
2017-06-13 12:34:24 +08:00
|
|
|
// 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.
|
2019-02-25 05:59:49 +08:00
|
|
|
buff.resize(nchars);
|
2017-06-13 12:34:24 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
exit_res = STATUS_CMD_ERROR;
|
|
|
|
}
|
|
|
|
reader_pop();
|
|
|
|
return 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.
|
|
|
|
#define READ_CHUNK_SIZE 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.
|
|
|
|
static int read_in_chunks(int fd, wcstring &buff, bool split_null) {
|
|
|
|
int exit_res = STATUS_CMD_OK;
|
|
|
|
std::string str;
|
|
|
|
bool eof = false;
|
|
|
|
bool finished = false;
|
|
|
|
|
|
|
|
while (!finished) {
|
|
|
|
char inbuf[READ_CHUNK_SIZE];
|
|
|
|
long bytes_read = read_blocked(fd, inbuf, READ_CHUNK_SIZE);
|
|
|
|
|
|
|
|
if (bytes_read <= 0) {
|
|
|
|
eof = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const char *end = std::find(inbuf, inbuf + bytes_read, split_null ? L'\0' : L'\n');
|
|
|
|
long bytes_consumed = end - inbuf; // must be signed for use in lseek
|
|
|
|
assert(bytes_consumed <= bytes_read);
|
|
|
|
str.append(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.
|
2019-05-28 08:24:19 +08:00
|
|
|
if (lseek(fd, bytes_consumed - bytes_read + 1, SEEK_CUR) == -1) {
|
|
|
|
wperror(L"lseek");
|
|
|
|
return STATUS_CMD_ERROR;
|
|
|
|
}
|
2017-06-13 12:34:24 +08:00
|
|
|
finished = true;
|
|
|
|
} else if (str.size() > read_byte_limit) {
|
|
|
|
exit_res = STATUS_READ_TOO_MUCH;
|
|
|
|
finished = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
buff = str2wcstring(str);
|
|
|
|
if (buff.empty() && eof) {
|
|
|
|
exit_res = STATUS_CMD_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 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.
|
|
|
|
static int read_one_char_at_a_time(int fd, wcstring &buff, int nchars, bool split_null) {
|
|
|
|
int exit_res = STATUS_CMD_OK;
|
|
|
|
bool eof = false;
|
|
|
|
size_t nbytes = 0;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
bool finished = false;
|
|
|
|
wchar_t res = 0;
|
|
|
|
mbstate_t state = {};
|
|
|
|
|
|
|
|
while (!finished) {
|
|
|
|
char b;
|
|
|
|
if (read_blocked(fd, &b, 1) <= 0) {
|
|
|
|
eof = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
nbytes++;
|
|
|
|
if (MB_CUR_MAX == 1) {
|
2019-11-19 09:08:16 +08:00
|
|
|
res = static_cast<unsigned char>(b);
|
2017-06-13 12:34:24 +08:00
|
|
|
finished = true;
|
|
|
|
} else {
|
2019-03-13 05:06:01 +08:00
|
|
|
size_t sz = std::mbrtowc(&res, &b, 1, &state);
|
2019-11-19 09:08:16 +08:00
|
|
|
if (sz == static_cast<size_t>(-1)) {
|
2019-03-13 06:07:07 +08:00
|
|
|
std::memset(&state, 0, sizeof(state));
|
2019-11-19 09:08:16 +08:00
|
|
|
} else if (sz != static_cast<size_t>(-2)) {
|
2017-06-13 12:34:24 +08:00
|
|
|
finished = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nbytes > read_byte_limit) {
|
|
|
|
exit_res = STATUS_READ_TOO_MUCH;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (eof) break;
|
|
|
|
if (!split_null && res == L'\n') break;
|
|
|
|
if (split_null && res == L'\0') break;
|
|
|
|
|
|
|
|
buff.push_back(res);
|
2019-11-19 09:08:16 +08:00
|
|
|
if (nchars > 0 && static_cast<size_t>(nchars) <= buff.size()) {
|
2017-06-13 12:34:24 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buff.empty() && eof) {
|
|
|
|
exit_res = STATUS_CMD_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
return exit_res;
|
|
|
|
}
|
|
|
|
|
2017-07-20 11:22:34 +08:00
|
|
|
/// Validate the arguments given to `read` and provide defaults where needed.
|
|
|
|
static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int argc,
|
|
|
|
const wchar_t *const *argv, parser_t &parser, io_streams_t &streams) {
|
2017-06-13 12:34:24 +08:00
|
|
|
if (opts.prompt && opts.prompt_str) {
|
2019-05-05 18:09:25 +08:00
|
|
|
streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
|
|
|
L"-p", L"-P");
|
2019-03-27 02:13:01 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2018-04-17 19:57:33 +08:00
|
|
|
if (opts.have_delimiter && opts.one_line) {
|
2019-05-05 18:09:25 +08:00
|
|
|
streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
|
|
|
L"--delimiter", L"--line");
|
2018-04-17 19:57:33 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
if (opts.one_line && opts.split_null) {
|
2019-05-05 18:09:25 +08:00
|
|
|
streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
|
|
|
L"-z", L"--line");
|
2018-04-17 19:57:33 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2017-06-13 12:34:24 +08:00
|
|
|
if (opts.prompt_str) {
|
|
|
|
opts.prompt_cmd = L"echo " + escape_string(opts.prompt_str, ESCAPE_ALL);
|
|
|
|
opts.prompt = opts.prompt_cmd.c_str();
|
|
|
|
} else if (!opts.prompt) {
|
|
|
|
opts.prompt = DEFAULT_READ_PROMPT;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((opts.place & ENV_UNEXPORT) && (opts.place & ENV_EXPORT)) {
|
|
|
|
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd);
|
2019-03-27 02:13:01 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((opts.place & ENV_LOCAL ? 1 : 0) + (opts.place & ENV_GLOBAL ? 1 : 0) +
|
|
|
|
(opts.place & ENV_UNIVERSAL ? 1 : 0) >
|
|
|
|
1) {
|
|
|
|
streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd);
|
2019-03-27 02:13:01 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2018-04-11 03:46:43 +08:00
|
|
|
if (!opts.array && argc < 1 && !opts.to_stdout) {
|
2017-07-20 11:22:34 +08:00
|
|
|
streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, argc);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (opts.array && argc != 1) {
|
|
|
|
streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2018-04-11 03:46:43 +08:00
|
|
|
if (opts.to_stdout && argc > 0) {
|
|
|
|
streams.err.append_format(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, 0, argc);
|
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2019-11-30 03:05:31 +08:00
|
|
|
if (opts.tokenize && opts.have_delimiter) {
|
2019-12-30 06:25:42 +08:00
|
|
|
streams.err.append_format(BUILTIN_ERR_COMBO2, cmd,
|
|
|
|
L"--delimiter and --tokenize can not be used together");
|
2019-11-30 03:05:31 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (opts.tokenize && opts.one_line) {
|
2019-12-30 06:25:42 +08:00
|
|
|
streams.err.append_format(BUILTIN_ERR_COMBO2, cmd,
|
|
|
|
L"--line and --tokenize can not be used together");
|
2019-11-30 03:05:31 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
|
2017-06-13 12:34:24 +08:00
|
|
|
// Verify all variable names.
|
2017-07-20 11:22:34 +08:00
|
|
|
for (int i = 0; i < argc; i++) {
|
2017-06-13 12:34:24 +08:00
|
|
|
if (!valid_var_name(argv[i])) {
|
|
|
|
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, argv[i]);
|
2019-03-27 02:13:01 +08:00
|
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
2017-06-13 12:34:24 +08:00
|
|
|
return STATUS_INVALID_ARGS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-20 11:22:34 +08:00
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The read builtin. Reads from stdin and stores the values in environment variables.
|
2020-07-19 01:25:43 +08:00
|
|
|
maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
2017-07-20 11:22:34 +08:00
|
|
|
wchar_t *cmd = argv[0];
|
|
|
|
int argc = builtin_count_args(argv);
|
|
|
|
wcstring buff;
|
|
|
|
int exit_res = STATUS_CMD_OK;
|
|
|
|
read_cmd_opts_t opts;
|
|
|
|
|
|
|
|
int optind;
|
|
|
|
int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
|
|
|
if (retval != STATUS_CMD_OK) return retval;
|
2017-10-13 00:44:14 +08:00
|
|
|
if (!opts.to_stdout) {
|
2017-10-11 13:22:42 +08:00
|
|
|
argc -= optind;
|
|
|
|
argv += optind;
|
|
|
|
}
|
2017-07-20 11:22:34 +08:00
|
|
|
|
2018-04-11 03:46:43 +08:00
|
|
|
if (argc == 0) {
|
|
|
|
opts.to_stdout = true;
|
|
|
|
}
|
|
|
|
|
2017-07-20 11:22:34 +08:00
|
|
|
if (opts.print_help) {
|
2019-10-20 17:38:17 +08:00
|
|
|
builtin_print_help(parser, streams, cmd);
|
2017-07-20 11:22:34 +08:00
|
|
|
return STATUS_CMD_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
retval = validate_read_args(cmd, opts, argc, argv, parser, streams);
|
|
|
|
if (retval != STATUS_CMD_OK) return retval;
|
|
|
|
|
2018-11-20 23:56:15 +08:00
|
|
|
if (opts.one_line) {
|
2018-04-18 02:28:56 +08:00
|
|
|
// --line is the same as read -d \n repeated N times
|
2018-04-17 19:57:33 +08:00
|
|
|
opts.have_delimiter = true;
|
|
|
|
opts.delimiter = L"\n";
|
|
|
|
opts.split_null = false;
|
|
|
|
opts.shell = false;
|
|
|
|
}
|
|
|
|
|
2019-05-05 18:09:25 +08:00
|
|
|
wchar_t *const *var_ptr = argv;
|
|
|
|
auto vars_left = [&]() { return argv + argc - var_ptr; };
|
|
|
|
auto clear_remaining_vars = [&]() {
|
2018-04-18 02:28:56 +08:00
|
|
|
while (vars_left()) {
|
2018-09-11 12:27:25 +08:00
|
|
|
parser.vars().set_empty(*var_ptr, opts.place);
|
2018-04-18 02:28:56 +08:00
|
|
|
++var_ptr;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-05-05 18:09:25 +08:00
|
|
|
// 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.
|
2018-04-18 02:28:56 +08:00
|
|
|
do {
|
|
|
|
buff.clear();
|
|
|
|
|
2019-05-05 18:09:25 +08:00
|
|
|
// TODO: Determine if the original set of conditions for interactive reads should be
|
|
|
|
// reinstated: if (isatty(0) && streams.stdin_fd == STDIN_FILENO && !split_null) {
|
2018-04-18 02:28:56 +08:00
|
|
|
int stream_stdin_is_a_tty = isatty(streams.stdin_fd);
|
|
|
|
if (stream_stdin_is_a_tty && !opts.split_null) {
|
|
|
|
// Read interactively using reader_readline(). This does not support splitting on null.
|
2019-03-25 17:26:50 +08:00
|
|
|
exit_res = read_interactive(parser, buff, opts.nchars, opts.shell, opts.silent,
|
|
|
|
opts.prompt, opts.right_prompt, opts.commandline);
|
2018-04-18 02:28:56 +08:00
|
|
|
} else if (!opts.nchars && !stream_stdin_is_a_tty &&
|
|
|
|
lseek(streams.stdin_fd, 0, SEEK_CUR) != -1) {
|
|
|
|
exit_res = read_in_chunks(streams.stdin_fd, buff, opts.split_null);
|
|
|
|
} else {
|
2019-05-05 18:09:25 +08:00
|
|
|
exit_res =
|
|
|
|
read_one_char_at_a_time(streams.stdin_fd, buff, opts.nchars, opts.split_null);
|
2018-04-18 02:28:56 +08:00
|
|
|
}
|
2017-06-13 12:34:24 +08:00
|
|
|
|
2018-04-18 02:28:56 +08:00
|
|
|
if (exit_res != STATUS_CMD_OK) {
|
|
|
|
clear_remaining_vars();
|
|
|
|
return exit_res;
|
|
|
|
}
|
2017-10-07 06:15:04 +08:00
|
|
|
|
2018-04-18 02:28:56 +08:00
|
|
|
if (opts.to_stdout) {
|
|
|
|
streams.out.append(buff);
|
|
|
|
return exit_res;
|
|
|
|
}
|
2017-08-06 06:08:39 +08:00
|
|
|
|
2019-11-30 03:05:31 +08:00
|
|
|
if (opts.tokenize) {
|
|
|
|
tokenizer_t tok{buff.c_str(), TOK_ACCEPT_UNFINISHED};
|
|
|
|
wcstring out;
|
|
|
|
if (opts.array) {
|
|
|
|
// Array mode: assign each token as a separate element of the sole var.
|
|
|
|
wcstring_list_t tokens;
|
|
|
|
while (auto t = tok.next()) {
|
|
|
|
auto text = tok.text_of(*t);
|
|
|
|
if (unescape_string(text, &out, UNESCAPE_DEFAULT)) {
|
|
|
|
tokens.push_back(out);
|
|
|
|
} else {
|
|
|
|
tokens.push_back(text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, std::move(tokens));
|
2019-11-30 03:05:31 +08:00
|
|
|
} else {
|
|
|
|
maybe_t<tok_t> t;
|
|
|
|
while ((vars_left() - 1 > 0) && (t = tok.next())) {
|
|
|
|
auto text = tok.text_of(*t);
|
|
|
|
if (unescape_string(text, &out, UNESCAPE_DEFAULT)) {
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, out);
|
2019-11-30 03:05:31 +08:00
|
|
|
} else {
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, text);
|
2019-11-30 03:05:31 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we still have tokens, set the last variable to them.
|
2019-12-04 05:00:27 +08:00
|
|
|
if ((t = tok.next())) {
|
2019-11-30 03:05:31 +08:00
|
|
|
wcstring rest = wcstring(buff, t->offset);
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, std::move(rest));
|
2019-11-30 03:05:31 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// The rest of the loop is other split-modes, we don't care about those.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-04-18 02:28:56 +08:00
|
|
|
if (!opts.have_delimiter) {
|
2018-09-25 10:26:46 +08:00
|
|
|
auto ifs = parser.vars().get(L"IFS");
|
2018-04-18 02:28:56 +08:00
|
|
|
if (!ifs.missing_or_empty()) opts.delimiter = ifs->as_string();
|
2017-08-06 06:08:39 +08:00
|
|
|
}
|
|
|
|
|
2018-04-18 02:28:56 +08:00
|
|
|
if (opts.delimiter.empty()) {
|
2019-01-17 05:44:10 +08:00
|
|
|
// 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.
|
2018-04-18 02:28:56 +08:00
|
|
|
size_t x = std::max(static_cast<size_t>(1), buff.size());
|
2019-05-05 18:09:25 +08:00
|
|
|
size_t n_splits =
|
|
|
|
(opts.array || static_cast<size_t>(vars_left()) > x) ? x : vars_left();
|
2018-04-18 02:28:56 +08:00
|
|
|
wcstring_list_t chars;
|
|
|
|
chars.reserve(n_splits);
|
2019-01-17 05:44:10 +08:00
|
|
|
|
2017-07-20 11:22:34 +08:00
|
|
|
int i = 0;
|
2018-04-18 02:28:56 +08:00
|
|
|
for (auto it = buff.begin(), end = buff.end(); it != end; ++i, ++it) {
|
|
|
|
if (opts.array || i + 1 < vars_left()) {
|
|
|
|
chars.emplace_back(1, *it);
|
2017-06-13 12:34:24 +08:00
|
|
|
} else {
|
2018-04-18 02:28:56 +08:00
|
|
|
chars.emplace_back(it, buff.end());
|
|
|
|
break;
|
2017-06-13 12:34:24 +08:00
|
|
|
}
|
|
|
|
}
|
2017-08-06 06:08:39 +08:00
|
|
|
|
2018-04-18 02:28:56 +08:00
|
|
|
if (opts.array) {
|
|
|
|
// Array mode: assign each char as a separate element of the sole var.
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, chars);
|
2017-08-06 06:08:39 +08:00
|
|
|
} else {
|
2019-01-17 05:44:10 +08:00
|
|
|
// Not array mode: assign each char to a separate var with the remainder being
|
|
|
|
// assigned to the last var.
|
2019-11-20 05:46:47 +08:00
|
|
|
for (const auto &c : chars) {
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, c);
|
2018-04-18 02:28:56 +08:00
|
|
|
}
|
2017-08-06 06:08:39 +08:00
|
|
|
}
|
2018-04-18 02:28:56 +08:00
|
|
|
} else if (opts.array) {
|
2019-01-17 05:44:10 +08:00
|
|
|
// 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.
|
2018-04-18 02:28:56 +08:00
|
|
|
if (!opts.have_delimiter) {
|
|
|
|
// We're using IFS, so tokenize the buffer using each IFS char. This is for backward
|
|
|
|
// compatibility with old versions of fish.
|
|
|
|
wcstring_list_t tokens;
|
|
|
|
|
|
|
|
for (wcstring_range loc = wcstring_tok(buff, opts.delimiter);
|
|
|
|
loc.first != wcstring::npos; loc = wcstring_tok(buff, opts.delimiter, loc)) {
|
|
|
|
tokens.emplace_back(wcstring(buff, loc.first, loc.second));
|
|
|
|
}
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, tokens);
|
2018-04-18 02:28:56 +08:00
|
|
|
} else {
|
|
|
|
// We're using a delimiter provided by the user so use the `string split` behavior.
|
|
|
|
wcstring_list_t splits;
|
|
|
|
split_about(buff.begin(), buff.end(), opts.delimiter.begin(), opts.delimiter.end(),
|
|
|
|
&splits);
|
2017-07-27 21:06:01 +08:00
|
|
|
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, splits);
|
2017-07-27 21:06:01 +08:00
|
|
|
}
|
|
|
|
} else {
|
2018-04-18 02:28:56 +08:00
|
|
|
// Not array mode. Split the input into tokens and assign each to the vars in sequence.
|
|
|
|
if (!opts.have_delimiter) {
|
|
|
|
// We're using IFS, so tokenize the buffer using each IFS char. This is for backward
|
|
|
|
// compatibility with old versions of fish.
|
|
|
|
wcstring_range loc = wcstring_range(0, 0);
|
|
|
|
while (vars_left()) {
|
|
|
|
wcstring substr;
|
|
|
|
loc = wcstring_tok(buff, (vars_left() > 1) ? opts.delimiter : wcstring(), loc);
|
|
|
|
if (loc.first != wcstring::npos) {
|
|
|
|
substr = wcstring(buff, loc.first, loc.second);
|
|
|
|
}
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, substr);
|
2018-04-18 02:28:56 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// We're using a delimiter provided by the user so use the `string split` behavior.
|
|
|
|
wcstring_list_t splits;
|
|
|
|
// We're making at most argc - 1 splits so the last variable
|
|
|
|
// is set to the remaining string.
|
|
|
|
split_about(buff.begin(), buff.end(), opts.delimiter.begin(), opts.delimiter.end(),
|
|
|
|
&splits, argc - 1);
|
2020-04-09 07:56:59 +08:00
|
|
|
assert(splits.size() <= static_cast<size_t>(vars_left()));
|
2018-04-18 02:28:56 +08:00
|
|
|
for (const auto &split : splits) {
|
2020-03-08 11:44:58 +08:00
|
|
|
parser.set_var_and_fire(*var_ptr++, opts.place, split);
|
2017-08-06 06:08:39 +08:00
|
|
|
}
|
2017-07-27 21:06:01 +08:00
|
|
|
}
|
2017-06-13 12:34:24 +08:00
|
|
|
}
|
2018-04-18 02:28:56 +08:00
|
|
|
} while (opts.one_line && vars_left());
|
|
|
|
|
|
|
|
if (!opts.array) {
|
|
|
|
// In case there were more args than splits
|
|
|
|
clear_remaining_vars();
|
2017-06-13 12:34:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return exit_res;
|
|
|
|
}
|