How do I create a Random process of copying file images to a specific folder

Hello folks! I just found this board while googling information about making Random scripts in automator or Terminal.

I wanted to be able to randomly copy an image or a set of images and put them into a folder. I found a script, but I could not point it in the direction of an external drive. For some reason I can’t seem to figure out the path of my externals and have the OS find it either.

This is the script I found in another thread from earlier this year.

ls -p ~/Documents | grep -v / | sort --random-sort | head -n 1

It works great if I put a certain amount of images in the “Documents” folder, but I’m trying to have a script where I can designate a folder on an external drive that holds hundred of thousands of images and it picks one and possibly moves it to a “Work Folder”

Eventually I’d make a slideshow that Randomly picks images I can use or reference from in my artwork.

Have you checked under /Volumes/?

I used /Volumes/… all the way down and it doesn’t seem to work. I’m trying it again and rechecking to see if I did something wrong.

Forgive me if this advice sounds insulting. The people who post here have a wide variety of experience.

Pipelines like this are best debugged by working through each command in turn and building them back up piece by piece. In your case, the ls command seems most likely to be the failure point, and I wouldn’t be surprised if putting the path to the external drive in quotes fixed your problem.

1 Like

I have very little experience in this type of thing. I wouldn’t find any advice insulting. I’m just learning and trying to do the one or couple tasks that are specific to my needs and not something that most people would need to do.

So I can only seem to duplicate this code inside the computers Macintosh HD and not on my externals. I’m sure I’m doing something wrong. Perhaps the wrapping in quotes is the issue. One of my Drives is called “2018 Lacie” so how would the code look like if it has a folder called “Images” with 400+ images that I’d like for it to randomly select?

Start with

ls -p '/Volumes/2018 Lacie/Images/'

If that gives you a list of all the files in that folder, you can move on to the other commands.

2 Likes

Thank you! That’s one of the issues. I think I can move forward.

ls -p ‘/Volumes/2018 Lacie/Images/’ | grep -v / | sort --random-sort | head -n 1

I have been able to move forward with what I have been trying to do with 1 specific folder titled Images which has 1000 jpegs. It is able to give me back a random file with its number.

I want to do two more things inside a larger group of image folders.

  1. I want to be able to grab any file within the main large folder that has multiple subfolders inside them with 100,000s of images without picking the folder titles themselves and just any jpeg file at random from inside the main folder.

  2. After it picks the file (pictured as Results) have that file copied and sent to a specific folder. Can this be done? I know it doesn’t work with the picture shown, but it is a place holder for what I mean.

After that I’m pretty set up for being able to incorporate this code for the art process I’m doing.

Isn’t that first part covered here?

For the second part, the cp command copies files.

I’m sorry I thought having my request asked in this thread again might help me understand it. Many of the things in these coding and scripts are more complex than I thought (currently). I’m obviously a novice at this point trying to do a few things for a specific need in my art process. I have been able to do this externally using a random generator and through finder I could gran that image and copy it, but since I’m gong to be using hundreds if not thousands of images through out the year I was hoping to have it all down for me inside the macOS.

I tried what was said in the other thread and in the process it started to ask me for allowing computer to go though my contacts and 2 or three other various steps that sounded weird. Perhaps I was using the information the original creator of the post was trying to do. They wanted to have a random image inside an email to send to friends. I just want a random image to be copied and put into a folder each time I use the process.

Can you put the script I need here avoiding anything that has to do with allowing these other programs to ask for permissions? I can try it again and see if it works.

Okay here is a script that I think does what you are now asking to do.

I’ve added explanatory comments and echo commands to output a description of what is happening at each step. I’ve guessed at the path you would use for the destination, and I’ve switched from a listing (ls) to a find (find) as this I think is a bit easier for getting a full file path from a set of nested source folders for the random file.

#!/bin/zsh

# This path is where to find the folders of image files
pathOfSourceFoldersRoot="/Volumes/2018 Lacie/Images"
echo This script is searching for image files in \"$pathOfSourceFoldersRoot\"

# This path is for the folder to copy the randomly selected image to
pathOfCopyToFolder="/Users/KillerRabbit/Documents/05 Temporary Random Images"
echo This script will copy the randomly selected file to \"$pathOfCopyToFolder\"

# This line sets 'pathOfRandomFile' to the path of a randomly selected file
pathOfRandomFile=$(find "$pathOfSourceFoldersRoot" -type f | grep -v -e '^$' | sort --random-sort | head -n 1)
echo The path of the randomly selected file is \"$pathOfRandomFile\"

# This line copies the randomly selected file to its destination
cp "$pathOfRandomFile" "$pathOfCopyToFolder"
echo This script copied \"$pathOfRandomFile\" to the folder \"$pathOfCopyToFolder\"

I set the script to run againt an asset folder for one of my websites that contains images in subfolders. I set the output folder to be a temporary directory I have set up. Below is a screenshot of the execution of the script.

No, I didn’t run the script in Automator; but the tool I used should replicate the run as a step in Automator the same way.

Note also, that previous thread had an AppleScript suggestion from another forum member that would be worth considering too if shell scripting is not something you feel comfortable with.

Well, @sylumer came in while I was away from my Mac and gave you the key information, which is that the find command is the most natural way to get a list of files within a hierarchy of subfolders. But since I had a slightly different approach, I’ll give it to you, even though you now have a solution.

My solution is a pipeline, which I’ve written here with each component command on a separate line. The backslashes tell the shell to treat this as if it were typed out on a single line.

find '/Volumes/2018 Lacie/Images' -type f \
| sort --random-sort \
| head -n 20 \
| tr '\n' '\0' \
| xargs -0 -I '{}' cp {} '/Users/KillerRabbit/Documents/Random Images/'

I’m assuming the “Images” folder in your “2018 Lacie” disk contains all the subfolders of images you want to randomly select from, and I’ve taken “/Users/KillerRabbit/Documents/Random Images/” as the target folder.

If all of the files in the “/Volumes/2018 Lacie/Images” hierarchy are image files, the find command will work as given above. If you have non-image files in there, you’ll want to add another restriction to limit the output:

find '/Volumes/2018 Lacie/Images/' -type f \( -iname "*.jpg" -o -iname "*.jpeg" \) \
| sort --random-sort \
| head -n 20 \
| tr '\n' '\0' \
| xargs -0 -I '{}' cp {} '/Users/KillerRabbit/Documents/Random Images/'

The -iname "*.jpg" option does a case-insensitive search for files that end with “.jpg”, and the -iname "*.jpeg" option does a case-insensitive search for files that end with “.jpeg”. The -o between these two options tells find to look for one or the other. This set of options is wrapped in escaped parentheses so find treats this whole clause as a single thing. You could add a similar option for PNG files. Again, this addition isn’t needed if all the files are images.

You’ve been using head -n 1 to get just one image at a time, but based on your original description of the problem, I think you should copy as many images as you want into the target folder in a single command. That means giving head’s -n option the total number of files you want copied to the target folder. In the example above, I’m using 20.

The tr command converts all the newline characters that separate the paths into nulls. This is a trick to make the xargs command below work even when the files have characters that would otherwise screw it up.

Finally, the xargs command runs the given cp command for every line passed into it from head. The -0 option tells xargs that the items being passed to it are separated by nulls instead of newlines. The -I '{}' option tells it to substitute in the path passed to it wherever it sees “{}”.

A Shortcut in which the user gets to enter the number of images that get copied to the target folder would look like this:

Note that the head step in the pipeline is now head -n "$1" and the Run Shell Script step gets its input from the Provided Input and passes it as an argument. Those are the adjustments needed to put the user input into the script.

I don’t know whether I’m thinking straight or not, but the use of tr and xargs here, and the explanations for them, seem amiss. Usually, the reason for dealing with NUL-teminated strings is to handle entities, such as file paths, that may contain characters that normally separate arguments on the command line, e.g. a linefeed, or even horizontal white space. But I don’t think that’s happening here.

find is retuning a list of file paths. As discussed, these file paths may contain new line characters, at which point, they are going to be split and treated as items to be sorted randomly by the sort command. The first 20 items in the shuffled list are retrieved by head, which are then joined by the NUL character, which xargs is then asked to use to delimit individual items.

So if the original file paths contained new line items, they’ve been scrambled already, in which case there’s no point swapping out new line characters for NULs. If the original file paths didn’t contain new line items, then our file paths will at least all be in tact, but as they don’t contain new line characters, there’s also no point swapping them out for NUL characters.

Or am I too groggy to reason this out correctly ? [ After all, I already replied to the wrong person initially, hence the deleted post above this one. ]

I think something like this might be more in line with what you had in mind:

read -d $'\x00' < <( find '/path/to/images' \
                    -type f ! -name '.*' \
                    -print0 | sort -z -R )

Then the randomly chosen file (whose name can contain new line characters or white space characters, etc.) will be stored in the $REPLY shell variable if using either bash or zsh.

Currently, the filters applied in the find command restrict the search to files whose filename doesnt start with a dot. But additional filters can be added to retrieve specific file extensions etc. -print0 ensures that the file paths returned from searching are delimited by NUL characters, which the -z flag enables sort to operate on the same basis, randomising their order. This gets delivered by process substitution to the read command, that delimits (and hence stops reading) at the first occurrence of a NUL character, and stores the result as described above, which will be a single file path.

Frankly, it never occurred to me that anyone would include newlines in their file or folder names. I’m tempted to say that people who do that deserve whatever happens to them.

My main concern was being able to return any number of file paths. head seemed like the simplest way to do that, but as far as I know, head has no option for null-separation. That’s why I didn’t use -print0 in find. But I was concerned about spaces, quotation marks, and other special shell characters messing up my xargs command—hence the tr.

PS: Thank you for including ! -name '.*'. I forgot about the need to eliminate .DS_Store files from the output.

I kinda share this sentiment. But thats usually why people go through the trouble of using NUL strings. So, if that wasnt your motivation, why bother with NULs at all ? How does the tr protect spaces, quotes, etc. when it only substitutes out linefeeds ?

My first attempt was

find /Users/drang/Desktop/Images -type f \
| sort -R \
| head -n 20 
| xargs -I '{}' cp {} /Users/drang/Desktop/Target/

(I was testing this on my computer, hence the different source and target paths.)

This worked fine when the file paths included some special characters (spaces, dollar signs, ampersands, brackets, and braces), but not when they included single or double quotation marks. Those files would throw errors and weren’t copied. By using tr and adding the -0 option to xargs, I was able to get the pipeline to work with files and folders with quotation marks.

After reading your posts, I figured there must be a way to split the sorted string on NULs and then take some subset of those (a replacement for head). Here’s what I came up with:

filestr=$(find /Users/drang/Desktop/Images -type f ! -name '.*' -print0 | sort -zR)
filearr=(${(0)filestr})

for imgfile in $filearr[1,$1]; do
	cp "$imgfile" /Users/drang/Desktop/Target/
done

The key is the (0) parameter expansion flag, which makes an array from the null-separated paths that come out of sort. I don’t think this is as readable as the the head/xargs solution, but it would handle linefeeds in a file path.

That’s because xargs is part of a pipeline: it takes a bunch of arguments from the command line, one or more of which will be {} (the chosen replacement string set using the -I option). Arguments received via stdin replace occurrences of {}. You know all this already, but it’s worth walking through it to acknowledge what happens: consider the string "what's your name?". bash has no problem echoing this phrase:

$:bash> echo "what's your name?"
what's your name?

However, this is what happens when we send it through the pipeline to xargs:

$:bash> printf '%s' "what's your name?" | xargs -I '{}' echo {}
xargs: unterminated quote

If there are two single quotes in the string, that’ll be parsed successfully, but not as intended:

$:bash> printf '%s'  "what's xargs's problem?" | xargs -I '{}' echo {}
whats xargss problem?

You can see what’s going on: special characters didn’t cause a problem because they were escaped by quotes or double-quotes, both of which escape all characters except themselves. Therefore, the presence of single- or double-quotes in the file paths upset the balance the ones already present, or if escaping was achieved by means of backslashing.

That’s a reasonable solution. If you want readability, my previous offering is probably the least cryptic:

$:bash> while read -d $'\x00' PNGs
      >    do printf '%s\n' "$PNGs"
      > done < <( find ~/Desktop/Some\ Files \
                       -type f -name '*.png' \
                       -print0 | sort -z -R  )
/Users/CK/Desktop/Some Files/Unknown.png
/Users/CK/Desktop/Some Files/Order
Confirmation.png
/Users/CK/Desktop/Some Files/Pinky.png

This handles the space in the folder’s name, and the newline in the file name. I think because, previously I only posted the portion of the command that included read and find, it wasn’t necessarily obvious what one was expected to do with them if they weren’t familiar with how read works.