packages feed

language-bash (empty) → 0.2.0

raw patch · 11 files changed

+1351/−0 lines, 11 filesdep +basedep +parsecdep +prettysetup-changed

Dependencies added: base, parsec, pretty, transformers

Files

+ .gitignore view
@@ -0,0 +1,4 @@+dist/+cabal-dev/+*.o+*.hi
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Kyle Raftogianis++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Kyle Raftogianis nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,4 @@+language-bash+-------------++A library for parsing and pretty-printing Bash shell scripts.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ language-bash.cabal view
@@ -0,0 +1,44 @@+name:               language-bash+version:            0.2.0+category:           Language+license:            BSD3+license-file:       LICENSE+author:             Kyle Raftogianis+maintainer:         Kyle Raftogianis <kylerafto@gmail.com>+copyright:          Copyright (c) 2013 Kyle Raftogianis+build-type:         Simple+cabal-version:      >= 1.8+homepage:           http://github.com/knrafto/language-bash/+bug-reports:        http://github.com/knrafto/language-bash/issues+synopsis:           Parsing and pretty-printing Bash shell scripts+description:+    A library for parsing and pretty-printing Bash shell scripts.++extra-source-files:+  .gitignore+  README.md++source-repository head+  type: git+  location: git://github.com/knrafto/language-bash.git++library+  hs-source-dirs: src++  exposed-modules:+    Language.Bash.Parse+    Language.Bash.Parse.Builder+    Language.Bash.Pretty+    Language.Bash.Syntax++  other-modules:+    Language.Bash.Parse.Internal+    Language.Bash.Parse.Packrat++  build-depends:+    base         >= 4   && < 5,+    parsec       >= 3.0 && < 4.0,+    pretty       >= 1.0 && < 2.0,+    transformers >= 0.2 && < 0.4++  ghc-options: -Wall
+ src/Language/Bash/Parse.hs view
@@ -0,0 +1,396 @@+-- | Bash script and input parsing.+module Language.Bash.Parse+    ( parse+    ) where++import           Control.Applicative          hiding (many)+import           Control.Monad+import           Data.Either+import           Data.Functor.Identity+import           Text.Parsec.Char             hiding (newline)+import           Text.Parsec.Combinator       hiding (optional)+import           Text.Parsec.Error            (ParseError)+import           Text.Parsec.Pos+import           Text.Parsec.Prim             hiding (parse, (<|>))++import qualified Language.Bash.Parse.Internal as I+import           Language.Bash.Parse.Packrat+import           Language.Bash.Syntax++-- | User state.+data U = U { postHeredoc :: Maybe (State D U) }++-- | Bash parser type.+type Parser = ParsecT D U Identity++-- | Parse a script or input line into a (possibly empty) list of commands.+parse :: SourceName -> String -> Either ParseError List+parse source = runParser script (U Nothing) source . pack (initialPos source)++-------------------------------------------------------------------------------+-- Basic parsers+-------------------------------------------------------------------------------++infixl 3 </>+infix  0 ?:++-- | Backtracking choice.+(</>) :: ParsecT s u m a -> ParsecT s u m a -> ParsecT s u m a+p </> q = try p <|> q++-- | Name a parser from the front.+(?:) :: String -> ParsecT s u m a -> ParsecT s u m a+(?:) = flip (<?>)++-- | Get the next line of input.+line :: Parser String+line = lookAhead anyChar *> many (satisfy (/= '\n')) <* optional (char '\n')++-- | Parse the next here document.+heredoc :: Bool -> String -> Parser String+heredoc strip end = "here document" ?: do+    (h, s) <- lookAhead duck+    setState $ U (Just s)+    return h+  where+    process = if strip then dropWhile (== '\t') else id++    duck = do+        u <- getState+        case postHeredoc u of+            Nothing -> () <$ line+            Just s  -> () <$ setParserState s+        h <- unlines <$> heredocLines+        s <- getParserState+        return (h, s)++    heredocLines = do+        l <- process <$> line+        if l == end then return [] else (l :) <$> heredocLines++-- | Parse a newline, skipping any here documents.+newline :: Parser String+newline = "newline" ?: do+    _ <- operator "\n"+    u <- getState+    case postHeredoc u of+        Nothing -> return ()+        Just s  -> () <$ setParserState s+    setState $ U Nothing+    return "\n"++-- | Parse a list terminator.+listTerm :: Parser ListTerm+listTerm = term <* newlineList <?> "list terminator"+  where+    term = Sequential   <$ newline+       <|> Sequential   <$ operator ";"+       <|> Asynchronous <$ operator "&"++-- | Skip zero or more newlines.+newlineList :: Parser ()+newlineList = skipMany newline++-------------------------------------------------------------------------------+-- Simple commands+-------------------------------------------------------------------------------++-- | Skip a redirection.+redir :: Parser Redir+redir = normalRedir+    <|> heredocRedir+    <?> "redirection"+  where+    normalRedir = do+        desc   <- optional ioDesc+        op     <- redirOperator+        target <- anyWord+        return Redir+            { redirDesc   = desc+            , redirOp     = op+            , redirTarget = target+            }++    heredocRedir = do+        (strip, op) <- heredocOperator+        w <- anyWord+        let delim = I.unquote w+        h <- heredoc strip delim+        return Heredoc+            { redirOp            = op+            , heredocDelim       = delim+            , heredocDelimQuoted = delim /= w+            , document           = h+            }++    heredocOperator = (,) False <$> operator "<<"+                  <|> (,) True  <$> operator "<<-"++-- | Skip a list of redirections.+redirList :: Parser [Redir]+redirList = many redir++-- | Parse part of a command.+commandParts :: Parser a -> Parser ([a], [Redir])+commandParts p = partitionEithers <$> many part+  where+    part = Left  <$> p+       <|> Right <$> redir++-- | Parse a simple command.+simpleCommand :: Parser Command+simpleCommand = do+    notFollowedBy reservedWord+    assignCommand </> normalCommand+  where+    assignCommand = "assignment builtin" ?: do+        rs1 <- redirList+        w <- assignBuiltin+        (args, rs2) <- commandParts assignArg+        return $ Command (AssignBuiltin w args) (rs1 ++ rs2)++    normalCommand = "simple command" ?: do+        (as, rs1) <- commandParts assign+        (ws, rs2) <- commandParts anyWord+        guard (not $ null as && null ws)+        return $ Command (SimpleCommand as ws) (rs1 ++ rs2)++    assignArg = Left  <$> assign+            <|> Right <$> anyWord++-------------------------------------------------------------------------------+-- Lists+-------------------------------------------------------------------------------++-- | A list with one command.+singleton :: ShellCommand -> List+singleton c =+    List [Statement (Last (unmodifiedPipeline [Command c []])) Sequential]++-- | An unmodified pipeline.+unmodifiedPipeline :: [Command] -> Pipeline+unmodifiedPipeline cs = Pipeline+    { timed      = False+    , timedPosix = False+    , inverted   = False+    , commands   = cs+    }++-- | Parse a pipeline.+pipelineCommand :: Parser Pipeline+pipelineCommand = time+              <|> invert+              <|> pipeline1+              <?> "pipeline"+  where+    invert = do+        _ <- word "!"+        p <- pipeline0+        return $ p { inverted = not (inverted p) }++    time = do+        _ <- word "time"+        p <- posixFlag <|> invert <|> pipeline0+        return $ p { timed = True }++    posixFlag = do+        _ <- word "-p"+        _ <- optional (word "--")+        p <- invert <|> pipeline0+        return $ p { timedPosix = True }++    pipeline0 = unmodifiedPipeline <$> commandList0+    pipeline1 = unmodifiedPipeline <$> commandList1++    commandList0 = option [] commandList1+    commandList1 = do+        c <- command+        pipelineSep c <|> pure [c]++    pipelineSep c = do+        c' <- c          <$ operator "|"+          <|> addRedir c <$ operator "|&"+        (c' :) <$> commandList0++    addRedir (Command c rs) = Command c (stderrRedir : rs)++    stderrRedir = Redir (Just (IONumber 2)) ">&" "1"++-- | Parse a compound list of commands.+compoundList :: Parser List+compoundList = List <$ newlineList <*> many1 statement <?> "list"+  where+    statement = Statement <$> andOr <*> option Sequential listTerm++    andOr = do+        p <- pipelineCommand+        let rest = And p <$ operator "&&" <* newlineList <*> andOr+               <|> Or  p <$ operator "||" <* newlineList <*> andOr+        rest <|> pure (Last p)++-- | Parse a possible empty compound list of commands.+inputList :: Parser List+inputList = newlineList *> option (List []) compoundList++-- | Parse a command group, wrapped either in braces or in a @do...done@ block.+doGroup :: Parser List+doGroup = word "do" *> compoundList <* word "done"+      <|> word "{"  *> compoundList <* word "}"++-------------------------------------------------------------------------------+-- Compound commands+-------------------------------------------------------------------------------++-- | Parse a compound command.+shellCommand :: Parser ShellCommand+shellCommand = group+           <|> ifCommand+           <|> caseCommand+           <|> forCommand+           <|> whileCommand+           <|> untilCommand+           <|> selectCommand+           <|> condCommand+           <|> arithCommand+           <|> subshell+           <?> "compound command"++-- | Parse a @case@ command.+caseCommand :: Parser ShellCommand+caseCommand = Case <$ word "case"+          <*> anyWord <* newlineList+          <*  word "in" <* newlineList+          <*> clauses+  where+    clauses = [] <$ word "esac"+          <|> do p <- pattern+                 c <- inputList+                 nextClause (CaseClause p c)++    nextClause f = (:) <$> (f <$> clauseTerm) <* newlineList <*> clauses+               <|> [f Break] <$ newlineList <* word "esac"++    pattern = optional (operator "(")+           *> anyWord `sepBy` operator "|"+          <*  operator ")"+          <?> "pattern list"++    clauseTerm = Break       <$ operator ";;"+             <|> FallThrough <$ operator ";&"+             <|> Continue    <$ operator ";;&"+             <?> "case clause terminator"++-- | Parse a @while@ command.+whileCommand :: Parser ShellCommand+whileCommand = While <$ word "while"+           <*> compoundList+           <*  word "do" <*> compoundList <* word "done"++-- | Parse an @until@ command.+untilCommand :: Parser ShellCommand+untilCommand = Until <$ word "until"+           <*> compoundList+           <*  word "do" <*> compoundList <* word "done"++-- | Parse a list of words for a @for@ or @select@ command.+wordList :: Parser [Word]+wordList = ["$@\""] <$ operator ";" <* newlineList+       <|> newlineList *> inList+       <?> "word list"+  where+    inList = word "in" *> many anyWord <* listTerm+         <|> return ["$@\""]++-- | Parse a @for@ command.+forCommand :: Parser ShellCommand+forCommand = word "for" *> (arithFor_ <|> for_)+  where+    arithFor_ = ArithFor <$> arith <* optional listTerm <*> doGroup++    for_ = For <$> anyWord <*> wordList <*> doGroup++-- | Parse a @select@ command.+selectCommand :: Parser ShellCommand+selectCommand = Select <$ word "select" <*> anyWord <*> wordList <*> doGroup++-- | Parse an @if@ command.+ifCommand :: Parser ShellCommand+ifCommand = word "if" *> if_+  where+    if_ = If <$> compoundList <* word "then" <*> compoundList <*> alternative++    alternative = Just . singleton <$ word "elif" <*> if_+              <|> Just             <$ word "else" <*> compoundList <* word "fi"+              <|> Nothing          <$ word "fi"++-- | Parse a subshell command.+subshell :: Parser ShellCommand+subshell = Subshell <$ operator "(" <*> compoundList <* operator ")"++-- | Parse a command group.+group :: Parser ShellCommand+group = Group <$ word "{" <*> compoundList <* word "}"++-- | Parse an arithmetic command.+arithCommand :: Parser ShellCommand+arithCommand = Arith <$> arith++-- | Parse a conditional command.+condCommand :: Parser ShellCommand+condCommand = Cond <$ word "[[" <*> many1 condWord <* word "]]"++-------------------------------------------------------------------------------+-- Coprocesses+-------------------------------------------------------------------------------++-- | Parse a coprocess command.+coproc :: Parser ShellCommand+coproc = word "coproc" *> coprocCommand <?> "coprocess"+  where+    coprocCommand = Coproc <$> option "COPROC" name+                           <*> (Command <$> shellCommand <*> pure [])+                </> Coproc "COPROC" <$> simpleCommand++-------------------------------------------------------------------------------+-- Function definitions+-------------------------------------------------------------------------------++-- | Parse a function definition.+functionDef :: Parser ShellCommand+functionDef = functionDef2+          </> functionDef1+          <?> "function definition"+  where+    functionDef1 = FunctionDef <$ word "function" <*> anyWord+               <*  optional functionParens <* newlineList+               <*> functionBody++    functionDef2 = FunctionDef <$> unreservedWord+               <*  functionParens <* newlineList+               <*> functionBody++    functionParens = operator "(" <* operator ")"++    functionBody = unwrap <$> group+               <|> singleton <$> shellCommand++    unwrap (Group l) = l+    unwrap _         = List []++-------------------------------------------------------------------------------+-- Commands+-------------------------------------------------------------------------------++-- | Parse a single command.+command :: Parser Command+command = Command <$> compoundCommand <*> redirList+      <|> simpleCommand+      <?> "command"+  where+    compoundCommand = shellCommand+                  <|> coproc+                  <|> functionDef++-- | Parse an entire script (e.g. a file) as a list of commands.+script :: Parser List+script = skipSpace *> inputList <* eof
+ src/Language/Bash/Parse/Builder.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Builder-based parsing. This is useful for parsing Bash\'s complicated+-- words.+module Language.Bash.Parse.Builder+    ( -- * Builders+      Builder+    , fromChar+    , fromString+    , toString+      -- * Monoid parsing+    , (<+>)+    , many+    , many1+      -- * Characters+    , oneOf+    , noneOf+    , char+    , anyChar+    , satisfy+    , string+      -- * Spans+    , span+    , matchedPair+    ) where++import           Prelude                hiding (span)++import           Control.Applicative    hiding (many)+import qualified Control.Applicative    as Applicative+import           Data.Monoid+import qualified Text.Parsec.Char       as P+import           Text.Parsec.Prim       hiding ((<|>), many)++infixr 4 <+>++-- | An efficient 'String' builder.+type Builder = Endo String++-- | Construct a 'Builder' from a 'Char'.+fromChar :: Char -> Builder+fromChar = Endo . showChar++-- | Construct a 'Builder' from a 'String'.+fromString :: String -> Builder+fromString = Endo . showString++-- | Convert a 'Builder' to a 'String'.+toString :: Builder -> String+toString = flip appEndo ""++-- | Append two monoidal results.+(<+>) :: (Applicative f, Monoid a) => f a -> f a -> f a+(<+>) = liftA2 mappend++-- | Concat zero or more monoidal results.+many :: (Alternative f, Monoid a) => f a -> f a+many = fmap mconcat . Applicative.many++-- | Concat one or more monoidal results.+many1 :: (Alternative f, Monoid a) => f a -> f a+many1 = fmap mconcat . Applicative.some++-- | 'Builder' version of 'P.oneOf'.+oneOf :: Stream s m Char => [Char] -> ParsecT s u m Builder+oneOf cs = fromChar <$> P.oneOf cs++-- | 'Builder' version of 'P.noneOf'.+noneOf :: Stream s m Char => [Char] -> ParsecT s u m Builder+noneOf cs = fromChar <$> P.noneOf cs++-- | 'Builder' version of 'P.char'.+char :: Stream s m Char => Char -> ParsecT s u m Builder+char c = fromChar c <$ P.char c++-- | 'Builder' version of 'P.anyChar'.+anyChar :: Stream s m Char => ParsecT s u m Builder+anyChar = fromChar <$> P.anyChar++-- | 'Builder' version of 'P.satisfy'.+satisfy :: Stream s m Char => (Char -> Bool) -> ParsecT s u m Builder+satisfy p = fromChar <$> P.satisfy p++-- | 'Builder' version of 'P.string'.+string :: Stream s m Char => String -> ParsecT s u m Builder+string s = fromString s <$ P.string s++-- | @span start end escape@ parses a span of text starting with @start@ and+-- ending with @end@, with possible @escape@ sequences inside.+span+    :: Stream s m Char+    => Char+    -> Char+    -> ParsecT s u m Builder+    -> ParsecT s u m Builder+span start end esc = char start *> many inner <* char end+  where+    inner = esc <|> satisfy (/= end)++-- | @matchedPair start end escape@ parses @span start end escape@, including+-- the surrounding @start@ and @end@ characters.+matchedPair+    :: Stream s m Char+    => Char+    -> Char+    -> ParsecT s u m Builder+    -> ParsecT s u m Builder+matchedPair start end esc = char start <+> many inner <+> char end+  where+    inner = esc <|> satisfy (/= end)
+ src/Language/Bash/Parse/Internal.hs view
@@ -0,0 +1,196 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Low-level parsers.+module Language.Bash.Parse.Internal+    ( skipSpace+    , word+    , arith+    , name+    , assign+    , operator+    , unquote+    ) where++import           Control.Applicative+import           Data.Monoid+import           Text.Parsec.Char+import           Text.Parsec.Combinator      hiding (optional)+import           Text.Parsec.Prim            hiding ((<|>), many)+import           Text.Parsec.String          ()++import           Language.Bash.Parse.Builder (Builder, (<+>))+import qualified Language.Bash.Parse.Builder as B+import           Language.Bash.Syntax++-- | @surroundBy p sep@ parses zero or more occurences of @p@, beginning,+-- ending, and separated by @sep@.+surroundBy+    :: Stream s m t+    => ParsecT s u m a+    -> ParsecT s u m sep+    -> ParsecT s u m [a]+surroundBy p sep = sep *> endBy p sep++-- | Skip spaces, tabs, and comments.+skipSpace :: Stream s m Char => ParsecT s u m ()+skipSpace = skipMany spaceChar <* optional comment <?> "whitespace"+  where+    spaceChar = try (B.string "\\\n")+            <|> B.oneOf " \t"++    comment = char '#' *> many (satisfy (/= '\n'))++-- | Parse a backslash-escaped sequence.+escape :: Stream s m Char => ParsecT s u m Builder+escape = B.char '\\' <+> B.anyChar++-- | Parse a single-quoted string.+singleQuote :: Stream s m Char => ParsecT s u m Builder+singleQuote = B.matchedPair '\'' '\'' empty++-- | Parse a double-quoted string.+doubleQuote :: Stream s m Char => ParsecT s u m Builder+doubleQuote = B.matchedPair '"' '"' $ escape <|> backquote <|> dollar++-- | Parse an ANSI C string.+ansiQuote :: Stream s m Char => ParsecT s u m Builder+ansiQuote = B.char '$' <+> B.matchedPair '\'' '\'' escape++-- | Parse a locale string.+localeQuote :: Stream s m Char => ParsecT s u m Builder+localeQuote = B.char '$' <+> doubleQuote++-- | Parse a backquoted string.+backquote :: Stream s m Char => ParsecT s u m Builder+backquote = B.matchedPair '`' '`' escape++-- | Parse a brace-style parameter expansion, an arithmetic substitution,+-- or a command substitution.+dollar :: Stream s m Char => ParsecT s u m Builder+dollar = B.char '$' <+> rest+  where+    rest = braceParameter+       <|> try arithSubst+       <|> commandSubst+       <|> return mempty++    braceParameter = B.matchedPair '{' '}' $+            escape+        <|> singleQuote+        <|> doubleQuote+        <|> backquote+        <|> dollar++    arithSubst = B.string "((" <+> parens <+> B.string "))"++    commandSubst = subst++-- | Parse a process substitution.+processSubst :: Stream s m Char => ParsecT s u m Builder+processSubst = B.oneOf "<>" <+> subst++-- | Parse a parenthesized substitution.+subst :: Stream s m Char => ParsecT s u m Builder+subst = B.matchedPair '(' ')' $+        subst+    <|> B.char '#' <+> B.many (B.satisfy (/= '\n')) <+> B.char '\n'+    <|> escape+    <|> singleQuote+    <|> doubleQuote+    <|> backquote+    <|> dollar++-- | Parse a parenthesized expression.+parens :: Stream s m Char => ParsecT s u m Builder+parens = B.many inner+  where+    inner = B.matchedPair '(' ')' parens++-- | Parse a word part.+wordSpan :: Stream s m Char => ParsecT s u m Builder+wordSpan = mempty <$ try (string "\\\n")+       <|> escape+       <|> singleQuote+       <|> doubleQuote+       <|> try ansiQuote+       <|> try localeQuote+       <|> backquote+       <|> dollar+       <|> try processSubst++-- | Parse a word.+word :: Stream s m Char => ParsecT s u m String+word = B.toString <$> B.many wordPart <?> "word"+  where+    wordPart = wordSpan+           <|> B.noneOf " \t\n|&;()<>"++-- | Parse an arithmetic expression.+arith :: Stream s m Char => ParsecT s u m String+arith = B.toString <$> parens <?> "arithmetic expression"++-- | Parse a name.+name :: Stream s m Char => ParsecT s u m String+name = (:) <$> nameStart <*> many nameLetter+  where+    nameStart  = letter   <|> char '_'+    nameLetter = alphaNum <|> char '_'++-- | Parse an assignment.+assign :: Stream s m Char => ParsecT s u m Assign+assign = Assign <$> lvalue <*> assignOp <*> rvalue <?> "assignment"+  where+    lvalue = LValue <$> name <*> optional subscript++    subscript = B.toString <$> B.span '[' ']' wordSpan++    assignOp = Equals     <$ string "="+           <|> PlusEquals <$ string "+="++    rvalue = RArray <$  char '(' <*> arrayElems <* char ')'+         <|> RValue <$> word++    arrayElems = arrayElem `surroundBy` skipArraySpace++    arrayElem = do+        s <- optional (subscript <* char '=')+        w <- word+        case (s, w) of+            (Nothing, "") -> empty+            _             -> return (s, w)++    skipArraySpace = char '\n' `surroundBy` skipSpace++-- | Parse the longest available operator from a list.+operator :: Stream s m Char => [String] -> ParsecT s u m String+operator ops = go ops <?> "operator"+  where+    go xs+        | null xs      = empty+        | "" `elem` xs = try (continue xs) <|> pure ""+        | otherwise    = continue xs++    continue xs = do+        c <- anyChar+        (c :) <$> go (prefix c xs)++    prefix c = map tail . filter (\x -> not (null x) && head x == c)++-- | Unquote a word.+unquote :: String -> String+unquote s = case parse unquoteBare s s of+    Left _   -> s+    Right s' -> B.toString s'+  where+    unquoteBare = B.many $+            try unquoteEscape+        <|> try unquoteSingle+        <|> try unquoteDouble+        <|> try unquoteAnsi+        <|> try unquoteLocale+        <|> B.anyChar++    unquoteEscape = char '\\' *> B.anyChar+    unquoteSingle = B.span '\'' '\'' empty+    unquoteDouble = B.span '\"' '\"' unquoteEscape+    unquoteAnsi   = char '$' *> B.span '\'' '\'' unquoteEscape+    unquoteLocale = char '$' *> unquoteDouble
+ src/Language/Bash/Parse/Packrat.hs view
@@ -0,0 +1,186 @@+{-# LANGUAGE+    FlexibleContexts+  , FlexibleInstances+  , LambdaCase+  , MultiParamTypeClasses+  , PatternGuards+  , RecordWildCards+  #-}+-- | Memoized packrat parsing, inspired by Edward Kmett\'s+-- \"A Parsec Full of Rats\".+module Language.Bash.Parse.Packrat+    ( -- * Packrat parsing+      D+    , pack+      -- * Whitespace+    , I.skipSpace+      -- * Words+    , anyWord+    , word+    , reservedWord+    , unreservedWord+    , condWord+    , assignBuiltin+    , ioDesc+    , name+      -- * Operators+    , anyOperator+    , operator+    , redirOperator+      -- * Assignments+    , assign+      -- * Arithmetic expressions+    , arith+    ) where++import           Control.Applicative+import           Control.Monad+import           Data.Function+import           Data.Functor.Identity+import           Text.Parsec.Char+import           Text.Parsec.Combinator       hiding (optional)+import           Text.Parsec.Prim             hiding ((<|>), token)+import           Text.Parsec.Pos++import qualified Language.Bash.Parse.Internal as I+import           Language.Bash.Syntax++-- | A memoized result.+type Result d a = Consumed (Reply d () a)++-- | Build a parser from a field accessor.+rat :: Monad m => (d -> Result d a) -> ParsecT d u m a+rat f = mkPT $ \s0 -> return $+    return . patch s0 <$> f (stateInput s0)+  where+    patch (State _ _ u) (Ok a (State s p _) err) = Ok a (State s p u) err+    patch _             (Error e)                = Error e++-- | Obtain a result from a stateless parser.+womp :: d -> SourcePos -> ParsecT d () Identity a -> Result d a+womp d pos p = fmap runIdentity . runIdentity $+    runParsecT p (State d pos ())++-- | A token.+data Token+    = TWord Word+    | TIODesc IODesc++-- | A stream with memoized results.+data D = D+    { _token       :: Result D Token+    , _anyWord     :: Result D Word+    , _ioDesc      :: Result D IODesc+    , _anyOperator :: Result D String+    , _assign      :: Result D Assign+    , _uncons      :: Maybe (Char, D)+    }++instance Monad m => Stream D m Char where+    uncons = return . _uncons++-- | Create a source from a string.+pack :: SourcePos -> String -> D+pack p s = fix $ \d ->+    let result       = womp d p+        _token       = result $ do+            t <- I.word+            guard $ not (null t)+            next <- optional (lookAhead anyChar)+            I.skipSpace+            return $ case next of+                Just c | c == '<' || c == '>'+                       , Right desc <- parse (descriptor <* eof) "" t+                  -> TIODesc desc+                _ -> TWord t+        _anyWord     = result $ token >>= \case+            TWord w -> return w+            _       -> empty+        _ioDesc      = result $ token >>= \case+            TIODesc desc -> return desc+            _            -> empty+        _anyOperator = result $ I.operator normalOps <* I.skipSpace+        _assign      = result $ I.assign <* I.skipSpace+        _uncons      = case s of+            []     -> Nothing+            (x:xs) -> Just (x, pack (updatePosChar p x) xs)+    in  D {..}++-- | Parse a value satisfying the predicate.+satisfying+    :: (Stream s m t, Show a)+    => ParsecT s u m a+    -> (a -> Bool)+    -> ParsecT s u m a+satisfying a p = try $ do+    t <- a+    if p t then return t else unexpected (show t)++-- | Parse a descriptor.+descriptor :: Stream s m Char => ParsecT s u m IODesc+descriptor = IONumber . read <$> many1 digit+         <|> IOVar <$ char '{' <*> I.name <* char '}'++-- | Parse a single token.+token :: Monad m => ParsecT D u m Token+token = try (rat _token) <?> "token"++-- | Parse any word.+anyWord :: Monad m => ParsecT D u m Word+anyWord = try (rat _anyWord) <?> "word"++-- | Parse the given word.+word :: Monad m => Word -> ParsecT D u m Word+word w = anyWord `satisfying` (== w) <?> w++-- | Parse a reversed word.+reservedWord :: Monad m => ParsecT D u m Word+reservedWord = anyWord `satisfying` (`elem` reservedWords) <?> "reserved word"++-- | Parse a word that is not reserved.+unreservedWord :: Monad m => ParsecT D u m Word+unreservedWord = anyWord `satisfying` (`notElem` reservedWords)+    <?> "unreserved word"++-- | Parse a word in a @[[...]]@ command.+condWord :: Monad m => ParsecT D u m Word+condWord = (anyWord <|> anyOperator) `satisfying` (/= "]]")++-- | Parse an assignment builtin.+assignBuiltin :: Monad m => ParsecT D u m Word+assignBuiltin = anyWord `satisfying` (`elem` assignBuiltins)+    <?> "assignment builtin"++-- | Parse a redirection word or number.+ioDesc :: Monad m => ParsecT D u m IODesc+ioDesc = try (rat _ioDesc) <?> "IO descriptor"++-- | Parse a variable name.+name :: Monad m => ParsecT D u m String+name = unreservedWord `satisfying` isName <?> "name"+  where+    isName s = case parse (I.name <* eof) "" s of+        Left _  -> False+        Right _ -> True++-- | Parse any operator.+anyOperator :: Monad m => ParsecT D u m String+anyOperator = try (rat _anyOperator) <?> "operator"++-- | Parse a given operator.+operator :: Monad m => String -> ParsecT D u m String+operator op = anyOperator `satisfying` (== op) <?> op++-- | Parse a non-heredoc redirection operator.+redirOperator :: Monad m => ParsecT D u m String+redirOperator = anyOperator `satisfying` (`elem` redirOps)+    <?> "redirection operator"++-- | Parse an assignment.+assign :: Monad m => ParsecT D u m Assign+assign = try (rat _assign) <?> "assignment"++-- | Parse an arithmetic expression.+arith :: Monad m => ParsecT D u m String+arith = try (string "((") *> I.arith <* string "))" <* I.skipSpace+    <?> "arithmetic expression"
+ src/Language/Bash/Pretty.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}+-- | Pretty-printing of Bash scripts. This tries to stay close to the format+-- used by the Bash builtin @declare -f@.+module Language.Bash.Pretty+    ( Pretty(..)+    , render+    ) where++import Text.PrettyPrint++import Language.Bash.Syntax++-- | A class of types which may be pretty-printed.+class Pretty a where+    -- | Pretty-print to a 'Doc'.+    pretty     :: a -> Doc++    -- | Pretty-print a list. By default, this separates each element with+    -- a space using 'hsep'.+    prettyList :: [a] -> Doc+    prettyList = hsep . map pretty++instance Pretty a => Pretty [a] where+    pretty = prettyList++instance Pretty Doc where+    pretty = id++instance Pretty Char where+    pretty c   = text [c]+    prettyList = text++instance Pretty a => Pretty (Maybe a) where+    pretty = maybe empty pretty++instance (Pretty a, Pretty b) => Pretty (Either a b) where+    pretty = either pretty pretty++instance Pretty Command where+    pretty (Command c rs) = pretty c <+> pretty rs++instance Pretty Redir where+    pretty Redir{..} =+        pretty redirDesc <> text redirOp <> text redirTarget+    pretty Heredoc{..} =+        text redirOp <>+        text (if heredocDelimQuoted+              then "'" ++ heredocDelim ++ "'"+              else heredocDelim) <> "\n" <>+        text document <> text heredocDelim <> "\n"++    prettyList = foldr f empty+      where+        f a@(Redir{})   b = pretty a <+> b+        f a@(Heredoc{}) b = pretty a <> b++instance Pretty IODesc where+    pretty (IONumber n) = int n+    pretty (IOVar n)    = "{" <> text n <> "}"++instance Pretty ShellCommand where+    pretty (SimpleCommand as ws)  = pretty as <+> pretty ws+    pretty (AssignBuiltin w args) = text w <+> pretty args+    pretty (FunctionDef name l) =+        text name <+> "()" $+$ pretty (Group l)+    pretty (Coproc name c) =+        "coproc" <+> text name <+> pretty c+    pretty (Subshell l) =+        "(" <+> pretty l <+> ")"+    pretty (Group l) =+        "{" $+$ indent l $+$ "}"+    pretty (Arith s) =+        "((" <> text s <> "))"+    pretty (Cond ws) =+        "[[" <+> pretty ws <+> "]]"+    pretty (For w ws l) =+        "for" <+> text w <+> "in" <+> pretty ws <> ";" $+$ doDone l+    pretty (ArithFor s l) =+        "for" <+> "((" <> text s <> "))" $+$ doDone l+    pretty (Select w ws l) =+        "select" <+> text w <+> "in" <+> pretty ws <> ";" $+$ doDone l+    pretty (Case w cs) =+        "case" <+> text w <+> "in" $+$ indent cs $+$ "esac"+    pretty (If p t f) =+        "if" <+> pretty p <+> "then" $+$ indent t $+$+        pretty (fmap (\l -> "else" $+$ indent l) f) $+$+        "fi"+    pretty (Until p l) =+        "until" <+> pretty p <+> doDone l+    pretty (While p l) =+        "while" <+> pretty p <+> doDone l++instance Pretty CaseClause where+    pretty (CaseClause ps l term) =+        hcat (punctuate " | " (map text ps)) <> ")" $+$+        indent l $+$+        pretty term++instance Pretty CaseTerm where+    pretty Break       = ";;"+    pretty FallThrough = ";&"+    pretty Continue    = ";;&"++instance Pretty List where+    pretty (List as) = pretty as++instance Pretty Statement where+    pretty (Statement l Sequential)   = pretty l <> ";"+    pretty (Statement l Asynchronous) = pretty l <+> "&"++    prettyList = foldr f empty+      where+        f a@(Statement _ Sequential)   b = pretty a $+$ b+        f a@(Statement _ Asynchronous) b = pretty a <+> b++instance Pretty ListTerm where+    pretty Sequential   = ";"+    pretty Asynchronous = "&"++instance Pretty AndOr where+    pretty (Last p)  = pretty p+    pretty (And p a) = pretty p <+> "&&" <+> pretty a+    pretty (Or p a)  = pretty p <+> "||" <+> pretty a++instance Pretty Pipeline where+    pretty Pipeline{..} =+        (if timed      then "time" else empty) <+>+        (if timedPosix then "-p"   else empty) <+>+        (if inverted   then "!"    else empty) <+>+        pretty commands++instance Pretty Assign where+    pretty (Assign lhs op rhs) = pretty lhs <> pretty op <> pretty rhs++instance Pretty LValue where+    pretty (LValue name sub) =+        text name <> pretty (fmap (\s -> "[" ++ s ++ "]") sub)++instance Pretty AssignOp where+    pretty Equals     = "="+    pretty PlusEquals = "+="++instance Pretty RValue where+    pretty (RValue w)  = text w+    pretty (RArray rs) = "(" <> hsep (map f rs) <> ")"+      where+        f (sub, w) = pretty (fmap (\s -> "[" ++ s ++ "]=") sub) <> text w++-- | Indent by 4 columns.+indent :: Pretty a => a -> Doc+indent = nest 4 . pretty++-- | Render a @do...done@ block.+doDone :: Pretty a => a -> Doc+doDone a = "do" $+$ indent a $+$ "done"
+ src/Language/Bash/Syntax.hs view
@@ -0,0 +1,225 @@+-- | Shell script types.+module Language.Bash.Syntax+    ( -- * Syntax+      -- ** Words+      Word+    , Subscript+      -- ** Commands+    , Command(..)+    , Redir(..)+    , IODesc(..)+    , ShellCommand(..)+    , CaseClause(..)+    , CaseTerm(..)+      -- * Lists+    , List(..)+    , Statement(..)+    , ListTerm(..)+    , AndOr(..)+    , Pipeline(..)+      -- * Assignments+    , Assign(..)+    , LValue(..)+    , AssignOp(..)+    , RValue(..)+      -- * Syntax elements+    , reservedWords+    , assignBuiltins+    , redirOps+    , heredocOps+    , controlOps+    , normalOps+    ) where++-- | A Bash word.+type Word = String++-- | A variable subscript @[...]@.+type Subscript = Word++-- | A Bash command with redirections.+data Command = Command ShellCommand [Redir]+    deriving (Eq, Read, Show)++-- | A redirection.+data Redir+    -- | A redirection.+    = Redir+        { -- | An optional file descriptor.+          redirDesc   :: Maybe IODesc+          -- | The redirection operator.+        , redirOp     :: String+          -- | The redirection target.+        , redirTarget :: Word+        }+    -- | A here document.+    | Heredoc+        { redirOp            :: String+          -- | The here document delimiter.+        , heredocDelim       :: String+          -- | 'True' if the delimiter was quoted.+        , heredocDelimQuoted :: Bool+          -- | The document itself.+        , document           :: String+        }+    deriving (Eq, Read, Show)++-- | A redirection file descriptor.+data IODesc+    -- | A file descriptor number.+    = IONumber Int+    -- | A variable @{/varname/}@ to allocate a file descriptor for.+    | IOVar String+    deriving (Eq, Read, Show)++-- | A Bash command.+data ShellCommand+    -- | A simple command consisting of assignments followed by words.+    = SimpleCommand [Assign] [Word]+    -- | The shell builtins @declare@, @eval@, @export@, @local@, @readonly@,+    -- and @typeset@ can accept both assignments and words as arguments.+    | AssignBuiltin Word [Either Assign Word]+    -- | A function name and definition.+    | FunctionDef String List+    -- | A named coprocess.+    | Coproc String Command+    -- | A @(...)@ list, denoting a subshell.+    | Subshell List+    -- | A @{...}@ list.+    | Group List+    -- | An arithmetic expression.+    | Arith String+    -- | A Bash @[[...]]@ conditional expression.+    | Cond [Word]+    -- | A @for /word/ in /words/@ command. If @in /words/@ is absent,+    -- the word list defaults to @\"$\@\"@.+    | For Word [Word] List+    -- | An arithmetic @for ((...))@ command.+    | ArithFor String List+    -- | A @select /word/ in /words/@ command. If @in /words/@ is absent,+    -- the word list defaults to @\"$\@\"@.+    | Select Word [Word] List+    -- | A @case@ command.+    | Case Word [CaseClause]+    -- | An @if@ command, with a predicate, consequent, and alternative.+    -- @elif@ clauses are parsed as nested @if@ statements.+    | If List List (Maybe List)+    -- | An @until@ command.+    | Until List List+    -- | A @while@ command.+    | While List List+    deriving (Eq, Read, Show)++-- | A single case clause.+data CaseClause = CaseClause [Word] List CaseTerm+    deriving (Eq, Read, Show)++-- | A case clause terminator.+data CaseTerm+    -- | The @;;@ operator.+    = Break+    -- | The @;&@ operator.+    | FallThrough+    -- | The @;;&@ operator.+    | Continue+    deriving (Eq, Ord, Read, Show, Bounded, Enum)++-- | A compound list of statements.+newtype List = List [Statement]+    deriving (Eq, Read, Show)++-- | A single statement in a list.+data Statement = Statement AndOr ListTerm+    deriving (Eq, Read, Show)++-- | A statement terminator.+data ListTerm+    -- | The @;@ operator.+    = Sequential+    -- | The @&@ operator.+    | Asynchronous+    deriving (Eq, Ord, Read, Show, Bounded, Enum)++-- | A right-associative list of pipelines.+data AndOr+    -- | The last pipeline of a list.+    = Last Pipeline+    -- | An @&&@ construct.+    | And Pipeline AndOr+    -- | An @||@ construct.+    | Or Pipeline AndOr+    deriving (Eq, Read, Show)++-- | A (possibly timed or inverted) pipeline, linked with @|@ or @|&@.+data Pipeline = Pipeline+    { -- | 'True' if the pipeline is timed with @time@.+      timed      :: Bool+      -- | 'True' if the pipeline is timed with the @-p@ flag.+    , timedPosix :: Bool+      -- | 'True' if the pipeline is inverted with @!@.+    , inverted   :: Bool+      -- | A list of commands, separated by @|@, or @|&@.+      -- @command1 |& command2@ is treated as a shorthand for+      -- @command1 2>&1 | command2@.+    , commands   :: [Command]+    } deriving (Eq, Read, Show)++-- | An assignment.+data Assign = Assign LValue AssignOp RValue+    deriving (Eq, Read, Show)++-- | The left side of an assignment.+data LValue = LValue String (Maybe Subscript)+    deriving (Eq, Read, Show)++-- | An assignment operator.+data AssignOp+    -- | The @=@ operator.+    = Equals+    -- | The @+=@ operator.+    | PlusEquals+    deriving (Eq, Ord, Read, Show, Bounded, Enum)++-- | The right side of an assignment.+data RValue+    -- | A simple word.+    = RValue Word+    -- | An array assignment.+    | RArray [(Maybe Subscript, Word)]+    deriving (Eq, Read, Show)++-- | Shell reserved words.+reservedWords :: [Word]+reservedWords =+    [ "!", "[[", "]]", "{", "}"+    , "if", "then", "else", "elif", "fi"+    , "case", "esac", "for", "select", "while", "until"+    , "in", "do", "done", "time", "function"+    ]++-- | Shell assignment builtins. These builtins can take assignments as+-- arguments.+assignBuiltins :: [Word]+assignBuiltins =+    [ "alias", "declare", "export", "eval"+    , "let", "local", "readonly", "typeset"+    ]++-- | Redirection operators, not including here document operators.+redirOps :: [String]+redirOps = [">", "<", ">>", ">|", "<>", "<<<", "<&", ">&", "&>", "&>>"]++-- | Here document operators.+heredocOps :: [String]+heredocOps = ["<<", "<<-"]++-- | Shell control operators.+controlOps :: [String]+controlOps =+    [ "(", ")", ";;", ";&", ";;&"+    , "|", "|&", "||", "&&", ";", "&", "\n"+    ]++-- | All normal operators.+normalOps :: [String]+normalOps = redirOps ++ heredocOps ++ controlOps