@@ -0,0 +1,851 @@
1+{-# OPTIONS_GHC -Wall #-}
3+module CommandLine.Args.Internal
5+ , Flags, noFlags, flags, (|--)
7+ , Parser, int, file, anything, custom
8+ , Args, noArgs, required, optional, zeroOrMore, oneOrMore, oneOf
9+ , require0, require1, require2, require3, require4, require5
10+ , RequiredArgs, args, exactly, (!), (?), (...)
15+import qualified Data.Char as Char
16+import qualified Data.List as List
17+import GHC.IO.Handle (hIsTerminalDevice)
18+import System.Environment (getArgs)
19+import System.Exit (exitFailure)
20+import System.IO (hPutStrLn, stderr)
21+import Text.PrettyPrint.ANSI.Leijen
22+ ( Doc, (<>), comma, displayIO, fillSep, green
23+ , plain, red, renderPretty, text, vcat
26+import Elm.Utils (nearbyNames)
34+ Route :: (args -> flags -> a) -> Args args -> Flags flags -> Route a
42+ FDone :: a -> Flags a
43+ FMore :: Flags (a -> b) -> Flag a -> Flags b
56+(|--) :: Flags (a -> b) -> Flag a -> Flags b
66+ Flag :: String -> Parser a -> String -> Flag (Maybe a)
67+ OnOff :: String -> String -> Flag Bool
70+flag :: String -> Parser a -> String -> Flag (Maybe a)
75+onOff :: String -> String -> Flag Bool
85+ Anything :: String -> Parser String
86+ File :: Parser FilePath
87+ Int :: String -> Parser Int
88+ Custom :: String -> (String -> Maybe a) -> Parser a
91+file :: Parser FilePath
96+int :: String -> Parser Int
101+anything :: String -> Parser String
106+custom :: String -> (String -> Maybe a) -> Parser a
117+getParserInfo :: Parser a -> ParserInfo
118+getParserInfo parser =
133+runParser :: Parser a -> String -> Either ParserError a
134+runParser parser str =
143+ if all Char.isDigit str then
144+ Right (read str :: Int)
146+ Left (Expecting tipe)
148+ Custom tipe toMaybe ->
151+ Left (Expecting tipe)
157+data ParserError = Expecting String
169+required :: Parser a -> Args a
174+optional :: Parser a -> Args (Maybe a)
180+zeroOrMore :: Parser a -> Args [a]
186+oneOrMore :: Parser a -> Args (a, [a])
193+require0 :: args -> Args args
195+ exactly (args value)
198+require1 :: (a -> args) -> Parser a -> Args args
206+require2 :: (a -> b -> args) -> Parser a -> Parser b -> Args args
215+require3 :: (a -> b -> c -> args) -> Parser a -> Parser b -> Parser c -> Args args
216+require3 func a b c =
225+require4 :: (a -> b -> c -> d -> args) -> Parser a -> Parser b -> Parser c -> Parser d -> Args args
226+require4 func a b c d =
236+require5 :: (a -> b -> c -> d -> e -> args) -> Parser a -> Parser b -> Parser c -> Parser d -> Parser e -> Args args
237+require5 func a b c d e =
248+oneOf :: [Args a] -> Args a
257+data RequiredArgs a where
258+ Done :: a -> RequiredArgs a
259+ Required :: RequiredArgs (a -> b) -> Parser a -> RequiredArgs b
263+ Exactly :: RequiredArgs a -> Args a
264+ Optional :: RequiredArgs (Maybe a -> b) -> Parser a -> Args b
265+ Multiple :: RequiredArgs ([a] -> b) -> Parser a -> Args b
266+ OneOf :: [Args a] -> Args a
269+args :: a -> RequiredArgs a
274+exactly :: RequiredArgs a -> Args a
279+(!) :: RequiredArgs (a -> b) -> Parser a -> RequiredArgs b
284+(?) :: RequiredArgs (Maybe a -> b) -> Parser a -> Args b
289+(...) :: RequiredArgs ([a] -> b) -> Parser a -> Args b
298+get :: Route a -> [(String, Route a)] -> IO a
299+get route namedRoutes =
300+ do chunks <- getArgs
301+ case parse route namedRoutes chunks of
302+ Right flagsAndArgs ->
314+ = AfterCommand String RouteError
315+ | BadCommand String [String] String
316+ | NoCommand RouteError
319+parse :: Route a -> [(String, Route a)] -> [String] -> Either Error a
320+parse route namedRoutes chunks =
321+ case parseHelp namedRoutes chunks [] of
326+ case parseRoute route chunks of
332+ (name, specificRouteError) : _ ->
333+ Left (AfterCommand name specificRouteError)
337+ first : _ | not (List.isPrefixOf "-" first) ->
338+ case nearbyNames id first (map fst namedRoutes) of
340+ Left (NoCommand routeError)
343+ Left (BadCommand first (init suggestions) (last suggestions))
346+ Left (NoCommand routeError)
349+parseHelp :: [(String, Route a)] -> [String] -> RouteErrors -> Either RouteErrors a
350+parseHelp namedRoutes chunks errors =
355+ (name, route) : rest ->
356+ case parseNamedRoute name route chunks of
358+ parseHelp rest chunks (errs ++ errors)
368+type RouteErrors = [(String, RouteError)]
371+parseNamedRoute :: String -> Route a -> [String] -> Either RouteErrors a
372+parseNamedRoute name route rawChunks =
374+ first : chunks | name == first ->
375+ case parseRoute route chunks of
390+data RouteError = BadFlags FlagError | BadArgs ArgError
393+parseRoute :: Route a -> [String] -> Either RouteError a
394+parseRoute (Route func args_ flags_) chunks =
395+ case parseFlags flags_ chunks of
397+ Left (BadFlags flagError)
399+ Right (flagData, remainingChunks) ->
400+ case parseArgs args_ remainingChunks of
402+ Left (BadArgs argError)
405+ Right (func argData flagData)
414+ | MissingArg ParserInfo
415+ | BadArg String ParserError
416+ | ExtraArgs [String]
419+parseArgs :: Args a -> [String] -> Either ArgError a
420+parseArgs args_ chunks =
423+ parseOneOf argsList chunks []
425+ Exactly requiredArgs ->
426+ do (value, extras) <- parseRequiredArgs requiredArgs chunks
432+ Left (ExtraArgs extras)
434+ Optional requiredArgs optionalParser ->
435+ do (func, subChunks) <- parseRequiredArgs requiredArgs chunks
438+ Right (func Nothing)
441+ do value <- runArgParser optionalParser chunk
442+ Right (func (Just value))
445+ Left (ExtraArgs extras)
447+ Multiple requiredArgs multiParser ->
448+ do (func, extras) <- parseRequiredArgs requiredArgs chunks
449+ values <- traverse (runArgParser multiParser) extras
453+parseOneOf :: [Args a] -> [String] -> [ArgError] -> Either ArgError a
454+parseOneOf argsList chunks errors =
457+ Left (NoneOf (reverse errors))
460+ case parseArgs args_ chunks of
462+ parseOneOf rest chunks (err:errors)
468+parseRequiredArgs :: RequiredArgs a -> [String] -> Either ArgError (a, [String])
469+parseRequiredArgs args_ chunks =
472+ Right (value, chunks)
474+ Required funcArgs parser ->
475+ do (func, subChunks) <- parseRequiredArgs funcArgs chunks
478+ Left (MissingArg (getParserInfo parser))
481+ do value <- runArgParser parser chunk
482+ Right ( func value, rest )
485+runArgParser :: Parser a -> String -> Either ArgError a
486+runArgParser parser chunk =
487+ case runParser parser chunk of
489+ Left (BadArg chunk parserError)
500+ Either FlagError (a, [String])
504+ = BadOnOff String String
505+ | BadFlag String ParserInfo (Maybe (String, ParserError))
506+ | Unrecognized String [(String, String)]
509+parseFlags :: Flags a -> [String] -> Result a
510+parseFlags flags_ chunks =
511+ do (a, leftoverChunks) <- parseFlagsHelp flags_ chunks
512+ case List.find (List.isPrefixOf "-") leftoverChunks of
514+ Right (a, leftoverChunks)
517+ Left (Unrecognized badArg (collectFlagInfo flags_))
520+parseFlagsHelp :: Flags a -> [String] -> Result a
521+parseFlagsHelp flags_ chunks =
524+ Right (value, chunks)
526+ FMore makeFunc flag_ ->
527+ do (func, remainingChunks) <- parseFlagsHelp makeFunc chunks
528+ (arg, leftoverChunks) <- parseFlag flag_ remainingChunks
529+ Right (func arg, leftoverChunks)
532+parseFlag :: Flag a -> [String] -> Result a
533+parseFlag flag_ chunks =
535+ Flag name parser _summary ->
536+ parseFlagFlag name parser chunks
538+ OnOff name _summary ->
539+ parseFlagOnOff name chunks
542+parseFlagFlag :: String -> Parser a -> [String] -> Result (Maybe a)
543+parseFlagFlag name parser chunks =
544+ case findDoubleDash [] ("--" ++ name) chunks of
546+ Right (Nothing, chunks)
548+ Just (ChunkZipper before value after) ->
551+ parseFlagFlagHelp name parser before str after
554+ parseFlagFlagHelp name parser before str after
557+ Left (BadFlag name (getParserInfo parser) Nothing)
560+parseFlagFlagHelp :: String -> Parser a -> [String] -> String -> [String] -> Result (Maybe a)
561+parseFlagFlagHelp name parser before str after =
562+ case runParser parser str of
564+ Right (Just value, before ++ after)
567+ Left $ BadFlag name (getParserInfo parser) $
568+ Just (str, parserError)
571+parseFlagOnOff :: String -> [String] -> Result Bool
572+parseFlagOnOff name chunks =
573+ case findDoubleDash [] ("--" ++ name) chunks of
575+ Right (False, chunks)
577+ Just (ChunkZipper before value after) ->
580+ Right (True, before ++ after)
583+ Right (True, before ++ str : after)
586+ Left (BadOnOff name str)
595+ { _before :: [String]
597+ , _after :: [String]
607+findDoubleDash :: [String] -> String -> [String] -> Maybe ChunkZipper
608+findDoubleDash revPrev prefix chunks =
610+ flag_ : val : rest | flag_ == prefix ->
611+ if List.isPrefixOf "-" val then
612+ Just $ ChunkZipper (reverse revPrev) DefNope (val : rest)
614+ Just $ ChunkZipper (reverse revPrev) (Possibly val) rest
616+ [flag_] | flag_ == prefix ->
617+ Just $ ChunkZipper (reverse revPrev) DefNope []
619+ flag_ : rest | List.isPrefixOf (prefix ++ "=") flag_ ->
622+ drop (length prefix + 1) flag_
624+ Just $ ChunkZipper (reverse revPrev) (Definitely value) rest
627+ findDoubleDash (chunk : revPrev) prefix rest
634+-- COLLECT FLAG NAMES
637+collectFlagInfo :: Flags a -> [(String, String)]
638+collectFlagInfo flags_ =
639+ collectFlagInfoHelp flags_ []
642+collectFlagInfoHelp :: Flags a -> [(String, String)] -> [(String, String)]
643+collectFlagInfoHelp flags_ info =
648+ FMore otherFlags flag_ ->
649+ collectFlagInfoHelp otherFlags (getFlagInfo flag_ : info)
652+getFlagInfo :: Flag a -> (String, String)
655+ OnOff name summary ->
658+ Flag name _ summary ->
666+failure :: Error -> IO a
668+ do let doc = toDoc err
669+ isTerminal <- hIsTerminalDevice stderr
670+ displayIO stderr $ renderPretty 1 80 $
671+ if isTerminal then doc else plain doc
676+toDoc :: Error -> Doc
679+ BadCommand badName suggestions lastSuggestion ->
681+ [ text "There is no"
684+ , text "Did you mean"
686+ ++ oxfordify (map code suggestions) "or" (code lastSuggestion <> text "?")
688+ NoCommand (BadArgs argError) ->
689+ argErrorToDoc Nothing argError
691+ NoCommand (BadFlags flagError) ->
692+ flagErrorToDoc Nothing flagError
694+ AfterCommand name (BadArgs argError) ->
695+ argErrorToDoc (Just name) argError
697+ AfterCommand name (BadFlags flagError) ->
698+ flagErrorToDoc (Just name) flagError
705+argErrorToDoc :: Maybe String -> ArgError -> Doc
706+argErrorToDoc maybeCommand argError =
709+ text $ "TODO - " ++ show (map (argErrorToDoc maybeCommand) argErrors)
711+ MissingArg (ParserInfo tipe) ->
712+ text $ "TODO - " ++ tipe
714+ BadArg chunk _parserError ->
715+ text $ "TODO - " ++ chunk
717+ ExtraArgs extraChunks ->
718+ text $ "TODO - " ++ show extraChunks
725+flagErrorToDoc :: Maybe String -> FlagError -> Doc
726+flagErrorToDoc maybeCommand flagError =
728+ BadOnOff name value ->
731+ "--" ++ name ++ "=" ++ value
734+ flagErrorToDocHelp "This on/off flag has a trailing equals sign:" chunk
736+ [ text "Just get rid of the equals sign, like this:"
737+ , green (text ("--" ++ name))
742+ flagErrorToDocHelp "This on/off flag was given a value:" chunk
743+ [ text "An on/off flag either exists or not, no need for an equals sign and value."
744+ , text "Maybe you want this instead?"
746+ , text " " <> green (text ("--" ++ name))
750+ BadFlag name (ParserInfo tipe) Nothing ->
752+ "This flag cannot appear without a value:"
754+ [ text $ "It needs a " ++ tipe ++ " value."
757+ BadFlag name (ParserInfo tipe) (Just (value, parserError)) ->
759+ "This flag was given a bad value:"
760+ ("--" ++ name ++ "=" ++ value)
761+ [ fillSep $ map text $ words $
762+ "I need a " ++ tipe ++ " value, but "
765+ Expecting expectedType ->
766+ value ++ " is not a valid " ++ expectedType ++ "."
769+ Unrecognized chunk [] ->
772+ case maybeCommand of
774+ "I am not expecting any flags."
777+ "The `" ++ command ++ "` command does not expect any flags."
779+ flagErrorToDocHelp "Get rid of this flag:" chunk $
780+ [ fillSep (map text (words explanation))
783+ Unrecognized chunk info ->
784+ case takeWhile ('=' /=) (dropWhile ('-' ==) chunk) of
786+ flagErrorToDocHelp "This is not a valid flag:" chunk
787+ [ text "Valid flags look like --output=FILE"
793+ nearbyNames id name (map fst info)
795+ flagErrorToDocHelp "I do not recognize this flag:" chunk $
796+ unrecognizedExplanation $
797+ if null suggestions then map fst info else suggestions
800+flagErrorToDocHelp :: String -> String -> [Doc] -> Doc
801+flagErrorToDocHelp summary original explanation =
805+ , text " " <> red (text original)
811+unrecognizedExplanation :: [String] -> [Doc]
812+unrecognizedExplanation suggestions =
815+ text " " <> green (text ("--" ++ name))
822+ [ text "Looks like a typo. Maybe you want:"
824+ , toBullet suggestion
829+ [ text "Maybe you want one of these?"
832+ ++ map toBullet suggestions
836+code :: String -> Doc
841+oxfordify :: [Doc] -> String -> Doc -> [Doc]
842+oxfordify chunks conjuction lastChunk =
848+ [ chunk, text conjuction, lastChunk ]
851+ map (<> comma) chunks ++ [ text conjuction, lastChunk ]