0

I'm writing a script where I want to error out if the file I'm searching for exists in multiple locations, and tell the user the locations (the find results). So I've got a find like:

file_location=$(find $dir -name $file -print)

I'm thinking it should be simple to see if the file is found in multiple places, but I must not be matching what find uses to separate results with (seems like space sometimes, and a newline others). As such, rather than matching on that, I want to see if there are any characters after $file in $file_location.

I'm checking for

echo "$file_location" | grep -q "${file}."; then

and this still doesn't work. So I guess I don't care what I use, except I want to capture $file_location as a result of the find, and then check that. Can you suggest a good way?

Ray
  • 5,885
  • 16
  • 61
  • 97

6 Answers6

2

Something like below if you want to avoid errors on eols and such

files=() 
while IFS= read -d $'\0' -r match; do 
  files+=("$match") 
done < <(find "$dir" -name "$file" -print0) 
(${#files[@]} > 1) && printf '%s\n' "${files[@]}"

Or in bash 4+

shopt -s globstar dotglob 
files=("$dir"/**/"$file") 
((${#files[@]} > 1)) && printf '%s\n' "${files[@]}"
Reinstate Monica Please
  • 11,123
  • 3
  • 27
  • 48
1

If you specify the full name in the find command, the matches on name will be unique. That is, if you say find -name "hello.txt", just files named hello.txt will be found.

What you can do is something like

find $dir -name $file -printf '.'
                      ^^^^^^^^^^^

this will print as many . as matches are found. Then, to see how many files are found with this name it is just a matter of counting the number of dots you got as output.

fedorqui
  • 275,237
  • 103
  • 548
  • 598
  • I want to tell the user the locations, if there are more than one, that the find command finds. Since find is a bit slow, I would like to run it once. – Ray Jul 31 '15 at 21:44
  • Uhms, you should have mentioned this in your question. You may want to go through [How to loop through file names returned by find?](http://stackoverflow.com/a/9612232/1983854) – fedorqui Jul 31 '15 at 21:46
  • @Ray it would be good if you update your question indicating what edge cases you want to cover. I mentioned that other question as it is the canonical. So probably the best solution is to store the results in an array, `var=( $(find ...) )` and then use the standard ways to count the number of elements in it and, eventually, print all its values. – fedorqui Jul 31 '15 at 22:59
1
found=$(find "$dir" -name "$file" -ls)
count=$(wc -l <<< "$found")
if [ "$count" -gt 1 ]
then
  echo "I found more than one:"
  echo "$found"
fi

For zero matches found you will still receive a 1 because of the intransparent way the shell strips a trailing newline with the $() operator, so effectively one line output and zero lines output are both one line in the end. See xxd <<< "" for demonstration of the automatic appending of a newline when used as input again. A simple way to circumvent this is to add a fake newline in the beginning of the string, so no empty string can happen: found=$(echo; find …), and then subtract one from the number of lines.

EDIT: I changed the usage of -printf "%p\n" in my answer to -ls which performs a proper quoting of newlines. Otherwise file names with newlines in them would mess up the counting.

Alfe
  • 56,346
  • 20
  • 107
  • 159
  • Thanks I'm liking this way the best. I also test if the file wasn't found. Do you know why count is still 1 if the file is not found? – Ray Jul 31 '15 at 22:18
  • I extended my answer to explain this wart. – Alfe Jul 31 '15 at 22:30
  • May I ask how you'd compare your answer to: if [[ $file_location == *${file}* ]] ? – Ray Jul 31 '15 at 22:31
  • May I ask how you'd compare your answer to: if [[ $file_location == \*${file}\* ]] ? I tried this after. My question now properly showing the "*" – Ray Jul 31 '15 at 22:36
  • I would not use an equality comparison of a list with a string to find out of the list has exactly one element. Too brittle and confusing for any other developer. – Alfe Jul 31 '15 at 22:39
  • Thanks. I like your answer addition. My comparison is trying to see if the location contains any characters after the filename. This seems brittle to you? – Ray Jul 31 '15 at 22:50
  • Ah, that's the intention. Then I would prefer the `case` operator of the shell: `case "$file_location" in; "${file}"?*) echo "Yay!";; *) echo "nope";; esac` – Alfe Jul 31 '15 at 22:54
  • 2
    The standard way to cover this case is by setting the `nullglob`. See Greg's wiki on this http://mywiki.wooledge.org/glob#nullglob – fedorqui Jul 31 '15 at 22:58
  • @BroSlow, ...that's assigning find's output to a variable, Not a great idea to be newline-delimiting a list of filenames, but not a FAQ 50 violation. – Charles Duffy Aug 01 '15 at 03:40
  • @CharlesDuffy Guess it doesn't matter as much here, still errors with eol. – Reinstate Monica Please Aug 01 '15 at 03:40
  • Sure -- if you have a filename with a newline, it'll count as two names, which is bad; I just didn't see why it was a FAQ-50 problem. – Charles Duffy Aug 01 '15 at 03:41
  • Format `%p` quotes newlines in filenames as `?`. See the section `UNUSUAL FILENAMES` in the man page of `find` for details which way of outputting quotes how. For my answer this means that it should work without any problem, even for the weirdest of possible filenames. – Alfe Aug 01 '15 at 20:48
  • 1
    @Alfe Not a big issue, but that doesn't quite work here since `If the output is not going to a terminal, it is printed as-is`. – Reinstate Monica Please Aug 04 '15 at 00:30
  • I stand corrected! Then I'd propose `found=$(find "$dir" -name "$file" -ls)` which is ① shorter and ② gives out even more information. And quotes newlines also on non-terminals ;-) – Alfe Aug 04 '15 at 08:56
1

No need for find here if you're running a new (4.0+) bash which can do recursive globbing itself; just load glob results directly into a shell array, and check its length:

shopt -s nullglob globstar # enable recursive globbing, and null results
file_locations=( "$dir"/**/"$file" )
echo "${#file_locations[@]} files named $file found under $dir; they are:"
printf '  %q\n' "${file_locations[@]}"

If you don't want to mess with nullglob, then:

shopt -s globstar # enable recursive globbing
file_locations=( "$dir"/**/"$file" )

# without nullglob, a failed match will return the glob expression itself
# to test for this, see if our first entry exists
if [[ -e ${file_locations[0]} ]]; then
  echo "No instances of $file found under $dir"
else
  echo "${#file_locations[@]} files named $file found under $dir; they are:"
  printf '  %q\n' "${file_locations[@]}"
fi

You can still use an array to unambiguously read find results on old versions of bash; unlike more naive approaches, this will work even when file or directory names contain literal newlines:

file_locations=( )
while IFS= read -r -d '' filename; do
  file_locations+=( "$filename" )
done < <(find "$dir" -type f -name "$file" -print0)
echo "${#file_locations[@]} files named $file found under $dir; they are:"
printf '  %q\n' "${file_locations[@]}"
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
0

I recommend using:

find . -name blong.txt -print0

Which tells find to join its output together with null \0 characters. Makes it easier to use awk with the -F flag or xargs with the -0 flag.

Tripp Kinetics
  • 5,178
  • 2
  • 23
  • 37
  • Could you write the whole if statement please, saving the result of the find in a variable? – Ray Jul 31 '15 at 21:42
0

Try:

N=0

for i in `find $dir -name $file -printf '. '`
do
    N=$((N+1))
done

echo $N
  • this is quite buggy and may fail in many cases, yet alone when the result contains spaces. – fedorqui Jul 31 '15 at 21:55
  • @fedorqui: I combined it with your post and added a space between the '.' – Jeremiah Dicharry Jul 31 '15 at 22:00
  • well then this has two problems: one is that you are not explaining anything at all about the answer; two is that you are assuming things that weren't said. OP mentioned the file name can contain spaces, but the `print '. '` doesn't make sense. – fedorqui Jul 31 '15 at 22:02
  • `-printf '. '` is a way to avoid dealing with filenames that aren't under control. It creates output of `. . . .` with one `.` per-found file. – Etan Reisner Jul 31 '15 at 22:20
  • @EtanReisner yes, I know, I was using it myself in my answer. My point here is that saying `-printf '. '` without any kind of explanation was a bit... unexplained and looked weird (even though it would work). – fedorqui Jul 31 '15 at 22:57
  • 1
    This doesn't work for all directory names -- needs more quotes. Think about what happens if `$dir` or `$file` contains spaces. Yes, it solves the problem of unexpected characters in the output from `find`, but not that of unexpected content in the input variables. – Charles Duffy Aug 01 '15 at 03:00
  • @fedorqui Ah, I misunderstood that bit of your comment then, my apologies. – Etan Reisner Aug 02 '15 at 19:02