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 -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: | 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]}"