├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── example │ ├── _argv │ ├── argv │ ├── argv.1 │ ├── argv.js │ ├── argv.json │ ├── argv.md │ └── argv.txt ├── readme.md └── readme │ ├── .gitignore │ ├── badges.md │ ├── example-files.md │ ├── example.md │ ├── guide.md │ ├── help.md │ ├── install.md │ ├── license.md │ └── links.md ├── index.js ├── lib ├── compiler.js ├── format.js ├── optparse.js ├── parser.js ├── render │ ├── help.js │ ├── json.js │ ├── man.js │ └── zsh.js ├── state.js └── synopsis.js ├── mkdoc.js ├── package.json └── test ├── fixtures ├── bad-name.md ├── command.md ├── commands.md ├── completion │ ├── _notes │ ├── notes │ ├── notes-add.md │ ├── notes-list-bug.md │ ├── notes-list.md │ ├── notes.1 │ ├── notes.json │ ├── notes.md │ └── notes.txt ├── description.md ├── duplicate-key-sparse.md ├── duplicate-key.md ├── duplicate-name.md ├── empty.md ├── flag.md ├── leading-content.md ├── mock.txt ├── multiple-option.md ├── name-not-first.md ├── name.md ├── named-flag.md ├── names.md ├── no-specification.md ├── no-summary.md ├── option-type-value.md ├── option-type.md ├── option-value.md ├── option.md ├── program.md ├── required-option.md ├── section.md ├── subcommand-list-notes.md ├── subcommand-list.md ├── subcommand.md ├── synopsis-exapansion.md ├── synopsis-illegal.md ├── synopsis-name.md └── synopsis.md ├── mocha.opts ├── spec ├── camelcase.js ├── compile.js ├── dest.js ├── error │ ├── bad-name.js │ ├── duplicate-key-sparse.js │ ├── duplicate-key.js │ ├── duplicate-name.js │ ├── leading-content.js │ ├── name-not-first.js │ ├── no-specification.js │ ├── no-summary.js │ └── synopsis-illegal.js ├── format.js ├── help │ ├── cmd-style-w-header.js │ ├── cmd-style.js │ ├── command.js │ ├── description.js │ ├── empty.js │ ├── list-style.js │ ├── name.js │ ├── names.js │ ├── option-value.js │ ├── option.js │ ├── program.js │ ├── section.js │ ├── synopsis.js │ └── usage-style.js ├── json │ ├── command.js │ ├── commands.js │ ├── description.js │ ├── empty.js │ ├── flag.js │ ├── multiple-option.js │ ├── name.js │ ├── named-flag.js │ ├── names.js │ ├── option-type-value.js │ ├── option-type.js │ ├── option-value.js │ ├── option.js │ ├── options.js │ ├── required-option.js │ ├── section.js │ ├── subcommand.js │ └── synopsis.js ├── load.js ├── man │ └── name.js ├── optparse.js ├── src.js └── state.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.bak 3 | *.log 4 | node_modules 5 | coverage 6 | target 7 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "fileExtensions": [".js", "jscs"], 3 | "requireSemicolons": false, 4 | "requireParenthesesAroundIIFE": true, 5 | "maximumLineLength": 80, 6 | "validateLineBreaks": "LF", 7 | "validateIndentation": 2, 8 | "requireMultipleVarDecl": false, 9 | "disallowTrailingComma": true, 10 | "disallowSpacesInConditionalExpression": false, 11 | "disallowSpaceAfterKeywords": false, 12 | "disallowSpaceBeforeBlockStatements": false, 13 | "disallowSpacesInsideObjectBrackets": null, 14 | "excludeFiles": [ 15 | "node_modules", 16 | "coverage" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "laxcomma" : true, 3 | "laxbreak" : true, 4 | "node" : true, 5 | "curly" : true, 6 | "bitwise" : false, 7 | "undef" : true, 8 | "eqeqeq" : true, 9 | "noarg" : true, 10 | "mocha" : true, 11 | "unused" : true, 12 | "asi" : true, 13 | "validthis" : true 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | branches: 4 | except: 5 | - develop 6 | script: npm run cover 7 | node_js: 8 | - '4.1' 9 | - '4.0' 10 | - '0.12' 11 | - stable 12 | after_success: 13 | - npm run coveralls 14 | deploy: 15 | provider: npm 16 | email: freeformsystems@gmail.com 17 | api_key: 18 | secure: bBP+6gqU5DYOpf4x/u/dVDfUkbU4KL4a4m9hcWbrrfyPP6ytNkCsqlE4ZM+tcDx/TKxT7yHmMJHGXa3Wn8PclyJZSvSLXw3pte3k9V4QZgAVvLZqvSWMf2FR1QzUvqI450GcwwP8HvTxkS46RgClVYUEaVSllWIQcoIk0dIEux9MMfxH+LXubExpnQLld2xMWWhzRrXK0B1HbizTg0FMfMOHVcG1icC6/Jp4YQiF5wUVtdo0IAXi0a+QZ2KedAXxQRSAxlXWaCYDaBZwxC3haE4xzk+83IEXa90usd+lZPFtJlgG6OVGD34d3v12e4dDezCLuRjhVcskF/TjqGY3nVa9ZnPOwmHPePK+Bw5H4T826VLimGZBSxcpzAvTgT4jvjiR8Sj2dr4BQ7j/d7r4l5/8oJmZqPqHWEs5K6ScAXVwuKPaiSdRCA/joIyjNNUekvw1jGQNQ4zOFG3LThP0MsWt/0R11fVgy3uPTAOCEyrqje0kAdCxQ4J1ptUVzuI1v17LBTLbwlDJaQEHPQS4HzgnOklvRJMExS7cuWIiubPG63f8nuT8sk/kxmJ0uM4wcL/BOa2oEIYAcIC7xtthwdEe5e8lRQCoLN+l/Cc8Wc8NuJtI8QjKrhss5xRwo+5R4QnWSswGg0RYURb1JAUSNHU2ypBXuMkn//pc3CG3cng= 19 | on: 20 | tags: true 21 | repo: mkdoc/mkcli 22 | node: '4.1' 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 muji and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | NO WARRANTY OR LIABILITY 23 | 24 | YOU EXPRESSLY AGREE THAT DOWNLOADING THE SOFTWARE AND ANY USE OF THE 25 | SOFTWARE IS AT YOUR OWN RISK. NO WARRANTY, REPRESENTATION, CONDITION, 26 | UNDERTAKING OR TERM - EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE - 27 | INCLUDING BUT NOT LIMITED TO THE CONDITION, QUALITY, DURABILITY, 28 | PERFORMANCE, ACCURACY, STABILITY, RELIABILITY, NON-INFRINGEMENT, 29 | MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE OR USE OF THE 30 | SOFTWARE IS GIVEN OR ASSUMED BY FREEFORM SYSTEMS LTD. ALL SUCH 31 | WARRANTIES, REPRESENTATIONS, CONDITIONS, UNDERTAKINGS AND TERMS ARE 32 | HEREBY EXCLUDED. FREEFORM SYSTEMS LTD MAKES NO WARRANTY THAT THE 33 | SOFTWARE WILL MEET YOUR REQUIREMENTS, OR THAT IT WILL BE UNINTERRUPTED, 34 | TIMELY, SECURE, OR ERROR FREE; IN NO EVENT SHALL FREEFORM SYSTEMS LTD 35 | BE LIABLE TO ANY PARTY FOR ANY DAMAGES INCLUDING WITHOUT LIMITATION, 36 | ANY DIRECT, INDIRECT, SPECIAL, PUNITIVE, INCIDENTAL OR CONSEQUENTIAL 37 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, DAMAGES FOR LOSS OF BUSINESS 38 | PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR INFORMATION, LOSS 39 | OF PROFITS AND SAVINGS AND THE LIKE), OR ANY OTHER DAMAGES ARISING - 40 | IN ANY WAY, SHAPE OR FORM - OUT OF THE AVAILABILITY, USE, RELIANCE ON, 41 | INABILITY TO UTILIZE OR IMPROPER USE OF THE SOFTWARE EVEN IF FREEFORM 42 | SYSTEMS LTD SHALL HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, 43 | AND REGARDLESS OF THE FORM OF ACTION, WHETHER IN CONTRACT, TORT, OR 44 | OTHERWISE. BECAUSE SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR 45 | LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, THE ABOVE EXCLUSIONS 46 | OF INCIDENTAL AND CONSEQUENTIAL DAMAGES MAY NOT APPLY TO YOU. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Command Line Interface 2 | 3 | [![Build Status](https://travis-ci.org/mkdoc/mkcli.svg?v=3)](https://travis-ci.org/mkdoc/mkcli) 4 | [![npm version](http://img.shields.io/npm/v/mkcli.svg?v=3)](https://npmjs.org/package/mkcli) 5 | [![Coverage Status](https://coveralls.io/repos/mkdoc/mkcli/badge.svg?branch=master&service=github&v=3)](https://coveralls.io/github/mkdoc/mkcli?branch=master) 6 | 7 | > Define command line interfaces as markdown 8 | 9 | Describe a command line interface as an easy to read man-style markdown document and compile it to a program descriptor; the JSON program descriptor can then be used by the program implementation to parse and validate arguments. 10 | 11 | The markdown program definitions can be converted to man pages, help files and shell completion scripts. 12 | 13 | Encourages a document first approach to writing command line interfaces in a fluid natural language writing style. 14 | 15 | ## Install 16 | 17 | ``` 18 | npm i mkcli --save 19 | ``` 20 | 21 | For the command line interface install [mkdoc][] globally (`npm i -g mkdoc`). 22 | 23 | --- 24 | 25 | - [Install](#install) 26 | - [Example](#example) 27 | - [Guide](#guide) 28 | - [Defining Programs](#defining-programs) 29 | - [Name](#name) 30 | - [Synopsis](#synopsis) 31 | - [Description](#description) 32 | - [Arguments](#arguments) 33 | - [Flags](#flags) 34 | - [Options](#options) 35 | - [Required](#required) 36 | - [Multiple](#multiple) 37 | - [Type Info](#type-info) 38 | - [Default Value](#default-value) 39 | - [Commands](#commands) 40 | - [Identifiers](#identifiers) 41 | - [Manual Sections](#manual-sections) 42 | - [Synopsis Expansion](#synopsis-expansion) 43 | - [Flags](#flags-1) 44 | - [Options](#options-1) 45 | - [Exclusive Options](#exclusive-options) 46 | - [Expansion Example](#expansion-example) 47 | - [Compiling Programs](#compiling-programs) 48 | - [Creating Documentation](#creating-documentation) 49 | - [Help Styles](#help-styles) 50 | - [Help Sections](#help-sections) 51 | - [Completion](#completion) 52 | - [Actions](#actions) 53 | - [Synopsis Completion](#synopsis-completion) 54 | - [Specification Completion](#specification-completion) 55 | - [Command Completion](#command-completion) 56 | - [Help](#help) 57 | - [API](#api) 58 | - [src](#src) 59 | - [compiler](#compiler) 60 | - [dest](#dest) 61 | - [Options](#options-2) 62 | - [load](#load) 63 | - [run](#run) 64 | - [License](#license) 65 | 66 | --- 67 | 68 | ## Example 69 | 70 | To compile all output types to the same directory as the input file: 71 | 72 | ```shell 73 | mkcli program.md 74 | ``` 75 | 76 | Compile all output types to a specific directory: 77 | 78 | ```shell 79 | mkcli program.md -o build 80 | ``` 81 | 82 | Compile a specific output type: 83 | 84 | ```shell 85 | mkcli -t man program.md 86 | ``` 87 | 88 | Compile a specific output type to a particular directory: 89 | 90 | ```shell 91 | mkcli -t zsh program.md --zsh build/zsh 92 | ``` 93 | 94 | If you have a lot of programs pass a directory and all markdown documents in the directory are compiled: 95 | 96 | ```shell 97 | mkcli doc/cli -o build 98 | ``` 99 | 100 | You may pipe input for more control over the output; to set a man page title: 101 | 102 | ```shell 103 | mkcat program.md | mkcli -t man | mkman --title program > program.1 104 | ``` 105 | 106 | See [help](#help) for more options. 107 | 108 | Example files for a simple working program are in [doc/example](https://github.com/mkdoc/mkcli/blob/master/doc/example): 109 | 110 | * [program definition](https://github.com/mkdoc/mkcli/blob/master/doc/example/argv.md) 111 | * [program descriptor](https://github.com/mkdoc/mkcli/blob/master/doc/example/argv.json) 112 | * [help file](https://github.com/mkdoc/mkcli/blob/master/doc/example/argv.txt) 113 | * [man page](https://github.com/mkdoc/mkcli/blob/master/doc/example/argv.1) 114 | * [zsh completion](https://github.com/mkdoc/mkcli/blob/master/doc/example/_argv) 115 | * [program implementation](https://github.com/mkdoc/mkcli/blob/master/doc/example/argv.js) 116 | * [minimal executable](https://github.com/mkdoc/mkcli/blob/master/doc/example/argv) 117 | 118 | Every program in the [mkdoc][] toolkit is compiled using this library: 119 | 120 | * [definitions](https://github.com/mkdoc/mkdoc/tree/master/doc/cli) 121 | * [compiled descriptors](https://github.com/mkdoc/mkdoc/tree/master/doc/json) 122 | * [help files](https://github.com/mkdoc/mkdoc/tree/master/doc/help) 123 | * [man pages](https://github.com/mkdoc/mkdoc/tree/master/doc/man) 124 | * [zsh completion](https://github.com/mkdoc/mkdoc/tree/master/doc/zsh) 125 | * [program implementations](https://github.com/mkdoc/mkdoc/tree/master/cli) 126 | * [executables](https://github.com/mkdoc/mkdoc/tree/master/bin) 127 | 128 | ## Guide 129 | 130 | ### Defining Programs 131 | 132 | The markdown document defines sections that start with a level one heading and continue until the next level one heading or the end of file is reached. 133 | 134 | The sections that have special meaning to the compiler are [NAME](#name), [SYNOPSIS](#synopsis), [DESCRIPTION](#description), [COMMANDS](#commands) and [OPTIONS](#arguments). 135 | 136 | It is considered best practice to declare these sections in the order listed. 137 | 138 | All other sections are deemed to be man page sections they are ignored from help output by default (but may be included at compile time) and are always included when generating man pages. 139 | 140 | Section headings are not case-sensitive so you can use upper case, title case or lower case but they must match exactly. 141 | 142 | #### Name 143 | 144 | Like man pages the name section is required and it **must** include a brief summary of the program after the program name. Delimit the program name from the short summary using a hyphen surrounded by spaces as shown below. 145 | 146 | The name section must be the first section in the file otherwise the compiler will error. 147 | 148 | The program name and summary is extracted from the first paragraph under the *NAME* heading: 149 | 150 | ```markdown 151 | # Name 152 | 153 | prg - short program summary 154 | ``` 155 | 156 | For subcommands define parent names for a command using whitespace between the words: 157 | 158 | ```markdown 159 | # Name 160 | 161 | prg list - perform list action 162 | ``` 163 | 164 | Add a list when a program can have multiple names: 165 | 166 | ```markdown 167 | # Name 168 | 169 | prg - short program summary 170 | 171 | + prg-alias 172 | ``` 173 | 174 | #### Synopsis 175 | 176 | The program synopsis is created from all code block elements under the *SYNOPSIS* heading: 177 | 178 | ```markdown 179 | # Name 180 | 181 | prg - short program summary 182 | 183 | # Synopsis 184 | 185 | [options] [file...] 186 | ``` 187 | 188 | It is a compiler error if any other type is declared in the synopsis section. 189 | 190 | #### Description 191 | 192 | The program description is created from all block level elements under the *DESCRIPTION* heading: 193 | 194 | ```markdown 195 | # Name 196 | 197 | prg - short program summary 198 | 199 | # Description 200 | 201 | An extended description that can include paragraphs, lists, code blocks and other block level elements. 202 | ``` 203 | 204 | Note that the help output only includes paragraphs so some meaning may be lost if you include lists, code blocks or block quotes. For this reason it is recommended that the description section only contain paragraphs. 205 | 206 | If you mix content in the description section you can use the `-d, --desc` option when generating the help file to restrict the number of paragraphs included in the help output. 207 | 208 | Consider this example: 209 | 210 | ```markdown 211 | # Name 212 | 213 | prg - short program summary 214 | 215 | # Description 216 | 217 | Simple program. 218 | 219 | Run with: 220 | 221 | cat file.md | prg 222 | ``` 223 | 224 | Context would be lost on the second paragraph because the code block would not be included in the help output, whilst it would make perfect sense in the man output. 225 | 226 | To prevent this loss of context just include the first paragraph in the help output: 227 | 228 | ```shell 229 | mkcat program.md | mkcli --desc 1 | mktext 230 | ``` 231 | 232 | #### Arguments 233 | 234 | Program arguments are declared with a heading of *OPTIONS* and a list following the heading. 235 | 236 | Note the list bullet character `+` is preferred because it creates a more idiomatic listing in generated man pages. 237 | 238 | ```markdown 239 | # Name 240 | 241 | prg - short program summary 242 | 243 | # Options 244 | 245 | + `-i, --input [FILE...]` Input files 246 | + `-o, --output [FILE]` Output file 247 | ``` 248 | 249 | An argument is declared as a list item whose first child is an inline code element which defines a *specification*. 250 | 251 | The specification is parsed into an object representing the argument which may be of type `flag`, `option` or `command`. 252 | 253 | The remaining list item content after the specification is treated as a description for the argument. 254 | 255 | ##### Flags 256 | 257 | An argument specification with no value is treated as a flag option: 258 | 259 | ```markdown 260 | + `-v, --verbose` Print more information 261 | ``` 262 | 263 | ##### Options 264 | 265 | To create an option argument specify a value in either `[]` or `<>`: 266 | 267 | ```markdown 268 | + `-o, --output [FILE]` Output file 269 | ``` 270 | 271 | ###### Required 272 | 273 | When the `<>` notation is used it indicates that that the option is required: 274 | 275 | ```markdown 276 | + `-t, --type ` Output format 277 | ``` 278 | 279 | The parsed option will have the `required` flag set. 280 | 281 | ###### Multiple 282 | 283 | To signify that an option argument is repeatable include an ellipsis: 284 | 285 | ```markdown 286 | + `-i, --input [FILE...]` Input files 287 | ``` 288 | 289 | The parsed option will have the `multiple` flag set. 290 | 291 | ###### Type Info 292 | 293 | You can associate some type information with the `{}` notation: 294 | 295 | ```markdown 296 | + `-i, --indent [NUM] {Number}` Amount of indentation 297 | ``` 298 | 299 | The parsed option will have the `kind` property set to `Number`. 300 | 301 | You can delimit multiple types with `|` and `kind` is expanded to an array. This is useful to indicate an argument may be of multiple types or if you want to treat an argument value as an enum: 302 | 303 | ```markdown 304 | + `-t, --type [VAL] {json|help|man}` Renderer type 305 | ``` 306 | 307 | ###### Default Value 308 | 309 | To specify a default value for the option use the `=` operator in the type: 310 | 311 | ```markdown 312 | + `-i, --indent [NUM] {Number=2}` Amount of indentation 313 | ``` 314 | 315 | The parsed option will have the `kind` property set to `Number` and the `value` property set to `2`. 316 | 317 | You can just specify the default value using: 318 | 319 | ```markdown 320 | + `-i, --indent [NUM] {=2}` Amount of indentation 321 | ``` 322 | 323 | In which case the `kind` property will be `undefined` and the `value` property is set to `2`. 324 | 325 | #### Commands 326 | 327 | Commands are declared in the same way as program arguments but under the *COMMANDS* heading: 328 | 329 | ```markdown 330 | # Name 331 | 332 | prg - short program summary 333 | 334 | # Commands 335 | 336 | + `ls, list` List tasks 337 | + `i, info` Print task information 338 | ``` 339 | 340 | They allow you to create complex programs with options specific to a command. 341 | 342 | Command files are loaded and compiled automatically following a naming convention. Using the above example to define the `list` command create a file named `prg-list.md`: 343 | 344 | ```markdown 345 | # Name 346 | 347 | list - list tasks 348 | 349 | # Options 350 | 351 | + `-a, --all` List all tasks 352 | + `-t=[TYPE...]` List tasks of TYPE 353 | ``` 354 | 355 | Will result in the compiled tree containing options specific to the `list` command. 356 | 357 | #### Identifiers 358 | 359 | When a program is created from a source markdown document each argument and command is given a key for the resulting map. This key is generated automatically by using the longest argument (or command) name and converting it to camel case. 360 | 361 | If you wish to use a fixed key you can add an identifier followed by a colon (`:`) to the beginning of the specification: 362 | 363 | ```markdown 364 | # Name 365 | 366 | prg - short program summary 367 | 368 | # Commands 369 | 370 | + `tasks: ls, list` List tasks 371 | 372 | # Options 373 | 374 | + `verbose: -v` Print more information 375 | ``` 376 | 377 | #### Manual Sections 378 | 379 | A heading that is not matched by any of the rules above is treated as a manual section: 380 | 381 | ```markdown 382 | # Name 383 | 384 | prg - short program summary 385 | 386 | # Environment 387 | 388 | The environment variable FOO changes the behaviour to `bar`. 389 | ``` 390 | 391 | The section ends when the next level one heading is encountered or the end of the file is reached. 392 | 393 | ### Synopsis Expansion 394 | 395 | Unless disabled the synopsis declaration is expanded for the `man` and `help` output types. 396 | 397 | #### Flags 398 | 399 | Use the notation `[flags]` (or ``) in the synopsis and it will be replaced with all short form (single character) flag options (for example: `-xvf`). 400 | 401 | #### Options 402 | 403 | Use the notation `[options]` (or ``) in the synopsis and it will be replaced with all option names that are not declared in the synopsis and were not expanded using the `[flags]` notation. 404 | 405 | #### Exclusive Options 406 | 407 | You should indicate mutually exclusive options using a vertical bar between option names. 408 | 409 | When compiling to JSON the synopsis is parsed and any mutually exclusive declarations are added to the output using the target option keys. 410 | 411 | It is a compiler error if the target option is not declared. 412 | 413 | #### Expansion Example 414 | 415 | Given a definition such as: 416 | 417 | ```markdown 418 | # Name 419 | 420 | prg - short program summary 421 | 422 | # Synopsis 423 | 424 | [flags] [options] [--xml|--html] 425 | 426 | # Options 427 | 428 | + `-X, --xml` Print as XML 429 | + `-H, --html` Print as HTML 430 | + `-V` Print more information 431 | + `-h, --help` Display help and exit 432 | + `--version` Print the version and exit 433 | ``` 434 | 435 | The synopsis is expanded to: 436 | 437 | ``` 438 | prg [-XHVh] [--help] [--version] [--xml|--html] 439 | ``` 440 | 441 | ### Compiling Programs 442 | 443 | To compile the markdown document to a JSON program descriptor run: 444 | 445 | ```shell 446 | mkcli -t json program.md 447 | ``` 448 | 449 | Now you have a JSON document that describes your program commands and options. 450 | 451 | ### Creating Documentation 452 | 453 | Once you have defined the program you will want to generate a man page and some help text. 454 | 455 | To create the help text run: 456 | 457 | ```shell 458 | mkcli -t help program.md 459 | ``` 460 | 461 | For a man page run: 462 | 463 | ```shell 464 | mkcli -t man program.md 465 | ``` 466 | 467 | #### Help Styles 468 | 469 | The default column help style (`col`) should suit most purposes however the other styles can be useful. The `list` style renders a list of the commands and options which is designed for when you have very long argument names or a few arguments that require long descriptions. 470 | 471 | The `cmd` style is a list of command names (options are not printed) designed to be used when a program has lots of commands and a command is required. Typically the program would show this help page when no command was specified to indicate to the user a command is required. 472 | 473 | Sometimes you may want very minimal help output that just includes the usage synopsis in which case use the `usage` style. 474 | 475 | #### Help Sections 476 | 477 | Sometimes when creating help files you may want to include a section from the manual, possibly you want to include an *Environment* section to show the environment variables your program recognises. 478 | 479 | Pass regular expression patterns using the `--section` option and if they match a section heading the section will be included in the help after the commands and options. 480 | 481 | To include an *Environment* section you could use: 482 | 483 | ```shell 484 | mkcli -t help -S env program.md 485 | ``` 486 | 487 | To include the *Environment* and *Bugs* sections you could use: 488 | 489 | ```shell 490 | mkcli -t help -S env -S bug program.md 491 | ``` 492 | 493 | Or if you prefer: 494 | 495 | ```shell 496 | mkcli -t help -S '(env|bug)' program.md 497 | ``` 498 | 499 | See the [help](#help) for more options available when creating help and man pages. 500 | 501 | ### Completion 502 | 503 | Completion scripts are currently available for zsh. To install a completion script for a program copy the script to a directory in `$fpath` or modify `~/.zshrc` to autoload the directory containing the completion script: 504 | 505 | ```zsh 506 | fpath=(/path/to/completion $fpath) 507 | ``` 508 | 509 | A full working completion example is the [notes](https://github.com/mkdoc/mkcli/blob/master/test/fixtures/completion) test fixture. 510 | 511 | Sometimes you may wish to reload a completion for testing purposes: 512 | 513 | ```zsh 514 | unfunction _notes && autoload -U _notes 515 | ``` 516 | 517 | #### Actions 518 | 519 | Some option value specifications map to zsh completion functions: 520 | 521 | * user: `:user:_users` 522 | * group: `:group:_groups` 523 | * host: `:host:_hosts` 524 | * domain: `:domain:_domains` 525 | * file: `:file:_files` 526 | * dir: `:directory:_directories` 527 | * url: `:url:_urls` 528 | 529 | Such that an option specification such as: 530 | 531 | ```markdown 532 | + `-i, --input [file...]` Input files 533 | + `-o, --output ` Output directory 534 | ``` 535 | 536 | Will result in the `_files` completion function being called to complete file paths for the `--input` option and the `_directories` function for the `--output` option. Note that the ellipsis (...) multiple flag is respected so `--input` will be completed multiple times whilst `--output` will only complete once. 537 | 538 | For options that specify a list of types the `_values` completion function is called. 539 | 540 | ```markdown 541 | + `-t, --type=[TYPE] {json|yaml}` Output type 542 | ``` 543 | 544 | Results in automatic completion for the `--type` option to one of `json` or `yaml`. 545 | 546 | Actions are enclosed in double quotes (") so you may use single quotes and paired double quotes but not a single double quote which will generate an `unmatched "` zsh error. 547 | 548 | #### Synopsis Completion 549 | 550 | The program synopsis section is inspected and will use completion functions when a match is available, so a synopsis such as: 551 | 552 | ```markdown 553 | [options] [files...] 554 | ``` 555 | 556 | Will result in the _files completion function called, see above for the list of matches and completion functions. 557 | 558 | Sometimes you may need to create a custom completion list; you can set the info string of fenced code blocks in the synopsis section to inject scripts. The value may be either `zsh-locals` to inject code into the beginning of the body of the generated completion function and `zsh` to add to the list of completion actions. 559 | 560 | A real-world example is [mk](https://github.com/mkdoc/mkdoc#mk) ([program definition](https://raw.githubusercontent.com/mkdoc/mkdoc/master/doc/cli/mk.md) and [compiled completion script](https://github.com/mkdoc/mkdoc/blob/master/doc/zsh/_mk)) which completes on the available task names. 561 | 562 | #### Specification Completion 563 | 564 | You may wish to change the zsh action taken per option, this can be done by appending a colon and the zsh action to an option specification: 565 | 566 | ```markdown 567 | + `-p, --package=[FILE] :file:_files -g '+.json'` Package descriptor 568 | ``` 569 | 570 | Which will complete files with a `.json` extension for the `--package` option. 571 | 572 | #### Command Completion 573 | 574 | Commands are recursively added to the completion script; they are completed using the following rules: 575 | 576 | * Required commands (`` in the synopsis) will not list options by default. 577 | * Command options inherit from the global options. 578 | * Command options cascade to child options. 579 | * Rest pattern matches (`*: :file:_files` for example) are respected. 580 | 581 | It is recommended you use a program synopsis with the command first: 582 | 583 | ```markdown 584 | # Synopsis 585 | 586 | [options] [files...] 587 | ``` 588 | 589 | Or if the command is not required: 590 | 591 | ```markdown 592 | # Synopsis 593 | 594 | [command] [options] [files...] 595 | ``` 596 | 597 | Which is because command completion is terminated when an option is intermingled with the command hierarchy. Consider a program that has the command structure `notes > list > bug|todo|feature` if you present a command line such as: 598 | 599 | ```shell 600 | notes list --private 601 | ``` 602 | 603 | Completion will no longer be attempted on the `list` sub-commands. To put it another way *commands must be consecutive* for command completion to occur. 604 | 605 | ## Help 606 | 607 | ``` 608 | Usage: mkcli [-frRCHFNPh] [--full] [--recursive] [--raw-synopsis] [--colon] 609 | [--header] [--footer] [--newline] [--preserve] [--help] 610 | [--version] [--package=] [--type=] [--style=] 611 | [--cols=] [--split=] [--desc=] [--indent=] 612 | [--align=] [--usage=] [--section=] 613 | [--json=] [--text=] [--man=] [--zsh=] 614 | [--output=] [files...] 615 | 616 | Compiles markdown cli definitions. 617 | 618 | Options 619 | -p, --package=[FILE] Use package descriptor 620 | -t, --type=[TYPE] Output renderer type (json|help|man) 621 | -y, --style=[VAL] Help output style (col|list|cmd|usage) 622 | -c, --cols=[NUM] Wrap help output at NUM (default: 80) 623 | -s, --split=[NUM] Split help columns at NUM (default: 26) 624 | -d, --desc=[NUM] Number of description paragraphs for help output 625 | -i, --indent=[NUM] Number of spaces for help indentation (default: 2) 626 | -a, --align=[TYPE] Alignment of first help column (left|right) 627 | -u, --usage=[VAL] Set usage message for help synopsis (default: Usage:) 628 | -f, --full Do not compact compiled descriptor 629 | -r, --recursive Recursively load command definitions 630 | -R, --raw-synopsis Do not expand synopsis 631 | -C, --colon Append a colon to headings in help output 632 | -S, --section=[PTN...] Include sections matching patterns in help output 633 | -H, --header Include default header in help output 634 | -F, --footer Include default footer in help output 635 | -N, --newline Print leading newline when no header 636 | -P, --preserve Do not upper case headings in man output 637 | -J, --json=[DIR] Set output directory for json files 638 | -T, --text=[DIR] Set output directory for help text files 639 | -M, --man=[DIR] Set output directory for man pages 640 | -Z, --zsh=[DIR] Set output directory for zsh completion 641 | -o, --output=[DIR] Set output directory for all types 642 | -h, --help Display help and exit 643 | --version Print the version and exit 644 | 645 | mkcli@1.0.32 https://github.com/mkdoc/mkcli 646 | ``` 647 | 648 | ## API 649 | 650 | ### src 651 | 652 | ```javascript 653 | src([opts]) 654 | ``` 655 | 656 | Gets a source parser stream that transforms the incoming tree nodes into 657 | parser state information. 658 | 659 | Returns a parser stream. 660 | 661 | * `opts` Object parser options. 662 | 663 | ### compiler 664 | 665 | ```javascript 666 | compiler([opts]) 667 | ``` 668 | 669 | Gets a compiler stream that transforms the parser state information to 670 | a program definition. 671 | 672 | Returns a compiler stream. 673 | 674 | * `opts` Object compiler options. 675 | 676 | ### dest 677 | 678 | ```javascript 679 | dest([opts]) 680 | ``` 681 | 682 | Gets a destination renderer stream. 683 | 684 | When no type is specified the JSON renderer is assumed. 685 | 686 | Returns a renderer stream of the specified type. 687 | 688 | * `opts` Object renderer options. 689 | 690 | #### Options 691 | 692 | * `type` String=json the renderer type. 693 | 694 | ### load 695 | 696 | ```javascript 697 | load(def[, opts]) 698 | ``` 699 | 700 | Load a program definition into a new program assigning the definition 701 | properties to the program. 702 | 703 | Properties are passed by reference so if you modify the definition the 704 | program is also modified. 705 | 706 | Returns a new program. 707 | 708 | * `def` Object the program definition. 709 | * `opts` Object program options. 710 | 711 | ### run 712 | 713 | ```javascript 714 | run(src, argv[, runtime], cb) 715 | ``` 716 | 717 | Load a program definition into a new program assigning the definition 718 | properties to the program. 719 | 720 | Properties are passed by reference so if you modify the definition the 721 | program is also modified. 722 | 723 | The callback function signature is `function(err, req)` where `req` is a 724 | request object that contains state information for program execution. 725 | 726 | Plugins may decorate the request object with pertinent information that 727 | does not affect the `target` object that receives the parsed arguments. 728 | 729 | Returns a new program. 730 | 731 | * `src` Object the source program or definition. 732 | * `argv` Array the program arguments. 733 | * `runtime` Object runtime configuration. 734 | * `cb` Function callback function. 735 | 736 | ## License 737 | 738 | MIT 739 | 740 | --- 741 | 742 | Created by [mkdoc](https://github.com/mkdoc/mkdoc) on April 26, 2017 743 | 744 | [mkdoc]: https://github.com/mkdoc/mkdoc 745 | [mkast]: https://github.com/mkdoc/mkast 746 | [through]: https://github.com/tmpfs/through3 747 | [commonmark]: http://commonmark.org 748 | [jshint]: http://jshint.com 749 | [jscs]: http://jscs.info 750 | 751 | -------------------------------------------------------------------------------- /doc/example/_argv: -------------------------------------------------------------------------------- 1 | #compdef argv 2 | _arguments \ 3 | "(-e --err)"{-e,--err}"[Print to stderr]" \ 4 | "(-h --help)"{-h,--help}"[Display help and exit]" \ 5 | "--version[Print version and exit]" \ 6 | && return 0; 7 | -------------------------------------------------------------------------------- /doc/example/argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cli = require('./argv.js'); 4 | 5 | cli(function(err) { 6 | if(err) { 7 | console.error(err.message); 8 | process.exitCode = 1; 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /doc/example/argv.1: -------------------------------------------------------------------------------- 1 | .\" Generated by mkdoc on Tue Apr 12 2016 12:31:19 GMT+0800 (WITA) 2 | .TH "UNTITLED" "1" "April, 2016" "UNTITLED 1.0" "User Commands" 3 | .de nl 4 | .sp 0 5 | .. 6 | .de hr 7 | .sp 1 8 | .nf 9 | .ce 10 | .in 4 11 | \l’80’ 12 | .fi 13 | .. 14 | .de h1 15 | .RE 16 | .sp 1 17 | \fB\\$1\fR 18 | .RS 4 19 | .. 20 | .de h2 21 | .RE 22 | .sp 1 23 | .in 4 24 | \fB\\$1\fR 25 | .RS 6 26 | .. 27 | .de h3 28 | .RE 29 | .sp 1 30 | .in 6 31 | \fB\\$1\fR 32 | .RS 8 33 | .. 34 | .de h4 35 | .RE 36 | .sp 1 37 | .in 8 38 | \fB\\$1\fR 39 | .RS 10 40 | .. 41 | .de h5 42 | .RE 43 | .sp 1 44 | .in 10 45 | \fB\\$1\fR 46 | .RS 12 47 | .. 48 | .de h6 49 | .RE 50 | .sp 1 51 | .in 12 52 | \fB\\$1\fR 53 | .RS 14 54 | .. 55 | .h1 "NAME" 56 | .P 57 | argv \- prints the arguments passed to the program 58 | .nl 59 | .h1 "SYNOPSIS" 60 | .PP 61 | .in 10 62 | [options] 63 | .h1 "DESCRIPTION" 64 | .P 65 | When the \fB\-\-err\fR option is given the arguments are printed to stderr rather than stdout. 66 | .nl 67 | .h1 "OPTIONS" 68 | .BL 69 | .IP "\[ci]" 4 70 | \fB\-e, \-\-err\fR Print to stderr 71 | .nl 72 | .IP "\[ci]" 4 73 | \fB\-h, \-\-help\fR Display help and exit 74 | .nl 75 | .IP "\[ci]" 4 76 | \fB\-\-version\fR Print version and exit 77 | .nl 78 | .EL -------------------------------------------------------------------------------- /doc/example/argv.js: -------------------------------------------------------------------------------- 1 | var cli = require('../../index') 2 | // create a program with the descriptor information 3 | , prg = cli.load(require('./argv.json')); 4 | 5 | /** 6 | * @name argv 7 | * @cli doc/example/argv.md 8 | */ 9 | function main(argv, cb) { 10 | 11 | // we want to be able to accept arguments 12 | // for testing purposes but when they are not specified 13 | // it is ok becuase process.argv is used by default 14 | if(typeof argv === 'function') { 15 | cb = argv; 16 | argv = null; 17 | } 18 | 19 | // target for parsed command line options 20 | var scope = {} 21 | // runtime configuration for program execution 22 | , runtime = { 23 | // resolve paths relative to this directory 24 | base: __dirname, 25 | // pass the scope 26 | target: scope, 27 | // give the argument parser some hints 28 | hints: prg, 29 | // configure the help plugin to show the help file 30 | help: { 31 | file: 'argv.txt' 32 | }, 33 | // configure the version plugin 34 | version: { 35 | name: 'argv', 36 | version: '1.0.0' 37 | }, 38 | // configure plugins for program execution 39 | plugins: [ 40 | require('../../plugin/hints'), 41 | require('../../plugin/argv'), 42 | require('../../plugin/help'), 43 | require('../../plugin/version') 44 | ] 45 | }; 46 | 47 | // run the program passing the program, raw arguments and the 48 | // runtime configuration 49 | cli.run(prg, argv, runtime, function parsed(err, req) { 50 | 51 | // handle errors and aborted request 52 | // the request will have been aborted if --help or 53 | // the --version option was specified 54 | if(err || req.aborted) { 55 | return cb(err); 56 | } 57 | 58 | // `this` is the `scope` object passed as the `target` 59 | // parsed arguments are available using `this` 60 | // more information is available on the `req` object 61 | // of particular interest is `req.args` which is the argument 62 | // parser result object and `req.unparsed` which contains 63 | // any unparsed arguments 64 | 65 | // include the unparsed arguments in the output 66 | this.unparsed = req.unparsed; 67 | 68 | // respect the -e, --err option 69 | if(this.err) { 70 | return console.error(this); 71 | } 72 | 73 | console.log(this); 74 | }) 75 | } 76 | 77 | module.exports = main; 78 | -------------------------------------------------------------------------------- /doc/example/argv.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "When the --err option is given the arguments are printed to stderr rather than stdout.", 3 | "names": [ 4 | "argv" 5 | ], 6 | "type": "program", 7 | "name": "argv", 8 | "summary": "prints the arguments passed to the program", 9 | "synopsis": [ 10 | "[options]" 11 | ], 12 | "options": { 13 | "err": { 14 | "key": "err", 15 | "description": "Print to stderr", 16 | "names": [ 17 | "-e", 18 | "--err" 19 | ], 20 | "type": "flag" 21 | }, 22 | "help": { 23 | "key": "help", 24 | "description": "Display help and exit", 25 | "names": [ 26 | "-h", 27 | "--help" 28 | ], 29 | "type": "flag" 30 | }, 31 | "version": { 32 | "key": "version", 33 | "description": "Print version and exit", 34 | "names": [ 35 | "--version" 36 | ], 37 | "type": "flag" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /doc/example/argv.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | argv - prints the arguments passed to the program 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [options] 9 | ``` 10 | 11 | # Description 12 | 13 | When the `--err` option is given the arguments are printed to stderr rather than stdout. 14 | 15 | # Options 16 | 17 | * `-e, --err` Print to stderr 18 | * `-h, --help` Display help and exit 19 | * `--version` Print version and exit 20 | -------------------------------------------------------------------------------- /doc/example/argv.txt: -------------------------------------------------------------------------------- 1 | Usage: argv [options] 2 | 3 | When the --err option is given the arguments are printed to stderr rather 4 | than stdout. 5 | 6 | Options 7 | -e, --err Print to stderr 8 | -h, --help Display help and exit 9 | --version Print version and exit 10 | 11 | -------------------------------------------------------------------------------- /doc/readme.md: -------------------------------------------------------------------------------- 1 | # Command Line Interface 2 | 3 | 4 | 5 | > Define command line interfaces as markdown 6 | 7 | Describe a command line interface as an easy to read man-style markdown document and compile it to a program descriptor; the JSON program descriptor can then be used by the program implementation to parse and validate arguments. 8 | 9 | The markdown program definitions can be converted to man pages, help files and shell completion scripts. 10 | 11 | Encourages a document first approach to writing command line interfaces in a fluid natural language writing style. 12 | 13 | 14 | 15 | *** 16 | 17 | *** 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /doc/readme/.gitignore: -------------------------------------------------------------------------------- 1 | actions.md 2 | -------------------------------------------------------------------------------- /doc/readme/badges.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/mkdoc/mkcli.svg?v=3)](https://travis-ci.org/mkdoc/mkcli) 2 | [![npm version](http://img.shields.io/npm/v/mkcli.svg?v=3)](https://npmjs.org/package/mkcli) 3 | [![Coverage Status](https://coveralls.io/repos/mkdoc/mkcli/badge.svg?branch=master&service=github&v=3)](https://coveralls.io/github/mkdoc/mkcli?branch=master) 4 | 5 | -------------------------------------------------------------------------------- /doc/readme/example-files.md: -------------------------------------------------------------------------------- 1 | See [help](#help) for more options. 2 | 3 | Example files for a simple working program are in [doc/example](/doc/example): 4 | 5 | * [program definition](/doc/example/argv.md) 6 | * [program descriptor](/doc/example/argv.json) 7 | * [help file](/doc/example/argv.txt) 8 | * [man page](/doc/example/argv.1) 9 | * [zsh completion](/doc/example/_argv) 10 | * [program implementation](/doc/example/argv.js) 11 | * [minimal executable](/doc/example/argv) 12 | 13 | Every program in the [mkdoc][] toolkit is compiled using this library: 14 | 15 | * [definitions](https://github.com/mkdoc/mkdoc/tree/master/doc/cli) 16 | * [compiled descriptors](https://github.com/mkdoc/mkdoc/tree/master/doc/json) 17 | * [help files](https://github.com/mkdoc/mkdoc/tree/master/doc/help) 18 | * [man pages](https://github.com/mkdoc/mkdoc/tree/master/doc/man) 19 | * [zsh completion](https://github.com/mkdoc/mkdoc/tree/master/doc/zsh) 20 | * [program implementations](https://github.com/mkdoc/mkdoc/tree/master/cli) 21 | * [executables](https://github.com/mkdoc/mkdoc/tree/master/bin) 22 | 23 | -------------------------------------------------------------------------------- /doc/readme/example.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | To compile all output types to the same directory as the input file: 4 | 5 | ```shell 6 | mkcli program.md 7 | ``` 8 | 9 | Compile all output types to a specific directory: 10 | 11 | ```shell 12 | mkcli program.md -o build 13 | ``` 14 | 15 | Compile a specific output type: 16 | 17 | ```shell 18 | mkcli -t man program.md 19 | ``` 20 | 21 | Compile a specific output type to a particular directory: 22 | 23 | ```shell 24 | mkcli -t zsh program.md --zsh build/zsh 25 | ``` 26 | 27 | If you have a lot of programs pass a directory and all markdown documents in the directory are compiled: 28 | 29 | ```shell 30 | mkcli doc/cli -o build 31 | ``` 32 | 33 | You may pipe input for more control over the output; to set a man page title: 34 | 35 | ```shell 36 | mkcat program.md | mkcli -t man | mkman --title program > program.1 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /doc/readme/guide.md: -------------------------------------------------------------------------------- 1 | ## Guide 2 | 3 | ### Defining Programs 4 | 5 | The markdown document defines sections that start with a level one heading and continue until the next level one heading or the end of file is reached. 6 | 7 | The sections that have special meaning to the compiler are [NAME](#name), [SYNOPSIS](#synopsis), [DESCRIPTION](#description), [COMMANDS](#commands) and [OPTIONS](#arguments). 8 | 9 | It is considered best practice to declare these sections in the order listed. 10 | 11 | All other sections are deemed to be man page sections they are ignored from help output by default (but may be included at compile time) and are always included when generating man pages. 12 | 13 | Section headings are not case-sensitive so you can use upper case, title case or lower case but they must match exactly. 14 | 15 | #### Name 16 | 17 | Like man pages the name section is required and it **must** include a brief summary of the program after the program name. Delimit the program name from the short summary using a hyphen surrounded by spaces as shown below. 18 | 19 | The name section must be the first section in the file otherwise the compiler will error. 20 | 21 | The program name and summary is extracted from the first paragraph under the *NAME* heading: 22 | 23 | ```markdown 24 | # Name 25 | 26 | prg - short program summary 27 | ``` 28 | 29 | For subcommands define parent names for a command using whitespace between the words: 30 | 31 | ```markdown 32 | # Name 33 | 34 | prg list - perform list action 35 | ``` 36 | 37 | Add a list when a program can have multiple names: 38 | 39 | ```markdown 40 | # Name 41 | 42 | prg - short program summary 43 | 44 | + prg-alias 45 | ``` 46 | 47 | #### Synopsis 48 | 49 | The program synopsis is created from all code block elements under the *SYNOPSIS* heading: 50 | 51 | ```markdown 52 | # Name 53 | 54 | prg - short program summary 55 | 56 | # Synopsis 57 | 58 | [options] [file...] 59 | ``` 60 | 61 | It is a compiler error if any other type is declared in the synopsis section. 62 | 63 | #### Description 64 | 65 | The program description is created from all block level elements under the *DESCRIPTION* heading: 66 | 67 | ```markdown 68 | # Name 69 | 70 | prg - short program summary 71 | 72 | # Description 73 | 74 | An extended description that can include paragraphs, lists, code blocks and other block level elements. 75 | ``` 76 | 77 | Note that the help output only includes paragraphs so some meaning may be lost if you include lists, code blocks or block quotes. For this reason it is recommended that the description section only contain paragraphs. 78 | 79 | If you mix content in the description section you can use the `-d, --desc` option when generating the help file to restrict the number of paragraphs included in the help output. 80 | 81 | Consider this example: 82 | 83 | ```markdown 84 | # Name 85 | 86 | prg - short program summary 87 | 88 | # Description 89 | 90 | Simple program. 91 | 92 | Run with: 93 | 94 | cat file.md | prg 95 | ``` 96 | 97 | Context would be lost on the second paragraph because the code block would not be included in the help output, whilst it would make perfect sense in the man output. 98 | 99 | To prevent this loss of context just include the first paragraph in the help output: 100 | 101 | ```shell 102 | mkcat program.md | mkcli --desc 1 | mktext 103 | ``` 104 | 105 | #### Arguments 106 | 107 | Program arguments are declared with a heading of *OPTIONS* and a list following the heading. 108 | 109 | Note the list bullet character `+` is preferred because it creates a more idiomatic listing in generated man pages. 110 | 111 | ```markdown 112 | # Name 113 | 114 | prg - short program summary 115 | 116 | # Options 117 | 118 | + `-i, --input [FILE...]` Input files 119 | + `-o, --output [FILE]` Output file 120 | ``` 121 | 122 | An argument is declared as a list item whose first child is an inline code element which defines a *specification*. 123 | 124 | The specification is parsed into an object representing the argument which may be of type `flag`, `option` or `command`. 125 | 126 | The remaining list item content after the specification is treated as a description for the argument. 127 | 128 | ##### Flags 129 | 130 | An argument specification with no value is treated as a flag option: 131 | 132 | ```markdown 133 | + `-v, --verbose` Print more information 134 | ``` 135 | 136 | ##### Options 137 | 138 | To create an option argument specify a value in either `[]` or `<>`: 139 | 140 | ```markdown 141 | + `-o, --output [FILE]` Output file 142 | ``` 143 | 144 | ###### Required 145 | 146 | When the `<>` notation is used it indicates that that the option is required: 147 | 148 | ```markdown 149 | + `-t, --type ` Output format 150 | ``` 151 | 152 | The parsed option will have the `required` flag set. 153 | 154 | ###### Multiple 155 | 156 | To signify that an option argument is repeatable include an ellipsis: 157 | 158 | ```markdown 159 | + `-i, --input [FILE...]` Input files 160 | ``` 161 | 162 | The parsed option will have the `multiple` flag set. 163 | 164 | ###### Type Info 165 | 166 | You can associate some type information with the `{}` notation: 167 | 168 | ```markdown 169 | + `-i, --indent [NUM] {Number}` Amount of indentation 170 | ``` 171 | 172 | The parsed option will have the `kind` property set to `Number`. 173 | 174 | You can delimit multiple types with `|` and `kind` is expanded to an array. This is useful to indicate an argument may be of multiple types or if you want to treat an argument value as an enum: 175 | 176 | ```markdown 177 | + `-t, --type [VAL] {json|help|man}` Renderer type 178 | ``` 179 | 180 | ###### Default Value 181 | 182 | To specify a default value for the option use the `=` operator in the type: 183 | 184 | ```markdown 185 | + `-i, --indent [NUM] {Number=2}` Amount of indentation 186 | ``` 187 | 188 | The parsed option will have the `kind` property set to `Number` and the `value` property set to `2`. 189 | 190 | You can just specify the default value using: 191 | 192 | ```markdown 193 | + `-i, --indent [NUM] {=2}` Amount of indentation 194 | ``` 195 | 196 | In which case the `kind` property will be `undefined` and the `value` property is set to `2`. 197 | 198 | #### Commands 199 | 200 | Commands are declared in the same way as program arguments but under the *COMMANDS* heading: 201 | 202 | ```markdown 203 | # Name 204 | 205 | prg - short program summary 206 | 207 | # Commands 208 | 209 | + `ls, list` List tasks 210 | + `i, info` Print task information 211 | ``` 212 | 213 | They allow you to create complex programs with options specific to a command. 214 | 215 | Command files are loaded and compiled automatically following a naming convention. Using the above example to define the `list` command create a file named `prg-list.md`: 216 | 217 | ```markdown 218 | # Name 219 | 220 | list - list tasks 221 | 222 | # Options 223 | 224 | + `-a, --all` List all tasks 225 | + `-t=[TYPE...]` List tasks of TYPE 226 | ``` 227 | 228 | Will result in the compiled tree containing options specific to the `list` command. 229 | 230 | #### Identifiers 231 | 232 | When a program is created from a source markdown document each argument and command is given a key for the resulting map. This key is generated automatically by using the longest argument (or command) name and converting it to camel case. 233 | 234 | If you wish to use a fixed key you can add an identifier followed by a colon (`:`) to the beginning of the specification: 235 | 236 | ```markdown 237 | # Name 238 | 239 | prg - short program summary 240 | 241 | # Commands 242 | 243 | + `tasks: ls, list` List tasks 244 | 245 | # Options 246 | 247 | + `verbose: -v` Print more information 248 | ``` 249 | 250 | #### Manual Sections 251 | 252 | A heading that is not matched by any of the rules above is treated as a manual section: 253 | 254 | ```markdown 255 | # Name 256 | 257 | prg - short program summary 258 | 259 | # Environment 260 | 261 | The environment variable FOO changes the behaviour to `bar`. 262 | ``` 263 | 264 | The section ends when the next level one heading is encountered or the end of the file is reached. 265 | 266 | ### Synopsis Expansion 267 | 268 | Unless disabled the synopsis declaration is expanded for the `man` and `help` output types. 269 | 270 | #### Flags 271 | 272 | Use the notation `[flags]` (or ``) in the synopsis and it will be replaced with all short form (single character) flag options (for example: `-xvf`). 273 | 274 | #### Options 275 | 276 | Use the notation `[options]` (or ``) in the synopsis and it will be replaced with all option names that are not declared in the synopsis and were not expanded using the `[flags]` notation. 277 | 278 | #### Exclusive Options 279 | 280 | You should indicate mutually exclusive options using a vertical bar between option names. 281 | 282 | When compiling to JSON the synopsis is parsed and any mutually exclusive declarations are added to the output using the target option keys. 283 | 284 | It is a compiler error if the target option is not declared. 285 | 286 | #### Expansion Example 287 | 288 | Given a definition such as: 289 | 290 | ```markdown 291 | # Name 292 | 293 | prg - short program summary 294 | 295 | # Synopsis 296 | 297 | [flags] [options] [--xml|--html] 298 | 299 | # Options 300 | 301 | + `-X, --xml` Print as XML 302 | + `-H, --html` Print as HTML 303 | + `-V` Print more information 304 | + `-h, --help` Display help and exit 305 | + `--version` Print the version and exit 306 | ``` 307 | 308 | The synopsis is expanded to: 309 | 310 | ``` 311 | prg [-XHVh] [--help] [--version] [--xml|--html] 312 | ``` 313 | 314 | ### Compiling Programs 315 | 316 | To compile the markdown document to a JSON program descriptor run: 317 | 318 | ```shell 319 | mkcli -t json program.md 320 | ``` 321 | 322 | Now you have a JSON document that describes your program commands and options. 323 | 324 | ### Creating Documentation 325 | 326 | Once you have defined the program you will want to generate a man page and some help text. 327 | 328 | To create the help text run: 329 | 330 | ```shell 331 | mkcli -t help program.md 332 | ``` 333 | 334 | For a man page run: 335 | 336 | ```shell 337 | mkcli -t man program.md 338 | ``` 339 | 340 | #### Help Styles 341 | 342 | The default column help style (`col`) should suit most purposes however the other styles can be useful. The `list` style renders a list of the commands and options which is designed for when you have very long argument names or a few arguments that require long descriptions. 343 | 344 | The `cmd` style is a list of command names (options are not printed) designed to be used when a program has lots of commands and a command is required. Typically the program would show this help page when no command was specified to indicate to the user a command is required. 345 | 346 | Sometimes you may want very minimal help output that just includes the usage synopsis in which case use the `usage` style. 347 | 348 | #### Help Sections 349 | 350 | Sometimes when creating help files you may want to include a section from the manual, possibly you want to include an *Environment* section to show the environment variables your program recognises. 351 | 352 | Pass regular expression patterns using the `--section` option and if they match a section heading the section will be included in the help after the commands and options. 353 | 354 | To include an *Environment* section you could use: 355 | 356 | ```shell 357 | mkcli -t help -S env program.md 358 | ``` 359 | 360 | To include the *Environment* and *Bugs* sections you could use: 361 | 362 | ```shell 363 | mkcli -t help -S env -S bug program.md 364 | ``` 365 | 366 | Or if you prefer: 367 | 368 | ```shell 369 | mkcli -t help -S '(env|bug)' program.md 370 | ``` 371 | 372 | See the [help](#help) for more options available when creating help and man pages. 373 | 374 | ### Completion 375 | 376 | Completion scripts are currently available for zsh. To install a completion script for a program copy the script to a directory in `$fpath` or modify `~/.zshrc` to autoload the directory containing the completion script: 377 | 378 | ```zsh 379 | fpath=(/path/to/completion $fpath) 380 | ``` 381 | 382 | A full working completion example is the [notes](/test/fixtures/completion) test fixture. 383 | 384 | Sometimes you may wish to reload a completion for testing purposes: 385 | 386 | ```zsh 387 | unfunction _notes && autoload -U _notes 388 | ``` 389 | 390 | #### Actions 391 | 392 | Some option value specifications map to zsh completion functions: 393 | 394 | 395 | 396 | Such that an option specification such as: 397 | 398 | ```markdown 399 | + `-i, --input [file...]` Input files 400 | + `-o, --output ` Output directory 401 | ``` 402 | 403 | Will result in the `_files` completion function being called to complete file paths for the `--input` option and the `_directories` function for the `--output` option. Note that the ellipsis (...) multiple flag is respected so `--input` will be completed multiple times whilst `--output` will only complete once. 404 | 405 | For options that specify a list of types the `_values` completion function is called. 406 | 407 | ```markdown 408 | + `-t, --type=[TYPE] {json|yaml}` Output type 409 | ``` 410 | 411 | Results in automatic completion for the `--type` option to one of `json` or `yaml`. 412 | 413 | Actions are enclosed in double quotes (") so you may use single quotes and paired double quotes but not a single double quote which will generate an `unmatched "` zsh error. 414 | 415 | #### Synopsis Completion 416 | 417 | The program synopsis section is inspected and will use completion functions when a match is available, so a synopsis such as: 418 | 419 | ```markdown 420 | [options] [files...] 421 | ``` 422 | 423 | Will result in the _files completion function called, see above for the list of matches and completion functions. 424 | 425 | Sometimes you may need to create a custom completion list; you can set the info string of fenced code blocks in the synopsis section to inject scripts. The value may be either `zsh-locals` to inject code into the beginning of the body of the generated completion function and `zsh` to add to the list of completion actions. 426 | 427 | A real-world example is [mk](https://github.com/mkdoc/mkdoc#mk) ([program definition](https://raw.githubusercontent.com/mkdoc/mkdoc/master/doc/cli/mk.md) and [compiled completion script](https://github.com/mkdoc/mkdoc/blob/master/doc/zsh/_mk)) which completes on the available task names. 428 | 429 | #### Specification Completion 430 | 431 | You may wish to change the zsh action taken per option, this can be done by appending a colon and the zsh action to an option specification: 432 | 433 | ```markdown 434 | + `-p, --package=[FILE] :file:_files -g '+.json'` Package descriptor 435 | ``` 436 | 437 | Which will complete files with a `.json` extension for the `--package` option. 438 | 439 | #### Command Completion 440 | 441 | Commands are recursively added to the completion script; they are completed using the following rules: 442 | 443 | * Required commands (`` in the synopsis) will not list options by default. 444 | * Command options inherit from the global options. 445 | * Command options cascade to child options. 446 | * Rest pattern matches (`*: :file:_files` for example) are respected. 447 | 448 | It is recommended you use a program synopsis with the command first: 449 | 450 | ```markdown 451 | # Synopsis 452 | 453 | [options] [files...] 454 | ``` 455 | 456 | Or if the command is not required: 457 | 458 | ```markdown 459 | # Synopsis 460 | 461 | [command] [options] [files...] 462 | ``` 463 | 464 | Which is because command completion is terminated when an option is intermingled with the command hierarchy. Consider a program that has the command structure `notes > list > bug|todo|feature` if you present a command line such as: 465 | 466 | ```shell 467 | notes list --private 468 | ``` 469 | 470 | Completion will no longer be attempted on the `list` sub-commands. To put it another way *commands must be consecutive* for command completion to occur. 471 | 472 | -------------------------------------------------------------------------------- /doc/readme/help.md: -------------------------------------------------------------------------------- 1 | ## Help 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/readme/install.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ``` 4 | npm i mkcli --save 5 | ``` 6 | 7 | For the command line interface install [mkdoc][] globally (`npm i -g mkdoc`). 8 | -------------------------------------------------------------------------------- /doc/readme/license.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /doc/readme/links.md: -------------------------------------------------------------------------------- 1 | [mkdoc]: https://github.com/mkdoc/mkdoc 2 | [mkast]: https://github.com/mkdoc/mkast 3 | [through]: https://github.com/tmpfs/through3 4 | [commonmark]: http://commonmark.org 5 | [jshint]: http://jshint.com 6 | [jscs]: http://jscs.info 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var types = { 2 | json: 'json', 3 | help: 'help', 4 | man: 'man', 5 | zsh: 'zsh' 6 | }; 7 | 8 | /** 9 | * Creates documentation for command line interfaces. 10 | * 11 | * @private {function} cli 12 | * @param {Object} [opts] processing options. 13 | * @param {Function} [cb] callback function. 14 | * 15 | * @option {Readable} [input] input stream. 16 | * @option {Writable} [output] output stream. 17 | * 18 | * @returns an output stream. 19 | */ 20 | function cli(opts, cb) { 21 | opts = opts || {}; 22 | 23 | opts.type = opts.type || types.json; 24 | 25 | // do we need a compiler phase? 26 | var compiles = opts.type === cli.JSON || opts.type === cli.ZSH; 27 | 28 | opts.recursive = opts.recursive !== undefined 29 | ? opts.recursive : (compiles ? true : false); 30 | 31 | var stream = src(opts) 32 | , parser = stream 33 | , ast = require('mkast') 34 | , renderer 35 | , builder; 36 | 37 | try { 38 | renderer = dest(opts) 39 | }catch(e) { 40 | 41 | if(typeof cb === 'function') { 42 | return cb(e); 43 | } 44 | 45 | throw e; 46 | } 47 | 48 | if(!opts.input || !opts.output) { 49 | return stream; 50 | } 51 | 52 | // set up input stream 53 | stream = ast.parser(opts.input) 54 | .pipe(stream); 55 | 56 | if(compiles) { 57 | builder = compiler(opts); 58 | stream = stream.pipe(builder); 59 | } 60 | 61 | stream = stream.pipe(renderer) 62 | 63 | // convert output to newline delimited json 64 | if(opts.type !== cli.JSON) { 65 | stream = stream.pipe(ast.stringify()); 66 | } 67 | 68 | stream.pipe(opts.output); 69 | 70 | if(cb) { 71 | parser.once('error', cb); 72 | if(builder) { 73 | builder.once('error', cb); 74 | } 75 | opts.output 76 | .once('error', cb) 77 | .once('finish', cb); 78 | } 79 | 80 | return opts.output; 81 | } 82 | 83 | /** 84 | * Gets a source parser stream that transforms the incoming tree nodes into 85 | * parser state information. 86 | * 87 | * @function src 88 | * @param {Object} [opts] parser options. 89 | * 90 | * @returns a parser stream. 91 | */ 92 | function src(opts) { 93 | opts = opts || {}; 94 | var Parser = require('./lib/parser'); 95 | return new Parser(opts); 96 | } 97 | 98 | /** 99 | * Gets a compiler stream that transforms the parser state information to 100 | * a program definition. 101 | * 102 | * @function compiler 103 | * @param {Object} [opts] compiler options. 104 | * 105 | * @returns a compiler stream. 106 | */ 107 | function compiler(opts) { 108 | opts = opts || {}; 109 | var Compiler = require('./lib/compiler'); 110 | return new Compiler(opts); 111 | } 112 | 113 | 114 | /** 115 | * Gets a destination renderer stream. 116 | * 117 | * When no type is specified the JSON renderer is assumed. 118 | * 119 | * @function dest 120 | * @param {Object} [opts] renderer options. 121 | * 122 | * @option {String=json} type the renderer type. 123 | * 124 | * @returns a renderer stream of the specified type. 125 | */ 126 | function dest(opts) { 127 | opts = opts || {}; 128 | var type = opts.type || types.json 129 | 130 | if(!types[type]) { 131 | throw new Error('unknown output type: ' + type); 132 | } 133 | 134 | var Type = require('./lib/render/' + type) 135 | return new Type(opts); 136 | } 137 | 138 | /** 139 | * Load a program definition into a new program assigning the definition 140 | * properties to the program. 141 | * 142 | * Properties are passed by reference so if you modify the definition the 143 | * program is also modified. 144 | * 145 | * @function load 146 | * @param {Object} def the program definition. 147 | * @param {Object} [opts] program options. 148 | * 149 | * @returns a new program. 150 | */ 151 | //function load(def) { 152 | //var Program = require('./lib/program') 153 | //, prg = new Program(); 154 | //for(var k in def) { 155 | //prg[k] = def[k]; 156 | //} 157 | //return prg; 158 | //} 159 | 160 | /** 161 | * Load a program definition into a new program assigning the definition 162 | * properties to the program. 163 | * 164 | * Properties are passed by reference so if you modify the definition the 165 | * program is also modified. 166 | * 167 | * The callback function signature is `function(err, req)` where `req` is a 168 | * request object that contains state information for program execution. 169 | * 170 | * Plugins may decorate the request object with pertinent information that 171 | * does not affect the `target` object that receives the parsed arguments. 172 | * 173 | * @function run 174 | * @param {Object} src the source program or definition. 175 | * @param {Array} argv the program arguments. 176 | * @param {Object} [runtime] runtime configuration. 177 | * @param {Function} cb callback function. 178 | * 179 | * @returns a new program. 180 | */ 181 | //function run(src, argv, runtime, cb) { 182 | //var Program = require('./lib/program') 183 | //, runner = require('./lib/run'); 184 | 185 | //if(!(src instanceof Program)) { 186 | //src = load(src); 187 | //} 188 | 189 | //runner.call(src, argv, runtime, cb); 190 | //} 191 | 192 | var runtime = require('mkcli-runtime'); 193 | cli.load = runtime.load; 194 | cli.run = runtime.run; 195 | cli.camelcase = runtime.camelcase; 196 | 197 | //cli.load = load; 198 | cli.types = types; 199 | cli.src = src; 200 | cli.compiler = compiler; 201 | cli.dest = dest; 202 | //cli.run = run; 203 | //cli.camelcase = function() { 204 | //// lazy require 205 | //var camel = require('cli-argparse').camelcase; 206 | //return camel.apply(this, arguments); 207 | //} 208 | 209 | Object.keys(types).forEach(function(nm) { 210 | cli[nm.toUpperCase()] = nm; 211 | }); 212 | 213 | module.exports = cli; 214 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | var through = require('through3') 2 | , Program = require('mkcli-runtime/lib/program') 3 | , State = require('./state') 4 | , states = State.states; 5 | 6 | /** 7 | * Compiles the parser state information to a program definition. 8 | * 9 | * @constructor Compiler 10 | * @param {Object} opts processing options. 11 | * 12 | * @option {Object} program existing program or command. 13 | */ 14 | function Compiler(opts) { 15 | this.program = opts.program || new Program(); 16 | this.compact = opts.compact !== undefined ? opts.compact : true; 17 | } 18 | 19 | function transform(chunk, encoding, cb) { 20 | 21 | if(!chunk.data) { 22 | return cb(); 23 | } 24 | 25 | if(chunk.type === states.NAME) { 26 | this.program.name = chunk.data.name; 27 | // NOTE: don't overwrite command names when loading subcommands 28 | if(!this.program.names) { 29 | this.program.names = chunk.data.names; 30 | } 31 | this.program.summary = chunk.data.summary; 32 | }else if(chunk.type === states.SYNOPSIS) { 33 | this.program.synopsis = chunk.data.synopsis; 34 | this.program.zsh = chunk.data.zsh; 35 | }else if(chunk.type === states.DESCRIPTION) { 36 | this.program.description = chunk.data.literal; 37 | }else if(chunk.type === states.OPTIONS 38 | || chunk.type === states.COMMANDS) { 39 | this.program[chunk.type] = chunk.data[chunk.type]; 40 | // got section chunk 41 | }else{ 42 | this.program.sections = this.program.sections || []; 43 | this.program.sections.push(chunk); 44 | } 45 | cb(); 46 | } 47 | 48 | function exclusive() { 49 | var i 50 | , src 51 | , ptn 52 | , match 53 | , options = this.program.options 54 | , entry 55 | , names; 56 | 57 | // get the key for an option name 58 | function find(name) { 59 | for(var k in options) { 60 | if(~options[k].names.indexOf(name)) { 61 | return k; 62 | } 63 | } 64 | } 65 | 66 | function onName(name) { 67 | var key = find(name); 68 | if(!key) { 69 | throw new Error( 70 | 'missing key for synopsis option: ' + name + ' (option not found)'); 71 | } 72 | entry.keys.push(key); 73 | } 74 | 75 | // iterate all synopsis and extract exclusivity declarations 76 | if(this.program.synopsis) { 77 | for(i = 0;i < this.program.synopsis.length;i++) { 78 | src = this.program.synopsis[i]; 79 | ptn = /(\[|<)([^\]>]+)(\]|>)/g; 80 | while((match = ptn.exec(src)) !== null) { 81 | if(match[2] && /[|]/.test(match[2])) { 82 | this.program.exclusive = this.program.exclusive || []; 83 | entry = { 84 | literal: match[0], 85 | keys: [] 86 | } 87 | this.program.exclusive.push(entry); 88 | names = match[2].split(/\s*[|]\s*/); 89 | names.forEach(onName); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | function flatten(map) { 97 | for(var k in map) { 98 | delete map[k].literal; 99 | /* istanbul ignore else: guard against invalid literal */ 100 | if(map[k].description && map[k].description.literal !== undefined) { 101 | // remove first space from description literal 102 | map[k].description = map[k].description.literal.replace(/^ /, ''); 103 | } 104 | } 105 | return map; 106 | } 107 | 108 | function flush(cb) { 109 | try { 110 | this.exclusive(); 111 | }catch(e) { 112 | return cb(e); 113 | } 114 | 115 | if(this.compact) { 116 | if(Array.isArray(this.program.description)) { 117 | this.program.description = this.program.description.join('\n\n'); 118 | } 119 | this.program.commands = flatten(this.program.commands); 120 | this.program.options = flatten(this.program.options); 121 | delete this.program.sections; 122 | } 123 | this.push(this.program); 124 | cb(); 125 | } 126 | 127 | Compiler.prototype.exclusive = exclusive; 128 | 129 | module.exports = through.transform(transform, flush, {ctor: Compiler}); 130 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default format function invoked in the scope of the argument. 3 | * 4 | * @function formatter 5 | */ 6 | function formatter(opts) { 7 | var delimiter = opts.delimiter || ', ' 8 | , assign = opts.assign || '=' 9 | , str = this.names.join(delimiter) 10 | , extra; 11 | 12 | if(this.extra) { 13 | extra = this.extra.trim(); 14 | extra = extra.replace(/^=/, ''); 15 | extra = assign + extra; 16 | str += extra; 17 | } 18 | 19 | return str; 20 | } 21 | 22 | /** 23 | * Get a formatted argument string. 24 | * 25 | * If no `fmt` function is supplied the default formatter is used. 26 | * 27 | * The `fmt` function is invoked in the scope of the `arg`. 28 | * 29 | * @function format 30 | * @param {Object} arg the flag, option or command. 31 | * @param {Function} [fmt] custom format function. 32 | * @param {Object} [opts] formatting options. 33 | */ 34 | function format(arg, fmt, opts) { 35 | opts = opts || {}; 36 | 37 | if(typeof fmt === 'object') { 38 | opts = fmt; 39 | fmt = null; 40 | } 41 | 42 | if(typeof fmt !== 'function') { 43 | fmt = formatter; 44 | } 45 | return fmt.call(arg, opts); 46 | } 47 | 48 | format.formatter = formatter; 49 | 50 | module.exports = format; 51 | -------------------------------------------------------------------------------- /lib/optparse.js: -------------------------------------------------------------------------------- 1 | var Command = require('mkcli-runtime/lib/command') 2 | , Option = require('mkcli-runtime/lib/option') 3 | , Flag = require('mkcli-runtime/lib/flag'); 4 | 5 | var re = new RegExp( 6 | // optional leading key specification 7 | '^([\\w_\.]+:\\s*)?' 8 | // list of command/option names 9 | + '((?:-{0,2}[\\w\\[\\]|]+(?:,\\s*)?)+)' 10 | // option value 11 | + '((?:=|\\s+)[\\[<][^\\]>]+[\\]>])?' 12 | // optional type and default value specification 13 | + '(?:\\s*\\{([^}]+)\\})?' 14 | // zsh action specification 15 | + '(?:\\s*(:.+))?' 16 | ); 17 | 18 | /** 19 | * Parse a string literal to a command, flag or option. 20 | * 21 | * @static {function} parse 22 | * @param {String} input the option definition. 23 | * @param {Object} [opts] parsing options. 24 | * 25 | * @option {Boolean=false} command parse as a command. 26 | * @option {Boolean=true} camelcase convert automatic keys to camelcase. 27 | */ 28 | function parse(input, opts) { 29 | opts = opts || {}; 30 | 31 | var arg 32 | , res = {} 33 | , extra 34 | , pair; 35 | 36 | input.replace(re, function(match, key, names, value, info, zaction) { 37 | res.key = key; 38 | res.names = names; 39 | res.value = value; 40 | res.info = info; 41 | res.zaction = zaction; 42 | }) 43 | 44 | if(!res.names) { 45 | throw new TypeError('command or option declaration has no names'); 46 | } 47 | 48 | var names = res.names.split(/,\s*/) 49 | , keys; 50 | 51 | opts.camelcase = opts.camelcase !== undefined ? opts.camelcase : true; 52 | 53 | 54 | // work out automatic key generation from longest option name 55 | if(!res.key) { 56 | 57 | // strip leading hyphens from list of option names 58 | if(opts.camelcase) { 59 | keys = names.map(function strip(nm) { 60 | return nm.replace(/^-{1,2}/, ''); 61 | }) 62 | }else{ 63 | keys = names; 64 | } 65 | 66 | // sort keys by longest to shortest on `length` property 67 | keys = keys.sort(function(a, b) { 68 | a = a.length; 69 | b = b.length; 70 | if(a === b) { 71 | return 0; 72 | } 73 | return a < b ? 1 : -1; 74 | }) 75 | 76 | var camelcase = require('cli-argparse').camelcase; 77 | // do not include negation in automatic keys 78 | res.key = keys[0].replace(/^\[?no\]?-/,''); 79 | res.key = opts.camelcase ? camelcase(res.key) : res.key; 80 | 81 | }else{ 82 | res.key = res.key.replace(/:\s*$/, ''); 83 | } 84 | 85 | // no value specification so create a flag option 86 | if(opts.command) { 87 | arg = new Command(); 88 | }else if(!res.value) { 89 | arg = new Flag(); 90 | }else{ 91 | extra = res.value.trim(); 92 | arg = new Option(); 93 | arg.extra = res.value; 94 | arg.multiple = Boolean(~extra.indexOf('...')); 95 | arg.required = Boolean(~extra.indexOf('<') && ~extra.indexOf('>')) 96 | 97 | if(res.info) { 98 | // just the type info 99 | if(!~res.info.indexOf('=')) { 100 | arg.kind = res.info; 101 | }else{ 102 | pair = res.info.split('='); 103 | 104 | // set kind of option value 105 | if(pair[0]) { 106 | arg.kind = pair[0]; 107 | } 108 | 109 | // set default value for option, may be the empty string 110 | arg.value = pair[1]; 111 | } 112 | 113 | 114 | if(arg.kind) { 115 | // support enum style multiple type declarations 116 | if(~arg.kind.indexOf('|')) { 117 | arg.kind = arg.kind.split('|'); 118 | } 119 | } 120 | } 121 | } 122 | 123 | arg.literal = input; 124 | arg.key = res.key; 125 | arg.names = names; 126 | if(res.zaction) { 127 | arg.zaction = res.zaction; 128 | } 129 | 130 | // choose longest name 131 | var sorted = names.slice(0).sort(function(a, b) { 132 | a = a.length; 133 | b = b.length; 134 | if(a === b) {return 0;} 135 | return a < b ? 1 : -1; 136 | }) 137 | arg.name = sorted[0]; 138 | 139 | return arg; 140 | } 141 | 142 | module.exports = parse; 143 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var through = require('through3') 2 | , path = require('path') 3 | , fs = require('fs') 4 | , ast = require('mkast') 5 | , Node = ast.Node 6 | , walker = ast.NodeWalker 7 | , literal = walker.literal 8 | , collect = walker.collect 9 | , Compiler = require('./compiler') 10 | , State = require('./state') 11 | , optparse = require('./optparse') 12 | , format = require('./format') 13 | , states = State.states; 14 | 15 | /** 16 | * Reads the tree for a markdown document and pushes state information to 17 | * the stream. 18 | * 19 | * @constructor Parser 20 | * @param {Object} [opts] processing options. 21 | * 22 | * @option {Boolean=true} [synopsis] perform synopsis exapansion. 23 | * @option {Boolean} [recursive] recursively load command definitions. 24 | * 25 | */ 26 | function Parser(opts) { 27 | // current state 28 | this.state = undefined; 29 | 30 | this.match = {}; 31 | for(var k in states) { 32 | this.match[k.toLowerCase()] = new RegExp('^' + states[k] + '$', 'im'); 33 | } 34 | 35 | // secrion is defined for a string constant 36 | // we don't need to match on it 37 | delete this.match.section; 38 | 39 | // do we auto-expand synopsis 40 | this.synopsis = opts.synopsis !== undefined ? opts.synopsis : true; 41 | 42 | // buffer states while waiting for options/commands 43 | // used for synopsis expansion 44 | this.buffer = []; 45 | 46 | // have we seen any options 47 | this.options = false; 48 | 49 | // the state for the name section 50 | // so that we can test if it came first 51 | this.name = undefined; 52 | 53 | // current file being processed 54 | this.file = opts.file; 55 | 56 | // do we recursively load command definition files 57 | this.recursive = opts.recursive; 58 | 59 | // list of commands found - load subcommand definitions when available 60 | this.commands = undefined; 61 | } 62 | 63 | function transform(chunk, encoding, cb) { 64 | var lit 65 | , k; 66 | 67 | // start a section 68 | if(Node.is(chunk, Node.EOF) 69 | || Node.is(chunk, Node.DOCUMENT) 70 | || (Node.is(chunk, Node.HEADING) && chunk.level === 1)) { 71 | 72 | // finalize existing state on next level 1 73 | if(this.state) { 74 | 75 | if(!this.name) { 76 | return cb(new Error('name section must be declared first')); 77 | } 78 | 79 | try { 80 | this.finalize(this.state); 81 | }catch(e) { 82 | return cb(e); 83 | } 84 | } 85 | 86 | if(Node.is(chunk, Node.DOCUMENT) || Node.is(chunk, Node.EOF)) { 87 | 88 | // flush the synopsis buffer on EOF 89 | if(Node.is(chunk, Node.EOF) && this.buffer && this.buffer.length) { 90 | this.empty(); 91 | } 92 | 93 | this.push(chunk); 94 | 95 | // set on document 96 | if(!this.file && Node.is(chunk, Node.DOCUMENT)) { 97 | this.file = chunk.file; 98 | // clear on EOF 99 | }else{ 100 | if(this.recursive && this.commands) { 101 | return this.command(this.commands, cb); 102 | } 103 | } 104 | 105 | return cb(); 106 | } 107 | 108 | // does the header text match a known state? 109 | lit = literal(chunk); 110 | for(k in this.match) { 111 | if(this.match[k].test(lit)) { 112 | this.state = new State(k, chunk); 113 | break; 114 | } 115 | } 116 | 117 | // so we can test that the name section has been declared first 118 | if(this.state && this.state.type === states.NAME) { 119 | this.name = this.state; 120 | } 121 | 122 | // default state is custom man section 123 | if(!this.state) { 124 | this.state = new State(states.SECTION, chunk); 125 | } 126 | 127 | }else if(this.state) { 128 | this.state.nodes.push(chunk); 129 | }else{ 130 | return cb(new Error('content before name section ' + this.file)); 131 | } 132 | cb(); 133 | } 134 | 135 | /** 136 | * Parse options or commands. 137 | * 138 | * @private 139 | */ 140 | function parse(list, id) { 141 | var items = collect(list, Node.ITEM) 142 | , i 143 | // list item 144 | , item 145 | // declaration is the first inline code block 146 | , declaration 147 | // resulting option definition 148 | , opt 149 | // collection of description nodes 150 | , description 151 | // collection of item blocks 152 | , blocks 153 | // list of options in the order encountered 154 | , result = [] 155 | , map = {}; 156 | 157 | 158 | function onItemChild(block) { 159 | // only paragraphs for the moment 160 | if(Node.is(block, Node.PARAGRAPH)) { 161 | 162 | var code 163 | , isFullNode = typeof block.unlink === 'function'; 164 | 165 | // do not include the code specification 166 | // in the description 167 | if(block.firstChild === declaration) { 168 | code = block.firstChild; 169 | 170 | // NOTE: cannot delete firstChild when full AST node 171 | if(isFullNode) { 172 | code.unlink(); 173 | }else{ 174 | delete block.firstChild; 175 | block.firstChild = code.next; 176 | } 177 | } 178 | 179 | if(description.literal) { 180 | description.literal += '\n\n'; 181 | } 182 | 183 | description.literal += literal(block); 184 | 185 | // restore code node in tree 186 | if(code) { 187 | if(isFullNode) { 188 | block.prependChild(code); 189 | }else{ 190 | block.firstChild = code; 191 | } 192 | } 193 | } 194 | } 195 | 196 | for(i = 0;i < items.length;i++) { 197 | item = items[i]; 198 | 199 | if(!Node.is(item.firstChild.firstChild, Node.CODE)) { 200 | throw new Error( 201 | 'missing inline code specification for option or command'); 202 | } 203 | 204 | declaration = collect(item, Node.CODE)[0]; 205 | 206 | if(id === states.OPTIONS) { 207 | opt = optparse(declaration.literal); 208 | }else{ 209 | opt = optparse( 210 | declaration.literal, {command: true}); 211 | } 212 | 213 | // rewrite to formatted literal for output 214 | declaration.source = declaration.literal; 215 | declaration.literal = format(opt); 216 | 217 | description = { 218 | nodes: [], 219 | literal: '' 220 | } 221 | 222 | blocks = walker.children(item); 223 | blocks.forEach(onItemChild); 224 | opt.description = description; 225 | 226 | if(map[opt.key]) { 227 | throw new Error( 228 | 'duplicate key \'' + opt.key + '\' in section: ' + id); 229 | } 230 | 231 | map[opt.key] = opt; 232 | 233 | result.push(opt); 234 | } 235 | 236 | // state data for man renderer 237 | list.options = { 238 | map: map, 239 | list: result 240 | } 241 | 242 | return map; 243 | } 244 | 245 | function finalize(state) { 246 | var type = state.type 247 | , i 248 | , k 249 | , chunk 250 | , chunks 251 | , items 252 | // collect all command/option names so we can check 253 | // for duplicates 254 | , names = [] 255 | , map; 256 | 257 | function onListItem(item) { 258 | state.data.names.push(literal(item)); 259 | } 260 | 261 | function onName(nm) { 262 | if(~names.indexOf(nm)) { 263 | throw new Error( 264 | 'duplicate ' + type + ' name detected:' + nm); 265 | } 266 | } 267 | 268 | if(state.nodes.length > 1) { 269 | state.data = {}; 270 | switch(type) { 271 | case states.NAME: 272 | // skip the heading 273 | chunk = state.nodes[1]; 274 | if(!Node.is(chunk, Node.PARAGRAPH)) { 275 | throw new Error('name section must begin with a paragraph'); 276 | } 277 | 278 | state.data.name = literal(chunk); 279 | 280 | if(~state.data.name.indexOf(' - ')) { 281 | 282 | // extract short summary 283 | state.data.summary = 284 | state.data.name.substr(state.data.name.indexOf(' - ') + 1); 285 | 286 | state.data.name = 287 | state.data.name.substr(0, state.data.name.indexOf(' - ')); 288 | 289 | state.data.summary = 290 | state.data.summary.replace(/^\s*-\s*/, '').trim(); 291 | } 292 | 293 | if(!state.data.summary) { 294 | throw new Error('program summary is required ' + this.file); 295 | } 296 | 297 | state.data.name = state.data.name.trim(); 298 | 299 | if(/\s/.test(state.data.name)) { 300 | state.data.parents = state.data.name.split(/\s/); 301 | state.data.name = state.data.parents.pop(); 302 | } 303 | 304 | state.data.names = [state.data.name]; 305 | 306 | // skip the heading, add names from list declaration 307 | for(i = 1;i < state.nodes.length;i++) { 308 | chunk = state.nodes[i]; 309 | if(Node.is(chunk, Node.LIST)) { 310 | items = collect(chunk, Node.ITEM); 311 | items.forEach(onListItem); 312 | } 313 | } 314 | 315 | break; 316 | case states.DESCRIPTION: 317 | chunks = state.nodes.slice(1); 318 | 319 | state.data.literal = []; 320 | 321 | // collect paragraph elements 322 | for(i = 0;i < chunks.length;i++) { 323 | if(Node.is(chunks[i], Node.PARAGRAPH)) { 324 | state.data.literal.push(literal(chunks[i])); 325 | } 326 | } 327 | 328 | // all data without the heading 329 | state.data.nodes = chunks; 330 | break; 331 | case states.SYNOPSIS: 332 | chunks = state.nodes.slice(1); 333 | 334 | // collect code blocks for synopsis 335 | state.data.synopsis = []; 336 | 337 | // collect paragraph literals 338 | for(i = 0;i < chunks.length;i++) { 339 | chunk = chunks[i]; 340 | if(Node.is(chunk, Node.CODE_BLOCK)) { 341 | if(chunk.info && /^zsh/.test(chunk.info)) { 342 | state.data.zsh = state.data.zsh || []; 343 | state.data.zsh.push( 344 | { 345 | info: chunk.info, 346 | literal: chunk.literal.replace(/\n$/, '') 347 | }); 348 | }else{ 349 | state.data.synopsis.push(chunk.literal.replace(/\n$/, '')); 350 | } 351 | }else{ 352 | throw new Error('synopsis section may only contain code blocks'); 353 | } 354 | } 355 | 356 | // all data without the heading 357 | state.data.nodes = chunks; 358 | 359 | // pass reference to name state 360 | state.name = this.name; 361 | 362 | // overwrite flag with state information 363 | if(this.synopsis) { 364 | state.data.expand = true; 365 | this.synopsis = state; 366 | } 367 | 368 | break; 369 | case states.COMMANDS: 370 | case states.OPTIONS: 371 | chunks = state.nodes.slice(1); 372 | 373 | // collect lists for commands and options 374 | state.data[type] = {}; 375 | 376 | // collect paragraph literals 377 | for(i = 0;i < chunks.length;i++) { 378 | chunk = chunks[i]; 379 | if(Node.is(chunk, Node.LIST)) { 380 | map = this.parse(chunk, type); 381 | for(k in map) { 382 | 383 | // check for duplicate key 384 | if(state.data[type][k]) { 385 | throw new Error( 386 | 'duplicate key \'' + k + '\' in section: ' + type); 387 | } 388 | 389 | // check for duplicate command or option name 390 | map[k].names.forEach(onName); 391 | 392 | names = names.concat(map[k].names); 393 | state.data[type][k] = map[k]; 394 | } 395 | 396 | } 397 | } 398 | 399 | // all data without the heading 400 | state.data.nodes = chunks; 401 | 402 | // got the options 403 | if(type === states.OPTIONS) { 404 | this.options = state; 405 | } 406 | 407 | break; 408 | default: 409 | // NOTE: no custom parsing for other sections 410 | break; 411 | } 412 | } 413 | 414 | // keep reference so we can load sub-command definitions 415 | if(type === states.COMMANDS && state.data) { 416 | this.commands = state.data.commands; 417 | } 418 | 419 | // when expanding synopsis buffer nodes until 420 | // we have options 421 | if(this.synopsis && this.buffer) { 422 | this.buffer.push(state); 423 | // we have to flush when we have options or when 424 | // the first section is encountered to cater for the 425 | // scenario when a program has no options (the buffer would not be flushed) 426 | if(type === states.SECTION || this.options) { 427 | this.empty(); 428 | } 429 | }else{ 430 | this.push(state); 431 | } 432 | 433 | this.state = null; 434 | } 435 | 436 | function empty() { 437 | var i; 438 | 439 | if(this.synopsis && this.options) { 440 | this.synopsis.options = this.options; 441 | } 442 | 443 | for(i = 0;i < this.buffer.length;i++) { 444 | this.push(this.buffer[i]); 445 | } 446 | this.buffer = null; 447 | } 448 | 449 | function command(map, cb) { 450 | 451 | /* istanbul ignore next: tough to mock no file info */ 452 | if(!this.file) { 453 | // cannot autoload with no parent file information 454 | return cb(); 455 | } 456 | 457 | var k 458 | , ptn = /([^\.]+)\.(.*)$/ 459 | , file = this.file 460 | , name = path.basename(file) 461 | , dir = path.dirname(file) 462 | , id = name.replace(ptn, '$1') 463 | , ext = name.replace(ptn, '$2') 464 | , lookup = {} 465 | , files = [] 466 | , readable = [] 467 | , recursive = this.recursive 468 | , nm; 469 | 470 | // build list of potential subcommand files 471 | for(k in map) { 472 | nm = id + '-' + k + '.' + ext; 473 | nm = path.join(dir, nm); 474 | files.push(nm); 475 | // lookup table of files to command key 476 | lookup[nm] = k; 477 | } 478 | 479 | function load() { 480 | var file = readable.shift(); 481 | if(!file) { 482 | return cb(); 483 | } 484 | 485 | fs.readFile(file, function(err, contents) { 486 | /* istanbul ignore next: not going to mock io error */ 487 | if(err) { 488 | return cb(err); 489 | } 490 | 491 | var reader = ast.src('' + contents) 492 | , Parser = module.exports 493 | , parser = new Parser({file: file, recursive: recursive}) 494 | , compiler = new Compiler({program: map[lookup[file]]}); 495 | reader.pipe(parser).pipe(compiler); 496 | parser.once('error', cb); 497 | compiler.once('finish', load); 498 | }); 499 | 500 | } 501 | 502 | function done(err) { 503 | /* istanbul ignore next */ 504 | if(err) { 505 | return cb(err); 506 | } 507 | if(readable.length) { 508 | load(); 509 | }else{ 510 | cb(); 511 | } 512 | } 513 | 514 | // test for command files that exist 515 | function test() { 516 | var file = files.shift(); 517 | if(!file) { 518 | return done(); 519 | } 520 | fs.stat(file, function(err, stats) { 521 | /* istanbul ignore next */ 522 | if(err && err.code !== 'ENOENT') { 523 | return done(err); 524 | }else if(err) { 525 | return test(); 526 | } 527 | 528 | /* istanbul ignore else: not going to mock directory path */ 529 | if(stats.isFile()) { 530 | readable.push(file); 531 | } 532 | 533 | test(); 534 | }) 535 | } 536 | 537 | test(); 538 | 539 | this.file = null; 540 | } 541 | 542 | Parser.prototype.parse = parse; 543 | Parser.prototype.empty = empty; 544 | Parser.prototype.command = command; 545 | Parser.prototype.finalize = finalize; 546 | 547 | module.exports = through.transform(transform, {ctor: Parser}); 548 | -------------------------------------------------------------------------------- /lib/render/help.js: -------------------------------------------------------------------------------- 1 | var through = require('through3') 2 | , wordwrap = require('wordwrap') 3 | , repeat = require('string-repeater') 4 | , ast = require('mkast') 5 | , Node = ast.Node 6 | , literal = ast.NodeWalker.literal 7 | , EOL = require('os').EOL 8 | , LEFT = 'left' 9 | , RIGHT = 'right' 10 | , SPACE = ' ' 11 | , period = /\.$/ 12 | , format = require('../format') 13 | , State = require('../state') 14 | , states = State.states 15 | , synopsis = require('../synopsis') 16 | , styles = { 17 | col: 'col', 18 | list: 'list', 19 | cmd: 'cmd', 20 | usage: 'usage' 21 | }; 22 | 23 | /** 24 | * Transforms a program definition to plain text help. 25 | * 26 | * @constructor Help 27 | * @param {Object} opts renderer options. 28 | * 29 | * @option {String=col} [style] output style. 30 | * @option {String} [indent] initial indent for options and commands. 31 | * @option {String} [gap] gap between names and descriptions. 32 | * @option {Number=80} [cols] maximum column width for the output. 33 | * @option {Number=22} [split] column to split names and descriptions. 34 | * @option {Number} [desc] desc number of description paragraphs to include. 35 | * @option {String=Usage: } [usage] prefix for usage output. 36 | * @option {Boolean=true} [summarize] show summary below usage. 37 | * @option {Object} [pkg] package descriptor with additional information. 38 | * @option {Function} value custom function for default value. 39 | * @option {Function} kind custom function for type information output. 40 | * @option {Function|Boolean=false} header use custom or default header. 41 | * @option {Function|Boolean=false} footer use custom or default footer. 42 | * @option {Boolean=false} colon append a colon to headings. 43 | * @option {Boolean=false} newline without a header print a leading newline. 44 | * @option {Function} [footer] override default footer output. 45 | * @option {String} [align] align argument names `left` or `right`. 46 | * @option {Array} [section] list of regexp patterns for sections to include. 47 | * @option {String} [eol] end of line character(s). 48 | */ 49 | function Help(opts) { 50 | 51 | this.pkg = opts.pkg; 52 | this.eol = opts.eol || EOL; 53 | this.indent = opts.indent || SPACE + SPACE; 54 | this.gap = opts.gap || SPACE; 55 | this.cols = opts.cols || 80; 56 | this.split = opts.split || 26; 57 | this.usage = opts.usage !== undefined ? '' + opts.usage : 'Usage: '; 58 | this.value = opts.value !== undefined ? opts.value : false; 59 | this.kind = opts.kind !== undefined ? opts.kind : false; 60 | this.header = opts.header !== undefined ? opts.header : false; 61 | this.footer = opts.footer !== undefined ? opts.footer : false; 62 | this.colon = opts.colon !== undefined ? opts.colon : false; 63 | this.newline = opts.newline !== undefined ? opts.newline : false; 64 | this.summarize = opts.summarize !== undefined ? opts.summarize : true; 65 | this.align = opts.align === LEFT || opts.align === RIGHT 66 | ? opts.align : LEFT; 67 | 68 | this.desc = typeof opts.desc === 'number' && !isNaN(opts.desc) 69 | ? Math.abs(opts.desc) : Number.MAX_VALUE; 70 | 71 | this.section = Array.isArray(opts.section) ? opts.section : []; 72 | 73 | this.style = opts.style !== undefined ? opts.style : styles.col; 74 | 75 | // use default on bad style id 76 | if(!styles[this.style]) { 77 | this.style = styles.col; 78 | } 79 | 80 | // state variables 81 | this.name = undefined; 82 | 83 | // default wordwrap 84 | this.wrap = wordwrap(0, this.cols); 85 | } 86 | 87 | function transform(chunk, encoding, cb) { 88 | 89 | var i 90 | , container 91 | , wrap = this.wrap; 92 | 93 | // got state info 94 | if(chunk.data) { 95 | 96 | if(this.style === styles.usage 97 | && chunk.type !== states.NAME 98 | && chunk.type !== states.SYNOPSIS) { 99 | return cb(); 100 | } 101 | 102 | if(chunk.type === states.NAME) { 103 | this.name = chunk.data.name; 104 | this.summary = chunk.data.summary; 105 | header.call(this); 106 | }else if(chunk.type === states.SYNOPSIS) { 107 | container = Node.createNode(Node.PARAGRAPH); 108 | 109 | // fixed synopsis for `cmd` style 110 | if(this.style === styles.cmd) { 111 | if(!this.header) { 112 | container.appendChild(Node.createNode(Node.LINEBREAK)); 113 | } 114 | container.appendChild( 115 | Node.createNode(Node.TEXT, 116 | {literal: this.usage + this.name + ' '})); 117 | // buffered options for synopsis expansion 118 | }else if(chunk.data.expand) { 119 | container.appendChild( 120 | Node.createNode(Node.TEXT, 121 | {literal: 122 | this.usage 123 | + synopsis( 124 | chunk, {cols: this.cols, indent: this.usage.length})})); 125 | // print raw synopsis information 126 | }else{ 127 | var msg; 128 | wrap = wordwrap( 129 | this.usage.length 130 | + chunk.name.data.name.length + 1, this.cols); 131 | 132 | for(i = 0;i < chunk.data.synopsis.length;i++) { 133 | msg = chunk.data.synopsis[i].trim(); 134 | msg = this.name + ' ' + msg; 135 | if(!i) { 136 | msg = this.usage + msg; 137 | } 138 | container.appendChild( 139 | Node.createNode( 140 | Node.TEXT, {literal: wrap(msg).replace(/^\s+/, '')})); 141 | 142 | if(i < chunk.data.synopsis.length - 1) { 143 | container.appendChild(Node.createNode(Node.LINEBREAK)); 144 | } 145 | } 146 | } 147 | this.push(container); 148 | 149 | if(this.summarize && this.summary) { 150 | this.sum(); 151 | } 152 | 153 | }else if(chunk.type === states.DESCRIPTION && this.style !== styles.cmd) { 154 | wrap = wordwrap(this.indent.length, this.cols); 155 | for(i = 0;i < chunk.data.literal.length;i++) { 156 | if(i >= this.desc) { 157 | break; 158 | } 159 | container = Node.createNode(Node.PARAGRAPH); 160 | container.appendChild( 161 | Node.createNode(Node.TEXT, {literal: wrap(chunk.data.literal[i])})); 162 | this.push(container); 163 | } 164 | }else if(chunk.type === states.OPTIONS 165 | || chunk.type === states.COMMANDS) { 166 | 167 | // do not print options when `cmd` style 168 | if(this.style === styles.cmd && chunk.type === states.OPTIONS) { 169 | return cb(); 170 | } 171 | 172 | if(this.style === styles.cmd) { 173 | this.push(Node.createNode( 174 | Node.TEXT, {literal: 'where is one of:'})); 175 | this.push(Node.createNode(Node.LINEBREAK)); 176 | }else{ 177 | 178 | // print heading 179 | this.heading(chunk.nodes[0]); 180 | } 181 | 182 | // print option of command list 183 | container = Node.createNode(Node.PARAGRAPH); 184 | container.appendChild( 185 | Node.createNode(Node.TEXT, 186 | {literal: this.render(chunk.data[chunk.type], chunk)})); 187 | this.push(container); 188 | }else if(chunk.type === states.SECTION && chunk.nodes.length) { 189 | var include = false; 190 | for(i = 0;i < this.section.length;i++) { 191 | if((this.section[i] instanceof RegExp) 192 | && this.section[i].test(literal(chunk.nodes[0]))) { 193 | include = true; 194 | break; 195 | } 196 | } 197 | if(include) { 198 | wrap = wordwrap(this.indent.length, this.cols); 199 | for(i = 0;i < chunk.nodes.length;i++) { 200 | if(Node.is(chunk.nodes[i], Node.HEADING)) { 201 | // print heading 202 | this.heading(chunk.nodes[i]); 203 | }else{ 204 | container = Node.createNode(Node.PARAGRAPH); 205 | container.appendChild( 206 | Node.createNode(Node.TEXT, 207 | {literal: wrap(literal(chunk.nodes[i]))})); 208 | this.push(container); 209 | } 210 | } 211 | } 212 | } 213 | // got raw ast chunk: document, eof etc. 214 | }else{ 215 | this.push(chunk); 216 | } 217 | 218 | cb(); 219 | } 220 | 221 | function render(map, node) { 222 | return this[this.style](map, node); 223 | } 224 | 225 | function col(map) { 226 | var columns 227 | , max 228 | , name 229 | , desc 230 | , len 231 | , wrap 232 | , pad 233 | , over 234 | , maximum = this.split 235 | , buf = ''; 236 | 237 | columns = this.getColumns(map); 238 | max = columns.max; 239 | 240 | for(var i = 0;i < columns.cols.length;i++) { 241 | name = columns.cols[i].name; 242 | desc = columns.cols[i].description; 243 | len = name.length; 244 | 245 | pad = repeat(SPACE, 246 | this.split - name.length - this.indent.length - this.gap.length); 247 | 248 | over = this.indent.length + name.length + this.gap.length > maximum; 249 | 250 | wrap = wordwrap( 251 | this.split, 252 | this.cols); 253 | 254 | desc = wrap(desc); 255 | 256 | // over the maximum length 257 | if(!over) { 258 | desc = desc.replace(/^\s+/, ''); 259 | }else{ 260 | desc = desc.replace(/^ /, ''); 261 | } 262 | 263 | buf += this.indent; 264 | if(this.align === LEFT) { 265 | buf += name; 266 | buf += pad; 267 | }else{ 268 | buf += pad; 269 | buf += name; 270 | } 271 | if(!over) { 272 | buf += this.gap; 273 | }else{ 274 | buf += this.eol; 275 | } 276 | buf += desc; 277 | if(i < columns.cols.length - 1) { 278 | buf += this.eol; 279 | } 280 | } 281 | 282 | return buf; 283 | } 284 | 285 | function list(map) { 286 | var buf = '' 287 | , wrap = wordwrap(this.indent.length * 2, this.cols) 288 | , columns = this.getColumns(map) 289 | , i 290 | , name 291 | , desc; 292 | 293 | for(i = 0;i < columns.cols.length;i++) { 294 | name = columns.cols[i].name; 295 | desc = columns.cols[i].description; 296 | desc = desc.replace(/^ /, ''); 297 | desc = desc.replace(/\n$/, ''); 298 | buf += this.indent + name + this.eol; 299 | buf += wrap(desc); 300 | if(i < columns.cols.length - 1) { 301 | buf += this.eol + this.eol; 302 | } 303 | } 304 | return buf; 305 | } 306 | 307 | function cmd(map) { 308 | var list = [] 309 | , wrap = wordwrap(this.indent.length * 2, this.cols) 310 | 311 | // order with longest command names first 312 | for(var k in map) { 313 | list = list.concat(map[k].names); 314 | } 315 | 316 | // sort alphabetically 317 | list = list.sort(); 318 | return wrap(list.join(', ')); 319 | } 320 | 321 | function getColumns(target, fmt, opts) { 322 | var o = [] 323 | , k 324 | , arg 325 | , name 326 | , max = 0; 327 | 328 | for(k in target) { 329 | arg = target[k]; 330 | name = format(arg, fmt, opts); 331 | max = Math.max(max, name.length); 332 | o.push( 333 | { 334 | name: name, 335 | description: this.getDescription(arg) 336 | } 337 | ); 338 | } 339 | return {max: max, cols: o}; 340 | } 341 | 342 | function getDescription(arg) { 343 | var desc = arg.description.literal 344 | , val 345 | , type; 346 | // append default value to option description 347 | if(arg.value !== undefined) { 348 | val = value.call(this, arg, desc); 349 | } 350 | 351 | if(arg.kind !== undefined) { 352 | type = kind.call(this, arg, desc); 353 | } 354 | 355 | if(type) { 356 | desc += type; 357 | } 358 | 359 | if(val) { 360 | desc += val; 361 | } 362 | 363 | return desc; 364 | } 365 | 366 | function header() { 367 | 368 | if(typeof this.header === 'function') { 369 | return this.header.call(this); 370 | }else if(!this.header) { 371 | if(this.newline) { 372 | this.push(Node.createNode(Node.TEXT, {literal: '\n'})); 373 | } 374 | return; 375 | } 376 | 377 | // print program name 378 | var container = Node.createNode(Node.PARAGRAPH) 379 | container.appendChild(Node.createNode(Node.TEXT, {literal: this.name})); 380 | this.push(container); 381 | 382 | // print summary 383 | this.sum(); 384 | } 385 | 386 | function sum() { 387 | // title case summary 388 | var summary = this.summary.charAt(0).toUpperCase() + this.summary.substr(1) 389 | , wrap = wordwrap(this.indent.length, this.cols); 390 | if(!/\.$/.test(summary)) { 391 | summary += '.'; 392 | } 393 | var container = Node.createNode(Node.PARAGRAPH); 394 | container.appendChild( 395 | Node.createNode(Node.TEXT, {literal: wrap(summary)})); 396 | this.push(container); 397 | } 398 | 399 | function footer() { 400 | 401 | if(typeof this.footer === 'function') { 402 | return this.footer.call(this); 403 | }else if(!this.footer || !this.pkg) { 404 | return; 405 | } 406 | 407 | var hasInfo = this.pkg.name && this.pkg.version; 408 | 409 | if(hasInfo) { 410 | this.push( 411 | Node.createNode( 412 | Node.TEXT, {literal: this.pkg.name + '@' + this.pkg.version})); 413 | if(this.pkg.homepage) { 414 | this.push( 415 | Node.createNode( 416 | Node.TEXT, {literal: ' ' + this.pkg.homepage})); 417 | } 418 | this.push(Node.createNode(Node.LINEBREAK)); 419 | } 420 | } 421 | 422 | function value(opt, description) { 423 | if(typeof this.value === 'function') { 424 | return this.value.call(this, opt, description); 425 | } 426 | 427 | if(period.test(description)) { 428 | return '\nDefault: ' + opt.value; 429 | }else{ 430 | return ' (default: ' + opt.value + ')'; 431 | } 432 | } 433 | 434 | function kind(opt, description) { 435 | if(typeof this.kind === 'function') { 436 | return this.kind.call(this, opt, description); 437 | } 438 | 439 | if(Array.isArray(opt.kind)) { 440 | if(period.test(description)) { 441 | return '\n' + opt.kind.join(' | '); 442 | }else{ 443 | return ' (' + opt.kind.join('|') + ')'; 444 | } 445 | } 446 | } 447 | 448 | function heading(node) { 449 | var lit = literal(node); 450 | if(this.colon) { 451 | lit += typeof this.colon === 'string' ? this.colon : ':'; 452 | } 453 | this.push(Node.createNode(Node.TEXT, {literal: lit})); 454 | this.push(Node.createNode(Node.LINEBREAK)); 455 | } 456 | 457 | function flush(cb) { 458 | footer.call(this); 459 | cb(); 460 | } 461 | 462 | Help.prototype.render = render; 463 | Help.prototype.heading = heading; 464 | Help.prototype.col = col; 465 | Help.prototype.list = list; 466 | Help.prototype.cmd = cmd; 467 | Help.prototype.sum = sum; 468 | Help.prototype.getColumns = getColumns; 469 | Help.prototype.getDescription = getDescription; 470 | 471 | module.exports = through.transform(transform, flush, {ctor: Help}); 472 | -------------------------------------------------------------------------------- /lib/render/json.js: -------------------------------------------------------------------------------- 1 | var through = require('through3') 2 | , EOL = require('os').EOL; 3 | 4 | /** 5 | * Render a program definition to JSON. 6 | * 7 | * @constructor Json 8 | * @param {Object} opts renderer options. 9 | * 10 | * @option {Number=2} [indent] number of spaces to indent. 11 | */ 12 | function Json(opts) { 13 | this.indent = opts.indent !== undefined ? opts.indent : 2; 14 | } 15 | 16 | function transform(chunk, encoding, cb) { 17 | this.push(JSON.stringify(chunk, undefined, this.indent) + EOL); 18 | cb(); 19 | } 20 | 21 | module.exports = through.transform(transform, {ctor: Json}); 22 | -------------------------------------------------------------------------------- /lib/render/man.js: -------------------------------------------------------------------------------- 1 | var through = require('through3') 2 | , ast = require('mkast') 3 | , Node = ast.Node 4 | , walker = ast.NodeWalker 5 | , wordwrap = require('wordwrap') 6 | , states = require('../state').states 7 | , synopsis = require('../synopsis') 8 | 9 | /** 10 | * Transforms parser state information to nodes that reflect a final man 11 | * page output for the program. 12 | * 13 | * @constructor Man 14 | * @param {Object} opts renderer options. 15 | * 16 | * @option {Boolean=false} [preserve] do not upper case level one headings. 17 | * @option {Number=80} [cols] column to wrap synopsis. 18 | */ 19 | function Man(opts) { 20 | this.preserve = opts.preserve !== undefined ? opts.preserve : false; 21 | this.cols = opts.cols || 80; 22 | } 23 | 24 | function transform(chunk, encoding, cb) { 25 | var i 26 | , nodes 27 | , node 28 | , para = Node.createNode(Node.PARAGRAPH) 29 | , text 30 | , wrap; 31 | 32 | if(chunk.nodes) { 33 | nodes = chunk.nodes; 34 | if(chunk.type === states.SYNOPSIS) { 35 | // push the heading 36 | if(!this.preserve) { 37 | this.upper(nodes[0]); 38 | } 39 | this.push(nodes[0]); 40 | 41 | wrap = wordwrap( 42 | chunk.name.data.name.length + 1, this.cols); 43 | 44 | // perform synopsis expansion 45 | if(chunk.data.expand) { 46 | text = Node.createNode( 47 | Node.TEXT, {literal: synopsis(chunk, {wrap: wrap})}); 48 | para.appendChild(text); 49 | this.push(para); 50 | // raw synopsis - no expansion 51 | }else{ 52 | // use synopsis nodes (don't include zsh completion blocks) 53 | nodes = chunk.data.synopsis; 54 | for(i = 0;i < nodes.length;i++) { 55 | node = chunk.name.data.name + ' ' + nodes[i]; 56 | para = Node.createNode(Node.PARAGRAPH); 57 | text = Node.createNode(Node.TEXT, {literal: node}); 58 | para.appendChild(text); 59 | this.push(para); 60 | } 61 | } 62 | }else if(chunk.type === states.OPTIONS) { 63 | this.options(chunk); 64 | }else{ 65 | for(i = 0;i < nodes.length;i++) { 66 | node = nodes[i]; 67 | if(!this.preserve && Node.is(node, Node.HEADING) && node.level === 1) { 68 | this.upper(node); 69 | } 70 | this.push(node); 71 | } 72 | } 73 | }else{ 74 | this.push(chunk); 75 | } 76 | cb(); 77 | } 78 | 79 | function options(state) { 80 | var nodes = state.nodes 81 | , i 82 | , j 83 | , list 84 | , item 85 | , para 86 | , opt; 87 | 88 | // push the heading 89 | if(!this.preserve) { 90 | this.upper(nodes[0]); 91 | } 92 | this.push(nodes[0]); 93 | 94 | // push the rewritten nodes on to the stream 95 | for(i = 1;i < nodes.length;i++) { 96 | list = nodes[i]; 97 | if(!(list instanceof Node)) { 98 | list = Node.deserialize(nodes[i]); 99 | } 100 | j = 0; 101 | item = list.firstChild; 102 | while(item) { 103 | 104 | para = item.firstChild; 105 | 106 | // get parsed option declaration 107 | opt = nodes[i].options.list[j]; 108 | 109 | // NOTE: must rewrite the literal - do not remove the node! 110 | para.firstChild.literal = opt.names.join(', '); 111 | 112 | if(opt.extra) { 113 | var extra = Node.createNode(Node.EMPH) 114 | , ptn = /^([^\w]+)(\w+)([^\w].*)$/; 115 | 116 | if(!ptn.test(opt.extra)) { 117 | para.firstChild.insertAfter( 118 | Node.createNode(Node.TEXT, {literal: opt.extra})); 119 | 120 | para.firstChild.next.insertAfter(Node.createNode(Node.LINEBREAK)) 121 | }else{ 122 | 123 | para.firstChild.insertAfter(Node.createNode(Node.SOFTBREAK)); 124 | 125 | // final plain text part 126 | para.firstChild.insertAfter( 127 | Node.createNode( 128 | Node.TEXT, {literal: opt.extra.replace(ptn, '$3')})); 129 | 130 | // add emphasis - underline FILE etc 131 | extra.appendChild( 132 | Node.createNode( 133 | Node.TEXT, 134 | {literal: opt.extra.replace(ptn, '$2')} 135 | ) 136 | ) 137 | 138 | para.firstChild.insertAfter(extra); 139 | 140 | // initial plain text part 141 | para.firstChild.insertAfter( 142 | Node.createNode( 143 | Node.TEXT, {literal: opt.extra.replace(ptn, '$1')})); 144 | } 145 | }else{ 146 | para.firstChild.insertAfter(Node.createNode(Node.SOFTBREAK)); 147 | } 148 | 149 | item = item.next; 150 | j++; 151 | } 152 | 153 | this.push(list); 154 | } 155 | } 156 | 157 | function upper(node) { 158 | var kids = walker.children(node); 159 | kids.forEach(function(node) { 160 | /* istanbul ignore next: tough to mock no literal, softbreak? */ 161 | if(node.literal) { 162 | node.literal = node.literal.toUpperCase(); 163 | } 164 | }) 165 | } 166 | 167 | Man.prototype.options = options; 168 | Man.prototype.upper = upper; 169 | 170 | module.exports = through.transform(transform, {ctor: Man}); 171 | -------------------------------------------------------------------------------- /lib/render/zsh.js: -------------------------------------------------------------------------------- 1 | var through = require('through3') 2 | , ast = require('mkast') 3 | , Node = ast.Node 4 | , EOL = require('os').EOL 5 | , COMMAND = '' 6 | , map = { 7 | user: ':user:_users', 8 | group: ':group:_groups', 9 | host: ':host:_hosts', 10 | domain: ':domain:_domains', 11 | file: ':file:_files', 12 | dir: ':directory:_directories', 13 | url: ':url:_urls' 14 | } 15 | , keys = Object.keys(map) 16 | , ptn = {}; 17 | 18 | // build patterns, iterating the action keys 19 | keys.forEach(function(id) { 20 | ptn[id] = new RegExp( 21 | '((\\[|<)?' + id + 's?(\\.\\.\\.)?(>|\\])?)(.*)', 'i'); 22 | }) 23 | 24 | /** 25 | * Render a program definition as a zsh completion file. 26 | * 27 | * @constructor Zsh 28 | * @param {Object} opts renderer options. 29 | * 30 | * @option {String} indent indentation for the output script. 31 | */ 32 | function Zsh(opts) { 33 | this.indent = opts.indent || ' '; 34 | } 35 | 36 | /** 37 | * Stream transform implementation. 38 | * 39 | * @private {function} transform 40 | */ 41 | function transform(chunk, encoding, cb) { 42 | var buf = this.compdef(chunk) 43 | 44 | // open function 45 | buf += this.main(chunk); 46 | 47 | // render body 48 | buf += this.command(chunk, this.indent, 0); 49 | 50 | // close function 51 | buf += this.main(chunk, true); 52 | 53 | // push as a plain text node 54 | this.push(Node.createNode(Node.TEXT, {literal: buf})); 55 | 56 | cb(); 57 | } 58 | 59 | /** 60 | * Get the main program completion function. 61 | * 62 | * @function main 63 | * @param {Object} program program descriptor. 64 | * @param {Boolean} end whether to close the function. 65 | * 66 | * @returns String function declaration. 67 | */ 68 | function main(program, end) { 69 | var name = '_' + program.name 70 | , buf = ''; 71 | 72 | if(end) { 73 | // default catch all when nothing else matched 74 | buf += this.indent + '(( $ret == 1 )) && '; 75 | buf += this.args(this.options({}), this.indent).replace(/^\s+/, ''); 76 | 77 | // close the function 78 | buf += this.indent + 'return $ret;' + EOL + '}' 79 | 80 | // invoke the function 81 | buf += EOL + EOL + name + ' "$@"'; 82 | }else{ 83 | 84 | // function head - declare local variables 85 | buf = name + '(){' + EOL 86 | + this.indent 87 | + 'typeset -A opt_args;' + EOL 88 | + this.indent 89 | + 'local context state state_descr line ret=1;' + EOL 90 | + this.indent 91 | + 'local actions options commands;' + EOL; 92 | } 93 | return buf; 94 | } 95 | 96 | /** 97 | * Get the completion definition heading. 98 | * 99 | * @function compdef 100 | * @param {Object} program program descriptor. 101 | * 102 | * @returns String completion definition. 103 | */ 104 | function compdef(program) { 105 | return '#compdef ' + program.names.join(' ') + EOL; 106 | } 107 | 108 | /** 109 | * Get an array declaration with the specified name and list body content. 110 | * 111 | * @function array 112 | * @param {String} name variable name. 113 | * @param {Array} list body contents. 114 | * @param {String} lead leading whitespace. 115 | * 116 | * @returns String array variable declaration. 117 | */ 118 | function array(name, list, lead, append) { 119 | var buf = '' 120 | , opts = {returns: false, keyword: false, esc: false}; 121 | 122 | buf += lead + name + '=(' + EOL; 123 | buf += this.args(list, lead, opts); 124 | if(append) { 125 | buf += EOL + lead + this.indent + '$' + name; 126 | } 127 | buf += EOL + lead + ')' + EOL; 128 | 129 | return buf; 130 | } 131 | 132 | function command(program, lead, depth, opts) { 133 | opts = opts || {}; 134 | depth = depth || 0; 135 | lead = lead || ''; 136 | 137 | var indent = this.indent 138 | , specs = [] 139 | , list 140 | , info = this.collect(program) 141 | , buf = ''; 142 | 143 | // global options 144 | if(!depth) { 145 | list = this.optspec(program, program.options, {actions: false}); 146 | buf += EOL + this.array('options', list, lead); 147 | } 148 | 149 | if(info.locals.length) { 150 | buf += this.locals(info.locals, lead); 151 | if(depth) { 152 | buf += EOL; 153 | } 154 | } 155 | 156 | if(program.commands) { 157 | specs.push(this.quote('1:cmd:->cmd')); 158 | } 159 | 160 | specs.push(this.quote('*::arg:->args')); 161 | 162 | // inspect synopsis for default actions at main program level 163 | if(!info.actions.length && !depth) { 164 | info.actions = this.action(program.synopsis); 165 | } 166 | 167 | if(info.actions.length) { 168 | buf += EOL + this.array('actions', this.quote(info.actions), lead); 169 | } 170 | 171 | // cascade options for deeper commands 172 | if(program.options && depth) { 173 | buf += this.array( 174 | 'options', 175 | this.optspec(program, program.options), lead, true); 176 | } 177 | 178 | // shortcut out on root program with no commands 179 | if(!depth && !program.commands) { 180 | buf += EOL + this.args(this.options({}), lead) + EOL; 181 | return buf; 182 | } 183 | 184 | buf += EOL; 185 | buf += this.args(specs, lead); 186 | buf += EOL; 187 | 188 | buf += lead + 'case $state in' + EOL; 189 | 190 | // create command values list 191 | if(program.commands) { 192 | buf += lead + indent + '(cmd)' + EOL; 193 | list = this.describe(program, program.commands); 194 | 195 | buf += this.array('commands', list, lead + indent + indent); 196 | 197 | buf += lead + indent + indent 198 | + '_describe "' + program.name + ' commands" commands'; 199 | buf += ' && ret=0'; 200 | buf += EOL; 201 | 202 | // mix arguments when command is not required 203 | if(!this.required(program)) { 204 | list = this.options({}); 205 | buf += this.args(list, lead + indent + indent, {returns: true}); 206 | } 207 | 208 | // eof 209 | buf += lead + indent + indent + ';;' + EOL; 210 | } 211 | 212 | // arguments list 213 | buf += lead + indent + '(args)' + EOL; 214 | 215 | if(program.commands) { 216 | buf += this.esac( 217 | program.commands, lead + indent + indent, depth + 1); 218 | }else{ 219 | buf += this.array( 220 | 'options', 221 | this.optspec(program, program.options), lead + indent + indent, true); 222 | } 223 | 224 | // eof switch 225 | buf += lead + indent + indent + ';;' + EOL; 226 | 227 | // eof case 228 | buf += lead + 'esac' + EOL; 229 | 230 | return buf; 231 | } 232 | 233 | /** 234 | * Gets a case statement that switches on command names. 235 | * 236 | * @function esac 237 | * @param {Object} commands command map. 238 | * @param {String} lead leading whitespace. 239 | * @param {Number} depth current depth in the command tree. 240 | * 241 | * @returns String case statement. 242 | */ 243 | function esac(commands, lead, depth) { 244 | var buf = '' 245 | , k 246 | , list 247 | , cmd 248 | , info 249 | , indent = this.indent; 250 | 251 | buf += lead + 'case "$words[1]" in' + EOL; 252 | 253 | for(k in commands) { 254 | cmd = commands[k]; 255 | buf += lead + indent + cmd.names.join('|') + ')' + EOL; 256 | if(cmd.zsh) { 257 | info = this.collect(cmd, lead); 258 | if(info.actions.length) { 259 | buf += EOL 260 | + this.array('actions', this.quote(info.actions) 261 | , lead + indent + indent); 262 | } 263 | } 264 | if(cmd.commands) { 265 | // recurse for nested commands 266 | buf += this.command(cmd, lead + indent + indent, depth); 267 | }else if(cmd.options) { 268 | list = this.optspec(cmd, cmd.options); 269 | //buf += this.args(list, lead + indent + indent); 270 | buf += this.array( 271 | 'options', 272 | list, lead + indent + indent, true); 273 | } 274 | buf += lead + indent + ';;' + EOL; 275 | } 276 | 277 | buf += lead + 'esac' + EOL; 278 | return buf; 279 | } 280 | 281 | /** 282 | * Get a string for a list of specifications. 283 | * 284 | * Typically used to call `_arguments` but is also used to build array 285 | * declarations. 286 | * 287 | * @function args 288 | * @param {Array} list command or option specifications. 289 | * @param {String} lead leading whitespace. 290 | * @param {Object} [opts] processing options. 291 | * 292 | * @option {String=_arguments} keyword function name to call. 293 | * @option {Boolean} esc whether to escape newlines with a backslash. 294 | * @option {Boolean} returns whether to hard `return 0` or set `ret=0`. 295 | * 296 | * @returns String function call or array body. 297 | */ 298 | function args(list, lead, opts) { 299 | opts = opts || {}; 300 | lead = lead || ''; 301 | var keyword = opts.keyword || '_arguments' 302 | , buf = '' 303 | , i; 304 | 305 | // print keywords - disable with `keyword` false, used for array declarations 306 | if(opts.keyword !== false) { 307 | buf = lead + keyword + ' \\' + EOL; 308 | } 309 | 310 | for(i = 0;i < list.length;i++) { 311 | buf += lead + this.indent + list[i]; 312 | if(i < list.length - 1) { 313 | buf += ' ' + (opts.esc !== false ? '\\' : '') + EOL; 314 | }else{ 315 | if(opts.returns !== false) { 316 | buf += ' && ' + (opts.returns === true ? 'return 0' : 'ret=0'); 317 | buf += ';' + EOL; 318 | } 319 | } 320 | } 321 | return buf; 322 | } 323 | 324 | function optspec(program, map, opts) { 325 | opts = opts || {}; 326 | 327 | var k 328 | , opt 329 | , str 330 | , spec = [] 331 | , names 332 | , isOption 333 | , isCommand 334 | , actions; 335 | 336 | for(k in map) { 337 | opt = map[k]; 338 | names = opt.names.slice(0); 339 | str = ''; 340 | isOption = opt.type === 'option'; 341 | isCommand = opt.type === 'command'; 342 | 343 | names = names.reduce(expand, []); 344 | 345 | if(names.length > 1) { 346 | if(opt.multiple) { 347 | str += '"*"'; 348 | }else{ 349 | str += '"(' + names.join(' ') + ')"'; 350 | } 351 | str += '{' + names.join(isOption ? '=,' : ',') 352 | + (isOption ? '=' : '') + '}'; 353 | str += '"'; 354 | str += '[' + opt.description + ']'; 355 | }else{ 356 | names[0] = names[0].replace(/(\[|\])/g, '\\$1'); 357 | str += '"' + names[0] + (isOption ? '=' : ''); 358 | str += '[' + opt.description + ']'; 359 | } 360 | 361 | // enum style values 362 | if(opt.kind && Array.isArray(opt.kind)) { 363 | str += ':value:_values select ' + opt.kind.reduce(function(prev, item) { 364 | // quote values in case they contain spaces 365 | return prev + ' \'' + item + '\''; 366 | }, ''); 367 | // files and directories 368 | }else if(opt.extra) { 369 | actions = this.action(opt.extra, {multiple: false, option: opt}); 370 | if(actions.length) { 371 | str += actions[0]; 372 | } 373 | } 374 | 375 | // close quotation 376 | str += '"'; 377 | 378 | // add to the spec list 379 | spec.push(str); 380 | } 381 | 382 | return spec; 383 | } 384 | 385 | /** 386 | * Get completion actions list for string candidates. 387 | * 388 | * When an `option` is passed and it has a parsed `zaction` it is returned. 389 | * 390 | * @function action 391 | * @param {String|Array} val values to inspect. 392 | * @param {Object} [opts] processing options. 393 | * 394 | * @option {Boolean} multiple whether ellipsis matches have wildcards (*). 395 | * @option {Object} option current option. 396 | * 397 | * @returns Array of completion actions. 398 | */ 399 | function action(val, opts) { 400 | opts = opts || {}; 401 | val = Array.isArray(val) ? val : [val]; 402 | 403 | var actions = [] 404 | , opt = opts.option; 405 | 406 | if(opt && opt.zaction) { 407 | return [opt.zaction]; 408 | } 409 | 410 | val.forEach(function(value) { 411 | var i 412 | , id 413 | , val 414 | , match; 415 | 416 | for(i = 0;i < keys.length;i++) { 417 | id = keys[i]; 418 | if(ptn[id].test(value)) { 419 | val = map[id]; 420 | if(opts.multiple !== false) { 421 | match = value.replace(ptn[id], '$1'); 422 | if(~match.indexOf('...')) { 423 | // make completion repeatable 424 | val = '*' + val; 425 | } 426 | } 427 | actions.push(val); 428 | } 429 | } 430 | }); 431 | 432 | return actions; 433 | } 434 | 435 | /** 436 | * Determine if a subcommand is required for a command. 437 | * 438 | * Inspects the synopsis list for the command and if the string 439 | * `` is found in any of the synopsis declarations treat 440 | * the command as requiring a subcommand. 441 | * 442 | * @function required 443 | * @param {Object} cmd parent command. 444 | * 445 | * @returns Boolean true if a subcommand is required. 446 | */ 447 | function required(cmd) { 448 | if(!cmd.synopsis) { 449 | return; 450 | } 451 | for(var i = 0;i < cmd.synopsis.length;i++) { 452 | if(~cmd.synopsis[i].indexOf(COMMAND)) { 453 | return true; 454 | } 455 | } 456 | } 457 | 458 | /** 459 | * Get a list of commands suitable for passing to `_describe`. 460 | * 461 | * Command names are mapped to command descriptions delimited by a colon and 462 | * wrapped in double quotes. 463 | * 464 | * Any colons in the command name are escaped with a backslash. 465 | * 466 | * @function describe 467 | * @param {Object} cmd current command. 468 | * @param {Object} map command map. 469 | * 470 | * @returns Array of command definitions. 471 | */ 472 | function describe(cmd, map) { 473 | var spec = [] 474 | , k 475 | , name; 476 | for(k in map) { 477 | if(map[k].names.length === 1) { 478 | name = map[k].name.replace(/:/g, '\\:'); 479 | spec.push(this.quote(name + ':' + map[k].description)); 480 | }else{ 481 | name = '{' + map[k].names.join(',') + '}'; 482 | spec.push(name + this.quote(':' + map[k].description)); 483 | } 484 | } 485 | return spec; 486 | } 487 | 488 | /** 489 | * Enclose a string in double quotes. 490 | * 491 | * @function quote 492 | * @param {Array|String} val value to quote. 493 | * 494 | * @returns String enclosed in double quotes. 495 | */ 496 | function quote(val) { 497 | if(Array.isArray(val)) { 498 | return val.map(function(value) { 499 | return '"' + value + '"'; 500 | }) 501 | } 502 | return '"' + val + '"'; 503 | } 504 | 505 | /** 506 | * Get an options list for a command. 507 | * 508 | * @function options 509 | * @param {Object} cmd the current command. 510 | * 511 | * @returns Array of option specifications. 512 | */ 513 | function options(cmd) { 514 | var list = []; 515 | if(cmd.options) { 516 | list = this.optspec(cmd, cmd.options); 517 | } 518 | list.push('$options', '$actions'); 519 | return list; 520 | } 521 | 522 | /** 523 | * Helper function to expand --[no]- and --no- option names. 524 | * 525 | * @private {function} expand 526 | */ 527 | function expand(prev, item) { 528 | var re = /\[?no\]?-/ 529 | , yes 530 | , no; 531 | if(re.test(item)) { 532 | yes = item.replace(re, ''); 533 | no = item.replace(re, 'no-'); 534 | return prev.concat([yes, no]); 535 | } 536 | return prev.concat(item); 537 | } 538 | 539 | /** 540 | * Get collections of locals and actions. 541 | * 542 | * @function collect 543 | * @param {Object} cmd current command. 544 | * 545 | * @returns Object map with `actions` and `locals` arrays. 546 | */ 547 | function collect(cmd) { 548 | var specs = cmd.zsh || [] 549 | , i 550 | , locals = [] 551 | , actions = []; 552 | 553 | for(i = 0;i < specs.length;i++) { 554 | if(/locals/.test(specs[i].info)) { 555 | locals.push(specs[i].literal); 556 | }else{ 557 | actions.push(specs[i].literal); 558 | } 559 | } 560 | 561 | return {locals: locals, actions: actions}; 562 | } 563 | 564 | function locals(list, lead) { 565 | var buf = '' 566 | , i; 567 | 568 | function indentation(str) { 569 | return lead + str; 570 | } 571 | 572 | // inject function locals defined in program definition (eg: zsh-locals) 573 | if(list.length) { 574 | buf += EOL; 575 | for(i = 0;i < list.length;i++) { 576 | buf += list[i].split('\n').map(indentation).join('\n'); 577 | } 578 | buf += EOL; 579 | } 580 | 581 | return buf; 582 | } 583 | 584 | Zsh.prototype.main = main; 585 | Zsh.prototype.compdef = compdef; 586 | Zsh.prototype.command = command; 587 | Zsh.prototype.optspec = optspec; 588 | Zsh.prototype.args = args; 589 | Zsh.prototype.esac = esac; 590 | Zsh.prototype.action = action; 591 | Zsh.prototype.options = options; 592 | Zsh.prototype.describe = describe; 593 | Zsh.prototype.required = required; 594 | Zsh.prototype.array = array; 595 | Zsh.prototype.quote = quote; 596 | Zsh.prototype.collect = collect; 597 | Zsh.prototype.locals = locals; 598 | 599 | module.exports = through.transform(transform, {ctor: Zsh}); 600 | 601 | // expose actions map for documentation purposes 602 | module.exports.actions = map; 603 | 604 | -------------------------------------------------------------------------------- /lib/state.js: -------------------------------------------------------------------------------- 1 | function State(type, chunk) { 2 | this.type = type; 3 | this.nodes = chunk ? [chunk] : []; 4 | this.data = undefined; 5 | } 6 | 7 | State.states = { 8 | // in the program name declaration 9 | NAME: 'name', 10 | // in the program description 11 | DESCRIPTION: 'description', 12 | // in the synposis declaration 13 | SYNOPSIS: 'synopsis', 14 | // in an options declaration 15 | OPTIONS: 'options', 16 | // in a commands declaration 17 | COMMANDS: 'commands', 18 | // in a manual section entry 19 | SECTION: 'section' 20 | }; 21 | 22 | module.exports = State; 23 | -------------------------------------------------------------------------------- /lib/synopsis.js: -------------------------------------------------------------------------------- 1 | var wordwrap = require('wordwrap') 2 | , repeat = require('string-repeater'); 3 | 4 | /** 5 | * Performs formatting of the synopsis strings. 6 | * 7 | * @function synopsis 8 | * @param {Object} state synopsis state information. 9 | */ 10 | function synopsis(state, conf) { 11 | conf = conf || {}; 12 | var literal = state.data.synopsis 13 | , name = state.name.data.name 14 | , opts = state.options ? state.options.data.options : null 15 | , cols = conf.cols || 80 16 | , indent = conf.indent || 0 17 | , spaces = repeat(' ', indent) 18 | , wrap 19 | , info 20 | , str = '' 21 | , cre = /^(\[|<)(command)(\]|>) /im 22 | , fre = /(\[|<)?(flags)(\]|>)?/im 23 | , ore = /(\[|<)?(options)(\]|>)?/im 24 | , i 25 | , src; 26 | 27 | if(state.name.data.parents && state.name.data.parents.length) { 28 | name = state.name.data.parents.join(' ') + ' ' + name; 29 | } 30 | 31 | for(i = 0;i < literal.length;i++) { 32 | src = literal[i]; 33 | 34 | if(cols) { 35 | if(cre.test(src)) { 36 | // | [command] + single space == 10 37 | indent += 10; 38 | } 39 | wrap = wordwrap(indent + name.length + 1, cols) 40 | } 41 | 42 | 43 | if(opts) { 44 | info = gather(src, opts); 45 | 46 | if(fre.test(src)) { 47 | src = src.replace(fre, '$1' + flags(info) + '$3'); 48 | } 49 | 50 | if(ore.test(src)) { 51 | src = src.replace(ore, options(info)); 52 | } 53 | } 54 | 55 | src = (wrap ? wrap(src) : src); 56 | src = src.replace(/^\s+/, ''); 57 | 58 | if(i) { 59 | str += (indent ? '\n' + spaces : '\n'); 60 | } 61 | 62 | str += name + ' ' + src; 63 | } 64 | 65 | return str; 66 | } 67 | 68 | function options(info) { 69 | var i 70 | , opt 71 | , extra 72 | , list = info.longFlags.concat(info.opts) 73 | , str = ''; 74 | 75 | for(i = 0;i < list.length;i++) { 76 | opt = list[i]; 77 | 78 | if(str) { 79 | str += ' '; 80 | } 81 | 82 | str += '[' + opt.name 83 | 84 | extra = opt.extra; 85 | if(extra) { 86 | extra = extra.replace(/\[/, '<'); 87 | extra = extra.replace(/\]/, '>'); 88 | 89 | // make this configurable 90 | extra = extra.toLowerCase(); 91 | 92 | str += extra; 93 | } 94 | 95 | str += ']'; 96 | } 97 | 98 | return str; 99 | } 100 | 101 | function flags(info) { 102 | var i 103 | , j 104 | , opt 105 | , str = ''; 106 | 107 | for(i = 0;i < info.flags.length;i++) { 108 | opt = info.flags[i]; 109 | for(j = 0;j < opt.names.length;j++) { 110 | // got short single character flag option 111 | if(/^-.$/.test(opt.names[j])) { 112 | if(!str) { 113 | str = '-'; 114 | } 115 | str += opt.names[j].charAt(1); 116 | break; 117 | } 118 | } 119 | } 120 | 121 | return str; 122 | } 123 | 124 | /** 125 | * Gather options from the list that are not declared in the synopsis into 126 | * lists of flags and options. 127 | * 128 | * @function gather 129 | * @param {String} literal the synopsis declaration. 130 | * @param {Object} options map of parsed options. 131 | */ 132 | function gather(literal, options) { 133 | var flags = [] 134 | , longFlags = [] 135 | , opts = [] 136 | , k 137 | , opt 138 | , names 139 | , declared; 140 | 141 | function isDeclared(names) { 142 | var i 143 | , ptn 144 | , exists 145 | , diff; 146 | 147 | for(i = 0;i < names.length;i++) { 148 | ptn = new RegExp(names[i] + '[ \\|\\]\\)>]'); 149 | if(ptn.test(literal)) { 150 | exists = true; 151 | }else{ 152 | diff = diff || []; 153 | diff.push(names[i]); 154 | } 155 | } 156 | 157 | return {diff: diff, exists: exists}; 158 | } 159 | 160 | function hasLongFlag(names) { 161 | var i 162 | , ptn = /^--./; 163 | for(i = 0;i < names.length;i++) { 164 | if(ptn.test(names[i])) { 165 | return true; 166 | } 167 | } 168 | return false; 169 | } 170 | 171 | function hasShortFlag(names) { 172 | var i 173 | , ptn = /^-.$/; 174 | for(i = 0;i < names.length;i++) { 175 | if(ptn.test(names[i])) { 176 | return true; 177 | } 178 | } 179 | return false; 180 | } 181 | 182 | function add(names, opt) { 183 | if(opt.type === 'flag') { 184 | if(hasLongFlag(names)) { 185 | longFlags.push(opt); 186 | } 187 | 188 | if(hasShortFlag(names)) { 189 | flags.push(opt); 190 | } 191 | }else{ 192 | opts.push(opt); 193 | } 194 | } 195 | 196 | for(k in options) { 197 | opt = options[k]; 198 | names = opt.names; 199 | declared = isDeclared(names); 200 | if(!declared.exists) { 201 | add(names, opt); 202 | }else if(declared.diff) { 203 | add(declared.diff, opt); 204 | } 205 | } 206 | 207 | return {flags: flags, opts: opts, longFlags: longFlags} 208 | } 209 | 210 | module.exports = synopsis; 211 | -------------------------------------------------------------------------------- /mkdoc.js: -------------------------------------------------------------------------------- 1 | var mk = require('mktask') 2 | , fs = require('fs'); 3 | 4 | // @task actions build the zsh actions list 5 | function actions(cb) { 6 | // get list of actions 7 | var map = require('./lib/render/zsh').actions 8 | , buf = '' 9 | , stream = fs.createWriteStream('doc/readme/actions.md'); 10 | 11 | stream.once('finish', cb); 12 | 13 | // convert to markdown 14 | for(var k in map) { 15 | buf += '* ' + k + ': `' + map[k] + '`\n'; 16 | } 17 | buf += '\n'; 18 | stream.end(buf); 19 | } 20 | 21 | // @task readme build the readme file 22 | function readme(cb) { 23 | mk.doc('doc/readme.md') 24 | .pipe(mk.pi()) 25 | .pipe(mk.ref()) 26 | .pipe(mk.abs()) 27 | .pipe(mk.msg()) 28 | .pipe(mk.toc({depth: 2})) 29 | .pipe(mk.out()) 30 | .pipe(mk.dest('README.md')) 31 | .on('finish', cb); 32 | } 33 | 34 | mk.task(actions); 35 | mk.task([actions], readme); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mkcli", 3 | "version": "1.0.34", 4 | "description": "Markdown command line interface definition", 5 | "author": "muji", 6 | "license": "MIT", 7 | "homepage": "https://github.com/mkdoc/mkcli", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/mkdoc/mkcli" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/mkdoc/mkcli/issues" 14 | }, 15 | "keywords": [ 16 | "markdown", 17 | "commonmark", 18 | "ast", 19 | "stream", 20 | "cli", 21 | "docs", 22 | "man", 23 | "help" 24 | ], 25 | "dependencies": { 26 | "cli-argparse": "~1.1.2", 27 | "mkast": "~1.2.2", 28 | "mkcli-runtime": "~1.0.0", 29 | "string-repeater": "~1.0.3", 30 | "through3": "~1.1.5", 31 | "wordwrap": "~1.0.0" 32 | }, 33 | "devDependencies": { 34 | "chai": "~3.5.0", 35 | "coveralls": "~2.11.8", 36 | "istanbul": "~0.4.2", 37 | "mocha": "~2.4.5" 38 | }, 39 | "scripts": { 40 | "lint": "jshint . && jscs .", 41 | "clean": "rm -rf coverage", 42 | "pretest": "rm -rf target && mkdir target", 43 | "test": "NODE_ENV=test mocha ${SPEC:-test/spec}", 44 | "precover": "npm run pretest", 45 | "cover": "NODE_ENV=test istanbul cover _mocha -- ${SPEC:-test/spec}", 46 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 47 | }, 48 | "config": { 49 | "man": { 50 | "example": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/fixtures/bad-name.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | ``` 4 | error because name should have a paragraph first 5 | ``` 6 | 7 | Program Name 8 | -------------------------------------------------------------------------------- /test/fixtures/command.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Commands 6 | 7 | * `ls, list` List tasks 8 | 9 | # Options 10 | 11 | * `-i=[NUM] {=2}` Number 12 | * `-s=[VAL] {foo|bar|qux}` String 13 | -------------------------------------------------------------------------------- /test/fixtures/commands.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Commands 6 | 7 | * `ls, list` List tasks 8 | * `i, info` Show task info 9 | -------------------------------------------------------------------------------- /test/fixtures/completion/_notes: -------------------------------------------------------------------------------- 1 | #compdef notes 2 | _notes(){ 3 | typeset -A opt_args; 4 | local context state state_descr line ret=1; 5 | local actions options commands; 6 | 7 | options=( 8 | "(-p --package)"{-p=,--package=}"[File type completion]:file:_files -g '*.json'" 9 | "(-f --file)"{-f=,--file=}"[File completion]:file:_files" 10 | "(-d --directory)"{-d=,--directory=}"[Directory completion]:directory:_directories" 11 | "(-H --host)"{-H=,--host=}"[Host completion]:host:_hosts" 12 | "*"{-D=,--domain=}"[Domain completion]:domain:_domains" 13 | "*"{-u=,--url=}"[URL completion]:url:_urls" 14 | "(-h --help)"{-h,--help}"[Display help and exit]" 15 | "--version[Print version and exit]" 16 | ) 17 | 18 | typeset -A notesLocal; 19 | 20 | actions=( 21 | "*:file:_files -g '*.md'" 22 | ) 23 | 24 | _arguments \ 25 | "1:cmd:->cmd" \ 26 | "*::arg:->args" && ret=0; 27 | 28 | case $state in 29 | (cmd) 30 | commands=( 31 | "add:Add a note" 32 | "del:Delete a note" 33 | {ls,list}":List notes" 34 | "show:Show a note" 35 | "edit:Edit a note" 36 | ) 37 | _describe "notes commands" commands && ret=0 38 | ;; 39 | (args) 40 | case "$words[1]" in 41 | add) 42 | 43 | actions=( 44 | "*:dir:_directories" 45 | ) 46 | options=( 47 | "(-t --type)"{-t=,--type=}"[Type of note to create]:value:_values select 'todo' 'bug' 'feature'" 48 | $options 49 | ) 50 | ;; 51 | del) 52 | ;; 53 | ls|list) 54 | 55 | typeset -A listLocal; 56 | 57 | options=( 58 | "(--private --no-private)"{--private,--no-private}"[Include or exclude private notes]" 59 | "--\[enable|disable\]-feature[Enable or disable a feature]" 60 | $options 61 | ) 62 | 63 | _arguments \ 64 | "1:cmd:->cmd" \ 65 | "*::arg:->args" && ret=0; 66 | 67 | case $state in 68 | (cmd) 69 | commands=( 70 | "todo:Show todos" 71 | "bug:Show bugs" 72 | "feature:Show features" 73 | ) 74 | _describe "list commands" commands && ret=0 75 | _arguments \ 76 | $options \ 77 | $actions && return 0; 78 | ;; 79 | (args) 80 | case "$words[1]" in 81 | todo) 82 | ;; 83 | bug) 84 | 85 | _arguments \ 86 | "1:cmd:->cmd" \ 87 | "*::arg:->args" && ret=0; 88 | 89 | case $state in 90 | (cmd) 91 | commands=( 92 | "low:Low priority bugs" 93 | "medium:Medium priority bugs" 94 | "high:High priority bugs" 95 | "critical:Critical bugs" 96 | ) 97 | _describe "bug commands" commands && ret=0 98 | ;; 99 | (args) 100 | case "$words[1]" in 101 | low) 102 | ;; 103 | medium) 104 | ;; 105 | high) 106 | ;; 107 | critical) 108 | ;; 109 | esac 110 | ;; 111 | esac 112 | ;; 113 | feature) 114 | ;; 115 | esac 116 | ;; 117 | esac 118 | ;; 119 | show) 120 | ;; 121 | edit) 122 | ;; 123 | esac 124 | ;; 125 | esac 126 | (( $ret == 1 )) && _arguments \ 127 | $options \ 128 | $actions && ret=0; 129 | return $ret; 130 | } 131 | 132 | _notes "$@" -------------------------------------------------------------------------------- /test/fixtures/completion/notes: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | -------------------------------------------------------------------------------- /test/fixtures/completion/notes-add.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | add - add a note 4 | 5 | # Synopsis 6 | 7 | ```zsh 8 | *:dir:_directories 9 | ``` 10 | 11 | # Options 12 | 13 | * `-t, --type=[TYPE] {todo|bug|feature}` Type of note to create 14 | -------------------------------------------------------------------------------- /test/fixtures/completion/notes-list-bug.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | notes list bug - list bugs 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [options] 9 | ``` 10 | 11 | # Commands 12 | 13 | * `low` Low priority bugs 14 | * `medium` Medium priority bugs 15 | * `high` High priority bugs 16 | * `critical` Critical bugs 17 | 18 | -------------------------------------------------------------------------------- /test/fixtures/completion/notes-list.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | notes list - list notes 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [command] [options] 9 | ``` 10 | 11 | ```zsh-locals 12 | typeset -A listLocal; 13 | ``` 14 | 15 | # Commands 16 | 17 | * `todo` Show todos 18 | * `bug` Show bugs 19 | * `feature` Show features 20 | 21 | # Options 22 | 23 | * `--[no]-private` Include or exclude private notes 24 | * `--[enable|disable]-feature` Enable or disable a feature 25 | -------------------------------------------------------------------------------- /test/fixtures/completion/notes.1: -------------------------------------------------------------------------------- 1 | .\" Generated by mkdoc on Thu Apr 14 2016 14:40:08 GMT+0800 (WITA) 2 | .TH "UNTITLED" "1" "April, 2016" "UNTITLED 1.0" "User Commands" 3 | .de nl 4 | .sp 0 5 | .. 6 | .de hr 7 | .sp 1 8 | .nf 9 | .ce 10 | .in 4 11 | \l’80’ 12 | .fi 13 | .. 14 | .de h1 15 | .RE 16 | .sp 1 17 | \fB\\$1\fR 18 | .RS 4 19 | .. 20 | .de h2 21 | .RE 22 | .sp 1 23 | .in 4 24 | \fB\\$1\fR 25 | .RS 6 26 | .. 27 | .de h3 28 | .RE 29 | .sp 1 30 | .in 6 31 | \fB\\$1\fR 32 | .RS 8 33 | .. 34 | .de h4 35 | .RE 36 | .sp 1 37 | .in 8 38 | \fB\\$1\fR 39 | .RS 10 40 | .. 41 | .de h5 42 | .RE 43 | .sp 1 44 | .in 10 45 | \fB\\$1\fR 46 | .RS 12 47 | .. 48 | .de h6 49 | .RE 50 | .sp 1 51 | .in 12 52 | \fB\\$1\fR 53 | .RS 14 54 | .. 55 | .h1 "NAME" 56 | .P 57 | notes \- program to test the completion capabilities 58 | .nl 59 | .h1 "SYNOPSIS" 60 | .PP 61 | .in 10 62 | [options] [files...] 63 | .PP 64 | .in 10 65 | :file:_files \-g '*.md' 66 | .h1 "COMMANDS" 67 | .BL 68 | .IP "\[ci]" 4 69 | \fBadd\fR Add a note 70 | .nl 71 | .IP "\[ci]" 4 72 | \fBdel\fR Delete a note 73 | .nl 74 | .IP "\[ci]" 4 75 | \fBls, list\fR List notes 76 | .nl 77 | .IP "\[ci]" 4 78 | \fBshow\fR Show a note 79 | .nl 80 | .IP "\[ci]" 4 81 | \fBedit\fR Edit a note 82 | .nl 83 | .EL 84 | .h1 "OPTIONS" 85 | .BL 86 | .IP "\[ci]" 4 87 | \fB\-p, \-\-package=[FILE]\fR File type completion 88 | .nl 89 | .IP "\[ci]" 4 90 | \fB\-f, \-\-file=[FILE]\fR File completion 91 | .nl 92 | .IP "\[ci]" 4 93 | \fB\-d, \-\-directory=[DIR]\fR Directory completion 94 | .nl 95 | .IP "\[ci]" 4 96 | \fB\-H, \-\-host=[HOST]\fR Host completion 97 | .nl 98 | .IP "\[ci]" 4 99 | \fB\-D, \-\-domain=[DOMAIN...]\fR Domain completion 100 | .nl 101 | .IP "\[ci]" 4 102 | \fB\-u, \-\-url=[URL...]\fR URL completion 103 | .nl 104 | .IP "\[ci]" 4 105 | \fB\-h, \-\-help\fR Display help and exit 106 | .nl 107 | .IP "\[ci]" 4 108 | \fB\-\-version\fR Print version and exit 109 | .nl 110 | .EL 111 | .h1 "TEST" 112 | .P 113 | To test the program update fpath: 114 | .nl 115 | .PP 116 | .in 10 117 | fpath=($HOME/git/mkdoc/mkcli/test/fixtures/completion $fpath) 118 | .P 119 | And PATH: 120 | .nl 121 | .PP 122 | .in 10 123 | PATH="test/fixtures/completion:$PATH" -------------------------------------------------------------------------------- /test/fixtures/completion/notes.json: -------------------------------------------------------------------------------- 1 | { 2 | "names": [ 3 | "notes" 4 | ], 5 | "type": "program", 6 | "name": "notes", 7 | "summary": "program to test the completion capabilities", 8 | "synopsis": [ 9 | " [options] [files...]" 10 | ], 11 | "options": { 12 | "package": { 13 | "key": "package", 14 | "description": "File type completion", 15 | "names": [ 16 | "-p", 17 | "--package" 18 | ], 19 | "type": "option", 20 | "extra": "=[FILE]", 21 | "required": false, 22 | "multiple": false, 23 | "zaction": ":file:_files -g '*.json'", 24 | "name": "--package" 25 | }, 26 | "file": { 27 | "key": "file", 28 | "description": "File completion", 29 | "names": [ 30 | "-f", 31 | "--file" 32 | ], 33 | "type": "option", 34 | "extra": "=[FILE]", 35 | "required": false, 36 | "multiple": false, 37 | "name": "--file" 38 | }, 39 | "directory": { 40 | "key": "directory", 41 | "description": "Directory completion", 42 | "names": [ 43 | "-d", 44 | "--directory" 45 | ], 46 | "type": "option", 47 | "extra": "=[DIR]", 48 | "required": false, 49 | "multiple": false, 50 | "name": "--directory" 51 | }, 52 | "host": { 53 | "key": "host", 54 | "description": "Host completion", 55 | "names": [ 56 | "-H", 57 | "--host" 58 | ], 59 | "type": "option", 60 | "extra": "=[HOST]", 61 | "required": false, 62 | "multiple": false, 63 | "name": "--host" 64 | }, 65 | "domain": { 66 | "key": "domain", 67 | "description": "Domain completion", 68 | "names": [ 69 | "-D", 70 | "--domain" 71 | ], 72 | "type": "option", 73 | "extra": "=[DOMAIN...]", 74 | "required": false, 75 | "multiple": true, 76 | "name": "--domain" 77 | }, 78 | "url": { 79 | "key": "url", 80 | "description": "URL completion", 81 | "names": [ 82 | "-u", 83 | "--url" 84 | ], 85 | "type": "option", 86 | "extra": "=[URL...]", 87 | "required": false, 88 | "multiple": true, 89 | "name": "--url" 90 | }, 91 | "help": { 92 | "key": "help", 93 | "description": "Display help and exit", 94 | "names": [ 95 | "-h", 96 | "--help" 97 | ], 98 | "type": "flag", 99 | "name": "--help" 100 | }, 101 | "version": { 102 | "key": "version", 103 | "description": "Print version and exit", 104 | "names": [ 105 | "--version" 106 | ], 107 | "type": "flag", 108 | "name": "--version" 109 | } 110 | }, 111 | "commands": { 112 | "add": { 113 | "key": "add", 114 | "description": "Add a note", 115 | "names": [ 116 | "add" 117 | ], 118 | "type": "command", 119 | "name": "add", 120 | "summary": "add a note", 121 | "options": { 122 | "type": { 123 | "key": "type", 124 | "description": "Type of note to create", 125 | "names": [ 126 | "-t", 127 | "--type" 128 | ], 129 | "type": "option", 130 | "extra": "=[TYPE]", 131 | "required": false, 132 | "multiple": false, 133 | "kind": [ 134 | "todo", 135 | "bug", 136 | "feature" 137 | ], 138 | "name": "--type" 139 | } 140 | } 141 | }, 142 | "del": { 143 | "key": "del", 144 | "description": "Delete a note", 145 | "names": [ 146 | "del" 147 | ], 148 | "type": "command", 149 | "name": "del" 150 | }, 151 | "list": { 152 | "key": "list", 153 | "description": "List notes", 154 | "names": [ 155 | "ls", 156 | "list" 157 | ], 158 | "type": "command", 159 | "name": "list", 160 | "summary": "list notes", 161 | "synopsis": [ 162 | "[command] [options]" 163 | ], 164 | "options": { 165 | "private": { 166 | "key": "private", 167 | "description": "Include or exclude private notes", 168 | "names": [ 169 | "--[no]-private" 170 | ], 171 | "type": "flag", 172 | "name": "--[no]-private" 173 | }, 174 | "[enable|disable]Feature": { 175 | "key": "[enable|disable]Feature", 176 | "description": "Enable or disable a feature", 177 | "names": [ 178 | "--[enable|disable]-feature" 179 | ], 180 | "type": "flag", 181 | "name": "--[enable|disable]-feature" 182 | } 183 | }, 184 | "commands": { 185 | "todo": { 186 | "key": "todo", 187 | "description": "Show todos", 188 | "names": [ 189 | "todo" 190 | ], 191 | "type": "command", 192 | "name": "todo" 193 | }, 194 | "bug": { 195 | "key": "bug", 196 | "description": "Show bugs", 197 | "names": [ 198 | "bug" 199 | ], 200 | "type": "command", 201 | "name": "bug", 202 | "summary": "list bugs", 203 | "synopsis": [ 204 | " [options]" 205 | ], 206 | "commands": { 207 | "low": { 208 | "key": "low", 209 | "description": "Low priority bugs", 210 | "names": [ 211 | "low" 212 | ], 213 | "type": "command", 214 | "name": "low" 215 | }, 216 | "medium": { 217 | "key": "medium", 218 | "description": "Medium priority bugs", 219 | "names": [ 220 | "medium" 221 | ], 222 | "type": "command", 223 | "name": "medium" 224 | }, 225 | "high": { 226 | "key": "high", 227 | "description": "High priority bugs", 228 | "names": [ 229 | "high" 230 | ], 231 | "type": "command", 232 | "name": "high" 233 | }, 234 | "critical": { 235 | "key": "critical", 236 | "description": "Critical bugs", 237 | "names": [ 238 | "critical" 239 | ], 240 | "type": "command", 241 | "name": "critical" 242 | } 243 | } 244 | }, 245 | "feature": { 246 | "key": "feature", 247 | "description": "Show features", 248 | "names": [ 249 | "feature" 250 | ], 251 | "type": "command", 252 | "name": "feature" 253 | } 254 | } 255 | }, 256 | "show": { 257 | "key": "show", 258 | "description": "Show a note", 259 | "names": [ 260 | "show" 261 | ], 262 | "type": "command", 263 | "name": "show" 264 | }, 265 | "edit": { 266 | "key": "edit", 267 | "description": "Edit a note", 268 | "names": [ 269 | "edit" 270 | ], 271 | "type": "command", 272 | "name": "edit" 273 | } 274 | }, 275 | "zsh": [ 276 | { 277 | "info": "zsh", 278 | "literal": "*:file:_files -g '*.md'" 279 | } 280 | ] 281 | } 282 | -------------------------------------------------------------------------------- /test/fixtures/completion/notes.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | notes - program to test the completion capabilities 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [options] [--file|--directory] [files...] 9 | ``` 10 | 11 | ```zsh-locals 12 | typeset -A notesLocal; 13 | ``` 14 | 15 | ```zsh 16 | *:file:_files -g '*.md' 17 | ``` 18 | 19 | # Commands 20 | 21 | * `add` Add a note 22 | * `del` Delete a note 23 | * `ls, list` List notes 24 | * `show` Show a note 25 | * `edit` Edit a note 26 | 27 | # Options 28 | 29 | * `-p, --package=[FILE] :file:_files -g '*.json'` File type completion 30 | * `-f, --file=[FILE]` File completion 31 | * `-d, --directory=[DIR]` Directory completion 32 | * `-H, --host=[HOST]` Host completion 33 | * `-D, --domain=[DOMAIN...]` Domain completion 34 | * `-u, --url=[URL...]` URL completion 35 | * `-h, --help` Display help and exit 36 | * `--version` Print version and exit 37 | 38 | # Test 39 | 40 | To test the program update fpath: 41 | 42 | ``` 43 | fpath=($HOME/git/mkdoc/mkcli/test/fixtures/completion $fpath) 44 | ``` 45 | 46 | And PATH: 47 | 48 | ``` 49 | PATH="test/fixtures/completion:$PATH" 50 | ``` 51 | -------------------------------------------------------------------------------- /test/fixtures/completion/notes.txt: -------------------------------------------------------------------------------- 1 | Usage: notes [options] [files...] 2 | 3 | Commands 4 | add Add a note 5 | del Delete a note 6 | ls, list List notes 7 | show Show a note 8 | edit Edit a note 9 | 10 | Options 11 | -p, --package=[FILE] File type completion 12 | -f, --file=[FILE] File completion 13 | -d, --directory=[DIR] Directory completion 14 | -H, --host=[HOST] Host completion 15 | -D, --domain=[DOMAIN...] 16 | Domain completion 17 | -u, --url=[URL...] URL completion 18 | -h, --help Display help and exit 19 | --version Print version and exit 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/description.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Description 6 | 7 | This is a description of the program. 8 | 9 | With some more information about the program behaviour. 10 | -------------------------------------------------------------------------------- /test/fixtures/duplicate-key-sparse.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `--file` File 8 | 9 | Foo. 10 | 11 | * `--file` Oops, same key in different list 12 | -------------------------------------------------------------------------------- /test/fixtures/duplicate-key.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `--file` File 8 | * `--file` Oops, same key 9 | -------------------------------------------------------------------------------- /test/fixtures/duplicate-name.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-f, --input` File 8 | * `-f, --output` Oops, same name different keys 9 | -------------------------------------------------------------------------------- /test/fixtures/empty.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | # Synopsis 4 | 5 | # Description 6 | 7 | # Commands 8 | 9 | # Options 10 | -------------------------------------------------------------------------------- /test/fixtures/flag.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-h, --help` Display this help and exit 8 | -------------------------------------------------------------------------------- /test/fixtures/leading-content.md: -------------------------------------------------------------------------------- 1 | Oops, content before the name section that would be ignored but generates a compiler error. 2 | 3 | # Name 4 | 5 | mock - mock description 6 | -------------------------------------------------------------------------------- /test/fixtures/mock.txt: -------------------------------------------------------------------------------- 1 | mock-program [options] 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple-option.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-f, --file [FILE...]` Read input from files 8 | -------------------------------------------------------------------------------- /test/fixtures/name-not-first.md: -------------------------------------------------------------------------------- 1 | # Bugs 2 | 3 | Lots of bugs. 4 | 5 | # Name 6 | 7 | mock - mock description 8 | -------------------------------------------------------------------------------- /test/fixtures/name.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | Program-Name - short program summary 4 | 5 | ``` 6 | code in name section will be ignored 7 | ``` 8 | -------------------------------------------------------------------------------- /test/fixtures/named-flag.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `usage: -h, --help` Display this help and exit 8 | -------------------------------------------------------------------------------- /test/fixtures/names.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | prg - mock description. 4 | 5 | + prg-alias 6 | 7 | + prg-another-alias 8 | -------------------------------------------------------------------------------- /test/fixtures/no-specification.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * Missing inline code specifcation 8 | -------------------------------------------------------------------------------- /test/fixtures/no-summary.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | prg-without-a-summary 4 | -------------------------------------------------------------------------------- /test/fixtures/option-type-value.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-i, --indent [NUM] {Number=2}` Number of spaces for indentation 8 | -------------------------------------------------------------------------------- /test/fixtures/option-type.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-i, --indent [NUM] {Number}` Number of spaces for indentation 8 | -------------------------------------------------------------------------------- /test/fixtures/option-value.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-o, --output [FILE] {=stdout}` Output file 8 | * `-d, --default [FILE] {=foo}` Bar. 9 | -------------------------------------------------------------------------------- /test/fixtures/option.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `file: -f, --file, --full-file-path [FILE]` Write output to FILE 8 | 9 | With another paragraph to test a code path. 10 | 11 | And another paragraph under the one above. 12 | * `--no-description` 13 | * `--enum=[TYPE] {json|text|man}` Enumerable type definition 14 | -------------------------------------------------------------------------------- /test/fixtures/program.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | program - short program description 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [options] 9 | ``` 10 | 11 | ``` 12 | [options] [files..] 13 | ``` 14 | 15 | # Description 16 | 17 | Long program description with a lot more information about the program behaviour. 18 | 19 | It can contain multiple paragraphs and other block level elements but the help output will only include the paragraphs which might lose some meaning. 20 | 21 | ``` 22 | program --version 23 | ``` 24 | 25 | * foo 26 | * bar 27 | * baz 28 | 29 | # Commands 30 | 31 | * `ls, list` List tasks 32 | * `i, info` Print task information 33 | 34 | # Options 35 | 36 | * `-b, --base=[URL] {String=http://example.com}` Base URL for absolute links that also has quite a bit of text 37 | * `-r, --relative=[PATH] {foo|bar=/}` Relative path when repository url with some really 38 | long `text` that *should* force **word** wrapping. 39 | 40 | And some more text in a new paragraph. 41 | 42 | With another longer paragraph too that should wrap because it has quite a few words. 43 | * `-g, --greedy` Convert links starting with # and ? 44 | * `-h, --help` Display this help and exit 45 | * `--version` Print the version and exit 46 | 47 | # Environment 48 | 49 | Accepts the FOO variable. 50 | 51 | # Bugs 52 | 53 | Lots of them. 54 | -------------------------------------------------------------------------------- /test/fixtures/required-option.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Options 6 | 7 | * `-f, --file ` Write output to FILE 8 | -------------------------------------------------------------------------------- /test/fixtures/section.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Environment 6 | 7 | Section paragraph. 8 | 9 | # Bugs 10 | 11 | List of program bugs. 12 | -------------------------------------------------------------------------------- /test/fixtures/subcommand-list-notes.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | notes - list notes 4 | 5 | # Options 6 | 7 | * `-a, --all` List all notes 8 | -------------------------------------------------------------------------------- /test/fixtures/subcommand-list.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | list - list stuff for the mock subcommand program 4 | 5 | # Commands 6 | 7 | * `tasks` List tasks 8 | * `notes` List notes 9 | * `tracks` List tracks 10 | 11 | # Options 12 | 13 | * `--sub-option` Option specific to the subcommand 14 | -------------------------------------------------------------------------------- /test/fixtures/subcommand.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | cmd - program with subcommands 4 | 5 | # Commands 6 | 7 | * `ls, list` List tasks 8 | * `i, info` Print task information 9 | -------------------------------------------------------------------------------- /test/fixtures/synopsis-exapansion.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | prg - short program summary 4 | 5 | # Synopsis 6 | 7 | [flags] [options] [--xml|--html] 8 | 9 | # Options 10 | 11 | + `-X, --xml` Print as XML 12 | + `-H, --html` Print as HTML 13 | + `-V` Print more information 14 | + `-h, --help` Display help and exit 15 | + `--version` Print the version and exit 16 | 17 | -------------------------------------------------------------------------------- /test/fixtures/synopsis-illegal.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Synopsis 6 | 7 | Paragraph that will trigger an error. 8 | 9 | ``` 10 | [options] 11 | ``` 12 | -------------------------------------------------------------------------------- /test/fixtures/synopsis-name.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | prg - mock description 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [options] 9 | ``` 10 | 11 | ``` 12 | [options] [files...] 13 | ``` 14 | -------------------------------------------------------------------------------- /test/fixtures/synopsis.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | mock - mock description 4 | 5 | # Synopsis 6 | 7 | ``` 8 | [options] 9 | ``` 10 | 11 | ``` 12 | [options] [files...] 13 | ``` 14 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -u bdd 2 | --recursive 3 | --bail 4 | --check-leaks 5 | --reporter list 6 | --timeout 10000 7 | -A 8 | -------------------------------------------------------------------------------- /test/spec/camelcase.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , cli = require('../../index'); 3 | 4 | describe('cli:', function() { 5 | 6 | it('should convert option to camelcase', function(done) { 7 | expect(cli.camelcase('--file--path')).to.eql('filePath'); 8 | done(); 9 | }); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /test/spec/compile.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , cli = require('../../index'); 3 | 4 | describe('compile:', function() { 5 | 6 | it('should return stream', function(done) { 7 | var stream = cli.compiler(); 8 | expect(stream).to.be.an('object'); 9 | done(); 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/spec/dest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , cli = require('../../index'); 3 | 4 | describe('dest:', function() { 5 | 6 | it('should return stream', function(done) { 7 | var stream = cli.dest(); 8 | expect(stream).to.be.an('object'); 9 | done(); 10 | }); 11 | 12 | it('should return stream with type option', function(done) { 13 | var stream = cli.dest({type: 'help'}); 14 | expect(stream).to.be.an('object'); 15 | done(); 16 | }); 17 | 18 | it('should error with unsupported type', function(done) { 19 | function fn() { 20 | cli.dest({type: 'foo'}); 21 | } 22 | expect(fn).throws(/unknown output type/i); 23 | done(); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/spec/error/bad-name.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with bad name section', function(done) { 9 | var source = 'test/fixtures/bad-name.md' 10 | , target = 'target/bad-name.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/name section must begin with/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/duplicate-key-sparse.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with duplicate key in separate lists', function(done) { 9 | var source = 'test/fixtures/duplicate-key-sparse.md' 10 | , target = 'target/duplicate-key.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/duplicate key/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/duplicate-key.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with duplicate key', function(done) { 9 | var source = 'test/fixtures/duplicate-key.md' 10 | , target = 'target/duplicate-key.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/duplicate key/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/duplicate-name.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with duplicate name', function(done) { 9 | var source = 'test/fixtures/duplicate-name.md' 10 | , target = 'target/duplicate-name.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/duplicate options name detected/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/leading-content.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with leading content before name section', function(done) { 9 | var source = 'test/fixtures/leading-content.md' 10 | , target = 'target/leading-content.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/content before name section/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/name-not-first.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error when name section is not first section', function(done) { 9 | var source = 'test/fixtures/name-not-first.md' 10 | , target = 'target/name-not-first.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/name section must be declared first/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/no-specification.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with no specification', function(done) { 9 | var source = 'test/fixtures/no-specification.md' 10 | , target = 'target/no-specification.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/missing inline code specification/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/no-summary.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with no summary', function(done) { 9 | var source = 'test/fixtures/no-summary.md' 10 | , target = 'target/no-summary.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/program summary is required/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/error/synopsis-illegal.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('parser:', function() { 7 | 8 | it('should error with illegal type in synopsis section', function(done) { 9 | var source = 'test/fixtures/synopsis-illegal.md' 10 | , target = 'target/synopsis-illegal.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | function onFinish(err) { 25 | function fn() { 26 | throw err; 27 | } 28 | expect(fn).throws(/synopsis section may only contain/i); 29 | done(); 30 | } 31 | 32 | cli(opts, onFinish); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/format.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , optparse = require('../../lib/optparse') 3 | , format = require('../../lib/format'); 4 | 5 | describe('format:', function() { 6 | 7 | it('should format flag argument', function(done) { 8 | var res = optparse('-v, --verbose') 9 | , str = format(res); 10 | expect(str).to.eql('-v, --verbose'); 11 | done(); 12 | }); 13 | 14 | it('should format option argument', function(done) { 15 | var res = optparse('-o, --output=[FILE]') 16 | , str = format(res); 17 | expect(str).to.eql('-o, --output=[FILE]'); 18 | done(); 19 | }); 20 | 21 | it('should format required option argument', function(done) { 22 | var res = optparse('-o, --output ') 23 | , str = format(res); 24 | expect(str).to.eql('-o, --output='); 25 | done(); 26 | }); 27 | 28 | it('should use formatter function', function(done) { 29 | var res = optparse('-o, --output=[FILE]') 30 | , str = format(res, format.formatter); 31 | expect(str).to.eql('-o, --output=[FILE]'); 32 | done(); 33 | }); 34 | 35 | it('should format option argument with delimiter and assign', function(done) { 36 | var res = optparse('-o, --output=[FILE]') 37 | , str = format(res, {delimiter: ' | ', assign: ' '}); 38 | expect(str).to.eql('-o | --output [FILE]'); 39 | done(); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/help/cmd-style-w-header.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render cmd style', function(done) { 9 | var source = 'test/fixtures/program.md' 10 | , target = 'target/program.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | header: true, 24 | style: 'cmd' 25 | }; 26 | 27 | cli(opts); 28 | 29 | output.once('finish', function() { 30 | var result = '' + fs.readFileSync(target) 31 | expect(Boolean(~result.indexOf(''))).to.eql(true); 32 | expect(Boolean(~result.indexOf('i, info'))).to.eql(true); 33 | expect(Boolean(~result.indexOf('list, ls'))).to.eql(true); 34 | done(); 35 | }) 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/spec/help/cmd-style.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render cmd style', function(done) { 9 | var source = 'test/fixtures/program.md' 10 | , target = 'target/program.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | style: 'cmd' 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = '' + fs.readFileSync(target) 30 | expect(Boolean(~result.indexOf(''))).to.eql(true); 31 | expect(Boolean(~result.indexOf('i, info'))).to.eql(true); 32 | expect(Boolean(~result.indexOf('list, ls'))).to.eql(true); 33 | done(); 34 | }) 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /test/spec/help/command.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render command', function(done) { 9 | var source = 'test/fixtures/command.md' 10 | , target = 'target/command.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , valueCalled = false 20 | , kindCalled = false 21 | , headerCalled = false 22 | , footerCalled = false 23 | , opts = { 24 | input: input, 25 | output: output, 26 | type: cli.HELP, 27 | style: 'foo', 28 | align: 'right', 29 | usage: '', 30 | colon: ':', 31 | kind: function() { 32 | kindCalled = true; 33 | }, 34 | value: function() { 35 | valueCalled = true; 36 | }, 37 | header: function() { 38 | headerCalled = true; 39 | }, 40 | footer: function() { 41 | footerCalled = true; 42 | } 43 | }; 44 | 45 | cli(opts); 46 | 47 | output.once('finish', function() { 48 | 49 | expect(kindCalled).to.eql(true); 50 | expect(valueCalled).to.eql(true); 51 | expect(headerCalled).to.eql(true); 52 | expect(footerCalled).to.eql(true); 53 | 54 | var result = '' + fs.readFileSync(target) 55 | expect(Boolean(~result.indexOf('Commands:'))).to.eql(true); 56 | done(); 57 | }) 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/help/description.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render program description', function(done) { 9 | var source = 'test/fixtures/description.md' 10 | , target = 'target/description.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | desc: 2 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = '' + fs.readFileSync(target) 30 | expect(Boolean(~result.indexOf('description of the program'))) 31 | .to.eql(true); 32 | done(); 33 | }) 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/spec/help/empty.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , utils = require('../../util') 5 | , cli = require('../../../index'); 6 | 7 | describe('help renderer:', function() { 8 | 9 | it('should render empty definition', function(done) { 10 | var source = 'test/fixtures/empty.md' 11 | , target = 'target/empty.txt' 12 | , data = ast.parse('' + fs.readFileSync(source)) 13 | 14 | // mock file for correct relative path 15 | // mkcat normally injects this info 16 | data.file = source; 17 | 18 | var input = ast.serialize(data) 19 | , output = fs.createWriteStream(target) 20 | , opts = { 21 | input: input, 22 | output: output, 23 | type: cli.HELP 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = utils.result(target) 30 | expect(result).to.be.an('array'); 31 | expect(result[0].type).to.eql('document'); 32 | expect(result[1].type).to.eql('name'); 33 | expect(result[2].type).to.eql('synopsis'); 34 | expect(result[3].type).to.eql('description'); 35 | expect(result[4].type).to.eql('commands'); 36 | expect(result[5].type).to.eql('options'); 37 | expect(result[6].type).to.eql('eof'); 38 | done(); 39 | }) 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/help/list-style.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render list style', function(done) { 9 | var source = 'test/fixtures/program.md' 10 | , target = 'target/program.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | style: 'list' 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = '' + fs.readFileSync(target) 30 | expect(Boolean(~result.indexOf('--version\\n'))).to.eql(true); 31 | done(); 32 | }) 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/help/name.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render program name', function(done) { 9 | var source = 'test/fixtures/name.md' 10 | , target = 'target/name.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | header: true, 24 | footer: true, 25 | pkg: { 26 | name: 'foo', 27 | version: '1.0.0', 28 | homepage: 'http://example.com' 29 | } 30 | }; 31 | 32 | cli(opts); 33 | 34 | output.once('finish', function() { 35 | var result = '' + fs.readFileSync(target) 36 | expect(Boolean(~result.indexOf('Program-Name'))).to.eql(true); 37 | expect(Boolean(~result.indexOf('foo@1.0.0'))) 38 | .to.eql(true); 39 | expect(Boolean(~result.indexOf('http://example.com'))) 40 | .to.eql(true); 41 | done(); 42 | }) 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/spec/help/names.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render when multiple program names', function(done) { 9 | var source = 'test/fixtures/names.md' 10 | , target = 'target/names.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | header: true, 24 | footer: true, 25 | pkg: { 26 | name: 'foo', 27 | version: '1.0.0', 28 | homepage: 'http://example.com' 29 | } 30 | }; 31 | 32 | cli(opts); 33 | 34 | output.once('finish', function() { 35 | var result = '' + fs.readFileSync(target) 36 | // verifies not adding a period when terminated with a period 37 | expect(Boolean(~result.indexOf('Mock description.'))).to.eql(true); 38 | done(); 39 | }) 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/help/option-value.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render option w/ value spec ({=stdout})', function(done) { 9 | var source = 'test/fixtures/option-value.md' 10 | , target = 'target/option-value.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP 23 | }; 24 | 25 | cli(opts); 26 | 27 | output.once('finish', function() { 28 | var result = '' + fs.readFileSync(target) 29 | expect(Boolean(~result.indexOf('(default: stdout)'))).to.eql(true); 30 | expect(Boolean(~result.indexOf('Default: foo'))).to.eql(true); 31 | done(); 32 | }) 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/help/option.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render option help', function(done) { 9 | var source = 'test/fixtures/option.md' 10 | , target = 'target/option.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | footer: true, 24 | colon: true, 25 | pkg: {} 26 | }; 27 | 28 | cli(opts); 29 | 30 | output.once('finish', function() { 31 | var result = '' + fs.readFileSync(target) 32 | expect(Boolean(~result.indexOf('-f, --file'))).to.eql(true); 33 | done(); 34 | }) 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /test/spec/help/program.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render full program', function(done) { 9 | var source = 'test/fixtures/program.md' 10 | , target = 'target/program.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | footer: true, 24 | desc: 1, 25 | pkg: { 26 | name: 'foo', 27 | version: '1.0.0' 28 | } 29 | }; 30 | 31 | cli(opts); 32 | 33 | output.once('finish', function() { 34 | var result = '' + fs.readFileSync(target) 35 | expect(Boolean(~result.indexOf('program'))).to.eql(true); 36 | done(); 37 | }) 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/spec/help/section.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render section', function(done) { 9 | var source = 'test/fixtures/section.md' 10 | , target = 'target/section.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | newline: true, 24 | section: [ 25 | /env/i 26 | ] 27 | }; 28 | 29 | cli(opts); 30 | 31 | output.once('finish', function() { 32 | var result = '' + fs.readFileSync(target) 33 | expect(Boolean(~result.indexOf('Environment'))).to.eql(true); 34 | done(); 35 | }) 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/spec/help/synopsis.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render program synopsis', function(done) { 9 | var source = 'test/fixtures/synopsis.md' 10 | , target = 'target/synopsis.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP 23 | }; 24 | 25 | cli(opts); 26 | 27 | output.once('finish', function() { 28 | var result = '' + fs.readFileSync(target) 29 | expect(Boolean(~result.indexOf('[options]'))).to.eql(true); 30 | done(); 31 | }) 32 | }); 33 | 34 | it('should render program synopsis using name', function(done) { 35 | var source = 'test/fixtures/synopsis-name.md' 36 | , target = 'target/synopsis-name.txt' 37 | , data = ast.parse('' + fs.readFileSync(source)) 38 | 39 | // mock file for correct relative path 40 | // mkcat normally injects this info 41 | data.file = source; 42 | 43 | var input = ast.serialize(data) 44 | , output = fs.createWriteStream(target) 45 | , opts = { 46 | input: input, 47 | output: output, 48 | type: cli.HELP 49 | }; 50 | 51 | cli(opts); 52 | 53 | output.once('finish', function() { 54 | var result = '' + fs.readFileSync(target) 55 | expect(Boolean(~result.indexOf('[options]'))).to.eql(true); 56 | done(); 57 | }) 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/help/usage-style.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('help renderer:', function() { 7 | 8 | it('should render usage style', function(done) { 9 | var source = 'test/fixtures/program.md' 10 | , target = 'target/program.txt' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | type: cli.HELP, 23 | style: 'usage' 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = '' + fs.readFileSync(target) 30 | expect(Boolean(~result.indexOf('Usage:'))).to.eql(true); 31 | done(); 32 | }) 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/json/command.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse command', function(done) { 9 | var source = 'test/fixtures/command.md' 10 | , target = 'target/command.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | indent: 0 23 | }; 24 | 25 | cli(opts); 26 | 27 | output.once('finish', function() { 28 | var result = JSON.parse('' + fs.readFileSync(target)) 29 | , cmds = result.commands; 30 | 31 | expect(cmds.list).to.be.an('object'); 32 | expect(cmds.list.type).to.eql('command'); 33 | expect(cmds.list.key).to.eql('list'); 34 | expect(cmds.list.names).to.eql(['ls', 'list']); 35 | 36 | done(); 37 | }) 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/spec/json/commands.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse multiple commands', function(done) { 9 | var source = 'test/fixtures/commands.md' 10 | , target = 'target/commands.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , cmds = result.commands 29 | , keys = Object.keys(cmds); 30 | 31 | expect(keys.length).to.eql(2); 32 | 33 | expect(cmds.list).to.be.an('object'); 34 | expect(cmds.list.type).to.eql('command'); 35 | expect(cmds.list.key).to.eql('list'); 36 | expect(cmds.list.names).to.eql(['ls', 'list']); 37 | 38 | expect(cmds.info).to.be.an('object'); 39 | expect(cmds.info.type).to.eql('command'); 40 | expect(cmds.info.key).to.eql('info'); 41 | expect(cmds.info.names).to.eql(['i', 'info']); 42 | 43 | done(); 44 | }) 45 | }); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /test/spec/json/description.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse program description', function(done) { 9 | var source = 'test/fixtures/description.md' 10 | , target = 'target/description.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)); 28 | expect(result.description).to.be.a('string'); 29 | done(); 30 | }) 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/spec/json/empty.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse empty program', function(done) { 9 | var source = 'test/fixtures/empty.md' 10 | , target = 'target/empty.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)); 28 | expect(result.type).to.eql('program'); 29 | expect(result.name).to.eql(undefined); 30 | done(); 31 | }) 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/spec/json/flag.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse flag option', function(done) { 9 | var source = 'test/fixtures/flag.md' 10 | , target = 'target/flag.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.help).to.be.an('object'); 31 | expect(opts.help.type).to.eql('flag'); 32 | expect(opts.help.key).to.eql('help'); 33 | expect(opts.help.names).to.eql(['-h', '--help']); 34 | 35 | done(); 36 | }) 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/spec/json/multiple-option.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse option w/ multiple spec (...)', function(done) { 9 | var source = 'test/fixtures/multiple-option.md' 10 | , target = 'target/multiple-option.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.file).to.be.an('object'); 31 | expect(opts.file.type).to.eql('option'); 32 | expect(opts.file.multiple).to.eql(true); 33 | expect(opts.file.required).to.eql(false); 34 | expect(opts.file.key).to.eql('file'); 35 | expect(opts.file.names).to.eql(['-f', '--file']); 36 | 37 | done(); 38 | }) 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/spec/json/name.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse program name', function(done) { 9 | var source = 'test/fixtures/name.md' 10 | , target = 'target/name.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)); 28 | expect(result.name).to.eql('Program-Name'); 29 | done(); 30 | }) 31 | }); 32 | 33 | it('should parse program name w/ callback', function(done) { 34 | var source = 'test/fixtures/name.md' 35 | , target = 'target/name.json.log' 36 | , data = ast.parse('' + fs.readFileSync(source)) 37 | 38 | // mock file for correct relative path 39 | // mkcat normally injects this info 40 | data.file = source; 41 | 42 | var input = ast.serialize(data) 43 | , output = fs.createWriteStream(target) 44 | , opts = { 45 | input: input, 46 | output: output 47 | }; 48 | 49 | function onFinish() { 50 | var result = JSON.parse('' + fs.readFileSync(target)); 51 | expect(result.name).to.eql('Program-Name'); 52 | done(); 53 | } 54 | 55 | cli(opts, onFinish); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /test/spec/json/named-flag.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse named flag option', function(done) { 9 | var source = 'test/fixtures/named-flag.md' 10 | , target = 'target/named-flag.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.usage).to.be.an('object'); 31 | expect(opts.usage.key).to.eql('usage'); 32 | expect(opts.usage.names).to.eql(['-h', '--help']); 33 | 34 | done(); 35 | }) 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/spec/json/names.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse multiple program names', function(done) { 9 | var source = 'test/fixtures/names.md' 10 | , target = 'target/names.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)); 28 | expect(result.name).to.eql('prg'); 29 | expect(result.names).to.eql(['prg', 'prg-alias', 'prg-another-alias']); 30 | done(); 31 | }) 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/spec/json/option-type-value.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse option w/ type and value spec ({Number=2})', function(done) { 9 | var source = 'test/fixtures/option-type-value.md' 10 | , target = 'target/option-type-value.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.indent).to.be.an('object'); 31 | expect(opts.indent.type).to.eql('option'); 32 | expect(opts.indent.kind).to.eql('Number'); 33 | expect(opts.indent.value).to.eql('2'); 34 | expect(opts.indent.multiple).to.eql(false); 35 | expect(opts.indent.required).to.eql(false); 36 | expect(opts.indent.key).to.eql('indent'); 37 | expect(opts.indent.names).to.eql(['-i', '--indent']); 38 | 39 | done(); 40 | }) 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/spec/json/option-type.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse option w/ type spec ({Number})', function(done) { 9 | var source = 'test/fixtures/option-type.md' 10 | , target = 'target/option-type.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.indent).to.be.an('object'); 31 | expect(opts.indent.type).to.eql('option'); 32 | expect(opts.indent.kind).to.eql('Number'); 33 | expect(opts.indent.multiple).to.eql(false); 34 | expect(opts.indent.required).to.eql(false); 35 | expect(opts.indent.key).to.eql('indent'); 36 | expect(opts.indent.names).to.eql(['-i', '--indent']); 37 | 38 | done(); 39 | }) 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/json/option-value.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse option w/ value spec ({=stdout})', function(done) { 9 | var source = 'test/fixtures/option-value.md' 10 | , target = 'target/option-value.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.output).to.be.an('object'); 31 | expect(opts.output.type).to.eql('option'); 32 | expect(opts.output.kind).to.eql(undefined); 33 | expect(opts.output.value).to.eql('stdout'); 34 | expect(opts.output.multiple).to.eql(false); 35 | expect(opts.output.required).to.eql(false); 36 | expect(opts.output.key).to.eql('output'); 37 | expect(opts.output.names).to.eql(['-o', '--output']); 38 | 39 | done(); 40 | }) 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/spec/json/option.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse option', function(done) { 9 | var source = 'test/fixtures/option.md' 10 | , target = 'target/option.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.file).to.be.an('object'); 31 | expect(opts.file.type).to.eql('option'); 32 | expect(opts.file.multiple).to.eql(false); 33 | expect(opts.file.required).to.eql(false); 34 | expect(opts.file.key).to.eql('file'); 35 | 36 | done(); 37 | }) 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/spec/json/options.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , cli = require('../../../index'); 3 | 4 | describe('json:', function() { 5 | 6 | it('should return stream with no options', function(done) { 7 | var stream = cli(); 8 | expect(stream).to.be.an('object'); 9 | done(); 10 | }); 11 | 12 | it('should callback with error on bad type', function(done) { 13 | cli({type: 'foo'}, function(err) { 14 | function fn() { 15 | throw err; 16 | } 17 | expect(fn).throws(Error); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should throw error on bad type without a callback', function(done) { 23 | function fn() { 24 | cli({type: 'foo'}); 25 | } 26 | expect(fn).throws(Error); 27 | done(); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/spec/json/required-option.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse option w/ required spec (<>)', function(done) { 9 | var source = 'test/fixtures/required-option.md' 10 | , target = 'target/required-option.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)) 28 | , opts = result.options; 29 | 30 | expect(opts.file).to.be.an('object'); 31 | expect(opts.file.type).to.eql('option'); 32 | expect(opts.file.multiple).to.eql(false); 33 | expect(opts.file.required).to.eql(true); 34 | expect(opts.file.key).to.eql('file'); 35 | expect(opts.file.names).to.eql(['-f', '--file']); 36 | 37 | done(); 38 | }) 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/spec/json/section.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse program section', function(done) { 9 | var source = 'test/fixtures/section.md' 10 | , target = 'target/section.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | compact: false 23 | }; 24 | 25 | cli(opts); 26 | 27 | output.once('finish', function() { 28 | var result = JSON.parse('' + fs.readFileSync(target)); 29 | expect(result.sections).to.be.an('array').to.have.length(2); 30 | var section = result.sections[0]; 31 | expect(section.nodes).to.be.an('array').to.have.length(2); 32 | done(); 33 | }) 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/spec/json/subcommand.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse subcommand (recursive)', function(done) { 9 | var source = 'test/fixtures/subcommand.md' 10 | , target = 'target/subcommand.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output, 22 | // no need to specify this, it tests a code path 23 | recursive: true 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = JSON.parse('' + fs.readFileSync(target)) 30 | , cmds = result.commands; 31 | 32 | expect(cmds.list).to.be.an('object'); 33 | expect(cmds.list.type).to.eql('command'); 34 | expect(cmds.list.key).to.eql('list'); 35 | expect(cmds.list.names).to.eql(['ls', 'list']); 36 | 37 | // loaded sub-commands 38 | expect(cmds.list.commands).to.be.an('object'); 39 | expect(cmds.list.commands.tasks).to.be.an('object'); 40 | expect(cmds.list.commands.notes).to.be.an('object'); 41 | expect(cmds.list.commands.tracks).to.be.an('object'); 42 | 43 | // loaded command-specific options 44 | expect(cmds.list.options).to.be.an('object'); 45 | expect(cmds.list.options.subOption).to.be.an('object'); 46 | 47 | // loaded nested subcommand 48 | var notes = cmds.list.commands.notes; 49 | expect(notes.options).to.be.an('object'); 50 | expect(notes.options.all).to.be.an('object'); 51 | 52 | done(); 53 | }) 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/json/synopsis.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index'); 5 | 6 | describe('json:', function() { 7 | 8 | it('should parse program synopsis', function(done) { 9 | var source = 'test/fixtures/synopsis.md' 10 | , target = 'target/synopsis.json.log' 11 | , data = ast.parse('' + fs.readFileSync(source)) 12 | 13 | // mock file for correct relative path 14 | // mkcat normally injects this info 15 | data.file = source; 16 | 17 | var input = ast.serialize(data) 18 | , output = fs.createWriteStream(target) 19 | , opts = { 20 | input: input, 21 | output: output 22 | }; 23 | 24 | cli(opts); 25 | 26 | output.once('finish', function() { 27 | var result = JSON.parse('' + fs.readFileSync(target)); 28 | expect(result.synopsis).to.be.an('array') 29 | .to.have.length(2); 30 | expect(result.synopsis[0]).to.eql('[options]'); 31 | done(); 32 | }) 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/load.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , cli = require('../../index'); 3 | 4 | describe('cli:', function() { 5 | 6 | it('should return empty program from load()', function(done) { 7 | var prg = cli.load(); 8 | expect(prg).to.be.an('object'); 9 | done(); 10 | }); 11 | 12 | it('should return program from load()', function(done) { 13 | var def = {options: {foo:{}}} 14 | , prg = cli.load(def); 15 | expect(prg).to.be.an('object'); 16 | expect(prg.options.foo).to.be.an('object'); 17 | done(); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/man/name.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , fs = require('fs') 3 | , ast = require('mkast') 4 | , cli = require('../../../index') 5 | , utils = require('../../util'); 6 | 7 | describe('man renderer:', function() { 8 | 9 | it('should render program name', function(done) { 10 | var source = 'test/fixtures/name.md' 11 | , target = 'target/name-upper.json.log' 12 | , data = ast.parse('' + fs.readFileSync(source)) 13 | 14 | // mock file for correct relative path 15 | // mkcat normally injects this info 16 | data.file = source; 17 | 18 | var input = ast.serialize(data) 19 | , output = fs.createWriteStream(target) 20 | , opts = { 21 | input: input, 22 | output: output, 23 | type: cli.MAN 24 | }; 25 | 26 | cli(opts); 27 | 28 | output.once('finish', function() { 29 | var result = utils.result(target); 30 | // check conversion to upper case level one heading 31 | expect(result[1].firstChild.literal).to.eql('NAME'); 32 | done(); 33 | }) 34 | }); 35 | 36 | it('should render program name w/upper disabled', function(done) { 37 | var source = 'test/fixtures/name.md' 38 | , target = 'target/name.json.log' 39 | , data = ast.parse('' + fs.readFileSync(source)) 40 | 41 | // mock file for correct relative path 42 | // mkcat normally injects this info 43 | data.file = source; 44 | 45 | var input = ast.serialize(data) 46 | , output = fs.createWriteStream(target) 47 | , opts = { 48 | input: input, 49 | output: output, 50 | type: cli.MAN, 51 | preserve: true 52 | }; 53 | 54 | cli(opts); 55 | 56 | output.once('finish', function() { 57 | var result = utils.result(target); 58 | expect(result[1].firstChild.literal).to.eql('Name'); 59 | done(); 60 | }) 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/spec/optparse.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Argument = require('mkcli-runtime/lib/argument') 3 | , optparse = require('../../lib/optparse'); 4 | 5 | describe('optparse:', function() { 6 | 7 | it('should error with no names', function(done) { 8 | function fn() { 9 | optparse(''); 10 | } 11 | expect(fn).throws(/has no names/i); 12 | done(); 13 | }); 14 | 15 | it('should parse flag', function(done) { 16 | var res = optparse('-v, --verbose'); 17 | expect(res.type).to.eql(Argument.FLAG); 18 | expect(res.key).to.eql('verbose'); 19 | expect(res.names).to.eql(['-v', '--verbose']); 20 | done(); 21 | }); 22 | 23 | it('should parse flag with explicit key', function(done) { 24 | var res = optparse('more: -v, --verbose'); 25 | expect(res.type).to.eql(Argument.FLAG); 26 | expect(res.key).to.eql('more'); 27 | expect(res.names).to.eql(['-v', '--verbose']); 28 | done(); 29 | }); 30 | 31 | it('should sort with various names', function(done) { 32 | // NOTE: triggers sort() code paths 33 | var res = optparse('--verbose, -v, -v'); 34 | expect(res.type).to.eql(Argument.FLAG); 35 | expect(res.key).to.eql('verbose'); 36 | expect(res.names).to.eql(['--verbose', '-v', '-v']); 37 | done(); 38 | }); 39 | 40 | it('should parse option', function(done) { 41 | var res = optparse('-f, --file=[FILE]'); 42 | expect(res.type).to.eql(Argument.OPTION); 43 | expect(res.key).to.eql('file'); 44 | expect(res.names).to.eql(['-f', '--file']); 45 | done(); 46 | }); 47 | 48 | it('should parse option with explicit key', function(done) { 49 | var res = optparse('path: -f, --file=[FILE]'); 50 | expect(res.type).to.eql(Argument.OPTION); 51 | expect(res.key).to.eql('path'); 52 | expect(res.names).to.eql(['-f', '--file']); 53 | done(); 54 | }); 55 | 56 | it('should parse long option with hyphen', function(done) { 57 | var res = optparse('--file-path=[FILE]'); 58 | expect(res.type).to.eql(Argument.OPTION); 59 | expect(res.key).to.eql('filePath'); 60 | expect(res.names).to.eql(['--file-path']); 61 | done(); 62 | }); 63 | 64 | it('should parse long option with hyphen and camelcase disabled', 65 | function(done) { 66 | var res = optparse('--file-path=[FILE]', {camelcase: false}); 67 | expect(res.type).to.eql(Argument.OPTION); 68 | expect(res.key).to.eql('--file-path'); 69 | expect(res.names).to.eql(['--file-path']); 70 | done(); 71 | } 72 | ); 73 | 74 | it('should parse option w/ multiple ellipsis', function(done) { 75 | var res = optparse('-f, --file=[FILE...]'); 76 | expect(res.type).to.eql(Argument.OPTION); 77 | expect(res.key).to.eql('file'); 78 | expect(res.names).to.eql(['-f', '--file']); 79 | expect(res.multiple).to.eql(true); 80 | done(); 81 | }); 82 | 83 | it('should parse option w/ required brackets (<>)', function(done) { 84 | var res = optparse('-f, --file='); 85 | expect(res.type).to.eql(Argument.OPTION); 86 | expect(res.key).to.eql('file'); 87 | expect(res.names).to.eql(['-f', '--file']); 88 | expect(res.multiple).to.eql(true); 89 | expect(res.required).to.eql(true); 90 | done(); 91 | }); 92 | 93 | it('should parse option with type specification', function(done) { 94 | var res = optparse('-i, --indent [NUM] {Number}'); 95 | expect(res.type).to.eql(Argument.OPTION); 96 | expect(res.key).to.eql('indent'); 97 | expect(res.names).to.eql(['-i', '--indent']); 98 | expect(res.kind).to.eql('Number'); 99 | done(); 100 | }); 101 | 102 | it('should parse option with enum specification', function(done) { 103 | var res = optparse('-t, --type [VAL] {json|help|man}'); 104 | expect(res.type).to.eql(Argument.OPTION); 105 | expect(res.key).to.eql('type'); 106 | expect(res.names).to.eql(['-t', '--type']); 107 | expect(res.kind).to.eql(['json', 'help', 'man']); 108 | done(); 109 | }); 110 | 111 | it('should parse option with type and default value', function(done) { 112 | var res = optparse('-i, --indent [NUM] {Number=2}'); 113 | expect(res.type).to.eql(Argument.OPTION); 114 | expect(res.key).to.eql('indent'); 115 | expect(res.names).to.eql(['-i', '--indent']); 116 | expect(res.kind).to.eql('Number'); 117 | expect(res.value).to.eql('2'); 118 | done(); 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /test/spec/src.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , cli = require('../../index'); 3 | 4 | describe('src:', function() { 5 | 6 | it('should return stream', function(done) { 7 | var stream = cli.src(); 8 | expect(stream).to.be.an('object'); 9 | done(); 10 | }); 11 | 12 | it('should return stream with type option', function(done) { 13 | var stream = cli.src({type: 'help'}); 14 | expect(stream).to.be.an('object'); 15 | done(); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/state.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , State = require('../../lib/state'); 3 | 4 | describe('state:', function() { 5 | 6 | it('should create state ', function(done) { 7 | var state = new State(State.NAME); 8 | expect(state).to.be.an('object'); 9 | expect(state.nodes).to.eql([]); 10 | done(); 11 | }); 12 | 13 | it('should create state with chunk', function(done) { 14 | var state = new State(State.NAME, {foo: 'bar'}); 15 | expect(state).to.be.an('object'); 16 | expect(state.nodes).to.eql([{foo: 'bar'}]); 17 | done(); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | function getResult(target) { 4 | var result = ('' + fs.readFileSync(target)).trim().split('\n'); 5 | result = result.map(function(line) { 6 | var o = JSON.parse(line) 7 | return o; 8 | }) 9 | return result; 10 | } 11 | 12 | module.exports = { 13 | result: getResult 14 | } 15 | --------------------------------------------------------------------------------