Contact Info

(for those who care)

Instant Gratification   



Wed, 15 Sep 2010

Best way to make a popup menu for semi-portable shell scripts

Basically there are a lot of times I would love to say:

echo `grep ^foo /usr/share/dict/words | popup_menu`

…and have some type of keyboard navigable menu popup or selection tool, very similar to how vim’s “:Explore” mechanism works.

After thorough investigation, the winner of best(?) way to make a popup menu is as follows:

select f in aaa bbb ccc ddd ; do echo $f ; break ; done

It isn’t actually a popup menu per-se but you get the best bang for your buck as far as using standard unix-isms and it is pretty much universally available since it’s a bash builtin. Wrapping it in a simple shell script is easy to do wherever you are and means you can reliably integrate its benefits into your workflow.

    $ cat ~/bin/menu.sh
    #!/bin/sh
    ALL=`cat`
    select FOO in $ALL ; do echo $FOO ; break ; done

    $ ls /usr | ~/bin/menu.sh
    1) bin      3) include    5) lib64    7) sbin    9) src
    2) games    4) lib        6) local    8) share
    #? 2
    games

In actuality though, you want to use the “select f in …” idiom as a fallback for when the dialog command isn’t available. The following shell / dialog script is kindof ugly but gets the job done as far as providing the same inputs and outputs as above but with a more comfortable user interface.

    $ cat ~/bin/gui-menu.sh
    #!/bin/sh

    # get stdin
    ALL=`cat`

    # number the lines
    SPLITTED=$( echo $ALL | sed 's/ /\n/g' | awk -- '{print NR, $0 }' )

    # prompt via dialog (output-fd=1 is so that dialog gui doesn't go to subshell)
    OUT=$( dialog --output-fd 1 --ok-label Select --menu Choose 0 50 22 $SPLITTED )
    EXIT_CODE=$?

    # handle escape / cancel buttons
    if [ "1" = "$EXIT_CODE" ] ; then exit 1 ; fi
    if [ "255" = "$EXIT_CODE" ] ; then exit 1 ; fi

    # extract text corresponding to user's numeric selection
    CHOSEN=$( echo $ALL | sed 's/ /\n/g' | awk -- "NR==$OUT {print \$0 }" )

    # print result
    echo $CHOSEN

…it is used exactly as the above “menu.sh” but prompts with an ascii dialog gui instead of numerically. It’s relatively easy to expand the above to allow dialog multiple checkboxes (very inefficiently, probably n^2-ish in the below implementation), which is shown here:

    $ cat ~/bin/gui-multiselect.sh
    #!/bin/sh

    # get stdin
    ALL=`cat`

    # number the lines
    SPLITTED=$( echo $ALL | sed 's/ /\n/g' | awk -- '{print NR, $0, 0 }' )

    # prompt via dialog (output-fd=1 is so that dialog gui doesn't go to subshell)
    # --checklist  v.  --menu is the key differentiator here
    OUT=$(dialog --output-fd 1 --ok-label Select --separate-output --checklist Choose 0 50 22 $SPLITTED)
    EXIT_CODE=$?

    # handle escape / cancel buttons
    if [ "1" = "$EXIT_CODE" ] ; then exit 1 ; fi
    if [ "255" = "$EXIT_CODE" ] ; then exit 1 ; fi

    # loop through selected numbers
    for X in $OUT ; do
        # inefficiently print out the text corresponding to the selections
        CHOSEN=$( echo $ALL | sed 's/ /\n/g' | awk -- "NR==$X {print \$0 }" )
        echo $CHOSEN
    done;

And third place goes to Joey Hess’s “vipe” interactive pipeline editor (from “moreutils” package), which lets you edit a pipeline and pass its output back out.

echo `grep ^foo /usr/share/dict/words | vipe`

The above command isn’t quite a dialog box (can’t just use up / down arrows and press enter, actually have to delete all the lines you don’t want and “:wq” out of the file) but it is useful because it handles both interactive single and multi-select use cases and is just an all around interesting tool.

For a true GUI selection, zenity looks like a winner as far as ease of use compared to dialog and xdialog … dialog unfortunately doesn’t “ad-hoc” very well but combining the above dialog scripts with a “select f in …” fallback is what best matches my needs.

If it’s not immediately obvious as to why you might want to use a menu, you’re just not thinking lazily enough…

 $ git checkout -b `git branch -a | gui-menu.sh`
 $ ssh `grep foo /etc/hosts | gui-menu.sh`
 $ vim `grep -ir some_function_call * | gui-multiselect.sh`
 $ rm `ls | gui-multiselect.sh`  # ignore obvious quoting issues with this...

Note: Mostly a reprint of my question and answer at stackoverflow.com… it actually looks a lot nicer over there.

14:26 CST | category / entries
permanent link | comments?

Like what you just read? Subscribe to a syndicated feed of my weblog, brought to you by the wonders of RSS.



Thanks for Visiting!