refactor:: simplify/improve CLI API
This commit is contained in:
parent
c9128d1ce6
commit
2b0989ea28
3 changed files with 255 additions and 246 deletions
|
|
@ -11,7 +11,7 @@ import Lake.Config.Resolve
|
|||
import Lake.Config.Util
|
||||
import Lake.Util.Error
|
||||
import Lake.Util.MainM
|
||||
import Lake.Util.CliT
|
||||
import Lake.Util.Cli
|
||||
import Lake.CLI.Init
|
||||
import Lake.CLI.Help
|
||||
import Lake.CLI.Build
|
||||
|
|
@ -23,7 +23,7 @@ namespace Lake
|
|||
|
||||
-- # CLI
|
||||
|
||||
structure CliState where
|
||||
structure LakeOptions where
|
||||
rootDir : FilePath := "."
|
||||
configFile : FilePath := defaultConfigFile
|
||||
leanInstall? : Option LeanInstall := none
|
||||
|
|
@ -31,65 +31,65 @@ structure CliState where
|
|||
subArgs : List String := []
|
||||
wantsHelp : Bool := false
|
||||
|
||||
abbrev CliM := CliT <| StateT CliState MainM
|
||||
abbrev CliStateM := StateT LakeOptions <| MainM
|
||||
abbrev CliM := ArgsT CliStateM
|
||||
|
||||
namespace Cli
|
||||
open CliT
|
||||
|
||||
-- ## Basic Actions
|
||||
|
||||
/-- Print out a line wih the given message and then exit with an error code. -/
|
||||
protected def error (msg : String) (rc : UInt32 := 1) : CliM α := do
|
||||
protected def error (msg : String) (rc : UInt32 := 1) : MainM α := do
|
||||
IO.eprintln s!"error: {msg}" |>.catchExceptions fun _ => ()
|
||||
exit rc
|
||||
|
||||
instance : MonadError CliM := ⟨Cli.error⟩
|
||||
instance : MonadLift IO CliM := ⟨MonadError.runIO⟩
|
||||
instance : MonadError MainM := ⟨Cli.error⟩
|
||||
instance : MonadLift IO MainM := ⟨MonadError.runIO⟩
|
||||
|
||||
-- ## State Management
|
||||
-- ## Basic State Management
|
||||
|
||||
def getRootDir : CliM FilePath :=
|
||||
(·.rootDir) <$> getThe CliState
|
||||
def getRootDir : CliStateM FilePath :=
|
||||
(·.rootDir) <$> get
|
||||
|
||||
def setRootDir (dir : FilePath) : CliM PUnit :=
|
||||
modifyThe CliState fun st => {st with rootDir := dir}
|
||||
def setRootDir (dir : FilePath) : CliStateM PUnit :=
|
||||
modify fun st => {st with rootDir := dir}
|
||||
|
||||
def getConfigFile : CliM FilePath :=
|
||||
(·.configFile) <$> getThe CliState
|
||||
def getConfigFile : CliStateM FilePath :=
|
||||
(·.configFile) <$> get
|
||||
|
||||
def setConfigFile (file : FilePath) : CliM PUnit :=
|
||||
modifyThe CliState fun st => {st with configFile := file}
|
||||
def setConfigFile (file : FilePath) : CliStateM PUnit :=
|
||||
modify ({· with configFile := file})
|
||||
|
||||
def getSubArgs : CliM (List String) :=
|
||||
(·.subArgs) <$> getThe CliState
|
||||
def getSubArgs : CliStateM (List String) :=
|
||||
(·.subArgs) <$> get
|
||||
|
||||
def setSubArgs (args : List String) : CliM PUnit :=
|
||||
modifyThe CliState fun st => {st with subArgs := args}
|
||||
def setSubArgs (args : List String) : CliStateM PUnit :=
|
||||
modify fun st => {st with subArgs := args}
|
||||
|
||||
def getWantsHelp : CliM Bool :=
|
||||
(·.wantsHelp) <$> getThe CliState
|
||||
def getWantsHelp : CliStateM Bool :=
|
||||
(·.wantsHelp) <$> get
|
||||
|
||||
def setWantsHelp : CliM PUnit :=
|
||||
modifyThe CliState fun st => {st with wantsHelp := true}
|
||||
def setWantsHelp : CliStateM PUnit :=
|
||||
modify fun st => {st with wantsHelp := true}
|
||||
|
||||
def setLean (lean : String) : CliM PUnit := do
|
||||
def setLean (lean : String) : CliStateM PUnit := do
|
||||
let leanInstall? ← findLeanCmdInstall? lean
|
||||
modifyThe CliState fun st => {st with leanInstall?}
|
||||
modify fun st => {st with leanInstall?}
|
||||
|
||||
def getLeanInstall? : CliM (Option LeanInstall) :=
|
||||
(·.leanInstall?) <$> getThe CliState
|
||||
def getLeanInstall? : CliStateM (Option LeanInstall) :=
|
||||
(·.leanInstall?) <$> get
|
||||
|
||||
def getLakeInstall? : CliM (Option LakeInstall) :=
|
||||
(·.lakeInstall?) <$> getThe CliState
|
||||
def getLakeInstall? : CliStateM (Option LakeInstall) :=
|
||||
(·.lakeInstall?) <$> get
|
||||
|
||||
-- ## Complex Actions
|
||||
-- ## Complex State Management
|
||||
|
||||
def loadPkg (args : List String := []) : CliM Package := do
|
||||
def loadPkg (args : List String := []) : CliStateM Package := do
|
||||
let dir ← getRootDir; let file ← getConfigFile
|
||||
setupLeanSearchPath (← getLeanInstall?) (← getLakeInstall?)
|
||||
Package.load dir args (dir / file)
|
||||
|
||||
def loadConfig (args : List String := []) : CliM (Workspace × Package) := do
|
||||
def loadConfig (args : List String := []) : CliStateM (Workspace × Package) := do
|
||||
let pkg ← loadPkg args
|
||||
let ws ← Workspace.ofPackage pkg
|
||||
let packageMap ← resolveDeps ws pkg |>.run LogMethods.eio (m := IO)
|
||||
|
|
@ -97,31 +97,31 @@ def loadConfig (args : List String := []) : CliM (Workspace × Package) := do
|
|||
({ws with packageMap}, pkg)
|
||||
|
||||
/-- Get the Lean installation. Error if missing. -/
|
||||
def getLeanInstall : CliM LeanInstall := do
|
||||
def getLeanInstall : CliStateM LeanInstall := do
|
||||
if let some leanInstall ← getLeanInstall? then
|
||||
return leanInstall
|
||||
else
|
||||
error "could not detect a Lean installation"
|
||||
|
||||
/-- Get the Lake installation. Error if missing. -/
|
||||
def getLakeInstall : CliM LakeInstall := do
|
||||
def getLakeInstall : CliStateM LakeInstall := do
|
||||
if let some lakeInstall ← getLakeInstall? then
|
||||
return lakeInstall
|
||||
else
|
||||
error "could not detect the configuration of the Lake installation"
|
||||
|
||||
/-- Get the Lean and Lake installation. Error if either is missing. -/
|
||||
def getInstall : CliM (LeanInstall × LakeInstall) := do
|
||||
def getInstall : CliStateM (LeanInstall × LakeInstall) := do
|
||||
return (← getLeanInstall, ← getLakeInstall)
|
||||
|
||||
/-- Perform the given build action using information from CLI. -/
|
||||
def runBuildM (ws : Workspace) (x : BuildM α) : CliM α := do
|
||||
def runBuildM (ws : Workspace) (x : BuildM α) : CliStateM α := do
|
||||
let (leanInstall, lakeInstall) ← getInstall
|
||||
let ctx ← mkBuildContext ws leanInstall lakeInstall
|
||||
x.run LogMethods.io ctx
|
||||
|
||||
/-- Variant of `runBuildM` that discards the build monad's output. -/
|
||||
def runBuildM_ (ws : Workspace) (x : BuildM α) : CliM PUnit :=
|
||||
def runBuildM_ (ws : Workspace) (x : BuildM α) : CliStateM PUnit :=
|
||||
discard <| runBuildM ws x
|
||||
|
||||
-- ## Argument Parsing
|
||||
|
|
@ -135,7 +135,7 @@ def takeArg (errMsg : String := "missing argument") : CliM String := do
|
|||
Verify that there are no CLI arguments remaining
|
||||
before running the given action.
|
||||
-/
|
||||
def noArgsRem (act : CliM α) : CliM α := do
|
||||
def noArgsRem (act : CliStateM α) : CliM α := do
|
||||
let args ← getArgs
|
||||
if args.isEmpty then act else
|
||||
error s!"unexpected arguments: {" ".intercalate args}"
|
||||
|
|
@ -162,29 +162,25 @@ def longOption : (opt : String) → CliM PUnit
|
|||
| "--" => do setSubArgs <| ← takeArgs
|
||||
| opt => unknownLongOption opt
|
||||
|
||||
/-- Splits a long option of the form `--long=arg` into `--long arg`. -/
|
||||
def longOptionOrEq (optStr : String) : CliM PUnit :=
|
||||
let eqPos := optStr.posOf '='
|
||||
let arg := optStr.drop eqPos.succ
|
||||
let opt := optStr.take eqPos
|
||||
if arg.isEmpty then
|
||||
longOption opt
|
||||
else do
|
||||
consArg arg
|
||||
longOption opt
|
||||
def lakeOption :=
|
||||
option {
|
||||
short := shortOption
|
||||
long := longOption
|
||||
longShort := shortOptionWithArg shortOption
|
||||
}
|
||||
|
||||
-- ## Commands
|
||||
|
||||
def withPackage [MonadLiftT m CliM] (x : Package → LakeT m α) : CliM α := do
|
||||
def withPackage [MonadLiftT m CliStateM] (x : Package → LakeT m α) : CliStateM α := do
|
||||
let (ws, pkg) ← loadConfig
|
||||
let (lean, lake) ← getInstall
|
||||
liftM <| x pkg |>.run {lean, lake, opaqueWs := ws}
|
||||
|
||||
def withContext [MonadLiftT m CliM] (x : LakeT m α) : CliM α :=
|
||||
def withContext [MonadLiftT m CliStateM] (x : LakeT m α) : CliStateM α :=
|
||||
withPackage fun _ => x
|
||||
|
||||
/-- Run the given script from the given package with the given arguments. -/
|
||||
def script (pkg : Package) (name : String) (args : List String) : CliM PUnit := do
|
||||
def script (pkg : Package) (name : String) (args : List String) : CliStateM PUnit := do
|
||||
if let some script := pkg.scripts.find? name then
|
||||
if (← getWantsHelp) then
|
||||
if let some help := script.help? then
|
||||
|
|
@ -194,19 +190,19 @@ def script (pkg : Package) (name : String) (args : List String) : CliM PUnit :=
|
|||
else
|
||||
exit <| ← withContext <| script.run args
|
||||
else
|
||||
pkg.scripts.forM (m := CliM) fun name _ => do
|
||||
pkg.scripts.forM (m := CliStateM) fun name _ => do
|
||||
IO.println <| name.toString (escape := false)
|
||||
error s!"unknown script '{name}'"
|
||||
|
||||
/-- Verify the Lean version Lake was built with matches that of the Lean installation. -/
|
||||
def verifyLeanVersion : CliM PUnit := do
|
||||
def verifyLeanVersion : CliStateM PUnit := do
|
||||
let lean ← getLeanInstall
|
||||
unless lean.githash == Lean.githash do
|
||||
let githash := if lean.githash.isEmpty then "nothing" else lean.githash
|
||||
error s!"expected Lean commit {Lean.githash}, but got {lean.githash}"
|
||||
|
||||
/-- Output the detected installs and verify the Lean version. -/
|
||||
def verifyInstall : CliM PUnit := do
|
||||
def verifyInstall : CliStateM PUnit := do
|
||||
IO.println s!"Lean:\n{repr <| ← getLeanInstall?}"
|
||||
IO.println s!"Lake:\n{repr <| ← getLakeInstall?}"
|
||||
verifyLeanVersion
|
||||
|
|
@ -221,7 +217,7 @@ If no configuration file exists, exit silently with `noConfigFileCode` (i.e, 2).
|
|||
|
||||
The `print-paths` command is used internally by Lean 4 server.
|
||||
-/
|
||||
def printPaths (imports : List String := []) : CliM PUnit := do
|
||||
def printPaths (imports : List String := []) : CliStateM PUnit := do
|
||||
let (lean, lake) ← getInstall
|
||||
let configFile := (← getRootDir) / (← getConfigFile)
|
||||
if (← configFile.pathExists) then
|
||||
|
|
@ -239,27 +235,54 @@ def serve (pkg : Package) (args : Array String := #[]) : LakeT IO UInt32 := do
|
|||
env (← getLean).toString <| #["--server"] ++ pkg.moreServerArgs ++ args
|
||||
|
||||
def command : (cmd : String) → CliM PUnit
|
||||
| "new" => do processOptions; noArgsRem <| new (← takeArg "missing package name")
|
||||
| "init" => do processOptions; noArgsRem <| init (← takeArg "missing package name")
|
||||
| "run" => do processOptions; noArgsRem <| script (← loadPkg) (← takeArg "missing script") (← getSubArgs)
|
||||
| "env" => do exit <| ← withContext <| env (← takeArg "missing command") (← takeArgs).toArray
|
||||
| "serve" => do processOptions; let args ← getSubArgs; exit <| ← noArgsRem <| withPackage fun pkg => serve pkg args.toArray
|
||||
| "configure" => do processOptions; let (ws, pkg) ← loadConfig (← getSubArgs); noArgsRem <| runBuildM ws pkg.buildDepOleans
|
||||
| "print-paths" => do processOptions; printPaths (← takeArgs)
|
||||
| "build" => do processOptions; let (ws, pkg) ← loadConfig (← getSubArgs); runBuildM ws <| build pkg (← takeArgs)
|
||||
| "clean" => do processOptions; noArgsRem <| (← loadPkg (← getSubArgs)).clean
|
||||
| "self-check" => do processOptions; noArgsRem <| verifyInstall
|
||||
| "help" => do IO.println <| help (← takeArg?)
|
||||
| cmd => error s!"unknown command '{cmd}'"
|
||||
| "new" => do
|
||||
processOptions lakeOption
|
||||
let pkgName ← takeArg "missing package name"
|
||||
noArgsRem <| new pkgName
|
||||
| "init" => do
|
||||
processOptions lakeOption
|
||||
let pkgName ← takeArg "missing package name"
|
||||
noArgsRem <| init pkgName
|
||||
| "run" => do
|
||||
processOptions lakeOption
|
||||
let scriptName ← takeArg "missing script name"
|
||||
noArgsRem <| script (← loadPkg) scriptName (← getSubArgs)
|
||||
| "env" => do
|
||||
let cmd ← takeArg "missing command"; let args ← takeArgs
|
||||
exit <| ← withContext <| env cmd args.toArray
|
||||
| "serve" => do
|
||||
let args ← getSubArgs
|
||||
noArgsRem <| exit <| ← withPackage fun pkg => serve pkg args.toArray
|
||||
| "configure" => do
|
||||
processOptions lakeOption
|
||||
let (ws, pkg) ← loadConfig (← getSubArgs)
|
||||
noArgsRem <| runBuildM ws pkg.buildDepOleans
|
||||
| "print-paths" => do
|
||||
processOptions lakeOption
|
||||
printPaths (← takeArgs)
|
||||
| "build" => do
|
||||
processOptions lakeOption
|
||||
let (ws, pkg) ← loadConfig (← getSubArgs)
|
||||
runBuildM ws <| build pkg (← takeArgs)
|
||||
| "clean" => do
|
||||
processOptions lakeOption
|
||||
noArgsRem <| (← loadPkg (← getSubArgs)).clean
|
||||
| "self-check" => do
|
||||
processOptions lakeOption
|
||||
noArgsRem <| verifyInstall
|
||||
| "help" => do
|
||||
IO.println <| help (← takeArg?)
|
||||
| cmd =>
|
||||
error s!"unknown command '{cmd}'"
|
||||
|
||||
def processArgs : CliM PUnit := do
|
||||
match (← getArgs) with
|
||||
| [] => IO.println usage
|
||||
| ["--version"] => IO.println uiVersionString
|
||||
| _ => -- normal CLI
|
||||
processLeadingOptions -- between `lake` and command
|
||||
processLeadingOptions lakeOption -- between `lake` and command
|
||||
if let some cmd ← takeArg? then
|
||||
processLeadingOptions -- between command and args
|
||||
processLeadingOptions lakeOption -- between command and args
|
||||
if (← getWantsHelp) then
|
||||
IO.println <| help cmd
|
||||
else
|
||||
|
|
@ -272,13 +295,7 @@ end Cli
|
|||
open Cli in
|
||||
def CliM.run (self : CliM α) (args : List String) : IO UInt32 := do
|
||||
let (leanInstall?, lakeInstall?) ← findInstall?
|
||||
let initSt := {leanInstall?, lakeInstall?}
|
||||
let methods := {
|
||||
shortOption,
|
||||
longOption := longOptionOrEq,
|
||||
longShortOption := unknownLongOption,
|
||||
}
|
||||
match (← CliT.run self args methods |>.run' initSt |>.toIO') with
|
||||
match (← self args |>.run' {leanInstall?, lakeInstall?} |>.toIO') with
|
||||
| Except.ok _ => pure 0
|
||||
| Except.error rc => pure rc
|
||||
|
||||
|
|
|
|||
161
Lake/Util/Cli.lean
Normal file
161
Lake/Util/Cli.lean
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/-
|
||||
Copyright (c) 2021 Mac Malone. All rights reserved.
|
||||
Released under Apache 2.0 license as described in the file LICENSE.
|
||||
Authors: Mac Malone
|
||||
-/
|
||||
|
||||
namespace Lake
|
||||
|
||||
/-!
|
||||
Defines the abstract CLI interface for Lake.
|
||||
-/
|
||||
|
||||
-- # Types
|
||||
|
||||
def ArgList := List String
|
||||
|
||||
@[inline] def ArgList.mk (args : List String) : ArgList :=
|
||||
args
|
||||
|
||||
abbrev ArgsT := StateT ArgList
|
||||
|
||||
@[inline] def ArgsT.run (args : List String) (self : ArgsT m α) : m (α × List String) :=
|
||||
StateT.run self args
|
||||
|
||||
@[inline] def ArgsT.run' [Functor m] (args : List String) (self : ArgsT m α) : m α :=
|
||||
StateT.run' self args
|
||||
|
||||
structure OptionHandlers (m : Type u → Type v) (α : Type u) where
|
||||
/-- Process a long option (ex. `--long` or `"--long foo bar"`). -/
|
||||
long : String → m α
|
||||
/-- Process a short option (ex. `-x` or `--`). -/
|
||||
short : Char → m α
|
||||
/-- Process a long short option (ex. `-long`, `-xarg`, `-xyz`). -/
|
||||
longShort : String → m α
|
||||
|
||||
-- # Utilities
|
||||
|
||||
variable [Monad m] [MonadStateOf ArgList m]
|
||||
|
||||
/-- Get the remaining argument list. -/
|
||||
@[inline] def getArgs : m (List String) :=
|
||||
get
|
||||
|
||||
/-- Replace the argument list. -/
|
||||
@[inline] def setArgs (args : List String) : m PUnit :=
|
||||
set (ArgList.mk args)
|
||||
|
||||
/-- Take the head of the remaining argument list (or none if empty). -/
|
||||
@[inline] def takeArg? : m (Option String) :=
|
||||
modifyGet fun | [] => (none, []) | arg :: args => (some arg, args)
|
||||
|
||||
/-- Take the remaining argument list, leaving only an empty list. -/
|
||||
@[inline] def takeArgs : m (List String) :=
|
||||
modifyGet fun args => (args, [])
|
||||
|
||||
/-- Add a string to the head of the remaining argument list. -/
|
||||
@[inline] def consArg (arg : String) : m PUnit :=
|
||||
modify fun args => arg :: args
|
||||
|
||||
/-- Process a short option of the form `-x=arg`. -/
|
||||
@[inline] def shortOptionWithEq (handle : Char → m α) (opt : String) : m α := do
|
||||
consArg (opt.drop 3); handle opt[1]
|
||||
|
||||
/-- Process a short option of the form `"-x arg"`. -/
|
||||
@[inline] def shortOptionWithSpace (handle : Char → m α) (opt : String) : m α := do
|
||||
consArg <| opt.drop 2 |>.trimLeft; handle opt[1]
|
||||
|
||||
/-- Process a short option of the form `-xarg`. -/
|
||||
@[inline] def shortOptionWithArg (handle : Char → m α) (opt : String) : m α := do
|
||||
consArg (opt.drop 2); handle opt[1]
|
||||
|
||||
/-- Process a multiple short options grouped together (ex. `-xyz` as `x`, `y`, `z`). -/
|
||||
@[inline] def multiShortOption (handle : Char → m PUnit) (opt : String) : m PUnit := do
|
||||
for i in [1:opt.length] do handle opt[i]
|
||||
|
||||
/-- Splits a long option of the form `"--long foo bar"` into `--long` and `"foo bar"`. -/
|
||||
@[inline] def longOptionOrSpace (handle : String → m α) (opt : String) : m α :=
|
||||
let pos := opt.posOf ' '
|
||||
let arg := opt.drop pos.succ
|
||||
if arg.isEmpty then
|
||||
handle opt
|
||||
else do
|
||||
consArg arg
|
||||
handle <| opt.take pos |>.trimLeft
|
||||
|
||||
/-- Splits a long option of the form `--long=arg` into `--long` and `arg`. -/
|
||||
@[inline] def longOptionOrEq (handle : String → m α) (opt : String) : m α :=
|
||||
let pos := opt.posOf '='
|
||||
let arg := opt.drop pos.succ
|
||||
if arg.isEmpty then
|
||||
handle opt
|
||||
else do
|
||||
consArg arg
|
||||
handle <| opt.take pos
|
||||
|
||||
/-- Process a long option of the form `--long`, `--long=arg`, `"--long arg"`. -/
|
||||
@[inline] def longOption (handle : String → m α) : String → m α :=
|
||||
longOptionOrEq <| longOptionOrSpace handle
|
||||
|
||||
/-- Process a short option of the form `-x`, `-x=arg`, `-x arg`, or `-long`. -/
|
||||
@[inline] def shortOption
|
||||
(shortHandle : Char → m α) (longHandle : String → m α)
|
||||
(opt : String) : m α :=
|
||||
if opt.length == 2 then -- `-x`
|
||||
shortHandle opt[1]
|
||||
else -- `-c(.+)`
|
||||
match opt[2] with
|
||||
| '=' => -- `-x=arg`
|
||||
shortOptionWithEq shortHandle opt
|
||||
| ' ' => -- `"-x arg"`
|
||||
shortOptionWithSpace shortHandle opt
|
||||
| _ => -- `-long`
|
||||
longHandle opt
|
||||
|
||||
/--
|
||||
Process an option, short or long, using the given handlers.
|
||||
An option is an argument of length > 1 starting with a dash (`-`).
|
||||
An option may consume additional elements of the argument list.
|
||||
-/
|
||||
@[inline] def option (handlers : OptionHandlers m α) (opt : String) : m α :=
|
||||
if opt[1] == '-' then -- `--(.*)`
|
||||
longOption handlers.long opt
|
||||
else
|
||||
shortOption handlers.short handlers.longShort opt
|
||||
|
||||
/-- Process the head argument of the list using `handle` if it is an option. -/
|
||||
def processLeadingOption (handle : String → m PUnit) : m PUnit := do
|
||||
match (← getArgs) with
|
||||
| [] => pure ()
|
||||
| arg :: args =>
|
||||
if arg.length > 1 && arg[0] == '-' then -- `-(.+)`
|
||||
setArgs args
|
||||
handle arg
|
||||
|
||||
/--
|
||||
Process the leading options of the remaining argument list.
|
||||
Consumes empty leading arguments in the argument list.
|
||||
-/
|
||||
partial def processLeadingOptions (handle : String → m PUnit) : m PUnit := do
|
||||
match (← getArgs) with
|
||||
| [] => pure ()
|
||||
| arg :: args =>
|
||||
let len := arg.length
|
||||
if len > 1 && arg[0] == '-' then -- `-(.+)`
|
||||
setArgs args
|
||||
handle arg
|
||||
processLeadingOptions handle
|
||||
else if len == 0 then -- skip empty leading args
|
||||
setArgs args
|
||||
processLeadingOptions handle
|
||||
|
||||
/-- Process every option and collect the remaining arguments into an `Array`. -/
|
||||
partial def collectArgs (option : String → m PUnit) (args : Array String := #[]) : m (Array String) := do
|
||||
processLeadingOptions option
|
||||
match (← takeArg?) with
|
||||
| some arg => collectArgs option (args.push arg)
|
||||
| none => args
|
||||
|
||||
/-- Process every option in the argument list. -/
|
||||
@[inline] def processOptions (handle : String → m PUnit) : m PUnit := do
|
||||
setArgs (← collectArgs handle).toList
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
/-
|
||||
Copyright (c) 2021 Mac Malone. All rights reserved.
|
||||
Released under Apache 2.0 license as described in the file LICENSE.
|
||||
Authors: Mac Malone
|
||||
-/
|
||||
|
||||
namespace Lake
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- # CLI Transformer Definition
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Involves trickery patterned after `MacroM`
|
||||
-- to allow `CliMethods` to refer to `CliT`
|
||||
|
||||
constant CliMethodsRefPointed (m : Type → Type v) : PointedType.{0}
|
||||
def CliMethodsRef (m : Type → Type v) : Type := (CliMethodsRefPointed m).type
|
||||
|
||||
abbrev CliT (m : Type → Type v) :=
|
||||
ReaderT (CliMethodsRef m) <| StateT (List String) m
|
||||
|
||||
instance [Pure m] [Inhabited α] : Inhabited (CliT m α) :=
|
||||
⟨fun _ _ => pure (Inhabited.default, Inhabited.default)⟩
|
||||
|
||||
structure CliMethods (m : Type → Type v) where
|
||||
longOption : String → CliT m PUnit
|
||||
longShortOption : String → CliT m PUnit
|
||||
shortOption : Char → CliT m PUnit
|
||||
|
||||
instance [Pure m] : Inhabited (CliMethods m) :=
|
||||
⟨Inhabited.default, Inhabited.default, Inhabited.default⟩
|
||||
|
||||
namespace CliMethodsRef
|
||||
|
||||
unsafe def mkImp (methods : CliMethods m) : CliMethodsRef m :=
|
||||
unsafeCast methods
|
||||
|
||||
@[implementedBy mkImp]
|
||||
constant mk (methods : CliMethods m) : CliMethodsRef m :=
|
||||
(CliMethodsRefPointed m).val
|
||||
|
||||
instance : Coe (CliMethods m) (CliMethodsRef m) := ⟨mk⟩
|
||||
instance [Pure m] : Inhabited (CliMethodsRef m) := ⟨mk Inhabited.default⟩
|
||||
|
||||
unsafe def getImp [Pure m] (self : CliMethodsRef m) : CliMethods m :=
|
||||
unsafeCast self
|
||||
|
||||
@[implementedBy getImp]
|
||||
constant get [Pure m] (self : CliMethodsRef m) : CliMethods m
|
||||
|
||||
end CliMethodsRef
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- # CLI Utilities
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
namespace CliT
|
||||
variable [Monad m]
|
||||
|
||||
/-- Run the CLI on the given argument list using the given methods. -/
|
||||
def run (self : CliT m α) (args : List String) (methods : CliMethods m) : m α :=
|
||||
ReaderT.run self methods |>.run' args
|
||||
|
||||
/-- Get the remaining argument list. -/
|
||||
def getArgs : CliT m (List String) :=
|
||||
get
|
||||
|
||||
/-- Replace the argument list. -/
|
||||
def setArgs (args : List String) : CliT m PUnit :=
|
||||
set args
|
||||
|
||||
/-- Take the head of the remaining argument list (or none if empty). -/
|
||||
def takeArg? : CliT m (Option String) :=
|
||||
modifyGet fun | [] => (none, []) | arg :: args => (some arg, args)
|
||||
|
||||
/-- Take the remaining argument list, leaving only an empty list. -/
|
||||
def takeArgs : CliT m (List String) :=
|
||||
modifyGet fun args => (args, [])
|
||||
|
||||
/-- Add a string to the head of the remaining argument list. -/
|
||||
def consArg (arg : String) : CliT m PUnit :=
|
||||
modify fun args => arg :: args
|
||||
|
||||
/-- Get the methods of this CLI. -/
|
||||
def getMethods : CliT m (CliMethods m) :=
|
||||
(·.get) <$> read
|
||||
|
||||
/-- Change the methods of this CLI. -/
|
||||
def adaptMethods (f : CliMethods m → CliMethods m) (self : CliT m α) : CliT m α :=
|
||||
self.adapt fun ref => CliMethodsRef.mk <| f ref.get
|
||||
|
||||
/-- Process a short option (ex. `-x` or `--`). -/
|
||||
def shortOption (opt : Char) : CliT m PUnit :=
|
||||
getMethods >>= (·.shortOption opt)
|
||||
|
||||
/-- Process a short option of the form `-x=arg`. -/
|
||||
def shortOptionEq (opt : String) : CliT m PUnit := do
|
||||
consArg (opt.drop 3); shortOption opt[1]
|
||||
|
||||
/-- Process a short option of the form `-xarg`. -/
|
||||
def shortOptionArg (opt : String) : CliT m PUnit := do
|
||||
consArg (opt.drop 2); shortOption opt[1]
|
||||
|
||||
/-- Process a short option of the form `"-x arg"`. -/
|
||||
def shortOptionSpace (opt : String) : CliT m PUnit := do
|
||||
consArg <| opt.drop 2 |>.trimLeft; shortOption opt[1]
|
||||
|
||||
/-- Process a multiple short options grouped together (ex. `-xyz` as `x`, `y`, `z`). -/
|
||||
def multiShortOption (opt : String) : CliT m PUnit := do
|
||||
for i in [1:opt.length] do shortOption opt[i]
|
||||
|
||||
/-- Process a long option (ex. `--long` or `"--long a1 a2"`). -/
|
||||
def longOption (opt : String) : CliT m PUnit := do
|
||||
getMethods >>= (·.longOption opt)
|
||||
|
||||
/-- Process a long short option (ex. `-long`, `-xarg`, `-xyz`). -/
|
||||
def longShortOption (opt : String) : CliT m PUnit := do
|
||||
getMethods >>= (·.longShortOption opt)
|
||||
|
||||
/--
|
||||
Process an option, short or long.
|
||||
An option is an argument of length > 1 starting with a dash (`-`).
|
||||
An option may consume additional elements of the argument list.
|
||||
-/
|
||||
def option (opt : String) : CliT m PUnit :=
|
||||
if opt[1] == '-' then -- `--(.*)`
|
||||
longOption opt
|
||||
else
|
||||
if opt.length == 2 then -- `-x`
|
||||
shortOption opt[1]
|
||||
else -- `-c(.+)`
|
||||
match opt[2] with
|
||||
| '=' => -- `-x=arg`
|
||||
shortOptionEq opt
|
||||
| ' ' => -- `"-x arg"`
|
||||
shortOptionSpace opt
|
||||
| _ => -- `-long`
|
||||
longShortOption opt
|
||||
|
||||
/-- Process the leading options of the remaining argument list. -/
|
||||
partial def processLeadingOptions : CliT m PUnit := do
|
||||
match (← getArgs) with
|
||||
| [] => pure ()
|
||||
| arg :: args =>
|
||||
let len := arg.length
|
||||
if len > 1 && arg[0] == '-' then -- `-(.+)`
|
||||
setArgs args
|
||||
option arg
|
||||
processLeadingOptions
|
||||
else if len == 0 then -- skip empty leading args
|
||||
setArgs args
|
||||
processLeadingOptions
|
||||
|
||||
/-- Process every option and collect the remaining arguments into the given `Array`. -/
|
||||
partial def collectArgsInto (arr : Array String) : CliT m (Array String) := do
|
||||
processLeadingOptions
|
||||
match (← takeArg?) with
|
||||
| some arg => collectArgsInto (arr.push arg)
|
||||
| none => arr
|
||||
|
||||
/-- Process every option and collect the remaining arguments into an `Array`. -/
|
||||
def collectArgs : CliT m (Array String) :=
|
||||
collectArgsInto #[]
|
||||
|
||||
/-- Process every option in the argument list. -/
|
||||
def processOptions : CliT m PUnit := do
|
||||
setArgs (← collectArgs).toList
|
||||
|
||||
end CliT
|
||||
Loading…
Add table
Reference in a new issue