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