Software Tools in Haskell: tail

get the last k lines or chars from stdin

Posted on 2016-02-28 by nbloomf
Tags: software-tools-in-haskell, literate-haskell

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)