Score:4

Use `find` for filename with []

vu flag

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?

Score:7
us flag

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).

Score:4
hr flag

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.)

br flag
JoL
"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.
Score:2
ne flag
Mic

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"
MaanDoabeDa avatar
vu flag
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.
Kamil Maciorowski avatar
cn flag
(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?
Mic avatar
ne flag
Mic
@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.
Score:1
us flag

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
Score:0
tn flag

Maybe parse the filename with this tr command:

$ tr '[]' '*' <<< "$var"
R. Kelly - Step In The Name Of Love *mp3clan.com*.mp3
MaanDoabeDa avatar
vu flag
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`
Gilles Quenot avatar
tn flag
It's just a simple `sed` like one-liner. Could be doable with `sed`
MaanDoabeDa avatar
vu flag
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!
MaanDoabeDa avatar
vu flag
Hey Gilles, thanks for the reminder. I had to run to a potluck 3 hours ago
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.