Files
sausage/userland/brush/source/brush-shell/tests/cases/compat/builtins/read.yaml
T
2026-11-11 22:04:16 +00:00

568 lines
20 KiB
YAML

name: "Builtins: read"
cases:
- name: "Basic read usage from file"
test_files:
- path: "data.txt"
contents: |
a 1
b 2
stdin: |
while read name num; do echo "Hello, $name => $num"; done < data.txt
echo "Exited loop; name='$name', num='$num'"
- name: "Basic read usage from file and no var name"
test_files:
- path: "data.txt"
contents: |
a 1
b 2
stdin: |
while read; do echo "reply: '$REPLY'"; done < data.txt
echo "Exited loop; reply='$REPLY'"
- name: "Basic read usage with empty file"
stdin: |
while read name num; do echo "Hello, $name => $num"; done < /dev/null
declare -p name num
while read; do echo "reply: '$REPLY'"; done < /dev/null
declare -p REPLY
while read -a arr; do declare -p arr; done < /dev/null
declare -p arr
- name: "Basic read usage from pipe"
stdin: |
echo "1."
(echo a; echo b) | while read name; do echo "Hello, $name"; done
echo "2."
(echo "a b") | while read name; do echo "Hello, 1:$name REPLY:$REPLY"; done
echo "3."
(echo "a b") | while read -a arr; do declare -p arr; done
- name: "read with closed stdin"
ignore_stderr: true
stdin: |
echo 1 | read <&-
echo "result: $?"
- name: "read from here string"
stdin: |
read myvar <<< "hello"
echo "myvar: ${myvar}"
- name: "read from process substitution"
stdin: |
read myvar < <(echo hello)
echo "myvar: ${myvar}"
- name: "read with custom IFS"
stdin: |
content=" a b c "
while IFS= read line; do
echo "LINE: '$line'"
done <<<"${content}"
- name: "read text with tabs and custom IFS"
stdin: |
while IFS="" read myvar; do
echo "myvar1: |${myvar}|"
done < <(printf "a\tb\nc d\te\n")
- name: "read with empty entries"
stdin: |
(echo "x"; echo ""; echo "y"; echo ""; echo "") | while read line; do echo "LINE: '$line'"; done
echo 'x,,y,z' | (IFS=',' read READ1 READ2 READ3 READ4; declare -p READ1; declare -p READ2; declare -p READ3; declare -p READ4)
- name: "read -a with empty lines"
stdin: |
(echo "hi"; echo ""; echo "there"; echo ""; echo "you") | (read -a READ; declare -p READ)
(echo -e "hi\t\tthere\t\tyou") | (read -a READ -d $'\t'; declare -p READ)
- name: "read -a with empty interior field"
stdin: |
echo -e -n "hi||there||you" | (IFS='|' read -a READ; declare -p READ)
echo -e -n "hi there you " | (IFS=' ' read -a READ; declare -p READ)
echo -e -n "hi there you " | (read -a READ; declare -p READ)
- name: "read -a with empty leading field"
stdin: |
echo -e -n "|hi|there|you" | (IFS='|' read -a READ; declare -p READ)
- name: "read -a with empty trailing field"
stdin: |
echo -e -n "hi|there|you|" | (IFS='|' read -a READ; declare -p READ)
- name: "read -a with empty entries + empty delimiter"
stdin: |
(echo "hi"; echo ""; echo "there"; echo ""; echo "you") | (read -a READ -d ''; declare -p READ)
echo -e -n "hi\n\nthere\n\nyou\n" | (read -a READ -d ''; declare -p READ)
- name: "read -a with empty entries + empty delimiter + custom IFS"
stdin: |
(echo 'x,,y,z'; echo 'w,v'; echo ''; echo ''; echo 'm') | (IFS=',' read -d "" -a READ; declare -p READ)
- name: "read with empty delimiter"
min_oracle_version: 5.2 # \n renders differently in older shell versions
stdin: |
echo x | (read -d ""; declare -p REPLY)
# Timeout tests (read -t)
- name: "read -t 0 with data available in pipe"
# TODO(read):
# This test is racy - poll(0) may check the pipe before echo's data arrives.
# Bash consistently wins this race; our implementation sometimes doesn't.
# Worth investigating.
skip: true
stdin: |
echo "test" | { read -t 0; echo "exit: $?"; }
- name: "read -t 0 with no data (empty pipe)"
skip: true # TODO(read): see above
stdin: |
# Closed pipe with no data
: | { read -t 0; echo "exit: $?"; }
- name: "read -t with data immediately available"
stdin: |
echo "hello world" | { read -t 1 line; echo "exit: $?, line: '$line'"; }
- name: "read -t with fractional timeout and data available"
stdin: |
echo "test data" | { read -t 0.5 var; echo "exit: $?, var: '$var'"; }
- name: "read -t with regular file (always ready)"
test_files:
- path: "data.txt"
contents: |
line1
line2
stdin: |
read -t 0 < data.txt; echo "exit: $?"
read -t 1 line < data.txt; echo "exit: $?, line: '$line'"
- name: "read -t negative timeout is invalid"
ignore_stderr: true
stdin: |
read -t -1
echo "exit: $?"
- name: "read rejects an infinite or NaN timeout"
ignore_stderr: true
stdin: |
for t in inf -inf +inf Infinity nan NaN 1e400; do
echo x | { read -t "$t" v; echo "$t: rc=$? v='$v'"; }
done
- name: "read rejects a timeout that isn't a decimal number"
ignore_stderr: true
stdin: |
for t in 0x10 1:2 1.5.2 ++1 ' 1' '1 '; do
echo x | { read -t "$t" v; echo "[$t]: rc=$? v='$v'"; }
done
- name: "read accepts a timeout too long to represent"
incompatible_platforms: ["macos"] # bash fails with EINVAL there; brush waits
stdin: |
for t in 9223372036854775807 99999999999999999999; do
echo x | { read -t "$t" v; echo "$t: rc=$? v='$v'"; }
done
- name: "read treats an empty timeout as zero"
known_failure: true # TODO(read): an empty timeout is rejected as invalid
ignore_stderr: true
stdin: |
for t in '' .; do
read -t "$t" v <<< x; echo "[$t]: rc=$? v='$v'"
done
- name: "read -t with -n option"
stdin: |
echo "abcdef" | { read -t 1 -n 3 var; echo "exit: $?, var: '$var'"; }
- name: "read -t 0 with here string"
stdin: |
read -t 0 <<< "hello"; echo "exit: $?"
read -t 1 var <<< "hello"; echo "exit: $?, var: '$var'"
# Raw mode tests (read -r)
- name: "read -r preserves backslashes"
stdin: |
echo 'hello\nworld' | { read -r line; echo "line: '$line'"; }
echo 'path\\to\\file' | { read -r line; echo "line: '$line'"; }
- name: "read without -r processes escape sequences"
stdin: |
echo 'hello\nworld' | { read line; echo "line: '$line'"; }
echo 'a\\b' | { read line; echo "line: '$line'"; }
- name: "read -r with trailing backslash"
stdin: |
printf 'line with trailing\\' | { read -r line; echo "line: '$line'"; }
- name: "read without -r line continuation"
stdin: |
# Backslash-newline continues to next line
printf 'first\\\nsecond' | { read line; echo "line: '$line'"; }
- name: "read -r does not do line continuation"
stdin: |
printf 'first\\\nsecond' | { read -r line; echo "line: '$line'"; }
# -N vs -n option tests
- name: "read -n respects delimiter"
stdin: |
# -n stops at delimiter even before char count
echo "ab:cd" | { read -n 5 var; echo "var: '$var'"; }
- name: "read -N ignores delimiter"
skip: true # TODO(read): test sometimes results in printf error
stdin: |
# -N reads exactly N chars, delimiter treated as regular char
printf "ab:cd" | { read -N 4 var; echo "var: '$var'"; }
- name: "read -N does not split by IFS"
stdin: |
# -N should not split result by IFS
printf "a b c" | { read -N 5 var; echo "var: '$var'"; }
- name: "read -N with array"
stdin: |
# With -a and -N, entire input goes to single array element
printf "a b c" | { read -N 5 -a arr; declare -p arr; }
# Exit code tests
- name: "read exit code on success"
stdin: |
echo "data" | { read var; echo "exit: $?"; }
- name: "read exit code on EOF"
stdin: |
# EOF returns non-zero
read var < /dev/null; echo "exit: $?"
- name: "read exit code on EOF with partial data"
stdin: |
# EOF with partial data still returns non-zero but assigns
printf "partial" | { read var; echo "exit: $?, var: '$var'"; }
# IFS edge cases
- name: "read with IFS containing both whitespace and non-whitespace"
stdin: |
echo "a:b c:d" | (IFS=': ' read v1 v2 v3 v4; echo "v1='$v1' v2='$v2' v3='$v3' v4='$v4'")
- name: "read with consecutive non-whitespace delimiters"
stdin: |
echo "a::b" | (IFS=':' read v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'")
- name: "read assignment to fewer variables than fields"
stdin: |
echo "a b c d e" | { read v1 v2; echo "v1='$v1' v2='$v2'"; }
- name: "read assignment to more variables than fields"
stdin: |
echo "a b" | { read v1 v2 v3 v4; echo "v1='$v1' v2='$v2' v3='$v3' v4='$v4'"; }
- name: "read last variable preserves original delimiters"
stdin: |
# Last variable should get remainder with original delimiters, not re-joined with space
echo "x:y:z:w" | (IFS=':' read a b; echo "a='$a' b='$b'")
echo "x:y::z:w" | (IFS=':' read a b; echo "a='$a' b='$b'")
echo "a:b c:d e" | (IFS=': ' read v1 v2; echo "v1='$v1' v2='$v2'")
- name: "read with consecutive delimiters at field boundary"
stdin: |
# When field limit causes boundary at consecutive delimiters,
# extra delimiters should be preserved in remainder.
echo "x::y" | (IFS=':' read a b; echo "a='$a' b='$b'")
echo "x:::y" | (IFS=':' read a b; echo "a='$a' b='$b'")
# Also test with more than 2 variables
echo "a::b:c" | (IFS=':' read v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'")
- name: "read with empty IFS preserves entire line"
stdin: |
# Empty IFS means no splitting - entire line goes to first variable
echo "a b c" | (IFS='' read v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'")
echo "a:b:c" | (IFS='' read v1 v2; echo "v1='$v1' v2='$v2'")
# With array
echo "a b c" | (IFS='' read -a arr; declare -p arr)
- name: "read REPLY preserves leading and trailing whitespace"
stdin: |
# When no variable names given, REPLY should preserve whitespace
read <<< ' hello world '
echo "REPLY='$REPLY'"
read <<< ' tab separated '
echo "REPLY='$REPLY'"
- name: "read -a clears pre-existing array elements"
stdin: |
# Array should be cleared before assignment, not merged
arr=(old1 old2 old3 old4 old5)
read -a arr <<< 'new1 new2'
declare -p arr
- name: "read -N still processes backslashes without -r"
stdin: |
# -N ignores delimiter but still does backslash processing
printf 'a\\nb' | { read -N 4 var; echo "var='$var'"; }
printf 'a\\\\b' | { read -N 4 var; echo "var='$var'"; }
- name: "read -r -N for truly raw fixed-length reads"
stdin: |
# -r -N should preserve backslashes exactly
printf 'a\\nb' | { read -r -N 4 var; echo "var='$var'"; }
printf 'a\\\\b' | { read -r -N 4 var; echo "var='$var'"; }
- name: "read -u with invalid file descriptor"
ignore_stderr: true
stdin: |
exec 99<&- # fd 99 must be closed, but the harness can leak one in
read -u 99 var
echo "exit: $?"
- name: "read into readonly variable"
ignore_stderr: true
stdin: |
readonly myvar="original"
echo "test" | read myvar
echo "exit: $?, myvar='$myvar'"
- name: "read with mixed IFS into fewer variables"
stdin: |
# Complex IFS with fewer variables - verify remainder handling
echo 'x: y : z : w' | (IFS=': ' read a b; echo "a='$a' b='$b'")
# Backslash escape counting tests (bash counts OUTPUT chars, not INPUT)
- name: "read -n counts output chars after escape processing"
stdin: |
# Bash counts OUTPUT characters toward -n limit (after escape processing).
# Input: a, \, b, c = 4 input bytes. With -n 3:
# 'a' (output 1), '\b' → 'b' (output 2), 'c' (output 3) = "abc"
printf 'a\\bc' | { read -n 3 var; echo "var='$var'"; }
# Input: \, \, a, b = 4 input bytes. With -n 3:
# '\\' → '\' (output 1), 'a' (output 2), 'b' (output 3) = "\ab"
printf '\\\\ab' | { read -n 3 var; echo "var='$var'"; }
- name: "read -n with backslash-newline continuation"
stdin: |
# Backslash-newline is line continuation (consumes both, outputs nothing).
# Input: a, b, \, \n, c = 5 input bytes. With -n 3:
# 'a' (output 1), 'b' (output 2), '\n' → continuation, 'c' (output 3) = "abc"
printf 'ab\\\nc' | { read -n 3 var; echo "var='$var'"; }
- name: "read trailing backslash discarded on EOF"
stdin: |
# Bash discards a pending backslash when EOF is reached
printf 'ab\\' | { read var; echo "var='$var'"; }
printf 'ab\\' | { read -n 5 var; echo "var='$var'"; }
# Prompt tests (read -p)
- name: "read -p reads input correctly"
stdin: |
# Test that -p option reads input correctly (prompt display behavior
# differs between bash and brush when stdin is not a terminal)
read -p "Enter: " var <<< "hello"
echo "var='$var'"
# File descriptor tests (read -u)
- name: "read -u with valid file descriptor"
stdin: |
# Read from a specific file descriptor
exec 3<<< "from fd 3"
read -u 3 var
echo "var='$var'"
exec 3<&-
- name: "read -u combined with other options"
stdin: |
exec 4<<< "hello world test"
read -u 4 -a arr
declare -p arr
exec 4<&-
# Silent mode tests (read -s)
- name: "read -s with pipe input"
stdin: |
# Silent mode should still read input correctly (just not echo it)
echo "secret" | { read -s var; echo "var='$var'"; }
- name: "read -s with multiple variables"
stdin: |
echo "a b c" | { read -s v1 v2 v3; echo "v1='$v1' v2='$v2' v3='$v3'"; }
- name: "read into nameref variable"
known_failure: true
stdin: |
declare -n ref=result
read ref <<< "input_value"
echo "result: $result"
echo "ref: $ref"
- name: "read -a into nameref array"
known_failure: true
stdin: |
declare -n ref=myarr
read -a ref <<< "one two three"
echo "myarr[0]: ${myarr[0]}"
echo "myarr[1]: ${myarr[1]}"
echo "myarr[2]: ${myarr[2]}"
echo "ref[1]: ${ref[1]}"
- name: "read into subscripted nameref"
known_failure: true
stdin: |
arr=(a b c)
declare -n ref='arr[1]'
read ref <<< "replaced"
echo "arr[0]: ${arr[0]}"
echo "arr[1]: ${arr[1]}"
echo "arr[2]: ${arr[2]}"
- name: "read with multi-byte UTF-8 input"
stdin: |
printf '테스트 héllo\n' | { read -r x; echo "[$x]"; }
printf '\xed\x85\x8c b\n' | { read -r p q; echo "[$p][$q]"; }
- name: "read -d with a multi-byte delimiter"
stdin: |
printf 'ab\xc3\xa9c\n' | { read -r -d 'é' x; echo "[$x]"; }
# An invalid byte can't round-trip through the Rust String we read into, so these
# cases match on shape (`?` is one byte to the C-locale oracle, one character to us)
# rather than echoing the byte back.
- name: "read does not swallow bytes after an invalid UTF-8 byte"
stdin: |
# The 'Z' doesn't continue the sequence 0xC3 starts; it and 'b' must survive.
printf 'a\xc3Zb\n' | { read -r x; case "$x" in a?Zb) echo "match";; *) echo "NO: $x";; esac; }
# ...and when the byte that doesn't continue it is the delimiter, the line must
# still end there.
printf '\xc3\nSECOND\n' | { read -r a; read -r b; case "$a" in ?) echo "one";; *) echo "NO";; esac; echo "[$b]"; }
- name: "read with a truncated multi-byte sequence at EOF"
stdin: |
printf '\xe4\xb8' | {
read -r x; echo "rc=$?"
case "$x" in ??) echo "two";; *) echo "NO";; esac
read -r y; echo "rc2=$?"
}
- name: "read -n and -N count characters, not bytes"
# brush has no locale support and always decodes input as UTF-8, so the oracle
# needs a UTF-8 locale to agree; under the suite's default LC_ALL=C bash counts
# bytes instead.
env:
LC_ALL: "C.UTF-8"
stdin: |
printf '테스트\n' | { read -r -n 2 x; echo "[$x]"; }
printf '테스트\n' | { read -r -N 2 x; echo "[$x]"; }
printf 'héllo\n' | { read -r -n 3 x; echo "[$x]"; }
# 4-byte sequences.
printf '\xf0\x9f\x98\x80\xf0\x9f\x98\x80z\n' | { read -r -n 2 x; echo "[$x]"; }
printf '\xf0\x9f\x98\x80\xf0\x9f\x98\x80z\n' | { read -r -N 2 x; echo "[$x]"; }
# The limit is checked separately on the character after a backslash escape.
printf '\\\xc3\xa9\xed\x85\x8cx\n' | { read -n 2 x; echo "[$x]"; }
- name: "read into an invalid variable name fails"
known_failure: true # TODO(read): an invalid variable name is accepted
ignore_stderr: true
stdin: |
read 1x <<< "hello"
echo "read: $?"
- name: "read with an invalid timeout fails with status 1"
ignore_stderr: true
stdin: |
read -t abc x <<< "hello"
echo "read: $?"
- name: "read takes control characters from a pipe as data"
known_failure: true # TODO(signals): control characters are dropped from any input
stdin: |
printf 'a\003b\004c\n' | { read -r x; echo "rc=$? len=${#x}"; }
- name: "read options given more than once keep the last"
stdin: |
read -n 1 -n 2 x <<< "abc"; echo "x=[$x] rc=$?"
read -d , -d . x <<< "a.b,c"; echo "x=[$x] rc=$?"
read -a a1 -a a2 <<< "x y"; declare -p a2; declare -p a1 2>/dev/null || echo "a1 unset"
read -t 5 -t 0 <<< "a"; echo "rc=$?"
- name: "read with combined and attached short options"
stdin: |
read -rn1 x <<< 'abc'; echo "x=[$x]"
read -n2 x <<< 'abc'; echo "x=[$x]"
read -rsn2 x <<< 'abc'; echo "x=[$x]"
read -rN2 x <<< $'a\nbc'; echo "x=[$x]"
read -d, x <<< 'a,b'; echo "x=[$x]"
read -dX x <<< 'aXb'; echo "x=[$x]"
read -ra arr <<< 'a\b c'; declare -p arr
read -aarr <<< 'a b'; declare -p arr
read -u0 x <<< 'fd0'; echo "x=[$x]"
read -n +3 x <<< 'abcdef'; echo "x=[$x]"
read -t .5 x <<< 'a'; echo "x=[$x] rc=$?"
- name: "read with -- before the names"
stdin: |
read -- x y <<< 'a b c'; echo "x=[$x] y=[$y]"
read -r -- x <<< 'a\b'; echo "x=[$x]"
- name: "read with an unknown option or missing value fails with status 2"
ignore_stderr: true
stdin: |
read -Z x <<< 'a'; echo "rc=$?"
read -a; echo "rc=$?"
read -n; echo "rc=$?"
- name: "read with an invalid count or descriptor fails with status 1"
known_failure: true # TODO(read): fails with status 2
ignore_stderr: true
stdin: |
read -n abc x <<< 'a'; echo "rc=$?"
read -n -1 x <<< 'a'; echo "rc=$?"
read -N 1r x <<< 'a'; echo "rc=$?"
read -u abc x <<< 'a'; echo "rc=$?"
read -u -1 x <<< 'a'; echo "rc=$?"
- name: "read checks every -t and -u it is given"
ignore_stderr: true
stdin: |
exec 99<&- # fd 99 must be closed, but the harness can leak one in
x=keep; read -t abc -t 1 x <<< data; echo "rc=$? x=$x"
x=keep; read -t inf -t 1 x <<< data; echo "rc=$? x=$x"
x=keep; read -t 1 -t abc x <<< data; echo "rc=$? x=$x"
x=keep; read -u 99 -u 0 x <<< data; echo "rc=$? x=$x"
x=keep; read -t 5 -t 1 x <<< data; echo "rc=$? x=$x"
- name: "read -t with an exponent fails with status 1"
known_failure: true # TODO(read): accepts 1e1
ignore_stderr: true
stdin: |
read -t 1e1 x <<< 'a'; echo "rc=$? x=[$x]"
- name: "read -n 0 and -N 0 read nothing"
known_failure: true # TODO(read): reads one character
stdin: |
read -n 0 x <<< 'abc'; echo "x=[$x] rc=$?"
read -N 0 x <<< 'abc'; echo "x=[$x] rc=$?"
- name: "read option values may begin with a hyphen"
known_failure: true # TODO(read): such a value is taken for an option
stdin: |
read -d -x x <<< 'a-xb'; echo "x=[$x] rc=$?"
read -d -- x <<< 'a-b'; echo "x=[$x] rc=$?"
read -p -x x <<< 'a'; echo "x=[$x] rc=$?"
- name: "read takes words after the first name as names"
known_failure: true # TODO(read): a later option-like word is taken for an option
ignore_stderr: true
stdin: |
read x -r <<< 'a b'; echo "x=[$x] rc=$?"
read x -- y <<< 'a b'; echo "x=[$x] y=[$y] rc=$?"
- name: "read -h is an invalid option and prints nothing on stdout"
stdin: |
x=$(read -h 2>/dev/null); echo "rc=$? len=${#x}"