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.

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.

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.