0

I have a working bash script (working on OSX) that takes files and directories as input and does something like

for inputFile in $@
do
[someStuff]
done

but I want to provide a “fallback”, meaning, if the script is started with no arguments (double-clicked, for example), it can take input at that time, by letting the user drop the files directly on the terminal (possibly through read but not mandatory, I'm open to better/different solutions).

I'm guessing I should use some kind of if statement, but I'm not sure how. I'd like to not have to essentially duplicate the script's size by two by repeating [someStuff] for each case.

Thank you.

2 Answers 2

2
if [ $# -eq 0 ]; then
    read -p "File: " altfile
    set "$altfile"
fi

for inputFile in "$@"
    ...etc

EDIT: if you want to allow multiple filenames to be read in, you can either do that on a single line with spaces separating them:

if [ $# -eq 0 ]; then
    read -p "File(s): " -a altfiles
    set "${altfiles[@]}"
fi

or one per line:

if [ $# -eq 0 ]; then
    altfiles=()
    echo "Enter filenames one per line, enter Control-D after the last filename"
    while read -p "File: " altfile; do
        altfiles+=("$altfile")
    done
    set "${altfiles[@]}"
fi

Note that both of these will remove backslashes from the filenames, meaning that you can drag files from the Finder and they'll get parsed correctly, but entering filenames with funny characters by hand may not work as you'd expect.

4
  • Wouldn't that solution only allow me to work with one file at a time, in terms of the read option? It's important to be able to read multiple files/directories, else it would be a regression.
    – user137369
    Jun 9, 2012 at 10:52
  • That seems to work exactly as intended (the single line example), now I'm just struggling with the “else” part. As in, adding to that if that uses read, what should I put in the else part that uses the arguments passed through the command line when calling the script to say “set each of the $@ arguments to the ${altfiles[@]}”?
    – user137369
    Jun 9, 2012 at 23:46
  • You don't need an else part. Just use for inputFile in "$@" and it'll use either the original arguments or those read in later (and set with `set) as appropriate. Jun 10, 2012 at 6:27
  • Few lines, and seems to work great. Thank you.
    – user137369
    Jun 10, 2012 at 14:12
1

Assuming your backup plan is to provide a list of files on standard input, one per line, and your main plan is to provide a list of files on the command line, one per arg:

#!/bin/bash
declare -a files # let's use an array to avoid whitespace tokenization
let filecnt=0
if [ "$#" -gt 0 ]; then
    # we have args!
    for f in "$@"; do
        files[$filecnt]="$f"
        ((filecnt++))
    done
else
    # we have filenames on stdin!
    while read -e line; do
        files[$filecnt]="$line"
        ((filecnt++))
    done
fi

# now do whatever you need to do with the filenames
for filename in "${files[@]}"; do
    echo "$filename"
done

As is usual on Unix, Ctrl-D on its own line ends stdin, and allows the program to continue.

But you mentioned "drop", which makes me think what you'd really like is a drag-n-drop capability. For that, Platypus may be something to look into. I believe it will bundle your script into an app that makes a drag-n-drop target for you and passes the dropped files as args to your script.

Update: If you wanted to have files with just any old whitespace between them, so multiple files could be on the same line, replace that while loop with:

while read -ea lfiles; do
    for f in "${lfiles[@]}"; do 
        files[$filecnt]=$f
        ((filecnt++))
    done
done

Or if you expect only one line with all the files on it, you can simplify this whole while loop to:

read -ea files

Be aware, however, that with this approach, you must type a backslash in front of any space that you actually want to be part of your file or directory path, or the script will parse your pathname as multiple separate files! (Drag-n-drop from the Finder onto the Terminal will automatically do this backslash-escaping for you, though.)

7
  • The read part is not working for me, it just lets me add files indefinitely without doing anything. Also, will that force the user to add a file and make a new line, or can they add all the files on the same line (separated by spaces)?
    – user137369
    Jun 9, 2012 at 11:07
  • I know both platypus and dropscript, and they work fine to an extent, but earlier on the script I ask users for a number (using read) and that those apps can't handle. Unless I end up using oascript to do that number asking, but I'd rather craft a full terminal-only script first.
    – user137369
    Jun 9, 2012 at 11:11
  • For this solution, the way to signal the end of stdin (so the program will continue) is to type Ctrl-D on its own line. This is a pretty common unix-ism - you may be familiar with this way of closing stdin if you've worked with the Python and Ruby interpreters, for example. It's not exactly intuitive, but the nice thing is it works perfectly with passing in the contents of a file as your stdin as well: myscript < filenames.txt.
    – Owen S.
    Jun 9, 2012 at 18:55
  • This particular solution forces one filename per line. I'll add the on-the-same line solution as well, but it's dangerous in Mac OS X.
    – Owen S.
    Jun 9, 2012 at 18:59
  • I know, I'm counting on this terminal escaping to do the job, but I still make some escaping inside the script later, so I think I've got that covered. Thank you, I think that works, but I'd just like to know if it's possible to not need the Ctrl-D (i.e. set all the files on one line, press return, and it executes). If that is possible, it'd be just as I intended.
    – user137369
    Jun 9, 2012 at 23:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .