Files
ChuSQL/chusql-cli/app/Main.hs
T

163 lines
6.5 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import ChuSQL.CLI.Format (OutputFormat (..), formatName, parseFormat)
import ChuSQL.CLI.Script (stripTerminator)
import ChuSQL.CLI.Session (
Session,
authenticateSession,
isPlainIdentifier,
newSession,
switchDatabase,
)
import ChuSQL.Interface.Auth (Credential (..))
import ChuSQL.Interface.Config (ServerConfig (..), loadServerConfigAt, loadWebConfigAt, resolveCredential, resolvePlainPassword)
import ChuSQL.Interface.Link (Client, closeClient, connectClient)
import ChuSQL.Interface.Settings (readSettingsFile)
import ChuSQL.Interface.TOML (resolveConfigPath)
import Control.Exception (finally)
import Control.Monad (unless)
import Data.IORef (newIORef)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Options.Applicative
import Repl (reportError, runRepl, runStatementOnce)
import System.Console.Haskeline (defaultSettings, getPassword, runInputT)
import System.Directory (getHomeDirectory)
import System.Exit (exitFailure)
import System.FilePath ((</>))
import System.IO (hSetEncoding, stderr, stdout, utf8)
-- chusql-cli:与 Web 管理端并列的前端,接到拥有存储的 server 上跑 SQL,账号与 Web 共用。
data Options = Options
{ optUser :: Maybe Text
, optPromptPassword :: Bool
, optDatabase :: Text
, optFormat :: OutputFormat
, optExecute :: Maybe Text
, optHistory :: Maybe FilePath
, optConfig :: Maybe FilePath
}
-- | 命令行参数解析器
optionsParser :: Parser Options
optionsParser =
Options
<$> optional (T.pack <$> strOption (long "user" <> short 'u' <> metavar "USER" <> help "Sign in as this account (default: the configured administrator)"))
<*> switch (long "password" <> short 'p' <> help "Always ask for the password, ignoring the configured one")
<*> ( T.pack
<$> strOption
( long "database"
<> short 'd'
<> metavar "DB"
<> value ""
<> help "Database to use (default: none, select one with USE)"
)
)
<*> option
(eitherReader formatReader)
( long "format"
<> short 'f'
<> metavar "FORMAT"
<> value FormatTable
<> showDefaultWith (T.unpack . formatName)
<> help "Output format: table, json or csv"
)
<*> optional (T.pack <$> strOption (long "execute" <> short 'e' <> metavar "SQL" <> help "Run one statement and exit"))
<*> optional (strOption (long "history" <> metavar "FILE" <> help "SQL history file (default ~/.chusql_history)"))
<*> optional (strOption (long "config" <> metavar "FILE" <> help "Config file to read (default: the fixed chusql.toml under the system config directory)"))
-- | 把 --format 的取值解析成格式
formatReader :: String -> Either String OutputFormat
formatReader raw = maybe (Left "format must be table, json or csv") Right (parseFormat (T.pack raw))
-- | 命令行界面与帮助
parserInfo :: ParserInfo Options
parserInfo =
info
(optionsParser <**> helper)
( fullDesc
<> progDesc "Run SQL against the ChuSQL storage engine"
<> header "csql - the ChuSQL command line client"
)
-- | 解析参数、登录 server 后进入会话
main :: IO ()
main = do
hSetEncoding stdout utf8
hSetEncoding stderr utf8
options <- execParser parserInfo
configPath <- resolveConfigPath (optConfig options)
base <- loadWebConfigAt configPath
saved <- readSettingsFile configPath
credential <- resolveCredential base saved
config <- loadServerConfigAt configPath
let rootName = credUser credential
userName = fromMaybe rootName (optUser options)
password <- resolvePassword (optPromptPassword options) userName rootName saved
connected <- connectClient (T.pack (scHost config)) (scPort config)
case connected of
Left message -> reportError message >> exitFailure
Right client ->
connect options client userName password
`finally` closeClient client
-- | 连 server、登录,之后转入会话
connect :: Options -> Client -> Text -> Text -> IO ()
connect options client userName password = do
session <- newSession client
signedIn <- authenticateSession session userName password
case signedIn of
Left message -> reportError message >> exitFailure
Right () -> runSession options session
-- | 执行 -e 的单条语句,否则进交互循环
runSession :: Options -> Session -> IO ()
runSession options session = do
formatRef <- newIORef (optFormat options)
ready <- selectDatabase session (T.strip (optDatabase options))
if not ready
then exitFailure
else case optExecute options of
Just sql -> do
ok <- runStatementOnce session (optFormat options) (stripTerminator sql)
unless ok exitFailure
Nothing -> do
historyFile <- resolveHistory (optHistory options)
runRepl session formatRef historyFile
-- | -d 指定了库才切,否则保持未选库
selectDatabase :: Session -> Text -> IO Bool
selectDatabase _ wanted | T.null wanted = pure True
selectDatabase session wanted
| not (isPlainIdentifier wanted) = reportError ("not a plain database name: " <> wanted) >> pure False
| otherwise = do
switched <- switchDatabase session wanted
case switched of
Left message -> reportError message >> pure False
Right () -> pure True
-- | 决定口令是交互输入还是取自配置
resolvePassword :: Bool -> Text -> Text -> Map.Map Text Text -> IO Text
resolvePassword forcePrompt userName rootName saved
| forcePrompt = promptPassword userName
| T.toLower userName == T.toLower rootName = pure (resolvePlainPassword saved)
| otherwise = promptPassword userName
-- | 交互式读取口令
promptPassword :: Text -> IO Text
promptPassword user = do
entered <- runInputT defaultSettings (getPassword (Just '*') (T.unpack user <> "'s password: "))
pure (maybe "" T.pack entered)
-- | 取历史文件路径,没给就用默认
resolveHistory :: Maybe FilePath -> IO (Maybe FilePath)
resolveHistory (Just path) = pure (Just path)
resolveHistory Nothing = do
home <- getHomeDirectory
pure (Just (home </> ".chusql_history"))