# Various utilities for other POSIX sh scripts
# 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
[ "${BASH:-}" ] && shopt -s expand_aliases
alias ffmpeg='ffmpeg -hide_banner'
alias ffprobe='ffprobe -hide_banner'
# Portable echo, without any option
pecho()
{
printf '%s\n' "$*"
}
# Print all arguments to stderr and exits with status 1
die()
{
pecho "$@" >&2
exit 1
}
# Check if the variable named $1 exists
isset()
{
eval "[ \"\${$1+x}\" ]"
}
# Return the value of variable named $1, fails if no such variable exists
value()
{
match "$1" '[0-9]+' &&
{ pecho "positional parameters can't be used with value()" >&2; return 1; }
isset "$1" ||
{ pecho "$var: variable not set" >&2; return 1; }
eval pecho "\"\$$1\""
}
# Append to variable $1, which need not exists
append()
{
local var="$1"
shift
eval "$var=\${$var:+\$$var }\$*"
}
# Append to variable $1 as a newline delimited list (with a newline at the end)
# which needs not exists
appendnl()
{
local var="$1" nl='
'
shift
eval "$var=\${$var:+\$$var$nl}\$(printf '%s\n' \"\$@\")"
}
# Return its last argument
lastarg()
{
eval pecho "\${$#}"
}
# Check that type $1 is respected for the remaining argument variables
typecheck()
{
local type="$1" var= val= success=true
shift
for var
do
match "$var" '[0-9]+' && die "positional parameters can't be used with typecheck"
val=$(value "$var") || exit 1
case "$type" in
bool)
[ "$val" != true ] && [ "$val" != false ] && {
pecho "$var is of type bool, must contain \"true\" or \"false\", not \"$val\"" >&2
success=false
}
;;
int)
! match "$val" '[+-]?[0-9]+' && {
pecho "$var is of type int, must contain an integer, not \"$val\"" >&2
success=false
}
;;
uint)
! match "$val" '\+?[0-9]+' && {
pecho "$var is of type uint, must contain a positive integer, not \"$val\"" >&2
success=false
}
;;
float)
{ ! match "$val" '[+-]?[0-9]*\.[0-9]*' || match "$val" '[+-]?\.'; } && {
pecho "$var is of type float, must contain a floating point value, not \"$val\"" >&2
success=false
}
;;
*)
die "$type: unknown type"
;;
esac
done
$success
}
# Like atexit(3), a way to stack EXIT traps
# Caution: overwrite the traps for terminating signals
atexit()
{
if [ "${_atexit_scripts:-}" ]
then
_atexit_scripts="$_atexit_scripts; $1"
else
_atexit_scripts=$1
trap 'exit 1' HUP INT QUIT ABRT ALRM TERM
trap 'eval "$_atexit_scripts"' EXIT
fi
}
# Returns 0 if $1 matches $2 (as an ERE), 1 otherwise
match()
{
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()
{
local testarg="$1" i=
shift
for i
do
[ ! -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";;
esac
fi
done
}
# For each argument, check for executable presence; use a|b|c... for alternatives
requirebin()
{
local i= j= success=
for i
do
if match "$i" '.*|.*'
then
i=$(pecho "$i" | tr '|' ' ')
success=false
for j in $i
do
if command -v "$j" >/dev/null
then
success=true
fi
done
if ! $success
then
pecho "At least one executable amongst `$i` required"
return 1
fi
elif ! command -v "$i" >/dev/null
then
pecho "$i: executable not found"
return 1
fi
done
}
# Die with an appropriate message if one of the argument paths exists
forbidfile()
{
local i=
for i
do
if [ -L "$i" ] || [ -e "$i" ]
then
die "$i: file already exists"
fi
done
}
# Output all arguments separated by '\n'. If there is no argument or the only
# argument is '-', output stdin instead
readargs()
{
if [ $# -eq 0 ] || ([ $# -eq 1 ] && [ "$1" = "-" ])
then
cat
else
printf '%s\n' "$@"
fi
}
# Quote all arguments
quote()
{
pecho "$1" | sed "s#'#'\\\\''#g; s#^#'#; s#\$#'#"
}
# Double quote $1 $2 times (defaults to 1)
dquote()
{
local res="$1" cnt="${2:-1}"
while [ $cnt -ne 0 ]
do
res=$(pecho "$res" | sed 's#"#\\"#g; s#^#"#; s#$#"#')
cnt=$((cnt - 1))
done
pecho "$res"
}
# Escape all BRE metacharacters in $1
bre_escape()
{
pecho "$1" | sed 's#[[^$*.\\-]#\\&#g'
}
# Escape all BRE metacharacters in $1
ere_escape()
{
pecho "$1" | sed 's#[()|{}+?[^$*.\\-]#\\&#g'
}
# Escape all sed substitute metacharacters in $1
sed_repl_escape()
{
pecho "$1" | sed 's#[\\&]#\\&#g'
}
# Escape all globbing metacharacters in $1
glob_escape()
{
pecho "$1" | sed 's#[[*?]#\\&#g'
}
# List the absolute path of all files in $1 separated by \n. The remaining
# arguments are passed to find.
listfiles()
{
local dir="$1"
shift
find -- "$dir" \( ! -path "$(glob_escape "$dir")" -prune \) "$@"
}
# Portability: GNU, *BSD, MacOS, Illumos
# +----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | cmd/OS | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 |
# +----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | xargs -0 | o | o | o | o | o | o | o | | | | |
# +----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# xargs -d'\n' emulation
xargsnl()
{
tr '\n' '\000' | xargs -0 "$@"
}
# Convert all arguments to lowercase
tolower()
{
readargs "$@" | tr '[:upper:]' '[:lower:]'
}
# Portable head -n-val
headneg()
{
! match "$1" '[[:digit:]]+' && return 1
awk -varg="$1" '{if(NR > arg) print buf[NR % arg]; buf[NR % arg] = $0}'
}
# 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)
set -e
fi
sed -E \
-e ":a; s#([^_]|^)__(([^_]*(_[^_])*[^_]*)+)__([^_]|\$)#\1$smul\2$rmul\5#; ta" \
-e ":b; s#([^*]|^)\*\*(([^\*]*(\*[^*])*[^*]*)+)\*\*([^*]|\$)#\1$bold\2$sgr0\5#; tb"
}
# Extract the extension of a filename (only works for the last extension,
# e.g. "foo.tar.gz" returns "gz")
file_ext()
{
local i=
readargs "$@" | while IFS= read -r i
do
pecho "$i" | sed -n 's#.*[^/]\.\([^.]*\)$#\1#p'
done
}
# Print the mimetype of $1
mimetype()
{
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 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
read_password()
{
printf '%s ' "$1"
stty -echo
read -r "$2"
stty echo
echo
}
# Takes argv ("$@") as argument and determines if the help option is called
is_help()
{
[ $# -eq 1 ] && ([ "$1" = "-h" ] || [ "$1" = "--help" ])
}
# Print stdin lines as a pretty list: "line1, line2, ..."
list_join()
{
local sep="${1:-, }"
sed ':a; N; $!ba; s#\n#'"$(sed_repl_escape "$sep")"'#g'
}
# Execute $@ with sh then outputs the number of seconds it took
ptime()
{
command time -p sh -c '{ "$@";} 2>&3 >/dev/null' dummy "$@" 3>&2 2>&1 | \
sed -n 's#^real ##p'
}
# Prints a random unsigned number of $1 bytes
rand()
{
requirefile -c /dev/urandom
od -An -N$1 -t u$1 /dev/urandom | tr -d '[:blank:]'
}
# IFS split $1 into the variables $2, $3, ...
read_split()
{
local str="$1"
shift
read -r "$@" <<-EOF
$str
EOF
}
# Something a bit like select. The selection is read on stdin and the result is
# printed to stdout
pselect()
{
local choice= in="$(cat)"
local len="$(pecho "$in" | wc -l)"
while true
do
pecho "$in" | awk '{printf "%d) %s\n", NR, $0}' >&2
printf '\n#?' >&2
read -r choice </dev/tty
if ! match "$choice" '[0-9]+' || [ $choice -gt $len ]
then
printf '%s: invalid value\n' "$choice" >&2
continue
fi
pecho "$in" | sed -n "$choice"'{p; q}'
break
done
}
# GNU parallel handling nested instances
parallel()
{
if [ "${PARALLEL_PID:-}" ]
then
command env PARALLEL=$(pecho "$PARALLEL" | sed 's#--eta##g') \
parallel -j1 "$@"
else
command parallel "$@"
fi
}
# Like the usual tac(1), print lines in reverse
ptac()
{
awk '{line[NR] = $0} END {for (i = NR; i; --i) {print line[i]}}' "$@"
}
# Recursively delete empty directories
rmdir_recursive()
{
local i=
for i
do
find -- "$i" -type d | awk -F/ '{print NF, $0}' | sort -k1 -n | rev | \
cut -d' ' -f2- | xargsnl rmdir --
done
}
# Simple portable seq without fancy options (use list_join to get -s)
pseq()
{
local incr=1 first=1
case $# in
1) last=$1;;
2)
first=$1
last=$2
;;
3)
first=$1
incr=$2
last=$3
;;
esac
while [ $first -le $last ]
do
pecho $first
first=$((first + incr))
done
}
# URL decode input or arguments
urldecode()
{
readargs "$@" | sed 's#+# #g; s#%#\\x#g' | xargsnl printf '%b\n'
}
# Portable to Linux, *BSD and Mac
numcpu()
{
getconf _NPROCESSORS_ONLN
}
# 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
# Portable mktemp implementing only the -p option
pmktemp()
{
case $# in
0) pecho "mkstemp(${TMPDIR:-/tmp}/tmp.XXXXXX)" | m4;;
1) pecho "mkstemp($1)" | m4;;
3)
! [ "$1" == -p ] && { pecho "Usage: pmktemp [-p TMPDIR] [TEMPLATE]" >&2; return 1; }
pecho "mkstemp($2/$3)" | m4
;;
*) return 1;;
esac
}
##################################
# posix-build specific functions #
##################################
# Like rm, but echo what's going on too
rmv()
{
pecho rm "$@"
rm "$@"
}
# Call before rewrite_paths() to setup said paths
set_rpaths()
{
local i= key= val=
_rpath_script=$(pmktemp)
atexit 'rm -- "$_rpath_script"'
for i
do
key=$(bre_escape "rpath_${i%%=*}")
val=${i#*=}
val=$(sed_repl_escape "$(quote "${val%/}")")
pecho "s#^$key=.*#$key=$val#"
done >"$_rpath_script"
}
# Copy $1 to $2 while rewriting runtime paths of the form ^rpath_*=val$
cp_rewrite_rpaths()
{
! [ "${_rpath_script:-}" ] &&
die "set_rpaths must be called before cp_rewrite_rpaths"
sed -f "$_rpath_script" -- "$1" >"$2"
}
# Fetch URL $1 into path $2 (optional)
pb_fetch()
{
if command -v curl >/dev/null
then
curl -OL ${2:+-o "$2"} -- "$1"
elif command -v wget >/dev/null
then
wget ${2:+-O "$2"} -- "$1"
elif command -v fetch >/dev/null
then
fetch ${2:+-o "$2"} "$1"
else
pecho "Need curl, wget or fetch!" >&2
return 1
fi
}
pb_install_usage()
{
cat <<EOF
SYNOPSIS
pb_install [-qr] [-m MODE] DIRECTORY/ FILE...
pb_install [-qr] [-m MODE] DESTFILE FILE
DESCRIPTION
Copy the FILEs to their destination (DIRECTORY with the important / at the
end or DESTFILE for single FILEs).
OPTIONS
-m MODE
Run chmod MODE on the copied FILEs.
-r
Use cp_rewrite_rpaths instead of cp(1).
-q
Quiet mode.
EOF
exit 1
}
# See above
pb_install()
{
local mode= rpath=false quiet=false dest= destdir= i=
while getopts "m:rq" OPT
do
case "$OPT" in
m) mode=$OPTARG;;
r) rpath=true;;
q) quiet=true;;
\?) pb_install_usage;;
esac
done
shift $((OPTIND - 1))
[ $# -lt 2 ] && pb_install_usage
dest=$1
shift
# Check filetypes for FILE...
i=
for i
do
[ ! -L "$i" ] && [ ! -e "$i" ] && die "$i: file not found"
[ ! -f "$i" ] && die "$i: not a regular file"
done
# Create destination if needed
if match "$dest" '.*/'
then
dest=${dest%/}
mkdir -p -- "$dest"
else
[ $# -gt 1 ] && die "Target $dest must be a directory"
mkdir -p -- "$(dirname -- "$dest")"
fi
# The actual copy with rpath rewriting and chmod
if [ ! -e "$dest" ] || [ -f "$dest" ]
then
[ $# -gt 1 ] && die "Target $dest must be a directory"
! $quiet && pecho "Installing $1 to $dest"
if $rpath
then
cp_rewrite_rpaths "$1" "$dest"
else
cp -- "$1" "$dest"
fi
[ "$mode" ] && chmod "$mode" -- "$dest"
elif [ -d "$dest" ]
then
destdir=$dest
for i
do
dest=$destdir/$(basename -- "$i")
! $quiet && pecho "Installing $i into $dest"
if $rpath
then
cp_rewrite_rpaths "$i" "$dest"
else
cp -- "$i" "$dest"
fi
[ "$mode" ] && chmod "$mode" -- "$dest"
done
fi
}
# remove files and empty directories leftover
uninstall()
{
local i=
for i
do
rm -- "$i"
set +e
rmdir -p -- "$(dirname -- "$i")" 2>/dev/null
set -e
done
}
# Get the git version with or without git installed. $1 is the repository
# root directory
git_version()
{
local repo_root=${1%/} status= ref=
if command -v git >/dev/null
then
set +e
git rev-parse --verify --quiet HEAD
status=$?
set -e
else
if [ ! -f "$repo_root"/.git/HEAD ]
then
status=1
else
ref=$(cut -d' ' -f2 "$repo_root"/.git/HEAD)
if [ ! -f "$ref" ]
then
status=1
else
status=0
cat -- "$1"/.git/"$ref"
fi
fi
fi
if [ $status -ne 0 ]
then
pecho "No git version available"
fi
}
# General build.sh usage
pb_usage()
{
cat <<EOF
SYNOPSIS
$(basename -- "$0") [ACTION]
PGO=true $(basename -- "$0") CMD [ARGS]
DESCRIPTION
Build script getting its configuration from the environment and accepting
the following ACTIONs:
* clean
* install
* uninstall
* help
When no ACTION is given, the program is built.
When PGO is selected through the environment, CMD [ARGS] is run to profile
the program before rebuilding.
This notice is generic, read the README for possibly more.
ENVIRONMENT
CC
C compiler to use. Defaults to c99(1p).
LD
Linker passed to CC via -fuse-ld if this option is available.
CFLAGS
Additional flags passed to CC when compiling sources files into
objects.
LDFLAGS
Additional flags passed to CC when linking the objects into the final
executable.
PREFIX and DESTDIR
The program and its files are installed into or uninstalled from
"\$DESTDIR\$PREFIX/". PREFIX defaults to "/usr/local" and DESTDIR to
empty.
MAKE
Executable to use as make(1p). Defaults to "make".
JOBS
If some steps in the compilation support parallel processing, choose
the number of parallel jobs to run. Defaults to 1.
LTO
Try to use link-time optimization when value is "true". Defaults to
"false".
PGO
Try to use profile-guided optimization using the arguments as a command
to run to profile the program when value is "true". Defaults to "false".
STATIC
Try to link statically when value is "true". Defaults to "false".
NATIVE
Try to enable optimizations specific to the host CPU when value is
"true". Defaults to "false".
CONFIG
Choose a flag preset corresponding to a build type. Available values
are "release", "debug" and "profile". Defaults to "release".
EOF
exit $1
}
# Convert a make compatible source list (no whitespaces, space delimited) to
# its object counterpart
src2obj()
{
pecho "${1:-$SRC}" | sed 's#\.c\([[:blank:]]*\)#.o\1#g' | paste -sd' '
}
# C build.sh cleanup function
# Also remove "$@"
cclean()
{
local i=
cd -- "$(dirname -- "$BIN")"
listfiles . -name '*.gperf' -o -name '*.lex' | \
sed 's#\.lex$#.c#; s#\.gperf$#.c#' | while IFS= read -r i
do
rmv -f -- "${i#./}"
done
rmv -f -- "$(basename -- "$BIN")" $(src2obj) *.profraw default.profdata *.gcda
if [ $# -ne 0 ]
then
rmv -f -- "$@"
fi
cd - >/dev/null
}
# Compiler flag testing functions
cctest_setup()
{
_cctest_dir=$(mktemp -d)
atexit 'rm -r -- "$_cctest_dir"'
pecho 'int main(void){}' >"$_cctest_dir"/a.c
"$CC" -c -o "$_cctest_dir"/a.o "$_cctest_dir"/a.c
}
test_cflag()
{
! [ "${_cctest_dir:-}" ] && cctest_setup
"$CC" -c -o /dev/null "$@" "$_cctest_dir"/a.c >/dev/null 2>&1
}
xtest_cflag()
{
test_cflag "$@" || die "$CC doesn't support $*"
}
append_cflag()
{
append CFLAGS "$@"
}
test_append_cflag()
{
test_cflag "$@" && append_cflag "$@"
}
xtest_append_cflag()
{
test_append_cflag "$@" || die "$CC doesn't support $*"
}
test_ldflag()
{
! [ "${_cctest_dir:-}" ] && cctest_setup
"$CC" -o /dev/null "$@" "$_cctest_dir"/a.o >/dev/null 2>&1
}
xtest_ldflag()
{
test_ldflag "$@" || die "$CC doesn't support $*"
}
append_ldflag()
{
append LDFLAGS "$@"
}
test_append_ldflag()
{
test_ldflag "$@" && append_ldflag "$@"
}
xtest_append_ldflag()
{
test_append_ldflag "$@" || die "$CC doesn't support $*"
}
append_cppflag()
{
append CPPFLAGS "$@"
}
# Make wrapper for C projects, only mandatory variables are SRC as
# whitespace/newline delimited list of source files, BIN as
# output binary name and CC as compiler name
#
# If JOBS is set to something other than 1, the -j make option will be used
# If $1 is -, stdin is appended to the makefile content
# $@ (except $1 if it is -) is passed as arguments to make
pb_make()
{
local append_stdin=false
if [ $# -ge 1 ] && [ "$1" = - ]
then
append_stdin=true
shift
fi
{ cat <<'EOF'; $append_stdin && cat; } | \
"${MAKE:-make}" $([ "${JOBS:-}" -ne 1 ] && pecho -j$JOBS) -f - \
SRC="$SRC" OBJ="$(src2obj)" CC="$CC" BIN="$BIN" CFLAGS="${CFLAGS:-}" \
CPPFLAGS="${CPPFLAGS:-}" LDFLAGS="${LDFLAGS:-}" LDLIBS="${LDLIBS:-}" "$@"
.POSIX:
.SUFFIXES:
.SUFFIXES: .c .o .gperf .lex
.gperf.c:
gperf -- $< >$@
.lex.c:
$(LEX) $(LFLAGS) -t -- $< >$@
.c.o:
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<
$(BIN): $(OBJ)
$(CC) $(LDFLAGS) $(OBJ) -o $@ $(LDLIBS)
EOF
}