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

162 lines
6.5 KiB
Haskell
Raw Normal View History

2026-10-01 20:48:38 +08:00
{-# 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)
2026-10-01 20:48:38 +08:00
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 共用。
2026-10-01 20:48:38 +08:00
data Options = Options
{ optUser :: Maybe Text
, optPromptPassword :: Bool
, optDatabase :: Text
, optFormat :: OutputFormat
, optExecute :: Maybe Text
, optHistory :: Maybe FilePath
, optConfig :: Maybe FilePath
}
-- | 命令行参数解析器
2026-10-01 20:48:38 +08:00
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 的取值解析成格式
2026-10-01 20:48:38 +08:00
formatReader :: String -> Either String OutputFormat
formatReader raw = maybe (Left "format must be table, json or csv") Right (parseFormat (T.pack raw))
-- | 命令行界面与帮助
2026-10-01 20:48:38 +08:00
parserInfo :: ParserInfo Options
parserInfo =
info
(optionsParser <**> helper)
( fullDesc
<> progDesc "Run SQL against the ChuSQL storage engine"
<> header "csql - the ChuSQL command line client"
)
-- | 解析参数、登录 server 后进入会话
2026-10-01 20:48:38 +08:00
main :: IO ()
main = do
hSetEncoding stdout utf8
hSetEncoding stderr utf8
options <- execParser parserInfo
configPath <- resolveConfigPath (optConfig options)
base <- loadWebConfigAt configPath
saved <- readSettingsFile configPath
2026-10-01 20:48:38 +08:00
credential <- resolveCredential base saved
config <- loadServerConfigAt configPath
2026-10-01 20:48:38 +08:00
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
2026-10-01 20:48:38 +08:00
Left message -> reportError message >> exitFailure
Right client ->
connect options client userName password
`finally` closeClient client
2026-10-01 20:48:38 +08:00
-- | 连 server、登录,之后转入会话
connect :: Options -> Client -> Text -> Text -> IO ()
connect options client userName password = do
session <- newSession client
2026-10-01 20:48:38 +08:00
signedIn <- authenticateSession session userName password
case signedIn of
Left message -> reportError message >> exitFailure
Right () -> runSession options session
-- | 执行 -e 的单条语句,否则进交互循环
2026-10-01 20:48:38 +08:00
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 指定了库才切,否则保持未选库
2026-10-01 20:48:38 +08:00
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
-- | 决定口令是交互输入还是取自配置
2026-10-01 20:48:38 +08:00
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)
2026-10-01 20:48:38 +08:00
| otherwise = promptPassword userName
-- | 交互式读取口令
2026-10-01 20:48:38 +08:00
promptPassword :: Text -> IO Text
promptPassword user = do
entered <- runInputT defaultSettings (getPassword (Just '*') (T.unpack user <> "'s password: "))
pure (maybe "" T.pack entered)
-- | 取历史文件路径,没给就用默认
2026-10-01 20:48:38 +08:00
resolveHistory :: Maybe FilePath -> IO (Maybe FilePath)
resolveHistory (Just path) = pure (Just path)
resolveHistory Nothing = do
home <- getHomeDirectory
pure (Just (home </> ".chusql_history"))