mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-25 20:33:08 +00:00
Merge branch 'futurefeatures'
This merges support for feature flags. Closes #4940
This commit is contained in:
commit
aba69ac6ae
32 changed files with 451 additions and 66 deletions
|
@ -5,6 +5,8 @@ This section is for changes merged to the `major` branch that are not also merge
|
|||
- The `IFS` variable is deprecated and will be removed in fish 4.0 (#4156).
|
||||
- The `function --on-process-exit` event will be removed in future (#4700). Use the `fish_exit` event instead.
|
||||
- `$_` is deprecated and will removed in the future (#813). Use `status current-command` in a subshell instead.
|
||||
- `^` as a redirection deprecated and will be removed in the future. (#4394). Use `2>` to redirect stderr. This is controlled by the `stderr-nocaret` feature flag.
|
||||
- `?` as a glob is deprecated and will be removed in the future. (#4520). This is controlled by the `qmark-noglob` feature flag.
|
||||
|
||||
## Notable non-backward compatible changes
|
||||
- `.` command no longer exists -- use `source` (#4294).
|
||||
|
@ -15,10 +17,9 @@ This section is for changes merged to the `major` branch that are not also merge
|
|||
- Successive commas in brace expansions are handled in less surprising manner (`{,,,}` expands to four empty strings rather than an empty string, a comma and an empty string again). (#3002, #4632).
|
||||
- `%` is no longer used for process and job expansion. `$fish_pid` and `$last_pid` have taken the place of `%self` and `%last` respectively. (#4230, #1202)
|
||||
- The new `math` builtin (see below) does not support logical expressions; `test` should be used instead (#4777).
|
||||
- The `?` wildcard has been removed (#4520).
|
||||
- The `^` caret redirection for stderr has been removed (#4394). To redirect stderr, `2>/some/path` may be used, or `2>|` as a pipe.
|
||||
|
||||
## Notable fixes and improvements
|
||||
- A new feature flags mechanism is added for staging deprecations and breaking changes. (#4940)
|
||||
- `wait` builtin is added for waiting on processes (#4498).
|
||||
- `read` has a new `--delimiter` option as a better alternative to the `IFS` variable (#4256).
|
||||
- `read` writes directly to stdout if called without arguments (#4407)
|
||||
|
|
|
@ -58,6 +58,7 @@ SET(FISH_SRCS
|
|||
src/postfork.cpp src/proc.cpp src/reader.cpp src/sanity.cpp src/screen.cpp
|
||||
src/signal.cpp src/tnode.cpp src/tokenizer.cpp src/utf8.cpp src/util.cpp
|
||||
src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp src/wutil.cpp
|
||||
src/future_feature_flags.cpp
|
||||
)
|
||||
|
||||
# Header files are just globbed.
|
||||
|
|
|
@ -118,7 +118,8 @@ FISH_OBJS := obj/autoload.o obj/builtin.o obj/builtin_bg.o obj/builtin_bind.o ob
|
|||
obj/parse_productions.o obj/parse_tree.o obj/parse_util.o obj/parser.o \
|
||||
obj/parser_keywords.o obj/path.o obj/postfork.o obj/proc.o obj/reader.o \
|
||||
obj/sanity.o obj/screen.o obj/signal.o obj/tinyexpr.o obj/tokenizer.o obj/tnode.o obj/utf8.o \
|
||||
obj/util.o obj/wcstringutil.o obj/wgetopt.o obj/wildcard.o obj/wutil.o
|
||||
obj/util.o obj/wcstringutil.o obj/wgetopt.o obj/wildcard.o obj/wutil.o \
|
||||
obj/future_feature_flags.o
|
||||
|
||||
FISH_INDENT_OBJS := obj/fish_indent.o obj/print_help.o $(FISH_OBJS)
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ Some characters can not be written directly on the command line. For these chara
|
|||
- '<code>\\$</code>' escapes the dollar character
|
||||
- '<code>\\\\</code>' escapes the backslash character
|
||||
- '<code>\\*</code>' escapes the star character
|
||||
- '<code>\\?</code>' escapes the question mark character
|
||||
- '<code>\\~</code>' escapes the tilde character
|
||||
- '<code>\\#</code>' escapes the hash character
|
||||
- '<code>\\(</code>' escapes the left parenthesis character
|
||||
|
@ -141,11 +142,11 @@ An example of a file redirection is `echo hello > output.txt`, which directs the
|
|||
|
||||
- To read standard input from a file, write `<SOURCE_FILE`
|
||||
- To write standard output to a file, write `>DESTINATION`
|
||||
- To write standard error to a file, write `^DESTINATION`
|
||||
- To write standard error to a file, write `2>DESTINATION`
|
||||
- To append standard output to a file, write `>>DESTINATION_FILE`
|
||||
- To append standard error to a file, write `^^DESTINATION_FILE`
|
||||
- To append standard error to a file, write `2>>DESTINATION_FILE`
|
||||
|
||||
- To not overwrite ("clobber") an existing file, write '>?DESTINATION' or '^?DESTINATION'
|
||||
- To not overwrite ("clobber") an existing file, write '>?DESTINATION' or '2>?DESTINATION'
|
||||
|
||||
`DESTINATION` can be one of the following:
|
||||
|
||||
|
@ -157,7 +158,7 @@ An example of a file redirection is `echo hello > output.txt`, which directs the
|
|||
|
||||
Example:
|
||||
|
||||
To redirect both standard output and standard error to the file 'all_output.txt', you can write `echo Hello > all_output.txt ^&1`.
|
||||
To redirect both standard output and standard error to the file 'all_output.txt', you can write `echo Hello > all_output.txt 2>&1`.
|
||||
|
||||
Any file descriptor can be redirected in an arbitrary way by prefixing the redirection with the file descriptor.
|
||||
|
||||
|
@ -165,7 +166,7 @@ Any file descriptor can be redirected in an arbitrary way by prefixing the redir
|
|||
- To redirect output of FD N, write `N>DESTINATION`
|
||||
- To append the output of FD N to a file, write `N>>DESTINATION_FILE`
|
||||
|
||||
Example: `echo Hello 2>output.stderr` and `echo Hello ^output.stderr` are equivalent, and write the standard error (file descriptor 2) of the target program to `output.stderr`.
|
||||
Example: `echo Hello 2>output.stderr` and `echo Hello 2>output.stderr` are equivalent, and write the standard error (file descriptor 2) of the target program to `output.stderr`.
|
||||
|
||||
\subsection piping Piping
|
||||
|
||||
|
@ -329,7 +330,7 @@ These are the general purpose tab completions that `fish` provides:
|
|||
|
||||
- Completion of usernames for tilde expansion.
|
||||
|
||||
- Completion of filenames, even on strings with wildcards such as '`*`' and '`**`'.
|
||||
- Completion of filenames, even on strings with wildcards such as '`*`', '`**`' and '`?`'.
|
||||
|
||||
`fish` provides a large number of program specific completions. Most of these completions are simple options like the `-l` option for `ls`, but some are more advanced. The latter include:
|
||||
|
||||
|
@ -417,7 +418,9 @@ When an argument for a program is given on the commandline, it undergoes the pro
|
|||
|
||||
\subsection expand-wildcard Wildcards
|
||||
|
||||
If a star (`*`) is present in the parameter, `fish` attempts to match the given parameter to any files in such a way that:
|
||||
If a star (`*`) or a question mark (`?`) is present in the parameter, `fish` attempts to match the given parameter to any files in such a way that:
|
||||
|
||||
- `?` can match any single character except '/'.
|
||||
|
||||
- `*` can match any string of characters not containing '/'. This includes matching an empty string.
|
||||
|
||||
|
@ -443,6 +446,8 @@ Examples:
|
|||
|
||||
- `a*` matches any files beginning with an 'a' in the current directory.
|
||||
|
||||
- `???` matches any file in the current directory whose name is exactly three characters long.
|
||||
|
||||
- `**` matches any files and directories in the current directory and all of its subdirectories.
|
||||
|
||||
Note that for most commands, if any wildcard fails to expand, the command is not executed, <a href='#variables-status'>`$status`</a> is set to nonzero, and a warning is printed. This behavior is consistent with setting `shopt -s failglob` in bash. There are exactly 3 exceptions, namely <a href="commands.html#set">`set`</a>, <a href="commands.html#count">`count`</a> and <a href="commands.html#for">`for`</a>. Their globs are permitted to expand to zero arguments, as with `shopt -s nullglob` in bash.
|
||||
|
@ -1242,6 +1247,29 @@ function on_exit --on-process $fish_pid
|
|||
end
|
||||
\endfish
|
||||
|
||||
\section featureflags Future feature flags
|
||||
|
||||
Feature flags are how fish stages changes that might break scripts. Breaking changes are introduced as opt-in, in a few releases they become opt-out, and eventually the old behavior is removed.
|
||||
|
||||
You can see the current list of features via `status features`:
|
||||
|
||||
\fish
|
||||
> status features
|
||||
stderr-nocaret on 3.0 ^ no longer redirects stderr
|
||||
qmark-noglob off 3.0 ? no longer globs
|
||||
\endfish
|
||||
|
||||
There are two breaking changes in fish 3.0: caret `^` no longer redirects stderr, and question mark `?` is no longer a glob. These changes are off by default. They can be enabled on a per session basis:
|
||||
|
||||
\fish
|
||||
> fish --features qmark-noglob,stderr-nocaret
|
||||
\endfish
|
||||
|
||||
or opted into globally for a user:
|
||||
|
||||
\fish
|
||||
> set -U fish_features stderr-nocaret qmark-noglob
|
||||
\endfish
|
||||
|
||||
\section other Other features
|
||||
|
||||
|
|
|
@ -127,7 +127,11 @@ function __fish_git_files
|
|||
# Be careful about the ordering here!
|
||||
#
|
||||
# HACK: To allow this to work both with and without '?' globs
|
||||
set -l dq '??'
|
||||
set -l dq '\\?\\?'
|
||||
if status test-feature qmark-noglob
|
||||
# ? is not a glob
|
||||
set dq '??'
|
||||
end
|
||||
switch "$stat"
|
||||
case DD AU UD UA DU AA UU
|
||||
# Unmerged
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "builtin_status.h"
|
||||
#include "common.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "future_feature_flags.h"
|
||||
#include "io.h"
|
||||
#include "parser.h"
|
||||
#include "proc.h"
|
||||
|
@ -31,6 +32,8 @@ enum status_cmd_t {
|
|||
STATUS_LINE_NUMBER,
|
||||
STATUS_SET_JOB_CONTROL,
|
||||
STATUS_STACK_TRACE,
|
||||
STATUS_FEATURES,
|
||||
STATUS_TEST_FEATURE,
|
||||
STATUS_UNDEF
|
||||
};
|
||||
|
||||
|
@ -40,6 +43,7 @@ const enum_map<status_cmd_t> status_enum_map[] = {
|
|||
{STATUS_FILENAME, L"current-filename"},
|
||||
{STATUS_FUNCTION, L"current-function"},
|
||||
{STATUS_LINE_NUMBER, L"current-line-number"},
|
||||
{STATUS_FEATURES, L"features"},
|
||||
{STATUS_FILENAME, L"filename"},
|
||||
{STATUS_FUNCTION, L"function"},
|
||||
{STATUS_IS_BLOCK, L"is-block"},
|
||||
|
@ -54,6 +58,7 @@ const enum_map<status_cmd_t> status_enum_map[] = {
|
|||
{STATUS_LINE_NUMBER, L"line-number"},
|
||||
{STATUS_STACK_TRACE, L"print-stack-trace"},
|
||||
{STATUS_STACK_TRACE, L"stack-trace"},
|
||||
{STATUS_TEST_FEATURE, L"test-feature"},
|
||||
{STATUS_UNDEF, NULL}};
|
||||
#define status_enum_map_len (sizeof status_enum_map / sizeof *status_enum_map)
|
||||
|
||||
|
@ -66,6 +71,9 @@ const enum_map<status_cmd_t> status_enum_map[] = {
|
|||
break; \
|
||||
}
|
||||
|
||||
/// Values that may be returned from the test-feature option to status.
|
||||
enum { TEST_FEATURE_ON, TEST_FEATURE_OFF, TEST_FEATURE_NOT_RECOGNIZED };
|
||||
|
||||
int job_control_str_to_mode(const wchar_t *mode, wchar_t *cmd, io_streams_t &streams) {
|
||||
if (wcscmp(mode, L"full") == 0) {
|
||||
return JOB_CONTROL_ALL;
|
||||
|
@ -82,6 +90,7 @@ struct status_cmd_opts_t {
|
|||
bool print_help = false;
|
||||
int level = 1;
|
||||
int new_job_control_mode = -1;
|
||||
const wchar_t *feature_name;
|
||||
status_cmd_t status_cmd = STATUS_UNDEF;
|
||||
};
|
||||
|
||||
|
@ -126,6 +135,15 @@ static bool set_status_cmd(wchar_t *const cmd, status_cmd_opts_t &opts, status_c
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Print the features and their values.
|
||||
static void print_features(io_streams_t &streams) {
|
||||
for (const auto &md : features_t::metadata) {
|
||||
int set = feature_test(md.flag);
|
||||
streams.out.append_format(L"%ls\t%s\t%ls\t%ls\n", md.name, set ? "on" : "off", md.groups,
|
||||
md.description);
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_cmd_opts(status_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
|
||||
int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) {
|
||||
wchar_t *cmd = argv[0];
|
||||
|
@ -307,6 +325,24 @@ int builtin_status(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
|||
job_control_mode = opts.new_job_control_mode;
|
||||
break;
|
||||
}
|
||||
case STATUS_FEATURES: {
|
||||
print_features(streams);
|
||||
break;
|
||||
}
|
||||
case STATUS_TEST_FEATURE: {
|
||||
if (args.size() != 1) {
|
||||
const wchar_t *subcmd_str = enum_to_str(opts.status_cmd, status_enum_map);
|
||||
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 1, args.size());
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
const auto *metadata = features_t::metadata_for(args.front().c_str());
|
||||
if (!metadata) {
|
||||
retval = TEST_FEATURE_NOT_RECOGNIZED;
|
||||
} else {
|
||||
retval = feature_test(metadata->flag) ? TEST_FEATURE_ON : TEST_FEATURE_OFF;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case STATUS_FILENAME: {
|
||||
CHECK_FOR_UNEXPECTED_STATUS_ARGS(opts.status_cmd)
|
||||
const wchar_t *fn = parser.current_filename();
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include "env.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "future_feature_flags.h"
|
||||
#include "proc.h"
|
||||
#include "wildcard.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
@ -928,9 +929,11 @@ static bool unescape_string_var(const wchar_t *in, wcstring *out) {
|
|||
static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring &out,
|
||||
escape_flags_t flags) {
|
||||
const wchar_t *in = orig_in;
|
||||
bool escape_all = static_cast<bool>(flags & ESCAPE_ALL);
|
||||
bool no_quoted = static_cast<bool>(flags & ESCAPE_NO_QUOTED);
|
||||
bool no_tilde = static_cast<bool>(flags & ESCAPE_NO_TILDE);
|
||||
const bool escape_all = static_cast<bool>(flags & ESCAPE_ALL);
|
||||
const bool no_quoted = static_cast<bool>(flags & ESCAPE_NO_QUOTED);
|
||||
const bool no_tilde = static_cast<bool>(flags & ESCAPE_NO_TILDE);
|
||||
const bool no_caret = feature_test(features_t::stderr_nocaret);
|
||||
const bool no_qmark = feature_test(features_t::qmark_noglob);
|
||||
|
||||
int need_escape = 0;
|
||||
int need_complex_escape = 0;
|
||||
|
@ -995,6 +998,11 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring
|
|||
out += *in;
|
||||
break;
|
||||
}
|
||||
case ANY_CHAR: {
|
||||
// See #1614
|
||||
out += L'?';
|
||||
break;
|
||||
}
|
||||
case ANY_STRING: {
|
||||
out += L'*';
|
||||
break;
|
||||
|
@ -1003,10 +1011,12 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring
|
|||
out += L"**";
|
||||
break;
|
||||
}
|
||||
|
||||
case L'&':
|
||||
case L'$':
|
||||
case L' ':
|
||||
case L'#':
|
||||
case L'^':
|
||||
case L'<':
|
||||
case L'>':
|
||||
case L'(':
|
||||
|
@ -1015,12 +1025,14 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring
|
|||
case L']':
|
||||
case L'{':
|
||||
case L'}':
|
||||
case L'?':
|
||||
case L'*':
|
||||
case L'|':
|
||||
case L';':
|
||||
case L'"':
|
||||
case L'~': {
|
||||
if (!no_tilde || c != L'~') {
|
||||
bool char_is_normal = (c == L'~' && no_tilde) || (c == L'^' && no_caret) || (c == L'?' && no_qmark);
|
||||
if (!char_is_normal) {
|
||||
need_escape = 1;
|
||||
if (escape_all) out += L'\\';
|
||||
}
|
||||
|
@ -1348,6 +1360,12 @@ static bool unescape_string_internal(const wchar_t *const input, const size_t in
|
|||
}
|
||||
break;
|
||||
}
|
||||
case L'?': {
|
||||
if (unescape_special && !feature_test(features_t::qmark_noglob)) {
|
||||
to_append_or_none = ANY_CHAR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case L'$': {
|
||||
if (unescape_special) {
|
||||
to_append_or_none = VARIABLE_EXPAND;
|
||||
|
|
|
@ -828,6 +828,10 @@ static void remove_internal_separator(wcstring *str, bool conv) {
|
|||
if (conv) {
|
||||
for (size_t idx = 0; idx < str->size(); idx++) {
|
||||
switch (str->at(idx)) {
|
||||
case ANY_CHAR: {
|
||||
str->at(idx) = L'?';
|
||||
break;
|
||||
}
|
||||
case ANY_STRING:
|
||||
case ANY_STRING_RECURSIVE: {
|
||||
str->at(idx) = L'*';
|
||||
|
|
19
src/fish.cpp
19
src/fish.cpp
|
@ -44,6 +44,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "fish_version.h"
|
||||
#include "function.h"
|
||||
#include "future_feature_flags.h"
|
||||
#include "history.h"
|
||||
#include "io.h"
|
||||
#include "parser.h"
|
||||
|
@ -61,6 +62,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|||
// container to hold the options specified within the command line
|
||||
class fish_cmd_opts_t {
|
||||
public:
|
||||
// Future feature flags values string
|
||||
wcstring features;
|
||||
// Commands to be executed in place of interactive shell.
|
||||
std::vector<std::string> batch_cmds;
|
||||
// Commands to execute after the shell's config has been read.
|
||||
|
@ -238,9 +241,10 @@ int run_command_list(std::vector<std::string> *cmds, const io_chain_t &io) {
|
|||
|
||||
/// Parse the argument list, return the index of the first non-flag arguments.
|
||||
static int fish_parse_opt(int argc, char **argv, fish_cmd_opts_t *opts) {
|
||||
static const char *short_opts = "+hilnvc:C:p:d:D:";
|
||||
static const char *short_opts = "+hilnvc:C:p:d:f:D:";
|
||||
static const struct option long_opts[] = {{"command", required_argument, NULL, 'c'},
|
||||
{"init-command", required_argument, NULL, 'C'},
|
||||
{"features", required_argument, NULL, 'f'},
|
||||
{"debug-level", required_argument, NULL, 'd'},
|
||||
{"debug-stack-frames", required_argument, NULL, 'D'},
|
||||
{"interactive", no_argument, NULL, 'i'},
|
||||
|
@ -277,6 +281,10 @@ static int fish_parse_opt(int argc, char **argv, fish_cmd_opts_t *opts) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'f': {
|
||||
opts->features = str2wcstring(optarg);
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
opts->batch_cmds.push_back("__fish_print_help fish");
|
||||
break;
|
||||
|
@ -375,6 +383,15 @@ int main(int argc, char **argv) {
|
|||
|
||||
const struct config_paths_t paths = determine_config_directory_paths(argv[0]);
|
||||
env_init(&paths);
|
||||
// Set features early in case other initialization depends on them.
|
||||
// Start with the ones set in the environment, then those set on the command line (so the
|
||||
// command line takes precedence).
|
||||
if (auto features_var = env_get(L"fish_features")) {
|
||||
for (const wcstring &s : features_var->as_list()) {
|
||||
mutable_fish_features().set_from_string(s);
|
||||
}
|
||||
}
|
||||
mutable_fish_features().set_from_string(opts.features);
|
||||
proc_init();
|
||||
builtin_init();
|
||||
misc_init();
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "function.h"
|
||||
#include "future_feature_flags.h"
|
||||
#include "highlight.h"
|
||||
#include "history.h"
|
||||
#include "input.h"
|
||||
|
@ -534,7 +535,7 @@ static void test_tokenizer() {
|
|||
|
||||
const wchar_t *str =
|
||||
L"string <redirection 2>&1 'nested \"quoted\" '(string containing subshells "
|
||||
L"){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ 2> 2>^is_a_redirect "
|
||||
L"){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ ^ ^^is_a_redirect "
|
||||
L"&&& ||| "
|
||||
L"&& || & |"
|
||||
L"Compress_Newlines\n \n\t\n \nInto_Just_One";
|
||||
|
@ -608,6 +609,8 @@ static void test_tokenizer() {
|
|||
// Test redirection_type_for_string.
|
||||
if (redirection_type_for_string(L"<") != redirection_type_t::input)
|
||||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
if (redirection_type_for_string(L"^") != redirection_type_t::overwrite)
|
||||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
if (redirection_type_for_string(L">") != redirection_type_t::overwrite)
|
||||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
if (redirection_type_for_string(L"2>") != redirection_type_t::overwrite)
|
||||
|
@ -624,6 +627,16 @@ static void test_tokenizer() {
|
|||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
if (redirection_type_for_string(L"2>|"))
|
||||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
|
||||
// Test ^ with our feature flag on and off.
|
||||
auto saved_flags = fish_features();
|
||||
mutable_fish_features().set(features_t::stderr_nocaret, false);
|
||||
if (redirection_type_for_string(L"^") != redirection_type_t::overwrite)
|
||||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
mutable_fish_features().set(features_t::stderr_nocaret, true);
|
||||
if (redirection_type_for_string(L"^") != none())
|
||||
err(L"redirection_type_for_string failed on line %ld", (long)__LINE__);
|
||||
mutable_fish_features() = saved_flags;
|
||||
}
|
||||
|
||||
// Little function that runs in a background thread, bouncing to the main.
|
||||
|
@ -1352,6 +1365,34 @@ static void test_utf8() {
|
|||
#endif
|
||||
}
|
||||
|
||||
static void test_feature_flags() {
|
||||
say(L"Testing future feature flags");
|
||||
using ft = features_t;
|
||||
ft f;
|
||||
do_test(!f.test(ft::stderr_nocaret));
|
||||
f.set(ft::stderr_nocaret, true);
|
||||
do_test(f.test(ft::stderr_nocaret));
|
||||
f.set(ft::stderr_nocaret, false);
|
||||
do_test(!f.test(ft::stderr_nocaret));
|
||||
|
||||
f.set_from_string(L"stderr-nocaret,nonsense");
|
||||
do_test(f.test(ft::stderr_nocaret));
|
||||
f.set_from_string(L"stderr-nocaret,no-stderr-nocaret,nonsense");
|
||||
do_test(!f.test(ft::stderr_nocaret));
|
||||
|
||||
// Ensure every metadata is represented once.
|
||||
size_t counts[ft::flag_count] = {};
|
||||
for (const auto &md : ft::metadata) {
|
||||
counts[md.flag]++;
|
||||
}
|
||||
for (size_t c : counts) {
|
||||
do_test(c == 1);
|
||||
}
|
||||
do_test(ft::metadata[ft::stderr_nocaret].name == wcstring(L"stderr-nocaret"));
|
||||
do_test(ft::metadata_for(L"stderr-nocaret") == &ft::metadata[ft::stderr_nocaret]);
|
||||
do_test(ft::metadata_for(L"not-a-flag") == nullptr);
|
||||
}
|
||||
|
||||
static void test_escape_sequences() {
|
||||
say(L"Testing escape_sequences");
|
||||
if (escape_code_length(L"") != 0) err(L"test_escape_sequences failed on line %d\n", __LINE__);
|
||||
|
@ -4052,7 +4093,7 @@ static void test_wcstring_tok() {
|
|||
}
|
||||
|
||||
int builtin_string(parser_t &parser, io_streams_t &streams, wchar_t **argv);
|
||||
static void run_one_string_test(const wchar_t **argv, int expected_rc,
|
||||
static void run_one_string_test(const wchar_t *const *argv, int expected_rc,
|
||||
const wchar_t *expected_out) {
|
||||
parser_t parser;
|
||||
io_streams_t streams(0);
|
||||
|
@ -4074,7 +4115,7 @@ static void run_one_string_test(const wchar_t **argv, int expected_rc,
|
|||
}
|
||||
|
||||
static void test_string() {
|
||||
static struct string_test {
|
||||
const struct string_test {
|
||||
const wchar_t *argv[15];
|
||||
int expected_rc;
|
||||
const wchar_t *expected_out;
|
||||
|
@ -4118,16 +4159,15 @@ static void test_string() {
|
|||
{{L"string", L"match", 0}, STATUS_INVALID_ARGS, L""},
|
||||
{{L"string", L"match", L"", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"", L"", 0}, STATUS_CMD_OK, L"\n"},
|
||||
{{L"string", L"match", L"?", L"a", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"?", L"a", 0}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"match", L"*", L"", 0}, STATUS_CMD_OK, L"\n"},
|
||||
{{L"string", L"match", L"**", L"", 0}, STATUS_CMD_OK, L"\n"},
|
||||
{{L"string", L"match", L"*", L"xyzzy", 0}, STATUS_CMD_OK, L"xyzzy\n"},
|
||||
{{L"string", L"match", L"**", L"plugh", 0}, STATUS_CMD_OK, L"plugh\n"},
|
||||
{{L"string", L"match", L"a*b", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"},
|
||||
{{L"string", L"match", L"a??b", L"axxb", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"a??b", L"a??b", 0}, STATUS_CMD_OK, L"a??b\n"},
|
||||
{{L"string", L"match", L"-i", L"a??B", L"axxb", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"-i", L"a??b", L"Axxb", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"a??b", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"},
|
||||
{{L"string", L"match", L"-i", L"a??B", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"},
|
||||
{{L"string", L"match", L"-i", L"a??b", L"Axxb", 0}, STATUS_CMD_OK, L"Axxb\n"},
|
||||
{{L"string", L"match", L"a*", L"axxb", 0}, STATUS_CMD_OK, L"axxb\n"},
|
||||
{{L"string", L"match", L"*a", L"xxa", 0}, STATUS_CMD_OK, L"xxa\n"},
|
||||
{{L"string", L"match", L"*a*", L"axa", 0}, STATUS_CMD_OK, L"axa\n"},
|
||||
|
@ -4136,14 +4176,9 @@ static void test_string() {
|
|||
{{L"string", L"match", L"*a", L"a", 0}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"match", L"a*", L"a", 0}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"match", L"a*b*c", L"axxbyyc", 0}, STATUS_CMD_OK, L"axxbyyc\n"},
|
||||
{{L"string", L"match", L"a*b?c", L"axxb?c", 0}, STATUS_CMD_OK, L"axxb?c\n"},
|
||||
{{L"string", L"match", L"*?", L"a", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"*?", L"ab", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"?*", L"a", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"?*", L"ab", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"\\*", L"*", 0}, STATUS_CMD_OK, L"*\n"},
|
||||
{{L"string", L"match", L"a*\\", L"abc\\", 0}, STATUS_CMD_OK, L"abc\\\n"},
|
||||
{{L"string", L"match", L"a*\\?", L"abc?", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"a*\\?", L"abc?", 0}, STATUS_CMD_OK, L"abc?\n"},
|
||||
|
||||
{{L"string", L"match", L"?", L"", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"?", L"ab", 0}, STATUS_CMD_ERROR, L""},
|
||||
|
@ -4362,15 +4397,36 @@ static void test_string() {
|
|||
{{L"string", L"trim", L"-c", L"\\/", L"/a\\"}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"trim", L"-c", L"\\/", L"a/"}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"trim", L"-c", L"\\/", L"\\a/"}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"trim", L"-c", L"", L".a."}, STATUS_CMD_ERROR, L".a.\n"},
|
||||
|
||||
{{NULL}, STATUS_CMD_ERROR, NULL}};
|
||||
|
||||
struct string_test *t = string_tests;
|
||||
while (t->argv[0]) {
|
||||
run_one_string_test(t->argv, t->expected_rc, t->expected_out);
|
||||
t++;
|
||||
{{L"string", L"trim", L"-c", L"", L".a."}, STATUS_CMD_ERROR, L".a.\n"}};
|
||||
for (const auto &t : string_tests) {
|
||||
run_one_string_test(t.argv, t.expected_rc, t.expected_out);
|
||||
}
|
||||
|
||||
const auto saved_flags = fish_features();
|
||||
const struct string_test qmark_noglob_tests[] = {
|
||||
{{L"string", L"match", L"a*b?c", L"axxb?c", 0}, STATUS_CMD_OK, L"axxb?c\n"},
|
||||
{{L"string", L"match", L"*?", L"a", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"*?", L"ab", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"?*", L"a", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"?*", L"ab", 0}, STATUS_CMD_ERROR, L""},
|
||||
{{L"string", L"match", L"a*\\?", L"abc?", 0}, STATUS_CMD_ERROR, L""}};
|
||||
mutable_fish_features().set(features_t::qmark_noglob, true);
|
||||
for (const auto &t : qmark_noglob_tests) {
|
||||
run_one_string_test(t.argv, t.expected_rc, t.expected_out);
|
||||
}
|
||||
|
||||
const struct string_test qmark_glob_tests[] = {
|
||||
{{L"string", L"match", L"a*b?c", L"axxbyc", 0}, STATUS_CMD_OK, L"axxbyc\n"},
|
||||
{{L"string", L"match", L"*?", L"a", 0}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"match", L"*?", L"ab", 0}, STATUS_CMD_OK, L"ab\n"},
|
||||
{{L"string", L"match", L"?*", L"a", 0}, STATUS_CMD_OK, L"a\n"},
|
||||
{{L"string", L"match", L"?*", L"ab", 0}, STATUS_CMD_OK, L"ab\n"},
|
||||
{{L"string", L"match", L"a*\\?", L"abc?", 0}, STATUS_CMD_OK, L"abc?\n"}};
|
||||
mutable_fish_features().set(features_t::qmark_noglob, false);
|
||||
for (const auto &t : qmark_glob_tests) {
|
||||
run_one_string_test(t.argv, t.expected_rc, t.expected_out);
|
||||
}
|
||||
mutable_fish_features() = saved_flags;
|
||||
}
|
||||
|
||||
/// Helper for test_timezone_env_vars().
|
||||
|
@ -4427,15 +4483,11 @@ static void test_illegal_command_exit_code() {
|
|||
};
|
||||
|
||||
const command_result_tuple_t tests[] = {
|
||||
{L"echo -n", STATUS_CMD_OK},
|
||||
{L"pwd", STATUS_CMD_OK},
|
||||
// a `)` without a matching `(` is now a tokenizer error, and cannot be executed even as an
|
||||
// illegal command
|
||||
{L"echo -n", STATUS_CMD_OK}, {L"pwd", STATUS_CMD_OK},
|
||||
// a `)` without a matching `(` is now a tokenizer error, and cannot be executed even as an illegal command
|
||||
// {L")", STATUS_ILLEGAL_CMD}, {L") ", STATUS_ILLEGAL_CMD}, {L") ", STATUS_ILLEGAL_CMD}
|
||||
{L"*", STATUS_ILLEGAL_CMD},
|
||||
{L"**", STATUS_ILLEGAL_CMD},
|
||||
{L"?", STATUS_CMD_UNKNOWN},
|
||||
{L"abc?def", STATUS_CMD_UNKNOWN},
|
||||
{L"*", STATUS_ILLEGAL_CMD}, {L"**", STATUS_ILLEGAL_CMD},
|
||||
{L"?", STATUS_ILLEGAL_CMD}, {L"abc?def", STATUS_ILLEGAL_CMD},
|
||||
};
|
||||
|
||||
int res = 0;
|
||||
|
@ -4611,6 +4663,7 @@ int main(int argc, char **argv) {
|
|||
if (should_test_function("cancellation")) test_cancellation();
|
||||
if (should_test_function("indents")) test_indents();
|
||||
if (should_test_function("utf8")) test_utf8();
|
||||
if (should_test_function("feature_flags")) test_feature_flags();
|
||||
if (should_test_function("escape_sequences")) test_escape_sequences();
|
||||
if (should_test_function("lru")) test_lru();
|
||||
if (should_test_function("expand")) test_expand();
|
||||
|
|
58
src/future_feature_flags.cpp
Normal file
58
src/future_feature_flags.cpp
Normal file
|
@ -0,0 +1,58 @@
|
|||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include <wchar.h>
|
||||
#include "future_feature_flags.h"
|
||||
|
||||
/// The set of features applying to this instance.
|
||||
static features_t global_features;
|
||||
|
||||
const features_t &fish_features() { return global_features; }
|
||||
|
||||
features_t &mutable_fish_features() { return global_features; }
|
||||
|
||||
const features_t::metadata_t features_t::metadata[features_t::flag_count] = {
|
||||
{stderr_nocaret, L"stderr-nocaret", L"3.0", L"^ no longer redirects stderr"},
|
||||
{qmark_noglob, L"qmark-noglob", L"3.0", L"? no longer globs"},
|
||||
};
|
||||
|
||||
const struct features_t::metadata_t *features_t::metadata_for(const wchar_t *name) {
|
||||
assert(name && "null flag name");
|
||||
for (const auto &md : metadata) {
|
||||
if (!wcscmp(name, md.name)) return &md;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void features_t::set_from_string(const wcstring &str) {
|
||||
wcstring_list_t entries = split_string(str, L',');
|
||||
const wchar_t *whitespace = L"\t\n\v\f\r ";
|
||||
for (wcstring entry : entries) {
|
||||
if (entry.empty()) continue;
|
||||
|
||||
// Trim leading and trailing whitespace
|
||||
entry.erase(0, entry.find_first_not_of(whitespace));
|
||||
entry.erase(entry.find_last_not_of(whitespace) + 1);
|
||||
|
||||
const wchar_t *name = entry.c_str();
|
||||
bool value = true;
|
||||
// A "no-" prefix inverts the sense.
|
||||
if (string_prefixes_string(L"no-", name)) {
|
||||
value = false;
|
||||
name += 3; // wcslen(L"no-")
|
||||
}
|
||||
// Look for a feature with this name. If we don't find it, assume it's a group name and set
|
||||
// all features whose group contain it. Do nothing even if the string is unrecognized; this
|
||||
// is to allow uniform invocations of fish (e.g. disable a feature that is only present in
|
||||
// future versions).
|
||||
// The special name 'all' may be used for those who like to live on the edge.
|
||||
if (const metadata_t *md = metadata_for(name)) {
|
||||
this->set(md->flag, value);
|
||||
} else {
|
||||
for (const metadata_t &md : metadata) {
|
||||
if (wcsstr(md.groups, name) || !wcscmp(name, L"all")) {
|
||||
this->set(md.flag, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
src/future_feature_flags.h
Normal file
78
src/future_feature_flags.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Flags to enable upcoming features
|
||||
#ifndef FISH_FUTURE_FEATURE_FLAGS_H
|
||||
#define FISH_FUTURE_FEATURE_FLAGS_H
|
||||
|
||||
#include <assert.h>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
class features_t {
|
||||
public:
|
||||
/// The list of flags.
|
||||
enum flag_t {
|
||||
/// Whether ^ is supported for stderr redirection.
|
||||
stderr_nocaret,
|
||||
|
||||
/// Whether ? is supported as a glob.
|
||||
qmark_noglob,
|
||||
|
||||
/// The number of flags.
|
||||
flag_count
|
||||
};
|
||||
|
||||
/// Return whether a flag is set.
|
||||
bool test(flag_t f) const {
|
||||
assert(f >= 0 && f < flag_count && "Invalid flag");
|
||||
return values[f];
|
||||
}
|
||||
|
||||
/// Set a flag.
|
||||
void set(flag_t f, bool value) {
|
||||
assert(f >= 0 && f < flag_count && "Invalid flag");
|
||||
values[f] = value;
|
||||
}
|
||||
|
||||
/// Parses a comma-separated feature-flag string, updating ourselves with the values.
|
||||
/// Feature names or group names may be prefixed with "no-" to disable them.
|
||||
/// The special group name "all" may be used for those who like to live on the edge.
|
||||
/// Unknown features are silently ignored.
|
||||
void set_from_string(const wcstring &str);
|
||||
|
||||
/// Metadata about feature flags.
|
||||
struct metadata_t {
|
||||
/// The flag itself.
|
||||
features_t::flag_t flag;
|
||||
|
||||
/// User-presentable short name of the feature flag.
|
||||
const wchar_t *name;
|
||||
|
||||
/// Comma-separated list of feature groups.
|
||||
const wchar_t *groups;
|
||||
|
||||
/// User-presentable description of the feature flag.
|
||||
const wchar_t *description;
|
||||
};
|
||||
|
||||
/// The metadata, indexed by flag.
|
||||
static const metadata_t metadata[flag_count];
|
||||
|
||||
/// Return the metadata for a particular name, or nullptr if not found.
|
||||
static const struct metadata_t *metadata_for(const wchar_t *name);
|
||||
|
||||
private:
|
||||
/// Values for the flags.
|
||||
bool values[flag_count] = {};
|
||||
};
|
||||
|
||||
/// Return the global set of features for fish. This is const to prevent accidental mutation.
|
||||
const features_t &fish_features();
|
||||
|
||||
/// Perform a feature test on the global set of features.
|
||||
inline bool feature_test(features_t::flag_t f) { return fish_features().test(f); }
|
||||
|
||||
/// Return the global set of features for fish, but mutable. In general fish features should be set
|
||||
/// at startup only.
|
||||
features_t &mutable_fish_features();
|
||||
|
||||
#endif
|
|
@ -23,6 +23,7 @@
|
|||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "function.h"
|
||||
#include "future_feature_flags.h"
|
||||
#include "highlight.h"
|
||||
#include "history.h"
|
||||
#include "output.h"
|
||||
|
@ -125,6 +126,7 @@ bool is_potential_path(const wcstring &potential_path_fragment, const wcstring_l
|
|||
case BRACE_BEGIN:
|
||||
case BRACE_END:
|
||||
case BRACE_SEP:
|
||||
case ANY_CHAR:
|
||||
case ANY_STRING:
|
||||
case ANY_STRING_RECURSIVE: {
|
||||
has_magic = 1;
|
||||
|
@ -547,6 +549,12 @@ static void color_argument_internal(const wcstring &buffstr,
|
|||
in_pos -= 1;
|
||||
break;
|
||||
}
|
||||
case L'?': {
|
||||
if (!feature_test(features_t::qmark_noglob)) {
|
||||
colors[in_pos] = highlight_spec_operator;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case L'*':
|
||||
case L'(':
|
||||
case L')': {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include "common.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "future_feature_flags.h"
|
||||
#include "parse_constants.h"
|
||||
#include "parse_util.h"
|
||||
#include "tnode.h"
|
||||
|
@ -418,14 +419,20 @@ void parse_util_token_extent(const wchar_t *buff, size_t cursor_pos, const wchar
|
|||
wcstring parse_util_unescape_wildcards(const wcstring &str) {
|
||||
wcstring result;
|
||||
result.reserve(str.size());
|
||||
bool unesc_qmark = !feature_test(features_t::qmark_noglob);
|
||||
|
||||
const wchar_t *const cs = str.c_str();
|
||||
for (size_t i = 0; cs[i] != L'\0'; i++) {
|
||||
if (cs[i] == L'*') {
|
||||
result.push_back(ANY_STRING);
|
||||
} else if (cs[i] == L'?' && unesc_qmark) {
|
||||
result.push_back(ANY_CHAR);
|
||||
} else if (cs[i] == L'\\' && cs[i + 1] == L'*') {
|
||||
result.push_back(cs[i + 1]);
|
||||
i += 1;
|
||||
} else if (cs[i] == L'\\' && cs[i + 1] == L'?' && unesc_qmark) {
|
||||
result.push_back(cs[i + 1]);
|
||||
i += 1;
|
||||
} else if (cs[i] == L'\\' && cs[i + 1] == L'\\') {
|
||||
// Not a wildcard, but ensure the next iteration doesn't see this escaped backslash.
|
||||
result.append(L"\\\\");
|
||||
|
@ -890,7 +897,9 @@ void parse_util_expand_variable_error(const wcstring &token, size_t global_token
|
|||
default: {
|
||||
wchar_t token_stop_char = char_after_dollar;
|
||||
// Unescape (see issue #50).
|
||||
if (token_stop_char == ANY_STRING || token_stop_char == ANY_STRING_RECURSIVE)
|
||||
if (token_stop_char == ANY_CHAR)
|
||||
token_stop_char = L'?';
|
||||
else if (token_stop_char == ANY_STRING || token_stop_char == ANY_STRING_RECURSIVE)
|
||||
token_stop_char = L'*';
|
||||
|
||||
// Determine which error message to use. The format string may not consume all the
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include "common.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "future_feature_flags.h"
|
||||
#include "tokenizer.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
|
@ -34,6 +35,9 @@ const wchar_t *tokenizer_error::Message() const {
|
|||
return _(_message);
|
||||
}
|
||||
|
||||
// Whether carets redirect stderr.
|
||||
static bool caret_redirs() { return !feature_test(features_t::stderr_nocaret); }
|
||||
|
||||
/// Return an error token and mark that we no longer have a next token.
|
||||
tok_t tokenizer_t::call_error(tokenizer_error *error_type, const wchar_t *token_start,
|
||||
const wchar_t *error_loc) {
|
||||
|
@ -70,8 +74,10 @@ bool tokenizer_t::next(struct tok_t *result) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Tests if this character can be a part of a string.
|
||||
static bool tok_is_string_character(wchar_t c) {
|
||||
/// Tests if this character can be a part of a string. The redirect ^ is allowed unless it's the
|
||||
/// first character. Hash (#) starts a comment if it's the first character in a token; otherwise it
|
||||
/// is considered a string character. See issue #953.
|
||||
static bool tok_is_string_character(wchar_t c, bool is_first) {
|
||||
switch (c) {
|
||||
case L'\0':
|
||||
case L' ':
|
||||
|
@ -82,9 +88,15 @@ static bool tok_is_string_character(wchar_t c) {
|
|||
case L'\r':
|
||||
case L'<':
|
||||
case L'>':
|
||||
case L'&':
|
||||
case L'&': {
|
||||
// Unconditional separators.
|
||||
return false;
|
||||
default: return true;
|
||||
}
|
||||
case L'^': {
|
||||
// Conditional separator.
|
||||
return !caret_redirs() || !is_first;
|
||||
}
|
||||
default: { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,6 +121,7 @@ tok_t tokenizer_t::read_string() {
|
|||
std::vector<char> expecting;
|
||||
int slice_offset = 0;
|
||||
const wchar_t *const buff_start = this->buff;
|
||||
bool is_first = true;
|
||||
|
||||
while (true) {
|
||||
wchar_t c = *this->buff;
|
||||
|
@ -209,8 +222,7 @@ tok_t tokenizer_t::read_string() {
|
|||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (mode == tok_mode::regular_text && !tok_is_string_character(c)) {
|
||||
} else if (mode == tok_mode::regular_text && !tok_is_string_character(c, is_first)) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -224,6 +236,7 @@ tok_t tokenizer_t::read_string() {
|
|||
#endif
|
||||
|
||||
this->buff++;
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
if ((!this->accept_unfinished) && (mode != tok_mode::regular_text)) {
|
||||
|
@ -282,7 +295,7 @@ static maybe_t<parsed_redir_or_pipe_t> read_redirection_or_fd_pipe(const wchar_t
|
|||
size_t idx = 0;
|
||||
|
||||
// Determine the fd. This may be specified as a prefix like '2>...' or it may be implicit like
|
||||
// '>'. Try parsing out a number; if we did not get any digits then infer it from the
|
||||
// '>' or '^'. Try parsing out a number; if we did not get any digits then infer it from the
|
||||
// first character. Watch out for overflow.
|
||||
long long big_fd = 0;
|
||||
for (; iswdigit(buff[idx]); idx++) {
|
||||
|
@ -303,6 +316,14 @@ static maybe_t<parsed_redir_or_pipe_t> read_redirection_or_fd_pipe(const wchar_t
|
|||
result.fd = STDIN_FILENO;
|
||||
break;
|
||||
}
|
||||
case L'^': {
|
||||
if (caret_redirs()) {
|
||||
result.fd = STDERR_FILENO;
|
||||
} else {
|
||||
errored = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
errored = true;
|
||||
break;
|
||||
|
@ -311,11 +332,12 @@ static maybe_t<parsed_redir_or_pipe_t> read_redirection_or_fd_pipe(const wchar_t
|
|||
}
|
||||
|
||||
// Either way we should have ended on the redirection character itself like '>'.
|
||||
// Don't allow an fd with a caret redirection - see #1873
|
||||
wchar_t redirect_char = buff[idx++]; // note increment of idx
|
||||
if (redirect_char == L'>') {
|
||||
if (redirect_char == L'>' || (redirect_char == L'^' && idx == 1 && caret_redirs())) {
|
||||
result.redirection_mode = redirection_type_t::overwrite;
|
||||
if (buff[idx] == redirect_char) {
|
||||
// Doubled up like >>. That means append.
|
||||
// Doubled up like ^^ or >>. That means append.
|
||||
result.redirection_mode = redirection_type_t::append;
|
||||
idx++;
|
||||
}
|
||||
|
@ -507,7 +529,7 @@ maybe_t<tok_t> tokenizer_t::tok_next() {
|
|||
// Maybe a redirection like '2>&1', maybe a pipe like 2>|, maybe just a string.
|
||||
const wchar_t *error_location = this->buff;
|
||||
maybe_t<parsed_redir_or_pipe_t> redir_or_pipe;
|
||||
if (iswdigit(*this->buff)) {
|
||||
if (iswdigit(*this->buff) || (*this->buff == L'^' && caret_redirs())) {
|
||||
redir_or_pipe = read_redirection_or_fd_pipe(this->buff);
|
||||
}
|
||||
|
||||
|
@ -599,7 +621,9 @@ bool move_word_state_machine_t::consume_char_punctuation(wchar_t c) {
|
|||
}
|
||||
|
||||
bool move_word_state_machine_t::is_path_component_character(wchar_t c) {
|
||||
return tok_is_string_character(c) && !wcschr(L"/={,}'\"", c);
|
||||
// Always treat separators as first. All this does is ensure that we treat ^ as a string
|
||||
// character instead of as stderr redirection, which I hypothesize is usually what is desired.
|
||||
return tok_is_string_character(c, true) && !wcschr(L"/={,}'\"", c);
|
||||
}
|
||||
|
||||
bool move_word_state_machine_t::consume_char_path_components(wchar_t c) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "complete.h"
|
||||
#include "expand.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "future_feature_flags.h"
|
||||
#include "reader.h"
|
||||
#include "wildcard.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
@ -51,7 +52,7 @@
|
|||
/// Finds an internal (ANY_STRING, etc.) style wildcard, or wcstring::npos.
|
||||
static size_t wildcard_find(const wchar_t *wc) {
|
||||
for (size_t i = 0; wc[i] != L'\0'; i++) {
|
||||
if (wc[i] == ANY_STRING || wc[i] == ANY_STRING_RECURSIVE) {
|
||||
if (wc[i] == ANY_CHAR || wc[i] == ANY_STRING || wc[i] == ANY_STRING_RECURSIVE) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -61,15 +62,17 @@ static size_t wildcard_find(const wchar_t *wc) {
|
|||
/// Implementation of wildcard_has. Needs to take the length to handle embedded nulls (issue #1631).
|
||||
static bool wildcard_has_impl(const wchar_t *str, size_t len, bool internal) {
|
||||
assert(str != NULL);
|
||||
bool qmark_is_wild = !feature_test(features_t::qmark_noglob);
|
||||
const wchar_t *end = str + len;
|
||||
if (internal) {
|
||||
for (; str < end; str++) {
|
||||
if (*str == ANY_STRING || *str == ANY_STRING_RECURSIVE) return true;
|
||||
if ((*str == ANY_CHAR) || (*str == ANY_STRING) || (*str == ANY_STRING_RECURSIVE))
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
wchar_t prev = 0;
|
||||
for (; str < end; str++) {
|
||||
if (*str == L'*' && prev != L'\\') return true;
|
||||
if (((*str == L'*') || (*str == L'?' && qmark_is_wild)) && (prev != L'\\')) return true;
|
||||
prev = *str;
|
||||
}
|
||||
}
|
||||
|
@ -127,6 +130,13 @@ static enum fuzzy_match_type_t wildcard_match_internal(const wchar_t *str, const
|
|||
restart_is_out_of_str = (*str_x == 0);
|
||||
wc_x++;
|
||||
continue;
|
||||
} else if (*wc_x == ANY_CHAR && *str_x != 0) {
|
||||
if (is_first && *str_x == L'.') {
|
||||
return fuzzy_match_none;
|
||||
}
|
||||
wc_x++;
|
||||
str_x++;
|
||||
continue;
|
||||
} else if (*str_x != 0 && *str_x == *wc_x) { // ordinary character
|
||||
wc_x++;
|
||||
str_x++;
|
||||
|
@ -204,7 +214,7 @@ static bool wildcard_complete_internal(const wchar_t *str, const wchar_t *wc,
|
|||
return false;
|
||||
}
|
||||
|
||||
// Locate the next wildcard character position, e.g. ANY_STRING.
|
||||
// Locate the next wildcard character position, e.g. ANY_CHAR or ANY_STRING.
|
||||
const size_t next_wc_char_pos = wildcard_find(wc);
|
||||
|
||||
// Maybe we have no more wildcards at all. This includes the empty string.
|
||||
|
@ -257,6 +267,12 @@ static bool wildcard_complete_internal(const wchar_t *str, const wchar_t *wc,
|
|||
// Our first character is a wildcard.
|
||||
assert(next_wc_char_pos == 0);
|
||||
switch (wc[0]) {
|
||||
case ANY_CHAR: {
|
||||
if (str[0] == L'\0') {
|
||||
return false;
|
||||
}
|
||||
return wildcard_complete_internal(str + 1, wc + 1, params, flags, out);
|
||||
}
|
||||
case ANY_STRING: {
|
||||
// Hackish. If this is the last character of the wildcard, then just complete with
|
||||
// the empty string. This fixes cases like "f*<tab>" -> "f*o".
|
||||
|
@ -779,7 +795,7 @@ void wildcard_expander_t::expand_last_segment(const wcstring &base_dir, DIR *bas
|
|||
///
|
||||
/// Args:
|
||||
/// base_dir: the "working directory" against which the wildcard is to be resolved
|
||||
/// wc: the wildcard string itself, e.g. foo*bar/baz (where * is acutally ANY_STRING)
|
||||
/// wc: the wildcard string itself, e.g. foo*bar/baz (where * is acutally ANY_CHAR)
|
||||
/// prefix: the string that should be prepended for completions that replace their token.
|
||||
// This is usually the same thing as the original wildcard, but for fuzzy matching, we
|
||||
// expand intermediate segments. effective_prefix is always either empty, or ends with a slash
|
||||
|
@ -800,7 +816,7 @@ void wildcard_expander_t::expand(const wcstring &base_dir, const wchar_t *wc,
|
|||
const size_t wc_segment_len = next_slash ? next_slash - wc : wc_len;
|
||||
const wcstring wc_segment = wcstring(wc, wc_segment_len);
|
||||
const bool segment_has_wildcards =
|
||||
wildcard_has(wc_segment, true /* internal, i.e. look for ANY_STRING instead of * */);
|
||||
wildcard_has(wc_segment, true /* internal, i.e. look for ANY_CHAR instead of ? */);
|
||||
const wchar_t *const wc_remainder = next_slash ? next_slash + 1 : NULL;
|
||||
|
||||
if (wc_segment.empty()) {
|
||||
|
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
// Enumeration of all wildcard types.
|
||||
enum {
|
||||
/// Character representing any character except '/' (slash).
|
||||
ANY_CHAR = WILDCARD_RESERVED_BASE,
|
||||
/// Character representing any character string not containing '/' (slash).
|
||||
ANY_STRING = WILDCARD_RESERVED_BASE,
|
||||
ANY_STRING,
|
||||
/// Character representing any character string.
|
||||
ANY_STRING_RECURSIVE,
|
||||
/// This is a special psuedo-char that is not used other than to mark the
|
||||
|
|
1
tests/invocation/features-nocaret1.invoke
Normal file
1
tests/invocation/features-nocaret1.invoke
Normal file
|
@ -0,0 +1 @@
|
|||
--features 'no-stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status'
|
1
tests/invocation/features-nocaret1.out
Normal file
1
tests/invocation/features-nocaret1.out
Normal file
|
@ -0,0 +1 @@
|
|||
nocaret: 1
|
1
tests/invocation/features-nocaret2.invoke
Normal file
1
tests/invocation/features-nocaret2.invoke
Normal file
|
@ -0,0 +1 @@
|
|||
--features 'stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status'
|
1
tests/invocation/features-nocaret2.out
Normal file
1
tests/invocation/features-nocaret2.out
Normal file
|
@ -0,0 +1 @@
|
|||
nocaret: 0
|
1
tests/invocation/features-nocaret3.invoke
Normal file
1
tests/invocation/features-nocaret3.invoke
Normal file
|
@ -0,0 +1 @@
|
|||
--features 'no-stderr-nocaret' -c 'echo -n careton:; echo ^/dev/null'
|
1
tests/invocation/features-nocaret3.out
Normal file
1
tests/invocation/features-nocaret3.out
Normal file
|
@ -0,0 +1 @@
|
|||
careton:
|
1
tests/invocation/features-nocaret4.invoke
Normal file
1
tests/invocation/features-nocaret4.invoke
Normal file
|
@ -0,0 +1 @@
|
|||
--features ' stderr-nocaret' -c 'echo -n "caretoff: "; echo ^/dev/null'
|
1
tests/invocation/features-nocaret4.out
Normal file
1
tests/invocation/features-nocaret4.out
Normal file
|
@ -0,0 +1 @@
|
|||
caretoff: ^/dev/null
|
1
tests/invocation/features-qmark1.invoke
Normal file
1
tests/invocation/features-qmark1.invoke
Normal file
|
@ -0,0 +1 @@
|
|||
--features '' -c 'string match --quiet "??" ab ; echo "qmarkon: $status"'
|
1
tests/invocation/features-qmark1.out
Normal file
1
tests/invocation/features-qmark1.out
Normal file
|
@ -0,0 +1 @@
|
|||
qmarkon: 0
|
1
tests/invocation/features-qmark2.invoke
Normal file
1
tests/invocation/features-qmark2.invoke
Normal file
|
@ -0,0 +1 @@
|
|||
--features 'qmark-noglob' -C 'string match --quiet "??" ab ; echo "qmarkoff: $status"'
|
1
tests/invocation/features-qmark2.out
Normal file
1
tests/invocation/features-qmark2.out
Normal file
|
@ -0,0 +1 @@
|
|||
qmarkoff: 1
|
|
@ -6,3 +6,6 @@ status: Invalid combination of options,
|
|||
you cannot do both 'is-block' and 'is-interactive' in the same invocation
|
||||
status: Invalid job control mode 'full1'
|
||||
status: Invalid job control mode '1none'
|
||||
|
||||
####################
|
||||
# Future Feature Flags
|
||||
|
|
|
@ -48,4 +48,9 @@ function test_function
|
|||
end
|
||||
|
||||
test_function
|
||||
eval test_function
|
||||
eval test_function
|
||||
|
||||
logmsg Future Feature Flags
|
||||
status features
|
||||
status test-feature stderr-nocaret ; echo $status
|
||||
status test-feature not-a-feature ; echo $status
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
Not a function
|
||||
test_function
|
||||
test_function
|
||||
|
||||
####################
|
||||
# Future Feature Flags
|
||||
stderr-nocaret off 3.0 ^ no longer redirects stderr
|
||||
qmark-noglob off 3.0 ? no longer globs
|
||||
1
|
||||
2
|
||||
|
|
Loading…
Reference in a new issue