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.

Autocall: A Script to Watch and “Call” Programs on a Changed Target Source File

UPDATE July 24, 2010: This post is now totally obsolete. See this post instead.

Recently, I’ve realized that the Autolily script I made was just one solution to a larger class of problems — that of calling a specific program on a target source/text file repeatedly every time you change the source. So, I’ve modified it slightly to make it accept any program name, so that the general format is: autocall [program] [file]. The source code is below:

#!/usr/bin/ruby
#===============================================================================================================#
# Program name: Autocall                                                                                        #
# Author: Shinobu (zuttobenkyou.wordpress.com)                                                                  #
# Date: March 2010                                                                                              #
# LICENSE: PUBLIC DOMAIN                                                                                        #
#                                                                                                               #
# This program takes 2 or 3 arguments; the first 2 is the command and file, while the third optional arg is the #
# delay b/n each possible execution of the command. By default this delay is 1 second (it checks if the file has#
# been modified every second)                                                                                   #
#                                                                                                               #
# Place this script somewhere, like in ~/scripts                                                                #
# Then, open up a terminal and call it like so: ~/scripts/autocall.rb [program] [file]                          #
#                                                                                                               #
# You might want to do a "sudo ln -s" of autocall.rb to one of your system's $PATH directories (e.g., /usr/bin) #
# to avoid typing out the path to autocall.rb every time you use it. Continuing the example from above,         #
# something like "sudo ln -s ~/scripts/autocall.rb /usr/bin/autocall" should do (make sure that                 #
# /usr/bin/autocall does not exist already, as the above comman will overwrite that file if it exists).         #
#                                                                                                               #
# Now you can just do:                                                                                          #
#                                                                                                               #
#     autocall [command] [file]                                                                                 #
#                                                                                                               #
# from anywhere in your system!                                                                                 #
#                                                                                                               #
# To exit, press CTRL-C.                                                                                        #
#===============================================================================================================#

if ARGV.size > 1
    file_data_orig = ""
    call = ARGV.shift
    file = ARGV.shift
    delay = 1
    if ARGV.size > 0
        delay = ARGV.shift.to_i
    end
    pathsize = file.split("/").size
    ls_call = "ls --full-time"

    # make sure that the "file" variable is a filename, and not mixed with its path
    if pathsize > 1
        path_to_file = file.split("/").first(pathsize - 1).join("/")
        file = file.split("/").last
        ls_call << " #{path_to_file}" # modify our `ls` command to reflect relative location of file
    end

    `#{ls_call}`.split("\n").each do |line|
        if line.split(/\s/).last == file
            file_data_orig = line
            break
        end
    end
    file_data_new = ""

    # enter infinite loop -- keep compiling the given file if it has changed in the past 1 second
    while true
        # detect the file size and also timestamp
        lsarr = `#{ls_call}`.split("\n")
        lsarr.shift # get rid of the first line, since that is the size of all the files in the directory

        # find our file from ls's output!
        lsarr.each do |line|
            if line.split(/\s/).last == file
                file_data_new = line
                break
            end
        end

        # if there is any change detected, run given command on it
        if file_data_orig != file_data_new
            puts "\n\e[1;38;5;226mautocall: change detected @ #{Time.now.ctime} in file `#{file}'; invoking `#{call}'...\e[0m\n"
            if pathsize > 1
            `#{call} "#{path_to_file}/#{file}"`
            else
            `#{call} "#{file}"`
            end
            file_data_orig = file_data_new
        end
        sleep delay
    end
else
    puts "Usage: autocall [command] [file]\n"
end

I can think of at least 1 other time you would want to use this script aside from editing LilyPond files — when editing LaTeX files. For me, I use currently use autocall to call a program that converts text files intelligently to HTML files. You could further edit the source to let it pass along command line options to autocall as well, and not just the program name (I will probably do this myself if the situation presents itself in the future).

Zsh: univ_open: A Universal Directory/File Opener (no more bashrun)

UPDATE July 1, 2010: univ_open() has been updated here.

Like my previous post, the goal is to use a single alias to open up any kind of file or directory intelligently. I’ve decided to write a new post because of various improvements over the old code. For one, I’ve implemented everything in Zsh — now everything is much faster, especially for older systems. So now the components are: (1) a custom zsh function called univ_open and (2) some custom options in ~/.zshrc to complement univ_open.

Here is the new univ_open:

 1 # Author: Shinobu (https://zuttobenkyou.wordpress.com)
 2 # Date: December 2009
 3 # License: PUBLIC DOMAIN
 4 # Program Name: univ_open
 5 # Description: open up directories and files intelligently
 6 
 7 univ_open() {
 8     ls="ls -Ahs --color=auto"
 9     if [[ -z $@ ]]; then
10         # if we do not provide any arguments, go to the home directory
11         cd && ${=ls}
12     elif [[ ! -e $@ ]]; then
13         # go to the nearest valid parent directory if file does not exist; we
14         # use a while loop to find the closest valid directory, just in case
15         # the user gave a butchered-up path
16         d=$@:h
17         while [[ ! -d $d ]]
18         do
19             d=$d:h
20         done
21         cd $d && ${=ls}
22     elif [[ -d $@ ]]; then
23         cd $@ && ${=ls}
24     else
25         case $@:e:l in
26             (doc|odf|odt|rtf)
27                 soffice -writer $@ &>/dev/null & disown
28             ;;
29             (htm|html)
30                 firefox $@ &>/dev/null & disown
31             ;;
32             (pdf|ps)
33                 evince $@ &>/dev/null & disown
34             ;;
35             (bmp|gif|jpg|jpeg|png|svg|tiff)
36                 eog $@ &>/dev/null & disown
37             ;;
38             (psd|xcf)
39                 gimp $@ &>/dev/null & disown
40             ;;
41             (aac|flac|mp3|ogg|wav|wma)
42                 smplayer $@ &>/dev/null & disown
43             ;;
44             (mid|midi)
45                 timidity $@
46             ;;
47             (avi|flv|ogv|mkv|mov|mp4|mpg|mpeg|wmv)
48                 smplayer $@ &>/dev/null & disown
49             ;;
50             *)
51             vim $@
52             ;;
53         esac
54     fi
55 }

The basic operation of this function remains the same: if no arguments given, go to the home directory (this is cd‘s default behavior); if path is invalid, go to the nearest valid parent directory; if path is a valid directory, cd to it; otherwise, it must be a valid file, so open it up with a program based on the file’s extension, and use vim as the default program for unrecognized extensions (or files with no extensions). Again, filename extensions are lowercased to ensure that we are case-insensitive.

Some improvements over the old code: (1) univ_open now correctly handles messed-up, invalid directories by trying to find the closest valid parent directory — this is achieved with a simple while loop, and zsh’s neat little “:h” method to extract the parent directory (same functionality as the UNIX dirname utility); if there is not a single valid directory in the given path, then it will simply cd into the current directory (i.e., nothing happens); (2) the file extensions are handled much better depending on filetype — for commands that are GUI-based, we redirect its STDOUT and STDERR to /dev/null to silence any error messages, immediately background it with the “&” operator, and finally disown it so that we can continue other work from the same terminal where we used our alias from. Terminal-bound commands, such as timidity and vim, are left as-is.

The neat thing about this new version of univ_open is that this makes bashrun for me obsolete! I can do pretty much everything from the terminal as it is.

And now, the pertinent options in ~/.zshrc to complement our use of univ_open:

fpath=(~/.zsh/func $fpath) # add ~/.zsh/func to $fpath
autoload -U ~/.zsh/func/*(:t) # load all functions in ~/.zsh/func
zmodload zsh/complist
setopt auto_menu
unsetopt listambiguous
zstyle ':completion:*' menu select=1 _complete _ignored _approximate
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}
bindkey -M menuselect '^M' .accept-line
LISTMAX=9999
alias d='univ_open' # alias to use univ_open()

auto_menu enables the menu selection system after pressing TAB. unsetopt listambiguous makes zsh automatically complete an ambiguous word to the nearest, partial-word match (combined with auto_menu, this makes it so that you only have to type TAB once, and only once, in all situations). The zstyle lines affect the style of how the menu is displayed. The bindkey portion makes the ENTER key execute the command when using the menu. (Note, ^M is a special character that represents a newline; in vim, you have to type CTRL-V, then CTRL-M to insert it). Finally, setting LISTMAX to a high number prevents zsh from asking “do you wish to see all N possibilities?”, unless N > 9999.

Happy zsh-ing!

UPDATE January 13, 2010: If you’re in a hurry, then you can put bindkey ‘\t’ menu-expand-or-complete to make it so that pressing TAB once brings up the menu and select the first item in that menu. (Or, if there is just 1 possibility, then to expand to that item).

UPDATE February 25, 2010: See my comment #1 below.