From 31bcc70efbc24bb98a52c4c261f02f5e36f6ee0a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 10 Apr 2015 19:45:59 +0200 Subject: [PATCH 1/4] Treat commands using ;; in key config as valid. --- qutebrowser/config/parsers/keyconf.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 5d94a26fc..3d9fc6556 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -257,15 +257,32 @@ class KeyConfigParser(QObject): self.is_dirty = True self.config_dirty.emit() + def _validate_command(self, line): + """Check if a given command is valid.""" + commands = line.split(';;') + try: + cmd = cmdutils.cmd_dict[commands[0]] + if cmd.no_cmd_split: + commands = [line] + except KeyError: + pass + + for cmd in commands: + if not cmd.strip(): + raise KeyConfigError("Got empty command (line: {!r})!".format( + line)) + commands = [c.split(maxsplit=1)[0].strip() for c in commands] + for cmd in commands: + if cmd not in cmdutils.cmd_dict: + raise KeyConfigError("Invalid command '{}'!".format(cmd)) + def _read_command(self, line): """Read a command from a line.""" if self._cur_section is None: raise KeyConfigError("Got command '{}' without getting a " "section!".format(line)) else: - command = line.split(maxsplit=1)[0] - if command not in cmdutils.cmd_dict: - raise KeyConfigError("Invalid command '{}'!".format(command)) + self._validate_command(line) for rgx, repl in configdata.CHANGED_KEY_COMMANDS: if rgx.match(line): line = rgx.sub(repl, line) From 6b0c16f1090b1184c001126f74d82287a9b0744f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 11 Apr 2015 13:20:56 +0200 Subject: [PATCH 2/4] Fix default 'ga' binding. --- qutebrowser/config/configdata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index af0f5ac17..bd9187a8d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1106,7 +1106,7 @@ KEY_DATA = collections.OrderedDict([ ('set-cmd-text :open -b {url}', ['xO']), ('set-cmd-text -s :open -w', ['wo']), ('set-cmd-text :open -w {url}', ['wO']), - ('open -t', ['ga']), + ('open -t', ['ga', '']), ('tab-close', ['d', '']), ('tab-close -o', ['D']), ('tab-only', ['co']), @@ -1189,7 +1189,6 @@ KEY_DATA = collections.OrderedDict([ ('tab-focus last', ['']), ('enter-mode passthrough', ['']), ('quit', ['']), - ('open -t', ['']), ('scroll-page 0 1', ['']), ('scroll-page 0 -1', ['']), ('scroll-page 0 0.5', ['']), From e24b06cdf97dafaa4062058a760a0d9f75db70e2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 13 Apr 2015 07:16:41 +0200 Subject: [PATCH 3/4] Refactor and fix split commands in CommandRunner. - split() now returns a ParseResult namedtuple with (cmd, args, cmdline) arguments instead of only returning cmdline and setting self._cmd/self._args. - Handling of split commands (;;) is now done in a separate parse_all() function instead of run() to make testing easier. See #615. --- qutebrowser/commands/runners.py | 116 +++++++++++++++++----------- qutebrowser/completion/completer.py | 3 +- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index a0cde9992..138f2b8b2 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -19,6 +19,8 @@ """Module containing command managers (SearchRunner and CommandRunner).""" +import collections + from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl from PyQt5.QtWebKitWidgets import QWebPage @@ -28,6 +30,9 @@ from qutebrowser.utils import message, log, utils, objreg, qtutils from qutebrowser.misc import split +ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline') + + def replace_variables(win_id, arglist): """Utility function to replace variables like {url} in a list of args.""" args = [] @@ -152,15 +157,11 @@ class CommandRunner(QObject): """Parse and run qutebrowser commandline commands. Attributes: - _cmd: The command which was parsed. - _args: The arguments which were parsed. _win_id: The window this CommandRunner is associated with. """ def __init__(self, win_id, parent=None): super().__init__(parent) - self._cmd = None - self._args = [] self._win_id = win_id def _get_alias(self, text): @@ -186,7 +187,34 @@ class CommandRunner(QObject): new_cmd += ' ' return new_cmd - def parse(self, text, aliases=True, fallback=False, keep=False): + def parse_all(self, text, *args, **kwargs): + """Split a command on ;; and parse all parts. + + If the first command in the commandline is a non-split one, it only + returns that. + + Args: + text: Text to parse. + *args/**kwargs: Passed to parse(). + + Yields: + ParseResult tuples. + """ + if ';;' in text: + # Get the first command and check if it doesn't want to have ;; + # split. + first = text.split(';;')[0] + result = self.parse(first, *args, **kwargs) + if result.cmd.no_cmd_split: + sub_texts = [text] + else: + sub_texts = [e.strip() for e in text.split(';;')] + else: + sub_texts = [text] + for sub in sub_texts: + yield self.parse(sub, *args, **kwargs) + + def parse(self, text, *, aliases=True, fallback=False, keep=False): """Split the commandline text into command and arguments. Args: @@ -197,7 +225,7 @@ class CommandRunner(QObject): keep: Whether to keep special chars and whitespace Return: - A split string commandline, e.g ['open', 'www.google.com'] + A (cmd, args, cmdline) ParseResult tuple. """ cmdstr, sep, argstr = text.partition(' ') if not cmdstr and not fallback: @@ -209,29 +237,34 @@ class CommandRunner(QObject): return self.parse(new_cmd, aliases=False, fallback=fallback, keep=keep) try: - self._cmd = cmdutils.cmd_dict[cmdstr] + cmd = cmdutils.cmd_dict[cmdstr] except KeyError: - if fallback and keep: - cmdstr, sep, argstr = text.partition(' ') - return [cmdstr, sep] + argstr.split() - elif fallback: - return text.split() + if fallback: + cmd = None + args = None + if keep: + cmdstr, sep, argstr = text.partition(' ') + cmdline = [cmdstr, sep] + argstr.split() + else: + cmdline = text.split() else: - raise cmdexc.NoSuchCommandError( - '{}: no such command'.format(cmdstr)) - self._split_args(argstr, keep) - retargs = self._args[:] - if keep and retargs: - return [cmdstr, sep + retargs[0]] + retargs[1:] - elif keep: - return [cmdstr, sep] + raise cmdexc.NoSuchCommandError('{}: no such command'.format( + cmdstr)) else: - return [cmdstr] + retargs + args = self._split_args(cmd, argstr, keep) + if keep and args: + cmdline = [cmdstr, sep + args[0]] + args[1:] + elif keep: + cmdline = [cmdstr, sep] + else: + cmdline = [cmdstr] + args[:] + return ParseResult(cmd=cmd, args=args, cmdline=cmdline) - def _split_args(self, argstr, keep): + def _split_args(self, cmd, argstr, keep): """Split the arguments from an arg string. Args: + cmd: The command we're currently handling. argstr: An argument string. keep: Whether to keep special chars and whitespace @@ -239,9 +272,9 @@ class CommandRunner(QObject): A list containing the splitted strings. """ if not argstr: - self._args = [] - elif self._cmd.maxsplit is None: - self._args = split.split(argstr, keep=keep) + return [] + elif cmd.maxsplit is None: + return split.split(argstr, keep=keep) else: # If split=False, we still want to split the flags, but not # everything after that. @@ -259,18 +292,16 @@ class CommandRunner(QObject): for i, arg in enumerate(split_args): arg = arg.strip() if arg.startswith('-'): - if arg.lstrip('-') in self._cmd.flags_with_args: + if arg.lstrip('-') in cmd.flags_with_args: flag_arg_count += 1 else: - self._args = [] - maxsplit = i + self._cmd.maxsplit + flag_arg_count - self._args = split.simple_split(argstr, keep=keep, - maxsplit=maxsplit) - break - else: + maxsplit = i + cmd.maxsplit + flag_arg_count + return split.simple_split(argstr, keep=keep, + maxsplit=maxsplit) + else: # pylint: disable=useless-else-on-loop # If there are only flags, we got it right on the first try # already. - self._args = split_args + return split_args def run(self, text, count=None): """Parse a command from a line of text and run it. @@ -279,19 +310,12 @@ class CommandRunner(QObject): text: The text to parse. count: The count to pass to the command. """ - self.parse(text) - if ';;' in text: - # Get the first command and check if it doesn't want to have ;; - # split. - if not self._cmd.no_cmd_split: - for sub in text.split(';;'): - self.run(sub, count) - return - args = replace_variables(self._win_id, self._args) - if count is not None: - self._cmd.run(self._win_id, args, count=count) - else: - self._cmd.run(self._win_id, args) + for result in self.parse_all(text): + args = replace_variables(self._win_id, result.args) + if count is not None: + result.cmd.run(self._win_id, args, count=count) + else: + result.cmd.run(self._win_id, args) @pyqtSlot(str, int) def run_safely(self, text, count=None): diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 169465cd3..2fd1858ca 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -302,7 +302,8 @@ class Completer(QObject): # the whitespace. return [text] runner = runners.CommandRunner(self._win_id) - parts = runner.parse(text, fallback=True, aliases=aliases, keep=keep) + result = runner.parse(text, fallback=True, aliases=aliases, keep=keep) + parts = result.cmdline if self._empty_item_idx is not None: log.completion.debug("Empty element queued at {}, " "inserting.".format(self._empty_item_idx)) From d700d1878029ac06ac2cb3c45085d188b646c546 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 13 Apr 2015 07:39:18 +0200 Subject: [PATCH 4/4] Fix handling of no_cmd_split cmds with args. When we have something like ":bind x foo;;bar" it wasn't recognized "bind" is a no_cmd_split command because we tried to look up "bind x foo" in cmd_dict. See #615. --- qutebrowser/config/parsers/keyconf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 3d9fc6556..824841a21 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -261,10 +261,11 @@ class KeyConfigParser(QObject): """Check if a given command is valid.""" commands = line.split(';;') try: - cmd = cmdutils.cmd_dict[commands[0]] + first_cmd = commands[0].split(maxsplit=1)[0].strip() + cmd = cmdutils.cmd_dict[first_cmd] if cmd.no_cmd_split: commands = [line] - except KeyError: + except (KeyError, IndexError): pass for cmd in commands: