mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-19 18:01:15 -03:00
Port execution
Drop support for history file version 1. ParseExecutionContext no longer contains an OperationContext because in my first implementation, ParseExecutionContext didn't have interior mutability. We should probably try to add it back. Add a few to-do style comments. Search for "todo!" and "PORTING". Co-authored-by: Xiretza <xiretza@xiretza.xyz> (complete, wildcard, expand, history, history/file) Co-authored-by: Henrik Hørlück Berg <36937807+henrikhorluck@users.noreply.github.com> (builtins/set)
This commit is contained in:
@@ -82,6 +82,9 @@ struct keyword_base_t;
|
||||
|
||||
#endif
|
||||
|
||||
using DecoratedStatement = ast::decorated_statement_t;
|
||||
using BlockStatement = ast::block_statement_t;
|
||||
|
||||
namespace ast {
|
||||
using node_t = ::NodeFfi;
|
||||
}
|
||||
|
||||
246
src/autoload.cpp
246
src/autoload.cpp
@@ -1,246 +0,0 @@
|
||||
// The classes responsible for autoloading functions and completions.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "autoload.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "env.h"
|
||||
#include "io.h"
|
||||
#include "lru.h"
|
||||
#include "parser.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// The time before we'll recheck an autoloaded file.
|
||||
static const int kAutoloadStalenessInterval = 15;
|
||||
|
||||
/// Represents a file that we might want to autoload.
|
||||
namespace {
|
||||
struct autoloadable_file_t {
|
||||
/// The path to the file.
|
||||
wcstring path;
|
||||
|
||||
/// The metadata for the file.
|
||||
file_id_t file_id;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
/// Class representing a cache of files that may be autoloaded.
|
||||
/// This is responsible for performing cached accesses to a set of paths.
|
||||
class autoload_file_cache_t {
|
||||
/// A timestamp is a monotonic point in time.
|
||||
using timestamp_t = std::chrono::time_point<std::chrono::steady_clock>;
|
||||
|
||||
/// The directories from which to load.
|
||||
const std::vector<wcstring> dirs_{};
|
||||
|
||||
/// Our LRU cache of checks that were misses.
|
||||
/// The key is the command, the value is the time of the check.
|
||||
using misses_lru_cache_t = lru_cache_t<timestamp_t>;
|
||||
misses_lru_cache_t misses_cache_;
|
||||
|
||||
/// The set of files that we have returned to the caller, along with the time of the check.
|
||||
/// The key is the command (not the path).
|
||||
struct known_file_t {
|
||||
autoloadable_file_t file;
|
||||
timestamp_t last_checked;
|
||||
};
|
||||
std::unordered_map<wcstring, known_file_t> known_files_;
|
||||
|
||||
/// \return the current timestamp.
|
||||
static timestamp_t current_timestamp() { return std::chrono::steady_clock::now(); }
|
||||
|
||||
/// \return whether a timestamp is fresh enough to use.
|
||||
static bool is_fresh(timestamp_t then, timestamp_t now);
|
||||
|
||||
/// Attempt to find an autoloadable file by searching our path list for a given command.
|
||||
/// \return the file, or none() if none.
|
||||
maybe_t<autoloadable_file_t> locate_file(const wcstring &cmd) const;
|
||||
|
||||
public:
|
||||
/// Initialize with a set of directories.
|
||||
explicit autoload_file_cache_t(std::vector<wcstring> dirs) : dirs_(std::move(dirs)) {}
|
||||
|
||||
/// Initialize with empty directories.
|
||||
autoload_file_cache_t() = default;
|
||||
|
||||
/// \return the directories.
|
||||
const std::vector<wcstring> &dirs() const { return dirs_; }
|
||||
|
||||
/// Check if a command \p cmd can be loaded.
|
||||
/// If \p allow_stale is true, allow stale entries; otherwise discard them.
|
||||
/// This returns an autoloadable file, or none() if there is no such file.
|
||||
maybe_t<autoloadable_file_t> check(const wcstring &cmd, bool allow_stale = false);
|
||||
|
||||
/// \return true if a command is cached (either as a hit or miss).
|
||||
bool is_cached(const wcstring &cmd) const;
|
||||
};
|
||||
|
||||
maybe_t<autoloadable_file_t> autoload_file_cache_t::locate_file(const wcstring &cmd) const {
|
||||
// If the command is empty or starts with NULL (i.e. is empty as a path)
|
||||
// we'd try to source the *directory*, which exists.
|
||||
// So instead ignore these here.
|
||||
if (cmd.empty()) return none();
|
||||
if (cmd[0] == L'\0') return none();
|
||||
// Re-use the storage for path.
|
||||
wcstring path;
|
||||
for (const wcstring &dir : dirs()) {
|
||||
// Construct the path as dir/cmd.fish
|
||||
path = dir;
|
||||
path += L"/";
|
||||
path += cmd;
|
||||
path += L".fish";
|
||||
|
||||
file_id_t file_id = file_id_for_path(path);
|
||||
if (file_id != kInvalidFileID) {
|
||||
// Found it.
|
||||
autoloadable_file_t result;
|
||||
result.path = std::move(path);
|
||||
result.file_id = file_id;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
bool autoload_file_cache_t::is_fresh(timestamp_t then, timestamp_t now) {
|
||||
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now - then);
|
||||
return seconds.count() < kAutoloadStalenessInterval;
|
||||
}
|
||||
|
||||
maybe_t<autoloadable_file_t> autoload_file_cache_t::check(const wcstring &cmd, bool allow_stale) {
|
||||
// Check hits.
|
||||
auto iter = known_files_.find(cmd);
|
||||
if (iter != known_files_.end()) {
|
||||
if (allow_stale || is_fresh(iter->second.last_checked, current_timestamp())) {
|
||||
// Re-use this cached hit.
|
||||
return iter->second.file;
|
||||
}
|
||||
// The file is stale, remove it.
|
||||
known_files_.erase(iter);
|
||||
}
|
||||
|
||||
// Check misses.
|
||||
if (timestamp_t *miss = misses_cache_.get(cmd)) {
|
||||
if (allow_stale || is_fresh(*miss, current_timestamp())) {
|
||||
// Re-use this cached miss.
|
||||
return none();
|
||||
}
|
||||
// The miss is stale, remove it.
|
||||
misses_cache_.evict_node(cmd);
|
||||
}
|
||||
|
||||
// We couldn't satisfy this request from the cache. Hit the disk.
|
||||
maybe_t<autoloadable_file_t> file = locate_file(cmd);
|
||||
if (file.has_value()) {
|
||||
auto ins = known_files_.emplace(cmd, known_file_t{*file, current_timestamp()});
|
||||
assert(ins.second && "Known files cache should not have contained this cmd");
|
||||
(void)ins;
|
||||
} else {
|
||||
bool ins = misses_cache_.insert(cmd, current_timestamp());
|
||||
assert(ins && "Misses cache should not have contained this cmd");
|
||||
(void)ins;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
bool autoload_file_cache_t::is_cached(const wcstring &cmd) const {
|
||||
return known_files_.count(cmd) > 0 || misses_cache_.contains(cmd);
|
||||
}
|
||||
|
||||
autoload_t::autoload_t(wcstring env_var_name)
|
||||
: env_var_name_(std::move(env_var_name)), cache_(make_unique<autoload_file_cache_t>()) {}
|
||||
|
||||
autoload_t::autoload_t(autoload_t &&) noexcept = default;
|
||||
autoload_t::~autoload_t() = default;
|
||||
|
||||
void autoload_t::invalidate_cache() {
|
||||
auto cache = make_unique<autoload_file_cache_t>(cache_->dirs());
|
||||
cache_ = std::move(cache);
|
||||
}
|
||||
|
||||
bool autoload_t::can_autoload(const wcstring &cmd) {
|
||||
return cache_->check(cmd, true /* allow stale */).has_value();
|
||||
}
|
||||
|
||||
bool autoload_t::has_attempted_autoload(const wcstring &cmd) { return cache_->is_cached(cmd); }
|
||||
|
||||
std::vector<wcstring> autoload_t::get_autoloaded_commands() const {
|
||||
std::vector<wcstring> result;
|
||||
result.reserve(autoloaded_files_.size());
|
||||
for (const auto &kv : autoloaded_files_) {
|
||||
result.push_back(kv.first);
|
||||
}
|
||||
// Sort the output to make it easier to test.
|
||||
std::sort(result.begin(), result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
maybe_t<wcstring> autoload_t::resolve_command(const wcstring &cmd, const environment_t &env) {
|
||||
if (maybe_t<env_var_t> mvar = env.get(env_var_name_)) {
|
||||
return resolve_command(cmd, mvar->as_list());
|
||||
} else {
|
||||
return resolve_command(cmd, std::vector<wcstring>{});
|
||||
}
|
||||
}
|
||||
|
||||
wcstring autoload_t::resolve_command_ffi(const wcstring &cmd) {
|
||||
if (auto res = resolve_command(cmd, env_stack_t::globals())) {
|
||||
return std::move(*res);
|
||||
} else {
|
||||
return wcstring();
|
||||
}
|
||||
}
|
||||
|
||||
maybe_t<wcstring> autoload_t::resolve_command(const wcstring &cmd,
|
||||
const std::vector<wcstring> &paths) {
|
||||
// Are we currently in the process of autoloading this?
|
||||
if (current_autoloading_.count(cmd) > 0) return none();
|
||||
|
||||
// Check to see if our paths have changed. If so, replace our cache.
|
||||
// Note we don't have to modify autoloadable_files_. We'll naturally detect if those have
|
||||
// changed when we query the cache.
|
||||
if (paths != cache_->dirs()) {
|
||||
cache_ = make_unique<autoload_file_cache_t>(paths);
|
||||
}
|
||||
|
||||
// Do we have an entry to load?
|
||||
auto mfile = cache_->check(cmd);
|
||||
if (!mfile) return none();
|
||||
|
||||
// Is this file the same as what we previously autoloaded?
|
||||
auto iter = autoloaded_files_.find(cmd);
|
||||
if (iter != autoloaded_files_.end() && iter->second == mfile->file_id) {
|
||||
// The file has been autoloaded and is unchanged.
|
||||
return none();
|
||||
}
|
||||
|
||||
// We're going to (tell our caller to) autoload this command.
|
||||
current_autoloading_.insert(cmd);
|
||||
autoloaded_files_[cmd] = mfile->file_id;
|
||||
return std::move(mfile->path);
|
||||
}
|
||||
|
||||
void autoload_t::perform_autoload(const wcstring &path, parser_t &parser) {
|
||||
// We do the useful part of what exec_subshell does ourselves
|
||||
// - we source the file.
|
||||
// We don't create a buffer or check ifs or create a read_limit
|
||||
|
||||
wcstring script_source = L"source " + escape_string(path);
|
||||
auto prev_statuses = parser.get_last_statuses();
|
||||
const cleanup_t put_back([&] { parser.set_last_statuses(prev_statuses); });
|
||||
parser.eval(script_source, io_chain_t{});
|
||||
}
|
||||
|
||||
std::unique_ptr<autoload_t> make_autoload_ffi(wcstring env_var_name) {
|
||||
return make_unique<autoload_t>(std::move(env_var_name));
|
||||
}
|
||||
|
||||
void perform_autoload_ffi(const wcstring &path, parser_t &parser) {
|
||||
autoload_t::perform_autoload(path, parser);
|
||||
}
|
||||
113
src/autoload.h
113
src/autoload.h
@@ -1,114 +1 @@
|
||||
// The classes responsible for autoloading functions and completions.
|
||||
#ifndef FISH_AUTOLOAD_H
|
||||
#define FISH_AUTOLOAD_H
|
||||
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common.h"
|
||||
#include "maybe.h"
|
||||
#include "wutil.h"
|
||||
|
||||
class autoload_file_cache_t;
|
||||
class environment_t;
|
||||
class Parser; using parser_t = Parser;
|
||||
struct autoload_tester_t;
|
||||
|
||||
/// autoload_t is a class that knows how to autoload .fish files from a list of directories. This
|
||||
/// is used by autoloading functions and completions. It maintains a file cache, which is
|
||||
/// responsible for potentially cached accesses of files, and then a list of files that have
|
||||
/// actually been autoloaded. A client may request a file to autoload given a command name, and may
|
||||
/// be returned a path which it is expected to source.
|
||||
/// autoload_t does not have any internal locks; it is the responsibility of the caller to lock
|
||||
/// it.
|
||||
class autoload_t {
|
||||
/// The environment variable whose paths we observe.
|
||||
const wcstring env_var_name_;
|
||||
|
||||
/// A map from command to the files we have autoloaded.
|
||||
std::unordered_map<wcstring, file_id_t> autoloaded_files_;
|
||||
|
||||
/// The list of commands that we are currently autoloading.
|
||||
std::unordered_set<wcstring> current_autoloading_;
|
||||
|
||||
/// The autoload cache.
|
||||
/// This is a unique_ptr because want to change it if the value of our environment variable
|
||||
/// changes. This is never null (but it may be a cache with no paths).
|
||||
std::unique_ptr<autoload_file_cache_t> cache_;
|
||||
|
||||
/// Invalidate any underlying cache.
|
||||
/// This is exposed for testing.
|
||||
void invalidate_cache();
|
||||
|
||||
/// Like resolve_autoload(), but accepts the paths directly.
|
||||
/// This is exposed for testing.
|
||||
maybe_t<wcstring> resolve_command(const wcstring &cmd, const std::vector<wcstring> &paths);
|
||||
|
||||
friend autoload_tester_t;
|
||||
|
||||
public:
|
||||
/// Construct an autoloader that loads from the paths given by \p env_var_name.
|
||||
explicit autoload_t(wcstring env_var_name);
|
||||
|
||||
autoload_t(autoload_t &&);
|
||||
~autoload_t();
|
||||
|
||||
/// Given a command, get a path to autoload.
|
||||
/// For example, if the environment variable is 'fish_function_path' and the command is 'foo',
|
||||
/// this will look for a file 'foo.fish' in one of the directories given by fish_function_path.
|
||||
/// If there is no such file, OR if the file has been previously resolved and is now unchanged,
|
||||
/// this will return none. But if the file is either new or changed, this will return the path.
|
||||
/// After returning a path, the command is marked in-progress until the caller calls
|
||||
/// mark_autoload_finished() with the same command. Note this does not actually execute any
|
||||
/// code; it is the caller's responsibility to load the file.
|
||||
maybe_t<wcstring> resolve_command(const wcstring &cmd, const environment_t &env);
|
||||
|
||||
/// FFI cover. This always uses globals, and returns an empty string instead of None.
|
||||
wcstring resolve_command_ffi(const wcstring &cmd);
|
||||
|
||||
/// Helper to actually perform an autoload.
|
||||
/// This is a static function because it executes fish script, and so must be called without
|
||||
/// holding any particular locks.
|
||||
static void perform_autoload(const wcstring &path, parser_t &parser);
|
||||
|
||||
/// Mark that a command previously returned from path_to_autoload is finished autoloading.
|
||||
void mark_autoload_finished(const wcstring &cmd) {
|
||||
size_t amt = current_autoloading_.erase(cmd);
|
||||
assert(amt > 0 && "cmd was not being autoloaded");
|
||||
(void)amt;
|
||||
}
|
||||
|
||||
/// \return whether a command is currently being autoloaded.
|
||||
bool autoload_in_progress(const wcstring &cmd) const {
|
||||
return current_autoloading_.count(cmd) > 0;
|
||||
}
|
||||
|
||||
/// \return whether a command could potentially be autoloaded.
|
||||
/// This does not actually mark the command as being autoloaded.
|
||||
bool can_autoload(const wcstring &cmd);
|
||||
|
||||
/// \return whether autoloading has been attempted for a command.
|
||||
bool has_attempted_autoload(const wcstring &cmd);
|
||||
|
||||
/// \return the names of all commands that have been autoloaded. Note this includes "in-flight"
|
||||
/// commands.
|
||||
std::vector<wcstring> get_autoloaded_commands() const;
|
||||
|
||||
/// Mark that all autoloaded files have been forgotten.
|
||||
/// Future calls to path_to_autoload() will return previously-returned paths.
|
||||
void clear() {
|
||||
// Note there is no reason to invalidate the cache here.
|
||||
autoloaded_files_.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/// FFI helpers.
|
||||
std::unique_ptr<autoload_t> make_autoload_ffi(wcstring env_var_name);
|
||||
void perform_autoload_ffi(const wcstring &path, parser_t &parser);
|
||||
|
||||
#endif
|
||||
|
||||
549
src/builtin.cpp
549
src/builtin.cpp
@@ -1,67 +1,9 @@
|
||||
// Functions for executing builtin functions.
|
||||
//
|
||||
// How to add a new builtin function:
|
||||
//
|
||||
// 1). Create a function in builtin.c with the following signature:
|
||||
//
|
||||
// <tt>static maybe_t<int> builtin_NAME(parser_t &parser, io_streams_t &streams, wchar_t
|
||||
// **argv)</tt>
|
||||
//
|
||||
// where NAME is the name of the builtin, and args is a zero-terminated list of arguments.
|
||||
//
|
||||
// 2). Add a line like { L"NAME", &builtin_NAME, N_(L"Bla bla bla") }, to the builtin_data_t
|
||||
// variable. The description is used by the completion system. Note that this array is sorted.
|
||||
//
|
||||
// 3). Create a file doc_src/NAME.rst, containing the manual for the builtin in
|
||||
// reStructuredText-format. Check the other builtin manuals for proper syntax.
|
||||
//
|
||||
// 4). Use 'git add doc_src/NAME.txt' to start tracking changes to the documentation file.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "builtin.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "builtins/bind.h"
|
||||
#include "builtins/commandline.h"
|
||||
#include "builtins/complete.h"
|
||||
#include "builtins/disown.h"
|
||||
#include "builtins/eval.h"
|
||||
#include "builtins/fg.h"
|
||||
#include "builtins/history.h"
|
||||
#include "builtins/jobs.h"
|
||||
#include "builtins/read.h"
|
||||
#include "builtins/set.h"
|
||||
#include "builtins/shared.rs.h"
|
||||
#include "builtins/source.h"
|
||||
#include "builtins/ulimit.h"
|
||||
#include "complete.h"
|
||||
#include "cxx.h"
|
||||
#include "cxxgen.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "ffi.h"
|
||||
#include "flog.h"
|
||||
#include "io.h"
|
||||
#include "null_terminated_array.h"
|
||||
#include "parse_constants.h"
|
||||
#include "parse_util.h"
|
||||
#include "parser.h"
|
||||
#include "proc.h"
|
||||
#include "reader.h"
|
||||
#include "wgetopt.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
static maybe_t<RustBuiltin> try_get_rust_builtin(const wcstring &cmd);
|
||||
static maybe_t<int> builtin_run_rust(parser_t &parser, io_streams_t &streams,
|
||||
const std::vector<wcstring> &argv, RustBuiltin builtin);
|
||||
|
||||
/// Counts the number of arguments in the specified null-terminated array
|
||||
int builtin_count_args(const wchar_t *const *argv) {
|
||||
int argc;
|
||||
@@ -78,495 +20,12 @@ int builtin_count_args(const wchar_t *const *argv) {
|
||||
void builtin_wperror(const wchar_t *program_name, io_streams_t &streams) {
|
||||
char *err = std::strerror(errno);
|
||||
if (program_name != nullptr) {
|
||||
streams.err.append(program_name);
|
||||
streams.err.append(L": ");
|
||||
streams.err()->append(program_name);
|
||||
streams.err()->append(L": ");
|
||||
}
|
||||
if (err != nullptr) {
|
||||
const wcstring werr = str2wcstring(err);
|
||||
streams.err.append(werr);
|
||||
streams.err.push(L'\n');
|
||||
}
|
||||
}
|
||||
|
||||
static const wchar_t *const short_options = L"+:h";
|
||||
static const struct woption long_options[] = {{L"help", no_argument, 'h'}, {}};
|
||||
|
||||
int parse_help_only_cmd_opts(struct help_only_cmd_opts_t &opts, int *optind, int argc,
|
||||
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
|
||||
switch (opt) { //!OCLINT(too few branches)
|
||||
case 'h': {
|
||||
opts.print_help = true;
|
||||
break;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*optind = w.woptind;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
/// Display help/usage information for the specified builtin or function from manpage
|
||||
///
|
||||
/// @param name
|
||||
/// builtin or function name to get up help for
|
||||
///
|
||||
/// Process and print help for the specified builtin or function.
|
||||
void builtin_print_help(parser_t &parser, const io_streams_t &streams, const wchar_t *name,
|
||||
const wcstring &error_message) {
|
||||
// This won't ever work if no_exec is set.
|
||||
if (no_exec()) return;
|
||||
const wcstring name_esc = escape_string(name);
|
||||
wcstring cmd = format_string(L"__fish_print_help %ls ", name_esc.c_str());
|
||||
io_chain_t ios;
|
||||
if (!error_message.empty()) {
|
||||
cmd.append(escape_string(error_message));
|
||||
// If it's an error, redirect the output of __fish_print_help to stderr
|
||||
ios.push_back(std::make_shared<io_fd_t>(STDOUT_FILENO, STDERR_FILENO));
|
||||
}
|
||||
auto res = parser.eval(cmd, ios);
|
||||
if (res.status.normal_exited() && res.status.exit_code() == 2) {
|
||||
streams.err.append_format(BUILTIN_ERR_MISSING_HELP, name_esc.c_str(), name_esc.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform error reporting for encounter with unknown option.
|
||||
void builtin_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *cmd,
|
||||
const wchar_t *opt, bool print_hints) {
|
||||
streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, opt);
|
||||
if (print_hints) {
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform error reporting for encounter with missing argument.
|
||||
void builtin_missing_argument(parser_t &parser, io_streams_t &streams, const wchar_t *cmd,
|
||||
const wchar_t *opt, bool print_hints) {
|
||||
if (opt[0] == L'-' && opt[1] != L'-') {
|
||||
// if c in -qc '-qc' is missing the argument, now opt is just 'c'
|
||||
opt += std::wcslen(opt) - 1;
|
||||
// now prepend - to output -c
|
||||
streams.err.append_format(BUILTIN_ERR_MISSING, cmd, wcstring(L"-").append(opt).c_str());
|
||||
} else
|
||||
streams.err.append_format(BUILTIN_ERR_MISSING, cmd, opt);
|
||||
|
||||
if (print_hints) {
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the backtrace and call for help that we use at the end of error messages.
|
||||
void builtin_print_error_trailer(parser_t &parser, output_stream_t &b, const wchar_t *cmd) {
|
||||
b.append(L"\n");
|
||||
const wcstring stacktrace = parser.current_line();
|
||||
// Don't print two empty lines if we don't have a stacktrace.
|
||||
if (!stacktrace.empty()) {
|
||||
b.append(stacktrace);
|
||||
b.append(L"\n");
|
||||
}
|
||||
b.append_format(_(L"(Type 'help %ls' for related documentation)\n"), cmd);
|
||||
}
|
||||
|
||||
/// A generic builtin that only supports showing a help message. This is only a placeholder that
|
||||
/// prints the help message. Useful for commands that live in the parser.
|
||||
static maybe_t<int> builtin_generic(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
help_only_cmd_opts_t opts;
|
||||
int optind;
|
||||
int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// Hackish - if we have no arguments other than the command, we are a "naked invocation" and we
|
||||
// just print help.
|
||||
if (argc == 1 || wcscmp(cmd, L"time") == 0) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
static maybe_t<int> implemented_in_rust(parser_t &, io_streams_t &, const wchar_t **) {
|
||||
DIE("builtin is implemented in Rust, this should not be called");
|
||||
}
|
||||
|
||||
/// This function handles both the 'continue' and the 'break' builtins that are used for loop
|
||||
/// control.
|
||||
static maybe_t<int> builtin_break_continue(parser_t &parser, io_streams_t &streams,
|
||||
const wchar_t **argv) {
|
||||
int is_break = (std::wcscmp(argv[0], L"break") == 0);
|
||||
int argc = builtin_count_args(argv);
|
||||
|
||||
if (argc != 1) {
|
||||
wcstring error_message = format_string(BUILTIN_ERR_UNKNOWN, argv[0], argv[1]);
|
||||
builtin_print_help(parser, streams, argv[0], error_message);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Paranoia: ensure we have a real loop.
|
||||
// This is checked in the AST but we may be invoked dynamically, e.g. just via "eval break".
|
||||
bool has_loop = false;
|
||||
for (const auto &b : parser.blocks()) {
|
||||
if (b.type() == block_type_t::while_block || b.type() == block_type_t::for_block) {
|
||||
has_loop = true;
|
||||
break;
|
||||
}
|
||||
if (b.is_function_call()) break;
|
||||
}
|
||||
if (!has_loop) {
|
||||
wcstring error_message = format_string(_(L"%ls: Not inside of loop\n"), argv[0]);
|
||||
builtin_print_help(parser, streams, argv[0], error_message);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
// Mark the status in the libdata.
|
||||
parser.libdata().loop_status = is_break ? loop_status_t::breaks : loop_status_t::continues;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
/// Implementation of the builtin breakpoint command, used to launch the interactive debugger.
|
||||
static maybe_t<int> builtin_breakpoint(parser_t &parser, io_streams_t &streams,
|
||||
const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
if (argv[1] != nullptr) {
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 0, builtin_count_args(argv) - 1);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// If we're not interactive then we can't enter the debugger. So treat this command as a no-op.
|
||||
if (!parser.is_interactive()) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
// Ensure we don't allow creating a breakpoint at an interactive prompt. There may be a simpler
|
||||
// or clearer way to do this but this works.
|
||||
const block_t *block1 = parser.block_at_index(1);
|
||||
if (!block1 || block1->type() == block_type_t::breakpoint) {
|
||||
streams.err.append_format(_(L"%ls: Command not valid at an interactive prompt\n"), cmd);
|
||||
return STATUS_ILLEGAL_CMD;
|
||||
}
|
||||
|
||||
const block_t *bpb = parser.push_block(block_t::breakpoint_block());
|
||||
reader_read(parser, STDIN_FILENO, streams.io_chain ? *streams.io_chain : io_chain_t());
|
||||
parser.pop_block(bpb);
|
||||
return parser.get_last_status();
|
||||
}
|
||||
|
||||
static maybe_t<int> builtin_true(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
UNUSED(parser);
|
||||
UNUSED(streams);
|
||||
UNUSED(argv);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
static maybe_t<int> builtin_false(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
UNUSED(parser);
|
||||
UNUSED(streams);
|
||||
UNUSED(argv);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
static maybe_t<int> builtin_gettext(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
UNUSED(parser);
|
||||
for (int i = 1; i < builtin_count_args(argv); i++) {
|
||||
streams.out.append(_(argv[i]));
|
||||
}
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// END OF BUILTIN COMMANDS
|
||||
// Below are functions for handling the builtin commands.
|
||||
// THESE MUST BE SORTED BY NAME! Completion lookup uses binary search.
|
||||
|
||||
// Data about all the builtin commands in fish.
|
||||
// Functions that are bound to builtin_generic are handled directly by the parser.
|
||||
// NOTE: These must be kept in sorted order!
|
||||
static constexpr builtin_data_t builtin_datas[] = {
|
||||
{L".", &builtin_source, N_(L"Evaluate contents of file")},
|
||||
{L":", &builtin_true, N_(L"Return a successful result")},
|
||||
{L"[", &implemented_in_rust, N_(L"Test a condition")},
|
||||
{L"_", &builtin_gettext, N_(L"Translate a string")},
|
||||
{L"abbr", &implemented_in_rust, N_(L"Manage abbreviations")},
|
||||
{L"and", &builtin_generic, N_(L"Run command if last command succeeded")},
|
||||
{L"argparse", &implemented_in_rust, N_(L"Parse options in fish script")},
|
||||
{L"begin", &builtin_generic, N_(L"Create a block of code")},
|
||||
{L"bg", &implemented_in_rust, N_(L"Send job to background")},
|
||||
{L"bind", &builtin_bind, N_(L"Handle fish key bindings")},
|
||||
{L"block", &implemented_in_rust, N_(L"Temporarily block delivery of events")},
|
||||
{L"break", &builtin_break_continue, N_(L"Stop the innermost loop")},
|
||||
{L"breakpoint", &builtin_breakpoint, N_(L"Halt execution and start debug prompt")},
|
||||
{L"builtin", &implemented_in_rust, N_(L"Run a builtin specifically")},
|
||||
{L"case", &builtin_generic, N_(L"Block of code to run conditionally")},
|
||||
{L"cd", &implemented_in_rust, N_(L"Change working directory")},
|
||||
{L"command", &implemented_in_rust, N_(L"Run a command specifically")},
|
||||
{L"commandline", &builtin_commandline, N_(L"Set or get the commandline")},
|
||||
{L"complete", &builtin_complete, N_(L"Edit command specific completions")},
|
||||
{L"contains", &implemented_in_rust, N_(L"Search for a specified string in a list")},
|
||||
{L"continue", &builtin_break_continue, N_(L"Skip over remaining innermost loop")},
|
||||
{L"count", &implemented_in_rust, N_(L"Count the number of arguments")},
|
||||
{L"disown", &builtin_disown, N_(L"Remove job from job list")},
|
||||
{L"echo", &implemented_in_rust, N_(L"Print arguments")},
|
||||
{L"else", &builtin_generic, N_(L"Evaluate block if condition is false")},
|
||||
{L"emit", &implemented_in_rust, N_(L"Emit an event")},
|
||||
{L"end", &builtin_generic, N_(L"End a block of commands")},
|
||||
{L"eval", &builtin_eval, N_(L"Evaluate a string as a statement")},
|
||||
{L"exec", &builtin_generic, N_(L"Run command in current process")},
|
||||
{L"exit", &implemented_in_rust, N_(L"Exit the shell")},
|
||||
{L"false", &builtin_false, N_(L"Return an unsuccessful result")},
|
||||
{L"fg", &builtin_fg, N_(L"Send job to foreground")},
|
||||
{L"for", &builtin_generic, N_(L"Perform a set of commands multiple times")},
|
||||
{L"function", &builtin_generic, N_(L"Define a new function")},
|
||||
{L"functions", &implemented_in_rust, N_(L"List or remove functions")},
|
||||
{L"history", &builtin_history, N_(L"History of commands executed by user")},
|
||||
{L"if", &builtin_generic, N_(L"Evaluate block if condition is true")},
|
||||
{L"jobs", &builtin_jobs, N_(L"Print currently running jobs")},
|
||||
{L"math", &implemented_in_rust, N_(L"Evaluate math expressions")},
|
||||
{L"not", &builtin_generic, N_(L"Negate exit status of job")},
|
||||
{L"or", &builtin_generic, N_(L"Execute command if previous command failed")},
|
||||
{L"path", &implemented_in_rust, N_(L"Handle paths")},
|
||||
{L"printf", &implemented_in_rust, N_(L"Prints formatted text")},
|
||||
{L"pwd", &implemented_in_rust, N_(L"Print the working directory")},
|
||||
{L"random", &implemented_in_rust, N_(L"Generate random number")},
|
||||
{L"read", &builtin_read, N_(L"Read a line of input into variables")},
|
||||
{L"realpath", &implemented_in_rust, N_(L"Show absolute path sans symlinks")},
|
||||
{L"return", &implemented_in_rust, N_(L"Stop the currently evaluated function")},
|
||||
{L"set", &builtin_set, N_(L"Handle environment variables")},
|
||||
{L"set_color", &implemented_in_rust, N_(L"Set the terminal color")},
|
||||
{L"source", &builtin_source, N_(L"Evaluate contents of file")},
|
||||
{L"status", &implemented_in_rust, N_(L"Return status information about fish")},
|
||||
{L"string", &implemented_in_rust, N_(L"Manipulate strings")},
|
||||
{L"switch", &builtin_generic, N_(L"Conditionally run blocks of code")},
|
||||
{L"test", &implemented_in_rust, N_(L"Test a condition")},
|
||||
{L"time", &builtin_generic, N_(L"Measure how long a command or block takes")},
|
||||
{L"true", &builtin_true, N_(L"Return a successful result")},
|
||||
{L"type", &implemented_in_rust, N_(L"Check if a thing is a thing")},
|
||||
{L"ulimit", &builtin_ulimit, N_(L"Get/set resource usage limits")},
|
||||
{L"wait", &implemented_in_rust, N_(L"Wait for background processes completed")},
|
||||
{L"while", &builtin_generic, N_(L"Perform a command multiple times")},
|
||||
};
|
||||
ASSERT_SORTED_BY_NAME(builtin_datas);
|
||||
|
||||
#define BUILTIN_COUNT (sizeof builtin_datas / sizeof *builtin_datas)
|
||||
|
||||
/// Look up a builtin_data_t for a specified builtin
|
||||
///
|
||||
/// @param name
|
||||
/// Name of the builtin
|
||||
///
|
||||
/// @return
|
||||
/// Pointer to a builtin_data_t
|
||||
///
|
||||
static const builtin_data_t *builtin_lookup(const wcstring &name) {
|
||||
return get_by_sorted_name(name.c_str(), builtin_datas);
|
||||
}
|
||||
|
||||
/// Is there a builtin command with the given name?
|
||||
bool builtin_exists(const wcstring &cmd) { return static_cast<bool>(builtin_lookup(cmd)); }
|
||||
|
||||
/// Is the command a keyword we need to special-case the handling of `-h` and `--help`.
|
||||
static const wchar_t *const help_builtins[] = {L"for", L"while", L"function", L"if",
|
||||
L"end", L"switch", L"case"};
|
||||
static bool cmd_needs_help(const wcstring &cmd) { return contains(help_builtins, cmd); }
|
||||
|
||||
/// Execute a builtin command
|
||||
proc_status_t builtin_run(parser_t &parser, const std::vector<wcstring> &argv,
|
||||
io_streams_t &streams) {
|
||||
if (argv.empty()) return proc_status_t::from_exit_code(STATUS_INVALID_ARGS);
|
||||
const wcstring &cmdname = argv.front();
|
||||
|
||||
// We can be handed a keyword by the parser as if it was a command. This happens when the user
|
||||
// follows the keyword by `-h` or `--help`. Since it isn't really a builtin command we need to
|
||||
// handle displaying help for it here.
|
||||
if (argv.size() == 2 && parse_util_argument_is_help(argv[1]) && cmd_needs_help(cmdname)) {
|
||||
builtin_print_help(parser, streams, cmdname.c_str());
|
||||
return proc_status_t::from_exit_code(STATUS_CMD_OK);
|
||||
}
|
||||
|
||||
maybe_t<int> builtin_ret;
|
||||
|
||||
auto rust_builtin = try_get_rust_builtin(cmdname);
|
||||
if (rust_builtin.has_value()) {
|
||||
builtin_ret = builtin_run_rust(parser, streams, argv, *rust_builtin);
|
||||
} else if (const builtin_data_t *data = builtin_lookup(cmdname)) {
|
||||
// Construct the permutable argv array which the builtin expects, and execute the builtin.
|
||||
null_terminated_array_t<wchar_t> argv_arr(argv);
|
||||
builtin_ret = data->func(parser, streams, argv_arr.get());
|
||||
} else {
|
||||
FLOGF(error, UNKNOWN_BUILTIN_ERR_MSG, cmdname.c_str());
|
||||
return proc_status_t::from_exit_code(STATUS_CMD_ERROR);
|
||||
}
|
||||
|
||||
// Flush our out and error streams, and check for their errors.
|
||||
int out_ret = streams.out.flush_and_check_error();
|
||||
int err_ret = streams.err.flush_and_check_error();
|
||||
|
||||
// Resolve our status code.
|
||||
// If the builtin itself produced an error, use that error.
|
||||
// Otherwise use any errors from writing to out and writing to err, in that order.
|
||||
int code = builtin_ret.has_value() ? *builtin_ret : 0;
|
||||
if (code == 0) code = out_ret;
|
||||
if (code == 0) code = err_ret;
|
||||
|
||||
// The exit code is cast to an 8-bit unsigned integer, so saturate to 255. Otherwise,
|
||||
// multiples of 256 are reported as 0.
|
||||
if (code > 255) code = 255;
|
||||
|
||||
// Handle the case of an empty status.
|
||||
if (code == 0 && !builtin_ret.has_value()) {
|
||||
return proc_status_t::empty();
|
||||
}
|
||||
if (code < 0) {
|
||||
// If the code is below 0, constructing a proc_status_t
|
||||
// would assert() out, which is a terrible failure mode
|
||||
// So instead, what we do is we get a positive code,
|
||||
// and we avoid 0.
|
||||
code = abs((256 + code) % 256);
|
||||
if (code == 0) code = 255;
|
||||
FLOGF(warning, "builtin %ls returned invalid exit code %d", cmdname.c_str(), code);
|
||||
}
|
||||
return proc_status_t::from_exit_code(code);
|
||||
}
|
||||
|
||||
/// Returns a list of all builtin names.
|
||||
std::vector<wcstring> builtin_get_names() {
|
||||
std::vector<wcstring> result;
|
||||
result.reserve(BUILTIN_COUNT);
|
||||
for (const auto &builtin_data : builtin_datas) {
|
||||
result.push_back(builtin_data.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
wcstring_list_ffi_t builtin_get_names_ffi() { return builtin_get_names(); }
|
||||
|
||||
/// Insert all builtin names into list.
|
||||
void builtin_get_names(completion_list_t *list) {
|
||||
assert(list != nullptr);
|
||||
list->reserve(list->size() + BUILTIN_COUNT);
|
||||
for (const auto &builtin_data : builtin_datas) {
|
||||
append_completion(list, builtin_data.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a one-line description of the specified builtin.
|
||||
const wchar_t *builtin_get_desc(const wcstring &name) {
|
||||
const wchar_t *result = L"";
|
||||
const builtin_data_t *builtin = builtin_lookup(name);
|
||||
if (builtin) {
|
||||
result = _(builtin->desc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static maybe_t<RustBuiltin> try_get_rust_builtin(const wcstring &cmd) {
|
||||
if (cmd == L"abbr") {
|
||||
return RustBuiltin::Abbr;
|
||||
}
|
||||
if (cmd == L"argparse") {
|
||||
return RustBuiltin::Argparse;
|
||||
}
|
||||
if (cmd == L"bg") {
|
||||
return RustBuiltin::Bg;
|
||||
}
|
||||
if (cmd == L"block") {
|
||||
return RustBuiltin::Block;
|
||||
}
|
||||
if (cmd == L"builtin") {
|
||||
return RustBuiltin::Builtin;
|
||||
}
|
||||
if (cmd == L"cd") {
|
||||
return RustBuiltin::Cd;
|
||||
}
|
||||
if (cmd == L"contains") {
|
||||
return RustBuiltin::Contains;
|
||||
}
|
||||
if (cmd == L"command") {
|
||||
return RustBuiltin::Command;
|
||||
}
|
||||
if (cmd == L"count") {
|
||||
return RustBuiltin::Count;
|
||||
}
|
||||
if (cmd == L"echo") {
|
||||
return RustBuiltin::Echo;
|
||||
}
|
||||
if (cmd == L"emit") {
|
||||
return RustBuiltin::Emit;
|
||||
}
|
||||
if (cmd == L"exit") {
|
||||
return RustBuiltin::Exit;
|
||||
}
|
||||
if (cmd == L"functions") {
|
||||
return RustBuiltin::Functions;
|
||||
}
|
||||
if (cmd == L"math") {
|
||||
return RustBuiltin::Math;
|
||||
}
|
||||
if (cmd == L"pwd") {
|
||||
return RustBuiltin::Pwd;
|
||||
}
|
||||
if (cmd == L"random") {
|
||||
return RustBuiltin::Random;
|
||||
}
|
||||
if (cmd == L"realpath") {
|
||||
return RustBuiltin::Realpath;
|
||||
}
|
||||
if (cmd == L"set_color") {
|
||||
return RustBuiltin::SetColor;
|
||||
}
|
||||
if (cmd == L"status") {
|
||||
return RustBuiltin::Status;
|
||||
}
|
||||
if (cmd == L"string") {
|
||||
return RustBuiltin::String;
|
||||
}
|
||||
if (cmd == L"test" || cmd == L"[") {
|
||||
return RustBuiltin::Test;
|
||||
}
|
||||
if (cmd == L"type") {
|
||||
return RustBuiltin::Type;
|
||||
}
|
||||
if (cmd == L"wait") {
|
||||
return RustBuiltin::Wait;
|
||||
}
|
||||
if (cmd == L"path") {
|
||||
return RustBuiltin::Path;
|
||||
}
|
||||
if (cmd == L"printf") {
|
||||
return RustBuiltin::Printf;
|
||||
}
|
||||
if (cmd == L"return") {
|
||||
return RustBuiltin::Return;
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
static maybe_t<int> builtin_run_rust(parser_t &parser, io_streams_t &streams,
|
||||
const std::vector<wcstring> &argv, RustBuiltin builtin) {
|
||||
int status_code;
|
||||
bool update_status = rust_run_builtin(parser, streams, argv, builtin, status_code);
|
||||
if (update_status) {
|
||||
return status_code;
|
||||
} else {
|
||||
return none();
|
||||
streams.err()->append(werr);
|
||||
streams.err()->push(L'\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,24 @@
|
||||
#include "maybe.h"
|
||||
#include "wutil.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
struct Parser;
|
||||
struct IoStreams;
|
||||
|
||||
using parser_t = Parser;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
class proc_status_t;
|
||||
class output_stream_t;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
using completion_list_t = std::vector<completion_t>;
|
||||
struct OutputStreamFfi;
|
||||
using output_stream_t = OutputStreamFfi;
|
||||
struct CompletionListFfi;
|
||||
using completion_list_t = CompletionListFfi;
|
||||
|
||||
/// Data structure to describe a builtin.
|
||||
struct builtin_data_t {
|
||||
// Name of the builtin.
|
||||
const wchar_t *name;
|
||||
// Function pointer to the builtin implementation.
|
||||
maybe_t<int> (*func)(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
maybe_t<int> (*func)(const parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
// Description of what the builtin does.
|
||||
const wchar_t *desc;
|
||||
};
|
||||
@@ -78,65 +84,8 @@ struct builtin_data_t {
|
||||
/// The send stuff to foreground message.
|
||||
#define FG_MSG _(L"Send job %d (%ls) to foreground\n")
|
||||
|
||||
bool builtin_exists(const wcstring &cmd);
|
||||
|
||||
proc_status_t builtin_run(parser_t &parser, const std::vector<wcstring> &argv,
|
||||
io_streams_t &streams);
|
||||
|
||||
std::vector<wcstring> builtin_get_names();
|
||||
wcstring_list_ffi_t builtin_get_names_ffi();
|
||||
void builtin_get_names(completion_list_t *list);
|
||||
const wchar_t *builtin_get_desc(const wcstring &name);
|
||||
|
||||
wcstring builtin_help_get(parser_t &parser, const wchar_t *cmd);
|
||||
|
||||
void builtin_print_help(parser_t &parser, const io_streams_t &streams, const wchar_t *name,
|
||||
const wcstring &error_message = {});
|
||||
int builtin_count_args(const wchar_t *const *argv);
|
||||
|
||||
void builtin_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *cmd,
|
||||
const wchar_t *opt, bool print_hints = true);
|
||||
|
||||
void builtin_missing_argument(parser_t &parser, io_streams_t &streams, const wchar_t *cmd,
|
||||
const wchar_t *opt, bool print_hints = true);
|
||||
|
||||
void builtin_print_error_trailer(parser_t &parser, output_stream_t &b, const wchar_t *cmd);
|
||||
|
||||
void builtin_wperror(const wchar_t *program_name, io_streams_t &streams);
|
||||
|
||||
struct help_only_cmd_opts_t {
|
||||
bool print_help = false;
|
||||
};
|
||||
int parse_help_only_cmd_opts(help_only_cmd_opts_t &opts, int *optind, int argc,
|
||||
const wchar_t **argv, parser_t &parser, io_streams_t &streams);
|
||||
|
||||
/// An enum of the builtins implemented in Rust.
|
||||
enum class RustBuiltin : int32_t {
|
||||
Abbr,
|
||||
Argparse,
|
||||
Bg,
|
||||
Block,
|
||||
Builtin,
|
||||
Cd,
|
||||
Contains,
|
||||
Command,
|
||||
Count,
|
||||
Echo,
|
||||
Emit,
|
||||
Exit,
|
||||
Functions,
|
||||
Math,
|
||||
Path,
|
||||
Printf,
|
||||
Pwd,
|
||||
Random,
|
||||
Realpath,
|
||||
Return,
|
||||
SetColor,
|
||||
Status,
|
||||
String,
|
||||
Test,
|
||||
Type,
|
||||
Wait,
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../parser.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
#include "builtins/shared.rs.h"
|
||||
|
||||
enum { BIND_INSERT, BIND_ERASE, BIND_KEY_NAMES, BIND_FUNCTION_NAMES };
|
||||
struct bind_cmd_opts_t {
|
||||
@@ -42,7 +43,7 @@ struct bind_cmd_opts_t {
|
||||
namespace {
|
||||
class builtin_bind_t {
|
||||
public:
|
||||
maybe_t<int> builtin_bind(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
maybe_t<int> builtin_bind(const parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
|
||||
builtin_bind_t() : input_mappings_(input_mappings()) {}
|
||||
|
||||
@@ -54,7 +55,7 @@ class builtin_bind_t {
|
||||
/// lock again.
|
||||
acquired_lock<input_mapping_set_t> input_mappings_;
|
||||
|
||||
void list(const wchar_t *bind_mode, bool user, parser_t &parser, io_streams_t &streams);
|
||||
void list(const wchar_t *bind_mode, bool user, const parser_t &parser, io_streams_t &streams);
|
||||
void key_names(bool all, io_streams_t &streams);
|
||||
void function_names(io_streams_t &streams);
|
||||
bool add(const wcstring &seq, const wchar_t *const *cmds, size_t cmds_len, const wchar_t *mode,
|
||||
@@ -62,19 +63,19 @@ class builtin_bind_t {
|
||||
bool erase(const wchar_t *const *seq, bool all, const wchar_t *mode, bool use_terminfo,
|
||||
bool user, io_streams_t &streams);
|
||||
bool get_terminfo_sequence(const wcstring &seq, wcstring *out_seq, io_streams_t &streams) const;
|
||||
bool insert(int optind, int argc, const wchar_t **argv, parser_t &parser,
|
||||
bool insert(int optind, int argc, const wchar_t **argv, const parser_t &parser,
|
||||
io_streams_t &streams);
|
||||
void list_modes(io_streams_t &streams);
|
||||
bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, parser_t &parser,
|
||||
bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, const parser_t &parser,
|
||||
io_streams_t &streams);
|
||||
bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, bool preset,
|
||||
parser_t &parser, io_streams_t &streams);
|
||||
const parser_t &parser, io_streams_t &streams);
|
||||
};
|
||||
|
||||
/// List a single key binding.
|
||||
/// Returns false if no binding with that sequence and mode exists.
|
||||
bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bool user,
|
||||
parser_t &parser, io_streams_t &streams) {
|
||||
const parser_t &parser, io_streams_t &streams) {
|
||||
std::vector<wcstring> ecmds;
|
||||
wcstring sets_mode, out;
|
||||
|
||||
@@ -117,12 +118,13 @@ bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bo
|
||||
}
|
||||
out.push_back(L'\n');
|
||||
|
||||
if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) {
|
||||
std::vector<highlight_spec_t> colors;
|
||||
highlight_shell(out, colors, parser.context());
|
||||
streams.out.append(str2wcstring(colorize(out, colors, parser.vars())));
|
||||
if (!streams.out_is_redirected() && isatty(STDOUT_FILENO)) {
|
||||
auto ffi_colors = highlight_shell_ffi(out, *parser_context(parser), false, {});
|
||||
auto ffi_colored = colorize(out, *ffi_colors, parser.vars());
|
||||
std::string colored{ffi_colored.begin(), ffi_colored.end()};
|
||||
streams.out()->append(str2wcstring(std::move(colored)));
|
||||
} else {
|
||||
streams.out.append(out);
|
||||
streams.out()->append(out);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -131,7 +133,7 @@ bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bo
|
||||
// Overload with both kinds of bindings.
|
||||
// Returns false only if neither exists.
|
||||
bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bool user,
|
||||
bool preset, parser_t &parser, io_streams_t &streams) {
|
||||
bool preset, const parser_t &parser, io_streams_t &streams) {
|
||||
bool retval = false;
|
||||
if (preset) {
|
||||
retval |= list_one(seq, bind_mode, false, parser, streams);
|
||||
@@ -143,7 +145,7 @@ bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bo
|
||||
}
|
||||
|
||||
/// List all current key bindings.
|
||||
void builtin_bind_t::list(const wchar_t *bind_mode, bool user, parser_t &parser,
|
||||
void builtin_bind_t::list(const wchar_t *bind_mode, bool user, const parser_t &parser,
|
||||
io_streams_t &streams) {
|
||||
const std::vector<input_mapping_name_t> lst = input_mappings_->get_names(user);
|
||||
|
||||
@@ -163,8 +165,8 @@ void builtin_bind_t::list(const wchar_t *bind_mode, bool user, parser_t &parser,
|
||||
void builtin_bind_t::key_names(bool all, io_streams_t &streams) {
|
||||
const std::vector<wcstring> names = input_terminfo_get_names(!all);
|
||||
for (const wcstring &name : names) {
|
||||
streams.out.append(name);
|
||||
streams.out.push(L'\n');
|
||||
streams.out()->append(name);
|
||||
streams.out()->push(L'\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ void builtin_bind_t::function_names(io_streams_t &streams) {
|
||||
|
||||
for (const auto &name : names) {
|
||||
auto seq = name.c_str();
|
||||
streams.out.append_format(L"%ls\n", seq);
|
||||
streams.out()->append(format_string(L"%ls\n", seq));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,14 +190,15 @@ bool builtin_bind_t::get_terminfo_sequence(const wcstring &seq, wcstring *out_se
|
||||
wcstring eseq = escape_string(seq, ESCAPE_NO_PRINTABLES);
|
||||
if (!opts->silent) {
|
||||
if (errno == ENOENT) {
|
||||
streams.err.append_format(_(L"%ls: No key with name '%ls' found\n"), L"bind",
|
||||
eseq.c_str());
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: No key with name '%ls' found\n"), L"bind", eseq.c_str()));
|
||||
} else if (errno == EILSEQ) {
|
||||
streams.err.append_format(_(L"%ls: Key with name '%ls' does not have any mapping\n"),
|
||||
L"bind", eseq.c_str());
|
||||
streams.err()->append(format_string(
|
||||
_(L"%ls: Key with name '%ls' does not have any mapping\n"), L"bind", eseq.c_str()));
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: Unknown error trying to bind to key named '%ls'\n"),
|
||||
L"bind", eseq.c_str());
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: Unknown error trying to bind to key named '%ls'\n"), L"bind",
|
||||
eseq.c_str()));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -258,7 +261,7 @@ bool builtin_bind_t::erase(const wchar_t *const *seq, bool all, const wchar_t *m
|
||||
return res;
|
||||
}
|
||||
|
||||
bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, parser_t &parser,
|
||||
bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, const parser_t &parser,
|
||||
io_streams_t &streams) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int arg_count = argc - optind;
|
||||
@@ -272,7 +275,8 @@ bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, parser_t
|
||||
} else {
|
||||
// Inserting both on the other hand makes no sense.
|
||||
if (opts->have_preset && opts->have_user) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--preset", "--user");
|
||||
streams.err()->append(
|
||||
format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--preset", "--user"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -301,11 +305,11 @@ bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, parser_t
|
||||
wcstring eseq = escape_string(argv[optind], ESCAPE_NO_PRINTABLES);
|
||||
if (!opts->silent) {
|
||||
if (opts->use_terminfo) {
|
||||
streams.err.append_format(_(L"%ls: No binding found for key '%ls'\n"), cmd,
|
||||
eseq.c_str());
|
||||
streams.err()->append(format_string(_(L"%ls: No binding found for key '%ls'\n"),
|
||||
cmd, eseq.c_str()));
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: No binding found for sequence '%ls'\n"), cmd,
|
||||
eseq.c_str());
|
||||
streams.err()->append(format_string(
|
||||
_(L"%ls: No binding found for sequence '%ls'\n"), cmd, eseq.c_str()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -338,12 +342,13 @@ void builtin_bind_t::list_modes(io_streams_t &streams) {
|
||||
modes.insert(binding.mode);
|
||||
}
|
||||
for (const auto &mode : modes) {
|
||||
streams.out.append_format(L"%ls\n", mode.c_str());
|
||||
streams.out()->append(format_string(L"%ls\n", mode.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
int argc, const wchar_t **argv, const parser_t &parser,
|
||||
io_streams_t &streams) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
static const wchar_t *const short_options = L":aehkKfM:Lm:s";
|
||||
static const struct woption long_options[] = {{L"all", no_argument, 'a'},
|
||||
@@ -394,7 +399,7 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
}
|
||||
case L'M': {
|
||||
if (!valid_var_name(w.woptarg)) {
|
||||
streams.err.append_format(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
opts.bind_mode = w.woptarg;
|
||||
@@ -403,7 +408,7 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
}
|
||||
case L'm': {
|
||||
if (!valid_var_name(w.woptarg)) {
|
||||
streams.err.append_format(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
opts.sets_bind_mode = w.woptarg;
|
||||
@@ -424,11 +429,11 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
break;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case L'?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
@@ -444,7 +449,7 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
} // namespace
|
||||
|
||||
/// The bind builtin, used for setting character sequences.
|
||||
maybe_t<int> builtin_bind_t::builtin_bind(parser_t &parser, io_streams_t &streams,
|
||||
maybe_t<int> builtin_bind_t::builtin_bind(const parser_t &parser, io_streams_t &streams,
|
||||
const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
@@ -499,7 +504,7 @@ maybe_t<int> builtin_bind_t::builtin_bind(parser_t &parser, io_streams_t &stream
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
streams.err.append_format(_(L"%ls: Invalid state\n"), cmd);
|
||||
streams.err()->append(format_string(_(L"%ls: Invalid state\n"), cmd));
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -507,7 +512,10 @@ maybe_t<int> builtin_bind_t::builtin_bind(parser_t &parser, io_streams_t &stream
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
maybe_t<int> builtin_bind(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
int builtin_bind(const void *_parser, void *_streams, void *_argv) {
|
||||
const auto &parser = *static_cast<const parser_t *>(_parser);
|
||||
auto &streams = *static_cast<io_streams_t *>(_streams);
|
||||
auto argv = static_cast<const wchar_t **>(_argv);
|
||||
builtin_bind_t bind;
|
||||
return bind.builtin_bind(parser, streams, argv);
|
||||
return *bind.builtin_bind(parser, streams, argv);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
maybe_t<int> builtin_bind(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
struct Parser;
|
||||
struct IoStreams;
|
||||
using parser_t = Parser;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
int builtin_bind(const void *parser, void *streams, void *argv);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "../tokenizer.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
#include "builtins/shared.rs.h"
|
||||
|
||||
/// Which part of the comandbuffer are we operating on.
|
||||
enum {
|
||||
@@ -109,25 +110,30 @@ static void write_part(const wchar_t *begin, const wchar_t *end, int cut_at_curs
|
||||
|
||||
if (token->type_ == token_type_t::string) {
|
||||
wcstring tmp = *tok->text_of(*token);
|
||||
unescape_string_in_place(&tmp, UNESCAPE_INCOMPLETE);
|
||||
out.append(tmp);
|
||||
auto maybe_unescaped = unescape_string(tmp.c_str(), tmp.size(), UNESCAPE_INCOMPLETE,
|
||||
STRING_STYLE_SCRIPT);
|
||||
assert(maybe_unescaped);
|
||||
out.append(*maybe_unescaped);
|
||||
out.push_back(L'\n');
|
||||
}
|
||||
}
|
||||
|
||||
streams.out.append(out);
|
||||
streams.out()->append(out);
|
||||
} else {
|
||||
if (cut_at_cursor) {
|
||||
streams.out.append(begin, pos);
|
||||
streams.out()->append(wcstring{begin, pos});
|
||||
} else {
|
||||
streams.out.append(begin, end - begin);
|
||||
streams.out()->append(wcstring{begin, end});
|
||||
}
|
||||
streams.out.push(L'\n');
|
||||
streams.out()->push(L'\n');
|
||||
}
|
||||
}
|
||||
|
||||
/// The commandline builtin. It is used for specifying a new value for the commandline.
|
||||
maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
int builtin_commandline(const void *_parser, void *_streams, void *_argv) {
|
||||
const auto &parser = *static_cast<const parser_t *>(_parser);
|
||||
auto &streams = *static_cast<io_streams_t *>(_streams);
|
||||
auto argv = static_cast<const wchar_t **>(_argv);
|
||||
const commandline_state_t rstate = commandline_get_state();
|
||||
const wchar_t *cmd = argv[0];
|
||||
int buffer_part = 0;
|
||||
@@ -268,11 +274,11 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case L'?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
@@ -287,13 +293,13 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
// Check for invalid switch combinations.
|
||||
if (buffer_part || cut_at_cursor || append_mode || tokenize || cursor_mode || line_mode ||
|
||||
search_mode || paging_mode || selection_start_mode || selection_end_mode) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO, argv[0]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_COMBO, argv[0]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (argc == w.woptind) {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[0]);
|
||||
builtin_missing_argument(parser, streams, cmd, argv[0], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
@@ -303,7 +309,7 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
// Don't enqueue a repaint if we're currently in the middle of one,
|
||||
// because that's an infinite loop.
|
||||
if (mc == rl::repaint_mode || mc == rl::force_repaint || mc == rl::repaint) {
|
||||
if (ld.is_repaint) continue;
|
||||
if (ld.is_repaint()) continue;
|
||||
}
|
||||
|
||||
// HACK: Execute these right here and now so they can affect any insertions/changes
|
||||
@@ -318,8 +324,9 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
reader_queue_ch(*mc);
|
||||
}
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
@@ -329,22 +336,22 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
|
||||
if (selection_mode) {
|
||||
if (rstate.selection) {
|
||||
streams.out.append(rstate.text.c_str() + rstate.selection->start,
|
||||
rstate.selection->length);
|
||||
streams.out()->append(
|
||||
{rstate.text.c_str() + rstate.selection->start, rstate.selection->length});
|
||||
}
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// Check for invalid switch combinations.
|
||||
if ((selection_start_mode || selection_end_mode) && (argc - w.woptind)) {
|
||||
streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if ((search_mode || line_mode || cursor_mode || paging_mode) && (argc - w.woptind > 1)) {
|
||||
streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
@@ -352,16 +359,16 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
(cursor_mode || line_mode || search_mode || paging_mode || paging_full_mode) &&
|
||||
// Special case - we allow to get/set cursor position relative to the process/job/token.
|
||||
!(buffer_part && cursor_mode)) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO, argv[0]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_COMBO, argv[0]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if ((tokenize || cut_at_cursor) && (argc - w.woptind)) {
|
||||
streams.err.append_format(
|
||||
streams.err()->append(format_string(
|
||||
BUILTIN_ERR_COMBO2, cmd,
|
||||
L"--cut-at-cursor and --tokenize can not be used when setting the commandline");
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
L"--cut-at-cursor and --tokenize can not be used when setting the commandline"));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
@@ -380,7 +387,8 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
}
|
||||
|
||||
if (line_mode) {
|
||||
streams.out.append_format(L"%d\n", parse_util_lineno(rstate.text, rstate.cursor_pos));
|
||||
streams.out()->append(
|
||||
format_string(L"%d\n", parse_util_lineno(rstate.text, rstate.cursor_pos)));
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
@@ -402,7 +410,7 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
source_offset_t start = rstate.selection->start;
|
||||
streams.out.append_format(L"%lu\n", static_cast<unsigned long>(start));
|
||||
streams.out()->append(format_string(L"%lu\n", static_cast<unsigned long>(start)));
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
@@ -411,7 +419,7 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
source_offset_t end = rstate.selection->end();
|
||||
streams.out.append_format(L"%lu\n", static_cast<unsigned long>(end));
|
||||
streams.out()->append(format_string(L"%lu\n", static_cast<unsigned long>(end)));
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
@@ -425,8 +433,8 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
if (override_buffer) {
|
||||
current_buffer = override_buffer;
|
||||
current_cursor_pos = std::wcslen(current_buffer);
|
||||
} else if (!ld.transient_commandlines.empty() && !cursor_mode) {
|
||||
transient = ld.transient_commandlines.back();
|
||||
} else if (!ld.transient_commandlines_empty() && !cursor_mode) {
|
||||
transient = *ld.transient_commandlines_back();
|
||||
current_buffer = transient.c_str();
|
||||
current_cursor_pos = transient.size();
|
||||
} else if (rstate.initialized) {
|
||||
@@ -436,9 +444,9 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
// There is no command line, either because we are not interactive, or because we are
|
||||
// interactive and are still reading init files (in which case we silently ignore this).
|
||||
if (!is_interactive_session()) {
|
||||
streams.err.append(cmd);
|
||||
streams.err.append(L": Can not set commandline in non-interactive mode\n");
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(cmd);
|
||||
streams.err()->append(L": Can not set commandline in non-interactive mode\n");
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
}
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
@@ -481,8 +489,8 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
if (argc - w.woptind) {
|
||||
long new_pos = fish_wcstol(argv[w.woptind]) + (begin - current_buffer);
|
||||
if (errno) {
|
||||
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, argv[w.woptind]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_NOT_NUMBER, cmd, argv[w.woptind]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
}
|
||||
|
||||
new_pos =
|
||||
@@ -490,7 +498,7 @@ maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const
|
||||
commandline_set_buffer(current_buffer, static_cast<size_t>(new_pos));
|
||||
} else {
|
||||
size_t pos = current_cursor_pos - (begin - current_buffer);
|
||||
streams.out.append_format(L"%lu\n", static_cast<unsigned long>(pos));
|
||||
streams.out()->append(format_string(L"%lu\n", static_cast<unsigned long>(pos)));
|
||||
}
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
struct Parser;
|
||||
using parser_t = Parser;
|
||||
struct IoStreams;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_commandline(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
int builtin_commandline(const void *parser, void *streams, void *argv);
|
||||
#endif
|
||||
|
||||
@@ -1,476 +0,0 @@
|
||||
// Functions used for implementing the complete builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "complete.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../complete.h"
|
||||
#include "../env.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../highlight.h"
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parse_constants.h"
|
||||
#include "../parse_util.h"
|
||||
#include "../parser.h"
|
||||
#include "../reader.h"
|
||||
#include "../wcstringutil.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
// builtin_complete_* are a set of rather silly looping functions that make sure that all the proper
|
||||
// combinations of complete_add or complete_remove get called. This is needed since complete allows
|
||||
// you to specify multiple switches on a single commandline, like 'complete -s a -s b -s c', but the
|
||||
// complete_add function only accepts one short switch and one long switch.
|
||||
|
||||
/// Silly function.
|
||||
static void builtin_complete_add2(const wcstring &cmd, bool cmd_is_path, const wchar_t *short_opt,
|
||||
const std::vector<wcstring> &gnu_opts,
|
||||
const std::vector<wcstring> &old_opts,
|
||||
completion_mode_t result_mode,
|
||||
const std::vector<wcstring> &condition, const wchar_t *comp,
|
||||
const wchar_t *desc, complete_flags_t flags) {
|
||||
for (const wchar_t *s = short_opt; *s; s++) {
|
||||
complete_add(cmd, cmd_is_path, wcstring{*s}, option_type_short, result_mode, condition,
|
||||
comp, desc, flags);
|
||||
}
|
||||
|
||||
for (const wcstring &gnu_opt : gnu_opts) {
|
||||
complete_add(cmd, cmd_is_path, gnu_opt, option_type_double_long, result_mode, condition,
|
||||
comp, desc, flags);
|
||||
}
|
||||
|
||||
for (const wcstring &old_opt : old_opts) {
|
||||
complete_add(cmd, cmd_is_path, old_opt, option_type_single_long, result_mode, condition,
|
||||
comp, desc, flags);
|
||||
}
|
||||
|
||||
if (old_opts.empty() && gnu_opts.empty() && short_opt[0] == L'\0') {
|
||||
complete_add(cmd, cmd_is_path, wcstring(), option_type_args_only, result_mode, condition,
|
||||
comp, desc, flags);
|
||||
}
|
||||
}
|
||||
|
||||
/// Silly function.
|
||||
static void builtin_complete_add(const std::vector<wcstring> &cmds,
|
||||
const std::vector<wcstring> &paths, const wchar_t *short_opt,
|
||||
const std::vector<wcstring> &gnu_opt,
|
||||
const std::vector<wcstring> &old_opt,
|
||||
completion_mode_t result_mode,
|
||||
const std::vector<wcstring> &condition, const wchar_t *comp,
|
||||
const wchar_t *desc, complete_flags_t flags) {
|
||||
for (const wcstring &cmd : cmds) {
|
||||
builtin_complete_add2(cmd, false /* not path */, short_opt, gnu_opt, old_opt, result_mode,
|
||||
condition, comp, desc, flags);
|
||||
}
|
||||
|
||||
for (const wcstring &path : paths) {
|
||||
builtin_complete_add2(path, true /* is path */, short_opt, gnu_opt, old_opt, result_mode,
|
||||
condition, comp, desc, flags);
|
||||
}
|
||||
}
|
||||
|
||||
static void builtin_complete_remove_cmd(const wcstring &cmd, bool cmd_is_path,
|
||||
const wchar_t *short_opt,
|
||||
const std::vector<wcstring> &gnu_opt,
|
||||
const std::vector<wcstring> &old_opt) {
|
||||
bool removed = false;
|
||||
for (const wchar_t *s = short_opt; *s; s++) {
|
||||
complete_remove(cmd, cmd_is_path, wcstring{*s}, option_type_short);
|
||||
removed = true;
|
||||
}
|
||||
|
||||
for (const wcstring &opt : old_opt) {
|
||||
complete_remove(cmd, cmd_is_path, opt, option_type_single_long);
|
||||
removed = true;
|
||||
}
|
||||
|
||||
for (const wcstring &opt : gnu_opt) {
|
||||
complete_remove(cmd, cmd_is_path, opt, option_type_double_long);
|
||||
removed = true;
|
||||
}
|
||||
|
||||
if (!removed) {
|
||||
// This means that all loops were empty.
|
||||
complete_remove_all(cmd, cmd_is_path);
|
||||
}
|
||||
}
|
||||
|
||||
static void builtin_complete_remove(const std::vector<wcstring> &cmds,
|
||||
const std::vector<wcstring> &paths, const wchar_t *short_opt,
|
||||
const std::vector<wcstring> &gnu_opt,
|
||||
const std::vector<wcstring> &old_opt) {
|
||||
for (const wcstring &cmd : cmds) {
|
||||
builtin_complete_remove_cmd(cmd, false /* not path */, short_opt, gnu_opt, old_opt);
|
||||
}
|
||||
|
||||
for (const wcstring &path : paths) {
|
||||
builtin_complete_remove_cmd(path, true /* is path */, short_opt, gnu_opt, old_opt);
|
||||
}
|
||||
}
|
||||
|
||||
static void builtin_complete_print(const wcstring &cmd, io_streams_t &streams, parser_t &parser) {
|
||||
const wcstring repr = complete_print(cmd);
|
||||
|
||||
// colorize if interactive
|
||||
if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) {
|
||||
std::vector<highlight_spec_t> colors;
|
||||
highlight_shell(repr, colors, parser.context());
|
||||
streams.out.append(str2wcstring(colorize(repr, colors, parser.vars())));
|
||||
} else {
|
||||
streams.out.append(repr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Values used for long-only options.
|
||||
enum {
|
||||
opt_escape = 1,
|
||||
};
|
||||
/// The complete builtin. Used for specifying programmable tab-completions. Calls the functions in
|
||||
// complete.cpp for any heavy lifting.
|
||||
maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
completion_mode_t result_mode{};
|
||||
int remove = 0;
|
||||
wcstring short_opt;
|
||||
std::vector<wcstring> gnu_opt, old_opt, subcommand;
|
||||
const wchar_t *comp = L"", *desc = L"";
|
||||
std::vector<wcstring> condition;
|
||||
bool do_complete = false;
|
||||
bool have_do_complete_param = false;
|
||||
wcstring do_complete_param;
|
||||
std::vector<wcstring> cmd_to_complete;
|
||||
std::vector<wcstring> path;
|
||||
std::vector<wcstring> wrap_targets;
|
||||
bool preserve_order = false;
|
||||
bool unescape_output = true;
|
||||
|
||||
static const wchar_t *const short_options = L":a:c:p:s:l:o:d:fFrxeuAn:C::w:hk";
|
||||
static const struct woption long_options[] = {{L"exclusive", no_argument, 'x'},
|
||||
{L"no-files", no_argument, 'f'},
|
||||
{L"force-files", no_argument, 'F'},
|
||||
{L"require-parameter", no_argument, 'r'},
|
||||
{L"path", required_argument, 'p'},
|
||||
{L"command", required_argument, 'c'},
|
||||
{L"short-option", required_argument, 's'},
|
||||
{L"long-option", required_argument, 'l'},
|
||||
{L"old-option", required_argument, 'o'},
|
||||
{L"subcommand", required_argument, 'S'},
|
||||
{L"description", required_argument, 'd'},
|
||||
{L"arguments", required_argument, 'a'},
|
||||
{L"erase", no_argument, 'e'},
|
||||
{L"unauthoritative", no_argument, 'u'},
|
||||
{L"authoritative", no_argument, 'A'},
|
||||
{L"condition", required_argument, 'n'},
|
||||
{L"wraps", required_argument, 'w'},
|
||||
{L"do-complete", optional_argument, 'C'},
|
||||
{L"help", no_argument, 'h'},
|
||||
{L"keep-order", no_argument, 'k'},
|
||||
{L"escape", no_argument, opt_escape},
|
||||
{}};
|
||||
|
||||
bool have_x = false;
|
||||
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'x': {
|
||||
result_mode.no_files = true;
|
||||
result_mode.requires_param = true;
|
||||
// Needed to print an error later.
|
||||
have_x = true;
|
||||
break;
|
||||
}
|
||||
case 'f': {
|
||||
result_mode.no_files = true;
|
||||
break;
|
||||
}
|
||||
case 'F': {
|
||||
result_mode.force_files = true;
|
||||
break;
|
||||
}
|
||||
case 'r': {
|
||||
result_mode.requires_param = true;
|
||||
break;
|
||||
}
|
||||
case 'k': {
|
||||
preserve_order = true;
|
||||
break;
|
||||
}
|
||||
case 'p':
|
||||
case 'c': {
|
||||
if (auto tmp = unescape_string(w.woptarg, UNESCAPE_SPECIAL)) {
|
||||
if (opt == 'p')
|
||||
path.push_back(*tmp);
|
||||
else
|
||||
cmd_to_complete.push_back(*tmp);
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: Invalid token '%ls'\n"), cmd, w.woptarg);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'd': {
|
||||
desc = w.woptarg;
|
||||
assert(desc);
|
||||
break;
|
||||
}
|
||||
case 'u': {
|
||||
// This option was removed in commit 1911298 and is now a no-op.
|
||||
break;
|
||||
}
|
||||
case 'A': {
|
||||
// This option was removed in commit 1911298 and is now a no-op.
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
short_opt.append(w.woptarg);
|
||||
if (w.woptarg[0] == '\0') {
|
||||
streams.err.append_format(_(L"%ls: -s requires a non-empty string\n"), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'l': {
|
||||
gnu_opt.push_back(w.woptarg);
|
||||
if (w.woptarg[0] == '\0') {
|
||||
streams.err.append_format(_(L"%ls: -l requires a non-empty string\n"), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'o': {
|
||||
old_opt.push_back(w.woptarg);
|
||||
if (w.woptarg[0] == '\0') {
|
||||
streams.err.append_format(_(L"%ls: -o requires a non-empty string\n"), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'S': {
|
||||
subcommand.push_back(w.woptarg);
|
||||
if (w.woptarg[0] == '\0') {
|
||||
streams.err.append_format(_(L"%ls: -S requires a non-empty string\n"), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'a': {
|
||||
comp = w.woptarg;
|
||||
assert(comp);
|
||||
break;
|
||||
}
|
||||
case 'e': {
|
||||
remove = 1;
|
||||
break;
|
||||
}
|
||||
case 'n': {
|
||||
condition.push_back(w.woptarg);
|
||||
assert(w.woptarg);
|
||||
break;
|
||||
}
|
||||
case 'w': {
|
||||
wrap_targets.push_back(w.woptarg);
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
do_complete = true;
|
||||
have_do_complete_param = w.woptarg != nullptr;
|
||||
if (have_do_complete_param) do_complete_param = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case opt_escape: {
|
||||
unescape_output = false;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result_mode.no_files && result_mode.force_files) {
|
||||
if (!have_x) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2, L"complete",
|
||||
L"'--no-files' and '--force-files'");
|
||||
} else {
|
||||
// The reason for us not wanting files is `-x`,
|
||||
// which is short for `-rf`.
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2, L"complete",
|
||||
L"'--exclusive' and '--force-files'");
|
||||
}
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (w.woptind != argc) {
|
||||
// Use one left-over arg as the do-complete argument
|
||||
// to enable `complete -C "git check"`.
|
||||
if (do_complete && !have_do_complete_param && argc == w.woptind + 1) {
|
||||
do_complete_param = argv[argc - 1];
|
||||
have_do_complete_param = true;
|
||||
} else if (!do_complete && cmd_to_complete.empty() && argc == w.woptind + 1) {
|
||||
// Or use one left-over arg as the command to complete
|
||||
cmd_to_complete.push_back(argv[argc - 1]);
|
||||
} else {
|
||||
streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &condition_string : condition) {
|
||||
auto errors = new_parse_error_list();
|
||||
if (parse_util_detect_errors(condition_string, &*errors)) {
|
||||
for (size_t i = 0; i < errors->size(); i++) {
|
||||
wcstring prefix(wcstring(cmd) + L": -n '" + condition_string + L"': ");
|
||||
streams.err.append(*errors->at(i)->describe_with_prefix(
|
||||
condition_string, prefix, parser.is_interactive(), false));
|
||||
streams.err.push(L'\n');
|
||||
}
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
if (comp && *comp) {
|
||||
wcstring prefix;
|
||||
prefix.append(cmd);
|
||||
prefix.append(L": ");
|
||||
|
||||
if (maybe_t<wcstring> err_text = parse_util_detect_errors_in_argument_list(comp, prefix)) {
|
||||
streams.err.append_format(L"%ls: %ls: contains a syntax error\n", cmd, comp);
|
||||
streams.err.append(*err_text);
|
||||
streams.err.push(L'\n');
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
if (do_complete) {
|
||||
if (!have_do_complete_param) {
|
||||
// No argument given, try to use the current commandline.
|
||||
auto state = commandline_get_state();
|
||||
if (!state.initialized) {
|
||||
// This corresponds to using 'complete -C' in non-interactive mode.
|
||||
// See #2361 .
|
||||
builtin_missing_argument(parser, streams, cmd, L"-C");
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
do_complete_param = std::move(state.text);
|
||||
}
|
||||
const wchar_t *token;
|
||||
|
||||
parse_util_token_extent(do_complete_param.c_str(), do_complete_param.size(), &token,
|
||||
nullptr, nullptr, nullptr);
|
||||
|
||||
// Create a scoped transient command line, so that builtin_commandline will see our
|
||||
// argument, not the reader buffer.
|
||||
parser.libdata().transient_commandlines.push_back(do_complete_param);
|
||||
cleanup_t remove_transient([&] { parser.libdata().transient_commandlines.pop_back(); });
|
||||
|
||||
// Prevent accidental recursion (see #6171).
|
||||
if (!parser.libdata().builtin_complete_current_commandline) {
|
||||
if (!have_do_complete_param) {
|
||||
parser.libdata().builtin_complete_current_commandline = true;
|
||||
}
|
||||
|
||||
completion_list_t comp = complete(
|
||||
do_complete_param, completion_request_options_t::normal(), parser.context());
|
||||
|
||||
// Apply the same sort and deduplication treatment as pager completions
|
||||
completions_sort_and_prioritize(&comp);
|
||||
|
||||
for (const auto &next : comp) {
|
||||
// Make a fake commandline, and then apply the completion to it.
|
||||
const wcstring faux_cmdline = token;
|
||||
size_t tmp_cursor = faux_cmdline.size();
|
||||
wcstring faux_cmdline_with_completion = completion_apply_to_command_line(
|
||||
next.completion, next.flags, faux_cmdline, &tmp_cursor, false);
|
||||
|
||||
// completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE
|
||||
// is set. We don't want to set COMPLETE_NO_SPACE because that won't close
|
||||
// quotes. What we want is to close the quote, but not append the space. So we
|
||||
// just look for the space and clear it.
|
||||
if (!(next.flags & COMPLETE_NO_SPACE) &&
|
||||
string_suffixes_string(L" ", faux_cmdline_with_completion)) {
|
||||
faux_cmdline_with_completion.resize(faux_cmdline_with_completion.size() - 1);
|
||||
}
|
||||
|
||||
if (unescape_output) {
|
||||
// The input data is meant to be something like you would have on the command
|
||||
// line, e.g. includes backslashes. The output should be raw, i.e. unescaped. So
|
||||
// we need to unescape the command line. See #1127.
|
||||
unescape_string_in_place(&faux_cmdline_with_completion, UNESCAPE_DEFAULT);
|
||||
}
|
||||
|
||||
// Append any description.
|
||||
if (!next.description.empty()) {
|
||||
faux_cmdline_with_completion.reserve(faux_cmdline_with_completion.size() + 2 +
|
||||
next.description.size());
|
||||
faux_cmdline_with_completion.push_back(L'\t');
|
||||
faux_cmdline_with_completion.append(next.description);
|
||||
}
|
||||
faux_cmdline_with_completion.push_back(L'\n');
|
||||
streams.out.append(faux_cmdline_with_completion);
|
||||
}
|
||||
|
||||
parser.libdata().builtin_complete_current_commandline = false;
|
||||
}
|
||||
} else if (path.empty() && gnu_opt.empty() && short_opt.empty() && old_opt.empty() && !remove &&
|
||||
!*comp && !*desc && condition.empty() && wrap_targets.empty() &&
|
||||
!result_mode.no_files && !result_mode.force_files && !result_mode.requires_param) {
|
||||
// No arguments that would add or remove anything specified, so we print the definitions of
|
||||
// all matching completions.
|
||||
if (cmd_to_complete.empty()) {
|
||||
builtin_complete_print(L"", streams, parser);
|
||||
} else {
|
||||
for (auto &cmd : cmd_to_complete) {
|
||||
builtin_complete_print(cmd, streams, parser);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int flags = COMPLETE_AUTO_SPACE;
|
||||
// HACK: Don't escape tildes because at the beginning of a token they probably mean
|
||||
// $HOME, for example as produced by a recursive call to "complete -C".
|
||||
flags |= COMPLETE_DONT_ESCAPE_TILDES;
|
||||
if (preserve_order) {
|
||||
flags |= COMPLETE_DONT_SORT;
|
||||
}
|
||||
|
||||
if (remove) {
|
||||
builtin_complete_remove(cmd_to_complete, path, short_opt.c_str(), gnu_opt, old_opt);
|
||||
} else {
|
||||
builtin_complete_add(cmd_to_complete, path, short_opt.c_str(), gnu_opt, old_opt,
|
||||
result_mode, condition, comp, desc, flags);
|
||||
}
|
||||
|
||||
// Handle wrap targets (probably empty). We only wrap commands, not paths.
|
||||
for (const auto &wrap_target : wrap_targets) {
|
||||
for (const auto &i : cmd_to_complete) {
|
||||
(remove ? complete_remove_wrapper : complete_add_wrapper)(i, wrap_target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Prototypes for functions for executing builtin_complete functions.
|
||||
#ifndef FISH_BUILTIN_COMPLETE_H
|
||||
#define FISH_BUILTIN_COMPLETE_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -1,111 +0,0 @@
|
||||
// Implementation of the disown builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "disown.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parser.h"
|
||||
#include "../proc.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// Helper for builtin_disown.
|
||||
static void disown_job(const wchar_t *cmd, io_streams_t &streams, job_t *j) {
|
||||
assert(j && "Null job");
|
||||
|
||||
// Nothing to do if already disowned.
|
||||
if (j->flags().disown_requested) return;
|
||||
|
||||
// Stopped disowned jobs must be manually signaled; explain how to do so.
|
||||
auto pgid = j->get_pgid();
|
||||
if (j->is_stopped()) {
|
||||
if (pgid.has_value()) killpg(*pgid, SIGCONT);
|
||||
const wchar_t *fmt =
|
||||
_(L"%ls: job %d ('%ls') was stopped and has been signalled to continue.\n");
|
||||
streams.err.append_format(fmt, cmd, j->job_id(), j->command_wcstr());
|
||||
}
|
||||
|
||||
// We cannot directly remove the job from the jobs() list as `disown` might be called
|
||||
// within the context of a subjob which will cause the parent job to crash in exec_job().
|
||||
// Instead, we set a flag and the parser removes the job from the jobs list later.
|
||||
j->mut_flags().disown_requested = true;
|
||||
add_disowned_job(j);
|
||||
}
|
||||
|
||||
/// Builtin for removing jobs from the job list.
|
||||
maybe_t<int> builtin_disown(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
help_only_cmd_opts_t opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
if (argv[1] == nullptr) {
|
||||
// Select last constructed job (ie first job in the job queue) that is possible to disown.
|
||||
// Stopped jobs can be disowned (they will be continued).
|
||||
// Foreground jobs can be disowned.
|
||||
// Even jobs that aren't under job control can be disowned!
|
||||
job_t *job = nullptr;
|
||||
for (const auto &j : parser.jobs()) {
|
||||
if (j->is_constructed() && (!j->is_completed())) {
|
||||
job = j.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (job) {
|
||||
disown_job(cmd, streams, job);
|
||||
retval = STATUS_CMD_OK;
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: There are no suitable jobs\n"), cmd);
|
||||
retval = STATUS_CMD_ERROR;
|
||||
}
|
||||
} else {
|
||||
std::vector<job_t *> jobs;
|
||||
|
||||
// If one argument is not a valid pid (i.e. integer >= 0), fail without disowning anything,
|
||||
// but still print errors for all of them.
|
||||
// Non-existent jobs aren't an error, but information about them is useful.
|
||||
// Multiple PIDs may refer to the same job; include the job only once by using a set.
|
||||
for (int i = 1; argv[i]; i++) {
|
||||
int pid = fish_wcstoi(argv[i]);
|
||||
if (errno || pid < 0) {
|
||||
streams.err.append_format(_(L"%ls: '%ls' is not a valid job specifier\n"), cmd,
|
||||
argv[i]);
|
||||
retval = STATUS_INVALID_ARGS;
|
||||
} else {
|
||||
if (job_t *j = parser.job_get_from_pid(pid)) {
|
||||
jobs.push_back(j);
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: Could not find job '%d'\n"), cmd, pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (retval != STATUS_CMD_OK) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
// Disown all target jobs.
|
||||
for (job_t *j : jobs) {
|
||||
disown_job(cmd, streams, j);
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Prototypes for executing builtin_disown function.
|
||||
#ifndef FISH_BUILTIN_DISOWN_H
|
||||
#define FISH_BUILTIN_DISOWN_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_disown(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -1,84 +0,0 @@
|
||||
// Functions for executing the eval builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parser.h"
|
||||
#include "../proc.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// Implementation of eval builtin.
|
||||
maybe_t<int> builtin_eval(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
int argc = builtin_count_args(argv);
|
||||
if (argc <= 1) {
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
wcstring new_cmd;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (i > 1) new_cmd += L' ';
|
||||
new_cmd += argv[i];
|
||||
}
|
||||
|
||||
// Copy the full io chain; we may append bufferfills.
|
||||
io_chain_t ios = *streams.io_chain;
|
||||
|
||||
// If stdout is piped, then its output must go to the streams, not to the io_chain in our
|
||||
// streams, because the pipe may be intended to be consumed by a process which
|
||||
// is not yet launched (#6806). If stdout is NOT redirected, it must see the tty (#6955). So
|
||||
// create a bufferfill for stdout if and only if stdout is piped.
|
||||
// Note do not do this if stdout is merely redirected (say, to a file); we don't want to
|
||||
// buffer in that case.
|
||||
shared_ptr<io_bufferfill_t> stdout_fill{};
|
||||
if (streams.out_is_piped) {
|
||||
stdout_fill = io_bufferfill_t::create(parser.libdata().read_limit, STDOUT_FILENO);
|
||||
if (!stdout_fill) {
|
||||
// We were unable to create a pipe, probably fd exhaustion.
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
ios.push_back(stdout_fill);
|
||||
}
|
||||
|
||||
// Of course the same applies to stderr.
|
||||
shared_ptr<io_bufferfill_t> stderr_fill{};
|
||||
if (streams.err_is_piped) {
|
||||
stderr_fill = io_bufferfill_t::create(parser.libdata().read_limit, STDERR_FILENO);
|
||||
if (!stderr_fill) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
ios.push_back(stderr_fill);
|
||||
}
|
||||
|
||||
int status = STATUS_CMD_OK;
|
||||
auto res = parser.eval_with(new_cmd, ios, streams.job_group, block_type_t::top);
|
||||
if (res.was_empty) {
|
||||
// Issue #5692, in particular, to catch `eval ""`, `eval "begin; end;"`, etc.
|
||||
// where we have an argument but nothing is executed.
|
||||
status = STATUS_CMD_OK;
|
||||
} else {
|
||||
status = res.status.status_value();
|
||||
}
|
||||
|
||||
// Finish the bufferfills - exhaust and close our pipes.
|
||||
// Copy the output from the bufferfill back to the streams.
|
||||
// Note it is important that we hold no other references to the bufferfills here - they need to
|
||||
// deallocate to close.
|
||||
ios.clear();
|
||||
if (stdout_fill) {
|
||||
separated_buffer_t output = io_bufferfill_t::finish(std::move(stdout_fill));
|
||||
streams.out.append_narrow_buffer(std::move(output));
|
||||
}
|
||||
if (stderr_fill) {
|
||||
separated_buffer_t errput = io_bufferfill_t::finish(std::move(stderr_fill));
|
||||
streams.err.append_narrow_buffer(std::move(errput));
|
||||
}
|
||||
return status;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Prototypes for executing builtin_eval function.
|
||||
#ifndef FISH_BUILTIN_EVAL_H
|
||||
#define FISH_BUILTIN_EVAL_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_eval(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -1,139 +0,0 @@
|
||||
// Implementation of the fg builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "fg.h"
|
||||
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cwchar>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../env.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../fds.h"
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parser.h"
|
||||
#include "../proc.h"
|
||||
#include "../reader.h"
|
||||
#include "../tokenizer.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
#include "job_group.rs.h"
|
||||
|
||||
/// Builtin for putting a job in the foreground.
|
||||
maybe_t<int> builtin_fg(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
help_only_cmd_opts_t opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
job_t *job = nullptr;
|
||||
if (optind == argc) {
|
||||
// Select last constructed job (i.e. first job in the job queue) that can be brought
|
||||
// to the foreground.
|
||||
|
||||
for (const auto &j : parser.jobs()) {
|
||||
if (j->is_constructed() && (!j->is_completed()) &&
|
||||
((j->is_stopped() || (!j->is_foreground())) && j->wants_job_control())) {
|
||||
job = j.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!job) {
|
||||
streams.err.append_format(_(L"%ls: There are no suitable jobs\n"), cmd);
|
||||
}
|
||||
} else if (optind + 1 < argc) {
|
||||
// Specifying more than one job to put to the foreground is a syntax error, we still
|
||||
// try to locate the job $argv[1], since we need to determine which error message to
|
||||
// emit (ambigous job specification vs malformed job id).
|
||||
bool found_job = false;
|
||||
int pid = fish_wcstoi(argv[optind]);
|
||||
if (errno == 0 && pid > 0) {
|
||||
found_job = (parser.job_get_from_pid(pid) != nullptr);
|
||||
}
|
||||
|
||||
if (found_job) {
|
||||
streams.err.append_format(_(L"%ls: Ambiguous job\n"), cmd);
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: '%ls' is not a job\n"), cmd, argv[optind]);
|
||||
}
|
||||
|
||||
job = nullptr;
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
} else {
|
||||
int pid = abs(fish_wcstoi(argv[optind]));
|
||||
if (errno) {
|
||||
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, argv[optind]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
} else {
|
||||
job = parser.job_get_from_pid(pid);
|
||||
if (!job || !job->is_constructed() || job->is_completed()) {
|
||||
streams.err.append_format(_(L"%ls: No suitable job: %d\n"), cmd, pid);
|
||||
job = nullptr;
|
||||
} else if (!job->wants_job_control()) {
|
||||
streams.err.append_format(_(L"%ls: Can't put job %d, '%ls' to foreground because "
|
||||
L"it is not under job control\n"),
|
||||
cmd, pid, job->command_wcstr());
|
||||
job = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (streams.err_is_redirected) {
|
||||
streams.err.append_format(FG_MSG, job->job_id(), job->command_wcstr());
|
||||
} else {
|
||||
// If we aren't redirecting, send output to real stderr, since stuff in sb_err won't get
|
||||
// printed until the command finishes.
|
||||
std::fwprintf(stderr, FG_MSG, job->job_id(), job->command_wcstr());
|
||||
}
|
||||
|
||||
wcstring ft = *tok_command(job->command());
|
||||
if (!ft.empty()) {
|
||||
// Provide value for `status current-command`
|
||||
parser.libdata().status_vars.command = ft;
|
||||
// Also provide a value for the deprecated fish 2.0 $_ variable
|
||||
parser.set_var_and_fire(L"_", ENV_EXPORT, std::move(ft));
|
||||
// Provide value for `status current-commandline`
|
||||
parser.libdata().status_vars.commandline = job->command();
|
||||
}
|
||||
reader_write_title(job->command(), parser);
|
||||
|
||||
// Note if tty transfer fails, we still try running the job.
|
||||
parser.job_promote(job);
|
||||
make_fd_blocking(STDIN_FILENO);
|
||||
job->group->set_is_foreground(true);
|
||||
if (job->group->wants_terminal() && (job->group->get_modes_ffi(sizeof(termios)) != nullptr)) {
|
||||
auto *termios = (struct termios *)job->group->get_modes_ffi(sizeof(struct termios));
|
||||
int res = tcsetattr(STDIN_FILENO, TCSADRAIN, termios);
|
||||
if (res < 0) wperror(L"tcsetattr");
|
||||
}
|
||||
tty_transfer_t transfer;
|
||||
transfer.to_job_group(job->group);
|
||||
bool resumed = job->resume();
|
||||
if (resumed) {
|
||||
job->continue_job(parser);
|
||||
}
|
||||
if (job->is_stopped()) transfer.save_tty_modes();
|
||||
transfer.reclaim();
|
||||
return resumed ? STATUS_CMD_OK : STATUS_CMD_ERROR;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Prototypes for executing builtin_fg function.
|
||||
#ifndef FISH_BUILTIN_FG_H
|
||||
#define FISH_BUILTIN_FG_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_fg(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -1,332 +0,0 @@
|
||||
// Implementation of the history builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "history.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../enum_map.h"
|
||||
#include "../env.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../history.h"
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parser.h"
|
||||
#include "../reader.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
enum hist_cmd_t {
|
||||
HIST_SEARCH = 1,
|
||||
HIST_DELETE,
|
||||
HIST_CLEAR,
|
||||
HIST_MERGE,
|
||||
HIST_SAVE,
|
||||
HIST_UNDEF,
|
||||
HIST_CLEAR_SESSION
|
||||
};
|
||||
|
||||
// Must be sorted by string, not enum or random.
|
||||
static const enum_map<hist_cmd_t> hist_enum_map[] = {
|
||||
{HIST_CLEAR, L"clear"}, {HIST_CLEAR_SESSION, L"clear-session"},
|
||||
{HIST_DELETE, L"delete"}, {HIST_MERGE, L"merge"},
|
||||
{HIST_SAVE, L"save"}, {HIST_SEARCH, L"search"},
|
||||
{HIST_UNDEF, nullptr},
|
||||
};
|
||||
|
||||
struct history_cmd_opts_t {
|
||||
hist_cmd_t hist_cmd = HIST_UNDEF;
|
||||
history_search_type_t search_type = static_cast<history_search_type_t>(-1);
|
||||
const wchar_t *show_time_format = nullptr;
|
||||
size_t max_items = SIZE_MAX;
|
||||
bool print_help = false;
|
||||
bool history_search_type_defined = false;
|
||||
bool case_sensitive = false;
|
||||
bool null_terminate = false;
|
||||
bool reverse = false;
|
||||
};
|
||||
|
||||
/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to
|
||||
/// the non-flag subcommand form. While many of these flags are deprecated they must be
|
||||
/// supported at least until fish 3.0 and possibly longer to avoid breaking everyones
|
||||
/// config.fish and other scripts.
|
||||
static const wchar_t *const short_options = L":CRcehmn:pt::z";
|
||||
static const struct woption long_options[] = {{L"prefix", no_argument, 'p'},
|
||||
{L"contains", no_argument, 'c'},
|
||||
{L"help", no_argument, 'h'},
|
||||
{L"show-time", optional_argument, 't'},
|
||||
{L"exact", no_argument, 'e'},
|
||||
{L"max", required_argument, 'n'},
|
||||
{L"null", no_argument, 'z'},
|
||||
{L"case-sensitive", no_argument, 'C'},
|
||||
{L"delete", no_argument, 1},
|
||||
{L"search", no_argument, 2},
|
||||
{L"save", no_argument, 3},
|
||||
{L"clear", no_argument, 4},
|
||||
{L"merge", no_argument, 5},
|
||||
{L"reverse", no_argument, 'R'},
|
||||
{}};
|
||||
|
||||
/// Remember the history subcommand and disallow selecting more than one history subcommand.
|
||||
static bool set_hist_cmd(const wchar_t *cmd, hist_cmd_t *hist_cmd, hist_cmd_t sub_cmd,
|
||||
io_streams_t &streams) {
|
||||
if (*hist_cmd != HIST_UNDEF) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd,
|
||||
enum_to_str(*hist_cmd, hist_enum_map),
|
||||
enum_to_str(sub_cmd, hist_enum_map));
|
||||
return false;
|
||||
}
|
||||
|
||||
*hist_cmd = sub_cmd;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool check_for_unexpected_hist_args(const history_cmd_opts_t &opts, const wchar_t *cmd,
|
||||
const std::vector<wcstring> &args,
|
||||
io_streams_t &streams) {
|
||||
if (opts.history_search_type_defined || opts.show_time_format || opts.null_terminate) {
|
||||
const wchar_t *subcmd_str = enum_to_str(opts.hist_cmd, hist_enum_map);
|
||||
streams.err.append_format(_(L"%ls: %ls: subcommand takes no options\n"), cmd, subcmd_str);
|
||||
return true;
|
||||
}
|
||||
if (!args.empty()) {
|
||||
const wchar_t *subcmd_str = enum_to_str(opts.hist_cmd, hist_enum_map);
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int parse_cmd_opts(history_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 1: {
|
||||
if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_DELETE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_SEARCH, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_SAVE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_CLEAR, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_MERGE, streams)) {
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
opts.case_sensitive = true;
|
||||
break;
|
||||
}
|
||||
case 'R': {
|
||||
opts.reverse = true;
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
opts.search_type = history_search_type_t::prefix_glob;
|
||||
opts.history_search_type_defined = true;
|
||||
break;
|
||||
}
|
||||
case 'c': {
|
||||
opts.search_type = history_search_type_t::contains_glob;
|
||||
opts.history_search_type_defined = true;
|
||||
break;
|
||||
}
|
||||
case 'e': {
|
||||
opts.search_type = history_search_type_t::exact;
|
||||
opts.history_search_type_defined = true;
|
||||
break;
|
||||
}
|
||||
case 't': {
|
||||
opts.show_time_format = w.woptarg ? w.woptarg : L"# %c%n";
|
||||
break;
|
||||
}
|
||||
case 'n': {
|
||||
long x = fish_wcstol(w.woptarg);
|
||||
if (errno) {
|
||||
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
opts.max_items = static_cast<size_t>(x);
|
||||
break;
|
||||
}
|
||||
case 'z': {
|
||||
opts.null_terminate = true;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
opts.print_help = true;
|
||||
break;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
// Try to parse it as a number; e.g., "-123".
|
||||
opts.max_items = fish_wcstol(argv[w.woptind - 1] + 1);
|
||||
if (errno) {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
w.nextchar = nullptr;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*optind = w.woptind;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
/// Manipulate history of interactive commands executed by the user.
|
||||
maybe_t<int> builtin_history(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
history_cmd_opts_t opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// Use the default history if we have none (which happens if invoked non-interactively, e.g.
|
||||
// from webconfig.py.
|
||||
std::shared_ptr<history_t> history = commandline_get_state().history;
|
||||
if (!history) history = history_t::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.
|
||||
// See the TODO above regarding the `long_options` array.
|
||||
if (optind < argc) {
|
||||
constexpr size_t hist_enum_map_len = sizeof hist_enum_map / sizeof *hist_enum_map;
|
||||
hist_cmd_t subcmd = str_to_enum(argv[optind], hist_enum_map, hist_enum_map_len);
|
||||
if (subcmd != HIST_UNDEF) {
|
||||
if (!set_hist_cmd(cmd, &opts.hist_cmd, subcmd, streams)) {
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
optind++;
|
||||
}
|
||||
}
|
||||
|
||||
// Every argument that we haven't consumed already is an argument for a subcommand (e.g., a
|
||||
// search term).
|
||||
const std::vector<wcstring> args(argv + optind, argv + argc);
|
||||
|
||||
// Establish appropriate defaults.
|
||||
if (opts.hist_cmd == HIST_UNDEF) opts.hist_cmd = HIST_SEARCH;
|
||||
if (!opts.history_search_type_defined) {
|
||||
if (opts.hist_cmd == HIST_SEARCH) opts.search_type = history_search_type_t::contains_glob;
|
||||
if (opts.hist_cmd == HIST_DELETE) opts.search_type = history_search_type_t::exact;
|
||||
}
|
||||
|
||||
int status = STATUS_CMD_OK;
|
||||
switch (opts.hist_cmd) {
|
||||
case HIST_SEARCH: {
|
||||
if (!history->search(opts.search_type, args, opts.show_time_format, opts.max_items,
|
||||
opts.case_sensitive, opts.null_terminate, opts.reverse,
|
||||
parser.cancel_checker(), streams)) {
|
||||
status = STATUS_CMD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HIST_DELETE: {
|
||||
// TODO: Move this code to the history module and support the other search types
|
||||
// including case-insensitive matches. At this time we expect the non-exact deletions to
|
||||
// be handled only by the history function's interactive delete feature.
|
||||
if (opts.search_type != history_search_type_t::exact) {
|
||||
streams.err.append_format(_(L"builtin history delete only supports --exact\n"));
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
if (!opts.case_sensitive) {
|
||||
streams.err.append_format(
|
||||
_(L"builtin history delete --exact requires --case-sensitive\n"));
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
for (const wcstring &delete_string : args) {
|
||||
history->remove(delete_string);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HIST_CLEAR: {
|
||||
if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
history->clear();
|
||||
history->save();
|
||||
break;
|
||||
}
|
||||
case HIST_CLEAR_SESSION: {
|
||||
if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
history->clear_session();
|
||||
history->save();
|
||||
break;
|
||||
}
|
||||
case HIST_MERGE: {
|
||||
if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
|
||||
if (in_private_mode(parser.vars())) {
|
||||
streams.err.append_format(_(L"%ls: can't merge history in private mode\n"), cmd);
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
history->incorporate_external_changes();
|
||||
break;
|
||||
}
|
||||
case HIST_SAVE: {
|
||||
if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
|
||||
status = STATUS_INVALID_ARGS;
|
||||
break;
|
||||
}
|
||||
history->save();
|
||||
break;
|
||||
}
|
||||
case HIST_UNDEF: {
|
||||
DIE("Unexpected HIST_UNDEF seen");
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Prototypes for executing builtin_history function.
|
||||
#ifndef FISH_BUILTIN_HISTORY_H
|
||||
#define FISH_BUILTIN_HISTORY_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_history(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -1,245 +0,0 @@
|
||||
// Functions for executing the jobs builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <cerrno>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parser.h"
|
||||
#include "../proc.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// Print modes for the jobs builtin.
|
||||
enum {
|
||||
JOBS_DEFAULT, // print lots of general info
|
||||
JOBS_PRINT_PID, // print pid of each process in job
|
||||
JOBS_PRINT_COMMAND, // print command name of each process in job
|
||||
JOBS_PRINT_GROUP, // print group id of job
|
||||
JOBS_PRINT_NOTHING, // print nothing (exit status only)
|
||||
};
|
||||
|
||||
/// Calculates the cpu usage (as a fraction of 1) of the specified job.
|
||||
/// This may exceed 1 if there are multiple CPUs!
|
||||
static double cpu_use(const job_t *j) {
|
||||
double u = 0;
|
||||
for (const process_ptr_t &p : j->processes) {
|
||||
timepoint_t now = timef();
|
||||
clock_ticks_t jiffies = proc_get_jiffies(p->pid);
|
||||
double since = now - p->last_time;
|
||||
if (since > 0 && jiffies > p->last_jiffies) {
|
||||
u += clock_ticks_to_seconds(jiffies - p->last_jiffies) / since;
|
||||
}
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
/// Print information about the specified job.
|
||||
static void builtin_jobs_print(const job_t *j, int mode, int header, io_streams_t &streams) {
|
||||
int pgid = INVALID_PID;
|
||||
{
|
||||
auto job_pgid = j->get_pgid();
|
||||
if (job_pgid.has_value()) {
|
||||
pgid = *job_pgid;
|
||||
}
|
||||
}
|
||||
|
||||
wcstring out;
|
||||
switch (mode) {
|
||||
case JOBS_PRINT_NOTHING: {
|
||||
break;
|
||||
}
|
||||
case JOBS_DEFAULT: {
|
||||
if (header) {
|
||||
// Print table header before first job.
|
||||
out.append(_(L"Job\tGroup\t"));
|
||||
if (have_proc_stat()) {
|
||||
out.append(_(L"CPU\t"));
|
||||
}
|
||||
out.append(_(L"State\tCommand\n"));
|
||||
}
|
||||
|
||||
append_format(out, L"%d\t%d\t", j->job_id(), pgid);
|
||||
|
||||
if (have_proc_stat()) {
|
||||
append_format(out, L"%.0f%%\t", 100. * cpu_use(j));
|
||||
}
|
||||
|
||||
out.append(j->is_stopped() ? _(L"stopped") : _(L"running"));
|
||||
out.append(L"\t");
|
||||
|
||||
const wcstring cmd = escape_string(j->command(), ESCAPE_NO_PRINTABLES);
|
||||
out.append(cmd);
|
||||
|
||||
out.append(L"\n");
|
||||
streams.out.append(out);
|
||||
break;
|
||||
}
|
||||
case JOBS_PRINT_GROUP: {
|
||||
if (header) {
|
||||
// Print table header before first job.
|
||||
out.append(_(L"Group\n"));
|
||||
}
|
||||
append_format(out, L"%d\n", pgid);
|
||||
streams.out.append(out);
|
||||
break;
|
||||
}
|
||||
case JOBS_PRINT_PID: {
|
||||
if (header) {
|
||||
// Print table header before first job.
|
||||
out.append(_(L"Process\n"));
|
||||
}
|
||||
|
||||
for (const process_ptr_t &p : j->processes) {
|
||||
append_format(out, L"%d\n", p->pid);
|
||||
}
|
||||
streams.out.append(out);
|
||||
break;
|
||||
}
|
||||
case JOBS_PRINT_COMMAND: {
|
||||
if (header) {
|
||||
// Print table header before first job.
|
||||
out.append(_(L"Command\n"));
|
||||
}
|
||||
|
||||
for (const process_ptr_t &p : j->processes) {
|
||||
append_format(out, L"%ls\n", p->argv0());
|
||||
}
|
||||
streams.out.append(out);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The jobs builtin. Used for printing running jobs. Defined in builtin_jobs.c.
|
||||
maybe_t<int> builtin_jobs(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
bool found = false;
|
||||
int mode = JOBS_DEFAULT;
|
||||
bool print_last = false;
|
||||
|
||||
static const wchar_t *const short_options = L":cghlpq";
|
||||
static const struct woption long_options[] = {
|
||||
{L"command", no_argument, 'c'}, {L"group", no_argument, 'g'},
|
||||
{L"help", no_argument, 'h'}, {L"last", no_argument, 'l'},
|
||||
{L"pid", no_argument, 'p'}, {L"quiet", no_argument, 'q'},
|
||||
{L"query", no_argument, 'q'}, {}};
|
||||
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'p': {
|
||||
mode = JOBS_PRINT_PID;
|
||||
break;
|
||||
}
|
||||
case 'q': {
|
||||
mode = JOBS_PRINT_NOTHING;
|
||||
break;
|
||||
}
|
||||
case 'c': {
|
||||
mode = JOBS_PRINT_COMMAND;
|
||||
break;
|
||||
}
|
||||
case 'g': {
|
||||
mode = JOBS_PRINT_GROUP;
|
||||
break;
|
||||
}
|
||||
case 'l': {
|
||||
print_last = true;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (print_last) {
|
||||
// Ignore unconstructed jobs, i.e. ourself.
|
||||
for (const auto &j : parser.jobs()) {
|
||||
if (j->is_visible()) {
|
||||
builtin_jobs_print(j.get(), mode, !streams.out_is_redirected, streams);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
}
|
||||
return STATUS_CMD_ERROR;
|
||||
|
||||
} else {
|
||||
if (w.woptind < argc) {
|
||||
int i;
|
||||
|
||||
for (i = w.woptind; i < argc; i++) {
|
||||
const job_t *j = nullptr;
|
||||
|
||||
if (argv[i][0] == L'%') {
|
||||
int job_id = fish_wcstoi(argv[i] + 1);
|
||||
if (errno || job_id < 0) {
|
||||
streams.err.append_format(_(L"%ls: '%ls' is not a valid job id\n"), cmd,
|
||||
argv[i]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
j = parser.job_with_id(job_id);
|
||||
} else {
|
||||
int pid = fish_wcstoi(argv[i]);
|
||||
if (errno || pid < 0) {
|
||||
streams.err.append_format(_(L"%ls: '%ls' is not a valid process id\n"), cmd,
|
||||
argv[i]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
j = parser.job_get_from_pid(pid);
|
||||
}
|
||||
|
||||
if (j && !j->is_completed() && j->is_constructed()) {
|
||||
builtin_jobs_print(j, mode, false, streams);
|
||||
found = true;
|
||||
} else {
|
||||
if (mode != JOBS_PRINT_NOTHING) {
|
||||
streams.err.append_format(_(L"%ls: No suitable job: %ls\n"), cmd, argv[i]);
|
||||
}
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const auto &j : parser.jobs()) {
|
||||
// Ignore unconstructed jobs, i.e. ourself.
|
||||
if (j->is_visible()) {
|
||||
builtin_jobs_print(j.get(), mode, !found && !streams.out_is_redirected,
|
||||
streams);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// Do not babble if not interactive.
|
||||
if (!streams.out_is_redirected && mode != JOBS_PRINT_NOTHING) {
|
||||
streams.out.append_format(_(L"%ls: There are no jobs\n"), argv[0]);
|
||||
}
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Prototypes for functions for executing builtin_jobs functions.
|
||||
#ifndef FISH_BUILTIN_JOBS_H
|
||||
#define FISH_BUILTIN_JOBS_H
|
||||
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
maybe_t<int> builtin_jobs(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "../wcstringutil.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
#include "builtins/shared.rs.h"
|
||||
|
||||
namespace {
|
||||
struct read_cmd_opts_t {
|
||||
@@ -75,7 +76,8 @@ static const struct woption long_options[] = {{L"array", no_argument, 'a'},
|
||||
{}};
|
||||
|
||||
static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
int argc, const wchar_t **argv, const parser_t &parser,
|
||||
io_streams_t &streams) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
@@ -95,9 +97,10 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
break;
|
||||
}
|
||||
case 'i': {
|
||||
streams.err.append_format(_(L"%ls: usage of -i for --silent is deprecated. Please "
|
||||
L"use -s or --silent instead.\n"),
|
||||
cmd);
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: usage of -i for --silent is deprecated. Please "
|
||||
L"use -s or --silent instead.\n"),
|
||||
cmd));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case L'f': {
|
||||
@@ -124,14 +127,14 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
opts.nchars = fish_wcstoi(w.woptarg);
|
||||
if (errno) {
|
||||
if (errno == ERANGE) {
|
||||
streams.err.append_format(_(L"%ls: Argument '%ls' is out of range\n"), cmd,
|
||||
w.woptarg);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(
|
||||
_(L"%ls: Argument '%ls' is out of range\n"), cmd, w.woptarg));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
break;
|
||||
@@ -177,11 +180,11 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
break;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case L'?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
@@ -196,8 +199,8 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc
|
||||
|
||||
/// Read from the tty. This is only valid when the stream is stdin and it is attached to a tty and
|
||||
/// we weren't asked to split on null characters.
|
||||
static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool shell, bool silent,
|
||||
const wchar_t *prompt, const wchar_t *right_prompt,
|
||||
static int read_interactive(const parser_t &parser, wcstring &buff, int nchars, bool shell,
|
||||
bool silent, const wchar_t *prompt, const wchar_t *right_prompt,
|
||||
const wchar_t *commandline, int in) {
|
||||
int exit_res = STATUS_CMD_OK;
|
||||
|
||||
@@ -224,7 +227,7 @@ static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool s
|
||||
reader_push(parser, wcstring{}, std::move(conf));
|
||||
|
||||
commandline_set_buffer(commandline, std::wcslen(commandline));
|
||||
scoped_push<bool> interactive{&parser.libdata().is_interactive, true};
|
||||
scoped_push<bool> interactive{&parser.libdata_pods_mut().is_interactive, true};
|
||||
|
||||
auto mline = reader_readline(nchars);
|
||||
interactive.restore();
|
||||
@@ -352,22 +355,24 @@ static int read_one_char_at_a_time(int fd, wcstring &buff, int nchars, bool spli
|
||||
|
||||
/// Validate the arguments given to `read` and provide defaults where needed.
|
||||
static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int argc,
|
||||
const wchar_t *const *argv, parser_t &parser, io_streams_t &streams) {
|
||||
const wchar_t *const *argv, const parser_t &parser,
|
||||
io_streams_t &streams) {
|
||||
if (opts.prompt && opts.prompt_str) {
|
||||
streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
||||
L"-p", L"-P");
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(
|
||||
_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, L"-p", L"-P"));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (opts.have_delimiter && opts.one_line) {
|
||||
streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
||||
L"--delimiter", L"--line");
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
||||
L"--delimiter", L"--line"));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
if (opts.one_line && opts.split_null) {
|
||||
streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd,
|
||||
L"-z", L"--line");
|
||||
streams.err()->append(format_string(
|
||||
_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, L"-z", L"--line"));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
@@ -379,55 +384,57 @@ static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int arg
|
||||
}
|
||||
|
||||
if ((opts.place & ENV_UNEXPORT) && (opts.place & ENV_EXPORT)) {
|
||||
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_EXPUNEXP, cmd));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if ((opts.place & ENV_LOCAL ? 1 : 0) + (opts.place & ENV_FUNCTION ? 1 : 0) +
|
||||
(opts.place & ENV_GLOBAL ? 1 : 0) + (opts.place & ENV_UNIVERSAL ? 1 : 0) >
|
||||
1) {
|
||||
streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_GLOCAL, cmd));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (!opts.array && argc < 1 && !opts.to_stdout) {
|
||||
streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, argc);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, argc));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (opts.array && argc != 1) {
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (opts.to_stdout && argc > 0) {
|
||||
streams.err.append_format(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, 0, argc);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, 0, argc));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (opts.tokenize && opts.have_delimiter) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--delimiter", L"--tokenize");
|
||||
streams.err()->append(
|
||||
format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--delimiter", L"--tokenize"));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (opts.tokenize && opts.one_line) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--line", L"--tokenize");
|
||||
streams.err()->append(
|
||||
format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--line", L"--tokenize"));
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Verify all variable names.
|
||||
for (int i = 0; i < argc; i++) {
|
||||
if (!valid_var_name(argv[i])) {
|
||||
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, argv[i]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_VARNAME, cmd, argv[i]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
if (env_var_t::flags_for(argv[i]) & env_var_t::flag_read_only) {
|
||||
streams.err.append_format(_(L"%ls: %ls: cannot overwrite read-only variable"), cmd,
|
||||
argv[i]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
if (env_flags_for(argv[i]) & env_var_flag_read_only) {
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: %ls: cannot overwrite read-only variable"), cmd, argv[i]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
@@ -436,9 +443,12 @@ 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.
|
||||
maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int builtin_read(const void *_parser, void *_streams, void *_argv) {
|
||||
const auto &parser = *static_cast<const parser_t *>(_parser);
|
||||
auto &streams = *static_cast<io_streams_t *>(_streams);
|
||||
auto argv = static_cast<const wchar_t **>(_argv);
|
||||
int argc = builtin_count_args(argv);
|
||||
const wchar_t *cmd = argv[0];
|
||||
wcstring buff;
|
||||
int exit_res = STATUS_CMD_OK;
|
||||
read_cmd_opts_t opts;
|
||||
@@ -464,8 +474,8 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
// stdin may have been explicitly closed
|
||||
if (streams.stdin_fd < 0) {
|
||||
streams.err.append_format(_(L"%ls: stdin is closed\n"), cmd);
|
||||
if (streams.stdin_fd() < 0) {
|
||||
streams.err()->append(format_string(_(L"%ls: stdin is closed\n"), cmd));
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
@@ -481,7 +491,7 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
auto vars_left = [&]() { return argv + argc - var_ptr; };
|
||||
auto clear_remaining_vars = [&]() {
|
||||
while (vars_left()) {
|
||||
parser.vars().set_empty(*var_ptr, opts.place);
|
||||
parser.vars().set(*var_ptr, opts.place, std::vector<wcstring>{});
|
||||
++var_ptr;
|
||||
}
|
||||
};
|
||||
@@ -492,19 +502,19 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
do {
|
||||
buff.clear();
|
||||
|
||||
int stream_stdin_is_a_tty = isatty(streams.stdin_fd);
|
||||
int stream_stdin_is_a_tty = isatty(streams.stdin_fd());
|
||||
if (stream_stdin_is_a_tty && !opts.split_null) {
|
||||
// Read interactively using reader_readline(). This does not support splitting on null.
|
||||
exit_res =
|
||||
read_interactive(parser, buff, opts.nchars, opts.shell, opts.silent, opts.prompt,
|
||||
opts.right_prompt, opts.commandline, streams.stdin_fd);
|
||||
opts.right_prompt, opts.commandline, streams.stdin_fd());
|
||||
} else if (!opts.nchars && !stream_stdin_is_a_tty &&
|
||||
// "one_line" is implemented as reading n-times to a new line,
|
||||
// if we're chunking we could get multiple lines so we would have to advance
|
||||
// more than 1 per run through the loop. Let's skip that for now.
|
||||
!opts.one_line &&
|
||||
(streams.stdin_is_directly_redirected ||
|
||||
lseek(streams.stdin_fd, 0, SEEK_CUR) != -1)) {
|
||||
(streams.stdin_is_directly_redirected() ||
|
||||
lseek(streams.stdin_fd(), 0, SEEK_CUR) != -1)) {
|
||||
// We read in chunks when we either can seek (so we put the bytes back),
|
||||
// or we have the bytes to ourselves (because it's directly redirected).
|
||||
//
|
||||
@@ -512,11 +522,11 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
// under the assumption that the stream will be closed soon anyway.
|
||||
// You don't rewind VHS tapes before throwing them in the trash.
|
||||
// TODO: Do this when nchars is set by seeking back.
|
||||
exit_res = read_in_chunks(streams.stdin_fd, buff, opts.split_null,
|
||||
!streams.stdin_is_directly_redirected);
|
||||
exit_res = read_in_chunks(streams.stdin_fd(), buff, opts.split_null,
|
||||
!streams.stdin_is_directly_redirected());
|
||||
} else {
|
||||
exit_res =
|
||||
read_one_char_at_a_time(streams.stdin_fd, buff, opts.nchars, opts.split_null);
|
||||
read_one_char_at_a_time(streams.stdin_fd(), buff, opts.nchars, opts.split_null);
|
||||
}
|
||||
|
||||
if (exit_res != STATUS_CMD_OK) {
|
||||
@@ -525,7 +535,7 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
}
|
||||
|
||||
if (opts.to_stdout) {
|
||||
streams.out.append(buff);
|
||||
streams.out()->append(buff);
|
||||
return exit_res;
|
||||
}
|
||||
|
||||
@@ -536,7 +546,8 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
std::vector<wcstring> tokens;
|
||||
while (auto t = tok->next()) {
|
||||
auto text = *tok->text_of(*t);
|
||||
if (auto out = unescape_string(text, UNESCAPE_DEFAULT)) {
|
||||
if (auto out = unescape_string(text.c_str(), text.length(), UNESCAPE_DEFAULT,
|
||||
STRING_STYLE_SCRIPT)) {
|
||||
tokens.push_back(*out);
|
||||
} else {
|
||||
tokens.push_back(text);
|
||||
@@ -548,17 +559,21 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
std::unique_ptr<tok_t> t;
|
||||
while ((vars_left() - 1 > 0) && (t = tok->next())) {
|
||||
auto text = *tok->text_of(*t);
|
||||
if (auto out = unescape_string(text, UNESCAPE_DEFAULT)) {
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, *out);
|
||||
if (auto out = unescape_string(text.c_str(), text.length(), UNESCAPE_DEFAULT,
|
||||
STRING_STYLE_SCRIPT)) {
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place,
|
||||
std::vector<wcstring>{*out});
|
||||
} else {
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, text);
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place,
|
||||
std::vector<wcstring>{text});
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have tokens, set the last variable to them.
|
||||
if ((t = tok->next())) {
|
||||
wcstring rest = wcstring(buff, t->offset);
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, std::move(rest));
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place,
|
||||
std::vector<wcstring>{std::move(rest)});
|
||||
}
|
||||
}
|
||||
// The rest of the loop is other split-modes, we don't care about those.
|
||||
@@ -567,7 +582,7 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
|
||||
if (!opts.have_delimiter) {
|
||||
auto ifs = parser.vars().get_unless_empty(L"IFS");
|
||||
if (ifs) opts.delimiter = ifs->as_string();
|
||||
if (ifs) opts.delimiter = *ifs->as_string();
|
||||
}
|
||||
|
||||
if (opts.delimiter.empty()) {
|
||||
@@ -596,7 +611,7 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
// Not array mode: assign each char to a separate var with the remainder being
|
||||
// assigned to the last var.
|
||||
for (const auto &c : chars) {
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, c);
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, std::vector<wcstring>{c});
|
||||
}
|
||||
}
|
||||
} else if (opts.array) {
|
||||
@@ -630,7 +645,8 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
if (val_idx < var_vals.size()) {
|
||||
val = std::move(var_vals.at(val_idx++));
|
||||
}
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, std::move(val));
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place,
|
||||
std::vector<wcstring>{std::move(val)});
|
||||
}
|
||||
} else {
|
||||
// We're using a delimiter provided by the user so use the `string split` behavior.
|
||||
@@ -641,7 +657,7 @@ maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t
|
||||
&splits, argc - 1);
|
||||
assert(splits.size() <= static_cast<size_t>(vars_left()));
|
||||
for (const auto &split : splits) {
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, split);
|
||||
parser.set_var_and_fire(*var_ptr++, opts.place, std::vector<wcstring>{split});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
struct Parser;
|
||||
struct IoStreams;
|
||||
using parser_t = Parser;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
int builtin_read(const void *parser, void *streams, void *argv);
|
||||
#endif
|
||||
|
||||
@@ -1,850 +0,0 @@
|
||||
// Functions used for implementing the set builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "set.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cwchar>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../env.h"
|
||||
#include "../event.h"
|
||||
#include "../expand.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../history.h"
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../parser.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
struct set_cmd_opts_t {
|
||||
bool print_help = false;
|
||||
bool show = false;
|
||||
bool local = false;
|
||||
bool function = false;
|
||||
bool global = false;
|
||||
bool exportv = false;
|
||||
bool erase = false;
|
||||
bool list = false;
|
||||
bool unexport = false;
|
||||
bool pathvar = false;
|
||||
bool unpathvar = false;
|
||||
bool universal = false;
|
||||
bool query = false;
|
||||
bool shorten_ok = true;
|
||||
bool append = false;
|
||||
bool prepend = false;
|
||||
bool preserve_failure_exit_status = true;
|
||||
};
|
||||
|
||||
/// Values used for long-only options.
|
||||
enum {
|
||||
opt_path = 1,
|
||||
opt_unpath = 2,
|
||||
};
|
||||
|
||||
// Variables used for parsing the argument list. This command is atypical in using the "+"
|
||||
// (REQUIRE_ORDER) option for flag parsing. This is not typical of most fish commands. It means
|
||||
// we stop scanning for flags when the first non-flag argument is seen.
|
||||
static const wchar_t *const short_options = L"+:LSUaefghlnpqux";
|
||||
static const struct woption long_options[] = {{L"export", no_argument, 'x'},
|
||||
{L"global", no_argument, 'g'},
|
||||
{L"function", no_argument, 'f'},
|
||||
{L"local", no_argument, 'l'},
|
||||
{L"erase", no_argument, 'e'},
|
||||
{L"names", no_argument, 'n'},
|
||||
{L"unexport", no_argument, 'u'},
|
||||
{L"universal", no_argument, 'U'},
|
||||
{L"long", no_argument, 'L'},
|
||||
{L"query", no_argument, 'q'},
|
||||
{L"show", no_argument, 'S'},
|
||||
{L"append", no_argument, 'a'},
|
||||
{L"prepend", no_argument, 'p'},
|
||||
{L"path", no_argument, opt_path},
|
||||
{L"unpath", no_argument, opt_unpath},
|
||||
{L"help", no_argument, 'h'},
|
||||
{}};
|
||||
|
||||
// Hint for invalid path operation with a colon.
|
||||
#define BUILTIN_SET_MISMATCHED_ARGS _(L"%ls: given %d indexes but %d values\n")
|
||||
#define BUILTIN_SET_ARRAY_BOUNDS_ERR _(L"%ls: array index out of bounds\n")
|
||||
#define BUILTIN_SET_UVAR_ERR \
|
||||
_(L"%ls: successfully set universal '%ls'; but a global by that name shadows it\n")
|
||||
|
||||
static int parse_cmd_opts(set_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
|
||||
int opt;
|
||||
wgetopter_t w;
|
||||
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'a': {
|
||||
opts.append = true;
|
||||
break;
|
||||
}
|
||||
case 'e': {
|
||||
opts.erase = true;
|
||||
opts.preserve_failure_exit_status = false;
|
||||
break;
|
||||
}
|
||||
case 'f': {
|
||||
opts.function = true;
|
||||
break;
|
||||
}
|
||||
case 'g': {
|
||||
opts.global = true;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
opts.print_help = true;
|
||||
break;
|
||||
}
|
||||
case 'l': {
|
||||
opts.local = true;
|
||||
break;
|
||||
}
|
||||
case 'n': {
|
||||
opts.list = true;
|
||||
opts.preserve_failure_exit_status = false;
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
opts.prepend = true;
|
||||
break;
|
||||
}
|
||||
case 'q': {
|
||||
opts.query = true;
|
||||
opts.preserve_failure_exit_status = false;
|
||||
break;
|
||||
}
|
||||
case 'x': {
|
||||
opts.exportv = true;
|
||||
break;
|
||||
}
|
||||
case 'u': {
|
||||
opts.unexport = true;
|
||||
break;
|
||||
}
|
||||
case opt_path: {
|
||||
opts.pathvar = true;
|
||||
break;
|
||||
}
|
||||
case opt_unpath: {
|
||||
opts.unpathvar = true;
|
||||
break;
|
||||
}
|
||||
case 'U': {
|
||||
opts.universal = true;
|
||||
break;
|
||||
}
|
||||
case 'L': {
|
||||
opts.shorten_ok = false;
|
||||
break;
|
||||
}
|
||||
case 'S': {
|
||||
opts.show = true;
|
||||
opts.preserve_failure_exit_status = false;
|
||||
break;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
// Specifically detect `set -o` because people might be bringing over bashisms.
|
||||
if (wcsncmp(argv[w.woptind - 1], L"-o", 2) == 0) {
|
||||
streams.err.append(
|
||||
L"Fish does not have shell options. See `help fish-for-bash-users`.\n");
|
||||
if (w.woptind < argc) {
|
||||
if (wcscmp(argv[w.woptind], L"vi") == 0) {
|
||||
// Tell the vi users how to get what they need.
|
||||
streams.err.append(L"To enable vi-mode, run `fish_vi_key_bindings`.\n");
|
||||
} else if (wcscmp(argv[w.woptind], L"ed") == 0) {
|
||||
// This should be enough for make ed users feel at home
|
||||
streams.err.append(L"?\n?\n?\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected retval from wgetopt_long");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*optind = w.woptind;
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
static int validate_cmd_opts(const wchar_t *cmd, const set_cmd_opts_t &opts, int argc,
|
||||
const wchar_t *argv[], parser_t &parser, io_streams_t &streams) {
|
||||
// Can't query and erase or list.
|
||||
if (opts.query && (opts.erase || opts.list)) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// We can't both list and erase variables.
|
||||
if (opts.erase && opts.list) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Variables can only have one scope...
|
||||
if (opts.local + opts.function + opts.global + opts.universal > 1) {
|
||||
// ..unless we are erasing a variable, in which case we can erase from several in one go.
|
||||
if (!opts.erase) {
|
||||
streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
|
||||
// Variables can only have one export status.
|
||||
if (opts.exportv && opts.unexport) {
|
||||
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Variables can only have one path status.
|
||||
if (opts.pathvar && opts.unpathvar) {
|
||||
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Trying to erase and (un)export at the same time doesn't make sense.
|
||||
if (opts.erase && (opts.exportv || opts.unexport)) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// The --show flag cannot be combined with any other flag.
|
||||
if (opts.show && (opts.local || opts.function || opts.global || opts.erase || opts.list ||
|
||||
opts.exportv || opts.universal)) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (argc == 0 && opts.erase) {
|
||||
streams.err.append_format(BUILTIN_ERR_MISSING, cmd, argv[-1]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// 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 void warn_if_uvar_shadows_global(const wchar_t *cmd, const set_cmd_opts_t &opts,
|
||||
const wcstring &dest, io_streams_t &streams,
|
||||
const parser_t &parser) {
|
||||
if (opts.universal && parser.is_interactive()) {
|
||||
if (parser.vars().get(dest, ENV_GLOBAL).has_value()) {
|
||||
streams.err.append_format(BUILTIN_SET_UVAR_ERR, cmd, dest.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_env_return(int retval, const wchar_t *cmd, const wcstring &key,
|
||||
io_streams_t &streams) {
|
||||
switch (retval) {
|
||||
case ENV_OK: {
|
||||
break;
|
||||
}
|
||||
case ENV_PERM: {
|
||||
streams.err.append_format(_(L"%ls: Tried to change the read-only variable '%ls'\n"),
|
||||
cmd, key.c_str());
|
||||
break;
|
||||
}
|
||||
case ENV_SCOPE: {
|
||||
streams.err.append_format(
|
||||
_(L"%ls: Tried to modify the special variable '%ls' with the wrong scope\n"), cmd,
|
||||
key.c_str());
|
||||
break;
|
||||
}
|
||||
case ENV_INVALID: {
|
||||
streams.err.append_format(
|
||||
_(L"%ls: Tried to modify the special variable '%ls' to an invalid value\n"), cmd,
|
||||
key.c_str());
|
||||
break;
|
||||
}
|
||||
case ENV_NOT_FOUND: {
|
||||
streams.err.append_format(_(L"%ls: The variable '%ls' does not exist\n"), cmd,
|
||||
key.c_str());
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected vars.set() ret val");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 wcstring &key, int scope,
|
||||
std::vector<wcstring> list, io_streams_t &streams,
|
||||
parser_t &parser) {
|
||||
int retval = parser.set_var_and_fire(key, scope | ENV_USER, std::move(list));
|
||||
// If this returned OK, the parser already fired the event.
|
||||
handle_env_return(retval, cmd, key, streams);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
namespace {
|
||||
/// A helper type returned by split_var_and_indexes.
|
||||
struct split_var_t {
|
||||
wcstring varname; // name of the variable
|
||||
maybe_t<env_var_t> var{}; // value of the variable, or none if missing
|
||||
std::vector<long> indexes{}; // list of requested indexes
|
||||
|
||||
/// \return the number of elements in our variable, or 0 if missing.
|
||||
long varsize() const { return var ? static_cast<long>(var->as_list().size()) : 0L; }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
/// Extract indexes from an argument of the form `var_name[index1 index2...]`.
|
||||
/// The argument \p arg is split into a variable name and list of indexes, which is returned by
|
||||
/// reference. Indexes are "expanded" in the sense that range expressions .. and negative values are
|
||||
/// handled.
|
||||
///
|
||||
/// Returns:
|
||||
/// a split var on success, none() on error, in which case an error will have been printed.
|
||||
/// If no index is found, this leaves indexes empty.
|
||||
static maybe_t<split_var_t> split_var_and_indexes(const wchar_t *arg, env_mode_flags_t mode,
|
||||
const environment_t &vars,
|
||||
io_streams_t &streams) {
|
||||
split_var_t res{};
|
||||
wcstring argstr{arg};
|
||||
auto open_bracket = argstr.find(L'[');
|
||||
res.varname.assign(arg, open_bracket == wcstring::npos ? argstr.size() : open_bracket);
|
||||
res.var = vars.get(res.varname, mode);
|
||||
if (open_bracket == wcstring::npos) {
|
||||
// Common case of no bracket.
|
||||
return res;
|
||||
}
|
||||
|
||||
long varsize = res.varsize();
|
||||
const wchar_t *p = arg + open_bracket + 1;
|
||||
while (*p != L']') {
|
||||
const wchar_t *end;
|
||||
long l_ind;
|
||||
if (res.indexes.empty() && p[0] == L'.' && p[1] == L'.') {
|
||||
// If we are at the first index expression, a missing start-index means the range starts
|
||||
// at the first item.
|
||||
l_ind = 1; // first index
|
||||
} else {
|
||||
const wchar_t *end = nullptr;
|
||||
l_ind = fish_wcstol(p, &end);
|
||||
if (errno > 0) { // ignore errno == -1 meaning the int did not end with a '\0'
|
||||
streams.err.append_format(_(L"%ls: Invalid index starting at '%ls'\n"), L"set",
|
||||
res.varname.c_str());
|
||||
return none();
|
||||
}
|
||||
p = end;
|
||||
}
|
||||
|
||||
// Convert negative index to a positive index.
|
||||
if (l_ind < 0) l_ind = varsize + l_ind + 1;
|
||||
|
||||
if (p[0] == L'.' && p[1] == L'.') {
|
||||
p += 2;
|
||||
long l_ind2;
|
||||
// If we are at the last index range expression, a missing end-index means the range
|
||||
// spans until the last item.
|
||||
if (res.indexes.empty() && *p == L']') {
|
||||
l_ind2 = -1;
|
||||
} else {
|
||||
l_ind2 = fish_wcstol(p, &end);
|
||||
if (errno > 0) { // ignore errno == -1 meaning there was text after the int
|
||||
return none();
|
||||
}
|
||||
p = end;
|
||||
}
|
||||
|
||||
// Convert negative index to a positive index.
|
||||
if (l_ind2 < 0) l_ind2 = varsize + l_ind2 + 1;
|
||||
|
||||
int direction = l_ind2 < l_ind ? -1 : 1;
|
||||
for (long jjj = l_ind; jjj * direction <= l_ind2 * direction; jjj += direction) {
|
||||
res.indexes.push_back(jjj);
|
||||
}
|
||||
} else {
|
||||
res.indexes.push_back(l_ind);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Given a list of values and 1-based indexes, return a new list with those elements removed.
|
||||
/// Note this deliberately accepts both args by value, as it modifies them both.
|
||||
static std::vector<wcstring> erased_at_indexes(std::vector<wcstring> input,
|
||||
std::vector<long> indexes) {
|
||||
// Sort our indexes into *descending* order.
|
||||
std::sort(indexes.begin(), indexes.end(), std::greater<long>());
|
||||
|
||||
// Remove duplicates.
|
||||
indexes.erase(std::unique(indexes.begin(), indexes.end()), indexes.end());
|
||||
|
||||
// Now when we walk indexes, we encounter larger indexes first.
|
||||
for (long idx : indexes) {
|
||||
if (idx > 0 && static_cast<size_t>(idx) <= input.size()) {
|
||||
// One-based indexing!
|
||||
input.erase(input.begin() + idx - 1);
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
static env_mode_flags_t compute_scope(const set_cmd_opts_t &opts) {
|
||||
int scope = ENV_USER;
|
||||
if (opts.local) scope |= ENV_LOCAL;
|
||||
if (opts.function) scope |= ENV_FUNCTION;
|
||||
if (opts.global) scope |= ENV_GLOBAL;
|
||||
if (opts.exportv) scope |= ENV_EXPORT;
|
||||
if (opts.unexport) scope |= ENV_UNEXPORT;
|
||||
if (opts.universal) scope |= ENV_UNIVERSAL;
|
||||
if (opts.pathvar) scope |= ENV_PATHVAR;
|
||||
if (opts.unpathvar) scope |= ENV_UNPATHVAR;
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// Print the names of all environment variables in the scope. It will include the values unless the
|
||||
/// `set --names` flag was used.
|
||||
static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
|
||||
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
UNUSED(cmd);
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(parser);
|
||||
|
||||
bool names_only = opts.list;
|
||||
std::vector<wcstring> names = parser.vars().get_names(compute_scope(opts));
|
||||
sort(names.begin(), names.end());
|
||||
|
||||
for (const auto &key : names) {
|
||||
wcstring out;
|
||||
out.append(key);
|
||||
|
||||
if (!names_only) {
|
||||
wcstring val;
|
||||
if (opts.shorten_ok && key == L"history") {
|
||||
std::shared_ptr<history_t> history =
|
||||
history_t::with_name(history_session_id(parser.vars()));
|
||||
for (size_t i = 1; i < history->size() && val.size() < 64; i++) {
|
||||
if (i > 1) val += L' ';
|
||||
val += expand_escape_string(history->item_at_index(i).str());
|
||||
}
|
||||
} else {
|
||||
auto var = parser.vars().get_unless_empty(key, compute_scope(opts));
|
||||
if (var) {
|
||||
val = expand_escape_variable(*var);
|
||||
}
|
||||
}
|
||||
if (!val.empty()) {
|
||||
bool shorten = false;
|
||||
if (opts.shorten_ok && val.length() > 64) {
|
||||
shorten = true;
|
||||
val.resize(60);
|
||||
}
|
||||
out.push_back(L' ');
|
||||
out.append(val);
|
||||
|
||||
if (shorten) out.push_back(get_ellipsis_char());
|
||||
}
|
||||
}
|
||||
|
||||
out.push_back(L'\n');
|
||||
streams.out.append(out);
|
||||
}
|
||||
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// Query mode. Return the number of variables that do NOT exist out of the specified variables.
|
||||
static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
|
||||
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
int retval = 0;
|
||||
env_mode_flags_t scope = compute_scope(opts);
|
||||
|
||||
// No variables given, this is an error.
|
||||
// 255 is the maximum return code we allow.
|
||||
if (argc == 0) return 255;
|
||||
|
||||
for (int i = 0; i < argc; i++) {
|
||||
auto split = split_var_and_indexes(argv[i], scope, parser.vars(), streams);
|
||||
if (!split) {
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
if (split->indexes.empty()) {
|
||||
// No indexes, just increment if our variable is missing.
|
||||
if (!split->var) retval++;
|
||||
} else {
|
||||
// Increment for every index out of range.
|
||||
long varsize = split->varsize();
|
||||
for (long idx : split->indexes) {
|
||||
if (idx < 1 || idx > varsize) retval++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
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: {
|
||||
scope_name = L"local";
|
||||
break;
|
||||
}
|
||||
case ENV_GLOBAL: {
|
||||
scope_name = L"global";
|
||||
break;
|
||||
}
|
||||
case ENV_UNIVERSAL: {
|
||||
scope_name = L"universal";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("invalid scope");
|
||||
}
|
||||
}
|
||||
|
||||
const auto var = vars.get(var_name, scope);
|
||||
if (!var) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wchar_t *exportv = var->exports() ? _(L"exported") : _(L"unexported");
|
||||
const wchar_t *pathvarv = var->is_pathvar() ? _(L" a path variable") : L"";
|
||||
std::vector<wcstring> vals = var->as_list();
|
||||
streams.out.append_format(_(L"$%ls: set in %ls scope, %ls,%ls with %d elements"), var_name,
|
||||
scope_name, exportv, pathvarv, vals.size());
|
||||
// HACK: PWD can be set, depending on how you ask.
|
||||
// For our purposes it's read-only.
|
||||
if (env_var_t::flags_for(var_name) & env_var_t::flag_read_only) {
|
||||
streams.out.append(_(L" (read-only)\n"));
|
||||
} else
|
||||
streams.out.push(L'\n');
|
||||
|
||||
for (size_t i = 0; i < vals.size(); i++) {
|
||||
if (vals.size() > 100) {
|
||||
if (i == 50) {
|
||||
// try to print a mid-line ellipsis because we are eliding lines not words
|
||||
streams.out.append(get_ellipsis_char() > 256 ? L"\u22EF" : get_ellipsis_str());
|
||||
streams.out.push(L'\n');
|
||||
}
|
||||
if (i >= 50 && i < vals.size() - 50) continue;
|
||||
}
|
||||
const wcstring value = vals[i];
|
||||
const wcstring escaped_val = escape_string(value, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED);
|
||||
streams.out.append_format(_(L"$%ls[%d]: |%ls|\n"), var_name, i + 1, escaped_val.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Show mode. Show information about the named variable(s).
|
||||
static int builtin_set_show(const wchar_t *cmd, const set_cmd_opts_t &opts, int argc,
|
||||
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
UNUSED(opts);
|
||||
const auto &vars = parser.vars();
|
||||
auto inheriteds = env_get_inherited();
|
||||
if (argc == 0) { // show all vars
|
||||
std::vector<wcstring> names = vars.get_names(ENV_USER);
|
||||
sort(names.begin(), names.end());
|
||||
for (const auto &name : names) {
|
||||
if (name == L"history") continue;
|
||||
show_scope(name.c_str(), ENV_LOCAL, streams, vars);
|
||||
show_scope(name.c_str(), ENV_GLOBAL, streams, vars);
|
||||
show_scope(name.c_str(), ENV_UNIVERSAL, streams, vars);
|
||||
|
||||
// Show the originally imported value as a debugging aid.
|
||||
auto inherited = inheriteds.find(name);
|
||||
if (inherited != inheriteds.end()) {
|
||||
const wcstring escaped_val =
|
||||
escape_string(inherited->second, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED);
|
||||
streams.out.append_format(_(L"$%ls: originally inherited as |%ls|\n"), name.c_str(),
|
||||
escaped_val.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const wchar_t *arg = argv[i];
|
||||
|
||||
if (!valid_var_name(arg)) {
|
||||
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, arg);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
if (std::wcschr(arg, L'[')) {
|
||||
streams.err.append_format(
|
||||
_(L"%ls: `set --show` does not allow slices with the var names\n"), cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
show_scope(arg, ENV_LOCAL, streams, vars);
|
||||
show_scope(arg, ENV_GLOBAL, streams, vars);
|
||||
show_scope(arg, ENV_UNIVERSAL, streams, vars);
|
||||
auto inherited = inheriteds.find(arg);
|
||||
if (inherited != inheriteds.end()) {
|
||||
const wcstring escaped_val =
|
||||
escape_string(inherited->second, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED);
|
||||
streams.out.append_format(_(L"$%ls: originally inherited as |%ls|\n"), arg,
|
||||
escaped_val.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
/// Erase a variable.
|
||||
static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
|
||||
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
int ret = STATUS_CMD_OK;
|
||||
env_mode_flags_t scopes = compute_scope(opts);
|
||||
// `set -e` is allowed to be called with multiple scopes.
|
||||
for (int bit = 0; 1 << bit <= ENV_USER; ++bit) {
|
||||
int scope = scopes & 1 << bit;
|
||||
if (scope == 0 || (scope == ENV_USER && scopes != ENV_USER)) continue;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
auto split = split_var_and_indexes(argv[i], scope, parser.vars(), streams);
|
||||
if (!split) {
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
if (!valid_var_name(split->varname)) {
|
||||
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, split->varname.c_str());
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
int retval = STATUS_CMD_OK;
|
||||
if (split->indexes.empty()) { // unset the var
|
||||
retval = parser.vars().remove(split->varname, scope);
|
||||
// When a non-existent-variable is unset, return ENV_NOT_FOUND as $status
|
||||
// but do not emit any errors at the console as a compromise between user
|
||||
// friendliness and correctness.
|
||||
if (retval != ENV_NOT_FOUND) {
|
||||
handle_env_return(retval, cmd, split->varname, streams);
|
||||
}
|
||||
if (retval == ENV_OK) {
|
||||
event_fire(parser, *new_event_variable_erase(split->varname));
|
||||
}
|
||||
} else { // remove just the specified indexes of the var
|
||||
if (!split->var) return STATUS_CMD_ERROR;
|
||||
std::vector<wcstring> result =
|
||||
erased_at_indexes(split->var->as_list(), split->indexes);
|
||||
retval = env_set_reporting_errors(cmd, split->varname, scope, std::move(result),
|
||||
streams, parser);
|
||||
}
|
||||
|
||||
// Set $status to the last error value.
|
||||
// This is cheesy, but I don't expect this to be checked often.
|
||||
if (retval != STATUS_CMD_OK) {
|
||||
ret = retval;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Return a list of new values for the variable \p varname, respecting the \p opts.
|
||||
/// The arguments are given as the argc, argv pair.
|
||||
/// This handles the simple case where there are no indexes.
|
||||
static std::vector<wcstring> new_var_values(const wcstring &varname, const set_cmd_opts_t &opts,
|
||||
int argc, const wchar_t *const *argv,
|
||||
const environment_t &vars) {
|
||||
std::vector<wcstring> result;
|
||||
if (!opts.prepend && !opts.append) {
|
||||
// Not prepending or appending.
|
||||
result.assign(argv, argv + argc);
|
||||
} else {
|
||||
// Note: when prepending or appending, we always use default scoping when fetching existing
|
||||
// values. For example:
|
||||
// set --global var 1 2
|
||||
// set --local --append var 3 4
|
||||
// This starts with the existing global variable, appends to it, and sets it locally.
|
||||
// So do not use the given variable: we must re-fetch it.
|
||||
// TODO: this races under concurrent execution.
|
||||
if (auto existing = vars.get(varname, ENV_DEFAULT)) {
|
||||
result = existing->as_list();
|
||||
}
|
||||
|
||||
if (opts.prepend) {
|
||||
result.insert(result.begin(), argv, argv + argc);
|
||||
}
|
||||
|
||||
if (opts.append) {
|
||||
result.insert(result.end(), argv, argv + argc);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// This handles the more difficult case of setting individual slices of a var.
|
||||
static std::vector<wcstring> new_var_values_by_index(const split_var_t &split, int argc,
|
||||
const wchar_t *const *argv) {
|
||||
assert(static_cast<size_t>(argc) == split.indexes.size() &&
|
||||
"Must have the same number of indexes as arguments");
|
||||
|
||||
// Inherit any existing values.
|
||||
// Note unlike the append/prepend case, we start with a variable in the same scope as we are
|
||||
// setting.
|
||||
std::vector<wcstring> result;
|
||||
if (split.var) result = split.var->as_list();
|
||||
|
||||
// For each (index, argument) pair, set the element in our \p result to the replacement string.
|
||||
// Extend the list with empty strings as needed. The indexes are 1-based.
|
||||
for (int i = 0; i < argc; i++) {
|
||||
long lidx = split.indexes.at(i);
|
||||
assert(lidx >= 1 && "idx should have been verified in range already");
|
||||
// Convert from 1-based to 0-based.
|
||||
auto idx = static_cast<size_t>(lidx - 1);
|
||||
// Extend as needed with empty strings.
|
||||
if (idx >= result.size()) result.resize(idx + 1);
|
||||
result.at(idx) = argv[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Set a variable.
|
||||
static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, const wchar_t **argv,
|
||||
parser_t &parser, io_streams_t &streams) {
|
||||
if (argc == 0) {
|
||||
streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
env_mode_flags_t scope = compute_scope(opts);
|
||||
const wchar_t *var_expr = argv[0];
|
||||
argv++;
|
||||
argc--;
|
||||
|
||||
auto split = split_var_and_indexes(var_expr, scope, parser.vars(), streams);
|
||||
if (!split) {
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Is the variable valid?
|
||||
if (!valid_var_name(split->varname)) {
|
||||
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, split->varname.c_str());
|
||||
auto pos = split->varname.find(L'=');
|
||||
if (pos != wcstring::npos) {
|
||||
streams.err.append_format(L"%ls: Did you mean `set %ls %ls`?", cmd,
|
||||
escape_string(split->varname.substr(0, pos)).c_str(),
|
||||
escape_string(split->varname.substr(pos + 1)).c_str());
|
||||
}
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Setting with explicit indexes like `set foo[3] ...` has additional error handling.
|
||||
if (!split->indexes.empty()) {
|
||||
// Indexes must be > 0. (Note split_var_and_indexes negates negative values).
|
||||
for (long v : split->indexes) {
|
||||
if (v <= 0) {
|
||||
streams.err.append_format(BUILTIN_SET_ARRAY_BOUNDS_ERR, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
|
||||
// Append and prepend are disallowed.
|
||||
if (opts.append || opts.prepend) {
|
||||
streams.err.append_format(
|
||||
L"%ls: Cannot use --append or --prepend when assigning to a slice", cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Argument count and index count must agree.
|
||||
if (split->indexes.size() != static_cast<size_t>(argc)) {
|
||||
streams.err.append_format(BUILTIN_SET_MISMATCHED_ARGS, cmd, split->indexes.size(),
|
||||
argc);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<wcstring> new_values;
|
||||
if (split->indexes.empty()) {
|
||||
// Handle the simple, common, case. Set the var to the specified values.
|
||||
new_values = new_var_values(split->varname, opts, argc, argv, parser.vars());
|
||||
} else {
|
||||
// Handle the uncommon case of setting specific slices of a var.
|
||||
new_values = new_var_values_by_index(*split, argc, argv);
|
||||
}
|
||||
|
||||
// Set the value back in the variable stack and fire any events.
|
||||
int retval = env_set_reporting_errors(cmd, split->varname, scope, std::move(new_values),
|
||||
streams, parser);
|
||||
|
||||
if (retval == ENV_OK) {
|
||||
warn_if_uvar_shadows_global(cmd, opts, split->varname, streams, parser);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// The set builtin creates, updates, and erases (removes, deletes) variables.
|
||||
maybe_t<int> builtin_set(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
set_cmd_opts_t opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
argv += optind;
|
||||
argc -= optind;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
retval = validate_cmd_opts(cmd, opts, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.query) {
|
||||
retval = builtin_set_query(cmd, opts, argc, argv, parser, streams);
|
||||
} else if (opts.erase) {
|
||||
retval = builtin_set_erase(cmd, opts, argc, argv, parser, streams);
|
||||
} else if (opts.list) { // explicit list the vars we know about
|
||||
retval = builtin_set_list(cmd, opts, argc, argv, parser, streams);
|
||||
} else if (opts.show) {
|
||||
retval = builtin_set_show(cmd, opts, argc, argv, parser, streams);
|
||||
} else if (argc == 0) { // implicit list the vars we know about
|
||||
retval = builtin_set_list(cmd, opts, argc, argv, parser, streams);
|
||||
} else {
|
||||
retval = builtin_set_set(cmd, opts, argc, argv, parser, streams);
|
||||
}
|
||||
|
||||
if (retval == STATUS_CMD_OK && opts.preserve_failure_exit_status) return none();
|
||||
return retval;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Prototypes for functions for executing builtin_set functions.
|
||||
#ifndef FISH_BUILTIN_SET_H
|
||||
#define FISH_BUILTIN_SET_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
maybe_t<int> builtin_set(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -1,123 +0,0 @@
|
||||
// Implementation of the source builtin.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "source.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cwchar>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "../builtin.h"
|
||||
#include "../common.h"
|
||||
#include "../env.h"
|
||||
#include "../fallback.h" // IWYU pragma: keep
|
||||
#include "../fds.h"
|
||||
#include "../io.h"
|
||||
#include "../maybe.h"
|
||||
#include "../null_terminated_array.h"
|
||||
#include "../parser.h"
|
||||
#include "../reader.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// The source builtin, sometimes called `.`. Evaluates the contents of a file in the current
|
||||
/// context.
|
||||
maybe_t<int> builtin_source(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
help_only_cmd_opts_t opts;
|
||||
|
||||
int optind;
|
||||
int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams);
|
||||
if (retval != STATUS_CMD_OK) return retval;
|
||||
|
||||
if (opts.print_help) {
|
||||
builtin_print_help(parser, streams, cmd);
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
// If we open a file, this ensures we close it.
|
||||
autoclose_fd_t opened_fd;
|
||||
|
||||
// The fd that we read from, either from opened_fd or stdin.
|
||||
int fd = -1;
|
||||
|
||||
struct stat buf;
|
||||
filename_ref_t func_filename{};
|
||||
|
||||
if (argc == optind || std::wcscmp(argv[optind], L"-") == 0) {
|
||||
if (streams.stdin_fd < 0) {
|
||||
streams.err.append_format(_(L"%ls: stdin is closed\n"), cmd);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
// Either a bare `source` which means to implicitly read from stdin or an explicit `-`.
|
||||
if (argc == optind && isatty(streams.stdin_fd)) {
|
||||
// Don't implicitly read from the terminal.
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
func_filename = std::make_shared<wcstring>(L"-");
|
||||
fd = streams.stdin_fd;
|
||||
} else {
|
||||
opened_fd = autoclose_fd_t(wopen_cloexec(argv[optind], O_RDONLY));
|
||||
if (!opened_fd.valid()) {
|
||||
wcstring esc = escape_string(argv[optind]);
|
||||
streams.err.append_format(_(L"%ls: Error encountered while sourcing file '%ls':\n"),
|
||||
cmd, esc.c_str());
|
||||
builtin_wperror(cmd, streams);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
fd = opened_fd.fd();
|
||||
if (fstat(fd, &buf) == -1) {
|
||||
wcstring esc = escape_string(argv[optind]);
|
||||
streams.err.append_format(_(L"%ls: Error encountered while sourcing file '%ls':\n"),
|
||||
cmd, esc.c_str());
|
||||
builtin_wperror(L"source", streams);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
if (!S_ISREG(buf.st_mode)) {
|
||||
wcstring esc = escape_string(argv[optind]);
|
||||
streams.err.append_format(_(L"%ls: '%ls' is not a file\n"), cmd, esc.c_str());
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
|
||||
func_filename = std::make_shared<wcstring>(argv[optind]);
|
||||
}
|
||||
assert(fd >= 0 && "Should have a valid fd");
|
||||
assert(func_filename && "Should have valid function filename");
|
||||
|
||||
const block_t *sb = parser.push_block(block_t::source_block(func_filename));
|
||||
auto &ld = parser.libdata();
|
||||
scoped_push<filename_ref_t> filename_push{&ld.current_filename, func_filename};
|
||||
|
||||
// Construct argv from our null-terminated list.
|
||||
// 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.
|
||||
std::vector<wcstring> argv_list;
|
||||
const wchar_t *const *remaining_args = argv + optind + (argc == optind ? 0 : 1);
|
||||
for (size_t i = 0, len = null_terminated_array_length(remaining_args); i < len; i++) {
|
||||
argv_list.push_back(remaining_args[i]);
|
||||
}
|
||||
parser.vars().set_argv(std::move(argv_list));
|
||||
|
||||
retval = reader_read(parser, fd, streams.io_chain ? *streams.io_chain : io_chain_t());
|
||||
|
||||
parser.pop_block(sb);
|
||||
|
||||
if (retval != STATUS_CMD_OK) {
|
||||
wcstring esc = escape_string(*func_filename);
|
||||
streams.err.append_format(_(L"%ls: Error while reading file '%ls'\n"), cmd,
|
||||
esc == L"-" ? L"<stdin>" : esc.c_str());
|
||||
} else {
|
||||
retval = parser.get_last_status();
|
||||
}
|
||||
|
||||
// Do not close fd after calling reader_read. reader_read automatically closes it before calling
|
||||
// eval.
|
||||
return retval;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Prototypes for executing builtin_source function.
|
||||
#ifndef FISH_BUILTIN_SOURCE_H
|
||||
#define FISH_BUILTIN_SOURCE_H
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_source(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
#endif
|
||||
@@ -16,8 +16,8 @@
|
||||
#include "../maybe.h"
|
||||
#include "../wgetopt.h"
|
||||
#include "../wutil.h" // IWYU pragma: keep
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
#include "builtins/shared.rs.h"
|
||||
#include "builtins/ulimit.h"
|
||||
|
||||
/// Struct describing a resource limit.
|
||||
struct resource_t {
|
||||
@@ -108,9 +108,9 @@ static void print(int resource, int hard, io_streams_t &streams) {
|
||||
rlim_t l = get(resource, hard);
|
||||
|
||||
if (l == RLIM_INFINITY)
|
||||
streams.out.append(L"unlimited\n");
|
||||
streams.out()->append(format_string(L"unlimited\n"));
|
||||
else
|
||||
streams.out.append_format(L"%lu\n", l / get_multiplier(resource));
|
||||
streams.out()->append(format_string(L"%lu\n", l / get_multiplier(resource)));
|
||||
}
|
||||
|
||||
/// Print values of all resource limits.
|
||||
@@ -133,13 +133,14 @@ static void print_all(int hard, io_streams_t &streams) {
|
||||
? L"(seconds, "
|
||||
: (get_multiplier(resource_arr[i].resource) == 1 ? L"(" : L"(kB, "));
|
||||
|
||||
streams.out.append_format(L"%-*ls %10ls-%lc) ", w, resource_arr[i].desc, unit,
|
||||
resource_arr[i].switch_char);
|
||||
streams.out()->append(format_string(L"%-*ls %10ls-%lc) ", w, resource_arr[i].desc, unit,
|
||||
resource_arr[i].switch_char));
|
||||
|
||||
if (l == RLIM_INFINITY) {
|
||||
streams.out.append(L"unlimited\n");
|
||||
streams.out()->append(format_string(L"unlimited\n"));
|
||||
} else {
|
||||
streams.out.append_format(L"%lu\n", l / get_multiplier(resource_arr[i].resource));
|
||||
streams.out()->append(
|
||||
format_string(L"%lu\n", l / get_multiplier(resource_arr[i].resource)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,9 +176,9 @@ static int set_limit(int resource, int hard, int soft, rlim_t value, io_streams_
|
||||
|
||||
if (setrlimit(resource, &ls)) {
|
||||
if (errno == EPERM) {
|
||||
streams.err.append_format(
|
||||
L"ulimit: Permission denied when changing resource of type '%ls'\n",
|
||||
get_desc(resource));
|
||||
streams.err()->append(
|
||||
format_string(L"ulimit: Permission denied when changing resource of type '%ls'\n",
|
||||
get_desc(resource)));
|
||||
} else {
|
||||
builtin_wperror(L"ulimit", streams);
|
||||
}
|
||||
@@ -187,7 +188,10 @@ static int set_limit(int resource, int hard, int soft, rlim_t value, io_streams_
|
||||
}
|
||||
|
||||
/// The ulimit builtin, used for setting resource limits.
|
||||
maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
|
||||
int builtin_ulimit(const void *_parser, void *_streams, void *_argv) {
|
||||
const auto &parser = *static_cast<const parser_t *>(_parser);
|
||||
auto &streams = *static_cast<io_streams_t *>(_streams);
|
||||
auto argv = static_cast<const wchar_t **>(_argv);
|
||||
const wchar_t *cmd = argv[0];
|
||||
int argc = builtin_count_args(argv);
|
||||
bool report_all = false;
|
||||
@@ -379,11 +383,11 @@ maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
case ':': {
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case '?': {
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
|
||||
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
default: {
|
||||
@@ -398,9 +402,9 @@ maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar
|
||||
}
|
||||
|
||||
if (what == RLIMIT_UNKNOWN) {
|
||||
streams.err.append_format(
|
||||
_(L"%ls: Resource limit not available on this operating system\n"), cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: Resource limit not available on this operating system\n"), cmd));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
@@ -410,8 +414,8 @@ maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar
|
||||
print(what, hard, streams);
|
||||
return STATUS_CMD_OK;
|
||||
} else if (arg_count != 1) {
|
||||
streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
|
||||
@@ -423,8 +427,8 @@ maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar
|
||||
|
||||
rlim_t new_limit;
|
||||
if (*argv[w.woptind] == L'\0') {
|
||||
streams.err.append_format(_(L"%ls: New limit cannot be an empty string\n"), cmd);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(format_string(_(L"%ls: New limit cannot be an empty string\n"), cmd));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
} else if (wcscasecmp(argv[w.woptind], L"unlimited") == 0) {
|
||||
new_limit = RLIM_INFINITY;
|
||||
@@ -435,8 +439,9 @@ maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar
|
||||
} else {
|
||||
new_limit = fish_wcstol(argv[w.woptind]);
|
||||
if (errno) {
|
||||
streams.err.append_format(_(L"%ls: Invalid limit '%ls'\n"), cmd, argv[w.woptind]);
|
||||
builtin_print_error_trailer(parser, streams.err, cmd);
|
||||
streams.err()->append(
|
||||
format_string(_(L"%ls: Invalid limit '%ls'\n"), cmd, argv[w.woptind]));
|
||||
builtin_print_error_trailer(parser, *streams.err(), cmd);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
new_limit *= get_multiplier(what);
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#include "../maybe.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
struct Parser;
|
||||
struct IoStreams;
|
||||
using parser_t = Parser;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
maybe_t<int> builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
|
||||
int builtin_ulimit(const void *parser, void *streams, void *argv);
|
||||
#endif
|
||||
|
||||
11
src/color.h
11
src/color.h
@@ -76,6 +76,17 @@ class rgb_color_t {
|
||||
/// Returns whether the color is the normal special color.
|
||||
bool is_normal(void) const { return type == type_normal; }
|
||||
|
||||
void set_is_named() { type = type_named; }
|
||||
void set_is_rgb() { type = type_rgb; }
|
||||
void set_is_normal() { type = type_normal; }
|
||||
void set_is_reset() { type = type_reset; }
|
||||
void set_name_idx(uint8_t idx) { data.name_idx = idx; }
|
||||
void set_color(uint8_t r, uint8_t g, uint8_t b) {
|
||||
data.color.rgb[0] = r;
|
||||
data.color.rgb[1] = g;
|
||||
data.color.rgb[2] = b;
|
||||
}
|
||||
|
||||
/// Returns whether the color is the reset special color.
|
||||
bool is_reset(void) const { return type == type_reset; }
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
#include <memory>
|
||||
|
||||
#include "common.h"
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "common.rs.h"
|
||||
#endif
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "flog.h"
|
||||
@@ -42,7 +44,6 @@
|
||||
#include "iothread.h"
|
||||
#include "signals.h"
|
||||
#include "termsize.h"
|
||||
#include "topic_monitor.h"
|
||||
#include "wcstringutil.h"
|
||||
#include "wildcard.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
@@ -660,7 +661,7 @@ void narrow_string_safe(char buff[64], const wchar_t *s) {
|
||||
|
||||
/// Escape a string in a fashion suitable for using as a URL. Store the result in out_str.
|
||||
static void escape_string_url(const wcstring &in, wcstring &out) {
|
||||
auto result = rust_escape_string_url(in.c_str(), in.size());
|
||||
auto result = escape_string_url(in.c_str(), in.size());
|
||||
if (result) {
|
||||
out = *result;
|
||||
}
|
||||
@@ -668,7 +669,7 @@ static void escape_string_url(const wcstring &in, wcstring &out) {
|
||||
|
||||
/// Escape a string in a fashion suitable for using as a fish var name. Store the result in out_str.
|
||||
static void escape_string_var(const wcstring &in, wcstring &out) {
|
||||
auto result = rust_escape_string_var(in.c_str(), in.size());
|
||||
auto result = escape_string_var(in.c_str(), in.size());
|
||||
if (result) {
|
||||
out = *result;
|
||||
}
|
||||
@@ -693,7 +694,7 @@ wcstring escape_string_for_double_quotes(wcstring in) {
|
||||
/// Escape a string in a fashion suitable for using in fish script. Store the result in out_str.
|
||||
static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring &out,
|
||||
escape_flags_t flags) {
|
||||
auto result = rust_escape_string_script(orig_in, in_len, flags);
|
||||
auto result = escape_string_script(orig_in, in_len, flags);
|
||||
if (result) {
|
||||
out = *result;
|
||||
}
|
||||
@@ -977,32 +978,6 @@ maybe_t<size_t> read_unquoted_escape(const wchar_t *input, wcstring *result, boo
|
||||
return in_pos;
|
||||
}
|
||||
|
||||
bool unescape_string_in_place(wcstring *str, unescape_flags_t escape_special) {
|
||||
assert(str != nullptr);
|
||||
wcstring output;
|
||||
if (auto unescaped = unescape_string(str->c_str(), str->size(), escape_special)) {
|
||||
*str = *unescaped;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<wcstring> unescape_string(const wchar_t *input, unescape_flags_t escape_special,
|
||||
escape_string_style_t style) {
|
||||
return unescape_string(input, std::wcslen(input), escape_special, style);
|
||||
}
|
||||
|
||||
std::unique_ptr<wcstring> unescape_string(const wchar_t *input, size_t len,
|
||||
unescape_flags_t escape_special,
|
||||
escape_string_style_t style) {
|
||||
return rust_unescape_string(input, len, escape_special, style);
|
||||
}
|
||||
|
||||
std::unique_ptr<wcstring> unescape_string(const wcstring &input, unescape_flags_t escape_special,
|
||||
escape_string_style_t style) {
|
||||
return unescape_string(input.c_str(), input.size(), escape_special, style);
|
||||
}
|
||||
|
||||
wcstring format_size(long long sz) {
|
||||
wcstring result;
|
||||
const wchar_t *sz_name[] = {L"kB", L"MB", L"GB", L"TB", L"PB", L"EB", L"ZB", L"YB", nullptr};
|
||||
@@ -1294,7 +1269,5 @@ bool is_console_session() {
|
||||
/// can be init even if the rust version of the function is called instead. This is easier than
|
||||
/// declaring all those variables as extern, which I'll do in a separate PR.
|
||||
extern "C" {
|
||||
void fish_setlocale_ffi() {
|
||||
fish_setlocale();
|
||||
}
|
||||
void fish_setlocale_ffi() { fish_setlocale(); }
|
||||
}
|
||||
|
||||
20
src/common.h
20
src/common.h
@@ -502,22 +502,6 @@ wcstring escape_string_for_double_quotes(wcstring in);
|
||||
maybe_t<size_t> read_unquoted_escape(const wchar_t *input, wcstring *result, bool allow_incomplete,
|
||||
bool unescape_special);
|
||||
|
||||
/// Unescapes a string in-place. A true result indicates the string was unescaped, a false result
|
||||
/// indicates the string was unmodified.
|
||||
bool unescape_string_in_place(wcstring *str, unescape_flags_t escape_special);
|
||||
|
||||
/// Reverse the effects of calling `escape_string`. Returns the unescaped value by reference. On
|
||||
/// failure, the output is set to an empty string.
|
||||
std::unique_ptr<wcstring> unescape_string(const wchar_t *input, unescape_flags_t escape_special,
|
||||
escape_string_style_t style = STRING_STYLE_SCRIPT);
|
||||
|
||||
std::unique_ptr<wcstring> unescape_string(const wchar_t *input, size_t len,
|
||||
unescape_flags_t escape_special,
|
||||
escape_string_style_t style = STRING_STYLE_SCRIPT);
|
||||
|
||||
std::unique_ptr<wcstring> unescape_string(const wcstring &input, unescape_flags_t escape_special,
|
||||
escape_string_style_t style = STRING_STYLE_SCRIPT);
|
||||
|
||||
/// Return the number of seconds from the UNIX epoch, with subsecond precision. This function uses
|
||||
/// the gettimeofday function and will have the same precision as that function.
|
||||
using timepoint_t = double;
|
||||
@@ -694,4 +678,8 @@ __attribute__((always_inline)) bool inline iswdigit(const wchar_t c) {
|
||||
return c >= L'0' && c <= L'9';
|
||||
}
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "common.rs.h"
|
||||
#endif
|
||||
|
||||
#endif // FISH_COMMON_H
|
||||
|
||||
1967
src/complete.cpp
1967
src/complete.cpp
File diff suppressed because it is too large
Load Diff
249
src/complete.h
249
src/complete.h
@@ -13,8 +13,9 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// #include "expand.h"
|
||||
#include "common.h"
|
||||
#include "expand.h"
|
||||
#include "parser.h"
|
||||
#include "wcstringutil.h"
|
||||
|
||||
struct completion_mode_t {
|
||||
@@ -29,8 +30,6 @@ struct completion_mode_t {
|
||||
/// Character that separates the completion and description on programmable completions.
|
||||
#define PROG_COMPLETE_SEP L'\t'
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
enum {
|
||||
/// Do not insert space afterwards if this is the only completion. (The default is to try insert
|
||||
/// a space).
|
||||
@@ -53,240 +52,16 @@ enum {
|
||||
};
|
||||
using complete_flags_t = uint8_t;
|
||||
|
||||
/// std::function which accepts a completion string and returns its description.
|
||||
using description_func_t = std::function<wcstring(const wcstring &)>;
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "complete.rs.h"
|
||||
#else
|
||||
struct CompletionListFfi;
|
||||
struct Completion;
|
||||
struct CompletionRequestOptions;
|
||||
#endif
|
||||
|
||||
/// Helper to return a description_func_t for a constant string.
|
||||
description_func_t const_desc(const wcstring &s);
|
||||
|
||||
/// This is an individual completion entry, i.e. the result of an expansion of a completion rule.
|
||||
class completion_t {
|
||||
private:
|
||||
// No public default constructor.
|
||||
completion_t();
|
||||
|
||||
public:
|
||||
// Destructor. Not inlining it saves code size.
|
||||
~completion_t();
|
||||
|
||||
/// The completion string.
|
||||
wcstring completion;
|
||||
/// The description for this completion.
|
||||
wcstring description;
|
||||
/// The type of fuzzy match.
|
||||
string_fuzzy_match_t match;
|
||||
/// Flags determining the completion behavior.
|
||||
complete_flags_t flags;
|
||||
|
||||
// Construction.
|
||||
explicit completion_t(wcstring comp, wcstring desc = wcstring(),
|
||||
string_fuzzy_match_t match = string_fuzzy_match_t::exact_match(),
|
||||
complete_flags_t flags_val = 0);
|
||||
completion_t(const completion_t &);
|
||||
completion_t &operator=(const completion_t &);
|
||||
|
||||
// noexcepts are required for push_back to use the move ctor.
|
||||
completion_t(completion_t &&) noexcept;
|
||||
completion_t &operator=(completion_t &&) noexcept;
|
||||
|
||||
/// \return whether this replaces its token.
|
||||
bool replaces_token() const { return flags & COMPLETE_REPLACES_TOKEN; }
|
||||
|
||||
/// \return whether this replaces the entire commandline.
|
||||
bool replaces_commandline() const { return flags & COMPLETE_REPLACES_COMMANDLINE; }
|
||||
|
||||
/// \return the completion's match rank. Lower ranks are better completions.
|
||||
uint32_t rank() const { return match.rank(); }
|
||||
|
||||
// If this completion replaces the entire token, prepend a prefix. Otherwise do nothing.
|
||||
void prepend_token_prefix(const wcstring &prefix);
|
||||
};
|
||||
|
||||
using completion_list_t = std::vector<completion_t>;
|
||||
|
||||
struct completion_request_options_t {
|
||||
bool autosuggestion{}; // requesting autosuggestion
|
||||
bool descriptions{}; // make descriptions
|
||||
bool fuzzy_match{}; // if set, we do not require a prefix match
|
||||
|
||||
// Options for an autosuggestion.
|
||||
static completion_request_options_t autosuggest() {
|
||||
completion_request_options_t res{};
|
||||
res.autosuggestion = true;
|
||||
res.descriptions = false;
|
||||
res.fuzzy_match = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Options for a "normal" completion.
|
||||
static completion_request_options_t normal() {
|
||||
completion_request_options_t res{};
|
||||
res.autosuggestion = false;
|
||||
res.descriptions = true;
|
||||
res.fuzzy_match = true;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
using completion_list_t = std::vector<completion_t>;
|
||||
|
||||
/// A completion receiver accepts completions. It is essentially a wrapper around std::vector with
|
||||
/// some conveniences.
|
||||
class completion_receiver_t {
|
||||
public:
|
||||
/// Construct as empty, with a limit.
|
||||
explicit completion_receiver_t(size_t limit) : limit_(limit) {}
|
||||
|
||||
/// Acquire an existing list, with a limit.
|
||||
explicit completion_receiver_t(completion_list_t &&v, size_t limit)
|
||||
: completions_(std::move(v)), limit_(limit) {}
|
||||
|
||||
/// Add a completion.
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add(completion_t &&comp);
|
||||
|
||||
/// Add a completion with the given string, and default other properties.
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add(wcstring &&comp);
|
||||
|
||||
/// Add a completion with the given string, description, flags, and fuzzy match.
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
/// The 'desc' parameter is not && because if gettext is not enabled, then we end
|
||||
/// up passing a 'const wcstring &' here.
|
||||
__warn_unused bool add(wcstring &&comp, wcstring desc, complete_flags_t flags = 0,
|
||||
string_fuzzy_match_t match = string_fuzzy_match_t::exact_match());
|
||||
|
||||
/// Add a list of completions.
|
||||
/// \return true on success, false if this would overflow the limit.
|
||||
__warn_unused bool add_list(completion_list_t &&lst);
|
||||
|
||||
/// Swap our completions with a new list.
|
||||
void swap(completion_list_t &lst) { std::swap(completions_, lst); }
|
||||
|
||||
/// Clear the list of completions. This retains the storage inside completions_ which can be
|
||||
/// useful to prevent allocations.
|
||||
void clear() { completions_.clear(); }
|
||||
|
||||
/// \return whether our completion list is empty.
|
||||
bool empty() const { return completions_.empty(); }
|
||||
|
||||
/// \return how many completions we have stored.
|
||||
size_t size() const { return completions_.size(); }
|
||||
|
||||
/// \return a completion at an index.
|
||||
completion_t &at(size_t idx) { return completions_.at(idx); }
|
||||
const completion_t &at(size_t idx) const { return completions_.at(idx); }
|
||||
|
||||
/// \return the list of completions. Do not modify the size of the list via this function, as it
|
||||
/// may exceed our completion limit.
|
||||
const completion_list_t &get_list() const { return completions_; }
|
||||
completion_list_t &get_list() { return completions_; }
|
||||
|
||||
/// \return the list of completions, clearing it.
|
||||
completion_list_t take();
|
||||
|
||||
/// \return a new, empty receiver whose limit is our remaining capacity.
|
||||
/// This is useful for e.g. recursive calls when you want to act on the result before adding it.
|
||||
completion_receiver_t subreceiver() const;
|
||||
|
||||
private:
|
||||
// Our list of completions.
|
||||
completion_list_t completions_;
|
||||
|
||||
// The maximum number of completions to add. If our list length exceeds this, then new
|
||||
// completions are not added. Note 0 has no special significance here - use
|
||||
// numeric_limits<size_t>::max() instead.
|
||||
const size_t limit_;
|
||||
};
|
||||
|
||||
enum complete_option_type_t : uint8_t {
|
||||
option_type_args_only, // no option
|
||||
option_type_short, // -x
|
||||
option_type_single_long, // -foo
|
||||
option_type_double_long // --foo
|
||||
};
|
||||
|
||||
/// Sorts and remove any duplicate completions in the completion list, then puts them in priority
|
||||
/// order.
|
||||
void completions_sort_and_prioritize(completion_list_t *comps,
|
||||
completion_request_options_t flags = {});
|
||||
|
||||
/// Add an unexpanded completion "rule" to generate completions from for a command.
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// The command 'gcc -o' requires that a file follows it, so the requires_param mode is suitable.
|
||||
/// This can be done using the following line:
|
||||
///
|
||||
/// complete -c gcc -s o -r
|
||||
///
|
||||
/// The command 'grep -d' required that one of the strings 'read', 'skip' or 'recurse' is used. As
|
||||
/// such, it is suitable to specify that a completion requires one of them. This can be done using
|
||||
/// the following line:
|
||||
///
|
||||
/// complete -c grep -s d -x -a "read skip recurse"
|
||||
///
|
||||
/// \param cmd Command to complete.
|
||||
/// \param cmd_is_path If cmd_is_path is true, cmd will be interpreted as the absolute
|
||||
/// path of the program (optionally containing wildcards), otherwise it
|
||||
/// will be interpreted as the command name.
|
||||
/// \param option The name of an option.
|
||||
/// \param option_type The type of option: can be option_type_short (-x),
|
||||
/// option_type_single_long (-foo), option_type_double_long (--bar).
|
||||
/// \param result_mode Controls how to search further completions when this completion has been
|
||||
/// successfully matched.
|
||||
/// \param comp A space separated list of completions which may contain subshells.
|
||||
/// \param desc A description of the completion.
|
||||
/// \param condition a command to be run to check it this completion should be used. If \c condition
|
||||
/// is empty, the completion is always used.
|
||||
/// \param flags A set of completion flags
|
||||
void complete_add(const wcstring &cmd, bool cmd_is_path, const wcstring &option,
|
||||
complete_option_type_t option_type, completion_mode_t result_mode,
|
||||
std::vector<wcstring> condition, const wchar_t *comp, const wchar_t *desc,
|
||||
complete_flags_t flags);
|
||||
|
||||
/// Remove a previously defined completion.
|
||||
void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &option,
|
||||
complete_option_type_t type);
|
||||
|
||||
/// Removes all completions for a given command.
|
||||
void complete_remove_all(const wcstring &cmd, bool cmd_is_path);
|
||||
|
||||
/// Load command-specific completions for the specified command.
|
||||
/// \return true if something new was loaded, false if not.
|
||||
bool complete_load(const wcstring &cmd, parser_t &parser);
|
||||
|
||||
/// \return all completions of the command cmd.
|
||||
/// If \p ctx contains a parser, this will autoload functions and completions as needed.
|
||||
/// If it does not contain a parser, then any completions which need autoloading will be returned in
|
||||
/// \p needs_load, if not null.
|
||||
class operation_context_t;
|
||||
completion_list_t complete(const wcstring &cmd, completion_request_options_t flags,
|
||||
const operation_context_t &ctx,
|
||||
std::vector<wcstring> *out_needs_load = nullptr);
|
||||
|
||||
/// Return a list of all current completions.
|
||||
wcstring complete_print(const wcstring &cmd = L"");
|
||||
|
||||
/// Create a new completion entry.
|
||||
///
|
||||
/// \param completions The array of completions to append to
|
||||
/// \param comp The completion string
|
||||
/// \param desc The description of the completion
|
||||
/// \param flags completion flags
|
||||
void append_completion(completion_list_t *completions, wcstring comp, wcstring desc = wcstring(),
|
||||
complete_flags_t flags = 0,
|
||||
string_fuzzy_match_t match = string_fuzzy_match_t::exact_match());
|
||||
|
||||
/// Support for "wrap targets." A wrap target is a command that completes like another command.
|
||||
bool complete_add_wrapper(const wcstring &command, const wcstring &new_target);
|
||||
bool complete_remove_wrapper(const wcstring &command, const wcstring &target_to_remove);
|
||||
|
||||
/// Returns a list of wrap targets for a given command.
|
||||
std::vector<wcstring> complete_get_wrap_targets(const wcstring &command);
|
||||
wcstring_list_ffi_t complete_get_wrap_targets_ffi(const wcstring &command);
|
||||
|
||||
// Observes that fish_complete_path has changed.
|
||||
void complete_invalidate_path();
|
||||
using completion_t = Completion;
|
||||
using completion_request_options_t = CompletionRequestOptions;
|
||||
using completion_list_t = CompletionListFfi;
|
||||
|
||||
#endif
|
||||
|
||||
93
src/env.cpp
93
src/env.cpp
@@ -3,40 +3,12 @@
|
||||
|
||||
#include "env.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <pwd.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "abbrs.h"
|
||||
#include "common.h"
|
||||
#include "env_dispatch.rs.h"
|
||||
#include "env_universal_common.h"
|
||||
#include "event.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "fish_version.h"
|
||||
#include "flog.h"
|
||||
#include "global_safety.h"
|
||||
#include "history.h"
|
||||
#include "input.h"
|
||||
#include "null_terminated_array.h"
|
||||
#include "path.h"
|
||||
#include "proc.h"
|
||||
#include "reader.h"
|
||||
#include "termsize.h"
|
||||
#include "threads.rs.h"
|
||||
#include "wcstringutil.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// At init, we read all the environment variables from this array.
|
||||
extern char **environ;
|
||||
@@ -136,17 +108,7 @@ std::vector<wcstring> null_environment_t::get_names(env_mode_flags_t flags) cons
|
||||
return std::move(names.vals);
|
||||
}
|
||||
|
||||
/// Various things we need to initialize at run-time that don't really fit any of the other init
|
||||
/// routines.
|
||||
void misc_init() {
|
||||
// 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.
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
fflush(stdout);
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
bool env_stack_t::is_principal() const { return impl_->is_principal(); }
|
||||
|
||||
extern "C" {
|
||||
void env_cpp_init() {
|
||||
@@ -183,34 +145,6 @@ void set_inheriteds_ffi() {
|
||||
}
|
||||
}
|
||||
|
||||
bool env_stack_t::is_principal() const { return impl_->is_principal(); }
|
||||
|
||||
std::vector<rust::Box<Event>> env_stack_t::universal_sync(bool always) {
|
||||
event_list_ffi_t result;
|
||||
impl_->universal_sync(always, result);
|
||||
return std::move(result.events);
|
||||
}
|
||||
|
||||
void env_stack_t::apply_inherited_ffi(const function_properties_t &props) {
|
||||
impl_->apply_inherited_ffi(props);
|
||||
}
|
||||
|
||||
statuses_t env_stack_t::get_last_statuses() const {
|
||||
auto statuses_ffi = impl_->get_last_statuses();
|
||||
statuses_t res{};
|
||||
res.status = statuses_ffi->get_status();
|
||||
res.kill_signal = statuses_ffi->get_kill_signal();
|
||||
auto &pipestatus = statuses_ffi->get_pipestatus();
|
||||
res.pipestatus.assign(pipestatus.begin(), pipestatus.end());
|
||||
return res;
|
||||
}
|
||||
|
||||
int env_stack_t::get_last_status() const { return get_last_statuses().status; }
|
||||
|
||||
void env_stack_t::set_last_statuses(statuses_t s) {
|
||||
return impl_->set_last_statuses(s.status, s.kill_signal, s.pipestatus);
|
||||
}
|
||||
|
||||
/// Update the PWD variable directory from the result of getcwd().
|
||||
void env_stack_t::set_pwd_from_getcwd() { impl_->set_pwd_from_getcwd(); }
|
||||
|
||||
@@ -251,15 +185,6 @@ int env_stack_t::remove(const wcstring &key, int mode) {
|
||||
return static_cast<int>(impl_->remove(key, mode));
|
||||
}
|
||||
|
||||
std::shared_ptr<owning_null_terminated_array_t> env_stack_t::export_arr() {
|
||||
// export_array() returns a rust::Box<OwningNullTerminatedArrayRefFFI>.
|
||||
// Acquire ownership.
|
||||
OwningNullTerminatedArrayRefFFI *ptr = impl_->export_array();
|
||||
assert(ptr && "Null pointer");
|
||||
return std::make_shared<owning_null_terminated_array_t>(
|
||||
rust::Box<OwningNullTerminatedArrayRefFFI>::from_raw(ptr));
|
||||
}
|
||||
|
||||
maybe_t<env_var_t> env_dyn_t::get(const wcstring &key, env_mode_flags_t mode) const {
|
||||
if (auto *ptr = impl_->getf(key, mode)) {
|
||||
return env_var_t::new_ffi(ptr);
|
||||
@@ -302,6 +227,8 @@ const std::shared_ptr<env_stack_t> &env_stack_t::principal_ref() {
|
||||
env_stack_t::~env_stack_t() = default;
|
||||
env_stack_t::env_stack_t(env_stack_t &&) = default;
|
||||
env_stack_t::env_stack_t(rust::Box<EnvStackRef> imp) : impl_(std::move(imp)) {}
|
||||
env_stack_t::env_stack_t(uint8_t *imp)
|
||||
: impl_(rust::Box<EnvStackRef>::from_raw(reinterpret_cast<EnvStackRef *>(imp))) {}
|
||||
|
||||
#if defined(__APPLE__) || defined(__CYGWIN__)
|
||||
static int check_runtime_path(const char *path) {
|
||||
@@ -385,7 +312,7 @@ void unsetenv_lock(const char *name) {
|
||||
|
||||
wcstring_list_ffi_t get_history_variable_text_ffi(const wcstring &fish_history_val) {
|
||||
wcstring_list_ffi_t out{};
|
||||
std::shared_ptr<history_t> history = commandline_get_state().history;
|
||||
maybe_t<rust::Box<HistorySharedPtr>> history = commandline_get_state().history;
|
||||
if (!history) {
|
||||
// Effective duplication of history_session_id().
|
||||
wcstring session_id{};
|
||||
@@ -402,18 +329,12 @@ wcstring_list_ffi_t get_history_variable_text_ffi(const wcstring &fish_history_v
|
||||
// Valid session.
|
||||
session_id = fish_history_val;
|
||||
}
|
||||
history = history_t::with_name(session_id);
|
||||
history = history_with_name(session_id);
|
||||
}
|
||||
if (history) {
|
||||
history->get_history(out.vals);
|
||||
out = *(*history)->get_history();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
event_list_ffi_t::event_list_ffi_t() = default;
|
||||
|
||||
void event_list_ffi_t::push(void *event_vp) {
|
||||
auto event = static_cast<Event *>(event_vp);
|
||||
assert(event && "Null event");
|
||||
events.push_back(rust::Box<Event>::from_raw(event));
|
||||
}
|
||||
const EnvStackRef &env_stack_t::get_impl_ffi() const { return *impl_; }
|
||||
|
||||
102
src/env.h
102
src/env.h
@@ -17,17 +17,23 @@
|
||||
#include "maybe.h"
|
||||
#include "wutil.h"
|
||||
|
||||
struct event_list_ffi_t;
|
||||
struct function_properties_t;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "env/env_ffi.rs.h"
|
||||
#else
|
||||
struct EnvVar;
|
||||
struct EnvNull;
|
||||
struct EnvStack;
|
||||
struct EnvStackRef;
|
||||
struct EnvDyn;
|
||||
enum class env_stack_set_result_t : uint8_t;
|
||||
struct Statuses;
|
||||
#endif
|
||||
|
||||
struct event_list_ffi_t;
|
||||
struct function_properties_t;
|
||||
|
||||
using statuses_t = Statuses;
|
||||
|
||||
/// FFI helper for events.
|
||||
struct Event;
|
||||
struct event_list_ffi_t {
|
||||
@@ -44,6 +50,16 @@ struct event_list_ffi_t {
|
||||
|
||||
struct owning_null_terminated_array_t;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "env/env_ffi.rs.h"
|
||||
#else
|
||||
struct EnvVar;
|
||||
struct EnvNull;
|
||||
struct EnvStackRef;
|
||||
#endif
|
||||
|
||||
struct owning_null_terminated_array_t;
|
||||
|
||||
extern "C" {
|
||||
extern bool CURSES_INITIALIZED;
|
||||
|
||||
@@ -53,6 +69,8 @@ extern bool TERM_HAS_XN;
|
||||
extern size_t READ_BYTE_LIMIT;
|
||||
}
|
||||
|
||||
struct Event;
|
||||
|
||||
// Flags that may be passed as the 'mode' in env_stack_t::set() / environment_t::get().
|
||||
enum : uint16_t {
|
||||
/// Default mode. Used with `env_stack_t::get()` to indicate the caller doesn't care what scope
|
||||
@@ -84,31 +102,6 @@ using env_mode_flags_t = uint16_t;
|
||||
/// Return values for `env_stack_t::set()`.
|
||||
enum { ENV_OK, ENV_PERM, ENV_SCOPE, ENV_INVALID, ENV_NOT_FOUND };
|
||||
|
||||
/// A collection of status and pipestatus.
|
||||
struct statuses_t {
|
||||
/// Status of the last job to exit.
|
||||
int status{0};
|
||||
|
||||
/// Signal from the most recent process in the last job that was terminated by a signal.
|
||||
/// 0 if all processes exited normally.
|
||||
int kill_signal{0};
|
||||
|
||||
/// Pipestatus value.
|
||||
std::vector<int> pipestatus{};
|
||||
|
||||
/// Return a statuses for a single process status.
|
||||
static statuses_t just(int s) {
|
||||
statuses_t result{};
|
||||
result.status = s;
|
||||
result.pipestatus.push_back(s);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/// Various things we need to initialize at run-time that don't really fit any of the other init
|
||||
/// routines.
|
||||
void misc_init();
|
||||
|
||||
/// env_var_t is an immutable value-type data structure representing the value of an environment
|
||||
/// variable. This wraps the EnvVar type from Rust.
|
||||
class env_var_t {
|
||||
@@ -163,7 +156,6 @@ class env_var_t {
|
||||
|
||||
rust::Box<EnvVar> impl_;
|
||||
};
|
||||
typedef std::unordered_map<wcstring, env_var_t> var_table_t;
|
||||
|
||||
/// An environment is read-only access to variable values.
|
||||
class environment_t {
|
||||
@@ -201,7 +193,7 @@ class null_environment_t : public environment_t {
|
||||
|
||||
/// A mutable environment which allows scopes to be pushed and popped.
|
||||
class env_stack_t final : public environment_t {
|
||||
friend class Parser;
|
||||
friend struct Parser;
|
||||
|
||||
/// \return whether we are the principal stack.
|
||||
bool is_principal() const;
|
||||
@@ -209,6 +201,8 @@ class env_stack_t final : public environment_t {
|
||||
public:
|
||||
~env_stack_t() override;
|
||||
env_stack_t(env_stack_t &&);
|
||||
/* implicit */ env_stack_t(rust::Box<EnvStackRef> imp);
|
||||
/* implicit */ env_stack_t(uint8_t *imp);
|
||||
|
||||
/// Implementation of environment_t.
|
||||
maybe_t<env_var_t> get(const wcstring &key, env_mode_flags_t mode = ENV_DEFAULT) const override;
|
||||
@@ -248,21 +242,12 @@ class env_stack_t final : public environment_t {
|
||||
/// Pop the variable stack. Used for implementing local variables for functions and for-loops.
|
||||
void pop();
|
||||
|
||||
/// Returns an array containing all exported variables in a format suitable for execv.
|
||||
std::shared_ptr<owning_null_terminated_array_t> export_arr();
|
||||
|
||||
/// Snapshot this environment. This means returning a read-only copy. Local variables are copied
|
||||
/// but globals are shared (i.e. changes to global will be visible to this snapshot). This
|
||||
/// returns a shared_ptr for convenience, since the most common reason to snapshot is because
|
||||
/// you want to read from another thread.
|
||||
std::shared_ptr<environment_t> snapshot() const;
|
||||
|
||||
/// Helpers to get and set the proc statuses.
|
||||
/// These correspond to $status and $pipestatus.
|
||||
statuses_t get_last_statuses() const;
|
||||
int get_last_status() const;
|
||||
void set_last_statuses(statuses_t s);
|
||||
|
||||
/// Sets up argv as the given list of strings.
|
||||
void set_argv(std::vector<wcstring> argv);
|
||||
|
||||
@@ -275,15 +260,6 @@ class env_stack_t final : public environment_t {
|
||||
return environment_t::get_or_null(key, mode);
|
||||
}
|
||||
|
||||
/// Synchronizes universal variable changes.
|
||||
/// If \p always is set, perform synchronization even if there's no pending changes from this
|
||||
/// instance (that is, look for changes from other fish instances).
|
||||
/// \return a list of events for changed variables.
|
||||
std::vector<rust::Box<Event>> universal_sync(bool always);
|
||||
|
||||
/// Applies inherited variables in preparation for executing a function.
|
||||
void apply_inherited_ffi(const function_properties_t &props);
|
||||
|
||||
// Compatibility hack; access the "environment stack" from back when there was just one.
|
||||
static const std::shared_ptr<env_stack_t> &principal_ref();
|
||||
static env_stack_t &principal() { return *principal_ref(); }
|
||||
@@ -294,11 +270,9 @@ class env_stack_t final : public environment_t {
|
||||
|
||||
/// Access the underlying Rust implementation.
|
||||
/// This returns a const rust::Box<EnvStackRef> *, or in Rust terms, a *const Box<EnvStackRef>.
|
||||
const void *get_impl_ffi() const { return &impl_; }
|
||||
const EnvStackRef &get_impl_ffi() const;
|
||||
|
||||
private:
|
||||
env_stack_t(rust::Box<EnvStackRef> imp);
|
||||
|
||||
/// The implementation. Do not access this directly.
|
||||
rust::Box<EnvStackRef> impl_;
|
||||
};
|
||||
@@ -318,6 +292,32 @@ class env_dyn_t final : public environment_t {
|
||||
};
|
||||
#endif
|
||||
|
||||
/// A struct of configuration directories, determined in main() that fish will optionally pass to
|
||||
/// env_init.
|
||||
struct config_paths_t {
|
||||
wcstring data; // e.g., /usr/local/share
|
||||
wcstring sysconf; // e.g., /usr/local/etc
|
||||
wcstring doc; // e.g., /usr/local/share/doc/fish
|
||||
wcstring bin; // e.g., /usr/local/bin
|
||||
};
|
||||
|
||||
/// Initialize environment variable data.
|
||||
void env_init(const struct config_paths_t *paths = nullptr, bool do_uvars = true,
|
||||
bool default_paths = false);
|
||||
|
||||
using env_var_flags_t = uint8_t;
|
||||
enum {
|
||||
env_var_flag_export = 1 << 0, // whether the variable is exported
|
||||
env_var_flag_read_only = 1 << 1, // whether the variable is read only
|
||||
env_var_flag_pathvar = 1 << 2, // whether the variable is a path variable
|
||||
};
|
||||
|
||||
/// A mutable environment which allows scopes to be pushed and popped.
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
struct EnvDyn;
|
||||
#endif
|
||||
|
||||
/// Gets a path appropriate for runtime storage
|
||||
wcstring env_get_runtime_path();
|
||||
|
||||
|
||||
16
src/env_dispatch.h
Normal file
16
src/env_dispatch.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// Prototypes for functions that react to environment variable changes
|
||||
#ifndef FISH_ENV_DISPATCH_H
|
||||
#define FISH_ENV_DISPATCH_H
|
||||
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "common.h"
|
||||
#include "env.h"
|
||||
|
||||
/// Initialize variable dispatch.
|
||||
void env_dispatch_init(const environment_t &vars);
|
||||
|
||||
/// React to changes in variables like LANG which require running some code.
|
||||
void env_dispatch_var_change(const wcstring &key, env_stack_t &vars);
|
||||
|
||||
#endif
|
||||
2
src/env_fwd.h
Normal file
2
src/env_fwd.h
Normal file
@@ -0,0 +1,2 @@
|
||||
struct EnvStackRef;
|
||||
struct EnvDyn;
|
||||
@@ -55,830 +55,6 @@
|
||||
#include <bsd/ifaddrs.h>
|
||||
#endif // Haiku
|
||||
|
||||
/// Error message.
|
||||
#define PARSE_ERR L"Unable to parse universal variable message: '%ls'"
|
||||
|
||||
/// Small note about not editing ~/.fishd manually. Inserted at the top of all .fishd files.
|
||||
#define SAVE_MSG "# This file contains fish universal variable definitions.\n"
|
||||
|
||||
/// Version for fish 3.0
|
||||
#define UVARS_VERSION_3_0 "3.0"
|
||||
|
||||
// Maximum file size we'll read.
|
||||
static constexpr size_t k_max_read_size = 16 * 1024 * 1024;
|
||||
|
||||
// Fields used in fish 2.x uvars.
|
||||
namespace fish2x_uvars {
|
||||
namespace {
|
||||
constexpr const char *SET = "SET";
|
||||
constexpr const char *SET_EXPORT = "SET_EXPORT";
|
||||
} // namespace
|
||||
} // namespace fish2x_uvars
|
||||
|
||||
// Fields used in fish 3.0 uvars
|
||||
namespace fish3_uvars {
|
||||
namespace {
|
||||
constexpr const char *SETUVAR = "SETUVAR";
|
||||
constexpr const char *EXPORT = "--export";
|
||||
constexpr const char *PATH = "--path";
|
||||
} // namespace
|
||||
} // namespace fish3_uvars
|
||||
|
||||
/// The different types of messages found in the fishd file.
|
||||
enum class uvar_message_type_t { set, set_export };
|
||||
|
||||
static maybe_t<wcstring> default_vars_path_directory() {
|
||||
wcstring path;
|
||||
if (!path_get_config(path)) return none();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// \return the default variable path, or an empty string on failure.
|
||||
static wcstring default_vars_path() {
|
||||
if (auto path = default_vars_path_directory()) {
|
||||
path->append(L"/fish_variables");
|
||||
return path.acquire();
|
||||
}
|
||||
return wcstring{};
|
||||
}
|
||||
|
||||
/// Test if the message msg contains the command cmd.
|
||||
/// On success, updates the cursor to just past the command.
|
||||
static bool match(const wchar_t **inout_cursor, const char *cmd) {
|
||||
const wchar_t *cursor = *inout_cursor;
|
||||
size_t len = std::strlen(cmd);
|
||||
if (!std::equal(cmd, cmd + len, cursor)) {
|
||||
return false;
|
||||
}
|
||||
if (cursor[len] && cursor[len] != L' ' && cursor[len] != L'\t') return false;
|
||||
*inout_cursor = cursor + len;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The universal variable format has some funny escaping requirements; here we try to be safe.
|
||||
static bool is_universal_safe_to_encode_directly(wchar_t c) {
|
||||
if (c < 32 || c > 128) return false;
|
||||
|
||||
return iswalnum(c) || std::wcschr(L"/_", c);
|
||||
}
|
||||
|
||||
/// Escape specified string.
|
||||
static wcstring full_escape(const wcstring &in) {
|
||||
wcstring out;
|
||||
for (wchar_t c : in) {
|
||||
if (is_universal_safe_to_encode_directly(c)) {
|
||||
out.push_back(c);
|
||||
} else if (c <= static_cast<wchar_t>(ASCII_MAX)) {
|
||||
// See #1225 for discussion of use of ASCII_MAX here.
|
||||
append_format(out, L"\\x%.2x", c);
|
||||
} else if (c < 65536) {
|
||||
append_format(out, L"\\u%.4x", c);
|
||||
} else {
|
||||
append_format(out, L"\\U%.8x", c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Converts input to UTF-8 and appends it to receiver, using storage as temp storage.
|
||||
static bool append_utf8(const wcstring &input, std::string *receiver, std::string *storage) {
|
||||
bool result = false;
|
||||
if (wchar_to_utf8_string(input, storage)) {
|
||||
receiver->append(*storage);
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Creates a file entry like "SET fish_color_cwd:FF0". Appends the result to *result (as UTF8).
|
||||
/// Returns true on success. storage may be used for temporary storage, to avoid allocations.
|
||||
static bool append_file_entry(env_var_t::env_var_flags_t flags, const wcstring &key_in,
|
||||
const wcstring &val_in, std::string *result, std::string *storage) {
|
||||
namespace f3 = fish3_uvars;
|
||||
assert(storage != nullptr);
|
||||
assert(result != nullptr);
|
||||
|
||||
// Record the length on entry, in case we need to back up.
|
||||
bool success = true;
|
||||
const size_t result_length_on_entry = result->size();
|
||||
|
||||
// Append SETVAR header.
|
||||
result->append(f3::SETUVAR);
|
||||
result->push_back(' ');
|
||||
|
||||
// Append flags.
|
||||
if (flags & env_var_t::flag_export) {
|
||||
result->append(f3::EXPORT);
|
||||
result->push_back(' ');
|
||||
}
|
||||
if (flags & env_var_t::flag_pathvar) {
|
||||
result->append(f3::PATH);
|
||||
result->push_back(' ');
|
||||
}
|
||||
|
||||
// Append variable name like "fish_color_cwd".
|
||||
if (!valid_var_name(key_in)) {
|
||||
FLOGF(error, L"Illegal variable name: '%ls'", key_in.c_str());
|
||||
success = false;
|
||||
}
|
||||
if (success && !append_utf8(key_in, result, storage)) {
|
||||
FLOGF(error, L"Could not convert %ls to narrow character string", key_in.c_str());
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Append ":".
|
||||
if (success) {
|
||||
result->push_back(':');
|
||||
}
|
||||
|
||||
// Append value.
|
||||
if (success && !append_utf8(full_escape(val_in), result, storage)) {
|
||||
FLOGF(error, L"Could not convert %ls to narrow character string", val_in.c_str());
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Append newline.
|
||||
if (success) {
|
||||
result->push_back('\n');
|
||||
}
|
||||
|
||||
// Don't modify result on failure. It's sufficient to simply resize it since all we ever did was
|
||||
// append to it.
|
||||
if (!success) {
|
||||
result->resize(result_length_on_entry);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// Encoding of a null string.
|
||||
static const wchar_t *const ENV_NULL = L"\x1d";
|
||||
|
||||
/// Character used to separate arrays in universal variables file.
|
||||
/// This is 30, the ASCII record separator.
|
||||
static const wchar_t UVAR_ARRAY_SEP = 0x1e;
|
||||
|
||||
/// Decode a serialized universal variable value into a list.
|
||||
static std::vector<wcstring> decode_serialized(const wcstring &val) {
|
||||
if (val == ENV_NULL) return {};
|
||||
return split_string(val, UVAR_ARRAY_SEP);
|
||||
}
|
||||
|
||||
/// Decode a a list into a serialized universal variable value.
|
||||
static wcstring encode_serialized(const std::vector<wcstring> &vals) {
|
||||
if (vals.empty()) return ENV_NULL;
|
||||
return join_strings(vals, UVAR_ARRAY_SEP);
|
||||
}
|
||||
|
||||
maybe_t<env_var_t> env_universal_t::get(const wcstring &name) const {
|
||||
auto where = vars.find(name);
|
||||
if (where != vars.end()) return where->second;
|
||||
return none();
|
||||
}
|
||||
|
||||
std::unique_ptr<env_var_t> env_universal_t::get_ffi(const wcstring &name) const {
|
||||
if (auto var = this->get(name)) {
|
||||
return make_unique<env_var_t>(var.acquire());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
maybe_t<env_var_t::env_var_flags_t> env_universal_t::get_flags(const wcstring &name) const {
|
||||
auto where = vars.find(name);
|
||||
if (where != vars.end()) {
|
||||
return where->second.get_flags();
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
void env_universal_t::set(const wcstring &key, const env_var_t &var) {
|
||||
bool new_entry = vars.count(key) == 0;
|
||||
env_var_t &entry = vars[key];
|
||||
if (new_entry || entry != var) {
|
||||
entry = var;
|
||||
this->modified.insert(key);
|
||||
if (entry.exports()) export_generation += 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool env_universal_t::remove(const wcstring &key) {
|
||||
auto iter = this->vars.find(key);
|
||||
if (iter != this->vars.end()) {
|
||||
if (iter->second.exports()) export_generation += 1;
|
||||
this->vars.erase(iter);
|
||||
this->modified.insert(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<wcstring> env_universal_t::get_names(bool show_exported, bool show_unexported) const {
|
||||
std::vector<wcstring> result;
|
||||
for (const auto &kv : vars) {
|
||||
const wcstring &key = kv.first;
|
||||
const env_var_t &var = kv.second;
|
||||
if ((var.exports() && show_exported) || (!var.exports() && show_unexported)) {
|
||||
result.push_back(key);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Given a variable table, generate callbacks representing the difference between our vars and the
|
||||
// new vars. Update our exports generation.
|
||||
void env_universal_t::generate_callbacks_and_update_exports(const var_table_t &new_vars,
|
||||
callback_data_list_t &callbacks) {
|
||||
// Construct callbacks for erased values.
|
||||
for (const auto &kv : this->vars) {
|
||||
const wcstring &key = kv.first;
|
||||
// Skip modified values.
|
||||
if (this->modified.count(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the value is not present in new_vars, it has been erased.
|
||||
if (new_vars.count(key) == 0) {
|
||||
callbacks.push_back(callback_data_t(key, none()));
|
||||
if (kv.second.exports()) export_generation += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct callbacks for newly inserted or changed values.
|
||||
for (const auto &kv : new_vars) {
|
||||
const wcstring &key = kv.first;
|
||||
|
||||
// Skip modified values.
|
||||
if (this->modified.find(key) != this->modified.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// See if the value has changed.
|
||||
const env_var_t &new_entry = kv.second;
|
||||
var_table_t::const_iterator existing = this->vars.find(key);
|
||||
|
||||
bool old_exports = (existing != this->vars.end() && existing->second.exports());
|
||||
bool export_changed = (old_exports != new_entry.exports());
|
||||
bool value_changed = existing != this->vars.end() && existing->second != new_entry;
|
||||
if (export_changed || value_changed) {
|
||||
export_generation += 1;
|
||||
}
|
||||
if (existing == this->vars.end() || export_changed || value_changed) {
|
||||
// Value is set for the first time, or has changed.
|
||||
callbacks.push_back(callback_data_t(key, new_entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void env_universal_t::acquire_variables(var_table_t &&vars_to_acquire) {
|
||||
// Copy modified values from existing vars to vars_to_acquire.
|
||||
for (const auto &key : this->modified) {
|
||||
auto src_iter = this->vars.find(key);
|
||||
if (src_iter == this->vars.end()) {
|
||||
/* The value has been deleted. */
|
||||
vars_to_acquire.erase(key);
|
||||
} else {
|
||||
// The value has been modified. Copy it over. Note we can destructively modify the
|
||||
// source entry in vars since we are about to get rid of this->vars entirely.
|
||||
env_var_t &src = src_iter->second;
|
||||
env_var_t &dst = vars_to_acquire[key];
|
||||
dst = src;
|
||||
}
|
||||
}
|
||||
|
||||
// We have constructed all the callbacks and updated vars_to_acquire. Acquire it!
|
||||
this->vars = std::move(vars_to_acquire);
|
||||
}
|
||||
|
||||
void env_universal_t::load_from_fd(int fd, callback_data_list_t &callbacks) {
|
||||
assert(fd >= 0);
|
||||
// Get the dev / inode.
|
||||
const file_id_t current_file = file_id_for_fd(fd);
|
||||
if (current_file == last_read_file) {
|
||||
FLOGF(uvar_file, L"universal log sync elided based on fstat()");
|
||||
} else {
|
||||
// Read a variables table from the file.
|
||||
var_table_t new_vars;
|
||||
uvar_format_t format = this->read_message_internal(fd, &new_vars);
|
||||
|
||||
// Hacky: if the read format is in the future, avoid overwriting the file: never try to
|
||||
// save.
|
||||
if (format == uvar_format_t::future) {
|
||||
ok_to_save = false;
|
||||
}
|
||||
|
||||
// Announce changes and update our exports generation.
|
||||
this->generate_callbacks_and_update_exports(new_vars, callbacks);
|
||||
|
||||
// Acquire the new variables.
|
||||
this->acquire_variables(std::move(new_vars));
|
||||
last_read_file = current_file;
|
||||
}
|
||||
}
|
||||
|
||||
bool env_universal_t::load_from_path(const wcstring &path, callback_data_list_t &callbacks) {
|
||||
return load_from_path(wcs2zstring(path), callbacks);
|
||||
}
|
||||
|
||||
bool env_universal_t::load_from_path(const std::string &path, callback_data_list_t &callbacks) {
|
||||
// Check to see if the file is unchanged. We do this again in load_from_fd, but this avoids
|
||||
// opening the file unnecessarily.
|
||||
if (last_read_file != kInvalidFileID && file_id_for_path(path) == last_read_file) {
|
||||
FLOGF(uvar_file, L"universal log sync elided based on fast stat()");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result = false;
|
||||
autoclose_fd_t fd{open_cloexec(path, O_RDONLY)};
|
||||
if (fd.valid()) {
|
||||
FLOGF(uvar_file, L"universal log reading from file");
|
||||
this->load_from_fd(fd.fd(), callbacks);
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Serialize the contents to a string.
|
||||
std::string env_universal_t::serialize_with_vars(const var_table_t &vars) {
|
||||
std::string storage;
|
||||
std::string contents;
|
||||
contents.append(SAVE_MSG);
|
||||
contents.append("# VERSION: " UVARS_VERSION_3_0 "\n");
|
||||
|
||||
// Preserve legacy behavior by sorting the values first
|
||||
using env_pair_t =
|
||||
std::pair<std::reference_wrapper<const wcstring>, std::reference_wrapper<const env_var_t>>;
|
||||
std::vector<env_pair_t> cloned(vars.begin(), vars.end());
|
||||
std::sort(cloned.begin(), cloned.end(), [](const env_pair_t &p1, const env_pair_t &p2) {
|
||||
return p1.first.get() < p2.first.get();
|
||||
});
|
||||
|
||||
for (const auto &kv : cloned) {
|
||||
// Append the entry. Note that append_file_entry may fail, but that only affects one
|
||||
// variable; soldier on.
|
||||
const wcstring &key = kv.first;
|
||||
const env_var_t &var = kv.second;
|
||||
append_file_entry(var.get_flags(), key, encode_serialized(var.as_list()), &contents,
|
||||
&storage);
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
/// Writes our state to the fd. path is provided only for error reporting.
|
||||
bool env_universal_t::write_to_fd(int fd, const wcstring &path) {
|
||||
assert(fd >= 0);
|
||||
bool success = true;
|
||||
std::string contents = serialize_with_vars(vars);
|
||||
if (write_loop(fd, contents.data(), contents.size()) < 0) {
|
||||
const char *error = std::strerror(errno);
|
||||
FLOGF(error, _(L"Unable to write to universal variables file '%ls': %s"), path.c_str(),
|
||||
error);
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Since we just wrote out this file, it matches our internal state; pretend we read from it.
|
||||
this->last_read_file = file_id_for_fd(fd);
|
||||
|
||||
// We don't close the file.
|
||||
return success;
|
||||
}
|
||||
|
||||
bool env_universal_t::move_new_vars_file_into_place(const wcstring &src, const wcstring &dst) {
|
||||
int ret = wrename(src, dst);
|
||||
if (ret != 0) {
|
||||
const char *error = std::strerror(errno);
|
||||
FLOGF(error, _(L"Unable to rename file from '%ls' to '%ls': %s"), src.c_str(), dst.c_str(),
|
||||
error);
|
||||
}
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
void env_universal_t::initialize_at_path(callback_data_list_t &callbacks, wcstring path) {
|
||||
if (path.empty()) return;
|
||||
assert(!initialized() && "Already initialized");
|
||||
vars_path_ = std::move(path);
|
||||
narrow_vars_path_ = wcs2zstring(vars_path_);
|
||||
|
||||
if (load_from_path(narrow_vars_path_, callbacks)) {
|
||||
// Successfully loaded from our normal path.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void env_universal_t::initialize(callback_data_list_t &callbacks) {
|
||||
// Set do_flock to false immediately if the default variable path is on a remote filesystem.
|
||||
// See #7968.
|
||||
if (path_get_config_remoteness() == dir_remoteness_t::remote) do_flock = false;
|
||||
this->initialize_at_path(callbacks, default_vars_path());
|
||||
}
|
||||
|
||||
autoclose_fd_t env_universal_t::open_temporary_file(const wcstring &directory, wcstring *out_path) {
|
||||
// Create and open a temporary file for writing within the given directory. Try to create a
|
||||
// temporary file, up to 10 times. We don't use mkstemps because we want to open it CLO_EXEC.
|
||||
// This should almost always succeed on the first try.
|
||||
assert(!string_suffixes_string(L"/", directory)); //!OCLINT(multiple unary operator)
|
||||
|
||||
int saved_errno = 0;
|
||||
const wcstring tmp_name_template = directory + L"/fishd.tmp.XXXXXX";
|
||||
autoclose_fd_t result;
|
||||
std::string narrow_str;
|
||||
for (size_t attempt = 0; attempt < 10 && !result.valid(); attempt++) {
|
||||
narrow_str = wcs2zstring(tmp_name_template);
|
||||
result.reset(fish_mkstemp_cloexec(&narrow_str[0]));
|
||||
saved_errno = errno;
|
||||
}
|
||||
*out_path = str2wcstring(narrow_str);
|
||||
|
||||
if (!result.valid()) {
|
||||
const char *error = std::strerror(saved_errno);
|
||||
FLOGF(error, _(L"Unable to open temporary file '%ls': %s"), out_path->c_str(), error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Try locking the file.
|
||||
/// \return true on success, false on error.
|
||||
static bool flock_uvar_file(int fd) {
|
||||
double start_time = timef();
|
||||
while (flock(fd, LOCK_EX) == -1) {
|
||||
if (errno != EINTR) return false; // do nothing per issue #2149
|
||||
}
|
||||
double duration = timef() - start_time;
|
||||
if (duration > 0.25) {
|
||||
FLOGF(warning, _(L"Locking the universal var file took too long (%.3f seconds)."),
|
||||
duration);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool env_universal_t::open_and_acquire_lock(const wcstring &path, autoclose_fd_t *out_fd) {
|
||||
// Attempt to open the file for reading at the given path, atomically acquiring a lock. On BSD,
|
||||
// we can use O_EXLOCK. On Linux, we open the file, take a lock, and then compare fstat() to
|
||||
// stat(); if they match, it means that the file was not replaced before we acquired the lock.
|
||||
//
|
||||
// We pass O_RDONLY with O_CREAT; this creates a potentially empty file. We do this so that we
|
||||
// have something to lock on.
|
||||
bool locked_by_open = false;
|
||||
int flags = O_RDWR | O_CREAT;
|
||||
|
||||
#ifdef O_EXLOCK
|
||||
if (do_flock) {
|
||||
flags |= O_EXLOCK;
|
||||
locked_by_open = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
autoclose_fd_t fd{};
|
||||
while (!fd.valid()) {
|
||||
fd = autoclose_fd_t{wopen_cloexec(path, flags, 0644)};
|
||||
|
||||
if (!fd.valid()) {
|
||||
int err = errno;
|
||||
if (err == EINTR) continue; // signaled; try again
|
||||
|
||||
#ifdef O_EXLOCK
|
||||
if ((flags & O_EXLOCK) && (err == ENOTSUP || err == EOPNOTSUPP)) {
|
||||
// Filesystem probably does not support locking. Give up on locking.
|
||||
// Note that on Linux the two errno symbols have the same value but on BSD they're
|
||||
// different.
|
||||
flags &= ~O_EXLOCK;
|
||||
do_flock = false;
|
||||
locked_by_open = false;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
FLOGF(error, _(L"Unable to open universal variable file '%s': %s"), path.c_str(),
|
||||
std::strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
assert(fd.valid() && "Should have a valid fd here");
|
||||
|
||||
// Lock if we want to lock and open() didn't do it for us.
|
||||
// If flock fails, give up on locking forever.
|
||||
if (do_flock && !locked_by_open) {
|
||||
if (!flock_uvar_file(fd.fd())) do_flock = false;
|
||||
}
|
||||
|
||||
// Hopefully we got the lock. However, it's possible the file changed out from under us
|
||||
// while we were waiting for the lock. Make sure that didn't happen.
|
||||
if (file_id_for_fd(fd.fd()) != file_id_for_path(path)) {
|
||||
// Oops, it changed! Try again.
|
||||
fd.close();
|
||||
}
|
||||
}
|
||||
|
||||
*out_fd = std::move(fd);
|
||||
return out_fd->valid();
|
||||
}
|
||||
|
||||
// Returns true if modified variables were written, false if not. (There may still be variable
|
||||
// changes due to other processes on a false return).
|
||||
bool env_universal_t::sync(callback_data_list_t &callbacks) {
|
||||
if (!initialized()) return false;
|
||||
|
||||
FLOGF(uvar_file, L"universal log sync");
|
||||
// Our saving strategy:
|
||||
//
|
||||
// 1. Open the file, producing an fd.
|
||||
// 2. Lock the file (may be combined with step 1 on systems with O_EXLOCK)
|
||||
// 3. After taking the lock, check if the file at the given path is different from what we
|
||||
// opened. If so, start over.
|
||||
// 4. Read from the file. This can be elided if its dev/inode is unchanged since the last read
|
||||
// 5. Open an adjacent temporary file
|
||||
// 6. Write our changes to an adjacent file
|
||||
// 7. Move the adjacent file into place via rename. This is assumed to be atomic.
|
||||
// 8. Release the lock and close the file
|
||||
//
|
||||
// Consider what happens if Process 1 and 2 both do this simultaneously. Can there be data loss?
|
||||
// Process 1 opens the file and then attempts to take the lock. Now, either process 1 will see
|
||||
// the original file, or process 2's new file. If it sees the new file, we're OK: it's going to
|
||||
// read from the new file, and so there's no data loss. If it sees the old file, then process 2
|
||||
// must have locked it (if process 1 locks it, switch their roles). The lock will block until
|
||||
// process 2 reaches step 7; at that point process 1 will reach step 2, notice that the file has
|
||||
// changed, and then start over.
|
||||
//
|
||||
// It's possible that the underlying filesystem does not support locks (lockless NFS). In this
|
||||
// case, we risk data loss if two shells try to write their universal variables simultaneously.
|
||||
// In practice this is unlikely, since uvars are usually written interactively.
|
||||
//
|
||||
// Prior versions of fish used a hard link scheme to support file locking on lockless NFS. The
|
||||
// risk here is that if the process crashes or is killed while holding the lock, future
|
||||
// instances of fish will not be able to obtain it. This seems to be a greater risk than that of
|
||||
// data loss on lockless NFS. Users who put their home directory on lockless NFS are playing
|
||||
// with fire anyways.
|
||||
// If we have no changes, just load.
|
||||
if (modified.empty()) {
|
||||
this->load_from_path(narrow_vars_path_, callbacks);
|
||||
FLOGF(uvar_file, L"universal log no modifications");
|
||||
return false;
|
||||
}
|
||||
|
||||
const wcstring directory = wdirname(vars_path_);
|
||||
autoclose_fd_t vars_fd{};
|
||||
|
||||
FLOGF(uvar_file, L"universal log performing full sync");
|
||||
|
||||
// Open the file.
|
||||
if (!this->open_and_acquire_lock(vars_path_, &vars_fd)) {
|
||||
FLOGF(uvar_file, L"universal log open_and_acquire_lock() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read from it.
|
||||
assert(vars_fd.valid());
|
||||
this->load_from_fd(vars_fd.fd(), callbacks);
|
||||
|
||||
if (ok_to_save) {
|
||||
return this->save(directory, vars_path_);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Write our file contents.
|
||||
// \return true on success, false on failure.
|
||||
bool env_universal_t::save(const wcstring &directory, const wcstring &vars_path) {
|
||||
assert(ok_to_save && "It's not OK to save");
|
||||
|
||||
wcstring private_file_path;
|
||||
|
||||
// Open adjacent temporary file.
|
||||
autoclose_fd_t private_fd = this->open_temporary_file(directory, &private_file_path);
|
||||
bool success = private_fd.valid();
|
||||
|
||||
if (!success) FLOGF(uvar_file, L"universal log open_temporary_file() failed");
|
||||
|
||||
// Write to it.
|
||||
if (success) {
|
||||
assert(private_fd.valid());
|
||||
success = this->write_to_fd(private_fd.fd(), private_file_path);
|
||||
if (!success) FLOGF(uvar_file, L"universal log write_to_fd() failed");
|
||||
}
|
||||
|
||||
if (success) {
|
||||
wcstring real_path;
|
||||
if (auto maybe_real_path = wrealpath(vars_path)) {
|
||||
real_path = *maybe_real_path;
|
||||
} else {
|
||||
real_path = vars_path;
|
||||
}
|
||||
|
||||
// Ensure we maintain ownership and permissions (#2176).
|
||||
struct stat sbuf;
|
||||
if (wstat(real_path, &sbuf) >= 0) {
|
||||
if (fchown(private_fd.fd(), sbuf.st_uid, sbuf.st_gid) == -1)
|
||||
FLOGF(uvar_file, L"universal log fchown() failed");
|
||||
if (fchmod(private_fd.fd(), sbuf.st_mode) == -1)
|
||||
FLOGF(uvar_file, L"universal log fchmod() failed");
|
||||
}
|
||||
|
||||
// Linux by default stores the mtime with low precision, low enough that updates that occur
|
||||
// in quick succession may result in the same mtime (even the nanoseconds field). So
|
||||
// manually set the mtime of the new file to a high-precision clock. Note that this is only
|
||||
// necessary because Linux aggressively reuses inodes, causing the ABA problem; on other
|
||||
// platforms we tend to notice the file has changed due to a different inode (or file size!)
|
||||
//
|
||||
// The current time within the Linux kernel is cached, and generally only updated on a timer
|
||||
// interrupt. So if the timer interrupt is running at 10 milliseconds, the cached time will
|
||||
// only be updated once every 10 milliseconds.
|
||||
//
|
||||
// It's probably worth finding a simpler solution to this. The tests ran into this, but it's
|
||||
// unlikely to affect users.
|
||||
#if defined(UVAR_FILE_SET_MTIME_HACK)
|
||||
struct timespec times[2] = {};
|
||||
times[0].tv_nsec = UTIME_OMIT; // don't change ctime
|
||||
if (0 == clock_gettime(CLOCK_REALTIME, ×[1])) {
|
||||
futimens(private_fd.fd(), times);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Apply new file.
|
||||
success = this->move_new_vars_file_into_place(private_file_path, real_path);
|
||||
if (!success) FLOGF(uvar_file, L"universal log move_new_vars_file_into_place() failed");
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Since we moved the new file into place, clear the path so we don't try to unlink it.
|
||||
private_file_path.clear();
|
||||
}
|
||||
|
||||
// Clean up.
|
||||
if (!private_file_path.empty()) {
|
||||
wunlink(private_file_path);
|
||||
}
|
||||
if (success) {
|
||||
// All of our modified variables have now been written out.
|
||||
modified.clear();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
uvar_format_t env_universal_t::read_message_internal(int fd, var_table_t *vars) {
|
||||
// Read everything from the fd. Put a sane limit on it.
|
||||
std::string contents;
|
||||
while (contents.size() < k_max_read_size) {
|
||||
char buffer[4096];
|
||||
ssize_t amt = read_loop(fd, buffer, sizeof buffer);
|
||||
if (amt <= 0) {
|
||||
break;
|
||||
}
|
||||
contents.append(buffer, amt);
|
||||
}
|
||||
|
||||
// Handle overlong files.
|
||||
if (contents.size() >= k_max_read_size) {
|
||||
contents.resize(k_max_read_size);
|
||||
// Back up to a newline.
|
||||
size_t newline = contents.rfind('\n');
|
||||
contents.resize(newline == wcstring::npos ? 0 : newline);
|
||||
}
|
||||
|
||||
return populate_variables(contents, vars);
|
||||
}
|
||||
|
||||
/// \return the format corresponding to file contents \p s.
|
||||
uvar_format_t env_universal_t::format_for_contents(const std::string &s) {
|
||||
// Walk over leading comments, looking for one like '# version'
|
||||
line_iterator_t<std::string> iter{s};
|
||||
while (iter.next()) {
|
||||
const std::string &line = iter.line();
|
||||
if (line.empty()) continue;
|
||||
if (line.front() != L'#') {
|
||||
// Exhausted leading comments.
|
||||
break;
|
||||
}
|
||||
// Note scanf %s is max characters to write; add 1 for null terminator.
|
||||
char versionbuf[64 + 1];
|
||||
if (sscanf(line.c_str(), "# VERSION: %64s", versionbuf) != 1) continue;
|
||||
|
||||
// Try reading the version.
|
||||
if (!std::strcmp(versionbuf, UVARS_VERSION_3_0)) {
|
||||
return uvar_format_t::fish_3_0;
|
||||
} else {
|
||||
// Unknown future version.
|
||||
return uvar_format_t::future;
|
||||
}
|
||||
}
|
||||
// No version found, assume 2.x
|
||||
return uvar_format_t::fish_2_x;
|
||||
}
|
||||
|
||||
uvar_format_t env_universal_t::populate_variables(const std::string &s, var_table_t *out_vars) {
|
||||
// Decide on the format.
|
||||
const uvar_format_t format = format_for_contents(s);
|
||||
|
||||
line_iterator_t<std::string> iter{s};
|
||||
wcstring wide_line;
|
||||
wcstring storage;
|
||||
while (iter.next()) {
|
||||
const std::string &line = iter.line();
|
||||
// Skip empties and constants.
|
||||
if (line.empty() || line.front() == L'#') continue;
|
||||
|
||||
// Convert to UTF8.
|
||||
wide_line.clear();
|
||||
if (!utf8_to_wchar(line.data(), line.size(), &wide_line, 0)) continue;
|
||||
|
||||
switch (format) {
|
||||
case uvar_format_t::fish_2_x:
|
||||
env_universal_t::parse_message_2x_internal(wide_line, out_vars, &storage);
|
||||
break;
|
||||
case uvar_format_t::fish_3_0:
|
||||
// For future formats, just try with the most recent one.
|
||||
case uvar_format_t::future:
|
||||
env_universal_t::parse_message_30_internal(wide_line, out_vars, &storage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
static const wchar_t *skip_spaces(const wchar_t *str) {
|
||||
while (*str == L' ' || *str == L'\t') str++;
|
||||
return str;
|
||||
}
|
||||
|
||||
bool env_universal_t::populate_1_variable(const wchar_t *input, env_var_t::env_var_flags_t flags,
|
||||
var_table_t *vars, wcstring *storage) {
|
||||
const wchar_t *str = skip_spaces(input);
|
||||
const wchar_t *colon = std::wcschr(str, L':');
|
||||
if (!colon) return false;
|
||||
|
||||
// Parse out the value into storage, and decode it into a variable.
|
||||
storage->clear();
|
||||
auto unescaped = unescape_string(colon + 1, 0);
|
||||
if (!unescaped) {
|
||||
return false;
|
||||
}
|
||||
*storage = *unescaped;
|
||||
env_var_t var{decode_serialized(*storage), flags};
|
||||
|
||||
// Parse out the key and write into the map.
|
||||
storage->assign(str, colon - str);
|
||||
const wcstring &key = *storage;
|
||||
(*vars)[key] = std::move(var);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Parse message msg per fish 3.0 format.
|
||||
void env_universal_t::parse_message_30_internal(const wcstring &msgstr, var_table_t *vars,
|
||||
wcstring *storage) {
|
||||
namespace f3 = fish3_uvars;
|
||||
const wchar_t *const msg = msgstr.c_str();
|
||||
if (msg[0] == L'#') return;
|
||||
|
||||
const wchar_t *cursor = msg;
|
||||
if (!match(&cursor, f3::SETUVAR)) {
|
||||
FLOGF(warning, PARSE_ERR, msg);
|
||||
return;
|
||||
}
|
||||
// Parse out flags.
|
||||
env_var_t::env_var_flags_t flags = 0;
|
||||
for (;;) {
|
||||
cursor = skip_spaces(cursor);
|
||||
if (*cursor != L'-') break;
|
||||
if (match(&cursor, f3::EXPORT)) {
|
||||
flags |= env_var_t::flag_export;
|
||||
} else if (match(&cursor, f3::PATH)) {
|
||||
flags |= env_var_t::flag_pathvar;
|
||||
} else {
|
||||
// Skip this unknown flag, for future proofing.
|
||||
while (*cursor && *cursor != L' ' && *cursor != L'\t') cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the variable with these flags.
|
||||
if (!populate_1_variable(cursor, flags, vars, storage)) {
|
||||
FLOGF(warning, PARSE_ERR, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse message msg per fish 2.x format.
|
||||
void env_universal_t::parse_message_2x_internal(const wcstring &msgstr, var_table_t *vars,
|
||||
wcstring *storage) {
|
||||
namespace f2x = fish2x_uvars;
|
||||
const wchar_t *const msg = msgstr.c_str();
|
||||
const wchar_t *cursor = msg;
|
||||
|
||||
if (cursor[0] == L'#') return;
|
||||
|
||||
env_var_t::env_var_flags_t flags = 0;
|
||||
if (match(&cursor, f2x::SET_EXPORT)) {
|
||||
flags |= env_var_t::flag_export;
|
||||
} else if (match(&cursor, f2x::SET)) {
|
||||
flags |= 0;
|
||||
} else {
|
||||
FLOGF(warning, PARSE_ERR, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!populate_1_variable(cursor, flags, vars, storage)) {
|
||||
FLOGF(warning, PARSE_ERR, msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
class universal_notifier_shmem_poller_t final : public universal_notifier_t {
|
||||
#ifdef __CYGWIN__
|
||||
@@ -1420,10 +596,6 @@ bool universal_notifier_t::notification_fd_became_readable(int fd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var_table_ffi_t::var_table_ffi_t(const var_table_t &table) {
|
||||
for (const auto &kv : table) {
|
||||
this->names.push_back(kv.first);
|
||||
this->vars.push_back(kv.second);
|
||||
}
|
||||
void env_universal_notifier_t_default_notifier_post_notification_ffi() {
|
||||
return universal_notifier_t::default_notifier().post_notification();
|
||||
}
|
||||
var_table_ffi_t::~var_table_ffi_t() = default;
|
||||
|
||||
@@ -15,201 +15,9 @@
|
||||
#include "maybe.h"
|
||||
#include "wutil.h"
|
||||
|
||||
/// Callback data, reflecting a change in universal variables.
|
||||
struct callback_data_t {
|
||||
// The name of the variable.
|
||||
wcstring key;
|
||||
|
||||
// The value of the variable, or none if it is erased.
|
||||
maybe_t<env_var_t> val;
|
||||
|
||||
/// Construct from a key and maybe a value.
|
||||
callback_data_t(wcstring k, maybe_t<env_var_t> v) : key(std::move(k)), val(std::move(v)) {}
|
||||
|
||||
/// \return whether this callback represents an erased variable.
|
||||
bool is_erase() const { return !val.has_value(); }
|
||||
};
|
||||
using callback_data_list_t = std::vector<callback_data_t>;
|
||||
|
||||
/// Wrapper type for ffi purposes.
|
||||
struct env_universal_sync_result_t {
|
||||
// List of callbacks.
|
||||
callback_data_list_t list;
|
||||
|
||||
// Return value of sync().
|
||||
bool changed;
|
||||
|
||||
bool get_changed() const { return changed; }
|
||||
|
||||
size_t count() const { return list.size(); }
|
||||
const wcstring &get_key(size_t idx) const { return list.at(idx).key; }
|
||||
bool get_is_erase(size_t idx) const { return list.at(idx).is_erase(); }
|
||||
};
|
||||
|
||||
/// FFI helper to import our var_table into Rust.
|
||||
/// Parallel names of strings and environment variables.
|
||||
struct var_table_ffi_t {
|
||||
std::vector<wcstring> names;
|
||||
std::vector<env_var_t> vars;
|
||||
|
||||
size_t count() const { return names.size(); }
|
||||
const wcstring &get_name(size_t idx) const { return names.at(idx); }
|
||||
const env_var_t &get_var(size_t idx) const { return vars.at(idx); }
|
||||
|
||||
explicit var_table_ffi_t(const var_table_t &table);
|
||||
~var_table_ffi_t();
|
||||
};
|
||||
|
||||
// List of fish universal variable formats.
|
||||
// This is exposed for testing.
|
||||
enum class uvar_format_t { fish_2_x, fish_3_0, future };
|
||||
|
||||
/// Class representing universal variables.
|
||||
class env_universal_t {
|
||||
public:
|
||||
// Construct an empty universal variables.
|
||||
env_universal_t() = default;
|
||||
|
||||
// Construct inside a unique_ptr.
|
||||
static std::unique_ptr<env_universal_t> new_unique() {
|
||||
return std::unique_ptr<env_universal_t>(new env_universal_t());
|
||||
}
|
||||
|
||||
// Get the value of the variable with the specified name.
|
||||
maybe_t<env_var_t> get(const wcstring &name) const;
|
||||
|
||||
// Cover over get() for FFI purposes.
|
||||
std::unique_ptr<env_var_t> get_ffi(const wcstring &name) const;
|
||||
|
||||
// \return flags from the variable with the given name.
|
||||
maybe_t<env_var_t::env_var_flags_t> get_flags(const wcstring &name) const;
|
||||
|
||||
// Sets a variable.
|
||||
void set(const wcstring &key, const env_var_t &var);
|
||||
|
||||
// Removes a variable. Returns true if it was found, false if not.
|
||||
bool remove(const wcstring &key);
|
||||
|
||||
// Gets variable names.
|
||||
std::vector<wcstring> get_names(bool show_exported, bool show_unexported) const;
|
||||
|
||||
// Cover over get_names for FFI.
|
||||
wcstring_list_ffi_t get_names_ffi(bool show_exported, bool show_unexported) const {
|
||||
return get_names(show_exported, show_unexported);
|
||||
}
|
||||
|
||||
/// Get a view on the universal variable table.
|
||||
const var_table_t &get_table() const { return vars; }
|
||||
var_table_ffi_t get_table_ffi() const { return var_table_ffi_t(vars); }
|
||||
|
||||
/// Initialize this uvars for the default path.
|
||||
/// This should be called at most once on any given instance.
|
||||
void initialize(callback_data_list_t &callbacks);
|
||||
|
||||
/// Initialize a this uvars for a given path.
|
||||
/// This is exposed for testing only.
|
||||
void initialize_at_path(callback_data_list_t &callbacks, wcstring path);
|
||||
|
||||
/// FFI helpers.
|
||||
env_universal_sync_result_t initialize_ffi() {
|
||||
env_universal_sync_result_t res{};
|
||||
initialize(res.list);
|
||||
return res;
|
||||
}
|
||||
|
||||
env_universal_sync_result_t initialize_at_path_ffi(wcstring path) {
|
||||
env_universal_sync_result_t res{};
|
||||
initialize_at_path(res.list, std::move(path));
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Reads and writes variables at the correct path. Returns true if modified variables were
|
||||
/// written.
|
||||
bool sync(callback_data_list_t &callbacks);
|
||||
|
||||
/// FFI helper.
|
||||
env_universal_sync_result_t sync_ffi() {
|
||||
callback_data_list_t callbacks;
|
||||
bool changed = sync(callbacks);
|
||||
return env_universal_sync_result_t{std::move(callbacks), changed};
|
||||
}
|
||||
|
||||
/// Populate a variable table \p out_vars from a \p s string.
|
||||
/// This is exposed for testing only.
|
||||
/// \return the format of the file that we read.
|
||||
static uvar_format_t populate_variables(const std::string &s, var_table_t *out_vars);
|
||||
|
||||
/// Guess a file format. Exposed for testing only.
|
||||
static uvar_format_t format_for_contents(const std::string &s);
|
||||
|
||||
/// Serialize a variable list. Exposed for testing only.
|
||||
static std::string serialize_with_vars(const var_table_t &vars);
|
||||
|
||||
/// Exposed for testing only.
|
||||
bool is_ok_to_save() const { return ok_to_save; }
|
||||
|
||||
/// Access the export generation.
|
||||
uint64_t get_export_generation() const { return export_generation; }
|
||||
|
||||
private:
|
||||
// Path that we save to. This is set in initialize(). If empty, initialize has not been called.
|
||||
wcstring vars_path_;
|
||||
std::string narrow_vars_path_;
|
||||
|
||||
// The table of variables.
|
||||
var_table_t vars;
|
||||
|
||||
// Keys that have been modified, and need to be written. A value here that is not present in
|
||||
// vars indicates a deleted value.
|
||||
std::unordered_set<wcstring> modified;
|
||||
|
||||
// A generation count which is incremented every time an exported variable is modified.
|
||||
uint64_t export_generation{1};
|
||||
|
||||
// Whether it's OK to save. This may be set to false if we discover that a future version of
|
||||
// fish wrote the uvars contents.
|
||||
bool ok_to_save{true};
|
||||
|
||||
// If true, attempt to flock the uvars file.
|
||||
// This latches to false if the file is found to be remote, where flock may hang.
|
||||
bool do_flock{true};
|
||||
|
||||
// File id from which we last read.
|
||||
file_id_t last_read_file = kInvalidFileID;
|
||||
|
||||
/// \return whether we are initialized.
|
||||
bool initialized() const { return !vars_path_.empty(); }
|
||||
|
||||
bool load_from_path(const wcstring &path, callback_data_list_t &callbacks);
|
||||
bool load_from_path(const std::string &path, callback_data_list_t &callbacks);
|
||||
|
||||
void load_from_fd(int fd, callback_data_list_t &callbacks);
|
||||
|
||||
// Functions concerned with saving.
|
||||
bool open_and_acquire_lock(const wcstring &path, autoclose_fd_t *out_fd);
|
||||
autoclose_fd_t open_temporary_file(const wcstring &directory, wcstring *out_path);
|
||||
bool write_to_fd(int fd, const wcstring &path);
|
||||
bool move_new_vars_file_into_place(const wcstring &src, const wcstring &dst);
|
||||
|
||||
// Given a variable table, generate callbacks representing the difference between our vars and
|
||||
// the new vars. Also update our exports generation count as necessary.
|
||||
void generate_callbacks_and_update_exports(const var_table_t &new_vars,
|
||||
callback_data_list_t &callbacks);
|
||||
|
||||
// Given a variable table, copy unmodified values into self.
|
||||
void acquire_variables(var_table_t &&vars_to_acquire);
|
||||
|
||||
static bool populate_1_variable(const wchar_t *input, env_var_t::env_var_flags_t flags,
|
||||
var_table_t *vars, wcstring *storage);
|
||||
|
||||
static void parse_message_2x_internal(const wcstring &msg, var_table_t *vars,
|
||||
wcstring *storage);
|
||||
static void parse_message_30_internal(const wcstring &msg, var_table_t *vars,
|
||||
wcstring *storage);
|
||||
static uvar_format_t read_message_internal(int fd, var_table_t *vars);
|
||||
|
||||
bool save(const wcstring &directory, const wcstring &vars_path);
|
||||
};
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "env_universal_common.rs.h"
|
||||
#endif
|
||||
|
||||
/// The "universal notifier" is an object responsible for broadcasting and receiving universal
|
||||
/// variable change notifications. These notifications do not contain the change, but merely
|
||||
@@ -281,4 +89,6 @@ class universal_notifier_t {
|
||||
|
||||
wcstring get_runtime_path();
|
||||
|
||||
void env_universal_notifier_t_default_notifier_post_notification_ffi();
|
||||
|
||||
#endif
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
#include "wcstringutil.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
void event_fire_generic(parser_t &parser, const wcstring &name, const std::vector<wcstring> &args) {
|
||||
std::vector<wcharz_t> ffi_args;
|
||||
for (const auto &arg : args) ffi_args.push_back(arg.c_str());
|
||||
void event_fire_generic(const parser_t &parser, const wcstring &name,
|
||||
const std::vector<wcstring> &args) {
|
||||
wcstring_list_ffi_t ffi_args;
|
||||
for (const auto &arg : args) ffi_args.push(arg);
|
||||
event_fire_generic_ffi(parser, name, ffi_args);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
#include "common.h"
|
||||
#include "cxx.h"
|
||||
#include "global_safety.h"
|
||||
#include "parser.h"
|
||||
#include "wutil.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "event.rs.h"
|
||||
#else
|
||||
@@ -32,9 +32,7 @@ struct Event;
|
||||
// TODO: Remove after porting functions.cpp
|
||||
extern const wchar_t *const event_filter_names[];
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
void event_fire_generic(parser_t &parser, const wcstring &name,
|
||||
void event_fire_generic(const parser_t &parser, const wcstring &name,
|
||||
const std::vector<wcstring> &args = g_empty_string_list);
|
||||
|
||||
#endif
|
||||
|
||||
1328
src/exec.cpp
1328
src/exec.cpp
File diff suppressed because it is too large
Load Diff
52
src/exec.h
52
src/exec.h
@@ -1,51 +1,3 @@
|
||||
// Prototypes for functions for executing a program.
|
||||
#ifndef FISH_EXEC_H
|
||||
#define FISH_EXEC_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
|
||||
#include "flog.h"
|
||||
#include "io.h"
|
||||
#include "proc.h"
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
/// Execute the processes specified by \p j in the parser \p.
|
||||
/// On a true return, the job was successfully launched and the parser will take responsibility for
|
||||
/// cleaning it up. On a false return, the job could not be launched and the caller must clean it
|
||||
/// up.
|
||||
__warn_unused bool exec_job(parser_t &parser, const std::shared_ptr<job_t> &j,
|
||||
const io_chain_t &block_io);
|
||||
|
||||
/// Evaluate a command.
|
||||
///
|
||||
/// \param cmd the command to execute
|
||||
/// \param parser the parser with which to execute code
|
||||
/// \param outputs the list to insert output into.
|
||||
/// \param apply_exit_status if set, update $status within the parser, otherwise do not.
|
||||
///
|
||||
/// \return a value appropriate for populating $status.
|
||||
int exec_subshell(const wcstring &cmd, parser_t &parser, bool apply_exit_status);
|
||||
int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector<wcstring> &outputs,
|
||||
bool apply_exit_status);
|
||||
int exec_subshell_ffi(const wcstring &cmd, parser_t &parser, wcstring_list_ffi_t &outputs,
|
||||
bool apply_exit_status);
|
||||
|
||||
/// Like exec_subshell, but only returns expansion-breaking errors. That is, a zero return means
|
||||
/// "success" (even though the command may have failed), a non-zero return means that we should
|
||||
/// halt expansion. If the \p pgid is supplied, then any spawned external commands should join that
|
||||
/// pgroup.
|
||||
int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser,
|
||||
const job_group_ref_t &job_group, std::vector<wcstring> &outputs);
|
||||
|
||||
/// Add signals that should be masked for external processes in this job.
|
||||
bool blocked_signals_for_job(const job_t &job, sigset_t *sigmask);
|
||||
|
||||
/// This function checks the beginning of a file to see if it's safe to
|
||||
/// pass to the system interpreter when execve() returns ENOEXEC.
|
||||
bool is_thompson_shell_script(const char *path);
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "exec.rs.h"
|
||||
#endif
|
||||
|
||||
1296
src/expand.cpp
1296
src/expand.cpp
File diff suppressed because it is too large
Load Diff
154
src/expand.h
154
src/expand.h
@@ -14,64 +14,51 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "enum_set.h"
|
||||
#include "env.h"
|
||||
#include "maybe.h"
|
||||
#include "operation_context.h"
|
||||
#include "parse_constants.h"
|
||||
|
||||
class env_var_t;
|
||||
class environment_t;
|
||||
class operation_context_t;
|
||||
|
||||
/// Set of flags controlling expansions.
|
||||
enum class expand_flag {
|
||||
enum expand_flag {
|
||||
/// Skip command substitutions.
|
||||
skip_cmdsubst,
|
||||
skip_cmdsubst = 1 << 0,
|
||||
/// Skip variable expansion.
|
||||
skip_variables,
|
||||
skip_variables = 1 << 1,
|
||||
/// Skip wildcard expansion.
|
||||
skip_wildcards,
|
||||
skip_wildcards = 1 << 2,
|
||||
/// The expansion is being done for tab or auto completions. Returned completions may have the
|
||||
/// wildcard as a prefix instead of a match.
|
||||
for_completions,
|
||||
for_completions = 1 << 3,
|
||||
/// Only match files that are executable by the current user.
|
||||
executables_only,
|
||||
executables_only = 1 << 4,
|
||||
/// Only match directories.
|
||||
directories_only,
|
||||
directories_only = 1 << 5,
|
||||
/// Generate descriptions, stored in the description field of completions.
|
||||
gen_descriptions,
|
||||
gen_descriptions = 1 << 6,
|
||||
/// Un-expand home directories to tildes after.
|
||||
preserve_home_tildes,
|
||||
preserve_home_tildes = 1 << 7,
|
||||
/// Allow fuzzy matching.
|
||||
fuzzy_match,
|
||||
fuzzy_match = 1 << 8,
|
||||
/// Disallow directory abbreviations like /u/l/b for /usr/local/bin. Only applicable if
|
||||
/// fuzzy_match is set.
|
||||
no_fuzzy_directories,
|
||||
no_fuzzy_directories = 1 << 9,
|
||||
/// Allows matching a leading dot even if the wildcard does not contain one.
|
||||
/// By default, wildcards only match a leading dot literally; this is why e.g. '*' does not
|
||||
/// match hidden files.
|
||||
allow_nonliteral_leading_dot,
|
||||
allow_nonliteral_leading_dot = 1 << 10,
|
||||
/// Do expansions specifically to support cd. This means using CDPATH as a list of potential
|
||||
/// working directories, and to use logical instead of physical paths.
|
||||
special_for_cd,
|
||||
special_for_cd = 1 << 11,
|
||||
/// Do expansions specifically for cd autosuggestion. This is to differentiate between cd
|
||||
/// completions and cd autosuggestions.
|
||||
special_for_cd_autosuggestion,
|
||||
special_for_cd_autosuggestion = 1 << 12,
|
||||
/// Do expansions specifically to support external command completions. This means using PATH as
|
||||
/// a list of potential working directories.
|
||||
special_for_command,
|
||||
|
||||
COUNT,
|
||||
special_for_command = 1 << 13,
|
||||
};
|
||||
|
||||
template <>
|
||||
struct enum_info_t<expand_flag> {
|
||||
static constexpr auto count = expand_flag::COUNT;
|
||||
};
|
||||
|
||||
using expand_flags_t = enum_set_t<expand_flag>;
|
||||
|
||||
class completion_t;
|
||||
using completion_list_t = std::vector<completion_t>;
|
||||
class completion_receiver_t;
|
||||
using expand_flags_t = uint64_t;
|
||||
|
||||
enum : wchar_t {
|
||||
/// Character representing a home directory.
|
||||
@@ -100,111 +87,16 @@ enum : wchar_t {
|
||||
EXPAND_SENTINEL
|
||||
};
|
||||
|
||||
/// These are the possible return values for expand_string.
|
||||
struct expand_result_t {
|
||||
enum result_t {
|
||||
/// There was an error, for example, unmatched braces.
|
||||
error,
|
||||
/// Expansion succeeded.
|
||||
ok,
|
||||
/// Expansion was cancelled (e.g. control-C).
|
||||
cancel,
|
||||
/// Expansion succeeded, but a wildcard in the string matched no files,
|
||||
/// so the output is empty.
|
||||
wildcard_no_match,
|
||||
};
|
||||
|
||||
/// The result of expansion.
|
||||
result_t result;
|
||||
|
||||
/// If expansion resulted in an error, this is an appropriate value with which to populate
|
||||
/// $status.
|
||||
int status{0};
|
||||
|
||||
/* implicit */ expand_result_t(result_t result) : result(result) {}
|
||||
|
||||
/// operator== allows for comparison against result_t values.
|
||||
bool operator==(result_t rhs) const { return result == rhs; }
|
||||
bool operator!=(result_t rhs) const { return !(*this == rhs); }
|
||||
|
||||
/// Make an error value with the given status.
|
||||
static expand_result_t make_error(int status) {
|
||||
assert(status != 0 && "status cannot be 0 for an error result");
|
||||
expand_result_t result(error);
|
||||
result.status = status;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/// The string represented by PROCESS_EXPAND_SELF
|
||||
#define PROCESS_EXPAND_SELF_STR L"%self"
|
||||
#define PROCESS_EXPAND_SELF_STR_LEN 5
|
||||
|
||||
/// Perform various forms of expansion on in, such as tilde expansion (\~USER becomes the users home
|
||||
/// directory), variable expansion (\$VAR_NAME becomes the value of the environment variable
|
||||
/// VAR_NAME), cmdsubst expansion and wildcard expansion. The results are inserted into the list
|
||||
/// out.
|
||||
///
|
||||
/// If the parameter does not need expansion, it is copied into the list out.
|
||||
///
|
||||
/// \param input The parameter to expand
|
||||
/// \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 skip_cmdsubst skip_variables and skip_wildcards
|
||||
/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may
|
||||
/// be null. \param errors Resulting errors, or nullptr to ignore
|
||||
///
|
||||
/// \return An expand_result_t.
|
||||
/// wildcard_no_match and wildcard_match are normal exit conditions used only on
|
||||
/// strings containing wildcards to tell if the wildcard produced any matches.
|
||||
__warn_unused expand_result_t expand_string(wcstring input, completion_list_t *output,
|
||||
expand_flags_t flags, const operation_context_t &ctx,
|
||||
parse_error_list_t *errors = nullptr);
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "expand.rs.h"
|
||||
using expand_result_t = ExpandResult;
|
||||
#endif
|
||||
|
||||
/// Variant of string that inserts its results into a completion_receiver_t.
|
||||
__warn_unused expand_result_t expand_string(wcstring input, completion_receiver_t *output,
|
||||
expand_flags_t flags, const operation_context_t &ctx,
|
||||
parse_error_list_t *errors = nullptr);
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// \param inout_str The parameter to expand in-place
|
||||
/// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination
|
||||
/// of skip_cmdsubst skip_variables and skip_wildcards
|
||||
/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may be
|
||||
/// null. \param errors Resulting errors, or nullptr to ignore
|
||||
///
|
||||
/// \return Whether expansion succeeded.
|
||||
bool expand_one(wcstring &string, expand_flags_t flags, const operation_context_t &ctx,
|
||||
parse_error_list_t *errors = nullptr);
|
||||
|
||||
/// Expand a command string like $HOME/bin/cmd into a command and list of arguments.
|
||||
/// Return the command and arguments by reference.
|
||||
/// If the expansion resulted in no or an empty command, the command will be an empty string. Note
|
||||
/// that API does not distinguish between expansion resulting in an empty command (''), and
|
||||
/// expansion resulting in no command (e.g. unset variable).
|
||||
/// If \p skip_wildcards is true, then do not do wildcard expansion
|
||||
/// \return an expand error.
|
||||
expand_result_t expand_to_command_and_args(const wcstring &instr, const operation_context_t &ctx,
|
||||
wcstring *out_cmd, std::vector<wcstring> *out_args,
|
||||
parse_error_list_t *errors = nullptr,
|
||||
bool skip_wildcards = false);
|
||||
|
||||
/// Convert the variable value to a human readable form, i.e. escape things, handle arrays, etc.
|
||||
/// Suitable for pretty-printing.
|
||||
wcstring expand_escape_variable(const env_var_t &var);
|
||||
|
||||
/// Convert a string value to a human readable form, i.e. escape things, handle arrays, etc.
|
||||
/// Suitable for pretty-printing.
|
||||
wcstring expand_escape_string(const wcstring &el);
|
||||
|
||||
/// Perform tilde expansion and nothing else on the specified string, which is modified in place.
|
||||
///
|
||||
/// \param input the string to tilde expand
|
||||
void expand_tilde(wcstring &input, const environment_t &vars);
|
||||
|
||||
/// Perform the opposite of tilde expansion on the string, which is modified in place.
|
||||
wcstring replace_home_directory_with_tilde(const wcstring &str, const environment_t &vars);
|
||||
void expand_tilde(wcstring &input, const env_stack_t &vars);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
#include "builtin.h"
|
||||
#include "builtins/bind.h"
|
||||
#include "builtins/commandline.h"
|
||||
#include "builtins/read.h"
|
||||
#include "builtins/ulimit.h"
|
||||
#include "event.h"
|
||||
#include "fds.h"
|
||||
#include "fish_indent_common.h"
|
||||
#include "highlight.h"
|
||||
#include "input.h"
|
||||
#include "parse_util.h"
|
||||
#include "reader.h"
|
||||
#include "screen.h"
|
||||
|
||||
// Symbols that get autocxx bindings but are not used in a given binary, will cause "undefined
|
||||
// reference" when trying to link that binary. Work around this by marking them as used in
|
||||
// all binaries.
|
||||
void mark_as_used() {
|
||||
//
|
||||
void mark_as_used(const parser_t& parser, env_stack_t& env_stack) {
|
||||
wcstring s;
|
||||
|
||||
escape_code_length_ffi({});
|
||||
event_fire_generic(parser, {});
|
||||
event_fire_generic(parser, {}, {});
|
||||
expand_tilde(s, env_stack);
|
||||
get_history_variable_text_ffi({});
|
||||
highlight_spec_t{};
|
||||
init_input();
|
||||
make_pipes_ffi();
|
||||
pretty_printer_t({}, {});
|
||||
reader_change_cursor_selection_mode(cursor_selection_mode_t::exclusive);
|
||||
reader_change_history({});
|
||||
reader_read_ffi({}, {}, {});
|
||||
reader_schedule_prompt_repaint();
|
||||
reader_set_autosuggestion_enabled_ffi({});
|
||||
reader_status_count();
|
||||
restore_term_mode();
|
||||
rgb_color_t{};
|
||||
screen_clear_layout_cache_ffi();
|
||||
screen_set_midnight_commander_hack();
|
||||
setenv_lock({}, {}, {});
|
||||
set_inheriteds_ffi();
|
||||
term_copy_modes();
|
||||
unsetenv_lock({});
|
||||
|
||||
builtin_bind({}, {}, {});
|
||||
builtin_commandline({}, {}, {});
|
||||
builtin_read({}, {}, {});
|
||||
builtin_ulimit({}, {}, {});
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
#include "common.h"
|
||||
#include "fish.rs.h"
|
||||
#include "ffi_baggage.h"
|
||||
#include "fish.rs.h"
|
||||
|
||||
int main() {
|
||||
program_name = L"fish";
|
||||
|
||||
@@ -129,9 +129,8 @@ static const char *highlight_role_to_string(highlight_role_t role) {
|
||||
// 3,7,command
|
||||
static std::string make_pygments_csv(const wcstring &src) {
|
||||
const size_t len = src.size();
|
||||
std::vector<highlight_spec_t> colors;
|
||||
highlight_shell(src, colors, operation_context_t::globals());
|
||||
assert(colors.size() == len && "Colors and src should have same size");
|
||||
auto colors = highlight_shell_ffi(src, *operation_context_globals(), false, {});
|
||||
assert(colors->size() == len && "Colors and src should have same size");
|
||||
|
||||
struct token_range_t {
|
||||
unsigned long start;
|
||||
@@ -141,7 +140,7 @@ static std::string make_pygments_csv(const wcstring &src) {
|
||||
|
||||
std::vector<token_range_t> token_ranges;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
highlight_role_t role = colors.at(i).foreground;
|
||||
highlight_role_t role = colors->at(i).foreground;
|
||||
// See if we can extend the last range.
|
||||
if (!token_ranges.empty()) {
|
||||
auto &last = token_ranges.back();
|
||||
@@ -183,7 +182,7 @@ static wcstring prettify(const wcstring &src, bool do_indent) {
|
||||
/// for the various colors.
|
||||
static const wchar_t *html_class_name_for_color(highlight_spec_t spec) {
|
||||
#define P(x) L"fish_color_" #x
|
||||
switch (spec.foreground) {
|
||||
switch (spec->foreground) {
|
||||
case highlight_role_t::normal: {
|
||||
return P(normal);
|
||||
}
|
||||
@@ -450,8 +449,10 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
// Maybe colorize.
|
||||
std::vector<highlight_spec_t> colors;
|
||||
maybe_t<rust::Box<HighlightSpecListFFI>> ffi_colors;
|
||||
if (output_type != output_type_plain_text) {
|
||||
highlight_shell(output_wtext, colors, operation_context_t::globals());
|
||||
highlight_shell(output_wtext, colors, *operation_context_globals());
|
||||
ffi_colors = highlight_shell_ffi(output_wtext, *operation_context_globals(), false, {});
|
||||
}
|
||||
|
||||
std::string colored_output;
|
||||
@@ -473,7 +474,11 @@ int main(int argc, char *argv[]) {
|
||||
break;
|
||||
}
|
||||
case output_type_ansi: {
|
||||
colored_output = colorize(output_wtext, colors, env_stack_t::globals());
|
||||
auto ffi_colored =
|
||||
colorize(output_wtext, **ffi_colors, env_stack_t::globals().get_impl_ffi());
|
||||
for (uint8_t c : ffi_colored) {
|
||||
colored_output.push_back(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case output_type_html: {
|
||||
|
||||
@@ -252,8 +252,9 @@ wcstring pretty_printer_t::clean_text(const wcstring &input) {
|
||||
// Unescape the string - this leaves special markers around if there are any
|
||||
// expansions or anything. We specifically tell it to not compute backslash-escapes
|
||||
// like \U or \x, because we want to leave them intact.
|
||||
wcstring unescaped = input;
|
||||
unescape_string_in_place(&unescaped, UNESCAPE_SPECIAL | UNESCAPE_NO_BACKSLASHES);
|
||||
wcstring unescaped =
|
||||
*unescape_string(input.c_str(), input.size(), UNESCAPE_SPECIAL | UNESCAPE_NO_BACKSLASHES,
|
||||
STRING_STYLE_SCRIPT);
|
||||
|
||||
// Remove INTERNAL_SEPARATOR because that's a quote.
|
||||
auto quote = [](wchar_t ch) { return ch == INTERNAL_SEPARATOR; };
|
||||
|
||||
@@ -279,8 +279,9 @@ static void process_input(bool continuous_mode, bool verbose) {
|
||||
rust_init();
|
||||
rust_env_init(true);
|
||||
reader_init();
|
||||
parser_t &parser = parser_t::principal_parser();
|
||||
scoped_push<bool> interactive{&parser.libdata().is_interactive, true};
|
||||
auto parser_box = parser_principal_parser();
|
||||
const parser_t &parser = parser_box->deref();
|
||||
scoped_push<bool> interactive{&parser.libdata_pods_mut().is_interactive, true};
|
||||
signal_set_handlers(true);
|
||||
// We need to set the shell-modes for ICRNL,
|
||||
// in fish-proper this is done once a command is run.
|
||||
|
||||
2527
src/fish_tests.cpp
2527
src/fish_tests.cpp
File diff suppressed because it is too large
Load Diff
@@ -110,6 +110,8 @@ class category_list_t {
|
||||
category_t screen{L"screen", L"Screen repaints"};
|
||||
|
||||
category_t abbrs{L"abbrs", L"Abbreviation expansion"};
|
||||
|
||||
category_t refcell{L"refcell", L"Refcell dynamic borrowing"};
|
||||
};
|
||||
|
||||
/// The class responsible for logging.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Functions for storing and retrieving function information. These functions also take care of
|
||||
// autoloading functions in the $fish_function_path. Actual function evaluation is taken care of by
|
||||
// the parser and to some degree the builtin handling library.
|
||||
//
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "function.h"
|
||||
|
||||
#include "common.h"
|
||||
|
||||
maybe_t<rust::Box<function_properties_t>> function_get_props(const wcstring &name) {
|
||||
if (auto *ptr = function_get_props_raw(name)) {
|
||||
return rust::Box<function_properties_t>::from_raw(ptr);
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
maybe_t<rust::Box<function_properties_t>> function_get_props_autoload(const wcstring &name,
|
||||
parser_t &parser) {
|
||||
if (auto *ptr = function_get_props_autoload_raw(name, parser)) {
|
||||
return rust::Box<function_properties_t>::from_raw(ptr);
|
||||
}
|
||||
return none();
|
||||
}
|
||||
@@ -1,21 +1,3 @@
|
||||
// Prototypes for functions for storing and retrieving function information. These functions also
|
||||
// take care of autoloading functions in the $fish_function_path. Actual function evaluation is
|
||||
// taken care of by the parser and to some degree the builtin handling library.
|
||||
#ifndef FISH_FUNCTION_H
|
||||
#define FISH_FUNCTION_H
|
||||
|
||||
#include "cxx.h"
|
||||
#include "maybe.h"
|
||||
|
||||
struct function_properties_t;
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "function.rs.h"
|
||||
#endif
|
||||
|
||||
maybe_t<rust::Box<function_properties_t>> function_get_props(const wcstring &name);
|
||||
maybe_t<rust::Box<function_properties_t>> function_get_props_autoload(const wcstring &name,
|
||||
parser_t &parser);
|
||||
|
||||
#endif
|
||||
|
||||
1339
src/highlight.cpp
1339
src/highlight.cpp
File diff suppressed because it is too large
Load Diff
229
src/highlight.h
229
src/highlight.h
@@ -14,221 +14,48 @@
|
||||
#include "ast.h"
|
||||
#include "color.h"
|
||||
#include "cxx.h"
|
||||
#include "env.h"
|
||||
#include "flog.h"
|
||||
#include "history.h"
|
||||
#include "maybe.h"
|
||||
#include "operation_context.h"
|
||||
#include "parser.h"
|
||||
|
||||
struct Highlighter;
|
||||
struct highlight_spec_t;
|
||||
|
||||
class environment_t;
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "highlight.rs.h"
|
||||
#else
|
||||
struct HighlightSpec;
|
||||
enum class HighlightRole : uint8_t;
|
||||
#endif
|
||||
|
||||
/// Describes the role of a span of text.
|
||||
enum class highlight_role_t : uint8_t {
|
||||
normal = 0, // normal text
|
||||
error, // error
|
||||
command, // command
|
||||
keyword,
|
||||
statement_terminator, // process separator
|
||||
param, // command parameter (argument)
|
||||
option, // argument starting with "-", up to a "--"
|
||||
comment, // comment
|
||||
search_match, // search match
|
||||
operat, // operator
|
||||
escape, // escape sequences
|
||||
quote, // quoted string
|
||||
redirection, // redirection
|
||||
autosuggestion, // autosuggestion
|
||||
selection,
|
||||
using highlight_role_t = HighlightRole;
|
||||
// using highlight_spec_t = HighlightSpec;
|
||||
|
||||
// Pager support.
|
||||
// NOTE: pager.cpp relies on these being in this order.
|
||||
pager_progress,
|
||||
pager_background,
|
||||
pager_prefix,
|
||||
pager_completion,
|
||||
pager_description,
|
||||
pager_secondary_background,
|
||||
pager_secondary_prefix,
|
||||
pager_secondary_completion,
|
||||
pager_secondary_description,
|
||||
pager_selected_background,
|
||||
pager_selected_prefix,
|
||||
pager_selected_completion,
|
||||
pager_selected_description,
|
||||
};
|
||||
|
||||
/// Simply value type describing how a character should be highlighted..
|
||||
struct highlight_spec_t {
|
||||
highlight_role_t foreground{highlight_role_t::normal};
|
||||
highlight_role_t background{highlight_role_t::normal};
|
||||
bool valid_path{false};
|
||||
bool force_underline{false};
|
||||
rust::Box<HighlightSpec> val;
|
||||
|
||||
highlight_spec_t() = default;
|
||||
highlight_spec_t();
|
||||
highlight_spec_t(const highlight_spec_t &other);
|
||||
highlight_spec_t &operator=(const highlight_spec_t &other);
|
||||
|
||||
/* implicit */ highlight_spec_t(highlight_role_t fg,
|
||||
highlight_role_t bg = highlight_role_t::normal)
|
||||
: foreground(fg), background(bg) {}
|
||||
highlight_spec_t(HighlightSpec other);
|
||||
highlight_spec_t(highlight_role_t fg, highlight_role_t bg = {});
|
||||
|
||||
bool operator==(const highlight_spec_t &rhs) const {
|
||||
return foreground == rhs.foreground && background == rhs.background &&
|
||||
valid_path == rhs.valid_path && force_underline == rhs.force_underline;
|
||||
}
|
||||
bool operator==(const highlight_spec_t &other) const;
|
||||
bool operator!=(const highlight_spec_t &other) const;
|
||||
|
||||
bool operator!=(const highlight_spec_t &rhs) const { return !(*this == rhs); }
|
||||
HighlightSpec *operator->() { return &*this->val; }
|
||||
const HighlightSpec *operator->() const { return &*this->val; }
|
||||
|
||||
static highlight_spec_t make_background(highlight_role_t bg_role) {
|
||||
return highlight_spec_t{highlight_role_t::normal, bg_role};
|
||||
}
|
||||
operator HighlightSpec() const;
|
||||
|
||||
static highlight_spec_t make_background(highlight_role_t bg_role);
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<highlight_spec_t> {
|
||||
std::size_t operator()(const highlight_spec_t &v) const {
|
||||
const size_t vals[4] = {static_cast<uint32_t>(v.foreground),
|
||||
static_cast<uint32_t>(v.background), v.valid_path,
|
||||
v.force_underline};
|
||||
return (vals[0] << 0) + (vals[1] << 6) + (vals[2] << 12) + (vals[3] << 18);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
class history_item_t;
|
||||
class operation_context_t;
|
||||
|
||||
/// Given a string and list of colors of the same size, return the string with ANSI escape sequences
|
||||
/// representing the colors.
|
||||
std::string colorize(const wcstring &text, const std::vector<highlight_spec_t> &colors,
|
||||
const environment_t &vars);
|
||||
|
||||
/// Perform syntax highlighting for the shell commands in buff. The result is stored in the color
|
||||
/// array as a color_code from the HIGHLIGHT_ enum for each character in buff.
|
||||
///
|
||||
/// \param buffstr The buffer on which to perform syntax highlighting
|
||||
/// \param color The array in which to store the color codes. The first 8 bits are used for fg
|
||||
/// color, the next 8 bits for bg color.
|
||||
/// \param ctx The variables and cancellation check for this operation.
|
||||
/// \param io_ok If set, allow IO which may block. This means that e.g. invalid commands may be
|
||||
/// detected.
|
||||
/// \param cursor The position of the cursor in the commandline.
|
||||
void highlight_shell(const wcstring &buffstr, std::vector<highlight_spec_t> &color,
|
||||
void highlight_shell(const wcstring &buff, std::vector<highlight_spec_t> &colors,
|
||||
const operation_context_t &ctx, bool io_ok = false,
|
||||
maybe_t<size_t> cursor = {});
|
||||
|
||||
class Parser; using parser_t = Parser;
|
||||
/// Wrapper around colorize(highlight_shell)
|
||||
wcstring colorize_shell(const wcstring &text, parser_t &parser);
|
||||
|
||||
/// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors.
|
||||
/// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be
|
||||
/// one screen redraw.
|
||||
struct highlight_color_resolver_t {
|
||||
/// \return an RGB color for a given highlight spec.
|
||||
rgb_color_t resolve_spec(const highlight_spec_t &highlight, bool is_background,
|
||||
const environment_t &vars);
|
||||
|
||||
private:
|
||||
std::unordered_map<highlight_spec_t, rgb_color_t> fg_cache_;
|
||||
std::unordered_map<highlight_spec_t, rgb_color_t> bg_cache_;
|
||||
rgb_color_t resolve_spec_uncached(const highlight_spec_t &highlight, bool is_background,
|
||||
const environment_t &vars) const;
|
||||
};
|
||||
|
||||
/// Given an item \p item from the history which is a proposed autosuggestion, return whether the
|
||||
/// autosuggestion is valid. It may not be valid if e.g. it is attempting to cd into a directory
|
||||
/// which does not exist.
|
||||
bool autosuggest_validate_from_history(const history_item_t &item,
|
||||
const wcstring &working_directory,
|
||||
const operation_context_t &ctx);
|
||||
|
||||
// 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).
|
||||
// This does I/O!
|
||||
//
|
||||
// This is used only internally to this file, and is exposed only for testing.
|
||||
enum {
|
||||
// The path must be to a directory.
|
||||
PATH_REQUIRE_DIR = 1 << 0,
|
||||
// Expand any leading tilde in the path.
|
||||
PATH_EXPAND_TILDE = 1 << 1,
|
||||
// Normalize directories before resolving, as "cd".
|
||||
PATH_FOR_CD = 1 << 2,
|
||||
};
|
||||
typedef unsigned int path_flags_t;
|
||||
bool is_potential_path(const wcstring &potential_path_fragment, bool at_cursor,
|
||||
const std::vector<wcstring> &directories, const operation_context_t &ctx,
|
||||
path_flags_t flags);
|
||||
|
||||
/// Syntax highlighter helper.
|
||||
class highlighter_t {
|
||||
// The string we're highlighting. Note this is a reference member variable (to avoid copying)!
|
||||
// We must not outlive this!
|
||||
const wcstring &buff;
|
||||
// The position of the cursor within the string.
|
||||
const maybe_t<size_t> cursor;
|
||||
// The operation context. Again, a reference member variable!
|
||||
const operation_context_t &ctx;
|
||||
// Whether it's OK to do I/O.
|
||||
const bool io_ok;
|
||||
// Working directory.
|
||||
const wcstring working_directory;
|
||||
// The ast we produced.
|
||||
rust::Box<Ast> ast;
|
||||
rust::Box<Highlighter> highlighter;
|
||||
// The resulting colors.
|
||||
using color_array_t = std::vector<highlight_spec_t>;
|
||||
color_array_t color_array;
|
||||
// A stack of variables that the current commandline probably defines. We mark redirections
|
||||
// as valid if they use one of these variables, to avoid marking valid targets as error.
|
||||
std::vector<wcstring> pending_variables;
|
||||
|
||||
// Flags we use for AST parsing.
|
||||
static constexpr parse_tree_flags_t ast_flags =
|
||||
parse_flag_continue_after_error | parse_flag_include_comments |
|
||||
parse_flag_accept_incomplete_tokens | parse_flag_leave_unterminated |
|
||||
parse_flag_show_extra_semis;
|
||||
|
||||
bool io_still_ok() const;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
// Declaring methods with forward-declared opaque Rust types like "ast::node_t" will cause
|
||||
// undefined reference errors.
|
||||
// Color a command.
|
||||
void color_command(const ast::string_t &node);
|
||||
// Color a node as if it were an argument.
|
||||
void color_as_argument(const ast::node_t &node, bool options_allowed = true);
|
||||
// Colors the source range of a node with a given color.
|
||||
void color_node(const ast::node_t &node, highlight_spec_t color);
|
||||
// Colors a range with a given color.
|
||||
void color_range(source_range_t range, highlight_spec_t color);
|
||||
#endif
|
||||
|
||||
public:
|
||||
/// \return a substring of our buffer.
|
||||
wcstring get_source(source_range_t r) const;
|
||||
|
||||
// AST visitor implementations.
|
||||
void visit_keyword(const ast::node_t *kw);
|
||||
void visit_token(const ast::node_t *tok);
|
||||
void visit_argument(const void *arg, bool cmd_is_cd, bool options_allowed);
|
||||
void visit_redirection(const void *redir);
|
||||
void visit_variable_assignment(const void *varas);
|
||||
void visit_semi_nl(const ast::node_t *semi_nl);
|
||||
void visit_decorated_statement(const void *stmt);
|
||||
size_t visit_block_statement1(const void *block);
|
||||
void visit_block_statement2(size_t pending_variables_count);
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
// Visit an argument, perhaps knowing that our command is cd.
|
||||
void visit(const ast::argument_t &arg, bool cmd_is_cd = false, bool options_allowed = true);
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
highlighter_t(const wcstring &str, maybe_t<size_t> cursor, const operation_context_t &ctx,
|
||||
wcstring wd, bool can_do_io);
|
||||
|
||||
// Perform highlighting, returning an array of colors.
|
||||
color_array_t highlight();
|
||||
};
|
||||
std::shared_ptr<size_t> cursor = {});
|
||||
|
||||
#endif
|
||||
|
||||
1589
src/history.cpp
1589
src/history.cpp
File diff suppressed because it is too large
Load Diff
319
src/history.h
319
src/history.h
@@ -20,211 +20,29 @@
|
||||
#include "maybe.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
class IoStreams; using io_streams_t = IoStreams;
|
||||
class env_stack_t;
|
||||
class environment_t;
|
||||
class operation_context_t;
|
||||
|
||||
/**
|
||||
Fish supports multiple shells writing to history at once. Here is its strategy:
|
||||
|
||||
1. All history files are append-only. Data, once written, is never modified.
|
||||
|
||||
2. A history file may be re-written ("vacuumed"). This involves reading in the file and writing a
|
||||
new one, while performing maintenance tasks: discarding items in an LRU fashion until we reach
|
||||
the desired maximum count, removing duplicates, and sorting them by timestamp (eventually, not
|
||||
implemented yet). The new file is atomically moved into place via rename().
|
||||
|
||||
3. History files are mapped in via mmap(). Before the file is mapped, the file takes a fcntl read
|
||||
lock. The purpose of this lock is to avoid seeing a transient state where partial data has been
|
||||
written to the file.
|
||||
|
||||
4. History is appended to under a fcntl write lock.
|
||||
|
||||
5. The chaos_mode boolean can be set to true to do things like lower buffer sizes which can
|
||||
trigger race conditions. This is useful for testing.
|
||||
*/
|
||||
using path_list_t = std::vector<wcstring>;
|
||||
|
||||
enum class history_search_type_t {
|
||||
/// Search for commands exactly matching the given string.
|
||||
exact,
|
||||
/// Search for commands containing the given string.
|
||||
contains,
|
||||
/// Search for commands starting with the given string.
|
||||
prefix,
|
||||
/// Search for commands containing the given glob pattern.
|
||||
contains_glob,
|
||||
/// Search for commands starting with the given glob pattern.
|
||||
prefix_glob,
|
||||
/// Search for commands containing the given string as a subsequence
|
||||
contains_subsequence,
|
||||
/// Matches everything.
|
||||
match_everything,
|
||||
};
|
||||
|
||||
using history_identifier_t = uint64_t;
|
||||
|
||||
/// Ways that a history item may be written to disk (or omitted).
|
||||
enum class history_persistence_mode_t : uint8_t {
|
||||
disk, // the history item is written to disk normally
|
||||
memory, // the history item is stored in-memory only, not written to disk
|
||||
ephemeral, // the history item is stored in-memory and deleted when a new item is added
|
||||
};
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
|
||||
class history_item_t {
|
||||
public:
|
||||
/// Construct from a text, timestamp, and optional identifier.
|
||||
/// If \p persist_mode is ::ephemeral, then do not write this item to disk.
|
||||
explicit history_item_t(
|
||||
wcstring str = {}, time_t when = 0, history_identifier_t ident = 0,
|
||||
history_persistence_mode_t persist_mode = history_persistence_mode_t::disk);
|
||||
#include "history.rs.h"
|
||||
|
||||
/// \return the text as a string.
|
||||
const wcstring &str() const { return contents; }
|
||||
#else
|
||||
|
||||
/// \return whether the text is empty.
|
||||
bool empty() const { return contents.empty(); }
|
||||
struct HistoryItem;
|
||||
|
||||
/// \return whether our contents matches a search term.
|
||||
bool matches_search(const wcstring &term, enum history_search_type_t type,
|
||||
bool case_sensitive) const;
|
||||
class HistorySharedPtr;
|
||||
enum class PersistenceMode;
|
||||
enum class SearchDirection;
|
||||
enum class SearchType;
|
||||
|
||||
/// \return the timestamp for creating this history item.
|
||||
time_t timestamp() const { return creation_timestamp; }
|
||||
#endif // INCLUDE_RUST_HEADERS
|
||||
|
||||
/// \return whether this item should be persisted (written to disk).
|
||||
bool should_write_to_disk() const { return persist_mode == history_persistence_mode_t::disk; }
|
||||
|
||||
/// Get and set the list of arguments which referred to files.
|
||||
/// This is used for autosuggestion hinting.
|
||||
const path_list_t &get_required_paths() const { return required_paths; }
|
||||
void set_required_paths(path_list_t paths) { required_paths = std::move(paths); }
|
||||
|
||||
private:
|
||||
/// Attempts to merge two compatible history items together.
|
||||
bool merge(const history_item_t &item);
|
||||
|
||||
/// The actual contents of the entry.
|
||||
wcstring contents;
|
||||
|
||||
/// Original creation time for the entry.
|
||||
time_t creation_timestamp;
|
||||
|
||||
/// Paths that we require to be valid for this item to be autosuggested.
|
||||
path_list_t required_paths;
|
||||
|
||||
/// Sometimes unique identifier used for hinting.
|
||||
history_identifier_t identifier;
|
||||
|
||||
/// If set, do not write this item to disk.
|
||||
history_persistence_mode_t persist_mode;
|
||||
|
||||
friend class history_t;
|
||||
friend struct history_impl_t;
|
||||
friend class history_lru_cache_t;
|
||||
friend class history_tests_t;
|
||||
};
|
||||
|
||||
using history_item_list_t = std::deque<history_item_t>;
|
||||
|
||||
struct history_impl_t;
|
||||
|
||||
enum class history_search_direction_t { forward, backward };
|
||||
|
||||
class history_t : noncopyable_t, nonmovable_t {
|
||||
friend class history_tests_t;
|
||||
struct impl_wrapper_t;
|
||||
const std::unique_ptr<impl_wrapper_t> wrap_;
|
||||
|
||||
acquired_lock<history_impl_t> impl();
|
||||
acquired_lock<const history_impl_t> impl() const;
|
||||
|
||||
/// Privately add an item. If pending, the item will not be returned by history searches until a
|
||||
/// call to resolve_pending. Any trailing ephemeral items are dropped.
|
||||
void add(history_item_t &&item, bool pending = false);
|
||||
|
||||
/// Add a new history item with text \p str to the end of history.
|
||||
void add(wcstring str);
|
||||
|
||||
public:
|
||||
explicit history_t(wcstring name);
|
||||
~history_t();
|
||||
|
||||
/// Whether we're in maximum chaos mode, useful for testing.
|
||||
/// This causes things like locks to fail.
|
||||
static bool chaos_mode;
|
||||
|
||||
/// Whether to force the read path instead of mmap. This is useful for testing.
|
||||
static bool never_mmap;
|
||||
|
||||
/// Returns history with the given name, creating it if necessary.
|
||||
static std::shared_ptr<history_t> 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();
|
||||
|
||||
/// Remove a history item.
|
||||
void remove(const wcstring &str);
|
||||
|
||||
/// Remove any trailing ephemeral items.
|
||||
void remove_ephemeral_items();
|
||||
|
||||
/// Add a new pending history item to the end, and then begin file detection on the items to
|
||||
/// determine which arguments are paths. Arguments may be expanded (e.g. with PWD and variables)
|
||||
/// using the given \p vars. The item has the given \p persist_mode.
|
||||
static void add_pending_with_file_detection(
|
||||
const std::shared_ptr<history_t> &self, const wcstring &str,
|
||||
const std::shared_ptr<environment_t> &vars,
|
||||
history_persistence_mode_t persist_mode = history_persistence_mode_t::disk);
|
||||
|
||||
/// Resolves any pending history items, so that they may be returned in history searches.
|
||||
void resolve_pending();
|
||||
|
||||
/// Saves history.
|
||||
void save();
|
||||
|
||||
/// Searches history.
|
||||
bool search(history_search_type_t search_type, const std::vector<wcstring> &search_args,
|
||||
const wchar_t *show_time_format, size_t max_items, bool case_sensitive,
|
||||
bool null_terminate, bool reverse, const cancel_checker_t &cancel_check,
|
||||
io_streams_t &streams);
|
||||
|
||||
/// Irreversibly clears history.
|
||||
void clear();
|
||||
|
||||
/// Irreversibly clears history for the current session.
|
||||
void clear_session();
|
||||
|
||||
/// Populates from older location (in config path, rather than data path).
|
||||
void populate_from_config_path();
|
||||
|
||||
/// Populates from a bash history file.
|
||||
void populate_from_bash(FILE *f);
|
||||
|
||||
/// Incorporates the history of other shells into this history.
|
||||
void incorporate_external_changes();
|
||||
|
||||
/// Gets all the history into a list. This is intended for the $history environment variable.
|
||||
/// This may be long!
|
||||
void get_history(std::vector<wcstring> &result);
|
||||
|
||||
/// Let indexes be a list of one-based indexes into the history, matching the interpretation of
|
||||
/// $history. That is, $history[1] is the most recently executed command. Values less than one
|
||||
/// are skipped. Return a mapping from index to history item text.
|
||||
std::unordered_map<long, wcstring> items_at_indexes(const std::vector<long> &idxs);
|
||||
|
||||
/// Return the specified history at the specified index. 0 is the index of the current
|
||||
/// commandline. (So the most recent item is at index 1.)
|
||||
history_item_t item_at_index(size_t idx);
|
||||
|
||||
/// Return the number of history entries.
|
||||
size_t size();
|
||||
};
|
||||
using history_item_t = HistoryItem;
|
||||
using history_persistence_mode_t = PersistenceMode;
|
||||
using history_search_direction_t = SearchDirection;
|
||||
using history_search_type_t = SearchType;
|
||||
using history_search_flags_t = uint32_t;
|
||||
|
||||
/// Flags for history searching.
|
||||
enum {
|
||||
@@ -236,113 +54,4 @@ enum {
|
||||
};
|
||||
using history_search_flags_t = uint32_t;
|
||||
|
||||
/// Support for searching a history backwards.
|
||||
/// Note this does NOT de-duplicate; it is the caller's responsibility to do so.
|
||||
class history_search_t {
|
||||
private:
|
||||
/// The history in which we are searching.
|
||||
/// TODO: this should be a shared_ptr.
|
||||
history_t *history_;
|
||||
|
||||
/// The original search term.
|
||||
wcstring orig_term_;
|
||||
|
||||
/// The (possibly lowercased) search term.
|
||||
wcstring canon_term_;
|
||||
|
||||
/// Our search type.
|
||||
enum history_search_type_t search_type_ { history_search_type_t::contains };
|
||||
|
||||
/// Our flags.
|
||||
history_search_flags_t flags_{0};
|
||||
|
||||
/// The current history item.
|
||||
maybe_t<history_item_t> current_item_;
|
||||
|
||||
/// Index of the current history item.
|
||||
size_t current_index_{0};
|
||||
|
||||
/// If deduping, the items we've seen.
|
||||
std::unordered_set<wcstring> deduper_;
|
||||
|
||||
/// return whether we deduplicate items.
|
||||
bool dedup() const { return !(flags_ & history_search_no_dedup); }
|
||||
|
||||
public:
|
||||
/// Gets the original search term.
|
||||
const wcstring &original_term() const { return orig_term_; }
|
||||
|
||||
// Finds the next search result. Returns true if one was found.
|
||||
bool go_to_next_match(history_search_direction_t direction);
|
||||
|
||||
/// Returns the current search result item. asserts if there is no current item.
|
||||
const history_item_t ¤t_item() const;
|
||||
|
||||
/// Returns the current search result item contents. asserts if there is no current item.
|
||||
const wcstring ¤t_string() const;
|
||||
|
||||
/// Returns the index of the current history item.
|
||||
size_t current_index() const;
|
||||
|
||||
/// return whether we are case insensitive.
|
||||
bool ignores_case() const { return flags_ & history_search_ignore_case; }
|
||||
|
||||
/// Construct from a history pointer; the caller is responsible for ensuring the history stays
|
||||
/// alive.
|
||||
history_search_t(history_t *hist, const wcstring &str,
|
||||
enum history_search_type_t type = history_search_type_t::contains,
|
||||
history_search_flags_t flags = 0, size_t starting_index = 0)
|
||||
: history_(hist),
|
||||
orig_term_(str),
|
||||
canon_term_(str),
|
||||
search_type_(type),
|
||||
flags_(flags),
|
||||
current_index_(starting_index) {
|
||||
if (ignores_case()) {
|
||||
std::transform(canon_term_.begin(), canon_term_.end(), canon_term_.begin(), towlower);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct from a shared_ptr. TODO: this should be the only constructor.
|
||||
history_search_t(const std::shared_ptr<history_t> &hist, const wcstring &str,
|
||||
enum history_search_type_t type = history_search_type_t::contains,
|
||||
history_search_flags_t flags = 0, size_t starting_index = 0)
|
||||
: history_search_t(hist.get(), str, type, flags, starting_index) {}
|
||||
|
||||
/** Default constructor. */
|
||||
history_search_t() = default;
|
||||
};
|
||||
|
||||
/** Saves the new history to disk. */
|
||||
void history_save_all();
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
/** Return the prefix for the files to be used for command and read history. */
|
||||
wcstring history_session_id(const environment_t &vars);
|
||||
#endif
|
||||
|
||||
/** FFI version of above **/
|
||||
class env_var_t;
|
||||
wcstring history_session_id(std::unique_ptr<env_var_t> fish_history);
|
||||
|
||||
/**
|
||||
Given a list of proposed paths and a context, perform variable and home directory expansion,
|
||||
and detect if the result expands to a value which is also the path to a file.
|
||||
Wildcard expansions are suppressed - see implementation comments for why.
|
||||
This is used for autosuggestion hinting. If we add an item to history, and one of its arguments
|
||||
refers to a file, then we only want to suggest it if there is a valid file there.
|
||||
This does disk I/O and may only be called in a background thread.
|
||||
*/
|
||||
path_list_t expand_and_detect_paths(const path_list_t &paths, const environment_t &vars);
|
||||
|
||||
/**
|
||||
Given a list of proposed paths and a context, expand each one and see if it refers to a file.
|
||||
Wildcard expansions are suppressed.
|
||||
\return true if \p paths is empty or every path is valid.
|
||||
*/
|
||||
bool all_paths_are_valid(const path_list_t &paths, const operation_context_t &ctx);
|
||||
|
||||
/** Queries private mode status. */
|
||||
bool in_private_mode(const environment_t &vars);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,604 +0,0 @@
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "history_file.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <utility>
|
||||
|
||||
#include "common.h"
|
||||
#include "history.h"
|
||||
#include "path.h"
|
||||
#include "wutil.h"
|
||||
|
||||
// Some forward declarations.
|
||||
static history_item_t decode_item_fish_2_0(const char *base, size_t len);
|
||||
static history_item_t decode_item_fish_1_x(const char *begin, size_t length);
|
||||
|
||||
static maybe_t<size_t> offset_of_next_item_fish_2_0(const history_file_contents_t &contents,
|
||||
size_t *inout_cursor, time_t cutoff_timestamp);
|
||||
static maybe_t<size_t> offset_of_next_item_fish_1_x(const char *begin, size_t mmap_length,
|
||||
size_t *inout_cursor);
|
||||
|
||||
// Check if we should mmap the fd.
|
||||
// Don't try mmap() on non-local filesystems.
|
||||
static bool should_mmap() {
|
||||
if (history_t::never_mmap) return false;
|
||||
|
||||
// mmap only if we are known not-remote.
|
||||
return path_get_config_remoteness() == dir_remoteness_t::local;
|
||||
}
|
||||
|
||||
// Read up to len bytes from fd into address, zeroing the rest.
|
||||
// Return true on success, false on failure.
|
||||
static bool read_from_fd(int fd, void *address, size_t len) {
|
||||
size_t remaining = len;
|
||||
auto ptr = static_cast<char *>(address);
|
||||
while (remaining > 0) {
|
||||
ssize_t amt = read(fd, ptr, remaining);
|
||||
if (amt < 0) {
|
||||
if (errno != EINTR) {
|
||||
return false;
|
||||
}
|
||||
} else if (amt == 0) {
|
||||
break;
|
||||
} else {
|
||||
remaining -= amt;
|
||||
ptr += amt;
|
||||
}
|
||||
}
|
||||
std::memset(ptr, 0, remaining);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void replace_all(std::string *str, const char *needle, const char *replacement) {
|
||||
size_t needle_len = std::strlen(needle), replacement_len = std::strlen(replacement);
|
||||
size_t offset = 0;
|
||||
while ((offset = str->find(needle, offset)) != std::string::npos) {
|
||||
str->replace(offset, needle_len, replacement);
|
||||
offset += replacement_len;
|
||||
}
|
||||
}
|
||||
|
||||
// Support for escaping and unescaping the nonstandard "yaml" format introduced in fish 2.0.
|
||||
static void escape_yaml_fish_2_0(std::string *str) {
|
||||
replace_all(str, "\\", "\\\\"); // replace one backslash with two
|
||||
replace_all(str, "\n", "\\n"); // replace newline with backslash + literal n
|
||||
}
|
||||
|
||||
/// This function is called frequently, so it ought to be fast.
|
||||
static void unescape_yaml_fish_2_0(std::string *str) {
|
||||
size_t cursor = 0, size = str->size();
|
||||
while (cursor < size) {
|
||||
// Operate on a const version of str, to avoid needless COWs that at() does.
|
||||
const std::string &const_str = *str;
|
||||
|
||||
// Look for a backslash.
|
||||
size_t backslash = const_str.find('\\', cursor);
|
||||
if (backslash == std::string::npos || backslash + 1 >= size) {
|
||||
// Either not found, or found as the last character.
|
||||
break;
|
||||
} else {
|
||||
// Backslash found. Maybe we'll do something about it. Be sure to invoke the const
|
||||
// version of at().
|
||||
char escaped_char = const_str.at(backslash + 1);
|
||||
if (escaped_char == '\\') {
|
||||
// Two backslashes in a row. Delete the second one.
|
||||
str->erase(backslash + 1, 1);
|
||||
size--;
|
||||
} else if (escaped_char == 'n') {
|
||||
// Backslash + n. Replace with a newline.
|
||||
str->replace(backslash, 2, "\n");
|
||||
size--;
|
||||
}
|
||||
// The character at index backslash has now been made whole; start at the next
|
||||
// character.
|
||||
cursor = backslash + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A type wrapping up a region allocated via mmap().
|
||||
struct history_file_contents_t::mmap_region_t : noncopyable_t, nonmovable_t {
|
||||
void *const ptr;
|
||||
const size_t len;
|
||||
|
||||
mmap_region_t(void *ptr, size_t len) : ptr(ptr), len(len) {
|
||||
assert(ptr != MAP_FAILED && len > 0 && "Invalid params");
|
||||
}
|
||||
|
||||
~mmap_region_t() { (void)munmap(ptr, len); }
|
||||
|
||||
/// Map a region [0, len) from an fd.
|
||||
/// \return nullptr on failure.
|
||||
static std::unique_ptr<mmap_region_t> map_file(int fd, size_t len) {
|
||||
if (len == 0) return nullptr;
|
||||
void *ptr = mmap(nullptr, size_t(len), PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (ptr == MAP_FAILED) return nullptr;
|
||||
return make_unique<mmap_region_t>(ptr, len);
|
||||
}
|
||||
|
||||
/// Map anonymous memory of a given length.
|
||||
/// \return nullptr on failure.
|
||||
static std::unique_ptr<mmap_region_t> map_anon(size_t len) {
|
||||
if (len == 0) return nullptr;
|
||||
void *ptr =
|
||||
#ifdef MAP_ANON
|
||||
mmap(nullptr, size_t(len), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
#else
|
||||
mmap(0, size_t(len), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
#endif
|
||||
if (ptr == MAP_FAILED) return nullptr;
|
||||
return make_unique<mmap_region_t>(ptr, len);
|
||||
}
|
||||
};
|
||||
|
||||
history_file_contents_t::~history_file_contents_t() = default;
|
||||
|
||||
history_file_contents_t::history_file_contents_t(std::unique_ptr<mmap_region_t> region)
|
||||
: region_(std::move(region)), start_(static_cast<char *>(region_->ptr)), length_(region_->len) {
|
||||
assert(region_ && start_ && length_ > 0 && "Invalid params");
|
||||
}
|
||||
|
||||
history_file_contents_t::history_file_contents_t(const char *start, size_t length)
|
||||
: start_(start), length_(length) {
|
||||
// Construct from explicit data, not backed by a file. This is used in tests.
|
||||
}
|
||||
|
||||
/// Try to infer the history file type based on inspecting the data.
|
||||
bool history_file_contents_t::infer_file_type() {
|
||||
assert(length_ > 0 && "File should never be empty");
|
||||
if (start_[0] == '#') {
|
||||
this->type_ = history_type_fish_1_x;
|
||||
} else { // assume new fish
|
||||
this->type_ = history_type_fish_2_0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<history_file_contents_t> history_file_contents_t::create(int fd) {
|
||||
// Check that the file is seekable, and its size.
|
||||
off_t len = lseek(fd, 0, SEEK_END);
|
||||
if (len <= 0 || static_cast<uint64_t>(len) >= static_cast<uint64_t>(SIZE_MAX)) return nullptr;
|
||||
|
||||
bool mmap_file_directly = should_mmap();
|
||||
std::unique_ptr<mmap_region_t> region =
|
||||
mmap_file_directly ? mmap_region_t::map_file(fd, len) : mmap_region_t::map_anon(len);
|
||||
if (!region) return nullptr;
|
||||
|
||||
// If we mapped anonymous memory, we have to read from the file.
|
||||
if (!mmap_file_directly) {
|
||||
if (lseek(fd, 0, SEEK_SET) != 0) return nullptr;
|
||||
if (!read_from_fd(fd, region->ptr, region->len)) return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<history_file_contents_t> result(new history_file_contents_t(std::move(region)));
|
||||
|
||||
// Check the file type.
|
||||
if (!result->infer_file_type()) return nullptr;
|
||||
return result;
|
||||
}
|
||||
|
||||
history_item_t history_file_contents_t::decode_item(size_t offset) const {
|
||||
const char *base = address_at(offset);
|
||||
size_t len = this->length() - offset;
|
||||
switch (this->type()) {
|
||||
case history_type_fish_2_0:
|
||||
return decode_item_fish_2_0(base, len);
|
||||
case history_type_fish_1_x:
|
||||
return decode_item_fish_1_x(base, len);
|
||||
}
|
||||
return history_item_t{};
|
||||
}
|
||||
|
||||
maybe_t<size_t> history_file_contents_t::offset_of_next_item(size_t *cursor, time_t cutoff) const {
|
||||
switch (this->type()) {
|
||||
case history_type_fish_2_0:
|
||||
return offset_of_next_item_fish_2_0(*this, cursor, cutoff);
|
||||
case history_type_fish_1_x:
|
||||
return offset_of_next_item_fish_1_x(this->begin(), this->length(), cursor);
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
/// Read one line, stripping off any newline, and updating cursor. Note that our input string is NOT
|
||||
/// null terminated; it's just a memory mapped file.
|
||||
static size_t read_line(const char *base, size_t cursor, size_t len, std::string &result) {
|
||||
// Locate the newline.
|
||||
assert(cursor <= len);
|
||||
const char *start = base + cursor;
|
||||
auto a_newline = static_cast<const char *>(std::memchr(start, '\n', len - cursor));
|
||||
if (a_newline != nullptr) { // we found a newline
|
||||
result.assign(start, a_newline - start);
|
||||
// Return the amount to advance the cursor; skip over the newline.
|
||||
return a_newline - start + 1;
|
||||
}
|
||||
|
||||
// We ran off the end.
|
||||
result.clear();
|
||||
return len - cursor;
|
||||
}
|
||||
|
||||
/// Trims leading spaces in the given string, returning how many there were.
|
||||
static size_t trim_leading_spaces(std::string &str) {
|
||||
size_t i = 0, max = str.size();
|
||||
while (i < max && str[i] == ' ') i++;
|
||||
str.erase(0, i);
|
||||
return i;
|
||||
}
|
||||
|
||||
static bool extract_prefix_and_unescape_yaml(std::string *key, std::string *value,
|
||||
const std::string &line) {
|
||||
size_t where = line.find(':');
|
||||
if (where != std::string::npos) {
|
||||
key->assign(line, 0, where);
|
||||
|
||||
// Skip a space after the : if necessary.
|
||||
size_t val_start = where + 1;
|
||||
if (val_start < line.size() && line.at(val_start) == ' ') val_start++;
|
||||
value->assign(line, val_start, line.size() - val_start);
|
||||
|
||||
unescape_yaml_fish_2_0(key);
|
||||
unescape_yaml_fish_2_0(value);
|
||||
}
|
||||
return where != std::string::npos;
|
||||
}
|
||||
|
||||
/// Decode an item via the fish 2.0 format.
|
||||
static history_item_t decode_item_fish_2_0(const char *base, size_t len) {
|
||||
wcstring cmd;
|
||||
time_t when = 0;
|
||||
path_list_t paths;
|
||||
|
||||
size_t indent = 0, cursor = 0;
|
||||
std::string key, value, line;
|
||||
|
||||
// Read the "- cmd:" line.
|
||||
size_t advance = read_line(base, cursor, len, line);
|
||||
trim_leading_spaces(line);
|
||||
if (!extract_prefix_and_unescape_yaml(&key, &value, line) || key != "- cmd") {
|
||||
goto done; //!OCLINT(goto is the cleanest way to handle bad input)
|
||||
}
|
||||
|
||||
cursor += advance;
|
||||
cmd = str2wcstring(value);
|
||||
|
||||
// Read the remaining lines.
|
||||
for (;;) {
|
||||
size_t advance = read_line(base, cursor, len, line);
|
||||
|
||||
size_t this_indent = trim_leading_spaces(line);
|
||||
if (indent == 0) indent = this_indent;
|
||||
|
||||
if (this_indent == 0 || indent != this_indent) break;
|
||||
|
||||
if (!extract_prefix_and_unescape_yaml(&key, &value, line)) break;
|
||||
|
||||
// We are definitely going to consume this line.
|
||||
cursor += advance;
|
||||
|
||||
if (key == "when") {
|
||||
// Parse an int from the timestamp. Should this fail, strtol returns 0; that's
|
||||
// acceptable.
|
||||
char *end = nullptr;
|
||||
long tmp = strtol(value.c_str(), &end, 0);
|
||||
when = tmp;
|
||||
} else if (key == "paths") {
|
||||
// Read lines starting with " - " until we can't read any more.
|
||||
for (;;) {
|
||||
size_t advance = read_line(base, cursor, len, line);
|
||||
if (trim_leading_spaces(line) <= indent) break;
|
||||
|
||||
if (std::strncmp(line.c_str(), "- ", 2) != 0) break;
|
||||
|
||||
// We're going to consume this line.
|
||||
cursor += advance;
|
||||
|
||||
// Skip the leading dash-space and then store this path it.
|
||||
line.erase(0, 2);
|
||||
unescape_yaml_fish_2_0(&line);
|
||||
paths.push_back(str2wcstring(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
history_item_t result(cmd, when);
|
||||
result.set_required_paths(std::move(paths));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Parse a timestamp line that looks like this: spaces, "when:", spaces, timestamp, newline
|
||||
/// The string is NOT null terminated; however we do know it contains a newline, so stop when we
|
||||
/// reach it.
|
||||
static bool parse_timestamp(const char *str, time_t *out_when) {
|
||||
const char *cursor = str;
|
||||
// Advance past spaces.
|
||||
while (*cursor == ' ') cursor++;
|
||||
|
||||
// Look for "when:".
|
||||
size_t when_len = 5;
|
||||
if (std::strncmp(cursor, "when:", when_len) != 0) return false;
|
||||
cursor += when_len;
|
||||
|
||||
// Advance past spaces.
|
||||
while (*cursor == ' ') cursor++;
|
||||
|
||||
// Try to parse a timestamp.
|
||||
long timestamp = 0;
|
||||
if (isdigit(*cursor) && (timestamp = strtol(cursor, nullptr, 0)) > 0) {
|
||||
*out_when = static_cast<time_t>(timestamp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the start of the next line, or NULL. The next line must itself end with a
|
||||
/// newline. Note that the string is not null terminated.
|
||||
static const char *next_line(const char *start, const char *end) {
|
||||
// Handle the hopeless case.
|
||||
if (end == start) return nullptr;
|
||||
|
||||
// Skip past the next newline.
|
||||
const char *nextline = std::find(start, end, '\n');
|
||||
if (nextline == end) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Skip past the newline character itself.
|
||||
if (++nextline >= end) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Make sure this new line is itself "newline terminated". If it's not, return NULL.
|
||||
const char *next_newline = std::find(nextline, end, '\n');
|
||||
if (next_newline == end) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return nextline;
|
||||
}
|
||||
|
||||
/// Support for iteratively locating the offsets of history items.
|
||||
/// Pass the file contents and a pointer to a cursor size_t, initially 0.
|
||||
/// If custoff_timestamp is nonzero, skip items created at or after that timestamp.
|
||||
/// Returns (size_t)-1 when done.
|
||||
static maybe_t<size_t> offset_of_next_item_fish_2_0(const history_file_contents_t &contents,
|
||||
size_t *inout_cursor, time_t cutoff_timestamp) {
|
||||
size_t cursor = *inout_cursor;
|
||||
const size_t length = contents.length();
|
||||
const char *const begin = contents.begin();
|
||||
const char *const end = contents.end();
|
||||
while (cursor < length) {
|
||||
const char *line_start = contents.address_at(cursor);
|
||||
|
||||
// Advance the cursor to the next line.
|
||||
auto a_newline = static_cast<const char *>(std::memchr(line_start, '\n', length - cursor));
|
||||
if (a_newline == nullptr) break;
|
||||
|
||||
// Advance the cursor past this line. +1 is for the newline.
|
||||
cursor = a_newline - begin + 1;
|
||||
|
||||
// Skip lines with a leading space, since these are in the interior of one of our items.
|
||||
if (line_start[0] == ' ') continue;
|
||||
|
||||
// Skip very short lines to make one of the checks below easier.
|
||||
if (a_newline - line_start < 3) continue;
|
||||
|
||||
// Try to be a little YAML compatible. Skip lines with leading %, ---, or ...
|
||||
if (!std::memcmp(line_start, "%", 1) || !std::memcmp(line_start, "---", 3) ||
|
||||
!std::memcmp(line_start, "...", 3))
|
||||
continue;
|
||||
|
||||
// Hackish: fish 1.x rewriting a fish 2.0 history file can produce lines with lots of
|
||||
// leading "- cmd: - cmd: - cmd:". Trim all but one leading "- cmd:".
|
||||
constexpr const char double_cmd[] = "- cmd: - cmd: ";
|
||||
constexpr const size_t double_cmd_len = const_strlen(double_cmd);
|
||||
while (static_cast<size_t>(a_newline - line_start) > double_cmd_len &&
|
||||
!std::memcmp(line_start, double_cmd, double_cmd_len)) {
|
||||
// Skip over just one of the - cmd. In the end there will be just one left.
|
||||
line_start += const_strlen("- cmd: ");
|
||||
}
|
||||
|
||||
// Hackish: fish 1.x rewriting a fish 2.0 history file can produce commands like "when:
|
||||
// 123456". Ignore those.
|
||||
constexpr const char cmd_when[] = "- cmd: when:";
|
||||
constexpr const size_t cmd_when_len = const_strlen(cmd_when);
|
||||
if (static_cast<size_t>(a_newline - line_start) >= cmd_when_len &&
|
||||
!std::memcmp(line_start, cmd_when, cmd_when_len)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point, we know line_start is at the beginning of an item. But maybe we want to
|
||||
// skip this item because of timestamps. A 0 cutoff means we don't care; if we do care, then
|
||||
// try parsing out a timestamp.
|
||||
if (cutoff_timestamp != 0) {
|
||||
// Hackish fast way to skip items created after our timestamp. This is the mechanism by
|
||||
// which we avoid "seeing" commands from other sessions that started after we started.
|
||||
// We try hard to ensure that our items are sorted by their timestamps, so in theory we
|
||||
// could just break, but I don't think that works well if (for example) the clock
|
||||
// changes. So we'll read all subsequent items.
|
||||
// Walk over lines that we think are interior. These lines are not null terminated, but
|
||||
// are guaranteed to contain a newline.
|
||||
bool has_timestamp = false;
|
||||
time_t timestamp = 0;
|
||||
const char *interior_line;
|
||||
|
||||
for (interior_line = next_line(line_start, end);
|
||||
interior_line != nullptr && !has_timestamp;
|
||||
interior_line = next_line(interior_line, end)) {
|
||||
// If the first character is not a space, it's not an interior line, so we're done.
|
||||
if (interior_line[0] != ' ') break;
|
||||
|
||||
// Hackish optimization: since we just stepped over some interior line, update the
|
||||
// cursor so we don't have to look at these lines next time.
|
||||
cursor = interior_line - begin;
|
||||
|
||||
// Try parsing a timestamp from this line. If we succeed, the loop will break.
|
||||
has_timestamp = parse_timestamp(interior_line, ×tamp);
|
||||
}
|
||||
|
||||
// Skip this item if the timestamp is past our cutoff.
|
||||
if (has_timestamp && timestamp > cutoff_timestamp) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We made it through the gauntlet.
|
||||
*inout_cursor = cursor;
|
||||
return line_start - begin;
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
void append_history_item_to_buffer(const history_item_t &item, std::string *buffer) {
|
||||
assert(item.should_write_to_disk() && "Item should not be persisted");
|
||||
auto append = [=](const char *a, const char *b = nullptr, const char *c = nullptr) {
|
||||
if (a) buffer->append(a);
|
||||
if (b) buffer->append(b);
|
||||
if (c) buffer->append(c);
|
||||
};
|
||||
|
||||
std::string cmd = wcs2string(item.str());
|
||||
escape_yaml_fish_2_0(&cmd);
|
||||
append("- cmd: ", cmd.c_str(), "\n");
|
||||
append(" when: ", std::to_string(item.timestamp()).c_str(), "\n");
|
||||
const path_list_t &paths = item.get_required_paths();
|
||||
if (!paths.empty()) {
|
||||
append(" paths:\n");
|
||||
|
||||
for (const auto &wpath : paths) {
|
||||
std::string path = wcs2string(wpath);
|
||||
escape_yaml_fish_2_0(&path);
|
||||
append(" - ", path.c_str(), "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove backslashes from all newlines. This makes a string from the history file better formated
|
||||
/// for on screen display.
|
||||
static wcstring history_unescape_newlines_fish_1_x(const wcstring &in_str) {
|
||||
wcstring out;
|
||||
for (const wchar_t *in = in_str.c_str(); *in; in++) {
|
||||
if (*in == L'\\') {
|
||||
if (*(in + 1) != L'\n') {
|
||||
out.push_back(*in);
|
||||
}
|
||||
} else {
|
||||
out.push_back(*in);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Decode an item via the fish 1.x format. Adapted from fish 1.x's item_get().
|
||||
static history_item_t decode_item_fish_1_x(const char *begin, size_t length) {
|
||||
const char *end = begin + length;
|
||||
const char *pos = begin;
|
||||
wcstring out;
|
||||
bool was_backslash = false;
|
||||
bool first_char = true;
|
||||
bool timestamp_mode = false;
|
||||
time_t timestamp = 0;
|
||||
|
||||
while (true) {
|
||||
wchar_t c;
|
||||
size_t res;
|
||||
mbstate_t state = {};
|
||||
|
||||
if (MB_CUR_MAX == 1) { // single-byte locale
|
||||
c = static_cast<unsigned char>(*pos);
|
||||
res = 1;
|
||||
} else {
|
||||
res = std::mbrtowc(&c, pos, end - pos, &state);
|
||||
}
|
||||
|
||||
if (res == static_cast<size_t>(-1)) {
|
||||
pos++;
|
||||
continue;
|
||||
} else if (res == static_cast<size_t>(-2)) {
|
||||
break;
|
||||
} else if (res == static_cast<size_t>(0)) {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
pos += res;
|
||||
|
||||
if (c == L'\n') {
|
||||
if (timestamp_mode) {
|
||||
const wchar_t *time_string = out.c_str();
|
||||
while (*time_string && !iswdigit(*time_string)) time_string++;
|
||||
|
||||
if (*time_string) {
|
||||
auto tm = static_cast<time_t>(fish_wcstol(time_string));
|
||||
if (!errno && tm >= 0) {
|
||||
timestamp = tm;
|
||||
}
|
||||
}
|
||||
|
||||
out.clear();
|
||||
timestamp_mode = false;
|
||||
continue;
|
||||
}
|
||||
if (!was_backslash) break;
|
||||
}
|
||||
|
||||
if (first_char) {
|
||||
first_char = false;
|
||||
if (c == L'#') timestamp_mode = true;
|
||||
}
|
||||
|
||||
out.push_back(c);
|
||||
was_backslash = (c == L'\\') && !was_backslash;
|
||||
}
|
||||
|
||||
out = history_unescape_newlines_fish_1_x(out);
|
||||
return history_item_t(out, timestamp);
|
||||
}
|
||||
|
||||
/// Same as offset_of_next_item_fish_2_0, but for fish 1.x (pre fishfish).
|
||||
/// Adapted from history_populate_from_mmap in history.c
|
||||
static maybe_t<size_t> offset_of_next_item_fish_1_x(const char *begin, size_t mmap_length,
|
||||
size_t *inout_cursor) {
|
||||
if (mmap_length == 0 || *inout_cursor >= mmap_length) return none();
|
||||
|
||||
const char *end = begin + mmap_length;
|
||||
const char *pos;
|
||||
bool ignore_newline = false;
|
||||
bool do_push = true;
|
||||
bool all_done = false;
|
||||
size_t result = *inout_cursor;
|
||||
|
||||
for (pos = begin + *inout_cursor; pos < end && !all_done; pos++) {
|
||||
if (do_push) {
|
||||
ignore_newline = (*pos == '#');
|
||||
do_push = false;
|
||||
}
|
||||
|
||||
if (*pos == '\\') {
|
||||
pos++;
|
||||
} else if (*pos == '\n') {
|
||||
if (!ignore_newline) {
|
||||
// pos will be left pointing just after this newline, because of the ++ in the loop.
|
||||
all_done = true;
|
||||
}
|
||||
ignore_newline = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos == end && !all_done) {
|
||||
// No trailing newline, treat this item as incomplete and ignore it.
|
||||
return none();
|
||||
}
|
||||
|
||||
*inout_cursor = (pos - begin);
|
||||
return result;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#ifndef FISH_HISTORY_FILE_H
|
||||
#define FISH_HISTORY_FILE_H
|
||||
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "common.h"
|
||||
#include "maybe.h"
|
||||
|
||||
class history_item_t;
|
||||
|
||||
// History file types.
|
||||
enum history_file_type_t { history_type_fish_2_0, history_type_fish_1_x };
|
||||
|
||||
/// history_file_contents_t holds the read-only contents of a file.
|
||||
class history_file_contents_t : noncopyable_t, nonmovable_t {
|
||||
public:
|
||||
/// Construct a history file contents from a file descriptor. The file descriptor is not closed.
|
||||
static std::unique_ptr<history_file_contents_t> create(int fd);
|
||||
|
||||
/// Decode an item at a given offset.
|
||||
history_item_t decode_item(size_t offset) const;
|
||||
|
||||
/// Support for iterating item offsets.
|
||||
/// The cursor should initially be 0.
|
||||
/// If cutoff is nonzero, skip items whose timestamp is newer than cutoff.
|
||||
/// \return the offset of the next item, or none() on end.
|
||||
maybe_t<size_t> offset_of_next_item(size_t *cursor, time_t cutoff) const;
|
||||
|
||||
/// Get the file type.
|
||||
history_file_type_t type() const { return type_; }
|
||||
|
||||
/// Get the size of the contents.
|
||||
size_t length() const { return length_; }
|
||||
|
||||
/// Return a pointer to the beginning.
|
||||
const char *begin() const { return address_at(0); }
|
||||
|
||||
/// Return a pointer to one-past-the-end.
|
||||
const char *end() const { return address_at(length_); }
|
||||
|
||||
/// Access the address at a given offset.
|
||||
const char *address_at(size_t offset) const {
|
||||
assert(offset <= length_ && "Invalid offset");
|
||||
return start_ + offset;
|
||||
}
|
||||
|
||||
~history_file_contents_t();
|
||||
|
||||
private:
|
||||
// A type wrapping up the logic around mmap and munmap.
|
||||
struct mmap_region_t;
|
||||
const std::unique_ptr<mmap_region_t> region_;
|
||||
|
||||
// The memory mapped pointer and length.
|
||||
// The ptr aliases our region. The length may be slightly smaller, if there is a trailing
|
||||
// incomplete history item.
|
||||
const char *const start_;
|
||||
const size_t length_;
|
||||
|
||||
// The type of the mapped file.
|
||||
// This is set at construction and not changed after.
|
||||
history_file_type_t type_{};
|
||||
|
||||
// Private constructors; use the static create() function.
|
||||
explicit history_file_contents_t(std::unique_ptr<mmap_region_t> region);
|
||||
history_file_contents_t(const char *start, size_t length);
|
||||
|
||||
// Try to infer the file type to populate type_.
|
||||
// \return true on success, false on error.
|
||||
bool infer_file_type();
|
||||
};
|
||||
|
||||
/// Append a history item to a buffer, in preparation for outputting it to the history file.
|
||||
void append_history_item_to_buffer(const history_item_t &item, std::string *buffer);
|
||||
|
||||
#endif
|
||||
@@ -206,13 +206,13 @@ static wcstring input_get_bind_mode(const environment_t &vars) {
|
||||
}
|
||||
|
||||
/// Set the current bind mode.
|
||||
static void input_set_bind_mode(parser_t &parser, const wcstring &bm) {
|
||||
static void input_set_bind_mode(const parser_t &parser, 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(!bm.empty());
|
||||
if (input_get_bind_mode(parser.vars()) != bm) {
|
||||
if (input_get_bind_mode(env_stack_t{parser.vars_boxed()}) != bm) {
|
||||
// Must send events here - see #6653.
|
||||
parser.set_var_and_fire(FISH_BIND_MODE_VAR, ENV_GLOBAL, bm);
|
||||
parser.set_var_and_fire(FISH_BIND_MODE_VAR, ENV_GLOBAL, wcstring_list_ffi_t{{bm}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,12 +321,12 @@ void init_input() {
|
||||
}
|
||||
}
|
||||
|
||||
inputter_t::inputter_t(parser_t &parser, int in)
|
||||
inputter_t::inputter_t(const parser_t &parser, int in)
|
||||
: input_event_queue_t(in), parser_(parser.shared()) {}
|
||||
|
||||
void inputter_t::prepare_to_select() /* override */ {
|
||||
// Fire any pending events and reap stray processes, including printing exit status messages.
|
||||
auto &parser = *this->parser_;
|
||||
auto &parser = this->parser_->deref();
|
||||
event_fire_delayed(parser);
|
||||
if (job_reap(parser, true)) reader_schedule_prompt_repaint();
|
||||
}
|
||||
@@ -337,7 +337,7 @@ void inputter_t::select_interrupted() /* override */ {
|
||||
signal_clear_cancel();
|
||||
|
||||
// Fire any pending events and reap stray processes, including printing exit status messages.
|
||||
auto &parser = *this->parser_;
|
||||
auto &parser = this->parser_->deref();
|
||||
event_fire_delayed(parser);
|
||||
if (job_reap(parser, true)) reader_schedule_prompt_repaint();
|
||||
|
||||
@@ -353,7 +353,7 @@ void inputter_t::select_interrupted() /* override */ {
|
||||
}
|
||||
|
||||
void inputter_t::uvar_change_notified() /* override */ {
|
||||
this->parser_->sync_uvars_and_fire(true /* always */);
|
||||
this->parser_->deref().sync_uvars_and_fire(true /* always */);
|
||||
}
|
||||
|
||||
void inputter_t::function_push_arg(wchar_t arg) { input_function_args_.push_back(arg); }
|
||||
@@ -411,7 +411,7 @@ void inputter_t::mapping_execute(const input_mapping_t &m,
|
||||
|
||||
// !has_functions && !has_commands: only set bind mode
|
||||
if (!has_commands && !has_functions) {
|
||||
if (!m.sets_mode.empty()) input_set_bind_mode(*parser_, m.sets_mode);
|
||||
if (!m.sets_mode.empty()) input_set_bind_mode(parser_->deref(), m.sets_mode);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ void inputter_t::mapping_execute(const input_mapping_t &m,
|
||||
}
|
||||
|
||||
// Empty bind mode indicates to not reset the mode (#2871)
|
||||
if (!m.sets_mode.empty()) input_set_bind_mode(*parser_, m.sets_mode);
|
||||
if (!m.sets_mode.empty()) input_set_bind_mode(parser_->deref(), m.sets_mode);
|
||||
}
|
||||
|
||||
void inputter_t::queue_char(const char_event_t &ch) {
|
||||
@@ -617,7 +617,7 @@ static bool try_peek_sequence(event_queue_peeker_t *peeker, const wcstring &str)
|
||||
/// interrupted by a readline event.
|
||||
maybe_t<input_mapping_t> inputter_t::find_mapping(event_queue_peeker_t *peeker) {
|
||||
const input_mapping_t *generic = nullptr;
|
||||
const auto &vars = parser_->vars();
|
||||
env_stack_t vars{parser_->deref().vars_boxed()};
|
||||
const wcstring bind_mode = input_get_bind_mode(vars);
|
||||
const input_mapping_t *escape = nullptr;
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
#include "common.h"
|
||||
#include "input_common.h"
|
||||
#include "maybe.h"
|
||||
#include "parser.h"
|
||||
|
||||
#define FISH_BIND_MODE_VAR L"fish_bind_mode"
|
||||
#define DEFAULT_BIND_MODE L"default"
|
||||
|
||||
class event_queue_peeker_t;
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
wcstring describe_char(wint_t c);
|
||||
|
||||
@@ -30,7 +30,7 @@ struct input_mapping_t;
|
||||
class inputter_t final : private input_event_queue_t {
|
||||
public:
|
||||
/// Construct from a parser, and the fd from which to read.
|
||||
explicit inputter_t(parser_t &parser, int in = STDIN_FILENO);
|
||||
explicit inputter_t(const parser_t &parser, int in = STDIN_FILENO);
|
||||
|
||||
/// Read a character from stdin. Try to convert some escape sequences into character constants,
|
||||
/// but do not permanently block the escape character.
|
||||
@@ -74,8 +74,10 @@ class inputter_t final : private input_event_queue_t {
|
||||
maybe_t<input_mapping_t> find_mapping(event_queue_peeker_t *peeker);
|
||||
char_event_t read_characters_no_readline();
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
// We need a parser to evaluate bindings.
|
||||
const std::shared_ptr<parser_t> parser_;
|
||||
const rust::Box<ParserRef> parser_;
|
||||
#endif
|
||||
|
||||
std::vector<wchar_t> input_function_args_{};
|
||||
bool function_status_{false};
|
||||
|
||||
@@ -143,26 +143,25 @@ void update_wait_on_escape_ms(const environment_t& vars) {
|
||||
}
|
||||
}
|
||||
|
||||
void update_wait_on_escape_ms_ffi(std::unique_ptr<env_var_t> fish_escape_delay_ms) {
|
||||
if (!fish_escape_delay_ms) {
|
||||
void update_wait_on_escape_ms_ffi(bool empty, const wcstring& fish_escape_delay_ms) {
|
||||
if (empty) {
|
||||
wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT;
|
||||
return;
|
||||
}
|
||||
|
||||
long tmp = fish_wcstol(fish_escape_delay_ms->as_string().c_str());
|
||||
long tmp = fish_wcstol(fish_escape_delay_ms.c_str());
|
||||
if (errno || tmp < 10 || tmp >= 5000) {
|
||||
std::fwprintf(stderr,
|
||||
L"ignoring fish_escape_delay_ms: value '%ls' "
|
||||
L"is not an integer or is < 10 or >= 5000 ms\n",
|
||||
fish_escape_delay_ms->as_string().c_str());
|
||||
fish_escape_delay_ms.c_str());
|
||||
} else {
|
||||
wait_on_escape_ms = static_cast<int>(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update the wait_on_sequence_key_ms value in response to the fish_sequence_key_delay_ms user variable being
|
||||
// set.
|
||||
// Update the wait_on_sequence_key_ms value in response to the fish_sequence_key_delay_ms user
|
||||
// variable being set.
|
||||
void update_wait_on_sequence_key_ms(const environment_t& vars) {
|
||||
auto sequence_key_time_ms = vars.get_unless_empty(L"fish_sequence_key_delay_ms");
|
||||
if (!sequence_key_time_ms) {
|
||||
@@ -181,18 +180,18 @@ void update_wait_on_sequence_key_ms(const environment_t& vars) {
|
||||
}
|
||||
}
|
||||
|
||||
void update_wait_on_sequence_key_ms_ffi(std::unique_ptr<env_var_t> fish_sequence_key_delay_ms) {
|
||||
if (!fish_sequence_key_delay_ms) {
|
||||
void update_wait_on_sequence_key_ms_ffi(bool empty, const wcstring& fish_sequence_key_delay_ms) {
|
||||
if (empty) {
|
||||
wait_on_sequence_key_ms = WAIT_ON_SEQUENCE_KEY_INFINITE;
|
||||
return;
|
||||
}
|
||||
|
||||
long tmp = fish_wcstol(fish_sequence_key_delay_ms->as_string().c_str());
|
||||
long tmp = fish_wcstol(fish_sequence_key_delay_ms.c_str());
|
||||
if (errno || tmp < 10 || tmp >= 5000) {
|
||||
std::fwprintf(stderr,
|
||||
L"ignoring fish_sequence_key_delay_ms: value '%ls' "
|
||||
L"is not an integer or is < 10 or >= 5000 ms\n",
|
||||
fish_sequence_key_delay_ms->as_string().c_str());
|
||||
fish_sequence_key_delay_ms.c_str());
|
||||
} else {
|
||||
wait_on_sequence_key_ms = static_cast<int>(tmp);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "common.h"
|
||||
#include "env.h"
|
||||
#include "maybe.h"
|
||||
|
||||
enum class readline_cmd_t {
|
||||
@@ -187,12 +188,11 @@ class char_event_t {
|
||||
};
|
||||
|
||||
/// Adjust the escape timeout.
|
||||
class environment_t;
|
||||
void update_wait_on_escape_ms(const environment_t &vars);
|
||||
void update_wait_on_escape_ms_ffi(std::unique_ptr<env_var_t> fish_escape_delay_ms);
|
||||
void update_wait_on_escape_ms_ffi(bool empty, const wcstring &fish_escape_delay_ms);
|
||||
|
||||
void update_wait_on_sequence_key_ms(const environment_t &vars);
|
||||
void update_wait_on_sequence_key_ms_ffi(std::unique_ptr<env_var_t> fish_sequence_key_delay_ms);
|
||||
void update_wait_on_sequence_key_ms_ffi(bool empty, const wcstring &fish_sequence_key_delay_ms);
|
||||
|
||||
/// A class which knows how to produce a stream of input events.
|
||||
/// This is a base class; you may subclass it for its override points.
|
||||
|
||||
450
src/io.cpp
450
src/io.cpp
@@ -1,450 +0,0 @@
|
||||
// Utilities for io redirection.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "io.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cwchar>
|
||||
#include <functional>
|
||||
|
||||
#include "common.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "fd_monitor.rs.h"
|
||||
#include "fds.h"
|
||||
#include "fds.rs.h"
|
||||
#include "flog.h"
|
||||
#include "maybe.h"
|
||||
#include "path.h"
|
||||
#include "redirection.h"
|
||||
#include "threads.rs.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
/// File redirection error message.
|
||||
#define FILE_ERROR _(L"An error occurred while redirecting file '%ls'")
|
||||
#define NOCLOB_ERROR _(L"The file '%ls' already exists")
|
||||
|
||||
/// Base open mode to pass to calls to open.
|
||||
#define OPEN_MASK 0666
|
||||
|
||||
/// Provide the fd monitor used for background fillthread operations.
|
||||
static fd_monitor_t &fd_monitor() {
|
||||
// Deliberately leaked to avoid shutdown dtors.
|
||||
static auto fdm = make_fd_monitor_t();
|
||||
return *fdm;
|
||||
}
|
||||
|
||||
io_data_t::~io_data_t() = default;
|
||||
io_pipe_t::~io_pipe_t() = default;
|
||||
io_fd_t::~io_fd_t() = default;
|
||||
io_close_t::~io_close_t() = default;
|
||||
io_file_t::~io_file_t() = default;
|
||||
io_bufferfill_t::~io_bufferfill_t() = default;
|
||||
|
||||
void io_close_t::print() const { std::fwprintf(stderr, L"close %d\n", fd); }
|
||||
|
||||
void io_fd_t::print() const { std::fwprintf(stderr, L"FD map %d -> %d\n", source_fd, fd); }
|
||||
|
||||
void io_file_t::print() const { std::fwprintf(stderr, L"file %d -> %d\n", file_fd_.fd(), fd); }
|
||||
|
||||
void io_pipe_t::print() const {
|
||||
std::fwprintf(stderr, L"pipe {%d} (input: %s) -> %d\n", source_fd, is_input_ ? "yes" : "no",
|
||||
fd);
|
||||
}
|
||||
|
||||
void io_bufferfill_t::print() const {
|
||||
std::fwprintf(stderr, L"bufferfill %d -> %d\n", write_fd_.fd(), fd);
|
||||
}
|
||||
|
||||
ssize_t io_buffer_t::read_once(int fd, acquired_lock<separated_buffer_t> &buffer) {
|
||||
assert(fd >= 0 && "Invalid fd");
|
||||
errno = 0;
|
||||
char bytes[4096 * 4];
|
||||
|
||||
// We want to swallow EINTR only; in particular EAGAIN needs to be returned back to the caller.
|
||||
ssize_t amt;
|
||||
do {
|
||||
amt = read(fd, bytes, sizeof bytes);
|
||||
} while (amt < 0 && errno == EINTR);
|
||||
if (amt < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
wperror(L"read");
|
||||
} else if (amt > 0) {
|
||||
buffer->append(bytes, static_cast<size_t>(amt));
|
||||
}
|
||||
return amt;
|
||||
}
|
||||
|
||||
struct callback_args_t {
|
||||
io_buffer_t *instance;
|
||||
std::shared_ptr<std::promise<void>> promise;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
static void item_callback_trampoline(autoclose_fd_t2 &fd, item_wake_reason_t reason,
|
||||
callback_args_t *args) {
|
||||
(args->instance)->item_callback(fd, (uint8_t)reason, args);
|
||||
}
|
||||
}
|
||||
|
||||
void io_buffer_t::begin_filling(autoclose_fd_t fd) {
|
||||
assert(!fillthread_running() && "Already have a fillthread");
|
||||
|
||||
// We want to fill buffer_ by reading from fd. fd is the read end of a pipe; the write end is
|
||||
// owned by another process, or something else writing in fish.
|
||||
// Pass fd to an fd_monitor. It will add fd to its select() loop, and give us a callback when
|
||||
// the fd is readable, or when our item is poked. The usual path is that we will get called
|
||||
// back, read a bit from the fd, and append it to the buffer. Eventually the write end of the
|
||||
// pipe will be closed - probably the other process exited - and fd will be widowed; read() will
|
||||
// then return 0 and we will stop reading.
|
||||
// In exotic circumstances the write end of the pipe will not be closed; this may happen in
|
||||
// e.g.:
|
||||
// cmd ( background & ; echo hi )
|
||||
// Here the background process will inherit the write end of the pipe and hold onto it forever.
|
||||
// In this case, when complete_background_fillthread() is called, the callback will be invoked
|
||||
// with item_wake_reason_t::poke, and we will notice that the shutdown flag is set (this
|
||||
// indicates that the command substitution is done); in this case we will read until we get
|
||||
// EAGAIN and then give up.
|
||||
|
||||
// Construct a promise. We will fulfill it in our fill thread, and wait for it in
|
||||
// complete_background_fillthread(). Note that TSan complains if the promise's dtor races with
|
||||
// the future's call to wait(), so we store the promise, not just its future (#7681).
|
||||
auto promise = std::make_shared<std::promise<void>>();
|
||||
this->fill_waiter_ = promise;
|
||||
|
||||
// Run our function to read until the receiver is closed.
|
||||
// It's OK to capture 'this' by value because 'this' waits for the promise in its dtor.
|
||||
auto args = new callback_args_t;
|
||||
args->instance = this;
|
||||
args->promise = std::move(promise);
|
||||
|
||||
item_id_ = fd_monitor().add_item(fd.acquire(), kNoTimeout, (uint8_t *)item_callback_trampoline,
|
||||
(uint8_t *)args);
|
||||
}
|
||||
|
||||
/// This is a hack to work around the difficulties in passing a capturing lambda across FFI
|
||||
/// boundaries. A static function that takes a generic/untyped callback parameter is easy to
|
||||
/// marshall with the basic C ABI.
|
||||
void io_buffer_t::item_callback(autoclose_fd_t2 &fd, uint8_t r, callback_args_t *args) {
|
||||
item_wake_reason_t reason = (item_wake_reason_t)r;
|
||||
auto &promise = *args->promise;
|
||||
|
||||
// Only check the shutdown flag if we timed out or were poked.
|
||||
// It's important that if select() indicated we were readable, that we call select() again
|
||||
// allowing it to time out. Note the typical case is that the fd will be closed, in which
|
||||
// case select will return immediately.
|
||||
bool done = false;
|
||||
if (reason == item_wake_reason_t::Readable) {
|
||||
// select() reported us as readable; read a bit.
|
||||
auto buffer = buffer_.acquire();
|
||||
ssize_t ret = read_once(fd.fd(), buffer);
|
||||
done = (ret == 0 || (ret < 0 && errno != EAGAIN && errno != EWOULDBLOCK));
|
||||
} else if (shutdown_fillthread_) {
|
||||
// Here our caller asked us to shut down; read while we keep getting data.
|
||||
// This will stop when the fd is closed or if we get EAGAIN.
|
||||
auto buffer = buffer_.acquire();
|
||||
ssize_t ret;
|
||||
do {
|
||||
ret = read_once(fd.fd(), buffer);
|
||||
} while (ret > 0);
|
||||
done = true;
|
||||
}
|
||||
if (done) {
|
||||
fd.close();
|
||||
promise.set_value();
|
||||
// When we close the fd, we signal to the caller that the fd should be removed from its set
|
||||
// and that this callback should never be called again.
|
||||
// Manual memory management is not nice but this is just during the cpp-to-rust transition.
|
||||
delete args;
|
||||
}
|
||||
};
|
||||
|
||||
separated_buffer_t io_buffer_t::complete_background_fillthread_and_take_buffer() {
|
||||
// Mark that our fillthread is done, then wake it up.
|
||||
assert(fillthread_running() && "Should have a fillthread");
|
||||
assert(this->item_id_ > 0 && "Should have a valid item ID");
|
||||
shutdown_fillthread_ = true;
|
||||
fd_monitor().poke_item(this->item_id_);
|
||||
|
||||
// Wait for the fillthread to fulfill its promise, and then clear the future so we know we no
|
||||
// longer have one.
|
||||
fill_waiter_->get_future().wait();
|
||||
fill_waiter_.reset();
|
||||
|
||||
// Return our buffer, transferring ownership.
|
||||
auto locked_buff = buffer_.acquire();
|
||||
separated_buffer_t result = std::move(*locked_buff);
|
||||
locked_buff->clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
shared_ptr<io_bufferfill_t> io_bufferfill_t::create(size_t buffer_limit, int target) {
|
||||
assert(target >= 0 && "Invalid target fd");
|
||||
|
||||
// Construct our pipes.
|
||||
auto pipes = make_autoclose_pipes();
|
||||
if (!pipes) {
|
||||
return nullptr;
|
||||
}
|
||||
// Our buffer will read from the read end of the pipe. This end must be non-blocking. This is
|
||||
// because our fillthread needs to poll to decide if it should shut down, and also accept input
|
||||
// from direct buffer transfers.
|
||||
if (make_fd_nonblocking(pipes->read.fd())) {
|
||||
FLOGF(warning, PIPE_ERROR);
|
||||
wperror(L"fcntl");
|
||||
return nullptr;
|
||||
}
|
||||
// Our fillthread gets the read end of the pipe; out_pipe gets the write end.
|
||||
auto buffer = std::make_shared<io_buffer_t>(buffer_limit);
|
||||
buffer->begin_filling(std::move(pipes->read));
|
||||
return std::make_shared<io_bufferfill_t>(target, std::move(pipes->write), buffer);
|
||||
}
|
||||
|
||||
separated_buffer_t io_bufferfill_t::finish(std::shared_ptr<io_bufferfill_t> &&filler) {
|
||||
// The io filler is passed in. This typically holds the only instance of the write side of the
|
||||
// pipe used by the buffer's fillthread (except for that side held by other processes). Get the
|
||||
// buffer out of the bufferfill and clear the shared_ptr; this will typically widow the pipe.
|
||||
// Then allow the buffer to finish.
|
||||
assert(filler && "Null pointer in finish");
|
||||
auto buffer = filler->buffer();
|
||||
filler.reset();
|
||||
return buffer->complete_background_fillthread_and_take_buffer();
|
||||
}
|
||||
|
||||
io_buffer_t::~io_buffer_t() {
|
||||
assert(!fillthread_running() && "io_buffer_t destroyed with outstanding fillthread");
|
||||
}
|
||||
|
||||
void io_chain_t::remove(const shared_ptr<const io_data_t> &element) {
|
||||
// See if you can guess why std::find doesn't work here.
|
||||
for (auto iter = this->begin(); iter != this->end(); ++iter) {
|
||||
if (*iter == element) {
|
||||
this->erase(iter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void io_chain_t::push_back(io_data_ref_t element) {
|
||||
// Ensure we never push back NULL.
|
||||
assert(element.get() != nullptr);
|
||||
std::vector<io_data_ref_t>::push_back(std::move(element));
|
||||
}
|
||||
|
||||
bool io_chain_t::append(const io_chain_t &chain) {
|
||||
assert(&chain != this && "Cannot append self to self");
|
||||
this->insert(this->end(), chain.begin(), chain.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool io_chain_t::append_from_specs(const redirection_spec_list_t &specs, const wcstring &pwd) {
|
||||
bool have_error = false;
|
||||
for (size_t i = 0; i < specs.size(); i++) {
|
||||
const redirection_spec_t *spec = specs.at(i);
|
||||
switch (spec->mode()) {
|
||||
case redirection_mode_t::fd: {
|
||||
if (spec->is_close()) {
|
||||
this->push_back(make_unique<io_close_t>(spec->fd()));
|
||||
} else {
|
||||
auto target_fd = spec->get_target_as_fd();
|
||||
assert(target_fd && "fd redirection should have been validated already");
|
||||
this->push_back(make_unique<io_fd_t>(spec->fd(), *target_fd));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// We have a path-based redireciton. Resolve it to a file.
|
||||
// Mark it as CLO_EXEC because we don't want it to be open in any child.
|
||||
wcstring path = path_apply_working_directory(*spec->target(), pwd);
|
||||
int oflags = spec->oflags();
|
||||
autoclose_fd_t file{wopen_cloexec(path, oflags, OPEN_MASK)};
|
||||
if (!file.valid()) {
|
||||
if ((oflags & O_EXCL) && (errno == EEXIST)) {
|
||||
FLOGF(warning, NOCLOB_ERROR, spec->target()->c_str());
|
||||
} else {
|
||||
if (should_flog(warning)) {
|
||||
FLOGF(warning, FILE_ERROR, spec->target()->c_str());
|
||||
auto err = errno;
|
||||
// If the error is that the file doesn't exist
|
||||
// or there's a non-directory component,
|
||||
// find the first problematic component for a better message.
|
||||
if (err == ENOENT || err == ENOTDIR) {
|
||||
auto dname = *spec->target();
|
||||
struct stat buf;
|
||||
|
||||
while (!dname.empty()) {
|
||||
auto next = wdirname(dname);
|
||||
if (!wstat(next, &buf)) {
|
||||
if (!S_ISDIR(buf.st_mode)) {
|
||||
FLOGF(warning, _(L"Path '%ls' is not a directory"),
|
||||
next.c_str());
|
||||
} else {
|
||||
FLOGF(warning, _(L"Path '%ls' does not exist"),
|
||||
dname.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
dname = next;
|
||||
}
|
||||
} else {
|
||||
wperror(L"open");
|
||||
}
|
||||
}
|
||||
}
|
||||
// If opening a file fails, insert a closed FD instead of the file redirection
|
||||
// and return false. This lets execution potentially recover and at least gives
|
||||
// the shell a chance to gracefully regain control of the shell (see #7038).
|
||||
this->push_back(make_unique<io_close_t>(spec->fd()));
|
||||
have_error = true;
|
||||
break;
|
||||
}
|
||||
this->push_back(std::make_shared<io_file_t>(spec->fd(), std::move(file)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !have_error;
|
||||
}
|
||||
|
||||
void io_chain_t::print() const {
|
||||
if (this->empty()) {
|
||||
std::fwprintf(stderr, L"Empty chain %p\n", this);
|
||||
return;
|
||||
}
|
||||
|
||||
std::fwprintf(stderr, L"Chain %p (%ld items):\n", this, static_cast<long>(this->size()));
|
||||
for (size_t i = 0; i < this->size(); i++) {
|
||||
const auto &io = this->at(i);
|
||||
if (io == nullptr) {
|
||||
std::fwprintf(stderr, L"\t(null)\n");
|
||||
} else {
|
||||
std::fwprintf(stderr, L"\t%lu: fd:%d, ", static_cast<unsigned long>(i), io->fd);
|
||||
io->print();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const io_data_t> io_chain_t::io_for_fd(int fd) const {
|
||||
for (auto iter = rbegin(); iter != rend(); ++iter) {
|
||||
const auto &data = *iter;
|
||||
if (data->fd == fd) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dup2_list_t dup2_list_resolve_chain_shim(const io_chain_t &io_chain) {
|
||||
ASSERT_IS_NOT_FORKED_CHILD();
|
||||
std::vector<dup2_action_t> chain;
|
||||
for (const auto &io_data : io_chain) {
|
||||
chain.push_back(dup2_action_t{io_data->source_fd, io_data->fd});
|
||||
}
|
||||
return dup2_list_resolve_chain(chain);
|
||||
}
|
||||
|
||||
bool output_stream_t::append_narrow_buffer(const separated_buffer_t &buffer) {
|
||||
for (const auto &rhs_elem : buffer.elements()) {
|
||||
if (!append_with_separation(str2wcstring(rhs_elem.contents), rhs_elem.separation, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool output_stream_t::append_with_separation(const wchar_t *s, size_t len, separation_type_t type,
|
||||
bool want_newline) {
|
||||
if (type == separation_type_t::explicitly && want_newline) {
|
||||
// Try calling "append" less - it might write() to an fd
|
||||
wcstring buf{s, len};
|
||||
buf.push_back(L'\n');
|
||||
return append(buf);
|
||||
} else {
|
||||
return append(s, len);
|
||||
}
|
||||
}
|
||||
|
||||
const wcstring &output_stream_t::contents() const { return g_empty_string; }
|
||||
|
||||
int output_stream_t::flush_and_check_error() { return STATUS_CMD_OK; }
|
||||
|
||||
fd_output_stream_t::fd_output_stream_t(int fd) : fd_(fd), sigcheck_(topic_t::sighupint) {
|
||||
assert(fd_ >= 0 && "Invalid fd");
|
||||
}
|
||||
|
||||
bool fd_output_stream_t::append(const wchar_t *s, size_t amt) {
|
||||
if (errored_) return false;
|
||||
int res = wwrite_to_fd(s, amt, this->fd_);
|
||||
if (res < 0) {
|
||||
// Some of our builtins emit multiple screens worth of data sent to a pager (the primary
|
||||
// example being the `history` builtin) and receiving SIGINT should be considered normal and
|
||||
// non-exceptional (user request to abort via Ctrl-C), meaning we shouldn't print an error.
|
||||
if (errno == EINTR && sigcheck_.check()) {
|
||||
// We have two options here: we can either return false without setting errored_ to
|
||||
// true (*this* write will be silently aborted but the onus is on the caller to check
|
||||
// the return value and skip future calls to `append()`) or we can flag the entire
|
||||
// output stream as errored, causing us to both return false and skip any future writes.
|
||||
// We're currently going with the latter, especially seeing as no callers currently
|
||||
// check the result of `append()` (since it was always a void function before).
|
||||
} else if (errno != EPIPE) {
|
||||
wperror(L"write");
|
||||
}
|
||||
errored_ = true;
|
||||
}
|
||||
return !errored_;
|
||||
}
|
||||
|
||||
int fd_output_stream_t::flush_and_check_error() {
|
||||
// Return a generic 1 on any write failure.
|
||||
return errored_ ? STATUS_CMD_ERROR : STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
bool null_output_stream_t::append(const wchar_t *, size_t) { return true; }
|
||||
|
||||
std::unique_ptr<io_streams_t> make_null_io_streams_ffi() {
|
||||
// Temporary test helper.
|
||||
static null_output_stream_t *null = new null_output_stream_t();
|
||||
return std::make_unique<io_streams_t>(*null, *null);
|
||||
}
|
||||
|
||||
std::unique_ptr<io_streams_t> make_test_io_streams_ffi() {
|
||||
// Temporary test helper.
|
||||
auto streams = std::make_unique<owning_io_streams_t>();
|
||||
streams->stdin_is_directly_redirected = false; // read from argv instead of stdin
|
||||
return streams;
|
||||
}
|
||||
|
||||
wcstring get_test_output_ffi(const io_streams_t &streams) {
|
||||
string_output_stream_t *out = static_cast<string_output_stream_t *>(&streams.out);
|
||||
if (out == nullptr) {
|
||||
return wcstring();
|
||||
}
|
||||
return out->contents();
|
||||
}
|
||||
|
||||
bool string_output_stream_t::append(const wchar_t *s, size_t amt) {
|
||||
contents_.append(s, amt);
|
||||
return true;
|
||||
}
|
||||
|
||||
const wcstring &string_output_stream_t::contents() const { return contents_; }
|
||||
|
||||
bool buffered_output_stream_t::append(const wchar_t *s, size_t amt) {
|
||||
return buffer_->append(wcs2string(s, amt));
|
||||
}
|
||||
|
||||
bool buffered_output_stream_t::append_with_separation(const wchar_t *s, size_t len,
|
||||
separation_type_t type, bool want_newline) {
|
||||
UNUSED(want_newline);
|
||||
return buffer_->append(wcs2string(s, len), type);
|
||||
}
|
||||
|
||||
int buffered_output_stream_t::flush_and_check_error() {
|
||||
if (buffer_->discarded()) {
|
||||
return STATUS_READ_TOO_MUCH;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
265
src/io.h
265
src/io.h
@@ -13,11 +13,22 @@
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "cxx.h"
|
||||
#include "fds.h"
|
||||
#include "global_safety.h"
|
||||
#include "redirection.h"
|
||||
#include "signals.h"
|
||||
#include "topic_monitor.h"
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "io.rs.h"
|
||||
#else
|
||||
struct IoChain;
|
||||
struct IoStreams;
|
||||
struct OutputStreamFfi;
|
||||
#endif
|
||||
using output_stream_t = OutputStreamFfi;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
// null_output_stream_t
|
||||
|
||||
using std::shared_ptr;
|
||||
|
||||
@@ -278,260 +289,10 @@ class io_bufferfill_t final : public io_data_t {
|
||||
struct callback_args_t;
|
||||
struct autoclose_fd_t2;
|
||||
|
||||
/// An io_buffer_t is a buffer which can populate itself by reading from an fd.
|
||||
/// It is not an io_data_t.
|
||||
class io_buffer_t {
|
||||
public:
|
||||
explicit io_buffer_t(size_t limit) : buffer_(limit) {}
|
||||
|
||||
~io_buffer_t();
|
||||
|
||||
/// Append a string to the buffer.
|
||||
bool append(std::string &&str, separation_type_t type = separation_type_t::inferred) {
|
||||
return buffer_.acquire()->append(std::move(str), type);
|
||||
}
|
||||
|
||||
/// \return true if output was discarded due to exceeding the read limit.
|
||||
bool discarded() { return buffer_.acquire()->discarded(); }
|
||||
|
||||
/// FFI callback workaround.
|
||||
void item_callback(autoclose_fd_t2 &fd, uint8_t reason, callback_args_t *args);
|
||||
|
||||
private:
|
||||
/// Read some, filling the buffer. The buffer is passed in to enforce that the append lock is
|
||||
/// held. \return positive on success, 0 if closed, -1 on error (in which case errno will be
|
||||
/// set).
|
||||
ssize_t read_once(int fd, acquired_lock<separated_buffer_t> &buff);
|
||||
|
||||
/// Begin the fill operation, reading from the given fd in the background.
|
||||
void begin_filling(autoclose_fd_t readfd);
|
||||
|
||||
/// End the background fillthread operation, and return the buffer, transferring ownership.
|
||||
separated_buffer_t complete_background_fillthread_and_take_buffer();
|
||||
|
||||
/// Helper to return whether the fillthread is running.
|
||||
bool fillthread_running() const { return fill_waiter_.get() != nullptr; }
|
||||
|
||||
/// Buffer storing what we have read.
|
||||
owning_lock<separated_buffer_t> buffer_;
|
||||
|
||||
/// Atomic flag indicating our fillthread should shut down.
|
||||
relaxed_atomic_bool_t shutdown_fillthread_{false};
|
||||
|
||||
/// A promise, allowing synchronization with the background fill operation.
|
||||
/// The operation has a reference to this as well, and fulfills this promise when it exits.
|
||||
std::shared_ptr<std::promise<void>> fill_waiter_{};
|
||||
|
||||
/// The item id of our background fillthread fd monitor item.
|
||||
uint64_t item_id_{0};
|
||||
|
||||
friend io_bufferfill_t;
|
||||
};
|
||||
|
||||
using io_data_ref_t = std::shared_ptr<const io_data_t>;
|
||||
|
||||
class io_chain_t : public std::vector<io_data_ref_t> {
|
||||
public:
|
||||
using std::vector<io_data_ref_t>::vector;
|
||||
// user-declared ctor to allow const init. Do not default this, it will break the build.
|
||||
io_chain_t() {}
|
||||
|
||||
/// autocxx falls over with this so hide it.
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
void remove(const io_data_ref_t &element);
|
||||
void push_back(io_data_ref_t element);
|
||||
#endif
|
||||
bool append(const io_chain_t &chain);
|
||||
|
||||
/// \return the last io redirection in the chain for the specified file descriptor, or nullptr
|
||||
/// if none.
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
io_data_ref_t io_for_fd(int fd) const;
|
||||
#endif
|
||||
|
||||
/// Attempt to resolve a list of redirection specs to IOs, appending to 'this'.
|
||||
/// \return true on success, false on error, in which case an error will have been printed.
|
||||
bool append_from_specs(const redirection_spec_list_t &specs, const wcstring &pwd);
|
||||
|
||||
/// Output debugging information to stderr.
|
||||
void print() const;
|
||||
};
|
||||
using io_chain_t = IoChain;
|
||||
|
||||
dup2_list_t dup2_list_resolve_chain_shim(const io_chain_t &io_chain);
|
||||
|
||||
/// Base class representing the output that a builtin can generate.
|
||||
/// This has various subclasses depending on the ultimate output destination.
|
||||
class output_stream_t : noncopyable_t, nonmovable_t {
|
||||
public:
|
||||
/// Required override point. The output stream receives a string \p s with \p amt chars.
|
||||
virtual bool append(const wchar_t *s, size_t amt) = 0;
|
||||
|
||||
/// \return any internally buffered contents.
|
||||
/// This is only implemented for a string_output_stream; others flush data to their underlying
|
||||
/// receiver (fd, or separated buffer) immediately and so will return an empty string here.
|
||||
virtual const wcstring &contents() const;
|
||||
|
||||
/// Flush any unwritten data to the underlying device, and return an error code.
|
||||
/// A 0 code indicates success. The base implementation returns 0.
|
||||
virtual int flush_and_check_error();
|
||||
|
||||
/// An optional override point. This is for explicit separation.
|
||||
/// \param want_newline this is true if the output item should be ended with a newline. This
|
||||
/// is only relevant if we are printing the output to a stream,
|
||||
virtual bool append_with_separation(const wchar_t *s, size_t len, separation_type_t type,
|
||||
bool want_newline = true);
|
||||
|
||||
/// The following are all convenience overrides.
|
||||
bool append_with_separation(const wcstring &s, separation_type_t type,
|
||||
bool want_newline = true) {
|
||||
return append_with_separation(s.data(), s.size(), type, want_newline);
|
||||
}
|
||||
|
||||
/// Append a string.
|
||||
bool append(const wcstring &s) { return append(s.data(), s.size()); }
|
||||
bool append(const wchar_t *s) { return append(s, std::wcslen(s)); }
|
||||
|
||||
/// Append a char.
|
||||
bool push(wchar_t s) { return append(&s, 1); }
|
||||
|
||||
// Append data from a narrow buffer, widening it.
|
||||
bool append_narrow_buffer(const separated_buffer_t &buffer);
|
||||
|
||||
/// Append a format string.
|
||||
bool append_format(const wchar_t *format, ...) {
|
||||
va_list va;
|
||||
va_start(va, format);
|
||||
bool r = append_formatv(format, va);
|
||||
va_end(va);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
bool append_formatv(const wchar_t *format, va_list va) {
|
||||
return append(vformat_string(format, va));
|
||||
}
|
||||
|
||||
output_stream_t() = default;
|
||||
virtual ~output_stream_t() = default;
|
||||
};
|
||||
|
||||
/// A null output stream which ignores all writes.
|
||||
class null_output_stream_t final : public output_stream_t {
|
||||
virtual bool append(const wchar_t *s, size_t amt) override;
|
||||
};
|
||||
|
||||
/// An output stream for builtins which outputs to an fd.
|
||||
/// Note the fd may be something like stdout; there is no ownership implied here.
|
||||
class fd_output_stream_t final : public output_stream_t {
|
||||
public:
|
||||
/// Construct from a file descriptor, which must be nonegative.
|
||||
explicit fd_output_stream_t(int fd);
|
||||
|
||||
int flush_and_check_error() override;
|
||||
|
||||
bool append(const wchar_t *s, size_t amt) override;
|
||||
|
||||
private:
|
||||
/// The file descriptor to write to.
|
||||
const int fd_;
|
||||
|
||||
/// Used to check if a SIGINT has been received when EINTR is encountered
|
||||
sigchecker_t sigcheck_;
|
||||
|
||||
/// Whether we have received an error.
|
||||
bool errored_{false};
|
||||
};
|
||||
|
||||
/// A simple output stream which buffers into a wcstring.
|
||||
class string_output_stream_t final : public output_stream_t {
|
||||
public:
|
||||
string_output_stream_t() = default;
|
||||
bool append(const wchar_t *s, size_t amt) override;
|
||||
|
||||
/// \return the wcstring containing the output.
|
||||
const wcstring &contents() const override;
|
||||
|
||||
private:
|
||||
wcstring contents_;
|
||||
};
|
||||
|
||||
/// An output stream for builtins which writes into a separated buffer.
|
||||
class buffered_output_stream_t final : public output_stream_t {
|
||||
public:
|
||||
explicit buffered_output_stream_t(std::shared_ptr<io_buffer_t> buffer)
|
||||
: buffer_(std::move(buffer)) {
|
||||
assert(buffer_ && "Buffer must not be null");
|
||||
}
|
||||
|
||||
bool append(const wchar_t *s, size_t amt) override;
|
||||
bool append_with_separation(const wchar_t *s, size_t len, separation_type_t type,
|
||||
bool want_newline) override;
|
||||
int flush_and_check_error() override;
|
||||
|
||||
private:
|
||||
/// The buffer we are filling.
|
||||
std::shared_ptr<io_buffer_t> buffer_;
|
||||
};
|
||||
|
||||
class IoStreams;
|
||||
using io_streams_t = IoStreams;
|
||||
|
||||
class IoStreams : noncopyable_t {
|
||||
public:
|
||||
// Streams for out and err.
|
||||
output_stream_t &out;
|
||||
output_stream_t &err;
|
||||
|
||||
// fd representing stdin. This is not closed by the destructor.
|
||||
// Note: if stdin is explicitly closed by `<&-` then this is -1!
|
||||
int stdin_fd{-1};
|
||||
|
||||
// Whether stdin is "directly redirected," meaning it is the recipient of a pipe (foo | cmd) or
|
||||
// direct redirection (cmd < foo.txt). An "indirect redirection" would be e.g.
|
||||
// begin ; cmd ; end < foo.txt
|
||||
// If stdin is closed (cmd <&-) this is false.
|
||||
bool stdin_is_directly_redirected{false};
|
||||
|
||||
// Indicates whether stdout and stderr are specifically piped.
|
||||
// If this is set, then the is_redirected flags must also be set.
|
||||
bool out_is_piped{false};
|
||||
bool err_is_piped{false};
|
||||
|
||||
// Indicates whether stdout and stderr are at all redirected (e.g. to a file or piped).
|
||||
bool out_is_redirected{false};
|
||||
bool err_is_redirected{false};
|
||||
|
||||
// Actual IO redirections. This is only used by the source builtin. Unowned.
|
||||
const io_chain_t *io_chain{nullptr};
|
||||
|
||||
// The job group of the job, if any. This enables builtins which run more code like eval() to
|
||||
// share pgid.
|
||||
// FIXME: this is awkwardly placed.
|
||||
std::shared_ptr<job_group_t> job_group{};
|
||||
|
||||
IoStreams(output_stream_t &out, output_stream_t &err) : out(out), err(err) {}
|
||||
virtual ~IoStreams() = default;
|
||||
|
||||
/// autocxx junk.
|
||||
output_stream_t &get_out() { return out; };
|
||||
output_stream_t &get_err() { return err; };
|
||||
IoStreams(const io_streams_t &) = delete;
|
||||
bool get_out_redirected() { return out_is_redirected; };
|
||||
bool get_err_redirected() { return err_is_redirected; };
|
||||
bool ffi_stdin_is_directly_redirected() const { return stdin_is_directly_redirected; };
|
||||
int ffi_stdin_fd() const { return stdin_fd; };
|
||||
};
|
||||
|
||||
/// FFI helper.
|
||||
class owning_io_streams_t : public io_streams_t {
|
||||
public:
|
||||
string_output_stream_t out_storage;
|
||||
null_output_stream_t err_storage;
|
||||
owning_io_streams_t() : io_streams_t(out_storage, err_storage) {}
|
||||
};
|
||||
|
||||
std::unique_ptr<io_streams_t> make_null_io_streams_ffi();
|
||||
std::unique_ptr<io_streams_t> make_test_io_streams_ffi();
|
||||
wcstring get_test_output_ffi(const io_streams_t &streams);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Utilities for io redirection.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "operation_context.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "env.h"
|
||||
|
||||
bool no_cancel() { return false; }
|
||||
|
||||
operation_context_t::operation_context_t(std::shared_ptr<parser_t> parser,
|
||||
const environment_t &vars, cancel_checker_t cancel_checker,
|
||||
size_t expansion_limit)
|
||||
: parser(std::move(parser)),
|
||||
vars(vars),
|
||||
expansion_limit(expansion_limit),
|
||||
cancel_checker(std::move(cancel_checker)) {}
|
||||
|
||||
operation_context_t operation_context_t::empty() {
|
||||
static const null_environment_t nullenv{};
|
||||
return operation_context_t{nullenv};
|
||||
}
|
||||
|
||||
operation_context_t operation_context_t::globals() {
|
||||
return operation_context_t{env_stack_t::globals()};
|
||||
}
|
||||
|
||||
operation_context_t::~operation_context_t() = default;
|
||||
@@ -1,18 +1,15 @@
|
||||
#ifndef FISH_OPERATION_CONTEXT_H
|
||||
#define FISH_OPERATION_CONTEXT_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "common.h"
|
||||
struct OperationContext;
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "operation_context.rs.h"
|
||||
#else
|
||||
#endif
|
||||
|
||||
class environment_t;
|
||||
class Parser; using parser_t = Parser;
|
||||
struct job_group_t;
|
||||
|
||||
/// A common helper which always returns false.
|
||||
bool no_cancel();
|
||||
using operation_context_t = OperationContext;
|
||||
|
||||
/// Default limits for expansion.
|
||||
enum expansion_limit_t : size_t {
|
||||
@@ -23,50 +20,4 @@ enum expansion_limit_t : size_t {
|
||||
kExpansionLimitBackground = 512,
|
||||
};
|
||||
|
||||
/// A operation_context_t is a simple property bag which wraps up data needed for highlighting,
|
||||
/// expansion, completion, and more.
|
||||
class operation_context_t {
|
||||
public:
|
||||
// The parser, if this is a foreground operation. If this is a background operation, this may be
|
||||
// nullptr.
|
||||
std::shared_ptr<parser_t> parser;
|
||||
|
||||
// The set of variables. It is the creator's responsibility to ensure this lives as log as the
|
||||
// context itself.
|
||||
const environment_t &vars;
|
||||
|
||||
// The limit in the number of expansions which should be produced.
|
||||
const size_t expansion_limit;
|
||||
|
||||
/// The job group of the parental job.
|
||||
/// This is used only when expanding command substitutions. If this is set, any jobs created by
|
||||
/// the command substitutions should use this tree.
|
||||
std::shared_ptr<job_group_t> job_group{};
|
||||
|
||||
// A function which may be used to poll for cancellation.
|
||||
cancel_checker_t cancel_checker;
|
||||
|
||||
// Invoke the cancel checker. \return if we should cancel.
|
||||
bool check_cancel() const { return cancel_checker(); }
|
||||
|
||||
// \return an "empty" context which contains no variables, no parser, and never cancels.
|
||||
static operation_context_t empty();
|
||||
|
||||
// \return an operation context that contains only global variables, no parser, and never
|
||||
// cancels.
|
||||
static operation_context_t globals();
|
||||
|
||||
/// Construct from a full set of properties.
|
||||
operation_context_t(std::shared_ptr<parser_t> parser, const environment_t &vars,
|
||||
cancel_checker_t cancel_checker,
|
||||
size_t expansion_limit = kExpansionLimitDefault);
|
||||
|
||||
/// Construct from vars alone.
|
||||
explicit operation_context_t(const environment_t &vars,
|
||||
size_t expansion_limit = kExpansionLimitDefault)
|
||||
: operation_context_t(nullptr, vars, no_cancel, expansion_limit) {}
|
||||
|
||||
~operation_context_t();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -83,7 +83,8 @@ rgb_color_t parse_color(const env_var_t &var, bool is_background) {
|
||||
|
||||
bool next_is_background = false;
|
||||
wcstring color_name;
|
||||
for (const wcstring &next : var.as_list()) {
|
||||
auto vals = var.as_list();
|
||||
for (const wcstring &next : vals) {
|
||||
color_name.clear();
|
||||
if (is_background) {
|
||||
if (color_name.empty() && next_is_background) {
|
||||
|
||||
3
src/output.h
Normal file → Executable file
3
src/output.h
Normal file → Executable file
@@ -16,7 +16,8 @@
|
||||
struct outputter_t;
|
||||
#endif
|
||||
|
||||
class env_var_t;
|
||||
#include "env.h"
|
||||
|
||||
rgb_color_t parse_color(const env_var_t &var, bool is_background);
|
||||
|
||||
/// Sets what colors are supported.
|
||||
|
||||
@@ -29,6 +29,19 @@
|
||||
using comp_t = pager_t::comp_t;
|
||||
using comp_info_list_t = std::vector<comp_t>;
|
||||
|
||||
comp_t &comp_t::operator=(const comp_t &other) {
|
||||
if (this == &other) return *this;
|
||||
comp = other.comp;
|
||||
desc = other.desc;
|
||||
representative = other.representative->clone();
|
||||
colors = other.colors;
|
||||
comp_width = other.comp_width;
|
||||
desc_width = other.desc_width;
|
||||
return *this;
|
||||
}
|
||||
|
||||
comp_t::comp_t(const comp_t &other) { *this = other; }
|
||||
|
||||
/// The minimum width (in characters) the terminal must to show completions at all.
|
||||
#define PAGER_MIN_WIDTH 16
|
||||
|
||||
@@ -333,7 +346,7 @@ static comp_info_list_t process_completions_into_infos(const completion_list_t &
|
||||
|
||||
// Append the single completion string. We may later merge these into multiple.
|
||||
comp_info->comp.push_back(escape_string(
|
||||
comp.completion, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED | ESCAPE_SYMBOLIC));
|
||||
*comp.completion(), ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED | ESCAPE_SYMBOLIC));
|
||||
if (comp.replaces_commandline()
|
||||
// HACK We want to render a full shell command, with syntax highlighting. Above we
|
||||
// escape nonprintables, which might make the rendered command longer than the original
|
||||
@@ -346,16 +359,16 @@ static comp_info_list_t process_completions_into_infos(const completion_list_t &
|
||||
// then writing a variant of escape_string() that adjusts highlighting according
|
||||
// so it matches the escaped string.
|
||||
&& MB_CUR_MAX > 1) {
|
||||
highlight_shell(comp.completion, comp_info->colors, operation_context_t::empty());
|
||||
highlight_shell(*comp.completion(), comp_info->colors, *empty_operation_context());
|
||||
assert(comp_info->comp.back().size() >= comp_info->colors.size());
|
||||
}
|
||||
|
||||
// Append the mangled description.
|
||||
comp_info->desc = comp.description;
|
||||
comp_info->desc = std::move(*comp.description());
|
||||
mangle_1_completion_description(&comp_info->desc);
|
||||
|
||||
// Set the representative completion.
|
||||
comp_info->representative = comp;
|
||||
comp_info->representative = comp.clone();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -578,7 +591,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co
|
||||
|
||||
// We limit the width to term_width - 1.
|
||||
highlight_spec_t underline{};
|
||||
underline.force_underline = true;
|
||||
underline->force_underline = true;
|
||||
|
||||
size_t search_field_remaining = term_width - 1;
|
||||
search_field_remaining -= print_max(SEARCH_FIELD_PROMPT, highlight_role_t::normal,
|
||||
@@ -878,7 +891,7 @@ const completion_t *pager_t::selected_completion(const page_rendering_t &renderi
|
||||
const completion_t *result = nullptr;
|
||||
size_t idx = visual_selected_completion_index(rendering.rows, rendering.cols);
|
||||
if (idx != PAGER_SELECTION_NONE) {
|
||||
result = &completion_infos.at(idx).representative;
|
||||
result = &*completion_infos.at(idx).representative;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "complete.h"
|
||||
#include "cxx.h"
|
||||
#include "highlight.h"
|
||||
#include "reader.h"
|
||||
#include "screen.h"
|
||||
@@ -87,7 +88,7 @@ class pager_t {
|
||||
/// The description.
|
||||
wcstring desc{};
|
||||
/// The representative completion.
|
||||
completion_t representative{L""};
|
||||
rust::Box<completion_t> representative = new_completion();
|
||||
/// The per-character highlighting, used when this is a full shell command.
|
||||
std::vector<highlight_spec_t> colors{};
|
||||
/// On-screen width of the completion string.
|
||||
@@ -95,6 +96,12 @@ class pager_t {
|
||||
/// On-screen width of the description information.
|
||||
size_t desc_width{0};
|
||||
|
||||
comp_t() = default;
|
||||
comp_t(const comp_t &other);
|
||||
comp_t &operator=(const comp_t &other);
|
||||
comp_t(comp_t &&) = default;
|
||||
comp_t &operator=(comp_t &&) = default;
|
||||
|
||||
// Our text looks like this:
|
||||
// completion (description)
|
||||
// Two spaces separating, plus parens, yields 4 total extra space
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
// Provides the "linkage" between an ast and actual execution structures (job_t, etc.).
|
||||
#ifndef FISH_PARSE_EXECUTION_H
|
||||
#define FISH_PARSE_EXECUTION_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "ast.h" // IWYU pragma: keep
|
||||
#include "common.h"
|
||||
#include "io.h"
|
||||
#include "maybe.h"
|
||||
#include "parse_constants.h"
|
||||
#include "parse_tree.h"
|
||||
#include "proc.h"
|
||||
#include "redirection.h"
|
||||
|
||||
class block_t;
|
||||
class operation_context_t;
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
/// An eval_result represents evaluation errors including wildcards which failed to match, syntax
|
||||
/// errors, or other expansion errors. It also tracks when evaluation was skipped due to signal
|
||||
/// cancellation. Note it does not track the exit status of commands.
|
||||
enum class end_execution_reason_t {
|
||||
/// Evaluation was successfull.
|
||||
ok,
|
||||
|
||||
/// Evaluation was skipped due to control flow (break or return).
|
||||
control_flow,
|
||||
|
||||
/// Evaluation was cancelled, e.g. because of a signal or exit.
|
||||
cancelled,
|
||||
|
||||
/// A parse error or failed expansion (but not an error exit status from a command).
|
||||
error,
|
||||
};
|
||||
|
||||
class parse_execution_context_t : noncopyable_t {
|
||||
private:
|
||||
rust::Box<parsed_source_ref_t> pstree;
|
||||
parser_t *const parser;
|
||||
const operation_context_t &ctx;
|
||||
|
||||
// If set, one of our processes received a cancellation signal (INT or QUIT) so we are
|
||||
// unwinding.
|
||||
int cancel_signal{0};
|
||||
|
||||
// The currently executing job node, used to indicate the line number.
|
||||
const ast::job_pipeline_t *executing_job_node{};
|
||||
|
||||
// Cached line number information.
|
||||
size_t cached_lineno_offset = 0;
|
||||
int cached_lineno_count = 0;
|
||||
|
||||
/// The block IO chain.
|
||||
/// For example, in `begin; foo ; end < file.txt` this would have the 'file.txt' IO.
|
||||
io_chain_t block_io{};
|
||||
|
||||
// Check to see if we should end execution.
|
||||
// \return the eval result to end with, or none() to continue on.
|
||||
// This will never return end_execution_reason_t::ok.
|
||||
maybe_t<end_execution_reason_t> check_end_execution() const;
|
||||
|
||||
// Report an error, setting $status to \p status. Always returns
|
||||
// 'end_execution_reason_t::error'.
|
||||
end_execution_reason_t report_error(int status, const ast::node_t &node, const wchar_t *fmt,
|
||||
...) const;
|
||||
end_execution_reason_t report_errors(int status, const parse_error_list_t &error_list) const;
|
||||
|
||||
/// Command not found support.
|
||||
end_execution_reason_t handle_command_not_found(const wcstring &cmd,
|
||||
const ast::decorated_statement_t &statement,
|
||||
int err_code);
|
||||
|
||||
// Utilities
|
||||
wcstring get_source(const ast::node_t &node) const;
|
||||
const ast::decorated_statement_t *infinite_recursive_statement_in_job_list(
|
||||
const ast::job_list_t &jobs, wcstring *out_func_name) const;
|
||||
|
||||
// Expand a command which may contain variables, producing an expand command and possibly
|
||||
// arguments. Prints an error message on error.
|
||||
end_execution_reason_t expand_command(const ast::decorated_statement_t &statement,
|
||||
wcstring *out_cmd, std::vector<wcstring> *out_args) const;
|
||||
|
||||
/// Indicates whether a job is a simple block (one block, no redirections).
|
||||
bool job_is_simple_block(const ast::job_pipeline_t &job) const;
|
||||
|
||||
enum process_type_t process_type_for_command(const ast::decorated_statement_t &statement,
|
||||
const wcstring &cmd) const;
|
||||
end_execution_reason_t apply_variable_assignments(
|
||||
process_t *proc, const ast::variable_assignment_list_t &variable_assignment_list,
|
||||
const block_t **block);
|
||||
|
||||
// These create process_t structures from statements.
|
||||
end_execution_reason_t populate_job_process(
|
||||
job_t *job, process_t *proc, const ast::statement_t &statement,
|
||||
const ast::variable_assignment_list_t &variable_assignments_list_t);
|
||||
end_execution_reason_t populate_not_process(job_t *job, process_t *proc,
|
||||
const ast::not_statement_t ¬_statement);
|
||||
end_execution_reason_t populate_plain_process(process_t *proc,
|
||||
const ast::decorated_statement_t &statement);
|
||||
|
||||
template <typename Type>
|
||||
end_execution_reason_t populate_block_process(process_t *proc,
|
||||
const ast::statement_t &statement,
|
||||
const Type &specific_statement);
|
||||
|
||||
// These encapsulate the actual logic of various (block) statements.
|
||||
end_execution_reason_t run_block_statement(const ast::block_statement_t &statement,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t run_for_statement(const ast::for_header_t &header,
|
||||
const ast::job_list_t &contents);
|
||||
end_execution_reason_t run_if_statement(const ast::if_statement_t &statement,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t run_switch_statement(const ast::switch_statement_t &statement);
|
||||
end_execution_reason_t run_while_statement(const ast::while_header_t &header,
|
||||
const ast::job_list_t &contents,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t run_function_statement(const ast::block_statement_t &statement,
|
||||
const ast::function_header_t &header);
|
||||
end_execution_reason_t run_begin_statement(const ast::job_list_t &contents);
|
||||
|
||||
enum globspec_t { failglob, nullglob };
|
||||
using ast_args_list_t = std::vector<const ast::argument_t *>;
|
||||
|
||||
static ast_args_list_t get_argument_nodes(const ast::argument_list_t &args);
|
||||
static ast_args_list_t get_argument_nodes(const ast::argument_or_redirection_list_t &args);
|
||||
|
||||
end_execution_reason_t expand_arguments_from_nodes(const ast_args_list_t &argument_nodes,
|
||||
std::vector<wcstring> *out_arguments,
|
||||
globspec_t glob_behavior);
|
||||
|
||||
// Determines the list of redirections for a node.
|
||||
end_execution_reason_t determine_redirections(const ast::argument_or_redirection_list_t &list,
|
||||
redirection_spec_list_t *out_redirections);
|
||||
|
||||
end_execution_reason_t run_1_job(const ast::job_pipeline_t &job,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t test_and_run_1_job_conjunction(const ast::job_conjunction_t &jc,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t run_job_conjunction(const ast::job_conjunction_t &job_expr,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t run_job_list(const ast::job_list_t &job_list_node,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t run_job_list(const ast::andor_job_list_t &job_list_node,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t populate_job_from_job_node(job_t *j, const ast::job_pipeline_t &job_node,
|
||||
const block_t *associated_block);
|
||||
|
||||
// Assign a job group to the given job.
|
||||
void setup_group(job_t *j);
|
||||
|
||||
// \return whether we should apply job control to our processes.
|
||||
bool use_job_control() const;
|
||||
|
||||
// Returns the line number of the node. Not const since it touches cached_lineno_offset.
|
||||
int line_offset_of_node(const ast::job_pipeline_t *node);
|
||||
int line_offset_of_character_at_offset(size_t offset);
|
||||
|
||||
public:
|
||||
/// Construct a context in preparation for evaluating a node in a tree, with the given block_io.
|
||||
/// The execution context may access the parser and parent job group (if any) through ctx.
|
||||
parse_execution_context_t(rust::Box<parsed_source_ref_t> pstree, const operation_context_t &ctx,
|
||||
io_chain_t block_io);
|
||||
|
||||
/// Returns the current line number, indexed from 1. Not const since it touches
|
||||
/// cached_lineno_offset.
|
||||
int get_current_line_number();
|
||||
|
||||
/// Returns the source offset, or -1.
|
||||
int get_current_source_offset() const;
|
||||
|
||||
/// Returns the source string.
|
||||
const wcstring &get_source() const { return pstree->src(); }
|
||||
|
||||
/// Return the parsed ast.
|
||||
const ast::ast_t &ast() const { return pstree->ast(); }
|
||||
|
||||
/// Start executing at the given node. Returns 0 if there was no error, 1 if there was an
|
||||
/// error.
|
||||
end_execution_reason_t eval_node(const ast::statement_t &statement,
|
||||
const block_t *associated_block);
|
||||
end_execution_reason_t eval_node(const ast::job_list_t &job_list,
|
||||
const block_t *associated_block);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <utility>
|
||||
|
||||
#include "ast.h"
|
||||
#include "builtin.h"
|
||||
#include "common.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
@@ -41,9 +40,6 @@
|
||||
/// Error message for arguments to 'end'
|
||||
#define END_ARG_ERR_MSG _(L"'end' does not take arguments. Did you forget a ';'?")
|
||||
|
||||
/// Error message when 'time' is in a pipeline.
|
||||
#define TIME_IN_PIPELINE_ERR_MSG _(L"The 'time' command may only be at the beginning of a pipeline")
|
||||
|
||||
/// Maximum length of a variable name to show in error reports before truncation
|
||||
static constexpr int var_err_len = 16;
|
||||
|
||||
@@ -616,13 +612,6 @@ static bool append_syntax_error(parse_error_list_t *errors, size_t source_locati
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns true if the specified command is a builtin that may not be used in a pipeline.
|
||||
static const wchar_t *const forbidden_pipe_commands[] = {L"exec", L"case", L"break", L"return",
|
||||
L"continue"};
|
||||
static bool parser_is_pipe_forbidden(const wcstring &word) {
|
||||
return contains(forbidden_pipe_commands, word);
|
||||
}
|
||||
|
||||
bool parse_util_argument_is_help(const wcstring &s) { return s == L"-h" || s == L"--help"; }
|
||||
|
||||
// \return a pointer to the first argument node of an argument_or_redirection_list_t, or nullptr if
|
||||
@@ -751,7 +740,8 @@ parser_test_error_bits_t parse_util_detect_errors_in_argument(const ast::argumen
|
||||
parser_test_error_bits_t err = 0;
|
||||
|
||||
auto check_subtoken = [&arg_src, &out_errors, source_start](size_t begin, size_t end) -> int {
|
||||
auto maybe_unesc = unescape_string(arg_src.c_str() + begin, end - begin, UNESCAPE_SPECIAL);
|
||||
auto maybe_unesc = unescape_string(arg_src.c_str() + begin, end - begin, UNESCAPE_SPECIAL,
|
||||
STRING_STYLE_SCRIPT);
|
||||
if (!maybe_unesc) {
|
||||
if (out_errors) {
|
||||
const wchar_t *fmt = L"Invalid token '%ls'";
|
||||
@@ -898,165 +888,6 @@ static bool detect_errors_in_backgrounded_job(const ast::job_pipeline_t &job,
|
||||
return errored;
|
||||
}
|
||||
|
||||
/// Given a source buffer \p buff_src and decorated statement \p dst within it, return true if there
|
||||
/// is an error and false if not. \p storage may be used to reduce allocations.
|
||||
static bool detect_errors_in_decorated_statement(const wcstring &buff_src,
|
||||
const ast::decorated_statement_t &dst,
|
||||
wcstring *storage,
|
||||
parse_error_list_t *parse_errors) {
|
||||
using namespace ast;
|
||||
bool errored = false;
|
||||
auto source_start = dst.source_range().start;
|
||||
auto source_length = dst.source_range().length;
|
||||
const statement_decoration_t decoration = dst.decoration();
|
||||
|
||||
// Determine if the first argument is help.
|
||||
bool first_arg_is_help = false;
|
||||
if (const auto *arg = get_first_arg(dst.args_or_redirs())) {
|
||||
wcstring arg_src = *arg->source(buff_src);
|
||||
*storage = arg_src;
|
||||
first_arg_is_help = parse_util_argument_is_help(arg_src);
|
||||
}
|
||||
|
||||
// Get the statement we are part of.
|
||||
const statement_t &st = dst.ptr()->parent()->as_statement();
|
||||
|
||||
// Walk up to the job.
|
||||
const ast::job_pipeline_t *job = nullptr;
|
||||
for (auto cursor = dst.ptr()->parent(); job == nullptr; cursor = cursor->parent()) {
|
||||
assert(cursor->has_value() && "Reached root without finding a job");
|
||||
job = cursor->try_as_job_pipeline();
|
||||
}
|
||||
assert(job && "Should have found the job");
|
||||
|
||||
// Check our pipeline position.
|
||||
pipeline_position_t pipe_pos;
|
||||
if (job->continuation().empty()) {
|
||||
pipe_pos = pipeline_position_t::none;
|
||||
} else if (&job->statement() == &st) {
|
||||
pipe_pos = pipeline_position_t::first;
|
||||
} else {
|
||||
pipe_pos = pipeline_position_t::subsequent;
|
||||
}
|
||||
|
||||
// Check that we don't try to pipe through exec.
|
||||
bool is_in_pipeline = (pipe_pos != pipeline_position_t::none);
|
||||
if (is_in_pipeline && decoration == statement_decoration_t::exec) {
|
||||
errored = append_syntax_error(parse_errors, source_start, source_length,
|
||||
INVALID_PIPELINE_CMD_ERR_MSG, L"exec");
|
||||
}
|
||||
|
||||
// This is a somewhat stale check that 'and' and 'or' are not in pipelines, except at the
|
||||
// beginning. We can't disallow them as commands entirely because we need to support 'and
|
||||
// --help', etc.
|
||||
if (pipe_pos == pipeline_position_t::subsequent) {
|
||||
// We only reject it if we have no decoration.
|
||||
// `echo foo | command time something`
|
||||
// is entirely fair and valid.
|
||||
// Other current decorations like "exec"
|
||||
// are already forbidden.
|
||||
const auto &deco = dst.decoration();
|
||||
if (deco == statement_decoration_t::none) {
|
||||
// check if our command is 'and' or 'or'. This is very clumsy; we don't catch e.g. quoted
|
||||
// commands.
|
||||
wcstring command = *dst.command().source(buff_src);
|
||||
*storage = command;
|
||||
if (command == L"and" || command == L"or") {
|
||||
errored = append_syntax_error(parse_errors, source_start, source_length,
|
||||
INVALID_PIPELINE_CMD_ERR_MSG, command.c_str());
|
||||
}
|
||||
|
||||
// Similarly for time (#8841).
|
||||
if (command == L"time") {
|
||||
errored = append_syntax_error(parse_errors, source_start, source_length,
|
||||
TIME_IN_PIPELINE_ERR_MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $status specifically is invalid as a command,
|
||||
// to avoid people trying `if $status`.
|
||||
// We see this surprisingly regularly.
|
||||
wcstring com = *dst.command().source(buff_src);
|
||||
*storage = com;
|
||||
if (com == L"$status") {
|
||||
errored =
|
||||
append_syntax_error(parse_errors, source_start, source_length,
|
||||
_(L"$status is not valid as a command. See `help conditions`"));
|
||||
}
|
||||
|
||||
wcstring unexp_command = *dst.command().source(buff_src);
|
||||
*storage = unexp_command;
|
||||
if (!unexp_command.empty()) {
|
||||
// Check that we can expand the command.
|
||||
// Make a new error list so we can fix the offset for just those, then append later.
|
||||
wcstring command;
|
||||
auto new_errors = new_parse_error_list();
|
||||
if (expand_to_command_and_args(unexp_command, operation_context_t::empty(), &command,
|
||||
nullptr, &*new_errors,
|
||||
true /* skip wildcards */) == expand_result_t::error) {
|
||||
errored = true;
|
||||
}
|
||||
|
||||
// Check that pipes are sound.
|
||||
if (!errored && parser_is_pipe_forbidden(command) && is_in_pipeline) {
|
||||
errored = append_syntax_error(parse_errors, source_start, source_length,
|
||||
INVALID_PIPELINE_CMD_ERR_MSG, command.c_str());
|
||||
}
|
||||
|
||||
// Check that we don't break or continue from outside a loop.
|
||||
if (!errored && (command == L"break" || command == L"continue") && !first_arg_is_help) {
|
||||
// Walk up until we hit a 'for' or 'while' loop. If we hit a function first,
|
||||
// stop the search; we can't break an outer loop from inside a function.
|
||||
// This is a little funny because we can't tell if it's a 'for' or 'while'
|
||||
// loop from the ancestor alone; we need the header. That is, we hit a
|
||||
// block_statement, and have to check its header.
|
||||
bool found_loop = false;
|
||||
for (auto ancestor = dst.ptr(); ancestor->has_value(); ancestor = ancestor->parent()) {
|
||||
const auto *block = ancestor->try_as_block_statement();
|
||||
if (!block) continue;
|
||||
if (block->header().ptr()->typ() == type_t::for_header ||
|
||||
block->header().ptr()->typ() == type_t::while_header) {
|
||||
// This is a loop header, so we can break or continue.
|
||||
found_loop = true;
|
||||
break;
|
||||
} else if (block->header().ptr()->typ() == type_t::function_header) {
|
||||
// This is a function header, so we cannot break or
|
||||
// continue. We stop our search here.
|
||||
found_loop = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_loop) {
|
||||
errored = append_syntax_error(
|
||||
parse_errors, source_start, source_length,
|
||||
(command == L"break" ? INVALID_BREAK_ERR_MSG : INVALID_CONTINUE_ERR_MSG));
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we don't do an invalid builtin (issue #1252).
|
||||
if (!errored && decoration == statement_decoration_t::builtin) {
|
||||
wcstring command = unexp_command;
|
||||
if (expand_one(command, expand_flag::skip_cmdsubst, operation_context_t::empty(),
|
||||
parse_errors) &&
|
||||
!builtin_exists(unexp_command)) {
|
||||
errored = append_syntax_error(parse_errors, source_start, source_length,
|
||||
UNKNOWN_BUILTIN_ERR_MSG, unexp_command.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (parse_errors) {
|
||||
// The expansion errors here go from the *command* onwards,
|
||||
// so we need to offset them by the *command* offset,
|
||||
// excluding the decoration.
|
||||
new_errors->offset_source_start(dst.command().source_range().start);
|
||||
parse_errors->append(&*new_errors);
|
||||
}
|
||||
}
|
||||
return errored;
|
||||
}
|
||||
|
||||
// Given we have a trailing argument_or_redirection_list, like `begin; end > /dev/null`, verify that
|
||||
// there are no arguments in the list.
|
||||
static bool detect_errors_in_block_redirection_list(
|
||||
@@ -1068,8 +899,9 @@ static bool detect_errors_in_block_redirection_list(
|
||||
return false;
|
||||
}
|
||||
|
||||
parser_test_error_bits_t parse_util_detect_errors_ffi(const ast::ast_t* ast, const wcstring &buff_src,
|
||||
parse_error_list_t *out_errors) {
|
||||
parser_test_error_bits_t parse_util_detect_errors_ffi(const ast::ast_t *ast,
|
||||
const wcstring &buff_src,
|
||||
parse_error_list_t *out_errors) {
|
||||
return parse_util_detect_errors(*ast, buff_src, out_errors);
|
||||
}
|
||||
|
||||
@@ -1130,7 +962,8 @@ parser_test_error_bits_t parse_util_detect_errors(const ast::ast_t &ast, const w
|
||||
errored |= detect_errors_in_backgrounded_job(*job, out_errors);
|
||||
}
|
||||
} else if (const auto *stmt = node->try_as_decorated_statement()) {
|
||||
errored |= detect_errors_in_decorated_statement(buff_src, *stmt, &storage, out_errors);
|
||||
errored |=
|
||||
detect_errors_in_decorated_statement(buff_src, (size_t)stmt, (size_t)out_errors);
|
||||
} else if (const auto *block = node->try_as_block_statement()) {
|
||||
// If our 'end' had no source, we are unsourced.
|
||||
if (!block->end().ptr()->has_source()) has_unclosed_block = true;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "cxx.h"
|
||||
#include "maybe.h"
|
||||
#include "parse_constants.h"
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "parse_util.rs.h"
|
||||
#endif
|
||||
|
||||
struct Tok;
|
||||
using tok_t = Tok;
|
||||
|
||||
878
src/parser.cpp
878
src/parser.cpp
@@ -1,878 +0,0 @@
|
||||
// The fish parser. Contains functions for parsing and evaluating code.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "parser.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cwchar>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "ast.h"
|
||||
#include "common.h"
|
||||
#include "complete.h"
|
||||
#include "env.h"
|
||||
#include "event.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "fds.h"
|
||||
#include "flog.h"
|
||||
#include "function.h"
|
||||
#include "job_group.rs.h"
|
||||
#include "parse_constants.h"
|
||||
#include "parse_execution.h"
|
||||
#include "proc.h"
|
||||
#include "signals.h"
|
||||
#include "threads.rs.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
class io_chain_t;
|
||||
|
||||
// Given a file path, return something nicer. Currently we just "unexpand" tildes.
|
||||
static wcstring user_presentable_path(const wcstring &path, const environment_t &vars) {
|
||||
return replace_home_directory_with_tilde(path, vars);
|
||||
}
|
||||
|
||||
parser_t::Parser(std::shared_ptr<env_stack_t> vars, bool is_principal)
|
||||
: wait_handles(new_wait_handle_store_ffi()),
|
||||
variables(std::move(vars)),
|
||||
is_principal_(is_principal) {
|
||||
assert(variables.get() && "Null variables in parser initializer");
|
||||
int cwd = open_cloexec(".", O_RDONLY);
|
||||
if (cwd < 0) {
|
||||
perror("Unable to open the current working directory");
|
||||
return;
|
||||
}
|
||||
libdata().cwd_fd = std::make_shared<const autoclose_fd_t>(cwd);
|
||||
}
|
||||
|
||||
// Out of line destructor to enable forward declaration of parse_execution_context_t
|
||||
parser_t::~Parser() = default;
|
||||
|
||||
parser_t &parser_t::principal_parser() {
|
||||
static const std::shared_ptr<parser_t> principal{
|
||||
new parser_t(env_stack_t::principal_ref(), true)};
|
||||
principal->assert_can_execute();
|
||||
return *principal;
|
||||
}
|
||||
|
||||
parser_t *parser_t::principal_parser_ffi() { return &principal_parser(); }
|
||||
|
||||
void parser_t::assert_can_execute() const { ASSERT_IS_MAIN_THREAD(); }
|
||||
|
||||
rust::Box<WaitHandleStoreFFI> &parser_t::get_wait_handles_ffi() { return wait_handles; }
|
||||
|
||||
const rust::Box<WaitHandleStoreFFI> &parser_t::get_wait_handles_ffi() const { return wait_handles; }
|
||||
|
||||
int parser_t::set_var_and_fire(const wcstring &key, env_mode_flags_t mode,
|
||||
std::vector<wcstring> vals) {
|
||||
int res = vars().set(key, mode, std::move(vals));
|
||||
if (res == ENV_OK) {
|
||||
event_fire(*this, *new_event_variable_set(key));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
int parser_t::set_var_and_fire(const wcstring &key, env_mode_flags_t mode, wcstring val) {
|
||||
std::vector<wcstring> vals;
|
||||
vals.push_back(std::move(val));
|
||||
return set_var_and_fire(key, mode, std::move(vals));
|
||||
}
|
||||
|
||||
void parser_t::sync_uvars_and_fire(bool always) {
|
||||
if (this->syncs_uvars_) {
|
||||
auto evts = this->vars().universal_sync(always);
|
||||
for (const auto &evt : evts) {
|
||||
event_fire(*this, *evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
block_t *parser_t::push_block(block_t &&block) {
|
||||
block.src_lineno = parser_t::get_lineno();
|
||||
block.src_filename = parser_t::current_filename();
|
||||
if (block.type() != block_type_t::top) {
|
||||
bool new_scope = (block.type() == block_type_t::function_call);
|
||||
vars().push(new_scope);
|
||||
block.wants_pop_env = true;
|
||||
}
|
||||
|
||||
// Push it onto our list and return a pointer to it.
|
||||
// Note that deques do not move their contents so this is safe.
|
||||
this->block_list.push_front(std::move(block));
|
||||
return &this->block_list.front();
|
||||
}
|
||||
|
||||
void parser_t::pop_block(const block_t *expected) {
|
||||
assert(expected && expected == &this->block_list.at(0) && "Unexpected block");
|
||||
bool pop_env = expected->wants_pop_env;
|
||||
block_list.pop_front(); // beware, this deallocates 'expected'.
|
||||
if (pop_env) vars().pop();
|
||||
}
|
||||
|
||||
const block_t *parser_t::block_at_index(size_t idx) const {
|
||||
return idx < block_list.size() ? &block_list[idx] : nullptr;
|
||||
}
|
||||
|
||||
block_t *parser_t::block_at_index(size_t idx) {
|
||||
return idx < block_list.size() ? &block_list[idx] : nullptr;
|
||||
}
|
||||
|
||||
/// Print profiling information to the specified stream.
|
||||
static void print_profile(const std::deque<profile_item_t> &items, FILE *out) {
|
||||
for (size_t idx = 0; idx < items.size(); idx++) {
|
||||
const profile_item_t &item = items.at(idx);
|
||||
if (item.skipped || item.cmd.empty()) continue;
|
||||
|
||||
long long total_time = item.duration;
|
||||
|
||||
// Compute the self time as the total time, minus the total time consumed by subsequent
|
||||
// items exactly one eval level deeper.
|
||||
long long self_time = item.duration;
|
||||
for (size_t i = idx + 1; i < items.size(); i++) {
|
||||
const profile_item_t &nested_item = items.at(i);
|
||||
if (nested_item.skipped) continue;
|
||||
|
||||
// If the eval level is not larger, then we have exhausted nested items.
|
||||
if (nested_item.level <= item.level) break;
|
||||
|
||||
// If the eval level is exactly one more than our level, it is a directly nested item.
|
||||
if (nested_item.level == item.level + 1) self_time -= nested_item.duration;
|
||||
}
|
||||
|
||||
if (std::fwprintf(out, L"%lld\t%lld\t", self_time, total_time) < 0) {
|
||||
wperror(L"fwprintf");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < item.level; i++) {
|
||||
if (std::fwprintf(out, L"-") < 0) {
|
||||
wperror(L"fwprintf");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (std::fwprintf(out, L"> %ls\n", item.cmd.c_str()) < 0) {
|
||||
wperror(L"fwprintf");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void parser_t::clear_profiling() { profile_items.clear(); }
|
||||
|
||||
void parser_t::emit_profiling(const char *path) const {
|
||||
// Save profiling information. OK to not use CLO_EXEC here because this is called while fish is
|
||||
// exiting (and hence will not fork).
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
FLOGF(warning, _(L"Could not write profiling information to file '%s'"), path);
|
||||
} else {
|
||||
if (std::fwprintf(f, _(L"Time\tSum\tCommand\n"), profile_items.size()) < 0) {
|
||||
wperror(L"fwprintf");
|
||||
} else {
|
||||
print_profile(profile_items, f);
|
||||
}
|
||||
|
||||
if (fclose(f)) {
|
||||
wperror(L"fclose");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion_list_t parser_t::expand_argument_list(const wcstring &arg_list_src,
|
||||
expand_flags_t eflags,
|
||||
const operation_context_t &ctx) {
|
||||
// Parse the string as an argument list.
|
||||
auto ast = ast_parse_argument_list(arg_list_src);
|
||||
if (ast->errored()) {
|
||||
// Failed to parse. Here we expect to have reported any errors in test_args.
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get the root argument list and extract arguments from it.
|
||||
completion_list_t result;
|
||||
const ast::freestanding_argument_list_t &list = ast->top()->as_freestanding_argument_list();
|
||||
for (size_t i = 0; i < list.arguments().count(); i++) {
|
||||
const ast::argument_t &arg = *list.arguments().at(i);
|
||||
wcstring arg_src = *arg.source(arg_list_src);
|
||||
if (expand_string(arg_src, &result, eflags, ctx) == expand_result_t::error) {
|
||||
break; // failed to expand a string
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void parser_t::set_cwd_fd(int fd) {
|
||||
assert(fd >= 0 && "Invalid fd");
|
||||
this->libdata().cwd_fd = std::make_shared<autoclose_fd_t>(fd);
|
||||
}
|
||||
|
||||
std::shared_ptr<parser_t> parser_t::shared() { return shared_from_this(); }
|
||||
|
||||
cancel_checker_t parser_t::cancel_checker() const {
|
||||
return [] { return signal_check_cancel() != 0; };
|
||||
}
|
||||
|
||||
operation_context_t parser_t::context() {
|
||||
return operation_context_t{this->shared(), this->vars(), this->cancel_checker()};
|
||||
}
|
||||
|
||||
/// Append stack trace info for the block \p b to \p trace.
|
||||
static void append_block_description_to_stack_trace(const parser_t &parser, const block_t &b,
|
||||
wcstring &trace) {
|
||||
bool print_call_site = false;
|
||||
switch (b.type()) {
|
||||
case block_type_t::function_call:
|
||||
case block_type_t::function_call_no_shadow: {
|
||||
append_format(trace, _(L"in function '%ls'"), b.function_name.c_str());
|
||||
// Print arguments on the same line.
|
||||
wcstring args_str;
|
||||
for (const wcstring &arg : b.function_args) {
|
||||
if (!args_str.empty()) args_str.push_back(L' ');
|
||||
// We can't quote the arguments because we print this in quotes.
|
||||
// As a special-case, add the empty argument as "".
|
||||
if (!arg.empty()) {
|
||||
args_str.append(escape_string(arg, ESCAPE_NO_QUOTED));
|
||||
} else {
|
||||
args_str.append(L"\"\"");
|
||||
}
|
||||
}
|
||||
if (!args_str.empty()) {
|
||||
// TODO: Escape these.
|
||||
append_format(trace, _(L" with arguments '%ls'"), args_str.c_str());
|
||||
}
|
||||
trace.push_back('\n');
|
||||
print_call_site = true;
|
||||
break;
|
||||
}
|
||||
case block_type_t::subst: {
|
||||
append_format(trace, _(L"in command substitution\n"));
|
||||
print_call_site = true;
|
||||
break;
|
||||
}
|
||||
case block_type_t::source: {
|
||||
const filename_ref_t &source_dest = b.sourced_file;
|
||||
append_format(trace, _(L"from sourcing file %ls\n"),
|
||||
user_presentable_path(*source_dest, parser.vars()).c_str());
|
||||
print_call_site = true;
|
||||
break;
|
||||
}
|
||||
case block_type_t::event: {
|
||||
assert(b.event && "Should have an event");
|
||||
wcstring description = *event_get_desc(parser, **b.event);
|
||||
append_format(trace, _(L"in event handler: %ls\n"), description.c_str());
|
||||
print_call_site = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case block_type_t::top:
|
||||
case block_type_t::begin:
|
||||
case block_type_t::switch_block:
|
||||
case block_type_t::while_block:
|
||||
case block_type_t::for_block:
|
||||
case block_type_t::if_block:
|
||||
case block_type_t::breakpoint:
|
||||
case block_type_t::variable_assignment:
|
||||
break;
|
||||
}
|
||||
|
||||
if (print_call_site) {
|
||||
// Print where the function is called.
|
||||
const auto &file = b.src_filename;
|
||||
if (file) {
|
||||
append_format(trace, _(L"\tcalled on line %d of file %ls\n"), b.src_lineno,
|
||||
user_presentable_path(*file, parser.vars()).c_str());
|
||||
} else if (parser.libdata().within_fish_init) {
|
||||
append_format(trace, _(L"\tcalled during startup\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wcstring parser_t::stack_trace() const {
|
||||
wcstring trace;
|
||||
for (const auto &b : blocks()) {
|
||||
append_block_description_to_stack_trace(*this, b, trace);
|
||||
|
||||
// Stop at event handler. No reason to believe that any other code is relevant.
|
||||
//
|
||||
// It might make sense in the future to continue printing the stack trace of the code
|
||||
// that invoked the event, if this is a programmatic event, but we can't currently
|
||||
// detect that.
|
||||
if (b.type() == block_type_t::event) break;
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
bool parser_t::is_function() const {
|
||||
for (const auto &b : block_list) {
|
||||
if (b.is_function_call()) {
|
||||
return true;
|
||||
} else if (b.type() == block_type_t::source) {
|
||||
// If a function sources a file, don't descend further.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parser_t::is_block() const {
|
||||
// Note historically this has descended into 'source', unlike 'is_function'.
|
||||
for (const auto &b : block_list) {
|
||||
if (b.type() != block_type_t::top && b.type() != block_type_t::subst) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parser_t::is_breakpoint() const {
|
||||
for (const auto &b : block_list) {
|
||||
if (b.type() == block_type_t::breakpoint) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parser_t::is_command_substitution() const {
|
||||
for (const auto &b : block_list) {
|
||||
if (b.type() == block_type_t::subst) {
|
||||
return true;
|
||||
} else if (b.type() == block_type_t::source) {
|
||||
// If a function sources a file, don't descend further.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
wcstring parser_t::get_function_name_ffi(int level) {
|
||||
auto name = get_function_name(level);
|
||||
if (name.has_value()) {
|
||||
return name.acquire();
|
||||
} else {
|
||||
return wcstring();
|
||||
}
|
||||
}
|
||||
|
||||
maybe_t<wcstring> parser_t::get_function_name(int level) {
|
||||
if (level == 0) {
|
||||
// Return the function name for the level preceding the most recent breakpoint. If there
|
||||
// isn't one return the function name for the current level.
|
||||
// Walk until we find a breakpoint, then take the next function.
|
||||
bool found_breakpoint = false;
|
||||
for (const auto &b : block_list) {
|
||||
if (b.type() == block_type_t::breakpoint) {
|
||||
found_breakpoint = true;
|
||||
} else if (found_breakpoint && b.is_function_call()) {
|
||||
return b.function_name;
|
||||
}
|
||||
}
|
||||
return none(); // couldn't find a breakpoint frame
|
||||
}
|
||||
|
||||
// Level 1 is the topmost function call. Level 2 is its caller. Etc.
|
||||
int funcs_seen = 0;
|
||||
for (const auto &b : block_list) {
|
||||
if (b.is_function_call()) {
|
||||
funcs_seen++;
|
||||
if (funcs_seen == level) {
|
||||
return b.function_name;
|
||||
}
|
||||
} else if (b.type() == block_type_t::source && level == 1) {
|
||||
// Historical: If we want the topmost function, but we are really in a file sourced by a
|
||||
// function, don't consider ourselves to be in a function.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
int parser_t::get_lineno() const {
|
||||
int lineno = -1;
|
||||
if (execution_context) {
|
||||
lineno = execution_context->get_current_line_number();
|
||||
}
|
||||
return lineno;
|
||||
}
|
||||
|
||||
filename_ref_t parser_t::current_filename() const {
|
||||
for (const auto &b : block_list) {
|
||||
if (b.is_function_call()) {
|
||||
auto props = function_get_props(b.function_name);
|
||||
return props ? (*props)->definition_file() : nullptr;
|
||||
} else if (b.type() == block_type_t::source) {
|
||||
return b.sourced_file;
|
||||
}
|
||||
}
|
||||
// Fall back to the file being sourced.
|
||||
return libdata().current_filename;
|
||||
}
|
||||
|
||||
void parser_t::set_filename_ffi(wcstring filename) {
|
||||
libdata().current_filename = std::make_shared<wcstring>(filename);
|
||||
}
|
||||
|
||||
// FFI glue
|
||||
wcstring parser_t::current_filename_ffi() const {
|
||||
auto filename = current_filename();
|
||||
if (filename) {
|
||||
return wcstring(*filename);
|
||||
} else {
|
||||
return wcstring();
|
||||
}
|
||||
}
|
||||
|
||||
bool parser_t::function_stack_is_overflowing() const {
|
||||
// We are interested in whether the count of functions on the stack exceeds
|
||||
// FISH_MAX_STACK_DEPTH. We don't separately track the number of functions, but we can have a
|
||||
// fast path through the eval_level. If the eval_level is in bounds, so must be the stack depth.
|
||||
if (eval_level <= FISH_MAX_STACK_DEPTH) {
|
||||
return false;
|
||||
}
|
||||
// Count the functions.
|
||||
int depth = 0;
|
||||
for (const auto &b : block_list) {
|
||||
depth += b.is_function_call();
|
||||
}
|
||||
return depth > FISH_MAX_STACK_DEPTH;
|
||||
}
|
||||
|
||||
wcstring parser_t::current_line() {
|
||||
if (!execution_context) {
|
||||
return wcstring();
|
||||
}
|
||||
int source_offset = execution_context->get_current_source_offset();
|
||||
if (source_offset < 0) {
|
||||
return wcstring();
|
||||
}
|
||||
|
||||
const int lineno = this->get_lineno();
|
||||
filename_ref_t file = this->current_filename();
|
||||
|
||||
wcstring prefix;
|
||||
|
||||
// If we are not going to print a stack trace, at least print the line number and filename.
|
||||
if (!is_interactive() || is_function()) {
|
||||
if (file) {
|
||||
append_format(prefix, _(L"%ls (line %d): "),
|
||||
user_presentable_path(*file, vars()).c_str(), lineno);
|
||||
} else if (libdata().within_fish_init) {
|
||||
append_format(prefix, L"%ls (line %d): ", _(L"Startup"), lineno);
|
||||
} else {
|
||||
append_format(prefix, L"%ls (line %d): ", _(L"Standard input"), lineno);
|
||||
}
|
||||
}
|
||||
|
||||
bool skip_caret = is_interactive() && !is_function();
|
||||
|
||||
// Use an error with empty text.
|
||||
assert(source_offset >= 0);
|
||||
parse_error_t empty_error = {};
|
||||
empty_error.text = std::make_unique<wcstring>();
|
||||
empty_error.source_start = source_offset;
|
||||
|
||||
wcstring line_info = *empty_error.describe_with_prefix(execution_context->get_source(), prefix,
|
||||
is_interactive(), skip_caret);
|
||||
if (!line_info.empty()) {
|
||||
line_info.push_back(L'\n');
|
||||
}
|
||||
|
||||
line_info.append(this->stack_trace());
|
||||
return line_info;
|
||||
}
|
||||
|
||||
void parser_t::job_add(shared_ptr<job_t> job) {
|
||||
assert(job != nullptr);
|
||||
assert(!job->processes.empty());
|
||||
job_list.insert(job_list.begin(), std::move(job));
|
||||
}
|
||||
|
||||
void parser_t::job_promote(job_list_t::iterator job_it) {
|
||||
// Move the job to the beginning.
|
||||
std::rotate(job_list.begin(), job_it, std::next(job_it));
|
||||
}
|
||||
|
||||
void parser_t::job_promote(const job_t *job) {
|
||||
job_list_t::iterator loc;
|
||||
for (loc = job_list.begin(); loc != job_list.end(); ++loc) {
|
||||
if (loc->get() == job) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(loc != job_list.end());
|
||||
job_promote(loc);
|
||||
}
|
||||
|
||||
void parser_t::job_promote_at(size_t job_pos) {
|
||||
assert(job_pos < job_list.size());
|
||||
job_promote(job_list.begin() + job_pos);
|
||||
}
|
||||
|
||||
const job_t *parser_t::job_with_id(job_id_t id) const {
|
||||
for (const auto &job : job_list) {
|
||||
if (id <= 0 || job->job_id() == id) return job.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
job_t *parser_t::job_get_from_pid(pid_t pid) const {
|
||||
size_t job_pos{};
|
||||
return job_get_from_pid(pid, job_pos);
|
||||
}
|
||||
|
||||
job_t *parser_t::job_get_from_pid(int pid, size_t &job_pos) const {
|
||||
for (auto it = job_list.begin(); it != job_list.end(); ++it) {
|
||||
for (const process_ptr_t &p : (*it)->processes) {
|
||||
if (p->pid == pid) {
|
||||
job_pos = it - job_list.begin();
|
||||
return (*it).get();
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const wcstring *library_data_t::get_current_filename() const {
|
||||
if (current_filename) {
|
||||
return &*current_filename;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
library_data_pod_t *parser_t::ffi_libdata_pod() { return &library_data; }
|
||||
|
||||
job_t *parser_t::ffi_job_get_from_pid(int pid) const { return job_get_from_pid(pid); }
|
||||
const library_data_pod_t &parser_t::ffi_libdata_pod_const() const { return library_data; }
|
||||
|
||||
profile_item_t *parser_t::create_profile_item() {
|
||||
if (g_profiling_active) {
|
||||
profile_items.emplace_back();
|
||||
return &profile_items.back();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
eval_res_t parser_t::eval(const wcstring &cmd, const io_chain_t &io) {
|
||||
return eval_with(cmd, io, {}, block_type_t::top);
|
||||
}
|
||||
|
||||
eval_res_t parser_t::eval_with(const wcstring &cmd, const io_chain_t &io,
|
||||
const job_group_ref_t &job_group, enum block_type_t block_type) {
|
||||
// Parse the source into a tree, if we can.
|
||||
auto error_list = new_parse_error_list();
|
||||
auto ps = parse_source(wcstring{cmd}, parse_flag_none, &*error_list);
|
||||
if (ps->has_value()) {
|
||||
return this->eval_parsed_source(*ps, io, job_group, block_type);
|
||||
} else {
|
||||
// Get a backtrace. This includes the message.
|
||||
wcstring backtrace_and_desc;
|
||||
this->get_backtrace(cmd, *error_list, backtrace_and_desc);
|
||||
|
||||
// Print it.
|
||||
std::fwprintf(stderr, L"%ls\n", backtrace_and_desc.c_str());
|
||||
|
||||
// Set a valid status.
|
||||
this->set_last_statuses(statuses_t::just(STATUS_ILLEGAL_CMD));
|
||||
bool break_expand = true;
|
||||
return eval_res_t{proc_status_t::from_exit_code(STATUS_ILLEGAL_CMD), break_expand};
|
||||
}
|
||||
}
|
||||
|
||||
eval_res_t parser_t::eval_string_ffi1(const wcstring &cmd) { return eval(cmd, io_chain_t()); }
|
||||
|
||||
eval_res_t parser_t::eval_parsed_source_ffi1(const parsed_source_ref_t *ps,
|
||||
enum block_type_t block_type) {
|
||||
return eval_parsed_source(*ps, io_chain_t(), {}, block_type);
|
||||
}
|
||||
|
||||
eval_res_t parser_t::eval_parsed_source(const parsed_source_ref_t &ps, const io_chain_t &io,
|
||||
const job_group_ref_t &job_group,
|
||||
enum block_type_t block_type) {
|
||||
assert(block_type == block_type_t::top || block_type == block_type_t::subst);
|
||||
const auto &job_list = ps.ast().top()->as_job_list();
|
||||
if (!job_list.empty()) {
|
||||
// Execute the top job list.
|
||||
return this->eval_node(ps, job_list, io, job_group, block_type);
|
||||
} else {
|
||||
auto status = proc_status_t::from_exit_code(get_last_status());
|
||||
bool break_expand = false;
|
||||
bool was_empty = true;
|
||||
bool no_status = true;
|
||||
return eval_res_t{status, break_expand, was_empty, no_status};
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
|
||||
const io_chain_t &block_io, const job_group_ref_t &job_group,
|
||||
block_type_t block_type) {
|
||||
static_assert(
|
||||
std::is_same<T, ast::statement_t>::value || std::is_same<T, ast::job_list_t>::value,
|
||||
"Unexpected node type");
|
||||
|
||||
// Only certain blocks are allowed.
|
||||
assert((block_type == block_type_t::top || block_type == block_type_t::subst) &&
|
||||
"Invalid block type");
|
||||
|
||||
// If fish itself got a cancel signal, then we want to unwind back to the principal parser.
|
||||
// If we are the principal parser and our block stack is empty, then we want to clear the
|
||||
// signal.
|
||||
// Note this only happens in interactive sessions. In non-interactive sessions, SIGINT will
|
||||
// cause fish to exit.
|
||||
if (int sig = signal_check_cancel()) {
|
||||
if (is_principal_ && block_list.empty()) {
|
||||
signal_clear_cancel();
|
||||
} else {
|
||||
return proc_status_t::from_signal(sig);
|
||||
}
|
||||
}
|
||||
|
||||
// A helper to detect if we got a signal.
|
||||
// This includes both signals sent to fish (user hit control-C while fish is foreground) and
|
||||
// signals from the job group (e.g. some external job terminated with SIGQUIT).
|
||||
auto check_cancel_signal = [=] {
|
||||
// Did fish itself get a signal?
|
||||
int sig = signal_check_cancel();
|
||||
// Has this job group been cancelled?
|
||||
if (!sig && job_group) sig = job_group->get_cancel_signal();
|
||||
return sig;
|
||||
};
|
||||
|
||||
// If we have a job group which is cancelled, then do nothing.
|
||||
if (int sig = check_cancel_signal()) {
|
||||
return proc_status_t::from_signal(sig);
|
||||
}
|
||||
|
||||
job_reap(*this, false); // not sure why we reap jobs here
|
||||
|
||||
// Start it up
|
||||
operation_context_t op_ctx = this->context();
|
||||
block_t *scope_block = this->push_block(block_t::scope_block(block_type));
|
||||
|
||||
// Propagate our job group.
|
||||
op_ctx.job_group = job_group;
|
||||
|
||||
// Replace the context's cancel checker with one that checks the job group's signal.
|
||||
op_ctx.cancel_checker = [=] { return check_cancel_signal() != 0; };
|
||||
|
||||
// Create and set a new execution context.
|
||||
using exc_ctx_ref_t = std::unique_ptr<parse_execution_context_t>;
|
||||
scoped_push<exc_ctx_ref_t> exc(
|
||||
&execution_context, make_unique<parse_execution_context_t>(ps.clone(), op_ctx, block_io));
|
||||
|
||||
// Check the exec count so we know if anything got executed.
|
||||
const size_t prev_exec_count = libdata().exec_count;
|
||||
const size_t prev_status_count = libdata().status_count;
|
||||
end_execution_reason_t reason = execution_context->eval_node(node, scope_block);
|
||||
const size_t new_exec_count = libdata().exec_count;
|
||||
const size_t new_status_count = libdata().status_count;
|
||||
|
||||
exc.restore();
|
||||
this->pop_block(scope_block);
|
||||
|
||||
job_reap(*this, false); // reap again
|
||||
|
||||
if (int sig = check_cancel_signal()) {
|
||||
return proc_status_t::from_signal(sig);
|
||||
} else {
|
||||
auto status = proc_status_t::from_exit_code(this->get_last_status());
|
||||
bool break_expand = (reason == end_execution_reason_t::error);
|
||||
bool was_empty = !break_expand && prev_exec_count == new_exec_count;
|
||||
bool no_status = prev_status_count == new_status_count;
|
||||
return eval_res_t{status, break_expand, was_empty, no_status};
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit instantiations. TODO: use overloads instead?
|
||||
template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, const ast::statement_t &,
|
||||
const io_chain_t &, const job_group_ref_t &, block_type_t);
|
||||
template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, const ast::job_list_t &,
|
||||
const io_chain_t &, const job_group_ref_t &, block_type_t);
|
||||
|
||||
void parser_t::get_backtrace_ffi(const wcstring &src, const parse_error_list_t *errors,
|
||||
wcstring &output) const {
|
||||
return get_backtrace(src, *errors, output);
|
||||
};
|
||||
void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &errors,
|
||||
wcstring &output) const {
|
||||
if (!errors.empty()) {
|
||||
const auto *err = errors.at(0);
|
||||
|
||||
// Determine if we want to try to print a caret to point at the source error. The
|
||||
// err.source_start() <= src.size() check is due to the nasty way that slices work, which is
|
||||
// by rewriting the source.
|
||||
size_t which_line = 0;
|
||||
bool skip_caret = true;
|
||||
if (err->source_start() != SOURCE_LOCATION_UNKNOWN && err->source_start() <= src.size()) {
|
||||
// Determine which line we're on.
|
||||
which_line = 1 + std::count(src.begin(), src.begin() + err->source_start(), L'\n');
|
||||
|
||||
// Don't include the caret if we're interactive, this is the first line of text, and our
|
||||
// source is at its beginning, because then it's obvious.
|
||||
skip_caret = (is_interactive() && which_line == 1 && err->source_start() == 0);
|
||||
}
|
||||
|
||||
wcstring prefix;
|
||||
filename_ref_t filename = this->current_filename();
|
||||
if (filename) {
|
||||
if (which_line > 0) {
|
||||
prefix =
|
||||
format_string(_(L"%ls (line %lu): "),
|
||||
user_presentable_path(*filename, vars()).c_str(), which_line);
|
||||
} else {
|
||||
prefix =
|
||||
format_string(_(L"%ls: "), user_presentable_path(*filename, vars()).c_str());
|
||||
}
|
||||
} else {
|
||||
prefix = L"fish: ";
|
||||
}
|
||||
|
||||
const wcstring description =
|
||||
*err->describe_with_prefix(src, prefix, is_interactive(), skip_caret);
|
||||
if (!description.empty()) {
|
||||
output.append(description);
|
||||
output.push_back(L'\n');
|
||||
}
|
||||
output.append(this->stack_trace());
|
||||
}
|
||||
}
|
||||
|
||||
RustFFIJobList parser_t::ffi_jobs() const {
|
||||
return RustFFIJobList{const_cast<job_ref_t *>(job_list.data()), job_list.size()};
|
||||
}
|
||||
|
||||
bool parser_t::ffi_has_funtion_block() const {
|
||||
for (const auto &b : blocks()) {
|
||||
if (b.is_function_call()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t parser_t::ffi_global_event_blocks() const { return global_event_blocks; }
|
||||
void parser_t::ffi_incr_global_event_blocks() { ++global_event_blocks; }
|
||||
void parser_t::ffi_decr_global_event_blocks() { --global_event_blocks; }
|
||||
|
||||
size_t parser_t::ffi_blocks_size() const { return block_list.size(); }
|
||||
|
||||
block_t::block_t(block_type_t t) : block_type(t) {}
|
||||
|
||||
wcstring block_t::description() const {
|
||||
wcstring result;
|
||||
switch (this->type()) {
|
||||
case block_type_t::while_block: {
|
||||
result.append(L"while");
|
||||
break;
|
||||
}
|
||||
case block_type_t::for_block: {
|
||||
result.append(L"for");
|
||||
break;
|
||||
}
|
||||
case block_type_t::if_block: {
|
||||
result.append(L"if");
|
||||
break;
|
||||
}
|
||||
case block_type_t::function_call: {
|
||||
result.append(L"function_call");
|
||||
break;
|
||||
}
|
||||
case block_type_t::function_call_no_shadow: {
|
||||
result.append(L"function_call_no_shadow");
|
||||
break;
|
||||
}
|
||||
case block_type_t::switch_block: {
|
||||
result.append(L"switch");
|
||||
break;
|
||||
}
|
||||
case block_type_t::subst: {
|
||||
result.append(L"substitution");
|
||||
break;
|
||||
}
|
||||
case block_type_t::top: {
|
||||
result.append(L"top");
|
||||
break;
|
||||
}
|
||||
case block_type_t::begin: {
|
||||
result.append(L"begin");
|
||||
break;
|
||||
}
|
||||
case block_type_t::source: {
|
||||
result.append(L"source");
|
||||
break;
|
||||
}
|
||||
case block_type_t::event: {
|
||||
result.append(L"event");
|
||||
break;
|
||||
}
|
||||
case block_type_t::breakpoint: {
|
||||
result.append(L"breakpoint");
|
||||
break;
|
||||
}
|
||||
case block_type_t::variable_assignment: {
|
||||
result.append(L"variable_assignment");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->src_lineno >= 0) {
|
||||
append_format(result, L" (line %d)", this->src_lineno);
|
||||
}
|
||||
if (this->src_filename) {
|
||||
append_format(result, L" (file %ls)", this->src_filename->c_str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool block_t::is_function_call() const {
|
||||
return type() == block_type_t::function_call || type() == block_type_t::function_call_no_shadow;
|
||||
}
|
||||
|
||||
// Various block constructors.
|
||||
|
||||
block_t block_t::if_block() { return block_t(block_type_t::if_block); }
|
||||
|
||||
block_t block_t::event_block(const void *evt_) {
|
||||
const auto &evt = *static_cast<const Event *>(evt_);
|
||||
block_t b{block_type_t::event};
|
||||
b.event =
|
||||
std::make_shared<rust::Box<Event>>(evt.clone()); // TODO Post-FFI: move instead of clone.
|
||||
return b;
|
||||
}
|
||||
|
||||
block_t block_t::function_block(wcstring name, std::vector<wcstring> args, bool shadows) {
|
||||
block_t b{shadows ? block_type_t::function_call : block_type_t::function_call_no_shadow};
|
||||
b.function_name = std::move(name);
|
||||
b.function_args = std::move(args);
|
||||
return b;
|
||||
}
|
||||
|
||||
block_t block_t::source_block(filename_ref_t src) {
|
||||
block_t b{block_type_t::source};
|
||||
b.sourced_file = std::move(src);
|
||||
return b;
|
||||
}
|
||||
|
||||
block_t block_t::for_block() { return block_t{block_type_t::for_block}; }
|
||||
block_t block_t::while_block() { return block_t{block_type_t::while_block}; }
|
||||
block_t block_t::switch_block() { return block_t{block_type_t::switch_block}; }
|
||||
block_t block_t::scope_block(block_type_t type) {
|
||||
assert(
|
||||
(type == block_type_t::begin || type == block_type_t::top || type == block_type_t::subst) &&
|
||||
"Invalid scope type");
|
||||
return block_t(type);
|
||||
}
|
||||
block_t block_t::breakpoint_block() { return block_t(block_type_t::breakpoint); }
|
||||
block_t block_t::variable_assignment_block() { return block_t(block_type_t::variable_assignment); }
|
||||
|
||||
void block_t::ffi_incr_event_blocks() { ++event_blocks; }
|
||||
517
src/parser.h
517
src/parser.h
@@ -12,524 +12,17 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "cxx.h"
|
||||
#include "env.h"
|
||||
#include "event.h"
|
||||
#include "expand.h"
|
||||
#include "maybe.h"
|
||||
#include "operation_context.h"
|
||||
#include "parse_constants.h"
|
||||
#include "parse_tree.h"
|
||||
#include "proc.h"
|
||||
#include "util.h"
|
||||
#include "wait_handle.h"
|
||||
|
||||
class autoclose_fd_t;
|
||||
class io_chain_t;
|
||||
struct Event;
|
||||
struct job_group_t;
|
||||
|
||||
/// Types of blocks.
|
||||
enum class block_type_t : uint8_t {
|
||||
while_block, /// While loop block
|
||||
for_block, /// For loop block
|
||||
if_block, /// If block
|
||||
function_call, /// Function invocation block
|
||||
function_call_no_shadow, /// Function invocation block with no variable shadowing
|
||||
switch_block, /// Switch block
|
||||
subst, /// Command substitution scope
|
||||
top, /// Outermost block
|
||||
begin, /// Unconditional block
|
||||
source, /// Block created by the . (source) builtin
|
||||
event, /// Block created on event notifier invocation
|
||||
breakpoint, /// Breakpoint block
|
||||
variable_assignment, /// Variable assignment before a command
|
||||
};
|
||||
|
||||
/// Possible states for a loop.
|
||||
enum class loop_status_t {
|
||||
normals, /// current loop block executed as normal
|
||||
breaks, /// current loop block should be removed
|
||||
continues, /// current loop block should be skipped
|
||||
};
|
||||
|
||||
/// block_t represents a block of commands.
|
||||
class block_t {
|
||||
public:
|
||||
/// Construct from a block type.
|
||||
explicit block_t(block_type_t t);
|
||||
|
||||
// If this is a function block, the function name. Otherwise empty.
|
||||
wcstring function_name{};
|
||||
|
||||
/// List of event blocks.
|
||||
uint64_t event_blocks{};
|
||||
|
||||
// If this is a function block, the function args. Otherwise empty.
|
||||
std::vector<wcstring> function_args{};
|
||||
|
||||
/// Name of file that created this block.
|
||||
filename_ref_t src_filename{};
|
||||
|
||||
// If this is an event block, the event. Otherwise ignored.
|
||||
std::shared_ptr<rust::Box<Event>> event;
|
||||
|
||||
// If this is a source block, the source'd file, interned.
|
||||
// Otherwise nothing.
|
||||
filename_ref_t sourced_file{};
|
||||
|
||||
/// Line number where this block was created.
|
||||
int src_lineno{0};
|
||||
|
||||
private:
|
||||
/// Type of block.
|
||||
const block_type_t block_type;
|
||||
|
||||
public:
|
||||
/// Whether we should pop the environment variable stack when we're popped off of the block
|
||||
/// stack.
|
||||
bool wants_pop_env{false};
|
||||
|
||||
/// Description of the block, for debugging.
|
||||
wcstring description() const;
|
||||
|
||||
block_type_t type() const { return this->block_type; }
|
||||
|
||||
/// \return if we are a function call (with or without shadowing).
|
||||
bool is_function_call() const;
|
||||
|
||||
/// Entry points for creating blocks.
|
||||
static block_t if_block();
|
||||
static block_t event_block(const void *evt_);
|
||||
static block_t function_block(wcstring name, std::vector<wcstring> args, bool shadows);
|
||||
static block_t source_block(filename_ref_t src);
|
||||
static block_t for_block();
|
||||
static block_t while_block();
|
||||
static block_t switch_block();
|
||||
static block_t scope_block(block_type_t type);
|
||||
static block_t breakpoint_block();
|
||||
static block_t variable_assignment_block();
|
||||
|
||||
/// autocxx junk.
|
||||
void ffi_incr_event_blocks();
|
||||
uint64_t ffi_event_blocks() const { return event_blocks; }
|
||||
};
|
||||
|
||||
struct profile_item_t {
|
||||
using microseconds_t = long long;
|
||||
|
||||
/// Time spent executing the command, including nested blocks.
|
||||
microseconds_t duration{};
|
||||
|
||||
/// The block level of the specified command. Nested blocks and command substitutions both
|
||||
/// increase the block level.
|
||||
size_t level{};
|
||||
|
||||
/// If the execution of this command was skipped.
|
||||
bool skipped{};
|
||||
|
||||
/// The command string.
|
||||
wcstring cmd{};
|
||||
|
||||
/// \return the current time as a microsecond timestamp since the epoch.
|
||||
static microseconds_t now() { return get_time(); }
|
||||
};
|
||||
|
||||
class parse_execution_context_t;
|
||||
|
||||
/// Plain-Old-Data components of `struct library_data_t` that can be shared over FFI
|
||||
struct library_data_pod_t {
|
||||
/// A counter incremented every time a command executes.
|
||||
uint64_t exec_count{0};
|
||||
|
||||
/// A counter incremented every time a command produces a $status.
|
||||
uint64_t status_count{0};
|
||||
|
||||
/// Last reader run count.
|
||||
uint64_t last_exec_run_counter{UINT64_MAX};
|
||||
|
||||
/// Number of recursive calls to the internal completion function.
|
||||
uint32_t complete_recursion_level{0};
|
||||
|
||||
/// If set, we are currently within fish's initialization routines.
|
||||
bool within_fish_init{false};
|
||||
|
||||
/// If we're currently repainting the commandline.
|
||||
/// Useful to stop infinite loops.
|
||||
bool is_repaint{false};
|
||||
|
||||
/// Whether we called builtin_complete -C without parameter.
|
||||
bool builtin_complete_current_commandline{false};
|
||||
|
||||
/// Whether we are currently cleaning processes.
|
||||
bool is_cleaning_procs{false};
|
||||
|
||||
/// The internal job id of the job being populated, or 0 if none.
|
||||
/// This supports the '--on-job-exit caller' feature.
|
||||
internal_job_id_t caller_id{0};
|
||||
|
||||
/// Whether we are running a subshell command.
|
||||
bool is_subshell{false};
|
||||
|
||||
/// Whether we are running an event handler. This is not a bool because we keep count of the
|
||||
/// event nesting level.
|
||||
int is_event{0};
|
||||
|
||||
/// Whether we are currently interactive.
|
||||
bool is_interactive{false};
|
||||
|
||||
/// Whether to suppress fish_trace output. This occurs in the prompt, event handlers, and key
|
||||
/// bindings.
|
||||
bool suppress_fish_trace{false};
|
||||
|
||||
/// Whether we should break or continue the current loop.
|
||||
/// This is set by the 'break' and 'continue' commands.
|
||||
enum loop_status_t loop_status { loop_status_t::normals };
|
||||
|
||||
/// Whether we should return from the current function.
|
||||
/// This is set by the 'return' command.
|
||||
bool returning{false};
|
||||
|
||||
/// Whether we should stop executing.
|
||||
/// This is set by the 'exit' command, and unset after 'reader_read'.
|
||||
/// Note this only exits up to the "current script boundary." That is, a call to exit within a
|
||||
/// 'source' or 'read' command will only exit up to that command.
|
||||
bool exit_current_script{false};
|
||||
|
||||
/// The read limit to apply to captured subshell output, or 0 for none.
|
||||
size_t read_limit{0};
|
||||
};
|
||||
|
||||
/// Miscellaneous data used to avoid recursion and others.
|
||||
struct library_data_t : public library_data_pod_t {
|
||||
/// The current filename we are evaluating, either from builtin source or on the command line.
|
||||
filename_ref_t current_filename{};
|
||||
|
||||
/// A stack of fake values to be returned by builtin_commandline. This is used by the completion
|
||||
/// machinery when wrapping: e.g. if `tig` wraps `git` then git completions need to see git on
|
||||
/// the command line.
|
||||
std::vector<wcstring> transient_commandlines{};
|
||||
|
||||
/// A file descriptor holding the current working directory, for use in openat().
|
||||
/// This is never null and never invalid.
|
||||
std::shared_ptr<const autoclose_fd_t> cwd_fd{};
|
||||
|
||||
/// Status variables set by the main thread as jobs are parsed and read by various consumers.
|
||||
struct {
|
||||
/// Used to get the head of the current job (not the current command, at least for now)
|
||||
/// for `status current-command`.
|
||||
wcstring command;
|
||||
/// Used to get the full text of the current job for `status current-commandline`.
|
||||
wcstring commandline;
|
||||
} status_vars;
|
||||
|
||||
public:
|
||||
wcstring get_status_vars_command() const { return status_vars.command; }
|
||||
wcstring get_status_vars_commandline() const { return status_vars.commandline; }
|
||||
const wcstring *get_current_filename() const; // may return nullptr if None
|
||||
};
|
||||
|
||||
/// The result of parser_t::eval family.
|
||||
struct eval_res_t {
|
||||
/// The value for $status.
|
||||
proc_status_t status;
|
||||
|
||||
/// If set, there was an error that should be considered a failed expansion, such as
|
||||
/// command-not-found. For example, `touch (not-a-command)` will not invoke 'touch' because
|
||||
/// command-not-found will mark break_expand.
|
||||
bool break_expand{false};
|
||||
|
||||
/// If set, no commands were executed and there we no errors.
|
||||
bool was_empty{false};
|
||||
|
||||
/// If set, no commands produced a $status value.
|
||||
bool no_status{false};
|
||||
|
||||
/* implicit */ eval_res_t(proc_status_t status, bool break_expand = false,
|
||||
bool was_empty = false, bool no_status = false)
|
||||
: status(status), break_expand(break_expand), was_empty(was_empty), no_status(no_status) {}
|
||||
};
|
||||
|
||||
enum class parser_status_var_t : uint8_t {
|
||||
current_command,
|
||||
current_commandline,
|
||||
count_,
|
||||
};
|
||||
|
||||
class Parser;
|
||||
struct Parser;
|
||||
using parser_t = Parser;
|
||||
|
||||
class Parser : public std::enable_shared_from_this<parser_t> {
|
||||
friend class parse_execution_context_t;
|
||||
|
||||
private:
|
||||
/// The current execution context.
|
||||
std::unique_ptr<parse_execution_context_t> execution_context;
|
||||
|
||||
/// The jobs associated with this parser.
|
||||
job_list_t job_list;
|
||||
|
||||
/// Our store of recorded wait-handles. These are jobs that finished in the background, and have
|
||||
/// been reaped, but may still be wait'ed on.
|
||||
rust::Box<WaitHandleStoreFFI> wait_handles;
|
||||
|
||||
/// The list of blocks. This is a deque because we give out raw pointers to callers, who hold
|
||||
/// them across manipulating this stack.
|
||||
/// This is in "reverse" order: the topmost block is at the front. This enables iteration from
|
||||
/// top down using range-based for loops.
|
||||
std::deque<block_t> block_list;
|
||||
|
||||
/// The 'depth' of the fish call stack.
|
||||
int eval_level = -1;
|
||||
|
||||
/// Set of variables for the parser.
|
||||
const std::shared_ptr<env_stack_t> variables;
|
||||
|
||||
/// Miscellaneous library data.
|
||||
library_data_t library_data{};
|
||||
|
||||
/// If set, we synchronize universal variables after external commands,
|
||||
/// including sending on-variable change events.
|
||||
bool syncs_uvars_{false};
|
||||
|
||||
/// If set, we are the principal parser.
|
||||
bool is_principal_{false};
|
||||
|
||||
/// List of profile items.
|
||||
/// This must be a deque because we return pointers to them to callers,
|
||||
/// who may hold them across blocks (which would cause reallocations internal
|
||||
/// to profile_items). deque does not move items on reallocation.
|
||||
std::deque<profile_item_t> profile_items;
|
||||
|
||||
/// Adds a job to the beginning of the job list.
|
||||
void job_add(std::shared_ptr<job_t> job);
|
||||
|
||||
/// \return whether we are currently evaluating a function.
|
||||
bool is_function() const;
|
||||
|
||||
/// \return whether we are currently evaluating a command substitution.
|
||||
bool is_command_substitution() const;
|
||||
|
||||
/// Create a parser
|
||||
Parser(std::shared_ptr<env_stack_t> vars, bool is_principal = false);
|
||||
|
||||
public:
|
||||
// No copying allowed.
|
||||
Parser(const parser_t &) = delete;
|
||||
Parser &operator=(const parser_t &) = delete;
|
||||
|
||||
/// Get the "principal" parser, whatever that is.
|
||||
static parser_t &principal_parser();
|
||||
|
||||
/// ffi helper. Obviously this is totally bogus.
|
||||
static parser_t *principal_parser_ffi();
|
||||
|
||||
/// Assert that this parser is allowed to execute on the current thread.
|
||||
void assert_can_execute() const;
|
||||
|
||||
/// Global event blocks.
|
||||
uint64_t global_event_blocks{};
|
||||
|
||||
eval_res_t eval(const wcstring &cmd, const io_chain_t &io);
|
||||
|
||||
/// Evaluate the expressions contained in cmd.
|
||||
///
|
||||
/// \param cmd the string to evaluate
|
||||
/// \param io io redirections to perform on all started jobs
|
||||
/// \param job_group if set, the job group to give to spawned jobs.
|
||||
/// \param block_type The type of block to push on the block stack, which must be either 'top'
|
||||
/// or 'subst'.
|
||||
/// \return the result of evaluation.
|
||||
eval_res_t eval_with(const wcstring &cmd, const io_chain_t &io,
|
||||
const job_group_ref_t &job_group, block_type_t block_type);
|
||||
|
||||
eval_res_t eval_string_ffi1(const wcstring &cmd);
|
||||
|
||||
/// Evaluate the parsed source ps.
|
||||
/// Because the source has been parsed, a syntax error is impossible.
|
||||
eval_res_t eval_parsed_source(const parsed_source_ref_t &ps, const io_chain_t &io,
|
||||
const job_group_ref_t &job_group = {},
|
||||
block_type_t block_type = block_type_t::top);
|
||||
eval_res_t eval_parsed_source_ffi1(const parsed_source_ref_t *ps, block_type_t block_type);
|
||||
/// Evaluates a node.
|
||||
/// The node type must be ast_t::statement_t or ast::job_list_t.
|
||||
template <typename T>
|
||||
eval_res_t eval_node(const parsed_source_ref_t &ps, const T &node, const io_chain_t &block_io,
|
||||
const job_group_ref_t &job_group,
|
||||
block_type_t block_type = block_type_t::top);
|
||||
|
||||
/// Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and
|
||||
/// cmdsubst execution on the tokens. Errors are ignored. If a parser is provided, it is used
|
||||
/// for command substitution expansion.
|
||||
static completion_list_t expand_argument_list(const wcstring &arg_list_src,
|
||||
expand_flags_t flags,
|
||||
const operation_context_t &ctx);
|
||||
|
||||
/// Returns a string describing the current parser position in the format 'FILENAME (line
|
||||
/// LINE_NUMBER): LINE'. Example:
|
||||
///
|
||||
/// init.fish (line 127): ls|grep pancake
|
||||
wcstring current_line();
|
||||
|
||||
/// Returns the current line number.
|
||||
int get_lineno() const;
|
||||
|
||||
/// \return whether we are currently evaluating a "block" such as an if statement.
|
||||
/// This supports 'status is-block'.
|
||||
bool is_block() const;
|
||||
|
||||
/// \return whether we have a breakpoint block.
|
||||
bool is_breakpoint() const;
|
||||
|
||||
/// Returns the block at the given index. 0 corresponds to the innermost block. Returns nullptr
|
||||
/// when idx is at or equal to the number of blocks.
|
||||
const block_t *block_at_index(size_t idx) const;
|
||||
block_t *block_at_index(size_t idx);
|
||||
|
||||
/// Return the list of blocks. The first block is at the top.
|
||||
const std::deque<block_t> &blocks() const { return block_list; }
|
||||
|
||||
size_t blocks_size() const { return block_list.size(); }
|
||||
|
||||
/// Get the list of jobs.
|
||||
job_list_t &jobs() { return job_list; }
|
||||
const job_list_t &jobs() const { return job_list; }
|
||||
|
||||
/// Get the variables.
|
||||
env_stack_t &vars() { return *variables; }
|
||||
const env_stack_t &vars() const { return *variables; }
|
||||
|
||||
int remove_var_ffi(const wcstring &key, int mode) { return vars().remove(key, mode); }
|
||||
|
||||
/// Get the library data.
|
||||
library_data_t &libdata() { return library_data; }
|
||||
const library_data_t &libdata() const { return library_data; }
|
||||
|
||||
/// Get our wait handle store.
|
||||
rust::Box<WaitHandleStoreFFI> &get_wait_handles_ffi();
|
||||
const rust::Box<WaitHandleStoreFFI> &get_wait_handles_ffi() const;
|
||||
|
||||
/// As get_wait_handles(), but void* pointer-to-Box to satisfy autocxx.
|
||||
void *get_wait_handles_void() const {
|
||||
const void *ptr = &get_wait_handles_ffi();
|
||||
return const_cast<void *>(ptr);
|
||||
}
|
||||
|
||||
/// Get and set the last proc statuses.
|
||||
int get_last_status() const { return vars().get_last_status(); }
|
||||
statuses_t get_last_statuses() const { return vars().get_last_statuses(); }
|
||||
void set_last_statuses(statuses_t s) { vars().set_last_statuses(std::move(s)); }
|
||||
|
||||
/// Cover of vars().set(), which also fires any returned event handlers.
|
||||
/// \return a value like ENV_OK.
|
||||
int set_var_and_fire(const wcstring &key, env_mode_flags_t mode, wcstring val);
|
||||
int set_var_and_fire(const wcstring &key, env_mode_flags_t mode, std::vector<wcstring> vals);
|
||||
|
||||
/// Update any universal variables and send event handlers.
|
||||
/// If \p always is set, then do it even if we have no pending changes (that is, look for
|
||||
/// changes from other fish instances); otherwise only sync if this instance has changed uvars.
|
||||
void sync_uvars_and_fire(bool always = false);
|
||||
|
||||
/// Pushes a new block. Returns a pointer to the block, stored in the parser. The pointer is
|
||||
/// valid until the call to pop_block().
|
||||
block_t *push_block(block_t &&b);
|
||||
|
||||
/// Remove the outermost block, asserting it's the given one.
|
||||
void pop_block(const block_t *expected);
|
||||
|
||||
/// Avoid maybe_t usage for ffi, sends a empty string in case of none.
|
||||
wcstring get_function_name_ffi(int level);
|
||||
/// Return the function name for the specified stack frame. Default is one (current frame).
|
||||
maybe_t<wcstring> get_function_name(int level = 1);
|
||||
|
||||
/// Promotes a job to the front of the list.
|
||||
void job_promote(job_list_t::iterator job_it);
|
||||
void job_promote(const job_t *job);
|
||||
void job_promote_at(size_t job_pos);
|
||||
|
||||
/// Return the job with the specified job id. If id is 0 or less, return the last job used.
|
||||
const job_t *job_with_id(job_id_t job_id) const;
|
||||
|
||||
/// Returns the job with the given pid.
|
||||
job_t *job_get_from_pid(pid_t pid) const;
|
||||
|
||||
/// Returns the job and position with the given pid.
|
||||
job_t *job_get_from_pid(int pid, size_t &job_pos) const;
|
||||
|
||||
/// Returns a new profile item if profiling is active. The caller should fill it in.
|
||||
/// The parser_t will deallocate it.
|
||||
/// If profiling is not active, this returns nullptr.
|
||||
profile_item_t *create_profile_item();
|
||||
|
||||
/// Remove the profiling items.
|
||||
void clear_profiling();
|
||||
|
||||
/// Output profiling data to the given filename.
|
||||
void emit_profiling(const char *path) const;
|
||||
|
||||
void get_backtrace_ffi(const wcstring &src, const parse_error_list_t *errors,
|
||||
wcstring &output) const;
|
||||
void get_backtrace(const wcstring &src, const parse_error_list_t &errors,
|
||||
wcstring &output) const;
|
||||
|
||||
/// Returns the file currently evaluated by the parser. This can be different than
|
||||
/// reader_current_filename, e.g. if we are evaluating a function defined in a different file
|
||||
/// than the one currently read.
|
||||
filename_ref_t current_filename() const;
|
||||
wcstring current_filename_ffi() const;
|
||||
void set_filename_ffi(wcstring filename);
|
||||
|
||||
/// Return if we are interactive, which means we are executing a command that the user typed in
|
||||
/// (and not, say, a prompt).
|
||||
bool is_interactive() const { return libdata().is_interactive; }
|
||||
|
||||
/// Return a string representing the current stack trace.
|
||||
wcstring stack_trace() const;
|
||||
|
||||
/// \return whether the number of functions in the stack exceeds our stack depth limit.
|
||||
bool function_stack_is_overflowing() const;
|
||||
|
||||
/// Mark whether we should sync universal variables.
|
||||
void set_syncs_uvars(bool flag) { syncs_uvars_ = flag; }
|
||||
|
||||
/// Set the given file descriptor as the working directory for this parser.
|
||||
/// This acquires ownership.
|
||||
void set_cwd_fd(int fd);
|
||||
|
||||
/// \return a shared pointer reference to this parser.
|
||||
std::shared_ptr<parser_t> shared();
|
||||
|
||||
/// \return a cancel poller for checking if this parser has been signalled.
|
||||
/// autocxx falls over with this so hide it.
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
cancel_checker_t cancel_checker() const;
|
||||
#include "parser.rs.h"
|
||||
#else
|
||||
struct EvalRes;
|
||||
#endif
|
||||
|
||||
/// \return the operation context for this parser.
|
||||
operation_context_t context();
|
||||
|
||||
/// Checks if the max eval depth has been exceeded
|
||||
bool is_eval_depth_exceeded() const { return eval_level >= FISH_MAX_EVAL_DEPTH; }
|
||||
|
||||
/// autocxx junk.
|
||||
RustFFIJobList ffi_jobs() const;
|
||||
library_data_pod_t *ffi_libdata_pod();
|
||||
job_t *ffi_job_get_from_pid(int pid) const;
|
||||
const library_data_pod_t &ffi_libdata_pod_const() const;
|
||||
|
||||
/// autocxx junk.
|
||||
bool ffi_has_funtion_block() const;
|
||||
|
||||
/// autocxx junk.
|
||||
uint64_t ffi_global_event_blocks() const;
|
||||
void ffi_incr_global_event_blocks();
|
||||
void ffi_decr_global_event_blocks();
|
||||
|
||||
/// autocxx junk.
|
||||
size_t ffi_blocks_size() const;
|
||||
|
||||
~Parser();
|
||||
};
|
||||
using eval_res_t = EvalRes;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// Functions having to do with parser keywords, like testing if a function is a block command.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "parser_keywords.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
|
||||
using string_set_t = std::unordered_set<wcstring>;
|
||||
|
||||
static const wcstring skip_keywords[]{
|
||||
L"else",
|
||||
L"begin",
|
||||
};
|
||||
|
||||
static const wcstring subcommand_keywords[]{L"command", L"builtin", L"while", L"exec", L"if",
|
||||
L"and", L"or", L"not", L"time", L"begin"};
|
||||
|
||||
static const string_set_t block_keywords = {L"for", L"while", L"if",
|
||||
L"function", L"switch", L"begin"};
|
||||
|
||||
// Don't forget to add any new reserved keywords to the documentation
|
||||
static const wcstring reserved_keywords[] = {
|
||||
L"end", L"case", L"else", L"return", L"continue", L"break", L"argparse", L"read",
|
||||
L"string", L"set", L"status", L"test", L"[", L"_", L"eval"};
|
||||
|
||||
// The lists above are purposely implemented separately from the logic below, so that future
|
||||
// maintainers may assume the contents of the list based off their names, and not off what the
|
||||
// functions below require them to contain.
|
||||
|
||||
static size_t list_max_length(const string_set_t &list) {
|
||||
size_t result = 0;
|
||||
for (const auto &w : list) {
|
||||
if (w.length() > result) {
|
||||
result = w.length();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool parser_keywords_is_subcommand(const wcstring &cmd) {
|
||||
const static string_set_t search_list = ([] {
|
||||
string_set_t results;
|
||||
results.insert(std::begin(subcommand_keywords), std::end(subcommand_keywords));
|
||||
results.insert(std::begin(skip_keywords), std::end(skip_keywords));
|
||||
return results;
|
||||
})();
|
||||
|
||||
const static auto max_len = list_max_length(search_list);
|
||||
const static auto not_found = search_list.end();
|
||||
|
||||
// Everything above is executed only at startup, this is the actual optimized search routine:
|
||||
return cmd.length() <= max_len && search_list.find(cmd) != not_found;
|
||||
}
|
||||
|
||||
bool parser_keywords_is_reserved(const wcstring &word) {
|
||||
const static string_set_t search_list = ([] {
|
||||
string_set_t results;
|
||||
results.insert(std::begin(subcommand_keywords), std::end(subcommand_keywords));
|
||||
results.insert(std::begin(skip_keywords), std::end(skip_keywords));
|
||||
results.insert(std::begin(block_keywords), std::end(block_keywords));
|
||||
results.insert(std::begin(reserved_keywords), std::end(reserved_keywords));
|
||||
return results;
|
||||
})();
|
||||
const static size_t max_len = list_max_length(search_list);
|
||||
return word.length() <= max_len && search_list.count(word) > 0;
|
||||
}
|
||||
@@ -134,7 +134,8 @@ static dir_remoteness_t path_remoteness(const wcstring &path) {
|
||||
}
|
||||
|
||||
std::vector<wcstring> path_apply_cdpath(const wcstring &dir, const wcstring &wd,
|
||||
const environment_t &env_vars) {
|
||||
// todo!("houd be environment_t")
|
||||
const env_stack_t &env_vars) {
|
||||
std::vector<wcstring> paths;
|
||||
if (dir.at(0) == L'/') {
|
||||
// Absolute path.
|
||||
@@ -174,7 +175,8 @@ std::vector<wcstring> path_apply_cdpath(const wcstring &dir, const wcstring &wd,
|
||||
}
|
||||
|
||||
maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
|
||||
const environment_t &env_vars) {
|
||||
// todo!("should be environment_t")
|
||||
const env_stack_t &env_vars) {
|
||||
int err = ENOENT;
|
||||
if (dir.empty()) return none();
|
||||
assert(!wd.empty() && wd.back() == L'/');
|
||||
@@ -195,7 +197,8 @@ maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
|
||||
}
|
||||
|
||||
maybe_t<wcstring> path_as_implicit_cd(const wcstring &path, const wcstring &wd,
|
||||
const environment_t &vars) {
|
||||
// todo!("should be environment_t")
|
||||
const env_stack_t &vars) {
|
||||
wcstring exp_path = path;
|
||||
expand_tilde(exp_path, vars);
|
||||
if (string_prefixes_string(L"/", exp_path) || string_prefixes_string(L"./", exp_path) ||
|
||||
|
||||
10
src/path.h
10
src/path.h
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "common.h"
|
||||
#include "env.h"
|
||||
#include "maybe.h"
|
||||
#include "parser.h"
|
||||
#include "wutil.h"
|
||||
@@ -41,12 +42,10 @@ dir_remoteness_t path_get_data_remoteness();
|
||||
/// Like path_get_data_remoteness but for the config directory.
|
||||
dir_remoteness_t path_get_config_remoteness();
|
||||
|
||||
class env_stack_t;
|
||||
/// Emit any errors if config directories are missing.
|
||||
/// Use the given environment stack to ensure this only occurs once.
|
||||
void path_emit_config_directory_messages(env_stack_t &vars);
|
||||
|
||||
class environment_t;
|
||||
/// Finds the path of an executable named \p cmd, by looking in $PATH taken from \p vars.
|
||||
/// \returns the path if found, none if not.
|
||||
maybe_t<wcstring> path_get_path(const wcstring &cmd, const environment_t &vars);
|
||||
@@ -75,16 +74,17 @@ get_path_result_t path_try_get_path(const wcstring &cmd, const environment_t &va
|
||||
/// \param vars The environment variables to use (for the CDPATH variable)
|
||||
/// \return the command, or none() if it could not be found.
|
||||
maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
|
||||
const environment_t &vars);
|
||||
// todo!("should be environment_t")
|
||||
const env_stack_t &vars);
|
||||
|
||||
/// Returns the given directory with all CDPATH components applied.
|
||||
std::vector<wcstring> path_apply_cdpath(const wcstring &dir, const wcstring &wd,
|
||||
const environment_t &env_vars);
|
||||
const env_stack_t &env_vars);
|
||||
|
||||
/// 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<wcstring> path_as_implicit_cd(const wcstring &path, const wcstring &wd,
|
||||
const environment_t &vars);
|
||||
const env_stack_t &vars);
|
||||
|
||||
/// Check if two paths are equivalent, which means to ignore runs of multiple slashes (or trailing
|
||||
/// slashes).
|
||||
|
||||
1040
src/proc.cpp
1040
src/proc.cpp
File diff suppressed because it is too large
Load Diff
606
src/proc.h
606
src/proc.h
@@ -22,609 +22,17 @@
|
||||
#include "cxx.h"
|
||||
#include "maybe.h"
|
||||
#include "parse_tree.h"
|
||||
#include "parser.h"
|
||||
#include "redirection.h"
|
||||
#include "topic_monitor.h"
|
||||
#include "wait_handle.h"
|
||||
|
||||
struct statuses_t;
|
||||
struct Parser;
|
||||
|
||||
/// Types of processes.
|
||||
enum class process_type_t : uint8_t {
|
||||
/// A regular external command.
|
||||
external,
|
||||
/// A builtin command.
|
||||
builtin,
|
||||
/// A shellscript function.
|
||||
function,
|
||||
/// A block of commands, represented as a node.
|
||||
block_node,
|
||||
/// The exec builtin.
|
||||
exec,
|
||||
};
|
||||
|
||||
enum class job_control_t : uint8_t {
|
||||
all,
|
||||
interactive,
|
||||
none,
|
||||
};
|
||||
|
||||
/// A number of clock ticks.
|
||||
using clock_ticks_t = uint64_t;
|
||||
|
||||
/// \return clock ticks in seconds, or 0 on failure.
|
||||
/// This uses sysconf(_SC_CLK_TCK) to convert to seconds.
|
||||
double clock_ticks_to_seconds(clock_ticks_t ticks);
|
||||
|
||||
struct job_group_t;
|
||||
using job_group_ref_t = std::shared_ptr<job_group_t>;
|
||||
|
||||
/// A proc_status_t is a value type that encapsulates logic around exited vs stopped vs signaled,
|
||||
/// etc.
|
||||
class proc_status_t {
|
||||
int status_{};
|
||||
|
||||
/// If set, there is no actual status to report, e.g. background or variable assignment.
|
||||
bool empty_{};
|
||||
|
||||
explicit proc_status_t(int status) : status_(status), empty_(false) {}
|
||||
|
||||
proc_status_t(int status, bool empty) : status_(status), empty_(empty) {}
|
||||
|
||||
/// Encode a return value \p ret and signal \p sig into a status value like waitpid() does.
|
||||
static constexpr int w_exitcode(int ret, int sig) {
|
||||
#ifdef W_EXITCODE
|
||||
return W_EXITCODE(ret, sig);
|
||||
#elif HAVE_WAITSTATUS_SIGNAL_RET
|
||||
// It's encoded signal and then status
|
||||
// The return status is in the lower byte.
|
||||
return ((sig) << 8 | (ret));
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "proc.rs.h"
|
||||
#else
|
||||
// The status is encoded in the upper byte.
|
||||
return ((ret) << 8 | (sig));
|
||||
struct JobRefFfi;
|
||||
struct JobGroupRefFfi;
|
||||
struct JobListFFI;
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
proc_status_t() = default;
|
||||
|
||||
/// Construct from a status returned from a waitpid call.
|
||||
static proc_status_t from_waitpid(int status) { return proc_status_t(status); }
|
||||
|
||||
/// Construct directly from an exit code.
|
||||
static proc_status_t from_exit_code(int ret) {
|
||||
assert(ret >= 0 &&
|
||||
"trying to create proc_status_t from failed wait{,id,pid}() call"
|
||||
" or invalid builtin exit code!");
|
||||
|
||||
// Some paranoia.
|
||||
constexpr int zerocode = w_exitcode(0, 0);
|
||||
static_assert(WIFEXITED(zerocode), "Synthetic exit status not reported as exited");
|
||||
|
||||
assert(ret < 256);
|
||||
return proc_status_t(w_exitcode(ret, 0 /* sig */));
|
||||
}
|
||||
|
||||
/// Construct directly from a signal.
|
||||
static proc_status_t from_signal(int sig) {
|
||||
return proc_status_t(w_exitcode(0 /* ret */, sig));
|
||||
}
|
||||
|
||||
/// Construct an empty status_t (e.g. `set foo bar`).
|
||||
static proc_status_t empty() {
|
||||
bool empty = true;
|
||||
return proc_status_t(0, empty);
|
||||
}
|
||||
|
||||
/// \return if we are stopped (as in SIGSTOP).
|
||||
bool stopped() const { return WIFSTOPPED(status_); }
|
||||
|
||||
/// \return if we are continued (as in SIGCONT).
|
||||
bool continued() const { return WIFCONTINUED(status_); }
|
||||
|
||||
/// \return if we exited normally (not a signal).
|
||||
bool normal_exited() const { return WIFEXITED(status_); }
|
||||
|
||||
/// \return if we exited because of a signal.
|
||||
bool signal_exited() const { return WIFSIGNALED(status_); }
|
||||
|
||||
/// \return the signal code, given that we signal exited.
|
||||
int signal_code() const {
|
||||
assert(signal_exited() && "Process is not signal exited");
|
||||
return WTERMSIG(status_);
|
||||
}
|
||||
|
||||
/// \return the exit code, given that we normal exited.
|
||||
int exit_code() const {
|
||||
assert(normal_exited() && "Process is not normal exited");
|
||||
return WEXITSTATUS(status_);
|
||||
}
|
||||
|
||||
/// \return if this status represents success.
|
||||
bool is_success() const { return normal_exited() && exit_code() == EXIT_SUCCESS; }
|
||||
|
||||
/// \return if this status is empty.
|
||||
bool is_empty() const { return empty_; }
|
||||
|
||||
/// \return the value appropriate to populate $status.
|
||||
int status_value() const {
|
||||
if (signal_exited()) {
|
||||
return 128 + signal_code();
|
||||
} else if (normal_exited()) {
|
||||
return exit_code();
|
||||
} else {
|
||||
DIE("Process is not exited");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// A structure representing a "process" internal to fish. This is backed by a pthread instead of a
|
||||
/// separate process.
|
||||
class internal_proc_t {
|
||||
/// An identifier for internal processes.
|
||||
/// This is used for logging purposes only.
|
||||
const uint64_t internal_proc_id_;
|
||||
|
||||
/// Whether the process has exited.
|
||||
std::atomic<bool> exited_{};
|
||||
|
||||
/// If the process has exited, its status code.
|
||||
std::atomic<proc_status_t> status_{};
|
||||
|
||||
public:
|
||||
/// \return if this process has exited.
|
||||
bool exited() const { return exited_.load(std::memory_order_acquire); }
|
||||
|
||||
/// Mark this process as exited, with the given status.
|
||||
void mark_exited(proc_status_t status);
|
||||
|
||||
proc_status_t get_status() const {
|
||||
assert(exited() && "Process is not exited");
|
||||
return status_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint64_t get_id() const { return internal_proc_id_; }
|
||||
|
||||
internal_proc_t();
|
||||
};
|
||||
|
||||
/// 0 should not be used; although it is not a valid PGID in userspace,
|
||||
/// the Linux kernel will use it for kernel processes.
|
||||
/// -1 should not be used; it is a possible return value of the getpgid()
|
||||
/// function
|
||||
enum { INVALID_PID = -2 };
|
||||
|
||||
// Allows transferring the tty to a job group, while it runs.
|
||||
class tty_transfer_t : nonmovable_t, noncopyable_t {
|
||||
public:
|
||||
tty_transfer_t() = default;
|
||||
|
||||
/// Transfer to the given job group, if it wants to own the terminal.
|
||||
void to_job_group(const job_group_ref_t &jg);
|
||||
|
||||
/// Reclaim the tty if we transferred it.
|
||||
void reclaim();
|
||||
|
||||
/// Save the current tty modes into the owning job group, if we are transferred.
|
||||
void save_tty_modes();
|
||||
|
||||
/// The destructor will assert if reclaim() has not been called.
|
||||
~tty_transfer_t();
|
||||
|
||||
private:
|
||||
// Try transferring the tty to the given job group.
|
||||
// \return true if we should reclaim it.
|
||||
static bool try_transfer(const job_group_ref_t &jg);
|
||||
|
||||
// The job group which owns the tty, or empty if none.
|
||||
job_group_ref_t owner_;
|
||||
};
|
||||
|
||||
/// A structure representing a single fish process. Contains variables for tracking process state
|
||||
/// and the process argument list. Actually, a fish process can be either a regular external
|
||||
/// process, an internal builtin which may or may not spawn a fake IO process during execution, a
|
||||
/// shellscript function or a block of commands to be evaluated by calling eval. Lastly, this
|
||||
/// process can be the result of an exec command. The role of this process_t is determined by the
|
||||
/// type field, which can be one of process_type_t::external, process_type_t::builtin,
|
||||
/// process_type_t::function, process_type_t::exec.
|
||||
///
|
||||
/// The process_t contains information on how the process should be started, such as command name
|
||||
/// and arguments, as well as runtime information on the status of the actual physical process which
|
||||
/// represents it. Shellscript functions, builtins and blocks of code may all need to spawn an
|
||||
/// external process that handles the piping and redirecting of IO for them.
|
||||
///
|
||||
/// If the process is of type process_type_t::external or process_type_t::exec, argv is the argument
|
||||
/// array and actual_cmd is the absolute path of the command to execute.
|
||||
///
|
||||
/// If the process is of type process_type_t::builtin, argv is the argument vector, and argv[0] is
|
||||
/// the name of the builtin command.
|
||||
///
|
||||
/// If the process is of type process_type_t::function, argv is the argument vector, and argv[0] is
|
||||
/// the name of the shellscript function.
|
||||
class process_t {
|
||||
public:
|
||||
process_t();
|
||||
|
||||
/// Note whether we are the first and/or last in the job
|
||||
bool is_first_in_job{false};
|
||||
bool is_last_in_job{false};
|
||||
|
||||
/// Type of process.
|
||||
process_type_t type{process_type_t::external};
|
||||
|
||||
/// For internal block processes only, the node of the statement.
|
||||
/// This is always either block, ifs, or switchs, never boolean or decorated.
|
||||
rust::Box<ParsedSourceRefFFI> block_node_source;
|
||||
const ast::statement_t *internal_block_node{};
|
||||
|
||||
struct concrete_assignment {
|
||||
wcstring variable_name;
|
||||
std::vector<wcstring> values;
|
||||
};
|
||||
/// The expanded variable assignments for this process, as specified by the `a=b cmd` syntax.
|
||||
std::vector<concrete_assignment> variable_assignments;
|
||||
|
||||
/// Sets argv.
|
||||
void set_argv(std::vector<wcstring> argv) { argv_ = std::move(argv); }
|
||||
|
||||
/// Returns argv.
|
||||
const std::vector<wcstring> &argv() { return argv_; }
|
||||
|
||||
/// Returns argv[0], or nullptr.
|
||||
const wchar_t *argv0() const { return argv_.empty() ? nullptr : argv_.front().c_str(); }
|
||||
|
||||
/// Redirection list getter and setter.
|
||||
const redirection_spec_list_t &redirection_specs() const { return *proc_redirection_specs_; }
|
||||
|
||||
void set_redirection_specs(rust::Box<redirection_spec_list_t> specs) {
|
||||
this->proc_redirection_specs_ = std::move(specs);
|
||||
}
|
||||
|
||||
/// Store the current topic generations. That is, right before the process is launched, record
|
||||
/// the generations of all topics; then we can tell which generation values have changed after
|
||||
/// launch. This helps us avoid spurious waitpid calls.
|
||||
void check_generations_before_launch();
|
||||
|
||||
/// Mark that this process was part of a pipeline which was aborted.
|
||||
/// The process was never successfully launched; give it a status of EXIT_FAILURE.
|
||||
void mark_aborted_before_launch();
|
||||
|
||||
/// \return whether this process type is internal (block, function, or builtin).
|
||||
bool is_internal() const;
|
||||
|
||||
/// \return whether this process leads its process group.
|
||||
bool get_leads_pgrp() const { return leads_pgrp; }
|
||||
|
||||
/// \return our pid, or 0 if not an external process.
|
||||
int get_pid() const { return pid; }
|
||||
|
||||
/// \return the wait handle for the process, if it exists.
|
||||
rust::Box<WaitHandleRefFFI> *get_wait_handle_ffi() const;
|
||||
|
||||
/// Create a wait handle for the process.
|
||||
/// As a process does not know its job id, we pass it in.
|
||||
/// Note this will return null if the process is not waitable (has no pid).
|
||||
rust::Box<WaitHandleRefFFI> *make_wait_handle_ffi(internal_job_id_t jid);
|
||||
|
||||
/// Variants of get and make that return void*, to satisfy autocxx.
|
||||
void *get_wait_handle_void() const;
|
||||
void *make_wait_handle_void(internal_job_id_t jid);
|
||||
|
||||
/// Actual command to pass to exec in case of process_type_t::external or process_type_t::exec.
|
||||
wcstring actual_cmd;
|
||||
|
||||
/// Generation counts for reaping.
|
||||
generation_list_t gens_{};
|
||||
|
||||
/// Process ID
|
||||
pid_t pid{0};
|
||||
|
||||
/// If we are an "internal process," that process.
|
||||
std::shared_ptr<internal_proc_t> internal_proc_{};
|
||||
|
||||
/// File descriptor that pipe output should bind to.
|
||||
int pipe_write_fd{0};
|
||||
|
||||
/// True if process has completed.
|
||||
bool completed{false};
|
||||
|
||||
/// True if process has stopped.
|
||||
bool stopped{false};
|
||||
|
||||
/// If set, this process is (or will become) the pgroup leader.
|
||||
/// This is only meaningful for external processes.
|
||||
bool leads_pgrp{false};
|
||||
|
||||
/// Whether we have generated a proc_exit event.
|
||||
bool posted_proc_exit{false};
|
||||
|
||||
/// Reported status value.
|
||||
proc_status_t status{};
|
||||
|
||||
/// Last time of cpu time check, in seconds (per timef).
|
||||
timepoint_t last_time{0};
|
||||
|
||||
/// Number of jiffies spent in process at last cpu time check.
|
||||
clock_ticks_t last_jiffies{0};
|
||||
|
||||
process_t(process_t &&) = delete;
|
||||
process_t &operator=(process_t &&) = delete;
|
||||
process_t(const process_t &) = delete;
|
||||
process_t &operator=(const process_t &) = delete;
|
||||
|
||||
private:
|
||||
std::vector<wcstring> argv_;
|
||||
rust::Box<redirection_spec_list_t> proc_redirection_specs_;
|
||||
|
||||
// The wait handle. This is constructed lazily, and cached.
|
||||
// This may be null.
|
||||
std::unique_ptr<rust::Box<WaitHandleRefFFI>> wait_handle_;
|
||||
};
|
||||
|
||||
using process_ptr_t = std::unique_ptr<process_t>;
|
||||
using process_list_t = std::vector<process_ptr_t>;
|
||||
class Parser; using parser_t = Parser;
|
||||
|
||||
struct RustFFIProcList {
|
||||
process_ptr_t *procs;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
/// A struct representing a job. A job is a pipeline of one or more processes.
|
||||
class job_t : noncopyable_t {
|
||||
public:
|
||||
/// A set of jobs properties. These are immutable: they do not change for the lifetime of the
|
||||
/// job.
|
||||
struct properties_t {
|
||||
/// Whether the specified job is a part of a subshell, event handler or some other form of
|
||||
/// special job that should not be reported.
|
||||
bool skip_notification{};
|
||||
|
||||
/// Whether the job had the background ampersand when constructed, e.g. /bin/echo foo &
|
||||
/// Note that a job may move between foreground and background; this just describes what the
|
||||
/// initial state should be.
|
||||
bool initial_background{};
|
||||
|
||||
/// Whether the job has the 'time' prefix and so we should print timing for this job.
|
||||
bool wants_timing{};
|
||||
|
||||
/// Whether this job was created as part of an event handler.
|
||||
bool from_event_handler{};
|
||||
};
|
||||
|
||||
private:
|
||||
/// Set of immutable job properties.
|
||||
const properties_t properties;
|
||||
|
||||
/// The original command which led to the creation of this job. It is used for displaying
|
||||
/// messages about job status on the terminal.
|
||||
const wcstring command_str;
|
||||
|
||||
public:
|
||||
job_t(const properties_t &props, wcstring command_str);
|
||||
~job_t();
|
||||
|
||||
/// Autocxx needs to see this.
|
||||
job_t(const job_t &) = delete;
|
||||
|
||||
/// Returns the command as a wchar_t *. */
|
||||
const wchar_t *command_wcstr() const { return command_str.c_str(); }
|
||||
|
||||
/// Returns the command.
|
||||
const wcstring &command() const { return command_str; }
|
||||
|
||||
/// \return whether it is OK to reap a given process. Sometimes we want to defer reaping a
|
||||
/// process if it is the group leader and the job is not yet constructed, because then we might
|
||||
/// also reap the process group and then we cannot add new processes to the group.
|
||||
bool can_reap(const process_ptr_t &p) const {
|
||||
if (p->completed) {
|
||||
// Can't reap twice.
|
||||
return false;
|
||||
} else if (p->pid && !is_constructed() && this->get_pgid() == maybe_t<pid_t>{p->pid}) {
|
||||
// p is the the group leader in an under-construction job.
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a truncated version of the job string. Used when a message has already been emitted
|
||||
/// containing the full job string and job id, but using the job id alone would be confusing
|
||||
/// due to reuse of freed job ids. Prevents overloading the debug comments with the full,
|
||||
/// untruncated job string when we don't care what the job is, only which of the currently
|
||||
/// running jobs it is.
|
||||
wcstring preview() const {
|
||||
if (processes.empty()) return L"";
|
||||
// Note argv0 may be empty in e.g. a block process.
|
||||
const wchar_t *argv0 = processes.front()->argv0();
|
||||
wcstring result = argv0 ? argv0 : L"null";
|
||||
return result + L" ...";
|
||||
}
|
||||
|
||||
/// All the processes in this job.
|
||||
process_list_t processes;
|
||||
|
||||
// The group containing this job.
|
||||
// This is never null and not changed after construction.
|
||||
job_group_ref_t group{};
|
||||
|
||||
/// \return our pgid, or none if we don't have one, or are internal to fish
|
||||
/// This never returns fish's own pgroup.
|
||||
maybe_t<pid_t> get_pgid() const;
|
||||
|
||||
/// \return the pid of the last external process in the job.
|
||||
/// This may be none if the job consists of just internal fish functions or builtins.
|
||||
/// This will never be fish's own pid.
|
||||
maybe_t<pid_t> get_last_pid() const;
|
||||
|
||||
/// The id of this job.
|
||||
/// This is user-visible, is recycled, and may be -1.
|
||||
job_id_t job_id() const;
|
||||
|
||||
/// A non-user-visible, never-recycled job ID.
|
||||
const internal_job_id_t internal_job_id;
|
||||
|
||||
/// Getter to enable ffi.
|
||||
internal_job_id_t get_internal_job_id() const { return internal_job_id; }
|
||||
|
||||
/// Flags associated with the job.
|
||||
struct flags_t {
|
||||
/// Whether the specified job is completely constructed: every process in the job has been
|
||||
/// forked, etc.
|
||||
bool constructed{false};
|
||||
|
||||
/// Whether the user has been notified that this job is stopped (if it is).
|
||||
bool notified_of_stop{false};
|
||||
|
||||
/// Whether the exit status should be negated. This flag can only be set by the not builtin.
|
||||
/// Two "not" prefixes on a single job cancel each other out.
|
||||
bool negate{false};
|
||||
|
||||
/// This job is disowned, and should be removed from the active jobs list.
|
||||
bool disown_requested{false};
|
||||
|
||||
// Indicates that we are the "group root." Any other jobs using this tree are nested.
|
||||
bool is_group_root{false};
|
||||
|
||||
} job_flags{};
|
||||
|
||||
/// Access the job flags.
|
||||
const flags_t &flags() const { return job_flags; }
|
||||
|
||||
/// Access mutable job flags.
|
||||
flags_t &mut_flags() { return job_flags; }
|
||||
|
||||
// \return whether we should print timing information.
|
||||
bool wants_timing() const { return properties.wants_timing; }
|
||||
|
||||
/// \return if we want job control.
|
||||
bool wants_job_control() const;
|
||||
|
||||
/// \return whether this job is initially going to run in the background, because & was
|
||||
/// specified.
|
||||
bool is_initially_background() const { return properties.initial_background; }
|
||||
|
||||
/// Mark this job as constructed. The job must not have previously been marked as constructed.
|
||||
void mark_constructed();
|
||||
|
||||
/// \return whether we have internal or external procs, respectively.
|
||||
/// Internal procs are builtins, blocks, and functions.
|
||||
/// External procs include exec and external.
|
||||
bool has_external_proc() const;
|
||||
|
||||
/// \return whether this job, when run, will want a job ID.
|
||||
/// Jobs that are only a single internal block do not get a job ID.
|
||||
bool wants_job_id() const;
|
||||
|
||||
// Helper functions to check presence of flags on instances of jobs
|
||||
/// The job has been fully constructed, i.e. all its member processes have been launched
|
||||
bool is_constructed() const { return flags().constructed; }
|
||||
/// The job is complete, i.e. all its member processes have been reaped
|
||||
bool is_completed() const;
|
||||
/// The job is in a stopped state
|
||||
bool is_stopped() const;
|
||||
/// The job is OK to be externally visible, e.g. to the user via `jobs`
|
||||
bool is_visible() const {
|
||||
return !is_completed() && is_constructed() && !flags().disown_requested;
|
||||
}
|
||||
bool skip_notification() const { return properties.skip_notification; }
|
||||
bool from_event_handler() const { return properties.from_event_handler; }
|
||||
|
||||
/// \return whether this job's group is in the foreground.
|
||||
bool is_foreground() const;
|
||||
|
||||
/// \return whether we should post job_exit events.
|
||||
bool posts_job_exit_events() const;
|
||||
|
||||
/// Run ourselves. Returning once we complete or stop.
|
||||
void continue_job(parser_t &parser);
|
||||
|
||||
/// Prepare to resume a stopped job by sending SIGCONT and clearing the stopped flag.
|
||||
/// \return true on success, false if we failed to send the signal.
|
||||
bool resume();
|
||||
|
||||
/// Send the specified signal to all processes in this job.
|
||||
/// \return true on success, false on failure.
|
||||
bool signal(int signal);
|
||||
|
||||
/// \returns the statuses for this job.
|
||||
maybe_t<statuses_t> get_statuses() const;
|
||||
|
||||
/// autocxx junk.
|
||||
RustFFIProcList ffi_processes() const;
|
||||
|
||||
/// autocxx junk.
|
||||
const job_group_t &ffi_group() const;
|
||||
|
||||
/// autocxx junk.
|
||||
/// The const is a lie and is only necessary since at the moment cxx's SharedPtr doesn't support
|
||||
/// getting a mutable reference.
|
||||
bool ffi_resume() const;
|
||||
};
|
||||
using job_ref_t = std::shared_ptr<job_t>;
|
||||
|
||||
// Helper junk for autocxx.
|
||||
struct RustFFIJobList {
|
||||
job_ref_t *jobs;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
/// Whether this shell is attached to a tty.
|
||||
bool is_interactive_session();
|
||||
void set_interactive_session(bool flag);
|
||||
|
||||
/// Whether we are a login shell.
|
||||
bool get_login();
|
||||
void mark_login();
|
||||
|
||||
/// If this flag is set, fish will never fork or run execve. It is used to put fish into a syntax
|
||||
/// verifier mode where fish tries to validate the syntax of a file but doesn't actually do
|
||||
/// anything.
|
||||
bool no_exec();
|
||||
void mark_no_exec();
|
||||
|
||||
// List of jobs.
|
||||
using job_list_t = std::vector<job_ref_t>;
|
||||
|
||||
/// The current job control mode.
|
||||
///
|
||||
/// Must be one of job_control_t::all, job_control_t::interactive and job_control_t::none.
|
||||
job_control_t get_job_control_mode();
|
||||
void set_job_control_mode(job_control_t mode);
|
||||
|
||||
/// Notify the user about stopped or terminated jobs, and delete completed jobs from the job list.
|
||||
/// If \p interactive is set, allow removing interactive jobs; otherwise skip them.
|
||||
/// \return whether text was printed to stdout.
|
||||
bool job_reap(parser_t &parser, bool interactive);
|
||||
|
||||
/// \return the list of background jobs which we should warn the user about, if the user attempts to
|
||||
/// exit. An empty result (common) means no such jobs.
|
||||
job_list_t jobs_requiring_warning_on_exit(const parser_t &parser);
|
||||
|
||||
/// Print the exit warning for the given jobs, which should have been obtained via
|
||||
/// jobs_requiring_warning_on_exit().
|
||||
void print_exit_warning_for_jobs(const job_list_t &jobs);
|
||||
|
||||
/// Use the procfs filesystem to look up how many jiffies of cpu time was used by a given pid. This
|
||||
/// function is only available on systems with the procfs file entry 'stat', i.e. Linux.
|
||||
clock_ticks_t proc_get_jiffies(pid_t inpid);
|
||||
|
||||
/// Update process time usage for all processes by calling the proc_get_jiffies function for every
|
||||
/// process of every job.
|
||||
void proc_update_jiffies(parser_t &parser);
|
||||
|
||||
/// Initializations.
|
||||
void proc_init();
|
||||
|
||||
/// Wait for any process finishing, or receipt of a signal.
|
||||
void proc_wait_any(parser_t &parser);
|
||||
|
||||
/// Send SIGHUP to the list \p jobs, excepting those which are in fish's pgroup.
|
||||
void hup_jobs(const job_list_t &jobs);
|
||||
|
||||
/// Add a job to the list of PIDs/PGIDs we wait on even though they are not associated with any
|
||||
/// jobs. Used to avoid zombie processes after disown.
|
||||
void add_disowned_job(const job_t *j);
|
||||
|
||||
bool have_proc_stat();
|
||||
|
||||
#endif
|
||||
|
||||
669
src/reader.cpp
669
src/reader.cpp
File diff suppressed because it is too large
Load Diff
77
src/reader.h
77
src/reader.h
@@ -14,15 +14,16 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "complete.h"
|
||||
#include "env.h"
|
||||
#include "highlight.h"
|
||||
#include "io.h"
|
||||
#include "maybe.h"
|
||||
#include "parse_constants.h"
|
||||
#include "parser.h"
|
||||
|
||||
class env_stack_t;
|
||||
class environment_t;
|
||||
class history_t;
|
||||
class io_chain_t;
|
||||
class Parser; using parser_t = Parser;
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "reader.rs.h"
|
||||
#endif
|
||||
|
||||
/// An edit action that can be undone.
|
||||
struct edit_t {
|
||||
@@ -87,10 +88,7 @@ class editable_line_t {
|
||||
const wcstring &text() const { return text_; }
|
||||
|
||||
const std::vector<highlight_spec_t> &colors() const { return colors_; }
|
||||
void set_colors(std::vector<highlight_spec_t> colors) {
|
||||
assert(colors.size() == size());
|
||||
colors_ = std::move(colors);
|
||||
}
|
||||
void set_colors(std::vector<highlight_spec_t> colors);
|
||||
|
||||
size_t position() const { return position_; }
|
||||
void set_position(size_t position) { position_ = position; }
|
||||
@@ -139,15 +137,10 @@ class editable_line_t {
|
||||
uint32_t edit_group_id_ = -1;
|
||||
};
|
||||
|
||||
int reader_read_ffi(parser_t &parser, int fd);
|
||||
int reader_read_ffi(const void *parser, int fd, const void *io_chain);
|
||||
/// Read commands from \c fd until encountering EOF.
|
||||
/// The fd is not closed.
|
||||
int reader_read(parser_t &parser, int fd, const io_chain_t &io);
|
||||
|
||||
/// Mark that we encountered SIGHUP and must (soon) exit. This is invoked from a signal handler.
|
||||
extern "C" {
|
||||
void reader_sighup();
|
||||
}
|
||||
int reader_read(const parser_t &parser, int fd, const io_chain_t &io);
|
||||
|
||||
/// Initialize the reader.
|
||||
void reader_init();
|
||||
@@ -186,7 +179,10 @@ void reader_set_autosuggestion_enabled_ffi(bool enabled);
|
||||
/// \param cmd Command line string passed to \c fish_title if is defined.
|
||||
/// \param parser The parser to use for autoloading fish_title.
|
||||
/// \param reset_cursor_position If set, issue a \r so the line driver knows where we are
|
||||
void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor_position = true);
|
||||
void reader_write_title(const wcstring &cmd, const parser_t &parser,
|
||||
bool reset_cursor_position = true);
|
||||
|
||||
void reader_write_title_ffi(const wcstring &cmd, const void *parser, bool reset_cursor_position);
|
||||
|
||||
/// Tell the reader that it needs to re-exec the prompt and repaint.
|
||||
/// This may be called in response to e.g. a color variable change.
|
||||
@@ -196,15 +192,6 @@ void reader_schedule_prompt_repaint();
|
||||
class char_event_t;
|
||||
void reader_queue_ch(const char_event_t &ch);
|
||||
|
||||
/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it
|
||||
/// was set. In practice this will return 0 or SIGINT.
|
||||
int reader_test_and_clear_interrupted();
|
||||
|
||||
/// Clear the interrupted flag unconditionally without handling anything. The flag could have been
|
||||
/// set e.g. when an interrupt arrived just as we were ending an earlier \c reader_readline
|
||||
/// invocation but before the \c is_interactive_read flag was cleared.
|
||||
void reader_reset_interrupted();
|
||||
|
||||
/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it
|
||||
/// was set. If the current reader is interruptible, call \c reader_exit().
|
||||
int reader_reading_interrupted();
|
||||
@@ -216,6 +203,8 @@ int reader_reading_interrupted();
|
||||
/// commandline.
|
||||
maybe_t<wcstring> reader_readline(int nchars);
|
||||
|
||||
bool reader_readline_ffi(wcstring &line, int nchars);
|
||||
|
||||
/// Configuration that we provide to a reader.
|
||||
struct reader_config_t {
|
||||
/// Left prompt command, typically fish_prompt.
|
||||
@@ -257,16 +246,13 @@ bool check_exit_loop_maybe_warning(reader_data_t *data);
|
||||
|
||||
/// Push a new reader environment controlled by \p conf, using the given history name.
|
||||
/// If \p history_name is empty, then save history in-memory only; do not write it to disk.
|
||||
void reader_push(parser_t &parser, const wcstring &history_name, reader_config_t &&conf);
|
||||
void reader_push(const parser_t &parser, const wcstring &history_name, reader_config_t &&conf);
|
||||
|
||||
void reader_push_ffi(const void *parser, const wcstring &history_name, const void *conf);
|
||||
|
||||
/// Return to previous reader environment.
|
||||
void reader_pop();
|
||||
|
||||
/// The readers interrupt signal handler. Cancels all currently running blocks.
|
||||
extern "C" {
|
||||
void reader_handle_sigint();
|
||||
}
|
||||
|
||||
/// \return whether fish is currently unwinding the stack in preparation to exit.
|
||||
bool fish_is_unwinding_for_exit();
|
||||
|
||||
@@ -282,7 +268,7 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
|
||||
struct abbrs_replacement_t;
|
||||
maybe_t<abbrs_replacement_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline,
|
||||
size_t cursor_pos,
|
||||
parser_t &parser);
|
||||
const parser_t &parser);
|
||||
|
||||
/// Apply a completion string. Exposed for testing only.
|
||||
wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags,
|
||||
@@ -291,22 +277,28 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag
|
||||
|
||||
/// Snapshotted state from the reader.
|
||||
struct commandline_state_t {
|
||||
wcstring text; // command line text, or empty if not interactive
|
||||
size_t cursor_pos{0}; // position of the cursor, may be as large as text.size()
|
||||
maybe_t<source_range_t> selection{}; // visual selection, or none if none
|
||||
std::shared_ptr<history_t> history{}; // current reader history, or null if not interactive
|
||||
bool pager_mode{false}; // pager is visible
|
||||
bool pager_fully_disclosed{false}; // pager already shows everything if possible
|
||||
bool search_mode{false}; // pager is visible and search is active
|
||||
bool initialized{false}; // if false, the reader has not yet been entered
|
||||
wcstring text; // command line text, or empty if not interactive
|
||||
size_t cursor_pos{0}; // position of the cursor, may be as large as text.size()
|
||||
maybe_t<source_range_t> selection{}; // visual selection, or none if none
|
||||
maybe_t<rust::Box<HistorySharedPtr>>
|
||||
history{}; // current reader history, or null if not interactive
|
||||
bool pager_mode{false}; // pager is visible
|
||||
bool pager_fully_disclosed{false}; // pager already shows everything if possible
|
||||
bool search_mode{false}; // pager is visible and search is active
|
||||
bool initialized{false}; // if false, the reader has not yet been entered
|
||||
};
|
||||
|
||||
/// Get the command line state. This may be fetched on a background thread.
|
||||
commandline_state_t commandline_get_state();
|
||||
|
||||
HistorySharedPtr *commandline_get_state_history_ffi();
|
||||
bool commandline_get_state_initialized_ffi();
|
||||
wcstring commandline_get_state_text_ffi();
|
||||
|
||||
/// Set the command line text and position. This may be called on a background thread; the reader
|
||||
/// will pick it up when it is done executing.
|
||||
void commandline_set_buffer(wcstring text, size_t cursor_pos = -1);
|
||||
void commandline_set_buffer_ffi(const wcstring &text, size_t cursor_pos);
|
||||
|
||||
/// Return the current interactive reads loop count. Useful for determining how many commands have
|
||||
/// been executed between invocations of code.
|
||||
@@ -316,4 +308,7 @@ uint64_t reader_run_count();
|
||||
/// previous command produced a status.
|
||||
uint64_t reader_status_count();
|
||||
|
||||
// For FFI
|
||||
uint32_t read_generation_count();
|
||||
|
||||
#endif
|
||||
|
||||
@@ -17,7 +17,7 @@ enum class RedirectionMode {
|
||||
noclob,
|
||||
};
|
||||
struct Dup2Action;
|
||||
class Dup2List;
|
||||
struct Dup2List;
|
||||
struct RedirectionSpec;
|
||||
struct RedirectionSpecListFfi;
|
||||
|
||||
|
||||
@@ -706,12 +706,15 @@ bool screen_t::handle_soft_wrap(int x, int y) {
|
||||
|
||||
/// Update the screen to match the desired output.
|
||||
void screen_t::update(const wcstring &left_prompt, const wcstring &right_prompt,
|
||||
const environment_t &vars) {
|
||||
const env_stack_t &vars) {
|
||||
// Helper function to set a resolved color, using the caching resolver.
|
||||
highlight_color_resolver_t color_resolver{};
|
||||
auto color_resolver = new_highlight_color_resolver();
|
||||
auto set_color = [&](highlight_spec_t c) {
|
||||
this->outp().set_color(color_resolver.resolve_spec(c, false, vars),
|
||||
color_resolver.resolve_spec(c, true, vars));
|
||||
rgb_color_t fg;
|
||||
rgb_color_t bg;
|
||||
color_resolver->resolve_spec(c, false, vars.get_impl_ffi(), fg);
|
||||
color_resolver->resolve_spec(c, true, vars.get_impl_ffi(), bg);
|
||||
this->outp().set_color(fg, bg);
|
||||
};
|
||||
|
||||
layout_cache_t &cached_layouts = layout_cache_t::shared;
|
||||
@@ -1141,8 +1144,10 @@ static screen_layout_t compute_layout(screen_t *s, size_t screen_width,
|
||||
void screen_t::write(const wcstring &left_prompt, const wcstring &right_prompt,
|
||||
const wcstring &commandline, size_t explicit_len,
|
||||
const std::vector<highlight_spec_t> &colors, const std::vector<int> &indent,
|
||||
size_t cursor_pos, const environment_t &vars, pager_t &pager,
|
||||
page_rendering_t &page_rendering, bool cursor_is_within_pager) {
|
||||
size_t cursor_pos,
|
||||
// todo!("should be environment_t")
|
||||
const env_stack_t &vars, pager_t &pager, page_rendering_t &page_rendering,
|
||||
bool cursor_is_within_pager) {
|
||||
termsize_t curr_termsize = termsize_last();
|
||||
int screen_width = curr_termsize.width;
|
||||
static relaxed_atomic_t<uint32_t> s_repaints{0};
|
||||
|
||||
11
src/screen.h
11
src/screen.h
@@ -22,11 +22,11 @@
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "env.h"
|
||||
#include "highlight.h"
|
||||
#include "maybe.h"
|
||||
#include "wcstringutil.h"
|
||||
|
||||
class environment_t;
|
||||
class pager_t;
|
||||
class page_rendering_t;
|
||||
|
||||
@@ -149,8 +149,10 @@ class screen_t {
|
||||
void write(const wcstring &left_prompt, const wcstring &right_prompt,
|
||||
const wcstring &commandline, size_t explicit_len,
|
||||
const std::vector<highlight_spec_t> &colors, const std::vector<int> &indent,
|
||||
size_t cursor_pos, const environment_t &vars, pager_t &pager,
|
||||
page_rendering_t &page_rendering, bool cursor_is_within_pager);
|
||||
size_t cursor_pos,
|
||||
// todo!("this should be environment_t")
|
||||
const env_stack_t &vars, pager_t &pager, page_rendering_t &page_rendering,
|
||||
bool cursor_is_within_pager);
|
||||
|
||||
/// Resets the screen buffer's internal knowledge about the contents of the screen,
|
||||
/// optionally repainting the prompt as well.
|
||||
@@ -238,7 +240,8 @@ class screen_t {
|
||||
|
||||
/// Update the screen to match the desired output.
|
||||
void update(const wcstring &left_prompt, const wcstring &right_prompt,
|
||||
const environment_t &vars);
|
||||
// todo!("this should be environment_t")
|
||||
const env_stack_t &vars);
|
||||
};
|
||||
|
||||
/// Issues an immediate clr_eos.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// The library for various signal related issues.
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <errno.h>
|
||||
#ifdef HAVE_SIGINFO_H
|
||||
#include <siginfo.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <cwchar>
|
||||
#include <mutex>
|
||||
|
||||
#include "common.h"
|
||||
#include "event.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "global_safety.h"
|
||||
#include "reader.h"
|
||||
#include "signals.h"
|
||||
#include "termsize.h"
|
||||
#include "topic_monitor.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
extern "C" {
|
||||
void get_signals_with_handlers_ffi(sigset_t *set);
|
||||
}
|
||||
void get_signals_with_handlers(sigset_t *set) { get_signals_with_handlers_ffi(set); }
|
||||
|
||||
sigchecker_t::sigchecker_t(topic_t signal) : topic_(signal) {
|
||||
// Call check() to update our generation.
|
||||
check();
|
||||
}
|
||||
|
||||
bool sigchecker_t::check() {
|
||||
auto &tm = topic_monitor_principal();
|
||||
generation_t gen = tm.generation_for_topic(topic_);
|
||||
bool changed = this->gen_ != gen;
|
||||
this->gen_ = gen;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void sigchecker_t::wait() const {
|
||||
auto &tm = topic_monitor_principal();
|
||||
generation_list_t gens = invalid_generations();
|
||||
gens.at_mut(topic_) = this->gen_;
|
||||
tm.check(&gens, true /* wait */);
|
||||
}
|
||||
@@ -7,26 +7,11 @@
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "signal.rs.h"
|
||||
#else
|
||||
struct IoStreams;
|
||||
struct SigChecker;
|
||||
#endif
|
||||
|
||||
/// Returns signals with non-default handlers.
|
||||
void get_signals_with_handlers(sigset_t *set);
|
||||
|
||||
enum class topic_t : uint8_t;
|
||||
/// A sigint_detector_t can be used to check if a SIGINT (or SIGHUP) has been delivered.
|
||||
class sigchecker_t {
|
||||
const topic_t topic_;
|
||||
uint64_t gen_{0};
|
||||
|
||||
public:
|
||||
sigchecker_t(topic_t signal);
|
||||
|
||||
/// Check if a sigint has been delivered since the last call to check(), or since the detector
|
||||
/// was created.
|
||||
bool check();
|
||||
|
||||
/// Wait until a sigint is delivered.
|
||||
void wait() const;
|
||||
};
|
||||
using sigchecker_t = SigChecker;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#ifndef FISH_TOPIC_MONITOR_H
|
||||
#define FISH_TOPIC_MONITOR_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
using generation_t = uint64_t;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
|
||||
#include "topic_monitor.rs.h"
|
||||
|
||||
#else
|
||||
|
||||
// Hacks to allow us to compile without Rust headers.
|
||||
struct generation_list_t {
|
||||
uint64_t sighupint;
|
||||
uint64_t sigchld;
|
||||
uint64_t internal_exit;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,12 +0,0 @@
|
||||
#ifndef FISH_WAIT_HANDLE_H
|
||||
#define FISH_WAIT_HANDLE_H
|
||||
|
||||
// Hacks to allow us to compile without Rust headers.
|
||||
struct WaitHandleStoreFFI;
|
||||
struct WaitHandleRefFFI;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "wait_handle.rs.h"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -242,8 +242,8 @@ wildcard_result_t wildcard_complete(const wcstring &str, const wchar_t *wc,
|
||||
/// \param is_dir Whether the file is a directory or not (might be behind a link)
|
||||
/// \param is_link Whether it's a link (that might point to a directory)
|
||||
/// \param definitely_executable Whether we know that it is executable, or don't know
|
||||
static const wchar_t *file_get_desc(const wcstring &filename, bool is_dir,
|
||||
bool is_link, bool definitely_executable) {
|
||||
static const wchar_t *file_get_desc(const wcstring &filename, bool is_dir, bool is_link,
|
||||
bool definitely_executable) {
|
||||
if (is_link) {
|
||||
if (is_dir) {
|
||||
return COMPLETE_DIRECTORY_SYMLINK_DESC;
|
||||
@@ -267,7 +267,8 @@ static const wchar_t *file_get_desc(const wcstring &filename, bool is_dir,
|
||||
/// up. Note that the filename came from a readdir() call, so we know it exists.
|
||||
static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wcstring &filename,
|
||||
const wchar_t *wc, expand_flags_t expand_flags,
|
||||
completion_receiver_t *out, const dir_iter_t::entry_t &entry) {
|
||||
completion_receiver_t *out,
|
||||
const dir_iter_t::entry_t &entry) {
|
||||
const bool executables_only = expand_flags & expand_flag::executables_only;
|
||||
const bool need_directory = expand_flags & expand_flag::directories_only;
|
||||
// Fast path: If we need directories, and we already know it is one,
|
||||
@@ -322,7 +323,8 @@ static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wc
|
||||
}
|
||||
|
||||
// If we have executables_only, we already checked waccess above,
|
||||
// so we tell file_get_desc that this file is definitely executable so it can skip the check.
|
||||
// so we tell file_get_desc that this file is definitely executable so it can skip the
|
||||
// check.
|
||||
desc = file_get_desc(filepath, entry.is_dir(), is_link, executables_only);
|
||||
}
|
||||
|
||||
@@ -553,7 +555,8 @@ void wildcard_expander_t::expand_trailing_slash(const wcstring &base_dir, const
|
||||
}
|
||||
|
||||
if (!(flags & expand_flag::for_completions)) {
|
||||
// Trailing slash and not accepting incomplete, e.g. `echo /xyz/`. Insert this file, we already know it exists!
|
||||
// Trailing slash and not accepting incomplete, e.g. `echo /xyz/`. Insert this file, we
|
||||
// already know it exists!
|
||||
this->add_expansion_result(wcstring{base_dir});
|
||||
} else {
|
||||
// Trailing slashes and accepting incomplete, e.g. `echo /xyz/<tab>`. Everything is added.
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "common.h"
|
||||
#include "complete.h"
|
||||
#include "expand.h"
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "wildcard.rs.h"
|
||||
#endif
|
||||
|
||||
/// Description for generic executable.
|
||||
#define COMPLETE_EXEC_DESC _(L"command")
|
||||
@@ -70,46 +73,5 @@ enum class wildcard_result_t {
|
||||
cancel, /// Expansion was cancelled (e.g. control-C).
|
||||
overflow, /// Expansion produced too many results.
|
||||
};
|
||||
wildcard_result_t wildcard_expand_string(const wcstring &wc, const wcstring &working_directory,
|
||||
expand_flags_t flags,
|
||||
const cancel_checker_t &cancel_checker,
|
||||
completion_receiver_t *output);
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
|
||||
#include "wildcard.rs.h"
|
||||
|
||||
#else
|
||||
/// Test whether the given wildcard matches the string. Does not perform any I/O.
|
||||
///
|
||||
/// \param str The string to test
|
||||
/// \param wc The wildcard to test against
|
||||
/// \param leading_dots_fail_to_match if set, strings with leading dots are assumed to be hidden
|
||||
/// files and are not matched
|
||||
///
|
||||
/// \return true if the wildcard matched
|
||||
bool wildcard_match_ffi(const wcstring &str, const wcstring &wc, bool leading_dots_fail_to_match);
|
||||
|
||||
// Check if the string has any unescaped wildcards (e.g. ANY_STRING).
|
||||
bool wildcard_has_internal(const wcstring &s);
|
||||
|
||||
/// Check if the specified string contains wildcards (e.g. *).
|
||||
bool wildcard_has(const wcstring &s);
|
||||
|
||||
#endif
|
||||
|
||||
inline bool wildcard_match(const wcstring &str, const wcstring &wc,
|
||||
bool leading_dots_fail_to_match = false) {
|
||||
return wildcard_match_ffi(str, wc, leading_dots_fail_to_match);
|
||||
}
|
||||
|
||||
inline bool wildcard_has(const wchar_t *s, size_t len) {
|
||||
return wildcard_has(wcstring(s, len));
|
||||
};
|
||||
|
||||
/// Test wildcard completion.
|
||||
wildcard_result_t wildcard_complete(const wcstring &str, const wchar_t *wc,
|
||||
const description_func_t &desc_func, completion_receiver_t *out,
|
||||
expand_flags_t expand_flags, complete_flags_t flags);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -40,6 +40,8 @@ using cstring = std::string;
|
||||
|
||||
const file_id_t kInvalidFileID{};
|
||||
|
||||
wcstring_list_ffi_t::~wcstring_list_ffi_t() = default;
|
||||
|
||||
/// Map used as cache by wgettext.
|
||||
static owning_lock<std::unordered_map<wcstring, wcstring>> wgettext_map;
|
||||
|
||||
|
||||
12
src/wutil.h
12
src/wutil.h
@@ -44,7 +44,9 @@ struct wcstring_list_ffi_t {
|
||||
|
||||
wcstring_list_ffi_t() = default;
|
||||
/* implicit */ wcstring_list_ffi_t(std::vector<wcstring> vals) : vals(std::move(vals)) {}
|
||||
~wcstring_list_ffi_t();
|
||||
|
||||
bool empty() const { return vals.empty(); }
|
||||
size_t size() const { return vals.size(); }
|
||||
const wcstring &at(size_t idx) const { return vals.at(idx); }
|
||||
void clear() { vals.clear(); }
|
||||
@@ -62,6 +64,16 @@ struct wcstring_list_ffi_t {
|
||||
static void check_test_data(wcstring_list_ffi_t data);
|
||||
};
|
||||
|
||||
/// Convert an iterable of strings to a list of wcharz_t.
|
||||
template <typename T>
|
||||
std::vector<wcharz_t> wcstring_list_to_ffi(const T &list) {
|
||||
std::vector<wcharz_t> result;
|
||||
for (const wcstring &str : list) {
|
||||
result.push_back(str.c_str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class autoclose_fd_t;
|
||||
|
||||
/// Wide character version of opendir(). Note that opendir() is guaranteed to set close-on-exec by
|
||||
|
||||
Reference in New Issue
Block a user