Posts Tagged ‘line’

Programming Praxis – Comm

May 10, 2011

In today’s Programming Praxis exercise, our goal is to implement the Unix command line utility comm. Let’s get started, shall we?

Some imports:

import Control.Monad
import System.Environment
import Text.Printf
import System.IO
import qualified System.IO.Strict as SIO
import GHC.IO.Handle

Determining the common lines isn’t too difficult. We go trough the two lists element by element, putting them in column 1,2 or 3 as appropriate. Afterwards, we filter out the specified columns.

comm :: (Num b, Ord a) => [b] -> [a] -> [a] -> [(a, b)]
comm flags zs = filter ((`notElem` flags) . snd) . f zs where
    f xs     []     = map (flip (,) 1) xs
    f []     ys     = map (flip (,) 2) ys
    f (x:xs) (y:ys) = case compare x y of 
        LT -> (x,1) : f xs     (y:ys)
        GT -> (y,2) : f (x:xs) ys
        EQ -> (x,3) : f xs     ys

Displaying the results in columns can be achieved with printf.

columns :: [(String, Int)] -> IO ()
columns xs = let width = maximum (map (length . fst) xs) + 2 in
    mapM_ (\(s,c) -> printf "%*s%-*s\n" ((c - 1) * width) "" width s) xs

Handling the arguments is fairly straightforward for the most part, with one exception: if the input for both files comes from stdin, the default getContents function will not work for two reasons: first, since the handle gets closed after the first one, the second call to getContents will fail. The way to resolve this is to duplicate the handle to stdin. Secondly, since getContents is lazy by default it will read the first file from stdin first, marking each line as unique to the first file, followed by doing the same thing for the second file. We therefore need to read both files strictly first. Both problems are resolved by the newStdIn function.

main :: IO ()
main = do args <- getArgs
          columns =<< case args of
              (('-':p:ps):fs) -> go (map (read . return) (p:ps)) fs
              fs              -> go [] fs
    where go args ~[f1, f2] = liftM2 (comm args) (file f1) (file f2)
          file src = fmap lines $ if src == "-" then newStdIn
                                                else readFile src
          newStdIn = catch (SIO.hGetContents =<< hDuplicate stdin)
                           (\_ -> return [])

Programming Praxis – Fortune

April 5, 2011

In today’s Programming Praxis exercise, our goal is to implement the fortune Unix command line tool. Let’s get started, shall we?

Some imports:

import Control.Monad
import System.Environment
import System.Random

Since we wrote some code in a previous exercise to select a random item from a list we can just use that here again:

chance :: Int -> Int -> a -> a -> IO a
chance x y a b = fmap (\r -> if r < x then a else b) $ randomRIO (0, y-1)

fortune :: [a] -> IO a
fortune = foldM (\a (n,x) -> chance 1 n x a) undefined . zip [1..]

All that’s left to do to implement the fortune program is to read the appropriate file and choose a random line.

main :: IO ()
main = putStrLn =<< fortune . lines =<<
       readFile . head . (++ ["fortunes.txt"]) =<< getArgs

Four lines versus the 23 of the C version. Not bad.

Programming Praxis – Word Count

December 8, 2009

In today’s Programming Praxis exercise, we have to implement the Unix wc command line utility. Let’s get started.

First, we need some imports.

import System.Environment
import Text.Printf

We handle the command line arguments with a bit of pattern matching.

parseOpts :: [String] -> ([Bool], [String])
parseOpts (('-':ps):args) = (map (`elem` ps) "lwc", args)
parseOpts args            = (replicate 3 True, args)

Next, we need to do the actual counting:

count :: [Bool] -> [(String, String)] -> [String]
count opts = map (\(name, text) -> concat
    [printf "%8s" $ if opt then show . length $ f text else "-"
    | (f, opt) <- zip [lines, words, map return] opts] ++ " " ++ name)

And finally, the program itself.

main :: IO ()
main = do args <- getArgs
          let (opts, files) = parseOpts args
          mapM_ putStrLn . count opts =<< if null files
              then fmap (\x -> [("", x)]) getContents
              else fmap (zip files) $ mapM readFile files