Learn You a Haskell for Great Good! - Miran Lipovaca [81]
import System.IO
import System.Directory
import Data.List
import Control.Exception
main = do
contents <- readFile "todo.txt"
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
[0..] todoTasks
putStrLn "These are your TO-DO items:"
mapM_ putStrLn numberedTasks
putStrLn "Which one do you want to delete?"
numberString <- getLine
let number = read numberString
newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
bracketOnError (openTempFile "." "temp")
(\(tempName, tempHandle) -> do
hClose tempHandle
removeFile tempName)
(\(tempName, tempHandle) -> do
hPutStr tempHandle newTodoItems
hClose tempHandle
removeFile "todo.txt"
renameFile tempName "todo.txt")
Instead of just using openTempFile normally, we use it with bracketOnError. Next, we write what we want to happen if an error occurs; that is, we want to close the temporary handle and remove the temporary file. Finally, we write what we want to do with the temporary file while things are going well, and these lines are the same as they were before. We write the new items, close the temporary handle, remove our current file, and rename the temporary file.
Command-Line Arguments
Dealing with command-line arguments is pretty much a necessity if you want to make a script or application that runs on a terminal. Luckily, Haskell’s standard library has a nice way of getting command-line arguments for a program.
In the previous section, we made one program for adding an item to our to-do list and one program for removing an item. A problem with them is that we just hardcoded the name of our to-do file. We decided that the file will be named todo.txt and that users will never have a need for managing several to-do lists.
One solution is to always ask the users which file they want to use as their to-do list. We used that approach when we wanted to know which item to delete. It works, but it’s not the ideal solution because it requires the users to run the program, wait for the program to ask them something, and then give the program some input. That’s called an interactive program.
The difficult bit with interactive command-line programs is this: What if you want to automate the execution of that program, as with a script? It’s harder to make a script that interacts with a program than a script that just calls one or more programs. That’s why we sometimes want users to tell a program what they want when they run the program, instead of having the program ask the user once it’s running. And what better way to have the users tell the program what they want it to do when they run it than via command-line arguments?
The System.Environment module has two cool I/O actions that are useful for getting command-line arguments: getArgs and getProgName. getArgs has a type of getArgs :: IO [String] and is an I/O action that will get the arguments that the program was run with and yield a list of those arguments. getProgName has a type of getProgName :: IO String and is an I/O action that yields the program name. Here’s a small program that demonstrates how these two work:
import System.Environment
import Data.List
main = do
args <- getArgs
progName <- getProgName
putStrLn "The arguments are:"
mapM putStrLn args
putStrLn "The program name is:"
putStrLn progName
First, we bind the command-line arguments to args and program name to progName. Next, we use putStrLn to print all the program’s arguments and then the name of the program itself. Let’s compile this as arg-test and try it out:
$ ./arg-test first second w00t "multi word arg"
The arguments are:
first
second
w00t
multi word arg
The program name is:
arg-test
More Fun with To-Do Lists
In the previous examples, we made one program for adding