From b3ea21e7bc09efdf3a89773385013fbe5e606f0d Mon Sep 17 00:00:00 2001 From: Mahmoud Al-Qudsi Date: Sun, 15 Dec 2024 21:21:47 -0600 Subject: [PATCH] Convert $IFS to a read-specific thing This removes $IFS globally (which prevents fish internals from breaking when IFS is manipulated) but keeps it as a legacy/deprecated mode of specifying `read`'s `--delimiter` value for compatibility with (ba)sh and legacy fish scripts. --- src/builtins/read.rs | 12 ++++++-- tests/checks/read.fish | 63 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/builtins/read.rs b/src/builtins/read.rs index 27c6684af..3cfc42fe9 100644 --- a/src/builtins/read.rs +++ b/src/builtins/read.rs @@ -9,6 +9,7 @@ use crate::common::unescape_string; use crate::common::valid_var_name; use crate::common::UnescapeStringStyle; use crate::env::EnvMode; +use crate::env::Environment; use crate::env::READ_BYTE_LIMIT; use crate::env::{EnvVar, EnvVarFlags}; use crate::input_common::terminal_protocols_disable_ifn; @@ -688,11 +689,18 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Opt continue; } - // todo!("don't clone") + // Order of precedence: --delimiter, an explicitly set $IFS, the default IFS constant above. + // An empty --delimiter or $IFS is a special mode that splits by character (i.e. does not + // fall back to the next candidate). + let mut ifs_var = None; let delimiter = opts .delimiter .as_ref() - .map(|delim| delim.as_utfstr()) + .or_else(|| { + ifs_var = Some(parser.vars().get(L!("IFS")).map(|ifs| ifs.as_string())); + ifs_var.as_ref().unwrap().as_ref() + }) + .map(|s| s.as_utfstr()) .unwrap_or(IFS); if delimiter.is_empty() { diff --git a/tests/checks/read.fish b/tests/checks/read.fish index 77d8fde75..65ed38192 100644 --- a/tests/checks/read.fish +++ b/tests/checks/read.fish @@ -43,6 +43,31 @@ echo -n a | read -l one echo "$status $one" #CHECK: 0 a +# Test splitting input with IFS empty +set -l IFS +echo hello | read -l one +print_vars one +#CHECK: 1 'hello' +echo hello | read -l one two +print_vars one two +#CHECK: 1 'h' 1 'ello' +echo hello | read -l one two three +print_vars one two three +#CHECK: 1 'h' 1 'e' 1 'llo' +echo '' | read -l one +print_vars one +#CHECK: 0 +echo t | read -l one two +print_vars one two +#CHECK: 1 't' 0 +echo t | read -l one two three +print_vars one two three +#CHECK: 1 't' 0 0 +echo ' t' | read -l one two +print_vars one two +#CHECK: 1 ' ' 1 't' +set -le IFS + echo 'hello there' | read -la ary print_vars ary #CHECK: 2 'hello' 'there' @@ -59,6 +84,18 @@ echo '' | read -la ary print_vars ary #CHECK: 0 +set -l IFS +echo hello | read -la ary +print_vars ary +#CHECK: 5 'h' 'e' 'l' 'l' 'o' +echo h | read -la ary +print_vars ary +#CHECK: 1 'h' +echo '' | read -la ary +print_vars ary +#CHECK: 0 +set -le IFS + # read -n tests echo testing | read -n 3 foo echo $foo @@ -190,7 +227,7 @@ echo abc\ndef | $fish -i -c 'read a; read b; set --show a; set --show b' | $filt #CHECK: $b: set in global scope, unexported, with 1 elements #CHECK: $b[1]: |def| -# Test --delimiter +# Test --delimiter (and $IFS, for now) echo a=b | read -l foo bar echo $foo echo $bar @@ -212,7 +249,18 @@ echo $bar echo $baz #CHECK: b -# Default behavior +# IFS empty string +set -l IFS '' +echo a=b | read -l foo bar baz +echo $foo +#CHECK: a +echo $bar +#CHECK: = +echo $baz +#CHECK: b + +# IFS unset +set -e IFS echo a=b | read -l foo bar baz echo $foo #CHECK: a=b @@ -238,6 +286,17 @@ echo $b #CHECK: b echo $c #CHECK: c +# Multi-char delimiters with IFS +begin + set -l IFS "..." + echo a...b...c | read -l a b c + echo $a + echo $b + echo $c +end +#CHECK: a +#CHECK: b +#CHECK: ..c # At one point, whatever was read was printed _before_ banana echo banana (echo sausage | read)