/*1* cli.js2* Copyright (C) 2015 Kovid Goyal <kovid at kovidgoyal.net>3*4* Distributed under terms of the BSD license.5*/6"use strict"; /*jshint node:true */78var path = require("path");9var utils = require("./utils");10var colored = utils.colored;11var has_prop = Object.prototype.hasOwnProperty.call.bind(12Object.prototype.hasOwnProperty13);1415function OptionGroup(name) {16this.name = name;17this.description = undefined;18this.extra = undefined;19this.options = {20string: {},21boolean: {},22alias: {},23default: {},24choices: {},25};2627this.help = {};28this.seen = {};29}3031var groups = {},32group;3334function create_group(name, usage, description, extra) {35group = new OptionGroup(name);36var match = utils.comment_contents.exec(description.toString());37if (!match) {38throw new TypeError("Multiline comment missing for: " + name);39}40group.description = match[1];41group.usage = name + " [options] " + usage;42groups[name] = group;4344if (extra) {45match = utils.comment_contents.exec(extra.toString());46if (match) group.extra = match[1];47}4849opt("help", "h", "bool", false, function () {50/*51show this help message and exit52*/53});5455opt("version", "V", "bool", false, function () {56/*57show the version and exit58*/59});60}6162var COL1 = "yellow",63COL2 = "green";6465function print_usage(group) {66// {{{67var COL_WIDTH = 79;68var OPT_WIDTH = 23;6970var usage = group ? group.usage : "[subcommand] ...";71console.log(72colored("Usage:", COL1),73colored(path.basename(process.argv[1]), COL2),74usage,75"\n"76);77if (!group) {78// Overall usage79help =80"PyLang can perform many actions, depending on which" +81"\nsubcommand is invoked. With no arguments, it will start a REPL," +82"\nunless STDIN is a pipe, in which case it will compile whatever" +83"\nyou pass on STDIN and write the output to STDOUT. See the full" +84"\nlist of subcommands below.";85console.log(help, "\n");86console.log(colored("Subcommands:", COL1));87Object.keys(groups).forEach(function (name) {88console.log();89var dt = utils.wrap(90groups[name].description.split("\n"),91COL_WIDTH - OPT_WIDTH92);93console.log(94colored(95(name + utils.repeat(" ", OPT_WIDTH)).slice(0, OPT_WIDTH),96COL297),98dt[0]99);100dt.slice(1).forEach(function (line) {101console.log(utils.repeat(" ", OPT_WIDTH), line);102});103});104return;105}106107// Group specific usage108109console.log(group.description);110if (group.extra) console.log("\n" + group.extra);111console.log(colored("\nOptions:", COL1));112var options = group.options;113var help = group.help;114115Object.getOwnPropertyNames(options.alias).forEach(function (name) {116var optstr = " --" + name.replace(/_/g, "-");117options.alias[name].forEach(function (alias) {118optstr +=119", " + (alias.length > 1 ? "--" : "-") + alias.replace(/_/g, "-");120});121var ht = utils.wrap(help[name].split("\n"), COL_WIDTH - OPT_WIDTH);122123if (optstr.length > OPT_WIDTH) console.log(colored(optstr, COL2));124else {125console.log(126colored(127(optstr + utils.repeat(" ", OPT_WIDTH)).slice(0, OPT_WIDTH),128COL2129),130ht[0]131);132ht = ht.splice(1);133}134ht.forEach(function (line) {135console.log(utils.repeat(" ", OPT_WIDTH), line);136});137console.log();138});139} // }}}140141// Process options {{{142143function opt(name, aliases, type, default_val, help_text, choices) {144var match = utils.comment_contents.exec(help_text.toString());145var options = group.options;146var seen = group.seen;147var help = group.help;148149if (!match) {150throw new TypeError("Multiline comment missing for: " + name);151}152help_text = match[1];153154if (!type || type == "bool") options.boolean[name] = true;155else if (type == "string") {156options.string[name] = true;157if (choices) options.choices[name] = choices;158}159160if (default_val !== undefined) options.default[name] = default_val;161162if (aliases && aliases.length) {163aliases.split(",").forEach(function (alias) {164if (has_prop(seen, alias))165throw "The option name:" + alias + " has already been used.";166seen[alias] = true;167});168options.alias[name] = aliases.split(",");169} else options.alias[name] = [];170171if (has_prop(seen, name))172throw "The option name:" + name + " has already been used.";173seen[name] = true;174175help[name] = help_text;176}177// }}}178179function parse_args() {180// {{{181var ans = { files: [] };182var name_map = {};183var state, options, group;184185function plain_arg(arg) {186if (state !== undefined) ans[state] = arg;187else ans.files.push(arg);188state = undefined;189}190191function handle_opt(arg) {192var oarg = arg;193var is_long_opt = arg[0] === "-" ? true : false;194if (is_long_opt) arg = arg.substr(1);195if (state !== undefined) ans[state] = "";196state = undefined;197if (!is_long_opt && arg.length > 1) {198arg.split("").forEach(handle_opt);199return;200}201var val = arg.indexOf("=");202if (val > -1) {203var t = arg.substr(val + 1);204arg = arg.substr(0, val);205val = t;206} else val = undefined;207208var name = name_map[arg.replace(/-/g, "_")];209if (!name) {210print_usage(group);211console.error(212"\nThe option:",213colored("-" + oarg, "red"),214"is not recognized"215);216process.exit(1);217}218if (has_prop(options.boolean, name)) {219if (!val) val = "true";220if (val === "true" || val === "1") val = true;221else if (val === "false" || val === "0") val = false;222else {223console.error(224"The value:",225colored(val, "red"),226"is invalid for the boolean option:",227colored(name, "red")228);229process.exit(1);230}231ans[name] = val;232} else {233if (val !== undefined) ans[name] = val;234else state = name;235}236}237238var all_args = process.argv.slice(2);239ans.auto_mode = false;240if (has_prop(groups, all_args[0])) {241ans.mode = all_args[0];242all_args = all_args.slice(1);243} else {244// this check is not robust, but, it will only fail if the repl mode takes any non-boolean options245var has_files =246all_args.filter(function (a) {247return a[0] !== "-";248}).length > 0;249ans.mode = !has_files ? "repl" : "compile";250if (has_files) {251ans.execute = true;252}253ans.auto_mode = true;254}255options = groups[ans.mode].options;256257Object.getOwnPropertyNames(options.default).forEach(function (name) {258if (ans[name] == null) {259ans[name] = options["default"][name];260}261});262263Object.getOwnPropertyNames(options.alias).forEach(function (name) {264name_map[name] = name;265options.alias[name].forEach(function (alias) {266name_map[alias] = name;267});268});269270var options_ended = false;271272all_args.forEach(function (arg) {273if (options_ended) plain_arg(arg);274else if (arg === "--") options_ended = true;275else if (arg === "-") plain_arg(arg);276else if (arg[0] === "-") handle_opt(arg.substr(1));277else plain_arg(arg);278});279if (state !== undefined) plain_arg("");280Object.keys(options.choices).forEach(function (name) {281var allowed = options.choices[name];282if (allowed.indexOf(ans[name]) < 0) {283print_usage(groups[ans.mode]);284console.error(285'The value "' +286colored(ans[name], "red") +287'" is not allowed for ' +288colored(name, "red") +289". Allowed values: " +290options.choices[name].join(", ")291);292process.exit(1);293}294});295return ans;296} // }}}297298create_group("compile", "[input1.py input2.py ...]", function () {299/*300Compile PyLang source code into JavaScript301output. You can also pipe the source code into302stdin.303*/304});305306opt("output", "o", "string", "", function () {307/*308Output file (default STDOUT)309*/310});311312opt("bare", "b", "bool", false, function () {313/*314Remove the module wrapper that prevents PyLang315scope from bleeding into other JavaScript logic316*/317});318319opt("keep_docstrings", "d", "bool", false, function () {320/*321Keep the docstrings in the generated JavaScript as __doc__322attributes on functions, classes and modules. Normally,323the docstring are deleted to reduce download size.324*/325});326327opt("discard_asserts", "a", "bool", false, function () {328/*329Discard any assert statements. If you use assert statements330for debugging, then use this option to generate an optimized build331without the assert statements.332*/333});334335opt("omit_baselib", "m", "bool", false, function () {336/*337Omit baselib functions. Use this if you have a338different way of ensuring they're imported. For example,339you could import one of the baselib-plain-*.js files directly340into the global namespace.341*/342});343344opt("import_path", "p", "string", "", function () {345/*346A list of paths in which to look for imported modules.347Multiple paths must be separated by the path separator348(: on Unix and ; on Windows). You can also use the349environment variable PYLANGPATH for this,350with identical syntax. Note that these directories351are searched before the builtin paths, which means you352can use them to replace builtin modules.353*/354});355356opt("filename_for_stdin", "P", "string", "", function () {357/*358filename to use for data piped into STDIN. Imports will359be resolved relative to the directory this filename is in.360Note, that you can also use the --import-path option to361add directories to the import path.362*/363});364365opt("cache_dir", "C", "string", "", function () {366/*367directory to use to store the cached files generated368by the compiler. If set to '' (the default) then no369cached files are stored at all.370*/371});372373opt("comments", undefined, "string", "", function () {374/*375Preserve copyright comments in the output.376By default this works like Google Closure, keeping377JSDoc-style comments that contain "@license" or378"@preserve". You can optionally pass one of the379following arguments to this flag:380- "all" to keep all comments381- a valid JS regexp (needs to start with a slash) to382keep only comments that match.383384Note that currently not *all* comments can be kept385when compression is on, because of dead code removal386or cascading statements into sequences.387*/388});389390opt("stats", undefined, "bool", false, function () {391/*392Display operations run time on STDERR.393*/394});395396opt("execute", "x,exec", "bool", false, function () {397/*398Compile and execute the PyLang code, all in399one invocation. Useful if you wish to use PyLang for400scripting. Note that you can also use the -o option to401have the compiled JavaScript written out to a file402before being executed. If you specify this option you403should not specify the -m option to omit the baselib, or404execution will fail.405*/406});407408create_group("repl", "", function () {409/*410Run a Read-Eval-Print-Loop (REPL). This allows411you to type and run PyLang at a live412command prompt. Type show_js=True to show Javascript.413*/414});415416opt("no_js", "", "bool", true, function () {417/*418Do not display the compiled JavaScript before executing419it.420*/421});422423opt("jsage", "", "bool", false, function () {424/*425Enable everything implemented from our Sage-style preparser426*/427});428429opt("tokens", "", "bool", false, function () {430/*431Show every token as they are parsed.432*/433});434435create_group(436"lint",437"[input1.py input2.py ...]",438function () {439/*440Run the PyLang linter. This will find various441possible problems in the .py files you specify and442write messages about them to stdout. Use - to read from STDIN.443The main check it performs is for unused/undefined444symbols, like pyflakes does for python.445*/446},447function () {448/*449In addition to the command line options listed below,450you can also control the linter in a couple of other ways.451452In the actual source files, you can turn off specific checks453on a line by line basis by adding: # noqa:check1,check2...454to the end of the line. For example:455456f() # noqa: undef457458will prevent the linter from showing undefined symbol459errors for this line. You can also turn off individual checks460at the file level, by putting the noqa directive on a461line by itself near the top of the file, for example:462463# noqa: undef464465Similarly, you can tell the linter466about global (builtin) symbols with a comment near the top467of the file, for example:468469# globals:assert,myglobalvar470471This will prevent the linter form treating these names as472undefined symbols.473474Finally, the linter looks for a setup.cfg file in the475directory containing the file being linted or any of its476parent directories. You can both turn off individual checks477and define project specific global symbols in the setup.cfg478file, like this:479480[rapydscript]481globals=myglobalvar,otherglobalvar482noqa=undef,eol-semicolon483484*/485}486);487488opt("globals", "g,b,builtins", "string", "", function () {489/*490Comma separated list of additional names that the linter will491treat as global symbols. It ignores undefined errors for492global symbols.493*/494});495496opt("noqa", "e,ignore,exclude", "string", "", function () {497/*498Comma separated list of linter checks to skip. The linter499will not report errors corresponding to these checks.500The check names are output in the linter's normal output, you501can also list all check names with --noqa-list.502*/503});504505opt(506"errorformat",507"f,s,style",508"string",509"human",510function () {511/*512Output the results in the specified format. Valid formats are:513human - output is suited for reading by humans (the default)514json - output is in JSON format515vim - output can be consumed easily by vim's errorformat516directive. Format is:517filename:line:col:errortype:token:message [identifier]518undef - output only the names of undefined symbols in a form that519can be easily copy/pasted520*/521},522["human", "json", "vim", "undef"]523);524525opt("noqa_list", "", "bool", false, function () {526/*527List all available linter checks, with a brief528description, and exit.529*/530});531532opt("stdin_filename", "", "string", "STDIN", function () {533/*534The filename for data read from STDIN. If not specified535STDIN is used.536*/537});538539create_group("test", "[test1 test2...]", function () {540/*541Run PyLang tests. You can specify the name of542individual test files to only run tests from those543files. For example:544test baselib functions545*/546});547548create_group("self", "", function () {549/*550Compile the compiler itself. It will only actually551compile if something has changed since the last time552it was called. To force a recompilation, simply553delete lib/signatures.json554*/555});556557opt("complete", "c,f,full", "bool", false, function () {558/*559Run the compilation repeatedly, as many times as neccessary,560so that the compiler is built with the most up to date version561of itself.562*/563});564565opt("test", "t", "bool", false, function () {566/*567Run the test suite after building completes.568*/569});570571opt("profile", "p", "bool", false, function () {572/*573Run a CPU profiler which will output its data to574self.cpuprofile. The data can then be analysed with575node-inspector.576*/577});578579opt("omit_header", "m", "bool", false, function () {580/*581Do not write header with 'msgid ""' entry.582*/583});584585opt("package_name", "", "string", "XXX", function () {586/*587Set the package name in the header588*/589});590591opt("base_path", "", "string", "", function () {592/*593Sets the base path of the source code, instead of594automatically determining it from the bin.595This is very useful since it allows us to compile596the source of one version of the compiler using597a binary distribution of an older version, hence598we can bootstrap without having to store binaries599in our Git repo.600*/601});602603opt("package_version", "", "string", "XXX", function () {604/*605Set the package version in the header606*/607});608609opt("bugs_address", "bug_address", "string", "[email protected]", function () {610/*611Set the email address for bug reports in the header612*/613});614615create_group(616"msgfmt",617"",618function () {619/*620Compile a .po file into a .json file that can621be used to load translations in a browser.622*/623},624function () {625/*626The .po file is read from627stdin and the .json file written to stdout. Note628that it is assumed the .po file is encoded in UTF-8.629If you .po file is in some other encoding, you will need to630convert it to UTF-8 first.631*/632}633);634635opt("use_fuzzy", "f", "bool", false, function () {636/*637Use fuzzy translations, they are ignored by default.638*/639});640641var argv = (module.exports.argv = parse_args());642643if (argv.help) {644print_usage(!argv.auto_mode ? groups[argv.mode] : undefined);645process.exit(0);646}647648if (argv.version) {649var json = require("../package.json");650console.log(json.name + " " + json.version);651process.exit(0);652}653654655