12

I want to use find to find files in a set of folders restricted by wildcards, but where there are spaces in the path name.

From the command line, this is easy. The following examples all work.

find  te*/my\ files/more   -print
find  te*/'my files'/more  -print
find  te*/my' 'files/more  -print

These will find files in, for example, terminal/my files/more and tepid/my files/more.

However, I need this to be part of a script; what I need is something like this:

SEARCH='te*/my\ files/more'
find ${SEARCH} -print

Unfortunately, whatever I do, I don't seem to be able to mix wildcards and spaces in a find command within a script. The above example returns the following errors (note the unexpected doubling of the backslash):

find: ‘te*/my\\’: No such file or directory
find: ‘files/more’: No such file or directory

Trying to use quotes also fails.

SEARCH="te*/'my files'/more"
find ${SEARCH} -print

This returns the following errors, having ignored the meaning of the quotes:

find: ‘te*/'my’: No such file or directory
find: ‘files'/more’: No such file or directory

Here's one more example.

SEARCH='te*/my files/more'
find ${SEARCH} -print

As expected:

find: ‘te*/my’: No such file or directory
find: ‘files/more’: No such file or directory

Every variation that I've tried returns an error.

I have a workaround, which is potentially dangerous because it returns too many folders. I convert all spaces to a question mark (single-character wildcard) like this:

SEARCH='te*/my files/more'
SEARCH=${SEARCH// /?}       # Convert every space to a question mark.
find ${SEARCH} -print

This is the equivalent of:

find te*/my?files/more -print

This returns not only the correct folders but also terse/myxfiles/more, which it's not supposed to.

How can I achieve what I'm trying to do? Google has not helped me :(

αғsнιη
  • 35,660
Paddy Landau
  • 4,548
  • @KasiyA I'm using bash; you must be using something else, as I've not seen that construction before. The command results in SEARCH: command not found with the command find -print being executed. – Paddy Landau Sep 08 '14 at 10:13
  • A shot in the dark, but what about quoting? find "${SEARCH}" -print? – Alaa Ali Nov 22 '14 at 15:48
  • @AlaaAli No, it doesn't work, because quoting prevents Bash from using the wildcards. It will look for a path specifically with the name (in my example) te*/'my files'/more. – Paddy Landau Nov 22 '14 at 20:38

4 Answers4

11

The exact same command should work fine in a script:

#!/usr/bin/env bash
find  te*/my\ files/ -print

If you need to have it as a variable, it gets a bit more complex:

#!/usr/bin/env bash
search='te*/my\ files/'
eval find "$search" -print

WARNING:

Using eval like that is not safe and can result in executing arbitrary and possibly harmful code if your file names can contain certain characters. See bash FAQ 48 for details.

It's better to pass the path as an argument:

#!/usr/bin/env bash
find "$@" -name "file*"

Another approach is to avoid find altogether and use bash's extended globbing features and globs:

#!/usr/bin/env bash
shopt -s globstar
for file in te*/my\ files/**; do echo "$file"; done

The globstar bash option lets you use ** to match recursively:

globstar
      If set, the pattern ** used in a pathname expansion con‐
      text will match all files and zero or  more  directories
      and  subdirectories.  If the pattern is followed by a /,
      only directories and subdirectories match.

To make it act 100% like find and include dotfiles (hidden files), use

#!/usr/bin/env bash
shopt -s globstar
shopt -s dotglob
for file in te*/my\ files/**; do echo "$file"; done

You can even echo them directly without the loop:

echo te*/my\ files/**
terdon
  • 100,812
  • 2
    I have set this as the answer because of the helpful comments that terdon has made (not forgetting the helpful comments from others). I have used Bash's globbing ability on the command line to pass multiple paths to my script, instead of the script trying to sort it out. It works well. – Paddy Landau Nov 22 '14 at 15:49
2

How about arrays?

$ tree Desktop/ Documents/
Desktop/
└── my folder
    └── more
        └── file
Documents/
└── my folder
    ├── folder
    └── more

5 directories, 1 file
$ SEARCH=(D*/my\ folder)
$ find "${SEARCH[@]}" 
Desktop/my folder
Desktop/my folder/more
Desktop/my folder/more/file
Documents/my folder
Documents/my folder/more
Documents/my folder/folder

(*) expands into an array of whatever matches the wildcard. And "${SEARCH[@]}" expands into all the elements in the array ([@]), with each individually quoted.

Belatedly, I realise find itself should be capable of this. Something like:

find . -path 'D*/my folder/more/'
muru
  • 197,895
  • 55
  • 485
  • 740
  • Clever idea, but alas, it doesn't work. Why? Because the path itself is held in a variable; hence, INPUTPATH='te*/my files/more and SEARCH=(${INPUTPATH}). No matter how I vary the way I do this, I still end up with a non-functional result. This seems impossible! – Paddy Landau Nov 21 '14 at 14:04
  • This is all true of course, but the OP needs to do it in a script. That changes things since the expansion of wildcards gets significantly more complex and this doesn't work. – terdon Nov 21 '14 at 14:05
  • @PaddyLandau In which case, why can you not use find's -path filter? It uses wildcards and definitely does not need expansion. – muru Nov 21 '14 at 14:30
  • @muru That's interesting; I didn't know about -path. In the past 10 minutes, though, I have figured out the answer: use eval! It seems to be simpler than -path. – Paddy Landau Nov 21 '14 at 14:49
  • @PaddyLandau Long term, though you should consider the path you have taken. :) eval is considered terrible practice. terdon's answer warned of it, but I can't find it now. – muru Nov 21 '14 at 15:05
  • @PaddyLandau yes. I have deleted my answer which was using eval (undeleted now) because I was trying to to it the "better" way using printf but I couldn't get it to work. This is almost certainly an XY problem. You should probably explain more about what your script is doing and why this needs to be hardcoded into it. I am pretty sure there will be better ways to do it. – terdon Nov 21 '14 at 15:14
  • @terdon I have a script that searches for files within a given path (which may contain wildcards), and the script performs a complex action on each of the found files. I need a way to pass that path to the find command within the script. Perhaps I need to rethink the entire process, given the difficulty. – Paddy Landau Nov 22 '14 at 12:39
  • @PaddyLandau since this is too old to migrate, you might want to post a question on [unix.se] explaining what the script does and what you need. There may be a way to do this as you have been trying to or there might be a different solution that will only become apparent if we understand what the script does. There tends to be more scripting expertise on U&L so you're likely to get a solution there. Just try and change the question enough so it's not considered cross-posting. In the meantime, I'll offer a bounty on this one. – terdon Nov 22 '14 at 14:06
  • @terdon posting a bounty is pointless, since the problem is not completely specified. It is not clear how the path is provided to the script, and why the path is specified in the form given in the first comment in this question (i.e., why stick it in a variable, and then try to expand it - what prevents it being used directly in the array). Until then this question will likely attract random stabs in the dark. Paddy, please edit the question to add the information you have given in this comment thread. – muru Nov 22 '14 at 14:43
  • @muru perhaps but I'm also curious how to do the small part of thw problem that is specified. As we both told the OP, this is probably not the best way but how do you pass a variable with both wildcards and spaces to find from within a script? The answer might be "don't" but I'm not sure. – terdon Nov 22 '14 at 14:46
  • @terdon I don't think that will be productive either. Consider: 'How do I expand variables in single-quoted strings?' Ans: a) You don't, use double quotes or b) use eval instead. Similarly, here: you don't, use find's -path, or b) use eval instead. – muru Nov 22 '14 at 15:04
  • Here's what i had in mind: http://weblogs.asp.net/alex_papadimoulis/408925 – muru Nov 22 '14 at 15:09
  • 2
    @terdon and muru and everybody: Thank you. I have heard what you have all said, and I realised that I should be making my script do one thing, and letting Bash globbing pass multiple files or paths to the script. I have modified my script thus. It works well and fits better with the Linux philosophy. Thank you again! – Paddy Landau Nov 22 '14 at 15:47
1

It's a little dated now but, if that can help anybody with this question, using RE's collating symbol [[.space.]] without quoting $SEARCH variable within find command line arguments, is working as long as there's no special characters into the expanded path-names in place of the asterisk.

set -x
mkdir -p te{flon,nnis,rrine}/my\ files/more
SEARCH=te*/my[[.space.]]files/more
find $SEARCH

give the following results:

+ mkdir -p 'teflon/my files/more' 'tennis/my files/more' 'terrine/my files/more'
+ SEARCH='te*/my[[.space.]]files/more'
+ find 'teflon/my files/more' 'tennis/my files/more' 'terrine/my files/more'
teflon/my files/more
tennis/my files/more
terrine/my files/more

To prevent globing unwanted chars, One can replace the asterisk (*) by any collating elements (characters):

set -x
mkdir -p -- te{,-,_}{flon,nnis,rrine}/my\ files/more
SEARCH=te[[:alnum:][.space.][.hyphen.][.underscore.]]*/my[[.space.]]files/more
find $SEARCH

giving the following results:

+ mkdir -p -- 'teflon/my files/more' 'tennis/my files/more' 'terrine/my files/more' \
'te-flon/my files/more' 'te-nnis/my files/more' 'te-rrine/my files/more' \
'te_flon/my files/more' 'te_nnis/my files/more' 'te_rrine/my files/more'
+ SEARCH='te[[:alnum:][.space.][.hyphen.][.underscore.]]*/my[[.space.]]files/more'
+ find 'te-flon/my files/more' 'te_flon/my files/more' 'teflon/my files/more' \
'te-nnis/my files/more' 'te_nnis/my files/more' 'tennis/my files/more' \
'te-rrine/my files/more' 'te_rrine/my files/more' 'terrine/my files/more'
te-flon/my files/more
te_flon/my files/more
teflon/my files/more
te-nnis/my files/more
te_nnis/my files/more
tennis/my files/more
te-rrine/my files/more
te_rrine/my files/more
terrine/my files/more

Note that to shorten the line, [.hyphen.] can be replaced by either [.-.] or - and [.underscore.] can be replaced by either [._.] or _.



Truncating lines with backslashes (\) added by myself for readability purpose.

daneros
  • 11
  • 2
0

I have finally found out the answer.

Add a backslash to all spaces:

SEARCH='te*/my files/more'
SEARCH=${SEARCH// /\\ }

At this point, SEARCH contains te*/my\ files/more.

Then, use eval.

eval find ${SEARCH} -print

It's that simple! Using eval bypasses the interpretation that ${SEARCH} is from a variable.

Paddy Landau
  • 4,548
  • @terdon Thank you for the warning. Back to the drawing board! – Paddy Landau Nov 22 '14 at 10:55
  • Yeah, this is surprisingly tricky. I just updated my answer with another approach, why not use globbing instead? If that still doesn't work, I suggest you post a new question on [unix.se] explaining what your final objective is and why you need to have the pattern as a variable. This type of thing is likelier to get a better answer there. – terdon Nov 22 '14 at 11:47