forked from scala/scala3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCliCommand.scala
198 lines (167 loc) · 9.58 KB
/
CliCommand.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package dotty.tools.dotc
package config
import scala.language.unsafeNulls
import Settings.*
import core.Contexts.*
import printing.Highlighting
import scala.util.chaining.given
import scala.PartialFunction.cond
trait CliCommand:
type ConcreteSettings <: CommonScalaSettings & Settings.SettingGroup
def versionMsg: String
def ifErrorsMsg: String
/** The name of the command */
def cmdName: String
def isHelpFlag(using settings: ConcreteSettings)(using SettingsState): Boolean
def helpMsg(using settings: ConcreteSettings)(using SettingsState, Context): String
private def explainAdvanced = """
|-- Notes on option parsing --
|Boolean settings are always false unless set.
|Where multiple values are accepted, they should be comma-separated.
| example: -Xplugin:plugin1,plugin2
|<phases> means one or a comma-separated list of:
| - (partial) phase names with an optional "+" suffix to include the next phase
| - the string "all"
| example: -Xprint:all prints all phases.
| example: -Xprint:typer,mixin prints the typer and mixin phases.
| example: -Ylog:erasure+ logs the erasure phase and the phase after the erasure phase.
| This is useful because during the tree transform of phase X, we often
| already are in phase X + 1.
"""
/** Distill arguments into summary detailing settings, errors and files to main */
def distill(args: Array[String], sg: Settings.SettingGroup)(ss: SettingsState = sg.defaultState)(using Context): ArgsSummary =
// expand out @filename to the contents of that filename
def expandedArguments = args.toList flatMap {
case x if x startsWith "@" => CommandLineParser.expandArg(x)
case x => List(x)
}
sg.processArguments(expandedArguments, processAll = true, settingsState = ss)
end distill
/** Creates a help message for a subset of options based on cond */
protected def availableOptionsMsg(p: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
// result is (Option Name, descrption\ndefault: value\nchoices: x, y, z
def help(s: Setting[?]): (String, String) =
// For now, skip the default values that do not make sense for the end user, such as 'false' for the version command.
def defaultValue = s.default match
case _: Int | _: String => s.default.toString
case _ => ""
val info = List(shortHelp(s), if defaultValue.nonEmpty then s"Default $defaultValue" else "", if s.legalChoices.nonEmpty then s"Choices : ${s.legalChoices}" else "")
(s.name, info.filter(_.nonEmpty).mkString("\n"))
end help
val ss = settings.allSettings.filter(p).toList.sortBy(_.name)
val formatter = Columnator("", "", maxField = 30)
val fresh = ContextBase().initialCtx.fresh.setSettings(summon[SettingsState])
formatter(List(ss.map(help) :+ ("@<file>", "A text file containing compiler arguments (options and source files).")))(using fresh)
end availableOptionsMsg
protected def shortUsage: String = s"Usage: $cmdName <options> <source files>"
protected def createUsageMsg(label: String, shouldExplain: Boolean, cond: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
val prefix = List(
Some(shortUsage),
Some(explainAdvanced).filter(_ => shouldExplain),
Some(label + " options include:")
).flatten.mkString("\n")
prefix + "\n" + availableOptionsMsg(cond)
protected def isStandard(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
!isVerbose(s) && !isWarning(s) && !isAdvanced(s) && !isPrivate(s) || s.name == "-Werror" || s.name == "-Wconf"
protected def isVerbose(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-V") && s.name != "-V"
protected def isWarning(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-W") && s.name != "-W"
protected def isAdvanced(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-X") && s.name != "-X"
protected def isPrivate(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-Y") && s.name != "-Y"
protected def shortHelp(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): String =
s.description.linesIterator.next()
protected def isHelping(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
cond(s.value) {
case ss: List[?] if s.isMultivalue => ss.contains("help")
case s: String => "help" == s
}
/** Messages explaining usage and options */
protected def usageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("where possible standard", shouldExplain = false, isStandard)
protected def vusageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("Possible verbose", shouldExplain = true, isVerbose)
protected def wusageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("Possible warning", shouldExplain = true, isWarning)
protected def xusageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("Possible advanced", shouldExplain = true, isAdvanced)
protected def yusageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("Possible private", shouldExplain = true, isPrivate)
/** Used for the formatted output of -Xshow-phases */
protected def phasesMessage(using Context): String =
val phases = new Compiler().phases
val formatter = Columnator("phase name", "description", maxField = 25)
formatter(phases.map(mega => mega.map(p => (p.phaseName, p.description))))
/** Provide usage feedback on argument summary, assuming that all settings
* are already applied in context.
* @return Either Some list of files passed as arguments or None if further processing should be interrupted.
*/
def checkUsage(summary: ArgsSummary, sourcesRequired: Boolean)(using settings: ConcreteSettings)(using SettingsState, Context): Option[List[String]] =
// Print all warnings encountered during arguments parsing
summary.warnings.foreach(report.warning(_))
if summary.errors.nonEmpty then
summary.errors foreach (report.error(_))
report.echo(ifErrorsMsg)
None
else if settings.version.value then
report.echo(versionMsg)
None
else if isHelpFlag then
report.echo(helpMsg)
None
else if (sourcesRequired && summary.arguments.isEmpty)
report.echo(usageMessage)
None
else
Some(summary.arguments)
extension [T](setting: Setting[T])
protected def value(using ss: SettingsState): T = setting.valueIn(ss)
extension (s: String)
def padLeft(width: Int): String = String.format(s"%${width}s", s)
// Formatting for -help and -Vphases in two columns, handling long field1 and wrapping long field2
class Columnator(heading1: String, heading2: String, maxField: Int, separation: Int = 2):
def apply(texts: List[List[(String, String)]])(using Context): String = StringBuilder().tap(columnate(_, texts)).toString
private def columnate(sb: StringBuilder, texts: List[List[(String, String)]])(using Context): Unit =
import Highlighting.*
val colors = Seq(Green(_), Yellow(_), Magenta(_), Cyan(_), Red(_))
val nocolor = texts.length == 1
def color(index: Int): String => Highlight = if nocolor then NoColor(_) else colors(index % colors.length)
val maxCol = ctx.settings.pageWidth.value
val field1 = maxField.min(texts.flatten.map(_._1.length).filter(_ < maxField).max) // widest field under maxField
val field2 = if field1 + separation + maxField < maxCol then maxCol - field1 - separation else 0 // skinny window -> terminal wrap
val separator = " " * separation
val EOL = "\n"
def formatField1(text: String): String = if text.length <= field1 then text.padLeft(field1) else text + EOL + "".padLeft(field1)
def formatField2(text: String): String =
def loopOverField2(fld: String): List[String] =
if field2 == 0 || fld.length <= field2 then List(fld)
else
fld.lastIndexOf(" ", field2) match
case -1 => List(fld)
case i => val (prefix, rest) = fld.splitAt(i) ; prefix :: loopOverField2(rest.trim)
text.split("\n").toList.flatMap(loopOverField2).filter(_.nonEmpty).mkString(EOL + "".padLeft(field1) + separator)
end formatField2
def format(first: String, second: String, index: Int, colorPicker: Int => String => Highlight) =
sb.append(colorPicker(index)(formatField1(first)).show)
.append(separator)
.append(formatField2(second))
.append(EOL): Unit
def fancy(first: String, second: String, index: Int) = format(first, second, index, color)
def plain(first: String, second: String) = format(first, second, 0, _ => NoColor(_))
if heading1.nonEmpty then
plain(heading1, heading2)
plain("-" * heading1.length, "-" * heading2.length)
def emit(index: Int)(textPair: (String, String)): Unit = fancy(textPair._1, textPair._2, index)
def group(index: Int)(body: Int => Unit): Unit =
if !ctx.useColors then plain(s"{", "")
body(index)
if !ctx.useColors then plain(s"}", "")
texts.zipWithIndex.foreach { (text, index) =>
text match
case List(single) => emit(index)(single)
case Nil =>
case mega => group(index)(i => mega.foreach(emit(i)))
}
end Columnator