From e7f87c08e1d4acf4e227d0f2b7257469e8915c4e Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Mon, 12 Jun 2017 21:34:24 -0700 Subject: [PATCH] split builtin status and read into its own module --- Makefile.in | 2 +- src/builtin.cpp | 717 +---------------------------------------- src/builtin_read.cpp | 450 ++++++++++++++++++++++++++ src/builtin_read.h | 9 + src/builtin_status.cpp | 347 ++++++++++++++++++++ src/builtin_status.h | 9 + 6 files changed, 818 insertions(+), 716 deletions(-) create mode 100644 src/builtin_read.cpp create mode 100644 src/builtin_read.h create mode 100644 src/builtin_status.cpp create mode 100644 src/builtin_status.h diff --git a/Makefile.in b/Makefile.in index 73a3c8140..e185fa80c 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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 \ diff --git a/src/builtin.cpp b/src/builtin.cpp index 9c5ffa01d..e54dd2b23 100644 --- a/src/builtin.cpp +++ b/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_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); diff --git a/src/builtin_read.cpp b/src/builtin_read.cpp new file mode 100644 index 000000000..21a33794c --- /dev/null +++ b/src/builtin_read.cpp @@ -0,0 +1,450 @@ +// Implementation of the read builtin. +#include "config.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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; +} diff --git a/src/builtin_read.h b/src/builtin_read.h new file mode 100644 index 000000000..c65fe5bfa --- /dev/null +++ b/src/builtin_read.h @@ -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 diff --git a/src/builtin_status.cpp b/src/builtin_status.cpp new file mode 100644 index 000000000..1808b963f --- /dev/null +++ b/src/builtin_status.cpp @@ -0,0 +1,347 @@ +// Implementation of the status builtin. +#include "config.h" // IWYU pragma: keep + +#include +#include + +#include + +#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_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; +} diff --git a/src/builtin_status.h b/src/builtin_status.h new file mode 100644 index 000000000..bf5eea440 --- /dev/null +++ b/src/builtin_status.h @@ -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