# $NetBSD: printf.sh,v 1.9 2022/05/24 20:50:20 andvar Exp $
#
# Copyright (c) 2018 The NetBSD Foundation, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
Running_under_ATF=false
test -n "${Atf_Shell}" && test -n "${Atf_Check}" && Running_under_ATF=true
Tests=
# create a test case:
# "$1" is basic test name, "$2" is description
define()
{
NAME=$1; shift
if $Running_under_ATF
then
eval "${NAME}_head() { set descr 'Tests printf: $*'; }"
eval "${NAME}_body() { ${NAME} ; }"
else
eval "TEST_${NAME}_MSG="'"$*"'
fi
Tests="${Tests} ${NAME}"
}
# 1st arg is printf format conversion specifier
# other args (if any) are args to that format
# returns success if that conversion specifier is supported, false otherwise
supported()
{
FMT="$1"; shift
case "$#" in
0) set -- 123;; # provide an arg for format to use
esac
(do_printf >/dev/null 2>&1 "%${FMT}" "$@")
}
LastErrorTest=
$Running_under_ATF || {
# Provide functions to emulate (roughly) what ATF gives us
# (that we actually use)
atf_skip() {
echo >&2 "${CurrentTest} skipped: ${MSG} $*"
}
atf_fail() {
if [ "${CurrentTest}" != "${LastErrorTest}" ]
then
echo >&2 "======== In Test ${CurrentTest}:"
LastErrorTest="${CurrentTest}"
fi
echo >&2 "${CurrentTest} FAIL: ${MSG} $*"
RVAL=1
}
atf_require_prog() {
# Just allow progs we want to run to be, or not be, found
return 0
}
}
# 1st arg is the result expected, remaining args are handed to do_printf
# to execute, fail if result does not match the expected result (treated
# as a sh pattern), or if do_printf fails
expect()
{
WANT="$1"; shift
negated=false
case "${WANT}" in
('!') WANT="$1"; negated=true; shift;;
esac
RES=${RES%X} # hack to defeat \n removal from $() output
if $negated
then
case "${RES}" in
(${WANT})
atf_fail \
"$* ... Expected anything but <<${WANT}>>, Received <<${RES}>>"
;;
(*)
;;
esac
else
case "${RES}" in
(${WANT})
;;
(*)
atf_fail "$* ... Expected <<${WANT}>> Received <<${RES}>>"
;;
esac
fi
return 0
}
# a variant which allows for two possible results
# It would be nice to have just one function, and allow the pattern
# to contain alternatives ... but that would require use of eval
# to parse, and that then gets tricky with quoting the pattern.
# and we only ever need two (so far anyway), so this is easier...
expect2()
{
WANT1="$1"; shift
WANT2="$1"; shift
RES=${RES%X} # hack to defeat \n removal from $() output
case "${RES}" in
(${WANT1} | ${WANT2})
;;
(*)
atf_fail \
"$* ... Expected <<${WANT1}|${WANT2}>> Received <<${RES}>>"
;;
esac
return 0
}
expect_fail()
{
WANT="$1"; shift # we do not really expect this, but ...
RES=$( do_printf "$@" 2>/dev/null && echo X ) && {
RES=${RES%X}
case "${RES}" in
(${WANT})
atf_fail \
"$* ... success${WANT:+ with expected <<${WANT}>>}"
;;
('')
atf_fail "$* ... success (without output)"
;;
(*)
atf_fail "$* ... success with <<${RES}>> (not <<${WANT}>>)"
;;
esac
RVAL=1
return 0
}
RES=$( do_printf "$@" 2>&1 >/dev/null )
STAT=$?
test -z "${RES}" &&
atf_fail "$* ... failed (${STAT}) without error message"
RES="$( do_printf "$@" 2>/dev/null || : ; echo X )"
RES=${RES%X} # hack to defeat \n removal from $() output
case "${RES}" in
(${WANT})
# All is good, printf failed, sent a message to stderr
# and printed what it should to stdout
;;
(*)
atf_fail \
"$* ... should fail with <<${WANT}>> did exit(${STAT}) with <<${RES}>>"
;;
esac
return 0
}
##########################################################################
##########################################################################
#
# Actual tests follow
#
##########################################################################
##########################################################################
basic()
{
setmsg basic
if (do_printf >/dev/null 2>&1)
then
atf_fail "with no args successful"
fi
if test -n "$( do_printf 2>/dev/null )"
then
atf_fail "with no args produces text on stdout"
fi
if test -z "$( do_printf 2>&1 )"
then
atf_fail "with no args no err/usage message"
fi
for A in - -- X 1
do
if (do_printf "%${A}%" >/dev/null 2>&1)
then
atf_fail "%${A}% successful"
fi
done
# technically these are all unspecified, but the only rational thing
expect_fail '' %3%
expect_fail a a%.%
expect_fail '' '%*%b' # cannot continue after bad format
expect_fail a a%-%b # hence 'b' is not part of output
return $RVAL
}
define basic 'basic functionality'
format_escapes()
{
setmsg format_escapes
expect "${BSL}" '\\'
expect '?' '\\' # must be just 1 char
expect "${NL}" '\n'
expect " " '\t' # a literal <tab> in " "
atf_require_prog wc
atf_require_prog od
atf_require_prog tr
for fmt in '\0' '\00' '\000'
do
RES=$(( $( do_printf "${fmt}" | wc -c ) ))
if [ "${RES}" -ne 1 ]
then
atf_fail "'${fmt}' output $RES bytes, expected 1"
elif [ $(( $( do_printf "${fmt}" | od -A n -to1 ) )) -ne 0 ]
then
RES="$( do_printf "${fmt}" | od -A n -to1 | tr -d ' ')"
atf_fail \
"'${fmt}' output was '\\${RES}' should be '\\000'"
fi
done
# There are no expected failures here, as all other \Z
# sequences produce unspecified results -- anything is OK.
return $RVAL
}
define format_escapes "backslash escapes in format string"
s_strings()
{
setmsg s_strings
# The # and 0 flags produce undefined results (so don't test)
# The + and ' ' flags are ignored (only apply to signed conversions)
expect abcd %s abcd
expect ' a' %3s a
expect 'a ' %-3s a
expect abcd %3s abcd
expect abcd %-3s abcd
expect a '%c' a
expect a '%c' abc
expect 'ad' '%c%c' abc def
expect '@ a@a @' "@%3c@%-3c@" a a
expect '@ a@a @' "@%2c@%-4c@" a a
# do not test with '' (null string) as operand to %c
# as whether that produces \0 or nothing is unspecified.
# (test NetBSD specific behaviour in NetBSD specific test)
return $RVAL
}
define c_chars '%c (character) format conversions'
# Only tests of negative numbers are that we do not
# fail, and do not get a '-' in the result
# This is because the number of bits available is not defined
# so we cannot anticipate what value a negative number will
# produce when interpreted as unsigned (unlike hex and octal
# where we can at least examine the least significant bits)
# Note that the ' ' and '+' flags only apply to signed conversions
# so they should be simply ignored for '%u'
expect 1 '% u' 1
expect 1 '% 1u' 1
expect 1 '% 0u' 1
expect ' 1' '% 5u' 1
expect 001 '%0 3u' 1
expect ' 03' '% 4.2u' 3
# negative numbers are allowed, but printed as unsigned.
# Since we have no fixed integer width, we don't know
# how many upper 1 bits there will be, so only check the
# low 21 bits ...
expect '*7777777' '%o' -1
expect '*7777776' '%04o' -2
expect '*7777770' '%7o' -8
expect '0*7777700' '%#o' -0100
expect '*7777663' '%o' -77
# The 'alternate representation' (# flag) inserts 0x unless value==0
expect 0 %#x 0
expect 0x1 %#x 1
# We can also print negative numbers (treated as unsigned)
# but as there is no defined integer width for printf(1)
# we don't know how many F's in FFF...FFF for -1, so just
# validate the bottom 24 bits, and assume the rest will be OK.
# (tests above will fail if printf can't handle at least 32 bits)
# The only difference between %f and %f is how Inf and NaN
# are printed ... so just test a couple of those and
# a couple of the others above (to verify nothing else changes)
supported F || {
atf_skip "%F conversion not supported"
return $RVAL
}
return $RVAL
}
define E_floats "%E floating point conversions"
g_floats()
{
setmsg g_floats
supported g || {
atf_skip "%g conversion not supported"
return $RVAL
}
# for a value writtem in %e format, which has an exponent of x
# then %.Pg will produce 'f' format if x >= -4, and P > x,
# otherwise it produces 'e' format.
# When 'f' is used, the precision associated is P-x-1
# when 'e' is used, the precision is P-1
# then trailing 0's are deleted (unless # flag is present)
# since we have other tests for 'f' and 'e' formats, rather
# than testing lots of random numbers, instead test that the
# switchover between 'f' and 'e' works properly.
expect 1 %.1g 1 # p = 1, x = 0 : %.0f
expect 0.5 %.1g 0.5 # p = 1, x = -1: %.1f
expect 1 %.2g 1 # p = 2, x = 0 : %.1f
expect 0.5 %.2g 0.5 # p = 2, x = -1: %.2f
expect 1 %g 1 # p = 6, x = 0 : %.5f
expect -0.5 %g -0.5 # p = 6, x = -1: %.6f
supported G || {
atf_skip "%G conversion not supported"
return $RVAL
}
# 'G' uses 'F' or 'E' instead or 'f' or 'e'.
# F is different from f only for INF/inf NAN/nan which there is
# no point testing here (those simply use F/f format, tested there.
# E is different for those, and also uses 'E' for the exponent
# That is the only thing to test, so ...
expect 1.2E-05 %.2G 0.000012 # p = 2, x = -5: $.1e
expect2 INF INFINITY %G Infinity
expect2 -INF -INFINITY %G -INF
expect2 NAN 'NAN(*)' %G NaN
# It is difficult to test correct results from the %a conversions,
# as they depend upon the underlying floating point format (not
# necessarily IEEE) and other factors chosen by the implementation,
# eg: the (floating) number 1 could be 0x8p-3 0x4p-2 0x1p-1 even
# assuming IEEE formats wnen using %.0a. But we can test 0
a_floats()
{
setmsg a_floats
supported a || {
atf_skip "%a conversion not supported"
return $RVAL
}
# and unlikely to occur IRL
expect " ab
" %7.4b 'ab\r\bxy\t\t\n'
expect "111 " %-6.3b '\00611\061\01\n\t\n'
# and last, that pesky \0
atf_require_prog wc
atf_require_prog sed
for fmt in '\0' '\00' '\000' '\0000'
do
if [ $( do_printf %b "${fmt}" | wc -c ) -ne 1 ]
then
atf_fail \
"%b '${fmt}' did not output exactly 1 character (byte)"
elif [ $(( $( do_printf %b "${fmt}" | od -A n -to1 ) )) -ne 0 ]
then
atf_require_prog od
atf_require_prog tr
RES="$(do_printf %b "${fmt}" | od -An -to1 | tr -d ' ')"
atf_fail \
"%b '${fmt}' output was '\\${RES}' should be '\\000'"
fi
for xt in "x${fmt}" "${fmt}q" "x${fmt}q" "${fmt}\\0" \
"${fmt}|\\0|\\0|" "${fmt}${fmt}" "+${fmt}-${fmt}*"
do
# nb: we "know" here that the only \'s are \0's
# nb: not do_printf, we are not testing ...
bsl=$( printf %s "${xt}" | sed -e 's/\\00*/X/g' )
xl=${#bsl}
RES=$(( $( do_printf %b "${xt}" | wc -c ) ))
if [ "${RES}" -ne "${xl}" ]
then
atf_fail \
"%b '${xt}' output ${RES} chars, expected ${xl}"
fi
done
test ${#fmt} -lt 5 && continue
if [ $( do_printf %b "${fmt}1" | wc -c ) -ne 2 ]
then
atf_fail \
"%b '${fmt}1' did not output exactly 2 characters"
fi
done
# test \c in arg to printf %b .. causes instant death...
expect ab %b 'ab\cdef'
expect ab a%bc 'b\cd'
expect abcd %s%c%x%b a bcd 12 'd\c'
expect ad %.1s%x%b%c%x all 13 '\cars' cost 12
expect "a${NL}b" '%b\n' a 'b\c' d '\ce'
# This is undefined, though would be nice if we could rely upon it
# expect "abcd" %.1b 'a\c' 'b\c' 'c\c' 'd\c' '\c' e
# Check for interference from one instance of execution of
# a builtin printf execution to another
# (this makes no sense to test for standalone printf, and for which
# the tests don't handle ';' magic args, so this would not work)
if $BUILTIN_TEST
then
expect abcdefjklmno %s%b%s abc 'def\c' ghi ';' %s%s jkl mno
fi
return $RVAL
}
define b_SysV_echo_backslash_c 'Use of \c in arg to %b format'
indirect_width()
{
setmsg indirect_width
supported '*d' 5 123 || {
atf_skip "%*d not supported (indirect field width)"
return $RVAL
}
lpad= rpad= zpad=
for i in 1 2 3 4 5 6 7 8 9 10
do
expect "${lpad}7" '%*d' "$i" 7
expect "6${rpad}" '%-*d' "$i" 6
expect "${zpad}5" '%0*d' "$i" 5
return $RVAL
}
define indirect_both 'Using *.* as to get width & precision from args'
q_quoting()
{
setmsg q_quoting
if ! supported q
then
atf_skip '%q format not supported'
return $RVAL
fi
# Testing quoting isn't as straightforward as many of the
# others, as there is no specific form in which the output
# is required to appear
# Instead, we will apply %q to various strings, and then
# process them again in this shell, and see if the string
# we get back is the same as the string we started with.
for string in \
abcd \
'hello world' \
'# a comment ....' \
'' \
'a* b* c*' \
'ls | wc' \
'[<> # | { ~.** } $@]' \
'( who & echo $! )'
do
QUOTED="$(do_printf %q "$string")"
eval "RES=${QUOTED}"
if [ "${RES}" != "${string}" ]
then
atf_fail \
"%q <<${string}>> as <<${QUOTED}>> makes <<${RES}>>"
continue
fi
QUOTED="$(do_printf %-32q "$string")"
if [ ${#QUOTED} -lt 32 ]
then
atf-fail \
"%-32q <<${string}>> short result (${#QUOTED}) <<${QUOTED}>>"
fi
eval "RES=${QUOTED}"
if [ "${RES}" != "${string}" ]
then
atf_fail \
"%-32q <<${string}>> as <<${QUOTED}>> makes <<${RES}>>"
continue
fi
done
# %q is a variant of %s, but using field width (except as above),
# and especially precision makes no sense, and is implrmented so
# badly that testing it would be hopeless. Other flags do nothing.
return $RVAL
}
define q_quoting '%q quote string suitably for sh processing'
NetBSD_extensions()
{
setmsg NetBSD_extensions
if $BUILTIN_TEST
then
# what matters if $TEST_SH is a NetBSD sh
${TEST_SH} -c 'test -n "$NETBSD_SHELL"' || {
atf_skip \
"- ${TEST_SH%% *} is not a (modern) NetBSD shell"
return $RVAL
}
fi
if ! supported '*.*%%_._' 78 66
then
if $BUILTIN_TEST
then
atf_skip \
"- ${TEST_SH%% *} is not a (modern enough) NetBSD shell"
else
atf_skip "- ${PRINTF} is not a (modern) NetBSD printf"
fi
return $RVAL
fi
# Even in the most modern NetBSD printf the data length modifiers
# might not be supported.
# This is unspecified in posix:
# If arg string uses no args, but there are some, run format just once
expect 'hello world' 'hello world' a b c d
# Same as in format_escapes, but for \x (hex) constants
atf_require_prog wc
atf_require_prog od
atf_require_prog tr
for fmt in '\x0' '\x00'
do
if [ $( do_printf "${fmt}" | wc -c ) -ne 1 ]
then
atf_fail \
"printf '${fmt}' did not output exactly 1 character (byte)"
elif [ $(( $( do_printf "${fmt}" | od -A n -to1 ) )) -ne 0 ]
then
RES="$( do_printf "${fmt}" | od -A n -to1 | tr -d ' ')"
atf_fail \
"printf '${fmt}' output was '\\${RES}' should be '\\000'"
fi
done
# We get different results here from the builtin and command
# versions of printf ... OK, as which result is unspecified.
if $BUILTIN_TEST
then
if [ $( do_printf %c '' | wc -c ) -ne 0 ]
then
atf_require_prog sed
RES="$( do_printf %c '' |
od -A n -to1 |
sed -e 's/ [0-9]/\\&/g' -e 's/ //g' )"
atf_fail \
"printf %c '' did not output nothing: got '${RES}'"
fi
else
if [ $( do_printf %c '' | wc -c ) -ne 1 ]
then
atf_require_prog sed
RES="$( do_printf %c '' |
od -A n -to1 |
sed -e 's/ [0-9]/\\&/g' -e 's/ //g' )"
atf_fail \
"printf %c '' did not output nothing: got '${RES}'"
elif [ $(( $( do_printf %c '' | od -A n -to1 ) )) -ne 0 ]
then
RES="$( do_printf %c '' | od -A n -to1 | tr -d ' ')"
atf_fail \
"printf %c '' output was '\\${RES}' should be '\\000'"
fi
fi
return $RVAL
}
define NetBSD_extensions "Local NetBSD additions to printf"
B_string_expand()
{
setmsg B_string_expand
if ! supported B
then
atf_skip "%B format not supported"
return $RVAL
fi
# Even if %B is supported, it is not necessarily *our* %B ...
if $BUILTIN_TEST
then
# what matters if $TEST_SH is a NetBSD sh
${TEST_SH} -c 'test -n "$NETBSD_SHELL"' || {
atf_skip \
"- ${TEST_SH%% *} is not a (modern) NetBSD shell"
return $RVAL
}
else
atf_require_prog uname
SYS="$(uname -s)"
case "${SYS}" in
(NetBSD) ;;
(*) atf_skip "- Not NetBSD (is $SYS), %B format unspecified"
return $RVAL
;;
esac
fi
return $RVAL
}
define B_string_expand "NetBSD specific %B string expansion"
#############################################################################
#############################################################################
#
# The code to make the tests above actually run starts here...
#
# if setup fails, then ignore any test names on command line
# Just run the (one) test that setup() established
setup || set --
NL='
'
# test how the shell we're running handles quoted patterns in vars
# Note: it is not our task here to diagnose the broken shell
B1='\'
B2='\\'
case "${B1}" in
(${B2}) BSL="${B2}";; # This one is correct
(${B1}) BSL="${B1}";; # but some shells can't handle that
(*) BSL=BROKEN_SHELL;; # !!!
esac
if $Running_under_ATF
then
# When in ATF, just add the test cases, and finish, and ATF
# will take care of running everything
atf_init_test_cases() {
for T in $Tests
do
atf_add_test_case "$T"
done
return 0
}
exec 3>&2
else
# When not in AFT, we need to do it all here...