~q3cpma/mus

c2e706cac6a8826b302be7310db9138bdeade197 — q3cpma 3 months ago 0b156fa
Improve README and some help messages
Sync posix-build and genhtab
Remove runtime readlink -f requirement
mus_player: use -ffast-math for triangle_dither if available, fix Opus and Vorbis (!!)
M README => README +18 -32
@@ 10,7 10,7 @@ directory path playlist.
File support is limited to 16 bits mono/stereo FLAC, Vorbis and Opus on
GNU/Linux and {Free,Net,Dragonfly}BSD, out of the box. It can work easily on
OpenBSD and MacOS by installing a port for flock(1).
Features include gapless playback, replaygain support and event reporting, see
Features include gapless playback, ReplayGain support and event reporting, see
TODO for stuff in the work.

mus is formed of several independant parts:


@@ 30,7 30,7 @@ mus is formed of several independant parts:
        Play audio files and answer to mus_client's commands.

    * mus_udsend:
        The helper program to communicate with UNIX domain sockets (thanks
        Helper program to communicate with UNIX domain sockets (thanks
        POSIX). Mandatory if you want to use the IPC feature.

A lemonbar status script can be found at [1]


@@ 40,46 40,32 @@ A lemonbar status script can be found at [1]
        ----------------------------

In addition to everything specified by the latest POSIX, you'll need
the following for mus_player:
    * C11 compiler (build)
the following at build time:
    * C11 compiler
    * posix-build dependencies (mktemp -d and local/typeset support in /bin/sh)
    * pthread
    * libao
    * libflac (build, optional)
    * vorbisfile (build, optional), often part of the libvorbis package
    * opusfile (build, optional)
    * libao headers
    * libflac, vorbisfile, opusfile headers (each is optional)

and some runtime dependencies:
    * GNU/Linux:
        ed(1) (POSIX requirement but often absent) and the util-linux package.

    * FreeBSD, NetBSD, DragonflyBSD:
        None.

    * OpenBSD:
        flock(1) from the port tree.

    * MacOS:
        flock(1) from somewhere (e.g. [2]) and a readlink implementation with
        the -f option (e.g. GNU readlink).
	* libao
	* libflac, vorbisfile, opusfile (according to the build options)
	* On GNU/Linux, ed(1) and the util-linux package.
	* On OpenBSD, flock(1) from the port tree.
	* On MacOS, flock(1) from somewhere (e.g. [2]).

mus was tested on Gentoo GNU/Linux with:
    * /bin/awk -> busybox
        OK
    * /bin/awk -> mawk
        Works with mawk >= 20181114 (see [3]).
    * /bin/sh -> busybox
        OK
    * /bin/sh -> dash
        OK
    * /bin/awk -> busybox: OK
    * /bin/awk -> mawk:    OK with mawk >= 20181114 (see [3]).
    * /bin/sh -> busybox:  OK
    * /bin/sh -> dash:     OK


        Building and installation
        -------------------------

To build and install mus (default values shown); you'll have to specify a
different CC like gcc or clang, since mus_player is C11:
    $ [CC=c99] [LTO=false] [NATIVE=false] [USE_FLAC=true] [USE_OGG_VORBIS=false] \
To build and install mus (default values shown between brackets), you'll have
to specify a C11 C compiler like gcc or clang:
    $ CC=gcc [LTO=false] [NATIVE=false] [USE_FLAC=true] [USE_OGG_VORBIS=false] \
          [USE_OGG_OPUS=false] ./build.sh
    # [DESTDIR=] [PREFIX=/usr/local] ./build.sh install


M TODO => TODO +2 -2
@@ 1,7 1,7 @@
* 24/32 bit output (needs double dithering process)?
* 24/32 bit output (needs double dithering process) ?
* Add GAIN/VOLUME command to avoid a second dithering?
* Install signal handlers for SIGINT/TERM/HUP
* Kill the status command after some time? Or just use timeout(1)
* Kill the status command after some time ?
* Obey RG spec by trying to use track gain when album gain is selected but
  absent from tags
* Start paused option

M build_util.sh => build_util.sh +36 -29
@@ 50,7 50,7 @@ value()
# Append to variable $1, which need not exists
append()
{
	local var=$1
	local var="$1"
	shift
	eval "$var=\${$var:+\$$var }\$*"
}


@@ 59,9 59,10 @@ append()
# which needs not exists
appendnl()
{
	local var=$1
	local var="$1" nl='
'
	shift
	eval "$var=\${$var:+\$$var }\$(printf '%s\n' \"\$@\")"
	eval "$var=\${$var:+\$$var$nl}\$(printf '%s\n' \"\$@\")"
}

# Return its last argument


@@ 73,7 74,7 @@ lastarg()
# Check that type $1 is respected for the remaining argument variables
typecheck()
{
	local type=$1 var= val= success=true
	local type="$1" var= val= success=true
	shift
	for var
	do


@@ 136,7 137,7 @@ match()
# remaining argument paths or if one of these doesn't exists
requirefile()
{
	local testarg=$1 i=
	local testarg="$1" i=
	shift
	for i
	do


@@ 162,7 163,7 @@ requirefile()
	done
}

# For each argument, check for executable presence; use a,b,c... for alternatives
# For each argument, check for executable presence; use a|b|c... for alternatives
requirebin()
{
	local i= j= success=


@@ 170,7 171,7 @@ requirebin()
	do
		if match "$i" '.*|.*'
		then
			i=$(pecho "$i" | tr ',' ' ')
			i=$(pecho "$i" | tr '|' ' ')
			success=false
			for j in $i
			do


@@ 226,7 227,7 @@ quote()
# Double quote $1 $2 times (defaults to 1)
dquote()
{
	local res=$1 cnt=${2:-1}
	local res="$1" cnt="${2:-1}"
	while [ $cnt -ne 0 ]
	do
		res=$(pecho "$res" | sed 's#"#\\"#g; s#^#"#; s#$#"#')


@@ 263,7 264,7 @@ glob_escape()
# arguments are passed to find.
listfiles()
{
	local dir=$1
	local dir="$1"
	shift
	find -- "$dir" \( ! -path "$(glob_escape "$dir")" -prune \) "$@"
}


@@ 315,26 316,32 @@ text_format()
# e.g. "foo.tar.gz" returns "gz")
file_ext()
{
	pecho "$1" | sed -n 's#.*[^/]\.\([^.]*\)$#\1#p'
	local i=
	readargs "$@" | while IFS= read -r i
	do
		pecho "$i" | sed -n 's#.*[^/]\.\([^.]*\)$#\1#p'
	done
}

# Print the mimetype of $1
mimetype()
{
	file --dereference --brief --mime-type -- "$1"
	local i=
	readargs "$@" | while IFS= read -r i
	do
		file --dereference --brief --mime-type -- "$i"
	done
}

# Extract the basename of a filename (minus the extension)
file_base()
{
	local tmp=$(basename -- "$1")
	local base=${tmp%.*}
	if [ ! "$base" ]
	then
		pecho "$tmp"
	else
		pecho "$base"
	fi
	local i= base=
	readargs "$@" | while IFS= read -r i
	do
		base=${i%.*}
		pecho "${base:+$(basename -- "$i")}"
	done
}

# Read a password securely into variable $2 using $1 as a prompt


@@ 356,7 363,7 @@ is_help()
# Print stdin lines as a pretty list: "line1, line2, ..."
list_join()
{
	local sep=${1:-, }
	local sep="${1:-, }"
	sed ':a; N; $!ba; s#\n#'"$(sed_repl_escape "$sep")"'#g'
}



@@ 377,7 384,7 @@ rand()
# IFS split $1 into the variables $2, $3, ...
read_split()
{
	local str=$1
	local str="$1"
	shift
	read -r "$@" <<-EOF
		$str


@@ 388,8 395,8 @@ EOF
# printed to stdout
pselect()
{
	local choice= in=$(cat)
	local len=$(pecho "$in" | wc -l)
	local choice= in="$(cat)"
	local len="$(pecho "$in" | wc -l)"
	while true
	do
		pecho "$in" | awk '{printf "%d) %s\n", NR, $0}' >&2


@@ 480,20 487,20 @@ preadlink()
	done
}

if command readlink --version 2>/dev/null | grep -qF '^readlink (GNU coreutils)'
then
	alias preadlinkf='readlink -f --'
elif command -v greadlink >/dev/null
if command -v greadlink >/dev/null
then
	alias preadlinkf='greadlink -f --'
elif readlink --version 2>/dev/null | grep -q '^readlink (GNU coreutils)'
then
	alias preadlinkf='readlink -f --'
else
	# Portable readlink -f using GNU's semantics (last component need not exist)
	# Currently passes the tests from https://github.com/ko1nksm/preadlinkf
	# Currently passes the tests from https://github.com/ko1nksm/readlinkf
	# except loop detection (`getconf SYMLOOP_MAX` is undefined here, anyway)
	preadlinkf()
	{
		[ $# -eq 0 ] && return 1
		local i= status=0 pwd=$(pwd -P) base= dir=
		local i= status=0 pwd="$(pwd -P)" base= dir=
		for i
		do
			! [ "$i" ] && { status=1; continue; }

M fair_shuf => fair_shuf +11 -10
@@ 13,17 13,18 @@ usage()
    $(basename -- "$0") [**-hd0**] [**-n** __NLINES__] [**-m** __MULT__] [**-e** __EXP__] __DB_PATH__

**DESCRIPTION**
    A simple shuf(1) alternative keeping count of the lines it already picked
    and favouring the less often picked ones. Unlike shuf(1), it default to
    outputting one line.
    A simple shuf(1) alternative keeping count of the lines picked in the past
    and favouring the less often picked ones. Unlike shuf(1), it defaults to
    outputting a single line.

    For that purpose, it reads a database __DB_PATH__ consisting of lines
    prefixed by their pick counter followed by at least one whitespace, which
    will be updated after execution.
    For that purpose, it reads a database at __DB_PATH__ consisting of lines
    prefixed by their pick counter and at least one whitespace, which will be
    updated after execution.

    Each line's probability of being picked will be weighted by its absolute
    deviation from the mean raised to the __EXP__th power then multiplied by
    __MULT__.  Both __EXP__ and __MULT__ defaults to 1.
    Each line's probability of being picked will be weighted by the absolute
    deviation of its pick count from the mean pick count raised to the
    __EXP__th power then multiplied by __MULT__.  Both __EXP__ and __MULT__
    default to 1.

**OPTIONS**
    **-h**


@@ 36,7 37,7 @@ usage()
        Use '\0' instead of '\n' as database and output line separator.

    **-n** __NLINES__
        Output __NLINES__ lines instead of one.
        Output __NLINES__ lines instead of just one.

    **-m** __MULT__
        Set the weight multiplier to __MULT__.

M mus_album_db_create => mus_album_db_create +1 -1
@@ 11,7 11,7 @@ usage()
{
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Album db creator
    $(basename -- "$0") - Album database creator

**SYNOPSIS**
    $(basename -- "$0") [**-h**] [**-d** __FILE__] [__MUSIC_DIR__...]

M mus_album_db_update => mus_album_db_update +2 -2
@@ 11,7 11,7 @@ usage()
{
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Album db updater
    $(basename -- "$0") - Album database updater

**SYNOPSIS**
    $(basename -- "$0") [**-h**] [**-d** __FILE__] [__MUSIC_DIR__...]


@@ 73,4 73,4 @@ updated_db=$("$rpath_bin"/mus_album_find "$@" | awk '
	db[$0] {print db[$0]; next}
	{print 0, $0}' "$album_db" - | LC_ALL=C sort -k2)

echop "$updated_db" | flock -- "$album_db" sh -c 'cat >"$1"' dummy "$album_db"
pecho "$updated_db" | flock -- "$album_db" sh -c 'cat >"$1"' dummy "$album_db"

M mus_album_find => mus_album_find +7 -8
@@ 1,12 1,11 @@
#!/bin/sh
# Dependencies:
# Portability:  GNU, *BSD
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+-------+
#| cmd\OS      | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 | POSIX |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+-------+
#| readlink -f | o   | o       | o      | o            | o       |       |         |       |     |      |       |       |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+-------+
# o: supported, X: different behaviour
# Portability:  GNU, *BSD, MacOS, Illumos, HP-UX, AIX, IRIX, Tru64
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
#| cmd\OS      | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
#| readlink    | o   | o       | o      | o            | o       | o     | o       | o     | o   | o    | o     |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
set -eu
rpath_share=$(dirname -- "$0")
. "$rpath_share"/mus_util.sh


@@ 44,6 43,6 @@ fi
for i
do
	requirefile -d "$i"
	i=$(readlink -f -- "$i")
	i=$(preadlinkf "$i")
	find -- "$i" -type d -path "$i/*/*" -print -prune
done

M mus_album_pick => mus_album_pick +10 -11
@@ 1,12 1,11 @@
#!/bin/sh
# Dependencies:
# Portability:  GNU, *BSD
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+-------+
#| cmd\OS      | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 | POSIX |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+-------+
#| readlink -f | o   | o       | o      | o            | o       |       |         |       |     |      |       |       |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+-------+
# o: supported, X: different behaviour
# Portability:  GNU, *BSD, MacOS, Illumos, HP-UX, AIX, IRIX, Tru64
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
#| cmd\OS      | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
#| readlink    | o   | o       | o      | o            | o       | o     | o       | o     | o   | o    | o     |
#+-------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
set -eu
rpath_share=$(dirname -- "$0")
. "$rpath_share"/mus_util.sh


@@ 67,12 66,12 @@ requirefile -f "$album_db"
albums=$(readargs "$@" | while IFS= read -r line
		 do
			 requirefile -d "$line"
			 readlink -f -- "$line"
			 preadlinkf "$line"
		 done)
[ ! "$albums" ] && die "Empty stdin"

set +e
updated_db=$(echop "$albums" | awk '
updated_db=$(pecho "$albums" | awk '
	FILENAME == ARGV[1]\
	{
		save = $0


@@ 101,5 100,5 @@ updated_db=$(echop "$albums" | awk '
[ $? -eq 1 ] && exit 1
set -e

echop "$updated_db" | flock -- "$album_db" sh -c 'cat >"$1"' dummy "$album_db"
echop "$albums"
pecho "$updated_db" | flock -- "$album_db" sh -c 'cat >"$1"' dummy "$album_db"
pecho "$albums"

M mus_client => mus_client +1 -2
@@ 134,14 134,13 @@ then
	ans=$("$rpath_cbin"/mus_udsend "$udsock" "$cmd")
	if [ "$cmd" = STATUS ]
	then
		echop "$ans"
		pecho "$ans"
	else
		if [ "$ans" != "OK" ]
		then
			die "$ans: unexpected server answer to command $cmd"
		fi
	fi
fi
elif [ "$arg" != QUIT ]
then
	requirefile -S "$udsock" # To get the problem error message

M mus_daemon => mus_daemon +3 -3
@@ 29,7 29,7 @@ EOF
exec_status_cmd()
{
	set +e
	[ "$status_cmd" ] && echop "status $1" | sh -c "$status_cmd"
	[ "$status_cmd" ] && pecho "status $1" | sh -c "$status_cmd"
	set -e
}



@@ 123,7 123,7 @@ done
shift $((OPTIND - 1))
[ $# -ne 0 ] && usage 1

echop $$ >"$pidfile"
pecho $$ >"$pidfile"

trap 'exit' INT TERM HUP QUIT
trap 'rm -f -- "$pidfile"' EXIT


@@ 162,7 162,7 @@ do
	album=$(plread "$playlist")

	[ ! -f "$album" ] && [ ! -d "$album" ] &&
		{ echop "$album: file not found"; continue;}
		{ pecho "$album: file not found"; continue;}

	find -- "$album" -type f $find_opts | sort -n | xargsnl "$@" 2>>"$log"
done

M mus_player/build.sh => mus_player/build.sh +1 -1
@@ 96,7 96,7 @@ else
				;;
		esac
		[ "$target" = player ] && genhtab/build.sh "$1"
		exit
		exit 0
	elif [ $# -gt 1 ]
	then
		pb_usage 1

M mus_player/filter.c => mus_player/filter.c +8 -0
@@ 16,6 16,11 @@ void interleave16_stereo(const int32_t *restrict inbuf[],
}

#define INT16_HEADROOM_MULT (32767.497f / 32768.f)

#if defined(__GNUC__) || defined(__clang__)
#	pragma GCC push_options
#	pragma GCC optimize ("-ffast-math")
#endif
static inline int16_t triangle_dither(float x)
{
	static float prev_rand = 0.f;


@@ 33,6 38,9 @@ void apply_gain(int16_t *restrict buf, size_t nbsample, float gain)
	for (size_t i = 0; i < nbsample; ++i)
		buf[i] = triangle_dither(buf[i] * gain);
}
#if defined(__GNUC__) || defined(__clang__)
#	pragma GCC pop_options
#endif

bool write_pcm(int16_t *restrict buf, int outfd, size_t nbsample)
{

M mus_player/genhtab/array.h => mus_player/genhtab/array.h +2 -3
@@ 1,6 1,5 @@
/* Generic array with length and capacity stored in the first 8 bytes.
 * The array pointer then points to the real pointer (given by malloc)
 * with and offset of 2 * sizeof(size_t) bytes.
/* Generic array with length and capacity stored in the first 2 *
 * sizeof(size_t) bytes.
 *
 * Inspired by https://github.com/eteran/c-vector, but without all the NULL
 * checking and the PTR_GARRAY functions added */

M mus_player/opus.c => mus_player/opus.c +3 -3
@@ 59,7 59,7 @@ void * opus_init(int fd, Decoder *dec)
		op_free(of);
		return NULL;
	}
	dec->tinfo->format.bits = 0;
	dec->tinfo->format.bits = 16;
	dec->tinfo->format.rate = 48000;
	dec->tinfo->format.channels = nbchan;
	dec->tinfo->bitrate = op_bitrate(of, -1);


@@ 71,8 71,8 @@ void * opus_init(int fd, Decoder *dec)
	for (int i = 0; i < ot->comments; ++i)
		tag_vorbis_comment_parse(ot->user_comments[i], dec->tinfo->tags);

	if (!dec->tinfo->tags[RG_ALBUM_GAIN].empty ||
	   !dec->tinfo->tags[RG_TRACK_GAIN].empty)
	if (!dec->tinfo->tags[TAG_RG_ALBUM_GAIN].empty ||
	   !dec->tinfo->tags[TAG_RG_TRACK_GAIN].empty)
	{
		log_append(LOG_WARNING, "%s: invalid ReplayGain tags, use R128_ "
			"tags", path);

M mus_player/vorbis.c => mus_player/vorbis.c +1 -1
@@ 60,7 60,7 @@ void * vorbis_init(int fd, Decoder *dec)
		vorbis_free(vf);
		return NULL;
	}
	dec->tinfo->format.bits = 0;
	dec->tinfo->format.bits = 16;
	dec->tinfo->format.rate = vi->rate;
	dec->tinfo->format.channels = vi->channels;
	dec->tinfo->bitrate = vi->bitrate_nominal;

M mus_util.sh => mus_util.sh +107 -67
@@ 1,5 1,17 @@
# Check if local or typeset is available
if ! sh -c 'fun() { local a=1; }; fun'
then
	if sh -c 'fun() { typeset a=1; }; fun'
	then
		alias local=typeset
	else
		echo "local/typeset not supported by sh, aborting"
		exit 1
	fi
fi

# Portable echo, without any option
echop()
pecho()
{
	printf '%s\n' "$*"
}


@@ 7,7 19,7 @@ echop()
# Print all arguments to stderr and exits with status 1
die()
{
	echop "[$(basename -- "$0")]" "$@" >&2
	pecho "$@" >&2
	exit 1
}



@@ 20,23 32,19 @@ toupper()
# Format stdin so that __foo_bar__ is underlined and **foo*bar** is emboldened
text_format()
{
	local sgr0= bol= smul= rmul=
	if [ -t 1 ]
	then
		set +e
		_sgr0=$(tput sgr0)
		_bold=$(tput bold)
		_smul=$(tput smul)
		_rmul=$(tput rmul)
		sgr0=$(tput sgr0)
		bold=$(tput bold)
		smul=$(tput smul)
		rmul=$(tput rmul)
		set -e
	else
		_sgr0=
		_bold=
		_smul=
		_rmul=
	fi
	sed -E \
		-e ":a; s#([^_]|^)__(([^_]*(_[^_])*[^_]*)+)__([^_]|\$)#\1$_smul\2$_rmul\5#; ta" \
		-e ":b; s#([^*]|^)\*\*(([^\*]*(\*[^*])*[^*]*)+)\*\*([^*]|\$)#\1$_bold\2$_sgr0\5#; tb"
		-e ":a; s#([^_]|^)__(([^_]*(_[^_])*[^_]*)+)__([^_]|\$)#\1$smul\2$rmul\5#; ta" \
		-e ":b; s#([^*]|^)\*\*(([^\*]*(\*[^*])*[^*]*)+)\*\*([^*]|\$)#\1$bold\2$sgr0\5#; tb"
}

# flock wrapper with timeout handling. Pass $1 to sh with the rest of $@ as


@@ 44,82 52,56 @@ text_format()
flock_try_cmd()
{
	set +e
	_cmd=$1
	local cmd="$1" file=
	shift
	eval _file=\$\{$#\}
	flock -E2 -w2 -- "$_file" sh -c "$_cmd" argv0 "$@"
	[ $? -eq 2 ] && die "$_file: lock aquisition timed out"
	eval file=\$\{$#\}
	flock -E2 -w2 -- "$file" sh -c "$cmd" argv0 "$@"
	[ $? -eq 2 ] && die "$file: lock aquisition timed out"
	set -e
}

# poll until a condition is true
poll_test()
{
	_poll_duration=$1
	local poll_duration="$1"
	shift
    until [ "$@" ]
	do
		sleep $_poll_duration
		sleep $poll_duration
	done
}

# Returns 0 if $1 matches $2 (as an ERE), 1 otherwise
match()
{
	echop "$1" | grep -Eqx -- "$2"
	pecho "$1" | grep -Eqx -- "$2"
}

# Die with an appropriate message if "test $1" returns false for any of the
# remaining argument paths or if one of these doesn't exists
requirefile()
{
	_testarg=$1
	local testarg="$1" i=
	shift
	for _i
	for i
	do
		[ ! -L "$_i" ] && [ ! -e "$_i" ] && die "$_i: file not found"
		if [ "$_testarg" != "-e" ] && [ ! "$_testarg" "$_i" ]
		[ ! -L "$i" ] && [ ! -e "$i" ] && die "$i: file not found"
		if [ "$testarg" != "-e" ] && [ ! "$testarg" "$i" ]
		then
			case "$_testarg" in
				-b)
					die "$_i: not a block device"
					;;
				-c)
					die "$_i: not a character special file"
					;;
				-d)
					die "$_i: not a directory"
					;;
				-f)
					die "$_i: not a regular file"
					;;
				-g)
					die "$_i: not a setgid file"
					;;
				-h|-L)
					die "$_i: not a symbolic link"
					;;
				-p)
					die "$_i: not a FIFO"
					;;
				-r)
					die "$_i: not a readable file"
					;;
				-S)
					die "$_i: not a socket"
					;;
				-s)
					die "$_i: not a non-empty file"
					;;
				-u)
					die "$_i: not a setuid file"
					;;
				-w)
					die "$_i: not a writeable file"
					;;
				-x)
					die "$_i: not a executable file"
					;;
			case "$testarg" in
				-b)    die "$i: not a block device";;
				-c)    die "$i: not a character special file";;
				-d)    die "$i: not a directory";;
				-f)    die "$i: not a regular file";;
				-g)    die "$i: not a setgid file";;
				-h|-L) die "$i: not a symbolic link";;
				-p)    die "$i: not a FIFO";;
				-r)    die "$i: not a readable file";;
				-S)    die "$i: not a socket";;
				-s)    die "$i: not a non-empty file";;
				-u)    die "$i: not a setuid file";;
				-w)    die "$i: not a writeable file";;
				-x)    die "$i: not a executable file";;
			esac
		fi
	done


@@ 128,11 110,12 @@ requirefile()
# Die with an appropriate message if one of the argument paths exists
forbidfile()
{
	for _i
	local i=
	for i
	do
		if [ -L "$_i" ] || [ -e "$_i" ]
		if [ -L "$i" ] || [ -e "$i" ]
		then
			die "$_i: file already exists"
			die "$i: file already exists"
		fi
	done
}


@@ 154,3 137,60 @@ xargsnl()
{
	tr '\n' '\0' | xargs -0 "$@"
}

# Simple readlink
preadlink()
{
	local i= lsout=
	for i
	do
		lsout=$(ls -l -- "$i")
		pecho "${lsout#*"$i -> "}"
	done
}

if command -v greadlink >/dev/null
then
	alias preadlinkf='greadlink -f --'
elif readlink --version 2>/dev/null | grep -q '^readlink (GNU coreutils)'
then
	alias preadlinkf='readlink -f --'
else
	# Portable readlink -f using GNU's semantics (last component need not exist)
	# Currently passes the tests from https://github.com/ko1nksm/readlinkf
	# except loop detection (`getconf SYMLOOP_MAX` is undefined here, anyway)
	preadlinkf()
	{
		[ $# -eq 0 ] && return 1
		local i= status=0 pwd="$(pwd -P)" base= dir=
		for i
		do
			! [ "$i" ] && { status=1; continue; }
			if [ -d "$i" ]
			then
				CDPATH= cd -P -- "$i" >/dev/null 2>&1 || return 1
				pwd -P
			else
				case "$i" in */) [ -e "${i%/}" ] && return 1;; esac
				while true
				do
					CDPATH= cd -P -- "$(dirname -- "$i")" >/dev/null 2>&1 || return 1
					base=${i%/}
					base=${base##*/}
					if [ -L "$base" ]
					then
						i=$(ls -l -- "$base")
						i=${i#*"$base -> "}
					else
						dir=$(pwd -P)
						dir=${dir%/}
						printf '%s/%s\n' "$dir" "$base"
						break
					fi
				done
			fi
			cd -- "$pwd"
		done
		return $status
	}
fi