Skip to content

Instantly share code, notes, and snippets.

@kurahaupo
Last active November 8, 2022 16:49
Show Gist options
  • Save kurahaupo/8130030 to your computer and use it in GitHub Desktop.
Save kurahaupo/8130030 to your computer and use it in GitHub Desktop.
There's no excuse for using #!/usr/bin/env ...
#!/usr/bin/env bash
# WARNING: if this file starts with #!/usr/bin/env, please use it to install itself,
# and then use that to install other files.
#
# 1. No write permission on runnable programs
# 2. No partially-written files during installation; pivot into place once complete
# 3. No .sh etc suffices on runnable programs
# 4. Set the #! line at installation, to use a set path, not /usr/bin/env
#
verbose=0
sources=()
bindir=
declare -A interp=(
[.pl]=perl
[.py]=python
[.pyc]=python
[bash]=$BASH
[sh]=/bin/sh
)
default_lang=.bash
owner=
group=
mybin=$HOME/bin
sysbin=/usr/local/bin
match_prefix() {
local i= j= k= n=0
k=$1 ; shift
for i do [[ "$i" = "$k"* ]] && { j=$i ; ((++n)) ; } ; done
if ((n==1)) ; then printf '%s' "$j" ; else printf '%s' "$k" ; false ; fi
}
while (($#))
do case $( match_prefix "$1" --help --admin --dir --group --interpreter --owner --quiet --user --verbose --bash --perl --php --python ) in
(-h|--help) cat <<EndOfHelp ; exit 0 ;;
$0 [options | files] ...
--admin --me
--bindir PATH
--group GID
--owner UID
--verbose --quiet
EndOfHelp
(-B|--bash) interp[bash]=$2 ; shift ;;
(-H|--php) interp[php]=$2 ; shift ;;
(-P|--perl) interp[perl]=$2 ; shift ;;
(-S|--sh) interp[sh]=$2 ; shift ;;
(-Y|--python) interp[python]=$2 ; shift ;;
(-a|--admin) bindir=$sysbin ; : ${owner:=root} ${group:=root} ;;
(-b|--bindir) bindir=$2 ; shift ;;
(-g|--group) group=$2 ; shift ;;
(-i|--interpreter) interp[${2##*/}]=$2 ; shift ;;
(-m|--me) bindir=$mybin owner= group= ;;
(-o|--owner) owner=$2 ; shift ;;
(-q|--quiet) verbose=0 _v= _vv= ;;
(-v|--verbose) ((++verbose)) ; _vv=$_v _v=${_v:--}v ;;
# Don't change bundled-options boiler-plate, except for ──╮
# ╭──── the list of option letters which take args ←────╯
# ↓
(-[dgio]?*) set x "${1:0:2}" "${1:2}" "${@:2}" ;;
(-[^-]?*) set x "${1:0:2}" "-${1:2}" "${@:2}" ;;
(--*=*) set x "${1%%=*}" "${1#*=}" "${@:2}" ;;
# boiler-plate error handling
(-*) echo >&2 "Invalid option '$1'" ; exit 64 ;;
# permutable non-option args...
(*) sources+=("$1") ;;
esac
shift
done
: ${bindir:=$mybin}
for p in "${sources[@]}"
do
n=${p##*/}
r=${n%%.*}
s=${n#"$r"}
t=$bindir/$r
tt=$bindir/.$n.$$
read -r l < "$p" || break
if [[ $l = '#!'* ]]
then
# use basename of existing #! line
i=${l#'#!'}
i=${i#"${i%%[^ ]*}"}
i=${i#/usr/bin/env}
i=${i#"${i%%[^ ]*}"}
i=${i%%' '*}
i=${i##*/}
((verbose>1)) && echo "hashbang filetype $i"
else
# look up suffix
i=${interp[${s:-$default_lang}]:=${s#.}}
((verbose>1)) && echo "suffix filetype $i"
fi
# if not an absolute path, look up in $PATH (and cache result)
[[ $i = /* ]] ||
i=${interp[$i]:=$(type -p "$i")} || break
((verbose>1)) && echo "filetype path $i"
[[ $p -ef $t ]] && {
echo "Source '$p' and target '$t' are same file; won't overwrite"
continue
}
sed -e "
1{
s@^#! */[^ ]*/${i##*/} *\( [^ ].*|\)@#!$i\1@;tz;
s@^#! */usr/bin/env *${i##*/}@#!$i@;tz;
s@^@#!$i\n@;tz;
:z
}
" < "$p" > "$tt" || {
echo "Cannot create '$tt'"
continue
}
if
chmod 555 "$tt" &&
{ [[ -z $owner$group ]] ||
chown $owner:$group "$tt" ; } &&
mv -f "$tt" "$t"
then
((verbose)) && echo "Installed '$p' as '$t' with interpreter '$i'"
else
rm -f "$tt"
echo "Could not install '$p' as '$t'"
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment