cli.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. #!/usr/bin/env node
  2. /**
  3. * math.js
  4. * https://github.com/josdejong/mathjs
  5. *
  6. * Math.js is an extensive math library for JavaScript and Node.js,
  7. * It features real and complex numbers, units, matrices, a large set of
  8. * mathematical functions, and a flexible expression parser.
  9. *
  10. * Usage:
  11. *
  12. * mathjs [scriptfile(s)] {OPTIONS}
  13. *
  14. * Options:
  15. *
  16. * --version, -v Show application version
  17. * --help, -h Show this message
  18. * --tex Generate LaTeX instead of evaluating
  19. * --string Generate string instead of evaluating
  20. * --parenthesis= Set the parenthesis option to
  21. * either of "keep", "auto" and "all"
  22. *
  23. * Example usage:
  24. * mathjs Open a command prompt
  25. * mathjs 1+2 Evaluate expression
  26. * mathjs script.txt Run a script file
  27. * mathjs script1.txt script2.txt Run two script files
  28. * mathjs script.txt > results.txt Run a script file, output to file
  29. * cat script.txt | mathjs Run input stream
  30. * cat script.txt | mathjs > results.txt Run input stream, output to file
  31. *
  32. * @license
  33. * Copyright (C) 2013-2023 Jos de Jong <wjosdejong@gmail.com>
  34. *
  35. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  36. * use this file except in compliance with the License. You may obtain a copy
  37. * of the License at
  38. *
  39. * https://www.apache.org/licenses/LICENSE-2.0
  40. *
  41. * Unless required by applicable law or agreed to in writing, software
  42. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  43. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  44. * License for the specific language governing permissions and limitations under
  45. * the License.
  46. */
  47. const fs = require('fs')
  48. const path = require('path')
  49. const { createEmptyMap } = require('../lib/cjs/utils/map.js')
  50. let scope = createEmptyMap()
  51. const PRECISION = 14 // decimals
  52. /**
  53. * "Lazy" load math.js: only require when we actually start using it.
  54. * This ensures the cli application looks like it loads instantly.
  55. * When requesting help or version number, math.js isn't even loaded.
  56. * @return {*}
  57. */
  58. function getMath () {
  59. return require('../lib/cjs/defaultInstance.js').default
  60. }
  61. /**
  62. * Helper function to format a value. Regular numbers will be rounded
  63. * to 14 digits to prevent round-off errors from showing up.
  64. * @param {*} value
  65. */
  66. function format (value) {
  67. const math = getMath()
  68. return math.format(value, {
  69. fn: function (value) {
  70. if (typeof value === 'number') {
  71. // round numbers
  72. return math.format(value, PRECISION)
  73. } else {
  74. return math.format(value)
  75. }
  76. }
  77. })
  78. }
  79. /**
  80. * auto complete a text
  81. * @param {String} text
  82. * @return {[Array, String]} completions
  83. */
  84. function completer (text) {
  85. const math = getMath()
  86. let matches = []
  87. let keyword
  88. const m = /[a-zA-Z_0-9]+$/.exec(text)
  89. if (m) {
  90. keyword = m[0]
  91. // scope variables
  92. for (const def in scope.keys()) {
  93. if (def.indexOf(keyword) === 0) {
  94. matches.push(def)
  95. }
  96. }
  97. // commandline keywords
  98. ['exit', 'quit', 'clear'].forEach(function (cmd) {
  99. if (cmd.indexOf(keyword) === 0) {
  100. matches.push(cmd)
  101. }
  102. })
  103. // math functions and constants
  104. const ignore = ['expr', 'type']
  105. for (const func in math.expression.mathWithTransform) {
  106. if (hasOwnProperty(math.expression.mathWithTransform, func)) {
  107. if (func.indexOf(keyword) === 0 && ignore.indexOf(func) === -1) {
  108. matches.push(func)
  109. }
  110. }
  111. }
  112. // units
  113. const Unit = math.Unit
  114. for (const name in Unit.UNITS) {
  115. if (hasOwnProperty(Unit.UNITS, name)) {
  116. if (name.indexOf(keyword) === 0) {
  117. matches.push(name)
  118. }
  119. }
  120. }
  121. for (const name in Unit.PREFIXES) {
  122. if (hasOwnProperty(Unit.PREFIXES, name)) {
  123. const prefixes = Unit.PREFIXES[name]
  124. for (const prefix in prefixes) {
  125. if (hasOwnProperty(prefixes, prefix)) {
  126. if (prefix.indexOf(keyword) === 0) {
  127. matches.push(prefix)
  128. } else if (keyword.indexOf(prefix) === 0) {
  129. const unitKeyword = keyword.substring(prefix.length)
  130. for (const n in Unit.UNITS) {
  131. if (hasOwnProperty(Unit.UNITS, n)) {
  132. if (n.indexOf(unitKeyword) === 0 &&
  133. Unit.isValuelessUnit(prefix + n)) {
  134. matches.push(prefix + n)
  135. }
  136. }
  137. }
  138. }
  139. }
  140. }
  141. }
  142. }
  143. // remove duplicates
  144. matches = matches.filter(function (elem, pos, arr) {
  145. return arr.indexOf(elem) === pos
  146. })
  147. }
  148. return [matches, keyword]
  149. }
  150. /**
  151. * Run stream, read and evaluate input and stream that to output.
  152. * Text lines read from the input are evaluated, and the results are send to
  153. * the output.
  154. * @param input Input stream
  155. * @param output Output stream
  156. * @param mode Output mode
  157. * @param parenthesis Parenthesis option
  158. */
  159. function runStream (input, output, mode, parenthesis) {
  160. const readline = require('readline')
  161. const rl = readline.createInterface({
  162. input: input || process.stdin,
  163. output: output || process.stdout,
  164. completer: completer
  165. })
  166. if (rl.output.isTTY) {
  167. rl.setPrompt('> ')
  168. rl.prompt()
  169. }
  170. // load math.js now, right *after* loading the prompt.
  171. const math = getMath()
  172. // TODO: automatic insertion of 'ans' before operators like +, -, *, /
  173. rl.on('line', function (line) {
  174. const expr = line.trim()
  175. switch (expr.toLowerCase()) {
  176. case 'quit':
  177. case 'exit':
  178. // exit application
  179. rl.close()
  180. break
  181. case 'clear':
  182. // clear memory
  183. scope = createEmptyMap()
  184. console.log('memory cleared')
  185. // get next input
  186. if (rl.output.isTTY) {
  187. rl.prompt()
  188. }
  189. break
  190. default:
  191. if (!expr) {
  192. break
  193. }
  194. switch (mode) {
  195. case 'evaluate':
  196. // evaluate expression
  197. try {
  198. let node = math.parse(expr)
  199. let res = node.evaluate(scope)
  200. if (math.isResultSet(res)) {
  201. // we can have 0 or 1 results in the ResultSet, as the CLI
  202. // does not allow multiple expressions separated by a return
  203. res = res.entries[0]
  204. node = node.blocks
  205. .filter(function (entry) { return entry.visible })
  206. .map(function (entry) { return entry.node })[0]
  207. }
  208. if (node) {
  209. if (math.isAssignmentNode(node)) {
  210. const name = findSymbolName(node)
  211. if (name !== null) {
  212. const value = scope.get(name)
  213. scope.set('ans', value)
  214. console.log(name + ' = ' + format(value))
  215. } else {
  216. scope.set('ans', res)
  217. console.log(format(res))
  218. }
  219. } else if (math.isHelp(res)) {
  220. console.log(res.toString())
  221. } else {
  222. scope.set('ans', res)
  223. console.log(format(res))
  224. }
  225. }
  226. } catch (err) {
  227. console.log(err.toString())
  228. }
  229. break
  230. case 'string':
  231. try {
  232. const string = math.parse(expr).toString({ parenthesis: parenthesis })
  233. console.log(string)
  234. } catch (err) {
  235. console.log(err.toString())
  236. }
  237. break
  238. case 'tex':
  239. try {
  240. const tex = math.parse(expr).toTex({ parenthesis: parenthesis })
  241. console.log(tex)
  242. } catch (err) {
  243. console.log(err.toString())
  244. }
  245. break
  246. }
  247. }
  248. // get next input
  249. if (rl.output.isTTY) {
  250. rl.prompt()
  251. }
  252. })
  253. rl.on('close', function () {
  254. console.log()
  255. process.exit(0)
  256. })
  257. }
  258. /**
  259. * Find the symbol name of an AssignmentNode. Recurses into the chain of
  260. * objects to the root object.
  261. * @param {AssignmentNode} node
  262. * @return {string | null} Returns the name when found, else returns null.
  263. */
  264. function findSymbolName (node) {
  265. const math = getMath()
  266. let n = node
  267. while (n) {
  268. if (math.isSymbolNode(n)) {
  269. return n.name
  270. }
  271. n = n.object
  272. }
  273. return null
  274. }
  275. /**
  276. * Output application version number.
  277. * Version number is read version from package.json.
  278. */
  279. function outputVersion () {
  280. fs.readFile(path.join(__dirname, '/../package.json'), function (err, data) {
  281. if (err) {
  282. console.log(err.toString())
  283. } else {
  284. const pkg = JSON.parse(data)
  285. const version = pkg && pkg.version ? pkg.version : 'unknown'
  286. console.log(version)
  287. }
  288. process.exit(0)
  289. })
  290. }
  291. /**
  292. * Output a help message
  293. */
  294. function outputHelp () {
  295. console.log('math.js')
  296. console.log('https://mathjs.org')
  297. console.log()
  298. console.log('Math.js is an extensive math library for JavaScript and Node.js. It features ')
  299. console.log('real and complex numbers, units, matrices, a large set of mathematical')
  300. console.log('functions, and a flexible expression parser.')
  301. console.log()
  302. console.log('Usage:')
  303. console.log(' mathjs [scriptfile(s)|expression] {OPTIONS}')
  304. console.log()
  305. console.log('Options:')
  306. console.log(' --version, -v Show application version')
  307. console.log(' --help, -h Show this message')
  308. console.log(' --tex Generate LaTeX instead of evaluating')
  309. console.log(' --string Generate string instead of evaluating')
  310. console.log(' --parenthesis= Set the parenthesis option to')
  311. console.log(' either of "keep", "auto" and "all"')
  312. console.log()
  313. console.log('Example usage:')
  314. console.log(' mathjs Open a command prompt')
  315. console.log(' mathjs 1+2 Evaluate expression')
  316. console.log(' mathjs script.txt Run a script file')
  317. console.log(' mathjs script.txt script2.txt Run two script files')
  318. console.log(' mathjs script.txt > results.txt Run a script file, output to file')
  319. console.log(' cat script.txt | mathjs Run input stream')
  320. console.log(' cat script.txt | mathjs > results.txt Run input stream, output to file')
  321. console.log()
  322. process.exit(0)
  323. }
  324. /**
  325. * Process input and output, based on the command line arguments
  326. */
  327. const scripts = [] // queue of scripts that need to be processed
  328. let mode = 'evaluate' // one of 'evaluate', 'tex' or 'string'
  329. let parenthesis = 'keep'
  330. let version = false
  331. let help = false
  332. process.argv.forEach(function (arg, index) {
  333. if (index < 2) {
  334. return
  335. }
  336. switch (arg) {
  337. case '-v':
  338. case '--version':
  339. version = true
  340. break
  341. case '-h':
  342. case '--help':
  343. help = true
  344. break
  345. case '--tex':
  346. mode = 'tex'
  347. break
  348. case '--string':
  349. mode = 'string'
  350. break
  351. case '--parenthesis=keep':
  352. parenthesis = 'keep'
  353. break
  354. case '--parenthesis=auto':
  355. parenthesis = 'auto'
  356. break
  357. case '--parenthesis=all':
  358. parenthesis = 'all'
  359. break
  360. // TODO: implement configuration via command line arguments
  361. default:
  362. scripts.push(arg)
  363. }
  364. })
  365. if (version) {
  366. outputVersion()
  367. } else if (help) {
  368. outputHelp()
  369. } else if (scripts.length === 0) {
  370. // run a stream, can be user input or pipe input
  371. runStream(process.stdin, process.stdout, mode, parenthesis)
  372. } else {
  373. fs.stat(scripts[0], function (e, f) {
  374. if (e) {
  375. console.log(getMath().evaluate(scripts.join(' ')).toString())
  376. } else {
  377. // work through the queue of scripts
  378. scripts.forEach(function (arg) {
  379. // run a script file
  380. runStream(fs.createReadStream(arg), process.stdout, mode, parenthesis)
  381. })
  382. }
  383. })
  384. }
  385. // helper function to safely check whether an object as a property
  386. // copy from the function in object.js which is ES6
  387. function hasOwnProperty (object, property) {
  388. return object && Object.hasOwnProperty.call(object, property)
  389. }