diff --git a/NEWS b/NEWS index edc25f26b..d4043c0cd 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,16 @@ This documents significant changes in the 1.0 branch of ksh 93u+m. For full details, see the git log at: https://github.com/ksh93/ksh/tree/1.0 Uppercase BUG_* IDs are shell bug IDs as used by the Modernish shell library. +2022-08-20: + +- Fixed a bug in command line options processing that caused short-form + option equivalents on some built-in commands to be ignored after one use, + e.g., the new read -a equivalent of read -A (introduced on 2022-02-16). + +- Fixed a bug in the /opt/ast/bin/cp built-in command that caused the -r and + -R options to sometimes ignore -P, -L and -H. Additionally, the -r and -R + options no longer follow symlinks by default. + 2022-08-16: - Fixed an old bug in history expansion (set -H) where any use of the history diff --git a/src/cmd/ksh93/bltins/whence.c b/src/cmd/ksh93/bltins/whence.c index 0c783127e..08ccd3613 100644 --- a/src/cmd/ksh93/bltins/whence.c +++ b/src/cmd/ksh93/bltins/whence.c @@ -124,7 +124,6 @@ int b_whence(int argc,char *argv[],Shbltin_t *context) case 'f': flags |= F_FLAG; break; - case 'P': case 'p': flags |= P_FLAG; break; diff --git a/src/cmd/ksh93/include/version.h b/src/cmd/ksh93/include/version.h index d102799c0..7adaa23f0 100644 --- a/src/cmd/ksh93/include/version.h +++ b/src/cmd/ksh93/include/version.h @@ -18,7 +18,7 @@ #define SH_RELEASE_FORK "93u+m" /* only change if you develop a new ksh93 fork */ #define SH_RELEASE_SVER "1.0.3-alpha" /* semantic version number: https://semver.org */ -#define SH_RELEASE_DATE "2022-08-19" /* must be in this format for $((.sh.version)) */ +#define SH_RELEASE_DATE "2022-08-20" /* must be in this format for $((.sh.version)) */ #define SH_RELEASE_CPYR "(c) 2020-2022 Contributors to ksh " SH_RELEASE_FORK /* Scripts sometimes field-split ${.sh.version}, so don't change amount of whitespace. */ diff --git a/src/cmd/ksh93/tests/b_head.sh b/src/cmd/ksh93/tests/b_head.sh deleted file mode 100644 index f8eac9174..000000000 --- a/src/cmd/ksh93/tests/b_head.sh +++ /dev/null @@ -1,92 +0,0 @@ -######################################################################## -# # -# This software is part of the ast package # -# Copyright (c) 2019-2020 Contributors to ksh2020 # -# Copyright (c) 2022 Contributors to ksh 93u+m # -# and is licensed under the # -# Eclipse Public License, Version 2.0 # -# # -# A copy of the License is available at # -# https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html # -# (with md5 checksum 84283fa8859daf213bdda5a9f8d1be1d) # -# # -# Kurtis Rader # -# Johnothan King # -# # -######################################################################## - -# Tests for `head` builtin - -. "${SHTESTS_COMMON:-${0%/*}/_common}" -if ! builtin head 2> /dev/null; then - warning 'Could not detect head builtin; skipping tests' - exit 0 -fi - -cat > "$tmp/file1" < "$tmp/file2" </dev/null) [[ $exp == $got ]] || err_exit "temp var assignment with 'command'" \ "(expected $(printf %q "$expect"), got $(printf %q "$actual"))" -# ====== -# Regression test for https://github.com/att/ast/issues/949 -if (builtin chmod) 2>/dev/null -then foo_script='#!/bin/sh - exit 0' - echo "$foo_script" > "$tmp/foo1.sh" - echo "$foo_script" > "$tmp/foo2.sh" - builtin chmod - chmod +x "$tmp/foo1.sh" "$tmp/foo2.sh" - $SHELL "$tmp/foo1.sh" || err_exit "builtin 'chmod +x' doesn't work on first script" - $SHELL "$tmp/foo2.sh" || err_exit "builtin 'chmod +x' doesn't work on second script" -fi - # ====== # In ksh93v- 2013-10-10 alpha cd doesn't fail on directories without execute permission. # Additionally, ksh93v- added a regression test for attempting to use cd on a file. @@ -1343,26 +1333,6 @@ then builtin uname "(expected $(printf %q "$exp"), got $(printf %q "$got"))" fi -# ====== -# https://github.com/ksh93/ksh/issues/138 -builtin -d cat -if [[ $'\n'${ builtin; }$'\n' == *$'\n/opt/ast/bin/cat\n'* ]] -then exp=' version cat (*) ????-??-??' - got=$(/opt/ast/bin/cat --version 2>&1) - [[ $got == $exp ]] || err_exit "path-bound builtin not executable by literal canonical path" \ - "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" - got=$(PATH=/opt/ast/bin:$PATH; "${ whence -p cat; }" --version 2>&1) - [[ $got == $exp ]] || err_exit "path-bound builtin not executable by canonical path resulting from expansion" \ - "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" - got=$(PATH=/opt/ast/bin:$PATH; "$SHELL" -o restricted -c 'cat --version' 2>&1) - [[ $got == $exp ]] || err_exit "restricted shells do not recognize path-bound builtins" \ - "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" - got=$(set +x; PATH=/opt/ast/bin cat --version 2>&1) - [[ $got == $exp ]] || err_exit "path-bound builtin not found on PATH in preceding assignment" \ - "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" -else warning 'skipping path-bound builtin tests: builtin /opt/ast/bin/cat not found' -fi - # ====== # part of https://github.com/ksh93/ksh/issues/153 mkdir "$tmp/deleted" @@ -1439,55 +1409,6 @@ printf -v 'got[1][two][3]' 'ok\f%012d\n' $ver 2>/dev/null unset got ver } -# ====== -# The rm builtin's -d option should remove files and empty directories without -# removing non-empty directories (unless the -r option is also passed). -# https://www.austingroupbugs.net/view.php?id=802 -if builtin rm 2> /dev/null; then - echo foo > "$tmp/bar" - mkdir "$tmp/emptydir" - mkdir -p "$tmp/nonemptydir1/subfolder" - mkdir "$tmp/nonemptydir2" - echo dummyfile > "$tmp/nonemptydir2/shouldexist" - - # Tests for lone -d option - got=$(rm -d "$tmp/emptydir" 2>&1) - [[ $? == 0 ]] || err_exit 'rm builtin fails to remove empty directory with -d option' \ - "(got $(printf %q "$got"))" - [[ -d $tmp/emptydir ]] && err_exit 'rm builtin fails to remove empty directory with -d option' - got=$(rm -d $tmp/bar 2>&1) - [[ $? == 0 ]] || err_exit 'rm builtin fails to remove files with -d option' \ - "(got $(printf %q "$got"))" - [[ -f $tmp/bar ]] && err_exit 'rm builtin fails to remove files with -d option' - rm -d "$tmp/nonemptydir1" 2> /dev/null - [[ ! -d $tmp/nonemptydir1/subfolder ]] && err_exit 'rm builtin has unwanted recursion with -d option on folder containing folder' - rm -d "$tmp/nonemptydir2" 2> /dev/null - [[ ! -f $tmp/nonemptydir2/shouldexist ]] && err_exit 'rm builtin has unwanted recursion with -d option on folder containing file' - - # Recreate non-empty directories in case the above tests failed - mkdir -p "$tmp/nonemptydir1/subfolder" - mkdir -p "$tmp/nonemptydir2" - echo dummyfile > "$tmp/nonemptydir2/shouldexist" - - # Tests combining -d with -r - got=$(rm -rd "$tmp/nonemptydir1" 2>&1) - [[ $? == 0 ]] || err_exit 'rm builtin fails to remove non-empty directory and subdirectory with -rd options' \ - "(got $(printf %q "$got"))" - [[ -d $tmp/nonemptydir1/subfolder || -d $tmp/nonemptydir1 ]] && err_exit 'rm builtin fails to remove all folders with -rd options' - got=$(rm -rd "$tmp/nonemptydir2" 2>&1) - [[ $? == 0 ]] || err_exit 'rm builtin fails to remove non-empty directory and file with -rd options' \ - "(got $(printf %q "$got"))" - [[ -f $tmp/nonemptydir2/shouldexist || -d $tmp/nonemptydir2 ]] && err_exit 'rm builtin fails to remove all folders and files with -rd options' - - # Additional test: 'rm -f' without additional arguments should act - # as a no-op command. This bug was fixed in ksh93u+ 2012-02-14. - got=$(rm -f 2>&1) - if (($? != 0)) || [[ ! -z $got ]] - then err_exit 'rm -f without additional arguments does not work correctly' \ - "(got $(printf %q "$got"))" - fi -fi - # ====== # These are regression tests for the cd command's -e and -P flags if ((.sh.version >= 20211205)) @@ -1544,23 +1465,6 @@ then (( got == exp )) || err_exit "cd -eP to empty string has wrong exit status (expected $exp, got $got)" fi -# ====== -# The head and tail builtins should work on files without newlines -if builtin head 2> /dev/null; then - print -n nonewline > "$tmp/nonewline" - exp=nonewline - got=$(head -1 "$tmp/nonewline") - [[ $got == $exp ]] || err_exit "head builtin fails to correctly handle files without an ending newline" \ - "(expected $(printf %q "$exp"), got $(printf %q "$got"))" -fi -if builtin tail 2> /dev/null; then - print -n 'newline\nnonewline' > "$tmp/nonewline" - exp=nonewline - got=$(tail -1 "$tmp/nonewline") - [[ $got == $exp ]] || err_exit "tail builtin fails to correctly handle files without an ending newline" \ - "(expected $(printf %q "$exp"), got $(printf %q "$got"))" -fi - # ====== # ksh93v- accidentally broke the sleep builtin's support for # using microseconds in the form of U. @@ -1597,5 +1501,43 @@ if ((SHOPT_BRACEPAT)); then "(expected $(printf %q "$exp"), got $(printf %q "$got"))" fi +# The read builtin's -a and -A flags should function identically +read_a_test=$tmp/read_a_test.sh +cat > "$read_a_test" << 'EOF' +. "${SHTESTS_COMMON}" +exp=foo +exp1=bar +exp2=baz +read -a foo_a <<< 'foo bar baz' +if [[ ${foo_a[0]} != ${exp} ]] || [[ ${foo_a[1]} != ${exp1} ]] || [[ ${foo_a[2]} != ${exp2} ]] +then + err_exit "read -a fails to create array with first use" \ + "(foo_a[0] is $(printf %q "${foo_a[0]}"), foo_a[1] is $(printf %q "${foo_a[1]}"), foo_a[2] is $(printf %q "${foo_a[2]}"))" +fi +unset foo_a +read -a foo_a <<< 'foo bar baz' +if [[ ${foo_a[0]} != ${exp} ]] || [[ ${foo_a[1]} != ${exp1} ]] || [[ ${foo_a[2]} != ${exp2} ]] +then + err_exit "read -a fails to create array with second use" \ + "(foo_a[0] is $(printf %q "${foo_a[0]}"), foo_a[1] is $(printf %q "${foo_a[1]}"), foo_a[2] is $(printf %q "${foo_a[2]}"))" +fi +read -A foo_A <<< 'foo bar baz' +if [[ ${foo_A[0]} != ${exp} ]] || [[ ${foo_A[1]} != ${exp1} ]] || [[ ${foo_A[2]} != ${exp2} ]] +then + err_exit "read -A fails to create array with first use" \ + "(foo_A[0] is $(printf %q "${foo_A[0]}"), foo_A[1] is $(printf %q "${foo_A[1]}"), foo_A[2] is $(printf %q "${foo_A[2]}"))" +fi +unset foo_A +read -A foo_A <<< 'foo bar baz' +if [[ ${foo_A[0]} != ${exp} ]] || [[ ${foo_A[1]} != ${exp1} ]] || [[ ${foo_A[2]} != ${exp2} ]] +then + err_exit "read -A fails to create array with second use" \ + "(foo_A[0] is $(printf %q "${foo_A[0]}"), foo_A[1] is $(printf %q "${foo_A[1]}"), foo_A[2] is $(printf %q "${foo_A[2]}"))" +fi +exit $Errors +EOF +"$SHELL" "$read_a_test" +let Errors+=$? + # ====== exit $((Errors<125?Errors:125)) diff --git a/src/cmd/ksh93/tests/libcmd.sh b/src/cmd/ksh93/tests/libcmd.sh new file mode 100755 index 000000000..d96485bf3 --- /dev/null +++ b/src/cmd/ksh93/tests/libcmd.sh @@ -0,0 +1,327 @@ +######################################################################## +# # +# This software is part of the ast package # +# Copyright (c) 2019-2020 Contributors to ksh2020 # +# Copyright (c) 2022 Contributors to ksh 93u+m # +# and is licensed under the # +# Eclipse Public License, Version 2.0 # +# # +# A copy of the License is available at # +# https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html # +# (with md5 checksum 84283fa8859daf213bdda5a9f8d1be1d) # +# # +# Siteshwar Vashisht # +# Kurtis Rader # +# Johnothan King # +# Martijn Dekker # +# # +######################################################################## + +# Tests for path-bound built-ins from src/lib/libcmd + +. "${SHTESTS_COMMON:-${0%/*}/_common}" + +# ====== +# Tests for the cp builtin +if builtin cp 2> /dev/null; then + # The cp builtin's -r/-R flag should not interfere with the -L, -P and -H flags + echo 'test file' > "$tmp/cp_testfile" + ln -s "$tmp/cp_testfile" "$tmp/symlink1" + cp -r "$tmp/symlink1" "$tmp/symlink2" + { test -f "$tmp/symlink2" && test -L "$tmp/symlink2"; } || err_exit "default behavior of 'cp -r' follows symlinks" + rm "$tmp/symlink2" + cp -R "$tmp/symlink1" "$tmp/symlink2" + { test -f "$tmp/symlink2" && test -L "$tmp/symlink2"; } || err_exit "default behavior of 'cp -R' follows symlinks" + rm "$tmp/symlink2" + cp -Pr "$tmp/symlink1" "$tmp/symlink2" + { test -f "$tmp/symlink2" && test -L "$tmp/symlink2"; } || err_exit "'cp -Pr' follows symlinks" + rm "$tmp/symlink2" + cp -PR "$tmp/symlink1" "$tmp/symlink2" + { test -f "$tmp/symlink2" && test -L "$tmp/symlink2"; } || err_exit "'cp -PR' follows symlinks" + rm "$tmp/symlink2" + cp -rP "$tmp/symlink1" "$tmp/symlink2" + { test -f "$tmp/symlink2" && test -L "$tmp/symlink2"; } || err_exit "'cp -rP' follows symlinks" + rm "$tmp/symlink2" + cp -RP "$tmp/symlink1" "$tmp/symlink2" + { test -f "$tmp/symlink2" && test -L "$tmp/symlink2"; } || err_exit "'cp -RP' follows symlinks" + rm "$tmp/symlink2" + cp -Lr "$tmp/symlink1" "$tmp/testfile2" + { test -f "$tmp/testfile2" && ! test -L "$tmp/testfile2"; } || err_exit "'cp -Lr' doesn't follow symlinks" + rm "$tmp/testfile2" + cp -LR "$tmp/symlink1" "$tmp/testfile2" + { test -f "$tmp/testfile2" && ! test -L "$tmp/testfile2"; } || err_exit "'cp -LR' doesn't follow symlinks" + rm "$tmp/testfile2" + cp -rL "$tmp/symlink1" "$tmp/testfile2" + { test -f "$tmp/testfile2" && ! test -L "$tmp/testfile2"; } || err_exit "'cp -rL' doesn't follow symlinks" + rm "$tmp/testfile2" + cp -RL "$tmp/symlink1" "$tmp/testfile2" + { test -f "$tmp/testfile2" && ! test -L "$tmp/testfile2"; } || err_exit "'cp -RL' doesn't follow symlinks" + mkdir -p "$tmp/cp_testdir/dir1" + ln -s "$tmp/cp_testdir" "$tmp/testdir_symlink" + ln -s "$tmp/testfile2" "$tmp/cp_testdir/testfile2_sym" + cp -RH "$tmp/testdir_symlink" "$tmp/result" + { test -d "$tmp/result" && ! test -L "$tmp/result"; } || err_exit "'cp -RH' didn't follow the given symlink" + { test -f "$tmp/result/testfile2_sym" && test -L "$tmp/result/testfile2_sym"; } || err_exit "'cp -RH' follows symlinks not given on the command line" + rm -r "$tmp/result" + cp -rH "$tmp/testdir_symlink" "$tmp/result" + { test -d "$tmp/result" && ! test -L "$tmp/result"; } || err_exit "'cp -rH' didn't follow the given symlink" + { test -f "$tmp/result/testfile2_sym" && test -L "$tmp/result/testfile2_sym"; } || err_exit "'cp -rH' follows symlinks not given on the command line" + rm -r "$tmp/result" + cp -Hr "$tmp/testdir_symlink" "$tmp/result" + { test -d "$tmp/result" && ! test -L "$tmp/result"; } || err_exit "'cp -Hr' didn't follow the given symlink" + { test -f "$tmp/result/testfile2_sym" && test -L "$tmp/result/testfile2_sym"; } || err_exit "'cp -Hr' follows symlinks not given on the command line" + rm -r "$tmp/result" + cp -HR "$tmp/testdir_symlink" "$tmp/result" + { test -d "$tmp/result" && ! test -L "$tmp/result"; } || err_exit "'cp -HR' didn't follow the given symlink" + { test -f "$tmp/result/testfile2_sym" && test -L "$tmp/result/testfile2_sym"; } || err_exit "'cp -HR' follows symlinks not given on the command line" +fi + +# ====== +# Tests for the head builtin +if builtin head 2> /dev/null; then + cat > "$tmp/file1" <<-EOF + This is line 1 in file1 + This is line 2 in file1 + This is line 3 in file1 + This is line 4 in file1 + This is line 5 in file1 + This is line 6 in file1 + This is line 7 in file1 + This is line 8 in file1 + This is line 9 in file1 + This is line 10 in file1 + This is line 11 in file1 + This is line 12 in file1 + EOF + + cat > "$tmp/file2" <<-EOF2 + This is line 1 in file2 + This is line 2 in file2 + This is line 3 in file2 + This is line 4 in file2 + This is line 5 in file2 + EOF2 + + # -*n*; i.e., an integer presented as a flag. + # + # The `awk` invocation is to strip whitespace around the output of `wc` since it might pad the + # value. + exp=11 + got=$(head -11 < "$tmp/file1" | wc -l | awk '{ print $1 }') + [[ $got == "$exp" ]] || err_exit "'head -n' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -n, --lines=lines + # Copy lines lines from each file. The default value is 10. + got=$(head -n 3 "$tmp/file1") + exp=$'This is line 1 in file1\nThis is line 2 in file1\nThis is line 3 in file1' + [[ $got == "$exp" ]] || err_exit "'head -n' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -c, --bytes=chars + # Copy chars bytes from each file. + got=$(head -c 14 "$tmp/file1") + exp=$'This is line 1' + [[ $got == "$exp" ]] || err_exit "'head -c' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -q, --quiet|silent + # Never output filename headers. + got=$(head -q -n 3 "$tmp/file1" "$tmp/file2") + exp=$'This is line 1 in file1\nThis is line 2 in file1\nThis is line 3 in file1\nThis is line 1 in file2\nThis is line 2 in file2\nThis is line 3 in file2' + [[ $got == "$exp" ]] || err_exit "'head -q' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -s, --skip=char Skip char characters or lines from each file before copying. + got=$(head -s 5 -c 18 "$tmp/file1") + exp=$'is line 1 in file1' + [[ $got == "$exp" ]] || err_exit "'head -s' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -v, --verbose Always output filename headers. + got=$(head -v -n 3 "$tmp/file1") + exp=$'file1 <==\nThis is line 1 in file1\nThis is line 2 in file1\nThis is line 3 in file1' + [[ $got =~ "$exp" ]] || err_exit "'head -v' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" +fi + +# ====== +# Tests for the wc builtin +# wc - print the number of bytes, words, and lines in files +if builtin wc 2> /dev/null; then + cat > "$tmp/file1" <<-EOF + This is line 1 in file1 + This is line 2 in file1 + This is line 3 in file1 + This is line 4 in file1 + This is line 5 in file1 + EOF + + cat > "$tmp/file2" <<-EOF + This is line 1 in file2 + This is line 2 in file2 + This is line 3 in file2 + This is line 4 in file2 + This is line 5 in file2 + This is the longest line in file2 + 神 + EOF + + # -l, --lines List the line counts. + got=$(wc -l "$tmp/file1") + exp=$" 5 $tmp/file1" + [[ $got == "$exp" ]] || err_exit "'wc -l' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -w, --words List the word counts. + got=$(wc -w "$tmp/file1") + exp=$" 30 $tmp/file1" + [[ $got == "$exp" ]] || err_exit "'wc -w' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -c, --bytes|chars + # List the byte counts. + got=$(wc -c "$tmp/file1") + exp=$" 120 $tmp/file1" + [[ $got == "$exp" ]] || err_exit "'wc -c' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + if ((SHOPT_MULTIBYTE)) && [[ ${LC_ALL:-${LC_CTYPE:-${LANG:-}}} =~ [Uu][Tt][Ff]-?8 ]]; then + # -m|C, --multibyte-chars + # List the character counts. + got=$(wc -m "$tmp/file2") + exp=$" 156 $tmp/file2" + [[ $got == "$exp" ]] || err_exit "'wc -m' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + got=$(wc -C "$tmp/file2") + exp=$" 156 $tmp/file2" + [[ $got == "$exp" ]] || err_exit "'wc -C' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -q, --quiet Suppress invalid multibyte character warnings. + got=$(wc -q -m "$tmp/file2") + exp=$" 156 $tmp/file2" + [[ $got == "$exp" ]] || err_exit "'wc -q -m' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + got=$(wc -q -C "$tmp/file2") + exp=$" 156 $tmp/file2" + [[ $got == "$exp" ]] || err_exit "'wc -q -C' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + fi + + # -L, --longest-line|max-line-length + # List the longest line length; the newline,if any, is not + # counted in the length. + got=$(wc -L "$tmp/file2") + exp=$" 33 $tmp/file2" + [[ $got == "$exp" ]] || err_exit "'wc -l' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" + + # -N, --utf8 For UTF-8 locales --noutf8 disables UTF-8 optimzations and + # relies on the native mbtowc(3). On by default; -N means + # --noutf8. + got=$(wc -N "$tmp/file2") + exp=" 7 38 158 $tmp/file2" + [[ $got == "$exp" ]] || err_exit "'wc -N' failed (expected $(printf %q "$exp"), got $(printf %q "$got"))" +fi + +# ====== +# The rm builtin's -d option should remove files and empty directories without +# removing non-empty directories (unless the -r option is also passed). +# https://www.austingroupbugs.net/view.php?id=802 +if builtin rm 2> /dev/null; then + echo foo > "$tmp/bar" + mkdir "$tmp/emptydir" + mkdir -p "$tmp/nonemptydir1/subfolder" + mkdir "$tmp/nonemptydir2" + echo dummyfile > "$tmp/nonemptydir2/shouldexist" + + # Tests for lone -d option + got=$(rm -d "$tmp/emptydir" 2>&1) || err_exit 'rm builtin fails to remove empty directory with -d option' \ + "(got $(printf %q "$got"))" + [[ -d $tmp/emptydir ]] && err_exit 'rm builtin fails to remove empty directory with -d option' + got=$(rm -d $tmp/bar 2>&1) || err_exit 'rm builtin fails to remove files with -d option' \ + "(got $(printf %q "$got"))" + [[ -f $tmp/bar ]] && err_exit 'rm builtin fails to remove files with -d option' + rm -d "$tmp/nonemptydir1" 2> /dev/null + [[ ! -d $tmp/nonemptydir1/subfolder ]] && err_exit 'rm builtin has unwanted recursion with -d option on folder containing folder' + rm -d "$tmp/nonemptydir2" 2> /dev/null + [[ ! -f $tmp/nonemptydir2/shouldexist ]] && err_exit 'rm builtin has unwanted recursion with -d option on folder containing file' + + # Recreate non-empty directories in case the above tests failed + mkdir -p "$tmp/nonemptydir1/subfolder" + mkdir -p "$tmp/nonemptydir2" + echo dummyfile > "$tmp/nonemptydir2/shouldexist" + + # Tests combining -d with -r + got=$(rm -rd "$tmp/nonemptydir1" 2>&1) \ + || err_exit 'rm builtin fails to remove non-empty directory and subdirectory with -rd options' \ + "(got $(printf %q "$got"))" + [[ -d $tmp/nonemptydir1/subfolder || -d $tmp/nonemptydir1 ]] \ + && err_exit 'rm builtin fails to remove all folders with -rd options' + got=$(rm -rd "$tmp/nonemptydir2" 2>&1) \ + || err_exit 'rm builtin fails to remove non-empty directory and file with -rd options' \ + "(got $(printf %q "$got"))" + [[ -f $tmp/nonemptydir2/shouldexist || -d $tmp/nonemptydir2 ]] \ + && err_exit 'rm builtin fails to remove all folders and files with -rd options' + + # Repeat the above tests with -R instead of -r (because of possible optget bugs) + mkdir -p "$tmp/nonemptydir1/subfolder" + mkdir -p "$tmp/nonemptydir2" + echo dummyfile > "$tmp/nonemptydir2/shouldexist" + got=$(rm -Rd "$tmp/nonemptydir1" 2>&1) \ + || err_exit 'rm builtin fails to remove non-empty directory and subdirectory with -Rd options' \ + "(got $(printf %q "$got"))" + [[ -d $tmp/nonemptydir1/subfolder || -d $tmp/nonemptydir1 ]] \ + && err_exit 'rm builtin fails to remove all folders with -Rd options' + got=$(rm -Rd "$tmp/nonemptydir2" 2>&1) \ + || err_exit 'rm builtin fails to remove non-empty directory and file with -Rd options' \ + "(got $(printf %q "$got"))" + [[ -f $tmp/nonemptydir2/shouldexist || -d $tmp/nonemptydir2 ]] \ + && err_exit 'rm builtin fails to remove all folders and files with -Rd options' + + # Additional test: 'rm -f' without additional arguments should act + # as a no-op command. This bug was fixed in ksh93u+ 2012-02-14. + got=$(rm -f 2>&1) + if (($? != 0)) || [[ ! -z $got ]] + then err_exit 'rm -f without additional arguments does not work correctly' \ + "(got $(printf %q "$got"))" + fi +fi + +# ====== +# Regression test for https://github.com/att/ast/issues/949 +if builtin chmod 2>/dev/null; then + foo_script='exit 0' + echo "$foo_script" > "$tmp/foo1.sh" + echo "$foo_script" > "$tmp/foo2.sh" + chmod +x "$tmp/foo1.sh" "$tmp/foo2.sh" + "$tmp/foo1.sh" || err_exit "builtin 'chmod +x' doesn't work on first script" + "$tmp/foo2.sh" || err_exit "builtin 'chmod +x' doesn't work on second script" +fi + +# ====== +# https://github.com/ksh93/ksh/issues/138 +builtin -d cat +if [[ $'\n'${ builtin; }$'\n' == *$'\n/opt/ast/bin/cat\n'* ]] +then exp=' version cat (*) ????-??-??' + got=$(/opt/ast/bin/cat --version 2>&1) + [[ $got == $exp ]] || err_exit "path-bound builtin not executable by literal canonical path" \ + "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" + got=$(PATH=/opt/ast/bin:$PATH; "${ whence -p cat; }" --version 2>&1) + [[ $got == $exp ]] || err_exit "path-bound builtin not executable by canonical path resulting from expansion" \ + "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" + got=$(PATH=/opt/ast/bin:$PATH; "$SHELL" -o restricted -c 'cat --version' 2>&1) + [[ $got == $exp ]] || err_exit "restricted shells do not recognize path-bound builtins" \ + "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" + got=$(set +x; PATH=/opt/ast/bin cat --version 2>&1) + [[ $got == $exp ]] || err_exit "path-bound builtin not found on PATH in preceding assignment" \ + "(expected match of $(printf %q "$exp"), got $(printf %q "$got"))" +else warning 'skipping path-bound builtin tests: builtin /opt/ast/bin/cat not found' +fi + +# ====== +# The head and tail builtins should work on files without newlines +if builtin head 2> /dev/null; then + print -n nonewline > "$tmp/nonewline" + exp=nonewline + got=$(head -1 "$tmp/nonewline") + [[ $got == $exp ]] || err_exit "head builtin fails to correctly handle files without an ending newline" \ + "(expected $(printf %q "$exp"), got $(printf %q "$got"))" +fi +if builtin tail 2> /dev/null; then + print -n 'newline\nnonewline' > "$tmp/nonewline" + exp=nonewline + got=$(tail -1 "$tmp/nonewline") + [[ $got == $exp ]] || err_exit "tail builtin fails to correctly handle files without an ending newline" \ + "(expected $(printf %q "$exp"), got $(printf %q "$got"))" +fi + +# ====== +exit $((Errors<125?Errors:125)) diff --git a/src/lib/libast/misc/optget.c b/src/lib/libast/misc/optget.c index d8ed23a0a..a77f1948b 100644 --- a/src/lib/libast/misc/optget.c +++ b/src/lib/libast/misc/optget.c @@ -4549,6 +4549,8 @@ optget(register char** argv, const char* oopts) { if (cache) { + if (c >= 0 && c < sizeof(map) && map[c] && cache->equiv[map[c]]) + c = cache->equiv[map[c]]; if (k = cache->flags[map[c]]) { opt_info.arg = 0; @@ -4979,6 +4981,7 @@ optget(register char** argv, const char* oopts) v = f; for (;;) { + char eqv; if (isdigit(*f) && isdigit(*(f + 1))) while (isdigit(*(f + 1))) f++; @@ -4987,12 +4990,17 @@ optget(register char** argv, const char* oopts) else cache->flags[map[*f]] = m; j = 0; + /* + * parse and cache short option equivalents, + * e.g. x|y|z means -y and -z yield -x + */ + eqv = *f; while (*(f + 1) == '|') { f += 2; if (!(j = *f) || j == '!' || j == '=' || j == ':' || j == '?' || j == ']') break; - cache->flags[map[j]] = m; + cache->equiv[map[j]] = eqv; } if (j != '!' || (m & OPT_cache_invert)) break; diff --git a/src/lib/libast/misc/optlib.h b/src/lib/libast/misc/optlib.h index 3ec626591..a5f1dfd7d 100644 --- a/src/lib/libast/misc/optlib.h +++ b/src/lib/libast/misc/optlib.h @@ -71,6 +71,7 @@ typedef struct Optcache_s Optpass_t pass; int caching; unsigned char flags[sizeof(OPT_FLAGS)]; + char equiv[sizeof(OPT_FLAGS)]; /* short option equivalents */ } Optcache_t; typedef struct Optstate_s diff --git a/src/lib/libcmd/cp.c b/src/lib/libcmd/cp.c index 331002542..d81746c58 100644 --- a/src/lib/libcmd/cp.c +++ b/src/lib/libcmd/cp.c @@ -24,7 +24,7 @@ */ static const char usage_head[] = -"[-?@(#)$Id: cp (AT&T Research) 2012-04-20 $\n]" +"[-?@(#)$Id: cp (ksh 93u+m) 2022-08-20 $\n]" "[--catalog?" ERROR_CATALOG "]" ; @@ -806,8 +806,12 @@ b_cp(int argc, register char** argv, Shbltin_t* context) continue; case 'r': state->recursive = 1; - if (path_resolve < 0) - path_resolve = 0; + if (path_resolve < 1) + { + state->flags &= ~FTS_META; + state->flags |= FTS_PHYSICAL; + path_resolve = 1; + } continue; case 's': state->op = LN; @@ -847,12 +851,6 @@ b_cp(int argc, register char** argv, Shbltin_t* context) state->flags |= FTS_PHYSICAL; path_resolve = 1; continue; - case 'R': - state->recursive = 1; - state->flags &= ~FTS_META; - state->flags |= FTS_PHYSICAL; - path_resolve = 1; - continue; case 'S': state->suffix = opt_info.arg; continue; diff --git a/src/lib/libcmd/cut.c b/src/lib/libcmd/cut.c index 5750fa04a..7dea61fd1 100644 --- a/src/lib/libcmd/cut.c +++ b/src/lib/libcmd/cut.c @@ -23,7 +23,7 @@ */ static const char usage[] = -"[-?\n@(#)$Id: cut (AT&T Research) 2010-08-11 $\n]" +"[-?\n@(#)$Id: cut (ksh 93u_m) 2022-08-20 $\n]" "[--catalog?" ERROR_CATALOG "]" "[+NAME?cut - cut out selected columns or fields of each line of a file]" "[+DESCRIPTION?\bcut\b bytes, characters, or character-delimited fields " @@ -655,7 +655,6 @@ b_cut(int argc, char** argv, Shbltin_t* context) mode |= C_NONEWLINE; continue; case 'R': - case 'r': if(opt_info.num>0) reclen = opt_info.num; continue; diff --git a/src/lib/libcmd/rm.c b/src/lib/libcmd/rm.c index dc26bcb38..6d3213f48 100644 --- a/src/lib/libcmd/rm.c +++ b/src/lib/libcmd/rm.c @@ -24,7 +24,7 @@ */ static const char usage[] = -"[-?\n@(#)$Id: rm (AT&T Research) 2013-12-01 $\n]" +"[-?\n@(#)$Id: rm (ksh 93u+m) 2022-08-20 $\n]" "[--catalog?" ERROR_CATALOG "]" "[+NAME?rm - remove files]" "[+DESCRIPTION?\brm\b removes the named \afile\a arguments. By default it" @@ -347,10 +347,9 @@ b_rm(int argc, register char** argv, Shbltin_t* context) state.force = 0; continue; case 'r': - case 'R': state.recursive = 1; continue; - case 'F': + case 'c': #if _lib_fsync state.clobber = 1; #else diff --git a/src/lib/libcmd/wc.c b/src/lib/libcmd/wc.c index f060d7083..86e3d6474 100644 --- a/src/lib/libcmd/wc.c +++ b/src/lib/libcmd/wc.c @@ -23,7 +23,7 @@ */ static const char usage[] = -"[-?\n@(#)$Id: wc (AT&T Research) 2009-11-28 $\n]" +"[-?\n@(#)$Id: wc (ksh 93u+m) 2022-08-20 $\n]" "[--catalog?" ERROR_CATALOG "]" "[+NAME?wc - print the number of bytes, words, and lines in files]" "[+DESCRIPTION?\bwc\b reads one or more input files and, by default, " @@ -111,7 +111,6 @@ b_wc(int argc,register char **argv, Shbltin_t* context) mode |= WC_NOUTF8; continue; case 'm': - case 'C': mode |= WC_MBYTE; continue; case 'q':