6

I have a problem, with this simple script (pick a random file):

#!/usr/bin/env bash
set -x
srcDir="/home/user/Desktop/wallPapers/{dir1,dir2,dir3}"
randomFile=$(find "$srcDir" -type f -iname "*.jpg" | shuf -n 1)
printf '[%s]\n' $randomFile
set +x

The problem is that while I can type this at the command line (and works perfectly fine):

find /home/user/Desktop/wallPapers/{dir1,dir2,dir3} -type f -iname "*.jpg"

Then the bash debugging set-commands (set -x and +x) tells me, that for some reason bash both encloses the directory string with single quotation marks and it also replaces the double quotation marks with single quotation marks?

./script.sh
+ srcDir='/home/user/Desktop/wallPapers/{dir1,dir2,dir3}'
++ find '/home/user/Desktop/wallPapers/{dir1,dir2,dir3}' -type f -iname '"*.jpg"'
find: ‘/home/user/Desktop/wallPapers/{dir1,dir2,dir3}’: No such file or directory
+ randomFile=
+ printf '[%s]\n'
[]
+ set +x

I understand, this is what bash sees, when the script runs:

find '/home/user/Desktop/wallPapers/{dir1,dir2,dir3}' -type -iname '*.jpg'

And this causes the "No such file or directory"-message, very very annoying... I do not understand, why it inserts these single quotation marks, I want double quotation marks used instead, just like on the command line... Could anyone please explain, I would be happy for that, thanks!

  • It's not "*.jpg" that doesn't happen, it's {dir1,dir2,dir3}. – Charles Duffy Jun 5 '19 at 15:47
  • Thank you very much. On second thought: Isn't it both? I also don't think '.jpg' expands correctly (if there wasn't a single quotation mark around '.jpg', which was shown from inside the "set -x" and "set +x"-section, I would agree... But I honestly also do not know why bash encloses the directory and the file inside single quotation mark... – Okay Dokey Jun 5 '19 at 18:48
  • *.jpg, in find . -iname '*.jpg', is parsed by find, not by bash; it's expected that it not be expanded by the shell -- if it were, find would never see it. Thus, having a single layer of syntactic quotes (and no literal quotes) is entirely correct and desired in that case. ("Literal" quotes are quotes that are part of the data, and thus passed to find; "syntactic" quotes are quotes that are part of the shell syntax, and thus consumed by bash itself). – Charles Duffy Jun 5 '19 at 20:05
  • @Charles Duffy Ok, thank you. I'm not that used to the terminology about syntactic / literal quotes. But it's very helpful of you elaborate, thank you very much (eventually I'll hopefully learn it)... – Okay Dokey Jun 6 '19 at 15:26
11

Brace expansion doesn't occur within a variable assignment, as explained here:

Why do tilde prefixes expand prior to assignment, but braces don't

In other contexts, the quotes would have prevented brace expansion as well.

Even if you do manage to get srcDir to expand to a list of directories, quoting it again in the find command will cause it to be treated as a single argument instead of 3 separate paths.

Probably the right way to do this in bash is to use an array:

#!/usr/bin/env bash
set -x
srcDir=("/home/user/Desktop/wallPapers/"{dir1,dir2,dir3})
randomFile=$(find "${srcDir[@]}" -type f -iname "*.jpg" | shuf -n 1)
printf '[%s]\n' "$randomFile"
set +x
  • This is exactly the solution I was looking for, thank you very much. You keep using the variables, which I wanted to learn how to use. Also, I didn't realise/knew I had to use an array, now I know in the future. In the top of my script I then define "srcDir" - and then the script does whatever it's supposed to do later on, with the result in the "randomFile"-variable. Thank you very much. – Okay Dokey Jun 4 '19 at 16:00
4

As others have already pointed out, the quotes are preventing the brace expansion. You could simplify your script to just:

#!/usr/bin/env bash
printf '[%s]\n' "$(find /home/terdon/Desktop/wallPapers/{dir1,dir2,dir3} -type f -iname "*.jpg" | shuf -n 1)"

Or, if your file names can contain newline characters:

#!/usr/bin/env bash
printf '[%s]\n' "$(find /home/terdon/Desktop/wallPapers/{dir1,dir2,dir3} -type f -iname "*.jpg" -print0| shuf -zn 1)"

If you want something that can run on arbitrary directories and file types, try this:

#!/usr/bin/env bash

targetFilePattern="$1"
shift
declare -a targetDirs=("$@")
echo "find ${targetDirs[@]} -type f -iname '$targetFilePattern' | shuf -n 1"
randomFile=$(find "${targetDirs[@]}" -type f -iname "$targetFilePattern" | shuf -n 1)
echo "$randomFile"

You can then run it as:

printRandomFile '*jpg' /home/terdon/Desktop/wallPapers/{dir1,dir2,dir3}

Or even

printRandomFile '*jpg' /home/terdon/Desktop/wallPapers/{dir1,dir2,dir3,'a dir with a space'}
  • Thanks - also this looks a bit "hardcoded" - I wanted to have the resulting random file-name in a new variable, so I could keep do some manipulation later on in the script. The prinf was only for myself, to see the value. Sorry, you, couldn't know that, because I didn't tell it before... I'm however upvoting for the good explanation, as my way of saying thanks. – Okay Dokey Jun 4 '19 at 15:58
  • @OkayDokey if you want it in a variable, just use randomFile="$(find /home/terdon/Desktop/wallPapers/{dir1,dir2,dir3} -type f -iname "*.jpg" -print0| shuf -zn 1)" – terdon Jun 4 '19 at 16:03
  • Yes, ok, I know. But the path is kind of "hard-coded" which is ok for a small script. But you're right. The solutions you presented are all good, thanks. – Okay Dokey Jun 4 '19 at 16:20
  • @OkayDokey see update for how to make it truly not hardcoded. – terdon Jun 4 '19 at 17:16
  • Very, very nice, thank you very much, this is also (more of) what I was looking for... – Okay Dokey Jun 5 '19 at 18:50
3

Brace expansion isnt performed inside double quotes. There is a duplicate question regarding this somewhere. Also, use -printf flag for find, doing command substitution is unnecessary, so you can do this

find /home/user/Desktop/wallPapers/{dir1,dir2,dir3} -type f -iname "*.jpg" -printf '[%f]\n' | shuf -n1
  • This breaks in the unlikely case where the file names contain newlines. – terdon Jun 4 '19 at 15:50
  • Fair point, -print0 is better for those cases – Sergiy Kolodyazhnyy Jun 4 '19 at 15:52
  • Thank you very much, but for a script that could eventually become bigger I prefer executing some kind of expression, based on variables, defined in the top of the script. This looks a bit more like something on the command line - but I upvoted, due to the explanation. The purpose of my script was also not to just print something out on the screen (the printf-line) - but to do some further manipulation. I know you couldn't see that from my original post, so sorry about that... And thanks. – Okay Dokey Jun 4 '19 at 15:56
  • @OkayDokey you seem to be misunderstanding. This (and all other answers so far) are no more "hardcoded" than your approach. But perhaps we can give you better solutions if you edit your question and explain in more detail. – terdon Jun 4 '19 at 16:06
  • @OkayDokey If you do need to use dir1,dir2,dir3 later in a script, use set command or array to save the directory names. I would explain further but currently have no time. And yes, it was not obvious that you want to reuse those further. – Sergiy Kolodyazhnyy Jun 4 '19 at 16:13

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.