packages feed

shellwords-0.1.4.0: src/ShellWords/Quote.hs

{-# LANGUAGE NamedFieldPuns #-}

module ShellWords.Quote
  ( -- * Quoting for shells
    quote
  , join
  ) where

import Prelude

-- | How to escape a `String` for @sh(1)@.
data EscapeStyle
  = -- | No escaping.
    NoEscaping
  | -- | Wrapped in single quotes.
    SingleQuoted
  | -- | Wrapped in single quotes
    Mixed

-- | Internal state for `escapeStyle`.
data EscapeStyleState
  = EscapeStyleState
  { hasSpecial :: Bool
  , hasNewline :: Bool
  , hasSingleQuote :: Bool
  }

-- | Determine how to escape a `String`.
escapeStyle :: String -> EscapeStyle
escapeStyle "" = SingleQuoted
escapeStyle str =
  let
    isOtherSpecial :: Char -> Bool
    isOtherSpecial c = c `elem` ("|&;<>()$`\\\" \t*?[#~=%" :: String)

    helper :: EscapeStyleState -> String -> EscapeStyle
    helper state (c : s)
      | c == '\n' = helper state {hasNewline = True, hasSpecial = True} s
      | c == '\'' = helper state {hasSingleQuote = True, hasSpecial = True} s
      | isOtherSpecial c = helper state {hasSpecial = True} s
      | otherwise = helper state s
    helper EscapeStyleState {hasSpecial, hasNewline, hasSingleQuote} []
      | not hasSpecial = NoEscaping
      | hasNewline && not hasSingleQuote = SingleQuoted
      | otherwise = Mixed
  in
    helper
      EscapeStyleState
        { hasSpecial = False
        , hasNewline = False
        , hasSingleQuote = False
        }
      str

-- | Escape special characters in a string, so that it will retain its literal
-- meaning when used as a part of command in a Unix shell.
--
-- It tries to avoid introducing any unnecessary quotes or escape characters,
-- but specifics regarding quoting style are left unspecified.
quote :: String -> String
quote str =
  case escapeStyle str of
    NoEscaping -> str
    SingleQuoted -> "'" <> str <> "'"
    Mixed ->
      let
        quoteMixed :: String -> String
        quoteMixed [] = []
        quoteMixed ('\'' : s) = "'\\''" <> quoteMixed s
        quoteMixed (c : s) = c : quoteMixed s
      in
        "'" <> quoteMixed str <> "'"

-- | Joins arguments into a single command line suitable for execution in a Unix shell.
--
-- Each argument is quoted using `quote` to preserve its literal meaning when
-- parsed by Unix shell.
--
-- Note: This function is essentially an (infallible) inverse of `parse`.
join :: [String] -> String
join = unwords . map quote