From af46758ff17dab17d23a1f30b92a0c470bc53f29 Mon Sep 17 00:00:00 2001
From: Pontus Andersson <epontan@gmail.com>
Date: Mon, 22 Apr 2019 14:34:38 +0200
Subject: [PATCH] Add option to look for sources in alternate root paths

Add a new optional flag "-r|--root ROOTPATHS", where ROOTPATHS is a
colon separated list of paths, that will look for external sources in
alternate roots.

This is particular useful when the run-time environment does not fully
match the development environment. The #shellcheck source=file directive
is useful, but has its limitations in certain scenarios. Also, in many
cases the directive could be removed from scripts when the root flag is
used.

Script example.bash:
  #!/bin/bash
  source /etc/foo/config

Example usage where etc/foo/config exists in skel/foo:
  # shellcheck -x -r skel/foo:skel/core example.bash
---
 shellcheck.hs               | 30 ++++++++++++++++++++++++++++++
 src/ShellCheck/Interface.hs |  4 ++++
 src/ShellCheck/Parser.hs    |  4 +++-
 3 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/shellcheck.hs b/shellcheck.hs
index 06516e5..c57c379 100644
--- a/shellcheck.hs
+++ b/shellcheck.hs
@@ -69,6 +69,7 @@ instance Monoid Status where
 data Options = Options {
     checkSpec        :: CheckSpec,
     externalSources  :: Bool,
+    rootPaths        :: [FilePath],
     formatterOptions :: FormatterOptions,
     minSeverity      :: Severity
 }
@@ -76,6 +77,7 @@ data Options = Options {
 defaultOptions = Options {
     checkSpec = emptyCheckSpec,
     externalSources = False,
+    rootPaths = [],
     formatterOptions = newFormatterOptions {
         foColorOption = ColorAuto
     },
@@ -98,6 +100,9 @@ options = [
         "Output format (" ++ formatList ++ ")",
     Option "" ["norc"]
         (NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
+    Option "r" ["root"]
+        (ReqArg (Flag "root") "ROOTPATHS")
+        "Specify alternate root path(s) when looking for sources (colon separated)",
     Option "s" ["shell"]
         (ReqArg (Flag "shell") "SHELLNAME")
         "Specify dialect (sh, bash, dash, ksh)",
@@ -311,6 +316,12 @@ parseOption flag options =
                 }
             }
 
+        Flag "root" str -> do
+            let paths = filter (not . null) $ split ':' str
+            return options {
+                rootPaths = paths
+            }
+
         Flag "sourced" _ ->
             return options {
                 checkSpec = (checkSpec options) {
@@ -362,8 +373,10 @@ ioInterface options files = do
     inputs <- mapM normalize files
     cache <- newIORef emptyCache
     configCache <- newIORef ("", Nothing)
+    let rootPathsCache = rootPaths options
     return SystemInterface {
         siReadFile = get cache inputs,
+        siFindSource = findSourceFile rootPathsCache,
         siGetConfig = getConfig configCache
     }
   where
@@ -455,6 +468,23 @@ ioInterface options files = do
             putStrLn $ file ++ ": " ++ show err
             return ("", True)
 
+    findSourceFile rootPaths file = do
+        case file of
+            ('/':root) -> do
+                source <- find root
+                return source
+            _ ->
+                return file
+        where
+            find root = do
+                sources <- filterM doesFileExist paths
+                case sources of
+                    [] -> return file
+                    (first:_) -> return first
+                where
+                    paths = map join rootPaths
+                    join path = joinPath [path, root]
+
 inputFile file = do
     (handle, shouldCache) <-
             if file == "-"
diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs
index 0661386..8b0ba0f 100644
--- a/src/ShellCheck/Interface.hs
+++ b/src/ShellCheck/Interface.hs
@@ -73,6 +73,8 @@ import qualified Data.Map as Map
 data SystemInterface m = SystemInterface {
     -- Read a file by filename, or return an error
     siReadFile :: String -> m (Either ErrorMessage String),
+    -- Find source file in alternate root paths
+    siFindSource :: String -> m (FilePath),
     -- Get the configuration file (name, contents) for a filename
     siGetConfig :: String -> m (Maybe (FilePath, String))
 }
@@ -287,6 +289,7 @@ data ColorOption =
 mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
 mockedSystemInterface files = SystemInterface {
     siReadFile = rf,
+    siFindSource = fs,
     siGetConfig = const $ return Nothing
 }
   where
@@ -294,6 +297,7 @@ mockedSystemInterface files = SystemInterface {
         case filter ((== file) . fst) files of
             [] -> return $ Left "File not included in mock."
             [(_, contents)] -> return $ Right contents
+    fs file = return file
 
 mockRcFile rcfile mock = mock {
     siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs
index 4ecf602..2abdb36 100644
--- a/src/ShellCheck/Parser.hs
+++ b/src/ShellCheck/Parser.hs
@@ -2087,7 +2087,9 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
                 input <-
                     if filename == "/dev/null" -- always allow /dev/null
                     then return (Right "")
-                    else system $ siReadFile sys filename
+                    else do
+                        filename' <- system $ siFindSource sys filename
+                        system $ siReadFile sys filename'
                 case input of
                     Left err -> do
                         parseNoteAtId (getId file) InfoC 1091 $