diff --git a/src/autoload.cpp b/src/autoload.cpp index 6d577fc46..bcd980796 100644 --- a/src/autoload.cpp +++ b/src/autoload.cpp @@ -19,6 +19,7 @@ #include "common.h" #include "env.h" #include "exec.h" +#include "parser.h" #include "wutil.h" // IWYU pragma: keep /// The time before we'll recheck an autoloaded file. @@ -65,8 +66,12 @@ int autoload_t::load(const wcstring &cmd, bool reload) { CHECK_BLOCK(0); ASSERT_IS_MAIN_THREAD(); + // TODO: Justify this principal_parser. + auto &parser = parser_t::principal_parser(); + auto &vars = parser.vars(); + if (!this->paths) { - auto path_var = env_get(env_var_name); + auto path_var = vars.get(env_var_name); if (path_var.missing_or_empty()) return 0; this->paths = path_var->as_list(); } @@ -96,7 +101,7 @@ int autoload_t::load(const wcstring &cmd, bool reload) { return res; } -bool autoload_t::can_load(const wcstring &cmd, const env_vars_snapshot_t &vars) { +bool autoload_t::can_load(const wcstring &cmd, const environment_t &vars) { auto path_var = vars.get(env_var_name); if (path_var.missing_or_empty()) return false; @@ -256,7 +261,9 @@ bool autoload_t::locate_file_and_maybe_load_it(const wcstring &cmd, bool really_ // If we have a script, either built-in or a file source, then run it. if (really_load && !script_source.empty()) { // Do nothing on failure. - exec_subshell(script_source, false /* do not apply exit status */); + // TODO: rationalize this use of principal_parser, or inject the loading from outside. + exec_subshell(script_source, parser_t::principal_parser(), + false /* do not apply exit status */); } if (really_load) { diff --git a/src/autoload.h b/src/autoload.h index ceb703c53..249b49c34 100644 --- a/src/autoload.h +++ b/src/autoload.h @@ -40,7 +40,7 @@ struct autoload_function_t { bool is_placeholder; }; -class env_vars_snapshot_t; +class environment_t; /// Class representing a path from which we can autoload and the autoloaded contents. class autoload_t : public lru_cache_t { @@ -88,7 +88,7 @@ class autoload_t : public lru_cache_t { int unload(const wcstring &cmd); /// Check whether the given command could be loaded, but do not load it. - bool can_load(const wcstring &cmd, const env_vars_snapshot_t &vars); + bool can_load(const wcstring &cmd, const environment_t &vars); /// Invalidates all entries. Uesd when the underlying path variable changes. void invalidate(); diff --git a/src/builtin.cpp b/src/builtin.cpp index 2e3306f94..8e3ddeca5 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -169,7 +169,7 @@ wcstring builtin_help_get(parser_t &parser, io_streams_t &streams, const wchar_t wcstring out; const wcstring name_esc = escape_string(name, 1); wcstring cmd = format_string(L"__fish_print_help %ls", name_esc.c_str()); - if (exec_subshell(cmd, lst, false /* don't apply exit status */) >= 0) { + if (exec_subshell(cmd, parser, lst, false /* don't apply exit status */) >= 0) { for (size_t i = 0; i < lst.size(); i++) { out.append(lst.at(i)); out.push_back(L'\n'); diff --git a/src/builtin_argparse.cpp b/src/builtin_argparse.cpp index ae15cfc6e..c4b88736e 100644 --- a/src/builtin_argparse.cpp +++ b/src/builtin_argparse.cpp @@ -22,6 +22,7 @@ #include "exec.h" #include "fallback.h" // IWYU pragma: keep #include "io.h" +#include "parser.h" #include "wgetopt.h" // IWYU pragma: keep #include "wutil.h" // IWYU pragma: keep @@ -433,29 +434,31 @@ static void populate_option_strings(const argparse_cmd_opts_t &opts, wcstring *s long_options->push_back({NULL, 0, NULL, 0}); } -static int validate_arg(const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, bool is_long_flag, - const wchar_t *woptarg, io_streams_t &streams) { +static int validate_arg(parser_t &parser, const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, + bool is_long_flag, const wchar_t *woptarg, io_streams_t &streams) { // Obviously if there is no arg validation command we assume the arg is okay. if (opt_spec->validation_command.empty()) return STATUS_CMD_OK; wcstring_list_t cmd_output; - env_push(true); - env_set_one(L"_argparse_cmd", ENV_LOCAL, opts.name); - if (is_long_flag) { - env_set_one(var_name_prefix + L"name", ENV_LOCAL, opt_spec->long_flag); - } else { - env_set_one(var_name_prefix + L"name", ENV_LOCAL, - wcstring(1, opt_spec->short_flag).c_str()); - } - env_set_one(var_name_prefix + L"value", ENV_LOCAL, woptarg); + auto &vars = parser.vars(); - int retval = exec_subshell(opt_spec->validation_command, cmd_output, false); + vars.push(true); + vars.set_one(L"_argparse_cmd", ENV_LOCAL, opts.name); + if (is_long_flag) { + vars.set_one(var_name_prefix + L"name", ENV_LOCAL, opt_spec->long_flag); + } else { + vars.set_one(var_name_prefix + L"name", ENV_LOCAL, + wcstring(1, opt_spec->short_flag).c_str()); + } + vars.set_one(var_name_prefix + L"value", ENV_LOCAL, woptarg); + + int retval = exec_subshell(opt_spec->validation_command, parser, cmd_output, false); for (const auto &output : cmd_output) { streams.err.append(output); streams.err.push_back(L'\n'); } - env_pop(); + vars.pop(); return retval; } @@ -473,13 +476,14 @@ static bool is_implicit_int(const argparse_cmd_opts_t &opts, const wchar_t *val) } // Store this value under the implicit int option. -static int validate_and_store_implicit_int(const argparse_cmd_opts_t &opts, const wchar_t *val, - wgetopter_t &w, int long_idx, io_streams_t &streams) { +static int validate_and_store_implicit_int(parser_t &parser, const argparse_cmd_opts_t &opts, + const wchar_t *val, wgetopter_t &w, int long_idx, + io_streams_t &streams) { // See if this option passes the validation checks. auto found = opts.options.find(opts.implicit_int_flag); assert(found != opts.options.end()); const auto &opt_spec = found->second; - int retval = validate_arg(opts, opt_spec.get(), long_idx != -1, val, streams); + int retval = validate_arg(parser, opts, opt_spec.get(), long_idx != -1, val, streams); if (retval != STATUS_CMD_OK) return retval; // It's a valid integer so store it and return success. @@ -490,8 +494,8 @@ static int validate_and_store_implicit_int(const argparse_cmd_opts_t &opts, cons return STATUS_CMD_OK; } -static int handle_flag(const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, int long_idx, - const wchar_t *woptarg, io_streams_t &streams) { +static int handle_flag(parser_t &parser, const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, + int long_idx, const wchar_t *woptarg, io_streams_t &streams) { opt_spec->num_seen++; if (opt_spec->num_allowed == 0) { // It's a boolean flag. Save the flag we saw since it might be useful to know if the @@ -506,7 +510,7 @@ static int handle_flag(const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, } if (woptarg) { - int retval = validate_arg(opts, opt_spec, long_idx != -1, woptarg, streams); + int retval = validate_arg(parser, opts, opt_spec, long_idx != -1, woptarg, streams); if (retval != STATUS_CMD_OK) return retval; } @@ -526,9 +530,9 @@ static int handle_flag(const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, return STATUS_CMD_OK; } -static int argparse_parse_flags(const argparse_cmd_opts_t &opts, const wchar_t *short_options, - const woption *long_options, const wchar_t *cmd, int argc, - wchar_t **argv, int *optind, parser_t &parser, +static int argparse_parse_flags(parser_t &parser, const argparse_cmd_opts_t &opts, + const wchar_t *short_options, const woption *long_options, + const wchar_t *cmd, int argc, wchar_t **argv, int *optind, io_streams_t &streams) { int opt; int long_idx = -1; @@ -544,7 +548,8 @@ static int argparse_parse_flags(const argparse_cmd_opts_t &opts, const wchar_t * const wchar_t *arg_contents = argv[w.woptind - 1] + 1; int retval = STATUS_CMD_OK; if (is_implicit_int(opts, arg_contents)) { - retval = validate_and_store_implicit_int(opts, arg_contents, w, long_idx, streams); + retval = validate_and_store_implicit_int(parser, opts, arg_contents, w, long_idx, + streams); } else { builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); retval = STATUS_INVALID_ARGS; @@ -558,7 +563,7 @@ static int argparse_parse_flags(const argparse_cmd_opts_t &opts, const wchar_t * auto found = opts.options.find(opt); assert(found != opts.options.end()); - int retval = handle_flag(opts, found->second.get(), long_idx, w.woptarg, streams); + int retval = handle_flag(parser, opts, found->second.get(), long_idx, w.woptarg, streams); if (retval != STATUS_CMD_OK) return retval; long_idx = -1; } @@ -590,8 +595,8 @@ static int argparse_parse_args(argparse_cmd_opts_t &opts, const wcstring_list_t auto argv = argv_container.get(); int optind; - int retval = argparse_parse_flags(opts, short_options.c_str(), long_options.data(), cmd, argc, - argv, &optind, parser, streams); + int retval = argparse_parse_flags(parser, opts, short_options.c_str(), long_options.data(), cmd, + argc, argv, &optind, streams); if (retval != STATUS_CMD_OK) return retval; retval = check_for_mutually_exclusive_flags(opts, streams); @@ -620,13 +625,13 @@ static int check_min_max_args_constraints(const argparse_cmd_opts_t &opts, parse } /// Put the result of parsing the supplied args into the caller environment as local vars. -static void set_argparse_result_vars(const argparse_cmd_opts_t &opts) { +static void set_argparse_result_vars(env_stack_t &vars, const argparse_cmd_opts_t &opts) { for (const auto &kv : opts.options) { const auto &opt_spec = kv.second; if (!opt_spec->num_seen) continue; if (opt_spec->short_flag_valid) { - env_set(var_name_prefix + opt_spec->short_flag, ENV_LOCAL, opt_spec->vals); + vars.set(var_name_prefix + opt_spec->short_flag, ENV_LOCAL, opt_spec->vals); } if (!opt_spec->long_flag.empty()) { // We do a simple replacement of all non alphanum chars rather than calling @@ -635,11 +640,11 @@ static void set_argparse_result_vars(const argparse_cmd_opts_t &opts) { for (size_t pos = 0; pos < long_flag.size(); pos++) { if (!iswalnum(long_flag[pos])) long_flag[pos] = L'_'; } - env_set(var_name_prefix + long_flag, ENV_LOCAL, opt_spec->vals); + vars.set(var_name_prefix + long_flag, ENV_LOCAL, opt_spec->vals); } } - env_set(L"argv", ENV_LOCAL, opts.argv); + vars.set(L"argv", ENV_LOCAL, opts.argv); } /// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this @@ -676,6 +681,6 @@ int builtin_argparse(parser_t &parser, io_streams_t &streams, wchar_t **argv) { retval = check_min_max_args_constraints(opts, parser, streams); if (retval != STATUS_CMD_OK) return retval; - set_argparse_result_vars(opts); + set_argparse_result_vars(parser.vars(), opts); return retval; } diff --git a/src/builtin_cd.cpp b/src/builtin_cd.cpp index 10362f444..92f7d9ce4 100644 --- a/src/builtin_cd.cpp +++ b/src/builtin_cd.cpp @@ -34,12 +34,10 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) { } wcstring dir_in; - wcstring dir; - if (argv[optind]) { dir_in = argv[optind]; } else { - auto maybe_dir_in = env_get(L"HOME"); + auto maybe_dir_in = parser.vars().get(L"HOME"); if (maybe_dir_in.missing_or_empty()) { streams.err.append_format(_(L"%ls: Could not find home directory\n"), cmd); return STATUS_CMD_ERROR; @@ -47,7 +45,9 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) { dir_in = maybe_dir_in->as_string(); } - if (!path_get_cdpath(dir_in, &dir, env_get_pwd_slash())) { + wcstring pwd = parser.vars().get_pwd_slash(); + maybe_t mdir = path_get_cdpath(dir_in, pwd, parser.vars()); + if (!mdir) { if (errno == ENOTDIR) { streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str()); } else if (errno == ENOENT) { @@ -64,6 +64,7 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) { return STATUS_CMD_ERROR; } + const wcstring &dir = *mdir; wcstring norm_dir = normalize_path(dir); @@ -86,6 +87,6 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) { return STATUS_CMD_ERROR; } - env_set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, std::move(norm_dir)); + parser.vars().set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, std::move(norm_dir)); return STATUS_CMD_OK; } diff --git a/src/builtin_command.cpp b/src/builtin_command.cpp index 50b985691..1e038e68d 100644 --- a/src/builtin_command.cpp +++ b/src/builtin_command.cpp @@ -10,6 +10,7 @@ #include "common.h" #include "fallback.h" // IWYU pragma: keep #include "io.h" +#include "parser.h" #include "path.h" #include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep @@ -95,14 +96,14 @@ int builtin_command(parser_t &parser, io_streams_t &streams, wchar_t **argv) { for (int idx = optind; argv[idx]; ++idx) { const wchar_t *command_name = argv[idx]; if (opts.all_paths) { - wcstring_list_t paths = path_get_paths(command_name); + wcstring_list_t paths = path_get_paths(command_name, parser.vars()); for (auto path : paths) { if (!opts.quiet) streams.out.append_format(L"%ls\n", path.c_str()); ++found; } } else { wcstring path; - if (path_get_path(command_name, &path)) { + if (path_get_path(command_name, &path, parser.vars())) { if (!opts.quiet) streams.out.append_format(L"%ls\n", path.c_str()); ++found; } diff --git a/src/builtin_complete.cpp b/src/builtin_complete.cpp index d40a4e303..2a01fcd26 100644 --- a/src/builtin_complete.cpp +++ b/src/builtin_complete.cpp @@ -318,7 +318,7 @@ int builtin_complete(parser_t &parser, io_streams_t &streams, wchar_t **argv) { recursion_level++; std::vector comp; - complete(do_complete_param, &comp, COMPLETION_REQUEST_DEFAULT); + complete(do_complete_param, &comp, COMPLETION_REQUEST_DEFAULT, parser.vars()); for (size_t i = 0; i < comp.size(); i++) { const completion_t &next = comp.at(i); diff --git a/src/builtin_fg.cpp b/src/builtin_fg.cpp index ac4af798a..605a1301a 100644 --- a/src/builtin_fg.cpp +++ b/src/builtin_fg.cpp @@ -12,6 +12,7 @@ #include "env.h" #include "fallback.h" // IWYU pragma: keep #include "io.h" +#include "parser.h" #include "proc.h" #include "reader.h" #include "tokenizer.h" @@ -103,7 +104,7 @@ int builtin_fg(parser_t &parser, io_streams_t &streams, wchar_t **argv) { const wcstring ft = tok_first(j->command()); //For compatibility with fish 2.0's $_, now replaced with `status current-command` - if (!ft.empty()) env_set_one(L"_", ENV_EXPORT, ft); + if (!ft.empty()) parser.vars().set_one(L"_", ENV_EXPORT, ft); reader_write_title(j->command()); j->promote(); diff --git a/src/builtin_history.cpp b/src/builtin_history.cpp index d86311531..bd0a49fa0 100644 --- a/src/builtin_history.cpp +++ b/src/builtin_history.cpp @@ -15,6 +15,7 @@ #include "fallback.h" // IWYU pragma: keep #include "history.h" #include "io.h" +#include "parser.h" #include "reader.h" #include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep @@ -218,7 +219,7 @@ int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) { // Use the default history if we have none (which happens if invoked non-interactively, e.g. // from webconfig.py. history_t *history = reader_get_history(); - if (!history) history = &history_t::history_with_name(history_session_id()); + if (!history) history = &history_t::history_with_name(history_session_id(parser.vars())); // If a history 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. diff --git a/src/builtin_pwd.cpp b/src/builtin_pwd.cpp index f32a783c7..f9d7fda9c 100644 --- a/src/builtin_pwd.cpp +++ b/src/builtin_pwd.cpp @@ -6,6 +6,7 @@ #include "common.h" #include "fallback.h" // IWYU pragma: keep #include "io.h" +#include "parser.h" #include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep @@ -48,7 +49,7 @@ int builtin_pwd(parser_t &parser, io_streams_t &streams, wchar_t **argv) { } wcstring pwd; - if (auto tmp = env_get(L"PWD")) { + if (auto tmp = parser.vars().get(L"PWD")) { pwd = tmp->as_string(); } if (resolve_symlinks) { diff --git a/src/builtin_read.cpp b/src/builtin_read.cpp index 0ce1e3be0..0c1ea1a00 100644 --- a/src/builtin_read.cpp +++ b/src/builtin_read.cpp @@ -26,6 +26,7 @@ #include "highlight.h" #include "history.h" #include "io.h" +#include "parser.h" #include "proc.h" #include "reader.h" #include "wcstringutil.h" @@ -203,7 +204,9 @@ static int read_interactive(wcstring &buff, int nchars, bool shell, bool silent, int exit_res = STATUS_CMD_OK; const wchar_t *line; - wcstring read_history_ID = history_session_id(); + // TODO: rationalize this. + const auto &vars = env_stack_t::principal(); + wcstring read_history_ID = history_session_id(vars); if (!read_history_ID.empty()) read_history_ID += L"_read"; reader_push(read_history_ID); @@ -414,6 +417,7 @@ static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int arg /// 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) { + auto &vars = parser.vars(); wchar_t *cmd = argv[0]; int argc = builtin_count_args(argv); wcstring buff; @@ -452,8 +456,7 @@ int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) { auto vars_left = [&] () { return argv + argc - var_ptr; }; auto clear_remaining_vars = [&] () { while (vars_left()) { - env_set_empty(*var_ptr, opts.place); - // env_set_one(*var_ptr, opts.place, L""); + parser.vars().set_empty(*var_ptr, opts.place); ++var_ptr; } }; @@ -488,7 +491,7 @@ int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) { } if (!opts.have_delimiter) { - auto ifs = env_get(L"IFS"); + auto ifs = parser.vars().get(L"IFS"); if (!ifs.missing_or_empty()) opts.delimiter = ifs->as_string(); } @@ -512,13 +515,13 @@ int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) { if (opts.array) { // Array mode: assign each char as a separate element of the sole var. - env_set(*var_ptr++, opts.place, chars); + vars.set(*var_ptr++, opts.place, chars); } else { // Not array mode: assign each char to a separate var with the remainder being assigned // to the last var. auto c = chars.begin(); for (; c != chars.end() && vars_left(); ++c) { - env_set_one(*var_ptr++, opts.place, *c); + vars.set_one(*var_ptr++, opts.place, *c); } } } else if (opts.array) { @@ -534,14 +537,14 @@ int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) { loc.first != wcstring::npos; loc = wcstring_tok(buff, opts.delimiter, loc)) { tokens.emplace_back(wcstring(buff, loc.first, loc.second)); } - env_set(*var_ptr++, opts.place, tokens); + vars.set(*var_ptr++, opts.place, tokens); } else { // We're using a delimiter provided by the user so use the `string split` behavior. wcstring_list_t splits; split_about(buff.begin(), buff.end(), opts.delimiter.begin(), opts.delimiter.end(), &splits); - env_set(*var_ptr++, opts.place, splits); + vars.set(*var_ptr++, opts.place, splits); } } else { // Not array mode. Split the input into tokens and assign each to the vars in sequence. @@ -555,7 +558,7 @@ int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) { if (loc.first != wcstring::npos) { substr = wcstring(buff, loc.first, loc.second); } - env_set_one(*var_ptr++, opts.place, substr); + vars.set_one(*var_ptr++, opts.place, substr); } } else { // We're using a delimiter provided by the user so use the `string split` behavior. @@ -566,7 +569,7 @@ int builtin_read(parser_t &parser, io_streams_t &streams, wchar_t **argv) { &splits, argc - 1); assert(splits.size() <= (size_t) vars_left()); for (const auto &split : splits) { - env_set_one(*var_ptr++, opts.place, split); + vars.set_one(*var_ptr++, opts.place, split); } } } diff --git a/src/builtin_set.cpp b/src/builtin_set.cpp index e78fc4ee7..017d2f3cc 100644 --- a/src/builtin_set.cpp +++ b/src/builtin_set.cpp @@ -22,12 +22,11 @@ #include "expand.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 -class parser_t; - struct set_cmd_opts_t { bool print_help = false; bool show = false; @@ -235,9 +234,9 @@ static int validate_cmd_opts(const wchar_t *cmd, set_cmd_opts_t &opts, //!OCLIN // Check if we are setting a uvar and a global of the same name exists. See // https://github.com/fish-shell/fish-shell/issues/806 static int check_global_scope_exists(const wchar_t *cmd, set_cmd_opts_t &opts, const wchar_t *dest, - io_streams_t &streams) { + io_streams_t &streams, const environment_t &vars) { if (opts.universal) { - auto global_dest = env_get(dest, ENV_GLOBAL); + auto global_dest = vars.get(dest, ENV_GLOBAL); if (global_dest && shell_is_interactive()) { streams.err.append_format(BUILTIN_SET_UVAR_ERR, cmd, dest); } @@ -250,7 +249,8 @@ static int check_global_scope_exists(const wchar_t *cmd, set_cmd_opts_t &opts, c // contain a colon, then complain. Return true if any path element was valid, false if not. static bool validate_path_warning_on_colons(const wchar_t *cmd, const wchar_t *key, //!OCLINT(npath complexity) - const wcstring_list_t &list, io_streams_t &streams) { + const wcstring_list_t &list, io_streams_t &streams, + const environment_t &vars) { // Always allow setting an empty value. if (list.empty()) return true; @@ -266,7 +266,7 @@ static bool validate_path_warning_on_colons(const wchar_t *cmd, // not the (missing) local value. Also don't bother to complain about relative paths, which // don't start with /. wcstring_list_t existing_values; - const auto existing_variable = env_get(key, ENV_DEFAULT); + const auto existing_variable = vars.get(key, ENV_DEFAULT); if (!existing_variable.missing_or_empty()) existing_variable->to_list(existing_values); for (const wcstring &dir : list) { @@ -336,21 +336,22 @@ static void handle_env_return(int retval, const wchar_t *cmd, const wchar_t *key break; } default: { - DIE("unexpected env_set() ret val"); + DIE("unexpected vars.set() ret val"); break; } } } -/// Call env_set. If this is a path variable, e.g. PATH, validate the elements. On error, print a +/// Call vars.set. If this is a path variable, e.g. PATH, validate the elements. On error, print a /// description of the problem to stderr. static int env_set_reporting_errors(const wchar_t *cmd, const wchar_t *key, int scope, - wcstring_list_t &list, io_streams_t &streams) { - if (is_path_variable(key) && !validate_path_warning_on_colons(cmd, key, list, streams)) { + const wcstring_list_t &list, io_streams_t &streams, + env_stack_t &vars) { + if (is_path_variable(key) && !validate_path_warning_on_colons(cmd, key, list, streams, vars)) { return STATUS_CMD_ERROR; } - int retval = env_set(key, scope | ENV_USER, list); + int retval = vars.set(key, scope | ENV_USER, list); handle_env_return(retval, cmd, key, streams); return retval; @@ -365,13 +366,14 @@ static int env_set_reporting_errors(const wchar_t *cmd, const wchar_t *key, int /// Returns: /// The total number of indexes parsed, or -1 on error. If any indexes were found the `src` string /// is modified to omit the index expression leaving just the var name. -static int parse_index(std::vector &indexes, wchar_t *src, int scope, io_streams_t &streams) { +static int parse_index(std::vector &indexes, wchar_t *src, int scope, io_streams_t &streams, + const environment_t &vars) { wchar_t *p = wcschr(src, L'['); if (!p) return 0; // no slices so nothing for us to do *p = L'\0'; // split the var name from the indexes/slices p++; - auto var_str = env_get(src, scope); + auto var_str = vars.get(src, scope); wcstring_list_t var; if (var_str) var_str->to_list(var); @@ -475,7 +477,7 @@ static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, UNUSED(parser); bool names_only = opts.list; - wcstring_list_t names = env_get_names(compute_scope(opts)); + wcstring_list_t names = parser.vars().get_names(compute_scope(opts)); sort(names.begin(), names.end()); for (size_t i = 0; i < names.size(); i++) { @@ -484,7 +486,7 @@ static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, streams.out.append(e_key); if (!names_only) { - auto var = env_get(key, compute_scope(opts)); + auto var = parser.vars().get(key, compute_scope(opts)); if (!var.missing_or_empty()) { bool shorten = false; wcstring val = expand_escape_variable(*var); @@ -517,7 +519,7 @@ static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, assert(dest); std::vector indexes; - int idx_count = parse_index(indexes, dest, scope, streams); + int idx_count = parse_index(indexes, dest, scope, streams, parser.vars()); if (idx_count == -1) { free(dest); builtin_print_help(parser, streams, cmd, streams.err); @@ -526,14 +528,14 @@ static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, if (idx_count) { wcstring_list_t result; - auto dest_str = env_get(dest, scope); + auto dest_str = parser.vars().get(dest, scope); if (dest_str) dest_str->to_list(result); for (auto idx : indexes) { if (idx < 1 || (size_t)idx > result.size()) retval++; } } else { - if (! env_get(arg, scope)) retval++; + if (!parser.vars().get(arg, scope)) retval++; } free(dest); @@ -542,7 +544,8 @@ static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, return retval; } -static void show_scope(const wchar_t *var_name, int scope, io_streams_t &streams) { +static void show_scope(const wchar_t *var_name, int scope, io_streams_t &streams, + const environment_t &vars) { const wchar_t *scope_name; switch (scope) { case ENV_LOCAL: { @@ -563,7 +566,7 @@ static void show_scope(const wchar_t *var_name, int scope, io_streams_t &streams } } - const auto var = env_get(var_name, scope); + const auto var = vars.get(var_name, scope); if (!var) { streams.out.append_format(_(L"$%ls: not set in %ls scope\n"), var_name, scope_name); return; @@ -590,14 +593,14 @@ static void show_scope(const wchar_t *var_name, int scope, io_streams_t &streams static int builtin_set_show(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) { UNUSED(opts); - + auto &vars = parser.vars(); if (argc == 0) { // show all vars - wcstring_list_t names = env_get_names(ENV_USER); + wcstring_list_t names = parser.vars().get_names(ENV_USER); sort(names.begin(), names.end()); for (auto it : names) { - show_scope(it.c_str(), ENV_LOCAL, streams); - show_scope(it.c_str(), ENV_GLOBAL, streams); - show_scope(it.c_str(), ENV_UNIVERSAL, streams); + show_scope(it.c_str(), ENV_LOCAL, streams, vars); + show_scope(it.c_str(), ENV_GLOBAL, streams, vars); + show_scope(it.c_str(), ENV_UNIVERSAL, streams, vars); streams.out.push_back(L'\n'); } } else { @@ -616,9 +619,9 @@ static int builtin_set_show(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, return STATUS_CMD_ERROR; } - show_scope(arg, ENV_LOCAL, streams); - show_scope(arg, ENV_GLOBAL, streams); - show_scope(arg, ENV_UNIVERSAL, streams); + show_scope(arg, ENV_LOCAL, streams, vars); + show_scope(arg, ENV_GLOBAL, streams, vars); + show_scope(arg, ENV_UNIVERSAL, streams, vars); streams.out.push_back(L'\n'); } } @@ -639,7 +642,7 @@ static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, wchar_t *dest = argv[0]; std::vector indexes; - int idx_count = parse_index(indexes, dest, scope, streams); + int idx_count = parse_index(indexes, dest, scope, streams, parser.vars()); if (idx_count == -1) { builtin_print_help(parser, streams, cmd, streams.err); return STATUS_CMD_ERROR; @@ -653,23 +656,23 @@ static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, } if (idx_count == 0) { // unset the var - retval = env_remove(dest, scope); + retval = parser.vars().remove(dest, scope); // Temporarily swallowing ENV_NOT_FOUND errors to prevent // breaking all tests that unset variables that aren't set. if (retval != ENV_NOT_FOUND) { handle_env_return(retval, cmd, dest, streams); } } else { // remove just the specified indexes of the var - const auto dest_var = env_get(dest, scope); + const auto dest_var = parser.vars().get(dest, scope); if (!dest_var) return STATUS_CMD_ERROR; wcstring_list_t result; dest_var->to_list(result); erase_values(result, indexes); - retval = env_set_reporting_errors(cmd, dest, scope, result, streams); + retval = env_set_reporting_errors(cmd, dest, scope, result, streams, parser.vars()); } if (retval != STATUS_CMD_OK) return retval; - return check_global_scope_exists(cmd, opts, dest, streams); + return check_global_scope_exists(cmd, opts, dest, streams, parser.vars()); } /// This handles the common case of setting the entire var to a set of values. @@ -684,8 +687,7 @@ static int set_var_array(const wchar_t *cmd, set_cmd_opts_t &opts, const wchar_t if (opts.prepend) { for (int i = 0; i < argc; i++) new_values.push_back(argv[i]); } - - auto var_str = env_get(varname, ENV_DEFAULT); + auto var_str = parser.vars().get(varname, ENV_DEFAULT); wcstring_list_t var_array; if (var_str) var_str->to_list(var_array); new_values.insert(new_values.end(), var_array.begin(), var_array.end()); @@ -719,7 +721,7 @@ static int set_var_slices(const wchar_t *cmd, set_cmd_opts_t &opts, const wchar_ } int scope = compute_scope(opts); // calculate the variable scope based on the provided options - const auto var_str = env_get(varname, scope); + const auto var_str = parser.vars().get(varname, scope); if (var_str) var_str->to_list(new_values); // Slice indexes have been calculated, do the actual work. @@ -750,7 +752,7 @@ static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, w argc--; std::vector indexes; - int idx_count = parse_index(indexes, varname, scope, streams); + int idx_count = parse_index(indexes, varname, scope, streams, parser.vars()); if (idx_count == -1) { builtin_print_help(parser, streams, cmd, streams.err); return STATUS_INVALID_ARGS; @@ -774,9 +776,9 @@ static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, w } if (retval != STATUS_CMD_OK) return retval; - retval = env_set_reporting_errors(cmd, varname, scope, new_values, streams); + retval = env_set_reporting_errors(cmd, varname, scope, new_values, streams, parser.vars()); if (retval != STATUS_CMD_OK) return retval; - return check_global_scope_exists(cmd, opts, varname, streams); + return check_global_scope_exists(cmd, opts, varname, streams, parser.vars()); } /// The set builtin creates, updates, and erases (removes, deletes) variables. diff --git a/src/builtin_set_color.cpp b/src/builtin_set_color.cpp index e0f98261f..f1b11efe4 100644 --- a/src/builtin_set_color.cpp +++ b/src/builtin_set_color.cpp @@ -27,6 +27,7 @@ #include "env.h" #include "io.h" #include "output.h" +#include "parser.h" #include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep @@ -75,10 +76,10 @@ int builtin_set_color(parser_t &parser, io_streams_t &streams, wchar_t **argv) { // Hack in missing italics and dim capabilities omitted from MacOS xterm-256color terminfo // Helps Terminal.app/iTerm #if __APPLE__ - const auto term_prog = env_get(L"TERM_PROGRAM"); + const auto term_prog = parser.vars().get(L"TERM_PROGRAM"); if (!term_prog.missing_or_empty() && (term_prog->as_string() == L"Apple_Terminal" || term_prog->as_string() == L"iTerm.app")) { - const auto term = env_get(L"TERM"); + const auto term = parser.vars().get(L"TERM"); if (!term.missing_or_empty() && (term->as_string() == L"xterm-256color")) { enter_italics_mode = sitm_esc; exit_italics_mode = ritm_esc; diff --git a/src/builtin_source.cpp b/src/builtin_source.cpp index 1e09b702e..8784b0603 100644 --- a/src/builtin_source.cpp +++ b/src/builtin_source.cpp @@ -78,7 +78,7 @@ int builtin_source(parser_t &parser, io_streams_t &streams, wchar_t **argv) { // This is slightly subtle. If this is a bare `source` with no args then `argv + optind` already // points to the end of argv. Otherwise we want to skip the file name to get to the args if any. - env_set_argv(argv + optind + (argc == optind ? 0 : 1)); + parser.vars().set_argv(argv + optind + (argc == optind ? 0 : 1)); retval = reader_read(fd, streams.io_chain ? *streams.io_chain : io_chain_t()); diff --git a/src/builtin_status.cpp b/src/builtin_status.cpp index 4862a8897..9a6b66c26 100644 --- a/src/builtin_status.cpp +++ b/src/builtin_status.cpp @@ -424,7 +424,7 @@ int builtin_status(parser_t &parser, io_streams_t &streams, wchar_t **argv) { case STATUS_CURRENT_CMD: { CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd) // HACK: Go via the deprecated variable to get the command. - const auto var = env_get(L"_"); + const auto var = parser.vars().get(L"_"); if (!var.missing_or_empty()) { streams.out.append(var->as_string()); streams.out.push_back(L'\n'); diff --git a/src/common.cpp b/src/common.cpp index e13f68738..718bffed0 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -1721,7 +1721,7 @@ void common_handle_winch(int signal) { /// Validate the new terminal size. Fallback to the env vars if necessary. Ensure the values are /// sane and if not fallback to a default of 80x24. -static void validate_new_termsize(struct winsize *new_termsize) { +static void validate_new_termsize(struct winsize *new_termsize, const environment_t &vars) { if (new_termsize->ws_col == 0 || new_termsize->ws_row == 0) { #ifdef HAVE_WINSIZE if (shell_is_interactive()) { @@ -1731,8 +1731,8 @@ static void validate_new_termsize(struct winsize *new_termsize) { } #endif // Fallback to the environment vars. - maybe_t col_var = env_get(L"COLUMNS"); - maybe_t row_var = env_get(L"LINES"); + maybe_t col_var = vars.get(L"COLUMNS"); + maybe_t row_var = vars.get(L"LINES"); if (!col_var.missing_or_empty() && !row_var.missing_or_empty()) { // Both vars have to have valid values. int col = fish_wcstoi(col_var->as_string().c_str()); @@ -1757,16 +1757,17 @@ static void validate_new_termsize(struct winsize *new_termsize) { } /// Export the new terminal size as env vars and to the kernel if possible. -static void export_new_termsize(struct winsize *new_termsize) { +static void export_new_termsize(struct winsize *new_termsize, env_stack_t &vars) { wchar_t buf[64]; - auto cols = env_get(L"COLUMNS", ENV_EXPORT); + auto cols = vars.get(L"COLUMNS", ENV_EXPORT); swprintf(buf, 64, L"%d", (int)new_termsize->ws_col); - env_set_one(L"COLUMNS", ENV_GLOBAL | (cols.missing_or_empty() ? ENV_DEFAULT : ENV_EXPORT), buf); + vars.set_one(L"COLUMNS", ENV_GLOBAL | (cols.missing_or_empty() ? ENV_DEFAULT : ENV_EXPORT), + buf); - auto lines = env_get(L"LINES", ENV_EXPORT); + auto lines = vars.get(L"LINES", ENV_EXPORT); swprintf(buf, 64, L"%d", (int)new_termsize->ws_row); - env_set_one(L"LINES", ENV_GLOBAL | (lines.missing_or_empty() ? ENV_DEFAULT : ENV_EXPORT), buf); + vars.set_one(L"LINES", ENV_GLOBAL | (lines.missing_or_empty() ? ENV_DEFAULT : ENV_EXPORT), buf); #ifdef HAVE_WINSIZE // Only write the new terminal size if we are in the foreground (#4477) @@ -1791,9 +1792,9 @@ struct winsize get_current_winsize() { return termsize; } #endif - - validate_new_termsize(&new_termsize); - export_new_termsize(&new_termsize); + auto &vars = env_stack_t::globals(); + validate_new_termsize(&new_termsize, vars); + export_new_termsize(&new_termsize, vars); termsize.ws_col = new_termsize.ws_col; termsize.ws_row = new_termsize.ws_row; termsize_valid = true; diff --git a/src/common.h b/src/common.h index dac5d9a6d..d52204929 100644 --- a/src/common.h +++ b/src/common.h @@ -624,8 +624,8 @@ class null_terminated_array_t { CharType_t **array{NULL}; // No assignment or copying. - void operator=(null_terminated_array_t rhs); - null_terminated_array_t(const null_terminated_array_t &); + void operator=(null_terminated_array_t rhs) = delete; + null_terminated_array_t(const null_terminated_array_t &) = delete; typedef std::vector> string_list_t; diff --git a/src/complete.cpp b/src/complete.cpp index 440d5f3c9..effcb05e9 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -74,20 +74,6 @@ static const wcstring &C_(const wcstring &s) { return s; } static void complete_load(const wcstring &name, bool reload); -/// Testing apparatus. -const wcstring_list_t *s_override_variable_names = NULL; - -void complete_set_variable_names(const wcstring_list_t *names) { - s_override_variable_names = names; -} - -static inline wcstring_list_t complete_get_variable_names() { - if (s_override_variable_names != NULL) { - return *s_override_variable_names; - } - return env_get_names(0); -} - /// Struct describing a completion option entry. /// /// If option is empty, the comp field must not be empty and contains a list of arguments to the @@ -301,8 +287,16 @@ void completions_sort_and_prioritize(std::vector *comps, /// Class representing an attempt to compute completions. class completer_t { + /// Environment inside which we are completing. + const environment_t &vars; + + /// The command to complete. const wcstring cmd; + + /// Flags associated with the completion request. const completion_request_flags_t flags; + + /// The output cmopletions. std::vector completions; /// Table of completions conditions that have already been tested and the corresponding test @@ -372,7 +366,8 @@ class completer_t { void mark_completions_duplicating_arguments(const wcstring &prefix, const arg_list_t &args); public: - completer_t(wcstring c, completion_request_flags_t f) : cmd(std::move(c)), flags(f) {} + completer_t(const environment_t &vars, wcstring c, completion_request_flags_t f) + : vars(vars), cmd(std::move(c)), flags(f) {} void perform(); @@ -415,7 +410,9 @@ bool completer_t::condition_test(const wcstring &condition) { condition_cache_t::iterator cached_entry = condition_cache.find(condition); if (cached_entry == condition_cache.end()) { // Compute new value and reinsert it. - test_res = (0 == exec_subshell(condition, false /* don't apply exit status */)); + // TODO: rationalize this principal_parser. + test_res = (0 == exec_subshell(condition, parser_t::principal_parser(), + false /* don't apply exit status */)); condition_cache[condition] = test_res; } else { // Use the old value. @@ -500,18 +497,19 @@ void complete_remove_all(const wcstring &cmd, bool cmd_is_path) { } /// Find the full path and commandname from a command string 'str'. -static void parse_cmd_string(const wcstring &str, wcstring &path, wcstring &cmd) { - if (!path_get_path(str, &path)) { +static void parse_cmd_string(const wcstring &str, wcstring *path, wcstring *cmd, + const environment_t &vars) { + if (!path_get_path(str, path, vars)) { /// Use the empty string as the 'path' for commands that can not be found. - path = L""; + *path = L""; } // Make sure the path is not included in the command. size_t last_slash = str.find_last_of(L'/'); if (last_slash != wcstring::npos) { - cmd = str.substr(last_slash + 1); + *cmd = str.substr(last_slash + 1); } else { - cmd = str; + *cmd = str; } } @@ -540,7 +538,7 @@ void completer_t::complete_strings(const wcstring &wc_escaped, const description const std::vector &possible_comp, complete_flags_t flags) { wcstring tmp = wc_escaped; - if (!expand_one(tmp, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_WILDCARDS | this->expand_flags(), NULL)) + if (!expand_one(tmp, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_WILDCARDS | this->expand_flags(), vars)) return; const wcstring wc = parse_util_unescape_wildcards(tmp); @@ -595,7 +593,9 @@ void completer_t::complete_cmd_desc(const wcstring &str) { // search if we know the location of the whatis database. This can take some time on slower // systems with a large set of manuals, but it should be ok since apropos is only called once. wcstring_list_t list; - if (exec_subshell(lookup_cmd, list, false /* don't apply exit status */) != -1) { + // TODO: justify this use of parser_t::principal_parser. + if (exec_subshell(lookup_cmd, parser_t::principal_parser(), list, + false /* don't apply exit status */) != -1) { std::unordered_map lookup; lookup.reserve(list.size()); @@ -661,7 +661,7 @@ void completer_t::complete_cmd(const wcstring &str_cmd, bool use_function, bool expand_error_t result = expand_string(str_cmd, &this->completions, EXPAND_SPECIAL_FOR_COMMAND | EXPAND_FOR_COMPLETIONS | EXECUTABLES_ONLY | this->expand_flags(), - NULL); + vars, NULL); if (result != EXPAND_ERROR && this->wants_descriptions()) { this->complete_cmd_desc(str_cmd); } @@ -673,7 +673,8 @@ void completer_t::complete_cmd(const wcstring &str_cmd, bool use_function, bool expand_error_t ignore = // Append all matching directories expand_string(str_cmd, &this->completions, - EXPAND_FOR_COMPLETIONS | DIRECTORIES_ONLY | this->expand_flags(), NULL); + EXPAND_FOR_COMPLETIONS | DIRECTORIES_ONLY | this->expand_flags(), vars, + NULL); UNUSED(ignore); } @@ -745,7 +746,7 @@ void completer_t::complete_from_args(const wcstring &str, const wcstring &args, } std::vector possible_comp; - parser_t::expand_argument_list(args, eflags, &possible_comp); + parser_t::expand_argument_list(args, eflags, vars, &possible_comp); if (!is_autosuggest) { proc_pop_interactive(); @@ -869,7 +870,7 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt, bool use_common = 1, use_files = 1; wcstring cmd, path; - parse_cmd_string(cmd_orig, path, cmd); + parse_cmd_string(cmd_orig, &path, &cmd, vars); // mqudsi: run_on_main_thread() already just runs `func` if we're on the main thread, // but it makes a kcall to get the current thread id to ascertain that. Perhaps even @@ -886,28 +887,17 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt, } }; - // This was originally written as a static variable protected by a mutex that is updated only if - // `scmd.size() == 1` to prevent too many lookups, but it turns out that this is mainly only - // called when the user explicitly presses after a command, so the overhead of the - // additional env lookup should be negligible. - env_vars_snapshot_t completion_snapshot; - // debug(0, L"\nThinking about looking up completions for %ls\n", cmd.c_str()); bool head_exists = builtin_exists(cmd); // Only reload environment variables if builtin_exists returned false, as an optimization if (head_exists == false) { - run_on_main_thread([&completion_snapshot]() { - completion_snapshot = std::move( - env_vars_snapshot_t((wchar_t const *const[]){L"fish_function_path", nullptr})); - }); - - head_exists = function_exists_no_autoload(cmd, completion_snapshot); + head_exists = function_exists_no_autoload(cmd.c_str(), vars); // While it may seem like first testing `path_get_path` before resorting to an env lookup // may be faster, path_get_path can potentially do a lot of FS/IO access, so env.get() + // function_exists() should still be faster. head_exists = - head_exists || - path_get_path(cmd_orig, nullptr); // use cmd_orig here as it is potentially pathed + head_exists || path_get_path(cmd_orig, nullptr, + vars); // use cmd_orig here as it is potentially pathed } if (!head_exists) { @@ -1095,7 +1085,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file, // See #4954. const wcstring sep_string = wcstring(str, sep_index + 1); std::vector local_completions; - if (expand_string(sep_string, &local_completions, flags, NULL) == EXPAND_ERROR) { + if (expand_string(sep_string, &local_completions, flags, vars, NULL) == EXPAND_ERROR) { debug(3, L"Error while expanding string '%ls'", sep_string.c_str()); } @@ -1114,7 +1104,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file, // consider relaxing this if there was a preceding double-dash argument. if (string_prefixes_string(L"-", str)) flags &= ~EXPAND_FUZZY_MATCH; - if (expand_string(str, &this->completions, flags, NULL) == EXPAND_ERROR) { + if (expand_string(str, &this->completions, flags, vars, NULL) == EXPAND_ERROR) { debug(3, L"Error while expanding string '%ls'", str.c_str()); } } @@ -1127,10 +1117,8 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) { size_t varlen = str.length() - start_offset; bool res = false; - const wcstring_list_t names = complete_get_variable_names(); - for (size_t i = 0; i < names.size(); i++) { - const wcstring &env_name = names.at(i); - + const wcstring_list_t names = vars.get_names(0); + for (const wcstring &env_name : vars.get_names(0)) { string_fuzzy_match_t match = string_fuzzy_match_string(var, env_name, this->max_fuzzy_match_type()); if (match.type == fuzzy_match_none) { @@ -1152,7 +1140,7 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) { wcstring desc; if (this->wants_descriptions()) { // Can't use this->vars here, it could be any variable. - auto var = env_get(env_name); + auto var = vars.get(env_name); if (!var) continue; wcstring value = expand_escape_variable(*var); @@ -1578,14 +1566,14 @@ void completer_t::perform() { } void complete(const wcstring &cmd_with_subcmds, std::vector *out_comps, - completion_request_flags_t flags) { + completion_request_flags_t flags, const environment_t &vars) { // Determine the innermost subcommand. const wchar_t *cmdsubst_begin, *cmdsubst_end; parse_util_cmdsubst_extent(cmd_with_subcmds.c_str(), cmd_with_subcmds.size(), &cmdsubst_begin, &cmdsubst_end); assert(cmdsubst_begin != NULL && cmdsubst_end != NULL && cmdsubst_end >= cmdsubst_begin); wcstring cmd = wcstring(cmdsubst_begin, cmdsubst_end - cmdsubst_begin); - completer_t completer(std::move(cmd), flags); + completer_t completer(vars, std::move(cmd), flags); completer.perform(); *out_comps = completer.acquire_completions(); } diff --git a/src/complete.h b/src/complete.h index 2349d1ff1..d73ae1d8c 100644 --- a/src/complete.h +++ b/src/complete.h @@ -30,6 +30,8 @@ /// Character that separates the completion and description on programmable completions. #define PROG_COMPLETE_SEP L'\t' +class environment_t; + enum { /// Do not insert space afterwards if this is the only completion. (The default is to try insert /// a space). @@ -172,7 +174,7 @@ void complete_remove_all(const wcstring &cmd, bool cmd_is_path); /// Find all completions of the command cmd, insert them into out. void complete(const wcstring &cmd, std::vector *out_comps, - completion_request_flags_t flags); + completion_request_flags_t flags, const environment_t &vars); /// Return a list of all current completions. wcstring complete_print(); @@ -194,8 +196,6 @@ void append_completion(std::vector *completions, wcstring comp, wcstring desc = wcstring(), int flags = 0, string_fuzzy_match_t match = string_fuzzy_match_t(fuzzy_match_exact)); -/// Function used for testing. -void complete_set_variable_names(const wcstring_list_t *names); /// Support for "wrap targets." A wrap target is a command that completes like another command. bool complete_add_wrapper(const wcstring &command, const wcstring &wrap_target); diff --git a/src/env.cpp b/src/env.cpp index 148e0eb71..f48cfde2b 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -95,7 +95,7 @@ bool term_has_xn = false; /// found in `TERMINFO_DIRS` we don't to call `handle_curses()` before we've imported the latter. static bool env_initialized = false; -typedef std::unordered_map +typedef std::unordered_map var_dispatch_table_t; static var_dispatch_table_t var_dispatch_table; @@ -112,15 +112,12 @@ static const wcstring_list_t locale_variables({L"LANG", L"LANGUAGE", L"LC_ALL", static const wcstring_list_t curses_variables({L"TERM", L"TERMINFO", L"TERMINFO_DIRS"}); // Some forward declarations to make it easy to logically group the code. -static void init_locale(); -static void init_curses(); +static void init_locale(const environment_t &vars); +static void init_curses(const environment_t &vars); // Struct representing one level in the function variable stack. // Only our variable stack should create and destroy these class env_node_t { - friend struct var_stack_t; - env_node_t(bool is_new_scope) : new_scope(is_new_scope) {} - public: /// Variable table. var_table_t env; @@ -132,38 +129,48 @@ class env_node_t { /// or does it redefine any variables to not be exported? bool exportv = false; /// Pointer to next level. - std::unique_ptr next; + std::shared_ptr next; + + env_node_t(bool is_new_scope) : new_scope(is_new_scope) {} maybe_t find_entry(const wcstring &key); bool contains_any_of(const wcstring_list_t &vars) const; }; -static std::mutex env_lock; +using env_node_ref_t = std::shared_ptr; -static bool local_scope_exports(const env_node_t *n); +static std::mutex env_lock; // A class wrapping up a variable stack // Currently there is only one variable stack in fish, // but we can imagine having separate (linked) stacks // if we introduce multiple threads of execution struct var_stack_t { - // Top node on the function stack. - std::unique_ptr top = NULL; + var_stack_t(var_stack_t &&) = default; - // Bottom node on the function stack - // This is an observer pointer - env_node_t *global_env = NULL; + // Top node on the function stack. + env_node_ref_t top; + + // Bottom node on the function stack. + env_node_ref_t global_env; // Exported variable array used by execv. - null_terminated_array_t export_array; + maybe_t> export_array; /// Flag for checking if we need to regenerate the exported variable array. - bool has_changed_exported = true; - void mark_changed_exported() { has_changed_exported = true; } + void mark_changed_exported() { export_array.reset(); } + + bool has_changed_exported() const { return !export_array; } + void update_export_array_if_necessary(); - var_stack_t() : top(new env_node_t(false)) { this->global_env = this->top.get(); } + var_stack_t() : top(globals()), global_env(globals()) { + // Add a toplevel local scope on top of the global scope. This local scope will persist + // throughout the lifetime of the fish process, and it will ensure that `set -l` commands + // run at the command-line don't affect the global scope. + push(false); + } // Pushes a new node onto our stack // Optionally creates a new scope for the node @@ -172,21 +179,40 @@ struct var_stack_t { // Pops the top node if it's not global void pop(); - // Returns the next scope to search for a given node, respecting the new_scope lag - // Returns NULL if we're done - env_node_t *next_scope_to_search(env_node_t *node); - const env_node_t *next_scope_to_search(const env_node_t *node) const; + // Returns the next scope to search for a given node, respecting the new_scope flag. + // Returns an empty pointer if we're done. + env_node_ref_t next_scope_to_search(const env_node_ref_t &node) const; // Returns the scope used for unspecified scopes. An unspecified scope is either the topmost // shadowing scope, or the global scope if none. This implements the default behavior of `set`. - env_node_t *resolve_unspecified_scope(); + env_node_ref_t resolve_unspecified_scope(); + + /// Copy this vars_stack. + var_stack_t clone() const { + return var_stack_t(*this); + } + + private: + /// Copy constructor. This does not copy the export array; it just allows it to be regenerated. + var_stack_t(const var_stack_t &rhs) : top(rhs.top), global_env(rhs.global_env) {} + + bool local_scope_exports(const env_node_ref_t &n) const; + void get_exported(const env_node_t *n, var_table_t &h) const; + + /// Returns the global variable set. + static env_node_ref_t globals(); }; +env_node_ref_t var_stack_t::globals() { + static env_node_ref_t s_globals{std::make_shared(false)}; + return s_globals; +} + void var_stack_t::push(bool new_scope) { - std::unique_ptr node(new env_node_t(new_scope)); + auto node = std::make_shared(new_scope); // Copy local-exported variables. - auto top_node = top.get(); + auto top_node = top; // Only if we introduce a new shadowing scope; i.e. not if it's just `begin; end` or // "--no-scope-shadowing". if (new_scope && top_node != this->global_env) { @@ -195,9 +221,9 @@ void var_stack_t::push(bool new_scope) { } } - node->next = std::move(this->top); - this->top = std::move(node); - if (new_scope && local_scope_exports(this->top.get())) { + node->next = this->top; + this->top = node; + if (new_scope && local_scope_exports(this->top)) { this->mark_changed_exported(); } } @@ -212,7 +238,7 @@ bool env_node_t::contains_any_of(const wcstring_list_t &vars) const { void var_stack_t::pop() { // Don't pop the top-most, global, level. - if (top.get() == this->global_env) { + if (top == this->global_env) { debug(0, _(L"Tried to pop empty environment stack.")); sanity_lose(); return; @@ -222,20 +248,14 @@ void var_stack_t::pop() { bool curses_changed = top->contains_any_of(curses_variables); if (top->new_scope) { //!OCLINT(collapsible if statements) - if (top->exportv || local_scope_exports(top->next.get())) { + if (top->exportv || local_scope_exports(top->next)) { this->mark_changed_exported(); } } - // Actually do the pop! Move the top pointer into a local variable, then replace the top pointer - // with the next pointer afterwards we should have a node with no next pointer, and our top - // should be non-null. - std::unique_ptr old_top = std::move(this->top); - this->top = std::move(old_top->next); - old_top->next.reset(); - assert(this->top && old_top && !old_top->next); - assert(this->top != NULL); - + // Actually do the pop! + env_node_ref_t old_top = this->top; + this->top = old_top->next; for (const auto &entry_pair : old_top->env) { const env_var_t &var = entry_pair.second; if (var.exports()) { @@ -244,39 +264,35 @@ void var_stack_t::pop() { } } - if (locale_changed) init_locale(); - if (curses_changed) init_curses(); + // TODO: instantize this locale and curses + const auto &vars = env_stack_t::principal(); + if (locale_changed) init_locale(vars); + if (curses_changed) init_curses(vars); } -const env_node_t *var_stack_t::next_scope_to_search(const env_node_t *node) const { +env_node_ref_t var_stack_t::next_scope_to_search(const env_node_ref_t &node) const { assert(node != NULL); if (node == this->global_env) { - return NULL; + return nullptr; } - return node->new_scope ? this->global_env : node->next.get(); + return node->new_scope ? this->global_env : node->next; } -env_node_t *var_stack_t::next_scope_to_search(env_node_t *node) { - assert(node != NULL); - if (node == this->global_env) { - return NULL; - } - return node->new_scope ? this->global_env : node->next.get(); -} - -env_node_t *var_stack_t::resolve_unspecified_scope() { - env_node_t *node = this->top.get(); +env_node_ref_t var_stack_t::resolve_unspecified_scope() { + env_node_ref_t node = this->top; while (node && !node->new_scope) { - node = node->next.get(); + node = node->next; } return node ? node : this->global_env; } -// Get the global variable stack -static var_stack_t &vars_stack() { - static var_stack_t global_stack; - return global_stack; -} +env_stack_t::env_stack_t() : vars_(make_unique()) {} +env_stack_t::env_stack_t(std::unique_ptr vars) : vars_(std::move(vars)) {} + +// Get the variable stack +var_stack_t &env_stack_t::vars_stack() { return *vars_; } + +const var_stack_t &env_stack_t::vars_stack() const { return *vars_; } /// Universal variables global instance. Initialized in env_init. static env_universal_t *s_universal_variables = NULL; @@ -332,9 +348,8 @@ static mode_t get_umask() { } /// Properly sets all timezone information. -static void handle_timezone(const wchar_t *env_var_name) { - // const env_var_t var = env_get(env_var_name, ENV_EXPORT); - const auto var = env_get(env_var_name, ENV_DEFAULT); +static void handle_timezone(const wchar_t *env_var_name, const environment_t &vars) { + const auto var = vars.get(env_var_name, ENV_DEFAULT); debug(2, L"handle_timezone() current timezone var: |%ls| => |%ls|", env_var_name, !var ? L"MISSING" : var->as_string().c_str()); const std::string &name = wcs2string(env_var_name); @@ -350,8 +365,8 @@ static void handle_timezone(const wchar_t *env_var_name) { /// Some env vars contain a list of paths where an empty path element is equivalent to ".". /// Unfortunately that convention causes problems for fish scripts. So this function replaces the /// empty path element with an explicit ".". See issue #3914. -static void fix_colon_delimited_var(const wcstring &var_name) { - const auto paths = env_get(var_name); +static void fix_colon_delimited_var(const wcstring &var_name, env_stack_t &vars) { + const auto paths = vars.get(var_name); if (paths.missing_or_empty()) return; // See if there's any empties. @@ -361,7 +376,7 @@ static void fix_colon_delimited_var(const wcstring &var_name) { // Copy the list and replace empties with L"." wcstring_list_t newstrs = strs; std::replace(newstrs.begin(), newstrs.end(), empty, wcstring(L".")); - int retval = env_set(var_name, ENV_DEFAULT | ENV_USER, std::move(newstrs)); + int retval = vars.set(var_name, ENV_DEFAULT | ENV_USER, std::move(newstrs)); if (retval != ENV_OK) { debug(0, L"fix_colon_delimited_var failed unexpectedly with retval %d", retval); } @@ -369,13 +384,13 @@ static void fix_colon_delimited_var(const wcstring &var_name) { } /// Initialize the locale subsystem. -static void init_locale() { +static void init_locale(const environment_t &vars) { // We have to make a copy because the subsequent setlocale() call to change the locale will // invalidate the pointer from the this setlocale() call. char *old_msg_locale = strdup(setlocale(LC_MESSAGES, NULL)); for (const auto &var_name : locale_variables) { - const auto var = env_get(var_name, ENV_EXPORT); + const auto var = vars.get(var_name, ENV_EXPORT); const std::string &name = wcs2string(var_name); if (var.missing_or_empty()) { debug(5, L"locale var %s missing or empty", name.c_str()); @@ -420,8 +435,8 @@ bool term_supports_setting_title() { return can_set_term_title; } /// terminal title if the underlying terminal does so, but will print garbage on terminals that /// don't. Since we can't see the underlying terminal below screen there is no way to fix this. static const wchar_t *const title_terms[] = {L"xterm", L"screen", L"tmux", L"nxterm", L"rxvt"}; -static bool does_term_support_setting_title() { - const auto term_var = env_get(L"TERM"); +static bool does_term_support_setting_title(const environment_t &vars) { + const auto term_var = vars.get(L"TERM"); if (term_var.missing_or_empty()) return false; const wcstring term_str = term_var->as_string(); @@ -446,11 +461,11 @@ static bool does_term_support_setting_title() { } /// Updates our idea of whether we support term256 and term24bit (see issue #10222). -static void update_fish_color_support() { +static void update_fish_color_support(const environment_t &vars) { // Detect or infer term256 support. If fish_term256 is set, we respect it; // otherwise infer it from the TERM variable or use terminfo. - auto fish_term256 = env_get(L"fish_term256"); - auto term_var = env_get(L"TERM"); + auto fish_term256 = vars.get(L"fish_term256"); + auto term_var = vars.get(L"TERM"); wcstring term = term_var.missing_or_empty() ? L"" : term_var->as_string(); bool support_term256 = false; // default to no support if (!fish_term256.missing_or_empty()) { @@ -462,8 +477,8 @@ static void update_fish_color_support() { debug(2, L"256 color support enabled for '256color' in TERM"); } else if (term.find(L"xterm") != wcstring::npos) { // Assume that all xterms are 256, except for OS X SnowLeopard - const auto prog_var = env_get(L"TERM_PROGRAM"); - const auto progver_var = env_get(L"TERM_PROGRAM_VERSION"); + const auto prog_var = vars.get(L"TERM_PROGRAM"); + const auto progver_var = vars.get(L"TERM_PROGRAM_VERSION"); wcstring term_program = prog_var.missing_or_empty() ? L"" : prog_var->as_string(); if (term_program == L"Apple_Terminal" && !progver_var.missing_or_empty()) { // OS X Lion is version 300+, it has 256 color support @@ -483,7 +498,7 @@ static void update_fish_color_support() { debug(2, L"256 color support not enabled (yet)"); } - auto fish_term24bit = env_get(L"fish_term24bit"); + auto fish_term24bit = vars.get(L"fish_term24bit"); bool support_term24bit; if (!fish_term24bit.missing_or_empty()) { support_term24bit = from_string(fish_term24bit->as_string()); @@ -506,7 +521,8 @@ static void update_fish_color_support() { static bool initialize_curses_using_fallback(const char *term) { // If $TERM is already set to the fallback name we're about to use there isn't any point in // seeing if the fallback name can be used. - auto term_var = env_get(L"TERM"); + auto &vars = env_stack_t::globals(); + auto term_var = vars.get(L"TERM"); if (term_var.missing_or_empty()) return false; auto term_env = wcs2string(term_var->as_string()); @@ -526,19 +542,20 @@ static bool initialize_curses_using_fallback(const char *term) { /// elements are converted to explicit "." to make the vars easier to use in fish scripts. static void init_path_vars() { // Do not replace empties in MATHPATH - see #4158. - fix_colon_delimited_var(L"PATH"); - fix_colon_delimited_var(L"CDPATH"); + fix_colon_delimited_var(L"PATH", env_stack_t::globals()); + fix_colon_delimited_var(L"CDPATH", env_stack_t::globals()); } /// Update the value of g_guessed_fish_emoji_width static void guess_emoji_width() { wcstring term; - if (auto term_var = env_get(L"TERM_PROGRAM")) { + auto &vars = env_stack_t::globals(); + if (auto term_var = vars.get(L"TERM_PROGRAM")) { term = term_var->as_string(); } double version = 0; - if (auto version_var = env_get(L"TERM_PROGRAM_VERSION")) { + if (auto version_var = vars.get(L"TERM_PROGRAM_VERSION")) { std::string narrow_version = wcs2string(version_var->as_string()); version = strtod(narrow_version.c_str(), NULL); } @@ -555,10 +572,10 @@ static void guess_emoji_width() { } /// Initialize the curses subsystem. -static void init_curses() { +static void init_curses(const environment_t &vars) { for (const auto &var_name : curses_variables) { std::string name = wcs2string(var_name); - const auto var = env_get(var_name, ENV_EXPORT); + const auto var = vars.get(var_name, ENV_EXPORT); if (var.missing_or_empty()) { debug(2, L"curses var %s missing or empty", name.c_str()); unsetenv(name.c_str()); @@ -571,7 +588,7 @@ static void init_curses() { int err_ret; if (setupterm(NULL, STDOUT_FILENO, &err_ret) == ERR) { - auto term = env_get(L"TERM"); + auto term = vars.get(L"TERM"); if (is_interactive_session) { debug(1, _(L"Could not set up terminal.")); if (term.missing_or_empty()) { @@ -587,16 +604,16 @@ static void init_curses() { } } - can_set_term_title = does_term_support_setting_title(); + can_set_term_title = does_term_support_setting_title(vars); term_has_xn = tigetflag((char *)"xenl") == 1; // does terminal have the eat_newline_glitch - update_fish_color_support(); + update_fish_color_support(vars); // Invalidate the cached escape sequences since they may no longer be valid. cached_layouts.clear(); curses_initialized = true; } /// React to modifying the given variable. -static void react_to_variable_change(const wchar_t *op, const wcstring &key) { +static void react_to_variable_change(const wchar_t *op, const wcstring &key, env_stack_t &vars) { // Don't do any of this until `env_init()` has run. We only want to do this in response to // variables set by the user; e.g., in a script like *config.fish* or interactively or as part // of loading the universal variables for the first time. Variables we import from the @@ -606,9 +623,7 @@ static void react_to_variable_change(const wchar_t *op, const wcstring &key) { auto dispatch = var_dispatch_table.find(key); if (dispatch != var_dispatch_table.end()) { - (*dispatch->second)(op, key); - } else if (string_prefixes_string(L"_fish_abbr_", key)) { - update_abbr_cache(op, key); + (*dispatch->second)(op, key, vars); } else if (string_prefixes_string(L"fish_color_", key)) { reader_react_to_color_change(); } @@ -616,11 +631,11 @@ static void react_to_variable_change(const wchar_t *op, const wcstring &key) { /// Universal variable callback function. This function makes sure the proper events are triggered /// when an event occurs. -static void universal_callback(const callback_data_t &cb) { +static void universal_callback(env_stack_t *stack, const callback_data_t &cb) { const wchar_t *op = cb.is_erase() ? L"ERASE" : L"SET"; - react_to_variable_change(op, cb.key); - vars_stack().mark_changed_exported(); + react_to_variable_change(op, cb.key, *stack); + stack->mark_changed_exported(); event_t ev = event_t::variable_event(cb.key); ev.arguments.push_back(L"VARIABLE"); @@ -631,39 +646,41 @@ static void universal_callback(const callback_data_t &cb) { /// Make sure the PATH variable contains something. static void setup_path() { - const auto path = env_get(L"PATH"); + auto &vars = env_stack_t::globals(); + const auto path = vars.get(L"PATH"); if (path.missing_or_empty()) { wcstring_list_t value({L"/usr/bin", L"/bin"}); - env_set(L"PATH", ENV_GLOBAL | ENV_EXPORT, value); + vars.set(L"PATH", ENV_GLOBAL | ENV_EXPORT, value); } } /// If they don't already exist initialize the `COLUMNS` and `LINES` env vars to reasonable /// defaults. They will be updated later by the `get_current_winsize()` function if they need to be /// adjusted. -static void env_set_termsize() { - auto cols = env_get(L"COLUMNS"); - if (cols.missing_or_empty()) env_set_one(L"COLUMNS", ENV_GLOBAL, DFLT_TERM_COL_STR); +void env_stack_t::set_termsize() { + auto &vars = env_stack_t::globals(); + auto cols = get(L"COLUMNS"); + if (cols.missing_or_empty()) vars.set_one(L"COLUMNS", ENV_GLOBAL, DFLT_TERM_COL_STR); - auto rows = env_get(L"LINES"); - if (rows.missing_or_empty()) env_set_one(L"LINES", ENV_GLOBAL, DFLT_TERM_ROW_STR); + auto rows = get(L"LINES"); + if (rows.missing_or_empty()) vars.set_one(L"LINES", ENV_GLOBAL, DFLT_TERM_ROW_STR); } /// Update the PWD variable directory from the result of getcwd(). -void env_set_pwd_from_getcwd() { +void env_stack_t::set_pwd_from_getcwd() { wcstring cwd = wgetcwd(); if (cwd.empty()) { debug(0, _(L"Could not determine current working directory. Is your locale set correctly?")); return; } - env_set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, std::move(cwd)); + set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, cwd); } /// Allow the user to override the limit on how much data the `read` command will process. /// This is primarily for testing but could be used by users in special situations. -void env_set_read_limit() { - auto read_byte_limit_var = env_get(L"fish_read_limit"); +void env_stack_t::set_read_limit() { + auto read_byte_limit_var = this->get(L"fish_read_limit"); if (!read_byte_limit_var.missing_or_empty()) { size_t limit = fish_wcstoull(read_byte_limit_var->as_string().c_str()); if (errno) { @@ -674,10 +691,12 @@ void env_set_read_limit() { } } -wcstring env_get_pwd_slash() { +void env_stack_t::mark_changed_exported() { vars_stack().mark_changed_exported(); } + +wcstring environment_t::get_pwd_slash() const { // Return "/" if PWD is missing. // See https://github.com/fish-shell/fish-shell/issues/5080 - auto pwd_var = env_get(L"PWD"); + auto pwd_var = get(L"PWD"); wcstring pwd; if (!pwd_var.missing_or_empty()) { pwd = pwd_var->as_string(); @@ -690,14 +709,15 @@ wcstring env_get_pwd_slash() { /// Set up the USER variable. static void setup_user(bool force) { - if (force || env_get(L"USER").missing_or_empty()) { + auto &vars = env_stack_t::globals(); + if (force || vars.get(L"USER").missing_or_empty()) { struct passwd userinfo; struct passwd *result; char buf[8192]; int retval = getpwuid_r(getuid(), &userinfo, buf, sizeof(buf), &result); if (!retval && result) { const wcstring uname = str2wcstring(userinfo.pw_name); - env_set_one(L"USER", ENV_GLOBAL | ENV_EXPORT, uname); + vars.set_one(L"USER", ENV_GLOBAL | ENV_EXPORT, uname); } } } @@ -705,8 +725,6 @@ static void setup_user(bool force) { /// Various things we need to initialize at run-time that don't really fit any of the other init /// routines. void misc_init() { - env_set_read_limit(); - // If stdout is open on a tty ensure stdio is unbuffered. That's because those functions might // be intermixed with `write()` calls and we need to ensure the writes are not reordered. See // issue #3748. @@ -716,13 +734,13 @@ void misc_init() { } } -static void env_universal_callbacks(const callback_data_list_t &callbacks) { +static void env_universal_callbacks(env_stack_t *stack, const callback_data_list_t &callbacks) { for (const callback_data_t &cb : callbacks) { - universal_callback(cb); + universal_callback(stack, cb); } } -void env_universal_barrier() { +void env_stack_t::universal_barrier() { ASSERT_IS_MAIN_THREAD(); if (!uvars()) return; @@ -732,93 +750,103 @@ void env_universal_barrier() { universal_notifier_t::default_notifier().post_notification(); } - env_universal_callbacks(callbacks); + env_universal_callbacks(this, callbacks); } -static void handle_fish_term_change(const wcstring &op, const wcstring &var_name) { +static void handle_fish_term_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); - update_fish_color_support(); + update_fish_color_support(vars); reader_react_to_color_change(); } -static void handle_escape_delay_change(const wcstring &op, const wcstring &var_name) { +static void handle_escape_delay_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); - update_wait_on_escape_ms(); + update_wait_on_escape_ms(vars); } -static void handle_change_emoji_width(const wcstring &op, const wcstring &var_name) { +static void handle_change_emoji_width(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { (void)op; (void)var_name; int new_width = 0; - if (auto width_str = env_get(L"fish_emoji_width")) { + if (auto width_str = vars.get(L"fish_emoji_width")) { new_width = fish_wcstol(width_str->as_string().c_str()); } g_fish_emoji_width = std::max(0, new_width); } -static void handle_change_ambiguous_width(const wcstring &op, const wcstring &var_name) { +static void handle_change_ambiguous_width(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { (void)op; (void)var_name; int new_width = 1; - if (auto width_str = env_get(L"fish_ambiguous_width")) { + if (auto width_str = vars.get(L"fish_ambiguous_width")) { new_width = fish_wcstol(width_str->as_string().c_str()); } g_fish_ambiguous_width = std::max(0, new_width); } -static void handle_term_size_change(const wcstring &op, const wcstring &var_name) { +static void handle_term_size_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); invalidate_termsize(true); // force fish to update its idea of the terminal size plus vars } -static void handle_read_limit_change(const wcstring &op, const wcstring &var_name) { +static void handle_read_limit_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); - env_set_read_limit(); + vars.set_read_limit(); } -static void handle_fish_history_change(const wcstring &op, const wcstring &var_name) { +static void handle_fish_history_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); - reader_change_history(history_session_id().c_str()); + reader_change_history(history_session_id(vars).c_str()); } -static void handle_function_path_change(const wcstring &op, const wcstring &var_name) { +static void handle_function_path_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); function_invalidate_path(); } -static void handle_complete_path_change(const wcstring &op, const wcstring &var_name) { +static void handle_complete_path_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); UNUSED(var_name); complete_invalidate_path(); } -static void handle_tz_change(const wcstring &op, const wcstring &var_name) { +static void handle_tz_change(const wcstring &op, const wcstring &var_name, env_stack_t &vars) { UNUSED(op); - handle_timezone(var_name.c_str()); + handle_timezone(var_name.c_str(), vars); } -static void handle_magic_colon_var_change(const wcstring &op, const wcstring &var_name) { +static void handle_magic_colon_var_change(const wcstring &op, const wcstring &var_name, + env_stack_t &vars) { UNUSED(op); - fix_colon_delimited_var(var_name); + fix_colon_delimited_var(var_name, vars); } -static void handle_locale_change(const wcstring &op, const wcstring &var_name) { +static void handle_locale_change(const wcstring &op, const wcstring &var_name, env_stack_t &vars) { UNUSED(op); UNUSED(var_name); - init_locale(); + init_locale(vars); } -static void handle_curses_change(const wcstring &op, const wcstring &var_name) { +static void handle_curses_change(const wcstring &op, const wcstring &var_name, env_stack_t &vars) { UNUSED(op); UNUSED(var_name); guess_emoji_width(); - init_curses(); + init_curses(vars); } /// Populate the dispatch table used by `react_to_variable_change()` to efficiently call the @@ -851,8 +879,7 @@ static void setup_var_dispatch_table() { void env_init(const struct config_paths_t *paths /* or NULL */) { setup_var_dispatch_table(); - // Now the environment variable handling is set up, the next step is to insert valid data. - + env_stack_t &vars = env_stack_t::globals(); // Import environment variables. Walk backwards so that the first one out of any duplicates wins // (See issue #2784). wcstring key, val; @@ -865,33 +892,33 @@ void env_init(const struct config_paths_t *paths /* or NULL */) { if (eql == wcstring::npos) { // No equal-sign found so treat it as a defined var that has no value(s). if (is_read_only(key_and_val) || is_electric(key_and_val)) continue; - env_set_empty(key_and_val, ENV_EXPORT | ENV_GLOBAL); + vars.set_empty(key_and_val, ENV_EXPORT | ENV_GLOBAL); } else { key.assign(key_and_val, 0, eql); val.assign(key_and_val, eql+1, wcstring::npos); if (is_read_only(key) || is_electric(key)) continue; - env_set(key, ENV_EXPORT | ENV_GLOBAL, {val}); + vars.set(key, ENV_EXPORT | ENV_GLOBAL, {val}); } } // Set the given paths in the environment, if we have any. if (paths != NULL) { - env_set_one(FISH_DATADIR_VAR, ENV_GLOBAL, paths->data); - env_set_one(FISH_SYSCONFDIR_VAR, ENV_GLOBAL, paths->sysconf); - env_set_one(FISH_HELPDIR_VAR, ENV_GLOBAL, paths->doc); - env_set_one(FISH_BIN_DIR, ENV_GLOBAL, paths->bin); + vars.set_one(FISH_DATADIR_VAR, ENV_GLOBAL, paths->data); + vars.set_one(FISH_SYSCONFDIR_VAR, ENV_GLOBAL, paths->sysconf); + vars.set_one(FISH_HELPDIR_VAR, ENV_GLOBAL, paths->doc); + vars.set_one(FISH_BIN_DIR, ENV_GLOBAL, paths->bin); } wcstring user_config_dir; path_get_config(user_config_dir); - env_set_one(FISH_CONFIG_DIR, ENV_GLOBAL, user_config_dir); + vars.set_one(FISH_CONFIG_DIR, ENV_GLOBAL, user_config_dir); wcstring user_data_dir; path_get_data(user_data_dir); - env_set_one(FISH_USER_DATA_DIR, ENV_GLOBAL, user_data_dir); + vars.set_one(FISH_USER_DATA_DIR, ENV_GLOBAL, user_data_dir); - init_locale(); - init_curses(); + init_locale(vars); + init_curses(vars); init_input(); init_path_vars(); guess_emoji_width(); @@ -908,23 +935,23 @@ void env_init(const struct config_paths_t *paths /* or NULL */) { setup_user(uid == 0); // Set up $IFS - this used to be in share/config.fish, but really breaks if it isn't done. - env_set_one(L"IFS", ENV_GLOBAL, L"\n \t"); + vars.set_one(L"IFS", ENV_GLOBAL, L"\n \t"); // Set up the version variable. wcstring version = str2wcstring(get_fish_version()); - env_set_one(L"version", ENV_GLOBAL, version); - env_set_one(L"FISH_VERSION", ENV_GLOBAL, version); + vars.set_one(L"version", ENV_GLOBAL, version); + vars.set_one(L"FISH_VERSION", ENV_GLOBAL, version); // Set the $fish_pid variable. - env_set_one(L"fish_pid", ENV_GLOBAL, to_string(getpid())); + vars.set_one(L"fish_pid", ENV_GLOBAL, to_string(getpid())); // Set the $hostname variable wcstring hostname = L"fish"; get_hostname_identifier(hostname); - env_set_one(L"hostname", ENV_GLOBAL, hostname); + vars.set_one(L"hostname", ENV_GLOBAL, hostname); - // Set up SHLVL variable. Not we can't use env_get because SHLVL is read-only, and therefore was - // not inherited from the environment. + // Set up SHLVL variable. Not we can't use vars.get() because SHLVL is read-only, and therefore + // was not inherited from the environment. wcstring nshlvl_str = L"1"; if (const char *shlvl_var = getenv("SHLVL")) { const wchar_t *end; @@ -934,15 +961,15 @@ void env_init(const struct config_paths_t *paths /* or NULL */) { nshlvl_str = to_string(shlvl_i + 1); } } - env_set_one(L"SHLVL", ENV_GLOBAL | ENV_EXPORT, nshlvl_str); + vars.set_one(L"SHLVL", ENV_GLOBAL | ENV_EXPORT, nshlvl_str); // Set up the HOME variable. // Unlike $USER, it doesn't seem that `su`s pass this along // if the target user is root, unless "--preserve-environment" is used. // Since that is an explicit choice, we should allow it to enable e.g. // env HOME=(mktemp -d) su --preserve-environment fish - if (env_get(L"HOME").missing_or_empty()) { - auto user_var = env_get(L"USER"); + if (vars.get(L"HOME").missing_or_empty()) { + auto user_var = vars.get(L"USER"); if (!user_var.missing_or_empty()) { char *unam_narrow = wcs2str(user_var->as_string()); struct passwd userinfo; @@ -952,7 +979,7 @@ void env_init(const struct config_paths_t *paths /* or NULL */) { if (retval || !result) { // Maybe USER is set but it's bogus. Reset USER from the db and try again. setup_user(true); - user_var = env_get(L"USER"); + user_var = vars.get(L"USER"); if (!user_var.missing_or_empty()) { unam_narrow = wcs2str(user_var->as_string()); retval = getpwnam_r(unam_narrow, &userinfo, buf, sizeof(buf), &result); @@ -960,34 +987,34 @@ void env_init(const struct config_paths_t *paths /* or NULL */) { } if (!retval && result && userinfo.pw_dir) { const wcstring dir = str2wcstring(userinfo.pw_dir); - env_set_one(L"HOME", ENV_GLOBAL | ENV_EXPORT, dir); + vars.set_one(L"HOME", ENV_GLOBAL | ENV_EXPORT, dir); } else { // We cannot get $HOME. This triggers warnings for history and config.fish already, // so it isn't necessary to warn here as well. - env_set_empty(L"HOME", ENV_GLOBAL | ENV_EXPORT); + vars.set_empty(L"HOME", ENV_GLOBAL | ENV_EXPORT); } free(unam_narrow); } else { // If $USER is empty as well (which we tried to set above), we can't get $HOME. - env_set_empty(L"HOME", ENV_GLOBAL | ENV_EXPORT); + vars.set_empty(L"HOME", ENV_GLOBAL | ENV_EXPORT); } } // initialize the PWD variable if necessary // Note we may inherit a virtual PWD that doesn't match what getcwd would return; respect that. - if (env_get(L"PWD").missing_or_empty()) { - env_set_pwd_from_getcwd(); + if (vars.get(L"PWD").missing_or_empty()) { + vars.set_pwd_from_getcwd(); } - env_set_termsize(); // initialize the terminal size variables - env_set_read_limit(); // initialize the read_byte_limit + vars.set_termsize(); // initialize the terminal size variables + vars.set_read_limit(); // initialize the read_byte_limit // Set g_use_posix_spawn. Default to true. - auto use_posix_spawn = env_get(L"fish_use_posix_spawn"); + auto use_posix_spawn = vars.get(L"fish_use_posix_spawn"); g_use_posix_spawn = use_posix_spawn.missing_or_empty() ? true : from_string(use_posix_spawn->as_string()); // Set fish_bind_mode to "default". - env_set_one(FISH_BIND_MODE_VAR, ENV_GLOBAL, DEFAULT_BIND_MODE); + vars.set_one(FISH_BIND_MODE_VAR, ENV_GLOBAL, DEFAULT_BIND_MODE); // This is somewhat subtle. At this point we consider our environment to be sufficiently // initialized that we can react to changes to variables. Prior to doing this we expect that the @@ -1000,19 +1027,14 @@ void env_init(const struct config_paths_t *paths /* or NULL */) { s_universal_variables = new env_universal_t(L""); callback_data_list_t callbacks; s_universal_variables->initialize(callbacks); - env_universal_callbacks(callbacks); - - // Now that the global scope is fully initialized, add a toplevel local scope. This same local - // scope will persist throughout the lifetime of the fish process, and it will ensure that `set - // -l` commands run at the command-line don't affect the global scope. - env_push(false); + env_universal_callbacks(&env_stack_t::principal(), callbacks); } /// Search all visible scopes in order for the specified key. Return the first scope in which it was /// found. -static env_node_t *env_get_node(const wcstring &key) { - env_node_t *env = vars_stack().top.get(); - while (env != NULL) { +env_node_ref_t env_stack_t::get_node(const wcstring &key) { + env_node_ref_t env = vars_stack().top; + while (env) { if (env->find_entry(key)) break; env = vars_stack().next_scope_to_search(env); } @@ -1026,14 +1048,14 @@ static int set_umask(const wcstring_list_t &list_val) { } if (errno || mask > 0777 || mask < 0) return ENV_INVALID; - // Do not actually create a umask variable. On env_get() it will be calculated. + // Do not actually create a umask variable. On env_stack_t::get() it will be calculated. umask(mask); return ENV_OK; } /// Set a universal variable, inheriting as applicable from the given old variable. static void env_set_internal_universal(const wcstring &key, wcstring_list_t val, - env_mode_flags_t input_var_mode) { + env_mode_flags_t input_var_mode, env_stack_t *stack) { ASSERT_IS_MAIN_THREAD(); if (!uvars()) return; env_mode_flags_t var_mode = input_var_mode; @@ -1068,7 +1090,7 @@ static void env_set_internal_universal(const wcstring &key, wcstring_list_t val, uvars()->set(key, new_var); env_universal_barrier(); if (new_var.exports() || (oldvar && oldvar->exports())) { - vars_stack().mark_changed_exported(); + stack->mark_changed_exported(); } } @@ -1088,11 +1110,10 @@ static void env_set_internal_universal(const wcstring &key, wcstring_list_t val, /// * ENV_SCOPE, the variable cannot be set in the given scope. This applies to readonly/electric /// variables set from the local or universal scopes, or set as exported. /// * ENV_INVALID, the variable value was invalid. This applies only to special variables. -static int env_set_internal(const wcstring &key, env_mode_flags_t input_var_mode, - wcstring_list_t val) { +int env_stack_t::set_internal(const wcstring &key, env_mode_flags_t input_var_mode, wcstring_list_t val) { ASSERT_IS_MAIN_THREAD(); env_mode_flags_t var_mode = input_var_mode; - bool has_changed_old = vars_stack().has_changed_exported; + bool has_changed_old = vars_stack().has_changed_exported(); int done = 0; if (val.size() == 1 && (key == L"PWD" || key == L"HOME")) { @@ -1100,7 +1121,7 @@ static int env_set_internal(const wcstring &key, env_mode_flags_t input_var_mode wcstring val_canonical = val.front(); path_make_canonical(val_canonical); if (val.front() != val_canonical) { - return env_set_internal(key, var_mode, {val_canonical}); + return set_internal(key, var_mode, {val_canonical}); } } @@ -1121,12 +1142,12 @@ static int env_set_internal(const wcstring &key, env_mode_flags_t input_var_mode if (var_mode & ENV_UNIVERSAL) { if (uvars()) { - env_set_internal_universal(key, std::move(val), var_mode); + env_set_internal_universal(key, std::move(val), var_mode, this); } } else { // Determine the node. bool has_changed_new = false; - env_node_t *preexisting_node = env_get_node(key); + env_node_ref_t preexisting_node = get_node(key); maybe_t preexisting_flags{}; if (preexisting_node != NULL) { var_table_t::const_iterator result = preexisting_node->env.find(key); @@ -1137,11 +1158,11 @@ static int env_set_internal(const wcstring &key, env_mode_flags_t input_var_mode } } - env_node_t *node = NULL; + env_node_ref_t node = nullptr; if (var_mode & ENV_GLOBAL) { node = vars_stack().global_env; } else if (var_mode & ENV_LOCAL) { - node = vars_stack().top.get(); + node = vars_stack().top; } else if (preexisting_node != NULL) { node = preexisting_node; if ((var_mode & (ENV_EXPORT | ENV_UNEXPORT)) == 0) { @@ -1157,7 +1178,7 @@ static int env_set_internal(const wcstring &key, env_mode_flags_t input_var_mode } if (uvars() && uvars()->get(key)) { // Modifying an existing universal variable. - env_set_internal_universal(key, std::move(val), var_mode); + env_set_internal_universal(key, std::move(val), var_mode, this); done = true; } else { // New variable with unspecified scope @@ -1219,44 +1240,40 @@ static int env_set_internal(const wcstring &key, env_mode_flags_t input_var_mode ev.arguments.push_back(L"VARIABLE"); ev.arguments.push_back(L"SET"); ev.arguments.push_back(key); - - // debug(1, L"env_set: fire events on variable |%ls|", key); event_fire(&ev); - // debug(1, L"env_set: return from event firing"); - - react_to_variable_change(L"SET", key); + react_to_variable_change(L"SET", key, *this); return ENV_OK; } /// Sets the variable with the specified name to the given values. -int env_set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t vals) { - return env_set_internal(key, mode, std::move(vals)); +int env_stack_t::set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t vals) { + return set_internal(key, mode, std::move(vals)); } /// Sets the variable with the specified name to a single value. -int env_set_one(const wcstring &key, env_mode_flags_t mode, wcstring val) { +int env_stack_t::set_one(const wcstring &key, env_mode_flags_t mode, wcstring val) { wcstring_list_t vals; vals.push_back(std::move(val)); - return env_set_internal(key, mode, std::move(vals)); + return set_internal(key, mode, std::move(vals)); } /// Sets the variable with the specified name without any (i.e., zero) values. -int env_set_empty(const wcstring &key, env_mode_flags_t mode) { - return env_set_internal(key, mode, {}); +int env_stack_t::set_empty(const wcstring &key, env_mode_flags_t mode) { + return set_internal(key, mode, {}); } /// Attempt to remove/free the specified key/value pair from the specified map. /// /// \return zero if the variable was not found, non-zero otherwise -static bool try_remove(env_node_t *n, const wchar_t *key, int var_mode) { - if (n == NULL) { +bool env_stack_t::try_remove(env_node_ref_t n, const wchar_t *key, int var_mode) { + if (n == nullptr) { return false; } var_table_t::iterator result = n->env.find(key); if (result != n->env.end()) { if (result->second.exports()) { - vars_stack().mark_changed_exported(); + mark_changed_exported(); } n->env.erase(result); return true; @@ -1269,19 +1286,19 @@ static bool try_remove(env_node_t *n, const wchar_t *key, int var_mode) { if (n->new_scope) { return try_remove(vars_stack().global_env, key, var_mode); } - return try_remove(n->next.get(), key, var_mode); + return try_remove(n->next, key, var_mode); } -int env_remove(const wcstring &key, int var_mode) { +int env_stack_t::remove(const wcstring &key, int var_mode) { ASSERT_IS_MAIN_THREAD(); - env_node_t *first_node; + env_node_ref_t first_node{}; int erased = 0; if ((var_mode & ENV_USER) && is_read_only(key)) { return ENV_SCOPE; } - first_node = vars_stack().top.get(); + first_node = vars_stack().top; if (!(var_mode & ENV_UNIVERSAL)) { if (var_mode & ENV_GLOBAL) { @@ -1319,7 +1336,7 @@ int env_remove(const wcstring &key, int var_mode) { if (is_exported) vars_stack().mark_changed_exported(); } - react_to_variable_change(L"ERASE", key); + react_to_variable_change(L"ERASE", key, *this); return erased ? ENV_OK : ENV_NOT_FOUND; } @@ -1345,7 +1362,7 @@ env_var_t::env_var_flags_t env_var_t::flags_for(const wchar_t *name) { return result; } -maybe_t env_get(const wcstring &key, env_mode_flags_t mode) { +maybe_t env_stack_t::get(const wcstring &key, env_mode_flags_t mode) const { const bool has_scope = mode & (ENV_LOCAL | ENV_GLOBAL | ENV_UNIVERSAL); const bool search_local = !has_scope || (mode & ENV_LOCAL); const bool search_global = !has_scope || (mode & ENV_GLOBAL); @@ -1355,7 +1372,7 @@ maybe_t env_get(const wcstring &key, env_mode_flags_t mode) { const bool search_unexported = (mode & ENV_UNEXPORT) || !(mode & ENV_EXPORT); // Make the assumption that electric keys can't be shadowed elsewhere, since we currently block - // that in env_set(). + // that in env_stack_t::set(). if (is_electric(key)) { if (!search_global) return none(); if (key == L"history") { @@ -1367,7 +1384,7 @@ maybe_t env_get(const wcstring &key, env_mode_flags_t mode) { history_t *history = reader_get_history(); if (!history) { - history = &history_t::history_with_name(history_session_id()); + history = &history_t::history_with_name(history_session_id(*this)); } wcstring_list_t result; if (history) history->get_history(result); @@ -1383,7 +1400,7 @@ maybe_t env_get(const wcstring &key, env_mode_flags_t mode) { if (search_local || search_global) { scoped_lock locker(env_lock); // lock around a local region - env_node_t *env = search_local ? vars_stack().top.get() : vars_stack().global_env; + env_node_ref_t env = search_local ? vars_stack().top : vars_stack().global_env; while (env != NULL) { if (env == vars_stack().global_env && !search_global) { @@ -1404,7 +1421,7 @@ maybe_t env_get(const wcstring &key, env_mode_flags_t mode) { if (!search_universal) return none(); // Another hack. Only do a universal barrier on the main thread (since it can change variable - // values). Make sure we do this outside the env_lock because it may itself call `env_get()`. + // values). Make sure we do this outside the env_lock because it may itself call `get()`. if (is_main_thread() && !get_proc_had_barrier()) { set_proc_had_barrier(true); env_universal_barrier(); @@ -1422,22 +1439,24 @@ maybe_t env_get(const wcstring &key, env_mode_flags_t mode) { return none(); } +void env_universal_barrier() { env_stack_t::principal().universal_barrier(); } + /// Returns true if the specified scope or any non-shadowed non-global subscopes contain an exported /// variable. -static bool local_scope_exports(const env_node_t *n) { - assert(n != NULL); - if (n == vars_stack().global_env) return false; +bool var_stack_t::local_scope_exports(const env_node_ref_t &n) const { + assert(n != nullptr); + if (n == global_env) return false; if (n->exportv) return true; if (n->new_scope) return false; - return local_scope_exports(n->next.get()); + return local_scope_exports(n->next); } -void env_push(bool new_scope) { vars_stack().push(new_scope); } +void env_stack_t::push(bool new_scope) { vars_stack().push(new_scope); } -void env_pop() { vars_stack().pop(); } +void env_stack_t::pop() { vars_stack().pop(); } /// Function used with to insert keys of one table into a set::set. static void add_key_to_string_set(const var_table_t &envs, std::set *str_set, @@ -1453,7 +1472,7 @@ static void add_key_to_string_set(const var_table_t &envs, std::set *s } } -wcstring_list_t env_get_names(int flags) { +wcstring_list_t env_stack_t::get_names(int flags) const { scoped_lock locker(env_lock); wcstring_list_t result; @@ -1462,7 +1481,7 @@ wcstring_list_t env_get_names(int flags) { int show_global = flags & ENV_GLOBAL; int show_universal = flags & ENV_UNIVERSAL; - const env_node_t *n = vars_stack().top.get(); + env_node_ref_t n = vars_stack().top; const bool show_exported = (flags & ENV_EXPORT) || !(flags & ENV_UNEXPORT); const bool show_unexported = (flags & ENV_UNEXPORT) || !(flags & ENV_EXPORT); @@ -1478,7 +1497,7 @@ wcstring_list_t env_get_names(int flags) { if (n->new_scope) break; else - n = n->next.get(); + n = n->next; } } @@ -1499,11 +1518,11 @@ wcstring_list_t env_get_names(int flags) { } /// Get list of all exported variables. -static void get_exported(const env_node_t *n, var_table_t &h) { +void var_stack_t::get_exported(const env_node_t *n, var_table_t &h) const { if (!n) return; if (n->new_scope) { - get_exported(vars_stack().global_env, h); + get_exported(global_env.get(), h); } else { get_exported(n->next.get(), h); } @@ -1544,11 +1563,11 @@ static std::vector get_export_list(const var_table_t &envs) { } void var_stack_t::update_export_array_if_necessary() { - if (!this->has_changed_exported) { + if (!this->has_changed_exported()) { return; } - debug(4, L"env_export_arr() recalc"); + debug(4, L"export_arr() recalc"); var_table_t vals; get_exported(this->top.get(), vals); @@ -1565,61 +1584,84 @@ void var_stack_t::update_export_array_if_necessary() { } } - export_array.set(get_export_list(vals)); - has_changed_exported = false; + export_array.emplace(get_export_list(vals)); } -const char *const *env_export_arr() { +const char *const *env_stack_t::export_arr() { ASSERT_IS_MAIN_THREAD(); ASSERT_IS_NOT_FORKED_CHILD(); vars_stack().update_export_array_if_necessary(); - return vars_stack().export_array.get(); + assert(vars_stack().export_array && "Should have export array"); + return vars_stack().export_array->get(); } -void env_set_argv(const wchar_t *const *argv) { +void env_stack_t::set_argv(const wchar_t *const *argv) { if (argv && *argv) { wcstring_list_t list; for (auto arg = argv; *arg; arg++) { - list.push_back(*arg); + list.emplace_back(*arg); } - - env_set(L"argv", ENV_LOCAL, list); + set(L"argv", ENV_LOCAL, std::move(list)); } else { - env_set_empty(L"argv", ENV_LOCAL); + set_empty(L"argv", ENV_LOCAL); } } -env_vars_snapshot_t::env_vars_snapshot_t(const wchar_t *const *keys) { +environment_t::~environment_t() = default; +env_stack_t::~env_stack_t() = default; +env_stack_t::env_stack_t(env_stack_t &&) = default; + +null_environment_t::null_environment_t() = default; +null_environment_t::~null_environment_t() = default; +maybe_t null_environment_t::get(const wcstring &key, env_mode_flags_t mode) const { + return none(); +} +wcstring_list_t null_environment_t::get_names(int flags) const { return {}; } + +env_stack_t env_stack_t::make_principal() { + const env_stack_t &gl = env_stack_t::globals(); + std::unique_ptr dup_stack = make_unique(gl.vars_stack().clone()); + return env_stack_t{std::move(dup_stack)}; +} + +env_stack_t &env_stack_t::principal() { + static env_stack_t s_principal = make_principal(); + return s_principal; +} + +env_stack_t &env_stack_t::globals() { + static env_stack_t s_global; + return s_global; +} + +env_vars_snapshot_t::env_vars_snapshot_t(const environment_t &source, const wchar_t *const *keys) { ASSERT_IS_MAIN_THREAD(); wcstring key; for (size_t i = 0; keys[i]; i++) { key.assign(keys[i]); - const auto var = env_get(key); + const auto var = source.get(key); if (var) { - vars[key] = *var; + vars[key] = std::move(*var); } } + names = source.get_names(0); } -env_vars_snapshot_t::env_vars_snapshot_t() {} +env_vars_snapshot_t::~env_vars_snapshot_t() = default; -// The "current" variables are not a snapshot at all, but instead trampoline to env_get, etc. -// We identify the current snapshot based on pointer values. -static const env_vars_snapshot_t sCurrentSnapshot; -const env_vars_snapshot_t &env_vars_snapshot_t::current() { return sCurrentSnapshot; } - -bool env_vars_snapshot_t::is_current() const { return this == &sCurrentSnapshot; } - -maybe_t env_vars_snapshot_t::get(const wcstring &key) const { - // If we represent the current state, bounce to env_get. - if (this->is_current()) { - return env_get(key); - } +maybe_t env_vars_snapshot_t::get(const wcstring &key, env_mode_flags_t mode) const { auto iter = vars.find(key); if (iter == vars.end()) return none(); return iter->second; } +wcstring_list_t env_vars_snapshot_t::get_names(int flags) const { return names; } + +const wchar_t *const env_vars_snapshot_t::highlighting_keys[] = {L"PATH", L"CDPATH", + L"fish_function_path", NULL}; + +const wchar_t *const env_vars_snapshot_t::completing_keys[] = {L"PATH", L"CDPATH", + L"fish_function_path", NULL}; #if defined(__APPLE__) || defined(__CYGWIN__) static int check_runtime_path(const char *path) { @@ -1682,9 +1724,3 @@ wcstring env_get_runtime_path() { } return result; } - - -const wchar_t *const env_vars_snapshot_t::highlighting_keys[] = {L"PATH", L"CDPATH", - L"fish_function_path", NULL}; - -const wchar_t *const env_vars_snapshot_t::completing_keys[] = {L"PATH", L"CDPATH", NULL}; diff --git a/src/env.h b/src/env.h index 30fb4ab0d..970d05cad 100644 --- a/src/env.h +++ b/src/env.h @@ -16,10 +16,14 @@ extern size_t read_byte_limit; extern bool curses_initialized; -// Flags that may be passed as the 'mode' in env_set / env_get. +/// Character for separating two array elements. We use 30, i.e. the ascii record separator since +/// that seems logical. +#define ARRAY_SEP (wchar_t)0x1e + +// Flags that may be passed as the 'mode' in env_stack_t::set() / environment_t::get(). enum { - /// Default mode. Used with `env_get()` to indicate the caller doesn't care what scope the var - /// is in or whether it is exported or unexported. + /// Default mode. Used with `env_stack_t::get()` to indicate the caller doesn't care what scope + /// the var is in or whether it is exported or unexported. ENV_DEFAULT = 0, /// Flag for local (to the current block) variable. ENV_LOCAL = 1 << 0, @@ -43,7 +47,7 @@ enum { }; typedef uint32_t env_mode_flags_t; -/// Return values for `env_set()`. +/// Return values for `env_stack_t::set()`. enum { ENV_OK, ENV_PERM, ENV_SCOPE, ENV_INVALID, ENV_NOT_FOUND }; /// A struct of configuration directories, determined in main() that fish will optionally pass to @@ -131,70 +135,135 @@ class env_var_t { bool operator!=(const env_var_t &rhs) const { return ! (*this == rhs); } }; -/// Gets the variable with the specified name, or none() if it does not exist. -maybe_t env_get(const wcstring &key, env_mode_flags_t mode = ENV_DEFAULT); +/// An environment is read-only access to variable values. +class environment_t { + protected: + environment_t() = default; -/// Sets the variable with the specified name to the given values. -int env_set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t vals); + public: + virtual maybe_t get(const wcstring &key, + env_mode_flags_t mode = ENV_DEFAULT) const = 0; + virtual wcstring_list_t get_names(int flags) const = 0; + virtual ~environment_t(); -/// Sets the variable with the specified name to a single value. -int env_set_one(const wcstring &key, env_mode_flags_t mode, wcstring val); + /// Returns the PWD with a terminating slash. + wcstring get_pwd_slash() const; +}; -/// Sets the variable with the specified name to no values. -int env_set_empty(const wcstring &key, env_mode_flags_t mode); +/// The null environment contains nothing. +class null_environment_t : public environment_t { + public: + null_environment_t(); + ~null_environment_t() override; -/// Remove environment variable. -/// -/// \param key The name of the variable to remove -/// \param mode should be ENV_USER if this is a remove request from the user, 0 otherwise. If this -/// is a user request, read-only variables can not be removed. The mode may also specify the scope -/// of the variable that should be erased. -/// -/// \return zero if the variable existed, and non-zero if the variable did not exist -int env_remove(const wcstring &key, int mode); - -/// Push the variable stack. Used for implementing local variables for functions and for-loops. -void env_push(bool new_scope); - -/// Pop the variable stack. Used for implementing local variables for functions and for-loops. -void env_pop(); + maybe_t get(const wcstring &key, env_mode_flags_t mode = ENV_DEFAULT) const override; + wcstring_list_t get_names(int flags) const override; +}; /// Synchronizes all universal variable changes: writes everything out, reads stuff in. void env_universal_barrier(); -/// Returns an array containing all exported variables in a format suitable for execv -const char *const *env_export_arr(); +/// A environment stack of scopes. This is the main class that tracks fish variables. +struct var_stack_t; +class env_node_t; +class env_stack_t final : public environment_t { + friend class parser_t; + std::unique_ptr vars_; -/// Sets up argv as the given null terminated array of strings. -void env_set_argv(const wchar_t *const *argv); + int set_internal(const wcstring &key, env_mode_flags_t var_mode, wcstring_list_t val); -/// Returns all variable names. -wcstring_list_t env_get_names(int flags); + bool try_remove(std::shared_ptr n, const wchar_t *key, int var_mode); + std::shared_ptr get_node(const wcstring &key); -/// Update the PWD variable based on the result of getcwd. -void env_set_pwd_from_getcwd(); + static env_stack_t make_principal(); -/// Returns the PWD with a terminating slash. -wcstring env_get_pwd_slash(); + var_stack_t &vars_stack(); + const var_stack_t &vars_stack() const; -/// Update the read_byte_limit variable. -void env_set_read_limit(); + explicit env_stack_t(std::unique_ptr vars_); + env_stack_t(); + ~env_stack_t() override; -class env_vars_snapshot_t { - std::map vars; - bool is_current() const; + env_stack_t(env_stack_t &&); public: + /// Gets the variable with the specified name, or none() if it does not exist. + maybe_t get(const wcstring &key, env_mode_flags_t mode = ENV_DEFAULT) const override; + + /// Sets the variable with the specified name to the given values. + int set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t vals); + + /// Sets the variable with the specified name to a single value. + int set_one(const wcstring &key, env_mode_flags_t mode, wcstring val); + + /// Sets the variable with the specified name to no values. + int set_empty(const wcstring &key, env_mode_flags_t mode); + + /// Update the PWD variable based on the result of getcwd. + void set_pwd_from_getcwd(); + + /// Remove environment variable. + /// + /// \param key The name of the variable to remove + /// \param mode should be ENV_USER if this is a remove request from the user, 0 otherwise. If + /// this is a user request, read-only variables can not be removed. The mode may also specify + /// the scope of the variable that should be erased. + /// + /// \return zero if the variable existed, and non-zero if the variable did not exist + int remove(const wcstring &key, int mode); + + /// Push the variable stack. Used for implementing local variables for functions and for-loops. + void push(bool new_scope); + + /// Pop the variable stack. Used for implementing local variables for functions and for-loops. + void pop(); + + /// Synchronizes all universal variable changes: writes everything out, reads stuff in. + void universal_barrier(); + + /// Returns an array containing all exported variables in a format suitable for execv + const char *const *export_arr(); + + /// Returns all variable names. + wcstring_list_t get_names(int flags) const override; + + /// Update the termsize variable. + void set_termsize(); + + /// Update the PWD variable directory. + bool set_pwd(); + + /// Sets up argv as the given null terminated array of strings. + void set_argv(const wchar_t *const *argv); + + /// Update the read_byte_limit variable. + void set_read_limit(); + + /// Mark that exported variables have changed. + void mark_changed_exported(); + + // Compatibility hack; access the "environment stack" from back when there was just one. + static env_stack_t &principal(); + + // Access a variable stack that only represents globals. + // Do not push or pop from this. + static env_stack_t &globals(); +}; + +class env_vars_snapshot_t : public environment_t { + std::map vars; + wcstring_list_t names; + + public: + env_vars_snapshot_t() = default; env_vars_snapshot_t(const env_vars_snapshot_t &) = default; env_vars_snapshot_t &operator=(const env_vars_snapshot_t &) = default; + env_vars_snapshot_t(const environment_t &source, const wchar_t *const *keys); + ~env_vars_snapshot_t() override; - env_vars_snapshot_t(const wchar_t *const *keys); - env_vars_snapshot_t(); + maybe_t get(const wcstring &key, env_mode_flags_t mode = ENV_DEFAULT) const override; - maybe_t get(const wcstring &key) const; - - // Returns the fake snapshot representing the live variables array. - static const env_vars_snapshot_t ¤t(); + wcstring_list_t get_names(int flags) const override; // Vars necessary for highlighting. static const wchar_t *const highlighting_keys[]; diff --git a/src/exec.cpp b/src/exec.cpp index 0bbd8721b..294f6eef7 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -202,14 +202,14 @@ static void safe_launch_process(process_t *p, const char *actual_cmd, const char /// This function is similar to launch_process, except it is not called after a fork (i.e. it only /// calls exec) and therefore it can allocate memory. -static void launch_process_nofork(process_t *p) { +static void launch_process_nofork(env_stack_t &vars, process_t *p) { ASSERT_IS_MAIN_THREAD(); ASSERT_IS_NOT_FORKED_CHILD(); null_terminated_array_t argv_array; convert_wide_array_to_narrow(p->get_argv_array(), &argv_array); - const char *const *envv = env_export_arr(); + const char *const *envv = vars.export_arr(); char *actual_cmd = wcs2str(p->actual_cmd); // Ensure the terminal modes are what they were before we changed them. @@ -358,7 +358,7 @@ static bool can_use_posix_spawn_for_job(const std::shared_ptr &job, return result; } -void internal_exec(job_t *j, const io_chain_t &&all_ios) { +void internal_exec(env_stack_t &vars, job_t *j, const io_chain_t &all_ios) { // Do a regular launch - but without forking first... // setup_child_process makes sure signals are properly set up. @@ -369,7 +369,7 @@ void internal_exec(job_t *j, const io_chain_t &&all_ios) { // really make sense, so I'm not trying to fix it here. if (!setup_child_process(0, all_ios)) { // Decrement SHLVL as we're removing ourselves from the shell "stack". - auto shlvl_var = env_get(L"SHLVL", ENV_GLOBAL | ENV_EXPORT); + auto shlvl_var = vars.get(L"SHLVL", ENV_GLOBAL | ENV_EXPORT); wcstring shlvl_str = L"0"; if (shlvl_var) { long shlvl = fish_wcstol(shlvl_var->as_string().c_str()); @@ -377,10 +377,10 @@ void internal_exec(job_t *j, const io_chain_t &&all_ios) { shlvl_str = to_string(shlvl - 1); } } - env_set_one(L"SHLVL", ENV_GLOBAL | ENV_EXPORT, shlvl_str); + vars.set_one(L"SHLVL", ENV_GLOBAL | ENV_EXPORT, std::move(shlvl_str)); // launch_process _never_ returns. - launch_process_nofork(j->processes.front().get()); + launch_process_nofork(vars, j->processes.front().get()); } else { j->set_flag(job_flag_t::CONSTRUCTED, true); j->processes.front()->completed = 1; @@ -648,8 +648,8 @@ static bool handle_builtin_output(const std::shared_ptr &j, process_t *p, /// Executes an external command. /// \return true on success, false if there is an exec error. -static bool exec_external_command(const std::shared_ptr &j, process_t *p, - const io_chain_t &proc_io_chain) { +static bool exec_external_command(env_stack_t &vars, const std::shared_ptr &j, + process_t *p, const io_chain_t &proc_io_chain) { assert(p->type == EXTERNAL && "Process is not external"); // Get argv and envv before we fork. null_terminated_array_t argv_array; @@ -663,7 +663,7 @@ static bool exec_external_command(const std::shared_ptr &j, process_t *p, make_fd_blocking(STDIN_FILENO); const char *const *argv = argv_array.get(); - const char *const *envv = env_export_arr(); + const char *const *envv = vars.export_arr(); std::string actual_cmd_str = wcs2string(p->actual_cmd); const char *actual_cmd = actual_cmd_str.c_str(); @@ -787,7 +787,7 @@ static bool exec_block_or_func_process(parser_t &parser, std::shared_ptr function_block_t *fb = parser.push_block(p, func_name, props->shadow_scope); - function_prepare_environment(func_name, p->get_argv() + 1, inherit_vars); + function_prepare_environment(parser.vars(), func_name, p->get_argv() + 1, inherit_vars); parser.forbid_function(func_name); internal_exec_helper(parser, props->parsed_source, props->body_node, io_chain, j); @@ -919,7 +919,7 @@ static bool exec_process_in_job(parser_t &parser, process_t *p, std::shared_ptr< set_proc_had_barrier(true); env_universal_barrier(); } - env_export_arr(); + parser.vars().export_arr(); } // Set up fds that will be used in the pipe. @@ -975,7 +975,7 @@ static bool exec_process_in_job(parser_t &parser, process_t *p, std::shared_ptr< } case EXTERNAL: { - if (!exec_external_command(j, p, process_net_io_chain)) { + if (!exec_external_command(parser.vars(), j, p, process_net_io_chain)) { return false; } break; @@ -1028,7 +1028,7 @@ bool exec_job(parser_t &parser, shared_ptr j) { } if (j->processes.front()->type == INTERNAL_EXEC) { - internal_exec(j.get(), std::move(all_ios)); + internal_exec(parser.vars(), j.get(), all_ios); DIE("this should be unreachable"); } @@ -1074,7 +1074,7 @@ bool exec_job(parser_t &parser, shared_ptr j) { j->set_flag(job_flag_t::CONSTRUCTED, true); if (!j->is_foreground()) { - env_set_one(L"last_pid", ENV_GLOBAL, to_string(j->pgid)); + parser.vars().set_one(L"last_pid", ENV_GLOBAL, to_string(j->pgid)); } if (exec_error) { @@ -1085,14 +1085,14 @@ bool exec_job(parser_t &parser, shared_ptr j) { return true; } -static int exec_subshell_internal(const wcstring &cmd, wcstring_list_t *lst, bool apply_exit_status, - bool is_subcmd) { +static int exec_subshell_internal(const wcstring &cmd, parser_t &parser, wcstring_list_t *lst, + bool apply_exit_status, bool is_subcmd) { ASSERT_IS_MAIN_THREAD(); bool prev_subshell = is_subshell; const int prev_status = proc_get_last_status(); bool split_output = false; - const auto ifs = env_get(L"IFS"); + const auto ifs = parser.vars().get(L"IFS"); if (!ifs.missing_or_empty()) { split_output = true; } @@ -1163,13 +1163,13 @@ static int exec_subshell_internal(const wcstring &cmd, wcstring_list_t *lst, boo return subcommand_status; } -int exec_subshell(const wcstring &cmd, std::vector &outputs, bool apply_exit_status, - bool is_subcmd) { +int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector &outputs, + bool apply_exit_status, bool is_subcmd) { ASSERT_IS_MAIN_THREAD(); - return exec_subshell_internal(cmd, &outputs, apply_exit_status, is_subcmd); + return exec_subshell_internal(cmd, parser, &outputs, apply_exit_status, is_subcmd); } -int exec_subshell(const wcstring &cmd, bool apply_exit_status, bool is_subcmd) { +int exec_subshell(const wcstring &cmd, parser_t &parser, bool apply_exit_status, bool is_subcmd) { ASSERT_IS_MAIN_THREAD(); - return exec_subshell_internal(cmd, NULL, apply_exit_status, is_subcmd); + return exec_subshell_internal(cmd, parser, NULL, apply_exit_status, is_subcmd); } diff --git a/src/exec.h b/src/exec.h index 5e04fe3e5..010de4654 100644 --- a/src/exec.h +++ b/src/exec.h @@ -23,9 +23,10 @@ bool exec_job(parser_t &parser, std::shared_ptr j); /// \param outputs The list to insert output into. /// /// \return the status of the last job to exit, or -1 if en error was encountered. -int exec_subshell(const wcstring &cmd, std::vector &outputs, bool preserve_exit_status, +int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector &outputs, + bool preserve_exit_status, bool is_subcmd = false); +int exec_subshell(const wcstring &cmd, parser_t &parser, bool preserve_exit_status, bool is_subcmd = false); -int exec_subshell(const wcstring &cmd, bool preserve_exit_status, bool is_subcmd = false); /// Loops over close until the syscall was run without being interrupted. void exec_close(int fd); diff --git a/src/expand.cpp b/src/expand.cpp index 2f4dc27a9..1d0b1601c 100644 --- a/src/expand.cpp +++ b/src/expand.cpp @@ -43,6 +43,7 @@ #include "iothread.h" #include "parse_constants.h" #include "parse_util.h" +#include "parser.h" #include "path.h" #include "proc.h" #include "reader.h" @@ -292,7 +293,7 @@ static size_t parse_slice(const wchar_t *in, wchar_t **end_ptr, std::vector *out, size_t last_idx, - parse_error_list_t *errors) { + const environment_t &vars, parse_error_list_t *errors) { const size_t insize = instr.size(); // last_idx may be 1 past the end of the string, but no further. @@ -353,10 +354,10 @@ static bool expand_variables(wcstring instr, std::vector *out, siz // Note reader_get_history may return null, if we are running non-interactively (e.g. from // web_config). if (is_main_thread()) { - history = &history_t::history_with_name(history_session_id()); + history = &history_t::history_with_name(history_session_id(env_stack_t::principal())); } } else if (var_name != wcstring{VARIABLE_EXPAND_EMPTY}) { - var = env_get(var_name); + var = vars.get(var_name); } // Parse out any following slice. @@ -406,7 +407,7 @@ static bool expand_variables(wcstring instr, std::vector *out, siz res.push_back(VARIABLE_EXPAND_EMPTY); } res.append(instr, var_name_and_slice_stop, wcstring::npos); - return expand_variables(std::move(res), out, varexp_char_idx, errors); + return expand_variables(std::move(res), out, varexp_char_idx, vars, errors); } } @@ -463,7 +464,7 @@ static bool expand_variables(wcstring instr, std::vector *out, siz // Append all entries in var_item_list, separated by the delimiter. res.append(join_strings(var_item_list, delimit)); res.append(instr, var_name_and_slice_stop, wcstring::npos); - return expand_variables(std::move(res), out, varexp_char_idx, errors); + return expand_variables(std::move(res), out, varexp_char_idx, vars, errors); } else { // Normal cartesian-product expansion. for (const wcstring &item : var_item_list) { @@ -480,7 +481,7 @@ static bool expand_variables(wcstring instr, std::vector *out, siz } new_in.append(item); new_in.append(instr, var_name_and_slice_stop, wcstring::npos); - if (!expand_variables(std::move(new_in), out, varexp_char_idx, errors)) { + if (!expand_variables(std::move(new_in), out, varexp_char_idx, vars, errors)) { return false; } } @@ -637,7 +638,10 @@ static bool expand_cmdsubst(const wcstring &input, std::vector *ou wcstring_list_t sub_res; const wcstring subcmd(paren_begin + 1, paren_end - paren_begin - 1); - if (exec_subshell(subcmd, sub_res, true /* apply_exit_status */, true /* is_subcmd */) == -1) { + // TODO: justify this parser_t::principal_parser + auto &parser = parser_t::principal_parser(); + if (exec_subshell(subcmd, parser, sub_res, true /* apply_exit_status */, + true /* is_subcmd */) == -1) { append_cmdsub_error(errors, SOURCE_LOCATION_UNKNOWN, L"Unknown error while evaulating command substitution"); return false; @@ -742,7 +746,7 @@ static wcstring get_home_directory_name(const wcstring &input, size_t *out_tail_ } /// Attempts tilde expansion of the string specified, modifying it in place. -static void expand_home_directory(wcstring &input) { +static void expand_home_directory(wcstring &input, const environment_t &vars) { if (!input.empty() && input.at(0) == HOME_DIRECTORY) { size_t tail_idx; wcstring username = get_home_directory_name(input, &tail_idx); @@ -750,7 +754,7 @@ static void expand_home_directory(wcstring &input) { maybe_t home; if (username.empty()) { // Current users home directory. - auto home_var = env_get(L"HOME"); + auto home_var = vars.get(L"HOME"); if (home_var.missing_or_empty()) { input.clear(); return; @@ -787,16 +791,17 @@ static void expand_percent_self(wcstring &input) { } } -void expand_tilde(wcstring &input) { +void expand_tilde(wcstring &input, const environment_t &vars) { // Avoid needless COW behavior by ensuring we use const at. const wcstring &tmp = input; if (!tmp.empty() && tmp.at(0) == L'~') { input.at(0) = HOME_DIRECTORY; - expand_home_directory(input); + expand_home_directory(input, vars); } } -static void unexpand_tildes(const wcstring &input, std::vector *completions) { +static void unexpand_tildes(const wcstring &input, const environment_t &vars, + std::vector *completions) { // If input begins with tilde, then try to replace the corresponding string in each completion // with the tilde. If it does not, there's nothing to do. if (input.empty() || input.at(0) != L'~') return; @@ -818,7 +823,7 @@ static void unexpand_tildes(const wcstring &input, std::vector *co // Expand username_with_tilde. wcstring home = username_with_tilde; - expand_tilde(home); + expand_tilde(home, vars); // Now for each completion that starts with home, replace it with the username_with_tilde. for (size_t i = 0; i < completions->size(); i++) { @@ -835,12 +840,12 @@ static void unexpand_tildes(const wcstring &input, std::vector *co // If the given path contains the user's home directory, replace that with a tilde. We don't try to // be smart about case insensitivity, etc. -wcstring replace_home_directory_with_tilde(const wcstring &str) { +wcstring replace_home_directory_with_tilde(const wcstring &str, const environment_t &vars) { // Only absolute paths get this treatment. wcstring result = str; if (string_prefixes_string(L"/", result)) { wcstring home_directory = L"~"; - expand_tilde(home_directory); + expand_tilde(home_directory, vars); if (!string_suffixes_string(L"/", home_directory)) { home_directory.push_back(L'/'); } @@ -883,15 +888,17 @@ static void remove_internal_separator(wcstring *str, bool conv) { } /// A stage in string expansion is represented as a function that takes an input and returns a list -/// of output (by reference). We get flags and errors. It may return an error; if so expansion +/// of output (by reference). We get flags, vars and errors. It may return an error; if so expansion /// halts. typedef expand_error_t (*expand_stage_t)(wcstring input, //!OCLINT(unused param) std::vector *out, //!OCLINT(unused param) expand_flags_t flags, //!OCLINT(unused param) + const environment_t &vars, //!OCLINT(unused param) parse_error_list_t *errors); //!OCLINT(unused param) static expand_error_t expand_stage_cmdsubst(wcstring input, std::vector *out, - expand_flags_t flags, parse_error_list_t *errors) { + expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors) { if (EXPAND_SKIP_CMDSUBST & flags) { wchar_t *begin, *end; if (parse_util_locate_cmdsubst(input.c_str(), &begin, &end, true) == 0) { @@ -910,7 +917,8 @@ static expand_error_t expand_stage_cmdsubst(wcstring input, std::vector *out, - expand_flags_t flags, parse_error_list_t *errors) { + expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors) { // We accept incomplete strings here, since complete uses expand_string to expand incomplete // strings from the commandline. wcstring next; @@ -925,7 +933,7 @@ static expand_error_t expand_stage_variables(wcstring input, std::vector *out, - expand_flags_t flags, parse_error_list_t *errors) { + expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors) { return expand_braces(input, flags, out, errors); } static expand_error_t expand_stage_home_and_self(wcstring input, std::vector *out, - expand_flags_t flags, parse_error_list_t *errors) { + expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors) { (void)errors; if (!(EXPAND_SKIP_HOME_DIRECTORIES & flags)) { - expand_home_directory(input); + expand_home_directory(input, vars); } expand_percent_self(input); append_completion(out, std::move(input)); return EXPAND_OK; } -static expand_error_t expand_stage_wildcards(wcstring path_to_expand, - std::vector *out, expand_flags_t flags, +static expand_error_t expand_stage_wildcards(wcstring path_to_expand, std::vector *out, + expand_flags_t flags, const environment_t &vars, parse_error_list_t *errors) { UNUSED(errors); expand_error_t result = EXPAND_OK; @@ -968,7 +978,7 @@ static expand_error_t expand_stage_wildcards(wcstring path_to_expand, // // So we're going to treat this input as a file path. Compute the "working directories", // which may be CDPATH if the special flag is set. - const wcstring working_dir = env_get_pwd_slash(); + const wcstring working_dir = vars.get_pwd_slash(); wcstring_list_t effective_working_dirs; bool for_cd = static_cast(flags & EXPAND_SPECIAL_FOR_CD); bool for_command = static_cast(flags & EXPAND_SPECIAL_FOR_COMMAND); @@ -1000,7 +1010,7 @@ static expand_error_t expand_stage_wildcards(wcstring path_to_expand, // Get the PATH/CDPATH and CWD. Perhaps these should be passed in. An empty CDPATH // implies just the current directory, while an empty PATH is left empty. wcstring_list_t paths; - if (auto paths_var = env_get(for_cd ? L"CDPATH" : L"PATH")) { + if (auto paths_var = vars.get(for_cd ? L"CDPATH" : L"PATH")) { paths = paths_var->as_list(); } if (paths.empty()) { @@ -1042,7 +1052,8 @@ static expand_error_t expand_stage_wildcards(wcstring path_to_expand, } expand_error_t expand_string(wcstring input, std::vector *out_completions, - expand_flags_t flags, parse_error_list_t *errors) { + expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors) { // Early out. If we're not completing, and there's no magic in the input, we're done. if (!(flags & EXPAND_FOR_COMPLETIONS) && expand_is_clean(input)) { append_completion(out_completions, std::move(input)); @@ -1064,7 +1075,7 @@ expand_error_t expand_string(wcstring input, std::vector *out_comp for (size_t i = 0; total_result != EXPAND_ERROR && i < completions.size(); i++) { wcstring &next = completions.at(i).completion; expand_error_t this_result = - stages[stage_idx](std::move(next), &output_storage, flags, errors); + stages[stage_idx](std::move(next), &output_storage, flags, vars, errors); // If this_result was no match, but total_result is that we have a match, then don't // change it. if (!(this_result == EXPAND_WILDCARD_NO_MATCH && @@ -1081,7 +1092,7 @@ expand_error_t expand_string(wcstring input, std::vector *out_comp if (total_result != EXPAND_ERROR) { // Hack to un-expand tildes (see #647). if (!(flags & EXPAND_SKIP_HOME_DIRECTORIES)) { - unexpand_tildes(input, &completions); + unexpand_tildes(input, vars, &completions); } out_completions->insert(out_completions->end(), std::make_move_iterator(completions.begin()), @@ -1090,14 +1101,15 @@ expand_error_t expand_string(wcstring input, std::vector *out_comp return total_result; } -bool expand_one(wcstring &string, expand_flags_t flags, parse_error_list_t *errors) { +bool expand_one(wcstring &string, expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors) { std::vector completions; if (!(flags & EXPAND_FOR_COMPLETIONS) && expand_is_clean(string)) { return true; } - if (expand_string(string, &completions, flags | EXPAND_NO_DESCRIPTIONS, errors) && + if (expand_string(string, &completions, flags | EXPAND_NO_DESCRIPTIONS, vars, errors) && completions.size() == 1) { string = std::move(completions.at(0).completion); return true; @@ -1105,8 +1117,9 @@ bool expand_one(wcstring &string, expand_flags_t flags, parse_error_list_t *erro return false; } -expand_error_t expand_to_command_and_args(const wcstring &instr, wcstring *out_cmd, - wcstring_list_t *out_args, parse_error_list_t *errors) { +expand_error_t expand_to_command_and_args(const wcstring &instr, const environment_t &vars, + wcstring *out_cmd, wcstring_list_t *out_args, + parse_error_list_t *errors) { // Fast path. if (expand_is_clean(instr)) { *out_cmd = instr; @@ -1114,9 +1127,9 @@ expand_error_t expand_to_command_and_args(const wcstring &instr, wcstring *out_c } std::vector completions; - expand_error_t expand_err = - expand_string(instr, &completions, - EXPAND_SKIP_CMDSUBST | EXPAND_NO_DESCRIPTIONS | EXPAND_SKIP_JOBS, errors); + expand_error_t expand_err = expand_string( + instr, &completions, EXPAND_SKIP_CMDSUBST | EXPAND_NO_DESCRIPTIONS | EXPAND_SKIP_JOBS, vars, + errors); if (expand_err == EXPAND_OK || expand_err == EXPAND_WILDCARD_MATCH) { // The first completion is the command, any remaning are arguments. bool first = true; @@ -1180,34 +1193,31 @@ bool fish_xdm_login_hack_hack_hack_hack(std::vector *cmds, int argc return result; } -static owning_lock> s_abbreviations; -void update_abbr_cache(const wchar_t *op, const wcstring &varname) { - wcstring abbr; - if (!unescape_string(varname.substr(wcslen(L"_fish_abbr_")), &abbr, 0, STRING_STYLE_VAR)) { - debug(1, L"Abbreviation var '%ls' is not correctly encoded, ignoring it.", varname.c_str()); - return; - } - auto abbreviations = s_abbreviations.acquire(); - abbreviations->erase(abbr); - if (wcscmp(op, L"ERASE") != 0) { - const auto expansion = env_get(varname); - if (!expansion.missing_or_empty()) { - abbreviations->emplace(abbr, expansion->as_string()); - } - } -} +maybe_t expand_abbreviation(const wcstring &src, const environment_t &vars) { + if (src.empty()) return none(); -bool expand_abbreviation(const wcstring &src, wcstring *output) { - if (src.empty()) return false; - - auto abbreviations = s_abbreviations.acquire(); - auto abbr = abbreviations->find(src); - if (abbr == abbreviations->end()) return false; - if (output != NULL) output->assign(abbr->second); - return true; + wcstring unesc_src; + if (!unescape_string(src, &unesc_src, STRING_STYLE_VAR)) { + return none(); + } + wcstring var_name = L"_fish_abbr_" + unesc_src; + if (auto var_value = vars.get(var_name)) { + return var_value->as_string(); + } + return none(); } std::map get_abbreviations() { - auto abbreviations = s_abbreviations.acquire(); - return *abbreviations; + // TODO: try to make this cheaper + const auto &vars = env_stack_t::principal(); + const size_t fish_abbr_len = wcslen(L"_fish_abbr_"); + auto names = vars.get_names(0); + std::map result; + for (const wcstring &name : names) { + if (string_prefixes_string(L"_fish_abbr_", name)) { + result[name.substr(fish_abbr_len)] = vars.get(name)->as_string(); + } + } + return result; } + diff --git a/src/expand.h b/src/expand.h index f7336d405..180ed9d2f 100644 --- a/src/expand.h +++ b/src/expand.h @@ -14,9 +14,12 @@ #include #include "common.h" +#include "maybe.h" #include "parse_constants.h" +class environment_t; class env_var_t; +class environment_t; enum { /// Flag specifying that cmdsubst expansion should be skipped. @@ -111,13 +114,15 @@ enum expand_error_t { /// \param output The list to which the result will be appended. /// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination /// of EXPAND_SKIP_CMDSUBST EXPAND_SKIP_VARIABLES and EXPAND_SKIP_WILDCARDS +/// \param vars variables used during expansion. /// \param errors Resulting errors, or NULL to ignore /// /// \return One of EXPAND_OK, EXPAND_ERROR, EXPAND_WILDCARD_MATCH and EXPAND_WILDCARD_NO_MATCH. /// EXPAND_WILDCARD_NO_MATCH and EXPAND_WILDCARD_MATCH are normal exit conditions used only on /// strings containing wildcards to tell if the wildcard produced any matches. __warn_unused expand_error_t expand_string(wcstring input, std::vector *output, - expand_flags_t flags, parse_error_list_t *errors); + expand_flags_t flags, const environment_t &vars, + parse_error_list_t *errors); /// expand_one is identical to expand_string, except it will fail if in expands to more than one /// string. This is used for expanding command names. @@ -128,7 +133,8 @@ __warn_unused expand_error_t expand_string(wcstring input, std::vector expand_abbreviation(const wcstring &src, const environment_t &vars); /// \return a snapshot of all abbreviations as a map abbreviation->expansion. std::map get_abbreviations(); diff --git a/src/fish.cpp b/src/fish.cpp index c213321d9..ceb49c053 100644 --- a/src/fish.cpp +++ b/src/fish.cpp @@ -184,9 +184,9 @@ static void source_config_in_directory(const wcstring &dir) { const wcstring cmd = L"builtin source " + escaped_pathname; parser_t &parser = parser_t::principal_parser(); - parser.set_is_within_fish_initialization(true); + set_is_within_fish_initialization(true); parser.eval(cmd, io_chain_t(), TOP); - parser.set_is_within_fish_initialization(false); + set_is_within_fish_initialization(false); } /// Parse init files. exec_path is the path of fish executable as determined by argv[0]. @@ -364,12 +364,13 @@ int main(int argc, char **argv) { save_term_foreground_process_group(); } + auto &globals = env_stack_t::globals(); const struct config_paths_t paths = determine_config_directory_paths(argv[0]); env_init(&paths); // Set features early in case other initialization depends on them. // Start with the ones set in the environment, then those set on the command line (so the // command line takes precedence). - if (auto features_var = env_get(L"fish_features")) { + if (auto features_var = globals.get(L"fish_features")) { for (const wcstring &s : features_var->as_list()) { mutable_fish_features().set_from_string(s); } @@ -417,7 +418,7 @@ int main(int argc, char **argv) { for (char **ptr = argv + my_optind; *ptr; ptr++) { list.push_back(str2wcstring(*ptr)); } - env_set(L"argv", ENV_DEFAULT, list); + parser.vars().set(L"argv", ENV_DEFAULT, list); const wcstring rel_filename = str2wcstring(file); diff --git a/src/fish_indent.cpp b/src/fish_indent.cpp index 428bd417f..f985c4b19 100644 --- a/src/fish_indent.cpp +++ b/src/fish_indent.cpp @@ -529,7 +529,7 @@ int main(int argc, char *argv[]) { std::vector colors; if (output_type != output_type_plain_text) { highlight_shell_no_io(output_wtext, colors, output_wtext.size(), NULL, - env_vars_snapshot_t::current()); + env_stack_t::globals()); } std::string colored_output; diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 4270fd699..cd030583b 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -174,7 +174,7 @@ static bool pushd(const char *path) { return false; } - env_set_pwd_from_getcwd(); + env_stack_t::principal().set_pwd_from_getcwd(); return true; } @@ -184,7 +184,7 @@ static void popd() { err(L"chdir(\"%s\") from popd() failed: errno = %d", old_cwd.c_str(), errno); } pushed_dirs.pop_back(); - env_set_pwd_from_getcwd(); + env_stack_t::principal().set_pwd_from_getcwd(); } // The odd formulation of these macros is to avoid "multiple unary operator" warnings from oclint @@ -911,7 +911,7 @@ static void test_parser() { say(L"Testing eval_args"); completion_list_t comps; - parser_t::expand_argument_list(L"alpha 'beta gamma' delta", 0, &comps); + parser_t::expand_argument_list(L"alpha 'beta gamma' delta", 0, null_environment_t{}, &comps); do_test(comps.size() == 3); do_test(comps.at(0).completion == L"alpha"); do_test(comps.at(1).completion == L"beta gamma"); @@ -1500,6 +1500,25 @@ static void test_lru() { do_test(cache.evicted.size() == size_t(total_nodes)); } +/// A crappy environment_t that only knows about PWD. +struct pwd_environment_t : public environment_t { + std::map extras; + + virtual maybe_t get(const wcstring &key, + env_mode_flags_t mode = ENV_DEFAULT) const override { + if (key == L"PWD") { + return env_var_t{wgetcwd(), 0}; + } + auto extra = extras.find(key); + if (extra != extras.end()) { + return env_var_t(extra->second, ENV_DEFAULT); + } + return {}; + } + + wcstring_list_t get_names(int flags) const override { return {L"PWD"}; } +}; + /// Perform parameter expansion and test if the output equals the zero-terminated parameter list /// supplied. /// @@ -1515,7 +1534,7 @@ static bool expand_test(const wchar_t *in, expand_flags_t flags, ...) { wchar_t *arg; parse_error_list_t errors; - if (expand_string(in, &output, flags, &errors) == EXPAND_ERROR) { + if (expand_string(in, &output, flags, pwd_environment_t{}, &errors) == EXPAND_ERROR) { if (errors.empty()) { err(L"Bug: Parse error reported but no error text found."); } else { @@ -1755,7 +1774,8 @@ static void test_ifind_fuzzy() { static void test_abbreviations() { say(L"Testing abbreviations"); - env_push(true); + auto &vars = parser_t::principal_parser().vars(); + vars.push(true); const std::vector> abbreviations = { {L"gc", L"git checkout"}, @@ -1763,66 +1783,69 @@ static void test_abbreviations() { {L"gx", L"git checkout"}, }; for (const auto &kv : abbreviations) { - int ret = env_set_one(L"_fish_abbr_" + kv.first, ENV_LOCAL, kv.second); + int ret = vars.set_one(L"_fish_abbr_" + kv.first, ENV_LOCAL, kv.second); if (ret != 0) err(L"Unable to set abbreviation variable"); } - wcstring result; - if (expand_abbreviation(L"", &result)) err(L"Unexpected success with empty abbreviation"); - if (expand_abbreviation(L"nothing", &result)) - err(L"Unexpected success with missing abbreviation"); + if (expand_abbreviation(L"", vars)) err(L"Unexpected success with empty abbreviation"); + if (expand_abbreviation(L"nothing", vars)) err(L"Unexpected success with missing abbreviation"); - if (!expand_abbreviation(L"gc", &result)) err(L"Unexpected failure with gc abbreviation"); - if (result != L"git checkout") err(L"Wrong abbreviation result for gc"); - result.clear(); + auto mresult = expand_abbreviation(L"gc", vars); + if (!mresult) err(L"Unexpected failure with gc abbreviation"); + if (*mresult != L"git checkout") err(L"Wrong abbreviation result for gc"); - if (!expand_abbreviation(L"foo", &result)) err(L"Unexpected failure with foo abbreviation"); - if (result != L"bar") err(L"Wrong abbreviation result for foo"); + mresult = expand_abbreviation(L"foo", vars); + if (!mresult) err(L"Unexpected failure with foo abbreviation"); + if (*mresult != L"bar") err(L"Wrong abbreviation result for foo"); bool expanded; - expanded = reader_expand_abbreviation_in_command(L"just a command", 3, &result); + wcstring result; + expanded = reader_expand_abbreviation_in_command(L"just a command", 3, vars, &result); if (expanded) err(L"Command wrongly expanded on line %ld", (long)__LINE__); - expanded = reader_expand_abbreviation_in_command(L"gc somebranch", 0, &result); + expanded = reader_expand_abbreviation_in_command(L"gc somebranch", 0, vars, &result); if (!expanded) err(L"Command not expanded on line %ld", (long)__LINE__); - expanded = reader_expand_abbreviation_in_command(L"gc somebranch", wcslen(L"gc"), &result); + expanded = + reader_expand_abbreviation_in_command(L"gc somebranch", wcslen(L"gc"), vars, &result); if (!expanded) err(L"gc not expanded"); if (result != L"git checkout somebranch") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result.c_str()); // Space separation. - expanded = reader_expand_abbreviation_in_command(L"gx somebranch", wcslen(L"gc"), &result); + expanded = + reader_expand_abbreviation_in_command(L"gx somebranch", wcslen(L"gc"), vars, &result); if (!expanded) err(L"gx not expanded"); if (result != L"git checkout somebranch") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result.c_str()); expanded = reader_expand_abbreviation_in_command(L"echo hi ; gc somebranch", - wcslen(L"echo hi ; g"), &result); + wcslen(L"echo hi ; g"), vars, &result); if (!expanded) err(L"gc not expanded on line %ld", (long)__LINE__); if (result != L"echo hi ; git checkout somebranch") err(L"gc incorrectly expanded on line %ld", (long)__LINE__); expanded = reader_expand_abbreviation_in_command( - L"echo (echo (echo (echo (gc ", wcslen(L"echo (echo (echo (echo (gc"), &result); + L"echo (echo (echo (echo (gc ", wcslen(L"echo (echo (echo (echo (gc"), vars, &result); if (!expanded) err(L"gc not expanded on line %ld", (long)__LINE__); if (result != L"echo (echo (echo (echo (git checkout ") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result.c_str()); // If commands should be expanded. - expanded = reader_expand_abbreviation_in_command(L"if gc", wcslen(L"if gc"), &result); + expanded = reader_expand_abbreviation_in_command(L"if gc", wcslen(L"if gc"), vars, &result); if (!expanded) err(L"gc not expanded on line %ld", (long)__LINE__); if (result != L"if git checkout") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result.c_str()); // Others should not be. - expanded = reader_expand_abbreviation_in_command(L"of gc", wcslen(L"of gc"), &result); + expanded = reader_expand_abbreviation_in_command(L"of gc", wcslen(L"of gc"), vars, &result); if (expanded) err(L"gc incorrectly expanded on line %ld", (long)__LINE__); // Others should not be. - expanded = reader_expand_abbreviation_in_command(L"command gc", wcslen(L"command gc"), &result); + expanded = + reader_expand_abbreviation_in_command(L"command gc", wcslen(L"command gc"), vars, &result); if (expanded) err(L"gc incorrectly expanded on line %ld", (long)__LINE__); - env_pop(); + vars.pop(); } /// Test path functions. @@ -2119,30 +2142,32 @@ static void test_is_potential_path() { const wcstring wd = L"test/is_potential_path_test/"; const wcstring_list_t wds({L".", wd}); - do_test(is_potential_path(L"al", wds, PATH_REQUIRE_DIR)); - do_test(is_potential_path(L"alpha/", wds, PATH_REQUIRE_DIR)); - do_test(is_potential_path(L"aard", wds, 0)); + const auto &vars = env_stack_t::principal(); + do_test(is_potential_path(L"al", wds, vars, PATH_REQUIRE_DIR)); + do_test(is_potential_path(L"alpha/", wds, vars, PATH_REQUIRE_DIR)); + do_test(is_potential_path(L"aard", wds, vars, 0)); - do_test(!is_potential_path(L"balpha/", wds, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"aard", wds, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"aarde", wds, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"aarde", wds, 0)); + do_test(!is_potential_path(L"balpha/", wds, vars, PATH_REQUIRE_DIR)); + do_test(!is_potential_path(L"aard", wds, vars, PATH_REQUIRE_DIR)); + do_test(!is_potential_path(L"aarde", wds, vars, PATH_REQUIRE_DIR)); + do_test(!is_potential_path(L"aarde", wds, vars, 0)); - do_test(is_potential_path(L"test/is_potential_path_test/aardvark", wds, 0)); - do_test(is_potential_path(L"test/is_potential_path_test/al", wds, PATH_REQUIRE_DIR)); - do_test(is_potential_path(L"test/is_potential_path_test/aardv", wds, 0)); + do_test(is_potential_path(L"test/is_potential_path_test/aardvark", wds, vars, 0)); + do_test(is_potential_path(L"test/is_potential_path_test/al", wds, vars, PATH_REQUIRE_DIR)); + do_test(is_potential_path(L"test/is_potential_path_test/aardv", wds, vars, 0)); - do_test(!is_potential_path(L"test/is_potential_path_test/aardvark", wds, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"test/is_potential_path_test/al/", wds, 0)); - do_test(!is_potential_path(L"test/is_potential_path_test/ar", wds, 0)); + do_test( + !is_potential_path(L"test/is_potential_path_test/aardvark", wds, vars, PATH_REQUIRE_DIR)); + do_test(!is_potential_path(L"test/is_potential_path_test/al/", wds, vars, 0)); + do_test(!is_potential_path(L"test/is_potential_path_test/ar", wds, vars, 0)); - do_test(is_potential_path(L"/usr", wds, PATH_REQUIRE_DIR)); + do_test(is_potential_path(L"/usr", wds, vars, PATH_REQUIRE_DIR)); } /// Test the 'test' builtin. int builtin_test(parser_t &parser, io_streams_t &streams, wchar_t **argv); static bool run_one_test_test(int expected, wcstring_list_t &lst, bool bracket) { - parser_t parser; + parser_t &parser = parser_t::principal_parser(); size_t i, count = lst.size(); wchar_t **argv = new wchar_t *[count + 3]; argv[0] = (wchar_t *)(bracket ? L"[" : L"test"); @@ -2172,7 +2197,7 @@ static bool run_test_test(int expected, const wcstring &str) { // We need to tokenize the string in the same manner a normal shell would do. This is because we // need to test things like quoted strings that have leading and trailing whitespace. - parser_t::expand_argument_list(str, 0, &comps); + parser_t::expand_argument_list(str, 0, null_environment_t{}, &comps); for (completion_list_t::const_iterator it = comps.begin(), end = comps.end(); it != end; ++it) { argv.push_back(it->completion); } @@ -2185,7 +2210,7 @@ static bool run_test_test(int expected, const wcstring &str) { static void test_test_brackets() { // Ensure [ knows it needs a ]. - parser_t parser; + parser_t &parser = parser_t::principal_parser(); io_streams_t streams(0); null_terminated_array_t args; @@ -2333,13 +2358,23 @@ static void test_colors() { static void test_complete() { say(L"Testing complete"); - const wchar_t *name_strs[] = {L"Foo1", L"Foo2", L"Foo3", L"Bar1", L"Bar2", L"Bar3"}; - size_t count = sizeof name_strs / sizeof *name_strs; - const wcstring_list_t names(name_strs, name_strs + count); - std::vector completions; - complete_set_variable_names(&names); + struct test_complete_vars_t : environment_t { + wcstring_list_t get_names(int flags) const override { + return {L"Foo1", L"Foo2", L"Foo3", L"Bar1", L"Bar2", L"Bar3"}; + } - complete(L"$", &completions, COMPLETION_REQUEST_DEFAULT); + maybe_t get(const wcstring &key, + env_mode_flags_t mode = ENV_DEFAULT) const override { + if (key == L"PWD") { + return env_var_t{wgetcwd(), 0}; + } + return {}; + } + }; + test_complete_vars_t vars; + + completion_list_t completions; + complete(L"$", &completions, COMPLETION_REQUEST_DEFAULT, vars); completions_sort_and_prioritize(&completions); do_test(completions.size() == 6); do_test(completions.at(0).completion == L"Bar1"); @@ -2350,7 +2385,7 @@ static void test_complete() { do_test(completions.at(5).completion == L"Foo3"); completions.clear(); - complete(L"$F", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"$F", &completions, COMPLETION_REQUEST_DEFAULT, vars); completions_sort_and_prioritize(&completions); do_test(completions.size() == 3); do_test(completions.at(0).completion == L"oo1"); @@ -2358,12 +2393,13 @@ static void test_complete() { do_test(completions.at(2).completion == L"oo3"); completions.clear(); - complete(L"$1", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"$1", &completions, COMPLETION_REQUEST_DEFAULT, vars); completions_sort_and_prioritize(&completions); do_test(completions.empty()); completions.clear(); - complete(L"$1", &completions, COMPLETION_REQUEST_DEFAULT | COMPLETION_REQUEST_FUZZY_MATCH); + complete(L"$1", &completions, COMPLETION_REQUEST_DEFAULT | COMPLETION_REQUEST_FUZZY_MATCH, + vars); completions_sort_and_prioritize(&completions); do_test(completions.size() == 2); do_test(completions.at(0).completion == L"$Bar1"); @@ -2375,24 +2411,25 @@ static void test_complete() { if (system("chmod 700 'test/complete_test/testfile'")) err(L"chmod failed"); completions.clear(); - complete(L"echo (test/complete_test/testfil", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo (test/complete_test/testfil", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"e"); completions.clear(); - complete(L"echo (ls test/complete_test/testfil", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo (ls test/complete_test/testfil", &completions, COMPLETION_REQUEST_DEFAULT, + vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"e"); completions.clear(); complete(L"echo (command ls test/complete_test/testfil", &completions, - COMPLETION_REQUEST_DEFAULT); + COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"e"); // Completing after spaces - see #2447 completions.clear(); - complete(L"echo (ls test/complete_test/has\\ ", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo (ls test/complete_test/has\\ ", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"space"); @@ -2405,116 +2442,116 @@ static void test_complete() { // Complete a function name. completions.clear(); - complete(L"echo (scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo (scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"t"); // But not with the command prefix. completions.clear(); - complete(L"echo (command scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo (command scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 0); // Not with the builtin prefix. completions.clear(); - complete(L"echo (builtin scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo (builtin scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 0); // Not after a redirection. completions.clear(); - complete(L"echo hi > scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo hi > scuttlebut", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 0); // Trailing spaces (#1261). complete_add(L"foobarbaz", false, wcstring(), option_type_args_only, NO_FILES, NULL, L"qux", NULL, COMPLETE_AUTO_SPACE); completions.clear(); - complete(L"foobarbaz ", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"foobarbaz ", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"qux"); // Don't complete variable names in single quotes (#1023). completions.clear(); - complete(L"echo '$Foo", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo '$Foo", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.empty()); completions.clear(); - complete(L"echo \\$Foo", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo \\$Foo", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.empty()); // File completions. completions.clear(); - complete(L"cat test/complete_test/te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat test/complete_test/te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); completions.clear(); - complete(L"echo sup > test/complete_test/te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo sup > test/complete_test/te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); completions.clear(); - complete(L"echo sup > test/complete_test/te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"echo sup > test/complete_test/te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); if (!pushd("test/complete_test")) return; - complete(L"cat te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); do_test(!(completions.at(0).flags & COMPLETE_REPLACES_TOKEN)); do_test(!(completions.at(0).flags & COMPLETE_DUPLICATES_ARGUMENT)); completions.clear(); - complete(L"cat testfile te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat testfile te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); do_test(completions.at(0).flags & COMPLETE_DUPLICATES_ARGUMENT); completions.clear(); - complete(L"cat testfile TE", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat testfile TE", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"testfile"); do_test(completions.at(0).flags & COMPLETE_REPLACES_TOKEN); do_test(completions.at(0).flags & COMPLETE_DUPLICATES_ARGUMENT); completions.clear(); - complete(L"something --abc=te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"something --abc=te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); completions.clear(); - complete(L"something -abc=te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"something -abc=te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); completions.clear(); - complete(L"something abc=te", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"something abc=te", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"stfile"); completions.clear(); - complete(L"something abc=stfile", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"something abc=stfile", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.size() == 0); completions.clear(); - complete(L"something abc=stfile", &completions, COMPLETION_REQUEST_FUZZY_MATCH); + complete(L"something abc=stfile", &completions, COMPLETION_REQUEST_FUZZY_MATCH, vars); do_test(completions.size() == 1); do_test(completions.at(0).completion == L"abc=testfile"); // Zero escapes can cause problems. See issue #1631. completions.clear(); - complete(L"cat foo\\0", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat foo\\0", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.empty()); completions.clear(); - complete(L"cat foo\\0bar", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat foo\\0bar", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.empty()); completions.clear(); - complete(L"cat \\0", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat \\0", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.empty()); completions.clear(); - complete(L"cat te\\0", &completions, COMPLETION_REQUEST_DEFAULT); + complete(L"cat te\\0", &completions, COMPLETION_REQUEST_DEFAULT, vars); do_test(completions.empty()); popd(); completions.clear(); - complete_set_variable_names(NULL); // Test abbreviations. + auto &pvars = parser_t::principal_parser().vars(); function_data_t fd; fd.name = L"testabbrsonetwothreefour"; function_add(fd, parser_t::principal_parser()); - int ret = env_set_one(L"_fish_abbr_testabbrsonetwothreezero", ENV_LOCAL, L"expansion"); - complete(L"testabbrsonetwothree", &completions, COMPLETION_REQUEST_DEFAULT); + int ret = pvars.set_one(L"_fish_abbr_testabbrsonetwothreezero", ENV_LOCAL, L"expansion"); + complete(L"testabbrsonetwothree", &completions, COMPLETION_REQUEST_DEFAULT, pvars); do_test(ret == 0); do_test(completions.size() == 2); do_test(completions.at(0).completion == L"four"); @@ -2592,9 +2629,9 @@ static void test_completion_insertions() { } static void perform_one_autosuggestion_cd_test(const wcstring &command, const wcstring &expected, - long line) { + const environment_t &vars, long line) { std::vector comps; - complete(command, &comps, COMPLETION_REQUEST_AUTOSUGGESTION); + complete(command, &comps, COMPLETION_REQUEST_AUTOSUGGESTION, vars); bool expects_error = (expected == L""); @@ -2628,9 +2665,9 @@ static void perform_one_autosuggestion_cd_test(const wcstring &command, const wc } static void perform_one_completion_cd_test(const wcstring &command, const wcstring &expected, - long line) { + const environment_t &vars, long line) { std::vector comps; - complete(command, &comps, COMPLETION_REQUEST_DEFAULT); + complete(command, &comps, COMPLETION_REQUEST_DEFAULT, vars); bool expects_error = (expected == L""); @@ -2679,7 +2716,7 @@ static void test_autosuggest_suggest_special() { // This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia` // test below. // Fake out the home directory - env_set_one(L"HOME", ENV_LOCAL | ENV_EXPORT, L"test/test-home"); + parser_t::principal_parser().vars().set_one(L"HOME", ENV_LOCAL | ENV_EXPORT, L"test/test-home"); if (system("mkdir -p test/test-home/test_autosuggest_suggest_special/")) { err(L"mkdir failed"); } @@ -2695,70 +2732,82 @@ static void test_autosuggest_suggest_special() { const wcstring wd = L"test/autosuggest_test"; - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/1", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/1", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/1", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/2", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/2", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/2", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/3", L"foo\\bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/3", L"foo\\bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/3", L"foo\\bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/4", L"foo'bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/4", L"foo'bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/4", L"foo'bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/5", L"foo\"bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/5", L"foo\"bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/5", L"foo\"bar/", __LINE__); + pwd_environment_t vars{}; + vars.extras[L"HOME"] = parser_t::principal_parser().vars().get(L"HOME")->as_string(); - env_set_one(L"AUTOSUGGEST_TEST_LOC", ENV_LOCAL, wd); - perform_one_autosuggestion_cd_test(L"cd $AUTOSUGGEST_TEST_LOC/0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd ~/test_autosuggest_suggest_specia", L"l/", __LINE__); - - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/start/", L"unique2/unique3/", + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/1", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/1", L"foo bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/1", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/2", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/2", L"foo bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/2", L"foo bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/3", L"foo\\bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/3", L"foo\\bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/3", L"foo\\bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/4", L"foo'bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/4", L"foo'bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/4", L"foo'bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/5", L"foo\"bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/5", L"foo\"bar/", vars, + __LINE__); + perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/5", L"foo\"bar/", vars, __LINE__); + vars.extras[L"AUTOSUGGEST_TEST_LOC"] = wd; + perform_one_autosuggestion_cd_test(L"cd $AUTOSUGGEST_TEST_LOC/0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd ~/test_autosuggest_suggest_specia", L"l/", vars, + __LINE__); + + perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/start/", L"unique2/unique3/", + vars, __LINE__); + if (!pushd(wcs2string(wd).c_str())) return; - perform_one_autosuggestion_cd_test(L"cd 0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd '0", L"foobar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 1", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"1", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd '1", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 2", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"2", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd '2", L"foo bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 3", L"foo\\bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"3", L"foo\\bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd '3", L"foo\\bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 4", L"foo'bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"4", L"foo'bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd '4", L"foo'bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd 5", L"foo\"bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"5", L"foo\"bar/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd '5", L"foo\"bar/", __LINE__); + perform_one_autosuggestion_cd_test(L"cd 0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd '0", L"foobar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd 1", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"1", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd '1", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd 2", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"2", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd '2", L"foo bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd 3", L"foo\\bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"3", L"foo\\bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd '3", L"foo\\bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd 4", L"foo'bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"4", L"foo'bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd '4", L"foo'bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd 5", L"foo\"bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd \"5", L"foo\"bar/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd '5", L"foo\"bar/", vars, __LINE__); // A single quote should defeat tilde expansion. - perform_one_autosuggestion_cd_test(L"cd '~/test_autosuggest_suggest_specia'", L"", + perform_one_autosuggestion_cd_test(L"cd '~/test_autosuggest_suggest_specia'", L"", vars, __LINE__); // Don't crash on ~ (issue #2696). Note this is cwd dependent. if (system("mkdir -p '~hahaha/path1/path2/'")) err(L"mkdir failed"); - perform_one_autosuggestion_cd_test(L"cd ~haha", L"ha/path1/path2/", __LINE__); - perform_one_autosuggestion_cd_test(L"cd ~hahaha/", L"path1/path2/", __LINE__); - perform_one_completion_cd_test(L"cd ~haha", L"ha/", __LINE__); - perform_one_completion_cd_test(L"cd ~hahaha/", L"path1/", __LINE__); + perform_one_autosuggestion_cd_test(L"cd ~haha", L"ha/path1/path2/", vars, __LINE__); + perform_one_autosuggestion_cd_test(L"cd ~hahaha/", L"path1/path2/", vars, __LINE__); + perform_one_completion_cd_test(L"cd ~haha", L"ha/", vars, __LINE__); + perform_one_completion_cd_test(L"cd ~hahaha/", L"path1/", vars, __LINE__); - env_remove(L"HOME", ENV_LOCAL | ENV_EXPORT); + parser_t::principal_parser().vars().remove(L"HOME", ENV_LOCAL | ENV_EXPORT); popd(); } static void perform_one_autosuggestion_should_ignore_test(const wcstring &command, long line) { completion_list_t comps; - complete(command, &comps, COMPLETION_REQUEST_AUTOSUGGESTION); + complete(command, &comps, COMPLETION_REQUEST_AUTOSUGGESTION, env_vars_snapshot_t{}); do_test(comps.empty()); if (!comps.empty()) { const wcstring &suggestion = comps.front().completion; @@ -4274,9 +4323,10 @@ static void test_highlighting() { {L"self%not", highlight_spec_param}, }); + auto &vars = parser_t::principal_parser().vars(); // Verify variables and wildcards in commands using /bin/cat. - env_set(L"VARIABLE_IN_COMMAND", ENV_LOCAL, {L"a"}); - env_set(L"VARIABLE_IN_COMMAND2", ENV_LOCAL, {L"at"}); + vars.set(L"VARIABLE_IN_COMMAND", ENV_LOCAL, {L"a"}); + vars.set(L"VARIABLE_IN_COMMAND2", ENV_LOCAL, {L"at"}); highlight_tests.push_back( {{L"/bin/ca", highlight_spec_command, ns}, {L"*", highlight_spec_operator, ns}}); @@ -4309,7 +4359,7 @@ static void test_highlighting() { do_test(expected_colors.size() == text.size()); std::vector colors(text.size()); - highlight_shell(text, colors, 20, NULL, env_vars_snapshot_t::current()); + highlight_shell(text, colors, 20, NULL, vars); if (expected_colors.size() != colors.size()) { err(L"Color vector has wrong size! Expected %lu, actual %lu", expected_colors.size(), @@ -4328,8 +4378,8 @@ static void test_highlighting() { } } } - env_remove(L"VARIABLE_IN_COMMAND", ENV_DEFAULT); - env_remove(L"VARIABLE_IN_COMMAND2", ENV_DEFAULT); + vars.remove(L"VARIABLE_IN_COMMAND", ENV_DEFAULT); + vars.remove(L"VARIABLE_IN_COMMAND2", ENV_DEFAULT); } static void test_wcstring_tok() { @@ -4389,7 +4439,7 @@ static void test_pcre2_escape() { int builtin_string(parser_t &parser, io_streams_t &streams, wchar_t **argv); static void run_one_string_test(const wchar_t *const *argv, int expected_rc, const wchar_t *expected_out) { - parser_t parser; + parser_t &parser = parser_t::principal_parser(); io_streams_t streams(0); streams.stdin_is_directly_redirected = false; // read from argv instead of stdin int rc = builtin_string(parser, streams, const_cast(argv)); @@ -4728,14 +4778,15 @@ static void test_string() { /// Helper for test_timezone_env_vars(). long return_timezone_hour(time_t tstamp, const wchar_t *timezone) { + auto &vars = parser_t::principal_parser().vars(); struct tm ltime; char ltime_str[3]; char *str_ptr; size_t n; - env_set_one(L"TZ", ENV_EXPORT, timezone); + vars.set_one(L"TZ", ENV_EXPORT, timezone); - const auto var = env_get(L"TZ", ENV_DEFAULT); + const auto var = vars.get(L"TZ", ENV_DEFAULT); (void)var; localtime_r(&tstamp, <ime); diff --git a/src/function.cpp b/src/function.cpp index 30593812a..d2ef6e9c7 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -25,29 +25,31 @@ #include "fallback.h" // IWYU pragma: keep #include "function.h" #include "intern.h" +#include "parser.h" #include "parser_keywords.h" #include "reader.h" #include "wutil.h" // IWYU pragma: keep class function_info_t { - public: - /// Immutable properties of the function. - std::shared_ptr props; - /// Function description. This may be changed after the function is created. - wcstring description; - /// File where this function was defined (intern'd string). - const wchar_t *const definition_file; - /// Mapping of all variables that were inherited from the function definition scope to their - /// values. - const std::map inherit_vars; - /// Flag for specifying that this function was automatically loaded. - const bool is_autoload; +public: + /// Immutable properties of the function. + std::shared_ptr props; + /// Function description. This may be changed after the function is created. + wcstring description; + /// File where this function was defined (intern'd string). + const wchar_t *const definition_file; + /// Mapping of all variables that were inherited from the function definition scope to their + /// values. + const std::map inherit_vars; + /// Flag for specifying that this function was automatically loaded. + const bool is_autoload; - /// Constructs relevant information from the function_data. - function_info_t(function_data_t data, const wchar_t *filename, bool autoload); + /// Constructs relevant information from the function_data. + function_info_t(function_data_t data, const environment_t &vars, const wchar_t *filename, + bool autoload); - /// Used by function_copy. - function_info_t(const function_info_t &data, const wchar_t *filename, bool autoload); + /// Used by function_copy. + function_info_t(const function_info_t &data, const wchar_t *filename, bool autoload); }; /// Table containing all functions. @@ -101,7 +103,9 @@ static int load(const wcstring &name) { static void autoload_names(std::unordered_set &names, int get_hidden) { size_t i; - const auto path_var = env_get(L"fish_function_path"); + // TODO: justfy this. + auto &vars = env_stack_t::principal(); + const auto path_var = vars.get(L"fish_function_path"); if (path_var.missing_or_empty()) return; wcstring_list_t path_list; @@ -127,20 +131,22 @@ static void autoload_names(std::unordered_set &names, int get_hidden) } } -static std::map snapshot_vars(const wcstring_list_t &vars) { +static std::map snapshot_vars(const wcstring_list_t &vars, + const environment_t &src) { std::map result; for (const wcstring &name : vars) { - auto var = env_get(name); + auto var = src.get(name); if (var) result[name] = std::move(*var); } return result; } -function_info_t::function_info_t(function_data_t data, const wchar_t *filename, bool autoload) +function_info_t::function_info_t(function_data_t data, const environment_t &vars, + const wchar_t *filename, bool autoload) : props(std::make_shared(std::move(data.props))), description(std::move(data.description)), definition_file(intern(filename)), - inherit_vars(snapshot_vars(data.inherit_vars)), + inherit_vars(snapshot_vars(data.inherit_vars, vars)), is_autoload(autoload) {} function_info_t::function_info_t(const function_info_t &data, const wchar_t *filename, @@ -164,8 +170,8 @@ void function_add(const function_data_t &data, const parser_t &parser) { // Create and store a new function. const wchar_t *filename = reader_current_filename(); - const function_map_t::value_type new_pair(data.name, - function_info_t(data, filename, is_autoload)); + const function_map_t::value_type new_pair( + data.name, function_info_t(data, parser.vars(), filename, is_autoload)); loaded_functions.insert(new_pair); // Add event handlers. @@ -198,7 +204,7 @@ void function_load(const wcstring &cmd) { } } -int function_exists_no_autoload(const wcstring &cmd, const env_vars_snapshot_t &vars) { +int function_exists_no_autoload(const wcstring &cmd, const environment_t &vars) { if (parser_keywords_is_reserved(cmd)) return 0; scoped_rlock locker(functions_lock); return loaded_functions.find(cmd) != loaded_functions.end() || @@ -344,23 +350,24 @@ void function_invalidate_path() { function_autoloader.invalidate(); } // 1. argv // 2. named arguments // 3. inherited variables -void function_prepare_environment(const wcstring &name, const wchar_t *const *argv, +void function_prepare_environment(env_stack_t &vars, const wcstring &name, + const wchar_t *const *argv, const std::map &inherited_vars) { - env_set_argv(argv); + vars.set_argv(argv); auto props = function_get_properties(name); if (props && !props->named_arguments.empty()) { const wchar_t *const *arg = argv; for (const wcstring &named_arg : props->named_arguments) { if (*arg) { - env_set_one(named_arg, ENV_LOCAL | ENV_USER, *arg); + vars.set_one(named_arg, ENV_LOCAL | ENV_USER, *arg); arg++; } else { - env_set_empty(named_arg, ENV_LOCAL | ENV_USER); + vars.set_empty(named_arg, ENV_LOCAL | ENV_USER); } } } for (const auto &kv : inherited_vars) { - env_set(kv.first, ENV_LOCAL | ENV_USER, kv.second.as_list()); + vars.set(kv.first, ENV_LOCAL | ENV_USER, kv.second.as_list()); } } diff --git a/src/function.h b/src/function.h index 7520e19c7..9ee7df6eb 100644 --- a/src/function.h +++ b/src/function.h @@ -77,7 +77,7 @@ int function_exists(const wcstring &name); void function_load(const wcstring &name); /// Returns true if the function with the name name exists, without triggering autoload. -int function_exists_no_autoload(const wcstring &name, const env_vars_snapshot_t &vars); +int function_exists_no_autoload(const wcstring &name, const environment_t &vars); /// Returns all function names. /// @@ -109,7 +109,8 @@ std::map function_get_inherit_vars(const wcstring &name); bool function_copy(const wcstring &name, const wcstring &new_name); /// Prepares the environment for executing a function. -void function_prepare_environment(const wcstring &name, const wchar_t *const *argv, +void function_prepare_environment(env_stack_t &vars, const wcstring &name, + const wchar_t *const *argv, const std::map &inherited_vars); /// Observes that fish_function_path has changed. diff --git a/src/highlight.cpp b/src/highlight.cpp index a6612b68f..cda55083b 100644 --- a/src/highlight.cpp +++ b/src/highlight.cpp @@ -105,7 +105,7 @@ bool fs_is_case_insensitive(const wcstring &path, int fd, /// /// We expect the path to already be unescaped. bool is_potential_path(const wcstring &potential_path_fragment, const wcstring_list_t &directories, - path_flags_t flags) { + const environment_t &vars, path_flags_t flags) { ASSERT_IS_BACKGROUND_THREAD(); const bool require_dir = static_cast(flags & PATH_REQUIRE_DIR); @@ -114,7 +114,7 @@ bool is_potential_path(const wcstring &potential_path_fragment, const wcstring_l bool result = false; wcstring path_with_magic(potential_path_fragment); - if (flags & PATH_EXPAND_TILDE) expand_tilde(path_with_magic); + if (flags & PATH_EXPAND_TILDE) expand_tilde(path_with_magic, vars); // debug( 1, L"%ls -> %ls ->%ls", path, tilde, unescaped ); @@ -216,7 +216,7 @@ bool is_potential_path(const wcstring &potential_path_fragment, const wcstring_l // Given a string, return whether it prefixes a path that we could cd into. Return that path in // out_path. Expects path to be unescaped. static bool is_potential_cd_path(const wcstring &path, const wcstring &working_directory, - path_flags_t flags) { + const environment_t &vars, path_flags_t flags) { wcstring_list_t directories; if (string_prefixes_string(L"./", path)) { @@ -224,7 +224,7 @@ static bool is_potential_cd_path(const wcstring &path, const wcstring &working_d directories.push_back(working_directory); } else { // Get the CDPATH. - auto cdpath = env_get(L"CDPATH"); + auto cdpath = vars.get(L"CDPATH"); std::vector pathsv = cdpath.missing_or_empty() ? wcstring_list_t{L"."} : cdpath->as_list(); @@ -236,22 +236,24 @@ static bool is_potential_cd_path(const wcstring &path, const wcstring &working_d } // Call is_potential_path with all of these directories. - return is_potential_path(path, directories, flags | PATH_REQUIRE_DIR); + return is_potential_path(path, directories, vars, flags | PATH_REQUIRE_DIR); } // Given a plain statement node in a parse tree, get the command and return it, expanded // appropriately for commands. If we succeed, return true. static bool plain_statement_get_expanded_command(const wcstring &src, tnode_t stmt, - wcstring *out_cmd) { + const environment_t &vars, wcstring *out_cmd) { // Get the command. Try expanding it. If we cannot, it's an error. maybe_t cmd = command_for_plain_statement(stmt, src); if (!cmd) return false; - expand_error_t err = expand_to_command_and_args(*cmd, out_cmd, nullptr); + expand_error_t err = expand_to_command_and_args(*cmd, vars, out_cmd, nullptr); return err == EXPAND_OK || err == EXPAND_WILDCARD_MATCH; } rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background) { + // TODO: rationalize this principal_vars. + const auto &vars = env_stack_t::principal(); rgb_color_t result = rgb_color_t::normal(); // If sloppy_background is set, then we look at the foreground color even if is_background is @@ -264,17 +266,17 @@ rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background) return rgb_color_t::normal(); } - auto var = env_get(highlight_var[idx]); + auto var = vars.get(highlight_var[idx]); // debug( 1, L"%d -> %d -> %ls", highlight, idx, val ); - if (!var) var = env_get(highlight_var[0]); + if (!var) var = vars.get(highlight_var[0]); if (var) result = parse_color(*var, treat_as_background); // Handle modifiers. if (highlight & highlight_modifier_valid_path) { - auto var2 = env_get(L"fish_color_valid_path"); + auto var2 = vars.get(L"fish_color_valid_path"); if (var2) { rgb_color_t result2 = parse_color(*var2, is_background); if (result.is_normal()) @@ -310,8 +312,8 @@ static bool has_expand_reserved(const wcstring &str) { // Parse a command line. Return by reference the last command, and the last argument to that command // (as a string), if any. This is used by autosuggestions. -static bool autosuggest_parse_command(const wcstring &buff, wcstring *out_expanded_command, - wcstring *out_last_arg) { +static bool autosuggest_parse_command(const wcstring &buff, const environment_t &vars, + wcstring *out_expanded_command, wcstring *out_last_arg) { // Parse the buffer. parse_node_tree_t parse_tree; parse_tree_from_string(buff, @@ -321,7 +323,7 @@ static bool autosuggest_parse_command(const wcstring &buff, wcstring *out_expand // Find the last statement. auto last_statement = parse_tree.find_last_node(); if (last_statement && - plain_statement_get_expanded_command(buff, last_statement, out_expanded_command)) { + plain_statement_get_expanded_command(buff, last_statement, vars, out_expanded_command)) { // Find the last argument. If we don't get one, return an invalid node. if (auto last_arg = parse_tree.find_last_node(last_statement)) { *out_last_arg = last_arg.get_source(buff); @@ -333,7 +335,7 @@ static bool autosuggest_parse_command(const wcstring &buff, wcstring *out_expand bool autosuggest_validate_from_history(const history_item_t &item, const wcstring &working_directory, - const env_vars_snapshot_t &vars) { + const environment_t &vars) { ASSERT_IS_BACKGROUND_THREAD(); bool handled = false, suggestionOK = false; @@ -341,18 +343,17 @@ bool autosuggest_validate_from_history(const history_item_t &item, // Parse the string. wcstring parsed_command; wcstring cd_dir; - if (!autosuggest_parse_command(item.str(), &parsed_command, &cd_dir)) return false; + if (!autosuggest_parse_command(item.str(), vars, &parsed_command, &cd_dir)) return false; if (parsed_command == L"cd" && !cd_dir.empty()) { // We can possibly handle this specially. - if (expand_one(cd_dir, EXPAND_SKIP_CMDSUBST)) { + if (expand_one(cd_dir, EXPAND_SKIP_CMDSUBST, vars)) { handled = true; bool is_help = string_prefixes_string(cd_dir, L"--help") || string_prefixes_string(cd_dir, L"-h"); if (!is_help) { - wcstring path; - bool can_cd = path_get_cdpath(cd_dir, &path, working_directory, vars); - if (can_cd && !paths_are_same_file(working_directory, path)) { + auto path = path_get_cdpath(cd_dir, working_directory, vars); + if (path && !paths_are_same_file(working_directory, *path)) { suggestionOK = true; } } @@ -365,7 +366,7 @@ bool autosuggest_validate_from_history(const history_item_t &item, // Not handled specially so handle it here. bool cmd_ok = false; - if (path_get_path(parsed_command, NULL)) { + if (path_get_path(parsed_command, NULL, vars)) { cmd_ok = true; } else if (builtin_exists(parsed_command) || function_exists_no_autoload(parsed_command, vars)) { @@ -667,7 +668,7 @@ class highlighter_t { // Cursor position. const size_t cursor_pos; // Environment variables. Again, a reference member variable! - const env_vars_snapshot_t &vars; + const environment_t &vars; // Whether it's OK to do I/O. const bool io_ok; // Working directory. @@ -698,7 +699,7 @@ class highlighter_t { public: // Constructor - highlighter_t(const wcstring &str, size_t pos, const env_vars_snapshot_t &ev, wcstring wd, + highlighter_t(const wcstring &str, size_t pos, const environment_t &ev, wcstring wd, bool can_do_io) : buff(str), cursor_pos(pos), @@ -801,7 +802,7 @@ void highlighter_t::color_argument(tnode_t node) { /// Indicates whether the source range of the given node forms a valid path in the given /// working_directory. static bool node_is_potential_path(const wcstring &src, const parse_node_t &node, - const wcstring &working_directory) { + const environment_t &vars, const wcstring &working_directory) { if (!node.has_source()) return false; // Get the node source, unescape it, and then pass it to is_potential_path along with the @@ -814,7 +815,7 @@ static bool node_is_potential_path(const wcstring &src, const parse_node_t &node if (!token.empty() && token.at(0) == HOME_DIRECTORY) token.at(0) = L'~'; const wcstring_list_t working_directory_list(1, working_directory); - result = is_potential_path(token, working_directory_list, PATH_EXPAND_TILDE); + result = is_potential_path(token, working_directory_list, vars, PATH_EXPAND_TILDE); } return result; } @@ -823,7 +824,7 @@ bool highlighter_t::is_cd(tnode_t stmt) const { bool cmd_is_cd = false; if (this->io_ok && stmt.has_source()) { wcstring cmd_str; - if (plain_statement_get_expanded_command(this->buff, stmt, &cmd_str)) { + if (plain_statement_get_expanded_command(this->buff, stmt, vars, &cmd_str)) { cmd_is_cd = (cmd_str == L"cd"); } } @@ -840,11 +841,11 @@ void highlighter_t::color_arguments(const std::vector> &arg if (cmd_is_cd) { // Mark this as an error if it's not 'help' and not a valid cd path. wcstring param = arg.get_source(this->buff); - if (expand_one(param, EXPAND_SKIP_CMDSUBST)) { + if (expand_one(param, EXPAND_SKIP_CMDSUBST, vars)) { bool is_help = string_prefixes_string(param, L"--help") || string_prefixes_string(param, L"-h"); if (!is_help && this->io_ok && - !is_potential_cd_path(param, working_directory, PATH_EXPAND_TILDE)) { + !is_potential_cd_path(param, working_directory, vars, PATH_EXPAND_TILDE)) { this->color_node(arg, highlight_spec_error); } } @@ -883,7 +884,7 @@ void highlighter_t::color_redirection(tnode_t redirection_node) // I/O is disallowed, so we don't have much hope of catching anything but gross // errors. Assume it's valid. target_is_valid = true; - } else if (!expand_one(target, EXPAND_SKIP_CMDSUBST)) { + } else if (!expand_one(target, EXPAND_SKIP_CMDSUBST, vars)) { // Could not be expanded. target_is_valid = false; } else { @@ -985,7 +986,7 @@ void highlighter_t::color_children(const parse_node_t &parent, parse_token_type_ /// Determine if a command is valid. static bool command_is_valid(const wcstring &cmd, enum parse_statement_decoration_t decoration, - const wcstring &working_directory, const env_vars_snapshot_t &vars) { + const wcstring &working_directory, const environment_t &vars) { // Determine which types we check, based on the decoration. bool builtin_ok = true, function_ok = true, abbreviation_ok = true, command_ok = true, implicit_cd_ok = true; @@ -1014,14 +1015,14 @@ static bool command_is_valid(const wcstring &cmd, enum parse_statement_decoratio if (!is_valid && function_ok) is_valid = function_exists_no_autoload(cmd, vars); // Abbreviations - if (!is_valid && abbreviation_ok) is_valid = expand_abbreviation(cmd, NULL); + if (!is_valid && abbreviation_ok) is_valid = expand_abbreviation(cmd, vars).has_value(); // Regular commands if (!is_valid && command_ok) is_valid = path_get_path(cmd, NULL, vars); // Implicit cd if (!is_valid && implicit_cd_ok) { - is_valid = path_can_be_implicit_cd(cmd, working_directory, NULL, vars); + is_valid = path_as_implicit_cd(cmd, working_directory, vars).has_value(); } // Return what we got. @@ -1117,7 +1118,8 @@ const highlighter_t::color_array_t &highlighter_t::highlight() { wcstring expanded_cmd; // Check to see if the command is valid. // Try expanding it. If we cannot, it's an error. - bool expanded = plain_statement_get_expanded_command(buff, stmt, &expanded_cmd); + bool expanded = + plain_statement_get_expanded_command(buff, stmt, vars, &expanded_cmd); if (expanded && !has_expand_reserved(expanded_cmd)) { is_valid_cmd = command_is_valid(expanded_cmd, decoration, working_directory, vars); @@ -1180,7 +1182,7 @@ const highlighter_t::color_array_t &highlighter_t::highlight() { // (and the cursor is just beyond the last token), we may still underline it. if (this->cursor_pos >= node.source_start && this->cursor_pos - node.source_start <= node.source_length && - node_is_potential_path(buff, node, working_directory)) { + node_is_potential_path(buff, node, vars, working_directory)) { // It is, underline it. for (size_t i = node.source_start; i < node.source_start + node.source_length; i++) { // Don't color highlight_spec_error because it looks dorky. For example, @@ -1196,11 +1198,11 @@ const highlighter_t::color_array_t &highlighter_t::highlight() { } void highlight_shell(const wcstring &buff, std::vector &color, size_t pos, - wcstring_list_t *error, const env_vars_snapshot_t &vars) { + wcstring_list_t *error, const environment_t &vars) { UNUSED(error); // Do something sucky and get the current working directory on this background thread. This // should really be passed in. - const wcstring working_directory = env_get_pwd_slash(); + const wcstring working_directory = vars.get_pwd_slash(); // Highlight it! highlighter_t highlighter(buff, pos, vars, working_directory, true /* can do IO */); @@ -1208,11 +1210,11 @@ void highlight_shell(const wcstring &buff, std::vector &color, } void highlight_shell_no_io(const wcstring &buff, std::vector &color, size_t pos, - wcstring_list_t *error, const env_vars_snapshot_t &vars) { + wcstring_list_t *error, const environment_t &vars) { UNUSED(error); // Do something sucky and get the current working directory on this background thread. This // should really be passed in. - const wcstring working_directory = env_get_pwd_slash(); + const wcstring working_directory = vars.get_pwd_slash(); // Highlight it! highlighter_t highlighter(buff, pos, vars, working_directory, false /* no IO allowed */); @@ -1306,7 +1308,7 @@ static void highlight_universal_internal(const wcstring &buffstr, } void highlight_universal(const wcstring &buff, std::vector &color, size_t pos, - wcstring_list_t *error, const env_vars_snapshot_t &vars) { + wcstring_list_t *error, const environment_t &vars) { UNUSED(error); UNUSED(vars); assert(buff.size() == color.size()); diff --git a/src/highlight.h b/src/highlight.h index e4e255f27..a7aa66bcb 100644 --- a/src/highlight.h +++ b/src/highlight.h @@ -75,12 +75,12 @@ class history_item_t; /// \param error a list in which a description of each error will be inserted. May be 0, in whcich /// case no error descriptions will be generated. void highlight_shell(const wcstring &buffstr, std::vector &color, size_t pos, - wcstring_list_t *error, const env_vars_snapshot_t &vars); + wcstring_list_t *error, const environment_t &vars); /// Perform a non-blocking shell highlighting. The function will not do any I/O that may block. As a /// result, invalid commands may not be detected, etc. void highlight_shell_no_io(const wcstring &buffstr, std::vector &color, - size_t pos, wcstring_list_t *error, const env_vars_snapshot_t &vars); + size_t pos, wcstring_list_t *error, const environment_t &vars); /// Perform syntax highlighting for the text in buff. Matching quotes and paranthesis are /// highlighted. The result is stored in the color array as a color_code from the HIGHLIGHT_ enum @@ -93,7 +93,7 @@ void highlight_shell_no_io(const wcstring &buffstr, std::vector &color, size_t pos, - wcstring_list_t *error, const env_vars_snapshot_t &vars); + wcstring_list_t *error, const environment_t &vars); /// Translate from HIGHLIGHT_* to FISH_COLOR_* according to environment variables. Defaults to /// FISH_COLOR_NORMAL. @@ -109,7 +109,7 @@ rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background); /// reference whether the suggestion is valid or not. bool autosuggest_validate_from_history(const history_item_t &item, const wcstring &working_directory, - const env_vars_snapshot_t &vars); + const environment_t &vars); // Tests whether the specified string cpath is the prefix of anything we could cd to. directories is // a list of possible parent directories (typically either the working directory, or the cdpath). @@ -124,6 +124,6 @@ enum { }; typedef unsigned int path_flags_t; bool is_potential_path(const wcstring &const_path, const wcstring_list_t &directories, - path_flags_t flags); + const environment_t &vars, path_flags_t flags); #endif diff --git a/src/history.cpp b/src/history.cpp index 8ee7974e8..920b4c46d 100644 --- a/src/history.cpp +++ b/src/history.cpp @@ -36,6 +36,7 @@ #include "io.h" #include "iothread.h" #include "lru.h" +#include "parser.h" #include "parse_constants.h" #include "parse_util.h" #include "path.h" @@ -1693,6 +1694,8 @@ void history_t::clear() { this->clear_file_state(); } +bool history_t::is_default() const { return name == DFLT_FISH_HISTORY_SESSION_ID; } + bool history_t::is_empty() { scoped_lock locker(lock); @@ -1803,9 +1806,6 @@ static bool should_import_bash_history_line(const std::string &line) { /// commands. We can't actually parse bash syntax and the bash history file does not unambiguously /// encode multiline commands. void history_t::populate_from_bash(FILE *stream) { - // We do not import bash history if an alternative fish history file is being used. - if (history_session_id() != DFLT_FISH_HISTORY_SESSION_ID) return; - // Process the entire history file until EOF is observed. bool eof = false; while (!eof) { @@ -1866,10 +1866,10 @@ void history_collection_t::save() { void history_save_all() { histories.save(); } /// Return the prefix for the files to be used for command and read history. -wcstring history_session_id() { +wcstring history_session_id(const environment_t &vars) { wcstring result = DFLT_FISH_HISTORY_SESSION_ID; - const auto var = env_get(L"fish_history"); + const auto var = vars.get(L"fish_history"); if (var) { wcstring session_id = var->as_string(); if (session_id.empty()) { @@ -1918,7 +1918,8 @@ static bool string_could_be_path(const wcstring &potential_path) { return true; } -void history_t::add_pending_with_file_detection(const wcstring &str) { +void history_t::add_pending_with_file_detection(const wcstring &str, + const wcstring &working_dir_slash) { ASSERT_IS_MAIN_THREAD(); // Find all arguments that look like they could be file paths. @@ -1967,8 +1968,7 @@ void history_t::add_pending_with_file_detection(const wcstring &str) { // Check for which paths are valid on a background thread, // then on the main thread update our history item - const wcstring wd = env_get_pwd_slash(); - iothread_perform([=]() { return valid_paths(potential_paths, wd); }, + iothread_perform([=]() { return valid_paths(potential_paths, working_dir_slash); }, [=](path_list_t validated_paths) { this->set_valid_file_paths(validated_paths, identifier); this->enable_automatic_saving(); @@ -1992,13 +1992,15 @@ void history_t::resolve_pending() { } -static bool private_mode = false; +static std::atomic private_mode{false}; + void start_private_mode() { - private_mode = true; - env_set_one(L"fish_history", ENV_GLOBAL, L""); - env_set_one(L"fish_private_mode", ENV_GLOBAL, L"1"); + private_mode.store(true); + auto &vars = parser_t::principal_parser().vars(); + vars.set_one(L"fish_history", ENV_GLOBAL, L""); + vars.set_one(L"fish_private_mode", ENV_GLOBAL, L"1"); } bool in_private_mode() { - return private_mode; + return private_mode.load(); } diff --git a/src/history.h b/src/history.h index 6419cdcc9..033766060 100644 --- a/src/history.h +++ b/src/history.h @@ -22,6 +22,7 @@ #include "wutil.h" // IWYU pragma: keep struct io_streams_t; +class environment_t; // Fish supports multiple shells writing to history at once. Here is its strategy: // @@ -214,6 +215,9 @@ class history_t { // Returns history with the given name, creating it if necessary. static history_t &history_with_name(const wcstring &name); + /// Returns whether this is using the default name. + bool is_default() const; + // Determines whether the history is empty. Unfortunately this cannot be const, since it may // require populating the history. bool is_empty(); @@ -229,7 +233,7 @@ class history_t { // Add a new pending history item to the end, and then begin file detection on the items to // determine which arguments are paths - void add_pending_with_file_detection(const wcstring &str); + void add_pending_with_file_detection(const wcstring &str, const wcstring &working_dir_slash); // Resolves any pending history items, so that they may be returned in history searches. void resolve_pending(); @@ -357,7 +361,7 @@ class history_search_t { void history_save_all(); /// Return the prefix for the files to be used for command and read history. -wcstring history_session_id(); +wcstring history_session_id(const environment_t &vars); /// Given a list of paths and a working directory, return the paths that are valid /// This does disk I/O and may only be called in a background thread diff --git a/src/input.cpp b/src/input.cpp index 9374fb253..2fe26049d 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -164,8 +164,8 @@ static bool input_function_status; static int input_function_args_index = 0; /// Return the current bind mode. -wcstring input_get_bind_mode() { - auto mode = env_get(FISH_BIND_MODE_VAR); +wcstring input_get_bind_mode(const environment_t &vars) { + auto mode = vars.get(FISH_BIND_MODE_VAR); return mode ? mode->as_string() : DEFAULT_BIND_MODE; } @@ -173,9 +173,11 @@ wcstring input_get_bind_mode() { void input_set_bind_mode(const wcstring &bm) { // Only set this if it differs to not execute variable handlers all the time. // modes may not be empty - empty is a sentinel value meaning to not change the mode + ASSERT_IS_MAIN_THREAD(); + auto &vars = parser_t::principal_parser().vars(); assert(!bm.empty()); - if (input_get_bind_mode() != bm.c_str()) { - env_set_one(FISH_BIND_MODE_VAR, ENV_GLOBAL, bm); + if (input_get_bind_mode(vars) != bm.c_str()) { + vars.set_one(FISH_BIND_MODE_VAR, ENV_GLOBAL, bm); } } @@ -417,7 +419,8 @@ void input_queue_ch(wint_t ch) { input_common_queue_ch(ch); } static void input_mapping_execute_matching_or_generic(bool allow_commands) { const input_mapping_t *generic = NULL; - const wcstring bind_mode = input_get_bind_mode(); + const auto &vars = parser_t::principal_parser().vars(); + const wcstring bind_mode = input_get_bind_mode(vars); for (auto& m : mapping_list) { if (m.mode != bind_mode) { diff --git a/src/input.h b/src/input.h index 553a68b19..757a243ac 100644 --- a/src/input.h +++ b/src/input.h @@ -12,6 +12,8 @@ #define FISH_BIND_MODE_VAR L"fish_bind_mode" +class environment_t; + wcstring describe_char(wint_t c); /// Set to true when the input subsytem has been initialized. @@ -74,7 +76,7 @@ bool input_mapping_get(const wcstring &sequence, const wcstring &mode, wcstring_ wcstring *out_new_mode); /// Return the current bind mode. -wcstring input_get_bind_mode(); +wcstring input_get_bind_mode(const environment_t &vars); /// Set the current bind mode. void input_set_bind_mode(const wcstring &bind_mode); diff --git a/src/input_common.cpp b/src/input_common.cpp index 59e0e77d7..a9fc3bc11 100644 --- a/src/input_common.cpp +++ b/src/input_common.cpp @@ -158,8 +158,8 @@ static wint_t readb() { // Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being // set. -void update_wait_on_escape_ms() { - auto escape_time_ms = env_get(L"fish_escape_delay_ms"); +void update_wait_on_escape_ms(const environment_t &vars) { + auto escape_time_ms = vars.get(L"fish_escape_delay_ms"); if (escape_time_ms.missing_or_empty()) { wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT; return; diff --git a/src/input_common.h b/src/input_common.h index ef4550cba..c80bf6f97 100644 --- a/src/input_common.h +++ b/src/input_common.h @@ -89,7 +89,7 @@ void input_common_init(int (*ih)()); void input_common_destroy(); /// Adjust the escape timeout. -void update_wait_on_escape_ms(); +void update_wait_on_escape_ms(const environment_t &vars); /// Function used by input_readch to read bytes from stdin until enough bytes have been read to /// convert them to a wchar_t. Conversion is done using mbrtowc. If a character has previously been diff --git a/src/maybe.h b/src/maybe.h index 9e8301ed5..3281dca02 100644 --- a/src/maybe.h +++ b/src/maybe.h @@ -53,6 +53,14 @@ class maybe_t { } } + // Construct a value in-place. + template + void emplace(Args &&... args) { + reset(); + filled = true; + new (storage) T(std::forward(args)...); + } + // Access the value. T &value() { assert(filled && "maybe_t does not have a value"); diff --git a/src/output.cpp b/src/output.cpp index 4246fd084..585c5fb3f 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -554,7 +554,7 @@ void writembs_check(const char *mbs, const char *mbs_name, bool critical, const if (mbs != NULL) { tputs(mbs, 1, &writeb); } else if (critical) { - auto term = env_get(L"TERM"); + auto term = env_stack_t::globals().get(L"TERM"); const wchar_t *fmt = _(L"Tried to use terminfo string %s on line %ld of %s, which is " L"undefined in terminal of type \"%ls\". Please report this error to %s"); diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index 09b7f7d9d..f1821176c 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -121,13 +121,14 @@ tnode_t parse_execution_context_t::infinite_recursive_statem // are not infinite recursion. In particular that is what enables 'wrapper functions'. tnode_t statement = first_job.child<0>(); tnode_t continuation = first_job.child<1>(); + const null_environment_t nullenv{}; while (statement) { tnode_t plain_statement = statement.try_get_child() .try_get_child(); if (plain_statement) { maybe_t cmd = command_for_plain_statement(plain_statement, pstree->src); - if (cmd && expand_one(*cmd, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES, NULL) && + if (cmd && expand_one(*cmd, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES, nullenv) && cmd == forbidden_function_name) { // This is it. infinite_recursive_statement = plain_statement; @@ -371,7 +372,7 @@ parse_execution_result_t parse_execution_context_t::run_for_statement( // in just one. tnode_t var_name_node = header.child<1>(); wcstring for_var_name = get_source(var_name_node); - if (!expand_one(for_var_name, 0, NULL)) { + if (!expand_one(for_var_name, 0, parser->vars())) { report_error(var_name_node, FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, for_var_name.c_str()); return parse_execution_errored; } @@ -384,10 +385,11 @@ parse_execution_result_t parse_execution_context_t::run_for_statement( return ret; } - auto var = env_get(for_var_name, ENV_LOCAL); - if (!var && !is_function_context()) var = env_get(for_var_name, ENV_DEFAULT); + auto &vars = parser->vars(); + auto var = vars.get(for_var_name, ENV_LOCAL); + if (!var && !is_function_context()) var = vars.get(for_var_name, ENV_DEFAULT); if (!var || var->read_only()) { - int retval = env_set_empty(for_var_name, ENV_LOCAL | ENV_USER); + int retval = parser->vars().set_empty(for_var_name, ENV_LOCAL | ENV_USER); if (retval != ENV_OK) { report_error(var_name_node, L"You cannot use read-only variable '%ls' in a for loop", for_var_name.c_str()); @@ -404,7 +406,7 @@ parse_execution_result_t parse_execution_context_t::run_for_statement( break; } - int retval = env_set_one(for_var_name, ENV_DEFAULT | ENV_USER, val); + int retval = parser->vars().set_one(for_var_name, ENV_DEFAULT | ENV_USER, val); assert(retval == ENV_OK && "for loop variable should have been successfully set"); (void)retval; @@ -438,8 +440,8 @@ parse_execution_result_t parse_execution_context_t::run_switch_statement( // Expand it. We need to offset any errors by the position of the string. std::vector switch_values_expanded; parse_error_list_t errors; - int expand_ret = - expand_string(switch_value, &switch_values_expanded, EXPAND_NO_DESCRIPTIONS, &errors); + int expand_ret = expand_string(switch_value, &switch_values_expanded, EXPAND_NO_DESCRIPTIONS, + parser->vars(), &errors); parse_error_offset_source_start(&errors, switch_value_n.source_range()->start); switch (expand_ret) { @@ -722,7 +724,8 @@ parse_execution_result_t parse_execution_context_t::expand_command( wcstring_list_t args; // Expand the string to produce completions, and report errors. - expand_error_t expand_err = expand_to_command_and_args(unexp_cmd, out_cmd, out_args, &errors); + expand_error_t expand_err = + expand_to_command_and_args(unexp_cmd, parser->vars(), out_cmd, out_args, &errors); if (expand_err == EXPAND_ERROR) { proc_set_last_status(STATUS_ILLEGAL_CMD); return report_errors(errors); @@ -798,7 +801,7 @@ parse_execution_result_t parse_execution_context_t::populate_plain_process( wcstring path_to_external_command; if (process_type == EXTERNAL || process_type == INTERNAL_EXEC) { // Determine the actual command. This may be an implicit cd. - bool has_command = path_get_path(cmd, &path_to_external_command); + bool has_command = path_get_path(cmd, &path_to_external_command, parser->vars()); // If there was no command, then we care about the value of errno after checking for it, to // distinguish between e.g. no file vs permissions problem. @@ -811,9 +814,9 @@ parse_execution_result_t parse_execution_context_t::populate_plain_process( if (args_from_cmd_expansion.empty() && !args.try_get_child() && !args.try_get_child()) { // Ok, no arguments or redirections; check to see if the command is a directory. - wcstring implicit_cd_path; use_implicit_cd = - path_can_be_implicit_cd(cmd, env_get_pwd_slash(), &implicit_cd_path); + path_as_implicit_cd(cmd, parser->vars().get_pwd_slash(), parser->vars()) + .has_value(); } } @@ -883,7 +886,8 @@ parse_execution_result_t parse_execution_context_t::expand_arguments_from_nodes( // Expand this string. parse_error_list_t errors; arg_expanded.clear(); - int expand_ret = expand_string(arg_str, &arg_expanded, EXPAND_NO_DESCRIPTIONS, &errors); + int expand_ret = + expand_string(arg_str, &arg_expanded, EXPAND_NO_DESCRIPTIONS, parser->vars(), &errors); parse_error_offset_source_start(&errors, arg_node.source_range()->start); switch (expand_ret) { case EXPAND_ERROR: { @@ -931,7 +935,8 @@ bool parse_execution_context_t::determine_io_chain(tnode_tsrc, &source_fd, &target); // PCA: I can't justify this EXPAND_SKIP_VARIABLES flag. It was like this when I got here. - bool target_expanded = expand_one(target, no_exec ? EXPAND_SKIP_VARIABLES : 0, NULL); + bool target_expanded = + expand_one(target, no_exec ? EXPAND_SKIP_VARIABLES : 0, parser->vars()); if (!target_expanded || target.empty()) { // TODO: Improve this error message. errored = diff --git a/src/parse_util.cpp b/src/parse_util.cpp index 83d4fa109..6d4d3f5a7 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -19,6 +19,7 @@ #include "future_feature_flags.h" #include "parse_constants.h" #include "parse_util.h" +#include "parser.h" #include "tnode.h" #include "tokenizer.h" #include "util.h" @@ -1131,8 +1132,8 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src, if (maybe_t unexp_command = command_for_plain_statement(pst, buff_src)) { wcstring command; // Check that we can expand the command. - if (expand_to_command_and_args(*unexp_command, &command, nullptr, parse_errors) == - EXPAND_ERROR) { + if (expand_to_command_and_args(*unexp_command, null_environment_t{}, &command, nullptr, + parse_errors) == EXPAND_ERROR) { errored = true; } diff --git a/src/parser.cpp b/src/parser.cpp index 4196c3dd8..124fdbb6b 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -96,30 +96,26 @@ static const struct block_lookup_entry block_lookup[] = { {(block_type_t)0, 0, 0}}; // Given a file path, return something nicer. Currently we just "unexpand" tildes. -static wcstring user_presentable_path(const wcstring &path) { - return replace_home_directory_with_tilde(path); +wcstring parser_t::user_presentable_path(const wcstring &path) const { + return replace_home_directory_with_tilde(path, vars()); } -parser_t::parser_t() : cancellation_requested(false), is_within_fish_initialization(false) {} +parser_t::parser_t() : variables(env_stack_t::principal()) {} // Out of line destructor to enable forward declaration of parse_execution_context_t parser_t::~parser_t() = default; -static parser_t s_principal_parser; +parser_t parser_t::principal; parser_t &parser_t::principal_parser() { ASSERT_IS_MAIN_THREAD(); - return s_principal_parser; -} - -void parser_t::set_is_within_fish_initialization(bool flag) { - is_within_fish_initialization = flag; + return principal; } void parser_t::skip_all_blocks() { // Tell all blocks to skip. // This may be called from a signal handler! - s_principal_parser.cancellation_requested = true; + principal.cancellation_requested = true; } // Given a new-allocated block, push it onto our block stack, acquiring ownership @@ -158,7 +154,7 @@ void parser_t::push_block_int(block_t *new_current) { } if (new_current->type() != TOP) { - env_push(type == FUNCTION_CALL); + vars().push(type == FUNCTION_CALL); new_current->wants_pop_env = true; } } @@ -176,7 +172,7 @@ void parser_t::pop_block(const block_t *expected) { std::unique_ptr old = std::move(block_stack.back()); block_stack.pop_back(); - if (old->wants_pop_env) env_pop(); + if (old->wants_pop_env) vars().pop(); // Figure out if `status is-block` should consider us to be in a block now. bool new_is_block = false; @@ -318,6 +314,7 @@ void parser_t::emit_profiling(const char *path) const { } void parser_t::expand_argument_list(const wcstring &arg_list_src, expand_flags_t eflags, + const environment_t &vars, std::vector *output_arg_list) { assert(output_arg_list != NULL); @@ -334,7 +331,8 @@ void parser_t::expand_argument_list(const wcstring &arg_list_src, expand_flags_t tnode_t arg_list(&tree, &tree.at(0)); while (auto arg = arg_list.next_in_list()) { const wcstring arg_src = arg.get_source(arg_list_src); - if (expand_string(arg_src, output_arg_list, eflags, NULL) == EXPAND_ERROR) { + if (expand_string(arg_src, output_arg_list, eflags, vars, NULL /* errors */) == + EXPAND_ERROR) { break; // failed to expand a string } } @@ -399,7 +397,7 @@ void parser_t::stack_trace_internal(size_t block_idx, wcstring *buff) const { if (file) { append_format(*buff, _(L"\tcalled on line %d of file %ls\n"), b->src_lineno, user_presentable_path(file).c_str()); - } else if (is_within_fish_initialization) { + } else if (is_within_fish_initialization()) { append_format(*buff, _(L"\tcalled during startup\n")); } else { append_format(*buff, _(L"\tcalled on standard input\n")); @@ -536,7 +534,7 @@ wcstring parser_t::current_line() { if (file) { append_format(prefix, _(L"%ls (line %d): "), user_presentable_path(file).c_str(), lineno); - } else if (is_within_fish_initialization) { + } else if (is_within_fish_initialization()) { append_format(prefix, L"%ls (line %d): ", _(L"Startup"), lineno); } else { append_format(prefix, L"%ls (line %d): ", _(L"Standard input"), lineno); diff --git a/src/parser.h b/src/parser.h index 5449db970..6edea461e 100644 --- a/src/parser.h +++ b/src/parser.h @@ -162,9 +162,7 @@ class parser_t { private: /// Indication that we should skip all blocks. - volatile sig_atomic_t cancellation_requested; - /// Indicates that we are within the process of initializing fish. - bool is_within_fish_initialization; + volatile sig_atomic_t cancellation_requested = false; /// The current execution context. std::unique_ptr execution_context; /// List of called functions, used to help prevent infinite recursion. @@ -175,7 +173,8 @@ class parser_t { std::vector> block_stack; /// The 'depth' of the fish call stack. int eval_level = -1; - + /// Set of variables for the parser. + env_stack_t &variables; #if 0 // TODO: Lint says this isn't used (which is true). Should this be removed? /// Gets a description of the block stack, for debugging. @@ -200,12 +199,21 @@ class parser_t { /// every block if it is of type FUNCTION_CALL. const wchar_t *is_function(size_t idx = 0) const; + // Given a file path, return something nicer. Currently we just "unexpand" tildes. + wcstring user_presentable_path(const wcstring &path) const; + /// Helper for stack_trace(). void stack_trace_internal(size_t block_idx, wcstring *out) const; /// Helper for push_block() void push_block_int(block_t *b); + /// Create a parser. + parser_t(); + + /// The main parser. + static parser_t principal; + public: /// Get the "principal" parser, whatever that is. static parser_t &principal_parser(); @@ -214,9 +222,6 @@ class parser_t { /// from signal handlers! static void skip_all_blocks(); - /// Create a parser. - parser_t(); - /// Global event blocks. event_blockage_list_t global_event_blocks; @@ -245,7 +250,7 @@ class parser_t { /// \param flags Some expand flags to use /// \param output List to insert output into static void expand_argument_list(const wcstring &arg_src, expand_flags_t flags, - std::vector *output); + const environment_t &vars, std::vector *output); /// Returns a string describing the current parser position in the format 'FILENAME (line /// LINE_NUMBER): LINE'. Example: @@ -270,9 +275,9 @@ class parser_t { /// Get the list of jobs. job_list_t &job_list() { return my_job_list; } - // Hackish. In order to correctly report the origin of code with no associated file, we need to - // know whether it's run during initialization or not. - void set_is_within_fish_initialization(bool flag); + /// Get the variables. + env_stack_t &vars() { return variables; } + const env_stack_t &vars() const { return variables; } /// Pushes a new block created with the given arguments /// Returns a pointer to the block. The pointer is valid diff --git a/src/path.cpp b/src/path.cpp index 2b1b027dc..4117cec41 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -115,16 +115,12 @@ static bool path_get_path_core(const wcstring &cmd, wcstring *out_path, return false; } -bool path_get_path(const wcstring &cmd, wcstring *out_path, const env_vars_snapshot_t &vars) { +bool path_get_path(const wcstring &cmd, wcstring *out_path, const environment_t &vars) { return path_get_path_core(cmd, out_path, vars.get(L"PATH")); } -bool path_get_path(const wcstring &cmd, wcstring *out_path) { - return path_get_path_core(cmd, out_path, env_get(L"PATH")); -} - -wcstring_list_t path_get_paths(const wcstring &cmd) { - debug(5, L"path_get_paths('%ls')", cmd.c_str()); +wcstring_list_t path_get_paths(const wcstring &cmd, const environment_t &vars) { + debug(3, L"path_get_paths('%ls')", cmd.c_str()); wcstring_list_t paths; // If the command has a slash, it must be an absolute or relative path and thus we don't bother @@ -138,7 +134,7 @@ wcstring_list_t path_get_paths(const wcstring &cmd) { return paths; } - auto path_var = env_get(L"PATH"); + auto path_var = vars.get(L"PATH"); std::vector pathsv; if (path_var) path_var->to_list(pathsv); for (auto path : pathsv) { @@ -157,13 +153,12 @@ wcstring_list_t path_get_paths(const wcstring &cmd) { return paths; } -bool path_get_cdpath(const wcstring &dir, wcstring *out, const wcstring &wd, - const env_vars_snapshot_t &env_vars) { +maybe_t path_get_cdpath(const wcstring &dir, const wcstring &wd, + const environment_t &env_vars) { int err = ENOENT; - if (dir.empty()) return false; - - assert(!wd.empty() && wd.back() == L'/'); + if (dir.empty()) return none(); + assert(wd.empty() || wd.back() == L'/'); wcstring_list_t paths; if (dir.at(0) == L'/') { // Absolute path. @@ -188,7 +183,7 @@ bool path_get_cdpath(const wcstring &dir, wcstring *out, const wcstring &wd, // TODO: if next_path starts with ./ we need to replace the . with the wd. next_path = wd; } - expand_tilde(next_path); + expand_tilde(next_path, env_vars); if (next_path.empty()) continue; wcstring whole_path = next_path; @@ -197,36 +192,32 @@ bool path_get_cdpath(const wcstring &dir, wcstring *out, const wcstring &wd, } } - bool success = false; for (const wcstring &dir : paths) { struct stat buf; if (wstat(dir, &buf) == 0) { if (S_ISDIR(buf.st_mode)) { - success = true; - if (out) out->assign(dir); - break; - } else { - err = ENOTDIR; + return dir; } + err = ENOTDIR; } } - if (!success) errno = err; - return success; + errno = err; + return none(); } -bool path_can_be_implicit_cd(const wcstring &path, const wcstring &wd, wcstring *out_path, - const env_vars_snapshot_t &vars) { +maybe_t path_as_implicit_cd(const wcstring &path, const wcstring &wd, + const environment_t &vars) { wcstring exp_path = path; - expand_tilde(exp_path); - - bool result = false; + expand_tilde(exp_path, vars); if (string_prefixes_string(L"/", exp_path) || string_prefixes_string(L"./", exp_path) || string_prefixes_string(L"../", exp_path) || string_suffixes_string(L"/", exp_path) || exp_path == L"..") { - result = path_get_cdpath(exp_path, out_path, wd, vars); + // These paths can be implicit cd, so see if you cd to the path. Note that a single period + // cannot (that's used for sourcing files anyways). + return path_get_cdpath(exp_path, wd, vars); } - return result; + return none(); } // If the given path looks like it's relative to the working directory, then prepend that working @@ -266,10 +257,11 @@ wcstring path_apply_working_directory(const wcstring &path, const wcstring &work static void maybe_issue_path_warning(const wcstring &which_dir, const wcstring &custom_error_msg, bool using_xdg, const wcstring &xdg_var, const wcstring &path, int saved_errno) { + auto &vars = env_stack_t::globals(); wcstring warning_var_name = L"_FISH_WARNED_" + which_dir; - auto var = env_get(warning_var_name, ENV_GLOBAL | ENV_EXPORT); + auto var = vars.get(warning_var_name, ENV_GLOBAL | ENV_EXPORT); if (!var) return; - env_set_one(warning_var_name, ENV_GLOBAL | ENV_EXPORT, L"1"); + vars.set_one(warning_var_name, ENV_GLOBAL | ENV_EXPORT, L"1"); debug(0, custom_error_msg.c_str()); if (path.empty()) { @@ -295,7 +287,8 @@ static void path_create(wcstring &path, const wcstring &xdg_var, const wcstring // The vars we fetch must be exported. Allowing them to be universal doesn't make sense and // allowing that creates a lock inversion that deadlocks the shell since we're called before // uvars are available. - const auto xdg_dir = env_get(xdg_var, ENV_GLOBAL | ENV_EXPORT); + const auto &vars = env_stack_t::globals(); + const auto xdg_dir = vars.get(xdg_var, ENV_GLOBAL | ENV_EXPORT); if (!xdg_dir.missing_or_empty()) { using_xdg = true; path = xdg_dir->as_string() + L"/fish"; @@ -305,7 +298,7 @@ static void path_create(wcstring &path, const wcstring &xdg_var, const wcstring saved_errno = errno; } } else { - const auto home = env_get(L"HOME", ENV_GLOBAL | ENV_EXPORT); + const auto home = vars.get(L"HOME", ENV_GLOBAL | ENV_EXPORT); if (!home.missing_or_empty()) { path = home->as_string() + (which_dir == L"config" ? L"/.config/fish" : L"/.local/share/fish"); diff --git a/src/path.h b/src/path.h index bb166c812..ae0cc31c0 100644 --- a/src/path.h +++ b/src/path.h @@ -34,40 +34,36 @@ bool path_get_data(wcstring &path); /// Args: /// cmd - The name of the executable. /// output_or_NULL - If non-NULL, store the full path. -/// vars - The environment variables snapshot to use +/// vars - The environment variables to use /// /// Returns: /// false if the command can not be found else true. The result /// should be freed with free(). -bool path_get_path(const wcstring &cmd, wcstring *output_or_NULL, - const env_vars_snapshot_t &vars = env_vars_snapshot_t::current()); +bool path_get_path(const wcstring &cmd, wcstring *output_or_NULL, const environment_t &vars); /// Return all the paths that match the given command. -wcstring_list_t path_get_paths(const wcstring &cmd); +wcstring_list_t path_get_paths(const wcstring &cmd, const environment_t &vars); /// Returns the full path of the specified directory, using the CDPATH variable as a list of base -/// directories for relative paths. The returned string is allocated using halloc and the specified -/// context. +/// directories for relative paths. /// -/// If no valid path is found, null is returned and errno is set to ENOTDIR if at least one such +/// If no valid path is found, false is returned and errno is set to ENOTDIR if at least one such /// path was found, but it did not point to a directory, EROTTEN if a arotten symbolic link was /// found, or ENOENT if no file of the specified name was found. If both a rotten symlink and a file /// are found, it is undefined which error status will be returned. /// /// \param dir The name of the directory. /// \param out_or_NULL If non-NULL, return the path to the resolved directory -/// \param wd The working directory, which should have a slash appended at the end. -/// \param vars The environment variable snapshot to use (for the CDPATH variable) -/// \return 0 if the command can not be found, the path of the command otherwise. The path should be -/// free'd with free(). -bool path_get_cdpath(const wcstring &dir, wcstring *out_or_NULL, const wcstring &wd, - const env_vars_snapshot_t &vars = env_vars_snapshot_t::current()); +/// \param wd The working directory. The working directory should have a slash appended at the end. +/// \param vars The environment variables to use (for the CDPATH variable) +/// \return the command, or none() if it could not be found. +maybe_t path_get_cdpath(const wcstring &dir, const wcstring &wd, + const environment_t &vars); -/// Returns whether the path can be used for an implicit cd command; if so, also returns the path by -/// reference (if desired). This requires it to start with one of the allowed prefixes (., .., ~) -/// and resolve to a directory. -bool path_can_be_implicit_cd(const wcstring &path, const wcstring &wd, wcstring *out_path = NULL, - const env_vars_snapshot_t &vars = env_vars_snapshot_t::current()); +/// Returns the path resolved as an implicit cd command, or none() if none. This requires it to +/// start with one of the allowed prefixes (., .., ~) and resolve to a directory. +maybe_t path_as_implicit_cd(const wcstring &path, const wcstring &wd, + const environment_t &vars); /// Remove double slashes and trailing slashes from a path, e.g. transform foo//bar/ into foo/bar. /// The string is modified in-place. diff --git a/src/proc.cpp b/src/proc.cpp index 69494c8dc..f8794b82e 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -6,6 +6,7 @@ // IWYU pragma: no_include <__bit_reference> #include "config.h" +#include #include #include #include @@ -1289,3 +1290,9 @@ void hup_background_jobs() { } } } + +static std::atomic s_is_within_fish_initialization{false}; + +void set_is_within_fish_initialization(bool flag) { s_is_within_fish_initialization.store(flag); } + +bool is_within_fish_initialization() { return s_is_within_fish_initialization.load(); } diff --git a/src/proc.h b/src/proc.h index 2f14c89da..b48e5d215 100644 --- a/src/proc.h +++ b/src/proc.h @@ -399,6 +399,12 @@ int proc_format_status(int status); /// Wait for any process finishing. pid_t proc_wait_any(); +/// Set and get whether we are in initialization. +// Hackish. In order to correctly report the origin of code with no associated file, we need to +// know whether it's run during initialization or not. +void set_is_within_fish_initialization(bool flag); +bool is_within_fish_initialization(); + /// Terminate all background jobs void hup_background_jobs(); diff --git a/src/reader.cpp b/src/reader.cpp index 70b6c4a3d..69a1d2d41 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -395,7 +395,15 @@ class reader_data_t { void pager_selection_changed(); /// Expand abbreviations at the current cursor position, minus backtrack_amt. - bool expand_abbreviation_as_necessary(size_t cursor_backtrack) const; + static bool expand_abbreviation_as_necessary(size_t cursor_backtrack); + + /// Return the variable set used for e.g. command duration. + env_stack_t &vars() { return parser_t::principal_parser().vars(); } + + /// Hackish access to the parser. TODO: rationalize this. + parser_t &parser() { return parser_t::principal_parser(); } + + const env_stack_t &vars() const { return parser_t::principal_parser().vars(); } /// Constructor reader_data_t(history_t *hist) : history(hist) {} @@ -691,7 +699,7 @@ void reader_data_t::pager_selection_changed() { /// Expand abbreviations at the given cursor position. Does NOT inspect 'data'. bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos, - wcstring *output) { + const environment_t &vars, wcstring *output) { // See if we are at "command position". Get the surrounding command substitution, and get the // extent of the first token. const wchar_t *const buff = cmdline.c_str(); @@ -742,14 +750,13 @@ bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t curso bool result = false; if (matching_cmd_node) { const wcstring token = matching_cmd_node.get_source(subcmd); - wcstring abbreviation; - if (expand_abbreviation(token, &abbreviation)) { + if (auto abbreviation = expand_abbreviation(token, vars)) { // There was an abbreviation! Replace the token in the full command. Maintain the // relative position of the cursor. if (output != NULL) { output->assign(cmdline); source_range_t r = *matching_cmd_node.source_range(); - output->replace(subcmd_offset + r.start, r.length, abbreviation); + output->replace(subcmd_offset + r.start, r.length, *abbreviation); } result = true; } @@ -760,15 +767,16 @@ bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t curso /// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may /// change the command line but does NOT repaint it. This is to allow the caller to coalesce /// repaints. -bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) const { +bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) { reader_data_t *data = current_data(); bool result = false; editable_line_t *el = data->active_edit_line(); - if (this->expand_abbreviations && el == &data->command_line) { + if (data->expand_abbreviations && el == &data->command_line) { // Try expanding abbreviations. wcstring new_cmdline; size_t cursor_pos = el->position - mini(el->position, cursor_backtrack); - if (reader_expand_abbreviation_in_command(el->text, cursor_pos, &new_cmdline)) { + if (reader_expand_abbreviation_in_command(el->text, cursor_pos, data->vars(), + &new_cmdline)) { // We expanded an abbreviation! The cursor moves by the difference in the command line // lengths. size_t new_buff_pos = el->position + new_cmdline.size() - el->text.size(); @@ -825,7 +833,8 @@ void reader_write_title(const wcstring &cmd, bool reset_cursor_position) { wcstring_list_t lst; proc_push_interactive(0); - if (exec_subshell(fish_title_command, lst, false /* ignore exit status */) != -1 && + if (exec_subshell(fish_title_command, current_data()->parser(), lst, + false /* ignore exit status */) != -1 && !lst.empty()) { fputws(L"\x1B]0;", stdout); for (size_t i = 0; i < lst.size(); i++) { @@ -863,7 +872,8 @@ static void exec_prompt() { // Prepend any mode indicator to the left prompt (issue #1988). if (function_exists(MODE_PROMPT_FUNCTION_NAME)) { wcstring_list_t mode_indicator_list; - exec_subshell(MODE_PROMPT_FUNCTION_NAME, mode_indicator_list, apply_exit_status); + exec_subshell(MODE_PROMPT_FUNCTION_NAME, data->parser(), mode_indicator_list, + apply_exit_status); // We do not support multiple lines in the mode indicator, so just concatenate all of // them. for (size_t i = 0; i < mode_indicator_list.size(); i++) { @@ -874,7 +884,7 @@ static void exec_prompt() { if (!data->left_prompt.empty()) { wcstring_list_t prompt_list; // Ignore return status. - exec_subshell(data->left_prompt, prompt_list, apply_exit_status); + exec_subshell(data->left_prompt, data->parser(), prompt_list, apply_exit_status); for (size_t i = 0; i < prompt_list.size(); i++) { if (i > 0) data->left_prompt_buff += L'\n'; data->left_prompt_buff += prompt_list.at(i); @@ -884,7 +894,7 @@ static void exec_prompt() { if (!data->right_prompt.empty()) { wcstring_list_t prompt_list; // Status is ignored. - exec_subshell(data->right_prompt, prompt_list, apply_exit_status); + exec_subshell(data->right_prompt, data->parser(), prompt_list, apply_exit_status); for (size_t i = 0; i < prompt_list.size(); i++) { // Right prompt does not support multiple lines, so just concatenate all of them. data->right_prompt_buff += prompt_list.at(i); @@ -903,10 +913,12 @@ static void exec_prompt() { void reader_init() { DIE_ON_FAILURE(pthread_key_create(&generation_count_key, NULL)); + auto &vars = parser_t::principal_parser().vars(); + // Ensure this var is present even before an interactive command is run so that if it is used // in a function like `fish_prompt` or `fish_right_prompt` it is defined at the time the first // prompt is written. - env_set_one(ENV_CMD_DURATION, ENV_UNEXPORT, L"0"); + vars.set_one(ENV_CMD_DURATION, ENV_UNEXPORT, L"0"); // Save the initial terminal mode. tcgetattr(STDIN_FILENO, &terminal_mode_on_startup); @@ -1277,9 +1289,10 @@ struct autosuggestion_result_t { // on a background thread) to determine the autosuggestion static std::function get_autosuggestion_performer( const wcstring &search_string, size_t cursor_pos, history_t *history) { + const auto &parser_vars = parser_t::principal_parser().vars(); const unsigned int generation_count = read_generation_count(); - const wcstring working_directory(env_get_pwd_slash()); - env_vars_snapshot_t vars(env_vars_snapshot_t::highlighting_keys); + const wcstring working_directory = parser_vars.get_pwd_slash(); + env_vars_snapshot_t vars(vars, env_vars_snapshot_t::highlighting_keys); // TODO: suspicious use of 'history' here // This is safe because histories are immortal, but perhaps // this should use shared_ptr @@ -1329,7 +1342,7 @@ static std::function get_autosuggestion_performer // Try normal completions. completion_request_flags_t complete_flags = COMPLETION_REQUEST_AUTOSUGGESTION; std::vector completions; - complete(search_string, &completions, complete_flags); + complete(search_string, &completions, complete_flags, vars); completions_sort_and_prioritize(&completions, complete_flags); if (!completions.empty()) { const completion_t &comp = completions.at(0); @@ -1824,7 +1837,7 @@ static void reader_interactive_init() { invalidate_termsize(); // For compatibility with fish 2.0's $_, now replaced with `status current-command` - env_set_one(L"_", ENV_GLOBAL, L"fish"); + parser_t::principal_parser().vars().set_one(L"_", ENV_GLOBAL, L"fish"); } /// Destroy data for interactive use. @@ -1997,7 +2010,7 @@ bool reader_get_selection(size_t *start, size_t *len) { return result; } -void set_env_cmd_duration(struct timeval *after, struct timeval *before) { +void set_env_cmd_duration(struct timeval *after, struct timeval *before, env_stack_t &vars) { time_t secs = after->tv_sec - before->tv_sec; suseconds_t usecs = after->tv_usec - before->tv_usec; wchar_t buf[16]; @@ -2008,7 +2021,7 @@ void set_env_cmd_duration(struct timeval *after, struct timeval *before) { } swprintf(buf, 16, L"%d", (secs * 1000) + (usecs / 1000)); - env_set_one(ENV_CMD_DURATION, ENV_UNEXPORT, buf); + vars.set_one(ENV_CMD_DURATION, ENV_UNEXPORT, buf); } void reader_run_command(parser_t &parser, const wcstring &cmd) { @@ -2017,7 +2030,7 @@ void reader_run_command(parser_t &parser, const wcstring &cmd) { wcstring ft = tok_first(cmd); // For compatibility with fish 2.0's $_, now replaced with `status current-command` - if (!ft.empty()) env_set_one(L"_", ENV_GLOBAL, ft); + if (!ft.empty()) parser.vars().set_one(L"_", ENV_GLOBAL, ft); reader_write_title(cmd); @@ -2032,12 +2045,12 @@ void reader_run_command(parser_t &parser, const wcstring &cmd) { // update the execution duration iff a command is requested for execution // issue - #4926 - if (!ft.empty()) set_env_cmd_duration(&time_after, &time_before); + if (!ft.empty()) set_env_cmd_duration(&time_after, &time_before, parser.vars()); term_steal(); // For compatibility with fish 2.0's $_, now replaced with `status current-command` - env_set_one(L"_", ENV_GLOBAL, program_name); + parser.vars().set_one(L"_", ENV_GLOBAL, program_name); #ifdef HAVE__PROC_SELF_STAT proc_update_jiffies(); @@ -2142,14 +2155,15 @@ void reader_import_history_if_necessary() { data->history->populate_from_config_path(); } - // Import history from bash, etc. if our current history is still empty. - if (data->history && data->history->is_empty()) { + // Import history from bash, etc. if our current history is still empty and is the default + // history. + if (data->history && data->history->is_empty() && data->history->is_default()) { // Try opening a bash file. We make an effort to respect $HISTFILE; this isn't very complete // (AFAIK it doesn't have to be exported), and to really get this right we ought to ask bash // itself. But this is better than nothing. - const auto var = env_get(L"HISTFILE"); + const auto var = data->vars().get(L"HISTFILE"); wcstring path = (var ? var->as_string() : L"~/.bash_history"); - expand_tilde(path); + expand_tilde(path, data->vars()); FILE *f = wfopen(path, "r"); if (f) { data->history->populate_from_bash(f); @@ -2201,7 +2215,8 @@ static void highlight_complete(highlight_result_t result) { static std::function get_highlight_performer(const wcstring &text, long match_highlight_pos, bool no_io) { - env_vars_snapshot_t vars(env_vars_snapshot_t::highlighting_keys); + env_vars_snapshot_t vars(parser_t::principal_parser().vars(), + env_vars_snapshot_t::highlighting_keys); unsigned int generation_count = read_generation_count(); highlight_function_t highlight_func = no_io ? highlight_shell_no_io : current_data()->highlight_func; @@ -2337,7 +2352,8 @@ uint32_t reader_run_count() { return run_count; } /// Read interactively. Read input from stdin while providing editing facilities. static int read_i() { - reader_push(history_session_id()); + parser_t &parser = parser_t::principal_parser(); + reader_push(history_session_id(parser.vars())); reader_set_complete_function(&complete); reader_set_highlight_function(&highlight_shell); reader_set_test_function(&reader_shell_test); @@ -2345,7 +2361,6 @@ static int read_i() { reader_set_expand_abbreviations(true); reader_import_history_if_necessary(); - parser_t &parser = parser_t::principal_parser(); reader_data_t *data = current_data(); data->prev_end_loop = 0; @@ -2471,6 +2486,8 @@ const wchar_t *reader_readline(int nchars) { s_reset(&data->screen, screen_reset_abandon_line); reader_repaint(); + const auto &vars = parser_t::principal_parser().vars(); + // Get the current terminal modes. These will be restored when the function returns. if (tcgetattr(STDIN_FILENO, &old_modes) == -1 && errno == EIO) redirect_tty_output(); // Set the new modes. @@ -2683,7 +2700,7 @@ const wchar_t *reader_readline(int nchars) { complete_flags_t complete_flags = COMPLETION_REQUEST_DEFAULT | COMPLETION_REQUEST_DESCRIPTIONS | COMPLETION_REQUEST_FUZZY_MATCH; - data->complete_func(buffcpy, &comp, complete_flags); + data->complete_func(buffcpy, &comp, complete_flags, vars); // Munge our completions. completions_sort_and_prioritize(&comp); @@ -2890,7 +2907,8 @@ const wchar_t *reader_readline(int nchars) { // space. const editable_line_t *el = &data->command_line; if (data->history != NULL && !el->empty() && el->text.at(0) != L' ') { - data->history->add_pending_with_file_detection(el->text); + data->history->add_pending_with_file_detection(el->text, + vars.get_pwd_slash()); } finished = 1; update_buff_pos(&data->command_line, data->command_line.size()); diff --git a/src/reader.h b/src/reader.h index 2d4338b76..873b8341d 100644 --- a/src/reader.h +++ b/src/reader.h @@ -14,8 +14,8 @@ #include "highlight.h" #include "parse_constants.h" +class environment_t; class history_t; -class env_vars_snapshot_t; class io_chain_t; /// Helper class for storing a command line. @@ -149,18 +149,14 @@ void reader_push(const wcstring &name); /// Return to previous reader environment. void reader_pop(); -/// Specify function to use for finding possible tab completions. The function must take these -/// arguments: -/// -/// - The command to be completed as a null terminated array of wchar_t -/// - An array_list_t in which completions will be inserted. +/// Specify function to use for finding possible tab completions. typedef void (*complete_function_t)(const wcstring &, std::vector *, - completion_request_flags_t); + completion_request_flags_t, const environment_t &); void reader_set_complete_function(complete_function_t); /// The type of a highlight function. typedef void (*highlight_function_t)(const wcstring &, std::vector &, size_t, - wcstring_list_t *, const env_vars_snapshot_t &vars); + wcstring_list_t *, const environment_t &vars); /// Function type for testing if a string is valid for the reader to return. using test_function_t = parser_test_error_bits_t (*)(const wcstring &); @@ -220,7 +216,7 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline, /// Expand abbreviations at the given cursor position. Exposed for testing purposes only. bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos, - wcstring *output); + const environment_t &vars, wcstring *output); /// Apply a completion string. Exposed for testing only. wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags, diff --git a/src/screen.cpp b/src/screen.cpp index 63e0906e7..1f4058405 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -110,21 +110,11 @@ static bool allow_soft_wrap() { return auto_right_margin; } -/// Does this look like the escape sequence for setting a screen name. +/// Does this look like the escape sequence for setting a screen name? static bool is_screen_name_escape_seq(const wchar_t *code, size_t *resulting_length) { if (code[1] != L'k') { return false; } - -#if 0 - // TODO: Decide if this should be removed or modified to also test for TERM values that begin - // with "tmux". See issue #3512. - const env_var_t term_name = env_get(L"TERM"); - if (term_name.missing_or_empty() || !string_prefixes_string(L"screen", term_name)) { - return false; - } -#endif - const wchar_t *const screen_name_end_sentinel = L"\x1B\\"; const wchar_t *screen_name_end = wcsstr(&code[2], screen_name_end_sentinel); if (screen_name_end == NULL) {