Improved Autocall Script

I’ve updated the Autocall Ruby script I’ve mentioned various times before by porting it to Zsh and adding a TON of features. The following posts are now obsolete:

Autocall: A Script to Watch and “Call” Programs on a Changed Target Source File
Updated Autolily Script
Auto-update/make/compile script for Lilypond: Autolily

If you haven’t read my previous posts on this topic, basically, Autocall is a script that I wrote to automate executing an arbitrary command (from the shell) any time some file is modified. In practice, it is very useful for any small/medium project that needs to compile/build something out of source code. Typical use cases are for LilyPond files, LaTeX/XeTeX, and C/C++ source code.

Anyway, the script is now very robust and much more intelligent. It now parses options and flags instead of stupidly looking at ARGV one at a time. Perhaps the best new feature is the ability to kill processes that hang. Also, you can now specify up to 9 different commands with 9 instances of the “-c” flag (additional instances are ignored). Commands 2-9 are accessible manually via the numeric keys 2-9 (command 1 is the default). This is useful if you have, for instance, different build targets in a makefile. E.g., you could do

$ autocall -c "make" -c "make -B all" -c "make clean" -l file_list

to make things a bit easier.

I use this script all the time — mostly when working with XeTeX files or compiling source code. It works best in a situation where you have to do something X whenever files/directories Y changes in any way. Again, the command to be executed is arbitrary, so you could use it to call some script X whenever a change is detected in a file/directory. If you use it with LaTeX/XeTeX/TeX, use the “-halt-on-error” option so that you don’t have to have autocall kill it (only available with the -k flag). The copious comments should help you get started. Like all my stuff, it is not licensed at all — it’s released into the PUBLIC DOMAIN, without ANY warranties whatsoever in any jurisdiction (use at your own risk!).

#!/bin/zsh
# PROGRAM: autocall
# AUTHOR: Shinobu (https://zuttobenkyou.wordpress.com)
# LICENSE: PUBLIC DOMAIN
#
#
# DESCRIPTION:
#
# Autocall watches (1) a single file, (2) directory, and/or (3) a text file
# containing a list of files/directories, and if the watched files and/or
# directories become modified, runs the (first) given command string. Multiple
# commands can be provided (a total of 9 command strings are recognized) to
# manually execute different commands.
#
#
# USAGE:
#
# See msg("help") function below -- read that portion first!
#
#
# USER INTERACTION:
#
# Press "h" for help.
# Pressing a SPACE, ENTER, or "1" key forces execution of COMMAND immediately.
# Keys 2-9 are hotkeys to extra commands, if there are any.
# Press "c" for the command list.
# To exit autocall gracefully, press "q".
#
#
# DEFAULT SETTINGS:
#
# (-w) DELAY    = 5
# (-x) FACTOR   = 4
#
#
# EXAMPLES:
#
# Execute "pdflatex -halt-on-error report.tex" every time "report.tex" or "ch1.tex" is
# modified (if line count changes in either file; modification checked every 5
# seconds by default):
#    autocall -c "pdflatex -halt-on-error report.tex" -F report.tex -f ch1.tex
#
# Same, but only look at "ch1.tex" (useful, assuming that report.tex includes
# ch1.tex), and automatically execute every 4 seconds:
#    autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -w 1 -x 4
#       (-x 0 or -x 1 here would also work)
#
# Same, but also automatically execute every 20 (5 * 4) seconds:
#    autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -x 4
#
# Same, but automatically execute every 5 (5 * 1) seconds (-w is 5 by default):
#    autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -x 1
#
# Same, but automatically execute every 1 (1 * 1) second:
#    autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -w 1 -x 1
#
# Same, but automatically execute every 17 (1 * 17) seconds:
#    autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -w 1 -x 17
#
# Same, but for "ch1.tex", watch its byte size, not line count:
#    autocall -c "pdflatex -halt-on-error report.tex" -b ch1.tex -w 1 -x 17
#
# Same, but for "ch1.tex", watch its timestamp instead (i.e., every time
# this file is saved, the modification timestamp will be different):
#    autocall -c "pdflatex -halt-on-error report.tex" -f ch1.tex -w 1 -x 17
#
# Same, but also look at the contents of directory "images/ocean":
#    autocall -c "pdflatex -halt-on-error report.tex" -f ch1.tex -d images/ocean -w 1 -x 17
#
# Same, but also look at the contents of directory "other" recursively:
#    autocall -c "pdflatex -halt-on-error report.tex" -f ch1.tex -d images/ocean -D other -w 1 -x 17
#
# Same, but look at all files and/or directories (recursively) listed in file
# "watchlist" instead:
#    autocall -c "pdflatex -halt-on-error report.tex" -l watchlist -w 1 -x 17
#
# Same, but also look at "newfile.tex":
#    autocall -c "pdflatex -halt-on-error report.tex" -l watchlist -f newfile.tex -w 1 -x 17
#
# Same, but also allow manual execution of "make clean" with hotkey "2":
#    autocall -c "pdflatex -halt-on-error report.tex" -c "make clean" -l watchlist -f newfile.tex -w 1 -x 17
#
###############################################################################
###############################################################################

#-----------------#
# Local functions #
#-----------------#

msg () {
    case $1 in
        "help")
echo "
autocall: Usage:

autocall [OPTIONS]

Required parameter:
-c COMMAND      The command to be executed (put COMMAND in quotes). Note that
                COMMAND can be a set of multiple commands, e.g. \"make clean;
                make\". You can also specify multiple commands by invoking
                -c COMMAND multiple times -- the first 9 of these are set to
                hotkeys 1 through 9, if present. This is useful if you want to
                have a separate command that is available and can only be
                executed manually.

One or more required parameters (but see -x below):
-f FILE         File to be watched. Modification detected by time.
-F FILE         File to be watched. Modification detected by line-size.
-b FILE         File to be watched. Modification detected by bytes.
-d DIRECTORY    Directory to be watched. Modification detected by time.
-D DIRECTORY    Directory to be watched, recursively. Modification
                detected by time.
-l FILE         Text file containing a list of files/directories (each on
                its own line) to be watched (directories listed here are
                watched recursively). Modification is detected with 'ls'.

Optional parameters:
-w DELAY        Wait DELAY seconds before checking on the watched
                files/directories for modification; default 5.
-t TIMEOUT      If COMMAND does not finish execution after TIMEOUT seconds,
                send a SIGTERM signal to it (but do nothing else afterwards).
-k KDELAY       If COMMAND does not finish execution after TIMEOUT,
                then wait KDELAY seconds and send SIGKILL to it if COMMAND is
                still running. If only -k is given without -t, then -t is
                automatically set to the same value as TIMEOUT.
-x FACTOR       Automatically execute the command repeatedly every DELAY *
                FACTOR seconds, regardless of whether the watched
                files/directories were modified. If FACTOR is zero, it is set
                to 1. If -x is set, then -f, -d, and -l are not required (i.e.,
                if only the -c and -x options are specified, autocall will
                simply act as a while loop executing COMMAND every 20 (or
                more if FACTOR is greater than 1) seconds). Since the
                formula is (DELAY * FACTOR) seconds, if DELAY is 1,
                FACTOR's face value itself, if greater than 0, is the seconds
                amount.
-a              Same as \`-x 1'
-h              Show this page and exit (regardless of other parameters).
-v              Show version number and exit (regardless of other parameters).
"
            exit 0
            ;;
        "version")
            echo "autocall version 1.0"
            exit 0
            ;;
        *)
            echo "autocall: $1"
            exit 1
            ;;
    esac
}

is_number () {
    if [[ $(echo $1 | sed 's/^[0-9]\+//' | wc -c) -eq 1 ]]; then
        true
    else
        false
    fi
}

autocall_exec () {
    timeout=$2
    killdelay=$3
    col=""
    case $4 in
        1) col=$c1 ;;
        2) col=$c2 ;;
        3) col=$c3 ;;
        4) col=$c4 ;;
        5) col=$c5 ;;
        6) col=$c6 ;;
        *) col=$c1 ;;
    esac
    echo "\nautocall:$c2 [$(date --rfc-3339=ns)]$ce$col $5$ce"
    if [[ $# -eq 7 ]]; then
        diff -u0 -B -d <(echo "$6") <(echo "$7") | tail -n +4 | sed -e "/^[@-].\+/d" -e "s/\(\S\+\s\+\S\+\s\+\S\+\s\+\S\+\s\+\)\(\S\+\s\+\)\(\S\+\s\+\S\+\s\+\S\+\s\+\)/\1$c1\2$ce$c2\3$ce/" -e "s/^/  $c1>$ce /"
        echo
    fi
    echo "autocall: calling command \`$c4$1$ce'..."
    # see the "z" flag under PARAMTER EXPANSION under "man ZSHEXPN" for more info
    if [[ $tflag == true || $kflag == true ]]; then
        # the 'timeout' command gives nice exit statuses -- it gives 124 if
        # command times out, but if the command exits with an error of its own,
        # it gives that error number (so if the command doesn't time out, but
        # exits with 4 or 255 or whatever, it (the timeout command) will exit
        # with that number instead)

        # note: if kflag is true, then tflag is always true
        com_exit_status=0
        if [[ $kflag == true ]]; then
            eval timeout -k $killdelay $timeout $1 2>&1 | sed "s/^/  $col>$ce /"
            com_exit_status=$pipestatus[1]
        else
            eval timeout $timeout $1 2>&1 | sed "s/^/  $col>$ce /"
            com_exit_status=$pipestatus[1]
        fi
        if [[ $com_exit_status -eq 124 ]]; then
            echo "\n${c6}autocall: command timed out$ce"
        elif [[ $com_exit_status -ne 0 ]]; then
            echo "\n${c6}autocall: command exited with error status $com_exit_status$ce"
        else
            echo "\n${c1}autocall: command executed successfully$ce"
        fi
    else
        eval $1 2>&1 | sed "s/^/  $col>$ce /"
        com_exit_status=$pipestatus[1]
        if [[ $com_exit_status -ne 0 ]]; then
            echo "\n${c6}autocall: command exited with error status $com_exit_status$ce"
        else
            echo "\n${c1}autocall: command executed successfully$ce"
        fi
    fi
}

#------------------#
# Global variables #
#------------------#

# colors
c1="\x1b[1;32m" # bright green
c2="\x1b[1;33m" # bright yellow
c3="\x1b[1;34m" # bright blue
c4="\x1b[1;36m" # bright cyan
c5="\x1b[1;35m" # bright purple
c6="\x1b[1;31m" # bright red
ce="\x1b[0m"

coms=()
delay=5
xdelay_factor=4
f=()
F=()
b=()
d=()
D=()
l=()
l_targets=()
wflag=false
xflag=false
tflag=false
kflag=false
timeout=0
killdelay=0

tstampf="" # used to DISPLAY modification only for -f flag
linestamp="" # used to DETECT modification only for -f flag
tstampF="" # used to detect AND display modifications for -F flag
tstampb="" # used to DISPLAY modification only for -b flag
bytestamp="" # used to DETECT modification only for -b flag
tstampd="" # used to detect AND display modifications for -d flag
tstampD="" # used to detect AND display modifications for -D flag
tstampl="" # used to detect AND display modifications for -l flag

tstampf_new=""
linestamp_new=""
tstampF_new=""
tstampb_new=""
bytestamp_new=""
tstampd_new=""
tstampD_new=""
tstampl_new=""

#----------------#
# PROGRAM START! #
#----------------#

#---------------#
# Parse options #
#---------------#

# the leading ":" in the opstring silences getopts's own error messages;
# the colon after a single letter indicates that that letter requires an
# argument

# first parse for the presence of any -h and -v flags (while silently ignoring
# the other recognized options)
while getopts ":c:w:f:F:b:d:D:l:t:k:x:ahv" opt; do
    case "$opt" in
    h)  msg "help" ;;
    v)  msg "version" ;;
    *) ;;
    esac
done
# re-parse from the beginning again if there were no -h or -v flags
OPTIND=1
while getopts ":c:w:f:F:b:d:D:l:t:k:x:a" opt; do
    case "$opt" in
    c)
        com_binary=$(echo "$OPTARG" | sed 's/ \+/ /g' | sed 's/;/ /g' | cut -d " " -f1)
        if [[ $(which $com_binary) == "$com_binary not found" ]]; then
            msg "invalid command \`$com_binary'"
        else
            coms+=("$OPTARG")
        fi
        ;;
    w)
        if $(is_number "$OPTARG"); then
            if [[ $OPTARG -gt 0 ]]; then
                wflag=true
                delay=$OPTARG
            else
                msg "DELAY must be greater than 0"
            fi
        else
            msg "invalid DELAY \`$OPTARG'"
        fi
        ;;
    f)
        if [[ ! -f "$OPTARG" ]]; then
            msg "file \`$OPTARG' does not exist"
        else
            f+=("$OPTARG")
        fi
        ;;
    F)
        if [[ ! -f "$OPTARG" ]]; then
            msg "file \`$OPTARG' does not exist"
        else
            F+=("$OPTARG")
        fi
        ;;
    b)
        if [[ ! -f "$OPTARG" ]]; then
            msg "file \`$OPTARG' does not exist"
        else
            b+=("$OPTARG")
        fi
        ;;
    d)
        if [[ ! -d "$OPTARG" ]]; then
            msg "directory \`$OPTARG' does not exist"
        else
            d+=("$OPTARG")
        fi
        ;;
    D)
        if [[ ! -d "$OPTARG" ]]; then
            msg "directory \`$OPTARG' does not exist"
        else
            D+=("$OPTARG")
        fi
        ;;
    l)
        if [[ ! -f $OPTARG ]]; then
            msg "file \`$OPTARG' does not exist"
        else
            l+=("$OPTARG")
        fi
        ;;
    t)
        tflag=true
        if $(is_number "$OPTARG"); then
            if [[ $OPTARG -gt 0 ]]; then
                timeout=$OPTARG
            else
                msg "TIMEOUT must be greater than 0"
            fi
        else
            msg "invalid TIMEOUT \`$OPTARG'"
        fi
        ;;
    k)
        kflag=true
        if $(is_number "$OPTARG"); then
            if [[ $OPTARG -gt 0 ]]; then
                killdelay=$OPTARG
            else
                msg "TIMEOUT must be greater than 0"
            fi
        else
            msg "invalid KDELAY \`$OPTARG'"
        fi
        ;;
    x)
        xflag=true
        if $(is_number "$OPTARG"); then
            if [[ $OPTARG -gt 0 ]]; then
                xdelay_factor=$OPTARG
            elif [[ $OPTARG -eq 0 ]]; then
                xdelay_factor=1
            else
                msg "invalid FACTOR \`$OPTARG'"
            fi
        fi
        ;;
    a) xflag=true ;;
    🙂
        msg "missing argument for option \`$OPTARG'"
        ;;
    *)
        msg "unrecognized option \`$OPTARG'"
        ;;
    esac
done

#-----------------#
# Set misc values #
#-----------------#

if [[ $kflag == true && $tflag == false ]]; then
    tflag=true
    timeout=$killdelay
fi

#------------------#
# Check for errors #
#------------------#

# check that the given options are in good working order
if [[ -z $coms[1] ]]; then
    msg "help"
elif [[ (-n $f && -n $d && -n $D && -n $l) && $xflag == false ]]; then
    echo "autocall: see help with -h"
    msg "at least one or more of the (1) -f, -d, -D, or -l paramters, or (2) the -x parameter, required"
fi

#-------------------------------#
# Record state of watched files #
#-------------------------------#

if [[ -n $F ]]; then
    if [[ $#F -eq 1 ]]; then
        linestamp=$(wc -l $F)
    else
        linestamp=$(wc -l $F | head -n -1) # remove the last "total" line
    fi
    tstampF=$(ls --full-time $F)
fi
if [[ -n $f ]]; then
    tstampf=$(ls --full-time $f)
fi
if [[ -n $b ]]; then
    if [[ $#b -eq 1 ]]; then
        bytestamp=$(wc -c $b)
    else
        bytestamp=$(wc -c $b | head -n -1) # remove the last "total" line
    fi
    tstampb=$(ls --full-time $b)
fi
if [[ -n $d ]]; then
    tstampd=$(ls --full-time $d)
fi
if [[ -n $D ]]; then
    tstampD=$(ls --full-time -R $D)
fi
if [[ -n $l ]]; then
    for listfile in $l; do
        if [[ ! -f $listfile ]]; then
            msg "file \`$listfile ' does not exist"
        else
            while read line; do
                if [[ ! -e "$line" ]]; then
                    msg "\`$listfile': file/path \`$line' does not exist"
                else
                    l_targets+=("$line")
                fi
            done < $listfile # read contents of $listfile!
        fi
    done
    tstampl=$(ls --full-time -R $l_targets)
fi

#----------------------#
# Begin execution loop #
#----------------------#
# This is like Russian Roulette (where "firing" is executing the command),
# except that all the chambers are loaded, and that on every new turn, instead
# of picking the chamber randomly, we look at the very next chamber. After
# every chamber is given a turn, we reload the gun and start over.
#
# If we detect file/directory modification, we pull the trigger. We can also
# pull the trigger by pressing SPACE or ENTER. If the -x option is provided,
# the last chamber will be set to "always shoot" and will always fire (if the
# trigger hasn't been pulled by the above methods yet).

if [[ $xflag == true && $xdelay_factor -le 1 ]]; then
    xdelay_factor=1
fi
com_num=1
for c in $coms; do
    echo "autocall: command slot $com_num set to \`$c4$coms[$com_num]$ce'"
    let com_num+=1
done
echo "autocall: press keys 1-$#coms to execute a specific command"
if [[ $wflag == true ]]; then
    echo "autocall: modification check interval set to $delay sec"
else
    echo "autocall: modification check interval set to $delay sec (default)"
fi
if [[ $xflag == true ]]; then
    echo "autocall: auto-execution interval set to ($delay * $xdelay_factor) = $(($delay*$xdelay_factor)) sec"
fi
if [[ $tflag == true ]]; then
    echo "autocall: TIMEOUT set to $timeout"
    if [[ $kflag == true ]]; then
        echo "autocall: KDELAY set to $killdelay"
    fi
fi
echo "autocall: press ENTER or SPACE to execute manually"
echo "autocall: press \`c' for command list"
echo "autocall: press \`h' for help"
echo "autocall: press \`q' to quit"
key=""
while true; do
    for i in {1..$xdelay_factor}; do
        #------------------------------------------#
        # Case 1: the user forces manual execution #
        #------------------------------------------#
        # read a single key from the user
        read -s -t $delay -k key
        case $key in
            # note the special notation $'\n' to detect an ENTER key
            $'\n'|" "|1)
                autocall_exec $coms[1] $timeout $killdelay 4 "manual execution"
                key=""
                continue
                ;;
            2|3|4|5|6|7|8|9)
                if [[ -n $coms[$key] ]]; then
                    autocall_exec $coms[$key] $timeout $killdelay 4 "manual execution"
                    key=""
                    continue
                else
                    echo "autocall: command slot $key is not set"
                    key=""
                    continue
                fi
                ;;
            c)
                com_num=1
                echo ""
                for c in $coms; do
                    echo "autocall: command slot $com_num set to \`$c4$coms[$com_num]$ce'"
                    let com_num+=1
                done
                key=""
                continue
                ;;
            h)
                echo "\nautocall: press \`c' for command list"
                echo "autocall: press \`h' for help"
                echo "autocall: press \`q' to exit"
                com_num=1
                for c in $coms; do
                    echo "autocall: command slot $com_num set to \`$c4$coms[$com_num]$ce'"
                    let com_num+=1
                done
                echo "autocall: press keys 1-$#coms to execute a specific command"
                echo "autocall: press ENTER or SPACE or \`1' to execute first command manually"
                key=""
                continue
                ;;
            q)
                echo "\nautocall: exiting..."
                exit 0
                ;;
            *) ;;
        esac

        #------------------------------------------------------------------#
        # Case 2: modification is detected among watched files/directories #
        #------------------------------------------------------------------#
        if [[ -n $f ]]; then
            tstampf_new=$(ls --full-time $f)
        fi
        if [[ -n $F ]]; then
            if [[ $#F -eq 1 ]]; then
                linestamp_new=$(wc -l $F)
            else
                linestamp_new=$(wc -l $F | head -n -1) # remove the last "total" line
            fi
            tstampF_new=$(ls --full-time $F)
        fi
        if [[ -n $b ]]; then
            if [[ $#b -eq 1 ]]; then
                bytestamp_new=$(wc -c $b)
            else
                bytestamp_new=$(wc -c $b | head -n -1) # remove the last "total" line
            fi
            tstampb_new=$(ls --full-time $b)
        fi
        if [[ -n $d ]]; then
            tstampd_new=$(ls --full-time $d)
        fi
        if [[ -n $D ]]; then
            tstampD_new=$(ls --full-time -R $D)
        fi
        if [[ -n $l ]]; then
            tstampl_new=$(ls --full-time -R $l_targets)
        fi
        if [[ -n $f && "$tstampf" != "$tstampf_new" ]]; then
            autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampf" "$tstampf_new"
            tstampf=$tstampf_new
            continue
        elif [[ -n $F && "$linestamp" != "$linestamp_new" ]]; then
            autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampF" "$tstampF_new"
            linestamp=$linestamp_new
            tstampF=$tstampF_new
            continue
        elif [[ -n $b && "$bytestamp" != "$bytestamp_new" ]]; then
            autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampb" "$tstampb_new"
            bytestamp=$bytestamp_new
            tstampb=$tstampb_new
            continue
        elif [[ -n $d && "$tstampd" != "$tstampd_new" ]]; then
            autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampd" "$tstampd_new"
            tstampd=$tstampd_new
            continue
        elif [[ -n $D && "$tstampD" != "$tstampD_new" ]]; then
            autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampD" "$tstampD_new"
            tstampD=$tstampD_new
            continue
        elif [[ -n $l && "$tstampl" != "$tstampl_new" ]]; then
            autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampl" "$tstampl_new"
            tstampl=$tstampl_new
            continue
        fi

        #-----------------------------------------------------#
        # Case 3: periodic, automatic execution was requested #
        #-----------------------------------------------------#
        if [[ $xflag == true && $i -eq $xdelay_factor ]]; then
            autocall_exec $coms[1] $timeout $killdelay 3 "commencing auto-execution ($(($delay*$xdelay_factor)) sec)"
        fi
    done
done

# vim:syntax=zsh
Advertisements

Zsh: univ_open() update (new dir_info() script)

I’ve been heavily using the univ_open() shell function (a universal file/directory opener from the command line) that I wrote for many months now, and have made some slight changes to it. It’s all PUBLIC DOMAIN like my other stuff, so you have my blessing if you want to make it better (if you do, send me a link to your version or something).

What’s new? The code that prettily lists all directory contents after entering it has been cut off and made into its own function, called “dir_info”. So there are 2 functions now: univ_open(), which opens any file/directory from the commandline according to predefined preferred apps (dead simple to do, as you can see in the code), and dir_info(), a prettified “ls” replacement that also lists helpful info like “largest file” and the number of files in the directory.

univ_open() has not changed much — the only new stuff are the helpful error messages.

dir_info() should be used by itself from a shell alias (I’ve aliased all its 8 modes to quick keys like “ll”, “l”, “lk”, “lj”, etc. for quick access in ~/.zshrc). For example, “l” is aliased to “dir_info 1”. Here is the code below for dir_info():

#!/bin/zsh

# dir_info(), a function that acts as an intelligent "ls". This function is
# used by univ_open() to display directory contents, but it should additionally
# be used by itself. By default, you can call dir_info() without any arguments,
# but there are 8 presets that are hardcoded below (presets 0-7). Thus, you
# could do "dir_info [0-7]" to use those modes. You sould alias these modes to
# something easy like "ll" or "l", etc. from your ~/.zshrc. For a detailed
# explanation of the 8 presets, see the code below.

dir_info() {
    # colors
    c1="33[1;32m" # bright green
    c2="33[1;33m" # bright yellow
    c3="33[1;34m" # bright blue
    c4="33[1;36m" # bright cyan
    c5="33[1;35m" # bright purple
    c6="33[1;31m" # bright red
    # only pre-emptively give newline to prettify listing of directory contents
    # if the directory is not empty
    [[ $(ls -A1 | wc -l) -ne 0 ]] && echo
    countcolor() {
        if [[ $1 -eq 0 ]]; then
            echo $c4
        elif [[ $1 -le 25 ]]; then
            echo $c1
        elif [[ $1 -le 50 ]]; then
            echo $c2
        elif [[ $1 -le 100 ]]; then
            echo $c3
        elif [[ $1 -le 200 ]]; then
            echo $c5
        else
            echo $c6
        fi
    }

    sizecolor() {
        case $1 in
            B)
                echo $c0
                ;;
            K)
                echo $c1
                ;;
            M)
                echo $c2
                ;;
            G)
                echo $c3
                ;;
            T)
                echo $c5
                ;;
            *)
                echo $c4
                ;;
        esac
    }
    ce="33[0m"

    ctag_size=""

    # only show information if the directory is not empty
    if [[ $(ls -A1 | wc -l) -gt 0 ]]; then
        size=$(ls -Ahl | head -n1 | head -c -2)
        suff=$(ls -Ahl | head -n1 | tail -c -2)
        size_num=$(echo -n $size | cut -d " " -f2 | head -c -1)
        ctag_size=$(sizecolor $suff)
        simple=false
        # show variation of `ls` based on given argument
        case $1 in
            0) # simple
                ls -Chs -w $COLUMNS --color | tail -n +2
                simple=true
                ;;
            1) # verbose
                ls -Ahl --color | tail -n +2
                ;;
            2) # simple, but sorted by size (biggest file on bottom with -r flag)
                ls -ChsSr -w $COLUMNS --color | tail -n +2
                simple=true
                ;;
            3) # verbose, but sorted by size (biggest file on bottom with -r flag)
                ls -AhlSr --color | tail -n +2
                ;;
            4) # simple, but sorted by time (newest file on bottom with -r flag)
                ls -Chstr -w $COLUMNS --color | tail -n +2
                simple=true
                ;;
            5) # verbose, but sorted by time (newest file on bottom with -r flag)
                ls -Ahltr --color | tail -n +2
                ;;
            6) # simple, but sorted by extension
                ls -ChsX -w $COLUMNS --color | tail -n +2
                simple=true
                ;;
            7) # verbose, but sorted by extension
                ls -AhlX --color | tail -n +2
                ;;
            *)
                simple=true
                ls --color
                ;;
        esac

        # show number of files or number of shown vs hidden (as a fraction),
        # depending on which version of `ls` was used
        denom=$(ls -A1 | wc -l)
        numer=$denom
        # redefine numer to be a smaller number if we're in simple mode (and
        # only showing non-dotfiles/non-dotdirectories
        $simple && numer=$(ls -1 | wc -l)
        ctag_count=$(countcolor $denom)

        if [[ $numer != $denom ]]; then
            if [[ $numer -gt 1 ]]; then
                echo -n "\nfiles $numer/$ctag_count$denom$ce | "
            else
                dotfilecnt=$(($denom - $numer))
                s=""
                [[ $dotfilecnt -gt 1 ]] && s="s" || s=""
                echo -n "\nfiles $numer/$ctag_count$denom$ce ($dotfilecnt dotfile$s) | "
            fi
        else
            echo -n "\nfiles $ctag_count$denom$ce | "
        fi

        if [[ $suff != "0" ]]; then
            echo -n "size $ctag_size$size_num $suff$ce"
        else
            echo -n "size$ctag_size nil$ce"
        fi

        # Find the biggest file in this directory.
        #
        # We first use ls to list all contents, sorted by size; then, we strip
        # all non-regular file entries (such as directories and symlinks);
        # then, we truncate our result to kill all newlines with 'tr' (e.g., if
        # there is a tiny file (say, 5 bytes) and there are directories and
        # symlinks, it's likely that the file is NOT the biggest "file"
        # according to 'ls', which means that the output up to this point will
        # have trailing whitespaces (thus making the next command 'tail -n 1'
        # fail, even though there is a valid file!)); we then fetch the last
        # line of this list, which is the biggest file, then make it so that
        # all multiple-continguous spaces are replaced with a single space --
        # and using this new property, we can safely call 'cut' by specifying
        # the single space " " as a delimiter to finally get our filename.
        big=$(ls -lSr | sed 's/^[^-].\+//' | tr -s "\n" | tail -n 1 | sed 's/ \+/ /g' | cut -d " " -f9-)
        if [[ -f "$big" ]]; then
            # since $suff needs a file size suffix (K,M,G, etc.), we reassign
            # $big_size here from pure block size to human-readable notation
            # make $big_size more "accurate" (not in terms of disk space usage,
            # but in terms of actual number of bytes inside the file) if it is
            # smaller than 4096 bytes
            suff=""
            if [[ $(du -b "$big" | cut -f1) -lt 4096 ]]; then
                big_num="$(du -b "$big" | cut -f1)"
                suff="B"
            else
                big_num=$(ls -hs "$big" | cut -d " " -f1 | sed 's/[a-zA-Z]//')
                suff=$(ls -hs "$big" | cut -d " " -f1 | tail -c -2)
            fi
            ctag_size=$(sizecolor "$suff")
            echo " | \`$big' $ctag_size$big_num $suff$ce"
        else
            echo
        fi
    fi
}

Here is the updated univ_open() shell function:

#!/bin/zsh

# univ_open() is intended to be used to pass either a SINGLE valid FILE or
# DIRECTORY. For illustrative purposes, we assume "d" to be aliased to
# univ_open() in ~/.zshrc. If optional flags are desired, then either prepend
# or append them appropriately. E.g., if you have jpg's to be opened by eog,
# then doing "d -f file.jpg" or "d file.jpg -f" will be the same as "eog -f
# file.jpg" or "eog file.jpg -f", respectively. The only requirement when
# passing flags is that the either the first word or last word must be a valid
# filename.

# univ_open requires the custom shell function dir_info() (`ls` with saner
# default args) to work properly

univ_open() {
    if [[ -z $@ ]]; then
        # if we do not provide any arguments, go to the home directory
        cd && dir_info # ("cd" w/o any arguments goes to the home directory)
    elif [[ -f $1 || -f ${=@[-1]} ]]; then
        # if we're here, it means that the user either (1) provided a single valid file name, or (2) a number of
        # commandline arguments PLUS a single valid file name; use of the $@ variable ensures that we preserve all the
        # arguments the user passed to us

        # $1 is the first arg; ${=@[-1]} is the last arg (i.e., if user passes "-o -m FILE" to us, then obviously the
        # last arg is the filename
        #
        # we use && and || for simple ternary operation (like ? and : in C)
        [[ -f $1 ]] && file=$1 || file=${=@[-1]}
        case $file:e:l in
            (doc|odf|odt|rtf)
                soffice -writer $@ &>/dev/null & disown
            ;;
            (pps|ppt)
                soffice -impress $@ &>/dev/null & disown
            ;;
            (htm|html)
                firefox $@ &>/dev/null & disown
            ;;
            (eps|pdf|ps)
                evince -f $@ &>/dev/null & disown
            ;;
            (bmp|gif|jpg|jpeg|png|svg|tga|tiff)
                eog $@ &>/dev/null & disown
            ;;
            (psd|xcf)
                gimp $@ &>/dev/null & disown
            ;;
            (aac|flac|mp3|ogg|wav|wma)
                mplayer $@
            ;;
            (mid|midi)
                timidity $@
            ;;
            (asf|avi|flv|ogm|ogv|mkv|mov|mp4|mpg|mpeg|rmvb|wmv)
                smplayer $@ &>/dev/null & disown
            ;;
            (djvu)
                djview $@
            ;;
            (exe)
                wine $@ &>/dev/null & disown
            ;;
            *)
                vim $@
            ;;
        esac
    elif [[ -d $1 ]]; then
        # if the first argument is a valid directory, just cd into it -- ignore
        # any trailing arguments (in zsh, '#' is the same as ARGC, and denotes the number of arguments passed to the
        # script (so '$#' is the same as $ARGC)
        if [[ $# -eq 1 ]]; then
            cd $@ && dir_info
        else
            # if the first argument was a valid directory, but there was more than 1 argument, then we ignore these
            # trailing args but still cd into the first given directory
            cd $1 && dir_info
            # i.e., show arguments 2 ... all the way to the last one (last one has an index of -1 argument array)
            echo "\nuniv_open: argument(s) ignored: \`${=@[2,-1]}\`"
            echo "univ_open: went to \`$1'\n"
        fi
    elif [[ ! -e $@ ]]; then
        [[ $# -gt 1 ]] && head=$1:h || head=$@:h
        # if we're given just 1 argument, and that argument does not exist,
        # then go to the nearest valid parent directory; we use a while loop to
        # find the closest valid directory, just in case the user gave a
        # butchered-up path
        while [[ ! -d $head ]]; do head=$head:h; done
        cd $head && dir_info
        echo "\nuniv_open: path \`$@' does not exist"
        [[ $head == "." ]] && echo "univ_open: stayed in same directory\n" || echo "univ_open: relocated to nearest parent directory \`$head'\n"
    else
        # possible error -- should re-program the above if this ever happens,
        # but, it seems unlikely
        echo "\nuniv_open: error -- exiting peacefully"
    fi
}

Put these two functions inside your ~/.zsh/func folder with filenames “dir_info” and “univ_open” and autoload them. I personally do this in my ~/.zshrc:

fpath=(~/.zsh/func $fpath)
autoload -U ~/.zsh/func/*(:t)

to have them autoloaded. Then, simply alias “d” to “univ_open” and alias “ll” to “dir_info 0” and “l” to “dir_info 1”, and you’re set (the aliases to dir_info are optional, but highly recommended). The real star of the show in this post is dir_info() and how it displays various info for the directory after the “ls” stuff is printed on the screen. I hope you enjoy them as much as I do.