fish-shell/src/builtin_commandline.cpp
ridiculousfish a32248277f Make commandline state thread safe
Today the reader exposes its internals directly, e.g. to the commandline
builtin. This is of course not thread safe. For example in concurrent
execution, running `commandline` twice in separate threads would cause a
race and likely a crash.

Fix this by factoring all the commandline state into a new type
'commandline_state_t'. Make it a singleton (there is only one command
line
after all) and protect it with a lock.

No user visible change here.
2021-07-21 11:51:46 -07:00

446 lines
16 KiB
C++

// Functions used for implementing the commandline builtin.
#include "config.h" // IWYU pragma: keep
#include <cerrno>
#include <cstddef>
#include <cstdlib>
#include <cwchar>
#include "builtin.h"
#include "common.h"
#include "fallback.h" // IWYU pragma: keep
#include "input.h"
#include "io.h"
#include "parse_util.h"
#include "parser.h"
#include "proc.h"
#include "reader.h"
#include "tokenizer.h"
#include "wgetopt.h"
#include "wutil.h" // IWYU pragma: keep
class parser_t;
/// Which part of the comandbuffer are we operating on.
enum {
STRING_MODE = 1, // operate on entire buffer
JOB_MODE, // operate on job under cursor
PROCESS_MODE, // operate on process under cursor
TOKEN_MODE // operate on token under cursor
};
/// For text insertion, how should it be done.
enum {
REPLACE_MODE = 1, // replace current text
INSERT_MODE, // insert at cursor position
APPEND_MODE // insert at end of current token/command/buffer
};
/// Handle a single readline_cmd_t command out-of-band.
void reader_handle_command(readline_cmd_t cmd);
/// Replace/append/insert the selection with/at/after the specified string.
///
/// \param begin beginning of selection
/// \param end end of selection
/// \param insert the string to insert
/// \param append_mode can be one of REPLACE_MODE, INSERT_MODE or APPEND_MODE, affects the way the
/// test update is performed
/// \param buff the original command line buffer
/// \param cursor_pos the position of the cursor in the command line
static void replace_part(const wchar_t *begin, const wchar_t *end, const wchar_t *insert,
int append_mode, const wchar_t *buff, size_t cursor_pos) {
size_t out_pos = cursor_pos;
wcstring out;
out.append(buff, begin - buff);
switch (append_mode) {
case REPLACE_MODE: {
out.append(insert);
out_pos = std::wcslen(insert) + (begin - buff);
break;
}
case APPEND_MODE: {
out.append(begin, end - begin);
out.append(insert);
break;
}
case INSERT_MODE: {
long cursor = cursor_pos - (begin - buff);
out.append(begin, cursor);
out.append(insert);
out.append(begin + cursor, end - begin - cursor);
out_pos += std::wcslen(insert);
break;
}
default: {
DIE("unexpected append_mode");
}
}
out.append(end);
commandline_set_buffer(out, out_pos);
}
/// Output the specified selection.
///
/// \param begin start of selection
/// \param end end of selection
/// \param cut_at_cursor whether printing should stop at the surrent cursor position
/// \param tokenize whether the string should be tokenized, printing one string token on every line
/// and skipping non-string tokens
/// \param buffer the original command line buffer
/// \param cursor_pos the position of the cursor in the command line
static void write_part(const wchar_t *begin, const wchar_t *end, int cut_at_cursor, int tokenize,
const wchar_t *buffer, size_t cursor_pos, io_streams_t &streams) {
size_t pos = cursor_pos - (begin - buffer);
if (tokenize) {
// std::fwprintf( stderr, L"Subshell: %ls, end char %lc\n", buff, *end );
wcstring out;
wcstring buff(begin, end - begin);
tokenizer_t tok(buff.c_str(), TOK_ACCEPT_UNFINISHED);
while (auto token = tok.next()) {
if ((cut_at_cursor) && (token->offset + token->length >= pos)) break;
if (token->type == token_type_t::string) {
wcstring tmp = tok.text_of(*token);
unescape_string_in_place(&tmp, UNESCAPE_INCOMPLETE);
out.append(tmp);
out.push_back(L'\n');
}
}
streams.out.append(out);
} else {
if (cut_at_cursor) {
streams.out.append(begin, pos);
} else {
streams.out.append(begin, end - begin);
}
streams.out.push_back(L'\n');
}
}
/// The commandline builtin. It is used for specifying a new value for the commandline.
maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
const commandline_state_t rstate = commandline_get_state();
const wchar_t *cmd = argv[0];
int buffer_part = 0;
bool cut_at_cursor = false;
int argc = builtin_count_args(argv);
int append_mode = 0;
bool function_mode = false;
bool selection_mode = false;
bool tokenize = false;
bool cursor_mode = false;
bool line_mode = false;
bool search_mode = false;
bool paging_mode = false;
const wchar_t *begin = nullptr, *end = nullptr;
const wchar_t *override_buffer = nullptr;
const auto &ld = parser.libdata();
static const wchar_t *const short_options = L":abijpctforhI:CLSsP";
static const struct woption long_options[] = {{L"append", no_argument, nullptr, 'a'},
{L"insert", no_argument, nullptr, 'i'},
{L"replace", no_argument, nullptr, 'r'},
{L"current-buffer", no_argument, nullptr, 'b'},
{L"current-job", no_argument, nullptr, 'j'},
{L"current-process", no_argument, nullptr, 'p'},
{L"current-selection", no_argument, nullptr, 's'},
{L"current-token", no_argument, nullptr, 't'},
{L"cut-at-cursor", no_argument, nullptr, 'c'},
{L"function", no_argument, nullptr, 'f'},
{L"tokenize", no_argument, nullptr, 'o'},
{L"help", no_argument, nullptr, 'h'},
{L"input", required_argument, nullptr, 'I'},
{L"cursor", no_argument, nullptr, 'C'},
{L"line", no_argument, nullptr, 'L'},
{L"search-mode", no_argument, nullptr, 'S'},
{L"paging-mode", no_argument, nullptr, 'P'},
{nullptr, 0, nullptr, 0}};
int opt;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
switch (opt) {
case L'a': {
append_mode = APPEND_MODE;
break;
}
case L'b': {
buffer_part = STRING_MODE;
break;
}
case L'i': {
append_mode = INSERT_MODE;
break;
}
case L'r': {
append_mode = REPLACE_MODE;
break;
}
case 'c': {
cut_at_cursor = true;
break;
}
case 't': {
buffer_part = TOKEN_MODE;
break;
}
case 'j': {
buffer_part = JOB_MODE;
break;
}
case 'p': {
buffer_part = PROCESS_MODE;
break;
}
case 'f': {
function_mode = true;
break;
}
case 'o': {
tokenize = true;
break;
}
case 'I': {
override_buffer = w.woptarg;
break;
}
case 'C': {
cursor_mode = true;
break;
}
case 'L': {
line_mode = true;
break;
}
case 'S': {
search_mode = true;
break;
}
case 's': {
selection_mode = true;
break;
}
case 'P': {
paging_mode = true;
break;
}
case 'h': {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
case L'?': {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
default: {
DIE("unexpected retval from wgetopt_long");
}
}
}
if (function_mode) {
int i;
// Check for invalid switch combinations.
if (buffer_part || cut_at_cursor || append_mode || tokenize || cursor_mode || line_mode ||
search_mode || paging_mode) {
streams.err.append_format(BUILTIN_ERR_COMBO, argv[0]);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
if (argc == w.woptind) {
builtin_missing_argument(parser, streams, cmd, argv[0]);
return STATUS_INVALID_ARGS;
}
using rl = readline_cmd_t;
for (i = w.woptind; i < argc; i++) {
if (auto mc = input_function_get_code(argv[i])) {
// Don't enqueue a repaint if we're currently in the middle of one,
// because that's an infinite loop.
if (mc == rl::repaint_mode || mc == rl::force_repaint || mc == rl::repaint) {
if (ld.is_repaint) continue;
}
// HACK: Execute these right here and now so they can affect any insertions/changes
// made via bindings. The correct solution is to change all `commandline`
// insert/replace operations into readline functions with associated data, so that
// all queued `commandline` operations - including buffer modifications - are
// executed in order
if (mc == rl::begin_undo_group || mc == rl::end_undo_group) {
reader_handle_command(*mc);
} else {
// Inserts the readline function at the back of the queue.
reader_queue_ch(*mc);
}
} else {
streams.err.append_format(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i]);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
}
return STATUS_CMD_OK;
}
if (selection_mode) {
if (rstate.selection) {
streams.out.append(rstate.text.c_str() + rstate.selection->start,
rstate.selection->length);
}
return STATUS_CMD_OK;
}
// Check for invalid switch combinations.
if ((search_mode || line_mode || cursor_mode || paging_mode) && (argc - w.woptind > 1)) {
streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
if ((buffer_part || tokenize || cut_at_cursor) &&
(cursor_mode || line_mode || search_mode || paging_mode) &&
// Special case - we allow to get/set cursor position relative to the process/job/token.
!(buffer_part && cursor_mode)) {
streams.err.append_format(BUILTIN_ERR_COMBO, argv[0]);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
if ((tokenize || cut_at_cursor) && (argc - w.woptind)) {
streams.err.append_format(
BUILTIN_ERR_COMBO2, cmd,
L"--cut-at-cursor and --tokenize can not be used when setting the commandline");
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
if (append_mode && !(argc - w.woptind)) {
// No tokens in insert mode just means we do nothing.
return STATUS_CMD_ERROR;
}
// Set default modes.
if (!append_mode) {
append_mode = REPLACE_MODE;
}
if (!buffer_part) {
buffer_part = STRING_MODE;
}
if (line_mode) {
streams.out.append_format(L"%d\n",
parse_util_lineno(rstate.text.c_str(), rstate.cursor_pos));
return STATUS_CMD_OK;
}
if (search_mode) {
return commandline_get_state().search_mode ? 0 : 1;
}
if (paging_mode) {
return commandline_get_state().pager_mode ? 0 : 1;
}
// At this point we have (nearly) exhausted the options which always operate on the true command
// line. Now we respect the possibility of a transient command line due to evaluating a wrapped
// completion. Don't do this in cursor_mode: it makes no sense to move the cursor based on a
// transient commandline.
const wchar_t *current_buffer = nullptr;
size_t current_cursor_pos{0};
wcstring transient;
if (!ld.transient_commandlines.empty() && !cursor_mode) {
transient = ld.transient_commandlines.back();
current_buffer = transient.c_str();
current_cursor_pos = transient.size();
} else if (rstate.initialized) {
current_buffer = rstate.text.c_str();
current_cursor_pos = rstate.cursor_pos;
} else {
// There is no command line, either because we are not interactive, or because we are
// interactive and are still reading init files (in which case we silently ignore this).
if (!is_interactive_session()) {
streams.err.append(cmd);
streams.err.append(L": Can not set commandline in non-interactive mode\n");
builtin_print_error_trailer(parser, streams.err, cmd);
}
return STATUS_CMD_ERROR;
}
switch (buffer_part) {
case STRING_MODE: {
begin = current_buffer;
end = begin + std::wcslen(begin);
break;
}
case PROCESS_MODE: {
parse_util_process_extent(current_buffer, current_cursor_pos, &begin, &end, nullptr);
break;
}
case JOB_MODE: {
parse_util_job_extent(current_buffer, current_cursor_pos, &begin, &end);
break;
}
case TOKEN_MODE: {
parse_util_token_extent(current_buffer, current_cursor_pos, &begin, &end, nullptr,
nullptr);
break;
}
default: {
DIE("unexpected buffer_part");
}
}
if (cursor_mode) {
if (argc - w.woptind) {
long new_pos = fish_wcstol(argv[w.woptind]) + (begin - current_buffer);
if (errno) {
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, argv[w.woptind]);
builtin_print_error_trailer(parser, streams.err, cmd);
}
new_pos =
std::max(0L, std::min(new_pos, static_cast<long>(std::wcslen(current_buffer))));
commandline_set_buffer(current_buffer, static_cast<size_t>(new_pos));
} else {
size_t pos = current_cursor_pos - (begin - current_buffer);
streams.out.append_format(L"%lu\n", static_cast<unsigned long>(pos));
}
return STATUS_CMD_OK;
}
int arg_count = argc - w.woptind;
if (arg_count == 0) {
write_part(begin, end, cut_at_cursor, tokenize, current_buffer, current_cursor_pos,
streams);
} else if (arg_count == 1) {
replace_part(begin, end, argv[w.woptind], append_mode, current_buffer, current_cursor_pos);
} else {
wcstring sb = argv[w.woptind];
for (int i = w.woptind + 1; i < argc; i++) {
sb.push_back(L'\n');
sb.append(argv[i]);
}
replace_part(begin, end, sb.c_str(), append_mode, current_buffer, current_cursor_pos);
}
return STATUS_CMD_OK;
}