Hsed-0.1: StreamEd.hs
{-# LANGUAGE DeriveDataTypeable #-}
-- |
-- Module : StreamEd
-- Copyright : (c) Vitaliy Rkavishnikov
-- License : BSD-style (see the file LICENSE)
--
-- Maintainer : virukav@gmail.com
-- Stability : experimental
-- Portability : non-portable
--
-- The Sed runtime engine. The execution sequence includes the parseArgs (parse the Sed options/args),
-- compile (parse) Sed commands, execute Sed commands.
module StreamEd where
import System.IO
import System.FilePath (splitFileName)
import Control.Monad (unless, when)
import qualified Control.Monad.State as S
import qualified Control.Exception as E
import qualified System.FilePath.Glob as G (compile, globDir1)
import Data.List (isPrefixOf)
import Data.Char (isPrint)
import Data.Maybe (fromMaybe)
import qualified Data.ByteString.Char8 as B
import Text.Printf (printf)
import Parsec (parseSed, sedCmds)
import Ast
import SedRegex
import SedState
data FileStatus = EOF | Cont
deriving (Eq, Show)
data CmdSchedule = None | NextCmd | Break | Continue | Jump (Maybe B.ByteString) | Exit
deriving (Eq, Show)
-- | Run Sed program
run :: [String] -> IO ()
run args =
S.execStateT (do
(files, cmds) <- parseArgs args
compile cmds
execute files
) initEnv >> return ()
-- | Parse Sed program's arguments
parseArgs :: [String] -> SedState ([FilePath], String)
parseArgs xs = parseArgs' xs ([],[]) where
parseArgs' [] fs = return fs
parseArgs' [x] fs
| x == "-n" = set defOutput False >> return fs
| otherwise = parseCmds x fs
parseArgs' (x:y:ys) fs
| x == "-e" = parseArgs' ys (addCmd fs y)
| x == "-f" = do
sed <- S.lift $ System.IO.readFile y `catch` openFileError y
when ("#n" `isPrefixOf` sed) $ set defOutput False
parseArgs' ys (addCmd fs sed)
| x == "-n" = set defOutput False >> parseArgs' (y:ys) fs
| otherwise = parseCmds x fs >>= \fs' -> parseArgs' (y:ys) fs'
where
addCmd (files, cmds) x' = (files, cmds ++ ('\n':x'))
openFileError :: String -> E.IOException -> IO [a]
openFileError f e = putStr ("Error: Couldn't open " ++ f ++ ": " ++
show (e :: E.IOException)) >> return []
-- | Parse Sed program's embedded commands and the input files arguments
parseCmds :: String -> ([FilePath], String) -> SedState ([FilePath], String)
parseCmds x (files, cmds) =
if null cmds then return (files, x)
else do
let (dir, fp) = splitFileName x
fs <- S.lift $ G.globDir1 (G.compile fp) dir
if null fs then error $ fp ++ ": No such file or directory"
else return (files ++ fs, cmds)
-- | Parse the Sed commands
compile :: String -> SedState ()
compile cmds = do
case parseSed sedCmds cmds of
Right x -> set ast x
Left e -> error $ show e ++ " in " ++ cmds
return ()
-- | Execute the parsed Sed commands against input data
execute :: [FilePath] -> SedState ()
execute cont =
if null cont then processStdin
else do
let (files,len) = (cont, length cont)
let fs = zipWith (\x y -> (x, y == len)) files [1..len]
processFiles fs
fout <- get fileout
S.lift $ S.mapM_ hClose (map snd fout)
return ()
-- | The standard input will be used if no file operands are specified
processStdin :: SedState ()
processStdin = do
e <- get exit
unless e $ do
a <- get ast
loop stdin True a
-- | Process the input text files
processFiles :: [(FilePath, Bool)] -> SedState ()
processFiles fs =
S.forM_ fs $ \(file, lastFile) -> do
e <- get exit
unless e $ do
h <- S.lift $ openFile file ReadMode
a <- get ast
loop h lastFile a
-- | Cyclically append a line of input without newline into the pattern space
loop :: Handle -> Bool -> [SedCmd] -> SedState ()
loop h b cs = do
(res, str) <- line h b
case res of
EOF -> S.lift $ hClose h >> return ()
_ -> do
set patternSpace str
set appendSpace []
sch <- eval h b cs
case sch of
Exit -> return ()
_ -> loop h b cs
-- | Read an input line and handle the EOF status
line :: Handle -> Bool -> SedState (FileStatus, B.ByteString)
line h b = do
p <- S.lift $ hIsEOF h
if p then return (EOF,B.empty)
else do
str <- S.lift $ B.hGetLine h
modify curLine (+1)
isLast <- if h == stdin then return False
else S.lift (hIsEOF h) >>= \eof -> return eof
if isLast && b then
get curLine >>= \l -> set lastLine l >> return (Cont, str)
else return (Cont, str)
-- | Manage the flow control of the Sed commands
eval :: Handle -> Bool -> [SedCmd] -> SedState CmdSchedule
eval h b cs = do
sch <- execCmds cs (line h b)
case sch of
Jump lbl -> get ast >>= \as -> eval h b (goto as lbl)
Exit -> printPatSpace >> get appendSpace >>= \a ->
mapM_ prnStr a >> set exit True >> return Exit
_ -> get appendSpace >>= \a -> mapM_ prnStr a >> return sch
-- | Transfer control to the command marked with the label
goto :: [SedCmd] -> Maybe Label -> [SedCmd]
goto cmds = maybe [] (go cmds)
where
go [] _ = []
go (SedCmd _ fun:cs) str = case fun of
Group cs' -> go cs' str
Label x -> if x == str then cs
else go cs str
_ -> go cs str
-- | Execute the specified Sed commands
execCmds :: [SedCmd] -> SedState (FileStatus,B.ByteString) -> SedState CmdSchedule
execCmds [] _ = printPatSpace >> return None
execCmds (x:xs) line' = do
sch <- execCmd x line'
if sch `elem` [NextCmd, None] then execCmds xs line'
else if sch == Continue then do
cs <- get ast
execCmds cs line'
else return sch
-- | Execute the Sed command if the address interval of the command is matched
execCmd :: SedCmd -> SedState (FileStatus, B.ByteString) -> SedState CmdSchedule
execCmd (SedCmd a fun) line' = do
b <- matchAddress a
if b then runCmd fun line' >>= \sch -> return sch
else return NextCmd
-- | Check if the address interval is matched
matchAddress :: Address -> SedState Bool
matchAddress (Address addr1 addr2 invert) =
case (addr1,addr2) of
(Just x, Nothing) -> matchAddr x x >>= \b -> return $ b /= invert
(Just x, Just y) -> matchAddr x y >>= \b -> return $ b /= invert
_ -> return $ not invert
where
matchAddr :: Addr -> Addr -> SedState Bool
matchAddr a1 a2 = do
lineNum <- get curLine
patSpace <- get patternSpace
lastLineNum <- get lastLine
case (a1,a2) of
(LineNumber x, LineNumber y) -> matchRange (x == lineNum) (y == lineNum)
(LineNumber x, Pat y) -> matchRange (x == lineNum) (matchRE y patSpace)
(LineNumber x, LastLine) -> matchRange (x == lineNum) (lineNum == lastLineNum)
(LastLine, _) -> return $ lineNum == lastLineNum
(Pat x, Pat y) -> matchRange (matchRE x patSpace) (matchRE y patSpace)
(Pat x, LineNumber y) -> matchRange (matchRE x patSpace) (y == lineNum)
(Pat x, LastLine) -> matchRange (matchRE x patSpace) (lineNum == lastLineNum)
matchRange :: Bool -> Bool -> SedState Bool
matchRange b1 b2 = do
let setRange = set inRange
range <- get inRange
if not range then
if b1 && b2 then return True
else if b1 then setRange True >> return True
else return False
else if b2 then setRange False >> return True
else return True
-- | Execute the Sed function
runCmd :: SedFun -> SedState (FileStatus,B.ByteString) -> SedState CmdSchedule
runCmd cmd line' =
case cmd of
Group cs -> group cs line'
LineNum -> get curLine >>= (prnStrLn . B.pack . show) >> return NextCmd
Append txt -> modify appendSpace (++ [txt]) >> return NextCmd
Branch lbl -> return (Jump lbl)
Change txt -> change txt
Delete -> set patternSpace B.empty >> return Break
DeletePat -> deletePat
ReplacePat -> get holdSpace >>= \h -> set patternSpace h >> return NextCmd
AppendPat -> get holdSpace >>= \h -> modify patternSpace (`B.append` B.cons '\n' h)
>> return NextCmd
ReplaceHold -> get patternSpace >>= \p -> set holdSpace p >> return NextCmd
AppendHold -> get patternSpace >>= \p -> modify holdSpace (`B.append` B.cons '\n' p)
>> return NextCmd
Insert txt -> prnStrLn txt >> return NextCmd
List -> list
Next -> next line'
AppendLinePat -> appendLinePat line'
PrintPat -> get patternSpace >>= \p -> prnStrLn p >> return NextCmd
WriteUpPat -> get patternSpace >>= (prnStrLn . B.takeWhile (/='\n')) >> return NextCmd
Quit -> return Exit
ReadFile file -> readF file
Substitute pat repl fs -> substitute pat repl fs
Test lbl -> get subst >>= \s -> if s then return $ Jump lbl else return NextCmd
WriteFile file -> writeF file
Exchange -> exchange
Transform t1 t2 -> transform t1 t2
Label _ -> return NextCmd
Comment -> return NextCmd
EmptyCmd -> return None
-- | Execute 'group' Sed function
group :: [SedCmd] -> SedState (FileStatus, B.ByteString) -> SedState CmdSchedule
group [] _ = return NextCmd
group (cmd:xs) line' = do
sch <- execCmd cmd line'
if sch `elem` [NextCmd, None] then
group xs line'
else return sch
-- | Execute 'delete' from multiline pattern space Sed function
deletePat :: SedState CmdSchedule
deletePat = do
p <- get patternSpace
set patternSpace (B.drop 1 $ B.dropWhile (/='\n') p)
return Continue
-- | Execute 'substitute' Sed function
substitute :: B.ByteString -> B.ByteString -> Flags -> SedState CmdSchedule
substitute pat repl fs = do
let (gn, p, w) = getFlags fs
patSpace <- get patternSpace
let (repl', b) = sedSubRegex pat patSpace repl gn
set subst b
if b then do
set patternSpace repl'
_ <- if p then get patternSpace >>= \ps -> prnStrLn ps >> return NextCmd
else return NextCmd
if (not.null) w then writeF w >> return NextCmd else return NextCmd
else return NextCmd
where
getFlags :: Flags -> (Int, Bool, FilePath)
getFlags (Flags o f) = (occurr, printPat, file) where
(occurr, printPat) = case o of
Nothing -> (1, False)
Just (OccurrencePrint x y) -> (occ x, y)
Just (PrintOccurrence x y) -> (occ y, x)
occ x = case x of
Nothing -> 1
Just ReplaceAll -> 0
Just (Replace n) -> n
file = fromMaybe "" f
-- | Execute 'change' Sed function
change :: B.ByteString -> SedState CmdSchedule
change txt = do
range <- get inRange
if not range then do
prnStrLn txt
return Break
else return Break
-- | Execute 'next' Sed function
next :: SedState (FileStatus, B.ByteString) -> SedState CmdSchedule
next line' = do
printPatSpace
(res,str) <- line'
set patternSpace str
if res == EOF then return Break else return NextCmd
-- | Execute 'list' Sed function
list :: SedState CmdSchedule
list = do
patSpace <- get patternSpace
S.forM_ (B.unpack patSpace) $ \ch ->
if isPrint ch then prnChar ch
else case lookup ch esc of
Nothing -> do
prnChar '\\'
prnPrintf ch
Just x -> prnStr (B.pack x)
prnChar '\n'
return NextCmd
where esc = zip "\\\a\b\f\r\t\v"
["\\\\","\\a", "\\b", "\\f", "\\r", "\\t", "\\v"]
-- | Execute 'exchange' Sed function
exchange :: SedState CmdSchedule
exchange = do
hold <- get holdSpace
pat <- get patternSpace
set holdSpace pat
set patternSpace hold
return NextCmd
-- | Execute 'appendLinePat' Sed function
appendLinePat :: SedState (FileStatus, B.ByteString) -> SedState CmdSchedule
appendLinePat line' = do
(res,ln) <- line'
if res == EOF then return Break
else do
let suffix = B.append (B.pack "\n") ln
modify patternSpace (`B.append` suffix)
return None
-- | Execute 'transform' Sed function
transform :: B.ByteString -> B.ByteString -> SedState CmdSchedule
transform t1 t2 = do
when (B.length t1 /= B.length t2) $
error "Transform strings are not the same length"
patSpace <- get patternSpace
let tr = B.map go patSpace
set patternSpace tr
return NextCmd
where go ch = fromMaybe ch (lookup ch (B.zip t1 t2))
-- | Write contents of the pattern space to the file
writeF :: FilePath -> SedState CmdSchedule
writeF file = do
fout <- get fileout
patSpace <- get patternSpace
let printFileout h = S.lift $ B.hPutStrLn h patSpace
case lookup file fout of
Nothing -> do
h <- S.lift $ openFile file WriteMode
modify fileout (++ [(file,h)])
printFileout h
Just h -> printFileout h
return NextCmd
-- | Add contents of the input file to the append space
readF :: FilePath -> SedState CmdSchedule
readF file = do
cont <- S.lift $ B.readFile file `catch` \_ -> return B.empty
modify appendSpace (++ [cont])
return NextCmd
-- | Print the pattern space to the standard output
printPatSpace :: SedState ()
printPatSpace = do
out <- get defOutput
when out $ get patternSpace >>= \p -> prnStrLn p
-- | Check if the current line in the pattern space is the last line
isLastLine :: SedState Bool
isLastLine = do
l <- get lastLine
cur <- get curLine
return $ l == cur
-- | Writes the string to the standard output or save the string in the memory buffer
prnStr :: B.ByteString -> SedState ()
prnStr str = do
useMem <- get useMemSpace
if useMem then modify memorySpace (`B.append` str)
else S.lift $ B.putStr str
-- | The same as prnStr, but adds a newline character
prnStrLn :: B.ByteString -> SedState ()
prnStrLn str = prnStr $ B.snoc str '\n'
-- | The same as prnStr, but for char
prnChar :: Char -> SedState ()
prnChar c = prnStr $ B.singleton c
-- | Print the character as three-digit octal number
prnPrintf :: Char -> SedState ()
prnPrintf c = do
let str = printf "%03o" c :: String
prnStr $ B.pack str