Merge branch 'futurefeatures'

This merges support for feature flags.

Closes #4940
This commit is contained in:
ridiculousfish 2018-05-06 12:31:51 -07:00
commit aba69ac6ae
32 changed files with 451 additions and 66 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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;

View file

@ -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'*';

View file

@ -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();

View file

@ -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();

View 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);
}
}
}
}
}

View 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

View file

@ -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')': {

View file

@ -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

View file

@ -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) {

View file

@ -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()) {

View file

@ -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

View file

@ -0,0 +1 @@
--features 'no-stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status'

View file

@ -0,0 +1 @@
nocaret: 1

View file

@ -0,0 +1 @@
--features 'stderr-nocaret' -c 'status test-feature stderr-nocaret; echo nocaret: $status'

View file

@ -0,0 +1 @@
nocaret: 0

View file

@ -0,0 +1 @@
--features 'no-stderr-nocaret' -c 'echo -n careton:; echo ^/dev/null'

View file

@ -0,0 +1 @@
careton:

View file

@ -0,0 +1 @@
--features ' stderr-nocaret' -c 'echo -n "caretoff: "; echo ^/dev/null'

View file

@ -0,0 +1 @@
caretoff: ^/dev/null

View file

@ -0,0 +1 @@
--features '' -c 'string match --quiet "??" ab ; echo "qmarkon: $status"'

View file

@ -0,0 +1 @@
qmarkon: 0

View file

@ -0,0 +1 @@
--features 'qmark-noglob' -C 'string match --quiet "??" ab ; echo "qmarkoff: $status"'

View file

@ -0,0 +1 @@
qmarkoff: 1

View file

@ -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

View file

@ -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

View file

@ -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