An Exploration In Selecting Things

November 03, 2013 at 04:44 PM

Sometime last year, I remember finding a code snippet to help switch between Python virtual environments, which I added to my .bashrc.

menuvirtualenv() {
    select env in `lsvirtualenv -b`; do
        if [ -n "$env" ]; then
            workon "$env"
        fi;
        break;
    done;
}
alias v.menu='menuvirtualenv'

12:54:26 pcoles@peters_air:~ > v.menu
1) category-cms
2) collect
3) mrcoles
4) readmd
#? 3
(mrcoles)12:54:33 pcoles@peters_air:~/projects/mrcoles >

This method for selecting an input stood out to me for being so simple: just a numbered list. Many command line applications make input too complex, making the user think about what they want to select, making them type it in again, while many don’t even support tab complete.

Several months ago, I was on an airplane with no internet and decided to challenge myself to implement that select interface in Python. When looking at the code again, I found it was just a Bash builtin function:

select name [ in word ] ; do list ; done
    The list of words following in is expanded, generating a list of items. The
    set of expanded words is printed on the standard error, each preceded by
    a number. If the in word is omitted, the positional parameters are printed
    (see PARAMETERS below). The PS3 prompt is then displayed and a line read
    from the standard input. If the line consists of a number corresponding to
    one of the displayed words, then the value of name is set to that word. If
    the line is empty, the words and prompt are displayed again. If EOF is read,
    the command completes. Any other value read causes name to be set to null.
    The line read is saved in the variable REPLY. The list is executed after
    each selection until a break command is executed. The exit status of select
    is the exit status of the last command executed in list, or zero if no
    commands were executed.

I started hacking, got it working, then forgot it about. Now coming back to the code with an internet connection, I’ve released it on PyPI and Github.

Pyselect

Pyselect wraps raw_input(), more or less:

In [1]: import pyselect
In [2]: pyselect.select(['apples', 'oranges', 'bananas'])
1) apples
2) oranges
3) bananas
#? 2
Out[2]: 'oranges'

But can also be used as a Python module, when scripting:

$ python -m pyselect $(ls)
1) LICENSE.txt
2) build/
3) dist/
4) pyselect.egg-info/
5) pyselect.py
6) pyselect.pyc
7) setup.py
8) test.py
#? 4
pyselect.egg-info/

Or in a Bash pipe:

$ ls | xargs python -m pyselect
1) LICENSE.txt
2) build/
3) dist/
4) pyselect.egg-info/
5) pyselect.py
6) pyselect.pyc
7) setup.py
8) test.py
#? 5
pyselect.py

But that’s where things kind of fall apart. Within a standard interactive Python application, stdin and stdout are simple and pyselect just works. Getting the pipe-in to work required a bit more work, hooking in/out up the user’s tty, which the pipe drops. My holy grail would be a pipe-in and pipe-out to make selecting anything much easier:

$ ls | xargs python -m pyselect | cp $0 test.txt

Or display all your git branches and jump to one. Or virtualenvs. Or directories. Double pipe redirects the pyselect output/input and doesn’t work. I’ve read up on named pipes that might be able to solve this, but I haven’t found a Python solution yet.

To jump between git branches with bash select, I used this:

function gobranch() {
    select branch in $(git for-each-ref --sort=-committerdate refs/heads/ --format='%(refname)' | sed 's/refs\/heads\///g'); do
        git checkout "$branch"
        break;
    done;
}

Moving Forward

For now, I have some other ideas to try with selecting things:

  • Auto-select an option when you have less than 9 options and enter 1-9, no need to hit the enter key
  • A-Z input, maybe default to home row, ala vim-easymotion
  • Multi-option select, 1-4 ala git add --interactive mode
  • Integration with Fabric perhaps, which has some simple input functions

Ideally, pyselect could become “input for humans”, ala Requests, because raw_input() could always use a more friendly API.

Powered by Middleman.