From 95ac199c2f5a463561a2c9aa5d996be540622bae Mon Sep 17 00:00:00 2001 From: Packit Service Date: Jan 04 2021 09:54:25 +0000 Subject: mksh-56c base --- diff --git a/Build.sh b/Build.sh new file mode 100644 index 0000000..ebf4e1c --- /dev/null +++ b/Build.sh @@ -0,0 +1,2789 @@ +#!/bin/sh +srcversion='$MirOS: src/bin/mksh/Build.sh,v 1.731 2018/01/13 21:38:06 tg Exp $' +#- +# Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016, 2017 +# mirabilos +# +# Provided that these terms and disclaimer and all copyright notices +# are retained or reproduced in an accompanying document, permission +# is granted to deal in this work without restriction, including un- +# limited rights to use, publicly perform, distribute, sell, modify, +# merge, give away, or sublicence. +# +# This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to +# the utmost extent permitted by applicable law, neither express nor +# implied; without malicious intent or gross negligence. In no event +# may a licensor, author or contributor be held liable for indirect, +# direct, other damage, loss, or other issues arising in any way out +# of dealing in the work, even if advised of the possibility of such +# damage or existence of a defect, except proven that it results out +# of said person's immediate fault when using the work as intended. +#- +# People analysing the output must whitelist conftest.c for any kind +# of compiler warning checks (mirtoconf is by design not quiet). +# +# Used environment documentation is at the end of this file. + +LC_ALL=C +export LC_ALL + +case $ZSH_VERSION:$VERSION in +:zsh*) ZSH_VERSION=2 ;; +esac + +if test -n "${ZSH_VERSION+x}" && (emulate sh) >/dev/null 2>&1; then + emulate sh + NULLCMD=: +fi + +if test -d /usr/xpg4/bin/. >/dev/null 2>&1; then + # Solaris: some of the tools have weird behaviour, use portable ones + PATH=/usr/xpg4/bin:$PATH + export PATH +fi + +nl=' +' +safeIFS=' ' +safeIFS=" $safeIFS$nl" +IFS=$safeIFS +allu=QWERTYUIOPASDFGHJKLZXCVBNM +alll=qwertyuiopasdfghjklzxcvbnm +alln=0123456789 +alls=______________________________________________________________ + +case `echo a | tr '\201' X` in +X) + # EBCDIC build system + lfcr='\n\r' + ;; +*) + lfcr='\012\015' + ;; +esac + +genopt_die() { + if test -n "$1"; then + echo >&2 "E: $*" + echo >&2 "E: in '$srcfile': '$line'" + else + echo >&2 "E: invalid input in '$srcfile': '$line'" + fi + rm -f "$bn.gen" + exit 1 +} + +genopt_soptc() { + optc=`echo "$line" | sed 's/^[<>]\(.\).*$/\1/'` + test x"$optc" = x'|' && return + optclo=`echo "$optc" | tr $allu $alll` + if test x"$optc" = x"$optclo"; then + islo=1 + else + islo=0 + fi + sym=`echo "$line" | sed 's/^[<>]/|/'` + o_str=$o_str$nl"<$optclo$islo$sym" +} + +genopt_scond() { + case x$cond in + x) + cond= + ;; + x*' '*) + cond=`echo "$cond" | sed 's/^ //'` + cond="#if $cond" + ;; + x'!'*) + cond=`echo "$cond" | sed 's/^!//'` + cond="#ifndef $cond" + ;; + x*) + cond="#ifdef $cond" + ;; + esac +} + +do_genopt() { + srcfile=$1 + test -f "$srcfile" || genopt_die Source file \$srcfile not set. + bn=`basename "$srcfile" | sed 's/.opt$//'` + o_hdr='/* +++ GENERATED FILE +++ DO NOT EDIT +++ */' + o_gen= + o_str= + o_sym= + ddefs= + state=0 + exec <"$srcfile" + IFS= + while IFS= read line; do + IFS=$safeIFS + case $state:$line in + 2:'|'*) + # end of input + o_sym=`echo "$line" | sed 's/^.//'` + o_gen=$o_gen$nl"#undef F0" + o_gen=$o_gen$nl"#undef FN" + o_gen=$o_gen$ddefs + state=3 + ;; + 1:@@) + # start of data block + o_gen=$o_gen$nl"#endif" + o_gen=$o_gen$nl"#ifndef F0" + o_gen=$o_gen$nl"#define F0 FN" + o_gen=$o_gen$nl"#endif" + state=2 + ;; + *:@@*) + genopt_die ;; + 0:/\*-|0:\ \**|0:) + o_hdr=$o_hdr$nl$line + ;; + 0:@*|1:@*) + # start of a definition block + sym=`echo "$line" | sed 's/^@//'` + if test $state = 0; then + o_gen=$o_gen$nl"#if defined($sym)" + else + o_gen=$o_gen$nl"#elif defined($sym)" + fi + ddefs="$ddefs$nl#undef $sym" + state=1 + ;; + 0:*|3:*) + genopt_die ;; + 1:*) + # definition line + o_gen=$o_gen$nl$line + ;; + 2:'<'*'|'*) + genopt_soptc + ;; + 2:'>'*'|'*) + genopt_soptc + cond=`echo "$line" | sed 's/^[^|]*|//'` + genopt_scond + case $optc in + '|') optc=0 ;; + *) optc=\'$optc\' ;; + esac + IFS= read line || genopt_die Unexpected EOF + IFS=$safeIFS + test -n "$cond" && o_gen=$o_gen$nl"$cond" + o_gen=$o_gen$nl"$line, $optc)" + test -n "$cond" && o_gen=$o_gen$nl"#endif" + ;; + esac + done + case $state:$o_sym in + 3:) genopt_die Expected optc sym at EOF ;; + 3:*) ;; + *) genopt_die Missing EOF marker ;; + esac + echo "$o_str" | sort | while IFS='|' read x opts cond; do + IFS=$safeIFS + test -n "$x" || continue + genopt_scond + test -n "$cond" && echo "$cond" + echo "\"$opts\"" + test -n "$cond" && echo "#endif" + done | { + echo "$o_hdr" + echo "#ifndef $o_sym$o_gen" + echo "#else" + cat + echo "#undef $o_sym" + echo "#endif" + } >"$bn.gen" + IFS=$safeIFS + return 0 +} + +if test x"$BUILDSH_RUN_GENOPT" = x"1"; then + set x -G "$srcfile" + shift +fi +if test x"$1" = x"-G"; then + do_genopt "$2" + exit $? +fi + +echo "For the build logs, demonstrate that /dev/null and /dev/tty exist:" +ls -l /dev/null /dev/tty + +v() { + $e "$*" + eval "$@" +} + +vv() { + _c=$1 + shift + $e "\$ $*" 2>&1 + eval "$@" >vv.out 2>&1 + sed "s^${_c} " $fd...$ao $ui$fr$ao$fx" + fx= +} + +# ac_cache label: sets f, fu, fv?=0 +ac_cache() { + f=$1 + fu=`upper $f` + eval fv=\$HAVE_$fu + case $fv in + 0|1) + fx=' (cached)' + return 0 + ;; + esac + fv=0 + return 1 +} + +# ac_testinit label [!] checkif[!]0 [setlabelifcheckis[!]0] useroutput +# returns 1 if value was cached/implied, 0 otherwise: call ac_testdone +ac_testinit() { + if ac_cache $1; then + test x"$2" = x"!" && shift + test x"$2" = x"" || shift + fd=${3-$f} + ac_testdone + return 1 + fi + fc=0 + if test x"$2" = x""; then + ft=1 + else + if test x"$2" = x"!"; then + fc=1 + shift + fi + eval ft=\$HAVE_`upper $2` + shift + fi + fd=${3-$f} + if test $fc = "$ft"; then + fv=$2 + fx=' (implied)' + ac_testdone + return 1 + fi + $e ... $fd + return 0 +} + +# pipe .c | ac_test[n] [!] label [!] checkif[!]0 [setlabelifcheckis[!]0] useroutput +ac_testnnd() { + if test x"$1" = x"!"; then + fr=1 + shift + else + fr=0 + fi + ac_testinit "$@" || return 1 + cat >conftest.c + vv ']' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN conftest.c $LIBS $ccpr" + test $tcfn = no && test -f a.out && tcfn=a.out + test $tcfn = no && test -f a.exe && tcfn=a.exe + test $tcfn = no && test -f conftest.exe && tcfn=conftest.exe + test $tcfn = no && test -f conftest && tcfn=conftest + if test -f $tcfn; then + test 1 = $fr || fv=1 + else + test 0 = $fr || fv=1 + fi + vscan= + if test $phase = u; then + test $ct = gcc && vscan='unrecogni[sz]ed' + test $ct = hpcc && vscan='unsupported' + test $ct = pcc && vscan='unsupported' + test $ct = sunpro && vscan='-e ignored -e turned.off' + fi + test -n "$vscan" && grep $vscan vv.out >/dev/null 2>&1 && fv=$fr + return 0 +} +ac_testn() { + ac_testnnd "$@" || return + rmf conftest.c conftest.o ${tcfn}* vv.out + ac_testdone +} + +# ac_ifcpp cppexpr [!] label [!] checkif[!]0 [setlabelifcheckis[!]0] useroutput +ac_ifcpp() { + expr=$1; shift + ac_testn "$@" <<-EOF + #include + extern int thiswillneverbedefinedIhope(void); + int main(void) { return (isatty(0) + + #$expr + 0 + #else + /* force a failure: expr is false */ + thiswillneverbedefinedIhope() + #endif + ); } +EOF + test x"$1" = x"!" && shift + f=$1 + fu=`upper $f` + eval fv=\$HAVE_$fu + test x"$fv" = x"1" +} + +add_cppflags() { + CPPFLAGS="$CPPFLAGS $*" +} + +ac_cppflags() { + test x"$1" = x"" || fu=$1 + fv=$2 + test x"$2" = x"" && eval fv=\$HAVE_$fu + add_cppflags -DHAVE_$fu=$fv +} + +ac_test() { + ac_testn "$@" + ac_cppflags +} + +# ac_flags [-] add varname cflags [text] [ldflags] +ac_flags() { + if test x"$1" = x"-"; then + shift + hf=1 + else + hf=0 + fi + fa=$1 + vn=$2 + f=$3 + ft=$4 + fl=$5 + test x"$ft" = x"" && ft="if $f can be used" + save_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS $f" + if test -n "$fl"; then + save_LDFLAGS=$LDFLAGS + LDFLAGS="$LDFLAGS $fl" + fi + if test 1 = $hf; then + ac_testn can_$vn '' "$ft" + else + ac_testn can_$vn '' "$ft" <<-'EOF' + /* evil apo'stroph in comment test */ + #include + int main(void) { return (isatty(0)); } + EOF + fi + eval fv=\$HAVE_CAN_`upper $vn` + if test -n "$fl"; then + test 11 = $fa$fv || LDFLAGS=$save_LDFLAGS + fi + test 11 = $fa$fv || CFLAGS=$save_CFLAGS +} + +# ac_header [!] header [prereq ...] +ac_header() { + if test x"$1" = x"!"; then + na=1 + shift + else + na=0 + fi + hf=$1; shift + hv=`echo "$hf" | tr -d "$lfcr" | tr -c $alll$allu$alln $alls` + echo "/* NeXTstep bug workaround */" >x + for i + do + case $i in + _time) + echo '#if HAVE_BOTH_TIME_H' >>x + echo '#include ' >>x + echo '#include ' >>x + echo '#elif HAVE_SYS_TIME_H' >>x + echo '#include ' >>x + echo '#elif HAVE_TIME_H' >>x + echo '#include ' >>x + echo '#endif' >>x + ;; + *) + echo "#include <$i>" >>x + ;; + esac + done + echo "#include <$hf>" >>x + echo '#include ' >>x + echo 'int main(void) { return (isatty(0)); }' >>x + ac_testn "$hv" "" "<$hf>" /dev/null` +case x$srcdir in +x) + srcdir=. + ;; +*\ *|*" "*|*"$nl"*) + echo >&2 Source directory should not contain space or tab or newline. + echo >&2 Errors may occur. + ;; +*"'"*) + echo Source directory must not contain single quotes. + exit 1 + ;; +esac +dstversion=`sed -n '/define MKSH_VERSION/s/^.*"\([^"]*\)".*$/\1/p' "$srcdir/sh.h"` +add_cppflags -DMKSH_BUILDSH + +e=echo +r=0 +eq=0 +pm=0 +cm=normal +optflags=-std-compile-opts +check_categories= +last= +tfn= +legacy=0 +textmode=0 +ebcdic=false + +for i +do + case $last:$i in + c:combine|c:dragonegg|c:llvm|c:lto) + cm=$i + last= + ;; + c:*) + echo "$me: Unknown option -c '$i'!" >&2 + exit 1 + ;; + o:*) + optflags=$i + last= + ;; + t:*) + tfn=$i + last= + ;; + :-c) + last=c + ;; + :-E) + ebcdic=true + ;; + :-G) + echo "$me: Do not call me with '-G'!" >&2 + exit 1 + ;; + :-g) + # checker, debug, valgrind build + add_cppflags -DDEBUG + CFLAGS="$CFLAGS -g3 -fno-builtin" + ;; + :-j) + pm=1 + ;; + :-L) + legacy=1 + ;; + :+L) + legacy=0 + ;; + :-M) + cm=makefile + ;; + :-O) + optflags=-std-compile-opts + ;; + :-o) + last=o + ;; + :-Q) + eq=1 + ;; + :-r) + r=1 + ;; + :-T) + textmode=1 + ;; + :+T) + textmode=0 + ;; + :-t) + last=t + ;; + :-v) + echo "Build.sh $srcversion" + echo "for mksh $dstversion" + exit 0 + ;; + :*) + echo "$me: Unknown option '$i'!" >&2 + exit 1 + ;; + *) + echo "$me: Unknown option -'$last' '$i'!" >&2 + exit 1 + ;; + esac +done +if test -n "$last"; then + echo "$me: Option -'$last' not followed by argument!" >&2 + exit 1 +fi + +test -z "$tfn" && if test $legacy = 0; then + tfn=mksh +else + tfn=lksh +fi +if test -d $tfn || test -d $tfn.exe; then + echo "$me: Error: ./$tfn is a directory!" >&2 + exit 1 +fi +rmf a.exe* a.out* conftest.c conftest.exe* *core core.* ${tfn}* *.bc *.dbg \ + *.ll *.o *.gen *.cat1 Rebuild.sh lft no signames.inc test.sh x vv.out + +SRCS="lalloc.c edit.c eval.c exec.c expr.c funcs.c histrap.c jobs.c" +SRCS="$SRCS lex.c main.c misc.c shf.c syn.c tree.c var.c" + +if test $legacy = 0; then + check_categories="$check_categories shell:legacy-no int:32" +else + check_categories="$check_categories shell:legacy-yes" + add_cppflags -DMKSH_LEGACY_MODE +fi + +if $ebcdic; then + add_cppflags -DMKSH_EBCDIC +fi + +if test $textmode = 0; then + check_categories="$check_categories shell:textmode-no shell:binmode-yes" +else + check_categories="$check_categories shell:textmode-yes shell:binmode-no" + add_cppflags -DMKSH_WITH_TEXTMODE +fi + +if test x"$srcdir" = x"."; then + CPPFLAGS="-I. $CPPFLAGS" +else + CPPFLAGS="-I. -I'$srcdir' $CPPFLAGS" +fi +test -n "$LDSTATIC" && if test -n "$LDFLAGS"; then + LDFLAGS="$LDFLAGS $LDSTATIC" +else + LDFLAGS=$LDSTATIC +fi + +if test -z "$TARGET_OS"; then + x=`uname -s 2>/dev/null || uname` + test x"$x" = x"`uname -n 2>/dev/null`" || TARGET_OS=$x +fi +if test -z "$TARGET_OS"; then + echo "$me: Set TARGET_OS, your uname is broken!" >&2 + exit 1 +fi +oswarn= +ccpc=-Wc, +ccpl=-Wl, +tsts= +ccpr='|| for _f in ${tcfn}*; do case $_f in Build.sh|check.pl|check.t|dot.mkshrc|*.1|*.c|*.h|*.ico|*.opt) ;; *) rm -f "$_f" ;; esac; done' + +# Evil hack +if test x"$TARGET_OS" = x"Android"; then + check_categories="$check_categories android" + TARGET_OS=Linux +fi + +# Evil OS +if test x"$TARGET_OS" = x"Minix"; then + echo >&2 " +WARNING: additional checks before running Build.sh required! +You can avoid these by calling Build.sh correctly, see below. +" + cat >conftest.c <<'EOF' +#include +const char * +#ifdef _NETBSD_SOURCE +ct="Ninix3" +#else +ct="Minix3" +#endif +; +EOF + ct=unknown + vv ']' "${CC-cc} -E $CFLAGS $CPPFLAGS $NOWARN conftest.c | grep ct= | tr -d \\\\015 >x" + sed 's/^/[ /' x + eval `cat x` + rmf x vv.out + case $ct in + Minix3|Ninix3) + echo >&2 " +Warning: you set TARGET_OS to $TARGET_OS but that is ambiguous. +Please set it to either Minix3 or Ninix3, whereas the latter is +all versions of Minix with even partial NetBSD(R) userland. The +value determined from your compiler for the current compilation +(which may be wrong) is: $ct +" + TARGET_OS=$ct + ;; + *) + echo >&2 " +Warning: you set TARGET_OS to $TARGET_OS but that is ambiguous. +Please set it to either Minix3 or Ninix3, whereas the latter is +all versions of Minix with even partial NetBSD(R) userland. The +proper value couldn't be determined, continue at your own risk. +" + ;; + esac +fi + +# Configuration depending on OS revision, on OSes that need them +case $TARGET_OS in +NEXTSTEP) + test x"$TARGET_OSREV" = x"" && TARGET_OSREV=`hostinfo 2>&1 | \ + grep 'NeXT Mach [0-9][0-9.]*:' | \ + sed 's/^.*NeXT Mach \([0-9][0-9.]*\):.*$/\1/'` + ;; +QNX|SCO_SV) + test x"$TARGET_OSREV" = x"" && TARGET_OSREV=`uname -r` + ;; +esac + +# Configuration depending on OS name +case $TARGET_OS in +386BSD) + : "${HAVE_CAN_OTWO=0}" + add_cppflags -DMKSH_NO_SIGSETJMP + add_cppflags -DMKSH_TYPEDEF_SIG_ATOMIC_T=int + ;; +AIX) + add_cppflags -D_ALL_SOURCE + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +BeOS) + case $KSH_VERSION in + *MIRBSD\ KSH*) + oswarn="; it has minor issues" + ;; + *) + oswarn="; you must recompile mksh with" + oswarn="$oswarn${nl}itself in a second stage" + ;; + esac + # BeOS has no real tty either + add_cppflags -DMKSH_UNEMPLOYED + add_cppflags -DMKSH_DISABLE_TTY_WARNING + # BeOS doesn't have different UIDs and GIDs + add_cppflags -DMKSH__NO_SETEUGID + ;; +BSD/OS) + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +Coherent) + oswarn="; it has major issues" + add_cppflags -DMKSH__NO_SYMLINK + check_categories="$check_categories nosymlink" + add_cppflags -DMKSH__NO_SETEUGID + add_cppflags -DMKSH_DISABLE_TTY_WARNING + ;; +CYGWIN*) + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +Darwin) + add_cppflags -D_DARWIN_C_SOURCE + ;; +DragonFly) + ;; +FreeBSD) + ;; +FreeMiNT) + oswarn="; it has minor issues" + add_cppflags -D_GNU_SOURCE + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +GNU) + case $CC in + *tendracc*) ;; + *) add_cppflags -D_GNU_SOURCE ;; + esac + add_cppflags -DSETUID_CAN_FAIL_WITH_EAGAIN + # define MKSH__NO_PATH_MAX to use Hurd-only functions + add_cppflags -DMKSH__NO_PATH_MAX + ;; +GNU/kFreeBSD) + case $CC in + *tendracc*) ;; + *) add_cppflags -D_GNU_SOURCE ;; + esac + add_cppflags -DSETUID_CAN_FAIL_WITH_EAGAIN + ;; +Haiku) + add_cppflags -DMKSH_ASSUME_UTF8 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=0 + ;; +Harvey) + add_cppflags -D_POSIX_SOURCE + add_cppflags -D_LIMITS_EXTENSION + add_cppflags -D_BSD_EXTENSION + add_cppflags -D_SUSV2_SOURCE + add_cppflags -D_GNU_SOURCE + add_cppflags -DMKSH_ASSUME_UTF8 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=0 + add_cppflags -DMKSH__NO_SYMLINK + check_categories="$check_categories nosymlink" + add_cppflags -DMKSH_NO_CMDLINE_EDITING + add_cppflags -DMKSH__NO_SETEUGID + oswarn=' and will currently not work' + add_cppflags -DMKSH_UNEMPLOYED + add_cppflags -DMKSH_NOPROSPECTOFWORK + # these taken from Harvey-OS github and need re-checking + add_cppflags -D_setjmp=setjmp -D_longjmp=longjmp + : "${HAVE_CAN_NO_EH_FRAME=0}" + : "${HAVE_CAN_FNOSTRICTALIASING=0}" + : "${HAVE_CAN_FSTACKPROTECTORSTRONG=0}" + ;; +HP-UX) + ;; +Interix) + ccpc='-X ' + ccpl='-Y ' + add_cppflags -D_ALL_SOURCE + : "${LIBS=-lcrypt}" + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +IRIX*) + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +Jehanne) + add_cppflags -DMKSH_ASSUME_UTF8 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=0 + add_cppflags -DMKSH__NO_SYMLINK + check_categories="$check_categories nosymlink" + add_cppflags -DMKSH_NO_CMDLINE_EDITING + add_cppflags -DMKSH_DISABLE_REVOKE_WARNING + add_cppflags '-D_PATH_DEFPATH=\"/cmd\"' + add_cppflags '-DMKSH_DEFAULT_EXECSHELL=\"/cmd/mksh\"' + add_cppflags '-DMKSH_DEFAULT_PROFILEDIR=\"/cfg/mksh\"' + add_cppflags '-DMKSH_ENVDIR=\"/env\"' + SRCS="$SRCS jehanne.c" + ;; +Linux) + case $CC in + *tendracc*) ;; + *) add_cppflags -D_GNU_SOURCE ;; + esac + add_cppflags -DSETUID_CAN_FAIL_WITH_EAGAIN + : "${HAVE_REVOKE=0}" + ;; +LynxOS) + oswarn="; it has minor issues" + ;; +MidnightBSD) + ;; +Minix-vmd) + add_cppflags -DMKSH__NO_SETEUGID + add_cppflags -DMKSH_UNEMPLOYED + add_cppflags -D_MINIX_SOURCE + oldish_ed=no-stderr-ed # no /bin/ed, maybe see below + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +Minix3) + add_cppflags -DMKSH_UNEMPLOYED + add_cppflags -DMKSH_NO_LIMITS + add_cppflags -D_POSIX_SOURCE -D_POSIX_1_SOURCE=2 -D_MINIX + oldish_ed=no-stderr-ed # /usr/bin/ed(!) is broken + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +MirBSD) + ;; +MSYS_*) + add_cppflags -DMKSH_ASSUME_UTF8=0 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=1 + # almost same as CYGWIN* (from RT|Chatzilla) + : "${HAVE_SETLOCALE_CTYPE=0}" + # broken on this OE (from ir0nh34d) + : "${HAVE_STDINT_H=0}" + ;; +NetBSD) + ;; +NEXTSTEP) + add_cppflags -D_NEXT_SOURCE + add_cppflags -D_POSIX_SOURCE + : "${AWK=gawk}" + : "${CC=cc -posix}" + add_cppflags -DMKSH_NO_SIGSETJMP + # NeXTstep cannot get a controlling tty + add_cppflags -DMKSH_UNEMPLOYED + case $TARGET_OSREV in + 4.2*) + # OpenStep 4.2 is broken by default + oswarn="; it needs libposix.a" + ;; + esac + ;; +Ninix3) + # similar to Minix3 + add_cppflags -DMKSH_UNEMPLOYED + add_cppflags -DMKSH_NO_LIMITS + # but no idea what else could be needed + oswarn="; it has unknown issues" + ;; +OpenBSD) + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +OS/2) + add_cppflags -DMKSH_ASSUME_UTF8=0 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=1 + HAVE_TERMIOS_H=0 + HAVE_MKNOD=0 # setmode() incompatible + oswarn="; it is being ported" + check_categories="$check_categories nosymlink" + : "${CC=gcc}" + : "${SIZE=: size}" + SRCS="$SRCS os2.c" + add_cppflags -DMKSH_UNEMPLOYED + add_cppflags -DMKSH_NOPROSPECTOFWORK + add_cppflags -DMKSH_NO_LIMITS + add_cppflags -DMKSH_DOSPATH + if test $textmode = 0; then + x='dis' + y='standard OS/2 tools' + else + x='en' + y='standard Unix mksh and other tools' + fi + echo >&2 " +OS/2 Note: mksh can be built with or without 'textmode'. +Without 'textmode' it will behave like a standard Unix utility, +compatible to mksh on all other platforms, using only ASCII LF +(0x0A) as line ending character. This is supported by the mksh +upstream developer. +With 'textmode', mksh will be modified to behave more like other +OS/2 utilities, supporting ASCII CR+LF (0x0D 0x0A) as line ending +at the cost of deviation from standard mksh. This is supported by +the mksh-os2 porter. + +] You are currently compiling with textmode ${x}abled, introducing +] incompatibilities with $y. +" + ;; +OS/390) + add_cppflags -DMKSH_ASSUME_UTF8=0 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=1 + : "${CC=xlc}" + : "${SIZE=: size}" + add_cppflags -DMKSH_FOR_Z_OS + add_cppflags -D_ALL_SOURCE + oswarn='; EBCDIC support is incomplete' + ;; +OSF1) + HAVE_SIG_T=0 # incompatible + add_cppflags -D_OSF_SOURCE + add_cppflags -D_POSIX_C_SOURCE=200112L + add_cppflags -D_XOPEN_SOURCE=600 + add_cppflags -D_XOPEN_SOURCE_EXTENDED + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +Plan9) + add_cppflags -D_POSIX_SOURCE + add_cppflags -D_LIMITS_EXTENSION + add_cppflags -D_BSD_EXTENSION + add_cppflags -D_SUSV2_SOURCE + add_cppflags -DMKSH_ASSUME_UTF8 + HAVE_ISSET_MKSH_ASSUME_UTF8=1 + HAVE_ISOFF_MKSH_ASSUME_UTF8=0 + add_cppflags -DMKSH__NO_SYMLINK + check_categories="$check_categories nosymlink" + add_cppflags -DMKSH_NO_CMDLINE_EDITING + add_cppflags -DMKSH__NO_SETEUGID + oswarn=' and will currently not work' + add_cppflags -DMKSH_UNEMPLOYED + # this is for detecting kencc + add_cppflags -DMKSH_MAYBE_KENCC + ;; +PW32*) + HAVE_SIG_T=0 # incompatible + oswarn=' and will currently not work' + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +QNX) + add_cppflags -D__NO_EXT_QNX + add_cppflags -D__EXT_UNIX_MISC + case $TARGET_OSREV in + [012345].*|6.[0123].*|6.4.[01]) + oldish_ed=no-stderr-ed # oldish /bin/ed is broken + ;; + esac + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +SCO_SV) + case $TARGET_OSREV in + 3.2*) + # SCO OpenServer 5 + add_cppflags -DMKSH_UNEMPLOYED + ;; + 5*) + # SCO OpenServer 6 + ;; + *) + oswarn='; this is an unknown version of' + oswarn="$oswarn$nl$TARGET_OS ${TARGET_OSREV}, please tell me what to do" + ;; + esac + : "${HAVE_SYS_SIGLIST=0}${HAVE__SYS_SIGLIST=0}" + ;; +skyos) + oswarn="; it has minor issues" + ;; +SunOS) + add_cppflags -D_BSD_SOURCE + add_cppflags -D__EXTENSIONS__ + ;; +syllable) + add_cppflags -D_GNU_SOURCE + add_cppflags -DMKSH_NO_SIGSUSPEND + oswarn=' and will currently not work' + ;; +ULTRIX) + : "${CC=cc -YPOSIX}" + add_cppflags -DMKSH_TYPEDEF_SSIZE_T=int + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +UnixWare|UNIX_SV) + # SCO UnixWare + : "${HAVE_SYS_SIGLIST=0}${HAVE__SYS_SIGLIST=0}" + ;; +UWIN*) + ccpc='-Yc,' + ccpl='-Yl,' + tsts=" 3<>/dev/tty" + oswarn="; it will compile, but the target" + oswarn="$oswarn${nl}platform itself is very flakey/unreliable" + : "${HAVE_SETLOCALE_CTYPE=0}" + ;; +_svr4) + # generic target for SVR4 Unix with uname -s = uname -n + # this duplicates the * target below + oswarn='; it may or may not work' + test x"$TARGET_OSREV" = x"" && TARGET_OSREV=`uname -r` + ;; +*) + oswarn='; it may or may not work' + test x"$TARGET_OSREV" = x"" && TARGET_OSREV=`uname -r` + ;; +esac + +: "${HAVE_MKNOD=0}" + +: "${AWK=awk}${CC=cc}${NROFF=nroff}${SIZE=size}" +test 0 = $r && echo | $NROFF -v 2>&1 | grep GNU >/dev/null 2>&1 && \ + echo | $NROFF -c >/dev/null 2>&1 && NROFF="$NROFF -c" + +# this aids me in tracing FTBFSen without access to the buildd +$e "Hi from$ao $bi$srcversion$ao on:" +case $TARGET_OS in +AIX) + vv '|' "oslevel >&2" + vv '|' "uname -a >&2" + ;; +Darwin) + vv '|' "hwprefs machine_type os_type os_class >&2" + vv '|' "sw_vers >&2" + vv '|' "system_profiler SPSoftwareDataType SPHardwareDataType >&2" + vv '|' "/bin/sh --version >&2" + vv '|' "xcodebuild -version >&2" + vv '|' "uname -a >&2" + vv '|' "sysctl kern.version hw.machine hw.model hw.memsize hw.availcpu hw.cpufrequency hw.byteorder hw.cpu64bit_capable >&2" + ;; +IRIX*) + vv '|' "uname -a >&2" + vv '|' "hinv -v >&2" + ;; +OSF1) + vv '|' "uname -a >&2" + vv '|' "/usr/sbin/sizer -v >&2" + ;; +SCO_SV|UnixWare|UNIX_SV) + vv '|' "uname -a >&2" + vv '|' "uname -X >&2" + ;; +*) + vv '|' "uname -a >&2" + ;; +esac +test -z "$oswarn" || echo >&2 " +Warning: mksh has not yet been ported to or tested on your +operating system '$TARGET_OS'$oswarn. If you can provide +a shell account to the developer, this may improve; please +drop us a success or failure notice or even send in diffs. +" +$e "$bi$me: Building the MirBSD Korn Shell$ao $ui$dstversion$ao on $TARGET_OS ${TARGET_OSREV}..." + +# +# Start of mirtoconf checks +# +$e $bi$me: Scanning for functions... please ignore any errors.$ao + +# +# Compiler: which one? +# +# notes: +# - ICC defines __GNUC__ too +# - GCC defines __hpux too +# - LLVM+clang defines __GNUC__ too +# - nwcc defines __GNUC__ too +CPP="$CC -E" +$e ... which compiler type seems to be used +cat >conftest.c <<'EOF' +const char * +#if defined(__ICC) || defined(__INTEL_COMPILER) +ct="icc" +#elif defined(__xlC__) || defined(__IBMC__) +ct="xlc" +#elif defined(__SUNPRO_C) +ct="sunpro" +#elif defined(__ACK__) +ct="ack" +#elif defined(__BORLANDC__) +ct="bcc" +#elif defined(__WATCOMC__) +ct="watcom" +#elif defined(__MWERKS__) +ct="metrowerks" +#elif defined(__HP_cc) +ct="hpcc" +#elif defined(__DECC) || (defined(__osf__) && !defined(__GNUC__)) +ct="dec" +#elif defined(__PGI) +ct="pgi" +#elif defined(__DMC__) +ct="dmc" +#elif defined(_MSC_VER) +ct="msc" +#elif defined(__ADSPBLACKFIN__) || defined(__ADSPTS__) || defined(__ADSP21000__) +ct="adsp" +#elif defined(__IAR_SYSTEMS_ICC__) +ct="iar" +#elif defined(SDCC) +ct="sdcc" +#elif defined(__PCC__) +ct="pcc" +#elif defined(__TenDRA__) +ct="tendra" +#elif defined(__TINYC__) +ct="tcc" +#elif defined(__llvm__) && defined(__clang__) +ct="clang" +#elif defined(__NWCC__) +ct="nwcc" +#elif defined(__GNUC__) +ct="gcc" +#elif defined(_COMPILER_VERSION) +ct="mipspro" +#elif defined(__sgi) +ct="mipspro" +#elif defined(__hpux) || defined(__hpua) +ct="hpcc" +#elif defined(__ultrix) +ct="ucode" +#elif defined(__USLC__) +ct="uslc" +#elif defined(__LCC__) +ct="lcc" +#elif defined(MKSH_MAYBE_KENCC) +/* and none of the above matches */ +ct="kencc" +#else +ct="unknown" +#endif +; +const char * +#if defined(__KLIBC__) && !defined(__OS2__) +et="klibc" +#else +et="unknown" +#endif +; +EOF +ct=untested +et=untested +vv ']' "$CPP $CFLAGS $CPPFLAGS $NOWARN conftest.c | \ + sed -n '/^ *[ce]t *= */s/^ *\([ce]t\) *= */\1=/p' | tr -d \\\\015 >x" +sed 's/^/[ /' x +eval `cat x` +rmf x vv.out +cat >conftest.c <<'EOF' +#include +int main(void) { return (isatty(0)); } +EOF +case $ct in +ack) + # work around "the famous ACK const bug" + CPPFLAGS="-Dconst= $CPPFLAGS" + ;; +adsp) + echo >&2 'Warning: Analog Devices C++ compiler for Blackfin, TigerSHARC + and SHARC (21000) DSPs detected. This compiler has not yet + been tested for compatibility with mksh. Continue at your + own risk, please report success/failure to the developers.' + ;; +bcc) + echo >&2 "Warning: Borland C++ Builder detected. This compiler might + produce broken executables. Continue at your own risk, + please report success/failure to the developers." + ;; +clang) + # does not work with current "ccc" compiler driver + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -version" + # one of these two works, for now + vv '|' "${CLANG-clang} -version" + vv '|' "${CLANG-clang} --version" + # ensure compiler and linker are in sync unless overridden + case $CCC_CC:$CCC_LD in + :*) ;; + *:) CCC_LD=$CCC_CC; export CCC_LD ;; + esac + : "${HAVE_STRING_POOLING=i1}" + ;; +dec) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -V" + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -Wl,-V conftest.c $LIBS" + ;; +dmc) + echo >&2 "Warning: Digital Mars Compiler detected. When running under" + echo >&2 " UWIN, mksh tends to be unstable due to the limitations" + echo >&2 " of this platform. Continue at your own risk," + echo >&2 " please report success/failure to the developers." + ;; +gcc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -v conftest.c $LIBS" + vv '|' 'echo `$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS \ + -dumpmachine` gcc`$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN \ + $LIBS -dumpversion`' + : "${HAVE_STRING_POOLING=i2}" + ;; +hpcc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -V conftest.c $LIBS" + ;; +iar) + echo >&2 'Warning: IAR Systems (http://www.iar.com) compiler for embedded + systems detected. This unsupported compiler has not yet + been tested for compatibility with mksh. Continue at your + own risk, please report success/failure to the developers.' + ;; +icc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -V" + ;; +kencc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -v conftest.c $LIBS" + ;; +lcc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -v conftest.c $LIBS" + add_cppflags -D__inline__=__inline + ;; +metrowerks) + echo >&2 'Warning: Metrowerks C compiler detected. This has not yet + been tested for compatibility with mksh. Continue at your + own risk, please report success/failure to the developers.' + ;; +mipspro) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -version" + ;; +msc) + ccpr= # errorlevels are not reliable + case $TARGET_OS in + Interix) + if [[ -n $C89_COMPILER ]]; then + C89_COMPILER=`ntpath2posix -c "$C89_COMPILER"` + else + C89_COMPILER=CL.EXE + fi + if [[ -n $C89_LINKER ]]; then + C89_LINKER=`ntpath2posix -c "$C89_LINKER"` + else + C89_LINKER=LINK.EXE + fi + vv '|' "$C89_COMPILER /HELP >&2" + vv '|' "$C89_LINKER /LINK >&2" + ;; + esac + ;; +nwcc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -version" + ;; +pcc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -v" + ;; +pgi) + echo >&2 'Warning: PGI detected. This unknown compiler has not yet + been tested for compatibility with mksh. Continue at your + own risk, please report success/failure to the developers.' + ;; +sdcc) + echo >&2 'Warning: sdcc (http://sdcc.sourceforge.net), the small devices + C compiler for embedded systems detected. This has not yet + been tested for compatibility with mksh. Continue at your + own risk, please report success/failure to the developers.' + ;; +sunpro) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -V conftest.c $LIBS" + ;; +tcc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -v" + ;; +tendra) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -V 2>&1 | \ + grep -F -i -e version -e release" + ;; +ucode) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -V" + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -Wl,-V conftest.c $LIBS" + ;; +uslc) + case $TARGET_OS:$TARGET_OSREV in + SCO_SV:3.2*) + # SCO OpenServer 5 + CFLAGS="$CFLAGS -g" + : "${HAVE_CAN_OTWO=0}${HAVE_CAN_OPTIMISE=0}" + ;; + esac + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -V conftest.c $LIBS" + ;; +watcom) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -v conftest.c $LIBS" + ;; +xlc) + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -qversion" + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN $LIBS -qversion=verbose" + vv '|' "ld -V" + ;; +*) + test x"$ct" = x"untested" && $e "!!! detecting preprocessor failed" + ct=unknown + vv "$CC --version" + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -v conftest.c $LIBS" + vv '|' "$CC $CFLAGS $CPPFLAGS $LDFLAGS $NOWARN -V conftest.c $LIBS" + ;; +esac +case $cm in +dragonegg|llvm) + vv '|' "llc -version" + ;; +esac +etd=" on $et" +case $et in +klibc) + add_cppflags -DMKSH_NO_LIMITS + ;; +unknown) + # nothing special detected, don’t worry + etd= + ;; +*) + # huh? + ;; +esac +$e "$bi==> which compiler type seems to be used...$ao $ui$ct$etd$ao" +rmf conftest.c conftest.o conftest a.out* a.exe* conftest.exe* vv.out + +# +# Compiler: works as-is, with -Wno-error and -Werror +# +save_NOWARN=$NOWARN +NOWARN= +DOWARN= +ac_flags 0 compiler_works '' 'if the compiler works' +test 1 = $HAVE_CAN_COMPILER_WORKS || exit 1 +HAVE_COMPILER_KNOWN=0 +test $ct = unknown || HAVE_COMPILER_KNOWN=1 +if ac_ifcpp 'if 0' compiler_fails '' \ + 'if the compiler does not fail correctly'; then + save_CFLAGS=$CFLAGS + : "${HAVE_CAN_DELEXE=x}" + case $ct in + dec) + CFLAGS="$CFLAGS ${ccpl}-non_shared" + ac_testn can_delexe compiler_fails 0 'for the -non_shared linker option' <<-EOF + #include + int main(void) { return (isatty(0)); } + EOF + ;; + dmc) + CFLAGS="$CFLAGS ${ccpl}/DELEXECUTABLE" + ac_testn can_delexe compiler_fails 0 'for the /DELEXECUTABLE linker option' <<-EOF + #include + int main(void) { return (isatty(0)); } + EOF + ;; + *) + exit 1 + ;; + esac + test 1 = $HAVE_CAN_DELEXE || CFLAGS=$save_CFLAGS + ac_testn compiler_still_fails '' 'if the compiler still does not fail correctly' <<-EOF + EOF + test 1 = $HAVE_COMPILER_STILL_FAILS && exit 1 +fi +if ac_ifcpp 'ifdef __TINYC__' couldbe_tcc '!' compiler_known 0 \ + 'if this could be tcc'; then + ct=tcc + CPP='cpp -D__TINYC__' + HAVE_COMPILER_KNOWN=1 +fi + +case $ct in +bcc) + save_NOWARN="${ccpc}-w" + DOWARN="${ccpc}-w!" + ;; +dec) + # -msg_* flags not used yet, or is -w2 correct? + ;; +dmc) + save_NOWARN="${ccpc}-w" + DOWARN="${ccpc}-wx" + ;; +hpcc) + save_NOWARN= + DOWARN=+We + ;; +kencc) + save_NOWARN= + DOWARN= + ;; +mipspro) + save_NOWARN= + DOWARN="-diag_error 1-10000" + ;; +msc) + save_NOWARN="${ccpc}/w" + DOWARN="${ccpc}/WX" + ;; +sunpro) + test x"$save_NOWARN" = x"" && save_NOWARN='-errwarn=%none' + ac_flags 0 errwarnnone "$save_NOWARN" + test 1 = $HAVE_CAN_ERRWARNNONE || save_NOWARN= + ac_flags 0 errwarnall "-errwarn=%all" + test 1 = $HAVE_CAN_ERRWARNALL && DOWARN="-errwarn=%all" + ;; +tendra) + save_NOWARN=-w + ;; +ucode) + save_NOWARN= + DOWARN=-w2 + ;; +watcom) + save_NOWARN= + DOWARN=-Wc,-we + ;; +xlc) + case $TARGET_OS in + OS/390) + save_NOWARN=-qflag=e + DOWARN=-qflag=i + ;; + *) + save_NOWARN=-qflag=i:e + DOWARN=-qflag=i:i + ;; + esac + ;; +*) + test x"$save_NOWARN" = x"" && save_NOWARN=-Wno-error + ac_flags 0 wnoerror "$save_NOWARN" + test 1 = $HAVE_CAN_WNOERROR || save_NOWARN= + ac_flags 0 werror -Werror + test 1 = $HAVE_CAN_WERROR && DOWARN=-Werror + test $ct = icc && DOWARN="$DOWARN -wd1419" + ;; +esac +NOWARN=$save_NOWARN + +# +# Compiler: extra flags (-O2 -f* -W* etc.) +# +i=`echo :"$orig_CFLAGS" | sed 's/^://' | tr -c -d $alll$allu$alln` +# optimisation: only if orig_CFLAGS is empty +test x"$i" = x"" && case $ct in +hpcc) + phase=u + ac_flags 1 otwo +O2 + phase=x + ;; +kencc|tcc|tendra) + # no special optimisation + ;; +sunpro) + cat >x <<-'EOF' + #include + int main(void) { return (isatty(0)); } + #define __IDSTRING_CONCAT(l,p) __LINTED__ ## l ## _ ## p + #define __IDSTRING_EXPAND(l,p) __IDSTRING_CONCAT(l,p) + #define pad void __IDSTRING_EXPAND(__LINE__,x)(void) { } + EOF + yes pad | head -n 256 >>x + ac_flags - 1 otwo -xO2 x + ac_flags - 1 stackon "${ccpc}/GZ" 'if stack checks can be enabled' not found. + # CCN3944: Attribute "__foo__" is not supported and is ignored. + # CCN3963: The attribute "foo" is not a valid variable attribute and is ignored. + ac_flags 1 halton '-qhaltonmsg=CCN3296 -qhaltonmsg=CCN3944 -qhaltonmsg=CCN3963' + # CCN3290: Unknown macro name FOO on #undef directive. + # CCN4108: The use of keyword '__attribute__' is non-portable. + ac_flags 1 supprss '-qsuppress=CCN3290 -qsuppress=CCN4108' + ;; + *) + ac_flags 1 rodata '-qro -qroconst -qroptr' + ac_flags 1 rtcheck -qcheck=all + #ac_flags 1 rtchkc -qextchk # reported broken + ac_flags 1 wformat '-qformat=all -qformat=nozln' + ;; + esac + #ac_flags 1 wp64 -qwarn64 # too verbose for now + ;; +esac +# flags common to a subset of compilers (run with -Werror on gcc) +if test 1 = $i; then + ac_flags 1 wall -Wall + ac_flags 1 fwrapv -fwrapv +fi + +# “on demand” means: GCC version >= 4 +fd='if to rely on compiler for string pooling' +ac_cache string_pooling || case $HAVE_STRING_POOLING in +2) fx=' (on demand, cached)' ;; +i1) fv=1 ;; +i2) fv=2; fx=' (on demand)' ;; +esac +ac_testdone +test x"$HAVE_STRING_POOLING" = x"0" || ac_cppflags + +phase=x +# The following tests run with -Werror or similar (all compilers) if possible +NOWARN=$DOWARN +test $ct = pcc && phase=u + +# +# Compiler: check for stuff that only generates warnings +# +ac_test attribute_bounded '' 'for __attribute__((__bounded__))' <<-'EOF' + #if defined(__TenDRA__) || (defined(__GNUC__) && (__GNUC__ < 2)) + extern int thiswillneverbedefinedIhope(void); + /* force a failure: TenDRA and gcc 1.42 have false positive here */ + int main(void) { return (thiswillneverbedefinedIhope()); } + #else + #include + #undef __attribute__ + int xcopy(const void *, void *, size_t) + __attribute__((__bounded__(__buffer__, 1, 3))) + __attribute__((__bounded__(__buffer__, 2, 3))); + int main(int ac, char *av[]) { return (xcopy(av[0], av[--ac], 1)); } + int xcopy(const void *s, void *d, size_t n) { + /* + * if memmove does not exist, we are not on a system + * with GCC with __bounded__ attribute either so poo + */ + memmove(d, s, n); return ((int)n); + } + #endif +EOF +ac_test attribute_format '' 'for __attribute__((__format__))' <<-'EOF' + #if defined(__TenDRA__) || (defined(__GNUC__) && (__GNUC__ < 2)) + extern int thiswillneverbedefinedIhope(void); + /* force a failure: TenDRA and gcc 1.42 have false positive here */ + int main(void) { return (thiswillneverbedefinedIhope()); } + #else + #define fprintf printfoo + #include + #undef __attribute__ + #undef fprintf + extern int fprintf(FILE *, const char *format, ...) + __attribute__((__format__(__printf__, 2, 3))); + int main(int ac, char **av) { return (fprintf(stderr, "%s%d", *av, ac)); } + #endif +EOF +ac_test attribute_noreturn '' 'for __attribute__((__noreturn__))' <<-'EOF' + #if defined(__TenDRA__) || (defined(__GNUC__) && (__GNUC__ < 2)) + extern int thiswillneverbedefinedIhope(void); + /* force a failure: TenDRA and gcc 1.42 have false positive here */ + int main(void) { return (thiswillneverbedefinedIhope()); } + #else + #include + #undef __attribute__ + void fnord(void) __attribute__((__noreturn__)); + int main(void) { fnord(); } + void fnord(void) { exit(0); } + #endif +EOF +ac_test attribute_pure '' 'for __attribute__((__pure__))' <<-'EOF' + #if defined(__TenDRA__) || (defined(__GNUC__) && (__GNUC__ < 2)) + extern int thiswillneverbedefinedIhope(void); + /* force a failure: TenDRA and gcc 1.42 have false positive here */ + int main(void) { return (thiswillneverbedefinedIhope()); } + #else + #include + #undef __attribute__ + int foo(const char *) __attribute__((__pure__)); + int main(int ac, char **av) { return (foo(av[ac - 1]) + isatty(0)); } + int foo(const char *s) { return ((int)s[0]); } + #endif +EOF +ac_test attribute_unused '' 'for __attribute__((__unused__))' <<-'EOF' + #if defined(__TenDRA__) || (defined(__GNUC__) && (__GNUC__ < 2)) + extern int thiswillneverbedefinedIhope(void); + /* force a failure: TenDRA and gcc 1.42 have false positive here */ + int main(void) { return (thiswillneverbedefinedIhope()); } + #else + #include + #undef __attribute__ + int main(int ac __attribute__((__unused__)), char **av + __attribute__((__unused__))) { return (isatty(0)); } + #endif +EOF +ac_test attribute_used '' 'for __attribute__((__used__))' <<-'EOF' + #if defined(__TenDRA__) || (defined(__GNUC__) && (__GNUC__ < 2)) + extern int thiswillneverbedefinedIhope(void); + /* force a failure: TenDRA and gcc 1.42 have false positive here */ + int main(void) { return (thiswillneverbedefinedIhope()); } + #else + #include + #undef __attribute__ + static const char fnord[] __attribute__((__used__)) = "42"; + int main(void) { return (isatty(0)); } + #endif +EOF + +# End of tests run with -Werror +NOWARN=$save_NOWARN +phase=x + +# +# mksh: flavours (full/small mksh, omit certain stuff) +# +if ac_ifcpp 'ifdef MKSH_SMALL' isset_MKSH_SMALL '' \ + "if a reduced-feature mksh is requested"; then + : "${HAVE_NICE=0}" + : "${HAVE_PERSISTENT_HISTORY=0}" + check_categories="$check_categories smksh" +fi +ac_ifcpp 'if defined(MKSH_BINSHPOSIX) || defined(MKSH_BINSHREDUCED)' \ + isset_MKSH_BINSH '' 'if invoking as sh should be handled specially' && \ + check_categories="$check_categories binsh" +ac_ifcpp 'ifdef MKSH_UNEMPLOYED' isset_MKSH_UNEMPLOYED '' \ + "if mksh will be built without job control" && \ + check_categories="$check_categories arge" +ac_ifcpp 'ifdef MKSH_NOPROSPECTOFWORK' isset_MKSH_NOPROSPECTOFWORK '' \ + "if mksh will be built without job signals" && \ + check_categories="$check_categories arge nojsig" +ac_ifcpp 'ifdef MKSH_ASSUME_UTF8' isset_MKSH_ASSUME_UTF8 '' \ + 'if the default UTF-8 mode is specified' && : "${HAVE_SETLOCALE_CTYPE=0}" +ac_ifcpp 'if !MKSH_ASSUME_UTF8' isoff_MKSH_ASSUME_UTF8 \ + isset_MKSH_ASSUME_UTF8 0 \ + 'if the default UTF-8 mode is disabled' && \ + check_categories="$check_categories noutf8" +#ac_ifcpp 'ifdef MKSH_DISABLE_DEPRECATED' isset_MKSH_DISABLE_DEPRECATED '' \ +# "if deprecated features are to be omitted" && \ +# check_categories="$check_categories nodeprecated" +#ac_ifcpp 'ifdef MKSH_DISABLE_EXPERIMENTAL' isset_MKSH_DISABLE_EXPERIMENTAL '' \ +# "if experimental features are to be omitted" && \ +# check_categories="$check_categories noexperimental" +ac_ifcpp 'ifdef MKSH_MIDNIGHTBSD01ASH_COMPAT' isset_MKSH_MIDNIGHTBSD01ASH_COMPAT '' \ + 'if the MidnightBSD 0.1 ash compatibility mode is requested' && \ + check_categories="$check_categories mnbsdash" + +# +# Environment: headers +# +ac_header sys/time.h sys/types.h +ac_header time.h sys/types.h +test "11" = "$HAVE_SYS_TIME_H$HAVE_TIME_H" || HAVE_BOTH_TIME_H=0 +ac_test both_time_h '' 'whether and can both be included' <<-'EOF' + #include + #include + #include + #include + int main(void) { struct tm tm; return ((int)sizeof(tm) + isatty(0)); } +EOF +ac_header sys/bsdtypes.h +ac_header sys/file.h sys/types.h +ac_header sys/mkdev.h sys/types.h +ac_header sys/mman.h sys/types.h +ac_header sys/param.h +ac_header sys/resource.h sys/types.h _time +ac_header sys/select.h sys/types.h +ac_header sys/sysmacros.h +ac_header bstring.h +ac_header grp.h sys/types.h +ac_header io.h +ac_header libgen.h +ac_header libutil.h sys/types.h +ac_header paths.h +ac_header stdint.h stdarg.h +# include strings.h only if compatible with string.h +ac_header strings.h sys/types.h string.h +ac_header termios.h +ac_header ulimit.h sys/types.h +ac_header values.h + +# +# Environment: definitions +# +echo '#include +#include +/* check that off_t can represent 2^63-1 correctly, thx FSF */ +#define LARGE_OFF_T ((((off_t)1 << 31) << 31) - 1 + (((off_t)1 << 31) << 31)) +int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && + LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; +int main(void) { return (isatty(0)); }' >lft.c +ac_testn can_lfs '' "for large file support" + #include + int main(int ac, char **av) { return ((uint32_t)(size_t)*av + (int32_t)ac); } +EOF +ac_test can_ucbints '!' can_inttypes 1 "for UCB 32-bit integer types" <<-'EOF' + #include + #include + int main(int ac, char **av) { return ((u_int32_t)(size_t)*av + (int32_t)ac); } +EOF +ac_test can_int8type '!' stdint_h 1 "for standard 8-bit integer type" <<-'EOF' + #include + #include + int main(int ac, char **av) { return ((uint8_t)(size_t)av[ac]); } +EOF +ac_test can_ucbint8 '!' can_int8type 1 "for UCB 8-bit integer type" <<-'EOF' + #include + #include + int main(int ac, char **av) { return ((u_int8_t)(size_t)av[ac]); } +EOF + +ac_test rlim_t <<-'EOF' + #include + #if HAVE_BOTH_TIME_H + #include + #include + #elif HAVE_SYS_TIME_H + #include + #elif HAVE_TIME_H + #include + #endif + #if HAVE_SYS_RESOURCE_H + #include + #endif + #include + int main(void) { return (((int)(rlim_t)0) + isatty(0)); } +EOF + +# only testn: added later below +ac_testn sig_t <<-'EOF' + #include + #include + #include + volatile sig_t foo = (sig_t)0; + int main(void) { return (foo == (sig_t)0); } +EOF + +ac_testn sighandler_t '!' sig_t 0 <<-'EOF' + #include + #include + #include + volatile sighandler_t foo = (sighandler_t)0; + int main(void) { return (foo == (sighandler_t)0); } +EOF +if test 1 = $HAVE_SIGHANDLER_T; then + add_cppflags -Dsig_t=sighandler_t + HAVE_SIG_T=1 +fi + +ac_testn __sighandler_t '!' sig_t 0 <<-'EOF' + #include + #include + #include + volatile __sighandler_t foo = (__sighandler_t)0; + int main(void) { return (foo == (__sighandler_t)0); } +EOF +if test 1 = $HAVE___SIGHANDLER_T; then + add_cppflags -Dsig_t=__sighandler_t + HAVE_SIG_T=1 +fi + +test 1 = $HAVE_SIG_T || add_cppflags -Dsig_t=nosig_t +ac_cppflags SIG_T + +# +# check whether whatever we use for the final link will succeed +# +if test $cm = makefile; then + : nothing to check +else + HAVE_LINK_WORKS=x + ac_testinit link_works '' 'checking if the final link command may succeed' + fv=1 + cat >conftest.c <<-EOF + #define EXTERN + #define MKSH_INCLUDES_ONLY + #include "sh.h" + __RCSID("$srcversion"); + int main(void) { printf("Hello, World!\\n"); return (isatty(0)); } +EOF + case $cm in + llvm) + v "$CC $CFLAGS $CPPFLAGS $NOWARN -emit-llvm -c conftest.c" || fv=0 + rmf $tfn.s + test $fv = 0 || v "llvm-link -o - conftest.o | opt $optflags | llc -o $tfn.s" || fv=0 + test $fv = 0 || v "$CC $CFLAGS $LDFLAGS -o $tcfn $tfn.s $LIBS $ccpr" + ;; + dragonegg) + v "$CC $CFLAGS $CPPFLAGS $NOWARN -S -flto conftest.c" || fv=0 + test $fv = 0 || v "mv conftest.s conftest.ll" + test $fv = 0 || v "llvm-as conftest.ll" || fv=0 + rmf $tfn.s + test $fv = 0 || v "llvm-link -o - conftest.bc | opt $optflags | llc -o $tfn.s" || fv=0 + test $fv = 0 || v "$CC $CFLAGS $LDFLAGS -o $tcfn $tfn.s $LIBS $ccpr" + ;; + combine) + v "$CC $CFLAGS $CPPFLAGS $LDFLAGS -fwhole-program --combine $NOWARN -o $tcfn conftest.c $LIBS $ccpr" + ;; + lto|normal) + cm=normal + v "$CC $CFLAGS $CPPFLAGS $NOWARN -c conftest.c" || fv=0 + test $fv = 0 || v "$CC $CFLAGS $LDFLAGS -o $tcfn conftest.o $LIBS $ccpr" + ;; + esac + test -f $tcfn || fv=0 + ac_testdone + test $fv = 1 || exit 1 +fi + +# +# Environment: errors and signals +# +test x"NetBSD" = x"$TARGET_OS" && $e Ignore the compatibility warning. + +ac_testn sys_errlist '' "the sys_errlist[] array and sys_nerr" <<-'EOF' + extern const int sys_nerr; + extern const char * const sys_errlist[]; + extern int isatty(int); + int main(void) { return (*sys_errlist[sys_nerr - 1] + isatty(0)); } +EOF +ac_testn _sys_errlist '!' sys_errlist 0 "the _sys_errlist[] array and _sys_nerr" <<-'EOF' + extern const int _sys_nerr; + extern const char * const _sys_errlist[]; + extern int isatty(int); + int main(void) { return (*_sys_errlist[_sys_nerr - 1] + isatty(0)); } +EOF +if test 1 = "$HAVE__SYS_ERRLIST"; then + add_cppflags -Dsys_nerr=_sys_nerr + add_cppflags -Dsys_errlist=_sys_errlist + HAVE_SYS_ERRLIST=1 +fi +ac_cppflags SYS_ERRLIST + +for what in name list; do + uwhat=`upper $what` + ac_testn sys_sig$what '' "the sys_sig${what}[] array" <<-EOF + extern const char * const sys_sig${what}[]; + extern int isatty(int); + int main(void) { return (sys_sig${what}[0][0] + isatty(0)); } + EOF + ac_testn _sys_sig$what '!' sys_sig$what 0 "the _sys_sig${what}[] array" <<-EOF + extern const char * const _sys_sig${what}[]; + extern int isatty(int); + int main(void) { return (_sys_sig${what}[0][0] + isatty(0)); } + EOF + eval uwhat_v=\$HAVE__SYS_SIG$uwhat + if test 1 = "$uwhat_v"; then + add_cppflags -Dsys_sig$what=_sys_sig$what + eval HAVE_SYS_SIG$uwhat=1 + fi + ac_cppflags SYS_SIG$uwhat +done + +# +# Environment: library functions +# +ac_test flock <<-'EOF' + #include + #include + #undef flock + #if HAVE_SYS_FILE_H + #include + #endif + int main(void) { return (flock(0, LOCK_EX | LOCK_UN)); } +EOF + +ac_test lock_fcntl '!' flock 1 'whether we can lock files with fcntl' <<-'EOF' + #include + #undef flock + int main(void) { + struct flock lks; + lks.l_type = F_WRLCK | F_UNLCK; + return (fcntl(0, F_SETLKW, &lks)); + } +EOF + +ac_test getrusage <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + int main(void) { + struct rusage ru; + return (getrusage(RUSAGE_SELF, &ru) + + getrusage(RUSAGE_CHILDREN, &ru)); + } +EOF + +ac_test getsid <<-'EOF' + #include + int main(void) { return ((int)getsid(0)); } +EOF + +ac_test gettimeofday <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + int main(void) { struct timeval tv; return (gettimeofday(&tv, NULL)); } +EOF + +ac_test killpg <<-'EOF' + #include + int main(int ac, char *av[]) { return (av[0][killpg(123, ac)]); } +EOF + +ac_test memmove <<-'EOF' + #include + #include + #include + #if HAVE_STRINGS_H + #include + #endif + int main(int ac, char *av[]) { + return (*(int *)(void *)memmove(av[0], av[1], (size_t)ac)); + } +EOF + +ac_test mknod '' 'if to use mknod(), makedev() and friends' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + int main(int ac, char *av[]) { + dev_t dv; + dv = makedev((unsigned int)ac, (unsigned int)av[0][0]); + return (mknod(av[0], (mode_t)0, dv) ? (int)major(dv) : + (int)minor(dv)); + } +EOF + +ac_test mmap lock_fcntl 0 'for mmap and munmap' <<-'EOF' + #include + #if HAVE_SYS_FILE_H + #include + #endif + #if HAVE_SYS_MMAN_H + #include + #endif + #include + #include + int main(void) { return ((void *)mmap(NULL, (size_t)0, + PROT_READ, MAP_PRIVATE, 0, (off_t)0) == (void *)NULL ? 1 : + munmap(NULL, 0)); } +EOF + +ac_test ftruncate mmap 0 'for ftruncate' <<-'EOF' + #include + int main(void) { return (ftruncate(0, 0)); } +EOF + +ac_test nice <<-'EOF' + #include + int main(void) { return (nice(4)); } +EOF + +ac_test revoke <<-'EOF' + #include + #if HAVE_LIBUTIL_H + #include + #endif + #include + int main(int ac, char *av[]) { return (ac + revoke(av[0])); } +EOF + +ac_test setlocale_ctype '' 'setlocale(LC_CTYPE, "")' <<-'EOF' + #include + #include + int main(void) { return ((int)(size_t)(void *)setlocale(LC_CTYPE, "")); } +EOF + +ac_test langinfo_codeset setlocale_ctype 0 'nl_langinfo(CODESET)' <<-'EOF' + #include + #include + int main(void) { return ((int)(size_t)(void *)nl_langinfo(CODESET)); } +EOF + +ac_test select <<-'EOF' + #include + #if HAVE_BOTH_TIME_H + #include + #include + #elif HAVE_SYS_TIME_H + #include + #elif HAVE_TIME_H + #include + #endif + #if HAVE_SYS_BSDTYPES_H + #include + #endif + #if HAVE_SYS_SELECT_H + #include + #endif + #if HAVE_BSTRING_H + #include + #endif + #include + #include + #include + #if HAVE_STRINGS_H + #include + #endif + #include + int main(void) { + struct timeval tv = { 1, 200000 }; + fd_set fds; FD_ZERO(&fds); FD_SET(0, &fds); + return (select(FD_SETSIZE, &fds, NULL, NULL, &tv)); + } +EOF + +ac_test setresugid <<-'EOF' + #include + #include + int main(void) { return (setresuid(0,0,0) + setresgid(0,0,0)); } +EOF + +ac_test setgroups setresugid 0 <<-'EOF' + #include + #if HAVE_GRP_H + #include + #endif + #include + int main(void) { gid_t gid = 0; return (setgroups(0, &gid)); } +EOF + +if test x"$et" = x"klibc"; then + + ac_testn __rt_sigsuspend '' 'whether klibc uses RT signals' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + extern int __rt_sigsuspend(const sigset_t *, size_t); + int main(void) { return (__rt_sigsuspend(NULL, 0)); } +EOF + + # no? damn! legacy crap ahead! + + ac_testn __sigsuspend_s '!' __rt_sigsuspend 1 \ + 'whether sigsuspend is usable (1/2)' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + extern int __sigsuspend_s(sigset_t); + int main(void) { return (__sigsuspend_s(0)); } +EOF + ac_testn __sigsuspend_xxs '!' __sigsuspend_s 1 \ + 'whether sigsuspend is usable (2/2)' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + extern int __sigsuspend_xxs(int, int, sigset_t); + int main(void) { return (__sigsuspend_xxs(0, 0, 0)); } +EOF + + if test "000" = "$HAVE___RT_SIGSUSPEND$HAVE___SIGSUSPEND_S$HAVE___SIGSUSPEND_XXS"; then + # no usable sigsuspend(), use pause() *ugh* + add_cppflags -DMKSH_NO_SIGSUSPEND + fi +fi + +ac_test strerror '!' sys_errlist 0 <<-'EOF' + extern char *strerror(int); + int main(int ac, char *av[]) { return (*strerror(*av[ac])); } +EOF + +ac_test strsignal '!' sys_siglist 0 <<-'EOF' + #include + #include + int main(void) { return (strsignal(1)[0]); } +EOF + +ac_test strlcpy <<-'EOF' + #include + int main(int ac, char *av[]) { return (strlcpy(*av, av[1], + (size_t)ac)); } +EOF + +# +# check headers for declarations +# +ac_test flock_decl flock 1 'for declaration of flock()' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + #if HAVE_SYS_FILE_H + #include + #endif + int main(void) { return ((flock)(0, 0)); } +EOF +ac_test revoke_decl revoke 1 'for declaration of revoke()' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + int main(void) { return ((revoke)("")); } +EOF +ac_test sys_errlist_decl sys_errlist 0 "for declaration of sys_errlist[] and sys_nerr" <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + int main(void) { return (*sys_errlist[sys_nerr - 1] + isatty(0)); } +EOF +ac_test sys_siglist_decl sys_siglist 0 'for declaration of sys_siglist[]' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + int main(void) { return (sys_siglist[0][0] + isatty(0)); } +EOF + +# +# other checks +# +fd='if to use persistent history' +ac_cache PERSISTENT_HISTORY || case $HAVE_FTRUNCATE$HAVE_MMAP$HAVE_FLOCK$HAVE_LOCK_FCNTL in +111*|1101) fv=1 ;; +esac +test 1 = $fv || check_categories="$check_categories no-histfile" +ac_testdone +ac_cppflags + +# +# extra checks for legacy mksh +# +if test $legacy = 1; then + ac_test long_32bit '' 'whether long is 32 bit wide' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + #ifndef CHAR_BIT + #define CHAR_BIT 0 + #endif + struct ctasserts { + #define cta(name, assertion) char name[(assertion) ? 1 : -1] + cta(char_is_8_bits, (CHAR_BIT) == 8); + cta(long_is_32_bits, sizeof(long) == 4); + }; + int main(void) { return (sizeof(struct ctasserts)); } +EOF + + ac_test long_64bit '!' long_32bit 0 'whether long is 64 bit wide' <<-'EOF' + #define MKSH_INCLUDES_ONLY + #include "sh.h" + #ifndef CHAR_BIT + #define CHAR_BIT 0 + #endif + struct ctasserts { + #define cta(name, assertion) char name[(assertion) ? 1 : -1] + cta(char_is_8_bits, (CHAR_BIT) == 8); + cta(long_is_64_bits, sizeof(long) == 8); + }; + int main(void) { return (sizeof(struct ctasserts)); } +EOF + + case $HAVE_LONG_32BIT$HAVE_LONG_64BIT in + 10) check_categories="$check_categories int:32" ;; + 01) check_categories="$check_categories int:64" ;; + *) check_categories="$check_categories int:u" ;; + esac +fi + +# +# Compiler: Praeprocessor (only if needed) +# +test 0 = $HAVE_SYS_SIGNAME && if ac_testinit cpp_dd '' \ + 'checking if the C Preprocessor supports -dD'; then + echo '#define foo bar' >conftest.c + vv ']' "$CPP $CFLAGS $CPPFLAGS $NOWARN -dD conftest.c >x" + grep '#define foo bar' x >/dev/null 2>&1 && fv=1 + rmf conftest.c x vv.out + ac_testdone +fi + +# +# End of mirtoconf checks +# +$e ... done. + +# Some operating systems have ancient versions of ed(1) writing +# the character count to standard output; cope for that +echo wq >x +ed x /dev/null | grep 3 >/dev/null 2>&1 && \ + check_categories="$check_categories $oldish_ed" +rmf x vv.out + +if test 0 = $HAVE_SYS_SIGNAME; then + if test 1 = $HAVE_CPP_DD; then + $e Generating list of signal names... + else + $e No list of signal names available via cpp. Falling back... + fi + sigseenone=: + sigseentwo=: + echo '#include +#if defined(NSIG_MAX) +#define cfg_NSIG NSIG_MAX +#elif defined(NSIG) +#define cfg_NSIG NSIG +#elif defined(_NSIG) +#define cfg_NSIG _NSIG +#elif defined(SIGMAX) +#define cfg_NSIG (SIGMAX + 1) +#elif defined(_SIGMAX) +#define cfg_NSIG (_SIGMAX + 1) +#else +/*XXX better error out, see sh.h */ +#define cfg_NSIG 64 +#endif +int +mksh_cfg= cfg_NSIG +;' >conftest.c + # GNU sed 2.03 segfaults when optimising this to sed -n + NSIG=`vq "$CPP $CFLAGS $CPPFLAGS $NOWARN conftest.c" | \ + grep -v '^#' | \ + sed '/mksh_cfg.*= *$/{ + N + s/\n/ / + }' | \ + grep '^ *mksh_cfg *=' | \ + sed 's/^ *mksh_cfg *=[ ]*\([()0-9x+-][()0-9x+ -]*\).*$/\1/'` + case $NSIG in + *mksh_cfg*) $e "Error: NSIG='$NSIG'"; NSIG=0 ;; + *[\ \(\)+-]*) NSIG=`"$AWK" "BEGIN { print $NSIG }" /dev/null 2>&1 || printf=echo + test $printf = echo || test "`printf %d 42`" = 42 || printf=echo + test $printf = echo || NSIG=`printf %d "$NSIG" 2>/dev/null` + $printf "NSIG=$NSIG ... " + sigs="ABRT FPE ILL INT SEGV TERM ALRM BUS CHLD CONT HUP KILL PIPE QUIT" + sigs="$sigs STOP TSTP TTIN TTOU USR1 USR2 POLL PROF SYS TRAP URG VTALRM" + sigs="$sigs XCPU XFSZ INFO WINCH EMT IO DIL LOST PWR SAK CLD IOT STKFLT" + sigs="$sigs ABND DCE DUMP IOERR TRACE DANGER THCONT THSTOP RESV UNUSED" + test 1 = $HAVE_CPP_DD && test $NSIG -gt 1 && sigs="$sigs "`vq \ + "$CPP $CFLAGS $CPPFLAGS $NOWARN -dD conftest.c" | \ + grep '[ ]SIG[A-Z0-9][A-Z0-9]*[ ]' | \ + sed 's/^.*[ ]SIG\([A-Z0-9][A-Z0-9]*\)[ ].*$/\1/' | sort` + test $NSIG -gt 1 || sigs= + for name in $sigs; do + case $sigseenone in + *:$name:*) continue ;; + esac + sigseenone=$sigseenone$name: + echo '#include ' >conftest.c + echo int >>conftest.c + echo mksh_cfg= SIG$name >>conftest.c + echo ';' >>conftest.c + # GNU sed 2.03 croaks on optimising this, too + vq "$CPP $CFLAGS $CPPFLAGS $NOWARN conftest.c" | \ + grep -v '^#' | \ + sed '/mksh_cfg.*= *$/{ + N + s/\n/ / + }' | \ + grep '^ *mksh_cfg *=' | \ + sed 's/^ *mksh_cfg *=[ ]*\([0-9][0-9x]*\).*$/:\1 '$name/ + done | sed -n '/^:[^ ]/s/^://p' | while read nr name; do + test $printf = echo || nr=`printf %d "$nr" 2>/dev/null` + test $nr -gt 0 && test $nr -lt $NSIG || continue + case $sigseentwo in + *:$nr:*) ;; + *) echo " { \"$name\", $nr }," + sigseentwo=$sigseentwo$nr: + $printf "$name=$nr " >&2 + ;; + esac + done 2>&1 >signames.inc + rmf conftest.c + $e done. +fi + +addsrcs '!' HAVE_STRLCPY strlcpy.c +addsrcs USE_PRINTF_BUILTIN printf.c +test 1 = "$USE_PRINTF_BUILTIN" && add_cppflags -DMKSH_PRINTF_BUILTIN +test 1 = "$HAVE_CAN_VERB" && CFLAGS="$CFLAGS -verbose" +add_cppflags -DMKSH_BUILD_R=563 + +$e $bi$me: Finished configuration testing, now producing output.$ao + +files= +objs= +sp= +case $tcfn in +a.exe|conftest.exe) + mkshexe=$tfn.exe + add_cppflags -DMKSH_EXE_EXT + ;; +*) + mkshexe=$tfn + ;; +esac +case $curdir in +*\ *) mkshshebang="#!./$mkshexe" ;; +*) mkshshebang="#!$curdir/$mkshexe" ;; +esac +cat >test.sh <<-EOF + $mkshshebang + LC_ALL=C PATH='$PATH'; export LC_ALL PATH + test -n "\$KSH_VERSION" || exit 1 + set -A check_categories -- $check_categories + pflag='$curdir/$mkshexe' + sflag='$srcdir/check.t' + usee=0 useU=0 Pflag=0 Sflag=0 uset=0 vflag=1 xflag=0 + while getopts "C:e:fPp:QSs:t:U:v" ch; do case \$ch { + (C) check_categories[\${#check_categories[*]}]=\$OPTARG ;; + (e) usee=1; eflag=\$OPTARG ;; + (f) check_categories[\${#check_categories[*]}]=fastbox ;; + (P) Pflag=1 ;; + (+P) Pflag=0 ;; + (p) pflag=\$OPTARG ;; + (Q) vflag=0 ;; + (+Q) vflag=1 ;; + (S) Sflag=1 ;; + (+S) Sflag=0 ;; + (s) sflag=\$OPTARG ;; + (t) uset=1; tflag=\$OPTARG ;; + (U) useU=1; Uflag=\$OPTARG ;; + (v) vflag=1 ;; + (+v) vflag=0 ;; + (*) xflag=1 ;; + } + done + shift \$((OPTIND - 1)) + set -A args -- '$srcdir/check.pl' -p "\$pflag" + if $ebcdic; then + args[\${#args[*]}]=-E + fi + x= + for y in "\${check_categories[@]}"; do + x=\$x,\$y + done + if [[ -n \$x ]]; then + args[\${#args[*]}]=-C + args[\${#args[*]}]=\${x#,} + fi + if (( usee )); then + args[\${#args[*]}]=-e + args[\${#args[*]}]=\$eflag + fi + (( Pflag )) && args[\${#args[*]}]=-P + if (( uset )); then + args[\${#args[*]}]=-t + args[\${#args[*]}]=\$tflag + fi + if (( useU )); then + args[\${#args[*]}]=-U + args[\${#args[*]}]=\$Uflag + fi + (( vflag )) && args[\${#args[*]}]=-v + (( xflag )) && args[\${#args[*]}]=-x # force usage by synerr + if [[ -n \$TMPDIR && -d \$TMPDIR/. ]]; then + args[\${#args[*]}]=-T + args[\${#args[*]}]=\$TMPDIR + fi + print Testing mksh for conformance: + grep -F -e Mir''OS: -e MIRBSD "\$sflag" + print "This shell is actually:\\n\\t\$KSH_VERSION" + print 'test.sh built for mksh $dstversion' + cstr='\$os = defined \$^O ? \$^O : "unknown";' + cstr="\$cstr"'print \$os . ", Perl version " . \$];' + for perli in \$PERL perl5 perl no; do + if [[ \$perli = no ]]; then + print Cannot find a working Perl interpreter, aborting. + exit 1 + fi + print "Trying Perl interpreter '\$perli'..." + perlos=\$(\$perli -e "\$cstr") + rv=\$? + print "Errorlevel \$rv, running on '\$perlos'" + if (( rv )); then + print "=> not using" + continue + fi + if [[ -n \$perlos ]]; then + print "=> using it" + break + fi + done + (( Sflag )) || echo + \$perli "\${args[@]}" -s "\$sflag" "\$@" + (( Sflag )) || exec \$perli "\${args[@]}" -s "\$sflag" "\$@"$tsts + # use of the -S option for check.t split into multiple chunks + rv=0 + for s in "\$sflag".*; do + echo + \$perli "\${args[@]}" -s "\$s" "\$@" + \$perli "\${args[@]}" -s "\$s" "\$@"$tsts + rc=\$? + (( rv = rv ? rv : rc )) + done + exit \$rv +EOF +chmod 755 test.sh +case $cm in +dragonegg) + emitbc="-S -flto" + ;; +llvm) + emitbc="-emit-llvm -c" + ;; +*) + emitbc=-c + ;; +esac +echo ": # work around NeXTstep bug" >Rebuild.sh +cd "$srcdir" +optfiles=`echo *.opt` +cd "$curdir" +for file in $optfiles; do + echo "echo + Running genopt on '$file'..." + echo "(srcfile='$srcdir/$file'; BUILDSH_RUN_GENOPT=1; . '$srcdir/Build.sh')" +done >>Rebuild.sh +echo set -x >>Rebuild.sh +for file in $SRCS; do + op=`echo x"$file" | sed 's/^x\(.*\)\.c$/\1./'` + test -f $file || file=$srcdir/$file + files="$files$sp$file" + sp=' ' + echo "$CC $CFLAGS $CPPFLAGS $emitbc $file || exit 1" >>Rebuild.sh + if test $cm = dragonegg; then + echo "mv ${op}s ${op}ll" >>Rebuild.sh + echo "llvm-as ${op}ll || exit 1" >>Rebuild.sh + objs="$objs$sp${op}bc" + else + objs="$objs$sp${op}o" + fi +done +case $cm in +dragonegg|llvm) + echo "rm -f $tfn.s" >>Rebuild.sh + echo "llvm-link -o - $objs | opt $optflags | llc -o $tfn.s" >>Rebuild.sh + lobjs=$tfn.s + ;; +*) + lobjs=$objs + ;; +esac +echo tcfn=$mkshexe >>Rebuild.sh +echo "$CC $CFLAGS $LDFLAGS -o \$tcfn $lobjs $LIBS $ccpr" >>Rebuild.sh +echo "test -f \$tcfn || exit 1; $SIZE \$tcfn" >>Rebuild.sh +if test $cm = makefile; then + extras='emacsfn.h exprtok.h rlimits.opt sh.h sh_flags.opt var_spec.h' + test 0 = $HAVE_SYS_SIGNAME && extras="$extras signames.inc" + gens= genq= + for file in $optfiles; do + genf=`basename "$file" | sed 's/.opt$/.gen/'` + gens="$gens $genf" + genq="$genq$nl$genf: $srcdir/Build.sh $srcdir/$file + srcfile=$srcdir/$file; BUILDSH_RUN_GENOPT=1; . $srcdir/Build.sh" + done + cat >Makefrag.inc < +EOF + $e + $e Generated Makefrag.inc successfully. + exit 0 +fi +for file in $optfiles; do + $e "+ Running genopt on '$file'..." + do_genopt "$srcdir/$file" || exit 1 +done +if test $cm = combine; then + objs="-o $mkshexe" + for file in $SRCS; do + test -f $file || file=$srcdir/$file + objs="$objs $file" + done + emitbc="-fwhole-program --combine" + v "$CC $CFLAGS $CPPFLAGS $LDFLAGS $emitbc $objs $LIBS $ccpr" +elif test 1 = $pm; then + for file in $SRCS; do + test -f $file || file=$srcdir/$file + v "$CC $CFLAGS $CPPFLAGS $emitbc $file" & + done + wait +else + for file in $SRCS; do + test $cm = dragonegg && \ + op=`echo x"$file" | sed 's/^x\(.*\)\.c$/\1./'` + test -f $file || file=$srcdir/$file + v "$CC $CFLAGS $CPPFLAGS $emitbc $file" || exit 1 + if test $cm = dragonegg; then + v "mv ${op}s ${op}ll" + v "llvm-as ${op}ll" || exit 1 + fi + done +fi +case $cm in +dragonegg|llvm) + rmf $tfn.s + v "llvm-link -o - $objs | opt $optflags | llc -o $tfn.s" + ;; +esac +tcfn=$mkshexe +test $cm = combine || v "$CC $CFLAGS $LDFLAGS -o $tcfn $lobjs $LIBS $ccpr" +test -f $tcfn || exit 1 +test 1 = $r || v "$NROFF -mdoc <'$srcdir/lksh.1' >lksh.cat1" || rmf lksh.cat1 +test 1 = $r || v "$NROFF -mdoc <'$srcdir/mksh.1' >mksh.cat1" || rmf mksh.cat1 +test 0 = $eq && v $SIZE $tcfn +i=install +test -f /usr/ucb/$i && i=/usr/ucb/$i +test 1 = $eq && e=: +$e +$e Installing the shell: +$e "# $i -c -s -o root -g bin -m 555 $tfn /bin/$tfn" +if test $legacy = 0; then + $e "# grep -x /bin/$tfn /etc/shells >/dev/null || echo /bin/$tfn >>/etc/shells" + $e "# $i -c -o root -g bin -m 444 dot.mkshrc /usr/share/doc/mksh/examples/" +fi +$e +$e Installing the manual: +if test -f mksh.cat1; then + $e "# $i -c -o root -g bin -m 444 lksh.cat1" \ + "/usr/share/man/cat1/lksh.0" + $e "# $i -c -o root -g bin -m 444 mksh.cat1" \ + "/usr/share/man/cat1/mksh.0" + $e or +fi +$e "# $i -c -o root -g bin -m 444 lksh.1 mksh.1 /usr/share/man/man1/" +$e +$e Run the regression test suite: ./test.sh +$e Please also read the sample file dot.mkshrc and the fine manual. +exit 0 + +: <<'EOD' + +=== Environment used === + +==== build environment ==== +AWK default: awk +CC default: cc +CFLAGS if empty, defaults to -xO2 or +O2 + or -O3 -qstrict or -O2, per compiler +CPPFLAGS default empty +LDFLAGS default empty; added before sources +LDSTATIC set this to '-static'; default unset +LIBS default empty; added after sources + [Interix] default: -lcrypt (XXX still needed?) +NOWARN -Wno-error or similar +NROFF default: nroff +TARGET_OS default: $(uname -s || uname) +TARGET_OSREV [QNX] default: $(uname -r) + +==== feature selectors ==== +USE_PRINTF_BUILTIN 1 to include (unsupported) printf(1) as builtin +===== general format ===== +HAVE_STRLEN ac_test +HAVE_STRING_H ac_header +HAVE_CAN_FSTACKPROTECTORALL ac_flags + +==== cpp definitions ==== +DEBUG dont use in production, wants gcc, implies: +DEBUG_LEAKS enable freeing resources before exiting +MKSHRC_PATH "~/.mkshrc" (do not change) +MKSH_A4PB force use of arc4random_pushb +MKSH_ASSUME_UTF8 (0=disabled, 1=enabled; default: unset) +MKSH_BINSHPOSIX if */sh or */-sh, enable set -o posix +MKSH_BINSHREDUCED if */sh or */-sh, enable set -o sh +MKSH_CLS_STRING KSH_ESC_STRING "[;H" KSH_ESC_STRING "[J" +MKSH_DEFAULT_EXECSHELL "/bin/sh" (do not change) +MKSH_DEFAULT_PROFILEDIR "/etc" (do not change) +MKSH_DEFAULT_TMPDIR "/tmp" (do not change) +MKSH_DISABLE_DEPRECATED disable code paths scheduled for later removal +MKSH_DISABLE_EXPERIMENTAL disable code not yet comfy for (LTS) snapshots +MKSH_DISABLE_TTY_WARNING shut up warning about ctty if OS cant be fixed +MKSH_DONT_EMIT_IDSTRING omit RCS IDs from binary +MKSH_EARLY_LOCALE_TRACKING track utf8-mode from POSIX locale, for SuSE +MKSH_MIDNIGHTBSD01ASH_COMPAT set -o sh: additional compatibility quirk +MKSH_NOPROSPECTOFWORK disable jobs, co-processes, etc. (do not use) +MKSH_NOPWNAM skip PAM calls, for -static on glibc or Solaris +MKSH_NO_CMDLINE_EDITING disable command line editing code entirely +MKSH_NO_DEPRECATED_WARNING omit warning when deprecated stuff is run +MKSH_NO_LIMITS omit ulimit code +MKSH_NO_SIGSETJMP define if sigsetjmp is broken or not available +MKSH_NO_SIGSUSPEND use sigprocmask+pause instead of sigsuspend +MKSH_SMALL omit some code, optimise hard for size (slower) +MKSH_SMALL_BUT_FAST disable some hard-for-size optim. (modern sys.) +MKSH_S_NOVI=1 disable Vi editing mode (default if MKSH_SMALL) +MKSH_TYPEDEF_SIG_ATOMIC_T define to e.g. 'int' if sig_atomic_t is missing +MKSH_TYPEDEF_SSIZE_T define to e.g. 'long' if your OS has no ssize_t +MKSH_UNEMPLOYED disable job control (but not jobs/co-processes) + +=== generic installation instructions === + +Set CC and possibly CFLAGS, CPPFLAGS, LDFLAGS, LIBS. If cross-compiling, +also set TARGET_OS. To disable tests, set e.g. HAVE_STRLCPY=0; to enable +them, set to a value other than 0 or 1. Ensure /bin/ed is installed. For +MKSH_SMALL but with Vi mode, add -DMKSH_S_NOVI=0 to CPPFLAGS as well. + +Normally, the following command is what you want to run, then: +$ (sh Build.sh -r -c lto && ./test.sh -f) 2>&1 | tee log + +Copy dot.mkshrc to /etc/skel/.mkshrc; install mksh into $prefix/bin; or +/bin; install the manpage, if omitting the -r flag a catmanpage is made +using $NROFF. Consider using a forward script as /etc/skel/.mkshrc like +http://anonscm.debian.org/cgit/collab-maint/mksh.git/plain/debian/.mkshrc +and put dot.mkshrc as /etc/mkshrc so users need not keep up their HOME. + +You may also want to install the lksh binary (also as /bin/sh) built by: +$ CPPFLAGS="$CPPFLAGS -DMKSH_BINSHPOSIX" sh Build.sh -L -r -c lto + +EOD diff --git a/check.pl b/check.pl new file mode 100644 index 0000000..e9c2437 --- /dev/null +++ b/check.pl @@ -0,0 +1,1363 @@ +# $MirOS: src/bin/mksh/check.pl,v 1.49 2017/05/05 21:17:31 tg Exp $ +# $OpenBSD: th,v 1.1 2013/12/02 20:39:44 millert Exp $ +#- +# Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2011, +# 2012, 2013, 2014, 2015, 2017 +# mirabilos +# +# Provided that these terms and disclaimer and all copyright notices +# are retained or reproduced in an accompanying document, permission +# is granted to deal in this work without restriction, including un- +# limited rights to use, publicly perform, distribute, sell, modify, +# merge, give away, or sublicence. +# +# This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to +# the utmost extent permitted by applicable law, neither express nor +# implied; without malicious intent or gross negligence. In no event +# may a licensor, author or contributor be held liable for indirect, +# direct, other damage, loss, or other issues arising in any way out +# of dealing in the work, even if advised of the possibility of such +# damage or existence of a defect, except proven that it results out +# of said person's immediate fault when using the work as intended. +#- +# Example test: +# name: a-test +# description: +# a test to show how tests are done +# arguments: !-x!-f! +# stdin: +# echo -n * +# false +# expected-stdout: ! +# * +# expected-stderr: +# + echo -n * +# + false +# expected-exit: 1 +# --- +# This runs the test-program (eg, mksh) with the arguments -x and -f, +# standard input is a file containing "echo hi*\nfalse\n". The program +# is expected to produce "hi*" (no trailing newline) on standard output, +# "+ echo hi*\n+false\n" on standard error, and an exit code of 1. +# +# +# Format of test files: +# - blank lines and lines starting with # are ignored +# - a test file contains a series of tests +# - a test is a series of tag:value pairs ended with a "---" line +# (leading/trailing spaces are stripped from the first line of value) +# - test tags are: +# Tag Flag Description +# ----- ---- ----------- +# name r The name of the test; should be unique +# description m What test does +# arguments M Arguments to pass to the program; +# default is no arguments. +# script m Value is written to a file which +# is passed as an argument to the program +# (after the arguments arguments) +# stdin m Value is written to a file which is +# used as standard-input for the program; +# default is to use /dev/null. +# perl-setup m Value is a perl script which is executed +# just before the test is run. Try to +# avoid using this... +# perl-cleanup m Value is a perl script which is executed +# just after the test is run. Try to +# avoid using this... +# env-setup M Value is a list of NAME=VALUE elements +# which are put in the environment before +# the test is run. If the =VALUE is +# missing, NAME is removed from the +# environment. Programs are run with +# the following minimal environment: +# HOME, LD_LIBRARY_PATH, LOCPATH, +# LOGNAME, PATH, SHELL, UNIXMODE, +# UNIXROOT, USER +# (values taken from the environment of +# the test harness). +# CYGWIN is set to nodosfilewarning. +# ENV is set to /nonexistant. +# __progname is set to the -p argument. +# __perlname is set to $^X (perlexe). +# @utflocale@ is substituted from -U. +# file-setup mps Used to create files, directories +# and symlinks. First word is either +# file, dir or symlink; second word is +# permissions; this is followed by a +# quoted word that is the name of the +# file; the end-quote should be followed +# by a newline, then the file data +# (if any). The first word may be +# preceded by a ! to strip the trailing +# newline in a symlink. +# file-result mps Used to verify a file, symlink or +# directory is created correctly. +# The first word is either +# file, dir or symlink; second word is +# expected permissions; third word +# is user-id; fourth is group-id; +# fifth is "exact" or "pattern" +# indicating whether the file contents +# which follow is to be matched exactly +# or if it is a regular expression. +# The fifth argument is the quoted name +# of the file that should be created. +# The end-quote should be followed +# by a newline, then the file data +# (if any). The first word may be +# preceded by a ! to strip the trailing +# newline in the file contents. +# The permissions, user and group fields +# may be * meaning accept any value. +# time-limit Time limit - the program is sent a +# SIGKILL N seconds. Default is no +# limit. +# expected-fail 'yes' if the test is expected to fail. +# expected-exit expected exit code. Can be a number, +# or a C expression using the variables +# e, s and w (exit code, termination +# signal, and status code). +# expected-stdout m What the test should generate on stdout; +# default is to expect no output. +# expected-stdout-pattern m A perl pattern which matches the +# expected output. +# expected-stderr m What the test should generate on stderr; +# default is to expect no output. +# expected-stderr-pattern m A perl pattern which matches the +# expected standard error. +# category m Specify a comma separated list of +# 'categories' of program that the test +# is to be run for. A category can be +# negated by prefixing the name with a !. +# The idea is that some tests in a +# test suite may apply to a particular +# program version and shouldn't be run +# on other versions. The category(s) of +# the program being tested can be +# specified on the command line. +# One category os:XXX is predefined +# (XXX is the operating system name, +# eg, linux, dec_osf). +# need-ctty 'yes' if the test needs a ctty, run +# with -C regress:no-ctty to disable. +# Flag meanings: +# r tag is required (eg, a test must have a name tag). +# m value can be multiple lines. Lines must be prefixed with +# a tab. If the value part of the initial tag:value line is +# - empty: the initial blank line is stripped. +# - a lone !: the last newline in the value is stripped; +# M value can be multiple lines (prefixed by a tab) and consists +# of multiple fields, delimited by a field separator character. +# The value must start and end with the f-s-c. +# p tag takes parameters (used with m). +# s tag can be used several times. + +# require Config only if it exists +# pull EINTR from POSIX.pm or Errno.pm if they exist +# otherwise just skip it +BEGIN { + eval { + require Config; + import Config; + 1; + }; + $EINTR = 0; + eval { + require POSIX; + $EINTR = POSIX::EINTR(); + }; + if ($@) { + eval { + require Errno; + $EINTR = Errno::EINTR(); + } or do { + $EINTR = 0; + }; + } +}; + +use Getopt::Std; + +$os = defined $^O ? $^O : 'unknown'; + +($prog = $0) =~ s#.*/##; + +$Usage = < 0): $opt_t\n" + if $opt_t !~ /^\d+$/ || $opt_t <= 0; + $default_time_limit = $opt_t; +} +$program_kludge = defined $opt_P ? $opt_P : 0; + +if ($is_ebcdic) { + $categories{'shell:ebcdic-yes'} = 1; + $categories{'shell:ascii-no'} = 1; +} else { + $categories{'shell:ebcdic-no'} = 1; + $categories{'shell:ascii-yes'} = 1; +} + +if (defined $opt_C) { + foreach $c (split(',', $opt_C)) { + $c =~ s/\s+//; + die "$prog: categories can't be negated on the command line\n" + if ($c =~ /^!/); + $categories{$c} = 1; + } +} + +# Note which tests are to be run. +%do_test = (); +grep($do_test{$_} = 1, @ARGV); +$all_tests = @ARGV == 0; + +# Set up a very minimal environment +%new_env = (); +foreach $env (('HOME', 'LD_LIBRARY_PATH', 'LOCPATH', 'LOGNAME', + 'PATH', 'SHELL', 'UNIXMODE', 'UNIXROOT', 'USER')) { + $new_env{$env} = $ENV{$env} if defined $ENV{$env}; +} +$new_env{'CYGWIN'} = 'nodosfilewarning'; +$new_env{'ENV'} = '/nonexistant'; + +if (($os eq 'VMS') || ($Config{perlpath} =~ m/$Config{_exe}$/i)) { + $new_env{'__perlname'} = $Config{perlpath}; +} else { + $new_env{'__perlname'} = $Config{perlpath} . $Config{_exe}; +} +$new_env{'__perlname'} = $^X if ($new_env{'__perlname'} eq '') and -f $^X and -x $^X; +if ($new_env{'__perlname'} eq '') { + foreach $pathelt (split /:/,$ENV{'PATH'}) { + chomp($pathelt = `pwd`) if $pathelt eq ''; + my $x = $pathelt . '/' . $^X; + next unless -f $x and -x $x; + $new_env{'__perlname'} = $x; + last; + } +} +$new_env{'__perlname'} = $^X if ($new_env{'__perlname'} eq ''); + +if (defined $opt_e) { + # XXX need a way to allow many -e arguments... + if ($opt_e =~ /^([a-zA-Z_]\w*)(|=(.*))$/) { + $new_env{$1} = $2 eq '' ? $ENV{$1} : $3; + } else { + die "$0: bad -e argument: $opt_e\n"; + } +} +%old_env = %ENV; + +chop($pwd = `pwd 2>/dev/null`); +die "$prog: couldn't get current working directory\n" if $pwd eq ''; +die "$prog: couldn't cd to $pwd - $!\n" if !chdir($pwd); + +die "$prog: couldn't cd to $temp_base - $!\n" if !chdir($temp_base); +die "$prog: couldn't get temporary directory base\n" unless -d '.'; +$temps = sprintf("chk%d-%d.", $$, time()); +$tempi = 0; +until (mkdir(($tempdir = sprintf("%s%03d", $temps, $tempi)), 0700)) { + die "$prog: couldn't get temporary directory\n" if $tempi++ >= 999; +} +die "$prog: couldn't cd to $tempdir - $!\n" if !chdir($tempdir); +chop($temp_dir = `pwd 2>/dev/null`); +die "$prog: couldn't get temporary directory\n" if $temp_dir eq ''; +die "$prog: couldn't cd to $pwd - $!\n" if !chdir($pwd); + +if (!$program_kludge) { + $test_prog = "$pwd/$test_prog" if (substr($test_prog, 0, 1) ne '/') && + ($os ne 'os2' || substr($test_prog, 1, 1) ne ':'); + die "$prog: $test_prog is not executable - bye\n" + if (! -x $test_prog && $os ne 'os2'); +} + +@trap_sigs = ('TERM', 'QUIT', 'INT', 'PIPE', 'HUP'); +@SIG{@trap_sigs} = ('cleanup_exit') x @trap_sigs; +$child_kill_ok = 0; +$SIG{'ALRM'} = 'catch_sigalrm'; + +$| = 1; + +# Create temp files +$temps = "${temp_dir}/rts"; +$tempi = "${temp_dir}/rti"; +$tempo = "${temp_dir}/rto"; +$tempe = "${temp_dir}/rte"; +$tempdir = "${temp_dir}/rtd"; +mkdir($tempdir, 0700) or die "$prog: couldn't mkdir $tempdir - $!\n"; + +if (-d $test_set) { + $file_prefix_skip = length($test_set) + 1; + $ret = &process_test_dir($test_set); +} else { + $file_prefix_skip = 0; + $ret = &process_test_file($test_set); +} +&cleanup_exit() if !defined $ret; + +$tot_failed = $nfailed + $nifailed + $nxfailed; +$tot_passed = $npassed + $nxpassed; +if ($tot_failed || $tot_passed) { + print "Total failed: $tot_failed"; + print " ($nifailed ignored)" if $nifailed; + print " ($nxfailed unexpected)" if $nxfailed; + print " (as expected)" if $nfailed && !$nxfailed && !$nifailed; + print " ($nfailed expected)" if $nfailed && ($nxfailed || $nifailed); + print "\nTotal passed: $tot_passed"; + print " ($nxpassed unexpected)" if $nxpassed; + print "\n"; +} + +&cleanup_exit('ok'); + +sub +cleanup_exit +{ + local($sig, $exitcode) = ('', 1); + + if ($_[0] eq 'ok') { + unless ($nxfailed) { + $exitcode = 0; + } else { + $exitcode = 1; + } + } elsif ($_[0] ne '') { + $sig = $_[0]; + } + + unlink($tempi, $tempo, $tempe, $temps); + &scrub_dir($tempdir) if defined $tempdir; + rmdir($tempdir) if defined $tempdir; + rmdir($temp_dir) if defined $temp_dir; + + if ($sig) { + $SIG{$sig} = 'DEFAULT'; + kill $sig, $$; + return; + } + exit $exitcode; +} + +sub +catch_sigalrm +{ + $SIG{'ALRM'} = 'catch_sigalrm'; + kill(9, $child_pid) if $child_kill_ok; + $child_killed = 1; +} + +sub +process_test_dir +{ + local($dir) = @_; + local($ret, $file); + local(@todo) = (); + + if (!opendir(DIR, $dir)) { + print STDERR "$prog: can't open directory $dir - $!\n"; + return undef; + } + while (defined ($file = readdir(DIR))) { + push(@todo, $file) if $file =~ /^[^.].*\.t$/; + } + closedir(DIR); + + foreach $file (@todo) { + $file = "$dir/$file"; + if (-d $file) { + $ret = &process_test_dir($file); + } elsif (-f _) { + $ret = &process_test_file($file); + } + last if !defined $ret; + } + + return $ret; +} + +sub +process_test_file +{ + local($file) = @_; + local($ret); + + if (!open(IN, $file)) { + print STDERR "$prog: can't open $file - $!\n"; + return undef; + } + binmode(IN); + while (1) { + $ret = &read_test($file, IN, *test); + last if !defined $ret || !$ret; + next if !$all_tests && !$do_test{$test{'name'}}; + next if !&category_check(*test); + $ret = &run_test(*test); + last if !defined $ret; + } + close(IN); + + return $ret; +} + +sub +run_test +{ + local(*test) = @_; + local($name) = $test{':full-name'}; + + return undef if !&scrub_dir($tempdir); + + if (defined $test{'stdin'}) { + return undef if !&write_file($tempi, $test{'stdin'}); + $ifile = $tempi; + } else { + $ifile = '/dev/null'; + } + + if (defined $test{'script'}) { + return undef if !&write_file($temps, $test{'script'}); + } + + if (!chdir($tempdir)) { + print STDERR "$prog: couldn't cd to $tempdir - $!\n"; + return undef; + } + + if (defined $test{'file-setup'}) { + local($i); + local($type, $perm, $rest, $c, $len, $name); + + for ($i = 0; $i < $test{'file-setup'}; $i++) { + $val = $test{"file-setup:$i"}; + + # format is: type perm "name" + ($type, $perm, $rest) = + split(' ', $val, 3); + $c = substr($rest, 0, 1); + $len = index($rest, $c, 1) - 1; + $name = substr($rest, 1, $len); + $rest = substr($rest, 2 + $len); + $perm = oct($perm) if $perm =~ /^\d+$/; + if ($type eq 'file') { + return undef if !&write_file($name, $rest); + if (!chmod($perm, $name)) { + print STDERR + "$prog:$test{':long-name'}: can't chmod $perm $name - $!\n"; + return undef; + } + } elsif ($type eq 'dir') { + if (!mkdir($name, $perm)) { + print STDERR + "$prog:$test{':long-name'}: can't mkdir $perm $name - $!\n"; + return undef; + } + } elsif ($type eq 'symlink') { + local($oumask) = umask($perm); + local($ret) = symlink($rest, $name); + umask($oumask); + if (!$ret) { + print STDERR + "$prog:$test{':long-name'}: couldn't create symlink $name - $!\n"; + return undef; + } + } + } + } + + if (defined $test{'perl-setup'}) { + eval $test{'perl-setup'}; + if ($@ ne '') { + print STDERR "$prog:$test{':long-name'}: error running perl-setup - $@\n"; + return undef; + } + } + + $pid = fork; + if (!defined $pid) { + print STDERR "$prog: can't fork - $!\n"; + return undef; + } + if (!$pid) { + @SIG{@trap_sigs} = ('DEFAULT') x @trap_sigs; + $SIG{'ALRM'} = 'DEFAULT'; + if (defined $test{'env-setup'}) { + local($var, $val, $i); + + foreach $var (split(substr($test{'env-setup'}, 0, 1), + $test{'env-setup'})) + { + $i = index($var, '='); + next if $i == 0 || $var eq ''; + if ($i < 0) { + delete $new_env{$var}; + } else { + $new_env{substr($var, 0, $i)} = substr($var, $i + 1); + } + } + } + if (!open(STDIN, "< $ifile")) { + print STDERR "$prog: couldn't open $ifile in child - $!\n"; + kill('TERM', $$); + } + binmode(STDIN); + if (!open(STDOUT, "> $tempo")) { + print STDERR "$prog: couldn't open $tempo in child - $!\n"; + kill('TERM', $$); + } + binmode(STDOUT); + if (!open(STDERR, "> $tempe")) { + print STDOUT "$prog: couldn't open $tempe in child - $!\n"; + kill('TERM', $$); + } + binmode(STDERR); + if ($program_kludge) { + @argv = split(' ', $test_prog); + } else { + @argv = ($test_prog); + } + if (defined $test{'arguments'}) { + push(@argv, + split(substr($test{'arguments'}, 0, 1), + substr($test{'arguments'}, 1))); + } + push(@argv, $temps) if defined $test{'script'}; + + #XXX realpathise, use command -v/whence -p/which, or sth. like that + #XXX if !$program_kludge, we get by with not doing it for now tho + $new_env{'__progname'} = $argv[0]; + + # The following doesn't work with perl5... Need to do it explicitly - yuck. + #%ENV = %new_env; + foreach $k (keys(%ENV)) { + delete $ENV{$k}; + } + $ENV{$k} = $v while ($k,$v) = each %new_env; + + exec { $argv[0] } @argv; + print STDERR "$prog: couldn't execute $test_prog - $!\n"; + kill('TERM', $$); + exit(95); + } + $child_pid = $pid; + $child_killed = 0; + $child_kill_ok = 1; + alarm($test{'time-limit'}) if defined $test{'time-limit'}; + while (1) { + $xpid = waitpid($pid, 0); + $child_kill_ok = 0; + if ($xpid < 0) { + if ($EINTR) { + next if $! == $EINTR; + } + print STDERR "$prog: error waiting for child - $!\n"; + return undef; + } + last; + } + $status = $?; + alarm(0) if defined $test{'time-limit'}; + + $failed = 0; + $why = ''; + + if ($child_killed) { + $failed = 1; + $why .= "\ttest timed out (limit of $test{'time-limit'} seconds)\n"; + } + + $ret = &eval_exit($test{'long-name'}, $status, $test{'expected-exit'}); + return undef if !defined $ret; + if (!$ret) { + local($expl); + + $failed = 1; + if (($status & 0xff) == 0x7f) { + $expl = "stopped"; + } elsif (($status & 0xff)) { + $expl = "signal " . ($status & 0x7f); + } else { + $expl = "exit-code " . (($status >> 8) & 0xff); + } + $why .= + "\tunexpected exit status $status ($expl), expected $test{'expected-exit'}\n"; + } + + $tmp = &check_output($test{'long-name'}, $tempo, 'stdout', + $test{'expected-stdout'}, $test{'expected-stdout-pattern'}); + return undef if !defined $tmp; + if ($tmp ne '') { + $failed = 1; + $why .= $tmp; + } + + $tmp = &check_output($test{'long-name'}, $tempe, 'stderr', + $test{'expected-stderr'}, $test{'expected-stderr-pattern'}); + return undef if !defined $tmp; + if ($tmp ne '') { + $failed = 1; + $why .= $tmp; + } + + $tmp = &check_file_result(*test); + return undef if !defined $tmp; + if ($tmp ne '') { + $failed = 1; + $why .= $tmp; + } + + if (defined $test{'perl-cleanup'}) { + eval $test{'perl-cleanup'}; + if ($@ ne '') { + print STDERR "$prog:$test{':long-name'}: error running perl-cleanup - $@\n"; + return undef; + } + } + + if (!chdir($pwd)) { + print STDERR "$prog: couldn't cd to $pwd - $!\n"; + return undef; + } + + if ($failed) { + if (!$test{'expected-fail'}) { + if ($test{'need-pass'}) { + print "FAIL $name\n"; + $nxfailed++; + } else { + print "FAIL $name (ignored)\n"; + $nifailed++; + } + } else { + print "fail $name (as expected)\n"; + $nfailed++; + } + $why = "\tDescription" + . &wrap_lines($test{'description'}, " (missing)\n") + . $why; + } elsif ($test{'expected-fail'}) { + print "PASS $name (unexpectedly)\n"; + $nxpassed++; + } else { + print "pass $name\n"; + $npassed++; + } + print $why if $verbose; + return 0; +} + +sub +category_check +{ + local(*test) = @_; + local($c); + + return 0 if ($test{'need-ctty'} && defined $categories{'regress:no-ctty'}); + return 1 if (!defined $test{'category'}); + local($ok) = 0; + foreach $c (split(',', $test{'category'})) { + $c =~ s/\s+//; + if ($c =~ /^!/) { + $c = $'; + return 0 if (defined $categories{$c}); + $ok = 1; + } else { + $ok = 1 if (defined $categories{$c}); + } + } + return $ok; +} + +sub +scrub_dir +{ + local($dir) = @_; + local(@todo) = (); + local($file); + + if (!opendir(DIR, $dir)) { + print STDERR "$prog: couldn't open directory $dir - $!\n"; + return undef; + } + while (defined ($file = readdir(DIR))) { + push(@todo, $file) if $file ne '.' && $file ne '..'; + } + closedir(DIR); + foreach $file (@todo) { + $file = "$dir/$file"; + if (-d $file) { + return undef if !&scrub_dir($file); + if (!rmdir($file)) { + print STDERR "$prog: couldn't rmdir $file - $!\n"; + return undef; + } + } else { + if (!unlink($file)) { + print STDERR "$prog: couldn't unlink $file - $!\n"; + return undef; + } + } + } + return 1; +} + +sub +write_file +{ + local($file, $str) = @_; + + if (!open(TEMP, "> $file")) { + print STDERR "$prog: can't open $file - $!\n"; + return undef; + } + binmode(TEMP); + print TEMP $str; + if (!close(TEMP)) { + print STDERR "$prog: error writing $file - $!\n"; + return undef; + } + return 1; +} + +sub +check_output +{ + local($name, $file, $what, $expect, $expect_pat) = @_; + local($got) = ''; + local($why) = ''; + local($ret); + + if (!open(TEMP, "< $file")) { + print STDERR "$prog:$name($what): couldn't open $file after running program - $!\n"; + return undef; + } + binmode(TEMP); + while () { + $got .= $_; + } + close(TEMP); + return compare_output($name, $what, $expect, $expect_pat, $got); +} + +sub +compare_output +{ + local($name, $what, $expect, $expect_pat, $got) = @_; + local($why) = ''; + + if (defined $expect_pat) { + $_ = $got; + $ret = eval "$expect_pat"; + if ($@ ne '') { + print STDERR "$prog:$name($what): error evaluating $what pattern: $expect_pat - $@\n"; + return undef; + } + if (!$ret) { + $why = "\tunexpected $what - wanted pattern"; + $why .= &wrap_lines($expect_pat); + $why .= "\tgot"; + $why .= &wrap_lines($got); + } + } else { + $expect = '' if !defined $expect; + if ($got ne $expect) { + $why .= "\tunexpected $what - " . &first_diff($expect, $got) . "\n"; + $why .= "\twanted"; + $why .= &wrap_lines($expect); + $why .= "\tgot"; + $why .= &wrap_lines($got); + } + } + return $why; +} + +sub +wrap_lines +{ + local($str, $empty) = @_; + local($nonl) = substr($str, -1, 1) ne "\n"; + + return (defined $empty ? $empty : " nothing\n") if $str eq ''; + substr($str, 0, 0) = ":\n"; + $str =~ s/\n/\n\t\t/g; + if ($nonl) { + $str .= "\n\t[incomplete last line]\n"; + } else { + chop($str); + chop($str); + } + return $str; +} + +sub +first_diff +{ + local($exp, $got) = @_; + local($lineno, $char) = (1, 1); + local($i, $exp_len, $got_len); + local($ce, $cg); + + $exp_len = length($exp); + $got_len = length($got); + if ($exp_len != $got_len) { + if ($exp_len < $got_len) { + if (substr($got, 0, $exp_len) eq $exp) { + return "got too much output"; + } + } elsif (substr($exp, 0, $got_len) eq $got) { + return "got too little output"; + } + } + for ($i = 0; $i < $exp_len; $i++) { + $ce = substr($exp, $i, 1); + $cg = substr($got, $i, 1); + last if $ce ne $cg; + $char++; + if ($ce eq "\n") { + $lineno++; + $char = 1; + } + } + return "first difference: line $lineno, char $char (wanted " . + &format_char($ce) . ", got " . &format_char($cg); +} + +sub +format_char +{ + local($ch, $s, $q); + + $ch = ord($_[0]); + $q = "'"; + + if ($is_ebcdic) { + if ($ch == 0x15) { + return $q . '\n' . $q; + } elsif ($ch == 0x16) { + return $q . '\b' . $q; + } elsif ($ch == 0x05) { + return $q . '\t' . $q; + } elsif ($ch < 64 || $ch == 255) { + return sprintf("X'%02X'", $ch); + } + return sprintf("'%c' (X'%02X')", $ch, $ch); + } + + $s = sprintf("0x%02X (", $ch); + if ($ch == 10) { + return $s . $q . '\n' . $q . ')'; + } elsif ($ch == 13) { + return $s . $q . '\r' . $q . ')'; + } elsif ($ch == 8) { + return $s . $q . '\b' . $q . ')'; + } elsif ($ch == 9) { + return $s . $q . '\t' . $q . ')'; + } elsif ($ch > 127) { + $ch -= 128; + $s .= "M-"; + } + if ($ch < 32) { + return sprintf("%s^%c)", $s, $ch + ord('@')); + } elsif ($ch == 127) { + return $s . "^?)"; + } + return sprintf("%s'%c')", $s, $ch); +} + +sub +eval_exit +{ + local($name, $status, $expect) = @_; + local($expr); + local($w, $e, $s) = ($status, ($status >> 8) & 0xff, $status & 0x7f); + + $e = -1000 if $status & 0xff; + $s = -1000 if $s == 0x7f; + if (!defined $expect) { + $expr = '$w == 0'; + } elsif ($expect =~ /^(|-)\d+$/) { + $expr = "\$e == $expect"; + } else { + $expr = $expect; + $expr =~ s/\b([wse])\b/\$$1/g; + $expr =~ s/\b(SIG[A-Z][A-Z0-9]*)\b/&$1/g; + } + $w = eval $expr; + if ($@ ne '') { + print STDERR "$prog:$test{':long-name'}: bad expected-exit expression: $expect ($@)\n"; + return undef; + } + return $w; +} + +sub +read_test +{ + local($file, $in, *test) = @_; + local($field, $val, $flags, $do_chop, $need_redo, $start_lineno); + local(%cnt, $sfield); + + %test = (); + %cnt = (); + while (<$in>) { + chop; + next if /^\s*$/; + next if /^ *#/; + last if /^\s*---\s*$/; + $start_lineno = $. if !defined $start_lineno; + if (!/^([-\w]+):\s*(|\S|\S.*\S)\s*$/) { + print STDERR "$prog:$file:$.: unrecognised line \"$_\"\n"; + return undef; + } + ($field, $val) = ($1, $2); + $sfield = $field; + $flags = $test_fields{$field}; + if (!defined $flags) { + print STDERR "$prog:$file:$.: unrecognised field \"$field\"\n"; + return undef; + } + if ($flags =~ /s/) { + local($cnt) = $cnt{$field}++; + $test{$field} = $cnt{$field}; + $cnt = 0 if $cnt eq ''; + $sfield .= ":$cnt"; + } elsif (defined $test{$field}) { + print STDERR "$prog:$file:$.: multiple \"$field\" fields\n"; + return undef; + } + $do_chop = $flags !~ /m/; + $need_redo = 0; + if ($val eq '' || $val eq '!' || $flags =~ /p/) { + if ($flags =~ /[Mm]/) { + if ($flags =~ /p/) { + if ($val =~ /^!/) { + $do_chop = 1; + $val = $'; + } else { + $do_chop = 0; + } + if ($val eq '') { + print STDERR + "$prog:$file:$.: no parameters given for field \"$field\"\n"; + return undef; + } + } else { + if ($val eq '!') { + $do_chop = 1; + } + $val = ''; + } + while (<$in>) { + last if !/^\t/; + $val .= $'; + } + chop $val if $do_chop; + $do_chop = 1; + $need_redo = 1; + + # Syntax check on fields that can several instances + # (can give useful line numbers this way) + + if ($field eq 'file-setup') { + local($type, $perm, $rest, $c, $len, $name); + + # format is: type perm "name" + if ($val !~ /^[ \t]*(\S+)[ \t]+(\S+)[ \t]+([^ \t].*)/) { + print STDERR + "$prog:$file:$.: bad parameter line for file-setup field\n"; + return undef; + } + ($type, $perm, $rest) = ($1, $2, $3); + if ($type !~ /^(file|dir|symlink)$/) { + print STDERR + "$prog:$file:$.: bad file type for file-setup: $type\n"; + return undef; + } + if ($perm !~ /^\d+$/) { + print STDERR + "$prog:$file:$.: bad permissions for file-setup: $type\n"; + return undef; + } + $c = substr($rest, 0, 1); + if (($len = index($rest, $c, 1) - 1) <= 0) { + print STDERR + "$prog:$file:$.: missing end quote for file name in file-setup: $rest\n"; + return undef; + } + $name = substr($rest, 1, $len); + if ($name =~ /^\// || $name =~ /(^|\/)\.\.(\/|$)/) { + # Note: this is not a security thing - just a sanity + # check - a test can still use symlinks to get at files + # outside the test directory. + print STDERR +"$prog:$file:$.: file name in file-setup is absolute or contains ..: $name\n"; + return undef; + } + } + if ($field eq 'file-result') { + local($type, $perm, $uid, $gid, $matchType, + $rest, $c, $len, $name); + + # format is: type perm uid gid matchType "name" + if ($val !~ /^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S.*)/) { + print STDERR + "$prog:$file:$.: bad parameter line for file-result field\n"; + return undef; + } + ($type, $perm, $uid, $gid, $matchType, $rest) + = ($1, $2, $3, $4, $5, $6); + if ($type !~ /^(file|dir|symlink)$/) { + print STDERR + "$prog:$file:$.: bad file type for file-result: $type\n"; + return undef; + } + if ($perm !~ /^\d+$/ && $perm ne '*') { + print STDERR + "$prog:$file:$.: bad permissions for file-result: $perm\n"; + return undef; + } + if ($uid !~ /^\d+$/ && $uid ne '*') { + print STDERR + "$prog:$file:$.: bad user-id for file-result: $uid\n"; + return undef; + } + if ($gid !~ /^\d+$/ && $gid ne '*') { + print STDERR + "$prog:$file:$.: bad group-id for file-result: $gid\n"; + return undef; + } + if ($matchType !~ /^(exact|pattern)$/) { + print STDERR + "$prog:$file:$.: bad match type for file-result: $matchType\n"; + return undef; + } + $c = substr($rest, 0, 1); + if (($len = index($rest, $c, 1) - 1) <= 0) { + print STDERR + "$prog:$file:$.: missing end quote for file name in file-result: $rest\n"; + return undef; + } + $name = substr($rest, 1, $len); + if ($name =~ /^\// || $name =~ /(^|\/)\.\.(\/|$)/) { + # Note: this is not a security thing - just a sanity + # check - a test can still use symlinks to get at files + # outside the test directory. + print STDERR +"$prog:$file:$.: file name in file-result is absolute or contains ..: $name\n"; + return undef; + } + } + } elsif ($val eq '') { + print STDERR + "$prog:$file:$.: no value given for field \"$field\"\n"; + return undef; + } + } + $val .= "\n" if !$do_chop; + $test{$sfield} = $val; + redo if $need_redo; + } + if ($_ eq '') { + if (%test) { + print STDERR + "$prog:$file:$start_lineno: end-of-file while reading test\n"; + return undef; + } + return 0; + } + + while (($field, $val) = each %test_fields) { + if ($val =~ /r/ && !defined $test{$field}) { + print STDERR + "$prog:$file:$start_lineno: required field \"$field\" missing\n"; + return undef; + } + } + + $test{':full-name'} = substr($file, $file_prefix_skip) . ":$test{'name'}"; + $test{':long-name'} = "$file:$start_lineno:$test{'name'}"; + + # Syntax check on specific fields + if (defined $test{'expected-fail'}) { + if ($test{'expected-fail'} !~ /^(yes|no)$/) { + print STDERR + "$prog:$test{':long-name'}: bad value for expected-fail field\n"; + return undef; + } + $test{'expected-fail'} = $1 eq 'yes'; + } else { + $test{'expected-fail'} = 0; + } + if (defined $test{'need-ctty'}) { + if ($test{'need-ctty'} !~ /^(yes|no)$/) { + print STDERR + "$prog:$test{':long-name'}: bad value for need-ctty field\n"; + return undef; + } + $test{'need-ctty'} = $1 eq 'yes'; + } else { + $test{'need-ctty'} = 0; + } + if (defined $test{'need-pass'}) { + if ($test{'need-pass'} !~ /^(yes|no)$/) { + print STDERR + "$prog:$test{':long-name'}: bad value for need-pass field\n"; + return undef; + } + $test{'need-pass'} = $1 eq 'yes'; + } else { + $test{'need-pass'} = 1; + } + if (defined $test{'arguments'}) { + local($firstc) = substr($test{'arguments'}, 0, 1); + + if (substr($test{'arguments'}, -1, 1) ne $firstc) { + print STDERR "$prog:$test{':long-name'}: arguments field doesn't start and end with the same character\n"; + return undef; + } + } + if (defined $test{'env-setup'}) { + local($firstc) = substr($test{'env-setup'}, 0, 1); + + if (substr($test{'env-setup'}, -1, 1) ne $firstc) { + print STDERR "$prog:$test{':long-name'}: env-setup field doesn't start and end with the same character\n"; + return undef; + } + + $test{'env-setup'} =~ s/\@utflocale\@/$utflocale/g; + } + if (defined $test{'expected-exit'}) { + local($val) = $test{'expected-exit'}; + + if ($val =~ /^(|-)\d+$/) { + if ($val < 0 || $val > 255) { + print STDERR "$prog:$test{':long-name'}: expected-exit value $val not in 0..255\n"; + return undef; + } + } elsif ($val !~ /^([\s\d<>+=*%\/&|!()-]|\b[wse]\b|\bSIG[A-Z][A-Z0-9]*\b)+$/) { + print STDERR "$prog:$test{':long-name'}: bad expected-exit expression: $val\n"; + return undef; + } + } else { + $test{'expected-exit'} = 0; + } + if (defined $test{'expected-stdout'} + && defined $test{'expected-stdout-pattern'}) + { + print STDERR "$prog:$test{':long-name'}: can't use both expected-stdout and expected-stdout-pattern\n"; + return undef; + } + if (defined $test{'expected-stderr'} + && defined $test{'expected-stderr-pattern'}) + { + print STDERR "$prog:$test{':long-name'}: can't use both expected-stderr and expected-stderr-pattern\n"; + return undef; + } + if (defined $test{'time-limit'}) { + if ($test{'time-limit'} !~ /^\d+$/ || $test{'time-limit'} == 0) { + print STDERR + "$prog:$test{':long-name'}: bad value for time-limit field\n"; + return undef; + } + } elsif (defined $default_time_limit) { + $test{'time-limit'} = $default_time_limit; + } + + if (defined $known_tests{$test{'name'}}) { + print STDERR "$prog:$test{':long-name'}: warning: duplicate test name ${test{'name'}}\n"; + } + $known_tests{$test{'name'}} = 1; + + return 1; +} + +sub +tty_msg +{ + local($msg) = @_; + + open(TTY, "> /dev/tty") || return 0; + print TTY $msg; + close(TTY); + return 1; +} + +sub +never_called_funcs +{ + return 0; + &tty_msg("hi\n"); + &never_called_funcs(); + &catch_sigalrm(); + $old_env{'foo'} = 'bar'; + $internal_test_fields{'foo'} = 'bar'; +} + +sub +check_file_result +{ + local(*test) = @_; + + return '' if (!defined $test{'file-result'}); + + local($why) = ''; + local($i); + local($type, $perm, $uid, $gid, $rest, $c, $len, $name); + local(@stbuf); + + for ($i = 0; $i < $test{'file-result'}; $i++) { + $val = $test{"file-result:$i"}; + + # format is: type perm "name" + ($type, $perm, $uid, $gid, $matchType, $rest) = + split(' ', $val, 6); + $c = substr($rest, 0, 1); + $len = index($rest, $c, 1) - 1; + $name = substr($rest, 1, $len); + $rest = substr($rest, 2 + $len); + $perm = oct($perm) if $perm =~ /^\d+$/; + + @stbuf = lstat($name); + if (!@stbuf) { + $why .= "\texpected $type \"$name\" not created\n"; + next; + } + if ($perm ne '*' && ($stbuf[2] & 07777) != $perm) { + $why .= "\t$type \"$name\" has unexpected permissions\n"; + $why .= sprintf("\t\texpected 0%o, found 0%o\n", + $perm, $stbuf[2] & 07777); + } + if ($uid ne '*' && $stbuf[4] != $uid) { + $why .= "\t$type \"$name\" has unexpected user-id\n"; + $why .= sprintf("\t\texpected %d, found %d\n", + $uid, $stbuf[4]); + } + if ($gid ne '*' && $stbuf[5] != $gid) { + $why .= "\t$type \"$name\" has unexpected group-id\n"; + $why .= sprintf("\t\texpected %d, found %d\n", + $gid, $stbuf[5]); + } + + if ($type eq 'file') { + if (-l _ || ! -f _) { + $why .= "\t$type \"$name\" is not a regular file\n"; + } else { + local $tmp = &check_output($test{'long-name'}, $name, + "$type contents in \"$name\"", + $matchType eq 'exact' ? $rest : undef + $matchType eq 'pattern' ? $rest : undef); + return undef if (!defined $tmp); + $why .= $tmp; + } + } elsif ($type eq 'dir') { + if ($rest !~ /^\s*$/) { + print STDERR "$prog:$test{':long-name'}: file-result test for directory $name should not have content specified\n"; + return undef; + } + if (-l _ || ! -d _) { + $why .= "\t$type \"$name\" is not a directory\n"; + } + } elsif ($type eq 'symlink') { + if (!-l _) { + $why .= "\t$type \"$name\" is not a symlink\n"; + } else { + local $content = readlink($name); + if (!defined $content) { + print STDERR "$prog:$test{':long-name'}: file-result test for $type $name failed - could not readlink - $!\n"; + return undef; + } + local $tmp = &compare_output($test{'long-name'}, + "$type contents in \"$name\"", + $matchType eq 'exact' ? $rest : undef + $matchType eq 'pattern' ? $rest : undef); + return undef if (!defined $tmp); + $why .= $tmp; + } + } + } + + return $why; +} + +sub +HELP_MESSAGE +{ + print STDERR $Usage; + exit 0; +} diff --git a/check.t b/check.t new file mode 100644 index 0000000..ea414d3 --- /dev/null +++ b/check.t @@ -0,0 +1,13435 @@ +# $MirOS: src/bin/mksh/check.t,v 1.801 2018/01/14 01:47:33 tg Exp $ +# -*- mode: sh -*- +#- +# Copyright © 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016, 2017 +# mirabilos +# +# Provided that these terms and disclaimer and all copyright notices +# are retained or reproduced in an accompanying document, permission +# is granted to deal in this work without restriction, including un‐ +# limited rights to use, publicly perform, distribute, sell, modify, +# merge, give away, or sublicence. +# +# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to +# the utmost extent permitted by applicable law, neither express nor +# implied; without malicious intent or gross negligence. In no event +# may a licensor, author or contributor be held liable for indirect, +# direct, other damage, loss, or other issues arising in any way out +# of dealing in the work, even if advised of the possibility of such +# damage or existence of a defect, except proven that it results out +# of said person’s immediate fault when using the work as intended. +#- +# You may also want to test IFS with the script at +# http://www.research.att.com/~gsf/public/ifs.sh +# +# More testsuites at: +# http://svnweb.freebsd.org/base/head/bin/test/tests/legacy_test.sh?view=co&content-type=text%2Fplain +# +# Integrated testsuites from: +# (2013/12/02 20:39:44) http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/regress/bin/ksh/?sortby=date + +expected-stdout: + @(#)MIRBSD KSH R56 2018/01/14 +description: + Check base version of full shell +stdin: + echo ${KSH_VERSION%%' +'*} +name: KSH_VERSION +category: !shell:legacy-yes +--- +expected-stdout: + @(#)LEGACY KSH R56 2018/01/14 +description: + Check base version of legacy shell +stdin: + echo ${KSH_VERSION%%' +'*} +name: KSH_VERSION-legacy +category: !shell:legacy-no +--- +name: KSH_VERSION-ascii +description: + Check that the shell version tag does not include EBCDIC +category: !shell:ebcdic-yes +stdin: + for x in $KSH_VERSION; do + [[ $x = '+EBCDIC' ]] && exit 1 + done + exit 0 +--- +name: KSH_VERSION-ebcdic +description: + Check that the shell version tag includes EBCDIC +category: !shell:ebcdic-no +stdin: + for x in $KSH_VERSION; do + [[ $x = '+EBCDIC' ]] && exit 0 + done + exit 1 +--- +name: KSH_VERSION-binmode +description: + Check that the shell version tag does not include TEXTMODE +category: !shell:textmode-yes +stdin: + for x in $KSH_VERSION; do + [[ $x = '+TEXTMODE' ]] && exit 1 + done + exit 0 +--- +name: KSH_VERSION-textmode +description: + Check that the shell version tag includes TEXTMODE +category: !shell:textmode-no +stdin: + for x in $KSH_VERSION; do + [[ $x = '+TEXTMODE' ]] && exit 0 + done + exit 1 +--- +name: selftest-1 +description: + Regression test self-testing +stdin: + echo ${foo:-baz} +expected-stdout: + baz +--- +name: selftest-2 +description: + Regression test self-testing +env-setup: !foo=bar! +stdin: + echo ${foo:-baz} +expected-stdout: + bar +--- +name: selftest-3 +description: + Regression test self-testing +env-setup: !ENV=fnord! +stdin: + echo "<$ENV>" +expected-stdout: + +--- +name: selftest-exec +description: + Ensure that the test run directory (default /tmp but can be changed + with check.pl flag -T or test.sh $TMPDIR) is not mounted noexec, as + we execute scripts from the scratch directory during several tests. +stdin: + print '#!'"$__progname"'\necho tf' >lq + chmod +x lq + ./lq +expected-stdout: + tf +--- +name: selftest-env +description: + Just output the environment variables set (always fails) +category: disabled +stdin: + set +--- +name: selftest-direct-builtin-call +description: + Check that direct builtin calls work +stdin: + ln -s "$__progname" cat || cp "$__progname" cat + ln -s "$__progname" echo || cp "$__progname" echo + ./echo -c 'echo foo' | ./cat -u +expected-stdout: + -c echo foo +--- +name: selftest-pathsep-unix +description: + Check that $PATHSEP is set correctly. +category: !os:os2 +stdin: + PATHSEP=.; export PATHSEP + "$__progname" -c 'print -r -- $PATHSEP' +expected-stdout: + : +--- +name: selftest-pathsep-dospath +description: + Check that $PATHSEP is set correctly. +category: os:os2 +stdin: + PATHSEP=.; export PATHSEP + "$__progname" -c 'print -r -- $PATHSEP' +expected-stdout: + ; +--- +name: alias-1 +description: + Check that recursion is detected/avoided in aliases. +stdin: + alias fooBar=fooBar + fooBar + exit 0 +expected-stderr-pattern: + /fooBar.*not found.*/ +--- +name: alias-2 +description: + Check that recursion is detected/avoided in aliases. +stdin: + alias fooBar=barFoo + alias barFoo=fooBar + fooBar + barFoo + exit 0 +expected-stderr-pattern: + /fooBar.*not found.*\n.*barFoo.*not found/ +--- +name: alias-3 +description: + Check that recursion is detected/avoided in aliases. +stdin: + alias Echo='echo ' + alias fooBar=barFoo + alias barFoo=fooBar + Echo fooBar + unalias barFoo + Echo fooBar +expected-stdout: + fooBar + barFoo +--- +name: alias-4 +description: + Check that alias expansion isn't done on keywords (in keyword + postitions). +stdin: + alias Echo='echo ' + alias while=While + while false; do echo hi ; done + Echo while +expected-stdout: + While +--- +name: alias-5 +description: + Check that alias expansion done after alias with trailing space. +stdin: + alias Echo='echo ' + alias foo='bar stuff ' + alias bar='Bar1 Bar2 ' + alias stuff='Stuff' + alias blah='Blah' + Echo foo blah +expected-stdout: + Bar1 Bar2 Stuff Blah +--- +name: alias-6 +description: + Check that alias expansion done after alias with trailing space. +stdin: + alias Echo='echo ' + alias foo='bar bar' + alias bar='Bar ' + alias blah=Blah + Echo foo blah +expected-stdout: + Bar Bar Blah +--- +name: alias-7 +description: + Check that alias expansion done after alias with trailing space + after a keyword. +stdin: + alias X='case ' + alias Y=Z + X Y in 'Y') echo is y ;; Z) echo is z ;; esac +expected-stdout: + is z +--- +name: alias-8 +description: + Check that newlines in an alias don't cause the command to be lost. +stdin: + alias foo=' + + + echo hi + + + + echo there + + + ' + foo +expected-stdout: + hi + there +--- +name: alias-9 +description: + Check that recursion is detected/avoided in aliases. + This check fails for slow machines or Cygwin, raise + the time-limit clause (e.g. to 7) if this occurs. +time-limit: 3 +stdin: + print '#!'"$__progname"'\necho tf' >lq + chmod +x lq + PATH=$PWD$PATHSEP$PATH + alias lq=lq + lq + echo = now + i=`lq` + print -r -- $i + echo = out + exit 0 +expected-stdout: + tf + = now + tf + = out +--- +name: alias-10 +description: + Check that recursion is detected/avoided in aliases. + Regression, introduced during an old bugfix. +stdin: + alias foo='print hello ' + alias bar='foo world' + echo $(bar) +expected-stdout: + hello world +--- +name: alias-11 +description: + Check that special argument handling still applies with escaped aliases +stdin: + alias local1='\typeset' + alias local2='\\builtin typeset' + function fooa { + local1 x=$1 y=z + print -r -- "$x,$y" + } + function foob { + local2 x=$1 y=z + print -r -- "$x,$y" + } + x=1 y=2; fooa 'bar - baz' + x=1 y=2; foob 'bar - baz' +expected-stdout: + bar - baz,z + bar - baz,z +--- +name: alias-12 +description: + Something weird from Martijn Dekker +stdin: + alias echo=print + x() { echo a; (echo b); x=$(echo c); } + typeset -f x + alias OPEN='{' CLOSE='};' + { OPEN echo hi1; CLOSE } + var=`{ OPEN echo hi2; CLOSE }` && echo "$var" + var=$({ OPEN echo hi3; CLOSE }) && echo "$var" +expected-stdout: + x() { + \print a + ( \print b ) + x=$(\print c ) + } + hi1 + hi2 + hi3 +--- +name: arith-compound +description: + Check that arithmetic expressions are compound constructs +stdin: + { ! (( 0$(cat >&2) )) <<<1; } <<<2 +expected-stderr: + 1 +--- +name: arith-lazy-1 +description: + Check that only one side of ternary operator is evaluated +stdin: + x=i+=2 + y=j+=2 + typeset -i i=1 j=1 + echo $((1 ? 20 : (x+=2))) + echo $i,$x + echo $((0 ? (y+=2) : 30)) + echo $j,$y +expected-stdout: + 20 + 1,i+=2 + 30 + 1,j+=2 +--- +name: arith-lazy-2 +description: + Check that assignments not done on non-evaluated side of ternary + operator +stdin: + x=i+=2 + y=j+=2 + typeset -i i=1 j=1 + echo $((1 ? 20 : (x+=2))) + echo $i,$x + echo $((0 ? (y+=2) : 30)) + echo $i,$y +expected-stdout: + 20 + 1,i+=2 + 30 + 1,j+=2 +--- +name: arith-lazy-3 +description: + Check that assignments not done on non-evaluated side of ternary + operator and this construct is parsed correctly (Debian #445651) +stdin: + x=4 + y=$((0 ? x=1 : 2)) + echo = $x $y = +expected-stdout: + = 4 2 = +--- +name: arith-lazy-4 +description: + Check that preun/postun not done on non-evaluated side of ternary + operator +stdin: + (( m = n = 0, 1 ? n++ : m++ ? 2 : 3 )) + echo "($n, $m)" + m=0; echo $(( 0 ? ++m : 2 )); echo $m + m=0; echo $(( 0 ? m++ : 2 )); echo $m +expected-stdout: + (1, 0) + 2 + 0 + 2 + 0 +--- +name: arith-lazy-5-arr-n +description: Check lazy evaluation with side effects +stdin: + a=0; echo "$((0&&b[a++],a))" +expected-stdout: + 0 +--- +name: arith-lazy-5-arr-p +description: Check lazy evaluation with side effects +stdin: + a=0; echo "$((0&&(b[a++]),a))" +expected-stdout: + 0 +--- +name: arith-lazy-5-str-n +description: Check lazy evaluation with side effects +stdin: + a=0 b=a++; ((0&&b)); echo $a +expected-stdout: + 0 +--- +name: arith-lazy-5-str-p +description: Check lazy evaluation with side effects +stdin: + a=0 b=a++; ((0&&(b))); echo $a +expected-stdout: + 0 +--- +name: arith-lazy-5-tern-l-n +description: Check lazy evaluation with side effects +stdin: + a=0; echo "$((0?b[a++]:999,a))" +expected-stdout: + 0 +--- +name: arith-lazy-5-tern-l-p +description: Check lazy evaluation with side effects +stdin: + a=0; echo "$((0?(b[a++]):999,a))" +expected-stdout: + 0 +--- +name: arith-lazy-5-tern-r-n +description: Check lazy evaluation with side effects +stdin: + a=0; echo "$((1?999:b[a++],a))" +expected-stdout: + 0 +--- +name: arith-lazy-5-tern-r-p +description: Check lazy evaluation with side effects +stdin: + a=0; echo "$((1?999:(b[a++]),a))" +expected-stdout: + 0 +--- +name: arith-ternary-prec-1 +description: + Check precedence of ternary operator vs assignment +stdin: + typeset -i x=2 + y=$((1 ? 20 : x+=2)) +expected-exit: e != 0 +expected-stderr-pattern: + /.*:.*1 \? 20 : x\+=2.*lvalue.*\n$/ +--- +name: arith-ternary-prec-2 +description: + Check precedence of ternary operator vs assignment +stdin: + typeset -i x=2 + echo $((0 ? x+=2 : 20)) +expected-stdout: + 20 +--- +name: arith-prec-1 +description: + Prove arithmetic expressions with embedded parameter + substitutions cannot be parsed ahead of time +stdin: + a='3 + 4' + print 1 $((2 * a)) . + print 2 $((2 * $a)) . +expected-stdout: + 1 14 . + 2 10 . +--- +name: arith-div-assoc-1 +description: + Check associativity of division operator +stdin: + echo $((20 / 2 / 2)) +expected-stdout: + 5 +--- +name: arith-div-byzero +description: + Check division by zero errors out +stdin: + x=$(echo $((1 / 0))) + echo =$?:$x. +expected-stdout: + =1:. +expected-stderr-pattern: + /.*divisor/ +--- +name: arith-div-intmin-by-minusone +description: + Check division overflow wraps around silently +category: int:32 +stdin: + echo signed:$((-2147483648 / -1))r$((-2147483648 % -1)). + echo unsigned:$((# -2147483648 / -1))r$((# -2147483648 % -1)). +expected-stdout: + signed:-2147483648r0. + unsigned:0r2147483648. +--- +name: arith-div-intmin-by-minusone-64 +description: + Check division overflow wraps around silently +category: int:64 +stdin: + echo signed:$((-9223372036854775808 / -1))r$((-9223372036854775808 % -1)). + echo unsigned:$((# -9223372036854775808 / -1))r$((# -9223372036854775808 % -1)). +expected-stdout: + signed:-9223372036854775808r0. + unsigned:0r9223372036854775808. +--- +name: arith-assop-assoc-1 +description: + Check associativity of assignment-operator operator +stdin: + typeset -i i=1 j=2 k=3 + echo $((i += j += k)) + echo $i,$j,$k +expected-stdout: + 6 + 6,5,3 +--- +name: arith-mandatory +description: + Passing of this test is *mandatory* for a valid mksh executable! +category: shell:legacy-no +stdin: + typeset -i sari=0 + typeset -Ui uari=0 + typeset -i x=0 + print -r -- $((x++)):$sari=$uari. #0 + let --sari --uari + print -r -- $((x++)):$sari=$uari. #1 + sari=2147483647 uari=2147483647 + print -r -- $((x++)):$sari=$uari. #2 + let ++sari ++uari + print -r -- $((x++)):$sari=$uari. #3 + let --sari --uari + let 'sari *= 2' 'uari *= 2' + let ++sari ++uari + print -r -- $((x++)):$sari=$uari. #4 + let ++sari ++uari + print -r -- $((x++)):$sari=$uari. #5 + sari=-2147483648 uari=-2147483648 + print -r -- $((x++)):$sari=$uari. #6 + let --sari --uari + print -r -- $((x++)):$sari=$uari. #7 + (( sari = -5 >> 1 )) + ((# uari = -5 >> 1 )) + print -r -- $((x++)):$sari=$uari. #8 + (( sari = -2 )) + ((# uari = sari )) + print -r -- $((x++)):$sari=$uari. #9 +expected-stdout: + 0:0=0. + 1:-1=4294967295. + 2:2147483647=2147483647. + 3:-2147483648=2147483648. + 4:-1=4294967295. + 5:0=0. + 6:-2147483648=2147483648. + 7:2147483647=2147483647. + 8:-3=2147483645. + 9:-2=4294967294. +--- +name: arith-unsigned-1 +description: + Check if unsigned arithmetics work +category: int:32 +stdin: + # signed vs unsigned + echo x1 $((-1)) $((#-1)) + # calculating + typeset -i vs + typeset -Ui vu + vs=4123456789; vu=4123456789 + echo x2 $vs $vu + (( vs %= 2147483647 )) + (( vu %= 2147483647 )) + echo x3 $vs $vu + vs=4123456789; vu=4123456789 + (( # vs %= 2147483647 )) + (( # vu %= 2147483647 )) + echo x4 $vs $vu + # make sure the calculation does not change unsigned flag + vs=4123456789; vu=4123456789 + echo x5 $vs $vu + # short form + echo x6 $((# vs % 2147483647)) $((# vu % 2147483647)) + # array refs + set -A va + va[1975973142]=right + va[4123456789]=wrong + echo x7 ${va[#4123456789%2147483647]} + # make sure multiple calculations don't interfere with each other + let '# mca = -4 % -2' ' mcb = -4 % -2' + echo x8 $mca $mcb +expected-stdout: + x1 -1 4294967295 + x2 -171510507 4123456789 + x3 -171510507 4123456789 + x4 1975973142 1975973142 + x5 -171510507 4123456789 + x6 1975973142 1975973142 + x7 right + x8 -4 0 +--- +name: arith-limit32-1 +description: + Check if arithmetics are 32 bit +category: int:32 +stdin: + # signed vs unsigned + echo x1 $((-1)) $((#-1)) + # calculating + typeset -i vs + typeset -Ui vu + vs=2147483647; vu=2147483647 + echo x2 $vs $vu + let vs++ vu++ + echo x3 $vs $vu + vs=4294967295; vu=4294967295 + echo x4 $vs $vu + let vs++ vu++ + echo x5 $vs $vu + let vs++ vu++ + echo x6 $vs $vu +expected-stdout: + x1 -1 4294967295 + x2 2147483647 2147483647 + x3 -2147483648 2147483648 + x4 -1 4294967295 + x5 0 0 + x6 1 1 +--- +name: arith-limit64-1 +description: + Check if arithmetics are 64 bit +category: int:64 +stdin: + # signed vs unsigned + echo x1 $((-1)) $((#-1)) + # calculating + typeset -i vs + typeset -Ui vu + vs=9223372036854775807; vu=9223372036854775807 + echo x2 $vs $vu + let vs++ vu++ + echo x3 $vs $vu + vs=18446744073709551615; vu=18446744073709551615 + echo x4 $vs $vu + let vs++ vu++ + echo x5 $vs $vu + let vs++ vu++ + echo x6 $vs $vu +expected-stdout: + x1 -1 18446744073709551615 + x2 9223372036854775807 9223372036854775807 + x3 -9223372036854775808 9223372036854775808 + x4 -1 18446744073709551615 + x5 0 0 + x6 1 1 +--- +name: bksl-nl-ign-1 +description: + Check that \newline is not collapsed after # +stdin: + echo hi #there \ + echo folks +expected-stdout: + hi + folks +--- +name: bksl-nl-ign-2 +description: + Check that \newline is not collapsed inside single quotes +stdin: + echo 'hi \ + there' + echo folks +expected-stdout: + hi \ + there + folks +--- +name: bksl-nl-ign-3 +description: + Check that \newline is not collapsed inside single quotes +stdin: + cat << \EOF + hi \ + there + EOF +expected-stdout: + hi \ + there +--- +name: bksl-nl-ign-4 +description: + Check interaction of aliases, single quotes and here-documents + with backslash-newline + (don't know what POSIX has to say about this) +stdin: + a=2 + alias x='echo hi + cat << "EOF" + foo\ + bar + some' + x + more\ + stuff$a + EOF +expected-stdout: + hi + foo\ + bar + some + more\ + stuff$a +--- +name: bksl-nl-ign-5 +description: + Check what happens with backslash at end of input + (the old Bourne shell trashes them; so do we) +stdin: ! + echo `echo foo\\`bar + echo hi\ +expected-stdout: + foobar + hi +--- +# +# Places \newline should be collapsed +# +name: bksl-nl-1 +description: + Check that \newline is collapsed before, in the middle of, and + after words +stdin: + \ + echo hi\ + There, \ + folks +expected-stdout: + hiThere, folks +--- +name: bksl-nl-2 +description: + Check that \newline is collapsed in $ sequences + (ksh93 fails this) +stdin: + a=12 + ab=19 + echo $\ + a + echo $a\ + b + echo $\ + {a} + echo ${a\ + b} + echo ${ab\ + } +expected-stdout: + 12 + 19 + 12 + 19 + 19 +--- +name: bksl-nl-3 +description: + Check that \newline is collapsed in $(..) and `...` sequences + (ksh93 fails this) +stdin: + echo $\ + (echo foobar1) + echo $(\ + echo foobar2) + echo $(echo foo\ + bar3) + echo $(echo foobar4\ + ) + echo ` + echo stuff1` + echo `echo st\ + uff2` +expected-stdout: + foobar1 + foobar2 + foobar3 + foobar4 + stuff1 + stuff2 +--- +name: bksl-nl-4 +description: + Check that \newline is collapsed in $((..)) sequences + (ksh93 fails this) +stdin: + echo $\ + ((1+2)) + echo $(\ + (1+2+3)) + echo $((\ + 1+2+3+4)) + echo $((1+\ + 2+3+4+5)) + echo $((1+2+3+4+5+6)\ + ) +expected-stdout: + 3 + 6 + 10 + 15 + 21 +--- +name: bksl-nl-5 +description: + Check that \newline is collapsed in double quoted strings +stdin: + echo "\ + hi" + echo "foo\ + bar" + echo "folks\ + " +expected-stdout: + hi + foobar + folks +--- +name: bksl-nl-6 +description: + Check that \newline is collapsed in here document delimiters + (ksh93 fails second part of this) +stdin: + a=12 + cat << EO\ + F + a=$a + foo\ + bar + EOF + cat << E_O_F + foo + E_O_\ + F + echo done +expected-stdout: + a=12 + foobar + foo + done +--- +name: bksl-nl-7 +description: + Check that \newline is collapsed in double-quoted here-document + delimiter. +stdin: + a=12 + cat << "EO\ + F" + a=$a + foo\ + bar + EOF + echo done +expected-stdout: + a=$a + foo\ + bar + done +--- +name: bksl-nl-8 +description: + Check that \newline is collapsed in various 2+ character tokens + delimiter. + (ksh93 fails this) +stdin: + echo hi &\ + & echo there + echo foo |\ + | echo bar + cat <\ + < EOF + stuff + EOF + cat <\ + <\ + - EOF + more stuff + EOF + cat <<\ + EOF + abcdef + EOF + echo hi >\ + > /dev/null + echo $? + i=1 + case $i in + (\ + x|\ + 1\ + ) echo hi;\ + ; + (*) echo oops + esac +expected-stdout: + hi + there + foo + stuff + more stuff + abcdef + 0 + hi +--- +name: bksl-nl-9 +description: + Check that \ at the end of an alias is collapsed when followed + by a newline + (don't know what POSIX has to say about this) +stdin: + alias x='echo hi\' + x + echo there +expected-stdout: + hiecho there +--- +name: bksl-nl-10 +description: + Check that \newline in a keyword is collapsed +stdin: + i\ + f true; then\ + echo pass; el\ + se echo fail; fi +expected-stdout: + pass +--- +# +# Places \newline should be collapsed (ksh extensions) +# +name: bksl-nl-ksh-1 +description: + Check that \newline is collapsed in extended globbing + (ksh93 fails this) +stdin: + xxx=foo + case $xxx in + (f*\ + (\ + o\ + )\ + ) echo ok ;; + *) echo bad + esac +expected-stdout: + ok +--- +name: bksl-nl-ksh-2 +description: + Check that \newline is collapsed in ((...)) expressions + (ksh93 fails this) +stdin: + i=1 + (\ + (\ + i=i+2\ + )\ + ) + echo $i +expected-stdout: + 3 +--- +name: break-1 +description: + See if break breaks out of loops +stdin: + for i in a b c; do echo $i; break; echo bad-$i; done + echo end-1 + for i in a b c; do echo $i; break 1; echo bad-$i; done + echo end-2 + for i in a b c; do + for j in x y z; do + echo $i:$j + break + echo bad-$i + done + echo end-$i + done + echo end-3 + for i in a b c; do echo $i; eval break; echo bad-$i; done + echo end-4 +expected-stdout: + a + end-1 + a + end-2 + a:x + end-a + b:x + end-b + c:x + end-c + end-3 + a + end-4 +--- +name: break-2 +description: + See if break breaks out of nested loops +stdin: + for i in a b c; do + for j in x y z; do + echo $i:$j + break 2 + echo bad-$i + done + echo end-$i + done + echo end +expected-stdout: + a:x + end +--- +name: break-3 +description: + What if break used outside of any loops + (ksh88,ksh93 don't print error messages here) +stdin: + break +expected-stderr-pattern: + /.*break.*/ +--- +name: break-4 +description: + What if break N used when only N-1 loops + (ksh88,ksh93 don't print error messages here) +stdin: + for i in a b c; do echo $i; break 2; echo bad-$i; done + echo end +expected-stdout: + a + end +expected-stderr-pattern: + /.*break.*/ +--- +name: break-5 +description: + Error if break argument isn't a number +stdin: + for i in a b c; do echo $i; break abc; echo more-$i; done + echo end +expected-stdout: + a +expected-exit: e != 0 +expected-stderr-pattern: + /.*break.*/ +--- +name: continue-1 +description: + See if continue continues loops +stdin: + for i in a b c; do echo $i; continue; echo bad-$i ; done + echo end-1 + for i in a b c; do echo $i; continue 1; echo bad-$i; done + echo end-2 + for i in a b c; do + for j in x y z; do + echo $i:$j + continue + echo bad-$i-$j + done + echo end-$i + done + echo end-3 + for i in a b c; do echo $i; eval continue; echo bad-$i ; done + echo end-4 +expected-stdout: + a + b + c + end-1 + a + b + c + end-2 + a:x + a:y + a:z + end-a + b:x + b:y + b:z + end-b + c:x + c:y + c:z + end-c + end-3 + a + b + c + end-4 +--- +name: continue-2 +description: + See if continue breaks out of nested loops +stdin: + for i in a b c; do + for j in x y z; do + echo $i:$j + continue 2 + echo bad-$i-$j + done + echo end-$i + done + echo end +expected-stdout: + a:x + b:x + c:x + end +--- +name: continue-3 +description: + What if continue used outside of any loops + (ksh88,ksh93 don't print error messages here) +stdin: + continue +expected-stderr-pattern: + /.*continue.*/ +--- +name: continue-4 +description: + What if continue N used when only N-1 loops + (ksh88,ksh93 don't print error messages here) +stdin: + for i in a b c; do echo $i; continue 2; echo bad-$i; done + echo end +expected-stdout: + a + b + c + end +expected-stderr-pattern: + /.*continue.*/ +--- +name: continue-5 +description: + Error if continue argument isn't a number +stdin: + for i in a b c; do echo $i; continue abc; echo more-$i; done + echo end +expected-stdout: + a +expected-exit: e != 0 +expected-stderr-pattern: + /.*continue.*/ +--- +name: cd-history +description: + Test someone's CD history package (uses arrays) +stdin: + # go to known place before doing anything + cd / + + alias cd=_cd + function _cd + { + typeset -i cdlen i + typeset t + + if [ $# -eq 0 ] + then + set -- $HOME + fi + + if [ "$CDHISTFILE" -a -r "$CDHISTFILE" ] # if directory history exists + then + typeset CDHIST + i=-1 + while read -r t # read directory history file + do + CDHIST[i=i+1]=$t + done <$CDHISTFILE + fi + + if [ "${CDHIST[0]}" != "$PWD" -a "$PWD" != "" ] + then + _cdins # insert $PWD into cd history + fi + + cdlen=${#CDHIST[*]} # number of elements in history + + case "$@" in + -) # cd to new dir + if [ "$OLDPWD" = "" ] && ((cdlen>1)) + then + 'print' ${CDHIST[1]} + 'cd' ${CDHIST[1]} + _pwd + else + 'cd' $@ + _pwd + fi + ;; + -l) # print directory list + typeset -R3 num + ((i=cdlen)) + while (((i=i-1)>=0)) + do + num=$i + 'print' "$num ${CDHIST[i]}" + done + return + ;; + -[0-9]|-[0-9][0-9]) # cd to dir in list + if (((i=${1#-})=cdlen)) + then + 'cd' $@ + _pwd + fi + ;; + *) # cd to new dir + 'cd' $@ + _pwd + ;; + esac + + _cdins # insert $PWD into cd history + + if [ "$CDHISTFILE" ] + then + cdlen=${#CDHIST[*]} # number of elements in history + + i=0 + while ((i$CDHISTFILE + fi + } + + function _cdins # insert $PWD into cd history + { # meant to be called only by _cd + typeset -i i + + ((i=0)) + while ((i<${#CDHIST[*]})) # see if dir is already in list + do + if [ "${CDHIST[$i]}" = "$PWD" ] + then + break + fi + ((i=i+1)) + done + + if ((i>22)) # limit max size of list + then + i=22 + fi + + while (((i=i-1)>=0)) # bump old dirs in list + do + CDHIST[i+1]=${CDHIST[i]} + done + + CDHIST[0]=$PWD # insert new directory in list + } + + + function _pwd + { + if [ -n "$ECD" ] + then + pwd 1>&6 + fi + } + # Start of test + cd /tmp + cd /bin + cd /etc + cd - + cd -2 + cd -l +expected-stdout: + /bin + /tmp + 3 / + 2 /etc + 1 /bin + 0 /tmp +--- +name: cd-pe +description: + Check package for cd -Pe +need-pass: no +# the mv command fails on Cygwin and z/OS +# Hurd aborts the testsuite (permission denied) +# QNX does not find subdir to cd into +category: !os:cygwin,!os:gnu,!os:msys,!os:nto,!os:os390,!nosymlink +file-setup: file 644 "x" + mkdir noread noread/target noread/target/subdir + ln -s noread link + chmod 311 noread + cd -P$1 . + echo 0=$? + bwd=$PWD + cd -P$1 link/target + echo 1=$?,${PWD#$bwd/} + epwd=$($TSHELL -c pwd 2>/dev/null) + # This unexpectedly succeeds on GNU/Linux and MidnightBSD + #echo pwd=$?,$epwd + # expect: pwd=1, + mv ../../noread ../../renamed + cd -P$1 subdir + echo 2=$?,${PWD#$bwd/} + cd $bwd + chmod 755 noread renamed 2>/dev/null + rm -rf noread link renamed +stdin: + export TSHELL="$__progname" + "$__progname" x + echo "now with -e:" + "$__progname" x e +expected-stdout: + 0=0 + 1=0,noread/target + 2=0,noread/target/subdir + now with -e: + 0=0 + 1=0,noread/target + 2=1,noread/target/subdir +--- +name: env-prompt +description: + Check that prompt not printed when processing ENV +env-setup: !ENV=./foo! +file-setup: file 644 "foo" + XXX=_ + PS1=X + false && echo hmmm +need-ctty: yes +arguments: !-i! +stdin: + echo hi${XXX}there +expected-stdout: + hi_there +expected-stderr: ! + XX +--- +name: expand-ugly +description: + Check that weird ${foo+bar} constructs are parsed correctly +stdin: + print '#!'"$__progname"'\nfor x in "$@"; do print -r -- "$x"; done' >pfn + print '#!'"$__progname"'\nfor x in "$@"; do print -nr -- "<$x> "; done' >pfs + chmod +x pfn pfs + (echo 1 ${IFS+'}'z}) 2>/dev/null || echo failed in 1 + (echo 2 "${IFS+'}'z}") 2>/dev/null || echo failed in 2 + (echo 3 "foo ${IFS+'bar} baz") 2>/dev/null || echo failed in 3 + (echo -n '4 '; ./pfn "foo ${IFS+"b c"} baz") 2>/dev/null || echo failed in 4 + (echo -n '5 '; ./pfn "foo ${IFS+b c} baz") 2>/dev/null || echo failed in 5 + (echo 6 ${IFS+"}"z}) 2>/dev/null || echo failed in 6 + (echo 7 "${IFS+"}"z}") 2>/dev/null || echo failed in 7 + (echo 8 "${IFS+\"}\"z}") 2>/dev/null || echo failed in 8 + (echo 9 "${IFS+\"\}\"z}") 2>/dev/null || echo failed in 9 + (echo 10 foo ${IFS+'bar} baz'}) 2>/dev/null || echo failed in 10 + (echo 11 "$(echo "${IFS+'}'z}")") 2>/dev/null || echo failed in 11 + (echo 12 "$(echo ${IFS+'}'z})") 2>/dev/null || echo failed in 12 + (echo 13 ${IFS+\}z}) 2>/dev/null || echo failed in 13 + (echo 14 "${IFS+\}z}") 2>/dev/null || echo failed in 14 + u=x; (echo -n '15 '; ./pfs "foo ${IFS+a"b$u{ {"{{\}b} c ${IFS+d{}} bar" ${IFS-e{}} baz; echo .) 2>/dev/null || echo failed in 15 + l=t; (echo 16 ${IFS+h`echo -n i ${IFS+$l}h`ere}) 2>/dev/null || echo failed in 16 + l=t; (echo 17 ${IFS+h$(echo -n i ${IFS+$l}h)ere}) 2>/dev/null || echo failed in 17 + l=t; (echo 18 "${IFS+h`echo -n i ${IFS+$l}h`ere}") 2>/dev/null || echo failed in 18 + l=t; (echo 19 "${IFS+h$(echo -n i ${IFS+$l}h)ere}") 2>/dev/null || echo failed in 19 + l=t; (echo 20 ${IFS+h`echo -n i "${IFS+$l}"h`ere}) 2>/dev/null || echo failed in 20 + l=t; (echo 21 ${IFS+h$(echo -n i "${IFS+$l}"h)ere}) 2>/dev/null || echo failed in 21 + l=t; (echo 22 "${IFS+h`echo -n i "${IFS+$l}"h`ere}") 2>/dev/null || echo failed in 22 + l=t; (echo 23 "${IFS+h$(echo -n i "${IFS+$l}"h)ere}") 2>/dev/null || echo failed in 23 + key=value; (echo -n '24 '; ./pfn "${IFS+'$key'}") 2>/dev/null || echo failed in 24 + key=value; (echo -n '25 '; ./pfn "${IFS+"'$key'"}") 2>/dev/null || echo failed in 25 # ksh93: “'$key'” + key=value; (echo -n '26 '; ./pfn ${IFS+'$key'}) 2>/dev/null || echo failed in 26 + key=value; (echo -n '27 '; ./pfn ${IFS+"'$key'"}) 2>/dev/null || echo failed in 27 + (echo -n '28 '; ./pfn "${IFS+"'"x ~ x'}'x"'}"x}" #') 2>/dev/null || echo failed in 28 + u=x; (echo -n '29 '; ./pfs foo ${IFS+a"b$u{ {"{ {\}b} c ${IFS+d{}} bar ${IFS-e{}} baz; echo .) 2>/dev/null || echo failed in 29 + (echo -n '30 '; ./pfs ${IFS+foo 'b\ + ar' baz}; echo .) 2>/dev/null || (echo failed in 30; echo failed in 31) + (echo -n '32 '; ./pfs ${IFS+foo "b\ + ar" baz}; echo .) 2>/dev/null || echo failed in 32 + (echo -n '33 '; ./pfs "${IFS+foo 'b\ + ar' baz}"; echo .) 2>/dev/null || echo failed in 33 + (echo -n '34 '; ./pfs "${IFS+foo "b\ + ar" baz}"; echo .) 2>/dev/null || echo failed in 34 + (echo -n '35 '; ./pfs ${v=a\ b} x ${v=c\ d}; echo .) 2>/dev/null || echo failed in 35 + (echo -n '36 '; ./pfs "${v=a\ b}" x "${v=c\ d}"; echo .) 2>/dev/null || echo failed in 36 + (echo -n '37 '; ./pfs ${v-a\ b} x ${v-c\ d}; echo .) 2>/dev/null || echo failed in 37 + (echo 38 ${IFS+x'a'y} / "${IFS+x'a'y}" .) 2>/dev/null || echo failed in 38 + foo="x'a'y"; (echo 39 ${foo%*'a'*} / "${foo%*'a'*}" .) 2>/dev/null || echo failed in 39 + foo="a b c"; (echo -n '40 '; ./pfs "${foo#a}"; echo .) 2>/dev/null || echo failed in 40 + (foo() { return 100; }; foo; echo 41 ${#+${#:+${#?}}\ \}\}\}}) 2>/dev/null || echo failed in 41 +expected-stdout: + 1 }z + 2 ''z} + 3 foo 'bar baz + 4 foo b c baz + 5 foo b c baz + 6 }z + 7 }z + 8 ""z} + 9 "}"z + 10 foo bar} baz + 11 ''z} + 12 }z + 13 }z + 14 }z + 15 <}> . + 16 hi there + 17 hi there + 18 hi there + 19 hi there + 20 hi there + 21 hi there + 22 hi there + 23 hi there + 24 'value' + 25 'value' + 26 $key + 27 'value' + 28 'x ~ x''x}"x}" # + 29 <{}b> <}> . + 30 . + 32 . + 33 . + 34 . + 35 . + 36 . + 37 . + 38 xay / x'a'y . + 39 x' / x' . + 40 < b c> . + 41 3 }}} +--- +name: expand-unglob-dblq +description: + Check that regular "${foo+bar}" constructs are parsed correctly +stdin: + u=x + tl_norm() { + v=$2 + test x"$v" = x"-" && unset v + (echo "$1 plus norm foo ${v+'bar'} baz") + (echo "$1 dash norm foo ${v-'bar'} baz") + (echo "$1 eqal norm foo ${v='bar'} baz") + (echo "$1 qstn norm foo ${v?'bar'} baz") 2>/dev/null || \ + echo "$1 qstn norm -> error" + (echo "$1 PLUS norm foo ${v:+'bar'} baz") + (echo "$1 DASH norm foo ${v:-'bar'} baz") + (echo "$1 EQAL norm foo ${v:='bar'} baz") + (echo "$1 QSTN norm foo ${v:?'bar'} baz") 2>/dev/null || \ + echo "$1 QSTN norm -> error" + } + tl_paren() { + v=$2 + test x"$v" = x"-" && unset v + (echo "$1 plus parn foo ${v+(bar)} baz") + (echo "$1 dash parn foo ${v-(bar)} baz") + (echo "$1 eqal parn foo ${v=(bar)} baz") + (echo "$1 qstn parn foo ${v?(bar)} baz") 2>/dev/null || \ + echo "$1 qstn parn -> error" + (echo "$1 PLUS parn foo ${v:+(bar)} baz") + (echo "$1 DASH parn foo ${v:-(bar)} baz") + (echo "$1 EQAL parn foo ${v:=(bar)} baz") + (echo "$1 QSTN parn foo ${v:?(bar)} baz") 2>/dev/null || \ + echo "$1 QSTN parn -> error" + } + tl_brace() { + v=$2 + test x"$v" = x"-" && unset v + (echo "$1 plus brac foo ${v+a$u{{{\}b} c ${v+d{}} baz") + (echo "$1 dash brac foo ${v-a$u{{{\}b} c ${v-d{}} baz") + (echo "$1 eqal brac foo ${v=a$u{{{\}b} c ${v=d{}} baz") + (echo "$1 qstn brac foo ${v?a$u{{{\}b} c ${v?d{}} baz") 2>/dev/null || \ + echo "$1 qstn brac -> error" + (echo "$1 PLUS brac foo ${v:+a$u{{{\}b} c ${v:+d{}} baz") + (echo "$1 DASH brac foo ${v:-a$u{{{\}b} c ${v:-d{}} baz") + (echo "$1 EQAL brac foo ${v:=a$u{{{\}b} c ${v:=d{}} baz") + (echo "$1 QSTN brac foo ${v:?a$u{{{\}b} c ${v:?d{}} baz") 2>/dev/null || \ + echo "$1 QSTN brac -> error" + } + : '}}}' '}}}' '}}}' '}}}' '}}}' '}}}' '}}}' '}}}' + tl_norm 1 - + tl_norm 2 '' + tl_norm 3 x + tl_paren 4 - + tl_paren 5 '' + tl_paren 6 x + tl_brace 7 - + tl_brace 8 '' + tl_brace 9 x +expected-stdout: + 1 plus norm foo baz + 1 dash norm foo 'bar' baz + 1 eqal norm foo 'bar' baz + 1 qstn norm -> error + 1 PLUS norm foo baz + 1 DASH norm foo 'bar' baz + 1 EQAL norm foo 'bar' baz + 1 QSTN norm -> error + 2 plus norm foo 'bar' baz + 2 dash norm foo baz + 2 eqal norm foo baz + 2 qstn norm foo baz + 2 PLUS norm foo baz + 2 DASH norm foo 'bar' baz + 2 EQAL norm foo 'bar' baz + 2 QSTN norm -> error + 3 plus norm foo 'bar' baz + 3 dash norm foo x baz + 3 eqal norm foo x baz + 3 qstn norm foo x baz + 3 PLUS norm foo 'bar' baz + 3 DASH norm foo x baz + 3 EQAL norm foo x baz + 3 QSTN norm foo x baz + 4 plus parn foo baz + 4 dash parn foo (bar) baz + 4 eqal parn foo (bar) baz + 4 qstn parn -> error + 4 PLUS parn foo baz + 4 DASH parn foo (bar) baz + 4 EQAL parn foo (bar) baz + 4 QSTN parn -> error + 5 plus parn foo (bar) baz + 5 dash parn foo baz + 5 eqal parn foo baz + 5 qstn parn foo baz + 5 PLUS parn foo baz + 5 DASH parn foo (bar) baz + 5 EQAL parn foo (bar) baz + 5 QSTN parn -> error + 6 plus parn foo (bar) baz + 6 dash parn foo x baz + 6 eqal parn foo x baz + 6 qstn parn foo x baz + 6 PLUS parn foo (bar) baz + 6 DASH parn foo x baz + 6 EQAL parn foo x baz + 6 QSTN parn foo x baz + 7 plus brac foo c } baz + 7 dash brac foo ax{{{}b c d{} baz + 7 eqal brac foo ax{{{}b c ax{{{}b} baz + 7 qstn brac -> error + 7 PLUS brac foo c } baz + 7 DASH brac foo ax{{{}b c d{} baz + 7 EQAL brac foo ax{{{}b c ax{{{}b} baz + 7 QSTN brac -> error + 8 plus brac foo ax{{{}b c d{} baz + 8 dash brac foo c } baz + 8 eqal brac foo c } baz + 8 qstn brac foo c } baz + 8 PLUS brac foo c } baz + 8 DASH brac foo ax{{{}b c d{} baz + 8 EQAL brac foo ax{{{}b c ax{{{}b} baz + 8 QSTN brac -> error + 9 plus brac foo ax{{{}b c d{} baz + 9 dash brac foo x c x} baz + 9 eqal brac foo x c x} baz + 9 qstn brac foo x c x} baz + 9 PLUS brac foo ax{{{}b c d{} baz + 9 DASH brac foo x c x} baz + 9 EQAL brac foo x c x} baz + 9 QSTN brac foo x c x} baz +--- +name: expand-unglob-unq +description: + Check that regular ${foo+bar} constructs are parsed correctly +stdin: + u=x + tl_norm() { + v=$2 + test x"$v" = x"-" && unset v + (echo $1 plus norm foo ${v+'bar'} baz) + (echo $1 dash norm foo ${v-'bar'} baz) + (echo $1 eqal norm foo ${v='bar'} baz) + (echo $1 qstn norm foo ${v?'bar'} baz) 2>/dev/null || \ + echo "$1 qstn norm -> error" + (echo $1 PLUS norm foo ${v:+'bar'} baz) + (echo $1 DASH norm foo ${v:-'bar'} baz) + (echo $1 EQAL norm foo ${v:='bar'} baz) + (echo $1 QSTN norm foo ${v:?'bar'} baz) 2>/dev/null || \ + echo "$1 QSTN norm -> error" + } + tl_paren() { + v=$2 + test x"$v" = x"-" && unset v + (echo $1 plus parn foo ${v+\(bar')'} baz) + (echo $1 dash parn foo ${v-\(bar')'} baz) + (echo $1 eqal parn foo ${v=\(bar')'} baz) + (echo $1 qstn parn foo ${v?\(bar')'} baz) 2>/dev/null || \ + echo "$1 qstn parn -> error" + (echo $1 PLUS parn foo ${v:+\(bar')'} baz) + (echo $1 DASH parn foo ${v:-\(bar')'} baz) + (echo $1 EQAL parn foo ${v:=\(bar')'} baz) + (echo $1 QSTN parn foo ${v:?\(bar')'} baz) 2>/dev/null || \ + echo "$1 QSTN parn -> error" + } + tl_brace() { + v=$2 + test x"$v" = x"-" && unset v + (echo $1 plus brac foo ${v+a$u{{{\}b} c ${v+d{}} baz) + (echo $1 dash brac foo ${v-a$u{{{\}b} c ${v-d{}} baz) + (echo $1 eqal brac foo ${v=a$u{{{\}b} c ${v=d{}} baz) + (echo $1 qstn brac foo ${v?a$u{{{\}b} c ${v?d{}} baz) 2>/dev/null || \ + echo "$1 qstn brac -> error" + (echo $1 PLUS brac foo ${v:+a$u{{{\}b} c ${v:+d{}} baz) + (echo $1 DASH brac foo ${v:-a$u{{{\}b} c ${v:-d{}} baz) + (echo $1 EQAL brac foo ${v:=a$u{{{\}b} c ${v:=d{}} baz) + (echo $1 QSTN brac foo ${v:?a$u{{{\}b} c ${v:?d{}} baz) 2>/dev/null || \ + echo "$1 QSTN brac -> error" + } + : '}}}' '}}}' '}}}' '}}}' '}}}' '}}}' '}}}' '}}}' + tl_norm 1 - + tl_norm 2 '' + tl_norm 3 x + tl_paren 4 - + tl_paren 5 '' + tl_paren 6 x + tl_brace 7 - + tl_brace 8 '' + tl_brace 9 x +expected-stdout: + 1 plus norm foo baz + 1 dash norm foo bar baz + 1 eqal norm foo bar baz + 1 qstn norm -> error + 1 PLUS norm foo baz + 1 DASH norm foo bar baz + 1 EQAL norm foo bar baz + 1 QSTN norm -> error + 2 plus norm foo bar baz + 2 dash norm foo baz + 2 eqal norm foo baz + 2 qstn norm foo baz + 2 PLUS norm foo baz + 2 DASH norm foo bar baz + 2 EQAL norm foo bar baz + 2 QSTN norm -> error + 3 plus norm foo bar baz + 3 dash norm foo x baz + 3 eqal norm foo x baz + 3 qstn norm foo x baz + 3 PLUS norm foo bar baz + 3 DASH norm foo x baz + 3 EQAL norm foo x baz + 3 QSTN norm foo x baz + 4 plus parn foo baz + 4 dash parn foo (bar) baz + 4 eqal parn foo (bar) baz + 4 qstn parn -> error + 4 PLUS parn foo baz + 4 DASH parn foo (bar) baz + 4 EQAL parn foo (bar) baz + 4 QSTN parn -> error + 5 plus parn foo (bar) baz + 5 dash parn foo baz + 5 eqal parn foo baz + 5 qstn parn foo baz + 5 PLUS parn foo baz + 5 DASH parn foo (bar) baz + 5 EQAL parn foo (bar) baz + 5 QSTN parn -> error + 6 plus parn foo (bar) baz + 6 dash parn foo x baz + 6 eqal parn foo x baz + 6 qstn parn foo x baz + 6 PLUS parn foo (bar) baz + 6 DASH parn foo x baz + 6 EQAL parn foo x baz + 6 QSTN parn foo x baz + 7 plus brac foo c } baz + 7 dash brac foo ax{{{}b c d{} baz + 7 eqal brac foo ax{{{}b c ax{{{}b} baz + 7 qstn brac -> error + 7 PLUS brac foo c } baz + 7 DASH brac foo ax{{{}b c d{} baz + 7 EQAL brac foo ax{{{}b c ax{{{}b} baz + 7 QSTN brac -> error + 8 plus brac foo ax{{{}b c d{} baz + 8 dash brac foo c } baz + 8 eqal brac foo c } baz + 8 qstn brac foo c } baz + 8 PLUS brac foo c } baz + 8 DASH brac foo ax{{{}b c d{} baz + 8 EQAL brac foo ax{{{}b c ax{{{}b} baz + 8 QSTN brac -> error + 9 plus brac foo ax{{{}b c d{} baz + 9 dash brac foo x c x} baz + 9 eqal brac foo x c x} baz + 9 qstn brac foo x c x} baz + 9 PLUS brac foo ax{{{}b c d{} baz + 9 DASH brac foo x c x} baz + 9 EQAL brac foo x c x} baz + 9 QSTN brac foo x c x} baz +--- +name: expand-threecolons-dblq +description: + Check for a particular thing that used to segfault +stdin: + TEST=1234 + echo "${TEST:1:2:3}" + echo $? but still living +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: expand-threecolons-unq +description: + Check for a particular thing that used to not error out +stdin: + TEST=1234 + echo ${TEST:1:2:3} + echo $? but still living +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: expand-weird-1 +description: + Check corner cases of trim expansion vs. $# vs. ${#var} vs. ${var?} +stdin: + set 1 2 3 4 5 6 7 8 9 10 11 + echo ${#} # value of $# + echo ${##} # length of $# + echo ${##1} # $# trimmed 1 + set 1 2 3 4 5 6 7 8 9 10 11 12 + echo ${##1} + (exit 0) + echo $? = ${#?} . + (exit 111) + echo $? = ${#?} . +expected-stdout: + 11 + 2 + 1 + 2 + 0 = 1 . + 111 = 3 . +--- +name: expand-weird-2 +description: + Check more substitution and extension corner cases +stdin: + :& set -C; pid=$$; sub=$!; flg=$-; set -- i; exec 3>x.tmp + #echo "D: !=$! #=$# \$=$$ -=$- ?=$?" + echo >&3 3 = s^${!-word} , ${#-word} , p^${$-word} , f^${--word} , ${?-word} . + echo >&3 4 = ${!+word} , ${#+word} , ${$+word} , ${-+word} , ${?+word} . + echo >&3 5 = s^${!=word} , ${#=word} , p^${$=word} , f^${-=word} , ${?=word} . + echo >&3 6 = s^${!?word} , ${#?word} , p^${$?word} , f^${-?word} , ${??word} . + echo >&3 7 = sl^${#!} , ${##} , pl^${#$} , fl^${#-} , ${#?} . + echo >&3 8 = sw^${%!} , ${%#} , pw^${%$} , fw^${%-} , ${%?} . + echo >&3 9 = ${!!} , s^${!#} , ${!$} , s^${!-} , s^${!?} . + echo >&3 10 = s^${!#pattern} , ${##pattern} , p^${$#pattern} , f^${-#pattern} , ${?#pattern} . + echo >&3 11 = s^${!%pattern} , ${#%pattern} , p^${$%pattern} , f^${-%pattern} , ${?%pattern} . + echo >&3 12 = $# : ${##} , ${##1} . + set -- + echo >&3 14 = $# : ${##} , ${##1} . + set -- 1 2 3 4 5 + echo >&3 16 = $# : ${##} , ${##1} . + set -- 1 2 3 4 5 6 7 8 9 a b c d e + echo >&3 18 = $# : ${##} , ${##1} . + exec 3>&- + <${a#\~}> <${b:-~}> <${b:-\~}> <${c:=~}><$c> <${a/~}> <${a/x/~}> <${a/x/\~}>" +expected-stdout: + <~/x> <~> <\~> <~><~> <~/x> <~//etc> <~/~> +--- +name: expand-bang-1 +description: + Check corner case of ${!?} with ! being var vs. op +stdin: + echo ${!?} +expected-exit: 1 +expected-stderr-pattern: /not set/ +--- +name: expand-bang-2 +description: + Check corner case of ${!var} vs. ${var op} with var=! +stdin: + echo 1 $! . + echo 2 ${!#} . + echo 3 ${!#[0-9]} . + echo 4 ${!-foo} . + # get an at least three-digit bg pid + while :; do + :& + x=$! + if [[ $x != +([0-9]) ]]; then + echo >&2 "cannot test, pid '$x' not numeric" + echo >&2 report this with as many details as possible + exit 1 + fi + [[ $x = [0-9][0-9][0-9]* ]] && break + done + y=${x#?} + t=$!; [[ $t = $x ]]; echo 5 $? . + t=${!#}; [[ $t = $x ]]; echo 6 $? . + t=${!#[0-9]}; [[ $t = $y ]]; echo 7 $? . + t=${!-foo}; [[ $t = $x ]]; echo 8 $? . + t=${!?bar}; [[ $t = $x ]]; echo 9 $? . +expected-stdout: + 1 . + 2 . + 3 . + 4 foo . + 5 0 . + 6 0 . + 7 0 . + 8 0 . + 9 0 . +--- +name: expand-number-1 +description: + Check that positional arguments do not overflow +stdin: + echo "1 ${12345678901234567890} ." +expected-stdout: + 1 . +--- +name: expand-slashes-1 +description: + Check that side effects in substring replacement are handled correctly +stdin: + foo=n1n1n1n2n3 + i=2 + n=1 + echo 1 ${foo//n$((n++))/[$((++i))]} . + echo 2 $n , $i . +expected-stdout: + 1 [3][3][3]n2n3 . + 2 2 , 3 . +--- +name: expand-slashes-2 +description: + Check that side effects in substring replacement are handled correctly +stdin: + foo=n1n1n1n2n3 + i=2 + n=1 + echo 1 ${foo@/n$((n++))/[$((++i))]} . + echo 2 $n , $i . +expected-stdout: + 1 [3]n1n1[4][5] . + 2 5 , 5 . +--- +name: expand-slashes-3 +description: + Check that we can access the replaced string +stdin: + foo=n1n1n1n2n3 + echo 1 ${foo@/n[12]/[$KSH_MATCH]} . +expected-stdout: + 1 [n1][n1][n1][n2]n3 . +--- +name: eglob-bad-1 +description: + Check that globbing isn't done when glob has syntax error +category: !os:cygwin,!os:msys,!os:os2 +file-setup: file 644 "@(a[b|)c]foo" +stdin: + echo @(a[b|)c]* +expected-stdout: + @(a[b|)c]* +--- +name: eglob-bad-2 +description: + Check that globbing isn't done when glob has syntax error + (AT&T ksh fails this test) +file-setup: file 644 "abcx" +file-setup: file 644 "abcz" +file-setup: file 644 "bbc" +stdin: + echo [a*(]*)z +expected-stdout: + [a*(]*)z +--- +name: eglob-infinite-plus +description: + Check that shell doesn't go into infinite loop expanding +(...) + expressions. +file-setup: file 644 "abc" +time-limit: 3 +stdin: + echo +()c + echo +()x + echo +(*)c + echo +(*)x +expected-stdout: + +()c + +()x + abc + +(*)x +--- +name: eglob-subst-1 +description: + Check that eglobbing isn't done on substitution results +file-setup: file 644 "abc" +stdin: + x='@(*)' + echo $x +expected-stdout: + @(*) +--- +name: eglob-nomatch-1 +description: + Check that the pattern doesn't match +stdin: + echo 1: no-file+(a|b)stuff + echo 2: no-file+(a*(c)|b)stuff + echo 3: no-file+((((c)))|b)stuff +expected-stdout: + 1: no-file+(a|b)stuff + 2: no-file+(a*(c)|b)stuff + 3: no-file+((((c)))|b)stuff +--- +name: eglob-match-1 +description: + Check that the pattern matches correctly +file-setup: file 644 "abd" +file-setup: file 644 "acd" +file-setup: file 644 "abac" +stdin: + echo 1: a+(b|c)d + echo 2: a!(@(b|B))d + echo 3: *(a(b|c)) # (...|...) can be used within X(..) + echo 4: a[b*(foo|bar)]d # patterns not special inside [...] +expected-stdout: + 1: abd acd + 2: acd + 3: abac + 4: abd +--- +name: eglob-case-1 +description: + Simple negation tests +stdin: + case foo in !(foo|bar)) echo yes;; *) echo no;; esac + case bar in !(foo|bar)) echo yes;; *) echo no;; esac +expected-stdout: + no + no +--- +name: eglob-case-2 +description: + Simple kleene tests +stdin: + case foo in *(a|b[)) echo yes;; *) echo no;; esac + case foo in *(a|b[)|f*) echo yes;; *) echo no;; esac + case '*(a|b[)' in *(a|b[)) echo yes;; *) echo no;; esac + case 'aab[b[ab[a' in *(a|b[)) echo yes;; *) echo no;; esac +expected-stdout: + no + yes + no + yes +--- +name: eglob-trim-1 +description: + Eglobbing in trim expressions... + (AT&T ksh fails this - docs say # matches shortest string, ## matches + longest...) +stdin: + x=abcdef + echo 1: ${x#a|abc} + echo 2: ${x##a|abc} + echo 3: ${x%def|f} + echo 4: ${x%%f|def} +expected-stdout: + 1: bcdef + 2: def + 3: abcde + 4: abc +--- +name: eglob-trim-2 +description: + Check eglobbing works in trims... +stdin: + x=abcdef + echo 1: ${x#*(a|b)cd} + echo 2: "${x#*(a|b)cd}" + echo 3: ${x#"*(a|b)cd"} + echo 4: ${x#a(b|c)} +expected-stdout: + 1: ef + 2: ef + 3: abcdef + 4: cdef +--- +name: eglob-trim-3 +description: + Check eglobbing works in trims, for Korn Shell + Ensure eglobbing does not work for reduced-feature /bin/sh +stdin: + set +o sh + x=foobar + y=foobaz + z=fooba\? + echo "<${x%bar|baz},${y%bar|baz},${z%\?}>" + echo "<${x%ba(r|z)},${y%ba(r|z)}>" + set -o sh + echo "<${x%bar|baz},${y%bar|baz},${z%\?}>" + z='foo(bar' + echo "<${z%(*}>" +expected-stdout: + + + + +--- +name: eglob-substrpl-1 +description: + Check eglobbing works in substs... and they work at all +stdin: + [[ -n $BASH_VERSION ]] && shopt -s extglob + x=1222321_ab/cde_b/c_1221 + y=xyz + echo 1: ${x/2} . ${x/} + echo 2: ${x//2} + echo 3: ${x/+(2)} + echo 4: ${x//+(2)} + echo 5: ${x/2/4} + echo 6: ${x//2/4} + echo 7: ${x/+(2)/4} + echo 8: ${x//+(2)/4} + echo 9: ${x/b/c/e/f} + echo 10: ${x/b\/c/e/f} + echo 11: ${x/b\/c/e\/f} + echo 12: ${x/b\/c/e\\/f} + echo 13: ${x/b\\/c/e\\/f} + echo 14: ${x//b/c/e/f} + echo 15: ${x//b\/c/e/f} + echo 16: ${x//b\/c/e\/f} + echo 17: ${x//b\/c/e\\/f} + echo 18: ${x//b\\/c/e\\/f} + echo 19: ${x/b\/*\/c/x} + echo 20: ${x/\//.} + echo 21: ${x//\//.} + echo 22: ${x///.} + echo 23: ${x/#1/9} + echo 24: ${x//#1/9} + echo 25: ${x/%1/9} + echo 26: ${x//%1/9} + echo 27: ${x//\%1/9} + echo 28: ${x//\\%1/9} + echo 29: ${x//\a/9} + echo 30: ${x//\\a/9} + echo 31: ${x/2/$y} +expected-stdout: + 1: 122321_ab/cde_b/c_1221 . 1222321_ab/cde_b/c_1221 + 2: 131_ab/cde_b/c_11 + 3: 1321_ab/cde_b/c_1221 + 4: 131_ab/cde_b/c_11 + 5: 1422321_ab/cde_b/c_1221 + 6: 1444341_ab/cde_b/c_1441 + 7: 14321_ab/cde_b/c_1221 + 8: 14341_ab/cde_b/c_141 + 9: 1222321_ac/e/f/cde_b/c_1221 + 10: 1222321_ae/fde_b/c_1221 + 11: 1222321_ae/fde_b/c_1221 + 12: 1222321_ae\/fde_b/c_1221 + 13: 1222321_ab/cde_b/c_1221 + 14: 1222321_ac/e/f/cde_c/e/f/c_1221 + 15: 1222321_ae/fde_e/f_1221 + 16: 1222321_ae/fde_e/f_1221 + 17: 1222321_ae\/fde_e\/f_1221 + 18: 1222321_ab/cde_b/c_1221 + 19: 1222321_ax_1221 + 20: 1222321_ab.cde_b/c_1221 + 21: 1222321_ab.cde_b.c_1221 + 22: 1222321_ab/cde_b/c_1221 + 23: 9222321_ab/cde_b/c_1221 + 24: 1222321_ab/cde_b/c_1221 + 25: 1222321_ab/cde_b/c_1229 + 26: 1222321_ab/cde_b/c_1221 + 27: 1222321_ab/cde_b/c_1221 + 28: 1222321_ab/cde_b/c_1221 + 29: 1222321_9b/cde_b/c_1221 + 30: 1222321_ab/cde_b/c_1221 + 31: 1xyz22321_ab/cde_b/c_1221 +--- +name: eglob-substrpl-2 +description: + Check anchored substring replacement works, corner cases +stdin: + foo=123 + echo 1: ${foo/#/x} + echo 2: ${foo/%/x} + echo 3: ${foo/#/} + echo 4: ${foo/#} + echo 5: ${foo/%/} + echo 6: ${foo/%} +expected-stdout: + 1: x123 + 2: 123x + 3: 123 + 4: 123 + 5: 123 + 6: 123 +--- +name: eglob-substrpl-3a +description: + Check substring replacement works with variables and slashes, too +stdin: + HOME=/etc + pfx=/home/user + wd=/home/user/tmp + echo "${wd/#$pfx/~}" + echo "${wd/#\$pfx/~}" + echo "${wd/#"$pfx"/~}" + echo "${wd/#'$pfx'/~}" + echo "${wd/#"\$pfx"/~}" + echo "${wd/#'\$pfx'/~}" +expected-stdout: + /etc/tmp + /home/user/tmp + /etc/tmp + /home/user/tmp + /home/user/tmp + /home/user/tmp +--- +name: eglob-substrpl-3b +description: + More of this, bash fails it (bash4 passes) +stdin: + HOME=/etc + pfx=/home/user + wd=/home/user/tmp + echo "${wd/#$(echo /home/user)/~}" + echo "${wd/#"$(echo /home/user)"/~}" + echo "${wd/#'$(echo /home/user)'/~}" +expected-stdout: + /etc/tmp + /etc/tmp + /home/user/tmp +--- +name: eglob-substrpl-3c +description: + Even more weird cases +stdin: + HOME=/etc + pfx=/home/user + wd='$pfx/tmp' + echo 1: ${wd/#$pfx/~} + echo 2: ${wd/#\$pfx/~} + echo 3: ${wd/#"$pfx"/~} + echo 4: ${wd/#'$pfx'/~} + echo 5: ${wd/#"\$pfx"/~} + echo 6: ${wd/#'\$pfx'/~} + ts='a/ba/b$tp$tp_a/b$tp_*(a/b)_*($tp)' + tp=a/b + tr=c/d + [[ -n $BASH_VERSION ]] && shopt -s extglob + echo 7: ${ts/a\/b/$tr} + echo 8: ${ts/a\/b/\$tr} + echo 9: ${ts/$tp/$tr} + echo 10: ${ts/\$tp/$tr} + echo 11: ${ts/\\$tp/$tr} + echo 12: ${ts/$tp/c/d} + echo 13: ${ts/$tp/c\/d} + echo 14: ${ts/$tp/c\\/d} + echo 15: ${ts/+(a\/b)/$tr} + echo 16: ${ts/+(a\/b)/\$tr} + echo 17: ${ts/+($tp)/$tr} + echo 18: ${ts/+($tp)/c/d} + echo 19: ${ts/+($tp)/c\/d} + echo 20: ${ts//a\/b/$tr} + echo 21: ${ts//a\/b/\$tr} + echo 22: ${ts//$tp/$tr} + echo 23: ${ts//$tp/c/d} + echo 24: ${ts//$tp/c\/d} + echo 25: ${ts//+(a\/b)/$tr} + echo 26: ${ts//+(a\/b)/\$tr} + echo 27: ${ts//+($tp)/$tr} + echo 28: ${ts//+($tp)/c/d} + echo 29: ${ts//+($tp)/c\/d} + tp="+($tp)" + echo 30: ${ts/$tp/$tr} + echo 31: ${ts//$tp/$tr} +expected-stdout: + 1: $pfx/tmp + 2: /etc/tmp + 3: $pfx/tmp + 4: /etc/tmp + 5: /etc/tmp + 6: $pfx/tmp + 7: c/da/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 8: $tra/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 9: c/da/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 10: a/ba/bc/d$tp_a/b$tp_*(a/b)_*($tp) + 11: a/ba/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 12: c/da/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 13: c/da/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 14: c\/da/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 15: c/d$tp$tp_a/b$tp_*(a/b)_*($tp) + 16: $tr$tp$tp_a/b$tp_*(a/b)_*($tp) + 17: c/d$tp$tp_a/b$tp_*(a/b)_*($tp) + 18: c/d$tp$tp_a/b$tp_*(a/b)_*($tp) + 19: c/d$tp$tp_a/b$tp_*(a/b)_*($tp) + 20: c/dc/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 21: $tr$tr$tp$tp_$tr$tp_*($tr)_*($tp) + 22: c/dc/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 23: c/dc/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 24: c/dc/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 25: c/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 26: $tr$tp$tp_$tr$tp_*($tr)_*($tp) + 27: c/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 28: c/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 29: c/d$tp$tp_c/d$tp_*(c/d)_*($tp) + 30: a/ba/b$tp$tp_a/b$tp_*(a/b)_*($tp) + 31: a/ba/b$tp$tp_a/b$tp_*(a/b)_*($tp) +# This is what GNU bash does: +# 30: c/d$tp$tp_a/b$tp_*(a/b)_*($tp) +# 31: c/d$tp$tp_c/d$tp_*(c/d)_*($tp) +--- +name: eglob-utf8-1 +description: + UTF-8 mode differences for eglobbing +category: !shell:ebcdic-yes +stdin: + s=blöd + set +U + print 1: ${s%???} . + print 2: ${s/b???d/x} . + set -U + print 3: ${s%???} . + print 4: ${s/b??d/x} . + x=nö + print 5: ${x%?} ${x%%?} . + x=äh + print 6: ${x#?} ${x##?} . + x=�� + print 7: ${x%?} ${x%%?} . + x=mä� + print 8: ${x%?} ${x%%?} . + x=何 + print 9: ${x%?} ${x%%?} . +expected-stdout: + 1: bl . + 2: x . + 3: b . + 4: x . + 5: n n . + 6: h h . + 7: � � . + 8: mä mä . + 9: . +--- +name: glob-bad-1 +description: + Check that [ matches itself if it's not a valid bracket expr + but does not prevent globbing, while backslash-escaping does +file-setup: dir 755 "[x" +file-setup: file 644 "[x/foo" +stdin: + echo [* + echo *[x + echo [x/* + :>'ab[x' + :>'a[a-z][x' + echo a[a-z][* + echo a[a-z]* + echo a[a\-z]* +expected-stdout: + [x + [x + [x/foo + ab[x + ab[x + a[a-z]* +--- +name: glob-bad-2 +description: + Check that symbolic links aren't stat()'d +# breaks on Dell UNIX 4.0 R2.2 (SVR4) where unlink also fails +# breaks on FreeMiNT (cannot unlink dangling symlinks) +# breaks on MSYS, OS/2 (do not support symlinks) +category: !os:mint,!os:msys,!os:svr4.0,!nosymlink +file-setup: dir 755 "dir" +file-setup: symlink 644 "dir/abc" + non-existent-file +stdin: + echo d*/* + echo d*/abc +expected-stdout: + dir/abc + dir/abc +--- +name: glob-bad-3 +description: + Check that the slash is parsed before the glob +stdin: + mkdir a 'a[b' + (cd 'a[b'; echo ok >'c]d') + echo nok >abd + echo fail >a/d + cat a[b/c]d +expected-stdout: + ok +--- +name: glob-range-1 +description: + Test range matching +file-setup: file 644 ".bc" +file-setup: file 644 "abc" +file-setup: file 644 "bbc" +file-setup: file 644 "cbc" +file-setup: file 644 "-bc" +file-setup: file 644 "!bc" +file-setup: file 644 "^bc" +file-setup: file 644 "+bc" +file-setup: file 644 ",bc" +file-setup: file 644 "0bc" +file-setup: file 644 "1bc" +stdin: + echo [ab-]* + echo [-ab]* + echo [!-ab]* + echo [!ab]* + echo []ab]* + echo [^ab]* + echo [+--]* + echo [--1]* + +expected-stdout: + -bc abc bbc + -bc abc bbc + !bc +bc ,bc 0bc 1bc ^bc cbc + !bc +bc ,bc -bc 0bc 1bc ^bc cbc + abc bbc + ^bc abc bbc + +bc ,bc -bc + -bc 0bc 1bc +--- +name: glob-range-2 +description: + Test range matching + (AT&T ksh fails this; POSIX says invalid) +file-setup: file 644 "abc" +stdin: + echo [a--]* +expected-stdout: + [a--]* +--- +name: glob-range-3 +description: + Check that globbing matches the right things... +# breaks on Mac OSX (HFS+ non-standard Unicode canonical decomposition) +# breaks on Cygwin 1.7 (files are now UTF-16 or something) +# breaks on QNX 6.4.1 (says RT) +category: !os:cygwin,!os:darwin,!os:msys,!os:nto,!os:os2,!os:os390 +need-pass: no +file-setup: file 644 "a�c" +stdin: + echo a[�-�]* +expected-stdout: + a�c +--- +name: glob-range-4 +description: + Results unspecified according to POSIX +file-setup: file 644 ".bc" +stdin: + echo [a.]* +expected-stdout: + [a.]* +--- +name: glob-range-5 +description: + Results unspecified according to POSIX + (AT&T ksh treats this like [a-cc-e]*) +file-setup: file 644 "abc" +file-setup: file 644 "bbc" +file-setup: file 644 "cbc" +file-setup: file 644 "dbc" +file-setup: file 644 "ebc" +file-setup: file 644 "-bc" +file-setup: file 644 "@bc" +stdin: + echo [a-c-e]* + echo [a--@]* +expected-stdout: + -bc abc bbc cbc ebc + @bc +--- +name: glob-word-1 +description: + Check BSD word boundary matches +stdin: + t() { [[ $1 = *[[:\<:]]bar[[:\>:]]* ]]; echo =$?; } + t 'foo bar baz' + t 'foobar baz' + t 'foo barbaz' + t 'bar' + t '_bar' + t 'bar_' +expected-stdout: + =0 + =1 + =1 + =0 + =1 + =1 +--- +name: glob-trim-1 +description: + Check against a regression from fixing IFS-subst-2 +stdin: + x='#foo' + print -r "before='$x'" + x=${x%%#*} + print -r "after ='$x'" +expected-stdout: + before='#foo' + after ='' +--- +name: heredoc-1 +description: + Check ordering/content of redundent here documents. +stdin: + cat << EOF1 << EOF2 + hi + EOF1 + there + EOF2 +expected-stdout: + there +--- +name: heredoc-2 +description: + Check quoted here-doc is protected. +stdin: + a=foo + cat << 'EOF' + hi\ + there$a + stuff + EO\ + F + EOF +expected-stdout: + hi\ + there$a + stuff + EO\ + F +--- +name: heredoc-3 +description: + Check that newline isn't needed after heredoc-delimiter marker. +stdin: ! + cat << EOF + hi + there + EOF +expected-stdout: + hi + there +--- +name: heredoc-4a +description: + Check that an error occurs if the heredoc-delimiter is missing. +stdin: ! + cat << EOF + hi + there +expected-exit: e > 0 +expected-stderr-pattern: /.*/ +--- +name: heredoc-4an +description: + Check that an error occurs if the heredoc-delimiter is missing. +arguments: !-n! +stdin: ! + cat << EOF + hi + there +expected-exit: e > 0 +expected-stderr-pattern: /.*/ +--- +name: heredoc-4b +description: + Check that an error occurs if the heredoc is missing. +stdin: ! + cat << EOF +expected-exit: e > 0 +expected-stderr-pattern: /.*/ +--- +name: heredoc-4bn +description: + Check that an error occurs if the heredoc is missing. +arguments: !-n! +stdin: ! + cat << EOF +expected-exit: e > 0 +expected-stderr-pattern: /.*/ +--- +name: heredoc-5 +description: + Check that backslash quotes a $, ` and \ and kills a \newline +stdin: + a=BAD + b=ok + cat << EOF + h\${a}i + h\\${b}i + th\`echo not-run\`ere + th\\`echo is-run`ere + fol\\ks + more\\ + last \ + line + EOF +expected-stdout: + h${a}i + h\oki + th`echo not-run`ere + th\is-runere + fol\ks + more\ + last line +--- +name: heredoc-6 +description: + Check that \newline in initial here-delim word doesn't imply + a quoted here-doc. +stdin: + a=i + cat << EO\ + F + h$a + there + EOF +expected-stdout: + hi + there +--- +name: heredoc-7 +description: + Check that double quoted $ expressions in here delimiters are + not expanded and match the delimiter. + POSIX says only quote removal is applied to the delimiter. +stdin: + a=b + cat << "E$a" + hi + h$a + hb + E$a + echo done +expected-stdout: + hi + h$a + hb + done +--- +name: heredoc-8 +description: + Check that double quoted escaped $ expressions in here + delimiters are not expanded and match the delimiter. + POSIX says only quote removal is applied to the delimiter + (\ counts as a quote). +stdin: + a=b + cat << "E\$a" + hi + h$a + h\$a + hb + h\b + E$a + echo done +expected-stdout: + hi + h$a + h\$a + hb + h\b + done +--- +name: heredoc-9a +description: + Check that here strings work. +stdin: + bar="bar + baz" + tr abcdefghijklmnopqrstuvwxyz nopqrstuvwxyzabcdefghijklm <</dev/null) +expected-stdout: + baz +--- +name: heredoc-9f +description: + Check long here strings +stdin: + cat <<< "$( : )aa" +expected-stdout: + aa +--- +name: heredoc-10 +description: + Check direct here document assignment +category: !shell:ebcdic-yes +stdin: + x=u + va=<" + y=$(<<-EOF + hi! + + $foo) is not a problem + + + EOF) + echo "7<$y>" +expected-stdout: + 3 + 7 +--- +name: heredoc-subshell-1 +description: + Tests for here documents in subshells, taken from Austin ML +stdin: + (cat <&1 + echo hi + echo there + fc -e - +expected-stdout-pattern: + /X*hi\nX*there\nX*echo there\nthere\nX*/ +expected-stderr-pattern: + /^X*$/ +--- +name: history-e-minus-3 +description: + fc -e - fails when there is no history + (ksh93 has a bug that causes this to fail) + (ksh88 loops on this) +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + fc -e - + echo ok +expected-stdout: + ok +expected-stderr-pattern: + /^X*.*:.*history.*\nX*$/ +--- +name: history-e-minus-4 +description: + Check if "fc -e -" command output goes to stdout. +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc + fc -e - | (read x; echo "A $x") + echo ok +expected-stdout: + abc + A abc + ok +expected-stderr-pattern: + /^X*echo abc\nX*/ +--- +name: history-e-minus-5 +description: + fc is replaced in history by new command. +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + echo ghi jkl + : + fc -e - echo + fc -l 2 5 +expected-stdout: + abc def + ghi jkl + ghi jkl + 2 echo ghi jkl + 3 : + 4 echo ghi jkl + 5 fc -l 2 5 +expected-stderr-pattern: + /^X*echo ghi jkl\nX*$/ +--- +name: history-list-1 +description: + List lists correct range + (ksh88 fails 'cause it lists the fc command) +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + fc -l -- -2 +expected-stdout: + line 1 + line 2 + line 3 + 2 echo line 2 + 3 echo line 3 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-2 +description: + Lists oldest history if given pre-historic number + (ksh93 has a bug that causes this to fail) + (ksh88 fails 'cause it lists the fc command) +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + fc -l -- -40 +expected-stdout: + line 1 + line 2 + line 3 + 1 echo line 1 + 2 echo line 2 + 3 echo line 3 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-3 +description: + Can give number 'options' to fc +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + fc -l -3 -2 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + 2 echo line 2 + 3 echo line 3 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-4 +description: + -1 refers to previous command +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + fc -l -1 -1 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + 4 echo line 4 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-5 +description: + List command stays in history +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + fc -l -1 -1 + fc -l -2 -1 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + 4 echo line 4 + 4 echo line 4 + 5 fc -l -1 -1 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-6 +description: + HISTSIZE limits about of history kept. + (ksh88 fails 'cause it lists the fc command) +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file!HISTSIZE=3! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + echo line 5 + fc -l +expected-stdout: + line 1 + line 2 + line 3 + line 4 + line 5 + 4 echo line 4 + 5 echo line 5 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-7 +description: + fc allows too old/new errors in range specification +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file!HISTSIZE=3! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + echo line 5 + fc -l 1 30 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + line 5 + 4 echo line 4 + 5 echo line 5 + 6 fc -l 1 30 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-r-1 +description: + test -r flag in history +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + echo line 5 + fc -l -r 2 4 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + line 5 + 4 echo line 4 + 3 echo line 3 + 2 echo line 2 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-r-2 +description: + If first is newer than last, -r is implied. +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + echo line 5 + fc -l 4 2 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + line 5 + 4 echo line 4 + 3 echo line 3 + 2 echo line 2 +expected-stderr-pattern: + /^X*$/ +--- +name: history-list-r-3 +description: + If first is newer than last, -r is cancelled. +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 + echo line 3 + echo line 4 + echo line 5 + fc -l -r 4 2 +expected-stdout: + line 1 + line 2 + line 3 + line 4 + line 5 + 2 echo line 2 + 3 echo line 3 + 4 echo line 4 +expected-stderr-pattern: + /^X*$/ +--- +name: history-subst-1 +description: + Basic substitution +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + echo ghi jkl + fc -e - abc=AB 'echo a' +expected-stdout: + abc def + ghi jkl + AB def +expected-stderr-pattern: + /^X*echo AB def\nX*$/ +--- +name: history-subst-2 +description: + Does subst find previous command? +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + echo ghi jkl + fc -e - jkl=XYZQRT 'echo g' +expected-stdout: + abc def + ghi jkl + ghi XYZQRT +expected-stderr-pattern: + /^X*echo ghi XYZQRT\nX*$/ +--- +name: history-subst-3 +description: + Does subst find previous command when no arguments given +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + echo ghi jkl + fc -e - jkl=XYZQRT +expected-stdout: + abc def + ghi jkl + ghi XYZQRT +expected-stderr-pattern: + /^X*echo ghi XYZQRT\nX*$/ +--- +name: history-subst-4 +description: + Global substitutions work + (ksh88 and ksh93 do not have -g option) +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def asjj sadjhasdjh asdjhasd + fc -e - -g a=FooBAR +expected-stdout: + abc def asjj sadjhasdjh asdjhasd + FooBARbc def FooBARsjj sFooBARdjhFooBARsdjh FooBARsdjhFooBARsd +expected-stderr-pattern: + /^X*echo FooBARbc def FooBARsjj sFooBARdjhFooBARsdjh FooBARsdjhFooBARsd\nX*$/ +--- +name: history-subst-5 +description: + Make sure searches don't find current (fc) command + (ksh88/ksh93 don't have the ? prefix thing so they fail this test) +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + echo ghi jkl + fc -e - abc=AB \?abc +expected-stdout: + abc def + ghi jkl + AB def +expected-stderr-pattern: + /^X*echo AB def\nX*$/ +--- +name: history-ed-1-old +description: + Basic (ed) editing works (assumes you have generic ed editor + that prints no prompts). This is for oldish ed(1) which write + the character count to stdout. +category: stdout-ed +need-ctty: yes +need-pass: no +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + fc echo + s/abc/FOOBAR/ + w + q +expected-stdout: + abc def + 13 + 16 + FOOBAR def +expected-stderr-pattern: + /^X*echo FOOBAR def\nX*$/ +--- +name: history-ed-2-old +description: + Correct command is edited when number given +category: stdout-ed +need-ctty: yes +need-pass: no +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 is here + echo line 3 + echo line 4 + fc 2 + s/is here/is changed/ + w + q +expected-stdout: + line 1 + line 2 is here + line 3 + line 4 + 20 + 23 + line 2 is changed +expected-stderr-pattern: + /^X*echo line 2 is changed\nX*$/ +--- +name: history-ed-3-old +description: + Newly created multi line commands show up as single command + in history. + (ksh88 fails 'cause it lists the fc command) +category: stdout-ed +need-ctty: yes +need-pass: no +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + fc echo + s/abc/FOOBAR/ + $a + echo a new line + . + w + q + fc -l +expected-stdout: + abc def + 13 + 32 + FOOBAR def + a new line + 1 echo abc def + 2 echo FOOBAR def + echo a new line +expected-stderr-pattern: + /^X*echo FOOBAR def\necho a new line\nX*$/ +--- +name: history-ed-1 +description: + Basic (ed) editing works (assumes you have generic ed editor + that prints no prompts). This is for newish ed(1) and stderr. +category: !no-stderr-ed +need-ctty: yes +need-pass: no +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + fc echo + s/abc/FOOBAR/ + w + q +expected-stdout: + abc def + FOOBAR def +expected-stderr-pattern: + /^X*13\n16\necho FOOBAR def\nX*$/ +--- +name: history-ed-2 +description: + Correct command is edited when number given +category: !no-stderr-ed +need-ctty: yes +need-pass: no +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo line 1 + echo line 2 is here + echo line 3 + echo line 4 + fc 2 + s/is here/is changed/ + w + q +expected-stdout: + line 1 + line 2 is here + line 3 + line 4 + line 2 is changed +expected-stderr-pattern: + /^X*20\n23\necho line 2 is changed\nX*$/ +--- +name: history-ed-3 +description: + Newly created multi line commands show up as single command + in history. +category: !no-stderr-ed +need-ctty: yes +need-pass: no +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + echo abc def + fc echo + s/abc/FOOBAR/ + $a + echo a new line + . + w + q + fc -l +expected-stdout: + abc def + FOOBAR def + a new line + 1 echo abc def + 2 echo FOOBAR def + echo a new line +expected-stderr-pattern: + /^X*13\n32\necho FOOBAR def\necho a new line\nX*$/ +--- +name: IFS-space-1 +description: + Simple test, default IFS +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + set -- A B C + showargs 1 $* + showargs 2 "$*" + showargs 3 $@ + showargs 4 "$@" +expected-stdout: + <1> . + <2> . + <3> . + <4> . +--- +name: IFS-colon-1 +description: + Simple test, IFS=: +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS=: + set -- A B C + showargs 1 $* + showargs 2 "$*" + showargs 3 $@ + showargs 4 "$@" +expected-stdout: + <1> . + <2> . + <3> . + <4> . +--- +name: IFS-null-1 +description: + Simple test, IFS="" +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="" + set -- A B C + showargs 1 $* + showargs 2 "$*" + showargs 3 $@ + showargs 4 "$@" +expected-stdout: + <1> . + <2> . + <3> . + <4> . +--- +name: IFS-space-colon-1 +description: + Simple test, IFS=: +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="$IFS:" + set -- + showargs 1 $* + showargs 2 "$*" + showargs 3 $@ + showargs 4 "$@" + showargs 5 : "$@" +expected-stdout: + <1> . + <2> <> . + <3> . + <4> . + <5> <:> . +--- +name: IFS-space-colon-2 +description: + Simple test, IFS=: + AT&T ksh fails this, POSIX says the test is correct. +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="$IFS:" + set -- + showargs :"$@" +expected-stdout: + <:> . +--- +name: IFS-space-colon-4 +description: + Simple test, IFS=: +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="$IFS:" + set -- + showargs "$@$@" +expected-stdout: + . +--- +name: IFS-space-colon-5 +description: + Simple test, IFS=: + Don't know what POSIX thinks of this. AT&T ksh does not do this. +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="$IFS:" + set -- + showargs "${@:-}" +expected-stdout: + <> . +--- +name: IFS-subst-1 +description: + Simple test, IFS=: +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="$IFS:" + x=":b: :" + echo -n '1:'; for i in $x ; do echo -n " [$i]" ; done ; echo + echo -n '2:'; for i in :b:: ; do echo -n " [$i]" ; done ; echo + showargs 3 $x + showargs 4 :b:: + x="a:b:" + echo -n '5:'; for i in $x ; do echo -n " [$i]" ; done ; echo + showargs 6 $x + x="a::c" + echo -n '7:'; for i in $x ; do echo -n " [$i]" ; done ; echo + showargs 8 $x + echo -n '9:'; for i in ${FOO-`echo -n h:i`th:ere} ; do echo -n " [$i]" ; done ; echo + showargs 10 ${FOO-`echo -n h:i`th:ere} + showargs 11 "${FOO-`echo -n h:i`th:ere}" + x=" A : B::D" + echo -n '12:'; for i in $x ; do echo -n " [$i]" ; done ; echo + showargs 13 $x +expected-stdout: + 1: [] [b] [] + 2: [:b::] + <3> <> <> . + <4> <:b::> . + 5: [a] [b] + <6> . + 7: [a] [] [c] + <8> <> . + 9: [h] [ith] [ere] + <10> . + <11> . + 12: [A] [B] [] [D] + <13> <> . +--- +name: IFS-subst-2 +description: + Check leading whitespace after trim does not make a field +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + x="X 1 2" + showargs 1 shift ${x#X} +expected-stdout: + <1> <1> <2> . +--- +name: IFS-subst-3-arr +description: + Check leading IFS non-whitespace after trim does make a field + but leading IFS whitespace does not, nor empty replacements +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + showargs 0 ${-+} + IFS=: + showargs 1 ${-+:foo:bar} + IFS=' ' + showargs 2 ${-+ foo bar} +expected-stdout: + <0> . + <1> <> . + <2> . +--- +name: IFS-subst-3-ass +description: + Check non-field semantics +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + showargs 0 x=${-+} + IFS=: + showargs 1 x=${-+:foo:bar} + IFS=' ' + showargs 2 x=${-+ foo bar} +expected-stdout: + <0> . + <1> . + <2> . +--- +name: IFS-subst-3-lcl +description: + Check non-field semantics, smaller corner case (LP#1381965) +stdin: + set -x + local regex=${2:-} + exit 1 +expected-exit: e != 0 +expected-stderr-pattern: + /regex=/ +--- +name: IFS-subst-4-1 +description: + reported by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a + IFS= ; q="$*" ; nq=$* + pfn "$*" $* "$q" "$nq" + [ "$q" = "$nq" ] && echo =true || echo =false +expected-stdout: + + + + + + + =true +--- +name: IFS-subst-4-2 +description: + extended testsuite based on problem by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a + IFS= ; q="$@" ; nq=$@ + pfn "$*" $* "$q" "$nq" + [ "$q" = "$nq" ] && echo =true || echo =false +expected-stdout: + + + + + + + =true +--- +name: IFS-subst-4-3 +description: + extended testsuite based on problem by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a; IFS= + qs="$*" + nqs=$* + qk="$@" + nqk=$@ + print -nr -- '= qs '; pfn "$qs" + print -nr -- '=nqs '; pfn "$nqs" + print -nr -- '= qk '; pfn "$qk" + print -nr -- '=nqk '; pfn "$nqk" + print -nr -- '~ qs '; pfn "$*" + print -nr -- '~nqs '; pfn $* + print -nr -- '~ qk '; pfn "$@" + print -nr -- '~nqk '; pfn $@ +expected-stdout: + = qs + =nqs + = qk + =nqk + ~ qs + ~nqs + + + ~ qk + + + ~nqk + + +--- +name: IFS-subst-4-4 +description: + extended testsuite based on problem by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a; IFS= + qs="$*" + print -nr -- '= qs '; pfn "$qs" + print -nr -- '~ qs '; pfn "$*" + nqs=$* + print -nr -- '=nqs '; pfn "$nqs" + print -nr -- '~nqs '; pfn $* + qk="$@" + print -nr -- '= qk '; pfn "$qk" + print -nr -- '~ qk '; pfn "$@" + nqk=$@ + print -nr -- '=nqk '; pfn "$nqk" + print -nr -- '~nqk '; pfn $@ +expected-stdout: + = qs + ~ qs + =nqs + ~nqs + + + = qk + ~ qk + + + =nqk + ~nqk + + +--- +name: IFS-subst-4-4p +description: + extended testsuite based on problem by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a; IFS= + unset v + qs=${v:-"$*"} + print -nr -- '= qs '; pfn "$qs" + print -nr -- '~ qs '; pfn ${v:-"$*"} + nqs=${v:-$*} + print -nr -- '=nqs '; pfn "$nqs" + print -nr -- '~nqs '; pfn ${v:-$*} + qk=${v:-"$@"} + print -nr -- '= qk '; pfn "$qk" + print -nr -- '~ qk '; pfn ${v:-"$@"} + nqk=${v:-$@} + print -nr -- '=nqk '; pfn "$nqk" + print -nr -- '~nqk '; pfn ${v:-$@} +expected-stdout: + = qs + ~ qs + =nqs + ~nqs + + + = qk + ~ qk + + + =nqk + ~nqk + + +--- +name: IFS-subst-4-5 +description: + extended testsuite based on problem by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a; IFS=, + qs="$*" + print -nr -- '= qs '; pfn "$qs" + print -nr -- '~ qs '; pfn "$*" + nqs=$* + print -nr -- '=nqs '; pfn "$nqs" + print -nr -- '~nqs '; pfn $* + qk="$@" + print -nr -- '= qk '; pfn "$qk" + print -nr -- '~ qk '; pfn "$@" + nqk=$@ + print -nr -- '=nqk '; pfn "$nqk" + print -nr -- '~nqk '; pfn $@ +expected-stdout: + = qs + ~ qs + =nqs + ~nqs + + + = qk + ~ qk + + + =nqk + ~nqk + + +--- +name: IFS-subst-4-5p +description: + extended testsuite based on problem by mikeserv +stdin: + pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; } + a='space divded argument + here' + IFS=\ ; set -- $a; IFS=, + unset v + qs=${v:-"$*"} + print -nr -- '= qs '; pfn "$qs" + print -nr -- '~ qs '; pfn ${v:-"$*"} + nqs=${v:-$*} + print -nr -- '=nqs '; pfn "$nqs" + print -nr -- '~nqs '; pfn ${v:-$*} + qk=${v:-"$@"} + print -nr -- '= qk '; pfn "$qk" + print -nr -- '~ qk '; pfn ${v:-"$@"} + nqk=${v:-$@} + print -nr -- '=nqk '; pfn "$nqk" + print -nr -- '~nqk '; pfn ${v:-$@} +expected-stdout: + = qs + ~ qs + =nqs + ~nqs + + + = qk + ~ qk + + + =nqk + ~nqk + + +--- +name: IFS-subst-5 +description: + extended testsuite based on IFS-subst-3 + differs slightly from ksh93: + - omit trailing field in a3zna, a7ina (unquoted $@ expansion) + - has extra middle fields in b5ins, b7ina (IFS_NWS unquoted expansion) + differs slightly from bash: + - omit leading field in a5ins, a7ina (IFS_NWS unquoted expansion) + differs slightly from zsh: + - differs in assignment, not expansion; probably zsh bug + - has extra middle fields in b5ins, b7ina (IFS_NWS unquoted expansion) + 'emulate sh' zsh has extra fields in + - a5ins (IFS_NWS unquoted $*) + - b5ins, matching mksh’s + !!WARNING!! more to come: http://austingroupbugs.net/view.php?id=888 +stdin: + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- "" 2 ""; pfb $*; x=$*; pfn "$x"' + echo '=a1zns' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- "" 2 ""; pfb "$*"; x="$*"; pfn "$x"' + echo '=a2zqs' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- "" 2 ""; pfb $@; x=$@; pfn "$x"' + echo '=a3zna' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- "" 2 ""; pfb "$@"; x="$@"; pfn "$x"' + echo '=a4zqa' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- "" 2 ""; pfb $*; x=$*; pfn "$x"' + echo '=a5ins' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- "" 2 ""; pfb "$*"; x="$*"; pfn "$x"' + echo '=a6iqs' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- "" 2 ""; pfb $@; x=$@; pfn "$x"' + echo '=a7ina' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- "" 2 ""; pfb "$@"; x="$@"; pfn "$x"' + echo '=a8iqa' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- A B "" "" C; pfb $*; x=$*; pfn "$x"' + echo '=b1zns' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- A B "" "" C; pfb "$*"; x="$*"; pfn "$x"' + echo '=b2zqs' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- A B "" "" C; pfb $@; x=$@; pfn "$x"' + echo '=b3zna' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=; set -- A B "" "" C; pfb "$@"; x="$@"; pfn "$x"' + echo '=b4zqa' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- A B "" "" C; pfb $*; x=$*; pfn "$x"' + echo '=b5ins' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- A B "" "" C; pfb "$*"; x="$*"; pfn "$x"' + echo '=b6iqs' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- A B "" "" C; pfb $@; x=$@; pfn "$x"' + echo '=b7ina' + "$__progname" -c 'pfb() { for s_arg in "$@"; do print -r -- "[$s_arg]"; done; }; pfn() { for s_arg in "$@"; do print -r -- "<$s_arg>"; done; }; + IFS=,; set -- A B "" "" C; pfb "$@"; x="$@"; pfn "$x"' + echo '=b8iqa' +expected-stdout: + [2] + <2> + =a1zns + [2] + <2> + =a2zqs + [2] + < 2 > + =a3zna + [] + [2] + [] + < 2 > + =a4zqa + [2] + <,2,> + =a5ins + [,2,] + <,2,> + =a6iqs + [2] + < 2 > + =a7ina + [] + [2] + [] + < 2 > + =a8iqa + [A] + [B] + [C] + + =b1zns + [ABC] + + =b2zqs + [A] + [B] + [C] + + =b3zna + [A] + [B] + [] + [] + [C] + + =b4zqa + [A] + [B] + [] + [] + [C] + + =b5ins + [A,B,,,C] + + =b6iqs + [A] + [B] + [] + [] + [C] + + =b7ina + [A] + [B] + [] + [] + [C] + + =b8iqa +--- +name: IFS-subst-6 +description: + Regression wrt. vector expansion in trim +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS= + x=abc + set -- a b + showargs ${x#$*} +expected-stdout: + . +--- +name: IFS-subst-7 +description: + ksh93 bug wrt. vector expansion in trim +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS="*" + a=abcd + set -- '' c + showargs "$*" ${a##"$*"} +expected-stdout: + <*c> . +--- +name: IFS-subst-8 +description: + http://austingroupbugs.net/view.php?id=221 +stdin: + n() { echo "$#"; }; n "${foo-$@}" +expected-stdout: + 1 +--- +name: IFS-subst-9 +description: + Scalar context for $*/$@ in [[ and case +stdin: + "$__progname" -c 'IFS=; set a b; [[ $* = "$1$2" ]]; echo 1 $?' sh a b + "$__progname" -c 'IFS=; [[ $* = ab ]]; echo 2 "$?"' sh a b + "$__progname" -c 'IFS=; [[ "$*" = ab ]]; echo 3 "$?"' sh a b + "$__progname" -c 'IFS=; [[ $* = a ]]; echo 4 "$?"' sh a b + "$__progname" -c 'IFS=; [[ "$*" = a ]]; echo 5 "$?"' sh a b + "$__progname" -c 'IFS=; [[ "$@" = a ]]; echo 6 "$?"' sh a b + "$__progname" -c 'IFS=; case "$@" in a) echo 7 a;; ab) echo 7 b;; a\ b) echo 7 ok;; esac' sh a b + "$__progname" -c 'IFS=; case $* in a) echo 8 a;; ab) echo 8 ok;; esac' sh a b + "$__progname" -c 'pfsp() { for s_arg in "$@"; do print -nr -- "<$s_arg> "; done; print .; }; IFS=; star=$* at="$@"; pfsp 9 "$star" "$at"' sh a b +expected-stdout: + 1 0 + 2 0 + 3 0 + 4 1 + 5 1 + 6 1 + 7 ok + 8 ok + <9> . +--- +name: IFS-subst-10 +description: + Scalar context in ${var=$subst} +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + set -- one "two three" four + unset -v var + save_IFS=$IFS + IFS= + set -- ${var=$*} + IFS=$save_IFS + echo "var=$var" + showargs "$@" +expected-stdout: + var=onetwo threefour + . +--- +name: IFS-arith-1 +description: + http://austingroupbugs.net/view.php?id=832 +stdin: + ${ZSH_VERSION+false} || emulate sh + ${BASH_VERSION+set -o posix} + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + IFS=0 + showargs $((1230456)) +expected-stdout: + <123> <456> . +--- +name: integer-base-err-1 +description: + Can't have 0 base (causes shell to exit) +expected-exit: e != 0 +stdin: + typeset -i i + i=3 + i=0#4 + echo $i +expected-stderr-pattern: + /^.*:.*0#4.*\n$/ +--- +name: integer-base-err-2 +description: + Can't have multiple bases in a 'constant' (causes shell to exit) + (ksh88 fails this test) +expected-exit: e != 0 +stdin: + typeset -i i + i=3 + i=2#110#11 + echo $i +expected-stderr-pattern: + /^.*:.*2#110#11.*\n$/ +--- +name: integer-base-err-3 +description: + Syntax errors in expressions and effects on bases + (interactive so errors don't cause exits) + (ksh88 fails this test - shell exits, even with -i) +need-ctty: yes +arguments: !-i! +stdin: + PS1= # minimise prompt hassles + typeset -i4 a=10 + typeset -i a=2+ + echo $a + typeset -i4 a=10 + typeset -i2 a=2+ + echo $a +expected-stderr-pattern: + /^([#\$] )?.*:.*2+.*\n.*:.*2+.*\n$/ +expected-stdout: + 4#22 + 4#22 +--- +name: integer-base-err-4 +description: + Are invalid digits (according to base) errors? + (ksh93 fails this test) +expected-exit: e != 0 +stdin: + typeset -i i; + i=3#4 +expected-stderr-pattern: + /^([#\$] )?.*:.*3#4.*\n$/ +--- +name: integer-base-1 +description: + Missing number after base is treated as 0. +stdin: + typeset -i i + i=3 + i=2# + echo $i +expected-stdout: + 0 +--- +name: integer-base-2 +description: + Check 'stickyness' of base in various situations +stdin: + typeset -i i=8 + echo $i + echo ---------- A + typeset -i4 j=8 + echo $j + echo ---------- B + typeset -i k=8 + typeset -i4 k=8 + echo $k + echo ---------- C + typeset -i4 l + l=3#10 + echo $l + echo ---------- D + typeset -i m + m=3#10 + echo $m + echo ---------- E + n=2#11 + typeset -i n + echo $n + n=10 + echo $n + echo ---------- F + typeset -i8 o=12 + typeset -i4 o + echo $o + echo ---------- G + typeset -i p + let p=8#12 + echo $p +expected-stdout: + 8 + ---------- A + 4#20 + ---------- B + 4#20 + ---------- C + 4#3 + ---------- D + 3#10 + ---------- E + 2#11 + 2#1010 + ---------- F + 4#30 + ---------- G + 8#12 +--- +name: integer-base-3 +description: + More base parsing (hmm doesn't test much..) +stdin: + typeset -i aa + aa=1+12#10+2 + echo $aa + typeset -i bb + bb=1+$aa + echo $bb + typeset -i bb + bb=$aa + echo $bb + typeset -i cc + cc=$aa + echo $cc +expected-stdout: + 15 + 16 + 15 + 15 +--- +name: integer-base-4 +description: + Check that things not declared as integers are not made integers, + also, check if base is not reset by -i with no arguments. + (ksh93 fails - prints 10#20 - go figure) +stdin: + xx=20 + let xx=10 + typeset -i | grep '^xx=' + typeset -i4 a=10 + typeset -i a=20 + echo $a +expected-stdout: + 4#110 +--- +name: integer-base-5 +description: + More base stuff +stdin: + typeset -i4 a=3#10 + echo $a + echo -- + typeset -i j=3 + j='~3' + echo $j + echo -- + typeset -i k=1 + x[k=k+1]=3 + echo $k + echo -- + typeset -i l + for l in 1 2+3 4; do echo $l; done +expected-stdout: + 4#3 + -- + -4 + -- + 2 + -- + 1 + 5 + 4 +--- +name: integer-base-6 +description: + Even more base stuff + (ksh93 fails this test - prints 0) +stdin: + typeset -i7 i + i= + echo $i +expected-stdout: + 7#0 +--- +name: integer-base-7 +description: + Check that non-integer parameters don't get bases assigned +stdin: + echo $(( zz = 8#100 )) + echo $zz +expected-stdout: + 64 + 64 +--- +name: integer-base-8 +description: + Check that base-36 works (full span) +stdin: + echo 1:$((36#109AZ)). + typeset -i36 x=1691675 + echo 2:$x. + typeset -Uui36 x + echo 3:$x. +expected-stdout: + 1:1691675. + 2:36#109az. + 3:36#109AZ. +--- +name: integer-base-check-flat +description: + Check behaviour does not match POSuX (except if set -o posix), + because a not type-safe scripting language has *no* business + interpreting the string "010" as octal number eight (dangerous). +stdin: + echo 1 "$("$__progname" -c 'echo :$((10))/$((010)),$((0x10)):')" . + echo 2 "$("$__progname" -o posix -c 'echo :$((10))/$((010)),$((0x10)):')" . + echo 3 "$("$__progname" -o sh -c 'echo :$((10))/$((010)),$((0x10)):')" . +expected-stdout: + 1 :10/10,16: . + 2 :10/8,16: . + 3 :10/10,16: . +--- +name: integer-base-check-numeric-from-1 +description: + Check behaviour for base one +category: !shell:ebcdic-yes +stdin: + echo 1:$((1#1))0. +expected-stdout: + 1:490. +--- +name: integer-base-check-numeric-from-1-ebcdic +description: + Check behaviour for base one +category: !shell:ebcdic-no +stdin: + echo 1:$((1#1))0. +expected-stdout: + 1:2410. +--- +name: integer-base-check-numeric-from-2 +description: + Check behaviour for base two to 36, and that 37 degrades to 10 +stdin: + i=1 + while (( ++i <= 37 )); do + eval 'echo '$i':$(('$i'#10)).' + done + echo 37:$($__progname -c 'echo $((37#10))').$?: +expected-stdout: + 2:2. + 3:3. + 4:4. + 5:5. + 6:6. + 7:7. + 8:8. + 9:9. + 10:10. + 11:11. + 12:12. + 13:13. + 14:14. + 15:15. + 16:16. + 17:17. + 18:18. + 19:19. + 20:20. + 21:21. + 22:22. + 23:23. + 24:24. + 25:25. + 26:26. + 27:27. + 28:28. + 29:29. + 30:30. + 31:31. + 32:32. + 33:33. + 34:34. + 35:35. + 36:36. + 37:10. + 37:10.0: +--- +name: integer-base-check-numeric-to-1 +description: + Check behaviour for base one +category: !shell:ebcdic-yes +stdin: + i=1 + typeset -Uui$i x=0x40 + eval "typeset -i10 y=$x" + print $i:$x.$y. +expected-stdout: + 1:1#@.64. +--- +name: integer-base-check-numeric-to-1-ebcdic +description: + Check behaviour for base one +category: !shell:ebcdic-no +stdin: + i=1 + typeset -Uui$i x=0x7C + eval "typeset -i10 y=$x" + print $i:$x.$y. +expected-stdout: + 1:1#@.124. +--- +name: integer-base-check-numeric-to-2 +description: + Check behaviour for base two to 36, and that 37 degrades to 10 +stdin: + i=1 + while (( ++i <= 37 )); do + typeset -Uui$i x=0x40 + eval "typeset -i10 y=$x" + print $i:$x.$y. + done +expected-stdout: + 2:2#1000000.64. + 3:3#2101.64. + 4:4#1000.64. + 5:5#224.64. + 6:6#144.64. + 7:7#121.64. + 8:8#100.64. + 9:9#71.64. + 10:64.64. + 11:11#59.64. + 12:12#54.64. + 13:13#4C.64. + 14:14#48.64. + 15:15#44.64. + 16:16#40.64. + 17:17#3D.64. + 18:18#3A.64. + 19:19#37.64. + 20:20#34.64. + 21:21#31.64. + 22:22#2K.64. + 23:23#2I.64. + 24:24#2G.64. + 25:25#2E.64. + 26:26#2C.64. + 27:27#2A.64. + 28:28#28.64. + 29:29#26.64. + 30:30#24.64. + 31:31#22.64. + 32:32#20.64. + 33:33#1V.64. + 34:34#1U.64. + 35:35#1T.64. + 36:36#1S.64. + 37:64.64. +--- +name: integer-arithmetic-span +description: + Check wraparound and size that is defined in mksh +category: int:32 +stdin: + echo s:$((2147483647+1)).$(((2147483647*2)+1)).$(((2147483647*2)+2)). + echo u:$((#2147483647+1)).$((#(2147483647*2)+1)).$((#(2147483647*2)+2)). +expected-stdout: + s:-2147483648.-1.0. + u:2147483648.4294967295.0. +--- +name: integer-arithmetic-span-64 +description: + Check wraparound and size that is defined in mksh +category: int:64 +stdin: + echo s:$((9223372036854775807+1)).$(((9223372036854775807*2)+1)).$(((9223372036854775807*2)+2)). + echo u:$((#9223372036854775807+1)).$((#(9223372036854775807*2)+1)).$((#(9223372036854775807*2)+2)). +expected-stdout: + s:-9223372036854775808.-1.0. + u:9223372036854775808.18446744073709551615.0. +--- +name: integer-size-FAIL-to-detect +description: + Notify the user that their ints are not 32 or 64 bit +category: int:u +stdin: + : +--- +name: lineno-stdin +description: + See if $LINENO is updated and can be modified. +stdin: + echo A $LINENO + echo B $LINENO + LINENO=20 + echo C $LINENO +expected-stdout: + A 1 + B 2 + C 20 +--- +name: lineno-inc +description: + See if $LINENO is set for .'d files. +file-setup: file 644 "dotfile" + echo dot A $LINENO + echo dot B $LINENO + LINENO=20 + echo dot C $LINENO +stdin: + echo A $LINENO + echo B $LINENO + . ./dotfile +expected-stdout: + A 1 + B 2 + dot A 1 + dot B 2 + dot C 20 +--- +name: lineno-func +description: + See if $LINENO is set for commands in a function. +stdin: + echo A $LINENO + echo B $LINENO + bar() { + echo func A $LINENO + echo func B $LINENO + } + bar + echo C $LINENO +expected-stdout: + A 1 + B 2 + func A 4 + func B 5 + C 8 +--- +name: lineno-unset +description: + See if unsetting LINENO makes it non-magic. +file-setup: file 644 "dotfile" + echo dot A $LINENO + echo dot B $LINENO +stdin: + unset LINENO + echo A $LINENO + echo B $LINENO + bar() { + echo func A $LINENO + echo func B $LINENO + } + bar + . ./dotfile + echo C $LINENO +expected-stdout: + A + B + func A + func B + dot A + dot B + C +--- +name: lineno-unset-use +description: + See if unsetting LINENO makes it non-magic even + when it is re-used. +file-setup: file 644 "dotfile" + echo dot A $LINENO + echo dot B $LINENO +stdin: + unset LINENO + LINENO=3 + echo A $LINENO + echo B $LINENO + bar() { + echo func A $LINENO + echo func B $LINENO + } + bar + . ./dotfile + echo C $LINENO +expected-stdout: + A 3 + B 3 + func A 3 + func B 3 + dot A 3 + dot B 3 + C 3 +--- +name: lineno-trap +description: + Check if LINENO is tracked in traps +stdin: + fail() { + echo "line <$1>" + exit 1 + } + trap 'fail $LINENO' INT ERR + false +expected-stdout: + line <6> +expected-exit: 1 +--- +name: lineno-eval-alias +description: + Check if LINENO is trapped in eval and aliases +stdin: + ${ZSH_VERSION+false} || emulate sh; echo $LINENO + echo $LINENO + eval ' echo $LINENO + echo $LINENO + echo $LINENO' + echo $LINENO +expected-stdout: + 1 + 2 + 3 + 3 + 3 + 6 +--- +name: unknown-trap +description: + Ensure unknown traps are not a syntax error +stdin: + ( + trap "echo trap 1 executed" UNKNOWNSIGNAL || echo "foo" + echo =1 + trap "echo trap 2 executed" UNKNOWNSIGNAL EXIT 999999 FNORD + echo = $? + ) 2>&1 | sed "s^${__progname%.exe}\.*e*x*e*: \[[0-9]*]PROG" +expected-stdout: + PROG: trap: bad signal 'UNKNOWNSIGNAL' + foo + =1 + PROG: trap: bad signal 'UNKNOWNSIGNAL' + PROG: trap: bad signal '999999' + PROG: trap: bad signal 'FNORD' + = 1 + trap 2 executed +--- +name: read-IFS-1 +description: + Simple test, default IFS +stdin: + echo "A B " > IN + unset x y z + read x y z < IN + echo 1: "x[$x] y[$y] z[$z]" + echo 1a: ${z-z not set} + read x < IN + echo 2: "x[$x]" +expected-stdout: + 1: x[A] y[B] z[] + 1a: + 2: x[A B] +--- +name: read-IFS-2 +description: + Complex tests, IFS either colon (IFS-NWS) or backslash (tricky) +stdin: + n=0 + showargs() { print -nr "$1"; shift; for s_arg in "$@"; do print -nr -- " [$s_arg]"; done; print; } + (IFS=\\ a=\<\\\>; showargs 3 $a) + (IFS=: b=\<:\>; showargs 4 $b) + print -r '<\>' | (IFS=\\ read f g; showargs 5 "$f" "$g") + print -r '<\\>' | (IFS=\\ read f g; showargs 6 "$f" "$g") + print '<\\\n>' | (IFS=\\ read f g; showargs 7 "$f" "$g") + print -r '<\>' | (IFS=\\ read f; showargs 8 "$f") + print -r '<\\>' | (IFS=\\ read f; showargs 9 "$f") + print '<\\\n>' | (IFS=\\ read f; showargs 10 "$f") + print -r '<\>' | (IFS=\\ read -r f g; showargs 11 "$f" "$g") + print -r '<\\>' | (IFS=\\ read -r f g; showargs 12 "$f" "$g") + print '<\\\n>' | (IFS=\\ read -r f g; showargs 13 "$f" "$g") + print -r '<\>' | (IFS=\\ read -r f; showargs 14 "$f") + print -r '<\\>' | (IFS=\\ read -r f; showargs 15 "$f") + print '<\\\n>' | (IFS=\\ read -r f; showargs 16 "$f") + print -r '<:>' | (IFS=: read f g; showargs 17 "$f" "$g") + print -r '<::>' | (IFS=: read f g; showargs 18 "$f" "$g") + print '<:\n>' | (IFS=: read f g; showargs 19 "$f" "$g") + print -r '<:>' | (IFS=: read f; showargs 20 "$f") + print -r '<::>' | (IFS=: read f; showargs 21 "$f") + print '<:\n>' | (IFS=: read f; showargs 22 "$f") + print -r '<:>' | (IFS=: read -r f g; showargs 23 "$f" "$g") + print -r '<::>' | (IFS=: read -r f g; showargs 24 "$f" "$g") + print '<:\n>' | (IFS=: read -r f g; showargs 25 "$f" "$g") + print -r '<:>' | (IFS=: read -r f; showargs 26 "$f") + print -r '<::>' | (IFS=: read -r f; showargs 27 "$f") + print '<:\n>' | (IFS=: read -r f; showargs 28 "$f") +expected-stdout: + 3 [<] [>] + 4 [<] [>] + 5 [<] [>] + 6 [<] [>] + 7 [<>] [] + 8 [<>] + 9 [<\>] + 10 [<>] + 11 [<] [>] + 12 [<] [\>] + 13 [<] [] + 14 [<\>] + 15 [<\\>] + 16 [<] + 17 [<] [>] + 18 [<] [:>] + 19 [<] [] + 20 [<:>] + 21 [<::>] + 22 [<] + 23 [<] [>] + 24 [<] [:>] + 25 [<] [] + 26 [<:>] + 27 [<::>] + 28 [<] +--- +name: read-ksh-1 +description: + If no var specified, REPLY is used +stdin: + echo "abc" > IN + read < IN + echo "[$REPLY]"; +expected-stdout: + [abc] +--- +name: read-regress-1 +description: + Check a regression of read +file-setup: file 644 "foo" + foo bar + baz + blah +stdin: + while read a b c; do + read d + break + done <$d>" +expected-stdout: + +--- +name: read-delim-1 +description: + Check read with delimiters +stdin: + emit() { + print -n 'foo bar\tbaz\nblah \0blub\tblech\nmyok meck \0' + } + emit | while IFS= read -d "" foo; do print -r -- "<$foo>"; done + emit | while read -d "" foo; do print -r -- "<$foo>"; done + emit | while read -d "eh?" foo; do print -r -- "<$foo>"; done +expected-stdout: + + + + + + +--- +name: read-ext-1 +description: + Check read with number of bytes specified, and -A +stdin: + print 'foo\nbar' >x1 + print -n x >x2 + print 'foo\\ bar baz' >x3 + x1a=u; read x1a " + print -r "x1b=<$x1b>" + print -r "x2a=$r2a<$x2a>" + print -r "x2b=$r2b<$x2b>" + print -r "x2c=$r2c<$x2c>" + print -r "x3a=<${x3a[0]}|${x3a[1]}|${x3a[2]}>" +expected-stdout: + x1a= + x1b= + x2a=1 + x2b=1 + x2c=0 + x3a= +--- +name: regression-1 +description: + Lex array code had problems with this. +stdin: + echo foo[ + n=bar + echo "hi[ $n ]=1" +expected-stdout: + foo[ + hi[ bar ]=1 +--- +name: regression-2 +description: + When PATH is set before running a command, the new path is + not used in doing the path search + $ echo echo hi > /tmp/q ; chmod a+rx /tmp/q + $ PATH=/tmp q + q: not found + $ + in comexec() the two lines + while (*vp != NULL) + (void) typeset(*vp++, xxx, 0); + need to be moved out of the switch to before findcom() is + called - I don't know what this will break. +stdin: + : "${PWD:-`pwd 2> /dev/null`}" + : "${PWD:?"PWD not set - cannot do test"}" + mkdir Y + cat > Y/xxxscript << EOF + #!/bin/sh + # Need to restore path so echo can be found (some shells don't have + # it as a built-in) + PATH=\$OLDPATH + echo hi + exit 0 + EOF + chmod a+rx Y/xxxscript + export OLDPATH="$PATH" + PATH=$PWD/Y xxxscript + exit $? +expected-stdout: + hi +--- +name: regression-6 +description: + Parsing of $(..) expressions is non-optimal. It is + impossible to have any parentheses inside the expression. + I.e., + $ ksh -c 'echo $(echo \( )' + no closing quote + $ ksh -c 'echo $(echo "(" )' + no closing quote + $ + The solution is to hack the parsing clode in lex.c, the + question is how to hack it: should any parentheses be + escaped by a backslash, or should recursive parsing be done + (so quotes could also be used to hide hem). The former is + easier, the later better... +stdin: + echo $(echo \( ) + echo $(echo "(" ) +expected-stdout: + ( + ( +--- +name: regression-9 +description: + Continue in a for loop does not work right: + for i in a b c ; do + if [ $i = b ] ; then + continue + fi + echo $i + done + Prints a forever... +stdin: + first=yes + for i in a b c ; do + if [ $i = b ] ; then + if [ $first = no ] ; then + echo 'continue in for loop broken' + break # hope break isn't broken too :-) + fi + first=no + continue + fi + done + echo bye +expected-stdout: + bye +--- +name: regression-10 +description: + The following: + set -- `false` + echo $? + should print 0 according to POSIX (dash, bash, ksh93, posh) + but not 0 according to the getopt(1) manual page, ksh88, and + Bourne sh (such as /bin/sh on Solaris). + We honour POSIX except when -o sh is set. +category: shell:legacy-no +stdin: + showf() { + [[ -o posix ]]; FPOSIX=$((1-$?)) + [[ -o sh ]]; FSH=$((1-$?)) + echo -n "FPOSIX=$FPOSIX FSH=$FSH " + } + set +o posix +o sh + showf + set -- `false` + echo rv=$? + set -o sh + showf + set -- `false` + echo rv=$? + set -o posix + showf + set -- `false` + echo rv=$? + set -o posix -o sh + showf + set -- `false` + echo rv=$? +expected-stdout: + FPOSIX=0 FSH=0 rv=0 + FPOSIX=0 FSH=1 rv=1 + FPOSIX=1 FSH=0 rv=0 + FPOSIX=1 FSH=1 rv=0 +--- +name: regression-10-legacy +description: + The following: + set -- `false` + echo $? + should print 0 according to POSIX (dash, bash, ksh93, posh) + but not 0 according to the getopt(1) manual page, ksh88, and + Bourne sh (such as /bin/sh on Solaris). +category: shell:legacy-yes +stdin: + showf() { + [[ -o posix ]]; FPOSIX=$((1-$?)) + [[ -o sh ]]; FSH=$((1-$?)) + echo -n "FPOSIX=$FPOSIX FSH=$FSH " + } + set +o posix +o sh + showf + set -- `false` + echo rv=$? + set -o sh + showf + set -- `false` + echo rv=$? + set -o posix + showf + set -- `false` + echo rv=$? + set -o posix -o sh + showf + set -- `false` + echo rv=$? +expected-stdout: + FPOSIX=0 FSH=0 rv=1 + FPOSIX=0 FSH=1 rv=1 + FPOSIX=1 FSH=0 rv=0 + FPOSIX=1 FSH=1 rv=0 +--- +name: regression-11 +description: + The following: + x=/foo/bar/blah + echo ${x##*/} + should echo blah but on some machines echos /foo/bar/blah. +stdin: + x=/foo/bar/blah + echo ${x##*/} +expected-stdout: + blah +--- +name: regression-12 +description: + Both of the following echos produce the same output under sh/ksh.att: + #!/bin/sh + x="foo bar" + echo "`echo \"$x\"`" + echo "`echo "$x"`" + pdksh produces different output for the former (foo instead of foo\tbar) +stdin: + x="foo bar" + echo "`echo \"$x\"`" + echo "`echo "$x"`" +expected-stdout: + foo bar + foo bar +--- +name: regression-13 +description: + The following command hangs forever: + $ (: ; cat /etc/termcap) | sleep 2 + This is because the shell forks a shell to run the (..) command + and this shell has the pipe open. When the sleep dies, the cat + doesn't get a SIGPIPE 'cause a process (ie, the second shell) + still has the pipe open. + + NOTE: this test provokes a bizarre bug in ksh93 (shell starts reading + commands from /etc/termcap..) +time-limit: 10 +stdin: + echo A line of text that will be duplicated quite a number of times.> t1 + cat t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 t1 > t2 + cat t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 t2 > t1 + cat t1 t1 t1 t1 > t2 + (: ; cat t2 2>/dev/null) | sleep 1 +--- +name: regression-14 +description: + The command + $ (foobar) 2> /dev/null + generates no output under /bin/sh, but pdksh produces the error + foobar: not found + Also, the command + $ foobar 2> /dev/null + generates an error under /bin/sh and pdksh, but AT&T ksh88 produces + no error (redirected to /dev/null). +stdin: + (you/should/not/see/this/error/1) 2> /dev/null + you/should/not/see/this/error/2 2> /dev/null + true +--- +name: regression-15 +description: + The command + $ whence foobar + generates a blank line under pdksh and sets the exit status to 0. + AT&T ksh88 generates no output and sets the exit status to 1. Also, + the command + $ whence foobar cat + generates no output under AT&T ksh88 (pdksh generates a blank line + and /bin/cat). +stdin: + whence does/not/exist > /dev/null + echo 1: $? + echo 2: $(whence does/not/exist | wc -l) + echo 3: $(whence does/not/exist cat | wc -l) +expected-stdout: + 1: 1 + 2: 0 + 3: 0 +--- +name: regression-16 +description: + ${var%%expr} seems to be broken in many places. On the mips + the commands + $ read line < /etc/passwd + $ echo $line + root:0:1:... + $ echo ${line%%:*} + root + $ echo $line + root + $ + change the value of line. On sun4s & pas, the echo ${line%%:*} doesn't + work. Haven't checked elsewhere... +script: + read x + y=$x + echo ${x%%:*} + echo $x +stdin: + root:asdjhasdasjhs:0:1:Root:/:/bin/sh +expected-stdout: + root + root:asdjhasdasjhs:0:1:Root:/:/bin/sh +--- +name: regression-17 +description: + The command + . /foo/bar + should set the exit status to non-zero (sh and AT&T ksh88 do). + XXX doting a non existent file is a fatal error for a script +stdin: + . does/not/exist +expected-exit: e != 0 +expected-stderr-pattern: /.?/ +--- +name: regression-19 +description: + Both of the following echos should produce the same thing, but don't: + $ x=foo/bar + $ echo ${x%/*} + foo + $ echo "${x%/*}" + foo/bar +stdin: + x=foo/bar + echo "${x%/*}" +expected-stdout: + foo +--- +name: regression-21 +description: + backslash does not work as expected in case labels: + $ x='-x' + $ case $x in + -\?) echo hi + esac + hi + $ x='-?' + $ case $x in + -\\?) echo hi + esac + hi + $ +stdin: + case -x in + -\?) echo fail + esac +--- +name: regression-22 +description: + Quoting backquotes inside backquotes doesn't work: + $ echo `echo hi \`echo there\` folks` + asks for more info. sh and AT&T ksh88 both echo + hi there folks +stdin: + echo `echo hi \`echo there\` folks` +expected-stdout: + hi there folks +--- +name: regression-23 +description: + )) is not treated `correctly': + $ (echo hi ; (echo there ; echo folks)) + missing (( + $ + instead of (as sh and ksh.att) + $ (echo hi ; (echo there ; echo folks)) + hi + there + folks + $ +stdin: + ( : ; ( : ; echo hi)) +expected-stdout: + hi +--- +name: regression-25 +description: + Check reading stdin in a while loop. The read should only read + a single line, not a whole stdio buffer; the cat should get + the rest. +stdin: + (echo a; echo b) | while read x ; do + echo $x + cat > /dev/null + done +expected-stdout: + a +--- +name: regression-26 +description: + Check reading stdin in a while loop. The read should read both + lines, not just the first. +script: + a= + while [ "$a" != xxx ] ; do + last=$x + read x + cat /dev/null | sed 's/x/y/' + a=x$a + done + echo $last +stdin: + a + b +expected-stdout: + b +--- +name: regression-27 +description: + The command + . /does/not/exist + should cause a script to exit. +stdin: + . does/not/exist + echo hi +expected-exit: e != 0 +expected-stderr-pattern: /does\/not\/exist/ +--- +name: regression-28 +description: + variable assignements not detected well +stdin: + a.x=1 echo hi +expected-exit: e != 0 +expected-stderr-pattern: /a\.x=1/ +--- +name: regression-29 +description: + alias expansion different from AT&T ksh88 +stdin: + alias a='for ' b='i in' + a b hi ; do echo $i ; done +expected-stdout: + hi +--- +name: regression-30 +description: + strange characters allowed inside ${...} +stdin: + echo ${a{b}} +expected-exit: e != 0 +expected-stderr-pattern: /.?/ +--- +name: regression-31 +description: + Does read handle partial lines correctly +script: + a= ret= + while [ "$a" != xxx ] ; do + read x y z + ret=$? + a=x$a + done + echo "[$x]" + echo $ret +stdin: ! + a A aA + b B Bb + c +expected-stdout: + [c] + 1 +--- +name: regression-32 +description: + Does read set variables to null at eof? +script: + a= + while [ "$a" != xxx ] ; do + read x y z + a=x$a + done + echo 1: ${x-x not set} ${y-y not set} ${z-z not set} + echo 2: ${x:+x not null} ${y:+y not null} ${z:+z not null} +stdin: + a A Aa + b B Bb +expected-stdout: + 1: + 2: +--- +name: regression-33 +description: + Does umask print a leading 0 when umask is 3 digits? +stdin: + # on MiNT, the first umask call seems to fail + umask 022 + # now, the test proper + umask 222 + umask +expected-stdout: + 0222 +--- +name: regression-35 +description: + Tempory files used for here-docs in functions get trashed after + the function is parsed (before it is executed) +stdin: + f1() { + cat <<- EOF + F1 + EOF + f2() { + cat <<- EOF + F2 + EOF + } + } + f1 + f2 + unset -f f1 + f2 +expected-stdout: + F1 + F2 + F2 +--- +name: regression-36 +description: + Command substitution breaks reading in while loop + (test from ) +stdin: + (echo abcdef; echo; echo 123) | + while read line + do + # the following line breaks it + c=`echo $line | wc -c` + echo $c + done +expected-stdout: + 7 + 1 + 4 +--- +name: regression-37 +description: + Machines with broken times() (reported by ) + time does not report correct real time +stdin: + time sleep 1 +expected-stderr-pattern: !/^\s*0\.0[\s\d]+real|^\s*real[\s]+0+\.0/ +--- +name: regression-38 +description: + set -e doesn't ignore exit codes for if/while/until/&&/||/!. +arguments: !-e! +stdin: + if false; then echo hi ; fi + false || true + false && true + while false; do echo hi; done + echo ok +expected-stdout: + ok +--- +name: regression-39 +description: + Only posh and oksh(2013-07) say “hi” below; FreeBSD sh, + GNU bash in POSIX mode, dash, ksh93, mksh don’t. All of + them exit 0. The POSIX behaviour is needed by BSD make. +stdin: + set -e + echo `false; echo hi` $(env; chmod +x env; PATH=.$PATHSEP$PATH + foo=bar + readonly foo + foo=stuff env | grep '^foo' +expected-exit: e != 0 +expected-stderr-pattern: + /read-only/ +--- +name: regression-43 +description: + Can subshells be prefixed by redirections (historical shells allow + this) +stdin: + < /dev/null (sed 's/^/X/') +--- +name: regression-45 +description: + Parameter assignments with [] recognised correctly +stdin: + FOO=*[12] + BAR=abc[ + MORE=[abc] + JUNK=a[bc + echo "<$FOO>" + echo "<$BAR>" + echo "<$MORE>" + echo "<$JUNK>" +expected-stdout: + <*[12]> + + <[abc]> + +--- +name: regression-46 +description: + Check that alias expansion works in command substitutions and + at the end of file. +stdin: + alias x='echo hi' + FOO="`x` " + echo "[$FOO]" + x +expected-stdout: + [hi ] + hi +--- +name: regression-47 +description: + Check that aliases are fully read. +stdin: + alias x='echo hi; + echo there' + x + echo done +expected-stdout: + hi + there + done +--- +name: regression-48 +description: + Check that (here doc) temp files are not left behind after an exec. +stdin: + mkdir foo || exit 1 + TMPDIR=$PWD/foo "$__progname" <<- 'EOF' + x() { + sed 's/^/X /' << E_O_F + hi + there + folks + E_O_F + echo "done ($?)" + } + echo=echo; [ -x /bin/echo ] && echo=/bin/echo + exec $echo subtest-1 hi + EOF + echo subtest-1 foo/* + TMPDIR=$PWD/foo "$__progname" <<- 'EOF' + echo=echo; [ -x /bin/echo ] && echo=/bin/echo + sed 's/^/X /' << E_O_F; exec $echo subtest-2 hi + a + few + lines + E_O_F + EOF + echo subtest-2 foo/* +expected-stdout: + subtest-1 hi + subtest-1 foo/* + X a + X few + X lines + subtest-2 hi + subtest-2 foo/* +--- +name: regression-49 +description: + Check that unset params with attributes are reported by set, those + sans attributes are not. +stdin: + unset FOO BAR + echo X$FOO + export BAR + typeset -i BLAH + set | grep FOO + set | grep BAR + set | grep BLAH +expected-stdout: + X + BAR + BLAH +--- +name: regression-50 +description: + Check that aliases do not use continuation prompt after trailing + semi-colon. +file-setup: file 644 "envf" + PS1=Y + PS2=X +env-setup: !ENV=./envf! +need-ctty: yes +arguments: !-i! +stdin: + alias foo='echo hi ; ' + foo + foo echo there +expected-stdout: + hi + hi + there +expected-stderr: ! + YYYY +--- +name: regression-51 +description: + Check that set allows both +o and -o options on same command line. +stdin: + set a b c + set -o noglob +o allexport + echo A: $*, * +expected-stdout: + A: a b c, * +--- +name: regression-52 +description: + Check that globbing works in pipelined commands +file-setup: file 644 "envf" + PS1=P +file-setup: file 644 "abc" + stuff +env-setup: !ENV=./envf! +need-ctty: yes +arguments: !-i! +stdin: + sed 's/^/X /' < ab* + echo mark 1 + sed 's/^/X /' < ab* | sed 's/^/Y /' + echo mark 2 +expected-stdout: + X stuff + mark 1 + Y X stuff + mark 2 +expected-stderr: ! + PPPPP +--- +name: regression-53 +description: + Check that getopts works in functions +stdin: + bfunc() { + echo bfunc: enter "(args: $*; OPTIND=$OPTIND)" + while getopts B oc; do + case $oc in + (B) + echo bfunc: B option + ;; + (*) + echo bfunc: odd option "($oc)" + ;; + esac + done + echo bfunc: leave + } + + function kfunc { + echo kfunc: enter "(args: $*; OPTIND=$OPTIND)" + while getopts K oc; do + case $oc in + (K) + echo kfunc: K option + ;; + (*) + echo bfunc: odd option "($oc)" + ;; + esac + done + echo kfunc: leave + } + + set -- -f -b -k -l + echo "line 1: OPTIND=$OPTIND" + getopts kbfl optc + echo "line 2: ret=$?, optc=$optc, OPTIND=$OPTIND" + bfunc -BBB blah + echo "line 3: OPTIND=$OPTIND" + getopts kbfl optc + echo "line 4: ret=$?, optc=$optc, OPTIND=$OPTIND" + kfunc -KKK blah + echo "line 5: OPTIND=$OPTIND" + getopts kbfl optc + echo "line 6: ret=$?, optc=$optc, OPTIND=$OPTIND" + echo + + OPTIND=1 + set -- -fbkl + echo "line 10: OPTIND=$OPTIND" + getopts kbfl optc + echo "line 20: ret=$?, optc=$optc, OPTIND=$OPTIND" + bfunc -BBB blah + echo "line 30: OPTIND=$OPTIND" + getopts kbfl optc + echo "line 40: ret=$?, optc=$optc, OPTIND=$OPTIND" + kfunc -KKK blah + echo "line 50: OPTIND=$OPTIND" + getopts kbfl optc + echo "line 60: ret=$?, optc=$optc, OPTIND=$OPTIND" +expected-stdout: + line 1: OPTIND=1 + line 2: ret=0, optc=f, OPTIND=2 + bfunc: enter (args: -BBB blah; OPTIND=2) + bfunc: B option + bfunc: B option + bfunc: leave + line 3: OPTIND=2 + line 4: ret=0, optc=b, OPTIND=3 + kfunc: enter (args: -KKK blah; OPTIND=1) + kfunc: K option + kfunc: K option + kfunc: K option + kfunc: leave + line 5: OPTIND=3 + line 6: ret=0, optc=k, OPTIND=4 + + line 10: OPTIND=1 + line 20: ret=0, optc=f, OPTIND=2 + bfunc: enter (args: -BBB blah; OPTIND=2) + bfunc: B option + bfunc: B option + bfunc: leave + line 30: OPTIND=2 + line 40: ret=1, optc=?, OPTIND=2 + kfunc: enter (args: -KKK blah; OPTIND=1) + kfunc: K option + kfunc: K option + kfunc: K option + kfunc: leave + line 50: OPTIND=2 + line 60: ret=1, optc=?, OPTIND=2 +--- +name: regression-54 +description: + Check that ; is not required before the then in if (( ... )) then ... +stdin: + if (( 1 )) then + echo ok dparen + fi + if [[ -n 1 ]] then + echo ok dbrackets + fi +expected-stdout: + ok dparen + ok dbrackets +--- +name: regression-55 +description: + Check ${foo:%bar} is allowed (ksh88 allows it...) +stdin: + x=fooXbarXblah + echo 1 ${x%X*} + echo 2 ${x:%X*} + echo 3 ${x%%X*} + echo 4 ${x:%%X*} + echo 5 ${x#*X} + echo 6 ${x:#*X} + echo 7 ${x##*X} + echo 8 ${x:##*X} +expected-stdout: + 1 fooXbar + 2 fooXbar + 3 foo + 4 foo + 5 barXblah + 6 barXblah + 7 blah + 8 blah +--- +name: regression-57 +description: + Check if typeset output is correct for + uninitialised array elements. +stdin: + typeset -i xxx[4] + echo A + typeset -i | grep xxx | sed 's/^/ /' + echo B + typeset | grep xxx | sed 's/^/ /' + + xxx[1]=2+5 + echo M + typeset -i | grep xxx | sed 's/^/ /' + echo N + typeset | grep xxx | sed 's/^/ /' +expected-stdout: + A + xxx + B + typeset -i xxx + M + xxx[1]=7 + N + set -A xxx + typeset -i xxx[1] +--- +name: regression-58 +description: + Check if trap exit is ok (exit not mistaken for signal name) +stdin: + trap 'echo hi' exit + trap exit 1 +expected-stdout: + hi +--- +name: regression-59 +description: + Check if ${#array[*]} is calculated correctly. +stdin: + a[12]=hi + a[8]=there + echo ${#a[*]} +expected-stdout: + 2 +--- +name: regression-60 +description: + Check if default exit status is previous command +stdin: + (true; exit) + echo A $? + (false; exit) + echo B $? + ( (exit 103) ; exit) + echo C $? +expected-stdout: + A 0 + B 1 + C 103 +--- +name: regression-61 +description: + Check if EXIT trap is executed for sub shells. +stdin: + trap 'echo parent exit' EXIT + echo start + (echo A; echo A last) + echo B + (echo C; trap 'echo sub exit' EXIT; echo C last) + echo parent last +expected-stdout: + start + A + A last + B + C + C last + sub exit + parent last + parent exit +--- +name: regression-62 +description: + Check if test -nt/-ot succeeds if second(first) file is missing. +stdin: + :>a + test a -nt b && echo nt OK || echo nt BAD + test b -ot a && echo ot OK || echo ot BAD +expected-stdout: + nt OK + ot OK +--- +name: regression-63 +description: + Check if typeset, export, and readonly work +stdin: + { + echo FNORD-0 + FNORD_A=1 + FNORD_B=2 + FNORD_C=3 + FNORD_D=4 + FNORD_E=5 + FNORD_F=6 + FNORD_G=7 + FNORD_H=8 + integer FNORD_E FNORD_F FNORD_G FNORD_H + export FNORD_C FNORD_D FNORD_G FNORD_H + readonly FNORD_B FNORD_D FNORD_F FNORD_H + echo FNORD-1 + export + echo FNORD-2 + export -p + echo FNORD-3 + readonly + echo FNORD-4 + readonly -p + echo FNORD-5 + typeset + echo FNORD-6 + typeset -p + echo FNORD-7 + typeset - + echo FNORD-8 + } | fgrep FNORD + fnord=(42 23) + typeset -p fnord + echo FNORD-9 +expected-stdout: + FNORD-0 + FNORD-1 + FNORD_C + FNORD_D + FNORD_G + FNORD_H + FNORD-2 + export FNORD_C=3 + export FNORD_D=4 + export FNORD_G=7 + export FNORD_H=8 + FNORD-3 + FNORD_B + FNORD_D + FNORD_F + FNORD_H + FNORD-4 + readonly FNORD_B=2 + readonly FNORD_D=4 + readonly FNORD_F=6 + readonly FNORD_H=8 + FNORD-5 + typeset FNORD_A + typeset -r FNORD_B + typeset -x FNORD_C + typeset -x -r FNORD_D + typeset -i FNORD_E + typeset -i -r FNORD_F + typeset -i -x FNORD_G + typeset -i -x -r FNORD_H + FNORD-6 + typeset FNORD_A=1 + typeset -r FNORD_B=2 + typeset -x FNORD_C=3 + typeset -x -r FNORD_D=4 + typeset -i FNORD_E=5 + typeset -i -r FNORD_F=6 + typeset -i -x FNORD_G=7 + typeset -i -x -r FNORD_H=8 + FNORD-7 + FNORD_A=1 + FNORD_B=2 + FNORD_C=3 + FNORD_D=4 + FNORD_E=5 + FNORD_F=6 + FNORD_G=7 + FNORD_H=8 + FNORD-8 + set -A fnord + typeset fnord[0]=42 + typeset fnord[1]=23 + FNORD-9 +--- +name: regression-64 +description: + Check that we can redefine functions calling time builtin +stdin: + t() { + time >/dev/null + } + t 2>/dev/null + t() { + time + } +--- +name: regression-65 +description: + check for a regression with sleep builtin and signal mask +category: !nojsig +time-limit: 5 +stdin: + sleep 1 + echo blub |& + while read -p line; do :; done + echo ok +expected-stdout: + ok +--- +name: regression-66 +description: + Check that quoting is sane +category: !nojsig +stdin: + ac_space=' ' + ac_newline=' + ' + set | grep ^ac_ |& + set -A lines + while IFS= read -pr line; do + if [[ $line = *space* ]]; then + lines[0]=$line + else + lines[1]=$line + fi + done + for line in "${lines[@]}"; do + print -r -- "$line" + done +expected-stdout: + ac_space=' ' + ac_newline=$'\n' +--- +name: regression-67 +description: + Check that we can both break and use source on the same line +stdin: + for s in s; do break; done; print -s s +--- +name: regression-68 +description: + Check that all common arithmetic operators work as expected +stdin: + echo 1 $(( a = 5 )) . + echo 2 $(( ++a )) , $(( a++ )) , $(( a )) . + echo 3 $(( --a )) , $(( a-- )) , $(( a )) . + echo 4 $(( a == 5 )) , $(( a == 6 )) . + echo 5 $(( a != 5 )) , $(( a != 6 )) . + echo 6 $(( a *= 3 )) . + echo 7 $(( a /= 5 )) . + echo 8 $(( a %= 2 )) . + echo 9 $(( a += 9 )) . + echo 10 $(( a -= 4 )) . + echo 11 $(( a <<= 1 )) . + echo 12 $(( a >>= 1 )) . + echo 13 $(( a &= 4 )) . + echo 14 $(( a ^= a )) . + echo 15 $(( a |= 5 )) . + echo 16 $(( 5 << 1 )) . + echo 17 $(( 5 >> 1 )) . + echo 18 $(( 5 <= 6 )) , $(( 5 <= 5 )) , $(( 5 <= 4 )) . + echo 19 $(( 5 >= 6 )) , $(( 5 >= 5 )) , $(( 5 >= 4 )) . + echo 20 $(( 5 < 6 )) , $(( 5 < 5 )) , $(( 5 < 4 )) . + echo 21 $(( 5 > 6 )) , $(( 5 > 5 )) , $(( 5 > 4 )) . + echo 22 $(( 0 && 0 )) , $(( 0 && 1 )) , $(( 1 && 0 )) , $(( 1 && 1 )) . + echo 23 $(( 0 || 0 )) , $(( 0 || 1 )) , $(( 1 || 0 )) , $(( 1 || 1 )) . + echo 24 $(( 5 * 3 )) . + echo 25 $(( 7 / 2 )) . + echo 26 $(( 5 % 5 )) , $(( 5 % 4 )) , $(( 5 % 1 )) , $(( 5 % -1 )) , $(( 5 % -2 )) . + echo 27 $(( 5 + 2 )) , $(( 5 + 0 )) , $(( 5 + -2 )) . + echo 28 $(( 5 - 2 )) , $(( 5 - 0 )) , $(( 5 - -2 )) . + echo 29 $(( 6 & 4 )) , $(( 6 & 8 )) . + echo 30 $(( 4 ^ 2 )) , $(( 4 ^ 4 )) . + echo 31 $(( 4 | 2 )) , $(( 4 | 4 )) , $(( 4 | 0 )) . + echo 32 $(( 0 ? 1 : 2 )) , $(( 3 ? 4 : 5 )) . + echo 33 $(( 5 , 2 , 3 )) . + echo 34 $(( ~0 )) , $(( ~1 )) , $(( ~~1 )) , $(( ~~2 )) . + echo 35 $(( !0 )) , $(( !1 )) , $(( !!1 )) , $(( !!2 )) . + echo 36 $(( (5) )) . +expected-stdout: + 1 5 . + 2 6 , 6 , 7 . + 3 6 , 6 , 5 . + 4 1 , 0 . + 5 0 , 1 . + 6 15 . + 7 3 . + 8 1 . + 9 10 . + 10 6 . + 11 12 . + 12 6 . + 13 4 . + 14 0 . + 15 5 . + 16 10 . + 17 2 . + 18 1 , 1 , 0 . + 19 0 , 1 , 1 . + 20 1 , 0 , 0 . + 21 0 , 0 , 1 . + 22 0 , 0 , 0 , 1 . + 23 0 , 1 , 1 , 1 . + 24 15 . + 25 3 . + 26 0 , 1 , 0 , 0 , 1 . + 27 7 , 5 , 3 . + 28 3 , 5 , 7 . + 29 4 , 0 . + 30 6 , 0 . + 31 6 , 4 , 4 . + 32 2 , 4 . + 33 3 . + 34 -1 , -2 , 1 , 2 . + 35 1 , 0 , 1 , 1 . + 36 5 . +--- +name: regression-69 +description: + Check that all non-lksh arithmetic operators work as expected +category: shell:legacy-no +stdin: + a=5 b=0x80000005 + echo 1 $(( a ^<= 1 )) , $(( b ^<= 1 )) . + echo 2 $(( a ^>= 2 )) , $(( b ^>= 2 )) . + echo 3 $(( 5 ^< 1 )) . + echo 4 $(( 5 ^> 1 )) . +expected-stdout: + 1 10 , 11 . + 2 -2147483646 , -1073741822 . + 3 10 . + 4 -2147483646 . +--- +name: readonly-0 +description: + Ensure readonly is honoured for assignments and unset +stdin: + "$__progname" -c 'u=x; echo $? $u .' || echo aborted, $? + echo = + "$__progname" -c 'readonly u; u=x; echo $? $u .' || echo aborted, $? + echo = + "$__progname" -c 'u=x; readonly u; unset u; echo $? $u .' || echo aborted, $? +expected-stdout: + 0 x . + = + aborted, 2 + = + 1 x . +expected-stderr-pattern: + /read-only/ +--- +name: readonly-1 +description: + http://austingroupbugs.net/view.php?id=367 for export +stdin: + "$__progname" -c 'readonly foo; export foo=a; echo $?' || echo aborted, $? +expected-stdout: + aborted, 2 +expected-stderr-pattern: + /read-only/ +--- +name: readonly-2a +description: + Check that getopts works as intended, for readonly-2b to be valid +stdin: + "$__progname" -c 'set -- -a b; getopts a c; echo $? $c .; getopts a c; echo $? $c .' || echo aborted, $? +expected-stdout: + 0 a . + 1 ? . +--- +name: readonly-2b +description: + http://austingroupbugs.net/view.php?id=367 for getopts +stdin: + "$__progname" -c 'readonly c; set -- -a b; getopts a c; echo $? $c .' || echo aborted, $? +expected-stdout: + 2 . +expected-stderr-pattern: + /read-only/ +--- +name: readonly-3 +description: + http://austingroupbugs.net/view.php?id=367 for read +stdin: + echo x | "$__progname" -c 'read s; echo $? $s .' || echo aborted, $? + echo y | "$__progname" -c 'readonly s; read s; echo $? $s .' || echo aborted, $? +expected-stdout: + 0 x . + 2 . +expected-stderr-pattern: + /read-only/ +--- +name: readonly-4 +description: + Do not permit bypassing readonly for first array item +stdin: + set -A arr -- foo bar + readonly arr + arr=baz + print -r -- "${arr[@]}" +expected-exit: e != 0 +expected-stderr-pattern: + /read[ -]?only/ +--- +name: readonly-5 +description: + Ensure readonly is idempotent +stdin: + readonly x=1 + readonly x +--- +name: syntax-1 +description: + Check that lone ampersand is a syntax error +stdin: + & +expected-exit: e != 0 +expected-stderr-pattern: + /syntax error/ +--- +name: xxx-quoted-newline-1 +description: + Check that \ works inside of ${} +stdin: + abc=2 + echo ${ab\ + c} +expected-stdout: + 2 +--- +name: xxx-quoted-newline-2 +description: + Check that \ works at the start of a here document +stdin: + cat << EO\ + F + hi + EOF +expected-stdout: + hi +--- +name: xxx-quoted-newline-3 +description: + Check that \ works at the end of a here document +stdin: + cat << EOF + hi + EO\ + F +expected-stdout: + hi +--- +name: xxx-multi-assignment-cmd +description: + Check that assignments in a command affect subsequent assignments + in the same command +stdin: + FOO=abc + FOO=123 BAR=$FOO + echo $BAR +expected-stdout: + 123 +--- +name: xxx-multi-assignment-posix-cmd +description: + Check that the behaviour for multiple assignments with a + command name matches POSIX. See: + http://thread.gmane.org/gmane.comp.standards.posix.austin.general/1925 +stdin: + X=a Y=b; X=$Y Y=$X "$__progname" -c 'echo 1 $X $Y .'; echo 2 $X $Y . + unset X Y Z + X=a Y=${X=b} Z=$X "$__progname" -c 'echo 3 $Z .' + unset X Y Z + X=a Y=${X=b} Z=$X; echo 4 $Z . +expected-stdout: + 1 b a . + 2 a b . + 3 b . + 4 a . +--- +name: xxx-multi-assignment-posix-nocmd +description: + Check that the behaviour for multiple assignments with no + command name matches POSIX (Debian #334182). See: + http://thread.gmane.org/gmane.comp.standards.posix.austin.general/1925 +stdin: + X=a Y=b; X=$Y Y=$X; echo 1 $X $Y . +expected-stdout: + 1 b b . +--- +name: xxx-multi-assignment-posix-subassign +description: + Check that the behaviour for multiple assignments matches POSIX: + - The assignment words shall be expanded in the current execution + environment. + - The assignments happen in the temporary execution environment. +stdin: + unset X Y Z + Z=a Y=${X:=b} sh -c 'echo +$X+ +$Y+ +$Z+' + echo /$X/ + # Now for the special case: + unset X Y Z + X= Y=${X:=b} sh -c 'echo +$X+ +$Y+' + echo /$X/ +expected-stdout: + ++ +b+ +a+ + /b/ + ++ +b+ + /b/ +--- +name: xxx-exec-environment-1 +description: + Check to see if exec sets it's environment correctly +stdin: + print '#!'"$__progname"'\nunset RANDOM\nexport | while IFS= read -r' \ + 'RANDOM; do eval '\''print -r -- "$RANDOM=$'\''"$RANDOM"'\'\"\'\; \ + done >env; chmod +x env; PATH=.$PATHSEP$PATH + FOO=bar exec env +expected-stdout-pattern: + /(^|.*\n)FOO=bar\n/ +--- +name: xxx-exec-environment-2 +description: + Check to make sure exec doesn't change environment if a program + isn't exec-ed +stdin: + print '#!'"$__progname"'\nunset RANDOM\nexport | while IFS= read -r' \ + 'RANDOM; do eval '\''print -r -- "$RANDOM=$'\''"$RANDOM"'\'\"\'\; \ + done >env; chmod +x env; PATH=.$PATHSEP$PATH + env >bar1 + FOO=bar exec; env >bar2 + cmp -s bar1 bar2 +--- +name: exec-function-environment-1 +description: + Check assignments in function calls and whether they affect + the current execution environment (ksh93, SUSv4) +stdin: + f() { a=2; }; g() { b=3; echo y$c-; }; a=1 f; b=2; c=1 g + echo x$a-$b- z$c- +expected-stdout: + y1- + x2-3- z1- +--- +name: exec-modern-korn-shell +description: + Check that exec can execute any command that makes it + through syntax and parser +stdin: + print '#!'"$__progname"'\necho tf' >lq + chmod +x lq + PATH=$PWD + exec 2>&1 + foo() { print two; } + print =1 + (exec print one) + print =2 + (exec foo) + print =3 + (exec ls) + print =4 + (exec lq) +expected-stdout-pattern: + /=1\none\n=2\ntwo\n=3\n.*: ls: not found\n=4\ntf\n/ +--- +name: exec-ksh88 +description: + Check that exec only executes after a PATH search +arguments: !-o!posix! +stdin: + print '#!'"$__progname"'\necho tf' >lq + chmod +x lq + PATH=$PWD + exec 2>&1 + foo() { print two; } + print =1 + (exec print one) + print =2 + (exec foo) + print =3 + (exec ls) + print =4 + (exec lq) +expected-stdout-pattern: + /=1\n.*: print: not found\n=2\n.*: foo: not found\n=3\n.*: ls: not found\n=4\ntf\n/ +--- +name: xxx-what-do-you-call-this-1 +stdin: + echo "${foo:-"a"}*" +expected-stdout: + a* +--- +name: xxx-prefix-strip-1 +stdin: + foo='a cdef' + echo ${foo#a c} +expected-stdout: + def +--- +name: xxx-prefix-strip-2 +stdin: + set a c + x='a cdef' + echo ${x#$*} +expected-stdout: + def +--- +name: xxx-variable-syntax-1 +stdin: + echo ${:} +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: xxx-variable-syntax-2 +stdin: + set 0 + echo ${*:0} +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: xxx-variable-syntax-3 +stdin: + set -A foo 0 + echo ${foo[*]:0} +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: xxx-variable-syntax-4 +description: + Not all kinds of trims are currently impossible, check those who do +stdin: + foo() { + echo "<$*> X${*:+ }X" + } + foo a b + foo "" c + foo "" + foo "" "" + IFS=: + foo a b + foo "" c + foo "" + foo "" "" + IFS= + foo a b + foo "" c + foo "" + foo "" "" +expected-stdout: + X X + < c> X X + <> XX + < > X X + X X + <:c> X X + <> XX + <:> X X + X X + X X + <> XX + <> XX +--- +name: xxx-substitution-eval-order +description: + Check order of evaluation of expressions +stdin: + i=1 x= y= + set -A A abc def GHI j G k + echo ${A[x=(i+=1)]#${A[y=(i+=2)]}} + echo $x $y +expected-stdout: + HI + 2 4 +--- +name: xxx-set-option-1 +description: + Check option parsing in set +stdin: + set -vsA foo -- A 1 3 2 + echo ${foo[*]} +expected-stderr: + echo ${foo[*]} +expected-stdout: + 1 2 3 A +--- +name: xxx-exec-1 +description: + Check that exec exits for built-ins +need-ctty: yes +arguments: !-i! +stdin: + exec echo hi + echo still herre +expected-stdout: + hi +expected-stderr-pattern: /.*/ +--- +name: xxx-while-1 +description: + Check the return value of while loops + XXX need to do same for for/select/until loops +stdin: + i=x + while [ $i != xxx ] ; do + i=x$i + if [ $i = xxx ] ; then + false + continue + fi + done + echo loop1=$? + + i=x + while [ $i != xxx ] ; do + i=x$i + if [ $i = xxx ] ; then + false + break + fi + done + echo loop2=$? + + i=x + while [ $i != xxx ] ; do + i=x$i + false + done + echo loop3=$? +expected-stdout: + loop1=0 + loop2=0 + loop3=1 +--- +name: xxx-status-1 +description: + Check that blank lines don't clear $? +need-ctty: yes +arguments: !-i! +stdin: + (exit 1) + echo $? + (exit 1) + + echo $? + true +expected-stdout: + 1 + 1 +expected-stderr-pattern: /.*/ +--- +name: xxx-status-2 +description: + Check that $? is preserved in subshells, includes, traps. +stdin: + (exit 1) + + echo blank: $? + + (exit 2) + (echo subshell: $?) + + echo 'echo include: $?' > foo + (exit 3) + . ./foo + + trap 'echo trap: $?' ERR + (exit 4) + echo exit: $? +expected-stdout: + blank: 1 + subshell: 2 + include: 3 + trap: 4 + exit: 4 +--- +name: xxx-clean-chars-1 +description: + Check MAGIC character is stuffed correctly +stdin: + echo `echo [�` +expected-stdout: + [� +--- +name: xxx-param-subst-qmark-1 +description: + Check suppresion of error message with null string. According to + POSIX, it shouldn't print the error as 'word' isn't ommitted. + ksh88/93, Solaris /bin/sh and /usr/xpg4/bin/sh all print the error. +stdin: + unset foo + x= + echo x${foo?$x} +expected-exit: 1 +expected-stderr-pattern: !/not set/ +--- +name: xxx-param-subst-qmark-namespec +description: + Check special names are output correctly +stdin: + doit() { + "$__progname" -c "$@" >o1 2>o2 + rv=$? + echo RETVAL: $rv + sed -e "s^${__progname%.exe}\.*e*x*e*: PROG: " -e 's/^/STDOUT: /g' 'c=a' + typeset c=[ab] + :>'d=a' + x=typeset; $x d=[ab] + echo "<$c>" "<$d>" + wd=$PWD + cd / + plus=$(print -r -- ~+) + minus=$(print -r -- ~-) + nix=$(print -r -- ~) + [[ $plus = / ]]; echo one $? . + [[ $minus = "$wd" ]]; echo two $? . + [[ $nix = /sweet ]]; echo nix $? . +expected-stdout: + <[ab]> + one 0 . + two 0 . + nix 0 . +--- +name: tilde-expand-3 +description: + Check mostly Austin 351 stuff +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + set "1 b=2" "3 d=4" + export a=$1 \c=$2 + showargs 1 "$a" "$b" "$c" "$d" + unset a b c d + HOME=/tmp + export \a=~ b=~ + command export c=~ + builtin export d=~ + \\builtin export e=~ + showargs 2 "$a" "$b" "$c" "$d" "$e" ksh + unset a b c d e + set -o posix + export \a=~ b=~ + command export c=~ + builtin export d=~ + \\builtin export e=~ + showargs 3 "$a" "$b" "$c" "$d" "$e" posix + unset a b c d e + set +o posix + export a=$1 + showargs 4 "$a" "$b" ksh + unset a b + showargs 5 a=$1 ksh + export \a=$1 + showargs 6 "$a" "$b" ksh + unset a b + set -o posix + export a=$1 + showargs 7 "$a" "$b" posix + unset a b + showargs 8 a=$1 posix + export \a=$1 + showargs 9 "$a" "$b" posix + unset a b + set +o posix + command echo 10 ksh a=~ + command command export a=~ + showargs 11 "$a" + unset a + set -o posix + command echo 12 posix a=~ + command command export a=~ + showargs 13 "$a" + unset a + # unspecified whether /tmp or ~ + var=export; command $var a=~ + showargs 14 "$a" + echo 'echo "<$foo>"' >bar + "$__progname" bar + var=foo + export $var=1 + "$__progname" bar + export $var=~ + "$__progname" bar + # unspecified + command -- export a=~ + showargs 18 "$a" + set -A bla + typeset bla[1]=~:~ + global gbl=~ g2=$1 + local lcl=~ l2=$1 + readonly ro=~ r2=$1 + showargs 19 "${bla[1]}" a=~ "$gbl" "$lcl" "$ro" "$g2" "$l2" "$r2" + set +o posix + echo "20 some arbitrary stuff "=~ + set -o posix + echo "21 some arbitrary stuff "=~ +expected-stdout: + <1> <1 b=2> <> <3> <4> . + <2> . + <3> <~> <~> . + <4> <1 b=2> <> . + <5> . + <6> <1> <2> . + <7> <1 b=2> <> . + <8> . + <9> <1> <2> . + 10 ksh a=/tmp + <11> . + 12 posix a=~ + <13> . + <14> <~> . + <> + <1> + <~> + <18> <~> . + <19> <1 b=2> <1 b=2> <1 b=2> . + 20 some arbitrary stuff =/tmp + 21 some arbitrary stuff =~ +--- +name: exit-err-1 +description: + Check some "exit on error" conditions +stdin: + print '#!'"$__progname"'\nexec "$1"' >env + print '#!'"$__progname"'\nexit 1' >false + chmod +x env false + PATH=.$PATHSEP$PATH + set -ex + env false && echo something + echo END +expected-stdout: + END +expected-stderr: + + env false + + echo END +--- +name: exit-err-2 +description: + Check some "exit on error" edge conditions (POSIXly) +stdin: + print '#!'"$__progname"'\nexec "$1"' >env + print '#!'"$__progname"'\nexit 1' >false + print '#!'"$__progname"'\nexit 0' >true + chmod +x env false + PATH=.$PATHSEP$PATH + set -ex + if env true; then + env false && echo something + fi + echo END +expected-stdout: + END +expected-stderr: + + env true + + env false + + echo END +--- +name: exit-err-3 +description: + pdksh regression which AT&T ksh does right + TFM says: [set] -e | errexit + Exit (after executing the ERR trap) ... +stdin: + trap 'echo EXIT' EXIT + trap 'echo ERR' ERR + set -e + cd /XXXXX 2>/dev/null + echo DONE + exit 0 +expected-stdout: + ERR + EXIT +expected-exit: e != 0 +--- +name: exit-err-4 +description: + "set -e" test suite (POSIX) +stdin: + set -e + echo pre + if true ; then + false && echo foo + fi + echo bar +expected-stdout: + pre + bar +--- +name: exit-err-5 +description: + "set -e" test suite (POSIX) +stdin: + set -e + foo() { + while [ "$1" ]; do + for E in $x; do + [ "$1" = "$E" ] && { shift ; continue 2 ; } + done + x="$x $1" + shift + done + echo $x + } + echo pre + foo a b b c + echo post +expected-stdout: + pre + a b c + post +--- +name: exit-err-6 +description: + "set -e" test suite (BSD make) +category: os:mirbsd +stdin: + mkdir zd zd/a zd/b + print 'all:\n\t@echo eins\n\t@exit 42\n' >zd/a/Makefile + print 'all:\n\t@echo zwei\n' >zd/b/Makefile + wd=$(pwd) + set -e + for entry in a b; do ( set -e; if [[ -d $wd/zd/$entry.i386 ]]; then _newdir_="$entry.i386"; else _newdir_="$entry"; fi; if [[ -z $_THISDIR_ ]]; then _nextdir_="$_newdir_"; else _nextdir_="$_THISDIR_/$_newdir_"; fi; _makefile_spec_=; [[ ! -f $wd/zd/$_newdir_/Makefile.bsd-wrapper ]] || _makefile_spec_="-f Makefile.bsd-wrapper"; subskipdir=; for skipdir in ; do subentry=${skipdir#$entry}; if [[ $subentry != $skipdir ]]; then if [[ -z $subentry ]]; then echo "($_nextdir_ skipped)"; break; fi; subskipdir="$subskipdir ${subentry#/}"; fi; done; if [[ -z $skipdir || -n $subentry ]]; then echo "===> $_nextdir_"; cd $wd/zd/$_newdir_; make SKIPDIR="$subskipdir" $_makefile_spec_ _THISDIR_="$_nextdir_" all; fi; ) done 2>&1 | sed "s!$wd!WD!g" +expected-stdout: + ===> a + eins + *** Error code 42 + + Stop in WD/zd/a (line 2 of Makefile). +--- +name: exit-err-7 +description: + "set -e" regression (LP#1104543) +stdin: + set -e + bla() { + [ -x $PWD/nonexistant ] && $PWD/nonexistant + } + echo x + bla + echo y$? +expected-stdout: + x +expected-exit: 1 +--- +name: exit-err-8 +description: + "set -e" regression (Debian #700526) +stdin: + set -e + _db_cmd() { return $1; } + db_input() { _db_cmd 30; } + db_go() { _db_cmd 0; } + db_input || : + db_go + exit 0 +--- +name: exit-err-9 +description: + "set -e" versus bang pipelines +stdin: + set -e + ! false | false + echo 1 ok + ! false && false + echo 2 wrong +expected-stdout: + 1 ok +expected-exit: 1 +--- +name: exit-enoent-1 +description: + SUSv4 says that the shell should exit with 126/127 in some situations +stdin: + i=0 + (echo; echo :) >x + "$__progname" ./x >/dev/null 2>&1; r=$?; echo $((i++)) $r . + "$__progname" -c ./x >/dev/null 2>&1; r=$?; echo $((i++)) $r . + echo exit 42 >x + "$__progname" ./x >/dev/null 2>&1; r=$?; echo $((i++)) $r . + "$__progname" -c ./x >/dev/null 2>&1; r=$?; echo $((i++)) $r . + rm -f x + "$__progname" ./x >/dev/null 2>&1; r=$?; echo $((i++)) $r . + "$__progname" -c ./x >/dev/null 2>&1; r=$?; echo $((i++)) $r . +expected-stdout: + 0 0 . + 1 126 . + 2 42 . + 3 126 . + 4 127 . + 5 127 . +--- +name: exit-eval-1 +description: + Check eval vs substitution exit codes (ksh93 alike) +stdin: + (exit 12) + eval $(false) + echo A $? + (exit 12) + eval ' $(false)' + echo B $? + (exit 12) + eval " $(false)" + echo C $? + (exit 12) + eval "eval $(false)" + echo D $? + (exit 12) + eval 'eval '"$(false)" + echo E $? + IFS="$IFS:" + (exit 12) + eval $(echo :; false) + echo F $? + echo -n "G " + (exit 12) + eval 'echo $?' + echo H $? +expected-stdout: + A 0 + B 1 + C 0 + D 0 + E 0 + F 0 + G 12 + H 0 +--- +name: exit-trap-1 +description: + Check that "exit" with no arguments behaves SUSv4 conformant. +stdin: + trap 'echo hi; exit' EXIT + exit 9 +expected-stdout: + hi +expected-exit: 9 +--- +name: exit-trap-2 +description: + Check that ERR and EXIT traps are run just like ksh93 does. + GNU bash does not run ERtrap in ±e eval-undef but runs it + twice (bug?) in +e eval-false, so does ksh93 (bug?), which + also has a bug to continue execution (echoing "and out" and + returning 0) in +e eval-undef. +file-setup: file 644 "x" + v=; unset v + trap 'echo EXtrap' EXIT + trap 'echo ERtrap' ERR + set $1 + echo "and run $2" + eval $2 + echo and out +file-setup: file 644 "xt" + v=; unset v + trap 'echo EXtrap' EXIT + trap 'echo ERtrap' ERR + set $1 + echo 'and run true' + true + echo and out +file-setup: file 644 "xf" + v=; unset v + trap 'echo EXtrap' EXIT + trap 'echo ERtrap' ERR + set $1 + echo 'and run false' + false + echo and out +file-setup: file 644 "xu" + v=; unset v + trap 'echo EXtrap' EXIT + trap 'echo ERtrap' ERR + set $1 + echo 'and run ${v?}' + ${v?} + echo and out +stdin: + runtest() { + rm -f rc + ( + "$__progname" "$@" + echo $? >rc + ) 2>&1 | sed \ + -e 's/parameter not set/parameter null or not set/' \ + -e 's/[[]6]//' -e 's/: eval: line 1//' -e 's/: line 6//' \ + -e "s^${__progname%.exe}\.*e*x*e*: \[[0-9]*]PROG" + } + xe=-e + echo : $xe + runtest x $xe true + echo = eval-true $(' 2005/08/21 && echo ja || echo nein + test 2005/08/21 \> 2005/10/08 && echo ja || echo nein +expected-stdout: + nein + ja + ja + nein +expected-stderr-pattern: !/unexpected op/ +--- +name: test-precedence-1 +description: + Check a weird precedence case (and POSIX echo) +stdin: + test \( -f = -f \) + rv=$? + echo $rv +expected-stdout: + 0 +--- +name: test-option-1 +description: + Test the test -o operator +stdin: + runtest() { + test -o $1; echo $? + [ -o $1 ]; echo $? + [[ -o $1 ]]; echo $? + } + if_test() { + test -o $1 -o -o !$1; echo $? + [ -o $1 -o -o !$1 ]; echo $? + [[ -o $1 || -o !$1 ]]; echo $? + test -o ?$1; echo $? + } + echo 0y $(if_test utf8-mode) = + echo 0n $(if_test utf8-hack) = + echo 1= $(runtest utf8-hack) = + echo 2= $(runtest !utf8-hack) = + echo 3= $(runtest ?utf8-hack) = + set +U + echo 1+ $(runtest utf8-mode) = + echo 2+ $(runtest !utf8-mode) = + echo 3+ $(runtest ?utf8-mode) = + set -U + echo 1- $(runtest utf8-mode) = + echo 2- $(runtest !utf8-mode) = + echo 3- $(runtest ?utf8-mode) = + echo = short flags = + echo 0y $(if_test -U) = + echo 0y $(if_test +U) = + echo 0n $(if_test -_) = + echo 0n $(if_test -U-) = + echo 1= $(runtest -_) = + echo 2= $(runtest !-_) = + echo 3= $(runtest ?-_) = + set +U + echo 1+ $(runtest -U) = + echo 2+ $(runtest !-U) = + echo 3+ $(runtest ?-U) = + echo 1+ $(runtest +U) = + echo 2+ $(runtest !+U) = + echo 3+ $(runtest ?+U) = + set -U + echo 1- $(runtest -U) = + echo 2- $(runtest !-U) = + echo 3- $(runtest ?-U) = + echo 1- $(runtest +U) = + echo 2- $(runtest !+U) = + echo 3- $(runtest ?+U) = +expected-stdout: + 0y 0 0 0 0 = + 0n 1 1 1 1 = + 1= 1 1 1 = + 2= 1 1 1 = + 3= 1 1 1 = + 1+ 1 1 1 = + 2+ 0 0 0 = + 3+ 0 0 0 = + 1- 0 0 0 = + 2- 1 1 1 = + 3- 0 0 0 = + = short flags = + 0y 0 0 0 0 = + 0y 0 0 0 0 = + 0n 1 1 1 1 = + 0n 1 1 1 1 = + 1= 1 1 1 = + 2= 1 1 1 = + 3= 1 1 1 = + 1+ 1 1 1 = + 2+ 0 0 0 = + 3+ 0 0 0 = + 1+ 1 1 1 = + 2+ 0 0 0 = + 3+ 0 0 0 = + 1- 0 0 0 = + 2- 1 1 1 = + 3- 0 0 0 = + 1- 0 0 0 = + 2- 1 1 1 = + 3- 0 0 0 = +--- +name: test-varset-1 +description: + Test the test -v operator +stdin: + [[ -v a ]] + rv=$?; echo $((++i)) $rv + a= + [[ -v a ]] + rv=$?; echo $((++i)) $rv + unset a + [[ -v a ]] + rv=$?; echo $((++i)) $rv + a=x + [[ -v a ]] + rv=$?; echo $((++i)) $rv + nameref b=a + [[ -v b ]] + rv=$?; echo $((++i)) $rv + unset a + [[ -v b ]] + rv=$?; echo $((++i)) $rv + x[1]=y + [[ -v x ]] + rv=$?; echo $((++i)) $rv + [[ -v x[0] ]] + rv=$?; echo $((++i)) $rv + [[ -v x[1] ]] + rv=$?; echo $((++i)) $rv + [[ -v x[2] ]] + rv=$?; echo $((++i)) $rv +expected-stdout: + 1 1 + 2 0 + 3 1 + 4 0 + 5 0 + 6 1 + 7 1 + 8 1 + 9 0 + 10 1 +--- +name: test-varset-2 +description: + test -v works only on scalars +stdin: + [[ -v x[*] ]] + echo ok +expected-exit: e != 0 +expected-stderr-pattern: + /unexpected '\*'/ +--- +name: test-stnze-1 +description: + Check that the short form [ $x ] works +stdin: + i=0 + [ -n $x ] + rv=$?; echo $((++i)) $rv + [ $x ] + rv=$?; echo $((++i)) $rv + [ -n "$x" ] + rv=$?; echo $((++i)) $rv + [ "$x" ] + rv=$?; echo $((++i)) $rv + x=0 + [ -n $x ] + rv=$?; echo $((++i)) $rv + [ $x ] + rv=$?; echo $((++i)) $rv + [ -n "$x" ] + rv=$?; echo $((++i)) $rv + [ "$x" ] + rv=$?; echo $((++i)) $rv + x='1 -a 1 = 2' + [ -n $x ] + rv=$?; echo $((++i)) $rv + [ $x ] + rv=$?; echo $((++i)) $rv + [ -n "$x" ] + rv=$?; echo $((++i)) $rv + [ "$x" ] + rv=$?; echo $((++i)) $rv +expected-stdout: + 1 0 + 2 1 + 3 1 + 4 1 + 5 0 + 6 0 + 7 0 + 8 0 + 9 1 + 10 1 + 11 0 + 12 0 +--- +name: test-stnze-2 +description: + Check that the short form [[ $x ]] works (ksh93 extension) +stdin: + i=0 + [[ -n $x ]] + rv=$?; echo $((++i)) $rv + [[ $x ]] + rv=$?; echo $((++i)) $rv + [[ -n "$x" ]] + rv=$?; echo $((++i)) $rv + [[ "$x" ]] + rv=$?; echo $((++i)) $rv + x=0 + [[ -n $x ]] + rv=$?; echo $((++i)) $rv + [[ $x ]] + rv=$?; echo $((++i)) $rv + [[ -n "$x" ]] + rv=$?; echo $((++i)) $rv + [[ "$x" ]] + rv=$?; echo $((++i)) $rv + x='1 -a 1 = 2' + [[ -n $x ]] + rv=$?; echo $((++i)) $rv + [[ $x ]] + rv=$?; echo $((++i)) $rv + [[ -n "$x" ]] + rv=$?; echo $((++i)) $rv + [[ "$x" ]] + rv=$?; echo $((++i)) $rv +expected-stdout: + 1 1 + 2 1 + 3 1 + 4 1 + 5 0 + 6 0 + 7 0 + 8 0 + 9 0 + 10 0 + 11 0 + 12 0 +--- +name: test-numeq +description: + Check numeric -eq works (R40d regression); spotted by Martijn Dekker +stdin: + tst() { + eval "$2" + case $? in + (0) echo yepp 0 \#"$*" ;; + (1) echo nope 1 \#"$*" ;; + (2) echo terr 2 \#"$*" ;; + (*) echo wtf\? $? \#"$*" ;; + esac + } + tst 1 'test 2 -eq 2' + tst 2 'test 2 -eq 2a' + tst 3 'test 2 -eq 3' + tst 4 'test 2 -ne 2' + tst 5 'test 2 -ne 2a' + tst 6 'test 2 -ne 3' + tst 7 'test \! 2 -eq 2' + tst 8 'test \! 2 -eq 2a' + tst 9 'test \! 2 -eq 3' +expected-stdout: + yepp 0 #1 test 2 -eq 2 + terr 2 #2 test 2 -eq 2a + nope 1 #3 test 2 -eq 3 + nope 1 #4 test 2 -ne 2 + terr 2 #5 test 2 -ne 2a + yepp 0 #6 test 2 -ne 3 + nope 1 #7 test \! 2 -eq 2 + terr 2 #8 test \! 2 -eq 2a + yepp 0 #9 test \! 2 -eq 3 +expected-stderr-pattern: + /bad number/ +--- +name: mkshrc-1 +description: + Check that ~/.mkshrc works correctly. + Part 1: verify user environment is not read (internal) +stdin: + echo x $FNORD +expected-stdout: + x +--- +name: mkshrc-2a +description: + Check that ~/.mkshrc works correctly. + Part 2: verify mkshrc is not read (non-interactive shells) +file-setup: file 644 ".mkshrc" + FNORD=42 +env-setup: !HOME=.!ENV=! +stdin: + echo x $FNORD +expected-stdout: + x +--- +name: mkshrc-2b +description: + Check that ~/.mkshrc works correctly. + Part 2: verify mkshrc can be read (interactive shells) +file-setup: file 644 ".mkshrc" + FNORD=42 +need-ctty: yes +arguments: !-i! +env-setup: !HOME=.!ENV=!PS1=! +stdin: + echo x $FNORD +expected-stdout: + x 42 +expected-stderr-pattern: + /(# )*/ +--- +name: mkshrc-3 +description: + Check that ~/.mkshrc works correctly. + Part 3: verify mkshrc can be turned off +file-setup: file 644 ".mkshrc" + FNORD=42 +env-setup: !HOME=.!ENV=nonexistant! +stdin: + echo x $FNORD +expected-stdout: + x +--- +name: sh-mode-1 +description: + Check that sh mode turns braceexpand off + and that that works correctly +stdin: + set -o braceexpand + set +o sh + [[ $(set +o) == *@(-o sh)@(| *) ]] && echo sh || echo nosh + [[ $(set +o) == *@(-o braceexpand)@(| *) ]] && echo brex || echo nobrex + echo {a,b,c} + set +o braceexpand + echo {a,b,c} + set -o braceexpand + echo {a,b,c} + set -o sh + echo {a,b,c} + [[ $(set +o) == *@(-o sh)@(| *) ]] && echo sh || echo nosh + [[ $(set +o) == *@(-o braceexpand)@(| *) ]] && echo brex || echo nobrex + set -o braceexpand + echo {a,b,c} + [[ $(set +o) == *@(-o sh)@(| *) ]] && echo sh || echo nosh + [[ $(set +o) == *@(-o braceexpand)@(| *) ]] && echo brex || echo nobrex +expected-stdout: + nosh + brex + a b c + {a,b,c} + a b c + {a,b,c} + sh + nobrex + a b c + sh + brex +--- +name: sh-mode-2a +description: + Check that posix or sh mode is *not* automatically turned on +category: !binsh +stdin: + ln -s "$__progname" ksh || cp "$__progname" ksh + ln -s "$__progname" sh || cp "$__progname" sh + ln -s "$__progname" ./-ksh || cp "$__progname" ./-ksh + ln -s "$__progname" ./-sh || cp "$__progname" ./-sh + for shell in {,-}{,k}sh; do + print -- $shell $(./$shell +l -c \ + '[[ $(set +o) == *"-o "@(sh|posix)@(| *) ]] && echo sh || echo nosh') + done +expected-stdout: + sh nosh + ksh nosh + -sh nosh + -ksh nosh +--- +name: sh-mode-2b +description: + Check that posix or sh mode *is* automatically turned on +category: binsh +stdin: + ln -s "$__progname" ksh || cp "$__progname" ksh + ln -s "$__progname" sh || cp "$__progname" sh + ln -s "$__progname" ./-ksh || cp "$__progname" ./-ksh + ln -s "$__progname" ./-sh || cp "$__progname" ./-sh + for shell in {,-}{,k}sh; do + print -- $shell $(./$shell +l -c \ + '[[ $(set +o) == *"-o "@(sh|posix)@(| *) ]] && echo sh || echo nosh') + done +expected-stdout: + sh sh + ksh nosh + -sh sh + -ksh nosh +--- +name: pipeline-1 +description: + pdksh bug: last command of a pipeline is executed in a + subshell - make sure it still is, scripts depend on it +file-setup: file 644 "abcx" +file-setup: file 644 "abcy" +stdin: + echo * + echo a | while read d; do + echo $d + echo $d* + echo * + set -o noglob + echo $d* + echo * + done + echo * +expected-stdout: + abcx abcy + a + abcx abcy + abcx abcy + a* + * + abcx abcy +--- +name: pipeline-2 +description: + check that co-processes work with TCOMs, TPIPEs and TPARENs +category: !nojsig +stdin: + "$__progname" -c 'i=100; echo hi |& while read -p line; do echo "$((i++)) $line"; done' + "$__progname" -c 'i=200; echo hi | cat |& while read -p line; do echo "$((i++)) $line"; done' + "$__progname" -c 'i=300; (echo hi | cat) |& while read -p line; do echo "$((i++)) $line"; done' +expected-stdout: + 100 hi + 200 hi + 300 hi +--- +name: pipeline-3 +description: + Check that PIPESTATUS does what it's supposed to +stdin: + echo 1 $PIPESTATUS . + echo 2 ${PIPESTATUS[0]} . + echo 3 ${PIPESTATUS[1]} . + (echo x; exit 12) | (cat; exit 23) | (cat; exit 42) + echo 5 $? , $PIPESTATUS , ${PIPESTATUS[0]} , ${PIPESTATUS[1]} , ${PIPESTATUS[2]} , ${PIPESTATUS[3]} . + echo 6 ${PIPESTATUS[0]} . + set | fgrep PIPESTATUS + echo 8 $(set | fgrep PIPESTATUS) . +expected-stdout: + 1 0 . + 2 0 . + 3 . + x + 5 42 , 12 , 12 , 23 , 42 , . + 6 0 . + PIPESTATUS[0]=0 + 8 PIPESTATUS[0]=0 PIPESTATUS[1]=0 . +--- +name: pipeline-4 +description: + Check that "set -o pipefail" does what it's supposed to +stdin: + echo 1 "$("$__progname" -c '(exit 12) | (exit 23) | (exit 42); echo $?')" . + echo 2 "$("$__progname" -c '! (exit 12) | (exit 23) | (exit 42); echo $?')" . + echo 3 "$("$__progname" -o pipefail -c '(exit 12) | (exit 23) | (exit 42); echo $?')" . + echo 4 "$("$__progname" -o pipefail -c '! (exit 12) | (exit 23) | (exit 42); echo $?')" . + echo 5 "$("$__progname" -c '(exit 23) | (exit 42) | :; echo $?')" . + echo 6 "$("$__progname" -c '! (exit 23) | (exit 42) | :; echo $?')" . + echo 7 "$("$__progname" -o pipefail -c '(exit 23) | (exit 42) | :; echo $?')" . + echo 8 "$("$__progname" -o pipefail -c '! (exit 23) | (exit 42) | :; echo $?')" . + echo 9 "$("$__progname" -o pipefail -c 'x=$( (exit 23) | (exit 42) | :); echo $?')" . +expected-stdout: + 1 42 . + 2 0 . + 3 42 . + 4 0 . + 5 0 . + 6 1 . + 7 42 . + 8 0 . + 9 42 . +--- +name: persist-history-1 +description: + Check if persistent history saving works +category: !no-histfile +need-ctty: yes +arguments: !-i! +env-setup: !ENV=./Env!HISTFILE=hist.file! +file-setup: file 644 "Env" + PS1=X +stdin: + cat hist.file +expected-stdout-pattern: + /cat hist.file/ +expected-stderr-pattern: + /^X*$/ +--- +name: typeset-1 +description: + Check that typeset -g works correctly +stdin: + set -A arrfoo 65 + foo() { + typeset -g -Uui16 arrfoo[*] + } + echo before ${arrfoo[0]} . + foo + echo after ${arrfoo[0]} . + set -A arrbar 65 + bar() { + echo inside before ${arrbar[0]} . + arrbar[0]=97 + echo inside changed ${arrbar[0]} . + typeset -g -Uui16 arrbar[*] + echo inside typeset ${arrbar[0]} . + arrbar[0]=48 + echo inside changed ${arrbar[0]} . + } + echo before ${arrbar[0]} . + bar + echo after ${arrbar[0]} . +expected-stdout: + before 65 . + after 16#41 . + before 65 . + inside before 65 . + inside changed 97 . + inside typeset 16#61 . + inside changed 16#30 . + after 16#30 . +--- +name: typeset-2 +description: + Check that typeset -p on arrays works correctly +stdin: + set -A x -- a b c + echo = + typeset -p x + echo = + typeset -p x[1] +expected-stdout: + = + set -A x + typeset x[0]=a + typeset x[1]=b + typeset x[2]=c + = + typeset x[1]=b +--- +name: typeset-padding-1 +description: + Check if left/right justification works as per TFM +stdin: + typeset -L10 ln=0hall0 + typeset -R10 rn=0hall0 + typeset -ZL10 lz=0hall0 + typeset -ZR10 rz=0hall0 + typeset -Z10 rx=" hallo " + echo "<$ln> <$rn> <$lz> <$rz> <$rx>" +expected-stdout: + <0hall0 > < 0hall0> <00000hall0> <0000 hallo> +--- +name: typeset-padding-2 +description: + Check if base-!10 integers are padded right +stdin: + typeset -Uui16 -L9 ln=16#1 + typeset -Uui16 -R9 rn=16#1 + typeset -Uui16 -Z9 zn=16#1 + typeset -L9 ls=16#1 + typeset -R9 rs=16#1 + typeset -Z9 zs=16#1 + echo "<$ln> <$rn> <$zn> <$ls> <$rs> <$zs>" +expected-stdout: + <16#1 > < 16#1> <16#000001> <16#1 > < 16#1> <0000016#1> +--- +name: utf8bom-1 +description: + Check that the UTF-8 Byte Order Mark is ignored as the first + multibyte character of the shell input (with -c, from standard + input, as file, or as eval argument), but nowhere else +# breaks on Mac OSX (HFS+ non-standard Unicode canonical decomposition) +category: !os:darwin,!shell:ebcdic-yes +stdin: + mkdir foo + print '#!/bin/sh\necho ohne' >foo/fnord + print '#!/bin/sh\necho mit' >foo/fnord + print 'fnord\nfnord\nfnord\nfnord' >foo/bar + print eval \''fnord\nfnord\nfnord\nfnord'\' >foo/zoo + set -A anzahl -- foo/* + echo got ${#anzahl[*]} files + chmod +x foo/* + export PATH=$(pwd)/foo$PATHSEP$PATH + "$__progname" -c 'fnord' + echo = + "$__progname" -c 'fnord; fnord; fnord; fnord' + echo = + "$__progname" foo/bar + echo = + "$__progname" t1 + print '#!'"$__progname"'\nprint "2 a=$ENV{FOO}";' >t2 + print '#!'"$__perlname"'\nprint "3 a=$ENV{FOO}\n";' >t3 + print '#!'"$__perlname"'\nprint "4 a=$ENV{FOO}\n";' >t4 + chmod +x t? + ./t1 + ./t2 + ./t3 + ./t4 +expected-stdout: + 1 a=/nonexistant{FOO} + 2 a=/nonexistant{FOO} + 3 a=BAR + 4 a=BAR +expected-stderr-pattern: + /(Unrecognized character .... ignored at \..t4 line 1)*/ +--- +name: utf8opt-1 +description: + Check that the utf8-mode flag is not set at non-interactive startup +env-setup: !PS1=!PS2=!LC_CTYPE=@utflocale@! +stdin: + if [[ $- = *U* ]]; then + echo is set + else + echo is not set + fi +expected-stdout: + is not set +--- +name: utf8opt-2 +description: + Check that the utf8-mode flag is set at interactive startup. + If your OS is old, try passing HAVE_SETLOCALE_CTYPE=0 to Build.sh +need-pass: no +category: !noutf8 +need-ctty: yes +arguments: !-i! +env-setup: !PS1=!PS2=!LC_CTYPE=@utflocale@! +stdin: + if [[ $- = *U* ]]; then + echo is set + else + echo is not set + fi +expected-stdout: + is set +expected-stderr-pattern: + /(# )*/ +--- +name: utf8opt-3a +description: + Ensure ±U on the command line is honoured + (these two tests may pass falsely depending on CPPFLAGS) +stdin: + export i=0 + code='if [[ $- = *U* ]]; then echo $i on; else echo $i off; fi' + let i++; "$__progname" -U -c "$code" + let i++; "$__progname" +U -c "$code" + echo $((++i)) done +expected-stdout: + 1 on + 2 off + 3 done +--- +name: utf8opt-3b +description: + Ensure ±U on the command line is honoured, interactive shells +need-ctty: yes +stdin: + export i=0 + code='if [[ $- = *U* ]]; then echo $i on; else echo $i off; fi' + let i++; "$__progname" -U -ic "$code" + let i++; "$__progname" +U -ic "$code" + echo $((++i)) done +expected-stdout: + 1 on + 2 off + 3 done +--- +name: utf8bug-1 +description: + Ensure trailing combining characters are not lost +stdin: + set -U + a=a + b=$'\u0301' + x=$a$b + print -r -- "" + x=$a + x+=$b + print -r -- "" + b=$'\u0301'b + x=$a + x+=$b + print -r -- "" +expected-stdout: + + + +--- +name: aliases-1 +description: + Check if built-in shell aliases are okay +stdin: + alias + typeset -f +expected-stdout: + autoload='\\builtin typeset -fu' + functions='\\builtin typeset -f' + hash='\\builtin alias -t' + history='\\builtin fc -l' + integer='\\builtin typeset -i' + local='\\builtin typeset' + login='\\builtin exec login' + nameref='\\builtin typeset -n' + nohup='nohup ' + r='\\builtin fc -e -' + type='\\builtin whence -v' +--- +name: aliases-2b +description: + Check if “set -o sh” does not influence built-in aliases +arguments: !-o!sh! +stdin: + alias + typeset -f +expected-stdout: + autoload='\\builtin typeset -fu' + functions='\\builtin typeset -f' + hash='\\builtin alias -t' + history='\\builtin fc -l' + integer='\\builtin typeset -i' + local='\\builtin typeset' + login='\\builtin exec login' + nameref='\\builtin typeset -n' + nohup='nohup ' + r='\\builtin fc -e -' + type='\\builtin whence -v' +--- +name: aliases-3b +description: + Check if running as sh does not influence built-in aliases +stdin: + cp "$__progname" sh + ./sh -c 'alias; typeset -f' + rm -f sh +expected-stdout: + autoload='\\builtin typeset -fu' + functions='\\builtin typeset -f' + hash='\\builtin alias -t' + history='\\builtin fc -l' + integer='\\builtin typeset -i' + local='\\builtin typeset' + login='\\builtin exec login' + nameref='\\builtin typeset -n' + nohup='nohup ' + r='\\builtin fc -e -' + type='\\builtin whence -v' +--- +name: aliases-cmdline +description: + Check that aliases work from the command line (Debian #517009) + Note that due to the nature of the lexing process, defining + aliases in COMSUBs then immediately using them, and things + like 'alias foo=bar && foo', still fail. +stdin: + "$__progname" -c $'alias a="echo OK"\na' +expected-stdout: + OK +--- +name: aliases-funcdef-1 +description: + Check if POSIX functions take precedences over aliases +stdin: + alias foo='echo makro' + foo() { + echo funktion + } + foo +expected-stdout: + makro +--- +name: aliases-funcdef-2 +description: + Check if POSIX functions take precedences over aliases +stdin: + alias foo='echo makro' + foo () { + echo funktion + } + foo +expected-stdout: + makro +--- +name: aliases-funcdef-3 +description: + Check if aliases take precedences over Korn functions +stdin: + alias foo='echo makro' + function foo { + echo funktion + } + foo +expected-stdout: + makro +--- +name: aliases-funcdef-4 +description: + Functions should only take over if actually being defined +stdin: + alias local + :|| local() { :; } + alias local +expected-stdout: + local='\\builtin typeset' + local='\\builtin typeset' +--- +name: arrays-1 +description: + Check if Korn Shell arrays work as expected +stdin: + v="c d" + set -A foo -- a \$v "$v" '$v' b + echo "${#foo[*]}|${foo[0]}|${foo[1]}|${foo[2]}|${foo[3]}|${foo[4]}|" +expected-stdout: + 5|a|$v|c d|$v|b| +--- +name: arrays-2a +description: + Check if bash-style arrays work as expected +stdin: + v="c d" + foo=(a \$v "$v" '$v' b) + echo "${#foo[*]}|${foo[0]}|${foo[1]}|${foo[2]}|${foo[3]}|${foo[4]}|" +expected-stdout: + 5|a|$v|c d|$v|b| +--- +name: arrays-2b +description: + Check if bash-style arrays work as expected, with newlines +stdin: + print '#!'"$__progname"'\nfor x in "$@"; do print -nr -- "$x|"; done' >pfp + chmod +x pfp + test -n "$ZSH_VERSION" && setopt KSH_ARRAYS + v="e f" + foo=(a + bc + d \$v "$v" '$v' g + ) + ./pfp "${#foo[*]}" "${foo[0]}" "${foo[1]}" "${foo[2]}" "${foo[3]}" "${foo[4]}" "${foo[5]}" "${foo[6]}"; echo + foo=(a\ + bc + d \$v "$v" '$v' g + ) + ./pfp "${#foo[*]}" "${foo[0]}" "${foo[1]}" "${foo[2]}" "${foo[3]}" "${foo[4]}" "${foo[5]}" "${foo[6]}"; echo + foo=(a\ + bc\\ + d \$v "$v" '$v' + g) + ./pfp "${#foo[*]}" "${foo[0]}" "${foo[1]}" "${foo[2]}" "${foo[3]}" "${foo[4]}" "${foo[5]}" "${foo[6]}"; echo +expected-stdout: + 7|a|bc|d|$v|e f|$v|g| + 7|a|bc|d|$v|e f|$v|g| + 6|abc\|d|$v|e f|$v|g|| +--- +name: arrays-3 +description: + Check if array bounds are uint32_t +stdin: + set -A foo a b c + foo[4097]=d + foo[2147483637]=e + echo ${foo[*]} + foo[-1]=f + echo ${foo[4294967295]} g ${foo[*]} +expected-stdout: + a b c d e + f g a b c d e f +--- +name: arrays-4 +description: + Check if Korn Shell arrays with specified indices work as expected +stdin: + v="c d" + set -A foo -- [1]=\$v [2]="$v" [4]='$v' [0]=a [5]=b + echo "${#foo[*]}|${foo[0]}|${foo[1]}|${foo[2]}|${foo[3]}|${foo[4]}|${foo[5]}|" + # we don't want this at all: + # 5|a|$v|c d||$v|b| + set -A arr "[5]=meh" + echo "<${arr[0]}><${arr[5]}>" +expected-stdout: + 5|[1]=$v|[2]=c d|[4]=$v|[0]=a|[5]=b|| + <[5]=meh><> +--- +name: arrays-5 +description: + Check if bash-style arrays with specified indices work as expected + (taken out temporarily to fix arrays-4; see also arrays-9a comment) +category: disabled +stdin: + v="c d" + foo=([1]=\$v [2]="$v" [4]='$v' [0]=a [5]=b) + echo "${#foo[*]}|${foo[0]}|${foo[1]}|${foo[2]}|${foo[3]}|${foo[4]}|${foo[5]}|" + x=([128]=foo bar baz) + echo k= ${!x[*]} . + echo v= ${x[*]} . + # Check that we do not break this by globbing + :>b=blah + bleh=5 + typeset -a arr + arr+=([bleh]=blah) + echo "<${arr[0]}><${arr[5]}>" +expected-stdout: + 5|a|$v|c d||$v|b| + k= 128 129 130 . + v= foo bar baz . + <> +--- +name: arrays-6 +description: + Check if we can get the array keys (indices) for indexed arrays, + Korn shell style +stdin: + of() { + i=0 + for x in "$@"; do + echo -n "$((i++))<$x>" + done + echo + } + foo[1]=eins + set | grep '^foo' + echo = + foo[0]=zwei + foo[4]=drei + set | grep '^foo' + echo = + echo a $(of ${foo[*]}) = $(of ${bar[*]}) a + echo b $(of "${foo[*]}") = $(of "${bar[*]}") b + echo c $(of ${foo[@]}) = $(of ${bar[@]}) c + echo d $(of "${foo[@]}") = $(of "${bar[@]}") d + echo e $(of ${!foo[*]}) = $(of ${!bar[*]}) e + echo f $(of "${!foo[*]}") = $(of "${!bar[*]}") f + echo g $(of ${!foo[@]}) = $(of ${!bar[@]}) g + echo h $(of "${!foo[@]}") = $(of "${!bar[@]}") h +expected-stdout: + foo[1]=eins + = + foo[0]=zwei + foo[1]=eins + foo[4]=drei + = + a 012 = a + b 0 = 0<> b + c 012 = c + d 012 = d + e 0<0>1<1>2<4> = e + f 0<0 1 4> = 0<> f + g 0<0>1<1>2<4> = g + h 0<0>1<1>2<4> = h +--- +name: arrays-7 +description: + Check if we can get the array keys (indices) for indexed arrays, + Korn shell style, in some corner cases +stdin: + echo !arz: ${!arz} + echo !arz[0]: ${!arz[0]} + echo !arz[1]: ${!arz[1]} + arz=foo + echo !arz: ${!arz} + echo !arz[0]: ${!arz[0]} + echo !arz[1]: ${!arz[1]} + unset arz + echo !arz: ${!arz} + echo !arz[0]: ${!arz[0]} + echo !arz[1]: ${!arz[1]} +expected-stdout: + !arz: arz + !arz[0]: arz[0] + !arz[1]: arz[1] + !arz: arz + !arz[0]: arz[0] + !arz[1]: arz[1] + !arz: arz + !arz[0]: arz[0] + !arz[1]: arz[1] +--- +name: arrays-8 +description: + Check some behavioural rules for arrays. +stdin: + fna() { + set -A aa 9 + } + fnb() { + typeset ab + set -A ab 9 + } + fnc() { + typeset ac + set -A ac 91 + unset ac + set -A ac 92 + } + fnd() { + set +A ad 9 + } + fne() { + unset ae + set +A ae 9 + } + fnf() { + unset af[0] + set +A af 9 + } + fng() { + unset ag[*] + set +A ag 9 + } + set -A aa 1 2 + set -A ab 1 2 + set -A ac 1 2 + set -A ad 1 2 + set -A ae 1 2 + set -A af 1 2 + set -A ag 1 2 + set -A ah 1 2 + typeset -Z3 aa ab ac ad ae af ag + print 1a ${aa[*]} . + print 1b ${ab[*]} . + print 1c ${ac[*]} . + print 1d ${ad[*]} . + print 1e ${ae[*]} . + print 1f ${af[*]} . + print 1g ${ag[*]} . + print 1h ${ah[*]} . + fna + fnb + fnc + fnd + fne + fnf + fng + typeset -Z5 ah[*] + print 2a ${aa[*]} . + print 2b ${ab[*]} . + print 2c ${ac[*]} . + print 2d ${ad[*]} . + print 2e ${ae[*]} . + print 2f ${af[*]} . + print 2g ${ag[*]} . + print 2h ${ah[*]} . +expected-stdout: + 1a 001 002 . + 1b 001 002 . + 1c 001 002 . + 1d 001 002 . + 1e 001 002 . + 1f 001 002 . + 1g 001 002 . + 1h 1 2 . + 2a 9 . + 2b 001 002 . + 2c 92 . + 2d 009 002 . + 2e 9 . + 2f 9 002 . + 2g 009 . + 2h 00001 00002 . +--- +name: arrays-9a +description: + Check that we can concatenate arrays +stdin: + unset foo; foo=(bar); foo+=(baz); echo 1 ${!foo[*]} : ${foo[*]} . + unset foo; foo=(foo bar); foo+=(baz); echo 2 ${!foo[*]} : ${foo[*]} . +# unset foo; foo=([2]=foo [0]=bar); foo+=(baz [5]=quux); echo 3 ${!foo[*]} : ${foo[*]} . +expected-stdout: + 1 0 1 : bar baz . + 2 0 1 2 : foo bar baz . +# 3 0 2 3 5 : bar foo baz quux . +--- +name: arrays-9b +description: + Check that we can concatenate parameters too +stdin: + unset foo; foo=bar; foo+=baz; echo 1 $foo . + unset foo; typeset -i16 foo=10; foo+=20; echo 2 $foo . +expected-stdout: + 1 barbaz . + 2 16#a20 . +--- +name: arrassign-basic +description: + Check basic whitespace conserving properties of wdarrassign +stdin: + a=($(echo a b)) + b=($(echo "a b")) + c=("$(echo "a b")") + d=("$(echo a b)") + a+=($(echo c d)) + b+=($(echo "c d")) + c+=("$(echo "c d")") + d+=("$(echo c d)") + echo ".a:${a[0]}.${a[1]}.${a[2]}.${a[3]}:" + echo ".b:${b[0]}.${b[1]}.${b[2]}.${b[3]}:" + echo ".c:${c[0]}.${c[1]}.${c[2]}.${c[3]}:" + echo ".d:${d[0]}.${d[1]}.${d[2]}.${d[3]}:" +expected-stdout: + .a:a.b.c.d: + .b:a.b.c.d: + .c:a b.c d..: + .d:a b.c d..: +--- +name: arrassign-eol +description: + Commands after array assignments are not permitted +stdin: + foo=(a b) env +expected-exit: e != 0 +expected-stderr-pattern: + /syntax error: unexpected 'env'/ +--- +name: arrassign-fnc-none +description: + Check locality of array access inside a function +stdin: + function fn { + x+=(f) + echo ".fn:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + } + function rfn { + if [[ -n $BASH_VERSION ]]; then + y=() + else + set -A y + fi + y+=(f) + echo ".rfn:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + } + x=(m m) + y=(m m) + echo ".f0:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + fn + echo ".f1:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + fn + echo ".f2:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + echo ".rf0:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + rfn + echo ".rf1:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + rfn + echo ".rf2:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" +expected-stdout: + .f0:m.m..: + .fn:m.m.f.: + .f1:m.m.f.: + .fn:m.m.f.f: + .f2:m.m.f.f: + .rf0:m.m..: + .rfn:f...: + .rf1:f...: + .rfn:f...: + .rf2:f...: +--- +name: arrassign-fnc-local +description: + Check locality of array access inside a function + with the bash/mksh/ksh93 local/typeset keyword + (note: ksh93 has no local; typeset works only in FKSH) +stdin: + function fn { + typeset x + x+=(f) + echo ".fn:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + } + function rfn { + if [[ -n $BASH_VERSION ]]; then + y=() + else + set -A y + fi + typeset y + y+=(f) + echo ".rfn:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + } + function fnr { + typeset z + if [[ -n $BASH_VERSION ]]; then + z=() + else + set -A z + fi + z+=(f) + echo ".fnr:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" + } + x=(m m) + y=(m m) + z=(m m) + echo ".f0:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + fn + echo ".f1:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + fn + echo ".f2:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + echo ".rf0:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + rfn + echo ".rf1:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + rfn + echo ".rf2:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + echo ".f0r:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" + fnr + echo ".f1r:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" + fnr + echo ".f2r:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" +expected-stdout: + .f0:m.m..: + .fn:f...: + .f1:m.m..: + .fn:f...: + .f2:m.m..: + .rf0:m.m..: + .rfn:f...: + .rf1:...: + .rfn:f...: + .rf2:...: + .f0r:m.m..: + .fnr:f...: + .f1r:m.m..: + .fnr:f...: + .f2r:m.m..: +--- +name: arrassign-fnc-global +description: + Check locality of array access inside a function + with the bash4/mksh/yash/zsh typeset -g keyword +stdin: + function fn { + typeset -g x + x+=(f) + echo ".fn:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + } + function rfn { + set -A y + typeset -g y + y+=(f) + echo ".rfn:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + } + function fnr { + typeset -g z + set -A z + z+=(f) + echo ".fnr:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" + } + x=(m m) + y=(m m) + z=(m m) + echo ".f0:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + fn + echo ".f1:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + fn + echo ".f2:${x[0]}.${x[1]}.${x[2]}.${x[3]}:" + echo ".rf0:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + rfn + echo ".rf1:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + rfn + echo ".rf2:${y[0]}.${y[1]}.${y[2]}.${y[3]}:" + echo ".f0r:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" + fnr + echo ".f1r:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" + fnr + echo ".f2r:${z[0]}.${z[1]}.${z[2]}.${z[3]}:" +expected-stdout: + .f0:m.m..: + .fn:m.m.f.: + .f1:m.m.f.: + .fn:m.m.f.f: + .f2:m.m.f.f: + .rf0:m.m..: + .rfn:f...: + .rf1:f...: + .rfn:f...: + .rf2:f...: + .f0r:m.m..: + .fnr:f...: + .f1r:f...: + .fnr:f...: + .f2r:f...: +--- +name: strassign-fnc-none +description: + Check locality of string access inside a function +stdin: + function fn { + x+=f + echo ".fn:$x:" + } + function rfn { + y= + y+=f + echo ".rfn:$y:" + } + x=m + y=m + echo ".f0:$x:" + fn + echo ".f1:$x:" + fn + echo ".f2:$x:" + echo ".rf0:$y:" + rfn + echo ".rf1:$y:" + rfn + echo ".rf2:$y:" +expected-stdout: + .f0:m: + .fn:mf: + .f1:mf: + .fn:mff: + .f2:mff: + .rf0:m: + .rfn:f: + .rf1:f: + .rfn:f: + .rf2:f: +--- +name: strassign-fnc-local +description: + Check locality of string access inside a function + with the bash/mksh/ksh93 local/typeset keyword + (note: ksh93 has no local; typeset works only in FKSH) +stdin: + function fn { + typeset x + x+=f + echo ".fn:$x:" + } + function rfn { + y= + typeset y + y+=f + echo ".rfn:$y:" + } + function fnr { + typeset z + z= + z+=f + echo ".fnr:$z:" + } + x=m + y=m + z=m + echo ".f0:$x:" + fn + echo ".f1:$x:" + fn + echo ".f2:$x:" + echo ".rf0:$y:" + rfn + echo ".rf1:$y:" + rfn + echo ".rf2:$y:" + echo ".f0r:$z:" + fnr + echo ".f1r:$z:" + fnr + echo ".f2r:$z:" +expected-stdout: + .f0:m: + .fn:f: + .f1:m: + .fn:f: + .f2:m: + .rf0:m: + .rfn:f: + .rf1:: + .rfn:f: + .rf2:: + .f0r:m: + .fnr:f: + .f1r:m: + .fnr:f: + .f2r:m: +--- +name: strassign-fnc-global +description: + Check locality of string access inside a function + with the bash4/mksh/yash/zsh typeset -g keyword +stdin: + function fn { + typeset -g x + x+=f + echo ".fn:$x:" + } + function rfn { + y= + typeset -g y + y+=f + echo ".rfn:$y:" + } + function fnr { + typeset -g z + z= + z+=f + echo ".fnr:$z:" + } + x=m + y=m + z=m + echo ".f0:$x:" + fn + echo ".f1:$x:" + fn + echo ".f2:$x:" + echo ".rf0:$y:" + rfn + echo ".rf1:$y:" + rfn + echo ".rf2:$y:" + echo ".f0r:$z:" + fnr + echo ".f1r:$z:" + fnr + echo ".f2r:$z:" +expected-stdout: + .f0:m: + .fn:mf: + .f1:mf: + .fn:mff: + .f2:mff: + .rf0:m: + .rfn:f: + .rf1:f: + .rfn:f: + .rf2:f: + .f0r:m: + .fnr:f: + .f1r:f: + .fnr:f: + .f2r:f: +--- +name: unset-fnc-local-ksh +description: + Check that “unset” removes a previous “local” + (ksh93 syntax compatible version); apparently, + there are shells which fail this? +stdin: + function f { + echo f0: $x + typeset x + echo f1: $x + x=fa + echo f2: $x + unset x + echo f3: $x + x=fb + echo f4: $x + } + x=o + echo before: $x + f + echo after: $x +expected-stdout: + before: o + f0: o + f1: + f2: fa + f3: o + f4: fb + after: fb +--- +name: unset-fnc-local-sh +description: + Check that “unset” removes a previous “local” + (Debian Policy §10.4 sh version); apparently, + there are shells which fail this? +stdin: + f() { + echo f0: $x + local x + echo f1: $x + x=fa + echo f2: $x + unset x + echo f3: $x + x=fb + echo f4: $x + } + x=o + echo before: $x + f + echo after: $x +expected-stdout: + before: o + f0: o + f1: + f2: fa + f3: o + f4: fb + after: fb +--- +name: varexpand-substr-1 +description: + Check if bash-style substring expansion works + when using positive numerics +stdin: + x=abcdefghi + typeset -i y=123456789 + typeset -i 16 z=123456789 # 16#75bcd15 + echo a t${x:2:2} ${y:2:3} ${z:2:3} a + echo b ${x::3} ${y::3} ${z::3} b + echo c ${x:2:} ${y:2:} ${z:2:} c + echo d ${x:2} ${y:2} ${z:2} d + echo e ${x:2:6} ${y:2:6} ${z:2:7} e + echo f ${x:2:7} ${y:2:7} ${z:2:8} f + echo g ${x:2:8} ${y:2:8} ${z:2:9} g +expected-stdout: + a tcd 345 #75 a + b abc 123 16# b + c c + d cdefghi 3456789 #75bcd15 d + e cdefgh 345678 #75bcd1 e + f cdefghi 3456789 #75bcd15 f + g cdefghi 3456789 #75bcd15 g +--- +name: varexpand-substr-2 +description: + Check if bash-style substring expansion works + when using negative numerics or expressions +stdin: + x=abcdefghi + typeset -i y=123456789 + typeset -i 16 z=123456789 # 16#75bcd15 + n=2 + echo a ${x:$n:3} ${y:$n:3} ${z:$n:3} a + echo b ${x:(n):3} ${y:(n):3} ${z:(n):3} b + echo c ${x:(-2):1} ${y:(-2):1} ${z:(-2):1} c + echo d t${x: n:2} ${y: n:3} ${z: n:3} d +expected-stdout: + a cde 345 #75 a + b cde 345 #75 b + c h 8 1 c + d tcd 345 #75 d +--- +name: varexpand-substr-3 +description: + Check that some things that work in bash fail. + This is by design. Oh and vice versa, nowadays. +stdin: + export x=abcdefghi n=2 + "$__progname" -c 'echo v${x:(n)}x' + "$__progname" -c 'echo w${x: n}x' + "$__progname" -c 'echo x${x:n}x' + "$__progname" -c 'echo y${x:}x' + "$__progname" -c 'echo z${x}x' + # next fails only in bash + "$__progname" -c 'x=abcdef;y=123;echo ${x:${y:2:1}:2}' >/dev/null 2>&1; echo $? +expected-stdout: + vcdefghix + wcdefghix + zabcdefghix + 0 +expected-stderr-pattern: + /x:n.*bad substitution.*\n.*bad substitution/ +--- +name: varexpand-substr-4 +description: + Check corner cases for substring expansion +stdin: + x=abcdefghi + integer y=2 + echo a ${x:(y == 1 ? 2 : 3):4} a +expected-stdout: + a defg a +--- +name: varexpand-substr-5A +description: + Check that substring expansions work on characters +stdin: + set +U + x=mäh + echo a ${x::1} ${x: -1} a + echo b ${x::3} ${x: -3} b + echo c ${x:1:2} ${x: -3:2} c + echo d ${#x} d +expected-stdout: + a m h a + b mä äh b + c ä ä c + d 4 d +--- +name: varexpand-substr-5W +description: + Check that substring expansions work on characters +stdin: + set -U + x=mäh + echo a ${x::1} ${x: -1} a + echo b ${x::2} ${x: -2} b + echo c ${x:1:1} ${x: -2:1} c + echo d ${#x} d +expected-stdout: + a m h a + b mä äh b + c ä ä c + d 3 d +--- +name: varexpand-substr-6 +description: + Check that string substitution works correctly +stdin: + foo=1 + bar=2 + baz=qwertyuiop + echo a ${baz: foo: bar} + echo b ${baz: foo: $bar} + echo c ${baz: $foo: bar} + echo d ${baz: $foo: $bar} +expected-stdout: + a we + b we + c we + d we +--- +name: varexpand-special-hash +description: + Check special ${var@x} expansion for x=hash +category: !shell:ebcdic-yes +stdin: + typeset -i8 foo=10 + bar=baz + unset baz + print ${foo@#} ${bar@#} ${baz@#} . +expected-stdout: + 9B15FBFB CFBDD32B 00000000 . +--- +name: varexpand-special-hash-ebcdic +description: + Check special ${var@x} expansion for x=hash +category: !shell:ebcdic-no +stdin: + typeset -i8 foo=10 + bar=baz + unset baz + print ${foo@#} ${bar@#} ${baz@#} . +expected-stdout: + 016AE33D 9769C4AF 00000000 . +--- +name: varexpand-special-quote +description: + Check special ${var@Q} expansion for quoted strings +category: !shell:faux-ebcdic +stdin: + set +U + i=x + j=a\ b + k=$'c + d\xA0''e€f' + print -r -- "" + s="u=${i@Q} v=${j@Q} w=${k@Q}" + print -r -- "s=\"$s\"" + eval "$s" + typeset -p u v w +expected-stdout: + + s="u=x v='a b' w=$'c\nd\240e\u20ACf'" + typeset u=x + typeset v='a b' + typeset w=$'c\nd\240e\u20ACf' +--- +name: varexpand-special-quote-faux-EBCDIC +description: + Check special ${var@Q} expansion for quoted strings +category: shell:faux-ebcdic +stdin: + set +U + i=x + j=a\ b + k=$'c + d\xA0''e€f' + print -r -- "" + s="u=${i@Q} v=${j@Q} w=${k@Q}" + print -r -- "s=\"$s\"" + eval "$s" + typeset -p u v w +expected-stdout: + + s="u=x v='a b' w=$'c\nd�e\u20ACf'" + typeset u=x + typeset v='a b' + typeset w=$'c\nd�e\u20ACf' +--- +name: varexpand-null-1 +description: + Ensure empty strings expand emptily +stdin: + print s ${a} . ${b} S + print t ${a#?} . ${b%?} T + print r ${a=} . ${b/c/d} R + print q + print s "${a}" . "${b}" S + print t "${a#?}" . "${b%?}" T + print r "${a=}" . "${b/c/d}" R +expected-stdout: + s . S + t . T + r . R + q + s . S + t . T + r . R +--- +name: varexpand-null-2 +description: + Ensure empty strings, when quoted, are expanded as empty strings +stdin: + print '#!'"$__progname"'\nfor x in "$@"; do print -nr -- "<$x> "; done' >pfs + chmod +x pfs + ./pfs 1 "${a}" 2 "${a#?}" + "${b%?}" 3 "${a=}" + "${b/c/d}" + echo . +expected-stdout: + <1> <> <2> <> <+> <> <3> <> <+> <> . +--- +name: varexpand-null-3 +description: + Ensure concatenating behaviour matches other shells +stdin: + showargs() { for s_arg in "$@"; do echo -n "<$s_arg> "; done; echo .; } + showargs 0 ""$@ + x=; showargs 1 "$x"$@ + set A; showargs 2 "${@:+}" + n() { echo "$#"; } + unset e + set -- a b + n """$@" + n "$@" + n "$@""" + n "$e""$@" + n "$@" + n "$@""$e" + set -- + n """$@" + n "$@" + n "$@""" + n "$e""$@" + n "$@" + n "$@""$e" +expected-stdout: + <0> <> . + <1> <> . + <2> <> . + 2 + 2 + 2 + 2 + 2 + 2 + 1 + 0 + 1 + 1 + 0 + 1 +--- +name: varexpand-funny-chars +description: + Check some characters + XXX \uEF80 is asymmetric, possibly buggy so we don’t check this +stdin: + x=$'<\x00>'; typeset -p x + x=$'<\x01>'; typeset -p x + x=$'<\u0000>'; typeset -p x + x=$'<\u0001>'; typeset -p x +expected-stdout: + typeset x='<' + typeset x=$'<\001>' + typeset x='<' + typeset x=$'<\001>' +--- +name: print-funny-chars +description: + Check print builtin's capability to output designated characters +stdin: + { + print '<\0144\0344\xDB\u00DB\u20AC\uDB\x40>' + print '<\x00>' + print '<\x01>' + print '<\u0000>' + print '<\u0001>' + } | { + # integer-base-one-3Ar + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z5 hv=2147483647 + dasc= + if read -arN -1 line; then + typeset -i1 line + i=0 + while (( i < ${#line[*]} )); do + hv=${line[i++]} + if (( (pos & 15) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + print -n "${hv#16#} " + if (( (hv < 32) || (hv > 126) )); then + dasc=$dasc. + else + dasc=$dasc${line[i-1]#1#} + fi + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + fi + while (( pos & 15 )); do + print -n ' ' + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 3C 64 E4 DB C3 9B E2 82 - AC C3 9B 40 3E 0A 3C 00 |.<.| + 00000010 3E 0A 3C 01 3E 0A 3C 00 - 3E 0A 3C 01 3E 0A |>.<.>.<.>.<.>.| +--- +name: print-bksl-c +description: + Check print builtin's \c escape +stdin: + print '\ca'; print b +expected-stdout: + ab +--- +name: print-cr +description: + Check that CR+LF is not collapsed into LF as some MSYS shells wrongly do +stdin: + echo '#!'"$__progname" >foo + cat >>foo <<-'EOF' + print -n -- '220-blau.mirbsd.org ESMTP ready at Thu, 25 Jul 2013 15:57:57 GMT\r\n220->> Bitte keine Werbung einwerfen! <<\r\r\n220 Who do you wanna pretend to be today' + print \? + EOF + chmod +x foo + echo "[$(./foo)]" + ./foo | while IFS= read -r line; do + print -r -- "{$line}" + done +expected-stdout: + [220-blau.mirbsd.org ESMTP ready at Thu, 25 Jul 2013 15:57:57 GMT + 220->> Bitte keine Werbung einwerfen! << + 220 Who do you wanna pretend to be today? ] + {220-blau.mirbsd.org ESMTP ready at Thu, 25 Jul 2013 15:57:57 GMT } + {220->> Bitte keine Werbung einwerfen! << } + {220 Who do you wanna pretend to be today? } +--- +name: print-crlf +description: + Check that CR+LF is shown and read as-is +category: shell:textmode-no +stdin: + cat >foo <<-'EOF' + x='bar + ' # + echo .${#x} # + if test x"$KSH_VERSION" = x""; then # + printf '<%s>' "$x" # + else # + print -nr -- "<$x>" # + fi # + EOF + echo "[$("$__progname" foo)]" + "$__progname" foo | while IFS= read -r line; do + print -r -- "{$line}" + done +expected-stdout: + [.5 + ] + {.5} + {foo <<-'EOF' + x='bar + ' # + echo .${#x} # + if test x"$KSH_VERSION" = x""; then # + printf '<%s>' "$x" # + else # + print -nr -- "<$x>" # + fi # + EOF + echo "[$("$__progname" foo)]" + "$__progname" foo | while IFS= read -r line; do + print -r -- "{$line}" + done +expected-stdout: + [.4 + ] + {.4} + {foo <<-'EOF' + x='bar + ' # + echo .${#x} # + if test x"$KSH_VERSION" = x""; then # + printf '<%s>' "$x" # + else # + print -nr -- "<$x>" # + fi # + EOF + echo "[$("$__progname" foo)]" + "$__progname" foo | while IFS= read -r line; do + print -r -- "{$line}" + done +expected-stdout: + [.4 + ] + {.4} + {') + print $(($(print '<\0>' | wc -c))) $(($(print "$x" | wc -c))) \ + ${#x} "$x" '<\0>' +expected-stdout-pattern: + /^4 3 2 <> <\0>$/ +--- +name: print-array +description: + Check that print -A works as expected +stdin: + print -An 0x20AC 0xC3 0xBC 8#101 + set -U + print -A 0x20AC 0xC3 0xBC 8#102 +expected-stdout: + �üA€Ã¼B +--- +name: print-escapes +description: + Check backslash expansion by the print builtin +stdin: + print '\ \!\"\#\$\%\&'\\\''\(\)\*\+\,\-\.\/\0\1\2\3\4\5\6\7\8' \ + '\9\:\;\<\=\>\?\@\A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T' \ + '\U\V\W\X\Y\Z\[\\\]\^\_\`\a\b \d\e\f\g\h\i\j\k\l\m\n\o\p' \ + '\q\r\s\t\u\v\w\x\y\z\{\|\}\~' '\u20acd' '\U20acd' '\x123' \ + '\0x' '\0123' '\01234' | { + # integer-base-one-3As + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z5 hv=2147483647 + typeset -i1 wc=0x0A + dasc= + nl=${wc#1#} + while IFS= read -r line; do + line=$line$nl + while [[ -n $line ]]; do + hv=1#${line::1} + if (( (pos & 15) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + print -n "${hv#16#} " + if (( (hv < 32) || (hv > 126) )); then + dasc=$dasc. + else + dasc=$dasc${line::1} + fi + (( (pos++ & 15) == 7 )) && print -n -- '- ' + line=${line:1} + done + done + while (( pos & 15 )); do + print -n ' ' + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 5C 20 5C 21 5C 22 5C 23 - 5C 24 5C 25 5C 26 5C 27 |\ \!\"\#\$\%\&\'| + 00000010 5C 28 5C 29 5C 2A 5C 2B - 5C 2C 5C 2D 5C 2E 5C 2F |\(\)\*\+\,\-\.\/| + 00000020 5C 31 5C 32 5C 33 5C 34 - 5C 35 5C 36 5C 37 5C 38 |\1\2\3\4\5\6\7\8| + 00000030 20 5C 39 5C 3A 5C 3B 5C - 3C 5C 3D 5C 3E 5C 3F 5C | \9\:\;\<\=\>\?\| + 00000040 40 5C 41 5C 42 5C 43 5C - 44 1B 5C 46 5C 47 5C 48 |@\A\B\C\D.\F\G\H| + 00000050 5C 49 5C 4A 5C 4B 5C 4C - 5C 4D 5C 4E 5C 4F 5C 50 |\I\J\K\L\M\N\O\P| + 00000060 5C 51 5C 52 5C 53 5C 54 - 20 5C 55 5C 56 5C 57 5C |\Q\R\S\T \U\V\W\| + 00000070 58 5C 59 5C 5A 5C 5B 5C - 5C 5D 5C 5E 5C 5F 5C 60 |X\Y\Z\[\\]\^\_\`| + 00000080 07 08 20 20 5C 64 1B 0C - 5C 67 5C 68 5C 69 5C 6A |.. \d..\g\h\i\j| + 00000090 5C 6B 5C 6C 5C 6D 0A 5C - 6F 5C 70 20 5C 71 0D 5C |\k\l\m.\o\p \q.\| + 000000A0 73 09 5C 75 0B 5C 77 5C - 78 5C 79 5C 7A 5C 7B 5C |s.\u.\w\x\y\z\{\| + 000000B0 7C 5C 7D 5C 7E 20 E2 82 - AC 64 20 EF BF BD 20 12 ||\}\~ ...d ... .| + 000000C0 33 20 78 20 53 20 53 34 - 0A |3 x S S4.| +--- +name: dollar-doublequoted-strings +description: + Check that a $ preceding "…" is ignored +stdin: + echo $"Localise me!" + cat <<<$"Me too!" + V=X + aol=aol + cat <<-$"aol" + I do not take a $V for a V! + aol +expected-stdout: + Localise me! + Me too! + I do not take a $V for a V! +--- +name: dollar-quoted-strings +description: + Check backslash expansion by $'…' strings +stdin: + print '#!'"$__progname"'\nfor x in "$@"; do print -r -- "$x"; done' >pfn + chmod +x pfn + ./pfn $'\ \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/ \1\2\3\4\5\6' \ + $'a\0b' $'a\01b' $'\7\8\9\:\;\<\=\>\?\@\A\B\C\D\E\F\G\H\I' \ + $'\J\K\L\M\N\O\P\Q\R\S\T\U1\V\W\X\Y\Z\[\\\]\^\_\`\a\b\d\e' \ + $'\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u1\v\w\x1\y\z\{\|\}\~ $x' \ + $'\u20acd' $'\U20acd' $'\x123' $'fn\x0rd' $'\0234' $'\234' \ + $'\2345' $'\ca' $'\c!' $'\c?' $'\c…' $'a\ + b' | { + # integer-base-one-3As + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z5 hv=2147483647 + typeset -i1 wc=0x0A + dasc= + nl=${wc#1#} + while IFS= read -r line; do + line=$line$nl + while [[ -n $line ]]; do + hv=1#${line::1} + if (( (pos & 15) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + print -n "${hv#16#} " + if (( (hv < 32) || (hv > 126) )); then + dasc=$dasc. + else + dasc=$dasc${line::1} + fi + (( (pos++ & 15) == 7 )) && print -n -- '- ' + line=${line:1} + done + done + while (( pos & 15 )); do + print -n ' ' + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 20 21 22 23 24 25 26 27 - 28 29 2A 2B 2C 2D 2E 2F | !"#$%&'()*+,-./| + 00000010 20 01 02 03 04 05 06 0A - 61 0A 61 01 62 0A 07 38 | .......a.a.b..8| + 00000020 39 3A 3B 3C 3D 3E 3F 40 - 41 42 43 44 1B 46 47 48 |9:;<=>?@ABCD.FGH| + 00000030 49 0A 4A 4B 4C 4D 4E 4F - 50 51 52 53 54 01 56 57 |I.JKLMNOPQRST.VW| + 00000040 58 59 5A 5B 5C 5D 5E 5F - 60 07 08 64 1B 0A 0C 67 |XYZ[\]^_`..d...g| + 00000050 68 69 6A 6B 6C 6D 0A 6F - 70 71 0D 73 09 01 0B 77 |hijklm.opq.s...w| + 00000060 01 79 7A 7B 7C 7D 7E 20 - 24 78 0A E2 82 AC 64 0A |.yz{|}~ $x....d.| + 00000070 EF BF BD 0A C4 A3 0A 66 - 6E 0A 13 34 0A 9C 0A 9C |.......fn..4....| + 00000080 35 0A 01 0A 01 0A 7F 0A - 82 80 A6 0A 61 0A 62 0A |5...........a.b.| +--- +name: dollar-quotes-in-heredocs-strings +description: + They are, however, not parsed in here documents, here strings + (outside of string delimiters) or regular strings, but in + parameter substitutions. +stdin: + cat <dotfile + (exit 42) + . ./dotfile + echo 1 $? . +expected-stdout: + 1 0 . +--- +name: alias-function-no-conflict +description: + make aliases not conflict with function definitions +stdin: + # POSIX function can be defined, but alias overrides it + alias foo='echo bar' + foo + foo() { + echo baz + } + foo + unset -f foo + foo 2>/dev/null || echo rab + # alias overrides ksh function + alias korn='echo bar' + korn + function korn { + echo baz + } + korn + # alias temporarily overrides POSIX function + bla() { + echo bfn + } + bla + alias bla='echo bal' + bla + unalias bla + bla +expected-stdout: + bar + bar + bar + bar + bar + bfn + bal + bfn +--- +name: bash-function-parens +description: + ensure the keyword function is ignored when preceding + POSIX style function declarations (bashism) +stdin: + mk() { + echo '#!'"$__progname" + echo "$1 {" + echo ' echo "bar='\''$0'\'\" + echo '}' + print -r -- "${2:-foo}" + } + mk 'function foo' >f-korn + mk 'foo ()' >f-dash + mk 'function foo ()' >f-bash + print '#!'"$__progname"'\nprint -r -- "${0%/f-argh}"' >f-argh + chmod +x f-* + u=$(./f-argh) + x="korn: $(./f-korn)"; echo "${x/@("$u")/.}" + x="dash: $(./f-dash)"; echo "${x/@("$u")/.}" + x="bash: $(./f-bash)"; echo "${x/@("$u")/.}" +expected-stdout: + korn: bar='foo' + dash: bar='./f-dash' + bash: bar='./f-bash' +--- +name: integer-base-one-1 +description: + check if the use of fake integer base 1 works +stdin: + set -U + typeset -Uui16 i0=1#� i1=1#€ + typeset -i1 o0a=64 + typeset -i1 o1a=0x263A + typeset -Uui1 o0b=0x7E + typeset -Uui1 o1b=0xFDD0 + integer px=0xCAFE 'p0=1# ' p1=1#… pl=1#f + echo "in <$i0> <$i1>" + echo "out <${o0a#1#}|${o0b#1#}> <${o1a#1#}|${o1b#1#}>" + typeset -Uui1 i0 i1 + echo "pass <$px> <$p0> <$p1> <$pl> <${i0#1#}|${i1#1#}>" + typeset -Uui16 tv1=1#~ tv2=1# tv3=1#� tv4=1#� tv5=1#� tv6=1#� tv7=1#  tv8=1#€ + echo "specX <${tv1#16#}> <${tv2#16#}> <${tv3#16#}> <${tv4#16#}> <${tv5#16#}> <${tv6#16#}> <${tv7#16#}> <${tv8#16#}>" + typeset -i1 tv1 tv2 tv3 tv4 tv5 tv6 tv7 tv8 + echo "specW <${tv1#1#}> <${tv2#1#}> <${tv3#1#}> <${tv4#1#}> <${tv5#1#}> <${tv6#1#}> <${tv7#1#}> <${tv8#1#}>" + typeset -i1 xs1=0xEF7F xs2=0xEF80 xs3=0xFDD0 + echo "specU <${xs1#1#}> <${xs2#1#}> <${xs3#1#}>" +expected-stdout: + in <16#EFEF> <16#20AC> + out <@|~> <☺|﷐> + pass <16#cafe> <1# > <1#…> <1#f> <�|€> + specX <7E> <7F> <80> + specW <~> <> <�> <�> <�> <�> < > <€> + specU <> <�> <﷐> +--- +name: integer-base-one-2a +description: + check if the use of fake integer base 1 stops at correct characters +stdin: + set -U + integer x=1#foo + echo /$x/ +expected-stderr-pattern: + /1#foo: unexpected 'oo'/ +expected-exit: e != 0 +--- +name: integer-base-one-2b +description: + check if the use of fake integer base 1 stops at correct characters +stdin: + set -U + integer x=1#�� + echo /$x/ +expected-stderr-pattern: + /1#��: unexpected '�'/ +expected-exit: e != 0 +--- +name: integer-base-one-2c1 +description: + check if the use of fake integer base 1 stops at correct characters +stdin: + set -U + integer x=1#… + echo /$x/ +expected-stdout: + /1#…/ +--- +name: integer-base-one-2c2 +description: + check if the use of fake integer base 1 stops at correct characters +stdin: + set +U + integer x=1#… + echo /$x/ +expected-stderr-pattern: + /1#…: unexpected '�'/ +expected-exit: e != 0 +--- +name: integer-base-one-2d1 +description: + check if the use of fake integer base 1 handles octets okay +stdin: + set -U + typeset -i16 x=1#� + echo /$x/ # invalid utf-8 +expected-stdout: + /16#efff/ +--- +name: integer-base-one-2d2 +description: + check if the use of fake integer base 1 handles octets +stdin: + set -U + typeset -i16 x=1#� + echo /$x/ # invalid 2-byte +expected-stdout: + /16#efc2/ +--- +name: integer-base-one-2d3 +description: + check if the use of fake integer base 1 handles octets +stdin: + set -U + typeset -i16 x=1#� + echo /$x/ # invalid 2-byte +expected-stdout: + /16#efef/ +--- +name: integer-base-one-2d4 +description: + check if the use of fake integer base 1 stops at invalid input +stdin: + set -U + typeset -i16 x=1#�� + echo /$x/ # invalid 3-byte +expected-stderr-pattern: + /1#��: unexpected '�'/ +expected-exit: e != 0 +--- +name: integer-base-one-2d5 +description: + check if the use of fake integer base 1 stops at invalid input +stdin: + set -U + typeset -i16 x=1#�� + echo /$x/ # non-minimalistic +expected-stderr-pattern: + /1#��: unexpected '�'/ +expected-exit: e != 0 +--- +name: integer-base-one-2d6 +description: + check if the use of fake integer base 1 stops at invalid input +stdin: + set -U + typeset -i16 x=1#��� + echo /$x/ # non-minimalistic +expected-stderr-pattern: + /1#���: unexpected '�'/ +expected-exit: e != 0 +--- +name: integer-base-one-3As +description: + some sample code for hexdumping + not NUL safe; input lines must be NL terminated +stdin: + { + print 'Hello, World!\\\nこんにちは!' + typeset -Uui16 i=0x100 + # change that to 0xFF once we can handle embedded + # NUL characters in strings / here documents + while (( i++ < 0x1FF )); do + print -n "\x${i#16#1}" + done + print '\0z' + } | { + # integer-base-one-3As + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z5 hv=2147483647 + typeset -i1 wc=0x0A + dasc= + nl=${wc#1#} + while IFS= read -r line; do + line=$line$nl + while [[ -n $line ]]; do + hv=1#${line::1} + if (( (pos & 15) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + print -n "${hv#16#} " + if (( (hv < 32) || (hv > 126) )); then + dasc=$dasc. + else + dasc=$dasc${line::1} + fi + (( (pos++ & 15) == 7 )) && print -n -- '- ' + line=${line:1} + done + done + while (( pos & 15 )); do + print -n ' ' + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 48 65 6C 6C 6F 2C 20 57 - 6F 72 6C 64 21 5C 0A E3 |Hello, World!\..| + 00000010 81 93 E3 82 93 E3 81 AB - E3 81 A1 E3 81 AF EF BC |................| + 00000020 81 0A 01 02 03 04 05 06 - 07 08 09 0A 0B 0C 0D 0E |................| + 00000030 0F 10 11 12 13 14 15 16 - 17 18 19 1A 1B 1C 1D 1E |................| + 00000040 1F 20 21 22 23 24 25 26 - 27 28 29 2A 2B 2C 2D 2E |. !"#$%&'()*+,-.| + 00000050 2F 30 31 32 33 34 35 36 - 37 38 39 3A 3B 3C 3D 3E |/0123456789:;<=>| + 00000060 3F 40 41 42 43 44 45 46 - 47 48 49 4A 4B 4C 4D 4E |?@ABCDEFGHIJKLMN| + 00000070 4F 50 51 52 53 54 55 56 - 57 58 59 5A 5B 5C 5D 5E |OPQRSTUVWXYZ[\]^| + 00000080 5F 60 61 62 63 64 65 66 - 67 68 69 6A 6B 6C 6D 6E |_`abcdefghijklmn| + 00000090 6F 70 71 72 73 74 75 76 - 77 78 79 7A 7B 7C 7D 7E |opqrstuvwxyz{|}~| + 000000A0 7F 80 81 82 83 84 85 86 - 87 88 89 8A 8B 8C 8D 8E |................| + 000000B0 8F 90 91 92 93 94 95 96 - 97 98 99 9A 9B 9C 9D 9E |................| + 000000C0 9F A0 A1 A2 A3 A4 A5 A6 - A7 A8 A9 AA AB AC AD AE |................| + 000000D0 AF B0 B1 B2 B3 B4 B5 B6 - B7 B8 B9 BA BB BC BD BE |................| + 000000E0 BF C0 C1 C2 C3 C4 C5 C6 - C7 C8 C9 CA CB CC CD CE |................| + 000000F0 CF D0 D1 D2 D3 D4 D5 D6 - D7 D8 D9 DA DB DC DD DE |................| + 00000100 DF E0 E1 E2 E3 E4 E5 E6 - E7 E8 E9 EA EB EC ED EE |................| + 00000110 EF F0 F1 F2 F3 F4 F5 F6 - F7 F8 F9 FA FB FC FD FE |................| + 00000120 FF 7A 0A - |.z.| +--- +name: integer-base-one-3Ws +description: + some sample code for hexdumping Unicode + not NUL safe; input lines must be NL terminated +stdin: + set -U + { + print 'Hello, World!\\\nこんにちは!' + typeset -Uui16 i=0x100 + # change that to 0xFF once we can handle embedded + # NUL characters in strings / here documents + while (( i++ < 0x1FF )); do + print -n "\u${i#16#1}" + done + print + print \\xff # invalid utf-8 + print \\xc2 # invalid 2-byte + print \\xef\\xbf\\xc0 # invalid 3-byte + print \\xc0\\x80 # non-minimalistic + print \\xe0\\x80\\x80 # non-minimalistic + print '�￾￿' # end of range + print '\0z' # embedded NUL + } | { + # integer-base-one-3Ws + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z7 hv + typeset -i1 wc=0x0A + typeset -i lpos + dasc= + nl=${wc#1#} + while IFS= read -r line; do + line=$line$nl + lpos=0 + while (( lpos < ${#line} )); do + wc=1#${line:(lpos++):1} + if (( (wc < 32) || \ + ((wc > 126) && (wc < 160)) )); then + dch=. + elif (( (wc & 0xFF80) == 0xEF80 )); then + dch=� + else + dch=${wc#1#} + fi + if (( (pos & 7) == 7 )); then + dasc=$dasc$dch + dch= + elif (( (pos & 7) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + let hv=wc + print -n "${hv#16#} " + (( (pos++ & 7) == 3 )) && \ + print -n -- '- ' + dasc=$dasc$dch + done + done + while (( pos & 7 )); do + print -n ' ' + (( (pos++ & 7) == 3 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 0048 0065 006C 006C - 006F 002C 0020 0057 |Hello, W| + 00000008 006F 0072 006C 0064 - 0021 005C 000A 3053 |orld!\.こ| + 00000010 3093 306B 3061 306F - FF01 000A 0001 0002 |んにちは!...| + 00000018 0003 0004 0005 0006 - 0007 0008 0009 000A |........| + 00000020 000B 000C 000D 000E - 000F 0010 0011 0012 |........| + 00000028 0013 0014 0015 0016 - 0017 0018 0019 001A |........| + 00000030 001B 001C 001D 001E - 001F 0020 0021 0022 |..... !"| + 00000038 0023 0024 0025 0026 - 0027 0028 0029 002A |#$%&'()*| + 00000040 002B 002C 002D 002E - 002F 0030 0031 0032 |+,-./012| + 00000048 0033 0034 0035 0036 - 0037 0038 0039 003A |3456789:| + 00000050 003B 003C 003D 003E - 003F 0040 0041 0042 |;<=>?@AB| + 00000058 0043 0044 0045 0046 - 0047 0048 0049 004A |CDEFGHIJ| + 00000060 004B 004C 004D 004E - 004F 0050 0051 0052 |KLMNOPQR| + 00000068 0053 0054 0055 0056 - 0057 0058 0059 005A |STUVWXYZ| + 00000070 005B 005C 005D 005E - 005F 0060 0061 0062 |[\]^_`ab| + 00000078 0063 0064 0065 0066 - 0067 0068 0069 006A |cdefghij| + 00000080 006B 006C 006D 006E - 006F 0070 0071 0072 |klmnopqr| + 00000088 0073 0074 0075 0076 - 0077 0078 0079 007A |stuvwxyz| + 00000090 007B 007C 007D 007E - 007F 0080 0081 0082 |{|}~....| + 00000098 0083 0084 0085 0086 - 0087 0088 0089 008A |........| + 000000A0 008B 008C 008D 008E - 008F 0090 0091 0092 |........| + 000000A8 0093 0094 0095 0096 - 0097 0098 0099 009A |........| + 000000B0 009B 009C 009D 009E - 009F 00A0 00A1 00A2 |..... ¡¢| + 000000B8 00A3 00A4 00A5 00A6 - 00A7 00A8 00A9 00AA |£¤¥¦§¨©ª| + 000000C0 00AB 00AC 00AD 00AE - 00AF 00B0 00B1 00B2 |«¬­®¯°±²| + 000000C8 00B3 00B4 00B5 00B6 - 00B7 00B8 00B9 00BA |³´µ¶·¸¹º| + 000000D0 00BB 00BC 00BD 00BE - 00BF 00C0 00C1 00C2 |»¼½¾¿ÀÁÂ| + 000000D8 00C3 00C4 00C5 00C6 - 00C7 00C8 00C9 00CA |ÃÄÅÆÇÈÉÊ| + 000000E0 00CB 00CC 00CD 00CE - 00CF 00D0 00D1 00D2 |ËÌÍÎÏÐÑÒ| + 000000E8 00D3 00D4 00D5 00D6 - 00D7 00D8 00D9 00DA |ÓÔÕÖ×ØÙÚ| + 000000F0 00DB 00DC 00DD 00DE - 00DF 00E0 00E1 00E2 |ÛÜÝÞßàáâ| + 000000F8 00E3 00E4 00E5 00E6 - 00E7 00E8 00E9 00EA |ãäåæçèéê| + 00000100 00EB 00EC 00ED 00EE - 00EF 00F0 00F1 00F2 |ëìíîïðñò| + 00000108 00F3 00F4 00F5 00F6 - 00F7 00F8 00F9 00FA |óôõö÷øùú| + 00000110 00FB 00FC 00FD 00FE - 00FF 000A EFFF 000A |ûüýþÿ.�.| + 00000118 EFC2 000A EFEF EFBF - EFC0 000A EFC0 EF80 |�.���.��| + 00000120 000A EFE0 EF80 EF80 - 000A FFFD EFEF EFBF |.���.���| + 00000128 EFBE EFEF EFBF EFBF - 000A 007A 000A |����.z.| +--- +name: integer-base-one-3Ar +description: + some sample code for hexdumping; NUL and binary safe +stdin: + { + print 'Hello, World!\\\nこんにちは!' + typeset -Uui16 i=0x100 + # change that to 0xFF once we can handle embedded + # NUL characters in strings / here documents + while (( i++ < 0x1FF )); do + print -n "\x${i#16#1}" + done + print '\0z' + } | { + # integer-base-one-3Ar + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z5 hv=2147483647 + dasc= + if read -arN -1 line; then + typeset -i1 line + i=0 + while (( i < ${#line[*]} )); do + hv=${line[i++]} + if (( (pos & 15) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + print -n "${hv#16#} " + if (( (hv < 32) || (hv > 126) )); then + dasc=$dasc. + else + dasc=$dasc${line[i-1]#1#} + fi + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + fi + while (( pos & 15 )); do + print -n ' ' + (( (pos++ & 15) == 7 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 48 65 6C 6C 6F 2C 20 57 - 6F 72 6C 64 21 5C 0A E3 |Hello, World!\..| + 00000010 81 93 E3 82 93 E3 81 AB - E3 81 A1 E3 81 AF EF BC |................| + 00000020 81 0A 01 02 03 04 05 06 - 07 08 09 0A 0B 0C 0D 0E |................| + 00000030 0F 10 11 12 13 14 15 16 - 17 18 19 1A 1B 1C 1D 1E |................| + 00000040 1F 20 21 22 23 24 25 26 - 27 28 29 2A 2B 2C 2D 2E |. !"#$%&'()*+,-.| + 00000050 2F 30 31 32 33 34 35 36 - 37 38 39 3A 3B 3C 3D 3E |/0123456789:;<=>| + 00000060 3F 40 41 42 43 44 45 46 - 47 48 49 4A 4B 4C 4D 4E |?@ABCDEFGHIJKLMN| + 00000070 4F 50 51 52 53 54 55 56 - 57 58 59 5A 5B 5C 5D 5E |OPQRSTUVWXYZ[\]^| + 00000080 5F 60 61 62 63 64 65 66 - 67 68 69 6A 6B 6C 6D 6E |_`abcdefghijklmn| + 00000090 6F 70 71 72 73 74 75 76 - 77 78 79 7A 7B 7C 7D 7E |opqrstuvwxyz{|}~| + 000000A0 7F 80 81 82 83 84 85 86 - 87 88 89 8A 8B 8C 8D 8E |................| + 000000B0 8F 90 91 92 93 94 95 96 - 97 98 99 9A 9B 9C 9D 9E |................| + 000000C0 9F A0 A1 A2 A3 A4 A5 A6 - A7 A8 A9 AA AB AC AD AE |................| + 000000D0 AF B0 B1 B2 B3 B4 B5 B6 - B7 B8 B9 BA BB BC BD BE |................| + 000000E0 BF C0 C1 C2 C3 C4 C5 C6 - C7 C8 C9 CA CB CC CD CE |................| + 000000F0 CF D0 D1 D2 D3 D4 D5 D6 - D7 D8 D9 DA DB DC DD DE |................| + 00000100 DF E0 E1 E2 E3 E4 E5 E6 - E7 E8 E9 EA EB EC ED EE |................| + 00000110 EF F0 F1 F2 F3 F4 F5 F6 - F7 F8 F9 FA FB FC FD FE |................| + 00000120 FF 00 7A 0A - |..z.| +--- +name: integer-base-one-3Wr +description: + some sample code for hexdumping Unicode; NUL and binary safe +stdin: + set -U + { + print 'Hello, World!\\\nこんにちは!' + typeset -Uui16 i=0x100 + # change that to 0xFF once we can handle embedded + # NUL characters in strings / here documents + while (( i++ < 0x1FF )); do + print -n "\u${i#16#1}" + done + print + print \\xff # invalid utf-8 + print \\xc2 # invalid 2-byte + print \\xef\\xbf\\xc0 # invalid 3-byte + print \\xc0\\x80 # non-minimalistic + print \\xe0\\x80\\x80 # non-minimalistic + print '�￾￿' # end of range + print '\0z' # embedded NUL + } | { + # integer-base-one-3Wr + typeset -Uui16 -Z11 pos=0 + typeset -Uui16 -Z7 hv=2147483647 + dasc= + if read -arN -1 line; then + typeset -i1 line + i=0 + while (( i < ${#line[*]} )); do + hv=${line[i++]} + if (( (hv < 32) || \ + ((hv > 126) && (hv < 160)) )); then + dch=. + elif (( (hv & 0xFF80) == 0xEF80 )); then + dch=� + else + dch=${line[i-1]#1#} + fi + if (( (pos & 7) == 7 )); then + dasc=$dasc$dch + dch= + elif (( (pos & 7) == 0 )); then + (( pos )) && print -r -- "$dasc|" + print -n "${pos#16#} " + dasc=' |' + fi + print -n "${hv#16#} " + (( (pos++ & 7) == 3 )) && \ + print -n -- '- ' + dasc=$dasc$dch + done + fi + while (( pos & 7 )); do + print -n ' ' + (( (pos++ & 7) == 3 )) && print -n -- '- ' + done + (( hv == 2147483647 )) || print -r -- "$dasc|" + } +expected-stdout: + 00000000 0048 0065 006C 006C - 006F 002C 0020 0057 |Hello, W| + 00000008 006F 0072 006C 0064 - 0021 005C 000A 3053 |orld!\.こ| + 00000010 3093 306B 3061 306F - FF01 000A 0001 0002 |んにちは!...| + 00000018 0003 0004 0005 0006 - 0007 0008 0009 000A |........| + 00000020 000B 000C 000D 000E - 000F 0010 0011 0012 |........| + 00000028 0013 0014 0015 0016 - 0017 0018 0019 001A |........| + 00000030 001B 001C 001D 001E - 001F 0020 0021 0022 |..... !"| + 00000038 0023 0024 0025 0026 - 0027 0028 0029 002A |#$%&'()*| + 00000040 002B 002C 002D 002E - 002F 0030 0031 0032 |+,-./012| + 00000048 0033 0034 0035 0036 - 0037 0038 0039 003A |3456789:| + 00000050 003B 003C 003D 003E - 003F 0040 0041 0042 |;<=>?@AB| + 00000058 0043 0044 0045 0046 - 0047 0048 0049 004A |CDEFGHIJ| + 00000060 004B 004C 004D 004E - 004F 0050 0051 0052 |KLMNOPQR| + 00000068 0053 0054 0055 0056 - 0057 0058 0059 005A |STUVWXYZ| + 00000070 005B 005C 005D 005E - 005F 0060 0061 0062 |[\]^_`ab| + 00000078 0063 0064 0065 0066 - 0067 0068 0069 006A |cdefghij| + 00000080 006B 006C 006D 006E - 006F 0070 0071 0072 |klmnopqr| + 00000088 0073 0074 0075 0076 - 0077 0078 0079 007A |stuvwxyz| + 00000090 007B 007C 007D 007E - 007F 0080 0081 0082 |{|}~....| + 00000098 0083 0084 0085 0086 - 0087 0088 0089 008A |........| + 000000A0 008B 008C 008D 008E - 008F 0090 0091 0092 |........| + 000000A8 0093 0094 0095 0096 - 0097 0098 0099 009A |........| + 000000B0 009B 009C 009D 009E - 009F 00A0 00A1 00A2 |..... ¡¢| + 000000B8 00A3 00A4 00A5 00A6 - 00A7 00A8 00A9 00AA |£¤¥¦§¨©ª| + 000000C0 00AB 00AC 00AD 00AE - 00AF 00B0 00B1 00B2 |«¬­®¯°±²| + 000000C8 00B3 00B4 00B5 00B6 - 00B7 00B8 00B9 00BA |³´µ¶·¸¹º| + 000000D0 00BB 00BC 00BD 00BE - 00BF 00C0 00C1 00C2 |»¼½¾¿ÀÁÂ| + 000000D8 00C3 00C4 00C5 00C6 - 00C7 00C8 00C9 00CA |ÃÄÅÆÇÈÉÊ| + 000000E0 00CB 00CC 00CD 00CE - 00CF 00D0 00D1 00D2 |ËÌÍÎÏÐÑÒ| + 000000E8 00D3 00D4 00D5 00D6 - 00D7 00D8 00D9 00DA |ÓÔÕÖ×ØÙÚ| + 000000F0 00DB 00DC 00DD 00DE - 00DF 00E0 00E1 00E2 |ÛÜÝÞßàáâ| + 000000F8 00E3 00E4 00E5 00E6 - 00E7 00E8 00E9 00EA |ãäåæçèéê| + 00000100 00EB 00EC 00ED 00EE - 00EF 00F0 00F1 00F2 |ëìíîïðñò| + 00000108 00F3 00F4 00F5 00F6 - 00F7 00F8 00F9 00FA |óôõö÷øùú| + 00000110 00FB 00FC 00FD 00FE - 00FF 000A EFFF 000A |ûüýþÿ.�.| + 00000118 EFC2 000A EFEF EFBF - EFC0 000A EFC0 EF80 |�.���.��| + 00000120 000A EFE0 EF80 EF80 - 000A FFFD EFEF EFBF |.���.���| + 00000128 EFBE EFEF EFBF EFBF - 000A 0000 007A 000A |����..z.| +--- +name: integer-base-one-4 +description: + Check if ksh93-style base-one integers work +category: !smksh +stdin: + set -U + echo 1 $(('a')) + (echo 2f $(('aa'))) 2>&1 | sed "s/^[^']*'/2p '/" + echo 3 $(('…')) + x="'a'" + echo "4 <$x>" + echo 5 $(($x)) + echo 6 $((x)) +expected-stdout: + 1 97 + 2p 'aa': multi-character character constant + 3 8230 + 4 <'a'> + 5 97 + 6 97 +--- +name: integer-base-one-5A +description: + Check to see that we’re NUL and Unicode safe +category: !shell:ebcdic-yes +stdin: + set +U + print 'a\0b\xfdz' >x + read -a y x + read -a y x + read -a y a 2>b + echo =1= + cat a + echo =2= + cat b + echo =3= + d 2>&1 >c + echo =4= + cat c + echo =5= +expected-stdout: + =1= + o0. + =2= + e0. + =3= + e1. + =4= + o1. + =5= +--- +name: bashiop-1 +description: + Check if GNU bash-like I/O redirection works + Part 1: this is also supported by GNU bash +stdin: + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + threeout &>foo + echo === + cat foo +expected-stdout: + tri + === + ras + dwa +--- +name: bashiop-2a +description: + Check if GNU bash-like I/O redirection works + Part 2: this is *not* supported by GNU bash +stdin: + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + threeout 3&>foo + echo === + cat foo +expected-stdout: + ras + === + dwa + tri +--- +name: bashiop-2b +description: + Check if GNU bash-like I/O redirection works + Part 2: this is *not* supported by GNU bash +stdin: + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + threeout 3>foo &>&3 + echo === + cat foo +expected-stdout: + === + ras + dwa + tri +--- +name: bashiop-2c +description: + Check if GNU bash-like I/O redirection works + Part 2: this is supported by GNU bash 4 only +stdin: + echo mir >foo + set -o noclobber + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + threeout &>>foo + echo === + cat foo +expected-stdout: + tri + === + mir + ras + dwa +--- +name: bashiop-3a +description: + Check if GNU bash-like I/O redirection fails correctly + Part 1: this is also supported by GNU bash +stdin: + echo mir >foo + set -o noclobber + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + threeout &>foo + echo === + cat foo +expected-stdout: + === + mir +expected-stderr-pattern: /.*: can't (create|overwrite) .*/ +--- +name: bashiop-3b +description: + Check if GNU bash-like I/O redirection fails correctly + Part 2: this is *not* supported by GNU bash +stdin: + echo mir >foo + set -o noclobber + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + threeout &>|foo + echo === + cat foo +expected-stdout: + tri + === + ras + dwa +--- +name: bashiop-4 +description: + Check if GNU bash-like I/O redirection works + Part 4: this is also supported by GNU bash, + but failed in some mksh versions +stdin: + exec 3>&1 + function threeout { + echo ras + echo dwa >&2 + echo tri >&3 + } + function blubb { + [[ -e bar ]] && threeout "$bf" &>foo + } + blubb + echo -n >bar + blubb + echo === + cat foo +expected-stdout: + tri + === + ras + dwa +--- +name: bashiop-5 +description: + Check if GNU bash-like I/O redirection is only supported + in !POSIX !sh mode as it breaks existing scripts' syntax +stdin: + :>x; echo 1 "$("$__progname" -c 'echo foo>/dev/null&>x echo bar')" = "$(x; echo 2 "$("$__progname" -o posix -c 'echo foo>/dev/null&>x echo bar')" = "$(x; echo 3 "$("$__progname" -o sh -c 'echo foo>/dev/null&>x echo bar')" = "$(env; chmod +x env; PATH=.$PATHSEP$PATH + function k { + if [ x$FOO != xbar ]; then + echo 1 + return 1 + fi + x=$(env | grep FOO) + if [ "x$x" != "xFOO=bar" ]; then + echo 2 + return 1; + fi + FOO=foo + return 0 + } + b () { + if [ x$FOO != xbar ]; then + echo 3 + return 1 + fi + x=$(env | grep FOO) + if [ "x$x" != "xFOO=bar" ]; then + echo 4 + return 1; + fi + FOO=foo + return 0 + } + FOO=bar k + if [ $? != 0 ]; then + exit 1 + fi + if [ x$FOO != x ]; then + exit 1 + fi + FOO=bar b + if [ $? != 0 ]; then + exit 1 + fi + if [ x$FOO != xfoo ]; then + exit 1 + fi + FOO=barbar + FOO=bar k + if [ $? != 0 ]; then + exit 1 + fi + if [ x$FOO != xbarbar ]; then + exit 1 + fi + FOO=bar b + if [ $? != 0 ]; then + exit 1 + fi + if [ x$FOO != xfoo ]; then + exit 1 + fi +--- +name: fd-cloexec-1 +description: + Verify that file descriptors > 2 are private for Korn shells + AT&T ksh93 does this still, which means we must keep it as well + XXX fails on some old Perl installations +need-pass: no +stdin: + cat >cld <<-EOF + #!$__perlname + open(my \$fh, ">&", 9) or die "E: open \$!"; + syswrite(\$fh, "Fowl\\n", 5) or die "E: write \$!"; + EOF + chmod +x cld + exec 9>&1 + ./cld +expected-exit: e != 0 +expected-stderr-pattern: + /E: open / +--- +name: fd-cloexec-2 +description: + Verify that file descriptors > 2 are not private for POSIX shells + See Debian Bug #154540, Closes: #499139 + XXX fails on some old Perl installations +need-pass: no +stdin: + cat >cld <<-EOF + #!$__perlname + open(my \$fh, ">&", 9) or die "E: open \$!"; + syswrite(\$fh, "Fowl\\n", 5) or die "E: write \$!"; + EOF + chmod +x cld + test -n "$POSH_VERSION" || set -o posix + exec 9>&1 + ./cld +expected-stdout: + Fowl +--- +name: comsub-1a +description: + COMSUB are now parsed recursively, so this works + see also regression-6: matching parenthesēs bug + Fails on: pdksh bash2 bash3 zsh + Passes on: bash4 ksh93 mksh(20110313+) +stdin: + echo 1 $(case 1 in (1) echo yes;; (2) echo no;; esac) . + echo 2 $(case 1 in 1) echo yes;; 2) echo no;; esac) . + TEST=1234; echo 3 ${TEST: $(case 1 in (1) echo 1;; (*) echo 2;; esac)} . + TEST=5678; echo 4 ${TEST: $(case 1 in 1) echo 1;; *) echo 2;; esac)} . + a=($(case 1 in (1) echo 1;; (*) echo 2;; esac)); echo 5 ${a[0]} . + a=($(case 1 in 1) echo 1;; *) echo 2;; esac)); echo 6 ${a[0]} . +expected-stdout: + 1 yes . + 2 yes . + 3 234 . + 4 678 . + 5 1 . + 6 1 . +--- +name: comsub-1b +description: + COMSUB are now parsed recursively, so this works + Fails on: pdksh bash2 bash3 bash4 zsh + Passes on: ksh93 mksh(20110313+) +stdin: + echo 1 $(($(case 1 in (1) echo 1;; (*) echo 2;; esac)+10)) . + echo 2 $(($(case 1 in 1) echo 1;; *) echo 2;; esac)+20)) . + (( a = $(case 1 in (1) echo 1;; (*) echo 2;; esac) )); echo 3 $a . + (( a = $(case 1 in 1) echo 1;; *) echo 2;; esac) )); echo 4 $a . + a=($(($(case 1 in (1) echo 1;; (*) echo 2;; esac)+10))); echo 5 ${a[0]} . + a=($(($(case 1 in 1) echo 1;; *) echo 2;; esac)+20))); echo 6 ${a[0]} . +expected-stdout: + 1 11 . + 2 21 . + 3 1 . + 4 1 . + 5 11 . + 6 21 . +--- +name: comsub-2 +description: + RedHat BZ#496791 – another case of missing recursion + in parsing COMSUB expressions + Fails on: pdksh bash2 bash3¹ bash4¹ zsh + Passes on: ksh93 mksh(20110305+) + ① bash[34] seem to choke on comment ending with backslash-newline +stdin: + # a comment with " ' \ + x=$( + echo yes + # a comment with " ' \ + ) + echo $x +expected-stdout: + yes +--- +name: comsub-3 +description: + Extended test for COMSUB explaining why a recursive parser + is a must (a non-recursive parser cannot pass all three of + these test cases, especially the ‘#’ is difficult) +stdin: + print '#!'"$__progname"'\necho 1234' >id; chmod +x id; PATH=.$PATHSEP$PATH + echo $(typeset -i10 x=16#20; echo $x) + echo $(typeset -Uui16 x=16#$(id -u) + ) . + echo $(c=1; d=1 + typeset -Uui16 a=36#foo; c=2 + typeset -Uui16 b=36 #foo; d=2 + echo $a $b $c $d) +expected-stdout: + 32 + . + 16#4F68 16#24 2 1 +--- +name: comsub-4 +description: + Check the tree dump functions for !MKSH_SMALL functionality +category: !smksh +stdin: + x() { case $1 in u) echo x ;;& *) echo $1 ;; esac; } + typeset -f x +expected-stdout: + x() { + case $1 in + (u) + \echo x + ;| + (*) + \echo $1 + ;; + esac + } +--- +name: comsub-5 +description: + Check COMSUB works with aliases (does not expand them twice) + and reentrancy safety +stdin: + print '#!'"$__progname"'\nfor x in "$@"; do print -r -- "$x"; done' >pfn + chmod +x pfn + alias echo='echo a' + foo() { + echo moo + ./pfn "$(echo foo)" + } + ./pfn "$(echo b)" + typeset -f foo >x + cat x + foo + . ./x + typeset -f foo + foo +expected-stdout: + a b + foo() { + \echo a moo + ./pfn "$(\echo a foo )" + } + a moo + a foo + foo() { + \echo a moo + ./pfn "$(\echo a foo )" + } + a moo + a foo +--- +name: comsub-torture +description: + Check the tree dump functions work correctly +stdin: + if [[ -z $__progname ]]; then echo >&2 call me with __progname; exit 1; fi + while IFS= read -r line; do + if [[ $line = '#1' ]]; then + lastf=0 + continue + elif [[ $line = EOFN* ]]; then + fbody=$fbody$'\n'$line + continue + elif [[ $line != '#'* ]]; then + fbody=$fbody$'\n\t'$line + continue + fi + if (( lastf )); then + x="inline_${nextf}() {"$fbody$'\n}\n' + print -nr -- "$x" + print -r -- "${x}typeset -f inline_$nextf" | "$__progname" + x="function comsub_$nextf { x=\$("$fbody$'\n); }\n' + print -nr -- "$x" + print -r -- "${x}typeset -f comsub_$nextf" | "$__progname" + x="function reread_$nextf { x=\$(("$fbody$'\n)|tr u x); }\n' + print -nr -- "$x" + print -r -- "${x}typeset -f reread_$nextf" | "$__progname" + fi + lastf=1 + fbody= + nextf=${line#?} + done <<'EOD' + #1 + #TCOM + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" + #TPAREN_TPIPE_TLIST + (echo $foo | tr -dc 0-9; echo) + #TAND_TOR + cmd && echo ja || echo nein + #TSELECT + select file in *; do echo "<$file>" ; break ; done + #TFOR_TTIME + time for i in {1,2,3} ; do echo $i ; done + #TCASE + case $foo in 1) echo eins;& 2) echo zwei ;| *) echo kann net bis drei zählen;; esac + #TIF_TBANG_TDBRACKET_TELIF + if ! [[ 1 = 1 ]] ; then echo eins; elif [[ 1 = 2 ]]; then echo zwei ;else echo drei; fi + #TWHILE + i=1; while (( i < 10 )); do echo $i; let ++i; done + #TUNTIL + i=10; until (( !--i )) ; do echo $i; done + #TCOPROC + cat * |& ls + #TFUNCT_TBRACE_TASYNC + function korn { echo eins; echo zwei ; } + bourne () { logger * & } + #IOREAD_IOCAT + tr x u 0>bar + #IOWRITE_IOCLOB_IOHERE_noIOSKIP + cat >|bar <<'EOFN' + foo + EOFN + #IOWRITE_noIOCLOB_IOHERE_IOSKIP + cat 1>bar <<-EOFI + foo + EOFI + #IORDWR_IODUP + sh 1<>/dev/console 0<&1 2>&1 + #COMSUB_EXPRSUB_FUNSUB_VALSUB + echo $(true) $((1+ 2)) ${ :;} ${| REPLY=x;} + #QCHAR_OQUOTE_CQUOTE + echo fo\ob\"a\`r\'b\$az + echo "fo\ob\"a\`r\'b\$az" + echo 'fo\ob\"a\`r'\''b\$az' + #OSUBST_CSUBST_OPAT_SPAT_CPAT + [[ ${foo#bl\(u\)b} = @(bar|baz) ]] + #heredoc_closed + x=$(cat <&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF)" = @(?) ]] && rm -f /etc/motd + if [[ ! -s /etc/motd ]]; then + install -c -o root -g wheel -m 664 /dev/null /etc/motd + print -- "$x\n" >/etc/motd + fi + #wdarrassign + case x in + x) a+=b; c+=(d e) + esac + #0 + EOD +expected-stdout: + inline_TCOM() { + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" + } + inline_TCOM() { + vara=1 varb="2 3" \cmd arg1 $arg2 "$arg3 4" + } + function comsub_TCOM { x=$( + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" + ); } + function comsub_TCOM { + x=$(vara=1 varb="2 3" \cmd arg1 $arg2 "$arg3 4" ) + } + function reread_TCOM { x=$(( + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" + )|tr u x); } + function reread_TCOM { + x=$( ( vara=1 varb="2 3" \cmd arg1 $arg2 "$arg3 4" ) | \tr u x ) + } + inline_TPAREN_TPIPE_TLIST() { + (echo $foo | tr -dc 0-9; echo) + } + inline_TPAREN_TPIPE_TLIST() { + ( \echo $foo | \tr -dc 0-9 + \echo ) + } + function comsub_TPAREN_TPIPE_TLIST { x=$( + (echo $foo | tr -dc 0-9; echo) + ); } + function comsub_TPAREN_TPIPE_TLIST { + x=$( ( \echo $foo | \tr -dc 0-9 ; \echo ) ) + } + function reread_TPAREN_TPIPE_TLIST { x=$(( + (echo $foo | tr -dc 0-9; echo) + )|tr u x); } + function reread_TPAREN_TPIPE_TLIST { + x=$( ( ( \echo $foo | \tr -dc 0-9 ; \echo ) ) | \tr u x ) + } + inline_TAND_TOR() { + cmd && echo ja || echo nein + } + inline_TAND_TOR() { + \cmd && \echo ja || \echo nein + } + function comsub_TAND_TOR { x=$( + cmd && echo ja || echo nein + ); } + function comsub_TAND_TOR { + x=$(\cmd && \echo ja || \echo nein ) + } + function reread_TAND_TOR { x=$(( + cmd && echo ja || echo nein + )|tr u x); } + function reread_TAND_TOR { + x=$( ( \cmd && \echo ja || \echo nein ) | \tr u x ) + } + inline_TSELECT() { + select file in *; do echo "<$file>" ; break ; done + } + inline_TSELECT() { + select file in * + do + \echo "<$file>" + \break + done + } + function comsub_TSELECT { x=$( + select file in *; do echo "<$file>" ; break ; done + ); } + function comsub_TSELECT { + x=$(select file in * ; do \echo "<$file>" ; \break ; done ) + } + function reread_TSELECT { x=$(( + select file in *; do echo "<$file>" ; break ; done + )|tr u x); } + function reread_TSELECT { + x=$( ( select file in * ; do \echo "<$file>" ; \break ; done ) | \tr u x ) + } + inline_TFOR_TTIME() { + time for i in {1,2,3} ; do echo $i ; done + } + inline_TFOR_TTIME() { + time for i in {1,2,3} + do + \echo $i + done + } + function comsub_TFOR_TTIME { x=$( + time for i in {1,2,3} ; do echo $i ; done + ); } + function comsub_TFOR_TTIME { + x=$(time for i in {1,2,3} ; do \echo $i ; done ) + } + function reread_TFOR_TTIME { x=$(( + time for i in {1,2,3} ; do echo $i ; done + )|tr u x); } + function reread_TFOR_TTIME { + x=$( ( time for i in {1,2,3} ; do \echo $i ; done ) | \tr u x ) + } + inline_TCASE() { + case $foo in 1) echo eins;& 2) echo zwei ;| *) echo kann net bis drei zählen;; esac + } + inline_TCASE() { + case $foo in + (1) + \echo eins + ;& + (2) + \echo zwei + ;| + (*) + \echo kann net bis drei zählen + ;; + esac + } + function comsub_TCASE { x=$( + case $foo in 1) echo eins;& 2) echo zwei ;| *) echo kann net bis drei zählen;; esac + ); } + function comsub_TCASE { + x=$(case $foo in (1) \echo eins ;& (2) \echo zwei ;| (*) \echo kann net bis drei zählen ;; esac ) + } + function reread_TCASE { x=$(( + case $foo in 1) echo eins;& 2) echo zwei ;| *) echo kann net bis drei zählen;; esac + )|tr u x); } + function reread_TCASE { + x=$( ( case $foo in (1) \echo eins ;& (2) \echo zwei ;| (*) \echo kann net bis drei zählen ;; esac ) | \tr u x ) + } + inline_TIF_TBANG_TDBRACKET_TELIF() { + if ! [[ 1 = 1 ]] ; then echo eins; elif [[ 1 = 2 ]]; then echo zwei ;else echo drei; fi + } + inline_TIF_TBANG_TDBRACKET_TELIF() { + if ! [[ 1 = 1 ]] + then + \echo eins + elif [[ 1 = 2 ]] + then + \echo zwei + else + \echo drei + fi + } + function comsub_TIF_TBANG_TDBRACKET_TELIF { x=$( + if ! [[ 1 = 1 ]] ; then echo eins; elif [[ 1 = 2 ]]; then echo zwei ;else echo drei; fi + ); } + function comsub_TIF_TBANG_TDBRACKET_TELIF { + x=$(if ! [[ 1 = 1 ]] ; then \echo eins ; elif [[ 1 = 2 ]] ; then \echo zwei ; else \echo drei ; fi ) + } + function reread_TIF_TBANG_TDBRACKET_TELIF { x=$(( + if ! [[ 1 = 1 ]] ; then echo eins; elif [[ 1 = 2 ]]; then echo zwei ;else echo drei; fi + )|tr u x); } + function reread_TIF_TBANG_TDBRACKET_TELIF { + x=$( ( if ! [[ 1 = 1 ]] ; then \echo eins ; elif [[ 1 = 2 ]] ; then \echo zwei ; else \echo drei ; fi ) | \tr u x ) + } + inline_TWHILE() { + i=1; while (( i < 10 )); do echo $i; let ++i; done + } + inline_TWHILE() { + i=1 + while { + \\builtin let " i < 10 " + } + do + \echo $i + \let ++i + done + } + function comsub_TWHILE { x=$( + i=1; while (( i < 10 )); do echo $i; let ++i; done + ); } + function comsub_TWHILE { + x=$(i=1 ; while { \\builtin let " i < 10 " ; } ; do \echo $i ; \let ++i ; done ) + } + function reread_TWHILE { x=$(( + i=1; while (( i < 10 )); do echo $i; let ++i; done + )|tr u x); } + function reread_TWHILE { + x=$( ( i=1 ; while { \\builtin let " i < 10 " ; } ; do \echo $i ; \let ++i ; done ) | \tr u x ) + } + inline_TUNTIL() { + i=10; until (( !--i )) ; do echo $i; done + } + inline_TUNTIL() { + i=10 + until { + \\builtin let " !--i " + } + do + \echo $i + done + } + function comsub_TUNTIL { x=$( + i=10; until (( !--i )) ; do echo $i; done + ); } + function comsub_TUNTIL { + x=$(i=10 ; until { \\builtin let " !--i " ; } ; do \echo $i ; done ) + } + function reread_TUNTIL { x=$(( + i=10; until (( !--i )) ; do echo $i; done + )|tr u x); } + function reread_TUNTIL { + x=$( ( i=10 ; until { \\builtin let " !--i " ; } ; do \echo $i ; done ) | \tr u x ) + } + inline_TCOPROC() { + cat * |& ls + } + inline_TCOPROC() { + \cat * |& + \ls + } + function comsub_TCOPROC { x=$( + cat * |& ls + ); } + function comsub_TCOPROC { + x=$(\cat * |& \ls ) + } + function reread_TCOPROC { x=$(( + cat * |& ls + )|tr u x); } + function reread_TCOPROC { + x=$( ( \cat * |& \ls ) | \tr u x ) + } + inline_TFUNCT_TBRACE_TASYNC() { + function korn { echo eins; echo zwei ; } + bourne () { logger * & } + } + inline_TFUNCT_TBRACE_TASYNC() { + function korn { + \echo eins + \echo zwei + } + bourne() { + \logger * & + } + } + function comsub_TFUNCT_TBRACE_TASYNC { x=$( + function korn { echo eins; echo zwei ; } + bourne () { logger * & } + ); } + function comsub_TFUNCT_TBRACE_TASYNC { + x=$(function korn { \echo eins ; \echo zwei ; } ; bourne() { \logger * & } ) + } + function reread_TFUNCT_TBRACE_TASYNC { x=$(( + function korn { echo eins; echo zwei ; } + bourne () { logger * & } + )|tr u x); } + function reread_TFUNCT_TBRACE_TASYNC { + x=$( ( function korn { \echo eins ; \echo zwei ; } ; bourne() { \logger * & } ) | \tr u x ) + } + inline_IOREAD_IOCAT() { + tr x u 0>bar + } + inline_IOREAD_IOCAT() { + \tr x u >bar + } + function comsub_IOREAD_IOCAT { x=$( + tr x u 0>bar + ); } + function comsub_IOREAD_IOCAT { + x=$(\tr x u >bar ) + } + function reread_IOREAD_IOCAT { x=$(( + tr x u 0>bar + )|tr u x); } + function reread_IOREAD_IOCAT { + x=$( ( \tr x u >bar ) | \tr u x ) + } + inline_IOWRITE_IOCLOB_IOHERE_noIOSKIP() { + cat >|bar <<'EOFN' + foo + EOFN + } + inline_IOWRITE_IOCLOB_IOHERE_noIOSKIP() { + \cat >|bar <<"EOFN" + foo + EOFN + + } + function comsub_IOWRITE_IOCLOB_IOHERE_noIOSKIP { x=$( + cat >|bar <<'EOFN' + foo + EOFN + ); } + function comsub_IOWRITE_IOCLOB_IOHERE_noIOSKIP { + x=$(\cat >|bar <<"EOFN" + foo + EOFN + ) + } + function reread_IOWRITE_IOCLOB_IOHERE_noIOSKIP { x=$(( + cat >|bar <<'EOFN' + foo + EOFN + )|tr u x); } + function reread_IOWRITE_IOCLOB_IOHERE_noIOSKIP { + x=$( ( \cat >|bar <<"EOFN" + foo + EOFN + ) | \tr u x ) + } + inline_IOWRITE_noIOCLOB_IOHERE_IOSKIP() { + cat 1>bar <<-EOFI + foo + EOFI + } + inline_IOWRITE_noIOCLOB_IOHERE_IOSKIP() { + \cat >bar <<-EOFI + foo + EOFI + + } + function comsub_IOWRITE_noIOCLOB_IOHERE_IOSKIP { x=$( + cat 1>bar <<-EOFI + foo + EOFI + ); } + function comsub_IOWRITE_noIOCLOB_IOHERE_IOSKIP { + x=$(\cat >bar <<-EOFI + foo + EOFI + ) + } + function reread_IOWRITE_noIOCLOB_IOHERE_IOSKIP { x=$(( + cat 1>bar <<-EOFI + foo + EOFI + )|tr u x); } + function reread_IOWRITE_noIOCLOB_IOHERE_IOSKIP { + x=$( ( \cat >bar <<-EOFI + foo + EOFI + ) | \tr u x ) + } + inline_IORDWR_IODUP() { + sh 1<>/dev/console 0<&1 2>&1 + } + inline_IORDWR_IODUP() { + \sh 1<>/dev/console <&1 2>&1 + } + function comsub_IORDWR_IODUP { x=$( + sh 1<>/dev/console 0<&1 2>&1 + ); } + function comsub_IORDWR_IODUP { + x=$(\sh 1<>/dev/console <&1 2>&1 ) + } + function reread_IORDWR_IODUP { x=$(( + sh 1<>/dev/console 0<&1 2>&1 + )|tr u x); } + function reread_IORDWR_IODUP { + x=$( ( \sh 1<>/dev/console <&1 2>&1 ) | \tr u x ) + } + inline_COMSUB_EXPRSUB_FUNSUB_VALSUB() { + echo $(true) $((1+ 2)) ${ :;} ${| REPLY=x;} + } + inline_COMSUB_EXPRSUB_FUNSUB_VALSUB() { + \echo $(\true ) $((1+ 2)) ${ \: ;} ${|REPLY=x ;} + } + function comsub_COMSUB_EXPRSUB_FUNSUB_VALSUB { x=$( + echo $(true) $((1+ 2)) ${ :;} ${| REPLY=x;} + ); } + function comsub_COMSUB_EXPRSUB_FUNSUB_VALSUB { + x=$(\echo $(\true ) $((1+ 2)) ${ \: ;} ${|REPLY=x ;} ) + } + function reread_COMSUB_EXPRSUB_FUNSUB_VALSUB { x=$(( + echo $(true) $((1+ 2)) ${ :;} ${| REPLY=x;} + )|tr u x); } + function reread_COMSUB_EXPRSUB_FUNSUB_VALSUB { + x=$( ( \echo $(\true ) $((1+ 2)) ${ \: ;} ${|REPLY=x ;} ) | \tr u x ) + } + inline_QCHAR_OQUOTE_CQUOTE() { + echo fo\ob\"a\`r\'b\$az + echo "fo\ob\"a\`r\'b\$az" + echo 'fo\ob\"a\`r'\''b\$az' + } + inline_QCHAR_OQUOTE_CQUOTE() { + \echo fo\ob\"a\`r\'b\$az + \echo "fo\ob\"a\`r\'b\$az" + \echo "fo\\ob\\\"a\\\`r"\'"b\\\$az" + } + function comsub_QCHAR_OQUOTE_CQUOTE { x=$( + echo fo\ob\"a\`r\'b\$az + echo "fo\ob\"a\`r\'b\$az" + echo 'fo\ob\"a\`r'\''b\$az' + ); } + function comsub_QCHAR_OQUOTE_CQUOTE { + x=$(\echo fo\ob\"a\`r\'b\$az ; \echo "fo\ob\"a\`r\'b\$az" ; \echo "fo\\ob\\\"a\\\`r"\'"b\\\$az" ) + } + function reread_QCHAR_OQUOTE_CQUOTE { x=$(( + echo fo\ob\"a\`r\'b\$az + echo "fo\ob\"a\`r\'b\$az" + echo 'fo\ob\"a\`r'\''b\$az' + )|tr u x); } + function reread_QCHAR_OQUOTE_CQUOTE { + x=$( ( \echo fo\ob\"a\`r\'b\$az ; \echo "fo\ob\"a\`r\'b\$az" ; \echo "fo\\ob\\\"a\\\`r"\'"b\\\$az" ) | \tr u x ) + } + inline_OSUBST_CSUBST_OPAT_SPAT_CPAT() { + [[ ${foo#bl\(u\)b} = @(bar|baz) ]] + } + inline_OSUBST_CSUBST_OPAT_SPAT_CPAT() { + [[ ${foo#bl\(u\)b} = @(bar|baz) ]] + } + function comsub_OSUBST_CSUBST_OPAT_SPAT_CPAT { x=$( + [[ ${foo#bl\(u\)b} = @(bar|baz) ]] + ); } + function comsub_OSUBST_CSUBST_OPAT_SPAT_CPAT { + x=$([[ ${foo#bl\(u\)b} = @(bar|baz) ]] ) + } + function reread_OSUBST_CSUBST_OPAT_SPAT_CPAT { x=$(( + [[ ${foo#bl\(u\)b} = @(bar|baz) ]] + )|tr u x); } + function reread_OSUBST_CSUBST_OPAT_SPAT_CPAT { + x=$( ( [[ ${foo#bl\(u\)b} = @(bar|baz) ]] ) | \tr u x ) + } + inline_heredoc_closed() { + x=$(cat <&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF)" = @(?) ]] && rm -f /etc/motd + if [[ ! -s /etc/motd ]]; then + install -c -o root -g wheel -m 664 /dev/null /etc/motd + print -- "$x\n" >/etc/motd + fi + } + inline_patch_motd() { + x=$(\sysctl -n kern.version | \sed 1q ) + [[ -s /etc/motd && "$([[ "$(\head -1 /etc/motd )" != $x ]] && \ed -s /etc/motd 2>&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF + )" = @(?) ]] && \rm -f /etc/motd + if [[ ! -s /etc/motd ]] + then + \install -c -o root -g wheel -m 664 /dev/null /etc/motd + \print -- "$x\n" >/etc/motd + fi + } + function comsub_patch_motd { x=$( + x=$(sysctl -n kern.version | sed 1q) + [[ -s /etc/motd && "$([[ "$(head -1 /etc/motd)" != $x ]] && \ + ed -s /etc/motd 2>&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF)" = @(?) ]] && rm -f /etc/motd + if [[ ! -s /etc/motd ]]; then + install -c -o root -g wheel -m 664 /dev/null /etc/motd + print -- "$x\n" >/etc/motd + fi + ); } + function comsub_patch_motd { + x=$(x=$(\sysctl -n kern.version | \sed 1q ) ; [[ -s /etc/motd && "$([[ "$(\head -1 /etc/motd )" != $x ]] && \ed -s /etc/motd 2>&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF + )" = @(?) ]] && \rm -f /etc/motd ; if [[ ! -s /etc/motd ]] ; then \install -c -o root -g wheel -m 664 /dev/null /etc/motd ; \print -- "$x\n" >/etc/motd ; fi ) + } + function reread_patch_motd { x=$(( + x=$(sysctl -n kern.version | sed 1q) + [[ -s /etc/motd && "$([[ "$(head -1 /etc/motd)" != $x ]] && \ + ed -s /etc/motd 2>&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF)" = @(?) ]] && rm -f /etc/motd + if [[ ! -s /etc/motd ]]; then + install -c -o root -g wheel -m 664 /dev/null /etc/motd + print -- "$x\n" >/etc/motd + fi + )|tr u x); } + function reread_patch_motd { + x=$( ( x=$(\sysctl -n kern.version | \sed 1q ) ; [[ -s /etc/motd && "$([[ "$(\head -1 /etc/motd )" != $x ]] && \ed -s /etc/motd 2>&1 <<-EOF + 1,/^\$/d + 0a + $x + + . + wq + EOF + )" = @(?) ]] && \rm -f /etc/motd ; if [[ ! -s /etc/motd ]] ; then \install -c -o root -g wheel -m 664 /dev/null /etc/motd ; \print -- "$x\n" >/etc/motd ; fi ) | \tr u x ) + } + inline_wdarrassign() { + case x in + x) a+=b; c+=(d e) + esac + } + inline_wdarrassign() { + case x in + (x) + a+=b + \\builtin set -A c+ -- d e + ;; + esac + } + function comsub_wdarrassign { x=$( + case x in + x) a+=b; c+=(d e) + esac + ); } + function comsub_wdarrassign { + x=$(case x in (x) a+=b ; \\builtin set -A c+ -- d e ;; esac ) + } + function reread_wdarrassign { x=$(( + case x in + x) a+=b; c+=(d e) + esac + )|tr u x); } + function reread_wdarrassign { + x=$( ( case x in (x) a+=b ; \\builtin set -A c+ -- d e ;; esac ) | \tr u x ) + } +--- +name: comsub-torture-io +description: + Check the tree dump functions work correctly with I/O redirection +stdin: + if [[ -z $__progname ]]; then echo >&2 call me with __progname; exit 1; fi + while IFS= read -r line; do + if [[ $line = '#1' ]]; then + lastf=0 + continue + elif [[ $line = EOFN* ]]; then + fbody=$fbody$'\n'$line + continue + elif [[ $line != '#'* ]]; then + fbody=$fbody$'\n\t'$line + continue + fi + if (( lastf )); then + x="inline_${nextf}() {"$fbody$'\n}\n' + print -nr -- "$x" + print -r -- "${x}typeset -f inline_$nextf" | "$__progname" + x="function comsub_$nextf { x=\$("$fbody$'\n); }\n' + print -nr -- "$x" + print -r -- "${x}typeset -f comsub_$nextf" | "$__progname" + x="function reread_$nextf { x=\$(("$fbody$'\n)|tr u x); }\n' + print -nr -- "$x" + print -r -- "${x}typeset -f reread_$nextf" | "$__progname" + fi + lastf=1 + fbody= + nextf=${line#?} + done <<'EOD' + #1 + #TCOM + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" >&3 + #TPAREN_TPIPE_TLIST + (echo $foo | tr -dc 0-9 >&3; echo >&3) >&3 + #TAND_TOR + cmd >&3 && >&3 echo ja || echo >&3 nein + #TSELECT + select file in *; do echo "<$file>" ; break >&3 ; done >&3 + #TFOR_TTIME + for i in {1,2,3} ; do time >&3 echo $i ; done >&3 + #TCASE + case $foo in 1) echo eins >&3;& 2) echo zwei >&3 ;| *) echo kann net bis drei zählen >&3;; esac >&3 + #TIF_TBANG_TDBRACKET_TELIF + if ! [[ 1 = 1 ]] >&3 ; then echo eins; elif [[ 1 = 2 ]] >&3; then echo zwei ;else echo drei; fi >&3 + #TWHILE + i=1; while (( i < 10 )) >&3; do echo $i; let ++i; done >&3 + #TUNTIL + i=10; until (( !--i )) >&3 ; do echo $i; done >&3 + #TCOPROC + cat * >&3 |& >&3 ls + #TFUNCT_TBRACE_TASYNC + function korn { echo eins; echo >&3 zwei ; } + bourne () { logger * >&3 & } + #COMSUB_EXPRSUB + echo $(true >&3) $((1+ 2)) + #0 + EOD +expected-stdout: + inline_TCOM() { + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" >&3 + } + inline_TCOM() { + vara=1 varb="2 3" \cmd arg1 $arg2 "$arg3 4" >&3 + } + function comsub_TCOM { x=$( + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" >&3 + ); } + function comsub_TCOM { + x=$(vara=1 varb="2 3" \cmd arg1 $arg2 "$arg3 4" >&3 ) + } + function reread_TCOM { x=$(( + vara=1 varb='2 3' cmd arg1 $arg2 "$arg3 4" >&3 + )|tr u x); } + function reread_TCOM { + x=$( ( vara=1 varb="2 3" \cmd arg1 $arg2 "$arg3 4" >&3 ) | \tr u x ) + } + inline_TPAREN_TPIPE_TLIST() { + (echo $foo | tr -dc 0-9 >&3; echo >&3) >&3 + } + inline_TPAREN_TPIPE_TLIST() { + ( \echo $foo | \tr -dc 0-9 >&3 + \echo >&3 ) >&3 + } + function comsub_TPAREN_TPIPE_TLIST { x=$( + (echo $foo | tr -dc 0-9 >&3; echo >&3) >&3 + ); } + function comsub_TPAREN_TPIPE_TLIST { + x=$( ( \echo $foo | \tr -dc 0-9 >&3 ; \echo >&3 ) >&3 ) + } + function reread_TPAREN_TPIPE_TLIST { x=$(( + (echo $foo | tr -dc 0-9 >&3; echo >&3) >&3 + )|tr u x); } + function reread_TPAREN_TPIPE_TLIST { + x=$( ( ( \echo $foo | \tr -dc 0-9 >&3 ; \echo >&3 ) >&3 ) | \tr u x ) + } + inline_TAND_TOR() { + cmd >&3 && >&3 echo ja || echo >&3 nein + } + inline_TAND_TOR() { + \cmd >&3 && \echo ja >&3 || \echo nein >&3 + } + function comsub_TAND_TOR { x=$( + cmd >&3 && >&3 echo ja || echo >&3 nein + ); } + function comsub_TAND_TOR { + x=$(\cmd >&3 && \echo ja >&3 || \echo nein >&3 ) + } + function reread_TAND_TOR { x=$(( + cmd >&3 && >&3 echo ja || echo >&3 nein + )|tr u x); } + function reread_TAND_TOR { + x=$( ( \cmd >&3 && \echo ja >&3 || \echo nein >&3 ) | \tr u x ) + } + inline_TSELECT() { + select file in *; do echo "<$file>" ; break >&3 ; done >&3 + } + inline_TSELECT() { + select file in * + do + \echo "<$file>" + \break >&3 + done >&3 + } + function comsub_TSELECT { x=$( + select file in *; do echo "<$file>" ; break >&3 ; done >&3 + ); } + function comsub_TSELECT { + x=$(select file in * ; do \echo "<$file>" ; \break >&3 ; done >&3 ) + } + function reread_TSELECT { x=$(( + select file in *; do echo "<$file>" ; break >&3 ; done >&3 + )|tr u x); } + function reread_TSELECT { + x=$( ( select file in * ; do \echo "<$file>" ; \break >&3 ; done >&3 ) | \tr u x ) + } + inline_TFOR_TTIME() { + for i in {1,2,3} ; do time >&3 echo $i ; done >&3 + } + inline_TFOR_TTIME() { + for i in {1,2,3} + do + time \echo $i >&3 + done >&3 + } + function comsub_TFOR_TTIME { x=$( + for i in {1,2,3} ; do time >&3 echo $i ; done >&3 + ); } + function comsub_TFOR_TTIME { + x=$(for i in {1,2,3} ; do time \echo $i >&3 ; done >&3 ) + } + function reread_TFOR_TTIME { x=$(( + for i in {1,2,3} ; do time >&3 echo $i ; done >&3 + )|tr u x); } + function reread_TFOR_TTIME { + x=$( ( for i in {1,2,3} ; do time \echo $i >&3 ; done >&3 ) | \tr u x ) + } + inline_TCASE() { + case $foo in 1) echo eins >&3;& 2) echo zwei >&3 ;| *) echo kann net bis drei zählen >&3;; esac >&3 + } + inline_TCASE() { + case $foo in + (1) + \echo eins >&3 + ;& + (2) + \echo zwei >&3 + ;| + (*) + \echo kann net bis drei zählen >&3 + ;; + esac >&3 + } + function comsub_TCASE { x=$( + case $foo in 1) echo eins >&3;& 2) echo zwei >&3 ;| *) echo kann net bis drei zählen >&3;; esac >&3 + ); } + function comsub_TCASE { + x=$(case $foo in (1) \echo eins >&3 ;& (2) \echo zwei >&3 ;| (*) \echo kann net bis drei zählen >&3 ;; esac >&3 ) + } + function reread_TCASE { x=$(( + case $foo in 1) echo eins >&3;& 2) echo zwei >&3 ;| *) echo kann net bis drei zählen >&3;; esac >&3 + )|tr u x); } + function reread_TCASE { + x=$( ( case $foo in (1) \echo eins >&3 ;& (2) \echo zwei >&3 ;| (*) \echo kann net bis drei zählen >&3 ;; esac >&3 ) | \tr u x ) + } + inline_TIF_TBANG_TDBRACKET_TELIF() { + if ! [[ 1 = 1 ]] >&3 ; then echo eins; elif [[ 1 = 2 ]] >&3; then echo zwei ;else echo drei; fi >&3 + } + inline_TIF_TBANG_TDBRACKET_TELIF() { + if ! [[ 1 = 1 ]] >&3 + then + \echo eins + elif [[ 1 = 2 ]] >&3 + then + \echo zwei + else + \echo drei + fi >&3 + } + function comsub_TIF_TBANG_TDBRACKET_TELIF { x=$( + if ! [[ 1 = 1 ]] >&3 ; then echo eins; elif [[ 1 = 2 ]] >&3; then echo zwei ;else echo drei; fi >&3 + ); } + function comsub_TIF_TBANG_TDBRACKET_TELIF { + x=$(if ! [[ 1 = 1 ]] >&3 ; then \echo eins ; elif [[ 1 = 2 ]] >&3 ; then \echo zwei ; else \echo drei ; fi >&3 ) + } + function reread_TIF_TBANG_TDBRACKET_TELIF { x=$(( + if ! [[ 1 = 1 ]] >&3 ; then echo eins; elif [[ 1 = 2 ]] >&3; then echo zwei ;else echo drei; fi >&3 + )|tr u x); } + function reread_TIF_TBANG_TDBRACKET_TELIF { + x=$( ( if ! [[ 1 = 1 ]] >&3 ; then \echo eins ; elif [[ 1 = 2 ]] >&3 ; then \echo zwei ; else \echo drei ; fi >&3 ) | \tr u x ) + } + inline_TWHILE() { + i=1; while (( i < 10 )) >&3; do echo $i; let ++i; done >&3 + } + inline_TWHILE() { + i=1 + while { + \\builtin let " i < 10 " + } >&3 + do + \echo $i + \let ++i + done >&3 + } + function comsub_TWHILE { x=$( + i=1; while (( i < 10 )) >&3; do echo $i; let ++i; done >&3 + ); } + function comsub_TWHILE { + x=$(i=1 ; while { \\builtin let " i < 10 " ; } >&3 ; do \echo $i ; \let ++i ; done >&3 ) + } + function reread_TWHILE { x=$(( + i=1; while (( i < 10 )) >&3; do echo $i; let ++i; done >&3 + )|tr u x); } + function reread_TWHILE { + x=$( ( i=1 ; while { \\builtin let " i < 10 " ; } >&3 ; do \echo $i ; \let ++i ; done >&3 ) | \tr u x ) + } + inline_TUNTIL() { + i=10; until (( !--i )) >&3 ; do echo $i; done >&3 + } + inline_TUNTIL() { + i=10 + until { + \\builtin let " !--i " + } >&3 + do + \echo $i + done >&3 + } + function comsub_TUNTIL { x=$( + i=10; until (( !--i )) >&3 ; do echo $i; done >&3 + ); } + function comsub_TUNTIL { + x=$(i=10 ; until { \\builtin let " !--i " ; } >&3 ; do \echo $i ; done >&3 ) + } + function reread_TUNTIL { x=$(( + i=10; until (( !--i )) >&3 ; do echo $i; done >&3 + )|tr u x); } + function reread_TUNTIL { + x=$( ( i=10 ; until { \\builtin let " !--i " ; } >&3 ; do \echo $i ; done >&3 ) | \tr u x ) + } + inline_TCOPROC() { + cat * >&3 |& >&3 ls + } + inline_TCOPROC() { + \cat * >&3 |& + \ls >&3 + } + function comsub_TCOPROC { x=$( + cat * >&3 |& >&3 ls + ); } + function comsub_TCOPROC { + x=$(\cat * >&3 |& \ls >&3 ) + } + function reread_TCOPROC { x=$(( + cat * >&3 |& >&3 ls + )|tr u x); } + function reread_TCOPROC { + x=$( ( \cat * >&3 |& \ls >&3 ) | \tr u x ) + } + inline_TFUNCT_TBRACE_TASYNC() { + function korn { echo eins; echo >&3 zwei ; } + bourne () { logger * >&3 & } + } + inline_TFUNCT_TBRACE_TASYNC() { + function korn { + \echo eins + \echo zwei >&3 + } + bourne() { + \logger * >&3 & + } + } + function comsub_TFUNCT_TBRACE_TASYNC { x=$( + function korn { echo eins; echo >&3 zwei ; } + bourne () { logger * >&3 & } + ); } + function comsub_TFUNCT_TBRACE_TASYNC { + x=$(function korn { \echo eins ; \echo zwei >&3 ; } ; bourne() { \logger * >&3 & } ) + } + function reread_TFUNCT_TBRACE_TASYNC { x=$(( + function korn { echo eins; echo >&3 zwei ; } + bourne () { logger * >&3 & } + )|tr u x); } + function reread_TFUNCT_TBRACE_TASYNC { + x=$( ( function korn { \echo eins ; \echo zwei >&3 ; } ; bourne() { \logger * >&3 & } ) | \tr u x ) + } + inline_COMSUB_EXPRSUB() { + echo $(true >&3) $((1+ 2)) + } + inline_COMSUB_EXPRSUB() { + \echo $(\true >&3 ) $((1+ 2)) + } + function comsub_COMSUB_EXPRSUB { x=$( + echo $(true >&3) $((1+ 2)) + ); } + function comsub_COMSUB_EXPRSUB { + x=$(\echo $(\true >&3 ) $((1+ 2)) ) + } + function reread_COMSUB_EXPRSUB { x=$(( + echo $(true >&3) $((1+ 2)) + )|tr u x); } + function reread_COMSUB_EXPRSUB { + x=$( ( \echo $(\true >&3 ) $((1+ 2)) ) | \tr u x ) + } +--- +name: funsub-1 +description: + Check that non-subenvironment command substitution works +stdin: + set -e + foo=bar + echo "ob $foo ." + echo "${ + echo "ib $foo :" + foo=baz + echo "ia $foo :" + false + }" . + echo "oa $foo ." +expected-stdout: + ob bar . + ib bar : + ia baz : . + oa baz . +--- +name: funsub-2 +description: + You can now reliably use local and return in funsubs + (not exit though) +stdin: + x=q; e=1; x=${ echo a; e=2; echo x$e;}; echo 1:y$x,$e,$?. + x=q; e=1; x=${ echo a; typeset e=2; echo x$e;}; echo 2:y$x,$e,$?. + x=q; e=1; x=${ echo a; typeset e=2; return 3; echo x$e;}; echo 3:y$x,$e,$?. +expected-stdout: + 1:ya x2,2,0. + 2:ya x2,1,0. + 3:ya,1,3. +--- +name: valsub-1 +description: + Check that "value substitutions" work as advertised +stdin: + x=1 + y=2 + z=3 + REPLY=4 + echo "before: x<$x> y<$y> z<$z> R<$REPLY>" + x=${| + local y + echo "start: x<$x> y<$y> z<$z> R<$REPLY>" + x=5 + y=6 + z=7 + REPLY=8 + echo "end: x<$x> y<$y> z<$z> R<$REPLY>" + } + echo "after: x<$x> y<$y> z<$z> R<$REPLY>" + # ensure trailing newlines are kept + t=${|REPLY=$'foo\n\n';} + typeset -p t + echo -n this used to segfault + echo ${|true;}$(true). +expected-stdout: + before: x<1> y<2> z<3> R<4> + start: x<1> y<> z<3> R<> + end: x<5> y<6> z<7> R<8> + after: x<8> y<2> z<7> R<4> + typeset t=$'foo\n\n' + this used to segfault. +--- +name: event-subst-3 +description: + Check that '!' substitution in noninteractive mode is ignored +file-setup: file 755 "falsetto" + #! /bin/sh + echo molto bene + exit 42 +file-setup: file 755 "!false" + #! /bin/sh + echo si +stdin: + export PATH=.$PATHSEP$PATH + falsetto + echo yeap + !false + echo meow + ! false + echo = $? + if + ! false; then echo foo; else echo bar; fi +expected-stdout: + molto bene + yeap + si + meow + = 0 + foo +--- +name: event-subst-0 +description: + Check that '!' substitution in interactive mode is ignored +need-ctty: yes +arguments: !-i! +file-setup: file 755 "falsetto" + #! /bin/sh + echo molto bene + exit 42 +file-setup: file 755 "!false" + #! /bin/sh + echo si +stdin: + export PATH=.$PATHSEP$PATH + falsetto + echo yeap + !false + echo meow + ! false + echo = $? + if + ! false; then echo foo; else echo bar; fi +expected-stdout: + molto bene + yeap + si + meow + = 0 + foo +expected-stderr-pattern: + /.*/ +--- +name: nounset-1 +description: + Check that "set -u" matches (future) SUSv4 requirement +stdin: + (set -u + try() { + local v + eval v=\$$1 + if [[ -n $v ]]; then + echo $1=nz + else + echo $1=zf + fi + } + x=y + (echo $x) + echo =1 + (echo $y) + echo =2 + (try x) + echo =3 + (try y) + echo =4 + (try 0) + echo =5 + (try 2) + echo =6 + (try) + echo =7 + (echo at=$@) + echo =8 + (echo asterisk=$*) + echo =9 + (echo $?) + echo =10 + (echo $!) + echo =11 + (echo $-) + echo =12 + #(echo $_) + #echo =13 + (echo $#) + echo =14 + (mypid=$$; try mypid) + echo =15 + ) 2>&1 | sed -e 's/^[^]]*]//' -e 's/^[^:]*: *//' + exit ${PIPESTATUS[0]} +expected-stdout: + y + =1 + y: parameter not set + =2 + x=nz + =3 + y: parameter not set + =4 + 0=nz + =5 + 2: parameter not set + =6 + 1: parameter not set + =7 + at= + =8 + asterisk= + =9 + 0 + =10 + !: parameter not set + =11 + ush + =12 + 0 + =14 + mypid=nz + =15 +--- +name: nameref-1 +description: + Testsuite for nameref (bound variables) +stdin: + bar=global + typeset -n ir2=bar + typeset -n ind=ir2 + echo !ind: ${!ind} + echo ind: $ind + echo !ir2: ${!ir2} + echo ir2: $ir2 + typeset +n ind + echo !ind: ${!ind} + echo ind: $ind + typeset -n ir2=ind + echo !ir2: ${!ir2} + echo ir2: $ir2 + set|grep ^ir2|sed 's/^/s1: /' + typeset|grep ' ir2'|sed -e 's/^/s2: /' -e 's/nameref/typeset -n/' + set -A blub -- e1 e2 e3 + typeset -n ind=blub + typeset -n ir2=blub[2] + echo !ind[1]: ${!ind[1]} + echo !ir2: $!ir2 + echo ind[1]: ${ind[1]} + echo ir2: $ir2 +expected-stdout: + !ind: bar + ind: global + !ir2: bar + ir2: global + !ind: ind + ind: ir2 + !ir2: ind + ir2: ir2 + s1: ir2=ind + s2: typeset -n ir2 + !ind[1]: blub[1] + !ir2: ir2 + ind[1]: e2 + ir2: e3 +--- +name: nameref-2da +description: + Testsuite for nameref (bound variables) + Functions, argument given directly, after local +stdin: + function foo { + typeset bar=lokal baz=auch + typeset -n v=bar + echo entering + echo !v: ${!v} + echo !bar: ${!bar} + echo !baz: ${!baz} + echo bar: $bar + echo v: $v + v=123 + echo bar: $bar + echo v: $v + echo exiting + } + bar=global + echo bar: $bar + foo bar + echo bar: $bar +expected-stdout: + bar: global + entering + !v: bar + !bar: bar + !baz: baz + bar: lokal + v: lokal + bar: 123 + v: 123 + exiting + bar: global +--- +name: nameref-3 +description: + Advanced testsuite for bound variables (ksh93 fails this) +stdin: + typeset -n foo=bar[i] + set -A bar -- b c a + for i in 0 1 2 3; do + print $i $foo . + done +expected-stdout: + 0 b . + 1 c . + 2 a . + 3 . +--- +name: nameref-4 +description: + Ensure we don't run in an infinite loop +time-limit: 3 +stdin: + baz() { + typeset -n foo=fnord fnord=foo + foo[0]=bar + } + set -A foo bad + echo sind $foo . + baz + echo blah $foo . +expected-stdout: + sind bad . + blah bad . +expected-stderr-pattern: + /fnord: expression recurses on parameter/ +--- +name: better-parens-1a +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + if ( (echo fubar)|tr u x); then + echo ja + else + echo nein + fi +expected-stdout: + fxbar + ja +--- +name: better-parens-1b +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + echo $( (echo fubar)|tr u x) $? +expected-stdout: + fxbar 0 +--- +name: better-parens-1c +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + x=$( (echo fubar)|tr u x); echo $x $? +expected-stdout: + fxbar 0 +--- +name: better-parens-2a +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + if ((echo fubar)|tr u x); then + echo ja + else + echo nein + fi +expected-stdout: + fxbar + ja +--- +name: better-parens-2b +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + echo $((echo fubar)|tr u x) $? +expected-stdout: + fxbar 0 +--- +name: better-parens-2c +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + x=$((echo fubar)|tr u x); echo $x $? +expected-stdout: + fxbar 0 +--- +name: better-parens-3a +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + if ( (echo fubar)|(tr u x)); then + echo ja + else + echo nein + fi +expected-stdout: + fxbar + ja +--- +name: better-parens-3b +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + echo $( (echo fubar)|(tr u x)) $? +expected-stdout: + fxbar 0 +--- +name: better-parens-3c +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + x=$( (echo fubar)|(tr u x)); echo $x $? +expected-stdout: + fxbar 0 +--- +name: better-parens-4a +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + if ((echo fubar)|(tr u x)); then + echo ja + else + echo nein + fi +expected-stdout: + fxbar + ja +--- +name: better-parens-4b +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + echo $((echo fubar)|(tr u x)) $? +expected-stdout: + fxbar 0 +--- +name: better-parens-4c +description: + Check support for ((…)) and $((…)) vs (…) and $(…) +stdin: + x=$((echo fubar)|(tr u x)); echo $x $? +expected-stdout: + fxbar 0 +--- +name: better-parens-5 +description: + Another corner case +stdin: + ( (echo 'fo o$bar' "baz\$bla\"" m\$eh) | tr a A) + ((echo 'fo o$bar' "baz\$bla\"" m\$eh) | tr a A) +expected-stdout: + fo o$bAr bAz$blA" m$eh + fo o$bAr bAz$blA" m$eh +--- +name: echo-test-1 +description: + Test what the echo builtin does (mksh) +category: !shell:ebcdic-yes +stdin: + echo -n 'foo\x40bar' + echo -e '\tbaz' +expected-stdout: + foo@bar baz +--- +name: echo-test-1-ebcdic +description: + Test what the echo builtin does (mksh) +category: !shell:ebcdic-no +stdin: + echo -n 'foo\x7Cbar' + echo -e '\tbaz' +expected-stdout: + foo@bar baz +--- +name: echo-test-2 +description: + Test what the echo builtin does (POSIX) + Note: this follows Debian Policy 10.4 which mandates + that -n shall be treated as an option, not XSI which + mandates it shall be treated as string but escapes + shall be expanded. +stdin: + test -n "$POSH_VERSION" || set -o posix + echo -n 'foo\x40bar' + echo -e '\tbaz' +expected-stdout: + foo\x40bar-e \tbaz +--- +name: echo-test-3-mnbsd +description: + Test what the echo builtin does, and test a compatibility flag. +category: mnbsdash +stdin: + "$__progname" -c 'echo -n 1=\\x40$1; echo -e \\x2E' -- foo bar + "$__progname" -o posix -c 'echo -n 2=\\x40$1; echo -e \\x2E' -- foo bar + "$__progname" -o sh -c 'echo -n 3=\\x40$1; echo -e \\x2E' -- foo bar +expected-stdout: + 1=@foo. + 2=\x40foo-e \x2E + 3=\x40bar. +--- +name: echo-test-3-normal +description: + Test what the echo builtin does, and test a compatibility flag. +category: !mnbsdash,!shell:ebcdic-yes +stdin: + "$__progname" -c 'echo -n 1=\\x40$1; echo -e \\x2E' -- foo bar + "$__progname" -o posix -c 'echo -n 2=\\x40$1; echo -e \\x2E' -- foo bar + "$__progname" -o sh -c 'echo -n 3=\\x40$1; echo -e \\x2E' -- foo bar +expected-stdout: + 1=@foo. + 2=\x40foo-e \x2E + 3=\x40foo-e \x2E +--- +name: echo-test-3-ebcdic +description: + Test what the echo builtin does, and test a compatibility flag. +category: !mnbsdash,!shell:ebcdic-no +stdin: + "$__progname" -c 'echo -n 1=\\x7C$1; echo -e \\x4B' -- foo bar + "$__progname" -o posix -c 'echo -n 2=\\x7C$1; echo -e \\x4B' -- foo bar + "$__progname" -o sh -c 'echo -n 3=\\x7C$1; echo -e \\x4B' -- foo bar +expected-stdout: + 1=@foo. + 2=\x7Cfoo-e \x4B + 3=\x7Cfoo-e \x4B +--- +name: utilities-getopts-1 +description: + getopts sets OPTIND correctly for unparsed option +stdin: + set -- -a -a -x + while getopts :a optc; do + echo "OPTARG=$OPTARG, OPTIND=$OPTIND, optc=$optc." + done + echo done +expected-stdout: + OPTARG=, OPTIND=2, optc=a. + OPTARG=, OPTIND=3, optc=a. + OPTARG=x, OPTIND=4, optc=?. + done +--- +name: utilities-getopts-2 +description: + Check OPTARG +stdin: + set -- -a Mary -x + while getopts a: optc; do + echo "OPTARG=$OPTARG, OPTIND=$OPTIND, optc=$optc." + done + echo done +expected-stdout: + OPTARG=Mary, OPTIND=3, optc=a. + OPTARG=, OPTIND=4, optc=?. + done +expected-stderr-pattern: /.*-x.*option/ +--- +name: utilities-getopts-3 +description: + Check unsetting OPTARG +stdin: + set -- -x arg -y + getopts x:y opt && echo "${OPTARG-unset}" + getopts x:y opt && echo "${OPTARG-unset}" +expected-stdout: + arg + unset +--- +name: wcswidth-1 +description: + Check the new wcswidth feature +stdin: + s=何 + set +U + print octets: ${#s} . + print 8-bit width: ${%s} . + set -U + print characters: ${#s} . + print columns: ${%s} . + s=� + set +U + print octets: ${#s} . + print 8-bit width: ${%s} . + set -U + print characters: ${#s} . + print columns: ${%s} . +expected-stdout: + octets: 3 . + 8-bit width: -1 . + characters: 1 . + columns: 2 . + octets: 3 . + 8-bit width: 3 . + characters: 1 . + columns: 1 . +--- +name: wcswidth-2 +description: + Check some corner cases +stdin: + print % $% . + set -U + x='a b' + print c ${%x} . + set +U + x='a b' + print d ${%x} . +expected-stdout: + % $% . + c -1 . + d -1 . +--- +name: wcswidth-3 +description: + Check some corner cases +stdin: + print ${%} . +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: wcswidth-4a +description: + Check some corner cases +stdin: + print ${%*} . +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: wcswidth-4b +description: + Check some corner cases +stdin: + print ${%@} . +expected-stderr-pattern: + /bad substitution/ +expected-exit: 1 +--- +name: wcswidth-4c +description: + Check some corner cases +stdin: + : + print ${%?} . +expected-stdout: + 1 . +--- +name: realpath-1 +description: + Check proper return values for realpath +category: os:mirbsd +stdin: + wd=$(realpath .) + mkdir dir + :>file + :>dir/file + ln -s dir lndir + ln -s file lnfile + ln -s nix lnnix + ln -s . lnself + i=0 + chk() { + typeset x y + x=$(realpath "$wd/$1" 2>&1); y=$? + print $((++i)) "?$1" =${x##*$wd/} !$y + } + chk dir + chk dir/ + chk dir/file + chk dir/nix + chk file + chk file/ + chk file/file + chk file/nix + chk nix + chk nix/ + chk nix/file + chk nix/nix + chk lndir + chk lndir/ + chk lndir/file + chk lndir/nix + chk lnfile + chk lnfile/ + chk lnfile/file + chk lnfile/nix + chk lnnix + chk lnnix/ + chk lnnix/file + chk lnnix/nix + chk lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself + rm lnself +expected-stdout: + 1 ?dir =dir !0 + 2 ?dir/ =dir !0 + 3 ?dir/file =dir/file !0 + 4 ?dir/nix =dir/nix !0 + 5 ?file =file !0 + 6 ?file/ =file/: Not a directory !20 + 7 ?file/file =file/file: Not a directory !20 + 8 ?file/nix =file/nix: Not a directory !20 + 9 ?nix =nix !0 + 10 ?nix/ =nix !0 + 11 ?nix/file =nix/file: No such file or directory !2 + 12 ?nix/nix =nix/nix: No such file or directory !2 + 13 ?lndir =dir !0 + 14 ?lndir/ =dir !0 + 15 ?lndir/file =dir/file !0 + 16 ?lndir/nix =dir/nix !0 + 17 ?lnfile =file !0 + 18 ?lnfile/ =lnfile/: Not a directory !20 + 19 ?lnfile/file =lnfile/file: Not a directory !20 + 20 ?lnfile/nix =lnfile/nix: Not a directory !20 + 21 ?lnnix =nix !0 + 22 ?lnnix/ =nix !0 + 23 ?lnnix/file =lnnix/file: No such file or directory !2 + 24 ?lnnix/nix =lnnix/nix: No such file or directory !2 + 25 ?lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself =lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself/lnself: Too many levels of symbolic links !62 +--- +name: realpath-2 +description: + Ensure that exactly two leading slashes are not collapsed + POSIX guarantees this exception, e.g. for UNC paths on Cygwin +category: os:mirbsd +stdin: + ln -s /bin t1 + ln -s //bin t2 + ln -s ///bin t3 + realpath /bin + realpath //bin + realpath ///bin + realpath /usr/bin + realpath /usr//bin + realpath /usr///bin + realpath t1 + realpath t2 + realpath t3 + rm -f t1 t2 t3 + cd //usr/bin + pwd + cd ../lib + pwd + realpath //usr/include/../bin +expected-stdout: + /bin + //bin + /bin + /usr/bin + /usr/bin + /usr/bin + /bin + //bin + /bin + //usr/bin + //usr/lib + //usr/bin +--- +name: crash-1 +description: + Crashed during March 2011, fixed on vernal equinōx ☺ +category: os:mirbsd,os:openbsd +stdin: + export MALLOC_OPTIONS=FGJPRSX + "$__progname" -c 'x=$(tr z r <<?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377\u00A0\u20AC\uFFFD\357\277\276\357\277\277\360\220\200\200.' +--- +name: duffs-device-ebcdic +description: + Check that the compiler did not optimise-break them +category: !shell:ebcdic-no +stdin: + set +U + s= + typeset -i1 i=0 + while (( ++i < 256 )); do + s+=${i#1#} + done + #s+=$'\xC2\xA0\xE2\x82\xAC\xEF\xBF\xBD\xEF\xBF\xBE\xEF\xBF\xBF\xF0\x90\x80\x80.' #XXX + typeset -p s +expected-stdout: + typeset s=$'\001\002\003\004\t\006\007\010\011\012\v\f\r\016\017\020\021\022\023\024\n\b\027\030\031\032\033\034\035\036\037\040\041\042\043\044\045\046\E\050\051\052\053\054\055\056\a\060\061\062\063\064\065\066\067\070\071\072\073\074\075\076\077 ���������.<(+|&���������!$*);^-/�������Ѧ,%_>?���������`:#@\175="�abcdefghi�������jklmnopqr���Ƥ�~stuvwxyz���[ޮ����������ݨ�]��{ABCDEFGHI������}JKLMNOPQR������\\�STUVWXYZ������0123456789�����\377' +--- +name: duffs-device-faux-EBCDIC +description: + Check that the compiler did not optimise-break them +category: shell:faux-ebcdic +stdin: + set +U + s= + typeset -i1 i=0 + while (( ++i < 256 )); do + s+=${i#1#} + done + s+=$'\xC2\xA0\xE2\x82\xAC\xEF\xBF\xBD\xEF\xBF\xBE\xEF\xBF\xBF\xF0\x90\x80\x80.' + typeset -p s +expected-stdout: + typeset s=$'\001\002\003\004\005\006\a\b\t\n\v\f\r\016\017\020\021\022\023\024\025\026\027\030\031\032\E\034\035\036\037 !"#$%&\047()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237������������������������������������������������������������������������������������������������\u00A0\u20AC\uFFFD￾￿�\220\200\200.' +--- +name: stateptr-underflow +description: + This check overflows an Xrestpos stored in a short in R40 +category: fastbox +stdin: + function Lb64decode { + [[ -o utf8-mode ]]; local u=$? + set +U + local c s="$*" t= + [[ -n $s ]] || { s=$(cat;print x); s=${s%x}; } + local -i i=0 n=${#s} p=0 v x + local -i16 o + + while (( i < n )); do + c=${s:(i++):1} + case $c { + (=) break ;; + ([A-Z]) (( v = 1#$c - 65 )) ;; + ([a-z]) (( v = 1#$c - 71 )) ;; + ([0-9]) (( v = 1#$c + 4 )) ;; + (+) v=62 ;; + (/) v=63 ;; + (*) continue ;; + } + (( x = (x << 6) | v )) + case $((p++)) { + (0) continue ;; + (1) (( o = (x >> 4) & 255 )) ;; + (2) (( o = (x >> 2) & 255 )) ;; + (3) (( o = x & 255 )) + p=0 + ;; + } + t=$t\\x${o#16#} + done + print -n $t + (( u )) || set -U + } + + i=-1 + s= + while (( ++i < 12120 )); do + s+=a + done + Lb64decode $s >/dev/null +--- +name: xtrace-1 +description: + Check that "set -x" doesn't redirect too quickly +stdin: + print '#!'"$__progname" >bash + cat >>bash <<'EOF' + echo 'GNU bash, version 2.05b.0(1)-release (i386-ecce-mirbsd10) + Copyright (C) 2002 Free Software Foundation, Inc.' + EOF + chmod +x bash + "$__progname" -xc 'foo=$(./bash --version 2>&1 | sed q); echo "=$foo="' +expected-stdout: + =GNU bash, version 2.05b.0(1)-release (i386-ecce-mirbsd10)= +expected-stderr-pattern: + /.*/ +--- +name: xtrace-2 +description: + Check that "set -x" is off during PS4 expansion +stdin: + f() { + print -n "(f1:$-)" + set -x + print -n "(f2:$-)" + } + PS4='[(p:$-)$(f)] ' + print "(o0:$-)" + set -x -o inherit-xtrace + print "(o1:$-)" + set +x + print "(o2:$-)" +expected-stdout: + (o0:sh) + (o1:shx) + (o2:sh) +expected-stderr: + [(p:sh)(f1:sh)(f2:sh)] print '(o1:shx)' + [(p:sh)(f1:sh)(f2:sh)] set +x +--- +name: fksh-flags +description: + Check that FKSH functions have their own shell flags +category: shell:legacy-no +stdin: + [[ $KSH_VERSION = Version* ]] && set +B + function foo { + set +f + set -e + echo 2 "${-/s}" . + } + set -fh + echo 1 "${-/s}" . + foo + echo 3 "${-/s}" . +expected-stdout: + 1 fh . + 2 eh . + 3 fh . +--- +name: fksh-flags-legacy +description: + Check that even FKSH functions share the shell flags +category: shell:legacy-yes +stdin: + [[ $KSH_VERSION = Version* ]] && set +B + foo() { + set +f + set -e + echo 2 "${-/s}" . + } + set -fh + echo 1 "${-/s}" . + foo + echo 3 "${-/s}" . +expected-stdout: + 1 fh . + 2 eh . + 3 eh . +--- +name: fsh-flags +description: + Check that !FKSH functions share the shell flags +stdin: + [[ $KSH_VERSION = Version* ]] && set +B + foo() { + set +f + set -e + echo 2 "${-/s}" . + } + set -fh + echo 1 "${-/s}" . + foo + echo 3 "${-/s}" . +expected-stdout: + 1 fh . + 2 eh . + 3 eh . +--- diff --git a/dot.mkshrc b/dot.mkshrc new file mode 100644 index 0000000..4a3dfea --- /dev/null +++ b/dot.mkshrc @@ -0,0 +1,629 @@ +# $Id$ +# $MirOS: src/bin/mksh/dot.mkshrc,v 1.121 2017/08/08 21:10:21 tg Exp $ +#- +# Copyright (c) 2002, 2003, 2004, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016, 2017 +# mirabilos +# +# Provided that these terms and disclaimer and all copyright notices +# are retained or reproduced in an accompanying document, permission +# is granted to deal in this work without restriction, including un- +# limited rights to use, publicly perform, distribute, sell, modify, +# merge, give away, or sublicence. +# +# This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to +# the utmost extent permitted by applicable law, neither express nor +# implied; without malicious intent or gross negligence. In no event +# may a licensor, author or contributor be held liable for indirect, +# direct, other damage, loss, or other issues arising in any way out +# of dealing in the work, even if advised of the possibility of such +# damage or existence of a defect, except proven that it results out +# of said person's immediate fault when using the work as intended. +#- +# ${ENV:-~/.mkshrc}: mksh initialisation file for interactive shells + +# catch non-mksh, non-lksh, trying to run this file +case ${KSH_VERSION:-} in +*LEGACY\ KSH*|*MIRBSD\ KSH*) ;; +*) \return 0 ;; +esac + +# give MidnightBSD's laffer1 a bit of csh feeling +function setenv { + if (( $# )); then + \\builtin eval '\\builtin export "$1"="${2:-}"' + else + \\builtin typeset -x + fi +} + +# pager (not control character safe) +smores() ( + \\builtin set +m + \\builtin cat "$@" |& + \\builtin trap "rv=\$?; \\\\builtin kill $! >/dev/null 2>&1; \\\\builtin exit \$rv" EXIT + while IFS= \\builtin read -pr line; do + llen=${%line} + (( llen == -1 )) && llen=${#line} + (( llen = llen ? (llen + COLUMNS - 1) / COLUMNS : 1 )) + if (( (curlin += llen) >= LINES )); then + \\builtin print -nr -- $'\e[7m--more--\e[0m' + \\builtin read -u1 || \\builtin exit $? + [[ $REPLY = [Qq]* ]] && \\builtin exit 0 + curlin=$llen + fi + \\builtin print -r -- "$line" + done +) + +# customise your favourite editor here; the first one found is used +for EDITOR in "${EDITOR:-}" jupp jstar mcedit ed vi; do + EDITOR=$(\\builtin whence -p "$EDITOR") || EDITOR= + [[ -n $EDITOR && -x $EDITOR ]] && break + EDITOR= +done + +\\builtin alias ls=ls l='ls -F' la='l -a' ll='l -l' lo='l -alo' +\: "${HOSTNAME:=$(\\builtin ulimit -c 0; \\builtin print -r -- $(hostname \ + 2>/dev/null))}${EDITOR:=/bin/ed}${TERM:=vt100}${USER:=$(\\builtin ulimit \ + -c 0; id -un 2>/dev/null)}${USER:=?}" +[[ $HOSTNAME = ?(?(ip6-)localhost?(6)) ]] && HOSTNAME=nil; \\builtin unalias ls +\\builtin export EDITOR HOSTNAME TERM USER + +# minimal support for lksh users +if [[ $KSH_VERSION = *LEGACY\ KSH* ]]; then + PS1='$USER@${HOSTNAME%%.*}:$PWD>' + \\builtin return 0 +fi + +# mksh-specific from here +\: "${MKSH:=$(\\builtin whence -p mksh)}${MKSH:=/bin/mksh}" +\\builtin export MKSH + +# prompts +PS4='[$EPOCHREALTIME] '; PS1='#'; (( USER_ID )) && PS1='$'; PS1=$'\001\r''${| + \\builtin typeset e=$? + + (( e )) && REPLY+="$e|" + REPLY+=${USER}@${HOSTNAME%%.*}: + + \\builtin typeset d=${PWD:-?}/ p=~; [[ $p = ?(*/) ]] || d=${d/#$p\//\~/} + d=${d%/}; \\builtin typeset m=${%d} n p=...; (( m > 0 )) || m=${#d} + (( m > (n = (COLUMNS/3 < 7 ? 7 : COLUMNS/3)) )) && d=${d:(-n)} || p= + REPLY+=$p$d + + \\builtin return $e +} '"$PS1 " + +# utilities +\\builtin alias doch='sudo mksh -c "$(\\builtin fc -ln -1)"' +\\builtin command -v rot13 >/dev/null || \\builtin alias rot13='tr \ + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \ + nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM' +if \\builtin command -v hd >/dev/null; then + \: +elif \\builtin command -v hexdump >/dev/null; then + function hd { + hexdump -e '"%08.8_ax " 8/1 "%02X " " - " 8/1 "%02X "' \ + -e '" |" "%_p"' -e '"|\n"' "$@" + } +else + function hd { + \\builtin cat "$@" | hd_mksh "$@" + } +fi + +# NUL-safe and EBCDIC-safe hexdump (from stdin) +function hd_mksh { + \\builtin typeset -Uui16 -Z11 pos=0 + \\builtin typeset -Uui16 -Z5 hv=2147483647 + \\builtin typeset dasc dn line i + \\builtin set +U + + while \\builtin read -arn 512 line; do + \\builtin typeset -i1 'line[*]' + i=0 + while (( i < ${#line[*]} )); do + dn= + (( (hv = line[i++]) != 0 )) && dn=${line[i-1]#1#} + if (( (pos & 15) == 0 )); then + (( pos )) && \ + \\builtin print -r -- "$dasc|" + \\builtin print -nr "${pos#16#} " + dasc=' |' + fi + \\builtin print -nr "${hv#16#} " + if [[ $dn = [[:print:]] ]]; then + dasc+=$dn + else + dasc+=. + fi + (( (pos++ & 15) == 7 )) && \ + \\builtin print -nr -- '- ' + done + done + while (( pos & 15 )); do + \\builtin print -nr ' ' + (( (pos++ & 15) == 7 )) && \ + \\builtin print -nr -- '- ' + done + (( hv == 2147483647 )) || \\builtin print -r -- "$dasc|" +} + +# Berkeley C shell compatible dirs, popd, and pushd functions +# Z shell compatible chpwd() hook, used to update DIRSTACK[0] +DIRSTACKBASE=$(\\builtin realpath ~/. 2>/dev/null || \ + \\builtin print -nr -- "${HOME:-/}") +\\builtin set -A DIRSTACK +function chpwd { + DIRSTACK[0]=$(\\builtin realpath . 2>/dev/null || \ + \\builtin print -nr -- "$PWD") + [[ $DIRSTACKBASE = ?(*/) ]] || \ + DIRSTACK[0]=${DIRSTACK[0]/#$DIRSTACKBASE/\~} + \: +} +\chpwd . +cd() { + \\builtin cd "$@" || \\builtin return $? + \chpwd "$@" +} +function cd_csh { + \\builtin typeset d t=${1/#\~/$DIRSTACKBASE} + + if ! d=$(\\builtin cd "$t" 2>&1); then + \\builtin print -ru2 "${1}: ${d##*cd: $t: }." + \\builtin return 1 + fi + \cd "$t" +} +function dirs { + \\builtin typeset d dwidth + \\builtin typeset -i fl=0 fv=0 fn=0 cpos=0 + + while \\builtin getopts ":lvn" d; do + case $d { + (l) fl=1 ;; + (v) fv=1 ;; + (n) fn=1 ;; + (*) \\builtin print -ru2 'Usage: dirs [-lvn].' + \\builtin return 1 ;; + } + done + \\builtin shift $((OPTIND - 1)) + if (( $# > 0 )); then + \\builtin print -ru2 'Usage: dirs [-lvn].' + \\builtin return 1 + fi + if (( fv )); then + fv=0 + while (( fv < ${#DIRSTACK[*]} )); do + d=${DIRSTACK[fv]} + (( fl )) && d=${d/#\~/$DIRSTACKBASE} + \\builtin print -r -- "$fv $d" + (( ++fv )) + done + else + fv=0 + while (( fv < ${#DIRSTACK[*]} )); do + d=${DIRSTACK[fv]} + (( fl )) && d=${d/#\~/$DIRSTACKBASE} + (( dwidth = (${%d} > 0 ? ${%d} : ${#d}) )) + if (( fn && (cpos += dwidth + 1) >= 79 && \ + dwidth < 80 )); then + \\builtin print + (( cpos = dwidth + 1 )) + fi + \\builtin print -nr -- "$d " + (( ++fv )) + done + \\builtin print + fi + \\builtin return 0 +} +function popd { + \\builtin typeset d fa + \\builtin typeset -i n=1 + + while \\builtin getopts ":0123456789lvn" d; do + case $d { + (l|v|n) fa+=" -$d" ;; + (+*) n=2 + \\builtin break ;; + (*) \\builtin print -ru2 'Usage: popd [-lvn] [+].' + \\builtin return 1 ;; + } + done + \\builtin shift $((OPTIND - n)) + n=0 + if (( $# > 1 )); then + \\builtin print -ru2 popd: Too many arguments. + \\builtin return 1 + elif [[ $1 = ++([0-9]) && $1 != +0 ]]; then + if (( (n = ${1#+}) >= ${#DIRSTACK[*]} )); then + \\builtin print -ru2 popd: Directory stack not that deep. + \\builtin return 1 + fi + elif [[ -n $1 ]]; then + \\builtin print -ru2 popd: Bad directory. + \\builtin return 1 + fi + if (( ${#DIRSTACK[*]} < 2 )); then + \\builtin print -ru2 popd: Directory stack empty. + \\builtin return 1 + fi + \\builtin unset DIRSTACK[n] + \\builtin set -A DIRSTACK -- "${DIRSTACK[@]}" + \cd_csh "${DIRSTACK[0]}" || \\builtin return 1 + \dirs $fa +} +function pushd { + \\builtin typeset d fa + \\builtin typeset -i n=1 + + while \\builtin getopts ":0123456789lvn" d; do + case $d { + (l|v|n) fa+=" -$d" ;; + (+*) n=2 + \\builtin break ;; + (*) \\builtin print -ru2 'Usage: pushd [-lvn] [|+].' + \\builtin return 1 ;; + } + done + \\builtin shift $((OPTIND - n)) + if (( $# == 0 )); then + if (( ${#DIRSTACK[*]} < 2 )); then + \\builtin print -ru2 pushd: No other directory. + \\builtin return 1 + fi + d=${DIRSTACK[1]} + DIRSTACK[1]=${DIRSTACK[0]} + \cd_csh "$d" || \\builtin return 1 + elif (( $# > 1 )); then + \\builtin print -ru2 pushd: Too many arguments. + \\builtin return 1 + elif [[ $1 = ++([0-9]) && $1 != +0 ]]; then + if (( (n = ${1#+}) >= ${#DIRSTACK[*]} )); then + \\builtin print -ru2 pushd: Directory stack not that deep. + \\builtin return 1 + fi + while (( n-- )); do + d=${DIRSTACK[0]} + \\builtin unset DIRSTACK[0] + \\builtin set -A DIRSTACK -- "${DIRSTACK[@]}" "$d" + done + \cd_csh "${DIRSTACK[0]}" || \\builtin return 1 + else + \\builtin set -A DIRSTACK -- placeholder "${DIRSTACK[@]}" + \cd_csh "$1" || \\builtin return 1 + fi + \dirs $fa +} + +# base64 encoder and decoder, RFC compliant, NUL safe, not EBCDIC safe +function Lb64decode { + \\builtin set +U + \\builtin typeset c s="$*" t + [[ -n $s ]] || { s=$(\\builtin cat; \\builtin print x); s=${s%x}; } + \\builtin typeset -i i=0 j=0 n=${#s} p=0 v x + \\builtin typeset -i16 o + + while (( i < n )); do + c=${s:(i++):1} + case $c { + (=) \\builtin break ;; + ([A-Z]) (( v = 1#$c - 65 )) ;; + ([a-z]) (( v = 1#$c - 71 )) ;; + ([0-9]) (( v = 1#$c + 4 )) ;; + (+) v=62 ;; + (/) v=63 ;; + (*) \\builtin continue ;; + } + (( x = (x << 6) | v )) + case $((p++)) { + (0) \\builtin continue ;; + (1) (( o = (x >> 4) & 255 )) ;; + (2) (( o = (x >> 2) & 255 )) ;; + (3) (( o = x & 255 )) + p=0 + ;; + } + t+=\\x${o#16#} + (( ++j & 4095 )) && \\builtin continue + \\builtin print -n $t + t= + done + \\builtin print -n $t +} +function Lb64encode { + \\builtin set +U + \\builtin typeset c s t table + \\builtin set -A table -- A B C D E F G H I J K L M N O P Q R S T U V W X Y Z \ + a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 + / + if (( $# )); then + \\builtin read -raN-1 s <<<"$*" + \\builtin unset s[${#s[*]}-1] + else + \\builtin read -raN-1 s + fi + \\builtin typeset -i i=0 n=${#s[*]} v + + while (( i < n )); do + (( v = s[i++] << 16 )) + (( v |= s[i++] << 8 )) + (( v |= s[i++] )) + t+=${table[v >> 18]}${table[v >> 12 & 63]} + c=${table[v >> 6 & 63]} + if (( i <= n )); then + t+=$c${table[v & 63]} + elif (( i == n + 1 )); then + t+=$c= + else + t+=== + fi + if (( ${#t} == 76 || i >= n )); then + \\builtin print -r $t + t= + fi + done +} + +# Better Avalanche for the Jenkins Hash +\\builtin typeset -Z11 -Uui16 Lbafh_v +function Lbafh_init { + Lbafh_v=0 +} +function Lbafh_add { + \\builtin set +U + \\builtin typeset s + if (( $# )); then + \\builtin read -raN-1 s <<<"$*" + \\builtin unset s[${#s[*]}-1] + else + \\builtin read -raN-1 s + fi + \\builtin typeset -i i=0 n=${#s[*]} + + while (( i < n )); do + ((# Lbafh_v = (Lbafh_v + s[i++] + 1) * 1025 )) + ((# Lbafh_v ^= Lbafh_v >> 6 )) + done +} +function Lbafh_finish { + \\builtin typeset -Ui t + + ((# t = (((Lbafh_v >> 7) & 0x01010101) * 0x1B) ^ \ + ((Lbafh_v << 1) & 0xFEFEFEFE) )) + ((# Lbafh_v = t ^ (t ^> 8) ^ (Lbafh_v ^> 8) ^ \ + (Lbafh_v ^> 16) ^ (Lbafh_v ^> 24) )) + \: +} + +# strip comments (and leading/trailing whitespace if IFS is set) from +# any file(s) given as argument, or stdin if none, and spew to stdout +function Lstripcom { + \\builtin set -o noglob + \\builtin cat "$@" | while \\builtin read _line; do + _line=${_line%%#*} + [[ -n $_line ]] && \\builtin print -r -- $_line + done +} + +# toggle built-in aliases and utilities, and aliases and functions from mkshrc +function enable { + \\builtin typeset doprnt=0 mode=1 x y z rv=0 + \\builtin typeset b_alias i_alias i_func nalias=0 nfunc=0 i_all + \\builtin set -A b_alias + \\builtin set -A i_alias + \\builtin set -A i_func + + # accumulate mksh built-in aliases, in ASCIIbetical order + i_alias[nalias]=autoload; b_alias[nalias++]='\\builtin typeset -fu' + i_alias[nalias]=functions; b_alias[nalias++]='\\builtin typeset -f' + i_alias[nalias]=hash; b_alias[nalias++]='\\builtin alias -t' + i_alias[nalias]=history; b_alias[nalias++]='\\builtin fc -l' + i_alias[nalias]=integer; b_alias[nalias++]='\\builtin typeset -i' + i_alias[nalias]=local; b_alias[nalias++]='\\builtin typeset' + i_alias[nalias]=login; b_alias[nalias++]='\\builtin exec login' + i_alias[nalias]=nameref; b_alias[nalias++]='\\builtin typeset -n' + i_alias[nalias]=nohup; b_alias[nalias++]='nohup ' + i_alias[nalias]=r; b_alias[nalias++]='\\builtin fc -e -' + i_alias[nalias]=type; b_alias[nalias++]='\\builtin whence -v' + + # accumulate mksh built-in utilities, in definition order, even ifndef + i_func[nfunc++]=. + i_func[nfunc++]=: + i_func[nfunc++]='[' + i_func[nfunc++]=alias + i_func[nfunc++]=break + # \\builtin cannot, by design, be overridden + i_func[nfunc++]=builtin + i_func[nfunc++]=cat + i_func[nfunc++]=cd + i_func[nfunc++]=chdir + i_func[nfunc++]=command + i_func[nfunc++]=continue + i_func[nfunc++]=echo + i_func[nfunc++]=eval + i_func[nfunc++]=exec + i_func[nfunc++]=exit + i_func[nfunc++]=export + i_func[nfunc++]=false + i_func[nfunc++]=fc + i_func[nfunc++]=getopts + i_func[nfunc++]=global + i_func[nfunc++]=jobs + i_func[nfunc++]=kill + i_func[nfunc++]=let + i_func[nfunc++]=print + i_func[nfunc++]=pwd + i_func[nfunc++]=read + i_func[nfunc++]=readonly + i_func[nfunc++]=realpath + i_func[nfunc++]=rename + i_func[nfunc++]=return + i_func[nfunc++]=set + i_func[nfunc++]=shift + i_func[nfunc++]=source + i_func[nfunc++]=suspend + i_func[nfunc++]=test + i_func[nfunc++]=times + i_func[nfunc++]=trap + i_func[nfunc++]=true + i_func[nfunc++]=typeset + i_func[nfunc++]=ulimit + i_func[nfunc++]=umask + i_func[nfunc++]=unalias + i_func[nfunc++]=unset + i_func[nfunc++]=wait + i_func[nfunc++]=whence + i_func[nfunc++]=bg + i_func[nfunc++]=fg + i_func[nfunc++]=bind + i_func[nfunc++]=mknod + i_func[nfunc++]=printf + i_func[nfunc++]=sleep + i_func[nfunc++]=domainname + i_func[nfunc++]=extproc + + # accumulate aliases from dot.mkshrc, in definition order + i_alias[nalias]=l; b_alias[nalias++]='ls -F' + i_alias[nalias]=la; b_alias[nalias++]='l -a' + i_alias[nalias]=ll; b_alias[nalias++]='l -l' + i_alias[nalias]=lo; b_alias[nalias++]='l -alo' + i_alias[nalias]=doch; b_alias[nalias++]='sudo mksh -c "$(\\builtin fc -ln -1)"' + i_alias[nalias]=rot13; b_alias[nalias++]='tr abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM' + i_alias[nalias]=cls; b_alias[nalias++]='\\builtin print -n \\ec' + + # accumulate functions from dot.mkshrc, in definition order + i_func[nfunc++]=setenv + i_func[nfunc++]=smores + i_func[nfunc++]=hd + i_func[nfunc++]=hd_mksh + i_func[nfunc++]=chpwd + i_func[nfunc++]=cd + i_func[nfunc++]=cd_csh + i_func[nfunc++]=dirs + i_func[nfunc++]=popd + i_func[nfunc++]=pushd + i_func[nfunc++]=Lb64decode + i_func[nfunc++]=Lb64encode + i_func[nfunc++]=Lbafh_init + i_func[nfunc++]=Lbafh_add + i_func[nfunc++]=Lbafh_finish + i_func[nfunc++]=Lstripcom + i_func[nfunc++]=enable + + # collect all identifiers, sorted ASCIIbetically + \\builtin set -sA i_all -- "${i_alias[@]}" "${i_func[@]}" + + # handle options, we don't do dynamic loading + while \\builtin getopts "adf:nps" x; do + case $x { + (a) + mode=-1 + ;; + (d) + # deliberately causing an error, like bash-static + ;| + (f) + \\builtin print -ru2 enable: dynamic loading not available + \\builtin return 2 + ;; + (n) + mode=0 + ;; + (p) + doprnt=1 + ;; + (s) + \\builtin set -sA i_all -- . : break continue eval \ + exec exit export readonly return set shift times \ + trap unset + ;; + (*) + \\builtin print -ru2 enable: usage: \ + "enable [-adnps] [-f filename] [name ...]" + return 2 + ;; + } + done + \\builtin shift $((OPTIND - 1)) + + # display builtins enabled/disabled/all/special? + if (( doprnt || ($# == 0) )); then + for x in "${i_all[@]}"; do + y=$(\\builtin alias "$x") || y= + [[ $y = "$x='\\\\builtin whence -p $x >/dev/null || (\\\\builtin print -r mksh: $x: not found; \\\\builtin exit 127) && \$(\\\\builtin whence -p $x)'" ]]; z=$? + case $mode:$z { + (-1:0|0:0) + \\builtin print -r -- "enable -n $x" + ;; + (-1:1|1:1) + \\builtin print -r -- "enable $x" + ;; + } + done + \\builtin return 0 + fi + + for x in "$@"; do + z=0 + for y in "${i_alias[@]}" "${i_func[@]}"; do + [[ $x = "$y" ]] || \\builtin continue + z=1 + \\builtin break + done + if (( !z )); then + \\builtin print -ru2 enable: "$x": not a shell builtin + rv=1 + \\builtin continue + fi + if (( !mode )); then + # disable this + \\builtin alias "$x=\\\\builtin whence -p $x >/dev/null || (\\\\builtin print -r mksh: $x: not found; \\\\builtin exit 127) && \$(\\\\builtin whence -p $x)" + else + # find out if this is an alias or not, first + z=0 + y=-1 + while (( ++y < nalias )); do + [[ $x = "${i_alias[y]}" ]] || \\builtin continue + z=1 + \\builtin break + done + if (( z )); then + # re-enable the original alias body + \\builtin alias "$x=${b_alias[y]}" + else + # re-enable the original utility/function + \\builtin unalias "$x" + fi + fi + done + \\builtin return $rv +} + +\: place customisations below this line + +# some defaults follow — you are supposed to adjust these to your +# liking; by default we add ~/.etc/bin and ~/bin (whichever exist) +# to $PATH, set $SHELL to mksh, set some defaults for man and less +# and show a few more possible things for users to begin moving in + +for p in ~/.etc/bin ~/bin; do + [[ -d $p/. ]] || \\builtin continue + [[ $PATHSEP$PATH$PATHSEP = *"$PATHSEP$p$PATHSEP"* ]] || \ + PATH=$p$PATHSEP$PATH +done + +\\builtin export SHELL=$MKSH MANWIDTH=80 LESSHISTFILE=- +\\builtin alias cls='\\builtin print -n \\ec' + +#\\builtin unset LANGUAGE LC_ADDRESS LC_ALL LC_COLLATE LC_IDENTIFICATION LC_MONETARY \ +# LC_NAME LC_NUMERIC LC_TELEPHONE LC_TIME +#p=en_GB.UTF-8 +#\\builtin export LANG=C LC_CTYPE=$p LC_MEASUREMENT=$p LC_MESSAGES=$p LC_PAPER=$p +#\\builtin set -U + +\\builtin unset p + +\: place customisations above this line diff --git a/edit.c b/edit.c new file mode 100644 index 0000000..0e51780 --- /dev/null +++ b/edit.c @@ -0,0 +1,5648 @@ +/* $OpenBSD: edit.c,v 1.41 2015/09/01 13:12:31 tedu Exp $ */ +/* $OpenBSD: edit.h,v 1.9 2011/05/30 17:14:35 martynas Exp $ */ +/* $OpenBSD: emacs.c,v 1.52 2015/09/10 22:48:58 nicm Exp $ */ +/* $OpenBSD: vi.c,v 1.30 2015/09/10 22:48:58 nicm Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +#ifndef MKSH_NO_CMDLINE_EDITING + +__RCSID("$MirOS: src/bin/mksh/edit.c,v 1.342 2018/01/14 00:03:00 tg Exp $"); + +/* + * in later versions we might use libtermcap for this, but since external + * dependencies are problematic, this has not yet been decided on; another + * good string is KSH_ESC_STRING "c" except on hardware terminals like the + * DEC VT420 which do a full power cycle then... + */ +#ifndef MKSH_CLS_STRING +#define MKSH_CLS_STRING KSH_ESC_STRING "[;H" KSH_ESC_STRING "[J" +#endif + +/* tty driver characters we are interested in */ +#define EDCHAR_DISABLED 0xFFFFU +#define EDCHAR_INITIAL 0xFFFEU +static struct { + unsigned short erase; + unsigned short kill; + unsigned short werase; + unsigned short intr; + unsigned short quit; + unsigned short eof; +} edchars; + +#define isched(x,e) ((unsigned short)(unsigned char)(x) == (e)) +#define isedchar(x) (!((x) & ~0xFF)) +#ifndef _POSIX_VDISABLE +#define toedchar(x) ((unsigned short)(unsigned char)(x)) +#else +#define toedchar(x) (((_POSIX_VDISABLE != -1) && ((x) == _POSIX_VDISABLE)) ? \ + ((unsigned short)EDCHAR_DISABLED) : \ + ((unsigned short)(unsigned char)(x))) +#endif + +/* x_cf_glob() flags */ +#define XCF_COMMAND BIT(0) /* Do command completion */ +#define XCF_FILE BIT(1) /* Do file completion */ +#define XCF_FULLPATH BIT(2) /* command completion: store full path */ +#define XCF_COMMAND_FILE (XCF_COMMAND | XCF_FILE) +#define XCF_IS_COMMAND BIT(3) /* return flag: is command */ +#define XCF_IS_NOSPACE BIT(4) /* return flag: do not append a space */ + +static char editmode; +static int xx_cols; /* for Emacs mode */ +static int modified; /* buffer has been "modified" */ +static char *holdbufp; /* place to hold last edit buffer */ + +/* 0=dumb 1=tmux (for now) */ +static uint8_t x_term_mode; + +static void x_adjust(void); +static int x_getc(void); +static void x_putcf(int); +static void x_modified(void); +static void x_mode(bool); +static int x_do_comment(char *, ssize_t, ssize_t *); +static void x_print_expansions(int, char * const *, bool); +static int x_cf_glob(int *, const char *, int, int, int *, int *, char ***); +static size_t x_longest_prefix(int, char * const *); +static void x_glob_hlp_add_qchar(char *); +static char *x_glob_hlp_tilde_and_rem_qchar(char *, bool); +static size_t x_basename(const char *, const char *); +static void x_free_words(int, char **); +static int x_escape(const char *, size_t, int (*)(const char *, size_t)); +static int x_emacs(char *); +static void x_init_prompt(bool); +#if !MKSH_S_NOVI +static int x_vi(char *); +#endif +static void x_intr(int, int) MKSH_A_NORETURN; + +#define x_flush() shf_flush(shl_out) +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +#define x_putc(c) x_putcf(c) +#else +#define x_putc(c) shf_putc((c), shl_out) +#endif + +static int path_order_cmp(const void *, const void *); +static void glob_table(const char *, XPtrV *, struct table *); +static void glob_path(int, const char *, XPtrV *, const char *); +static int x_file_glob(int *, char *, char ***); +static int x_command_glob(int, char *, char ***); +static int x_locate_word(const char *, int, int, int *, bool *); + +static int x_e_getmbc(char *); + +/* +++ generic editing functions +++ */ + +/* + * read an edited command line + */ +int +x_read(char *buf) +{ + int i; + + x_mode(true); + modified = 1; + if (Flag(FEMACS) || Flag(FGMACS)) + i = x_emacs(buf); +#if !MKSH_S_NOVI + else if (Flag(FVI)) + i = x_vi(buf); +#endif + else + /* internal error */ + i = -1; + editmode = 0; + x_mode(false); + return (i); +} + +/* tty I/O */ + +static int +x_getc(void) +{ +#ifdef __OS2__ + return (_read_kbd(0, 1, 0)); +#else + char c; + ssize_t n; + + while ((n = blocking_read(STDIN_FILENO, &c, 1)) < 0 && errno == EINTR) + if (trap) { + x_mode(false); + runtraps(0); +#ifdef SIGWINCH + if (got_winch) { + change_winsz(); + if (x_cols != xx_cols && editmode == 1) { + /* redraw line in Emacs mode */ + xx_cols = x_cols; + x_init_prompt(false); + x_adjust(); + } + } +#endif + x_mode(true); + } + return ((n == 1) ? (int)(unsigned char)c : -1); +#endif +} + +static void +x_putcf(int c) +{ + shf_putc(c, shl_out); +} + +/********************************* + * Misc common code for vi/emacs * + *********************************/ + +/*- + * Handle the commenting/uncommenting of a line. + * Returns: + * 1 if a carriage return is indicated (comment added) + * 0 if no return (comment removed) + * -1 if there is an error (not enough room for comment chars) + * If successful, *lenp contains the new length. Note: cursor should be + * moved to the start of the line after (un)commenting. + */ +static int +x_do_comment(char *buf, ssize_t bsize, ssize_t *lenp) +{ + ssize_t i, j, len = *lenp; + + if (len == 0) + /* somewhat arbitrary - it's what AT&T ksh does */ + return (1); + + /* Already commented? */ + if (buf[0] == '#') { + bool saw_nl = false; + + for (j = 0, i = 1; i < len; i++) { + if (!saw_nl || buf[i] != '#') + buf[j++] = buf[i]; + saw_nl = buf[i] == '\n'; + } + *lenp = j; + return (0); + } else { + int n = 1; + + /* See if there's room for the #s - 1 per \n */ + for (i = 0; i < len; i++) + if (buf[i] == '\n') + n++; + if (len + n >= bsize) + return (-1); + /* Now add them... */ + for (i = len, j = len + n; --i >= 0; ) { + if (buf[i] == '\n') + buf[--j] = '#'; + buf[--j] = buf[i]; + } + buf[0] = '#'; + *lenp += n; + return (1); + } +} + +/**************************************************** + * Common file/command completion code for vi/emacs * + ****************************************************/ + +static void +x_print_expansions(int nwords, char * const *words, bool is_command) +{ + bool use_copy = false; + size_t prefix_len; + XPtrV l = { NULL, 0, 0 }; + struct columnise_opts co; + + /* + * Check if all matches are in the same directory (in this + * case, we want to omit the directory name) + */ + if (!is_command && + (prefix_len = x_longest_prefix(nwords, words)) > 0) { + int i; + + /* Special case for 1 match (prefix is whole word) */ + if (nwords == 1) + prefix_len = x_basename(words[0], NULL); + /* Any (non-trailing) slashes in non-common word suffixes? */ + for (i = 0; i < nwords; i++) + if (x_basename(words[i] + prefix_len, NULL) > + prefix_len) + break; + /* All in same directory? */ + if (i == nwords) { + while (prefix_len > 0 && + !mksh_cdirsep(words[0][prefix_len - 1])) + prefix_len--; + use_copy = true; + XPinit(l, nwords + 1); + for (i = 0; i < nwords; i++) + XPput(l, words[i] + prefix_len); + XPput(l, NULL); + } + } + /* + * Enumerate expansions + */ + x_putc('\r'); + x_putc('\n'); + co.shf = shl_out; + co.linesep = '\n'; + co.do_last = true; + co.prefcol = false; + pr_list(&co, use_copy ? (char **)XPptrv(l) : words); + + if (use_copy) + /* not x_free_words() */ + XPfree(l); +} + +/* + * Convert backslash-escaped string to QCHAR-escaped + * string useful for globbing; loses QCHAR unless it + * can squeeze in, eg. by previous loss of backslash + */ +static void +x_glob_hlp_add_qchar(char *cp) +{ + char ch, *dp = cp; + bool escaping = false; + + while ((ch = *cp++)) { + if (ch == '\\' && !escaping) { + escaping = true; + continue; + } + if (escaping || (ch == QCHAR && (cp - dp) > 1)) { + /* + * empirically made list of chars to escape + * for globbing as well as QCHAR itself + */ + switch (ord(ch)) { + case QCHAR: + case ORD('$'): + case ORD('*'): + case ORD('?'): + case ORD('['): + case ORD('\\'): + case ORD('`'): + *dp++ = QCHAR; + break; + } + escaping = false; + } + *dp++ = ch; + } + *dp = '\0'; +} + +/* + * Run tilde expansion on argument string, return the result + * after unescaping; if the flag is set, the original string + * is freed if changed and assumed backslash-escaped, if not + * it is assumed QCHAR-escaped + */ +static char * +x_glob_hlp_tilde_and_rem_qchar(char *s, bool magic_flag) +{ + char ch, *cp, *dp; + + /* + * On the string, check whether we have a tilde expansion, + * and if so, discern "~foo/bar" and "~/baz" from "~blah"; + * if we have a directory part (the former), try to expand + */ + if (*s == '~' && (cp = /* not sdirsep */ strchr(s, '/')) != NULL) { + /* ok, so split into "~foo"/"bar" or "~"/"baz" */ + *cp++ = 0; + /* try to expand the tilde */ + if (!(dp = do_tilde(s + 1))) { + /* nope, revert damage */ + *--cp = '/'; + } else { + /* ok, expand and replace */ + cp = shf_smprintf(Tf_sSs, dp, cp); + if (magic_flag) + afree(s, ATEMP); + s = cp; + } + } + + /* ... convert it from backslash-escaped via QCHAR-escaped... */ + if (magic_flag) + x_glob_hlp_add_qchar(s); + /* ... to unescaped, for comparison with the matches */ + cp = dp = s; + + while ((ch = *cp++)) { + if (ch == QCHAR && !(ch = *cp++)) + break; + *dp++ = ch; + } + *dp = '\0'; + + return (s); +} + +/** + * Do file globbing: + * - does expansion, checks for no match, etc. + * - sets *wordsp to array of matching strings + * - returns number of matching strings + */ +static int +x_file_glob(int *flagsp, char *toglob, char ***wordsp) +{ + char **words, *cp; + int nwords; + XPtrV w; + struct source *s, *sold; + + /* remove all escaping backward slashes */ + x_glob_hlp_add_qchar(toglob); + + /* + * Convert "foo*" (toglob) to an array of strings (words) + */ + sold = source; + s = pushs(SWSTR, ATEMP); + s->start = s->str = toglob; + source = s; + if (yylex(ONEWORD | LQCHAR) != LWORD) { + source = sold; + internal_warningf(Tfg_badsubst); + return (0); + } + source = sold; + afree(s, ATEMP); + XPinit(w, 32); + cp = yylval.cp; + while (*cp == CHAR || *cp == QCHAR) + cp += 2; + nwords = DOGLOB | DOTILDE | DOMARKDIRS; + if (*cp != EOS) { + /* probably a $FOO expansion */ + *flagsp |= XCF_IS_NOSPACE; + /* this always results in at most one match */ + nwords = 0; + } + expand(yylval.cp, &w, nwords); + XPput(w, NULL); + words = (char **)XPclose(w); + + for (nwords = 0; words[nwords]; nwords++) + ; + if (nwords == 1) { + struct stat statb; + + /* Expand any tilde and drop all QCHAR for comparison */ + toglob = x_glob_hlp_tilde_and_rem_qchar(toglob, false); + + /* + * Check if globbing failed (returned glob pattern), + * but be careful (e.g. toglob == "ab*" when the file + * "ab*" exists is not an error). + * Also, check for empty result - happens if we tried + * to glob something which evaluated to an empty + * string (e.g., "$FOO" when there is no FOO, etc). + */ + if ((strcmp(words[0], toglob) == 0 && + stat(words[0], &statb) < 0) || + words[0][0] == '\0') { + x_free_words(nwords, words); + words = NULL; + nwords = 0; + } + } + + if ((*wordsp = nwords ? words : NULL) == NULL && words != NULL) + x_free_words(nwords, words); + + return (nwords); +} + +/* Data structure used in x_command_glob() */ +struct path_order_info { + char *word; + size_t base; + size_t path_order; +}; + +/* Compare routine used in x_command_glob() */ +static int +path_order_cmp(const void *aa, const void *bb) +{ + const struct path_order_info *a = (const struct path_order_info *)aa; + const struct path_order_info *b = (const struct path_order_info *)bb; + int t; + + if ((t = ascstrcmp(a->word + a->base, b->word + b->base))) + return (t); + if (a->path_order > b->path_order) + return (1); + if (a->path_order < b->path_order) + return (-1); + return (0); +} + +static int +x_command_glob(int flags, char *toglob, char ***wordsp) +{ + char *pat, *fpath; + size_t nwords; + XPtrV w; + struct block *l; + + /* Convert "foo*" (toglob) to a pattern for future use */ + pat = evalstr(toglob, DOPAT | DOTILDE); + + XPinit(w, 32); + + glob_table(pat, &w, &keywords); + glob_table(pat, &w, &aliases); + glob_table(pat, &w, &builtins); + for (l = e->loc; l; l = l->next) + glob_table(pat, &w, &l->funs); + + glob_path(flags, pat, &w, path); + if ((fpath = str_val(global(TFPATH))) != null) + glob_path(flags, pat, &w, fpath); + + nwords = XPsize(w); + + if (!nwords) { + *wordsp = NULL; + XPfree(w); + return (0); + } + /* Sort entries */ + if (flags & XCF_FULLPATH) { + /* Sort by basename, then path order */ + struct path_order_info *info, *last_info = NULL; + char **words = (char **)XPptrv(w); + size_t i, path_order = 0; + + info = (struct path_order_info *) + alloc2(nwords, sizeof(struct path_order_info), ATEMP); + for (i = 0; i < nwords; i++) { + info[i].word = words[i]; + info[i].base = x_basename(words[i], NULL); + if (!last_info || info[i].base != last_info->base || + strncmp(words[i], last_info->word, info[i].base) != 0) { + last_info = &info[i]; + path_order++; + } + info[i].path_order = path_order; + } + qsort(info, nwords, sizeof(struct path_order_info), + path_order_cmp); + for (i = 0; i < nwords; i++) + words[i] = info[i].word; + afree(info, ATEMP); + } else { + /* Sort and remove duplicate entries */ + char **words = (char **)XPptrv(w); + size_t i, j; + + qsort(words, nwords, sizeof(void *), ascpstrcmp); + for (i = j = 0; i < nwords - 1; i++) { + if (strcmp(words[i], words[i + 1])) + words[j++] = words[i]; + else + afree(words[i], ATEMP); + } + words[j++] = words[i]; + w.len = nwords = j; + } + + XPput(w, NULL); + *wordsp = (char **)XPclose(w); + + return (nwords); +} + +#define IS_WORDC(c) (!ctype(c, C_EDNWC)) + +static int +x_locate_word(const char *buf, int buflen, int pos, int *startp, + bool *is_commandp) +{ + int start, end; + + /* Bad call? Probably should report error */ + if (pos < 0 || pos > buflen) { + *startp = pos; + *is_commandp = false; + return (0); + } + /* The case where pos == buflen happens to take care of itself... */ + + start = pos; + /* + * Keep going backwards to start of word (has effect of allowing + * one blank after the end of a word) + */ + for (; (start > 0 && IS_WORDC(buf[start - 1])) || + (start > 1 && buf[start - 2] == '\\'); start--) + ; + /* Go forwards to end of word */ + for (end = start; end < buflen && IS_WORDC(buf[end]); end++) { + if (buf[end] == '\\' && (end + 1) < buflen) + end++; + } + + if (is_commandp) { + bool iscmd; + int p = start - 1; + + /* Figure out if this is a command */ + while (p >= 0 && ctype(buf[p], C_SPACE)) + p--; + iscmd = p < 0 || ctype(buf[p], C_EDCMD); + if (iscmd) { + /* + * If command has a /, path, etc. is not searched; + * only current directory is searched which is just + * like file globbing. + */ + for (p = start; p < end; p++) + if (mksh_cdirsep(buf[p])) + break; + iscmd = p == end; + } + *is_commandp = iscmd; + } + *startp = start; + + return (end - start); +} + +static int +x_cf_glob(int *flagsp, const char *buf, int buflen, int pos, int *startp, + int *endp, char ***wordsp) +{ + int len, nwords = 0; + char **words = NULL; + bool is_command; + + len = x_locate_word(buf, buflen, pos, startp, &is_command); + if (!((*flagsp) & XCF_COMMAND)) + is_command = false; + /* + * Don't do command globing on zero length strings - it takes too + * long and isn't very useful. File globs are more likely to be + * useful, so allow these. + */ + if (len == 0 && is_command) + return (0); + + if (len >= 0) { + char *toglob, *s; + + /* + * Given a string, copy it and possibly add a '*' to the end. + */ + + strndupx(toglob, buf + *startp, len + /* the '*' */ 1, ATEMP); + toglob[len] = '\0'; + + /* + * If the pathname contains a wildcard (an unquoted '*', + * '?', or '[') or an extglob, then it is globbed based + * on that value (i.e., without the appended '*'). Same + * for parameter substitutions (as in “cat $HOME/.ss↹”) + * without appending a trailing space (LP: #710539), as + * well as for “~foo” (but not “~foo/”). + */ + for (s = toglob; *s; s++) { + if (*s == '\\' && s[1]) + s++; + else if (ctype(*s, C_QUEST | C_DOLAR) || + ord(*s) == ORD('*') || ord(*s) == ORD('[') || + /* ?() *() +() @() !() but two already checked */ + (ord(s[1]) == ORD('(' /*)*/) && + (ord(*s) == ORD('+') || ord(*s) == ORD('@') || + ord(*s) == ORD('!')))) { + /* + * just expand based on the extglob + * or parameter + */ + goto dont_add_glob; + } + } + + if (*toglob == '~' && /* not vdirsep */ !vstrchr(toglob, '/')) { + /* neither for '~foo' (but '~foo/bar') */ + *flagsp |= XCF_IS_NOSPACE; + goto dont_add_glob; + } + + /* append a glob */ + toglob[len] = '*'; + toglob[len + 1] = '\0'; + dont_add_glob: + /* + * Expand (glob) it now. + */ + + nwords = is_command ? + x_command_glob(*flagsp, toglob, &words) : + x_file_glob(flagsp, toglob, &words); + afree(toglob, ATEMP); + } + if (nwords == 0) { + *wordsp = NULL; + return (0); + } + if (is_command) + *flagsp |= XCF_IS_COMMAND; + *wordsp = words; + *endp = *startp + len; + + return (nwords); +} + +/* + * Find longest common prefix + */ +static size_t +x_longest_prefix(int nwords, char * const * words) +{ + int i; + size_t j, prefix_len; + char *p; + + if (nwords <= 0) + return (0); + + prefix_len = strlen(words[0]); + for (i = 1; i < nwords; i++) + for (j = 0, p = words[i]; j < prefix_len; j++) + if (p[j] != words[0][j]) { + prefix_len = j; + break; + } + /* false for nwords==1 as 0 = words[0][prefix_len] then */ + if (UTFMODE && prefix_len && (rtt2asc(words[0][prefix_len]) & 0xC0) == 0x80) + while (prefix_len && (rtt2asc(words[0][prefix_len]) & 0xC0) != 0xC0) + --prefix_len; + return (prefix_len); +} + +static void +x_free_words(int nwords, char **words) +{ + while (nwords) + afree(words[--nwords], ATEMP); + afree(words, ATEMP); +} + +/*- + * Return the offset of the basename of string s (which ends at se - need not + * be null terminated). Trailing slashes are ignored. If s is just a slash, + * then the offset is 0 (actually, length - 1). + * s Return + * /etc 1 + * /etc/ 1 + * /etc// 1 + * /etc/fo 5 + * foo 0 + * /// 2 + * 0 + */ +static size_t +x_basename(const char *s, const char *se) +{ + const char *p; + + if (se == NULL) + se = strnul(s); + if (s == se) + return (0); + + /* skip trailing directory separators */ + p = se - 1; + while (p > s && mksh_cdirsep(*p)) + --p; + /* drop last component */ + while (p > s && !mksh_cdirsep(*p)) + --p; + if (mksh_cdirsep(*p) && p + 1 < se) + ++p; + + return (p - s); +} + +/* + * Apply pattern matching to a table: all table entries that match a pattern + * are added to wp. + */ +static void +glob_table(const char *pat, XPtrV *wp, struct table *tp) +{ + struct tstate ts; + struct tbl *te; + + ktwalk(&ts, tp); + while ((te = ktnext(&ts))) + if (gmatchx(te->name, pat, false)) { + char *cp; + + strdupx(cp, te->name, ATEMP); + XPput(*wp, cp); + } +} + +static void +glob_path(int flags, const char *pat, XPtrV *wp, const char *lpath) +{ + const char *sp = lpath, *p; + char *xp, **words; + size_t pathlen, patlen, oldsize, newsize, i, j; + XString xs; + + patlen = strlen(pat); + checkoktoadd(patlen, 129 + X_EXTRA); + ++patlen; + Xinit(xs, xp, patlen + 128, ATEMP); + while (sp) { + xp = Xstring(xs, xp); + if (!(p = cstrchr(sp, MKSH_PATHSEPC))) + p = strnul(sp); + pathlen = p - sp; + if (pathlen) { + /* + * Copy sp into xp, stuffing any MAGIC characters + * on the way + */ + const char *s = sp; + + XcheckN(xs, xp, pathlen * 2); + while (s < p) { + if (ISMAGIC(*s)) + *xp++ = MAGIC; + *xp++ = *s++; + } + *xp++ = '/'; + pathlen++; + } + sp = p; + XcheckN(xs, xp, patlen); + memcpy(xp, pat, patlen); + + oldsize = XPsize(*wp); + /* mark dirs */ + glob_str(Xstring(xs, xp), wp, true); + newsize = XPsize(*wp); + + /* Check that each match is executable... */ + words = (char **)XPptrv(*wp); + for (i = j = oldsize; i < newsize; i++) { + if (ksh_access(words[i], X_OK) == 0) { + words[j] = words[i]; + if (!(flags & XCF_FULLPATH)) + memmove(words[j], words[j] + pathlen, + strlen(words[j] + pathlen) + 1); + j++; + } else + afree(words[i], ATEMP); + } + wp->len = j; + + if (!*sp++) + break; + } + Xfree(xs, xp); +} + +/* + * if argument string contains any special characters, they will + * be escaped and the result will be put into edit buffer by + * keybinding-specific function + */ +static int +x_escape(const char *s, size_t len, int (*putbuf_func)(const char *, size_t)) +{ + size_t add = 0, wlen = len; + int rval = 0; + + while (wlen - add > 0) + if (ctype(s[add], C_IFS | C_EDQ)) { + if (putbuf_func(s, add) != 0) { + rval = -1; + break; + } + putbuf_func(s[add] == '\n' ? "'" : "\\", 1); + putbuf_func(&s[add], 1); + if (s[add] == '\n') + putbuf_func("'", 1); + + add++; + wlen -= add; + s += add; + add = 0; + } else + ++add; + if (wlen > 0 && rval == 0) + rval = putbuf_func(s, wlen); + + return (rval); +} + + +/* +++ emacs editing mode +++ */ + +static Area aedit; +#define AEDIT &aedit /* area for kill ring and macro defns */ + +/* values returned by keyboard functions */ +#define KSTD 0 +#define KEOL 1 /* ^M, ^J */ +#define KINTR 2 /* ^G, ^C */ + +struct x_ftab { + int (*xf_func)(int c); + const char *xf_name; + short xf_flags; +}; + +struct x_defbindings { + unsigned char xdb_func; /* XFUNC_* */ + unsigned char xdb_tab; + unsigned char xdb_char; +}; + +#define XF_ARG 1 /* command takes number prefix */ +#define XF_NOBIND 2 /* not allowed to bind to function */ +#define XF_PREFIX 4 /* function sets prefix */ + +#define X_NTABS 4 /* normal, meta1, meta2, pc */ +#define X_TABSZ 256 /* size of keydef tables etc */ + +/*- + * Arguments for do_complete() + * 0 = enumerate M-= complete as much as possible and then list + * 1 = complete M-Esc + * 2 = list M-? + */ +typedef enum { + CT_LIST, /* list the possible completions */ + CT_COMPLETE, /* complete to longest prefix */ + CT_COMPLIST /* complete and then list (if non-exact) */ +} Comp_type; + +/* + * The following are used for my horizontal scrolling stuff + */ +static char *xbuf; /* beg input buffer */ +static char *xend; /* end input buffer */ +static char *xcp; /* current position */ +static char *xep; /* current end */ +static char *xbp; /* start of visible portion of input buffer */ +static char *xlp; /* last char visible on screen */ +static bool x_adj_ok; +/* + * we use x_adj_done so that functions can tell + * whether x_adjust() has been called while they are active. + */ +static int x_adj_done; /* is incremented by x_adjust() */ + +static int x_displen; +static int x_arg; /* general purpose arg */ +static bool x_arg_defaulted; /* x_arg not explicitly set; defaulted to 1 */ + +static bool xlp_valid; /* lastvis pointer was recalculated */ + +static char **x_histp; /* history position */ +static int x_nextcmd; /* for newline-and-next */ +static char **x_histncp; /* saved x_histp for " */ +static char **x_histmcp; /* saved x_histp for " */ +static char *xmp; /* mark pointer */ +static unsigned char x_last_command; +static unsigned char (*x_tab)[X_TABSZ]; /* key definition */ +#ifndef MKSH_SMALL +static char *(*x_atab)[X_TABSZ]; /* macro definitions */ +#endif +static unsigned char x_bound[(X_TABSZ * X_NTABS + 7) / 8]; +#define KILLSIZE 20 +static char *killstack[KILLSIZE]; +static int killsp, killtp; +static int x_curprefix; +#ifndef MKSH_SMALL +static char *macroptr; /* bind key macro active? */ +#endif +#if !MKSH_S_NOVI +static int winwidth; /* width of window */ +static char *wbuf[2]; /* window buffers */ +static int wbuf_len; /* length of window buffers (x_cols - 3) */ +static int win; /* window buffer in use */ +static char morec; /* more character at right of window */ +static int lastref; /* argument to last refresh() */ +static int holdlen; /* length of holdbuf */ +#endif +static int pwidth; /* width of prompt */ +static int prompt_trunc; /* how much of prompt to truncate or -1 */ +static int x_col; /* current column on line */ + +static int x_ins(const char *); +static void x_delete(size_t, bool); +static size_t x_bword(void); +static size_t x_fword(bool); +static void x_goto(char *); +static char *x_bs0(char *, char *) MKSH_A_PURE; +static void x_bs3(char **); +static int x_size2(char *, char **); +static void x_zots(char *); +static void x_zotc3(char **); +static void x_vi_zotc(int); +static void x_load_hist(char **); +static int x_search(char *, int, int); +#ifndef MKSH_SMALL +static int x_search_dir(int); +#endif +static int x_match(char *, char *); +static void x_redraw(int); +static void x_push(size_t); +static char *x_mapin(const char *, Area *); +static char *x_mapout(int); +static void x_mapout2(int, char **); +static void x_print(int, int); +static void x_e_ungetc(int); +static int x_e_getc(void); +static void x_e_putc2(int); +static void x_e_putc3(const char **); +static void x_e_puts(const char *); +#ifndef MKSH_SMALL +static int x_fold_case(int); +#endif +static char *x_lastcp(void); +static void x_lastpos(void); +static void do_complete(int, Comp_type); +static size_t x_nb2nc(size_t) MKSH_A_PURE; + +static int unget_char = -1; + +static int x_do_ins(const char *, size_t); +static void bind_if_not_bound(int, int, int); + +enum emacs_funcs { +#define EMACSFN_ENUMS +#include "emacsfn.h" + XFUNC_MAX +}; + +#define EMACSFN_DEFNS +#include "emacsfn.h" + +static const struct x_ftab x_ftab[] = { +#define EMACSFN_ITEMS +#include "emacsfn.h" +}; + +static struct x_defbindings const x_defbindings[] = { + { XFUNC_del_back, 0, CTRL_QM }, + { XFUNC_del_bword, 1, CTRL_QM }, + { XFUNC_eot_del, 0, CTRL_D }, + { XFUNC_del_back, 0, CTRL_H }, + { XFUNC_del_bword, 1, CTRL_H }, + { XFUNC_del_bword, 1, 'h' }, + { XFUNC_mv_bword, 1, 'b' }, + { XFUNC_mv_fword, 1, 'f' }, + { XFUNC_del_fword, 1, 'd' }, + { XFUNC_mv_back, 0, CTRL_B }, + { XFUNC_mv_forw, 0, CTRL_F }, + { XFUNC_search_char_forw, 0, CTRL_BC }, + { XFUNC_search_char_back, 1, CTRL_BC }, + { XFUNC_newline, 0, CTRL_M }, + { XFUNC_newline, 0, CTRL_J }, + { XFUNC_end_of_text, 0, CTRL_US }, + { XFUNC_abort, 0, CTRL_G }, + { XFUNC_prev_com, 0, CTRL_P }, + { XFUNC_next_com, 0, CTRL_N }, + { XFUNC_nl_next_com, 0, CTRL_O }, + { XFUNC_search_hist, 0, CTRL_R }, + { XFUNC_beg_hist, 1, '<' }, + { XFUNC_end_hist, 1, '>' }, + { XFUNC_goto_hist, 1, 'g' }, + { XFUNC_mv_end, 0, CTRL_E }, + { XFUNC_mv_beg, 0, CTRL_A }, + { XFUNC_draw_line, 0, CTRL_L }, + { XFUNC_cls, 1, CTRL_L }, + { XFUNC_meta1, 0, CTRL_BO }, + { XFUNC_meta2, 0, CTRL_X }, + { XFUNC_kill, 0, CTRL_K }, + { XFUNC_yank, 0, CTRL_Y }, + { XFUNC_meta_yank, 1, 'y' }, + { XFUNC_literal, 0, CTRL_CA }, + { XFUNC_comment, 1, '#' }, + { XFUNC_transpose, 0, CTRL_T }, + { XFUNC_complete, 1, CTRL_BO }, + { XFUNC_comp_list, 0, CTRL_I }, + { XFUNC_comp_list, 1, '=' }, + { XFUNC_enumerate, 1, '?' }, + { XFUNC_expand, 1, '*' }, + { XFUNC_comp_file, 1, CTRL_X }, + { XFUNC_comp_comm, 2, CTRL_BO }, + { XFUNC_list_comm, 2, '?' }, + { XFUNC_list_file, 2, CTRL_Y }, + { XFUNC_set_mark, 1, ' ' }, + { XFUNC_kill_region, 0, CTRL_W }, + { XFUNC_xchg_point_mark, 2, CTRL_X }, + { XFUNC_literal, 0, CTRL_V }, + { XFUNC_version, 1, CTRL_V }, + { XFUNC_prev_histword, 1, '.' }, + { XFUNC_prev_histword, 1, '_' }, + { XFUNC_set_arg, 1, '0' }, + { XFUNC_set_arg, 1, '1' }, + { XFUNC_set_arg, 1, '2' }, + { XFUNC_set_arg, 1, '3' }, + { XFUNC_set_arg, 1, '4' }, + { XFUNC_set_arg, 1, '5' }, + { XFUNC_set_arg, 1, '6' }, + { XFUNC_set_arg, 1, '7' }, + { XFUNC_set_arg, 1, '8' }, + { XFUNC_set_arg, 1, '9' }, +#ifndef MKSH_SMALL + { XFUNC_fold_upper, 1, 'U' }, + { XFUNC_fold_upper, 1, 'u' }, + { XFUNC_fold_lower, 1, 'L' }, + { XFUNC_fold_lower, 1, 'l' }, + { XFUNC_fold_capitalise, 1, 'C' }, + { XFUNC_fold_capitalise, 1, 'c' }, +#endif + /* + * These for ANSI arrow keys: arguablely shouldn't be here by + * default, but its simpler/faster/smaller than using termcap + * entries. + */ + { XFUNC_meta2, 1, '[' }, + { XFUNC_meta2, 1, 'O' }, + { XFUNC_prev_com, 2, 'A' }, + { XFUNC_next_com, 2, 'B' }, + { XFUNC_mv_forw, 2, 'C' }, + { XFUNC_mv_back, 2, 'D' }, +#ifndef MKSH_SMALL + { XFUNC_vt_hack, 2, '1' }, + { XFUNC_mv_beg | 0x80, 2, '7' }, + { XFUNC_mv_beg, 2, 'H' }, + { XFUNC_mv_end | 0x80, 2, '4' }, + { XFUNC_mv_end | 0x80, 2, '8' }, + { XFUNC_mv_end, 2, 'F' }, + { XFUNC_del_char | 0x80, 2, '3' }, + { XFUNC_del_char, 2, 'P' }, + { XFUNC_search_hist_up | 0x80, 2, '5' }, + { XFUNC_search_hist_dn | 0x80, 2, '6' }, +#endif + /* PC scancodes */ +#if !defined(MKSH_SMALL) || defined(__OS2__) + { XFUNC_meta3, 0, 0 }, + { XFUNC_mv_beg, 3, 71 }, + { XFUNC_prev_com, 3, 72 }, +#ifndef MKSH_SMALL + { XFUNC_search_hist_up, 3, 73 }, +#endif + { XFUNC_mv_back, 3, 75 }, + { XFUNC_mv_forw, 3, 77 }, + { XFUNC_mv_end, 3, 79 }, + { XFUNC_next_com, 3, 80 }, +#ifndef MKSH_SMALL + { XFUNC_search_hist_dn, 3, 81 }, +#endif + { XFUNC_del_char, 3, 83 }, +#endif +#ifndef MKSH_SMALL + /* more non-standard ones */ + { XFUNC_eval_region, 1, CTRL_E }, + { XFUNC_edit_line, 2, 'e' } +#endif +}; + +static size_t +x_nb2nc(size_t nb) +{ + char *cp; + size_t nc = 0; + + for (cp = xcp; cp < (xcp + nb); ++nc) + cp += utf_ptradj(cp); + return (nc); +} + +static void +x_modified(void) +{ + if (!modified) { + x_histmcp = x_histp; + x_histp = histptr + 1; + modified = 1; + } +} + +#ifdef MKSH_SMALL +#define XFUNC_VALUE(f) (f) +#else +#define XFUNC_VALUE(f) (f & 0x7F) +#endif + +static int +x_e_getmbc(char *sbuf) +{ + int c, pos = 0; + unsigned char *buf = (unsigned char *)sbuf; + + memset(buf, 0, 4); + buf[pos++] = c = x_e_getc(); + if (c == -1) + return (-1); + if (UTFMODE) { + if ((rtt2asc(buf[0]) >= (unsigned char)0xC2) && + (rtt2asc(buf[0]) < (unsigned char)0xF0)) { + c = x_e_getc(); + if (c == -1) + return (-1); + if ((rtt2asc(c) & 0xC0) != 0x80) { + x_e_ungetc(c); + return (1); + } + buf[pos++] = c; + } + if ((rtt2asc(buf[0]) >= (unsigned char)0xE0) && + (rtt2asc(buf[0]) < (unsigned char)0xF0)) { + /* XXX x_e_ungetc is one-octet only */ + buf[pos++] = c = x_e_getc(); + if (c == -1) + return (-1); + } + } + return (pos); +} + +/* + * minimum required space to work with on a line - if the prompt + * leaves less space than this on a line, the prompt is truncated + */ +#define MIN_EDIT_SPACE 7 + +static void +x_init_prompt(bool doprint) +{ + prompt_trunc = pprompt(prompt, doprint ? 0 : -1); + pwidth = prompt_trunc % x_cols; + prompt_trunc -= pwidth; + if ((mksh_uari_t)pwidth > ((mksh_uari_t)x_cols - 3 - MIN_EDIT_SPACE)) { + /* force newline after prompt */ + prompt_trunc = -1; + pwidth = 0; + if (doprint) + x_e_putc2('\n'); + } +} + +static int +x_emacs(char *buf) +{ + int c, i; + unsigned char f; + + xbp = xbuf = buf; + xend = buf + LINE; + xlp = xcp = xep = buf; + *xcp = 0; + xlp_valid = true; + xmp = NULL; + x_curprefix = 0; + x_histmcp = x_histp = histptr + 1; + x_last_command = XFUNC_error; + + x_init_prompt(true); + x_displen = (xx_cols = x_cols) - 2 - (x_col = pwidth); + x_adj_done = 0; + x_adj_ok = true; + + x_histncp = NULL; + if (x_nextcmd >= 0) { + int off = source->line - x_nextcmd; + if (histptr - history >= off) { + x_load_hist(histptr - off); + x_histncp = x_histp; + } + x_nextcmd = -1; + } + editmode = 1; + while (/* CONSTCOND */ 1) { + x_flush(); + if ((c = x_e_getc()) < 0) + return (0); + + f = x_curprefix == -1 ? XFUNC_insert : + x_tab[x_curprefix][c]; +#ifndef MKSH_SMALL + if (f & 0x80) { + f &= 0x7F; + if ((i = x_e_getc()) != '~') + x_e_ungetc(i); + } + + /* avoid bind key macro recursion */ + if (macroptr && f == XFUNC_ins_string) + f = XFUNC_insert; +#endif + + if (!(x_ftab[f].xf_flags & XF_PREFIX) && + x_last_command != XFUNC_set_arg) { + x_arg = 1; + x_arg_defaulted = true; + } + i = c | (x_curprefix << 8); + x_curprefix = 0; + switch ((*x_ftab[f].xf_func)(i)) { + case KSTD: + if (!(x_ftab[f].xf_flags & XF_PREFIX)) + x_last_command = f; + break; + case KEOL: + i = xep - xbuf; + return (i); + case KINTR: + /* special case for interrupt */ + x_intr(SIGINT, c); + } + /* ad-hoc hack for fixing the cursor position */ + x_goto(xcp); + } +} + +static int +x_insert(int c) +{ + static int left, pos, save_arg; + static char str[4]; + + /* + * Should allow tab and control chars. + */ + if (c == 0) { + invmbs: + left = 0; + x_e_putc2(KSH_BEL); + return (KSTD); + } + if (UTFMODE) { + if (((rtt2asc(c) & 0xC0) == 0x80) && left) { + str[pos++] = c; + if (!--left) { + str[pos] = '\0'; + x_arg = save_arg; + while (x_arg--) + x_ins(str); + } + return (KSTD); + } + if (left) { + if (x_curprefix == -1) { + /* flush invalid multibyte */ + str[pos] = '\0'; + while (save_arg--) + x_ins(str); + } + } + if ((c >= 0xC2) && (c < 0xE0)) + left = 1; + else if ((c >= 0xE0) && (c < 0xF0)) + left = 2; + else if (c > 0x7F) + goto invmbs; + else + left = 0; + if (left) { + save_arg = x_arg; + pos = 1; + str[0] = c; + return (KSTD); + } + } + left = 0; + str[0] = c; + str[1] = '\0'; + while (x_arg--) + x_ins(str); + return (KSTD); +} + +#ifndef MKSH_SMALL +static int +x_ins_string(int c) +{ + macroptr = x_atab[c >> 8][c & 255]; + /* + * we no longer need to bother checking if macroptr is + * not NULL but first char is NUL; x_e_getc() does it + */ + return (KSTD); +} +#endif + +static int +x_do_ins(const char *cp, size_t len) +{ + if (xep + len >= xend) { + x_e_putc2(KSH_BEL); + return (-1); + } + memmove(xcp + len, xcp, xep - xcp + 1); + memmove(xcp, cp, len); + xcp += len; + xep += len; + x_modified(); + return (0); +} + +static int +x_ins(const char *s) +{ + char *cp = xcp; + int adj = x_adj_done; + + if (x_do_ins(s, strlen(s)) < 0) + return (-1); + /* + * x_zots() may result in a call to x_adjust() + * we want xcp to reflect the new position. + */ + xlp_valid = false; + x_lastcp(); + x_adj_ok = tobool(xcp >= xlp); + x_zots(cp); + if (adj == x_adj_done) + /* x_adjust() has not been called */ + x_lastpos(); + x_adj_ok = true; + return (0); +} + +static int +x_del_back(int c MKSH_A_UNUSED) +{ + ssize_t i = 0; + + if (xcp == xbuf) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + do { + x_goto(xcp - 1); + } while ((++i < x_arg) && (xcp != xbuf)); + x_delete(i, false); + return (KSTD); +} + +static int +x_del_char(int c MKSH_A_UNUSED) +{ + char *cp, *cp2; + size_t i = 0; + + cp = xcp; + while (i < (size_t)x_arg) { + utf_ptradjx(cp, cp2); + if (cp2 > xep) + break; + cp = cp2; + i++; + } + + if (!i) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + x_delete(i, false); + return (KSTD); +} + +/* Delete nc chars to the right of the cursor (including cursor position) */ +static void +x_delete(size_t nc, bool push) +{ + size_t i, nb, nw; + char *cp; + + if (nc == 0) + return; + + nw = 0; + cp = xcp; + for (i = 0; i < nc; ++i) { + char *cp2; + int j; + + j = x_size2(cp, &cp2); + if (cp2 > xep) + break; + cp = cp2; + nw += j; + } + nb = cp - xcp; + /* nc = i; */ + + if (xmp != NULL && xmp > xcp) { + if (xcp + nb > xmp) + xmp = xcp; + else + xmp -= nb; + } + /* + * This lets us yank a word we have deleted. + */ + if (push) + x_push(nb); + + xep -= nb; + /* Copies the NUL */ + memmove(xcp, xcp + nb, xep - xcp + 1); + /* don't redraw */ + x_adj_ok = false; + xlp_valid = false; + x_zots(xcp); + /* + * if we are already filling the line, + * there is no need to ' ', '\b'. + * But if we must, make sure we do the minimum. + */ + if ((i = xx_cols - 2 - x_col) > 0 || xep - xlp == 0) { + nw = i = (nw < i) ? nw : i; + while (i--) + x_e_putc2(' '); + if (x_col == xx_cols - 2) { + x_e_putc2((xep > xlp) ? '>' : (xbp > xbuf) ? '<' : ' '); + ++nw; + } + while (nw--) + x_e_putc2('\b'); + } + /*x_goto(xcp);*/ + x_adj_ok = true; + xlp_valid = false; + x_lastpos(); + x_modified(); + return; +} + +static int +x_del_bword(int c MKSH_A_UNUSED) +{ + x_delete(x_bword(), true); + return (KSTD); +} + +static int +x_mv_bword(int c MKSH_A_UNUSED) +{ + x_bword(); + return (KSTD); +} + +static int +x_mv_fword(int c MKSH_A_UNUSED) +{ + x_fword(true); + return (KSTD); +} + +static int +x_del_fword(int c MKSH_A_UNUSED) +{ + x_delete(x_fword(false), true); + return (KSTD); +} + +static size_t +x_bword(void) +{ + size_t nb = 0; + char *cp = xcp; + + if (cp == xbuf) { + x_e_putc2(KSH_BEL); + return (0); + } + while (x_arg--) { + while (cp != xbuf && ctype(cp[-1], C_MFS)) { + cp--; + nb++; + } + while (cp != xbuf && !ctype(cp[-1], C_MFS)) { + cp--; + nb++; + } + } + x_goto(cp); + return (x_nb2nc(nb)); +} + +static size_t +x_fword(bool move) +{ + size_t nc; + char *cp = xcp; + + if (cp == xep) { + x_e_putc2(KSH_BEL); + return (0); + } + while (x_arg--) { + while (cp != xep && ctype(*cp, C_MFS)) + cp++; + while (cp != xep && !ctype(*cp, C_MFS)) + cp++; + } + nc = x_nb2nc(cp - xcp); + if (move) + x_goto(cp); + return (nc); +} + +static void +x_goto(char *cp) +{ + cp = cp >= xep ? xep : x_bs0(cp, xbuf); + if (cp < xbp || cp >= utf_skipcols(xbp, x_displen, NULL)) { + /* we are heading off screen */ + xcp = cp; + x_adjust(); + } else if (cp < xcp) { + /* move back */ + while (cp < xcp) + x_bs3(&xcp); + } else if (cp > xcp) { + /* move forward */ + while (cp > xcp) + x_zotc3(&xcp); + } +} + +static char * +x_bs0(char *cp, char *lower_bound) +{ + if (UTFMODE) + while ((!lower_bound || (cp > lower_bound)) && + ((rtt2asc(*cp) & 0xC0) == 0x80)) + --cp; + return (cp); +} + +static void +x_bs3(char **p) +{ + int i; + + *p = x_bs0((*p) - 1, NULL); + i = x_size2(*p, NULL); + while (i--) + x_e_putc2('\b'); +} + +static int +x_size2(char *cp, char **dcp) +{ + uint8_t c = *(unsigned char *)cp; + + if (UTFMODE && (rtt2asc(c) > 0x7F)) + return (utf_widthadj(cp, (const char **)dcp)); + if (dcp) + *dcp = cp + 1; + if (c == '\t') + /* Kludge, tabs are always four spaces. */ + return (4); + if (ksh_isctrl(c)) + /* control unsigned char */ + return (2); + return (1); +} + +static void +x_zots(char *str) +{ + int adj = x_adj_done; + + x_lastcp(); + while (*str && str < xlp && x_col < xx_cols && adj == x_adj_done) + x_zotc3(&str); +} + +static void +x_zotc3(char **cp) +{ + unsigned char c = **(unsigned char **)cp; + + if (c == '\t') { + /* Kludge, tabs are always four spaces. */ + x_e_puts(T4spaces); + (*cp)++; + } else if (ksh_isctrl(c)) { + x_e_putc2('^'); + x_e_putc2(ksh_unctrl(c)); + (*cp)++; + } else + x_e_putc3((const char **)cp); +} + +static int +x_mv_back(int c MKSH_A_UNUSED) +{ + if (xcp == xbuf) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + while (x_arg--) { + x_goto(xcp - 1); + if (xcp == xbuf) + break; + } + return (KSTD); +} + +static int +x_mv_forw(int c MKSH_A_UNUSED) +{ + char *cp = xcp, *cp2; + + if (xcp == xep) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + while (x_arg--) { + utf_ptradjx(cp, cp2); + if (cp2 > xep) + break; + cp = cp2; + } + x_goto(cp); + return (KSTD); +} + +static int +x_search_char_forw(int c MKSH_A_UNUSED) +{ + char *cp = xcp; + char tmp[4]; + + *xep = '\0'; + if (x_e_getmbc(tmp) < 0) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + while (x_arg--) { + if ((cp = (cp == xep) ? NULL : strstr(cp + 1, tmp)) == NULL && + (cp = strstr(xbuf, tmp)) == NULL) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + } + x_goto(cp); + return (KSTD); +} + +static int +x_search_char_back(int c MKSH_A_UNUSED) +{ + char *cp = xcp, *p, tmp[4]; + bool b; + + if (x_e_getmbc(tmp) < 0) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + for (; x_arg--; cp = p) + for (p = cp; ; ) { + if (p-- == xbuf) + p = xep; + if (p == cp) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + if ((tmp[1] && ((p+1) > xep)) || + (tmp[2] && ((p+2) > xep))) + continue; + b = true; + if (*p != tmp[0]) + b = false; + if (b && tmp[1] && p[1] != tmp[1]) + b = false; + if (b && tmp[2] && p[2] != tmp[2]) + b = false; + if (b) + break; + } + x_goto(cp); + return (KSTD); +} + +static int +x_newline(int c MKSH_A_UNUSED) +{ + x_e_putc2('\r'); + x_e_putc2('\n'); + x_flush(); + *xep++ = '\n'; + return (KEOL); +} + +static int +x_end_of_text(int c MKSH_A_UNUSED) +{ + unsigned char tmp[1], *cp = tmp; + + *tmp = isedchar(edchars.eof) ? (unsigned char)edchars.eof : + (unsigned char)CTRL_D; + x_zotc3((char **)&cp); + x_putc('\r'); + x_putc('\n'); + x_flush(); + return (KEOL); +} + +static int +x_beg_hist(int c MKSH_A_UNUSED) +{ + x_load_hist(history); + return (KSTD); +} + +static int +x_end_hist(int c MKSH_A_UNUSED) +{ + x_load_hist(histptr); + return (KSTD); +} + +static int +x_prev_com(int c MKSH_A_UNUSED) +{ + x_load_hist(x_histp - x_arg); + return (KSTD); +} + +static int +x_next_com(int c MKSH_A_UNUSED) +{ + x_load_hist(x_histp + x_arg); + return (KSTD); +} + +/* + * Goto a particular history number obtained from argument. + * If no argument is given history 1 is probably not what you + * want so we'll simply go to the oldest one. + */ +static int +x_goto_hist(int c MKSH_A_UNUSED) +{ + if (x_arg_defaulted) + x_load_hist(history); + else + x_load_hist(histptr + x_arg - source->line); + return (KSTD); +} + +static void +x_load_hist(char **hp) +{ + char *sp = NULL; + + if (hp == histptr + 1) { + sp = holdbufp; + modified = 0; + } else if (hp < history || hp > histptr) { + x_e_putc2(KSH_BEL); + return; + } + if (sp == NULL) + sp = *hp; + x_histp = hp; + if (modified) + strlcpy(holdbufp, xbuf, LINE); + strlcpy(xbuf, sp, xend - xbuf); + xbp = xbuf; + xep = xcp = strnul(xbuf); + x_adjust(); + modified = 0; +} + +static int +x_nl_next_com(int c MKSH_A_UNUSED) +{ + if (!modified) + x_histmcp = x_histp; + if (!x_histncp || (x_histmcp != x_histncp && x_histmcp != histptr + 1)) + /* fresh start of ^O */ + x_histncp = x_histmcp; + x_nextcmd = source->line - (histptr - x_histncp) + 1; + return (x_newline('\n')); +} + +static int +x_eot_del(int c) +{ + if (xep == xbuf && x_arg_defaulted) + return (x_end_of_text(c)); + else + return (x_del_char(c)); +} + +/* reverse incremental history search */ +static int +x_search_hist(int c) +{ + int offset = -1; /* offset of match in xbuf, else -1 */ + char pat[80 + 1]; /* pattern buffer */ + char *p = pat; + unsigned char f; + + *p = '\0'; + while (/* CONSTCOND */ 1) { + if (offset < 0) { + x_e_puts("\nI-search: "); + x_e_puts(pat); + } + x_flush(); + if ((c = x_e_getc()) < 0) + return (KSTD); + f = x_tab[0][c]; + if (c == CTRL_BO) { + if ((f & 0x7F) == XFUNC_meta1) { + if ((c = x_e_getc()) < 0) + return (KSTD); + f = x_tab[1][c] & 0x7F; + if (f == XFUNC_meta1 || f == XFUNC_meta2) + x_meta1(CTRL_BO); + x_e_ungetc(c); + } + break; + } +#ifndef MKSH_SMALL + if (f & 0x80) { + f &= 0x7F; + if ((c = x_e_getc()) != '~') + x_e_ungetc(c); + } +#endif + if (f == XFUNC_search_hist) + offset = x_search(pat, 0, offset); + else if (f == XFUNC_del_back) { + if (p == pat) { + offset = -1; + break; + } + if (p > pat) { + p = x_bs0(p - 1, pat); + *p = '\0'; + } + if (p == pat) + offset = -1; + else + offset = x_search(pat, 1, offset); + continue; + } else if (f == XFUNC_insert) { + /* add char to pattern */ + /* overflow check... */ + if ((size_t)(p - pat) >= sizeof(pat) - 1) { + x_e_putc2(KSH_BEL); + continue; + } + *p++ = c, *p = '\0'; + if (offset >= 0) { + /* already have partial match */ + offset = x_match(xbuf, pat); + if (offset >= 0) { + x_goto(xbuf + offset + (p - pat) - + (*pat == '^')); + continue; + } + } + offset = x_search(pat, 0, offset); + } else if (f == XFUNC_abort) { + if (offset >= 0) + x_load_hist(histptr + 1); + break; + } else { + /* other command */ + x_e_ungetc(c); + break; + } + } + if (offset < 0) + x_redraw('\n'); + return (KSTD); +} + +/* search backward from current line */ +static int +x_search(char *pat, int sameline, int offset) +{ + char **hp; + int i; + + for (hp = x_histp - (sameline ? 0 : 1); hp >= history; --hp) { + i = x_match(*hp, pat); + if (i >= 0) { + if (offset < 0) + x_e_putc2('\n'); + x_load_hist(hp); + x_goto(xbuf + i + strlen(pat) - (*pat == '^')); + return (i); + } + } + x_e_putc2(KSH_BEL); + x_histp = histptr; + return (-1); +} + +#ifndef MKSH_SMALL +/* anchored search up from current line */ +static int +x_search_hist_up(int c MKSH_A_UNUSED) +{ + return (x_search_dir(-1)); +} + +/* anchored search down from current line */ +static int +x_search_hist_dn(int c MKSH_A_UNUSED) +{ + return (x_search_dir(1)); +} + +/* anchored search in the indicated direction */ +static int +x_search_dir(int search_dir /* should've been bool */) +{ + char **hp = x_histp + search_dir; + size_t curs = xcp - xbuf; + + while (histptr >= hp && hp >= history) { + if (strncmp(xbuf, *hp, curs) == 0) { + x_load_hist(hp); + x_goto(xbuf + curs); + break; + } + hp += search_dir; + } + return (KSTD); +} +#endif + +/* return position of first match of pattern in string, else -1 */ +static int +x_match(char *str, char *pat) +{ + if (*pat == '^') { + return ((strncmp(str, pat + 1, strlen(pat + 1)) == 0) ? 0 : -1); + } else { + char *q = strstr(str, pat); + return ((q == NULL) ? -1 : q - str); + } +} + +static int +x_del_line(int c MKSH_A_UNUSED) +{ + *xep = 0; + x_push(xep - (xcp = xbuf)); + xlp = xbp = xep = xbuf; + xlp_valid = true; + *xcp = 0; + xmp = NULL; + x_redraw('\r'); + x_modified(); + return (KSTD); +} + +static int +x_mv_end(int c MKSH_A_UNUSED) +{ + x_goto(xep); + return (KSTD); +} + +static int +x_mv_beg(int c MKSH_A_UNUSED) +{ + x_goto(xbuf); + return (KSTD); +} + +static int +x_draw_line(int c MKSH_A_UNUSED) +{ + x_redraw('\n'); + return (KSTD); +} + +static int +x_cls(int c MKSH_A_UNUSED) +{ + shf_puts(MKSH_CLS_STRING, shl_out); + x_redraw(0); + return (KSTD); +} + +/* + * clear line from x_col (current cursor position) to xx_cols - 2, + * then output lastch, then go back to x_col; if lastch is space, + * clear with termcap instead of spaces, or not if line_was_cleared; + * lastch MUST be an ASCII character with wcwidth(lastch) == 1 + */ +static void +x_clrtoeol(int lastch, bool line_was_cleared) +{ + int col; + + if (lastch == ' ' && !line_was_cleared && x_term_mode == 1) { + shf_puts(KSH_ESC_STRING "[K", shl_out); + line_was_cleared = true; + } + if (lastch == ' ' && line_was_cleared) + return; + + col = x_col; + while (col < (xx_cols - 2)) { + x_putc(' '); + ++col; + } + x_putc(lastch); + ++col; + while (col > x_col) { + x_putc('\b'); + --col; + } +} + +/* output the prompt, assuming a line has just been started */ +static void +x_pprompt(void) +{ + if (prompt_trunc != -1) + pprompt(prompt, prompt_trunc); + x_col = pwidth; +} + +/* output CR, then redraw the line, clearing to EOL if needed (cr ≠ 0, LF) */ +static void +x_redraw(int cr) +{ + int lch; + + x_adj_ok = false; + /* clear the line */ + x_e_putc2(cr ? cr : '\r'); + x_flush(); + /* display the prompt */ + if (xbp == xbuf) + x_pprompt(); + x_displen = xx_cols - 2 - x_col; + /* display the line content */ + xlp_valid = false; + x_zots(xbp); + /* check whether there is more off-screen */ + lch = xep > xlp ? (xbp > xbuf ? '*' : '>') : (xbp > xbuf) ? '<' : ' '; + /* clear the rest of the line */ + x_clrtoeol(lch, !cr || cr == '\n'); + /* go back to actual cursor position */ + x_lastpos(); + x_adj_ok = true; +} + +static int +x_transpose(int c MKSH_A_UNUSED) +{ + unsigned int tmpa, tmpb; + + /*- + * What transpose is meant to do seems to be up for debate. This + * is a general summary of the options; the text is abcd with the + * upper case character or underscore indicating the cursor position: + * Who Before After Before After + * AT&T ksh in emacs mode: abCd abdC abcd_ (bell) + * AT&T ksh in gmacs mode: abCd baCd abcd_ abdc_ + * gnu emacs: abCd acbD abcd_ abdc_ + * Pdksh currently goes with GNU behavior since I believe this is the + * most common version of emacs, unless in gmacs mode, in which case + * it does the AT&T ksh gmacs mode. + * This should really be broken up into 3 functions so users can bind + * to the one they want. + */ + if (xcp == xbuf) { + x_e_putc2(KSH_BEL); + return (KSTD); + } else if (xcp == xep || Flag(FGMACS)) { + if (xcp - xbuf == 1) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + /* + * Gosling/Unipress emacs style: Swap two characters before + * the cursor, do not change cursor position + */ + x_bs3(&xcp); + if (utf_mbtowc(&tmpa, xcp) == (size_t)-1) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + x_bs3(&xcp); + if (utf_mbtowc(&tmpb, xcp) == (size_t)-1) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + utf_wctomb(xcp, tmpa); + x_zotc3(&xcp); + utf_wctomb(xcp, tmpb); + x_zotc3(&xcp); + } else { + /* + * GNU emacs style: Swap the characters before and under the + * cursor, move cursor position along one. + */ + if (utf_mbtowc(&tmpa, xcp) == (size_t)-1) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + x_bs3(&xcp); + if (utf_mbtowc(&tmpb, xcp) == (size_t)-1) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + utf_wctomb(xcp, tmpa); + x_zotc3(&xcp); + utf_wctomb(xcp, tmpb); + x_zotc3(&xcp); + } + x_modified(); + return (KSTD); +} + +static int +x_literal(int c MKSH_A_UNUSED) +{ + x_curprefix = -1; + return (KSTD); +} + +static int +x_meta1(int c MKSH_A_UNUSED) +{ + x_curprefix = 1; + return (KSTD); +} + +static int +x_meta2(int c MKSH_A_UNUSED) +{ + x_curprefix = 2; + return (KSTD); +} + +static int +x_meta3(int c MKSH_A_UNUSED) +{ + x_curprefix = 3; + return (KSTD); +} + +static int +x_kill(int c MKSH_A_UNUSED) +{ + size_t col = xcp - xbuf; + size_t lastcol = xep - xbuf; + size_t ndel, narg; + + if (x_arg_defaulted || (narg = x_arg) > lastcol) + narg = lastcol; + if (narg < col) { + x_goto(xbuf + narg); + ndel = col - narg; + } else + ndel = narg - col; + x_delete(x_nb2nc(ndel), true); + return (KSTD); +} + +static void +x_push(size_t nchars) +{ + afree(killstack[killsp], AEDIT); + strndupx(killstack[killsp], xcp, nchars, AEDIT); + killsp = (killsp + 1) % KILLSIZE; +} + +static int +x_yank(int c MKSH_A_UNUSED) +{ + if (killsp == 0) + killtp = KILLSIZE; + else + killtp = killsp; + killtp--; + if (killstack[killtp] == 0) { + x_e_puts("\nnothing to yank"); + x_redraw('\n'); + return (KSTD); + } + xmp = xcp; + x_ins(killstack[killtp]); + return (KSTD); +} + +static int +x_meta_yank(int c MKSH_A_UNUSED) +{ + size_t len; + + if ((x_last_command != XFUNC_yank && x_last_command != XFUNC_meta_yank) || + killstack[killtp] == 0) { + killtp = killsp; + x_e_puts("\nyank something first"); + x_redraw('\n'); + return (KSTD); + } + len = strlen(killstack[killtp]); + x_goto(xcp - len); + x_delete(x_nb2nc(len), false); + do { + if (killtp == 0) + killtp = KILLSIZE - 1; + else + killtp--; + } while (killstack[killtp] == 0); + x_ins(killstack[killtp]); + return (KSTD); +} + +/* fake receiving an interrupt */ +static void +x_intr(int signo, int c) +{ + x_vi_zotc(c); + *xep = '\0'; + strip_nuls(xbuf, xep - xbuf); + if (*xbuf) + histsave(&source->line, xbuf, HIST_STORE, true); + xlp = xep = xcp = xbp = xbuf; + xlp_valid = true; + *xcp = 0; + x_modified(); + x_flush(); + trapsig(signo); + x_mode(false); + unwind(LSHELL); +} + +static int +x_abort(int c MKSH_A_UNUSED) +{ + return (KINTR); +} + +static int +x_error(int c MKSH_A_UNUSED) +{ + x_e_putc2(KSH_BEL); + return (KSTD); +} + +#ifndef MKSH_SMALL +/* special VT100 style key sequence hack */ +static int +x_vt_hack(int c) +{ + /* we only support PF2-'1' for now */ + if (c != (2 << 8 | '1')) + return (x_error(c)); + + /* what's the next character? */ + switch ((c = x_e_getc())) { + case '~': + x_arg = 1; + x_arg_defaulted = true; + return (x_mv_beg(0)); + case ';': + /* "interesting" sequence detected */ + break; + default: + goto unwind_err; + } + + /* XXX x_e_ungetc is one-octet only */ + if ((c = x_e_getc()) != '5' && c != '3') + goto unwind_err; + + /*- + * At this point, we have read the following octets so far: + * - ESC+[ or ESC+O or Ctrl-X (Prefix 2) + * - 1 (vt_hack) + * - ; + * - 5 (Ctrl key combiner) or 3 (Alt key combiner) + * We can now accept one more octet designating the key. + */ + + switch ((c = x_e_getc())) { + case 'C': + return (x_mv_fword(c)); + case 'D': + return (x_mv_bword(c)); + } + + unwind_err: + x_e_ungetc(c); + return (x_error(c)); +} +#endif + +static char * +x_mapin(const char *cp, Area *ap) +{ + char *news, *op; + + strdupx(news, cp, ap); + op = news; + while (*cp) { + switch (*cp) { + case '^': + cp++; + *op++ = ksh_toctrl(*cp); + break; + case '\\': + if (cp[1] == '\\' || cp[1] == '^') + ++cp; + /* FALLTHROUGH */ + default: + *op++ = *cp; + } + cp++; + } + *op = '\0'; + + return (news); +} + +static void +x_mapout2(int c, char **buf) +{ + char *p = *buf; + + if (ksh_isctrl(c)) { + *p++ = '^'; + *p++ = ksh_unctrl(c); + } else + *p++ = c; + *p = 0; + *buf = p; +} + +static char * +x_mapout(int c) +{ + static char buf[8]; + char *bp = buf; + + x_mapout2(c, &bp); + return (buf); +} + +static void +x_print(int prefix, int key) +{ + int f = x_tab[prefix][key]; + + if (prefix) + /* prefix == 1 || prefix == 2 || prefix == 3 */ + shf_puts(x_mapout(prefix == 1 ? CTRL_BO : + prefix == 2 ? CTRL_X : 0), shl_stdout); +#ifdef MKSH_SMALL + shprintf("%s = ", x_mapout(key)); +#else + shprintf("%s%s = ", x_mapout(key), (f & 0x80) ? "~" : ""); + if (XFUNC_VALUE(f) != XFUNC_ins_string) +#endif + shprintf(Tf_sN, x_ftab[XFUNC_VALUE(f)].xf_name); +#ifndef MKSH_SMALL + else + shprintf("'%s'\n", x_atab[prefix][key]); +#endif +} + +int +x_bind(const char *a1, const char *a2, +#ifndef MKSH_SMALL + /* bind -m */ + bool macro, +#endif + /* bind -l */ + bool list) +{ + unsigned char f; + int prefix, key; + char *m1, *m2; +#ifndef MKSH_SMALL + char *sp = NULL; + bool hastilde; +#endif + + if (x_tab == NULL) { + bi_errorf("can't bind, not a tty"); + return (1); + } + /* List function names */ + if (list) { + for (f = 0; f < NELEM(x_ftab); f++) + if (!(x_ftab[f].xf_flags & XF_NOBIND)) + shprintf(Tf_sN, x_ftab[f].xf_name); + return (0); + } + if (a1 == NULL) { + for (prefix = 0; prefix < X_NTABS; prefix++) + for (key = 0; key < X_TABSZ; key++) { + f = XFUNC_VALUE(x_tab[prefix][key]); + if (f == XFUNC_insert || f == XFUNC_error +#ifndef MKSH_SMALL + || (macro && f != XFUNC_ins_string) +#endif + ) + continue; + x_print(prefix, key); + } + return (0); + } + m2 = m1 = x_mapin(a1, ATEMP); + prefix = 0; + for (;; m1++) { + key = (unsigned char)*m1; + f = XFUNC_VALUE(x_tab[prefix][key]); + if (f == XFUNC_meta1) + prefix = 1; + else if (f == XFUNC_meta2) + prefix = 2; + else if (f == XFUNC_meta3) + prefix = 3; + else + break; + } + if (*++m1 +#ifndef MKSH_SMALL + && ((*m1 != '~') || *(m1 + 1)) +#endif + ) { + char msg[256]; + const char *c = a1; + m1 = msg; + while (*c && (size_t)(m1 - msg) < sizeof(msg) - 3) + x_mapout2(*c++, &m1); + bi_errorf("too long key sequence: %s", msg); + return (1); + } +#ifndef MKSH_SMALL + hastilde = tobool(*m1); +#endif + afree(m2, ATEMP); + + if (a2 == NULL) { + x_print(prefix, key); + return (0); + } + if (*a2 == 0) { + f = XFUNC_insert; +#ifndef MKSH_SMALL + } else if (macro) { + f = XFUNC_ins_string; + sp = x_mapin(a2, AEDIT); +#endif + } else { + for (f = 0; f < NELEM(x_ftab); f++) + if (!strcmp(x_ftab[f].xf_name, a2)) + break; + if (f == NELEM(x_ftab) || x_ftab[f].xf_flags & XF_NOBIND) { + bi_errorf("%s: no such function", a2); + return (1); + } + } + +#ifndef MKSH_SMALL + if (XFUNC_VALUE(x_tab[prefix][key]) == XFUNC_ins_string && + x_atab[prefix][key]) + afree(x_atab[prefix][key], AEDIT); +#endif + x_tab[prefix][key] = f +#ifndef MKSH_SMALL + | (hastilde ? 0x80 : 0) +#endif + ; +#ifndef MKSH_SMALL + x_atab[prefix][key] = sp; +#endif + + /* Track what the user has bound so x_mode(true) won't toast things */ + if (f == XFUNC_insert) + x_bound[(prefix * X_TABSZ + key) / 8] &= + ~(1 << ((prefix * X_TABSZ + key) % 8)); + else + x_bound[(prefix * X_TABSZ + key) / 8] |= + (1 << ((prefix * X_TABSZ + key) % 8)); + + return (0); +} + +static void +bind_if_not_bound(int p, int k, int func) +{ + int t; + + /* + * Has user already bound this key? + * If so, do not override it. + */ + t = p * X_TABSZ + k; + if (x_bound[t >> 3] & (1 << (t & 7))) + return; + + x_tab[p][k] = func; +} + +static int +x_set_mark(int c MKSH_A_UNUSED) +{ + xmp = xcp; + return (KSTD); +} + +static int +x_kill_region(int c MKSH_A_UNUSED) +{ + size_t rsize; + char *xr; + + if (xmp == NULL) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + if (xmp > xcp) { + rsize = xmp - xcp; + xr = xcp; + } else { + rsize = xcp - xmp; + xr = xmp; + } + x_goto(xr); + x_delete(x_nb2nc(rsize), true); + xmp = xr; + return (KSTD); +} + +static int +x_xchg_point_mark(int c MKSH_A_UNUSED) +{ + char *tmp; + + if (xmp == NULL) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + tmp = xmp; + xmp = xcp; + x_goto(tmp); + return (KSTD); +} + +static int +x_noop(int c MKSH_A_UNUSED) +{ + return (KSTD); +} + +/* + * File/command name completion routines + */ +static int +x_comp_comm(int c MKSH_A_UNUSED) +{ + do_complete(XCF_COMMAND, CT_COMPLETE); + return (KSTD); +} + +static int +x_list_comm(int c MKSH_A_UNUSED) +{ + do_complete(XCF_COMMAND, CT_LIST); + return (KSTD); +} + +static int +x_complete(int c MKSH_A_UNUSED) +{ + do_complete(XCF_COMMAND_FILE, CT_COMPLETE); + return (KSTD); +} + +static int +x_enumerate(int c MKSH_A_UNUSED) +{ + do_complete(XCF_COMMAND_FILE, CT_LIST); + return (KSTD); +} + +static int +x_comp_file(int c MKSH_A_UNUSED) +{ + do_complete(XCF_FILE, CT_COMPLETE); + return (KSTD); +} + +static int +x_list_file(int c MKSH_A_UNUSED) +{ + do_complete(XCF_FILE, CT_LIST); + return (KSTD); +} + +static int +x_comp_list(int c MKSH_A_UNUSED) +{ + do_complete(XCF_COMMAND_FILE, CT_COMPLIST); + return (KSTD); +} + +static int +x_expand(int c MKSH_A_UNUSED) +{ + char **words; + int start, end, nwords, i; + + i = XCF_FILE; + nwords = x_cf_glob(&i, xbuf, xep - xbuf, xcp - xbuf, + &start, &end, &words); + + if (nwords == 0) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + x_goto(xbuf + start); + x_delete(x_nb2nc(end - start), false); + + i = 0; + while (i < nwords) { + if (x_escape(words[i], strlen(words[i]), x_do_ins) < 0 || + (++i < nwords && x_ins(T1space) < 0)) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + } + x_adjust(); + + return (KSTD); +} + +static void +do_complete( + /* XCF_{COMMAND,FILE,COMMAND_FILE} */ + int flags, + /* 0 for list, 1 for complete and 2 for complete-list */ + Comp_type type) +{ + char **words; + int start, end, nlen, olen, nwords; + bool completed; + + nwords = x_cf_glob(&flags, xbuf, xep - xbuf, xcp - xbuf, + &start, &end, &words); + /* no match */ + if (nwords == 0) { + x_e_putc2(KSH_BEL); + return; + } + if (type == CT_LIST) { + x_print_expansions(nwords, words, + tobool(flags & XCF_IS_COMMAND)); + x_redraw(0); + x_free_words(nwords, words); + return; + } + olen = end - start; + nlen = x_longest_prefix(nwords, words); + if (nwords == 1) { + /* + * always complete single matches; + * any expansion of parameter substitution + * is always at most one result, too + */ + completed = true; + } else { + char *unescaped; + + /* make a copy of the original string part */ + strndupx(unescaped, xbuf + start, olen, ATEMP); + + /* expand any tilde and unescape the string for comparison */ + unescaped = x_glob_hlp_tilde_and_rem_qchar(unescaped, true); + + /* + * match iff entire original string is part of the + * longest prefix, implying the latter is at least + * the same size (after unescaping) + */ + completed = !strncmp(words[0], unescaped, strlen(unescaped)); + + afree(unescaped, ATEMP); + } + if (type == CT_COMPLIST && nwords > 1) { + /* + * print expansions, since we didn't get back + * just a single match + */ + x_print_expansions(nwords, words, + tobool(flags & XCF_IS_COMMAND)); + } + if (completed) { + /* expand on the command line */ + xmp = NULL; + xcp = xbuf + start; + xep -= olen; + memmove(xcp, xcp + olen, xep - xcp + 1); + x_escape(words[0], nlen, x_do_ins); + } + x_adjust(); + /* + * append a space if this is a single non-directory match + * and not a parameter or homedir substitution + */ + if (nwords == 1 && !mksh_cdirsep(words[0][nlen - 1]) && + !(flags & XCF_IS_NOSPACE)) { + x_ins(T1space); + } + + x_free_words(nwords, words); +} + +/*- + * NAME: + * x_adjust - redraw the line adjusting starting point etc. + * + * DESCRIPTION: + * This function is called when we have exceeded the bounds + * of the edit window. It increments x_adj_done so that + * functions like x_ins and x_delete know that we have been + * called and can skip the x_bs() stuff which has already + * been done by x_redraw. + * + * RETURN VALUE: + * None + */ +static void +x_adjust(void) +{ + int col_left, n; + + /* flag the fact that we were called */ + x_adj_done++; + + /* + * calculate the amount of columns we need to "go back" + * from xcp to set xbp to (but never < xbuf) to 2/3 of + * the display width; take care of pwidth though + */ + if ((col_left = xx_cols * 2 / 3) < MIN_EDIT_SPACE) { + /* + * cowardly refuse to do anything + * if the available space is too small; + * fall back to dumb pdksh code + */ + if ((xbp = xcp - (x_displen / 2)) < xbuf) + xbp = xbuf; + /* elide UTF-8 fixup as penalty */ + goto x_adjust_out; + } + + /* fix up xbp to just past a character end first */ + xbp = xcp >= xep ? xep : x_bs0(xcp, xbuf); + /* walk backwards */ + while (xbp > xbuf && col_left > 0) { + xbp = x_bs0(xbp - 1, xbuf); + col_left -= (n = x_size2(xbp, NULL)); + } + /* check if we hit the prompt */ + if (xbp == xbuf && xcp != xbuf && col_left >= 0 && col_left < pwidth) { + /* so we did; force scrolling occurs */ + xbp += utf_ptradj(xbp); + } + + x_adjust_out: + xlp_valid = false; + x_redraw('\r'); + x_flush(); +} + +static void +x_e_ungetc(int c) +{ + unget_char = c < 0 ? -1 : (c & 255); +} + +static int +x_e_getc(void) +{ + int c; + + if (unget_char >= 0) { + c = unget_char; + unget_char = -1; + return (c); + } + +#ifndef MKSH_SMALL + if (macroptr) { + if ((c = (unsigned char)*macroptr++)) + return (c); + macroptr = NULL; + } +#endif + + return (x_getc()); +} + +static void +x_e_putc2(int c) +{ + int width = 1; + + if (ctype(c, C_CR | C_LF)) + x_col = 0; + if (x_col < xx_cols) { +#ifndef MKSH_EBCDIC + if (UTFMODE && (c > 0x7F)) { + char utf_tmp[3]; + size_t x; + + if (c < 0xA0) + c = 0xFFFD; + x = utf_wctomb(utf_tmp, c); + x_putc(utf_tmp[0]); + if (x > 1) + x_putc(utf_tmp[1]); + if (x > 2) + x_putc(utf_tmp[2]); + width = utf_wcwidth(c); + } else +#endif + x_putc(c); + switch (c) { + case KSH_BEL: + break; + case '\r': + case '\n': + break; + case '\b': + x_col--; + break; + default: + x_col += width; + break; + } + } + if (x_adj_ok && (x_col < 0 || x_col >= (xx_cols - 2))) + x_adjust(); +} + +static void +x_e_putc3(const char **cp) +{ + int width = 1, c = **(const unsigned char **)cp; + + if (ctype(c, C_CR | C_LF)) + x_col = 0; + if (x_col < xx_cols) { + if (UTFMODE && (c > 0x7F)) { + char *cp2; + + width = utf_widthadj(*cp, (const char **)&cp2); + if (cp2 == *cp + 1) { + (*cp)++; +#ifdef MKSH_EBCDIC + x_putc(asc2rtt(0xEF)); + x_putc(asc2rtt(0xBF)); + x_putc(asc2rtt(0xBD)); +#else + shf_puts("\xEF\xBF\xBD", shl_out); +#endif + } else + while (*cp < cp2) + x_putcf(*(*cp)++); + } else { + (*cp)++; + x_putc(c); + } + switch (c) { + case KSH_BEL: + break; + case '\r': + case '\n': + break; + case '\b': + x_col--; + break; + default: + x_col += width; + break; + } + } + if (x_adj_ok && (x_col < 0 || x_col >= (xx_cols - 2))) + x_adjust(); +} + +static void +x_e_puts(const char *s) +{ + int adj = x_adj_done; + + while (*s && adj == x_adj_done) + x_e_putc3(&s); +} + +/*- + * NAME: + * x_set_arg - set an arg value for next function + * + * DESCRIPTION: + * This is a simple implementation of M-[0-9]. + * + * RETURN VALUE: + * KSTD + */ +static int +x_set_arg(int c) +{ + unsigned int n = 0; + bool first = true; + + /* strip command prefix */ + c &= 255; + while (c >= 0 && ctype(c, C_DIGIT)) { + n = n * 10 + ksh_numdig(c); + if (n > LINE) + /* upper bound for repeat */ + goto x_set_arg_too_big; + c = x_e_getc(); + first = false; + } + if (c < 0 || first) { + x_set_arg_too_big: + x_e_putc2(KSH_BEL); + x_arg = 1; + x_arg_defaulted = true; + } else { + x_e_ungetc(c); + x_arg = n; + x_arg_defaulted = false; + } + return (KSTD); +} + +/* Comment or uncomment the current line. */ +static int +x_comment(int c MKSH_A_UNUSED) +{ + ssize_t len = xep - xbuf; + int ret = x_do_comment(xbuf, xend - xbuf, &len); + + if (ret < 0) + x_e_putc2(KSH_BEL); + else { + x_modified(); + xep = xbuf + len; + *xep = '\0'; + xcp = xbp = xbuf; + x_redraw('\r'); + if (ret > 0) + return (x_newline('\n')); + } + return (KSTD); +} + +static int +x_version(int c MKSH_A_UNUSED) +{ + char *o_xbuf = xbuf, *o_xend = xend; + char *o_xbp = xbp, *o_xep = xep, *o_xcp = xcp; + char *v; + + strdupx(v, KSH_VERSION, ATEMP); + + xbuf = xbp = xcp = v; + xend = xep = strnul(v); + x_redraw('\r'); + x_flush(); + + c = x_e_getc(); + xbuf = o_xbuf; + xend = o_xend; + xbp = o_xbp; + xep = o_xep; + xcp = o_xcp; + x_redraw('\r'); + + if (c < 0) + return (KSTD); + /* This is what AT&T ksh seems to do... Very bizarre */ + if (c != ' ') + x_e_ungetc(c); + + afree(v, ATEMP); + return (KSTD); +} + +#ifndef MKSH_SMALL +static int +x_edit_line(int c MKSH_A_UNUSED) +{ + if (x_arg_defaulted) { + if (xep == xbuf) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + if (modified) { + *xep = '\0'; + histsave(&source->line, xbuf, HIST_STORE, true); + x_arg = 0; + } else + x_arg = source->line - (histptr - x_histp); + } + if (x_arg) + shf_snprintf(xbuf, xend - xbuf, Tf_sd, + "fc -e ${VISUAL:-${EDITOR:-vi}} --", x_arg); + else + strlcpy(xbuf, "fc -e ${VISUAL:-${EDITOR:-vi}} --", xend - xbuf); + xep = strnul(xbuf); + return (x_newline('\n')); +} +#endif + +/*- + * NAME: + * x_prev_histword - recover word from prev command + * + * DESCRIPTION: + * This function recovers the last word from the previous + * command and inserts it into the current edit line. If a + * numeric arg is supplied then the n'th word from the + * start of the previous command is used. + * As a side effect, trashes the mark in order to achieve + * being called in a repeatable fashion. + * + * Bound to M-. + * + * RETURN VALUE: + * KSTD + */ +static int +x_prev_histword(int c MKSH_A_UNUSED) +{ + char *rcp, *cp; + char **xhp; + int m = 1; + /* -1 = defaulted; 0+ = argument */ + static int last_arg = -1; + + if (x_last_command == XFUNC_prev_histword) { + if (xmp && modified > 1) + x_kill_region(0); + if (modified) + m = modified; + } else + last_arg = x_arg_defaulted ? -1 : x_arg; + xhp = histptr - (m - 1); + if ((xhp < history) || !(cp = *xhp)) { + x_e_putc2(KSH_BEL); + x_modified(); + return (KSTD); + } + x_set_mark(0); + if ((x_arg = last_arg) == -1) { + /* x_arg_defaulted */ + + rcp = &cp[strlen(cp) - 1]; + /* + * ignore white-space after the last word + */ + while (rcp > cp && ctype(*rcp, C_CFS)) + rcp--; + while (rcp > cp && !ctype(*rcp, C_CFS)) + rcp--; + if (ctype(*rcp, C_CFS)) + rcp++; + x_ins(rcp); + } else { + /* not x_arg_defaulted */ + char ch; + + rcp = cp; + /* + * ignore white-space at start of line + */ + while (*rcp && ctype(*rcp, C_CFS)) + rcp++; + while (x_arg-- > 0) { + while (*rcp && !ctype(*rcp, C_CFS)) + rcp++; + while (*rcp && ctype(*rcp, C_CFS)) + rcp++; + } + cp = rcp; + while (*rcp && !ctype(*rcp, C_CFS)) + rcp++; + ch = *rcp; + *rcp = '\0'; + x_ins(cp); + *rcp = ch; + } + if (!modified) + x_histmcp = x_histp; + modified = m + 1; + return (KSTD); +} + +#ifndef MKSH_SMALL +/* Uppercase N(1) words */ +static int +x_fold_upper(int c MKSH_A_UNUSED) +{ + return (x_fold_case('U')); +} + +/* Lowercase N(1) words */ +static int +x_fold_lower(int c MKSH_A_UNUSED) +{ + return (x_fold_case('L')); +} + +/* Titlecase N(1) words */ +static int +x_fold_capitalise(int c MKSH_A_UNUSED) +{ + return (x_fold_case('C')); +} + +/*- + * NAME: + * x_fold_case - convert word to UPPER/lower/Capital case + * + * DESCRIPTION: + * This function is used to implement M-U/M-u, M-L/M-l, M-C/M-c + * to UPPER CASE, lower case or Capitalise Words. + * + * RETURN VALUE: + * None + */ +static int +x_fold_case(int c) +{ + char *cp = xcp; + + if (cp == xep) { + x_e_putc2(KSH_BEL); + return (KSTD); + } + while (x_arg--) { + /* + * first skip over any white-space + */ + while (cp != xep && ctype(*cp, C_MFS)) + cp++; + /* + * do the first char on its own since it may be + * a different action than for the rest. + */ + if (cp != xep) { + if (c == 'L') + /* lowercase */ + *cp = ksh_tolower(*cp); + else + /* uppercase, capitalise */ + *cp = ksh_toupper(*cp); + cp++; + } + /* + * now for the rest of the word + */ + while (cp != xep && !ctype(*cp, C_MFS)) { + if (c == 'U') + /* uppercase */ + *cp = ksh_toupper(*cp); + else + /* lowercase, capitalise */ + *cp = ksh_tolower(*cp); + cp++; + } + } + x_goto(cp); + x_modified(); + return (KSTD); +} +#endif + +/*- + * NAME: + * x_lastcp - last visible char + * + * DESCRIPTION: + * This function returns a pointer to that char in the + * edit buffer that will be the last displayed on the + * screen. + */ +static char * +x_lastcp(void) +{ + if (!xlp_valid) { + int i = 0, j; + char *xlp2; + + xlp = xbp; + while (xlp < xep) { + j = x_size2(xlp, &xlp2); + if ((i + j) > x_displen) + break; + i += j; + xlp = xlp2; + } + } + xlp_valid = true; + return (xlp); +} + +/* correctly position the cursor on the screen from end of visible area */ +static void +x_lastpos(void) +{ + char *cp = x_lastcp(); + + while (cp > xcp) + x_bs3(&cp); +} + +static void +x_mode(bool onoff) +{ + static bool x_cur_mode; + + if (x_cur_mode == onoff) + return; + x_cur_mode = onoff; + + if (onoff) { + x_mkraw(tty_fd, NULL, false); + + edchars.erase = toedchar(tty_state.c_cc[VERASE]); + edchars.kill = toedchar(tty_state.c_cc[VKILL]); + edchars.intr = toedchar(tty_state.c_cc[VINTR]); + edchars.quit = toedchar(tty_state.c_cc[VQUIT]); + edchars.eof = toedchar(tty_state.c_cc[VEOF]); +#ifdef VWERASE + edchars.werase = toedchar(tty_state.c_cc[VWERASE]); +#else + edchars.werase = 0; +#endif + + if (!edchars.erase) + edchars.erase = CTRL_H; + if (!edchars.kill) + edchars.kill = CTRL_U; + if (!edchars.intr) + edchars.intr = CTRL_C; + if (!edchars.quit) + edchars.quit = CTRL_BK; + if (!edchars.eof) + edchars.eof = CTRL_D; + if (!edchars.werase) + edchars.werase = CTRL_W; + + if (isedchar(edchars.erase)) { + bind_if_not_bound(0, edchars.erase, XFUNC_del_back); + bind_if_not_bound(1, edchars.erase, XFUNC_del_bword); + } + if (isedchar(edchars.kill)) + bind_if_not_bound(0, edchars.kill, XFUNC_del_line); + if (isedchar(edchars.werase)) + bind_if_not_bound(0, edchars.werase, XFUNC_del_bword); + if (isedchar(edchars.intr)) + bind_if_not_bound(0, edchars.intr, XFUNC_abort); + if (isedchar(edchars.quit)) + bind_if_not_bound(0, edchars.quit, XFUNC_noop); + } else + mksh_tcset(tty_fd, &tty_state); +} + +#if !MKSH_S_NOVI +/* +++ vi editing mode +++ */ + +struct edstate { + char *cbuf; + ssize_t winleft; + ssize_t cbufsize; + ssize_t linelen; + ssize_t cursor; +}; + +static int vi_hook(int); +static int nextstate(int); +static int vi_insert(int); +static int vi_cmd(int, const char *); +static int domove(int, const char *, int); +static int domovebeg(void); +static int redo_insert(int); +static void yank_range(int, int); +static int bracktype(int); +static void save_cbuf(void); +static void restore_cbuf(void); +static int putbuf(const char *, ssize_t, bool); +static void del_range(int, int); +static int findch(int, int, bool, bool) MKSH_A_PURE; +static int forwword(int); +static int backword(int); +static int endword(int); +static int Forwword(int); +static int Backword(int); +static int Endword(int); +static int grabhist(int, int); +static int grabsearch(int, int, int, const char *); +static void redraw_line(bool); +static void refresh(int); +static int outofwin(void); +static void rewindow(void); +static int newcol(unsigned char, int); +static void display(char *, char *, int); +static void ed_mov_opt(int, char *); +static int expand_word(int); +static int complete_word(int, int); +static int print_expansions(struct edstate *, int); +static void vi_error(void); +static void vi_macro_reset(void); +static int x_vi_putbuf(const char *, size_t); +#define char_len(c) (ksh_isctrl(c) ? 2 : 1) + +#define vC 0x01 /* a valid command that isn't a vM, vE, vU */ +#define vM 0x02 /* movement command (h, l, etc.) */ +#define vE 0x04 /* extended command (c, d, y) */ +#define vX 0x08 /* long command (@, f, F, t, T, etc.) */ +#define vU 0x10 /* an UN-undoable command (that isn't a vM) */ +#define vB 0x20 /* bad command (^@) */ +#define vZ 0x40 /* repeat count defaults to 0 (not 1) */ +#define vS 0x80 /* search (/, ?) */ + +#define is_bad(c) (classify[rtt2asc(c) & 0x7F] & vB) +#define is_cmd(c) (classify[rtt2asc(c) & 0x7F] & (vM | vE | vC | vU)) +#define is_move(c) (classify[rtt2asc(c) & 0x7F] & vM) +#define is_extend(c) (classify[rtt2asc(c) & 0x7F] & vE) +#define is_long(c) (classify[rtt2asc(c) & 0x7F] & vX) +#define is_undoable(c) (!(classify[rtt2asc(c) & 0x7F] & vU)) +#define is_srch(c) (classify[rtt2asc(c) & 0x7F] & vS) +#define is_zerocount(c) (classify[rtt2asc(c) & 0x7F] & vZ) + +static const unsigned char classify[128] = { +/* 0 1 2 3 4 5 6 7 */ +/* 0 ^@ ^A ^B ^C ^D ^E ^F ^G */ + vB, 0, 0, 0, 0, vC|vU, vC|vZ, 0, +/* 1 ^H ^I ^J ^K ^L ^M ^N ^O */ + vM, vC|vZ, 0, 0, vC|vU, 0, vC, 0, +/* 2 ^P ^Q ^R ^S ^T ^U ^V ^W */ + vC, 0, vC|vU, 0, 0, 0, vC, 0, +/* 3 ^X ^Y ^Z ^[ ^\ ^] ^^ ^_ */ + vC, 0, 0, vC|vZ, 0, 0, 0, 0, +/* 4 ! " # $ % & ' */ + vM, 0, 0, vC, vM, vM, 0, 0, +/* 5 ( ) * + , - . / */ + 0, 0, vC, vC, vM, vC, 0, vC|vS, +/* 6 0 1 2 3 4 5 6 7 */ + vM, 0, 0, 0, 0, 0, 0, 0, +/* 7 8 9 : ; < = > ? */ + 0, 0, 0, vM, 0, vC, 0, vC|vS, +/* 8 @ A B C D E F G */ + vC|vX, vC, vM, vC, vC, vM, vM|vX, vC|vU|vZ, +/* 9 H I J K L M N O */ + 0, vC, 0, 0, 0, 0, vC|vU, vU, +/* A P Q R S T U V W */ + vC, 0, vC, vC, vM|vX, vC, 0, vM, +/* B X Y Z [ \ ] ^ _ */ + vC, vC|vU, 0, vU, vC|vZ, 0, vM, vC|vZ, +/* C ` a b c d e f g */ + 0, vC, vM, vE, vE, vM, vM|vX, vC|vZ, +/* D h i j k l m n o */ + vM, vC, vC|vU, vC|vU, vM, 0, vC|vU, 0, +/* E p q r s t u v w */ + vC, 0, vX, vC, vM|vX, vC|vU, vC|vU|vZ, vM, +/* F x y z { | } ~ ^? */ + vC, vE|vU, 0, 0, vM|vZ, 0, vC, 0 +}; + +#define MAXVICMD 3 +#define SRCHLEN 40 + +#define INSERT 1 +#define REPLACE 2 + +#define VNORMAL 0 /* command, insert or replace mode */ +#define VARG1 1 /* digit prefix (first, eg, 5l) */ +#define VEXTCMD 2 /* cmd + movement (eg, cl) */ +#define VARG2 3 /* digit prefix (second, eg, 2c3l) */ +#define VXCH 4 /* f, F, t, T, @ */ +#define VFAIL 5 /* bad command */ +#define VCMD 6 /* single char command (eg, X) */ +#define VREDO 7 /* . */ +#define VLIT 8 /* ^V */ +#define VSEARCH 9 /* /, ? */ +#define VVERSION 10 /* ^V */ +#define VPREFIX2 11 /* ^[[ and ^[O in insert mode */ + +static struct edstate *save_edstate(struct edstate *old); +static void restore_edstate(struct edstate *old, struct edstate *news); +static void free_edstate(struct edstate *old); + +static struct edstate ebuf; +static struct edstate undobuf; + +static struct edstate *vs; /* current Vi editing mode state */ +static struct edstate *undo; + +static char *ibuf; /* input buffer */ +static bool first_insert; /* set when starting in insert mode */ +static int saved_inslen; /* saved inslen for first insert */ +static int inslen; /* length of input buffer */ +static int srchlen; /* length of current search pattern */ +static char *ybuf; /* yank buffer */ +static int yanklen; /* length of yank buffer */ +static int fsavecmd = ' '; /* last find command */ +static int fsavech; /* character to find */ +static char lastcmd[MAXVICMD]; /* last non-move command */ +static int lastac; /* argcnt for lastcmd */ +static int lastsearch = ' '; /* last search command */ +static char srchpat[SRCHLEN]; /* last search pattern */ +static int insert; /* <>0 in insert mode */ +static int hnum; /* position in history */ +static int ohnum; /* history line copied (after mod) */ +static int hlast; /* 1 past last position in history */ +static int state; + +/* + * Information for keeping track of macros that are being expanded. + * The format of buf is the alias contents followed by a NUL byte followed + * by the name (letter) of the alias. The end of the buffer is marked by + * a double NUL. The name of the alias is stored so recursive macros can + * be detected. + */ +struct macro_state { + unsigned char *p; /* current position in buf */ + unsigned char *buf; /* pointer to macro(s) being expanded */ + size_t len; /* how much data in buffer */ +}; +static struct macro_state macro; + +/* last input was expanded */ +static enum expand_mode { + NONE = 0, EXPAND, COMPLETE, PRINT +} expanded; + +static int +x_vi(char *buf) +{ + int c; + + state = VNORMAL; + ohnum = hnum = hlast = histnum(-1) + 1; + insert = INSERT; + saved_inslen = inslen; + first_insert = true; + inslen = 0; + vi_macro_reset(); + + ebuf.cbuf = buf; + if (undobuf.cbuf == NULL) { + ibuf = alloc(LINE, AEDIT); + ybuf = alloc(LINE, AEDIT); + undobuf.cbuf = alloc(LINE, AEDIT); + } + undobuf.cbufsize = ebuf.cbufsize = LINE; + undobuf.linelen = ebuf.linelen = 0; + undobuf.cursor = ebuf.cursor = 0; + undobuf.winleft = ebuf.winleft = 0; + vs = &ebuf; + undo = &undobuf; + + x_init_prompt(true); + x_col = pwidth; + + if (wbuf_len != x_cols - 3 && ((wbuf_len = x_cols - 3))) { + wbuf[0] = aresize(wbuf[0], wbuf_len, AEDIT); + wbuf[1] = aresize(wbuf[1], wbuf_len, AEDIT); + } + if (wbuf_len) { + memset(wbuf[0], ' ', wbuf_len); + memset(wbuf[1], ' ', wbuf_len); + } + winwidth = x_cols - pwidth - 3; + win = 0; + morec = ' '; + lastref = 1; + holdlen = 0; + + editmode = 2; + x_flush(); + while (/* CONSTCOND */ 1) { + if (macro.p) { + c = (unsigned char)*macro.p++; + /* end of current macro? */ + if (!c) { + /* more macros left to finish? */ + if (*macro.p++) + continue; + /* must be the end of all the macros */ + vi_macro_reset(); + c = x_getc(); + } + } else + c = x_getc(); + + if (c == -1) + break; + if (state != VLIT) { + if (isched(c, edchars.intr) || + isched(c, edchars.quit)) { + /* shove input buffer away */ + xbuf = ebuf.cbuf; + xep = xbuf; + if (ebuf.linelen > 0) + xep += ebuf.linelen; + /* pretend we got an interrupt */ + x_intr(isched(c, edchars.intr) ? + SIGINT : SIGQUIT, c); + } else if (isched(c, edchars.eof) && + state != VVERSION) { + if (vs->linelen == 0) { + x_vi_zotc(c); + c = -1; + break; + } + continue; + } + } + if (vi_hook(c)) + break; + x_flush(); + } + + x_putc('\r'); + x_putc('\n'); + x_flush(); + + if (c == -1 || (ssize_t)LINE <= vs->linelen) + return (-1); + + if (vs->cbuf != buf) + memcpy(buf, vs->cbuf, vs->linelen); + + buf[vs->linelen++] = '\n'; + + return (vs->linelen); +} + +static int +vi_hook(int ch) +{ + static char curcmd[MAXVICMD], locpat[SRCHLEN]; + static int cmdlen, argc1, argc2; + + switch (state) { + + case VNORMAL: + /* PC scancodes */ + if (!ch) switch (cmdlen = 0, (ch = x_getc())) { + case 71: ch = '0'; goto pseudo_vi_command; + case 72: ch = 'k'; goto pseudo_vi_command; + case 73: ch = 'A'; goto vi_xfunc_search_up; + case 75: ch = 'h'; goto pseudo_vi_command; + case 77: ch = 'l'; goto pseudo_vi_command; + case 79: ch = '$'; goto pseudo_vi_command; + case 80: ch = 'j'; goto pseudo_vi_command; + case 83: ch = 'x'; goto pseudo_vi_command; + default: ch = 0; goto vi_insert_failed; + } + if (insert != 0) { + if (ch == CTRL_V) { + state = VLIT; + ch = '^'; + } + switch (vi_insert(ch)) { + case -1: + vi_insert_failed: + vi_error(); + state = VNORMAL; + break; + case 0: + if (state == VLIT) { + vs->cursor--; + refresh(0); + } else + refresh(insert != 0); + break; + case 1: + return (1); + } + } else { + if (ctype(ch, C_CR | C_LF)) + return (1); + cmdlen = 0; + argc1 = 0; + if (ctype(ch, C_DIGIT) && ord(ch) != ORD('0')) { + argc1 = ksh_numdig(ch); + state = VARG1; + } else { + pseudo_vi_command: + curcmd[cmdlen++] = ch; + state = nextstate(ch); + if (state == VSEARCH) { + save_cbuf(); + vs->cursor = 0; + vs->linelen = 0; + if (putbuf(ch == '/' ? "/" : "?", 1, + false) != 0) + return (-1); + refresh(0); + } + if (state == VVERSION) { + save_cbuf(); + vs->cursor = 0; + vs->linelen = 0; + putbuf(KSH_VERSION, + strlen(KSH_VERSION), false); + refresh(0); + } + } + } + break; + + case VLIT: + if (is_bad(ch)) { + del_range(vs->cursor, vs->cursor + 1); + vi_error(); + } else + vs->cbuf[vs->cursor++] = ch; + refresh(1); + state = VNORMAL; + break; + + case VVERSION: + restore_cbuf(); + state = VNORMAL; + refresh(0); + break; + + case VARG1: + if (ctype(ch, C_DIGIT)) + argc1 = argc1 * 10 + ksh_numdig(ch); + else { + curcmd[cmdlen++] = ch; + state = nextstate(ch); + } + break; + + case VEXTCMD: + argc2 = 0; + if (ctype(ch, C_DIGIT) && ord(ch) != ORD('0')) { + argc2 = ksh_numdig(ch); + state = VARG2; + return (0); + } else { + curcmd[cmdlen++] = ch; + if (ch == curcmd[0]) + state = VCMD; + else if (is_move(ch)) + state = nextstate(ch); + else + state = VFAIL; + } + break; + + case VARG2: + if (ctype(ch, C_DIGIT)) + argc2 = argc2 * 10 + ksh_numdig(ch); + else { + if (argc1 == 0) + argc1 = argc2; + else + argc1 *= argc2; + curcmd[cmdlen++] = ch; + if (ch == curcmd[0]) + state = VCMD; + else if (is_move(ch)) + state = nextstate(ch); + else + state = VFAIL; + } + break; + + case VXCH: + if (ch == CTRL_BO) + state = VNORMAL; + else { + curcmd[cmdlen++] = ch; + state = VCMD; + } + break; + + case VSEARCH: + if (ctype(ch, C_CR | C_LF) /* || ch == CTRL_BO */ ) { + restore_cbuf(); + /* Repeat last search? */ + if (srchlen == 0) { + if (!srchpat[0]) { + vi_error(); + state = VNORMAL; + refresh(0); + return (0); + } + } else { + locpat[srchlen] = '\0'; + memcpy(srchpat, locpat, srchlen + 1); + } + state = VCMD; + } else if (isched(ch, edchars.erase) || ch == CTRL_H) { + if (srchlen != 0) { + srchlen--; + vs->linelen -= char_len(locpat[srchlen]); + vs->cursor = vs->linelen; + refresh(0); + return (0); + } + restore_cbuf(); + state = VNORMAL; + refresh(0); + } else if (isched(ch, edchars.kill)) { + srchlen = 0; + vs->linelen = 1; + vs->cursor = 1; + refresh(0); + return (0); + } else if (isched(ch, edchars.werase)) { + unsigned int i, n; + struct edstate new_es, *save_es; + + new_es.cursor = srchlen; + new_es.cbuf = locpat; + + save_es = vs; + vs = &new_es; + n = backword(1); + vs = save_es; + + i = (unsigned)srchlen; + while (--i >= n) + vs->linelen -= char_len(locpat[i]); + srchlen = (int)n; + vs->cursor = vs->linelen; + refresh(0); + return (0); + } else { + if (srchlen == SRCHLEN - 1) + vi_error(); + else { + locpat[srchlen++] = ch; + if (ksh_isctrl(ch)) { + if ((size_t)vs->linelen + 2 > + (size_t)vs->cbufsize) + vi_error(); + vs->cbuf[vs->linelen++] = '^'; + vs->cbuf[vs->linelen++] = ksh_unctrl(ch); + } else { + if (vs->linelen >= vs->cbufsize) + vi_error(); + vs->cbuf[vs->linelen++] = ch; + } + vs->cursor = vs->linelen; + refresh(0); + } + return (0); + } + break; + + case VPREFIX2: + vi_xfunc_search_up: + state = VFAIL; + switch (ch) { + case 'A': + /* the cursor may not be at the BOL */ + if (!vs->cursor) + break; + /* nor further in the line than we can search for */ + if ((size_t)vs->cursor >= sizeof(srchpat) - 1) + vs->cursor = sizeof(srchpat) - 2; + /* anchor the search pattern */ + srchpat[0] = '^'; + /* take the current line up to the cursor */ + memmove(srchpat + 1, vs->cbuf, vs->cursor); + srchpat[vs->cursor + 1] = '\0'; + /* set a magic flag */ + argc1 = 2 + (int)vs->cursor; + /* and emulate a backwards history search */ + lastsearch = '/'; + *curcmd = 'n'; + goto pseudo_VCMD; + } + break; + } + + switch (state) { + case VCMD: + pseudo_VCMD: + state = VNORMAL; + switch (vi_cmd(argc1, curcmd)) { + case -1: + vi_error(); + refresh(0); + break; + case 0: + if (insert != 0) + inslen = 0; + refresh(insert != 0); + break; + case 1: + refresh(0); + return (1); + case 2: + /* back from a 'v' command - don't redraw the screen */ + return (1); + } + break; + + case VREDO: + state = VNORMAL; + if (argc1 != 0) + lastac = argc1; + switch (vi_cmd(lastac, lastcmd)) { + case -1: + vi_error(); + refresh(0); + break; + case 0: + if (insert != 0) { + if (lastcmd[0] == 's' || + ksh_eq(lastcmd[0], 'C', 'c')) { + if (redo_insert(1) != 0) + vi_error(); + } else { + if (redo_insert(lastac) != 0) + vi_error(); + } + } + refresh(0); + break; + case 1: + refresh(0); + return (1); + case 2: + /* back from a 'v' command - can't happen */ + break; + } + break; + + case VFAIL: + state = VNORMAL; + vi_error(); + break; + } + return (0); +} + +static int +nextstate(int ch) +{ + if (is_extend(ch)) + return (VEXTCMD); + else if (is_srch(ch)) + return (VSEARCH); + else if (is_long(ch)) + return (VXCH); + else if (ch == '.') + return (VREDO); + else if (ch == CTRL_V) + return (VVERSION); + else if (is_cmd(ch)) + return (VCMD); + else + return (VFAIL); +} + +static int +vi_insert(int ch) +{ + int tcursor; + + if (isched(ch, edchars.erase) || ch == CTRL_H) { + if (insert == REPLACE) { + if (vs->cursor == undo->cursor) { + vi_error(); + return (0); + } + if (inslen > 0) + inslen--; + vs->cursor--; + if (vs->cursor >= undo->linelen) + vs->linelen--; + else + vs->cbuf[vs->cursor] = undo->cbuf[vs->cursor]; + } else { + if (vs->cursor == 0) + return (0); + if (inslen > 0) + inslen--; + vs->cursor--; + vs->linelen--; + memmove(&vs->cbuf[vs->cursor], &vs->cbuf[vs->cursor + 1], + vs->linelen - vs->cursor + 1); + } + expanded = NONE; + return (0); + } + if (isched(ch, edchars.kill)) { + if (vs->cursor != 0) { + inslen = 0; + memmove(vs->cbuf, &vs->cbuf[vs->cursor], + vs->linelen - vs->cursor); + vs->linelen -= vs->cursor; + vs->cursor = 0; + } + expanded = NONE; + return (0); + } + if (isched(ch, edchars.werase)) { + if (vs->cursor != 0) { + tcursor = backword(1); + memmove(&vs->cbuf[tcursor], &vs->cbuf[vs->cursor], + vs->linelen - vs->cursor); + vs->linelen -= vs->cursor - tcursor; + if (inslen < vs->cursor - tcursor) + inslen = 0; + else + inslen -= vs->cursor - tcursor; + vs->cursor = tcursor; + } + expanded = NONE; + return (0); + } + /* + * If any chars are entered before escape, trash the saved insert + * buffer (if user inserts & deletes char, ibuf gets trashed and + * we don't want to use it) + */ + if (first_insert && ch != CTRL_BO) + saved_inslen = 0; + switch (ch) { + case '\0': + return (-1); + + case '\r': + case '\n': + return (1); + + case CTRL_BO: + expanded = NONE; + if (first_insert) { + first_insert = false; + if (inslen == 0) { + inslen = saved_inslen; + return (redo_insert(0)); + } + lastcmd[0] = 'a'; + lastac = 1; + } + if (lastcmd[0] == 's' || ksh_eq(lastcmd[0], 'C', 'c')) + return (redo_insert(0)); + else + return (redo_insert(lastac - 1)); + + /* { start nonstandard vi commands */ + case CTRL_X: + expand_word(0); + break; + + case CTRL_F: + complete_word(0, 0); + break; + + case CTRL_E: + print_expansions(vs, 0); + break; + + case CTRL_I: + if (Flag(FVITABCOMPLETE)) { + complete_word(0, 0); + break; + } + /* FALLTHROUGH */ + /* end nonstandard vi commands } */ + + default: + if (vs->linelen >= vs->cbufsize - 1) + return (-1); + ibuf[inslen++] = ch; + if (insert == INSERT) { + memmove(&vs->cbuf[vs->cursor + 1], &vs->cbuf[vs->cursor], + vs->linelen - vs->cursor); + vs->linelen++; + } + vs->cbuf[vs->cursor++] = ch; + if (insert == REPLACE && vs->cursor > vs->linelen) + vs->linelen++; + expanded = NONE; + } + return (0); +} + +static int +vi_cmd(int argcnt, const char *cmd) +{ + int ncursor; + int cur, c1, c2, c3 = 0; + int any; + struct edstate *t; + + if (argcnt == 0 && !is_zerocount(*cmd)) + argcnt = 1; + + if (is_move(*cmd)) { + if ((cur = domove(argcnt, cmd, 0)) >= 0) { + if (cur == vs->linelen && cur != 0) + cur--; + vs->cursor = cur; + } else + return (-1); + } else { + /* Don't save state in middle of macro.. */ + if (is_undoable(*cmd) && !macro.p) { + undo->winleft = vs->winleft; + memmove(undo->cbuf, vs->cbuf, vs->linelen); + undo->linelen = vs->linelen; + undo->cursor = vs->cursor; + lastac = argcnt; + memmove(lastcmd, cmd, MAXVICMD); + } + switch (ord(*cmd)) { + + case CTRL_L: + case CTRL_R: + redraw_line(true); + break; + + case ORD('@'): + { + static char alias[] = "_\0"; + struct tbl *ap; + size_t olen, nlen; + char *p, *nbuf; + + /* lookup letter in alias list... */ + alias[1] = cmd[1]; + ap = ktsearch(&aliases, alias, hash(alias)); + if (!cmd[1] || !ap || !(ap->flag & ISSET)) + return (-1); + /* check if this is a recursive call... */ + if ((p = (char *)macro.p)) + while ((p = strnul(p)) && p[1]) + if (*++p == cmd[1]) + return (-1); + /* insert alias into macro buffer */ + nlen = strlen(ap->val.s) + 1; + olen = !macro.p ? 2 : + macro.len - (macro.p - macro.buf); + /* + * at this point, it's fairly reasonable that + * nlen + olen + 2 doesn't overflow + */ + nbuf = alloc(nlen + 1 + olen, AEDIT); + memcpy(nbuf, ap->val.s, nlen); + nbuf[nlen++] = cmd[1]; + if (macro.p) { + memcpy(nbuf + nlen, macro.p, olen); + afree(macro.buf, AEDIT); + nlen += olen; + } else { + nbuf[nlen++] = '\0'; + nbuf[nlen++] = '\0'; + } + macro.p = macro.buf = (unsigned char *)nbuf; + macro.len = nlen; + } + break; + + case ORD('a'): + modified = 1; + hnum = hlast; + if (vs->linelen != 0) + vs->cursor++; + insert = INSERT; + break; + + case ORD('A'): + modified = 1; + hnum = hlast; + del_range(0, 0); + vs->cursor = vs->linelen; + insert = INSERT; + break; + + case ORD('S'): + vs->cursor = domovebeg(); + del_range(vs->cursor, vs->linelen); + modified = 1; + hnum = hlast; + insert = INSERT; + break; + + case ORD('Y'): + cmd = "y$"; + /* ahhhhhh... */ + + /* FALLTHROUGH */ + case ORD('c'): + case ORD('d'): + case ORD('y'): + if (*cmd == cmd[1]) { + c1 = *cmd == 'c' ? domovebeg() : 0; + c2 = vs->linelen; + } else if (!is_move(cmd[1])) + return (-1); + else { + if ((ncursor = domove(argcnt, &cmd[1], 1)) < 0) + return (-1); + if (*cmd == 'c' && ksh_eq(cmd[1], 'W', 'w') && + !ctype(vs->cbuf[vs->cursor], C_SPACE)) { + do { + --ncursor; + } while (ctype(vs->cbuf[ncursor], C_SPACE)); + ncursor++; + } + if (ncursor > vs->cursor) { + c1 = vs->cursor; + c2 = ncursor; + } else { + c1 = ncursor; + c2 = vs->cursor; + if (cmd[1] == '%') + c2++; + } + } + if (*cmd != 'c' && c1 != c2) + yank_range(c1, c2); + if (*cmd != 'y') { + del_range(c1, c2); + vs->cursor = c1; + } + if (*cmd == 'c') { + modified = 1; + hnum = hlast; + insert = INSERT; + } + break; + + case ORD('p'): + modified = 1; + hnum = hlast; + if (vs->linelen != 0) + vs->cursor++; + while (putbuf(ybuf, yanklen, false) == 0 && + --argcnt > 0) + ; + if (vs->cursor != 0) + vs->cursor--; + if (argcnt != 0) + return (-1); + break; + + case ORD('P'): + modified = 1; + hnum = hlast; + any = 0; + while (putbuf(ybuf, yanklen, false) == 0 && + --argcnt > 0) + any = 1; + if (any && vs->cursor != 0) + vs->cursor--; + if (argcnt != 0) + return (-1); + break; + + case ORD('C'): + modified = 1; + hnum = hlast; + del_range(vs->cursor, vs->linelen); + insert = INSERT; + break; + + case ORD('D'): + yank_range(vs->cursor, vs->linelen); + del_range(vs->cursor, vs->linelen); + if (vs->cursor != 0) + vs->cursor--; + break; + + case ORD('g'): + if (!argcnt) + argcnt = hlast; + /* FALLTHROUGH */ + case ORD('G'): + if (!argcnt) + argcnt = 1; + else + argcnt = hlast - (source->line - argcnt); + if (grabhist(modified, argcnt - 1) < 0) + return (-1); + else { + modified = 0; + hnum = argcnt - 1; + } + break; + + case ORD('i'): + modified = 1; + hnum = hlast; + insert = INSERT; + break; + + case ORD('I'): + modified = 1; + hnum = hlast; + vs->cursor = domovebeg(); + insert = INSERT; + break; + + case ORD('j'): + case ORD('+'): + case CTRL_N: + if (grabhist(modified, hnum + argcnt) < 0) + return (-1); + else { + modified = 0; + hnum += argcnt; + } + break; + + case ORD('k'): + case ORD('-'): + case CTRL_P: + if (grabhist(modified, hnum - argcnt) < 0) + return (-1); + else { + modified = 0; + hnum -= argcnt; + } + break; + + case ORD('r'): + if (vs->linelen == 0) + return (-1); + modified = 1; + hnum = hlast; + if (cmd[1] == 0) + vi_error(); + else { + int n; + + if (vs->cursor + argcnt > vs->linelen) + return (-1); + for (n = 0; n < argcnt; ++n) + vs->cbuf[vs->cursor + n] = cmd[1]; + vs->cursor += n - 1; + } + break; + + case ORD('R'): + modified = 1; + hnum = hlast; + insert = REPLACE; + break; + + case ORD('s'): + if (vs->linelen == 0) + return (-1); + modified = 1; + hnum = hlast; + if (vs->cursor + argcnt > vs->linelen) + argcnt = vs->linelen - vs->cursor; + del_range(vs->cursor, vs->cursor + argcnt); + insert = INSERT; + break; + + case ORD('v'): + if (!argcnt) { + if (vs->linelen == 0) + return (-1); + if (modified) { + vs->cbuf[vs->linelen] = '\0'; + histsave(&source->line, vs->cbuf, + HIST_STORE, true); + } else + argcnt = source->line + 1 - + (hlast - hnum); + } + if (argcnt) + shf_snprintf(vs->cbuf, vs->cbufsize, Tf_sd, + "fc -e ${VISUAL:-${EDITOR:-vi}} --", + argcnt); + else + strlcpy(vs->cbuf, + "fc -e ${VISUAL:-${EDITOR:-vi}} --", + vs->cbufsize); + vs->linelen = strlen(vs->cbuf); + return (2); + + case ORD('x'): + if (vs->linelen == 0) + return (-1); + modified = 1; + hnum = hlast; + if (vs->cursor + argcnt > vs->linelen) + argcnt = vs->linelen - vs->cursor; + yank_range(vs->cursor, vs->cursor + argcnt); + del_range(vs->cursor, vs->cursor + argcnt); + break; + + case ORD('X'): + if (vs->cursor > 0) { + modified = 1; + hnum = hlast; + if (vs->cursor < argcnt) + argcnt = vs->cursor; + yank_range(vs->cursor - argcnt, vs->cursor); + del_range(vs->cursor - argcnt, vs->cursor); + vs->cursor -= argcnt; + } else + return (-1); + break; + + case ORD('u'): + t = vs; + vs = undo; + undo = t; + break; + + case ORD('U'): + if (!modified) + return (-1); + if (grabhist(modified, ohnum) < 0) + return (-1); + modified = 0; + hnum = ohnum; + break; + + case ORD('?'): + if (hnum == hlast) + hnum = -1; + /* ahhh */ + + /* FALLTHROUGH */ + case ORD('/'): + c3 = 1; + srchlen = 0; + lastsearch = *cmd; + /* FALLTHROUGH */ + case ORD('n'): + case ORD('N'): + if (lastsearch == ' ') + return (-1); + if (lastsearch == '?') + c1 = 1; + else + c1 = 0; + if (*cmd == 'N') + c1 = !c1; + if ((c2 = grabsearch(modified, hnum, + c1, srchpat)) < 0) { + if (c3) { + restore_cbuf(); + refresh(0); + } + return (-1); + } else { + modified = 0; + hnum = c2; + ohnum = hnum; + } + if (argcnt >= 2) { + /* flag from cursor-up command */ + vs->cursor = argcnt - 2; + return (0); + } + break; + case ORD('_'): + { + bool inspace; + char *p, *sp; + + if (histnum(-1) < 0) + return (-1); + p = *histpos(); + if (argcnt) { + while (ctype(*p, C_SPACE)) + p++; + while (*p && --argcnt) { + while (*p && !ctype(*p, C_SPACE)) + p++; + while (ctype(*p, C_SPACE)) + p++; + } + if (!*p) + return (-1); + sp = p; + } else { + sp = p; + inspace = false; + while (*p) { + if (ctype(*p, C_SPACE)) + inspace = true; + else if (inspace) { + inspace = false; + sp = p; + } + p++; + } + p = sp; + } + modified = 1; + hnum = hlast; + if (vs->cursor != vs->linelen) + vs->cursor++; + while (*p && !ctype(*p, C_SPACE)) { + argcnt++; + p++; + } + if (putbuf(T1space, 1, false) != 0 || + putbuf(sp, argcnt, false) != 0) { + if (vs->cursor != 0) + vs->cursor--; + return (-1); + } + insert = INSERT; + } + break; + + case ORD('~'): + { + char *p; + int i; + + if (vs->linelen == 0) + return (-1); + for (i = 0; i < argcnt; i++) { + p = &vs->cbuf[vs->cursor]; + if (ctype(*p, C_LOWER)) { + modified = 1; + hnum = hlast; + *p = ksh_toupper(*p); + } else if (ctype(*p, C_UPPER)) { + modified = 1; + hnum = hlast; + *p = ksh_tolower(*p); + } + if (vs->cursor < vs->linelen - 1) + vs->cursor++; + } + break; + } + + case ORD('#'): + { + int ret = x_do_comment(vs->cbuf, vs->cbufsize, + &vs->linelen); + if (ret >= 0) + vs->cursor = 0; + return (ret); + } + + /* AT&T ksh */ + case ORD('='): + /* Nonstandard vi/ksh */ + case CTRL_E: + print_expansions(vs, 1); + break; + + + /* Nonstandard vi/ksh */ + case CTRL_I: + if (!Flag(FVITABCOMPLETE)) + return (-1); + complete_word(1, argcnt); + break; + + /* some annoying AT&T kshs */ + case CTRL_BO: + if (!Flag(FVIESCCOMPLETE)) + return (-1); + /* FALLTHROUGH */ + /* AT&T ksh */ + case ORD('\\'): + /* Nonstandard vi/ksh */ + case CTRL_F: + complete_word(1, argcnt); + break; + + + /* AT&T ksh */ + case ORD('*'): + /* Nonstandard vi/ksh */ + case CTRL_X: + expand_word(1); + break; + + + /* mksh: cursor movement */ + case ORD('['): + case ORD('O'): + state = VPREFIX2; + if (vs->linelen != 0) + vs->cursor++; + insert = INSERT; + return (0); + } + if (insert == 0 && vs->cursor != 0 && vs->cursor >= vs->linelen) + vs->cursor--; + } + return (0); +} + +static int +domove(int argcnt, const char *cmd, int sub) +{ + int ncursor = 0, i = 0, t; + unsigned int bcount; + + switch (ord(*cmd)) { + case ORD('b'): + if (!sub && vs->cursor == 0) + return (-1); + ncursor = backword(argcnt); + break; + + case ORD('B'): + if (!sub && vs->cursor == 0) + return (-1); + ncursor = Backword(argcnt); + break; + + case ORD('e'): + if (!sub && vs->cursor + 1 >= vs->linelen) + return (-1); + ncursor = endword(argcnt); + if (sub && ncursor < vs->linelen) + ncursor++; + break; + + case ORD('E'): + if (!sub && vs->cursor + 1 >= vs->linelen) + return (-1); + ncursor = Endword(argcnt); + if (sub && ncursor < vs->linelen) + ncursor++; + break; + + case ORD('f'): + case ORD('F'): + case ORD('t'): + case ORD('T'): + fsavecmd = *cmd; + fsavech = cmd[1]; + /* FALLTHROUGH */ + case ORD(','): + case ORD(';'): + if (fsavecmd == ' ') + return (-1); + i = ksh_eq(fsavecmd, 'F', 'f'); + t = fsavecmd > 'a'; + if (*cmd == ',') + t = !t; + if ((ncursor = findch(fsavech, argcnt, tobool(t), + tobool(i))) < 0) + return (-1); + if (sub && t) + ncursor++; + break; + + case ORD('h'): + case CTRL_H: + if (!sub && vs->cursor == 0) + return (-1); + ncursor = vs->cursor - argcnt; + if (ncursor < 0) + ncursor = 0; + break; + + case ORD(' '): + case ORD('l'): + if (!sub && vs->cursor + 1 >= vs->linelen) + return (-1); + if (vs->linelen != 0) { + ncursor = vs->cursor + argcnt; + if (ncursor > vs->linelen) + ncursor = vs->linelen; + } + break; + + case ORD('w'): + if (!sub && vs->cursor + 1 >= vs->linelen) + return (-1); + ncursor = forwword(argcnt); + break; + + case ORD('W'): + if (!sub && vs->cursor + 1 >= vs->linelen) + return (-1); + ncursor = Forwword(argcnt); + break; + + case ORD('0'): + ncursor = 0; + break; + + case ORD('^'): + ncursor = domovebeg(); + break; + + case ORD('|'): + ncursor = argcnt; + if (ncursor > vs->linelen) + ncursor = vs->linelen; + if (ncursor) + ncursor--; + break; + + case ORD('$'): + if (vs->linelen != 0) + ncursor = vs->linelen; + else + ncursor = 0; + break; + + case ORD('%'): + ncursor = vs->cursor; + while (ncursor < vs->linelen && + (i = bracktype(vs->cbuf[ncursor])) == 0) + ncursor++; + if (ncursor == vs->linelen) + return (-1); + bcount = 1; + do { + if (i > 0) { + if (++ncursor >= vs->linelen) + return (-1); + } else { + if (--ncursor < 0) + return (-1); + } + t = bracktype(vs->cbuf[ncursor]); + if (t == i) + bcount++; + else if (t == -i) + bcount--; + } while (bcount != 0); + if (sub && i > 0) + ncursor++; + break; + + default: + return (-1); + } + return (ncursor); +} + +static int +domovebeg(void) +{ + int ncursor = 0; + + while (ncursor < vs->linelen - 1 && + ctype(vs->cbuf[ncursor], C_SPACE)) + ncursor++; + return (ncursor); +} + +static int +redo_insert(int count) +{ + while (count-- > 0) + if (putbuf(ibuf, inslen, tobool(insert == REPLACE)) != 0) + return (-1); + if (vs->cursor > 0) + vs->cursor--; + insert = 0; + return (0); +} + +static void +yank_range(int a, int b) +{ + yanklen = b - a; + if (yanklen != 0) + memmove(ybuf, &vs->cbuf[a], yanklen); +} + +static int +bracktype(int ch) +{ + switch (ord(ch)) { + + case ORD('('): + return (1); + + case ORD('['): + return (2); + + case ORD('{'): + return (3); + + case ORD(')'): + return (-1); + + case ORD(']'): + return (-2); + + case ORD('}'): + return (-3); + + default: + return (0); + } +} + +/* + * Non user interface editor routines below here + */ + +static void +save_cbuf(void) +{ + memmove(holdbufp, vs->cbuf, vs->linelen); + holdlen = vs->linelen; + holdbufp[holdlen] = '\0'; +} + +static void +restore_cbuf(void) +{ + vs->cursor = 0; + vs->linelen = holdlen; + memmove(vs->cbuf, holdbufp, holdlen); +} + +/* return a new edstate */ +static struct edstate * +save_edstate(struct edstate *old) +{ + struct edstate *news; + + news = alloc(sizeof(struct edstate), AEDIT); + news->cbuf = alloc(old->cbufsize, AEDIT); + memcpy(news->cbuf, old->cbuf, old->linelen); + news->cbufsize = old->cbufsize; + news->linelen = old->linelen; + news->cursor = old->cursor; + news->winleft = old->winleft; + return (news); +} + +static void +restore_edstate(struct edstate *news, struct edstate *old) +{ + memcpy(news->cbuf, old->cbuf, old->linelen); + news->linelen = old->linelen; + news->cursor = old->cursor; + news->winleft = old->winleft; + free_edstate(old); +} + +static void +free_edstate(struct edstate *old) +{ + afree(old->cbuf, AEDIT); + afree(old, AEDIT); +} + +/* + * this is used for calling x_escape() in complete_word() + */ +static int +x_vi_putbuf(const char *s, size_t len) +{ + return (putbuf(s, len, false)); +} + +static int +putbuf(const char *buf, ssize_t len, bool repl) +{ + if (len == 0) + return (0); + if (repl) { + if (vs->cursor + len >= vs->cbufsize) + return (-1); + if (vs->cursor + len > vs->linelen) + vs->linelen = vs->cursor + len; + } else { + if (vs->linelen + len >= vs->cbufsize) + return (-1); + memmove(&vs->cbuf[vs->cursor + len], &vs->cbuf[vs->cursor], + vs->linelen - vs->cursor); + vs->linelen += len; + } + memmove(&vs->cbuf[vs->cursor], buf, len); + vs->cursor += len; + return (0); +} + +static void +del_range(int a, int b) +{ + if (vs->linelen != b) + memmove(&vs->cbuf[a], &vs->cbuf[b], vs->linelen - b); + vs->linelen -= b - a; +} + +static int +findch(int ch, int cnt, bool forw, bool incl) +{ + int ncursor; + + if (vs->linelen == 0) + return (-1); + ncursor = vs->cursor; + while (cnt--) { + do { + if (forw) { + if (++ncursor == vs->linelen) + return (-1); + } else { + if (--ncursor < 0) + return (-1); + } + } while (vs->cbuf[ncursor] != ch); + } + if (!incl) { + if (forw) + ncursor--; + else + ncursor++; + } + return (ncursor); +} + +static int +forwword(int argcnt) +{ + int ncursor; + + ncursor = vs->cursor; + while (ncursor < vs->linelen && argcnt--) { + if (ctype(vs->cbuf[ncursor], C_ALNUX)) + while (ncursor < vs->linelen && + ctype(vs->cbuf[ncursor], C_ALNUX)) + ncursor++; + else if (!ctype(vs->cbuf[ncursor], C_SPACE)) + while (ncursor < vs->linelen && + !ctype(vs->cbuf[ncursor], C_ALNUX | C_SPACE)) + ncursor++; + while (ncursor < vs->linelen && + ctype(vs->cbuf[ncursor], C_SPACE)) + ncursor++; + } + return (ncursor); +} + +static int +backword(int argcnt) +{ + int ncursor; + + ncursor = vs->cursor; + while (ncursor > 0 && argcnt--) { + while (--ncursor > 0 && ctype(vs->cbuf[ncursor], C_SPACE)) + ; + if (ncursor > 0) { + if (ctype(vs->cbuf[ncursor], C_ALNUX)) + while (--ncursor >= 0 && + ctype(vs->cbuf[ncursor], C_ALNUX)) + ; + else + while (--ncursor >= 0 && + !ctype(vs->cbuf[ncursor], C_ALNUX | C_SPACE)) + ; + ncursor++; + } + } + return (ncursor); +} + +static int +endword(int argcnt) +{ + int ncursor; + + ncursor = vs->cursor; + while (ncursor < vs->linelen && argcnt--) { + while (++ncursor < vs->linelen - 1 && + ctype(vs->cbuf[ncursor], C_SPACE)) + ; + if (ncursor < vs->linelen - 1) { + if (ctype(vs->cbuf[ncursor], C_ALNUX)) + while (++ncursor < vs->linelen && + ctype(vs->cbuf[ncursor], C_ALNUX)) + ; + else + while (++ncursor < vs->linelen && + !ctype(vs->cbuf[ncursor], C_ALNUX | C_SPACE)) + ; + ncursor--; + } + } + return (ncursor); +} + +static int +Forwword(int argcnt) +{ + int ncursor; + + ncursor = vs->cursor; + while (ncursor < vs->linelen && argcnt--) { + while (ncursor < vs->linelen && + !ctype(vs->cbuf[ncursor], C_SPACE)) + ncursor++; + while (ncursor < vs->linelen && + ctype(vs->cbuf[ncursor], C_SPACE)) + ncursor++; + } + return (ncursor); +} + +static int +Backword(int argcnt) +{ + int ncursor; + + ncursor = vs->cursor; + while (ncursor > 0 && argcnt--) { + while (--ncursor >= 0 && ctype(vs->cbuf[ncursor], C_SPACE)) + ; + while (ncursor >= 0 && !ctype(vs->cbuf[ncursor], C_SPACE)) + ncursor--; + ncursor++; + } + return (ncursor); +} + +static int +Endword(int argcnt) +{ + int ncursor; + + ncursor = vs->cursor; + while (ncursor < vs->linelen - 1 && argcnt--) { + while (++ncursor < vs->linelen - 1 && + ctype(vs->cbuf[ncursor], C_SPACE)) + ; + if (ncursor < vs->linelen - 1) { + while (++ncursor < vs->linelen && + !ctype(vs->cbuf[ncursor], C_SPACE)) + ; + ncursor--; + } + } + return (ncursor); +} + +static int +grabhist(int save, int n) +{ + char *hptr; + + if (n < 0 || n > hlast) + return (-1); + if (n == hlast) { + restore_cbuf(); + ohnum = n; + return (0); + } + (void)histnum(n); + if ((hptr = *histpos()) == NULL) { + internal_warningf("grabhist: bad history array"); + return (-1); + } + if (save) + save_cbuf(); + if ((vs->linelen = strlen(hptr)) >= vs->cbufsize) + vs->linelen = vs->cbufsize - 1; + memmove(vs->cbuf, hptr, vs->linelen); + vs->cursor = 0; + ohnum = n; + return (0); +} + +static int +grabsearch(int save, int start, int fwd, const char *pat) +{ + char *hptr; + int hist; + bool anchored; + + if ((start == 0 && fwd == 0) || (start >= hlast - 1 && fwd == 1)) + return (-1); + if (fwd) + start++; + else + start--; + anchored = *pat == '^' ? (++pat, true) : false; + if ((hist = findhist(start, fwd, pat, anchored)) < 0) { + /* (start != 0 && fwd && match(holdbufp, pat) >= 0) */ + if (start != 0 && fwd && strcmp(holdbufp, pat) >= 0) { + restore_cbuf(); + return (0); + } else + return (-1); + } + if (save) + save_cbuf(); + histnum(hist); + hptr = *histpos(); + if ((vs->linelen = strlen(hptr)) >= vs->cbufsize) + vs->linelen = vs->cbufsize - 1; + memmove(vs->cbuf, hptr, vs->linelen); + vs->cursor = 0; + return (hist); +} + +static void +redraw_line(bool newl) +{ + if (wbuf_len) + memset(wbuf[win], ' ', wbuf_len); + if (newl) { + x_putc('\r'); + x_putc('\n'); + } + x_pprompt(); + morec = ' '; +} + +static void +refresh(int leftside) +{ + if (leftside < 0) + leftside = lastref; + else + lastref = leftside; + if (outofwin()) + rewindow(); + display(wbuf[1 - win], wbuf[win], leftside); + win = 1 - win; +} + +static int +outofwin(void) +{ + int cur, col; + + if (vs->cursor < vs->winleft) + return (1); + col = 0; + cur = vs->winleft; + while (cur < vs->cursor) + col = newcol((unsigned char)vs->cbuf[cur++], col); + if (col >= winwidth) + return (1); + return (0); +} + +static void +rewindow(void) +{ + int tcur, tcol; + int holdcur1, holdcol1; + int holdcur2, holdcol2; + + holdcur1 = holdcur2 = tcur = 0; + holdcol1 = holdcol2 = tcol = 0; + while (tcur < vs->cursor) { + if (tcol - holdcol2 > winwidth / 2) { + holdcur1 = holdcur2; + holdcol1 = holdcol2; + holdcur2 = tcur; + holdcol2 = tcol; + } + tcol = newcol((unsigned char)vs->cbuf[tcur++], tcol); + } + while (tcol - holdcol1 > winwidth / 2) + holdcol1 = newcol((unsigned char)vs->cbuf[holdcur1++], + holdcol1); + vs->winleft = holdcur1; +} + +static int +newcol(unsigned char ch, int col) +{ + if (ch == '\t') + return ((col | 7) + 1); + return (col + char_len(ch)); +} + +static void +display(char *wb1, char *wb2, int leftside) +{ + unsigned char ch; + char *twb1, *twb2, mc; + int cur, col, cnt; + int ncol = 0; + int moreright; + + col = 0; + cur = vs->winleft; + moreright = 0; + twb1 = wb1; + while (col < winwidth && cur < vs->linelen) { + if (cur == vs->cursor && leftside) + ncol = col + pwidth; + if ((ch = vs->cbuf[cur]) == '\t') + do { + *twb1++ = ' '; + } while (++col < winwidth && (col & 7) != 0); + else if (col < winwidth) { + if (ksh_isctrl(ch)) { + *twb1++ = '^'; + if (++col < winwidth) { + *twb1++ = ksh_unctrl(ch); + col++; + } + } else { + *twb1++ = ch; + col++; + } + } + if (cur == vs->cursor && !leftside) + ncol = col + pwidth - 1; + cur++; + } + if (cur == vs->cursor) + ncol = col + pwidth; + if (col < winwidth) { + while (col < winwidth) { + *twb1++ = ' '; + col++; + } + } else + moreright++; + *twb1 = ' '; + + col = pwidth; + cnt = winwidth; + twb1 = wb1; + twb2 = wb2; + while (cnt--) { + if (*twb1 != *twb2) { + if (x_col != col) + ed_mov_opt(col, wb1); + x_putc(*twb1); + x_col++; + } + twb1++; + twb2++; + col++; + } + if (vs->winleft > 0 && moreright) + /* + * POSIX says to use * for this but that is a globbing + * character and may confuse people; + is more innocuous + */ + mc = '+'; + else if (vs->winleft > 0) + mc = '<'; + else if (moreright) + mc = '>'; + else + mc = ' '; + if (mc != morec) { + ed_mov_opt(pwidth + winwidth + 1, wb1); + x_putc(mc); + x_col++; + morec = mc; + } + if (x_col != ncol) + ed_mov_opt(ncol, wb1); +} + +static void +ed_mov_opt(int col, char *wb) +{ + if (col < x_col) { + if (col + 1 < x_col - col) { + x_putc('\r'); + x_pprompt(); + while (x_col++ < col) + x_putcf(*wb++); + } else { + while (x_col-- > col) + x_putc('\b'); + } + } else { + wb = &wb[x_col - pwidth]; + while (x_col++ < col) + x_putcf(*wb++); + } + x_col = col; +} + + +/* replace word with all expansions (ie, expand word*) */ +static int +expand_word(int cmd) +{ + static struct edstate *buf; + int rval = 0, nwords, start, end, i; + char **words; + + /* Undo previous expansion */ + if (cmd == 0 && expanded == EXPAND && buf) { + restore_edstate(vs, buf); + buf = 0; + expanded = NONE; + return (0); + } + if (buf) { + free_edstate(buf); + buf = 0; + } + + i = XCF_COMMAND_FILE | XCF_FULLPATH; + nwords = x_cf_glob(&i, vs->cbuf, vs->linelen, vs->cursor, + &start, &end, &words); + if (nwords == 0) { + vi_error(); + return (-1); + } + + buf = save_edstate(vs); + expanded = EXPAND; + del_range(start, end); + vs->cursor = start; + i = 0; + while (i < nwords) { + if (x_escape(words[i], strlen(words[i]), x_vi_putbuf) != 0) { + rval = -1; + break; + } + if (++i < nwords && putbuf(T1space, 1, false) != 0) { + rval = -1; + break; + } + } + i = buf->cursor - end; + if (rval == 0 && i > 0) + vs->cursor += i; + modified = 1; + hnum = hlast; + insert = INSERT; + lastac = 0; + refresh(0); + return (rval); +} + +static int +complete_word(int cmd, int count) +{ + static struct edstate *buf; + int rval, nwords, start, end, flags; + size_t match_len; + char **words; + char *match; + bool is_unique; + + /* Undo previous completion */ + if (cmd == 0 && expanded == COMPLETE && buf) { + print_expansions(buf, 0); + expanded = PRINT; + return (0); + } + if (cmd == 0 && expanded == PRINT && buf) { + restore_edstate(vs, buf); + buf = 0; + expanded = NONE; + return (0); + } + if (buf) { + free_edstate(buf); + buf = 0; + } + + /* + * XCF_FULLPATH for count 'cause the menu printed by + * print_expansions() was done this way. + */ + flags = XCF_COMMAND_FILE; + if (count) + flags |= XCF_FULLPATH; + nwords = x_cf_glob(&flags, vs->cbuf, vs->linelen, vs->cursor, + &start, &end, &words); + if (nwords == 0) { + vi_error(); + return (-1); + } + if (count) { + int i; + + count--; + if (count >= nwords) { + vi_error(); + x_print_expansions(nwords, words, + tobool(flags & XCF_IS_COMMAND)); + x_free_words(nwords, words); + redraw_line(false); + return (-1); + } + /* + * Expand the count'th word to its basename + */ + if (flags & XCF_IS_COMMAND) { + match = words[count] + + x_basename(words[count], NULL); + /* If more than one possible match, use full path */ + for (i = 0; i < nwords; i++) + if (i != count && + strcmp(words[i] + x_basename(words[i], + NULL), match) == 0) { + match = words[count]; + break; + } + } else + match = words[count]; + match_len = strlen(match); + is_unique = true; + /* expanded = PRINT; next call undo */ + } else { + match = words[0]; + match_len = x_longest_prefix(nwords, words); + /* next call will list completions */ + expanded = COMPLETE; + is_unique = nwords == 1; + } + + buf = save_edstate(vs); + del_range(start, end); + vs->cursor = start; + + /* + * escape all shell-sensitive characters and put the result into + * command buffer + */ + rval = x_escape(match, match_len, x_vi_putbuf); + + if (rval == 0 && is_unique) { + /* + * If exact match, don't undo. Allows directory completions + * to be used (ie, complete the next portion of the path). + */ + expanded = NONE; + + /* + * append a space if this is a non-directory match + * and not a parameter or homedir substitution + */ + if (match_len > 0 && !mksh_cdirsep(match[match_len - 1]) && + !(flags & XCF_IS_NOSPACE)) + rval = putbuf(T1space, 1, false); + } + x_free_words(nwords, words); + + modified = 1; + hnum = hlast; + insert = INSERT; + /* prevent this from being redone... */ + lastac = 0; + refresh(0); + + return (rval); +} + +static int +print_expansions(struct edstate *est, int cmd MKSH_A_UNUSED) +{ + int start, end, nwords, i; + char **words; + + i = XCF_COMMAND_FILE | XCF_FULLPATH; + nwords = x_cf_glob(&i, est->cbuf, est->linelen, est->cursor, + &start, &end, &words); + if (nwords == 0) { + vi_error(); + return (-1); + } + x_print_expansions(nwords, words, tobool(i & XCF_IS_COMMAND)); + x_free_words(nwords, words); + redraw_line(false); + return (0); +} +#endif /* !MKSH_S_NOVI */ + +/* Similar to x_zotc(emacs.c), but no tab weirdness */ +static void +x_vi_zotc(int c) +{ + if (ksh_isctrl(c)) { + x_putc('^'); + c = ksh_unctrl(c); + } + x_putc(c); +} + +#if !MKSH_S_NOVI +static void +vi_error(void) +{ + /* Beem out of any macros as soon as an error occurs */ + vi_macro_reset(); + x_putc(KSH_BEL); + x_flush(); +} + +static void +vi_macro_reset(void) +{ + if (macro.p) { + afree(macro.buf, AEDIT); + memset((char *)¯o, 0, sizeof(macro)); + } +} +#endif /* !MKSH_S_NOVI */ + +/* called from main.c */ +void +x_init(void) +{ + int i, j; + + /* + * set edchars to force initial binding, except we need + * default values for ^W for some deficient systems… + */ + edchars.erase = edchars.kill = edchars.intr = edchars.quit = + edchars.eof = EDCHAR_INITIAL; + edchars.werase = 027; + + /* command line editing specific memory allocation */ + ainit(AEDIT); + holdbufp = alloc(LINE, AEDIT); + + /* initialise Emacs command line editing mode */ + x_nextcmd = -1; + + x_tab = alloc2(X_NTABS, sizeof(*x_tab), AEDIT); + for (j = 0; j < X_TABSZ; j++) + x_tab[0][j] = XFUNC_insert; + for (i = 1; i < X_NTABS; i++) + for (j = 0; j < X_TABSZ; j++) + x_tab[i][j] = XFUNC_error; + for (i = 0; i < (int)NELEM(x_defbindings); i++) + x_tab[x_defbindings[i].xdb_tab][x_defbindings[i].xdb_char] + = x_defbindings[i].xdb_func; + +#ifndef MKSH_SMALL + x_atab = alloc2(X_NTABS, sizeof(*x_atab), AEDIT); + for (i = 1; i < X_NTABS; i++) + for (j = 0; j < X_TABSZ; j++) + x_atab[i][j] = NULL; +#endif +} + +#ifdef DEBUG_LEAKS +void +x_done(void) +{ + if (x_tab != NULL) + afreeall(AEDIT); +} +#endif + +void +x_initterm(const char *termtype) +{ + /* default must be 0 (bss) */ + x_term_mode = 0; + /* this is what tmux uses, don't ask me about it */ + if (!strcmp(termtype, "screen") || !strncmp(termtype, "screen-", 7)) + x_term_mode = 1; +} + +#ifndef MKSH_SMALL +static char * +x_eval_region_helper(const char *cmd, size_t len) +{ + char * volatile cp; + newenv(E_ERRH); + + if (!kshsetjmp(e->jbuf)) { + char *wds = alloc(len + 3, ATEMP); + + wds[0] = FUNASUB; + memcpy(wds + 1, cmd, len); + wds[len + 1] = '\0'; + wds[len + 2] = EOS; + + cp = evalstr(wds, DOSCALAR); + afree(wds, ATEMP); + strdupx(cp, cp, AEDIT); + } else + cp = NULL; + quitenv(NULL); + return (cp); +} + +static int +x_eval_region(int c MKSH_A_UNUSED) +{ + char *evbeg, *evend, *cp; + size_t newlen; + /* only for LINE overflow checking */ + size_t restlen; + + if (xmp == NULL) { + evbeg = xbuf; + evend = xep; + } else if (xmp < xcp) { + evbeg = xmp; + evend = xcp; + } else { + evbeg = xcp; + evend = xmp; + } + + x_e_putc2('\r'); + x_clrtoeol(' ', false); + x_flush(); + x_mode(false); + cp = x_eval_region_helper(evbeg, evend - evbeg); + x_mode(true); + + if (cp == NULL) { + /* command cannot be parsed */ + x_eval_region_err: + x_e_putc2(KSH_BEL); + x_redraw('\r'); + return (KSTD); + } + + newlen = strlen(cp); + restlen = xep - evend; + /* check for LINE overflow, until this is dynamically allocated */ + if (evbeg + newlen + restlen >= xend) + goto x_eval_region_err; + + xmp = evbeg; + xcp = evbeg + newlen; + xep = xcp + restlen; + memmove(xcp, evend, restlen + /* NUL */ 1); + memcpy(xmp, cp, newlen); + afree(cp, AEDIT); + x_adjust(); + x_modified(); + return (KSTD); +} +#endif /* !MKSH_SMALL */ +#endif /* !MKSH_NO_CMDLINE_EDITING */ diff --git a/emacsfn.h b/emacsfn.h new file mode 100644 index 0000000..a1e8e82 --- /dev/null +++ b/emacsfn.h @@ -0,0 +1,113 @@ +/*- + * Copyright (c) 2009, 2010, 2015, 2016 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#if defined(EMACSFN_DEFNS) +__RCSID("$MirOS: src/bin/mksh/emacsfn.h,v 1.10 2016/09/01 12:59:09 tg Exp $"); +#define FN(cname,sname,flags) static int x_##cname(int); +#elif defined(EMACSFN_ENUMS) +#define FN(cname,sname,flags) XFUNC_##cname, +#define F0(cname,sname,flags) XFUNC_##cname = 0, +#elif defined(EMACSFN_ITEMS) +#define FN(cname,sname,flags) { x_##cname, sname, flags }, +#endif + +#ifndef F0 +#define F0 FN +#endif + +F0(abort, "abort", 0) +FN(beg_hist, "beginning-of-history", 0) +FN(cls, "clear-screen", 0) +FN(comment, "comment", 0) +FN(comp_comm, "complete-command", 0) +FN(comp_file, "complete-file", 0) +FN(comp_list, "complete-list", 0) +FN(complete, "complete", 0) +FN(del_back, "delete-char-backward", XF_ARG) +FN(del_bword, "delete-word-backward", XF_ARG) +FN(del_char, "delete-char-forward", XF_ARG) +FN(del_fword, "delete-word-forward", XF_ARG) +FN(del_line, "kill-line", 0) +FN(draw_line, "redraw", 0) +#ifndef MKSH_SMALL +FN(edit_line, "edit-line", XF_ARG) +#endif +FN(end_hist, "end-of-history", 0) +FN(end_of_text, "eot", 0) +FN(enumerate, "list", 0) +FN(eot_del, "eot-or-delete", XF_ARG) +FN(error, "error", 0) +#ifndef MKSH_SMALL +FN(eval_region, "evaluate-region", 0) +#endif +FN(expand, "expand-file", 0) +#ifndef MKSH_SMALL +FN(fold_capitalise, "capitalize-word", XF_ARG) +FN(fold_lower, "downcase-word", XF_ARG) +FN(fold_upper, "upcase-word", XF_ARG) +#endif +FN(goto_hist, "goto-history", XF_ARG) +#ifndef MKSH_SMALL +FN(ins_string, "macro-string", XF_NOBIND) +#endif +FN(insert, "auto-insert", XF_ARG) +FN(kill, "kill-to-eol", XF_ARG) +FN(kill_region, "kill-region", 0) +FN(list_comm, "list-command", 0) +FN(list_file, "list-file", 0) +FN(literal, "quote", 0) +FN(meta1, "prefix-1", XF_PREFIX) +FN(meta2, "prefix-2", XF_PREFIX) +FN(meta3, "prefix-3", XF_PREFIX) +FN(meta_yank, "yank-pop", 0) +FN(mv_back, "backward-char", XF_ARG) +FN(mv_beg, "beginning-of-line", 0) +FN(mv_bword, "backward-word", XF_ARG) +FN(mv_end, "end-of-line", 0) +FN(mv_forw, "forward-char", XF_ARG) +FN(mv_fword, "forward-word", XF_ARG) +FN(newline, "newline", 0) +FN(next_com, "down-history", XF_ARG) +FN(nl_next_com, "newline-and-next", 0) +FN(noop, "no-op", 0) +FN(prev_com, "up-history", XF_ARG) +FN(prev_histword, "prev-hist-word", XF_ARG) +FN(search_char_back, "search-character-backward", XF_ARG) +FN(search_char_forw, "search-character-forward", XF_ARG) +FN(search_hist, "search-history", 0) +#ifndef MKSH_SMALL +FN(search_hist_dn, "search-history-down", 0) +FN(search_hist_up, "search-history-up", 0) +#endif +FN(set_arg, "set-arg", XF_NOBIND) +FN(set_mark, "set-mark-command", 0) +FN(transpose, "transpose-chars", 0) +FN(version, "version", 0) +#ifndef MKSH_SMALL +FN(vt_hack, "vt100-hack", XF_ARG) +#endif +FN(xchg_point_mark, "exchange-point-and-mark", 0) +FN(yank, "yank", 0) + +#undef FN +#undef F0 +#undef EMACSFN_DEFNS +#undef EMACSFN_ENUMS +#undef EMACSFN_ITEMS diff --git a/eval.c b/eval.c new file mode 100644 index 0000000..6aa1844 --- /dev/null +++ b/eval.c @@ -0,0 +1,1961 @@ +/* $OpenBSD: eval.c,v 1.40 2013/09/14 20:09:30 millert Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/eval.c,v 1.219 2018/01/14 01:29:47 tg Exp $"); + +/* + * string expansion + * + * first pass: quoting, IFS separation, ~, ${}, $() and $(()) substitution. + * second pass: alternation ({,}), filename expansion (*?[]). + */ + +/* expansion generator state */ +typedef struct { + /* not including an "int type;" member, see expand() */ + /* string */ + const char *str; + /* source */ + union { + /* string[] */ + const char **strv; + /* file */ + struct shf *shf; + } u; + /* variable in ${var...} */ + struct tbl *var; + /* split "$@" / call waitlast in $() */ + bool split; +} Expand; + +#define XBASE 0 /* scanning original */ +#define XSUB 1 /* expanding ${} string */ +#define XARGSEP 2 /* ifs0 between "$*" */ +#define XARG 3 /* expanding $*, $@ */ +#define XCOM 4 /* expanding $() */ +#define XNULLSUB 5 /* "$@" when $# is 0 (don't generate word) */ +#define XSUBMID 6 /* middle of expanding ${} */ + +/* States used for field splitting */ +#define IFS_WORD 0 /* word has chars (or quotes except "$@") */ +#define IFS_WS 1 /* have seen IFS white-space */ +#define IFS_NWS 2 /* have seen IFS non-white-space */ +#define IFS_IWS 3 /* beginning of word, ignore IFS WS */ +#define IFS_QUOTE 4 /* beg.w/quote, become IFS_WORD unless "$@" */ + +#define STYPE_CHAR 0xFF +#define STYPE_DBL 0x100 +#define STYPE_AT 0x200 +#define STYPE_SINGLE 0x2FF +#define STYPE_MASK 0x300 + +static int varsub(Expand *, const char *, const char *, int *, int *); +static int comsub(Expand *, const char *, int); +static char *valsub(struct op *, Area *); +static char *trimsub(char *, char *, int); +static void glob(char *, XPtrV *, bool); +static void globit(XString *, char **, char *, XPtrV *, int); +static const char *maybe_expand_tilde(const char *, XString *, char **, bool); +#ifndef MKSH_NOPWNAM +static char *homedir(char *); +#endif +static void alt_expand(XPtrV *, char *, char *, char *, int); +static int utflen(const char *) MKSH_A_PURE; +static void utfincptr(const char *, mksh_ari_t *); + +/* UTFMODE functions */ +static int +utflen(const char *s) +{ + size_t n; + + if (UTFMODE) { + n = 0; + while (*s) { + s += utf_ptradj(s); + ++n; + } + } else + n = strlen(s); + + if (n > 2147483647) + n = 2147483647; + return ((int)n); +} + +static void +utfincptr(const char *s, mksh_ari_t *lp) +{ + const char *cp = s; + + while ((*lp)--) + cp += utf_ptradj(cp); + *lp = cp - s; +} + +/* compile and expand word */ +char * +substitute(const char *cp, int f) +{ + struct source *s, *sold; + + sold = source; + s = pushs(SWSTR, ATEMP); + s->start = s->str = cp; + source = s; + if (yylex(ONEWORD) != LWORD) + internal_errorf(Tbadsubst); + source = sold; + afree(s, ATEMP); + return (evalstr(yylval.cp, f)); +} + +/* + * expand arg-list + */ +char ** +eval(const char **ap, int f) +{ + XPtrV w; + + if (*ap == NULL) { + union mksh_ccphack vap; + + vap.ro = ap; + return (vap.rw); + } + XPinit(w, 32); + /* space for shell name */ + XPput(w, NULL); + while (*ap != NULL) + expand(*ap++, &w, f); + XPput(w, NULL); + return ((char **)XPclose(w) + 1); +} + +/* + * expand string + */ +char * +evalstr(const char *cp, int f) +{ + XPtrV w; + char *dp = null; + + XPinit(w, 1); + expand(cp, &w, f); + if (XPsize(w)) + dp = *XPptrv(w); + XPfree(w); + return (dp); +} + +/* + * expand string - return only one component + * used from iosetup to expand redirection files + */ +char * +evalonestr(const char *cp, int f) +{ + XPtrV w; + char *rv; + + XPinit(w, 1); + expand(cp, &w, f); + switch (XPsize(w)) { + case 0: + rv = null; + break; + case 1: + rv = (char *) *XPptrv(w); + break; + default: + rv = evalstr(cp, f & ~DOGLOB); + break; + } + XPfree(w); + return (rv); +} + +/* for nested substitution: ${var:=$var2} */ +typedef struct SubType { + struct tbl *var; /* variable for ${var..} */ + struct SubType *prev; /* old type */ + struct SubType *next; /* poped type (to avoid re-allocating) */ + size_t base; /* start position of expanded word */ + short stype; /* [=+-?%#] action after expanded word */ + short f; /* saved value of f (DOPAT, etc) */ + uint8_t quotep; /* saved value of quote (for ${..[%#]..}) */ + uint8_t quotew; /* saved value of quote (for ${..[+-=]..}) */ +} SubType; + +void +expand( + /* input word */ + const char *ccp, + /* output words */ + XPtrV *wp, + /* DO* flags */ + int f) +{ + int c = 0; + /* expansion type */ + int type; + /* quoted */ + int quote = 0; + /* destination string and live pointer */ + XString ds; + char *dp; + /* source */ + const char *sp; + /* second pass flags */ + int fdo; + /* have word */ + int word; + /* field splitting of parameter/command substitution */ + int doblank; + /* expansion variables */ + Expand x = { + NULL, { NULL }, NULL, 0 + }; + SubType st_head, *st; + /* record number of trailing newlines in COMSUB */ + int newlines = 0; + bool saw_eq, make_magic; + unsigned int tilde_ok; + size_t len; + char *cp; + + if (ccp == NULL) + internal_errorf("expand(NULL)"); + /* for alias, readonly, set, typeset commands */ + if ((f & DOVACHECK) && is_wdvarassign(ccp)) { + f &= ~(DOVACHECK | DOBLANK | DOGLOB | DOTILDE); + f |= DOASNTILDE | DOSCALAR; + } + if (Flag(FNOGLOB)) + f &= ~DOGLOB; + if (Flag(FMARKDIRS)) + f |= DOMARKDIRS; + if (Flag(FBRACEEXPAND) && (f & DOGLOB)) + f |= DOBRACE; + + /* init destination string */ + Xinit(ds, dp, 128, ATEMP); + type = XBASE; + sp = ccp; + fdo = 0; + saw_eq = false; + /* must be 1/0 */ + tilde_ok = (f & (DOTILDE | DOASNTILDE)) ? 1 : 0; + doblank = 0; + make_magic = false; + word = (f&DOBLANK) ? IFS_WS : IFS_WORD; + /* clang doesn't know OSUBST comes before CSUBST */ + memset(&st_head, 0, sizeof(st_head)); + st = &st_head; + + while (/* CONSTCOND */ 1) { + Xcheck(ds, dp); + + switch (type) { + case XBASE: + /* original prefixed string */ + c = ord(*sp++); + switch (c) { + case EOS: + c = 0; + break; + case CHAR: + c = ord(*sp++); + break; + case QCHAR: + /* temporary quote */ + quote |= 2; + c = ord(*sp++); + break; + case OQUOTE: + if (word != IFS_WORD) + word = IFS_QUOTE; + tilde_ok = 0; + quote = 1; + continue; + case CQUOTE: + if (word == IFS_QUOTE) + word = IFS_WORD; + quote = st->quotew; + continue; + case COMASUB: + case COMSUB: + case FUNASUB: + case FUNSUB: + case VALSUB: + tilde_ok = 0; + if (f & DONTRUNCOMMAND) { + word = IFS_WORD; + *dp++ = '$'; + switch (c) { + case COMASUB: + case COMSUB: + *dp++ = '('; + c = ORD(')'); + break; + case FUNASUB: + case FUNSUB: + case VALSUB: + *dp++ = '{'; + *dp++ = c == VALSUB ? '|' : ' '; + c = ORD('}'); + break; + } + while (*sp != '\0') { + Xcheck(ds, dp); + *dp++ = *sp++; + } + if ((unsigned int)c == ORD('}')) + *dp++ = ';'; + *dp++ = c; + } else { + type = comsub(&x, sp, c); + if (type != XBASE && (f & DOBLANK)) + doblank++; + sp = strnul(sp) + 1; + newlines = 0; + } + continue; + case EXPRSUB: + tilde_ok = 0; + if (f & DONTRUNCOMMAND) { + word = IFS_WORD; + *dp++ = '$'; *dp++ = '('; *dp++ = '('; + while (*sp != '\0') { + Xcheck(ds, dp); + *dp++ = *sp++; + } + *dp++ = ')'; *dp++ = ')'; + } else { + struct tbl v; + + v.flag = DEFINED|ISSET|INTEGER; + /* not default */ + v.type = 10; + v.name[0] = '\0'; + v_evaluate(&v, substitute(sp, 0), + KSH_UNWIND_ERROR, true); + sp = strnul(sp) + 1; + x.str = str_val(&v); + type = XSUB; + if (f & DOBLANK) + doblank++; + } + continue; + case OSUBST: { + /* ${{#}var{:}[=+-?#%]word} */ + /*- + * format is: + * OSUBST [{x] plain-variable-part \0 + * compiled-word-part CSUBST [}x] + * This is where all syntax checking gets done... + */ + /* skip the { or x (}) */ + const char *varname = ++sp; + int stype; + int slen = 0; + + /* skip variable */ + sp = cstrchr(sp, '\0') + 1; + type = varsub(&x, varname, sp, &stype, &slen); + if (type < 0) { + char *beg, *end, *str; + unwind_substsyn: + /* restore sp */ + sp = varname - 2; + beg = wdcopy(sp, ATEMP); + end = (wdscan(cstrchr(sp, '\0') + 1, + CSUBST) - sp) + beg; + /* ({) the } or x is already skipped */ + if (end < wdscan(beg, EOS)) + *end = EOS; + str = snptreef(NULL, 64, Tf_S, beg); + afree(beg, ATEMP); + errorf(Tf_sD_s, str, Tbadsubst); + } + if (f & DOBLANK) + doblank++; + tilde_ok = 0; + if (word == IFS_QUOTE && type != XNULLSUB) + word = IFS_WORD; + if (type == XBASE) { + /* expand? */ + if (!st->next) { + SubType *newst; + + newst = alloc(sizeof(SubType), ATEMP); + newst->next = NULL; + newst->prev = st; + st->next = newst; + } + st = st->next; + st->stype = stype; + st->base = Xsavepos(ds, dp); + st->f = f; + if (x.var == vtemp) { + st->var = tempvar(vtemp->name); + st->var->flag &= ~INTEGER; + /* can't fail here */ + setstr(st->var, + str_val(x.var), + KSH_RETURN_ERROR | 0x4); + } else + st->var = x.var; + + st->quotew = st->quotep = quote; + /* skip qualifier(s) */ + if (stype) + sp += slen; + switch (stype & STYPE_SINGLE) { + case ORD('#') | STYPE_AT: + x.str = shf_smprintf("%08X", + (unsigned int)hash(str_val(st->var))); + break; + case ORD('Q') | STYPE_AT: { + struct shf shf; + + shf_sopen(NULL, 0, SHF_WR|SHF_DYNAMIC, &shf); + print_value_quoted(&shf, str_val(st->var)); + x.str = shf_sclose(&shf); + break; + } + case ORD('0'): { + char *beg, *mid, *end, *stg; + mksh_ari_t from = 0, num = -1, flen, finc = 0; + + beg = wdcopy(sp, ATEMP); + mid = beg + (wdscan(sp, ADELIM) - sp); + stg = beg + (wdscan(sp, CSUBST) - sp); + mid[-2] = EOS; + if (ord(mid[-1]) == ORD(/*{*/ '}')) { + sp += mid - beg - 1; + end = NULL; + } else { + end = mid + + (wdscan(mid, ADELIM) - mid); + if (ord(end[-1]) != ORD(/*{*/ '}')) + /* more than max delimiters */ + goto unwind_substsyn; + end[-2] = EOS; + sp += end - beg - 1; + } + evaluate(substitute(stg = wdstrip(beg, 0), 0), + &from, KSH_UNWIND_ERROR, true); + afree(stg, ATEMP); + if (end) { + evaluate(substitute(stg = wdstrip(mid, 0), 0), + &num, KSH_UNWIND_ERROR, true); + afree(stg, ATEMP); + } + afree(beg, ATEMP); + beg = str_val(st->var); + flen = utflen(beg); + if (from < 0) { + if (-from < flen) + finc = flen + from; + } else + finc = from < flen ? from : flen; + if (UTFMODE) + utfincptr(beg, &finc); + beg += finc; + flen = utflen(beg); + if (num < 0 || num > flen) + num = flen; + if (UTFMODE) + utfincptr(beg, &num); + strndupx(x.str, beg, num, ATEMP); + goto do_CSUBST; + } + case ORD('/') | STYPE_AT: + case ORD('/'): { + char *s, *p, *d, *sbeg, *end; + char *pat = NULL, *rrep = null; + char fpat = 0, *tpat1, *tpat2; + char *ws, *wpat, *wrep; + + s = ws = wdcopy(sp, ATEMP); + p = s + (wdscan(sp, ADELIM) - sp); + d = s + (wdscan(sp, CSUBST) - sp); + p[-2] = EOS; + if (ord(p[-1]) == ORD(/*{*/ '}')) + d = NULL; + else + d[-2] = EOS; + sp += (d ? d : p) - s - 1; + if (!(stype & STYPE_MASK) && + s[0] == CHAR && + ctype(s[1], C_SUB2)) + fpat = s[1]; + wpat = s + (fpat ? 2 : 0); + wrep = d ? p : NULL; + if (!(stype & STYPE_AT)) { + rrep = wrep ? evalstr(wrep, + DOTILDE | DOSCALAR) : + null; + } + + /* prepare string on which to work */ + strdupx(s, str_val(st->var), ATEMP); + sbeg = s; + again_search: + pat = evalstr(wpat, + DOTILDE | DOSCALAR | DOPAT); + /* check for special cases */ + if (!*pat && !fpat) { + /* + * empty unanchored + * pattern => reject + */ + goto no_repl; + } + if ((stype & STYPE_MASK) && + gmatchx(null, pat, false)) { + /* + * pattern matches empty + * string => don't loop + */ + stype &= ~STYPE_MASK; + } + + /* first see if we have any match at all */ + if (ord(fpat) == ORD('#')) { + /* anchor at the beginning */ + tpat1 = shf_smprintf("%s%c*", pat, MAGIC); + tpat2 = tpat1; + } else if (ord(fpat) == ORD('%')) { + /* anchor at the end */ + tpat1 = shf_smprintf("%c*%s", MAGIC, pat); + tpat2 = pat; + } else { + /* float */ + tpat1 = shf_smprintf("%c*%s%c*", MAGIC, pat, MAGIC); + tpat2 = tpat1 + 2; + } + again_repl: + /* + * this would not be necessary if gmatchx would return + * the start and end values of a match found, like re* + */ + if (!gmatchx(sbeg, tpat1, false)) + goto end_repl; + end = strnul(s); + /* now anchor the beginning of the match */ + if (ord(fpat) != ORD('#')) + while (sbeg <= end) { + if (gmatchx(sbeg, tpat2, false)) + break; + else + sbeg++; + } + /* now anchor the end of the match */ + p = end; + if (ord(fpat) != ORD('%')) + while (p >= sbeg) { + bool gotmatch; + + c = ord(*p); + *p = '\0'; + gotmatch = tobool(gmatchx(sbeg, pat, false)); + *p = c; + if (gotmatch) + break; + p--; + } + strndupx(end, sbeg, p - sbeg, ATEMP); + record_match(end); + afree(end, ATEMP); + if (stype & STYPE_AT) { + if (rrep != null) + afree(rrep, ATEMP); + rrep = wrep ? evalstr(wrep, + DOTILDE | DOSCALAR) : + null; + } + strndupx(end, s, sbeg - s, ATEMP); + d = shf_smprintf(Tf_sss, end, rrep, p); + afree(end, ATEMP); + sbeg = d + (sbeg - s) + strlen(rrep); + afree(s, ATEMP); + s = d; + if (stype & STYPE_AT) { + afree(tpat1, ATEMP); + afree(pat, ATEMP); + goto again_search; + } else if (stype & STYPE_DBL) + goto again_repl; + end_repl: + afree(tpat1, ATEMP); + x.str = s; + no_repl: + afree(pat, ATEMP); + if (rrep != null) + afree(rrep, ATEMP); + afree(ws, ATEMP); + goto do_CSUBST; + } + case ORD('#'): + case ORD('%'): + /* ! DOBLANK,DOBRACE */ + f = (f & DONTRUNCOMMAND) | + DOPAT | DOTILDE | + DOTEMP | DOSCALAR; + tilde_ok = 1; + st->quotew = quote = 0; + /* + * Prepend open pattern (so | + * in a trim will work as + * expected) + */ + if (!Flag(FSH)) { + *dp++ = MAGIC; + *dp++ = ORD(0x80 | '@'); + } + break; + case ORD('='): + /* + * Tilde expansion for string + * variables in POSIX mode is + * governed by Austinbug 351. + * In non-POSIX mode historic + * ksh behaviour (enable it!) + * us followed. + * Not doing tilde expansion + * for integer variables is a + * non-POSIX thing - makes + * sense though, since ~ is + * a arithmetic operator. + */ + if (!(x.var->flag & INTEGER)) + f |= DOASNTILDE | DOTILDE; + f |= DOTEMP | DOSCALAR; + /* + * These will be done after the + * value has been assigned. + */ + f &= ~(DOBLANK|DOGLOB|DOBRACE); + tilde_ok = 1; + break; + case ORD('?'): + if (*sp == CSUBST) + errorf("%s: parameter null or not set", + st->var->name); + f &= ~DOBLANK; + f |= DOTEMP; + /* FALLTHROUGH */ + default: + /* '-' '+' '?' */ + if (quote) + word = IFS_WORD; + else if (dp == Xstring(ds, dp)) + word = IFS_IWS; + /* Enable tilde expansion */ + tilde_ok = 1; + f |= DOTILDE; + } + } else + /* skip word */ + sp += wdscan(sp, CSUBST) - sp; + continue; + } + case CSUBST: + /* only get here if expanding word */ + do_CSUBST: + /* ({) skip the } or x */ + sp++; + /* in case of ${unset:-} */ + tilde_ok = 0; + *dp = '\0'; + quote = st->quotep; + f = st->f; + if (f & DOBLANK) + doblank--; + switch (st->stype & STYPE_SINGLE) { + case ORD('#'): + case ORD('%'): + if (!Flag(FSH)) { + /* Append end-pattern */ + *dp++ = MAGIC; + *dp++ = ')'; + } + *dp = '\0'; + dp = Xrestpos(ds, dp, st->base); + /* + * Must use st->var since calling + * global would break things + * like x[i+=1]. + */ + x.str = trimsub(str_val(st->var), + dp, st->stype); + if (x.str[0] != '\0') { + word = IFS_IWS; + type = XSUB; + } else if (quote) { + word = IFS_WORD; + type = XSUB; + } else { + if (dp == Xstring(ds, dp)) + word = IFS_IWS; + type = XNULLSUB; + } + if (f & DOBLANK) + doblank++; + st = st->prev; + continue; + case ORD('='): + /* + * Restore our position and substitute + * the value of st->var (may not be + * the assigned value in the presence + * of integer/right-adj/etc attributes). + */ + dp = Xrestpos(ds, dp, st->base); + /* + * Must use st->var since calling + * global would cause with things + * like x[i+=1] to be evaluated twice. + */ + /* + * Note: not exported by FEXPORT + * in AT&T ksh. + */ + /* + * XXX POSIX says readonly is only + * fatal for special builtins (setstr + * does readonly check). + */ + len = strlen(dp) + 1; + setstr(st->var, + debunk(alloc(len, ATEMP), + dp, len), KSH_UNWIND_ERROR); + x.str = str_val(st->var); + type = XSUB; + if (f & DOBLANK) + doblank++; + st = st->prev; + word = quote || (!*x.str && (f & DOSCALAR)) ? IFS_WORD : IFS_IWS; + continue; + case ORD('?'): + dp = Xrestpos(ds, dp, st->base); + + errorf(Tf_sD_s, st->var->name, + debunk(dp, dp, strlen(dp) + 1)); + break; + case ORD('0'): + case ORD('/') | STYPE_AT: + case ORD('/'): + case ORD('#') | STYPE_AT: + case ORD('Q') | STYPE_AT: + dp = Xrestpos(ds, dp, st->base); + type = XSUB; + word = quote || (!*x.str && (f & DOSCALAR)) ? IFS_WORD : IFS_IWS; + if (f & DOBLANK) + doblank++; + st = st->prev; + continue; + /* default: '-' '+' */ + } + st = st->prev; + type = XBASE; + continue; + + case OPAT: + /* open pattern: *(foo|bar) */ + /* Next char is the type of pattern */ + make_magic = true; + c = ord(*sp++) | 0x80U; + break; + + case SPAT: + /* pattern separator (|) */ + make_magic = true; + c = ORD('|'); + break; + + case CPAT: + /* close pattern */ + make_magic = true; + c = ORD(/*(*/ ')'); + break; + } + break; + + case XNULLSUB: + /* + * Special case for "$@" (and "${foo[@]}") - no + * word is generated if $# is 0 (unless there is + * other stuff inside the quotes). + */ + type = XBASE; + if (f & DOBLANK) { + doblank--; + if (dp == Xstring(ds, dp) && word != IFS_WORD) + word = IFS_IWS; + } + continue; + + case XSUB: + case XSUBMID: + if ((c = ord(*x.str++)) == 0) { + type = XBASE; + if (f & DOBLANK) + doblank--; + continue; + } + break; + + case XARGSEP: + type = XARG; + quote = 1; + /* FALLTHROUGH */ + case XARG: + if ((c = ord(*x.str++)) == '\0') { + /* + * force null words to be created so + * set -- "" 2 ""; echo "$@" will do + * the right thing + */ + if (quote && x.split) + word = IFS_WORD; + if ((x.str = *x.u.strv++) == NULL) { + type = XBASE; + if (f & DOBLANK) + doblank--; + continue; + } + c = ord(ifs0); + if ((f & DOHEREDOC)) { + /* pseudo-field-split reliably */ + if (c == 0) + c = ORD(' '); + break; + } + if ((f & DOSCALAR)) { + /* do not field-split */ + if (x.split) { + c = ORD(' '); + break; + } + if (c == 0) + continue; + } + if (c == 0) { + if (quote && !x.split) + continue; + if (!quote && word == IFS_WS) + continue; + /* this is so we don't terminate */ + c = ORD(' '); + /* now force-emit a word */ + goto emit_word; + } + if (quote && x.split) { + /* terminate word for "$@" */ + type = XARGSEP; + quote = 0; + } + } + break; + + case XCOM: + if (x.u.shf == NULL) { + /* $(<...) failed */ + subst_exstat = 1; + /* fake EOF */ + c = -1; + } else if (newlines) { + /* spit out saved NLs */ + c = ORD('\n'); + --newlines; + } else { + while ((c = shf_getc(x.u.shf)) == 0 || + cinttype(c, C_NL)) { +#ifdef MKSH_WITH_TEXTMODE + if (c == ORD('\r')) { + c = shf_getc(x.u.shf); + switch (c) { + case ORD('\n'): + break; + default: + shf_ungetc(c, x.u.shf); + /* FALLTHROUGH */ + case -1: + c = ORD('\r'); + break; + } + } +#endif + if (c == ORD('\n')) + /* save newlines */ + newlines++; + } + if (newlines && c != -1) { + shf_ungetc(c, x.u.shf); + c = ORD('\n'); + --newlines; + } + } + if (c == -1) { + newlines = 0; + if (x.u.shf) + shf_close(x.u.shf); + if (x.split) + subst_exstat = waitlast(); + type = XBASE; + if (f & DOBLANK) + doblank--; + continue; + } + break; + } + + /* check for end of word or IFS separation */ + if (c == 0 || (!quote && (f & DOBLANK) && doblank && + !make_magic && ctype(c, C_IFS))) { + /*- + * How words are broken up: + * | value of c + * word | ws nws 0 + * ----------------------------------- + * IFS_WORD w/WS w/NWS w + * IFS_WS -/WS -/NWS - + * IFS_NWS -/NWS w/NWS - + * IFS_IWS -/WS w/NWS - + * (w means generate a word) + */ + if ((word == IFS_WORD) || (word == IFS_QUOTE) || (c && + (word == IFS_IWS || word == IFS_NWS) && + !ctype(c, C_IFSWS))) { + emit_word: + if (f & DOHERESTR) + *dp++ = '\n'; + *dp++ = '\0'; + cp = Xclose(ds, dp); + if (fdo & DOBRACE) + /* also does globbing */ + alt_expand(wp, cp, cp, + cp + Xlength(ds, (dp - 1)), + fdo | (f & DOMARKDIRS)); + else if (fdo & DOGLOB) + glob(cp, wp, tobool(f & DOMARKDIRS)); + else if ((f & DOPAT) || !(fdo & DOMAGIC)) + XPput(*wp, cp); + else + XPput(*wp, debunk(cp, cp, + strlen(cp) + 1)); + fdo = 0; + saw_eq = false; + /* must be 1/0 */ + tilde_ok = (f & (DOTILDE | DOASNTILDE)) ? 1 : 0; + if (c == 0) + return; + Xinit(ds, dp, 128, ATEMP); + } else if (c == 0) { + return; + } else if (type == XSUB && ctype(c, C_IFS) && + !ctype(c, C_IFSWS) && Xlength(ds, dp) == 0) { + *(cp = alloc(1, ATEMP)) = '\0'; + XPput(*wp, cp); + type = XSUBMID; + } + if (word != IFS_NWS) + word = ctype(c, C_IFSWS) ? IFS_WS : IFS_NWS; + } else { + if (type == XSUB) { + if (word == IFS_NWS && + Xlength(ds, dp) == 0) { + *(cp = alloc(1, ATEMP)) = '\0'; + XPput(*wp, cp); + } + type = XSUBMID; + } + + /* age tilde_ok info - ~ code tests second bit */ + tilde_ok <<= 1; + /* mark any special second pass chars */ + if (!quote) + switch (ord(c)) { + case ORD('['): + case ORD('!'): + case ORD('-'): + case ORD(']'): + /* + * For character classes - doesn't hurt + * to have magic !,-,]s outside of + * [...] expressions. + */ + if (f & (DOPAT | DOGLOB)) { + fdo |= DOMAGIC; + if ((unsigned int)c == ORD('[')) + fdo |= f & DOGLOB; + *dp++ = MAGIC; + } + break; + case ORD('*'): + case ORD('?'): + if (f & (DOPAT | DOGLOB)) { + fdo |= DOMAGIC | (f & DOGLOB); + *dp++ = MAGIC; + } + break; + case ORD('{'): + case ORD('}'): + case ORD(','): + if ((f & DOBRACE) && + (ord(c) == ORD('{' /*}*/) || + (fdo & DOBRACE))) { + fdo |= DOBRACE|DOMAGIC; + *dp++ = MAGIC; + } + break; + case ORD('='): + /* Note first unquoted = for ~ */ + if (!(f & DOTEMP) && (!Flag(FPOSIX) || + (f & DOASNTILDE)) && !saw_eq) { + saw_eq = true; + tilde_ok = 1; + } + break; + case ORD(':'): + /* : */ + /* Note unquoted : for ~ */ + if (!(f & DOTEMP) && (f & DOASNTILDE)) + tilde_ok = 1; + break; + case ORD('~'): + /* + * tilde_ok is reset whenever + * any of ' " $( $(( ${ } are seen. + * Note that tilde_ok must be preserved + * through the sequence ${A=a=}~ + */ + if (type == XBASE && + (f & (DOTILDE | DOASNTILDE)) && + (tilde_ok & 2)) { + const char *tcp; + char *tdp = dp; + + tcp = maybe_expand_tilde(sp, + &ds, &tdp, + tobool(f & DOASNTILDE)); + if (tcp) { + if (dp != tdp) + word = IFS_WORD; + dp = tdp; + sp = tcp; + continue; + } + } + break; + } + else + /* undo temporary */ + quote &= ~2; + + if (make_magic) { + make_magic = false; + fdo |= DOMAGIC | (f & DOGLOB); + *dp++ = MAGIC; + } else if (ISMAGIC(c)) { + fdo |= DOMAGIC; + *dp++ = MAGIC; + } + /* save output char */ + *dp++ = c; + word = IFS_WORD; + } + } +} + +static bool +hasnonempty(const char **strv) +{ + size_t i = 0; + + while (strv[i]) + if (*strv[i++]) + return (true); + return (false); +} + +/* + * Prepare to generate the string returned by ${} substitution. + */ +static int +varsub(Expand *xp, const char *sp, const char *word, + int *stypep, /* becomes qualifier type */ + int *slenp) /* " " len (=, :=, etc.) valid iff *stypep != 0 */ +{ + int c; + int state; /* next state: XBASE, XARG, XSUB, XNULLSUB */ + int stype; /* substitution type */ + int slen = 0; + const char *p; + struct tbl *vp; + bool zero_ok = false; + + if ((stype = ord(sp[0])) == '\0') + /* Bad variable name */ + return (-1); + + xp->var = NULL; + + /*- + * ${#var}, string length (-U: characters, +U: octets) or array size + * ${%var}, string width (-U: screen columns, +U: octets) + */ + c = ord(sp[1]); + if ((unsigned int)stype == ORD('%') && c == '\0') + return (-1); + if (ctype(stype, C_SUB2) && c != '\0') { + /* Can't have any modifiers for ${#...} or ${%...} */ + if (*word != CSUBST) + return (-1); + sp++; + /* Check for size of array */ + if ((p = cstrchr(sp, '[')) && (ord(p[1]) == ORD('*') || + ord(p[1]) == ORD('@')) && ord(p[2]) == ORD(']')) { + int n = 0; + + if ((unsigned int)stype != ORD('#')) + return (-1); + vp = global(arrayname(sp)); + if (vp->flag & (ISSET|ARRAY)) + zero_ok = true; + for (; vp; vp = vp->u.array) + if (vp->flag & ISSET) + n++; + c = n; + } else if ((unsigned int)c == ORD('*') || + (unsigned int)c == ORD('@')) { + if ((unsigned int)stype != ORD('#')) + return (-1); + c = e->loc->argc; + } else { + p = str_val(global(sp)); + zero_ok = p != null; + if ((unsigned int)stype == ORD('#')) + c = utflen(p); + else { + /* partial utf_mbswidth reimplementation */ + const char *s = p; + unsigned int wc; + size_t len; + int cw; + + c = 0; + while (*s) { + if (!UTFMODE || (len = utf_mbtowc(&wc, + s)) == (size_t)-1) + /* not UTFMODE or not UTF-8 */ + wc = rtt2asc(*s++); + else + /* UTFMODE and UTF-8 */ + s += len; + /* wc == char or wchar at s++ */ + if ((cw = utf_wcwidth(wc)) == -1) { + /* 646, 8859-1, 10646 C0/C1 */ + c = -1; + break; + } + c += cw; + } + } + } + if (Flag(FNOUNSET) && c == 0 && !zero_ok) + errorf(Tf_parm, sp); + /* unqualified variable/string substitution */ + *stypep = 0; + xp->str = shf_smprintf(Tf_d, c); + return (XSUB); + } + if ((unsigned int)stype == ORD('!') && c != '\0' && *word == CSUBST) { + sp++; + if ((p = cstrchr(sp, '[')) && (ord(p[1]) == ORD('*') || + ord(p[1]) == ORD('@')) && ord(p[2]) == ORD(']')) { + c = ORD('!'); + stype = 0; + goto arraynames; + } + xp->var = global(sp); + xp->str = p ? shf_smprintf("%s[%lu]", + xp->var->name, arrayindex(xp->var)) : xp->var->name; + *stypep = 0; + return (XSUB); + } + + /* Check for qualifiers in word part */ + stype = 0; + c = word[slen + 0] == CHAR ? ord(word[slen + 1]) : 0; + if ((unsigned int)c == ORD(':')) { + slen += 2; + stype = STYPE_DBL; + c = word[slen + 0] == CHAR ? ord(word[slen + 1]) : 0; + } + if (!stype && (unsigned int)c == ORD('/')) { + slen += 2; + stype = c; + if (word[slen] == ADELIM && + ord(word[slen + 1]) == (unsigned int)c) { + slen += 2; + stype |= STYPE_DBL; + } + } else if (stype == STYPE_DBL && ((unsigned int)c == ORD(' ') || + (unsigned int)c == ORD('0'))) { + stype |= ORD('0'); + } else if (ctype(c, C_SUB1)) { + slen += 2; + stype |= c; + } else if (ctype(c, C_SUB2)) { + /* Note: ksh88 allows :%, :%%, etc */ + slen += 2; + stype = c; + if (word[slen + 0] == CHAR && + ord(word[slen + 1]) == (unsigned int)c) { + stype |= STYPE_DBL; + slen += 2; + } + } else if ((unsigned int)c == ORD('@')) { + /* @x where x is command char */ + switch (c = ord(word[slen + 2]) == CHAR ? + ord(word[slen + 3]) : 0) { + case ORD('#'): + case ORD('/'): + case ORD('Q'): + break; + default: + return (-1); + } + stype |= STYPE_AT | c; + slen += 4; + } else if (stype) + /* : is not ok */ + return (-1); + if (!stype && *word != CSUBST) + return (-1); + + c = ord(sp[0]); + if ((unsigned int)c == ORD('*') || (unsigned int)c == ORD('@')) { + switch (stype & STYPE_SINGLE) { + /* can't assign to a vector */ + case ORD('='): + /* can't trim a vector (yet) */ + case ORD('%'): + case ORD('#'): + case ORD('?'): + case ORD('0'): + case ORD('/') | STYPE_AT: + case ORD('/'): + case ORD('#') | STYPE_AT: + case ORD('Q') | STYPE_AT: + return (-1); + } + if (e->loc->argc == 0) { + xp->str = null; + xp->var = global(sp); + state = (unsigned int)c == ORD('@') ? XNULLSUB : XSUB; + } else { + xp->u.strv = (const char **)e->loc->argv + 1; + xp->str = *xp->u.strv++; + /* $@ */ + xp->split = tobool((unsigned int)c == ORD('@')); + state = XARG; + } + /* POSIX 2009? */ + zero_ok = true; + } else if ((p = cstrchr(sp, '[')) && (ord(p[1]) == ORD('*') || + ord(p[1]) == ORD('@')) && ord(p[2]) == ORD(']')) { + XPtrV wv; + + switch (stype & STYPE_SINGLE) { + /* can't assign to a vector */ + case ORD('='): + /* can't trim a vector (yet) */ + case ORD('%'): + case ORD('#'): + case ORD('?'): + case ORD('0'): + case ORD('/') | STYPE_AT: + case ORD('/'): + case ORD('#') | STYPE_AT: + case ORD('Q') | STYPE_AT: + return (-1); + } + c = 0; + arraynames: + XPinit(wv, 32); + vp = global(arrayname(sp)); + for (; vp; vp = vp->u.array) { + if (!(vp->flag&ISSET)) + continue; + XPput(wv, (unsigned int)c == ORD('!') ? + shf_smprintf(Tf_lu, arrayindex(vp)) : + str_val(vp)); + } + if (XPsize(wv) == 0) { + xp->str = null; + state = ord(p[1]) == ORD('@') ? XNULLSUB : XSUB; + XPfree(wv); + } else { + XPput(wv, 0); + xp->u.strv = (const char **)XPptrv(wv); + xp->str = *xp->u.strv++; + /* ${foo[@]} */ + xp->split = tobool(ord(p[1]) == ORD('@')); + state = XARG; + } + } else { + xp->var = global(sp); + xp->str = str_val(xp->var); + /* can't assign things like $! or $1 */ + if ((unsigned int)(stype & STYPE_SINGLE) == ORD('=') && + !*xp->str && ctype(*sp, C_VAR1 | C_DIGIT)) + return (-1); + state = XSUB; + } + + c = stype & STYPE_CHAR; + /* test the compiler's code generator */ + if ((!(stype & STYPE_AT) && (ctype(c, C_SUB2) || + (((stype & STYPE_DBL) ? *xp->str == '\0' : xp->str == null) && + (state != XARG || (ifs0 || xp->split ? + (xp->u.strv[0] == NULL) : !hasnonempty(xp->u.strv))) ? + ctype(c, C_EQUAL | C_MINUS | C_QUEST) : (unsigned int)c == ORD('+')))) || + (unsigned int)stype == (ORD('0') | STYPE_DBL) || + (unsigned int)stype == (ORD('#') | STYPE_AT) || + (unsigned int)stype == (ORD('Q') | STYPE_AT) || + (unsigned int)(stype & STYPE_CHAR) == ORD('/')) + /* expand word instead of variable value */ + state = XBASE; + if (Flag(FNOUNSET) && xp->str == null && !zero_ok && + (ctype(c, C_SUB2) || (state != XBASE && (unsigned int)c != ORD('+')))) + errorf(Tf_parm, sp); + *stypep = stype; + *slenp = slen; + return (state); +} + +/* + * Run the command in $(...) and read its output. + */ +static int +comsub(Expand *xp, const char *cp, int fn) +{ + Source *s, *sold; + struct op *t; + struct shf *shf; + bool doalias = false; + uint8_t old_utfmode = UTFMODE; + + switch (fn) { + case COMASUB: + fn = COMSUB; + if (0) + /* FALLTHROUGH */ + case FUNASUB: + fn = FUNSUB; + doalias = true; + } + + s = pushs(SSTRING, ATEMP); + s->start = s->str = cp; + sold = source; + t = compile(s, true, doalias); + afree(s, ATEMP); + source = sold; + + UTFMODE = old_utfmode; + + if (t == NULL) + return (XBASE); + + /* no waitlast() unless specifically enabled later */ + xp->split = false; + + if (t->type == TCOM && + *t->args == NULL && *t->vars == NULL && t->ioact != NULL) { + /* $(ioact; + char *name; + + switch (io->ioflag & IOTYPE) { + case IOREAD: + shf = shf_open(name = evalstr(io->ioname, DOTILDE), + O_RDONLY, 0, SHF_MAPHI | SHF_CLEXEC); + if (shf == NULL) + warningf(!Flag(FTALKING), Tf_sD_s_sD_s, + name, Tcant_open, "$(<...) input", + cstrerror(errno)); + break; + case IOHERE: + if (!herein(io, &name)) { + xp->str = name; + /* as $(…) requires, trim trailing newlines */ + name = strnul(name); + while (name > xp->str && name[-1] == '\n') + --name; + *name = '\0'; + return (XSUB); + } + shf = NULL; + break; + default: + errorf(Tf_sD_s, T_funny_command, + snptreef(NULL, 32, Tft_R, io)); + } + } else if (fn == FUNSUB) { + int ofd1; + struct temp *tf = NULL; + + /* + * create a temporary file, open for reading and writing, + * with an shf open for reading (buffered) but yet unused + */ + maketemp(ATEMP, TT_FUNSUB, &tf); + if (!tf->shf) { + errorf(Tf_temp, + Tcreate, tf->tffn, cstrerror(errno)); + } + /* extract shf from temporary file, unlink and free it */ + shf = tf->shf; + unlink(tf->tffn); + afree(tf, ATEMP); + /* save stdout and let it point to the tempfile */ + ofd1 = savefd(1); + ksh_dup2(shf_fileno(shf), 1, false); + /* + * run tree, with output thrown into the tempfile, + * in a new function block + */ + valsub(t, NULL); + subst_exstat = exstat & 0xFF; + /* rewind the tempfile and restore regular stdout */ + lseek(shf_fileno(shf), (off_t)0, SEEK_SET); + restfd(1, ofd1); + } else if (fn == VALSUB) { + xp->str = valsub(t, ATEMP); + subst_exstat = exstat & 0xFF; + return (XSUB); + } else { + int ofd1, pv[2]; + + openpipe(pv); + shf = shf_fdopen(pv[0], SHF_RD, NULL); + ofd1 = savefd(1); + if (pv[1] != 1) { + ksh_dup2(pv[1], 1, false); + close(pv[1]); + } + execute(t, XXCOM | XPIPEO | XFORK, NULL); + restfd(1, ofd1); + startlast(); + /* waitlast() */ + xp->split = true; + } + + xp->u.shf = shf; + return (XCOM); +} + +/* + * perform #pattern and %pattern substitution in ${} + */ +static char * +trimsub(char *str, char *pat, int how) +{ + char *end = strnul(str); + char *p, c; + + switch (how & (STYPE_CHAR | STYPE_DBL)) { + case ORD('#'): + /* shortest match at beginning */ + for (p = str; p <= end; p += utf_ptradj(p)) { + c = *p; *p = '\0'; + if (gmatchx(str, pat, false)) { + record_match(str); + *p = c; + return (p); + } + *p = c; + } + break; + case ORD('#') | STYPE_DBL: + /* longest match at beginning */ + for (p = end; p >= str; p--) { + c = *p; *p = '\0'; + if (gmatchx(str, pat, false)) { + record_match(str); + *p = c; + return (p); + } + *p = c; + } + break; + case ORD('%'): + /* shortest match at end */ + p = end; + while (p >= str) { + if (gmatchx(p, pat, false)) + goto trimsub_match; + if (UTFMODE) { + char *op = p; + while ((p-- > str) && ((rtt2asc(*p) & 0xC0) == 0x80)) + ; + if ((p < str) || (p + utf_ptradj(p) != op)) + p = op - 1; + } else + --p; + } + break; + case ORD('%') | STYPE_DBL: + /* longest match at end */ + for (p = str; p <= end; p++) + if (gmatchx(p, pat, false)) { + trimsub_match: + record_match(p); + strndupx(end, str, p - str, ATEMP); + return (end); + } + break; + } + + /* no match, return string */ + return (str); +} + +/* + * glob + * Name derived from V6's /etc/glob, the program that expanded filenames. + */ + +/* XXX cp not const 'cause slashes are temporarily replaced with NULs... */ +static void +glob(char *cp, XPtrV *wp, bool markdirs) +{ + int oldsize = XPsize(*wp); + + if (glob_str(cp, wp, markdirs) == 0) + XPput(*wp, debunk(cp, cp, strlen(cp) + 1)); + else + qsort(XPptrv(*wp) + oldsize, XPsize(*wp) - oldsize, + sizeof(void *), ascpstrcmp); +} + +#define GF_NONE 0 +#define GF_EXCHECK BIT(0) /* do existence check on file */ +#define GF_GLOBBED BIT(1) /* some globbing has been done */ +#define GF_MARKDIR BIT(2) /* add trailing / to directories */ + +/* + * Apply file globbing to cp and store the matching files in wp. Returns + * the number of matches found. + */ +int +glob_str(char *cp, XPtrV *wp, bool markdirs) +{ + int oldsize = XPsize(*wp); + XString xs; + char *xp; + + Xinit(xs, xp, 256, ATEMP); + globit(&xs, &xp, cp, wp, markdirs ? GF_MARKDIR : GF_NONE); + Xfree(xs, xp); + + return (XPsize(*wp) - oldsize); +} + +static void +globit(XString *xs, /* dest string */ + char **xpp, /* ptr to dest end */ + char *sp, /* source path */ + XPtrV *wp, /* output list */ + int check) /* GF_* flags */ +{ + char *np; /* next source component */ + char *xp = *xpp; + char *se; + char odirsep; + + /* This to allow long expansions to be interrupted */ + intrcheck(); + + if (sp == NULL) { + /* end of source path */ + /* + * We only need to check if the file exists if a pattern + * is followed by a non-pattern (eg, foo*x/bar; no check + * is needed for foo* since the match must exist) or if + * any patterns were expanded and the markdirs option is set. + * Symlinks make things a bit tricky... + */ + if ((check & GF_EXCHECK) || + ((check & GF_MARKDIR) && (check & GF_GLOBBED))) { +#define stat_check() (stat_done ? stat_done : (stat_done = \ + stat(Xstring(*xs, xp), &statb) < 0 ? -1 : 1)) + struct stat lstatb, statb; + /* -1: failed, 1 ok, 0 not yet done */ + int stat_done = 0; + + if (mksh_lstat(Xstring(*xs, xp), &lstatb) < 0) + return; + /* + * special case for systems which strip trailing + * slashes from regular files (eg, /etc/passwd/). + * SunOS 4.1.3 does this... + */ + if ((check & GF_EXCHECK) && xp > Xstring(*xs, xp) && + mksh_cdirsep(xp[-1]) && !S_ISDIR(lstatb.st_mode) && + (!S_ISLNK(lstatb.st_mode) || + stat_check() < 0 || !S_ISDIR(statb.st_mode))) + return; + /* + * Possibly tack on a trailing / if there isn't already + * one and if the file is a directory or a symlink to a + * directory + */ + if (((check & GF_MARKDIR) && (check & GF_GLOBBED)) && + xp > Xstring(*xs, xp) && !mksh_cdirsep(xp[-1]) && + (S_ISDIR(lstatb.st_mode) || + (S_ISLNK(lstatb.st_mode) && stat_check() > 0 && + S_ISDIR(statb.st_mode)))) { + *xp++ = '/'; + *xp = '\0'; + } + } + strndupx(np, Xstring(*xs, xp), Xlength(*xs, xp), ATEMP); + XPput(*wp, np); + return; + } + + if (xp > Xstring(*xs, xp)) + *xp++ = '/'; + while (mksh_cdirsep(*sp)) { + Xcheck(*xs, xp); + *xp++ = *sp++; + } + np = mksh_sdirsep(sp); + if (np != NULL) { + se = np; + /* don't assume '/', can be multiple kinds */ + odirsep = *np; + *np++ = '\0'; + } else { + odirsep = '\0'; /* keep gcc quiet */ + se = strnul(sp); + } + + + /* + * Check if sp needs globbing - done to avoid pattern checks for strings + * containing MAGIC characters, open [s without the matching close ], + * etc. (otherwise opendir() will be called which may fail because the + * directory isn't readable - if no globbing is needed, only execute + * permission should be required (as per POSIX)). + */ + if (!has_globbing(sp)) { + XcheckN(*xs, xp, se - sp + 1); + debunk(xp, sp, Xnleft(*xs, xp)); + xp = strnul(xp); + *xpp = xp; + globit(xs, xpp, np, wp, check); + } else { + DIR *dirp; + struct dirent *d; + char *name; + size_t len, prefix_len; + + /* xp = *xpp; copy_non_glob() may have re-alloc'd xs */ + *xp = '\0'; + prefix_len = Xlength(*xs, xp); + dirp = opendir(prefix_len ? Xstring(*xs, xp) : Tdot); + if (dirp == NULL) + goto Nodir; + while ((d = readdir(dirp)) != NULL) { + name = d->d_name; + if (name[0] == '.' && + (name[1] == 0 || (name[1] == '.' && name[2] == 0))) + /* always ignore . and .. */ + continue; + if ((*name == '.' && *sp != '.') || + !gmatchx(name, sp, true)) + continue; + + len = strlen(d->d_name) + 1; + XcheckN(*xs, xp, len); + memcpy(xp, name, len); + *xpp = xp + len - 1; + globit(xs, xpp, np, wp, (check & GF_MARKDIR) | + GF_GLOBBED | (np ? GF_EXCHECK : GF_NONE)); + xp = Xstring(*xs, xp) + prefix_len; + } + closedir(dirp); + Nodir: + ; + } + + if (np != NULL) + *--np = odirsep; +} + +/* remove MAGIC from string */ +char * +debunk(char *dp, const char *sp, size_t dlen) +{ + char *d; + const char *s; + + if ((s = cstrchr(sp, MAGIC))) { + if (s - sp >= (ssize_t)dlen) + return (dp); + memmove(dp, sp, s - sp); + for (d = dp + (s - sp); *s && (d - dp < (ssize_t)dlen); s++) + if (!ISMAGIC(*s) || !(*++s & 0x80) || + !ctype(*s & 0x7F, C_PATMO | C_SPC)) + *d++ = *s; + else { + /* extended pattern operators: *+?@! */ + if ((*s & 0x7f) != ' ') + *d++ = *s & 0x7f; + if (d - dp < (ssize_t)dlen) + *d++ = '('; + } + *d = '\0'; + } else if (dp != sp) + strlcpy(dp, sp, dlen); + return (dp); +} + +/* + * Check if p is an unquoted name, possibly followed by a / or :. If so + * puts the expanded version in *dcp,dp and returns a pointer in p just + * past the name, otherwise returns 0. + */ +static const char * +maybe_expand_tilde(const char *p, XString *dsp, char **dpp, bool isassign) +{ + XString ts; + char *dp = *dpp; + char *tp; + const char *r; + + Xinit(ts, tp, 16, ATEMP); + /* : only for DOASNTILDE form */ + while (p[0] == CHAR && /* not cdirsep */ p[1] != '/' && + (!isassign || p[1] != ':')) { + Xcheck(ts, tp); + *tp++ = p[1]; + p += 2; + } + *tp = '\0'; + r = (p[0] == EOS || p[0] == CHAR || p[0] == CSUBST) ? + do_tilde(Xstring(ts, tp)) : NULL; + Xfree(ts, tp); + if (r) { + while (*r) { + Xcheck(*dsp, dp); + if (ISMAGIC(*r)) + *dp++ = MAGIC; + *dp++ = *r++; + } + *dpp = dp; + r = p; + } + return (r); +} + +/* + * tilde expansion + * + * based on a version by Arnold Robbins + */ +char * +do_tilde(char *cp) +{ + char *dp = null; +#ifndef MKSH_NOPWNAM + bool do_simplify = true; +#endif + + if (cp[0] == '\0') + dp = str_val(global("HOME")); + else if (cp[0] == '+' && cp[1] == '\0') + dp = str_val(global(TPWD)); + else if (ksh_isdash(cp)) + dp = str_val(global(TOLDPWD)); +#ifndef MKSH_NOPWNAM + else { + dp = homedir(cp); + do_simplify = false; + } +#endif + + /* if parameters aren't set, don't expand ~ */ + if (dp == NULL || dp == null) + return (NULL); + + /* simplify parameters as if cwd upon entry */ +#ifndef MKSH_NOPWNAM + if (do_simplify) +#endif + { + strdupx(dp, dp, ATEMP); + simplify_path(dp); + } + return (dp); +} + +#ifndef MKSH_NOPWNAM +/* + * map userid to user's home directory. + * note that 4.3's getpw adds more than 6K to the shell, + * and the YP version probably adds much more. + * we might consider our own version of getpwnam() to keep the size down. + */ +static char * +homedir(char *name) +{ + struct tbl *ap; + + ap = ktenter(&homedirs, name, hash(name)); + if (!(ap->flag & ISSET)) { + struct passwd *pw; + + pw = getpwnam(name); + if (pw == NULL) + return (NULL); + strdupx(ap->val.s, pw->pw_dir, APERM); + ap->flag |= DEFINED|ISSET|ALLOC; + } + return (ap->val.s); +} +#endif + +static void +alt_expand(XPtrV *wp, char *start, char *exp_start, char *end, int fdo) +{ + unsigned int count = 0; + char *brace_start, *brace_end, *comma = NULL; + char *field_start; + char *p = exp_start; + + /* search for open brace */ + while ((p = strchr(p, MAGIC)) && ord(p[1]) != ORD('{' /*}*/)) + p += 2; + brace_start = p; + + /* find matching close brace, if any */ + if (p) { + comma = NULL; + count = 1; + p += 2; + while (*p && count) { + if (ISMAGIC(*p++)) { + if (ord(*p) == ORD('{' /*}*/)) + ++count; + else if (ord(*p) == ORD(/*{*/ '}')) + --count; + else if (*p == ',' && count == 1) + comma = p; + ++p; + } + } + } + /* no valid expansions... */ + if (!p || count != 0) { + /* + * Note that given a{{b,c} we do not expand anything (this is + * what AT&T ksh does. This may be changed to do the {b,c} + * expansion. } + */ + if (fdo & DOGLOB) + glob(start, wp, tobool(fdo & DOMARKDIRS)); + else + XPput(*wp, debunk(start, start, end - start)); + return; + } + brace_end = p; + if (!comma) { + alt_expand(wp, start, brace_end, end, fdo); + return; + } + + /* expand expression */ + field_start = brace_start + 2; + count = 1; + for (p = brace_start + 2; p != brace_end; p++) { + if (ISMAGIC(*p)) { + if (ord(*++p) == ORD('{' /*}*/)) + ++count; + else if ((ord(*p) == ORD(/*{*/ '}') && --count == 0) || + (*p == ',' && count == 1)) { + char *news; + int l1, l2, l3; + + /* + * addition safe since these operate on + * one string (separate substrings) + */ + l1 = brace_start - start; + l2 = (p - 1) - field_start; + l3 = end - brace_end; + news = alloc(l1 + l2 + l3 + 1, ATEMP); + memcpy(news, start, l1); + memcpy(news + l1, field_start, l2); + memcpy(news + l1 + l2, brace_end, l3); + news[l1 + l2 + l3] = '\0'; + alt_expand(wp, news, news + l1, + news + l1 + l2 + l3, fdo); + field_start = p + 1; + } + } + } + return; +} + +/* helper function due to setjmp/longjmp woes */ +static char * +valsub(struct op *t, Area *ap) +{ + char * volatile cp = NULL; + struct tbl * volatile vp = NULL; + + newenv(E_FUNC); + newblock(); + if (ap) + vp = local("REPLY", false); + if (!kshsetjmp(e->jbuf)) + execute(t, XXCOM | XERROK, NULL); + if (vp) + strdupx(cp, str_val(vp), ap); + quitenv(NULL); + + return (cp); +} diff --git a/exec.c b/exec.c new file mode 100644 index 0000000..8330174 --- /dev/null +++ b/exec.c @@ -0,0 +1,1865 @@ +/* $OpenBSD: exec.c,v 1.52 2015/09/10 22:48:58 nicm Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/exec.c,v 1.201 2017/10/11 21:09:24 tg Exp $"); + +#ifndef MKSH_DEFAULT_EXECSHELL +#define MKSH_DEFAULT_EXECSHELL MKSH_UNIXROOT "/bin/sh" +#endif + +static int comexec(struct op *, struct tbl * volatile, const char **, + int volatile, volatile int *); +static void scriptexec(struct op *, const char **) MKSH_A_NORETURN; +static int call_builtin(struct tbl *, const char **, const char *, bool); +static int iosetup(struct ioword *, struct tbl *); +static const char *do_selectargs(const char **, bool); +static Test_op dbteste_isa(Test_env *, Test_meta); +static const char *dbteste_getopnd(Test_env *, Test_op, bool); +static void dbteste_error(Test_env *, int, const char *); +/* XXX: horrible kludge to fit within the framework */ +static void plain_fmt_entry(char *, size_t, unsigned int, const void *); +static void select_fmt_entry(char *, size_t, unsigned int, const void *); + +/* + * execute command tree + */ +int +execute(struct op * volatile t, + /* if XEXEC don't fork */ + volatile int flags, + volatile int * volatile xerrok) +{ + int i; + volatile int rv = 0, dummy = 0; + int pv[2]; + const char ** volatile ap = NULL; + char ** volatile up; + const char *s, *ccp; + struct ioword **iowp; + struct tbl *tp = NULL; + + if (t == NULL) + return (0); + + /* Caller doesn't care if XERROK should propagate. */ + if (xerrok == NULL) + xerrok = &dummy; + + if ((flags&XFORK) && !(flags&XEXEC) && t->type != TPIPE) + /* run in sub-process */ + return (exchild(t, flags & ~XTIME, xerrok, -1)); + + newenv(E_EXEC); + if (trap) + runtraps(0); + + /* we want to run an executable, do some variance checks */ + if (t->type == TCOM) { + /* + * Clear subst_exstat before argument expansion. Used by + * null commands (see comexec() and c_eval()) and by c_set(). + */ + subst_exstat = 0; + + /* for $LINENO */ + current_lineno = t->lineno; + + /* check if this is 'var=<args[0] == NULL && + /* we have exactly one variable assignment */ + t->vars[0] != NULL && t->vars[1] == NULL && + /* we have exactly one I/O redirection */ + t->ioact != NULL && t->ioact[0] != NULL && + t->ioact[1] == NULL && + /* of type "here document" (or "here string") */ + (t->ioact[0]->ioflag & IOTYPE) == IOHERE && + /* the variable assignment begins with a valid varname */ + (ccp = skip_wdvarname(t->vars[0], true)) != t->vars[0] && + /* and has no right-hand side (i.e. "varname=") */ + ccp[0] == CHAR && ((ccp[1] == '=' && ccp[2] == EOS) || + /* or "varname+=" */ (ccp[1] == '+' && ccp[2] == CHAR && + ccp[3] == '=' && ccp[4] == EOS))) { + char *cp, *dp; + + if ((rv = herein(t->ioact[0], &cp) /*? 1 : 0*/)) + cp = NULL; + dp = shf_smprintf(Tf_ss, evalstr(t->vars[0], + DOASNTILDE | DOSCALAR), rv ? null : cp); + typeset(dp, Flag(FEXPORT) ? EXPORT : 0, 0, 0, 0); + /* free the expanded value */ + afree(cp, APERM); + afree(dp, ATEMP); + goto Break; + } + + /* + * POSIX says expand command words first, then redirections, + * and assignments last.. + */ + up = eval(t->args, t->u.evalflags | DOBLANK | DOGLOB | DOTILDE); + if (flags & XTIME) + /* Allow option parsing (bizarre, but POSIX) */ + timex_hook(t, &up); + ap = (const char **)up; + if (ap[0]) + tp = findcom(ap[0], FC_BI|FC_FUNC); + } + flags &= ~XTIME; + + if (t->ioact != NULL || t->type == TPIPE || t->type == TCOPROC) { + e->savefd = alloc2(NUFILE, sizeof(short), ATEMP); + /* initialise to not redirected */ + memset(e->savefd, 0, NUFILE * sizeof(short)); + } + + /* mark for replacement later (unless TPIPE) */ + vp_pipest->flag |= INT_L; + + /* do redirection, to be restored in quitenv() */ + if (t->ioact != NULL) + for (iowp = t->ioact; *iowp != NULL; iowp++) { + if (iosetup(*iowp, tp) < 0) { + exstat = rv = 1; + /* + * Redirection failures for special commands + * cause (non-interactive) shell to exit. + */ + if (tp && tp->type == CSHELL && + (tp->flag & SPEC_BI)) + errorfz(); + /* Deal with FERREXIT, quitenv(), etc. */ + goto Break; + } + } + + switch (t->type) { + case TCOM: + rv = comexec(t, tp, (const char **)ap, flags, xerrok); + break; + + case TPAREN: + rv = execute(t->left, flags | XFORK, xerrok); + break; + + case TPIPE: + flags |= XFORK; + flags &= ~XEXEC; + e->savefd[0] = savefd(0); + e->savefd[1] = savefd(1); + while (t->type == TPIPE) { + openpipe(pv); + /* stdout of curr */ + ksh_dup2(pv[1], 1, false); + /** + * Let exchild() close pv[0] in child + * (if this isn't done, commands like + * (: ; cat /etc/termcap) | sleep 1 + * will hang forever). + */ + exchild(t->left, flags | XPIPEO | XCCLOSE, + NULL, pv[0]); + /* stdin of next */ + ksh_dup2(pv[0], 0, false); + closepipe(pv); + flags |= XPIPEI; + t = t->right; + } + /* stdout of last */ + restfd(1, e->savefd[1]); + /* no need to re-restore this */ + e->savefd[1] = 0; + /* Let exchild() close 0 in parent, after fork, before wait */ + i = exchild(t, flags | XPCLOSE | XPIPEST, xerrok, 0); + if (!(flags&XBGND) && !(flags&XXCOM)) + rv = i; + break; + + case TLIST: + while (t->type == TLIST) { + execute(t->left, flags & XERROK, NULL); + t = t->right; + } + rv = execute(t, flags & XERROK, xerrok); + break; + + case TCOPROC: { +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + /* + * Block sigchild as we are using things changed in the + * signal handler + */ + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); + e->type = E_ERRH; + if ((i = kshsetjmp(e->jbuf))) { + sigprocmask(SIG_SETMASK, &omask, NULL); + quitenv(NULL); + unwind(i); + /* NOTREACHED */ + } +#endif + /* Already have a (live) co-process? */ + if (coproc.job && coproc.write >= 0) + errorf("coprocess already exists"); + + /* Can we re-use the existing co-process pipe? */ + coproc_cleanup(true); + + /* do this before opening pipes, in case these fail */ + e->savefd[0] = savefd(0); + e->savefd[1] = savefd(1); + + openpipe(pv); + if (pv[0] != 0) { + ksh_dup2(pv[0], 0, false); + close(pv[0]); + } + coproc.write = pv[1]; + coproc.job = NULL; + + if (coproc.readw >= 0) + ksh_dup2(coproc.readw, 1, false); + else { + openpipe(pv); + coproc.read = pv[0]; + ksh_dup2(pv[1], 1, false); + /* closed before first read */ + coproc.readw = pv[1]; + coproc.njobs = 0; + /* create new coprocess id */ + ++coproc.id; + } +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); + /* no more need for error handler */ + e->type = E_EXEC; +#endif + + /* + * exchild() closes coproc.* in child after fork, + * will also increment coproc.njobs when the + * job is actually created. + */ + flags &= ~XEXEC; + exchild(t->left, flags | XBGND | XFORK | XCOPROC | XCCLOSE, + NULL, coproc.readw); + break; + } + + case TASYNC: + /* + * XXX non-optimal, I think - "(foo &)", forks for (), + * forks again for async... parent should optimise + * this to "foo &"... + */ + rv = execute(t->left, (flags&~XEXEC)|XBGND|XFORK, xerrok); + break; + + case TOR: + case TAND: + rv = execute(t->left, XERROK, NULL); + if ((rv == 0) == (t->type == TAND)) + rv = execute(t->right, flags & XERROK, xerrok); + else { + flags |= XERROK; + if (xerrok) + *xerrok = 1; + } + break; + + case TBANG: + rv = !execute(t->right, XERROK, xerrok); + flags |= XERROK; + if (xerrok) + *xerrok = 1; + break; + + case TDBRACKET: { + Test_env te; + + te.flags = TEF_DBRACKET; + te.pos.wp = t->args; + te.isa = dbteste_isa; + te.getopnd = dbteste_getopnd; + te.eval = test_eval; + te.error = dbteste_error; + + rv = test_parse(&te); + break; + } + + case TFOR: + case TSELECT: { + volatile bool is_first = true; + + ap = (t->vars == NULL) ? e->loc->argv + 1 : + (const char **)eval((const char **)t->vars, + DOBLANK | DOGLOB | DOTILDE); + e->type = E_LOOP; + while ((i = kshsetjmp(e->jbuf))) { + if ((e->flags&EF_BRKCONT_PASS) || + (i != LBREAK && i != LCONTIN)) { + quitenv(NULL); + unwind(i); + } else if (i == LBREAK) { + rv = 0; + goto Break; + } + } + /* in case of a continue */ + rv = 0; + if (t->type == TFOR) { + while (*ap != NULL) { + setstr(global(t->str), *ap++, KSH_UNWIND_ERROR); + rv = execute(t->left, flags & XERROK, xerrok); + } + } else { + do_TSELECT: + if ((ccp = do_selectargs(ap, is_first))) { + is_first = false; + setstr(global(t->str), ccp, KSH_UNWIND_ERROR); + execute(t->left, flags & XERROK, xerrok); + goto do_TSELECT; + } + rv = 1; + } + break; + } + + case TWHILE: + case TUNTIL: + e->type = E_LOOP; + while ((i = kshsetjmp(e->jbuf))) { + if ((e->flags&EF_BRKCONT_PASS) || + (i != LBREAK && i != LCONTIN)) { + quitenv(NULL); + unwind(i); + } else if (i == LBREAK) { + rv = 0; + goto Break; + } + } + /* in case of a continue */ + rv = 0; + while ((execute(t->left, XERROK, NULL) == 0) == + (t->type == TWHILE)) + rv = execute(t->right, flags & XERROK, xerrok); + break; + + case TIF: + case TELIF: + if (t->right == NULL) + /* should be error */ + break; + rv = execute(execute(t->left, XERROK, NULL) == 0 ? + t->right->left : t->right->right, flags & XERROK, xerrok); + break; + + case TCASE: + i = 0; + ccp = evalstr(t->str, DOTILDE | DOSCALAR); + for (t = t->left; t != NULL && t->type == TPAT; t = t->right) { + for (ap = (const char **)t->vars; *ap; ap++) { + if (i || ((s = evalstr(*ap, DOTILDE|DOPAT)) && + gmatchx(ccp, s, false))) { + record_match(ccp); + rv = execute(t->left, flags & XERROK, + xerrok); + i = 0; + switch (t->u.charflag) { + case '&': + i = 1; + /* FALLTHROUGH */ + case '|': + goto TCASE_next; + } + goto TCASE_out; + } + } + i = 0; + TCASE_next: + /* empty */; + } + TCASE_out: + break; + + case TBRACE: + rv = execute(t->left, flags & XERROK, xerrok); + break; + + case TFUNCT: + rv = define(t->str, t); + break; + + case TTIME: + /* + * Clear XEXEC so nested execute() call doesn't exit + * (allows "ls -l | time grep foo"). + */ + rv = timex(t, flags & ~XEXEC, xerrok); + break; + + case TEXEC: + /* an eval'd TCOM */ + up = makenv(); + restoresigs(); + cleanup_proc_env(); + { + union mksh_ccphack cargs; + + cargs.ro = t->args; + execve(t->str, cargs.rw, up); + rv = errno; + } + if (rv == ENOEXEC) + scriptexec(t, (const char **)up); + else + errorf(Tf_sD_s, t->str, cstrerror(rv)); + } + Break: + exstat = rv & 0xFF; + if (vp_pipest->flag & INT_L) { + unset(vp_pipest, 1); + vp_pipest->flag = DEFINED | ISSET | INTEGER | RDONLY | + ARRAY | INT_U | INT_L; + vp_pipest->val.i = rv; + } + + /* restores IO */ + quitenv(NULL); + if ((flags&XEXEC)) + /* exit child */ + unwind(LEXIT); + if (rv != 0 && !(flags & XERROK) && + (xerrok == NULL || !*xerrok)) { + if (Flag(FERREXIT) & 0x80) { + /* inside eval */ + Flag(FERREXIT) = 0; + } else { + trapsig(ksh_SIGERR); + if (Flag(FERREXIT)) + unwind(LERROR); + } + } + return (rv); +} + +/* + * execute simple command + */ + +static int +comexec(struct op *t, struct tbl * volatile tp, const char **ap, + volatile int flags, volatile int *xerrok) +{ + int i; + volatile int rv = 0; + const char *cp; + const char **lastp; + /* Must be static (XXX but why?) */ + static struct op texec; + int type_flags; + bool resetspec; + int fcflags = FC_BI|FC_FUNC|FC_PATH; + struct block *l_expand, *l_assign; + int optc; + const char *exec_argv0 = NULL; + bool exec_clrenv = false; + + /* snag the last argument for $_ */ + if (Flag(FTALKING) && *(lastp = ap)) { + /* + * XXX not the same as AT&T ksh, which only seems to set $_ + * after a newline (but not in functions/dot scripts, but in + * interactive and script) - perhaps save last arg here and + * set it in shell()?. + */ + while (*++lastp) + ; + /* setstr() can't fail here */ + setstr(typeset("_", LOCAL, 0, INTEGER, 0), *--lastp, + KSH_RETURN_ERROR); + } + + /** + * Deal with the shell builtins builtin, exec and command since + * they can be followed by other commands. This must be done before + * we know if we should create a local block which must be done + * before we can do a path search (in case the assignments change + * PATH). + * Odd cases: + * FOO=bar exec >/dev/null FOO is kept but not exported + * FOO=bar exec foobar FOO is exported + * FOO=bar command exec >/dev/null FOO is neither kept nor exported + * FOO=bar command FOO is neither kept nor exported + * PATH=... foobar use new PATH in foobar search + */ + resetspec = false; + while (tp && tp->type == CSHELL) { + /* undo effects of command */ + fcflags = FC_BI|FC_FUNC|FC_PATH; + if (tp->val.f == c_builtin) { + if ((cp = *++ap) == NULL || + (!strcmp(cp, "--") && (cp = *++ap) == NULL)) { + tp = NULL; + break; + } + if ((tp = findcom(cp, FC_BI)) == NULL) + errorf(Tf_sD_sD_s, Tbuiltin, cp, Tnot_found); + if (tp->type == CSHELL && (tp->flag & LOW_BI)) + break; + continue; + } else if (tp->val.f == c_exec) { + if (ap[1] == NULL) + break; + ksh_getopt_reset(&builtin_opt, GF_ERROR); + while ((optc = ksh_getopt(ap, &builtin_opt, "a:c")) != -1) + switch (optc) { + case 'a': + exec_argv0 = builtin_opt.optarg; + break; + case 'c': + exec_clrenv = true; + /* ensure we can actually do this */ + resetspec = true; + break; + default: + rv = 2; + goto Leave; + } + ap += builtin_opt.optind; + flags |= XEXEC; + /* POSuX demands ksh88-like behaviour here */ + if (Flag(FPOSIX)) + fcflags = FC_PATH; + } else if (tp->val.f == c_command) { + bool saw_p = false; + + /* + * Ugly dealing with options in two places (here + * and in c_command(), but such is life) + */ + ksh_getopt_reset(&builtin_opt, 0); + while ((optc = ksh_getopt(ap, &builtin_opt, ":p")) == 'p') + saw_p = true; + if (optc != -1) + /* command -vV or something */ + break; + /* don't look for functions */ + fcflags = FC_BI|FC_PATH; + if (saw_p) { + if (Flag(FRESTRICTED)) { + warningf(true, Tf_sD_s, + "command -p", "restricted"); + rv = 1; + goto Leave; + } + fcflags |= FC_DEFPATH; + } + ap += builtin_opt.optind; + /* + * POSIX says special builtins lose their status + * if accessed using command. + */ + resetspec = true; + if (!ap[0]) { + /* ensure command with no args exits with 0 */ + subst_exstat = 0; + break; + } + } else if (tp->flag & LOW_BI) { + /* if we have any flags, do not use the builtin */ + if ((ap[1] && ap[1][0] == '-' && ap[1][1] != '\0' && + /* argument, begins with -, is not - or -- */ + (ap[1][1] != '-' || ap[1][2] != '\0')) || + /* always prefer the external utility */ + (tp->flag & LOWER_BI)) { + struct tbl *ext_cmd; + + ext_cmd = findcom(tp->name, FC_PATH | FC_FUNC); + if (ext_cmd && (ext_cmd->type != CTALIAS || + (ext_cmd->flag & ISSET))) + tp = ext_cmd; + } + break; + } else if (tp->val.f == c_trap) { + t->u.evalflags &= ~DOTCOMEXEC; + break; + } else + break; + tp = findcom(ap[0], fcflags & (FC_BI|FC_FUNC)); + } + if (t->u.evalflags & DOTCOMEXEC) + flags |= XEXEC; + l_expand = e->loc; + if (!resetspec && (!ap[0] || (tp && (tp->flag & KEEPASN)))) + type_flags = 0; + else { + /* create new variable/function block */ + newblock(); + /* ksh functions don't keep assignments, POSIX functions do. */ + if (!resetspec && tp && tp->type == CFUNC && + !(tp->flag & FKSH)) + type_flags = EXPORT; + else + type_flags = LOCAL|LOCAL_COPY|EXPORT; + } + l_assign = e->loc; + if (exec_clrenv) + l_assign->flags |= BF_STOPENV; + if (Flag(FEXPORT)) + type_flags |= EXPORT; + if (Flag(FXTRACE)) + change_xtrace(2, false); + for (i = 0; t->vars[i]; i++) { + /* do NOT lookup in the new var/fn block just created */ + e->loc = l_expand; + cp = evalstr(t->vars[i], DOASNTILDE | DOSCALAR); + e->loc = l_assign; + if (Flag(FXTRACE)) { + const char *ccp; + + ccp = skip_varname(cp, true); + if (*ccp == '+') + ++ccp; + if (*ccp == '=') + ++ccp; + shf_write(cp, ccp - cp, shl_xtrace); + print_value_quoted(shl_xtrace, ccp); + shf_putc(' ', shl_xtrace); + } + /* but assign in there as usual */ + typeset(cp, type_flags, 0, 0, 0); + } + + if (Flag(FXTRACE)) { + change_xtrace(2, false); + if (ap[rv = 0]) { + xtrace_ap_loop: + print_value_quoted(shl_xtrace, ap[rv]); + if (ap[++rv]) { + shf_putc(' ', shl_xtrace); + goto xtrace_ap_loop; + } + } + change_xtrace(1, false); + } + + if ((cp = *ap) == NULL) { + rv = subst_exstat; + goto Leave; + } else if (!tp) { + if (Flag(FRESTRICTED) && mksh_vdirsep(cp)) { + warningf(true, Tf_sD_s, cp, "restricted"); + rv = 1; + goto Leave; + } + tp = findcom(cp, fcflags); + } + + switch (tp->type) { + + /* shell built-in */ + case CSHELL: + do_call_builtin: + rv = call_builtin(tp, (const char **)ap, null, resetspec); + if (resetspec && tp->val.f == c_shift) { + l_expand->argc = l_assign->argc; + l_expand->argv = l_assign->argv; + } + break; + + /* function call */ + case CFUNC: { + volatile uint32_t old_inuse; + const char * volatile old_kshname; + volatile uint8_t old_flags[FNFLAGS]; + + if (!(tp->flag & ISSET)) { + struct tbl *ftp; + + if (!tp->u.fpath) { + fpath_error: + rv = (tp->u2.errnov == ENOENT) ? 127 : 126; + warningf(true, Tf_sD_s_sD_s, cp, + Tcant_find, Tfile_fd, + cstrerror(tp->u2.errnov)); + break; + } + errno = 0; + if (include(tp->u.fpath, 0, NULL, false) < 0 || + !(ftp = findfunc(cp, hash(cp), false)) || + !(ftp->flag & ISSET)) { + rv = errno; + if ((ftp = findcom(cp, FC_BI)) && + (ftp->type == CSHELL) && + (ftp->flag & LOW_BI)) { + tp = ftp; + goto do_call_builtin; + } + if (rv) { + tp->u2.errnov = rv; + cp = tp->u.fpath; + goto fpath_error; + } + warningf(true, Tf_sD_s_s, cp, + "function not defined by", tp->u.fpath); + rv = 127; + break; + } + tp = ftp; + } + + /* + * ksh functions set $0 to function name, POSIX + * functions leave $0 unchanged. + */ + old_kshname = kshname; + if (tp->flag & FKSH) + kshname = ap[0]; + else + ap[0] = kshname; + e->loc->argv = ap; + for (i = 0; *ap++ != NULL; i++) + ; + e->loc->argc = i - 1; + /* + * ksh-style functions handle getopts sanely, + * Bourne/POSIX functions are insane... + */ + if (tp->flag & FKSH) { + e->loc->flags |= BF_DOGETOPTS; + e->loc->getopts_state = user_opt; + getopts_reset(1); + } + + for (type_flags = 0; type_flags < FNFLAGS; ++type_flags) + old_flags[type_flags] = shell_flags[type_flags]; + change_xtrace((Flag(FXTRACEREC) ? Flag(FXTRACE) : 0) | + ((tp->flag & TRACE) ? 1 : 0), false); + old_inuse = tp->flag & FINUSE; + tp->flag |= FINUSE; + + e->type = E_FUNC; + if (!(i = kshsetjmp(e->jbuf))) { + execute(tp->val.t, flags & XERROK, NULL); + i = LRETURN; + } + + kshname = old_kshname; + change_xtrace(old_flags[(int)FXTRACE], false); +#ifndef MKSH_LEGACY_MODE + if (tp->flag & FKSH) { + /* Korn style functions restore Flags on return */ + old_flags[(int)FXTRACE] = Flag(FXTRACE); + for (type_flags = 0; type_flags < FNFLAGS; ++type_flags) + shell_flags[type_flags] = old_flags[type_flags]; + } +#endif + tp->flag = (tp->flag & ~FINUSE) | old_inuse; + + /* + * Were we deleted while executing? If so, free the + * execution tree. + */ + if ((tp->flag & (FDELETE|FINUSE)) == FDELETE) { + if (tp->flag & ALLOC) { + tp->flag &= ~ALLOC; + tfree(tp->val.t, tp->areap); + } + tp->flag = 0; + } + switch (i) { + case LRETURN: + case LERROR: + rv = exstat & 0xFF; + break; + case LINTR: + case LEXIT: + case LLEAVE: + case LSHELL: + quitenv(NULL); + unwind(i); + /* NOTREACHED */ + default: + quitenv(NULL); + internal_errorf(Tunexpected_type, Tunwind, Tfunction, i); + } + break; + } + + /* executable command */ + case CEXEC: + /* tracked alias */ + case CTALIAS: + if (!(tp->flag&ISSET)) { + if (tp->u2.errnov == ENOENT) { + rv = 127; + warningf(true, Tf_sD_s, cp, Tnot_found); + } else { + rv = 126; + warningf(true, Tf_sD_sD_s, cp, "can't execute", + cstrerror(tp->u2.errnov)); + } + break; + } + + /* set $_ to program's full path */ + /* setstr() can't fail here */ + setstr(typeset("_", LOCAL | EXPORT, 0, INTEGER, 0), + tp->val.s, KSH_RETURN_ERROR); + + /* to fork, we set up a TEXEC node and call execute */ + texec.type = TEXEC; + /* for vistree/dumptree */ + texec.left = t; + texec.str = tp->val.s; + texec.args = ap; + + /* in this case we do not fork, of course */ + if (flags & XEXEC) { + if (exec_argv0) + texec.args[0] = exec_argv0; + j_exit(); + if (!(flags & XBGND) +#ifndef MKSH_UNEMPLOYED + || Flag(FMONITOR) +#endif + ) { + setexecsig(&sigtraps[SIGINT], SS_RESTORE_ORIG); + setexecsig(&sigtraps[SIGQUIT], SS_RESTORE_ORIG); + } + } + + rv = exchild(&texec, flags, xerrok, -1); + break; + } + Leave: + if (flags & XEXEC) { + exstat = rv & 0xFF; + unwind(LLEAVE); + } + return (rv); +} + +static void +scriptexec(struct op *tp, const char **ap) +{ + const char *sh; +#ifndef MKSH_SMALL + int fd; + unsigned char buf[68]; +#endif + union mksh_ccphack args, cap; + + sh = str_val(global(TEXECSHELL)); + if (sh && *sh) + sh = search_path(sh, path, X_OK, NULL); + if (!sh || !*sh) + sh = MKSH_DEFAULT_EXECSHELL; + + *tp->args-- = tp->str; + +#ifndef MKSH_SMALL + if ((fd = binopen2(tp->str, O_RDONLY)) >= 0) { + unsigned char *cp; +#ifndef MKSH_EBCDIC + unsigned short m; +#endif + ssize_t n; + +#if defined(__OS2__) && defined(MKSH_WITH_TEXTMODE) + setmode(fd, O_TEXT); +#endif + /* read first couple of octets from file */ + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + /* read error or short read? */ + if (n < 5) + goto nomagic; + /* terminate buffer */ + buf[n] = '\0'; + + /* skip UTF-8 Byte Order Mark, if present */ + cp = buf + (n = ((buf[0] == 0xEF) && (buf[1] == 0xBB) && + (buf[2] == 0xBF)) ? 3 : 0); + + /* scan for newline or NUL (end of buffer) */ + while (!ctype(*cp, C_NL | C_NUL)) + ++cp; + /* if the shebang line is longer than MAXINTERP, bail out */ + if (!*cp) + goto noshebang; + /* replace newline by NUL */ + *cp = '\0'; + + /* restore start of shebang position (buf+0 or buf+3) */ + cp = buf + n; + /* bail out if no shebang magic found */ + if (cp[0] == '#' && cp[1] == '!') + cp += 2; +#ifdef __OS2__ + else if (!strncmp(cp, Textproc, 7) && + ctype(cp[7], C_BLANK)) + cp += 8; +#endif + else + goto noshebang; + /* skip whitespace before shell name */ + while (ctype(*cp, C_BLANK)) + ++cp; + /* just whitespace on the line? */ + if (*cp == '\0') + goto noshebang; + /* no, we actually found an interpreter name */ + sh = (char *)cp; + /* look for end of shell/interpreter name */ + while (!ctype(*cp, C_BLANK | C_NUL)) + ++cp; + /* any arguments? */ + if (*cp) { + *cp++ = '\0'; + /* skip spaces before arguments */ + while (ctype(*cp, C_BLANK)) + ++cp; + /* pass it all in ONE argument (historic reasons) */ + if (*cp) + *tp->args-- = (char *)cp; + } +#ifdef __OS2__ + /* + * On OS/2, the directory structure differs from normal + * Unix, which can make many scripts whose shebang + * hardcodes the path to an interpreter fail (and there + * might be no /usr/bin/env); for user convenience, if + * the specified interpreter is not usable, do a PATH + * search to find it. + */ + if (mksh_vdirsep(sh) && !search_path(sh, path, X_OK, NULL)) { + cp = search_path(_getname(sh), path, X_OK, NULL); + if (cp) + sh = cp; + } +#endif + goto nomagic; + noshebang: +#ifndef MKSH_EBCDIC + m = buf[0] << 8 | buf[1]; + if (m == 0x7F45 && buf[2] == 'L' && buf[3] == 'F') + errorf("%s: not executable: %d-bit ELF file", tp->str, + 32 * buf[4]); + if ((m == /* OMAGIC */ 0407) || + (m == /* NMAGIC */ 0410) || + (m == /* ZMAGIC */ 0413) || + (m == /* QMAGIC */ 0314) || + (m == /* ECOFF_I386 */ 0x4C01) || + (m == /* ECOFF_M68K */ 0x0150 || m == 0x5001) || + (m == /* ECOFF_SH */ 0x0500 || m == 0x0005) || + (m == /* bzip */ 0x425A) || (m == /* "MZ" */ 0x4D5A) || + (m == /* "NE" */ 0x4E45) || (m == /* "LX" */ 0x4C58) || + (m == /* ksh93 */ 0x0B13) || (m == /* LZIP */ 0x4C5A) || + (m == /* xz */ 0xFD37 && buf[2] == 'z' && buf[3] == 'X' && + buf[4] == 'Z') || (m == /* 7zip */ 0x377A) || + (m == /* gzip */ 0x1F8B) || (m == /* .Z */ 0x1F9D)) + errorf("%s: not executable: magic %04X", tp->str, m); +#endif +#ifdef __OS2__ + cp = _getext(tp->str); + if (cp && (!stricmp(cp, ".cmd") || !stricmp(cp, ".bat"))) { + /* execute .cmd and .bat with OS2_SHELL, usually CMD.EXE */ + sh = str_val(global("OS2_SHELL")); + *tp->args-- = "/c"; + /* convert slahes to backslashes */ + for (cp = tp->str; *cp; cp++) { + if (*cp == '/') + *cp = '\\'; + } + } +#endif + nomagic: + ; + } +#endif + args.ro = tp->args; + *args.ro = sh; + + cap.ro = ap; + execve(args.rw[0], args.rw, cap.rw); + + /* report both the programme that was run and the bogus interpreter */ + errorf(Tf_sD_sD_s, tp->str, sh, cstrerror(errno)); +} + +/* actual 'builtin' built-in utility call is handled in comexec() */ +int +c_builtin(const char **wp) +{ + return (call_builtin(get_builtin(*wp), wp, Tbuiltin, false)); +} + +struct tbl * +get_builtin(const char *s) +{ + return (s && *s ? ktsearch(&builtins, s, hash(s)) : NULL); +} + +/* + * Search function tables for a function. If create set, a table entry + * is created if none is found. + */ +struct tbl * +findfunc(const char *name, uint32_t h, bool create) +{ + struct block *l; + struct tbl *tp = NULL; + + for (l = e->loc; l; l = l->next) { + tp = ktsearch(&l->funs, name, h); + if (tp) + break; + if (!l->next && create) { + tp = ktenter(&l->funs, name, h); + tp->flag = DEFINED; + tp->type = CFUNC; + tp->val.t = NULL; + break; + } + } + return (tp); +} + +/* + * define function. Returns 1 if function is being undefined (t == 0) and + * function did not exist, returns 0 otherwise. + */ +int +define(const char *name, struct op *t) +{ + uint32_t nhash; + struct tbl *tp; + bool was_set = false; + + nhash = hash(name); + + while (/* CONSTCOND */ 1) { + tp = findfunc(name, nhash, true); + + if (tp->flag & ISSET) + was_set = true; + /* + * If this function is currently being executed, we zap + * this table entry so findfunc() won't see it + */ + if (tp->flag & FINUSE) { + tp->name[0] = '\0'; + /* ensure it won't be found */ + tp->flag &= ~DEFINED; + tp->flag |= FDELETE; + } else + break; + } + + if (tp->flag & ALLOC) { + tp->flag &= ~(ISSET|ALLOC|FKSH); + tfree(tp->val.t, tp->areap); + } + + if (t == NULL) { + /* undefine */ + ktdelete(tp); + return (was_set ? 0 : 1); + } + + tp->val.t = tcopy(t->left, tp->areap); + tp->flag |= (ISSET|ALLOC); + if (t->u.ksh_func) + tp->flag |= FKSH; + + return (0); +} + +/* + * add builtin + */ +const char * +builtin(const char *name, int (*func) (const char **)) +{ + struct tbl *tp; + uint32_t flag = DEFINED; + + /* see if any flags should be set for this builtin */ + flags_loop: + switch (*name) { + case '=': + /* command does variable assignment */ + flag |= KEEPASN; + break; + case '*': + /* POSIX special builtin */ + flag |= SPEC_BI; + break; + case '~': + /* external utility overrides built-in utility, always */ + flag |= LOWER_BI; + /* FALLTHROUGH */ + case '!': + /* external utility overrides built-in utility, with flags */ + flag |= LOW_BI; + break; + case '-': + /* is declaration utility if argv[1] is one (POSIX: command) */ + flag |= DECL_FWDR; + break; + case '^': + /* is declaration utility (POSIX: export, readonly) */ + flag |= DECL_UTIL; + break; + default: + goto flags_seen; + } + ++name; + goto flags_loop; + flags_seen: + + /* enter into the builtins hash table */ + tp = ktenter(&builtins, name, hash(name)); + tp->flag = flag; + tp->type = CSHELL; + tp->val.f = func; + + /* return name, for direct builtin call check in main.c */ + return (name); +} + +/* + * find command + * either function, hashed command, or built-in (in that order) + */ +struct tbl * +findcom(const char *name, int flags) +{ + static struct tbl temp; + uint32_t h = hash(name); + struct tbl *tp = NULL, *tbi; + /* insert if not found */ + unsigned char insert = Flag(FTRACKALL); + /* for function autoloading */ + char *fpath; + union mksh_cchack npath; + + if (mksh_vdirsep(name)) { + insert = 0; + /* prevent FPATH search below */ + flags &= ~FC_FUNC; + goto Search; + } + tbi = (flags & FC_BI) ? ktsearch(&builtins, name, h) : NULL; + /* + * POSIX says special builtins first, then functions, then + * regular builtins, then search path... + */ + if ((flags & FC_SPECBI) && tbi && (tbi->flag & SPEC_BI)) + tp = tbi; + if (!tp && (flags & FC_FUNC)) { + tp = findfunc(name, h, false); + if (tp && !(tp->flag & ISSET)) { + if ((fpath = str_val(global(TFPATH))) == null) { + tp->u.fpath = NULL; + tp->u2.errnov = ENOENT; + } else + tp->u.fpath = search_path(name, fpath, R_OK, + &tp->u2.errnov); + } + } + if (!tp && (flags & FC_NORMBI) && tbi) + tp = tbi; + if (!tp && (flags & FC_PATH) && !(flags & FC_DEFPATH)) { + tp = ktsearch(&taliases, name, h); + if (tp && (tp->flag & ISSET) && + ksh_access(tp->val.s, X_OK) != 0) { + if (tp->flag & ALLOC) { + tp->flag &= ~ALLOC; + afree(tp->val.s, APERM); + } + tp->flag &= ~ISSET; + } + } + + Search: + if ((!tp || (tp->type == CTALIAS && !(tp->flag&ISSET))) && + (flags & FC_PATH)) { + if (!tp) { + if (insert && !(flags & FC_DEFPATH)) { + tp = ktenter(&taliases, name, h); + tp->type = CTALIAS; + } else { + tp = &temp; + tp->type = CEXEC; + } + /* make ~ISSET */ + tp->flag = DEFINED; + } + npath.ro = search_path(name, + (flags & FC_DEFPATH) ? def_path : path, + X_OK, &tp->u2.errnov); + if (npath.ro) { + strdupx(tp->val.s, npath.ro, APERM); + if (npath.ro != name) + afree(npath.rw, ATEMP); + tp->flag |= ISSET|ALLOC; + } else if ((flags & FC_FUNC) && + (fpath = str_val(global(TFPATH))) != null && + (npath.ro = search_path(name, fpath, R_OK, + &tp->u2.errnov)) != NULL) { + /* + * An undocumented feature of AT&T ksh is that + * it searches FPATH if a command is not found, + * even if the command hasn't been set up as an + * autoloaded function (ie, no typeset -uf). + */ + tp = &temp; + tp->type = CFUNC; + /* make ~ISSET */ + tp->flag = DEFINED; + tp->u.fpath = npath.ro; + } + } + return (tp); +} + +/* + * flush executable commands with relative paths + * (just relative or all?) + */ +void +flushcom(bool all) +{ + struct tbl *tp; + struct tstate ts; + + for (ktwalk(&ts, &taliases); (tp = ktnext(&ts)) != NULL; ) + if ((tp->flag&ISSET) && (all || !mksh_abspath(tp->val.s))) { + if (tp->flag&ALLOC) { + tp->flag &= ~(ALLOC|ISSET); + afree(tp->val.s, APERM); + } + tp->flag &= ~ISSET; + } +} + +/* check if path is something we want to find */ +int +search_access(const char *fn, int mode) +{ + struct stat sb; + + if (stat(fn, &sb) < 0) + /* file does not exist */ + return (ENOENT); + /* LINTED use of access */ + if (access(fn, mode) < 0) { + /* file exists, but we can't access it */ + int eno; + + eno = errno; + return (eno ? eno : EACCES); + } +#ifdef __OS2__ + /* treat all files as executable on OS/2 */ + sb.st_mode |= S_IXUSR | S_IXGRP | S_IXOTH; +#endif + if (mode == X_OK && (!S_ISREG(sb.st_mode) || + !(sb.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))) + /* access(2) may say root can execute everything */ + return (S_ISDIR(sb.st_mode) ? EISDIR : EACCES); + return (0); +} + +#ifdef __OS2__ +/* check if path is something we want to find, adding executable extensions */ +#define search_access(fn, mode) access_ex((search_access), (fn), (mode)) +#else +#define search_access(fn, mode) (search_access)((fn), (mode)) +#endif + +/* + * search for command with PATH + */ +const char * +search_path(const char *name, const char *lpath, + /* R_OK or X_OK */ + int mode, + /* set if candidate found, but not suitable */ + int *errnop) +{ + const char *sp, *p; + char *xp; + XString xs; + size_t namelen; + int ec = 0, ev; + + if (mksh_vdirsep(name)) { + if ((ec = search_access(name, mode)) == 0) { + search_path_ok: + if (errnop) + *errnop = 0; +#ifndef __OS2__ + return (name); +#else + return (real_exec_name(name)); +#endif + } + goto search_path_err; + } + + namelen = strlen(name) + 1; + Xinit(xs, xp, 128, ATEMP); + + sp = lpath; + while (sp != NULL) { + xp = Xstring(xs, xp); + if (!(p = cstrchr(sp, MKSH_PATHSEPC))) + p = strnul(sp); + if (p != sp) { + XcheckN(xs, xp, p - sp); + memcpy(xp, sp, p - sp); + xp += p - sp; +#ifdef __OS2__ + if (xp > Xstring(xs, xp) && mksh_cdirsep(xp[-1])) + xp--; +#endif + *xp++ = '/'; + } + sp = p; + XcheckN(xs, xp, namelen); + memcpy(xp, name, namelen); + if ((ev = search_access(Xstring(xs, xp), mode)) == 0) { + name = Xclose(xs, xp + namelen); + goto search_path_ok; + } + /* accumulate non-ENOENT errors only */ + if (ev != ENOENT && ec == 0) + ec = ev; + if (*sp++ == '\0') + sp = NULL; + } + Xfree(xs, xp); + search_path_err: + if (errnop) + *errnop = ec ? ec : ENOENT; + return (NULL); +} + +static int +call_builtin(struct tbl *tp, const char **wp, const char *where, bool resetspec) +{ + int rv; + + if (!tp) + internal_errorf(Tf_sD_s, where, wp[0]); + builtin_argv0 = wp[0]; + builtin_spec = tobool(!resetspec && (tp->flag & SPEC_BI)); + shf_reopen(1, SHF_WR, shl_stdout); + shl_stdout_ok = true; + ksh_getopt_reset(&builtin_opt, GF_ERROR); + rv = (*tp->val.f)(wp); + shf_flush(shl_stdout); + shl_stdout_ok = false; + builtin_argv0 = NULL; + builtin_spec = false; + return (rv); +} + +/* + * set up redirection, saving old fds in e->savefd + */ +static int +iosetup(struct ioword *iop, struct tbl *tp) +{ + int u = -1; + char *cp = iop->ioname; + int iotype = iop->ioflag & IOTYPE; + bool do_open = true, do_close = false, do_fstat = false; + int flags = 0; + struct ioword iotmp; + struct stat statb; + + if (iotype != IOHERE) + cp = evalonestr(cp, DOTILDE|(Flag(FTALKING_I) ? DOGLOB : 0)); + + /* Used for tracing and error messages to print expanded cp */ + iotmp = *iop; + iotmp.ioname = (iotype == IOHERE) ? NULL : cp; + iotmp.ioflag |= IONAMEXP; + + if (Flag(FXTRACE)) { + change_xtrace(2, false); + fptreef(shl_xtrace, 0, Tft_R, &iotmp); + change_xtrace(1, false); + } + + switch (iotype) { + case IOREAD: + flags = O_RDONLY; + break; + + case IOCAT: + flags = O_WRONLY | O_APPEND | O_CREAT; + break; + + case IOWRITE: + if (Flag(FNOCLOBBER) && !(iop->ioflag & IOCLOB)) { + /* >file under set -C */ + if (stat(cp, &statb)) { + /* nonexistent file */ + flags = O_WRONLY | O_CREAT | O_EXCL; + } else if (S_ISREG(statb.st_mode)) { + /* regular file, refuse clobbering */ + goto clobber_refused; + } else { + /* + * allow redirections to things + * like /dev/null without error + */ + flags = O_WRONLY; + /* but check again after opening */ + do_fstat = true; + } + } else { + /* >|file or set +C */ + flags = O_WRONLY | O_CREAT | O_TRUNC; + } + break; + + case IORDWR: + flags = O_RDWR | O_CREAT; + break; + + case IOHERE: + do_open = false; + /* herein() returns -2 if error has been printed */ + u = herein(iop, NULL); + /* cp may have wrong name */ + break; + + case IODUP: { + const char *emsg; + + do_open = false; + if (ksh_isdash(cp)) { + /* prevent error return below */ + u = 1009; + do_close = true; + } else if ((u = check_fd(cp, + X_OK | ((iop->ioflag & IORDUP) ? R_OK : W_OK), + &emsg)) < 0) { + char *sp; + + warningf(true, Tf_sD_s, + (sp = snptreef(NULL, 32, Tft_R, &iotmp)), emsg); + afree(sp, ATEMP); + return (-1); + } + if (u == (int)iop->unit) + /* "dup from" == "dup to" */ + return (0); + break; + } + } + + if (do_open) { + if (Flag(FRESTRICTED) && (flags & O_CREAT)) { + warningf(true, Tf_sD_s, cp, "restricted"); + return (-1); + } + u = binopen3(cp, flags, 0666); + if (do_fstat && u >= 0) { + /* prevent race conditions */ + if (fstat(u, &statb) || S_ISREG(statb.st_mode)) { + close(u); + clobber_refused: + u = -1; + errno = EEXIST; + } + } + } + if (u < 0) { + /* herein() may already have printed message */ + if (u == -1) { + u = errno; + warningf(true, Tf_cant_ss_s, +#if 0 + /* can't happen */ + iotype == IODUP ? "dup" : +#endif + (iotype == IOREAD || iotype == IOHERE) ? + Topen : Tcreate, cp, cstrerror(u)); + } + return (-1); + } + /* Do not save if it has already been redirected (i.e. "cat >x >y"). */ + if (e->savefd[iop->unit] == 0) { + /* If these are the same, it means unit was previously closed */ + if (u == (int)iop->unit) + e->savefd[iop->unit] = -1; + else + /* + * c_exec() assumes e->savefd[fd] set for any + * redirections. Ask savefd() not to close iop->unit; + * this allows error messages to be seen if iop->unit + * is 2; also means we can't lose the fd (eg, both + * dup2 below and dup2 in restfd() failing). + */ + e->savefd[iop->unit] = savefd(iop->unit); + } + + if (do_close) + close(iop->unit); + else if (u != (int)iop->unit) { + if (ksh_dup2(u, iop->unit, true) < 0) { + int eno; + char *sp; + + eno = errno; + warningf(true, Tf_s_sD_s, Tredirection_dup, + (sp = snptreef(NULL, 32, Tft_R, &iotmp)), + cstrerror(eno)); + afree(sp, ATEMP); + if (iotype != IODUP) + close(u); + return (-1); + } + if (iotype != IODUP) + close(u); + /* + * Touching any co-process fd in an empty exec + * causes the shell to close its copies + */ + else if (tp && tp->type == CSHELL && tp->val.f == c_exec) { + if (iop->ioflag & IORDUP) + /* possible exec <&p */ + coproc_read_close(u); + else + /* possible exec >&p */ + coproc_write_close(u); + } + } + if (u == 2) + /* Clear any write errors */ + shf_reopen(2, SHF_WR, shl_out); + return (0); +} + +/* + * Process here documents by providing the content, either as + * result (globally allocated) string or in a temp file; if + * unquoted, the string is expanded first. + */ +static int +hereinval(struct ioword *iop, int sub, char **resbuf, struct shf *shf) +{ + const char * volatile ccp = iop->heredoc; + struct source *s, *osource; + + osource = source; + newenv(E_ERRH); + if (kshsetjmp(e->jbuf)) { + source = osource; + quitenv(shf); + /* special to iosetup(): don't print error */ + return (-2); + } + if (iop->ioflag & IOHERESTR) { + ccp = evalstr(iop->delim, DOHERESTR | DOSCALAR | DOHEREDOC); + } else if (sub) { + /* do substitutions on the content of heredoc */ + s = pushs(SSTRING, ATEMP); + s->start = s->str = ccp; + source = s; + if (yylex(sub) != LWORD) + internal_errorf("herein: yylex"); + source = osource; + ccp = evalstr(yylval.cp, DOSCALAR | DOHEREDOC); + } + + if (resbuf == NULL) + shf_puts(ccp, shf); + else + strdupx(*resbuf, ccp, APERM); + + quitenv(NULL); + return (0); +} + +int +herein(struct ioword *iop, char **resbuf) +{ + int fd = -1; + struct shf *shf; + struct temp *h; + int i; + + /* lexer substitution flags */ + i = (iop->ioflag & IOEVAL) ? (ONEWORD | HEREDOC) : 0; + + /* skip all the fd setup if we just want the value */ + if (resbuf != NULL) + return (hereinval(iop, i, resbuf, NULL)); + + /* + * Create temp file to hold content (done before newenv + * so temp doesn't get removed too soon). + */ + h = maketemp(ATEMP, TT_HEREDOC_EXP, &e->temps); + if (!(shf = h->shf) || (fd = binopen3(h->tffn, O_RDONLY, 0)) < 0) { + i = errno; + warningf(true, Tf_temp, + !shf ? Tcreate : Topen, h->tffn, cstrerror(i)); + if (shf) + shf_close(shf); + /* special to iosetup(): don't print error */ + return (-2); + } + + if (hereinval(iop, i, NULL, shf) == -2) { + close(fd); + /* special to iosetup(): don't print error */ + return (-2); + } + + if (shf_close(shf) == -1) { + i = errno; + close(fd); + warningf(true, Tf_temp, + Twrite, h->tffn, cstrerror(i)); + /* special to iosetup(): don't print error */ + return (-2); + } + + return (fd); +} + +/* + * ksh special - the select command processing section + * print the args in column form - assuming that we can + */ +static const char * +do_selectargs(const char **ap, bool print_menu) +{ + static const char *read_args[] = { + Tread, "-r", "REPLY", NULL + }; + char *s; + int i, argct; + + for (argct = 0; ap[argct]; argct++) + ; + while (/* CONSTCOND */ 1) { + /*- + * Menu is printed if + * - this is the first time around the select loop + * - the user enters a blank line + * - the REPLY parameter is empty + */ + if (print_menu || !*str_val(global("REPLY"))) + pr_menu(ap); + shellf(Tf_s, str_val(global("PS3"))); + if (call_builtin(findcom(Tread, FC_BI), read_args, Tselect, + false)) + return (NULL); + if (*(s = str_val(global("REPLY")))) + return ((getn(s, &i) && i >= 1 && i <= argct) ? + ap[i - 1] : null); + print_menu = true; + } +} + +struct select_menu_info { + const char * const *args; + int num_width; +}; + +/* format a single select menu item */ +static void +select_fmt_entry(char *buf, size_t buflen, unsigned int i, const void *arg) +{ + const struct select_menu_info *smi = + (const struct select_menu_info *)arg; + + shf_snprintf(buf, buflen, "%*u) %s", + smi->num_width, i + 1, smi->args[i]); +} + +/* + * print a select style menu + */ +void +pr_menu(const char * const *ap) +{ + struct select_menu_info smi; + const char * const *pp; + size_t acols = 0, aocts = 0, i; + unsigned int n; + struct columnise_opts co; + + /* + * width/column calculations were done once and saved, but this + * means select can't be used recursively so we re-calculate + * each time (could save in a structure that is returned, but + * it's probably not worth the bother) + */ + + /* + * get dimensions of the list + */ + for (n = 0, pp = ap; *pp; n++, pp++) { + i = strlen(*pp); + if (i > aocts) + aocts = i; + i = utf_mbswidth(*pp); + if (i > acols) + acols = i; + } + + /* + * we will print an index of the form "%d) " in front of + * each entry, so get the maximum width of this + */ + for (i = n, smi.num_width = 1; i >= 10; i /= 10) + smi.num_width++; + + smi.args = ap; + co.shf = shl_out; + co.linesep = '\n'; + co.prefcol = co.do_last = true; + print_columns(&co, n, select_fmt_entry, (void *)&smi, + smi.num_width + 2 + aocts, smi.num_width + 2 + acols); +} + +static void +plain_fmt_entry(char *buf, size_t buflen, unsigned int i, const void *arg) +{ + strlcpy(buf, ((const char * const *)arg)[i], buflen); +} + +void +pr_list(struct columnise_opts *cop, char * const *ap) +{ + size_t acols = 0, aocts = 0, i; + unsigned int n; + char * const *pp; + + for (n = 0, pp = ap; *pp; n++, pp++) { + i = strlen(*pp); + if (i > aocts) + aocts = i; + i = utf_mbswidth(*pp); + if (i > acols) + acols = i; + } + + print_columns(cop, n, plain_fmt_entry, (const void *)ap, + aocts, acols); +} + +/* + * [[ ... ]] evaluation routines + */ + +/* + * Test if the current token is a whatever. Accepts the current token if + * it is. Returns 0 if it is not, non-zero if it is (in the case of + * TM_UNOP and TM_BINOP, the returned value is a Test_op). + */ +static Test_op +dbteste_isa(Test_env *te, Test_meta meta) +{ + Test_op ret = TO_NONOP; + bool uqword; + const char *p; + + if (!*te->pos.wp) + return (meta == TM_END ? TO_NONNULL : TO_NONOP); + + /* unquoted word? */ + for (p = *te->pos.wp; *p == CHAR; p += 2) + ; + uqword = *p == EOS; + + if (meta == TM_UNOP || meta == TM_BINOP) { + if (uqword) { + /* longer than the longest operator */ + char buf[8]; + char *q = buf; + + p = *te->pos.wp; + while (*p++ == CHAR && + (size_t)(q - buf) < sizeof(buf) - 1) + *q++ = *p++; + *q = '\0'; + ret = test_isop(meta, buf); + } + } else if (meta == TM_END) + ret = TO_NONOP; + else + ret = (uqword && !strcmp(*te->pos.wp, + dbtest_tokens[(int)meta])) ? TO_NONNULL : TO_NONOP; + + /* Accept the token? */ + if (ret != TO_NONOP) + te->pos.wp++; + + return (ret); +} + +static const char * +dbteste_getopnd(Test_env *te, Test_op op, bool do_eval) +{ + const char *s = *te->pos.wp; + int flags = DOTILDE | DOSCALAR; + + if (!s) + return (NULL); + + te->pos.wp++; + + if (!do_eval) + return (null); + + if (op == TO_STEQL || op == TO_STNEQ) + flags |= DOPAT; + + return (evalstr(s, flags)); +} + +static void +dbteste_error(Test_env *te, int offset, const char *msg) +{ + te->flags |= TEF_ERROR; + internal_warningf("dbteste_error: %s (offset %d)", msg, offset); +} diff --git a/expr.c b/expr.c new file mode 100644 index 0000000..499b961 --- /dev/null +++ b/expr.c @@ -0,0 +1,1193 @@ +/* $OpenBSD: expr.c,v 1.24 2014/12/08 14:26:31 otto Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/expr.c,v 1.103 2018/01/14 01:29:47 tg Exp $"); + +#define EXPRTOK_DEFNS +#include "exprtok.h" + +/* precisions; used to be enum prec but we do arithmetics on it */ +#define P_PRIMARY 0 /* VAR, LIT, (), ! ~ ++ -- */ +#define P_MULT 1 /* * / % */ +#define P_ADD 2 /* + - */ +#define P_SHIFT 3 /* ^< ^> << >> */ +#define P_RELATION 4 /* < <= > >= */ +#define P_EQUALITY 5 /* == != */ +#define P_BAND 6 /* & */ +#define P_BXOR 7 /* ^ */ +#define P_BOR 8 /* | */ +#define P_LAND 9 /* && */ +#define P_LOR 10 /* || */ +#define P_TERN 11 /* ?: */ + /* = += -= *= /= %= ^<= ^>= <<= >>= &= ^= |= */ +#define P_ASSIGN 12 +#define P_COMMA 13 /* , */ +#define MAX_PREC P_COMMA + +enum token { +#define EXPRTOK_ENUM +#include "exprtok.h" +}; + +static const char opname[][4] = { +#define EXPRTOK_NAME +#include "exprtok.h" +}; + +static const uint8_t oplen[] = { +#define EXPRTOK_LEN +#include "exprtok.h" +}; + +static const uint8_t opprec[] = { +#define EXPRTOK_PREC +#include "exprtok.h" +}; + +typedef struct expr_state { + /* expression being evaluated */ + const char *expression; + /* lexical position */ + const char *tokp; + /* value from token() */ + struct tbl *val; + /* variable that is being recursively expanded (EXPRINEVAL flag set) */ + struct tbl *evaling; + /* token from token() */ + enum token tok; + /* don't do assignments (for ?:, &&, ||) */ + uint8_t noassign; + /* evaluating an $(()) expression? */ + bool arith; + /* unsigned arithmetic calculation */ + bool natural; +} Expr_state; + +enum error_type { + ET_UNEXPECTED, ET_BADLIT, ET_RECURSIVE, + ET_LVALUE, ET_RDONLY, ET_STR +}; + +static void evalerr(Expr_state *, enum error_type, const char *) + MKSH_A_NORETURN; +static struct tbl *evalexpr(Expr_state *, unsigned int); +static void exprtoken(Expr_state *); +static struct tbl *do_ppmm(Expr_state *, enum token, struct tbl *, bool); +static void assign_check(Expr_state *, enum token, struct tbl *); +static struct tbl *intvar(Expr_state *, struct tbl *); + +/* + * parse and evaluate expression + */ +int +evaluate(const char *expr, mksh_ari_t *rval, int error_ok, bool arith) +{ + struct tbl v; + int ret; + + v.flag = DEFINED | INTEGER; + v.type = 0; + ret = v_evaluate(&v, expr, error_ok, arith); + *rval = v.val.i; + return (ret); +} + +/* + * parse and evaluate expression, storing result in vp. + */ +int +v_evaluate(struct tbl *vp, const char *expr, volatile int error_ok, + bool arith) +{ + struct tbl *v; + Expr_state curstate; + Expr_state * const es = &curstate; + int i; + + /* save state to allow recursive calls */ + memset(&curstate, 0, sizeof(curstate)); + curstate.expression = curstate.tokp = expr; + curstate.tok = BAD; + curstate.arith = arith; + + newenv(E_ERRH); + if ((i = kshsetjmp(e->jbuf))) { + /* Clear EXPRINEVAL in of any variables we were playing with */ + if (curstate.evaling) + curstate.evaling->flag &= ~EXPRINEVAL; + quitenv(NULL); + if (i == LAEXPR) { + if (error_ok == KSH_RETURN_ERROR) + return (0); + errorfz(); + } + unwind(i); + /* NOTREACHED */ + } + + exprtoken(es); + if (es->tok == END) { + es->tok = LIT; + es->val = tempvar(""); + } + v = intvar(es, evalexpr(es, MAX_PREC)); + + if (es->tok != END) + evalerr(es, ET_UNEXPECTED, NULL); + + if (es->arith && es->natural) + vp->flag |= INT_U; + if (vp->flag & INTEGER) + setint_v(vp, v, es->arith); + else + /* can fail if readonly */ + setstr(vp, str_val(v), error_ok); + + quitenv(NULL); + + return (1); +} + +static void +evalerr(Expr_state *es, enum error_type type, const char *str) +{ + char tbuf[2]; + const char *s; + + es->arith = false; + switch (type) { + case ET_UNEXPECTED: + switch (es->tok) { + case VAR: + s = es->val->name; + break; + case LIT: + s = str_val(es->val); + break; + case END: + s = "end of expression"; + break; + case BAD: + tbuf[0] = *es->tokp; + tbuf[1] = '\0'; + s = tbuf; + break; + default: + s = opname[(int)es->tok]; + } + warningf(true, Tf_sD_s_qs, es->expression, + Tunexpected, s); + break; + + case ET_BADLIT: + warningf(true, Tf_sD_s_qs, es->expression, + Tbadnum, str); + break; + + case ET_RECURSIVE: + warningf(true, Tf_sD_s_qs, es->expression, + "expression recurses on parameter", str); + break; + + case ET_LVALUE: + warningf(true, Tf_sD_s_s, + es->expression, str, "requires lvalue"); + break; + + case ET_RDONLY: + warningf(true, Tf_sD_s_s, + es->expression, str, "applied to read-only variable"); + break; + + default: /* keep gcc happy */ + case ET_STR: + warningf(true, Tf_sD_s, es->expression, str); + break; + } + unwind(LAEXPR); +} + +/* do a ++ or -- operation */ +static struct tbl * +do_ppmm(Expr_state *es, enum token op, struct tbl *vasn, bool is_prefix) +{ + struct tbl *vl; + mksh_uari_t oval; + + assign_check(es, op, vasn); + + vl = intvar(es, vasn); + oval = vl->val.u; + if (op == O_PLUSPLUS) + ++vl->val.u; + else + --vl->val.u; + if (!es->noassign) { + if (vasn->flag & INTEGER) + setint_v(vasn, vl, es->arith); + else + setint(vasn, vl->val.i); + } + if (!is_prefix) + /* undo the increment/decrement */ + vl->val.u = oval; + + return (vl); +} + +static struct tbl * +evalexpr(Expr_state *es, unsigned int prec) +{ + struct tbl *vl, *vr = NULL, *vasn; + enum token op; + mksh_uari_t res = 0, t1, t2, t3; + + if (prec == P_PRIMARY) { + switch ((int)(op = es->tok)) { + case O_BNOT: + case O_LNOT: + case O_MINUS: + case O_PLUS: + exprtoken(es); + vl = intvar(es, evalexpr(es, P_PRIMARY)); + switch ((int)op) { + case O_BNOT: + vl->val.u = ~vl->val.u; + break; + case O_LNOT: + vl->val.u = !vl->val.u; + break; + case O_MINUS: + vl->val.u = -vl->val.u; + break; + case O_PLUS: + /* nop */ + break; + } + break; + + case OPEN_PAREN: + exprtoken(es); + vl = evalexpr(es, MAX_PREC); + if (es->tok != CLOSE_PAREN) + evalerr(es, ET_STR, "missing )"); + exprtoken(es); + break; + + case O_PLUSPLUS: + case O_MINUSMINUS: + exprtoken(es); + vl = do_ppmm(es, op, es->val, true); + exprtoken(es); + break; + + case VAR: + case LIT: + vl = es->val; + exprtoken(es); + break; + + default: + evalerr(es, ET_UNEXPECTED, NULL); + /* NOTREACHED */ + } + + if (es->tok == O_PLUSPLUS || es->tok == O_MINUSMINUS) { + vl = do_ppmm(es, es->tok, vl, false); + exprtoken(es); + } + + return (vl); + /* prec == P_PRIMARY */ + } + + vl = evalexpr(es, prec - 1); + while ((int)(op = es->tok) >= (int)O_EQ && (int)op <= (int)O_COMMA && + opprec[(int)op] == prec) { + switch ((int)op) { + case O_TERN: + case O_LAND: + case O_LOR: + break; + default: + exprtoken(es); + } + + vasn = vl; + if (op != O_ASN) + /* vl may not have a value yet */ + vl = intvar(es, vl); + if (IS_ASSIGNOP(op)) { + if (!es->noassign) + assign_check(es, op, vasn); + vr = intvar(es, evalexpr(es, P_ASSIGN)); + } else if (op == O_TERN) { + bool ev = vl->val.u != 0; + + if (!ev) + es->noassign++; + exprtoken(es); + vl = evalexpr(es, MAX_PREC); + if (!ev) + es->noassign--; + if (es->tok != CTERN) + evalerr(es, ET_STR, "missing :"); + if (ev) + es->noassign++; + exprtoken(es); + vr = evalexpr(es, P_TERN); + if (ev) + es->noassign--; + vl = ev ? vl : vr; + continue; + } else if (op != O_LAND && op != O_LOR) + vr = intvar(es, evalexpr(es, prec - 1)); + + /* common ops setup */ + switch ((int)op) { + case O_DIV: + case O_DIVASN: + case O_MOD: + case O_MODASN: + if (vr->val.u == 0) { + if (!es->noassign) + evalerr(es, ET_STR, "zero divisor"); + vr->val.u = 1; + } + /* calculate the absolute values */ + t1 = vl->val.i < 0 ? -vl->val.u : vl->val.u; + t2 = vr->val.i < 0 ? -vr->val.u : vr->val.u; + break; +#ifndef MKSH_LEGACY_MODE + case O_LSHIFT: + case O_LSHIFTASN: + case O_RSHIFT: + case O_RSHIFTASN: + case O_ROL: + case O_ROLASN: + case O_ROR: + case O_RORASN: + t1 = vl->val.u; + t2 = vr->val.u & 31; + break; +#endif + case O_LAND: + case O_LOR: + t1 = vl->val.u; + t2 = 0; /* gcc */ + break; + default: + t1 = vl->val.u; + t2 = vr->val.u; + break; + } + +#define cmpop(op) (es->natural ? \ + (mksh_uari_t)(vl->val.u op vr->val.u) : \ + (mksh_uari_t)(vl->val.i op vr->val.i) \ +) + + /* op calculation */ + switch ((int)op) { + case O_TIMES: + case O_TIMESASN: + res = t1 * t2; + break; + case O_MOD: + case O_MODASN: + if (es->natural) { + res = vl->val.u % vr->val.u; + break; + } + goto signed_division; + case O_DIV: + case O_DIVASN: + if (es->natural) { + res = vl->val.u / vr->val.u; + break; + } + signed_division: + /* + * a / b = abs(a) / abs(b) * sgn((u)a^(u)b) + */ + t3 = t1 / t2; +#ifndef MKSH_LEGACY_MODE + res = ((vl->val.u ^ vr->val.u) & 0x80000000) ? -t3 : t3; +#else + res = ((t1 == vl->val.u ? 0 : 1) ^ + (t2 == vr->val.u ? 0 : 1)) ? -t3 : t3; +#endif + if (op == O_MOD || op == O_MODASN) { + /* + * primitive modulo, to get the sign of + * the result correct: + * (a % b) = a - ((a / b) * b) + * the subtraction and multiplication + * are, amazingly enough, sign ignorant + */ + res = vl->val.u - (res * vr->val.u); + } + break; + case O_PLUS: + case O_PLUSASN: + res = t1 + t2; + break; + case O_MINUS: + case O_MINUSASN: + res = t1 - t2; + break; +#ifndef MKSH_LEGACY_MODE + case O_ROL: + case O_ROLASN: + res = (t1 << t2) | (t1 >> (32 - t2)); + break; + case O_ROR: + case O_RORASN: + res = (t1 >> t2) | (t1 << (32 - t2)); + break; +#endif + case O_LSHIFT: + case O_LSHIFTASN: + res = t1 << t2; + break; + case O_RSHIFT: + case O_RSHIFTASN: + res = es->natural || vl->val.i >= 0 ? + t1 >> t2 : + ~(~t1 >> t2); + break; + case O_LT: + res = cmpop(<); + break; + case O_LE: + res = cmpop(<=); + break; + case O_GT: + res = cmpop(>); + break; + case O_GE: + res = cmpop(>=); + break; + case O_EQ: + res = t1 == t2; + break; + case O_NE: + res = t1 != t2; + break; + case O_BAND: + case O_BANDASN: + res = t1 & t2; + break; + case O_BXOR: + case O_BXORASN: + res = t1 ^ t2; + break; + case O_BOR: + case O_BORASN: + res = t1 | t2; + break; + case O_LAND: + if (!t1) + es->noassign++; + exprtoken(es); + vr = intvar(es, evalexpr(es, prec - 1)); + res = t1 && vr->val.u; + if (!t1) + es->noassign--; + break; + case O_LOR: + if (t1) + es->noassign++; + exprtoken(es); + vr = intvar(es, evalexpr(es, prec - 1)); + res = t1 || vr->val.u; + if (t1) + es->noassign--; + break; + case O_ASN: + case O_COMMA: + res = t2; + break; + } + +#undef cmpop + + if (IS_ASSIGNOP(op)) { + vr->val.u = res; + if (!es->noassign) { + if (vasn->flag & INTEGER) + setint_v(vasn, vr, es->arith); + else + setint(vasn, vr->val.i); + } + vl = vr; + } else + vl->val.u = res; + } + return (vl); +} + +static void +exprtoken(Expr_state *es) +{ + const char *cp = es->tokp; + int c; + char *tvar; + + /* skip whitespace */ + skip_spaces: + --cp; + do { + c = ord(*++cp); + } while (ctype(c, C_SPACE)); + if (es->tokp == es->expression && (unsigned int)c == ORD('#')) { + /* expression begins with # */ + /* switch to unsigned */ + es->natural = true; + ++cp; + goto skip_spaces; + } + es->tokp = cp; + + if (c == '\0') + es->tok = END; + else if (ctype(c, C_ALPHX)) { + do { + c = ord(*++cp); + } while (ctype(c, C_ALNUX)); + if ((unsigned int)c == ORD('[')) { + size_t len; + + len = array_ref_len(cp); + if (len == 0) + evalerr(es, ET_STR, "missing ]"); + cp += len; + } + if (es->noassign) { + es->val = tempvar(""); + es->val->flag |= EXPRLVALUE; + } else { + strndupx(tvar, es->tokp, cp - es->tokp, ATEMP); + es->val = global(tvar); + afree(tvar, ATEMP); + } + es->tok = VAR; + } else if (c == '1' && cp[1] == '#') { + cp += 2; + if (*cp) + cp += utf_ptradj(cp); + strndupx(tvar, es->tokp, cp - es->tokp, ATEMP); + goto process_tvar; +#ifndef MKSH_SMALL + } else if (c == '\'') { + if (*++cp == '\0') { + es->tok = END; + evalerr(es, ET_UNEXPECTED, NULL); + } + cp += utf_ptradj(cp); + if (*cp++ != '\'') + evalerr(es, ET_STR, + "multi-character character constant"); + /* 'x' -> 1#x (x = one multibyte character) */ + c = cp - es->tokp; + tvar = alloc(c + /* NUL */ 1, ATEMP); + tvar[0] = '1'; + tvar[1] = '#'; + memcpy(tvar + 2, es->tokp + 1, c - 2); + tvar[c] = '\0'; + goto process_tvar; +#endif + } else if (ctype(c, C_DIGIT)) { + while (ctype(c, C_ALNUM | C_HASH)) + c = ord(*cp++); + strndupx(tvar, es->tokp, --cp - es->tokp, ATEMP); + process_tvar: + es->val = tempvar(""); + es->val->flag &= ~INTEGER; + es->val->type = 0; + es->val->val.s = tvar; + if (setint_v(es->val, es->val, es->arith) == NULL) + evalerr(es, ET_BADLIT, tvar); + afree(tvar, ATEMP); + es->tok = LIT; + } else { + int i, n0; + + for (i = 0; (n0 = ord(opname[i][0])); i++) + if (c == n0 && strncmp(cp, opname[i], + (size_t)oplen[i]) == 0) { + es->tok = (enum token)i; + cp += oplen[i]; + break; + } + if (!n0) + es->tok = BAD; + } + es->tokp = cp; +} + +static void +assign_check(Expr_state *es, enum token op, struct tbl *vasn) +{ + if (es->tok == END || !vasn || + (vasn->name[0] == '\0' && !(vasn->flag & EXPRLVALUE))) + evalerr(es, ET_LVALUE, opname[(int)op]); + else if (vasn->flag & RDONLY) + evalerr(es, ET_RDONLY, opname[(int)op]); +} + +struct tbl * +tempvar(const char *vname) +{ + struct tbl *vp; + size_t vsize; + + vsize = strlen(vname) + 1; + vp = alloc(offsetof(struct tbl, name[0]) + vsize, ATEMP); + memcpy(vp->name, vname, vsize); + vp->flag = ISSET|INTEGER; + vp->type = 0; + vp->areap = ATEMP; + vp->ua.hval = 0; + vp->val.i = 0; + return (vp); +} + +/* cast (string) variable to temporary integer variable */ +static struct tbl * +intvar(Expr_state *es, struct tbl *vp) +{ + struct tbl *vq; + + /* try to avoid replacing a temp var with another temp var */ + if (vp->name[0] == '\0' && + (vp->flag & (ISSET|INTEGER|EXPRLVALUE)) == (ISSET|INTEGER)) + return (vp); + + vq = tempvar(""); + if (setint_v(vq, vp, es->arith) == NULL) { + if (vp->flag & EXPRINEVAL) + evalerr(es, ET_RECURSIVE, vp->name); + es->evaling = vp; + vp->flag |= EXPRINEVAL; + v_evaluate(vq, str_val(vp), KSH_UNWIND_ERROR, es->arith); + vp->flag &= ~EXPRINEVAL; + es->evaling = NULL; + } + return (vq); +} + + +/* + * UTF-8 support code: high-level functions + */ + +int +utf_widthadj(const char *src, const char **dst) +{ + size_t len; + unsigned int wc; + int width; + + if (!UTFMODE || (len = utf_mbtowc(&wc, src)) == (size_t)-1 || + wc == 0) + len = width = 1; + else if ((width = utf_wcwidth(wc)) < 0) + /* XXX use 2 for x_zotc3 here? */ + width = 1; + + if (dst) + *dst = src + len; + return (width); +} + +size_t +utf_mbswidth(const char *s) +{ + size_t len, width = 0; + unsigned int wc; + int cw; + + if (!UTFMODE) + return (strlen(s)); + + while (*s) + if (((len = utf_mbtowc(&wc, s)) == (size_t)-1) || + ((cw = utf_wcwidth(wc)) == -1)) { + s++; + width += 1; + } else { + s += len; + width += cw; + } + return (width); +} + +const char * +utf_skipcols(const char *p, int cols, int *colp) +{ + int c = 0; + const char *q; + + while (c < cols) { + if (!*p) { + /* end of input; special handling for edit.c */ + if (!colp) + return (p + cols - c); + *colp = c; + return (p); + } + c += utf_widthadj(p, &p); + } + if (UTFMODE) + while (utf_widthadj(p, &q) == 0) + p = q; + if (colp) + *colp = c; + return (p); +} + +size_t +utf_ptradj(const char *src) +{ + register size_t n; + + if (!UTFMODE || rtt2asc(*src) < 0xC2 || + (n = utf_mbtowc(NULL, src)) == (size_t)-1) + n = 1; + return (n); +} + +/* + * UTF-8 support code: low-level functions + */ + +/* CESU-8 multibyte and wide character conversion crafted for mksh */ + +size_t +utf_mbtowc(unsigned int *dst, const char *src) +{ + const unsigned char *s = (const unsigned char *)src; + unsigned int c, wc; + + if ((wc = ord(rtt2asc(*s++))) < 0x80) { + out: + if (dst != NULL) + *dst = wc; + return (wc ? ((const char *)s - src) : 0); + } + if (wc < 0xC2 || wc >= 0xF0) + /* < 0xC0: spurious second byte */ + /* < 0xC2: non-minimalistic mapping error in 2-byte seqs */ + /* > 0xEF: beyond BMP */ + goto ilseq; + + if (wc < 0xE0) { + wc = (wc & 0x1F) << 6; + if (((c = ord(rtt2asc(*s++))) & 0xC0) != 0x80) + goto ilseq; + wc |= c & 0x3F; + goto out; + } + + wc = (wc & 0x0F) << 12; + + if (((c = ord(rtt2asc(*s++))) & 0xC0) != 0x80) + goto ilseq; + wc |= (c & 0x3F) << 6; + + if (((c = ord(rtt2asc(*s++))) & 0xC0) != 0x80) + goto ilseq; + wc |= c & 0x3F; + + /* Check for non-minimalistic mapping error in 3-byte seqs */ + if (wc >= 0x0800 && wc <= 0xFFFD) + goto out; + ilseq: + return ((size_t)(-1)); +} + +size_t +utf_wctomb(char *dst, unsigned int wc) +{ + unsigned char *d; + + if (wc < 0x80) { + *dst = asc2rtt(wc); + return (1); + } + + d = (unsigned char *)dst; + if (wc < 0x0800) + *d++ = asc2rtt((wc >> 6) | 0xC0); + else { + *d++ = asc2rtt(((wc = wc > 0xFFFD ? 0xFFFD : wc) >> 12) | 0xE0); + *d++ = asc2rtt(((wc >> 6) & 0x3F) | 0x80); + } + *d++ = asc2rtt((wc & 0x3F) | 0x80); + return ((char *)d - dst); +} + +/* + * Wrapper around access(2) because it says root can execute everything + * on some operating systems. Does not set errno, no user needs it. Use + * this iff mode can have the X_OK bit set, access otherwise. + */ +int +ksh_access(const char *fn, int mode) +{ +#ifdef __OS2__ + return (access_ex(access, fn, mode)); +#else + int rv; + struct stat sb; + + if ((rv = access(fn, mode)) == 0 && kshuid == 0 && (mode & X_OK) && + (rv = stat(fn, &sb)) == 0 && !S_ISDIR(sb.st_mode) && + (sb.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)) == 0) + rv = -1; + + return (rv); +#endif +} + +#ifndef MIRBSD_BOOTFLOPPY +/* From: X11/xc/programs/xterm/wcwidth.c,v 1.10 */ + +struct mb_ucsrange { + unsigned short beg; + unsigned short end; +}; + +static int mb_ucsbsearch(const struct mb_ucsrange arr[], size_t elems, + unsigned int val) MKSH_A_PURE; + +/* + * Generated from the Unicode Character Database, Version 10.0.0, by + * MirOS: contrib/code/Snippets/eawparse,v 1.12 2017/09/06 16:05:45 tg Exp $ + */ + +static const struct mb_ucsrange mb_ucs_combining[] = { + { 0x0300, 0x036F }, + { 0x0483, 0x0489 }, + { 0x0591, 0x05BD }, + { 0x05BF, 0x05BF }, + { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C5 }, + { 0x05C7, 0x05C7 }, + { 0x0610, 0x061A }, + { 0x061C, 0x061C }, + { 0x064B, 0x065F }, + { 0x0670, 0x0670 }, + { 0x06D6, 0x06DC }, + { 0x06DF, 0x06E4 }, + { 0x06E7, 0x06E8 }, + { 0x06EA, 0x06ED }, + { 0x0711, 0x0711 }, + { 0x0730, 0x074A }, + { 0x07A6, 0x07B0 }, + { 0x07EB, 0x07F3 }, + { 0x0816, 0x0819 }, + { 0x081B, 0x0823 }, + { 0x0825, 0x0827 }, + { 0x0829, 0x082D }, + { 0x0859, 0x085B }, + { 0x08D4, 0x08E1 }, + { 0x08E3, 0x0902 }, + { 0x093A, 0x093A }, + { 0x093C, 0x093C }, + { 0x0941, 0x0948 }, + { 0x094D, 0x094D }, + { 0x0951, 0x0957 }, + { 0x0962, 0x0963 }, + { 0x0981, 0x0981 }, + { 0x09BC, 0x09BC }, + { 0x09C1, 0x09C4 }, + { 0x09CD, 0x09CD }, + { 0x09E2, 0x09E3 }, + { 0x0A01, 0x0A02 }, + { 0x0A3C, 0x0A3C }, + { 0x0A41, 0x0A42 }, + { 0x0A47, 0x0A48 }, + { 0x0A4B, 0x0A4D }, + { 0x0A51, 0x0A51 }, + { 0x0A70, 0x0A71 }, + { 0x0A75, 0x0A75 }, + { 0x0A81, 0x0A82 }, + { 0x0ABC, 0x0ABC }, + { 0x0AC1, 0x0AC5 }, + { 0x0AC7, 0x0AC8 }, + { 0x0ACD, 0x0ACD }, + { 0x0AE2, 0x0AE3 }, + { 0x0AFA, 0x0AFF }, + { 0x0B01, 0x0B01 }, + { 0x0B3C, 0x0B3C }, + { 0x0B3F, 0x0B3F }, + { 0x0B41, 0x0B44 }, + { 0x0B4D, 0x0B4D }, + { 0x0B56, 0x0B56 }, + { 0x0B62, 0x0B63 }, + { 0x0B82, 0x0B82 }, + { 0x0BC0, 0x0BC0 }, + { 0x0BCD, 0x0BCD }, + { 0x0C00, 0x0C00 }, + { 0x0C3E, 0x0C40 }, + { 0x0C46, 0x0C48 }, + { 0x0C4A, 0x0C4D }, + { 0x0C55, 0x0C56 }, + { 0x0C62, 0x0C63 }, + { 0x0C81, 0x0C81 }, + { 0x0CBC, 0x0CBC }, + { 0x0CBF, 0x0CBF }, + { 0x0CC6, 0x0CC6 }, + { 0x0CCC, 0x0CCD }, + { 0x0CE2, 0x0CE3 }, + { 0x0D00, 0x0D01 }, + { 0x0D3B, 0x0D3C }, + { 0x0D41, 0x0D44 }, + { 0x0D4D, 0x0D4D }, + { 0x0D62, 0x0D63 }, + { 0x0DCA, 0x0DCA }, + { 0x0DD2, 0x0DD4 }, + { 0x0DD6, 0x0DD6 }, + { 0x0E31, 0x0E31 }, + { 0x0E34, 0x0E3A }, + { 0x0E47, 0x0E4E }, + { 0x0EB1, 0x0EB1 }, + { 0x0EB4, 0x0EB9 }, + { 0x0EBB, 0x0EBC }, + { 0x0EC8, 0x0ECD }, + { 0x0F18, 0x0F19 }, + { 0x0F35, 0x0F35 }, + { 0x0F37, 0x0F37 }, + { 0x0F39, 0x0F39 }, + { 0x0F71, 0x0F7E }, + { 0x0F80, 0x0F84 }, + { 0x0F86, 0x0F87 }, + { 0x0F8D, 0x0F97 }, + { 0x0F99, 0x0FBC }, + { 0x0FC6, 0x0FC6 }, + { 0x102D, 0x1030 }, + { 0x1032, 0x1037 }, + { 0x1039, 0x103A }, + { 0x103D, 0x103E }, + { 0x1058, 0x1059 }, + { 0x105E, 0x1060 }, + { 0x1071, 0x1074 }, + { 0x1082, 0x1082 }, + { 0x1085, 0x1086 }, + { 0x108D, 0x108D }, + { 0x109D, 0x109D }, + { 0x1160, 0x11FF }, + { 0x135D, 0x135F }, + { 0x1712, 0x1714 }, + { 0x1732, 0x1734 }, + { 0x1752, 0x1753 }, + { 0x1772, 0x1773 }, + { 0x17B4, 0x17B5 }, + { 0x17B7, 0x17BD }, + { 0x17C6, 0x17C6 }, + { 0x17C9, 0x17D3 }, + { 0x17DD, 0x17DD }, + { 0x180B, 0x180E }, + { 0x1885, 0x1886 }, + { 0x18A9, 0x18A9 }, + { 0x1920, 0x1922 }, + { 0x1927, 0x1928 }, + { 0x1932, 0x1932 }, + { 0x1939, 0x193B }, + { 0x1A17, 0x1A18 }, + { 0x1A1B, 0x1A1B }, + { 0x1A56, 0x1A56 }, + { 0x1A58, 0x1A5E }, + { 0x1A60, 0x1A60 }, + { 0x1A62, 0x1A62 }, + { 0x1A65, 0x1A6C }, + { 0x1A73, 0x1A7C }, + { 0x1A7F, 0x1A7F }, + { 0x1AB0, 0x1ABE }, + { 0x1B00, 0x1B03 }, + { 0x1B34, 0x1B34 }, + { 0x1B36, 0x1B3A }, + { 0x1B3C, 0x1B3C }, + { 0x1B42, 0x1B42 }, + { 0x1B6B, 0x1B73 }, + { 0x1B80, 0x1B81 }, + { 0x1BA2, 0x1BA5 }, + { 0x1BA8, 0x1BA9 }, + { 0x1BAB, 0x1BAD }, + { 0x1BE6, 0x1BE6 }, + { 0x1BE8, 0x1BE9 }, + { 0x1BED, 0x1BED }, + { 0x1BEF, 0x1BF1 }, + { 0x1C2C, 0x1C33 }, + { 0x1C36, 0x1C37 }, + { 0x1CD0, 0x1CD2 }, + { 0x1CD4, 0x1CE0 }, + { 0x1CE2, 0x1CE8 }, + { 0x1CED, 0x1CED }, + { 0x1CF4, 0x1CF4 }, + { 0x1CF8, 0x1CF9 }, + { 0x1DC0, 0x1DF9 }, + { 0x1DFB, 0x1DFF }, + { 0x200B, 0x200F }, + { 0x202A, 0x202E }, + { 0x2060, 0x2064 }, + { 0x2066, 0x206F }, + { 0x20D0, 0x20F0 }, + { 0x2CEF, 0x2CF1 }, + { 0x2D7F, 0x2D7F }, + { 0x2DE0, 0x2DFF }, + { 0x302A, 0x302D }, + { 0x3099, 0x309A }, + { 0xA66F, 0xA672 }, + { 0xA674, 0xA67D }, + { 0xA69E, 0xA69F }, + { 0xA6F0, 0xA6F1 }, + { 0xA802, 0xA802 }, + { 0xA806, 0xA806 }, + { 0xA80B, 0xA80B }, + { 0xA825, 0xA826 }, + { 0xA8C4, 0xA8C5 }, + { 0xA8E0, 0xA8F1 }, + { 0xA926, 0xA92D }, + { 0xA947, 0xA951 }, + { 0xA980, 0xA982 }, + { 0xA9B3, 0xA9B3 }, + { 0xA9B6, 0xA9B9 }, + { 0xA9BC, 0xA9BC }, + { 0xA9E5, 0xA9E5 }, + { 0xAA29, 0xAA2E }, + { 0xAA31, 0xAA32 }, + { 0xAA35, 0xAA36 }, + { 0xAA43, 0xAA43 }, + { 0xAA4C, 0xAA4C }, + { 0xAA7C, 0xAA7C }, + { 0xAAB0, 0xAAB0 }, + { 0xAAB2, 0xAAB4 }, + { 0xAAB7, 0xAAB8 }, + { 0xAABE, 0xAABF }, + { 0xAAC1, 0xAAC1 }, + { 0xAAEC, 0xAAED }, + { 0xAAF6, 0xAAF6 }, + { 0xABE5, 0xABE5 }, + { 0xABE8, 0xABE8 }, + { 0xABED, 0xABED }, + { 0xFB1E, 0xFB1E }, + { 0xFE00, 0xFE0F }, + { 0xFE20, 0xFE2F }, + { 0xFEFF, 0xFEFF }, + { 0xFFF9, 0xFFFB } +}; + +static const struct mb_ucsrange mb_ucs_fullwidth[] = { + { 0x1100, 0x115F }, + { 0x231A, 0x231B }, + { 0x2329, 0x232A }, + { 0x23E9, 0x23EC }, + { 0x23F0, 0x23F0 }, + { 0x23F3, 0x23F3 }, + { 0x25FD, 0x25FE }, + { 0x2614, 0x2615 }, + { 0x2648, 0x2653 }, + { 0x267F, 0x267F }, + { 0x2693, 0x2693 }, + { 0x26A1, 0x26A1 }, + { 0x26AA, 0x26AB }, + { 0x26BD, 0x26BE }, + { 0x26C4, 0x26C5 }, + { 0x26CE, 0x26CE }, + { 0x26D4, 0x26D4 }, + { 0x26EA, 0x26EA }, + { 0x26F2, 0x26F3 }, + { 0x26F5, 0x26F5 }, + { 0x26FA, 0x26FA }, + { 0x26FD, 0x26FD }, + { 0x2705, 0x2705 }, + { 0x270A, 0x270B }, + { 0x2728, 0x2728 }, + { 0x274C, 0x274C }, + { 0x274E, 0x274E }, + { 0x2753, 0x2755 }, + { 0x2757, 0x2757 }, + { 0x2795, 0x2797 }, + { 0x27B0, 0x27B0 }, + { 0x27BF, 0x27BF }, + { 0x2B1B, 0x2B1C }, + { 0x2B50, 0x2B50 }, + { 0x2B55, 0x2B55 }, + { 0x2E80, 0x3029 }, + { 0x302E, 0x303E }, + { 0x3040, 0x3098 }, + { 0x309B, 0xA4CF }, + { 0xA960, 0xA97F }, + { 0xAC00, 0xD7A3 }, + { 0xF900, 0xFAFF }, + { 0xFE10, 0xFE19 }, + { 0xFE30, 0xFE6F }, + { 0xFF01, 0xFF60 }, + { 0xFFE0, 0xFFE6 } +}; + +/* simple binary search in ranges, with bounds optimisation */ +static int +mb_ucsbsearch(const struct mb_ucsrange arr[], size_t elems, unsigned int val) +{ + size_t min = 0, mid, max = elems; + + if (val < arr[min].beg || val > arr[max - 1].end) + return (0); + + while (min < max) { + mid = (min + max) / 2; + + if (val < arr[mid].beg) + max = mid; + else if (val > arr[mid].end) + min = mid + 1; + else + return (1); + } + return (0); +} + +/* Unix column width of a wide character (Unicode code point, really) */ +int +utf_wcwidth(unsigned int wc) +{ + /* except NUL, C0/C1 control characters and DEL yield -1 */ + if (wc < 0x20 || (wc >= 0x7F && wc < 0xA0)) + return (wc ? -1 : 0); + + /* combining characters use 0 screen columns */ + if (mb_ucsbsearch(mb_ucs_combining, NELEM(mb_ucs_combining), wc)) + return (0); + + /* all others use 1 or 2 screen columns */ + if (mb_ucsbsearch(mb_ucs_fullwidth, NELEM(mb_ucs_fullwidth), wc)) + return (2); + return (1); +} +#endif diff --git a/exprtok.h b/exprtok.h new file mode 100644 index 0000000..e4329da --- /dev/null +++ b/exprtok.h @@ -0,0 +1,123 @@ +/*- + * Copyright (c) 2016 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#if defined(EXPRTOK_DEFNS) +__RCSID("$MirOS: src/bin/mksh/exprtok.h,v 1.2 2016/08/12 16:48:05 tg Exp $"); +/* see range comment below */ +#define IS_ASSIGNOP(op) ((int)(op) >= (int)O_ASN && (int)(op) <= (int)O_BORASN) +#define FN(name, len, prec, enum) /* nothing */ +#define F1(enum) /* nothing */ +#elif defined(EXPRTOK_ENUM) +#define F0(name, len, prec, enum) enum = 0, +#define FN(name, len, prec, enum) enum, +#define F1(enum) enum, +#define F2(enum) enum, +#define F9(enum) enum +#elif defined(EXPRTOK_NAME) +#define FN(name, len, prec, enum) name, +#define F1(enum) "" +#elif defined(EXPRTOK_LEN) +#define FN(name, len, prec, enum) len, +#define F1(enum) 0 +#elif defined(EXPRTOK_PREC) +#define FN(name, len, prec, enum) prec, +#define F1(enum) P_PRIMARY +#endif + +#ifndef F0 +#define F0 FN +#endif + +#ifndef F2 +#define F2(enum) /* nothing */ +#define F9(enum) /* nothing */ +#endif + +/* tokens must be ordered so the longest are first (e.g. += before +) */ + +/* some (long) unary operators */ +FN("++", 2, P_PRIMARY, O_PLUSPLUS = 0) /* before + */ +FN("--", 2, P_PRIMARY, O_MINUSMINUS) /* before - */ +/* binary operators */ +FN("==", 2, P_EQUALITY, O_EQ) /* before = */ +FN("!=", 2, P_EQUALITY, O_NE) /* before ! */ +/* assignments are assumed to be in range O_ASN .. O_BORASN */ +FN("=", 1, P_ASSIGN, O_ASN) +FN("*=", 2, P_ASSIGN, O_TIMESASN) +FN("/=", 2, P_ASSIGN, O_DIVASN) +FN("%=", 2, P_ASSIGN, O_MODASN) +FN("+=", 2, P_ASSIGN, O_PLUSASN) +FN("-=", 2, P_ASSIGN, O_MINUSASN) +#ifndef MKSH_LEGACY_MODE +FN("^<=", 3, P_ASSIGN, O_ROLASN) /* before ^< */ +FN("^>=", 3, P_ASSIGN, O_RORASN) /* before ^> */ +#endif +FN("<<=", 3, P_ASSIGN, O_LSHIFTASN) +FN(">>=", 3, P_ASSIGN, O_RSHIFTASN) +FN("&=", 2, P_ASSIGN, O_BANDASN) +FN("^=", 2, P_ASSIGN, O_BXORASN) +FN("|=", 2, P_ASSIGN, O_BORASN) +/* binary non-assignment operators */ +#ifndef MKSH_LEGACY_MODE +FN("^<", 2, P_SHIFT, O_ROL) /* before ^ */ +FN("^>", 2, P_SHIFT, O_ROR) /* before ^ */ +#endif +FN("<<", 2, P_SHIFT, O_LSHIFT) +FN(">>", 2, P_SHIFT, O_RSHIFT) +FN("<=", 2, P_RELATION, O_LE) +FN(">=", 2, P_RELATION, O_GE) +FN("<", 1, P_RELATION, O_LT) +FN(">", 1, P_RELATION, O_GT) +FN("&&", 2, P_LAND, O_LAND) +FN("||", 2, P_LOR, O_LOR) +FN("*", 1, P_MULT, O_TIMES) +FN("/", 1, P_MULT, O_DIV) +FN("%", 1, P_MULT, O_MOD) +FN("+", 1, P_ADD, O_PLUS) +FN("-", 1, P_ADD, O_MINUS) +FN("&", 1, P_BAND, O_BAND) +FN("^", 1, P_BXOR, O_BXOR) +FN("|", 1, P_BOR, O_BOR) +FN("?", 1, P_TERN, O_TERN) +FN(",", 1, P_COMMA, O_COMMA) +/* things after this aren't used as binary operators */ +/* unary that are not also binaries */ +FN("~", 1, P_PRIMARY, O_BNOT) +FN("!", 1, P_PRIMARY, O_LNOT) +/* misc */ +FN("(", 1, P_PRIMARY, OPEN_PAREN) +FN(")", 1, P_PRIMARY, CLOSE_PAREN) +FN(":", 1, P_PRIMARY, CTERN) +/* things that don't appear in the opinfo[] table */ +F1(VAR) /*XXX should be F2 */ +F2(LIT) +F2(END) +F9(BAD) + +#undef FN +#undef F0 +#undef F1 +#undef F2 +#undef F9 +#undef EXPRTOK_DEFNS +#undef EXPRTOK_ENUM +#undef EXPRTOK_NAME +#undef EXPRTOK_LEN +#undef EXPRTOK_PREC diff --git a/funcs.c b/funcs.c new file mode 100644 index 0000000..55c8218 --- /dev/null +++ b/funcs.c @@ -0,0 +1,3635 @@ +/* $OpenBSD: c_ksh.c,v 1.37 2015/09/10 22:48:58 nicm Exp $ */ +/* $OpenBSD: c_sh.c,v 1.46 2015/07/20 20:46:24 guenther Exp $ */ +/* $OpenBSD: c_test.c,v 1.18 2009/03/01 20:11:06 otto Exp $ */ +/* $OpenBSD: c_ulimit.c,v 1.19 2013/11/28 10:33:37 sobrado Exp $ */ + +/*- + * Copyright (c) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, + * 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +#if HAVE_SELECT +#if HAVE_SYS_BSDTYPES_H +#include +#endif +#if HAVE_SYS_SELECT_H +#include +#endif +#if HAVE_BSTRING_H +#include +#endif +#endif + +__RCSID("$MirOS: src/bin/mksh/funcs.c,v 1.353 2018/01/14 01:26:49 tg Exp $"); + +#if HAVE_KILLPG +/* + * use killpg if < -1 since -1 does special things + * for some non-killpg-endowed kills + */ +#define mksh_kill(p,s) ((p) < -1 ? killpg(-(p), (s)) : kill((p), (s))) +#else +/* cross fingers and hope kill is killpg-endowed */ +#define mksh_kill kill +#endif + +/* XXX conditions correct? */ +#if !defined(RLIM_INFINITY) && !defined(MKSH_NO_LIMITS) +#define MKSH_NO_LIMITS 1 +#endif + +#ifdef MKSH_NO_LIMITS +#define c_ulimit c_true +#endif + +#if !defined(MKSH_UNEMPLOYED) && HAVE_GETSID +static int c_suspend(const char **); +#endif + +static int do_whence(const char **, int, bool, bool); + +/* getn() that prints error */ +static int +bi_getn(const char *as, int *ai) +{ + int rv; + + if (!(rv = getn(as, ai))) + bi_errorf(Tf_sD_s, Tbadnum, as); + return (rv); +} + +static int +c_true(const char **wp MKSH_A_UNUSED) +{ + return (0); +} + +static int +c_false(const char **wp MKSH_A_UNUSED) +{ + return (1); +} + +/* + * A leading = means assignments before command are kept. + * A leading * means a POSIX special builtin. + * A leading ^ means declaration utility, - forwarder. + */ +const struct builtin mkshbuiltins[] = { + {Tsgdot, c_dot}, + {"*=:", c_true}, + {Tbracket, c_test}, + /* no =: AT&T manual wrong */ + {Talias, c_alias}, + {Tsgbreak, c_brkcont}, + {T__builtin, c_builtin}, + {Tbuiltin, c_builtin}, + {Tbcat, c_cat}, + {Tcd, c_cd}, + /* dash compatibility hack */ + {"chdir", c_cd}, + {T_command, c_command}, + {Tsgcontinue, c_brkcont}, + {"echo", c_print}, + {"*=eval", c_eval}, + {"*=exec", c_exec}, + {"*=exit", c_exitreturn}, + {Tdsgexport, c_typeset}, + {Tfalse, c_false}, + {"fc", c_fc}, + {Tgetopts, c_getopts}, + /* deprecated, replaced by typeset -g */ + {"^=global", c_typeset}, + {Tjobs, c_jobs}, + {"kill", c_kill}, + {"let", c_let}, + {"print", c_print}, + {"pwd", c_pwd}, + {Tread, c_read}, + {Tdsgreadonly, c_typeset}, + {"!realpath", c_realpath}, + {"~rename", c_rename}, + {"*=return", c_exitreturn}, + {Tsgset, c_set}, + {"*=shift", c_shift}, + {Tgsource, c_dot}, +#if !defined(MKSH_UNEMPLOYED) && HAVE_GETSID + {Tsuspend, c_suspend}, +#endif + {"test", c_test}, + {"*=times", c_times}, + {"*=trap", c_trap}, + {Ttrue, c_true}, + {Tdgtypeset, c_typeset}, + {"ulimit", c_ulimit}, + {"umask", c_umask}, + {Tunalias, c_unalias}, + {"*=unset", c_unset}, + {"wait", c_wait}, + {"whence", c_whence}, +#ifndef MKSH_UNEMPLOYED + {Tbg, c_fgbg}, + {Tfg, c_fgbg}, +#endif +#ifndef MKSH_NO_CMDLINE_EDITING + {"bind", c_bind}, +#endif +#if HAVE_MKNOD + {"mknod", c_mknod}, +#endif +#ifdef MKSH_PRINTF_BUILTIN + {"~printf", c_printf}, +#endif +#if HAVE_SELECT + {"sleep", c_sleep}, +#endif +#ifdef __MirBSD__ + /* alias to "true" for historical reasons */ + {"domainname", c_true}, +#endif +#ifdef __OS2__ + {Textproc, c_true}, +#endif + {NULL, (int (*)(const char **))NULL} +}; + +struct kill_info { + int num_width; + int name_width; +}; + +static const struct t_op { + char op_text[4]; + Test_op op_num; +} u_ops[] = { + {"-a", TO_FILAXST }, + {"-b", TO_FILBDEV }, + {"-c", TO_FILCDEV }, + {"-d", TO_FILID }, + {"-e", TO_FILEXST }, + {"-f", TO_FILREG }, + {"-G", TO_FILGID }, + {"-g", TO_FILSETG }, + {"-H", TO_FILCDF }, + {"-h", TO_FILSYM }, + {"-k", TO_FILSTCK }, + {"-L", TO_FILSYM }, + {"-n", TO_STNZE }, + {"-O", TO_FILUID }, + {"-o", TO_OPTION }, + {"-p", TO_FILFIFO }, + {"-r", TO_FILRD }, + {"-S", TO_FILSOCK }, + {"-s", TO_FILGZ }, + {"-t", TO_FILTT }, + {"-u", TO_FILSETU }, + {"-v", TO_ISSET }, + {"-w", TO_FILWR }, + {"-x", TO_FILEX }, + {"-z", TO_STZER }, + {"", TO_NONOP } +}; +static const struct t_op b_ops[] = { + {"=", TO_STEQL }, + {"==", TO_STEQL }, + {"!=", TO_STNEQ }, + {"<", TO_STLT }, + {">", TO_STGT }, + {"-eq", TO_INTEQ }, + {"-ne", TO_INTNE }, + {"-gt", TO_INTGT }, + {"-ge", TO_INTGE }, + {"-lt", TO_INTLT }, + {"-le", TO_INTLE }, + {"-ef", TO_FILEQ }, + {"-nt", TO_FILNT }, + {"-ot", TO_FILOT }, + {"", TO_NONOP } +}; + +static int test_oexpr(Test_env *, bool); +static int test_aexpr(Test_env *, bool); +static int test_nexpr(Test_env *, bool); +static int test_primary(Test_env *, bool); +static Test_op ptest_isa(Test_env *, Test_meta); +static const char *ptest_getopnd(Test_env *, Test_op, bool); +static void ptest_error(Test_env *, int, const char *); +static void kill_fmt_entry(char *, size_t, unsigned int, const void *); +static void p_time(struct shf *, bool, long, int, int, + const char *, const char *); + +int +c_pwd(const char **wp) +{ + int optc; + bool physical = tobool(Flag(FPHYSICAL)); + char *p, *allocd = NULL; + + while ((optc = ksh_getopt(wp, &builtin_opt, "LP")) != -1) + switch (optc) { + case 'L': + physical = false; + break; + case 'P': + physical = true; + break; + case '?': + return (1); + } + wp += builtin_opt.optind; + + if (wp[0]) { + bi_errorf(Ttoo_many_args); + return (1); + } + p = current_wd[0] ? (physical ? allocd = do_realpath(current_wd) : + current_wd) : NULL; + /* LINTED use of access */ + if (p && access(p, R_OK) < 0) + p = NULL; + if (!p && !(p = allocd = ksh_get_wd())) { + bi_errorf(Tf_sD_s, "can't determine current directory", + cstrerror(errno)); + return (1); + } + shprintf(Tf_sN, p); + afree(allocd, ATEMP); + return (0); +} + +static const char *s_ptr; +static int s_get(void); +static void s_put(int); + +int +c_print(const char **wp) +{ + int c; + const char *s; + char *xp; + XString xs; + struct { + /* storage for columnisation */ + XPtrV words; + /* temporary storage for a wide character */ + mksh_ari_t wc; + /* output file descriptor (if any) */ + int fd; + /* temporary storage for a multibyte character */ + char ts[4]; + /* output word separator */ + char ws; + /* output line separator */ + char ls; + /* output a trailing line separator? */ + bool nl; + /* expand backslash sequences? */ + bool exp; + /* columnise output? */ + bool col; + /* print to history instead of file descriptor / stdout? */ + bool hist; + /* print words as wide characters? */ + bool chars; + /* writing to a coprocess (SIGPIPE blocked)? */ + bool coproc; + bool copipe; + } po; + + memset(&po, 0, sizeof(po)); + po.fd = 1; + po.ws = ' '; + po.ls = '\n'; + po.nl = true; + + if (wp[0][0] == 'e') { + /* "echo" builtin */ + if (Flag(FPOSIX) || +#ifndef MKSH_MIDNIGHTBSD01ASH_COMPAT + Flag(FSH) || +#endif + Flag(FAS_BUILTIN)) { + /* BSD "echo" cmd, Debian Policy 10.4 compliant */ + ++wp; + bsd_echo: + if (*wp && !strcmp(*wp, "-n")) { + po.nl = false; + ++wp; + } + po.exp = false; + } else { + bool new_exp, new_nl = true; + + /*- + * compromise between various historic echos: only + * recognise -Een if they appear in arguments with + * no illegal options; e.g. echo -nq outputs '-nq' + */ +#ifdef MKSH_MIDNIGHTBSD01ASH_COMPAT + /* MidnightBSD /bin/sh needs -e supported but off */ + if (Flag(FSH)) + new_exp = false; + else +#endif + /* otherwise compromise on -e enabled by default */ + new_exp = true; + goto print_tradparse_beg; + + print_tradparse_arg: + if ((s = *wp) && *s++ == '-' && *s) { + print_tradparse_ch: + switch ((c = *s++)) { + case 'E': + new_exp = false; + goto print_tradparse_ch; + case 'e': + new_exp = true; + goto print_tradparse_ch; + case 'n': + new_nl = false; + goto print_tradparse_ch; + case '\0': + print_tradparse_beg: + po.exp = new_exp; + po.nl = new_nl; + ++wp; + goto print_tradparse_arg; + } + } + } + } else { + /* "print" builtin */ + const char *opts = "AcelNnpRrsu,"; + const char *emsg; + + po.exp = true; + + while ((c = ksh_getopt(wp, &builtin_opt, opts)) != -1) + switch (c) { + case 'A': + po.chars = true; + break; + case 'c': + po.col = true; + break; + case 'e': + po.exp = true; + break; + case 'l': + po.ws = '\n'; + break; + case 'N': + po.ws = '\0'; + po.ls = '\0'; + break; + case 'n': + po.nl = false; + break; + case 'p': + if ((po.fd = coproc_getfd(W_OK, &emsg)) < 0) { + bi_errorf(Tf_coproc, emsg); + return (1); + } + break; + case 'R': + /* fake BSD echo but don't reset other flags */ + wp += builtin_opt.optind; + goto bsd_echo; + case 'r': + po.exp = false; + break; + case 's': + po.hist = true; + break; + case 'u': + if (!*(s = builtin_opt.optarg)) + po.fd = 0; + else if ((po.fd = check_fd(s, W_OK, &emsg)) < 0) { + bi_errorf("-u%s: %s", s, emsg); + return (1); + } + break; + case '?': + return (1); + } + + if (!(builtin_opt.info & GI_MINUSMINUS)) { + /* treat a lone "-" like "--" */ + if (wp[builtin_opt.optind] && + ksh_isdash(wp[builtin_opt.optind])) + builtin_opt.optind++; + } + wp += builtin_opt.optind; + } + + if (po.col) { + if (*wp == NULL) + return (0); + + XPinit(po.words, 16); + } + + Xinit(xs, xp, 128, ATEMP); + + if (*wp == NULL) + goto print_no_arg; + print_read_arg: + if (po.chars) { + while (*wp != NULL) { + s = *wp++; + if (*s == '\0') + break; + if (!evaluate(s, &po.wc, KSH_RETURN_ERROR, true)) + return (1); + Xcheck(xs, xp); + if (UTFMODE) { + po.ts[utf_wctomb(po.ts, po.wc)] = 0; + c = 0; + do { + Xput(xs, xp, po.ts[c]); + } while (po.ts[++c]); + } else + Xput(xs, xp, po.wc & 0xFF); + } + } else { + s = *wp++; + while ((c = *s++) != '\0') { + Xcheck(xs, xp); + if (po.exp && c == '\\') { + s_ptr = s; + c = unbksl(false, s_get, s_put); + s = s_ptr; + if (c == -1) { + /* rejected by generic function */ + switch ((c = *s++)) { + case 'c': + po.nl = false; + /* AT&T brain damage */ + continue; + case '\0': + --s; + c = '\\'; + break; + default: + Xput(xs, xp, '\\'); + } + } else if ((unsigned int)c > 0xFF) { + /* generic function returned Unicode */ + po.ts[utf_wctomb(po.ts, c - 0x100)] = 0; + c = 0; + do { + Xput(xs, xp, po.ts[c]); + } while (po.ts[++c]); + continue; + } + } + Xput(xs, xp, c); + } + } + if (po.col) { + Xput(xs, xp, '\0'); + XPput(po.words, Xclose(xs, xp)); + Xinit(xs, xp, 128, ATEMP); + } + if (*wp != NULL) { + if (!po.col) + Xput(xs, xp, po.ws); + goto print_read_arg; + } + if (po.col) { + size_t w = XPsize(po.words); + struct columnise_opts co; + + XPput(po.words, NULL); + co.shf = shf_sopen(NULL, 128, SHF_WR | SHF_DYNAMIC, NULL); + co.linesep = po.ls; + co.prefcol = co.do_last = false; + pr_list(&co, (char **)XPptrv(po.words)); + while (w--) + afree(XPptrv(po.words)[w], ATEMP); + XPfree(po.words); + w = co.shf->wp - co.shf->buf; + XcheckN(xs, xp, w); + memcpy(xp, co.shf->buf, w); + xp += w; + shf_sclose(co.shf); + } + print_no_arg: + if (po.nl) + Xput(xs, xp, po.ls); + + c = 0; + if (po.hist) { + Xput(xs, xp, '\0'); + histsave(&source->line, Xstring(xs, xp), HIST_STORE, false); + } else { + size_t len = Xlength(xs, xp); + + /* + * Ensure we aren't killed by a SIGPIPE while writing to + * a coprocess. AT&T ksh doesn't seem to do this (seems + * to just check that the co-process is alive which is + * not enough). + */ + if (coproc.write >= 0 && coproc.write == po.fd) { + po.coproc = true; + po.copipe = block_pipe(); + } else + po.coproc = po.copipe = false; + + s = Xstring(xs, xp); + while (len > 0) { + ssize_t nwritten; + + if ((nwritten = write(po.fd, s, len)) < 0) { + if (errno == EINTR) { + if (po.copipe) + restore_pipe(); + /* give the user a chance to ^C out */ + intrcheck(); + /* interrupted, try again */ + if (po.coproc) + po.copipe = block_pipe(); + continue; + } + c = 1; + break; + } + s += nwritten; + len -= nwritten; + } + if (po.copipe) + restore_pipe(); + } + Xfree(xs, xp); + + return (c); +} + +static int +s_get(void) +{ + return (ord(*s_ptr++)); +} + +static void +s_put(int c MKSH_A_UNUSED) +{ + --s_ptr; +} + +int +c_whence(const char **wp) +{ + int optc; + bool pflag = false, vflag = false; + + while ((optc = ksh_getopt(wp, &builtin_opt, Tpv)) != -1) + switch (optc) { + case 'p': + pflag = true; + break; + case 'v': + vflag = true; + break; + case '?': + return (1); + } + wp += builtin_opt.optind; + + return (do_whence(wp, pflag ? FC_PATH : + FC_BI | FC_FUNC | FC_PATH | FC_WHENCE, vflag, false)); +} + +/* note: command without -vV is dealt with in comexec() */ +int +c_command(const char **wp) +{ + int optc, fcflags = FC_BI | FC_FUNC | FC_PATH | FC_WHENCE; + bool vflag = false; + + while ((optc = ksh_getopt(wp, &builtin_opt, TpVv)) != -1) + switch (optc) { + case 'p': + fcflags |= FC_DEFPATH; + break; + case 'V': + vflag = true; + break; + case 'v': + vflag = false; + break; + case '?': + return (1); + } + wp += builtin_opt.optind; + + return (do_whence(wp, fcflags, vflag, true)); +} + +static int +do_whence(const char **wp, int fcflags, bool vflag, bool iscommand) +{ + uint32_t h; + int rv = 0; + struct tbl *tp; + const char *id; + + while ((vflag || rv == 0) && (id = *wp++) != NULL) { + h = hash(id); + tp = NULL; + + if (fcflags & FC_WHENCE) + tp = ktsearch(&keywords, id, h); + if (!tp && (fcflags & FC_WHENCE)) { + tp = ktsearch(&aliases, id, h); + if (tp && !(tp->flag & ISSET)) + tp = NULL; + } + if (!tp) + tp = findcom(id, fcflags); + + switch (tp->type) { + case CSHELL: + case CFUNC: + case CKEYWD: + shf_puts(id, shl_stdout); + break; + } + + switch (tp->type) { + case CSHELL: + if (vflag) + shprintf(" is a %sshell %s", + (tp->flag & SPEC_BI) ? "special " : "", + Tbuiltin); + break; + case CFUNC: + if (vflag) { + shf_puts(" is a", shl_stdout); + if (tp->flag & EXPORT) + shf_puts("n exported", shl_stdout); + if (tp->flag & TRACE) + shf_puts(" traced", shl_stdout); + if (!(tp->flag & ISSET)) { + shf_puts(" undefined", shl_stdout); + if (tp->u.fpath) + shprintf(" (autoload from %s)", + tp->u.fpath); + } + shf_puts(T_function, shl_stdout); + } + break; + case CEXEC: + case CTALIAS: + if (tp->flag & ISSET) { + if (vflag) { + shprintf("%s is ", id); + if (tp->type == CTALIAS) + shprintf("a tracked %s%s for ", + (tp->flag & EXPORT) ? + "exported " : "", + Talias); + } + shf_puts(tp->val.s, shl_stdout); + } else { + if (vflag) + shprintf(Tnot_found_s, id); + rv = 1; + } + break; + case CALIAS: + if (vflag) { + shprintf("%s is an %s%s for ", id, + (tp->flag & EXPORT) ? "exported " : "", + Talias); + } else if (iscommand) + shprintf("%s %s=", Talias, id); + print_value_quoted(shl_stdout, tp->val.s); + break; + case CKEYWD: + if (vflag) + shf_puts(" is a reserved word", shl_stdout); + break; +#ifndef MKSH_SMALL + default: + bi_errorf(Tunexpected_type, id, Tcommand, tp->type); + return (1); +#endif + } + if (vflag || !rv) + shf_putc('\n', shl_stdout); + } + return (rv); +} + +bool +valid_alias_name(const char *cp) +{ + if (ord(*cp) == ORD('-')) + return (false); + if (ord(cp[0]) == ORD('[') && ord(cp[1]) == ORD('[') && !cp[2]) + return (false); + while (*cp) + if (ctype(*cp, C_ALIAS)) + ++cp; + else + return (false); + return (true); +} + +int +c_alias(const char **wp) +{ + struct table *t = &aliases; + int rv = 0, prefix = 0; + bool rflag = false, tflag, Uflag = false, pflag = false, chkalias; + uint32_t xflag = 0; + int optc; + + builtin_opt.flags |= GF_PLUSOPT; + while ((optc = ksh_getopt(wp, &builtin_opt, "dprtUx")) != -1) { + prefix = builtin_opt.info & GI_PLUS ? '+' : '-'; + switch (optc) { + case 'd': +#ifdef MKSH_NOPWNAM + t = NULL; /* fix "alias -dt" */ +#else + t = &homedirs; +#endif + break; + case 'p': + pflag = true; + break; + case 'r': + rflag = true; + break; + case 't': + t = &taliases; + break; + case 'U': + /* + * kludge for tracked alias initialization + * (don't do a path search, just make an entry) + */ + Uflag = true; + break; + case 'x': + xflag = EXPORT; + break; + case '?': + return (1); + } + } +#ifdef MKSH_NOPWNAM + if (t == NULL) + return (0); +#endif + wp += builtin_opt.optind; + + if (!(builtin_opt.info & GI_MINUSMINUS) && *wp && + ctype(wp[0][0], C_MINUS | C_PLUS) && wp[0][1] == '\0') { + prefix = wp[0][0]; + wp++; + } + + tflag = t == &taliases; + chkalias = t == &aliases; + + /* "hash -r" means reset all the tracked aliases.. */ + if (rflag) { + static const char *args[] = { + Tunalias, "-ta", NULL + }; + + if (!tflag || *wp) { + shprintf("%s: -r flag can only be used with -t" + " and without arguments\n", Talias); + return (1); + } + ksh_getopt_reset(&builtin_opt, GF_ERROR); + return (c_unalias(args)); + } + + if (*wp == NULL) { + struct tbl *ap, **p; + + for (p = ktsort(t); (ap = *p++) != NULL; ) + if ((ap->flag & (ISSET|xflag)) == (ISSET|xflag)) { + if (pflag) + shprintf(Tf_s_, Talias); + shf_puts(ap->name, shl_stdout); + if (prefix != '+') { + shf_putc('=', shl_stdout); + print_value_quoted(shl_stdout, ap->val.s); + } + shf_putc('\n', shl_stdout); + } + } + + for (; *wp != NULL; wp++) { + const char *alias = *wp, *val, *newval; + char *xalias = NULL; + struct tbl *ap; + uint32_t h; + + if ((val = cstrchr(alias, '='))) { + strndupx(xalias, alias, val++ - alias, ATEMP); + alias = xalias; + } + if (chkalias && !valid_alias_name(alias)) { + bi_errorf(Tinvname, alias, Talias); + afree(xalias, ATEMP); + return (1); + } + h = hash(alias); + if (val == NULL && !tflag && !xflag) { + ap = ktsearch(t, alias, h); + if (ap != NULL && (ap->flag&ISSET)) { + if (pflag) + shprintf(Tf_s_, Talias); + shf_puts(ap->name, shl_stdout); + if (prefix != '+') { + shf_putc('=', shl_stdout); + print_value_quoted(shl_stdout, ap->val.s); + } + shf_putc('\n', shl_stdout); + } else { + shprintf(Tf_s_s_sN, alias, Talias, Tnot_found); + rv = 1; + } + continue; + } + ap = ktenter(t, alias, h); + ap->type = tflag ? CTALIAS : CALIAS; + /* Are we setting the value or just some flags? */ + if ((val && !tflag) || (!val && tflag && !Uflag)) { + if (ap->flag&ALLOC) { + ap->flag &= ~(ALLOC|ISSET); + afree(ap->val.s, APERM); + } + /* ignore values for -t (AT&T ksh does this) */ + newval = tflag ? + search_path(alias, path, X_OK, NULL) : + val; + if (newval) { + strdupx(ap->val.s, newval, APERM); + ap->flag |= ALLOC|ISSET; + } else + ap->flag &= ~ISSET; + } + ap->flag |= DEFINED; + if (prefix == '+') + ap->flag &= ~xflag; + else + ap->flag |= xflag; + afree(xalias, ATEMP); + } + + return (rv); +} + +int +c_unalias(const char **wp) +{ + struct table *t = &aliases; + struct tbl *ap; + int optc, rv = 0; + bool all = false; + + while ((optc = ksh_getopt(wp, &builtin_opt, "adt")) != -1) + switch (optc) { + case 'a': + all = true; + break; + case 'd': +#ifdef MKSH_NOPWNAM + /* fix "unalias -dt" */ + t = NULL; +#else + t = &homedirs; +#endif + break; + case 't': + t = &taliases; + break; + case '?': + return (1); + } +#ifdef MKSH_NOPWNAM + if (t == NULL) + return (0); +#endif + wp += builtin_opt.optind; + + for (; *wp != NULL; wp++) { + ap = ktsearch(t, *wp, hash(*wp)); + if (ap == NULL) { + /* POSIX */ + rv = 1; + continue; + } + if (ap->flag&ALLOC) { + ap->flag &= ~(ALLOC|ISSET); + afree(ap->val.s, APERM); + } + ap->flag &= ~(DEFINED|ISSET|EXPORT); + } + + if (all) { + struct tstate ts; + + for (ktwalk(&ts, t); (ap = ktnext(&ts)); ) { + if (ap->flag&ALLOC) { + ap->flag &= ~(ALLOC|ISSET); + afree(ap->val.s, APERM); + } + ap->flag &= ~(DEFINED|ISSET|EXPORT); + } + } + + return (rv); +} + +int +c_let(const char **wp) +{ + int rv = 1; + mksh_ari_t val; + + if (wp[1] == NULL) + /* AT&T ksh does this */ + bi_errorf(Tno_args); + else + for (wp++; *wp; wp++) + if (!evaluate(*wp, &val, KSH_RETURN_ERROR, true)) { + /* distinguish error from zero result */ + rv = 2; + break; + } else + rv = val == 0; + return (rv); +} + +int +c_jobs(const char **wp) +{ + int optc, flag = 0, nflag = 0, rv = 0; + + while ((optc = ksh_getopt(wp, &builtin_opt, "lpnz")) != -1) + switch (optc) { + case 'l': + flag = 1; + break; + case 'p': + flag = 2; + break; + case 'n': + nflag = 1; + break; + case 'z': + /* debugging: print zombies */ + nflag = -1; + break; + case '?': + return (1); + } + wp += builtin_opt.optind; + if (!*wp) { + if (j_jobs(NULL, flag, nflag)) + rv = 1; + } else { + for (; *wp; wp++) + if (j_jobs(*wp, flag, nflag)) + rv = 1; + } + return (rv); +} + +#ifndef MKSH_UNEMPLOYED +int +c_fgbg(const char **wp) +{ + bool bg = strcmp(*wp, Tbg) == 0; + int rv = 0; + + if (!Flag(FMONITOR)) { + bi_errorf("job control not enabled"); + return (1); + } + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + wp += builtin_opt.optind; + if (*wp) + for (; *wp; wp++) + rv = j_resume(*wp, bg); + else + rv = j_resume("%%", bg); + /* fg returns $? of the job unless POSIX */ + return ((bg | Flag(FPOSIX)) ? 0 : rv); +} +#endif + +/* format a single kill item */ +static void +kill_fmt_entry(char *buf, size_t buflen, unsigned int i, const void *arg) +{ + const struct kill_info *ki = (const struct kill_info *)arg; + + i++; + shf_snprintf(buf, buflen, "%*u %*s %s", + ki->num_width, i, + ki->name_width, sigtraps[i].name, + sigtraps[i].mess); +} + +int +c_kill(const char **wp) +{ + Trap *t = NULL; + const char *p; + bool lflag = false; + int i, n, rv, sig; + + /* assume old style options if -digits or -UPPERCASE */ + if ((p = wp[1]) && *p == '-' && ctype(p[1], C_DIGIT | C_UPPER)) { + if (!(t = gettrap(p + 1, false, false))) { + bi_errorf(Tbad_sig_s, p + 1); + return (1); + } + i = (wp[2] && strcmp(wp[2], "--") == 0) ? 3 : 2; + } else { + int optc; + + while ((optc = ksh_getopt(wp, &builtin_opt, "ls:")) != -1) + switch (optc) { + case 'l': + lflag = true; + break; + case 's': + if (!(t = gettrap(builtin_opt.optarg, + true, false))) { + bi_errorf(Tbad_sig_s, + builtin_opt.optarg); + return (1); + } + break; + case '?': + return (1); + } + i = builtin_opt.optind; + } + if ((lflag && t) || (!wp[i] && !lflag)) { +#ifndef MKSH_SMALL + shf_puts("usage:\tkill [-s signame | -signum | -signame]" + " { job | pid | pgrp } ...\n" + "\tkill -l [exit_status ...]\n", shl_out); +#endif + bi_errorfz(); + return (1); + } + + if (lflag) { + if (wp[i]) { + for (; wp[i]; i++) { + if (!bi_getn(wp[i], &n)) + return (1); +#if (ksh_NSIG <= 128) + if (n > 128 && n < 128 + ksh_NSIG) + n -= 128; +#endif + if (n > 0 && n < ksh_NSIG) + shprintf(Tf_sN, sigtraps[n].name); + else + shprintf(Tf_dN, n); + } + } else if (Flag(FPOSIX)) { + n = 1; + while (n < ksh_NSIG) { + shf_puts(sigtraps[n].name, shl_stdout); + shf_putc(++n == ksh_NSIG ? '\n' : ' ', + shl_stdout); + } + } else { + ssize_t w, mess_cols = 0, mess_octs = 0; + int j = ksh_NSIG - 1; + struct kill_info ki = { 0, 0 }; + struct columnise_opts co; + + do { + ki.num_width++; + } while ((j /= 10)); + + for (j = 1; j < ksh_NSIG; j++) { + w = strlen(sigtraps[j].name); + if (w > ki.name_width) + ki.name_width = w; + w = strlen(sigtraps[j].mess); + if (w > mess_octs) + mess_octs = w; + w = utf_mbswidth(sigtraps[j].mess); + if (w > mess_cols) + mess_cols = w; + } + + co.shf = shl_stdout; + co.linesep = '\n'; + co.prefcol = co.do_last = true; + + print_columns(&co, (unsigned int)(ksh_NSIG - 1), + kill_fmt_entry, (void *)&ki, + ki.num_width + 1 + ki.name_width + 1 + mess_octs, + ki.num_width + 1 + ki.name_width + 1 + mess_cols); + } + return (0); + } + rv = 0; + sig = t ? t->signal : SIGTERM; + for (; (p = wp[i]); i++) { + if (*p == '%') { + if (j_kill(p, sig)) + rv = 1; + } else if (!getn(p, &n)) { + bi_errorf(Tf_sD_s, p, + "arguments must be jobs or process IDs"); + rv = 1; + } else { + if (mksh_kill(n, sig) < 0) { + bi_errorf(Tf_sD_s, p, cstrerror(errno)); + rv = 1; + } + } + } + return (rv); +} + +void +getopts_reset(int val) +{ + if (val >= 1) { + ksh_getopt_reset(&user_opt, GF_NONAME | + (Flag(FPOSIX) ? 0 : GF_PLUSOPT)); + user_opt.optind = user_opt.uoptind = val; + } +} + +int +c_getopts(const char **wp) +{ + int argc, optc, rv; + const char *opts, *var; + char buf[3]; + struct tbl *vq, *voptarg; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + wp += builtin_opt.optind; + + opts = *wp++; + if (!opts) { + bi_errorf(Tf_sD_s, "options", Tno_args); + return (1); + } + + var = *wp++; + if (!var) { + bi_errorf(Tf_sD_s, Tname, Tno_args); + return (1); + } + if (!*var || *skip_varname(var, true)) { + bi_errorf(Tf_sD_s, var, Tnot_ident); + return (1); + } + + if (e->loc->next == NULL) { + internal_warningf(Tf_sD_s, Tgetopts, Tno_args); + return (1); + } + /* Which arguments are we parsing... */ + if (*wp == NULL) + wp = e->loc->next->argv; + else + *--wp = e->loc->next->argv[0]; + + /* Check that our saved state won't cause a core dump... */ + for (argc = 0; wp[argc]; argc++) + ; + if (user_opt.optind > argc || + (user_opt.p != 0 && + user_opt.p > strlen(wp[user_opt.optind - 1]))) { + bi_errorf("arguments changed since last call"); + return (1); + } + + user_opt.optarg = NULL; + optc = ksh_getopt(wp, &user_opt, opts); + + if (optc >= 0 && optc != '?' && (user_opt.info & GI_PLUS)) { + buf[0] = '+'; + buf[1] = optc; + buf[2] = '\0'; + } else { + /* + * POSIX says var is set to ? at end-of-options, AT&T ksh + * sets it to null - we go with POSIX... + */ + buf[0] = optc < 0 ? '?' : optc; + buf[1] = '\0'; + } + + /* AT&T ksh93 in fact does change OPTIND for unknown options too */ + user_opt.uoptind = user_opt.optind; + + voptarg = global("OPTARG"); + /* AT&T ksh clears ro and int */ + voptarg->flag &= ~RDONLY; + /* Paranoia: ensure no bizarre results. */ + if (voptarg->flag & INTEGER) + typeset("OPTARG", 0, INTEGER, 0, 0); + if (user_opt.optarg == NULL) + unset(voptarg, 1); + else + /* this can't fail (haing cleared readonly/integer) */ + setstr(voptarg, user_opt.optarg, KSH_RETURN_ERROR); + + rv = 0; + + vq = global(var); + /* Error message already printed (integer, readonly) */ + if (!setstr(vq, buf, KSH_RETURN_ERROR)) + rv = 2; + if (Flag(FEXPORT)) + typeset(var, EXPORT, 0, 0, 0); + + return (optc < 0 ? 1 : rv); +} + +#ifndef MKSH_NO_CMDLINE_EDITING +int +c_bind(const char **wp) +{ + int optc, rv = 0; +#ifndef MKSH_SMALL + bool macro = false; +#endif + bool list = false; + const char *cp; + char *up; + + while ((optc = ksh_getopt(wp, &builtin_opt, +#ifndef MKSH_SMALL + "lm" +#else + "l" +#endif + )) != -1) + switch (optc) { + case 'l': + list = true; + break; +#ifndef MKSH_SMALL + case 'm': + macro = true; + break; +#endif + case '?': + return (1); + } + wp += builtin_opt.optind; + + if (*wp == NULL) + /* list all */ + rv = x_bind(NULL, NULL, +#ifndef MKSH_SMALL + false, +#endif + list); + + for (; *wp != NULL; wp++) { + if ((cp = cstrchr(*wp, '=')) == NULL) + up = NULL; + else { + strdupx(up, *wp, ATEMP); + up[cp++ - *wp] = '\0'; + } + if (x_bind(up ? up : *wp, cp, +#ifndef MKSH_SMALL + macro, +#endif + false)) + rv = 1; + afree(up, ATEMP); + } + + return (rv); +} +#endif + +int +c_shift(const char **wp) +{ + struct block *l = e->loc; + int n; + mksh_ari_t val; + const char *arg; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + arg = wp[builtin_opt.optind]; + + if (!arg) + n = 1; + else if (!evaluate(arg, &val, KSH_RETURN_ERROR, false)) { + /* error already printed */ + bi_errorfz(); + return (1); + } else if (!(n = val)) { + /* nothing to do */ + return (0); + } else if (n < 0) { + bi_errorf(Tf_sD_s, Tbadnum, arg); + return (1); + } + if (l->argc < n) { + bi_errorf("nothing to shift"); + return (1); + } + l->argv[n] = l->argv[0]; + l->argv += n; + l->argc -= n; + return (0); +} + +int +c_umask(const char **wp) +{ + int i, optc; + const char *cp; + bool symbolic = false; + mode_t old_umask; + + while ((optc = ksh_getopt(wp, &builtin_opt, "S")) != -1) + switch (optc) { + case 'S': + symbolic = true; + break; + case '?': + return (1); + } + cp = wp[builtin_opt.optind]; + if (cp == NULL) { + old_umask = umask((mode_t)0); + umask(old_umask); + if (symbolic) { + char buf[18], *p; + int j; + + old_umask = ~old_umask; + p = buf; + for (i = 0; i < 3; i++) { + *p++ = Tugo[i]; + *p++ = '='; + for (j = 0; j < 3; j++) + if (old_umask & (1 << (8 - (3*i + j)))) + *p++ = "rwx"[j]; + *p++ = ','; + } + p[-1] = '\0'; + shprintf(Tf_sN, buf); + } else + shprintf("%#3.3o\n", (unsigned int)old_umask); + } else { + mode_t new_umask; + + if (ctype(*cp, C_DIGIT)) { + new_umask = 0; + while (ctype(*cp, C_OCTAL)) { + new_umask = new_umask * 8 + ksh_numdig(*cp); + ++cp; + } + if (*cp) { + bi_errorf(Tbadnum); + return (1); + } + } else { + /* symbolic format */ + int positions, new_val; + char op; + + old_umask = umask((mode_t)0); + /* in case of error */ + umask(old_umask); + old_umask = ~old_umask; + new_umask = old_umask; + positions = 0; + while (*cp) { + while (*cp && vstrchr(Taugo, *cp)) + switch (*cp++) { + case 'a': + positions |= 0111; + break; + case 'u': + positions |= 0100; + break; + case 'g': + positions |= 0010; + break; + case 'o': + positions |= 0001; + break; + } + if (!positions) + /* default is a */ + positions = 0111; + if (!ctype((op = *cp), C_EQUAL | C_MINUS | C_PLUS)) + break; + cp++; + new_val = 0; + while (*cp && vstrchr("rwxugoXs", *cp)) + switch (*cp++) { + case 'r': new_val |= 04; break; + case 'w': new_val |= 02; break; + case 'x': new_val |= 01; break; + case 'u': + new_val |= old_umask >> 6; + break; + case 'g': + new_val |= old_umask >> 3; + break; + case 'o': + new_val |= old_umask >> 0; + break; + case 'X': + if (old_umask & 0111) + new_val |= 01; + break; + case 's': + /* ignored */ + break; + } + new_val = (new_val & 07) * positions; + switch (op) { + case '-': + new_umask &= ~new_val; + break; + case '=': + new_umask = new_val | + (new_umask & ~(positions * 07)); + break; + case '+': + new_umask |= new_val; + } + if (*cp == ',') { + positions = 0; + cp++; + } else if (!ctype(*cp, C_EQUAL | C_MINUS | C_PLUS)) + break; + } + if (*cp) { + bi_errorf("bad mask"); + return (1); + } + new_umask = ~new_umask; + } + umask(new_umask); + } + return (0); +} + +int +c_dot(const char **wp) +{ + const char *file, *cp, **argv; + int argc, rv, errcode; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + + if ((cp = wp[builtin_opt.optind]) == NULL) { + bi_errorf(Tno_args); + return (1); + } + file = search_path(cp, path, R_OK, &errcode); + if (!file && errcode == ENOENT && wp[0][0] == 's' && + search_access(cp, R_OK) == 0) + file = cp; + if (!file) { + bi_errorf(Tf_sD_s, cp, cstrerror(errcode)); + return (1); + } + + /* Set positional parameters? */ + if (wp[builtin_opt.optind + 1]) { + argv = wp + builtin_opt.optind; + /* preserve $0 */ + argv[0] = e->loc->argv[0]; + for (argc = 0; argv[argc + 1]; argc++) + ; + } else { + argc = 0; + argv = NULL; + } + /* SUSv4: OR with a high value never written otherwise */ + exstat |= 0x4000; + if ((rv = include(file, argc, argv, false)) < 0) { + /* should not happen */ + bi_errorf(Tf_sD_s, cp, cstrerror(errno)); + return (1); + } + if (exstat & 0x4000) + /* detect old exstat, use 0 in that case */ + rv = 0; + return (rv); +} + +int +c_wait(const char **wp) +{ + int rv = 0, sig; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + wp += builtin_opt.optind; + if (*wp == NULL) { + while (waitfor(NULL, &sig) >= 0) + ; + rv = sig; + } else { + for (; *wp; wp++) + rv = waitfor(*wp, &sig); + if (rv < 0) + /* magic exit code: bad job-id */ + rv = sig ? sig : 127; + } + return (rv); +} + +static const char REPLY[] = "REPLY"; +int +c_read(const char **wp) +{ +#define is_ifsws(c) (ctype((c), C_IFS) && ctype((c), C_IFSWS)) + int c, fd = 0, rv = 0; + bool savehist = false, intoarray = false, aschars = false; + bool rawmode = false, expanding = false; + bool lastparmmode = false, lastparmused = false; + enum { LINES, BYTES, UPTO, READALL } readmode = LINES; + char delim = '\n'; + size_t bytesleft = 128, bytesread; + struct tbl *vp /* FU gcc */ = NULL, *vq = NULL; + char *cp, *allocd = NULL, *xp; + const char *ccp; + XString xs; + size_t xsave = 0; + mksh_ttyst tios; + bool restore_tios = false; + /* to catch read -aN2 foo[i] */ + bool subarray = false; +#if HAVE_SELECT + bool hastimeout = false; + struct timeval tv, tvlim; +#define c_read_opts "Aad:N:n:prst:u," +#else +#define c_read_opts "Aad:N:n:prsu," +#endif +#if defined(__OS2__) && defined(MKSH_WITH_TEXTMODE) + int saved_mode; + int saved_errno; +#endif + + while ((c = ksh_getopt(wp, &builtin_opt, c_read_opts)) != -1) + switch (c) { + case 'a': + aschars = true; + /* FALLTHROUGH */ + case 'A': + intoarray = true; + break; + case 'd': + delim = builtin_opt.optarg[0]; + break; + case 'N': + case 'n': + readmode = c == 'N' ? BYTES : UPTO; + if (!bi_getn(builtin_opt.optarg, &c)) + return (2); + if (c == -1) { + readmode = readmode == BYTES ? READALL : UPTO; + bytesleft = 1024; + } else + bytesleft = (unsigned int)c; + break; + case 'p': + if ((fd = coproc_getfd(R_OK, &ccp)) < 0) { + bi_errorf(Tf_coproc, ccp); + return (2); + } + break; + case 'r': + rawmode = true; + break; + case 's': + savehist = true; + break; +#if HAVE_SELECT + case 't': + if (parse_usec(builtin_opt.optarg, &tv)) { + bi_errorf(Tf_sD_s_qs, Tsynerr, cstrerror(errno), + builtin_opt.optarg); + return (2); + } + hastimeout = true; + break; +#endif + case 'u': + if (!builtin_opt.optarg[0]) + fd = 0; + else if ((fd = check_fd(builtin_opt.optarg, R_OK, &ccp)) < 0) { + bi_errorf(Tf_sD_sD_s, "-u", builtin_opt.optarg, ccp); + return (2); + } + break; + case '?': + return (2); + } + wp += builtin_opt.optind; + if (*wp == NULL) + *--wp = REPLY; + + if (intoarray && wp[1] != NULL) { + bi_errorf(Ttoo_many_args); + return (2); + } + + if ((ccp = cstrchr(*wp, '?')) != NULL) { + strdupx(allocd, *wp, ATEMP); + allocd[ccp - *wp] = '\0'; + *wp = allocd; + if (isatty(fd)) { + /* + * AT&T ksh says it prints prompt on fd if it's open + * for writing and is a tty, but it doesn't do it + * (it also doesn't check the interactive flag, + * as is indicated in the Korn Shell book). + */ + shf_puts(ccp + 1, shl_out); + shf_flush(shl_out); + } + } + + Xinit(xs, xp, bytesleft, ATEMP); + + if (readmode == LINES) + bytesleft = 1; + else if (isatty(fd)) { + x_mkraw(fd, &tios, true); + restore_tios = true; + } + +#if HAVE_SELECT + if (hastimeout) { + mksh_TIME(tvlim); + timeradd(&tvlim, &tv, &tvlim); + } +#endif + + c_read_readloop: +#if HAVE_SELECT + if (hastimeout) { + fd_set fdset; + + FD_ZERO(&fdset); + FD_SET((unsigned int)fd, &fdset); + mksh_TIME(tv); + timersub(&tvlim, &tv, &tv); + if (tv.tv_sec < 0) { + /* timeout expired globally */ + rv = 3; + goto c_read_out; + } + + switch (select(fd + 1, &fdset, NULL, NULL, &tv)) { + case 1: + break; + case 0: + /* timeout expired for this call */ + bytesread = 0; + rv = 3; + goto c_read_readdone; + default: + bi_errorf(Tf_sD_s, Tselect, cstrerror(errno)); + rv = 2; + goto c_read_out; + } + } +#endif + +#if defined(__OS2__) && defined(MKSH_WITH_TEXTMODE) + saved_mode = setmode(fd, O_TEXT); +#endif + if ((bytesread = blocking_read(fd, xp, bytesleft)) == (size_t)-1) { +#if defined(__OS2__) && defined(MKSH_WITH_TEXTMODE) + saved_errno = errno; + setmode(fd, saved_mode); + errno = saved_errno; +#endif + if (errno == EINTR) { + /* check whether the signal would normally kill */ + if (!fatal_trap_check()) { + /* no, just ignore the signal */ + goto c_read_readloop; + } + /* pretend the read was killed */ + } else { + /* unexpected error */ + bi_errorf(Tf_s, cstrerror(errno)); + } + rv = 2; + goto c_read_out; + } +#if defined(__OS2__) && defined(MKSH_WITH_TEXTMODE) + setmode(fd, saved_mode); +#endif + + switch (readmode) { + case READALL: + if (bytesread == 0) { + /* end of file reached */ + rv = 1; + goto c_read_readdone; + } + xp += bytesread; + XcheckN(xs, xp, bytesleft); + break; + + case UPTO: + if (bytesread == 0) + /* end of file reached */ + rv = 1; + xp += bytesread; + goto c_read_readdone; + + case BYTES: + if (bytesread == 0) { + /* end of file reached */ + rv = 1; + /* may be partial read: $? = 1, but content */ + goto c_read_readdone; + } + xp += bytesread; + if ((bytesleft -= bytesread) == 0) + goto c_read_readdone; + break; + case LINES: + if (bytesread == 0) { + /* end of file reached */ + rv = 1; + goto c_read_readdone; + } + if ((c = *xp) == '\0' && !aschars && delim != '\0') { + /* skip any read NULs unless delimiter */ + break; + } + if (expanding) { + expanding = false; + if (c == delim) { + if (Flag(FTALKING_I) && isatty(fd)) { + /* + * set prompt in case this is + * called from .profile or $ENV + */ + set_prompt(PS2, NULL); + pprompt(prompt, 0); + } + /* drop the backslash */ + --xp; + /* and the delimiter */ + break; + } + } else if (c == delim) { + goto c_read_readdone; + } else if (!rawmode && c == '\\') { + expanding = true; + } + Xcheck(xs, xp); + ++xp; + break; + } + goto c_read_readloop; + + c_read_readdone: + bytesread = Xlength(xs, xp); + Xput(xs, xp, '\0'); + + /*- + * state: we finished reading the input and NUL terminated it + * Xstring(xs, xp) -> xp-1 = input string without trailing delim + * rv = 3 if timeout, 1 if EOF, 0 otherwise (errors handled already) + */ + + if (rv) { + /* clean up coprocess if needed, on EOF/error/timeout */ + coproc_read_close(fd); + if (readmode == READALL && (rv == 1 || (rv == 3 && bytesread))) + /* EOF is no error here */ + rv = 0; + } + + if (savehist) + histsave(&source->line, Xstring(xs, xp), HIST_STORE, false); + + ccp = cp = Xclose(xs, xp); + expanding = false; + XinitN(xs, 128, ATEMP); + if (intoarray) { + vp = global(*wp); + subarray = last_lookup_was_array; + if (vp->flag & RDONLY) { + c_read_splitro: + bi_errorf(Tf_ro, *wp); + c_read_spliterr: + rv = 2; + afree(cp, ATEMP); + goto c_read_out; + } + /* counter for array index */ + c = subarray ? arrayindex(vp) : 0; + /* exporting an array is currently pointless */ + unset(vp, subarray ? 0 : 1); + } + if (!aschars) { + /* skip initial IFS whitespace */ + while (bytesread && is_ifsws(*ccp)) { + ++ccp; + --bytesread; + } + /* trim trailing IFS whitespace */ + while (bytesread && is_ifsws(ccp[bytesread - 1])) { + --bytesread; + } + } + c_read_splitloop: + xp = Xstring(xs, xp); + /* generate next word */ + if (!bytesread) { + /* no more input */ + if (intoarray) + goto c_read_splitdone; + /* zero out next parameters */ + goto c_read_gotword; + } + if (aschars) { + Xput(xs, xp, '1'); + Xput(xs, xp, '#'); + bytesleft = utf_ptradj(ccp); + while (bytesleft && bytesread) { + *xp++ = *ccp++; + --bytesleft; + --bytesread; + } + if (xp[-1] == '\0') { + xp[-1] = '0'; + xp[-3] = '2'; + } + goto c_read_gotword; + } + + if (!intoarray && wp[1] == NULL) + lastparmmode = true; + + c_read_splitlast: + /* copy until IFS character */ + while (bytesread) { + char ch; + + ch = *ccp; + if (expanding) { + expanding = false; + goto c_read_splitcopy; + } else if (ctype(ch, C_IFS)) { + break; + } else if (!rawmode && ch == '\\') { + expanding = true; + } else { + c_read_splitcopy: + Xcheck(xs, xp); + Xput(xs, xp, ch); + } + ++ccp; + --bytesread; + } + xsave = Xsavepos(xs, xp); + /* copy word delimiter: IFSWS+IFS,IFSWS */ + expanding = false; + while (bytesread) { + char ch; + + ch = *ccp; + if (!ctype(ch, C_IFS)) + break; + if (lastparmmode && !expanding && !rawmode && ch == '\\') { + expanding = true; + } else { + Xcheck(xs, xp); + Xput(xs, xp, ch); + } + ++ccp; + --bytesread; + if (expanding) + continue; + if (!ctype(ch, C_IFSWS)) + break; + } + while (bytesread && is_ifsws(*ccp)) { + Xcheck(xs, xp); + Xput(xs, xp, *ccp); + ++ccp; + --bytesread; + } + /* if no more parameters, rinse and repeat */ + if (lastparmmode && bytesread) { + lastparmused = true; + goto c_read_splitlast; + } + /* get rid of the delimiter unless we pack the rest */ + if (!lastparmused) + xp = Xrestpos(xs, xp, xsave); + c_read_gotword: + Xput(xs, xp, '\0'); + if (intoarray) { + if (subarray) { + /* array element passed, accept first read */ + if (vq) { + bi_errorf("nested arrays not yet supported"); + goto c_read_spliterr; + } + vq = vp; + if (c) + /* [0] doesn't */ + vq->flag |= AINDEX; + } else + vq = arraysearch(vp, c++); + } else { + vq = global(*wp); + /* must be checked before exporting */ + if (vq->flag & RDONLY) + goto c_read_splitro; + if (Flag(FEXPORT)) + typeset(*wp, EXPORT, 0, 0, 0); + } + if (!setstr(vq, Xstring(xs, xp), KSH_RETURN_ERROR)) + goto c_read_spliterr; + if (aschars) { + setint_v(vq, vq, false); + /* protect from UTFMODE changes */ + vq->type = 0; + } + if (intoarray || *++wp != NULL) + goto c_read_splitloop; + + c_read_splitdone: + /* free up */ + afree(cp, ATEMP); + + c_read_out: + afree(allocd, ATEMP); + Xfree(xs, xp); + if (restore_tios) + mksh_tcset(fd, &tios); + return (rv == 3 ? ksh_sigmask(SIGALRM) : rv); +#undef is_ifsws +} + +int +c_eval(const char **wp) +{ + struct source *s, *saves = source; + unsigned char savef; + int rv; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + s = pushs(SWORDS, ATEMP); + s->u.strv = wp + builtin_opt.optind; + s->line = current_lineno; + + /*- + * The following code handles the case where the command is + * empty due to failed command substitution, for example by + * eval "$(false)" + * This has historically returned 1 by AT&T ksh88. In this + * case, shell() will not set or change exstat because the + * compiled tree is empty, so it will use the value we pass + * from subst_exstat, which is cleared in execute(), so it + * should have been 0 if there were no substitutions. + * + * POSIX however says we don't do this, even though it is + * traditionally done. AT&T ksh93 agrees with POSIX, so we + * do. The following is an excerpt from SUSv4 [1003.2-2008]: + * + * 2.9.1: Simple Commands + * ... If there is a command name, execution shall + * continue as described in 2.9.1.1 [Command Search + * and Execution]. If there is no command name, but + * the command contained a command substitution, the + * command shall complete with the exit status of the + * last command substitution performed. + * 2.9.1.1: Command Search and Execution + * (1) a. If the command name matches the name of a + * special built-in utility, that special built-in + * utility shall be invoked. + * 2.14.5: eval + * If there are no arguments, or only null arguments, + * eval shall return a zero exit status; ... + */ + /* AT&T ksh88: use subst_exstat */ + /* exstat = subst_exstat; */ + /* SUSv4: OR with a high value never written otherwise */ + exstat |= 0x4000; + + savef = Flag(FERREXIT); + Flag(FERREXIT) |= 0x80; + rv = shell(s, 2); + Flag(FERREXIT) = savef; + source = saves; + afree(s, ATEMP); + if (exstat & 0x4000) + /* detect old exstat, use 0 in that case */ + rv = 0; + return (rv); +} + +int +c_trap(const char **wp) +{ + Trap *p = sigtraps; + int i = ksh_NSIG; + const char *s; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + return (1); + wp += builtin_opt.optind; + + if (*wp == NULL) { + do { + if (p->trap) { + shf_puts("trap -- ", shl_stdout); + print_value_quoted(shl_stdout, p->trap); + shprintf(Tf__sN, p->name); + } + ++p; + } while (i--); + return (0); + } + + if (getn(*wp, &i)) { + /* first argument is a signal number, reset them all */ + s = NULL; + } else { + /* first argument must be a command, then */ + s = *wp++; + /* reset traps? */ + if (ksh_isdash(s)) + s = NULL; + } + + /* set/clear the traps */ + i = 0; + while (*wp) + if (!(p = gettrap(*wp++, true, true))) { + warningf(true, Tbad_sig_ss, builtin_argv0, wp[-1]); + i = 1; + } else + settrap(p, s); + return (i); +} + +int +c_exitreturn(const char **wp) +{ + int n, how = LEXIT; + + if (wp[1]) { + if (wp[2]) + goto c_exitreturn_err; + exstat = bi_getn(wp[1], &n) ? (n & 0xFF) : 1; + } else if (trap_exstat != -1) + exstat = trap_exstat; + + if (wp[0][0] == 'r') { + /* return */ + struct env *ep; + + /* + * need to tell if this is exit or return so trap exit will + * work right (POSIX) + */ + for (ep = e; ep; ep = ep->oenv) + if (STOP_RETURN(ep->type)) { + how = LRETURN; + break; + } + } + + if (how == LEXIT && !really_exit && j_stopped_running()) { + really_exit = true; + how = LSHELL; + } + + /* get rid of any I/O redirections */ + quitenv(NULL); + unwind(how); + /* NOTREACHED */ + + c_exitreturn_err: + bi_errorf(Ttoo_many_args); + return (1); +} + +int +c_brkcont(const char **wp) +{ + unsigned int quit; + int n; + struct env *ep, *last_ep = NULL; + const char *arg; + + if (ksh_getopt(wp, &builtin_opt, null) == '?') + goto c_brkcont_err; + arg = wp[builtin_opt.optind]; + + if (!arg) + n = 1; + else if (!bi_getn(arg, &n)) + goto c_brkcont_err; + if (n <= 0) { + /* AT&T ksh does this for non-interactive shells only - weird */ + bi_errorf("%s: bad value", arg); + goto c_brkcont_err; + } + quit = (unsigned int)n; + + /* Stop at E_NONE, E_PARSE, E_FUNC, or E_INCL */ + for (ep = e; ep && !STOP_BRKCONT(ep->type); ep = ep->oenv) + if (ep->type == E_LOOP) { + if (--quit == 0) + break; + ep->flags |= EF_BRKCONT_PASS; + last_ep = ep; + } + + if (quit) { + /* + * AT&T ksh doesn't print a message - just does what it + * can. We print a message 'cause it helps in debugging + * scripts, but don't generate an error (ie, keep going). + */ + if ((unsigned int)n == quit) { + warningf(true, Tf_cant_s, wp[0], wp[0]); + return (0); + } + /* + * POSIX says if n is too big, the last enclosing loop + * shall be used. Doesn't say to print an error but we + * do anyway 'cause the user messed up. + */ + if (last_ep) + last_ep->flags &= ~EF_BRKCONT_PASS; + warningf(true, "%s: can only %s %u level(s)", + wp[0], wp[0], (unsigned int)n - quit); + } + + unwind(*wp[0] == 'b' ? LBREAK : LCONTIN); + /* NOTREACHED */ + + c_brkcont_err: + return (1); +} + +int +c_set(const char **wp) +{ + int argi; + bool setargs; + struct block *l = e->loc; + const char **owp; + + if (wp[1] == NULL) { + static const char *args[] = { Tset, "-", NULL }; + return (c_typeset(args)); + } + + if ((argi = parse_args(wp, OF_SET, &setargs)) < 0) + return (2); + /* set $# and $* */ + if (setargs) { + wp += argi - 1; + owp = wp; + /* save $0 */ + wp[0] = l->argv[0]; + while (*++wp != NULL) + strdupx(*wp, *wp, &l->area); + l->argc = wp - owp - 1; + l->argv = alloc2(l->argc + 2, sizeof(char *), &l->area); + for (wp = l->argv; (*wp++ = *owp++) != NULL; ) + ; + } + /*- + * POSIX says set exit status is 0, but old scripts that use + * getopt(1) use the construct + * set -- $(getopt ab:c "$@") + * which assumes the exit value set will be that of the $() + * (subst_exstat is cleared in execute() so that it will be 0 + * if there are no command substitutions). + */ +#ifdef MKSH_LEGACY_MODE + /* traditional behaviour, unless set -o posix */ + return (Flag(FPOSIX) ? 0 : subst_exstat); +#else + /* conformant behaviour, unless set -o sh +o posix */ + return (Flag(FSH) && !Flag(FPOSIX) ? subst_exstat : 0); +#endif +} + +int +c_unset(const char **wp) +{ + const char *id; + int optc, rv = 0; + bool unset_var = true; + + while ((optc = ksh_getopt(wp, &builtin_opt, "fv")) != -1) + switch (optc) { + case 'f': + unset_var = false; + break; + case 'v': + unset_var = true; + break; + case '?': + /*XXX not reached due to GF_ERROR */ + return (2); + } + wp += builtin_opt.optind; + for (; (id = *wp) != NULL; wp++) + if (unset_var) { + /* unset variable */ + struct tbl *vp; + char *cp = NULL; + size_t n; + + n = strlen(id); + if (n > 3 && ord(id[n - 3]) == ORD('[') && + ord(id[n - 2]) == ORD('*') && + ord(id[n - 1]) == ORD(']')) { + strndupx(cp, id, n - 3, ATEMP); + id = cp; + optc = 3; + } else + optc = vstrchr(id, '[') ? 0 : 1; + + vp = global(id); + afree(cp, ATEMP); + + if ((vp->flag&RDONLY)) { + warningf(true, Tf_ro, vp->name); + rv = 1; + } else + unset(vp, optc); + } else + /* unset function */ + define(id, NULL); + return (rv); +} + +static void +p_time(struct shf *shf, bool posix, long tv_sec, int tv_usec, int width, + const char *prefix, const char *suffix) +{ + tv_usec /= 10000; + if (posix) + shf_fprintf(shf, "%s%*ld.%02d%s", prefix, width, + tv_sec, tv_usec, suffix); + else + shf_fprintf(shf, "%s%*ldm%02d.%02ds%s", prefix, width, + tv_sec / 60, (int)(tv_sec % 60), tv_usec, suffix); +} + +int +c_times(const char **wp MKSH_A_UNUSED) +{ + struct rusage usage; + + getrusage(RUSAGE_SELF, &usage); + p_time(shl_stdout, false, usage.ru_utime.tv_sec, + usage.ru_utime.tv_usec, 0, null, T1space); + p_time(shl_stdout, false, usage.ru_stime.tv_sec, + usage.ru_stime.tv_usec, 0, null, "\n"); + + getrusage(RUSAGE_CHILDREN, &usage); + p_time(shl_stdout, false, usage.ru_utime.tv_sec, + usage.ru_utime.tv_usec, 0, null, T1space); + p_time(shl_stdout, false, usage.ru_stime.tv_sec, + usage.ru_stime.tv_usec, 0, null, "\n"); + + return (0); +} + +/* + * time pipeline (really a statement, not a built-in command) + */ +int +timex(struct op *t, int f, volatile int *xerrok) +{ +#define TF_NOARGS BIT(0) +#define TF_NOREAL BIT(1) /* don't report real time */ +#define TF_POSIX BIT(2) /* report in POSIX format */ + int rv = 0, tf = 0; + struct rusage ru0, ru1, cru0, cru1; + struct timeval usrtime, systime, tv0, tv1; + + mksh_TIME(tv0); + getrusage(RUSAGE_SELF, &ru0); + getrusage(RUSAGE_CHILDREN, &cru0); + if (t->left) { + /* + * Two ways of getting cpu usage of a command: just use t0 + * and t1 (which will get cpu usage from other jobs that + * finish while we are executing t->left), or get the + * cpu usage of t->left. AT&T ksh does the former, while + * pdksh tries to do the later (the j_usrtime hack doesn't + * really work as it only counts the last job). + */ + timerclear(&j_usrtime); + timerclear(&j_systime); + rv = execute(t->left, f | XTIME, xerrok); + if (t->left->type == TCOM) + tf |= t->left->str[0]; + mksh_TIME(tv1); + getrusage(RUSAGE_SELF, &ru1); + getrusage(RUSAGE_CHILDREN, &cru1); + } else + tf = TF_NOARGS; + + if (tf & TF_NOARGS) { + /* ksh93 - report shell times (shell+kids) */ + tf |= TF_NOREAL; + timeradd(&ru0.ru_utime, &cru0.ru_utime, &usrtime); + timeradd(&ru0.ru_stime, &cru0.ru_stime, &systime); + } else { + timersub(&ru1.ru_utime, &ru0.ru_utime, &usrtime); + timeradd(&usrtime, &j_usrtime, &usrtime); + timersub(&ru1.ru_stime, &ru0.ru_stime, &systime); + timeradd(&systime, &j_systime, &systime); + } + + if (!(tf & TF_NOREAL)) { + timersub(&tv1, &tv0, &tv1); + if (tf & TF_POSIX) + p_time(shl_out, true, tv1.tv_sec, tv1.tv_usec, + 5, Treal_sp1, "\n"); + else + p_time(shl_out, false, tv1.tv_sec, tv1.tv_usec, + 5, null, Treal_sp2); + } + if (tf & TF_POSIX) + p_time(shl_out, true, usrtime.tv_sec, usrtime.tv_usec, + 5, Tuser_sp1, "\n"); + else + p_time(shl_out, false, usrtime.tv_sec, usrtime.tv_usec, + 5, null, Tuser_sp2); + if (tf & TF_POSIX) + p_time(shl_out, true, systime.tv_sec, systime.tv_usec, + 5, "sys ", "\n"); + else + p_time(shl_out, false, systime.tv_sec, systime.tv_usec, + 5, null, " system\n"); + shf_flush(shl_out); + + return (rv); +} + +void +timex_hook(struct op *t, char **volatile *app) +{ + char **wp = *app; + int optc, i, j; + Getopt opt; + + ksh_getopt_reset(&opt, 0); + /* start at the start */ + opt.optind = 0; + while ((optc = ksh_getopt((const char **)wp, &opt, ":p")) != -1) + switch (optc) { + case 'p': + t->str[0] |= TF_POSIX; + break; + case '?': + errorf(Tf_optfoo, Ttime, Tcolsp, + opt.optarg[0], Tunknown_option); + case ':': + errorf(Tf_optfoo, Ttime, Tcolsp, + opt.optarg[0], Treq_arg); + } + /* Copy command words down over options. */ + if (opt.optind != 0) { + for (i = 0; i < opt.optind; i++) + afree(wp[i], ATEMP); + for (i = 0, j = opt.optind; (wp[i] = wp[j]); i++, j++) + ; + } + if (!wp[0]) + t->str[0] |= TF_NOARGS; + *app = wp; +} + +/* exec with no args - args case is taken care of in comexec() */ +int +c_exec(const char **wp MKSH_A_UNUSED) +{ + int i; + + /* make sure redirects stay in place */ + if (e->savefd != NULL) { + for (i = 0; i < NUFILE; i++) { + if (e->savefd[i] > 0) + close(e->savefd[i]); + /* + * keep all file descriptors > 2 private for ksh, + * but not for POSIX or legacy/kludge sh + */ + if (!Flag(FPOSIX) && !Flag(FSH) && i > 2 && + e->savefd[i]) + fcntl(i, F_SETFD, FD_CLOEXEC); + } + e->savefd = NULL; + } + return (0); +} + +#if HAVE_MKNOD && !defined(__OS2__) +int +c_mknod(const char **wp) +{ + int argc, optc, rv = 0; + bool ismkfifo = false; + const char **argv; + void *set = NULL; + mode_t mode = 0, oldmode = 0; + + while ((optc = ksh_getopt(wp, &builtin_opt, "m:")) != -1) { + switch (optc) { + case 'm': + set = setmode(builtin_opt.optarg); + if (set == NULL) { + bi_errorf("invalid file mode"); + return (1); + } + mode = getmode(set, (mode_t)(DEFFILEMODE)); + free_ossetmode(set); + break; + default: + goto c_mknod_usage; + } + } + argv = &wp[builtin_opt.optind]; + if (argv[0] == NULL) + goto c_mknod_usage; + for (argc = 0; argv[argc]; argc++) + ; + if (argc == 2 && argv[1][0] == 'p') + ismkfifo = true; + else if (argc != 4 || (argv[1][0] != 'b' && argv[1][0] != 'c')) + goto c_mknod_usage; + + if (set != NULL) + oldmode = umask((mode_t)0); + else + mode = DEFFILEMODE; + + mode |= (argv[1][0] == 'b') ? S_IFBLK : + (argv[1][0] == 'c') ? S_IFCHR : 0; + + if (!ismkfifo) { + unsigned long majnum, minnum; + dev_t dv; + char *c; + + majnum = strtoul(argv[2], &c, 0); + if ((c == argv[2]) || (*c != '\0')) { + bi_errorf(Tf_nonnum, "device", "major", argv[2]); + goto c_mknod_err; + } + minnum = strtoul(argv[3], &c, 0); + if ((c == argv[3]) || (*c != '\0')) { + bi_errorf(Tf_nonnum, "device", "minor", argv[3]); + goto c_mknod_err; + } + dv = makedev(majnum, minnum); + if ((unsigned long)(major(dv)) != majnum) { + bi_errorf(Tf_toolarge, "device", "major", majnum); + goto c_mknod_err; + } + if ((unsigned long)(minor(dv)) != minnum) { + bi_errorf(Tf_toolarge, "device", "minor", minnum); + goto c_mknod_err; + } + if (mknod(argv[0], mode, dv)) + goto c_mknod_failed; + } else if (mkfifo(argv[0], mode)) { + c_mknod_failed: + bi_errorf(Tf_sD_s, argv[0], cstrerror(errno)); + c_mknod_err: + rv = 1; + } + + if (set) + umask(oldmode); + return (rv); + c_mknod_usage: + bi_errorf("usage: mknod [-m mode] name %s", "b|c major minor"); + bi_errorf("usage: mknod [-m mode] name %s", "p"); + return (1); +} +#endif + +/*- + test(1) roughly accepts the following grammar: + oexpr ::= aexpr | aexpr "-o" oexpr ; + aexpr ::= nexpr | nexpr "-a" aexpr ; + nexpr ::= primary | "!" nexpr ; + primary ::= unary-operator operand + | operand binary-operator operand + | operand + | "(" oexpr ")" + ; + + unary-operator ::= "-a"|"-b"|"-c"|"-d"|"-e"|"-f"|"-G"|"-g"|"-H"|"-h"| + "-k"|"-L"|"-n"|"-O"|"-o"|"-p"|"-r"|"-S"|"-s"|"-t"| + "-u"|"-v"|"-w"|"-x"|"-z"; + + binary-operator ::= "="|"=="|"!="|"<"|">"|"-eq"|"-ne"|"-gt"|"-ge"| + "-lt"|"-le"|"-ef"|"-nt"|"-ot"; + + operand ::= +*/ + +/* POSIX says > 1 for errors */ +#define T_ERR_EXIT 2 + +int +c_test(const char **wp) +{ + int argc, rv, invert = 0; + Test_env te; + Test_op op; + Test_meta tm; + const char *lhs, **swp; + + te.flags = 0; + te.isa = ptest_isa; + te.getopnd = ptest_getopnd; + te.eval = test_eval; + te.error = ptest_error; + + for (argc = 0; wp[argc]; argc++) + ; + + if (strcmp(wp[0], Tbracket) == 0) { + if (strcmp(wp[--argc], "]") != 0) { + bi_errorf("missing ]"); + return (T_ERR_EXIT); + } + } + + te.pos.wp = wp + 1; + te.wp_end = wp + argc; + + /* + * Attempt to conform to POSIX special cases. This is pretty + * dumb code straight-forward from the 2008 spec, but unlike + * the old pdksh code doesn't live from so many assumptions. + * It does, though, inline some calls to '(*te.funcname)()'. + */ + switch (argc - 1) { + case 0: + return (1); + case 1: + ptest_one: + op = TO_STNZE; + goto ptest_unary; + case 2: + ptest_two: + if (ptest_isa(&te, TM_NOT)) { + ++invert; + goto ptest_one; + } + if ((op = ptest_isa(&te, TM_UNOP))) { + ptest_unary: + rv = test_eval(&te, op, *te.pos.wp++, NULL, true); + ptest_out: + if (te.flags & TEF_ERROR) + return (T_ERR_EXIT); + return ((invert & 1) ? rv : !rv); + } + /* let the parser deal with anything else */ + break; + case 3: + ptest_three: + swp = te.pos.wp; + /* use inside knowledge of ptest_getopnd inlined below */ + lhs = *te.pos.wp++; + if ((op = ptest_isa(&te, TM_BINOP))) { + /* test lhs op rhs */ + rv = test_eval(&te, op, lhs, *te.pos.wp++, true); + goto ptest_out; + } + if (ptest_isa(&te, tm = TM_AND) || ptest_isa(&te, tm = TM_OR)) { + /* XSI */ + argc = test_eval(&te, TO_STNZE, lhs, NULL, true); + rv = test_eval(&te, TO_STNZE, *te.pos.wp++, NULL, true); + if (tm == TM_AND) + rv = argc && rv; + else + rv = argc || rv; + goto ptest_out; + } + /* back up to lhs */ + te.pos.wp = swp; + if (ptest_isa(&te, TM_NOT)) { + ++invert; + goto ptest_two; + } + if (ptest_isa(&te, TM_OPAREN)) { + swp = te.pos.wp; + /* skip operand, without evaluation */ + te.pos.wp++; + /* check for closing parenthesis */ + op = ptest_isa(&te, TM_CPAREN); + /* back up to operand */ + te.pos.wp = swp; + /* if there was a closing paren, handle it */ + if (op) + goto ptest_one; + /* backing up is done before calling the parser */ + } + /* let the parser deal with it */ + break; + case 4: + if (ptest_isa(&te, TM_NOT)) { + ++invert; + goto ptest_three; + } + if (ptest_isa(&te, TM_OPAREN)) { + swp = te.pos.wp; + /* skip two operands, without evaluation */ + te.pos.wp++; + te.pos.wp++; + /* check for closing parenthesis */ + op = ptest_isa(&te, TM_CPAREN); + /* back up to first operand */ + te.pos.wp = swp; + /* if there was a closing paren, handle it */ + if (op) + goto ptest_two; + /* backing up is done before calling the parser */ + } + /* defer this to the parser */ + break; + } + + /* "The results are unspecified." */ + te.pos.wp = wp + 1; + return (test_parse(&te)); +} + +/* + * Generic test routines. + */ + +Test_op +test_isop(Test_meta meta, const char *s) +{ + char sc1; + const struct t_op *tbl; + + tbl = meta == TM_UNOP ? u_ops : b_ops; + if (*s) { + sc1 = s[1]; + for (; tbl->op_text[0]; tbl++) + if (sc1 == tbl->op_text[1] && !strcmp(s, tbl->op_text)) + return (tbl->op_num); + } + return (TO_NONOP); +} + +#ifdef __OS2__ +#define test_access(name, mode) access_ex(access, (name), (mode)) +#define test_stat(name, buffer) stat_ex((name), (buffer)) +#else +#define test_access(name, mode) access((name), (mode)) +#define test_stat(name, buffer) stat((name), (buffer)) +#endif + +int +test_eval(Test_env *te, Test_op op, const char *opnd1, const char *opnd2, + bool do_eval) +{ + int i, s; + size_t k; + struct stat b1, b2; + mksh_ari_t v1, v2; + struct tbl *vp; + + if (!do_eval) + return (0); + +#ifdef DEBUG + switch (op) { + /* Binary operators */ + case TO_STEQL: + case TO_STNEQ: + case TO_STLT: + case TO_STGT: + case TO_INTEQ: + case TO_INTNE: + case TO_INTGT: + case TO_INTGE: + case TO_INTLT: + case TO_INTLE: + case TO_FILEQ: + case TO_FILNT: + case TO_FILOT: + /* consistency check, but does not happen in practice */ + if (!opnd2) { + te->flags |= TEF_ERROR; + return (1); + } + break; + default: + /* for completeness of switch */ + break; + } +#endif + + switch (op) { + + /* + * Unary Operators + */ + + /* -n */ + case TO_STNZE: + return (*opnd1 != '\0'); + + /* -z */ + case TO_STZER: + return (*opnd1 == '\0'); + + /* -v */ + case TO_ISSET: + return ((vp = isglobal(opnd1, false)) && (vp->flag & ISSET)); + + /* -o */ + case TO_OPTION: + if ((i = *opnd1) == '!' || i == '?') + opnd1++; + if ((k = option(opnd1)) == (size_t)-1) + return (0); + return (i == '?' ? 1 : i == '!' ? !Flag(k) : Flag(k)); + + /* -r */ + case TO_FILRD: + /* LINTED use of access */ + return (test_access(opnd1, R_OK) == 0); + + /* -w */ + case TO_FILWR: + /* LINTED use of access */ + return (test_access(opnd1, W_OK) == 0); + + /* -x */ + case TO_FILEX: + return (ksh_access(opnd1, X_OK) == 0); + + /* -a */ + case TO_FILAXST: + /* -e */ + case TO_FILEXST: + return (test_stat(opnd1, &b1) == 0); + + /* -f */ + case TO_FILREG: + return (test_stat(opnd1, &b1) == 0 && S_ISREG(b1.st_mode)); + + /* -d */ + case TO_FILID: + return (stat(opnd1, &b1) == 0 && S_ISDIR(b1.st_mode)); + + /* -c */ + case TO_FILCDEV: + return (stat(opnd1, &b1) == 0 && S_ISCHR(b1.st_mode)); + + /* -b */ + case TO_FILBDEV: + return (stat(opnd1, &b1) == 0 && S_ISBLK(b1.st_mode)); + + /* -p */ + case TO_FILFIFO: + return (stat(opnd1, &b1) == 0 && S_ISFIFO(b1.st_mode)); + + /* -h or -L */ + case TO_FILSYM: +#ifdef MKSH__NO_SYMLINK + return (0); +#else + return (lstat(opnd1, &b1) == 0 && S_ISLNK(b1.st_mode)); +#endif + + /* -S */ + case TO_FILSOCK: + return (stat(opnd1, &b1) == 0 && S_ISSOCK(b1.st_mode)); + + /* -H => HP context dependent files (directories) */ + case TO_FILCDF: +#ifdef S_ISCDF + { + char *nv; + + /* + * Append a + to filename and check to see if result is + * a setuid directory. CDF stuff in general is hookey, + * since it breaks for, e.g., the following sequence: + * echo hi >foo+; mkdir foo; echo bye >foo/default; + * chmod u+s foo (foo+ refers to the file with hi in it, + * there is no way to get at the file with bye in it; + * please correct me if I'm wrong about this). + */ + + nv = shf_smprintf("%s+", opnd1); + i = (stat(nv, &b1) == 0 && S_ISCDF(b1.st_mode)); + afree(nv, ATEMP); + return (i); + } +#else + return (0); +#endif + + /* -u */ + case TO_FILSETU: + return (stat(opnd1, &b1) == 0 && + (b1.st_mode & S_ISUID) == S_ISUID); + + /* -g */ + case TO_FILSETG: + return (stat(opnd1, &b1) == 0 && + (b1.st_mode & S_ISGID) == S_ISGID); + + /* -k */ + case TO_FILSTCK: +#ifdef S_ISVTX + return (stat(opnd1, &b1) == 0 && + (b1.st_mode & S_ISVTX) == S_ISVTX); +#else + return (0); +#endif + + /* -s */ + case TO_FILGZ: + return (stat(opnd1, &b1) == 0 && (off_t)b1.st_size > (off_t)0); + + /* -t */ + case TO_FILTT: + if (opnd1 && !bi_getn(opnd1, &i)) { + te->flags |= TEF_ERROR; + i = 0; + } else + i = isatty(opnd1 ? i : 0); + return (i); + + /* -O */ + case TO_FILUID: + return (stat(opnd1, &b1) == 0 && (uid_t)b1.st_uid == ksheuid); + + /* -G */ + case TO_FILGID: + return (stat(opnd1, &b1) == 0 && (gid_t)b1.st_gid == getegid()); + + /* + * Binary Operators + */ + + /* =, == */ + case TO_STEQL: + if (te->flags & TEF_DBRACKET) { + if ((i = gmatchx(opnd1, opnd2, false))) + record_match(opnd1); + return (i); + } + return (strcmp(opnd1, opnd2) == 0); + + /* != */ + case TO_STNEQ: + if (te->flags & TEF_DBRACKET) { + if ((i = gmatchx(opnd1, opnd2, false))) + record_match(opnd1); + return (!i); + } + return (strcmp(opnd1, opnd2) != 0); + + /* < */ + case TO_STLT: + return (strcmp(opnd1, opnd2) < 0); + + /* > */ + case TO_STGT: + return (strcmp(opnd1, opnd2) > 0); + + /* -eq */ + case TO_INTEQ: + /* -ne */ + case TO_INTNE: + /* -ge */ + case TO_INTGE: + /* -gt */ + case TO_INTGT: + /* -le */ + case TO_INTLE: + /* -lt */ + case TO_INTLT: + if (!evaluate(opnd1, &v1, KSH_RETURN_ERROR, false) || + !evaluate(opnd2, &v2, KSH_RETURN_ERROR, false)) { + /* error already printed.. */ + te->flags |= TEF_ERROR; + return (1); + } + switch (op) { + case TO_INTEQ: + return (v1 == v2); + case TO_INTNE: + return (v1 != v2); + case TO_INTGE: + return (v1 >= v2); + case TO_INTGT: + return (v1 > v2); + case TO_INTLE: + return (v1 <= v2); + case TO_INTLT: + return (v1 < v2); + default: + /* NOTREACHED */ + break; + } + /* NOTREACHED */ + + /* -nt */ + case TO_FILNT: + /* + * ksh88/ksh93 succeed if file2 can't be stated + * (subtly different from 'does not exist'). + */ + return (stat(opnd1, &b1) == 0 && + (((s = stat(opnd2, &b2)) == 0 && + b1.st_mtime > b2.st_mtime) || s < 0)); + + /* -ot */ + case TO_FILOT: + /* + * ksh88/ksh93 succeed if file1 can't be stated + * (subtly different from 'does not exist'). + */ + return (stat(opnd2, &b2) == 0 && + (((s = stat(opnd1, &b1)) == 0 && + b1.st_mtime < b2.st_mtime) || s < 0)); + + /* -ef */ + case TO_FILEQ: + return (stat (opnd1, &b1) == 0 && stat (opnd2, &b2) == 0 && + b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino); + + /* all other cases */ + case TO_NONOP: + case TO_NONNULL: + /* throw the error */ + break; + } + (*te->error)(te, 0, "internal error: unknown op"); + return (1); +} + +int +test_parse(Test_env *te) +{ + int rv; + + rv = test_oexpr(te, 1); + + if (!(te->flags & TEF_ERROR) && !(*te->isa)(te, TM_END)) + (*te->error)(te, 0, "unexpected operator/operand"); + + return ((te->flags & TEF_ERROR) ? T_ERR_EXIT : !rv); +} + +static int +test_oexpr(Test_env *te, bool do_eval) +{ + int rv; + + if ((rv = test_aexpr(te, do_eval))) + do_eval = false; + if (!(te->flags & TEF_ERROR) && (*te->isa)(te, TM_OR)) + return (test_oexpr(te, do_eval) || rv); + return (rv); +} + +static int +test_aexpr(Test_env *te, bool do_eval) +{ + int rv; + + if (!(rv = test_nexpr(te, do_eval))) + do_eval = false; + if (!(te->flags & TEF_ERROR) && (*te->isa)(te, TM_AND)) + return (test_aexpr(te, do_eval) && rv); + return (rv); +} + +static int +test_nexpr(Test_env *te, bool do_eval) +{ + if (!(te->flags & TEF_ERROR) && (*te->isa)(te, TM_NOT)) + return (!test_nexpr(te, do_eval)); + return (test_primary(te, do_eval)); +} + +static int +test_primary(Test_env *te, bool do_eval) +{ + const char *opnd1, *opnd2; + int rv; + Test_op op; + + if (te->flags & TEF_ERROR) + return (0); + if ((*te->isa)(te, TM_OPAREN)) { + rv = test_oexpr(te, do_eval); + if (te->flags & TEF_ERROR) + return (0); + if (!(*te->isa)(te, TM_CPAREN)) { + (*te->error)(te, 0, "missing )"); + return (0); + } + return (rv); + } + /* + * Binary should have precedence over unary in this case + * so that something like test \( -f = -f \) is accepted + */ + if ((te->flags & TEF_DBRACKET) || (&te->pos.wp[1] < te->wp_end && + !test_isop(TM_BINOP, te->pos.wp[1]))) { + if ((op = (*te->isa)(te, TM_UNOP))) { + /* unary expression */ + opnd1 = (*te->getopnd)(te, op, do_eval); + if (!opnd1) { + (*te->error)(te, -1, Tno_args); + return (0); + } + + return ((*te->eval)(te, op, opnd1, NULL, do_eval)); + } + } + opnd1 = (*te->getopnd)(te, TO_NONOP, do_eval); + if (!opnd1) { + (*te->error)(te, 0, "expression expected"); + return (0); + } + if ((op = (*te->isa)(te, TM_BINOP))) { + /* binary expression */ + opnd2 = (*te->getopnd)(te, op, do_eval); + if (!opnd2) { + (*te->error)(te, -1, "missing second argument"); + return (0); + } + + return ((*te->eval)(te, op, opnd1, opnd2, do_eval)); + } + return ((*te->eval)(te, TO_STNZE, opnd1, NULL, do_eval)); +} + +/* + * Plain test (test and [ .. ]) specific routines. + */ + +/* + * Test if the current token is a whatever. Accepts the current token if + * it is. Returns 0 if it is not, non-zero if it is (in the case of + * TM_UNOP and TM_BINOP, the returned value is a Test_op). + */ +static Test_op +ptest_isa(Test_env *te, Test_meta meta) +{ + /* Order important - indexed by Test_meta values */ + static const char * const tokens[] = { + "-o", "-a", "!", "(", ")" + }; + Test_op rv; + + if (te->pos.wp >= te->wp_end) + return (meta == TM_END ? TO_NONNULL : TO_NONOP); + + if (meta == TM_UNOP || meta == TM_BINOP) + rv = test_isop(meta, *te->pos.wp); + else if (meta == TM_END) + rv = TO_NONOP; + else + rv = !strcmp(*te->pos.wp, tokens[(int)meta]) ? + TO_NONNULL : TO_NONOP; + + /* Accept the token? */ + if (rv != TO_NONOP) + te->pos.wp++; + + return (rv); +} + +static const char * +ptest_getopnd(Test_env *te, Test_op op, bool do_eval MKSH_A_UNUSED) +{ + if (te->pos.wp >= te->wp_end) + return (op == TO_FILTT ? "1" : NULL); + return (*te->pos.wp++); +} + +static void +ptest_error(Test_env *te, int ofs, const char *msg) +{ + const char *op; + + te->flags |= TEF_ERROR; + if ((op = te->pos.wp + ofs >= te->wp_end ? NULL : te->pos.wp[ofs])) + bi_errorf(Tf_sD_s, op, msg); + else + bi_errorf(Tf_s, msg); +} + +#ifndef MKSH_NO_LIMITS +#define SOFT 0x1 +#define HARD 0x2 + +/* Magic to divine the 'm' and 'v' limits */ + +#ifdef RLIMIT_AS +#if !defined(RLIMIT_VMEM) || (RLIMIT_VMEM == RLIMIT_AS) || \ + !defined(RLIMIT_RSS) || (RLIMIT_VMEM == RLIMIT_RSS) +#define ULIMIT_V_IS_AS +#elif defined(RLIMIT_VMEM) +#if !defined(RLIMIT_RSS) || (RLIMIT_RSS == RLIMIT_AS) +#define ULIMIT_V_IS_AS +#else +#define ULIMIT_V_IS_VMEM +#endif +#endif +#endif + +#ifdef RLIMIT_RSS +#ifdef ULIMIT_V_IS_VMEM +#define ULIMIT_M_IS_RSS +#elif defined(RLIMIT_VMEM) && (RLIMIT_VMEM == RLIMIT_RSS) +#define ULIMIT_M_IS_VMEM +#else +#define ULIMIT_M_IS_RSS +#endif +#if defined(ULIMIT_M_IS_RSS) && defined(RLIMIT_AS) && (RLIMIT_RSS == RLIMIT_AS) +#undef ULIMIT_M_IS_RSS +#endif +#endif + +#if !defined(RLIMIT_AS) && !defined(ULIMIT_M_IS_VMEM) && defined(RLIMIT_VMEM) +#define ULIMIT_V_IS_VMEM +#endif + +#if !defined(ULIMIT_V_IS_VMEM) && defined(RLIMIT_VMEM) && \ + (!defined(RLIMIT_RSS) || (defined(RLIMIT_AS) && (RLIMIT_RSS == RLIMIT_AS))) +#define ULIMIT_M_IS_VMEM +#endif + +#if defined(ULIMIT_M_IS_VMEM) && defined(RLIMIT_AS) && \ + (RLIMIT_VMEM == RLIMIT_AS) +#undef ULIMIT_M_IS_VMEM +#endif + +#if defined(ULIMIT_M_IS_RSS) && defined(ULIMIT_M_IS_VMEM) +# error nonsensical m ulimit +#endif + +#if defined(ULIMIT_V_IS_VMEM) && defined(ULIMIT_V_IS_AS) +# error nonsensical v ulimit +#endif + +struct limits { + /* limit resource */ + int resource; + /* multiply by to get rlim_{cur,max} values */ + unsigned int factor; + /* getopts char */ + char optchar; + /* limit name */ + char name[1]; +}; + +#define RLIMITS_DEFNS +#define FN(lname,lid,lfac,lopt) \ + static const struct { \ + int resource; \ + unsigned int factor; \ + char optchar; \ + char name[sizeof(lname)]; \ + } rlimits_ ## lid = { \ + lid, lfac, lopt, lname \ + }; +#include "rlimits.gen" + +static void print_ulimit(const struct limits *, int); +static int set_ulimit(const struct limits *, const char *, int); + +static const struct limits * const rlimits[] = { +#define RLIMITS_ITEMS +#include "rlimits.gen" +}; + +static const char rlimits_opts[] = +#define RLIMITS_OPTCS +#include "rlimits.gen" + ; + +int +c_ulimit(const char **wp) +{ + size_t i = 0; + int how = SOFT | HARD, optc, what = 'f'; + bool all = false; + + while ((optc = ksh_getopt(wp, &builtin_opt, rlimits_opts)) != -1) + switch (optc) { + case 'H': + how = HARD; + break; + case 'S': + how = SOFT; + break; + case 'a': + all = true; + break; + case '?': + bi_errorf("usage: ulimit [-%s] [value]", rlimits_opts); + return (1); + default: + what = optc; + } + + while (i < NELEM(rlimits)) { + if (rlimits[i]->optchar == what) + goto found; + ++i; + } + internal_warningf("ulimit: %c", what); + return (1); + found: + if (wp[builtin_opt.optind]) { + if (all || wp[builtin_opt.optind + 1]) { + bi_errorf(Ttoo_many_args); + return (1); + } + return (set_ulimit(rlimits[i], wp[builtin_opt.optind], how)); + } + if (!all) + print_ulimit(rlimits[i], how); + else for (i = 0; i < NELEM(rlimits); ++i) { + shprintf("-%c: %-20s ", rlimits[i]->optchar, rlimits[i]->name); + print_ulimit(rlimits[i], how); + } + return (0); +} + +static int +set_ulimit(const struct limits *l, const char *v, int how) +{ + rlim_t val = (rlim_t)0; + struct rlimit limit; + + if (strcmp(v, "unlimited") == 0) + val = (rlim_t)RLIM_INFINITY; + else { + mksh_uari_t rval; + + if (!evaluate(v, (mksh_ari_t *)&rval, KSH_RETURN_ERROR, false)) + return (1); + /* + * Avoid problems caused by typos that evaluate misses due + * to evaluating unset parameters to 0... + * If this causes problems, will have to add parameter to + * evaluate() to control if unset params are 0 or an error. + */ + if (!rval && !ctype(v[0], C_DIGIT)) { + bi_errorf("invalid %s limit: %s", l->name, v); + return (1); + } + val = (rlim_t)((rlim_t)rval * l->factor); + } + + if (getrlimit(l->resource, &limit) < 0) { +#ifndef MKSH_SMALL + bi_errorf("limit %s could not be read, contact the mksh developers: %s", + l->name, cstrerror(errno)); +#endif + /* some can't be read */ + limit.rlim_cur = RLIM_INFINITY; + limit.rlim_max = RLIM_INFINITY; + } + if (how & SOFT) + limit.rlim_cur = val; + if (how & HARD) + limit.rlim_max = val; + if (!setrlimit(l->resource, &limit)) + return (0); + if (errno == EPERM) + bi_errorf("%s exceeds allowable %s limit", v, l->name); + else + bi_errorf("bad %s limit: %s", l->name, cstrerror(errno)); + return (1); +} + +static void +print_ulimit(const struct limits *l, int how) +{ + rlim_t val = (rlim_t)0; + struct rlimit limit; + + if (getrlimit(l->resource, &limit)) { + shf_puts("unknown\n", shl_stdout); + return; + } + if (how & SOFT) + val = limit.rlim_cur; + else if (how & HARD) + val = limit.rlim_max; + if (val == (rlim_t)RLIM_INFINITY) + shf_puts("unlimited\n", shl_stdout); + else + shprintf("%lu\n", (unsigned long)(val / l->factor)); +} +#endif + +int +c_rename(const char **wp) +{ + int rv = 1; + + /* skip argv[0] */ + ++wp; + if (wp[0] && !strcmp(wp[0], "--")) + /* skip "--" (options separator) */ + ++wp; + + /* check for exactly two arguments */ + if (wp[0] == NULL /* first argument */ || + wp[1] == NULL /* second argument */ || + wp[2] != NULL /* no further args please */) + bi_errorf(Tsynerr); + else if ((rv = rename(wp[0], wp[1])) != 0) { + rv = errno; + bi_errorf(Tf_sD_s, "failed", cstrerror(rv)); + } + + return (rv); +} + +int +c_realpath(const char **wp) +{ + int rv = 1; + char *buf; + + /* skip argv[0] */ + ++wp; + if (wp[0] && !strcmp(wp[0], "--")) + /* skip "--" (options separator) */ + ++wp; + + /* check for exactly one argument */ + if (wp[0] == NULL || wp[1] != NULL) + bi_errorf(Tsynerr); + else if ((buf = do_realpath(wp[0])) == NULL) { + rv = errno; + bi_errorf(Tf_sD_s, wp[0], cstrerror(rv)); + if ((unsigned int)rv > 255) + rv = 255; + } else { + shprintf(Tf_sN, buf); + afree(buf, ATEMP); + rv = 0; + } + + return (rv); +} + +int +c_cat(const char **wp) +{ + int fd = STDIN_FILENO, rv; + ssize_t n, w; + const char *fn = ""; + char *buf, *cp; + bool opipe; +#define MKSH_CAT_BUFSIZ 4096 + + /* parse options: POSIX demands we support "-u" as no-op */ + while ((rv = ksh_getopt(wp, &builtin_opt, "u")) != -1) { + switch (rv) { + case 'u': + /* we already operate unbuffered */ + break; + default: + bi_errorf(Tsynerr); + return (1); + } + } + wp += builtin_opt.optind; + rv = 0; + + if ((buf = malloc_osfunc(MKSH_CAT_BUFSIZ)) == NULL) { + bi_errorf(Toomem, (size_t)MKSH_CAT_BUFSIZ); + return (1); + } + + /* catch SIGPIPE */ + opipe = block_pipe(); + + do { + if (*wp) { + fn = *wp++; + if (ksh_isdash(fn)) + fd = STDIN_FILENO; + else if ((fd = binopen2(fn, O_RDONLY)) < 0) { + bi_errorf(Tf_sD_s, fn, cstrerror(errno)); + rv = 1; + continue; + } + } + while (/* CONSTCOND */ 1) { + if ((n = blocking_read(fd, (cp = buf), + MKSH_CAT_BUFSIZ)) == -1) { + if (errno == EINTR) { + if (opipe) + restore_pipe(); + /* give the user a chance to ^C out */ + intrcheck(); + /* interrupted, try again */ + opipe = block_pipe(); + continue; + } + /* an error occured during reading */ + bi_errorf(Tf_sD_s, fn, cstrerror(errno)); + rv = 1; + break; + } else if (n == 0) + /* end of file reached */ + break; + while (n) { + if (intrsig) + goto has_intrsig; + if ((w = write(STDOUT_FILENO, cp, n)) != -1) { + n -= w; + cp += w; + continue; + } + if (errno == EINTR) { + has_intrsig: + if (opipe) + restore_pipe(); + /* give the user a chance to ^C out */ + intrcheck(); + /* interrupted, try again */ + opipe = block_pipe(); + continue; + } + if (errno == EPIPE) { + /* fake receiving signal */ + rv = ksh_sigmask(SIGPIPE); + } else { + /* an error occured during writing */ + bi_errorf(Tf_sD_s, "", + cstrerror(errno)); + rv = 1; + } + if (fd != STDIN_FILENO) + close(fd); + goto out; + } + } + if (fd != STDIN_FILENO) + close(fd); + } while (*wp); + + out: + if (opipe) + restore_pipe(); + free_osfunc(buf); + return (rv); +} + +#if HAVE_SELECT +int +c_sleep(const char **wp) +{ + struct timeval tv; + int rv = 1; + + /* skip argv[0] */ + ++wp; + if (wp[0] && !strcmp(wp[0], "--")) + /* skip "--" (options separator) */ + ++wp; + + if (!wp[0] || wp[1]) + bi_errorf(Tsynerr); + else if (parse_usec(wp[0], &tv)) + bi_errorf(Tf_sD_s_qs, Tsynerr, cstrerror(errno), wp[0]); + else { +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask, bmask; + + /* block a number of signals from interrupting us, though */ + (void)sigemptyset(&bmask); + (void)sigaddset(&bmask, SIGPIPE); + (void)sigaddset(&bmask, SIGCHLD); +#ifdef SIGWINCH + (void)sigaddset(&bmask, SIGWINCH); +#endif +#ifdef SIGINFO + (void)sigaddset(&bmask, SIGINFO); +#endif +#ifdef SIGUSR1 + (void)sigaddset(&bmask, SIGUSR1); +#endif +#ifdef SIGUSR2 + (void)sigaddset(&bmask, SIGUSR2); +#endif + sigprocmask(SIG_BLOCK, &bmask, &omask); +#endif + if (select(1, NULL, NULL, NULL, &tv) == 0 || errno == EINTR) + /* + * strictly speaking only for SIGALRM, but the + * execution may be interrupted by other signals + */ + rv = 0; + else + bi_errorf(Tf_sD_s, Tselect, cstrerror(errno)); +#ifndef MKSH_NOPROSPECTOFWORK + /* this will re-schedule signal delivery */ + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + } + return (rv); +} +#endif + +#if !defined(MKSH_UNEMPLOYED) && HAVE_GETSID +static int +c_suspend(const char **wp) +{ + if (wp[1] != NULL) { + bi_errorf(Ttoo_many_args); + return (1); + } + if (Flag(FLOGIN)) { + /* Can't suspend an orphaned process group. */ + if (getpgid(kshppid) == getpgid(0) || + getsid(kshppid) != getsid(0)) { + bi_errorf("can't suspend a login shell"); + return (1); + } + } + j_suspend(); + return (0); +} +#endif diff --git a/histrap.c b/histrap.c new file mode 100644 index 0000000..6b9396e --- /dev/null +++ b/histrap.c @@ -0,0 +1,1625 @@ +/* $OpenBSD: history.c,v 1.41 2015/09/01 13:12:31 tedu Exp $ */ +/* $OpenBSD: trap.c,v 1.23 2010/05/19 17:36:08 jasper Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2014, 2015, 2016, 2017 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" +#if HAVE_SYS_FILE_H +#include +#endif + +__RCSID("$MirOS: src/bin/mksh/histrap.c,v 1.166 2017/08/07 23:25:09 tg Exp $"); + +Trap sigtraps[ksh_NSIG + 1]; +static struct sigaction Sigact_ign; + +#if HAVE_PERSISTENT_HISTORY +static int histload(Source *, unsigned char *, size_t); +static int writehistline(int, int, const char *); +static void writehistfile(int, const char *); +#endif + +static int hist_execute(char *, Area *); +static char **hist_get(const char *, bool, bool); +static char **hist_get_oldest(void); + +static bool hstarted; /* set after hist_init() called */ +static Source *hist_source; + +#if HAVE_PERSISTENT_HISTORY +/*XXX imake style */ +#if defined(__linux) +#define caddr_cast(x) ((void *)(x)) +#else +#define caddr_cast(x) ((caddr_t)(x)) +#endif + +/* several OEs do not have these constants */ +#ifndef MAP_FAILED +#define MAP_FAILED caddr_cast(-1) +#endif + +/* some OEs need the default mapping type specified */ +#ifndef MAP_FILE +#define MAP_FILE 0 +#endif + +/* current history file: name, fd, size */ +static char *hname; +static int histfd = -1; +static off_t histfsize; +#endif + +/* HISTSIZE default: size of saved history, persistent or standard */ +#ifdef MKSH_SMALL +#define MKSH_DEFHISTSIZE 255 +#else +#define MKSH_DEFHISTSIZE 2047 +#endif +/* maximum considered size of persistent history file */ +#define MKSH_MAXHISTFSIZE ((off_t)1048576 * 96) + +/* hidden option */ +#define HIST_DISCARD 5 + +int +c_fc(const char **wp) +{ + struct shf *shf; + struct temp *tf; + bool gflag = false, lflag = false, nflag = false, rflag = false, + sflag = false; + int optc; + const char *p, *first = NULL, *last = NULL; + char **hfirst, **hlast, **hp, *editor = NULL; + + if (!Flag(FTALKING_I)) { + bi_errorf("history %ss not available", Tfunction); + return (1); + } + + while ((optc = ksh_getopt(wp, &builtin_opt, + "e:glnrs0,1,2,3,4,5,6,7,8,9,")) != -1) + switch (optc) { + + case 'e': + p = builtin_opt.optarg; + if (ksh_isdash(p)) + sflag = true; + else { + size_t len = strlen(p); + + /* almost certainly not overflowing */ + editor = alloc(len + 4, ATEMP); + memcpy(editor, p, len); + memcpy(editor + len, Tspdollaru, 4); + } + break; + + /* non-AT&T ksh */ + case 'g': + gflag = true; + break; + + case 'l': + lflag = true; + break; + + case 'n': + nflag = true; + break; + + case 'r': + rflag = true; + break; + + /* POSIX version of -e - */ + case 's': + sflag = true; + break; + + /* kludge city - accept -num as -- -num (kind of) */ + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + p = shf_smprintf("-%c%s", + optc, builtin_opt.optarg); + if (!first) + first = p; + else if (!last) + last = p; + else { + bi_errorf(Ttoo_many_args); + return (1); + } + break; + + case '?': + return (1); + } + wp += builtin_opt.optind; + + /* Substitute and execute command */ + if (sflag) { + char *pat = NULL, *rep = NULL, *line; + + if (editor || lflag || nflag || rflag) { + bi_errorf("can't use -e, -l, -n, -r with -s (-e -)"); + return (1); + } + + /* Check for pattern replacement argument */ + if (*wp && **wp && (p = cstrchr(*wp + 1, '='))) { + strdupx(pat, *wp, ATEMP); + rep = pat + (p - *wp); + *rep++ = '\0'; + wp++; + } + /* Check for search prefix */ + if (!first && (first = *wp)) + wp++; + if (last || *wp) { + bi_errorf(Ttoo_many_args); + return (1); + } + + hp = first ? hist_get(first, false, false) : + hist_get_newest(false); + if (!hp) + return (1); + /* hist_replace */ + if (!pat) + strdupx(line, *hp, ATEMP); + else { + char *s, *s1; + size_t len, pat_len, rep_len; + XString xs; + char *xp; + bool any_subst = false; + + pat_len = strlen(pat); + rep_len = strlen(rep); + Xinit(xs, xp, 128, ATEMP); + for (s = *hp; (s1 = strstr(s, pat)) && + (!any_subst || gflag); s = s1 + pat_len) { + any_subst = true; + len = s1 - s; + XcheckN(xs, xp, len + rep_len); + /*; first part */ + memcpy(xp, s, len); + xp += len; + /* replacement */ + memcpy(xp, rep, rep_len); + xp += rep_len; + } + if (!any_subst) { + bi_errorf(Tbadsubst); + return (1); + } + len = strlen(s) + 1; + XcheckN(xs, xp, len); + memcpy(xp, s, len); + xp += len; + line = Xclose(xs, xp); + } + return (hist_execute(line, ATEMP)); + } + + if (editor && (lflag || nflag)) { + bi_errorf("can't use -l, -n with -e"); + return (1); + } + + if (!first && (first = *wp)) + wp++; + if (!last && (last = *wp)) + wp++; + if (*wp) { + bi_errorf(Ttoo_many_args); + return (1); + } + if (!first) { + hfirst = lflag ? hist_get("-16", true, true) : + hist_get_newest(false); + if (!hfirst) + return (1); + /* can't fail if hfirst didn't fail */ + hlast = hist_get_newest(false); + } else { + /* + * POSIX says not an error if first/last out of bounds + * when range is specified; AT&T ksh and pdksh allow out + * of bounds for -l as well. + */ + hfirst = hist_get(first, tobool(lflag || last), lflag); + if (!hfirst) + return (1); + hlast = last ? hist_get(last, true, lflag) : + (lflag ? hist_get_newest(false) : hfirst); + if (!hlast) + return (1); + } + if (hfirst > hlast) { + char **temp; + + temp = hfirst; hfirst = hlast; hlast = temp; + /* POSIX */ + rflag = !rflag; + } + + /* List history */ + if (lflag) { + char *s, *t; + + for (hp = rflag ? hlast : hfirst; + hp >= hfirst && hp <= hlast; hp += rflag ? -1 : 1) { + if (!nflag) + shf_fprintf(shl_stdout, Tf_lu, + (unsigned long)hist_source->line - + (unsigned long)(histptr - hp)); + shf_putc('\t', shl_stdout); + /* print multi-line commands correctly */ + s = *hp; + while ((t = strchr(s, '\n'))) { + *t = '\0'; + shf_fprintf(shl_stdout, "%s\n\t", s); + *t++ = '\n'; + s = t; + } + shf_fprintf(shl_stdout, Tf_sN, s); + } + shf_flush(shl_stdout); + return (0); + } + + /* Run editor on selected lines, then run resulting commands */ + + tf = maketemp(ATEMP, TT_HIST_EDIT, &e->temps); + if (!(shf = tf->shf)) { + bi_errorf(Tf_temp, Tcreate, tf->tffn, cstrerror(errno)); + return (1); + } + for (hp = rflag ? hlast : hfirst; + hp >= hfirst && hp <= hlast; hp += rflag ? -1 : 1) + shf_fprintf(shf, Tf_sN, *hp); + if (shf_close(shf) == -1) { + bi_errorf(Tf_temp, Twrite, tf->tffn, cstrerror(errno)); + return (1); + } + + /* Ignore setstr errors here (arbitrary) */ + setstr(local("_", false), tf->tffn, KSH_RETURN_ERROR); + + if ((optc = command(editor ? editor : TFCEDIT_dollaru, 0))) + return (optc); + + { + struct stat statb; + XString xs; + char *xp; + ssize_t n; + + if (!(shf = shf_open(tf->tffn, O_RDONLY, 0, 0))) { + bi_errorf(Tf_temp, Topen, tf->tffn, cstrerror(errno)); + return (1); + } + + if (stat(tf->tffn, &statb) < 0) + n = 128; + else if ((off_t)statb.st_size > MKSH_MAXHISTFSIZE) { + bi_errorf(Tf_toolarge, Thistory, + Tfile, (unsigned long)statb.st_size); + goto errout; + } else + n = (size_t)statb.st_size + 1; + Xinit(xs, xp, n, hist_source->areap); + while ((n = shf_read(xp, Xnleft(xs, xp), shf)) > 0) { + xp += n; + if (Xnleft(xs, xp) <= 0) + XcheckN(xs, xp, Xlength(xs, xp)); + } + if (n < 0) { + bi_errorf(Tf_temp, Tread, tf->tffn, + cstrerror(shf_errno(shf))); + errout: + shf_close(shf); + return (1); + } + shf_close(shf); + *xp = '\0'; + strip_nuls(Xstring(xs, xp), Xlength(xs, xp)); + return (hist_execute(Xstring(xs, xp), hist_source->areap)); + } +} + +/* save cmd in history, execute cmd (cmd gets afree’d) */ +static int +hist_execute(char *cmd, Area *areap) +{ + static int last_line = -1; + + /* Back up over last histsave */ + if (histptr >= history && last_line != hist_source->line) { + hist_source->line--; + afree(*histptr, APERM); + histptr--; + last_line = hist_source->line; + } + + histsave(&hist_source->line, cmd, HIST_STORE, true); + /* now *histptr == cmd without all trailing newlines */ + afree(cmd, areap); + cmd = *histptr; + /* pdksh says POSIX doesn’t say this is done, testsuite needs it */ + shellf(Tf_sN, cmd); + + /*- + * Commands are executed here instead of pushing them onto the + * input 'cause POSIX says the redirection and variable assignments + * in + * X=y fc -e - 42 2> /dev/null + * are to effect the repeated commands environment. + */ + return (command(cmd, 0)); +} + +/* + * get pointer to history given pattern + * pattern is a number or string + */ +static char ** +hist_get(const char *str, bool approx, bool allow_cur) +{ + char **hp = NULL; + int n; + + if (getn(str, &n)) { + hp = histptr + (n < 0 ? n : (n - hist_source->line)); + if ((size_t)hp < (size_t)history) { + if (approx) + hp = hist_get_oldest(); + else { + bi_errorf(Tf_sD_s, str, Tnot_in_history); + hp = NULL; + } + } else if ((size_t)hp > (size_t)histptr) { + if (approx) + hp = hist_get_newest(allow_cur); + else { + bi_errorf(Tf_sD_s, str, Tnot_in_history); + hp = NULL; + } + } else if (!allow_cur && hp == histptr) { + bi_errorf(Tf_sD_s, str, "invalid range"); + hp = NULL; + } + } else { + bool anchored = *str == '?' ? (++str, false) : true; + + /* the -1 is to avoid the current fc command */ + if ((n = findhist(histptr - history - 1, 0, str, anchored)) < 0) + bi_errorf(Tf_sD_s, str, Tnot_in_history); + else + hp = &history[n]; + } + return (hp); +} + +/* Return a pointer to the newest command in the history */ +char ** +hist_get_newest(bool allow_cur) +{ + if (histptr < history || (!allow_cur && histptr == history)) { + bi_errorf("no history (yet)"); + return (NULL); + } + return (allow_cur ? histptr : histptr - 1); +} + +/* Return a pointer to the oldest command in the history */ +static char ** +hist_get_oldest(void) +{ + if (histptr <= history) { + bi_errorf("no history (yet)"); + return (NULL); + } + return (history); +} + +#if !defined(MKSH_NO_CMDLINE_EDITING) && !MKSH_S_NOVI +/* current position in history[] */ +static char **current; + +/* + * Return the current position. + */ +char ** +histpos(void) +{ + return (current); +} + +int +histnum(int n) +{ + int last = histptr - history; + + if (n < 0 || n >= last) { + current = histptr; + return (last); + } else { + current = &history[n]; + return (n); + } +} +#endif + +/* + * This will become unnecessary if hist_get is modified to allow + * searching from positions other than the end, and in either + * direction. + */ +int +findhist(int start, int fwd, const char *str, bool anchored) +{ + char **hp; + int maxhist = histptr - history; + int incr = fwd ? 1 : -1; + size_t len = strlen(str); + + if (start < 0 || start >= maxhist) + start = maxhist; + + hp = &history[start]; + for (; hp >= history && hp <= histptr; hp += incr) + if ((anchored && strncmp(*hp, str, len) == 0) || + (!anchored && strstr(*hp, str))) + return (hp - history); + + return (-1); +} + +/* + * set history; this means reallocating the dataspace + */ +void +sethistsize(mksh_ari_t n) +{ + if (n > 0 && n != histsize) { + int cursize = histptr - history; + + /* save most recent history */ + if (n < cursize) { + memmove(history, histptr - n + 1, n * sizeof(char *)); + cursize = n - 1; + } + + history = aresize2(history, n, sizeof(char *), APERM); + + histsize = n; + histptr = history + cursize; + } +} + +#if HAVE_PERSISTENT_HISTORY +/* + * set history file; this can mean reloading/resetting/starting + * history file maintenance + */ +void +sethistfile(const char *name) +{ + /* if not started then nothing to do */ + if (hstarted == false) + return; + + /* if the name is the same as the name we have */ + if (hname && name && !strcmp(hname, name)) + return; + + /* + * it's a new name - possibly + */ + if (histfd != -1) { + /* yes the file is open */ + (void)close(histfd); + histfd = -1; + histfsize = 0; + afree(hname, APERM); + hname = NULL; + /* let's reset the history */ + histsave(NULL, NULL, HIST_DISCARD, true); + histptr = history - 1; + hist_source->line = 0; + } + + if (name) + hist_init(hist_source); +} +#endif + +/* + * initialise the history vector + */ +void +init_histvec(void) +{ + if (history == (char **)NULL) { + histsize = MKSH_DEFHISTSIZE; + history = alloc2(histsize, sizeof(char *), APERM); + histptr = history - 1; + } +} + + +/* + * It turns out that there is a lot of ghastly hackery here + */ + +#if !defined(MKSH_SMALL) && HAVE_PERSISTENT_HISTORY +/* do not save command in history but possibly sync */ +bool +histsync(void) +{ + bool changed = false; + + /* called by histsave(), may not HIST_DISCARD, caller should flush */ + + if (histfd != -1) { + int lno = hist_source->line; + + hist_source->line++; + writehistfile(0, NULL); + hist_source->line--; + + if (lno != hist_source->line) + changed = true; + } + + return (changed); +} +#endif + +/* + * save command in history + */ +void +histsave(int *lnp, const char *cmd, int svmode, bool ignoredups) +{ + static char *enqueued = NULL; + char **hp, *c; + const char *ccp; + + if (svmode == HIST_DISCARD) { + afree(enqueued, APERM); + enqueued = NULL; + return; + } + + if (svmode == HIST_APPEND) { + if (!enqueued) + svmode = HIST_STORE; + } else if (enqueued) { + c = enqueued; + enqueued = NULL; + --*lnp; + histsave(lnp, c, HIST_STORE, true); + afree(c, APERM); + } + + if (svmode == HIST_FLUSH) + return; + + ccp = strnul(cmd); + while (ccp > cmd && ccp[-1] == '\n') + --ccp; + strndupx(c, cmd, ccp - cmd, APERM); + + if (svmode != HIST_APPEND) { + if (ignoredups && + histptr >= history && + !strcmp(c, *histptr) +#if !defined(MKSH_SMALL) && HAVE_PERSISTENT_HISTORY + && !histsync() +#endif + ) { + afree(c, APERM); + return; + } + ++*lnp; + } + +#if HAVE_PERSISTENT_HISTORY + if (svmode == HIST_STORE && histfd != -1) + writehistfile(*lnp, c); +#endif + + if (svmode == HIST_QUEUE || svmode == HIST_APPEND) { + size_t nenq, ncmd; + + if (!enqueued) { + if (*c) + enqueued = c; + else + afree(c, APERM); + return; + } + + nenq = strlen(enqueued); + ncmd = strlen(c); + enqueued = aresize(enqueued, nenq + 1 + ncmd + 1, APERM); + enqueued[nenq] = '\n'; + memcpy(enqueued + nenq + 1, c, ncmd + 1); + afree(c, APERM); + return; + } + + hp = histptr; + + if (++hp >= history + histsize) { + /* remove oldest command */ + afree(*history, APERM); + for (hp = history; hp < history + histsize - 1; hp++) + hp[0] = hp[1]; + } + *hp = c; + histptr = hp; +} + +/* + * Write history data to a file nominated by HISTFILE; + * if HISTFILE is unset then history still happens, but + * the data is not written to a file. All copies of ksh + * looking at the file will maintain the same history. + * This is ksh behaviour. + * + * This stuff uses mmap() + * + * This stuff is so totally broken it must eventually be + * redesigned, without mmap, better checks, support for + * larger files, etc. and handle partially corrupted files + */ + +/*- + * Open a history file + * Format is: + * Bytes 1, 2: + * HMAGIC - just to check that we are dealing with the correct object + * Then follows a number of stored commands + * Each command is + * + */ +#define HMAGIC1 0xAB +#define HMAGIC2 0xCD +#define COMMAND 0xFF + +#if HAVE_PERSISTENT_HISTORY +static const unsigned char sprinkle[2] = { HMAGIC1, HMAGIC2 }; + +static int +hist_persist_back(int srcfd) +{ + off_t tot, mis; + ssize_t n, w; + char *buf, *cp; + int rv = 0; +#define MKSH_HS_BUFSIZ 4096 + + if ((tot = lseek(srcfd, (off_t)0, SEEK_END)) < 0 || + lseek(srcfd, (off_t)0, SEEK_SET) < 0 || + lseek(histfd, (off_t)0, SEEK_SET) < 0) + return (1); + + if ((buf = malloc_osfunc(MKSH_HS_BUFSIZ)) == NULL) + return (1); + + mis = tot; + while (mis > 0) { + if ((n = blocking_read(srcfd, (cp = buf), + MKSH_HS_BUFSIZ)) == -1) { + if (errno == EINTR) { + intrcheck(); + continue; + } + goto copy_error; + } + mis -= n; + while (n) { + if (intrsig) + goto has_intrsig; + if ((w = write(histfd, cp, n)) != -1) { + n -= w; + cp += w; + continue; + } + if (errno == EINTR) { + has_intrsig: + intrcheck(); + continue; + } + goto copy_error; + } + } + if (ftruncate(histfd, tot)) { + copy_error: + rv = 1; + } + free_osfunc(buf); + return (rv); +} + +static void +hist_persist_init(void) +{ + unsigned char *base; + int lines, fd; + enum { hist_init_first, hist_init_retry, hist_use_it } hs; + + if (((hname = str_val(global("HISTFILE"))) == NULL) || !*hname) { + hname = NULL; + return; + } + strdupx(hname, hname, APERM); + hs = hist_init_first; + + retry: + /* we have a file and are interactive */ + if ((fd = binopen3(hname, O_RDWR | O_CREAT | O_APPEND, 0600)) < 0) + return; + if ((histfd = savefd(fd)) < 0) + return; + if (histfd != fd) + close(fd); + + mksh_lockfd(histfd); + + histfsize = lseek(histfd, (off_t)0, SEEK_END); + if (histfsize > MKSH_MAXHISTFSIZE) { + /* we ignore too large files but still append to them */ + goto hist_init_tail; + } else if (histfsize > 2) { + /* we have some data, check its validity */ + base = (void *)mmap(NULL, (size_t)histfsize, PROT_READ, + MAP_FILE | MAP_PRIVATE, histfd, (off_t)0); + if (base == (unsigned char *)MAP_FAILED) + goto hist_init_fail; + if (base[0] != HMAGIC1 || base[1] != HMAGIC2) { + munmap(caddr_cast(base), (size_t)histfsize); + goto hist_init_fail; + } + /* load _all_ data */ + lines = histload(hist_source, base + 2, (size_t)histfsize - 2); + munmap(caddr_cast(base), (size_t)histfsize); + /* check if the file needs to be truncated */ + if (lines > histsize && histptr >= history) { + /* you're fucked up with the current code, trust me */ + char *nhname, **hp; + struct stat sb; + + /* create temporary file */ + nhname = shf_smprintf("%s.%d", hname, (int)procpid); + if ((fd = binopen3(nhname, O_RDWR | O_CREAT | O_TRUNC | + O_EXCL, 0600)) < 0) { + /* just don't truncate then, meh. */ + hs = hist_use_it; + goto hist_trunc_dont; + } + if (fstat(histfd, &sb) >= 0 && + chown(nhname, sb.st_uid, sb.st_gid)) { + /* abort the truncation then, meh. */ + goto hist_trunc_abort; + } + /* we definitively want some magic in that file */ + if (write(fd, sprinkle, 2) != 2) + goto hist_trunc_abort; + /* and of course the entries */ + hp = history; + while (hp < histptr) { + if (!writehistline(fd, + hist_source->line - (histptr - hp), *hp)) + goto hist_trunc_abort; + ++hp; + } + /* now transfer back */ + if (!hist_persist_back(fd)) { + /* success! */ + hs = hist_use_it; + } + hist_trunc_abort: + /* remove temporary file */ + close(fd); + fd = -1; + unlink(nhname); + /* use whatever is in the file now */ + hist_trunc_dont: + afree(nhname, ATEMP); + if (hs == hist_use_it) + goto hist_trunc_done; + goto hist_init_fail; + } + } else if (histfsize != 0) { + /* negative or too small... */ + hist_init_fail: + /* ... or mmap failed or illegal */ + hist_finish(); + /* nuke the bogus file then retry, at most once */ + if (!unlink(hname) && hs != hist_init_retry) { + hs = hist_init_retry; + goto retry; + } + if (hs != hist_init_retry) + bi_errorf(Tf_cant_ss_s, + "unlink HISTFILE", hname, cstrerror(errno)); + histfsize = 0; + return; + } else { + /* size 0, add magic to the history file */ + if (write(histfd, sprinkle, 2) != 2) { + hist_finish(); + return; + } + } + hist_trunc_done: + histfsize = lseek(histfd, (off_t)0, SEEK_END); + hist_init_tail: + mksh_unlkfd(histfd); +} +#endif + +void +hist_init(Source *s) +{ + histsave(NULL, NULL, HIST_DISCARD, true); + + if (Flag(FTALKING) == 0) + return; + + hstarted = true; + hist_source = s; + +#if HAVE_PERSISTENT_HISTORY + hist_persist_init(); +#endif +} + +#if HAVE_PERSISTENT_HISTORY +/* + * load the history structure from the stored data + */ +static int +histload(Source *s, unsigned char *base, size_t bytes) +{ + int lno = 0, lines = 0; + unsigned char *cp; + + histload_loop: + /* !bytes check as some systems (older FreeBSDs) have buggy memchr */ + if (!bytes || (cp = memchr(base, COMMAND, bytes)) == NULL) + return (lines); + /* advance base pointer past COMMAND byte */ + bytes -= ++cp - base; + base = cp; + /* if there is no full string left, don't bother with the rest */ + if (bytes < 5 || (cp = memchr(base + 4, '\0', bytes - 4)) == NULL) + return (lines); + /* load the stored line number */ + lno = ((base[0] & 0xFF) << 24) | ((base[1] & 0xFF) << 16) | + ((base[2] & 0xFF) << 8) | (base[3] & 0xFF); + /* store away the found line (@base[4]) */ + ++lines; + if (histptr >= history && lno - 1 != s->line) { + /* a replacement? */ + char **hp; + + if (lno >= s->line - (histptr - history) && lno <= s->line) { + hp = &histptr[lno - s->line]; + afree(*hp, APERM); + strdupx(*hp, (char *)(base + 4), APERM); + } + } else { + s->line = lno--; + histsave(&lno, (char *)(base + 4), HIST_NOTE, false); + } + /* advance base pointer past NUL */ + bytes -= ++cp - base; + base = cp; + /* repeat until no more */ + goto histload_loop; +} + +/* + * write a command to the end of the history file + * + * This *MAY* seem easy but it's also necessary to check + * that the history file has not changed in size. + * If it has - then some other shell has written to it and + * we should (re)read those commands to update our history + */ +static void +writehistfile(int lno, const char *cmd) +{ + off_t sizenow; + size_t bytes; + unsigned char *base, *news; + + mksh_lockfd(histfd); + sizenow = lseek(histfd, (off_t)0, SEEK_END); + if (sizenow < histfsize) { + /* the file has shrunk; trust it just appending the new data */ + /* well, for now, anyway… since mksh strdups all into memory */ + /* we can use a nicer approach some time later… */ + ; + } else if ( + /* ignore changes when the file is too large */ + sizenow <= MKSH_MAXHISTFSIZE + && + /* the size has changed, we need to do read updates */ + sizenow > histfsize + ) { + /* both sizenow and histfsize are <= MKSH_MAXHISTFSIZE */ + bytes = (size_t)(sizenow - histfsize); + base = (void *)mmap(NULL, (size_t)sizenow, PROT_READ, + MAP_FILE | MAP_PRIVATE, histfd, (off_t)0); + if (base == (unsigned char *)MAP_FAILED) + goto bad; + news = base + (size_t)histfsize; + if (*news == COMMAND) { + hist_source->line--; + histload(hist_source, news, bytes); + hist_source->line++; + lno = hist_source->line; + } else + bytes = 0; + munmap(caddr_cast(base), (size_t)sizenow); + if (!bytes) + goto bad; + } + if (cmd && !writehistline(histfd, lno, cmd)) { + bad: + hist_finish(); + return; + } + histfsize = lseek(histfd, (off_t)0, SEEK_END); + mksh_unlkfd(histfd); +} + +static int +writehistline(int fd, int lno, const char *cmd) +{ + ssize_t n; + unsigned char hdr[5]; + + hdr[0] = COMMAND; + hdr[1] = (lno >> 24) & 0xFF; + hdr[2] = (lno >> 16) & 0xFF; + hdr[3] = (lno >> 8) & 0xFF; + hdr[4] = lno & 0xFF; + n = strlen(cmd) + 1; + return (write(fd, hdr, 5) == 5 && write(fd, cmd, n) == n); +} + +void +hist_finish(void) +{ + if (histfd >= 0) { + mksh_unlkfd(histfd); + (void)close(histfd); + } + histfd = -1; +} +#endif + + +#if !HAVE_SYS_SIGNAME +static const struct mksh_sigpair { + const char * const name; + int nr; +} mksh_sigpairs[] = { +#include "signames.inc" + { NULL, 0 } +}; +#endif + +#if HAVE_SYS_SIGLIST +#if !HAVE_SYS_SIGLIST_DECL +extern const char * const sys_siglist[]; +#endif +#endif + +void +inittraps(void) +{ + int i; + const char *cs; +#if !HAVE_SYS_SIGNAME + const struct mksh_sigpair *pair; +#endif + + trap_exstat = -1; + + /* populate sigtraps based on sys_signame and sys_siglist */ + for (i = 1; i < ksh_NSIG; i++) { + sigtraps[i].signal = i; +#if HAVE_SYS_SIGNAME + cs = sys_signame[i]; +#else + pair = mksh_sigpairs; + while ((pair->nr != i) && (pair->name != NULL)) + ++pair; + cs = pair->name; +#endif + if ((cs == NULL) || + (cs[0] == '\0')) + sigtraps[i].name = null; + else { + char *s; + + /* this is not optimal, what about SIGSIG1? */ + if (ksh_eq(cs[0], 'S', 's') && + ksh_eq(cs[1], 'I', 'i') && + ksh_eq(cs[2], 'G', 'g') && + cs[3] != '\0') { + /* skip leading "SIG" */ + cs += 3; + } + strdupx(s, cs, APERM); + sigtraps[i].name = s; + while ((*s = ksh_toupper(*s))) + ++s; + /* check for reserved names */ + if (!strcmp(sigtraps[i].name, "EXIT") || + !strcmp(sigtraps[i].name, "ERR")) { +#ifndef MKSH_SMALL + internal_warningf(Tinvname, sigtraps[i].name, + "signal"); +#endif + sigtraps[i].name = null; + } + } + if (sigtraps[i].name == null) + sigtraps[i].name = shf_smprintf(Tf_d, i); +#if HAVE_SYS_SIGLIST + sigtraps[i].mess = sys_siglist[i]; +#elif HAVE_STRSIGNAL + sigtraps[i].mess = strsignal(i); +#else + sigtraps[i].mess = NULL; +#endif + if ((sigtraps[i].mess == NULL) || + (sigtraps[i].mess[0] == '\0')) + sigtraps[i].mess = shf_smprintf(Tf_sd, + "Signal", i); + } + sigtraps[ksh_SIGEXIT].signal = ksh_SIGEXIT; + sigtraps[ksh_SIGEXIT].name = "EXIT"; + sigtraps[ksh_SIGEXIT].mess = "Exit trap"; + sigtraps[ksh_SIGERR].signal = ksh_SIGERR; + sigtraps[ksh_SIGERR].name = "ERR"; + sigtraps[ksh_SIGERR].mess = "Error handler"; + + (void)sigemptyset(&Sigact_ign.sa_mask); + Sigact_ign.sa_flags = 0; /* interruptible */ + Sigact_ign.sa_handler = SIG_IGN; + + sigtraps[SIGINT].flags |= TF_DFL_INTR | TF_TTY_INTR; + sigtraps[SIGQUIT].flags |= TF_DFL_INTR | TF_TTY_INTR; + /* SIGTERM is not fatal for interactive */ + sigtraps[SIGTERM].flags |= TF_DFL_INTR; + sigtraps[SIGHUP].flags |= TF_FATAL; + sigtraps[SIGCHLD].flags |= TF_SHELL_USES; + + /* these are always caught so we can clean up any temporary files. */ + setsig(&sigtraps[SIGINT], trapsig, SS_RESTORE_ORIG); + setsig(&sigtraps[SIGQUIT], trapsig, SS_RESTORE_ORIG); + setsig(&sigtraps[SIGTERM], trapsig, SS_RESTORE_ORIG); + setsig(&sigtraps[SIGHUP], trapsig, SS_RESTORE_ORIG); +} + +static void alarm_catcher(int sig); + +void +alarm_init(void) +{ + sigtraps[SIGALRM].flags |= TF_SHELL_USES; + setsig(&sigtraps[SIGALRM], alarm_catcher, + SS_RESTORE_ORIG|SS_FORCE|SS_SHTRAP); +} + +/* ARGSUSED */ +static void +alarm_catcher(int sig MKSH_A_UNUSED) +{ + /* this runs inside interrupt context, with errno saved */ + + if (ksh_tmout_state == TMOUT_READING) { + int left = alarm(0); + + if (left == 0) { + ksh_tmout_state = TMOUT_LEAVING; + intrsig = 1; + } else + alarm(left); + } +} + +Trap * +gettrap(const char *cs, bool igncase, bool allsigs) +{ + int i; + Trap *p; + char *as; + + /* signal number (1..ksh_NSIG) or 0? */ + + if (ctype(*cs, C_DIGIT)) + return ((getn(cs, &i) && 0 <= i && i < ksh_NSIG) ? + (&sigtraps[i]) : NULL); + + /* do a lookup by name then */ + + /* this breaks SIGSIG1, but we do that above anyway */ + if (ksh_eq(cs[0], 'S', 's') && + ksh_eq(cs[1], 'I', 'i') && + ksh_eq(cs[2], 'G', 'g') && + cs[3] != '\0') { + /* skip leading "SIG" */ + cs += 3; + } + if (igncase) { + char *s; + + strdupx(as, cs, ATEMP); + cs = s = as; + while ((*s = ksh_toupper(*s))) + ++s; + } else + as = NULL; + + /* this is idiotic, we really want a hashtable here */ + + p = sigtraps; + i = ksh_NSIG + 1; + do { + if (!strcmp(p->name, cs)) + goto found; + ++p; + } while (--i); + goto notfound; + + found: + if (!allsigs) { + if (p->signal == ksh_SIGEXIT || p->signal == ksh_SIGERR) { + notfound: + p = NULL; + } + } + afree(as, ATEMP); + return (p); +} + +/* + * trap signal handler + */ +void +trapsig(int i) +{ + Trap *p = &sigtraps[i]; + int eno = errno; + + trap = p->set = 1; + if (p->flags & TF_DFL_INTR) + intrsig = 1; + if ((p->flags & TF_FATAL) && !p->trap) { + fatal_trap = 1; + intrsig = 1; + } + if (p->shtrap) + (*p->shtrap)(i); + errno = eno; +} + +/* + * called when we want to allow the user to ^C out of something - won't + * work if user has trapped SIGINT. + */ +void +intrcheck(void) +{ + if (intrsig) + runtraps(TF_DFL_INTR|TF_FATAL); +} + +/* + * called after EINTR to check if a signal with normally causes process + * termination has been received. + */ +int +fatal_trap_check(void) +{ + Trap *p = sigtraps; + int i = ksh_NSIG + 1; + + /* todo: should check if signal is fatal, not the TF_DFL_INTR flag */ + do { + if (p->set && (p->flags & (TF_DFL_INTR|TF_FATAL))) + /* return value is used as an exit code */ + return (ksh_sigmask(p->signal)); + ++p; + } while (--i); + return (0); +} + +/* + * Returns the signal number of any pending traps: ie, a signal which has + * occurred for which a trap has been set or for which the TF_DFL_INTR flag + * is set. + */ +int +trap_pending(void) +{ + Trap *p = sigtraps; + int i = ksh_NSIG + 1; + + do { + if (p->set && ((p->trap && p->trap[0]) || + ((p->flags & (TF_DFL_INTR|TF_FATAL)) && !p->trap))) + return (p->signal); + ++p; + } while (--i); + return (0); +} + +/* + * run any pending traps. If intr is set, only run traps that + * can interrupt commands. + */ +void +runtraps(int flag) +{ + Trap *p = sigtraps; + int i = ksh_NSIG + 1; + + if (ksh_tmout_state == TMOUT_LEAVING) { + ksh_tmout_state = TMOUT_EXECUTING; + warningf(false, "timed out waiting for input"); + unwind(LEXIT); + } else + /* + * XXX: this means the alarm will have no effect if a trap + * is caught after the alarm() was started...not good. + */ + ksh_tmout_state = TMOUT_EXECUTING; + if (!flag) + trap = 0; + if (flag & TF_DFL_INTR) + intrsig = 0; + if (flag & TF_FATAL) + fatal_trap = 0; + ++trap_nested; + do { + if (p->set && (!flag || + ((p->flags & flag) && p->trap == NULL))) + runtrap(p, false); + ++p; + } while (--i); + if (!--trap_nested) + runtrap(NULL, true); +} + +void +runtrap(Trap *p, bool is_last) +{ + int old_changed = 0, i; + char *trapstr; + + if (p == NULL) + /* just clean up, see runtraps() above */ + goto donetrap; + i = p->signal; + trapstr = p->trap; + p->set = 0; + if (trapstr == NULL) { + /* SIG_DFL */ + if (p->flags & (TF_FATAL | TF_DFL_INTR)) { + exstat = (int)(128U + (unsigned)i); + if ((unsigned)exstat > 255U) + exstat = 255; + } + /* e.g. SIGHUP */ + if (p->flags & TF_FATAL) + unwind(LLEAVE); + /* e.g. SIGINT, SIGQUIT, SIGTERM, etc. */ + if (p->flags & TF_DFL_INTR) + unwind(LINTR); + goto donetrap; + } + if (trapstr[0] == '\0') + /* SIG_IGN */ + goto donetrap; + if (i == ksh_SIGEXIT || i == ksh_SIGERR) { + /* avoid recursion on these */ + old_changed = p->flags & TF_CHANGED; + p->flags &= ~TF_CHANGED; + p->trap = NULL; + } + if (trap_exstat == -1) + trap_exstat = exstat & 0xFF; + /* + * Note: trapstr is fully parsed before anything is executed, thus + * no problem with afree(p->trap) in settrap() while still in use. + */ + command(trapstr, current_lineno); + if (i == ksh_SIGEXIT || i == ksh_SIGERR) { + if (p->flags & TF_CHANGED) + /* don't clear TF_CHANGED */ + afree(trapstr, APERM); + else + p->trap = trapstr; + p->flags |= old_changed; + } + + donetrap: + /* we're the last trap of a sequence executed */ + if (is_last && trap_exstat != -1) { + exstat = trap_exstat; + trap_exstat = -1; + } +} + +/* clear pending traps and reset user's trap handlers; used after fork(2) */ +void +cleartraps(void) +{ + Trap *p = sigtraps; + int i = ksh_NSIG + 1; + + trap = 0; + intrsig = 0; + fatal_trap = 0; + + do { + p->set = 0; + if ((p->flags & TF_USER_SET) && (p->trap && p->trap[0])) + settrap(p, NULL); + ++p; + } while (--i); +} + +/* restore signals just before an exec(2) */ +void +restoresigs(void) +{ + Trap *p = sigtraps; + int i = ksh_NSIG + 1; + + do { + if (p->flags & (TF_EXEC_IGN|TF_EXEC_DFL)) + setsig(p, (p->flags & TF_EXEC_IGN) ? SIG_IGN : SIG_DFL, + SS_RESTORE_CURR|SS_FORCE); + ++p; + } while (--i); +} + +void +settrap(Trap *p, const char *s) +{ + sig_t f; + + afree(p->trap, APERM); + /* handles s == NULL */ + strdupx(p->trap, s, APERM); + p->flags |= TF_CHANGED; + f = !s ? SIG_DFL : s[0] ? trapsig : SIG_IGN; + + p->flags |= TF_USER_SET; + if ((p->flags & (TF_DFL_INTR|TF_FATAL)) && f == SIG_DFL) + f = trapsig; + else if (p->flags & TF_SHELL_USES) { + if (!(p->flags & TF_ORIG_IGN) || Flag(FTALKING)) { + /* do what user wants at exec time */ + p->flags &= ~(TF_EXEC_IGN|TF_EXEC_DFL); + if (f == SIG_IGN) + p->flags |= TF_EXEC_IGN; + else + p->flags |= TF_EXEC_DFL; + } + + /* + * assumes handler already set to what shell wants it + * (normally trapsig, but could be j_sigchld() or SIG_IGN) + */ + return; + } + + /* todo: should we let user know signal is ignored? how? */ + setsig(p, f, SS_RESTORE_CURR|SS_USER); +} + +/* + * called by c_print() when writing to a co-process to ensure + * SIGPIPE won't kill shell (unless user catches it and exits) + */ +bool +block_pipe(void) +{ + bool restore_dfl = false; + Trap *p = &sigtraps[SIGPIPE]; + + if (!(p->flags & (TF_ORIG_IGN|TF_ORIG_DFL))) { + setsig(p, SIG_IGN, SS_RESTORE_CURR); + if (p->flags & TF_ORIG_DFL) + restore_dfl = true; + } else if (p->cursig == SIG_DFL) { + setsig(p, SIG_IGN, SS_RESTORE_CURR); + /* restore to SIG_DFL */ + restore_dfl = true; + } + return (restore_dfl); +} + +/* called by c_print() to undo whatever block_pipe() did */ +void +restore_pipe(void) +{ + setsig(&sigtraps[SIGPIPE], SIG_DFL, SS_RESTORE_CURR); +} + +/* + * Set action for a signal. Action may not be set if original + * action was SIG_IGN, depending on the value of flags and FTALKING. + */ +int +setsig(Trap *p, sig_t f, int flags) +{ + struct sigaction sigact; + + if (p->signal == ksh_SIGEXIT || p->signal == ksh_SIGERR) + return (1); + + memset(&sigact, 0, sizeof(sigact)); + + /* + * First time setting this signal? If so, get and note the current + * setting. + */ + if (!(p->flags & (TF_ORIG_IGN|TF_ORIG_DFL))) { + sigaction(p->signal, &Sigact_ign, &sigact); + p->flags |= sigact.sa_handler == SIG_IGN ? + TF_ORIG_IGN : TF_ORIG_DFL; + p->cursig = SIG_IGN; + } + + /*- + * Generally, an ignored signal stays ignored, except if + * - the user of an interactive shell wants to change it + * - the shell wants for force a change + */ + if ((p->flags & TF_ORIG_IGN) && !(flags & SS_FORCE) && + (!(flags & SS_USER) || !Flag(FTALKING))) + return (0); + + setexecsig(p, flags & SS_RESTORE_MASK); + + /* + * This is here 'cause there should be a way of clearing + * shtraps, but don't know if this is a sane way of doing + * it. At the moment, all users of shtrap are lifetime + * users (SIGALRM, SIGCHLD, SIGWINCH). + */ + if (!(flags & SS_USER)) + p->shtrap = (sig_t)NULL; + if (flags & SS_SHTRAP) { + p->shtrap = f; + f = trapsig; + } + + if (p->cursig != f) { + p->cursig = f; + (void)sigemptyset(&sigact.sa_mask); + /* interruptible */ + sigact.sa_flags = 0; + sigact.sa_handler = f; + sigaction(p->signal, &sigact, NULL); + } + + return (1); +} + +/* control what signal is set to before an exec() */ +void +setexecsig(Trap *p, int restore) +{ + /* XXX debugging */ + if (!(p->flags & (TF_ORIG_IGN|TF_ORIG_DFL))) + internal_errorf("setexecsig: unset signal %d(%s)", + p->signal, p->name); + + /* restore original value for exec'd kids */ + p->flags &= ~(TF_EXEC_IGN|TF_EXEC_DFL); + switch (restore & SS_RESTORE_MASK) { + case SS_RESTORE_CURR: + /* leave things as they currently are */ + break; + case SS_RESTORE_ORIG: + p->flags |= p->flags & TF_ORIG_IGN ? TF_EXEC_IGN : TF_EXEC_DFL; + break; + case SS_RESTORE_DFL: + p->flags |= TF_EXEC_DFL; + break; + case SS_RESTORE_IGN: + p->flags |= TF_EXEC_IGN; + break; + } +} + +#if HAVE_PERSISTENT_HISTORY || defined(DF) +/* + * File descriptor locking and unlocking functions. + * Could use some error handling, but hey, this is only + * advisory locking anyway, will often not work over NFS, + * and you are SOL if this fails... + */ + +void +mksh_lockfd(int fd) +{ +#if defined(__OpenBSD__) + /* flock is not interrupted by signals */ + (void)flock(fd, LOCK_EX); +#elif HAVE_FLOCK + int rv; + + /* e.g. on Linux */ + do { + rv = flock(fd, LOCK_EX); + } while (rv == 1 && errno == EINTR); +#elif HAVE_LOCK_FCNTL + int rv; + struct flock lks; + + memset(&lks, 0, sizeof(lks)); + lks.l_type = F_WRLCK; + do { + rv = fcntl(fd, F_SETLKW, &lks); + } while (rv == 1 && errno == EINTR); +#endif +} + +/* designed to not define mksh_unlkfd if none triggered */ +#if HAVE_FLOCK +void +mksh_unlkfd(int fd) +{ + (void)flock(fd, LOCK_UN); +} +#elif HAVE_LOCK_FCNTL +void +mksh_unlkfd(int fd) +{ + struct flock lks; + + memset(&lks, 0, sizeof(lks)); + lks.l_type = F_UNLCK; + (void)fcntl(fd, F_SETLKW, &lks); +} +#endif +#endif diff --git a/jehanne.c b/jehanne.c new file mode 100644 index 0000000..ba0eb0f --- /dev/null +++ b/jehanne.c @@ -0,0 +1,36 @@ +/*- + * Copyright (c) 2017 + * Giacomo Tesio + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + *- + * Initialisation code for the Jehanne operating system (a Plan 9 de- + * rivative, using GCC) + */ + +static const char __rcsid[] __attribute__((__used__)) = + "$MirOS: src/bin/mksh/jehanne.c,v 1.1 2017/12/22 16:30:00 tg Exp $"; + +#include +#include +#include + +void +__application_newlib_init(int argc, char *argv[]) +{ + rfork(RFFDG | RFREND | RFNOTEG); + libposix_emulate_SIGCHLD(); +} diff --git a/jobs.c b/jobs.c new file mode 100644 index 0000000..a0b9b79 --- /dev/null +++ b/jobs.c @@ -0,0 +1,1957 @@ +/* $OpenBSD: jobs.c,v 1.43 2015/09/10 22:48:58 nicm Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2011, + * 2012, 2013, 2014, 2015, 2016, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/jobs.c,v 1.125 2018/01/05 20:08:34 tg Exp $"); + +#if HAVE_KILLPG +#define mksh_killpg killpg +#else +/* cross fingers and hope kill is killpg-endowed */ +#define mksh_killpg(p,s) kill(-(p), (s)) +#endif + +/* Order important! */ +#define PRUNNING 0 +#define PEXITED 1 +#define PSIGNALLED 2 +#define PSTOPPED 3 + +typedef struct proc Proc; +/* to take alignment into consideration */ +struct proc_dummy { + Proc *next; + pid_t pid; + int state; + int status; + char command[128]; +}; +/* real structure */ +struct proc { + /* next process in pipeline (if any) */ + Proc *next; + /* process id of this Unix process in the job */ + pid_t pid; + /* one of the four P… above */ + int state; + /* wait status */ + int status; + /* process command string from vistree */ + char command[256 - (ALLOC_OVERHEAD + + offsetof(struct proc_dummy, command[0]))]; +}; + +/* Notify/print flag - j_print() argument */ +#define JP_SHORT 1 /* print signals processes were killed by */ +#define JP_MEDIUM 2 /* print [job-num] -/+ command */ +#define JP_LONG 3 /* print [job-num] -/+ pid command */ +#define JP_PGRP 4 /* print pgrp */ + +/* put_job() flags */ +#define PJ_ON_FRONT 0 /* at very front */ +#define PJ_PAST_STOPPED 1 /* just past any stopped jobs */ + +/* Job.flags values */ +#define JF_STARTED 0x001 /* set when all processes in job are started */ +#define JF_WAITING 0x002 /* set if j_waitj() is waiting on job */ +#define JF_W_ASYNCNOTIFY 0x004 /* set if waiting and async notification ok */ +#define JF_XXCOM 0x008 /* set for $(command) jobs */ +#define JF_FG 0x010 /* running in foreground (also has tty pgrp) */ +#define JF_SAVEDTTY 0x020 /* j->ttystat is valid */ +#define JF_CHANGED 0x040 /* process has changed state */ +#define JF_KNOWN 0x080 /* $! referenced */ +#define JF_ZOMBIE 0x100 /* known, unwaited process */ +#define JF_REMOVE 0x200 /* flagged for removal (j_jobs()/j_noityf()) */ +#define JF_USETTYMODE 0x400 /* tty mode saved if process exits normally */ +#define JF_SAVEDTTYPGRP 0x800 /* j->saved_ttypgrp is valid */ + +typedef struct job Job; +struct job { + Job *next; /* next job in list */ + Proc *proc_list; /* process list */ + Proc *last_proc; /* last process in list */ + struct timeval systime; /* system time used by job */ + struct timeval usrtime; /* user time used by job */ + pid_t pgrp; /* process group of job */ + pid_t ppid; /* pid of process that forked job */ + int job; /* job number: %n */ + int flags; /* see JF_* */ + volatile int state; /* job state */ + int status; /* exit status of last process */ + int age; /* number of jobs started */ + Coproc_id coproc_id; /* 0 or id of coprocess output pipe */ +#ifndef MKSH_UNEMPLOYED + mksh_ttyst ttystat; /* saved tty state for stopped jobs */ + pid_t saved_ttypgrp; /* saved tty process group for stopped jobs */ +#endif +}; + +/* Flags for j_waitj() */ +#define JW_NONE 0x00 +#define JW_INTERRUPT 0x01 /* ^C will stop the wait */ +#define JW_ASYNCNOTIFY 0x02 /* asynchronous notification during wait ok */ +#define JW_STOPPEDWAIT 0x04 /* wait even if job stopped */ +#define JW_PIPEST 0x08 /* want PIPESTATUS */ + +/* Error codes for j_lookup() */ +#define JL_NOSUCH 0 /* no such job */ +#define JL_AMBIG 1 /* %foo or %?foo is ambiguous */ +#define JL_INVALID 2 /* non-pid, non-% job id */ + +static const char * const lookup_msgs[] = { + "no such job", + "ambiguous", + "argument must be %job or process id" +}; + +static Job *job_list; /* job list */ +static Job *last_job; +static Job *async_job; +static pid_t async_pid; + +static int nzombie; /* # of zombies owned by this process */ +static int njobs; /* # of jobs started */ + +#ifndef CHILD_MAX +#define CHILD_MAX 25 +#endif + +#ifndef MKSH_NOPROSPECTOFWORK +/* held_sigchld is set if sigchld occurs before a job is completely started */ +static volatile sig_atomic_t held_sigchld; +#endif + +#ifndef MKSH_UNEMPLOYED +static struct shf *shl_j; +static bool ttypgrp_ok; /* set if can use tty pgrps */ +static pid_t restore_ttypgrp = -1; +static int const tt_sigs[] = { SIGTSTP, SIGTTIN, SIGTTOU }; +#endif + +static void j_set_async(Job *); +static void j_startjob(Job *); +static int j_waitj(Job *, int, const char *); +static void j_sigchld(int); +static void j_print(Job *, int, struct shf *); +static Job *j_lookup(const char *, int *); +static Job *new_job(void); +static Proc *new_proc(void); +static void check_job(Job *); +static void put_job(Job *, int); +static void remove_job(Job *, const char *); +static int kill_job(Job *, int); + +static void tty_init_talking(void); +static void tty_init_state(void); + +/* initialise job control */ +void +j_init(void) +{ +#ifndef MKSH_UNEMPLOYED + bool mflagset = Flag(FMONITOR) != 127; + + Flag(FMONITOR) = 0; +#endif + +#ifndef MKSH_NOPROSPECTOFWORK + (void)sigemptyset(&sm_default); + sigprocmask(SIG_SETMASK, &sm_default, NULL); + + (void)sigemptyset(&sm_sigchld); + (void)sigaddset(&sm_sigchld, SIGCHLD); + + setsig(&sigtraps[SIGCHLD], j_sigchld, + SS_RESTORE_ORIG|SS_FORCE|SS_SHTRAP); +#else + /* Make sure SIGCHLD isn't ignored - can do odd things under SYSV */ + setsig(&sigtraps[SIGCHLD], SIG_DFL, SS_RESTORE_ORIG|SS_FORCE); +#endif + +#ifndef MKSH_UNEMPLOYED + if (!mflagset && Flag(FTALKING)) + Flag(FMONITOR) = 1; + + /* + * shl_j is used to do asynchronous notification (used in + * an interrupt handler, so need a distinct shf) + */ + shl_j = shf_fdopen(2, SHF_WR, NULL); + + if (Flag(FMONITOR) || Flag(FTALKING)) { + int i; + + /* + * the TF_SHELL_USES test is a kludge that lets us know if + * if the signals have been changed by the shell. + */ + for (i = NELEM(tt_sigs); --i >= 0; ) { + sigtraps[tt_sigs[i]].flags |= TF_SHELL_USES; + /* j_change() sets this to SS_RESTORE_DFL if FMONITOR */ + setsig(&sigtraps[tt_sigs[i]], SIG_IGN, + SS_RESTORE_IGN|SS_FORCE); + } + } + + /* j_change() calls tty_init_talking() and tty_init_state() */ + if (Flag(FMONITOR)) + j_change(); + else +#endif + if (Flag(FTALKING)) { + tty_init_talking(); + tty_init_state(); + } +} + +static int +proc_errorlevel(Proc *p) +{ + switch (p->state) { + case PEXITED: + return ((WEXITSTATUS(p->status)) & 255); + case PSIGNALLED: + return (ksh_sigmask(WTERMSIG(p->status))); + default: + return (0); + } +} + +#if !defined(MKSH_UNEMPLOYED) && HAVE_GETSID +/* suspend the shell */ +void +j_suspend(void) +{ + struct sigaction sa, osa; + + /* Restore tty and pgrp. */ + if (ttypgrp_ok) { + if (tty_hasstate) + mksh_tcset(tty_fd, &tty_state); + if (restore_ttypgrp >= 0) { + if (tcsetpgrp(tty_fd, restore_ttypgrp) < 0) { + warningf(false, Tf_ssfaileds, + Tj_suspend, "tcsetpgrp", cstrerror(errno)); + } else if (setpgid(0, restore_ttypgrp) < 0) { + warningf(false, Tf_ssfaileds, + Tj_suspend, "setpgid", cstrerror(errno)); + } + } + } + + /* Suspend the shell. */ + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_handler = SIG_DFL; + sigaction(SIGTSTP, &sa, &osa); + kill(0, SIGTSTP); + + /* Back from suspend, reset signals, pgrp and tty. */ + sigaction(SIGTSTP, &osa, NULL); + if (ttypgrp_ok) { + if (restore_ttypgrp >= 0) { + if (setpgid(0, kshpid) < 0) { + warningf(false, Tf_ssfaileds, + Tj_suspend, "setpgid", cstrerror(errno)); + ttypgrp_ok = false; + } else if (tcsetpgrp(tty_fd, kshpid) < 0) { + warningf(false, Tf_ssfaileds, + Tj_suspend, "tcsetpgrp", cstrerror(errno)); + ttypgrp_ok = false; + } + } + tty_init_state(); + } +} +#endif + +/* job cleanup before shell exit */ +void +j_exit(void) +{ + /* kill stopped, and possibly running, jobs */ + Job *j; + bool killed = false; + + for (j = job_list; j != NULL; j = j->next) { + if (j->ppid == procpid && + (j->state == PSTOPPED || + (j->state == PRUNNING && + ((j->flags & JF_FG) || + (Flag(FLOGIN) && !Flag(FNOHUP) && procpid == kshpid))))) { + killed = true; + if (j->pgrp == 0) + kill_job(j, SIGHUP); + else + mksh_killpg(j->pgrp, SIGHUP); +#ifndef MKSH_UNEMPLOYED + if (j->state == PSTOPPED) { + if (j->pgrp == 0) + kill_job(j, SIGCONT); + else + mksh_killpg(j->pgrp, SIGCONT); + } +#endif + } + } + if (killed) + sleep(1); + j_notify(); + +#ifndef MKSH_UNEMPLOYED + if (kshpid == procpid && restore_ttypgrp >= 0) { + /* + * Need to restore the tty pgrp to what it was when the + * shell started up, so that the process that started us + * will be able to access the tty when we are done. + * Also need to restore our process group in case we are + * about to do an exec so that both our parent and the + * process we are to become will be able to access the tty. + */ + tcsetpgrp(tty_fd, restore_ttypgrp); + setpgid(0, restore_ttypgrp); + } + if (Flag(FMONITOR)) { + Flag(FMONITOR) = 0; + j_change(); + } +#endif +} + +#ifndef MKSH_UNEMPLOYED +/* turn job control on or off according to Flag(FMONITOR) */ +void +j_change(void) +{ + int i; + + if (Flag(FMONITOR)) { + bool use_tty = Flag(FTALKING); + + /* don't call mksh_tcget until we own the tty process group */ + if (use_tty) + tty_init_talking(); + + /* no controlling tty, no SIGT* */ + if ((ttypgrp_ok = (use_tty && tty_fd >= 0 && tty_devtty))) { + setsig(&sigtraps[SIGTTIN], SIG_DFL, + SS_RESTORE_ORIG|SS_FORCE); + /* wait to be given tty (POSIX.1, B.2, job control) */ + while (/* CONSTCOND */ 1) { + pid_t ttypgrp; + + if ((ttypgrp = tcgetpgrp(tty_fd)) < 0) { + warningf(false, Tf_ssfaileds, + "j_init", "tcgetpgrp", + cstrerror(errno)); + ttypgrp_ok = false; + break; + } + if (ttypgrp == kshpgrp) + break; + kill(0, SIGTTIN); + } + } + for (i = NELEM(tt_sigs); --i >= 0; ) + setsig(&sigtraps[tt_sigs[i]], SIG_IGN, + SS_RESTORE_DFL|SS_FORCE); + if (ttypgrp_ok && kshpgrp != kshpid) { + if (setpgid(0, kshpid) < 0) { + warningf(false, Tf_ssfaileds, + "j_init", "setpgid", cstrerror(errno)); + ttypgrp_ok = false; + } else { + if (tcsetpgrp(tty_fd, kshpid) < 0) { + warningf(false, Tf_ssfaileds, + "j_init", "tcsetpgrp", + cstrerror(errno)); + ttypgrp_ok = false; + } else + restore_ttypgrp = kshpgrp; + kshpgrp = kshpid; + } + } +#ifndef MKSH_DISABLE_TTY_WARNING + if (use_tty && !ttypgrp_ok) + warningf(false, Tf_sD_s, "warning", + "won't have full job control"); +#endif + } else { + ttypgrp_ok = false; + if (Flag(FTALKING)) + for (i = NELEM(tt_sigs); --i >= 0; ) + setsig(&sigtraps[tt_sigs[i]], SIG_IGN, + SS_RESTORE_IGN|SS_FORCE); + else + for (i = NELEM(tt_sigs); --i >= 0; ) { + if (sigtraps[tt_sigs[i]].flags & + (TF_ORIG_IGN | TF_ORIG_DFL)) + setsig(&sigtraps[tt_sigs[i]], + (sigtraps[tt_sigs[i]].flags & TF_ORIG_IGN) ? + SIG_IGN : SIG_DFL, + SS_RESTORE_ORIG|SS_FORCE); + } + } + tty_init_state(); +} +#endif + +#if HAVE_NICE +/* run nice(3) and ignore the result */ +static void +ksh_nice(int ness) +{ +#if defined(__USE_FORTIFY_LEVEL) && (__USE_FORTIFY_LEVEL > 0) + int eno; + + errno = 0; + /* this is gonna annoy users; complain to your distro, people! */ + if (nice(ness) == -1 && (eno = errno) != 0) + warningf(false, Tf_sD_s, "bgnice", cstrerror(eno)); +#else + (void)nice(ness); +#endif +} +#endif + +/* execute tree in child subprocess */ +int +exchild(struct op *t, int flags, + volatile int *xerrok, + /* used if XPCLOSE or XCCLOSE */ + int close_fd) +{ + /* for pipelines */ + static Proc *last_proc; + + int rv = 0, forksleep, jwflags = JW_NONE; +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; +#endif + Proc *p; + Job *j; + pid_t cldpid; + + if (flags & XPIPEST) { + flags &= ~XPIPEST; + jwflags |= JW_PIPEST; + } + + if (flags & XEXEC) + /* + * Clear XFORK|XPCLOSE|XCCLOSE|XCOPROC|XPIPEO|XPIPEI|XXCOM|XBGND + * (also done in another execute() below) + */ + return (execute(t, flags & (XEXEC | XERROK), xerrok)); + +#ifndef MKSH_NOPROSPECTOFWORK + /* no SIGCHLDs while messing with job and process lists */ + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + p = new_proc(); + p->next = NULL; + p->state = PRUNNING; + p->status = 0; + p->pid = 0; + + /* link process into jobs list */ + if (flags & XPIPEI) { + /* continuing with a pipe */ + if (!last_job) + internal_errorf("exchild: XPIPEI and no last_job - pid %d", + (int)procpid); + j = last_job; + if (last_proc) + last_proc->next = p; + last_proc = p; + } else { + /* fills in j->job */ + j = new_job(); + /* + * we don't consider XXCOMs foreground since they don't get + * tty process group and we don't save or restore tty modes. + */ + j->flags = (flags & XXCOM) ? JF_XXCOM : + ((flags & XBGND) ? 0 : (JF_FG|JF_USETTYMODE)); + timerclear(&j->usrtime); + timerclear(&j->systime); + j->state = PRUNNING; + j->pgrp = 0; + j->ppid = procpid; + j->age = ++njobs; + j->proc_list = p; + j->coproc_id = 0; + last_job = j; + last_proc = p; + put_job(j, PJ_PAST_STOPPED); + } + + vistree(p->command, sizeof(p->command), t); + + /* create child process */ + forksleep = 1; + while ((cldpid = fork()) < 0 && errno == EAGAIN && forksleep < 32) { + if (intrsig) + /* allow user to ^C out... */ + break; + sleep(forksleep); + forksleep <<= 1; + } + /* ensure $RANDOM changes between parent and child */ + rndset((unsigned long)cldpid); + /* fork failed? */ + if (cldpid < 0) { + kill_job(j, SIGKILL); + remove_job(j, "fork failed"); +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + errorf("can't fork - try again"); + } + p->pid = cldpid ? cldpid : (procpid = getpid()); + +#ifndef MKSH_UNEMPLOYED + /* job control set up */ + if (Flag(FMONITOR) && !(flags&XXCOM)) { + bool dotty = false; + + if (j->pgrp == 0) { + /* First process */ + j->pgrp = p->pid; + dotty = true; + } + + /* + * set pgrp in both parent and child to deal with race + * condition + */ + setpgid(p->pid, j->pgrp); + if (ttypgrp_ok && dotty && !(flags & XBGND)) + tcsetpgrp(tty_fd, j->pgrp); + } +#endif + + /* used to close pipe input fd */ + if (close_fd >= 0 && (((flags & XPCLOSE) && cldpid) || + ((flags & XCCLOSE) && !cldpid))) + close(close_fd); + if (!cldpid) { + /* child */ + + /* Do this before restoring signal */ + if (flags & XCOPROC) + coproc_cleanup(false); + cleanup_parents_env(); +#ifndef MKSH_UNEMPLOYED + /* + * If FMONITOR or FTALKING is set, these signals are ignored, + * if neither FMONITOR nor FTALKING are set, the signals have + * their inherited values. + */ + if (Flag(FMONITOR) && !(flags & XXCOM)) { + for (forksleep = NELEM(tt_sigs); --forksleep >= 0; ) + setsig(&sigtraps[tt_sigs[forksleep]], SIG_DFL, + SS_RESTORE_DFL|SS_FORCE); + } +#endif +#if HAVE_NICE + if (Flag(FBGNICE) && (flags & XBGND)) + ksh_nice(4); +#endif + if ((flags & XBGND) +#ifndef MKSH_UNEMPLOYED + && !Flag(FMONITOR) +#endif + ) { + setsig(&sigtraps[SIGINT], SIG_IGN, + SS_RESTORE_IGN|SS_FORCE); + setsig(&sigtraps[SIGQUIT], SIG_IGN, + SS_RESTORE_IGN|SS_FORCE); + if ((!(flags & (XPIPEI | XCOPROC))) && + ((forksleep = open("/dev/null", 0)) > 0)) { + (void)ksh_dup2(forksleep, 0, true); + close(forksleep); + } + } + /* in case of $(jobs) command */ + remove_job(j, "child"); +#ifndef MKSH_NOPROSPECTOFWORK + /* remove_job needs SIGCHLD blocked still */ + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + nzombie = 0; +#ifndef MKSH_UNEMPLOYED + ttypgrp_ok = false; + Flag(FMONITOR) = 0; +#endif + Flag(FTALKING) = 0; + cleartraps(); + /* no return */ + execute(t, (flags & XERROK) | XEXEC, NULL); +#ifndef MKSH_SMALL + if (t->type == TPIPE) + unwind(LLEAVE); + internal_warningf("%s: execute() returned", "exchild"); + fptreef(shl_out, 8, "%s: tried to execute {\n\t%T\n}\n", + "exchild", t); + shf_flush(shl_out); +#endif + unwind(LLEAVE); + /* NOTREACHED */ + } + + /* shell (parent) stuff */ + if (!(flags & XPIPEO)) { + /* last process in a job */ + j_startjob(j); + if (flags & XCOPROC) { + j->coproc_id = coproc.id; + /* n jobs using co-process output */ + coproc.njobs++; + /* j using co-process input */ + coproc.job = (void *)j; + } + if (flags & XBGND) { + j_set_async(j); + if (Flag(FTALKING)) { + shf_fprintf(shl_out, "[%d]", j->job); + for (p = j->proc_list; p; p = p->next) + shf_fprintf(shl_out, Tf__d, + (int)p->pid); + shf_putchar('\n', shl_out); + shf_flush(shl_out); + } + } else + rv = j_waitj(j, jwflags, "jw:last proc"); + } + +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + + return (rv); +} + +/* start the last job: only used for $(command) jobs */ +void +startlast(void) +{ +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + /* no need to report error - waitlast() will do it */ + if (last_job) { + /* ensure it isn't removed by check_job() */ + last_job->flags |= JF_WAITING; + j_startjob(last_job); + } +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif +} + +/* wait for last job: only used for $(command) jobs */ +int +waitlast(void) +{ + int rv; + Job *j; +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + j = last_job; + if (!j || !(j->flags & JF_STARTED)) { + if (!j) + warningf(true, Tf_sD_s, "waitlast", "no last job"); + else + internal_warningf(Tf_sD_s, "waitlast", Tnot_started); +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + /* not so arbitrary, non-zero value */ + return (125); + } + + rv = j_waitj(j, JW_NONE, "waitlast"); + +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + + return (rv); +} + +/* wait for child, interruptable. */ +int +waitfor(const char *cp, int *sigp) +{ + int rv, ecode, flags = JW_INTERRUPT|JW_ASYNCNOTIFY; + Job *j; +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + *sigp = 0; + + if (cp == NULL) { + /* + * wait for an unspecified job - always returns 0, so + * don't have to worry about exited/signaled jobs + */ + for (j = job_list; j; j = j->next) + /* AT&T ksh will wait for stopped jobs - we don't */ + if (j->ppid == procpid && j->state == PRUNNING) + break; + if (!j) { +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + return (-1); + } + } else if ((j = j_lookup(cp, &ecode))) { + /* don't report normal job completion */ + flags &= ~JW_ASYNCNOTIFY; + if (j->ppid != procpid) { +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + return (-1); + } + } else { +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + if (ecode != JL_NOSUCH) + bi_errorf(Tf_sD_s, cp, lookup_msgs[ecode]); + return (-1); + } + + /* AT&T ksh will wait for stopped jobs - we don't */ + rv = j_waitj(j, flags, "jw:waitfor"); + +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + + if (rv < 0) + /* we were interrupted */ + *sigp = ksh_sigmask(-rv); + + return (rv); +} + +/* kill (built-in) a job */ +int +j_kill(const char *cp, int sig) +{ + Job *j; + int rv = 0, ecode; +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + if ((j = j_lookup(cp, &ecode)) == NULL) { +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + bi_errorf(Tf_sD_s, cp, lookup_msgs[ecode]); + return (1); + } + + if (j->pgrp == 0) { + /* started when !Flag(FMONITOR) */ + if (kill_job(j, sig) < 0) { + bi_errorf(Tf_sD_s, cp, cstrerror(errno)); + rv = 1; + } + } else { +#ifndef MKSH_UNEMPLOYED + if (j->state == PSTOPPED && (sig == SIGTERM || sig == SIGHUP)) + mksh_killpg(j->pgrp, SIGCONT); +#endif + if (mksh_killpg(j->pgrp, sig) < 0) { + bi_errorf(Tf_sD_s, cp, cstrerror(errno)); + rv = 1; + } + } + +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + + return (rv); +} + +#ifndef MKSH_UNEMPLOYED +/* fg and bg built-ins: called only if Flag(FMONITOR) set */ +int +j_resume(const char *cp, int bg) +{ + Job *j; + Proc *p; + int ecode, rv = 0; + bool running; + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); + + if ((j = j_lookup(cp, &ecode)) == NULL) { + sigprocmask(SIG_SETMASK, &omask, NULL); + bi_errorf(Tf_sD_s, cp, lookup_msgs[ecode]); + return (1); + } + + if (j->pgrp == 0) { + sigprocmask(SIG_SETMASK, &omask, NULL); + bi_errorf("job not job-controlled"); + return (1); + } + + if (bg) + shprintf("[%d] ", j->job); + + running = false; + for (p = j->proc_list; p != NULL; p = p->next) { + if (p->state == PSTOPPED) { + p->state = PRUNNING; + p->status = 0; + running = true; + } + shf_puts(p->command, shl_stdout); + if (p->next) + shf_puts("| ", shl_stdout); + } + shf_putc('\n', shl_stdout); + shf_flush(shl_stdout); + if (running) + j->state = PRUNNING; + + put_job(j, PJ_PAST_STOPPED); + if (bg) + j_set_async(j); + else { + /* attach tty to job */ + if (j->state == PRUNNING) { + if (ttypgrp_ok && (j->flags & JF_SAVEDTTY)) + mksh_tcset(tty_fd, &j->ttystat); + /* See comment in j_waitj regarding saved_ttypgrp. */ + if (ttypgrp_ok && + tcsetpgrp(tty_fd, (j->flags & JF_SAVEDTTYPGRP) ? + j->saved_ttypgrp : j->pgrp) < 0) { + rv = errno; + if (j->flags & JF_SAVEDTTY) + mksh_tcset(tty_fd, &tty_state); + sigprocmask(SIG_SETMASK, &omask, NULL); + bi_errorf(Tf_ldfailed, + "fg: 1st", "tcsetpgrp", tty_fd, + (long)((j->flags & JF_SAVEDTTYPGRP) ? + j->saved_ttypgrp : j->pgrp), + cstrerror(rv)); + return (1); + } + } + j->flags |= JF_FG; + j->flags &= ~JF_KNOWN; + if (j == async_job) + async_job = NULL; + } + + if (j->state == PRUNNING && mksh_killpg(j->pgrp, SIGCONT) < 0) { + int eno = errno; + + if (!bg) { + j->flags &= ~JF_FG; + if (ttypgrp_ok && (j->flags & JF_SAVEDTTY)) + mksh_tcset(tty_fd, &tty_state); + if (ttypgrp_ok && tcsetpgrp(tty_fd, kshpgrp) < 0) + warningf(true, Tf_ldfailed, + "fg: 2nd", "tcsetpgrp", tty_fd, + (long)kshpgrp, cstrerror(errno)); + } + sigprocmask(SIG_SETMASK, &omask, NULL); + bi_errorf(Tf_s_sD_s, "can't continue job", + cp, cstrerror(eno)); + return (1); + } + if (!bg) { + if (ttypgrp_ok) { + j->flags &= ~(JF_SAVEDTTY | JF_SAVEDTTYPGRP); + } + rv = j_waitj(j, JW_NONE, "jw:resume"); + } + sigprocmask(SIG_SETMASK, &omask, NULL); + return (rv); +} +#endif + +/* are there any running or stopped jobs ? */ +int +j_stopped_running(void) +{ + Job *j; + int which = 0; + + for (j = job_list; j != NULL; j = j->next) { +#ifndef MKSH_UNEMPLOYED + if (j->ppid == procpid && j->state == PSTOPPED) + which |= 1; +#endif + if (Flag(FLOGIN) && !Flag(FNOHUP) && procpid == kshpid && + j->ppid == procpid && j->state == PRUNNING) + which |= 2; + } + if (which) { + shellf("You have %s%s%s jobs\n", + which & 1 ? "stopped" : "", + which == 3 ? " and " : "", + which & 2 ? "running" : ""); + return (1); + } + + return (0); +} + + +/* list jobs for jobs built-in */ +int +j_jobs(const char *cp, int slp, + /* 0: short, 1: long, 2: pgrp */ + int nflag) +{ + Job *j, *tmp; + int how, zflag = 0; +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + if (nflag < 0) { + /* kludge: print zombies */ + nflag = 0; + zflag = 1; + } + if (cp) { + int ecode; + + if ((j = j_lookup(cp, &ecode)) == NULL) { +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + bi_errorf(Tf_sD_s, cp, lookup_msgs[ecode]); + return (1); + } + } else + j = job_list; + how = slp == 0 ? JP_MEDIUM : (slp == 1 ? JP_LONG : JP_PGRP); + for (; j; j = j->next) { + if ((!(j->flags & JF_ZOMBIE) || zflag) && + (!nflag || (j->flags & JF_CHANGED))) { + j_print(j, how, shl_stdout); + if (j->state == PEXITED || j->state == PSIGNALLED) + j->flags |= JF_REMOVE; + } + if (cp) + break; + } + /* Remove jobs after printing so there won't be multiple + or - jobs */ + for (j = job_list; j; j = tmp) { + tmp = j->next; + if (j->flags & JF_REMOVE) + remove_job(j, Tjobs); + } +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + return (0); +} + +/* list jobs for top-level notification */ +void +j_notify(void) +{ + Job *j, *tmp; +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + for (j = job_list; j; j = j->next) { +#ifndef MKSH_UNEMPLOYED + if (Flag(FMONITOR) && (j->flags & JF_CHANGED)) + j_print(j, JP_MEDIUM, shl_out); +#endif + /* + * Remove job after doing reports so there aren't + * multiple +/- jobs. + */ + if (j->state == PEXITED || j->state == PSIGNALLED) + j->flags |= JF_REMOVE; + } + for (j = job_list; j; j = tmp) { + tmp = j->next; + if (j->flags & JF_REMOVE) { + if (j == async_job || (j->flags & JF_KNOWN)) { + j->flags = (j->flags & ~JF_REMOVE) | JF_ZOMBIE; + j->job = -1; + nzombie++; + } else + remove_job(j, "notify"); + } + } + shf_flush(shl_out); +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif +} + +/* Return pid of last process in last asynchronous job */ +pid_t +j_async(void) +{ +#ifndef MKSH_NOPROSPECTOFWORK + sigset_t omask; + + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + + if (async_job) + async_job->flags |= JF_KNOWN; + +#ifndef MKSH_NOPROSPECTOFWORK + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + + return (async_pid); +} + +/* + * Make j the last async process + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static void +j_set_async(Job *j) +{ + Job *jl, *oldest; + + if (async_job && (async_job->flags & (JF_KNOWN|JF_ZOMBIE)) == JF_ZOMBIE) + remove_job(async_job, "async"); + if (!(j->flags & JF_STARTED)) { + internal_warningf(Tf_sD_s, "j_async", Tjob_not_started); + return; + } + async_job = j; + async_pid = j->last_proc->pid; + while (nzombie > CHILD_MAX) { + oldest = NULL; + for (jl = job_list; jl; jl = jl->next) + if (jl != async_job && (jl->flags & JF_ZOMBIE) && + (!oldest || jl->age < oldest->age)) + oldest = jl; + if (!oldest) { + /* XXX debugging */ + if (!(async_job->flags & JF_ZOMBIE) || nzombie != 1) { + internal_warningf("%s: bad nzombie (%d)", + "j_async", nzombie); + nzombie = 0; + } + break; + } + remove_job(oldest, "zombie"); + } +} + +/* + * Start a job: set STARTED, check for held signals and set j->last_proc + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static void +j_startjob(Job *j) +{ + Proc *p; + + j->flags |= JF_STARTED; + for (p = j->proc_list; p->next; p = p->next) + ; + j->last_proc = p; + +#ifndef MKSH_NOPROSPECTOFWORK + if (held_sigchld) { + held_sigchld = 0; + /* Don't call j_sigchld() as it may remove job... */ + kill(procpid, SIGCHLD); + } +#endif +} + +/* + * wait for job to complete or change state + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static int +j_waitj(Job *j, + /* see JW_* */ + int flags, + const char *where) +{ + Proc *p; + int rv; +#ifdef MKSH_NO_SIGSUSPEND + sigset_t omask; +#endif + + /* + * No auto-notify on the job we are waiting on. + */ + j->flags |= JF_WAITING; + if (flags & JW_ASYNCNOTIFY) + j->flags |= JF_W_ASYNCNOTIFY; + +#ifndef MKSH_UNEMPLOYED + if (!Flag(FMONITOR)) +#endif + flags |= JW_STOPPEDWAIT; + + while (j->state == PRUNNING || + ((flags & JW_STOPPEDWAIT) && j->state == PSTOPPED)) { +#ifndef MKSH_NOPROSPECTOFWORK +#ifdef MKSH_NO_SIGSUSPEND + sigprocmask(SIG_SETMASK, &sm_default, &omask); + pause(); + /* note that handlers may run here so they need to know */ + sigprocmask(SIG_SETMASK, &omask, NULL); +#else + sigsuspend(&sm_default); +#endif +#else + j_sigchld(SIGCHLD); +#endif + if (fatal_trap) { + int oldf = j->flags & (JF_WAITING|JF_W_ASYNCNOTIFY); + j->flags &= ~(JF_WAITING|JF_W_ASYNCNOTIFY); + runtraps(TF_FATAL); + /* not reached... */ + j->flags |= oldf; + } + if ((flags & JW_INTERRUPT) && (rv = trap_pending())) { + j->flags &= ~(JF_WAITING|JF_W_ASYNCNOTIFY); + return (-rv); + } + } + j->flags &= ~(JF_WAITING|JF_W_ASYNCNOTIFY); + + if (j->flags & JF_FG) { + j->flags &= ~JF_FG; +#ifndef MKSH_UNEMPLOYED + if (Flag(FMONITOR) && ttypgrp_ok && j->pgrp) { + /* + * Save the tty's current pgrp so it can be restored + * when the job is foregrounded. This is to + * deal with things like the GNU su which does + * a fork/exec instead of an exec (the fork means + * the execed shell gets a different pid from its + * pgrp, so naturally it sets its pgrp and gets hosed + * when it gets foregrounded by the parent shell which + * has restored the tty's pgrp to that of the su + * process). + */ + if (j->state == PSTOPPED && + (j->saved_ttypgrp = tcgetpgrp(tty_fd)) >= 0) + j->flags |= JF_SAVEDTTYPGRP; + if (tcsetpgrp(tty_fd, kshpgrp) < 0) + warningf(true, Tf_ldfailed, + "j_waitj:", "tcsetpgrp", tty_fd, + (long)kshpgrp, cstrerror(errno)); + if (j->state == PSTOPPED) { + j->flags |= JF_SAVEDTTY; + mksh_tcget(tty_fd, &j->ttystat); + } + } +#endif + if (tty_hasstate) { + /* + * Only restore tty settings if job was originally + * started in the foreground. Problems can be + * caused by things like 'more foobar &' which will + * typically get and save the shell's vi/emacs tty + * settings before setting up the tty for itself; + * when more exits, it restores the 'original' + * settings, and things go down hill from there... + */ + if (j->state == PEXITED && j->status == 0 && + (j->flags & JF_USETTYMODE)) { + mksh_tcget(tty_fd, &tty_state); + } else { + mksh_tcset(tty_fd, &tty_state); + /*- + * Don't use tty mode if job is stopped and + * later restarted and exits. Consider + * the sequence: + * vi foo (stopped) + * ... + * stty something + * ... + * fg (vi; ZZ) + * mode should be that of the stty, not what + * was before the vi started. + */ + if (j->state == PSTOPPED) + j->flags &= ~JF_USETTYMODE; + } + } +#ifndef MKSH_UNEMPLOYED + /* + * If it looks like user hit ^C to kill a job, pretend we got + * one too to break out of for loops, etc. (AT&T ksh does this + * even when not monitoring, but this doesn't make sense since + * a tty generated ^C goes to the whole process group) + */ + if (Flag(FMONITOR) && j->state == PSIGNALLED && + WIFSIGNALED(j->last_proc->status)) { + int termsig; + + if ((termsig = WTERMSIG(j->last_proc->status)) > 0 && + termsig < ksh_NSIG && + (sigtraps[termsig].flags & TF_TTY_INTR)) + trapsig(termsig); + } +#endif + } + + j_usrtime = j->usrtime; + j_systime = j->systime; + rv = j->status; + + if (!(p = j->proc_list)) { + ; /* nothing */ + } else if (flags & JW_PIPEST) { + uint32_t num = 0; + struct tbl *vp; + + unset(vp_pipest, 1); + vp = vp_pipest; + vp->flag = DEFINED | ISSET | INTEGER | RDONLY | ARRAY | INT_U; + goto got_array; + + while (p != NULL) { + { + struct tbl *vq; + + /* strlen(vp_pipest->name) == 10 */ + vq = alloc(offsetof(struct tbl, name[0]) + 11, + vp_pipest->areap); + memset(vq, 0, offsetof(struct tbl, name[0])); + memcpy(vq->name, vp_pipest->name, 11); + vp->u.array = vq; + vp = vq; + } + vp->areap = vp_pipest->areap; + vp->ua.index = ++num; + vp->flag = DEFINED | ISSET | INTEGER | RDONLY | + ARRAY | INT_U | AINDEX; + got_array: + vp->val.i = proc_errorlevel(p); + if (Flag(FPIPEFAIL) && vp->val.i) + rv = vp->val.i; + p = p->next; + } + } else if (Flag(FPIPEFAIL)) { + do { + const int i = proc_errorlevel(p); + + if (i) + rv = i; + } while ((p = p->next)); + } + + if (!(flags & JW_ASYNCNOTIFY) +#ifndef MKSH_UNEMPLOYED + && (!Flag(FMONITOR) || j->state != PSTOPPED) +#endif + ) { + j_print(j, JP_SHORT, shl_out); + shf_flush(shl_out); + } + if (j->state != PSTOPPED +#ifndef MKSH_UNEMPLOYED + && (!Flag(FMONITOR) || !(flags & JW_ASYNCNOTIFY)) +#endif + ) + remove_job(j, where); + + return (rv); +} + +/* + * SIGCHLD handler to reap children and update job states + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +/* ARGSUSED */ +static void +j_sigchld(int sig MKSH_A_UNUSED) +{ + int saved_errno = errno; + Job *j; + Proc *p = NULL; + pid_t pid; + int status; + struct rusage ru0, ru1; +#ifdef MKSH_NO_SIGSUSPEND + sigset_t omask; + + /* this handler can run while SIGCHLD is not blocked, so block it now */ + sigprocmask(SIG_BLOCK, &sm_sigchld, &omask); +#endif + +#ifndef MKSH_NOPROSPECTOFWORK + /* + * Don't wait for any processes if a job is partially started. + * This is so we don't do away with the process group leader + * before all the processes in a pipe line are started (so the + * setpgid() won't fail) + */ + for (j = job_list; j; j = j->next) + if (j->ppid == procpid && !(j->flags & JF_STARTED)) { + held_sigchld = 1; + goto j_sigchld_out; + } +#endif + + getrusage(RUSAGE_CHILDREN, &ru0); + do { +#ifndef MKSH_NOPROSPECTOFWORK + pid = waitpid(-1, &status, (WNOHANG | +#if defined(WCONTINUED) && defined(WIFCONTINUED) + WCONTINUED | +#endif + WUNTRACED)); +#else + pid = wait(&status); +#endif + + /* + * return if this would block (0) or no children + * or interrupted (-1) + */ + if (pid <= 0) + goto j_sigchld_out; + + getrusage(RUSAGE_CHILDREN, &ru1); + + /* find job and process structures for this pid */ + for (j = job_list; j != NULL; j = j->next) + for (p = j->proc_list; p != NULL; p = p->next) + if (p->pid == pid) + goto found; + found: + if (j == NULL) { + /* Can occur if process has kids, then execs shell + warningf(true, "bad process waited for (pid = %d)", + pid); + */ + ru0 = ru1; + continue; + } + + timeradd(&j->usrtime, &ru1.ru_utime, &j->usrtime); + timersub(&j->usrtime, &ru0.ru_utime, &j->usrtime); + timeradd(&j->systime, &ru1.ru_stime, &j->systime); + timersub(&j->systime, &ru0.ru_stime, &j->systime); + ru0 = ru1; + p->status = status; +#ifndef MKSH_UNEMPLOYED + if (WIFSTOPPED(status)) + p->state = PSTOPPED; + else +#if defined(WCONTINUED) && defined(WIFCONTINUED) + if (WIFCONTINUED(status)) { + p->state = j->state = PRUNNING; + /* skip check_job(), no-op in this case */ + continue; + } else +#endif +#endif + if (WIFSIGNALED(status)) + p->state = PSIGNALLED; + else + p->state = PEXITED; + + /* check to see if entire job is done */ + check_job(j); + } +#ifndef MKSH_NOPROSPECTOFWORK + while (/* CONSTCOND */ 1); +#else + while (/* CONSTCOND */ 0); +#endif + + j_sigchld_out: +#ifdef MKSH_NO_SIGSUSPEND + sigprocmask(SIG_SETMASK, &omask, NULL); +#endif + errno = saved_errno; +} + +/* + * Called only when a process in j has exited/stopped (ie, called only + * from j_sigchld()). If no processes are running, the job status + * and state are updated, asynchronous job notification is done and, + * if unneeded, the job is removed. + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static void +check_job(Job *j) +{ + int jstate; + Proc *p; + + /* XXX debugging (nasty - interrupt routine using shl_out) */ + if (!(j->flags & JF_STARTED)) { + internal_warningf("check_job: job started (flags 0x%X)", + (unsigned int)j->flags); + return; + } + + jstate = PRUNNING; + for (p=j->proc_list; p != NULL; p = p->next) { + if (p->state == PRUNNING) + /* some processes still running */ + return; + if (p->state > jstate) + jstate = p->state; + } + j->state = jstate; + j->status = proc_errorlevel(j->last_proc); + + /* + * Note when co-process dies: can't be done in j_wait() nor + * remove_job() since neither may be called for non-interactive + * shells. + */ + if (j->state == PEXITED || j->state == PSIGNALLED) { + /* + * No need to keep co-process input any more + * (at least, this is what ksh93d thinks) + */ + if (coproc.job == j) { + coproc.job = NULL; + /* + * XXX would be nice to get the closes out of here + * so they aren't done in the signal handler. + * Would mean a check in coproc_getfd() to + * do "if job == 0 && write >= 0, close write". + */ + coproc_write_close(coproc.write); + } + /* Do we need to keep the output? */ + if (j->coproc_id && j->coproc_id == coproc.id && + --coproc.njobs == 0) + coproc_readw_close(coproc.read); + } + + j->flags |= JF_CHANGED; +#ifndef MKSH_UNEMPLOYED + if (Flag(FMONITOR) && !(j->flags & JF_XXCOM)) { + /* + * Only put stopped jobs at the front to avoid confusing + * the user (don't want finished jobs effecting %+ or %-) + */ + if (j->state == PSTOPPED) + put_job(j, PJ_ON_FRONT); + if (Flag(FNOTIFY) && + (j->flags & (JF_WAITING|JF_W_ASYNCNOTIFY)) != JF_WAITING) { + /* Look for the real file descriptor 2 */ + { + struct env *ep; + int fd = 2; + + for (ep = e; ep; ep = ep->oenv) + if (ep->savefd && ep->savefd[2]) + fd = ep->savefd[2]; + shf_reopen(fd, SHF_WR, shl_j); + } + /* + * Can't call j_notify() as it removes jobs. The job + * must stay in the job list as j_waitj() may be + * running with this job. + */ + j_print(j, JP_MEDIUM, shl_j); + shf_flush(shl_j); + if (!(j->flags & JF_WAITING) && j->state != PSTOPPED) + remove_job(j, "notify"); + } + } +#endif + if ( +#ifndef MKSH_UNEMPLOYED + !Flag(FMONITOR) && +#endif + !(j->flags & (JF_WAITING|JF_FG)) && + j->state != PSTOPPED) { + if (j == async_job || (j->flags & JF_KNOWN)) { + j->flags |= JF_ZOMBIE; + j->job = -1; + nzombie++; + } else + remove_job(j, "checkjob"); + } +} + +/* + * Print job status in either short, medium or long format. + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static void +j_print(Job *j, int how, struct shf *shf) +{ + Proc *p; + int state; + int status; +#ifdef WCOREDUMP + bool coredumped; +#endif + char jobchar = ' '; + char buf[64]; + const char *filler; + int output = 0; + + if (how == JP_PGRP) { + /* + * POSIX doesn't say what to do it there is no process + * group leader (ie, !FMONITOR). We arbitrarily return + * last pid (which is what $! returns). + */ + shf_fprintf(shf, Tf_dN, (int)(j->pgrp ? j->pgrp : + (j->last_proc ? j->last_proc->pid : 0))); + return; + } + j->flags &= ~JF_CHANGED; + filler = j->job > 10 ? "\n " : "\n "; + if (j == job_list) + jobchar = '+'; + else if (j == job_list->next) + jobchar = '-'; + + for (p = j->proc_list; p != NULL;) { +#ifdef WCOREDUMP + coredumped = false; +#endif + switch (p->state) { + case PRUNNING: + memcpy(buf, "Running", 8); + break; + case PSTOPPED: { + int stopsig = WSTOPSIG(p->status); + + strlcpy(buf, stopsig > 0 && stopsig < ksh_NSIG ? + sigtraps[stopsig].mess : "Stopped", sizeof(buf)); + break; + } + case PEXITED: { + int exitstatus = (WEXITSTATUS(p->status)) & 255; + + if (how == JP_SHORT) + buf[0] = '\0'; + else if (exitstatus == 0) + memcpy(buf, "Done", 5); + else + shf_snprintf(buf, sizeof(buf), "Done (%d)", + exitstatus); + break; + } + case PSIGNALLED: { + int termsig = WTERMSIG(p->status); +#ifdef WCOREDUMP + if (WCOREDUMP(p->status)) + coredumped = true; +#endif + /* + * kludge for not reporting 'normal termination + * signals' (i.e. SIGINT, SIGPIPE) + */ + if (how == JP_SHORT && +#ifdef WCOREDUMP + !coredumped && +#endif + (termsig == SIGINT || termsig == SIGPIPE)) { + buf[0] = '\0'; + } else + strlcpy(buf, termsig > 0 && termsig < ksh_NSIG ? + sigtraps[termsig].mess : "Signalled", + sizeof(buf)); + break; + } + default: + buf[0] = '\0'; + } + + if (how != JP_SHORT) { + if (p == j->proc_list) + shf_fprintf(shf, "[%d] %c ", j->job, jobchar); + else + shf_puts(filler, shf); + } + + if (how == JP_LONG) + shf_fprintf(shf, "%5d ", (int)p->pid); + + if (how == JP_SHORT) { + if (buf[0]) { + output = 1; +#ifdef WCOREDUMP + shf_fprintf(shf, "%s%s ", + buf, coredumped ? " (core dumped)" : null); +#else + shf_puts(buf, shf); + shf_putchar(' ', shf); +#endif + } + } else { + output = 1; + shf_fprintf(shf, "%-20s %s%s%s", buf, p->command, + p->next ? "|" : null, +#ifdef WCOREDUMP + coredumped ? " (core dumped)" : +#endif + null); + } + + state = p->state; + status = p->status; + p = p->next; + while (p && p->state == state && p->status == status) { + if (how == JP_LONG) + shf_fprintf(shf, "%s%5d %-20s %s%s", filler, + (int)p->pid, T1space, p->command, + p->next ? "|" : null); + else if (how == JP_MEDIUM) + shf_fprintf(shf, Tf__ss, p->command, + p->next ? "|" : null); + p = p->next; + } + } + if (output) + shf_putc('\n', shf); +} + +/* + * Convert % sequence to job + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static Job * +j_lookup(const char *cp, int *ecodep) +{ + Job *j, *last_match; + Proc *p; + size_t len; + int job = 0; + + if (ctype(*cp, C_DIGIT) && getn(cp, &job)) { + /* Look for last_proc->pid (what $! returns) first... */ + for (j = job_list; j != NULL; j = j->next) + if (j->last_proc && j->last_proc->pid == job) + return (j); + /* + * ...then look for process group (this is non-POSIX, + * but should not break anything + */ + for (j = job_list; j != NULL; j = j->next) + if (j->pgrp && j->pgrp == job) + return (j); + goto j_lookup_nosuch; + } + if (*cp != '%') { + j_lookup_invalid: + if (ecodep) + *ecodep = JL_INVALID; + return (NULL); + } + switch (*++cp) { + case '\0': /* non-standard */ + case '+': + case '%': + if (job_list != NULL) + return (job_list); + break; + + case '-': + if (job_list != NULL && job_list->next) + return (job_list->next); + break; + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if (!getn(cp, &job)) + goto j_lookup_invalid; + for (j = job_list; j != NULL; j = j->next) + if (j->job == job) + return (j); + break; + + /* %?string */ + case '?': + last_match = NULL; + for (j = job_list; j != NULL; j = j->next) + for (p = j->proc_list; p != NULL; p = p->next) + if (strstr(p->command, cp+1) != NULL) { + if (last_match) { + if (ecodep) + *ecodep = JL_AMBIG; + return (NULL); + } + last_match = j; + } + if (last_match) + return (last_match); + break; + + /* %string */ + default: + len = strlen(cp); + last_match = NULL; + for (j = job_list; j != NULL; j = j->next) + if (strncmp(cp, j->proc_list->command, len) == 0) { + if (last_match) { + if (ecodep) + *ecodep = JL_AMBIG; + return (NULL); + } + last_match = j; + } + if (last_match) + return (last_match); + break; + } + j_lookup_nosuch: + if (ecodep) + *ecodep = JL_NOSUCH; + return (NULL); +} + +static Job *free_jobs; +static Proc *free_procs; + +/* + * allocate a new job and fill in the job number. + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static Job * +new_job(void) +{ + int i; + Job *newj, *j; + + if (free_jobs != NULL) { + newj = free_jobs; + free_jobs = free_jobs->next; + } else + newj = alloc(sizeof(Job), APERM); + + /* brute force method */ + for (i = 1; ; i++) { + for (j = job_list; j && j->job != i; j = j->next) + ; + if (j == NULL) + break; + } + newj->job = i; + + return (newj); +} + +/* + * Allocate new process struct + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static Proc * +new_proc(void) +{ + Proc *p; + + if (free_procs != NULL) { + p = free_procs; + free_procs = free_procs->next; + } else + p = alloc(sizeof(Proc), APERM); + + return (p); +} + +/* + * Take job out of job_list and put old structures into free list. + * Keeps nzombies, last_job and async_job up to date. + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static void +remove_job(Job *j, const char *where) +{ + Proc *p, *tmp; + Job **prev, *curr; + + prev = &job_list; + curr = job_list; + while (curr && curr != j) { + prev = &curr->next; + curr = *prev; + } + if (curr != j) { + internal_warningf("remove_job: job %s (%s)", Tnot_found, where); + return; + } + *prev = curr->next; + + /* free up proc structures */ + for (p = j->proc_list; p != NULL; ) { + tmp = p; + p = p->next; + tmp->next = free_procs; + free_procs = tmp; + } + + if ((j->flags & JF_ZOMBIE) && j->ppid == procpid) + --nzombie; + j->next = free_jobs; + free_jobs = j; + + if (j == last_job) + last_job = NULL; + if (j == async_job) + async_job = NULL; +} + +/* + * put j in a particular location (taking it out job_list if it is there + * already) + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static void +put_job(Job *j, int where) +{ + Job **prev, *curr; + + /* Remove job from list (if there) */ + prev = &job_list; + curr = job_list; + while (curr && curr != j) { + prev = &curr->next; + curr = *prev; + } + if (curr == j) + *prev = curr->next; + + switch (where) { + case PJ_ON_FRONT: + j->next = job_list; + job_list = j; + break; + + case PJ_PAST_STOPPED: + prev = &job_list; + curr = job_list; + for (; curr && curr->state == PSTOPPED; prev = &curr->next, + curr = *prev) + ; + j->next = curr; + *prev = j; + break; + } +} + +/* + * nuke a job (called when unable to start full job). + * + * If jobs are compiled in then this routine expects sigchld to be blocked. + */ +static int +kill_job(Job *j, int sig) +{ + Proc *p; + int rval = 0; + + for (p = j->proc_list; p != NULL; p = p->next) + if (p->pid != 0) + if (kill(p->pid, sig) < 0) + rval = -1; + return (rval); +} + +static void +tty_init_talking(void) +{ + switch (tty_init_fd()) { + case 0: + break; + case 1: +#ifndef MKSH_DISABLE_TTY_WARNING + warningf(false, Tf_sD_s_sD_s, + "No controlling tty", Topen, T_devtty, cstrerror(errno)); +#endif + break; + case 2: +#ifndef MKSH_DISABLE_TTY_WARNING + warningf(false, Tf_sD_s_s, Tcant_find, Ttty_fd, + cstrerror(errno)); +#endif + break; + case 3: + warningf(false, Tf_ssfaileds, "j_ttyinit", + Ttty_fd_dupof, cstrerror(errno)); + break; + case 4: + warningf(false, Tf_sD_sD_s, "j_ttyinit", + "can't set close-on-exec flag", cstrerror(errno)); + break; + } +} + +static void +tty_init_state(void) +{ + if (tty_fd >= 0) { + mksh_tcget(tty_fd, &tty_state); + tty_hasstate = true; + } +} diff --git a/lalloc.c b/lalloc.c new file mode 100644 index 0000000..0aff3aa --- /dev/null +++ b/lalloc.c @@ -0,0 +1,193 @@ +/*- + * Copyright (c) 2009, 2010, 2011, 2013, 2014, 2016 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" +#ifdef MKSH_ALLOC_CATCH_UNDERRUNS +#include +#endif + +__RCSID("$MirOS: src/bin/mksh/lalloc.c,v 1.26 2016/02/26 21:53:36 tg Exp $"); + +/* build with CPPFLAGS+= -DUSE_REALLOC_MALLOC=0 on ancient systems */ +#if defined(USE_REALLOC_MALLOC) && (USE_REALLOC_MALLOC == 0) +#define remalloc(p,n) ((p) == NULL ? malloc_osi(n) : realloc_osi((p), (n))) +#else +#define remalloc(p,n) realloc_osi((p), (n)) +#endif + + +static struct lalloc_common *findptr(struct lalloc_common **, char *, Area *); + +#ifndef MKSH_ALLOC_CATCH_UNDERRUNS +#define ALLOC_ISUNALIGNED(p) (((size_t)(p)) % sizeof(struct lalloc_common)) +#else +#define ALLOC_ISUNALIGNED(p) (((size_t)(p)) & 4095) +#undef remalloc +#undef free_osimalloc + +static void +free_osimalloc(void *ptr) +{ + struct lalloc_item *lp = ptr; + + if (munmap(lp, lp->len)) + err(1, "free_osimalloc"); +} + +static void * +remalloc(void *ptr, size_t size) +{ + struct lalloc_item *lp, *lold = ptr; + + size = (size + 4095) & ~(size_t)4095; + + if (lold && lold->len >= size) + return (ptr); + + if ((lp = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_ANON | MAP_PRIVATE, -1, (off_t)0)) == MAP_FAILED) + err(1, "remalloc: mmap(%zu)", size); + if (ALLOC_ISUNALIGNED(lp)) + errx(1, "remalloc: unaligned(%p)", lp); + if (mprotect(((char *)lp) + 4096, 4096, PROT_NONE)) + err(1, "remalloc: mprotect"); + lp->len = size; + + if (lold) { + memcpy(((char *)lp) + 8192, ((char *)lold) + 8192, + lold->len - 8192); + if (munmap(lold, lold->len)) + err(1, "remalloc: munmap"); + } + + return (lp); +} +#endif + +void +ainit(Area *ap) +{ +#ifdef MKSH_ALLOC_CATCH_UNDERRUNS + if (sysconf(_SC_PAGESIZE) != 4096) { + fprintf(stderr, "mksh: fatal: pagesize %lu not 4096!\n", + sysconf(_SC_PAGESIZE)); + fflush(stderr); + abort(); + } +#endif + /* area pointer and items share struct lalloc_common */ + ap->next = NULL; +} + +static struct lalloc_common * +findptr(struct lalloc_common **lpp, char *ptr, Area *ap) +{ + void *lp; + +#ifndef MKSH_SMALL + if (ALLOC_ISUNALIGNED(ptr)) + goto fail; +#endif + /* get address of ALLOC_ITEM from user item */ + /* + * note: the alignment of "ptr" to ALLOC_ITEM is checked + * above; the "void *" gets us rid of a gcc 2.95 warning + */ + *lpp = (lp = ptr - sizeof(ALLOC_ITEM)); + /* search for allocation item in group list */ + while (ap->next != lp) + if ((ap = ap->next) == NULL) { +#ifndef MKSH_SMALL + fail: +#endif +#ifdef DEBUG + internal_warningf("rogue pointer %zX in ap %zX", + (size_t)ptr, (size_t)ap); + /* try to get a coredump */ + abort(); +#else + internal_errorf("rogue pointer %zX", (size_t)ptr); +#endif + } + return (ap); +} + +void * +aresize2(void *ptr, size_t fac1, size_t fac2, Area *ap) +{ + if (notoktomul(fac1, fac2)) + internal_errorf(Tintovfl, fac1, '*', fac2); + return (aresize(ptr, fac1 * fac2, ap)); +} + +void * +aresize(void *ptr, size_t numb, Area *ap) +{ + struct lalloc_common *lp = NULL; + + /* resizing (true) or newly allocating? */ + if (ptr != NULL) { + struct lalloc_common *pp; + + pp = findptr(&lp, ptr, ap); + pp->next = lp->next; + } + + if (notoktoadd(numb, sizeof(ALLOC_ITEM)) || + (lp = remalloc(lp, numb + sizeof(ALLOC_ITEM))) == NULL +#ifndef MKSH_SMALL + || ALLOC_ISUNALIGNED(lp) +#endif + ) + internal_errorf(Toomem, numb); + /* area pointer and items share struct lalloc_common */ + lp->next = ap->next; + ap->next = lp; + /* return user item address */ + return ((char *)lp + sizeof(ALLOC_ITEM)); +} + +void +afree(void *ptr, Area *ap) +{ + if (ptr != NULL) { + struct lalloc_common *lp, *pp; + + pp = findptr(&lp, ptr, ap); + /* unhook */ + pp->next = lp->next; + /* now free ALLOC_ITEM */ + free_osimalloc(lp); + } +} + +void +afreeall(Area *ap) +{ + struct lalloc_common *lp; + + /* traverse group (linked list) */ + while ((lp = ap->next) != NULL) { + /* make next ALLOC_ITEM head of list */ + ap->next = lp->next; + /* free old head */ + free_osimalloc(lp); + } +} diff --git a/lex.c b/lex.c new file mode 100644 index 0000000..9300311 --- /dev/null +++ b/lex.c @@ -0,0 +1,1800 @@ +/* $OpenBSD: lex.c,v 1.51 2015/09/10 22:48:58 nicm Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/lex.c,v 1.247 2018/01/14 01:44:01 tg Exp $"); + +/* + * states while lexing word + */ +#define SBASE 0 /* outside any lexical constructs */ +#define SWORD 1 /* implicit quoting for substitute() */ +#define SLETPAREN 2 /* inside (( )), implicit quoting */ +#define SSQUOTE 3 /* inside '' */ +#define SDQUOTE 4 /* inside "" */ +#define SEQUOTE 5 /* inside $'' */ +#define SBRACE 6 /* inside ${} */ +#define SQBRACE 7 /* inside "${}" */ +#define SBQUOTE 8 /* inside `` */ +#define SASPAREN 9 /* inside $(( )) */ +#define SHEREDELIM 10 /* parsing << or <<- delimiter */ +#define SHEREDQUOTE 11 /* parsing " in << or <<- delimiter */ +#define SPATTERN 12 /* parsing *(...|...) pattern (*+?@!) */ +#define SADELIM 13 /* like SBASE, looking for delimiter */ +#define STBRACEKORN 14 /* parsing ${...[#%]...} !FSH */ +#define STBRACEBOURNE 15 /* parsing ${...[#%]...} FSH */ +#define SINVALID 255 /* invalid state */ + +struct sretrace_info { + struct sretrace_info *next; + XString xs; + char *xp; +}; + +/* + * Structure to keep track of the lexing state and the various pieces of info + * needed for each particular state. + */ +typedef struct lex_state { + union { + /* point to the next state block */ + struct lex_state *base; + /* marks start of state output in output string */ + size_t start; + /* SBQUOTE: true if in double quotes: "`...`" */ + /* SEQUOTE: got NUL, ignore rest of string */ + bool abool; + /* SADELIM information */ + struct { + /* character to search for */ + unsigned char delimiter; + /* max. number of delimiters */ + unsigned char num; + } adelim; + } u; + /* count open parentheses */ + short nparen; + /* type of this state */ + uint8_t type; +} Lex_state; +#define ls_base u.base +#define ls_start u.start +#define ls_bool u.abool +#define ls_adelim u.adelim + +typedef struct { + Lex_state *base; + Lex_state *end; +} State_info; + +static void readhere(struct ioword *); +static void ungetsc(int); +static void ungetsc_i(int); +static int getsc_uu(void); +static void getsc_line(Source *); +static int getsc_bn(void); +static int getsc_i(void); +static char *get_brace_var(XString *, char *); +static bool arraysub(char **); +static void gethere(void); +static Lex_state *push_state_i(State_info *, Lex_state *); +static Lex_state *pop_state_i(State_info *, Lex_state *); + +static int backslash_skip; +static int ignore_backslash_newline; + +/* optimised getsc_bn() */ +#define o_getsc() (*source->str != '\0' && *source->str != '\\' && \ + !backslash_skip ? *source->str++ : getsc_bn()) +/* optimised getsc_uu() */ +#define o_getsc_u() ((*source->str != '\0') ? *source->str++ : getsc_uu()) + +/* retrace helper */ +#define o_getsc_r(carg) \ + int cev = (carg); \ + struct sretrace_info *rp = retrace_info; \ + \ + while (rp) { \ + Xcheck(rp->xs, rp->xp); \ + *rp->xp++ = cev; \ + rp = rp->next; \ + } \ + \ + return (cev); + +/* callback */ +static int +getsc_i(void) +{ + o_getsc_r((unsigned int)(unsigned char)o_getsc()); +} + +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +#define getsc() getsc_i() +#else +static int getsc_r(int); + +static int +getsc_r(int c) +{ + o_getsc_r(c); +} + +#define getsc() getsc_r((unsigned int)(unsigned char)o_getsc()) +#endif + +#define STATE_BSIZE 8 + +#define PUSH_STATE(s) do { \ + if (++statep == state_info.end) \ + statep = push_state_i(&state_info, statep); \ + state = statep->type = (s); \ +} while (/* CONSTCOND */ 0) + +#define POP_STATE() do { \ + if (--statep == state_info.base) \ + statep = pop_state_i(&state_info, statep); \ + state = statep->type; \ +} while (/* CONSTCOND */ 0) + +#define PUSH_SRETRACE(s) do { \ + struct sretrace_info *ri; \ + \ + PUSH_STATE(s); \ + statep->ls_start = Xsavepos(ws, wp); \ + ri = alloc(sizeof(struct sretrace_info), ATEMP); \ + Xinit(ri->xs, ri->xp, 64, ATEMP); \ + ri->next = retrace_info; \ + retrace_info = ri; \ +} while (/* CONSTCOND */ 0) + +#define POP_SRETRACE() do { \ + wp = Xrestpos(ws, wp, statep->ls_start); \ + *retrace_info->xp = '\0'; \ + sp = Xstring(retrace_info->xs, retrace_info->xp); \ + dp = (void *)retrace_info; \ + retrace_info = retrace_info->next; \ + afree(dp, ATEMP); \ + POP_STATE(); \ +} while (/* CONSTCOND */ 0) + +/** + * Lexical analyser + * + * tokens are not regular expressions, they are LL(1). + * for example, "${var:-${PWD}}", and "$(size $(whence ksh))". + * hence the state stack. Note "$(...)" are now parsed recursively. + */ + +int +yylex(int cf) +{ + Lex_state states[STATE_BSIZE], *statep, *s2, *base; + State_info state_info; + int c, c2, state; + size_t cz; + XString ws; /* expandable output word */ + char *wp; /* output word pointer */ + char *sp, *dp; + + Again: + states[0].type = SINVALID; + states[0].ls_base = NULL; + statep = &states[1]; + state_info.base = states; + state_info.end = &state_info.base[STATE_BSIZE]; + + Xinit(ws, wp, 64, ATEMP); + + backslash_skip = 0; + ignore_backslash_newline = 0; + + if (cf & ONEWORD) + state = SWORD; + else if (cf & LETEXPR) { + /* enclose arguments in (double) quotes */ + *wp++ = OQUOTE; + state = SLETPAREN; + statep->nparen = 0; + } else { + /* normal lexing */ + state = (cf & HEREDELIM) ? SHEREDELIM : SBASE; + do { + c = getsc(); + } while (ctype(c, C_BLANK)); + if (c == '#') { + ignore_backslash_newline++; + do { + c = getsc(); + } while (!ctype(c, C_NUL | C_LF)); + ignore_backslash_newline--; + } + ungetsc(c); + } + if (source->flags & SF_ALIAS) { + /* trailing ' ' in alias definition */ + source->flags &= ~SF_ALIAS; + /* POSIX: trailing space only counts if parsing simple cmd */ + if (!Flag(FPOSIX) || (cf & CMDWORD)) + cf |= ALIAS; + } + + /* Initial state: one of SWORD SLETPAREN SHEREDELIM SBASE */ + statep->type = state; + + /* collect non-special or quoted characters to form word */ + while (!((c = getsc()) == 0 || + ((state == SBASE || state == SHEREDELIM) && ctype(c, C_LEX1)))) { + if (state == SBASE && + subshell_nesting_type == ORD(/*{*/ '}') && + (unsigned int)c == ORD(/*{*/ '}')) + /* possibly end ${ :;} */ + break; + Xcheck(ws, wp); + switch (state) { + case SADELIM: + if ((unsigned int)c == ORD('(')) + statep->nparen++; + else if ((unsigned int)c == ORD(')')) + statep->nparen--; + else if (statep->nparen == 0 && + ((unsigned int)c == ORD(/*{*/ '}') || + c == (int)statep->ls_adelim.delimiter)) { + *wp++ = ADELIM; + *wp++ = c; + if ((unsigned int)c == ORD(/*{*/ '}') || + --statep->ls_adelim.num == 0) + POP_STATE(); + if ((unsigned int)c == ORD(/*{*/ '}')) + POP_STATE(); + break; + } + /* FALLTHROUGH */ + case SBASE: + if ((unsigned int)c == ORD('[') && (cf & CMDASN)) { + /* temporary */ + *wp = EOS; + if (is_wdvarname(Xstring(ws, wp), false)) { + char *p, *tmp; + + if (arraysub(&tmp)) { + *wp++ = CHAR; + *wp++ = c; + for (p = tmp; *p; ) { + Xcheck(ws, wp); + *wp++ = CHAR; + *wp++ = *p++; + } + afree(tmp, ATEMP); + break; + } + } + *wp++ = CHAR; + *wp++ = c; + break; + } + /* FALLTHROUGH */ + Sbase1: /* includes *(...|...) pattern (*+?@!) */ + if (ctype(c, C_PATMO)) { + c2 = getsc(); + if ((unsigned int)c2 == ORD('(' /*)*/)) { + *wp++ = OPAT; + *wp++ = c; + PUSH_STATE(SPATTERN); + break; + } + ungetsc(c2); + } + /* FALLTHROUGH */ + Sbase2: /* doesn't include *(...|...) pattern (*+?@!) */ + switch (c) { + case ORD('\\'): + getsc_qchar: + if ((c = getsc())) { + /* trailing \ is lost */ + *wp++ = QCHAR; + *wp++ = c; + } + break; + case ORD('\''): + open_ssquote_unless_heredoc: + if ((cf & HEREDOC)) + goto store_char; + *wp++ = OQUOTE; + ignore_backslash_newline++; + PUSH_STATE(SSQUOTE); + break; + case ORD('"'): + open_sdquote: + *wp++ = OQUOTE; + PUSH_STATE(SDQUOTE); + break; + case ORD('$'): + /* + * processing of dollar sign belongs into + * Subst, except for those which can open + * a string: $'…' and $"…" + */ + subst_dollar_ex: + c = getsc(); + switch (c) { + case ORD('"'): + goto open_sdquote; + case ORD('\''): + goto open_sequote; + default: + goto SubstS; + } + default: + goto Subst; + } + break; + + Subst: + switch (c) { + case ORD('\\'): + c = getsc(); + switch (c) { + case ORD('"'): + if ((cf & HEREDOC)) + goto heredocquote; + /* FALLTHROUGH */ + case ORD('\\'): + case ORD('$'): + case ORD('`'): + store_qchar: + *wp++ = QCHAR; + *wp++ = c; + break; + default: + heredocquote: + Xcheck(ws, wp); + if (c) { + /* trailing \ is lost */ + *wp++ = CHAR; + *wp++ = '\\'; + *wp++ = CHAR; + *wp++ = c; + } + break; + } + break; + case ORD('$'): + c = getsc(); + SubstS: + if ((unsigned int)c == ORD('(' /*)*/)) { + c = getsc(); + if ((unsigned int)c == ORD('(' /*)*/)) { + *wp++ = EXPRSUB; + PUSH_SRETRACE(SASPAREN); + statep->nparen = 2; + *retrace_info->xp++ = '('; + } else { + ungetsc(c); + subst_command: + c = COMSUB; + subst_command2: + sp = yyrecursive(c); + cz = strlen(sp) + 1; + XcheckN(ws, wp, cz); + *wp++ = c; + memcpy(wp, sp, cz); + wp += cz; + } + } else if ((unsigned int)c == ORD('{' /*}*/)) { + if ((unsigned int)(c = getsc()) == ORD('|')) { + /* + * non-subenvironment + * value substitution + */ + c = VALSUB; + goto subst_command2; + } else if (ctype(c, C_IFSWS)) { + /* + * non-subenvironment + * "command" substitution + */ + c = FUNSUB; + goto subst_command2; + } + ungetsc(c); + *wp++ = OSUBST; + *wp++ = '{' /*}*/; + wp = get_brace_var(&ws, wp); + c = getsc(); + /* allow :# and :% (ksh88 compat) */ + if ((unsigned int)c == ORD(':')) { + *wp++ = CHAR; + *wp++ = c; + c = getsc(); + if ((unsigned int)c == ORD(':')) { + *wp++ = CHAR; + *wp++ = '0'; + *wp++ = ADELIM; + *wp++ = ':'; + PUSH_STATE(SBRACE); + PUSH_STATE(SADELIM); + statep->ls_adelim.delimiter = ':'; + statep->ls_adelim.num = 1; + statep->nparen = 0; + break; + } else if (ctype(c, C_DIGIT | C_DOLAR | C_SPC) || + /*XXX what else? */ + c == '(' /*)*/) { + /* substring subst. */ + if (c != ' ') { + *wp++ = CHAR; + *wp++ = ' '; + } + ungetsc(c); + PUSH_STATE(SBRACE); + PUSH_STATE(SADELIM); + statep->ls_adelim.delimiter = ':'; + statep->ls_adelim.num = 2; + statep->nparen = 0; + break; + } + } else if (c == '/') { + c2 = ADELIM; + parse_adelim_slash: + *wp++ = CHAR; + *wp++ = c; + if ((unsigned int)(c = getsc()) == ORD('/')) { + *wp++ = c2; + *wp++ = c; + } else + ungetsc(c); + PUSH_STATE(SBRACE); + PUSH_STATE(SADELIM); + statep->ls_adelim.delimiter = '/'; + statep->ls_adelim.num = 1; + statep->nparen = 0; + break; + } else if (c == '@') { + c2 = getsc(); + ungetsc(c2); + if ((unsigned int)c2 == ORD('/')) { + c2 = CHAR; + goto parse_adelim_slash; + } + } + /* + * If this is a trim operation, + * treat (,|,) specially in STBRACE. + */ + if (ctype(c, C_SUB2)) { + ungetsc(c); + if (Flag(FSH)) + PUSH_STATE(STBRACEBOURNE); + else + PUSH_STATE(STBRACEKORN); + } else { + ungetsc(c); + if (state == SDQUOTE || + state == SQBRACE) + PUSH_STATE(SQBRACE); + else + PUSH_STATE(SBRACE); + } + } else if (ctype(c, C_ALPHX)) { + *wp++ = OSUBST; + *wp++ = 'X'; + do { + Xcheck(ws, wp); + *wp++ = c; + c = getsc(); + } while (ctype(c, C_ALNUX)); + *wp++ = '\0'; + *wp++ = CSUBST; + *wp++ = 'X'; + ungetsc(c); + } else if (ctype(c, C_VAR1 | C_DIGIT)) { + Xcheck(ws, wp); + *wp++ = OSUBST; + *wp++ = 'X'; + *wp++ = c; + *wp++ = '\0'; + *wp++ = CSUBST; + *wp++ = 'X'; + } else { + *wp++ = CHAR; + *wp++ = '$'; + ungetsc(c); + } + break; + case ORD('`'): + subst_gravis: + PUSH_STATE(SBQUOTE); + *wp++ = COMASUB; + /* + * We need to know whether we are within double + * quotes in order to translate \" to " within + * "…`…\"…`…" because, unlike for COMSUBs, the + * outer double quoteing changes the backslash + * meaning for the inside. For more details: + * http://austingroupbugs.net/view.php?id=1015 + */ + statep->ls_bool = false; + s2 = statep; + base = state_info.base; + while (/* CONSTCOND */ 1) { + for (; s2 != base; s2--) { + if (s2->type == SDQUOTE) { + statep->ls_bool = true; + break; + } + } + if (s2 != base) + break; + if (!(s2 = s2->ls_base)) + break; + base = s2-- - STATE_BSIZE; + } + break; + case QCHAR: + if (cf & LQCHAR) { + *wp++ = QCHAR; + *wp++ = getsc(); + break; + } + /* FALLTHROUGH */ + default: + store_char: + *wp++ = CHAR; + *wp++ = c; + } + break; + + case SEQUOTE: + if ((unsigned int)c == ORD('\'')) { + POP_STATE(); + *wp++ = CQUOTE; + ignore_backslash_newline--; + } else if ((unsigned int)c == ORD('\\')) { + if ((c2 = unbksl(true, getsc_i, ungetsc)) == -1) + c2 = getsc(); + if (c2 == 0) + statep->ls_bool = true; + if (!statep->ls_bool) { + char ts[4]; + + if ((unsigned int)c2 < 0x100) { + *wp++ = QCHAR; + *wp++ = c2; + } else { + cz = utf_wctomb(ts, c2 - 0x100); + ts[cz] = 0; + cz = 0; + do { + *wp++ = QCHAR; + *wp++ = ts[cz]; + } while (ts[++cz]); + } + } + } else if (!statep->ls_bool) { + *wp++ = QCHAR; + *wp++ = c; + } + break; + + case SSQUOTE: + if ((unsigned int)c == ORD('\'')) { + POP_STATE(); + if ((cf & HEREDOC) || state == SQBRACE) + goto store_char; + *wp++ = CQUOTE; + ignore_backslash_newline--; + } else { + *wp++ = QCHAR; + *wp++ = c; + } + break; + + case SDQUOTE: + if ((unsigned int)c == ORD('"')) { + POP_STATE(); + *wp++ = CQUOTE; + } else + goto Subst; + break; + + /* $(( ... )) */ + case SASPAREN: + if ((unsigned int)c == ORD('(')) + statep->nparen++; + else if ((unsigned int)c == ORD(')')) { + statep->nparen--; + if (statep->nparen == 1) { + /* end of EXPRSUB */ + POP_SRETRACE(); + + if ((unsigned int)(c2 = getsc()) == ORD(/*(*/ ')')) { + cz = strlen(sp) - 2; + XcheckN(ws, wp, cz); + memcpy(wp, sp + 1, cz); + wp += cz; + afree(sp, ATEMP); + *wp++ = '\0'; + break; + } else { + Source *s; + + ungetsc(c2); + /* + * mismatched parenthesis - + * assume we were really + * parsing a $(...) expression + */ + --wp; + s = pushs(SREREAD, + source->areap); + s->start = s->str = + s->u.freeme = sp; + s->next = source; + source = s; + goto subst_command; + } + } + } + /* reuse existing state machine */ + goto Sbase2; + + case SQBRACE: + if ((unsigned int)c == ORD('\\')) { + /* + * perform POSIX "quote removal" if the back- + * slash is "special", i.e. same cases as the + * {case '\\':} in Subst: plus closing brace; + * in mksh code "quote removal" on '\c' means + * write QCHAR+c, otherwise CHAR+\+CHAR+c are + * emitted (in heredocquote:) + */ + if ((unsigned int)(c = getsc()) == ORD('"') || + (unsigned int)c == ORD('\\') || + ctype(c, C_DOLAR | C_GRAVE) || + (unsigned int)c == ORD(/*{*/ '}')) + goto store_qchar; + goto heredocquote; + } + goto common_SQBRACE; + + case SBRACE: + if ((unsigned int)c == ORD('\'')) + goto open_ssquote_unless_heredoc; + else if ((unsigned int)c == ORD('\\')) + goto getsc_qchar; + common_SQBRACE: + if ((unsigned int)c == ORD('"')) + goto open_sdquote; + else if ((unsigned int)c == ORD('$')) + goto subst_dollar_ex; + else if ((unsigned int)c == ORD('`')) + goto subst_gravis; + else if ((unsigned int)c != ORD(/*{*/ '}')) + goto store_char; + POP_STATE(); + *wp++ = CSUBST; + *wp++ = /*{*/ '}'; + break; + + /* Same as SBASE, except (,|,) treated specially */ + case STBRACEKORN: + if ((unsigned int)c == ORD('|')) + *wp++ = SPAT; + else if ((unsigned int)c == ORD('(')) { + *wp++ = OPAT; + /* simile for @ */ + *wp++ = ' '; + PUSH_STATE(SPATTERN); + } else /* FALLTHROUGH */ + case STBRACEBOURNE: + if ((unsigned int)c == ORD(/*{*/ '}')) { + POP_STATE(); + *wp++ = CSUBST; + *wp++ = /*{*/ '}'; + } else + goto Sbase1; + break; + + case SBQUOTE: + if ((unsigned int)c == ORD('`')) { + *wp++ = 0; + POP_STATE(); + } else if ((unsigned int)c == ORD('\\')) { + switch (c = getsc()) { + case 0: + /* trailing \ is lost */ + break; + case ORD('$'): + case ORD('`'): + case ORD('\\'): + *wp++ = c; + break; + case ORD('"'): + if (statep->ls_bool) { + *wp++ = c; + break; + } + /* FALLTHROUGH */ + default: + *wp++ = '\\'; + *wp++ = c; + break; + } + } else + *wp++ = c; + break; + + /* ONEWORD */ + case SWORD: + goto Subst; + + /* LETEXPR: (( ... )) */ + case SLETPAREN: + if ((unsigned int)c == ORD(/*(*/ ')')) { + if (statep->nparen > 0) + --statep->nparen; + else if ((unsigned int)(c2 = getsc()) == ORD(/*(*/ ')')) { + c = 0; + *wp++ = CQUOTE; + goto Done; + } else { + Source *s; + + ungetsc(c2); + ungetsc(c); + /* + * mismatched parenthesis - + * assume we were really + * parsing a (...) expression + */ + *wp = EOS; + sp = Xstring(ws, wp); + dp = wdstrip(sp + 1, WDS_TPUTS); + s = pushs(SREREAD, source->areap); + s->start = s->str = s->u.freeme = dp; + s->next = source; + source = s; + ungetsc('(' /*)*/); + return (ORD('(' /*)*/)); + } + } else if ((unsigned int)c == ORD('(')) + /* + * parentheses inside quotes and + * backslashes are lost, but AT&T ksh + * doesn't count them either + */ + ++statep->nparen; + goto Sbase2; + + /* << or <<- delimiter */ + case SHEREDELIM: + /* + * here delimiters need a special case since + * $ and `...` are not to be treated specially + */ + switch (c) { + case ORD('\\'): + if ((c = getsc())) { + /* trailing \ is lost */ + *wp++ = QCHAR; + *wp++ = c; + } + break; + case ORD('\''): + goto open_ssquote_unless_heredoc; + case ORD('$'): + if ((unsigned int)(c2 = getsc()) == ORD('\'')) { + open_sequote: + *wp++ = OQUOTE; + ignore_backslash_newline++; + PUSH_STATE(SEQUOTE); + statep->ls_bool = false; + break; + } else if ((unsigned int)c2 == ORD('"')) { + /* FALLTHROUGH */ + case ORD('"'): + PUSH_SRETRACE(SHEREDQUOTE); + break; + } + ungetsc(c2); + /* FALLTHROUGH */ + default: + *wp++ = CHAR; + *wp++ = c; + } + break; + + /* " in << or <<- delimiter */ + case SHEREDQUOTE: + if ((unsigned int)c != ORD('"')) + goto Subst; + POP_SRETRACE(); + dp = strnul(sp) - 1; + /* remove the trailing double quote */ + *dp = '\0'; + /* store the quoted string */ + *wp++ = OQUOTE; + XcheckN(ws, wp, (dp - sp) * 2); + dp = sp; + while ((c = *dp++)) { + if (c == '\\') { + switch ((c = *dp++)) { + case ORD('\\'): + case ORD('"'): + case ORD('$'): + case ORD('`'): + break; + default: + *wp++ = CHAR; + *wp++ = '\\'; + break; + } + } + *wp++ = CHAR; + *wp++ = c; + } + afree(sp, ATEMP); + *wp++ = CQUOTE; + state = statep->type = SHEREDELIM; + break; + + /* in *(...|...) pattern (*+?@!) */ + case SPATTERN: + if ((unsigned int)c == ORD(/*(*/ ')')) { + *wp++ = CPAT; + POP_STATE(); + } else if ((unsigned int)c == ORD('|')) { + *wp++ = SPAT; + } else if ((unsigned int)c == ORD('(')) { + *wp++ = OPAT; + /* simile for @ */ + *wp++ = ' '; + PUSH_STATE(SPATTERN); + } else + goto Sbase1; + break; + } + } + Done: + Xcheck(ws, wp); + if (statep != &states[1]) + /* XXX figure out what is missing */ + yyerror("no closing quote"); + + /* This done to avoid tests for SHEREDELIM wherever SBASE tested */ + if (state == SHEREDELIM) + state = SBASE; + + dp = Xstring(ws, wp); + if (state == SBASE && ( + (c == '&' && !Flag(FSH) && !Flag(FPOSIX)) || + ctype(c, C_ANGLE)) && ((c2 = Xlength(ws, wp)) == 0 || + (c2 == 2 && dp[0] == CHAR && ctype(dp[1], C_DIGIT)))) { + struct ioword *iop = alloc(sizeof(struct ioword), ATEMP); + + iop->unit = c2 == 2 ? ksh_numdig(dp[1]) : c == '<' ? 0 : 1; + + if (c == '&') { + if ((unsigned int)(c2 = getsc()) != ORD('>')) { + ungetsc(c2); + goto no_iop; + } + c = c2; + iop->ioflag = IOBASH; + } else + iop->ioflag = 0; + + c2 = getsc(); + /* <<, >>, <> are ok, >< is not */ + if (c == c2 || ((unsigned int)c == ORD('<') && + (unsigned int)c2 == ORD('>'))) { + iop->ioflag |= c == c2 ? + ((unsigned int)c == ORD('>') ? IOCAT : IOHERE) : IORDWR; + if (iop->ioflag == IOHERE) { + if ((unsigned int)(c2 = getsc()) == ORD('-')) + iop->ioflag |= IOSKIP; + else if ((unsigned int)c2 == ORD('<')) + iop->ioflag |= IOHERESTR; + else + ungetsc(c2); + } + } else if ((unsigned int)c2 == ORD('&')) + iop->ioflag |= IODUP | ((unsigned int)c == ORD('<') ? IORDUP : 0); + else { + iop->ioflag |= (unsigned int)c == ORD('>') ? IOWRITE : IOREAD; + if ((unsigned int)c == ORD('>') && (unsigned int)c2 == ORD('|')) + iop->ioflag |= IOCLOB; + else + ungetsc(c2); + } + + iop->ioname = NULL; + iop->delim = NULL; + iop->heredoc = NULL; + /* free word */ + Xfree(ws, wp); + yylval.iop = iop; + return (REDIR); + no_iop: + afree(iop, ATEMP); + } + + if (wp == dp && state == SBASE) { + /* free word */ + Xfree(ws, wp); + /* no word, process LEX1 character */ + if (((unsigned int)c == ORD('|')) || + ((unsigned int)c == ORD('&')) || + ((unsigned int)c == ORD(';')) || + ((unsigned int)c == ORD('(' /*)*/))) { + if ((c2 = getsc()) == c) + c = ((unsigned int)c == ORD(';')) ? BREAK : + ((unsigned int)c == ORD('|')) ? LOGOR : + ((unsigned int)c == ORD('&')) ? LOGAND : + /* (unsigned int)c == ORD('(' )) */ MDPAREN; + else if ((unsigned int)c == ORD('|') && (unsigned int)c2 == ORD('&')) + c = COPROC; + else if ((unsigned int)c == ORD(';') && (unsigned int)c2 == ORD('|')) + c = BRKEV; + else if ((unsigned int)c == ORD(';') && (unsigned int)c2 == ORD('&')) + c = BRKFT; + else + ungetsc(c2); +#ifndef MKSH_SMALL + if (c == BREAK) { + if ((unsigned int)(c2 = getsc()) == ORD('&')) + c = BRKEV; + else + ungetsc(c2); + } +#endif + } else if ((unsigned int)c == ORD('\n')) { + if (cf & HEREDELIM) + ungetsc(c); + else { + gethere(); + if (cf & CONTIN) + goto Again; + } + } else if (c == '\0' && !(cf & HEREDELIM)) { + struct ioword **p = heres; + + while (p < herep) + if ((*p)->ioflag & IOHERESTR) + ++p; + else + /* ksh -c 'cat <delim, 0)); + } + return (c); + } + + /* terminate word */ + *wp++ = EOS; + yylval.cp = Xclose(ws, wp); + if (state == SWORD || state == SLETPAREN + /* XXX ONEWORD? */) + return (LWORD); + + /* unget terminator */ + ungetsc(c); + + /* + * note: the alias-vs-function code below depends on several + * interna: starting from here, source->str is not modified; + * the way getsc() and ungetsc() operate; etc. + */ + + /* copy word to unprefixed string ident */ + sp = yylval.cp; + dp = ident; + while ((dp - ident) < IDENT && (c = *sp++) == CHAR) + *dp++ = *sp++; + if (c != EOS) + /* word is not unquoted, or space ran out */ + dp = ident; + /* make sure the ident array stays NUL padded */ + memset(dp, 0, (ident + IDENT) - dp + 1); + + if (*ident != '\0' && (cf & (KEYWORD | ALIAS))) { + struct tbl *p; + uint32_t h = hash(ident); + + if ((cf & KEYWORD) && (p = ktsearch(&keywords, ident, h)) && + (!(cf & ESACONLY) || p->val.i == ESAC || + (unsigned int)p->val.i == ORD(/*{*/ '}'))) { + afree(yylval.cp, ATEMP); + return (p->val.i); + } + if ((cf & ALIAS) && (p = ktsearch(&aliases, ident, h)) && + (p->flag & ISSET)) { + /* + * this still points to the same character as the + * ungetsc'd terminator from above + */ + const char *cp = source->str; + + /* prefer POSIX but not Korn functions over aliases */ + while (ctype(*cp, C_BLANK)) + /* + * this is like getsc() without skipping + * over Source boundaries (including not + * parsing ungetsc'd characters that got + * pushed into an SREREAD) which is what + * we want here anyway: find out whether + * the alias name is followed by a POSIX + * function definition + */ + ++cp; + /* prefer functions over aliases */ + if (cp[0] != '(' || cp[1] != ')') { + Source *s = source; + + while (s && (s->flags & SF_HASALIAS)) + if (s->u.tblp == p) + return (LWORD); + else + s = s->next; + /* push alias expansion */ + s = pushs(SALIAS, source->areap); + s->start = s->str = p->val.s; + s->u.tblp = p; + s->flags |= SF_HASALIAS; + s->line = source->line; + s->next = source; + if (source->type == SEOF) { + /* prevent infinite recursion at EOS */ + source->u.tblp = p; + source->flags |= SF_HASALIAS; + } + source = s; + afree(yylval.cp, ATEMP); + goto Again; + } + } + } else if (*ident == '\0') { + /* retain typeset et al. even when quoted */ + struct tbl *tt = get_builtin((dp = wdstrip(yylval.cp, 0))); + uint32_t flag = tt ? tt->flag : 0; + + if (flag & (DECL_UTIL | DECL_FWDR)) + strlcpy(ident, dp, sizeof(ident)); + afree(dp, ATEMP); + } + + return (LWORD); +} + +static void +gethere(void) +{ + struct ioword **p; + + for (p = heres; p < herep; p++) + if (!((*p)->ioflag & IOHERESTR)) + readhere(*p); + herep = heres; +} + +/* + * read "<delim, 0); + + if (!(iop->ioflag & IOEVAL)) + ignore_backslash_newline++; + + Xinit(xs, xp, 256, ATEMP); + + heredoc_read_line: + /* beginning of line */ + eofp = eof; + xpos = Xsavepos(xs, xp); + if (iop->ioflag & IOSKIP) { + /* skip over leading tabs */ + while ((c = getsc()) == '\t') + ; /* nothing */ + goto heredoc_parse_char; + } + heredoc_read_char: + c = getsc(); + heredoc_parse_char: + /* compare with here document marker */ + if (!*eofp) { + /* end of here document marker, what to do? */ + switch (c) { + case ORD(/*(*/ ')'): + if (!subshell_nesting_type) + /*- + * not allowed outside $(...) or (...) + * => mismatch + */ + break; + /* allow $(...) or (...) to close here */ + ungetsc(/*(*/ ')'); + /* FALLTHROUGH */ + case 0: + /* + * Allow EOF here to commands without trailing + * newlines (mksh -c '...') will work as well. + */ + case ORD('\n'): + /* Newline terminates here document marker */ + goto heredoc_found_terminator; + } + } else if (c == *eofp++) + /* store; then read and compare next character */ + goto heredoc_store_and_loop; + /* nope, mismatch; read until end of line */ + while (c != '\n') { + if (!c) + /* oops, reached EOF */ + yyerror(Tf_heredoc, eof); + /* store character */ + Xcheck(xs, xp); + Xput(xs, xp, c); + /* read next character */ + c = getsc(); + } + /* we read a newline as last character */ + heredoc_store_and_loop: + /* store character */ + Xcheck(xs, xp); + Xput(xs, xp, c); + if (c == '\n') + goto heredoc_read_line; + goto heredoc_read_char; + + heredoc_found_terminator: + /* jump back to saved beginning of line */ + xp = Xrestpos(xs, xp, xpos); + /* terminate, close and store */ + Xput(xs, xp, '\0'); + iop->heredoc = Xclose(xs, xp); + + if (!(iop->ioflag & IOEVAL)) + ignore_backslash_newline--; +} + +void +yyerror(const char *fmt, ...) +{ + va_list va; + + /* pop aliases and re-reads */ + while (source->type == SALIAS || source->type == SREREAD) + source = source->next; + /* zap pending input */ + source->str = null; + + error_prefix(true); + va_start(va, fmt); + shf_vfprintf(shl_out, fmt, va); + shf_putc('\n', shl_out); + va_end(va); + errorfz(); +} + +/* + * input for yylex with alias expansion + */ + +Source * +pushs(int type, Area *areap) +{ + Source *s; + + s = alloc(sizeof(Source), areap); + memset(s, 0, sizeof(Source)); + s->type = type; + s->str = null; + s->areap = areap; + if (type == SFILE || type == SSTDIN) + XinitN(s->xs, 256, s->areap); + return (s); +} + +static int +getsc_uu(void) +{ + Source *s = source; + int c; + + while ((c = ord(*s->str++)) == 0) { + /* return 0 for EOF by default */ + s->str = NULL; + switch (s->type) { + case SEOF: + s->str = null; + return (0); + + case SSTDIN: + case SFILE: + getsc_line(s); + break; + + case SWSTR: + break; + + case SSTRING: + case SSTRINGCMDLINE: + break; + + case SWORDS: + s->start = s->str = *s->u.strv++; + s->type = SWORDSEP; + break; + + case SWORDSEP: + if (*s->u.strv == NULL) { + s->start = s->str = "\n"; + s->type = SEOF; + } else { + s->start = s->str = T1space; + s->type = SWORDS; + } + break; + + case SALIAS: + if (s->flags & SF_ALIASEND) { + /* pass on an unused SF_ALIAS flag */ + source = s->next; + source->flags |= s->flags & SF_ALIAS; + s = source; + } else if (*s->u.tblp->val.s && + ctype((c = strnul(s->u.tblp->val.s)[-1]), C_SPACE)) { + /* pop source stack */ + source = s = s->next; + /* + * Note that this alias ended with a + * space, enabling alias expansion on + * the following word. + */ + s->flags |= SF_ALIAS; + } else { + /* + * At this point, we need to keep the current + * alias in the source list so recursive + * aliases can be detected and we also need to + * return the next character. Do this by + * temporarily popping the alias to get the + * next character and then put it back in the + * source list with the SF_ALIASEND flag set. + */ + /* pop source stack */ + source = s->next; + source->flags |= s->flags & SF_ALIAS; + c = getsc_uu(); + if (c) { + s->flags |= SF_ALIASEND; + s->ugbuf[0] = c; s->ugbuf[1] = '\0'; + s->start = s->str = s->ugbuf; + s->next = source; + source = s; + } else { + s = source; + /* avoid reading EOF twice */ + s->str = NULL; + break; + } + } + continue; + + case SREREAD: + if (s->start != s->ugbuf) + /* yuck */ + afree(s->u.freeme, ATEMP); + source = s = s->next; + continue; + } + if (s->str == NULL) { + s->type = SEOF; + s->start = s->str = null; + return ('\0'); + } + if (s->flags & SF_ECHO) { + shf_puts(s->str, shl_out); + shf_flush(shl_out); + } + } + return (c); +} + +static void +getsc_line(Source *s) +{ + char *xp = Xstring(s->xs, xp), *cp; + bool interactive = Flag(FTALKING) && s->type == SSTDIN; + bool have_tty = tobool(interactive && (s->flags & SF_TTY)); + + /* Done here to ensure nothing odd happens when a timeout occurs */ + XcheckN(s->xs, xp, LINE); + *xp = '\0'; + s->start = s->str = xp; + + if (have_tty && ksh_tmout) { + ksh_tmout_state = TMOUT_READING; + alarm(ksh_tmout); + } + if (interactive) { + if (cur_prompt == PS1) + histsave(&s->line, NULL, HIST_FLUSH, true); + change_winsz(); + } +#ifndef MKSH_NO_CMDLINE_EDITING + if (have_tty && ( +#if !MKSH_S_NOVI + Flag(FVI) || +#endif + Flag(FEMACS) || Flag(FGMACS))) { + int nread; + + nread = x_read(xp); + if (nread < 0) + /* read error */ + nread = 0; + xp[nread] = '\0'; + xp += nread; + } else +#endif + { + if (interactive) + pprompt(prompt, 0); + else + s->line++; + + while (/* CONSTCOND */ 1) { + char *p = shf_getse(xp, Xnleft(s->xs, xp), s->u.shf); + + if (!p && shf_error(s->u.shf) && + shf_errno(s->u.shf) == EINTR) { + shf_clearerr(s->u.shf); + if (trap) + runtraps(0); + continue; + } + if (!p || (xp = p, xp[-1] == '\n')) + break; + /* double buffer size */ + /* move past NUL so doubling works... */ + xp++; + XcheckN(s->xs, xp, Xlength(s->xs, xp)); + /* ...and move back again */ + xp--; + } + /* + * flush any unwanted input so other programs/builtins + * can read it. Not very optimal, but less error prone + * than flushing else where, dealing with redirections, + * etc. + * TODO: reduce size of shf buffer (~128?) if SSTDIN + */ + if (s->type == SSTDIN) + shf_flush(s->u.shf); + } + /* + * XXX: temporary kludge to restore source after a + * trap may have been executed. + */ + source = s; + if (have_tty && ksh_tmout) { + ksh_tmout_state = TMOUT_EXECUTING; + alarm(0); + } + cp = Xstring(s->xs, xp); + rndpush(cp); + s->start = s->str = cp; + strip_nuls(Xstring(s->xs, xp), Xlength(s->xs, xp)); + /* Note: if input is all nulls, this is not eof */ + if (Xlength(s->xs, xp) == 0) { + /* EOF */ + if (s->type == SFILE) + shf_fdclose(s->u.shf); + s->str = NULL; + } else if (interactive && *s->str) { + if (cur_prompt != PS1) + histsave(&s->line, s->str, HIST_APPEND, true); + else if (!ctype(*s->str, C_IFS | C_IFSWS)) + histsave(&s->line, s->str, HIST_QUEUE, true); +#if !defined(MKSH_SMALL) && HAVE_PERSISTENT_HISTORY + else + goto check_for_sole_return; + } else if (interactive && cur_prompt == PS1) { + check_for_sole_return: + cp = Xstring(s->xs, xp); + while (ctype(*cp, C_IFSWS)) + ++cp; + if (!*cp) { + histsave(&s->line, NULL, HIST_FLUSH, true); + histsync(); + } +#endif + } + if (interactive) + set_prompt(PS2, NULL); +} + +void +set_prompt(int to, Source *s) +{ + cur_prompt = (uint8_t)to; + + switch (to) { + /* command */ + case PS1: + /* + * Substitute ! and !! here, before substitutions are done + * so ! in expanded variables are not expanded. + * NOTE: this is not what AT&T ksh does (it does it after + * substitutions, POSIX doesn't say which is to be done. + */ + { + struct shf *shf; + char * volatile ps1; + Area *saved_atemp; + int saved_lineno; + + ps1 = str_val(global("PS1")); + shf = shf_sopen(NULL, strlen(ps1) * 2, + SHF_WR | SHF_DYNAMIC, NULL); + while (*ps1) + if (*ps1 != '!' || *++ps1 == '!') + shf_putchar(*ps1++, shf); + else + shf_fprintf(shf, Tf_lu, s ? + (unsigned long)s->line + 1 : 0UL); + ps1 = shf_sclose(shf); + saved_lineno = current_lineno; + if (s) + current_lineno = s->line + 1; + saved_atemp = ATEMP; + newenv(E_ERRH); + if (kshsetjmp(e->jbuf)) { + prompt = safe_prompt; + /* + * Don't print an error - assume it has already + * been printed. Reason is we may have forked + * to run a command and the child may be + * unwinding its stack through this code as it + * exits. + */ + } else { + char *cp = substitute(ps1, 0); + strdupx(prompt, cp, saved_atemp); + } + current_lineno = saved_lineno; + quitenv(NULL); + } + break; + /* command continuation */ + case PS2: + prompt = str_val(global("PS2")); + break; + } +} + +int +pprompt(const char *cp, int ntruncate) +{ + char delimiter = 0; + bool doprint = (ntruncate != -1); + bool indelimit = false; + int columns = 0, lines = 0; + + /* + * Undocumented AT&T ksh feature: + * If the second char in the prompt string is \r then the first + * char is taken to be a non-printing delimiter and any chars + * between two instances of the delimiter are not considered to + * be part of the prompt length + */ + if (*cp && cp[1] == '\r') { + delimiter = *cp; + cp += 2; + } + for (; *cp; cp++) { + if (indelimit && *cp != delimiter) + ; + else if (ctype(*cp, C_CR | C_LF)) { + lines += columns / x_cols + ((*cp == '\n') ? 1 : 0); + columns = 0; + } else if (*cp == '\t') { + columns = (columns | 7) + 1; + } else if (*cp == '\b') { + if (columns > 0) + columns--; + } else if (*cp == delimiter) + indelimit = !indelimit; + else if (UTFMODE && (rtt2asc(*cp) > 0x7F)) { + const char *cp2; + columns += utf_widthadj(cp, &cp2); + if (doprint && (indelimit || + (ntruncate < (x_cols * lines + columns)))) + shf_write(cp, cp2 - cp, shl_out); + cp = cp2 - /* loop increment */ 1; + continue; + } else + columns++; + if (doprint && (*cp != delimiter) && + (indelimit || (ntruncate < (x_cols * lines + columns)))) + shf_putc(*cp, shl_out); + } + if (doprint) + shf_flush(shl_out); + return (x_cols * lines + columns); +} + +/* + * Read the variable part of a ${...} expression (i.e. up to but not + * including the :[-+?=#%] or close-brace). + */ +static char * +get_brace_var(XString *wsp, char *wp) +{ + char c; + enum parse_state { + PS_INITIAL, PS_SAW_PERCENT, PS_SAW_HASH, PS_SAW_BANG, + PS_IDENT, PS_NUMBER, PS_VAR1 + } state = PS_INITIAL; + + while (/* CONSTCOND */ 1) { + c = getsc(); + /* State machine to figure out where the variable part ends. */ + switch (state) { + case PS_SAW_HASH: + if (ctype(c, C_VAR1)) { + char c2; + + c2 = getsc(); + ungetsc(c2); + if (ord(c2) != ORD(/*{*/ '}')) { + ungetsc(c); + goto out; + } + } + goto ps_common; + case PS_SAW_BANG: + switch (ord(c)) { + case ORD('@'): + case ORD('#'): + case ORD('-'): + case ORD('?'): + goto out; + } + goto ps_common; + case PS_INITIAL: + switch (ord(c)) { + case ORD('%'): + state = PS_SAW_PERCENT; + goto next; + case ORD('#'): + state = PS_SAW_HASH; + goto next; + case ORD('!'): + state = PS_SAW_BANG; + goto next; + } + /* FALLTHROUGH */ + case PS_SAW_PERCENT: + ps_common: + if (ctype(c, C_ALPHX)) + state = PS_IDENT; + else if (ctype(c, C_DIGIT)) + state = PS_NUMBER; + else if (ctype(c, C_VAR1)) + state = PS_VAR1; + else + goto out; + break; + case PS_IDENT: + if (!ctype(c, C_ALNUX)) { + if (ord(c) == ORD('[')) { + char *tmp, *p; + + if (!arraysub(&tmp)) + yyerror("missing ]"); + *wp++ = c; + p = tmp; + while (*p) { + Xcheck(*wsp, wp); + *wp++ = *p++; + } + afree(tmp, ATEMP); + /* the ] */ + c = getsc(); + } + goto out; + } + next: + break; + case PS_NUMBER: + if (!ctype(c, C_DIGIT)) + goto out; + break; + case PS_VAR1: + goto out; + } + Xcheck(*wsp, wp); + *wp++ = c; + } + out: + /* end of variable part */ + *wp++ = '\0'; + ungetsc(c); + return (wp); +} + +/* + * Save an array subscript - returns true if matching bracket found, false + * if eof or newline was found. + * (Returned string double null terminated) + */ +static bool +arraysub(char **strp) +{ + XString ws; + char *wp, c; + /* we are just past the initial [ */ + unsigned int depth = 1; + + Xinit(ws, wp, 32, ATEMP); + + do { + c = getsc(); + Xcheck(ws, wp); + *wp++ = c; + if (ord(c) == ORD('[')) + depth++; + else if (ord(c) == ORD(']')) + depth--; + } while (depth > 0 && c && c != '\n'); + + *wp++ = '\0'; + *strp = Xclose(ws, wp); + + return (tobool(depth == 0)); +} + +/* Unget a char: handles case when we are already at the start of the buffer */ +static void +ungetsc(int c) +{ + struct sretrace_info *rp = retrace_info; + + if (backslash_skip) + backslash_skip--; + /* Don't unget EOF... */ + if (source->str == null && c == '\0') + return; + while (rp) { + if (Xlength(rp->xs, rp->xp)) + rp->xp--; + rp = rp->next; + } + ungetsc_i(c); +} +static void +ungetsc_i(int c) +{ + if (source->str > source->start) + source->str--; + else { + Source *s; + + s = pushs(SREREAD, source->areap); + s->ugbuf[0] = c; s->ugbuf[1] = '\0'; + s->start = s->str = s->ugbuf; + s->next = source; + source = s; + } +} + + +/* Called to get a char that isn't a \newline sequence. */ +static int +getsc_bn(void) +{ + int c, c2; + + if (ignore_backslash_newline) + return (o_getsc_u()); + + if (backslash_skip == 1) { + backslash_skip = 2; + return (o_getsc_u()); + } + + backslash_skip = 0; + + while (/* CONSTCOND */ 1) { + c = o_getsc_u(); + if (c == '\\') { + if ((c2 = o_getsc_u()) == '\n') + /* ignore the \newline; get the next char... */ + continue; + ungetsc_i(c2); + backslash_skip = 1; + } + return (c); + } +} + +void +yyskiputf8bom(void) +{ + int c; + + if (rtt2asc((c = o_getsc_u())) != 0xEF) { + ungetsc_i(c); + return; + } + if (rtt2asc((c = o_getsc_u())) != 0xBB) { + ungetsc_i(c); + ungetsc_i(asc2rtt(0xEF)); + return; + } + if (rtt2asc((c = o_getsc_u())) != 0xBF) { + ungetsc_i(c); + ungetsc_i(asc2rtt(0xBB)); + ungetsc_i(asc2rtt(0xEF)); + return; + } + UTFMODE |= 8; +} + +static Lex_state * +push_state_i(State_info *si, Lex_state *old_end) +{ + Lex_state *news = alloc2(STATE_BSIZE, sizeof(Lex_state), ATEMP); + + news[0].ls_base = old_end; + si->base = &news[0]; + si->end = &news[STATE_BSIZE]; + return (&news[1]); +} + +static Lex_state * +pop_state_i(State_info *si, Lex_state *old_end) +{ + Lex_state *old_base = si->base; + + si->base = old_end->ls_base - STATE_BSIZE; + si->end = old_end->ls_base; + + afree(old_base, ATEMP); + + return (si->base + STATE_BSIZE - 1); +} diff --git a/lksh.1 b/lksh.1 new file mode 100644 index 0000000..c3a82cb --- /dev/null +++ b/lksh.1 @@ -0,0 +1,319 @@ +.\" $MirOS: src/bin/mksh/lksh.1,v 1.23 2017/04/02 13:38:02 tg Exp $ +.\"- +.\" Copyright (c) 2008, 2009, 2010, 2012, 2013, 2015, 2016, 2017 +.\" mirabilos +.\" +.\" Provided that these terms and disclaimer and all copyright notices +.\" are retained or reproduced in an accompanying document, permission +.\" is granted to deal in this work without restriction, including un‐ +.\" limited rights to use, publicly perform, distribute, sell, modify, +.\" merge, give away, or sublicence. +.\" +.\" This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to +.\" the utmost extent permitted by applicable law, neither express nor +.\" implied; without malicious intent or gross negligence. In no event +.\" may a licensor, author or contributor be held liable for indirect, +.\" direct, other damage, loss, or other issues arising in any way out +.\" of dealing in the work, even if advised of the possibility of such +.\" damage or existence of a defect, except proven that it results out +.\" of said person’s immediate fault when using the work as intended. +.\"- +.\" Try to make GNU groff and AT&T nroff more compatible +.\" * ` generates ‘ in gnroff, so use \` +.\" * ' generates ’ in gnroff, \' generates ´, so use \*(aq +.\" * - generates ‐ in gnroff, \- generates −, so .tr it to - +.\" thus use - for hyphens and \- for minus signs and option dashes +.\" * ~ is size-reduced and placed atop in groff, so use \*(TI +.\" * ^ is size-reduced and placed atop in groff, so use \*(ha +.\" * \(en does not work in nroff, so use \*(en +.\" * <>| are problematic, so redefine and use \*(Lt\*(Gt\*(Ba +.\" Also make sure to use \& *before* a punctuation char that is to not +.\" be interpreted as punctuation, and especially with two-letter words +.\" but also (after) a period that does not end a sentence (“e.g.\&”). +.\" The section after the "doc" macropackage has been loaded contains +.\" additional code to convene between the UCB mdoc macropackage (and +.\" its variant as BSD mdoc in groff) and the GNU mdoc macropackage. +.\" +.ie \n(.g \{\ +. if \*[.T]ascii .tr \-\N'45' +. if \*[.T]latin1 .tr \-\N'45' +. if \*[.T]utf8 .tr \-\N'45' +. ds <= \[<=] +. ds >= \[>=] +. ds Rq \[rq] +. ds Lq \[lq] +. ds sL \(aq +. ds sR \(aq +. if \*[.T]utf8 .ds sL ` +. if \*[.T]ps .ds sL ` +. if \*[.T]utf8 .ds sR ' +. if \*[.T]ps .ds sR ' +. ds aq \(aq +. ds TI \(ti +. ds ha \(ha +. ds en \(en +.\} +.el \{\ +. ds aq ' +. ds TI ~ +. ds ha ^ +. ds en \(em +.\} +.\" +.\" Implement .Dd with the Mdocdate RCS keyword +.\" +.rn Dd xD +.de Dd +.ie \\$1$Mdocdate: \{\ +. xD \\$2 \\$3, \\$4 +.\} +.el .xD \\$1 \\$2 \\$3 \\$4 \\$5 \\$6 \\$7 \\$8 +.. +.\" +.\" .Dd must come before definition of .Mx, because when called +.\" with -mandoc, it might implement .Mx itself, but we want to +.\" use our own definition. And .Dd must come *first*, always. +.\" +.Dd $Mdocdate: April 2 2017 $ +.\" +.\" Check which macro package we use, and do other -mdoc setup. +.\" +.ie \n(.g \{\ +. if \*[.T]utf8 .tr \[la]\*(Lt +. if \*[.T]utf8 .tr \[ra]\*(Gt +. ie d volume-ds-1 .ds tT gnu +. el .ds tT bsd +.\} +.el .ds tT ucb +.\" +.\" Implement .Mx (MirBSD) +.\" +.ie "\*(tT"gnu" \{\ +. eo +. de Mx +. nr curr-font \n[.f] +. nr curr-size \n[.ps] +. ds str-Mx \f[\n[curr-font]]\s[\n[curr-size]u] +. ds str-Mx1 \*[Tn-font-size]\%MirOS\*[str-Mx] +. if !\n[arg-limit] \ +. if \n[.$] \{\ +. ds macro-name Mx +. parse-args \$@ +. \} +. if (\n[arg-limit] > \n[arg-ptr]) \{\ +. nr arg-ptr +1 +. ie (\n[type\n[arg-ptr]] == 2) \ +. as str-Mx1 \~\*[arg\n[arg-ptr]] +. el \ +. nr arg-ptr -1 +. \} +. ds arg\n[arg-ptr] "\*[str-Mx1] +. nr type\n[arg-ptr] 2 +. ds space\n[arg-ptr] "\*[space] +. nr num-args (\n[arg-limit] - \n[arg-ptr]) +. nr arg-limit \n[arg-ptr] +. if \n[num-args] \ +. parse-space-vector +. print-recursive +.. +. ec +. ds sP \s0 +. ds tN \*[Tn-font-size] +.\} +.el \{\ +. de Mx +. nr cF \\n(.f +. nr cZ \\n(.s +. ds aa \&\f\\n(cF\s\\n(cZ +. if \\n(aC==0 \{\ +. ie \\n(.$==0 \&MirOS\\*(aa +. el .aV \\$1 \\$2 \\$3 \\$4 \\$5 \\$6 \\$7 \\$8 \\$9 +. \} +. if \\n(aC>\\n(aP \{\ +. nr aP \\n(aP+1 +. ie \\n(C\\n(aP==2 \{\ +. as b1 \&MirOS\ #\&\\*(A\\n(aP\\*(aa +. ie \\n(aC>\\n(aP \{\ +. nr aP \\n(aP+1 +. nR +. \} +. el .aZ +. \} +. el \{\ +. as b1 \&MirOS\\*(aa +. nR +. \} +. \} +.. +.\} +.\"- +.Dt LKSH 1 +.Os MirBSD +.Sh NAME +.Nm lksh +.Nd Legacy Korn shell built on mksh +.Sh SYNOPSIS +.Nm +.Bk -words +.Op Fl +abCefhiklmnprUuvXx +.Op Fl +o Ar opt +.Oo +.Fl c Ar string \*(Ba +.Fl s \*(Ba +.Ar file +.Op Ar args ... +.Oc +.Ek +.Sh DESCRIPTION +.Nm +is a command interpreter intended exclusively for running legacy +shell scripts. +It is built on +.Nm mksh ; +refer to its manual page for details on the scripting language. +It is recommended to port scripts to +.Nm mksh +instead of relying on legacy or objectionable POSIX-mandated behaviour, +since the MirBSD Korn Shell scripting language is much more consistent. +.Pp +Do not use +.Nm +as an interactive or login shell; use +.Nm mksh +instead. +.Pp +Note that it's strongly recommended to invoke +.Nm +with +.Fl o Ic posix +to fully enjoy better compatibility to the +.Tn POSIX +standard (which is probably why you use +.Nm +over +.Nm mksh +in the first place); +.Fl o Ic sh +(possibly additionally to the above) may be needed for some legacy scripts. +.Sh LEGACY MODE +.Nm +currently has the following differences from +.Nm mksh : +.Bl -bullet +.It +The +.Ev KSH_VERSION +string identifies +.Nm +as +.Dq Li LEGACY KSH +instead of +.Dq Li MIRBSD KSH . +Note that the rest of the version string is identical between +the two shell flavours, and the behaviour and differences can +change between versions; see the accompanying manual page +.Xr mksh 1 +for the versions this document applies to. +.It +.Nm +uses +.Tn POSIX +arithmetic, which has quite a few implications: +The data type for arithmetic operations is the host +.Tn ISO +C +.Vt long +data type. +Signed integer wraparound is Undefined Behaviour; this means that... +.Bd -literal -offset indent +$ echo $((2147483647 + 1)) +.Ed +.Pp +\&... is permitted to, e.g. delete all files on your system +(the figure differs for non-32-bit systems, the rule doesn't). +The sign of the result of a modulo operation with at least one +negative operand is unspecified. +Shift operations on negative numbers are unspecified. +Division of the largest negative number by \-1 is Undefined Behaviour. +The compiler is permitted to delete all data and crash the system +if Undefined Behaviour occurs (see above for an example). +.It +The rotation arithmetic operators are not available. +.It +The shift arithmetic operators take all bits of the second operand into +account; if they exceed permitted precision, the result is unspecified. +.It +Unless +.Ic set -o posix +is active, +.Nm +always uses traditional mode for constructs like: +.Bd -literal -offset indent +$ set -- $(getopt ab:c "$@") +$ echo $? +.Ed +.Pp +POSIX mandates this to show 0, but traditional mode +passes through the errorlevel from the +.Xr getopt 1 +command. +.It +Functions defined with the +.Ic function +reserved word share the shell options +.Pq Ic set -o +instead of locally scoping them. +.El +.Sh SEE ALSO +.Xr mksh 1 +.Pp +.Pa http://www.mirbsd.org/mksh.htm +.Pp +.Pa http://www.mirbsd.org/ksh\-chan.htm +.Sh CAVEATS +To use +.Nm +as +.Pa /bin/sh , +compilation to enable +.Ic set -o posix +by default if called as +.Nm sh +.Pq adding Dv \-DMKSH_BINSHPOSIX to Dv CPPFLAGS +is highly recommended for better standards compliance. +.Pp +For better compatibility with legacy scripts, such as many +.Tn Debian +maintainer scripts, Upstart and SYSV init scripts, and other +unfixed scripts, also adding the +.Dv \-DMKSH_BINSHREDUCED +compile-time option to enable +.Em both +.Ic set -o posix -o sh +when the shell is run as +.Nm sh , +as well as integrating the optional disrecommended +.Xr printf 1 +builtin, might be necessary. +.Pp +.Nm +tries to make a cross between a legacy bourne/posix compatibl-ish +shell and a legacy pdksh-alike but +.Dq legacy +is not exactly specified. +.Pp +Talk to the +.Mx +development team using the mailing list at +.Aq miros\-mksh@mirbsd.org +or the +.Li \&#\&!/bin/mksh +.Pq or Li \&#ksh +IRC channel at +.Pa irc.freenode.net +.Pq Port 6697 SSL, 6667 unencrypted +if you need any further quirks or assistance, +and consider migrating your legacy scripts to work with +.Nm mksh +instead of requiring +.Nm . diff --git a/main.c b/main.c new file mode 100644 index 0000000..b7ac5a2 --- /dev/null +++ b/main.c @@ -0,0 +1,2078 @@ +/* $OpenBSD: main.c,v 1.57 2015/09/10 22:48:58 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.10 2014/08/10 02:44:26 guenther Exp $ */ +/* $OpenBSD: io.c,v 1.26 2015/09/11 08:00:27 guenther Exp $ */ +/* $OpenBSD: table.c,v 1.16 2015/09/01 13:12:31 tedu Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#define EXTERN +#include "sh.h" + +#if HAVE_LANGINFO_CODESET +#include +#endif +#if HAVE_SETLOCALE_CTYPE +#include +#endif + +__RCSID("$MirOS: src/bin/mksh/main.c,v 1.347 2018/01/13 21:45:07 tg Exp $"); + +#ifndef MKSHRC_PATH +#define MKSHRC_PATH "~/.mkshrc" +#endif + +#ifndef MKSH_DEFAULT_TMPDIR +#define MKSH_DEFAULT_TMPDIR MKSH_UNIXROOT "/tmp" +#endif + +static uint8_t isuc(const char *); +static int main_init(int, const char *[], Source **, struct block **); +void chvt_reinit(void); +static void reclaim(void); +static void remove_temps(struct temp *); +static mksh_uari_t rndsetup(void); +static void init_environ(void); +#ifdef SIGWINCH +static void x_sigwinch(int); +#endif + +static const char initsubs[] = + "${PS2=> }" + "${PS3=#? }" + "${PS4=+ }" + "${SECONDS=0}" + "${TMOUT=0}" + "${EPOCHREALTIME=}"; + +static const char *initcoms[] = { + Ttypeset, "-r", initvsn, NULL, + Ttypeset, "-x", "HOME", TPATH, TSHELL, NULL, + Ttypeset, "-i10", "COLUMNS", "LINES", "SECONDS", "TMOUT", NULL, + Talias, + "integer=\\\\builtin typeset -i", + "local=\\\\builtin typeset", + /* not "alias -t --": hash -r needs to work */ + "hash=\\\\builtin alias -t", + "type=\\\\builtin whence -v", + "autoload=\\\\builtin typeset -fu", + "functions=\\\\builtin typeset -f", + "history=\\\\builtin fc -l", + "nameref=\\\\builtin typeset -n", + "nohup=nohup ", + "r=\\\\builtin fc -e -", + "login=\\\\builtin exec login", + NULL, + /* this is what AT&T ksh seems to track, with the addition of emacs */ + Talias, "-tU", + Tcat, "cc", "chmod", "cp", "date", "ed", "emacs", "grep", "ls", + "make", "mv", "pr", "rm", "sed", Tsh, "vi", "who", NULL, + NULL +}; + +static const char *restr_com[] = { + Ttypeset, "-r", TPATH, "ENV", TSHELL, NULL +}; + +static bool initio_done; + +/* top-level parsing and execution environment */ +static struct env env; +struct env *e = &env; + +/* compile-time assertions */ +#define cta(name, expr) struct cta_ ## name { char t[(expr) ? 1 : -1]; } + +/* this one should be defined by the standard */ +cta(char_is_1_char, (sizeof(char) == 1) && (sizeof(signed char) == 1) && + (sizeof(unsigned char) == 1)); +cta(char_is_8_bits, ((CHAR_BIT) == 8) && ((int)(unsigned char)0xFF == 0xFF) && + ((int)(unsigned char)0x100 == 0) && ((int)(unsigned char)(int)-1 == 0xFF)); +/* the next assertion is probably not really needed */ +cta(short_is_2_char, sizeof(short) == 2); +cta(short_size_no_matter_of_signedness, sizeof(short) == sizeof(unsigned short)); +/* the next assertion is probably not really needed */ +cta(int_is_4_char, sizeof(int) == 4); +cta(int_size_no_matter_of_signedness, sizeof(int) == sizeof(unsigned int)); + +cta(long_ge_int, sizeof(long) >= sizeof(int)); +cta(long_size_no_matter_of_signedness, sizeof(long) == sizeof(unsigned long)); + +#ifndef MKSH_LEGACY_MODE +/* the next assertion is probably not really needed */ +cta(ari_is_4_char, sizeof(mksh_ari_t) == 4); +/* but this is */ +cta(ari_has_31_bit, 0 < (mksh_ari_t)(((((mksh_ari_t)1 << 15) << 15) - 1) * 2 + 1)); +/* the next assertion is probably not really needed */ +cta(uari_is_4_char, sizeof(mksh_uari_t) == 4); +/* but the next three are; we REQUIRE unsigned integer wraparound */ +cta(uari_has_31_bit, 0 < (mksh_uari_t)(((((mksh_uari_t)1 << 15) << 15) - 1) * 2 + 1)); +cta(uari_has_32_bit, 0 < (mksh_uari_t)(((((mksh_uari_t)1 << 15) << 15) - 1) * 4 + 3)); +cta(uari_wrap_32_bit, + (mksh_uari_t)(((((mksh_uari_t)1 << 15) << 15) - 1) * 4 + 3) > + (mksh_uari_t)(((((mksh_uari_t)1 << 15) << 15) - 1) * 4 + 4)); +#endif +/* these are always required */ +cta(ari_is_signed, (mksh_ari_t)-1 < (mksh_ari_t)0); +cta(uari_is_unsigned, (mksh_uari_t)-1 > (mksh_uari_t)0); +/* we require these to have the precisely same size and assume 2s complement */ +cta(ari_size_no_matter_of_signedness, sizeof(mksh_ari_t) == sizeof(mksh_uari_t)); + +cta(sizet_size_no_matter_of_signedness, sizeof(ssize_t) == sizeof(size_t)); +cta(sizet_voidptr_same_size, sizeof(size_t) == sizeof(void *)); +cta(sizet_funcptr_same_size, sizeof(size_t) == sizeof(void (*)(void))); +/* our formatting routines assume this */ +cta(ptr_fits_in_long, sizeof(size_t) <= sizeof(long)); +cta(ari_fits_in_long, sizeof(mksh_ari_t) <= sizeof(long)); + +static mksh_uari_t +rndsetup(void) +{ + register uint32_t h; + struct { + ALLOC_ITEM alloc_INT; + void *dataptr, *stkptr, *mallocptr; +#if defined(__GLIBC__) && (__GLIBC__ >= 2) + sigjmp_buf jbuf; +#endif + struct timeval tv; + } *bufptr; + char *cp; + + cp = alloc(sizeof(*bufptr) - sizeof(ALLOC_ITEM), APERM); + /* clear the allocated space, for valgrind and to avoid UB */ + memset(cp, 0, sizeof(*bufptr) - sizeof(ALLOC_ITEM)); + /* undo what alloc() did to the malloc result address */ + bufptr = (void *)(cp - sizeof(ALLOC_ITEM)); + /* PIE or something similar provides us with deltas here */ + bufptr->dataptr = &rndsetupstate; + /* ASLR in at least Windows, Linux, some BSDs */ + bufptr->stkptr = &bufptr; + /* randomised malloc in BSD (and possibly others) */ + bufptr->mallocptr = bufptr; +#if defined(__GLIBC__) && (__GLIBC__ >= 2) + /* glibc pointer guard */ + sigsetjmp(bufptr->jbuf, 1); +#endif + /* introduce variation (and yes, second arg MBZ for portability) */ + mksh_TIME(bufptr->tv); + +#ifdef MKSH_ALLOC_CATCH_UNDERRUNS + mprotect(((char *)bufptr) + 4096, 4096, PROT_READ | PROT_WRITE); +#endif + h = chvt_rndsetup(bufptr, sizeof(*bufptr)); + + afree(cp, APERM); + return ((mksh_uari_t)h); +} + +void +chvt_reinit(void) +{ + kshpid = procpid = getpid(); + ksheuid = geteuid(); + kshpgrp = getpgrp(); + kshppid = getppid(); +} + +static const char *empty_argv[] = { + Tmksh, NULL +}; + +static uint8_t +isuc(const char *cx) { + char *cp, *x; + uint8_t rv = 0; + + if (!cx || !*cx) + return (0); + + /* uppercase a string duplicate */ + strdupx(x, cx, ATEMP); + cp = x; + while ((*cp = ksh_toupper(*cp))) + ++cp; + + /* check for UTF-8 */ + if (strstr(x, "UTF-8") || strstr(x, "UTF8")) + rv = 1; + + /* free copy and out */ + afree(x, ATEMP); + return (rv); +} + +static int +main_init(int argc, const char *argv[], Source **sp, struct block **lp) +{ + int argi, i; + Source *s = NULL; + struct block *l; + unsigned char restricted_shell, errexit, utf_flag; + char *cp; + const char *ccp, **wp; + struct tbl *vp; + struct stat s_stdin; +#if !defined(_PATH_DEFPATH) && defined(_CS_PATH) + ssize_t k; +#endif + +#if defined(MKSH_EBCDIC) || defined(MKSH_FAUX_EBCDIC) + ebcdic_init(); +#endif + set_ifs(TC_IFSWS); + +#ifdef __OS2__ + os2_init(&argc, &argv); +#endif + + /* do things like getpgrp() et al. */ + chvt_reinit(); + + /* make sure argv[] is sane, for weird OSes */ + if (!*argv) { + argv = empty_argv; + argc = 1; + } + kshname = argv[0]; + + /* initialise permanent Area */ + ainit(&aperm); + /* max. name length: -2147483648 = 11 (+ NUL) */ + vtemp = alloc(offsetof(struct tbl, name[0]) + 12, APERM); + + /* set up base environment */ + env.type = E_NONE; + ainit(&env.area); + /* set up global l->vars and l->funs */ + newblock(); + + /* Do this first so output routines (eg, errorf, shellf) can work */ + initio(); + + /* determine the basename (without '-' or path) of the executable */ + ccp = kshname; + goto begin_parsing_kshname; + while ((i = ccp[argi++])) { + if (mksh_cdirsep(i)) { + ccp += argi; + begin_parsing_kshname: + argi = 0; + if (*ccp == '-') + ++ccp; + } + } + if (!*ccp) + ccp = empty_argv[0]; + + /* + * Turn on nohup by default. (AT&T ksh does not have a nohup + * option - it always sends the hup). + */ + Flag(FNOHUP) = 1; + + /* + * Turn on brace expansion by default. AT&T kshs that have + * alternation always have it on. + */ + Flag(FBRACEEXPAND) = 1; + + /* + * Turn on "set -x" inheritance by default. + */ + Flag(FXTRACEREC) = 1; + + /* define built-in commands and see if we were called as one */ + ktinit(APERM, &builtins, + /* currently up to 54 builtins: 75% of 128 = 2^7 */ + 7); + for (i = 0; mkshbuiltins[i].name != NULL; i++) + if (!strcmp(ccp, builtin(mkshbuiltins[i].name, + mkshbuiltins[i].func))) + Flag(FAS_BUILTIN) = 1; + + if (!Flag(FAS_BUILTIN)) { + /* check for -T option early */ + argi = parse_args(argv, OF_FIRSTTIME, NULL); + if (argi < 0) + return (1); + +#if defined(MKSH_BINSHPOSIX) || defined(MKSH_BINSHREDUCED) + /* are we called as -sh or /bin/sh or so? */ + if (!strcmp(ccp, "sh" MKSH_EXE_EXT)) { + /* either also turns off braceexpand */ +#ifdef MKSH_BINSHPOSIX + /* enable better POSIX conformance */ + change_flag(FPOSIX, OF_FIRSTTIME, true); +#endif +#ifdef MKSH_BINSHREDUCED + /* enable kludge/compat mode */ + change_flag(FSH, OF_FIRSTTIME, true); +#endif + } +#endif + } + + initvar(); + + inittraps(); + + coproc_init(); + + /* set up variable and command dictionaries */ + ktinit(APERM, &taliases, 0); + ktinit(APERM, &aliases, 0); +#ifndef MKSH_NOPWNAM + ktinit(APERM, &homedirs, 0); +#endif + + /* define shell keywords */ + initkeywords(); + + init_histvec(); + + /* initialise tty size before importing environment */ + change_winsz(); + +#ifdef _PATH_DEFPATH + def_path = _PATH_DEFPATH; +#else +#ifdef _CS_PATH + if ((k = confstr(_CS_PATH, NULL, 0)) > 0 && + confstr(_CS_PATH, cp = alloc(k + 1, APERM), k + 1) == k + 1) + def_path = cp; + else +#endif + /* + * this is uniform across all OSes unless it + * breaks somewhere hard; don't try to optimise, + * e.g. add stuff for Interix or remove /usr + * for HURD, because e.g. Debian GNU/HURD is + * "keeping a regular /usr"; this is supposed + * to be a sane 'basic' default PATH + */ + def_path = MKSH_UNIXROOT "/bin" MKSH_PATHSEPS + MKSH_UNIXROOT "/usr/bin" MKSH_PATHSEPS + MKSH_UNIXROOT "/sbin" MKSH_PATHSEPS + MKSH_UNIXROOT "/usr/sbin"; +#endif + + /* + * Set PATH to def_path (will set the path global variable). + * (import of environment below will probably change this setting). + */ + vp = global(TPATH); + /* setstr can't fail here */ + setstr(vp, def_path, KSH_RETURN_ERROR); + +#ifndef MKSH_NO_CMDLINE_EDITING + /* + * Set edit mode to emacs by default, may be overridden + * by the environment or the user. Also, we want tab completion + * on in vi by default. + */ + change_flag(FEMACS, OF_SPECIAL, true); +#if !MKSH_S_NOVI + Flag(FVITABCOMPLETE) = 1; +#endif +#endif + + /* import environment */ + init_environ(); + + /* for security */ + typeset(TinitIFS, 0, 0, 0, 0); + + /* assign default shell variable values */ + typeset("PATHSEP=" MKSH_PATHSEPS, 0, 0, 0, 0); + substitute(initsubs, 0); + + /* Figure out the current working directory and set $PWD */ + vp = global(TPWD); + cp = str_val(vp); + /* Try to use existing $PWD if it is valid */ + set_current_wd((mksh_abspath(cp) && test_eval(NULL, TO_FILEQ, cp, + Tdot, true)) ? cp : NULL); + if (current_wd[0]) + simplify_path(current_wd); + /* Only set pwd if we know where we are or if it had a bogus value */ + if (current_wd[0] || *cp) + /* setstr can't fail here */ + setstr(vp, current_wd, KSH_RETURN_ERROR); + + for (wp = initcoms; *wp != NULL; wp++) { + c_builtin(wp); + while (*wp != NULL) + wp++; + } + setint_n(global("OPTIND"), 1, 10); + + kshuid = getuid(); + kshgid = getgid(); + kshegid = getegid(); + + safe_prompt = ksheuid ? "$ " : "# "; + vp = global("PS1"); + /* Set PS1 if unset or we are root and prompt doesn't contain a # */ + if (!(vp->flag & ISSET) || + (!ksheuid && !strchr(str_val(vp), '#'))) + /* setstr can't fail here */ + setstr(vp, safe_prompt, KSH_RETURN_ERROR); + setint_n((vp = global("BASHPID")), 0, 10); + vp->flag |= INT_U; + setint_n((vp = global("PGRP")), (mksh_uari_t)kshpgrp, 10); + vp->flag |= INT_U; + setint_n((vp = global("PPID")), (mksh_uari_t)kshppid, 10); + vp->flag |= INT_U; + setint_n((vp = global("USER_ID")), (mksh_uari_t)ksheuid, 10); + vp->flag |= INT_U; + setint_n((vp = global("KSHUID")), (mksh_uari_t)kshuid, 10); + vp->flag |= INT_U; + setint_n((vp = global("KSHEGID")), (mksh_uari_t)kshegid, 10); + vp->flag |= INT_U; + setint_n((vp = global("KSHGID")), (mksh_uari_t)kshgid, 10); + vp->flag |= INT_U; + setint_n((vp = global("RANDOM")), rndsetup(), 10); + vp->flag |= INT_U; + setint_n((vp_pipest = global("PIPESTATUS")), 0, 10); + + /* Set this before parsing arguments */ + Flag(FPRIVILEGED) = (kshuid != ksheuid || kshgid != kshegid) ? 2 : 0; + + /* this to note if monitor is set on command line (see below) */ +#ifndef MKSH_UNEMPLOYED + Flag(FMONITOR) = 127; +#endif + /* this to note if utf-8 mode is set on command line (see below) */ + UTFMODE = 2; + + if (!Flag(FAS_BUILTIN)) { + argi = parse_args(argv, OF_CMDLINE, NULL); + if (argi < 0) + return (1); + } + + /* process this later only, default to off (hysterical raisins) */ + utf_flag = UTFMODE; + UTFMODE = 0; + + if (Flag(FAS_BUILTIN)) { + /* auto-detect from environment variables, always */ + utf_flag = 3; + } else if (Flag(FCOMMAND)) { + s = pushs(SSTRINGCMDLINE, ATEMP); + if (!(s->start = s->str = argv[argi++])) + errorf(Tf_optfoo, "", "", 'c', Treq_arg); + while (*s->str) { + if (ctype(*s->str, C_QUOTE)) + break; + s->str++; + } + if (!*s->str) + s->flags |= SF_MAYEXEC; + s->str = s->start; +#ifdef MKSH_MIDNIGHTBSD01ASH_COMPAT + /* compatibility to MidnightBSD 0.1 /bin/sh (kludge) */ + if (Flag(FSH) && argv[argi] && !strcmp(argv[argi], "--")) + ++argi; +#endif + if (argv[argi]) + kshname = argv[argi++]; + } else if (argi < argc && !Flag(FSTDIN)) { + s = pushs(SFILE, ATEMP); +#ifdef __OS2__ + /* + * A bug in OS/2 extproc (like shebang) handling makes + * it not pass the full pathname of a script, so we need + * to search for it. This changes the behaviour of a + * simple "mksh foo", but can't be helped. + */ + s->file = argv[argi++]; + if (search_access(s->file, X_OK) != 0) + s->file = search_path(s->file, path, X_OK, NULL); + if (!s->file || !*s->file) + s->file = argv[argi - 1]; +#else + s->file = argv[argi++]; +#endif + s->u.shf = shf_open(s->file, O_RDONLY, 0, + SHF_MAPHI | SHF_CLEXEC); + if (s->u.shf == NULL) { + shl_stdout_ok = false; + warningf(true, Tf_sD_s, s->file, cstrerror(errno)); + /* mandated by SUSv4 */ + exstat = 127; + unwind(LERROR); + } + kshname = s->file; + } else { + Flag(FSTDIN) = 1; + s = pushs(SSTDIN, ATEMP); + s->file = ""; + s->u.shf = shf_fdopen(0, SHF_RD | can_seek(0), + NULL); + if (isatty(0) && isatty(2)) { + Flag(FTALKING) = Flag(FTALKING_I) = 1; + /* The following only if isatty(0) */ + s->flags |= SF_TTY; + s->u.shf->flags |= SHF_INTERRUPT; + s->file = NULL; + } + } + + /* this bizarreness is mandated by POSIX */ + if (fstat(0, &s_stdin) >= 0 && S_ISCHR(s_stdin.st_mode) && + Flag(FTALKING)) + reset_nonblock(0); + + /* initialise job control */ + j_init(); + /* do this after j_init() which calls tty_init_state() */ + if (Flag(FTALKING)) { + if (utf_flag == 2) { +#ifndef MKSH_ASSUME_UTF8 + /* auto-detect from locale or environment */ + utf_flag = 4; +#else /* this may not be an #elif */ +#if MKSH_ASSUME_UTF8 + utf_flag = 1; +#else + /* always disable UTF-8 (for interactive) */ + utf_flag = 0; +#endif +#endif + } +#ifndef MKSH_NO_CMDLINE_EDITING + x_init(); +#endif + } + +#ifdef SIGWINCH + sigtraps[SIGWINCH].flags |= TF_SHELL_USES; + setsig(&sigtraps[SIGWINCH], x_sigwinch, + SS_RESTORE_ORIG|SS_FORCE|SS_SHTRAP); +#endif + + l = e->loc; + if (Flag(FAS_BUILTIN)) { + l->argc = argc; + l->argv = argv; + l->argv[0] = ccp; + } else { + l->argc = argc - argi; + /* + * allocate a new array because otherwise, when we modify + * it in-place, ps(1) output changes; the meaning of argc + * here is slightly different as it excludes kshname, and + * we add a trailing NULL sentinel as well + */ + l->argv = alloc2(l->argc + 2, sizeof(void *), APERM); + l->argv[0] = kshname; + memcpy(&l->argv[1], &argv[argi], l->argc * sizeof(void *)); + l->argv[l->argc + 1] = NULL; + getopts_reset(1); + } + + /* divine the initial state of the utf8-mode Flag */ + ccp = null; + switch (utf_flag) { + + /* auto-detect from locale or environment */ + case 4: +#if HAVE_SETLOCALE_CTYPE + ccp = setlocale(LC_CTYPE, ""); +#if HAVE_LANGINFO_CODESET + if (!isuc(ccp)) + ccp = nl_langinfo(CODESET); +#endif + if (!isuc(ccp)) + ccp = null; +#endif + /* FALLTHROUGH */ + + /* auto-detect from environment */ + case 3: + /* these were imported from environ earlier */ + if (ccp == null) + ccp = str_val(global("LC_ALL")); + if (ccp == null) + ccp = str_val(global("LC_CTYPE")); + if (ccp == null) + ccp = str_val(global("LANG")); + UTFMODE = isuc(ccp); + break; + + /* not set on command line, not FTALKING */ + case 2: + /* unknown values */ + default: + utf_flag = 0; + /* FALLTHROUGH */ + + /* known values */ + case 1: + case 0: + UTFMODE = utf_flag; + break; + } + + /* Disable during .profile/ENV reading */ + restricted_shell = Flag(FRESTRICTED); + Flag(FRESTRICTED) = 0; + errexit = Flag(FERREXIT); + Flag(FERREXIT) = 0; + + /* + * Do this before profile/$ENV so that if it causes problems in them, + * user will know why things broke. + */ + if (!current_wd[0] && Flag(FTALKING)) + warningf(false, "can't determine current directory"); + + if (Flag(FLOGIN)) + include(MKSH_SYSTEM_PROFILE, 0, NULL, true); + if (!Flag(FPRIVILEGED)) { + if (Flag(FLOGIN)) + include(substitute("$HOME/.profile", 0), 0, NULL, true); + if (Flag(FTALKING)) { + cp = substitute("${ENV:-" MKSHRC_PATH "}", DOTILDE); + if (cp[0] != '\0') + include(cp, 0, NULL, true); + } + } else { + include(MKSH_SUID_PROFILE, 0, NULL, true); + /* turn off -p if not set explicitly */ + if (Flag(FPRIVILEGED) != 1) + change_flag(FPRIVILEGED, OF_INTERNAL, false); + } + + if (restricted_shell) { + c_builtin(restr_com); + /* After typeset command... */ + Flag(FRESTRICTED) = 1; + } + Flag(FERREXIT) = errexit; + + if (Flag(FTALKING) && s) + hist_init(s); + else + /* set after ENV */ + Flag(FTRACKALL) = 1; + + alarm_init(); + + *sp = s; + *lp = l; + return (0); +} + +/* this indirection barrier reduces stack usage during normal operation */ + +int +main(int argc, const char *argv[]) +{ + int rv; + Source *s; + struct block *l; + + if ((rv = main_init(argc, argv, &s, &l)) == 0) { + if (Flag(FAS_BUILTIN)) { + rv = c_builtin(l->argv); + } else { + shell(s, 0); + /* NOTREACHED */ + } + } + return (rv); +} + +int +include(const char *name, int argc, const char **argv, bool intr_ok) +{ + Source *volatile s = NULL; + struct shf *shf; + const char **volatile old_argv; + volatile int old_argc; + int i; + + shf = shf_open(name, O_RDONLY, 0, SHF_MAPHI | SHF_CLEXEC); + if (shf == NULL) + return (-1); + + if (argv) { + old_argv = e->loc->argv; + old_argc = e->loc->argc; + } else { + old_argv = NULL; + old_argc = 0; + } + newenv(E_INCL); + if ((i = kshsetjmp(e->jbuf))) { + quitenv(s ? s->u.shf : NULL); + if (old_argv) { + e->loc->argv = old_argv; + e->loc->argc = old_argc; + } + switch (i) { + case LRETURN: + case LERROR: + /* see below */ + return (exstat & 0xFF); + case LINTR: + /* + * intr_ok is set if we are including .profile or $ENV. + * If user ^Cs out, we don't want to kill the shell... + */ + if (intr_ok && ((exstat & 0xFF) - 128) != SIGTERM) + return (1); + /* FALLTHROUGH */ + case LEXIT: + case LLEAVE: + case LSHELL: + unwind(i); + /* NOTREACHED */ + default: + internal_errorf(Tunexpected_type, Tunwind, Tsource, i); + /* NOTREACHED */ + } + } + if (argv) { + e->loc->argv = argv; + e->loc->argc = argc; + } + s = pushs(SFILE, ATEMP); + s->u.shf = shf; + strdupx(s->file, name, ATEMP); + i = shell(s, 1); + quitenv(s->u.shf); + if (old_argv) { + e->loc->argv = old_argv; + e->loc->argc = old_argc; + } + /* & 0xff to ensure value not -1 */ + return (i & 0xFF); +} + +/* spawn a command into a shell optionally keeping track of the line number */ +int +command(const char *comm, int line) +{ + Source *s, *sold = source; + int rv; + + s = pushs(SSTRING, ATEMP); + s->start = s->str = comm; + s->line = line; + rv = shell(s, 1); + source = sold; + return (rv); +} + +/* + * run the commands from the input source, returning status. + */ +int +shell(Source * volatile s, volatile int level) +{ + struct op *t; + volatile bool wastty = tobool(s->flags & SF_TTY); + volatile uint8_t attempts = 13; + volatile bool interactive = (level == 0) && Flag(FTALKING); + volatile bool sfirst = true; + Source *volatile old_source = source; + int i; + + newenv(level == 2 ? E_EVAL : E_PARSE); + if (interactive) + really_exit = false; + switch ((i = kshsetjmp(e->jbuf))) { + case 0: + break; + case LBREAK: + case LCONTIN: + if (level != 2) { + source = old_source; + quitenv(NULL); + internal_errorf(Tf_cant_s, Tshell, + i == LBREAK ? Tbreak : Tcontinue); + /* NOTREACHED */ + } + /* assert: interactive == false */ + /* FALLTHROUGH */ + case LINTR: + /* we get here if SIGINT not caught or ignored */ + case LERROR: + case LSHELL: + if (interactive) { + if (i == LINTR) + shellf("\n"); + /* + * Reset any eof that was read as part of a + * multiline command. + */ + if (Flag(FIGNOREEOF) && s->type == SEOF && wastty) + s->type = SSTDIN; + /* + * Used by exit command to get back to + * top level shell. Kind of strange since + * interactive is set if we are reading from + * a tty, but to have stopped jobs, one only + * needs FMONITOR set (not FTALKING/SF_TTY)... + */ + /* toss any input we have so far */ + yyrecursive_pop(true); + s->start = s->str = null; + retrace_info = NULL; + herep = heres; + break; + } + /* FALLTHROUGH */ + case LEXIT: + case LLEAVE: + case LRETURN: + source = old_source; + quitenv(NULL); + /* keep on going */ + unwind(i); + /* NOTREACHED */ + default: + source = old_source; + quitenv(NULL); + internal_errorf(Tunexpected_type, Tunwind, Tshell, i); + /* NOTREACHED */ + } + while (/* CONSTCOND */ 1) { + if (trap) + runtraps(0); + + if (s->next == NULL) { + if (Flag(FVERBOSE)) + s->flags |= SF_ECHO; + else + s->flags &= ~SF_ECHO; + } + if (interactive) { + j_notify(); + set_prompt(PS1, s); + } + t = compile(s, sfirst, true); + if (interactive) + histsave(&s->line, NULL, HIST_FLUSH, true); + sfirst = false; + if (!t) + goto source_no_tree; + if (t->type == TEOF) { + if (wastty && Flag(FIGNOREEOF) && --attempts > 0) { + shellf("Use 'exit' to leave mksh\n"); + s->type = SSTDIN; + } else if (wastty && !really_exit && + j_stopped_running()) { + really_exit = true; + s->type = SSTDIN; + } else { + /* + * this for POSIX which says EXIT traps + * shall be taken in the environment + * immediately after the last command + * executed. + */ + if (level == 0) + unwind(LEXIT); + break; + } + } else if ((s->flags & SF_MAYEXEC) && t->type == TCOM) + t->u.evalflags |= DOTCOMEXEC; + if (!Flag(FNOEXEC) || (s->flags & SF_TTY)) + exstat = execute(t, 0, NULL) & 0xFF; + + if (t->type != TEOF && interactive && really_exit) + really_exit = false; + + source_no_tree: + reclaim(); + } + quitenv(NULL); + source = old_source; + return (exstat & 0xFF); +} + +/* return to closest error handler or shell(), exit if none found */ +/* note: i MUST NOT be 0 */ +void +unwind(int i) +{ + /* + * This is a kludge. We need to restore everything that was + * changed in the new environment, see cid 1005090337C7A669439 + * and 10050903386452ACBF1, but fail to even save things most of + * the time. funcs.c:c_eval() changes FERREXIT temporarily to 0, + * which needs to be restored thus (related to Debian #696823). + * We did not save the shell flags, so we use a special or'd + * value here... this is mostly to clean up behind *other* + * callers of unwind(LERROR) here; exec.c has the regular case. + */ + if (Flag(FERREXIT) & 0x80) { + /* GNU bash does not run this trapsig */ + trapsig(ksh_SIGERR); + Flag(FERREXIT) &= ~0x80; + } + + /* ordering for EXIT vs ERR is a bit odd (this is what AT&T ksh does) */ + if (i == LEXIT || ((i == LERROR || i == LINTR) && + sigtraps[ksh_SIGEXIT].trap && + (!Flag(FTALKING) || Flag(FERREXIT)))) { + ++trap_nested; + runtrap(&sigtraps[ksh_SIGEXIT], trap_nested == 1); + --trap_nested; + i = LLEAVE; + } else if (Flag(FERREXIT) == 1 && (i == LERROR || i == LINTR)) { + ++trap_nested; + runtrap(&sigtraps[ksh_SIGERR], trap_nested == 1); + --trap_nested; + i = LLEAVE; + } + + while (/* CONSTCOND */ 1) { + switch (e->type) { + case E_PARSE: + case E_FUNC: + case E_INCL: + case E_LOOP: + case E_ERRH: + case E_EVAL: + kshlongjmp(e->jbuf, i); + /* NOTREACHED */ + case E_NONE: + if (i == LINTR) + e->flags |= EF_FAKE_SIGDIE; + /* FALLTHROUGH */ + default: + quitenv(NULL); + } + } +} + +void +newenv(int type) +{ + struct env *ep; + char *cp; + + /* + * struct env includes ALLOC_ITEM for alignment constraints + * so first get the actually used memory, then assign it + */ + cp = alloc(sizeof(struct env) - sizeof(ALLOC_ITEM), ATEMP); + /* undo what alloc() did to the malloc result address */ + ep = (void *)(cp - sizeof(ALLOC_ITEM)); + /* initialise public members of struct env (not the ALLOC_ITEM) */ + ainit(&ep->area); + ep->oenv = e; + ep->loc = e->loc; + ep->savefd = NULL; + ep->temps = NULL; + ep->yyrecursive_statep = NULL; + ep->type = type; + ep->flags = 0; + /* jump buffer is invalid because flags == 0 */ + e = ep; +} + +void +quitenv(struct shf *shf) +{ + struct env *ep = e; + char *cp; + int fd; + + yyrecursive_pop(true); + while (ep->oenv && ep->oenv->loc != ep->loc) + popblock(); + if (ep->savefd != NULL) { + for (fd = 0; fd < NUFILE; fd++) + /* if ep->savefd[fd] < 0, means fd was closed */ + if (ep->savefd[fd]) + restfd(fd, ep->savefd[fd]); + if (ep->savefd[2]) + /* Clear any write errors */ + shf_reopen(2, SHF_WR, shl_out); + } + /* + * Bottom of the stack. + * Either main shell is exiting or cleanup_parents_env() was called. + */ + if (ep->oenv == NULL) { +#ifdef DEBUG_LEAKS + int i; +#endif + + if (ep->type == E_NONE) { + /* Main shell exiting? */ +#if HAVE_PERSISTENT_HISTORY + if (Flag(FTALKING)) + hist_finish(); +#endif + j_exit(); + if (ep->flags & EF_FAKE_SIGDIE) { + int sig = (exstat & 0xFF) - 128; + + /* + * ham up our death a bit (AT&T ksh + * only seems to do this for SIGTERM) + * Don't do it for SIGQUIT, since we'd + * dump a core.. + */ + if ((sig == SIGINT || sig == SIGTERM) && + (kshpgrp == kshpid)) { + setsig(&sigtraps[sig], SIG_DFL, + SS_RESTORE_CURR | SS_FORCE); + kill(0, sig); + } + } + } + if (shf) + shf_close(shf); + reclaim(); +#ifdef DEBUG_LEAKS +#ifndef MKSH_NO_CMDLINE_EDITING + x_done(); +#endif +#ifndef MKSH_NOPROSPECTOFWORK + /* block at least SIGCHLD during/after afreeall */ + sigprocmask(SIG_BLOCK, &sm_sigchld, NULL); +#endif + afreeall(APERM); + for (fd = 3; fd < NUFILE; fd++) + if ((i = fcntl(fd, F_GETFD, 0)) != -1 && + (i & FD_CLOEXEC)) + close(fd); + close(2); + close(1); + close(0); +#endif + exit(exstat & 0xFF); + } + if (shf) + shf_close(shf); + reclaim(); + + e = e->oenv; + + /* free the struct env - tricky due to the ALLOC_ITEM inside */ + cp = (void *)ep; + afree(cp + sizeof(ALLOC_ITEM), ATEMP); +} + +/* Called after a fork to cleanup stuff left over from parents environment */ +void +cleanup_parents_env(void) +{ + struct env *ep; + int fd; + + /* + * Don't clean up temporary files - parent will probably need them. + * Also, can't easily reclaim memory since variables, etc. could be + * anywhere. + */ + + /* close all file descriptors hiding in savefd */ + for (ep = e; ep; ep = ep->oenv) { + if (ep->savefd) { + for (fd = 0; fd < NUFILE; fd++) + if (ep->savefd[fd] > 0) + close(ep->savefd[fd]); + afree(ep->savefd, &ep->area); + ep->savefd = NULL; + } +#ifdef DEBUG_LEAKS + if (ep->type != E_NONE) + ep->type = E_GONE; +#endif + } +#ifndef DEBUG_LEAKS + e->oenv = NULL; +#endif +} + +/* Called just before an execve cleanup stuff temporary files */ +void +cleanup_proc_env(void) +{ + struct env *ep; + + for (ep = e; ep; ep = ep->oenv) + remove_temps(ep->temps); +} + +/* remove temp files and free ATEMP Area */ +static void +reclaim(void) +{ + struct block *l; + + while ((l = e->loc) && (!e->oenv || e->oenv->loc != l)) { + e->loc = l->next; + afreeall(&l->area); + } + + remove_temps(e->temps); + e->temps = NULL; + + /* + * if the memory backing source is reclaimed, things + * will end up badly when a function expecting it to + * be valid is run; a NULL pointer is easily debugged + */ + if (source && source->areap == &e->area) + source = NULL; + afreeall(&e->area); +} + +static void +remove_temps(struct temp *tp) +{ + while (tp) { + if (tp->pid == procpid) + unlink(tp->tffn); + tp = tp->next; + } +} + +/* + * Initialise tty_fd. Used for tracking the size of the terminal, + * saving/resetting tty modes upon forground job completion, and + * for setting up the tty process group. Return values: + * 0 = got controlling tty + * 1 = got terminal but no controlling tty + * 2 = cannot find a terminal + * 3 = cannot dup fd + * 4 = cannot make fd close-on-exec + * An existing tty_fd is cached if no "better" one could be found, + * i.e. if tty_devtty was already set or the new would not set it. + */ +int +tty_init_fd(void) +{ + int fd, rv, eno = 0; + bool do_close = false, is_devtty = true; + + if (tty_devtty) { + /* already got a tty which is /dev/tty */ + return (0); + } + +#ifdef _UWIN + /*XXX imake style */ + if (isatty(3)) { + /* fd 3 on UWIN _is_ /dev/tty (or our controlling tty) */ + fd = 3; + goto got_fd; + } +#endif + if ((fd = open(T_devtty, O_RDWR, 0)) >= 0) { + do_close = true; + goto got_fd; + } + eno = errno; + + if (tty_fd >= 0) { + /* already got a non-devtty one */ + rv = 1; + goto out; + } + is_devtty = false; + + if (isatty((fd = 0)) || isatty((fd = 2))) + goto got_fd; + /* cannot find one */ + rv = 2; + /* assert: do_close == false */ + goto out; + + got_fd: + if ((rv = fcntl(fd, F_DUPFD, FDBASE)) < 0) { + eno = errno; + rv = 3; + goto out; + } + if (fcntl(rv, F_SETFD, FD_CLOEXEC) < 0) { + eno = errno; + close(rv); + rv = 4; + goto out; + } + tty_fd = rv; + tty_devtty = is_devtty; + rv = eno = 0; + out: + if (do_close) + close(fd); + errno = eno; + return (rv); +} + +/* A shell error occurred (eg, syntax error, etc.) */ + +#define VWARNINGF_ERRORPREFIX 1 +#define VWARNINGF_FILELINE 2 +#define VWARNINGF_BUILTIN 4 +#define VWARNINGF_INTERNAL 8 + +static void vwarningf(unsigned int, const char *, va_list) + MKSH_A_FORMAT(__printf__, 2, 0); + +static void +vwarningf(unsigned int flags, const char *fmt, va_list ap) +{ + if (fmt) { + if (flags & VWARNINGF_INTERNAL) + shf_fprintf(shl_out, Tf_sD_, "internal error"); + if (flags & VWARNINGF_ERRORPREFIX) + error_prefix(tobool(flags & VWARNINGF_FILELINE)); + if ((flags & VWARNINGF_BUILTIN) && + /* not set when main() calls parse_args() */ + builtin_argv0 && builtin_argv0 != kshname) + shf_fprintf(shl_out, Tf_sD_, builtin_argv0); + shf_vfprintf(shl_out, fmt, ap); + shf_putchar('\n', shl_out); + } + shf_flush(shl_out); +} + +void +errorfx(int rc, const char *fmt, ...) +{ + va_list va; + + exstat = rc; + + /* debugging: note that stdout not valid */ + shl_stdout_ok = false; + + va_start(va, fmt); + vwarningf(VWARNINGF_ERRORPREFIX | VWARNINGF_FILELINE, fmt, va); + va_end(va); + unwind(LERROR); +} + +void +errorf(const char *fmt, ...) +{ + va_list va; + + exstat = 1; + + /* debugging: note that stdout not valid */ + shl_stdout_ok = false; + + va_start(va, fmt); + vwarningf(VWARNINGF_ERRORPREFIX | VWARNINGF_FILELINE, fmt, va); + va_end(va); + unwind(LERROR); +} + +/* like errorf(), but no unwind is done */ +void +warningf(bool fileline, const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vwarningf(VWARNINGF_ERRORPREFIX | (fileline ? VWARNINGF_FILELINE : 0), + fmt, va); + va_end(va); +} + +/* + * Used by built-in utilities to prefix shell and utility name to message + * (also unwinds environments for special builtins). + */ +void +bi_errorf(const char *fmt, ...) +{ + va_list va; + + /* debugging: note that stdout not valid */ + shl_stdout_ok = false; + + exstat = 1; + + va_start(va, fmt); + vwarningf(VWARNINGF_ERRORPREFIX | VWARNINGF_FILELINE | + VWARNINGF_BUILTIN, fmt, va); + va_end(va); + + /* POSIX special builtins cause non-interactive shells to exit */ + if (builtin_spec) { + builtin_argv0 = NULL; + /* may not want to use LERROR here */ + unwind(LERROR); + } +} + +/* Called when something that shouldn't happen does */ +void +internal_errorf(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vwarningf(VWARNINGF_INTERNAL, fmt, va); + va_end(va); + unwind(LERROR); +} + +void +internal_warningf(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vwarningf(VWARNINGF_INTERNAL, fmt, va); + va_end(va); +} + +/* used by error reporting functions to print "ksh: .kshrc[25]: " */ +void +error_prefix(bool fileline) +{ + /* Avoid foo: foo[2]: ... */ + if (!fileline || !source || !source->file || + strcmp(source->file, kshname) != 0) + shf_fprintf(shl_out, Tf_sD_, kshname + (*kshname == '-')); + if (fileline && source && source->file != NULL) { + shf_fprintf(shl_out, "%s[%lu]: ", source->file, + (unsigned long)(source->errline ? + source->errline : source->line)); + source->errline = 0; + } +} + +/* printf to shl_out (stderr) with flush */ +void +shellf(const char *fmt, ...) +{ + va_list va; + + if (!initio_done) + /* shl_out may not be set up yet... */ + return; + va_start(va, fmt); + shf_vfprintf(shl_out, fmt, va); + va_end(va); + shf_flush(shl_out); +} + +/* printf to shl_stdout (stdout) */ +void +shprintf(const char *fmt, ...) +{ + va_list va; + + if (!shl_stdout_ok) + internal_errorf("shl_stdout not valid"); + va_start(va, fmt); + shf_vfprintf(shl_stdout, fmt, va); + va_end(va); +} + +/* test if we can seek backwards fd (returns 0 or SHF_UNBUF) */ +int +can_seek(int fd) +{ + struct stat statb; + + return (fstat(fd, &statb) == 0 && !S_ISREG(statb.st_mode) ? + SHF_UNBUF : 0); +} + +#ifdef DF +int shl_dbg_fd; +#define NSHF_IOB 4 +#else +#define NSHF_IOB 3 +#endif +struct shf shf_iob[NSHF_IOB]; + +void +initio(void) +{ +#ifdef DF + const char *lfp; +#endif + + /* force buffer allocation */ + shf_fdopen(1, SHF_WR, shl_stdout); + shf_fdopen(2, SHF_WR, shl_out); + shf_fdopen(2, SHF_WR, shl_xtrace); +#ifdef DF + if ((lfp = getenv("SDMKSH_PATH")) == NULL) { + if ((lfp = getenv("HOME")) == NULL || !mksh_abspath(lfp)) + errorf("can't get home directory"); + lfp = shf_smprintf(Tf_sSs, lfp, "mksh-dbg.txt"); + } + + if ((shl_dbg_fd = open(lfp, O_WRONLY | O_APPEND | O_CREAT, 0600)) < 0) + errorf("can't open debug output file %s", lfp); + if (shl_dbg_fd < FDBASE) { + int nfd; + + nfd = fcntl(shl_dbg_fd, F_DUPFD, FDBASE); + close(shl_dbg_fd); + if ((shl_dbg_fd = nfd) == -1) + errorf("can't dup debug output file"); + } + fcntl(shl_dbg_fd, F_SETFD, FD_CLOEXEC); + shf_fdopen(shl_dbg_fd, SHF_WR, shl_dbg); + DF("=== open ==="); +#endif + initio_done = true; +} + +/* A dup2() with error checking */ +int +ksh_dup2(int ofd, int nfd, bool errok) +{ + int rv; + + if (((rv = dup2(ofd, nfd)) < 0) && !errok && (errno != EBADF)) + errorf(Ttoo_many_files); + +#ifdef __ultrix + /*XXX imake style */ + if (rv >= 0) + fcntl(nfd, F_SETFD, 0); +#endif + + return (rv); +} + +/* + * Move fd from user space (0 <= fd < 10) to shell space (fd >= 10), + * set close-on-exec flag. See FDBASE in sh.h, maybe 24 not 10 here. + */ +short +savefd(int fd) +{ + int nfd = fd; + + if (fd < FDBASE && (nfd = fcntl(fd, F_DUPFD, FDBASE)) < 0 && + (errno == EBADF || errno == EPERM)) + return (-1); + if (nfd < 0 || nfd > SHRT_MAX) + errorf(Ttoo_many_files); + fcntl(nfd, F_SETFD, FD_CLOEXEC); + return ((short)nfd); +} + +void +restfd(int fd, int ofd) +{ + if (fd == 2) + shf_flush(&shf_iob[/* fd */ 2]); + if (ofd < 0) + /* original fd closed */ + close(fd); + else if (fd != ofd) { + /*XXX: what to do if this dup fails? */ + ksh_dup2(ofd, fd, true); + close(ofd); + } +} + +void +openpipe(int *pv) +{ + int lpv[2]; + + if (pipe(lpv) < 0) + errorf("can't create pipe - try again"); + pv[0] = savefd(lpv[0]); + if (pv[0] != lpv[0]) + close(lpv[0]); + pv[1] = savefd(lpv[1]); + if (pv[1] != lpv[1]) + close(lpv[1]); +#ifdef __OS2__ + setmode(pv[0], O_BINARY); + setmode(pv[1], O_BINARY); +#endif +} + +void +closepipe(int *pv) +{ + close(pv[0]); + close(pv[1]); +} + +/* + * Called by iosetup() (deals with 2>&4, etc.), c_read, c_print to turn + * a string (the X in 2>&X, read -uX, print -uX) into a file descriptor. + */ +int +check_fd(const char *name, int mode, const char **emsgp) +{ + int fd, fl; + + if (!name[0] || name[1]) + goto illegal_fd_name; + if (name[0] == 'p') + return (coproc_getfd(mode, emsgp)); + if (!ctype(name[0], C_DIGIT)) { + illegal_fd_name: + if (emsgp) + *emsgp = "illegal file descriptor name"; + return (-1); + } + + if ((fl = fcntl((fd = ksh_numdig(name[0])), F_GETFL, 0)) < 0) { + if (emsgp) + *emsgp = "bad file descriptor"; + return (-1); + } + fl &= O_ACCMODE; + /* + * X_OK is a kludge to disable this check for dups (x<&1): + * historical shells never did this check (XXX don't know what + * POSIX has to say). + */ + if (!(mode & X_OK) && fl != O_RDWR && ( + ((mode & R_OK) && fl != O_RDONLY) || + ((mode & W_OK) && fl != O_WRONLY))) { + if (emsgp) + *emsgp = (fl == O_WRONLY) ? + "fd not open for reading" : + "fd not open for writing"; + return (-1); + } + return (fd); +} + +/* Called once from main */ +void +coproc_init(void) +{ + coproc.read = coproc.readw = coproc.write = -1; + coproc.njobs = 0; + coproc.id = 0; +} + +/* Called by c_read() when eof is read - close fd if it is the co-process fd */ +void +coproc_read_close(int fd) +{ + if (coproc.read >= 0 && fd == coproc.read) { + coproc_readw_close(fd); + close(coproc.read); + coproc.read = -1; + } +} + +/* + * Called by c_read() and by iosetup() to close the other side of the + * read pipe, so reads will actually terminate. + */ +void +coproc_readw_close(int fd) +{ + if (coproc.readw >= 0 && coproc.read >= 0 && fd == coproc.read) { + close(coproc.readw); + coproc.readw = -1; + } +} + +/* + * Called by c_print when a write to a fd fails with EPIPE and by iosetup + * when co-process input is dup'd + */ +void +coproc_write_close(int fd) +{ + if (coproc.write >= 0 && fd == coproc.write) { + close(coproc.write); + coproc.write = -1; + } +} + +/* + * Called to check for existence of/value of the co-process file descriptor. + * (Used by check_fd() and by c_read/c_print to deal with -p option). + */ +int +coproc_getfd(int mode, const char **emsgp) +{ + int fd = (mode & R_OK) ? coproc.read : coproc.write; + + if (fd >= 0) + return (fd); + if (emsgp) + *emsgp = "no coprocess"; + return (-1); +} + +/* + * called to close file descriptors related to the coprocess (if any) + * Should be called with SIGCHLD blocked. + */ +void +coproc_cleanup(int reuse) +{ + /* This to allow co-processes to share output pipe */ + if (!reuse || coproc.readw < 0 || coproc.read < 0) { + if (coproc.read >= 0) { + close(coproc.read); + coproc.read = -1; + } + if (coproc.readw >= 0) { + close(coproc.readw); + coproc.readw = -1; + } + } + if (coproc.write >= 0) { + close(coproc.write); + coproc.write = -1; + } +} + +struct temp * +maketemp(Area *ap, Temp_type type, struct temp **tlist) +{ + char *cp; + size_t len; + int i, j; + struct temp *tp; + const char *dir; + struct stat sb; + + dir = tmpdir ? tmpdir : MKSH_DEFAULT_TMPDIR; + /* add "/shXXXXXX.tmp" plus NUL */ + len = strlen(dir); + checkoktoadd(len, offsetof(struct temp, tffn[0]) + 14); + tp = alloc(offsetof(struct temp, tffn[0]) + 14 + len, ap); + + tp->shf = NULL; + tp->pid = procpid; + tp->type = type; + + if (stat(dir, &sb) || !S_ISDIR(sb.st_mode)) { + tp->tffn[0] = '\0'; + goto maketemp_out; + } + + cp = (void *)tp; + cp += offsetof(struct temp, tffn[0]); + memcpy(cp, dir, len); + cp += len; + memcpy(cp, "/shXXXXXX.tmp", 14); + /* point to the first of six Xes */ + cp += 3; + + /* cyclically attempt to open a temporary file */ + do { + /* generate random part of filename */ + len = 0; + do { + cp[len++] = digits_lc[rndget() % 36]; + } while (len < 6); + + /* check if this one works */ + if ((i = binopen3(tp->tffn, O_CREAT | O_EXCL | O_RDWR, + 0600)) < 0 && errno != EEXIST) + goto maketemp_out; + } while (i < 0); + + if (type == TT_FUNSUB) { + /* map us high and mark as close-on-exec */ + if ((j = savefd(i)) != i) { + close(i); + i = j; + } + + /* operation mode for the shf */ + j = SHF_RD; + } else + j = SHF_WR; + + /* shf_fdopen cannot fail, so no fd leak */ + tp->shf = shf_fdopen(i, j, NULL); + + maketemp_out: + tp->next = *tlist; + *tlist = tp; + return (tp); +} + +/* + * We use a similar collision resolution algorithm as Python 2.5.4 + * but with a slightly tweaked implementation written from scratch. + */ + +#define INIT_TBLSHIFT 3 /* initial table shift (2^3 = 8) */ +#define PERTURB_SHIFT 5 /* see Python 2.5.4 Objects/dictobject.c */ + +static void tgrow(struct table *); +static int tnamecmp(const void *, const void *); + +static void +tgrow(struct table *tp) +{ + size_t i, j, osize, mask, perturb; + struct tbl *tblp, **pp; + struct tbl **ntblp, **otblp = tp->tbls; + + if (tp->tshift > 29) + internal_errorf("hash table size limit reached"); + + /* calculate old size, new shift and new size */ + osize = (size_t)1 << (tp->tshift++); + i = osize << 1; + + ntblp = alloc2(i, sizeof(struct tbl *), tp->areap); + /* multiplication cannot overflow: alloc2 checked that */ + memset(ntblp, 0, i * sizeof(struct tbl *)); + + /* table can get very full when reaching its size limit */ + tp->nfree = (tp->tshift == 30) ? 0x3FFF0000UL : + /* but otherwise, only 75% */ + ((i * 3) / 4); + tp->tbls = ntblp; + if (otblp == NULL) + return; + + mask = i - 1; + for (i = 0; i < osize; i++) + if ((tblp = otblp[i]) != NULL) { + if ((tblp->flag & DEFINED)) { + /* search for free hash table slot */ + j = perturb = tblp->ua.hval; + goto find_first_empty_slot; + find_next_empty_slot: + j = (j << 2) + j + perturb + 1; + perturb >>= PERTURB_SHIFT; + find_first_empty_slot: + pp = &ntblp[j & mask]; + if (*pp != NULL) + goto find_next_empty_slot; + /* found an empty hash table slot */ + *pp = tblp; + tp->nfree--; + } else if (!(tblp->flag & FINUSE)) { + afree(tblp, tp->areap); + } + } + afree(otblp, tp->areap); +} + +void +ktinit(Area *ap, struct table *tp, uint8_t initshift) +{ + tp->areap = ap; + tp->tbls = NULL; + tp->tshift = ((initshift > INIT_TBLSHIFT) ? + initshift : INIT_TBLSHIFT) - 1; + tgrow(tp); +} + +/* table, name (key) to search for, hash(name), rv pointer to tbl ptr */ +struct tbl * +ktscan(struct table *tp, const char *name, uint32_t h, struct tbl ***ppp) +{ + size_t j, perturb, mask; + struct tbl **pp, *p; + + mask = ((size_t)1 << (tp->tshift)) - 1; + /* search for hash table slot matching name */ + j = perturb = h; + goto find_first_slot; + find_next_slot: + j = (j << 2) + j + perturb + 1; + perturb >>= PERTURB_SHIFT; + find_first_slot: + pp = &tp->tbls[j & mask]; + if ((p = *pp) != NULL && (p->ua.hval != h || !(p->flag & DEFINED) || + strcmp(p->name, name))) + goto find_next_slot; + /* p == NULL if not found, correct found entry otherwise */ + if (ppp) + *ppp = pp; + return (p); +} + +/* table, name (key) to enter, hash(n) */ +struct tbl * +ktenter(struct table *tp, const char *n, uint32_t h) +{ + struct tbl **pp, *p; + size_t len; + + Search: + if ((p = ktscan(tp, n, h, &pp))) + return (p); + + if (tp->nfree == 0) { + /* too full */ + tgrow(tp); + goto Search; + } + + /* create new tbl entry */ + len = strlen(n); + checkoktoadd(len, offsetof(struct tbl, name[0]) + 1); + p = alloc(offsetof(struct tbl, name[0]) + ++len, tp->areap); + p->flag = 0; + p->type = 0; + p->areap = tp->areap; + p->ua.hval = h; + p->u2.field = 0; + p->u.array = NULL; + memcpy(p->name, n, len); + + /* enter in tp->tbls */ + tp->nfree--; + *pp = p; + return (p); +} + +void +ktwalk(struct tstate *ts, struct table *tp) +{ + ts->left = (size_t)1 << (tp->tshift); + ts->next = tp->tbls; +} + +struct tbl * +ktnext(struct tstate *ts) +{ + while (--ts->left >= 0) { + struct tbl *p = *ts->next++; + if (p != NULL && (p->flag & DEFINED)) + return (p); + } + return (NULL); +} + +static int +tnamecmp(const void *p1, const void *p2) +{ + const struct tbl *a = *((const struct tbl * const *)p1); + const struct tbl *b = *((const struct tbl * const *)p2); + + return (ascstrcmp(a->name, b->name)); +} + +struct tbl ** +ktsort(struct table *tp) +{ + size_t i; + struct tbl **p, **sp, **dp; + + /* + * since the table is never entirely full, no need to reserve + * additional space for the trailing NULL appended below + */ + i = (size_t)1 << (tp->tshift); + p = alloc2(i, sizeof(struct tbl *), ATEMP); + sp = tp->tbls; /* source */ + dp = p; /* dest */ + while (i--) + if ((*dp = *sp++) != NULL && (((*dp)->flag & DEFINED) || + ((*dp)->flag & ARRAY))) + dp++; + qsort(p, (i = dp - p), sizeof(struct tbl *), tnamecmp); + p[i] = NULL; + return (p); +} + +#ifdef SIGWINCH +static void +x_sigwinch(int sig MKSH_A_UNUSED) +{ + /* this runs inside interrupt context, with errno saved */ + + got_winch = 1; +} +#endif + +#ifdef DF +void +DF(const char *fmt, ...) +{ + va_list args; + struct timeval tv; + mirtime_mjd mjd; + + mksh_lockfd(shl_dbg_fd); + mksh_TIME(tv); + timet2mjd(&mjd, tv.tv_sec); + shf_fprintf(shl_dbg, "[%02u:%02u:%02u (%u) %u.%06u] ", + (unsigned)mjd.sec / 3600, ((unsigned)mjd.sec / 60) % 60, + (unsigned)mjd.sec % 60, (unsigned)getpid(), + (unsigned)tv.tv_sec, (unsigned)tv.tv_usec); + va_start(args, fmt); + shf_vfprintf(shl_dbg, fmt, args); + va_end(args); + shf_putc('\n', shl_dbg); + shf_flush(shl_dbg); + mksh_unlkfd(shl_dbg_fd); +} +#endif + +void +x_mkraw(int fd, mksh_ttyst *ocb, bool forread) +{ + mksh_ttyst cb; + + if (ocb) + mksh_tcget(fd, ocb); + else + ocb = &tty_state; + + cb = *ocb; + if (forread) { + cb.c_iflag &= ~(ISTRIP); + cb.c_lflag &= ~(ICANON) | ECHO; + } else { + cb.c_iflag &= ~(INLCR | ICRNL | ISTRIP); + cb.c_lflag &= ~(ISIG | ICANON | ECHO); + } +#if defined(VLNEXT) && defined(_POSIX_VDISABLE) + /* OSF/1 processes lnext when ~icanon */ + cb.c_cc[VLNEXT] = _POSIX_VDISABLE; +#endif + /* SunOS 4.1.x & OSF/1 processes discard(flush) when ~icanon */ +#if defined(VDISCARD) && defined(_POSIX_VDISABLE) + cb.c_cc[VDISCARD] = _POSIX_VDISABLE; +#endif + cb.c_cc[VTIME] = 0; + cb.c_cc[VMIN] = 1; + + mksh_tcset(fd, &cb); +} + +#ifdef MKSH_ENVDIR +static void +init_environ(void) +{ + char *xp; + ssize_t n; + XString xs; + struct shf *shf; + DIR *dirp; + struct dirent *dent; + + if ((dirp = opendir(MKSH_ENVDIR)) == NULL) { + warningf(false, "cannot read environment from %s: %s", + MKSH_ENVDIR, cstrerror(errno)); + return; + } + XinitN(xs, 256, ATEMP); + read_envfile: + errno = 0; + if ((dent = readdir(dirp)) != NULL) { + if (skip_varname(dent->d_name, true)[0] == '\0') { + xp = shf_smprintf(Tf_sSs, MKSH_ENVDIR, dent->d_name); + if (!(shf = shf_open(xp, O_RDONLY, 0, 0))) { + warningf(false, + "cannot read environment %s from %s: %s", + dent->d_name, MKSH_ENVDIR, + cstrerror(errno)); + goto read_envfile; + } + afree(xp, ATEMP); + n = strlen(dent->d_name); + xp = Xstring(xs, xp); + XcheckN(xs, xp, n + 32); + memcpy(xp, dent->d_name, n); + xp += n; + *xp++ = '='; + while ((n = shf_read(xp, Xnleft(xs, xp), shf)) > 0) { + xp += n; + if (Xnleft(xs, xp) <= 0) + XcheckN(xs, xp, Xlength(xs, xp)); + } + if (n < 0) { + warningf(false, + "cannot read environment %s from %s: %s", + dent->d_name, MKSH_ENVDIR, + cstrerror(shf_errno(shf))); + } else { + *xp = '\0'; + xp = Xstring(xs, xp); + rndpush(xp); + typeset(xp, IMPORT | EXPORT, 0, 0, 0); + } + shf_close(shf); + } + goto read_envfile; + } else if (errno) + warningf(false, "cannot read environment from %s: %s", + MKSH_ENVDIR, cstrerror(errno)); + closedir(dirp); + Xfree(xs, xp); +} +#else +extern char **environ; + +static void +init_environ(void) +{ + const char **wp; + + if (environ == NULL) + return; + + wp = (const char **)environ; + while (*wp != NULL) { + rndpush(*wp); + typeset(*wp, IMPORT | EXPORT, 0, 0, 0); + ++wp; + } +} +#endif + +#ifdef MKSH_EARLY_LOCALE_TRACKING +void +recheck_ctype(void) +{ + const char *ccp; + + ccp = str_val(global("LC_ALL")); + if (ccp == null) + ccp = str_val(global("LC_CTYPE")); + if (ccp == null) + ccp = str_val(global("LANG")); + UTFMODE = isuc(ccp); +#if HAVE_SETLOCALE_CTYPE + ccp = setlocale(LC_CTYPE, ccp); +#if HAVE_LANGINFO_CODESET + if (!isuc(ccp)) + ccp = nl_langinfo(CODESET); +#endif + if (isuc(ccp)) + UTFMODE = 1; +#endif + + if (Flag(FPOSIX)) + warningf(true, "early locale tracking enabled UTF-8 mode while in POSIX mode, you are now noncompliant"); +} +#endif diff --git a/mirhash.h b/mirhash.h new file mode 100644 index 0000000..0105b22 --- /dev/null +++ b/mirhash.h @@ -0,0 +1,226 @@ +/*- + * Copyright © 2011, 2014, 2015 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un‐ + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person’s immediate fault when using the work as intended. + *- + * This file provides BAFH (Better Avalanche for the Jenkins Hash) as + * inline macro bodies that operate on “register uint32_t” variables, + * with variants that use their local intermediate registers. + * + * Usage note for BAFH with entropy distribution: input up to 4 bytes + * is best combined into a 32-bit unsigned integer, which is then run + * through BAFHFinish_reg for mixing and then used as context instead + * of 0. Longer input should be handled the same: take the first four + * bytes as IV after mixing then add subsequent bytes the same way. + * This needs counting input bytes and is endian-dependent, thus not, + * for speed reasons, specified for the regular stable hash, but very + * much recommended if the actual output value may differ across runs + * (so is using a random value instead of 0 for the IV). + *- + * Little quote gem: + * We are looking into it. Changing the core + * hash function in PHP isn't a trivial change + * and will take us some time. + * -- Rasmus Lerdorf + */ + +#ifndef SYSKERN_MIRHASH_H +#define SYSKERN_MIRHASH_H 1 +#define SYSKERN_MIRHASH_BAFH + +#include + +__RCSID("$MirOS: src/bin/mksh/mirhash.h,v 1.6 2015/11/29 17:05:02 tg Exp $"); + +/*- + * BAFH itself is defined by the following primitives: + * + * • BAFHInit(ctx) initialises the hash context, which consists of a + * sole 32-bit unsigned integer (ideally in a register), to 0. + * It is possible to use any initial value out of [0; 2³²[ – which + * is, in fact, recommended if using BAFH for entropy distribution + * – but for a regular stable hash, the IV 0 is needed. + * + * • BAFHUpdateOctet(ctx,val) compresses the unsigned 8-bit quantity + * into the hash context. The algorithm used is Jenkins’ one-at-a- + * time, except that an additional constant 1 is added so that, if + * the context is (still) zero, adding a NUL byte is not ignored. + * + * • BAFHror(eax,cl) evaluates to the unsigned 32-bit integer “eax”, + * rotated right by “cl” ∈ [0; 31] (no casting, be careful!) where + * “eax” must be uint32_t and “cl” an in-range integer. + * + * • BAFHFinish(ctx) avalanches the context around so every sub-byte + * depends on all input octets; afterwards, the context variable’s + * value is the hash output. BAFH does not use any padding, nor is + * the input length added; this is due to the common use case (for + * quick entropy distribution and use with a hashtable). + * Warning: BAFHFinish uses the MixColumn algorithm of AES – which + * is reversible (to avoid introducing funnels and reducing entro‐ + * py), so blinding may need to be employed for some uses, e.g. in + * mksh, after a fork. + * + * The BAFHUpdateOctet and BAFHFinish are available in two flavours: + * suffixed with _reg (assumes the context is in a register) or _mem + * (which doesn’t). + * + * The following high-level macros (with _reg and _mem variants) are + * available: + * + * • BAFHUpdateMem(ctx,buf,len) adds a memory block to a context. + * • BAFHUpdateStr(ctx,buf) is equivalent to using len=strlen(buf). + * • BAFHHostMem(ctx,buf,len) calculates the hash of the memory buf‐ + * fer using the first 4 octets (mixed) for IV, as outlined above; + * the result is endian-dependent; “ctx” assumed to be a register. + * • BAFHHostStr(ctx,buf) does the same for C strings. + * + * All macros may use ctx multiple times in their expansion, but all + * other arguments are always evaluated at most once except BAFHror. + * + * To stay portable, never use the BAFHHost*() macros (these are for + * host-local entropy shuffling), and encode numbers using ULEB128. + */ + +#define BAFHInit(h) do { \ + (h) = 0; \ +} while (/* CONSTCOND */ 0) + +#define BAFHUpdateOctet_reg(h,b) do { \ + (h) += (uint8_t)(b); \ + ++(h); \ + (h) += (h) << 10; \ + (h) ^= (h) >> 6; \ +} while (/* CONSTCOND */ 0) + +#define BAFHUpdateOctet_mem(m,b) do { \ + register uint32_t BAFH_h = (m); \ + \ + BAFHUpdateOctet_reg(BAFH_h, (b)); \ + (m) = BAFH_h; \ +} while (/* CONSTCOND */ 0) + +#define BAFHror(eax,cl) (((eax) >> (cl)) | ((eax) << (32 - (cl)))) + +#define BAFHFinish_reg(h) do { \ + register uint32_t BAFHFinish_v; \ + \ + BAFHFinish_v = ((h) >> 7) & 0x01010101U; \ + BAFHFinish_v += BAFHFinish_v << 1; \ + BAFHFinish_v += BAFHFinish_v << 3; \ + BAFHFinish_v ^= ((h) << 1) & 0xFEFEFEFEU; \ + \ + BAFHFinish_v ^= BAFHror(BAFHFinish_v, 8); \ + BAFHFinish_v ^= ((h) = BAFHror((h), 8)); \ + BAFHFinish_v ^= ((h) = BAFHror((h), 8)); \ + (h) = BAFHror((h), 8) ^ BAFHFinish_v; \ +} while (/* CONSTCOND */ 0) + +#define BAFHFinish_mem(m) do { \ + register uint32_t BAFHFinish_v, BAFH_h = (m); \ + \ + BAFHFinish_v = (BAFH_h >> 7) & 0x01010101U; \ + BAFHFinish_v += BAFHFinish_v << 1; \ + BAFHFinish_v += BAFHFinish_v << 3; \ + BAFHFinish_v ^= (BAFH_h << 1) & 0xFEFEFEFEU; \ + \ + BAFHFinish_v ^= BAFHror(BAFHFinish_v, 8); \ + BAFHFinish_v ^= (BAFH_h = BAFHror(BAFH_h, 8)); \ + BAFHFinish_v ^= (BAFH_h = BAFHror(BAFH_h, 8)); \ + (m) = BAFHror(BAFH_h, 8) ^ BAFHFinish_v; \ +} while (/* CONSTCOND */ 0) + +#define BAFHUpdateMem_reg(h,p,z) do { \ + register const uint8_t *BAFHUpdate_p; \ + register size_t BAFHUpdate_z = (z); \ + \ + BAFHUpdate_p = (const void *)(p); \ + while (BAFHUpdate_z--) \ + BAFHUpdateOctet_reg((h), *BAFHUpdate_p++); \ +} while (/* CONSTCOND */ 0) + +/* meh should have named them _r/m but that’s not valid C */ +#define BAFHUpdateMem_mem(m,p,z) do { \ + register uint32_t BAFH_h = (m); \ + \ + BAFHUpdateMem_reg(BAFH_h, (p), (z)); \ + (m) = BAFH_h; \ +} while (/* CONSTCOND */ 0) + +#define BAFHUpdateStr_reg(h,s) do { \ + register const uint8_t *BAFHUpdate_s; \ + register uint8_t BAFHUpdate_c; \ + \ + BAFHUpdate_s = (const void *)(s); \ + while ((BAFHUpdate_c = *BAFHUpdate_s++) != 0) \ + BAFHUpdateOctet_reg((h), BAFHUpdate_c); \ +} while (/* CONSTCOND */ 0) + +#define BAFHUpdateStr_mem(m,s) do { \ + register uint32_t BAFH_h = (m); \ + \ + BAFHUpdateStr_reg(BAFH_h, (s)); \ + (m) = BAFH_h; \ +} while (/* CONSTCOND */ 0) + +#define BAFHHostMem(h,p,z) do { \ + register const uint8_t *BAFHUpdate_p; \ + register size_t BAFHUpdate_z = (z); \ + size_t BAFHHost_z; \ + union { \ + uint8_t as_u8[4]; \ + uint32_t as_u32; \ + } BAFHHost_v; \ + \ + BAFHUpdate_p = (const void *)(p); \ + BAFHHost_v.as_u32 = 0; \ + BAFHHost_z = BAFHUpdate_z < 4 ? BAFHUpdate_z : 4; \ + memcpy(BAFHHost_v.as_u8, BAFHUpdate_p, BAFHHost_z); \ + BAFHUpdate_p += BAFHHost_z; \ + BAFHUpdate_z -= BAFHHost_z; \ + (h) = BAFHHost_v.as_u32; \ + BAFHFinish_reg(h); \ + while (BAFHUpdate_z--) \ + BAFHUpdateOctet_reg((h), *BAFHUpdate_p++); \ + BAFHFinish_reg(h); \ +} while (/* CONSTCOND */ 0) + +#define BAFHHostStr(h,s) do { \ + register const uint8_t *BAFHUpdate_s; \ + register uint8_t BAFHUpdate_c; \ + union { \ + uint8_t as_u8[4]; \ + uint32_t as_u32; \ + } BAFHHost_v; \ + \ + BAFHUpdate_s = (const void *)(s); \ + BAFHHost_v.as_u32 = 0; \ + if ((BAFHHost_v.as_u8[0] = *BAFHUpdate_s) != 0) \ + ++BAFHUpdate_s; \ + if ((BAFHHost_v.as_u8[1] = *BAFHUpdate_s) != 0) \ + ++BAFHUpdate_s; \ + if ((BAFHHost_v.as_u8[2] = *BAFHUpdate_s) != 0) \ + ++BAFHUpdate_s; \ + if ((BAFHHost_v.as_u8[3] = *BAFHUpdate_s) != 0) \ + ++BAFHUpdate_s; \ + (h) = BAFHHost_v.as_u32; \ + BAFHFinish_reg(h); \ + while ((BAFHUpdate_c = *BAFHUpdate_s++) != 0) \ + BAFHUpdateOctet_reg((h), BAFHUpdate_c); \ + BAFHFinish_reg(h); \ +} while (/* CONSTCOND */ 0) + +#endif diff --git a/misc.c b/misc.c new file mode 100644 index 0000000..e51dcb1 --- /dev/null +++ b/misc.c @@ -0,0 +1,2576 @@ +/* $OpenBSD: misc.c,v 1.41 2015/09/10 22:48:58 nicm Exp $ */ +/* $OpenBSD: path.c,v 1.13 2015/09/05 09:47:08 jsg Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017 + * mirabilos + * Copyright (c) 2015 + * Daniel Richard G. + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" +#if !HAVE_GETRUSAGE +#include +#endif +#if HAVE_GRP_H +#include +#endif + +__RCSID("$MirOS: src/bin/mksh/misc.c,v 1.291 2018/01/14 00:03:03 tg Exp $"); + +#define KSH_CHVT_FLAG +#ifdef MKSH_SMALL +#undef KSH_CHVT_FLAG +#endif +#ifdef TIOCSCTTY +#define KSH_CHVT_CODE +#define KSH_CHVT_FLAG +#endif + +/* type bits for unsigned char */ +unsigned char chtypes[UCHAR_MAX + 1]; + +static const unsigned char *pat_scan(const unsigned char *, + const unsigned char *, bool) MKSH_A_PURE; +static int do_gmatch(const unsigned char *, const unsigned char *, + const unsigned char *, const unsigned char *, + const unsigned char *) MKSH_A_PURE; +static const unsigned char *gmatch_cclass(const unsigned char *, unsigned char) + MKSH_A_PURE; +#ifdef KSH_CHVT_CODE +static void chvt(const Getopt *); +#endif + +/*XXX this should go away */ +static int make_path(const char *, const char *, char **, XString *, int *); + +#ifdef SETUID_CAN_FAIL_WITH_EAGAIN +/* we don't need to check for other codes, EPERM won't happen */ +#define DO_SETUID(func, argvec) do { \ + if ((func argvec) && errno == EAGAIN) \ + errorf("%s failed with EAGAIN, probably due to a" \ + " too low process limit; aborting", #func); \ +} while (/* CONSTCOND */ 0) +#else +#define DO_SETUID(func, argvec) func argvec +#endif + + +/* called from XcheckN() to grow buffer */ +char * +Xcheck_grow(XString *xsp, const char *xp, size_t more) +{ + const char *old_beg = xsp->beg; + + if (more < xsp->len) + more = xsp->len; + /* (xsp->len + X_EXTRA) never overflows */ + checkoktoadd(more, xsp->len + X_EXTRA); + xsp->beg = aresize(xsp->beg, (xsp->len += more) + X_EXTRA, xsp->areap); + xsp->end = xsp->beg + xsp->len; + return (xsp->beg + (xp - old_beg)); +} + + +#define SHFLAGS_DEFNS +#define FN(sname,cname,flags,ochar) \ + static const struct { \ + /* character flag (if any) */ \ + char c; \ + /* OF_* */ \ + unsigned char optflags; \ + /* long name of option */ \ + char name[sizeof(sname)]; \ + } shoptione_ ## cname = { \ + ochar, flags, sname \ + }; +#include "sh_flags.gen" + +#define OFC(i) (options[i][-2]) +#define OFF(i) (((const unsigned char *)options[i])[-1]) +#define OFN(i) (options[i]) + +const char * const options[] = { +#define SHFLAGS_ITEMS +#include "sh_flags.gen" +}; + +/* + * translate -o option into F* constant (also used for test -o option) + */ +size_t +option(const char *n) +{ + size_t i = 0; + + if (ctype(n[0], C_MINUS | C_PLUS) && n[1] && !n[2]) + while (i < NELEM(options)) { + if (OFC(i) == n[1]) + return (i); + ++i; + } + else + while (i < NELEM(options)) { + if (!strcmp(OFN(i), n)) + return (i); + ++i; + } + + return ((size_t)-1); +} + +struct options_info { + int opt_width; + int opts[NELEM(options)]; +}; + +static void options_fmt_entry(char *, size_t, unsigned int, const void *); +static void printoptions(bool); + +/* format a single select menu item */ +static void +options_fmt_entry(char *buf, size_t buflen, unsigned int i, const void *arg) +{ + const struct options_info *oi = (const struct options_info *)arg; + + shf_snprintf(buf, buflen, "%-*s %s", + oi->opt_width, OFN(oi->opts[i]), + Flag(oi->opts[i]) ? "on" : "off"); +} + +static void +printoptions(bool verbose) +{ + size_t i = 0; + + if (verbose) { + size_t n = 0, len, octs = 0; + struct options_info oi; + struct columnise_opts co; + + /* verbose version */ + shf_puts("Current option settings\n", shl_stdout); + + oi.opt_width = 0; + while (i < NELEM(options)) { + if ((len = strlen(OFN(i)))) { + oi.opts[n++] = i; + if (len > octs) + octs = len; + len = utf_mbswidth(OFN(i)); + if ((int)len > oi.opt_width) + oi.opt_width = (int)len; + } + ++i; + } + co.shf = shl_stdout; + co.linesep = '\n'; + co.prefcol = co.do_last = true; + print_columns(&co, n, options_fmt_entry, &oi, + octs + 4, oi.opt_width + 4); + } else { + /* short version like AT&T ksh93 */ + shf_puts(Tset, shl_stdout); + while (i < NELEM(options)) { + if (Flag(i) && OFN(i)[0]) + shprintf(" -o %s", OFN(i)); + ++i; + } + shf_putc('\n', shl_stdout); + } +} + +char * +getoptions(void) +{ + size_t i = 0; + char c, m[(int)FNFLAGS + 1]; + char *cp = m; + + while (i < NELEM(options)) { + if ((c = OFC(i)) && Flag(i)) + *cp++ = c; + ++i; + } + strndupx(cp, m, cp - m, ATEMP); + return (cp); +} + +/* change a Flag(*) value; takes care of special actions */ +void +change_flag(enum sh_flag f, int what, bool newset) +{ + unsigned char oldval; + unsigned char newval = (newset ? 1 : 0); + + if (f == FXTRACE) { + change_xtrace(newval, true); + return; + } + oldval = Flag(f); + Flag(f) = newval = (newset ? 1 : 0); +#ifndef MKSH_UNEMPLOYED + if (f == FMONITOR) { + if (what != OF_CMDLINE && newval != oldval) + j_change(); + } else +#endif +#ifndef MKSH_NO_CMDLINE_EDITING + if (( +#if !MKSH_S_NOVI + f == FVI || +#endif + f == FEMACS || f == FGMACS) && newval) { +#if !MKSH_S_NOVI + Flag(FVI) = +#endif + Flag(FEMACS) = Flag(FGMACS) = 0; + Flag(f) = newval; + } else +#endif + if (f == FPRIVILEGED && oldval && !newval) { + /* Turning off -p? */ + + /*XXX this can probably be optimised */ + kshegid = kshgid = getgid(); + ksheuid = kshuid = getuid(); +#if HAVE_SETRESUGID + DO_SETUID(setresgid, (kshegid, kshegid, kshegid)); +#if HAVE_SETGROUPS + /* setgroups doesn't EAGAIN on Linux */ + setgroups(1, &kshegid); +#endif + DO_SETUID(setresuid, (ksheuid, ksheuid, ksheuid)); +#else /* !HAVE_SETRESUGID */ + /* setgid, setegid, seteuid don't EAGAIN on Linux */ + setgid(kshegid); +#ifndef MKSH__NO_SETEUGID + setegid(kshegid); +#endif + DO_SETUID(setuid, (ksheuid)); +#ifndef MKSH__NO_SETEUGID + seteuid(ksheuid); +#endif +#endif /* !HAVE_SETRESUGID */ + } else if ((f == FPOSIX || f == FSH) && newval) { + /* Turning on -o posix or -o sh? */ + Flag(FBRACEEXPAND) = 0; + /* Turning on -o posix? */ + if (f == FPOSIX) { + /* C locale required for compliance */ + UTFMODE = 0; + } + } else if (f == FTALKING) { + /* Changing interactive flag? */ + if ((what == OF_CMDLINE || what == OF_SET) && procpid == kshpid) + Flag(FTALKING_I) = newval; + } +} + +void +change_xtrace(unsigned char newval, bool dosnapshot) +{ + static bool in_xtrace; + + if (in_xtrace) + return; + + if (!dosnapshot && newval == Flag(FXTRACE)) + return; + + if (Flag(FXTRACE) == 2) { + shf_putc('\n', shl_xtrace); + Flag(FXTRACE) = 1; + shf_flush(shl_xtrace); + } + + if (!dosnapshot && Flag(FXTRACE) == 1) + switch (newval) { + case 1: + return; + case 2: + goto changed_xtrace; + } + + shf_flush(shl_xtrace); + if (shl_xtrace->fd != 2) + close(shl_xtrace->fd); + if (!newval || (shl_xtrace->fd = savefd(2)) == -1) + shl_xtrace->fd = 2; + + changed_xtrace: + if ((Flag(FXTRACE) = newval) == 2) { + in_xtrace = true; + Flag(FXTRACE) = 0; + shf_puts(substitute(str_val(global("PS4")), 0), shl_xtrace); + Flag(FXTRACE) = 2; + in_xtrace = false; + } +} + +/* + * Parse command line and set command arguments. Returns the index of + * non-option arguments, -1 if there is an error. + */ +int +parse_args(const char **argv, + /* OF_FIRSTTIME, OF_CMDLINE, or OF_SET */ + int what, + bool *setargsp) +{ + static const char cmd_opts[] = +#define SHFLAGS_NOT_SET +#define SHFLAGS_OPTCS +#include "sh_flags.gen" +#undef SHFLAGS_NOT_SET + ; + static const char set_opts[] = +#define SHFLAGS_NOT_CMD +#define SHFLAGS_OPTCS +#include "sh_flags.gen" +#undef SHFLAGS_NOT_CMD + ; + bool set; + const char *opts; + const char *array = NULL; + Getopt go; + size_t i; + int optc, arrayset = 0; + bool sortargs = false; + bool fcompatseen = false; + + if (what == OF_CMDLINE) { + const char *p = argv[0], *q; + /* + * Set FLOGIN before parsing options so user can clear + * flag using +l. + */ + if (*p != '-') + for (q = p; *q; ) + if (mksh_cdirsep(*q++)) + p = q; + Flag(FLOGIN) = (*p == '-'); + opts = cmd_opts; + } else if (what == OF_FIRSTTIME) { + opts = cmd_opts; + } else + opts = set_opts; + ksh_getopt_reset(&go, GF_ERROR|GF_PLUSOPT); + while ((optc = ksh_getopt(argv, &go, opts)) != -1) { + set = tobool(!(go.info & GI_PLUS)); + switch (optc) { + case 'A': + if (what == OF_FIRSTTIME) + break; + arrayset = set ? 1 : -1; + array = go.optarg; + break; + + case 'o': + if (what == OF_FIRSTTIME) + break; + if (go.optarg == NULL) { + /* + * lone -o: print options + * + * Note that on the command line, -o requires + * an option (ie, can't get here if what is + * OF_CMDLINE). + */ + printoptions(set); + break; + } + i = option(go.optarg); + if ((i == FPOSIX || i == FSH) && set && !fcompatseen) { + /* + * If running 'set -o posix' or + * 'set -o sh', turn off the other; + * if running 'set -o posix -o sh' + * allow both to be set though. + */ + Flag(FPOSIX) = 0; + Flag(FSH) = 0; + fcompatseen = true; + } + if ((i != (size_t)-1) && (set ? 1U : 0U) == Flag(i)) + /* + * Don't check the context if the flag + * isn't changing - makes "set -o interactive" + * work if you're already interactive. Needed + * if the output of "set +o" is to be used. + */ + ; + else if ((i != (size_t)-1) && (OFF(i) & what)) + change_flag((enum sh_flag)i, what, set); + else { + bi_errorf(Tf_sD_s, go.optarg, + Tunknown_option); + return (-1); + } + break; + +#ifdef KSH_CHVT_FLAG + case 'T': + if (what != OF_FIRSTTIME) + break; +#ifndef KSH_CHVT_CODE + errorf("no TIOCSCTTY ioctl"); +#else + change_flag(FTALKING, OF_CMDLINE, true); + chvt(&go); + break; +#endif +#endif + + case '?': + return (-1); + + default: + if (what == OF_FIRSTTIME) + break; + /* -s: sort positional params (AT&T ksh stupidity) */ + if (what == OF_SET && optc == 's') { + sortargs = true; + break; + } + for (i = 0; i < NELEM(options); i++) + if (optc == OFC(i) && + (what & OFF(i))) { + change_flag((enum sh_flag)i, what, set); + break; + } + if (i == NELEM(options)) + internal_errorf("parse_args: '%c'", optc); + } + } + if (!(go.info & GI_MINUSMINUS) && argv[go.optind] && + ctype(argv[go.optind][0], C_MINUS | C_PLUS) && + argv[go.optind][1] == '\0') { + /* lone - clears -v and -x flags */ + if (argv[go.optind][0] == '-') { + Flag(FVERBOSE) = 0; + change_xtrace(0, false); + } + /* set skips lone - or + option */ + go.optind++; + } + if (setargsp) + /* -- means set $#/$* even if there are no arguments */ + *setargsp = !arrayset && ((go.info & GI_MINUSMINUS) || + argv[go.optind]); + + if (arrayset) { + const char *ccp = NULL; + + if (array && *array) + ccp = skip_varname(array, false); + if (!ccp || !(!ccp[0] || (ccp[0] == '+' && !ccp[1]))) { + bi_errorf(Tf_sD_s, array, Tnot_ident); + return (-1); + } + } + if (sortargs) { + for (i = go.optind; argv[i]; i++) + ; + qsort(&argv[go.optind], i - go.optind, sizeof(void *), + ascpstrcmp); + } + if (arrayset) + go.optind += set_array(array, tobool(arrayset > 0), + argv + go.optind); + + return (go.optind); +} + +/* parse a decimal number: returns 0 if string isn't a number, 1 otherwise */ +int +getn(const char *s, int *ai) +{ + char c; + mksh_ari_u num; + bool neg = false; + + num.u = 0; + + do { + c = *s++; + } while (ctype(c, C_SPACE)); + + switch (c) { + case '-': + neg = true; + /* FALLTHROUGH */ + case '+': + c = *s++; + break; + } + + do { + if (!ctype(c, C_DIGIT)) + /* not numeric */ + return (0); + if (num.u > 214748364U) + /* overflow on multiplication */ + return (0); + num.u = num.u * 10U + (unsigned int)ksh_numdig(c); + /* now: num.u <= 2147483649U */ + } while ((c = *s++)); + + if (num.u > (neg ? 2147483648U : 2147483647U)) + /* overflow for signed 32-bit int */ + return (0); + + if (neg) + num.u = -num.u; + *ai = num.i; + return (1); +} + +/** + * pattern simplifications: + * - @(x) -> x (not @(x|y) though) + * - ** -> * + */ +static void * +simplify_gmatch_pattern(const unsigned char *sp) +{ + uint8_t c; + unsigned char *cp, *dp; + const unsigned char *ps, *se; + + cp = alloc(strlen((const void *)sp) + 1, ATEMP); + goto simplify_gmatch_pat1a; + + /* foo@(b@(a)r)b@(a|a)z -> foobarb@(a|a)z */ + simplify_gmatch_pat1: + sp = cp; + simplify_gmatch_pat1a: + dp = cp; + se = strnul(sp); + while ((c = *sp++)) { + if (!ISMAGIC(c)) { + *dp++ = c; + continue; + } + switch ((c = *sp++)) { + case 0x80|'@': + /* simile for @ */ + case 0x80|' ': + /* check whether it has only one clause */ + ps = pat_scan(sp, se, true); + if (!ps || ps[-1] != /*(*/ ')') + /* nope */ + break; + /* copy inner clause until matching close */ + ps -= 2; + while ((const unsigned char *)sp < ps) + *dp++ = *sp++; + /* skip MAGIC and closing parenthesis */ + sp += 2; + /* copy the rest of the pattern */ + memmove(dp, sp, strlen((const void *)sp) + 1); + /* redo from start */ + goto simplify_gmatch_pat1; + } + *dp++ = MAGIC; + *dp++ = c; + } + *dp = '\0'; + + /* collapse adjacent asterisk wildcards */ + sp = dp = cp; + while ((c = *sp++)) { + if (!ISMAGIC(c)) { + *dp++ = c; + continue; + } + switch ((c = *sp++)) { + case '*': + while (ISMAGIC(sp[0]) && sp[1] == c) + sp += 2; + break; + } + *dp++ = MAGIC; + *dp++ = c; + } + *dp = '\0'; + + /* return the result, allocated from ATEMP */ + return (cp); +} + +/* -------- gmatch.c -------- */ + +/* + * int gmatch(string, pattern) + * char *string, *pattern; + * + * Match a pattern as in sh(1). + * pattern character are prefixed with MAGIC by expand. + */ +int +gmatchx(const char *s, const char *p, bool isfile) +{ + const char *se, *pe; + char *pnew; + int rv; + + if (s == NULL || p == NULL) + return (0); + + pe = strnul(p); + /* + * isfile is false iff no syntax check has been done on + * the pattern. If check fails, just do a strcmp(). + */ + if (!isfile && !has_globbing(p)) { + size_t len = pe - p + 1; + char tbuf[64]; + char *t = len <= sizeof(tbuf) ? tbuf : alloc(len, ATEMP); + debunk(t, p, len); + return (!strcmp(t, s)); + } + se = strnul(s); + + /* + * since the do_gmatch() engine sucks so much, we must do some + * pattern simplifications + */ + pnew = simplify_gmatch_pattern((const unsigned char *)p); + pe = strnul(pnew); + + rv = do_gmatch((const unsigned char *)s, (const unsigned char *)se, + (const unsigned char *)pnew, (const unsigned char *)pe, + (const unsigned char *)s); + afree(pnew, ATEMP); + return (rv); +} + +/** + * Returns if p is a syntacticly correct globbing pattern, false + * if it contains no pattern characters or if there is a syntax error. + * Syntax errors are: + * - [ with no closing ] + * - imbalanced $(...) expression + * - [...] and *(...) not nested (eg, @(a[b|)c], *(a[b|c]d)) + */ +/*XXX + * - if no magic, + * if dest given, copy to dst + * return ? + * - if magic && (no globbing || syntax error) + * debunk to dst + * return ? + * - return ? + */ +bool +has_globbing(const char *pat) +{ + unsigned char c, subc; + bool saw_glob = false; + unsigned int nest = 0; + const unsigned char *p = (const unsigned char *)pat; + const unsigned char *s; + + while ((c = *p++)) { + /* regular character? ok. */ + if (!ISMAGIC(c)) + continue; + /* MAGIC + NUL? abort. */ + if (!(c = *p++)) + return (false); + /* some specials */ + if (ord(c) == ORD('*') || ord(c) == ORD('?')) { + /* easy glob, accept */ + saw_glob = true; + } else if (ord(c) == ORD('[')) { + /* bracket expression; eat negation and initial ] */ + if (ISMAGIC(p[0]) && ord(p[1]) == ORD('!')) + p += 2; + if (ISMAGIC(p[0]) && ord(p[1]) == ORD(']')) + p += 2; + /* check next string part */ + s = p; + while ((c = *s++)) { + /* regular chars are ok */ + if (!ISMAGIC(c)) + continue; + /* MAGIC + NUL cannot happen */ + if (!(c = *s++)) + return (false); + /* terminating bracket? */ + if (ord(c) == ORD(']')) { + /* accept and continue */ + p = s; + saw_glob = true; + break; + } + /* sub-bracket expressions */ + if (ord(c) == ORD('[') && ( + /* collating element? */ + ord(*s) == ORD('.') || + /* equivalence class? */ + ord(*s) == ORD('=') || + /* character class? */ + ord(*s) == ORD(':'))) { + /* must stop with exactly the same c */ + subc = *s++; + /* arbitrarily many chars in betwixt */ + while ((c = *s++)) + /* but only this sequence... */ + if (c == subc && ISMAGIC(*s) && + ord(s[1]) == ORD(']')) { + /* accept, terminate */ + s += 2; + break; + } + /* EOS without: reject bracket expr */ + if (!c) + break; + /* continue; */ + } + /* anything else just goes on */ + } + } else if ((c & 0x80) && ctype(c & 0x7F, C_PATMO | C_SPC)) { + /* opening pattern */ + saw_glob = true; + ++nest; + } else if (ord(c) == ORD(/*(*/ ')')) { + /* closing pattern */ + if (nest) + --nest; + } + } + return (saw_glob && !nest); +} + +/* Function must return either 0 or 1 (assumed by code for 0x80|'!') */ +static int +do_gmatch(const unsigned char *s, const unsigned char *se, + const unsigned char *p, const unsigned char *pe, + const unsigned char *smin) +{ + unsigned char sc, pc, sl = 0; + const unsigned char *prest, *psub, *pnext; + const unsigned char *srest; + + if (s == NULL || p == NULL) + return (0); + if (s > smin && s <= se) + sl = s[-1]; + while (p < pe) { + pc = *p++; + sc = s < se ? *s : '\0'; + s++; + if (!ISMAGIC(pc)) { + if (sc != pc) + return (0); + sl = sc; + continue; + } + switch (ord(*p++)) { + case ORD('['): + /* BSD cclass extension? */ + if (ISMAGIC(p[0]) && ord(p[1]) == ORD('[') && + ord(p[2]) == ORD(':') && + ctype((pc = p[3]), C_ANGLE) && + ord(p[4]) == ORD(':') && + ISMAGIC(p[5]) && ord(p[6]) == ORD(']') && + ISMAGIC(p[7]) && ord(p[8]) == ORD(']')) { + /* zero-length match */ + --s; + p += 9; + /* word begin? */ + if (ord(pc) == ORD('<') && + !ctype(sl, C_ALNUX) && + ctype(sc, C_ALNUX)) + break; + /* word end? */ + if (ord(pc) == ORD('>') && + ctype(sl, C_ALNUX) && + !ctype(sc, C_ALNUX)) + break; + /* neither */ + return (0); + } + if (sc == 0 || (p = gmatch_cclass(p, sc)) == NULL) + return (0); + break; + + case ORD('?'): + if (sc == 0) + return (0); + if (UTFMODE) { + --s; + s += utf_ptradj((const void *)s); + } + break; + + case ORD('*'): + if (p == pe) + return (1); + s--; + do { + if (do_gmatch(s, se, p, pe, smin)) + return (1); + } while (s++ < se); + return (0); + + /** + * [+*?@!](pattern|pattern|..) + * This is also needed for ${..%..}, etc. + */ + + /* matches one or more times */ + case ORD('+') | 0x80: + /* matches zero or more times */ + case ORD('*') | 0x80: + if (!(prest = pat_scan(p, pe, false))) + return (0); + s--; + /* take care of zero matches */ + if (ord(p[-1]) == (0x80 | ORD('*')) && + do_gmatch(s, se, prest, pe, smin)) + return (1); + for (psub = p; ; psub = pnext) { + pnext = pat_scan(psub, pe, true); + for (srest = s; srest <= se; srest++) { + if (do_gmatch(s, srest, psub, pnext - 2, smin) && + (do_gmatch(srest, se, prest, pe, smin) || + (s != srest && + do_gmatch(srest, se, p - 2, pe, smin)))) + return (1); + } + if (pnext == prest) + break; + } + return (0); + + /* matches zero or once */ + case ORD('?') | 0x80: + /* matches one of the patterns */ + case ORD('@') | 0x80: + /* simile for @ */ + case ORD(' ') | 0x80: + if (!(prest = pat_scan(p, pe, false))) + return (0); + s--; + /* Take care of zero matches */ + if (ord(p[-1]) == (0x80 | ORD('?')) && + do_gmatch(s, se, prest, pe, smin)) + return (1); + for (psub = p; ; psub = pnext) { + pnext = pat_scan(psub, pe, true); + srest = prest == pe ? se : s; + for (; srest <= se; srest++) { + if (do_gmatch(s, srest, psub, pnext - 2, smin) && + do_gmatch(srest, se, prest, pe, smin)) + return (1); + } + if (pnext == prest) + break; + } + return (0); + + /* matches none of the patterns */ + case ORD('!') | 0x80: + if (!(prest = pat_scan(p, pe, false))) + return (0); + s--; + for (srest = s; srest <= se; srest++) { + int matched = 0; + + for (psub = p; ; psub = pnext) { + pnext = pat_scan(psub, pe, true); + if (do_gmatch(s, srest, psub, + pnext - 2, smin)) { + matched = 1; + break; + } + if (pnext == prest) + break; + } + if (!matched && + do_gmatch(srest, se, prest, pe, smin)) + return (1); + } + return (0); + + default: + if (sc != p[-1]) + return (0); + break; + } + sl = sc; + } + return (s == se); +} + +/*XXX this is a prime example for bsearch or a const hashtable */ +static const struct cclass { + const char *name; + uint32_t value; +} cclasses[] = { + /* POSIX */ + { "alnum", C_ALNUM }, + { "alpha", C_ALPHA }, + { "blank", C_BLANK }, + { "cntrl", C_CNTRL }, + { "digit", C_DIGIT }, + { "graph", C_GRAPH }, + { "lower", C_LOWER }, + { "print", C_PRINT }, + { "punct", C_PUNCT }, + { "space", C_SPACE }, + { "upper", C_UPPER }, + { "xdigit", C_SEDEC }, + /* BSD */ + /* "<" and ">" are handled inline */ + /* GNU bash */ + { "ascii", C_ASCII }, + { "word", C_ALNUX }, + /* mksh */ + { "sh_alias", C_ALIAS }, + { "sh_edq", C_EDQ }, + { "sh_ifs", C_IFS }, + { "sh_ifsws", C_IFSWS }, + { "sh_nl", C_NL }, + { "sh_quote", C_QUOTE }, + /* sentinel */ + { NULL, 0 } +}; + +static const unsigned char * +gmatch_cclass(const unsigned char *pat, unsigned char sc) +{ + unsigned char c, subc, lc; + const unsigned char *p = pat, *s; + bool found = false; + bool negated = false; + char *subp; + + /* check for negation */ + if (ISMAGIC(p[0]) && ord(p[1]) == ORD('!')) { + p += 2; + negated = true; + } + /* make initial ] non-MAGIC */ + if (ISMAGIC(p[0]) && ord(p[1]) == ORD(']')) + ++p; + /* iterate over bracket expression, debunk()ing on the fly */ + while ((c = *p++)) { + nextc: + /* non-regular character? */ + if (ISMAGIC(c)) { + /* MAGIC + NUL cannot happen */ + if (!(c = *p++)) + break; + /* terminating bracket? */ + if (ord(c) == ORD(']')) { + /* accept and return */ + return (found != negated ? p : NULL); + } + /* sub-bracket expressions */ + if (ord(c) == ORD('[') && ( + /* collating element? */ + ord(*p) == ORD('.') || + /* equivalence class? */ + ord(*p) == ORD('=') || + /* character class? */ + ord(*p) == ORD(':'))) { + /* must stop with exactly the same c */ + subc = *p++; + /* save away start of substring */ + s = p; + /* arbitrarily many chars in betwixt */ + while ((c = *p++)) + /* but only this sequence... */ + if (c == subc && ISMAGIC(*p) && + ord(p[1]) == ORD(']')) { + /* accept, terminate */ + p += 2; + break; + } + /* EOS without: reject bracket expr */ + if (!c) + break; + /* debunk substring */ + strndupx(subp, s, p - s - 3, ATEMP); + debunk(subp, subp, p - s - 3 + 1); + cclass_common: + /* whither subexpression */ + if (ord(subc) == ORD(':')) { + const struct cclass *cls = cclasses; + + /* search for name in cclass list */ + while (cls->name) + if (!strcmp(subp, cls->name)) { + /* found, match? */ + if (ctype(sc, + cls->value)) + found = true; + /* break either way */ + break; + } else + ++cls; + /* that's all here */ + afree(subp, ATEMP); + continue; + } + /* collating element or equivalence class */ + /* Note: latter are treated as former */ + if (ctype(subp[0], C_ASCII) && !subp[1]) + /* [.a.] where a is one ASCII char */ + c = subp[0]; + else + /* force no match */ + c = 0; + /* no longer needed */ + afree(subp, ATEMP); + } else if (!ISMAGIC(c) && (c & 0x80)) { + /* 0x80|' ' is plain (...) */ + if ((c &= 0x7F) != ' ') { + /* check single match NOW */ + if (sc == c) + found = true; + /* next character is (...) */ + } + c = '(' /*)*/; + } + } + /* range expression? */ + if (!(ISMAGIC(p[0]) && ord(p[1]) == ORD('-') && + /* not terminating bracket? */ + (!ISMAGIC(p[2]) || ord(p[3]) != ORD(']')))) { + /* no, check single match */ + if (sc == c) + /* note: sc is never NUL */ + found = true; + /* do the next "first" character */ + continue; + } + /* save lower range bound */ + lc = c; + /* skip over the range operator */ + p += 2; + /* do the same shit as above... almost */ + subc = 0; + if (!(c = *p++)) + break; + /* non-regular character? */ + if (ISMAGIC(c)) { + /* MAGIC + NUL cannot happen */ + if (!(c = *p++)) + break; + /* sub-bracket expressions */ + if (ord(c) == ORD('[') && ( + /* collating element? */ + ord(*p) == ORD('.') || + /* equivalence class? */ + ord(*p) == ORD('=') || + /* character class? */ + ord(*p) == ORD(':'))) { + /* must stop with exactly the same c */ + subc = *p++; + /* save away start of substring */ + s = p; + /* arbitrarily many chars in betwixt */ + while ((c = *p++)) + /* but only this sequence... */ + if (c == subc && ISMAGIC(*p) && + ord(p[1]) == ORD(']')) { + /* accept, terminate */ + p += 2; + break; + } + /* EOS without: reject bracket expr */ + if (!c) + break; + /* debunk substring */ + strndupx(subp, s, p - s - 3, ATEMP); + debunk(subp, subp, p - s - 3 + 1); + /* whither subexpression */ + if (ord(subc) == ORD(':')) { + /* oops, not a range */ + + /* match single previous char */ + if (lc && (sc == lc)) + found = true; + /* match hyphen-minus */ + if (ord(sc) == ORD('-')) + found = true; + /* handle cclass common part */ + goto cclass_common; + } + /* collating element or equivalence class */ + /* Note: latter are treated as former */ + if (ctype(subp[0], C_ASCII) && !subp[1]) + /* [.a.] where a is one ASCII char */ + c = subp[0]; + else + /* force no match */ + c = 0; + /* no longer needed */ + afree(subp, ATEMP); + /* other meaning below */ + subc = 0; + } else if (c == (0x80 | ' ')) { + /* 0x80|' ' is plain (...) */ + c = '(' /*)*/; + } else if (!ISMAGIC(c) && (c & 0x80)) { + c &= 0x7F; + subc = '(' /*)*/; + } + } + /* now do the actual range match check */ + if (lc != 0 /* && c != 0 */ && + asciibetical(lc) <= asciibetical(sc) && + asciibetical(sc) <= asciibetical(c)) + found = true; + /* forced next character? */ + if (subc) { + c = subc; + goto nextc; + } + /* otherwise, just go on with the pattern string */ + } + /* if we broke here, the bracket expression was invalid */ + if (ord(sc) == ORD('[')) + /* initial opening bracket as literal match */ + return (pat); + /* or rather no match */ + return (NULL); +} + +/* Look for next ) or | (if match_sep) in *(foo|bar) pattern */ +static const unsigned char * +pat_scan(const unsigned char *p, const unsigned char *pe, bool match_sep) +{ + int nest = 0; + + for (; p < pe; p++) { + if (!ISMAGIC(*p)) + continue; + if ((*++p == /*(*/ ')' && nest-- == 0) || + (*p == '|' && match_sep && nest == 0)) + return (p + 1); + if ((*p & 0x80) && ctype(*p & 0x7F, C_PATMO | C_SPC)) + nest++; + } + return (NULL); +} + +int +ascstrcmp(const void *s1, const void *s2) +{ + const uint8_t *cp1 = s1, *cp2 = s2; + + while (*cp1 == *cp2) { + if (*cp1++ == '\0') + return (0); + ++cp2; + } + return ((int)asciibetical(*cp1) - (int)asciibetical(*cp2)); +} + +int +ascpstrcmp(const void *pstr1, const void *pstr2) +{ + return (ascstrcmp(*(const char * const *)pstr1, + *(const char * const *)pstr2)); +} + +/* Initialise a Getopt structure */ +void +ksh_getopt_reset(Getopt *go, int flags) +{ + go->optind = 1; + go->optarg = NULL; + go->p = 0; + go->flags = flags; + go->info = 0; + go->buf[1] = '\0'; +} + + +/** + * getopt() used for shell built-in commands, the getopts command, and + * command line options. + * A leading ':' in options means don't print errors, instead return '?' + * or ':' and set go->optarg to the offending option character. + * If GF_ERROR is set (and option doesn't start with :), errors result in + * a call to bi_errorf(). + * + * Non-standard features: + * - ';' is like ':' in options, except the argument is optional + * (if it isn't present, optarg is set to 0). + * Used for 'set -o'. + * - ',' is like ':' in options, except the argument always immediately + * follows the option character (optarg is set to the null string if + * the option is missing). + * Used for 'read -u2', 'print -u2' and fc -40. + * - '#' is like ':' in options, expect that the argument is optional + * and must start with a digit. If the argument doesn't start with a + * digit, it is assumed to be missing and normal option processing + * continues (optarg is set to 0 if the option is missing). + * Used for 'typeset -LZ4'. + * - accepts +c as well as -c IF the GF_PLUSOPT flag is present. If an + * option starting with + is accepted, the GI_PLUS flag will be set + * in go->info. + */ +int +ksh_getopt(const char **argv, Getopt *go, const char *optionsp) +{ + char c; + const char *o; + + if (go->p == 0 || (c = argv[go->optind - 1][go->p]) == '\0') { + const char *arg = argv[go->optind], flag = arg ? *arg : '\0'; + + go->p = 1; + if (flag == '-' && ksh_isdash(arg + 1)) { + go->optind++; + go->p = 0; + go->info |= GI_MINUSMINUS; + return (-1); + } + if (arg == NULL || + ((flag != '-' ) && + /* neither a - nor a + (if + allowed) */ + (!(go->flags & GF_PLUSOPT) || flag != '+')) || + (c = arg[1]) == '\0') { + go->p = 0; + return (-1); + } + go->optind++; + go->info &= ~(GI_MINUS|GI_PLUS); + go->info |= flag == '-' ? GI_MINUS : GI_PLUS; + } + go->p++; + if (ctype(c, C_QUEST | C_COLON | C_HASH) || c == ';' || c == ',' || + !(o = cstrchr(optionsp, c))) { + if (optionsp[0] == ':') { + go->buf[0] = c; + go->optarg = go->buf; + } else { + warningf(true, Tf_optfoo, + (go->flags & GF_NONAME) ? "" : argv[0], + (go->flags & GF_NONAME) ? "" : Tcolsp, + c, Tunknown_option); + if (go->flags & GF_ERROR) + bi_errorfz(); + } + return ('?'); + } + /** + * : means argument must be present, may be part of option argument + * or the next argument + * ; same as : but argument may be missing + * , means argument is part of option argument, and may be null. + */ + if (*++o == ':' || *o == ';') { + if (argv[go->optind - 1][go->p]) + go->optarg = argv[go->optind - 1] + go->p; + else if (argv[go->optind]) + go->optarg = argv[go->optind++]; + else if (*o == ';') + go->optarg = NULL; + else { + if (optionsp[0] == ':') { + go->buf[0] = c; + go->optarg = go->buf; + return (':'); + } + warningf(true, Tf_optfoo, + (go->flags & GF_NONAME) ? "" : argv[0], + (go->flags & GF_NONAME) ? "" : Tcolsp, + c, Treq_arg); + if (go->flags & GF_ERROR) + bi_errorfz(); + return ('?'); + } + go->p = 0; + } else if (*o == ',') { + /* argument is attached to option character, even if null */ + go->optarg = argv[go->optind - 1] + go->p; + go->p = 0; + } else if (*o == '#') { + /* + * argument is optional and may be attached or unattached + * but must start with a digit. optarg is set to 0 if the + * argument is missing. + */ + if (argv[go->optind - 1][go->p]) { + if (ctype(argv[go->optind - 1][go->p], C_DIGIT)) { + go->optarg = argv[go->optind - 1] + go->p; + go->p = 0; + } else + go->optarg = NULL; + } else { + if (argv[go->optind] && + ctype(argv[go->optind][0], C_DIGIT)) { + go->optarg = argv[go->optind++]; + go->p = 0; + } else + go->optarg = NULL; + } + } + return (c); +} + +/* + * print variable/alias value using necessary quotes + * (POSIX says they should be suitable for re-entry...) + * No trailing newline is printed. + */ +void +print_value_quoted(struct shf *shf, const char *s) +{ + unsigned char c; + const unsigned char *p = (const unsigned char *)s; + bool inquote = true; + + /* first, check whether any quotes are needed */ + while (rtt2asc(c = *p++) >= 32) + if (ctype(c, C_QUOTE | C_SPC)) + inquote = false; + + p = (const unsigned char *)s; + if (c == 0) { + if (inquote) { + /* nope, use the shortcut */ + shf_puts(s, shf); + return; + } + + /* otherwise, quote nicely via state machine */ + while ((c = *p++) != 0) { + if (c == '\'') { + /* + * multiple single quotes or any of them + * at the beginning of a string look nicer + * this way than when simply substituting + */ + if (inquote) { + shf_putc('\'', shf); + inquote = false; + } + shf_putc('\\', shf); + } else if (!inquote) { + shf_putc('\'', shf); + inquote = true; + } + shf_putc(c, shf); + } + } else { + unsigned int wc; + size_t n; + + /* use $'...' quote format */ + shf_putc('$', shf); + shf_putc('\'', shf); + while ((c = *p) != 0) { +#ifndef MKSH_EBCDIC + if (c >= 0xC2) { + n = utf_mbtowc(&wc, (const char *)p); + if (n != (size_t)-1) { + p += n; + shf_fprintf(shf, "\\u%04X", wc); + continue; + } + } +#endif + ++p; + switch (c) { + /* see unbksl() in this file for comments */ + case KSH_BEL: + c = 'a'; + if (0) + /* FALLTHROUGH */ + case '\b': + c = 'b'; + if (0) + /* FALLTHROUGH */ + case '\f': + c = 'f'; + if (0) + /* FALLTHROUGH */ + case '\n': + c = 'n'; + if (0) + /* FALLTHROUGH */ + case '\r': + c = 'r'; + if (0) + /* FALLTHROUGH */ + case '\t': + c = 't'; + if (0) + /* FALLTHROUGH */ + case KSH_VTAB: + c = 'v'; + if (0) + /* FALLTHROUGH */ + case KSH_ESC: + /* take E not e because \e is \ in *roff */ + c = 'E'; + /* FALLTHROUGH */ + case '\\': + shf_putc('\\', shf); + + if (0) + /* FALLTHROUGH */ + default: +#if defined(MKSH_EBCDIC) || defined(MKSH_FAUX_EBCDIC) + if (ksh_isctrl(c)) +#else + if (!ctype(c, C_PRINT)) +#endif + { + /* FALLTHROUGH */ + case '\'': + shf_fprintf(shf, "\\%03o", c); + break; + } + + shf_putc(c, shf); + break; + } + } + inquote = true; + } + if (inquote) + shf_putc('\'', shf); +} + +/* + * Print things in columns and rows - func() is called to format + * the i-th element + */ +void +print_columns(struct columnise_opts *opts, unsigned int n, + void (*func)(char *, size_t, unsigned int, const void *), + const void *arg, size_t max_oct, size_t max_colz) +{ + unsigned int i, r = 0, c, rows, cols, nspace, max_col; + char *str; + + if (!n) + return; + + if (max_colz > 2147483646) { +#ifndef MKSH_SMALL + internal_warningf("print_columns called with %s=%zu >= INT_MAX", + "max_col", max_colz); +#endif + return; + } + max_col = (unsigned int)max_colz; + + if (max_oct > 2147483646) { +#ifndef MKSH_SMALL + internal_warningf("print_columns called with %s=%zu >= INT_MAX", + "max_oct", max_oct); +#endif + return; + } + ++max_oct; + str = alloc(max_oct, ATEMP); + + /* + * We use (max_col + 2) to consider the separator space. + * Note that no spaces are printed after the last column + * to avoid problems with terminals that have auto-wrap, + * but we need to also take this into account in x_cols. + */ + cols = (x_cols + 1) / (max_col + 2); + + /* if we can only print one column anyway, skip the goo */ + if (cols < 2) { + goto prcols_easy; + while (r < n) { + shf_putc(opts->linesep, opts->shf); + prcols_easy: + (*func)(str, max_oct, r++, arg); + shf_puts(str, opts->shf); + } + goto out; + } + + rows = (n + cols - 1) / cols; + if (opts->prefcol && cols > rows) { + cols = rows; + rows = (n + cols - 1) / cols; + } + + nspace = (x_cols - max_col * cols) / cols; + if (nspace < 2) + nspace = 2; + max_col = -max_col; + goto prcols_hard; + while (r < rows) { + shf_putchar(opts->linesep, opts->shf); + prcols_hard: + for (c = 0; c < cols; c++) { + if ((i = c * rows + r) >= n) + break; + (*func)(str, max_oct, i, arg); + if (i + rows >= n) + shf_puts(str, opts->shf); + else + shf_fprintf(opts->shf, "%*s%*s", + (int)max_col, str, (int)nspace, null); + } + ++r; + } + out: + if (opts->do_last) + shf_putchar(opts->linesep, opts->shf); + afree(str, ATEMP); +} + +/* strip all NUL bytes from buf; output is NUL-terminated if stripped */ +void +strip_nuls(char *buf, size_t len) +{ + char *cp, *dp, *ep; + + if (!len || !(dp = memchr(buf, '\0', len))) + return; + + ep = buf + len; + cp = dp; + + cp_has_nul_byte: + while (cp++ < ep && *cp == '\0') + ; /* nothing */ + while (cp < ep && *cp != '\0') + *dp++ = *cp++; + if (cp < ep) + goto cp_has_nul_byte; + + *dp = '\0'; +} + +/* + * Like read(2), but if read fails due to non-blocking flag, + * resets flag and restarts read. + */ +ssize_t +blocking_read(int fd, char *buf, size_t nbytes) +{ + ssize_t ret; + bool tried_reset = false; + + while ((ret = read(fd, buf, nbytes)) < 0) { + if (!tried_reset && errno == EAGAIN) { + if (reset_nonblock(fd) > 0) { + tried_reset = true; + continue; + } + errno = EAGAIN; + } + break; + } + return (ret); +} + +/* + * Reset the non-blocking flag on the specified file descriptor. + * Returns -1 if there was an error, 0 if non-blocking wasn't set, + * 1 if it was. + */ +int +reset_nonblock(int fd) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL, 0)) < 0) + return (-1); + if (!(flags & O_NONBLOCK)) + return (0); + flags &= ~O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) < 0) + return (-1); + return (1); +} + +/* getcwd(3) equivalent, allocates from ATEMP but doesn't resize */ +char * +ksh_get_wd(void) +{ +#ifdef MKSH__NO_PATH_MAX + char *rv, *cp; + + if ((cp = get_current_dir_name())) { + strdupx(rv, cp, ATEMP); + free_gnu_gcdn(cp); + } else + rv = NULL; +#else + char *rv; + + if (!getcwd((rv = alloc(PATH_MAX + 1, ATEMP)), PATH_MAX)) { + afree(rv, ATEMP); + rv = NULL; + } +#endif + + return (rv); +} + +#ifndef ELOOP +#define ELOOP E2BIG +#endif + +char * +do_realpath(const char *upath) +{ + char *xp, *ip, *tp, *ipath, *ldest = NULL; + XString xs; + size_t pos, len; + int llen; + struct stat sb; +#ifdef MKSH__NO_PATH_MAX + size_t ldestlen = 0; +#define pathlen sb.st_size +#define pathcnd (ldestlen < (pathlen + 1)) +#else +#define pathlen PATH_MAX +#define pathcnd (!ldest) +#endif + /* max. recursion depth */ + int symlinks = 32; + + if (mksh_abspath(upath)) { + /* upath is an absolute pathname */ + strdupx(ipath, upath, ATEMP); +#ifdef MKSH_DOSPATH + } else if (mksh_drvltr(upath)) { + /* upath is a drive-relative pathname */ + if (getdrvwd(&ldest, ord(*upath))) + return (NULL); + /* A:foo -> A:/cwd/foo; A: -> A:/cwd */ + ipath = shf_smprintf(Tf_sss, ldest, + upath[2] ? "/" : "", upath + 2); +#endif + } else { + /* upath is a relative pathname, prepend cwd */ + if ((tp = ksh_get_wd()) == NULL || !mksh_abspath(tp)) + return (NULL); + ipath = shf_smprintf(Tf_sss, tp, "/", upath); + afree(tp, ATEMP); + } + + /* ipath and upath are in memory at the same time -> unchecked */ + Xinit(xs, xp, strlen(ip = ipath) + 1, ATEMP); + + /* now jump into the deep of the loop */ + goto beginning_of_a_pathname; + + while (*ip) { + /* skip slashes in input */ + while (mksh_cdirsep(*ip)) + ++ip; + if (!*ip) + break; + + /* get next pathname component from input */ + tp = ip; + while (*ip && !mksh_cdirsep(*ip)) + ++ip; + len = ip - tp; + + /* check input for "." and ".." */ + if (tp[0] == '.') { + if (len == 1) + /* just continue with the next one */ + continue; + else if (len == 2 && tp[1] == '.') { + /* strip off last pathname component */ + /*XXX consider a rooted pathname */ + while (xp > Xstring(xs, xp)) + if (mksh_cdirsep(*--xp)) + break; + /* then continue with the next one */ + continue; + } + } + + /* store output position away, then append slash to output */ + pos = Xsavepos(xs, xp); + /* 1 for the '/' and len + 1 for tp and the NUL from below */ + XcheckN(xs, xp, 1 + len + 1); + Xput(xs, xp, '/'); + + /* append next pathname component to output */ + memcpy(xp, tp, len); + xp += len; + *xp = '\0'; + + /* lstat the current output, see if it's a symlink */ + if (mksh_lstat(Xstring(xs, xp), &sb)) { + /* lstat failed */ + if (errno == ENOENT) { + /* because the pathname does not exist */ + while (mksh_cdirsep(*ip)) + /* skip any trailing slashes */ + ++ip; + /* no more components left? */ + if (!*ip) + /* we can still return successfully */ + break; + /* more components left? fall through */ + } + /* not ENOENT or not at the end of ipath */ + goto notfound; + } + + /* check if we encountered a symlink? */ + if (S_ISLNK(sb.st_mode)) { +#ifndef MKSH__NO_SYMLINK + /* reached maximum recursion depth? */ + if (!symlinks--) { + /* yep, prevent infinite loops */ + errno = ELOOP; + goto notfound; + } + + /* get symlink(7) target */ + if (pathcnd) { +#ifdef MKSH__NO_PATH_MAX + if (notoktoadd(pathlen, 1)) { + errno = ENAMETOOLONG; + goto notfound; + } +#endif + ldest = aresize(ldest, pathlen + 1, ATEMP); + } + llen = readlink(Xstring(xs, xp), ldest, pathlen); + if (llen < 0) + /* oops... */ + goto notfound; + ldest[llen] = '\0'; + + /* + * restart if symlink target is an absolute path, + * otherwise continue with currently resolved prefix + */ +#ifdef MKSH_DOSPATH + assemble_symlink: +#endif + /* append rest of current input path to link target */ + tp = shf_smprintf(Tf_sss, ldest, *ip ? "/" : "", ip); + afree(ipath, ATEMP); + ip = ipath = tp; + if (!mksh_abspath(ipath)) { +#ifdef MKSH_DOSPATH + /* symlink target might be drive-relative */ + if (mksh_drvltr(ipath)) { + if (getdrvwd(&ldest, ord(*ipath))) + goto notfound; + ip += 2; + goto assemble_symlink; + } +#endif + /* symlink target is a relative path */ + xp = Xrestpos(xs, xp, pos); + } else +#endif + { + /* symlink target is an absolute path */ + xp = Xstring(xs, xp); + beginning_of_a_pathname: + /* assert: mksh_abspath(ip == ipath) */ + /* assert: xp == xs.beg => start of path */ + + /* exactly two leading slashes? (SUSv4 3.266) */ + if (ip[1] == ip[0] && !mksh_cdirsep(ip[2])) { + /* keep them, e.g. for UNC pathnames */ + Xput(xs, xp, '/'); + } +#ifdef MKSH_DOSPATH + /* drive letter? */ + if (mksh_drvltr(ip)) { + /* keep it */ + Xput(xs, xp, *ip++); + Xput(xs, xp, *ip++); + } +#endif + } + } + /* otherwise (no symlink) merely go on */ + } + + /* + * either found the target and successfully resolved it, + * or found its parent directory and may create it + */ + if (Xlength(xs, xp) == 0) + /* + * if the resolved pathname is "", make it "/", + * otherwise do not add a trailing slash + */ + Xput(xs, xp, '/'); + Xput(xs, xp, '\0'); + + /* + * if source path had a trailing slash, check if target path + * is not a non-directory existing file + */ + if (ip > ipath && mksh_cdirsep(ip[-1])) { + if (stat(Xstring(xs, xp), &sb)) { + if (errno != ENOENT) + goto notfound; + } else if (!S_ISDIR(sb.st_mode)) { + errno = ENOTDIR; + goto notfound; + } + /* target now either does not exist or is a directory */ + } + + /* return target path */ + afree(ldest, ATEMP); + afree(ipath, ATEMP); + return (Xclose(xs, xp)); + + notfound: + /* save; freeing memory might trash it */ + llen = errno; + afree(ldest, ATEMP); + afree(ipath, ATEMP); + Xfree(xs, xp); + errno = llen; + return (NULL); + +#undef pathlen +#undef pathcnd +} + +/** + * Makes a filename into result using the following algorithm. + * - make result NULL + * - if file starts with '/', append file to result & set cdpathp to NULL + * - if file starts with ./ or ../ append cwd and file to result + * and set cdpathp to NULL + * - if the first element of cdpathp doesnt start with a '/' xx or '.' xx + * then cwd is appended to result. + * - the first element of cdpathp is appended to result + * - file is appended to result + * - cdpathp is set to the start of the next element in cdpathp (or NULL + * if there are no more elements. + * The return value indicates whether a non-null element from cdpathp + * was appended to result. + */ +static int +make_path(const char *cwd, const char *file, + /* pointer to colon-separated list */ + char **cdpathp, + XString *xsp, + int *phys_pathp) +{ + int rval = 0; + bool use_cdpath = true; + char *plist; + size_t len, plen = 0; + char *xp = Xstring(*xsp, xp); + + if (!file) + file = null; + + if (mksh_abspath(file)) { + *phys_pathp = 0; + use_cdpath = false; + } else { + if (file[0] == '.') { + char c = file[1]; + + if (c == '.') + c = file[2]; + if (mksh_cdirsep(c) || c == '\0') + use_cdpath = false; + } + + plist = *cdpathp; + if (!plist) + use_cdpath = false; + else if (use_cdpath) { + char *pend = plist; + + while (*pend && *pend != MKSH_PATHSEPC) + ++pend; + plen = pend - plist; + *cdpathp = *pend ? pend + 1 : NULL; + } + + if ((!use_cdpath || !plen || !mksh_abspath(plist)) && + (cwd && *cwd)) { + len = strlen(cwd); + XcheckN(*xsp, xp, len); + memcpy(xp, cwd, len); + xp += len; + if (!mksh_cdirsep(cwd[len - 1])) + Xput(*xsp, xp, '/'); + } + *phys_pathp = Xlength(*xsp, xp); + if (use_cdpath && plen) { + XcheckN(*xsp, xp, plen); + memcpy(xp, plist, plen); + xp += plen; + if (!mksh_cdirsep(plist[plen - 1])) + Xput(*xsp, xp, '/'); + rval = 1; + } + } + + len = strlen(file) + 1; + XcheckN(*xsp, xp, len); + memcpy(xp, file, len); + + if (!use_cdpath) + *cdpathp = NULL; + + return (rval); +} + +/*- + * Simplify pathnames containing "." and ".." entries. + * + * simplify_path(this) = that + * /a/b/c/./../d/.. /a/b + * //./C/foo/bar/../baz //C/foo/baz + * /foo/ /foo + * /foo/../../bar /bar + * /foo/./blah/.. /foo + * . . + * .. .. + * ./foo foo + * foo/../../../bar ../../bar + * C:/foo/../.. C:/ + * C:. C: + * C:.. C:.. + * C:foo/../../blah C:../blah + * + * XXX consider a rooted pathname: we cannot really 'cd ..' for + * pathnames like: '/', 'c:/', '//foo', '//foo/', '/@unixroot/' + * (no effect), 'c:', 'c:.' (effect is retaining the '../') but + * we need to honour this throughout the shell + */ +void +simplify_path(char *p) +{ + char *dp, *ip, *sp, *tp; + size_t len; + bool needslash; +#ifdef MKSH_DOSPATH + bool needdot = true; + + /* keep drive letter */ + if (mksh_drvltr(p)) { + p += 2; + needdot = false; + } +#else +#define needdot true +#endif + + switch (*p) { + case 0: + return; + case '/': +#ifdef MKSH_DOSPATH + case '\\': +#endif + /* exactly two leading slashes? (SUSv4 3.266) */ + if (p[1] == p[0] && !mksh_cdirsep(p[2])) + /* keep them, e.g. for UNC pathnames */ + ++p; + needslash = true; + break; + default: + needslash = false; + } + dp = ip = sp = p; + + while (*ip) { + /* skip slashes in input */ + while (mksh_cdirsep(*ip)) + ++ip; + if (!*ip) + break; + + /* get next pathname component from input */ + tp = ip; + while (*ip && !mksh_cdirsep(*ip)) + ++ip; + len = ip - tp; + + /* check input for "." and ".." */ + if (tp[0] == '.') { + if (len == 1) + /* just continue with the next one */ + continue; + else if (len == 2 && tp[1] == '.') { + /* parent level, but how? (see above) */ + if (mksh_abspath(p)) + /* absolute path, only one way */ + goto strip_last_component; + else if (dp > sp) { + /* relative path, with subpaths */ + needslash = false; + strip_last_component: + /* strip off last pathname component */ + while (dp > sp) + if (mksh_cdirsep(*--dp)) + break; + } else { + /* relative path, at its beginning */ + if (needslash) + /* or already dotdot-slash'd */ + *dp++ = '/'; + /* keep dotdot-slash if not absolute */ + *dp++ = '.'; + *dp++ = '.'; + needslash = true; + sp = dp; + } + /* then continue with the next one */ + continue; + } + } + + if (needslash) + *dp++ = '/'; + + /* append next pathname component to output */ + memmove(dp, tp, len); + dp += len; + + /* append slash if we continue */ + needslash = true; + /* try next component */ + } + if (dp == p) { + /* empty path -> dot (or slash, when absolute) */ + if (needslash) + *dp++ = '/'; + else if (needdot) + *dp++ = '.'; + } + *dp = '\0'; +#undef needdot +} + +void +set_current_wd(const char *nwd) +{ + char *allocd = NULL; + + if (nwd == NULL) { + allocd = ksh_get_wd(); + nwd = allocd ? allocd : null; + } + + afree(current_wd, APERM); + strdupx(current_wd, nwd, APERM); + + afree(allocd, ATEMP); +} + +int +c_cd(const char **wp) +{ + int optc, rv, phys_path; + bool physical = tobool(Flag(FPHYSICAL)); + /* was a node from cdpath added in? */ + int cdnode; + /* show where we went?, error for $PWD */ + bool printpath = false, eflag = false; + struct tbl *pwd_s, *oldpwd_s; + XString xs; + char *dir, *allocd = NULL, *tryp, *pwd, *cdpath; + + while ((optc = ksh_getopt(wp, &builtin_opt, "eLP")) != -1) + switch (optc) { + case 'e': + eflag = true; + break; + case 'L': + physical = false; + break; + case 'P': + physical = true; + break; + case '?': + return (2); + } + wp += builtin_opt.optind; + + if (Flag(FRESTRICTED)) { + bi_errorf(Tcant_cd); + return (2); + } + + pwd_s = global(TPWD); + oldpwd_s = global(TOLDPWD); + + if (!wp[0]) { + /* No arguments - go home */ + if ((dir = str_val(global("HOME"))) == null) { + bi_errorf("no home directory (HOME not set)"); + return (2); + } + } else if (!wp[1]) { + /* One argument: - or dir */ + strdupx(allocd, wp[0], ATEMP); + if (ksh_isdash((dir = allocd))) { + afree(allocd, ATEMP); + allocd = NULL; + dir = str_val(oldpwd_s); + if (dir == null) { + bi_errorf(Tno_OLDPWD); + return (2); + } + printpath = true; + } + } else if (!wp[2]) { + /* Two arguments - substitute arg1 in PWD for arg2 */ + size_t ilen, olen, nlen, elen; + char *cp; + + if (!current_wd[0]) { + bi_errorf("can't determine current directory"); + return (2); + } + /* + * substitute arg1 for arg2 in current path. + * if the first substitution fails because the cd fails + * we could try to find another substitution. For now + * we don't + */ + if ((cp = strstr(current_wd, wp[0])) == NULL) { + bi_errorf(Tbadsubst); + return (2); + } + /*- + * ilen = part of current_wd before wp[0] + * elen = part of current_wd after wp[0] + * because current_wd and wp[1] need to be in memory at the + * same time beforehand the addition can stay unchecked + */ + ilen = cp - current_wd; + olen = strlen(wp[0]); + nlen = strlen(wp[1]); + elen = strlen(current_wd + ilen + olen) + 1; + dir = allocd = alloc(ilen + nlen + elen, ATEMP); + memcpy(dir, current_wd, ilen); + memcpy(dir + ilen, wp[1], nlen); + memcpy(dir + ilen + nlen, current_wd + ilen + olen, elen); + printpath = true; + } else { + bi_errorf(Ttoo_many_args); + return (2); + } + +#ifdef MKSH_DOSPATH + tryp = NULL; + if (mksh_drvltr(dir) && !mksh_cdirsep(dir[2]) && + !getdrvwd(&tryp, ord(*dir))) { + dir = shf_smprintf(Tf_sss, tryp, + dir[2] ? "/" : "", dir + 2); + afree(tryp, ATEMP); + afree(allocd, ATEMP); + allocd = dir; + } +#endif + +#ifdef MKSH__NO_PATH_MAX + /* only a first guess; make_path will enlarge xs if necessary */ + XinitN(xs, 1024, ATEMP); +#else + XinitN(xs, PATH_MAX, ATEMP); +#endif + + cdpath = str_val(global("CDPATH")); + do { + cdnode = make_path(current_wd, dir, &cdpath, &xs, &phys_path); + if (physical) + rv = chdir(tryp = Xstring(xs, xp) + phys_path); + else { + simplify_path(Xstring(xs, xp)); + rv = chdir(tryp = Xstring(xs, xp)); + } + } while (rv < 0 && cdpath != NULL); + + if (rv < 0) { + if (cdnode) + bi_errorf(Tf_sD_s, dir, "bad directory"); + else + bi_errorf(Tf_sD_s, tryp, cstrerror(errno)); + afree(allocd, ATEMP); + Xfree(xs, xp); + return (2); + } + + rv = 0; + + /* allocd (above) => dir, which is no longer used */ + afree(allocd, ATEMP); + allocd = NULL; + + /* Clear out tracked aliases with relative paths */ + flushcom(false); + + /* + * Set OLDPWD (note: unsetting OLDPWD does not disable this + * setting in AT&T ksh) + */ + if (current_wd[0]) + /* Ignore failure (happens if readonly or integer) */ + setstr(oldpwd_s, current_wd, KSH_RETURN_ERROR); + + if (!mksh_abspath(Xstring(xs, xp))) { + pwd = NULL; + } else if (!physical) { + goto norealpath_PWD; + } else if ((pwd = allocd = do_realpath(Xstring(xs, xp))) == NULL) { + if (eflag) + rv = 1; + norealpath_PWD: + pwd = Xstring(xs, xp); + } + + /* Set PWD */ + if (pwd) { + char *ptmp = pwd; + + set_current_wd(ptmp); + /* Ignore failure (happens if readonly or integer) */ + setstr(pwd_s, ptmp, KSH_RETURN_ERROR); + } else { + set_current_wd(null); + pwd = Xstring(xs, xp); + /* XXX unset $PWD? */ + if (eflag) + rv = 1; + } + if (printpath || cdnode) + shprintf(Tf_sN, pwd); + + afree(allocd, ATEMP); + Xfree(xs, xp); + return (rv); +} + + +#ifdef KSH_CHVT_CODE +extern void chvt_reinit(void); + +static void +chvt(const Getopt *go) +{ + const char *dv = go->optarg; + char *cp = NULL; + int fd; + + switch (*dv) { + case '-': + dv = "/dev/null"; + break; + case '!': + ++dv; + /* FALLTHROUGH */ + default: { + struct stat sb; + + if (stat(dv, &sb)) { + cp = shf_smprintf("/dev/ttyC%s", dv); + dv = cp; + if (stat(dv, &sb)) { + memmove(cp + 1, cp, /* /dev/tty */ 8); + dv = cp + 1; + if (stat(dv, &sb)) { + errorf(Tf_sD_sD_s, "chvt", + "can't find tty", go->optarg); + } + } + } + if (!(sb.st_mode & S_IFCHR)) + errorf(Tf_sD_sD_s, "chvt", "not a char device", dv); +#ifndef MKSH_DISABLE_REVOKE_WARNING +#if HAVE_REVOKE + if (revoke(dv)) +#endif + warningf(false, Tf_sD_s_s, "chvt", + "new shell is potentially insecure, can't revoke", + dv); +#endif + } + } + if ((fd = binopen2(dv, O_RDWR)) < 0) { + sleep(1); + if ((fd = binopen2(dv, O_RDWR)) < 0) { + errorf(Tf_sD_s_s, "chvt", Tcant_open, dv); + } + } + if (go->optarg[0] != '!') { + switch (fork()) { + case -1: + errorf(Tf_sD_s_s, "chvt", "fork", "failed"); + case 0: + break; + default: + exit(0); + } + } + if (setsid() == -1) + errorf(Tf_sD_s_s, "chvt", "setsid", "failed"); + if (go->optarg[0] != '-') { + if (ioctl(fd, TIOCSCTTY, NULL) == -1) + errorf(Tf_sD_s_s, "chvt", "TIOCSCTTY", "failed"); + if (tcflush(fd, TCIOFLUSH)) + errorf(Tf_sD_s_s, "chvt", "TCIOFLUSH", "failed"); + } + ksh_dup2(fd, 0, false); + ksh_dup2(fd, 1, false); + ksh_dup2(fd, 2, false); + if (fd > 2) + close(fd); + rndset((unsigned long)chvt_rndsetup(go, sizeof(Getopt))); + chvt_reinit(); +} +#endif + +#ifdef DEBUG +char * +strchr(char *p, int ch) +{ + for (;; ++p) { + if (*p == ch) + return (p); + if (!*p) + return (NULL); + } + /* NOTREACHED */ +} + +char * +strstr(char *b, const char *l) +{ + char first, c; + size_t n; + + if ((first = *l++) == '\0') + return (b); + n = strlen(l); + strstr_look: + while ((c = *b++) != first) + if (c == '\0') + return (NULL); + if (strncmp(b, l, n)) + goto strstr_look; + return (b - 1); +} +#endif + +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +char * +strndup_i(const char *src, size_t len, Area *ap) +{ + char *dst = NULL; + + if (src != NULL) { + dst = alloc(len + 1, ap); + memcpy(dst, src, len); + dst[len] = '\0'; + } + return (dst); +} + +char * +strdup_i(const char *src, Area *ap) +{ + return (src == NULL ? NULL : strndup_i(src, strlen(src), ap)); +} +#endif + +#if !HAVE_GETRUSAGE +#define INVTCK(r,t) do { \ + r.tv_usec = ((t) % (1000000 / CLK_TCK)) * (1000000 / CLK_TCK); \ + r.tv_sec = (t) / CLK_TCK; \ +} while (/* CONSTCOND */ 0) + +int +getrusage(int what, struct rusage *ru) +{ + struct tms tms; + clock_t u, s; + + if (/* ru == NULL || */ times(&tms) == (clock_t)-1) + return (-1); + + switch (what) { + case RUSAGE_SELF: + u = tms.tms_utime; + s = tms.tms_stime; + break; + case RUSAGE_CHILDREN: + u = tms.tms_cutime; + s = tms.tms_cstime; + break; + default: + errno = EINVAL; + return (-1); + } + INVTCK(ru->ru_utime, u); + INVTCK(ru->ru_stime, s); + return (0); +} +#endif + +/* + * process the string available via fg (get a char) + * and fp (put back a char) for backslash escapes, + * assuming the first call to *fg gets the char di- + * rectly after the backslash; return the character + * (0..0xFF), Unicode (wc + 0x100), or -1 if no known + * escape sequence was found + */ +int +unbksl(bool cstyle, int (*fg)(void), void (*fp)(int)) +{ + int wc, i, c, fc, n; + + fc = (*fg)(); + switch (fc) { + case 'a': + wc = KSH_BEL; + break; + case 'b': + wc = '\b'; + break; + case 'c': + if (!cstyle) + goto unknown_escape; + c = (*fg)(); + wc = ksh_toctrl(c); + break; + case 'E': + case 'e': + wc = KSH_ESC; + break; + case 'f': + wc = '\f'; + break; + case 'n': + wc = '\n'; + break; + case 'r': + wc = '\r'; + break; + case 't': + wc = '\t'; + break; + case 'v': + wc = KSH_VTAB; + break; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + if (!cstyle) + goto unknown_escape; + /* FALLTHROUGH */ + case '0': + if (cstyle) + (*fp)(fc); + /* + * look for an octal number with up to three + * digits, not counting the leading zero; + * convert it to a raw octet + */ + wc = 0; + i = 3; + while (i--) + if (ctype((c = (*fg)()), C_OCTAL)) + wc = (wc << 3) + ksh_numdig(c); + else { + (*fp)(c); + break; + } + break; + case 'U': + i = 8; + if (/* CONSTCOND */ 0) + /* FALLTHROUGH */ + case 'u': + i = 4; + if (/* CONSTCOND */ 0) + /* FALLTHROUGH */ + case 'x': + i = cstyle ? -1 : 2; + /** + * x: look for a hexadecimal number with up to + * two (C style: arbitrary) digits; convert + * to raw octet (C style: Unicode if >0xFF) + * u/U: look for a hexadecimal number with up to + * four (U: eight) digits; convert to Unicode + */ + wc = 0; + n = 0; + while (n < i || i == -1) { + wc <<= 4; + if (!ctype((c = (*fg)()), C_SEDEC)) { + wc >>= 4; + (*fp)(c); + break; + } + if (ctype(c, C_DIGIT)) + wc += ksh_numdig(c); + else if (ctype(c, C_UPPER)) + wc += ksh_numuc(c) + 10; + else + wc += ksh_numlc(c) + 10; + ++n; + } + if (!n) + goto unknown_escape; + if ((cstyle && wc > 0xFF) || fc != 'x') + /* Unicode marker */ + wc += 0x100; + break; + case '\'': + if (!cstyle) + goto unknown_escape; + wc = '\''; + break; + case '\\': + wc = '\\'; + break; + default: + unknown_escape: + (*fp)(fc); + return (-1); + } + + return (wc); +} diff --git a/mksh.1 b/mksh.1 new file mode 100644 index 0000000..aa67ac9 --- /dev/null +++ b/mksh.1 @@ -0,0 +1,6906 @@ +.\" $MirOS: src/bin/mksh/mksh.1,v 1.451 2017/08/16 21:40:14 tg Exp $ +.\" $OpenBSD: ksh.1,v 1.160 2015/07/04 13:27:04 feinerer Exp $ +.\"- +.\" Copyright © 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, +.\" 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 +.\" mirabilos +.\" +.\" Provided that these terms and disclaimer and all copyright notices +.\" are retained or reproduced in an accompanying document, permission +.\" is granted to deal in this work without restriction, including un‐ +.\" limited rights to use, publicly perform, distribute, sell, modify, +.\" merge, give away, or sublicence. +.\" +.\" This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to +.\" the utmost extent permitted by applicable law, neither express nor +.\" implied; without malicious intent or gross negligence. In no event +.\" may a licensor, author or contributor be held liable for indirect, +.\" direct, other damage, loss, or other issues arising in any way out +.\" of dealing in the work, even if advised of the possibility of such +.\" damage or existence of a defect, except proven that it results out +.\" of said person’s immediate fault when using the work as intended. +.\"- +.\" Try to make GNU groff and AT&T nroff more compatible +.\" * ` generates ‘ in gnroff, so use \` +.\" * ' generates ’ in gnroff, \' generates ´, so use \*(aq +.\" * - generates ‐ in gnroff, \- generates −, so .tr it to - +.\" thus use - for hyphens and \- for minus signs and option dashes +.\" * ~ is size-reduced and placed atop in groff, so use \*(TI +.\" * ^ is size-reduced and placed atop in groff, so use \*(ha +.\" * \(en does not work in nroff, so use \*(en +.\" * <>| are problematic, so redefine and use \*(Lt\*(Gt\*(Ba +.\" Also make sure to use \& *before* a punctuation char that is to not +.\" be interpreted as punctuation, and especially with two-letter words +.\" but also (after) a period that does not end a sentence (“e.g.\&”). +.\" The section after the "doc" macropackage has been loaded contains +.\" additional code to convene between the UCB mdoc macropackage (and +.\" its variant as BSD mdoc in groff) and the GNU mdoc macropackage. +.\" +.ie \n(.g \{\ +. if \*[.T]ascii .tr \-\N'45' +. if \*[.T]latin1 .tr \-\N'45' +. if \*[.T]utf8 .tr \-\N'45' +. ds <= \[<=] +. ds >= \[>=] +. ds Rq \[rq] +. ds Lq \[lq] +. ds sL \(aq +. ds sR \(aq +. if \*[.T]utf8 .ds sL ` +. if \*[.T]ps .ds sL ` +. if \*[.T]utf8 .ds sR ' +. if \*[.T]ps .ds sR ' +. ds aq \(aq +. ds TI \(ti +. ds ha \(ha +. ds en \(en +.\} +.el \{\ +. ds aq ' +. ds TI ~ +. ds ha ^ +. ds en \(em +.\} +.\" +.\" Implement .Dd with the Mdocdate RCS keyword +.\" +.rn Dd xD +.de Dd +.ie \\$1$Mdocdate: \{\ +. xD \\$2 \\$3, \\$4 +.\} +.el .xD \\$1 \\$2 \\$3 \\$4 \\$5 \\$6 \\$7 \\$8 +.. +.\" +.\" .Dd must come before definition of .Mx, because when called +.\" with -mandoc, it might implement .Mx itself, but we want to +.\" use our own definition. And .Dd must come *first*, always. +.\" +.Dd $Mdocdate: August 16 2017 $ +.\" +.\" Check which macro package we use, and do other -mdoc setup. +.\" +.ie \n(.g \{\ +. if \*[.T]utf8 .tr \[la]\*(Lt +. if \*[.T]utf8 .tr \[ra]\*(Gt +. ie d volume-ds-1 .ds tT gnu +. el .ds tT bsd +.\} +.el .ds tT ucb +.\" +.\" Implement .Mx (MirBSD) +.\" +.ie "\*(tT"gnu" \{\ +. eo +. de Mx +. nr curr-font \n[.f] +. nr curr-size \n[.ps] +. ds str-Mx \f[\n[curr-font]]\s[\n[curr-size]u] +. ds str-Mx1 \*[Tn-font-size]\%MirOS\*[str-Mx] +. if !\n[arg-limit] \ +. if \n[.$] \{\ +. ds macro-name Mx +. parse-args \$@ +. \} +. if (\n[arg-limit] > \n[arg-ptr]) \{\ +. nr arg-ptr +1 +. ie (\n[type\n[arg-ptr]] == 2) \ +. as str-Mx1 \~\*[arg\n[arg-ptr]] +. el \ +. nr arg-ptr -1 +. \} +. ds arg\n[arg-ptr] "\*[str-Mx1] +. nr type\n[arg-ptr] 2 +. ds space\n[arg-ptr] "\*[space] +. nr num-args (\n[arg-limit] - \n[arg-ptr]) +. nr arg-limit \n[arg-ptr] +. if \n[num-args] \ +. parse-space-vector +. print-recursive +.. +. ec +. ds sP \s0 +. ds tN \*[Tn-font-size] +.\} +.el \{\ +. de Mx +. nr cF \\n(.f +. nr cZ \\n(.s +. ds aa \&\f\\n(cF\s\\n(cZ +. if \\n(aC==0 \{\ +. ie \\n(.$==0 \&MirOS\\*(aa +. el .aV \\$1 \\$2 \\$3 \\$4 \\$5 \\$6 \\$7 \\$8 \\$9 +. \} +. if \\n(aC>\\n(aP \{\ +. nr aP \\n(aP+1 +. ie \\n(C\\n(aP==2 \{\ +. as b1 \&MirOS\ #\&\\*(A\\n(aP\\*(aa +. ie \\n(aC>\\n(aP \{\ +. nr aP \\n(aP+1 +. nR +. \} +. el .aZ +. \} +. el \{\ +. as b1 \&MirOS\\*(aa +. nR +. \} +. \} +.. +.\} +.\"- +.Dt MKSH 1 +.Os MirBSD +.Sh NAME +.Nm mksh , +.Nm sh +.Nd MirBSD Korn shell +.Sh SYNOPSIS +.Nm +.Bk -words +.Op Fl +abCefhiklmnprUuvXx +.Oo +.Fl T Oo Ar \&! Oc Ns Ar tty +\*(Ba +.Ar \&\- +.Oc +.Op Fl +o Ar option +.Oo +.Fl c Ar string \*(Ba +.Fl s \*(Ba +.Ar file +.Op Ar argument ... +.Oc +.Ek +.Nm builtin-name +.Op Ar argument ... +.Sh DESCRIPTION +.Nm +is a command interpreter intended for both interactive and shell +script use. +Its command language is a superset of the +.Xr sh C +shell language and largely compatible to the original Korn shell. +At times, this manual page may give scripting advice; while it +sometimes does take portable shell scripting or various standards +into account all information is first and foremost presented with +.Nm +in mind and should be taken as such. +.Ss I use Android, OS/2, etc. so what...? +Please see the FAQ at the end of this document. +.Ss Invocation +Most builtins can be called directly, for example if a link points from its +name to the shell; not all make sense, have been tested or work at all though. +.Pp +The options are as follows: +.Bl -tag -width XcXstring +.It Fl c Ar string +.Nm +will execute the command(s) contained in +.Ar string . +.It Fl i +Interactive shell. +A shell that reads commands from standard input is +.Dq interactive +if this +option is used or if both standard input and standard error are attached +to a +.Xr tty 4 . +An interactive shell has job control enabled, ignores the +.Dv SIGINT , +.Dv SIGQUIT +and +.Dv SIGTERM +signals, and prints prompts before reading input (see the +.Ev PS1 +and +.Ev PS2 +parameters). +It also processes the +.Ev ENV +parameter or the +.Pa mkshrc +file (see below). +For non-interactive shells, the +.Ic trackall +option is on by default (see the +.Ic set +command below). +.It Fl l +Login shell. +If the basename the shell is called with (i.e. argv[0]) +starts with +.Ql \- +or if this option is used, +the shell is assumed to be a login shell; see +.Sx Startup files +below. +.It Fl p +Privileged shell. +A shell is +.Dq privileged +if the real user ID or group ID does not match the +effective user ID or group ID (see +.Xr getuid 2 +and +.Xr getgid 2 ) . +Clearing the privileged option causes the shell to set +its effective user ID (group ID) to its real user ID (group ID). +For further implications, see +.Sx Startup files . +If the shell is privileged and this flag is not explicitly set, the +.Dq privileged +option is cleared automatically after processing the startup files. +.It Fl r +Restricted shell. +A shell is +.Dq restricted +if this +option is used. +The following restrictions come into effect after the shell processes any +profile and +.Ev ENV +files: +.Pp +.Bl -bullet -compact +.It +The +.Ic cd +.Po and Ic chdir Pc +command is disabled. +.It +The +.Ev SHELL , +.Ev ENV +and +.Ev PATH +parameters cannot be changed. +.It +Command names can't be specified with absolute or relative paths. +.It +The +.Fl p +option of the built-in command +.Ic command +can't be used. +.It +Redirections that create files can't be used (i.e.\& +.Dq Li \*(Gt , +.Dq Li \*(Gt\*(Ba , +.Dq Li \*(Gt\*(Gt , +.Dq Li \*(Lt\*(Gt ) . +.El +.It Fl s +The shell reads commands from standard input; all non-option arguments +are positional parameters. +.It Fl T Ar name +Spawn +.Nm +on the +.Xr tty 4 +device given. +The paths +.Ar name , +.Pa /dev/ttyC Ns Ar name +and +.Pa /dev/tty Ns Ar name +are attempted in order. +Unless +.Ar name +begins with an exclamation mark +.Pq Ql \&! , +this is done in a subshell and returns immediately. +If +.Ar name +is a dash +.Pq Ql \&\- , +detach from controlling terminal (daemonise) instead. +.El +.Pp +In addition to the above, the options described in the +.Ic set +built-in command can also be used on the command line: +both +.Op Fl +abCefhkmnuvXx +and +.Op Fl +o Ar option +can be used for single letter or long options, respectively. +.Pp +If neither the +.Fl c +nor the +.Fl s +option is specified, the first non-option argument specifies the name +of a file the shell reads commands from. +If there are no non-option +arguments, the shell reads commands from the standard input. +The name of the shell (i.e. the contents of $0) +is determined as follows: if the +.Fl c +option is used and there is a non-option argument, it is used as the name; +if commands are being read from a file, the file is used as the name; +otherwise, the basename the shell was called with (i.e. argv[0]) is used. +.Pp +The exit status of the shell is 127 if the command file specified on the +command line could not be opened, or non-zero if a fatal syntax error +occurred during the execution of a script. +In the absence of fatal errors, +the exit status is that of the last command executed, or zero if no +command is executed. +.Ss Startup files +For the actual location of these files, see +.Sx FILES . +A login shell processes the system profile first. +A privileged shell then processes the suid profile. +A non-privileged login shell processes the user profile next. +A non-privileged interactive shell checks the value of the +.Ev ENV +parameter after subjecting it to parameter, command, arithmetic and tilde +.Pq Ql \*(TI +substitution; if unset or empty, the user mkshrc profile is processed; +otherwise, if a file whose name is the substitution result exists, +it is processed; non-existence is silently ignored. +A privileged shell then drops privileges if neither was the +.Fl p +option given on the command line nor set during execution of the startup files. +.Ss Command syntax +The shell begins parsing its input by removing any backslash-newline +combinations, then breaking it into +.Em words . +Words (which are sequences of characters) are delimited by unquoted whitespace +characters (space, tab and newline) or meta-characters +.Po +.Ql \*(Lt , +.Ql \*(Gt , +.Ql \*(Ba , +.Ql \&; , +.Ql \&( , +.Ql \&) +and +.Ql & +.Pc . +Aside from delimiting words, spaces and tabs are ignored, while newlines +usually delimit commands. +The meta-characters are used in building the following +.Em tokens : +.Dq Li \*(Lt , +.Dq Li \*(Lt& , +.Dq Li \*(Lt\*(Lt , +.Dq Li \*(Lt\*(Lt\*(Lt , +.Dq Li \*(Gt , +.Dq Li \*(Gt& , +.Dq Li \*(Gt\*(Gt , +.Dq Li &\*(Gt , +etc. are used to specify redirections (see +.Sx Input/output redirection +below); +.Dq Li \*(Ba +is used to create pipelines; +.Dq Li \*(Ba& +is used to create co-processes (see +.Sx Co-processes +below); +.Dq Li \&; +is used to separate commands; +.Dq Li & +is used to create asynchronous pipelines; +.Dq Li && +and +.Dq Li \*(Ba\*(Ba +are used to specify conditional execution; +.Dq Li \&;; , +.Dq Li \&;& +and +.Dq Li \&;\*(Ba +are used in +.Ic case +statements; +.Dq Li \&(( ... \&)) +is used in arithmetic expressions; +and lastly, +.Dq Li \&( ... \&) +is used to create subshells. +.Pp +Whitespace and meta-characters can be quoted individually using a backslash +.Pq Ql \e , +or in groups using double +.Pq Ql \&" +or single +.Pq Dq Li \*(aq +quotes. +Note that the following characters are also treated specially by the +shell and must be quoted if they are to represent themselves: +.Ql \e , +.Ql \&" , +.Dq Li \*(aq , +.Ql # , +.Ql $ , +.Ql \` , +.Ql \*(TI , +.Ql { , +.Ql } , +.Ql * , +.Ql \&? +and +.Ql \&[ . +The first three of these are the above mentioned quoting characters (see +.Sx Quoting +below); +.Ql # , +if used at the beginning of a word, introduces a comment \*(en everything after +the +.Ql # +up to the nearest newline is ignored; +.Ql $ +is used to introduce parameter, command and arithmetic substitutions (see +.Sx Substitution +below); +.Ql \` +introduces an old-style command substitution (see +.Sx Substitution +below); +.Ql \*(TI +begins a directory expansion (see +.Sx Tilde expansion +below); +.Ql { +and +.Ql } +delimit +.Xr csh 1 Ns -style +alternations (see +.Sx Brace expansion +below); +and finally, +.Ql * , +.Ql \&? +and +.Ql \&[ +are used in file name generation (see +.Sx File name patterns +below). +.Pp +As words and tokens are parsed, the shell builds commands, of which there +are two basic types: +.Em simple-commands , +typically programmes that are executed, and +.Em compound-commands , +such as +.Ic for +and +.Ic if +statements, grouping constructs and function definitions. +.Pp +A simple-command consists of some combination of parameter assignments +(see +.Sx Parameters +below), +input/output redirections (see +.Sx Input/output redirections +below) +and command words; the only restriction is that parameter assignments come +before any command words. +The command words, if any, define the command +that is to be executed and its arguments. +The command may be a shell built-in command, a function +or an external command +(i.e. a separate executable file that is located using the +.Ev PATH +parameter; see +.Sx Command execution +below). +Note that all command constructs have an exit status: for external commands, +this is related to the status returned by +.Xr wait 2 +(if the command could not be found, the exit status is 127; if it could not +be executed, the exit status is 126); the exit status of other command +constructs (built-in commands, functions, compound-commands, pipelines, lists, +etc.) are all well-defined and are described where the construct is +described. +The exit status of a command consisting only of parameter +assignments is that of the last command substitution performed during the +parameter assignment or 0 if there were no command substitutions. +.Pp +Commands can be chained together using the +.Dq Li \*(Ba +token to form pipelines, in which the standard output of each command but the +last is piped (see +.Xr pipe 2 ) +to the standard input of the following command. +The exit status of a pipeline is that of its last command, unless the +.Ic pipefail +option is set (see there). +All commands of a pipeline are executed in separate subshells; +this is allowed by POSIX but differs from both variants of +.At +.Nm ksh , +where all but the last command were executed in subshells; see the +.Ic read +builtin's description for implications and workarounds. +A pipeline may be prefixed by the +.Dq Li \&! +reserved word which causes the exit status of the pipeline to be logically +complemented: if the original status was 0, the complemented status will be 1; +if the original status was not 0, the complemented status will be 0. +.Pp +.Em Lists +of commands can be created by separating pipelines by any of the following +tokens: +.Dq Li && , +.Dq Li \*(Ba\*(Ba , +.Dq Li & , +.Dq Li \*(Ba& +and +.Dq Li \&; . +The first two are for conditional execution: +.Dq Ar cmd1 No && Ar cmd2 +executes +.Ar cmd2 +only if the exit status of +.Ar cmd1 +is zero; +.Dq Li \*(Ba\*(Ba +is the opposite \*(en +.Ar cmd2 +is executed only if the exit status of +.Ar cmd1 +is non-zero. +.Dq Li && +and +.Dq Li \*(Ba\*(Ba +have equal precedence which is higher than that of +.Dq Li & , +.Dq Li \*(Ba& +and +.Dq Li \&; , +which also have equal precedence. +Note that the +.Dq Li && +and +.Dq Li \*(Ba\*(Ba +operators are +.Qq left-associative . +For example, both of these commands will print only +.Qq bar : +.Bd -literal -offset indent +$ false && echo foo \*(Ba\*(Ba echo bar +$ true \*(Ba\*(Ba echo foo && echo bar +.Ed +.Pp +The +.Dq Li & +token causes the preceding command to be executed asynchronously; that is, +the shell starts the command but does not wait for it to complete (the shell +does keep track of the status of asynchronous commands; see +.Sx Job control +below). +When an asynchronous command is started when job control is disabled +(i.e. in most scripts), the command is started with signals +.Dv SIGINT +and +.Dv SIGQUIT +ignored and with input redirected from +.Pa /dev/null +(however, redirections specified in the asynchronous command have precedence). +The +.Dq Li \*(Ba& +operator starts a co-process which is a special kind of asynchronous process +(see +.Sx Co-processes +below). +Note that a command must follow the +.Dq Li && +and +.Dq Li \*(Ba\*(Ba +operators, while it need not follow +.Dq Li & , +.Dq Li \*(Ba& +or +.Dq Li \&; . +The exit status of a list is that of the last command executed, with the +exception of asynchronous lists, for which the exit status is 0. +.Pp +Compound commands are created using the following reserved words. +These words +are only recognised if they are unquoted and if they are used as the first +word of a command (i.e. they can't be preceded by parameter assignments or +redirections): +.Bd -literal -offset indent +case else function then ! ( +do esac if time [[ (( +done fi in until { +elif for select while } +.Ed +.Pp +In the following compound command descriptions, command lists (denoted as +.Em list ) +that are followed by reserved words must end with a semicolon, a newline or +a (syntactically correct) reserved word. +For example, the following are all valid: +.Bd -literal -offset indent +$ { echo foo; echo bar; } +$ { echo foo; echo bar\*(Ltnewline\*(Gt} +$ { { echo foo; echo bar; } } +.Ed +.Pp +This is not valid: +.Pp +.Dl $ { echo foo; echo bar } +.Bl -tag -width 4n +.It Pq Ar list +Execute +.Ar list +in a subshell. +There is no implicit way to pass environment changes from a +subshell back to its parent. +.It { Ar list ; No } +Compound construct; +.Ar list +is executed, but not in a subshell. +Note that +.Dq Li { +and +.Dq Li } +are reserved words, not meta-characters. +.It Xo case Ar word No in +.Oo Op \&( +.Ar pattern +.Op \*(Ba Ar pattern +.No ... Ns ) +.Ar list +.Ic terminator +.Oc No ... esac +.Xc +The +.Ic case +statement attempts to match +.Ar word +against a specified +.Ar pattern ; +the +.Ar list +associated with the first successfully matched pattern is executed. +Patterns used in +.Ic case +statements are the same as those used for file name patterns except that the +restrictions regarding +.Ql \&. +and +.Ql / +are dropped. +Note that any unquoted space before and after a pattern is +stripped; any space within a pattern must be quoted. +Both the word and the +patterns are subject to parameter, command and arithmetic substitution, as +well as tilde substitution. +.Pp +For historical reasons, open and close braces may be used instead of +.Ic in +and +.Ic esac +e.g.\& +.Ic case $foo { *) echo bar ;; } . +.Pp +The list +.Ic terminator Ns s +are: +.Bl -tag -width 4n +.It Dq Li ;; +Terminate after the list. +.It Dq Li \&;& +Fall through into the next list. +.It Dq Li \&;\*(Ba +Evaluate the remaining pattern-list tuples. +.El +.Pp +The exit status of a +.Ic case +statement is that of the executed +.Ar list ; +if no +.Ar list +is executed, the exit status is zero. +.It Xo for Ar name +.Oo in Ar word No ... Oc ; +.No do Ar list ; No done +.Xc +For each +.Ar word +in the specified word list, the parameter +.Ar name +is set to the word and +.Ar list +is executed. +If +.Ic in +is not used to specify a word list, the positional parameters ($1, $2, +etc.) are used instead. +For historical reasons, open and close braces may be used instead of +.Ic do +and +.Ic done +e.g.\& +.Ic for i; { echo $i; } . +The exit status of a +.Ic for +statement is the last exit status of +.Ar list ; +if +.Ar list +is never executed, the exit status is zero. +.It Xo if Ar list ; +.No then Ar list ; +.Oo elif Ar list ; +.No then Ar list ; Oc +.No ... +.Oo else Ar list ; Oc +.No fi +.Xc +If the exit status of the first +.Ar list +is zero, the second +.Ar list +is executed; otherwise, the +.Ar list +following the +.Ic elif , +if any, is executed with similar consequences. +If all the lists following the +.Ic if +and +.Ic elif Ns s +fail (i.e. exit with non-zero status), the +.Ar list +following the +.Ic else +is executed. +The exit status of an +.Ic if +statement is that of non-conditional +.Ar list +that is executed; if no non-conditional +.Ar list +is executed, the exit status is zero. +.It Xo select Ar name +.Oo in Ar word No ... Oc ; +.No do Ar list ; No done +.Xc +The +.Ic select +statement provides an automatic method of presenting the user with a menu and +selecting from it. +An enumerated list of the specified +.Ar word Ns (s) +is printed on standard error, followed by a prompt +.Po +.Ev PS3 : +normally +.Dq Li #?\ \& +.Pc . +A number corresponding to one of the enumerated words is then read from +standard input, +.Ar name +is set to the selected word (or unset if the selection is not valid), +.Ev REPLY +is set to what was read (leading/trailing space is stripped), and +.Ar list +is executed. +If a blank line (i.e. zero or more +.Ev IFS +octets) is entered, the menu is reprinted without executing +.Ar list . +.Pp +When +.Ar list +completes, the enumerated list is printed if +.Ev REPLY +is empty, the prompt is printed, and so on. +This process continues until an end-of-file +is read, an interrupt is received, or a +.Ic break +statement is executed inside the loop. +If +.Dq in Ar word ... +is omitted, the positional parameters are used +(i.e. $1, $2, etc.). +For historical reasons, open and close braces may be used instead of +.Ic do +and +.Ic done +e.g.\& +.Ic select i; { echo $i; } . +The exit status of a +.Ic select +statement is zero if a +.Ic break +statement is used to exit the loop, non-zero otherwise. +.It Xo until Ar list ; +.No do Ar list ; +.No done +.Xc +This works like +.Ic while , +except that the body is executed only while the exit status of the first +.Ar list +is non-zero. +.It Xo while Ar list ; +.No do Ar list ; +.No done +.Xc +A +.Ic while +is a pre-checked loop. +Its body is executed as often as the exit status of the first +.Ar list +is zero. +The exit status of a +.Ic while +statement is the last exit status of the +.Ar list +in the body of the loop; if the body is not executed, the exit status is zero. +.It Xo function Ar name +.No { Ar list ; No } +.Xc +Defines the function +.Ar name +(see +.Sx Functions +below). +Note that redirections specified after a function definition are +performed whenever the function is executed, not when the function definition +is executed. +.It Ar name Ns \&() Ar command +Mostly the same as +.Ic function +(see +.Sx Functions +below). +Whitespace (space or tab) after +.Ar name +will be ignored most of the time. +.It Xo function Ar name Ns \&() +.No { Ar list ; No } +.Xc +The same as +.Ar name Ns \&() +.Pq Nm bash Ns ism . +The +.Ic function +keyword is ignored. +.It Xo Ic time Op Fl p +.Op Ar pipeline +.Xc +The +.Sx Command execution +section describes the +.Ic time +reserved word. +.It \&(( Ar expression No )) +The arithmetic expression +.Ar expression +is evaluated; equivalent to +.Dq Li let \&" Ns Ar expression Ns \&" +(see +.Sx Arithmetic expressions +and the +.Ic let +command, below) in a compound construct. +.It Bq Bq Ar \ \&expression\ \& +Similar to the +.Ic test +and +.Ic \&[ ... \&] +commands (described later), with the following exceptions: +.Bl -bullet +.It +Field splitting and file name generation are not performed on arguments. +.It +The +.Fl a +.Pq AND +and +.Fl o +.Pq OR +operators are replaced with +.Dq Li && +and +.Dq Li \*(Ba\*(Ba , +respectively. +.It +Operators (e.g.\& +.Dq Li \-f , +.Dq Li = , +.Dq Li \&! ) +must be unquoted. +.It +Parameter, command and arithmetic substitutions are performed as expressions +are evaluated and lazy expression evaluation is used for the +.Dq Li && +and +.Dq Li \*(Ba\*(Ba +operators. +This means that in the following statement, +.Ic $(\*(Ltfoo) +is evaluated if and only if the file +.Pa foo +exists and is readable: +.Bd -literal -offset indent +$ [[ \-r foo && $(\*(Ltfoo) = b*r ]] +.Ed +.It +The second operand of the +.Dq Li != +and +.Dq Li = +expressions are a subset of patterns (e.g. the comparison +.Ic \&[[ foobar = f*r ]] +succeeds). +This even works indirectly: +.Bd -literal -offset indent +$ bar=foobar; baz=\*(aqf*r\*(aq +$ [[ $bar = $baz ]]; echo $? +$ [[ $bar = \&"$baz" ]]; echo $? +.Ed +.Pp +Perhaps surprisingly, the first comparison succeeds, +whereas the second doesn't. +This does not apply to all extglob metacharacters, currently. +.El +.El +.Ss Quoting +Quoting is used to prevent the shell from treating characters or words +specially. +There are three methods of quoting. +First, +.Ql \e +quotes the following character, unless it is at the end of a line, in which +case both the +.Ql \e +and the newline are stripped. +Second, a single quote +.Pq Dq Li \*(aq +quotes everything up to the next single quote (this may span lines). +Third, a double quote +.Pq Ql \&" +quotes all characters, except +.Ql $ , +.Ql \e +and +.Ql \` , +up to the next unescaped double quote. +.Ql $ +and +.Ql \` +inside double quotes have their usual meaning (i.e. parameter, arithmetic +or command substitution) except no field splitting is carried out on the +results of double-quoted substitutions, and the old-style form of command +substitution has backslash-quoting for double quotes enabled. +If a +.Ql \e +inside a double-quoted string is followed by +.Ql \&" , +.Ql $ , +.Ql \e +or +.Ql \` , +only the +.Ql \e +is removed, i.e. the combination is replaced by the second character; +if it is followed by a newline, both the +.Ql \e +and the newline are stripped; otherwise, both the +.Ql \e +and the character following are unchanged. +.Pp +If a single-quoted string is preceded by an unquoted +.Ql $ , +C style backslash expansion (see below) is applied (even single quote +characters inside can be escaped and do not terminate the string then); +the expanded result is treated as any other single-quoted string. +If a double-quoted string is preceded by an unquoted +.Ql $ , +the +.Ql $ +is simply ignored. +.Ss Backslash expansion +In places where backslashes are expanded, certain C and +.At +.Nm ksh +or GNU +.Nm bash +style escapes are translated. +These include +.Dq Li \ea , +.Dq Li \eb , +.Dq Li \ef , +.Dq Li \en , +.Dq Li \er , +.Dq Li \et , +.Dq Li \eU######## , +.Dq Li \eu#### +and +.Dq Li \ev . +For +.Dq Li \eU######## +and +.Dq Li \eu#### , +.Dq # +means a hexadecimal digit, of which there may be none up to four or eight; +these escapes translate a Unicode codepoint to UTF-8. +Furthermore, +.Dq Li \eE +and +.Dq Li \ee +expand to the escape character. +.Pp +In the +.Ic print +builtin mode, +.Dq Li \e" , +.Dq Li \e\*(aq +and +.Dq Li \e? +are explicitly excluded; +octal sequences must have the none up to three octal digits +.Dq # +prefixed with the digit zero +.Pq Dq Li \e0### ; +hexadecimal sequences +.Dq Li \ex## +are limited to none up to two hexadecimal digits +.Dq # ; +both octal and hexadecimal sequences convert to raw octets; +.Dq Li \e# , +where # is none of the above, translates to \e# (backslashes are retained). +.Pp +Backslash expansion in the C style mode slightly differs: octal sequences +.Dq Li \e### +must have no digit zero prefixing the one up to three octal digits +.Dq # +and yield raw octets; hexadecimal sequences +.Dq Li \ex#* +greedily eat up as many hexadecimal digits +.Dq # +as they can and terminate with the first non-hexadecimal digit; +these translate a Unicode codepoint to UTF-8. +The sequence +.Dq Li \ec# , +where +.Dq # +is any octet, translates to Ctrl-# (which basically means, +.Dq Li \ec? +becomes DEL, everything else is bitwise ANDed with 0x1F). +Finally, +.Dq Li \e# , +where # is none of the above, translates to # (has the backslash trimmed), +even if it is a newline. +.Ss Aliases +There are two types of aliases: normal command aliases and tracked aliases. +Command aliases are normally used as a short hand for a long or often used +command. +The shell expands command aliases (i.e. substitutes the alias name +for its value) when it reads the first word of a command. +An expanded alias is re-processed to check for more aliases. +If a command alias ends in a +space or tab, the following word is also checked for alias expansion. +The alias expansion process stops when a word that is not an alias is found, +when a quoted word is found, or when an alias word that is currently being +expanded is found. +Aliases are specifically an interactive feature: while they do happen +to work in scripts and on the command line in some cases, aliases are +expanded during lexing, so their use must be in a separate command tree +from their definition; otherwise, the alias will not be found. +Noticeably, command lists (separated by semicolon, in command substitutions +also by newline) may be one same parse tree. +.Pp +The following command aliases are defined automatically by the shell: +.Bd -literal -offset indent +autoload=\*(aq\e\ebuiltin typeset \-fu\*(aq +functions=\*(aq\e\ebuiltin typeset \-f\*(aq +hash=\*(aq\e\ebuiltin alias \-t\*(aq +history=\*(aq\e\ebuiltin fc \-l\*(aq +integer=\*(aq\e\ebuiltin typeset \-i\*(aq +local=\*(aq\e\ebuiltin typeset\*(aq +login=\*(aq\e\ebuiltin exec login\*(aq +nameref=\*(aq\e\ebuiltin typeset \-n\*(aq +nohup=\*(aqnohup \*(aq +r=\*(aq\e\ebuiltin fc \-e \-\*(aq +type=\*(aq\e\ebuiltin whence \-v\*(aq +.Ed +.Pp +Tracked aliases allow the shell to remember where it found a particular +command. +The first time the shell does a path search for a command that is +marked as a tracked alias, it saves the full path of the command. +The next +time the command is executed, the shell checks the saved path to see that it +is still valid, and if so, avoids repeating the path search. +Tracked aliases can be listed and created using +.Ic alias Fl t . +Note that changing the +.Ev PATH +parameter clears the saved paths for all tracked aliases. +If the +.Ic trackall +option is set (i.e.\& +.Ic set Fl o Ic trackall +or +.Ic set Fl h ) , +the shell tracks all commands. +This option is set automatically for non-interactive shells. +For interactive shells, only the following commands are +automatically tracked: +.Xr cat 1 , +.Xr cc 1 , +.Xr chmod 1 , +.Xr cp 1 , +.Xr date 1 , +.Xr ed 1 , +.Xr emacs 1 , +.Xr grep 1 , +.Xr ls 1 , +.Xr make 1 , +.Xr mv 1 , +.Xr pr 1 , +.Xr rm 1 , +.Xr sed 1 , +.Xr sh 1 , +.Xr vi 1 +and +.Xr who 1 . +.Ss Substitution +The first step the shell takes in executing a simple-command is to perform +substitutions on the words of the command. +There are three kinds of +substitution: parameter, command and arithmetic. +Parameter substitutions, +which are described in detail in the next section, take the form +.Pf $ Ns Ar name +or +.Pf ${ Ns Ar ... Ns } ; +command substitutions take the form +.Pf $( Ns Ar command Ns \&) +or (deprecated) +.Pf \` Ns Ar command Ns \` +or (executed in the current environment) +.Pf ${\ \& Ar command Ns \&;} +and strip trailing newlines; +and arithmetic substitutions take the form +.Pf $(( Ns Ar expression Ns )) . +Parsing the current-environment command substitution requires a space, +tab or newline after the opening brace and that the closing brace be +recognised as a keyword (i.e. is preceded by a newline or semicolon). +They are also called funsubs (function substitutions) and behave like +functions in that +.Ic local +and +.Ic return +work, and in that +.Ic exit +terminates the parent shell; shell options are shared. +.Pp +Another variant of substitution are the valsubs (value substitutions) +.Pf ${\*(Ba\& Ns Ar command Ns \&;} +which are also executed in the current environment, like funsubs, but +share their I/O with the parent; instead, they evaluate to whatever +the, initially empty, expression-local variable +.Ev REPLY +is set to within the +.Ar command Ns s . +.Pp +If a substitution appears outside of double quotes, the results of the +substitution are generally subject to word or field splitting according to +the current value of the +.Ev IFS +parameter. +The +.Ev IFS +parameter specifies a list of octets which are used to break a string up +into several words; any octets from the set space, tab and newline that +appear in the +.Ev IFS +octets are called +.Dq IFS whitespace . +Sequences of one or more +.Ev IFS +whitespace octets, in combination with zero or one +.Pf non- Ev IFS +whitespace octets, delimit a field. +As a special case, leading and trailing +.Ev IFS +whitespace is stripped (i.e. no leading or trailing empty field +is created by it); leading or trailing +.Pf non- Ev IFS +whitespace does create an empty field. +.Pp +Example: If +.Ev IFS +is set to +.Dq Li \*(Ltspace\*(Gt: +and VAR is set to +.Dq Li \*(Ltspace\*(GtA\*(Ltspace\*(Gt:\*(Ltspace\*(Gt\*(Ltspace\*(GtB::D , +the substitution for $VAR results in four fields: +.Dq Li A , +.Dq Li B , +.Dq +(an empty field) and +.Dq Li D . +Note that if the +.Ev IFS +parameter is set to the empty string, no field splitting is done; +if it is unset, the default value of space, tab and newline is used. +.Pp +Also, note that the field splitting applies only to the immediate result of +the substitution. +Using the previous example, the substitution for $VAR:E +results in the fields: +.Dq Li A , +.Dq Li B , +.Dq +and +.Dq Li D:E , +not +.Dq Li A , +.Dq Li B , +.Dq , +.Dq Li D +and +.Dq Li E . +This behavior is POSIX compliant, but incompatible with some other shell +implementations which do field splitting on the word which contained the +substitution or use +.Dv IFS +as a general whitespace delimiter. +.Pp +The results of substitution are, unless otherwise specified, also subject to +brace expansion and file name expansion (see the relevant sections below). +.Pp +A command substitution is replaced by the output generated by the specified +command which is run in a subshell. +For +.Pf $( Ns Ar command Ns \&) +and +.Pf ${\*(Ba\& Ns Ar command Ns \&;} +and +.Pf ${\ \& Ar command Ns \&;} +substitutions, normal quoting rules are used when +.Ar command +is parsed; however, for the deprecated +.Pf \` Ns Ar command Ns \` +form, a +.Ql \e +followed by any of +.Ql $ , +.Ql \` +or +.Ql \e +is stripped (as is +.Ql \&" +when the substitution is part of a double-quoted string); a backslash +.Ql \e +followed by any other character is unchanged. +As a special case in command substitutions, a command of the form +.Pf \*(Lt Ar file +is interpreted to mean substitute the contents of +.Ar file . +Note that +.Ic $(\*(Ltfoo) +has the same effect as +.Ic $(cat foo) . +.Pp +Note that some shells do not use a recursive parser for command substitutions, +leading to failure for certain constructs; to be portable, use as workaround +.Dq Li x=$(cat) \*(Lt\*(Lt\eEOF +(or the newline-keeping +.Dq Li x=\*(Lt\*(Lt\eEOF +extension) instead to merely slurp the string. +.St -p1003.1 +recommends using case statements of the form +.Li "x=$(case $foo in (bar) echo $bar ;; (*) echo $baz ;; esac)" +instead, which would work but not serve as example for this portability issue. +.Bd -literal -offset indent +x=$(case $foo in bar) echo $bar ;; *) echo $baz ;; esac) +# above fails to parse on old shells; below is the workaround +x=$(eval $(cat)) \*(Lt\*(Lt\eEOF +case $foo in bar) echo $bar ;; *) echo $baz ;; esac +EOF +.Ed +.Pp +Arithmetic substitutions are replaced by the value of the specified expression. +For example, the command +.Ic print $((2+3*4)) +displays 14. +See +.Sx Arithmetic expressions +for a description of an expression. +.Ss Parameters +Parameters are shell variables; they can be assigned values and their values +can be accessed using a parameter substitution. +A parameter name is either one +of the special single punctuation or digit character parameters described +below, or a letter followed by zero or more letters or digits +.Po +.Ql _ +counts as a letter +.Pc . +The latter form can be treated as arrays by appending an array index of the +form +.Op Ar expr +where +.Ar expr +is an arithmetic expression. +Array indices in +.Nm +are limited to the range 0 through 4294967295, inclusive. +That is, they are a 32-bit unsigned integer. +.Pp +Parameter substitutions take the form +.Pf $ Ns Ar name , +.Pf ${ Ns Ar name Ns } +or +.Sm off +.Pf ${ Ar name Oo Ar expr Oc } +.Sm on +where +.Ar name +is a parameter name. +Substitution of all array elements with +.Pf ${ Ns Ar name Ns \&[*]} +and +.Pf ${ Ns Ar name Ns \&[@]} +works equivalent to $* and $@ for positional parameters. +If substitution is performed on a parameter +(or an array parameter element) +that is not set, an empty string is substituted unless the +.Ic nounset +option +.Pq Ic set Fl u +is set, in which case an error occurs. +.Pp +Parameters can be assigned values in a number of ways. +First, the shell implicitly sets some parameters like +.Dq Li # , +.Dq Li PWD +and +.Dq Li $ ; +this is the only way the special single character parameters are set. +Second, parameters are imported from the shell's environment at startup. +Third, parameters can be assigned values on the command line: for example, +.Ic FOO=bar +sets the parameter +.Dq Li FOO +to +.Dq Li bar ; +multiple parameter assignments can be given on a single command line and they +can be followed by a simple-command, in which case the assignments are in +effect only for the duration of the command (such assignments are also +exported; see below for the implications of this). +Note that both the parameter name and the +.Ql = +must be unquoted for the shell to recognise a parameter assignment. +The construct +.Ic FOO+=baz +is also recognised; the old and new values are immediately concatenated. +The fourth way of setting a parameter is with the +.Ic export , +.Ic global , +.Ic readonly +and +.Ic typeset +commands; see their descriptions in the +.Sx Command execution +section. +Fifth, +.Ic for +and +.Ic select +loops set parameters as well as the +.Ic getopts , +.Ic read +and +.Ic set Fl A +commands. +Lastly, parameters can be assigned values using assignment operators +inside arithmetic expressions (see +.Sx Arithmetic expressions +below) or using the +.Sm off +.Pf ${ Ar name No = Ar value No } +.Sm on +form of the parameter substitution (see below). +.Pp +Parameters with the export attribute (set using the +.Ic export +or +.Ic typeset Fl x +commands, or by parameter assignments followed by simple commands) are put in +the environment (see +.Xr environ 7 ) +of commands run by the shell as +.Ar name Ns = Ns Ar value +pairs. +The order in which parameters appear in the environment of a command is +unspecified. +When the shell starts up, it extracts parameters and their values +from its environment and automatically sets the export attribute for those +parameters. +.Pp +Modifiers can be applied to the +.Pf ${ Ns Ar name Ns } +form of parameter substitution: +.Bl -tag -width Ds +.Sm off +.It ${ Ar name No :\- Ar word No } +.Sm on +If +.Ar name +is set and not empty, +it is substituted; otherwise, +.Ar word +is substituted. +.Sm off +.It ${ Ar name No :+ Ar word No } +.Sm on +If +.Ar name +is set and not empty, +.Ar word +is substituted; otherwise, nothing is substituted. +.Sm off +.It ${ Ar name No := Ar word No } +.Sm on +If +.Ar name +is set and not empty, +it is substituted; otherwise, it is assigned +.Ar word +and the resulting value of +.Ar name +is substituted. +.Sm off +.It ${ Ar name No :? Ar word No } +.Sm on +If +.Ar name +is set and not empty, +it is substituted; otherwise, +.Ar word +is printed on standard error (preceded by +.Ar name : ) +and an error occurs (normally causing termination of a shell script, function, +or a script sourced using the +.Dq Li \&. +built-in). +If +.Ar word +is omitted, the string +.Dq Li parameter null or not set +is used instead. +.El +.Pp +Note that, for all of the above, +.Ar word +is actually considered quoted, and special parsing rules apply. +The parsing rules also differ on whether the expression is double-quoted: +.Ar word +then uses double-quoting rules, except for the double quote itself +.Pq Ql \&" +and the closing brace, which, if backslash escaped, gets quote removal applied. +.Pp +In the above modifiers, the +.Ql \&: +can be omitted, in which case the conditions only depend on +.Ar name +being set (as opposed to set and not empty). +If +.Ar word +is needed, parameter, command, arithmetic and tilde substitution are performed +on it; if +.Ar word +is not needed, it is not evaluated. +.Pp +The following forms of parameter substitution can also be used (if +.Ar name +is an array, the element with the key +.Dq 0 +will be substituted in scalar context): +.Pp +.Bl -tag -width Ds -compact +.It Pf ${# Ns Ar name Ns \&} +The number of positional parameters if +.Ar name +is +.Dq Li * , +.Dq Li @ +or not specified; otherwise the length +.Pq in characters +of the string value of parameter +.Ar name . +.Pp +.It Pf ${# Ns Ar name Ns \&[*]} +.It Pf ${# Ns Ar name Ns \&[@]} +The number of elements in the array +.Ar name . +.Pp +.It Pf ${% Ns Ar name Ns \&} +The width +.Pq in screen columns +of the string value of parameter +.Ar name , +or \-1 if +.Pf ${ Ns Ar name Ns } +contains a control character. +.Pp +.It Pf ${! Ns Ar name Ns } +The name of the variable referred to by +.Ar name . +This will be +.Ar name +except when +.Ar name +is a name reference (bound variable), created by the +.Ic nameref +command (which is an alias for +.Ic typeset Fl n ) . +.Ar name +cannot be one of most special parameters (see below). +.Pp +.It Pf ${! Ns Ar name Ns \&[*]} +.It Pf ${! Ns Ar name Ns \&[@]} +The names of indices (keys) in the array +.Ar name . +.Pp +.Sm off +.It Xo +.Pf ${ Ar name +.Pf # Ar pattern No } +.Xc +.It Xo +.Pf ${ Ar name +.Pf ## Ar pattern No } +.Xc +.Sm on +If +.Ar pattern +matches the beginning of the value of parameter +.Ar name , +the matched text is deleted from the result of substitution. +A single +.Ql # +results in the shortest match, and two +of them result in the longest match. +Cannot be applied to a vector +.Pq ${*} or ${@} or ${array[*]} or ${array[@]} . +.Pp +.Sm off +.It Xo +.Pf ${ Ar name +.Pf % Ar pattern No } +.Xc +.It Xo +.Pf ${ Ar name +.Pf %% Ar pattern No } +.Xc +.Sm on +Like ${...#...} substitution, but it deletes from the end of the value. +Cannot be applied to a vector. +.Pp +.Sm off +.It Xo +.Pf ${ Ar name +.Pf / Ar pattern / Ar string No } +.Xc +.It Xo +.Pf ${ Ar name +.Pf /# Ar pattern / Ar string No } +.Xc +.It Xo +.Pf ${ Ar name +.Pf /% Ar pattern / Ar string No } +.Xc +.It Xo +.Pf ${ Ar name +.Pf // Ar pattern / Ar string No } +.Xc +.Sm on +The longest match of +.Ar pattern +in the value of parameter +.Ar name +is replaced with +.Ar string +(deleted if +.Ar string +is empty; the trailing slash +.Pq Ql / +may be omitted in that case). +A leading slash followed by +.Ql # +or +.Ql % +causes the pattern to be anchored at the beginning or end of +the value, respectively; empty unanchored +.Ar pattern Ns s +cause no replacement; a single leading slash or use of a +.Ar pattern +that matches the empty string causes the replacement to +happen only once; two leading slashes cause all occurrences +of matches in the value to be replaced. +Cannot be applied to a vector. +Inefficiently implemented, may be slow. +.Pp +.Sm off +.It Xo +.Pf ${ Ar name +.Pf @/ Ar pattern / Ar string No } +.Xc +.Sm on +The same as +.Sm off +.Xo +.Pf ${ Ar name +.Pf // Ar pattern / Ar string No } , +.Xc +.Sm on +except that both +.Ar pattern +and +.Ar string +are expanded anew for each iteration. +.Pp +.Sm off +.It Xo +.Pf ${ Ar name : Ns Ar pos +.Pf : Ns Ar len Ns } +.Xc +.Sm on +The first +.Ar len +characters of +.Ar name , +starting at position +.Ar pos , +are substituted. +Both +.Ar pos +and +.Pf : Ns Ar len +are optional. +If +.Ar pos +is negative, counting starts at the end of the string; if it +is omitted, it defaults to 0. +If +.Ar len +is omitted or greater than the length of the remaining string, +all of it is substituted. +Both +.Ar pos +and +.Ar len +are evaluated as arithmetic expressions. +Currently, +.Ar pos +must start with a space, opening parenthesis or digit to be recognised. +Cannot be applied to a vector. +.Pp +.It Pf ${ Ns Ar name Ns @#} +The hash (using the BAFH algorithm) of the expansion of +.Ar name . +This is also used internally for the shell's hashtables. +.Pp +.It Pf ${ Ns Ar name Ns @Q} +A quoted expression safe for re-entry, whose value is the value of the +.Ar name +parameter, is substituted. +.El +.Pp +Note that +.Ar pattern +may need extended globbing pattern +.Pq @(...) , +single +.Pq \&\*(aq...\&\*(aq +or double +.Pq \&"...\&" +quote escaping unless +.Fl o Ic sh +is set. +.Pp +The following special parameters are implicitly set by the shell and cannot be +set directly using assignments: +.Bl -tag -width "1 .. 9" +.It Ev \&! +Process ID of the last background process started. +If no background processes have been started, the parameter is not set. +.It Ev \&# +The number of positional parameters ($1, $2, etc.). +.It Ev \&$ +The PID of the shell or, if it is a subshell, the PID of the original shell. +Do +.Em NOT +use this mechanism for generating temporary file names; see +.Xr mktemp 1 +instead. +.It Ev \- +The concatenation of the current single letter options (see the +.Ic set +command below for a list of options). +.It Ev \&? +The exit status of the last non-asynchronous command executed. +If the last command was killed by a signal, +.Ic \&$? +is set to 128 plus the signal number, but at most 255. +.It Ev 0 +The name of the shell, determined as follows: +the first argument to +.Nm +if it was invoked with the +.Fl c +option and arguments were given; otherwise the +.Ar file +argument, if it was supplied; +or else the basename the shell was invoked with (i.e.\& +.Li argv[0] ) . +.Ev $0 +is also set to the name of the current script or +the name of the current function, if it was defined with the +.Ic function +keyword (i.e. a Korn shell style function). +.It Ev 1 No .. Ev 9 +The first nine positional parameters that were supplied to the shell, function, +or script sourced using the +.Dq Li \&. +built-in. +Further positional parameters may be accessed using +.Pf ${ Ar number Ns } . +.It Ev * +All positional parameters (except 0), i.e. $1, $2, $3, ... +.br +If used +outside of double quotes, parameters are separate words (which are subjected +to word splitting); if used within double quotes, parameters are separated +by the first character of the +.Ev IFS +parameter (or the empty string if +.Ev IFS +is unset. +.It Ev @ +Same as +.Ic $* , +unless it is used inside double quotes, in which case a separate word is +generated for each positional parameter. +If there are no positional parameters, no word is generated. +.Ic \&"$@" +can be used to access arguments, verbatim, without losing +empty arguments or splitting arguments with spaces (IFS, actually). +.El +.Pp +The following parameters are set and/or used by the shell: +.Bl -tag -width "KSH_VERSION" +.It Ev _ +.Pq underscore +When an external command is executed by the shell, this parameter is set in the +environment of the new process to the path of the executed command. +In interactive use, this parameter is also set in the parent shell to the last +word of the previous command. +.It Ev BASHPID +The PID of the shell or subshell. +.It Ev CDPATH +Like +.Ev PATH , +but used to resolve the argument to the +.Ic cd +built-in command. +Note that if +.Ev CDPATH +is set and does not contain +.Dq Li \&. +or an empty string element, the current directory is not searched. +Also, the +.Ic cd +built-in command will display the resulting directory when a match is found +in any search path other than the empty path. +.It Ev COLUMNS +Set to the number of columns on the terminal or window. +Always set, defaults to 80, unless the +value as reported by +.Xr stty 1 +is non-zero and sane enough (minimum is 12x3); similar for +.Ev LINES . +This parameter is used by the interactive line editing modes and by the +.Ic select , +.Ic set Fl o +and +.Ic kill Fl l +commands to format information columns. +Importing from the environment or unsetting this parameter removes the +binding to the actual terminal size in favour of the provided value. +.It Ev ENV +If this parameter is found to be set after any profile files are executed, the +expanded value is used as a shell startup file. +It typically contains function and alias definitions. +.It Ev EPOCHREALTIME +Time since the epoch, as returned by +.Xr gettimeofday 2 , +formatted as decimal +.Va tv_sec +followed by a dot +.Pq Ql \&. +and +.Va tv_usec +padded to exactly six decimal digits. +.It Ev EXECSHELL +If set, this parameter is assumed to contain the shell that is to be used to +execute commands that +.Xr execve 2 +fails to execute and which do not start with a +.Dq Li #! Ns Ar shell +sequence. +.It Ev FCEDIT +The editor used by the +.Ic fc +command (see below). +.It Ev FPATH +Like +.Ev PATH , +but used when an undefined function is executed to locate the file defining the +function. +It is also searched when a command can't be found using +.Ev PATH . +See +.Sx Functions +below for more information. +.It Ev HISTFILE +The name of the file used to store command history. +When assigned to or unset, the file is opened, history is truncated +then loaded from the file; subsequent new commands (possibly consisting +of several lines) are appended once they successfully compiled. +Also, several invocations of the shell will share history if their +.Ev HISTFILE +parameters all point to the same file. +.Pp +.Sy Note : +If +.Ev HISTFILE +is unset or empty, no history file is used. +This is different from +.At +.Nm ksh . +.It Ev HISTSIZE +The number of commands normally stored for history. +The default is 2047. +Do not set this value to insanely high values such as 1000000000 because +.Nm +can then not allocate enough memory for the history and will not start. +.It Ev HOME +The default directory for the +.Ic cd +command and the value substituted for an unqualified +.Ic \*(TI +(see +.Sx Tilde expansion +below). +.It Ev IFS +Internal field separator, used during substitution and by the +.Ic read +command, to split values into distinct arguments; normally set to space, tab +and newline. +See +.Sx Substitution +above for details. +.Pp +.Sy Note : +This parameter is not imported from the environment when the shell is +started. +.It Ev KSHEGID +The effective group id of the shell. +.It Ev KSHGID +The real group id of the shell. +.It Ev KSHUID +The real user id of the shell. +.It Ev KSH_MATCH +The last matched string. +In a future version, this will be an indexed array, +with indexes 1 and up capturing matching groups. +Set by string comparisons (== and !=) in double-bracket test +expressions when a match is found (when != returns false), by +.Ic case +when a match is encountered, and by the substitution operations +.Sm off +.Xo +.Pf ${ Ar x +.Pf # Ar pat No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf ## Ar pat No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf % Ar pat No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf %% Ar pat No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf / Ar pat / Ar rpl No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf /# Ar pat / Ar rpl No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf /% Ar pat / Ar rpl No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf // Ar pat / Ar rpl No } , +.Sm on +.Xc +and +.Sm off +.Xo +.Pf ${ Ar x +.Pf @/ Ar pat / Ar rpl No } . +.Sm on +.Xc +See the end of the Emacs editing mode documentation for an example. +.It Ev KSH_VERSION +The name and version of the shell (read-only). +See also the version commands in +.Sx Emacs editing mode +and +.Sx Vi editing mode +sections, below. +.It Ev LINENO +The line number of the function or shell script that is currently being +executed. +.It Ev LINES +Set to the number of lines on the terminal or window. +Always set, defaults to 24. +See +.Ev COLUMNS . +.It Ev OLDPWD +The previous working directory. +Unset if +.Ic cd +has not successfully changed directories since the shell started or if the +shell doesn't know where it is. +.It Ev OPTARG +When using +.Ic getopts , +it contains the argument for a parsed option, if it requires one. +.It Ev OPTIND +The index of the next argument to be processed when using +.Ic getopts . +Assigning 1 to this parameter causes +.Ic getopts +to process arguments from the beginning the next time it is invoked. +.It Ev PATH +A colon (semicolon on OS/2) separated list of directories that are +searched when looking for commands and files sourced using the +.Dq Li \&. +command (see below). +An empty string resulting from a leading or trailing +(semi)colon, or two adjacent ones, is treated as a +.Dq Li \&. +(the current directory). +.It Ev PATHSEP +A colon (semicolon on OS/2), for the user's convenience. +.It Ev PGRP +The process ID of the shell's process group leader. +.It Ev PIPESTATUS +An array containing the errorlevel (exit status) codes, +one by one, of the last pipeline run in the foreground. +.It Ev PPID +The process ID of the shell's parent. +.It Ev PS1 +The primary prompt for interactive shells. +Parameter, command and arithmetic +substitutions are performed, and +.Ql \&! +is replaced with the current command number (see the +.Ic fc +command below). +A literal +.Ql \&! +can be put in the prompt by placing +.Dq Li !! +in +.Ev PS1 . +.Pp +The default prompt is +.Dq Li $\ \& +for non-root users, +.Dq Li #\ \& +for root. +If +.Nm +is invoked by root and +.Ev PS1 +does not contain a +.Ql # +character, the default value will be used even if +.Ev PS1 +already exists in the environment. +.Pp +The +.Nm +distribution comes with a sample +.Pa dot.mkshrc +containing a sophisticated example, but you might like the following one +(note that ${HOSTNAME:=$(hostname)} and the +root-vs-user distinguishing clause are (in this example) executed at +.Ev PS1 +assignment time, while the $USER and $PWD are escaped +and thus will be evaluated each time a prompt is displayed): +.Bd -literal +PS1=\*(aq${USER:=$(id \-un)}\*(aq"@${HOSTNAME:=$(hostname)}:\e$PWD $( + if (( USER_ID )); then print \e$; else print \e#; fi) " +.Ed +.Pp +Note that since the command-line editors try to figure out how long the prompt +is (so they know how far it is to the edge of the screen), escape codes in +the prompt tend to mess things up. +You can tell the shell not to count certain +sequences (such as escape codes) by prefixing your prompt with a +character (such as Ctrl-A) followed by a carriage return and then delimiting +the escape codes with this character. +Any occurrences of that character in the prompt are not printed. +By the way, don't blame me for +this hack; it's derived from the original +.Xr ksh88 1 , +which did print the delimiter character so you were out of luck +if you did not have any non-printing characters. +.Pp +Since backslashes and other special characters may be +interpreted by the shell, to set +.Ev PS1 +either escape the backslash itself +or use double quotes. +The latter is more practical. +This is a more complex example, +avoiding to directly enter special characters (for example with +.Ic \*(haV +in the emacs editing mode), +which embeds the current working directory, +in reverse video +.Pq colour would work, too , +in the prompt string: +.Bd -literal -offset indent +x=$(print \e\e001) # otherwise unused char +PS1="$x$(print \e\er)$x$(tput so)$x\e$PWD$x$(tput se)$x\*(Gt " +.Ed +.Pp +Due to a strong suggestion from David G. Korn, +.Nm +now also supports the following form: +.Bd -literal -offset indent +PS1=$\*(aq\e1\er\e1\ee[7m\e1$PWD\e1\ee[0m\e1\*(Gt \*(aq +.Ed +.It Ev PS2 +Secondary prompt string, by default +.Dq Li \*(Gt\ \& , +used when more input is needed to complete a command. +.It Ev PS3 +Prompt used by the +.Ic select +statement when reading a menu selection. +The default is +.Dq Li #?\ \& . +.It Ev PS4 +Used to prefix commands that are printed during execution tracing (see the +.Ic set Fl x +command below). +Parameter, command and arithmetic substitutions are performed +before it is printed. +The default is +.Dq Li +\ \& . +You may want to set it to +.Dq Li \&[$EPOCHREALTIME]\ \& +instead, to include timestamps. +.It Ev PWD +The current working directory. +May be unset or empty if the shell doesn't know where it is. +.It Ev RANDOM +Each time +.Ev RANDOM +is referenced, it is assigned a number between 0 and 32767 from +a Linear Congruential PRNG first. +.It Ev REPLY +Default parameter for the +.Ic read +command if no names are given. +Also used in +.Ic select +loops to store the value that is read from standard input. +.It Ev SECONDS +The number of seconds since the shell started or, if the parameter has been +assigned an integer value, the number of seconds since the assignment plus the +value that was assigned. +.It Ev TMOUT +If set to a positive integer in an interactive shell, it specifies the maximum +number of seconds the shell will wait for input after printing the primary +prompt +.Pq Ev PS1 . +If the time is exceeded, the shell exits. +.It Ev TMPDIR +The directory temporary shell files are created in. +If this parameter is not +set or does not contain the absolute path of a writable directory, temporary +files are created in +.Pa /tmp . +.It Ev USER_ID +The effective user id of the shell. +.El +.Ss Tilde expansion +Tilde expansion, which is done in parallel with parameter substitution, +is applied to words starting with an unquoted +.Ql \*(TI . +In parameter assignments (such as those preceding a simple-command or those +occurring in the arguments of a declaration utility), tilde expansion is done +after any assignment (i.e. after the equals sign) or after an unquoted colon +.Pq Ql \&: ; +login names are also delimited by colons. +The Korn shell, except in POSIX mode, always expands tildes after unquoted +equals signs, not just in assignment context (see below), and enables tab +completion for tildes after all unquoted colons during command line editing. +.Pp +The characters following the tilde, up to the first +.Ql / , +if any, are assumed to be a login name. +If the login name is empty, +.Ql + +or +.Ql \- , +the simplified value of the +.Ev HOME , +.Ev PWD +or +.Ev OLDPWD +parameter is substituted, respectively. +Otherwise, the password file is +searched for the login name, and the tilde expression is substituted with the +user's home directory. +If the login name is not found in the password file or +if any quoting or parameter substitution occurs in the login name, no +substitution is performed. +.Pp +The home directory of previously expanded login names are cached and re-used. +The +.Ic alias Fl d +command may be used to list, change and add to this cache (e.g.\& +.Ic alias \-d fac=/usr/local/facilities; cd \*(TIfac/bin ) . +.Ss Brace expansion (alternation) +Brace expressions take the following form: +.Bd -unfilled -offset indent +.Sm off +.Xo +.Ar prefix No { Ar str1 No ,..., +.Ar strN No } Ar suffix +.Xc +.Sm on +.Ed +.Pp +The expressions are expanded to +.Ar N +words, each of which is the concatenation of +.Ar prefix , +.Ar str Ns i +and +.Ar suffix +(e.g.\& +.Dq Li a{c,b{X,Y},d}e +expands to four words: +.Dq Li ace , +.Dq Li abXe , +.Dq Li abYe +and +.Dq Li ade ) . +As noted in the example, brace expressions can be nested and the resulting +words are not sorted. +Brace expressions must contain an unquoted comma +.Pq Ql \&, +for expansion to occur (e.g.\& +.Ic {} +and +.Ic {foo} +are not expanded). +Brace expansion is carried out after parameter substitution +and before file name generation. +.Ss File name patterns +A file name pattern is a word containing one or more unquoted +.Ql \&? , +.Ql * , +.Ql + , +.Ql @ +or +.Ql \&! +characters or +.Dq Li \&[...] +sequences. +Once brace expansion has been performed, the shell replaces file +name patterns with the sorted names of all the files that match the pattern +(if no files match, the word is left unchanged). +The pattern elements have the following meaning: +.Bl -tag -width Ds +.It \&? +Matches any single character. +.It \&* +Matches any sequence of octets. +.It \&[...] +Matches any of the octets inside the brackets. +Ranges of octets can be specified by separating two octets by a +.Ql \- +(e.g.\& +.Dq Li \&[a0\-9] +matches the letter +.Ql a +or any digit). +In order to represent itself, a +.Ql \- +must either be quoted or the first or last octet in the octet list. +Similarly, a +.Ql \&] +must be quoted or the first octet in the list if it is to represent itself +instead of the end of the list. +Also, a +.Ql \&! +appearing at the start of the list has special meaning (see below), so to +represent itself it must be quoted or appear later in the list. +.It \&[!...] +Like [...], +except it matches any octet not inside the brackets. +.Sm off +.It *( Ar pattern\*(Ba No ...\*(Ba Ar pattern ) +.Sm on +Matches any string of octets that matches zero or more occurrences of the +specified patterns. +Example: The pattern +.Ic *(foo\*(Babar) +matches the strings +.Dq , +.Dq Li foo , +.Dq Li bar , +.Dq Li foobarfoo , +etc. +.Sm off +.It +( Ar pattern\*(Ba No ...\*(Ba Ar pattern ) +.Sm on +Matches any string of octets that matches one or more occurrences of the +specified patterns. +Example: The pattern +.Ic +(foo\*(Babar) +matches the strings +.Dq Li foo , +.Dq Li bar , +.Dq Li foobar , +etc. +.Sm off +.It ?( Ar pattern\*(Ba No ...\*(Ba Ar pattern ) +.Sm on +Matches the empty string or a string that matches one of the specified +patterns. +Example: The pattern +.Ic ?(foo\*(Babar) +only matches the strings +.Dq , +.Dq Li foo +and +.Dq Li bar . +.Sm off +.It @( Ar pattern\*(Ba No ...\*(Ba Ar pattern ) +.Sm on +Matches a string that matches one of the specified patterns. +Example: The pattern +.Ic @(foo\*(Babar) +only matches the strings +.Dq Li foo +and +.Dq Li bar . +.Sm off +.It !( Ar pattern\*(Ba No ...\*(Ba Ar pattern ) +.Sm on +Matches any string that does not match one of the specified patterns. +Examples: The pattern +.Ic !(foo\*(Babar) +matches all strings except +.Dq Li foo +and +.Dq Li bar ; +the pattern +.Ic \&!(*) +matches no strings; the pattern +.Ic \&!(?)* +matches all strings (think about it). +.El +.Pp +Note that complicated globbing, especially with alternatives, +is slow; using separate comparisons may (or may not) be faster. +.Pp +Note that +.Nm mksh +.Po and Nm pdksh Pc +never matches +.Dq Li \&. +and +.Dq Li .. , +but +.At +.Nm ksh , +Bourne +.Nm sh +and GNU +.Nm bash +do. +.Pp +Note that none of the above pattern elements match either a period +.Pq Ql \&. +at the start of a file name or a slash +.Pq Ql / , +even if they are explicitly used in a [...] sequence; also, the names +.Dq Li \&. +and +.Dq Li .. +are never matched, even by the pattern +.Dq Li .* . +.Pp +If the +.Ic markdirs +option is set, any directories that result from file name generation are marked +with a trailing +.Ql / . +.Ss Input/output redirection +When a command is executed, its standard input, standard output and standard +error (file descriptors 0, 1 and 2, respectively) are normally inherited from +the shell. +Three exceptions to this are commands in pipelines, for which +standard input and/or standard output are those set up by the pipeline, +asynchronous commands created when job control is disabled, for which standard +input is initially set to +.Pa /dev/null , +and commands for which any of the following redirections have been specified: +.Bl -tag -width XXxxmarker +.It \*(Gt Ns Ar file +Standard output is redirected to +.Ar file . +If +.Ar file +does not exist, it is created; if it does exist, is a regular file, and the +.Ic noclobber +option is set, an error occurs; otherwise, the file is truncated. +Note that this means the command +.Ic cmd \*(Ltfoo \*(Gtfoo +will open +.Ar foo +for reading and then truncate it when it opens it for writing, before +.Ar cmd +gets a chance to actually read +.Ar foo . +.It \*(Gt\*(Ba Ns Ar file +Same as +.Ic \*(Gt , +except the file is truncated, even if the +.Ic noclobber +option is set. +.It \*(Gt\*(Gt Ns Ar file +Same as +.Ic \*(Gt , +except if +.Ar file +exists it is appended to instead of being truncated. +Also, the file is opened +in append mode, so writes always go to the end of the file (see +.Xr open 2 ) . +.It \*(Lt Ns Ar file +Standard input is redirected from +.Ar file , +which is opened for reading. +.It \*(Lt\*(Gt Ns Ar file +Same as +.Ic \*(Lt , +except the file is opened for reading and writing. +.It \*(Lt\*(Lt Ns Ar marker +After reading the command line containing this kind of redirection (called a +.Dq here document ) , +the shell copies lines from the command source into a temporary file until a +line matching +.Ar marker +is read. +When the command is executed, standard input is redirected from the +temporary file. +If +.Ar marker +contains no quoted characters, the contents of the temporary file are processed +as if enclosed in double quotes each time the command is executed, so +parameter, command and arithmetic substitutions are performed, along with +backslash +.Pq Ql \e +escapes for +.Ql $ , +.Ql \` , +.Ql \e +and +.Dq Li \enewline , +but not for +.Ql \&" . +If multiple here documents are used on the same command line, they are saved in +order. +.Pp +If no +.Ar marker +is given, the here document ends at the next +.Ic \*(Lt\*(Lt +and substitution will be performed. +If +.Ar marker +is only a set of either single +.Dq Li \*(aq\*(aq +or double +.Ql \&"" +quotes with nothing in between, the here document ends at the next empty line +and substitution will not be performed. +.It \*(Lt\*(Lt\- Ns Ar marker +Same as +.Ic \*(Lt\*(Lt , +except leading tabs are stripped from lines in the here document. +.It \*(Lt\*(Lt\*(Lt Ns Ar word +Same as +.Ic \*(Lt\*(Lt , +except that +.Ar word +.Em is +the here document. +This is called a here string. +.It \*(Lt& Ns Ar fd +Standard input is duplicated from file descriptor +.Ar fd . +.Ar fd +can be a single digit, indicating the number of an existing file descriptor; +the letter +.Ql p , +indicating the file descriptor associated with the output of the current +co-process; or the character +.Ql \- , +indicating standard input is to be closed. +.It \*(Gt& Ns Ar fd +Same as +.Ic \*(Lt& , +except the operation is done on standard output. +.It &\*(Gt Ns Ar file +Same as +.Ic \*(Gt Ns Ar file 2\*(Gt&1 . +This is a deprecated (legacy) GNU +.Nm bash +extension supported by +.Nm +which also supports the preceding explicit fd digit, for example, +.Ic 3&\*(Gt Ns Ar file +is the same as +.Ic 3\*(Gt Ns Ar file 2\*(Gt&3 +in +.Nm +but a syntax error in GNU +.Nm bash . +.It Xo +.No &\*(Gt\*(Ba Ns Ar file , +.No &\*(Gt\*(Gt Ns Ar file , +.No &\*(Gt& Ns Ar fd +.Xc +Same as +.Ic \*(Gt\*(Ba Ns Ar file , +.Ic \*(Gt\*(Gt Ns Ar file +or +.Ic \*(Gt& Ns Ar fd , +followed by +.Ic 2\*(Gt&1 , +as above. +These are +.Nm +extensions. +.El +.Pp +In any of the above redirections, the file descriptor that is redirected +(i.e. standard input or standard output) +can be explicitly given by preceding the +redirection with a single digit. +Parameter, command and arithmetic +substitutions, tilde substitutions, and, if the shell is interactive, +file name generation are all performed on the +.Ar file , +.Ar marker +and +.Ar fd +arguments of redirections. +Note, however, that the results of any file name +generation are only used if a single file is matched; if multiple files match, +the word with the expanded file name generation characters is used. +Note +that in restricted shells, redirections which can create files cannot be used. +.Pp +For simple-commands, redirections may appear anywhere in the command; for +compound-commands +.Po +.Ic if +statements, etc. +.Pc , +any redirections must appear at the end. +Redirections are processed after +pipelines are created and in the order they are given, so the following +will print an error with a line number prepended to it: +.Pp +.Dl $ cat /foo/bar 2\*(Gt&1 \*(Gt/dev/null \*(Ba pr \-n \-t +.Pp +File descriptors created by I/O redirections are private to the shell. +.Ss Arithmetic expressions +Integer arithmetic expressions can be used with the +.Ic let +command, inside $((...)) expressions, inside array references (e.g.\& +.Ar name Ns Bq Ar expr ) , +as numeric arguments to the +.Ic test +command, and as the value of an assignment to an integer parameter. +.Em Warning : +This also affects implicit conversion to integer, for example as done by the +.Ic let +command. +.Em Never +use unchecked user input, e.g. from the environment, in an arithmetic context! +.Pp +Expressions are calculated using signed arithmetic and the +.Vt mksh_ari_t +type (a 32-bit signed integer), unless they begin with a sole +.Ql # +character, in which case they use +.Vt mksh_uari_t +.Po a 32-bit unsigned integer Pc . +.Pp +Expressions may contain alpha-numeric parameter identifiers, array references +and integer constants and may be combined with the following C operators +(listed and grouped in increasing order of precedence): +.Pp +Unary operators: +.Bd -literal -offset indent ++ \- ! \*(TI ++ \-\- +.Ed +.Pp +Binary operators: +.Bd -literal -offset indent +, += += \-= *= /= %= \*(Lt\*(Lt= \*(Gt\*(Gt= \*(ha\*(Lt= \*(ha\*(Gt= &= \*(ha= \*(Ba= +\*(Ba\*(Ba +&& +\*(Ba +\*(ha +& +== != +\*(Lt \*(Lt= \*(Gt \*(Gt= +\*(Lt\*(Lt \*(Gt\*(Gt \*(ha\*(Lt \*(ha\*(Gt ++ \- +* / % +.Ed +.Pp +Ternary operators: +.Bd -literal -offset indent +?: (precedence is immediately higher than assignment) +.Ed +.Pp +Grouping operators: +.Bd -literal -offset indent +( ) +.Ed +.Pp +Integer constants and expressions are calculated using an exactly 32-bit +wide, signed or unsigned, type with silent wraparound on integer overflow. +Integer constants may be specified with arbitrary bases using the notation +.Ar base Ns # Ns Ar number , +where +.Ar base +is a decimal integer specifying the base (up to 36), and +.Ar number +is a number in the specified base. +Additionally, base-16 integers may be specified by prefixing them with +.Dq Li 0x +.Pq case-insensitive +in all forms of arithmetic expressions, except as numeric arguments to the +.Ic test +built-in utility. +Prefixing numbers with a sole digit zero +.Pq Dq Li 0 +does not cause interpretation as octal (except in POSIX mode, +as required by the standard), as that's unsafe to do. +.Pp +As a special +.Nm mksh +extension, numbers to the base of one are treated as either (8-bit +transparent) ASCII or Unicode codepoints, depending on the shell's +.Ic utf8\-mode +flag (current setting). +The +.At +.Nm ksh93 +syntax of +.Dq Li \*(aqx\*(aq +instead of +.Dq Li 1#x +is also supported. +Note that NUL bytes (integral value of zero) cannot be used. +An unset or empty parameter evaluates to 0 in integer context. +In Unicode mode, raw octets are mapped into the range EF80..EFFF as in +OPTU-8, which is in the PUA and has been assigned by CSUR for this use. +If more than one octet in ASCII mode, or a sequence of more than one +octet not forming a valid and minimal CESU-8 sequence is passed, the +behaviour is undefined (usually, the shell aborts with a parse error, +but rarely, it succeeds, e.g. on the sequence C2 20). +That's why you should always use ASCII mode unless you know that the +input is well-formed UTF-8 in the range of 0000..FFFD if you use this +feature, as opposed to +.Ic read Fl a . +.Pp +The operators are evaluated as follows: +.Bl -tag -width Ds -offset indent +.It unary + +Result is the argument (included for completeness). +.It unary \- +Negation. +.It \&! +Logical NOT; +the result is 1 if argument is zero, 0 if not. +.It \*(TI +Arithmetic (bit-wise) NOT. +.It ++ +Increment; must be applied to a parameter (not a literal or other expression). +The parameter is incremented by 1. +When used as a prefix operator, the result +is the incremented value of the parameter; when used as a postfix operator, the +result is the original value of the parameter. +.It \-\- +Similar to +.Ic ++ , +except the parameter is decremented by 1. +.It \&, +Separates two arithmetic expressions; the left-hand side is evaluated first, +then the right. +The result is the value of the expression on the right-hand side. +.It = +Assignment; the variable on the left is set to the value on the right. +.It Xo +.No += \-= *= /= %= \*(Lt\*(Lt= \*(Gt\*(Gt= +.No \*(ha\*(Lt= \*(ha\*(Gt= &= \*(ha= \*(Ba= +.Xc +Assignment operators. +.Sm off +.Ao Ar var Ac Xo +.Aq Ar op +.No = Aq Ar expr +.Xc +.Sm on +is the same as +.Sm off +.Ao Ar var Ac Xo +.No = Aq Ar var +.Aq Ar op +.Aq Ar expr , +.Xc +.Sm on +with any operator precedence in +.Aq Ar expr +preserved. +For example, +.Dq Li var1 *= 5 + 3 +is the same as specifying +.Dq Li var1 = var1 * (5 + 3) . +.It \*(Ba\*(Ba +Logical OR; +the result is 1 if either argument is non-zero, 0 if not. +The right argument is evaluated only if the left argument is zero. +.It && +Logical AND; +the result is 1 if both arguments are non-zero, 0 if not. +The right argument is evaluated only if the left argument is non-zero. +.It \*(Ba +Arithmetic (bit-wise) OR. +.It \*(ha +Arithmetic (bit-wise) XOR +(exclusive-OR). +.It & +Arithmetic (bit-wise) AND. +.It == +Equal; the result is 1 if both arguments are equal, 0 if not. +.It != +Not equal; the result is 0 if both arguments are equal, 1 if not. +.It \*(Lt +Less than; the result is 1 if the left argument is less than the right, 0 if +not. +.It \*(Lt= \*(Gt \*(Gt= +Less than or equal, greater than, greater than or equal. +See +.Ic \*(Lt . +.It \*(Lt\*(Lt \*(Gt\*(Gt +Shift left (right); the result is the left argument with its bits +arithmetically (signed operation) or logically (unsigned expression) +shifted left (right) by the amount given in the right argument. +.It \*(ha\*(Lt \*(ha\*(Gt +Rotate left (right); the result is similar to shift, +except that the bits shifted out at one end are shifted in +at the other end, instead of zero or sign bits. +.It + \- * / +Addition, subtraction, multiplication and division. +.It % +Remainder; the result is the symmetric remainder of the division of the left +argument by the right. +To get the mathematical modulus of +.Dq a Ic mod No b , +use the formula +.Do +.Pq a % b + b +.No % b +.Dc . +.It Xo +.Sm off +.Aq Ar arg1 ? +.Aq Ar arg2 : +.Aq Ar arg3 +.Sm on +.Xc +If +.Aq Ar arg1 +is non-zero, the result is +.Aq Ar arg2 ; +otherwise the result is +.Aq Ar arg3 . +The non-result argument is not evaluated. +.El +.Ss Co-processes +A co-process (which is a pipeline created with the +.Dq Li \*(Ba& +operator) is an asynchronous process that the shell can both write to (using +.Ic print Fl p ) +and read from (using +.Ic read Fl p ) . +The input and output of the co-process can also be manipulated using +.Ic \*(Gt&p +and +.Ic \*(Lt&p +redirections, respectively. +Once a co-process has been started, another can't +be started until the co-process exits, or until the co-process's input has been +redirected using an +.Ic exec Ar n Ns Ic \*(Gt&p +redirection. +If a co-process's input is redirected in this way, the next +co-process to be started will share the output with the first co-process, +unless the output of the initial co-process has been redirected using an +.Ic exec Ar n Ns Ic \*(Lt&p +redirection. +.Pp +Some notes concerning co-processes: +.Bl -bullet +.It +The only way to close the co-process's input (so the co-process reads an +end-of-file) is to redirect the input to a numbered file descriptor and then +close that file descriptor: +.Ic exec 3\*(Gt&p; exec 3\*(Gt&\- +.It +In order for co-processes to share a common output, the shell must keep the +write portion of the output pipe open. +This means that end-of-file will not be +detected until all co-processes sharing the co-process's output have exited +(when they all exit, the shell closes its copy of the pipe). +This can be +avoided by redirecting the output to a numbered file descriptor (as this also +causes the shell to close its copy). +Note that this behaviour is slightly +different from the original Korn shell which closes its copy of the write +portion of the co-process output when the most recently started co-process +(instead of when all sharing co-processes) exits. +.It +.Ic print Fl p +will ignore +.Dv SIGPIPE +signals during writes if the signal is not being trapped or ignored; the same +is true if the co-process input has been duplicated to another file descriptor +and +.Ic print Fl u Ns Ar n +is used. +.El +.Ss Functions +Functions are defined using either Korn shell +.Ic function Ar function-name +syntax or the Bourne/POSIX shell +.Ar function-name Ns \&() +syntax (see below for the difference between the two forms). +Functions are like +.Li .\(hyscripts +(i.e. scripts sourced using the +.Dq Li \&. +built-in) +in that they are executed in the current environment. +However, unlike +.Li .\(hyscripts , +shell arguments (i.e. positional parameters $1, $2, etc.)\& +are never visible inside them. +When the shell is determining the location of a command, functions +are searched after special built-in commands, before builtins and the +.Ev PATH +is searched. +.Pp +An existing function may be deleted using +.Ic unset Fl f Ar function-name . +A list of functions can be obtained using +.Ic typeset +f +and the function definitions can be listed using +.Ic typeset Fl f . +The +.Ic autoload +command (which is an alias for +.Ic typeset Fl fu ) +may be used to create undefined functions: when an undefined function is +executed, the shell searches the path specified in the +.Ev FPATH +parameter for a file with the same name as the function which, if found, is +read and executed. +If after executing the file the named function is found to +be defined, the function is executed; otherwise, the normal command search is +continued (i.e. the shell searches the regular built-in command table and +.Ev PATH ) . +Note that if a command is not found using +.Ev PATH , +an attempt is made to autoload a function using +.Ev FPATH +(this is an undocumented feature of the original Korn shell). +.Pp +Functions can have two attributes, +.Dq trace +and +.Dq export , +which can be set with +.Ic typeset Fl ft +and +.Ic typeset Fl fx , +respectively. +When a traced function is executed, the shell's +.Ic xtrace +option is turned on for the function's duration. +The +.Dq export +attribute of functions is currently not used. +In the original Korn shell, +exported functions are visible to shell scripts that are executed. +.Pp +Since functions are executed in the current shell environment, parameter +assignments made inside functions are visible after the function completes. +If this is not the desired effect, the +.Ic typeset +command can be used inside a function to create a local parameter. +Note that +.At +.Nm ksh93 +uses static scoping (one global scope, one local scope per function) +and allows local variables only on Korn style functions, whereas +.Nm mksh +uses dynamic scoping (nested scopes of varying locality). +Note that special parameters (e.g.\& +.Ic \&$$ , $! ) +can't be scoped in this way. +.Pp +The exit status of a function is that of the last command executed in the +function. +A function can be made to finish immediately using the +.Ic return +command; this may also be used to explicitly specify the exit status. +Note that when called in a subshell, +.Ic return +will only exit that subshell and will not cause the original shell to exit +a running function (see the +.Ic while Ns Li \&... Ns Ic read +loop FAQ below). +.Pp +Functions defined with the +.Ic function +reserved word are treated differently in the following ways from functions +defined with the +.Ic \&() +notation: +.Bl -bullet +.It +The $0 parameter is set to the name of the function +(Bourne-style functions leave $0 untouched). +.It +Parameter assignments preceding function calls are not kept in the shell +environment (executing Bourne-style functions will keep assignments). +.It +.Ev OPTIND +is saved/reset and restored on entry and exit from the function so +.Ic getopts +can be used properly both inside and outside the function (Bourne-style +functions leave +.Ev OPTIND +untouched, so using +.Ic getopts +inside a function interferes with using +.Ic getopts +outside the function). +.It +Shell options +.Pq Ic set Fl o +have local scope, i.e. changes inside a function are reset upon its exit. +.El +.Pp +In the future, the following differences may also be added: +.Bl -bullet +.It +A separate trap/signal environment will be used during the execution of +functions. +This will mean that traps set inside a function will not affect the +shell's traps and signals that are not ignored in the shell (but may be +trapped) will have their default effect in a function. +.It +The EXIT trap, if set in a function, will be executed after the function +returns. +.El +.Ss Command execution +After evaluation of command-line arguments, redirections and parameter +assignments, the type of command is determined: a special built-in command, +a function, a normal builtin or the name of a file to execute found using the +.Ev PATH +parameter. +The checks are made in the above order. +Special built-in commands differ from other commands in that the +.Ev PATH +parameter is not used to find them, an error during their execution can +cause a non-interactive shell to exit, and parameter assignments that are +specified before the command are kept after the command completes. +Regular built-in commands are different only in that the +.Ev PATH +parameter is not used to find them. +.Pp +The original +.Nm ksh +and POSIX differ somewhat in which commands are considered +special or regular. +.Pp +POSIX special built-in utilities: +.Pp +.Ic \&. , \&: , break , continue , +.Ic eval , exec , exit , export , +.Ic readonly , return , set , shift , +.Ic times , trap , unset +.Pp +Additional +.Nm +commands keeping assignments: +.Pp +.Ic global , source , typeset +.Pp +Builtins that are not special: +.Pp +.Ic [ , alias , bg , bind , +.Ic builtin , cat , cd , command , +.Ic echo , false , fc , fg , +.Ic getopts , jobs , kill , let , +.Ic print , pwd , read , realpath , +.Ic rename , sleep , suspend , test , +.Ic true , ulimit , umask , unalias , +.Ic wait , whence +.Pp +Once the type of command has been determined, any command-line parameter +assignments are performed and exported for the duration of the command. +.Pp +The following describes the special and regular built-in commands and +builtin-like reserved words: +.Pp +.Bl -tag -width false -compact +.It Ic \&. Ar file Op Ar arg ... +This is called the +.Dq dot +command. +Execute the commands in +.Ar file +in the current environment. +The file is searched for in the directories of +.Ev PATH . +If arguments are given, the positional parameters may be used to access them +while +.Ar file +is being executed. +If no arguments are given, the positional parameters are +those of the environment the command is used in. +.Pp +.It Ic \&: Op Ar ... +The null command. +Exit status is set to zero. +.Pp +.It Ic \&[ Ar expression Ic \&] +See +.Ic test . +.Pp +.It Xo Ic alias +.Oo Fl d \*(Ba t Oo Fl r Oc \*(Ba +.Cm +\-x Oc +.Op Fl p +.Op Cm + +.Oo Ar name +.Op Ns = Ns Ar value +.Ar ... Oc +.Xc +Without arguments, +.Ic alias +lists all aliases. +For any name without a value, the existing alias is listed. +Any name with a value defines an alias; see +.Sx Aliases +above. +.Li \&[][A\-Za\-z0\-9_!%,.@:\-] +are valid in names, except they may not begin with a hyphen-minus, and +.Ic \&[[ +is not a valid alias name. +.Pp +When listing aliases, one of two formats is used. +Normally, aliases are listed as +.Ar name Ns = Ns Ar value , +where +.Ar value +is quoted. +If options were preceded with +.Ql + , +or a lone +.Ql + +is given on the command line, only +.Ar name +is printed. +.Pp +The +.Fl d +option causes directory aliases which are used in tilde expansion to be +listed or set (see +.Sx Tilde expansion +above). +.Pp +If the +.Fl p +option is used, each alias is prefixed with the string +.Dq Li alias\ \& . +.Pp +The +.Fl t +option indicates that tracked aliases are to be listed/set (values specified on +the command line are ignored for tracked aliases). +The +.Fl r +option indicates that all tracked aliases are to be reset. +.Pp +The +.Fl x +option sets +.Pq Ic +x No clears +the export attribute of an alias, or, if no names are given, lists the aliases +with the export attribute (exporting an alias has no effect). +.Pp +.It Ic bg Op Ar job ... +Resume the specified stopped job(s) in the background. +If no jobs are specified, +.Ic %+ +is assumed. +See +.Sx Job control +below for more information. +.Pp +.It Ic bind Op Fl l +The current bindings are listed. +If the +.Fl l +flag is given, +.Ic bind +instead lists the names of the functions to which keys may be bound. +See +.Sx Emacs editing mode +for more information. +.Pp +.It Xo Ic bind Op Fl m +.Ar string Ns = Ns Op Ar substitute +.Ar ... +.Xc +.It Xo Ic bind +.Ar string Ns = Ns Op Ar editing-command +.Ar ... +.Xc +The specified editing command is bound to the given +.Ar string , +which should consist of a control character +optionally preceded by one of the two prefix characters +and optionally succeeded by a tilde character. +Future input of the +.Ar string +will cause the editing command to be immediately invoked. +If the +.Fl m +flag is given, the specified input +.Ar string +will afterwards be immediately replaced by the given +.Ar substitute +string which may contain editing commands but not other macros. +If a tilde postfix is given, a tilde trailing the one or +two prefices and the control character is ignored, any +other trailing character will be processed afterwards. +.Pp +Control characters may be written using caret notation +i.e. \*(haX represents Ctrl-X. +The caret itself can be escaped by a backslash, which also escapes itself. +Note that although only three prefix characters (usually ESC, \*(haX and NUL) +are supported, some multi-character sequences can be supported. +.Pp +The following default bindings show how the arrow keys, the home, end and +delete key on a BSD wsvt25, xterm\-xfree86 or GNU screen terminal are bound +(of course some escape sequences won't work out quite this nicely): +.Bd -literal -offset indent +bind \*(aq\*(haX\*(aq=prefix\-2 +bind \*(aq\*(ha[[\*(aq=prefix\-2 +bind \*(aq\*(haXA\*(aq=up\-history +bind \*(aq\*(haXB\*(aq=down\-history +bind \*(aq\*(haXC\*(aq=forward\-char +bind \*(aq\*(haXD\*(aq=backward\-char +bind \*(aq\*(haX1\*(TI\*(aq=beginning\-of\-line +bind \*(aq\*(haX7\*(TI\*(aq=beginning\-of\-line +bind \*(aq\*(haXH\*(aq=beginning\-of\-line +bind \*(aq\*(haX4\*(TI\*(aq=end\-of\-line +bind \*(aq\*(haX8\*(TI\*(aq=end\-of\-line +bind \*(aq\*(haXF\*(aq=end\-of\-line +bind \*(aq\*(haX3\*(TI\*(aq=delete\-char\-forward +.Ed +.Pp +.It Ic break Op Ar level +Exit the +.Ar level Ns th +inner-most +.Ic for , +.Ic select , +.Ic until +or +.Ic while +loop. +.Ar level +defaults to 1. +.Pp +.It Xo +.Ic builtin +.Op Fl \- +.Ar command Op Ar arg ... +.Xc +Execute the built-in command +.Ar command . +.Pp +.It Xo +.Ic \ebuiltin +.Ar command Op Ar arg ... +.Xc +Same as +.Ic builtin . +Additionally acts as declaration utility forwarder, i.e. this is a +declaration utility (see +.Sx Tilde expansion ) +.No iff Ar command +is a declaration utility. +.Pp +.It Xo +.Ic cat +.Op Fl u +.Op Ar +.Xc +Read files sequentially, in command line order, and write them to +standard output. +If a +.Ar file +is a single dash +.Pq Dq Li \- +or absent, read from standard input. +For direct builtin calls, the +.Tn POSIX +.Fl u +option is supported as a no-op. +For calls from shell, if any options are given, an external +.Xr cat 1 +utility is preferred over the builtin. +.Pp +.It Xo +.Ic cd +.Op Fl L +.Op Ar dir +.Xc +.It Xo +.Ic cd +.Fl P Op Fl e +.Op Ar dir +.Xc +.It Xo +.Ic chdir +.Op Fl eLP +.Op Ar dir +.Xc +Set the working directory to +.Ar dir . +If the parameter +.Ev CDPATH +is set, it lists the search path for the directory containing +.Ar dir . +An unset or empty path means the current directory. +If +.Ar dir +is found in any component of the +.Ev CDPATH +search path other than an unset or empty path, +the name of the new working directory will be written to standard output. +If +.Ar dir +is missing, the home directory +.Ev HOME +is used. +If +.Ar dir +is +.Dq Li \- , +the previous working directory is used (see the +.Ev OLDPWD +parameter). +.Pp +If the +.Fl L +option (logical path) is used or if the +.Ic physical +option isn't set (see the +.Ic set +command below), references to +.Dq Li .. +in +.Ar dir +are relative to the path used to get to the directory. +If the +.Fl P +option (physical path) is used or if the +.Ic physical +option is set, +.Dq Li .. +is relative to the filesystem directory tree. +The +.Ev PWD +and +.Ev OLDPWD +parameters are updated to reflect the current and old working directory, +respectively. +If the +.Fl e +option is set for physical filesystem traversal and +.Ev PWD +could not be set, the exit code is 1; greater than 1 if an +error occurred, 0 otherwise. +.Pp +.It Xo +.Ic cd +.Op Fl eLP +.Ar old new +.Xc +.It Xo +.Ic chdir +.Op Fl eLP +.Ar old new +.Xc +The string +.Ar new +is substituted for +.Ar old +in the current directory, and the shell attempts to change to the new +directory. +.Pp +.It Xo +.Ic command +.Op Fl pVv +.Ar cmd +.Op Ar arg ... +.Xc +If neither the +.Fl v +nor +.Fl V +option is given, +.Ar cmd +is executed exactly as if +.Ic command +had not been specified, with two exceptions: +firstly, +.Ar cmd +cannot be a shell function; +and secondly, special built-in commands lose their specialness +(i.e. redirection and utility errors do not cause the shell to +exit, and command assignments are not permanent). +The declaration utility property is not reset. +.Pp +If the +.Fl p +option is given, a default search path is used instead of the current value of +.Ev PATH , +the actual value of which is system dependent. +.Pp +If the +.Fl v +option is given, instead of executing +.Ar cmd , +information about what would be executed is given (and the same is done for +.Ar arg ... ) . +For builtins, functions and keywords, their names are simply printed; +for aliases, a command that defines them is printed; +for utilities found by searching the +.Ev PATH +parameter, the full path of the command is printed. +If no command is found +(i.e. the path search fails), nothing is printed and +.Ic command +exits with a non-zero status. +The +.Fl V +option is like the +.Fl v +option, except it is more verbose. +.Pp +.It Ic continue Op Ar level +Jumps to the beginning of the +.Ar level Ns th +inner-most +.Ic for , +.Ic select , +.Ic until +or +.Ic while +loop. +.Ar level +defaults to 1. +.Pp +.It Xo +.Ic echo +.Op Fl Een +.Op Ar arg ... +.Xc +.Em Warning: +this utility is not portable; use the Korn shell builtin +.Ic print +instead. +.Pp +Prints its arguments (separated by spaces) followed by a newline, to the +standard output. +The newline is suppressed if any of the arguments contain the +backslash sequence +.Dq Li \ec . +See the +.Ic print +command below for a list of other backslash sequences that are recognised. +.Pp +The options are provided for compatibility with +.Bx +shell scripts. +The +.Fl n +option suppresses the trailing newline, +.Fl e +enables backslash interpretation (a no-op, since this is normally done), and +.Fl E +suppresses backslash interpretation. +.Pp +If the +.Ic posix +or +.Ic sh +option is set or this is a direct builtin call or +.Ic print +.Fl R , +only the first argument is treated as an option, and only if it is exactly +.Dq Li \-n . +Backslash interpretation is disabled. +.Pp +.It Ic eval Ar command ... +The arguments are concatenated (with spaces between them) to form a single +string which the shell then parses and executes in the current environment. +.Pp +.It Xo +.Ic exec +.Op Fl a Ar argv0 +.Op Fl c +.Op Ar command Op Ar arg ... +.Xc +The command is executed without forking, replacing the shell process. +This is currently absolute, i.e.\& +.Ic exec +never returns, even if the +.Ar command +is not found. +The +.Fl a +option permits setting a different +.Li argv[0] +value, and +.Fl c +clears the environment before executing the child process, except for the +.Ev _ +variable and direct assignments. +.Pp +If no command is given except for I/O redirection, the I/O redirection is +permanent and the shell is +not replaced. +Any file descriptors greater than 2 which are opened or +.Xr dup 2 Ns 'd +in this way are not made available to other executed commands (i.e. commands +that are not built-in to the shell). +Note that the Bourne shell differs here; +it does pass these file descriptors on. +.Pp +.It Ic exit Op Ar status +The shell or subshell exits with the specified exit status. +If +.Ar status +is not specified, the exit status is the current value of the +.Ic \&$? +parameter. +.Pp +.It Xo +.Ic export +.Op Fl p +.Op Ar parameter Ns Op = Ns Ar value +.Xc +Sets the export attribute of the named parameters. +Exported parameters are passed in the environment to executed commands. +If values are specified, the named parameters are also assigned. +This is a declaration utility. +.Pp +If no parameters are specified, all parameters with the export attribute +set are printed one per line; either their names, or, if a +.Dq Li \- +with no option letter is specified, name=value pairs, or, with +.Fl p , +.Ic export +commands suitable for re-entry. +.Pp +.It Ic false +A command that exits with a non-zero status. +.Pp +.It Xo +.Ic fc +.Oo Fl e Ar editor \*(Ba +.Fl l Op Fl n Oc +.Op Fl r +.Op Ar first Op Ar last +.Xc +.Ar first +and +.Ar last +select commands from the history. +Commands can be selected by history number +(negative numbers go backwards from the current, most recent, line) +or a string specifying the most recent command starting with that string. +The +.Fl l +option lists the command on standard output, and +.Fl n +inhibits the default command numbers. +The +.Fl r +option reverses the order of the list. +Without +.Fl l , +the selected commands are edited by the editor specified with the +.Fl e +option or, if no +.Fl e +is specified, the editor specified by the +.Ev FCEDIT +parameter (if this parameter is not set, +.Pa /bin/ed +is used), and then executed by the shell. +.Pp +.It Xo +.Ic fc +.Cm \-e \- \*(Ba Fl s +.Op Fl g +.Op Ar old Ns = Ns Ar new +.Op Ar prefix +.Xc +Re-execute the selected command (the previous command by default) after +performing the optional substitution of +.Ar old +with +.Ar new . +If +.Fl g +is specified, all occurrences of +.Ar old +are replaced with +.Ar new . +The meaning of +.Cm \-e \- +and +.Fl s +is identical: re-execute the selected command without invoking an editor. +This command is usually accessed with the predefined: +.Ic alias r=\*(aqfc \-e \-\*(aq +.Pp +.It Ic fg Op Ar job ... +Resume the specified job(s) in the foreground. +If no jobs are specified, +.Ic %+ +is assumed. +See +.Sx Job control +below for more information. +.Pp +.It Xo +.Ic getopts +.Ar optstring name +.Op Ar arg ... +.Xc +Used by shell procedures to parse the specified arguments (or positional +parameters, if no arguments are given) and to check for legal options. +.Ar optstring +contains the option letters that +.Ic getopts +is to recognise. +If a letter is followed by a colon, the option is expected to +have an argument. +Options that do not take arguments may be grouped in a single argument. +If an option takes an argument and the option character is not the +last character of the argument it is found in, the remainder of the argument is +taken to be the option's argument; otherwise, the next argument is the option's +argument. +.Pp +Each time +.Ic getopts +is invoked, it places the next option in the shell parameter +.Ar name +and the index of the argument to be processed by the next call to +.Ic getopts +in the shell parameter +.Ev OPTIND . +If the option was introduced with a +.Ql + , +the option placed in +.Ar name +is prefixed with a +.Ql + . +When an option requires an argument, +.Ic getopts +places it in the shell parameter +.Ev OPTARG . +.Pp +When an illegal option or a missing option argument is encountered, a question +mark or a colon is placed in +.Ar name +(indicating an illegal option or missing argument, respectively) and +.Ev OPTARG +is set to the option character that caused the problem. +Furthermore, if +.Ar optstring +does not begin with a colon, a question mark is placed in +.Ar name , +.Ev OPTARG +is unset, and an error message is printed to standard error. +.Pp +When the end of the options is encountered, +.Ic getopts +exits with a non-zero exit status. +Options end at the first (non-option +argument) argument that does not start with a +.Ql \- , +or when a +.Dq Li \-\- +argument is encountered. +.Pp +Option parsing can be reset by setting +.Ev OPTIND +to 1 (this is done automatically whenever the shell or a shell procedure is +invoked). +.Pp +Warning: Changing the value of the shell parameter +.Ev OPTIND +to a value other than 1 or parsing different sets of arguments without +resetting +.Ev OPTIND +may lead to unexpected results. +.Pp +.It Xo +.Ic global +.Op Ic +\-aglpnrtUux +.Oo Fl L Ns Op Ar n +.No \*(Ba Fl R Ns Op Ar n +.No \*(Ba Fl Z Ns Op Ar n Oc +.Op Fl i Ns Op Ar n +.Oo Ar name +.Op Ns = Ns Ar value +.Ar ... Oc +.Xc +See +.Ic typeset Fl g . +.No Deprecated , Em will +be removed from a future version of +.Nm . +.Pp +.It Xo +.Ic hash +.Op Fl r +.Op Ar name ... +.Xc +Without arguments, any hashed executable command pathnames are listed. +The +.Fl r +option causes all hashed commands to be removed from the hash table. +Each +.Ar name +is searched as if it were a command name and added to the hash table if it is +an executable command. +.Pp +.It Xo +.Ic jobs +.Op Fl lnp +.Op Ar job ... +.Xc +Display information about the specified job(s); if no jobs are specified, all +jobs are displayed. +The +.Fl n +option causes information to be displayed only for jobs that have changed +state since the last notification. +If the +.Fl l +option is used, the process ID of each process in a job is also listed. +The +.Fl p +option causes only the process group of each job to be printed. +See +.Sx Job control +below for the format of +.Ar job +and the displayed job. +.Pp +.It Xo +.Ic kill +.Oo Fl s Ar signame \*(Ba +.No \- Ns Ar signum \*(Ba +.No \- Ns Ar signame Oc +.No { Ar job \*(Ba pid \*(Ba pgrp No } +.Ar ... +.Xc +Send the specified signal to the specified jobs, process IDs or process +groups. +If no signal is specified, the +.Dv TERM +signal is sent. +If a job is specified, the signal is sent to the job's process group. +See +.Sx Job control +below for the format of +.Ar job . +.Pp +.It Xo +.Ic kill +.Fl l +.Op Ar exit-status ... +.Xc +Print the signal name corresponding to +.Ar exit-status . +If no arguments are specified, a list of all the signals with their numbers +and a short description of each are printed. +.Pp +.It Ic let Op Ar expression ... +Each expression is evaluated (see +.Sx Arithmetic expressions +above). +If all expressions are successfully evaluated, the exit status is 0 (1) +if the last expression evaluated to non-zero (zero). +If an error occurs during +the parsing or evaluation of an expression, the exit status is greater than 1. +Since expressions may need to be quoted, +.No \&(( Ar expr No )) +is syntactic sugar for: +.Dl "{ \e\ebuiltin let \*(aq" Ns Ar expr Ns "\*(aq; }" +.Pp +.It Xo +.Ic mknod +.Op Fl m Ar mode +.Ar name +.Cm b\*(Bac +.Ar major minor +.Xc +.It Xo +.Ic mknod +.Op Fl m Ar mode +.Ar name +.Cm p +.Xc +Create a device special file. +The file type may be +.Cm b +(block type device), +.Cm c +(character type device) +or +.Cm p +.Pq named pipe , Tn FIFO . +The file created may be modified according to its +.Ar mode +(via the +.Fl m +option), +.Ar major +(major device number), +and +.Ar minor +(minor device number). +This is not normally part of +.Nm mksh ; +however, distributors may have added this as builtin as a speed hack. +.Pp +.It Xo +.Ic print +.Oo Fl AcelNnprsu Ns Oo Ar n Oc \*(Ba +.Fl R Op Fl n Oc +.Op Ar argument ... +.Xc +Print the specified argument(s) on the standard output, +separated by spaces, terminated with a newline. +The escapes mentioned in +.Sx Backslash expansion +above, as well as +.Dq Li \ec , +which is equivalent to using the +.Fl n +option, are interpreted. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl A +Each +.Ar argument +is arithmetically evaluated; the character corresponding to the +resulting value is printed. +Empty +.Ar argument Ns s +separate input words. +.It Fl c +The output is printed columnised, line by line, similar to how the +.Xr rs 1 +utility, tab completion, the +.Ic kill Fl l +built-in utility and the +.Ic select +statement do. +.It Fl e +Restore backslash expansion after a previous +.Fl r . +.It Fl l +Change the output word separator to newline. +.It Fl N +Change the output word and line separator to ASCII NUL. +.It Fl n +Do not print the trailing line separator. +.It Fl p +Print to the co-process (see +.Sx Co-processes +above). +.It Fl r +Inhibit backslash expansion. +.It Fl s +Print to the history file instead of standard output. +.It Fl u Ns Op Ar n +Print to the file descriptor +.Ar n Pq defaults to 1 if omitted +instead of standard output. +.El +.Pp +The +.Fl R +option mostly emulates the +.Bx +.Xr echo 1 +command which does not expand backslashes and interprets +its first argument as option only if it is exactly +.Dq Li \-n +.Pq to suppress the trailing newline . +.Pp +.It Ic pwd Op Fl LP +Print the present working directory. +If the +.Fl L +option is used or if the +.Ic physical +option isn't set (see the +.Ic set +command below), the logical path is printed (i.e. the path used to +.Ic cd +to the current directory). +If the +.Fl P +option (physical path) is used or if the +.Ic physical +option is set, the path determined from the filesystem (by following +.Dq Li .. +directories to the root directory) is printed. +.Pp +.It Xo +.Ic read +.Op Fl A \*(Ba Fl a +.Op Fl d Ar x +.Oo Fl N Ar z \*(Ba +.Fl n Ar z Oc +.Oo Fl p \*(Ba +.Fl u Ns Op Ar n +.Oc Op Fl t Ar n +.Op Fl rs +.Op Ar p ... +.Xc +Reads a line of input, separates the input into fields using the +.Ev IFS +parameter (see +.Sx Substitution +above), and assigns each field to the specified parameters +.Ar p . +If no parameters are specified, the +.Ev REPLY +parameter is used to store the result. +With the +.Fl A +and +.Fl a +options, only no or one parameter is accepted. +If there are more parameters than fields, the extra parameters are set to +the empty string or 0; if there are more fields than parameters, the last +parameter is assigned the remaining fields (including the word separators). +.Pp +The options are as follows: +.Bl -tag -width XuXnX +.It Fl A +Store the result into the parameter +.Ar p +(or +.Ev REPLY ) +as array of words. +.It Fl a +Store the result without word splitting into the parameter +.Ar p +(or +.Ev REPLY ) +as array of characters (wide characters if the +.Ic utf8\-mode +option is enacted, octets otherwise); the codepoints are +encoded as decimal numbers by default. +.It Fl d Ar x +Use the first byte of +.Ar x , +.Dv NUL +if empty, instead of the ASCII newline character as input line delimiter. +.It Fl N Ar z +Instead of reading till end-of-line, read exactly +.Ar z +bytes. +Upon EOF, a partial read is returned with exit status 1. +After timeout, a partial read is returned with an exit status as if +.Dv SIGALRM +were caught. +.It Fl n Ar z +Instead of reading till end-of-line, read up to +.Ar z +bytes but return as soon as any bytes are read, e.g.\& from a +slow terminal device, or if EOF or a timeout occurs. +.It Fl p +Read from the currently active co-process, see +.Sx Co-processes +above for details on this. +.It Fl u Ns Op Ar n +Read from the file descriptor +.Ar n +(defaults to 0, i.e.\& standard input). +The argument must immediately follow the option character. +.It Fl t Ar n +Interrupt reading after +.Ar n +seconds (specified as positive decimal value with an optional fractional part). +The exit status of +.Nm read +is the same as if +.Dv SIGALRM +were caught if the timeout occurred, but partial reads may still be returned. +.It Fl r +Normally, the ASCII backslash character escapes the special +meaning of the following character and is stripped from the input; +.Ic read +does not stop when encountering a backslash-newline sequence and +does not store that newline in the result. +This option enables raw mode, in which backslashes are not processed. +.It Fl s +The input line is saved to the history. +.El +.Pp +If the input is a terminal, both the +.Fl N +and +.Fl n +options set it into raw mode; +they read an entire file if \-1 is passed as +.Ar z +argument. +.Pp +The first parameter may have a question mark and a string appended to it, in +which case the string is used as a prompt (printed to standard error before +any input is read) if the input is a +.Xr tty 4 +(e.g.\& +.Ic read nfoo?\*(aqnumber of foos: \*(aq ) . +.Pp +If no input is read or a timeout occurred, +.Ic read +exits with a non-zero status. +.Pp +.It Xo +.Ic readonly +.Op Fl p +.Oo Ar parameter +.Op Ns = Ns Ar value +.Ar ... Oc +.Xc +Sets the read-only attribute of the named parameters. +This is a declaration utility. +If values are given, +parameters are set to them before setting the attribute. +Once a parameter is +made read-only, it cannot be unset and its value cannot be changed. +.Pp +If no parameters are specified, the names of all parameters with the read-only +attribute are printed one per line, unless the +.Fl p +option is used, in which case +.Ic readonly +commands defining all read-only parameters, including their values, are +printed. +.Pp +.It Xo +.Ic realpath +.Op Fl \- +.Ar name +.Xc +Prints the resolved absolute pathname corresponding to +.Ar name . +If +.Ar name +ends with a slash +.Pq Ql / , +it's also checked for existence and whether it is a directory; otherwise, +.Ic realpath +returns 0 if the pathname either exists or can be created immediately, +i.e. all but the last component exist and are directories. +For calls from the shell, if any options are given, an external +.Xr realpath 1 +utility is preferred over the builtin. +.Pp +.It Xo +.Ic rename +.Op Fl \- +.Ar from to +.Xc +Renames the file +.Ar from +to +.Ar to . +Both must be complete pathnames and on the same device. +An external utility is preferred over this builtin, +which is intended for emergency situations +.Pq where Pa /bin/mv No becomes unusable +and directly calls +.Xr rename 2 . +.Pp +.It Ic return Op Ar status +Returns from a function or +.Ic \&. +script, with exit status +.Ar status . +If no +.Ar status +is given, the exit status of the last executed command is used. +If used outside of a function or +.Ic \&. +script, it has the same effect as +.Ic exit . +Note that +.Nm +treats both profile and +.Ev ENV +files as +.Ic \&. +scripts, while the original Korn shell only treats profiles as +.Ic \&. +scripts. +.Pp +.It Xo +.Ic set Op Ic +\-abCefhiklmnprsUuvXx +.Op Ic +\-o Ar option +.Op Ic +\-A Ar name +.Op Fl \- +.Op Ar arg ... +.Xc +The +.Ic set +command can be used to set +.Pq Ic \- +or clear +.Pq Ic + +shell options, set the positional parameters, or set an array parameter. +Options can be changed using the +.Cm +\-o Ar option +syntax, where +.Ar option +is the long name of an option, or using the +.Cm +\- Ns Ar letter +syntax, where +.Ar letter +is the option's single letter name (not all options have a single letter name). +The following table lists both option letters (if they exist) and long names +along with a description of what the option does: +.Bl -tag -width 3n +.It Fl A Ar name +Sets the elements of the array parameter +.Ar name +to +.Ar arg ... +If +.Fl A +is used, the array is reset (i.e. emptied) first; if +.Ic +A +is used, the first N elements are set (where N is the number of arguments); +the rest are left untouched. +.Pp +An alternative syntax for the command +.Ic set \-A foo \-\- a b c +which is compatible to +.Tn GNU +.Nm bash +and also supported by +.At +.Nm ksh93 +is: +.Ic foo=(a b c); foo+=(d e) +.It Fl a \*(Ba Fl o Ic allexport +All new parameters are created with the export attribute. +.It Fl b \*(Ba Fl o Ic notify +Print job notification messages asynchronously, instead of just before the +prompt. +Only used if job control is enabled +.Pq Fl m . +.It Fl C \*(Ba Fl o Ic noclobber +Prevent \*(Gt redirection from overwriting existing files. +Instead, \*(Gt\*(Ba must be used to force an overwrite. +Note that this is not safe to use for creation of temporary files or +lockfiles due to a TOCTOU in a check allowing one to redirect output to +.Pa /dev/null +or other device files even in +.Ic noclobber +mode. +.It Fl e \*(Ba Fl o Ic errexit +Exit (after executing the +.Dv ERR +trap) as soon as an error occurs or a command fails (i.e. exits with a +non-zero status). +This does not apply to commands whose exit status is +explicitly tested by a shell construct such as +.Ic if , +.Ic until , +.Ic while +or +.Ic \&! +statements. +For +.Ic && +or +.Ic \*(Ba\*(Ba , +only the status of the last command is tested. +.It Fl f \*(Ba Fl o Ic noglob +Do not expand file name patterns. +.It Fl h \*(Ba Fl o Ic trackall +Create tracked aliases for all executed commands (see +.Sx Aliases +above). +Enabled by default for non-interactive shells. +.It Fl i \*(Ba Fl o Ic interactive +The shell is an interactive shell. +This option can only be used when the shell is invoked. +See above for a description of what this means. +.It Fl k \*(Ba Fl o Ic keyword +Parameter assignments are recognised anywhere in a command. +.It Fl l \*(Ba Fl o Ic login +The shell is a login shell. +This option can only be used when the shell is invoked. +See above for a description of what this means. +.It Fl m \*(Ba Fl o Ic monitor +Enable job control (default for interactive shells). +.It Fl n \*(Ba Fl o Ic noexec +Do not execute any commands. +Useful for checking the syntax of scripts +(ignored if interactive). +.It Fl p \*(Ba Fl o Ic privileged +The shell is a privileged shell. +It is set automatically if, when the shell starts, +the real UID or GID does not match +the effective UID (EUID) or GID (EGID), respectively. +See above for a description of what this means. +.It Fl r \*(Ba Fl o Ic restricted +The shell is a restricted shell. +This option can only be used when the shell is invoked. +See above for a description of what this means. +.It Fl s \*(Ba Fl o Ic stdin +If used when the shell is invoked, commands are read from standard input. +Set automatically if the shell is invoked with no arguments. +.Pp +When +.Fl s +is used with the +.Ic set +command it causes the specified arguments to be sorted before assigning them to +the positional parameters (or to array +.Ar name , +if +.Fl A +is used). +.It Fl U \*(Ba Fl o Ic utf8\-mode +Enable UTF-8 support in the +.Sx Emacs editing mode +and internal string handling functions. +This flag is disabled by default, but can be enabled by setting it on the +shell command line; is enabled automatically for interactive shells if +requested at compile time, your system supports +.Fn setlocale LC_CTYPE \&"" +and optionally +.Fn nl_langinfo CODESET , +or the +.Ev LC_ALL , +.Ev LC_CTYPE +or +.Ev LANG +environment variables, +and at least one of these returns something that matches +.Dq UTF\-8 +or +.Dq utf8 +case-insensitively; for direct builtin calls depending on the +aforementioned environment variables; or for stdin or scripts, +if the input begins with a UTF-8 Byte Order Mark. +.Pp +In near future, locale tracking will be implemented, which means that +.Ic set Fl +U +is changed whenever one of the +.Tn POSIX +locale-related environment variables changes. +.It Fl u \*(Ba Fl o Ic nounset +Referencing of an unset parameter, other than +.Dq Li $@ +or +.Dq Li $* , +is treated as an error, unless one of the +.Ql \- , +.Ql + +or +.Ql = +modifiers is used. +.It Fl v \*(Ba Fl o Ic verbose +Write shell input to standard error as it is read. +.It Fl X \*(Ba Fl o Ic markdirs +Mark directories with a trailing +.Ql / +during file name generation. +.It Fl x \*(Ba Fl o Ic xtrace +Print command trees when they are executed, preceded by +the value of +.Ev PS4 . +.It Fl o Ic bgnice +Background jobs are run with lower priority. +.It Fl o Ic braceexpand +Enable brace expansion (a.k.a. alternation). +This is enabled by default. +.It Fl o Ic emacs +Enable BRL emacs-like command-line editing (interactive shells only); see +.Sx Emacs editing mode . +.It Fl o Ic gmacs +Enable gmacs-like command-line editing (interactive shells only). +Currently identical to emacs editing except that transpose\-chars (\*(haT) acts +slightly differently. +.It Fl o Ic ignoreeof +The shell will not (easily) exit when end-of-file is read; +.Ic exit +must be used. +To avoid infinite loops, the shell will exit if +.Dv EOF +is read 13 times in a row. +.It Fl o Ic inherit\-xtrace +Do not reset +.Fl o Ic xtrace +upon entering functions. +This is enabled by default. +.It Fl o Ic nohup +Do not kill running jobs with a +.Dv SIGHUP +signal when a login shell exits. +Currently set by default, but this may +change in the future to be compatible with +.At +.Nm ksh , +which +doesn't have this option, but does send the +.Dv SIGHUP +signal. +.It Fl o Ic nolog +No effect. +In the original Korn shell, this prevents function definitions from +being stored in the history file. +.It Fl o Ic physical +Causes the +.Ic cd +and +.Ic pwd +commands to use +.Dq physical +(i.e. the filesystem's) +.Dq Li .. +directories instead of +.Dq logical +directories (i.e. the shell handles +.Dq Li .. , +which allows the user to be oblivious of symbolic links to directories). +Clear by default. +Note that setting this option does not affect the current value of the +.Ev PWD +parameter; only the +.Ic cd +command changes +.Ev PWD . +See the +.Ic cd +and +.Ic pwd +commands above for more details. +.It Fl o Ic pipefail +Make the exit status of a pipeline (before logically complementing) the +rightmost non-zero errorlevel, or zero if all commands exited with zero. +.It Fl o Ic posix +Behave closer to the standards +(see +.Sx POSIX mode +for details). +Automatically enabled if the basename of the shell invocation begins with +.Dq sh +and this autodetection feature is compiled in +.Pq not in MirBSD . +As a side effect, setting this flag turns off the +.Ic braceexpand +and +.Ic utf8\-mode +flags, which can be turned back on manually, and +.Ic sh +mode (unless both are enabled at the same time). +.It Fl o Ic sh +Enable +.Pa /bin/sh +.Pq kludge +mode (see +.Sx SH mode ) . +Automatically enabled if the basename of the shell invocation begins with +.Dq sh +and this autodetection feature is compiled in +.Pq not in MirBSD . +As a side effect, setting this flag turns off +.Ic braceexpand +mode, which can be turned back on manually, and +.Ic posix +mode (unless both are enabled at the same time). +.It Fl o Ic vi +Enable +.Xr vi 1 Ns -like +command-line editing (interactive shells only). +See +.Sx Vi editing mode +for documentation and limitations. +.It Fl o Ic vi\-esccomplete +In vi command-line editing, do command and file name completion when escape +(\*(ha[) is entered in command mode. +.It Fl o Ic vi\-tabcomplete +In vi command-line editing, do command and file name completion when tab (\*(haI) +is entered in insert mode. +This is the default. +.It Fl o Ic viraw +No effect. +In the original Korn shell, unless +.Ic viraw +was set, the vi command-line mode would let the +.Xr tty 4 +driver do the work until ESC (\*(ha[) was entered. +.Nm +is always in viraw mode. +.El +.Pp +These options can also be used upon invocation of the shell. +The current set of +options (with single letter names) can be found in the parameter +.Dq Li $\- . +.Ic set Fl o +with no option name will list all the options and whether each is on or off; +.Ic set +o +will print the long names of all options that are currently on. +In a future version, +.Ic set +o +will behave +.Tn POSIX +compliant and print commands to restore the current options instead. +.Pp +Remaining arguments, if any, are positional parameters and are assigned, in +order, to the positional parameters (i.e. $1, $2, etc.). +If options end with +.Dq Li \-\- +and there are no remaining arguments, all positional parameters are cleared. +If no options or arguments are given, the values of all names are printed. +For unknown historical reasons, a lone +.Dq Li \- +option is treated specially \*(en it clears both the +.Fl v +and +.Fl x +options. +.Pp +.It Ic shift Op Ar number +The positional parameters +.Ar number Ns +1 , +.Ar number Ns +2 , +etc. are renamed to 1, 2, etc. +.Ar number +defaults to 1. +.Pp +.It Ic sleep Ar seconds +Suspends execution for a minimum of the +.Ar seconds +specified as positive decimal value with an optional fractional part. +Signal delivery may continue execution earlier. +.Pp +.It Ic source Ar file Op Ar arg ... +Like +.Ic \&. Po Do dot Dc Pc , +except that the current working directory is appended to the +search path (GNU +.Nm bash +extension). +.Pp +.It Ic suspend +Stops the shell as if it had received the suspend character from +the terminal. +It is not possible to suspend a login shell unless the parent process +is a member of the same terminal session but is a member of a different +process group. +As a general rule, if the shell was started by another shell or via +.Xr su 1 , +it can be suspended. +.Pp +.It Ic test Ar expression +.It Ic \&[ Ar expression Ic \&] +.Ic test +evaluates the +.Ar expression +and returns zero status if true, 1 if false, or greater than 1 if there +was an error. +It is normally used as the condition command of +.Ic if +and +.Ic while +statements. +Symbolic links are followed for all +.Ar file +expressions except +.Fl h +and +.Fl L . +.Pp +The following basic expressions are available: +.Bl -tag -width 17n +.It Fl a Ar file +.Ar file +exists. +.It Fl b Ar file +.Ar file +is a block special device. +.It Fl c Ar file +.Ar file +is a character special device. +.It Fl d Ar file +.Ar file +is a directory. +.It Fl e Ar file +.Ar file +exists. +.It Fl f Ar file +.Ar file +is a regular file. +.It Fl G Ar file +.Ar file Ns 's +group is the shell's effective group ID. +.It Fl g Ar file +.Ar file Ns 's +mode has the setgid bit set. +.It Fl H Ar file +.Ar file +is a context dependent directory (only useful on HP-UX). +.It Fl h Ar file +.Ar file +is a symbolic link. +.It Fl k Ar file +.Ar file Ns 's +mode has the +.Xr sticky 8 +bit set. +.It Fl L Ar file +.Ar file +is a symbolic link. +.It Fl O Ar file +.Ar file Ns 's +owner is the shell's effective user ID. +.It Fl p Ar file +.Ar file +is a named pipe +.Pq Tn FIFO . +.It Fl r Ar file +.Ar file +exists and is readable. +.It Fl S Ar file +.Ar file +is a +.Xr unix 4 Ns -domain +socket. +.It Fl s Ar file +.Ar file +is not empty. +.It Fl t Ar fd +File descriptor +.Ar fd +is a +.Xr tty 4 +device. +.It Fl u Ar file +.Ar file Ns 's +mode has the setuid bit set. +.It Fl w Ar file +.Ar file +exists and is writable. +.It Fl x Ar file +.Ar file +exists and is executable. +.It Ar file1 Fl nt Ar file2 +.Ar file1 +is newer than +.Ar file2 +or +.Ar file1 +exists and +.Ar file2 +does not. +.It Ar file1 Fl ot Ar file2 +.Ar file1 +is older than +.Ar file2 +or +.Ar file2 +exists and +.Ar file1 +does not. +.It Ar file1 Fl ef Ar file2 +.Ar file1 +is the same file as +.Ar file2 . +.It Ar string +.Ar string +has non-zero length. +.It Fl n Ar string +.Ar string +is not empty. +.It Fl z Ar string +.Ar string +is empty. +.It Fl v Ar name +The shell parameter +.Ar name +is set. +.It Fl o Ar option +Shell +.Ar option +is set (see the +.Ic set +command above for a list of options). +As a non-standard extension, if the option starts with a +.Ql \&! , +the test is negated; the test always fails if +.Ar option +doesn't exist (so [ \-o foo \-o \-o !foo ] returns true if and only if option +.Ar foo +exists). +The same can be achieved with [ \-o ?foo ] like in +.At +.Nm ksh93 . +.Ar option +can also be the short flag led by either +.Ql \- +or +.Ql + +.Pq no logical negation , +for example +.Dq Li \-x +or +.Dq Li +x +instead of +.Dq Li xtrace . +.It Ar string No = Ar string +Strings are equal. +.It Ar string No == Ar string +Strings are equal. +.It Ar string No \*(Gt Ar string +First string operand is greater than second string operand. +.It Ar string No \*(Lt Ar string +First string operand is less than second string operand. +.It Ar string No != Ar string +Strings are not equal. +.It Ar number Fl eq Ar number +Numbers compare equal. +.It Ar number Fl ne Ar number +Numbers compare not equal. +.It Ar number Fl ge Ar number +Numbers compare greater than or equal. +.It Ar number Fl gt Ar number +Numbers compare greater than. +.It Ar number Fl le Ar number +Numbers compare less than or equal. +.It Ar number Fl \< Ar number +Numbers compare less than. +.El +.Pp +The above basic expressions, in which unary operators have precedence over +binary operators, may be combined with the following operators (listed in +increasing order of precedence): +.Bd -literal -offset indent +expr \-o expr Logical OR. +expr \-a expr Logical AND. +! expr Logical NOT. +( expr ) Grouping. +.Ed +.Pp +Note that a number actually may be an arithmetic expression, such as +a mathematical term or the name of an integer variable: +.Bd -literal -offset indent +x=1; [ "x" \-eq 1 ] evaluates to true +.Ed +.Pp +Note that some special rules are applied (courtesy of +.Px +) if the number of arguments to +.Ic test +or inside the brackets +.Ic \&[ ... \&] +is less than five: if leading +.Dq Li \&! +arguments can be stripped such that only one to three arguments remain, +then the lowered comparison is executed; (thanks to XSI) parentheses +.Ic \e( ... \e) +lower four- and three-argument forms to two- and one-argument forms, +respectively; three-argument forms ultimately prefer binary operations, +followed by negation and parenthesis lowering; two- and four-argument forms +prefer negation followed by parenthesis; the one-argument form always implies +.Fl n . +.Pp +.Sy Note : +A common mistake is to use +.Dq Li if \&[ $foo = bar \&] +which fails if parameter +.Dq foo +is empty or unset, if it has embedded spaces (i.e.\& +.Ev IFS +octets) or if it is a unary operator like +.Dq Li \&! +or +.Dq Li \-n . +Use tests like +.Dq Li if \&[ x\&"$foo\&" = x"bar" \&] +instead, or the double-bracket operator +.Dq Li if \&[[ $foo = bar \&]] +or, to avoid pattern matching (see +.Ic \&[[ +above): +.Dq Li if \&[[ $foo = \&"$bar" \&]] +.Pp +The +.Ic \&[[ ... \&]] +construct is not only more secure to use but also often faster. +.Pp +.It Xo +.Ic time +.Op Fl p +.Op Ar pipeline +.Xc +If a +.Ar pipeline +is given, the times used to execute the pipeline are reported. +If no pipeline +is given, then the user and system time used by the shell itself, and all the +commands it has run since it was started, are reported. +The times reported are the real time (elapsed time from start to finish), +the user CPU time (time spent running in user mode), and the system CPU time +(time spent running in kernel mode). +Times are reported to standard error; the format of the output is: +.Pp +.Dl "0m0.00s real 0m0.00s user 0m0.00s system" +.Pp +If the +.Fl p +option is given the output is slightly longer: +.Bd -literal -offset indent +real 0.00 +user 0.00 +sys 0.00 +.Ed +.Pp +It is an error to specify the +.Fl p +option unless +.Ar pipeline +is a simple command. +.Pp +Simple redirections of standard error do not affect the output of the +.Ic time +command: +.Pp +.Dl $ time sleep 1 2\*(Gtafile +.Dl $ { time sleep 1; } 2\*(Gtafile +.Pp +Times for the first command do not go to +.Dq afile , +but those of the second command do. +.Pp +.It Ic times +Print the accumulated user and system times used both by the shell +and by processes that the shell started which have exited. +The format of the output is: +.Bd -literal -offset indent +0m0.00s 0m0.00s +0m0.00s 0m0.00s +.Ed +.Pp +.It Ic trap Ar n Op Ar signal ... +If the first operand is a decimal unsigned integer, this resets all +specified signals to the default action, i.e. is the same as calling +.Ic trap +with a dash +.Pq Dq Li \- +as +.Ar handler , +followed by the arguments +.Pq Ar n Op Ar signal ... , +all of which are treated as signals. +.Pp +.It Ic trap Op Ar handler signal ... +Sets a trap handler that is to be executed when any of the specified +.Ar signal Ns s +are received. +.Ar handler +is either an empty string, indicating the signals are to be ignored, a dash +.Pq Dq Li \- , +indicating that the default action is to be taken for the signals +.Pq see Xr signal 3 , +or a string containing shell commands to be executed at the first opportunity +(i.e. when the current command completes or before printing the next +.Ev PS1 +prompt) after receipt of one of the signals. +.Ar signal +is the name of a signal +.Pq e.g.\& Dv PIPE or Dv ALRM +or the number of the signal (see the +.Ic kill Fl l +command above). +.Pp +There are two special signals: +.Dv EXIT +.Pq also known as 0 , +which is executed when the shell is about to exit, and +.Dv ERR , +which is executed after an error occurs; an error is something +that would cause the shell to exit if the +.Ic set Fl e +or +.Ic set Fl o Ic errexit +option were set. +.Dv EXIT +handlers are executed in the environment of the last executed command. +.Pp +Note that, for non-interactive shells, the trap handler cannot be changed +for signals that were ignored when the shell started. +.Pp +With no arguments, the current state of the traps that have been set since +the shell started is shown as a series of +.Ic trap +commands. +Note that the output of +.Ic trap +cannot be usefully piped to another process (an artifact of the fact that +traps are cleared when subprocesses are created). +.Pp +The original Korn shell's +.Dv DEBUG +trap and the handling of +.Dv ERR +and +.Dv EXIT +traps in functions are not yet implemented. +.Pp +.It Ic true +A command that exits with a zero value. +.Pp +.It Xo +.Ic typeset +.Op Ic +\-aglpnrtUux +.Oo Fl L Ns Op Ar n +.No \*(Ba Fl R Ns Op Ar n +.No \*(Ba Fl Z Ns Op Ar n Oc +.Op Fl i Ns Op Ar n +.Oo Ar name +.Op Ns = Ns Ar value +.Ar ... Oc +.Xc +.It Xo +.Ic typeset +.Fl f Op Fl tux +.Op Ar name ... +.Xc +Display or set parameter attributes. +This is a declaration utility. +With no +.Ar name +arguments, parameter attributes are displayed; if no options are used, the +current attributes of all parameters are printed as +.Ic typeset +commands; if an option is given (or +.Dq Li \- +with no option letter), all parameters and their values with the specified +attributes are printed; if options are introduced with +.Ql + , +parameter values are not printed. +.Pp +If +.Ar name +arguments are given, the attributes of the named parameters are set +.Pq Ic \&\- +or cleared +.Pq Ic \&+ ; +inside a function, this will cause the parameters to be created +(with no value) in the local scope (but see +.Fl g ) . +Values for parameters may optionally be specified. +For +.Ar name Ns \&[*] , +the change affects all elements of the array, and no value may be specified. +.Pp +When +.Fl f +is used, +.Ic typeset +operates on the attributes of functions. +As with parameters, if no +.Ar name +arguments are given, +functions are listed with their values (i.e. definitions) unless +options are introduced with +.Ql + , +in which case only the function names are reported. +.Bl -tag -width Ds +.It Fl a +Indexed array attribute. +.It Fl f +Function mode. +Display or set functions and their attributes, instead of parameters. +.It Fl g +Do not cause named parameters to be created in +the local scope when called inside a function. +.It Fl i Ns Op Ar n +Integer attribute. +.Ar n +specifies the base to use when displaying the integer (if not specified, the +base given in the first assignment is used). +Parameters with this attribute may +be assigned values containing arithmetic expressions. +.It Fl L Ns Op Ar n +Left justify attribute. +.Ar n +specifies the field width. +If +.Ar n +is not specified, the current width of a parameter (or the width of its first +assigned value) is used. +Leading whitespace (and zeros, if used with the +.Fl Z +option) is stripped. +If necessary, values are either truncated or space padded +to fit the field width. +.It Fl l +Lower case attribute. +All upper case ASCII characters in values are converted to lower case. +(In the original Korn shell, this parameter meant +.Dq long integer +when used with the +.Fl i +option.) +.It Fl n +Create a bound variable (name reference): any access to the variable +.Ar name +will access the variable +.Ar value +in the current scope (this is different from +.At +.Nm ksh93 ! ) +instead. +Also different from +.At +.Nm ksh93 +is that +.Ar value +is lazily evaluated at the time +.Ar name +is accessed. +This can be used by functions to access variables whose names are +passed as parameters, instead of using +.Ic eval . +.It Fl p +Print complete +.Ic typeset +commands that can be used to re-create the attributes and values of +parameters. +.It Fl R Ns Op Ar n +Right justify attribute. +.Ar n +specifies the field width. +If +.Ar n +is not specified, the current width of a parameter (or the width of its first +assigned value) is used. +Trailing whitespace is stripped. +If necessary, values are either stripped of leading characters or space +padded to make them fit the field width. +.It Fl r +Read-only attribute. +Parameters with this attribute may not be assigned to or unset. +Once this attribute is set, it cannot be turned off. +.It Fl t +Tag attribute. +Has no meaning to the shell; provided for application use. +.Pp +For functions, +.Fl t +is the trace attribute. +When functions with the trace attribute are executed, the +.Ic xtrace +.Pq Fl x +shell option is temporarily turned on. +.It Fl U +Unsigned integer attribute. +Integers are printed as unsigned values (combine with the +.Fl i +option). +This option is not in the original Korn shell. +.It Fl u +Upper case attribute. +All lower case ASCII characters in values are converted to upper case. +(In the original Korn shell, this parameter meant +.Dq unsigned integer +when used with the +.Fl i +option which meant upper case letters would never be used for bases greater +than 10. +See the +.Fl U +option.) +.Pp +For functions, +.Fl u +is the undefined attribute. +See +.Sx Functions +above for the implications of this. +.It Fl x +Export attribute. +Parameters (or functions) are placed in the environment of +any executed commands. +Exported functions are not yet implemented. +.It Fl Z Ns Op Ar n +Zero fill attribute. +If not combined with +.Fl L , +this is the same as +.Fl R , +except zero padding is used instead of space padding. +For integers, the number instead of the base is padded. +.El +.Pp +If any of the +.\" long integer , +.Fl i , +.Fl L , +.Fl l , +.Fl R , +.Fl U , +.Fl u +or +.Fl Z +options are changed, all others from this set are cleared, +unless they are also given on the same command line. +.Pp +.It Xo +.Ic ulimit +.Op Fl aBCcdefHilMmnOPpqrSsTtVvw +.Op Ar value +.Xc +Display or set process limits. +If no options are used, the file size limit +.Pq Fl f +is assumed. +.Ar value , +if specified, may be either an arithmetic expression or the word +.Dq unlimited . +The limits affect the shell and any processes created by the shell after a +limit is imposed. +Note that some systems may not allow limits to be increased +once they are set. +Also note that the types of limits available are system +dependent \*(en some systems have only the +.Fl f +limit, or not even that, or can set only the soft limits +.Bl -tag -width 5n +.It Fl a +Display all limits; unless +.Fl H +is used, soft limits are displayed. +.It Fl B Ar n +Set the socket buffer size to +.Ar n +kibibytes. +.It Fl C Ar n +Set the number of cached threads to +.Ar n . +.It Fl c Ar n +Impose a size limit of +.Ar n +blocks on the size of core dumps. +.It Fl d Ar n +Impose a size limit of +.Ar n +kibibytes on the size of the data area. +.It Fl e Ar n +Set the maximum niceness to +.Ar n . +.It Fl f Ar n +Impose a size limit of +.Ar n +blocks on files written by the shell and its child processes (files of any +size may be read). +.It Fl H +Set the hard limit only (the default is to set both hard and soft limits). +.It Fl i Ar n +Set the number of pending signals to +.Ar n . +.It Fl l Ar n +Impose a limit of +.Ar n +kibibytes on the amount of locked (wired) physical memory. +.It Fl M Ar n +Set the AIO locked memory to +.Ar n +kibibytes. +.It Fl m Ar n +Impose a limit of +.Ar n +kibibytes on the amount of physical memory used. +.It Fl n Ar n +Impose a limit of +.Ar n +file descriptors that can be open at once. +.It Fl O Ar n +Set the number of AIO operations to +.Ar n . +.It Fl P Ar n +Limit the number of threads per process to +.Ar n . +.It Fl p Ar n +Impose a limit of +.Ar n +processes that can be run by the user at any one time. +.It Fl q Ar n +Limit the size of +.Tn POSIX +message queues to +.Ar n +bytes. +.It Fl r Ar n +Set the maximum real-time priority to +.Ar n . +.It Fl S +Set the soft limit only (the default is to set both hard and soft limits). +.It Fl s Ar n +Impose a size limit of +.Ar n +kibibytes on the size of the stack area. +.It Fl T Ar n +Impose a time limit of +.Ar n +real seconds to be used by each process. +.It Fl t Ar n +Impose a time limit of +.Ar n +CPU seconds spent in user mode to be used by each process. +.It Fl V Ar n +Set the number of vnode monitors on Haiku to +.Ar n . +.It Fl v Ar n +Impose a limit of +.Ar n +kibibytes on the amount of virtual memory (address space) used. +.It Fl w Ar n +Impose a limit of +.Ar n +kibibytes on the amount of swap space used. +.El +.Pp +As far as +.Ic ulimit +is concerned, a block is 512 bytes. +.Pp +.It Xo +.Ic umask +.Op Fl S +.Op Ar mask +.Xc +Display or set the file permission creation mask or umask (see +.Xr umask 2 ) . +If the +.Fl S +option is used, the mask displayed or set is symbolic; otherwise, it is an +octal number. +.Pp +Symbolic masks are like those used by +.Xr chmod 1 . +When used, they describe what permissions may be made available (as opposed to +octal masks in which a set bit means the corresponding bit is to be cleared). +For example, +.Dq Li ug=rwx,o= +sets the mask so files will not be readable, writable or executable by +.Dq others , +and is equivalent (on most systems) to the octal mask +.Dq Li 007 . +.Pp +.It Xo +.Ic unalias +.Op Fl adt +.Op Ar name ... +.Xc +The aliases for the given names are removed. +If the +.Fl a +option is used, all aliases are removed. +If the +.Fl t +or +.Fl d +options are used, the indicated operations are carried out on tracked or +directory aliases, respectively. +.Pp +.It Xo +.Ic unset +.Op Fl fv +.Ar parameter ... +.Xc +Unset the named parameters +.Po +.Fl v , +the default +.Pc +or functions +.Pq Fl f . +With +.Ar parameter Ns \&[*] , +attributes are kept, only values are unset. +.Pp +The exit status is non-zero if any of the parameters have the read-only +attribute set, zero otherwise. +.Pp +.It Ic wait Op Ar job ... +Wait for the specified job(s) to finish. +The exit status of +.Ic wait +is that of the last specified job; if the last job is killed by a signal, the +exit status is 128 + the number of the signal (see +.Ic kill Fl l Ar exit-status +above); if the last specified job can't be found (because it never existed or +had already finished), the exit status of +.Ic wait +is 127. +See +.Sx Job control +below for the format of +.Ar job . +.Ic wait +will return if a signal for which a trap has been set is received or if a +.Dv SIGHUP , +.Dv SIGINT +or +.Dv SIGQUIT +signal is received. +.Pp +If no jobs are specified, +.Ic wait +waits for all currently running jobs (if any) to finish and exits with a zero +status. +If job monitoring is enabled, the completion status of jobs is printed +(this is not the case when jobs are explicitly specified). +.Pp +.It Xo +.Ic whence +.Op Fl pv +.Op Ar name ... +.Xc +Without the +.Fl v +option, it is the same as +.Ic command Fl v , +except aliases are not printed as alias command. +With the +.Fl v +option, it is exactly the same as +.Ic command Fl V . +In either case, the +.Fl p +option differs: the search path is not affected in +.Ic whence , +but the search is restricted to the path. +.El +.Ss Job control +Job control refers to the shell's ability to monitor and control jobs which +are processes or groups of processes created for commands or pipelines. +At a minimum, the shell keeps track of the status of the background (i.e.\& +asynchronous) jobs that currently exist; this information can be displayed +using the +.Ic jobs +commands. +If job control is fully enabled (using +.Ic set Fl m +or +.Ic set Fl o Ic monitor ) , +as it is for interactive shells, the processes of a job are placed in their +own process group. +Foreground jobs can be stopped by typing the suspend +character from the terminal (normally \*(haZ), jobs can be restarted in either the +foreground or background using the +.Ic fg +and +.Ic bg +commands, and the state of the terminal is saved or restored when a foreground +job is stopped or restarted, respectively. +.Pp +Note that only commands that create processes (e.g. asynchronous commands, +subshell commands and non-built-in, non-function commands) can be stopped; +commands like +.Ic read +cannot be. +.Pp +When a job is created, it is assigned a job number. +For interactive shells, this number is printed inside +.Dq Li \&[...] , +followed by the process IDs of the processes in the job when an asynchronous +command is run. +A job may be referred to in the +.Ic bg , +.Ic fg , +.Ic jobs , +.Ic kill +and +.Ic wait +commands either by the process ID of the last process in the command pipeline +(as stored in the +.Ic \&$! +parameter) or by prefixing the job number with a percent sign +.Pq Ql % . +Other percent sequences can also be used to refer to jobs: +.Bl -tag -width "%+ x %% x %XX" +.It %+ \*(Ba %% \*(Ba % +The most recently stopped job or, if there are no stopped jobs, the oldest +running job. +.It %\- +The job that would be the +.Ic %+ +job if the latter did not exist. +.It % Ns Ar n +The job with job number +.Ar n . +.It %? Ns Ar string +The job with its command containing the string +.Ar string +(an error occurs if multiple jobs are matched). +.It % Ns Ar string +The job with its command starting with the string +.Ar string +(an error occurs if multiple jobs are matched). +.El +.Pp +When a job changes state (e.g. a background job finishes or foreground job is +stopped), the shell prints the following status information: +.Pp +.D1 [ Ns Ar number ] Ar flag status command +.Pp +where... +.Bl -tag -width "command" +.It Ar number +is the job number of the job; +.It Ar flag +is the +.Ql + +or +.Ql \- +character if the job is the +.Ic %+ +or +.Ic %\- +job, respectively, or space if it is neither; +.It Ar status +indicates the current state of the job and can be: +.Bl -tag -width "RunningXX" +.It Done Op Ar number +The job exited. +.Ar number +is the exit status of the job which is omitted if the status is zero. +.It Running +The job has neither stopped nor exited (note that running does not necessarily +mean consuming CPU time \*(en +the process could be blocked waiting for some event). +.It Stopped Op Ar signal +The job was stopped by the indicated +.Ar signal +(if no signal is given, the job was stopped by +.Dv SIGTSTP ) . +.It Ar signal-description Op Dq core dumped +The job was killed by a signal (e.g. memory fault, hangup); use +.Ic kill Fl l +for a list of signal descriptions. +The +.Dq Li core dumped +message indicates the process created a core file. +.El +.It Ar command +is the command that created the process. +If there are multiple processes in +the job, each process will have a line showing its +.Ar command +and possibly its +.Ar status , +if it is different from the status of the previous process. +.El +.Pp +When an attempt is made to exit the shell while there are jobs in the stopped +state, the shell warns the user that there are stopped jobs and does not exit. +If another attempt is immediately made to exit the shell, the stopped jobs are +sent a +.Dv SIGHUP +signal and the shell exits. +Similarly, if the +.Ic nohup +option is not set and there are running jobs when an attempt is made to exit +a login shell, the shell warns the user and does not exit. +If another attempt +is immediately made to exit the shell, the running jobs are sent a +.Dv SIGHUP +signal and the shell exits. +.Ss POSIX mode +Entering +.Ic set Fl o Ic posix +mode will cause +.Nm +to behave even more +.Tn POSIX +compliant in places where the defaults or opinions differ. +Note that +.Nm mksh +will still operate with unsigned 32-bit arithmetic; use +.Nm lksh +if arithmetic on the host +.Vt long +data type, complete with ISO C Undefined Behaviour, is required; +refer to the +.Xr lksh 1 +manual page for details. +Most other historic, +.At +.Nm ksh Ns -compatible +or opinionated differences can be disabled by using this mode; these are: +.Bl -bullet +.It +The incompatible GNU +.Nm bash +I/O redirection +.Ic &\*(Gt Ns Ar file +is not supported. +.It +File descriptors created by I/O redirections are inherited by +child processes. +.It +Numbers with a leading digit zero are interpreted as octal. +.It +The +.Nm echo +builtin does not interpret backslashes and only supports the exact option +.Fl n . +.It +Alias expansion with a trailing space only reruns on command words. +.It +Tilde expansion follows POSIX instead of Korn shell rules. +.It +The exit status of +.Ic fg +is always 0. +.It +.Ic kill +.Fl l +only lists signal names, all in one line. +.It +.Ic getopts +does not accept options with a leading +.Ql + . +.It +.Ic exec +skips builtins, functions and other commands and uses a +.Ev PATH +search to determine the utility to execute. +.El +.Ss SH mode +Compatibility mode; intended for use with legacy scripts that +cannot easily be fixed; the changes are as follows: +.Bl -bullet +.It +The incompatible GNU +.Nm bash +I/O redirection +.Ic &\*(Gt Ns Ar file +is not supported. +.It +File descriptors created by I/O redirections are inherited by +child processes. +.It +The +.Nm echo +builtin does not interpret backslashes and only supports the exact option +.Fl n , +unless built with +.Ev \-DMKSH_MIDNIGHTBSD01ASH_COMPAT . +.It +The substitution operations +.Sm off +.Xo +.Pf ${ Ar x +.Pf # Ar pat No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf ## Ar pat No } , +.Sm on +.Xc +.Sm off +.Xo +.Pf ${ Ar x +.Pf % Ar pat No } , +.Sm on +.Xc +and +.Sm off +.Xo +.Pf ${ Ar x +.Pf %% Ar pat No } +.Sm on +.Xc +wrongly do not require a parenthesis to be escaped and do not parse extglobs. +.It +The getopt construct from +.Xr lksh 1 +passes through the errorlevel. +.It +.Nm sh +.Fl c +eats a leading +.Fl \- +if built with +.Ev \-DMKSH_MIDNIGHTBSD01ASH_COMPAT . +.El +.Ss Interactive input line editing +The shell supports three modes of reading command lines from a +.Xr tty 4 +in an interactive session, controlled by the +.Ic emacs , +.Ic gmacs +and +.Ic vi +options (at most one of these can be set at once). +The default is +.Ic emacs . +Editing modes can be set explicitly using the +.Ic set +built-in. +If none of these options are enabled, +the shell simply reads lines using the normal +.Xr tty 4 +driver. +If the +.Ic emacs +or +.Ic gmacs +option is set, the shell allows emacs-like editing of the command; similarly, +if the +.Ic vi +option is set, the shell allows vi-like editing of the command. +These modes are described in detail in the following sections. +.Pp +In these editing modes, if a line is longer than the screen width (see the +.Ev COLUMNS +parameter), +a +.Ql \*(Gt , +.Ql + +or +.Ql \*(Lt +character is displayed in the last column indicating that there are more +characters after, before and after, or before the current position, +respectively. +The line is scrolled horizontally as necessary. +.Pp +Completed lines are pushed into the history, unless they begin with an +IFS octet or IFS white space or are the same as the previous line. +.Ss Emacs editing mode +When the +.Ic emacs +option is set, interactive input line editing is enabled. +Warning: This mode is +slightly different from the emacs mode in the original Korn shell. +In this mode, various editing commands +(typically bound to one or more control characters) cause immediate actions +without waiting for a newline. +Several editing commands are bound to particular +control characters when the shell is invoked; these bindings can be changed +using the +.Ic bind +command. +.Pp +The following is a list of available editing commands. +Each description starts with the name of the command, +suffixed with a colon; +an +.Op Ar n +(if the command can be prefixed with a count); and any keys the command is +bound to by default, written using caret notation +e.g. the ASCII ESC character is written as \*(ha[. +These control sequences are not case sensitive. +A count prefix for a command is entered using the sequence +.Pf \*(ha[ Ns Ar n , +where +.Ar n +is a sequence of 1 or more digits. +Unless otherwise specified, if a count is +omitted, it defaults to 1. +.Pp +Note that editing command names are used only with the +.Ic bind +command. +Furthermore, many editing commands are useful only on terminals with +a visible cursor. +The user's +.Xr tty 4 +characters (e.g.\& +.Dv ERASE ) +are bound to +reasonable substitutes and override the default bindings; +their customary values are shown in parentheses below. +The default bindings were chosen to resemble corresponding +Emacs key bindings: +.Bl -tag -width Ds +.It Xo abort: +.No INTR Pq \*(haC , +.No \*(haG +.Xc +Abort the current command, save it to the history, empty the line buffer and +set the exit state to interrupted. +.It auto\-insert: Op Ar n +Simply causes the character to appear as literal input. +Most ordinary characters are bound to this. +.It Xo backward\-char: +.Op Ar n +.No \*(haB , \*(haXD , ANSI-CurLeft , PC-CurLeft +.Xc +Moves the cursor backward +.Ar n +characters. +.It Xo backward\-word: +.Op Ar n +.No \*(ha[b , ANSI-Ctrl-CurLeft , ANSI-Alt-CurLeft +.Xc +Moves the cursor backward to the beginning of the word; words consist of +alphanumerics, underscore +.Pq Ql _ +and dollar sign +.Pq Ql $ +characters. +.It beginning\-of\-history: \*(ha[\*(Lt +Moves to the beginning of the history. +.It beginning\-of\-line: \*(haA, ANSI-Home, PC-Home +Moves the cursor to the beginning of the edited input line. +.It Xo capitalise\-word: +.Op Ar n +.No \*(ha[C , \*(ha[c +.Xc +Uppercase the first ASCII character in the next +.Ar n +words, leaving the cursor past the end of the last word. +.It clear\-screen: \*(ha[\*(haL +Prints a compile-time configurable sequence to clear the screen and home +the cursor, redraws the last line of the prompt string and the currently +edited input line. +The default sequence works for almost all standard terminals. +.It comment: \*(ha[# +If the current line does not begin with a comment character, one is added at +the beginning of the line and the line is entered (as if return had been +pressed); otherwise, the existing comment characters are removed and the cursor +is placed at the beginning of the line. +.It complete: \*(ha[\*(ha[ +Automatically completes as much as is unique of the command name or the file +name containing the cursor. +If the entire remaining command or file name is +unique, a space is printed after its completion, unless it is a directory name +in which case +.Ql / +is appended. +If there is no command or file name with the current partial word +as its prefix, a bell character is output (usually causing a beep to be +sounded). +.It complete\-command: \*(haX\*(ha[ +Automatically completes as much as is unique of the command name having the +partial word up to the cursor as its prefix, as in the +.Ic complete +command above. +.It complete\-file: \*(ha[\*(haX +Automatically completes as much as is unique of the file name having the +partial word up to the cursor as its prefix, as in the +.Ic complete +command described above. +.It complete\-list: \*(haI, \*(ha[= +Complete as much as is possible of the current word +and list the possible completions for it. +If only one completion is possible, +match as in the +.Ic complete +command above. +Note that \*(haI is usually generated by the TAB (tabulator) key. +.It Xo delete\-char\-backward: +.Op Ar n +.No ERASE Pq \*(haH , +.No \*(ha? , \*(haH +.Xc +Deletes +.Ar n +characters before the cursor. +.It Xo delete\-char\-forward: +.Op Ar n +.No ANSI-Del , PC-Del +.Xc +Deletes +.Ar n +characters after the cursor. +.It Xo delete\-word\-backward: +.Op Ar n +.No Pfx1+ERASE Pq \*(ha[\*(haH , +.No WERASE Pq \*(haW , +.No \*(ha[\*(ha? , \*(ha[\*(haH , \*(ha[h +.Xc +Deletes +.Ar n +words before the cursor. +.It Xo delete\-word\-forward: +.Op Ar n +.No \*(ha[d +.Xc +Deletes characters after the cursor up to the end of +.Ar n +words. +.It Xo down\-history: +.Op Ar n +.No \*(haN , \*(haXB , ANSI-CurDown , PC-CurDown +.Xc +Scrolls the history buffer forward +.Ar n +lines (later). +Each input line originally starts just after the last entry +in the history buffer, so +.Ic down\-history +is not useful until either +.Ic search\-history , +.Ic search\-history\-up +or +.Ic up\-history +has been performed. +.It Xo downcase\-word: +.Op Ar n +.No \*(ha[L , \*(ha[l +.Xc +Lowercases the next +.Ar n +words. +.It Xo edit\-line: +.Op Ar n +.No \*(haXe +.Xc +Edit line +.Ar n +or the current line, if not specified, interactively. +The actual command executed is +.Ic fc \-e ${VISUAL:\-${EDITOR:\-vi}} Ar n . +.It end\-of\-history: \*(ha[\*(Gt +Moves to the end of the history. +.It end\-of\-line: \*(haE, ANSI-End, PC-End +Moves the cursor to the end of the input line. +.It eot: \*(ha_ +Acts as an end-of-file; this is useful because edit-mode input disables +normal terminal input canonicalisation. +.It Xo eot\-or\-delete: +.Op Ar n +.No EOF Pq \*(haD +.Xc +If alone on a line, same as +.Ic eot , +otherwise, +.Ic delete\-char\-forward . +.It error: (not bound) +Error (ring the bell). +.It evaluate\-region: \*(ha[\*(haE +Evaluates the text between the mark and the cursor position +.Pq the entire line if no mark is set +as function substitution (if it cannot be parsed, the editing state is +unchanged and the bell is rung to signal an error); $? is updated accordingly. +.It exchange\-point\-and\-mark: \*(haX\*(haX +Places the cursor where the mark is and sets the mark to where the cursor was. +.It expand\-file: \*(ha[* +Appends a +.Ql * +to the current word and replaces the word with the result of performing file +globbing on the word. +If no files match the pattern, the bell is rung. +.It Xo forward\-char: +.Op Ar n +.No \*(haF , \*(haXC , ANSI-CurRight , PC-CurRight +.Xc +Moves the cursor forward +.Ar n +characters. +.It Xo forward\-word: +.Op Ar n +.No \*(ha[f , ANSI-Ctrl-CurRight , ANSI-Alt-CurRight +.Xc +Moves the cursor forward to the end of the +.Ar n Ns th +word. +.It Xo goto\-history: +.Op Ar n +.No \*(ha[g +.Xc +Goes to history number +.Ar n . +.It Xo kill\-line: +.No KILL Pq \*(haU +.Xc +Deletes the entire input line. +.It kill\-region: \*(haW +Deletes the input between the cursor and the mark. +.It Xo kill\-to\-eol: +.Op Ar n +.No \*(haK +.Xc +Deletes the input from the cursor to the end of the line if +.Ar n +is not specified; otherwise deletes characters between the cursor and column +.Ar n . +.It list: \*(ha[? +Prints a sorted, columnated list of command names or file names (if any) that +can complete the partial word containing the cursor. +Directory names have +.Ql / +appended to them. +.It list\-command: \*(haX? +Prints a sorted, columnated list of command names (if any) that can complete +the partial word containing the cursor. +.It list\-file: \*(haX\*(haY +Prints a sorted, columnated list of file names (if any) that can complete the +partial word containing the cursor. +File type indicators are appended as described under +.Ic list +above. +.It newline: \*(haJ , \*(haM +Causes the current input line to be processed by the shell. +The current cursor position may be anywhere on the line. +.It newline\-and\-next: \*(haO +Causes the current input line to be processed by the shell, and the next line +from history becomes the current line. +This is only useful after an +.Ic up\-history , +.Ic search\-history +or +.Ic search\-history\-up . +.It Xo no\-op: +.No QUIT Pq \*(ha\e +.Xc +This does nothing. +.It prefix\-1: \*(ha[ +Introduces a 2-character command sequence. +.It prefix\-2: \*(haX , \*(ha[[ , \*(ha[O +Introduces a multi-character command sequence. +.It Xo prev\-hist\-word: +.Op Ar n +.No \*(ha[. , \*(ha[_ +.Xc +The last word or, if given, the +.Ar n Ns th +word (zero-based) of the previous (on repeated execution, second-last, +third-last, etc.) command is inserted at the cursor. +Use of this editing command trashes the mark. +.It quote: \*(ha\*(ha , \*(haV +The following character is taken literally rather than as an editing command. +.It redraw: \*(haL +Reprints the last line of the prompt string and the current input line +on a new line. +.It Xo search\-character\-backward: +.Op Ar n +.No \*(ha[\*(ha] +.Xc +Search backward in the current line for the +.Ar n Ns th +occurrence of the next character typed. +.It Xo search\-character\-forward: +.Op Ar n +.No \*(ha] +.Xc +Search forward in the current line for the +.Ar n Ns th +occurrence of the next character typed. +.It search\-history: \*(haR +Enter incremental search mode. +The internal history list is searched +backwards for commands matching the input. +An initial +.Ql \*(ha +in the search string anchors the search. +The escape key will leave search mode. +Other commands, including sequences of escape as +.Ic prefix\-1 +followed by a +.Ic prefix\-1 +or +.Ic prefix\-2 +key will be executed after leaving search mode. +The +.Ic abort Pq \*(haG +command will restore the input line before search started. +Successive +.Ic search\-history +commands continue searching backward to the next previous occurrence of the +pattern. +The history buffer retains only a finite number of lines; the oldest +are discarded as necessary. +.It search\-history\-up: ANSI-PgUp, PC-PgUp +Search backwards through the history buffer for commands whose beginning match +the portion of the input line before the cursor. +When used on an empty line, this has the same effect as +.Ic up\-history . +.It search\-history\-down: ANSI-PgDn, PC-PgDn +Search forwards through the history buffer for commands whose beginning match +the portion of the input line before the cursor. +When used on an empty line, this has the same effect as +.Ic down\-history . +This is only useful after an +.Ic up\-history , +.Ic search\-history +or +.Ic search\-history\-up . +.It set\-mark\-command: \*(ha[ Ns Aq space +Set the mark at the cursor position. +.It transpose\-chars: \*(haT +If at the end of line or, if the +.Ic gmacs +option is set, this exchanges the two previous characters; otherwise, it +exchanges the previous and current characters and moves the cursor one +character to the right. +.It Xo up\-history: +.Op Ar n +.No \*(haP , \*(haXA , ANSI-CurUp , PC-CurUp +.Xc +Scrolls the history buffer backward +.Ar n +lines (earlier). +.It Xo upcase\-word: +.Op Ar n +.No \*(ha[U , \*(ha[u +.Xc +Uppercase the next +.Ar n +words. +.It version: \*(ha[\*(haV +Display the version of +.Nm mksh . +The current edit buffer is restored as soon as a key is pressed. +The restoring keypress is processed, unless it is a space. +.It yank: \*(haY +Inserts the most recently killed text string at the current cursor position. +.It yank\-pop: \*(ha[y +Immediately after a +.Ic yank , +replaces the inserted text string with the next previously killed text string. +.El +.Pp +The tab completion escapes characters the same way as the following code: +.Bd -literal +print \-nr \-\- "${x@/[\e"\-\e$\e&\-*:\-?[\e\e\e\`{\-\e}${IFS\-$\*(aq \et\en\*(aq}]/\e\e$KSH_MATCH}" +.Ed +.Ss Vi editing mode +.Em Note: +The vi command-line editing mode is orphaned, yet still functional. +It is 8-bit clean but specifically does not support UTF-8 or MBCS. +.Pp +The vi command-line editor in +.Nm +has basically the same commands as the +.Xr vi 1 +editor with the following exceptions: +.Bl -bullet +.It +You start out in insert mode. +.It +There are file name and command completion commands: +=, \e, *, \*(haX, \*(haE, \*(haF and, optionally, +.Aq tab +and +.Aq esc . +.It +The +.Ic _ +command is different (in +.Nm mksh , +it is the last argument command; in +.Xr vi 1 +it goes to the start of the current line). +.It +The +.Ic / +and +.Ic G +commands move in the opposite direction to the +.Ic j +command. +.It +Commands which don't make sense in a single line editor are not available +(e.g. screen movement commands and +.Xr ex 1 Ns -style +colon +.Pq Ic \&: +commands). +.El +.Pp +Like +.Xr vi 1 , +there are two modes: +.Dq insert +mode and +.Dq command +mode. +In insert mode, most characters are simply put in the buffer at the +current cursor position as they are typed; however, some characters are +treated specially. +In particular, the following characters are taken from current +.Xr tty 4 +settings +(see +.Xr stty 1 ) +and have their usual meaning (normal values are in parentheses): kill (\*(haU), +erase (\*(ha?), werase (\*(haW), eof (\*(haD), intr (\*(haC) and quit (\*(ha\e). +In addition to +the above, the following characters are also treated specially in insert mode: +.Bl -tag -width XJXXXXM +.It \*(haE +Command and file name enumeration (see below). +.It \*(haF +Command and file name completion (see below). +If used twice in a row, the +list of possible completions is displayed; if used a third time, the completion +is undone. +.It \*(haH +Erases previous character. +.It \*(haJ \*(Ba \*(haM +End of line. +The current line is read, parsed and executed by the shell. +.It \*(haV +Literal next. +The next character typed is not treated specially (can be used +to insert the characters being described here). +.It \*(haX +Command and file name expansion (see below). +.It Aq esc +Puts the editor in command mode (see below). +.It Aq tab +Optional file name and command completion (see +.Ic \*(haF +above), enabled with +.Ic set Fl o Ic vi\-tabcomplete . +.El +.Pp +In command mode, each character is interpreted as a command. +Characters that +don't correspond to commands, are illegal combinations of commands, or are +commands that can't be carried out, all cause beeps. +In the following command descriptions, an +.Op Ar n +indicates the command may be prefixed by a number (e.g.\& +.Ic 10l +moves right 10 characters); if no number prefix is used, +.Ar n +is assumed to be 1 unless otherwise specified. +The term +.Dq current position +refers to the position between the cursor and the character preceding the +cursor. +A +.Dq word +is a sequence of letters, digits and underscore characters or a sequence of +non-letter, non-digit, non-underscore and non-whitespace characters (e.g.\& +.Dq Li ab2*&\*(ha +contains two words) and a +.Dq big-word +is a sequence of non-whitespace characters. +.Pp +Special +.Nm +vi commands: +.Pp +The following commands are not in, or are different from, the normal vi file +editor: +.Bl -tag -width 10n +.It Xo +.Oo Ar n Oc Ns _ +.Xc +Insert a space followed by the +.Ar n Ns th +big-word from the last command in the history at the current position and enter +insert mode; if +.Ar n +is not specified, the last word is inserted. +.It # +Insert the comment character +.Pq Ql # +at the start of the current line and return the line to the shell (equivalent +to +.Ic I#\*(haJ ) . +.It Xo +.Oo Ar n Oc Ns g +.Xc +Like +.Ic G , +except if +.Ar n +is not specified, it goes to the most recent remembered line. +.It Xo +.Oo Ar n Oc Ns v +.Xc +Edit line +.Ar n +using the +.Xr vi 1 +editor; if +.Ar n +is not specified, the current line is edited. +The actual command executed is +.Ic fc \-e ${VISUAL:\-${EDITOR:\-vi}} Ar n . +.It * and \*(haX +Command or file name expansion is applied to the current big-word (with an +appended +.Ql * +if the word contains no file globbing characters) \*(en the big-word is replaced +with the resulting words. +If the current big-word is the first on the line +or follows one of the characters +.Ql \&; , +.Ql \*(Ba , +.Ql & , +.Ql \&( +or +.Ql \&) +and does not contain a slash +.Pq Ql / , +then command expansion is done; otherwise file name expansion is done. +Command expansion will match the big-word against all aliases, functions and +built-in commands as well as any executable files found by searching the +directories in the +.Ev PATH +parameter. +File name expansion matches the big-word against the files in the +current directory. +After expansion, the cursor is placed just past the last +word and the editor is in insert mode. +.It Xo +.Oo Ar n Oc Ns \e , +.Oo Ar n Oc Ns \*(haF , +.Oo Ar n Oc Ns Aq tab , +.No and +.Oo Ar n Oc Ns Aq esc +.Xc +Command/file name completion. +Replace the current big-word with the +longest unique match obtained after performing command and file name expansion. +.Aq tab +is only recognised if the +.Ic vi\-tabcomplete +option is set, while +.Aq esc +is only recognised if the +.Ic vi\-esccomplete +option is set (see +.Ic set Fl o ) . +If +.Ar n +is specified, the +.Ar n Ns th +possible completion is selected (as reported by the command/file name +enumeration command). +.It = and \*(haE +Command/file name enumeration. +List all the commands or files that match the current big-word. +.It \*(haV +Display the version of +.Nm mksh . +The current edit buffer is restored as soon as a key is pressed. +The restoring keypress is ignored. +.It @ Ns Ar c +Macro expansion. +Execute the commands found in the alias +.Ar c . +.El +.Pp +Intra-line movement commands: +.Bl -tag -width Ds +.It Xo +.Oo Ar n Oc Ns h and +.Oo Ar n Oc Ns \*(haH +.Xc +Move left +.Ar n +characters. +.It Xo +.Oo Ar n Oc Ns l and +.Oo Ar n Oc Ns Aq space +.Xc +Move right +.Ar n +characters. +.It 0 +Move to column 0. +.It \*(ha +Move to the first non-whitespace character. +.It Xo +.Oo Ar n Oc Ns \*(Ba +.Xc +Move to column +.Ar n . +.It $ +Move to the last character. +.It Xo +.Oo Ar n Oc Ns b +.Xc +Move back +.Ar n +words. +.It Xo +.Oo Ar n Oc Ns B +.Xc +Move back +.Ar n +big-words. +.It Xo +.Oo Ar n Oc Ns e +.Xc +Move forward to the end of the word, +.Ar n +times. +.It Xo +.Oo Ar n Oc Ns E +.Xc +Move forward to the end of the big-word, +.Ar n +times. +.It Xo +.Oo Ar n Oc Ns w +.Xc +Move forward +.Ar n +words. +.It Xo +.Oo Ar n Oc Ns W +.Xc +Move forward +.Ar n +big-words. +.It % +Find match. +The editor looks forward for the nearest parenthesis, bracket or +brace and then moves the cursor to the matching parenthesis, bracket or brace. +.It Xo +.Oo Ar n Oc Ns f Ns Ar c +.Xc +Move forward to the +.Ar n Ns th +occurrence of the character +.Ar c . +.It Xo +.Oo Ar n Oc Ns F Ns Ar c +.Xc +Move backward to the +.Ar n Ns th +occurrence of the character +.Ar c . +.It Xo +.Oo Ar n Oc Ns t Ns Ar c +.Xc +Move forward to just before the +.Ar n Ns th +occurrence of the character +.Ar c . +.It Xo +.Oo Ar n Oc Ns T Ns Ar c +.Xc +Move backward to just before the +.Ar n Ns th +occurrence of the character +.Ar c . +.It Xo +.Oo Ar n Oc Ns \&; +.Xc +Repeats the last +.Ic f , F , t +or +.Ic T +command. +.It Xo +.Oo Ar n Oc Ns \&, +.Xc +Repeats the last +.Ic f , F , t +or +.Ic T +command, but moves in the opposite direction. +.El +.Pp +Inter-line movement commands: +.Bl -tag -width Ds +.It Xo +.Oo Ar n Oc Ns j , +.Oo Ar n Oc Ns + , +.No and +.Oo Ar n Oc Ns \*(haN +.Xc +Move to the +.Ar n Ns th +next line in the history. +.It Xo +.Oo Ar n Oc Ns k , +.Oo Ar n Oc Ns \- , +.No and +.Oo Ar n Oc Ns \*(haP +.Xc +Move to the +.Ar n Ns th +previous line in the history. +.It Xo +.Oo Ar n Oc Ns G +.Xc +Move to line +.Ar n +in the history; if +.Ar n +is not specified, the number of the first remembered line is used. +.It Xo +.Oo Ar n Oc Ns g +.Xc +Like +.Ic G , +except if +.Ar n +is not specified, it goes to the most recent remembered line. +.It Xo +.Oo Ar n Oc Ns / Ns Ar string +.Xc +Search backward through the history for the +.Ar n Ns th +line containing +.Ar string ; +if +.Ar string +starts with +.Ql \*(ha , +the remainder of the string must appear at the start of the history line for +it to match. +.It Xo +.Oo Ar n Oc Ns \&? Ns Ar string +.Xc +Same as +.Ic / , +except it searches forward through the history. +.It Xo +.Oo Ar n Oc Ns n +.Xc +Search for the +.Ar n Ns th +occurrence of the last search string; +the direction of the search is the same as the last search. +.It Xo +.Oo Ar n Oc Ns N +.Xc +Search for the +.Ar n Ns th +occurrence of the last search string; +the direction of the search is the opposite of the last search. +.It Ar ANSI-CurUp , PC-PgUp +Take the characters from the beginning of the line to the current +cursor position as search string and do a backwards history search +for lines beginning with this string; keep the cursor position. +This works only in insert mode and keeps it enabled. +.El +.Pp +Edit commands +.Bl -tag -width Ds +.It Xo +.Oo Ar n Oc Ns a +.Xc +Append text +.Ar n +times; goes into insert mode just after the current position. +The append is +only replicated if command mode is re-entered i.e.\& +.Aq esc +is used. +.It Xo +.Oo Ar n Oc Ns A +.Xc +Same as +.Ic a , +except it appends at the end of the line. +.It Xo +.Oo Ar n Oc Ns i +.Xc +Insert text +.Ar n +times; goes into insert mode at the current position. +The insertion is only +replicated if command mode is re-entered i.e.\& +.Aq esc +is used. +.It Xo +.Oo Ar n Oc Ns I +.Xc +Same as +.Ic i , +except the insertion is done just before the first non-blank character. +.It Xo +.Oo Ar n Oc Ns s +.Xc +Substitute the next +.Ar n +characters (i.e. delete the characters and go into insert mode). +.It S +Substitute whole line. +All characters from the first non-blank character to the +end of the line are deleted and insert mode is entered. +.It Xo +.Oo Ar n Oc Ns c Ns Ar move-cmd +.Xc +Change from the current position to the position resulting from +.Ar n move-cmd Ns s +(i.e. delete the indicated region and go into insert mode); if +.Ar move-cmd +is +.Ic c , +the line starting from the first non-blank character is changed. +.It C +Change from the current position to the end of the line (i.e. delete to the +end of the line and go into insert mode). +.It Xo +.Oo Ar n Oc Ns x +.Xc +Delete the next +.Ar n +characters. +.It Xo +.Oo Ar n Oc Ns X +.Xc +Delete the previous +.Ar n +characters. +.It D +Delete to the end of the line. +.It Xo +.Oo Ar n Oc Ns d Ns Ar move-cmd +.Xc +Delete from the current position to the position resulting from +.Ar n move-cmd Ns s ; +.Ar move-cmd +is a movement command (see above) or +.Ic d , +in which case the current line is deleted. +.It Xo +.Oo Ar n Oc Ns r Ns Ar c +.Xc +Replace the next +.Ar n +characters with the character +.Ar c . +.It Xo +.Oo Ar n Oc Ns R +.Xc +Replace. +Enter insert mode but overwrite existing characters instead of +inserting before existing characters. +The replacement is repeated +.Ar n +times. +.It Xo +.Oo Ar n Oc Ns \*(TI +.Xc +Change the case of the next +.Ar n +characters. +.It Xo +.Oo Ar n Oc Ns y Ns Ar move-cmd +.Xc +Yank from the current position to the position resulting from +.Ar n move-cmd Ns s +into the yank buffer; if +.Ar move-cmd +is +.Ic y , +the whole line is yanked. +.It Y +Yank from the current position to the end of the line. +.It Xo +.Oo Ar n Oc Ns p +.Xc +Paste the contents of the yank buffer just after the current position, +.Ar n +times. +.It Xo +.Oo Ar n Oc Ns P +.Xc +Same as +.Ic p , +except the buffer is pasted at the current position. +.El +.Pp +Miscellaneous vi commands +.Bl -tag -width Ds +.It \*(haJ and \*(haM +The current line is read, parsed and executed by the shell. +.It \*(haL and \*(haR +Redraw the current line. +.It Xo +.Oo Ar n Oc Ns \&. +.Xc +Redo the last edit command +.Ar n +times. +.It u +Undo the last edit command. +.It U +Undo all changes that have been made to the current line. +.It PC Home, End, Del and cursor keys +They move as expected, both in insert and command mode. +.It Ar intr No and Ar quit +The interrupt and quit terminal characters cause the current line to be +removed to the history and a new prompt to be printed. +.El +.Sh FILES +.Bl -tag -width XetcXsuid_profile -compact +.It Pa \*(TI/.mkshrc +User mkshrc profile (non-privileged interactive shells); see +.Sx Startup files. +The location can be changed at compile time (for embedded systems); +AOSP Android builds use +.Pa /system/etc/mkshrc . +.It Pa \*(TI/.profile +User profile (non-privileged login shells); see +.Sx Startup files +near the top of this manual. +.It Pa /etc/profile +System profile (login shells); see +.Sx Startup files. +.It Pa /etc/shells +Shell database. +.It Pa /etc/suid_profile +Suid profile (privileged shells); see +.Sx Startup files. +.El +.Pp +Note: On Android, +.Pa /system/etc/ +contains the system and suid profile. +.Sh SEE ALSO +.Xr awk 1 , +.Xr cat 1 , +.Xr ed 1 , +.Xr getopt 1 , +.Xr lksh 1 , +.Xr sed 1 , +.Xr sh 1 , +.Xr stty 1 , +.Xr dup 2 , +.Xr execve 2 , +.Xr getgid 2 , +.Xr getuid 2 , +.Xr mknod 2 , +.Xr mkfifo 2 , +.Xr open 2 , +.Xr pipe 2 , +.Xr rename 2 , +.Xr wait 2 , +.Xr getopt 3 , +.Xr nl_langinfo 3 , +.Xr setlocale 3 , +.Xr signal 3 , +.Xr system 3 , +.Xr tty 4 , +.Xr shells 5 , +.Xr environ 7 , +.Xr script 7 , +.Xr utf\-8 7 , +.Xr mknod 8 +.Pp +.Pa http://www.mirbsd.org/ksh\-chan.htm +.Rs +.%A Morris Bolsky +.%B "The KornShell Command and Programming Language" +.%D 1989 +.%I "Prentice Hall PTR" +.%P "xvi\ +\ 356 pages" +.%O "ISBN 978\-0\-13\-516972\-8 (0\-13\-516972\-0)" +.Re +.Rs +.%A Morris I. Bolsky +.%A David G. Korn +.%B "The New KornShell Command and Programming Language (2nd Edition)" +.%D 1995 +.%I "Prentice Hall PTR" +.%P "xvi\ +\ 400 pages" +.%O "ISBN 978\-0\-13\-182700\-4 (0\-13\-182700\-6)" +.Re +.Rs +.%A Stephen G. Kochan +.%A Patrick H. Wood +.%B "\\*(tNUNIX\\*(sP Shell Programming" +.%V "3rd Edition" +.%D 2003 +.%I "Sams" +.%P "xiii\ +\ 437 pages" +.%O "ISBN 978\-0\-672\-32490\-1 (0\-672\-32490\-3)" +.Re +.Rs +.%A "IEEE Inc." +.%T "\\*(tNIEEE\\*(sP Standard for Information Technology \*(en Portable Operating System Interface (POSIX)" +.%V "Part 2: Shell and Utilities" +.%D 1993 +.%I "IEEE Press" +.%P "xvii\ +\ 1195 pages" +.%O "ISBN 978\-1\-55937\-255\-8 (1\-55937\-255\-9)" +.Re +.Rs +.%A Bill Rosenblatt +.%B "Learning the Korn Shell" +.%D 1993 +.%I "O'Reilly" +.%P "360 pages" +.%O "ISBN 978\-1\-56592\-054\-5 (1\-56592\-054\-6)" +.Re +.Rs +.%A Bill Rosenblatt +.%A Arnold Robbins +.%B "Learning the Korn Shell, Second Edition" +.%D 2002 +.%I "O'Reilly" +.%P "432 pages" +.%O "ISBN 978\-0\-596\-00195\-7 (0\-596\-00195\-9)" +.Re +.Rs +.%A Barry Rosenberg +.%B "KornShell Programming Tutorial" +.%D 1991 +.%I "Addison-Wesley Professional" +.%P "xxi\ +\ 324 pages" +.%O "ISBN 978\-0\-201\-56324\-5 (0\-201\-56324\-X)" +.Re +.Sh AUTHORS +.An -nosplit +.Nm "The MirBSD Korn Shell" +is developed by +.An mirabilos Aq Mt m@mirbsd.org +as part of The MirOS Project. +This shell is based on the public domain 7th edition Bourne shell clone by +.An Charles Forsyth , +who kindly agreed to, in countries where the Public Domain status of the work +may not be valid, grant a copyright licence to the general public to deal in +the work without restriction and permission to sublicence derivatives under +the terms of any (OSI approved) Open Source licence, +and parts of the BRL shell by +.An Doug A. Gwyn , +.An Doug Kingston , +.An Ron Natalie , +.An Arnold Robbins , +.An Lou Salkind +and others. +The first release of +.Nm pdksh +was created by +.An Eric Gisin , +and it was subsequently maintained by +.An John R. MacMillan , +.An Simon J. Gerraty +and +.An Michael Rendell . +The effort of several projects, such as Debian and OpenBSD, and other +contributors including our users, to improve the shell is appreciated. +See the documentation, website and source code (CVS) for details. +.Pp +.Nm mksh\-os2 +is developed by +.An KO Myung-Hun Aq Mt komh@chollian.net . +.Pp +.Nm mksh\-w32 +is developed by +.An Michael Langguth Aq Mt lan@scalaris.com . +.Pp +.Nm mksh Ns / Ns Tn z/OS +is contributed by +.An Daniel Richard G. Aq Mt skunk@iSKUNK.ORG . +.Pp +The BSD daemon is Copyright \(co Marshall Kirk McKusick. +The complete legalese is at: +.Pa http://www.mirbsd.org/TaC\-mksh.txt +.\" +.\" This boils down to: feel free to use mksh.ico as application icon +.\" or shortcut for mksh or mksh/Win32 or OS/2; distro patches are ok +.\" (but we request they amend $KSH_VERSION when modifying mksh). +.\" Authors are Marshall Kirk McKusick (UCB), Rick Collette (ekkoBSD), +.\" mirabilos, Benny Siegert (MirBSD), Michael Langguth (mksh/Win32), +.\" KO Myung-Hun (mksh for OS/2). +.\" +.\" As far as MirBSD is concerned, the files themselves are free +.\" to modification and distribution under BSD/MirOS Licence, the +.\" restriction on use stems only from trademark law's requirement +.\" to protect it or lose it, which McKusick almost did. +.\" +.Sh CAVEATS +.Nm mksh +provides a consistent 32-bit integer arithmetic implementation, both +signed and unsigned, with sign of the result of a remainder operation +and wraparound defined, even (defying POSIX) on 36-bit and 64-bit systems. +.Pp +.Nm mksh +provides a consistent, clear interface normally. +This may deviate from POSIX in historic or opinionated places. +.Ic set Fl o Ic posix +(see +.Sx POSIX mode +for details) +will cause the shell to behave more conformant. +.Pp +For the purpose of +.Tn POSIX , +.Nm mksh +supports only the +.Dq C +locale. +.Nm mksh Ns 's +.Ic utf8\-mode +.Em must +be disabled in POSIX mode, and it +only supports the Unicode BMP (Basic Multilingual Plane) and maps +raw octets into the U+EF80..U+EFFF wide character range; compare +.Sx Arithmetic expressions . +The following +.Tn POSIX +.Nm sh Ns -compatible +code toggles the +.Ic utf8\-mode +option dependent on the current +.Tn POSIX +locale for mksh to allow using the UTF-8 mode, within the constraints +outlined above, in code portable across various shell implementations: +.Bd -literal -offset indent +case ${KSH_VERSION:\-} in +*MIRBSD\ KSH*\*(Ba*LEGACY\ KSH*) + case ${LC_ALL:\-${LC_CTYPE:\-${LANG:\-}}} in + *[Uu][Tt][Ff]8*\*(Ba*[Uu][Tt][Ff]\-8*) set \-U ;; + *) set +U ;; + esac ;; +esac +.Ed +In near future, (Unicode) locale tracking will be implemented though. +.Pp +See also the FAQ below. +.Sh BUGS +Suspending (using \*(haZ) pipelines like the one below will only suspend +the currently running part of the pipeline; in this example, +.Dq Li fubar +is immediately printed on suspension (but not later after an +.Ic fg ) . +.Bd -literal -offset indent +$ /bin/sleep 666 && echo fubar +.Ed +.Pp +The truncation process involved when changing +.Ev HISTFILE +does not free old history entries (leaks memory) and leaks +old entries into the new history if their line numbers are +not overwritten by same-number entries from the persistent +history file; truncating the on-disc file to +.Ev HISTSIZE +lines has always been broken and prone to history file corruption +when multiple shells are accessing the file; the rollover process +for the in-memory portion of the history is slow, should use +.Xr memmove 3 . +.Pp +This document attempts to describe +.Nm mksh\ R56 +and up, +.\" with vendor patches from insert-your-name-here, +compiled without any options impacting functionality, such as +.Dv MKSH_SMALL , +when not called as +.Pa /bin/sh +which, on some systems only, enables +.Ic set Fl o Ic posix +or +.Ic set Fl o Ic sh +automatically (whose behaviour differs across targets), +for an operating environment supporting all of its advanced needs. +.Pp +Please report bugs in +.Nm +to the +.Aq Mt miros\-mksh@mirbsd.org +mailing list +or in the +.Li \&#\&!/bin/mksh +.Pq or Li \&#ksh +IRC channel at +.Pa irc.freenode.net +.Pq Port 6697 SSL, 6667 unencrypted , +or at: +.Pa https://launchpad.net/mksh +.Sh FREQUENTLY ASKED QUESTIONS +This FAQ attempts to document some of the questions users of +.Nm +or readers of this manual page may encounter. +.Ss I'm an Android user, so what's mksh? +.Nm mksh +is a +.Ux +shell / command interpreter, similar to +.Nm COMMAND.COM +or +.Nm CMD.EXE , +which has been included with +.Tn Android Open Source Project +for a while now. +Basically, it's a program that runs in a terminal (console window), +takes user input and runs commands or scripts, which it can also +be asked to do by other programs, even in the background. +Any privilege pop-ups you might be encountering are thus not +.Nm mksh +issues but questions by some other program utilising it. +.Ss "I'm an OS/2 user, what do I need to know?" +Unlike the native command prompt, the current working directory is, +for security reasons common on Unix systems which the shell is designed for, +not in the search path at all; if you really need this, run the command +.Li PATH=.$PATHSEP$PATH +or add that to a suitable initialisation file. +.Pp +There are two different newline modes for mksh-os2: standard (Unix) mode, +in which only LF (0A hex) is supported as line separator, and "textmode", +which also accepts ASCII newlines (CR+LF), like most other tools on OS/2, +but creating an incompatibility with standard +.Nm . +If you compiled mksh from source, you will get the standard Unix mode unless +.Fl T +is added during compilation; you will most likely have gotten this shell +through komh's port on Hobbes, or from his OS/2 Factory on eComStation +Korea, which uses "textmode", though. +Most OS/2 users will want to use "textmode" unless they need absolute +compatibility with Unix +.Nm . +.Ss "How do I start mksh on a specific terminal?" +Normally: +.Dl mksh \-T/dev/tty2 +.Pp +However, if you want for it to return (e.g. for an embedded +system rescue shell), use this on your real console device instead: +.Dl mksh \-T!/dev/ttyACM0 +.Pp +.Nm +can also daemonise (send to the background): +.Dl mksh \-T\- \-c \*(aqexec cdio lock\*(aq +.Ss "POSIX says..." +Run the shell in POSIX mode (and possibly +.Nm lksh +instead of +.Nm mksh ) : +.Dl set \-o posix +.Ss "My prompt from does not work!" +Contact us on the mailing list or on IRC, we'll convert it for you. +.Ss "Something is going wrong with my while...read loop" +Most likely, you've encountered the problem in which the shell runs +all parts of a pipeline as subshell. +The inner loop will be executed in a subshell and variable changes +cannot be propagated if run in a pipeline: +.Bd -literal -offset indent +bar \*(Ba baz \*(Ba while read foo; do ...; done +.Ed +.Pp +Note that +.Ic exit +in the inner loop will only exit the subshell and not the original shell. +Likewise, if the code is inside a function, +.Ic return +in the inner loop will only exit the subshell and won't terminate the function. +.Pp +Use co-processes instead: +.Bd -literal -offset indent +bar \*(Ba baz \*(Ba& +while read \-p foo; do ...; done +exec 3\*(Gt&p; exec 3\*(Gt&\- +.Ed +.Pp +If +.Ic read +is run in a loop such as +.Ic while read foo; do ...; done +then leading whitespace will be removed (IFS) and backslashes processed. +You might want to use +.Ic while IFS= read \-r foo; do ...; done +for pristine I/O. +Similarly, when using the +.Fl a +option, use of the +.Fl r +option might be prudent +.Pq Dq Li read \-raN\-1 arr \*(Ltfile ; +the same applies for NUL-terminated lines: +.Bd -literal -offset indent +find . \-type f \-print0 \*(Ba& \e + while IFS= read \-d \*(aq\*(aq \-pr filename; do + print \-r \-\- "found \*(Lt${filename#./}\*(Gt" +done +.Ed +.Pp +.Ss "What differences in function-local scopes are there?" +.Nm +has a different scope model from +.At +.Nm ksh , +which leads to subtle differences in semantics for identical builtins. +This can cause issues with a +.Ic nameref +to suddenly point to a local variable by accident. +.Pp +.Tn GNU +.Nm bash +allows unsetting local variables; in +.Nm , +doing so in a function allows back access to the global variable +(actually the one in the next scope up) with the same name. +The following code, when run before the function definitions, changes +the behaviour of +.Ic unset +to behave like other shells (the alias can be removed after the definitions): +.Bd -literal -offset indent +case ${KSH_VERSION:\-} in +*MIRBSD\ KSH*\*(Ba*LEGACY\ KSH*) + function unset_compat { + \e\ebuiltin typeset unset_compat_x + + for unset_compat_x in "$@"; do + eval "\e\e\e\ebuiltin unset $unset_compat_x[*]" + done + } + \e\ebuiltin alias unset=unset_compat + ;; +esac +.Ed +.Pp +When a local variable is created (e.g. using +.Ic local , +.Ic typeset , +.Ic integer , +.Ic \e\ebuiltin typeset ) +it does not, like in other shells, inherit the value from the global +(next scope up) variable with the same name; it is rather created +without any value (unset but defined). +.Ss "I get an error in this regex comparison" +Use extglobs instead of regexes: +.Dl "[[ foo =~ (foo\*(Babar).*baz ]] # becomes" +.Dl "[[ foo = *@(foo\*(Babar)*baz* ]] # instead" +.Ss "Are there any extensions to avoid?" +.Tn GNU +.Nm bash +supports +.Dq Li &\*(Gt +.Pq and Dq Li \*(Ba& +to redirect both stdout and stderr in one go, but this breaks POSIX +and Korn Shell syntax; use POSIX redirections instead: +.Dl "foo \*(Ba& bar \*(Ba& baz &\*(Gtlog # GNU bash" +.Dl "foo 2\*(Gt&1 \*(Ba bar 2\*(Gt&1 \*(Ba baz \*(Gtlog 2\*(Gt&1 # POSIX" +.Ss "\*(haL (Ctrl-L) does not clear the screen" +Use \*(ha[\*(haL (Escape+Ctrl-L) or rebind it: +.Dl bind \*(aq\*(haL=clear-screen\*(aq +.Ss "\*(haU (Ctrl-U) clears the entire line" +If it should only delete the line up to the cursor, use: +.Dl bind \-m \*(haU=\*(aq\*(ha[0\*(haK\*(aq +.Ss "Cursor Up behaves differently from zsh" +Some shells make Cursor Up search in the history only for +commands starting with what was already entered. +.Nm +separates the shortcuts: Cursor Up goes up one command +and PgUp searches the history as described above. +.Ss "My question is not answered here!" +Check +.Pa http://www.mirbsd.org/mksh\-faq.htm +which contains a collection of frequently asked questions about +.Nm +in general, for packagers, etc. while these above are in user scope. diff --git a/mksh.ico b/mksh.ico new file mode 100644 index 0000000..731c2b6 Binary files /dev/null and b/mksh.ico differ diff --git a/os2.c b/os2.c new file mode 100644 index 0000000..2bc63ed --- /dev/null +++ b/os2.c @@ -0,0 +1,575 @@ +/*- + * Copyright (c) 2015, 2017 + * KO Myung-Hun + * Copyright (c) 2017 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#define INCL_DOS +#include + +#include "sh.h" + +#include +#include +#include +#include +#include + +__RCSID("$MirOS: src/bin/mksh/os2.c,v 1.8 2017/12/22 16:41:42 tg Exp $"); + +static char *remove_trailing_dots(char *); +static int access_stat_ex(int (*)(), const char *, void *); +static int test_exec_exist(const char *, char *); +static void response(int *, const char ***); +static char *make_response_file(char * const *); +static void add_temp(const char *); +static void cleanup_temps(void); +static void cleanup(void); + +#define RPUT(x) do { \ + if (new_argc >= new_alloc) { \ + new_alloc += 20; \ + if (!(new_argv = realloc(new_argv, \ + new_alloc * sizeof(char *)))) \ + goto exit_out_of_memory; \ + } \ + new_argv[new_argc++] = (x); \ +} while (/* CONSTCOND */ 0) + +#define KLIBC_ARG_RESPONSE_EXCLUDE \ + (__KLIBC_ARG_DQUOTE | __KLIBC_ARG_WILDCARD | __KLIBC_ARG_SHELL) + +static void +response(int *argcp, const char ***argvp) +{ + int i, old_argc, new_argc, new_alloc = 0; + const char **old_argv, **new_argv; + char *line, *l, *p; + FILE *f; + + old_argc = *argcp; + old_argv = *argvp; + for (i = 1; i < old_argc; ++i) + if (old_argv[i] && + !(old_argv[i][-1] & KLIBC_ARG_RESPONSE_EXCLUDE) && + old_argv[i][0] == '@') + break; + + if (i >= old_argc) + /* do nothing */ + return; + + new_argv = NULL; + new_argc = 0; + for (i = 0; i < old_argc; ++i) { + if (i == 0 || !old_argv[i] || + (old_argv[i][-1] & KLIBC_ARG_RESPONSE_EXCLUDE) || + old_argv[i][0] != '@' || + !(f = fopen(old_argv[i] + 1, "rt"))) + RPUT(old_argv[i]); + else { + long filesize; + + fseek(f, 0, SEEK_END); + filesize = ftell(f); + fseek(f, 0, SEEK_SET); + + line = malloc(filesize + /* type */ 1 + /* NUL */ 1); + if (!line) { + exit_out_of_memory: + fputs("Out of memory while reading response file\n", stderr); + exit(255); + } + + line[0] = __KLIBC_ARG_NONZERO | __KLIBC_ARG_RESPONSE; + l = line + 1; + while (fgets(l, (filesize + 1) - (l - (line + 1)), f)) { + p = strchr(l, '\n'); + if (p) { + /* + * if a line ends with a backslash, + * concatenate with the next line + */ + if (p > l && p[-1] == '\\') { + char *p1; + int count = 0; + + for (p1 = p - 1; p1 >= l && + *p1 == '\\'; p1--) + count++; + + if (count & 1) { + l = p + 1; + + continue; + } + } + + *p = 0; + } + p = strdup(line); + if (!p) + goto exit_out_of_memory; + + RPUT(p + 1); + + l = line + 1; + } + + free(line); + + if (ferror(f)) { + fputs("Cannot read response file\n", stderr); + exit(255); + } + + fclose(f); + } + } + + RPUT(NULL); + --new_argc; + + *argcp = new_argc; + *argvp = new_argv; +} + +static void +init_extlibpath(void) +{ + const char *vars[] = { + "BEGINLIBPATH", + "ENDLIBPATH", + "LIBPATHSTRICT", + NULL + }; + char val[512]; + int flag; + + for (flag = 0; vars[flag]; flag++) { + DosQueryExtLIBPATH(val, flag + 1); + if (val[0]) + setenv(vars[flag], val, 1); + } +} + +void +os2_init(int *argcp, const char ***argvp) +{ + response(argcp, argvp); + + init_extlibpath(); + + if (!isatty(STDIN_FILENO)) + setmode(STDIN_FILENO, O_BINARY); + if (!isatty(STDOUT_FILENO)) + setmode(STDOUT_FILENO, O_BINARY); + if (!isatty(STDERR_FILENO)) + setmode(STDERR_FILENO, O_BINARY); + + atexit(cleanup); +} + +void +setextlibpath(const char *name, const char *val) +{ + int flag; + char *p, *cp; + + if (!strcmp(name, "BEGINLIBPATH")) + flag = BEGIN_LIBPATH; + else if (!strcmp(name, "ENDLIBPATH")) + flag = END_LIBPATH; + else if (!strcmp(name, "LIBPATHSTRICT")) + flag = LIBPATHSTRICT; + else + return; + + /* convert slashes to backslashes */ + strdupx(cp, val, ATEMP); + for (p = cp; *p; p++) { + if (*p == '/') + *p = '\\'; + } + + DosSetExtLIBPATH(cp, flag); + + afree(cp, ATEMP); +} + +/* remove trailing dots */ +static char * +remove_trailing_dots(char *name) +{ + char *p = strnul(name); + + while (--p > name && *p == '.') + /* nothing */; + + if (*p != '.' && *p != '/' && *p != '\\' && *p != ':') + p[1] = '\0'; + + return (name); +} + +#define REMOVE_TRAILING_DOTS(name) \ + remove_trailing_dots(memcpy(alloca(strlen(name) + 1), name, strlen(name) + 1)) + +/* alias of stat() */ +extern int _std_stat(const char *, struct stat *); + +/* replacement for stat() of kLIBC which fails if there are trailing dots */ +int +stat(const char *name, struct stat *buffer) +{ + return (_std_stat(REMOVE_TRAILING_DOTS(name), buffer)); +} + +/* alias of access() */ +extern int _std_access(const char *, int); + +/* replacement for access() of kLIBC which fails if there are trailing dots */ +int +access(const char *name, int mode) +{ + /* + * On OS/2 kLIBC, X_OK is set only for executable files. + * This prevents scripts from being executed. + */ + if (mode & X_OK) + mode = (mode & ~X_OK) | R_OK; + + return (_std_access(REMOVE_TRAILING_DOTS(name), mode)); +} + +#define MAX_X_SUFFIX_LEN 4 + +static const char *x_suffix_list[] = + { "", ".ksh", ".exe", ".sh", ".cmd", ".com", ".bat", NULL }; + +/* call fn() by appending executable extensions */ +static int +access_stat_ex(int (*fn)(), const char *name, void *arg) +{ + char *x_name; + const char **x_suffix; + int rc = -1; + size_t x_namelen = strlen(name) + MAX_X_SUFFIX_LEN + 1; + + /* otherwise, try to append executable suffixes */ + x_name = alloc(x_namelen, ATEMP); + + for (x_suffix = x_suffix_list; rc && *x_suffix; x_suffix++) { + strlcpy(x_name, name, x_namelen); + strlcat(x_name, *x_suffix, x_namelen); + + rc = fn(x_name, arg); + } + + afree(x_name, ATEMP); + + return (rc); +} + +/* access()/search_access() version */ +int +access_ex(int (*fn)(const char *, int), const char *name, int mode) +{ + /*XXX this smells fishy --mirabilos */ + return (access_stat_ex(fn, name, (void *)mode)); +} + +/* stat() version */ +int +stat_ex(const char *name, struct stat *buffer) +{ + return (access_stat_ex(stat, name, buffer)); +} + +static int +test_exec_exist(const char *name, char *real_name) +{ + struct stat sb; + + if (stat(name, &sb) < 0 || !S_ISREG(sb.st_mode)) + return (-1); + + /* safe due to calculations in real_exec_name() */ + memcpy(real_name, name, strlen(name) + 1); + + return (0); +} + +const char * +real_exec_name(const char *name) +{ + char x_name[strlen(name) + MAX_X_SUFFIX_LEN + 1]; + const char *real_name = name; + + if (access_stat_ex(test_exec_exist, real_name, x_name) != -1) + /*XXX memory leak */ + strdupx(real_name, x_name, ATEMP); + + return (real_name); +} + +/* make a response file to pass a very long command line */ +static char * +make_response_file(char * const *argv) +{ + char rsp_name_arg[] = "@mksh-rsp-XXXXXX"; + char *rsp_name = &rsp_name_arg[1]; + int i; + int fd; + char *result; + + if ((fd = mkstemp(rsp_name)) == -1) + return (NULL); + + /* write all the arguments except a 0th program name */ + for (i = 1; argv[i]; i++) { + write(fd, argv[i], strlen(argv[i])); + write(fd, "\n", 1); + } + + close(fd); + add_temp(rsp_name); + strdupx(result, rsp_name_arg, ATEMP); + + return (result); +} + +/* alias of execve() */ +extern int _std_execve(const char *, char * const *, char * const *); + +/* replacement for execve() of kLIBC */ +int +execve(const char *name, char * const *argv, char * const *envp) +{ + const char *exec_name; + FILE *fp; + char sign[2]; + int pid; + int status; + int fd; + int rc; + int saved_mode; + int saved_errno; + + /* + * #! /bin/sh : append .exe + * extproc sh : search sh.exe in PATH + */ + exec_name = search_path(name, path, X_OK, NULL); + if (!exec_name) { + errno = ENOENT; + return (-1); + } + + /*- + * kLIBC execve() has problems when executing scripts. + * 1. it fails to execute a script if a directory whose name + * is same as an interpreter exists in a current directory. + * 2. it fails to execute a script not starting with sharpbang. + * 3. it fails to execute a batch file if COMSPEC is set to a shell + * incompatible with cmd.exe, such as /bin/sh. + * And ksh process scripts more well, so let ksh process scripts. + */ + errno = 0; + if (!(fp = fopen(exec_name, "rb"))) + errno = ENOEXEC; + + if (!errno && fread(sign, 1, sizeof(sign), fp) != sizeof(sign)) + errno = ENOEXEC; + + if (fp && fclose(fp)) + errno = ENOEXEC; + + if (!errno && + !((sign[0] == 'M' && sign[1] == 'Z') || + (sign[0] == 'N' && sign[1] == 'E') || + (sign[0] == 'L' && sign[1] == 'X'))) + errno = ENOEXEC; + + if (errno == ENOEXEC) + return (-1); + + /* + * Normal OS/2 programs expect that standard IOs, especially stdin, + * are opened in text mode at the startup. By the way, on OS/2 kLIBC + * child processes inherit a translation mode of a parent process. + * As a result, if stdin is set to binary mode in a parent process, + * stdin of child processes is opened in binary mode as well at the + * startup. In this case, some programs such as sed suffer from CR. + */ + saved_mode = setmode(STDIN_FILENO, O_TEXT); + + pid = spawnve(P_NOWAIT, exec_name, argv, envp); + saved_errno = errno; + + /* arguments too long? */ + if (pid == -1 && saved_errno == EINVAL) { + /* retry with a response file */ + char *rsp_name_arg = make_response_file(argv); + + if (rsp_name_arg) { + char *rsp_argv[3] = { argv[0], rsp_name_arg, NULL }; + + pid = spawnve(P_NOWAIT, exec_name, rsp_argv, envp); + saved_errno = errno; + + afree(rsp_name_arg, ATEMP); + } + } + + /* restore translation mode of stdin */ + setmode(STDIN_FILENO, saved_mode); + + if (pid == -1) { + cleanup_temps(); + + errno = saved_errno; + return (-1); + } + + /* close all opened handles */ + for (fd = 0; fd < NUFILE; fd++) { + if (fcntl(fd, F_GETFD) == -1) + continue; + + close(fd); + } + + while ((rc = waitpid(pid, &status, 0)) < 0 && errno == EINTR) + /* nothing */; + + cleanup_temps(); + + /* Is this possible? And is this right? */ + if (rc == -1) + return (-1); + + if (WIFSIGNALED(status)) + _exit(ksh_sigmask(WTERMSIG(status))); + + _exit(WEXITSTATUS(status)); +} + +static struct temp *templist = NULL; + +static void +add_temp(const char *name) +{ + struct temp *tp; + + tp = alloc(offsetof(struct temp, tffn[0]) + strlen(name) + 1, APERM); + memcpy(tp->tffn, name, strlen(name) + 1); + tp->next = templist; + templist = tp; +} + +/* alias of unlink() */ +extern int _std_unlink(const char *); + +/* + * Replacement for unlink() of kLIBC not supporting to remove files used by + * another processes. + */ +int +unlink(const char *name) +{ + int rc; + + rc = _std_unlink(name); + if (rc == -1 && errno != ENOENT) + add_temp(name); + + return (rc); +} + +static void +cleanup_temps(void) +{ + struct temp *tp; + struct temp **tpnext; + + for (tpnext = &templist, tp = templist; tp; tp = *tpnext) { + if (_std_unlink(tp->tffn) == 0 || errno == ENOENT) { + *tpnext = tp->next; + afree(tp, APERM); + } else { + tpnext = &tp->next; + } + } +} + +static void +cleanup(void) +{ + cleanup_temps(); +} + +int +getdrvwd(char **cpp, unsigned int drvltr) +{ + PBYTE cp; + ULONG sz; + APIRET rc; + ULONG drvno; + + if (DosQuerySysInfo(QSV_MAX_PATH_LENGTH, QSV_MAX_PATH_LENGTH, + &sz, sizeof(sz)) != 0) { + errno = EDOOFUS; + return (-1); + } + + /* allocate 'X:/' plus sz plus NUL */ + checkoktoadd((size_t)sz, (size_t)4); + cp = aresize(*cpp, (size_t)sz + (size_t)4, ATEMP); + cp[0] = ksh_toupper(drvltr); + cp[1] = ':'; + cp[2] = '/'; + drvno = ksh_numuc(cp[0]) + 1; + /* NUL is part of space within buffer passed */ + ++sz; + if ((rc = DosQueryCurrentDir(drvno, cp + 3, &sz)) == 0) { + /* success! */ + *cpp = cp; + return (0); + } + afree(cp, ATEMP); + *cpp = NULL; + switch (rc) { + case 15: /* invalid drive */ + errno = ENOTBLK; + break; + case 26: /* not dos disk */ + errno = ENODEV; + break; + case 108: /* drive locked */ + errno = EDEADLK; + break; + case 111: /* buffer overflow */ + errno = ENAMETOOLONG; + break; + default: + errno = EINVAL; + } + return (-1); +} diff --git a/rlimits.opt b/rlimits.opt new file mode 100644 index 0000000..4f933ab --- /dev/null +++ b/rlimits.opt @@ -0,0 +1,105 @@ +/*- + * Copyright (c) 2013, 2015 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +@RLIMITS_DEFNS +__RCSID("$MirOS: src/bin/mksh/rlimits.opt,v 1.3 2015/12/12 21:08:44 tg Exp $"); +@RLIMITS_ITEMS +#define FN(lname,lid,lfac,lopt) (const struct limits *)(&rlimits_ ## lid), +@@ + +/* generic options for the ulimit builtin */ + +t|RLIMIT_CPU +FN("time(cpu-seconds)", RLIMIT_CPU, 1 + +>f|RLIMIT_FSIZE +FN("file(blocks)", RLIMIT_FSIZE, 512 + +>c|RLIMIT_CORE +FN("coredump(blocks)", RLIMIT_CORE, 512 + +>d|RLIMIT_DATA +FN("data(KiB)", RLIMIT_DATA, 1024 + +>s|RLIMIT_STACK +FN("stack(KiB)", RLIMIT_STACK, 1024 + +>l|RLIMIT_MEMLOCK +FN("lockedmem(KiB)", RLIMIT_MEMLOCK, 1024 + +>n|RLIMIT_NOFILE +FN("nofiles(descriptors)", RLIMIT_NOFILE, 1 + +>p|RLIMIT_NPROC +FN("processes", RLIMIT_NPROC, 1 + +>w|RLIMIT_SWAP +FN("swap(KiB)", RLIMIT_SWAP, 1024 + +>T|RLIMIT_TIME +FN("humantime(seconds)", RLIMIT_TIME, 1 + +>V|RLIMIT_NOVMON +FN("vnodemonitors", RLIMIT_NOVMON, 1 + +>i|RLIMIT_SIGPENDING +FN("sigpending", RLIMIT_SIGPENDING, 1 + +>q|RLIMIT_MSGQUEUE +FN("msgqueue(bytes)", RLIMIT_MSGQUEUE, 1 + +>M|RLIMIT_AIO_MEM +FN("AIOlockedmem(KiB)", RLIMIT_AIO_MEM, 1024 + +>O|RLIMIT_AIO_OPS +FN("AIOoperations", RLIMIT_AIO_OPS, 1 + +>C|RLIMIT_TCACHE +FN("cachedthreads", RLIMIT_TCACHE, 1 + +>B|RLIMIT_SBSIZE +FN("sockbufsiz(KiB)", RLIMIT_SBSIZE, 1024 + +>P|RLIMIT_PTHREAD +FN("threadsperprocess", RLIMIT_PTHREAD, 1 + +>e|RLIMIT_NICE +FN("maxnice", RLIMIT_NICE, 1 + +>r|RLIMIT_RTPRIO +FN("maxrtprio", RLIMIT_RTPRIO, 1 + +>m|ULIMIT_M_IS_RSS +FN("resident-set(KiB)", RLIMIT_RSS, 1024 +>m|ULIMIT_M_IS_VMEM +FN("memory(KiB)", RLIMIT_VMEM, 1024 + +>v|ULIMIT_V_IS_VMEM +FN("virtual-memory(KiB)", RLIMIT_VMEM, 1024 +>v|ULIMIT_V_IS_AS +FN("address-space(KiB)", RLIMIT_AS, 1024 + +|RLIMITS_OPTCS diff --git a/sh.h b/sh.h new file mode 100644 index 0000000..53629b1 --- /dev/null +++ b/sh.h @@ -0,0 +1,2796 @@ +/* $OpenBSD: sh.h,v 1.35 2015/09/10 22:48:58 nicm Exp $ */ +/* $OpenBSD: shf.h,v 1.6 2005/12/11 18:53:51 deraadt Exp $ */ +/* $OpenBSD: table.h,v 1.8 2012/02/19 07:52:30 otto Exp $ */ +/* $OpenBSD: tree.h,v 1.10 2005/03/28 21:28:22 deraadt Exp $ */ +/* $OpenBSD: expand.h,v 1.7 2015/09/01 13:12:31 tedu Exp $ */ +/* $OpenBSD: lex.h,v 1.13 2013/03/03 19:11:34 guenther Exp $ */ +/* $OpenBSD: proto.h,v 1.35 2013/09/04 15:49:19 millert Exp $ */ +/* $OpenBSD: c_test.h,v 1.4 2004/12/20 11:34:26 otto Exp $ */ +/* $OpenBSD: tty.h,v 1.5 2004/12/20 11:34:26 otto Exp $ */ + +/*- + * Copyright © 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un‐ + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person’s immediate fault when using the work as intended. + */ + +#ifdef __dietlibc__ +/* XXX imake style */ +#define _BSD_SOURCE /* live, BSD, live❣ */ +#endif + +#if HAVE_SYS_PARAM_H +#include +#endif +#include +#if HAVE_BOTH_TIME_H +#include +#include +#elif HAVE_SYS_TIME_H +#include +#elif HAVE_TIME_H +#include +#endif +#include +#if HAVE_SYS_SYSMACROS_H +#include +#endif +#if HAVE_SYS_MKDEV_H +#include +#endif +#if HAVE_SYS_MMAN_H +#include +#endif +#if HAVE_SYS_RESOURCE_H +#include +#endif +#include +#include +#include +#include +#include +#if HAVE_IO_H +#include +#endif +#if HAVE_LIBGEN_H +#include +#endif +#if HAVE_LIBUTIL_H +#include +#endif +#include +#if HAVE_PATHS_H +#include +#endif +#include +#include +#include +#include +#include +#if HAVE_STDINT_H +#include +#endif +#include +#include +#include +#if HAVE_STRINGS_H +#include +#endif +#if HAVE_TERMIOS_H +#include +#else +/* shudder… */ +#include +#endif +#ifdef _ISC_UNIX +/* XXX imake style */ +#include +#endif +#if HAVE_ULIMIT_H +#include +#endif +#include +#if HAVE_VALUES_H +#include +#endif +#ifdef MIRBSD_BOOTFLOPPY +#include +#endif + +/* monkey-patch known-bad offsetof versions to quell a warning */ +#if (defined(__KLIBC__) || defined(__dietlibc__)) && \ + ((defined(__GNUC__) && (__GNUC__ > 3)) || defined(__NWCC__)) +#undef offsetof +#define offsetof(s, e) __builtin_offsetof(s, e) +#endif + +#undef __attribute__ +#if HAVE_ATTRIBUTE_BOUNDED +#define MKSH_A_BOUNDED(x,y,z) __attribute__((__bounded__(x, y, z))) +#else +#define MKSH_A_BOUNDED(x,y,z) /* nothing */ +#endif +#if HAVE_ATTRIBUTE_FORMAT +#define MKSH_A_FORMAT(x,y,z) __attribute__((__format__(x, y, z))) +#else +#define MKSH_A_FORMAT(x,y,z) /* nothing */ +#endif +#if HAVE_ATTRIBUTE_NORETURN +#define MKSH_A_NORETURN __attribute__((__noreturn__)) +#else +#define MKSH_A_NORETURN /* nothing */ +#endif +#if HAVE_ATTRIBUTE_PURE +#define MKSH_A_PURE __attribute__((__pure__)) +#else +#define MKSH_A_PURE /* nothing */ +#endif +#if HAVE_ATTRIBUTE_UNUSED +#define MKSH_A_UNUSED __attribute__((__unused__)) +#else +#define MKSH_A_UNUSED /* nothing */ +#endif +#if HAVE_ATTRIBUTE_USED +#define MKSH_A_USED __attribute__((__used__)) +#else +#define MKSH_A_USED /* nothing */ +#endif + +#if defined(MirBSD) && (MirBSD >= 0x09A1) && \ + defined(__ELF__) && defined(__GNUC__) && \ + !defined(__llvm__) && !defined(__NWCC__) +/* + * We got usable __IDSTRING __COPYRIGHT __RCSID __SCCSID macros + * which work for all cases; no need to redefine them using the + * "portable" macros from below when we might have the "better" + * gcc+ELF specific macros or other system dependent ones. + */ +#else +#undef __IDSTRING +#undef __IDSTRING_CONCAT +#undef __IDSTRING_EXPAND +#undef __COPYRIGHT +#undef __RCSID +#undef __SCCSID +#define __IDSTRING_CONCAT(l,p) __LINTED__ ## l ## _ ## p +#define __IDSTRING_EXPAND(l,p) __IDSTRING_CONCAT(l,p) +#ifdef MKSH_DONT_EMIT_IDSTRING +#define __IDSTRING(prefix, string) /* nothing */ +#else +#define __IDSTRING(prefix, string) \ + static const char __IDSTRING_EXPAND(__LINE__,prefix) [] \ + MKSH_A_USED = "@(""#)" #prefix ": " string +#endif +#define __COPYRIGHT(x) __IDSTRING(copyright,x) +#define __RCSID(x) __IDSTRING(rcsid,x) +#define __SCCSID(x) __IDSTRING(sccsid,x) +#endif + +#ifdef EXTERN +__RCSID("$MirOS: src/bin/mksh/sh.h,v 1.858 2018/01/14 01:47:36 tg Exp $"); +#endif +#define MKSH_VERSION "R56 2018/01/14" + +/* arithmetic types: C implementation */ +#if !HAVE_CAN_INTTYPES +#if !HAVE_CAN_UCBINTS +typedef signed int int32_t; +typedef unsigned int uint32_t; +#else +typedef u_int32_t uint32_t; +#endif +#endif + +/* arithmetic types: shell arithmetics */ +#ifdef MKSH_LEGACY_MODE +/* + * POSIX demands these to be the C environment's long type + */ +typedef long mksh_ari_t; +typedef unsigned long mksh_uari_t; +#else +/* + * These types are exactly 32 bit wide; signed and unsigned + * integer wraparound, even across division and modulo, for + * any shell code using them, is guaranteed. + */ +typedef int32_t mksh_ari_t; +typedef uint32_t mksh_uari_t; +#endif + +/* boolean type (no deliberately) */ +typedef unsigned char mksh_bool; +#undef bool +/* false MUST equal the same 0 as written by static storage initialisation */ +#undef false +#undef true +/* access macros for boolean type */ +#define bool mksh_bool +/* values must have identity mapping between mksh_bool and short */ +#define false 0 +#define true 1 +/* make any-type into bool or short */ +#define tobool(cond) ((cond) ? true : false) + +/* char (octet) type: C implementation */ +#if !HAVE_CAN_INT8TYPE +#if !HAVE_CAN_UCBINT8 +typedef unsigned char uint8_t; +#else +typedef u_int8_t uint8_t; +#endif +#endif + +/* other standard types */ + +#if !HAVE_RLIM_T +typedef unsigned long rlim_t; +#endif + +#if !HAVE_SIG_T +#undef sig_t +typedef void (*sig_t)(int); +#endif + +#ifdef MKSH_TYPEDEF_SIG_ATOMIC_T +typedef MKSH_TYPEDEF_SIG_ATOMIC_T sig_atomic_t; +#endif + +#ifdef MKSH_TYPEDEF_SSIZE_T +typedef MKSH_TYPEDEF_SSIZE_T ssize_t; +#endif + +/* un-do vendor damage */ + +#undef BAD /* AIX defines that somewhere */ +#undef PRINT /* LynxOS defines that somewhere */ +#undef flock /* SCO UnixWare defines that to flock64 but ENOENT */ + + +#ifndef MKSH_INCLUDES_ONLY + +/* EBCDIC fun */ + +/* see the large comment in shf.c for an EBCDIC primer */ + +#if defined(MKSH_FOR_Z_OS) && defined(__MVS__) && defined(__IBMC__) && defined(__CHARSET_LIB) +# if !__CHARSET_LIB && !defined(MKSH_EBCDIC) +# error "Please compile with Build.sh -E for EBCDIC!" +# endif +# if __CHARSET_LIB && defined(MKSH_EBCDIC) +# error "Please compile without -E argument to Build.sh for ASCII!" +# endif +# if __CHARSET_LIB && !defined(_ENHANCED_ASCII_EXT) + /* go all-out on ASCII */ +# define _ENHANCED_ASCII_EXT 0xFFFFFFFF +# endif +#endif + +/* extra types */ + +/* getrusage does not exist on OS/2 kLIBC */ +#if !HAVE_GETRUSAGE && !defined(__OS2__) +#undef rusage +#undef RUSAGE_SELF +#undef RUSAGE_CHILDREN +#define rusage mksh_rusage +#define RUSAGE_SELF 0 +#define RUSAGE_CHILDREN -1 + +struct rusage { + struct timeval ru_utime; + struct timeval ru_stime; +}; +#endif + +/* extra macros */ + +#ifndef timerclear +#define timerclear(tvp) \ + do { \ + (tvp)->tv_sec = (tvp)->tv_usec = 0; \ + } while (/* CONSTCOND */ 0) +#endif +#ifndef timeradd +#define timeradd(tvp, uvp, vvp) \ + do { \ + (vvp)->tv_sec = (tvp)->tv_sec + (uvp)->tv_sec; \ + (vvp)->tv_usec = (tvp)->tv_usec + (uvp)->tv_usec; \ + if ((vvp)->tv_usec >= 1000000) { \ + (vvp)->tv_sec++; \ + (vvp)->tv_usec -= 1000000; \ + } \ + } while (/* CONSTCOND */ 0) +#endif +#ifndef timersub +#define timersub(tvp, uvp, vvp) \ + do { \ + (vvp)->tv_sec = (tvp)->tv_sec - (uvp)->tv_sec; \ + (vvp)->tv_usec = (tvp)->tv_usec - (uvp)->tv_usec; \ + if ((vvp)->tv_usec < 0) { \ + (vvp)->tv_sec--; \ + (vvp)->tv_usec += 1000000; \ + } \ + } while (/* CONSTCOND */ 0) +#endif + +#ifdef MKSH__NO_PATH_MAX +#undef PATH_MAX +#else +#ifndef PATH_MAX +#ifdef MAXPATHLEN +#define PATH_MAX MAXPATHLEN +#else +#define PATH_MAX 1024 +#endif +#endif +#endif +#ifndef SIZE_MAX +#ifdef SIZE_T_MAX +#define SIZE_MAX SIZE_T_MAX +#else +#define SIZE_MAX ((size_t)-1) +#endif +#endif +#ifndef S_ISLNK +#define S_ISLNK(m) ((m & 0170000) == 0120000) +#endif +#ifndef S_ISSOCK +#define S_ISSOCK(m) ((m & 0170000) == 0140000) +#endif +#if !defined(S_ISCDF) && defined(S_CDF) +#define S_ISCDF(m) (S_ISDIR(m) && ((m) & S_CDF)) +#endif +#ifndef DEFFILEMODE +#define DEFFILEMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) +#endif + + +/* determine ksh_NSIG: first, use the traditional definitions */ +#undef ksh_NSIG +#if defined(NSIG) +#define ksh_NSIG (NSIG) +#elif defined(_NSIG) +#define ksh_NSIG (_NSIG) +#elif defined(SIGMAX) +#define ksh_NSIG (SIGMAX + 1) +#elif defined(_SIGMAX) +#define ksh_NSIG (_SIGMAX + 1) +#elif defined(NSIG_MAX) +#define ksh_NSIG (NSIG_MAX) +#elif defined(MKSH_FOR_Z_OS) +#define ksh_NSIG 40 +#else +# error Please have your platform define NSIG. +#endif +/* range-check them */ +#if (ksh_NSIG < 1) +# error Your NSIG value is not positive. +#undef ksh_NSIG +#endif +/* second, see if the new POSIX definition is available */ +#ifdef NSIG_MAX +#if (NSIG_MAX < 2) +/* and usable */ +# error Your NSIG_MAX value is too small. +#undef NSIG_MAX +#elif (ksh_NSIG > NSIG_MAX) +/* and realistic */ +# error Your NSIG value is larger than your NSIG_MAX value. +#undef NSIG_MAX +#else +/* since it’s usable, prefer it */ +#undef ksh_NSIG +#define ksh_NSIG (NSIG_MAX) +#endif +/* if NSIG_MAX is now still defined, use sysconf(_SC_NSIG) at runtime */ +#endif +/* third, for cpp without the error directive, default */ +#ifndef ksh_NSIG +#define ksh_NSIG 64 +#endif + +#define ksh_sigmask(sig) (((sig) < 1 || (sig) > 127) ? 255 : 128 + (sig)) + + +/* OS-dependent additions (functions, variables, by OS) */ + +#ifdef MKSH_EXE_EXT +#undef MKSH_EXE_EXT +#define MKSH_EXE_EXT ".exe" +#else +#define MKSH_EXE_EXT "" +#endif + +#ifdef __OS2__ +#define MKSH_UNIXROOT "/@unixroot" +#else +#define MKSH_UNIXROOT "" +#endif + +#ifdef MKSH_DOSPATH +#ifndef __GNUC__ +# error GCC extensions needed later on +#endif +#define MKSH_PATHSEPS ";" +#define MKSH_PATHSEPC ';' +#else +#define MKSH_PATHSEPS ":" +#define MKSH_PATHSEPC ':' +#endif + +#if !HAVE_FLOCK_DECL +extern int flock(int, int); +#endif + +#if !HAVE_GETTIMEOFDAY +#define mksh_TIME(tv) do { \ + (tv).tv_usec = 0; \ + (tv).tv_sec = time(NULL); \ +} while (/* CONSTCOND */ 0) +#else +#define mksh_TIME(tv) gettimeofday(&(tv), NULL) +#endif + +#if !HAVE_GETRUSAGE +extern int getrusage(int, struct rusage *); +#endif + +#if !HAVE_MEMMOVE +/* we assume either memmove or bcopy exist, at the moment */ +#define memmove(dst, src, len) bcopy((src), (dst), (len)) +#endif + +#if !HAVE_REVOKE_DECL +extern int revoke(const char *); +#endif + +#if defined(DEBUG) || !HAVE_STRERROR +#undef strerror +#define strerror /* poisoned */ dontuse_strerror +#define cstrerror /* replaced */ cstrerror +extern const char *cstrerror(int); +#else +#define cstrerror(errnum) ((const char *)strerror(errnum)) +#endif + +#if !HAVE_STRLCPY +size_t strlcpy(char *, const char *, size_t); +#endif + +#ifdef __INTERIX +/* XXX imake style */ +#define makedev mkdev +extern int __cdecl seteuid(uid_t); +extern int __cdecl setegid(gid_t); +#endif + +#if defined(__COHERENT__) +#ifndef O_ACCMODE +/* this need not work everywhere, take care */ +#define O_ACCMODE (O_RDONLY | O_WRONLY | O_RDWR) +#endif +#endif + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +#ifdef MKSH__NO_SYMLINK +#undef S_ISLNK +#define S_ISLNK(m) (/* CONSTCOND */ 0) +#define mksh_lstat stat +#else +#define mksh_lstat lstat +#endif + +#if HAVE_TERMIOS_H +#define mksh_ttyst struct termios +#define mksh_tcget(fd,st) tcgetattr((fd), (st)) +#define mksh_tcset(fd,st) tcsetattr((fd), TCSADRAIN, (st)) +#else +#define mksh_ttyst struct termio +#define mksh_tcget(fd,st) ioctl((fd), TCGETA, (st)) +#define mksh_tcset(fd,st) ioctl((fd), TCSETAW, (st)) +#endif + +#ifndef ISTRIP +#define ISTRIP 0 +#endif + +#ifdef MKSH_EBCDIC +#define KSH_BEL '\a' +#define KSH_ESC 047 +#define KSH_ESC_STRING "\047" +#define KSH_VTAB '\v' +#else +/* + * According to the comments in pdksh, \007 seems to be more portable + * than \a (HP-UX cc, Ultrix cc, old pcc, etc.) so we avoid the escape + * sequence if ASCII can be assumed. + */ +#define KSH_BEL 7 +#define KSH_ESC 033 +#define KSH_ESC_STRING "\033" +#define KSH_VTAB 11 +#endif + + +/* some useful #defines */ +#ifdef EXTERN +# define E_INIT(i) = i +#else +# define E_INIT(i) +# define EXTERN extern +# define EXTERN_DEFINED +#endif + +/* define bit in flag */ +#define BIT(i) (1U << (i)) +#define NELEM(a) (sizeof(a) / sizeof((a)[0])) + +/* + * Make MAGIC a char that might be printed to make bugs more obvious, but + * not a char that is used often. Also, can't use the high bit as it causes + * portability problems (calling strchr(x, 0x80 | 'x') is error prone). + * + * MAGIC can be followed by MAGIC (to escape the octet itself) or one of: + * ' !)*,-?[]{|}' 0x80|' !*+?@' (probably… hysteric raisins abound) + * + * The |0x80 is likely unsafe on EBCDIC :( though the listed chars are + * low-bit7 at least on cp1047 so YMMV + */ +#define MAGIC KSH_BEL /* prefix for *?[!{,} during expand */ +#define ISMAGIC(c) (ord(c) == ORD(MAGIC)) + +EXTERN const char *safe_prompt; /* safe prompt if PS1 substitution fails */ + +#ifdef MKSH_LEGACY_MODE +#define KSH_VERSIONNAME_ISLEGACY "LEGACY" +#else +#define KSH_VERSIONNAME_ISLEGACY "MIRBSD" +#endif +#ifdef MKSH_WITH_TEXTMODE +#define KSH_VERSIONNAME_TEXTMODE " +TEXTMODE" +#else +#define KSH_VERSIONNAME_TEXTMODE "" +#endif +#ifdef MKSH_EBCDIC +#define KSH_VERSIONNAME_EBCDIC " +EBCDIC" +#else +#define KSH_VERSIONNAME_EBCDIC "" +#endif +#ifndef KSH_VERSIONNAME_VENDOR_EXT +#define KSH_VERSIONNAME_VENDOR_EXT "" +#endif +EXTERN const char initvsn[] E_INIT("KSH_VERSION=@(#)" KSH_VERSIONNAME_ISLEGACY \ + " KSH " MKSH_VERSION KSH_VERSIONNAME_EBCDIC KSH_VERSIONNAME_TEXTMODE \ + KSH_VERSIONNAME_VENDOR_EXT); +#define KSH_VERSION (initvsn + /* "KSH_VERSION=@(#)" */ 16) + +EXTERN const char digits_uc[] E_INIT("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); +EXTERN const char digits_lc[] E_INIT("0123456789abcdefghijklmnopqrstuvwxyz"); + +/* + * Evil hack for const correctness due to API brokenness + */ +union mksh_cchack { + char *rw; + const char *ro; +}; +union mksh_ccphack { + char **rw; + const char **ro; +}; + +/* + * Evil hack since casting uint to sint is implementation-defined + */ +typedef union { + mksh_ari_t i; + mksh_uari_t u; +} mksh_ari_u; + +/* for const debugging */ +#if defined(DEBUG) && defined(__GNUC__) && !defined(__ICC) && \ + !defined(__INTEL_COMPILER) && !defined(__SUNPRO_C) +char *ucstrchr(char *, int); +char *ucstrstr(char *, const char *); +#undef strchr +#define strchr ucstrchr +#define strstr ucstrstr +#define cstrchr(s,c) ({ \ + union mksh_cchack in, out; \ + \ + in.ro = (s); \ + out.rw = ucstrchr(in.rw, (c)); \ + (out.ro); \ +}) +#define cstrstr(b,l) ({ \ + union mksh_cchack in, out; \ + \ + in.ro = (b); \ + out.rw = ucstrstr(in.rw, (l)); \ + (out.ro); \ +}) +#define vstrchr(s,c) (cstrchr((s), (c)) != NULL) +#define vstrstr(b,l) (cstrstr((b), (l)) != NULL) +#else /* !DEBUG, !gcc */ +#define cstrchr(s,c) ((const char *)strchr((s), (c))) +#define cstrstr(s,c) ((const char *)strstr((s), (c))) +#define vstrchr(s,c) (strchr((s), (c)) != NULL) +#define vstrstr(b,l) (strstr((b), (l)) != NULL) +#endif + +#if defined(DEBUG) || defined(__COVERITY__) +#ifndef DEBUG_LEAKS +#define DEBUG_LEAKS +#endif +#endif + +#if (!defined(MKSH_BUILDMAKEFILE4BSD) && !defined(MKSH_BUILDSH)) || (MKSH_BUILD_R != 563) +#error Must run Build.sh to compile this. +extern void thiswillneverbedefinedIhope(void); +int +im_sorry_dave(void) +{ + /* I’m sorry, Dave. I’m afraid I can’t do that. */ + return (thiswillneverbedefinedIhope()); +} +#endif + +/* use this ipv strchr(s, 0) but no side effects in s! */ +#define strnul(s) ((s) + strlen((const void *)s)) + +#define utf_ptradjx(src, dst) do { \ + (dst) = (src) + utf_ptradj(src); \ +} while (/* CONSTCOND */ 0) + +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +#define strdupx(d, s, ap) do { \ + (d) = strdup_i((s), (ap)); \ +} while (/* CONSTCOND */ 0) +#define strndupx(d, s, n, ap) do { \ + (d) = strndup_i((s), (n), (ap)); \ +} while (/* CONSTCOND */ 0) +#else +/* be careful to evaluate arguments only once! */ +#define strdupx(d, s, ap) do { \ + const char *strdup_src = (const void *)(s); \ + char *strdup_dst = NULL; \ + \ + if (strdup_src != NULL) { \ + size_t strdup_len = strlen(strdup_src) + 1; \ + strdup_dst = alloc(strdup_len, (ap)); \ + memcpy(strdup_dst, strdup_src, strdup_len); \ + } \ + (d) = strdup_dst; \ +} while (/* CONSTCOND */ 0) +#define strndupx(d, s, n, ap) do { \ + const char *strdup_src = (const void *)(s); \ + char *strdup_dst = NULL; \ + \ + if (strdup_src != NULL) { \ + size_t strndup_len = (n); \ + strdup_dst = alloc(strndup_len + 1, (ap)); \ + memcpy(strdup_dst, strdup_src, strndup_len); \ + strdup_dst[strndup_len] = '\0'; \ + } \ + (d) = strdup_dst; \ +} while (/* CONSTCOND */ 0) +#endif + +#ifdef MKSH_SMALL +#ifndef MKSH_NOPWNAM +#define MKSH_NOPWNAM /* defined */ +#endif +#ifndef MKSH_S_NOVI +#define MKSH_S_NOVI 1 +#endif +#endif + +#ifndef MKSH_S_NOVI +#define MKSH_S_NOVI 0 +#endif + +#if defined(MKSH_NOPROSPECTOFWORK) && !defined(MKSH_UNEMPLOYED) +#define MKSH_UNEMPLOYED 1 +#endif + +#define NUFILE 32 /* Number of user-accessible files */ +#define FDBASE 10 /* First file usable by Shell */ + +/* + * simple grouping allocator + */ + + +/* 0. OS API: where to get memory from and how to free it (grouped) */ + +/* malloc(3)/realloc(3) -> free(3) for use by the memory allocator */ +#define malloc_osi(sz) malloc(sz) +#define realloc_osi(p,sz) realloc((p), (sz)) +#define free_osimalloc(p) free(p) + +/* malloc(3)/realloc(3) -> free(3) for use by mksh code */ +#define malloc_osfunc(sz) malloc(sz) +#define realloc_osfunc(p,sz) realloc((p), (sz)) +#define free_osfunc(p) free(p) + +#if HAVE_MKNOD +/* setmode(3) -> free(3) */ +#define free_ossetmode(p) free(p) +#endif + +#ifdef MKSH__NO_PATH_MAX +/* GNU libc: get_current_dir_name(3) -> free(3) */ +#define free_gnu_gcdn(p) free(p) +#endif + + +/* 1. internal structure */ +struct lalloc_common { + struct lalloc_common *next; +}; + +#ifdef MKSH_ALLOC_CATCH_UNDERRUNS +struct lalloc_item { + struct lalloc_common *next; + size_t len; + char dummy[8192 - sizeof(struct lalloc_common *) - sizeof(size_t)]; +}; +#endif + +/* 2. sizes */ +#ifdef MKSH_ALLOC_CATCH_UNDERRUNS +#define ALLOC_ITEM struct lalloc_item +#define ALLOC_OVERHEAD 0 +#else +#define ALLOC_ITEM struct lalloc_common +#define ALLOC_OVERHEAD (sizeof(ALLOC_ITEM)) +#endif + +/* 3. group structure */ +typedef struct lalloc_common Area; + + +EXTERN Area aperm; /* permanent object space */ +#define APERM &aperm +#define ATEMP &e->area + +/* + * flags (the order of these enums MUST match the order in misc.c(options[])) + */ +enum sh_flag { +#define SHFLAGS_ENUMS +#include "sh_flags.gen" + FNFLAGS /* (place holder: how many flags are there) */ +}; + +#define Flag(f) (shell_flags[(int)(f)]) +#define UTFMODE Flag(FUNICODE) + +/* + * parsing & execution environment + * + * note that kshlongjmp MUST NOT be passed 0 as second argument! + */ +#ifdef MKSH_NO_SIGSETJMP +#define kshjmp_buf jmp_buf +#define kshsetjmp(jbuf) _setjmp(jbuf) +#define kshlongjmp _longjmp +#else +#define kshjmp_buf sigjmp_buf +#define kshsetjmp(jbuf) sigsetjmp((jbuf), 0) +#define kshlongjmp siglongjmp +#endif + +struct sretrace_info; +struct yyrecursive_state; + +EXTERN struct sretrace_info *retrace_info; +EXTERN unsigned int subshell_nesting_type; + +extern struct env { + ALLOC_ITEM alloc_INT; /* internal, do not touch */ + Area area; /* temporary allocation area */ + struct env *oenv; /* link to previous environment */ + struct block *loc; /* local variables and functions */ + short *savefd; /* original redirected fds */ + struct temp *temps; /* temp files */ + /* saved parser recursion state */ + struct yyrecursive_state *yyrecursive_statep; + kshjmp_buf jbuf; /* long jump back to env creator */ + uint8_t type; /* environment type - see below */ + uint8_t flags; /* EF_* */ +} *e; + +/* struct env.type values */ +#define E_NONE 0 /* dummy environment */ +#define E_PARSE 1 /* parsing command # */ +#define E_FUNC 2 /* executing function # */ +#define E_INCL 3 /* including a file via . # */ +#define E_EXEC 4 /* executing command tree */ +#define E_LOOP 5 /* executing for/while # */ +#define E_ERRH 6 /* general error handler # */ +#define E_GONE 7 /* hidden in child */ +#define E_EVAL 8 /* running eval # */ +/* # indicates env has valid jbuf (see unwind()) */ + +/* struct env.flag values */ +#define EF_BRKCONT_PASS BIT(1) /* set if E_LOOP must pass break/continue on */ +#define EF_FAKE_SIGDIE BIT(2) /* hack to get info from unwind to quitenv */ + +/* Do breaks/continues stop at env type e? */ +#define STOP_BRKCONT(t) ((t) == E_NONE || (t) == E_PARSE || \ + (t) == E_FUNC || (t) == E_INCL) +/* Do returns stop at env type e? */ +#define STOP_RETURN(t) ((t) == E_FUNC || (t) == E_INCL) + +/* values for kshlongjmp(e->jbuf, i) */ +/* note that i MUST NOT be zero */ +#define LRETURN 1 /* return statement */ +#define LEXIT 2 /* exit statement */ +#define LERROR 3 /* errorf() called */ +#define LLEAVE 4 /* untrappable exit/error */ +#define LINTR 5 /* ^C noticed */ +#define LBREAK 6 /* break statement */ +#define LCONTIN 7 /* continue statement */ +#define LSHELL 8 /* return to interactive shell() */ +#define LAEXPR 9 /* error in arithmetic expression */ + +/* sort of shell global state */ +EXTERN pid_t procpid; /* PID of executing process */ +EXTERN int exstat; /* exit status */ +EXTERN int subst_exstat; /* exit status of last $(..)/`..` */ +EXTERN struct tbl *vp_pipest; /* global PIPESTATUS array */ +EXTERN short trap_exstat; /* exit status before running a trap */ +EXTERN uint8_t trap_nested; /* running nested traps */ +EXTERN uint8_t shell_flags[FNFLAGS]; +EXTERN const char *kshname; /* $0 */ +EXTERN struct { + uid_t kshuid_v; /* real UID of shell */ + uid_t ksheuid_v; /* effective UID of shell */ + gid_t kshgid_v; /* real GID of shell */ + gid_t kshegid_v; /* effective GID of shell */ + pid_t kshpgrp_v; /* process group of shell */ + pid_t kshppid_v; /* PID of parent of shell */ + pid_t kshpid_v; /* $$, shell PID */ +} rndsetupstate; + +#define kshpid rndsetupstate.kshpid_v +#define kshpgrp rndsetupstate.kshpgrp_v +#define kshuid rndsetupstate.kshuid_v +#define ksheuid rndsetupstate.ksheuid_v +#define kshgid rndsetupstate.kshgid_v +#define kshegid rndsetupstate.kshegid_v +#define kshppid rndsetupstate.kshppid_v + + +/* option processing */ +#define OF_CMDLINE 0x01 /* command line */ +#define OF_SET 0x02 /* set builtin */ +#define OF_SPECIAL 0x04 /* a special variable changing */ +#define OF_INTERNAL 0x08 /* set internally by shell */ +#define OF_FIRSTTIME 0x10 /* as early as possible, once */ +#define OF_ANY (OF_CMDLINE | OF_SET | OF_SPECIAL | OF_INTERNAL) + +/* null value for variable; comparison pointer for unset */ +EXTERN char null[] E_INIT(""); + +/* string pooling: do we rely on the compiler? */ +#ifndef HAVE_STRING_POOLING +/* no, we use our own, saves quite some space */ +#elif HAVE_STRING_POOLING == 2 +/* “on demand” */ +#ifdef __GNUC__ +/* only for GCC 4 or later, older ones can get by without */ +#if __GNUC__ < 4 +#undef HAVE_STRING_POOLING +#endif +#else +/* not GCC, default to on */ +#endif +#elif HAVE_STRING_POOLING == 0 +/* default to on, unless explicitly set to 0 */ +#undef HAVE_STRING_POOLING +#endif + +#ifndef HAVE_STRING_POOLING /* helpers for pooled strings */ +EXTERN const char T4spaces[] E_INIT(" "); +#define T1space (Treal_sp2 + 5) +#define Tcolsp (Tf_sD_ + 2) +#define TC_IFSWS (TinitIFS + 4) +EXTERN const char TinitIFS[] E_INIT("IFS= \t\n"); +EXTERN const char TFCEDIT_dollaru[] E_INIT("${FCEDIT:-/bin/ed} $_"); +#define Tspdollaru (TFCEDIT_dollaru + 18) +EXTERN const char Tsgdot[] E_INIT("*=."); +EXTERN const char Taugo[] E_INIT("augo"); +EXTERN const char Tbracket[] E_INIT("["); +#define Tdot (Tsgdot + 2) +#define Talias (Tunalias + 2) +EXTERN const char Tbadnum[] E_INIT("bad number"); +#define Tbadsubst (Tfg_badsubst + 10) +EXTERN const char Tbg[] E_INIT("bg"); +EXTERN const char Tbad_bsize[] E_INIT("bad shf/buf/bsize"); +#define Tbsize (Tbad_bsize + 12) +EXTERN const char Tbad_sig_ss[] E_INIT("%s: bad signal '%s'"); +#define Tbad_sig_s (Tbad_sig_ss + 4) +EXTERN const char Tsgbreak[] E_INIT("*=break"); +#define Tbreak (Tsgbreak + 2) +EXTERN const char T__builtin[] E_INIT("-\\builtin"); +#define T_builtin (T__builtin + 1) +#define Tbuiltin (T__builtin + 2) +EXTERN const char Toomem[] E_INIT("can't allocate %zu data bytes"); +EXTERN const char Tcant_cd[] E_INIT("restricted shell - can't cd"); +EXTERN const char Tcant_find[] E_INIT("can't find"); +EXTERN const char Tcant_open[] E_INIT("can't open"); +#define Tbytes (Toomem + 24) +EXTERN const char Tbcat[] E_INIT("!cat"); +#define Tcat (Tbcat + 1) +#define Tcd (Tcant_cd + 25) +#define T_command (T_funny_command + 9) +#define Tcommand (T_funny_command + 10) +EXTERN const char Tsgcontinue[] E_INIT("*=continue"); +#define Tcontinue (Tsgcontinue + 2) +EXTERN const char Tcreate[] E_INIT("create"); +EXTERN const char TELIF_unexpected[] E_INIT("TELIF unexpected"); +EXTERN const char TEXECSHELL[] E_INIT("EXECSHELL"); +EXTERN const char Tdsgexport[] E_INIT("^*=export"); +#define Texport (Tdsgexport + 3) +#ifdef __OS2__ +EXTERN const char Textproc[] E_INIT("extproc"); +#endif +EXTERN const char Tfalse[] E_INIT("false"); +EXTERN const char Tfg[] E_INIT("fg"); +EXTERN const char Tfg_badsubst[] E_INIT("fileglob: bad substitution"); +#define Tfile (Tfile_fd + 20) +EXTERN const char Tfile_fd[] E_INIT("function definition file"); +EXTERN const char TFPATH[] E_INIT("FPATH"); +EXTERN const char T_function[] E_INIT(" function"); +#define Tfunction (T_function + 1) +EXTERN const char T_funny_command[] E_INIT("funny $()-command"); +EXTERN const char Tgetopts[] E_INIT("getopts"); +#define Thistory (Tnot_in_history + 7) +EXTERN const char Tintovfl[] E_INIT("integer overflow %zu %c %zu prevented"); +EXTERN const char Tinvname[] E_INIT("%s: invalid %s name"); +EXTERN const char Tjobs[] E_INIT("jobs"); +EXTERN const char Tjob_not_started[] E_INIT("job not started"); +EXTERN const char Tmksh[] E_INIT("mksh"); +#define Tname (Tinvname + 15) +EXTERN const char Tno_args[] E_INIT("missing argument"); +EXTERN const char Tno_OLDPWD[] E_INIT("no OLDPWD"); +EXTERN const char Tnot_ident[] E_INIT("is not an identifier"); +EXTERN const char Tnot_in_history[] E_INIT("not in history"); +EXTERN const char Tnot_found_s[] E_INIT("%s not found"); +#define Tnot_found (Tnot_found_s + 3) +#define Tnot_started (Tjob_not_started + 4) +#define TOLDPWD (Tno_OLDPWD + 3) +#define Topen (Tcant_open + 6) +#define TPATH (TFPATH + 1) +#define Tpv (TpVv + 1) +EXTERN const char TpVv[] E_INIT("Vpv"); +#define TPWD (Tno_OLDPWD + 6) +#define Tread (Tshf_read + 4) +EXTERN const char Tdsgreadonly[] E_INIT("^*=readonly"); +#define Treadonly (Tdsgreadonly + 3) +EXTERN const char Tredirection_dup[] E_INIT("can't finish (dup) redirection"); +#define Tredirection (Tredirection_dup + 19) +#define Treal_sp1 (Treal_sp2 + 1) +EXTERN const char Treal_sp2[] E_INIT(" real "); +EXTERN const char Treq_arg[] E_INIT("requires an argument"); +EXTERN const char Tselect[] E_INIT("select"); +EXTERN const char Tsgset[] E_INIT("*=set"); +#define Tset (Tf_parm + 18) +#define Tsh (Tmksh + 2) +#define TSHELL (TEXECSHELL + 4) +#define Tshell (Ttoo_many_files + 23) +EXTERN const char Tshf_read[] E_INIT("shf_read"); +EXTERN const char Tshf_write[] E_INIT("shf_write"); +EXTERN const char Tgsource[] E_INIT("=source"); +#define Tsource (Tgsource + 1) +EXTERN const char Tj_suspend[] E_INIT("j_suspend"); +#define Tsuspend (Tj_suspend + 2) +EXTERN const char Tsynerr[] E_INIT("syntax error"); +EXTERN const char Ttime[] E_INIT("time"); +EXTERN const char Ttoo_many_args[] E_INIT("too many arguments"); +EXTERN const char Ttoo_many_files[] E_INIT("too many open files in shell"); +EXTERN const char Ttrue[] E_INIT("true"); +EXTERN const char Ttty_fd_dupof[] E_INIT("dup of tty fd"); +#define Ttty_fd (Ttty_fd_dupof + 7) +EXTERN const char Tdgtypeset[] E_INIT("^=typeset"); +#define Ttypeset (Tdgtypeset + 2) +#define Tugo (Taugo + 1) +EXTERN const char Tunalias[] E_INIT("unalias"); +#define Tunexpected (TELIF_unexpected + 6) +EXTERN const char Tunexpected_type[] E_INIT("%s: unexpected %s type %d"); +EXTERN const char Tunknown_option[] E_INIT("unknown option"); +EXTERN const char Tunwind[] E_INIT("unwind"); +#define Tuser_sp1 (Tuser_sp2 + 1) +EXTERN const char Tuser_sp2[] E_INIT(" user "); +#define Twrite (Tshf_write + 4) +EXTERN const char Tf__S[] E_INIT(" %S"); +#define Tf__d (Tunexpected_type + 22) +EXTERN const char Tf__ss[] E_INIT(" %s%s"); +#define Tf__sN (Tf_s_s_sN + 5) +EXTERN const char Tf_sSs[] E_INIT("%s/%s"); +#define Tf_T (Tf_s_T + 3) +EXTERN const char Tf_dN[] E_INIT("%d\n"); +EXTERN const char Tf_s_[] E_INIT("%s "); +EXTERN const char Tf_s_T[] E_INIT("%s %T"); +EXTERN const char Tf_s_s_sN[] E_INIT("%s %s %s\n"); +#define Tf_s_s (Tf_sD_s_s + 4) +#define Tf_s_sD_s (Tf_cant_ss_s + 6) +EXTERN const char Tf_optfoo[] E_INIT("%s%s-%c: %s"); +EXTERN const char Tf_sD_[] E_INIT("%s: "); +EXTERN const char Tf_szs[] E_INIT("%s: %zd %s"); +EXTERN const char Tf_parm[] E_INIT("%s: parameter not set"); +EXTERN const char Tf_coproc[] E_INIT("-p: %s"); +EXTERN const char Tf_cant_s[] E_INIT("%s: can't %s"); +EXTERN const char Tf_cant_ss_s[] E_INIT("can't %s %s: %s"); +EXTERN const char Tf_heredoc[] E_INIT("here document '%s' unclosed"); +#if HAVE_MKNOD +EXTERN const char Tf_nonnum[] E_INIT("non-numeric %s %s '%s'"); +#endif +EXTERN const char Tf_S_[] E_INIT("%S "); +#define Tf_S (Tf__S + 1) +#define Tf_lu (Tf_toolarge + 17) +EXTERN const char Tf_toolarge[] E_INIT("%s %s too large: %lu"); +EXTERN const char Tf_ldfailed[] E_INIT("%s %s(%d, %ld) failed: %s"); +#define Tf_ss (Tf_sss + 2) +EXTERN const char Tf_sss[] E_INIT("%s%s%s"); +EXTERN const char Tf_sD_s_sD_s[] E_INIT("%s: %s %s: %s"); +EXTERN const char Tf_toomany[] E_INIT("too many %ss"); +EXTERN const char Tf_sd[] E_INIT("%s %d"); +#define Tf_s (Tf_temp + 28) +EXTERN const char Tft_end[] E_INIT("%;"); +EXTERN const char Tft_R[] E_INIT("%R"); +#define Tf_d (Tunexpected_type + 23) +EXTERN const char Tf_sD_s_qs[] E_INIT("%s: %s '%s'"); +EXTERN const char Tf_ro[] E_INIT("read-only: %s"); +EXTERN const char Tf_flags[] E_INIT("%s: flags 0x%X"); +EXTERN const char Tf_temp[] E_INIT("can't %s temporary file %s: %s"); +EXTERN const char Tf_ssfaileds[] E_INIT("%s: %s failed: %s"); +EXTERN const char Tf_sD_sD_s[] E_INIT("%s: %s: %s"); +EXTERN const char Tf__c_[] E_INIT("-%c "); +EXTERN const char Tf_sD_s_s[] E_INIT("%s: %s %s"); +#define Tf_sN (Tf_s_s_sN + 6) +#define Tf_sD_s (Tf_temp + 24) +EXTERN const char T_devtty[] E_INIT("/dev/tty"); +#else /* helpers for string pooling */ +#define T4spaces " " +#define T1space " " +#define Tcolsp ": " +#define TC_IFSWS " \t\n" +#define TinitIFS "IFS= \t\n" +#define TFCEDIT_dollaru "${FCEDIT:-/bin/ed} $_" +#define Tspdollaru " $_" +#define Tsgdot "*=." +#define Taugo "augo" +#define Tbracket "[" +#define Tdot "." +#define Talias "alias" +#define Tbadnum "bad number" +#define Tbadsubst "bad substitution" +#define Tbg "bg" +#define Tbad_bsize "bad shf/buf/bsize" +#define Tbsize "bsize" +#define Tbad_sig_ss "%s: bad signal '%s'" +#define Tbad_sig_s "bad signal '%s'" +#define Tsgbreak "*=break" +#define Tbreak "break" +#define T__builtin "-\\builtin" +#define T_builtin "\\builtin" +#define Tbuiltin "builtin" +#define Toomem "can't allocate %zu data bytes" +#define Tcant_cd "restricted shell - can't cd" +#define Tcant_find "can't find" +#define Tcant_open "can't open" +#define Tbytes "bytes" +#define Tbcat "!cat" +#define Tcat "cat" +#define Tcd "cd" +#define T_command "-command" +#define Tcommand "command" +#define Tsgcontinue "*=continue" +#define Tcontinue "continue" +#define Tcreate "create" +#define TELIF_unexpected "TELIF unexpected" +#define TEXECSHELL "EXECSHELL" +#define Tdsgexport "^*=export" +#define Texport "export" +#ifdef __OS2__ +#define Textproc "extproc" +#endif +#define Tfalse "false" +#define Tfg "fg" +#define Tfg_badsubst "fileglob: bad substitution" +#define Tfile "file" +#define Tfile_fd "function definition file" +#define TFPATH "FPATH" +#define T_function " function" +#define Tfunction "function" +#define T_funny_command "funny $()-command" +#define Tgetopts "getopts" +#define Thistory "history" +#define Tintovfl "integer overflow %zu %c %zu prevented" +#define Tinvname "%s: invalid %s name" +#define Tjobs "jobs" +#define Tjob_not_started "job not started" +#define Tmksh "mksh" +#define Tname "name" +#define Tno_args "missing argument" +#define Tno_OLDPWD "no OLDPWD" +#define Tnot_ident "is not an identifier" +#define Tnot_in_history "not in history" +#define Tnot_found_s "%s not found" +#define Tnot_found "not found" +#define Tnot_started "not started" +#define TOLDPWD "OLDPWD" +#define Topen "open" +#define TPATH "PATH" +#define Tpv "pv" +#define TpVv "Vpv" +#define TPWD "PWD" +#define Tread "read" +#define Tdsgreadonly "^*=readonly" +#define Treadonly "readonly" +#define Tredirection_dup "can't finish (dup) redirection" +#define Tredirection "redirection" +#define Treal_sp1 "real " +#define Treal_sp2 " real " +#define Treq_arg "requires an argument" +#define Tselect "select" +#define Tsgset "*=set" +#define Tset "set" +#define Tsh "sh" +#define TSHELL "SHELL" +#define Tshell "shell" +#define Tshf_read "shf_read" +#define Tshf_write "shf_write" +#define Tgsource "=source" +#define Tsource "source" +#define Tj_suspend "j_suspend" +#define Tsuspend "suspend" +#define Tsynerr "syntax error" +#define Ttime "time" +#define Ttoo_many_args "too many arguments" +#define Ttoo_many_files "too many open files in shell" +#define Ttrue "true" +#define Ttty_fd_dupof "dup of tty fd" +#define Ttty_fd "tty fd" +#define Tdgtypeset "^=typeset" +#define Ttypeset "typeset" +#define Tugo "ugo" +#define Tunalias "unalias" +#define Tunexpected "unexpected" +#define Tunexpected_type "%s: unexpected %s type %d" +#define Tunknown_option "unknown option" +#define Tunwind "unwind" +#define Tuser_sp1 "user " +#define Tuser_sp2 " user " +#define Twrite "write" +#define Tf__S " %S" +#define Tf__d " %d" +#define Tf__ss " %s%s" +#define Tf__sN " %s\n" +#define Tf_sSs "%s/%s" +#define Tf_T "%T" +#define Tf_dN "%d\n" +#define Tf_s_ "%s " +#define Tf_s_T "%s %T" +#define Tf_s_s_sN "%s %s %s\n" +#define Tf_s_s "%s %s" +#define Tf_s_sD_s "%s %s: %s" +#define Tf_optfoo "%s%s-%c: %s" +#define Tf_sD_ "%s: " +#define Tf_szs "%s: %zd %s" +#define Tf_parm "%s: parameter not set" +#define Tf_coproc "-p: %s" +#define Tf_cant_s "%s: can't %s" +#define Tf_cant_ss_s "can't %s %s: %s" +#define Tf_heredoc "here document '%s' unclosed" +#if HAVE_MKNOD +#define Tf_nonnum "non-numeric %s %s '%s'" +#endif +#define Tf_S_ "%S " +#define Tf_S "%S" +#define Tf_lu "%lu" +#define Tf_toolarge "%s %s too large: %lu" +#define Tf_ldfailed "%s %s(%d, %ld) failed: %s" +#define Tf_ss "%s%s" +#define Tf_sss "%s%s%s" +#define Tf_sD_s_sD_s "%s: %s %s: %s" +#define Tf_toomany "too many %ss" +#define Tf_sd "%s %d" +#define Tf_s "%s" +#define Tft_end "%;" +#define Tft_R "%R" +#define Tf_d "%d" +#define Tf_sD_s_qs "%s: %s '%s'" +#define Tf_ro "read-only: %s" +#define Tf_flags "%s: flags 0x%X" +#define Tf_temp "can't %s temporary file %s: %s" +#define Tf_ssfaileds "%s: %s failed: %s" +#define Tf_sD_sD_s "%s: %s: %s" +#define Tf__c_ "-%c " +#define Tf_sD_s_s "%s: %s %s" +#define Tf_sN "%s\n" +#define Tf_sD_s "%s: %s" +#define T_devtty "/dev/tty" +#endif /* end of string pooling */ + +typedef uint8_t Temp_type; +/* expanded heredoc */ +#define TT_HEREDOC_EXP 0 +/* temporary file used for history editing (fc -e) */ +#define TT_HIST_EDIT 1 +/* temporary file used during in-situ command substitution */ +#define TT_FUNSUB 2 + +/* temp/heredoc files. The file is removed when the struct is freed. */ +struct temp { + struct temp *next; + struct shf *shf; + /* pid of process parsed here-doc */ + pid_t pid; + Temp_type type; + /* actually longer: name (variable length) */ + char tffn[3]; +}; + +/* + * stdio and our IO routines + */ + +#define shl_xtrace (&shf_iob[0]) /* for set -x */ +#define shl_stdout (&shf_iob[1]) +#define shl_out (&shf_iob[2]) +#ifdef DF +#define shl_dbg (&shf_iob[3]) /* for DF() */ +#endif +EXTERN bool shl_stdout_ok; + +/* + * trap handlers + */ +typedef struct trap { + const char *name; /* short name */ + const char *mess; /* descriptive name */ + char *trap; /* trap command */ + sig_t cursig; /* current handler (valid if TF_ORIG_* set) */ + sig_t shtrap; /* shell signal handler */ + int signal; /* signal number */ + int flags; /* TF_* */ + volatile sig_atomic_t set; /* trap pending */ +} Trap; + +/* values for Trap.flags */ +#define TF_SHELL_USES BIT(0) /* shell uses signal, user can't change */ +#define TF_USER_SET BIT(1) /* user has (tried to) set trap */ +#define TF_ORIG_IGN BIT(2) /* original action was SIG_IGN */ +#define TF_ORIG_DFL BIT(3) /* original action was SIG_DFL */ +#define TF_EXEC_IGN BIT(4) /* restore SIG_IGN just before exec */ +#define TF_EXEC_DFL BIT(5) /* restore SIG_DFL just before exec */ +#define TF_DFL_INTR BIT(6) /* when received, default action is LINTR */ +#define TF_TTY_INTR BIT(7) /* tty generated signal (see j_waitj) */ +#define TF_CHANGED BIT(8) /* used by runtrap() to detect trap changes */ +#define TF_FATAL BIT(9) /* causes termination if not trapped */ + +/* values for setsig()/setexecsig() flags argument */ +#define SS_RESTORE_MASK 0x3 /* how to restore a signal before an exec() */ +#define SS_RESTORE_CURR 0 /* leave current handler in place */ +#define SS_RESTORE_ORIG 1 /* restore original handler */ +#define SS_RESTORE_DFL 2 /* restore to SIG_DFL */ +#define SS_RESTORE_IGN 3 /* restore to SIG_IGN */ +#define SS_FORCE BIT(3) /* set signal even if original signal ignored */ +#define SS_USER BIT(4) /* user is doing the set (ie, trap command) */ +#define SS_SHTRAP BIT(5) /* trap for internal use (ALRM, CHLD, WINCH) */ + +#define ksh_SIGEXIT 0 /* for trap EXIT */ +#define ksh_SIGERR ksh_NSIG /* for trap ERR */ + +EXTERN volatile sig_atomic_t trap; /* traps pending? */ +EXTERN volatile sig_atomic_t intrsig; /* pending trap interrupts command */ +EXTERN volatile sig_atomic_t fatal_trap; /* received a fatal signal */ +extern Trap sigtraps[ksh_NSIG + 1]; + +/* got_winch = 1 when we need to re-adjust the window size */ +#ifdef SIGWINCH +EXTERN volatile sig_atomic_t got_winch E_INIT(1); +#else +#define got_winch true +#endif + +/* + * TMOUT support + */ +/* values for ksh_tmout_state */ +enum tmout_enum { + TMOUT_EXECUTING = 0, /* executing commands */ + TMOUT_READING, /* waiting for input */ + TMOUT_LEAVING /* have timed out */ +}; +EXTERN unsigned int ksh_tmout; +EXTERN enum tmout_enum ksh_tmout_state; + +/* For "You have stopped jobs" message */ +EXTERN bool really_exit; + +/* + * fast character classes + */ + +/* internal types, do not reference */ + +/* initially empty — filled at runtime from $IFS */ +#define CiIFS BIT(0) +#define CiCNTRL BIT(1) /* \x01‥\x08\x0E‥\x1F\x7F */ +#define CiUPPER BIT(2) /* A‥Z */ +#define CiLOWER BIT(3) /* a‥z */ +#define CiHEXLT BIT(4) /* A‥Fa‥f */ +#define CiOCTAL BIT(5) /* 0‥7 */ +#define CiQCL BIT(6) /* &();| */ +#define CiALIAS BIT(7) /* !,.@ */ +#define CiQCX BIT(8) /* *[\\ */ +#define CiVAR1 BIT(9) /* !*@ */ +#define CiQCM BIT(10) /* /^~ */ +#define CiDIGIT BIT(11) /* 89 */ +#define CiQC BIT(12) /* "' */ +#define CiSPX BIT(13) /* \x0B\x0C */ +#define CiCURLY BIT(14) /* {} */ +#define CiANGLE BIT(15) /* <> */ +#define CiNUL BIT(16) /* \x00 */ +#define CiTAB BIT(17) /* \x09 */ +#define CiNL BIT(18) /* \x0A */ +#define CiCR BIT(19) /* \x0D */ +#define CiSP BIT(20) /* \x20 */ +#define CiHASH BIT(21) /* # */ +#define CiSS BIT(22) /* $ */ +#define CiPERCT BIT(23) /* % */ +#define CiPLUS BIT(24) /* + */ +#define CiMINUS BIT(25) /* - */ +#define CiCOLON BIT(26) /* : */ +#define CiEQUAL BIT(27) /* = */ +#define CiQUEST BIT(28) /* ? */ +#define CiBRACK BIT(29) /* ] */ +#define CiUNDER BIT(30) /* _ */ +#define CiGRAVE BIT(31) /* ` */ +/* out of space, but one for *@ would make sense, possibly others */ + +/* compile-time initialised, ASCII only */ +extern const uint32_t tpl_ctypes[128]; +/* run-time, contains C_IFS as well, full 2⁸ octet range */ +EXTERN uint32_t ksh_ctypes[256]; +/* first octet of $IFS, for concatenating "$*" */ +EXTERN char ifs0; + +/* external types */ + +/* !%,-.0‥9:@A‥Z[]_a‥z valid characters in alias names */ +#define C_ALIAS (CiALIAS | CiBRACK | CiCOLON | CiDIGIT | CiLOWER | CiMINUS | CiOCTAL | CiPERCT | CiUNDER | CiUPPER) +/* 0‥9A‥Za‥z alphanumerical */ +#define C_ALNUM (CiDIGIT | CiLOWER | CiOCTAL | CiUPPER) +/* 0‥9A‥Z_a‥z alphanumerical plus underscore (“word character”) */ +#define C_ALNUX (CiDIGIT | CiLOWER | CiOCTAL | CiUNDER | CiUPPER) +/* A‥Za‥z alphabetical (upper plus lower) */ +#define C_ALPHA (CiLOWER | CiUPPER) +/* A‥Z_a‥z alphabetical plus underscore (identifier lead) */ +#define C_ALPHX (CiLOWER | CiUNDER | CiUPPER) +/* \x01‥\x7F 7-bit ASCII except NUL */ +#define C_ASCII (CiALIAS | CiANGLE | CiBRACK | CiCNTRL | CiCOLON | CiCR | CiCURLY | CiDIGIT | CiEQUAL | CiGRAVE | CiHASH | CiLOWER | CiMINUS | CiNL | CiOCTAL | CiPERCT | CiPLUS | CiQC | CiQCL | CiQCM | CiQCX | CiQUEST | CiSP | CiSPX | CiSS | CiTAB | CiUNDER | CiUPPER) +/* \x09\x20 tab and space */ +#define C_BLANK (CiSP | CiTAB) +/* \x09\x20"' separator for completion */ +#define C_CFS (CiQC | CiSP | CiTAB) +/* \x00‥\x1F\x7F POSIX control characters */ +#define C_CNTRL (CiCNTRL | CiCR | CiNL | CiNUL | CiSPX | CiTAB) +/* 0‥9 decimal digits */ +#define C_DIGIT (CiDIGIT | CiOCTAL) +/* &();`| editor x_locate_word() command */ +#define C_EDCMD (CiGRAVE | CiQCL) +/* \x09\x0A\x20"&'():;<=>`| editor non-word characters */ +#define C_EDNWC (CiANGLE | CiCOLON | CiEQUAL | CiGRAVE | CiNL | CiQC | CiQCL | CiSP | CiTAB) +/* "#$&'()*:;<=>?[\\`{|} editor quotes for tab completion */ +#define C_EDQ (CiANGLE | CiCOLON | CiCURLY | CiEQUAL | CiGRAVE | CiHASH | CiQC | CiQCL | CiQCX | CiQUEST | CiSS) +/* !‥~ POSIX graphical (alphanumerical plus punctuation) */ +#define C_GRAPH (C_PUNCT | CiDIGIT | CiLOWER | CiOCTAL | CiUPPER) +/* A‥Fa‥f hex letter */ +#define C_HEXLT CiHEXLT +/* \x00 + $IFS IFS whitespace, IFS non-whitespace, NUL */ +#define C_IFS (CiIFS | CiNUL) +/* \x09\x0A\x20 IFS whitespace */ +#define C_IFSWS (CiNL | CiSP | CiTAB) +/* \x09\x0A\x20&();<>| (for the lexer) */ +#define C_LEX1 (CiANGLE | CiNL | CiQCL | CiSP | CiTAB) +/* a‥z lowercase letters */ +#define C_LOWER CiLOWER +/* not alnux or dollar separator for motion */ +#define C_MFS (CiALIAS | CiANGLE | CiBRACK | CiCNTRL | CiCOLON | CiCR | CiCURLY | CiEQUAL | CiGRAVE | CiHASH | CiMINUS | CiNL | CiNUL | CiPERCT | CiPLUS | CiQC | CiQCL | CiQCM | CiQCX | CiQUEST | CiSP | CiSPX | CiTAB) +/* 0‥7 octal digit */ +#define C_OCTAL CiOCTAL +/* !*+?@ pattern magical operator, except space */ +#define C_PATMO (CiPLUS | CiQUEST | CiVAR1) +/* \x20‥~ POSIX printable characters (graph plus space) */ +#define C_PRINT (C_GRAPH | CiSP) +/* !"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ POSIX punctuation */ +#define C_PUNCT (CiALIAS | CiANGLE | CiBRACK | CiCOLON | CiCURLY | CiEQUAL | CiGRAVE | CiHASH | CiMINUS | CiPERCT | CiPLUS | CiQC | CiQCL | CiQCM | CiQCX | CiQUEST | CiSS | CiUNDER) +/* \x09\x0A"#$&'()*;<=>?[\\]`| characters requiring quoting, minus space */ +#define C_QUOTE (CiANGLE | CiBRACK | CiEQUAL | CiGRAVE | CiHASH | CiNL | CiQC | CiQCL | CiQCX | CiQUEST | CiSS | CiTAB) +/* 0‥9A‥Fa‥f hexadecimal digit */ +#define C_SEDEC (CiDIGIT | CiHEXLT | CiOCTAL) +/* \x09‥\x0D\x20 POSIX space class */ +#define C_SPACE (CiCR | CiNL | CiSP | CiSPX | CiTAB) +/* +-=? substitution operations with word */ +#define C_SUB1 (CiEQUAL | CiMINUS | CiPLUS | CiQUEST) +/* #% substitution operations with pattern */ +#define C_SUB2 (CiHASH | CiPERCT) +/* A‥Z uppercase letters */ +#define C_UPPER CiUPPER +/* !#$*-?@ substitution parameters, other than positional */ +#define C_VAR1 (CiHASH | CiMINUS | CiQUEST | CiSS | CiVAR1) + +/* individual chars you might like */ +#define C_ANGLE CiANGLE /* <> angle brackets */ +#define C_COLON CiCOLON /* : colon */ +#define C_CR CiCR /* \x0D ASCII carriage return */ +#define C_DOLAR CiSS /* $ dollar sign */ +#define C_EQUAL CiEQUAL /* = equals sign */ +#define C_GRAVE CiGRAVE /* ` accent gravis */ +#define C_HASH CiHASH /* # hash sign */ +#define C_LF CiNL /* \x0A ASCII line feed */ +#define C_MINUS CiMINUS /* - hyphen-minus */ +#ifdef MKSH_WITH_TEXTMODE +#define C_NL (CiNL | CiCR) /* CR or LF under OS/2 TEXTMODE */ +#else +#define C_NL CiNL /* LF only like under Unix */ +#endif +#define C_NUL CiNUL /* \x00 ASCII NUL */ +#define C_PLUS CiPLUS /* + plus sign */ +#define C_QC CiQC /* "' quote characters */ +#define C_QUEST CiQUEST /* ? question mark */ +#define C_SPC CiSP /* \x20 ASCII space */ +#define C_TAB CiTAB /* \x09 ASCII horizontal tabulator */ +#define C_UNDER CiUNDER /* _ underscore */ + +/* identity transform of octet */ +#if defined(DEBUG) && defined(__GNUC__) && !defined(__ICC) && \ + !defined(__INTEL_COMPILER) && !defined(__SUNPRO_C) +extern unsigned int eek_ord; +#define ORD(c) ((size_t)(c) > 0xFF ? eek_ord : \ + ((unsigned int)(unsigned char)(c))) +#define ord(c) __builtin_choose_expr( \ + __builtin_types_compatible_p(__typeof__(c), char) || \ + __builtin_types_compatible_p(__typeof__(c), unsigned char), \ + ((unsigned int)(unsigned char)(c)), ({ \ + size_t ord_c = (c); \ + \ + if (ord_c > (size_t)0xFFU) \ + internal_errorf("%s:%d:ord(%zX)", \ + __FILE__, __LINE__, ord_c); \ + ((unsigned int)(unsigned char)(ord_c)); \ +})) +#else +#define ord(c) ((unsigned int)(unsigned char)(c)) +#define ORD(c) ord(c) /* may evaluate arguments twice */ +#endif +#if defined(MKSH_EBCDIC) || defined(MKSH_FAUX_EBCDIC) +EXTERN unsigned short ebcdic_map[256]; +EXTERN unsigned char ebcdic_rtt_toascii[256]; +EXTERN unsigned char ebcdic_rtt_fromascii[256]; +extern void ebcdic_init(void); +/* one-way to-ascii-or-high conversion, for POSIX locale ordering */ +#define asciibetical(c) ((unsigned int)ebcdic_map[(unsigned char)(c)]) +/* two-way round-trip conversion, for general use */ +#define rtt2asc(c) ebcdic_rtt_toascii[(unsigned char)(c)] +#define asc2rtt(c) ebcdic_rtt_fromascii[(unsigned char)(c)] +/* case-independent char comparison */ +#define ksh_eq(c,u,l) (ord(c) == ord(u) || ord(c) == ord(l)) +#else +#define asciibetical(c) ord(c) +#define rtt2asc(c) ((unsigned char)(c)) +#define asc2rtt(c) ((unsigned char)(c)) +#define ksh_eq(c,u,l) ((ord(c) | 0x20) == ord(l)) +#endif +/* control character foo */ +#ifdef MKSH_EBCDIC +#define ksh_isctrl(c) (ord(c) < 0x40 || ord(c) == 0xFF) +#else +#define ksh_isctrl(c) ((ord(c) & 0x7F) < 0x20 || ord(c) == 0x7F) +#endif +/* new fast character classes */ +#define ctype(c,t) tobool(ksh_ctypes[ord(c)] & (t)) +#define cinttype(c,t) ((c) >= 0 && (c) <= 0xFF ? \ + tobool(ksh_ctypes[(unsigned char)(c)] & (t)) : false) +/* helper functions */ +#define ksh_isdash(s) tobool(ord((s)[0]) == '-' && ord((s)[1]) == '\0') +/* invariant distance even in EBCDIC */ +#define ksh_tolower(c) (ctype(c, C_UPPER) ? (c) - 'A' + 'a' : (c)) +#define ksh_toupper(c) (ctype(c, C_LOWER) ? (c) - 'a' + 'A' : (c)) +/* strictly speaking rtt2asc() here, but this works even in EBCDIC */ +#define ksh_numdig(c) (ord(c) - ORD('0')) +#define ksh_numuc(c) (rtt2asc(c) - rtt2asc('A')) +#define ksh_numlc(c) (rtt2asc(c) - rtt2asc('a')) +#define ksh_toctrl(c) asc2rtt(ord(c) == ORD('?') ? 0x7F : rtt2asc(c) & 0x9F) +#define ksh_unctrl(c) asc2rtt(rtt2asc(c) ^ 0x40U) + +/* Argument parsing for built-in commands and getopts command */ + +/* Values for Getopt.flags */ +#define GF_ERROR BIT(0) /* call errorf() if there is an error */ +#define GF_PLUSOPT BIT(1) /* allow +c as an option */ +#define GF_NONAME BIT(2) /* don't print argv[0] in errors */ + +/* Values for Getopt.info */ +#define GI_MINUS BIT(0) /* an option started with -... */ +#define GI_PLUS BIT(1) /* an option started with +... */ +#define GI_MINUSMINUS BIT(2) /* arguments were ended with -- */ + +/* in case some OS defines these */ +#undef optarg +#undef optind + +typedef struct { + const char *optarg; + int optind; + int uoptind; /* what user sees in $OPTIND */ + int flags; /* see GF_* */ + int info; /* see GI_* */ + unsigned int p; /* 0 or index into argv[optind - 1] */ + char buf[2]; /* for bad option OPTARG value */ +} Getopt; + +EXTERN Getopt builtin_opt; /* for shell builtin commands */ +EXTERN Getopt user_opt; /* parsing state for getopts builtin command */ + +/* This for co-processes */ + +/* something that won't (realisticly) wrap */ +typedef int Coproc_id; + +struct coproc { + void *job; /* 0 or job of co-process using input pipe */ + int read; /* pipe from co-process's stdout */ + int readw; /* other side of read (saved temporarily) */ + int write; /* pipe to co-process's stdin */ + int njobs; /* number of live jobs using output pipe */ + Coproc_id id; /* id of current output pipe */ +}; +EXTERN struct coproc coproc; + +#ifndef MKSH_NOPROSPECTOFWORK +/* used in jobs.c and by coprocess stuff in exec.c and select() calls */ +EXTERN sigset_t sm_default, sm_sigchld; +#endif + +/* name of called builtin function (used by error functions) */ +EXTERN const char *builtin_argv0; +/* is called builtin a POSIX special builtin? (error functions only) */ +EXTERN bool builtin_spec; + +/* current working directory */ +EXTERN char *current_wd; + +/* input line size */ +#ifdef MKSH_SMALL +#define LINE (4096 - ALLOC_OVERHEAD) +#else +#define LINE (16384 - ALLOC_OVERHEAD) +#endif +/* columns and lines of the tty */ +EXTERN mksh_ari_t x_cols E_INIT(80); +EXTERN mksh_ari_t x_lins E_INIT(24); + + +/* Determine the location of the system (common) profile */ + +#ifndef MKSH_DEFAULT_PROFILEDIR +#define MKSH_DEFAULT_PROFILEDIR MKSH_UNIXROOT "/etc" +#endif + +#define MKSH_SYSTEM_PROFILE MKSH_DEFAULT_PROFILEDIR "/profile" +#define MKSH_SUID_PROFILE MKSH_DEFAULT_PROFILEDIR "/suid_profile" + + +/* Used by v_evaluate() and setstr() to control action when error occurs */ +#define KSH_UNWIND_ERROR 0 /* unwind the stack (kshlongjmp) */ +#define KSH_RETURN_ERROR 1 /* return 1/0 for success/failure */ + +/* + * Shell file I/O routines + */ + +#define SHF_BSIZE 512 + +#define shf_fileno(shf) ((shf)->fd) +#define shf_setfileno(shf,nfd) ((shf)->fd = (nfd)) +#define shf_getc_i(shf) ((shf)->rnleft > 0 ? \ + (shf)->rnleft--, (int)ord(*(shf)->rp++) : \ + shf_getchar(shf)) +#define shf_putc_i(c, shf) ((shf)->wnleft == 0 ? \ + shf_putchar((uint8_t)(c), (shf)) : \ + ((shf)->wnleft--, *(shf)->wp++ = (c))) +#define shf_eof(shf) ((shf)->flags & SHF_EOF) +#define shf_error(shf) ((shf)->flags & SHF_ERROR) +#define shf_errno(shf) ((shf)->errnosv) +#define shf_clearerr(shf) ((shf)->flags &= ~(SHF_EOF | SHF_ERROR)) + +/* Flags passed to shf_*open() */ +#define SHF_RD 0x0001 +#define SHF_WR 0x0002 +#define SHF_RDWR (SHF_RD|SHF_WR) +#define SHF_ACCMODE 0x0003 /* mask */ +#define SHF_GETFL 0x0004 /* use fcntl() to figure RD/WR flags */ +#define SHF_UNBUF 0x0008 /* unbuffered I/O */ +#define SHF_CLEXEC 0x0010 /* set close on exec flag */ +#define SHF_MAPHI 0x0020 /* make fd > FDBASE (and close orig) + * (shf_open() only) */ +#define SHF_DYNAMIC 0x0040 /* string: increase buffer as needed */ +#define SHF_INTERRUPT 0x0080 /* EINTR in read/write causes error */ +/* Flags used internally */ +#define SHF_STRING 0x0100 /* a string, not a file */ +#define SHF_ALLOCS 0x0200 /* shf and shf->buf were alloc()ed */ +#define SHF_ALLOCB 0x0400 /* shf->buf was alloc()ed */ +#define SHF_ERROR 0x0800 /* read()/write() error */ +#define SHF_EOF 0x1000 /* read eof (sticky) */ +#define SHF_READING 0x2000 /* currently reading: rnleft,rp valid */ +#define SHF_WRITING 0x4000 /* currently writing: wnleft,wp valid */ + + +struct shf { + Area *areap; /* area shf/buf were allocated in */ + unsigned char *rp; /* read: current position in buffer */ + unsigned char *wp; /* write: current position in buffer */ + unsigned char *buf; /* buffer */ + ssize_t bsize; /* actual size of buf */ + ssize_t rbsize; /* size of buffer (1 if SHF_UNBUF) */ + ssize_t rnleft; /* read: how much data left in buffer */ + ssize_t wbsize; /* size of buffer (0 if SHF_UNBUF) */ + ssize_t wnleft; /* write: how much space left in buffer */ + int flags; /* see SHF_* */ + int fd; /* file descriptor */ + int errnosv; /* saved value of errno after error */ +}; + +extern struct shf shf_iob[]; + +struct table { + Area *areap; /* area to allocate entries */ + struct tbl **tbls; /* hashed table items */ + size_t nfree; /* free table entries */ + uint8_t tshift; /* table size (2^tshift) */ +}; + +/* table item */ +struct tbl { + /* Area to allocate from */ + Area *areap; + /* value */ + union { + char *s; /* string */ + mksh_ari_t i; /* integer */ + mksh_uari_t u; /* unsigned integer */ + int (*f)(const char **); /* built-in command */ + struct op *t; /* "function" tree */ + } val; + union { + struct tbl *array; /* array values */ + const char *fpath; /* temporary path to undef function */ + } u; + union { + int field; /* field with for -L/-R/-Z */ + int errnov; /* CEXEC/CTALIAS */ + } u2; + union { + uint32_t hval; /* hash(name) */ + uint32_t index; /* index for an array */ + } ua; + /* + * command type (see below), base (if INTEGER), + * offset from val.s of value (if EXPORT) + */ + int type; + /* flags (see below) */ + uint32_t flag; + + /* actually longer: name (variable length) */ + char name[4]; +}; + +EXTERN struct tbl *vtemp; +/* set by isglobal(), global() and local() */ +EXTERN bool last_lookup_was_array; + +/* common flag bits */ +#define ALLOC BIT(0) /* val.s has been allocated */ +#define DEFINED BIT(1) /* is defined in block */ +#define ISSET BIT(2) /* has value, vp->val.[si] */ +#define EXPORT BIT(3) /* exported variable/function */ +#define TRACE BIT(4) /* var: user flagged, func: execution tracing */ +/* (start non-common flags at 8) */ +/* flag bits used for variables */ +#define SPECIAL BIT(8) /* PATH, IFS, SECONDS, etc */ +#define INTEGER BIT(9) /* val.i contains integer value */ +#define RDONLY BIT(10) /* read-only variable */ +#define LOCAL BIT(11) /* for local typeset() */ +#define ARRAY BIT(13) /* array */ +#define LJUST BIT(14) /* left justify */ +#define RJUST BIT(15) /* right justify */ +#define ZEROFIL BIT(16) /* 0 filled if RJUSTIFY, strip 0s if LJUSTIFY */ +#define LCASEV BIT(17) /* convert to lower case */ +#define UCASEV_AL BIT(18) /* convert to upper case / autoload function */ +#define INT_U BIT(19) /* unsigned integer */ +#define INT_L BIT(20) /* long integer (no-op but used as magic) */ +#define IMPORT BIT(21) /* flag to typeset(): no arrays, must have = */ +#define LOCAL_COPY BIT(22) /* with LOCAL - copy attrs from existing var */ +#define EXPRINEVAL BIT(23) /* contents currently being evaluated */ +#define EXPRLVALUE BIT(24) /* useable as lvalue (temp flag) */ +#define AINDEX BIT(25) /* array index >0 = ua.index filled in */ +#define ASSOC BIT(26) /* ARRAY ? associative : reference */ +/* flag bits used for taliases/builtins/aliases/keywords/functions */ +#define KEEPASN BIT(8) /* keep command assignments (eg, var=x cmd) */ +#define FINUSE BIT(9) /* function being executed */ +#define FDELETE BIT(10) /* function deleted while it was executing */ +#define FKSH BIT(11) /* function defined with function x (vs x()) */ +#define SPEC_BI BIT(12) /* a POSIX special builtin */ +#define LOWER_BI BIT(13) /* (with LOW_BI) override even w/o flags */ +#define LOW_BI BIT(14) /* external utility overrides built-in one */ +#define DECL_UTIL BIT(15) /* is declaration utility */ +#define DECL_FWDR BIT(16) /* is declaration utility forwarder */ + +/* + * Attributes that can be set by the user (used to decide if an unset + * param should be repoted by set/typeset). Does not include ARRAY or + * LOCAL. + */ +#define USERATTRIB (EXPORT|INTEGER|RDONLY|LJUST|RJUST|ZEROFIL|\ + LCASEV|UCASEV_AL|INT_U|INT_L) + +#define arrayindex(vp) ((unsigned long)((vp)->flag & AINDEX ? \ + (vp)->ua.index : 0)) + +enum namerefflag { + SRF_NOP, + SRF_ENABLE, + SRF_DISABLE +}; + +/* command types */ +#define CNONE 0 /* undefined */ +#define CSHELL 1 /* built-in */ +#define CFUNC 2 /* function */ +#define CEXEC 4 /* executable command */ +#define CALIAS 5 /* alias */ +#define CKEYWD 6 /* keyword */ +#define CTALIAS 7 /* tracked alias */ + +/* Flags for findcom()/comexec() */ +#define FC_SPECBI BIT(0) /* special builtin */ +#define FC_FUNC BIT(1) /* function */ +#define FC_NORMBI BIT(2) /* not special builtin */ +#define FC_BI (FC_SPECBI | FC_NORMBI) +#define FC_PATH BIT(3) /* do path search */ +#define FC_DEFPATH BIT(4) /* use default path in path search */ +#define FC_WHENCE BIT(5) /* for use by command and whence */ + +#define AF_ARGV_ALLOC 0x1 /* argv[] array allocated */ +#define AF_ARGS_ALLOCED 0x2 /* argument strings allocated */ +#define AI_ARGV(a, i) ((i) == 0 ? (a).argv[0] : (a).argv[(i) - (a).skip]) +#define AI_ARGC(a) ((a).ai_argc - (a).skip) + +/* Argument info. Used for $#, $* for shell, functions, includes, etc. */ +struct arg_info { + const char **argv; + int flags; /* AF_* */ + int ai_argc; + int skip; /* first arg is argv[0], second is argv[1 + skip] */ +}; + +/* + * activation record for function blocks + */ +struct block { + Area area; /* area to allocate things */ + const char **argv; + char *error; /* error handler */ + char *exit; /* exit handler */ + struct block *next; /* enclosing block */ + struct table vars; /* local variables */ + struct table funs; /* local functions */ + Getopt getopts_state; + int argc; + int flags; /* see BF_* */ +}; + +/* Values for struct block.flags */ +#define BF_DOGETOPTS BIT(0) /* save/restore getopts state */ +#define BF_STOPENV BIT(1) /* do not export further */ + +/* + * Used by ktwalk() and ktnext() routines. + */ +struct tstate { + struct tbl **next; + ssize_t left; +}; + +EXTERN struct table taliases; /* tracked aliases */ +EXTERN struct table builtins; /* built-in commands */ +EXTERN struct table aliases; /* aliases */ +EXTERN struct table keywords; /* keywords */ +#ifndef MKSH_NOPWNAM +EXTERN struct table homedirs; /* homedir() cache */ +#endif + +struct builtin { + const char *name; + int (*func)(const char **); +}; + +extern const struct builtin mkshbuiltins[]; + +/* values for set_prompt() */ +#define PS1 0 /* command */ +#define PS2 1 /* command continuation */ + +EXTERN char *path; /* copy of either PATH or def_path */ +EXTERN const char *def_path; /* path to use if PATH not set */ +EXTERN char *tmpdir; /* TMPDIR value */ +EXTERN const char *prompt; +EXTERN uint8_t cur_prompt; /* PS1 or PS2 */ +EXTERN int current_lineno; /* LINENO value */ + +/* + * Description of a command or an operation on commands. + */ +struct op { + const char **args; /* arguments to a command */ + char **vars; /* variable assignments */ + struct ioword **ioact; /* IO actions (eg, < > >>) */ + struct op *left, *right; /* descendents */ + char *str; /* word for case; identifier for for, + * select, and functions; + * path to execute for TEXEC; + * time hook for TCOM. + */ + int lineno; /* TCOM/TFUNC: LINENO for this */ + short type; /* operation type, see below */ + /* WARNING: newtp(), tcopy() use evalflags = 0 to clear union */ + union { + /* TCOM: arg expansion eval() flags */ + short evalflags; + /* TFUNC: function x (vs x()) */ + short ksh_func; + /* TPAT: termination character */ + char charflag; + } u; +}; + +/* Tree.type values */ +#define TEOF 0 +#define TCOM 1 /* command */ +#define TPAREN 2 /* (c-list) */ +#define TPIPE 3 /* a | b */ +#define TLIST 4 /* a ; b */ +#define TOR 5 /* || */ +#define TAND 6 /* && */ +#define TBANG 7 /* ! */ +#define TDBRACKET 8 /* [[ .. ]] */ +#define TFOR 9 +#define TSELECT 10 +#define TCASE 11 +#define TIF 12 +#define TWHILE 13 +#define TUNTIL 14 +#define TELIF 15 +#define TPAT 16 /* pattern in case */ +#define TBRACE 17 /* {c-list} */ +#define TASYNC 18 /* c & */ +#define TFUNCT 19 /* function name { command; } */ +#define TTIME 20 /* time pipeline */ +#define TEXEC 21 /* fork/exec eval'd TCOM */ +#define TCOPROC 22 /* coprocess |& */ + +/* + * prefix codes for words in command tree + */ +#define EOS 0 /* end of string */ +#define CHAR 1 /* unquoted character */ +#define QCHAR 2 /* quoted character */ +#define COMSUB 3 /* $() substitution (0 terminated) */ +#define EXPRSUB 4 /* $(()) substitution (0 terminated) */ +#define OQUOTE 5 /* opening " or ' */ +#define CQUOTE 6 /* closing " or ' */ +#define OSUBST 7 /* opening ${ subst (followed by { or X) */ +#define CSUBST 8 /* closing } of above (followed by } or X) */ +#define OPAT 9 /* open pattern: *(, @(, etc. */ +#define SPAT 10 /* separate pattern: | */ +#define CPAT 11 /* close pattern: ) */ +#define ADELIM 12 /* arbitrary delimiter: ${foo:2:3} ${foo/bar/baz} */ +#define FUNSUB 14 /* ${ foo;} substitution (NUL terminated) */ +#define VALSUB 15 /* ${|foo;} substitution (NUL terminated) */ +#define COMASUB 16 /* `…` substitution (COMSUB but expand aliases) */ +#define FUNASUB 17 /* function substitution but expand aliases */ + +/* + * IO redirection + */ +struct ioword { + char *ioname; /* filename (unused if heredoc) */ + char *delim; /* delimiter for <<, <<- */ + char *heredoc; /* content of heredoc */ + unsigned short ioflag; /* action (below) */ + short unit; /* unit (fd) affected */ +}; + +/* ioword.flag - type of redirection */ +#define IOTYPE 0xF /* type: bits 0:3 */ +#define IOREAD 0x1 /* < */ +#define IOWRITE 0x2 /* > */ +#define IORDWR 0x3 /* <>: todo */ +#define IOHERE 0x4 /* << (here file) */ +#define IOCAT 0x5 /* >> */ +#define IODUP 0x6 /* <&/>& */ +#define IOEVAL BIT(4) /* expand in << */ +#define IOSKIP BIT(5) /* <<-, skip ^\t* */ +#define IOCLOB BIT(6) /* >|, override -o noclobber */ +#define IORDUP BIT(7) /* x<&y (as opposed to x>&y) */ +#define IONAMEXP BIT(8) /* name has been expanded */ +#define IOBASH BIT(9) /* &> etc. */ +#define IOHERESTR BIT(10) /* <<< (here string) */ +#define IONDELIM BIT(11) /* null delimiter (<<) */ + +/* execute/exchild flags */ +#define XEXEC BIT(0) /* execute without forking */ +#define XFORK BIT(1) /* fork before executing */ +#define XBGND BIT(2) /* command & */ +#define XPIPEI BIT(3) /* input is pipe */ +#define XPIPEO BIT(4) /* output is pipe */ +#define XXCOM BIT(5) /* `...` command */ +#define XPCLOSE BIT(6) /* exchild: close close_fd in parent */ +#define XCCLOSE BIT(7) /* exchild: close close_fd in child */ +#define XERROK BIT(8) /* non-zero exit ok (for set -e) */ +#define XCOPROC BIT(9) /* starting a co-process */ +#define XTIME BIT(10) /* timing TCOM command */ +#define XPIPEST BIT(11) /* want PIPESTATUS */ + +/* + * flags to control expansion of words (assumed by t->evalflags to fit + * in a short) + */ +#define DOBLANK BIT(0) /* perform blank interpretation */ +#define DOGLOB BIT(1) /* expand [?* */ +#define DOPAT BIT(2) /* quote *?[ */ +#define DOTILDE BIT(3) /* normal ~ expansion (first char) */ +#define DONTRUNCOMMAND BIT(4) /* do not run $(command) things */ +#define DOASNTILDE BIT(5) /* assignment ~ expansion (after =, :) */ +#define DOBRACE BIT(6) /* used by expand(): do brace expansion */ +#define DOMAGIC BIT(7) /* used by expand(): string contains MAGIC */ +#define DOTEMP BIT(8) /* dito: in word part of ${..[%#=?]..} */ +#define DOVACHECK BIT(9) /* var assign check (for typeset, set, etc) */ +#define DOMARKDIRS BIT(10) /* force markdirs behaviour */ +#define DOTCOMEXEC BIT(11) /* not an eval flag, used by sh -c hack */ +#define DOSCALAR BIT(12) /* change field handling to non-list context */ +#define DOHEREDOC BIT(13) /* change scalar handling to heredoc body */ +#define DOHERESTR BIT(14) /* append a newline char */ + +#define X_EXTRA 20 /* this many extra bytes in X string */ + +typedef struct XString { + /* beginning of string */ + char *beg; + /* length of allocated area, minus safety margin */ + size_t len; + /* end of string */ + char *end; + /* memory area used */ + Area *areap; +} XString; + +/* initialise expandable string */ +#define XinitN(xs, length, area) do { \ + (xs).len = (length); \ + (xs).areap = (area); \ + (xs).beg = alloc((xs).len + X_EXTRA, (xs).areap); \ + (xs).end = (xs).beg + (xs).len; \ +} while (/* CONSTCOND */ 0) +#define Xinit(xs, xp, length, area) do { \ + XinitN((xs), (length), (area)); \ + (xp) = (xs).beg; \ +} while (/* CONSTCOND */ 0) + +/* stuff char into string */ +#define Xput(xs, xp, c) (*xp++ = (c)) + +/* check if there are at least n bytes left */ +#define XcheckN(xs, xp, n) do { \ + ssize_t more = ((xp) + (n)) - (xs).end; \ + if (more > 0) \ + (xp) = Xcheck_grow(&(xs), (xp), (size_t)more); \ +} while (/* CONSTCOND */ 0) + +/* check for overflow, expand string */ +#define Xcheck(xs, xp) XcheckN((xs), (xp), 1) + +/* free string */ +#define Xfree(xs, xp) afree((xs).beg, (xs).areap) + +/* close, return string */ +#define Xclose(xs, xp) aresize((xs).beg, (xp) - (xs).beg, (xs).areap) + +/* beginning of string */ +#define Xstring(xs, xp) ((xs).beg) + +#define Xnleft(xs, xp) ((xs).end - (xp)) /* may be less than 0 */ +#define Xlength(xs, xp) ((xp) - (xs).beg) +#define Xsize(xs, xp) ((xs).end - (xs).beg) +#define Xsavepos(xs, xp) ((xp) - (xs).beg) +#define Xrestpos(xs, xp, n) ((xs).beg + (n)) + +char *Xcheck_grow(XString *, const char *, size_t); + +/* + * expandable vector of generic pointers + */ + +typedef struct { + /* beginning of allocated area */ + void **beg; + /* currently used number of entries */ + size_t len; + /* allocated number of entries */ + size_t siz; +} XPtrV; + +#define XPinit(x, n) do { \ + (x).siz = (n); \ + (x).len = 0; \ + (x).beg = alloc2((x).siz, sizeof(void *), ATEMP); \ +} while (/* CONSTCOND */ 0) \ + +#define XPput(x, p) do { \ + if ((x).len == (x).siz) { \ + (x).beg = aresize2((x).beg, (x).siz, \ + 2 * sizeof(void *), ATEMP); \ + (x).siz <<= 1; \ + } \ + (x).beg[(x).len++] = (p); \ +} while (/* CONSTCOND */ 0) + +#define XPptrv(x) ((x).beg) +#define XPsize(x) ((x).len) +#define XPclose(x) aresize2((x).beg, XPsize(x), sizeof(void *), ATEMP) +#define XPfree(x) afree((x).beg, ATEMP) + +/* for print_columns */ + +struct columnise_opts { + struct shf *shf; + char linesep; + bool do_last; + bool prefcol; +}; + +/* + * Lexer internals + */ + +typedef struct source Source; +struct source { + /* input buffer */ + XString xs; + /* memory area, also checked in reclaim() */ + Area *areap; + /* stacked source */ + Source *next; + /* input pointer */ + const char *str; + /* start of current buffer */ + const char *start; + /* input file name */ + const char *file; + /* extra data */ + union { + /* string[] */ + const char **strv; + /* shell file */ + struct shf *shf; + /* alias (SF_HASALIAS) */ + struct tbl *tblp; + /* (also for SREREAD) */ + char *freeme; + } u; + /* flags */ + int flags; + /* input type */ + int type; + /* line number */ + int line; + /* line the error occurred on (0 if not set) */ + int errline; + /* buffer for ungetsc() (SREREAD) and alias (SALIAS) */ + char ugbuf[2]; +}; + +/* Source.type values */ +#define SEOF 0 /* input EOF */ +#define SFILE 1 /* file input */ +#define SSTDIN 2 /* read stdin */ +#define SSTRING 3 /* string */ +#define SWSTR 4 /* string without \n */ +#define SWORDS 5 /* string[] */ +#define SWORDSEP 6 /* string[] separator */ +#define SALIAS 7 /* alias expansion */ +#define SREREAD 8 /* read ahead to be re-scanned */ +#define SSTRINGCMDLINE 9 /* string from "mksh -c ..." */ + +/* Source.flags values */ +#define SF_ECHO BIT(0) /* echo input to shlout */ +#define SF_ALIAS BIT(1) /* faking space at end of alias */ +#define SF_ALIASEND BIT(2) /* faking space at end of alias */ +#define SF_TTY BIT(3) /* type == SSTDIN & it is a tty */ +#define SF_HASALIAS BIT(4) /* u.tblp valid (SALIAS, SEOF) */ +#define SF_MAYEXEC BIT(5) /* special sh -c optimisation hack */ + +typedef union { + int i; + char *cp; + char **wp; + struct op *o; + struct ioword *iop; +} YYSTYPE; + +/* If something is added here, add it to tokentab[] in syn.c as well */ +#define LWORD 256 +#define LOGAND 257 /* && */ +#define LOGOR 258 /* || */ +#define BREAK 259 /* ;; */ +#define IF 260 +#define THEN 261 +#define ELSE 262 +#define ELIF 263 +#define FI 264 +#define CASE 265 +#define ESAC 266 +#define FOR 267 +#define SELECT 268 +#define WHILE 269 +#define UNTIL 270 +#define DO 271 +#define DONE 272 +#define IN 273 +#define FUNCTION 274 +#define TIME 275 +#define REDIR 276 +#define MDPAREN 277 /* (( )) */ +#define BANG 278 /* ! */ +#define DBRACKET 279 /* [[ .. ]] */ +#define COPROC 280 /* |& */ +#define BRKEV 281 /* ;| */ +#define BRKFT 282 /* ;& */ +#define YYERRCODE 300 + +/* flags to yylex */ +#define CONTIN BIT(0) /* skip new lines to complete command */ +#define ONEWORD BIT(1) /* single word for substitute() */ +#define ALIAS BIT(2) /* recognise alias */ +#define KEYWORD BIT(3) /* recognise keywords */ +#define LETEXPR BIT(4) /* get expression inside (( )) */ +#define CMDASN BIT(5) /* parse x[1 & 2] as one word, for typeset */ +#define HEREDOC BIT(6) /* parsing a here document body */ +#define ESACONLY BIT(7) /* only accept esac keyword */ +#define CMDWORD BIT(8) /* parsing simple command (alias related) */ +#define HEREDELIM BIT(9) /* parsing <<,<<- delimiter */ +#define LQCHAR BIT(10) /* source string contains QCHAR */ + +#define HERES 10 /* max number of << in line */ + +#ifdef MKSH_EBCDIC +#define CTRL_AT (0x00U) +#define CTRL_A (0x01U) +#define CTRL_B (0x02U) +#define CTRL_C (0x03U) +#define CTRL_D (0x37U) +#define CTRL_E (0x2DU) +#define CTRL_F (0x2EU) +#define CTRL_G (0x2FU) +#define CTRL_H (0x16U) +#define CTRL_I (0x05U) +#define CTRL_J (0x15U) +#define CTRL_K (0x0BU) +#define CTRL_L (0x0CU) +#define CTRL_M (0x0DU) +#define CTRL_N (0x0EU) +#define CTRL_O (0x0FU) +#define CTRL_P (0x10U) +#define CTRL_Q (0x11U) +#define CTRL_R (0x12U) +#define CTRL_S (0x13U) +#define CTRL_T (0x3CU) +#define CTRL_U (0x3DU) +#define CTRL_V (0x32U) +#define CTRL_W (0x26U) +#define CTRL_X (0x18U) +#define CTRL_Y (0x19U) +#define CTRL_Z (0x3FU) +#define CTRL_BO (0x27U) +#define CTRL_BK (0x1CU) +#define CTRL_BC (0x1DU) +#define CTRL_CA (0x1EU) +#define CTRL_US (0x1FU) +#define CTRL_QM (0x07U) +#else +#define CTRL_AT (0x00U) +#define CTRL_A (0x01U) +#define CTRL_B (0x02U) +#define CTRL_C (0x03U) +#define CTRL_D (0x04U) +#define CTRL_E (0x05U) +#define CTRL_F (0x06U) +#define CTRL_G (0x07U) +#define CTRL_H (0x08U) +#define CTRL_I (0x09U) +#define CTRL_J (0x0AU) +#define CTRL_K (0x0BU) +#define CTRL_L (0x0CU) +#define CTRL_M (0x0DU) +#define CTRL_N (0x0EU) +#define CTRL_O (0x0FU) +#define CTRL_P (0x10U) +#define CTRL_Q (0x11U) +#define CTRL_R (0x12U) +#define CTRL_S (0x13U) +#define CTRL_T (0x14U) +#define CTRL_U (0x15U) +#define CTRL_V (0x16U) +#define CTRL_W (0x17U) +#define CTRL_X (0x18U) +#define CTRL_Y (0x19U) +#define CTRL_Z (0x1AU) +#define CTRL_BO (0x1BU) +#define CTRL_BK (0x1CU) +#define CTRL_BC (0x1DU) +#define CTRL_CA (0x1EU) +#define CTRL_US (0x1FU) +#define CTRL_QM (0x7FU) +#endif + +#define IDENT 64 + +EXTERN Source *source; /* yyparse/yylex source */ +EXTERN YYSTYPE yylval; /* result from yylex */ +EXTERN struct ioword *heres[HERES], **herep; +EXTERN char ident[IDENT + 1]; + +EXTERN char **history; /* saved commands */ +EXTERN char **histptr; /* last history item */ +EXTERN mksh_ari_t histsize; /* history size */ + +/* flags to histsave */ +#define HIST_FLUSH 0 +#define HIST_QUEUE 1 +#define HIST_APPEND 2 +#define HIST_STORE 3 +#define HIST_NOTE 4 + +/* user and system time of last j_waitjed job */ +EXTERN struct timeval j_usrtime, j_systime; + +#define notok2mul(max, val, c) (((val) != 0) && ((c) != 0) && \ + (((max) / (c)) < (val))) +#define notok2add(max, val, c) ((val) > ((max) - (c))) +#define notoktomul(val, cnst) notok2mul(SIZE_MAX, (val), (cnst)) +#define notoktoadd(val, cnst) notok2add(SIZE_MAX, (val), (cnst)) +#define checkoktoadd(val, cnst) do { \ + if (notoktoadd((val), (cnst))) \ + internal_errorf(Tintovfl, (size_t)(val), \ + '+', (size_t)(cnst)); \ +} while (/* CONSTCOND */ 0) + + +/* lalloc.c */ +void ainit(Area *); +void afreeall(Area *); +/* these cannot fail and can take NULL (not for ap) */ +#define alloc(n, ap) aresize(NULL, (n), (ap)) +#define alloc2(m, n, ap) aresize2(NULL, (m), (n), (ap)) +void *aresize(void *, size_t, Area *); +void *aresize2(void *, size_t, size_t, Area *); +void afree(void *, Area *); /* can take NULL */ +/* edit.c */ +#ifndef MKSH_NO_CMDLINE_EDITING +#ifndef MKSH_SMALL +int x_bind(const char *, const char *, bool, bool); +#else +int x_bind(const char *, const char *, bool); +#endif +void x_init(void); +#ifdef DEBUG_LEAKS +void x_done(void); +#endif +int x_read(char *); +#endif +void x_mkraw(int, mksh_ttyst *, bool); +void x_initterm(const char *); +/* eval.c */ +char *substitute(const char *, int); +char **eval(const char **, int); +char *evalstr(const char *cp, int); +char *evalonestr(const char *cp, int); +char *debunk(char *, const char *, size_t); +void expand(const char *, XPtrV *, int); +int glob_str(char *, XPtrV *, bool); +char *do_tilde(char *); +/* exec.c */ +int execute(struct op * volatile, volatile int, volatile int * volatile); +int c_builtin(const char **); +struct tbl *get_builtin(const char *); +struct tbl *findfunc(const char *, uint32_t, bool); +int define(const char *, struct op *); +const char *builtin(const char *, int (*)(const char **)); +struct tbl *findcom(const char *, int); +void flushcom(bool); +int search_access(const char *, int); +const char *search_path(const char *, const char *, int, int *); +void pr_menu(const char * const *); +void pr_list(struct columnise_opts *, char * const *); +int herein(struct ioword *, char **); +/* expr.c */ +int evaluate(const char *, mksh_ari_t *, int, bool); +int v_evaluate(struct tbl *, const char *, volatile int, bool); +/* UTF-8 stuff */ +size_t utf_mbtowc(unsigned int *, const char *); +size_t utf_wctomb(char *, unsigned int); +int utf_widthadj(const char *, const char **); +size_t utf_mbswidth(const char *) MKSH_A_PURE; +const char *utf_skipcols(const char *, int, int *); +size_t utf_ptradj(const char *) MKSH_A_PURE; +#ifdef MIRBSD_BOOTFLOPPY +#define utf_wcwidth(i) wcwidth((wchar_t)(i)) +#else +int utf_wcwidth(unsigned int) MKSH_A_PURE; +#endif +int ksh_access(const char *, int); +struct tbl *tempvar(const char *); +/* funcs.c */ +int c_hash(const char **); +int c_pwd(const char **); +int c_print(const char **); +#ifdef MKSH_PRINTF_BUILTIN +int c_printf(const char **); +#endif +int c_whence(const char **); +int c_command(const char **); +int c_typeset(const char **); +bool valid_alias_name(const char *); +int c_alias(const char **); +int c_unalias(const char **); +int c_let(const char **); +int c_jobs(const char **); +#ifndef MKSH_UNEMPLOYED +int c_fgbg(const char **); +#endif +int c_kill(const char **); +void getopts_reset(int); +int c_getopts(const char **); +#ifndef MKSH_NO_CMDLINE_EDITING +int c_bind(const char **); +#endif +int c_shift(const char **); +int c_umask(const char **); +int c_dot(const char **); +int c_wait(const char **); +int c_read(const char **); +int c_eval(const char **); +int c_trap(const char **); +int c_brkcont(const char **); +int c_exitreturn(const char **); +int c_set(const char **); +int c_unset(const char **); +int c_ulimit(const char **); +int c_times(const char **); +int timex(struct op *, int, volatile int *); +void timex_hook(struct op *, char ** volatile *); +int c_exec(const char **); +int c_test(const char **); +#if HAVE_MKNOD +int c_mknod(const char **); +#endif +int c_realpath(const char **); +int c_rename(const char **); +int c_cat(const char **); +int c_sleep(const char **); +/* histrap.c */ +void init_histvec(void); +void hist_init(Source *); +#if HAVE_PERSISTENT_HISTORY +void hist_finish(void); +#endif +void histsave(int *, const char *, int, bool); +#if !defined(MKSH_SMALL) && HAVE_PERSISTENT_HISTORY +bool histsync(void); +#endif +int c_fc(const char **); +void sethistsize(mksh_ari_t); +#if HAVE_PERSISTENT_HISTORY +void sethistfile(const char *); +#endif +#if !defined(MKSH_NO_CMDLINE_EDITING) && !MKSH_S_NOVI +char **histpos(void) MKSH_A_PURE; +int histnum(int); +#endif +int findhist(int, int, const char *, bool) MKSH_A_PURE; +char **hist_get_newest(bool); +void inittraps(void); +void alarm_init(void); +Trap *gettrap(const char *, bool, bool); +void trapsig(int); +void intrcheck(void); +int fatal_trap_check(void); +int trap_pending(void); +void runtraps(int intr); +void runtrap(Trap *, bool); +void cleartraps(void); +void restoresigs(void); +void settrap(Trap *, const char *); +bool block_pipe(void); +void restore_pipe(void); +int setsig(Trap *, sig_t, int); +void setexecsig(Trap *, int); +#if HAVE_FLOCK || HAVE_LOCK_FCNTL +void mksh_lockfd(int); +void mksh_unlkfd(int); +#endif +/* jobs.c */ +void j_init(void); +void j_exit(void); +#ifndef MKSH_UNEMPLOYED +void j_change(void); +#endif +int exchild(struct op *, int, volatile int *, int); +void startlast(void); +int waitlast(void); +int waitfor(const char *, int *); +int j_kill(const char *, int); +#ifndef MKSH_UNEMPLOYED +int j_resume(const char *, int); +#endif +#if !defined(MKSH_UNEMPLOYED) && HAVE_GETSID +void j_suspend(void); +#endif +int j_jobs(const char *, int, int); +void j_notify(void); +pid_t j_async(void); +int j_stopped_running(void); +/* lex.c */ +int yylex(int); +void yyskiputf8bom(void); +void yyerror(const char *, ...) + MKSH_A_NORETURN + MKSH_A_FORMAT(__printf__, 1, 2); +Source *pushs(int, Area *); +void set_prompt(int, Source *); +int pprompt(const char *, int); +/* main.c */ +int include(const char *, int, const char **, bool); +int command(const char *, int); +int shell(Source * volatile, volatile int); +/* argument MUST NOT be 0 */ +void unwind(int) MKSH_A_NORETURN; +void newenv(int); +void quitenv(struct shf *); +void cleanup_parents_env(void); +void cleanup_proc_env(void); +void errorf(const char *, ...) + MKSH_A_NORETURN + MKSH_A_FORMAT(__printf__, 1, 2); +void errorfx(int, const char *, ...) + MKSH_A_NORETURN + MKSH_A_FORMAT(__printf__, 2, 3); +void warningf(bool, const char *, ...) + MKSH_A_FORMAT(__printf__, 2, 3); +void bi_errorf(const char *, ...) + MKSH_A_FORMAT(__printf__, 1, 2); +#define errorfz() errorf(NULL) +#define errorfxz(rc) errorfx((rc), NULL) +#define bi_errorfz() bi_errorf(NULL) +void internal_errorf(const char *, ...) + MKSH_A_NORETURN + MKSH_A_FORMAT(__printf__, 1, 2); +void internal_warningf(const char *, ...) + MKSH_A_FORMAT(__printf__, 1, 2); +void error_prefix(bool); +void shellf(const char *, ...) + MKSH_A_FORMAT(__printf__, 1, 2); +void shprintf(const char *, ...) + MKSH_A_FORMAT(__printf__, 1, 2); +int can_seek(int); +void initio(void); +void recheck_ctype(void); +int ksh_dup2(int, int, bool); +short savefd(int); +void restfd(int, int); +void openpipe(int *); +void closepipe(int *); +int check_fd(const char *, int, const char **); +void coproc_init(void); +void coproc_read_close(int); +void coproc_readw_close(int); +void coproc_write_close(int); +int coproc_getfd(int, const char **); +void coproc_cleanup(int); +struct temp *maketemp(Area *, Temp_type, struct temp **); +void ktinit(Area *, struct table *, uint8_t); +struct tbl *ktscan(struct table *, const char *, uint32_t, struct tbl ***); +/* table, name (key) to search for, hash(n) */ +#define ktsearch(tp, s, h) ktscan((tp), (s), (h), NULL) +struct tbl *ktenter(struct table *, const char *, uint32_t); +#define ktdelete(p) do { p->flag = 0; } while (/* CONSTCOND */ 0) +void ktwalk(struct tstate *, struct table *); +struct tbl *ktnext(struct tstate *); +struct tbl **ktsort(struct table *); +#ifdef DF +void DF(const char *, ...) + MKSH_A_FORMAT(__printf__, 1, 2); +#endif +/* misc.c */ +size_t option(const char *) MKSH_A_PURE; +char *getoptions(void); +void change_flag(enum sh_flag, int, bool); +void change_xtrace(unsigned char, bool); +int parse_args(const char **, int, bool *); +int getn(const char *, int *); +int gmatchx(const char *, const char *, bool); +bool has_globbing(const char *) MKSH_A_PURE; +int ascstrcmp(const void *, const void *) MKSH_A_PURE; +int ascpstrcmp(const void *, const void *) MKSH_A_PURE; +void ksh_getopt_reset(Getopt *, int); +int ksh_getopt(const char **, Getopt *, const char *); +void print_value_quoted(struct shf *, const char *); +char *quote_value(const char *); +void print_columns(struct columnise_opts *, unsigned int, + void (*)(char *, size_t, unsigned int, const void *), + const void *, size_t, size_t); +void strip_nuls(char *, size_t) + MKSH_A_BOUNDED(__string__, 1, 2); +ssize_t blocking_read(int, char *, size_t) + MKSH_A_BOUNDED(__buffer__, 2, 3); +int reset_nonblock(int); +char *ksh_get_wd(void); +char *do_realpath(const char *); +void simplify_path(char *); +void set_current_wd(const char *); +int c_cd(const char **); +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +char *strdup_i(const char *, Area *); +char *strndup_i(const char *, size_t, Area *); +#endif +int unbksl(bool, int (*)(void), void (*)(int)); +#ifdef __OS2__ +/* os2.c */ +void os2_init(int *, const char ***); +void setextlibpath(const char *, const char *); +int access_ex(int (*)(const char *, int), const char *, int); +int stat_ex(const char *, struct stat *); +const char *real_exec_name(const char *); +#endif +/* shf.c */ +struct shf *shf_open(const char *, int, int, int); +struct shf *shf_fdopen(int, int, struct shf *); +struct shf *shf_reopen(int, int, struct shf *); +struct shf *shf_sopen(char *, ssize_t, int, struct shf *); +int shf_close(struct shf *); +int shf_fdclose(struct shf *); +char *shf_sclose(struct shf *); +int shf_flush(struct shf *); +ssize_t shf_read(char *, ssize_t, struct shf *); +char *shf_getse(char *, ssize_t, struct shf *); +int shf_getchar(struct shf *s); +int shf_ungetc(int, struct shf *); +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +int shf_getc(struct shf *); +int shf_putc(int, struct shf *); +#else +#define shf_getc shf_getc_i +#define shf_putc shf_putc_i +#endif +int shf_putchar(int, struct shf *); +ssize_t shf_puts(const char *, struct shf *); +ssize_t shf_write(const char *, ssize_t, struct shf *); +ssize_t shf_fprintf(struct shf *, const char *, ...) + MKSH_A_FORMAT(__printf__, 2, 3); +ssize_t shf_snprintf(char *, ssize_t, const char *, ...) + MKSH_A_FORMAT(__printf__, 3, 4) + MKSH_A_BOUNDED(__string__, 1, 2); +char *shf_smprintf(const char *, ...) + MKSH_A_FORMAT(__printf__, 1, 2); +ssize_t shf_vfprintf(struct shf *, const char *, va_list) + MKSH_A_FORMAT(__printf__, 2, 0); +void set_ifs(const char *); +/* syn.c */ +void initkeywords(void); +struct op *compile(Source *, bool, bool); +bool parse_usec(const char *, struct timeval *); +char *yyrecursive(int); +void yyrecursive_pop(bool); +/* tree.c */ +void fptreef(struct shf *, int, const char *, ...); +char *snptreef(char *, ssize_t, const char *, ...); +struct op *tcopy(struct op *, Area *); +char *wdcopy(const char *, Area *); +const char *wdscan(const char *, int); +#define WDS_TPUTS BIT(0) /* tputS (dumpwdvar) mode */ +char *wdstrip(const char *, int); +void tfree(struct op *, Area *); +void dumpchar(struct shf *, int); +void dumptree(struct shf *, struct op *); +void dumpwdvar(struct shf *, const char *); +void dumpioact(struct shf *shf, struct op *t); +void vistree(char *, size_t, struct op *) + MKSH_A_BOUNDED(__string__, 1, 2); +void fpFUNCTf(struct shf *, int, bool, const char *, struct op *); +/* var.c */ +void newblock(void); +void popblock(void); +void initvar(void); +struct block *varsearch(struct block *, struct tbl **, const char *, uint32_t); +struct tbl *global(const char *); +struct tbl *isglobal(const char *, bool); +struct tbl *local(const char *, bool); +char *str_val(struct tbl *); +int setstr(struct tbl *, const char *, int); +struct tbl *setint_v(struct tbl *, struct tbl *, bool); +void setint(struct tbl *, mksh_ari_t); +void setint_n(struct tbl *, mksh_ari_t, int); +struct tbl *typeset(const char *, uint32_t, uint32_t, int, int); +void unset(struct tbl *, int); +const char *skip_varname(const char *, bool) MKSH_A_PURE; +const char *skip_wdvarname(const char *, bool) MKSH_A_PURE; +int is_wdvarname(const char *, bool) MKSH_A_PURE; +int is_wdvarassign(const char *) MKSH_A_PURE; +struct tbl *arraysearch(struct tbl *, uint32_t); +char **makenv(void); +void change_winsz(void); +size_t array_ref_len(const char *) MKSH_A_PURE; +char *arrayname(const char *); +mksh_uari_t set_array(const char *, bool, const char **); +uint32_t hash(const void *) MKSH_A_PURE; +uint32_t chvt_rndsetup(const void *, size_t) MKSH_A_PURE; +mksh_ari_t rndget(void); +void rndset(unsigned long); +void rndpush(const void *); +void record_match(const char *); + +enum Test_op { + /* non-operator */ + TO_NONOP = 0, + /* unary operators */ + TO_STNZE, TO_STZER, TO_ISSET, TO_OPTION, + TO_FILAXST, + TO_FILEXST, + TO_FILREG, TO_FILBDEV, TO_FILCDEV, TO_FILSYM, TO_FILFIFO, TO_FILSOCK, + TO_FILCDF, TO_FILID, TO_FILGID, TO_FILSETG, TO_FILSTCK, TO_FILUID, + TO_FILRD, TO_FILGZ, TO_FILTT, TO_FILSETU, TO_FILWR, TO_FILEX, + /* binary operators */ + TO_STEQL, TO_STNEQ, TO_STLT, TO_STGT, TO_INTEQ, TO_INTNE, TO_INTGT, + TO_INTGE, TO_INTLT, TO_INTLE, TO_FILEQ, TO_FILNT, TO_FILOT, + /* not an operator */ + TO_NONNULL /* !TO_NONOP */ +}; +typedef enum Test_op Test_op; + +/* Used by Test_env.isa() (order important - used to index *_tokens[] arrays) */ +enum Test_meta { + TM_OR, /* -o or || */ + TM_AND, /* -a or && */ + TM_NOT, /* ! */ + TM_OPAREN, /* ( */ + TM_CPAREN, /* ) */ + TM_UNOP, /* unary operator */ + TM_BINOP, /* binary operator */ + TM_END /* end of input */ +}; +typedef enum Test_meta Test_meta; + +#define TEF_ERROR BIT(0) /* set if we've hit an error */ +#define TEF_DBRACKET BIT(1) /* set if [[ .. ]] test */ + +typedef struct test_env { + union { + const char **wp; /* used by ptest_* */ + XPtrV *av; /* used by dbtestp_* */ + } pos; + const char **wp_end; /* used by ptest_* */ + Test_op (*isa)(struct test_env *, Test_meta); + const char *(*getopnd) (struct test_env *, Test_op, bool); + int (*eval)(struct test_env *, Test_op, const char *, const char *, bool); + void (*error)(struct test_env *, int, const char *); + int flags; /* TEF_* */ +} Test_env; + +extern const char * const dbtest_tokens[]; + +Test_op test_isop(Test_meta, const char *) MKSH_A_PURE; +int test_eval(Test_env *, Test_op, const char *, const char *, bool); +int test_parse(Test_env *); + +/* tty_fd is not opened O_BINARY, it's thus never read/written */ +EXTERN int tty_fd E_INIT(-1); /* dup'd tty file descriptor */ +EXTERN bool tty_devtty; /* true if tty_fd is from /dev/tty */ +EXTERN mksh_ttyst tty_state; /* saved tty state */ +EXTERN bool tty_hasstate; /* true if tty_state is valid */ + +extern int tty_init_fd(void); /* initialise tty_fd, tty_devtty */ + +#ifdef __OS2__ +#define binopen2(path,flags) __extension__({ \ + int binopen2_fd = open((path), (flags) | O_BINARY); \ + if (binopen2_fd >= 0) \ + setmode(binopen2_fd, O_BINARY); \ + (binopen2_fd); \ +}) +#define binopen3(path,flags,mode) __extension__({ \ + int binopen3_fd = open((path), (flags) | O_BINARY, (mode)); \ + if (binopen3_fd >= 0) \ + setmode(binopen3_fd, O_BINARY); \ + (binopen3_fd); \ +}) +#else +#define binopen2(path,flags) open((path), (flags) | O_BINARY) +#define binopen3(path,flags,mode) open((path), (flags) | O_BINARY, (mode)) +#endif + +#ifdef MKSH_DOSPATH +#define mksh_drvltr(s) __extension__({ \ + const char *mksh_drvltr_s = (s); \ + (ctype(mksh_drvltr_s[0], C_ALPHA) && mksh_drvltr_s[1] == ':'); \ +}) +#define mksh_abspath(s) __extension__({ \ + const char *mksh_abspath_s = (s); \ + (mksh_cdirsep(mksh_abspath_s[0]) || \ + (mksh_drvltr(mksh_abspath_s) && \ + mksh_cdirsep(mksh_abspath_s[2]))); \ +}) +#define mksh_cdirsep(c) __extension__({ \ + char mksh_cdirsep_c = (c); \ + (mksh_cdirsep_c == '/' || mksh_cdirsep_c == '\\'); \ +}) +#define mksh_sdirsep(s) strpbrk((s), "/\\") +#define mksh_vdirsep(s) __extension__({ \ + const char *mksh_vdirsep_s = (s); \ + (((mksh_drvltr(mksh_vdirsep_s) && \ + !mksh_cdirsep(mksh_vdirsep_s[2])) ? (!0) : \ + (mksh_sdirsep(mksh_vdirsep_s) != NULL)) && \ + (strcmp(mksh_vdirsep_s, T_builtin) != 0)); \ +}) +int getdrvwd(char **, unsigned int); +#else +#define mksh_abspath(s) (ord((s)[0]) == ORD('/')) +#define mksh_cdirsep(c) (ord(c) == ORD('/')) +#define mksh_sdirsep(s) strchr((s), '/') +#define mksh_vdirsep(s) vstrchr((s), '/') +#endif + +/* be sure not to interfere with anyone else's idea about EXTERN */ +#ifdef EXTERN_DEFINED +# undef EXTERN_DEFINED +# undef EXTERN +#endif +#undef E_INIT + +#endif /* !MKSH_INCLUDES_ONLY */ diff --git a/sh_flags.opt b/sh_flags.opt new file mode 100644 index 0000000..795d198 --- /dev/null +++ b/sh_flags.opt @@ -0,0 +1,198 @@ +/*- + * Copyright (c) 2013, 2014, 2015, 2017 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +@SHFLAGS_DEFNS +__RCSID("$MirOS: src/bin/mksh/sh_flags.opt,v 1.5 2017/02/18 02:33:15 tg Exp $"); +@SHFLAGS_ENUMS +#define FN(sname,cname,flags,ochar) cname, +#define F0(sname,cname,flags,ochar) cname = 0, +@SHFLAGS_ITEMS +#define FN(sname,cname,flags,ochar) ((const char *)(&shoptione_ ## cname)) + 2, +@@ + +/* special cases */ + +a| +F0("allexport", FEXPORT, OF_ANY + +/* ./. bgnice */ +>| HAVE_NICE +FN("bgnice", FBGNICE, OF_ANY + +/* ./. enable {} globbing (non-standard) */ +>| +FN("braceexpand", FBRACEEXPAND, OF_ANY + +/* ./. Emacs command line editing mode */ +>|!MKSH_NO_CMDLINE_EDITING +FN("emacs", FEMACS, OF_ANY + +/* -e quit on error */ +>e| +FN("errexit", FERREXIT, OF_ANY + +/* ./. Emacs command line editing mode, gmacs variant */ +>|!MKSH_NO_CMDLINE_EDITING +FN("gmacs", FGMACS, OF_ANY + +/* ./. reading EOF does not exit */ +>| +FN("ignoreeof", FIGNOREEOF, OF_ANY + +/* ./. inherit -x flag */ +>| +FN("inherit-xtrace", FXTRACEREC, OF_ANY + +/* -i interactive shell */ +>i|!SHFLAGS_NOT_CMD +FN("interactive", FTALKING, OF_CMDLINE + +/* -k name=value are recognised anywhere */ +>k| +FN("keyword", FKEYWORD, OF_ANY + +/* -l login shell */ +>l|!SHFLAGS_NOT_CMD +FN("login", FLOGIN, OF_CMDLINE + +/* -X mark dirs with / in file name completion */ +>X| +FN("markdirs", FMARKDIRS, OF_ANY + +/* -m job control monitoring */ +>m|!MKSH_UNEMPLOYED +FN("monitor", FMONITOR, OF_ANY + +/* -C don't overwrite existing files */ +>C| +FN("noclobber", FNOCLOBBER, OF_ANY + +/* -n don't execute any commands */ +>n| +FN("noexec", FNOEXEC, OF_ANY + +/* -f don't do file globbing */ +>f| +FN("noglob", FNOGLOB, OF_ANY + +/* ./. don't kill running jobs when login shell exits */ +>| +FN("nohup", FNOHUP, OF_ANY + +/* ./. don't save functions in history (no effect) */ +>| +FN("nolog", FNOLOG, OF_ANY + +/* -b asynchronous job completion notification */ +>b|!MKSH_UNEMPLOYED +FN("notify", FNOTIFY, OF_ANY + +/* -u using an unset variable is an error */ +>u| +FN("nounset", FNOUNSET, OF_ANY + +/* ./. don't do logical cds/pwds (non-standard) */ +>| +FN("physical", FPHYSICAL, OF_ANY + +/* ./. errorlevel of a pipeline is the rightmost nonzero value */ +>| +FN("pipefail", FPIPEFAIL, OF_ANY + +/* ./. adhere more closely to POSIX even when undesirable */ +>| +FN("posix", FPOSIX, OF_ANY + +/* -p privileged shell (suid) */ +>p| +FN("privileged", FPRIVILEGED, OF_ANY + +/* -r restricted shell */ +>r|!SHFLAGS_NOT_CMD +FN("restricted", FRESTRICTED, OF_CMDLINE + +/* ./. kludge mode for better compat with traditional sh (OS-specific) */ +>| +FN("sh", FSH, OF_ANY + +/* -s (invocation) parse stdin (pseudo non-standard) */ +>s|!SHFLAGS_NOT_CMD +FN("stdin", FSTDIN, OF_CMDLINE + +/* -h create tracked aliases for all commands */ +>h| +FN("trackall", FTRACKALL, OF_ANY + +/* -U enable UTF-8 processing (non-standard) */ +>U| +FN("utf8-mode", FUNICODE, OF_ANY + +/* -v echo input */ +>v| +FN("verbose", FVERBOSE, OF_ANY + +/* ./. Vi command line editing mode */ +>|!MKSH_NO_CMDLINE_EDITING +FN("vi", FVI, OF_ANY + +/* ./. enable ESC as file name completion character (non-standard) */ +>|!MKSH_NO_CMDLINE_EDITING +FN("vi-esccomplete", FVIESCCOMPLETE, OF_ANY + +/* ./. enable Tab as file name completion character (non-standard) */ +>|!MKSH_NO_CMDLINE_EDITING +FN("vi-tabcomplete", FVITABCOMPLETE, OF_ANY + +/* ./. always read in raw mode (no effect) */ +>|!MKSH_NO_CMDLINE_EDITING +FN("viraw", FVIRAW, OF_ANY + +/* -x execution trace (display commands as they are run) */ +>x| +FN("xtrace", FXTRACE, OF_ANY + +/* -c (invocation) execute specified command */ +>c|!SHFLAGS_NOT_CMD +FN("", FCOMMAND, OF_CMDLINE + +/* + * anonymous flags: used internally by shell only (not visible to user + */ + +/* ./. direct builtin call (divined from argv[0] multi-call binary) */ +>| +FN("", FAS_BUILTIN, OF_INTERNAL + +/* ./. (internal) initial shell was interactive */ +>| +FN("", FTALKING_I, OF_INTERNAL + +|SHFLAGS_OPTCS diff --git a/shf.c b/shf.c new file mode 100644 index 0000000..2ee0ec1 --- /dev/null +++ b/shf.c @@ -0,0 +1,1321 @@ +/* $OpenBSD: shf.c,v 1.16 2013/04/19 17:36:09 millert Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2011, + * 2012, 2013, 2015, 2016, 2017, 2018 + * mirabilos + * Copyright (c) 2015 + * Daniel Richard G. + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + *- + * Use %zX instead of %p and floating point isn't supported at all. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/shf.c,v 1.97 2018/01/14 01:28:16 tg Exp $"); + +/* flags to shf_emptybuf() */ +#define EB_READSW 0x01 /* about to switch to reading */ +#define EB_GROW 0x02 /* grow buffer if necessary (STRING+DYNAMIC) */ + +/* + * Replacement stdio routines. Stdio is too flakey on too many machines + * to be useful when you have multiple processes using the same underlying + * file descriptors. + */ + +static int shf_fillbuf(struct shf *); +static int shf_emptybuf(struct shf *, int); + +/* + * Open a file. First three args are for open(), last arg is flags for + * this package. Returns NULL if file could not be opened, or if a dup + * fails. + */ +struct shf * +shf_open(const char *name, int oflags, int mode, int sflags) +{ + struct shf *shf; + ssize_t bsize = + /* at most 512 */ + sflags & SHF_UNBUF ? (sflags & SHF_RD ? 1 : 0) : SHF_BSIZE; + int fd, eno; + + /* Done before open so if alloca fails, fd won't be lost. */ + shf = alloc(sizeof(struct shf) + bsize, ATEMP); + shf->areap = ATEMP; + shf->buf = (unsigned char *)&shf[1]; + shf->bsize = bsize; + shf->flags = SHF_ALLOCS; + /* Rest filled in by reopen. */ + + fd = binopen3(name, oflags, mode); + if (fd < 0) { + eno = errno; + afree(shf, shf->areap); + errno = eno; + return (NULL); + } + if ((sflags & SHF_MAPHI) && fd < FDBASE) { + int nfd; + + nfd = fcntl(fd, F_DUPFD, FDBASE); + eno = errno; + close(fd); + if (nfd < 0) { + afree(shf, shf->areap); + errno = eno; + return (NULL); + } + fd = nfd; + } + sflags &= ~SHF_ACCMODE; + sflags |= (oflags & O_ACCMODE) == O_RDONLY ? SHF_RD : + ((oflags & O_ACCMODE) == O_WRONLY ? SHF_WR : SHF_RDWR); + + return (shf_reopen(fd, sflags, shf)); +} + +/* helper function for shf_fdopen and shf_reopen */ +static void +shf_open_hlp(int fd, int *sflagsp, const char *where) +{ + int sflags = *sflagsp; + + /* use fcntl() to figure out correct read/write flags */ + if (sflags & SHF_GETFL) { + int flags = fcntl(fd, F_GETFL, 0); + + if (flags < 0) + /* will get an error on first read/write */ + sflags |= SHF_RDWR; + else { + switch (flags & O_ACCMODE) { + case O_RDONLY: + sflags |= SHF_RD; + break; + case O_WRONLY: + sflags |= SHF_WR; + break; + case O_RDWR: + sflags |= SHF_RDWR; + break; + } + } + *sflagsp = sflags; + } + + if (!(sflags & (SHF_RD | SHF_WR))) + internal_errorf(Tf_sD_s, where, "missing read/write"); +} + +/* Set up the shf structure for a file descriptor. Doesn't fail. */ +struct shf * +shf_fdopen(int fd, int sflags, struct shf *shf) +{ + ssize_t bsize = + /* at most 512 */ + sflags & SHF_UNBUF ? (sflags & SHF_RD ? 1 : 0) : SHF_BSIZE; + + shf_open_hlp(fd, &sflags, "shf_fdopen"); + if (shf) { + if (bsize) { + shf->buf = alloc(bsize, ATEMP); + sflags |= SHF_ALLOCB; + } else + shf->buf = NULL; + } else { + shf = alloc(sizeof(struct shf) + bsize, ATEMP); + shf->buf = (unsigned char *)&shf[1]; + sflags |= SHF_ALLOCS; + } + shf->areap = ATEMP; + shf->fd = fd; + shf->rp = shf->wp = shf->buf; + shf->rnleft = 0; + shf->rbsize = bsize; + shf->wnleft = 0; /* force call to shf_emptybuf() */ + shf->wbsize = sflags & SHF_UNBUF ? 0 : bsize; + shf->flags = sflags; + shf->errnosv = 0; + shf->bsize = bsize; + if (sflags & SHF_CLEXEC) + fcntl(fd, F_SETFD, FD_CLOEXEC); + return (shf); +} + +/* Set up an existing shf (and buffer) to use the given fd */ +struct shf * +shf_reopen(int fd, int sflags, struct shf *shf) +{ + ssize_t bsize = + /* at most 512 */ + sflags & SHF_UNBUF ? (sflags & SHF_RD ? 1 : 0) : SHF_BSIZE; + + shf_open_hlp(fd, &sflags, "shf_reopen"); + if (!shf || !shf->buf || shf->bsize < bsize) + internal_errorf(Tf_sD_s, "shf_reopen", Tbad_bsize); + + /* assumes shf->buf and shf->bsize already set up */ + shf->fd = fd; + shf->rp = shf->wp = shf->buf; + shf->rnleft = 0; + shf->rbsize = bsize; + shf->wnleft = 0; /* force call to shf_emptybuf() */ + shf->wbsize = sflags & SHF_UNBUF ? 0 : bsize; + shf->flags = (shf->flags & (SHF_ALLOCS | SHF_ALLOCB)) | sflags; + shf->errnosv = 0; + if (sflags & SHF_CLEXEC) + fcntl(fd, F_SETFD, FD_CLOEXEC); + return (shf); +} + +/* + * Open a string for reading or writing. If reading, bsize is the number + * of bytes that can be read. If writing, bsize is the maximum number of + * bytes that can be written. If shf is not NULL, it is filled in and + * returned, if it is NULL, shf is allocated. If writing and buf is NULL + * and SHF_DYNAMIC is set, the buffer is allocated (if bsize > 0, it is + * used for the initial size). Doesn't fail. + * When writing, a byte is reserved for a trailing NUL - see shf_sclose(). + */ +struct shf * +shf_sopen(char *buf, ssize_t bsize, int sflags, struct shf *shf) +{ + /* can't have a read+write string */ + if (!(!(sflags & SHF_RD) ^ !(sflags & SHF_WR))) + internal_errorf(Tf_flags, "shf_sopen", + (unsigned int)sflags); + + if (!shf) { + shf = alloc(sizeof(struct shf), ATEMP); + sflags |= SHF_ALLOCS; + } + shf->areap = ATEMP; + if (!buf && (sflags & SHF_WR) && (sflags & SHF_DYNAMIC)) { + if (bsize <= 0) + bsize = 64; + sflags |= SHF_ALLOCB; + buf = alloc(bsize, shf->areap); + } + shf->fd = -1; + shf->buf = shf->rp = shf->wp = (unsigned char *)buf; + shf->rnleft = bsize; + shf->rbsize = bsize; + shf->wnleft = bsize - 1; /* space for a '\0' */ + shf->wbsize = bsize; + shf->flags = sflags | SHF_STRING; + shf->errnosv = 0; + shf->bsize = bsize; + + return (shf); +} + +/* Flush and close file descriptor, free the shf structure */ +int +shf_close(struct shf *shf) +{ + int ret = 0; + + if (shf->fd >= 0) { + ret = shf_flush(shf); + if (close(shf->fd) < 0) + ret = -1; + } + if (shf->flags & SHF_ALLOCS) + afree(shf, shf->areap); + else if (shf->flags & SHF_ALLOCB) + afree(shf->buf, shf->areap); + + return (ret); +} + +/* Flush and close file descriptor, don't free file structure */ +int +shf_fdclose(struct shf *shf) +{ + int ret = 0; + + if (shf->fd >= 0) { + ret = shf_flush(shf); + if (close(shf->fd) < 0) + ret = -1; + shf->rnleft = 0; + shf->rp = shf->buf; + shf->wnleft = 0; + shf->fd = -1; + } + + return (ret); +} + +/* + * Close a string - if it was opened for writing, it is NUL terminated; + * returns a pointer to the string and frees shf if it was allocated + * (does not free string if it was allocated). + */ +char * +shf_sclose(struct shf *shf) +{ + unsigned char *s = shf->buf; + + /* NUL terminate */ + if (shf->flags & SHF_WR) { + shf->wnleft++; + shf_putc('\0', shf); + } + if (shf->flags & SHF_ALLOCS) + afree(shf, shf->areap); + return ((char *)s); +} + +/* + * Un-read what has been read but not examined, or write what has been + * buffered. Returns 0 for success, -1 for (write) error. + */ +int +shf_flush(struct shf *shf) +{ + int rv = 0; + + if (shf->flags & SHF_STRING) + rv = (shf->flags & SHF_WR) ? -1 : 0; + else if (shf->fd < 0) + internal_errorf(Tf_sD_s, "shf_flush", "no fd"); + else if (shf->flags & SHF_ERROR) { + errno = shf->errnosv; + rv = -1; + } else if (shf->flags & SHF_READING) { + shf->flags &= ~(SHF_EOF | SHF_READING); + if (shf->rnleft > 0) { + if (lseek(shf->fd, (off_t)-shf->rnleft, + SEEK_CUR) == -1) { + shf->flags |= SHF_ERROR; + shf->errnosv = errno; + rv = -1; + } + shf->rnleft = 0; + shf->rp = shf->buf; + } + } else if (shf->flags & SHF_WRITING) + rv = shf_emptybuf(shf, 0); + + return (rv); +} + +/* + * Write out any buffered data. If currently reading, flushes the read + * buffer. Returns 0 for success, -1 for (write) error. + */ +static int +shf_emptybuf(struct shf *shf, int flags) +{ + int ret = 0; + + if (!(shf->flags & SHF_STRING) && shf->fd < 0) + internal_errorf(Tf_sD_s, "shf_emptybuf", "no fd"); + + if (shf->flags & SHF_ERROR) { + errno = shf->errnosv; + return (-1); + } + + if (shf->flags & SHF_READING) { + if (flags & EB_READSW) + /* doesn't happen */ + return (0); + ret = shf_flush(shf); + shf->flags &= ~SHF_READING; + } + if (shf->flags & SHF_STRING) { + unsigned char *nbuf; + + /* + * Note that we assume SHF_ALLOCS is not set if + * SHF_ALLOCB is set... (changing the shf pointer could + * cause problems) + */ + if (!(flags & EB_GROW) || !(shf->flags & SHF_DYNAMIC) || + !(shf->flags & SHF_ALLOCB)) + return (-1); + /* allocate more space for buffer */ + nbuf = aresize2(shf->buf, 2, shf->wbsize, shf->areap); + shf->rp = nbuf + (shf->rp - shf->buf); + shf->wp = nbuf + (shf->wp - shf->buf); + shf->rbsize += shf->wbsize; + shf->wnleft += shf->wbsize; + shf->wbsize <<= 1; + shf->buf = nbuf; + } else { + if (shf->flags & SHF_WRITING) { + ssize_t n, ntowrite = shf->wp - shf->buf; + unsigned char *buf = shf->buf; + + while (ntowrite > 0) { + n = write(shf->fd, buf, ntowrite); + if (n < 0) { + if (errno == EINTR && + !(shf->flags & SHF_INTERRUPT)) + continue; + shf->flags |= SHF_ERROR; + shf->errnosv = errno; + shf->wnleft = 0; + if (buf != shf->buf) { + /* + * allow a second flush + * to work + */ + memmove(shf->buf, buf, + ntowrite); + shf->wp = shf->buf + ntowrite; + } + return (-1); + } + buf += n; + ntowrite -= n; + } + if (flags & EB_READSW) { + shf->wp = shf->buf; + shf->wnleft = 0; + shf->flags &= ~SHF_WRITING; + return (0); + } + } + shf->wp = shf->buf; + shf->wnleft = shf->wbsize; + } + shf->flags |= SHF_WRITING; + + return (ret); +} + +/* Fill up a read buffer. Returns -1 for a read error, 0 otherwise. */ +static int +shf_fillbuf(struct shf *shf) +{ + ssize_t n; + + if (shf->flags & SHF_STRING) + return (0); + + if (shf->fd < 0) + internal_errorf(Tf_sD_s, "shf_fillbuf", "no fd"); + + if (shf->flags & (SHF_EOF | SHF_ERROR)) { + if (shf->flags & SHF_ERROR) + errno = shf->errnosv; + return (-1); + } + + if ((shf->flags & SHF_WRITING) && shf_emptybuf(shf, EB_READSW) == -1) + return (-1); + + shf->flags |= SHF_READING; + + shf->rp = shf->buf; + while (/* CONSTCOND */ 1) { + n = blocking_read(shf->fd, (char *)shf->buf, shf->rbsize); + if (n < 0 && errno == EINTR && !(shf->flags & SHF_INTERRUPT)) + continue; + break; + } + if (n < 0) { + shf->flags |= SHF_ERROR; + shf->errnosv = errno; + shf->rnleft = 0; + shf->rp = shf->buf; + return (-1); + } + if ((shf->rnleft = n) == 0) + shf->flags |= SHF_EOF; + return (0); +} + +/* + * Read a buffer from shf. Returns the number of bytes read into buf, if + * no bytes were read, returns 0 if end of file was seen, -1 if a read + * error occurred. + */ +ssize_t +shf_read(char *buf, ssize_t bsize, struct shf *shf) +{ + ssize_t ncopy, orig_bsize = bsize; + + if (!(shf->flags & SHF_RD)) + internal_errorf(Tf_flags, Tshf_read, + (unsigned int)shf->flags); + + if (bsize <= 0) + internal_errorf(Tf_szs, Tshf_read, bsize, Tbsize); + + while (bsize > 0) { + if (shf->rnleft == 0 && + (shf_fillbuf(shf) == -1 || shf->rnleft == 0)) + break; + ncopy = shf->rnleft; + if (ncopy > bsize) + ncopy = bsize; + memcpy(buf, shf->rp, ncopy); + buf += ncopy; + bsize -= ncopy; + shf->rp += ncopy; + shf->rnleft -= ncopy; + } + /* Note: fread(3S) returns 0 for errors - this doesn't */ + return (orig_bsize == bsize ? (shf_error(shf) ? -1 : 0) : + orig_bsize - bsize); +} + +/* + * Read up to a newline or -1. The newline is put in buf; buf is always + * NUL terminated. Returns NULL on read error or if nothing was read + * before end of file, returns a pointer to the NUL byte in buf + * otherwise. + */ +char * +shf_getse(char *buf, ssize_t bsize, struct shf *shf) +{ + unsigned char *end; + ssize_t ncopy; + char *orig_buf = buf; + + if (!(shf->flags & SHF_RD)) + internal_errorf(Tf_flags, "shf_getse", + (unsigned int)shf->flags); + + if (bsize <= 0) + return (NULL); + + /* save room for NUL */ + --bsize; + do { + if (shf->rnleft == 0) { + if (shf_fillbuf(shf) == -1) + return (NULL); + if (shf->rnleft == 0) { + *buf = '\0'; + return (buf == orig_buf ? NULL : buf); + } + } + end = (unsigned char *)memchr((char *)shf->rp, '\n', + shf->rnleft); + ncopy = end ? end - shf->rp + 1 : shf->rnleft; + if (ncopy > bsize) + ncopy = bsize; + memcpy(buf, (char *) shf->rp, ncopy); + shf->rp += ncopy; + shf->rnleft -= ncopy; + buf += ncopy; + bsize -= ncopy; +#ifdef MKSH_WITH_TEXTMODE + if (end && buf > orig_buf + 1 && buf[-2] == '\r') { + buf--; + bsize++; + buf[-1] = '\n'; + } +#endif + } while (!end && bsize); +#ifdef MKSH_WITH_TEXTMODE + if (!bsize && buf[-1] == '\r') { + int c = shf_getc(shf); + if (c == '\n') + buf[-1] = '\n'; + else if (c != -1) + shf_ungetc(c, shf); + } +#endif + *buf = '\0'; + return (buf); +} + +/* Returns the char read. Returns -1 for error and end of file. */ +int +shf_getchar(struct shf *shf) +{ + if (!(shf->flags & SHF_RD)) + internal_errorf(Tf_flags, "shf_getchar", + (unsigned int)shf->flags); + + if (shf->rnleft == 0 && (shf_fillbuf(shf) == -1 || shf->rnleft == 0)) + return (-1); + --shf->rnleft; + return (ord(*shf->rp++)); +} + +/* + * Put a character back in the input stream. Returns the character if + * successful, -1 if there is no room. + */ +int +shf_ungetc(int c, struct shf *shf) +{ + if (!(shf->flags & SHF_RD)) + internal_errorf(Tf_flags, "shf_ungetc", + (unsigned int)shf->flags); + + if ((shf->flags & SHF_ERROR) || c == -1 || + (shf->rp == shf->buf && shf->rnleft)) + return (-1); + + if ((shf->flags & SHF_WRITING) && shf_emptybuf(shf, EB_READSW) == -1) + return (-1); + + if (shf->rp == shf->buf) + shf->rp = shf->buf + shf->rbsize; + if (shf->flags & SHF_STRING) { + /* + * Can unget what was read, but not something different; + * we don't want to modify a string. + */ + if ((int)(shf->rp[-1]) != c) + return (-1); + shf->flags &= ~SHF_EOF; + shf->rp--; + shf->rnleft++; + return (c); + } + shf->flags &= ~SHF_EOF; + *--(shf->rp) = c; + shf->rnleft++; + return (c); +} + +/* + * Write a character. Returns the character if successful, -1 if the + * char could not be written. + */ +int +shf_putchar(int c, struct shf *shf) +{ + if (!(shf->flags & SHF_WR)) + internal_errorf(Tf_flags, "shf_putchar", + (unsigned int)shf->flags); + + if (c == -1) + return (-1); + + if (shf->flags & SHF_UNBUF) { + unsigned char cc = (unsigned char)c; + ssize_t n; + + if (shf->fd < 0) + internal_errorf(Tf_sD_s, "shf_putchar", "no fd"); + if (shf->flags & SHF_ERROR) { + errno = shf->errnosv; + return (-1); + } + while ((n = write(shf->fd, &cc, 1)) != 1) + if (n < 0) { + if (errno == EINTR && + !(shf->flags & SHF_INTERRUPT)) + continue; + shf->flags |= SHF_ERROR; + shf->errnosv = errno; + return (-1); + } + } else { + /* Flush deals with strings and sticky errors */ + if (shf->wnleft == 0 && shf_emptybuf(shf, EB_GROW) == -1) + return (-1); + shf->wnleft--; + *shf->wp++ = c; + } + + return (c); +} + +/* + * Write a string. Returns the length of the string if successful, -1 + * if the string could not be written. + */ +ssize_t +shf_puts(const char *s, struct shf *shf) +{ + if (!s) + return (-1); + + return (shf_write(s, strlen(s), shf)); +} + +/* Write a buffer. Returns nbytes if successful, -1 if there is an error. */ +ssize_t +shf_write(const char *buf, ssize_t nbytes, struct shf *shf) +{ + ssize_t n, ncopy, orig_nbytes = nbytes; + + if (!(shf->flags & SHF_WR)) + internal_errorf(Tf_flags, Tshf_write, + (unsigned int)shf->flags); + + if (nbytes < 0) + internal_errorf(Tf_szs, Tshf_write, nbytes, Tbytes); + + /* Don't buffer if buffer is empty and we're writting a large amount. */ + if ((ncopy = shf->wnleft) && + (shf->wp != shf->buf || nbytes < shf->wnleft)) { + if (ncopy > nbytes) + ncopy = nbytes; + memcpy(shf->wp, buf, ncopy); + nbytes -= ncopy; + buf += ncopy; + shf->wp += ncopy; + shf->wnleft -= ncopy; + } + if (nbytes > 0) { + if (shf->flags & SHF_STRING) { + /* resize buffer until there's enough space left */ + while (nbytes > shf->wnleft) + if (shf_emptybuf(shf, EB_GROW) == -1) + return (-1); + /* then write everything into the buffer */ + } else { + /* flush deals with sticky errors */ + if (shf_emptybuf(shf, EB_GROW) == -1) + return (-1); + /* write chunks larger than window size directly */ + if (nbytes > shf->wbsize) { + ncopy = nbytes; + if (shf->wbsize) + ncopy -= nbytes % shf->wbsize; + nbytes -= ncopy; + while (ncopy > 0) { + n = write(shf->fd, buf, ncopy); + if (n < 0) { + if (errno == EINTR && + !(shf->flags & SHF_INTERRUPT)) + continue; + shf->flags |= SHF_ERROR; + shf->errnosv = errno; + shf->wnleft = 0; + /* + * Note: fwrite(3) returns 0 + * for errors - this doesn't + */ + return (-1); + } + buf += n; + ncopy -= n; + } + } + /* ... and buffer the rest */ + } + if (nbytes > 0) { + /* write remaining bytes to buffer */ + memcpy(shf->wp, buf, nbytes); + shf->wp += nbytes; + shf->wnleft -= nbytes; + } + } + + return (orig_nbytes); +} + +ssize_t +shf_fprintf(struct shf *shf, const char *fmt, ...) +{ + va_list args; + ssize_t n; + + va_start(args, fmt); + n = shf_vfprintf(shf, fmt, args); + va_end(args); + + return (n); +} + +ssize_t +shf_snprintf(char *buf, ssize_t bsize, const char *fmt, ...) +{ + struct shf shf; + va_list args; + ssize_t n; + + if (!buf || bsize <= 0) + internal_errorf("shf_snprintf: buf %zX, bsize %zd", + (size_t)buf, bsize); + + shf_sopen(buf, bsize, SHF_WR, &shf); + va_start(args, fmt); + n = shf_vfprintf(&shf, fmt, args); + va_end(args); + /* NUL terminates */ + shf_sclose(&shf); + return (n); +} + +char * +shf_smprintf(const char *fmt, ...) +{ + struct shf shf; + va_list args; + + shf_sopen(NULL, 0, SHF_WR|SHF_DYNAMIC, &shf); + va_start(args, fmt); + shf_vfprintf(&shf, fmt, args); + va_end(args); + /* NUL terminates */ + return (shf_sclose(&shf)); +} + +#define FL_HASH 0x001 /* '#' seen */ +#define FL_PLUS 0x002 /* '+' seen */ +#define FL_RIGHT 0x004 /* '-' seen */ +#define FL_BLANK 0x008 /* ' ' seen */ +#define FL_SHORT 0x010 /* 'h' seen */ +#define FL_LONG 0x020 /* 'l' seen */ +#define FL_ZERO 0x040 /* '0' seen */ +#define FL_DOT 0x080 /* '.' seen */ +#define FL_UPPER 0x100 /* format character was uppercase */ +#define FL_NUMBER 0x200 /* a number was formated %[douxefg] */ +#define FL_SIZET 0x400 /* 'z' seen */ +#define FM_SIZES 0x430 /* h/l/z mask */ + +ssize_t +shf_vfprintf(struct shf *shf, const char *fmt, va_list args) +{ + const char *s; + char c, *cp; + int tmp = 0, flags; + size_t field, precision, len; + unsigned long lnum; + /* %#o produces the longest output */ + char numbuf[(8 * sizeof(long) + 2) / 3 + 1 + /* NUL */ 1]; + /* this stuff for dealing with the buffer */ + ssize_t nwritten = 0; + +#define VA(type) va_arg(args, type) + + if (!fmt) + return (0); + + while ((c = *fmt++)) { + if (c != '%') { + shf_putc(c, shf); + nwritten++; + continue; + } + /* + * This will accept flags/fields in any order - not just + * the order specified in printf(3), but this is the way + * _doprnt() seems to work (on BSD and SYSV). The only + * restriction is that the format character must come + * last :-). + */ + flags = 0; + field = precision = 0; + while ((c = *fmt++)) { + switch (c) { + case '#': + flags |= FL_HASH; + continue; + + case '+': + flags |= FL_PLUS; + continue; + + case '-': + flags |= FL_RIGHT; + continue; + + case ' ': + flags |= FL_BLANK; + continue; + + case '0': + if (!(flags & FL_DOT)) + flags |= FL_ZERO; + continue; + + case '.': + flags |= FL_DOT; + precision = 0; + continue; + + case '*': + tmp = VA(int); + if (tmp < 0) { + if (flags & FL_DOT) + precision = 0; + else { + field = (unsigned int)-tmp; + flags |= FL_RIGHT; + } + } else if (flags & FL_DOT) + precision = (unsigned int)tmp; + else + field = (unsigned int)tmp; + continue; + + case 'l': + flags &= ~FM_SIZES; + flags |= FL_LONG; + continue; + + case 'h': + flags &= ~FM_SIZES; + flags |= FL_SHORT; + continue; + + case 'z': + flags &= ~FM_SIZES; + flags |= FL_SIZET; + continue; + } + if (ctype(c, C_DIGIT)) { + bool overflowed = false; + + tmp = ksh_numdig(c); + while (ctype((c = *fmt++), C_DIGIT)) + if (notok2mul(2147483647, tmp, 10)) + overflowed = true; + else + tmp = tmp * 10 + ksh_numdig(c); + --fmt; + if (overflowed) + tmp = 0; + if (flags & FL_DOT) + precision = (unsigned int)tmp; + else + field = (unsigned int)tmp; + continue; + } + break; + } + + if (!c) + /* nasty format */ + break; + + if (ctype(c, C_UPPER)) { + flags |= FL_UPPER; + c = ksh_tolower(c); + } + + switch (c) { + case 'd': + case 'i': + if (flags & FL_SIZET) + lnum = (long)VA(ssize_t); + else if (flags & FL_LONG) + lnum = VA(long); + else if (flags & FL_SHORT) + lnum = (long)(short)VA(int); + else + lnum = (long)VA(int); + goto integral; + + case 'o': + case 'u': + case 'x': + if (flags & FL_SIZET) + lnum = VA(size_t); + else if (flags & FL_LONG) + lnum = VA(unsigned long); + else if (flags & FL_SHORT) + lnum = (unsigned long)(unsigned short)VA(int); + else + lnum = (unsigned long)VA(unsigned int); + + integral: + flags |= FL_NUMBER; + cp = numbuf + sizeof(numbuf); + *--cp = '\0'; + + switch (c) { + case 'd': + case 'i': + if (0 > (long)lnum) { + lnum = -(long)lnum; + tmp = 1; + } else + tmp = 0; + /* FALLTHROUGH */ + case 'u': + do { + *--cp = digits_lc[lnum % 10]; + lnum /= 10; + } while (lnum); + + if (c != 'u') { + if (tmp) + *--cp = '-'; + else if (flags & FL_PLUS) + *--cp = '+'; + else if (flags & FL_BLANK) + *--cp = ' '; + } + break; + + case 'o': + do { + *--cp = digits_lc[lnum & 0x7]; + lnum >>= 3; + } while (lnum); + + if ((flags & FL_HASH) && *cp != '0') + *--cp = '0'; + break; + + case 'x': { + const char *digits = (flags & FL_UPPER) ? + digits_uc : digits_lc; + do { + *--cp = digits[lnum & 0xF]; + lnum >>= 4; + } while (lnum); + + if (flags & FL_HASH) { + *--cp = (flags & FL_UPPER) ? 'X' : 'x'; + *--cp = '0'; + } + } + } + len = numbuf + sizeof(numbuf) - 1 - (s = cp); + if (flags & FL_DOT) { + if (precision > len) { + field = precision; + flags |= FL_ZERO; + } else + /* no loss */ + precision = len; + } + break; + + case 's': + if ((s = VA(const char *)) == NULL) + s = "(null)"; + else if (flags & FL_HASH) { + print_value_quoted(shf, s); + continue; + } + len = utf_mbswidth(s); + break; + + case 'c': + flags &= ~FL_DOT; + c = (char)(VA(int)); + /* FALLTHROUGH */ + + case '%': + default: + numbuf[0] = c; + numbuf[1] = 0; + s = numbuf; + len = 1; + break; + } + + /* + * At this point s should point to a string that is to be + * formatted, and len should be the length of the string. + */ + if (!(flags & FL_DOT) || len < precision) + precision = len; + if (field > precision) { + field -= precision; + if (!(flags & FL_RIGHT)) { + /* skip past sign or 0x when padding with 0 */ + if ((flags & FL_ZERO) && (flags & FL_NUMBER)) { + if (ctype(*s, C_SPC | C_PLUS | C_MINUS)) { + shf_putc(*s, shf); + s++; + precision--; + nwritten++; + } else if (*s == '0') { + shf_putc(*s, shf); + s++; + nwritten++; + if (--precision && + ksh_eq(*s, 'X', 'x')) { + shf_putc(*s, shf); + s++; + precision--; + nwritten++; + } + } + c = '0'; + } else + c = flags & FL_ZERO ? '0' : ' '; + nwritten += field; + while (field--) + shf_putc(c, shf); + field = 0; + } else + c = ' '; + } else + field = 0; + + nwritten += precision; + precision = utf_skipcols(s, precision, &tmp) - s; + while (precision--) + shf_putc(*s++, shf); + + nwritten += field; + while (field--) + shf_putc(c, shf); + } + + return (shf_error(shf) ? -1 : nwritten); +} + +#if defined(MKSH_SMALL) && !defined(MKSH_SMALL_BUT_FAST) +int +shf_getc(struct shf *shf) +{ + return (shf_getc_i(shf)); +} + +int +shf_putc(int c, struct shf *shf) +{ + return (shf_putc_i(c, shf)); +} +#endif + +#ifdef DEBUG +const char * +cstrerror(int errnum) +{ +#undef strerror + return (strerror(errnum)); +#define strerror dontuse_strerror /* poisoned */ +} +#elif !HAVE_STRERROR + +#if HAVE_SYS_ERRLIST +#if !HAVE_SYS_ERRLIST_DECL +extern const int sys_nerr; +extern const char * const sys_errlist[]; +#endif +#endif + +const char * +cstrerror(int errnum) +{ + /* "Unknown error: " + sign + rough estimate + NUL */ + static char errbuf[15 + 1 + (8 * sizeof(int) + 2) / 3 + 1]; + +#if HAVE_SYS_ERRLIST + if (errnum > 0 && errnum < sys_nerr && sys_errlist[errnum]) + return (sys_errlist[errnum]); +#endif + + switch (errnum) { + case 0: + return ("Undefined error: 0"); + case EPERM: + return ("Operation not permitted"); + case ENOENT: + return ("No such file or directory"); +#ifdef ESRCH + case ESRCH: + return ("No such process"); +#endif +#ifdef E2BIG + case E2BIG: + return ("Argument list too long"); +#endif + case ENOEXEC: + return ("Exec format error"); + case EBADF: + return ("Bad file descriptor"); +#ifdef ENOMEM + case ENOMEM: + return ("Cannot allocate memory"); +#endif + case EACCES: + return ("Permission denied"); + case EEXIST: + return ("File exists"); + case ENOTDIR: + return ("Not a directory"); +#ifdef EINVAL + case EINVAL: + return ("Invalid argument"); +#endif +#ifdef ELOOP + case ELOOP: + return ("Too many levels of symbolic links"); +#endif + default: + shf_snprintf(errbuf, sizeof(errbuf), + "Unknown error: %d", errnum); + return (errbuf); + } +} +#endif + +/* fast character classes */ +const uint32_t tpl_ctypes[128] = { + /* 0x00 */ + CiNUL, CiCNTRL, CiCNTRL, CiCNTRL, + CiCNTRL, CiCNTRL, CiCNTRL, CiCNTRL, + CiCNTRL, CiTAB, CiNL, CiSPX, + CiSPX, CiCR, CiCNTRL, CiCNTRL, + /* 0x10 */ + CiCNTRL, CiCNTRL, CiCNTRL, CiCNTRL, + CiCNTRL, CiCNTRL, CiCNTRL, CiCNTRL, + CiCNTRL, CiCNTRL, CiCNTRL, CiCNTRL, + CiCNTRL, CiCNTRL, CiCNTRL, CiCNTRL, + /* 0x20 */ + CiSP, CiALIAS | CiVAR1, CiQC, CiHASH, + CiSS, CiPERCT, CiQCL, CiQC, + CiQCL, CiQCL, CiQCX | CiVAR1, CiPLUS, + CiALIAS, CiMINUS, CiALIAS, CiQCM, + /* 0x30 */ + CiOCTAL, CiOCTAL, CiOCTAL, CiOCTAL, + CiOCTAL, CiOCTAL, CiOCTAL, CiOCTAL, + CiDIGIT, CiDIGIT, CiCOLON, CiQCL, + CiANGLE, CiEQUAL, CiANGLE, CiQUEST, + /* 0x40 */ + CiALIAS | CiVAR1, CiUPPER | CiHEXLT, + CiUPPER | CiHEXLT, CiUPPER | CiHEXLT, + CiUPPER | CiHEXLT, CiUPPER | CiHEXLT, + CiUPPER | CiHEXLT, CiUPPER, + CiUPPER, CiUPPER, CiUPPER, CiUPPER, + CiUPPER, CiUPPER, CiUPPER, CiUPPER, + /* 0x50 */ + CiUPPER, CiUPPER, CiUPPER, CiUPPER, + CiUPPER, CiUPPER, CiUPPER, CiUPPER, + CiUPPER, CiUPPER, CiUPPER, CiQCX | CiBRACK, + CiQCX, CiBRACK, CiQCM, CiUNDER, + /* 0x60 */ + CiGRAVE, CiLOWER | CiHEXLT, + CiLOWER | CiHEXLT, CiLOWER | CiHEXLT, + CiLOWER | CiHEXLT, CiLOWER | CiHEXLT, + CiLOWER | CiHEXLT, CiLOWER, + CiLOWER, CiLOWER, CiLOWER, CiLOWER, + CiLOWER, CiLOWER, CiLOWER, CiLOWER, + /* 0x70 */ + CiLOWER, CiLOWER, CiLOWER, CiLOWER, + CiLOWER, CiLOWER, CiLOWER, CiLOWER, + CiLOWER, CiLOWER, CiLOWER, CiCURLY, + CiQCL, CiCURLY, CiQCM, CiCNTRL +}; + +void +set_ifs(const char *s) +{ +#if defined(MKSH_EBCDIC) || defined(MKSH_FAUX_EBCDIC) + int i = 256; + + memset(ksh_ctypes, 0, sizeof(ksh_ctypes)); + while (i--) + if (ebcdic_map[i] < 0x80U) + ksh_ctypes[i] = tpl_ctypes[ebcdic_map[i]]; +#else + memcpy(ksh_ctypes, tpl_ctypes, sizeof(tpl_ctypes)); + memset((char *)ksh_ctypes + sizeof(tpl_ctypes), '\0', + sizeof(ksh_ctypes) - sizeof(tpl_ctypes)); +#endif + ifs0 = *s; + while (*s) + ksh_ctypes[ord(*s++)] |= CiIFS; +} + +#if defined(MKSH_EBCDIC) || defined(MKSH_FAUX_EBCDIC) +#include + +/* + * Many headaches with EBCDIC: + * 1. There are numerous EBCDIC variants, and it is not feasible for us + * to support them all. But we can support the EBCDIC code pages that + * contain all (most?) of the characters in ASCII, and these + * usually tend to agree on the code points assigned to the ASCII + * subset. If you need a representative example, look at EBCDIC 1047, + * which is first among equals in the IBM MVS development + * environment: https://en.wikipedia.org/wiki/EBCDIC_1047 + * Unfortunately, the square brackets are not consistently mapped, + * and for certain reasons, we need an unambiguous bijective + * mapping between EBCDIC and "extended ASCII". + * 2. Character ranges that are contiguous in ASCII, like the letters + * in [A-Z], are broken up into segments (i.e. [A-IJ-RS-Z]), so we + * can't implement e.g. islower() as { return c >= 'a' && c <= 'z'; } + * because it will also return true for a handful of extraneous + * characters (like the plus-minus sign at 0x8F in EBCDIC 1047, a + * little after 'i'). But at least '_' is not one of these. + * 3. The normal [0-9A-Za-z] characters are at codepoints beyond 0x80. + * Not only do they require all 8 bits instead of 7, if chars are + * signed, they will have negative integer values! Something like + * (c - 'A') could actually become (c + 63)! Use the ord() macro to + * ensure you're getting a value in [0, 255] (ORD for constants). + * 4. '\n' is actually NL (0x15, U+0085) instead of LF (0x25, U+000A). + * EBCDIC has a proper newline character instead of "emulating" one + * with line feeds, although this is mapped to LF for our purposes. + * 5. Note that it is possible to compile programs in ASCII mode on IBM + * mainframe systems, using the -qascii option to the XL C compiler. + * We can determine the build mode by looking at __CHARSET_LIB: + * 0 == EBCDIC, 1 == ASCII + */ + +void +ebcdic_init(void) +{ + int i = 256; + unsigned char t; + bool mapcache[256]; + + while (i--) + ebcdic_rtt_toascii[i] = i; + memset(ebcdic_rtt_fromascii, 0xFF, sizeof(ebcdic_rtt_fromascii)); + setlocale(LC_ALL, ""); +#ifdef MKSH_EBCDIC + if (__etoa_l(ebcdic_rtt_toascii, 256) != 256) { + write(2, "mksh: could not map EBCDIC to ASCII\n", 36); + exit(255); + } +#endif + + memset(mapcache, 0, sizeof(mapcache)); + i = 256; + while (i--) { + t = ebcdic_rtt_toascii[i]; + /* ensure unique round-trip capable mapping */ + if (mapcache[t]) { + write(2, "mksh: duplicate EBCDIC to ASCII mapping\n", 40); + exit(255); + } + /* + * since there are 256 input octets, this also ensures + * the other mapping direction is completely filled + */ + mapcache[t] = true; + /* fill the complete round-trip map */ + ebcdic_rtt_fromascii[t] = i; + /* + * Only use the converted value if it's in the range + * [0x00; 0x7F], which I checked; the "extended ASCII" + * characters can be any encoding, not just Latin1, + * and the C1 control characters other than NEL are + * hopeless, but we map EBCDIC NEL to ASCII LF so we + * cannot even use C1 NEL. + * If ever we map to Unicode, bump the table width to + * an unsigned int, and or the raw unconverted EBCDIC + * values with 0x01000000 instead. + */ + if (t < 0x80U) + ebcdic_map[i] = (unsigned short)ord(t); + else + ebcdic_map[i] = (unsigned short)(0x100U | ord(i)); + } + if (ebcdic_rtt_toascii[0] || ebcdic_rtt_fromascii[0] || ebcdic_map[0]) { + write(2, "mksh: NUL not at position 0\n", 28); + exit(255); + } +} +#endif diff --git a/strlcpy.c b/strlcpy.c new file mode 100644 index 0000000..ba8581e --- /dev/null +++ b/strlcpy.c @@ -0,0 +1,53 @@ +/*- + * Copyright (c) 2006, 2008, 2009, 2013 + * mirabilos + * Copyright (c) 1998 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/strlcpy.c,v 1.10 2015/11/29 17:05:02 tg Exp $"); + +/* + * Copy src to string dst of size siz. At most siz-1 characters + * will be copied. Always NUL terminates (unless siz == 0). + * Returns strlen(src); if retval >= siz, truncation occurred. + */ +#undef strlcpy +size_t +strlcpy(char *dst, const char *src, size_t siz) +{ + const char *s = src; + + if (siz == 0) + goto traverse_src; + + /* copy as many chars as will fit */ + while (--siz && (*dst++ = *s++)) + ; + + /* not enough room in dst */ + if (siz == 0) { + /* safe to NUL-terminate dst since we copied <= siz-1 chars */ + *dst = '\0'; + traverse_src: + /* traverse rest of src */ + while (*s++) + ; + } + + /* count does not include NUL */ + return ((size_t)(s - src - 1)); +} diff --git a/syn.c b/syn.c new file mode 100644 index 0000000..e4c38e3 --- /dev/null +++ b/syn.c @@ -0,0 +1,1193 @@ +/* $OpenBSD: syn.c,v 1.30 2015/09/01 13:12:31 tedu Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, + * 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/syn.c,v 1.127 2018/01/14 00:22:30 tg Exp $"); + +struct nesting_state { + int start_token; /* token than began nesting (eg, FOR) */ + int start_line; /* line nesting began on */ +}; + +struct yyrecursive_state { + struct ioword *old_heres[HERES]; + struct yyrecursive_state *next; + struct ioword **old_herep; + int old_symbol; + unsigned int old_nesting_type; + bool old_reject; +}; + +static void yyparse(bool); +static struct op *pipeline(int, int); +static struct op *andor(int); +static struct op *c_list(int, bool); +static struct ioword *synio(int); +static struct op *nested(int, int, int, int); +static struct op *get_command(int, int); +static struct op *dogroup(int); +static struct op *thenpart(int); +static struct op *elsepart(int); +static struct op *caselist(int); +static struct op *casepart(int, int); +static struct op *function_body(char *, int, bool); +static char **wordlist(int); +static struct op *block(int, struct op *, struct op *); +static struct op *newtp(int); +static void syntaxerr(const char *) MKSH_A_NORETURN; +static void nesting_push(struct nesting_state *, int); +static void nesting_pop(struct nesting_state *); +static int inalias(struct source *) MKSH_A_PURE; +static Test_op dbtestp_isa(Test_env *, Test_meta); +static const char *dbtestp_getopnd(Test_env *, Test_op, bool); +static int dbtestp_eval(Test_env *, Test_op, const char *, + const char *, bool); +static void dbtestp_error(Test_env *, int, const char *) MKSH_A_NORETURN; + +static struct op *outtree; /* yyparse output */ +static struct nesting_state nesting; /* \n changed to ; */ + +static bool reject; /* token(cf) gets symbol again */ +static int symbol; /* yylex value */ + +#define REJECT (reject = true) +#define ACCEPT (reject = false) +#define token(cf) ((reject) ? (ACCEPT, symbol) : (symbol = yylex(cf))) +#define tpeek(cf) ((reject) ? (symbol) : (REJECT, symbol = yylex(cf))) +#define musthave(c,cf) do { \ + if ((unsigned int)token(cf) != (unsigned int)(c)) \ + syntaxerr(NULL); \ +} while (/* CONSTCOND */ 0) + +static const char Tcbrace[] = "}"; +static const char Tesac[] = "esac"; + +static void +yyparse(bool doalias) +{ + int c; + + ACCEPT; + + outtree = c_list(doalias ? ALIAS : 0, source->type == SSTRING); + c = tpeek(0); + if (c == 0 && !outtree) + outtree = newtp(TEOF); + else if (!cinttype(c, C_LF | C_NUL)) + syntaxerr(NULL); +} + +static struct op * +pipeline(int cf, int sALIAS) +{ + struct op *t, *p, *tl = NULL; + + t = get_command(cf, sALIAS); + if (t != NULL) { + while (token(0) == '|') { + if ((p = get_command(CONTIN, sALIAS)) == NULL) + syntaxerr(NULL); + if (tl == NULL) + t = tl = block(TPIPE, t, p); + else + tl = tl->right = block(TPIPE, tl->right, p); + } + REJECT; + } + return (t); +} + +static struct op * +andor(int sALIAS) +{ + struct op *t, *p; + int c; + + t = pipeline(0, sALIAS); + if (t != NULL) { + while ((c = token(0)) == LOGAND || c == LOGOR) { + if ((p = pipeline(CONTIN, sALIAS)) == NULL) + syntaxerr(NULL); + t = block(c == LOGAND? TAND: TOR, t, p); + } + REJECT; + } + return (t); +} + +static struct op * +c_list(int sALIAS, bool multi) +{ + struct op *t = NULL, *p, *tl = NULL; + int c; + bool have_sep; + + while (/* CONSTCOND */ 1) { + p = andor(sALIAS); + /* + * Token has always been read/rejected at this point, so + * we don't worry about what flags to pass token() + */ + c = token(0); + have_sep = true; + if (c == '\n' && (multi || inalias(source))) { + if (!p) + /* ignore blank lines */ + continue; + } else if (!p) + break; + else if (c == '&' || c == COPROC) + p = block(c == '&' ? TASYNC : TCOPROC, p, NULL); + else if (c != ';') + have_sep = false; + if (!t) + t = p; + else if (!tl) + t = tl = block(TLIST, t, p); + else + tl = tl->right = block(TLIST, tl->right, p); + if (!have_sep) + break; + } + REJECT; + return (t); +} + +static const char IONDELIM_delim[] = { CHAR, '<', CHAR, '<', EOS }; + +static struct ioword * +synio(int cf) +{ + struct ioword *iop; + static struct ioword *nextiop; + bool ishere; + + if (nextiop != NULL) { + iop = nextiop; + nextiop = NULL; + return (iop); + } + + if (tpeek(cf) != REDIR) + return (NULL); + ACCEPT; + iop = yylval.iop; + ishere = (iop->ioflag & IOTYPE) == IOHERE; + if (iop->ioflag & IOHERESTR) { + musthave(LWORD, 0); + } else if (ishere && tpeek(HEREDELIM) == '\n') { + ACCEPT; + yylval.cp = wdcopy(IONDELIM_delim, ATEMP); + iop->ioflag |= IOEVAL | IONDELIM; + } else + musthave(LWORD, ishere ? HEREDELIM : 0); + if (ishere) { + iop->delim = yylval.cp; + if (*ident != 0 && !(iop->ioflag & IOHERESTR)) { + /* unquoted */ + iop->ioflag |= IOEVAL; + } + if (herep > &heres[HERES - 1]) + yyerror(Tf_toomany, "<<"); + *herep++ = iop; + } else + iop->ioname = yylval.cp; + + if (iop->ioflag & IOBASH) { + char *cp; + + nextiop = alloc(sizeof(*iop), ATEMP); + nextiop->ioname = cp = alloc(3, ATEMP); + *cp++ = CHAR; + *cp++ = digits_lc[iop->unit % 10]; + *cp = EOS; + + iop->ioflag &= ~IOBASH; + nextiop->unit = 2; + nextiop->ioflag = IODUP; + nextiop->delim = NULL; + nextiop->heredoc = NULL; + } + return (iop); +} + +static struct op * +nested(int type, int smark, int emark, int sALIAS) +{ + struct op *t; + struct nesting_state old_nesting; + + nesting_push(&old_nesting, smark); + t = c_list(sALIAS, true); + musthave(emark, KEYWORD|sALIAS); + nesting_pop(&old_nesting); + return (block(type, t, NULL)); +} + +static const char builtin_cmd[] = { + QCHAR, '\\', CHAR, 'b', CHAR, 'u', CHAR, 'i', + CHAR, 'l', CHAR, 't', CHAR, 'i', CHAR, 'n', EOS +}; +static const char let_cmd[] = { + CHAR, 'l', CHAR, 'e', CHAR, 't', EOS +}; +static const char setA_cmd0[] = { + CHAR, 's', CHAR, 'e', CHAR, 't', EOS +}; +static const char setA_cmd1[] = { + CHAR, '-', CHAR, 'A', EOS +}; +static const char setA_cmd2[] = { + CHAR, '-', CHAR, '-', EOS +}; + +static struct op * +get_command(int cf, int sALIAS) +{ + struct op *t; + int c, iopn = 0, syniocf, lno; + struct ioword *iop, **iops; + XPtrV args, vars; + struct nesting_state old_nesting; + + /* NUFILE is small enough to leave this addition unchecked */ + iops = alloc2((NUFILE + 1), sizeof(struct ioword *), ATEMP); + XPinit(args, 16); + XPinit(vars, 16); + + syniocf = KEYWORD|sALIAS; + switch (c = token(cf|KEYWORD|sALIAS|CMDASN)) { + default: + REJECT; + afree(iops, ATEMP); + XPfree(args); + XPfree(vars); + /* empty line */ + return (NULL); + + case LWORD: + case REDIR: + REJECT; + syniocf &= ~(KEYWORD|sALIAS); + t = newtp(TCOM); + t->lineno = source->line; + goto get_command_start; + while (/* CONSTCOND */ 1) { + bool check_decl_utility; + + if (XPsize(args) == 0) { + get_command_start: + check_decl_utility = true; + cf = sALIAS | CMDASN; + } else if (t->u.evalflags) + cf = CMDWORD | CMDASN; + else + cf = CMDWORD; + switch (tpeek(cf)) { + case REDIR: + while ((iop = synio(cf)) != NULL) { + if (iopn >= NUFILE) + yyerror(Tf_toomany, + Tredirection); + iops[iopn++] = iop; + } + break; + + case LWORD: + ACCEPT; + if (check_decl_utility) { + struct tbl *tt = get_builtin(ident); + uint32_t flag; + + flag = tt ? tt->flag : 0; + if (flag & DECL_UTIL) + t->u.evalflags = DOVACHECK; + if (!(flag & DECL_FWDR)) + check_decl_utility = false; + } + if ((XPsize(args) == 0 || Flag(FKEYWORD)) && + is_wdvarassign(yylval.cp)) + XPput(vars, yylval.cp); + else + XPput(args, yylval.cp); + break; + + case ORD('(' /*)*/): + if (XPsize(args) == 0 && XPsize(vars) == 1 && + is_wdvarassign(yylval.cp)) { + char *tcp; + + /* wdarrassign: foo=(bar) */ + ACCEPT; + + /* manipulate the vars string */ + tcp = XPptrv(vars)[(vars.len = 0)]; + /* 'varname=' -> 'varname' */ + tcp[wdscan(tcp, EOS) - tcp - 3] = EOS; + + /* construct new args strings */ + XPput(args, wdcopy(builtin_cmd, ATEMP)); + XPput(args, wdcopy(setA_cmd0, ATEMP)); + XPput(args, wdcopy(setA_cmd1, ATEMP)); + XPput(args, tcp); + XPput(args, wdcopy(setA_cmd2, ATEMP)); + + /* slurp in words till closing paren */ + while (token(CONTIN) == LWORD) + XPput(args, yylval.cp); + if (symbol != /*(*/ ')') + syntaxerr(NULL); + } else { + /* + * Check for "> foo (echo hi)" + * which AT&T ksh allows (not + * POSIX, but not disallowed) + */ + afree(t, ATEMP); + if (XPsize(args) == 0 && + XPsize(vars) == 0) { + ACCEPT; + goto Subshell; + } + + /* must be a function */ + if (iopn != 0 || XPsize(args) != 1 || + XPsize(vars) != 0) + syntaxerr(NULL); + ACCEPT; + musthave(/*(*/ ')', 0); + t = function_body(XPptrv(args)[0], + sALIAS, false); + } + goto Leave; + + default: + goto Leave; + } + } + Leave: + break; + + case ORD('(' /*)*/): { + unsigned int subshell_nesting_type_saved; + Subshell: + subshell_nesting_type_saved = subshell_nesting_type; + subshell_nesting_type = ORD(')'); + t = nested(TPAREN, ORD('('), ORD(')'), sALIAS); + subshell_nesting_type = subshell_nesting_type_saved; + break; + } + + case ORD('{' /*}*/): + t = nested(TBRACE, ORD('{'), ORD('}'), sALIAS); + break; + + case MDPAREN: + /* leave KEYWORD in syniocf (allow if (( 1 )) then ...) */ + lno = source->line; + ACCEPT; + switch (token(LETEXPR)) { + case LWORD: + break; + case ORD('(' /*)*/): + c = ORD('('); + goto Subshell; + default: + syntaxerr(NULL); + } + t = newtp(TCOM); + t->lineno = lno; + XPput(args, wdcopy(builtin_cmd, ATEMP)); + XPput(args, wdcopy(let_cmd, ATEMP)); + XPput(args, yylval.cp); + break; + + case DBRACKET: /* [[ .. ]] */ + /* leave KEYWORD in syniocf (allow if [[ -n 1 ]] then ...) */ + t = newtp(TDBRACKET); + ACCEPT; + { + Test_env te; + + te.flags = TEF_DBRACKET; + te.pos.av = &args; + te.isa = dbtestp_isa; + te.getopnd = dbtestp_getopnd; + te.eval = dbtestp_eval; + te.error = dbtestp_error; + + test_parse(&te); + } + break; + + case FOR: + case SELECT: + t = newtp((c == FOR) ? TFOR : TSELECT); + musthave(LWORD, CMDASN); + if (!is_wdvarname(yylval.cp, true)) + yyerror("%s: bad identifier", + c == FOR ? "for" : Tselect); + strdupx(t->str, ident, ATEMP); + nesting_push(&old_nesting, c); + t->vars = wordlist(sALIAS); + t->left = dogroup(sALIAS); + nesting_pop(&old_nesting); + break; + + case WHILE: + case UNTIL: + nesting_push(&old_nesting, c); + t = newtp((c == WHILE) ? TWHILE : TUNTIL); + t->left = c_list(sALIAS, true); + t->right = dogroup(sALIAS); + nesting_pop(&old_nesting); + break; + + case CASE: + t = newtp(TCASE); + musthave(LWORD, 0); + t->str = yylval.cp; + nesting_push(&old_nesting, c); + t->left = caselist(sALIAS); + nesting_pop(&old_nesting); + break; + + case IF: + nesting_push(&old_nesting, c); + t = newtp(TIF); + t->left = c_list(sALIAS, true); + t->right = thenpart(sALIAS); + musthave(FI, KEYWORD|sALIAS); + nesting_pop(&old_nesting); + break; + + case BANG: + syniocf &= ~(KEYWORD|sALIAS); + t = pipeline(0, sALIAS); + if (t == NULL) + syntaxerr(NULL); + t = block(TBANG, NULL, t); + break; + + case TIME: + syniocf &= ~(KEYWORD|sALIAS); + t = pipeline(0, sALIAS); + if (t && t->type == TCOM) { + t->str = alloc(2, ATEMP); + /* TF_* flags */ + t->str[0] = '\0'; + t->str[1] = '\0'; + } + t = block(TTIME, t, NULL); + break; + + case FUNCTION: + musthave(LWORD, 0); + t = function_body(yylval.cp, sALIAS, true); + break; + } + + while ((iop = synio(syniocf)) != NULL) { + if (iopn >= NUFILE) + yyerror(Tf_toomany, Tredirection); + iops[iopn++] = iop; + } + + if (iopn == 0) { + afree(iops, ATEMP); + t->ioact = NULL; + } else { + iops[iopn++] = NULL; + iops = aresize2(iops, iopn, sizeof(struct ioword *), ATEMP); + t->ioact = iops; + } + + if (t->type == TCOM || t->type == TDBRACKET) { + XPput(args, NULL); + t->args = (const char **)XPclose(args); + XPput(vars, NULL); + t->vars = (char **)XPclose(vars); + } else { + XPfree(args); + XPfree(vars); + } + + if (c == MDPAREN) { + t = block(TBRACE, t, NULL); + t->ioact = t->left->ioact; + t->left->ioact = NULL; + } + + return (t); +} + +static struct op * +dogroup(int sALIAS) +{ + int c; + struct op *list; + + c = token(CONTIN|KEYWORD|sALIAS); + /* + * A {...} can be used instead of do...done for for/select loops + * but not for while/until loops - we don't need to check if it + * is a while loop because it would have been parsed as part of + * the conditional command list... + */ + if (c == DO) + c = DONE; + else if ((unsigned int)c == ORD('{')) + c = ORD('}'); + else + syntaxerr(NULL); + list = c_list(sALIAS, true); + musthave(c, KEYWORD|sALIAS); + return (list); +} + +static struct op * +thenpart(int sALIAS) +{ + struct op *t; + + musthave(THEN, KEYWORD|sALIAS); + t = newtp(0); + t->left = c_list(sALIAS, true); + if (t->left == NULL) + syntaxerr(NULL); + t->right = elsepart(sALIAS); + return (t); +} + +static struct op * +elsepart(int sALIAS) +{ + struct op *t; + + switch (token(KEYWORD|sALIAS|CMDASN)) { + case ELSE: + if ((t = c_list(sALIAS, true)) == NULL) + syntaxerr(NULL); + return (t); + + case ELIF: + t = newtp(TELIF); + t->left = c_list(sALIAS, true); + t->right = thenpart(sALIAS); + return (t); + + default: + REJECT; + } + return (NULL); +} + +static struct op * +caselist(int sALIAS) +{ + struct op *t, *tl; + int c; + + c = token(CONTIN|KEYWORD|sALIAS); + /* A {...} can be used instead of in...esac for case statements */ + if (c == IN) + c = ESAC; + else if ((unsigned int)c == ORD('{')) + c = ORD('}'); + else + syntaxerr(NULL); + t = tl = NULL; + /* no ALIAS here */ + while ((tpeek(CONTIN|KEYWORD|ESACONLY)) != c) { + struct op *tc = casepart(c, sALIAS); + if (tl == NULL) + t = tl = tc, tl->right = NULL; + else + tl->right = tc, tl = tc; + } + musthave(c, KEYWORD|sALIAS); + return (t); +} + +static struct op * +casepart(int endtok, int sALIAS) +{ + struct op *t; + XPtrV ptns; + + XPinit(ptns, 16); + t = newtp(TPAT); + /* no ALIAS here */ + if ((unsigned int)token(CONTIN | KEYWORD) != ORD('(')) + REJECT; + do { + switch (token(0)) { + case LWORD: + break; + case ORD('}'): + case ESAC: + if (symbol != endtok) { + strdupx(yylval.cp, (unsigned int)symbol == + ORD('}') ? Tcbrace : Tesac, ATEMP); + break; + } + /* FALLTHROUGH */ + default: + syntaxerr(NULL); + } + XPput(ptns, yylval.cp); + } while (token(0) == '|'); + REJECT; + XPput(ptns, NULL); + t->vars = (char **)XPclose(ptns); + musthave(ORD(')'), 0); + + t->left = c_list(sALIAS, true); + + /* initialise to default for ;; or omitted */ + t->u.charflag = ORD(';'); + /* SUSv4 requires the ;; except in the last casepart */ + if ((tpeek(CONTIN|KEYWORD|sALIAS)) != endtok) + switch (symbol) { + default: + syntaxerr(NULL); + case BRKEV: + t->u.charflag = ORD('|'); + if (0) + /* FALLTHROUGH */ + case BRKFT: + t->u.charflag = ORD('&'); + /* FALLTHROUGH */ + case BREAK: + /* initialised above, but we need to eat the token */ + ACCEPT; + } + return (t); +} + +static struct op * +function_body(char *name, int sALIAS, + /* function foo { ... } vs foo() { .. } */ + bool ksh_func) +{ + char *sname, *p; + struct op *t; + + sname = wdstrip(name, 0); + /*- + * Check for valid characters in name. POSIX and AT&T ksh93 say + * only allow [a-zA-Z_0-9] but this allows more as old pdkshs + * have allowed more; the following were never allowed: + * NUL TAB NL SP " $ & ' ( ) ; < = > \ ` | + * C_QUOTE|C_SPC covers all but adds # * ? [ ] + */ + for (p = sname; *p; p++) + if (ctype(*p, C_QUOTE | C_SPC)) + yyerror(Tinvname, sname, Tfunction); + + /* + * Note that POSIX allows only compound statements after foo(), + * sh and AT&T ksh allow any command, go with the later since it + * shouldn't break anything. However, for function foo, AT&T ksh + * only accepts an open-brace. + */ + if (ksh_func) { + if ((unsigned int)tpeek(CONTIN|KEYWORD|sALIAS) == ORD('(' /*)*/)) { + /* function foo () { //}*/ + ACCEPT; + musthave(ORD(/*(*/ ')'), 0); + /* degrade to POSIX function */ + ksh_func = false; + } + musthave(ORD('{' /*}*/), CONTIN|KEYWORD|sALIAS); + REJECT; + } + + t = newtp(TFUNCT); + t->str = sname; + t->u.ksh_func = tobool(ksh_func); + t->lineno = source->line; + + if ((t->left = get_command(CONTIN, sALIAS)) == NULL) { + char *tv; + /* + * Probably something like foo() followed by EOF or ';'. + * This is accepted by sh and ksh88. + * To make "typeset -f foo" work reliably (so its output can + * be used as input), we pretend there is a colon here. + */ + t->left = newtp(TCOM); + /* (2 * sizeof(char *)) is small enough */ + t->left->args = alloc(2 * sizeof(char *), ATEMP); + t->left->args[0] = tv = alloc(3, ATEMP); + tv[0] = QCHAR; + tv[1] = ':'; + tv[2] = EOS; + t->left->args[1] = NULL; + t->left->vars = alloc(sizeof(char *), ATEMP); + t->left->vars[0] = NULL; + t->left->lineno = 1; + } + + return (t); +} + +static char ** +wordlist(int sALIAS) +{ + int c; + XPtrV args; + + XPinit(args, 16); + /* POSIX does not do alias expansion here... */ + if ((c = token(CONTIN|KEYWORD|sALIAS)) != IN) { + if (c != ';') + /* non-POSIX, but AT&T ksh accepts a ; here */ + REJECT; + return (NULL); + } + while ((c = token(0)) == LWORD) + XPput(args, yylval.cp); + if (c != '\n' && c != ';') + syntaxerr(NULL); + XPput(args, NULL); + return ((char **)XPclose(args)); +} + +/* + * supporting functions + */ + +static struct op * +block(int type, struct op *t1, struct op *t2) +{ + struct op *t; + + t = newtp(type); + t->left = t1; + t->right = t2; + return (t); +} + +static const struct tokeninfo { + const char *name; + short val; + short reserved; +} tokentab[] = { + /* Reserved words */ + { "if", IF, true }, + { "then", THEN, true }, + { "else", ELSE, true }, + { "elif", ELIF, true }, + { "fi", FI, true }, + { "case", CASE, true }, + { Tesac, ESAC, true }, + { "for", FOR, true }, + { Tselect, SELECT, true }, + { "while", WHILE, true }, + { "until", UNTIL, true }, + { "do", DO, true }, + { "done", DONE, true }, + { "in", IN, true }, + { Tfunction, FUNCTION, true }, + { Ttime, TIME, true }, + { "{", ORD('{'), true }, + { Tcbrace, ORD('}'), true }, + { "!", BANG, true }, + { "[[", DBRACKET, true }, + /* Lexical tokens (0[EOF], LWORD and REDIR handled specially) */ + { "&&", LOGAND, false }, + { "||", LOGOR, false }, + { ";;", BREAK, false }, + { ";|", BRKEV, false }, + { ";&", BRKFT, false }, + { "((", MDPAREN, false }, + { "|&", COPROC, false }, + /* and some special cases... */ + { "newline", ORD('\n'), false }, + { NULL, 0, false } +}; + +void +initkeywords(void) +{ + struct tokeninfo const *tt; + struct tbl *p; + + ktinit(APERM, &keywords, + /* currently 28 keywords: 75% of 64 = 2^6 */ + 6); + for (tt = tokentab; tt->name; tt++) { + if (tt->reserved) { + p = ktenter(&keywords, tt->name, hash(tt->name)); + p->flag |= DEFINED|ISSET; + p->type = CKEYWD; + p->val.i = tt->val; + } + } +} + +static void +syntaxerr(const char *what) +{ + /* 23<<- is the longest redirection, I think */ + char redir[8]; + const char *s; + struct tokeninfo const *tt; + int c; + + if (!what) + what = Tunexpected; + REJECT; + c = token(0); + Again: + switch (c) { + case 0: + if (nesting.start_token) { + c = nesting.start_token; + source->errline = nesting.start_line; + what = "unmatched"; + goto Again; + } + /* don't quote the EOF */ + yyerror("%s: unexpected EOF", Tsynerr); + /* NOTREACHED */ + + case LWORD: + s = snptreef(NULL, 32, Tf_S, yylval.cp); + break; + + case REDIR: + s = snptreef(redir, sizeof(redir), Tft_R, yylval.iop); + break; + + default: + for (tt = tokentab; tt->name; tt++) + if (tt->val == c) + break; + if (tt->name) + s = tt->name; + else { + if (c > 0 && c < 256) { + redir[0] = c; + redir[1] = '\0'; + } else + shf_snprintf(redir, sizeof(redir), + "?%d", c); + s = redir; + } + } + yyerror(Tf_sD_s_qs, Tsynerr, what, s); +} + +static void +nesting_push(struct nesting_state *save, int tok) +{ + *save = nesting; + nesting.start_token = tok; + nesting.start_line = source->line; +} + +static void +nesting_pop(struct nesting_state *saved) +{ + nesting = *saved; +} + +static struct op * +newtp(int type) +{ + struct op *t; + + t = alloc(sizeof(struct op), ATEMP); + t->type = type; + t->u.evalflags = 0; + t->args = NULL; + t->vars = NULL; + t->ioact = NULL; + t->left = t->right = NULL; + t->str = NULL; + return (t); +} + +struct op * +compile(Source *s, bool skiputf8bom, bool doalias) +{ + nesting.start_token = 0; + nesting.start_line = 0; + herep = heres; + source = s; + if (skiputf8bom) + yyskiputf8bom(); + yyparse(doalias); + return (outtree); +} + +/* Check if we are in the middle of reading an alias */ +static int +inalias(struct source *s) +{ + while (s && s->type == SALIAS) { + if (!(s->flags & SF_ALIASEND)) + return (1); + s = s->next; + } + return (0); +} + + +/* + * Order important - indexed by Test_meta values + * Note that ||, &&, ( and ) can't appear in as unquoted strings + * in normal shell input, so these can be interpreted unambiguously + * in the evaluation pass. + */ +static const char dbtest_or[] = { CHAR, '|', CHAR, '|', EOS }; +static const char dbtest_and[] = { CHAR, '&', CHAR, '&', EOS }; +static const char dbtest_not[] = { CHAR, '!', EOS }; +static const char dbtest_oparen[] = { CHAR, '(', EOS }; +static const char dbtest_cparen[] = { CHAR, ')', EOS }; +const char * const dbtest_tokens[] = { + dbtest_or, dbtest_and, dbtest_not, + dbtest_oparen, dbtest_cparen +}; +static const char db_close[] = { CHAR, ']', CHAR, ']', EOS }; +static const char db_lthan[] = { CHAR, '<', EOS }; +static const char db_gthan[] = { CHAR, '>', EOS }; + +/* + * Test if the current token is a whatever. Accepts the current token if + * it is. Returns 0 if it is not, non-zero if it is (in the case of + * TM_UNOP and TM_BINOP, the returned value is a Test_op). + */ +static Test_op +dbtestp_isa(Test_env *te, Test_meta meta) +{ + int c = tpeek(CMDASN | (meta == TM_BINOP ? 0 : CONTIN)); + bool uqword; + char *save = NULL; + Test_op ret = TO_NONOP; + + /* unquoted word? */ + uqword = c == LWORD && *ident; + + if (meta == TM_OR) + ret = c == LOGOR ? TO_NONNULL : TO_NONOP; + else if (meta == TM_AND) + ret = c == LOGAND ? TO_NONNULL : TO_NONOP; + else if (meta == TM_NOT) + ret = (uqword && !strcmp(yylval.cp, + dbtest_tokens[(int)TM_NOT])) ? TO_NONNULL : TO_NONOP; + else if (meta == TM_OPAREN) + ret = (unsigned int)c == ORD('(') /*)*/ ? TO_NONNULL : TO_NONOP; + else if (meta == TM_CPAREN) + ret = (unsigned int)c == /*(*/ ORD(')') ? TO_NONNULL : TO_NONOP; + else if (meta == TM_UNOP || meta == TM_BINOP) { + if (meta == TM_BINOP && c == REDIR && + (yylval.iop->ioflag == IOREAD || + yylval.iop->ioflag == IOWRITE)) { + ret = TO_NONNULL; + save = wdcopy(yylval.iop->ioflag == IOREAD ? + db_lthan : db_gthan, ATEMP); + } else if (uqword && (ret = test_isop(meta, ident))) + save = yylval.cp; + } else + /* meta == TM_END */ + ret = (uqword && !strcmp(yylval.cp, + db_close)) ? TO_NONNULL : TO_NONOP; + if (ret != TO_NONOP) { + ACCEPT; + if ((unsigned int)meta < NELEM(dbtest_tokens)) + save = wdcopy(dbtest_tokens[(int)meta], ATEMP); + if (save) + XPput(*te->pos.av, save); + } + return (ret); +} + +static const char * +dbtestp_getopnd(Test_env *te, Test_op op MKSH_A_UNUSED, + bool do_eval MKSH_A_UNUSED) +{ + int c = tpeek(CMDASN); + + if (c != LWORD) + return (NULL); + + ACCEPT; + XPput(*te->pos.av, yylval.cp); + + return (null); +} + +static int +dbtestp_eval(Test_env *te MKSH_A_UNUSED, Test_op op MKSH_A_UNUSED, + const char *opnd1 MKSH_A_UNUSED, const char *opnd2 MKSH_A_UNUSED, + bool do_eval MKSH_A_UNUSED) +{ + return (1); +} + +static void +dbtestp_error(Test_env *te, int offset, const char *msg) +{ + te->flags |= TEF_ERROR; + + if (offset < 0) { + REJECT; + /* Kludgy to say the least... */ + symbol = LWORD; + yylval.cp = *(XPptrv(*te->pos.av) + XPsize(*te->pos.av) + + offset); + } + syntaxerr(msg); +} + +#if HAVE_SELECT + +#ifndef EOVERFLOW +#ifdef ERANGE +#define EOVERFLOW ERANGE +#else +#define EOVERFLOW EINVAL +#endif +#endif + +bool +parse_usec(const char *s, struct timeval *tv) +{ + struct timeval tt; + int i; + + tv->tv_sec = 0; + /* parse integral part */ + while (ctype(*s, C_DIGIT)) { + tt.tv_sec = tv->tv_sec * 10 + ksh_numdig(*s++); + /*XXX this overflow check maybe UB */ + if (tt.tv_sec / 10 != tv->tv_sec) { + errno = EOVERFLOW; + return (true); + } + tv->tv_sec = tt.tv_sec; + } + + tv->tv_usec = 0; + if (!*s) + /* no decimal fraction */ + return (false); + else if (*s++ != '.') { + /* junk after integral part */ + errno = EINVAL; + return (true); + } + + /* parse decimal fraction */ + i = 100000; + while (ctype(*s, C_DIGIT)) { + tv->tv_usec += i * ksh_numdig(*s++); + if (i == 1) + break; + i /= 10; + } + /* check for junk after fractional part */ + while (ctype(*s, C_DIGIT)) + ++s; + if (*s) { + errno = EINVAL; + return (true); + } + + /* end of input string reached, no errors */ + return (false); +} +#endif + +/* + * Helper function called from within lex.c:yylex() to parse + * a COMSUB recursively using the main shell parser and lexer + */ +char * +yyrecursive(int subtype) +{ + struct op *t; + char *cp; + struct yyrecursive_state *ys; + unsigned int stok, etok; + + if (subtype != COMSUB) { + stok = ORD('{'); + etok = ORD('}'); + } else { + stok = ORD('('); + etok = ORD(')'); + } + + ys = alloc(sizeof(struct yyrecursive_state), ATEMP); + + /* tell the lexer to accept a closing parenthesis as EOD */ + ys->old_nesting_type = subshell_nesting_type; + subshell_nesting_type = etok; + + /* push reject state, parse recursively, pop reject state */ + ys->old_reject = reject; + ys->old_symbol = symbol; + ACCEPT; + memcpy(ys->old_heres, heres, sizeof(heres)); + ys->old_herep = herep; + herep = heres; + ys->next = e->yyrecursive_statep; + e->yyrecursive_statep = ys; + /* we use TPAREN as a helper container here */ + t = nested(TPAREN, stok, etok, ALIAS); + yyrecursive_pop(false); + + /* t->left because nested(TPAREN, ...) hides our goodies there */ + cp = snptreef(NULL, 0, Tf_T, t->left); + tfree(t, ATEMP); + + return (cp); +} + +void +yyrecursive_pop(bool popall) +{ + struct yyrecursive_state *ys; + + popnext: + if (!(ys = e->yyrecursive_statep)) + return; + e->yyrecursive_statep = ys->next; + + memcpy(heres, ys->old_heres, sizeof(heres)); + herep = ys->old_herep; + reject = ys->old_reject; + symbol = ys->old_symbol; + + subshell_nesting_type = ys->old_nesting_type; + + afree(ys, ATEMP); + if (popall) + goto popnext; +} diff --git a/tree.c b/tree.c new file mode 100644 index 0000000..5e7326b --- /dev/null +++ b/tree.c @@ -0,0 +1,1176 @@ +/* $OpenBSD: tree.c,v 1.21 2015/09/01 13:12:31 tedu Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2015, 2016, 2017 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" + +__RCSID("$MirOS: src/bin/mksh/tree.c,v 1.95 2018/01/14 00:03:05 tg Exp $"); + +#define INDENT 8 + +static void ptree(struct op *, int, struct shf *); +static void pioact(struct shf *, struct ioword *); +static const char *wdvarput(struct shf *, const char *, int, int); +static void vfptreef(struct shf *, int, const char *, va_list); +static struct ioword **iocopy(struct ioword **, Area *); +static void iofree(struct ioword **, Area *); + +/* "foo& ; bar" and "foo |& ; bar" are invalid */ +static bool prevent_semicolon; + +static const char Telif_pT[] = "elif %T"; + +/* + * print a command tree + */ +static void +ptree(struct op *t, int indent, struct shf *shf) +{ + const char **w; + struct ioword **ioact; + struct op *t1; + int i; + const char *ccp; + + Chain: + if (t == NULL) + return; + switch (t->type) { + case TCOM: + prevent_semicolon = false; + /* special-case 'var=<args && + /* we have zero arguments, i.e. no program to run */ + t->args[0] == NULL && + /* we have exactly one variable assignment */ + t->vars[0] != NULL && t->vars[1] == NULL && + /* we have exactly one I/O redirection */ + t->ioact != NULL && t->ioact[0] != NULL && + t->ioact[1] == NULL && + /* of type "here document" (or "here string") */ + (t->ioact[0]->ioflag & IOTYPE) == IOHERE && + /* the variable assignment begins with a valid varname */ + (ccp = skip_wdvarname(t->vars[0], true)) != t->vars[0] && + /* and has no right-hand side (i.e. "varname=") */ + ccp[0] == CHAR && ((ccp[1] == '=' && ccp[2] == EOS) || + /* or "varname+=" */ (ccp[1] == '+' && ccp[2] == CHAR && + ccp[3] == '=' && ccp[4] == EOS))) { + fptreef(shf, indent, Tf_S, t->vars[0]); + break; + } + + if (t->vars) { + w = (const char **)t->vars; + while (*w) + fptreef(shf, indent, Tf_S_, *w++); + } else + shf_puts("#no-vars# ", shf); + if (t->args) { + w = t->args; + if (*w && **w == CHAR) { + char *cp = wdstrip(*w++, WDS_TPUTS); + + if (valid_alias_name(cp)) + shf_putc('\\', shf); + shf_puts(cp, shf); + shf_putc(' ', shf); + afree(cp, ATEMP); + } + while (*w) + fptreef(shf, indent, Tf_S_, *w++); + } else + shf_puts("#no-args# ", shf); + break; + case TEXEC: + t = t->left; + goto Chain; + case TPAREN: + fptreef(shf, indent + 2, "( %T) ", t->left); + break; + case TPIPE: + fptreef(shf, indent, "%T| ", t->left); + t = t->right; + goto Chain; + case TLIST: + fptreef(shf, indent, "%T%;", t->left); + t = t->right; + goto Chain; + case TOR: + case TAND: + fptreef(shf, indent, "%T%s %T", + t->left, (t->type == TOR) ? "||" : "&&", t->right); + break; + case TBANG: + shf_puts("! ", shf); + prevent_semicolon = false; + t = t->right; + goto Chain; + case TDBRACKET: + w = t->args; + shf_puts("[[", shf); + while (*w) + fptreef(shf, indent, Tf__S, *w++); + shf_puts(" ]] ", shf); + break; + case TSELECT: + case TFOR: + fptreef(shf, indent, "%s %s ", + (t->type == TFOR) ? "for" : Tselect, t->str); + if (t->vars != NULL) { + shf_puts("in ", shf); + w = (const char **)t->vars; + while (*w) + fptreef(shf, indent, Tf_S_, *w++); + fptreef(shf, indent, Tft_end); + } + fptreef(shf, indent + INDENT, "do%N%T", t->left); + fptreef(shf, indent, "%;done "); + break; + case TCASE: + fptreef(shf, indent, "case %S in", t->str); + for (t1 = t->left; t1 != NULL; t1 = t1->right) { + fptreef(shf, indent, "%N("); + w = (const char **)t1->vars; + while (*w) { + fptreef(shf, indent, "%S%c", *w, + (w[1] != NULL) ? '|' : ')'); + ++w; + } + fptreef(shf, indent + INDENT, "%N%T%N;%c", t1->left, + t1->u.charflag); + } + fptreef(shf, indent, "%Nesac "); + break; + case TELIF: + internal_errorf(TELIF_unexpected); + /* FALLTHROUGH */ + case TIF: + i = 2; + t1 = t; + goto process_TIF; + do { + t1 = t1->right; + i = 0; + fptreef(shf, indent, Tft_end); + process_TIF: + /* 5 == strlen("elif ") */ + fptreef(shf, indent + 5 - i, Telif_pT + i, t1->left); + t1 = t1->right; + if (t1->left != NULL) { + fptreef(shf, indent, Tft_end); + fptreef(shf, indent + INDENT, "%s%N%T", + "then", t1->left); + } + } while (t1->right && t1->right->type == TELIF); + if (t1->right != NULL) { + fptreef(shf, indent, Tft_end); + fptreef(shf, indent + INDENT, "%s%N%T", + "else", t1->right); + } + fptreef(shf, indent, "%;fi "); + break; + case TWHILE: + case TUNTIL: + /* 6 == strlen("while "/"until ") */ + fptreef(shf, indent + 6, Tf_s_T, + (t->type == TWHILE) ? "while" : "until", + t->left); + fptreef(shf, indent, Tft_end); + fptreef(shf, indent + INDENT, "do%N%T", t->right); + fptreef(shf, indent, "%;done "); + break; + case TBRACE: + fptreef(shf, indent + INDENT, "{%N%T", t->left); + fptreef(shf, indent, "%;} "); + break; + case TCOPROC: + fptreef(shf, indent, "%T|& ", t->left); + prevent_semicolon = true; + break; + case TASYNC: + fptreef(shf, indent, "%T& ", t->left); + prevent_semicolon = true; + break; + case TFUNCT: + fpFUNCTf(shf, indent, tobool(t->u.ksh_func), t->str, t->left); + break; + case TTIME: + fptreef(shf, indent, Tf_s_T, Ttime, t->left); + break; + default: + shf_puts("", shf); + prevent_semicolon = false; + break; + } + if ((ioact = t->ioact) != NULL) { + bool need_nl = false; + + while (*ioact != NULL) + pioact(shf, *ioact++); + /* Print here documents after everything else... */ + ioact = t->ioact; + while (*ioact != NULL) { + struct ioword *iop = *ioact++; + + /* heredoc is NULL when tracing (set -x) */ + if ((iop->ioflag & (IOTYPE | IOHERESTR)) == IOHERE && + iop->heredoc) { + shf_putc('\n', shf); + shf_puts(iop->heredoc, shf); + fptreef(shf, indent, Tf_s, + evalstr(iop->delim, 0)); + need_nl = true; + } + } + /* + * Last delimiter must be followed by a newline (this + * often leads to an extra blank line, but it's not + * worth worrying about) + */ + if (need_nl) { + shf_putc('\n', shf); + prevent_semicolon = true; + } + } +} + +static void +pioact(struct shf *shf, struct ioword *iop) +{ + unsigned short flag = iop->ioflag; + unsigned short type = flag & IOTYPE; + short expected; + + expected = (type == IOREAD || type == IORDWR || type == IOHERE) ? 0 : + (type == IOCAT || type == IOWRITE) ? 1 : + (type == IODUP && (iop->unit == !(flag & IORDUP))) ? iop->unit : + iop->unit + 1; + if (iop->unit != expected) + shf_fprintf(shf, Tf_d, (int)iop->unit); + + switch (type) { + case IOREAD: + shf_putc('<', shf); + break; + case IOHERE: + shf_puts("<<", shf); + if (flag & IOSKIP) + shf_putc('-', shf); + else if (flag & IOHERESTR) + shf_putc('<', shf); + break; + case IOCAT: + shf_puts(">>", shf); + break; + case IOWRITE: + shf_putc('>', shf); + if (flag & IOCLOB) + shf_putc('|', shf); + break; + case IORDWR: + shf_puts("<>", shf); + break; + case IODUP: + shf_puts(flag & IORDUP ? "<&" : ">&", shf); + break; + } + /* name/delim are NULL when printing syntax errors */ + if (type == IOHERE) { + if (iop->delim && !(iop->ioflag & IONDELIM)) + wdvarput(shf, iop->delim, 0, WDS_TPUTS); + } else if (iop->ioname) { + if (flag & IONAMEXP) + print_value_quoted(shf, iop->ioname); + else + wdvarput(shf, iop->ioname, 0, WDS_TPUTS); + } + shf_putc(' ', shf); + prevent_semicolon = false; +} + +/* variant of fputs for ptreef and wdstrip */ +static const char * +wdvarput(struct shf *shf, const char *wp, int quotelevel, int opmode) +{ + int c; + const char *cs; + + /*- + * problems: + * `...` -> $(...) + * 'foo' -> "foo" + * x${foo:-"hi"} -> x${foo:-hi} unless WDS_TPUTS + * x${foo:-'hi'} -> x${foo:-hi} + * could change encoding to: + * OQUOTE ["'] ... CQUOTE ["'] + * COMSUB [(`] ...\0 (handle $ ` \ and maybe " in `...` case) + */ + while (/* CONSTCOND */ 1) + switch (*wp++) { + case EOS: + return (--wp); + case ADELIM: + if (ord(*wp) == ORD(/*{*/ '}')) { + ++wp; + goto wdvarput_csubst; + } + /* FALLTHROUGH */ + case CHAR: + c = ord(*wp++); + shf_putc(c, shf); + break; + case QCHAR: + c = ord(*wp++); + if (opmode & WDS_TPUTS) + switch (c) { + case ORD('\n'): + if (quotelevel == 0) { + c = ORD('\''); + shf_putc(c, shf); + shf_putc(ORD('\n'), shf); + } + break; + default: + if (quotelevel == 0) + /* FALLTHROUGH */ + case ORD('"'): + case ORD('`'): + case ORD('$'): + case ORD('\\'): + shf_putc(ORD('\\'), shf); + break; + } + shf_putc(c, shf); + break; + case COMASUB: + case COMSUB: + shf_puts("$(", shf); + cs = ")"; + if (ord(*wp) == ORD('(' /*)*/)) + shf_putc(' ', shf); + pSUB: + while ((c = *wp++) != 0) + shf_putc(c, shf); + shf_puts(cs, shf); + break; + case FUNASUB: + case FUNSUB: + c = ORD(' '); + if (0) + /* FALLTHROUGH */ + case VALSUB: + c = ORD('|'); + shf_putc('$', shf); + shf_putc('{', shf); + shf_putc(c, shf); + cs = ";}"; + goto pSUB; + case EXPRSUB: + shf_puts("$((", shf); + cs = "))"; + goto pSUB; + case OQUOTE: + if (opmode & WDS_TPUTS) { + quotelevel++; + shf_putc('"', shf); + } + break; + case CQUOTE: + if (opmode & WDS_TPUTS) { + if (quotelevel) + quotelevel--; + shf_putc('"', shf); + } + break; + case OSUBST: + shf_putc('$', shf); + if (ord(*wp++) == ORD('{')) + shf_putc('{', shf); + while ((c = *wp++) != 0) + shf_putc(c, shf); + wp = wdvarput(shf, wp, 0, opmode); + break; + case CSUBST: + if (ord(*wp++) == ORD('}')) { + wdvarput_csubst: + shf_putc('}', shf); + } + return (wp); + case OPAT: + shf_putchar(*wp++, shf); + shf_putc('(', shf); + break; + case SPAT: + c = ORD('|'); + if (0) + /* FALLTHROUGH */ + case CPAT: + c = ORD(/*(*/ ')'); + shf_putc(c, shf); + break; + } +} + +/* + * this is the _only_ way to reliably handle + * variable args with an ANSI compiler + */ +/* VARARGS */ +void +fptreef(struct shf *shf, int indent, const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vfptreef(shf, indent, fmt, va); + va_end(va); +} + +/* VARARGS */ +char * +snptreef(char *s, ssize_t n, const char *fmt, ...) +{ + va_list va; + struct shf shf; + + shf_sopen(s, n, SHF_WR | (s ? 0 : SHF_DYNAMIC), &shf); + + va_start(va, fmt); + vfptreef(&shf, 0, fmt, va); + va_end(va); + + /* shf_sclose NUL terminates */ + return (shf_sclose(&shf)); +} + +static void +vfptreef(struct shf *shf, int indent, const char *fmt, va_list va) +{ + int c; + + while ((c = ord(*fmt++))) { + if (c == '%') { + switch ((c = ord(*fmt++))) { + case ORD('c'): + /* character (octet, probably) */ + shf_putchar(va_arg(va, int), shf); + break; + case ORD('s'): + /* string */ + shf_puts(va_arg(va, char *), shf); + break; + case ORD('S'): + /* word */ + wdvarput(shf, va_arg(va, char *), 0, WDS_TPUTS); + break; + case ORD('d'): + /* signed decimal */ + shf_fprintf(shf, Tf_d, va_arg(va, int)); + break; + case ORD('u'): + /* unsigned decimal */ + shf_fprintf(shf, "%u", va_arg(va, unsigned int)); + break; + case ORD('T'): + /* format tree */ + ptree(va_arg(va, struct op *), indent, shf); + goto dont_trash_prevent_semicolon; + case ORD(';'): + /* newline or ; */ + case ORD('N'): + /* newline or space */ + if (shf->flags & SHF_STRING) { + if ((unsigned int)c == ORD(';') && + !prevent_semicolon) + shf_putc(';', shf); + shf_putc(' ', shf); + } else { + int i; + + shf_putc('\n', shf); + i = indent; + while (i >= 8) { + shf_putc('\t', shf); + i -= 8; + } + while (i--) + shf_putc(' ', shf); + } + break; + case ORD('R'): + /* I/O redirection */ + pioact(shf, va_arg(va, struct ioword *)); + break; + default: + shf_putc(c, shf); + break; + } + } else + shf_putc(c, shf); + prevent_semicolon = false; + dont_trash_prevent_semicolon: + ; + } +} + +/* + * copy tree (for function definition) + */ +struct op * +tcopy(struct op *t, Area *ap) +{ + struct op *r; + const char **tw; + char **rw; + + if (t == NULL) + return (NULL); + + r = alloc(sizeof(struct op), ap); + + r->type = t->type; + r->u.evalflags = t->u.evalflags; + + if (t->type == TCASE) + r->str = wdcopy(t->str, ap); + else + strdupx(r->str, t->str, ap); + + if (t->vars == NULL) + r->vars = NULL; + else { + tw = (const char **)t->vars; + while (*tw) + ++tw; + rw = r->vars = alloc2(tw - (const char **)t->vars + 1, + sizeof(*tw), ap); + tw = (const char **)t->vars; + while (*tw) + *rw++ = wdcopy(*tw++, ap); + *rw = NULL; + } + + if (t->args == NULL) + r->args = NULL; + else { + tw = t->args; + while (*tw) + ++tw; + r->args = (const char **)(rw = alloc2(tw - t->args + 1, + sizeof(*tw), ap)); + tw = t->args; + while (*tw) + *rw++ = wdcopy(*tw++, ap); + *rw = NULL; + } + + r->ioact = (t->ioact == NULL) ? NULL : iocopy(t->ioact, ap); + + r->left = tcopy(t->left, ap); + r->right = tcopy(t->right, ap); + r->lineno = t->lineno; + + return (r); +} + +char * +wdcopy(const char *wp, Area *ap) +{ + size_t len; + + len = wdscan(wp, EOS) - wp; + return (memcpy(alloc(len, ap), wp, len)); +} + +/* return the position of prefix c in wp plus 1 */ +const char * +wdscan(const char *wp, int c) +{ + int nest = 0; + + while (/* CONSTCOND */ 1) + switch (*wp++) { + case EOS: + return (wp); + case ADELIM: + if (c == ADELIM && nest == 0) + return (wp + 1); + if (ord(*wp) == ORD(/*{*/ '}')) + goto wdscan_csubst; + /* FALLTHROUGH */ + case CHAR: + case QCHAR: + wp++; + break; + case COMASUB: + case COMSUB: + case FUNASUB: + case FUNSUB: + case VALSUB: + case EXPRSUB: + while (*wp++ != 0) + ; + break; + case OQUOTE: + case CQUOTE: + break; + case OSUBST: + nest++; + while (*wp++ != '\0') + ; + break; + case CSUBST: + wdscan_csubst: + wp++; + if (c == CSUBST && nest == 0) + return (wp); + nest--; + break; + case OPAT: + nest++; + wp++; + break; + case SPAT: + case CPAT: + if (c == wp[-1] && nest == 0) + return (wp); + if (wp[-1] == CPAT) + nest--; + break; + default: + internal_warningf( + "wdscan: unknown char 0x%X (carrying on)", + (unsigned char)wp[-1]); + } +} + +/* + * return a copy of wp without any of the mark up characters and with + * quote characters (" ' \) stripped. (string is allocated from ATEMP) + */ +char * +wdstrip(const char *wp, int opmode) +{ + struct shf shf; + + shf_sopen(NULL, 32, SHF_WR | SHF_DYNAMIC, &shf); + wdvarput(&shf, wp, 0, opmode); + /* shf_sclose NUL terminates */ + return (shf_sclose(&shf)); +} + +static struct ioword ** +iocopy(struct ioword **iow, Area *ap) +{ + struct ioword **ior; + int i; + + ior = iow; + while (*ior) + ++ior; + ior = alloc2(ior - iow + 1, sizeof(struct ioword *), ap); + + for (i = 0; iow[i] != NULL; i++) { + struct ioword *p, *q; + + p = iow[i]; + q = alloc(sizeof(struct ioword), ap); + ior[i] = q; + *q = *p; + if (p->ioname != NULL) + q->ioname = wdcopy(p->ioname, ap); + if (p->delim != NULL) + q->delim = wdcopy(p->delim, ap); + if (p->heredoc != NULL) + strdupx(q->heredoc, p->heredoc, ap); + } + ior[i] = NULL; + + return (ior); +} + +/* + * free tree (for function definition) + */ +void +tfree(struct op *t, Area *ap) +{ + char **w; + + if (t == NULL) + return; + + afree(t->str, ap); + + if (t->vars != NULL) { + for (w = t->vars; *w != NULL; w++) + afree(*w, ap); + afree(t->vars, ap); + } + + if (t->args != NULL) { + /*XXX we assume the caller is right */ + union mksh_ccphack cw; + + cw.ro = t->args; + for (w = cw.rw; *w != NULL; w++) + afree(*w, ap); + afree(t->args, ap); + } + + if (t->ioact != NULL) + iofree(t->ioact, ap); + + tfree(t->left, ap); + tfree(t->right, ap); + + afree(t, ap); +} + +static void +iofree(struct ioword **iow, Area *ap) +{ + struct ioword **iop; + struct ioword *p; + + iop = iow; + while ((p = *iop++) != NULL) { + afree(p->ioname, ap); + afree(p->delim, ap); + afree(p->heredoc, ap); + afree(p, ap); + } + afree(iow, ap); +} + +void +fpFUNCTf(struct shf *shf, int i, bool isksh, const char *k, struct op *v) +{ + if (isksh) + fptreef(shf, i, "%s %s %T", Tfunction, k, v); + else if (ktsearch(&keywords, k, hash(k))) + fptreef(shf, i, "%s %s() %T", Tfunction, k, v); + else + fptreef(shf, i, "%s() %T", k, v); +} + + +/* for jobs.c */ +void +vistree(char *dst, size_t sz, struct op *t) +{ + unsigned int c; + char *cp, *buf; + size_t n; + + buf = alloc(sz + 16, ATEMP); + snptreef(buf, sz + 16, Tf_T, t); + cp = buf; + vist_loop: + if (UTFMODE && (n = utf_mbtowc(&c, cp)) != (size_t)-1) { + if (c == 0 || n >= sz) + /* NUL or not enough free space */ + goto vist_out; + /* copy multibyte char */ + sz -= n; + while (n--) + *dst++ = *cp++; + goto vist_loop; + } + if (--sz == 0 || (c = ord(*cp++)) == 0) + /* NUL or not enough free space */ + goto vist_out; + if (ksh_isctrl(c)) { + /* C0 or C1 control character or DEL */ + if (--sz == 0) + /* not enough free space for two chars */ + goto vist_out; + *dst++ = '^'; + c = ksh_unctrl(c); + } else if (UTFMODE && rtt2asc(c) > 0x7F) { + /* better not try to display broken multibyte chars */ + /* also go easy on the Unicode: no U+FFFD here */ + c = ORD('?'); + } + *dst++ = c; + goto vist_loop; + + vist_out: + *dst = '\0'; + afree(buf, ATEMP); +} + +#ifdef DEBUG +void +dumpchar(struct shf *shf, int c) +{ + if (ksh_isctrl(c)) { + /* C0 or C1 control character or DEL */ + shf_putc('^', shf); + c = ksh_unctrl(c); + } + shf_putc(c, shf); +} + +/* see: wdvarput */ +static const char * +dumpwdvar_i(struct shf *shf, const char *wp, int quotelevel) +{ + int c; + + while (/* CONSTCOND */ 1) { + switch(*wp++) { + case EOS: + shf_puts("EOS", shf); + return (--wp); + case ADELIM: + if (ord(*wp) == ORD(/*{*/ '}')) { + shf_puts(/*{*/ "]ADELIM(})", shf); + return (wp + 1); + } + shf_puts("ADELIM=", shf); + if (0) + /* FALLTHROUGH */ + case CHAR: + shf_puts("CHAR=", shf); + dumpchar(shf, *wp++); + break; + case QCHAR: + shf_puts("QCHAR<", shf); + c = ord(*wp++); + if (quotelevel == 0 || c == ORD('"') || + c == ORD('\\') || ctype(c, C_DOLAR | C_GRAVE)) + shf_putc('\\', shf); + dumpchar(shf, c); + goto closeandout; + case COMASUB: + shf_puts("COMASUB<", shf); + goto dumpsub; + case COMSUB: + shf_puts("COMSUB<", shf); + dumpsub: + while ((c = *wp++) != 0) + dumpchar(shf, c); + closeandout: + shf_putc('>', shf); + break; + case FUNASUB: + shf_puts("FUNASUB<", shf); + goto dumpsub; + case FUNSUB: + shf_puts("FUNSUB<", shf); + goto dumpsub; + case VALSUB: + shf_puts("VALSUB<", shf); + goto dumpsub; + case EXPRSUB: + shf_puts("EXPRSUB<", shf); + goto dumpsub; + case OQUOTE: + shf_fprintf(shf, "OQUOTE{%d" /*}*/, ++quotelevel); + break; + case CQUOTE: + shf_fprintf(shf, /*{*/ "%d}CQUOTE", quotelevel); + if (quotelevel) + quotelevel--; + else + shf_puts("(err)", shf); + break; + case OSUBST: + shf_puts("OSUBST(", shf); + dumpchar(shf, *wp++); + shf_puts(")[", shf); + while ((c = *wp++) != 0) + dumpchar(shf, c); + shf_putc('|', shf); + wp = dumpwdvar_i(shf, wp, 0); + break; + case CSUBST: + shf_puts("]CSUBST(", shf); + dumpchar(shf, *wp++); + shf_putc(')', shf); + return (wp); + case OPAT: + shf_puts("OPAT=", shf); + dumpchar(shf, *wp++); + break; + case SPAT: + shf_puts("SPAT", shf); + break; + case CPAT: + shf_puts("CPAT", shf); + break; + default: + shf_fprintf(shf, "INVAL<%u>", (uint8_t)wp[-1]); + break; + } + shf_putc(' ', shf); + } +} +void +dumpwdvar(struct shf *shf, const char *wp) +{ + dumpwdvar_i(shf, wp, 0); +} + +void +dumpioact(struct shf *shf, struct op *t) +{ + struct ioword **ioact, *iop; + + if ((ioact = t->ioact) == NULL) + return; + + shf_puts("{IOACT", shf); + while ((iop = *ioact++) != NULL) { + unsigned short type = iop->ioflag & IOTYPE; +#define DT(x) case x: shf_puts(#x, shf); break; +#define DB(x) if (iop->ioflag & x) shf_puts("|" #x, shf); + + shf_putc(';', shf); + switch (type) { + DT(IOREAD) + DT(IOWRITE) + DT(IORDWR) + DT(IOHERE) + DT(IOCAT) + DT(IODUP) + default: + shf_fprintf(shf, "unk%d", type); + } + DB(IOEVAL) + DB(IOSKIP) + DB(IOCLOB) + DB(IORDUP) + DB(IONAMEXP) + DB(IOBASH) + DB(IOHERESTR) + DB(IONDELIM) + shf_fprintf(shf, ",unit=%d", (int)iop->unit); + if (iop->delim && !(iop->ioflag & IONDELIM)) { + shf_puts(",delim<", shf); + dumpwdvar(shf, iop->delim); + shf_putc('>', shf); + } + if (iop->ioname) { + if (iop->ioflag & IONAMEXP) { + shf_puts(",name=", shf); + print_value_quoted(shf, iop->ioname); + } else { + shf_puts(",name<", shf); + dumpwdvar(shf, iop->ioname); + shf_putc('>', shf); + } + } + if (iop->heredoc) { + shf_puts(",heredoc=", shf); + print_value_quoted(shf, iop->heredoc); + } +#undef DT +#undef DB + } + shf_putc('}', shf); +} + +void +dumptree(struct shf *shf, struct op *t) +{ + int i, j; + const char **w, *name; + struct op *t1; + static int nesting; + + for (i = 0; i < nesting; ++i) + shf_putc('\t', shf); + ++nesting; + shf_puts("{tree:" /*}*/, shf); + if (t == NULL) { + name = "(null)"; + goto out; + } + dumpioact(shf, t); + switch (t->type) { +#define OPEN(x) case x: name = #x; shf_puts(" {" #x ":", shf); /*}*/ + + OPEN(TCOM) + if (t->vars) { + i = 0; + w = (const char **)t->vars; + while (*w) { + shf_putc('\n', shf); + for (j = 0; j < nesting; ++j) + shf_putc('\t', shf); + shf_fprintf(shf, " var%d<", i++); + dumpwdvar(shf, *w++); + shf_putc('>', shf); + } + } else + shf_puts(" #no-vars#", shf); + if (t->args) { + i = 0; + w = t->args; + while (*w) { + shf_putc('\n', shf); + for (j = 0; j < nesting; ++j) + shf_putc('\t', shf); + shf_fprintf(shf, " arg%d<", i++); + dumpwdvar(shf, *w++); + shf_putc('>', shf); + } + } else + shf_puts(" #no-args#", shf); + break; + OPEN(TEXEC) + dumpleftandout: + t = t->left; + dumpandout: + shf_putc('\n', shf); + dumptree(shf, t); + break; + OPEN(TPAREN) + goto dumpleftandout; + OPEN(TPIPE) + dumpleftmidrightandout: + shf_putc('\n', shf); + dumptree(shf, t->left); +/* middumprightandout: (unused) */ + shf_fprintf(shf, "/%s:", name); + dumprightandout: + t = t->right; + goto dumpandout; + OPEN(TLIST) + goto dumpleftmidrightandout; + OPEN(TOR) + goto dumpleftmidrightandout; + OPEN(TAND) + goto dumpleftmidrightandout; + OPEN(TBANG) + goto dumprightandout; + OPEN(TDBRACKET) + i = 0; + w = t->args; + while (*w) { + shf_putc('\n', shf); + for (j = 0; j < nesting; ++j) + shf_putc('\t', shf); + shf_fprintf(shf, " arg%d<", i++); + dumpwdvar(shf, *w++); + shf_putc('>', shf); + } + break; + OPEN(TFOR) + dumpfor: + shf_fprintf(shf, " str<%s>", t->str); + if (t->vars != NULL) { + i = 0; + w = (const char **)t->vars; + while (*w) { + shf_putc('\n', shf); + for (j = 0; j < nesting; ++j) + shf_putc('\t', shf); + shf_fprintf(shf, " var%d<", i++); + dumpwdvar(shf, *w++); + shf_putc('>', shf); + } + } + goto dumpleftandout; + OPEN(TSELECT) + goto dumpfor; + OPEN(TCASE) + shf_fprintf(shf, " str<%s>", t->str); + i = 0; + for (t1 = t->left; t1 != NULL; t1 = t1->right) { + shf_putc('\n', shf); + for (j = 0; j < nesting; ++j) + shf_putc('\t', shf); + shf_fprintf(shf, " sub%d[(", i); + w = (const char **)t1->vars; + while (*w) { + dumpwdvar(shf, *w); + if (w[1] != NULL) + shf_putc('|', shf); + ++w; + } + shf_putc(')', shf); + dumpioact(shf, t); + shf_putc('\n', shf); + dumptree(shf, t1->left); + shf_fprintf(shf, " ;%c/%d]", t1->u.charflag, i++); + } + break; + OPEN(TWHILE) + goto dumpleftmidrightandout; + OPEN(TUNTIL) + goto dumpleftmidrightandout; + OPEN(TBRACE) + goto dumpleftandout; + OPEN(TCOPROC) + goto dumpleftandout; + OPEN(TASYNC) + goto dumpleftandout; + OPEN(TFUNCT) + shf_fprintf(shf, " str<%s> ksh<%s>", t->str, + t->u.ksh_func ? Ttrue : Tfalse); + goto dumpleftandout; + OPEN(TTIME) + goto dumpleftandout; + OPEN(TIF) + dumpif: + shf_putc('\n', shf); + dumptree(shf, t->left); + t = t->right; + dumpioact(shf, t); + if (t->left != NULL) { + shf_puts(" /TTHEN:\n", shf); + dumptree(shf, t->left); + } + if (t->right && t->right->type == TELIF) { + shf_puts(" /TELIF:", shf); + t = t->right; + dumpioact(shf, t); + goto dumpif; + } + if (t->right != NULL) { + shf_puts(" /TELSE:\n", shf); + dumptree(shf, t->right); + } + break; + OPEN(TEOF) + dumpunexpected: + shf_puts(Tunexpected, shf); + break; + OPEN(TELIF) + goto dumpunexpected; + OPEN(TPAT) + goto dumpunexpected; + default: + name = "TINVALID"; + shf_fprintf(shf, "{T<%d>:" /*}*/, t->type); + goto dumpunexpected; + +#undef OPEN + } + out: + shf_fprintf(shf, /*{*/ " /%s}\n", name); + --nesting; +} +#endif diff --git a/var.c b/var.c new file mode 100644 index 0000000..5219507 --- /dev/null +++ b/var.c @@ -0,0 +1,2199 @@ +/* $OpenBSD: var.c,v 1.44 2015/09/10 11:37:42 jca Exp $ */ + +/*- + * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + * 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#include "sh.h" +#include "mirhash.h" + +#if defined(__OpenBSD__) +#include +#endif + +__RCSID("$MirOS: src/bin/mksh/var.c,v 1.223 2018/01/13 23:55:15 tg Exp $"); + +/*- + * Variables + * + * WARNING: unreadable code, needs a rewrite + * + * if (flag&INTEGER), val.i contains integer value, and type contains base. + * otherwise, (val.s + type) contains string value. + * if (flag&EXPORT), val.s contains "name=value" for E-Z exporting. + */ + +static struct table specials; +static uint32_t lcg_state = 5381, qh_state = 4711; +/* may only be set by typeset() just before call to array_index_calc() */ +static enum namerefflag innermost_refflag = SRF_NOP; + +static void c_typeset_vardump(struct tbl *, uint32_t, int, int, bool, bool); +static void c_typeset_vardump_recursive(struct block *, uint32_t, int, bool, + bool); +static char *formatstr(struct tbl *, const char *); +static void exportprep(struct tbl *, const char *); +static int special(const char *); +static void unspecial(const char *); +static void getspec(struct tbl *); +static void setspec(struct tbl *); +static void unsetspec(struct tbl *); +static int getint(struct tbl *, mksh_ari_u *, bool); +static const char *array_index_calc(const char *, bool *, uint32_t *); + +/* + * create a new block for function calls and simple commands + * assume caller has allocated and set up e->loc + */ +void +newblock(void) +{ + struct block *l; + static const char *empty[] = { null }; + + l = alloc(sizeof(struct block), ATEMP); + l->flags = 0; + /* TODO: could use e->area (l->area => l->areap) */ + ainit(&l->area); + if (!e->loc) { + l->argc = 0; + l->argv = empty; + } else { + l->argc = e->loc->argc; + l->argv = e->loc->argv; + } + l->exit = l->error = NULL; + ktinit(&l->area, &l->vars, 0); + ktinit(&l->area, &l->funs, 0); + l->next = e->loc; + e->loc = l; +} + +/* + * pop a block handling special variables + */ +void +popblock(void) +{ + ssize_t i; + struct block *l = e->loc; + struct tbl *vp, **vpp = l->vars.tbls, *vq; + + /* pop block */ + e->loc = l->next; + + i = 1 << (l->vars.tshift); + while (--i >= 0) + if ((vp = *vpp++) != NULL && (vp->flag&SPECIAL)) { + if ((vq = global(vp->name))->flag & ISSET) + setspec(vq); + else + unsetspec(vq); + } + if (l->flags & BF_DOGETOPTS) + user_opt = l->getopts_state; + afreeall(&l->area); + afree(l, ATEMP); +} + +/* called by main() to initialise variable data structures */ +#define VARSPEC_DEFNS +#include "var_spec.h" + +enum var_specs { +#define VARSPEC_ENUMS +#include "var_spec.h" + V_MAX +}; + +/* this is biased with -1 relative to VARSPEC_ENUMS */ +static const char * const initvar_names[] = { +#define VARSPEC_ITEMS +#include "var_spec.h" +}; + +void +initvar(void) +{ + int i = 0; + struct tbl *tp; + + ktinit(APERM, &specials, + /* currently 21 specials: 75% of 32 = 2^5 */ + 5); + while (i < V_MAX - 1) { + tp = ktenter(&specials, initvar_names[i], + hash(initvar_names[i])); + tp->flag = DEFINED|ISSET; + tp->type = ++i; + } +} + +/* common code for several functions below and c_typeset() */ +struct block * +varsearch(struct block *l, struct tbl **vpp, const char *vn, uint32_t h) +{ + register struct tbl *vp; + + if (l) { + varsearch_loop: + if ((vp = ktsearch(&l->vars, vn, h)) != NULL) + goto varsearch_out; + if (l->next != NULL) { + l = l->next; + goto varsearch_loop; + } + } + vp = NULL; + varsearch_out: + *vpp = vp; + return (l); +} + +/* + * Used to calculate an array index for global()/local(). Sets *arrayp + * to true if this is an array, sets *valp to the array index, returns + * the basename of the array. May only be called from global()/local() + * and must be their first callee. + */ +static const char * +array_index_calc(const char *n, bool *arrayp, uint32_t *valp) +{ + const char *p; + size_t len; + char *ap = NULL; + + *arrayp = false; + redo_from_ref: + p = skip_varname(n, false); + if (innermost_refflag == SRF_NOP && (p != n) && ctype(n[0], C_ALPHX)) { + struct tbl *vp; + char *vn; + + strndupx(vn, n, p - n, ATEMP); + /* check if this is a reference */ + varsearch(e->loc, &vp, vn, hash(vn)); + afree(vn, ATEMP); + if (vp && (vp->flag & (DEFINED | ASSOC | ARRAY)) == + (DEFINED | ASSOC)) { + char *cp; + + /* gotcha! */ + cp = shf_smprintf(Tf_ss, str_val(vp), p); + afree(ap, ATEMP); + n = ap = cp; + goto redo_from_ref; + } + } + innermost_refflag = SRF_NOP; + + if (p != n && ord(*p) == ORD('[') && (len = array_ref_len(p))) { + char *sub, *tmp; + mksh_ari_t rval; + + /* calculate the value of the subscript */ + *arrayp = true; + strndupx(tmp, p + 1, len - 2, ATEMP); + sub = substitute(tmp, 0); + afree(tmp, ATEMP); + strndupx(n, n, p - n, ATEMP); + evaluate(sub, &rval, KSH_UNWIND_ERROR, true); + *valp = (uint32_t)rval; + afree(sub, ATEMP); + } + return (n); +} + +#define vn vname.ro +/* + * Search for variable, if not found create globally. + */ +struct tbl * +global(const char *n) +{ + return (isglobal(n, true)); +} + +/* search for variable; if not found, return NULL or create globally */ +struct tbl * +isglobal(const char *n, bool docreate) +{ + struct tbl *vp; + union mksh_cchack vname; + struct block *l = e->loc; + int c; + bool array; + uint32_t h, val; + + /* + * check to see if this is an array; + * dereference namerefs; must come first + */ + vn = array_index_calc(n, &array, &val); + h = hash(vn); + c = (unsigned char)vn[0]; + if (!ctype(c, C_ALPHX)) { + if (array) + errorf(Tbadsubst); + vp = vtemp; + vp->flag = DEFINED; + vp->type = 0; + vp->areap = ATEMP; + if (ctype(c, C_DIGIT)) { + if (getn(vn, &c)) { + /* main.c:main_init() says 12 */ + shf_snprintf(vp->name, 12, Tf_d, c); + if (c <= l->argc) { + /* setstr can't fail here */ + setstr(vp, l->argv[c], + KSH_RETURN_ERROR); + } + } else + vp->name[0] = '\0'; + vp->flag |= RDONLY; + goto out; + } + vp->name[0] = c; + vp->name[1] = '\0'; + vp->flag |= RDONLY; + if (vn[1] != '\0') + goto out; + vp->flag |= ISSET|INTEGER; + switch (c) { + case '$': + vp->val.i = kshpid; + break; + case '!': + /* if no job, expand to nothing */ + if ((vp->val.i = j_async()) == 0) + vp->flag &= ~(ISSET|INTEGER); + break; + case '?': + vp->val.i = exstat & 0xFF; + break; + case '#': + vp->val.i = l->argc; + break; + case '-': + vp->flag &= ~INTEGER; + vp->val.s = getoptions(); + break; + default: + vp->flag &= ~(ISSET|INTEGER); + } + goto out; + } + l = varsearch(e->loc, &vp, vn, h); + if (vp == NULL && docreate) + vp = ktenter(&l->vars, vn, h); + else + docreate = false; + if (vp != NULL) { + if (array) + vp = arraysearch(vp, val); + if (docreate) { + vp->flag |= DEFINED; + if (special(vn)) + vp->flag |= SPECIAL; + } + } + out: + last_lookup_was_array = array; + if (vn != n) + afree(vname.rw, ATEMP); + return (vp); +} + +/* + * Search for local variable, if not found create locally. + */ +struct tbl * +local(const char *n, bool copy) +{ + struct tbl *vp; + union mksh_cchack vname; + struct block *l = e->loc; + bool array; + uint32_t h, val; + + /* + * check to see if this is an array; + * dereference namerefs; must come first + */ + vn = array_index_calc(n, &array, &val); + h = hash(vn); + if (!ctype(*vn, C_ALPHX)) { + vp = vtemp; + vp->flag = DEFINED|RDONLY; + vp->type = 0; + vp->areap = ATEMP; + goto out; + } + vp = ktenter(&l->vars, vn, h); + if (copy && !(vp->flag & DEFINED)) { + struct tbl *vq; + + varsearch(l->next, &vq, vn, h); + if (vq != NULL) { + vp->flag |= vq->flag & + (EXPORT | INTEGER | RDONLY | LJUST | RJUST | + ZEROFIL | LCASEV | UCASEV_AL | INT_U | INT_L); + if (vq->flag & INTEGER) + vp->type = vq->type; + vp->u2.field = vq->u2.field; + } + } + if (array) + vp = arraysearch(vp, val); + vp->flag |= DEFINED; + if (special(vn)) + vp->flag |= SPECIAL; + out: + last_lookup_was_array = array; + if (vn != n) + afree(vname.rw, ATEMP); + return (vp); +} +#undef vn + +/* get variable string value */ +char * +str_val(struct tbl *vp) +{ + char *s; + + if ((vp->flag&SPECIAL)) + getspec(vp); + if (!(vp->flag&ISSET)) + /* special to dollar() */ + s = null; + else if (!(vp->flag&INTEGER)) + /* string source */ + s = vp->val.s + vp->type; + else { + /* integer source */ + mksh_uari_t n; + unsigned int base; + /** + * worst case number length is when base == 2: + * 1 (minus) + 2 (base, up to 36) + 1 ('#') + + * number of bits in the mksh_uari_t + 1 (NUL) + */ + char strbuf[1 + 2 + 1 + 8 * sizeof(mksh_uari_t) + 1]; + const char *digits = (vp->flag & UCASEV_AL) ? + digits_uc : digits_lc; + + s = strbuf + sizeof(strbuf); + if (vp->flag & INT_U) + n = vp->val.u; + else + n = (vp->val.i < 0) ? -vp->val.u : vp->val.u; + base = (vp->type == 0) ? 10U : (unsigned int)vp->type; + + if (base == 1 && n == 0) + base = 2; + if (base == 1) { + size_t sz = 1; + + *(s = strbuf) = '1'; + s[1] = '#'; + if (!UTFMODE) + s[2] = (unsigned char)n; + else if ((n & 0xFF80) == 0xEF80) + /* OPTU-16 -> raw octet */ + s[2] = asc2rtt(n & 0xFF); + else + sz = utf_wctomb(s + 2, n); + s[2 + sz] = '\0'; + } else { + *--s = '\0'; + do { + *--s = digits[n % base]; + n /= base; + } while (n != 0); + if (base != 10) { + *--s = '#'; + *--s = digits[base % 10]; + if (base >= 10) + *--s = digits[base / 10]; + } + if (!(vp->flag & INT_U) && vp->val.i < 0) + *--s = '-'; + } + if (vp->flag & (RJUST|LJUST)) + /* case already dealt with */ + s = formatstr(vp, s); + else + strdupx(s, s, ATEMP); + } + return (s); +} + +/* set variable to string value */ +int +setstr(struct tbl *vq, const char *s, int error_ok) +{ + char *salloc = NULL; + bool no_ro_check = tobool(error_ok & 0x4); + + error_ok &= ~0x4; + if ((vq->flag & RDONLY) && !no_ro_check) { + warningf(true, Tf_ro, vq->name); + if (!error_ok) + errorfxz(2); + return (0); + } + if (!(vq->flag&INTEGER)) { + /* string dest */ + if ((vq->flag&ALLOC)) { +#ifndef MKSH_SMALL + /* debugging */ + if (s >= vq->val.s && + s <= strnul(vq->val.s)) { + internal_errorf( + "setstr: %s=%s: assigning to self", + vq->name, s); + } +#endif + afree(vq->val.s, vq->areap); + } + vq->flag &= ~(ISSET|ALLOC); + vq->type = 0; + if (s && (vq->flag & (UCASEV_AL|LCASEV|LJUST|RJUST))) + s = salloc = formatstr(vq, s); + if ((vq->flag&EXPORT)) + exportprep(vq, s); + else { + strdupx(vq->val.s, s, vq->areap); + vq->flag |= ALLOC; + } + } else { + /* integer dest */ + if (!v_evaluate(vq, s, error_ok, true)) + return (0); + } + vq->flag |= ISSET; + if ((vq->flag&SPECIAL)) + setspec(vq); + afree(salloc, ATEMP); + return (1); +} + +/* set variable to integer */ +void +setint(struct tbl *vq, mksh_ari_t n) +{ + if (!(vq->flag&INTEGER)) { + vtemp->flag = (ISSET|INTEGER); + vtemp->type = 0; + vtemp->areap = ATEMP; + vtemp->val.i = n; + /* setstr can't fail here */ + setstr(vq, str_val(vtemp), KSH_RETURN_ERROR); + } else + vq->val.i = n; + vq->flag |= ISSET; + if ((vq->flag&SPECIAL)) + setspec(vq); +} + +static int +getint(struct tbl *vp, mksh_ari_u *nump, bool arith) +{ + mksh_uari_t c, num = 0, base = 10; + const char *s; + bool have_base = false, neg = false; + + if (vp->flag & SPECIAL) + getspec(vp); + /* XXX is it possible for ISSET to be set and val.s to be NULL? */ + if (!(vp->flag & ISSET) || (!(vp->flag & INTEGER) && vp->val.s == NULL)) + return (-1); + if (vp->flag & INTEGER) { + nump->i = vp->val.i; + return (vp->type); + } + s = vp->val.s + vp->type; + + do { + c = (unsigned char)*s++; + } while (ctype(c, C_SPACE)); + + switch (c) { + case '-': + neg = true; + /* FALLTHROUGH */ + case '+': + c = (unsigned char)*s++; + break; + } + + if (c == '0' && arith) { + if (ksh_eq(s[0], 'X', 'x')) { + /* interpret as hexadecimal */ + base = 16; + ++s; + goto getint_c_style_base; + } else if (Flag(FPOSIX) && ctype(s[0], C_DIGIT) && + !(vp->flag & ZEROFIL)) { + /* interpret as octal (deprecated) */ + base = 8; + getint_c_style_base: + have_base = true; + c = (unsigned char)*s++; + } + } + + do { + if (c == '#') { + /* ksh-style base determination */ + if (have_base || num < 1) + return (-1); + if ((base = num) == 1) { + /* mksh-specific extension */ + unsigned int wc; + + if (!UTFMODE) + wc = *(const unsigned char *)s; + else if (utf_mbtowc(&wc, s) == (size_t)-1) + /* OPTU-8 -> OPTU-16 */ + /* + * (with a twist: 1#\uEF80 converts + * the same as 1#\x80 does, thus is + * not round-tripping correctly XXX) + */ + wc = 0xEF00 + rtt2asc(*s); + nump->u = (mksh_uari_t)wc; + return (1); + } else if (base > 36) + base = 10; + num = 0; + have_base = true; + continue; + } + if (ctype(c, C_DIGIT)) + c = ksh_numdig(c); + else if (ctype(c, C_UPPER)) + c = ksh_numuc(c) + 10; + else if (ctype(c, C_LOWER)) + c = ksh_numlc(c) + 10; + else + return (-1); + if (c >= base) + return (-1); + /* handle overflow as truncation */ + num = num * base + c; + } while ((c = (unsigned char)*s++)); + + if (neg) + num = -num; + nump->u = num; + return (base); +} + +/* + * convert variable vq to integer variable, setting its value from vp + * (vq and vp may be the same) + */ +struct tbl * +setint_v(struct tbl *vq, struct tbl *vp, bool arith) +{ + int base; + mksh_ari_u num; + + if ((base = getint(vp, &num, arith)) == -1) + return (NULL); + setint_n(vq, num.i, 0); + if (vq->type == 0) + /* default base */ + vq->type = base; + return (vq); +} + +/* convert variable vq to integer variable, setting its value to num */ +void +setint_n(struct tbl *vq, mksh_ari_t num, int newbase) +{ + if (!(vq->flag & INTEGER) && (vq->flag & ALLOC)) { + vq->flag &= ~ALLOC; + vq->type = 0; + afree(vq->val.s, vq->areap); + } + vq->val.i = num; + if (newbase != 0) + vq->type = newbase; + vq->flag |= ISSET|INTEGER; + if (vq->flag&SPECIAL) + setspec(vq); +} + +static char * +formatstr(struct tbl *vp, const char *s) +{ + int olen, nlen; + char *p, *q; + size_t psiz; + + olen = (int)utf_mbswidth(s); + + if (vp->flag & (RJUST|LJUST)) { + if (!vp->u2.field) + /* default field width */ + vp->u2.field = olen; + nlen = vp->u2.field; + } else + nlen = olen; + + p = alloc((psiz = nlen * /* MB_LEN_MAX */ 3 + 1), ATEMP); + if (vp->flag & (RJUST|LJUST)) { + int slen = olen; + + if (vp->flag & RJUST) { + const char *qq; + int n = 0; + + qq = utf_skipcols(s, slen, &slen); + + /* strip trailing spaces (AT&T uses qq[-1] == ' ') */ + while (qq > s && ctype(qq[-1], C_SPACE)) { + --qq; + --slen; + } + if (vp->flag & ZEROFIL && vp->flag & INTEGER) { + if (!s[0] || !s[1]) + goto uhm_no; + if (s[1] == '#') + n = 2; + else if (s[2] == '#') + n = 3; + uhm_no: + if (vp->u2.field <= n) + n = 0; + } + if (n) { + memcpy(p, s, n); + s += n; + } + while (slen > vp->u2.field) + slen -= utf_widthadj(s, &s); + if (vp->u2.field - slen) + memset(p + n, (vp->flag & ZEROFIL) ? '0' : ' ', + vp->u2.field - slen); + slen -= n; + shf_snprintf(p + vp->u2.field - slen, + psiz - (vp->u2.field - slen), + "%.*s", slen, s); + } else { + /* strip leading spaces/zeros */ + while (ctype(*s, C_SPACE)) + s++; + if (vp->flag & ZEROFIL) + while (*s == '0') + s++; + shf_snprintf(p, nlen + 1, "%-*.*s", + vp->u2.field, vp->u2.field, s); + } + } else + memcpy(p, s, strlen(s) + 1); + + if (vp->flag & UCASEV_AL) { + for (q = p; *q; q++) + *q = ksh_toupper(*q); + } else if (vp->flag & LCASEV) { + for (q = p; *q; q++) + *q = ksh_tolower(*q); + } + + return (p); +} + +/* + * make vp->val.s be "name=value" for quick exporting. + */ +static void +exportprep(struct tbl *vp, const char *val) +{ + char *xp; + char *op = (vp->flag&ALLOC) ? vp->val.s : NULL; + size_t namelen, vallen; + + namelen = strlen(vp->name); + vallen = strlen(val) + 1; + + vp->flag |= ALLOC; + /* since name+val are both in memory this can go unchecked */ + xp = alloc(namelen + 1 + vallen, vp->areap); + memcpy(vp->val.s = xp, vp->name, namelen); + xp += namelen; + *xp++ = '='; + /* offset to value */ + vp->type = xp - vp->val.s; + memcpy(xp, val, vallen); + afree(op, vp->areap); +} + +/* + * lookup variable (according to (set&LOCAL)), set its attributes + * (INTEGER, RDONLY, EXPORT, TRACE, LJUST, RJUST, ZEROFIL, LCASEV, + * UCASEV_AL), and optionally set its value if an assignment. + */ +struct tbl * +typeset(const char *var, uint32_t set, uint32_t clr, int field, int base) +{ + struct tbl *vp; + struct tbl *vpbase, *t; + char *tvar; + const char *val; + size_t len; + bool vappend = false; + enum namerefflag new_refflag = SRF_NOP; + + if ((set & (ARRAY | ASSOC)) == ASSOC) { + new_refflag = SRF_ENABLE; + set &= ~(ARRAY | ASSOC); + } + if ((clr & (ARRAY | ASSOC)) == ASSOC) { + new_refflag = SRF_DISABLE; + clr &= ~(ARRAY | ASSOC); + } + + /* check for valid variable name, search for value */ + val = skip_varname(var, false); + if (val == var) { + /* no variable name given */ + return (NULL); + } + if (ord(*val) == ORD('[')) { + if (new_refflag != SRF_NOP) + errorf(Tf_sD_s, var, + "reference variable can't be an array"); + len = array_ref_len(val); + if (len == 0) + return (NULL); + /* + * IMPORT is only used when the shell starts up and is + * setting up its environment. Allow only simple array + * references at this time since parameter/command + * substitution is performed on the [expression] which + * would be a major security hole. + */ + if (set & IMPORT) { + size_t i; + + for (i = 1; i < len - 1; i++) + if (!ctype(val[i], C_DIGIT)) + return (NULL); + } + val += len; + } + if (ord(val[0]) == ORD('=')) { + strndupx(tvar, var, val - var, ATEMP); + ++val; + } else if (set & IMPORT) { + /* environment invalid variable name or no assignment */ + return (NULL); + } else if (ord(val[0]) == ORD('+') && ord(val[1]) == ORD('=')) { + strndupx(tvar, var, val - var, ATEMP); + val += 2; + vappend = true; + } else if (val[0] != '\0') { + /* other invalid variable names (not from environment) */ + return (NULL); + } else { + /* just varname with no value part nor equals sign */ + strdupx(tvar, var, ATEMP); + val = NULL; + /* handle foo[*] => foo (whole array) mapping for R39b */ + len = strlen(tvar); + if (len > 3 && ord(tvar[len - 3]) == ORD('[') && + ord(tvar[len - 2]) == ORD('*') && + ord(tvar[len - 1]) == ORD(']')) + tvar[len - 3] = '\0'; + } + + if (new_refflag == SRF_ENABLE) { + const char *qval, *ccp; + + /* bail out on 'nameref foo+=bar' */ + if (vappend) + errorf("appending not allowed for nameref"); + /* find value if variable already exists */ + if ((qval = val) == NULL) { + varsearch(e->loc, &vp, tvar, hash(tvar)); + if (vp == NULL) + goto nameref_empty; + qval = str_val(vp); + } + /* check target value for being a valid variable name */ + ccp = skip_varname(qval, false); + if (ccp == qval) { + int c; + + if (!(c = (unsigned char)qval[0])) + goto nameref_empty; + else if (ctype(c, C_DIGIT) && getn(qval, &c)) + goto nameref_rhs_checked; + else if (qval[1] == '\0') switch (c) { + case '$': + case '!': + case '?': + case '#': + case '-': + goto nameref_rhs_checked; + } + nameref_empty: + errorf(Tf_sD_s, var, "empty nameref target"); + } + len = (ord(*ccp) == ORD('[')) ? array_ref_len(ccp) : 0; + if (ccp[len]) { + /* + * works for cases "no array", "valid array with + * junk after it" and "invalid array"; in the + * latter case, len is also 0 and points to '[' + */ + errorf(Tf_sD_s, qval, + "nameref target not a valid parameter name"); + } + nameref_rhs_checked: + /* prevent nameref loops */ + while (qval) { + if (!strcmp(qval, tvar)) + errorf(Tf_sD_s, qval, + "expression recurses on parameter"); + varsearch(e->loc, &vp, qval, hash(qval)); + qval = NULL; + if (vp && ((vp->flag & (ARRAY | ASSOC)) == ASSOC)) + qval = str_val(vp); + } + } + + /* prevent typeset from creating a local PATH/ENV/SHELL */ + if (Flag(FRESTRICTED) && (strcmp(tvar, TPATH) == 0 || + strcmp(tvar, "ENV") == 0 || strcmp(tvar, TSHELL) == 0)) + errorf(Tf_sD_s, tvar, "restricted"); + + innermost_refflag = new_refflag; + vp = (set & LOCAL) ? local(tvar, tobool(set & LOCAL_COPY)) : + global(tvar); + if (new_refflag == SRF_DISABLE && (vp->flag & (ARRAY|ASSOC)) == ASSOC) + vp->flag &= ~ASSOC; + else if (new_refflag == SRF_ENABLE) { + if (vp->flag & ARRAY) { + struct tbl *a, *tmp; + + /* free up entire array */ + for (a = vp->u.array; a; ) { + tmp = a; + a = a->u.array; + if (tmp->flag & ALLOC) + afree(tmp->val.s, tmp->areap); + afree(tmp, tmp->areap); + } + vp->u.array = NULL; + vp->flag &= ~ARRAY; + } + vp->flag |= ASSOC; + } + + set &= ~(LOCAL|LOCAL_COPY); + + vpbase = (vp->flag & ARRAY) ? global(arrayname(tvar)) : vp; + + /* + * only allow export and readonly flag to be set; AT&T ksh + * allows any attribute to be changed which means it can be + * truncated or modified (-L/-R/-Z/-i) + */ + if ((vpbase->flag & RDONLY) && + (val || clr || (set & ~(EXPORT | RDONLY)))) + /* XXX check calls - is error here ok by POSIX? */ + errorfx(2, Tf_ro, tvar); + afree(tvar, ATEMP); + + /* most calls are with set/clr == 0 */ + if (set | clr) { + bool ok = true; + + /* + * XXX if x[0] isn't set, there will be problems: need + * to have one copy of attributes for arrays... + */ + for (t = vpbase; t; t = t->u.array) { + bool fake_assign; + char *s = NULL; + char *free_me = NULL; + + fake_assign = (t->flag & ISSET) && (!val || t != vp) && + ((set & (UCASEV_AL|LCASEV|LJUST|RJUST|ZEROFIL)) || + ((t->flag & INTEGER) && (clr & INTEGER)) || + (!(t->flag & INTEGER) && (set & INTEGER))); + if (fake_assign) { + if (t->flag & INTEGER) { + s = str_val(t); + free_me = NULL; + } else { + s = t->val.s + t->type; + free_me = (t->flag & ALLOC) ? t->val.s : + NULL; + } + t->flag &= ~ALLOC; + } + if (!(t->flag & INTEGER) && (set & INTEGER)) { + t->type = 0; + t->flag &= ~ALLOC; + } + t->flag = (t->flag | set) & ~clr; + /* + * Don't change base if assignment is to be + * done, in case assignment fails. + */ + if ((set & INTEGER) && base > 0 && (!val || t != vp)) + t->type = base; + if (set & (LJUST|RJUST|ZEROFIL)) + t->u2.field = field; + if (fake_assign) { + if (!setstr(t, s, KSH_RETURN_ERROR)) { + /* + * Somewhat arbitrary action + * here: zap contents of + * variable, but keep the flag + * settings. + */ + ok = false; + if (t->flag & INTEGER) + t->flag &= ~ISSET; + else { + if (t->flag & ALLOC) + afree(t->val.s, t->areap); + t->flag &= ~(ISSET|ALLOC); + t->type = 0; + } + } + afree(free_me, t->areap); + } + } + if (!ok) + errorfz(); + } + + if (val != NULL) { + char *tval; + + if (vappend) { + tval = shf_smprintf(Tf_ss, str_val(vp), val); + val = tval; + } else + tval = NULL; + + if (vp->flag&INTEGER) { + /* do not zero base before assignment */ + setstr(vp, val, KSH_UNWIND_ERROR | 0x4); + /* done after assignment to override default */ + if (base > 0) + vp->type = base; + } else + /* setstr can't fail (readonly check already done) */ + setstr(vp, val, KSH_RETURN_ERROR | 0x4); + + afree(tval, ATEMP); + } + + /* only x[0] is ever exported, so use vpbase */ + if ((vpbase->flag&EXPORT) && !(vpbase->flag&INTEGER) && + vpbase->type == 0) + exportprep(vpbase, (vpbase->flag&ISSET) ? vpbase->val.s : null); + + return (vp); +} + +/** + * Unset a variable. The flags can be: + * |1 = tear down entire array + * |2 = keep attributes, only unset content + */ +void +unset(struct tbl *vp, int flags) +{ + if (vp->flag & ALLOC) + afree(vp->val.s, vp->areap); + if ((vp->flag & ARRAY) && (flags & 1)) { + struct tbl *a, *tmp; + + /* free up entire array */ + for (a = vp->u.array; a; ) { + tmp = a; + a = a->u.array; + if (tmp->flag & ALLOC) + afree(tmp->val.s, tmp->areap); + afree(tmp, tmp->areap); + } + vp->u.array = NULL; + } + if (flags & 2) { + vp->flag &= ~(ALLOC|ISSET); + return; + } + /* if foo[0] is being unset, the remainder of the array is kept... */ + vp->flag &= SPECIAL | ((flags & 1) ? 0 : ARRAY|DEFINED); + if (vp->flag & SPECIAL) + /* responsible for 'unspecial'ing var */ + unsetspec(vp); +} + +/* + * Return a pointer to the first char past a legal variable name + * (returns the argument if there is no legal name, returns a pointer to + * the terminating NUL if whole string is legal). + */ +const char * +skip_varname(const char *s, bool aok) +{ + size_t alen; + + if (s && ctype(*s, C_ALPHX)) { + do { + ++s; + } while (ctype(*s, C_ALNUX)); + if (aok && ord(*s) == ORD('[') && (alen = array_ref_len(s))) + s += alen; + } + return (s); +} + +/* Return a pointer to the first character past any legal variable name */ +const char * +skip_wdvarname(const char *s, + /* skip array de-reference? */ + bool aok) +{ + if (s[0] == CHAR && ctype(s[1], C_ALPHX)) { + do { + s += 2; + } while (s[0] == CHAR && ctype(s[1], C_ALNUX)); + if (aok && s[0] == CHAR && ord(s[1]) == ORD('[')) { + /* skip possible array de-reference */ + const char *p = s; + char c; + int depth = 0; + + while (/* CONSTCOND */ 1) { + if (p[0] != CHAR) + break; + c = p[1]; + p += 2; + if (ord(c) == ORD('[')) + depth++; + else if (ord(c) == ORD(']') && --depth == 0) { + s = p; + break; + } + } + } + } + return (s); +} + +/* Check if coded string s is a variable name */ +int +is_wdvarname(const char *s, bool aok) +{ + const char *p = skip_wdvarname(s, aok); + + return (p != s && p[0] == EOS); +} + +/* Check if coded string s is a variable assignment */ +int +is_wdvarassign(const char *s) +{ + const char *p = skip_wdvarname(s, true); + + return (p != s && p[0] == CHAR && + (p[1] == '=' || (p[1] == '+' && p[2] == CHAR && p[3] == '='))); +} + +/* + * Make the exported environment from the exported names in the dictionary. + */ +char ** +makenv(void) +{ + ssize_t i; + struct block *l; + XPtrV denv; + struct tbl *vp, **vpp; + + XPinit(denv, 64); + for (l = e->loc; l != NULL; l = l->next) { + vpp = l->vars.tbls; + i = 1 << (l->vars.tshift); + while (--i >= 0) + if ((vp = *vpp++) != NULL && + (vp->flag&(ISSET|EXPORT)) == (ISSET|EXPORT)) { + struct block *l2; + struct tbl *vp2; + uint32_t h = hash(vp->name); + + /* unexport any redefined instances */ + for (l2 = l->next; l2 != NULL; l2 = l2->next) { + vp2 = ktsearch(&l2->vars, vp->name, h); + if (vp2 != NULL) + vp2->flag &= ~EXPORT; + } + if ((vp->flag&INTEGER)) { + /* integer to string */ + char *val; + val = str_val(vp); + vp->flag &= ~(INTEGER|RDONLY|SPECIAL); + /* setstr can't fail here */ + setstr(vp, val, KSH_RETURN_ERROR); + } +#ifdef __OS2__ + /* these special variables are not exported */ + if (!strcmp(vp->name, "BEGINLIBPATH") || + !strcmp(vp->name, "ENDLIBPATH") || + !strcmp(vp->name, "LIBPATHSTRICT")) + continue; +#endif + XPput(denv, vp->val.s); + } + if (l->flags & BF_STOPENV) + break; + } + XPput(denv, NULL); + return ((char **)XPclose(denv)); +} + +/* + * handle special variables with side effects - PATH, SECONDS. + */ + +/* Test if name is a special parameter */ +static int +special(const char *name) +{ + struct tbl *tp; + + tp = ktsearch(&specials, name, hash(name)); + return (tp && (tp->flag & ISSET) ? tp->type : V_NONE); +} + +/* Make a variable non-special */ +static void +unspecial(const char *name) +{ + struct tbl *tp; + + tp = ktsearch(&specials, name, hash(name)); + if (tp) + ktdelete(tp); +} + +static time_t seconds; /* time SECONDS last set */ +static mksh_uari_t user_lineno; /* what user set $LINENO to */ + +/* minimum values from the OS we consider sane, lowered for R53 */ +#define MIN_COLS 4 +#define MIN_LINS 2 + +static void +getspec(struct tbl *vp) +{ + mksh_ari_u num; + int st; + struct timeval tv; + + switch ((st = special(vp->name))) { + case V_COLUMNS: + case V_LINES: + /* + * Do NOT export COLUMNS/LINES. Many applications + * check COLUMNS/LINES before checking ws.ws_col/row, + * so if the app is started with C/L in the environ + * and the window is then resized, the app won't + * see the change cause the environ doesn't change. + */ + if (got_winch) + change_winsz(); + break; + } + switch (st) { + case V_BASHPID: + num.u = (mksh_uari_t)procpid; + break; + case V_COLUMNS: + num.i = x_cols; + break; + case V_HISTSIZE: + num.i = histsize; + break; + case V_LINENO: + num.u = (mksh_uari_t)current_lineno + user_lineno; + break; + case V_LINES: + num.i = x_lins; + break; + case V_EPOCHREALTIME: { + /* 10(%u) + 1(.) + 6 + NUL */ + char buf[18]; + + vp->flag &= ~SPECIAL; + mksh_TIME(tv); + shf_snprintf(buf, sizeof(buf), "%u.%06u", + (unsigned)tv.tv_sec, (unsigned)tv.tv_usec); + setstr(vp, buf, KSH_RETURN_ERROR | 0x4); + vp->flag |= SPECIAL; + return; + } + case V_OPTIND: + num.i = user_opt.uoptind; + break; + case V_RANDOM: + num.i = rndget(); + break; + case V_SECONDS: + /* + * On start up the value of SECONDS is used before + * it has been set - don't do anything in this case + * (see initcoms[] in main.c). + */ + if (vp->flag & ISSET) { + mksh_TIME(tv); + num.i = tv.tv_sec - seconds; + } else + return; + break; + default: + /* do nothing, do not touch vp at all */ + return; + } + vp->flag &= ~SPECIAL; + setint_n(vp, num.i, 0); + vp->flag |= SPECIAL; +} + +static void +setspec(struct tbl *vp) +{ + mksh_ari_u num; + char *s; + int st = special(vp->name); + +#ifdef MKSH_DOSPATH + switch (st) { + case V_PATH: + case V_TMPDIR: +#ifdef __OS2__ + case V_BEGINLIBPATH: + case V_ENDLIBPATH: +#endif + /* convert backslashes to slashes for convenience */ + if (!(vp->flag&INTEGER)) { + s = str_val(vp); + do { + if (*s == ORD('\\')) + *s = '/'; + } while (*s++); + } + break; + } +#endif + + switch (st) { +#ifdef __OS2__ + case V_BEGINLIBPATH: + case V_ENDLIBPATH: + case V_LIBPATHSTRICT: + setextlibpath(vp->name, str_val(vp)); + return; +#endif +#if HAVE_PERSISTENT_HISTORY + case V_HISTFILE: + sethistfile(str_val(vp)); + return; +#endif + case V_IFS: + set_ifs(str_val(vp)); + return; + case V_PATH: + afree(path, APERM); + s = str_val(vp); + strdupx(path, s, APERM); + /* clear tracked aliases */ + flushcom(true); + return; +#ifndef MKSH_NO_CMDLINE_EDITING + case V_TERM: + x_initterm(str_val(vp)); + return; +#endif + case V_TMPDIR: + afree(tmpdir, APERM); + tmpdir = NULL; + /* + * Use tmpdir iff it is an absolute path, is writable + * and searchable and is a directory... + */ + { + struct stat statb; + + s = str_val(vp); + /* LINTED use of access */ + if (mksh_abspath(s) && access(s, W_OK|X_OK) == 0 && + stat(s, &statb) == 0 && S_ISDIR(statb.st_mode)) + strdupx(tmpdir, s, APERM); + } + return; + /* common sub-cases */ + case V_COLUMNS: + case V_LINES: + if (vp->flag & IMPORT) { + /* do not touch */ + unspecial(vp->name); + vp->flag &= ~SPECIAL; + return; + } + /* FALLTHROUGH */ + case V_HISTSIZE: + case V_LINENO: + case V_OPTIND: + case V_RANDOM: + case V_SECONDS: + case V_TMOUT: + vp->flag &= ~SPECIAL; + if (getint(vp, &num, false) == -1) { + s = str_val(vp); + if (st != V_RANDOM) + errorf(Tf_sD_sD_s, vp->name, Tbadnum, s); + num.u = hash(s); + } + vp->flag |= SPECIAL; + break; +#ifdef MKSH_EARLY_LOCALE_TRACKING + case V_LANG: + case V_LC_ALL: + case V_LC_CTYPE: + recheck_ctype(); + return; +#endif + default: + /* do nothing, do not touch vp at all */ + return; + } + + /* process the singular parts of the common cases */ + + switch (st) { + case V_COLUMNS: + if (num.i >= MIN_COLS) + x_cols = num.i; + break; + case V_HISTSIZE: + sethistsize(num.i); + break; + case V_LINENO: + /* The -1 is because line numbering starts at 1. */ + user_lineno = num.u - (mksh_uari_t)current_lineno - 1; + break; + case V_LINES: + if (num.i >= MIN_LINS) + x_lins = num.i; + break; + case V_OPTIND: + getopts_reset((int)num.i); + break; + case V_RANDOM: + /* + * mksh R39d+ no longer has the traditional repeatability + * of $RANDOM sequences, but always retains state + */ + rndset((unsigned long)num.u); + break; + case V_SECONDS: + { + struct timeval tv; + + mksh_TIME(tv); + seconds = tv.tv_sec - num.i; + } + break; + case V_TMOUT: + ksh_tmout = num.i >= 0 ? num.i : 0; + break; + } +} + +static void +unsetspec(struct tbl *vp) +{ + /* + * AT&T ksh man page says OPTIND, OPTARG and _ lose special + * meaning, but OPTARG does not (still set by getopts) and _ is + * also still set in various places. Don't know what AT&T does + * for HISTSIZE, HISTFILE. Unsetting these in AT&T ksh does not + * loose the 'specialness': IFS, COLUMNS, PATH, TMPDIR + */ + + switch (special(vp->name)) { +#ifdef __OS2__ + case V_BEGINLIBPATH: + case V_ENDLIBPATH: + case V_LIBPATHSTRICT: + setextlibpath(vp->name, ""); + return; +#endif +#if HAVE_PERSISTENT_HISTORY + case V_HISTFILE: + sethistfile(NULL); + return; +#endif + case V_IFS: + set_ifs(TC_IFSWS); + break; + case V_PATH: + afree(path, APERM); + strdupx(path, def_path, APERM); + /* clear tracked aliases */ + flushcom(true); + break; +#ifndef MKSH_NO_CMDLINE_EDITING + case V_TERM: + x_initterm(null); + return; +#endif + case V_TMPDIR: + /* should not become unspecial */ + if (tmpdir) { + afree(tmpdir, APERM); + tmpdir = NULL; + } + break; + case V_LINENO: + case V_RANDOM: + case V_SECONDS: + case V_TMOUT: + /* AT&T ksh leaves previous value in place */ + unspecial(vp->name); + break; +#ifdef MKSH_EARLY_LOCALE_TRACKING + case V_LANG: + case V_LC_ALL: + case V_LC_CTYPE: + recheck_ctype(); + return; +#endif + } +} + +/* + * Search for (and possibly create) a table entry starting with + * vp, indexed by val. + */ +struct tbl * +arraysearch(struct tbl *vp, uint32_t val) +{ + struct tbl *prev, *curr, *news; + size_t len; + + vp->flag = (vp->flag | (ARRAY | DEFINED)) & ~ASSOC; + /* the table entry is always [0] */ + if (val == 0) + return (vp); + prev = vp; + curr = vp->u.array; + while (curr && curr->ua.index < val) { + prev = curr; + curr = curr->u.array; + } + if (curr && curr->ua.index == val) { + if (curr->flag&ISSET) + return (curr); + news = curr; + } else + news = NULL; + if (!news) { + len = strlen(vp->name); + checkoktoadd(len, 1 + offsetof(struct tbl, name[0])); + news = alloc(offsetof(struct tbl, name[0]) + ++len, vp->areap); + memcpy(news->name, vp->name, len); + } + news->flag = (vp->flag & ~(ALLOC|DEFINED|ISSET|SPECIAL)) | AINDEX; + news->type = vp->type; + news->areap = vp->areap; + news->u2.field = vp->u2.field; + news->ua.index = val; + + if (curr != news) { + /* not reusing old array entry */ + prev->u.array = news; + news->u.array = curr; + } + return (news); +} + +/* + * Return the length of an array reference (eg, [1+2]) - cp is assumed + * to point to the open bracket. Returns 0 if there is no matching + * closing bracket. + * + * XXX this should parse the actual arithmetic syntax + */ +size_t +array_ref_len(const char *cp) +{ + const char *s = cp; + char c; + int depth = 0; + + while ((c = *s++) && (ord(c) != ORD(']') || --depth)) + if (ord(c) == ORD('[')) + depth++; + if (!c) + return (0); + return (s - cp); +} + +/* + * Make a copy of the base of an array name + */ +char * +arrayname(const char *str) +{ + const char *p; + char *rv; + + if (!(p = cstrchr(str, '['))) + /* Shouldn't happen, but why worry? */ + strdupx(rv, str, ATEMP); + else + strndupx(rv, str, p - str, ATEMP); + + return (rv); +} + +/* set (or overwrite, if reset) the array variable var to the values in vals */ +mksh_uari_t +set_array(const char *var, bool reset, const char **vals) +{ + struct tbl *vp, *vq; + mksh_uari_t i = 0, j = 0; + const char *ccp = var; + char *cp = NULL; + size_t n; + + /* to get local array, use "local foo; set -A foo" */ + n = strlen(var); + if (n > 0 && var[n - 1] == '+') { + /* append mode */ + reset = false; + strndupx(cp, var, n - 1, ATEMP); + ccp = cp; + } + vp = global(ccp); + + /* Note: AT&T ksh allows set -A but not set +A of a read-only var */ + if ((vp->flag&RDONLY)) + errorfx(2, Tf_ro, ccp); + /* This code is quite non-optimal */ + if (reset) { + /* trash existing values and attributes */ + unset(vp, 1); + /* allocate-by-access the [0] element to keep in scope */ + arraysearch(vp, 0); + } + /* + * TODO: would be nice for assignment to completely succeed or + * completely fail. Only really effects integer arrays: + * evaluation of some of vals[] may fail... + */ + if (cp != NULL) { + /* find out where to set when appending */ + for (vq = vp; vq; vq = vq->u.array) { + if (!(vq->flag & ISSET)) + continue; + if (arrayindex(vq) >= j) + j = arrayindex(vq) + 1; + } + afree(cp, ATEMP); + } + while ((ccp = vals[i])) { +#if 0 /* temporarily taken out due to regression */ + if (ord(*ccp) == ORD('[')) { + int level = 0; + + while (*ccp) { + if (ord(*ccp) == ORD(']') && --level == 0) + break; + if (ord(*ccp) == ORD('[')) + ++level; + ++ccp; + } + if (ord(*ccp) == ORD(']') && level == 0 && + ord(ccp[1]) == ORD('=')) { + strndupx(cp, vals[i] + 1, ccp - (vals[i] + 1), + ATEMP); + evaluate(substitute(cp, 0), (mksh_ari_t *)&j, + KSH_UNWIND_ERROR, true); + afree(cp, ATEMP); + ccp += 2; + } else + ccp = vals[i]; + } +#endif + + vq = arraysearch(vp, j); + /* would be nice to deal with errors here... (see above) */ + setstr(vq, ccp, KSH_RETURN_ERROR); + i++; + j++; + } + + return (i); +} + +void +change_winsz(void) +{ + struct timeval tv; + + mksh_TIME(tv); + BAFHUpdateMem_mem(qh_state, &tv, sizeof(tv)); + +#ifdef TIOCGWINSZ + /* check if window size has changed */ + if (tty_init_fd() < 2) { + struct winsize ws; + + if (ioctl(tty_fd, TIOCGWINSZ, &ws) >= 0) { + if (ws.ws_col) + x_cols = ws.ws_col; + if (ws.ws_row) + x_lins = ws.ws_row; + } + } +#endif + + /* bounds check for sane values, use defaults otherwise */ + if (x_cols < MIN_COLS) + x_cols = 80; + if (x_lins < MIN_LINS) + x_lins = 24; + +#ifdef SIGWINCH + got_winch = 0; +#endif +} + +uint32_t +hash(const void *s) +{ + register uint32_t h; + + BAFHInit(h); + BAFHUpdateStr_reg(h, s); + BAFHFinish_reg(h); + return (h); +} + +uint32_t +chvt_rndsetup(const void *bp, size_t sz) +{ + register uint32_t h; + + /* use LCG as seed but try to get them to deviate immediately */ + h = lcg_state; + (void)rndget(); + BAFHFinish_reg(h); + /* variation through pid, ppid, and the works */ + BAFHUpdateMem_reg(h, &rndsetupstate, sizeof(rndsetupstate)); + /* some variation, some possibly entropy, depending on OE */ + BAFHUpdateMem_reg(h, bp, sz); + /* mix them all up */ + BAFHFinish_reg(h); + + return (h); +} + +mksh_ari_t +rndget(void) +{ + /* + * this is the same Linear Congruential PRNG as Borland + * C/C++ allegedly uses in its built-in rand() function + */ + return (((lcg_state = 22695477 * lcg_state + 1) >> 16) & 0x7FFF); +} + +void +rndset(unsigned long v) +{ + register uint32_t h; +#if defined(arc4random_pushb_fast) || defined(MKSH_A4PB) + register uint32_t t; +#endif + struct { + struct timeval tv; + void *sp; + uint32_t qh; + pid_t pp; + short r; + } z; + + /* clear the allocated space, for valgrind and to avoid UB */ + memset(&z, 0, sizeof(z)); + + h = lcg_state; + BAFHFinish_reg(h); + BAFHUpdateMem_reg(h, &v, sizeof(v)); + + mksh_TIME(z.tv); + z.sp = &lcg_state; + z.pp = procpid; + z.r = (short)rndget(); + +#if defined(arc4random_pushb_fast) || defined(MKSH_A4PB) + t = qh_state; + BAFHFinish_reg(t); + z.qh = (t & 0xFFFF8000) | rndget(); + lcg_state = (t << 15) | rndget(); + /* + * either we have very chap entropy get and push available, + * with malloc() pulling in this code already anyway, or the + * user requested us to use the old functions + */ + t = h; + BAFHUpdateMem_reg(t, &lcg_state, sizeof(lcg_state)); + BAFHFinish_reg(t); + lcg_state = t; +#if defined(arc4random_pushb_fast) + arc4random_pushb_fast(&lcg_state, sizeof(lcg_state)); + lcg_state = arc4random(); +#else + lcg_state = arc4random_pushb(&lcg_state, sizeof(lcg_state)); +#endif + BAFHUpdateMem_reg(h, &lcg_state, sizeof(lcg_state)); +#else + z.qh = qh_state; +#endif + + BAFHUpdateMem_reg(h, &z, sizeof(z)); + BAFHFinish_reg(h); + lcg_state = h; +} + +void +rndpush(const void *s) +{ + register uint32_t h = qh_state; + + BAFHUpdateStr_reg(h, s); + BAFHUpdateOctet_reg(h, 0); + qh_state = h; +} + +/* record last glob match */ +void +record_match(const char *istr) +{ + struct tbl *vp; + + vp = local("KSH_MATCH", false); + unset(vp, 1); + vp->flag = DEFINED | RDONLY; + setstr(vp, istr, 0x4); +} + +/* typeset, global(deprecated), export, and readonly */ +int +c_typeset(const char **wp) +{ + struct tbl *vp, **p; + uint32_t fset = 0, fclr = 0, flag; + int thing = 0, field = 0, base = 0, i; + struct block *l; + const char *opts; + const char *fieldstr = NULL, *basestr = NULL; + bool localv = false, func = false, pflag = false, istset = true; + enum namerefflag new_refflag = SRF_NOP; + + switch (**wp) { + + /* export */ + case 'e': + fset |= EXPORT; + istset = false; + break; + + /* readonly */ + case 'r': + fset |= RDONLY; + istset = false; + break; + + /* set */ + case 's': + /* called with 'typeset -' */ + break; + + /* typeset */ + case 't': + localv = true; + break; + } + + /* see comment below regarding possible opions */ + opts = istset ? "L#R#UZ#afgi#lnprtux" : "p"; + + builtin_opt.flags |= GF_PLUSOPT; + /* + * AT&T ksh seems to have 0-9 as options which are multiplied + * to get a number that is used with -L, -R, -Z or -i (eg, -1R2 + * sets right justify in a field of 12). This allows options + * to be grouped in an order (eg, -Lu12), but disallows -i8 -L3 and + * does not allow the number to be specified as a separate argument + * Here, the number must follow the RLZi option, but is optional + * (see the # kludge in ksh_getopt()). + */ + while ((i = ksh_getopt(wp, &builtin_opt, opts)) != -1) { + flag = 0; + switch (i) { + case 'L': + flag = LJUST; + fieldstr = builtin_opt.optarg; + break; + case 'R': + flag = RJUST; + fieldstr = builtin_opt.optarg; + break; + case 'U': + /* + * AT&T ksh uses u, but this conflicts with + * upper/lower case. If this option is changed, + * need to change the -U below as well + */ + flag = INT_U; + break; + case 'Z': + flag = ZEROFIL; + fieldstr = builtin_opt.optarg; + break; + case 'a': + /* + * this is supposed to set (-a) or unset (+a) the + * indexed array attribute; it does nothing on an + * existing regular string or indexed array though + */ + break; + case 'f': + func = true; + break; + case 'g': + localv = (builtin_opt.info & GI_PLUS) ? true : false; + break; + case 'i': + flag = INTEGER; + basestr = builtin_opt.optarg; + break; + case 'l': + flag = LCASEV; + break; + case 'n': + new_refflag = (builtin_opt.info & GI_PLUS) ? + SRF_DISABLE : SRF_ENABLE; + break; + /* export, readonly: POSIX -p flag */ + case 'p': + /* typeset: show values as well */ + pflag = true; + if (istset) + continue; + break; + case 'r': + flag = RDONLY; + break; + case 't': + flag = TRACE; + break; + case 'u': + /* upper case / autoload */ + flag = UCASEV_AL; + break; + case 'x': + flag = EXPORT; + break; + case '?': + return (1); + } + if (builtin_opt.info & GI_PLUS) { + fclr |= flag; + fset &= ~flag; + thing = '+'; + } else { + fset |= flag; + fclr &= ~flag; + thing = '-'; + } + } + + if (fieldstr && !getn(fieldstr, &field)) { + bi_errorf(Tf_sD_s, Tbadnum, fieldstr); + return (1); + } + if (basestr) { + if (!getn(basestr, &base)) { + bi_errorf(Tf_sD_s, "bad integer base", basestr); + return (1); + } + if (base < 1 || base > 36) + base = 10; + } + + if (!(builtin_opt.info & GI_MINUSMINUS) && wp[builtin_opt.optind] && + (wp[builtin_opt.optind][0] == '-' || + wp[builtin_opt.optind][0] == '+') && + wp[builtin_opt.optind][1] == '\0') { + thing = wp[builtin_opt.optind][0]; + builtin_opt.optind++; + } + + if (func && (((fset|fclr) & ~(TRACE|UCASEV_AL|EXPORT)) || + new_refflag != SRF_NOP)) { + bi_errorf("only -t, -u and -x options may be used with -f"); + return (1); + } + if (wp[builtin_opt.optind]) { + /* + * Take care of exclusions. + * At this point, flags in fset are cleared in fclr and vice + * versa. This property should be preserved. + */ + if (fset & LCASEV) + /* LCASEV has priority over UCASEV_AL */ + fset &= ~UCASEV_AL; + if (fset & LJUST) + /* LJUST has priority over RJUST */ + fset &= ~RJUST; + if ((fset & (ZEROFIL|LJUST)) == ZEROFIL) { + /* -Z implies -ZR */ + fset |= RJUST; + fclr &= ~RJUST; + } + /* + * Setting these attributes clears the others, unless they + * are also set in this command + */ + if ((fset & (LJUST | RJUST | ZEROFIL | UCASEV_AL | LCASEV | + INTEGER | INT_U | INT_L)) || new_refflag != SRF_NOP) + fclr |= ~fset & (LJUST | RJUST | ZEROFIL | UCASEV_AL | + LCASEV | INTEGER | INT_U | INT_L); + } + if (new_refflag != SRF_NOP) { + fclr &= ~(ARRAY | ASSOC); + fset &= ~(ARRAY | ASSOC); + fclr |= EXPORT; + fset |= ASSOC; + if (new_refflag == SRF_DISABLE) + fclr |= ASSOC; + } + + /* set variables and attributes */ + if (wp[builtin_opt.optind] && + /* not "typeset -p varname" */ + !(!func && pflag && !(fset | fclr))) { + int rv = 0; + struct tbl *f; + + if (localv && !func) + fset |= LOCAL; + for (i = builtin_opt.optind; wp[i]; i++) { + if (func) { + f = findfunc(wp[i], hash(wp[i]), + tobool(fset & UCASEV_AL)); + if (!f) { + /* AT&T ksh does ++rv: bogus */ + rv = 1; + continue; + } + if (fset | fclr) { + f->flag |= fset; + f->flag &= ~fclr; + } else { + fpFUNCTf(shl_stdout, 0, + tobool(f->flag & FKSH), + wp[i], f->val.t); + shf_putc('\n', shl_stdout); + } + } else if (!typeset(wp[i], fset, fclr, field, base)) { + bi_errorf(Tf_sD_s, wp[i], Tnot_ident); + return (1); + } + } + return (rv); + } + + /* list variables and attributes */ + + /* no difference at this point.. */ + flag = fset | fclr; + if (func) { + for (l = e->loc; l; l = l->next) { + for (p = ktsort(&l->funs); (vp = *p++); ) { + if (flag && (vp->flag & flag) == 0) + continue; + if (thing == '-') + fpFUNCTf(shl_stdout, 0, + tobool(vp->flag & FKSH), + vp->name, vp->val.t); + else + shf_puts(vp->name, shl_stdout); + shf_putc('\n', shl_stdout); + } + } + } else if (wp[builtin_opt.optind]) { + for (i = builtin_opt.optind; wp[i]; i++) { + vp = isglobal(wp[i], false); + c_typeset_vardump(vp, flag, thing, + last_lookup_was_array ? 4 : 0, pflag, istset); + } + } else + c_typeset_vardump_recursive(e->loc, flag, thing, pflag, istset); + return (0); +} + +static void +c_typeset_vardump_recursive(struct block *l, uint32_t flag, int thing, + bool pflag, bool istset) +{ + struct tbl **blockvars, *vp; + + if (l->next) + c_typeset_vardump_recursive(l->next, flag, thing, pflag, istset); + blockvars = ktsort(&l->vars); + while ((vp = *blockvars++)) + c_typeset_vardump(vp, flag, thing, 0, pflag, istset); + /*XXX doesn’t this leak? */ +} + +static void +c_typeset_vardump(struct tbl *vp, uint32_t flag, int thing, int any_set, + bool pflag, bool istset) +{ + struct tbl *tvp; + char *s; + + if (!vp) + return; + + /* + * See if the parameter is set (for arrays, if any + * element is set). + */ + for (tvp = vp; tvp; tvp = tvp->u.array) + if (tvp->flag & ISSET) { + any_set |= 1; + break; + } + + /* + * Check attributes - note that all array elements + * have (should have?) the same attributes, so checking + * the first is sufficient. + * + * Report an unset param only if the user has + * explicitly given it some attribute (like export); + * otherwise, after "echo $FOO", we would report FOO... + */ + if (!any_set && !(vp->flag & USERATTRIB)) + return; + if (flag && (vp->flag & flag) == 0) + return; + if (!(vp->flag & ARRAY)) + /* optimise later conditionals */ + any_set = 0; + do { + /* + * Ignore array elements that aren't set unless there + * are no set elements, in which case the first is + * reported on + */ + if (any_set && !(vp->flag & ISSET)) + continue; + /* no arguments */ + if (!thing && !flag) { + if (any_set == 1) { + shprintf(Tf_s_s_sN, Tset, "-A", vp->name); + any_set = 2; + } + /* + * AT&T ksh prints things like export, integer, + * leftadj, zerofill, etc., but POSIX says must + * be suitable for re-entry... + */ + shprintf(Tf_s_s, Ttypeset, ""); + if (((vp->flag & (ARRAY | ASSOC)) == ASSOC)) + shprintf(Tf__c_, 'n'); + if ((vp->flag & INTEGER)) + shprintf(Tf__c_, 'i'); + if ((vp->flag & EXPORT)) + shprintf(Tf__c_, 'x'); + if ((vp->flag & RDONLY)) + shprintf(Tf__c_, 'r'); + if ((vp->flag & TRACE)) + shprintf(Tf__c_, 't'); + if ((vp->flag & LJUST)) + shprintf("-L%d ", vp->u2.field); + if ((vp->flag & RJUST)) + shprintf("-R%d ", vp->u2.field); + if ((vp->flag & ZEROFIL)) + shprintf(Tf__c_, 'Z'); + if ((vp->flag & LCASEV)) + shprintf(Tf__c_, 'l'); + if ((vp->flag & UCASEV_AL)) + shprintf(Tf__c_, 'u'); + if ((vp->flag & INT_U)) + shprintf(Tf__c_, 'U'); + } else if (pflag) { + shprintf(Tf_s_s, istset ? Ttypeset : + (flag & EXPORT) ? Texport : Treadonly, ""); + } + if (any_set) + shprintf("%s[%lu]", vp->name, arrayindex(vp)); + else + shf_puts(vp->name, shl_stdout); + if ((!thing && !flag && pflag) || + (thing == '-' && (vp->flag & ISSET))) { + s = str_val(vp); + shf_putc('=', shl_stdout); + /* AT&T ksh can't have justified integers... */ + if ((vp->flag & (INTEGER | LJUST | RJUST)) == INTEGER) + shf_puts(s, shl_stdout); + else + print_value_quoted(shl_stdout, s); + } + shf_putc('\n', shl_stdout); + + /* + * Only report first 'element' of an array with + * no set elements. + */ + if (!any_set) + return; + } while (!(any_set & 4) && (vp = vp->u.array)); +} diff --git a/var_spec.h b/var_spec.h new file mode 100644 index 0000000..d8444dd --- /dev/null +++ b/var_spec.h @@ -0,0 +1,80 @@ +/*- + * Copyright (c) 2009, 2011, 2012, 2016, 2018 + * mirabilos + * + * Provided that these terms and disclaimer and all copyright notices + * are retained or reproduced in an accompanying document, permission + * is granted to deal in this work without restriction, including un- + * limited rights to use, publicly perform, distribute, sell, modify, + * merge, give away, or sublicence. + * + * This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to + * the utmost extent permitted by applicable law, neither express nor + * implied; without malicious intent or gross negligence. In no event + * may a licensor, author or contributor be held liable for indirect, + * direct, other damage, loss, or other issues arising in any way out + * of dealing in the work, even if advised of the possibility of such + * damage or existence of a defect, except proven that it results out + * of said person's immediate fault when using the work as intended. + */ + +#if defined(VARSPEC_DEFNS) +__RCSID("$MirOS: src/bin/mksh/var_spec.h,v 1.11 2018/01/13 21:38:10 tg Exp $"); +#define FN(name) /* nothing */ +#elif defined(VARSPEC_ENUMS) +#define FN(name) V_##name, +#define F0(name) V_##name = 0, +#elif defined(VARSPEC_ITEMS) +#define F0(name) /* nothing */ +#define FN(name) #name, +#endif + +#ifndef F0 +#define F0 FN +#endif + +/* NOTE: F0 are skipped for the ITEMS array, only FN generate names */ + +/* 0 is always V_NONE */ +F0(NONE) + +/* 1 and up are special variables */ +FN(BASHPID) +#ifdef __OS2__ +FN(BEGINLIBPATH) +#endif +FN(COLUMNS) +#ifdef __OS2__ +FN(ENDLIBPATH) +#endif +FN(EPOCHREALTIME) +#if HAVE_PERSISTENT_HISTORY +FN(HISTFILE) +#endif +FN(HISTSIZE) +FN(IFS) +#ifdef MKSH_EARLY_LOCALE_TRACKING +FN(LANG) +FN(LC_ALL) +FN(LC_CTYPE) +#endif +#ifdef __OS2__ +FN(LIBPATHSTRICT) +#endif +FN(LINENO) +FN(LINES) +FN(OPTIND) +FN(PATH) +FN(RANDOM) +FN(SECONDS) +#ifndef MKSH_NO_CMDLINE_EDITING +FN(TERM) +#endif +FN(TMOUT) +FN(TMPDIR) + +#undef FN +#undef F0 +#undef VARSPEC_DEFNS +#undef VARSPEC_ENUMS +#undef VARSPEC_ITEMS