1

I want to select a range of files by using part of their name to be used with copying, moving, etc.

Random example list:

  1. alpha1a
  2. alpha2aa
  3. alpha3abc
  4. bravoA1
  5. bravoB2bb
  6. charlie
  7. deltaA1fdfd
  8. deltaA2dafa
  9. deltaA2222
  10. deltaC1
  11. ... (a bunch of other files)

I'd like to run some command that goes from alpha2* to deltaA*, which would select 2-9 in the list. This way I can select a range based on name, not worry how many files there actually are, and get no extras.

1
start=alpha2*
end=deltaA*

for file in *
do
    if [[ $file > $start && ( $file < $end || $file == $end ) ]]; then
        echo $file
    fi
done

Instead of echo, you can store the files in an array:

array+=($file)

And then use the array to copy, move the files... or simply perform the command within the for loop.

  • Will this really work? To me is seems you're using a numeral comparison on a string or at least the size of the file. – Brandon Sep 25 '16 at 7:07
  • 1
    @Brandon in [[ (in shells that have it) < > etc are string comparison and == is string/pattern match; for numeric you use -lt -gt -eq etc. This contrasts with (( and $(( which only do numeric and use < > = and more. – dave_thompson_085 Sep 25 '16 at 12:19
  • This isn't quite right at the bounds. The < and > operators are strict inequalities between strings, so they treat the * character literally, whereas it's a wildcard in $file == $end. alpha2 and alpha2% are not included because they don't pass $file > $start. The end bound is correct but only because end consists of non-wildcard characters followed by *. – Gilles 'SO- stop being evil' Sep 25 '16 at 22:36
1

Shell programming is not very good at lexicographic comparisons. In bash, ksh or zsh, you can use the < conditional operator to compare two strings (POSIX sh only has numeric comparisons).

run_in_range () {
  start_at="$1"; end_before="$2"; command="$3"; shift 3
  for item do
    if [[ ! ($x < $start_at) && ($x < $end_before) ]]; then
      "$command" "$item"
    fi
  done
}
run_in_range alpha2 deltaB some_command *

Note that I used the first excluded item as the second argument: deltaB is the smallest string that comes after all strings that begin with deltaA. If you want to pass deltaA as an argument, you can do it this way.

run_in_inclusive_range () {
  start_at="$1"; end_on_prefix="$2"; command="$3"; shift 3
  for item do
    if [[ ! ($x < $start_at) && ($x < $end_on_prefix || $x == "$end_on_prefix"*) ]]; then
      "$command" "$item"
    fi
  done
}
run_in_inclusive_range alpha2 deltaA some_command *

This code doesn't rely on the order of the wildcard: it works even if the list of items is not sorted. This makes the code more robust, but slightly slower since it has to go through the whole list.

Here's an alternative approach that relies only on POSIX features. It uses sort to do the string comparisons. This one assumes that there are no newlines in items. This function restores the default setting for IFS and globbing (it's possible but annoyingly hard to restore the ambient setting).

run_in_range () {
  IFS='
'; set -f
  start_at="$1"; end_before="$2"; command="$3"; shift 3
  set $({
          printf '%s1\n' "$@"
          printf '%s0\n' "$start_at" "$end_before"
        } | sort | sed '/0$/,/0$/ s/1$//p')
  unset IFS; set +f
  for item do
    "$command" "$item"
  done
}

Explanation:

  1. Build a list containing the items with 1 appended, and the boundaries with 0 appended.
  2. Sort the list.
  3. Print the lines between the two boundary lines (the lines ending in 0), but only the lines ending with 1 (i.e. the items), and without the trailing 1.
0

It seems to me that the problem is simpler to solve than it seems:

Let's assume that there is a way to get the list, one file per line.
For the present directory:

list="$(printf '%s\n' *)"

Just append the limits to the list and sort it:

nl='
'
lim1="alpha2"
lim2="deltaA"

echo -- "$list""$nl""$lim1""$nl""$lim2" | sort

Then start printing on lim1 (taking care that lim1 and lim2 do not match an existing filename) and stop on lim2.

Also, to make it simple, select the shortest string that is just before the file you want printed and also the shortest string that sorts just after the last file that needs to be selected.

What is useful is an exclusive list (one that doesn't include the limits),
but both solutions are provided.

Basic values:

list="$(printf '%s\n' *)"

lim1=alpha2
lim2=deltaA

A solution with awk for an inclusive list:

echo "awk----------------inclusive"
echo "$list""$nl""$lim1""$nl""$lim2" | sort |
    awk -v lim1="$lim1" -v lim2="$lim2" '$0==lim1,$0==lim2{print}'

A solution with awk for an exclusive list:

echo "awk----------------exclusive"
echo "$list""$nl""$lim1""$nl""$lim2" | sort |
    awk -v lim1="$lim1" -v lim2="$lim2" '$0==lim1{p=1;next};$0==lim2{p=0};p'

A solution with the shell for an inclusive list:

echo "shell---------------inclusive"
show=0
echo "$list""$nl""$lim1""$nl""$lim2" | sort |
    while IFS="$nl" read -r line; do
        [ "$line" = "$lim1" ] && show=1
        [ "$show" = "1"     ] && printf '%s\n' "$line"
        [ "$line" = "$lim2" ] && show=0
    done

And an exclusive solution for the shell:

show=0
echo "$list""$nl""$lim1""$nl""$lim2" | sort |
    while read -r line; do
        [ "$line" = "$lim2" ] && show=0
        [ "$show" = "1"     ] && printf '%s\n' "$line"
        [ "$line" = "$lim1" ] && show=1
    done

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.