diff --git a/README.md b/README.md index 8c434cd..6ddb28b 100755 --- a/README.md +++ b/README.md @@ -31,6 +31,25 @@ Save `~/.profile` and run the following command in a terminal to refresh the sys Now run `greg` and you should see the greg help text displayed in your terminal. +### Completion support + +To enable support for command line completion in bash or zsh, install the python module [argcomplete](https://pypi.python.org/pypi/argcomplete). + +For bash, add the following line to `~/.bashrc`: + +`eval "$(register-python-argcomplete greg)"` + +Zsh support requires adding the following lines to `~/.zshrc`: + +``` +autoload -U bashcompinit && bashcompinit +eval "$(register-python-argcomplete greg)" +``` + +Then load the changes by running `source ~/.bashrc` or `source ~/.zshrc`. + +See the [argcomplete](https://pypi.python.org/pypi/argcomplete) documentation for further details. + ### Installing via homebrew python3 The normal pip `install --user` is disabled for homebrew Python 3, so you cannot follow the above instructions. You have 2 options: diff --git a/greg/classes.py b/greg/classes.py index dc02a65..63cd6d8 100644 --- a/greg/classes.py +++ b/greg/classes.py @@ -31,6 +31,7 @@ import time from pkg_resources import resource_filename from urllib.parse import urlparse +from urllib.parse import quote from urllib.error import URLError import greg.aux_functions as aux @@ -283,6 +284,7 @@ def download_entry(self, entry): notype = self.retrieve_config('notype', 'no') if ignoreenclosures == 'no': for enclosure in entry.enclosures: + enclosure["href"] = quote(enclosure["href"], safe="%/:=&?~#+!$,;'@()*[]") #Clean up url if notype == 'yes': downloadlinks[urlparse(enclosure["href"]).path.split( "/")[-1]] = enclosure["href"] @@ -303,6 +305,7 @@ def download_entry(self, entry): "option in your greg.conf", file=sys.stderr, flush=True) else: + entry.link = quote(entry.link, safe="%/:=&?~#+!$,;'@()*[]") #Clean up url downloadlinks[urlparse(entry.link).query.split( "/")[-1]] = entry.link for podname in downloadlinks: diff --git a/greg/commands.py b/greg/commands.py index 108686d..b27c8c8 100644 --- a/greg/commands.py +++ b/greg/commands.py @@ -20,10 +20,17 @@ import os.path import pickle import sys +import time import greg.classes as c import greg.aux_functions as aux +try: # lxml is an optional dependency for pretty printing opml export + from lxml import etree as ET + lxmlexists = True +except ImportError: + import xml.etree.ElementTree as ET + lxmlexists = False def retrieveglobalconf(args): """ @@ -37,29 +44,39 @@ def add(args): Add a new feed """ session = c.Session(args) - if args["name"] in session.feeds.sections(): - sys.exit("You already have a feed with that name.") - if args["name"] in ["all", "DEFAULT"]: - sys.exit( - ("greg uses ""{}"" for a special purpose." - "Please choose another name for your feed.").format(args["name"])) - entry = {} - for key, value in args.items(): - if value is not None and key != "func" and key != "name": - entry[key] = value - session.feeds[args["name"]] = entry - with open(session.data_filename, 'w') as configfile: - session.feeds.write(configfile) + isduplicate = False + for feed in session.list_feeds(): + if session.feeds.has_option(feed, "url") and args["url"] == session.feeds.get(feed, "url"): + print("You are already subscribed to %r as %r." % (args["url"], str(feed))) + isduplicate = True + elif args["name"] in feed: + print("You already have a feed called %r." % args["name"]) + isduplicate = True + elif args["name"] in ["all", "DEFAULT"]: + print( + ("greg uses ""{}"" for a special purpose." + "Please choose another name for your feed.").format(args["name"])) + isduplicate = True + if not isduplicate: + entry = {} + for key, value in args.items(): + if value is not None and key != "func" and key != "name" and key != "import": + entry[key] = value + session.feeds[args["name"]] = entry + with open(session.data_filename, 'w') as configfile: + session.feeds.write(configfile) + print("Added %r." % args["name"]) def edit(args): # Edits the information associated with a certain feed session = c.Session(args) - feed_info = os.path.join(session.data_dir, args["name"]) - if not args["name"] in session.feeds: + name = args["name"][0] + feed_info = os.path.join(session.data_dir, name) + if not(name in session.feeds): sys.exit("You don't have a feed with that name.") for key, value in args.items(): if value is not None and key == "url": - session.feeds[args["name"]][key] = str(value) + session.feeds[name][key] = str(value[0]) with open(session.data_filename, 'w') as configfile: session.feeds.write(configfile) if value is not None and key == "downloadfrom": @@ -77,7 +94,7 @@ def edit(args): # Edits the information associated with a certain feed "Using --downloadfrom might not have the" "results that you expect."). format(args["name"]), file=sys.stderr, flush=True) - line = ' '.join(["currentdate", str(value), "\n"]) + line = ' '.join(["currentdate", str(value[0]), "\n"]) # A dummy entry with the new downloadfrom date. try: # Remove from the feed file all entries @@ -100,22 +117,26 @@ def remove(args): Remove the feed given in """ session = c.Session(args) - if not args["name"] in session.feeds: - sys.exit("You don't have a feed with that name.") - inputtext = ("Are you sure you want to remove the {} " - " feed? (y/N) ").format(args["name"]) - reply = input(inputtext) - if reply != "y" and reply != "Y": - return 0 - else: - session.feeds.remove_section(args["name"]) - with open(session.data_filename, 'w') as configfile: - session.feeds.write(configfile) - try: - os.remove(os.path.join(session.data_dir, args["name"])) - except FileNotFoundError: - pass + for name in args["name"]: + if not name in session.feeds: + sys.exit("You don't have a feed with that name.") + inputtext = ("Are you sure you want to remove the {} " + "feed? (y/N) ").format(name) + reply = input(inputtext) + if reply != "y" and reply != "Y": + print('Not removed') + else: + session.feeds.remove_section(name) + with open(session.data_filename, 'w') as configfile: + session.feeds.write(configfile) + try: + os.remove(os.path.join(session.data_dir, name)) + except FileNotFoundError: + pass +def get_feeds(args): # Returns a list of feed names + session = c.Session(args) + return session.list_feeds() def info(args): """ @@ -123,7 +144,7 @@ def info(args): """ session = c.Session(args) if "all" in args["names"]: - feeds = session.list_feeds() + feeds = get_feeds(args) else: feeds = args["names"] for feed in feeds: @@ -132,7 +153,7 @@ def info(args): def list_for_user(args): session = c.Session(args) - for feed in session.list_feeds(): + for feed in sorted(session.list_feeds()): print(feed) print() @@ -190,15 +211,15 @@ def check(args): """ session = c.Session(args) if str(args["url"]) != 'None': - url = args["url"] + url = args["url"][0] name = "DEFAULT" else: try: - url = session.feeds[args["feed"]]["url"] - name = args["feed"] + name = args["feed"][0] + url = session.feeds[name]["url"] except KeyError: sys.exit("You don't appear to have a feed with that name.") - podcast = aux.parse_podcast(url) + podcast = aux.parse_podcast(str(url)) for entry in enumerate(podcast.entries): listentry = list(entry) print(listentry[0], end=": ") @@ -245,3 +266,63 @@ def download(args): feed.entrylinks = [] feed.fix_linkdate(entry) feed.download_entry(entry) + +def opml(args): + """ + Implement the 'greg opml' command + """ + if args["import"]: + opmlfile = args["import"][0] + try: + opmltree = ET.parse(opmlfile) + except ET.ParseError: + sys.exit("%r does not appear to be a valid opml file." % opmlfile) + for element in opmltree.iterfind('.//*[@type="rss"]'): + if element.get('xmlUrl'): + args["url"] = element.get('xmlUrl') + elif element.get('url'): + args["url"] = element.get('url') + if element.get('title'): + args["name"] = element.get('title') + elif element.get('text'): + args["name"] = element.get('text') + else: + print("No title found for this feed, using url as title.") + args["name"] = args["url"] + add(args) + if args["export"]: + session = c.Session(args) + filename = args["export"][0] + toplevel = ET.Element("opml") + toplevel.set("version", "2.0") + head = ET.SubElement(toplevel, "head") + title = ET.SubElement(head, "title") + title.text = "Podcasts" + dateCreated = ET.SubElement(head, "dateCreated") + dateCreated.text = time.strftime("%c %Z") + body = ET.SubElement(toplevel, "body") + for feedname in sorted(session.list_feeds()): + if session.feeds.has_option(feedname, "url"): + feedurl = session.feeds.get(feedname, "url") + feedtype = aux.feedparser.parse(feedurl).version + if "rss" in feedtype: feedtype = "rss" + elif "atom" in feedtype: feedtype = "atom" + else: + feedtype = False + print("%r is not a valid feed, skipping." % feedname) + if feedtype: + print("Exporting %r..." % (feedname)) + outline = ET.SubElement(body, "outline") + outline.set("text", feedname) + outline.set("title", feedname) + outline.set("xmlUrl", feedurl) + outline.set("type", feedtype) + opmlfile = open(filename, 'wb') + opmlfile.write(('\n' \ + + '\n\n').encode()) + if lxmlexists: + opmlfile.write(ET.tostring(toplevel, encoding="utf-8", pretty_print=True)) + else: + opmlfile.write(ET.tostring(toplevel, encoding="utf-8")) + opmlfile.close() diff --git a/greg/parser.py b/greg/parser.py index c1e27f7..f163c2f 100755 --- a/greg/parser.py +++ b/greg/parser.py @@ -21,6 +21,11 @@ import greg.commands as commands +try: + import argcomplete + argcompleteexists = True +except ImportError: + argcompleteexists = False # defining the from_date type def from_date(string): @@ -43,85 +48,114 @@ def url(string): raise argparse.ArgumentTypeError(msg) return string -# create the top-level parser +# get list of feed names and pass to global var +def set_FeedChoices(value): + global feednames + feednames=commands.get_feeds(value) + +# send list of subscribed feeds to argcomplete +def customCompleter(prefix, parsed_args, **kwargs): + if not parsed_args.datadirectory and not parsed_args.configfile: + set_FeedChoices({}) #if datadirectory or configfile are not specified, get greg to use default + return feednames + +# set possible choices to list of feed names for appropriate data dir or configfile +class customActionSetFeedChoices(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + if "datadirectory" in self.dest: + set_FeedChoices({"datadirectory" : values}) + setattr(namespace, self.dest, values) + elif "configfile" in self.dest: + set_FeedChoices({"configfile" : values}) + setattr(namespace, self.dest, values) + else: + if values == self.default: + setattr(namespace, self.dest, values) + else: + try: + self.choices = feednames + except NameError: + set_FeedChoices(vars(namespace)) + self.choices = feednames + for value in values: + if value not in self.choices: + msg = "%r is not a valid feed" % value + raise argparse.ArgumentError(self, msg) + setattr(namespace, self.dest, values) + +#Create the top-level parser parser = argparse.ArgumentParser() parser.add_argument('--configfile', '-cf', - help='specifies the config file that greg should use') + help='specifies the config file that greg should use', action=customActionSetFeedChoices, metavar='CONFIGFILE') parser.add_argument('--datadirectory', '-dtd', - help='specifies the directory where greg keeps its data') + help='specifies the directory where greg keeps its data', action=customActionSetFeedChoices, metavar='DATADIRECTORY') + subparsers = parser.add_subparsers() # create the parser for the "add" command parser_add = subparsers.add_parser('add', help='adds a new feed') parser_add.add_argument('name', help='the name of the new feed') -parser_add.add_argument('url', type=url, help='the url of the new feed') -parser_add.add_argument('--downloadfrom', '-d', type=from_date, help='the date\ - from which files should be downloaded (YYYY-MM-DD)') +parser_add.add_argument('url', type = url, help='the url of the new feed') +parser_add.add_argument('--downloadfrom', '-d', type=from_date, + help='the date from which files should be downloaded (YYYY-MM-DD)') parser_add.set_defaults(func=commands.add) # create the parser for the "edit" command parser_edit = subparsers.add_parser('edit', help='edits a feed') -parser_edit.add_argument('name', help='the name of the feed to be edited') -group = parser_edit.add_mutually_exclusive_group(required=True) -group.add_argument('--url', '-u', type=url, help='the new url of the feed') -group.add_argument('--downloadfrom', '-d', type=from_date, help='the date from\ - which files should be downloaded (YYYY-MM-DD)') +parser_edit.add_argument('name', action=customActionSetFeedChoices, help='the name of the feed to be edited', nargs=1, metavar='FEEDNAME').completer = customCompleter +group = parser_edit.add_mutually_exclusive_group(required = True) +group.add_argument('--url', '-u', type = url, help='the new url of the feed', nargs=1) +group.add_argument('--downloadfrom', '-d', + type=from_date, help='the date from which files should be downloaded (YYYY-MM-DD)', nargs=1) parser_edit.set_defaults(func=commands.edit) # create the parser for the "info" command -parser_info = subparsers.add_parser('info', help='provides information \ - about a feed') -parser_info.add_argument('names', help='the name(s) of the feed(s) you want to\ - know about', nargs='*', default='all') +parser_info = subparsers.add_parser('info', help='provides information about a feed') +parser_info.add_argument('names', action=customActionSetFeedChoices, help='the name(s) of the feed(s) you want to know about', nargs='*', default=['all'], metavar='FEEDNAME').completer = customCompleter parser_info.set_defaults(func=commands.info) # create the parser for the "list" command -parser_info = subparsers.add_parser('list', help='lists all feeds') -parser_info.set_defaults(func=commands.list_for_user) +parser_list = subparsers.add_parser('list', help='lists all feeds') +parser_list.set_defaults(func=commands.list_for_user) # create the parser for the "sync" command parser_sync = subparsers.add_parser('sync', help='syncs feed(s)') -parser_sync.add_argument('names', help='the name(s) of the feed(s) you want to\ - sync', nargs='*', default='all') -parser_sync.add_argument('--downloadhandler', '-dh', help='whatever you want\ - greg to do with the enclosure') -parser_sync.add_argument('--downloaddirectory', '-dd', help='the directory to\ - which you want to save your downloads') -parser_sync.add_argument('--firstsync', '-fs', help='the number of files to\ - download (if this is the first sync)') +parser_sync.add_argument('names', help='the name(s) of the feed(s) you want to sync', action=customActionSetFeedChoices, nargs='*', default=['all'], metavar='FEEDNAME').completer = customCompleter +parser_sync.add_argument('--downloadhandler', '-dh', help='whatever you want greg to do with the enclosure') +parser_sync.add_argument('--downloaddirectory', '-dd', help='the directory to which you want to save your downloads') +parser_sync.add_argument('--firstsync', '-fs', help='the number of files to download (if this is the first sync)') parser_sync.set_defaults(func=commands.sync) # create the parser for the "check" command -parser_check = subparsers.add_parser('check', help='checks feed(s)') -group = parser_check.add_mutually_exclusive_group(required=True) -group.add_argument('--url', '-u', type=url, help='the url that you want to\ - check') -group.add_argument('--feed', '-f', help='the feed that you want to check') +parser_check = subparsers.add_parser('check', help='checks feed') +check_group = parser_check.add_mutually_exclusive_group(required=True) +check_group.add_argument('--url', '-u', type = url, help='the url that you want to check', nargs=1) +check_group.add_argument('--feed', '-f', help='the feed that you want to check', action=customActionSetFeedChoices, nargs=1, metavar='FEEDNAME').completer = customCompleter parser_check.set_defaults(func=commands.check) # create the parser for the "download" command -parser_download = subparsers.add_parser('download', help='downloads particular\ - issues of a feed') -parser_download.add_argument('number', help='the issue numbers you want to\ - download', nargs="*") -parser_download.add_argument('--mime', help='(part of) the mime type of the\ - enclosure to download') -parser_download.add_argument('--downloadhandler', '-dh', help='whatever you\ - want greg to do with the enclosure') -parser_download.add_argument('--downloaddirectory', '-dd', help='the directory\ - to which you want to save your downloads') +parser_download = subparsers.add_parser('download', help='downloads particular issues of a feed') +parser_download.add_argument('number', help='the issue numbers you want to download', nargs="*") +parser_download.add_argument('--mime', help='(part of) the mime type of the enclosure to download') +parser_download.add_argument('--downloadhandler', '-dh', help='whatever you want greg to do with the enclosure') +parser_download.add_argument('--downloaddirectory', '-dd', help='the directory to which you want to save your downloads') parser_download.set_defaults(func=commands.download) # create the parser for the "remove" command parser_remove = subparsers.add_parser('remove', help='removes feed(s)') -parser_remove.add_argument('name', help='the name of the feed you want to\ - remove') +parser_remove.add_argument('name', help='the name of the feed(s) you want to remove', action=customActionSetFeedChoices, nargs='+', metavar='FEEDNAME').completer = customCompleter parser_remove.set_defaults(func=commands.remove) +# create the parser for the 'opml' command +parser_opml = subparsers.add_parser('opml', help='import/export an opml feed list') +opml_group = parser_opml.add_mutually_exclusive_group(required=True) +opml_group.add_argument('--import', '-i', help='import an opml feed', nargs=1, metavar='FILENAME') +opml_group.add_argument('--export', '-e', help='export an opml feed', nargs=1, metavar='FILENAME') +parser_opml.set_defaults(func=commands.opml) + # create the parser for the 'retrieveglobalconf' command parser_rgc = subparsers.add_parser('retrieveglobalconf', aliases=['rgc'], - help='retrieves the path to the global\ - config file') + help='retrieves the path to the global config file') parser_rgc.set_defaults(func=commands.retrieveglobalconf) @@ -129,6 +163,9 @@ def main(): """ Parse the args and call whatever function was selected """ + if argcompleteexists: + argcomplete.safe_actions = argcomplete.safe_actions + (customActionSetFeedChoices,) + argcomplete.autocomplete(parser) args = parser.parse_args() try: function = args.func diff --git a/pkgbuilds/git/PKGBUILD b/pkgbuilds/git/PKGBUILD index 5ff034a..091d3b1 100644 --- a/pkgbuilds/git/PKGBUILD +++ b/pkgbuilds/git/PKGBUILD @@ -12,7 +12,9 @@ depends=('python-feedparser') optdepends=('python3-stagger-svn: writing metadata' 'wget: alternative downloadhandler' 'aria2: alternative downloadhandler' - 'python-beautifulsoup4: convert html to text for tagging') + 'python-beautifulsoup4: convert html to text for tagging' + 'python-argcomplete: tab completion' + 'python-lxml: nicer opml formatting') makedepends=('git') provides=('greg') conflicts=('greg')