Last active
October 10, 2016 15:31
-
-
Save datadavev/b6289c7edcd831b3ba75c6d11347e4ff to your computer and use it in GitHub Desktop.
Set from stdin or get OS X finder comment for file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env /usr/bin/python | |
#https://gist.github.com/vdave/b6289c7edcd831b3ba75c6d11347e4ff | |
# | |
# Manages structured content in Finder Comments | |
# | |
# The finder comment is treated as a block of text with a section treated | |
# as a JSON object. The JSON section is delimited in a manner similar to | |
# ssl certificates with a BEGIN and END line delimiting the JSON object. | |
# | |
from applescript import AppleScript | |
import sys | |
import os | |
import argparse | |
import logging | |
import json | |
IMMUTABLE_FIELDS=["ID",] | |
JSON_START_SIGNATURE = "-----BEGIN JSON-----" | |
JSON_END_SIGNATURE = "-----END JSON-----" | |
setComment = AppleScript(''' | |
on run {arg1, arg2} | |
tell application "Finder" to set comment of (POSIX file arg1 as alias) to arg2 as Unicode text | |
return | |
end run | |
''') | |
getComment = AppleScript(''' | |
on run {arg1} | |
tell application "Finder" | |
set theComm to comment of (POSIX file arg1 as alias) | |
end tell | |
return theComm | |
end run | |
''') | |
appendComment = AppleScript(''' | |
on run {arg1, arg2} | |
tell application "Finder" | |
set Comm to comment of (POSIX file arg1 as alias) | |
set newComm to Comm & (arg2 as Unicode text) | |
set comment of (POSIX file arg1 as alias) to newComm | |
end tell | |
return | |
end run | |
''') | |
def getJSONStartEnd(comment): | |
start_pos = comment.find(JSON_START_SIGNATURE) | |
end_pos = comment.find(JSON_END_SIGNATURE) | |
return start_pos, end_pos | |
def getTextJSON(file_name): | |
'''Retrieves the text and JSON object from the comment of the specified file. | |
''' | |
comment = getComment.run(file_name) | |
start_pos, end_pos = getJSONStartEnd(comment) | |
if start_pos < 0: | |
#No JSON object, return an empty object | |
return comment, {} | |
json_text = comment[start_pos + len(JSON_START_SIGNATURE) : end_pos] | |
text = comment[:start_pos] | |
return text, json.loads(json_text) | |
def mergeTextJSON(comment, json_text): | |
start_pos, end_pos = getJSONStartEnd(comment) | |
if start_pos >= 0: | |
tmp = comment[:start_pos] | |
comment = tmp | |
#append the JSON entry | |
new_comment = comment.rstrip() + "\n" + JSON_START_SIGNATURE + "\n" + json_text | |
new_comment += "\n" + JSON_END_SIGNATURE + "\n" | |
return new_comment | |
def setJSON(file_name, data): | |
'''Set the JSON object in the comment of the specified file. | |
IMPORTANT: this method does no merging with an existing entry. It sets the | |
entire JSON entry to that provided. | |
''' | |
json_text = json.dumps(data) | |
comment = getComment.run(file_name) | |
new_comment = mergeTextJSON(comment, json_text) | |
setComment.run(file_name, new_comment) | |
def mergeObjects(original_object, new_object, override=False): | |
'''Simple top level merge of two dictionaries | |
''' | |
res = original_object | |
for k in new_object.keys(): | |
if k in IMMUTABLE_FIELDS: | |
if original_object.has_key(k): | |
logging.warn("Attempt to set immutable field %s", k) | |
if not override: | |
logging.warning("Immutable field %s not changed.", k) | |
break | |
res[k] = new_object[k] | |
return res | |
def presentJSON(file_name, indent=2): | |
'''Retrieves the JSON object form the file and presents it. | |
''' | |
text,data = getTextJSON(file_name) | |
print json.dumps(data, indent=indent) | |
def updateJSON(file_name, data, replace=False, override=False): | |
'''Updates the JSON entry in the specified file. | |
If replace is True, then the entire JSON object is replaced, otherwise a | |
merge is performed. | |
''' | |
if replace: | |
setJSON(file_name, data) | |
return | |
text, existing_data = getTextJSON(file_name) | |
new_data = mergeObjects( existing_data, data, override=override) | |
setJSON(file_name, new_data) | |
def updateComment(file_name, comment): | |
'''Sets the plain text portion of the comment. | |
''' | |
text,data = getTextJSON(file_name) | |
json_text = json.dumps(data) | |
new_comment = mergeTextJSON(comment, json_text) | |
setComment.run(file_name, new_comment) | |
def main(args): | |
file_name = fname = os.path.abspath(args.fname) | |
if not os.path.exists(fname): | |
logging.error("Path %s does not exist.", fname) | |
return 1 | |
#no stdin | |
if sys.stdin.isatty(): | |
logging.info("Reading comment from %s", fname) | |
if args.keys: | |
text,data = getTextJSON(file_name) | |
if args.json: | |
print json.dumps(data.keys()) | |
return 0 | |
print "\n".join(data.keys()) | |
return 0 | |
if args.json: | |
presentJSON(file_name) | |
return 0 | |
if args.setvalue is not None: | |
kv = args.setvalue.split(':', 1) | |
data = {kv[0]:kv[1]} | |
updateJSON(file_name, data, replace=args.replace, override=args.override_immutable) | |
return 0 | |
if args.all: | |
print getComment.run(file_name) | |
return 0 | |
text,data = getTextJSON(file_name) | |
print text | |
return 0 | |
comment = sys.stdin.read() | |
#stdin, json flag set | |
if args.json: | |
data = json.loads(comment) | |
updateJSON(file_name, data, replace=args.replace, override=args.override_immutable) | |
return 0 | |
#replace the plain text portion of the comment | |
updateComment(file_name, comment) | |
return 0 | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description='Set by stdin or get finder comment for file.') | |
parser.add_argument('-l', '--log_level', | |
action='count', | |
default=0, | |
help='Set logging level, multiples for more detailed.') | |
parser.add_argument('-k','--keys', | |
action='store_true', | |
default=False, | |
help='List the keys in the comment YAML section') | |
parser.add_argument('-s','--setvalue', | |
default=None, | |
help='Set a value, indicate as key:value or JSON (with -j)') | |
parser.add_argument('--all', | |
action='store_true', | |
default=False, | |
help='Dump the entire comment including text and JSON portions.') | |
parser.add_argument('-j','--json', | |
action='store_true', | |
default=False, | |
help='Set or dump YAML section as JSON.') | |
parser.add_argument('-r', '--replace', | |
action='store_true', | |
help='Replace JSON data rather than merge.') | |
parser.add_argument('--override_immutable', | |
action='store_true', | |
default=False, | |
help='Force overwriting of immutable fields.') | |
parser.add_argument('fname', default=None, | |
help='The file to update or file to read from.') | |
args = parser.parse_args() | |
# Setup logging verbosity | |
levels = [logging.WARNING, logging.INFO, logging.DEBUG] | |
level = levels[min(len(levels) - 1, args.log_level)] | |
logging.basicConfig(level=level, | |
format="%(asctime)s %(levelname)s %(message)s") | |
# do the work | |
sys.exit( main(args) ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment