Problem
As a practice for interacting with system commands, I wrote a Haskell program that compiles a .tex file specified by the argument.
When one execute this program:
- If no argument is specified, print
"Specify a file!"
then exit - If the specified file does not exist, print
"File not found."
then exit - If the specified file does not have the extension
.tex
, then print"Not a .tex file!"
then exit - If a
.tex
file is specified (report.tex
), execute the following commands$ uplatex report.tex -o report.dvi
$ dvipdfmx report.dvi
$ rm report.dvi
- The program does not have to handle errors in the execution of tex-related commands.
This is what I wrote:
import qualified Control.Shell as S
import Data.Char (toLower)
import Data.List (dropWhileEnd)
import qualified System.Environment as E
main = do
args <- E.getArgs
case args of
[] -> putStrLn "Specify a file!"
(filename:_) -> handler filename
isTeXFile :: S.FilePath -> Bool
isTeXFile = ( == ".tex") . map toLower . S.takeExtension
handler :: S.FilePath -> IO ()
handler f = do
(Right isfile) <- S.shell $ S.isFile f
if not isfile
then putStrLn "File not found."
else
if isTeXFile f
then do
result <- compile f
case result of
(Left l) -> putStrLn l
(Right _) -> return ()
else
putStrLn "Not a .tex file!"
compile :: S.FilePath -> IO (Either String ())
compile f = S.shell $ do
S.run_ "uplatex" [f, "-o", dvifile] ""
S.run_ "dvipdfmx" [dvifile] ""
S.rm dvifile
where
basename = dropWhileEnd (/= '.') f
dvifile = basename ++ "dvi"
I appreciate any kind of feedback. I’m especially concerned about:
- The body of
handler
seems unnecessarily complicated. How can I clean it up? - Am I using
Control.Shell
in the right way? Is there other function I should use instead? Maybe I should consider usingSystem.Process
?
Side note: I don’t use pdflatex because it is not as good as uplatex at Japanese typesetting and I haven’t been catching up with the latest LaTeX families.
Solution
When you are using packages, please make sure to say which ones. I guessed shellmate
btw. As for the code, it’s quite readable except for the handler definition which I’ve rewritten as follows:
handler :: S.FilePath -> IO ()
handler f = do
(Right isfile) <- S.shell $ S.isFile f
if not isfile then putStrLn "File not found."
else if not $ isTeXFile f then putStrLn "Not a .tex file!"
else compile f >>= either putStrLn return
Given that you are quickly dismissing the cases you are not interested in, I inspected not $
isTeXFile f
and systematically written the if (...) then
clauses on the same line. This avoids the crazy right-leaning nesting.
Using the either
combinator of type (a -> c) -> (b -> c) -> Either a b -> c
lets you rewrite the final case
expression in a more concise fashion.
Finally, I don’t think it’s good practice to test a file’s type based on its extension. Given that you are already running external tools, you may want to use ̀file
for that job. It would look something like this:
isTeXFile :: S.FilePath -> IO Bool
isTeXFile fp = do
(Right ans) <- S.shell $ S.run "file" [fp] ""
return $ " LaTeX " `isInfixOf` ans
handler :: S.FilePath -> IO ()
handler f = do
(Right isfile) <- S.shell $ S.isFile f
if not isfile then putStrLn "File not found."
else do
testTexFile <- isTeXFile f
if not $ testTexFile then putStrLn "Not a .tex file!"
else compile f >>= either putStrLn return
The rest is pretty much perfect as far as I’m concerned (modulo some cosmetic preferences which I won’t bore you with).