Software Tools in Haskell: tail
get the last k lines or chars from stdin
Posted on 2016-02-28 by nbloomf
This page is part of a series on Software Tools in Haskell.
This post is literate Haskell; you can load the source into GHCi and play along.
As usual, we start with some imports.
-- tail: get the last k lines or chars from stdin
module Main where
import System.Exit (exitSuccess, exitFailure)
import System.Environment (getArgs, getProgName)
import System.IO (hPutStrLn, stderr)
import Data.List (unfoldr)
The version of tail
on my system accepts 10 optional arguments, not including --help
and --version
. I am sure that there are very good reasons for these. But this version will take only two: --char
specifies that we want to take the last few characters, rather than lines, and an optional integer argument specifies the number to take.
We use a data type, Mode
, to represent the processing mode (lines or chars). Most of the complexity is in reading arguments and reporting errors.
data Mode = Chars | Lines
main :: IO ()
main = do
args <- getArgs
(mode,k) <- do
(flag,rest) <- case args of
("--char":xss) -> return (Chars,xss)
xss -> return (Lines,xss)
case rest of
[] -> return (flag,10)
[xs] -> case readDecimalNat xs of
Nothing -> argErr >> exitFailure
Just t -> return (flag,t)
_ -> argErr >> exitFailure
let
getTail = reverse . take k . reverse
case mode of
Chars -> fmap getTail getContents
>>= putStr
Lines -> fmap (getTail . getLines) getContents
>>= (sequence_ . map putStrLn)
exitSuccess
argErr :: IO ()
argErr = reportErrorMsgs
[ "usage:"
, " tail : send the last 10 lines of stdin to stdout"
, " tail INT : send the last INT lines of stdin to stdout"
, "options:"
, " --char : get chars instead of lines"
]
Old Stuff
-- parse a natural number base 10
readDecimalNat :: String -> Maybe Int
readDecimalNat xs = do
ys <- sequence $ map decToInt $ reverse xs
return $ sum $ zipWith (*) ys [10^t | t <- [0..]]
where
decToInt :: Char -> Maybe Int
decToInt x = lookup x
[ ('0',0), ('1',1), ('2',2), ('3',3), ('4',4)
, ('5',5), ('6',6), ('7',7), ('8',8), ('9',9)
]
-- split on \n
getLines :: String -> [String]
getLines = unfoldr firstLine
where
firstLine :: String -> Maybe (String, String)
firstLine xs = case break (== '\n') xs of
("","") -> Nothing
(as,"") -> Just (as,"")
(as,b:bs) -> Just (as,bs)
-- write list of messages to stderr
reportErrorMsgs :: [String] -> IO ()
reportErrorMsgs errs = do
name <- getProgName
sequence_ $ map (hPutStrLn stderr) $ ((name ++ " error"):errs)