How to Eject Mounted DMG Images?

I'd like to create a macro that ejects any mounted DMG images. I realise that this might be tricky because macOS doesn't know if a drive is physically real or not, but for my purposes it would be sufficient just to unmount any drives that are not on an exceptions list (the external drives I have connected 90% of the time).

I tried the following, but it doesn't work, so perhaps someone could point me in the right direction?

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application "Finder"
	activate
	set ejectableList to name of every disk whose startup is false and (ejectable is true or format is AppleShare format or format is SMB format)
	repeat with eachItem in ejectableList
		if name of eachItem is not in exceptionsList then
			eject disk eachItem
		end if
	end repeat
end tell

In line 4 you're asking for a list of the names of disks, so later on eachItem is a string -- you can't get the name of a string so the script errors. You check that in Script Editor by adding return name of eachItem directly after the repeat line:

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application "Finder"
	activate
	set ejectableList to name of every disk whose startup is false and (ejectable is true or format is AppleShare format or format is SMB format)
	repeat with eachItem in ejectableList
		return name of eachItem
                if name of eachItem is not in exceptionsList then
			eject disk eachItem
		end if
	end repeat
end tell

(A return directly after a repeat is an easy debugging trick to see what the first thing in a loop is then exit the script.)

So you either need to start with references to your disks then pull and check the name in the loop, or just use names throughout (and there's no need to activate the Finder unless you want to for something else):

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application "Finder"
	set ejectableList to every disk whose startup is false and (ejectable is true or format is AppleShare format or format is SMB format)
	repeat with eachItem in ejectableList
		if name of eachItem is not in exceptionsList then
			eject eachItem -- eachItem is a disk reference so no need to use 'disk'
		end if
	end repeat
end tell

...or:

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application "Finder"
	set ejectableList to name of every disk whose startup is false and (ejectable is true or format is AppleShare format or format is SMB format)
	repeat with eachItem in ejectableList
		set tmp to contents of eachItem
		if tmp is not in exceptionsList then
			eject disk tmp
		end if
	end repeat
end tell

In the second case you can skip the contents of de-referencing(? Probably not the right term!) by seeing if eachItem is in ejectList, so if you want to use names (and only do the one thing -- eject -- on a match) for concision and readability I'd go with

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application "Finder"
	set ejectableList to name of every disk whose startup is false and (ejectable is true or format is AppleShare format or format is SMB format)
	repeat with eachItem in ejectableList
		if eachItem is in ejectList then eject disk eachItem
	end repeat
end tell

Thanks so much for the detailed reply.

Ah, right. The string is:

AUDIO, Installers & Sessions, Time Machine

So it would have to be line-delimited in order to be used as a list. Is that right?

Quote

:point_up:t3:Am I right in saying that the main difference that makes this work is that you've removed name of from the third line? Does that leave them as one-per-line or am I missing the point?

Quote

:point_up:t3:So using contents here somehow means you can find each disk name in the string? Ouch, my brain hurts.

Quote

I think it's meant to be ejectableList, not ejectList, but this one ignores the exceptions, right?

All the syntactical subtleties are enough to make yer head spin! Nevertheless, your first suggestion works, and fast! Thanks again!

No -- I've obviously not explained well. In Script Editor, and cutting out unimportant bits for speed, try:

tell application "Finder"
    set ejectableList to every disk
    return item 1 of ejectableList
end
--> disk "Backups" of application "Finder"

...which you'll see is a reference to an object of class disk. You can even check that with:

tell application "Finder"
	set ejectableList to every disk
	return class of item 1 of ejectableList -- & return & item 1 of ejectableNames
end tell
--> disk

Whereas:

tell application "Finder"
	set ejectableList to name of every disk
	return item 1 of ejectableList
end tell
--> "Backups"

...which you can see is text:

tell application "Finder"
	set ejectableList to name of every disk
	return class of item 1 of ejectableList
end tell
--> text

You can get the name of an object of class disk, which is why my first version works. You can't get the name of a string, which is why yours was erroring.

Not quite. When you use repeat with eachItem in aList, eachItem is a reference to an item in the list:

tell application "Finder"
	set ejectableList to name of every disk
	repeat with eachItem in ejectableList
		return eachItem
	end repeat
end tell
--> item 1 of {"Backups", "iMac Macintosh HD", ...}

...but you can convert the reference to the "real thing" by getting the contents of:

tell application "Finder"
	set ejectableList to name of every disk
	repeat with eachItem in ejectableList
		return contents of eachItem
	end repeat
end tell
--> "Backups"

AppleScript will do this "dereferencing" for you when it's obvious (to AS, not to you!) that you want the contents of the list item rather than a reference to it:

tell application "Finder"
	set ejectableList to name of every disk
	repeat with eachItem in ejectableList
		return "The disk is called " & eachItem
	end repeat
end tell

TBH honest I don't understand the subtleties of this! But it's a good rule-of-thumb that if you're getting "unexpected" values when using items from a list, try using contents of those items instead.

No -- this one ignores the fact that my pork pie with macaroni cheese was ready, and in my rush I muffed the edit when transferring my test examples to the post :man_facepalming:

It should be exceptionsList -- "is this item of ejectableList also an item in exceptionsList?" Because you are comparing a list object to list objects, AS can work out what to do with the eachItem reference. For completeness:

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application "Finder"
	set ejectableList to name of every disk whose startup is false and (ejectable is true or format is AppleShare format or format is SMB format)
	repeat with eachItem in ejectableList
		if eachItem is in exceptionsList then eject disk eachItem
	end repeat
end tell

Ok I thiiiiink I get it. Correct me if (/when) I'm wrong:

When you ask for a disk's name, you get the name as text.
When you ask for a disk, you get its name.
An item in a list is the whole line, but if you ask for the contents, you get the bit in quotes (AS does this by magic).

That sounds nice. :drooling_face:

Hang on, isn't this doing the opposite of what we want though? If it's on the exceptionsList, then don't eject it. So it should be:

	if eachItem is NOT in exceptionsList then eject disk eachItem

Almost -- but there's some confusion over the textual representation in the editor and what's actually going on (which I think we all have most of the time!).

You are creating a list of objects -- what those objects are depends on how you create the list.

tell application "Finder"
    set ejectableList to every disk
end tell

...gets you a list of disk objects. If you look in the Finder's AppleScript dictionary you'll see that class disk has various properties, none of which are name. But it "inherits" properties from container which in turn inherits from item which does have name. So you can ask a disk for the value of its name property, which you'll also see from the dictionary is returned as text:

tell application "Finder"
    set ejectableList to every disk
    return {class of item 1 of ejectableList, name of item 1 of ejectableList}
end tell

But you could equally ask for the disk's ejectable, which is a boolean:

tell application "Finder"
    set ejectableList to every disk
    return {class of item 1 of ejectableList, ejectable of item 1 of ejectableList}
end tell

If you only want a property of the objects and not the objects themselves you can short-circuit things by gathering just the property when you create the list -- "Get me just the name of every disk", and as we saw above the name property returns text:

tell application "Finder"
	set ejectableList to name of every disk
	return {class of item 1 of ejectableList, item 1 of ejectableList}
end tell

The problem with that is each item in the list is just text -- and you can't eject text! So you have to tell AS to:

eject the disk "Backups"

...and AS knows you mean "eject the disk whose name is 'Backups"".

Long story short -- it's all about the class of data you get and the class of data a command requires. When there's no ambiguity AS can sometimes coerce data to fit in the background, but often you have to do it yourself.

The other confusing thing is the behaviour of the repeat with eachItem... structure. Consider the following:

set theList to {"1", "2", "3", "4"}
repeat with i from 1 to 4
	return item i of theList
end repeat
--> "1"

...and:

set theList to {"1", "2", "3", "4"}
repeat with eachItem in theList
	return eachItem
end repeat
--> item 1 of {"1", "2", "3", "4"}

As you can see, the eachItem construct returns a reference to a list item rather than the list item itself. Most of the time that doesn't matter -- AS can understand what we're trying to do from context and use the reference to get the value for us:

set theList to {"1", "2", "3", "4"}
repeat with eachItem in theList
	return eachItem & "a"
end repeat
--> "1a"

...but sometimes it can't. I don't have an example to hand, and I've never tried to work out when/why it goes wrong (it's probably my mistake causing it, though!) because, when it does, you just force the reference to be resolved by using contents of:

set theList to {"1", "2", "3", "4"}
repeat with eachItem in theList
	return (contents of eachItem) & "a"
end repeat

And finally ("At last!", you cry...)

Yep, my bad. In my defence, I was still on a pork pie and macaroni cheese high :wink:

1 Like

Honestly, thanks for patiently explaining that. I think might have to re-read it a few times!

Honestly, expect corrections from people who know better! The above works for me and helps me get my head round things, but probably wouldn't stand scrutiny from someone who is well embedded in AppleScript.

1 Like

Finder won't and can't eject disks that aren't ejectable, nor the startup disk, which will always be mounted at "/". This negates the need for most of the filtering, and the less filtering Finder has to do, the better. In fact, were there no exceptionsList, you would be free to just simply tell app "Finder" to eject every disk, and those which you want to be ejected will be ejected, while those which we probably aren’t meant to know were there will continue to be there.

But the one nice thing about Finder is its ability to utilise an ordinary list handed to it to predicate the filtering of files and folders. This negates the need for iterating manually:

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

tell application id "com.apple.finder" to eject (the ¬
        disks whose name is not in my exceptionsList)

If you wish the string comparisons to be case-sensitive, which, from the looks of your list, could be beneficial, then:

set exceptionsList to {"AUDIO", "Installers & Sessions", "Time Machine"}

considering case
        tell application id "com.apple.finder" to eject (the ¬
                disks whose name is not in my exceptionsList)
end considering

There is a way to check whether a mounted volume is that of a disk image file, but it really doesn't sound like it'll add much more than complexity and bulk to what is, at present, a nice, neat solution that you already had mostly composed and @Nige_S was able to quite expertly deconstruct and diagnose for you.

2 Likes

Interesting.

I got the basic eject disks script from another thread, so perhaps there was good reason for the filtering being included. Good to know!

Not needed, but considering is a new bit of AS language to me, so thanks!

Well, it would mean that external drives not manually included in the exceptions list wouldn't be ejected in error. That might be handy if I'm working on someone else's files. How complex are we talking?

Any reason why this isn't tell application "Finder"?

It's just a "best practice" habit that, in this case, was a result of me typing the code out on my phone by hand as it was only a couple of lines and I didn't think to change it.

In my own scripts, I will always formulate a reference to an application class object using its id key form, which makes use of the application's bundle identifier rather than its bundle name. It doesn't seem like something that would have any implications for it to make much difference, but it's a good ingredient for more robust scripts. To be brief, I'll highlight two important differences between an app's bundle id and its name:

  1. Application names can and do change very easily. Most often, this will happen between major version upgrades, but there's freedom for it to change at any time. Bundle identifiers change very infrequently, although they can change. But as these are registered with Apple, changing them isn't a simple act that will be done on a whim.

  2. Bundle identifiers are unique (to the application). Whereas multiple different applications can share the same name. I have two programs on my machine both called "Messages", but they both have different bundle ids. Apps available through the app store are mandated to register their bundle identifiers with Apple. Those that aren't available through the App Store still can register theirs (probably for a fee), and it's the only way to guarantee the identifier is associated with one app over another (which then won't be able to run on any system where its officially-registered counterpart exists).

But there are other reasons why it's a useful habit to adopt, especially if one likes to script the UI via System Events (many application processes are named like the application form which it's spawned, but there's a number that are not).

It utilises AppleScriptObjC, so it's automatically that much more verbose. The complexity in terms of how easy it is to understand is, I suppose, a subjective measure dependent on how comfortable one is with ASOC in general. But, there's the complexity in the sense of efficiency: the mounted volumes must each be tested individually, and this has to be implemented manually. So, computationally, it appears more complex (less efficient), but it's actually not a fair assessment, since an AppleScript whose filter doesn't truly eliminate the need for a repeat loop, it just hides it under nicer syntax and a layer or two of referential data structures. But, when it comes to execution, the ASOC code, though longer, will likely run a darn sight quicker, and potentially have your disks ejected sooner.

3 Likes

Thanks for taking the time to explain so eloquently! :+1:t3:

One for the scrapbook -- thanks, @CJK!

I must have been suffering from target fixation! I was so engrossed with the AppleScript that I never even noticed:

A shell script one-liner should be enough:

for disk in $(/usr/bin/hdiutil info | /usr/bin/egrep -o "^/dev/disk\d+" | /usr/bin/sort | /usr/bin/uniq); do /usr/bin/hdiutil detach $disk; done

Tested in sh, so should be fine in a KM action, and includes full paths in case your PATH environment variable is banjaxed.

1 Like

Wahhhh! That's amazing! Works a treat.

The reason I was interested in this is because I have four attached drives, one of which is a flash drive I use for storing plugin licenses. Some years ago I decided it would be hilarious to name it "Flash C*nt" (which, without the asterisk, means "show-off" in the UK), but I'm not sure everyone who visits my studio would find it as funny. Renaming it would mean going to all the plugin manufacturer websites and changing the associated device, so I figured simply hiding disks from the desktop would be easier. Of course, whenever you open a .dmg, there's no desktop icon to remind you to unmount it, hence my request. Anyway, this has been a very informative exchange with an even better outcome than anticipated, so thankyou very much, @Nige_S and @CJK!