GHC/ResponseFile.hs

    {-# LANGUAGE ScopedTypeVariables #-}

    -----------------------------------------------------------------------------
    -- |
    -- Module      :  GHC.ResponseFile
    -- License     :  BSD-style (see the file LICENSE)
    --
    -- Maintainer  :  libraries@haskell.org
    -- Stability   :  internal
    -- Portability :  portable
    --
    -- GCC style response files.
    --
    -- @since 4.12.0.0
    ----------------------------------------------------------------------------

    -- Migrated from Haddock.

    module GHC.ResponseFile (
        getArgsWithResponseFiles,
        unescapeArgs,
        escapeArgs,
        expandResponse
      ) where

    import Control.Exception
    import Data.Char          (isSpace)
    import Data.Foldable      (foldl')
    import System.Environment (getArgs)
    import System.Exit        (exitFailure)
    import System.IO

    {-|
    Like 'getArgs', but can also read arguments supplied via response files.

    For example, consider a program @foo@:

    @
    main :: IO ()
    main = do
      args <- getArgsWithResponseFiles
      putStrLn (show args)
    @

    And a response file @args.txt@:

    @
    --one 1
    --'two' 2
    --"three" 3
    @

    Then the result of invoking @foo@ with @args.txt@ is:

    > > ./foo @args.txt
    > ["--one","1","--two","2","--three","3"]

    -}
    getArgsWithResponseFiles :: IO [String]
    getArgsWithResponseFiles = getArgs >>= expandResponse

    -- | Given a string of concatenated strings, separate each by removing
    -- a layer of /quoting/ and\/or /escaping/ of certain characters.
    --
    -- These characters are: any whitespace, single quote, double quote,
    -- and the backslash character.  The backslash character always
    -- escapes (i.e., passes through without further consideration) the
    -- character which follows.  Characters can also be escaped in blocks
    -- by quoting (i.e., surrounding the blocks with matching pairs of
    -- either single- or double-quotes which are not themselves escaped).
    --
    -- Any whitespace which appears outside of either of the quoting and
    -- escaping mechanisms, is interpreted as having been added by this
    -- special concatenation process to designate where the boundaries
    -- are between the original, un-concatenated list of strings.  These
    -- added whitespace characters are removed from the output.
    --
    -- > unescapeArgs "hello\\ \\\"world\\\"\n" == escapeArgs "hello \"world\""
    unescapeArgs :: String -> [String]
    unescapeArgs = filter (not . null) . unescape

    -- | Given a list of strings, concatenate them into a single string
    -- with escaping of certain characters, and the addition of a newline
    -- between each string.  The escaping is done by adding a single
    -- backslash character before any whitespace, single quote, double
    -- quote, or backslash character, so this escaping character must be
    -- removed.  Unescaped whitespace (in this case, newline) is part
    -- of this "transport" format to indicate the end of the previous
    -- string and the start of a new string.
    --
    -- While 'unescapeArgs' allows using quoting (i.e., convenient
    -- escaping of many characters) by having matching sets of single- or
    -- double-quotes,'escapeArgs' does not use the quoting mechasnism,
    -- and thus will always escape any whitespace, quotes, and
    -- backslashes.
    --
    -- > unescapeArgs "hello\\ \\\"world\\\"\\n" == escapeArgs "hello \"world\""
    escapeArgs :: [String] -> String
    escapeArgs = unlines . map escapeArg

    -- | Arguments which look like '@foo' will be replaced with the
    -- contents of file @foo@. A gcc-like syntax for response files arguments
    -- is expected.  This must re-constitute the argument list by doing an
    -- inverse of the escaping mechanism done by the calling-program side.
    --
    -- We quit if the file is not found or reading somehow fails.
    -- (A convenience routine for haddock or possibly other clients)
    expandResponse :: [String] -> IO [String]
    expandResponse = fmap concat . mapM expand
      where
        expand :: String -> IO [String]
        expand ('@':f) = readFileExc f >>= return . unescapeArgs
        expand x = return [x]

        readFileExc f =
          readFile f `catch` \(e :: IOException) -> do
            hPutStrLn stderr $ "Error while expanding response file: " ++ show e
            exitFailure

    data Quoting = NoneQ | SngQ | DblQ

    unescape :: String -> [String]
    unescape args = reverse . map reverse $ go args NoneQ False [] []
        where
          -- n.b., the order of these cases matters; these are cribbed from gcc
          -- case 1: end of input
          go []     _q    _bs   a as = a:as
          -- case 2: back-slash escape in progress
          go (c:cs) q     True  a as = go cs q     False (c:a) as
          -- case 3: no back-slash escape in progress, but got a back-slash
          go (c:cs) q     False a as
            | '\\' == c              = go cs q     True  a     as
          -- case 4: single-quote escaping in progress
          go (c:cs) SngQ  False a as
            | '\'' == c              = go cs NoneQ False a     as
            | otherwise              = go cs SngQ  False (c:a) as
          -- case 5: double-quote escaping in progress
          go (c:cs) DblQ  False a as
            | '"' == c               = go cs NoneQ False a     as
            | otherwise              = go cs DblQ  False (c:a) as
          -- case 6: no escaping is in progress
          go (c:cs) NoneQ False a as
            | isSpace c              = go cs NoneQ False []    (a:as)
            | '\'' == c              = go cs SngQ  False a     as
            | '"'  == c              = go cs DblQ  False a     as
            | otherwise              = go cs NoneQ False (c:a) as

    escapeArg :: String -> String
    escapeArg = reverse . foldl' escape []

    escape :: String -> Char -> String
    escape cs c
      |    isSpace c
        || '\\' == c
        || '\'' == c
        || '"'  == c = c:'\\':cs -- n.b., our caller must reverse the result
      | otherwise    = c:cs