open-with for the command-line

Update 2008/11/18: Use xargs 😛

Here’s a bash script that you can pipe output into and tell it to run a specific program with the output as arguments. I’ve named it open-with and placed it in my personal script directory: /home/steve/bin/. Look within the script at a couple of the examples for how to use it.

#!/bin/bash

PROG=`basename $0`

DESC="Read arguments from standard input and run a specified program
with them.  Meant to be used as output for a pipe."


USAGE="OUTPUT | $PROG \"PROGRAM-NAME [OPTIONS AND ARGUMENTS]\""

EXAMPLES="
# List (with details), all the files that include \"downloads\" in their name:
  find /home/ -iname \"*downloads*\" | $PROG \"ls -l\"

# Queue all AVI files in current directory in vlc:
  ls *.avi | $PROG vlc

# View all time-related icons with eye-of-gnome:
  find /usr/share/icons/gnome/ -iregex \".*[^mul]time.*\" | open-with eog
"

function Usage() {
  echo "DESCRIPTION: $DESC" ; echo
  echo "USAGE: $USAGE" ; echo
  echo -n "EXAMPLES: $EXAMPLES"
}

# Quick check to see it's being called correctly, if not, print Usage and exit
if [ ${#@} -ne 1 ]
then
	Usage
	exit
fi

files=()						# empty array
while read -r					# read from stdin
do
	files+=( "$REPLY" )			# add result of read to array
done

# assume $1 is a valid program
$1 "${files[@]}"				# pass arguments to specified program

Motivation

You’re sitting at the command line and have a list of images (in different locations) that you would like to browse. Most image viewers let you iterate over a set of images, but only within the same directory. What would be great is if they accepted input from stdin through a pipe!

$ cat my_list_of_images | my_image_viewer

I didn’t find anything that did that, but using the open-with script, you can do something similar:

$ cat my_list_of_images | open-with my_image_viewer


Browsing a select list of images is actually kind of nice. It’s like a playlist for your image viewer – a viewlist. 🙂 Anyway, I’m sure there’s some problems with this script. Feel free to provide suggestions in the comments. But I certainly don’t want to look at it for a while….

I hate shell scripting

With a passion. It’s not a great surprise… many programmers do. I’m willing to go on record and state that I hate it even more than PERL programming. I rarely do it, and when I decide to try something that seems like it would be simple, it turns out taking forever due mostly to quirks. The rest of this post is a bit about how I went about writing this script, which ended up being mostly given to me by some #bash gurus on IRC. And it’s a bit of a rant.

Although not many GUI programs seem to accept stdin as input, most accept filenames as arguments:

$ eog image1.jpg image2.jpg image3.jpg "/home/steve/image seven.jpg"

So I figured I would just have to convert the list of images into an acceptable format: quoted, absolute filenames, separated by space. Doing this depends entirely on the format the list is currently in, but it’s likely a list of unquoted filenames separated by newlines:

/home/steve/image1.jpg
/home/steve/my images/wow.jpg
/tmp/anotherimage.png

For me, my test list looked like this:

/usr/share/icons/gnome/48x48/stock/generic/stock_timezone.png
/usr/share/icons/gnome/16x16/stock/generic/stock_timezone.png
/usr/share/icons/gnome/16x16/stock/generic/stock_timer.png
/usr/share/icons/gnome/16x16/stock/generic/stock_timer_stopped.png
/usr/share/icons/gnome/16x16/stock/form/stock_form-time-field.png
/usr/share/icons/gnome/24x24/stock/generic/stock_timezone.png
/usr/share/icons/gnome/24x24/stock/generic/stock_timer.png
/usr/share/icons/gnome/24x24/stock/generic/stock_timer_stopped.png
/usr/share/icons/gnome/24x24/stock/form/stock_form-time-field.png

I was looking for icons of clocks or representations of time, and I obtained that list using find:

$ find /usr/share/icons/gnome/ -iregex ".*[^mul]time.*"

So I can’t pipe it into my image viewer, but I can use the output as the command-line arguments if I change the newlines to spaces. find has an option for formatting the output which is perfect:

$ find /usr/share/icons/gnome/ -iregex ".*[^mul]time.*" -printf "'%p' "
'/usr/share/icons/gnome/48x48/stock/generic/stock_timezone.png' '/usr/share/icons/gnome/16x16/stock/generic/stock_timezone.png' '/usr/share/icons/gnome/16x16/stock/generic/stock_timer.png' '/usr/share/icons/gnome/16x16/stock/generic/stock_timer_stopped.png' '/usr/share/icons/gnome/16x16/stock/form/stock_form-time-field.png' '/usr/share/icons/gnome/24x24/stock/generic/stock_timezone.png' '/usr/share/icons/gnome/24x24/stock/generic/stock_timer.png' '/usr/share/icons/gnome/24x24/stock/generic/stock_timer_stopped.png' '/usr/share/icons/gnome/24x24/stock/form/stock_form-time-field.png' 

That’s great for me, because I’m using find. But not very useful if I’m not, so I wanted something more generic. Of course, there are many ways to do this, and again, I’m by no means a command-line guru. But here’s how I started:

$ find /usr/share/icons/gnome/ -iregex ".*[^mul]time.*" | sed -e 's/^/"/' | sed -e :a -e '$!N;s/\n/" /; ta' | tr '\n' '"'

This wrapped the lines in double-quotes and join them together with a space in between. The find command is the same as before, minus the formatting because that’s what I was trying to find an alternative to. The output of find is piped into sed, which adds a " at the beginning of each line. This output is then sent to another sed which replaces the newline character at the end of each line with a closing double-quote and a space, joining all lines into a single line. Finally, that output is piped into tr which replaces the one remaining newline with a final double-quote.

If the files don’t include spaces or other troublesome characters (mine didn’t), then you could get away with simply changing the newlines into spaces. But again, I wanted something generic.

$ find /usr/share/icons/gnome/ -iregex ".*[^mul]time.*" | tr '\n' ' '

Anyway, now that we have something that creates the desired input, we just have to wrap it in back-ticks and put the whole mess as the argument to the image viewer! In my case, I’m using eye-of-gnome (or eog).

$ eog `find /usr/share/icons/gnome/ -iregex ".*[^mul]time.*" | sed -e 's/^/"/' | sed -e :a -e '$!N;s/\n/" /; ta' | tr '\n' '"'`

Wait a second. That doesn’t actually work. Why not? Running the backtick contents by itself seemed to produce the correct output. Copying this output verbatim as arguments to eog worked as expected. The problem was that when the quoted arguments were manually entered on the command-line, bash (silently) escapes the filenames, removing the quotes, adding backslashes before space,s etc. But when the pipeline is wrapped in backticks, the result is not escaped, and eog complains about not being able to find files that begin with quotes. Fine. I was not about to write a bash “escape” script – something like that should already exist, right? And it should be built in to bash! Perhaps it’s even called “escape“. Well, if there exists such a built-in, I couldn’t find it. But I had to be going about this the wrong way. Heading over to IRC, it was kind of difficult to explain the problem, but ferret eventually gave me the meat of the script above:

files=(); while read -r; do files+=( "$REPLY" ); done < <(find /usr/share/icons/gnome/ -iregex ".*[^mul]time.*"); eog "${files[@]}"

It worked! I thanked him and began studying it. There were still a couple things I didn't understand:

  • Why two angle brackets with a space between them? I understand the first one is probably redirecting input, but I don't understand that one next to the opening parenthesis.
    Answer: The <(command) actually puts command's output in a temporary file and produces that filename. So <(command) becomes tempfile. (Thanks, kojiro.)
  • If I open some video or audio files in totem using open-with, there's an odd delay. Using vlc doesn't produce a delay, however.

And to think this was originally going to be a micro-blog post in Twitter and identi.ca. Wow. It's safe to say I still hate shell scripting. 🙂

Leave a comment

Your email address will not be published. Required fields are marked *