Repository: qutebrowser/qutebrowser

Issue:
Check that userscript exists before attempting to execute it
If a userscript is invoked by the `:spawn --userscript myscript` command, but `myscript` does not exist in the userscripts directory, qutebrowser gives the uninformative message:

```
Error while spawning userscript: The process failed to start.
```

The log output is similarly opaque. The log output does make it clear that qutebrowser simply assumes the existence of the userscript and attempts to execute it.

It would be more useful if qutebrowser checked that the userscript exists in the userscript directory before attempting to execute it. If the userscript cannot be found qutebrowser could provide a specific, more helpful, error message like:

```
Userscript 'myscript' not found in userscript directory 'path/to/userscript/directory'
```



Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -1122,7 +1122,7 @@ def _run_userscript(self, cmd, *args, verbose=False):
         try:
             userscripts.run_async(tab, cmd, *args, win_id=self._win_id,
                                   env=env, verbose=verbose)
-        except userscripts.UnsupportedError as e:
+        except userscripts.Error as e:
             raise cmdexc.CommandError(e)
 
     @cmdutils.register(instance='command-dispatcher', scope='window')
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -315,7 +315,34 @@ def prepare_run(self, *args, **kwargs):
             return
 
 
-class UnsupportedError(Exception):
+class Error(Exception):
+
+    """Base class for userscript exceptions."""
+
+
+class NotFoundError(Error):
+
+    """Raised when spawning a userscript that doesn't exist.
+
+    Attributes:
+        script_name: name of the userscript as called
+        paths: path names that were searched for the userscript
+    """
+
+    def __init__(self, script_name, paths=None):
+        super().__init__()
+        self.script_name = script_name
+        self.paths = paths
+
+    def __str__(self):
+        msg = "Userscript '{}' not found".format(self.script_name)
+        if self.paths:
+            msg += " in userscript directories {}".format(
+                ', '.join(repr(path) for path in self.paths))
+        return msg
+
+
+class UnsupportedError(Error):
 
     """Raised when userscripts aren't supported on this platform."""
 
@@ -323,12 +350,37 @@ def __str__(self):
         return "Userscripts are not supported on this platform!"
 
 
+def _lookup_path(cmd):
+    """Search userscript directories for given command.
+
+    Raises:
+        NotFoundError if the command could not be found.
+
+    Args:
+        cmd: The command to look for.
+
+    Returns:
+        A path to the userscript.
+    """
+    directories = [
+        os.path.join(standarddir.data(), "userscripts"),
+        os.path.join(standarddir.system_data(), "userscripts"),
+    ]
+    for directory in directories:
+        cmd_path = os.path.join(directory, cmd)
+        if os.path.exists(cmd_path):
+            return cmd_path
+
+    raise NotFoundError(cmd, directories)
+
+
 def run_async(tab, cmd, *args, win_id, env, verbose=False):
     """Run a userscript after dumping page html/source.
 
     Raises:
         UnsupportedError if userscripts are not supported on the current
         platform.
+        NotFoundError if the command could not be found.
 
     Args:
         tab: The WebKitTab/WebEngineTab to get the source from.
@@ -371,10 +423,9 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
     # ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_DIR)
     if not os.path.isabs(cmd_path):
         log.misc.debug("{} is no absolute path".format(cmd_path))
-        cmd_path = os.path.join(standarddir.data(), "userscripts", cmd)
-        if not os.path.exists(cmd_path):
-            cmd_path = os.path.join(standarddir.system_data(),
-                                    "userscripts", cmd)
+        cmd_path = _lookup_path(cmd)
+    elif not os.path.exists(cmd_path):
+        raise NotFoundError(cmd_path)
     log.misc.debug("Userscript to run: {}".format(cmd_path))
 
     runner.finished.connect(commandrunner.deleteLater)


Test patch:
diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature
--- a/tests/end2end/features/spawn.feature
+++ b/tests/end2end/features/spawn.feature
@@ -10,7 +10,11 @@ Feature: :spawn
 
     Scenario: Starting a userscript which doesn't exist
         When I run :spawn -u this_does_not_exist
-        Then the error "Error while spawning userscript: The process failed to start." should be shown
+        Then the error "Userscript 'this_does_not_exist' not found in userscript directories *" should be shown
+
+    Scenario: Starting a userscript with absoloute path which doesn't exist
+        When I run :spawn -u /this_does_not_exist
+        Then the error "Userscript '/this_does_not_exist' not found" should be shown
 
     # https://github.com/The-Compiler/qutebrowser/issues/1614
     @posix