ztail-1.0: Main.hs
{-# LANGUAGE CPP #-}
import System.Exit
import Control.Monad
import qualified System.Environment
import qualified System.Console.GetOpt as GetOpt
import System.Posix.Signals (installHandler, sigINT, Handler(..))
import qualified GHC.IOBase
import qualified Control.Concurrent
import qualified Control.Exception
import qualified Data.IORef
import qualified Text.Regex
#ifdef INOTIFY
import qualified System.INotify as INotify
#endif
import Util
import Display
import TailTypes
import Tail
import TailHandle
data Options = Options{
optionTails :: [Tail],
optionTail :: Tail,
optionMatch :: TailMatch
}
defaultTail = Tail{
tailTarg = undefined, -- read "-",
tailPollInterval = read "5",
tailReopenInterval = read "0",
#ifdef INOTIFY
tailPollINotify = True,
tailReopenINotify = False,
#endif
tailBegin = False,
tailTimeFmt = "%c",
tailMatches = []
}
defaultOptions = Options{
optionTails = [],
optionTail = defaultTail,
optionMatch = MatchAll
}
set_opt :: (Tail -> Tail) -> Options -> Options
set_opt p o = o{ optionTail = p $ optionTail o }
set_match :: TailMatch -> Options -> Options
set_match m o = o{ optionMatch = m }
add_action :: TailAction -> Options -> Options
add_action a o = set_opt add o where
add t = t{ tailMatches = (optionMatch o, a) : (tailMatches t) }
prog_header = "Usage: ztail [OPTIONS] FILE ...\n\
Follow the specified files (ala tail -f). FILE may be a path, '-' for stdin,\n\
or '&N' for file descriptor N. OPTIONS apply only to the following FILE\n\
(except for -irt), and match options (-amn) apply to all following actions\n\
(-hcdse). Actions involving TEXT can contain the following \\-escapes:\n\
\\0 current file \\@ current time (from -t)\n\
\\_ current line \\` \\' pre- and post-matching text\n\
\\& matching text \\N (1-9) group in match\n\
\&" --"
prog_usage = GetOpt.usageInfo prog_header prog_options
prog_options :: [GetOpt.OptDescr (Options -> Options)]
prog_options = [
GetOpt.Option "b" ["begin"]
(GetOpt.NoArg (set_opt $ \p -> p{ tailBegin = True }))
("start reading at the beginning of the file (rather than only new lines at the end)"),
GetOpt.Option "i" ["interval"]
(GetOpt.ReqArg (\i -> set_opt $ \p -> p{ tailPollInterval = read i }) "INT")
("poll for data every INT seconds [" ++ show (tailPollInterval defaultTail) ++ "] on all following files"),
GetOpt.Option "r" ["reopen"]
(GetOpt.OptArg (\i -> set_opt $ \p -> p{ tailReopenInterval = maybe (tailPollInterval p) read i }) "INT")
("check file name (like tail -F) every INT seconds or every poll"),
GetOpt.Option "t" ["timefmt"]
(GetOpt.ReqArg (\t -> set_opt $ \p -> p{ tailTimeFmt = t }) "FMT")
("set time format for \\@ substitution (in strftime(3)) [" ++ tailTimeFmt defaultTail ++ "]"),
GetOpt.Option "T" ["timestamp"]
(GetOpt.OptArg (maybe id $ \t -> add_action (ActionSubst "\\@ \\_") . set_opt (\p -> p{ tailTimeFmt = t })) "FMT")
("timestamp with FMT; equivalent to: [-t FMT] -h '\\@ '"),
#ifdef INOTIFY
GetOpt.Option "I" ["inotify"]
(GetOpt.NoArg (set_opt $ \p -> p{ tailPollINotify = True, tailPollInterval = read "0" }))
("use inotify to poll for new data (only for regular files)"),
GetOpt.Option "R" ["ireopen"]
(GetOpt.NoArg (set_opt $ \p -> p{ tailReopenINotify = True }))
("use inotify to monitor file renames (only for preexisting, leaf files)"),
#endif
GetOpt.Option "a" ["all"]
(GetOpt.NoArg (set_match MatchAll))
("perform following action for every line from this FILE (default)"),
GetOpt.Option "m" ["match"]
(GetOpt.ReqArg (\m -> set_match $ MatchRegex $ Text.Regex.mkRegexWithOpts m False True) "REGEX")
("perform following action for each line matching REGEX"),
GetOpt.Option "M" ["imatch"]
(GetOpt.ReqArg (\m -> set_match $ MatchRegex $ Text.Regex.mkRegexWithOpts m False False) "REGEX")
("perform following action for each line matching REGEX (case-insensitive)"),
GetOpt.Option "n" ["no-match"]
(GetOpt.ReqArg (\m -> set_match $ MatchNotRegex $ Text.Regex.mkRegexWithOpts m False True) "REGEX")
("perform following action for each line not matching REGEX"),
GetOpt.Option "N" ["no-imatch"]
(GetOpt.ReqArg (\m -> set_match $ MatchNotRegex $ Text.Regex.mkRegexWithOpts m False False) "REGEX")
("perform following action for each line not matching REGEX (case-insensitive)"),
GetOpt.Option "h" ["header"]
(GetOpt.ReqArg (\h -> add_action $ ActionSubst (h ++ "\\_")) "TEXT")
("display TEXT header before (matching) lines (same as -s 'TEXT\\_')"),
GetOpt.Option "c" ["color"]
(GetOpt.ReqArg (\c -> add_action $ ActionColor $ parseColor c) "COLOR")
("display (matching) lines in COLOR (valid colors are: normal, br,ul,bl,rev,hidden, nobr,noul..., black,red,green,yellow,blue,magenta,cyan,white, /black,/red,...)"),
GetOpt.Option "d" ["hide"]
(GetOpt.NoArg (add_action ActionHide))
("hide (matching) lines"),
GetOpt.Option "s" ["substitute"]
(GetOpt.ReqArg (\s -> add_action $ ActionSubst s) "TEXT")
("substitute (matching) lines with TEXT"),
GetOpt.Option "e" ["execute"]
(GetOpt.ReqArg (\e -> add_action $ ActionExecute e) "PROG")
("execute PROG for every (matching) line")
]
prog_arg a Options{ optionTails = l, optionTail = t } =
Options{
optionTails = t{
tailTarg = read a,
tailMatches = reverse (tailMatches t)
} : l,
optionTail = t{
tailBegin = False,
tailMatches = []
},
optionMatch = MatchAll
}
run :: [Tail] -> IO (Control.Concurrent.MVar ExitCode)
run tails = do
emv <- Control.Concurrent.newEmptyMVar
let exit = Control.Concurrent.putMVar emv
let exit0 = exit ExitSuccess
let exit1 = exit (ExitFailure 1)
installHandler sigINT (CatchOnce exit0) Nothing
lockv <- Control.Concurrent.newQSem 0
let lock = Control.Exception.bracket_ (Control.Concurrent.waitQSem lockv) (Control.Concurrent.signalQSem lockv)
let unlock = Control.Exception.bracket_ (Control.Concurrent.signalQSem lockv) (Control.Concurrent.waitQSem lockv)
count <- Data.IORef.newIORef (length tails)
#ifdef INOTIFY
inotify <-
catchOnlyIOET GHC.IOBase.UnsupportedOperation
(Just =.< INotify.initINotify)
(return Nothing)
#endif
let error t e = if isint e then exit0 else tailErrMsg t (show e) >> exit1
runt = runTail TailRuntime
{ trText = tailText
, trUnlock = unlock
#ifdef INOTIFY
, trINotify = inotify
#endif
}
mapM_ (\t -> Control.Concurrent.forkIO $ lock $ Control.Exception.handle (error t) $ do
runt t
i <- Data.IORef.atomicModifyIORef count (\i -> (pred i, i))
when (i == 1) exit0)
tails
Control.Concurrent.signalQSem lockv
return emv
where
isint (Control.Exception.IOException e) = GHC.IOBase.ioe_type e == GHC.IOBase.Interrupted
isint _ = False
main = do
args <- System.Environment.getArgs
tails <- case (GetOpt.getOpt (GetOpt.ReturnInOrder prog_arg) prog_options args) of
(s, [], []) -> case optionTails $ foldl (\s t -> t s) defaultOptions s of
[] -> do
putStrLn prog_usage
exitWith ExitSuccess
t -> return t
(_, _, err) -> do
mapM_ putStrLn err
putStrLn prog_usage
exitFailure
e <- run tails >>= Control.Concurrent.takeMVar
when (e == ExitSuccess) $
errMsg "ztail: done"
exitWith e