4

Background: I am trying to find music files that are mentioned in an xml file to organize my music playlists. I'm writing a script to pull out the filenames from the xml tag to automatically find the files.

My music lives in $musicroot, so I'm issuing find there. But I can't find the following file because of the [] characters.

find $musicroot -name "R. Kelly - Step In The Name Of Love [mp3clan.com].mp3" -type f

returns nothing, but if I escape the [] then it works.

find $musicroot -name "R. Kelly - Step In The Name Of Love \[mp3clan.com\].mp3" -type f
/mnt/Data/Dynamic/Multimedia/Music/Not Desi/Party/R. Kelly - Step In The Name Of Love [mp3clan.com].mp3

Note that in my script, the song file is iterated over in a loop of songs and exists in a variable $song. Not sure if that makes a difference.

I'd really like to be able to just pass in the filenames into find in my scripts without having to worry about escaping characters like this, but I can't seem to find anything on this online. Probably because it's hard to search for find as find is such a common English word.

Is this possible?

5 Answers 5

7

If you're just looking for this file, this could be done using only bash:

shopt -s globstar
printf "%s\n" "$musicroot"/**/"R. Kelly - Step In The Name Of Love [mp3clan.com].mp3"

In bash, globstar enables the use of ** for recursive globbing. And of course once the filename is in quotes, bash doesn't do further wildcard expansion on it (and for that reason, you should be quoting $musicroot too).

4

The bash shell's printf builtin has a %q format specifier that

      %q     causes  printf  to output the corresponding argument in a
             format that can be reused as shell input.

It's not exactly clear from the documentation exactly what "reused as shell input" means, however it at least escapes whitespace and the usual filename generation (glob) characters that are special in the find command's -name argument:

$ x="fo*o [ba?r]"
$ printf '%q\n' "$x"
fo\*o\ \[ba\?r\]

So you could try

song="R. Kelly - Step In The Name Of Love [mp3clan.com].mp3"
find dir -name "$(printf '%q' "$song")"

Note that if you have zsh, you can use its q parameter expansion flag to do the same more directly:

 ~ % song="R. Kelly - Step In The Name Of Love [mp3clan.com].mp3"
 ~ % find dir -name $song:q
dir/bar/R. Kelly - Step In The Name Of Love [mp3clan.com].mp3

(The bash shell has a superficially similar @Q expansion flag, however it operates differently - essentially just hard-quoting the whole string.)

1
  • "format that can be reused as shell input" means that it's quoted/escaped using the shell's syntax, desirable for when you want to copy-paste as an argument to another command. For example: printf '%q' '~foo$bar&baz!qux ax' outputs \~foo\$bar\&baz\!qux$'\t'ax. That might be worth keeping in mind if there's such symbols in the music titles.
    – JoL
    Dec 4, 2022 at 17:42
2

grep has a --fixed-strings option that does what you want and you can use it with find:

find $musicroot -type f | grep --fixed-strings "R. Kelly - Step In The Name Of Love [mp3clan.com].mp3"
3
  • This should probably work. It's surprising to me that find itself doesn't have some similar option. Perhaps it does and I just didn't find it in the man page when I briefly looked through. Dec 3, 2022 at 22:02
  • 2
    (1) What if the desired string is in the name of some directory? (2) What if the string is a part of a name, but not the whole name? (3) What if a matching file is in a directory the name of which contains a newline character? Dec 5, 2022 at 5:46
  • @KamilMaciorowski good questions. It will still work for case #1 and #2 but not #3. For finding music files like discussed in the question, I argue would this is a good simple and effective solution. But you are right, it is not going to work for all the corner cases.
    – Mic
    Dec 5, 2022 at 21:04
0

Maybe parse the filename with this tr command:

$ tr '[]' '*' <<< "$var"
R. Kelly - Step In The Name Of Love *mp3clan.com*.mp3
4
  • I thought to do something like this, but I don't know what other characters will trip this issue again, so I'd have to maintain some set of replacement rules. So an approach that is able to find the file using its exact filename without modification would be preferable. Though perhaps I would appreciate this answer more if I knew anything about perl Dec 3, 2022 at 22:01
  • It's just a simple sed like one-liner. Could be doable with sed Dec 3, 2022 at 22:10
  • Very cool! I didn't know about tr. It seems you could get the same result with just [] rather than [][], at least in this particular case. This is a handy thing to know about, thanks! Dec 3, 2022 at 22:37
  • Hey Gilles, thanks for the reminder. I had to run to a potluck 3 hours ago Dec 4, 2022 at 2:48
0

As you noticed, the -name option (and its friends) of find(1) award special treatment to some characters as described in glob(7). Fortunately, all these special characters can be escaped with a backslash. Therefore, one way to work around this issue is to escape these characters before passing them to -name.

The most portable way to escape these characters on the command-line is probably sed(1):

printf '%s' "some s[t]ra*nge? string" | sed -e 's/[][?*\\]/\\&/g'

Example:

$ song='R. Kelly - Step In The Name Of Love [mp3clan.com].mp3'
$ printf '%s' "$song" | sed -e 's/[][?*\\]/\\&/g'; echo
R. Kelly - Step In The Name Of Love \[mp3clan.com\].mp3

Putting it together using command substitution:

find "$musicroot" -name "$(printf '%s' "$song" | sed -e 's/[][?*\\]/\\&/g')"

If you need to do this to a lot of strings this may take quite a while because each invocation of sed spawns a new process which is very tedious for the operating system. An approach with better performance would be to do all the substitutions with only one invocation of sed and read them in a loop:

printf '%s\0' "s[o]me" "s?range" "str*ngs" "${string_array[@]}" |
sed -z -e 's/[][?*\\]/\\&/g' |
while read -r -d '' escaped_string; do
    do_something_with "$escaped_string"
done

I’m using null-terminated lines here because line breaks are character in file path names. sed -z and read -d '' are used to handle these correctly but they’re not so portable.

If you need both the escaped and the unescaped string inside the loop we’ll use another trick:

printf '%s\0' "s[o]me" "s?range" "str*ngs" "${string_array[@]}" |
awk 'BEGIN{ RS=ORS="\0"; } { print; gsub(/[][?*\\]/, "\\\\&"); print; }' |
while read -r -d '' unescaped_string && read -r -d '' escaped_string; do
    do_something_with "$escaped_string"
    do_something_else_with "$unescaped_string"
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.