#! /bin/bash source_me_please # The above shbang is intended to cause an error if this script is executed, # while still satisfying editors that use the shbang line to determine the # file type. # shopt -s extglob # password <varname>: prompt for and read a password into varname. password () { [[ -t 0 ]] || { echo "$FUNCNAME: stdin is not a tty"; return 2; } # We use the var=$(foo) && eval construct so that we can run foo # in a subshell but still get data back from it. # Since this variable would clash with the user-supplied variable # name in the eval, we pick something unlikely to be used. local __passwd_str=$( local ch='' str='' # Restore echo even if we die trap 'stty echo' EXIT # Just read -s is not enough, because characters might # arrive between calls to read; we must keep echo disabled # the whole time. stty -echo printf "Password: " >&2 while IFS='' read -r -s -n 1 ch || { exit 1; } # Is the character not a newline? [[ "$ch" ]] do str="$str$ch" printf "*" >&2 done printf "%s" "$str" echo >&2 ) && eval "${1-REPLY}="'$__passwd_str'; } # quote [ARGS...] - safely quote arguments for the shell. # # Each argument is printed, enclosed in single quotes and with embedded # single quotes appropriately escaped. Output is a space-separated list # of the quoted strings, followed by a newline. quote () { local arg fixed declare -i i=0 for arg; do ((i++)) && printf " "; # This expansion is misparsed inside double quotes :( fixed=${arg//"'"/"'\''"} printf "'%s'" "$fixed" done printf "\n" } # Like ssh, except arguments after '--' are not expanded on the remote end. # Assumes the remote shell can handle arbitrary strings inside single quotes # (except, of course, single quotes). Note that, unlike with ssh, the # hostname *must* precede the '--'. scmd () { local arg quotehence ssh_args=() cmd=() for arg do if [[ $arg = -- ]]; then quotehence=1 elif [[ $quotehence ]]; then cmd+=("$arg") else ssh_args+=("$arg") fi done ssh "${ssh_args[@]}" "$(quote "${cmd[@]}")" } # POSIX dirname(1) without fork/exec. # # "Step N" below refers to the steps in the POSIX specification for dirname: # http://www.opengroup.org/onlinepubs/009695399/utilities/dirname.html dirname () { # // is not treated specially by this implementation, so steps # 1 and 6 are ignored. # Ignore a first argument of '--' [[ $1 = -- ]] && shift # Detect incorrect number of arguments. We use string equality # instead of numeric for a slight speedup. if [[ $# != 1 ]]; then # Must use numeric comparison here if [[ $# -lt 1 ]]; then printf "%s: missing operand\n" "dirname" >&2 else printf "%s: extra operand '%s'\n" "dirname" "$2" >&2 fi return 1 fi # Handle empty string immediately. The next step converts all-slash # strings to the empty string, but we need to be able to distinguish # between the two. if [[ ! "$1" ]]; then echo . return fi # Remove trailing slashes (step 3). If the string was all slashes, # this gives us "" and our ${fn:-/} yields /. Thus steps 1 and 2 are # handled as well. local fn=${1%%+(/)} if [[ "$fn" = +([!/]) ]]; then # No slashes, and the string hadn't been all-slashes. It's # in the current directory (step 4). fn=. else # Remove trailing non-slashes and the slashes that precede them # (steps 5, 7). The string might actually be empty. fn=${fn%%+(/)*([!/])} fi # convert empty string to "/" (step 8). printf "%s\n" "${fn:-/}" } # POSIX basename(1) without fork/exec. # # "Step N" below refers to the steps in the POSIX specification for basename: # http://www.opengroup.org/onlinepubs/009695399/utilities/basename.html basename () { # Ignore a first argument of '--' [[ $1 = -- ]] && shift if [[ $# -lt 1 || $# -gt 2 ]]; then # Test again; this way we don't have to do two separate # comparisons in the fast case. if [[ $# -lt 1 ]]; then printf "%s: missing operand\n" "basename" >&2 return 1 elif [[ $# -gt 2 ]]; then printf "%s: extra operand '%s'\n" "basename" "$3" >&2 return 1 fi fi # Handle empty string immediately (step 1). if [[ ! "$1" ]]; then echo . return fi # All slashes (step 3). if [[ "$1" = +(/) ]]; then echo / return fi # Remove trailing slashes (step 4) local fn=${1%%+(/)} # Remove everything up to the last remaining slash (step 5) fn=${fn##*/} # Remove the suffix [[ "$fn" != "$2" ]] && fn=${fn%"$2"} printf "%s\n" "$fn" } # pid_is_terminal PID - Test whether a PID is a terminal emulator. # # Return success if PID belongs to a terminal emulator program, failure if # it does not. This test is not foolproof. pid_is_terminal() { local proc="$(ps -o comm= -p "$1")" [[ $proc = *term* || $proc = rxvt* || $proc = screen ]] } # dir_access_color [dir] - color based on directory permissions # # Output a terminal color code based on one's access rights to the named # directory. If no directory is specified, $PWD is used. Colors are: # black: the directory does not exist # blue: the directory exists but we have no permissions on it # magenta: the directory is executable but not readable or writable # red: the directory is readable, but not writable # green: the directory is writable # # The bold version of each color is used, so that, with most terminal # emulators, nonexistent directories yield dark grey, not black. dir_access_color() { local dir="${1-$PWD}" color=0 # black (dk grey) [[ -e "$dir" ]] && color=4 # blue [[ -x "$dir" ]] && color=5 # magenta [[ -r "$dir" ]] && color=1 # red [[ -w "$dir" ]] && color=2 # green tput -S <<-EOT bold setaf $color EOT } # nfm_xterm_title - Set the title of an xterm to something informative. nfm_xterm_title() { local status="$?" local host="${HOSTNAME%%.*}" local dir="$(dirs +0)" dir="${dir%/}/" local histline=$(HISTTIMEFORMAT='' history 1) histline=${histline#* } # gnome-terminal doesn't mind, but xterm refuses to set the title if # it contains non-ASCII characters. So, use x instead of ×. local TITLE="${XTTITLE-${LOGNAME}@${host}}: ${dir} [$$] ${COLUMNS}x${LINES}: $histline [$status]" local ICON="${XTTITLE-$LOGNAME}: ${dir}" TITLE=${TITLE//[[:cntrl:]]/?} ICON=${ICON//[[:cntrl:]]/?} printf "\e]2;%s\a\e]1;%s\a" "$TITLE" "$ICON" } # spin - run a command with a spinner # Runs in a subshell to localize trap. spin () ( local spinchars='-\|/' delay=1 i size usage=0 # TODO: make sure there is a tty. # TODO: option to send spinner to stderr rather than /dev/tty. # Parse arguments; sorry for the lack of getopts while (( $# )) && [[ "$1" = -* ]]; do if [[ "$1" = "-c" ]]; then spinchars=$2 shift 2 elif [[ "$1" = "-d" ]]; then # TODO: ensure that this is a valid argument to sleep delay=$2 shift 2 else echo "Invalid option: '$1'" >&2 usage=1 break fi done if (( usage )) || [[ "$#" -eq 0 ]]; then cat >&2 <<EOF Usage: $0 [-d delay] [-c spinchars] command [args] Print a spinner while command is running. spinchars is the list of characters to use for the spinner, default '-\\|/'. delay is the delay in seconds between animation frames, default 1. EOF return 125 fi # Spinner loop running in the background. Output goes directly # to the tty. for ((i=0,size=${#spinchars};; i=(i+1)%size)); do printf '%s\r' "${spinchars:i:1}" >/dev/tty sleep "$delay" done & # Clean up afterwards trap 'kill '"$!"'; echo >/dev/tty;' 0 # Run the command "$@" ) itime () { local precision=2 local date= pow t s b itime local OPTARG OPTIND opt while getopts p:wWdDh opt; do case $opt in p) let precision=OPTARG ;; w) precision=0 ;; W) precision=2 ;; d) date=yes ;; D) date= ;; h|*) cat >&2 <<-"EOF" Usage: itime [-p PREC | -w | -W | -d | -D ]... [ TIME ] Print the time TIME (default now) in Swatch Interent Time. -p PREC print to PREC digits after the decimal point -w equivalent to -p 0 -W equivalent to -p 2 -d/-D show/do not show date (default -D) EOF [[ $opt = h ]] # succeed if -h, fail otherwise return ;; esac done shift $((OPTIND - 1)) let t=$(date +%s -d "${1:-now}") let s=(t+3600)%86400 if (( precision > 0 )); then let pow=10**(1+precision) let b=(s*pow)/864 itime=@$(sed 's/[[:digit:]]\{'"$precision"'\}$/.&/' <<<"$b") else local i let pow=10**(-precision) let b=$(( (s*10)/(864*pow) )) itime=@$b for (( i=precision ; i < 0 && (i-precision < 2); i++ )); do itime+="0" done fi printf "%s" "$itime" if [[ $date ]]; then date -d "${1:-now}" +' d%d.%m.%y' else echo fi } song() { printf "%s\n\t%s\n%s %s / %s\n" \ "$(audtool current-song)" \ "$(audtool current-song-filename)" \ "$(audtool playback-status)" \ "$(audtool current-song-output-length)" \ "$(audtool current-song-length)" \ ; } ### Tests for some of the above functions. _TEST_dirname () { [[ $(dirname /) = "/" ]] || return 1 [[ $(dirname //) = /?(/) ]] || return 2 [[ $(dirname /a/b/) = "/a" ]] || return 3 [[ $(dirname //a//b//) = "//a" ]] || return 4 [[ $(dirname a) = "." ]] || return 5 [[ $(dirname "") = "." ]] || return 6 [[ $(dirname /a) = "/" ]] || return 7 [[ $(dirname /a/b) = "/a" ]] || return 8 [[ $(dirname a/b) = "a" ]] || return 9 [[ $(dirname -- /a/b) = "/a" ]] || return 10 } _TEST_basename () { [[ $(basename /) = "/" ]] || return 1 [[ $(basename //) = /?(/) ]] || return 2 [[ $(basename /a/b/) = "b" ]] || return 3 [[ $(basename //a//b//) = "b" ]] || return 4 [[ $(basename a) = "a" ]] || return 5 [[ $(basename "") = ?(.) ]] || return 6 [[ $(basename /a) = "a" ]] || return 7 [[ $(basename /a/b) = "b" ]] || return 8 [[ $(basename '/a/b/c\d') = 'c\d' ]] || return 9 [[ $(basename a/b) = "b" ]] || return 10 [[ $(basename a/ab b) = "a" ]] || return 11 [[ $(basename a/ab ab) = "ab" ]] || return 12 [[ $(basename a/ab c) = "ab" ]] || return 13 [[ $(basename a/ab "*") = "ab" ]] || return 14 [[ $(basename "a/a*" "*") = "a" ]] || return 15 [[ $(basename -- a/b) = "b" ]] || return 16 [[ $(basename -- a/bcd d) = "bc" ]] || return 17 } _TEST_quote () { local tests=( # test single- and double-quotes "foo'bar" "'foo" "''\"'\\'" "bar'" "''" # test spaces "foo bar" " baz " "fum '" " ' ' " # test other characters "'\"\$x &#{a,b}'" ) local arg declare -i i=0 for arg in "${tests[@]}"; do let i++ local evaled="$(eval "printf '%s' $(quote "$arg")")" [[ "$evaled" = "$arg" ]] || { printf "<<%s>> != <<%s>>\n" "$arg" "$evaled" return $i } done return 0 }