Solved it, using PyObjC, despite there being almost no documentation for PyObjC. You have to carefully convert ObjectiveC interfaces for NSURL to PyObjC calls, using the techniques described in "An Introduction to PyObjC" found on this site while referring to the NSURL interfaces described here.
Code in @MagerValp's reply to this question helped figure out how to get the target of an alias. I had to work out how to create a new alias with a revised target.
Below is a test program that contains and exercises all the functionality needed. Its setup and use are documented with comments in the code.
I'm a bad person and didn't do doc strings or descriptions of inputs and return values, but I've kept all functions short and single-functioned and hopefully I've named all variables and functions sufficiently clearly that they are not needed. There's an admittedly weird combination of CamelCaps and underscore_separated variable and function names. I normally use CamelCaps for global constants and underscore_separated names for functions and variables, but in this case I wanted to keep the variables and data types referred to in the PyObjC calls, which use camelCaps, unchanged, hence the odd mix.
Be warned, the Mac Finder caches some information about aliases. So if you do a Get Info or a resolve on file_alias
immediately after running this program, it will look like it didn't work, even though it did. You have to drag the one
folder to the Trash and empty the Trash, and only then will a Get Info or resolve of file_alias
show that it does indeed now point to ./two/file.txt
. (Grumble, grumble.) Fortunately this will not impact my use of these techniques, nor will it affect most people's use, I suspect. The point of the program will normally be to replace a broken alias with a fixed one, based on the fact that some single, simple thing changed, like the folder name in this example, or the volume name in my real application for this.
Finally, the code:
#!/usr/bin/env python
# fix_alias.py
# A test program to exercise functionality for retargeting a macOS file alias (bookmark).
# Author: Larry Yaeger, 20 Feb 2018
#
# Create a file and directory hierarchy like the following:
#
# one
# file.txt
# two
# file.txt
# file_alias
#
# where one and two are folders, the file.txt files are any files really, and
# file_alias is a Mac file alias that points to ./one/file.txt. Then run this program
# in the same folder as one, two, and file_alias. It will replace file_alias with
# an alias that points to ./two/file.txt.
#
# Note that file_alias is NOT a symbolic link, even though the Mac Finder sometimes
# pretends symbolic links are aliases; they are not.
import os
import string
from Foundation import *
OldFolder = 'one'
NewFolder = 'two'
AliasPath = 'file_alias'
def get_bookmarkData(alias_path):
alias_url = NSURL.fileURLWithPath_(alias_path)
bookmarkData, error = NSURL.bookmarkDataWithContentsOfURL_error_(alias_url, None)
return bookmarkData
def get_target_of_bookmarkData(bookmarkData):
if bookmarkData is None:
return None
options = NSURLBookmarkResolutionWithoutUI | NSURLBookmarkResolutionWithoutMounting
resolved_url, stale, error =
NSURL.URLByResolvingBookmarkData_options_relativeToURL_bookmarkDataIsStale_error_(
bookmarkData, options, None, None, None)
return resolved_url.path()
def create_bookmarkData(new_path):
new_url = NSURL.fileURLWithPath_(new_path)
options = NSURLBookmarkCreationSuitableForBookmarkFile
new_bookmarkData, error =
new_url.bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error_(
options, None, None, None)
return new_bookmarkData
def create_alias(bookmarkData, alias_path):
alias_url = NSURL.fileURLWithPath_(alias_path)
options = NSURLBookmarkCreationSuitableForBookmarkFile
success, error = NSURL.writeBookmarkData_toURL_options_error_(bookmarkData, alias_url, options, None)
return success
def main():
old_bookmarkData = get_bookmarkData(AliasPath)
old_path = get_target_of_bookmarkData(old_bookmarkData)
print old_path
new_path = string.replace(old_path, OldFolder, NewFolder, 1)
new_bookmarkData = create_bookmarkData(new_path)
new_path = get_target_of_bookmarkData(new_bookmarkData)
print new_path
os.remove(AliasPath)
create_alias(new_bookmarkData, AliasPath)
main()