Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<? try-input redirection #10387

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc_src/language.rst
Expand Up @@ -168,6 +168,7 @@ Each stream has a number called the file descriptor (FD): 0 for stdin, 1 for std
The destination of a stream can be changed using something called *redirection*. For example, ``echo hello > output.txt``, redirects the standard output of the ``echo`` command to a text file.

- To read standard input from a file, use ``<SOURCE_FILE``.
- To read standard input from a file or /dev/null if it can't be read, use ``<?SOURCE_FILE``.
- To write standard output to a file, use ``>DESTINATION``.
- To write standard error to a file, use ``2>DESTINATION``. [#]_
- To append standard output to a file, use ``>>DESTINATION_FILE``.
Expand All @@ -188,6 +189,8 @@ Any arbitrary file descriptor can be used in a redirection by prefixing the redi
- To redirect the output of descriptor N, use ``N>DESTINATION``.
- To append the output of descriptor N to a file, use ``N>>DESTINATION_FILE``.

File descriptors cannot be used with a ``<?`` input redirection, only a regular ``<`` one.

For example::

# Write `foo`'s standard error (file descriptor 2)
Expand All @@ -213,6 +216,9 @@ For example::
echo stderr >&2 # <- this goes to stderr!
end >/dev/null # ignore stdout, so this prints "stderr"

# print all lines that include "foo" from myfile, or nothing if it doesn't exist.
string match '*foo*' <?myfile

It is an error to redirect a builtin, function, or block to a file descriptor above 2. However this is supported for external commands.

.. [#] Previous versions of fish also allowed specifying this as ``^DESTINATION``, but that made another character special so it was deprecated and removed. See :ref:`feature flags<featureflags>`.
Expand Down
2 changes: 1 addition & 1 deletion share/functions/__fish_complete_groups.fish
Expand Up @@ -6,6 +6,6 @@ function __fish_complete_groups --description "Print a list of local groups, wit
else
while read -l line
string split -f 1,4 : -- $line | string join \t
end </etc/group
end <?/etc/group
end
end
4 changes: 2 additions & 2 deletions share/functions/__fish_complete_user_ids.fish
@@ -1,7 +1,7 @@
function __fish_complete_user_ids --description "Complete user IDs with user name as description"
if command -sq getent
getent passwd | string replace -f -r '^([[:alpha:]_][^:]*):[^:]*:(\d+).*' '$2\t$1'
else if test -r /etc/passwd
string replace -f -r '^([[:alpha:]_][^:]*):[^:]*:(\d+).*' '$2\t$1' </etc/passwd
else
string replace -f -r '^([[:alpha:]_][^:]*):[^:]*:(\d+).*' '$2\t$1' <?/etc/passwd
end
end
4 changes: 2 additions & 2 deletions share/functions/__fish_complete_users.fish
Expand Up @@ -10,8 +10,8 @@ function __fish_complete_users --description "Print a list of local users, with
else if command -sq dscl
# This is the "Directory Service command line utility" used on macOS in place of getent.
command dscl . -list /Users RealName | string match -r -v '^_' | string replace -r ' {2,}' \t
else if test -r /etc/passwd
string match -v -r '^\s*#' </etc/passwd | while read -l line
else
string match -v -r '^\s*#' <?/etc/passwd | while read -l line
string split -f 1,5 : -- $line | string join \t | string replace -r ',.*' ''
end
end
Expand Down
22 changes: 8 additions & 14 deletions share/functions/__fish_print_hostnames.fish
Expand Up @@ -18,10 +18,8 @@ function __fish_print_hostnames -d "Print a list of known hostnames"
string split ' '

# Print nfs servers from /etc/fstab
if test -r /etc/fstab
string match -r '^\s*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:|^[a-zA-Z\.]*:' </etc/fstab |
string replace -r ':.*' ''
end
string match -r '^\s*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:|^[a-zA-Z\.]*:' <?/etc/fstab |
string replace -r ':.*' ''

# Check hosts known to ssh.
# Yes, seriously - the default specifies both with and without "2".
Expand Down Expand Up @@ -65,13 +63,11 @@ function __fish_print_hostnames -d "Print a list of known hostnames"
function _recursive --no-scope-shadowing
set -l paths
for config in $argv
if test -r "$config" -a -f "$config"
set paths $paths (
# Keep only Include lines and remove Include syntax
string replace -rfi '^\s*Include\s+' '' <$config \
# Normalize whitespace
| string trim | string replace -r -a '\s+' ' ')
end
set paths $paths (
# Keep only Include lines and remove Include syntax
string replace -rfi '^\s*Include\s+' '' <?$config \
# Normalize whitespace
| string trim | string replace -r -a '\s+' ' ')
end

set -l new_paths
Expand Down Expand Up @@ -117,9 +113,7 @@ function __fish_print_hostnames -d "Print a list of known hostnames"

# Read all files and operate on their combined content
for file in $known_hosts
if test -r $file
read -z <$file
end
read -z <?$file
end |
# Ignore hosts that are hashed, commented or @-marked and strip the key
# Handle multiple comma-separated hostnames sharing a key, too.
Expand Down
36 changes: 16 additions & 20 deletions share/functions/__fish_set_locale.fish
Expand Up @@ -26,15 +26,13 @@ function __fish_set_locale
# but we operate under the assumption that the locale can't include whitespace. Other whitespace
# shouldn't concern us, but a quoted "locale.LANG=SOMETHING" as a value to something else might.
# Here the last definition of a variable takes precedence.
if test -r /proc/cmdline
for var in (string match -ra 'locale.[^=]+=\S+' < /proc/cmdline)
set -l kv (string replace 'locale.' '' -- $var | string split '=')
# Only set locale variables, not other stuff contained in these files - this also
# automatically ignores comments.
if contains -- $kv[1] $LOCALE_VARS
and set -q kv[2]
set -gx $kv[1] (string trim -c '\'"' -- $kv[2])
end
for var in (string match -ra 'locale.[^=]+=\S+' <?/proc/cmdline)
set -l kv (string replace 'locale.' '' -- $var | string split '=')
# Only set locale variables, not other stuff contained in these files - this also
# automatically ignores comments.
if contains -- $kv[1] $LOCALE_VARS
and set -q kv[2]
set -gx $kv[1] (string trim -c '\'"' -- $kv[2])
end
end

Expand All @@ -54,18 +52,16 @@ function __fish_set_locale
# full POSIX-shell script.
set -l user_cfg_dir (set -q XDG_CONFIG_HOME; and echo $XDG_CONFIG_HOME; or echo ~/.config)
for f in $user_cfg_dir/locale.conf /etc/locale.conf /etc/env.d/02locale /etc/sysconfig/i18n /etc/default/locale
if test -r $f
while read -l kv
set kv (string split '=' -- $kv)
if contains -- $kv[1] $LOCALE_VARS
and set -q kv[2]
# Do not set already set variables again - this makes the merging happen.
if not set -q $kv[1]
set -gx $kv[1] (string trim -c '\'"' -- $kv[2])
end
while read -l kv
set kv (string split '=' -- $kv)
if contains -- $kv[1] $LOCALE_VARS
and set -q kv[2]
# Do not set already set variables again - this makes the merging happen.
if not set -q $kv[1]
set -gx $kv[1] (string trim -c '\'"' -- $kv[2])
end
end <$f
end
end
end <?$f
end

# If we really cannot get anything, at least set character encoding to UTF-8.
Expand Down
5 changes: 1 addition & 4 deletions share/functions/fish_command_not_found.fish
Expand Up @@ -5,11 +5,8 @@
# This has a "ID=" line that defines the exact distribution,
# and an "ID_LIKE=" line that defines what it is derived from or otherwise like.
# For our purposes, we use both.
set -l os
if test -r /etc/os-release
set os (string match -r '^ID(?:_LIKE)?\s*=.*' < /etc/os-release | \
set -l os (string match -r '^ID(?:_LIKE)?\s*=.*' <?/etc/os-release | \
string replace -r '^ID(?:_LIKE)?\s*=(.*)' '$1' | string trim -c '\'"' | string split " ")
end

function __fish_default_command_not_found_handler
printf (_ "fish: Unknown command: %s\n") (string escape -- $argv[1]) >&2
Expand Down
12 changes: 6 additions & 6 deletions share/functions/fish_git_prompt.fish
Expand Up @@ -478,20 +478,20 @@ function __fish_git_prompt_operation_branch_bare --description "fish_git_prompt
set -l total

if test -d $git_dir/rebase-merge
set branch (cat $git_dir/rebase-merge/head-name 2>/dev/null)
set step (cat $git_dir/rebase-merge/msgnum 2>/dev/null)
set total (cat $git_dir/rebase-merge/end 2>/dev/null)
read branch <?$git_dir/rebase-merge/head-name
read step <?$git_dir/rebase-merge/msgnum
read total <?$git_dir/rebase-merge/end
if test -f $git_dir/rebase-merge/interactive
set operation "|REBASE-i"
else
set operation "|REBASE-m"
end
else
if test -d $git_dir/rebase-apply
set step (cat $git_dir/rebase-apply/next 2>/dev/null)
set total (cat $git_dir/rebase-apply/last 2>/dev/null)
read step <?$git_dir/rebase-apply/next
read total <?$git_dir/rebase-apply/last
if test -f $git_dir/rebase-apply/rebasing
set branch (cat $git_dir/rebase-apply/head-name 2>/dev/null)
read branch <?$git_dir/rebase-apply/head-name
set operation "|REBASE"
else if test -f $git_dir/rebase-apply/applying
set operation "|AM"
Expand Down
3 changes: 1 addition & 2 deletions share/functions/help.fish
Expand Up @@ -79,8 +79,7 @@ function help --description 'Show help for the fish shell'
#
# We use this instead of xdg-open because that's useless without a backend
# like wsl-open which we'll check in a minute.
if test -f /proc/version
and string match -riq 'Microsoft|WSL|MSYS|MINGW' </proc/version
if string match -riq 'Microsoft|WSL|MSYS|MINGW' <?/proc/version
and set -l cmd (command -s powershell.exe cmd.exe /mnt/c/Windows/System32/cmd.exe)
# Use the first of these.
set fish_browser $cmd[1]
Expand Down
2 changes: 1 addition & 1 deletion src/highlight.rs
Expand Up @@ -1235,7 +1235,7 @@ impl<'s> Highlighter<'s> {
};
}
}
RedirectionMode::input => {
RedirectionMode::input | RedirectionMode::try_input => {
// Input redirections must have a readable non-directory.
target_is_valid = waccess(&target_path, R_OK) == 0
&& match wstat(&target_path) {
Expand Down
90 changes: 52 additions & 38 deletions src/io.rs
Expand Up @@ -646,6 +646,35 @@ impl IoChain {
#[allow(clippy::collapsible_else_if)]
pub fn append_from_specs(&mut self, specs: &RedirectionSpecList, pwd: &wstr) -> bool {
let mut have_error = false;

let print_error = |err, target: &wstr| {
// If the error is that the file doesn't exist
// or there's a non-directory component,
// find the first problematic component for a better message.
if [ENOENT, ENOTDIR].contains(&err) {
FLOGF!(warning, FILE_ERROR, target);
let mut dname: &wstr = target;
while !dname.is_empty() {
let next: &wstr = wdirname(dname);
if let Ok(md) = wstat(next) {
if !md.is_dir() {
FLOGF!(warning, "Path '%ls' is not a directory", next);
} else {
FLOGF!(warning, "Path '%ls' does not exist", dname);
}
break;
}
dname = next;
}
} else if err != EINTR {
// If we get EINTR we had a cancel signal.
// That's expected (ctrl-c on the commandline),
// so no warning.
FLOGF!(warning, FILE_ERROR, target);
perror("open");
}
};

for spec in specs {
match spec.mode {
RedirectionMode::fd => {
Expand All @@ -671,50 +700,35 @@ impl IoChain {
Err(err) => {
if oflags.intersects(OFlag::O_EXCL) && err == nix::Error::EEXIST {
FLOGF!(warning, NOCLOB_ERROR, spec.target);
} else {
} else if spec.mode != RedirectionMode::try_input {
if should_flog!(warning) {
let err = errno::errno().0;
// If the error is that the file doesn't exist
// or there's a non-directory component,
// find the first problematic component for a better message.
if [ENOENT, ENOTDIR].contains(&err) {
FLOGF!(warning, FILE_ERROR, spec.target);
let mut dname: &wstr = &spec.target;
while !dname.is_empty() {
let next: &wstr = wdirname(dname);
if let Ok(md) = wstat(next) {
if !md.is_dir() {
FLOGF!(
warning,
"Path '%ls' is not a directory",
next
);
} else {
FLOGF!(
warning,
"Path '%ls' does not exist",
dname
);
}
break;
}
dname = next;
}
} else if err != EINTR {
// If we get EINTR we had a cancel signal.
// That's expected (ctrl-c on the commandline),
// so no warning.
FLOGF!(warning, FILE_ERROR, spec.target);
perror("open");
}
print_error(errno::errno().0, &spec.target);
}
}
// If opening a file fails, insert a closed FD instead of the file redirection
// and return false. This lets execution potentially recover and at least gives
// the shell a chance to gracefully regain control of the shell (see #7038).
self.push(Arc::new(IoClose::new(spec.fd)));
have_error = true;
continue;
if spec.mode != RedirectionMode::try_input {
self.push(Arc::new(IoClose::new(spec.fd)));
have_error = true;
continue;
} else {
// If we're told to try via `<?`, we use /dev/null
match wopen_cloexec(L!("/dev/null"), oflags, OPEN_MASK) {
Ok(fd) => {
self.push(Arc::new(IoFile::new(spec.fd, fd)));
}
_ => {
// /dev/null can't be opened???
if should_flog!(warning) {
print_error(errno::errno().0, L!("/dev/null"));
}
self.push(Arc::new(IoClose::new(spec.fd)));
have_error = true;
continue;
}
}
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/redirection.rs
Expand Up @@ -11,6 +11,7 @@ pub enum RedirectionMode {
overwrite, // normal redirection: > file.txt
append, // appending redirection: >> file.txt
input, // input redirection: < file.txt
try_input, // try-input redirection: <? file.txt
fd, // fd redirection: 2>&1
noclob, // noclobber redirection: >? file.txt
}
Expand Down Expand Up @@ -38,7 +39,7 @@ impl RedirectionMode {
RedirectionMode::append => Some(OFlag::O_CREAT | OFlag::O_APPEND | OFlag::O_WRONLY),
RedirectionMode::overwrite => Some(OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_TRUNC),
RedirectionMode::noclob => Some(OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY),
RedirectionMode::input => Some(OFlag::O_RDONLY),
RedirectionMode::input | RedirectionMode::try_input => Some(OFlag::O_RDONLY),
_ => None,
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/tokenizer.rs
Expand Up @@ -990,6 +990,9 @@ impl TryFrom<&wstr> for PipeOrRedir {
consume(&mut cursor, '<');
if try_consume(&mut cursor, '&') {
result.mode = RedirectionMode::fd;
} else if try_consume(&mut cursor, '?') {
// <? foo try-input redirection (uses /dev/null if file can't be used).
result.mode = RedirectionMode::try_input;
} else {
result.mode = RedirectionMode::input;
}
Expand Down
22 changes: 22 additions & 0 deletions tests/checks/redirect.fish
Expand Up @@ -142,3 +142,25 @@ echo "/bin/echo pipe 12 <&12 12<&-" | source 12<&0
echo foo >/bin/echo/file
#CHECKERR: warning: An error occurred while redirecting file '/bin/echo/file'
#CHECKERR: warning: Path '/bin/echo' is not a directory

echo foo <?nonexistent
#CHECK: foo
echo $status
#CHECK: 0

read -l foo <?nonexistent
echo $status
#CHECK: 1
set -S foo
#CHECK: $foo: set in local scope, unexported, with 0 elements

set -l fish (status fish-path)
$fish --no-config -c 'true <&?fail'
#CHECKERR: fish: Requested redirection to '?fail', which is not a valid file descriptor
#CHECKERR: true <&?fail
#CHECKERR: ^~~~~~^

$fish --no-config -c 'true <?&fail'
#CHECKERR: fish: Expected a string, but found a '&'
#CHECKERR: true <?&fail
#CHECKERR: ^