Ingen beskrivning

action_container.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /** internal
  2. * class ActionContainer
  3. *
  4. * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
  5. **/
  6. 'use strict';
  7. var format = require('util').format;
  8. var _ = require('underscore');
  9. _.str = require('underscore.string');
  10. // Constants
  11. var $$ = require('./const');
  12. //Actions
  13. var ActionHelp = require('./action/help');
  14. var ActionAppend = require('./action/append');
  15. var ActionAppendConstant = require('./action/append/constant');
  16. var ActionCount = require('./action/count');
  17. var ActionStore = require('./action/store');
  18. var ActionStoreConstant = require('./action/store/constant');
  19. var ActionStoreTrue = require('./action/store/true');
  20. var ActionStoreFalse = require('./action/store/false');
  21. var ActionVersion = require('./action/version');
  22. var ActionSubparsers = require('./action/subparsers');
  23. // Errors
  24. var argumentErrorHelper = require('./argument/error');
  25. /**
  26. * new ActionContainer(options)
  27. *
  28. * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
  29. *
  30. * ##### Options:
  31. *
  32. * - `description` -- A description of what the program does
  33. * - `prefixChars` -- Characters that prefix optional arguments
  34. * - `argumentDefault` -- The default value for all arguments
  35. * - `conflictHandler` -- The conflict handler to use for duplicate arguments
  36. **/
  37. var ActionContainer = module.exports = function ActionContainer(options) {
  38. options = options || {};
  39. this.description = options.description;
  40. this.argumentDefault = options.argumentDefault;
  41. this.prefixChars = options.prefixChars || '';
  42. this.conflictHandler = options.conflictHandler;
  43. // set up registries
  44. this._registries = {};
  45. // register actions
  46. this.register('action', null, ActionStore);
  47. this.register('action', 'store', ActionStore);
  48. this.register('action', 'storeConst', ActionStoreConstant);
  49. this.register('action', 'storeTrue', ActionStoreTrue);
  50. this.register('action', 'storeFalse', ActionStoreFalse);
  51. this.register('action', 'append', ActionAppend);
  52. this.register('action', 'appendConst', ActionAppendConstant);
  53. this.register('action', 'count', ActionCount);
  54. this.register('action', 'help', ActionHelp);
  55. this.register('action', 'version', ActionVersion);
  56. this.register('action', 'parsers', ActionSubparsers);
  57. // raise an exception if the conflict handler is invalid
  58. this._getHandler();
  59. // action storage
  60. this._actions = [];
  61. this._optionStringActions = {};
  62. // groups
  63. this._actionGroups = [];
  64. this._mutuallyExclusiveGroups = [];
  65. // defaults storage
  66. this._defaults = {};
  67. // determines whether an "option" looks like a negative number
  68. // -1, -1.5 -5e+4
  69. this._regexpNegativeNumber = new RegExp('^[-]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$');
  70. // whether or not there are any optionals that look like negative
  71. // numbers -- uses a list so it can be shared and edited
  72. this._hasNegativeNumberOptionals = [];
  73. };
  74. // Groups must be required, then ActionContainer already defined
  75. var ArgumentGroup = require('./argument/group');
  76. var MutuallyExclusiveGroup = require('./argument/exclusive');
  77. //
  78. // Registration methods
  79. //
  80. /**
  81. * ActionContainer#register(registryName, value, object) -> Void
  82. * - registryName (String) : object type action|type
  83. * - value (string) : keyword
  84. * - object (Object|Function) : handler
  85. *
  86. * Register handlers
  87. **/
  88. ActionContainer.prototype.register = function (registryName, value, object) {
  89. this._registries[registryName] = this._registries[registryName] || {};
  90. this._registries[registryName][value] = object;
  91. };
  92. ActionContainer.prototype._registryGet = function (registryName, value, defaultValue) {
  93. if (3 > arguments.length) {
  94. defaultValue = null;
  95. }
  96. return this._registries[registryName][value] || defaultValue;
  97. };
  98. //
  99. // Namespace default accessor methods
  100. //
  101. /**
  102. * ActionContainer#setDefaults(options) -> Void
  103. * - options (object):hash of options see [[Action.new]]
  104. *
  105. * Set defaults
  106. **/
  107. ActionContainer.prototype.setDefaults = function (options) {
  108. options = options || {};
  109. for (var property in options) {
  110. this._defaults[property] = options[property];
  111. }
  112. // if these defaults match any existing arguments, replace the previous
  113. // default on the object with the new one
  114. this._actions.forEach(function (action) {
  115. if (action.dest in options) {
  116. action.defaultValue = options[action.dest];
  117. }
  118. });
  119. };
  120. /**
  121. * ActionContainer#getDefault(dest) -> Mixed
  122. * - dest (string): action destination
  123. *
  124. * Return action default value
  125. **/
  126. ActionContainer.prototype.getDefault = function (dest) {
  127. var result = (_.has(this._defaults, dest)) ? this._defaults[dest] : null;
  128. this._actions.forEach(function (action) {
  129. if (action.dest === dest && _.has(action, 'defaultValue')) {
  130. result = action.defaultValue;
  131. }
  132. });
  133. return result;
  134. };
  135. //
  136. // Adding argument actions
  137. //
  138. /**
  139. * ActionContainer#addArgument(args, options) -> Object
  140. * - args (Array): array of argument keys
  141. * - options (Object): action objects see [[Action.new]]
  142. *
  143. * #### Examples
  144. * - addArgument([-f, --foo], {action:'store', defaultValue=1, ...})
  145. * - addArgument(['bar'], action: 'store', nargs:1, ...})
  146. **/
  147. ActionContainer.prototype.addArgument = function (args, options) {
  148. args = args;
  149. options = options || {};
  150. if (!_.isArray(args)) {
  151. throw new TypeError('addArgument first argument should be an array');
  152. }
  153. if (!_.isObject(options) || _.isArray(options)) {
  154. throw new TypeError('addArgument second argument should be a hash');
  155. }
  156. // if no positional args are supplied or only one is supplied and
  157. // it doesn't look like an option string, parse a positional argument
  158. if (!args || args.length === 1 && this.prefixChars.indexOf(args[0][0]) < 0) {
  159. if (args && !!options.dest) {
  160. throw new Error('dest supplied twice for positional argument');
  161. }
  162. options = this._getPositional(args, options);
  163. // otherwise, we're adding an optional argument
  164. } else {
  165. options = this._getOptional(args, options);
  166. }
  167. // if no default was supplied, use the parser-level default
  168. if (_.isUndefined(options.defaultValue)) {
  169. var dest = options.dest;
  170. if (_.has(this._defaults, dest)) {
  171. options.defaultValue = this._defaults[dest];
  172. } else if (!_.isUndefined(this.argumentDefault)) {
  173. options.defaultValue = this.argumentDefault;
  174. }
  175. }
  176. // create the action object, and add it to the parser
  177. var ActionClass = this._popActionClass(options);
  178. if (! _.isFunction(ActionClass)) {
  179. throw new Error(format('Unknown action "%s".', ActionClass));
  180. }
  181. var action = new ActionClass(options);
  182. // throw an error if the action type is not callable
  183. var typeFunction = this._registryGet('type', action.type, action.type);
  184. if (!_.isFunction(typeFunction)) {
  185. throw new Error(format('"%s" is not callable', typeFunction));
  186. }
  187. return this._addAction(action);
  188. };
  189. /**
  190. * ActionContainer#addArgumentGroup(options) -> ArgumentGroup
  191. * - options (Object): hash of options see [[ArgumentGroup.new]]
  192. *
  193. * Create new arguments groups
  194. **/
  195. ActionContainer.prototype.addArgumentGroup = function (options) {
  196. var group = new ArgumentGroup(this, options);
  197. this._actionGroups.push(group);
  198. return group;
  199. };
  200. /**
  201. * ActionContainer#addMutuallyExclusiveGroup(options) -> ArgumentGroup
  202. * - options (Object): {required: false}
  203. *
  204. * Create new mutual exclusive groups
  205. **/
  206. ActionContainer.prototype.addMutuallyExclusiveGroup = function (options) {
  207. var group = new MutuallyExclusiveGroup(this, options);
  208. this._mutuallyExclusiveGroups.push(group);
  209. return group;
  210. };
  211. ActionContainer.prototype._addAction = function (action) {
  212. var self = this;
  213. // resolve any conflicts
  214. this._checkConflict(action);
  215. // add to actions list
  216. this._actions.push(action);
  217. action.container = this;
  218. // index the action by any option strings it has
  219. action.optionStrings.forEach(function (optionString) {
  220. self._optionStringActions[optionString] = action;
  221. });
  222. // set the flag if any option strings look like negative numbers
  223. action.optionStrings.forEach(function (optionString) {
  224. if (optionString.match(self._regexpNegativeNumber)) {
  225. if (!_.any(self._hasNegativeNumberOptionals)) {
  226. self._hasNegativeNumberOptionals.push(true);
  227. }
  228. }
  229. });
  230. // return the created action
  231. return action;
  232. };
  233. ActionContainer.prototype._removeAction = function (action) {
  234. var actionIndex = this._actions.indexOf(action);
  235. if (actionIndex >= 0) {
  236. this._actions.splice(actionIndex, 1);
  237. }
  238. };
  239. ActionContainer.prototype._addContainerActions = function (container) {
  240. // collect groups by titles
  241. var titleGroupMap = {};
  242. this._actionGroups.forEach(function (group) {
  243. if (titleGroupMap[group.title]) {
  244. throw new Error(format('Cannot merge actions - two groups are named "%s".', group.title));
  245. }
  246. titleGroupMap[group.title] = group;
  247. });
  248. // map each action to its group
  249. var groupMap = {};
  250. function actionHash(action) {
  251. // unique (hopefully?) string suitable as dictionary key
  252. return action.getName();
  253. }
  254. container._actionGroups.forEach(function (group) {
  255. // if a group with the title exists, use that, otherwise
  256. // create a new group matching the container's group
  257. if (!titleGroupMap[group.title]) {
  258. titleGroupMap[group.title] = this.addArgumentGroup({
  259. title: group.title,
  260. description: group.description
  261. });
  262. }
  263. // map the actions to their new group
  264. group._groupActions.forEach(function (action) {
  265. groupMap[actionHash(action)] = titleGroupMap[group.title];
  266. });
  267. }, this);
  268. // add container's mutually exclusive groups
  269. // NOTE: if add_mutually_exclusive_group ever gains title= and
  270. // description= then this code will need to be expanded as above
  271. var mutexGroup;
  272. container._mutuallyExclusiveGroups.forEach(function (group) {
  273. mutexGroup = this.addMutuallyExclusiveGroup({
  274. required: group.required
  275. });
  276. // map the actions to their new mutex group
  277. group._groupActions.forEach(function (action) {
  278. groupMap[actionHash(action)] = mutexGroup;
  279. });
  280. }, this); // forEach takes a 'this' argument
  281. // add all actions to this container or their group
  282. container._actions.forEach(function (action) {
  283. var key = actionHash(action);
  284. if (!!groupMap[key]) {
  285. groupMap[key]._addAction(action);
  286. }
  287. else
  288. {
  289. this._addAction(action);
  290. }
  291. });
  292. };
  293. ActionContainer.prototype._getPositional = function (dest, options) {
  294. if (_.isArray(dest)) {
  295. dest = _.first(dest);
  296. }
  297. // make sure required is not specified
  298. if (options.required) {
  299. throw new Error('"required" is an invalid argument for positionals.');
  300. }
  301. // mark positional arguments as required if at least one is
  302. // always required
  303. if (options.nargs !== $$.OPTIONAL && options.nargs !== $$.ZERO_OR_MORE) {
  304. options.required = true;
  305. }
  306. if (options.nargs === $$.ZERO_OR_MORE && options.defaultValue === undefined) {
  307. options.required = true;
  308. }
  309. // return the keyword arguments with no option strings
  310. options.dest = dest;
  311. options.optionStrings = [];
  312. return options;
  313. };
  314. ActionContainer.prototype._getOptional = function (args, options) {
  315. var prefixChars = this.prefixChars;
  316. var optionStrings = [];
  317. var optionStringsLong = [];
  318. // determine short and long option strings
  319. args.forEach(function (optionString) {
  320. // error on strings that don't start with an appropriate prefix
  321. if (prefixChars.indexOf(optionString[0]) < 0) {
  322. throw new Error(format('Invalid option string "%s": must start with a "%s".',
  323. optionString,
  324. prefixChars
  325. ));
  326. }
  327. // strings starting with two prefix characters are long options
  328. optionStrings.push(optionString);
  329. if (optionString.length > 1 && prefixChars.indexOf(optionString[1]) >= 0) {
  330. optionStringsLong.push(optionString);
  331. }
  332. });
  333. // infer dest, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
  334. var dest = options.dest || null;
  335. delete options.dest;
  336. if (!dest) {
  337. var optionStringDest = optionStringsLong.length ? optionStringsLong[0] :optionStrings[0];
  338. dest = _.str.strip(optionStringDest, this.prefixChars);
  339. if (dest.length === 0) {
  340. throw new Error(
  341. format('dest= is required for options like "%s"', optionStrings.join(', '))
  342. );
  343. }
  344. dest = dest.replace(/-/g, '_');
  345. }
  346. // return the updated keyword arguments
  347. options.dest = dest;
  348. options.optionStrings = optionStrings;
  349. return options;
  350. };
  351. ActionContainer.prototype._popActionClass = function (options, defaultValue) {
  352. defaultValue = defaultValue || null;
  353. var action = (options.action || defaultValue);
  354. delete options.action;
  355. var actionClass = this._registryGet('action', action, action);
  356. return actionClass;
  357. };
  358. ActionContainer.prototype._getHandler = function () {
  359. var handlerString = this.conflictHandler;
  360. var handlerFuncName = "_handleConflict" + _.str.capitalize(handlerString);
  361. var func = this[handlerFuncName];
  362. if (typeof func === 'undefined') {
  363. var msg = "invalid conflict resolution value: " + handlerString;
  364. throw new Error(msg);
  365. } else {
  366. return func;
  367. }
  368. };
  369. ActionContainer.prototype._checkConflict = function (action) {
  370. var optionStringActions = this._optionStringActions;
  371. var conflictOptionals = [];
  372. // find all options that conflict with this option
  373. // collect pairs, the string, and an existing action that it conflicts with
  374. action.optionStrings.forEach(function (optionString) {
  375. var conflOptional = optionStringActions[optionString];
  376. if (typeof conflOptional !== 'undefined') {
  377. conflictOptionals.push([optionString, conflOptional]);
  378. }
  379. });
  380. if (conflictOptionals.length > 0) {
  381. var conflictHandler = this._getHandler();
  382. conflictHandler.call(this, action, conflictOptionals);
  383. }
  384. };
  385. ActionContainer.prototype._handleConflictError = function (action, conflOptionals) {
  386. var conflicts = _.map(conflOptionals, function (pair) {return pair[0]; });
  387. conflicts = conflicts.join(', ');
  388. throw argumentErrorHelper(
  389. action,
  390. format('Conflicting option string(s): %s', conflicts)
  391. );
  392. };
  393. ActionContainer.prototype._handleConflictResolve = function (action, conflOptionals) {
  394. // remove all conflicting options
  395. var self = this;
  396. conflOptionals.forEach(function (pair) {
  397. var optionString = pair[0];
  398. var conflictingAction = pair[1];
  399. // remove the conflicting option string
  400. var i = conflictingAction.optionStrings.indexOf(optionString);
  401. if (i >= 0) {
  402. conflictingAction.optionStrings.splice(i, 1);
  403. }
  404. delete self._optionStringActions[optionString];
  405. // if the option now has no option string, remove it from the
  406. // container holding it
  407. if (conflictingAction.optionStrings.length === 0) {
  408. conflictingAction.container._removeAction(conflictingAction);
  409. }
  410. });
  411. };