ferengi
ferengi

Reputation: 477

How can I list files with multiple criteria in bash and select a random one?

I have a directory of files of this nature:

1985-08-28_state.txt  2001-04-29_state.txt  2016-12-29_state.txt
1985-08-29_state.txt  2001-04-30_state.txt  2016-12-30_state.txt
1985-08-30_state.txt  2001-05-01_state.txt  2016-12-31_state.txt

And I want to select all files for May-June and pick a random one from the list.

#!/bin/bash -u

states_path=./daily_states/
date="*-05-*_state.txt *-06-*_state.txt" #Patterns for May-June files
statefile=`ls ${states_path}/$date| shuf -n 1`

printf "$statefile \n"

but this is not able to access the second argument.

Warning

ls: cannot access *-06-*_state.txt: No such file or directory

Why is this happening and how can I fix this?

I am open to any other way of doing this.

Alternate answer

I thought of another way of doing this. Not the most elegant solution. But I guess it works?

states_path=/daily_states/

statefile[0]=`ls ${states_path}/*-05-*_state.txt | shuf -n 1`
statefile[1]=`ls ${states_path}/*-06-*_state.txt | shuf -n 1`

randstate=$(($RANDOM % 2))

echo "${statefile[$randstate]}"

Upvotes: 1

Views: 928

Answers (2)

ghoti
ghoti

Reputation: 46856

Let's look at how your variables expand. You set:

date="*-05-*_state.txt *-06-*_state.txt"

And then you set $statefile with the output of the command:

ls ${states_path}/*-05-*_state.txt *-06-*_state.txt| shuf -n 1

Now do you see the problem?

A better way to handle this might be using an array to store the patterns, and another array to collect filenames, since parsing ls is problematic:

#!/usr/bin/env bash

states_path=./daily_states/
patterns=("*-05-*_state.txt" "*-06-*_state.txt")

a=()                    # initialize an empty array

shopt -s nullglob       # if no expansion occurs, expand to null

# Step through your patterns...
for monthpat in "${patterns[@]}"; do
  # And add files matching the pattern to the array.
  a+=( $states_path/$monthpat )
done

# Print the list
declare -p a
# or if you prefer,
printf '%s\n' "${a[@]}"

# Or just print one random entry:
printf 'random: %s\n' "${a[ $(( $RANDOM % ${#a[@]} )) ]}"

Note that I'm not using shuf, because it's not part of bash, and not available on the operating systems I use bash (FreeBSD and macOS).


If you KNOW that you're only interested in these two months worth of files, you could express them as a single glob. Thus:

a=( $states_path/*-0[56]-*_state.txt )
printf '%s\n' "${a[$(($RANDOM%${#a[@]}))]}"

Upvotes: 1

Socowi
Socowi

Reputation: 27225

Your short solution has a few issues:

  • Parses output of ls. Paths with spaces and so on might cause problems.
  • The distribution of chosen files may not be uniform. If the first glob returns 100 files and the second glob returns one file, the chance of getting that one file from the second glob is way higher than getting a specific file from the first glob.

Here is an alternative that fixes these problems and is also way shorter:

 shuf -en1 daily_states/*-05-*_state.txt daily_states/*-06-*_state.txt

Both globs can also be combined as follows.

 shuf -en1 daily_states/*-0{5,6}-*_state.txt

As ghoti mentioned, consider using shopt -s nullglob -- better safe than sorry.

Upvotes: 1

Related Questions