Skip to content

Commit

Permalink
Fix [ -t1 ] inside a command substitution
Browse files Browse the repository at this point in the history
Another non-forking subshell bug involving `[ -t 1 ]` being true when it
should be false.

Fixes #1079
  • Loading branch information
krader1961 committed Dec 20, 2018
1 parent 6cd0b41 commit 8e1e405
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ None at this time.

## Notable fixes and improvements

- Doing `[ -t1 ]` inside a command substitution behaves correctly
(issue #1079).
- The project now passes its unit tests when built with malloc debugging
enabled (i.e., `meson test --setup=malloc`).
- Changes to the project are now validated by running unit tests on the Travis
Expand Down
31 changes: 18 additions & 13 deletions src/cmd/ksh93/bltins/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ struct test {
};

static_fn char *nxtarg(struct test *, int);
static_fn int eval_expr(struct test *, int);
static_fn int eval_e3(struct test *);
static_fn int eval_expr(Shell_t *shp, struct test *, int);
static_fn int eval_e3(Shell_t *shp, struct test *);

static_fn int test_strmatch(Shell_t *shp, const char *str, const char *pat) {
int match[2 * (MATCH_MAX + 1)], n;
Expand Down Expand Up @@ -223,7 +223,7 @@ int b_test(int argc, char *argv[], Shbltin_t *context) {
default: { break; }
}
tdata.ac = argc;
result = !eval_expr(&tdata, 0);
result = !eval_expr(shp, &tdata, 0);

done:
sh_popcontext(shp, &buff);
Expand All @@ -236,11 +236,11 @@ int b_test(int argc, char *argv[], Shbltin_t *context) {
// Flag is 1 when in parenthesis.
// Flag is 2 when evaluating -a.
//
static_fn int eval_expr(struct test *tp, int flag) {
static_fn int eval_expr(Shell_t *shp, struct test *tp, int flag) {
int r;
char *p;

r = eval_e3(tp);
r = eval_e3(shp, tp);
while (tp->ap < tp->ac) {
p = nxtarg(tp, 0);
// Check for -o and -a.
Expand All @@ -254,10 +254,10 @@ static_fn int eval_expr(struct test *tp, int flag) {
tp->ap--;
break;
}
r |= eval_expr(tp, 3);
r |= eval_expr(shp, tp, 3);
continue;
} else if (*p == 'a') {
r &= eval_expr(tp, 2);
r &= eval_expr(shp, tp, 2);
continue;
}
}
Expand All @@ -280,15 +280,15 @@ static_fn char *nxtarg(struct test *tp, int mt) {
return tp->av[tp->ap++];
}

static_fn int eval_e3(struct test *tp) {
static_fn int eval_e3(Shell_t *shp, struct test *tp) {
char *arg, *cp;
int op;
char *binop;

arg = nxtarg(tp, 0);
if (c_eq(arg, '!')) return !eval_e3(tp);
if (c_eq(arg, '!')) return !eval_e3(shp, tp);
if (c_eq(arg, '(')) {
op = eval_expr(tp, 1);
op = eval_expr(shp, tp, 1);
cp = nxtarg(tp, 0);
if (!cp || !c_eq(cp, ')')) {
errormsg(SH_DICT, ERROR_exit(2), e_missing, "')'");
Expand All @@ -301,11 +301,14 @@ static_fn int eval_e3(struct test *tp) {
if (c2_eq(arg, '-', 't')) {
if (cp) {
op = strtol(cp, &binop, 10);
return *binop ? 0 : tty_check(op);
if (*binop) return 0;
if (shp->subshell && op == STDOUT_FILENO) return 0;

This comment has been minimized.

Copy link
@McDutchie

McDutchie May 29, 2020

Contributor

This is broken -- it simply always returns true for [ -t 1 ] if we're in a virtual/non-forked subshell, so it avoids doing the test altogether and returns a possibly false result.


(edit 1 Sep) Actually, I was partially wrong. On ksh93, command substitutions always fork when redirecting standard output within them. Forking (counter-intuitively) resets shp->subshell to zero. Consequently, it is safe to return false (0) if both shp->subshell and shp->comsub are set. This is a fix I'm going to apply to 93u+m now, but in a different place (the tty_check() function).

This comment has been minimized.

Copy link
@gordonwoodhull

gordonwoodhull May 29, 2020

Contributor

This repo is inactive - please see #1466 and #1464 (comment) onward.

Please search the web for a maintained version of ksh2020 or ksh93.

This comment has been minimized.

Copy link
@McDutchie

McDutchie May 30, 2020

Contributor

Thanks, I'm aware. In fact I maintain one. Just commenting for future reference, for others who may backport fixes.

This comment has been minimized.

Copy link
@gordonwoodhull

gordonwoodhull May 30, 2020

Contributor

Awesome, thanks. Good reason to keep comments open.

return tty_check(op);
}
// Test -t with no arguments.
tp->ap--;
return tty_check(1);
if (shp->subshell) return 0;
return tty_check(STDOUT_FILENO);
}
if (*arg == '-' && arg[2] == 0) {
op = arg[1];
Expand Down Expand Up @@ -441,7 +444,9 @@ int test_unop(Shell_t *shp, int op, const char *arg) {
case 't': {
char *last;
op = strtol(arg, &last, 10);
return *last ? 0 : tty_check(op);
if (*last) return 0;
if (shp->subshell && op == STDOUT_FILENO) return 0;
return tty_check(op);
}
case 'v':
case 'R': {
Expand Down
39 changes: 22 additions & 17 deletions src/cmd/ksh93/tests/basic.sh
Original file line number Diff line number Diff line change
Expand Up @@ -421,28 +421,33 @@ expect=$'x\ny\nz'
for tee in "$(whence tee)" $bin_tee
do
print xxx > $TEST_DIR/file
$tee >(sleep 1;cat > $TEST_DIR/file) <<< "hello" > /dev/null
[[ $(< $TEST_DIR/file) == hello ]] ||
log_error "process substitution does not wait for >() to complete with $tee"
$tee >(sleep 1; cat > $TEST_DIR/file) <<< "hello" > /dev/null
actual=$(< $TEST_DIR/file)
expect=hello
[[ $actual == $expect ]] ||
log_error "process substitution does not wait for >() to complete with $tee" "$expect" "$actual"

print yyy > $TEST_DIR/file2
$tee >(cat > $TEST_DIR/file) >(sleep 1;cat > $TEST_DIR/file2) <<< "hello" > /dev/null
[[ $(< $TEST_DIR/file2) == hello ]] ||
log_error "process substitution does not wait for second of two >() to complete with $tee"
$tee >(cat > $TEST_DIR/file) >(sleep 1; cat > $TEST_DIR/file2) <<< "hello" > /dev/null
actual=$(< $TEST_DIR/file2)
expect=hello
[[ $actual == $expect ]] ||
log_error "process substitution does not wait for second of two >() to complete with $tee" "$expect" "$actual"

print xxx > $TEST_DIR/file
$tee >(sleep 1;cat > $TEST_DIR/file) >(cat > $TEST_DIR/file2) <<< "hello" > /dev/null
[[ $(< $TEST_DIR/file) == hello ]] ||
log_error "process substitution does not wait for first of two >() to complete with $tee"
$tee >(sleep 1; cat > $TEST_DIR/file) >(cat > $TEST_DIR/file2) <<< "hello" > /dev/null
actual=$(< $TEST_DIR/file)
expect=hello
[[ $actual == $expect ]] ||
log_error "process substitution does not wait for first of two >() to complete with $tee" "$expect" "$actual"
done

if [[ -d /dev/fd ]]
if [[ $HAS_DEV_FD == yes ]]
then
if [[ $(print <(print foo) & sleep .5; kill $! 2>/dev/null) == /dev/fd/* ]]
then
expect='/dev/fd/+(\d) v=bam /dev/fd/+(\d)'
actual=$( print <(print foo) v=bam <(print bar))
[[ $actual == $expect ]] ||
log_error 'assignments after command subst not treated as arguments' "$expect" "$actual"
fi
expect='/dev/fd/+(\d) v=bam /dev/fd/+(\d)'
actual=$( print <(print foo) v=bam <(print bar))
[[ $actual == $expect ]] ||
log_error 'assignments after command substitution not treated as arguments' "$expect" "$actual"
fi

# ========
Expand Down
22 changes: 22 additions & 0 deletions src/cmd/ksh93/tests/test.exp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ expect "\r\nPASS\r\n" {
}
expect_prompt

# ==========
# Verify that constructs like `[ -t 1 ]` behave sensibly even inside a command substitution.

# This is the simple case that doesn't do any redirection of stdout within the command
# substitution. Thus the [ -t 1 ] test should be false.
send {v=$(echo begin; [ -t 1 ] && echo -t1 is true; echo end); echo "v=:$v:"}
send "\r"
expect "\r\nv=:begin\r\nend:\r\n" {
puts "-t1 in comsub works correctly"
}
expect_prompt

# This is the more complex case that does redirect stdout within the command substitution to the
# actual tty. Thus the [ -t 1 ] test should be true.
send {v=$(echo begin; exec >/dev/tty; [ -t 1 ] && echo -t1 is true; echo end); echo "v=:$v:"}
send "\r"
expect "\r\n-t1 is true\r\nend\r\nv=:begin:\r\n" {
puts "-t1 in comsub with exec >/dev/tty works correctly"
}
expect_prompt

# ==========
# Exit shell with ctrl-d
log_test_entry
send [ctrl D]
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/ksh93/tests/test.exp.out
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
stdin is a terminal device
stdout is a terminal device
-t1 in comsub works correctly
-t1 in comsub with exec >/dev/tty works correctly
13 changes: 13 additions & 0 deletions src/cmd/ksh93/tests/util/preamble.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ function empty_fifos {
}
alias empty_fifos='empty_fifos $LINENO'

#
# Verify that /dev/fd is functional. Note that on some systems (e.g., FreeBSD) there is a stub
# /dev/fd that only supports 0, 1, and 2. On such systems a special pseudo-filesystem may need to
# be mounted or a custom kernel built to get full /dev/fd support.
#
# Note that we can't do the straightforward `[[ -p /dev/fd/8 ]]` because such paths are
# special-cased by ksh and work even if the system doesn't support /dev/fd. But there may be tests
# where we need to know if those paths are recognized by the OS.
#
HAS_DEV_FD=no
[[ $(print /dev/fd/*) == *' /dev/fd/8 '* ]] && HAS_DEV_FD=yes
readonly HAS_DEV_FD

#
# Platforms like OpenBSD have `jot` instead of `seq`. For the simple case of emitting ints from one
# to n they are equivalent. And that should be the only use of `seq` in unit tests.
Expand Down

0 comments on commit 8e1e405

Please sign in to comment.