├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── adventure.js ├── async.js ├── calc ├── keyserver │ ├── README │ ├── client.js │ ├── key │ └── server.js ├── mud.js ├── shell.js └── web │ ├── public │ └── mingy_times.png │ └── web.js ├── lib ├── client.js ├── command.js ├── connectMiddleware.js ├── mingy.js ├── parser.js └── shell.js ├── package.json └── test └── mingy.test.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.2.3 / 2011-02-27 2 | ================== 3 | 4 | * Added lexeme transforms to parser, so parsing plugins are doable 5 | 6 | 0.2.2 / 2011-02-21 7 | ================== 8 | 9 | * Added client/server example 10 | 11 | 0.2.1 / 2011-02-21 12 | ================== 13 | 14 | * Added missing dependency to package.json 15 | 16 | 0.2.0 / 2011-02-19 17 | ================== 18 | 19 | * Changed web server to Connect middleware 20 | * Renamed "sendHtmlResponse" web helper function to "send" 21 | 22 | 0.1.3 / 2011-02-18 23 | ================== 24 | 25 | * Added 404 handling to web server 26 | 27 | 0.1.2 / 2011-02-16 28 | ================== 29 | 30 | * Added ability to load hash of commands when creating parser 31 | * Added WebServer class to use Mingy as a web router 32 | * Got web server example working 33 | 34 | 0.1.1 / 2011-02-16 35 | ================== 36 | 37 | * Changed third argument of command logic to take a hash instead of stream 38 | object 39 | * Added callback support 40 | 41 | 0.0.1 / 2011-02-14 42 | ================== 43 | 44 | * Initial release 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Mike Cantelon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ,---. ,---..-./`) ,---. .--. .-_'''-. ____ __ 2 | | \ / |\ .-.')| \ | | '_( )_ \ \ \ / / 3 | | , \/ , |/ `-' \| , \ | ||(_ o _)| ' \ _. / ' 4 | | |\_ /| | `-'`"`| |\_ \| |. (_,_)/___| _( )_ .' 5 | | _( )_/ | | .---. | _( )_\ || | .-----. ___(_ o _)' 6 | | (_ o _) | | | | | (_ o _) |' \ '- .'| |(_,_)' 7 | | (_,_) | | | | | (_,_)\ | \ `-'` | | `-' / 8 | | | | | | | | | | | \ / \ / 9 | '--' '--' '---' '--' '--' `'-...-' `-..-' 10 | 11 | Mingy is a cheap and cheerful command parser/server for node.js CLI tools, 12 | adventure games, MUDs, and other such endeavors. 13 | 14 | For CLI tools, Mingy works well with 15 | [node-optimist](https://github.com/substack/node-optimist) (available as 16 | "optimist" via npm). For interactive uses, Mingy includes a shell handler that 17 | lets you quickly use your commands interactively. A Mingy shell can work 18 | locally via the command line or remotely as a server. 19 | 20 | Mingy includes a number of simple demos, in the "examples" directory, that can 21 | give you a quick idea how things work. Examples include a CLI calculator, a 22 | file navigation shell, a tiny text adventure game, the foundation of a MUD, 23 | a key/value server, and a web application using Mingy for routing. 24 | 25 | In you use npm, simply `npm install mingy` to install. 26 | 27 | ## Commands 28 | 29 | In order to reap the "magic" of mingy you define a number of commands, each of 30 | which has one or more syntax forms. 31 | 32 | Syntax forms are strings that define command usage. The syntax form `look` 33 | would mean the parser input `look` would trigger the command. The syntax 34 | `look ` would mean the parser input `look mailbox` or `look demon` would 35 | both trigger the command (with the command's "prop" argument being set, 36 | respectively, to `mailbox` and `demon`). 37 | 38 | Application state is stored by the parser in the env property and is relayed to 39 | commands when executed. 40 | 41 | An example command for a text adventure game: 42 | 43 | parser.addCommand('go') 44 | .set('syntax', ['go ']) 45 | .set('logic', function(args, env) { 46 | 47 | var output = '' 48 | var direction = args.direction 49 | 50 | var location = env.locations[env.location] 51 | 52 | if (location.exits[direction]) { 53 | output += "You go " + direction + ".\n" 54 | env.location = location.exits[direction] 55 | } 56 | else { 57 | output += "You can't go that way.\n" 58 | } 59 | 60 | return output 61 | }) 62 | 63 | ## Validators 64 | 65 | Arguments can either be unvalidated, validated by type, or validated by 66 | handlers. Handlers check an argument, returning success or failure and, in 67 | the event of failure, an optional error message. 68 | 69 | An example that could be used to validate direction arguments for the text 70 | adventure game example above: 71 | 72 | parser.addValidator('validDirection', function(lexeme) { 73 | 74 | var validDirections = ['north', 'south', 'east', 'west'] 75 | 76 | return { 77 | 'success': (validDirections.indexOf(lexeme) != -1), 78 | 'message': "That's not a direction I understand.\n" 79 | } 80 | }) 81 | 82 | To have a validator apply to a command argument the command's syntax form(s) 83 | must prefix the name of an argument with the name of a handler and a colon. 84 | For example, to enable the above validator on the above command one would 85 | make the following change to the command: 86 | 87 | parser.addCommand('go') 88 | .set('syntax', ['go ']) 89 | 90 | Validation by type is simpler. One just needs to prefix the name of an 91 | argument with the name of the type and a colon. For example: 92 | 93 | parser.addCommand('go') 94 | .set('syntax', ['go ']) 95 | 96 | ## Parsing 97 | 98 | To parse command input, either the `Parser.parse` or `Parser.parseLexemes` 99 | methods are used. A response string is returned by the method (or, if 100 | the parse was unsuccessful, `undefined`). 101 | 102 | Here's an example of parsing a string of text input using `Parser.parse`: 103 | 104 | parser.parse('go north') 105 | 106 | Here's an example of using `Parser.parseLexemes` to parse an array of 107 | unhyphenated options returned from node-optimist: 108 | 109 | parser.parseLexemes(argv['_']) 110 | 111 | If your commands contain asynchronous logic and you'd like command output to 112 | be handled by a callback, you can supply one in the second argument to either 113 | `Parser.parse` or `Parser.parseLexemes`. Below is an example: 114 | 115 | parser.parse('go north', function(output) { 116 | console.log(output) 117 | }) 118 | 119 | To set an environmental variable use the `Parser.setEnv` method: 120 | 121 | parser.setEnv('skyIs', 'blue') 122 | 123 | ## Concatenation 124 | 125 | Normally an argument takes a single lexeme. If one wants to have an argument 126 | contain a concatenation of all subsequent lexemes adding a `*` after the 127 | argument name will enable this. 128 | 129 | Note that these arguments only work when they are the last argument in a 130 | syntax form. 131 | 132 | Below is an example: 133 | 134 | parser.addCommand('say') 135 | .set('syntax', ['say ']) 136 | .set('logic', function(args, env, stream) { 137 | 138 | return "You say '" + args['message*'] + "'.\n" 139 | }) 140 | 141 | ## Interaction via Shell 142 | 143 | Setting up an interactive shell is easy. Below is an example of setting 144 | up a shell with a couple of useless commands. 145 | 146 | var welcome = "Welcome to The Shell of Futility. You can brood and shout.\n" 147 | 148 | var parser = new Parser() 149 | 150 | parser.addCommand('brood') 151 | .set('syntax', ['brood']) 152 | .set('logic', function(args) { 153 | return "You stare, angrily, at the floor.\n" 154 | }) 155 | 156 | parser.addCommand('shout') 157 | .set('syntax', ['shout']) 158 | .set('logic', function(args) { 159 | return "You shout in definance and rage.\n" 160 | }) 161 | 162 | var shell = new Shell(parser) 163 | .set('welcome', welcome) 164 | .set('prompt', '$ ') 165 | .start() 166 | 167 | ## Interaction via Shell Server 168 | 169 | A shell server may be used to provide remote interactivity. The above example 170 | can function as a server simply by changing the last line to the following: 171 | 172 | .startServer() 173 | 174 | The shell is now accessible via telnet: 175 | 176 | telnet localhost 8000 177 | 178 | ## Dealing with Multiple Users 179 | 180 | For multi-user shells, you may wish to use the third optional parameter that 181 | is sent to command logic. This parameter provides access to context-specific 182 | data and functions. When using the shell server a property of this parameter 183 | is the node.js stream object representing the connection of the user to your 184 | server. The shell server adds a `userID` property to the stream. This property 185 | can be used to differentiate between users. Please see `examples/mud.js` for 186 | an example of this. 187 | 188 | ## Connect Middleware 189 | 190 | Using the included [Connect](https://github.com/senchalabs/connect) middleware 191 | module, Mingy can act as a request router. Please see `examples/web.js` for an 192 | example of this. 193 | 194 | ## Initialization 195 | 196 | To get easy access to Mingy's parser, command, and shell classes, include 197 | the folowing code: 198 | 199 | var mingy = require('/path/to/mingy') 200 | , Parser = mingy.Parser 201 | , Command = mingy.Command 202 | , Shell = mingy.Shell 203 | 204 | If you'd like to quickly specify and initialize a number of commands, you 205 | can create a hash of them and provide this hash to the parser when 206 | initializing. For example: 207 | 208 | var commands = { 209 | "look": { 210 | "syntax": ["look"], 211 | "logic": function(args) { 212 | return "You look around." 213 | } 214 | } 215 | } 216 | 217 | var parser = new Parser(commands) 218 | 219 | See the [node-deja](https://github.com/mcantelon/node-deja/blob/master/deja.js) 220 | module for a more substantial example of this. 221 | 222 | ## Testing 223 | 224 | Testing requires the [expresso](ihttps://github.com/visionmedia/expresso) 225 | and [should.js](https://github.com/visionmedia/should.js) modules (available 226 | via rpm as "expresso" and "should" respectively). 227 | 228 | Run the tests by entering: 229 | 230 | expresso 231 | -------------------------------------------------------------------------------- /examples/adventure.js: -------------------------------------------------------------------------------- 1 | var mingy = require('../lib/mingy') 2 | , Parser = mingy.Parser 3 | , Command = mingy.Command 4 | , Shell = mingy.Shell 5 | 6 | // define locations in our special game 7 | function Location() {} 8 | var locations = {} 9 | 10 | var hallway = new Location() 11 | hallway.description = "You are in a hallway. From here you can go north." 12 | hallway.exits = {"north": "room"} 13 | locations['hallway'] = hallway 14 | 15 | var room = new Location() 16 | room.description = "You are in a groovy room. From here you can go south." 17 | room.exits = {"south": "hallway"} 18 | locations['room'] = room 19 | 20 | // define props in our special game 21 | function Prop() {} 22 | var props = [] 23 | 24 | var rock = new Prop() 25 | rock.description = 'The rock is alright.' 26 | rock.location = 'hallway' 27 | props['rock'] = rock 28 | 29 | // set up parser and commands 30 | var parser = new Parser() 31 | 32 | // prop validator restricts lexeme to props in current location 33 | parser.addValidator('propPresent', function(lexeme, env) { 34 | 35 | // make sure prop exists 36 | var success = (env.props[lexeme]) ? true : false 37 | 38 | // make sure prop is in current location 39 | if (success && (env.props[lexeme].location != env.location)) { 40 | success = false 41 | } 42 | 43 | return { 44 | 'success': success, 45 | 'message': "I don't see that.\n" 46 | } 47 | }) 48 | 49 | // prop validator restricts lexeme to props in current location 50 | parser.addValidator('propHeld', function(lexeme, env) { 51 | 52 | // make sure prop exists 53 | var success = (env.props[lexeme]) ? true : false 54 | 55 | // make sure prop is in player's inventory 56 | if (success && (env.props[lexeme].location != 'player')) { 57 | success = false 58 | } 59 | 60 | return { 61 | 'success': success, 62 | 'message': "I don't have that.\n" 63 | } 64 | }) 65 | 66 | parser.addCommand('quit') 67 | .set('syntax', ['quit', 'exit']) 68 | .set('logic', function(args) { 69 | process.exit(0) 70 | }) 71 | 72 | parser.addCommand('help') 73 | .set('syntax', ['help']) 74 | .set('logic', function(args) { 75 | 76 | var output = '' 77 | 78 | output += 'You can use the following commands:\n'+ 79 | ' "look" (or "l") to look around\n'+ 80 | ' "go " to walk in a direction\n'+ 81 | ' "get " to pick up something\n'+ 82 | ' "drop " to drop something\n'+ 83 | ' "inventory" (or "i") to list what you\'re carrying\n'+ 84 | ' "exit" to quit the game\n' 85 | 86 | return output 87 | }) 88 | 89 | parser.addCommand('look') 90 | .set('syntax', ['l', 'look']) 91 | .set('logic', function(args, env) { 92 | 93 | var output = '' 94 | 95 | // describe location 96 | output += env.locations[env.location].description + "\n" 97 | 98 | // describe nearby props 99 | for (var propName in env.props) { 100 | var prop = env.props[propName] 101 | if (prop.location == env.location) { 102 | output += "You see a " + propName + ".\n" 103 | } 104 | } 105 | 106 | return output 107 | }) 108 | 109 | parser.addValidator('direction', function(lexeme) { 110 | 111 | var validDirections = ['north', 'south', 'east', 'west'] 112 | 113 | return { 114 | 'success': (validDirections.indexOf(lexeme) != -1), 115 | 'message': "That's not a direction I understand.\n" 116 | } 117 | }) 118 | 119 | parser.addCommand('go') 120 | .set('syntax', ['go ']) 121 | .set('logic', function(args, env) { 122 | 123 | var output = '' 124 | var direction = args.direction 125 | 126 | var location = env.locations[env.location] 127 | 128 | if (location.exits[direction]) { 129 | output += "You go " + direction + ".\n" 130 | env.location = location.exits[direction] 131 | } 132 | else { 133 | output += "You can't go that way.\n" 134 | } 135 | 136 | return output 137 | }) 138 | 139 | parser.addCommand('get') 140 | .set('syntax', ['get ']) 141 | .set('logic', function(args, env) { 142 | 143 | var output = '' 144 | var prop = args['propPresent'] 145 | 146 | env.props[prop].location = 'player' 147 | output += "You take the " + prop + ".\n" 148 | 149 | return output 150 | }) 151 | 152 | parser.addCommand('drop') 153 | .set('syntax', ['drop ']) 154 | .set('logic', function(args, env) { 155 | 156 | var output = '' 157 | var prop = args.propHeld 158 | 159 | env.props[prop].location = env.location 160 | output += "You drop the " + prop + ".\n" 161 | 162 | return output 163 | }) 164 | 165 | parser.addCommand('inventory') 166 | .set('syntax', ['i', 'inventory']) 167 | .set('logic', function(args, env) { 168 | 169 | var output = '' 170 | 171 | // list props being carried 172 | for (var propName in env.props) { 173 | var prop = env.props[propName] 174 | if (prop.location == 'player') { 175 | output += "You have a " + propName + ".\n" 176 | } 177 | } 178 | 179 | if (output == '') { 180 | output += "You're carrying nothing.\n" 181 | } 182 | 183 | return output 184 | }) 185 | 186 | parser.setEnv('locations', locations) 187 | parser.setEnv('props', props) 188 | parser.setEnv('propsStartingCondition', parser.clone(props)) 189 | parser.setEnv('location', 'hallway') 190 | 191 | // begin adventurings! 192 | var welcome = 'Welcome to Rock Moving Adventure!\n\n'+ 193 | 'In a world gone mad, one rock is out of place.\n'+ 194 | 'Enter "help" for a list of commands.\n' 195 | 196 | var shell = new Shell(parser) 197 | .set('welcome', welcome) 198 | .set('logic', function(shell) { 199 | 200 | var output = '' 201 | 202 | // if player wins, allow her to restart game 203 | if ( 204 | shell.parser.env.props.rock.location == 'room' 205 | && shell.mode != 'waitForRestart' 206 | ) { 207 | output += "Congratulations!!! You've set things right and won the game!\n\n" 208 | output += "Do you want to restart? ('yes' or 'no')\n" 209 | shell.mode = 'waitForRestart' 210 | } 211 | 212 | return output 213 | }) 214 | .setMode('waitForRestart', function(shell, data) { 215 | 216 | data = shell.parser.cleanInput(data) 217 | 218 | var output = '' 219 | 220 | if (data == "yes") { 221 | 222 | output += "Restarting...\n\n" 223 | 224 | // reset props and player location to initial state 225 | shell.parser.env.props = shell.parser.clone(shell.parser.env.propsStartingCondition) 226 | shell.parser.env.location = 'hallway' 227 | 228 | output += shell.parser.commands['look'].logic({}, shell.parser.env) 229 | 230 | shell.mode = "default" 231 | } 232 | else if (data == "no") { 233 | output += "Thanks for playing.\n" 234 | process.exit() 235 | } 236 | else { 237 | output += "Please enter 'yes' or 'no.\n" 238 | } 239 | 240 | return output 241 | }) 242 | .start() 243 | -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | var mingy = require('../lib/mingy') 2 | , Parser = mingy.Parser 3 | , Command = mingy.Command 4 | 5 | var parser = new Parser() 6 | 7 | parser.addCommand('add') 8 | .set('syntax', ['test']) 9 | .set('logic', function(args, env, system) { 10 | 11 | setTimeout(function() { 12 | system.callback('Time has passed.'); 13 | }, 1000); 14 | }) 15 | 16 | parser.parse('test', function(message) { 17 | console.log(message); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/calc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var mingy = require('../lib/mingy') 3 | , Parser = mingy.Parser 4 | , Command = mingy.Command 5 | , argv = require('optimist') 6 | .boolean(['e']) 7 | .argv 8 | 9 | // set up parser and commands 10 | var parser = new Parser() 11 | 12 | // allow command-line flag to determine whether equation gets shown 13 | parser.env['showEquation'] = argv['e'] 14 | 15 | parser.addCommand('add') 16 | .set('syntax', [ 17 | 'add ', 18 | '+ ' 19 | ]) 20 | .set('logic', function(args, env) { 21 | 22 | var output = '' 23 | 24 | if (env.showEquation) { 25 | output += args['a'] + " + " + args['b'] + " = " 26 | } 27 | 28 | output += args['a'] + args['b'] 29 | 30 | return output 31 | }) 32 | 33 | parser.addCommand('subtract') 34 | .set('syntax', [ 35 | 'subtract ', 36 | '- ' 37 | ]) 38 | .set('logic', function(args) { 39 | return args['a'] + args['b'] 40 | }) 41 | 42 | parser.addCommand('help') 43 | .set('syntax', ['help']) 44 | .set('logic', function(args, env) { 45 | 46 | var output = '' 47 | 48 | output += 'Welcome to POWERCALC!\n\n'+ 49 | 50 | ' Usage: calc [options] \n\n'+ 51 | 52 | ' Options:\n'+ 53 | ' -e show equation\n\n'+ 54 | 55 | ' Commands:\n'+ 56 | ' "add " to add two numbers\n'+ 57 | ' "+ " to add two numbers\n'+ 58 | ' "subtract " to subtract two numbers\n'+ 59 | ' "- " to subtract two numbers' 60 | 61 | return output 62 | }) 63 | 64 | var output = parser.parseLexemes(argv['_']) 65 | 66 | output = output || 'Bad arguments! Try "calc help".' 67 | 68 | console.log(output) 69 | -------------------------------------------------------------------------------- /examples/keyserver/README: -------------------------------------------------------------------------------- 1 | The keyserver example shows how Mingy can be used to implement simple TCP/IP 2 | protocols. 3 | 4 | Run server.js to start the server. 5 | 6 | Then run client.js to see a script communicating with the server. 7 | 8 | Then try running cli.js to see how a CLI command can also communicate using 9 | the protocol. 10 | 11 | Try entering: 12 | 13 | ./key set name Bob 14 | 15 | then: 16 | 17 | ./key get Bob 18 | -------------------------------------------------------------------------------- /examples/keyserver/client.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | , mingy = require('../../lib/mingy') 3 | , Client = mingy.Client 4 | , Parser = mingy.Parser 5 | , Command = mingy.Command 6 | , parser = new Parser() 7 | , client; 8 | 9 | // generate random key name 10 | parser.setEnv('keyName', Math.floor(Math.random() * 1000000000).toString(24)) 11 | console.log('Generated key name ' + parser.env.keyName + '.') 12 | 13 | // when connected with server, send command to store value 14 | parser.addCommand('connected') 15 | .set('syntax', ['connected']) 16 | .set('logic', function(args, env, system) { 17 | 18 | var randomNumber = Math.floor(Math.random() * 10) 19 | system.server.write('set ' + env.keyName + ' ' + randomNumber) 20 | return 'Setting data to ' + randomNumber + '...' 21 | }) 22 | 23 | // when server has stored value, send command to get value 24 | parser.addCommand('stored') 25 | .set('syntax', ['stored']) 26 | .set('logic', function(args, env, system) { 27 | 28 | system.server.write('get ' + env.keyName) 29 | return 'Set succeeded.' 30 | }) 31 | 32 | // receive value and output to console 33 | parser.addCommand('value') 34 | .set('syntax', ['value ']) 35 | .set('logic', function(args) { 36 | 37 | console.log('Retrieved value of ' + args.key + ': ' + args.value) 38 | client.close(); 39 | return 'Value retrieved.' 40 | }) 41 | 42 | client = new Client(parser) 43 | client.set('port', 8888).start() 44 | -------------------------------------------------------------------------------- /examples/keyserver/key: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var net = require('net') 3 | , mingy = require('../../lib/mingy') 4 | , Client = mingy.Client 5 | , Parser = mingy.Parser 6 | , Command = mingy.Command 7 | , parser = new Parser() 8 | , client; 9 | 10 | // send command to server to set key to value 11 | parser.addCommand('set') 12 | .set('syntax', ['set ']) 13 | .set('logic', function(args, env, system) { 14 | 15 | system.server.write('set ' + args.key + ' ' + args.value) 16 | 17 | return "Attempting set..." 18 | }) 19 | 20 | // when server has stored value, report success 21 | parser.addCommand('stored') 22 | .set('syntax', ['stored']) 23 | .set('logic', function(args, env) { 24 | 25 | client.close(); 26 | return 'Set succeeded.' 27 | }) 28 | 29 | // send command to set key to value 30 | parser.addCommand('get') 31 | .set('syntax', ['get ']) 32 | .set('logic', function(args, env, system) { 33 | system.server.write('get ' + args.key) 34 | 35 | return "Attempting get..." 36 | }) 37 | 38 | // receive value and output to console 39 | parser.addCommand('value') 40 | .set('syntax', ['value ']) 41 | .set('logic', function(args) { 42 | 43 | console.log('Retrieved value of ' + args.key + ': ' + args.value) 44 | client.close(); 45 | return 'Value retrieved.' 46 | }) 47 | 48 | // parse if valid, otherwise display usage 49 | var lexemes = process.argv.slice(2); 50 | if (parser.validCommands(lexemes).length > 0) { 51 | client = new Client(parser) 52 | client.set('port', 8888).start(lexemes) 53 | } else { 54 | console.log('Usage: key set | key get '); 55 | } 56 | -------------------------------------------------------------------------------- /examples/keyserver/server.js: -------------------------------------------------------------------------------- 1 | var mingy = require('../../lib/mingy') 2 | , Parser = mingy.Parser 3 | , Command = mingy.Command 4 | , Shell = mingy.Shell 5 | , parser = new Parser(); 6 | 7 | parser.setEnv('dataStore', {}) 8 | 9 | parser.addCommand('set') 10 | .set('syntax', ['set ']) 11 | .set('logic', function(args, env) { 12 | 13 | env.dataStore[args.key] = args.value 14 | console.log('Set ' + args.key + ' to ' + args.value + '.') 15 | return "stored\n" 16 | }) 17 | 18 | parser.addCommand('get') 19 | .set('syntax', ['get ']) 20 | .set('logic', function(args, env) { 21 | 22 | var value = env.dataStore[args.key] 23 | console.log('Retrieving value of ' + args.key + '.') 24 | return 'value ' + args.key + ' ' + value + "\n" 25 | }) 26 | 27 | var welcome = "connected\n" 28 | 29 | var shell = new Shell(parser) 30 | .set('port', 8888) 31 | .set('welcome', welcome) 32 | .set('prompt', '') 33 | .set('logic', function(shell, system) { 34 | 35 | var keysStored = Object.keys(shell.parser.env.dataStore).length 36 | console.log('Now storing ' + keysStored + ' keys.') 37 | }) 38 | .startServer() 39 | -------------------------------------------------------------------------------- /examples/mud.js: -------------------------------------------------------------------------------- 1 | var mingy = require('../lib/mingy') 2 | , Parser = mingy.Parser 3 | , Command = mingy.Command 4 | , Shell = mingy.Shell 5 | 6 | // define locations in our special game 7 | function Location() {} 8 | var locations = {} 9 | 10 | var hallway = new Location() 11 | hallway.description = "You are in a hallway. From here you can go north." 12 | hallway.exits = {"north": "room"} 13 | locations['hallway'] = hallway 14 | 15 | var room = new Location() 16 | room.description = "You are in a groovy room. From here you can go south." 17 | room.exits = {"south": "hallway"} 18 | locations['room'] = room 19 | 20 | // set up parser and commands 21 | var parser = new Parser() 22 | 23 | parser.addCommand('quit') 24 | .set('syntax', ['quit', 'exit']) 25 | .set('logic', function(args, env, system) { 26 | delete env.users[system.stream.userID] 27 | delete system.stream.userID 28 | return "Goodbye!\n" 29 | }) 30 | 31 | parser.addCommand('help') 32 | .set('syntax', ['help']) 33 | .set('logic', function(args) { 34 | 35 | var output = '' 36 | 37 | output += 'You can use the following commands:\n'+ 38 | ' "look" (or "l") to look around\n'+ 39 | ' "go " to walk in a direction\n'+ 40 | ' "say " to say some things\n'+ 41 | ' "exit" to quit the game\n' 42 | 43 | return output 44 | }) 45 | 46 | parser.addCommand('nick') 47 | .set('syntax', ['nick ']) 48 | .set('logic', function(args, env, system) { 49 | 50 | var output = '' 51 | 52 | env.users[system.stream.userID].name = args['username'] 53 | 54 | output += "You are now known as " + args['username'] + ".\n" 55 | 56 | return output 57 | }) 58 | 59 | parser.addCommand('say') 60 | .set('syntax', ['say ']) 61 | .set('logic', function(args, env, system) { 62 | 63 | var output = "You say your piece.\n" 64 | 65 | var name = env.users[system.stream.userID].name 66 | var location = env.users[system.stream.userID].location 67 | 68 | // broadcast to nearby users 69 | for (var userID in env.users) { 70 | var user = env.users[userID] 71 | if (user.location == location && (userID != system.stream.userID)) { 72 | user.messages.push(name + " says '" + args['message*'] + "'.\n") 73 | } 74 | } 75 | 76 | return output 77 | }) 78 | 79 | parser.addCommand('look') 80 | .set('syntax', ['l', 'look']) 81 | .set('logic', function(args, env, system) { 82 | 83 | var output = '' 84 | 85 | var location = env.users[system.stream.userID].location 86 | 87 | // describe location 88 | output += env.locations[location].description + "\n" 89 | 90 | // describe nearby users 91 | for (var userID in env.users) { 92 | var user = env.users[userID] 93 | if (user.location == location && (userID != system.stream.userID)) { 94 | output += "You see " + user.name + ".\n" 95 | } 96 | } 97 | 98 | return output 99 | }) 100 | 101 | parser.addValidator('direction', function(lexeme) { 102 | 103 | var validDirections = ['north', 'south', 'east', 'west'] 104 | 105 | return { 106 | 'success': (validDirections.indexOf(lexeme) != -1), 107 | 'message': "That's not a direction I understand.\n" 108 | } 109 | }) 110 | 111 | parser.addCommand('go') 112 | .set('syntax', ['go ']) 113 | .set('logic', function(args, env, system) { 114 | 115 | var output = '' 116 | var direction = args.direction 117 | var userLocation = env.users[system.stream.userID].location 118 | 119 | var location = env.locations[userLocation] 120 | 121 | if (location.exits[direction]) { 122 | output += "You go " + direction + ".\n" 123 | env.users[system.stream.userID].location = location.exits[direction] 124 | } 125 | else { 126 | output += "You can't go that way.\n" 127 | } 128 | 129 | return output 130 | }) 131 | 132 | parser.setEnv('locations', locations) 133 | parser.setEnv('location', 'hallway') 134 | parser.setEnv('users', {}) 135 | parser.setEnv('userNumber', 1) 136 | 137 | // begin adventurings! 138 | var welcome = 'Welcome to Low Rent MUD!\n\n'+ 139 | 'Not much happens here, but the stress level is low.\n' 140 | 141 | var shell = new Shell(parser) 142 | .set('port', 8888) 143 | .set('welcome', welcome) 144 | .set('connectLogic', function(shell, system) { 145 | 146 | var guestName = "Guest" + shell.parser.env.userNumber 147 | 148 | // set user properties to default 149 | shell.parser.env.users[system.stream.userID] = { 150 | "name": guestName, 151 | "location": "hallway", 152 | "messages": [] 153 | } 154 | 155 | shell.parser.env.userNumber++ 156 | 157 | return "You are now known as " + guestName + ".\n" 158 | }) 159 | .set('logic', function(shell, system) { 160 | 161 | var output = '' 162 | , message 163 | , messages 164 | 165 | if (shell.parser.env.users[system.stream.userID]) { 166 | // relay anything sent by other users 167 | messages = shell.parser.env.users[system.stream.userID].messages 168 | for (var index in messages) { 169 | output += messages.pop() 170 | } 171 | } 172 | 173 | return output 174 | }) 175 | .startServer() 176 | -------------------------------------------------------------------------------- /examples/shell.js: -------------------------------------------------------------------------------- 1 | var mingy = require('../lib/mingy') 2 | , Parser = mingy.Parser 3 | , Command = mingy.Command 4 | , Shell = mingy.Shell 5 | , fs = require('fs') 6 | 7 | var parser = new Parser() 8 | 9 | parser.addCommand('quit') 10 | .set('syntax', ['quit', 'exit']) 11 | .set('logic', function(args) { 12 | process.exit(0) 13 | }) 14 | 15 | parser.addCommand('ls') 16 | .set('syntax', ['ls']) 17 | .set('logic', function(args) { 18 | var output = '' 19 | var directory = fs.readdirSync(process.cwd()) 20 | for (var index in directory) { 21 | output += directory[index] + "\n" 22 | } 23 | return output 24 | }) 25 | 26 | parser.addCommand('cd') 27 | .set('syntax', ['cd ']) 28 | .set('logic', function(args) { 29 | var output = '' 30 | try { 31 | process.chdir(process.cwd() + '/' + args['path']) 32 | output += "Directory changed.\n" 33 | } catch(e) { 34 | output += "Bad directory.\n" 35 | } 36 | return output 37 | }) 38 | 39 | var welcome = 'Welcome to Sullen Shell: the shell with few aspirations.\n'+ 40 | 'Available commands: "ls", "cd", "quit", or "exit".\n\n' 41 | 42 | var shell = new Shell(parser) 43 | .set('welcome', welcome) 44 | .set('prompt', '$ ') 45 | .start() 46 | -------------------------------------------------------------------------------- /examples/web/public/mingy_times.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcantelon/node-mingy/b4fa98dfdebd568d32baadc01253b12b4fcdbc49/examples/web/public/mingy_times.png -------------------------------------------------------------------------------- /examples/web/web.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect') 2 | , mingy = require('../lib/mingy') 3 | , Parser = mingy.Parser 4 | , Command = mingy.Command 5 | , ConnectMiddleware = mingy.ConnectMiddleware 6 | 7 | // in-memory store of stories 8 | var stories = { 9 | "earthquake": { 10 | "title": "Horrid Earthquake", 11 | "body": "The earthquake was depressing." 12 | }, 13 | "disease": { 14 | "title": "That Disease isn't such a Problem Anymore", 15 | "body": "The disease was cured." 16 | } 17 | } 18 | 19 | var parser = new Parser() 20 | parser.setEnv('news', stories) 21 | 22 | // define commands (which function as routes) 23 | parser.addCommand('home') 24 | .set('syntax', ['']) 25 | .set('logic', function(args, env, system) { 26 | 27 | var output = '' 28 | 29 | if (args.method == 'GET') { 30 | 31 | output += '
'+ 32 | '

Hello World!

'+ 33 | '

Check out the news.

'+ 34 | '

Add Story

'+ 35 | '
'+ 36 | '

Slug:

'+ 37 | '

Title:

'+ 38 | '

Body:

'+ 39 | '

'+ 40 | '
' 41 | } 42 | else if(args.method == 'POST') { 43 | 44 | system.getPost(system.request, function(storyData) { 45 | 46 | env.news[storyData.slug] = { 47 | "title": storyData.title, 48 | "body": storyData.body 49 | } 50 | 51 | output += 'Story added! Check out the news.' 52 | 53 | system.callback(output) 54 | }) 55 | } 56 | 57 | return output 58 | }) 59 | 60 | parser.addValidator('storyExists', function(lexeme, env) { 61 | return { 62 | 'success': env.news[lexeme], 63 | 'message': "Story not found.\n" 64 | } 65 | }) 66 | 67 | parser.addCommand('news') 68 | .set('syntax', ['GET news', 'GET news ']) 69 | .set('logic', function(args, env, system) { 70 | 71 | var output = '' 72 | 73 | if (args.story) { 74 | 75 | if (env.news[args.story]) { 76 | output += "

" + env.news[args.story].title + "

" 77 | output += "

" + env.news[args.story].body + "

" 78 | } 79 | } 80 | else { 81 | 82 | output += "

News!

" 83 | 84 | for (var story in env.news) { 85 | output += "" + env.news[story].title + "
" 86 | } 87 | } 88 | 89 | return output 90 | }) 91 | 92 | var mingyRouter = new ConnectMiddleware(parser) 93 | 94 | connect.createServer( 95 | connect.static(__dirname + '/public'), 96 | mingyRouter 97 | ) 98 | .listen(8888) 99 | 100 | console.log("Server started at port 8888...") 101 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mingy 3 | * Copyright(c) 2011 Mike Cantelon 4 | * MIT Licensed 5 | */ 6 | 7 | var net = require('net') 8 | , Command = require('./command').Command 9 | 10 | exports.Client = function Shell(parser) { 11 | 12 | this.parser = parser || undefined 13 | this.host = '127.0.0.1' 14 | this.port = 8000 15 | this.parseErrorMessage = "Bad command or command usage.\n" 16 | 17 | return this 18 | } 19 | 20 | exports.Client.prototype = { 21 | 22 | set: function(property, value) { 23 | this[property] = value 24 | return this 25 | }, 26 | 27 | start: function(lexemes) { 28 | 29 | var parser = this.parser 30 | 31 | console.log('Connecting to host ' + this.host +' port ' + this.port + '...') 32 | 33 | var conn = net.createConnection(this.port, this.host) 34 | this.conn = conn 35 | 36 | conn.on('data', function(data) { 37 | 38 | data = data.toString() 39 | 40 | var output = parser.parse(data, false, {"server": conn}) 41 | 42 | if (output) { 43 | console.log(output) 44 | } 45 | }) 46 | 47 | if (lexemes) { 48 | var output = parser.parseLexemes(lexemes, false, {"server": conn}) 49 | 50 | if (output) { 51 | console.log(output) 52 | } 53 | } 54 | 55 | return this 56 | }, 57 | 58 | parse: function(input) { 59 | 60 | return this.parser.parse(input, false, {"server": this.conn}) 61 | }, 62 | 63 | close: function() { 64 | this.conn.end(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mingy 3 | * Copyright(c) 2011 Mike Cantelon 4 | * MIT Licensed 5 | */ 6 | 7 | exports.Command = function Command(name) { 8 | 9 | this.name = name || '' 10 | this.caseSensitive = false 11 | } 12 | 13 | exports.Command.prototype = { 14 | 15 | set: function(property, value) { 16 | this[property] = value 17 | return this 18 | }, 19 | 20 | // if test mode is true, return true if lexemes will trigger this command 21 | // if test mode is falsey, execute command logic 22 | try: function(validators, env, lexemes, testMode, system) { 23 | 24 | var args = false 25 | , commandResult 26 | 27 | // try each syntax form 28 | if (this.syntax) { 29 | for(var index in this.syntax) { 30 | 31 | var syntaxLexemes = this.syntax[index].split(' ') 32 | 33 | //if (syntaxLexemes.length == lexemes.length) { 34 | 35 | // test submitted lexemes against this syntax 36 | var valid = this.trySyntaxKeywords(syntaxLexemes, lexemes) 37 | 38 | // valid syntax pattern found... now see arg lexemes are proper 39 | if (valid) { 40 | 41 | // if the last syntax lexeme ends with an *, amalgamate execess 42 | // submitted lexemes to the submitted lexemes at the same 43 | // position as the last syntax lexeme 44 | var lastSyntaxLexemeIndex = syntaxLexemes.length - 1 45 | if (syntaxLexemes[lastSyntaxLexemeIndex].match(/\*>$/)) { 46 | 47 | lexemes[lastSyntaxLexemeIndex] = 48 | lexemes 49 | .slice(lastSyntaxLexemeIndex, lexemes.length) 50 | .join(' ') 51 | } 52 | 53 | // see if the arguments given to the command are valid 54 | var result = this.determineCommandArguments(validators, env, syntaxLexemes, lexemes) 55 | if (result.success === false) { 56 | if (result.message) { 57 | return result.message 58 | } 59 | 60 | valid = false 61 | } 62 | } 63 | 64 | if (valid) { 65 | 66 | commandResult = '' 67 | 68 | // a bunch of condition stuff goes here 69 | 70 | if (testMode) { 71 | return true 72 | } 73 | 74 | // do eval 75 | commandResult += this.logic(result['args'], env, system) 76 | 77 | return commandResult 78 | } 79 | //} 80 | } 81 | } 82 | 83 | return args 84 | }, 85 | 86 | type: function(object) { 87 | 88 | return object.constructor.name 89 | }, 90 | 91 | trySyntaxKeywords: function(syntaxLexemes, submittedLexemes) { 92 | 93 | var valid = true 94 | , lexemeToTest = 0 95 | , secondToLast 96 | 97 | for (var index in syntaxLexemes) { 98 | 99 | var syntaxLexeme = syntaxLexemes[index] 100 | var submittedLexeme = submittedLexemes[lexemeToTest] 101 | 102 | if (!this.caseSensitive) { 103 | syntaxLexeme = (typeof syntaxLexeme == 'string') 104 | ? syntaxLexeme.toLowerCase() 105 | : syntaxLexeme 106 | submittedLexeme = (typeof submittedLexeme == 'string') 107 | ? submittedLexeme.toLowerCase() 108 | : submittedLexeme 109 | } 110 | 111 | // if lexeme doesn't reference an object, test as a keyword 112 | if (syntaxLexeme[0] != '<' && (syntaxLexeme != submittedLexeme)) { 113 | valid = false 114 | } 115 | 116 | lexemeToTest++ 117 | } 118 | 119 | secondToLast = syntaxLexemes[index][(syntaxLexemes[index].length - 2)] 120 | 121 | // if final syntax lexeme didn't end with a * character and length of syntax 122 | // vs submitted is different then invalid 123 | if (secondToLast != '*' && syntaxLexemes.length != submittedLexemes.length) { 124 | valid = false 125 | } 126 | 127 | return valid 128 | }, 129 | 130 | trimArgDelimiters: function(arg) { 131 | 132 | return arg.slice(1, arg.length-1) 133 | }, 134 | 135 | determineCommandArguments: function(validators, env, syntaxLexemes, inputLexemes) { 136 | 137 | var lexemeToTest = 0 138 | 139 | var lexemes = inputLexemes 140 | 141 | var referenceData, referenceType, referenceName 142 | 143 | var success = true 144 | var arg = {} 145 | 146 | for (var index in syntaxLexemes) { 147 | 148 | var lexeme = syntaxLexemes[index] 149 | 150 | if (lexeme[0] == '<') { 151 | 152 | // determine reference type 153 | referenceData = this.trimArgDelimiters(lexeme).split(':') 154 | referenceType = referenceData[0] 155 | 156 | // trim "<" and ">" from reference to determine reference type 157 | referenceName = (referenceData[1]) 158 | ? referenceData[1] 159 | : referenceType 160 | 161 | // if there's a validator, use it to test lexeme 162 | if (validators[referenceType]) { 163 | 164 | // need to return an object with success, value, and message 165 | // success determines whether validation was successful 166 | // value allows transformation of the lexeme 167 | // message allows a message to be passed back??? 168 | var result = validators[referenceType](lexemes[lexemeToTest], env) 169 | if (result.success) { 170 | arg[referenceName] = (result.value === undefined) 171 | ? lexemes[lexemeToTest] 172 | : result.value 173 | } 174 | else { 175 | // if error is set to a string this message will be returned to the user 176 | if (result.message) { 177 | return {'success': false, 'message': result.message} 178 | } 179 | } 180 | } 181 | else { 182 | 183 | if (referenceData.length == 1 184 | || typeof lexemes[lexemeToTest] == referenceType 185 | ) { 186 | arg[referenceName] = lexemes[lexemeToTest] 187 | } 188 | else { 189 | // if error is simply true a generic error message will be returned to the user 190 | success = false 191 | } 192 | } 193 | } 194 | 195 | lexemeToTest += 1 196 | } 197 | 198 | return { 199 | 'success': success, 200 | 'args': arg 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /lib/connectMiddleware.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mingy 3 | * Copyright(c) 2011 Mike Cantelon 4 | * MIT Licensed 5 | */ 6 | 7 | var connect = require('connect') 8 | , url = require('url') 9 | , querystring = require('querystring') 10 | 11 | exports.ConnectMiddleware = function WebServer(parser) { 12 | 13 | this.parser = parser || undefined 14 | this.parseErrorMessage = "Bad command or command usage.\n" 15 | this.createMiddleware() 16 | return this.middleware 17 | } 18 | 19 | exports.ConnectMiddleware.prototype = { 20 | 21 | createMiddleware: function() { 22 | 23 | var parser = this.parser 24 | var web = this 25 | 26 | this.middleware = function (request, response, next) { 27 | 28 | // POSTs get data asynchronously so use callback 29 | var callback = false 30 | if (request.method == 'POST') { 31 | 32 | callback = function(output) { 33 | web.send(response, output) 34 | } 35 | } 36 | 37 | var system = { 38 | "request": request, 39 | "response": response, 40 | "send": web.send, 41 | "getPost": web.getPost 42 | } 43 | 44 | // create lexemes from request method and path 45 | var lexemes = web.requestToLexemes(request) 46 | 47 | // if the lexemes correspond to any commands, parse them 48 | if (parser.validCommands(lexemes).length) { 49 | 50 | var output = parser.parseLexemes( 51 | lexemes, 52 | callback, 53 | system 54 | ) 55 | } 56 | else { 57 | return next() 58 | } 59 | 60 | // allow POST requests to handle their own output 61 | if (!callback) { 62 | web.send(response, output) 63 | } 64 | } 65 | 66 | }, 67 | 68 | requestToLexemes: function(request) { 69 | // extract URL's path 70 | var path = url.parse(request.url).pathname 71 | 72 | // convert path to lexemes 73 | var lexemes = path.split('/').slice(1) 74 | 75 | // neutralize trailing slashes 76 | if(lexemes[lexemes.length - 1] == '') { 77 | lexemes.pop() 78 | } 79 | 80 | // prepend HTTP method to lexemes 81 | lexemes.unshift(request.method) 82 | 83 | return lexemes 84 | }, 85 | 86 | send: function(response, output, code) { 87 | 88 | code = code || 200 89 | 90 | response.writeHead(code, {'Content-Type': 'text/html'}) 91 | response.end(output) 92 | }, 93 | 94 | getPost: function(request, callback) { 95 | 96 | var dataRaw = '' 97 | 98 | // receive chunks of data 99 | request.on('data', function(data) { 100 | dataRaw += data.toString() 101 | }) 102 | 103 | // process data 104 | request.on('end', function() { 105 | 106 | callback(querystring.parse(dataRaw)) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/mingy.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mingy 3 | * Copyright(c) 2011 Mike Cantelon 4 | * MIT Licensed 5 | */ 6 | 7 | exports.Parser = require('./parser').Parser 8 | exports.Command = require('./command').Command 9 | exports.Shell = require('./shell').Shell 10 | exports.Client = require('./client').Client 11 | exports.ConnectMiddleware = require('./connectMiddleware').ConnectMiddleware 12 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mingy 3 | * Copyright(c) 2011 Mike Cantelon 4 | * MIT Licensed 5 | */ 6 | 7 | var util = require('util') 8 | , url = require('url') 9 | , Command = require('./command').Command 10 | 11 | exports.Parser = function Parser(commands) { 12 | 13 | this.loadCommands(commands) 14 | this.validators = {} 15 | this.env = {} 16 | this.lexemeTransforms = [] 17 | } 18 | 19 | exports.Parser.prototype = { 20 | 21 | setEnv: function(property, value) { 22 | this.env[property] = value 23 | }, 24 | 25 | loadCommands: function(commands) { 26 | 27 | if (commands) { 28 | 29 | if(commands.constructor.name == 'Array') { 30 | 31 | this.commands = commands 32 | } 33 | else if(commands.constructor.name == 'Object') { 34 | 35 | this.commands = {} 36 | 37 | for (var name in commands) { 38 | this.addCommand(name) 39 | .set('syntax', commands[name].syntax) 40 | .set('logic', commands[name].logic) 41 | } 42 | } 43 | else { 44 | 45 | throw "Commands given to parser must be an array or hash (object)." 46 | } 47 | } 48 | else { 49 | 50 | this.commands = {} 51 | } 52 | }, 53 | 54 | addCommand: function(name) { 55 | 56 | // throw error if name already taken 57 | var command = new Command('name') 58 | this.commands[name] = command 59 | return command 60 | }, 61 | 62 | addValidator: function(name, logic) { 63 | this.validators[name] = logic 64 | }, 65 | 66 | addLexemeTransform: function(logic) { 67 | this.lexemeTransforms.push(logic) 68 | }, 69 | 70 | parse: function(input, callback, system) { 71 | 72 | input = this.cleanInput(input) 73 | var lexemes = input.split(' ') 74 | return this.parseLexemes(lexemes, callback, system) 75 | }, 76 | 77 | cleanInput: function(input) { 78 | 79 | // server shell sends extra junk 80 | input = input.replace("\r\n", "\n") 81 | 82 | // remove trailing newline 83 | if (input.slice(-1) == "\n") { 84 | input = input.slice(0, input.length - 1) 85 | } 86 | 87 | // remove redundant spaces 88 | while (input.indexOf(' ') != -1) { 89 | input = input.replace(' ', ' ') 90 | } 91 | 92 | return input 93 | }, 94 | 95 | validCommands: function(lexemes) { 96 | 97 | var commands = [] 98 | 99 | // cycle through commands looking for syntax match 100 | for (var index in this.commands) { 101 | 102 | var command = this.commands[index] 103 | 104 | // we clone lexemes because if the last syntax lexeme has a wildcard the 105 | // submitted lexeme corresponding to the last syntax lexeme ends up 106 | // getting subsequent submitted lexemes added to it 107 | if (command.try(this.validators, this.env, this.clone(lexemes), true)) { 108 | commands.push(command) 109 | } 110 | } 111 | 112 | return commands 113 | }, 114 | 115 | parseLexemes: function(lexemes, callback, system) { 116 | 117 | var output = '' 118 | 119 | // cycle through plugins 120 | for (var index in this.lexemeTransforms) { 121 | 122 | lexemes = this.lexemeTransforms[index](lexemes, this.env) 123 | } 124 | 125 | // cycle through commands looking for syntax match 126 | var validCommands = this.validCommands(lexemes) 127 | for (var index in validCommands) { 128 | 129 | var command = validCommands[index] 130 | 131 | // command condition jazz goes here 132 | if (1) { 133 | 134 | system = system || {} 135 | system.callback = callback 136 | result = command.try(this.validators, this.env, lexemes, false, system) 137 | 138 | if (result) { 139 | return output + result 140 | } 141 | } 142 | else if(output) { 143 | return output 144 | } 145 | } 146 | }, 147 | 148 | clone: function(obj) { 149 | var newObj = (obj instanceof Array) ? [] : {}; 150 | for (i in obj) { 151 | if (i == 'clone') continue; 152 | if (obj[i] && typeof obj[i] == "object") { 153 | newObj[i] = this.clone(obj[i]); 154 | } else newObj[i] = obj[i] 155 | } return newObj; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/shell.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mingy 3 | * Copyright(c) 2011 Mike Cantelon 4 | * MIT Licensed 5 | */ 6 | 7 | var util = require('util') 8 | , net = require('net') 9 | , Command = require('./command').Command 10 | 11 | exports.Shell = function Shell(parser) { 12 | 13 | this.parser = parser || undefined 14 | this.prompt = '> ' 15 | this.port = 8000 16 | this.parseErrorMessage = "Bad command or command usage.\n" 17 | 18 | // what do modes do? 19 | this.modes = { 20 | 'default': this.defaultMode 21 | } 22 | this.mode = 'default' 23 | 24 | return this 25 | } 26 | 27 | exports.Shell.prototype = { 28 | 29 | set: function(property, value) { 30 | this[property] = value 31 | return this 32 | }, 33 | 34 | setMode: function(mode, logic) { 35 | this.modes[mode] = logic 36 | return this 37 | }, 38 | 39 | streamActive: function(stream) { 40 | 41 | return !stream.type || stream.userID 42 | }, 43 | 44 | defaultMode: function(shell, data, system) { 45 | 46 | if (shell.streamActive(system.stream)) { 47 | var output = shell.parser.parse(data, false, system) 48 | output = output || shell.parseErrorMessage 49 | 50 | return output 51 | } 52 | }, 53 | 54 | start: function() { 55 | 56 | var parser = this.parser 57 | var shell = this 58 | 59 | util.print(shell.welcome) 60 | util.print(shell.prompt) 61 | 62 | var stdin = process.openStdin() 63 | 64 | shell.main(parser, shell, stdin) 65 | }, 66 | 67 | startServer: function() { 68 | 69 | var parser = this.parser 70 | var shell = this 71 | 72 | var server = net.createServer(function (stream) { 73 | shell.main(parser, shell, stream, true) 74 | }) 75 | 76 | server.listen(this.port) 77 | }, 78 | 79 | main: function(parser, shell, stream, isStream) { 80 | 81 | stream.setEncoding('utf8') 82 | 83 | stream.userID = stream.remoteAddress + '|' + stream.remotePort 84 | 85 | if (shell.welcome) { 86 | stream.write(shell.welcome) 87 | } 88 | if (shell.connectLogic) { 89 | var connectOutput = shell.connectLogic(shell, {"stream": stream}) 90 | if (connectOutput) { 91 | stream.write(connectOutput) 92 | } 93 | } 94 | if (shell.prompt && isStream) { 95 | stream.write(shell.prompt) 96 | } 97 | 98 | stream.on('data', function(data) { 99 | 100 | if (shell.streamActive(stream)) { 101 | 102 | var output = shell.execute(parser, shell, data, stream) 103 | 104 | // don't show prompt if stream no longer active 105 | if (shell.streamActive(stream)) { 106 | output += shell.prompt 107 | } 108 | 109 | if (output) { 110 | if (isStream) { 111 | stream.write(output) 112 | } 113 | else { 114 | util.print(output) 115 | } 116 | } 117 | } 118 | }) 119 | 120 | stream.on('end', function() { 121 | stream.end() 122 | }) 123 | }, 124 | 125 | execute: function(parser, shell, data, stream) { 126 | 127 | var output = '' 128 | 129 | if (shell.mode && shell.modes[shell.mode]) { 130 | var modeOutput = shell.modes[shell.mode](shell, data, {"stream": stream}) 131 | output += (modeOutput) ? modeOutput : '' 132 | } 133 | 134 | if (shell.logic) { 135 | var postCommandOutput = shell.logic(shell, {"stream": stream}) 136 | output += (postCommandOutput) ? postCommandOutput : '' 137 | } 138 | 139 | return output 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mingy", 3 | "version": "0.2.7", 4 | "description": "Cheap parsing for your CLI tool and adventure game needs.", 5 | "tags" : ["cli", "parsing", "adventure"], 6 | "author" : "Mike Cantelon ", 7 | "contributors" : [], 8 | "dependencies" : { 9 | "optimist": ">= 0.1.6", 10 | "connect": "~2.13.1" 11 | }, 12 | "main": "./lib/mingy", 13 | "directories" : { 14 | "lib": "./lib" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url" : "http://github.com/mcantelon/node-mingy.git" 19 | }, 20 | "engines": { 21 | "node": ">= 0.3.0" 22 | }, 23 | "bugs" : { 24 | "web" : "http://github.com/mcantelon/node-mingy/issues" 25 | }, 26 | "licenses" : [{ 27 | "type" : "MIT", 28 | "url" : "http://github.com/mcantelon/node-deja/blob/master/LICENSE" 29 | }] 30 | } 31 | -------------------------------------------------------------------------------- /test/mingy.test.js: -------------------------------------------------------------------------------- 1 | var mingy = require('../lib/mingy') 2 | , Parser = mingy.Parser 3 | , Command = mingy.Command 4 | , assert = require('assert') 5 | , should = require('should') 6 | 7 | module.exports = { 8 | 9 | // basic command object setup 10 | 'command configuration': function() { 11 | var command = new Command('look') 12 | command.set('syntax', ['look ']) 13 | command.set('logic', function(args) { 14 | if (!args || !args['string']) { 15 | return 'Nothing to look at.' 16 | } 17 | else { 18 | return 'You look at ' + args['string'] + '.' 19 | } 20 | }) 21 | command.name.should.equal('look') 22 | command.syntax.length.should.equal(1) 23 | command.logic().should.equal('Nothing to look at.') 24 | }, 25 | 26 | // named argument 27 | 'command with named, untyped param': function() { 28 | var command = new Command('look') 29 | command.set('syntax', ['look ']) 30 | command.set('logic', function(args) { 31 | return 'You look at ' + args['thing'] + '.' 32 | }) 33 | 34 | var parser = new Parser([command]) 35 | var output = parser.parseLexemes(['look', 'cat']) 36 | output.should.equal('You look at cat.') 37 | }, 38 | 39 | // name argument and unrelated validator 40 | 'command with named, untyped param and validator handler set': function() { 41 | var command = new Command('look') 42 | command.set('syntax', ['look ']) 43 | command.set('logic', function(args) { 44 | return 'You look at ' + args['thing'] + '.' 45 | }) 46 | 47 | var parser = new Parser([command]) 48 | parser.addValidator('animal', function(lexeme) { 49 | throw "This should not have fired!" 50 | return lexeme 51 | }) 52 | var output = parser.parseLexemes(['look', 'cat']) 53 | output.should.equal('You look at cat.') 54 | }, 55 | 56 | // argument validator 57 | 'simple parse with named, validated param': function() { 58 | var command = new Command('look') 59 | command.set('syntax', ['look ']) 60 | command.set('logic', function(args) { 61 | if (!args || !args['thing']) { 62 | return 'Nothing to look at.' 63 | } 64 | else { 65 | return 'You look at ' + args['thing'] + '.' 66 | } 67 | }) 68 | 69 | var parser = new Parser([command]) 70 | parser.addValidator('is_cat', function(lexeme) { 71 | if (lexeme == 'cat') { 72 | return { 73 | "success": true, 74 | "value": lexeme 75 | } 76 | } 77 | else { 78 | return { 79 | "success": false, 80 | "message": "That is not a cat." 81 | } 82 | } 83 | }) 84 | 85 | var output = parser.parseLexemes(['look', 'cat']) 86 | output.should.equal('You look at cat.') 87 | 88 | var output = parser.parseLexemes(['look', 9]) 89 | output.should.equal('That is not a cat.') 90 | }, 91 | 92 | // typed name with no type hanlder 93 | 'simple parse with named, typed param': function() { 94 | var command = new Command('look') 95 | command.set('syntax', ['look ']) 96 | command.set('logic', function(args) { 97 | if (!args || !args['thing']) { 98 | return 'Nothing to look at.' 99 | } 100 | else { 101 | return 'You look at ' + args['thing'] + '.' 102 | } 103 | }) 104 | 105 | var parser = new Parser([command]) 106 | 107 | // arg is a string, so it should parse 108 | var output = parser.parseLexemes(['look', 'cat']) 109 | output.should.equal('You look at cat.') 110 | 111 | // arg is a number, so it shouldn't parse 112 | var output = parser.parseLexemes(['look', 9]) 113 | should.equal(undefined, output) 114 | }, 115 | 116 | // multiple command forms 117 | 'multiple command forms': function() { 118 | var command = new Command('look') 119 | command.set('syntax', [ 120 | 'look ', 121 | ' examine' 122 | ]) 123 | command.set('logic', function(args) { 124 | if (!args || !args['thing']) { 125 | return 'Nothing to look at.' 126 | } 127 | else { 128 | return 'You look at ' + args['thing'] + '.' 129 | } 130 | }) 131 | 132 | var parser = new Parser([command]) 133 | 134 | // try first command form 135 | var output = parser.parseLexemes(['look', 'cat']) 136 | output.should.equal('You look at cat.') 137 | 138 | // try second command form 139 | var output = parser.parseLexemes(['cat', 'examine']) 140 | output.should.equal('You look at cat.') 141 | }, 142 | 143 | // callback from command 144 | 'callback from command': function() { 145 | var parser = new Parser() 146 | parser.addCommand('test') 147 | .set('syntax', ['test']) 148 | .set('logic', function(args, env, system) { 149 | system.callback('jazzhands') 150 | }) 151 | var success = false 152 | parser.parse('test', function(output) { 153 | output.should.equal('jazzhands') 154 | success = true 155 | }) 156 | while(!success) { 157 | process.nextText() 158 | } 159 | }, 160 | 161 | // load commands from hash 162 | 'load commands from hash': function() { 163 | 164 | var commands = { 165 | "test1": { 166 | "syntax": ['test one'], 167 | "logic": function(args) { 168 | return "got test one" 169 | } 170 | }, 171 | "test2": { 172 | "syntax": ['test two'], 173 | "logic": function(args) { 174 | return "got test two" 175 | } 176 | } 177 | } 178 | 179 | var parser = new Parser(commands) 180 | var first_test = parser.parse('test one') 181 | first_test.should.equal('got test one') 182 | var second_test = parser.parse('test two') 183 | second_test.should.equal('got test two') 184 | }, 185 | 186 | // test out parser lexeme transformation functionality 187 | 'parser lexeme transform': function() { 188 | 189 | var parser = new Parser() 190 | parser.setEnv('fullNames', {"monkey": "golden monkey"}) 191 | 192 | parser.addCommand('touch') 193 | .set('syntax', ['touch ']) 194 | .set('logic', function(args) { 195 | 196 | return "You touch the " + args.thing + "." 197 | }) 198 | 199 | parser.addLexemeTransform(function(lexemes, env) { 200 | 201 | for (var lexemeIndex in lexemes) { 202 | if (env.fullNames[(lexemes[lexemeIndex])]) { 203 | lexemes[lexemeIndex] = env.fullNames[(lexemes[lexemeIndex])] 204 | } 205 | } 206 | 207 | return lexemes 208 | }) 209 | 210 | var output = parser.parse('touch monkey') 211 | output.should.equal('You touch the golden monkey.') 212 | } 213 | } 214 | --------------------------------------------------------------------------------