From 9e609bae8d828a266e4444573eddba5babc8784a Mon Sep 17 00:00:00 2001
From: Florian Bruhin <git@the-compiler.org>
Date: Sat, 3 May 2014 14:25:22 +0200
Subject: [PATCH] Add :spawn command

---
 TODO                              |  1 -
 qutebrowser/browser/curcommand.py | 19 +++++++++++++++++++
 qutebrowser/utils/misc.py         | 28 ++++++++++++++++++++++++++++
 3 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/TODO b/TODO
index a820a8fc5..f37da377e 100644
--- a/TODO
+++ b/TODO
@@ -46,7 +46,6 @@ catch import errors for PyQt and QtWebKit
          elem = frame.findFirstElement('*:focus')
 somehow unfocus elements (hide blinking cursor) when insert mode is left?
 tabs: some more padding?
-exec command for shell
 custom stylesheet
 Really fix URL detection properly
 
diff --git a/qutebrowser/browser/curcommand.py b/qutebrowser/browser/curcommand.py
index ccbe08da4..e3e94fcb6 100644
--- a/qutebrowser/browser/curcommand.py
+++ b/qutebrowser/browser/curcommand.py
@@ -19,6 +19,7 @@
 
 import os
 import logging
+import subprocess
 from tempfile import mkstemp
 from functools import partial
 
@@ -32,6 +33,7 @@ import qutebrowser.utils.message as message
 import qutebrowser.commands.utils as cmdutils
 import qutebrowser.utils.webelem as webelem
 import qutebrowser.config.config as config
+from qutebrowser.utils.misc import shell_escape
 
 
 class CurCommandDispatcher(QObject):
@@ -387,6 +389,23 @@ class CurCommandDispatcher(QObject):
         tab = self._tabs.currentWidget()
         tab.zoom(-count)
 
+    @cmdutils.register(instance='mainwindow.tabs.cur', split=False)
+    def spawn(self, cmd):
+        """Spawn a command in a shell. {} gets replaced by the current URL.
+
+        The URL will already be quoted correctly, so there's no need to do
+        that.
+
+        The command will be run in a shell, so you can use shell features like
+        redirections.
+
+        Args:
+            cmd: The command to execute.
+        """
+        url = urlutils.urlstring(self._tabs.currentWidget().url())
+        cmd = cmd.replace('{}', shell_escape(url))
+        subprocess.Popen(cmd, shell=True)
+
     @cmdutils.register(instance='mainwindow.tabs.cur', modes=['insert'],
                        name='open_editor', hide=True, needs_js=True)
     def editor(self):
diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py
index 8affc9a5d..7975df56a 100644
--- a/qutebrowser/utils/misc.py
+++ b/qutebrowser/utils/misc.py
@@ -17,6 +17,7 @@
 
 """Other utilities which don't fit anywhere else."""
 
+import re
 import shlex
 from functools import reduce
 from pkg_resources import resource_string
@@ -76,3 +77,30 @@ def safe_shlex_split(s):
         else:
             raise
         return shlex.split(s)
+
+
+def shell_escape(s):
+    """Escape a string so it's safe to pass to a shell.
+
+    Backported from python's shlex because that's only available since 3.3 and
+    we might want to support 3.2.
+
+    FIXME: Make this work correctly in Windows, but I'd probably rather kill
+    myself. [1] might help.
+
+    [1] https://en.wikibooks.org/wiki/Windows_Batch_Scripting#How_a_command_line_is_interpreted
+    """
+
+    try:
+        return shlex.quote(s)
+    except AttributeError:
+        _find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search
+
+        if not s:
+            return "''"
+        if _find_unsafe(s) is None:
+            return s
+
+        # use single quotes, and put single quotes into double quotes
+        # the string $'b is then quoted as '$'"'"'b'
+        return "'" + s.replace("'", "'\"'\"'") + "'"