mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-12 21:18:53 +00:00
split builtin status and read into its own module
This commit is contained in:
parent
ef8a0c93ea
commit
e7f87c08e1
6 changed files with 818 additions and 716 deletions
|
@ -100,7 +100,7 @@ HAVE_DOXYGEN=@HAVE_DOXYGEN@
|
|||
#
|
||||
FISH_OBJS := obj/autoload.o obj/builtin.o obj/builtin_bind.o obj/builtin_block.o \
|
||||
obj/builtin_commandline.o obj/builtin_emit.o obj/builtin_functions.o \
|
||||
obj/builtin_history.o \
|
||||
obj/builtin_history.o obj/builtin_status.o obj/builtin_read.o \
|
||||
obj/builtin_complete.o obj/builtin_jobs.o obj/builtin_printf.o \
|
||||
obj/builtin_set.o obj/builtin_set_color.o obj/builtin_string.o \
|
||||
obj/builtin_test.o obj/builtin_ulimit.o obj/color.o obj/common.o \
|
||||
|
|
717
src/builtin.cpp
717
src/builtin.cpp
|
@ -45,8 +45,10 @@
|
|||
#include "builtin_history.h"
|
||||
#include "builtin_jobs.h"
|
||||
#include "builtin_printf.h"
|
||||
#include "builtin_read.h"
|
||||
#include "builtin_set.h"
|
||||
#include "builtin_set_color.h"
|
||||
#include "builtin_status.h"
|
||||
#include "builtin_string.h"
|
||||
#include "builtin_test.h"
|
||||
#include "builtin_ulimit.h"
|
||||
|
@ -55,10 +57,8 @@
|
|||
#include "env.h"
|
||||
#include "event.h"
|
||||
#include "exec.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "function.h"
|
||||
#include "highlight.h"
|
||||
#include "intern.h"
|
||||
#include "io.h"
|
||||
#include "parse_constants.h"
|
||||
|
@ -70,7 +70,6 @@
|
|||
#include "reader.h"
|
||||
#include "signal.h"
|
||||
#include "tokenizer.h"
|
||||
#include "wcstringutil.h"
|
||||
#include "wgetopt.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
|
@ -1049,718 +1048,6 @@ static int builtin_random(parser_t &parser, io_streams_t &streams, wchar_t **arg
|
|||
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.
|
||||
static int read_interactive(wcstring &buff, int nchars, bool shell, bool silent,
|
||||
const wchar_t *mode_name, const wchar_t *prompt,
|
||||
const wchar_t *right_prompt, const wchar_t *commandline) {
|
||||
int exit_res = STATUS_CMD_OK;
|
||||
const wchar_t *line;
|
||||
|
||||
reader_push(mode_name);
|
||||
reader_set_left_prompt(prompt);
|
||||
reader_set_right_prompt(right_prompt);
|
||||
if (shell) {
|
||||
reader_set_complete_function(&complete);
|
||||
reader_set_highlight_function(&highlight_shell);
|
||||
reader_set_test_function(&reader_shell_test);
|
||||
}
|
||||
// No autosuggestions or abbreviations in builtin_read.
|
||||
reader_set_allow_autosuggesting(false);
|
||||
reader_set_expand_abbreviations(false);
|
||||
reader_set_exit_on_interrupt(true);
|
||||
reader_set_silent_status(silent);
|
||||
|
||||
reader_set_buffer(commandline, wcslen(commandline));
|
||||
proc_push_interactive(1);
|
||||
|
||||
event_fire_generic(L"fish_prompt");
|
||||
line = reader_readline(nchars);
|
||||
proc_pop_interactive();
|
||||
if (line) {
|
||||
if (0 < nchars && (size_t)nchars < wcslen(line)) {
|
||||
// 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.
|
||||
buff = wcstring(line, nchars);
|
||||
} else {
|
||||
buff = wcstring(line);
|
||||
}
|
||||
} 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.
|
||||
CHECK(lseek(fd, bytes_consumed - bytes_read + 1, SEEK_CUR) != -1, STATUS_CMD_ERROR)
|
||||
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) {
|
||||
res = (unsigned char)b;
|
||||
finished = true;
|
||||
} else {
|
||||
size_t sz = mbrtowc(&res, &b, 1, &state);
|
||||
if (sz == (size_t)-1) {
|
||||
memset(&state, 0, sizeof(state));
|
||||
} else if (sz != (size_t)-2) {
|
||||
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);
|
||||
if (nchars > 0 && (size_t)nchars <= buff.size()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (buff.empty() && eof) {
|
||||
exit_res = STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
return exit_res;
|
||||
}
|
||||
|
||||
/// The read builtin. Reads from stdin and stores the values in environment variables.
|
||||
static int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
int place = ENV_USER;
|
||||
wcstring buff;
|
||||
wcstring prompt_cmd;
|
||||
const wchar_t *prompt = NULL;
|
||||
const wchar_t *prompt_str = NULL;
|
||||
const wchar_t *right_prompt = L"";
|
||||
const wchar_t *commandline = L"";
|
||||
int exit_res = STATUS_CMD_OK;
|
||||
const wchar_t *mode_name = READ_MODE_NAME;
|
||||
int nchars = 0;
|
||||
bool shell = false;
|
||||
bool array = false;
|
||||
bool silent = false;
|
||||
bool split_null = false;
|
||||
|
||||
static const wchar_t *short_options = L"ac:ghilm:n:p:suxzP:UR:";
|
||||
static const struct woption long_options[] = {{L"export", no_argument, NULL, 'x'},
|
||||
{L"global", no_argument, NULL, 'g'},
|
||||
{L"local", no_argument, NULL, 'l'},
|
||||
{L"universal", no_argument, NULL, 'U'},
|
||||
{L"unexport", no_argument, NULL, 'u'},
|
||||
{L"prompt", required_argument, NULL, 'p'},
|
||||
{L"prompt-str", required_argument, NULL, 'P'},
|
||||
{L"right-prompt", required_argument, NULL, 'R'},
|
||||
{L"command", required_argument, NULL, 'c'},
|
||||
{L"mode-name", required_argument, NULL, 'm'},
|
||||
{L"silent", no_argument, NULL, 'i'},
|
||||
{L"nchars", required_argument, NULL, 'n'},
|
||||
{L"shell", no_argument, NULL, 's'},
|
||||
{L"array", no_argument, NULL, 'a'},
|
||||
{L"null", no_argument, NULL, 'z'},
|
||||
{L"help", no_argument, NULL, 'h'},
|
||||
{NULL, 0, NULL, 0}};
|
||||
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case L'x': {
|
||||
place |= ENV_EXPORT;
|
||||
break;
|
||||
}
|
||||
case L'g': {
|
||||
place |= ENV_GLOBAL;
|
||||
break;
|
||||
}
|
||||
case L'l': {
|
||||
place |= ENV_LOCAL;
|
||||
break;
|
||||
}
|
||||
case L'U': {
|
||||
place |= ENV_UNIVERSAL;
|
||||
break;
|
||||
}
|
||||
case L'u': {
|
||||
place |= ENV_UNEXPORT;
|
||||
break;
|
||||
}
|
||||
case L'p': {
|
||||
prompt = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'P': {
|
||||
prompt_str = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'R': {
|
||||
right_prompt = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'c': {
|
||||
commandline = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'm': {
|
||||
mode_name = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'n': {
|
||||
nchars = fish_wcstoi(w.woptarg);
|
||||
if (errno) {
|
||||
if (errno == ERANGE) {
|
||||
streams.err.append_format(_(L"%ls: Argument '%ls' is out of range\n"),
|
||||
argv[0], w.woptarg);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
streams.err.append_format(_(L"%ls: Argument '%ls' must be an integer\n"),
|
||||
argv[0], w.woptarg);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
shell = true;
|
||||
break;
|
||||
}
|
||||
case 'a': {
|
||||
array = true;
|
||||
break;
|
||||
}
|
||||
case L'i': {
|
||||
silent = true;
|
||||
break;
|
||||
}
|
||||
case L'z': {
|
||||
split_null = true;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
builtin_print_help(parser, streams, argv[0], streams.out);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
streams.err.append_format(BUILTIN_ERR_MISSING, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case L'?': {
|
||||
builtin_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prompt && prompt_str) {
|
||||
streams.err.append_format(_(L"%ls: You can't specify both -p and -P\n"), argv[0]);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (prompt_str) {
|
||||
prompt_cmd = L"echo " + escape_string(prompt_str, ESCAPE_ALL);
|
||||
prompt = prompt_cmd.c_str();
|
||||
} else if (!prompt) {
|
||||
prompt = DEFAULT_READ_PROMPT;
|
||||
}
|
||||
|
||||
if ((place & ENV_UNEXPORT) && (place & ENV_EXPORT)) {
|
||||
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, argv[0]);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if ((place & ENV_LOCAL ? 1 : 0) + (place & ENV_GLOBAL ? 1 : 0) +
|
||||
(place & ENV_UNIVERSAL ? 1 : 0) >
|
||||
1) {
|
||||
streams.err.append_format(BUILTIN_ERR_GLOCAL, argv[0]);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (array && w.woptind + 1 != argc) {
|
||||
streams.err.append_format(_(L"%ls: --array option requires a single variable name.\n"),
|
||||
argv[0]);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Verify all variable names.
|
||||
for (int i = w.woptind; i < argc; i++) {
|
||||
if (!valid_var_name(argv[i])) {
|
||||
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, argv[i]);
|
||||
builtin_print_help(parser, streams, argv[0], streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Determine if the original set of conditions for interactive reads should be reinstated:
|
||||
// if (isatty(0) && streams.stdin_fd == STDIN_FILENO && !split_null) {
|
||||
int stream_stdin_is_a_tty = isatty(streams.stdin_fd);
|
||||
if (stream_stdin_is_a_tty && !split_null) {
|
||||
// We should read interactively using reader_readline(). This does not support splitting on
|
||||
// null. The call to reader_readline may change woptind, so we save and restore it.
|
||||
int saved_woptind = w.woptind;
|
||||
exit_res = read_interactive(buff, nchars, shell, silent, mode_name, prompt, right_prompt,
|
||||
commandline);
|
||||
w.woptind = saved_woptind;
|
||||
} else if (!nchars && !stream_stdin_is_a_tty && lseek(streams.stdin_fd, 0, SEEK_CUR) != -1) {
|
||||
exit_res = read_in_chunks(streams.stdin_fd, buff, split_null);
|
||||
} else {
|
||||
exit_res = read_one_char_at_a_time(streams.stdin_fd, buff, nchars, split_null);
|
||||
}
|
||||
|
||||
if (w.woptind == argc || exit_res != STATUS_CMD_OK) {
|
||||
return exit_res;
|
||||
}
|
||||
|
||||
env_var_t ifs = env_get_string(L"IFS");
|
||||
if (ifs.missing_or_empty()) {
|
||||
// Every character is a separate token.
|
||||
size_t bufflen = buff.size();
|
||||
if (array) {
|
||||
if (bufflen > 0) {
|
||||
wcstring chars(bufflen + (bufflen - 1), ARRAY_SEP);
|
||||
wcstring::iterator out = chars.begin();
|
||||
for (wcstring::const_iterator it = buff.begin(), end = buff.end(); it != end;
|
||||
++it) {
|
||||
*out = *it;
|
||||
out += 2;
|
||||
}
|
||||
env_set(argv[w.woptind], chars.c_str(), place);
|
||||
} else {
|
||||
env_set(argv[w.woptind], NULL, place);
|
||||
}
|
||||
} else { // not array
|
||||
size_t j = 0;
|
||||
for (; w.woptind + 1 < argc; ++w.woptind) {
|
||||
if (j < bufflen) {
|
||||
wchar_t buffer[2] = {buff[j++], 0};
|
||||
env_set(argv[w.woptind], buffer, place);
|
||||
} else {
|
||||
env_set(argv[w.woptind], L"", place);
|
||||
}
|
||||
}
|
||||
if (w.woptind < argc) env_set(argv[w.woptind], &buff[j], place);
|
||||
}
|
||||
} else if (array) {
|
||||
wcstring tokens;
|
||||
tokens.reserve(buff.size());
|
||||
bool empty = true;
|
||||
|
||||
for (wcstring_range loc = wcstring_tok(buff, ifs); loc.first != wcstring::npos;
|
||||
loc = wcstring_tok(buff, ifs, loc)) {
|
||||
if (!empty) tokens.push_back(ARRAY_SEP);
|
||||
tokens.append(buff, loc.first, loc.second);
|
||||
empty = false;
|
||||
}
|
||||
env_set(argv[w.woptind], empty ? NULL : tokens.c_str(), place);
|
||||
} else { // not array
|
||||
wcstring_range loc = wcstring_range(0, 0);
|
||||
|
||||
while (w.woptind < argc) {
|
||||
loc = wcstring_tok(buff, (w.woptind + 1 < argc) ? ifs : wcstring(), loc);
|
||||
env_set(argv[w.woptind], loc.first == wcstring::npos ? L"" : &buff.c_str()[loc.first],
|
||||
place);
|
||||
++w.woptind;
|
||||
}
|
||||
}
|
||||
|
||||
return exit_res;
|
||||
}
|
||||
|
||||
enum status_cmd_t {
|
||||
STATUS_IS_LOGIN = 1,
|
||||
STATUS_IS_INTERACTIVE,
|
||||
STATUS_IS_BLOCK,
|
||||
STATUS_IS_COMMAND_SUB,
|
||||
STATUS_IS_FULL_JOB_CTRL,
|
||||
STATUS_IS_INTERACTIVE_JOB_CTRL,
|
||||
STATUS_IS_NO_JOB_CTRL,
|
||||
STATUS_CURRENT_FILENAME,
|
||||
STATUS_CURRENT_FUNCTION,
|
||||
STATUS_CURRENT_LINE_NUMBER,
|
||||
STATUS_SET_JOB_CONTROL,
|
||||
STATUS_PRINT_STACK_TRACE,
|
||||
STATUS_UNDEF
|
||||
};
|
||||
// Must be sorted by string, not enum or random.
|
||||
const enum_map<status_cmd_t> status_enum_map[] = {
|
||||
{STATUS_CURRENT_FILENAME, L"current-filename"},
|
||||
{STATUS_CURRENT_FUNCTION, L"current-function"},
|
||||
{STATUS_CURRENT_LINE_NUMBER, L"current-line-number"},
|
||||
{STATUS_IS_BLOCK, L"is-block"},
|
||||
{STATUS_IS_COMMAND_SUB, L"is-command-substitution"},
|
||||
{STATUS_IS_FULL_JOB_CTRL, L"is-full-job-control"},
|
||||
{STATUS_IS_INTERACTIVE, L"is-interactive"},
|
||||
{STATUS_IS_INTERACTIVE_JOB_CTRL, L"is-interactive-job-control"},
|
||||
{STATUS_IS_LOGIN, L"is-login"},
|
||||
{STATUS_IS_NO_JOB_CTRL, L"is-no-job-control"},
|
||||
{STATUS_SET_JOB_CONTROL, L"job-control"},
|
||||
{STATUS_PRINT_STACK_TRACE, L"print-stack-trace"},
|
||||
{STATUS_UNDEF, NULL}};
|
||||
#define status_enum_map_len (sizeof status_enum_map / sizeof *status_enum_map)
|
||||
|
||||
/// Remember the status subcommand and disallow selecting more than one status subcommand.
|
||||
static bool set_status_cmd(wchar_t *const cmd, status_cmd_t *status_cmd, status_cmd_t sub_cmd,
|
||||
io_streams_t &streams) {
|
||||
if (*status_cmd != STATUS_UNDEF) {
|
||||
wchar_t err_text[1024];
|
||||
const wchar_t *subcmd_str1 = enum_to_str(*status_cmd, status_enum_map);
|
||||
const wchar_t *subcmd_str2 = enum_to_str(sub_cmd, status_enum_map);
|
||||
swprintf(err_text, sizeof(err_text) / sizeof(wchar_t),
|
||||
_(L"you cannot do both '%ls' and '%ls' in the same invocation"), subcmd_str1,
|
||||
subcmd_str2);
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2, cmd, err_text);
|
||||
return false;
|
||||
}
|
||||
|
||||
*status_cmd = sub_cmd;
|
||||
return true;
|
||||
}
|
||||
|
||||
#define CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd) \
|
||||
if (args.size() != 0) { \
|
||||
const wchar_t *subcmd_str = enum_to_str(status_cmd, status_enum_map); \
|
||||
if (!subcmd_str) subcmd_str = L"default"; \
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size()); \
|
||||
status = STATUS_INVALID_ARGS; \
|
||||
break; \
|
||||
}
|
||||
|
||||
int job_control_str_to_mode(const wchar_t *mode, wchar_t *cmd, io_streams_t &streams) {
|
||||
if (wcscmp(mode, L"full") == 0) {
|
||||
return JOB_CONTROL_ALL;
|
||||
} else if (wcscmp(mode, L"interactive") == 0) {
|
||||
return JOB_CONTROL_INTERACTIVE;
|
||||
} else if (wcscmp(mode, L"none") == 0) {
|
||||
return JOB_CONTROL_NONE;
|
||||
}
|
||||
streams.err.append_format(L"%ls: Invalid job control mode '%ls'\n", cmd, mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// The status builtin. Gives various status information on fish.
|
||||
static int builtin_status(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
status_cmd_t status_cmd = STATUS_UNDEF;
|
||||
int status = STATUS_CMD_OK;
|
||||
int new_job_control_mode = -1;
|
||||
|
||||
/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to
|
||||
/// the non-flag subcommand form. While these flags are deprecated they must be supported at
|
||||
/// least until fish 3.0 and possibly longer to avoid breaking everyones config.fish and other
|
||||
/// scripts.
|
||||
static const wchar_t *short_options = L":cbilfnhj:t";
|
||||
static const struct woption long_options[] = {
|
||||
{L"help", no_argument, NULL, 'h'},
|
||||
{L"is-command-substitution", no_argument, NULL, 'c'},
|
||||
{L"is-block", no_argument, NULL, 'b'},
|
||||
{L"is-interactive", no_argument, NULL, 'i'},
|
||||
{L"is-login", no_argument, NULL, 'l'},
|
||||
{L"is-full-job-control", no_argument, NULL, 1},
|
||||
{L"is-interactive-job-control", no_argument, NULL, 2},
|
||||
{L"is-no-job-control", no_argument, NULL, 3},
|
||||
{L"current-filename", no_argument, NULL, 'f'},
|
||||
{L"current-line-number", no_argument, NULL, 'n'},
|
||||
{L"job-control", required_argument, NULL, 'j'},
|
||||
{L"print-stack-trace", no_argument, NULL, 't'},
|
||||
{NULL, 0, NULL, 0}};
|
||||
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 1: {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_FULL_JOB_CTRL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_INTERACTIVE_JOB_CTRL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_NO_JOB_CTRL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'c': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_COMMAND_SUB, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'b': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_BLOCK, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'i': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_INTERACTIVE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'l': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_IS_LOGIN, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'f': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_CURRENT_FILENAME, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'n': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_CURRENT_LINE_NUMBER, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'j': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_SET_JOB_CONTROL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
new_job_control_mode = job_control_str_to_mode(w.woptarg, cmd, streams);
|
||||
if (new_job_control_mode == -1) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 't': {
|
||||
if (!set_status_cmd(cmd, &status_cmd, STATUS_PRINT_STACK_TRACE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
builtin_print_help(parser, streams, argv[0], streams.out);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, argv[0], argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
builtin_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a status command hasn't already been specified via a flag check the first word.
|
||||
// Note that this can be simplified after we eliminate allowing subcommands as flags.
|
||||
if (w.woptind < argc) {
|
||||
status_cmd_t subcmd = str_to_enum(argv[w.woptind], status_enum_map, status_enum_map_len);
|
||||
if (subcmd != STATUS_UNDEF) {
|
||||
if (!set_status_cmd(cmd, &status_cmd, subcmd, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
w.woptind++;
|
||||
}
|
||||
}
|
||||
|
||||
// Every argument that we haven't consumed already is an argument for a subcommand.
|
||||
const wcstring_list_t args(argv + w.woptind, argv + argc);
|
||||
|
||||
switch (status_cmd) {
|
||||
case STATUS_UNDEF: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
if (is_login) {
|
||||
streams.out.append_format(_(L"This is a login shell\n"));
|
||||
} else {
|
||||
streams.out.append_format(_(L"This is not a login shell\n"));
|
||||
}
|
||||
|
||||
streams.out.append_format(
|
||||
_(L"Job control: %ls\n"),
|
||||
job_control_mode == JOB_CONTROL_INTERACTIVE
|
||||
? _(L"Only on interactive jobs")
|
||||
: (job_control_mode == JOB_CONTROL_NONE ? _(L"Never") : _(L"Always")));
|
||||
streams.out.append(parser.stack_trace());
|
||||
break;
|
||||
}
|
||||
case STATUS_SET_JOB_CONTROL: {
|
||||
if (new_job_control_mode != -1) {
|
||||
// Flag form was used.
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
} else {
|
||||
if (args.size() != 1) {
|
||||
const wchar_t *subcmd_str = enum_to_str(status_cmd, status_enum_map);
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 1,
|
||||
args.size());
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
new_job_control_mode = job_control_str_to_mode(args[0].c_str(), cmd, streams);
|
||||
if (new_job_control_mode == -1) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
}
|
||||
job_control_mode = new_job_control_mode;
|
||||
break;
|
||||
}
|
||||
case STATUS_CURRENT_FILENAME: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
const wchar_t *fn = parser.current_filename();
|
||||
|
||||
if (!fn) fn = _(L"Standard input");
|
||||
streams.out.append_format(L"%ls\n", fn);
|
||||
break;
|
||||
}
|
||||
case STATUS_CURRENT_FUNCTION: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
const wchar_t *fn = parser.get_function_name();
|
||||
|
||||
if (!fn) fn = _(L"Not a function");
|
||||
streams.out.append_format(L"%ls\n", fn);
|
||||
break;
|
||||
}
|
||||
case STATUS_CURRENT_LINE_NUMBER: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
streams.out.append_format(L"%d\n", parser.get_lineno());
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_INTERACTIVE: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = !is_interactive_session;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_COMMAND_SUB: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = !is_subshell;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_BLOCK: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = !is_block;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_LOGIN: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = !is_login;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_FULL_JOB_CTRL: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = job_control_mode != JOB_CONTROL_ALL;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_INTERACTIVE_JOB_CTRL: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = job_control_mode != JOB_CONTROL_INTERACTIVE;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_NO_JOB_CTRL: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
status = job_control_mode != JOB_CONTROL_NONE;
|
||||
break;
|
||||
}
|
||||
case STATUS_PRINT_STACK_TRACE: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd)
|
||||
streams.out.append(parser.stack_trace());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/// The exit builtin. Calls reader_exit to exit and returns the value specified.
|
||||
static int builtin_exit(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
int argc = builtin_count_args(argv);
|
||||
|
|
450
src/builtin_read.cpp
Normal file
450
src/builtin_read.cpp
Normal file
|
@ -0,0 +1,450 @@
|
|||
// Implementation of the read builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <errno.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <wchar.h>
|
||||
#include <xlocale.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "builtin.h"
|
||||
#include "builtin_read.h"
|
||||
#include "common.h"
|
||||
#include "complete.h"
|
||||
#include "env.h"
|
||||
#include "event.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "highlight.h"
|
||||
#include "io.h"
|
||||
#include "proc.h"
|
||||
#include "reader.h"
|
||||
#include "wcstringutil.h"
|
||||
#include "wgetopt.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
struct read_opts {
|
||||
bool print_help = false;
|
||||
int place = ENV_USER;
|
||||
wcstring prompt_cmd;
|
||||
const wchar_t *prompt = NULL;
|
||||
const wchar_t *prompt_str = NULL;
|
||||
const wchar_t *right_prompt = L"";
|
||||
const wchar_t *commandline = L"";
|
||||
bool shell = false;
|
||||
bool array = false;
|
||||
bool silent = false;
|
||||
bool split_null = false;
|
||||
const wchar_t *mode_name = READ_MODE_NAME;
|
||||
int nchars = 0;
|
||||
};
|
||||
|
||||
static const wchar_t *short_options = L"ac:ghilm:n:p:suxzP:UR:";
|
||||
static const struct woption long_options[] = {{L"export", no_argument, NULL, 'x'},
|
||||
{L"global", no_argument, NULL, 'g'},
|
||||
{L"local", no_argument, NULL, 'l'},
|
||||
{L"universal", no_argument, NULL, 'U'},
|
||||
{L"unexport", no_argument, NULL, 'u'},
|
||||
{L"prompt", required_argument, NULL, 'p'},
|
||||
{L"prompt-str", required_argument, NULL, 'P'},
|
||||
{L"right-prompt", required_argument, NULL, 'R'},
|
||||
{L"command", required_argument, NULL, 'c'},
|
||||
{L"mode-name", required_argument, NULL, 'm'},
|
||||
{L"silent", no_argument, NULL, 'i'},
|
||||
{L"nchars", required_argument, NULL, 'n'},
|
||||
{L"shell", no_argument, NULL, 's'},
|
||||
{L"array", no_argument, NULL, 'a'},
|
||||
{L"null", no_argument, NULL, 'z'},
|
||||
{L"help", no_argument, NULL, 'h'},
|
||||
{NULL, 0, NULL, 0}};
|
||||
|
||||
static int parse_read_opts(struct read_opts *opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
wchar_t *cmd = argv[0];
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case L'x': {
|
||||
opts->place |= ENV_EXPORT;
|
||||
break;
|
||||
}
|
||||
case L'g': {
|
||||
opts->place |= ENV_GLOBAL;
|
||||
break;
|
||||
}
|
||||
case L'l': {
|
||||
opts->place |= ENV_LOCAL;
|
||||
break;
|
||||
}
|
||||
case L'U': {
|
||||
opts->place |= ENV_UNIVERSAL;
|
||||
break;
|
||||
}
|
||||
case L'u': {
|
||||
opts->place |= ENV_UNEXPORT;
|
||||
break;
|
||||
}
|
||||
case L'p': {
|
||||
opts->prompt = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'P': {
|
||||
opts->prompt_str = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'R': {
|
||||
opts->right_prompt = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'c': {
|
||||
opts->commandline = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'm': {
|
||||
opts->mode_name = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case L'n': {
|
||||
opts->nchars = fish_wcstoi(w.woptarg);
|
||||
if (errno) {
|
||||
if (errno == ERANGE) {
|
||||
streams.err.append_format(_(L"%ls: Argument '%ls' is out of range\n"), cmd,
|
||||
w.woptarg);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
streams.err.append_format(_(L"%ls: Argument '%ls' must be an integer\n"), cmd,
|
||||
w.woptarg);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
opts->shell = true;
|
||||
break;
|
||||
}
|
||||
case 'a': {
|
||||
opts->array = true;
|
||||
break;
|
||||
}
|
||||
case L'i': {
|
||||
opts->silent = true;
|
||||
break;
|
||||
}
|
||||
case L'z': {
|
||||
opts->split_null = true;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
opts->print_help = true;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
streams.err.append_format(BUILTIN_ERR_MISSING, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case L'?': {
|
||||
builtin_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*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.
|
||||
static int read_interactive(wcstring &buff, int nchars, bool shell, bool silent,
|
||||
const wchar_t *mode_name, const wchar_t *prompt,
|
||||
const wchar_t *right_prompt, const wchar_t *commandline) {
|
||||
int exit_res = STATUS_CMD_OK;
|
||||
const wchar_t *line;
|
||||
|
||||
reader_push(mode_name);
|
||||
reader_set_left_prompt(prompt);
|
||||
reader_set_right_prompt(right_prompt);
|
||||
if (shell) {
|
||||
reader_set_complete_function(&complete);
|
||||
reader_set_highlight_function(&highlight_shell);
|
||||
reader_set_test_function(&reader_shell_test);
|
||||
}
|
||||
// No autosuggestions or abbreviations in builtin_read.
|
||||
reader_set_allow_autosuggesting(false);
|
||||
reader_set_expand_abbreviations(false);
|
||||
reader_set_exit_on_interrupt(true);
|
||||
reader_set_silent_status(silent);
|
||||
|
||||
reader_set_buffer(commandline, wcslen(commandline));
|
||||
proc_push_interactive(1);
|
||||
|
||||
event_fire_generic(L"fish_prompt");
|
||||
line = reader_readline(nchars);
|
||||
proc_pop_interactive();
|
||||
if (line) {
|
||||
if (0 < nchars && (size_t)nchars < wcslen(line)) {
|
||||
// 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.
|
||||
buff = wcstring(line, nchars);
|
||||
} else {
|
||||
buff = wcstring(line);
|
||||
}
|
||||
} 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.
|
||||
CHECK(lseek(fd, bytes_consumed - bytes_read + 1, SEEK_CUR) != -1, STATUS_CMD_ERROR)
|
||||
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) {
|
||||
res = (unsigned char)b;
|
||||
finished = true;
|
||||
} else {
|
||||
size_t sz = mbrtowc(&res, &b, 1, &state);
|
||||
if (sz == (size_t)-1) {
|
||||
memset(&state, 0, sizeof(state));
|
||||
} else if (sz != (size_t)-2) {
|
||||
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);
|
||||
if (nchars > 0 && (size_t)nchars <= buff.size()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (buff.empty() && eof) {
|
||||
exit_res = STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
return exit_res;
|
||||
}
|
||||
|
||||
/// The read builtin. Reads from stdin and stores the values in environment variables.
|
||||
int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
wcstring buff;
|
||||
int exit_res = STATUS_CMD_OK;
|
||||
struct read_opts opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_read_opts(&opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd, streams.out);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
if (opts.prompt && opts.prompt_str) {
|
||||
streams.err.append_format(_(L"%ls: You can't specify both -p and -P\n"), cmd);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
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);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
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);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (opts.array && optind + 1 != argc) {
|
||||
streams.err.append_format(_(L"%ls: --array option requires a single variable name.\n"),
|
||||
cmd);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Verify all variable names.
|
||||
for (int i = optind; i < argc; i++) {
|
||||
if (!valid_var_name(argv[i])) {
|
||||
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, argv[i]);
|
||||
builtin_print_help(parser, streams, cmd, streams.err);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Determine if the original set of conditions for interactive reads should be reinstated:
|
||||
// if (isatty(0) && streams.stdin_fd == STDIN_FILENO && !split_null) {
|
||||
int stream_stdin_is_a_tty = isatty(streams.stdin_fd);
|
||||
if (stream_stdin_is_a_tty && !opts.split_null) {
|
||||
// We should read interactively using reader_readline(). This does not support splitting on
|
||||
// null.
|
||||
exit_res = read_interactive(buff, opts.nchars, opts.shell, opts.silent, opts.mode_name,
|
||||
opts.prompt, opts.right_prompt, opts.commandline);
|
||||
} 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 {
|
||||
exit_res = read_one_char_at_a_time(streams.stdin_fd, buff, opts.nchars, opts.split_null);
|
||||
}
|
||||
|
||||
if (optind == argc || exit_res != STATUS_CMD_OK) {
|
||||
return exit_res;
|
||||
}
|
||||
|
||||
env_var_t ifs = env_get_string(L"IFS");
|
||||
if (ifs.missing_or_empty()) {
|
||||
// Every character is a separate token.
|
||||
size_t bufflen = buff.size();
|
||||
if (opts.array) {
|
||||
if (bufflen > 0) {
|
||||
wcstring chars(bufflen + (bufflen - 1), ARRAY_SEP);
|
||||
wcstring::iterator out = chars.begin();
|
||||
for (wcstring::const_iterator it = buff.begin(), end = buff.end(); it != end;
|
||||
++it) {
|
||||
*out = *it;
|
||||
out += 2;
|
||||
}
|
||||
env_set(argv[optind], chars.c_str(), opts.place);
|
||||
} else {
|
||||
env_set(argv[optind], NULL, opts.place);
|
||||
}
|
||||
} else { // not array
|
||||
size_t j = 0;
|
||||
for (; optind + 1 < argc; ++optind) {
|
||||
if (j < bufflen) {
|
||||
wchar_t buffer[2] = {buff[j++], 0};
|
||||
env_set(argv[optind], buffer, opts.place);
|
||||
} else {
|
||||
env_set(argv[optind], L"", opts.place);
|
||||
}
|
||||
}
|
||||
if (optind < argc) env_set(argv[optind], &buff[j], opts.place);
|
||||
}
|
||||
} else if (opts.array) {
|
||||
wcstring tokens;
|
||||
tokens.reserve(buff.size());
|
||||
bool empty = true;
|
||||
|
||||
for (wcstring_range loc = wcstring_tok(buff, ifs); loc.first != wcstring::npos;
|
||||
loc = wcstring_tok(buff, ifs, loc)) {
|
||||
if (!empty) tokens.push_back(ARRAY_SEP);
|
||||
tokens.append(buff, loc.first, loc.second);
|
||||
empty = false;
|
||||
}
|
||||
env_set(argv[optind], empty ? NULL : tokens.c_str(), opts.place);
|
||||
} else { // not array
|
||||
wcstring_range loc = wcstring_range(0, 0);
|
||||
|
||||
while (optind < argc) {
|
||||
loc = wcstring_tok(buff, (optind + 1 < argc) ? ifs : wcstring(), loc);
|
||||
env_set(argv[optind], loc.first == wcstring::npos ? L"" : &buff.c_str()[loc.first],
|
||||
opts.place);
|
||||
++optind;
|
||||
}
|
||||
}
|
||||
|
||||
return exit_res;
|
||||
}
|
9
src/builtin_read.h
Normal file
9
src/builtin_read.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Prototypes for executing builtin_read function.
|
||||
#ifndef FISH_BUILTIN_READ_H
|
||||
#define FISH_BUILTIN_READ_H
|
||||
|
||||
class parser_t;
|
||||
struct io_streams_t;
|
||||
|
||||
int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv);
|
||||
#endif
|
347
src/builtin_status.cpp
Normal file
347
src/builtin_status.cpp
Normal file
|
@ -0,0 +1,347 @@
|
|||
// Implementation of the status builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <stddef.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "builtin.h"
|
||||
#include "builtin_status.h"
|
||||
#include "common.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "io.h"
|
||||
#include "parser.h"
|
||||
#include "proc.h"
|
||||
#include "wgetopt.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
enum status_cmd_t {
|
||||
STATUS_IS_LOGIN = 1,
|
||||
STATUS_IS_INTERACTIVE,
|
||||
STATUS_IS_BLOCK,
|
||||
STATUS_IS_COMMAND_SUB,
|
||||
STATUS_IS_FULL_JOB_CTRL,
|
||||
STATUS_IS_INTERACTIVE_JOB_CTRL,
|
||||
STATUS_IS_NO_JOB_CTRL,
|
||||
STATUS_CURRENT_FILENAME,
|
||||
STATUS_CURRENT_FUNCTION,
|
||||
STATUS_CURRENT_LINE_NUMBER,
|
||||
STATUS_SET_JOB_CONTROL,
|
||||
STATUS_PRINT_STACK_TRACE,
|
||||
STATUS_UNDEF
|
||||
};
|
||||
|
||||
// Must be sorted by string, not enum or random.
|
||||
const enum_map<status_cmd_t> status_enum_map[] = {
|
||||
{STATUS_CURRENT_FILENAME, L"current-filename"},
|
||||
{STATUS_CURRENT_FUNCTION, L"current-function"},
|
||||
{STATUS_CURRENT_LINE_NUMBER, L"current-line-number"},
|
||||
{STATUS_IS_BLOCK, L"is-block"},
|
||||
{STATUS_IS_COMMAND_SUB, L"is-command-substitution"},
|
||||
{STATUS_IS_FULL_JOB_CTRL, L"is-full-job-control"},
|
||||
{STATUS_IS_INTERACTIVE, L"is-interactive"},
|
||||
{STATUS_IS_INTERACTIVE_JOB_CTRL, L"is-interactive-job-control"},
|
||||
{STATUS_IS_LOGIN, L"is-login"},
|
||||
{STATUS_IS_NO_JOB_CTRL, L"is-no-job-control"},
|
||||
{STATUS_SET_JOB_CONTROL, L"job-control"},
|
||||
{STATUS_PRINT_STACK_TRACE, L"print-stack-trace"},
|
||||
{STATUS_UNDEF, NULL}};
|
||||
#define status_enum_map_len (sizeof status_enum_map / sizeof *status_enum_map)
|
||||
|
||||
#define CHECK_FOR_UNEXPECTED_STATUS_ARGS(status_cmd) \
|
||||
if (args.size() != 0) { \
|
||||
const wchar_t *subcmd_str = enum_to_str(status_cmd, status_enum_map); \
|
||||
if (!subcmd_str) subcmd_str = L"default"; \
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size()); \
|
||||
retval = STATUS_INVALID_ARGS; \
|
||||
break; \
|
||||
}
|
||||
|
||||
int job_control_str_to_mode(const wchar_t *mode, wchar_t *cmd, io_streams_t &streams) {
|
||||
if (wcscmp(mode, L"full") == 0) {
|
||||
return JOB_CONTROL_ALL;
|
||||
} else if (wcscmp(mode, L"interactive") == 0) {
|
||||
return JOB_CONTROL_INTERACTIVE;
|
||||
} else if (wcscmp(mode, L"none") == 0) {
|
||||
return JOB_CONTROL_NONE;
|
||||
}
|
||||
streams.err.append_format(L"%ls: Invalid job control mode '%ls'\n", cmd, mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct status_opts {
|
||||
bool print_help = false;
|
||||
status_cmd_t status_cmd = STATUS_UNDEF;
|
||||
int new_job_control_mode = -1;
|
||||
};
|
||||
|
||||
/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to
|
||||
/// the non-flag subcommand form. While these flags are deprecated they must be supported at
|
||||
/// least until fish 3.0 and possibly longer to avoid breaking everyones config.fish and other
|
||||
/// scripts.
|
||||
static const wchar_t *short_options = L":cbilfnhj:t";
|
||||
static const struct woption long_options[] = {{L"help", no_argument, NULL, 'h'},
|
||||
{L"is-command-substitution", no_argument, NULL, 'c'},
|
||||
{L"is-block", no_argument, NULL, 'b'},
|
||||
{L"is-interactive", no_argument, NULL, 'i'},
|
||||
{L"is-login", no_argument, NULL, 'l'},
|
||||
{L"is-full-job-control", no_argument, NULL, 1},
|
||||
{L"is-interactive-job-control", no_argument, NULL, 2},
|
||||
{L"is-no-job-control", no_argument, NULL, 3},
|
||||
{L"current-filename", no_argument, NULL, 'f'},
|
||||
{L"current-line-number", no_argument, NULL, 'n'},
|
||||
{L"job-control", required_argument, NULL, 'j'},
|
||||
{L"print-stack-trace", no_argument, NULL, 't'},
|
||||
{NULL, 0, NULL, 0}};
|
||||
|
||||
/// Remember the status subcommand and disallow selecting more than one status subcommand.
|
||||
static bool set_status_cmd(wchar_t *const cmd, status_cmd_t *status_cmd, status_cmd_t sub_cmd,
|
||||
io_streams_t &streams) {
|
||||
if (*status_cmd != STATUS_UNDEF) {
|
||||
wchar_t err_text[1024];
|
||||
const wchar_t *subcmd_str1 = enum_to_str(*status_cmd, status_enum_map);
|
||||
const wchar_t *subcmd_str2 = enum_to_str(sub_cmd, status_enum_map);
|
||||
swprintf(err_text, sizeof(err_text) / sizeof(wchar_t),
|
||||
_(L"you cannot do both '%ls' and '%ls' in the same invocation"), subcmd_str1,
|
||||
subcmd_str2);
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2, cmd, err_text);
|
||||
return false;
|
||||
}
|
||||
|
||||
*status_cmd = sub_cmd;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int parse_status_opts(struct status_opts *opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
wchar_t *cmd = argv[0];
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 1: {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_FULL_JOB_CTRL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_INTERACTIVE_JOB_CTRL,
|
||||
streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_NO_JOB_CTRL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'c': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_COMMAND_SUB, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'b': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_BLOCK, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'i': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_INTERACTIVE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'l': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_IS_LOGIN, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'f': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_CURRENT_FILENAME, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'n': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_CURRENT_LINE_NUMBER, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'j': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_SET_JOB_CONTROL, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
opts->new_job_control_mode = job_control_str_to_mode(w.woptarg, cmd, streams);
|
||||
if (opts->new_job_control_mode == -1) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 't': {
|
||||
if (!set_status_cmd(cmd, &opts->status_cmd, STATUS_PRINT_STACK_TRACE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
opts->print_help = true;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*optind = w.woptind;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
/// The status builtin. Gives various status information on fish.
|
||||
int builtin_status(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
struct status_opts opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_status_opts(&opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd, streams.out);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// If a status command hasn't already been specified via a flag check the first word.
|
||||
// Note that this can be simplified after we eliminate allowing subcommands as flags.
|
||||
if (optind < argc) {
|
||||
status_cmd_t subcmd = str_to_enum(argv[optind], status_enum_map, status_enum_map_len);
|
||||
if (subcmd != STATUS_UNDEF) {
|
||||
if (!set_status_cmd(cmd, &opts.status_cmd, subcmd, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
optind++;
|
||||
}
|
||||
}
|
||||
|
||||
// Every argument that we haven't consumed already is an argument for a subcommand.
|
||||
const wcstring_list_t args(argv + optind, argv + argc);
|
||||
|
||||
switch (opts.status_cmd) {
|
||||
case STATUS_UNDEF: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
if (is_login) {
|
||||
streams.out.append_format(_(L"This is a login shell\n"));
|
||||
} else {
|
||||
streams.out.append_format(_(L"This is not a login shell\n"));
|
||||
}
|
||||
|
||||
streams.out.append_format(
|
||||
_(L"Job control: %ls\n"),
|
||||
job_control_mode == JOB_CONTROL_INTERACTIVE
|
||||
? _(L"Only on interactive jobs")
|
||||
: (job_control_mode == JOB_CONTROL_NONE ? _(L"Never") : _(L"Always")));
|
||||
streams.out.append(parser.stack_trace());
|
||||
break;
|
||||
}
|
||||
case STATUS_SET_JOB_CONTROL: {
|
||||
if (opts.new_job_control_mode != -1) {
|
||||
// Flag form was used.
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
} else {
|
||||
if (args.size() != 1) {
|
||||
const wchar_t *subcmd_str = enum_to_str(opts.status_cmd, status_enum_map);
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 1,
|
||||
args.size());
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
opts.new_job_control_mode = job_control_str_to_mode(args[0].c_str(), cmd, streams);
|
||||
if (opts.new_job_control_mode == -1) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
}
|
||||
job_control_mode = opts.new_job_control_mode;
|
||||
break;
|
||||
}
|
||||
case STATUS_CURRENT_FILENAME: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
const wchar_t *fn = parser.current_filename();
|
||||
|
||||
if (!fn) fn = _(L"Standard input");
|
||||
streams.out.append_format(L"%ls\n", fn);
|
||||
break;
|
||||
}
|
||||
case STATUS_CURRENT_FUNCTION: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
const wchar_t *fn = parser.get_function_name();
|
||||
|
||||
if (!fn) fn = _(L"Not a function");
|
||||
streams.out.append_format(L"%ls\n", fn);
|
||||
break;
|
||||
}
|
||||
case STATUS_CURRENT_LINE_NUMBER: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
streams.out.append_format(L"%d\n", parser.get_lineno());
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_INTERACTIVE: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = !is_interactive_session;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_COMMAND_SUB: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = !is_subshell;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_BLOCK: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = !is_block;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_LOGIN: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = !is_login;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_FULL_JOB_CTRL: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = job_control_mode != JOB_CONTROL_ALL;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_INTERACTIVE_JOB_CTRL: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = job_control_mode != JOB_CONTROL_INTERACTIVE;
|
||||
break;
|
||||
}
|
||||
case STATUS_IS_NO_JOB_CTRL: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
retval = job_control_mode != JOB_CONTROL_NONE;
|
||||
break;
|
||||
}
|
||||
case STATUS_PRINT_STACK_TRACE: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
streams.out.append(parser.stack_trace());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
9
src/builtin_status.h
Normal file
9
src/builtin_status.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Prototypes for executing builtin_status function.
|
||||
#ifndef FISH_BUILTIN_STATUS_H
|
||||
#define FISH_BUILTIN_STATUS_H
|
||||
|
||||
class parser_t;
|
||||
struct io_streams_t;
|
||||
|
||||
int builtin_status(parser_t &parser, io_streams_t &streams, wchar_t **argv);
|
||||
#endif
|
Loading…
Reference in a new issue