Command.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import CAC from "./CAC.ts";
  2. import Option, { OptionConfig } from "./Option.ts";
  3. import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
  4. import { platformInfo } from "./deno.ts";
  5. interface CommandArg {
  6. required: boolean;
  7. value: string;
  8. variadic: boolean;
  9. }
  10. interface HelpSection {
  11. title?: string;
  12. body: string;
  13. }
  14. interface CommandConfig {
  15. allowUnknownOptions?: boolean;
  16. ignoreOptionDefaultValue?: boolean;
  17. }
  18. type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
  19. type CommandExample = ((bin: string) => string) | string;
  20. class Command {
  21. options: Option[];
  22. aliasNames: string[];
  23. /* Parsed command name */
  24. name: string;
  25. args: CommandArg[];
  26. commandAction?: (...args: any[]) => any;
  27. usageText?: string;
  28. versionNumber?: string;
  29. examples: CommandExample[];
  30. helpCallback?: HelpCallback;
  31. globalCommand?: GlobalCommand;
  32. constructor(public rawName: string, public description: string, public config: CommandConfig = {}, public cli: CAC) {
  33. this.options = [];
  34. this.aliasNames = [];
  35. this.name = removeBrackets(rawName);
  36. this.args = findAllBrackets(rawName);
  37. this.examples = [];
  38. }
  39. usage(text: string) {
  40. this.usageText = text;
  41. return this;
  42. }
  43. allowUnknownOptions() {
  44. this.config.allowUnknownOptions = true;
  45. return this;
  46. }
  47. ignoreOptionDefaultValue() {
  48. this.config.ignoreOptionDefaultValue = true;
  49. return this;
  50. }
  51. version(version: string, customFlags = '-v, --version') {
  52. this.versionNumber = version;
  53. this.option(customFlags, 'Display version number');
  54. return this;
  55. }
  56. example(example: CommandExample) {
  57. this.examples.push(example);
  58. return this;
  59. }
  60. /**
  61. * Add a option for this command
  62. * @param rawName Raw option name(s)
  63. * @param description Option description
  64. * @param config Option config
  65. */
  66. option(rawName: string, description: string, config?: OptionConfig) {
  67. const option = new Option(rawName, description, config);
  68. this.options.push(option);
  69. return this;
  70. }
  71. alias(name: string) {
  72. this.aliasNames.push(name);
  73. return this;
  74. }
  75. action(callback: (...args: any[]) => any) {
  76. this.commandAction = callback;
  77. return this;
  78. }
  79. /**
  80. * Check if a command name is matched by this command
  81. * @param name Command name
  82. */
  83. isMatched(name: string) {
  84. return this.name === name || this.aliasNames.includes(name);
  85. }
  86. get isDefaultCommand() {
  87. return this.name === '' || this.aliasNames.includes('!');
  88. }
  89. get isGlobalCommand(): boolean {
  90. return this instanceof GlobalCommand;
  91. }
  92. /**
  93. * Check if an option is registered in this command
  94. * @param name Option name
  95. */
  96. hasOption(name: string) {
  97. name = name.split('.')[0];
  98. return this.options.find(option => {
  99. return option.names.includes(name);
  100. });
  101. }
  102. outputHelp() {
  103. const {
  104. name,
  105. commands
  106. } = this.cli;
  107. const {
  108. versionNumber,
  109. options: globalOptions,
  110. helpCallback
  111. } = this.cli.globalCommand;
  112. let sections: HelpSection[] = [{
  113. body: `${name}${versionNumber ? `/${versionNumber}` : ''}`
  114. }];
  115. sections.push({
  116. title: 'Usage',
  117. body: ` $ ${name} ${this.usageText || this.rawName}`
  118. });
  119. const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
  120. if (showCommands) {
  121. const longestCommandName = findLongest(commands.map(command => command.rawName));
  122. sections.push({
  123. title: 'Commands',
  124. body: commands.map(command => {
  125. return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
  126. }).join('\n')
  127. });
  128. sections.push({
  129. title: `For more info, run any command with the \`--help\` flag`,
  130. body: commands.map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`).join('\n')
  131. });
  132. }
  133. let options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];
  134. if (!this.isGlobalCommand && !this.isDefaultCommand) {
  135. options = options.filter(option => option.name !== 'version');
  136. }
  137. if (options.length > 0) {
  138. const longestOptionName = findLongest(options.map(option => option.rawName));
  139. sections.push({
  140. title: 'Options',
  141. body: options.map(option => {
  142. return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
  143. }).join('\n')
  144. });
  145. }
  146. if (this.examples.length > 0) {
  147. sections.push({
  148. title: 'Examples',
  149. body: this.examples.map(example => {
  150. if (typeof example === 'function') {
  151. return example(name);
  152. }
  153. return example;
  154. }).join('\n')
  155. });
  156. }
  157. if (helpCallback) {
  158. sections = helpCallback(sections) || sections;
  159. }
  160. console.log(sections.map(section => {
  161. return section.title ? `${section.title}:\n${section.body}` : section.body;
  162. }).join('\n\n'));
  163. }
  164. outputVersion() {
  165. const {
  166. name
  167. } = this.cli;
  168. const {
  169. versionNumber
  170. } = this.cli.globalCommand;
  171. if (versionNumber) {
  172. console.log(`${name}/${versionNumber} ${platformInfo}`);
  173. }
  174. }
  175. checkRequiredArgs() {
  176. const minimalArgsCount = this.args.filter(arg => arg.required).length;
  177. if (this.cli.args.length < minimalArgsCount) {
  178. throw new CACError(`missing required args for command \`${this.rawName}\``);
  179. }
  180. }
  181. /**
  182. * Check if the parsed options contain any unknown options
  183. *
  184. * Exit and output error when true
  185. */
  186. checkUnknownOptions() {
  187. const {
  188. options,
  189. globalCommand
  190. } = this.cli;
  191. if (!this.config.allowUnknownOptions) {
  192. for (const name of Object.keys(options)) {
  193. if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
  194. throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
  195. }
  196. }
  197. }
  198. }
  199. /**
  200. * Check if the required string-type options exist
  201. */
  202. checkOptionValue() {
  203. const {
  204. options: parsedOptions,
  205. globalCommand
  206. } = this.cli;
  207. const options = [...globalCommand.options, ...this.options];
  208. for (const option of options) {
  209. const value = parsedOptions[option.name.split('.')[0]]; // Check required option value
  210. if (option.required) {
  211. const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
  212. if (value === true || value === false && !hasNegated) {
  213. throw new CACError(`option \`${option.rawName}\` value is missing`);
  214. }
  215. }
  216. }
  217. }
  218. }
  219. class GlobalCommand extends Command {
  220. constructor(cli: CAC) {
  221. super('@@global@@', '', {}, cli);
  222. }
  223. }
  224. export type { HelpCallback, CommandExample, CommandConfig };
  225. export { GlobalCommand };
  226. export default Command;