If external disk found by terminal applescript

applescriptdisk-utilityexternal-diskterminal

Hey so MacBooks have this nasty habit of not mounting external disks when plugged in before… reference link

the solution is quite simple with terminal eject the disk and the manually unplug replug the drive.

I'd like to make a small AppleScript that can do the whole process of looking for the disk and if found eject it. This is what i got, but I'm stuck on recognising if the external disk is found by terminal…

tell application "Terminal"
do script "echo \"diskutil list\"" in window 1
if exists disk2 then
    do script "echo \"diskutil info disk2\"" in window 1
    do script "echo \"diskutil eject disk2\"" in window 1
    do script "echo \"diskutil list\"" in window 1
end if
end tell

the error i get is The variable disk2 is not defined.
questions are:
1) if i print the diskutil list, how would i define variables for all the external disk terminal found?
2) see if it's mounted, if not try to mount, if fails eject?

Best Answer

You're actually writing a shell script, and the AppleScript context seems rather superfluous at the moment. If you're predominantly using shell commands, you may want to consider writing a shell script instead.

I've employed a little of both because they each have their usefulness in this situation: the shell commands are necessary to retrieve the disk information via diskutil, since AppleScript has no inbuilt means of getting that information by itself; but then rather than mess around with grep, awk, or sed to pattern match text from the raw output, we can invoke the -plist option to return the output as XML-formatted property list data, which AppleScript can be very good at handling.

1. Retrieving a list of local disks and volumes

Locally attached disks and volumes have a blockdevice inode in the folder /dev, so getting a list of disks is pretty easy with AppleScript:

use application "System Events"

property devices : a reference to files in folder "/dev"
property disknodes : a reference to (devices whose name begins with "disk")

set diskinfo to name of disknodes

With my USB drive attached, I get this list of inodes:

    --> {"disk0", "disk0s1", "disk0s2", "disk1", "disk1s1", 
         "disk1s2", "disk1s3", "disk1s4", "disk2", "disk2s1"}

2. Isolating the desired volumes

To filter the list so that we only run the appropriate utilities on the appropriate disks or volumes, diskutil now does its part. We can create a shell process from inside AppleScript rather than doing it via Terminal. The relevant shell command is this:

diskutil info -plist <disk|volume>

To execute this from AppleScript, use the do shell script command:

do shell script "diskutil info -plist" & id

where id is the name of a disk retrieved from earlier and stored in diskinfo. You can't pass in the entire list all at once, so we have to iterate through the list and run the command once for each item:

use scripting additions

delete every property list item
repeat with id in diskinfo
    set shellcmd to "diskutil info -plist " & id
    set plist to do shell script shellcmd

With the property list data returned by diskutil, AppleScript can turn it into a property list object. The property list data returned by invoking diskutil info is all contained within a single level, so there's no hierarchy to traverse. The great thing with property list objects is that AppleScript can transform them into a record, which then becomes really easy to retrieve values:

    make property list item with properties {name:id, text:plist}
    tell the value of property list item id as record to if ¬
        (its |Ejectable|) and (not its WholeDisk) and ¬
        (its MountPoint = "") then set the contents of id ¬
        to its {disk:ParentWholeDisk, volume:DeviceIdentifier}
end repeat

In a single line, AppleScript has read the property list data, and discarded any disk that is:

  1. Not ejectable;
  2. Is a "whole" disk (i.e. not a mountable volume);
  3. Has an identifiable mount point on the system.

It also bins all the useless data we don't need, and returns only the name of the partition and its parent disk. These will only be ejectable disks (so not the recovery and boot disks, and not your computer's hard disk), and will be mountable volumes that are not currently mounted. For me, and my single USB drive, which I unmounted first, I got this record list back:

set unmounted to records of diskinfo
    --> {{disk:"disk2", volume:"disk2s1"}}

3. Mounting and ejecting each volume

The parsing of the data is now done, so now you can run diskutil operations on the appropriate disks. You have the choice of either mounting individual volumes, or mounting an entire disk (which will mount all of its volumes). The method is the same for each: iterate through our record list stored in the variable unmounted and use either the disk or the volume from each item as an argument for the appropriate shell command:

diskutil mount <volume>
diskutil mountDisk <disk>
diskutil eject <disk>

The AppleScript loop looks like this:

repeat with diskitem in unmounted
    set shellcmd to "diskutil mount " & diskitem's volume
    try
        do shell script shellcmd
    on error
        set shellcmd to "diskutil eject " & diskitem's disk
        try
            do shell script shellcmd
        end try
    end try
end repeat

do shell script returns on success the output to stdout. After successfully mounting my disk, the messages pane in Script Editor contained:

"Volume USB on disk2s1 mounted"

I haven't instigated a situation that forces my drive not to mount, but typically, upon failure of any shell command (that is, whenever a shell command returns a non-zero status), do shell script throws an error. This will be caught by the try block, which will redirect the script to execute another shell command responsible for ejecting the drive. This is also in its own try block in case it, too, returns a non-zero status, in which case the script will continue onto the next volume.

Below is the final script with some additional modifications. The script will return the original unmounted record list with extra information appended to each disk item reporting the outcome of the attempt to mount and/or eject the volume. A complete failure will simply be reported with false. So my final script result looked like this:

--> {{disk:"disk2", volume:"disk2s1", result:"Volume USB on disk2s1 mounted"}}

The final script

use application "System Events"

property devices : a reference to files in folder "/dev"
property disknodes : a reference to (devices whose name begins with "disk")

set diskinfo to name of disknodes

use scripting additions

delete every property list item
repeat with id in diskinfo
    set shellcmd to "diskutil info -plist " & id
    set plist to do shell script shellcmd
    make property list item with properties {name:id, text:plist}
    tell the value of property list item id as record ¬
        to if (its |Ejectable|) ¬
        and (not its WholeDisk) ¬
        and (its MountPoint = "") then set ¬
        id's contents to {disk:its ParentWholeDisk ¬
        , volume:its DeviceIdentifier ¬
        , result:missing value}
end repeat

set unmounted to records of diskinfo

repeat with diskitem in unmounted
    set shellcmd to "diskutil mount " & diskitem's volume
    try
        do shell script shellcmd
        set diskitem's result to the result
    on error
        set shellcmd to "diskutil eject " & diskitem's disk
        try
            do shell script shellcmd
            set diskitem's result to the result
        on error E
            set disk item's result to E
        end try
    end try
end repeat

return unmounted