mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-03-27 14:45:13 +08:00
Support for &> and &| as convenience redirections
This adds support for &> and &| syntax, which both redirect stdout, and also apply a redirection of stderr to stdout.
This commit is contained in:
parent
756e9826bc
commit
2a92e66902
@ -19,8 +19,9 @@
|
|||||||
- `switch` now allows arguments that expand to nothing, like empty variables (#5677).
|
- `switch` now allows arguments that expand to nothing, like empty variables (#5677).
|
||||||
- The null command (`:`) now always exits successfully, rather than passing through the previous exit status (#6022).
|
- The null command (`:`) now always exits successfully, rather than passing through the previous exit status (#6022).
|
||||||
- `jobs --last` returns 0 to indicate success when a job is found (#6104).
|
- `jobs --last` returns 0 to indicate success when a job is found (#6104).
|
||||||
- `commandline -p` and `commandline -j` now split on `&&` and `||` in addition to `;` and `&` (#6214)
|
- `commandline -p` and `commandline -j` now split on `&&` and `||` in addition to `;` and `&` (#6214).
|
||||||
- `fish` now correctly handles CDPATH entries that starts with `..` (#6220)
|
- `fish` now correctly handles CDPATH entries that starts with `..` (#6220).
|
||||||
|
- New redirections `&>` and `&|` may be used to redirect or pipe stdout, and also redirect stderr to stdout (#6192).
|
||||||
|
|
||||||
### Syntax changes and new commands
|
### Syntax changes and new commands
|
||||||
- Brace expansion now only takes place if the braces include a "," or a variable expansion, meaning common commands such as `git reset HEAD@{0}` do not require escaping (#5869).
|
- Brace expansion now only takes place if the braces include a "," or a variable expansion, meaning common commands such as `git reset HEAD@{0}` do not require escaping (#5869).
|
||||||
|
@ -337,8 +337,6 @@ Most programs use three input/output (IO) streams, each represented by a number
|
|||||||
|
|
||||||
- Standard error, FD 2, for writing errors and warnings, defaults to writing to the screen.
|
- Standard error, FD 2, for writing errors and warnings, defaults to writing to the screen.
|
||||||
|
|
||||||
The reason for providing for two output file descriptors is to allow separation of errors and warnings from regular program output.
|
|
||||||
|
|
||||||
Any file descriptor can be directed to a different output than its default through a simple mechanism called a redirection.
|
Any file descriptor can be directed to a different output than its default through a simple mechanism called a redirection.
|
||||||
|
|
||||||
An example of a file redirection is ``echo hello > output.txt``, which directs the output of the echo command to the file output.txt.
|
An example of a file redirection is ``echo hello > output.txt``, which directs the output of the echo command to the file output.txt.
|
||||||
@ -358,9 +356,11 @@ An example of a file redirection is ``echo hello > output.txt``, which directs t
|
|||||||
|
|
||||||
- An ampersand followed by a minus sign (``&-``). The file descriptor will be closed.
|
- An ampersand followed by a minus sign (``&-``). The file descriptor will be closed.
|
||||||
|
|
||||||
|
As a convenience, the redirection ``&>`` can be used to direct both stdout and stderr to the same file.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
To redirect both standard output and standard error to the file 'all_output.txt', you can write ``echo Hello > all_output.txt 2>&1``.
|
To redirect both standard output and standard error to the file 'all_output.txt', you can write ``echo Hello &> all_output.txt``, which is a convenience for ``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.
|
Any file descriptor can be redirected in an arbitrary way by prefixing the redirection with the file descriptor.
|
||||||
|
|
||||||
@ -388,6 +388,7 @@ Pipes usually connect file descriptor 1 (standard output) of the first process t
|
|||||||
|
|
||||||
will attempt to build the fish program, and any errors will be shown using the less pager.
|
will attempt to build the fish program, and any errors will be shown using the less pager.
|
||||||
|
|
||||||
|
As a convenience, the pipe ``&|`` may be used to redirect both stdout and stderr to the same process. (Note this is different from bash, which uses ``|&``).
|
||||||
|
|
||||||
.. _syntax-background:
|
.. _syntax-background:
|
||||||
|
|
||||||
|
@ -590,15 +590,16 @@ static void test_tokenizer() {
|
|||||||
const wchar_t *str =
|
const wchar_t *str =
|
||||||
L"string <redirection 2>&1 'nested \"quoted\" '(string containing subshells "
|
L"string <redirection 2>&1 'nested \"quoted\" '(string containing subshells "
|
||||||
L"){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ ^ ^^is_a_redirect "
|
L"){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ ^ ^^is_a_redirect "
|
||||||
|
L"&| &> "
|
||||||
L"&&& ||| "
|
L"&&& ||| "
|
||||||
L"&& || & |"
|
L"&& || & |"
|
||||||
L"Compress_Newlines\n \n\t\n \nInto_Just_One";
|
L"Compress_Newlines\n \n\t\n \nInto_Just_One";
|
||||||
using tt = token_type_t;
|
using tt = token_type_t;
|
||||||
const token_type_t types[] = {
|
const token_type_t types[] = {
|
||||||
tt::string, tt::redirect, tt::string, tt::redirect, tt::string, tt::string,
|
tt::string, tt::redirect, tt::string, tt::redirect, tt::string, tt::string,
|
||||||
tt::string, tt::redirect, tt::redirect, tt::string, tt::andand, tt::background,
|
tt::string, tt::redirect, tt::redirect, tt::string, tt::pipe, tt::redirect,
|
||||||
tt::oror, tt::pipe, tt::andand, tt::oror, tt::background, tt::pipe,
|
tt::andand, tt::background, tt::oror, tt::pipe, tt::andand, tt::oror,
|
||||||
tt::string, tt::end, tt::string};
|
tt::background, tt::pipe, tt::string, tt::end, tt::string};
|
||||||
|
|
||||||
say(L"Test correct tokenization");
|
say(L"Test correct tokenization");
|
||||||
|
|
||||||
@ -686,6 +687,13 @@ static void test_tokenizer() {
|
|||||||
do_test(pipe_or_redir(L"9999999999999>&2")->is_valid() == false);
|
do_test(pipe_or_redir(L"9999999999999>&2")->is_valid() == false);
|
||||||
do_test(pipe_or_redir(L"9999999999999>&2")->is_valid() == false);
|
do_test(pipe_or_redir(L"9999999999999>&2")->is_valid() == false);
|
||||||
|
|
||||||
|
do_test(pipe_or_redir(L"&|")->is_pipe);
|
||||||
|
do_test(pipe_or_redir(L"&|")->stderr_merge);
|
||||||
|
do_test(!pipe_or_redir(L"&>")->is_pipe);
|
||||||
|
do_test(pipe_or_redir(L"&>")->stderr_merge);
|
||||||
|
do_test(pipe_or_redir(L"&>>")->stderr_merge);
|
||||||
|
do_test(pipe_or_redir(L"&>?")->stderr_merge);
|
||||||
|
|
||||||
auto get_redir_mode = [](const wchar_t *s) -> maybe_t<redirection_mode_t> {
|
auto get_redir_mode = [](const wchar_t *s) -> maybe_t<redirection_mode_t> {
|
||||||
if (auto redir = pipe_or_redir_t::from_string(s)) {
|
if (auto redir = pipe_or_redir_t::from_string(s)) {
|
||||||
return redir->mode;
|
return redir->mode;
|
||||||
@ -4609,6 +4617,12 @@ static void test_highlighting() {
|
|||||||
{L"self%not", highlight_role_t::param},
|
{L"self%not", highlight_role_t::param},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
highlight_tests.push_back({
|
||||||
|
{L"false", highlight_role_t::command},
|
||||||
|
{L"&|", highlight_role_t::statement_terminator},
|
||||||
|
{L"true", highlight_role_t::command},
|
||||||
|
});
|
||||||
|
|
||||||
auto &vars = parser_t::principal_parser().vars();
|
auto &vars = parser_t::principal_parser().vars();
|
||||||
// Verify variables and wildcards in commands using /bin/cat.
|
// Verify variables and wildcards in commands using /bin/cat.
|
||||||
vars.set(L"VARIABLE_IN_COMMAND", ENV_LOCAL, {L"a"});
|
vars.set(L"VARIABLE_IN_COMMAND", ENV_LOCAL, {L"a"});
|
||||||
|
@ -80,6 +80,11 @@ static wcstring profiling_cmd_name_for_redirectable_block(const parse_node_t &no
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a redirection from stderr to stdout (i.e. 2>&1).
|
||||||
|
static std::shared_ptr<io_data_t> get_stderr_merge() {
|
||||||
|
return std::make_shared<io_fd_t>(STDERR_FILENO, STDOUT_FILENO, true /* user_supplied */);
|
||||||
|
}
|
||||||
|
|
||||||
parse_execution_context_t::parse_execution_context_t(parsed_source_ref_t pstree, parser_t *p,
|
parse_execution_context_t::parse_execution_context_t(parsed_source_ref_t pstree, parser_t *p,
|
||||||
std::shared_ptr<job_t> parent)
|
std::shared_ptr<job_t> parent)
|
||||||
: pstree(std::move(pstree)), parser(p), parent_job(std::move(parent)) {}
|
: pstree(std::move(pstree)), parser(p), parent_job(std::move(parent)) {}
|
||||||
@ -1019,6 +1024,12 @@ bool parse_execution_context_t::determine_io_chain(tnode_t<g::arguments_or_redir
|
|||||||
if (new_io.get() != NULL) {
|
if (new_io.get() != NULL) {
|
||||||
result.push_back(new_io);
|
result.push_back(new_io);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (redirect->stderr_merge) {
|
||||||
|
// This was a redirect like &> which also modifies stderr.
|
||||||
|
// Also redirect stderr to stdout.
|
||||||
|
result.push_back(get_stderr_merge());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (out_chain && !errored) {
|
if (out_chain && !errored) {
|
||||||
@ -1141,6 +1152,13 @@ parse_execution_result_t parse_execution_context_t::populate_job_from_job_node(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
processes.back()->pipe_write_fd = parsed_pipe->fd;
|
processes.back()->pipe_write_fd = parsed_pipe->fd;
|
||||||
|
if (parsed_pipe->stderr_merge) {
|
||||||
|
// This was a pipe like &| which redirects both stdout and stderr.
|
||||||
|
// Also redirect stderr to stdout.
|
||||||
|
auto ios = processes.back()->io_chain();
|
||||||
|
ios.push_back(get_stderr_merge());
|
||||||
|
processes.back()->set_io_chain(std::move(ios));
|
||||||
|
}
|
||||||
|
|
||||||
// Store the new process (and maybe with an error).
|
// Store the new process (and maybe with an error).
|
||||||
processes.emplace_back(new process_t());
|
processes.emplace_back(new process_t());
|
||||||
|
@ -215,7 +215,7 @@ class process_t {
|
|||||||
/// IO chain getter and setter.
|
/// IO chain getter and setter.
|
||||||
const io_chain_t &io_chain() const { return process_io_chain; }
|
const io_chain_t &io_chain() const { return process_io_chain; }
|
||||||
|
|
||||||
void set_io_chain(const io_chain_t &chain) { this->process_io_chain = chain; }
|
void set_io_chain(io_chain_t chain) { this->process_io_chain = std::move(chain); }
|
||||||
|
|
||||||
/// Store the current topic generations. That is, right before the process is launched, record
|
/// Store the current topic generations. That is, right before the process is launched, record
|
||||||
/// the generations of all topics; then we can tell which generation values have changed after
|
/// the generations of all topics; then we can tell which generation values have changed after
|
||||||
|
@ -296,6 +296,7 @@ maybe_t<pipe_or_redir_t> pipe_or_redir_t::from_string(const wchar_t *buff) {
|
|||||||
Note we are only responsible for parsing the redirection part, not 'cmd' or 'file'.
|
Note we are only responsible for parsing the redirection part, not 'cmd' or 'file'.
|
||||||
|
|
||||||
cmd | cmd normal pipe
|
cmd | cmd normal pipe
|
||||||
|
cmd &| cmd normal pipe plus stderr-merge
|
||||||
cmd >| cmd pipe with explicit fd
|
cmd >| cmd pipe with explicit fd
|
||||||
cmd 2>| cmd pipe with explicit fd
|
cmd 2>| cmd pipe with explicit fd
|
||||||
cmd < file stdin redirection
|
cmd < file stdin redirection
|
||||||
@ -308,6 +309,7 @@ maybe_t<pipe_or_redir_t> pipe_or_redir_t::from_string(const wchar_t *buff) {
|
|||||||
cmd 1>&2 file fd redirection with an explicit src fd
|
cmd 1>&2 file fd redirection with an explicit src fd
|
||||||
cmd <&2 file fd redirection with no explicit src fd (stdin is used)
|
cmd <&2 file fd redirection with no explicit src fd (stdin is used)
|
||||||
cmd 3<&0 file fd redirection with an explicit src fd
|
cmd 3<&0 file fd redirection with an explicit src fd
|
||||||
|
cmd &> file redirection with stderr merge
|
||||||
cmd ^ file caret (stderr) redirection, perhaps disabled via feature flags
|
cmd ^ file caret (stderr) redirection, perhaps disabled via feature flags
|
||||||
cmd ^^ file caret (stderr) redirection, perhaps disabled via feature flags
|
cmd ^^ file caret (stderr) redirection, perhaps disabled via feature flags
|
||||||
*/
|
*/
|
||||||
@ -404,7 +406,25 @@ maybe_t<pipe_or_redir_t> pipe_or_redir_t::from_string(const wchar_t *buff) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case L'&': {
|
||||||
|
consume(L'&');
|
||||||
|
if (try_consume(L'|')) {
|
||||||
|
// &| is pipe with stderr merge.
|
||||||
|
result.fd = STDOUT_FILENO;
|
||||||
|
result.is_pipe = true;
|
||||||
|
result.stderr_merge = true;
|
||||||
|
} else if (try_consume(L'>')) {
|
||||||
|
result.fd = STDOUT_FILENO;
|
||||||
|
result.stderr_merge = true;
|
||||||
|
result.mode = redirection_mode_t::overwrite;
|
||||||
|
if (try_consume(L'>')) result.mode = redirection_mode_t::append; // like &>>
|
||||||
|
if (try_consume(L'?'))
|
||||||
|
result.mode = redirection_mode_t::noclob; // like &>? or &>>?
|
||||||
|
} else {
|
||||||
|
return none();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
// Not a redirection.
|
// Not a redirection.
|
||||||
return none();
|
return none();
|
||||||
@ -521,10 +541,20 @@ maybe_t<tok_t> tokenizer_t::next() {
|
|||||||
}
|
}
|
||||||
case L'&': {
|
case L'&': {
|
||||||
if (this->buff[1] == L'&') {
|
if (this->buff[1] == L'&') {
|
||||||
|
// && is and.
|
||||||
result.emplace(token_type_t::andand);
|
result.emplace(token_type_t::andand);
|
||||||
result->offset = start_pos;
|
result->offset = start_pos;
|
||||||
result->length = 2;
|
result->length = 2;
|
||||||
this->buff += 2;
|
this->buff += 2;
|
||||||
|
} else if (this->buff[1] == L'>' || this->buff[1] == L'|') {
|
||||||
|
// &> and &| redirect both stdout and stderr.
|
||||||
|
auto redir = pipe_or_redir_t::from_string(buff);
|
||||||
|
assert(redir.has_value() &&
|
||||||
|
"Should always succeed to parse a &> or &| redirection");
|
||||||
|
result.emplace(redir->token_type());
|
||||||
|
result->offset = start_pos;
|
||||||
|
result->length = redir->consumed;
|
||||||
|
this->buff += redir->consumed;
|
||||||
} else {
|
} else {
|
||||||
result.emplace(token_type_t::background);
|
result.emplace(token_type_t::background);
|
||||||
result->offset = start_pos;
|
result->offset = start_pos;
|
||||||
@ -535,6 +565,7 @@ maybe_t<tok_t> tokenizer_t::next() {
|
|||||||
}
|
}
|
||||||
case L'|': {
|
case L'|': {
|
||||||
if (this->buff[1] == L'|') {
|
if (this->buff[1] == L'|') {
|
||||||
|
// || is or.
|
||||||
result.emplace(token_type_t::oror);
|
result.emplace(token_type_t::oror);
|
||||||
result->offset = start_pos;
|
result->offset = start_pos;
|
||||||
result->length = 2;
|
result->length = 2;
|
||||||
|
@ -152,6 +152,10 @@ struct pipe_or_redir_t {
|
|||||||
// Ignored for pipes.
|
// Ignored for pipes.
|
||||||
redirection_mode_t mode{redirection_mode_t::overwrite};
|
redirection_mode_t mode{redirection_mode_t::overwrite};
|
||||||
|
|
||||||
|
// Whether, in addition to this redirection, stderr should also be dup'd to stdout
|
||||||
|
// For example &| or &>
|
||||||
|
bool stderr_merge{false};
|
||||||
|
|
||||||
// Number of characters consumed when parsing the string.
|
// Number of characters consumed when parsing the string.
|
||||||
size_t consumed{0};
|
size_t consumed{0};
|
||||||
|
|
||||||
|
27
tests/checks/redirect.fish
Normal file
27
tests/checks/redirect.fish
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#RUN: %fish %s
|
||||||
|
|
||||||
|
function outnerr
|
||||||
|
command echo out $argv
|
||||||
|
command echo err $argv 1>&2
|
||||||
|
end
|
||||||
|
|
||||||
|
outnerr 0 &| count
|
||||||
|
#CHECK: 2
|
||||||
|
|
||||||
|
set -l tmpdir (mktemp -d)
|
||||||
|
outnerr overwrite &> $tmpdir/file.txt
|
||||||
|
cat $tmpdir/file.txt
|
||||||
|
#CHECK: out overwrite
|
||||||
|
#CHECK: err overwrite
|
||||||
|
|
||||||
|
outnerr append &>> $tmpdir/file.txt
|
||||||
|
cat $tmpdir/file.txt
|
||||||
|
#CHECK: out overwrite
|
||||||
|
#CHECK: err overwrite
|
||||||
|
#CHECK: out append
|
||||||
|
#CHECK: err append
|
||||||
|
|
||||||
|
echo noclobber &>>? $tmpdir/file.txt
|
||||||
|
#CHECKERR: {{.*}} The file {{.*}} already exists
|
||||||
|
|
||||||
|
rm -Rf $tmpdir
|
Loading…
x
Reference in New Issue
Block a user