mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-26 21:03:12 +00:00
Add <?
input redirection
This tries to open the given file to use as stdin, and if it fails,
for any reason, it uses /dev/null instead.
This is useful in cases where we would otherwise do either of these:
```fish
test -r /path/to/file
and string match foo < /path/to/file
cat /path/to/file 2>/dev/null | string match foo
```
This both makes it nicer and shorter, *and* helps with TOCTTOU - what if the file is removed/changed after the check?
The reason for reading /dev/null instead of a closed fd is that a closed fd will often cause an error.
In case opening /dev/null fails, it still skips the command.
That's really a last resort for when the operating system
has turned out to be a platypus and not a unix.
Fixes #4865
(cherry picked from commit df8b9b7095
)
This commit is contained in:
parent
b3444ea128
commit
20243132fb
6 changed files with 86 additions and 40 deletions
|
@ -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``.
|
||||
|
@ -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)
|
||||
|
@ -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>`.
|
||||
|
|
|
@ -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) {
|
||||
|
|
90
src/io.rs
90
src/io.rs
|
@ -647,6 +647,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 => {
|
||||
|
@ -672,50 +701,35 @@ impl IoChain {
|
|||
Err(err) => {
|
||||
if oflags.contains(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -991,6 +991,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;
|
||||
}
|
||||
|
|
|
@ -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: ^
|
||||
|
|
Loading…
Reference in a new issue