Feedback Needed: AppleScript for Automating Time Machine Backup

Hi all,

I’ve developed an AppleScript that automates Time Machine backups, including checking if the backup drive is mounted, verifying backup status, using caffeinate to prevent sleep, and ejecting the drive post-backup. I’m looking for feedback on its reliability and potential edge cases (e.g., drive disconnection mid-backup, error handling improvements). Any suggestions or insights from experienced users would be appreciated.

Here is my script:

-- Define the Time Machine volume name as a property
property timeMachineVolume : "SSD"
-- Declare a global variable for log messages
global statusMessages

-- Log messages with timestamp and display notifications
on logMessage(messageText)
	set timestamp to do shell script "date '+%Y-%m-%d %H:%M:%S'"
	set logLine to timestamp & " " & messageText
	copy logLine to the end of statusMessages -- Append message to log
	log logLine -- Log to Script Editor for debugging
	do shell script "osascript -e " & quoted form of ("display notification \"" & messageText & "\" with title \"Time Machine Backup\"")
end logMessage

-- Check if the drive is mounted by testing for the mount point directory
on isDriveMounted(volumeName)
	try
		set drivePath to "/Volumes/" & volumeName
		-- Use a shell script to check if the directory exists.
		set isMounted to do shell script "if [ -d " & quoted form of drivePath & " ]; then echo yes; else echo no; fi"
		if isMounted is equal to "yes" then
			logMessage("Drive is mounted: " & drivePath)
			return true
		else
			logMessage("Drive " & volumeName & " is not mounted.")
			return false
		end if
	on error errMsg
		logMessage("Error checking if drive " & volumeName & " is mounted: " & errMsg)
		return false
	end try
end isDriveMounted

-- Check if a Time Machine backup is running using tmutil currentphase
on isBackupRunning()
	try
		-- Get the current backup phase and remove any trailing newline
		set currentPhase to do shell script "tmutil currentphase | tr -d '
'"
		logMessage("Time Machine current phase: " & currentPhase)
		if currentPhase is equal to "BackupNotRunning" then
			return false
		else
			return true
		end if
	on error errMsg
		logMessage("Failed to get Time Machine current phase. Error: " & errMsg)
		return true
	end try
end isBackupRunning

-- Start a Time Machine backup and capture its output
on startBackup(volumeName)
	if not isDriveMounted(volumeName) then
		logMessage("Drive " & volumeName & " is not mounted. Backup cannot proceed.")
		error "Drive not mounted."
	end if
	
	logMessage("Checking Time Machine status...")
	if isBackupRunning() then
		logMessage("Backup is already in progress.")
	else
		logMessage("Starting Time Machine backup...")
		try
			set backupOutput to do shell script "caffeinate -i tmutil startbackup --auto --block"
			logMessage("tmutil startbackup output: " & backupOutput)
			logMessage("Backup completed successfully.")
		on error errMsg
			logMessage("Failed to complete backup. Error: " & errMsg)
			error "Backup failed."
		end try
	end if
end startBackup

-- Eject the Time Machine volume and capture its output
on ejectVolume(volumeName)
	logMessage("Preparing to eject Time Machine volume...")
	try
		set ejectResult to do shell script "diskutil eject '/Volumes/" & volumeName & "'"
		logMessage("Volume ejected successfully: " & ejectResult)
	on error errMsg
		if errMsg contains "Failed to find disk" then
			logMessage("Volume already ejected or not found; proceeding. Error: " & errMsg)
		else
			logMessage("Error during ejection: " & errMsg)
		end if
	end try
end ejectVolume

-- Main script execution
try
	set statusMessages to {} -- Initialize the log messages
	logMessage("Script started.")
	startBackup(timeMachineVolume)
	ejectVolume(timeMachineVolume)
	logMessage("Script completed successfully.")
on error errMsg
	logMessage("Script failed with error: " & errMsg)
end try

-- Return the collected log messages
return statusMessages

I’m unsure if my isBackupRunning function is robust and reliable enough. I tested a multiple hours Time Machine backup on a 2TB MBP & SSD, and smaller backups have worked fine so far. However, I haven’t been able to test edge cases or verify what would happen if the tmutil status output changes, IDK how to check how people check if Time Machine backup is running or not (old threads solutions does not work)…

Thanks!

Hi @zonal-oculars. Welcome to MacScripter.

Just looking through your script quickly, AppleScript’s result variable is automatically set to the result of each result-returning command. It’s not meant to be set explicitly.

What do you mean by AppleScript’s result variable?

From past discussions and threads, I’ve learned that using “eject” instead of “unmount” ensures the entire volume and its mounts are properly ejected.

I’m unsure if my isBackupRunning function is robust and reliable enough. I tested a multiple hours Time Machine backup on a 2TB SSD, and smaller backups have worked fine so far. However, I haven’t been able to test edge cases or verify what would happen if the tmutil status output changes, IDK how to check how people check if Time Machine backup is running or not (old threads solutions does not work)…

It’s a reserved variable called result which automatically contains the result of the action just carried out — if that action does indeed return a result. There’s no point in explicitly setting it to anything — as happens in a couple of places in your script — because it’s then just set automatically to the result of being set to that, if you see what I mean. :grin:

set fred to last item of {1, 2, 3, 4}
return result --> 4
"Hello"
return result --> "Hello"

Besides there being no point in explicitly setting it, it’s also not considered a good idea to read from it explicitly in a script. This is simply because you might edit the script later and absentmindedly insert another line between the one which gets a value and the one which reads the result. It’s best to use your own variables where possible, although I must admit I often use return result when I’m just trying things out and can’t be bothered to think of a variable name.

Hi @zonal-oculars.

I’ve been looking through your script more closely this morning. I can only talk about it from a scripting point of view, since I know little about Time Machine and nothing about awk! But generally it seems to be OK.

It’s not considered good practice to mix handler definitions with the main running code. I’d recommend changing the first set command to a property declaration (so it’s still at the top of the script and easy to edit later if required) and moving set statusMessages to {} into the main code at the bottom.

-- Define the Time Machine volume name
property timeMachineVolume : "SSD"
-- Initialize a global variable to store log messages
global statusMessages

-- Handlers here.

-- Main script execution
try
	set statusMessages to {}
	logMessage("Script started.")
	startBackup(timeMachineVolume)
	ejectVolume(timeMachineVolume)
	logMessage("Script completed successfully.")
on error errMsg
	logMessage("Script failed with error: " & errMsg)
end try

-- Return the collected status messages to Shortcuts
return statusMessages

Using a shell script in an AppleScript to execute an AppleScript command does seem a bit perverse! But if you want to run the script as a Shortcut, it does seem to be the least inconvenient way. Being old-school, I personally run scripts either from the Script Menu or as applications in their own right, where this seems to work:

	display notification messageText with title "Time Machine Backup"
	delay 0.2 -- Minuscule delay to give the notification process time to start.

I’ve already mentioned the misuse of result, which is a reserved variable. You could change it to something like volumeInfo in the isDriveMounted() handler. In startBackup(), you don’t need it at all since the shell script result isn’t used anyway.

isBackupRunning() seems to be OK as far as I can tell, but, as I’ve said, I’m not versed in awk. My own preferred weapon’s sed.

None of the above’s critical, but I hope some of it’s of interest. :slightly_smiling_face:

1 Like

Thank you! I’ve refined my script based on your feedback. I also simplified checks for reliability (instead of using awk and sed). I also log command outputs so I can later summarize them using the AI API and receive notifications.

-- Define the Time Machine volume name as a property
property timeMachineVolume : "SSD"
-- Declare a global variable for log messages
global statusMessages

-- Log messages with timestamp and display notifications
on logMessage(messageText)
	set timestamp to do shell script "date '+%Y-%m-%d %H:%M:%S'"
	set logLine to timestamp & " " & messageText
	copy logLine to the end of statusMessages -- Append message to log
	log logLine -- Log to Script Editor for debugging
	do shell script "osascript -e " & quoted form of ("display notification \"" & messageText & "\" with title \"Time Machine Backup\"")
end logMessage

-- Check if the drive is mounted by testing for the mount point directory
on isDriveMounted(volumeName)
	try
		set drivePath to "/Volumes/" & volumeName
		-- Use a shell script to check if the directory exists.
		set isMounted to do shell script "if [ -d " & quoted form of drivePath & " ]; then echo yes; else echo no; fi"
		if isMounted is equal to "yes" then
			logMessage("Drive is mounted: " & drivePath)
			return true
		else
			logMessage("Drive " & volumeName & " is not mounted.")
			return false
		end if
	on error errMsg
		logMessage("Error checking if drive " & volumeName & " is mounted: " & errMsg)
		return false
	end try
end isDriveMounted

-- Check if a Time Machine backup is running using tmutil currentphase
on isBackupRunning()
	try
		-- Get the current backup phase and remove any trailing newline
		set currentPhase to do shell script "tmutil currentphase | tr -d '
'"
		logMessage("Time Machine current phase: " & currentPhase)
		if currentPhase is equal to "BackupNotRunning" then
			return false
		else
			return true
		end if
	on error errMsg
		logMessage("Failed to get Time Machine current phase. Error: " & errMsg)
		return true
	end try
end isBackupRunning

-- Start a Time Machine backup and capture its output
on startBackup(volumeName)
	if not isDriveMounted(volumeName) then
		logMessage("Drive " & volumeName & " is not mounted. Backup cannot proceed.")
		error "Drive not mounted."
	end if
	
	logMessage("Checking Time Machine status...")
	if isBackupRunning() then
		logMessage("Backup is already in progress.")
	else
		logMessage("Starting Time Machine backup...")
		try
			set backupOutput to do shell script "caffeinate -i tmutil startbackup --auto --block"
			logMessage("tmutil startbackup output: " & backupOutput)
			logMessage("Backup completed successfully.")
		on error errMsg
			logMessage("Failed to complete backup. Error: " & errMsg)
			error "Backup failed."
		end try
	end if
end startBackup

-- Eject the Time Machine volume and capture its output
on ejectVolume(volumeName)
	logMessage("Preparing to eject Time Machine volume...")
	try
		set ejectResult to do shell script "diskutil eject '/Volumes/" & volumeName & "'"
		logMessage("Volume ejected successfully: " & ejectResult)
	on error errMsg
		if errMsg contains "Failed to find disk" then
			logMessage("Volume already ejected or not found; proceeding. Error: " & errMsg)
		else
			logMessage("Error during ejection: " & errMsg)
		end if
	end try
end ejectVolume

-- Main script execution
try
	set statusMessages to {} -- Initialize the log messages
	logMessage("Script started.")
	startBackup(timeMachineVolume)
	ejectVolume(timeMachineVolume)
	logMessage("Script completed successfully.")
on error errMsg
	logMessage("Script failed with error: " & errMsg)
end try

-- Return the collected log messages
return statusMessages

I have unit tested each function separately …

Late to the party again, but I had these (to original script):

  1. Maybe add the following to the top:
#! /usr/bin/env osascript
use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

-- Tested against:
-- * macOS 10.6.9

property author: "Oculars"

-- << actual script here >>

Nice to know what OS you’re positive it runs on…

  1. Care to punt early against CD-ROMs and readonly disks (as untested for), or let TM handle this later? If I were using it, I’d prefer to pick a particular volume rather than use a hardcoded volume name but understand if it’s for personal use

  2. Preferred this as more readable IMHO:

set status to do shell script " tmutil status | awk -F '=' '/Running/{ print sprintf(\"%d\", $2); }' "

BTW, nice find for status option of tmutil – my manpage doesn’t list it as an option.

I am running my script on macOS sequoia 15.3.1
I think using set currentPhase to do shell script tmutil currentphase | tr -d ' is more robust than set status to do shell script " tmutil status | awk -F '=' '/Running/{ print sprintf(\"%d\", $2); }' "

I am running my script on macOS sequoia 15.3.1

My point was that no one else knew that and it should be documented. I can barely remember what shirt I was wearing yesterday, so I can just imagine trying to figure out what version of OS a script was last ran on. But the AS version should definitely be there.

I think using set currentPhase to do shell script tmutil currentphase | tr -d ' is more robust than set status to do shell script " tmutil status | awk -F '=' '/Running/{ print sprintf(\"%d\", $2); }' "

Well, it’s at least cleaner and likely faster the new way (not requiring awk). Shame currentphase is yet another undocumented option on my tmutil manpage. I did the work but failed to post it until well after the fact. Your new script looks much improved !! Congrats