extsprintf.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /*
  2. * extsprintf.js: extended POSIX-style sprintf
  3. */
  4. var mod_assert = require('assert');
  5. var mod_util = require('util');
  6. /*
  7. * Public interface
  8. */
  9. exports.sprintf = jsSprintf;
  10. exports.printf = jsPrintf;
  11. exports.fprintf = jsFprintf;
  12. /*
  13. * Stripped down version of s[n]printf(3c). We make a best effort to throw an
  14. * exception when given a format string we don't understand, rather than
  15. * ignoring it, so that we won't break existing programs if/when we go implement
  16. * the rest of this.
  17. *
  18. * This implementation currently supports specifying
  19. * - field alignment ('-' flag),
  20. * - zero-pad ('0' flag)
  21. * - always show numeric sign ('+' flag),
  22. * - field width
  23. * - conversions for strings, decimal integers, and floats (numbers).
  24. * - argument size specifiers. These are all accepted but ignored, since
  25. * Javascript has no notion of the physical size of an argument.
  26. *
  27. * Everything else is currently unsupported, most notably precision, unsigned
  28. * numbers, non-decimal numbers, and characters.
  29. */
  30. function jsSprintf(ofmt)
  31. {
  32. var regex = [
  33. '([^%]*)', /* normal text */
  34. '%', /* start of format */
  35. '([\'\\-+ #0]*?)', /* flags (optional) */
  36. '([1-9]\\d*)?', /* width (optional) */
  37. '(\\.([1-9]\\d*))?', /* precision (optional) */
  38. '[lhjztL]*?', /* length mods (ignored) */
  39. '([diouxXfFeEgGaAcCsSp%jr])' /* conversion */
  40. ].join('');
  41. var re = new RegExp(regex);
  42. /* variadic arguments used to fill in conversion specifiers */
  43. var args = Array.prototype.slice.call(arguments, 1);
  44. /* remaining format string */
  45. var fmt = ofmt;
  46. /* components of the current conversion specifier */
  47. var flags, width, precision, conversion;
  48. var left, pad, sign, arg, match;
  49. /* return value */
  50. var ret = '';
  51. /* current variadic argument (1-based) */
  52. var argn = 1;
  53. /* 0-based position in the format string that we've read */
  54. var posn = 0;
  55. /* 1-based position in the format string of the current conversion */
  56. var convposn;
  57. /* current conversion specifier */
  58. var curconv;
  59. mod_assert.equal('string', typeof (fmt),
  60. 'first argument must be a format string');
  61. while ((match = re.exec(fmt)) !== null) {
  62. ret += match[1];
  63. fmt = fmt.substring(match[0].length);
  64. /*
  65. * Update flags related to the current conversion specifier's
  66. * position so that we can report clear error messages.
  67. */
  68. curconv = match[0].substring(match[1].length);
  69. convposn = posn + match[1].length + 1;
  70. posn += match[0].length;
  71. flags = match[2] || '';
  72. width = match[3] || 0;
  73. precision = match[4] || '';
  74. conversion = match[6];
  75. left = false;
  76. sign = false;
  77. pad = ' ';
  78. if (conversion == '%') {
  79. ret += '%';
  80. continue;
  81. }
  82. if (args.length === 0) {
  83. throw (jsError(ofmt, convposn, curconv,
  84. 'has no matching argument ' +
  85. '(too few arguments passed)'));
  86. }
  87. arg = args.shift();
  88. argn++;
  89. if (flags.match(/[\' #]/)) {
  90. throw (jsError(ofmt, convposn, curconv,
  91. 'uses unsupported flags'));
  92. }
  93. if (precision.length > 0) {
  94. throw (jsError(ofmt, convposn, curconv,
  95. 'uses non-zero precision (not supported)'));
  96. }
  97. if (flags.match(/-/))
  98. left = true;
  99. if (flags.match(/0/))
  100. pad = '0';
  101. if (flags.match(/\+/))
  102. sign = true;
  103. switch (conversion) {
  104. case 's':
  105. if (arg === undefined || arg === null) {
  106. throw (jsError(ofmt, convposn, curconv,
  107. 'attempted to print undefined or null ' +
  108. 'as a string (argument ' + argn + ' to ' +
  109. 'sprintf)'));
  110. }
  111. ret += doPad(pad, width, left, arg.toString());
  112. break;
  113. case 'd':
  114. arg = Math.floor(arg);
  115. /*jsl:fallthru*/
  116. case 'f':
  117. sign = sign && arg > 0 ? '+' : '';
  118. ret += sign + doPad(pad, width, left,
  119. arg.toString());
  120. break;
  121. case 'x':
  122. ret += doPad(pad, width, left, arg.toString(16));
  123. break;
  124. case 'j': /* non-standard */
  125. if (width === 0)
  126. width = 10;
  127. ret += mod_util.inspect(arg, false, width);
  128. break;
  129. case 'r': /* non-standard */
  130. ret += dumpException(arg);
  131. break;
  132. default:
  133. throw (jsError(ofmt, convposn, curconv,
  134. 'is not supported'));
  135. }
  136. }
  137. ret += fmt;
  138. return (ret);
  139. }
  140. function jsError(fmtstr, convposn, curconv, reason) {
  141. mod_assert.equal(typeof (fmtstr), 'string');
  142. mod_assert.equal(typeof (curconv), 'string');
  143. mod_assert.equal(typeof (convposn), 'number');
  144. mod_assert.equal(typeof (reason), 'string');
  145. return (new Error('format string "' + fmtstr +
  146. '": conversion specifier "' + curconv + '" at character ' +
  147. convposn + ' ' + reason));
  148. }
  149. function jsPrintf() {
  150. var args = Array.prototype.slice.call(arguments);
  151. args.unshift(process.stdout);
  152. jsFprintf.apply(null, args);
  153. }
  154. function jsFprintf(stream) {
  155. var args = Array.prototype.slice.call(arguments, 1);
  156. return (stream.write(jsSprintf.apply(this, args)));
  157. }
  158. function doPad(chr, width, left, str)
  159. {
  160. var ret = str;
  161. while (ret.length < width) {
  162. if (left)
  163. ret += chr;
  164. else
  165. ret = chr + ret;
  166. }
  167. return (ret);
  168. }
  169. /*
  170. * This function dumps long stack traces for exceptions having a cause() method.
  171. * See node-verror for an example.
  172. */
  173. function dumpException(ex)
  174. {
  175. var ret;
  176. if (!(ex instanceof Error))
  177. throw (new Error(jsSprintf('invalid type for %%r: %j', ex)));
  178. /* Note that V8 prepends "ex.stack" with ex.toString(). */
  179. ret = 'EXCEPTION: ' + ex.constructor.name + ': ' + ex.stack;
  180. if (ex.cause && typeof (ex.cause) === 'function') {
  181. var cex = ex.cause();
  182. if (cex) {
  183. ret += '\nCaused by: ' + dumpException(cex);
  184. }
  185. }
  186. return (ret);
  187. }