Cleaning Up Downloads with Folder Actions

4th May 2024 | Programming

I've been brushing up on my web development skills with the Web Development Bootcamp course through Udemy. The course involves downloading a lot of projects, bundled as zip files. Very quickly I tired of moving the zip file to my projects folder and then unzipping the file. It's only a couple of steps, but it becomes very repetitious and monotonous, an ideal task for automation.

Screenshot of Automator with a Folder Action and script

The initial setup steps:

The shell script:

Note: This script makes use of the zsh shell, but bash also seems to work, as well. This script was tested on macOS Ventura 13.6.6, but it should hopefully work on an array of other versions (older and newer) of macOS.

The Folder Action passes the downloaded file as an argument (and not via stdin, otherwise this script won't work), and then the script iterates over the supplied argument(s) $@.

If you open up the Get Info panel on a file, the More Info: section may contain information detailing where a file originated. To check where a file a file might have been downloaded from, the mdls command is used to check for the file's metadata attributes and the kMDItemWhereFroms option returns an array of possible values. One example of inspecting one of these zip files:

kMDItemWhereFroms = (

The script just takes the output from the mdls call as a string and then checks if the URL is found, which indicates that the file originated from the Udemy website. All other files will be ignored.

The final steps involve unzipping the file into the destination folder and finally moving the original zip file (optional, but good as a cleanup process).


This script is fairly short, but it required a number of iterations to get everything to work properly. I started by writing a shell script and running it against a downloaded file, but it doesn't work exactly the same as when it is treated as a script within Automator.

The first challenge was to figure out how to extract the Where From data via a script. Fortunately, mdls solved the initial problem. I initially thought I would have to parse out the data which returns as a CFArray object, but is is just a string in this context. One thing which didn't work initially was when I was creating the comparison string, I simply used "". This didn't work because the asterisks are required on both sides to act as wild cards. The fix is to compare against *""* .

When I initialized the destination directory variable dst_dir, I initially surrounded the path ("~/Sites/the-complete-web-development-bootcamp/") with quotes as a safety measure in case there were ever spaces. However this caused a different issue in properly resolving the path, so I was getting No such file or directory errors when trying to work with that directory. After some research I discovered that using quotes caused the path with the ~ to not resolve properly.

In my initial script, I first moved the zip file to the destination directory, then changed to that new directory, and finally would unzip it. This worked properly when I ran the script directly, but when it was run within Automator, nothing seemed to work. Trying to debug such issues can be perplexing so I ended up running the unzip command and then piped any errors to a text file (e.g. unzip &> errors.txt). After doing this, I was able to get a useful error:

unzip:  cannot find or open /Users/chad/Downloads/, /Users/chad/Downloads/ or /Users/chad/Downloads/

When I was running the shell script directly from the Downloads folder, I would make a call like this: ./ . In this example, the name of the file being passed in as an argument would be just, but when it is passed in through the Folder Action, it is the entire file path, which tripped up my initial approach.

My workaround for this issue was to just unzip the archive from the Downloads folder, but set the -d flag and use the dst_dir as the destination path. Once that is completed, I then moved the zip file to the destination folder.

Bonus Challenge

Another thing I tried, which was eventually abandoned, was to create a name for an enclosing folder where to unzip the contents of the archive. The enclosing folder would share the same name as the zip file, so I needed a way to take the file name and just strip off the file extension. This introduced me to some more UNIX commands, basename and its associated dirname. After doing more research, I tried two methods to isolate just the name of the file. One method worked for me, while the other did not.

# Get the file's name without the file suffix
enclosing_folder=$(basename $filename) # This didn't work for me
enclosing_folder="${filename%.*}" # This does work to get just the file name


For what I had originally expected would be a somewhat simple task of automation to relieve a minor burden, resulted in becoming an interesting and frustrating experience as I fought against the nit-picky syntax of shell scripting and trying to integrate it with a Folder Action. The end result is a minimal amount of code, but not before going through several rounds of experimentation to carve the code down to its functional essence.