simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

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 }