0

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?

  • I'd just try mounting everything you find. Have a look at "Try...End Try" blocks. They have the advantage of failing silently. This can save you a LOT of "if exists..." and other repetitive coding. Be aware that it's best to put your try-end try statements in after you get the basic algorithm working I put them in, then --comment them out after I test each part of the script. It's nice Not to get an Apple Script dialog, when your just trying to read a book. – Wayfaring Stranger May 19 at 20:31
  • I would, on principle, disregard the advice given by @WayfaringStranger here. In general, that is not how to use nor why one would implement error catching, and a lot of "repetitive if exists..." coding can be laborious, no doubt, but is absolutely the correct way to do things. But, since @Rottjung is a C++ programmer, I suspect they already know this, so this comment is aimed at other readers in general who don't want to pick up bad habits in scripting. – CJK May 22 at 4:05
  • @CJK Wait until you get five pages into your "if exists". Like the ancient "go to" controversy, Try-End try blocks can get rid of massive piles of ugly code, and give you a much more human readable, and maintainable, product. – Wayfaring Stranger May 22 at 19:03
  • @WayfaringStranger As with anything in life, when one understands how and why a rule exists, then one knows precisely how and when to break the rule effectively. But I would personally not be in a rush to encourage scripting newbies to do this, given that they typically rely on this poor technique a lot, causing a ton of problems in their scripts that are solved by eliminating every single try block. I'd also guess that if you have five pages' worth checking for existence of things, there's probably a better way to approach your task. – CJK May 22 at 19:26
  • Yes, you are correct about the newbs taking the well trodden path. I've been scripting since AppleScript came out, and there are times when condensing piles of code into a few statements is the way to go. My eyes get tired looking at too many nested "If thens". The "try...end try" block is also a sometimes useful tool when debugging. When things start going funny, the "try end try" blocks are easily commented out, which can make it easy to find where a fatal error is coming from. It's best to make darn sure you've got the code down good before enclosing it in try end try. – Wayfaring Stranger May 22 at 19:59
1

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
  • I have a question regarding delete every property list item... Which property list are the items being deleted from, and why is it necessary? – user3439894 May 19 at 17:19
  • It's deleted from System Events, which owns the property list item objects at the top level. When creating an orphaned property list item that hasn't been read from a property list file, System Events stores them in a temporary container, although I don't know where (likely a temporary file in a temporary directory). They usually expire after about 5 minutes of continuous inactivity (i.e. AppleScript having not accessed its data in that time). Between successive runs of a script, they accumulate, which irks me. Cont'd... – CJK May 19 at 17:29
  • ...Con'td But more importantly, the data don't seem to get consistently and reliably overwritten with new data on each successive run on the script, possibly because I declared my orphans with a specific name (even though System Events assigns each instance its own id). Deleting every property list at the beginning seems to ensure that the data I'm handling is the most up-to-date version of the data. ☐ – CJK May 19 at 17:32
  • So in the context of this question its the two property ... lines that follow the use application "System Events" that is the target of delete every property list item, if I'm understanding this correctly? – user3439894 May 19 at 17:40
  • 1
    Spent the last two days fixing a friends motorcycle then a long sunset and full moon rise ride. Too exhausted today to write any code, and now I need to finish cleaning my motorcycle. Bugs, the only part of a night ride that's not fun! – user3439894 May 19 at 18:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .