diff --git a/gcalcli/cli.py b/gcalcli/cli.py index 6fbf272..a76e5ab 100755 --- a/gcalcli/cli.py +++ b/gcalcli/cli.py @@ -20,9 +20,11 @@ # Everything you need to know (Google API Calendar v3): http://goo.gl/HfTGQ # # # # ######################################################################### # +from argparse import ArgumentTypeError import json import os import pathlib +import re import signal import sys from collections import namedtuple @@ -45,23 +47,43 @@ """ -def parse_cal_names(cal_names): +def rsplit_unescaped_hash(string): + # Use regex to find parts before/after last unescaped hash separator. + # Sadly, all the "proper solutions" are even more questionable: + # https://stackoverflow.com/questions/4020539/process-escape-sequences + match = re.match( + r"""(?x) + ^((?:\\.|[^\\])*) + [#] + ((?:\\.|[^#\\])*)$ + """, + string + ) + if not match: + return (string, None) + # Unescape and return (part1, part2) + return tuple(re.sub(r'\\(.)', r'\1', p) + for p in match.group(1, 2)) + + +def parse_cal_names(cal_names: list[str], printer: Printer): cal_colors = {} for name in cal_names: cal_color = 'default' - parts = name.split('#') - parts_count = len(parts) - if parts_count >= 1: - cal_name = parts[0] - - if len(parts) == 2: - cal_color = valid_color_name(parts[1]) - - if len(parts) > 2: - raise ValueError('Cannot parse calendar name: "%s"' % name) + p1, p2 = rsplit_unescaped_hash(name) + if p2 is not None: + try: + name, cal_color = p1, valid_color_name(p2) + except ArgumentTypeError: + printer.debug_msg( + f'Using entire name {name!r} as cal name.\n' + f'Change {p1!r} to a valid color name if intended to be a ' + 'color (or otherwise consider escaping "#" chars to "\\#").' + '\n' + ) - cal_colors[cal_name] = cal_color - return [CalName(name=k, color=cal_colors[k]) for k in cal_colors.keys()] + cal_colors[name] = cal_color + return [CalName(name=k, color=v) for k, v in cal_colors.items()] def run_add_prompt(parsed_args, printer): @@ -376,7 +398,7 @@ def set_resolved_calendars(parsed_args, printer: Printer) -> list[str]: 'calendars may be coming from gcalclirc.\n' ) - cal_names = parse_cal_names(parsed_args.calendars) + cal_names = parse_cal_names(parsed_args.calendars, printer=printer) # Only ignore calendars if they're not explicitly in --calendar list. parsed_args.ignore_calendars[:] = [ c diff --git a/tests/test_gcalcli.py b/tests/test_gcalcli.py index de2bfb8..29f67af 100644 --- a/tests/test_gcalcli.py +++ b/tests/test_gcalcli.py @@ -95,7 +95,7 @@ def test_cal_query(capsys, PatchedGCalI): def test_add_event(PatchedGCalI): - cal_names = parse_cal_names(['jcrowgey@uw.edu']) + cal_names = parse_cal_names(['jcrowgey@uw.edu'], printer=None) gcal = PatchedGCalI( cal_names=cal_names, allday=False, default_reminders=True) assert gcal.AddEvent(title='test event', @@ -109,7 +109,8 @@ def test_add_event(PatchedGCalI): def test_add_event_with_cal_prompt(PatchedGCalI, capsys, monkeypatch): - cal_names = parse_cal_names(['jcrowgey@uw.edu', 'joshuacrowgey@gmail.com']) + cal_names = parse_cal_names( + ['jcrowgey@uw.edu', 'joshuacrowgey@gmail.com'], None) gcal = PatchedGCalI( cal_names=cal_names, allday=False, default_reminders=True) # Fake selecting calendar 0 at the prompt @@ -131,7 +132,7 @@ def test_add_event_with_cal_prompt(PatchedGCalI, capsys, monkeypatch): def test_add_event_override_color(capsys, default_options, PatchedGCalIForEvents): default_options.update({'override_color': True}) - cal_names = parse_cal_names(['jcrowgey@uw.edu']) + cal_names = parse_cal_names(['jcrowgey@uw.edu'], None) gcal = PatchedGCalIForEvents(cal_names=cal_names, **default_options) gcal.AgendaQuery() captured = capsys.readouterr() @@ -141,7 +142,7 @@ def test_add_event_override_color(capsys, default_options, def test_quick_add(PatchedGCalI): - cal_names = parse_cal_names(['jcrowgey@uw.edu']) + cal_names = parse_cal_names(['jcrowgey@uw.edu'], None) gcal = PatchedGCalI(cal_names=cal_names) assert gcal.QuickAddEvent( event_text='quick test event', @@ -149,7 +150,8 @@ def test_quick_add(PatchedGCalI): def test_quick_add_with_cal_prompt(PatchedGCalI, capsys, monkeypatch): - cal_names = parse_cal_names(['jcrowgey@uw.edu', 'joshuacrowgey@gmail.com']) + cal_names = parse_cal_names( + ['jcrowgey@uw.edu', 'joshuacrowgey@gmail.com'], None) gcal = PatchedGCalI(cal_names=cal_names) # Fake selecting calendar 0 at the prompt monkeypatch.setattr('sys.stdin', io.StringIO('0\n')) @@ -271,14 +273,14 @@ def test_modify_event(PatchedGCalI): def test_import(PatchedGCalI): - cal_names = parse_cal_names(['jcrowgey@uw.edu']) + cal_names = parse_cal_names(['jcrowgey@uw.edu'], None) gcal = PatchedGCalI(cal_names=cal_names, default_reminders=True) vcal_path = TEST_DATA_DIR + '/vv.txt' assert gcal.ImportICS(icsFile=open(vcal_path, errors='replace')) def test_legacy_import(PatchedGCalI): - cal_names = parse_cal_names(['jcrowgey@uw.edu']) + cal_names = parse_cal_names(['jcrowgey@uw.edu'], None) gcal = PatchedGCalI( cal_names=cal_names, default_reminders=True, use_legacy_import=True) vcal_path = TEST_DATA_DIR + '/vv.txt' @@ -323,15 +325,15 @@ def test_parse_cal_names(PatchedGCalI): # and then assert the right number of events # for the moment, we assert 0 (which indicates successful completion of # the code path, but no events printed) - cal_names = parse_cal_names(['j*#green']) + cal_names = parse_cal_names(['j*#green'], None) gcal = PatchedGCalI(cal_names=cal_names) assert gcal.AgendaQuery() == 0 - cal_names = parse_cal_names(['j*']) + cal_names = parse_cal_names(['j*'], None) gcal = PatchedGCalI(cal_names=cal_names) assert gcal.AgendaQuery() == 0 - cal_names = parse_cal_names(['jcrowgey@uw.edu']) + cal_names = parse_cal_names(['jcrowgey@uw.edu'], None) gcal = PatchedGCalI(cal_names=cal_names) assert gcal.AgendaQuery() == 0