Skip to content

Instantly share code, notes, and snippets.

@datadavev
Last active October 10, 2016 15:31
Show Gist options
  • Save datadavev/b6289c7edcd831b3ba75c6d11347e4ff to your computer and use it in GitHub Desktop.
Save datadavev/b6289c7edcd831b3ba75c6d11347e4ff to your computer and use it in GitHub Desktop.
Set from stdin or get OS X finder comment for file
#!/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