Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/builtins/builtin_read2.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,29 @@ char *last_field(char *p, const char *ifs, bool raw)
}

/* Write a variable into the environment. `value_owned` is transferred —
do not free it after calling this; env_create takes ownership. */
void rd_set_var(t_shell *state, char *name, char *value_owned)
do not free it after calling this; env_create takes ownership. A
readonly variable is refused with bash's message and left as it was
(false); read goes on running, as bash's does -- it is not a special
builtin, so the error is not fatal. */
bool rd_set_var(t_shell *state, char *name, char *value_owned)
{
if (is_readonly_var(state, name))
{
ft_eprintf("%s: %s: readonly variable\n", state->ctx, name);
xfree(value_owned);
return (false);
}
env_set(&state->env, env_create(ft_strdup(name), value_owned, false));
return (true);
}

/* Split `line` across the variable names in argv[o->first..end-1]. All but
the last variable get one next_field() result; the last one gets
last_field() which includes the rest of the line minus trailing IFS
whitespace. This matches the POSIX `read` field-splitting algorithm. */
void assign_words(t_shell *state, char *line, t_vec argv, t_rdopt *o)
whitespace. This matches the POSIX `read` field-splitting algorithm.
A readonly name stops it the way it stops bash: before the last
variable, status 2 and nothing after it assigned; the last, status 1. */
int assign_words(t_shell *state, char *line, t_vec argv, t_rdopt *o)
{
size_t i;
char *p;
Expand All @@ -98,10 +110,12 @@ void assign_words(t_shell *state, char *line, t_vec argv, t_rdopt *o)
i = o->first;
while (i + 1 < argv.len)
{
rd_set_var(state, ((char **)argv.ctx)[i],
next_field(&p, o->ifs, o->raw));
if (!rd_set_var(state, ((char **)argv.ctx)[i],
next_field(&p, o->ifs, o->raw)))
return (2);
skip_delim(&p, o->ifs);
i++;
}
rd_set_var(state, ((char **)argv.ctx)[i], last_field(p, o->ifs, o->raw));
return (!rd_set_var(state, ((char **)argv.ctx)[i],
last_field(p, o->ifs, o->raw)));
}
40 changes: 26 additions & 14 deletions src/builtins/builtin_read3.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,26 @@ int rd_wait_input(t_rdopt *o)
/* Route the completed line to its destination: -a fills the named array,
no variable names sends the whole line to $REPLY, otherwise the line is
IFS-split across the named variables. rd_set_var takes ownership of the
line; the array/word paths copy fields out of it, so it is freed here. */
static void rd_dispatch(t_shell *state, t_vec argv, char *line, t_rdopt *o)
line; the array/word paths copy fields out of it, so it is freed here.
Returns the status a readonly variable forces, 0 when every assignment
was made: bash's 2 for REPLY, 1 for an -a array (assign_words has the
named-variable rules). */
static int rd_dispatch(t_shell *state, t_vec argv, char *line, t_rdopt *o)
{
if (o->aname)
{
int st;

if (o->first >= argv.len && !o->aname)
return (2 * !rd_set_var(state, "REPLY", line));
st = 0;
if (o->aname && is_readonly_var(state, o->aname))
st = (ft_eprintf("%s: %s: readonly variable\n", state->ctx,
o->aname), 1);
else if (o->aname)
rd_assign_array(state, line, o);
xfree(line);
}
else if (o->first >= argv.len)
rd_set_var(state, "REPLY", line);
else
{
assign_words(state, line, argv, o);
xfree(line);
}
st = assign_words(state, line, argv, o);
xfree(line);
return (st);
}

/* read [-r] [-n N] [-N N] [-d C] [-t S] [-p PROMPT] [-a NAME] [var ...]:
Expand All @@ -84,6 +89,10 @@ static void rd_dispatch(t_shell *state, t_vec argv, char *line, t_rdopt *o)
Returns 1 on EOF even if some data was read (mimics bash), so `while read
line; do …; done` processes the last line before stopping even when the
file lacks a trailing newline.
At EOF with nothing read the variables are still assigned, empty (an
-a array emptied), as bash does. They used to keep their old values, so
`while read -r l || [ -n "$l" ]` -- the loop that also handles a last
line with no newline -- never ended: $l still held that last line.
-N reads a fixed byte count and must NOT field-split: an empty IFS is
exactly that, so the ordinary assign_words path handles it with no second
code path to keep in step. */
Expand All @@ -92,6 +101,7 @@ int builtin_read(t_shell *state, t_vec argv)
char *line;
t_rdopt o;
int eof;
int st;

o = (t_rdopt){.nchars = -1, .delim = '\n', .tmo_ms = -1};
o.first = parse_read_opts2(argv, &o);
Expand All @@ -105,7 +115,9 @@ int builtin_read(t_shell *state, t_vec argv)
return (xfree(o.ifs), 142);
line = read_one_line(&o, &eof);
if (!line)
return (xfree(o.ifs), 1);
rd_dispatch(state, argv, line, &o);
line = ft_strdup("");
st = rd_dispatch(state, argv, line, &o);
if (st)
return (xfree(o.ifs), st);
return (xfree(o.ifs), eof != 0);
}
4 changes: 2 additions & 2 deletions src/builtins/builtins_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ void skip_delim(char **pp, const char *ifs);
char *last_field(char *p, const char *ifs, bool raw);
size_t parse_read_opts2(t_vec argv, t_rdopt *o);
void rd_assign_array(t_shell *state, char *line, t_rdopt *o);
void rd_set_var(t_shell *state, char *name, char *value_owned);
void assign_words(t_shell *state, char *line, t_vec argv, t_rdopt *o);
bool rd_set_var(t_shell *state, char *name, char *value_owned);
int assign_words(t_shell *state, char *line, t_vec argv, t_rdopt *o);
long rd_secs_ms(const char *s);
/* fc's parsed command line (builtin_fc3.c): -e NAME, the -l -n -r -s
switches, and the first [last] operands that follow them. */
Expand Down
53 changes: 53 additions & 0 deletions tests/scripts/58_read_eof_readonly.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# read assigns its variables even at end of input, and never a readonly one.
#
# At EOF with nothing read, hellish returned 1 without touching the
# variables, so they kept their old values. That made the common loop
# while IFS= read -r line || [ -n "$line" ]; do ...; done
# (which also takes a last line with no newline) run forever: $line still
# held the last line. bash assigns them empty.
#
# And read wrote readonly variables: `readonly r=1; echo x | read r`
# changed r. bash refuses with "r: readonly variable" and a status.
msg() { sed 's/^.*: line [0-9]*: //'; }

echo "-- EOF with nothing read empties the variables"
x=old; y=old; REPLY=old; arr=(o l d)
printf '' | { read -r x y; echo "rc=$? [${x-unset}] [${y-unset}]"; }
printf '' | { read -r; echo "rc=$? [${REPLY-unset}]"; }
printf '' | { read -r -a arr; echo "rc=$? n=${#arr[@]}"; }
printf 'a\n' | { read -r x; read -r x; echo "rc=$? [$x]"; }
printf '' | { read -r -d : x; echo "rc=$? [$x]"; }
printf '' | { read -r -n 3 x; echo "rc=$? [$x]"; }
printf '' | { read -r -N 3 x; echo "rc=$? [$x]"; }

echo "-- a last line with no newline, then EOF"
printf 'last' | { read -r x y; echo "rc=$? [$x] [$y]"; }
printf 'l1\nl2' | {
n=0
while IFS= read -r line || [ -n "$line" ]; do
n=$((n + 1)); echo "got [$line]"
[ "$n" -gt 5 ] && { echo "runaway loop"; break; }
done
}
printf 'k=v\nlast=1' | {
n=0
while IFS= read -r line || [ -n "$line" ]; do
n=$((n + 1)); echo "line: $line"
[ "$n" -gt 5 ] && { echo "runaway loop"; break; }
done
echo "done, line=[$line]"
}

echo "-- a readonly variable is refused, the others follow bash"
readonly ro=keep
echo x | { read -r ro 2>&1 | msg; }
echo x | { read -r ro 2>/dev/null; echo "only var: rc=$? [$ro]"; }
printf '' | { read -r ro 2>/dev/null; echo "only var at EOF: rc=$? [$ro]"; }
echo "a b" | { read -r ro y 2>/dev/null; echo "first of two: rc=$? [$ro] [${y-unset}]"; }
echo "a b" | { read -r y ro 2>/dev/null; echo "last of two: rc=$? [$ro] [$y]"; }
echo "a b c" | { y=; z=old; read -r y ro z 2>/dev/null; echo "middle: rc=$? [$ro] [$y] [$z]"; }
echo a | { readonly REPLY; read -r 2>/dev/null; echo "REPLY: rc=$?"; }
ra=(1 2); readonly ra
echo "p q" | { read -r -a ra 2>&1 | msg; }
echo "p q" | { read -r -a ra 2>/dev/null; echo "array: rc=$? [${ra[*]}]"; }
echo x | { read -r ro 2>/dev/null; echo "the shell goes on"; }
Loading