I had some fun pouring through your code, and I hope you won't be offended if I say that I noticed a few areas that look like they could be improved. All those "suceeds" add up to a lot of redundancy, and I wanted to see if Haskell had some mechanism of cutting them down.
I remembered reading an article on
Haskell error handling, and I decided that throwDyn and catchDyn looked the way to go. I wrote out some test code, and after solving the most obvious errors, I eventually got:
test.hs:6:5:
Can't make a derived instance of `Typeable ShellError'
(You need -fglasgow-exts to derive an instance for this class)
When deriving instances for type `ShellError' Adding that extension made it work, but it got a long "hmmm" from me. I decided to investigate why this error handling needed a specific compiler flag set, and eventually came across
this article. This gist of it seems to that it some advanced typing mojo for Haskell that is only supported by later versions (so Hugs doesn't support it, but GHC 6.4 and above does). Since GHC supports it, and since I couldn't find any error handling mechanism better than throwDyn/catchDyn, I decided to roll with it.
My first step, then, was to define an error type to handle all potential errors:
data ShellError = UsageError | AppError String String
deriving (Typeable) The error can either be a UsageError (the user put in the wrong arguments), or an AppError (there's something wrong with one of our embedded applications). This is essentially just an adaptation of the code in the Haskell error handling article.
Next, I put the ShellError type in the Show type class. This will allow me to use the "show" function on my errors:
usage = "usage: hsh [COMMANDS...] [OPTIONS]"
instance Show ShellError where
show UsageError = usage
show (AppError app msg) = printf "%s: %s" app msg So, if we show the UsageError, we get a little usage message, and if we show the AppError, we get a formatted message (like "ls: can't find dir").
Next, I'll use catchDyn to create a simple wrapper function:
handleErrors m = catchDyn m handler
where handler :: ShellError -> IO ()
handler err = hPutStrLn stderr (show err) >>
exitWith (ExitFailure 1) This will run the IO monad passed to it, and if an error occurs, it'll print it to STDERR and exit with failure code 1. I had to explicitly specify the type here, so Haskell knew what error type I wanted to handle.
Armed with handleErrors, I wrapped my main function in it:
main = handleErrors (getArgs >>= parse)
And wrote a quick parse function to test it:
parse [] = throwDyn UsageError
parse ("ls":xs) = ls xs The UsageError worked fine, so next I decided to see if the AppError would work. Since I figured that I'd be checking for files, directories etc. quite a bit, I create a quick "ensure" function to make my life easier:
ensure :: IO Bool -> ShellError -> IO () -> IO ()
ensure pred err m = do x <- pred
if x then m else throwDyn err This function takes in an error type, a predicate, and some code to run. If the predicate is true, it runs the code, but if it is false, it throws the error instead Thanks to some magic I don't fully understand, when the error is thrown the IO () type is still returned. I'll have to figure out what exactly is going on here, but it's rather neat.
Anyway, all that's left is my ls function:
ls [] = ls ["."]
ls (x:xs) =
ensure (doesDirectoryExist x) dirError $
getDirectoryContents x >>= mapM_ putStrLn
where dirError = AppError "ls" (printf "directory '%s' does not exist" x) Functionally similar to yours, Mad_guy, but it uses mapM_ to map putStrLn to each file in the directory, instead of using a separate recusive function. It also makes use of the "ensure" function, and the error handling.
Anyway, that's the best I could come up with, but I'm still a Haskell newbie too, so I'm sure this can be improved further. I'm not really that happy with the "ensure" function. It doesn't seem that neat to me.
Here's the full source for reference:
import IO
import System.Exit
import System.Directory
import System.Environment
import Data.Typeable
import Control.Exception
import Text.Printf
usage = "usage: hsh [COMMANDS...] [OPTIONS]"
data ShellError = UsageError | AppError String String
deriving (Typeable)
instance Show ShellError where
show UsageError = usage
show (AppError app msg) = printf "%s: %s" app msg
handleErrors m = catchDyn m handler
where handler :: ShellError -> IO ()
handler err = hPutStrLn stderr (show err) >>
exitWith (ExitFailure 1)
main = handleErrors (getArgs >>= parse)
parse [] = throwDyn UsageError
parse ("ls":xs) = ls xs
ensure :: IO Bool -> ShellError -> IO () -> IO ()
ensure pred err m = do x <- pred
if x then m else throwDyn err
ls [] = ls ["."]
ls (x:xs) =
ensure (doesDirectoryExist x) dirError $
getDirectoryContents x >>= mapM_ putStrLn
where dirError = AppError "ls" (printf "directory '%s' does not exist" x)