Last active
June 11, 2021 00:45
-
-
Save aayla-secura/6fc8362755ae2b824bc99679530a3e78 to your computer and use it in GitHub Desktop.
Functions to do useful stuff in a restricted bash shell; Uses only bash built-ins
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Uses only bash built-ins allowed in restricted mode | |
# Also includes a few functions that require some external commands, see | |
# FUNCTIONS THAT REQUIRE SOME EXTERNAL COMMANDS at the end | |
# TODO check for # of arguments; or an argument parser | |
function _echoarray { | |
# print array elements one per line | |
local IFS=$'\n' | |
echo "$*" | |
} | |
function _require_int { | |
# ensure i is an integer | |
local i="${1}" msg="${2}" | |
if [[ ! "${i}" =~ ^[0-9]+$ ]] ; then | |
[[ -n "${msg}" ]] && echo "${msg} must be an integer" >&2 | |
return 1 | |
fi | |
} | |
function strip { | |
local str c=" " side="lr" force=0 | |
while [[ $# -gt 0 ]] ; do | |
case "${1}" in | |
-f) | |
# always strip, even if this results in an empty string | |
force=1 | |
;; | |
-r) | |
# strip from the right only | |
side=r | |
;; | |
-l) | |
# strip from the left only | |
side=l | |
;; | |
-c) | |
# characters to strip | |
c="${2}" | |
shift | |
;; | |
*) | |
str="${1}" | |
;; | |
esac | |
shift | |
done | |
if [[ ${force} -eq 0 && -z "${str//${c}}" ]] ; then | |
# all charaters would be stripped, but -f not given | |
echo -n "${str}" | |
return | |
fi | |
if [[ "${side}" == *l* ]] ; then | |
str="$(_lstrip "${str}" "${c}")" | |
fi | |
if [[ "${side}" == *r* ]] ; then | |
str="$(_rstrip "${str}" "${c}")" | |
fi | |
echo -n "${str}" | |
} | |
function _lstrip { | |
local str="${1}" c="${2}" | |
while [[ "${str:0:1}" == "${c}" ]] ; do | |
str="${str#${c}}" | |
done | |
echo -n "${str}" | |
} | |
function _rstrip { | |
local str="${1}" c="${2}" | |
# note the space before -1, needed if using negative offsets | |
while [[ "${str: -1:1}" == "${c}" ]] ; do | |
str="${str%${c}}" | |
done | |
echo -n "${str}" | |
} | |
function rand { | |
_rand "${1}" | |
} | |
function urand { | |
_rand "${1}" "u" | |
} | |
function _rand { | |
# print a random hex string of length 2*l (entropy l) | |
local l="${1:-1}" pref="${2}" result c x | |
_require_int "${l}" "argument to ${pref}rand" || return 1 | |
while [[ ${#result} -lt $(( l * 2 )) ]] ; do | |
read -n1 -r c | |
# some multi-byte sequences result in more than 2 hex characters, so save | |
# this to a var and take only the first two hex digits | |
# the ' is needed to interpret it as an ASCII character | |
x=$(printf '%02x' "'${c}") | |
result+=${x:0:2} | |
done < "/dev/${pref}random" | |
echo -n "${result}" | |
} | |
function timedelta { | |
# return time elapsed since ref; ref should be a previous value returned by this function when NO argument was given | |
# (could have been uptime or system time, so not to be used as absolute value) | |
local ref="${1:-0}" rtcfile=/sys/class/rtc/rtc0/since_epoch t tmp | |
_require_int "${ref}" "argument to timedelta" || return 1 | |
if [[ ! -f "${rtcfile}" ]] ; then | |
rtcfile=/proc/uptime | |
fi | |
if [[ ! -f "${rtcfile}" ]] ; then | |
return 1 | |
fi | |
IFS=. read t tmp < "${rtcfile}" | |
echo $(( t - ref )) | |
} | |
function sleep { | |
local s="${1}" | |
_require_int "${s}" "argument to sleep" || return 1 | |
_sleep_reliable "${s}" || _sleep_read "${s}" | |
} | |
function _sleep_reliable { | |
local s="${1}" stime elapsed=0 | |
stime=$(timedelta) | |
[[ $? -eq 0 ]] || return 1 | |
while [[ ${elapsed} -lt ${s} ]] ; do | |
elapsed=$(timedelta "${stime}") | |
done | |
} | |
function _sleep_read { | |
# will exit early if user presses Enter | |
local s="${1}" rc | |
read -t "${s}" | |
rc=$? | |
if [[ ${rc} -gt 128 ]] ; then | |
return 0 # it timed out, so we managed to sleep for s seconds | |
else | |
return 1 # it didn't time out, user must have entered something | |
fi | |
} | |
function listening_ports { | |
# list listening ports | |
# TODO also list established TCP connections | |
local proto l i sl laddr raddr st ip port fmt='%-10s%-18s%s\n' | |
local -a protocols=("$@") | |
[[ ${#protocols[@]} -eq 0 ]] && protocols=(tcp udp) | |
printf "${fmt}" "PROTOCOL" "IP ADDRESS" "PORT" | |
for proto in "${protocols[@]}" ; do | |
while read sl laddr raddr st _ ; do | |
[[ "${sl}" == "sl" ]] && continue # header | |
[[ "${proto}" == tcp && ${st} != 0A ]] && continue # not listening | |
ip= | |
for i in 6 4 2 0 ; do | |
ip+=$(( 16#${laddr:${i}:2} )) | |
[[ ${i} -gt 0 ]] && ip+=. | |
done | |
port=$(( 16#${laddr:9} )) | |
printf "${fmt}" "${proto}" "${ip}" "${port}" | |
done < /proc/net/"${proto}" | |
done | |
} | |
function lsdir { | |
# list directory contents | |
# TODO include permissions, size, etc in the listing, like ls -l | |
local dir recurse=0 filter_i filter_e e | |
local -a entries dirs | |
while [[ $# -gt 0 ]] ; do | |
case "${1}" in | |
-r) | |
recurse=1 | |
;; | |
-i) | |
# include only matching regex | |
filter_i="${2}" | |
shift | |
;; | |
-e) | |
# exclude matching regex | |
filter_e="${2}" | |
shift | |
;; | |
*) | |
dirs+=("${1}") | |
;; | |
esac | |
shift | |
done | |
trap "$(shopt -p nullglob; shopt -p globstar; shopt -p dotglob)" RETURN | |
shopt -s nullglob | |
shopt -s globstar | |
shopt -s dotglob | |
[[ ${#dirs[@]} -eq 0 ]] && dirs=(${PWD}) | |
for dir in "${dirs[@]}" ; do | |
if [[ ! -d "${dir}" ]] ; then | |
echo "${dir} not a directory" >&2 | |
return 1 | |
fi | |
if [[ ${recurse} -eq 1 ]] ; then | |
entries=("${dir}"/**) | |
else | |
entries=("${dir}"/*) | |
fi | |
for e in "${entries[@]}" ; do | |
[[ -n "${filter_i}" && ! "${e}" =~ ${filter_i} ]] && continue | |
[[ -n "${filter_e}" && "${e}" =~ ${filter_e} ]] && continue | |
echo "${e}" | |
done | |
done | |
} | |
function readfile { | |
# cat or grep a file | |
local file filter_i filter_e max_l | |
local -a files | |
while [[ $# -gt 0 ]] ; do | |
case "${1}" in | |
-i) | |
# include only matching lines | |
filter_i="${2}" | |
shift | |
;; | |
-e) | |
# exclude matching lines | |
filter_e="${2}" | |
shift | |
;; | |
-m) | |
_require_int "${2}" "argument to -m" || return 1 | |
max_l="${2}" | |
shift | |
;; | |
*) | |
files+=("${1}") | |
;; | |
esac | |
shift | |
done | |
[[ ${#files[@]} -eq 0 ]] && files=(/dev/stdin) | |
for file in "${files[@]}" ; do | |
if [[ ${#files[@]} -gt 1 ]] ; then | |
echo "~~~~~~~~~~ ${file} ~~~~~~~~~~" | |
fi | |
if [[ -d "${file}" ]] ; then | |
echo "${file} is a directory" >&2 | |
return 1 | |
fi | |
while IFS= read line ; do | |
[[ -n "${filter_i}" && ! "${line}" =~ ${filter_i} ]] && continue | |
[[ -n "${filter_e}" && "${line}" =~ ${filter_e} ]] && continue | |
if [[ -n ${max_l} && ${#line} -gt ${max_l} ]] ; then | |
echo "${line:0:${max_l}}..." | |
else | |
echo "${line}" | |
fi | |
done < "${file}" | |
done | |
} | |
function wfile { | |
# recent bash versions have fixed this loophole in history | |
# and only files in the current directory can be written to | |
local file="${1}" disable_hist=0 | |
if [[ -z "${file}" ]] ; then | |
echo "Filename required" >&2 | |
return 1 | |
fi | |
if [[ -d "${file}" ]] ; then | |
echo "${file} is a directory" >&2 | |
return 1 | |
fi | |
if [[ "${SHELLOPTS}" != *history* ]] ; then | |
set -o history | |
disable_hist=1 | |
fi | |
# save history to file and clear it | |
# will fail unless HISTFILE is in the current dir | |
history -a "${HISTFILE}" | |
# delete everything up to the current command (using -c to clear everything | |
# will render the saving of lines below using -s useless) | |
[[ $HISTCMD -gt 1 ]] && history -d 1-$(( HISTCMD - 1 )) | |
echo "Enter input; press Ctrl-D when done" | |
echo | |
while IFS= read line ; do | |
history -s "${line}" | |
done | |
# TODO we can preserve permissions by using -a instead of -w but how to clear | |
# the contents of the file first? | |
history -w "${file}" | |
# reload history | |
history -c | |
# will fail unless HISTFILE is in the current dir | |
history -r "${HISTFILE}" | |
# restore options | |
[[ ${disable_hist} -eq 1 ]] && set +o history | |
} | |
function filemode { | |
# return the file mode in abbreviated or octal form | |
local file="${1}" mode | |
if [[ ! -e "${file}" ]] ; then | |
echo "No such file or directory ${file}" >&2 | |
return 1 | |
fi | |
# file type | |
if [[ -L "${file}" ]] ; then | |
mode+=l | |
elif [[ -b "${file}" ]] ; then | |
mode+=b | |
elif [[ -c "${file}" ]] ; then | |
mode+=c | |
elif [[ -d "${file}" ]] ; then | |
mode+=d | |
elif [[ -f "${file}" ]] ; then | |
mode+=- | |
else | |
echo "Can't determine file type. Bug?" >&2 | |
mode+=? | |
fi | |
if [[ -r "${file}" ]] ; then | |
mode+=x | |
fi | |
} | |
function filesize { | |
# TODO | |
: | |
} | |
function filemtime { | |
# TODO | |
: | |
} | |
function dirname { | |
# directory part of path or . if none | |
local path result | |
path="$(__dir_or_basename_process_args "${@}")" | |
if [[ $? -ne 0 ]] ; then | |
return 1 | |
fi | |
result="${path%/*}" | |
if [[ "${path}" == "${result}" ]] ; then | |
result=. | |
elif [[ -z "${result}" ]] ; then | |
result=/ | |
fi | |
result="$(strip -r -c / "${result}")" | |
echo -n "${result}" | |
} | |
function basename { | |
# filename or last segment of path | |
local path result | |
path="$(__dir_or_basename_process_args -f "${@}")" | |
if [[ $? -ne 0 ]] ; then | |
return 1 | |
fi | |
if [[ -z "${path}" ]] ; then | |
result=/ | |
else | |
result="${path##*/}" | |
fi | |
echo -n "${result}" | |
} | |
function __dir_or_basename_process_args { | |
local path strip_slash=1 | |
local -a strip_args | |
while [[ $# -gt 0 ]] ; do | |
case "${1}" in | |
-s) | |
# preserve trailing slash, i.e. /foo -> / and /foo/ -> /foo | |
# the UNIX command dirname ignores trailing slashes, i.e. /foo and | |
# /foo/ both return / | |
strip_slash=0 | |
;; | |
-f) | |
strip_args+=(-f) | |
;; | |
*) | |
path="${1}" | |
;; | |
esac | |
shift | |
done | |
if [[ -z "${path}" ]] ; then | |
echo "Specify path" >&2 | |
return 1 | |
fi | |
if [[ ${strip_slash} -eq 1 ]] ; then | |
path="$(strip "${strip_args[@]}" -r -c / "${path}")" | |
fi | |
echo -n "${path}" | |
} | |
function realpath { | |
# TODO | |
: | |
} | |
function nc { | |
# TODO | |
: | |
} | |
function curl { | |
# TODO | |
: | |
} | |
################################################################### | |
########## FUNCTIONS THAT REQUIRE SOME EXTERNAL COMMANDS ########## | |
################################################################### | |
function copyperms { | |
# sets the mode of file to be that of ref (/bin/ls by default) | |
# useful if chmod is not available, but a tool that can copy a file and | |
# preserve permissions is | |
# if file does not exist, then a blank file with the given mode is created | |
# REQUIRES | |
# - cp | |
# TODO | |
# - add support for rsync, tar or other tools that can do this instead of cp | |
local file="${1}" ref="${2:-/bin/ls}" | |
local bkpfile="/tmp/${file}.$(urand 16).bak" | |
if [[ -d "${file}" ]] ; then | |
echo "No such file or directory ${file}" >&2 | |
return 1 | |
fi | |
if [[ -e "${file}" ]] ; then | |
# backup | |
cp "${file}" "${bkpfile}" | |
fi | |
# copy an executable file to the target | |
cp --preserve=mode "${ref}" "${file}" | |
if [[ -e "${bkpfile}" ]] ; then | |
# restore | |
cp --no-preserve=mode "${bkpfile}" "${file}" | |
else | |
# or empty its contents | |
cp --no-preserve=mode /dev/null "${file}" | |
fi | |
# attempts to remove backup, may fail | |
rm -f "${bkpfile}" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment