List unmounted volumes

I have a script to do this, but it’s a bit rubbish.

It’s both slow and fragile (e.g, if the volume name was something like ‘My Great Archive of AppleScripts Disk’) it would miss it. The problem is I can’t find a better way to do it. The issue is that the volume name is part of a string, but it’s not delimited in any way save for whitespace, and the whitespace could itself be within the volume name.

There’s NSWorkspace, DiskArbitration and FileManager methods related to mounting and unmounting, but AFAICT they all only post notifications when something changes. I need to get the list of the connected but unmounted volumes before any changes take place. Disk Utility will show these, but I’ve no idea how it’s doing it.

Any suggestions on how to do this better?

set diskNames to {}
set unmounted to {}
set diskList to paragraphs of (do shell script "diskutil list")
repeat with i from 1 to count of diskList
	set this_line to item i of diskList
	if this_line contains "Apple_" and this_line does not contain "Boot" then
-- slow and fragile attempt to extract the volume name here:
		set this_line4 to (do shell script "echo " & this_line & "| cut -d\" \" -f3 -f4 -f5 -f6")
		set end of diskNames to this_line4
		set this_line3 to (do shell script "echo " & this_line & "| cut -d\" \" -f3 -f4 -f5")
		set end of diskNames to this_line3
		set this_line2 to (do shell script "echo " & this_line & "| cut -d\" \" -f3 -f4")
		set end of diskNames to this_line2
		set this_line1 to (do shell script "echo " & this_line & "| cut -d\" \" -f3")
		set end of diskNames to this_line1
	end if
end repeat

repeat with d from 1 to count of diskNames
	set this_vol to item d of diskNames
	try
		set isMounted to (do shell script "diskutil info '" & this_vol & "' | grep Mounted | awk '{print $2 }'")
		if isMounted contains "No" then
			set end of my unmounted to this_vol
		end if
	end try
end repeat
return unmounted

I think we’ve argued before about the merits of slicing-and-dicing slabs of text versus parsing the structure, and this is a pretty good example of why I think the latter is a better approach where possible.

By getting a property list and converting it to a dictionary, the structure is much easier to see, and hence parse. So something like this:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theString to do shell script "diskutil list -plist"
set theString to current application's NSString's stringWithString:theString
set theData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
-- convert to dictionary
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 |format|:(missing value) |error|:(reference)
if theDict is missing value then error (theError's localizedDescription() as text)
-- once we have a dictionary, we can see how to parse it for what we want, so...
set partitionInfo to theDict's valueForKeyPath:"AllDisksAndPartitions.Partitions"
-- comnine into single array
set partitionInfos to current application's NSMutableArray's array()
repeat with anInfo in partitionInfo
	if not (anInfo's isKindOfClass:(current application's NSNull)) then (partitionInfos's addObjectsFromArray:anInfo)
end repeat
-- change predicate to suit
set thePred to current application's NSPredicate's predicateWithFormat:"(Content BEGINSWITH %@) AND (NOT Content IN %@)" argumentArray:{"Apple_", {"Apple_Boot", "Apple_CoreStorage"}}
partitionInfos's filterUsingPredicate:thePred
(partitionInfos's valueForKey:"VolumeName") as list
2 Likes

I’d overlooked the possibility of the plist output actually, but I’d have no doubt hurt myself playing with that syntax had I tried.

Thanks, that’s pretty nifty work. :sunglasses:

For some reason yours would run here, so I think I guessed wrongly what you wanted. Try changing the last line to this:

set volumeNames to (partitionInfos's valueForKey:"VolumeName")'s mutableCopy()
set mountedVolumes to theDict's objectForKey:"VolumesFromDisks"
volumeNames's removeObjectsInArray:mountedVolumes
set unmountedVolumes to volumeNames as list

Once you see a dictionary in SD’s results pane, it’s really simple trial-and-error until you get what you want.

2 Likes

Originally, I replaced the first half of my original with yours and fed that into the list for my repeat loop and it worked OK, but this is now much faster.

For anyone following along, the entire thing looks like this:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theString to do shell script "diskutil list -plist"
set theString to current application's NSString's stringWithString:theString
set theData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
-- convert to dictionary
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 format:(missing value) |error|:(reference)
if theDict is missing value then error (theError's localizedDescription() as text)
-- once we have a dictionary, we can see how to parse it for what we want, so...
set partitionInfo to theDict's valueForKeyPath:"AllDisksAndPartitions.Partitions"
-- comnine into single array
set partitionInfos to current application's NSMutableArray's array()
repeat with anInfo in partitionInfo
	if not (anInfo's isKindOfClass:(current application's NSNull)) then (partitionInfos's addObjectsFromArray:anInfo)
end repeat
-- change predicate to suit
set thePred to current application's NSPredicate's predicateWithFormat:"(Content BEGINSWITH %@) AND (NOT Content IN %@)" argumentArray:{"Apple_", {"Apple_Boot", "Apple_CoreStorage"}}
partitionInfos's filterUsingPredicate:thePred
set volumeNames to (partitionInfos's valueForKey:"VolumeName")'s mutableCopy()
set mountedVolumes to theDict's objectForKey:"VolumesFromDisks"
volumeNames's removeObjectsInArray:mountedVolumes
set unmountedVolumes to volumeNames as list

@ShaneStanley, your script, with the indicated changes, works well for me.