{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedRecordDot #-} -- | CLI interface of the glualint executable module GLuaFixer.Cli (SettingsPath (..), Options (..), Command (..), OverriddenSettings (..), overrideSettings, runParse, legacyCliParser) where import Control.Applicative ((<**>), (<|>)) import Data.Maybe (fromMaybe) import GLuaFixer.LintMessage (LogFormat (..), LogFormatChoice (..)) import GLuaFixer.LintSettings (Indentation (..), LintSettings (..), StdInOrFiles (..)) import Options.Applicative (optional) import qualified Options.Applicative as Opt newtype SettingsPath = SettingsPath FilePath deriving newtype (Show) -- | Command line options of glualint data Options = Options { optsConfigFile :: Maybe SettingsPath , optsOverridden :: OverriddenSettings , optsCommand :: Command , optsFiles :: StdInOrFiles , optsDebug :: Bool } deriving (Show) -- | Available subcommands data Command = Lint | PrettyPrint | AnalyseGlobals | DumpLexicon | DumpAst | Test | PrintVersion deriving (Show) -- | Settings in the config file that can be overridden through the command line. data OverriddenSettings = OverriddenSettings { indentation :: Maybe Indentation , outputFormat :: Maybe LogFormatChoice } deriving (Show) -- | Override settings with the options passed on the command line overrideSettings :: OverriddenSettings -> LintSettings -> LintSettings overrideSettings overridden settings = settings { prettyprint_indentation = maybe settings.prettyprint_indentation unIndentation overridden.indentation , log_format = fromMaybe settings.log_format overridden.outputFormat } -- | Run the parser against arguments runParse :: [String] -> Opt.ParserResult Options runParse = Opt.execParserPure cliPreferences cliParserInfo -- | Preferences passed to optparse-applicative cliPreferences :: Opt.ParserPrefs cliPreferences = Opt.defaultPrefs{Opt.prefShowHelpOnEmpty = True} -- | Metadata of the application cliParserInfo :: Opt.ParserInfo Options cliParserInfo = Opt.info (cliParser <**> Opt.helper) ( Opt.fullDesc <> Opt.progDesc "Linter and pretty printer for Garry's mod's flavour of Lua." <> Opt.header "glualint - lint and pretty print GLua files." ) -- | Defines the command line interface parser cliParser :: Opt.Parser Options cliParser = Options <$> configOption <*> (OverriddenSettings <$> indentOption <*> outputFormatOption) <*> commandParser <*> parseStdInOrFiles <*> parseDebug where configOption :: Opt.Parser (Maybe SettingsPath) configOption = optional $ SettingsPath <$> Opt.strOption ( Opt.long "config" <> Opt.metavar "PATH" <> Opt.help "Explicitly define config file location. By default it will search for it." ) indentOption :: Opt.Parser (Maybe Indentation) indentOption = optional $ Opt.strOption ( Opt.long "indentation" <> Opt.metavar "STR" <> Opt.help "What to use for indentation when pretty printing, 4 spaces by default." ) outputFormatOption :: Opt.Parser (Maybe LogFormatChoice) outputFormatOption = optional $ Opt.option outputFormatReader ( Opt.long "output-format" <> Opt.metavar "FORMAT" <> Opt.help "Logging format, either 'auto', 'standard' or 'github', defaults to 'standard'" ) outputFormatReader :: Opt.ReadM LogFormatChoice outputFormatReader = Opt.eitherReader $ \case "standard" -> Right $ LogFormatChoice StandardLogFormat "github" -> Right $ LogFormatChoice GithubLogFormat "auto" -> Right AutoLogFormatChoice val -> Left $ "Bad output format '" <> val <> "', must be either 'auto', 'standard' or 'github'." parseDebug :: Opt.Parser Bool parseDebug = Opt.switch ( Opt.long "debug" <> Opt.help "Whether to enable some debug prints." ) commandParser :: Opt.Parser Command commandParser = Opt.hsubparser ( Opt.command "lint" ( Opt.info (pure Lint) $ Opt.progDesc "Lint the given files. Directories will be traversed recursively." ) <> Opt.command "pretty-print" ( Opt.info (pure PrettyPrint) $ Opt.progDesc "Pretty print the given files, replacing their contents with the pretty printed code." ) <> Opt.command "analyse-globals" ( Opt.info (pure AnalyseGlobals) $ Opt.progDesc "Print a list of all globals used and defined in the given files/directories." ) <> Opt.command "dump-lexicon" ( Opt.info (pure DumpLexicon) $ Opt.progDesc "Debug command to dump the tokens parsed in the Lua code." ) <> Opt.command "dump-ast" ( Opt.info (pure DumpAst) $ Opt.progDesc "Debug command to dump the internal Abstract Syntax Tree of the Lua code." ) <> Opt.command "test" ( Opt.info (pure Test) $ Opt.progDesc "Run tests on the given files. Use for testing/debugging glualint." ) <> Opt.command "version" ( Opt.info (pure PrintVersion) $ Opt.progDesc "Print the version of glualint and exit." ) ) parseStdInOrFiles :: Opt.Parser StdInOrFiles parseStdInOrFiles = Opt.flag' UseStdIn ( Opt.long "stdin" <> Opt.help "Use stdin instead of files." ) <|> UseFiles <$> filesArgument filesArgument :: Opt.Parser [FilePath] filesArgument = Opt.many (Opt.argument Opt.str $ Opt.metavar "FILES") -- -- Legacy -- -- | Deprecated and naive command line interface, kept in place for backwards -- compatibility. legacyCliParser :: [String] -> Maybe Options legacyCliParser args = case go False emptyOptions args of (False, _) -> Nothing (True, opts@Options{optsCommand = Lint, optsFiles = files}) -> case files of UseFiles [] -> Nothing _ -> Just opts (True, opts) -> Just opts where emptyOptions = Options { optsConfigFile = Nothing , optsOverridden = OverriddenSettings Nothing Nothing , optsCommand = Lint , optsFiles = UseFiles [] , optsDebug = False } go hasSuccessfulParse options = \case ["--config"] -> (False, options) -- fail when a subcommand of the new parser is given as argument "lint" : _ -> (False, options) "pretty-print" : _ -> (False, options) "analyse-globals" : _ -> (False, options) "dump-ast" : _ -> (False, options) "test" : _ -> (False, options) "version" : _ -> (False, options) "--bash-completion-script" : _ -> (False, options) "--fish-completion-script" : _ -> (False, options) "--zsh-completion-script" : _ -> (False, options) -- End of recursion case [] -> (hasSuccessfulParse, options) "--debug" : xs -> go True options{optsDebug = True} xs "--pretty-print-files" : xs -> go True options{optsCommand = PrettyPrint} xs "--pretty-print" : xs -> go True options{optsCommand = PrettyPrint, optsFiles = UseStdIn} xs "--analyse-globals" : xs -> go True options{optsCommand = AnalyseGlobals} xs "--dump-ast" : xs -> go True options{optsCommand = DumpAst} xs "--version" : xs -> go True options{optsCommand = PrintVersion} xs "--test" : xs -> go True options{optsCommand = Test} xs "--stdin" : xs -> go True options{optsFiles = UseStdIn} xs "--config" : f : xs -> go True options{optsConfigFile = Just $ SettingsPath f} xs ('-' : '-' : 'i' : 'n' : 'd' : 'e' : 'n' : 't' : 'a' : 't' : 'i' : 'o' : 'n' : '=' : '\'' : ind') : xs -> go True options{optsOverridden = options.optsOverridden{indentation = Just $ Indentation ind'}} xs ('-' : '-' : 'i' : 'n' : 'd' : 'e' : 'n' : 't' : 'a' : 't' : 'i' : 'o' : 'n' : '=' : ind') : xs -> go True options{optsOverridden = options.optsOverridden{indentation = Just $ Indentation ind'}} xs f : xs -> case optsFiles options of UseStdIn -> go True options{optsFiles = UseFiles [f]} xs UseFiles fs -> go True options{optsFiles = UseFiles $ f : fs} xs