Skip to content

Instantly share code, notes, and snippets.

@aayla-secura
Last active June 11, 2021 00:45
Show Gist options
  • Save aayla-secura/6fc8362755ae2b824bc99679530a3e78 to your computer and use it in GitHub Desktop.
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
#!/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