Bash shortcut for take the variable that exists from many

Viewed 96

I've had this question for awhile. In my work that uses bash scripting there are many instances where I have to save a filepath to a variable for use in a later command. We use files with suffixes T1w, T2w, and mask and in many cases we have a preferential file (e.g. T1ws) but, in their absence, will use another file (e.g. T2w). I usually use the following bash syntax to search for the appropriate file and save it to a variable (given I am searching in my current directory):

if [ -f xxx_T1w ]; then
  var1=xxx_T1w
elif [ -f xxx_T2w ]; then
  var1=xxx_T2w
fi

So ok fine, it works but I was always curious if there was a way in bash to code something like: If one of two given files exists, take the one that exists, and if both exist, take the first file. I'm horrendous at pseudocode but maybe something like:

if [ -f file_T1w ] || [ -f file_T2w ]; then
  var1=existing file
fi

I'm reusing this code a lot and sometimes the elif statements get pretty long if have a longer list of preferential files we can walk through and I've long wanted to know if there was a more compact way to do this.

3 Answers

The most promising thing I can think of is a loop:

for f in file1 file2; do
    if [ -f "$f" ]; then
        var="$f"
        break
    fi
done

You can shorten the code by using && if you like, at the expense of supporting the errexit shell option. For just two files, the benefits of this approach are questionable, since it's about the same amount of code as the if/elif statement you already wrote, and it's probably marginally less clear what it's trying to do. But for many files this will be significantly more concise.

And as others have suggested, I think this is definitely a good thing to wrap up in a shell function. Keep in mind that variable assignments are not (necessarily) scoped to shell functions in bash, so you'll have no problem setting a variable from inside the function and using it elsewhere; however, you could also consider having the function print the name of the found file to standard output if that's more useful to you.

If you put this in a function, you can actually use the shell's built-in ability to loop over positional parameters, like this:

find_file() {
    for f; do
        if [ -f "$f" ]; then
            var="$f"
            return
        fi
    done
}

Use this Perl one-liner (it works with 1 or more files as arguments):

var1=$( perl -MList::Util='first' -le 'print first { -f $_ } @ARGV;' xxx_T1w xxx_T2w )

Examples (note that if none of the listed files exist, $var1 will be empty):

$ rm xxx_T1w
$ touch xxx_T2w
$ var1=$( perl -MList::Util='first' -le 'print first { -f $_ } @ARGV;' xxx_T1w xxx_T2w )
$ echo ${var1}
xxx_T2w
$ var1=$( perl -MList::Util='first' -le 'print first { -f $_ } @ARGV;' xxx_T2w xxx_T1w )
$ echo ${var1}                                                                          
xxx_T2w
$ var1=$( perl -MList::Util='first' -le 'print first { -f $_ } @ARGV;' xxx_T1w )        
$ echo ${var1}                                                                  

$

The Perl one-liner uses these command line flags:
-e : Tells Perl to look for code in-line, instead of in a file.
-l : Strip the input line separator ("\n" on *NIX by default) before executing the code in-line, and append it when printing.
-MList::Util='first' : use module List::Util, and import from it subroutine first. This standard library module is included with your Perl distribution (nothing extra to install).

@ARGV : array of the command line arguments (here, files).

print first { -f $_ } @ARGV; : print the first of the files listed as arguments on the command line which exists AND which is a file (not a directory).

SEE ALSO:
perldoc perlrun: how to execute the Perl interpreter: command line switches
perldoc perlvar: Perl predefined variables
List::Util::first

Bash functions return only status code, so there is no return value to assign to a variable, like you do when using a standard programming language, like var = get_file(file1 file2).

But you can define a function and have this convenient way

get_file var file1 file2 file3

which is short reusable code you could have in your scripts.


Doing a modification to @David Z's answer, your function can reference the name of the first argument, and after shift, iterate for the rest of the arguments to test them. There is a pitfall here, see this, you have to avoid circular name reference, I would choose a variable name difficult to exist in the rest of your scripts.

function get_file() {
    declare -n get_file_var=$1
    shift
    for f; do
        if [ -f "$f" ]; then
            get_file_var="$f"
            return 0
        fi
    done
    return 1
}

The return status can be used to determine if any assignment happened.

get_file var file1 file2 file3
echo $?
echo $var

or this:

if get_file var file1 file2; then 
    echo var was assigned $var
else
    echo no file assigned # var can be empty string or keep its previous value
fi

You could also call it with an array:

arr=("missing filename" file1 file2)
get_file x "${arr[@]}"
Related