{-# LANGUAGE OverloadedStrings, PatternGuards, CPP #-}
module Language.Bash.Expand
( braceExpand
, TildePrefix(..)
, tildePrefix
, splitWord
) where
#if __GLASGOW_HASKELL__ >= 710
import Prelude hiding ((<>), Word)
#else
import Data.Traversable (traverse)
#endif
import Control.Applicative
import Control.Monad
import Data.Char
import Text.Parsec.Combinator hiding (optional, manyTill)
import Text.Parsec.Prim hiding ((<|>), many, token)
import Text.Parsec.String ()
import Text.PrettyPrint hiding (char)
import Language.Bash.Pretty
import Language.Bash.Word hiding (prefix)
type Parser = Parsec Word ()
infixl 3 </>
(</>) :: ParsecT s u m a -> ParsecT s u m a -> ParsecT s u m a
p </> q = try p <|> q
parseUnsafe :: String -> Parser a -> Word -> a
parseUnsafe f p w = case parse p (prettyText w) w of
Left e -> error $ "Language.Bash.Expand." ++ f ++ ": " ++ show e
Right a -> a
token :: (Span -> Maybe a) -> Parser a
token = tokenPrim (const "") (\pos _ _ -> pos)
satisfy :: (Char -> Bool) -> Parser Span
satisfy p = token $ \t -> case t of
Char c | p c -> Just t
_ -> Nothing
satisfy' :: (Char -> Bool) -> Parser Char
satisfy' p = token $ \t -> case t of
Char c | p c -> Just c
_ -> Nothing
except :: (Char -> Bool) -> Parser Span
except p = token $ \t -> case t of
Char c | p c -> Nothing
_ -> Just t
char :: Char -> Parser Span
char c = token $ \t -> case t of
Char d | c == d -> Just t
_ -> Nothing
string :: String -> Parser Word
string = traverse char
oneOf :: [Char] -> Parser Span
oneOf cs = satisfy (`elem` cs)
noneOf :: [Char] -> Parser Span
noneOf cs = except (`elem` cs)
readNumber :: MonadPlus m => String -> m Int
readNumber s = case reads (dropPlus s) of
[(n, "")] -> return n
_ -> mzero
where
dropPlus ('+':t) = t
dropPlus t = t
readAlpha :: MonadPlus m => String -> m Char
readAlpha [c] | isAlpha c = return c
readAlpha _ = mzero
enum :: (Ord a, Enum a) => a -> a -> Maybe Int -> [a]
enum x y inc = map toEnum [fromEnum x, fromEnum x + step .. fromEnum y]
where
step = case inc of
Nothing | y > x -> 1
| otherwise -> 1
Just i -> i
braceExpand :: Word -> [Word]
braceExpand = parseUnsafe "braceExpand" start
where
prefix a bs = map (a ++) bs
cross as bs = [a ++ b | a <- as, b <- bs]
start = prefix <$> string "{}" <*> expr ""
</> expr ""
expr delims = foldr ($) [[]] <$> many (exprPart delims)
exprPart delims = cross <$ char '{' <*> brace delims <* char '}'
</> prefix <$> emptyBrace
</> prefix . (:[]) <$> noneOf delims
brace delims = concat <$> braceParts delims
</> sequenceExpand
</> map (\s -> stringToWord "{" ++ s ++ stringToWord "}") <$> expr ",}"
braceParts delims =
(:) <$> expr (if ',' `elem` delims then ",}" else ",") <* char ','
<*> expr ",}" `sepBy1` char ','
emptyBrace = do
a <- token $ \t -> case t of
Char c | c `elem` ws -> Just t
Escape c | c `elem` ws -> Just t
_ -> Nothing
b <- char '{'
c <- char '}' <|> oneOf ws
return [a, b, c]
where
ws = " \t\r\n"
sequenceExpand = do
a <- sequencePart
b <- string ".." *> sequencePart
c <- optional (string ".." *> sequencePart)
inc <- traverse readNumber c
map stringToWord <$> (numExpand a b inc <|> charExpand a b inc)
where
sequencePart = many1 (satisfy' isAlphaNum)
charExpand a b inc = do
x <- readAlpha a
y <- readAlpha b
return . map (:[]) $ enum x y inc
numExpand a b inc = do
x <- readNumber a
y <- readNumber b
return . map showPadded $ enum x y inc
where
width = max (length a) (length b)
isPadded ('-':'0':_:_) = True
isPadded ('0':_:_) = True
isPadded _ = False
showPadded = if isPadded a || isPadded b then pad width else show
pad w n
| n < 0 = '-' : pad (w - 1) (negate n)
| otherwise = replicate (w - length s) '0' ++ s
where
s = show n
data TildePrefix
= Home
| UserHome String
| PWD
| OldPWD
| Dirs Int
deriving (Eq, Read, Show)
instance Pretty TildePrefix where
pretty Home = "~"
pretty (UserHome s) = "~" <> text s
pretty PWD = "~+"
pretty OldPWD = "~-"
pretty (Dirs n) = "~" <> int n
tildePrefix :: Word -> Maybe (TildePrefix, Word)
tildePrefix w = case parseUnsafe "tildePrefix" split w of
('~':s, w') -> Just (readPrefix s, w')
_ -> Nothing
where
split = (,) <$> many (satisfy' (/= '/')) <*> getInput
readPrefix s
| s == "" = Home
| s == "+" = PWD
| s == "-" = OldPWD
| Just n <- readNumber s = Dirs n
| otherwise = UserHome s
splitWord :: [Char] -> Word -> [Word]
splitWord ifs = parseUnsafe "splitWord" $ ifsep *> many (word <* ifsep)
where
ifsep = many (oneOf ifs)
word = many1 (noneOf ifs)