ztail-1.2: Main.hs
{-# LANGUAGE CPP, TupleSections, OverloadedStrings #-}
import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
import Control.Exception (AsyncException(UserInterrupt), fromException, handle)
import Control.Monad (void, when, join)
import Data.Bits ((.|.))
import qualified Data.ByteString.Char8 as BS
import Data.IORef (newIORef, readIORef, atomicModifyIORef')
import Data.List (foldl')
import Data.Monoid ((<>))
import GHC.IO.Exception (IOErrorType(UnsupportedOperation))
import qualified System.Console.GetOpt as Opt
import System.Environment (getArgs)
import System.Exit (ExitCode(..), exitSuccess, exitFailure, exitWith)
#ifdef INOTIFY
import qualified System.INotify as INotify
#endif
import System.IO.Error (ioeGetErrorType)
import System.Posix.Signals (installHandler, sigINT, Handler(..))
import Text.Regex.Posix (makeRegexOpts, compExtended, compIgnoreCase, compNoSub, defaultExecOpt)
import Util
import Display
import TailTypes
import TailHandle
data Options = Options
{ optionTails :: [Tail]
, optionTail :: Tail
, optionMatch :: TailMatch
}
defaultTail :: Tail
defaultTail = Tail
{ tailTarg = undefined -- read "-"
, tailPollInterval = 5
, tailReopenInterval = 0
#ifdef INOTIFY
, tailPollINotify = True
, tailReopenINotify = False
#endif
, tailBegin = False
, tailFileTail = True
, tailDirTail = False
, tailDirList = False
, tailDirRecursive = False
, tailTimeFmt = "%c"
, tailMatches = []
}
defaultOptions :: Options
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, prog_usage :: String
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 those marked '*' which apply to all following FILEs. Match options\n\
(-amn) apply to all following actions (-hcdse). Actions involving TEXT can\n\
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 = Opt.usageInfo prog_header prog_options
prog_options :: [Opt.OptDescr (Options -> Options)]
prog_options =
[ Opt.Option "i" ["interval"]
(Opt.ReqArg (\i -> set_opt $ \p -> p
{ tailPollInterval = read i
#ifdef INOTIFY
, tailPollINotify = False
#endif
}) "INT")
("*poll for data every INT seconds [" ++ show (tailPollInterval defaultTail) ++ "]")
, Opt.Option "r" ["reopen"]
(Opt.OptArg (\i -> set_opt $ \p -> p
{ tailReopenInterval = maybe (tailPollInterval p) read i
#ifdef INOTIFY
, tailReopenINotify = False
#endif
}) "INT")
("*check file name (like tail -F) every INT seconds or every poll [" ++ show (tailReopenInterval defaultTail) ++ "]")
#ifdef INOTIFY
, Opt.Option "I" ["inotify"]
(Opt.OptArg (\i -> set_opt $ \p -> p
{ tailPollINotify = True
, tailPollInterval = maybe 0 read i }) "INT")
("*use inotify to poll for new data (and also poll every INT)")
, Opt.Option "R" ["ireopen"]
(Opt.NoArg (set_opt $ \p -> p
{ tailReopenINotify = True }))
("*use inotify to monitor file renames (only for preexisting, leaf files)")
#endif
, Opt.Option "b" ["begin"]
(Opt.NoArg (set_opt $ \p -> p
{ tailBegin = True }))
(" start reading at the beginning of the file (rather than only new lines at the end)")
, Opt.Option "l" ["dirlist"]
(Opt.NoArg (set_opt $ \p -> p
{ tailDirList = True }))
(" watch the contents of a directory, reporting when files are added or removed")
, Opt.Option "D" ["dirtail"]
(Opt.NoArg (set_opt $ \p -> p
{ tailDirTail = True }))
(" tail all the files in a directory")
, Opt.Option "A" ["recursive"]
(Opt.NoArg (set_opt $ \p -> p
{ tailDirRecursive = True }))
(" apply the above directory modifiers recursively")
, Opt.Option "t" ["timefmt"]
(Opt.ReqArg (\t -> set_opt $ \p -> p
{ tailTimeFmt = t }) "FMT")
("*set time format for \\@ substitution (in strftime(3)) [" ++ tailTimeFmt defaultTail ++ "]")
, Opt.Option "T" ["timestamp"]
(Opt.OptArg (maybe id $ \t -> add_action (ActionSubst "\\@ \\_") . set_opt (\p -> p
{ tailTimeFmt = t })) "FMT")
(" timestamp with FMT; equivalent to: [-t FMT] -h '\\@ '")
, Opt.Option "a" ["all"]
(Opt.NoArg (set_match MatchAll))
(" perform following action for every line from this FILE (default)")
, Opt.Option "m" ["match"]
(Opt.ReqArg (set_match . MatchRegex . makeRegexOpts compExtended defaultExecOpt) "REGEX")
(" perform following action for each line matching REGEX")
, Opt.Option "M" ["imatch"]
(Opt.ReqArg (set_match . MatchRegex . makeRegexOpts (compExtended .|. compIgnoreCase) defaultExecOpt) "REGEX")
(" perform following action for each line matching REGEX (case-insensitive)")
, Opt.Option "n" ["no-match"]
(Opt.ReqArg (set_match . MatchNotRegex . makeRegexOpts (compExtended .|. compNoSub) defaultExecOpt) "REGEX")
(" perform following action for each line not matching REGEX")
, Opt.Option "N" ["no-imatch"]
(Opt.ReqArg (set_match . MatchNotRegex . makeRegexOpts (compExtended .|. compNoSub .|. compIgnoreCase) defaultExecOpt) "REGEX")
(" perform following action for each line not matching REGEX (case-insensitive)")
, Opt.Option "h" ["header"]
(Opt.ReqArg (add_action . ActionSubst . (<> "\\_") . BS.pack) "TEXT")
(" display TEXT header before (matching) lines (same as -s 'TEXT\\_')")
, Opt.Option "c" ["color"]
(Opt.ReqArg (add_action . ActionColor . parseColor) "COLOR")
(" display (matching) lines in COLOR (valid colors are: normal, bo,ul,bl,rev, nobo,noul..., black,red,green,yellow,blue,magenta,cyan,white, /black,/red,...)")
, Opt.Option "d" ["hide"]
(Opt.NoArg (add_action ActionHide))
(" hide (matching) lines")
, Opt.Option "s" ["substitute"]
(Opt.ReqArg (add_action . ActionSubst . BS.pack) "TEXT")
(" substitute (matching) lines with TEXT")
, Opt.Option "e" ["execute"]
(Opt.ReqArg (add_action . ActionExecute . BS.pack) "PROG")
(" execute PROG for every (matching) line")
]
prog_arg :: String -> Options -> Options
prog_arg a Options{ optionTails = l, optionTail = t } = Options
{ optionTails = t
{ tailTarg = read a
, tailMatches = reverse (tailMatches t)
} : l
, optionTail = t
{ tailBegin = False
, tailMatches = []
, tailDirList = False
, tailDirTail = False
, tailDirRecursive = False
}
, optionMatch = MatchAll
}
run :: [Tail] -> IO (MVar ExitCode)
run tails = do
emv <- newEmptyMVar
count <- newIORef (length tails)
errors <- newIORef False
#ifdef INOTIFY
inotify <-
catchWhen ((UnsupportedOperation ==) . ioeGetErrorType)
(Just <$> INotify.initINotify)
(return Nothing)
#endif
out <- runOutput
let done = do
e <- readIORef errors
putMVar emv $ if e
then ExitFailure 1
else ExitSuccess
err t e = case fromException e of
Just UserInterrupt -> done
_ -> tailErrMsg tr t (BS.pack $ show e) >> atomicModifyIORef' errors (const (True, ()))
tr = TailRuntime
{ trOutput = out
, trAddTail = (atomicModifyIORef' count ((, ()) . succ) >>) . runt
#ifdef INOTIFY
, trINotify = inotify
#endif
}
runt t = void $ forkIOUnmasked $ do
handle (err t) $ runTail tr t
i <- atomicModifyIORef' count (join (,) . pred)
when (i == 0) $ done
_ <- installHandler sigINT (CatchOnce done) Nothing
mapM_ runt tails
return emv
main :: IO ()
main = do
args <- getArgs
tails <- case Opt.getOpt (Opt.ReturnInOrder prog_arg) prog_options args of
(s, [], []) -> case optionTails $ foldl' (flip ($)) defaultOptions s of
[] -> do
putStrLn prog_usage
exitSuccess
t -> return $ reverse t
(_, _, err) -> do
mapM_ putStrLn err
putStrLn prog_usage
exitFailure
e <- run tails >>= takeMVar
when (e == ExitSuccess) $
rawErrMsg "ztail: done"
exitWith e