Improving this AppleScript to swap aliases with their original?

Hey y’all, I wrote this AppleScript to swap alias files with their original (I have a huge sample library for Logic Pro X, and I’m often relocating individual files; LPX doesn’t do well with aliased samples, but I like keeping them organized!).


on run {input, parameters}
    tell application "Finder"
        set theSelection to selection
        repeat with theAlias in theSelection
            if class of theAlias is alias file then
                try
                    set originalFile to original item of theAlias
                    set originalName to name of originalFile
                    set originalFolder to container of originalFile
                    set aliasName to name of theAlias
                    set aliasFolder to container of theAlias
                    set newName to aliasName & " _"
                    set newName2 to originalName & " _1"
                    set name of theAlias to newName
                    set name of originalFile to newName2
                    move item newName of aliasFolder to originalFolder
                    move item newName2 of originalFolder to aliasFolder
                    set name of item newName of originalFolder to originalName
                    set name of item newName2 of aliasFolder to aliasName
                end try
            end if
        end repeat
    end tell
return input
end run

The script works well, however, there are several things I would like to do that I have zero idea how to even begin implementing:

  1. Multithreading. As it is, each file is renamed and moved one after the other, and the “dropped file” sound is spammed for ages. I’ve found that running multiple instances of the script on small groups of files is faster than one instance on a single group. How can I have the script build a list of all the files selected, then swap them all at once or split the selection into smaller selections and have the script run multiple instances of itself?

  2. The AppleScript monitor reads 0% when running, then jumps to 100% once finished. Is there a way see an accurate %?

  3. Is there a way to have each “swap” be recorded into a log file?

The biggest, fattest, juiciest question of them all: Is my script the most efficient way to accomplish what I’m trying to do?

Thanks a ton guys :slight_smile:

Here are two faster versions.
version 1 use standard AppleScript

set tempFolder to (path to temporary items from user domain)
tell application "Finder"
	set theSelection to selection
	repeat with theAlias in theSelection
		if class of theAlias is alias file then
			try
				set aliasName to name of theAlias
				set aliasFolder to container of theAlias
				set originalFile to original item of theAlias
				set originalFolder to container of originalFile
				move theAlias to tempFolder
				move originalFile to aliasFolder
				move file aliasName of tempFolder to originalFolder
			end try
		end if
	end repeat
end tell

version 2 require more typing (but just have to click a button here to get it).
Using ASObjC it’s faster than the first one.

use scripting additions
use framework "Foundation"
use script "BridgePlus"
load framework

set tempFolder to POSIX path of (path to temporary items from user domain)

tell application "Finder"
	set theSelection to selection
	repeat with theAlias in theSelection
		if class of theAlias is alias file then
			try
				set originalFile to (get original item of theAlias)
				my swap(theAlias as alias, originalFile as alias, tempFolder)
			end try
		end if
	end repeat
end tell

#=====#=====#=====#=====#=====#=====

on swap(anAlias, itsOriginal, tempFolder)
	set {aliasName, aliasContainer} to my getNameAndContainer:(POSIX path of anAlias)
	set tempPath to my buildFullPath:aliasName inFolder:tempFolder
	# moves the alias in the temporay folder
	my movePath:(POSIX path of anAlias) toFolder:tempFolder
	set {originalName, originalContainer} to my getNameAndContainer:(POSIX path of itsOriginal)
	# moves the original in the original alias folder
	my movePath:(POSIX path of itsOriginal) toFolder:aliasContainer
	# moves the alias in the original original folder
	my movePath:tempPath toFolder:originalContainer
end swap

#=====#=====#=====#=====#=====#=====

on getNameAndContainer:POSIXPath # appelé par une instruction
	local theURL, PosixContainer, PosixName
	set theURL to current application's |NSURL|'s fileURLWithPath:POSIXPath
	set PosixContainer to theURL's URLByDeletingLastPathComponent()
	set PosixContainer to PosixContainer's |path|() as text
	set PosixName to (theURL's lastPathComponent()) as text
	# ATTENTION, Sierra ne met pas le / final
	return {PosixName, PosixContainer}
end getNameAndContainer:

#=====#=====#=====#=====#=====#=====

on buildFullPath:proposedName inFolder:POSIXPath # appelé par une instruction
	local theFolderURL, proposedName, theDestURL
	set theFolderURL to current application's |NSURL|'s fileURLWithPath:POSIXPath
	if class of proposedName is text then set proposedName to current application's NSString's stringWithString:proposedName
	set proposedName to proposedName's stringByReplacingOccurrencesOfString:"/" withString:":"
	set theDestURL to theFolderURL's URLByAppendingPathComponent:proposedName
	return theDestURL's |path| as text
end buildFullPath:inFolder:

#=====#=====#=====#=====#=====#=====

on movePath:posixSource toFolder:posixDestination #  appelé par trois instructions
	local theSourceURL, destURL, shortURL, origName, theFileManager, theResult, theError, destName
	set theSourceURL to current application's |NSURL|'s fileURLWithPath:posixSource
	set destURL to current application's |NSURL|'s fileURLWithPath:posixDestination
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set {theResult, theError} to theFileManager's createDirectoryAtURL:destURL withIntermediateDirectories:true attributes:(missing value) |error|:(specifier)
	--if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
	# maintenant, move cheminPosixDuFichierSource item
	set destName to theSourceURL's |lastPathComponent|()
	set destURL to destURL's URLByAppendingPathComponent:destName
	my moveFromURL:theSourceURL toURL:destURL withReplacing:true
	--destURL's |path| as text
end movePath:toFolder:

#=====

-- This handler is called by other handler, and is not meant to called directly
on moveFromURL:sourceURL toURL:destinationURL withReplacing:replaceFlag
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set {theResult, theError} to (theFileManager's moveItemAtURL:sourceURL toURL:destinationURL |error|:(specifier))
	if not theResult as boolean then
		if replaceFlag and (theError's code() = current application's NSFileWriteFileExistsError) then -- it already exists, so try replacing
			-- replace existing file with temp file atomically, then delete temp directory
			set {theResult, theError} to theFileManager's replaceItemAtURL:destinationURL withItemAtURL:sourceURL backupItemName:(missing value) options:(current application's NSFileManagerItemReplacementUsingNewMetadataOnly) resultingItemURL:(missing value) |error|:(specifier)
			-- if replacement failed, return error
			if not theResult as boolean then error (theError's |localizedDescription|() as text)
		else -- replaceFlag is false or an error other than file already exists, so return error
			error (theError's |localizedDescription|() as text)
		end if
	end if
end moveFromURL:toURL:withReplacing:

#=====#=====#=====#=====#=====#=====

Most of the code in version 2 is borrowed from Shane Stanley.

Yvan KOENIG running Sierra 10.12.1 in French (VALLAURIS, France) dimanche 13 novembre 2016 18:20:00

Thanks for your reply!

Unfortunately, I can’t seem to get the first script working… It finishes successfully, and I hear the file dropped sound, but no files are moved…

Any idea? Could it be that I’m doing this on an AFP network share?

Thanks :slight_smile:

Are your alias files and original files on the same volume?

They are!

Then you might do better to avoid aliases and use hard links instead.

I should have, but they are already aliases! I have ~100,000 more to swap with the originals!

If it’s possible, how would I go about using “as list”/“as alias” to build a list of the files in theSelection, then operating them all at once instead of one by one?

You can’t operate on them “all at once”.

Alright… Is there a way to rewrite

repeat with theAlias in theSelection

so I can build an app that runs two applescripts, one that operates on every other file (starting with the second) and the other that operates similarly but starting with the first?

When I select two groups of 100 files and run the script on both groups separately, it finishes much much faster than running one script on all 200 files.

Hope this makes sense.

One of the issues is that Finder scripting is very slow. That probably accounts for what you’re seeing.

I don’t guesss what fails on your mac.
I tested both scripts under 10.12.1 before posting them and they behaved flawlessly.

Oops, I missed that the files are on an AFP network.
I never used networks and have no plan to do so I can’t check this behavior.

Yvan KOENIG running Sierra 10.12.1 in French (VALLAURIS, France) lundi 14 novembre 2016 07:50:54

Thanks for the replies y’all! Here’s what I’ve come up with; it seems to run much faster as a dock applet than as a service.

As an AFP drive, I needed to set a temp folder instead of using /tmp.

If y’all see any redundancies, please let me know :slight_smile:

tell application "Finder"
	set theSelection to selection
	set fileCount to count of theSelection
end tell

set progress total steps to fileCount
set progress completed steps to 0
set progress description to "Working..."
set progress additional description to "Preparing alias swap."
set a to 1
set tempFolder to ("/Volumes/iMac Sample Library/tempfolder" as POSIX file)

repeat with theAlias in theSelection
	set progress additional description to "Swapping alias " & a & " of " & fileCount
	tell application "Finder"
		if class of theAlias is alias file then
			try
				set originalFile to original item of theAlias
				set originalFolder to container of originalFile
				set aliasFolder to container of theAlias
				set name of originalFile to name of theAlias
				move originalFile to tempFolder
				move theAlias to originalFolder
				move entire contents of item tempFolder to aliasFolder
			end try
		end if
	end tell
	set a to a + 1
end repeat

It seems that you missed a feature of my proposals.

I took care to move the alias to the temp folder because I’m quite sure that it’s smaller than the original file. Doing so, moving the original file - which is the longer task - is done only once.
You choose the reverse scheme so it’s the longer task which is made twice.
I don’t understand why you move the entire contents of the temp folder and not the unique file which you saved in it temporarily.

It would be a good idea to test the version using ASObjC because here it’s really faster than the one using the Finder.
With this scheme, it would be interesting too to drop the alias file and create a hard link in the dedicated location.
Creating a hard ling with ASObjC is fast and you no longer need the temp folder.

Here is a mix of old fashioned code and ASObjC one.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

-- Creates a hard link, Borrowed from Shane Stanley
on createHardLink:aPosixPath linkedTo:originalPosixPath
	set theLinkURL to current application's |NSURL|'s fileURLWithPath:aPosixPath
	set originalURL to current application's |NSURL|'s fileURLWithPath:originalPosixPath
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set {theResult, theError} to theFileManager's linkItemAtURL:originalURL toURL:theLinkURL |error|:(reference)
	if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
end createHardLink:linkedTo:

tell application "Finder"
	set theSelection to selection
	repeat with theAlias in theSelection
		if class of theAlias is alias file then
			try
				set aliasName to name of theAlias
				set aliasFolder to container of theAlias as alias
				set originalFile to original item of theAlias as alias
				set originalFolder to container of originalFile as text
				(move originalFile to aliasFolder)
				set newOriginalPath to POSIX path of (result as text)
				set hardLinkPath to (POSIX path of (originalFolder & aliasName))
				(my createHardLink:hardLinkPath linkedTo:newOriginalPath)
				delete theAlias
			end try
		end if
	end repeat
end tell

Of course, it would be faster if every tasks - except getting the selection -is made with ASObjC code.

Yvan KOENIG running Sierra 10.12.1 in French (VALLAURIS, France) mardi 15 novembre 2016 07:34:39

Thanks for your response!

Surprisingly, the aliases are actually larger than the files I’m moving, which is why I swapped them :slight_smile: The aliases are 129KB, and the majority of samples (1-2 second WAV files) are between 30-80KB!

I used “move entire contents” because “move originalFile” or “move item originalFile” gave me “Can not find…” errors!

I’ll have to try the ASObj scripts, thanks!

If alias are larger than original files it’s time to replace them by symlinks or hardlinks.

Here is a version creating symlinks.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

-- Creates a hard link, Borrowed from Shane Stanley
on createSymLink:aPosixPath linkedTo:originalPosixPath
	set theLinkURL to current application's |NSURL|'s fileURLWithPath:aPosixPath
	set originalURL to current application's |NSURL|'s fileURLWithPath:originalPosixPath
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set {theResult, theError} to theFileManager's createSymbolicLinkAtURL:theLinkURL withDestinationURL:originalURL |error|:(reference)
	if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
end createSymLink:linkedTo:

tell application "Finder"
	set theSelection to selection
	repeat with theAlias in theSelection
		if class of theAlias is alias file then
			try
				set aliasName to name of theAlias
				set aliasFolder to container of theAlias as alias
				set originalFile to original item of theAlias as alias
				set originalFolder to container of originalFile as text
				(move originalFile to aliasFolder)
				set newOriginalPath to POSIX path of (result as text)
				set hardLinkPath to (POSIX path of (originalFolder & aliasName))
				(my createSymLink:hardLinkPath linkedTo:newOriginalPath)
				delete theAlias
			end try
		end if
	end repeat
end tell

Hardlinks are more efficients, they use no disk space. The difference may appear to be short - a symlink use 42 bytes - but in fact as it’s a file, the symlink use a physical block : 4 Kbytes on my HD (I don’t know which is the block size on your server). As you wrote that you have more than 100,000 couples of file & alias, replacing old fashioned aliases by symlinks or better hardlinks would save a lot of space.

About the version using only old fashioned code it seems that you didn’t read carefully my first proposal.
The last move didn’t move a file described by a pathname but by its name in the temp folder.

set tempFolder to (path to temporary items from user domain)
tell application "Finder"
	set theSelection to selection
	repeat with theAlias in theSelection
		if class of theAlias is alias file then
			try
				set aliasFolder to container of theAlias # will receive the moved original file
				set originalFile to original item of theAlias
				set originalName to name of originalFile # will be used for the last move
				set originalFolder to container of originalFile # will receive the moved alias
				move originalFile to tempFolder # put it in a safe area
				move theAlias to originalFolder # put it in its final location
				move file originalName of tempFolder to aliasFolder # move it from its temp location to the final one
			end try
		end if
	end repeat
end tell

I will try to find an alternate scheme extracting directly the aliases available in the selection.

Yvan KOENIG running Sierra 10.12.1 in French (VALLAURIS, France) mardi 15 novembre 2016 15:50:26

No luck trying to filter only the aliases from the selection.

Here is a script reducing the Finder use to its minimal part.
Every other tasks are made thru ASObjC code.
Here it creates hardLinks.


-- most of the ASObjC instructions are borrowed from Shane Stanley's scripts
use scripting additions
use framework "Foundation"

tell application "Finder"
	set theSelection to selection
	repeat with theAlias in theSelection
		if class of theAlias is alias file then
			try
				set originalFile to (get original item of theAlias)
				my swap2(theAlias as alias, originalFile as alias)
			end try
		end if
	end repeat
end tell

#=====#=====#=====#=====#=====#=====

on swap2(anAlias, itsOriginal)
	set anAlias to POSIX path of anAlias
	set {aliasName, aliasContainer} to my getNameAndContainer:anAlias
	set itsOriginal to POSIX path of itsOriginal
	# Deletes the alias which is now useless
	my deleteThisPath:anAlias
	set {originalName, originalContainer} to my getNameAndContainer:itsOriginal
	# moves the original in the alias folder
	my movePath:itsOriginal toFolder:aliasContainer
	# Builds the path of the moved original
	set newOriginalPath to my buildFullPath:originalName inFolder:aliasContainer
	# Builds the path to the new link
	set hardLinkPath to my buildFullPath:aliasName inFolder:originalContainer
	# Creates the hardlink
	my createHardLink:hardLinkPath linkedTo:newOriginalPath
end swap2

#=====#=====#=====#=====#=====#=====

on createHardLink:aPosixPath linkedTo:originalPosixPath
	local theLinkURL, theriginalURL, theFileManager, theResult, theError
	set theLinkURL to current application's |NSURL|'s fileURLWithPath:aPosixPath
	set theOriginalURL to current application's |NSURL|'s fileURLWithPath:originalPosixPath
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set {theResult, theError} to theFileManager's linkItemAtURL:theOriginalURL toURL:theLinkURL |error|:(reference)
	if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
end createHardLink:linkedTo:

#=====#=====#=====#=====#=====#=====

on getNameAndContainer:POSIXPath
	local theURL, thePosixContainer, thePosixName
	set theURL to current application's |NSURL|'s fileURLWithPath:POSIXPath
	set thePosixContainer to theURL's URLByDeletingLastPathComponent()
	set thePosixContainer to thePosixContainer's |path|() as text
	set thePosixName to (theURL's lastPathComponent()) as text
	# ATTENTION, Sierra ne met pas le / final
	return {thePosixName, thePosixContainer}
end getNameAndContainer:

#=====#=====#=====#=====#=====#=====

on buildFullPath:proposedName inFolder:POSIXPath
	local theFolderURL, theDestURL
	set theFolderURL to current application's |NSURL|'s fileURLWithPath:POSIXPath
	if class of proposedName is text then set proposedName to current application's NSString's stringWithString:proposedName
	set proposedName to proposedName's stringByReplacingOccurrencesOfString:"/" withString:":"
	set theDestURL to theFolderURL's URLByAppendingPathComponent:proposedName
	return theDestURL's |path| as text
end buildFullPath:inFolder:

#=====#=====#=====#=====#=====#=====

# We know that the target folder exists so no need to create it
# I assume that there is no possible name conflict
on movePath:posixSource toFolder:posixDestination
	local theSourceURL, theDestURL, theFileManager, theDestName, theResult, theError
	set theSourceURL to current application's |NSURL|'s fileURLWithPath:posixSource
	set theDestURL to current application's |NSURL|'s fileURLWithPath:posixDestination
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set theDestName to theSourceURL's |lastPathComponent|()
	set theDestURL to theDestURL's URLByAppendingPathComponent:theDestName
	set {theResult, theError} to (theFileManager's moveItemAtURL:theSourceURL toURL:theDestURL |error|:(reference))
	if not theResult as boolean then
		error (theError's |localizedDescription|() as text)
	end if
	--destURL's |path| as text
end movePath:toFolder:

#=====#=====#=====#=====#=====#=====

on deleteThisPath:POSIXPath
	local theURL, theFileManager, theResult, theError
	set theURL to current application's |NSURL|'s fileURLWithPath:POSIXPath
	set theFileManager to current application's NSFileManager's |defaultManager|()
	set {theResult, theError} to theFileManager's removeItemAtURL:theURL |error|:(reference)
	if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
end deleteThisPath:

#=====#=====#=====#=====#=====#=====

Yvan KOENIG running Sierra 10.12.1 in French (VALLAURIS, France) mardi 15 novembre 2016 17:29:58

Hi.

If the files, aliases, and temporary folder are all in the same volume, the sizes shouldn’t be relevant. Only the directory entries get changed. The files don’t physically go anywhere.

It’ll also happen when moving the item to the temporary folder if renaming it beforehand actually changes its name. ‘originalFile’, like the other file variables, contains a Finder name reference. If the name changes or the item’s moved to a different location, the reference will no longer work for it.

	set aliasName to name of theAlias
	set name of originalFile to aliasName
	move item aliasName of originalFolder to tempFolder
	move theAlias to originalFolder
	move item aliasName of tempFolder to aliasFolder

Or perhaps:

	set aliasName to name of theAlias
	set name of originalFile to aliasName
	set originalFile to (move item aliasName of originalFolder to tempFolder)
	move theAlias to originalFolder
	move originalFile to aliasFolder

Your code assumes that the alias file’s name doesn’t already belong to an item to which it isn’t an alias in the same folder as its original.