The package optparse-applicative
provides a Haskell library for parsing options at the command line.
A minimal example, providing only minimal help, is as follows:
1 2 3 4 5 6 |
module Main (main) where import Options.Applicative main :: IO (() -> ()) main = execParser (info helper fullDesc) |
On Windows 10, the ‘Usage:’ help text depends on how the executable is called and can include a filename extension. For example, at the Command Prompt, a test executable, opTest.exe
, provides:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>opTest -h Usage: opTest Available options: -h,--help Show this help text >opTest.exe -h Usage: opTest.exe Available options: -h,--help Show this help text >stack exec opTest -- -h Usage: opTest.EXE Available options: -h,--help Show this help text |
This behaviour differs from Windows 10’s native command line executables. For example:
1 2 3 4 5 6 7 |
>where where C:\Windows\System32\where.exe >where.exe /? WHERE [/R dir] [/Q] [/F] [/T] pattern... ... |
I wanted to replicate the behaviour of the native executables in respect of the extension.
handleParseResult and getProgName
In package optparse-applicative-0.15.1.0
, the origin of the executable name in the help text is progn <- getProgName
in the implementation of the exported function Options.Applicative.Extra.handleParseResult
. Function customExecParser
relies on that function and execParser
(used in the minimal example above) specialises customExecParser
to defaultPrefs
.
In package base-4.12.0.0
, the Haddock documents for System.Environment.getProgName
state:
Computation
getProgName
returns the name of the program as it was invoked.However, this is hard-to-impossible to implement on some non-Unix OSes, so instead, for maximum portability, we just return the leafname of the program as invoked. Even then there are some differences between platforms: on Windows, for example, a program invoked as
foo
is probably reallyFOO.EXE
, and that is whatgetProgName
will return.
The explanation of what getProgName
will return on Windows does not appear to be strictly accurate for Windows 10. A test with the following minimal example, compiled as gpnTest.exe
, suggests that getProgName
returns the name as it is invoked in Command Prompt. However, the filename (case aware) is returned in PowerShell or PowerShell Core or by double clicking on the file in Windows 10's File Explorer.
1 2 3 4 5 6 7 8 |
module Main (main) where import System.Environment main :: IO Char main = do putStrLn =<< getProgName getChar -- Wait for key press before returning |
1 2 3 4 5 |
>gpnTest gpnTest >gpnText.eXe gpnTest.eXe |
handleParseResult'
A more general handleParseResult'
could take the program name as an argument, and similarly for execParser
and customExecParser
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
-- | Handle `ParserResult` with program name. handleParseResult' :: String -> ParserResult a -> IO a handleParseResult' _ (Success a) = return a handleParseResult' progn (Failure failure) = do let (msg, exit) = renderFailure failure progn case exit of ExitSuccess -> putStrLn msg _ -> hPutStrLn stderr msg exitWith exit handleParseResult' progn (CompletionInvoked compl) = do msg <- execCompletion compl progn putStr msg exitSuccess -- | Handle `ParserResult`. handleParseResult :: ParserResult a -> IO a handleParseResult = getProgName >>= flip handleParseResult' |
getProgName'
An action getProgName'
could, on Windows, return the program name shorn of any extension, if the extension is the exeExtension
. Package extra
provides a convenient isWindows
function.
However, System.FilePath.isExtensionOf
is case-sensitive and the Windows file system is, by default, case-insensitive. Package case-insensitive
provides a type which allows case-insensitive comparison of strings.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import System.Directory (exeExtension) import System.FilePath (splitExtension) import qualified Data.CaseInsensitive as CI (mk) import System.Info.Extra (isWindows) getProgName' :: IO String getProgName' = do progn <- getProgName let (progn', ext) = splitExtension progn ext' = CI.mk ext exeExtension' = CI.mk exeExtension pure $ if isWindows && ext' == exeExtension' then progn' else progn |
Then the revised minimal example would become:
1 2 |
main :: IO (() -> ()) main = getProgName' >>= flip execParser' (info helper fullDesc) |