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

Generic Screen Backlight-Toggling Shell Script

I’ve written a shell script to toggle my LCD backlight (aka, screen brightness). Currently it works on a Dell D505 laptop and Fujitsu V1040. Here it is:

#!/bin/zsh
# change backlight settings based on system
# symlink to /usr/bin/brightness, and call with "sudo brightness"
# make sure to disable password prompt for it with "sudo visudo"

case $HOST in
    aether) # Fujitsu V1040
        # since xbacklight always returns a single floating point number, we need to
        # convert it to an integer
        float=`xbacklight`
        int=${float/\.*}
        if [[ $int -eq 0 ]]; then
            xbacklight -set 100
        else
            xbacklight -set 0
        fi
    ;;
    luxion) # Dell Latidude D505
        b=$(cat /sys/class/backlight/dell_backlight/actual_brightness)
        let "bi=$b"
        if [[ $b -lt 7 ]]; then
            # gradually raise backlight, just like xbacklight
            while [[ bi -lt 7 ]]; do
                let "bi=$bi+1"
                echo $bi > /sys/class/backlight/dell_backlight/brightness
            done
        else
            while [[ bi -gt 0 ]]; do
                let "bi=$bi-1"
                echo $bi > /sys/class/backlight/dell_backlight/brightness
            done
        fi
        ;;
    *)
        ;;
esac

# vim:syntax=zsh

The script pretty simple. It looks at the name of the computer ($HOST environment variable), and then chooses the appropriate way to change the backlight (either gradually set it to 100 or 0). Since the neat xbacklight command doesn’t work on my Dell laptop, I’ve written my own method. This method of echo-ing the desired brightness number into a system file (yes, Linux is cool like that) seems to be the trend in the various Linux forums out there. The trick is to find the right “brightness” file. Some systems use /proc/acpi/video/…/brightness and others, like my Dell Latitude D505, in /sys/class/backlight/…/brightness (though, on my Dell, I have to read separately from the file “actual_brightness,” but you get the idea). The script is, very straightforward and easily modifiable to work on your own laptop.

The only requirement is that, at least when echo-ing anything into /proc or /sys, you need root privileges. So you have to run “sudo path/to/brightness.sh”. I’ve simplified the process as follows:

  • sudo ln -s path/to/brightness.sh /usr/bin/brightness -> This makes the command “brightness” become available from anywhere in the system (since /usr/bin is in your $PATH environment variable).
  • sudo visudo -> Then add the line “your_username your_hostname=NOPASSWD: /usr/bin/brightness” -> This makes it so that typing “sudo brightness” does not require any password.
  • bind “sudo brightness” to a hotkey (consult your window manager) -> For me, I use SHIFT + CAPSLOCK + Backslash (capslock is my modkey in Xmonad).

Again, all of this “sudo visudo” and “sudo brightness” business is only required because my aging Dell laptop doesn’t play well with xbacklight.

So there you have it. A simple script that relies on either xbacklight, or a customized method, depending on the system. If you have multiple laptops that are supported by xbacklight, simply replace “aether” in the script above with “system1|system2|system3|sytem4” to save yourself from having to copy/paste the entire block of code for each system.

Zsh and Ruby: Toward a universal directory/file opener

This post has been deprecated. Please read this post instead.

I’ve been using Linux for a while now (almost 2 years?), and have learned quite a bit about it. However, ever since I switched to Xmonad as my window manager, I’ve always been tied to the “urxvt + zsh” GUI. Don’t get me wrong — I really enjoy this setup (a plain old terminal has a beauty all of its own in how things get done), but it could be even better.

Lately, I’ve noticed my ~/.zshrc file grow larger and larger, with the addition of new aliases to open up common programs, like “m” for mplayer (audio, movies) and “e” for evince (PDF’s). To get rid of this clutter, and also to automate things, I decided to make a new, simple alias to handle pretty much everything I throw at it. The new functionality lets Zsh, with the help of Ruby, open up just about any file I throw at it, while also integrating the common “cd” command for directories — all from the command line.

The required files/code are as follows: (1) a custom zsh function (autoloaded into your zsh session) and (2) a custom Ruby script. I’ve only tested this on Ruby 1.9.1p243 (2009-07-16 revision 24175) [x86_64-linux].

For the custom zsh function, here is how I autoload it from ~/.zshrc:

fpath=(~/.zsh/func $fpath) # add ~/.zsh/func to $fpath
autoload -U ~/.zsh/func/*(:t) # load all functions in ~/.zsh/func

The function itself is as follows:


 1 # Author: Shinobu (https://zuttobenkyou.wordpress.com)
 2 # Date: December 2009
 3 # License: PUBLIC DOMAIN
 4 # Program Name: univ_open()
 5 # Description: zsh function to open up either directories or files
 6 univ_open() {
 7     ls_pretty="ls -Ahs --color=auto"
 8     if [[ ! -e $@ ]]; then
 9         # go to the parent directory if given path does not exist
10         /home/shinobu/syscfg/shellscripts/sys/univ_handler.rb $@ "parent" | read parent
11         cd $parent && ${=ls_pretty}
12     elif [[ -d $@ ]]; then
13         cd $@ && ${=ls_pretty}
14     else
15         # if it does exist, but is not a directory, open it up with the
16         # universal readerscript, to execute an appropriate program for the
17         # extension
18         /home/shinobu/syscfg/shellscripts/sys/univ_handler.rb $@ "extension" | read outputcmd
19         # since zsh does not split words by default, we need to manually do
20         # this by invoking outputcmd with the ${=VAR} syntax; this is because
21         # univ_handler will sometimes give a multi-word command where we use
22         # one or more commandline options, such as "mplayer -loop 0" or
23         # "soffice -writer" -- to prevent zsh from interpreting the entire
24         # string as the name of a command, we have to split it up into an array
25         ${=outputcmd} $@
26     fi
27 }

The file that contains this function is also called “univ_open”, which resides in ~/.zsh/func/. Anyway, this function is quite simple — it checks to see if the given argument does not exist (line 8), does exist and is a directory (line 12), or does exist and is a file (line 14).

In the “if” statement at line 8, it calls our custom Ruby script, univ_handler.rb, with an argument called “parent”. For lines 12-13, we don’t need to intelligently handle anything since the given argument is a valid directory path, so we just “cd” into it. Lastly, if the argument is a valid file (line 14), we call univ_handler.rb with the “extension” argument to determine which command to use to open up that file.

The univ_handler.rb file looks like this:


 1 #!/usr/bin/ruby
 2 # Author: Shinobu (https://zuttobenkyou.wordpress.com)
 3 # Date: December 2009
 4 # License: PUBLIC DOMAIN
 5 # Program Name: univ_handler.rb
 6 # Description: with the help of smart input from zsh, this script helps zsh
 7 # open up directories and files intelligently
 8
 9 class Ext
10     @doc = %w{doc odf odt rtf}
11     @web = %w{htm html}
12     @pdf = %w{pdf ps}
13     @img = %w{bmp gif jpg jpeg png svg tiff}
14     @img2 = %w{psd xcf}
15     @audio = %w{flac mp3 wma}
16     @midi = %w{mid midi}
17     @movie = %w{avi flv ogg mkv mov mp4 mpg mpeg wmv}
18
19     class<<self; attr_reader :doc end
20     class<<self; attr_reader :web end
21     class<<self; attr_reader :pdf end
22     class<<self; attr_reader :img end
23     class<<self; attr_reader :img2 end
24     class<<self; attr_reader :audio end
25     class<<self; attr_reader :midi  end
26     class<<self; attr_reader :movie end
27 end
28
29 file = ARGV.shift
30 arg = ARGV.shift
31
32 # zsh will read whatever we ultimately "print" to STDOUT!
33 if file.nil? # if the user does not specify an argument
34     print ""
35 else # figure out file extension, or if directory, the parent directory
36     if arg == "extension"
37         case file.split(".").last.downcase
38         when *Ext.doc
39             print "soffice -writer"
40         when *Ext.web
41             print "firefox"
42         when *Ext.pdf
43             print "evince"
44         # image files -- typical simple files for plain viewing
45         when *Ext.img
46             print "eog"
47         # image files -- ones meant to be edited (strange looking ones and also
48         # gimp's native format)
49         when *Ext.img2
50             print "gimp"
51         # audio files
52         when *Ext.audio
53             print "mplayer -loop 0"
54         # midi files
55         when *Ext.midi
56             print "timidity"
57         # movies
58         when *Ext.movie
59             print "mplayer -loop 0"
60         else
61             # use vim to access files by default, including those without an
62             # extension
63             print "vim"
64         end
65     elsif arg == "parent" # find the closest valid parent directory
66         if file.scan("/").empty? # if there is no "/" in the argument
67             print "." # let's keep the user in the same directory
68         else # if we have a filename (at least the user thinks so), but it is
69              # not a valid file path = file.split("/")
70             path.pop
71             # return 1 directory higher than this one -- we assume that the
72             # reader used tab completions enough to avoid having more than 1
73             # bad directory/file level (that only the text following the last
74             # "/" is invalid)
75             print path.join("/")
76         end
77     end
78 end

You don’t even need to know Ruby to see what’s going on. The important thing is what’s inside the curly braces — we simply group common extensions together based on the file they usually represent. We don’t bother with uppercased combinations, since we use Ruby’s String class’s “downcase” method on the original user-given argument before handling it. Since I love vim, I use it as the “catch-all” to open up any other kind of file (such as files that don’t have extensions).

Now, the most important part — the one alias to rule them all (in your ~/.zshrc):

alias d='univ_open' # alias to use univ_open()

And… that’s it! All you have to do is now do d [file, directory, path, symlink to either a directory or path] to open it up intelligently! I personally use “d” since historically I’ve used it to cd into a directory (and used “k” to go back up); I also find using my middle finger much easier to press keys than my index finger. Probably the best benefit of this setup is that you can do “d TAB TAB …” to go deep into directories, and then just press ENTER when you find the file you want to edit — no more cd-ing deep into a subdirectory first, and then doing “ls” on it to see what’s inside, and deciding which command to use to open up that file. Now it’s just “d” and away you go.

It took me about half a day to get everything working, with help from googling, of course. I hope you can make use of it as it is, or even better, to customize it to your own needs. There’s enough “foundation” code in there with enough complexity to let anyone customize it a great deal and add more extensions to it.

As an added bonus, notice how the code here does not mention symlinks at all — and yet they are handled properly (don’t you love symlinks?)).

UPDATE December 6, 2009: Apparently, there is a similar command line script to open anything called “gnome-open”: http://embraceubuntu.com/2006/12/16/gnome-open-open-anything-from-the-command-line/. You can open up URLs in addition to directories and files. I don’t really see the advantages of opening up a URL from the command line, though — I’m already on Xmonad and going to my supremely powerful Firefox + Vimperator setup is at most 2 or 3 keystrokes away. Still, it looks interesting.

UPDATE December 31, 2009: This post has been deprecated. Please read this post instead.

Useful shell aliases for pacman/powerpill in Arch Linux

If you’re an Arch Linux user, you’ll find yourself typing “pacman -Sy <package>” or “powerpill -Syu” a lot. Here are some useful shell aliases (here, they are for Zsh) that I use to make things much, much easier:

alias p='sudo pacman'
alias pu='sudo pacman -Syu'
alias pa='sudo pacman -Sy'
alias pq='sudo pacman -Q'

alias pp='sudo powerpill'
alias ppu='sudo powerpill -Syu'
alias ppa='sudo powerpill -Sy'

And then all I have to type is ppu, and my system is upgraded. Also, I have in my /etc/sudoers file the following:

shinobu exelion=NOPASSWD: /usr/bin/pacman
shinobu exelion=NOPASSWD: /usr/bin/powerpill

This way, I don’t have to type my password every time I want to use pacman/powerpill.