Stack and options

The Haskell Tool Stack’s stack command can run a Haskell source file as a script, making use of a Stack interpreter options comment at the start of the file. Stack’s stack script command can also run a script file, but it ignores any interpreter options comment – all Stack flags and arguments must be specified on the command line. stack script offers a —no-run flag, which compiles the script to an executable but does not run the executable. It is used with one of two alternative flags: —compile or —optimize.

Somebody wanted to test that a script compiled but did not want to have to reproduce the content of the script’s interpreter options comment on the command line. That caused me to look at how Stack handles its command line.

commandLineHandler

Stack’s main function has the folowing line to handle command line options. The False argument indicates the the interpreter is not being used:

commandLineHandler is, essentially, a wrapper around complicatedOptions. I want to focus on the argument of that function which is the handler for parser failure: Just failureCallback. The argument has type Maybe (ParserFailure ParserHelp -> [String] -> IO (a, (b, a))).

If the interpreter is not being used, the secondaryCommandHandler is applied to the args and the result is fed into interpreterHandler.

Main.interpreterHandler is interesting because it applies commandLineHandler a second time, but this time with a True argument indicating the interpreter is being used:

System.Environment.withArgs :: [String] -> IO a -> IO a is from the base package. While executing action parseCmdLine (commandLineHandler currentDir progName True), getArgs will return cmdArgs.

Data.Attoparsec.Interpreter.getInterpreterArgs :: String -> IO [String] is also interesting. Its Haddock documentation explains that it extracts Stack arguments from a correctly-placed and correctly-formatted comment in the file. The actual parser is Data.Attoparsec.Interpreter.interpreterArgsParser :: Bool -> String -> Data.Attoparsec.Text.Parser String.

To summarise, complicatedOptions is applied to the command line arguments. If that does not work in a certain way (‘Invalid argument’), then the arguments are searched for an existing file. Assuming one exists, that file is parsed for arguments. If -- is present in the file’s arguments, they are split between pre- and post- that divider. The pre-divider arguments are added to the pre-file name arguments. The post-divider arguments are added to the post-file name arguments, then compilatedOptions is applied again to those command line arguments.

For example, assume the stack command with arguments stack-arg1 file.hs file-arg1 fails the first time around. If the arguments in existing file.hs are stack-arg2 -- file-arg2, then the Stack arguments tried the second time around are stack-arg1 stack-arg2 -- file.hs file-arg1 file-arg2.

As a more concrete example, assume the command stack stack-arg1 file.hs fails the first time around, and the arguments in file.hs are script stack-arg2 … stack-argN. What is tried the second time around is the equivalent of stack stack-arg1 script stack-arg2 … stack-argN — file.hs. If stack-arg1 forced the —no-run and —compile flags of the script command, then the person’s object would be met. I named stack-arg1 the new flag —-force-script-no-run-compile.

interpreterArgsParser

interpreterArgsParser makes use of exports from module Data.Attoparsec.Text of the attoparsec package (imported qualified with a P). I’ve reformatted its code below and added explanatory comments: