Procházet zdrojové kódy

Fix 'read -a' failure to create array (re: d55e9686) (#516)

The commit that backported read -a did not add a case label for it
to read.c. This was under the assumption that AST optget(3) would
always convert -a to -A. However, that was only done for first use.

The cause is the short-form options caching mechanism in optget(3).
On the first run, the pre-caching result would be returned, but the
equivalent option (-a) would be cached as if it is its own option,
so on the second and subsequent runs, optget returned 'a' instead
of 'A'. This only happens if no long-form equivalent is present.

Reproducer:

  $ read -A foo <<< 'foo bar baz'
  $ unset foo
  $ read -a foo <<< 'foo bar baz'
  $ echo ${foo[0]}
  foo bar baz

Expected: foo

src/lib/libast/misc/optget.c,
src/lib/libast/misc/optlib.h:
- [by Martijn Dekker] Implement caching for short-option
  equivalents. If a short-form equivalent is found, instead of
  caching it as a separate option, cache the equivalent in a new
  equiv[] array. Check this when reading the cache and substitute
  the main option for the equivalent if one is cached.

src/lib/libcmd/cp.c:
- Fix cp -r/cp -R symlink handling. The -r and -R options sometimes
  ignored -P, -L and -H.
- The -r and -R options no longer follow symlinks by default.

src/cmd/ksh93/bltins/whence.c,
src/lib/libcmd/*.c:
- Remove case labels that are redundant now that the optget(3)
  caching bug is fixed.

src/cmd/ksh93/tests/libcmd.sh:
- Added. This is the new script for the /opt/ast/bin path-bound
  built-ins from libcmd. Other relevant tests are moved into here.

Co-authored-by: Martijn Dekker <martijn@inlv.org>
Johnothan King před 1 rokem
rodič
revize
bea4fd56e8

+ 10 - 0
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

+ 0 - 1
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;

+ 1 - 1
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. */

+ 0 - 92
src/cmd/ksh93/tests/b_head.sh

@@ -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 <krader@skepticism.us>                  #
-#            Johnothan King <johnothanking@protonmail.com>             #
-#                                                                      #
-########################################################################
-
-# 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" <<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"))"
-
-# ======
-exit $((Errors<125?Errors:125))

+ 41 - 99
src/cmd/ksh93/tests/builtins.sh

@@ -16,6 +16,9 @@
 #                                                                      #
 ########################################################################
 
+# Tests for special and regular built-in commands (except those for
+# libcmd path-bound built-ins; they should go into libcmd.sh instead)
+
 . "${SHTESTS_COMMON:-${0%/*}/_common}"
 
 bincat=$(whence -p cat)
@@ -1212,19 +1215,6 @@ got=$($SHELL -c 't=good; t=bad command -@; print $t' 2>/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 <num>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))

+ 327 - 0
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 <svashisht@redhat.com>               #
+#                 Kurtis Rader <krader@skepticism.us>                  #
+#            Johnothan King <johnothanking@protonmail.com>             #
+#                  Martijn Dekker <martijn@inlv.org>                   #
+#                                                                      #
+########################################################################
+
+# 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))

+ 9 - 1
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;

+ 1 - 0
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

+ 7 - 9
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;

+ 1 - 2
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;

+ 2 - 3
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

+ 1 - 2
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':