├── .gitmodules ├── .npmignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Makefile.targ ├── README.md ├── examples └── simple.js ├── jsl.node.conf ├── lib └── extsprintf.js ├── package.json └── test ├── tst.basic.js └── tst.invalid.js /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/catest"] 2 | path = deps/catest 3 | url = https://github.com/joyent/catest 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /deps 2 | /examples 3 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Not yet released 4 | 5 | None yet. 6 | 7 | ## v1.4.0 8 | 9 | * #13 could provide better error messages for programmer errors 10 | * #14 bring extsprintf into the modern world 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository uses GitHub pull requests for code review. 4 | 5 | See the [Joyent Engineering 6 | Guidelines](https://github.com/joyent/eng/blob/master/docs/index.md) for general 7 | best practices expected in this repository. 8 | 9 | Contributions should be "make prepush" clean. This target requires separate 10 | tools: 11 | 12 | * https://github.com/davepacheco/jsstyle 13 | * https://github.com/davepacheco/javascriptlint 14 | * https://github.com/joyent/catest 15 | 16 | If you're changing something non-trivial or user-facing, you may want to submit 17 | an issue first. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Joyent, Inc. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2017, Joyent, Inc. All rights reserved. 3 | # 4 | # Makefile: top-level Makefile 5 | # 6 | # This Makefile contains only repo-specific logic and uses included makefiles 7 | # to supply common targets (javascriptlint, jsstyle, restdown, etc.), which are 8 | # used by other repos as well. 9 | # 10 | 11 | # 12 | # Files 13 | # 14 | CATEST = deps/catest/catest 15 | JSL = jsl 16 | JSSTYLE = jsstyle 17 | JS_FILES := $(shell find examples lib -name '*.js') 18 | JSL_FILES_NODE = $(JS_FILES) 19 | JSSTYLE_FILES = $(JS_FILES) 20 | JSL_CONF_NODE = jsl.node.conf 21 | 22 | # Default target is "check" 23 | check: 24 | 25 | test: | $(CATEST) 26 | $(CATEST) -a 27 | 28 | CATEST: deps/catest/.git 29 | 30 | include ./Makefile.targ 31 | -------------------------------------------------------------------------------- /Makefile.targ: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.targ: common targets. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This Makefile defines several useful targets and rules. You can use it by 13 | # including it from a Makefile that specifies some of the variables below. 14 | # 15 | # Targets defined in this Makefile: 16 | # 17 | # check Checks JavaScript files for lint and style 18 | # Checks bash scripts for syntax 19 | # Checks SMF manifests for validity against the SMF DTD 20 | # 21 | # clean Removes built files 22 | # 23 | # docs Builds restdown documentation in docs/ 24 | # 25 | # prepush Depends on "check" and "test" 26 | # 27 | # test Does nothing (you should override this) 28 | # 29 | # xref Generates cscope (source cross-reference index) 30 | # 31 | # For details on what these targets are supposed to do, see the Joyent 32 | # Engineering Guide. 33 | # 34 | # To make use of these targets, you'll need to set some of these variables. Any 35 | # variables left unset will simply not be used. 36 | # 37 | # BASH_FILES Bash scripts to check for syntax 38 | # (paths relative to top-level Makefile) 39 | # 40 | # CLEAN_FILES Files to remove as part of the "clean" target. Note 41 | # that files generated by targets in this Makefile are 42 | # automatically included in CLEAN_FILES. These include 43 | # restdown-generated HTML and JSON files. 44 | # 45 | # DOC_FILES Restdown (documentation source) files. These are 46 | # assumed to be contained in "docs/", and must NOT 47 | # contain the "docs/" prefix. 48 | # 49 | # JSL_CONF_NODE Specify JavaScriptLint configuration files 50 | # JSL_CONF_WEB (paths relative to top-level Makefile) 51 | # 52 | # Node.js and Web configuration files are separate 53 | # because you'll usually want different global variable 54 | # configurations. If no file is specified, none is given 55 | # to jsl, which causes it to use a default configuration, 56 | # which probably isn't what you want. 57 | # 58 | # JSL_FILES_NODE JavaScript files to check with Node config file. 59 | # JSL_FILES_WEB JavaScript files to check with Web config file. 60 | # 61 | # You can also override these variables: 62 | # 63 | # BASH Path to bash (default: bash) 64 | # 65 | # CSCOPE_DIRS Directories to search for source files for the cscope 66 | # index. (default: ".") 67 | # 68 | # JSL Path to JavaScriptLint (default: "jsl") 69 | # 70 | # JSL_FLAGS_NODE Additional flags to pass through to JSL 71 | # JSL_FLAGS_WEB 72 | # JSL_FLAGS 73 | # 74 | # JSSTYLE Path to jsstyle (default: jsstyle) 75 | # 76 | # JSSTYLE_FLAGS Additional flags to pass through to jsstyle 77 | # 78 | 79 | # 80 | # Defaults for the various tools we use. 81 | # 82 | BASH ?= bash 83 | BASHSTYLE ?= tools/bashstyle 84 | CP ?= cp 85 | CSCOPE ?= cscope 86 | CSCOPE_DIRS ?= . 87 | JSL ?= jsl 88 | JSSTYLE ?= jsstyle 89 | MKDIR ?= mkdir -p 90 | MV ?= mv 91 | RESTDOWN_FLAGS ?= 92 | RMTREE ?= rm -rf 93 | JSL_FLAGS ?= --nologo --nosummary 94 | 95 | ifeq ($(shell uname -s),SunOS) 96 | TAR ?= gtar 97 | else 98 | TAR ?= tar 99 | endif 100 | 101 | 102 | # 103 | # Defaults for other fixed values. 104 | # 105 | BUILD = build 106 | DISTCLEAN_FILES += $(BUILD) 107 | DOC_BUILD = $(BUILD)/docs/public 108 | 109 | # 110 | # Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. 111 | # 112 | ifneq ($(origin JSL_CONF_NODE), undefined) 113 | JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) 114 | endif 115 | 116 | ifneq ($(origin JSL_CONF_WEB), undefined) 117 | JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) 118 | endif 119 | 120 | # 121 | # Targets. For descriptions on what these are supposed to do, see the 122 | # Joyent Engineering Guide. 123 | # 124 | 125 | # 126 | # Instruct make to keep around temporary files. We have rules below that 127 | # automatically update git submodules as needed, but they employ a deps/*/.git 128 | # temporary file. Without this directive, make tries to remove these .git 129 | # directories after the build has completed. 130 | # 131 | .SECONDARY: $($(wildcard deps/*):%=%/.git) 132 | 133 | # 134 | # This rule enables other rules that use files from a git submodule to have 135 | # those files depend on deps/module/.git and have "make" automatically check 136 | # out the submodule as needed. 137 | # 138 | deps/%/.git: 139 | git submodule update --init deps/$* 140 | 141 | # 142 | # These recipes make heavy use of dynamically-created phony targets. The parent 143 | # Makefile defines a list of input files like BASH_FILES. We then say that each 144 | # of these files depends on a fake target called filename.bashchk, and then we 145 | # define a pattern rule for those targets that runs bash in check-syntax-only 146 | # mode. This mechanism has the nice properties that if you specify zero files, 147 | # the rule becomes a noop (unlike a single rule to check all bash files, which 148 | # would invoke bash with zero files), and you can check individual files from 149 | # the command line with "make filename.bashchk". 150 | # 151 | .PHONY: check-bash 152 | check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) 153 | 154 | %.bashchk: % 155 | $(BASH) -n $^ 156 | 157 | %.bashstyle: % 158 | $(BASHSTYLE) $^ 159 | 160 | .PHONY: check-jsl check-jsl-node check-jsl-web 161 | check-jsl: check-jsl-node check-jsl-web 162 | 163 | check-jsl-node: $(JSL_FILES_NODE:%=%.jslnodechk) 164 | 165 | check-jsl-web: $(JSL_FILES_WEB:%=%.jslwebchk) 166 | 167 | %.jslnodechk: % $(JSL_EXEC) 168 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $< 169 | 170 | %.jslwebchk: % $(JSL_EXEC) 171 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $< 172 | 173 | .PHONY: check-jsstyle 174 | check-jsstyle: $(JSSTYLE_FILES:%=%.jsstylechk) 175 | 176 | %.jsstylechk: % $(JSSTYLE_EXEC) 177 | $(JSSTYLE) $(JSSTYLE_FLAGS) $< 178 | 179 | .PHONY: check 180 | check: check-jsl check-jsstyle check-bash 181 | @echo check ok 182 | 183 | .PHONY: clean 184 | clean:: 185 | -$(RMTREE) $(CLEAN_FILES) 186 | 187 | .PHONY: distclean 188 | distclean:: clean 189 | -$(RMTREE) $(DISTCLEAN_FILES) 190 | 191 | CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out 192 | CLEAN_FILES += $(CSCOPE_FILES) 193 | 194 | .PHONY: xref 195 | xref: cscope.files 196 | $(CSCOPE) -bqR 197 | 198 | .PHONY: cscope.files 199 | cscope.files: 200 | find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ 201 | -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ 202 | 203 | # 204 | # The "docs" target is complicated because we do several things here: 205 | # 206 | # (1) Use restdown to build HTML and JSON files from each of DOC_FILES. 207 | # 208 | # (2) Copy these files into $(DOC_BUILD) (build/docs/public), which 209 | # functions as a complete copy of the documentation that could be 210 | # mirrored or served over HTTP. 211 | # 212 | # (3) Then copy any directories and media from docs/media into 213 | # $(DOC_BUILD)/media. This allows projects to include their own media, 214 | # including files that will override same-named files provided by 215 | # restdown. 216 | # 217 | # Step (3) is the surprisingly complex part: in order to do this, we need to 218 | # identify the subdirectories in docs/media, recreate them in 219 | # $(DOC_BUILD)/media, then do the same with the files. 220 | # 221 | DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") 222 | DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) 223 | DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) 224 | 225 | DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) 226 | DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) 227 | DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) 228 | 229 | # 230 | # Like the other targets, "docs" just depends on the final files we want to 231 | # create in $(DOC_BUILD), leveraging other targets and recipes to define how 232 | # to get there. 233 | # 234 | .PHONY: docs 235 | docs: \ 236 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.html) \ 237 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.json) \ 238 | $(DOC_MEDIA_FILES_BUILD) 239 | 240 | # 241 | # We keep the intermediate files so that the next build can see whether the 242 | # files in DOC_BUILD are up to date. 243 | # 244 | .PRECIOUS: \ 245 | $(DOC_FILES:%.restdown=docs/%.html) \ 246 | $(DOC_FILES:%.restdown=docs/%json) 247 | 248 | # 249 | # We do clean those intermediate files, as well as all of DOC_BUILD. 250 | # 251 | CLEAN_FILES += \ 252 | $(DOC_BUILD) \ 253 | $(DOC_FILES:%.restdown=docs/%.html) \ 254 | $(DOC_FILES:%.restdown=docs/%.json) 255 | 256 | # 257 | # Before installing the files, we must make sure the directories exist. The | 258 | # syntax tells make that the dependency need only exist, not be up to date. 259 | # Otherwise, it might try to rebuild spuriously because the directory itself 260 | # appears out of date. 261 | # 262 | $(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) 263 | 264 | $(DOC_BUILD)/%: docs/% | $(DOC_BUILD) 265 | $(CP) $< $@ 266 | 267 | docs/%.json docs/%.html: docs/%.restdown | $(DOC_BUILD) $(RESTDOWN_EXEC) 268 | $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< 269 | 270 | $(DOC_BUILD): 271 | $(MKDIR) $@ 272 | 273 | $(DOC_MEDIA_DIRS_BUILD): 274 | $(MKDIR) $@ 275 | 276 | # 277 | # The default "test" target does nothing. This should usually be overridden by 278 | # the parent Makefile. It's included here so we can define "prepush" without 279 | # requiring the repo to define "test". 280 | # 281 | .PHONY: test 282 | test: 283 | 284 | .PHONY: prepush 285 | prepush: check test 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extsprintf: extended POSIX-style sprintf 2 | 3 | Stripped down version of s[n]printf(3c). We make a best effort to throw an 4 | exception when given a format string we don't understand, rather than ignoring 5 | it, so that we won't break existing programs if/when we go implement the rest 6 | of this. 7 | 8 | This implementation currently supports specifying 9 | 10 | * field alignment ('-' flag), 11 | * zero-pad ('0' flag) 12 | * always show numeric sign ('+' flag), 13 | * field width 14 | * conversions for strings, decimal integers, and floats (numbers). 15 | * argument size specifiers. These are all accepted but ignored, since 16 | Javascript has no notion of the physical size of an argument. 17 | 18 | Everything else is currently unsupported, most notably: precision, unsigned 19 | numbers, non-decimal numbers, and characters. 20 | 21 | Besides the usual POSIX conversions, this implementation supports: 22 | 23 | * `%j`: pretty-print a JSON object (using node's "inspect") 24 | * `%r`: pretty-print an Error object 25 | 26 | # Example 27 | 28 | First, install it: 29 | 30 | # npm install extsprintf 31 | 32 | Now, use it: 33 | 34 | var mod_extsprintf = require('extsprintf'); 35 | console.log(mod_extsprintf.sprintf('hello %25s', 'world')); 36 | 37 | outputs: 38 | 39 | hello world 40 | 41 | # Also supported 42 | 43 | **printf**: same args as sprintf, but prints the result to stdout 44 | 45 | **fprintf**: same args as sprintf, preceded by a Node stream. Prints the result 46 | to the given stream. 47 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | var mod_extsprintf = require('extsprintf'); 2 | console.log(mod_extsprintf.sprintf('hello %25s', 'world')); 3 | 4 | mod_extsprintf.printf('hi there\nvia %20s\n', 'printf'); 5 | mod_extsprintf.fprintf(process.stderr, 'sent to std%6s %03d\n', 'err', 7); 6 | -------------------------------------------------------------------------------- /jsl.node.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 3 | # 4 | # This configuration file can be used to lint a collection of scripts, or to enable 5 | # or disable warnings for scripts that are linted via the command line. 6 | # 7 | 8 | ### Warnings 9 | # Enable or disable warnings based on requirements. 10 | # Use "+WarningName" to display or "-WarningName" to suppress. 11 | # 12 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent 13 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 14 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 15 | +anon_no_return_value # anonymous function does not always return value 16 | +assign_to_function_call # assignment to a function call 17 | -block_without_braces # block statement without curly braces 18 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 19 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 20 | +default_not_at_end # the default case is not at the end of the switch statement 21 | +dup_option_explicit # duplicate "option explicit" control comment 22 | +duplicate_case_in_switch # duplicate case in switch statement 23 | +duplicate_formal # duplicate formal argument {name} 24 | +empty_statement # empty statement or extra semicolon 25 | +identifier_hides_another # identifer {name} hides an identifier in a parent scope 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. 28 | +invalid_fallthru # unexpected "fallthru" control comment 29 | +invalid_pass # unexpected "pass" control comment 30 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 33 | +meaningless_block # meaningless block; curly braces have no impact 34 | +mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence 35 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +missing_break # missing break statement 37 | +missing_break_for_last_case # missing break statement for last case in switch 38 | +missing_default_case # missing default case in switch statement 39 | +missing_option_explicit # the "option explicit" control comment is missing 40 | +missing_semicolon # missing semicolon 41 | +missing_semicolon_for_lambda # missing semicolon for lambda assignment 42 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 43 | +nested_comment # nested comment 44 | +no_return_value # function {name} does not always return a value 45 | +octal_number # leading zeros make an octal number 46 | +parseint_missing_radix # parseInt missing radix parameter 47 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 48 | +redeclared_var # redeclaration of {name} 49 | +trailing_comma_in_array # extra comma is not recommended in array initializers 50 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 51 | +undeclared_identifier # undeclared identifier: {name} 52 | +unreachable_code # unreachable code 53 | -unreferenced_argument # argument declared but never referenced: {name} 54 | -unreferenced_function # function is declared but never referenced: {name} 55 | +unreferenced_variable # variable is declared but never referenced: {name} 56 | +unsupported_version # JavaScript {version} is not supported 57 | +use_of_label # use of label 58 | +useless_assign # useless assignment 59 | +useless_comparison # useless comparison; comparing identical expressions 60 | -useless_quotes # the quotation marks are unnecessary 61 | +useless_void # use of the void type may be unnecessary (void is always undefined) 62 | +var_hides_arg # variable {name} hides argument 63 | +want_assign_or_call # expected an assignment or function call 64 | +with_statement # with statement hides undeclared variables; use temporary variable instead 65 | 66 | 67 | ### Output format 68 | # Customize the format of the error message. 69 | # __FILE__ indicates current file path 70 | # __FILENAME__ indicates current file name 71 | # __LINE__ indicates current line 72 | # __COL__ indicates current column 73 | # __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) 74 | # __ERROR_NAME__ indicates error name (used in configuration file) 75 | # __ERROR_PREFIX__ indicates error prefix 76 | # __ERROR_MSG__ indicates error message 77 | # 78 | # For machine-friendly output, the output format can be prefixed with 79 | # "encode:". If specified, all items will be encoded with C-slashes. 80 | # 81 | # Visual Studio syntax (default): 82 | +output-format __FILE__(__LINE__): __ERROR__ 83 | # Alternative syntax: 84 | #+output-format __FILE__:__LINE__: __ERROR__ 85 | 86 | 87 | ### Context 88 | # Show the in-line position of the error. 89 | # Use "+context" to display or "-context" to suppress. 90 | # 91 | +context 92 | 93 | 94 | ### Control Comments 95 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 96 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 97 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 98 | # although legacy control comments are enabled by default for backward compatibility. 99 | # 100 | -legacy_control_comments 101 | 102 | 103 | ### Defining identifiers 104 | # By default, "option explicit" is enabled on a per-file basis. 105 | # To enable this for all files, use "+always_use_option_explicit" 106 | -always_use_option_explicit 107 | 108 | # Define certain identifiers of which the lint is not aware. 109 | # (Use this in conjunction with the "undeclared identifier" warning.) 110 | # 111 | # Common uses for webpages might be: 112 | +define __dirname 113 | +define clearInterval 114 | +define clearTimeout 115 | +define console 116 | +define exports 117 | +define global 118 | +define process 119 | +define require 120 | +define setInterval 121 | +define setTimeout 122 | +define Buffer 123 | +define JSON 124 | +define Math 125 | 126 | ### JavaScript Version 127 | # To change the default JavaScript version: 128 | #+default-type text/javascript;version=1.5 129 | #+default-type text/javascript;e4x=1 130 | 131 | ### Files 132 | # Specify which files to lint 133 | # Use "+recurse" to enable recursion (disabled by default). 134 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 135 | # or "+process Folder\Path\*.htm". 136 | # 137 | 138 | -------------------------------------------------------------------------------- /lib/extsprintf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * extsprintf.js: extended POSIX-style sprintf 3 | */ 4 | 5 | var mod_assert = require('assert'); 6 | var mod_util = require('util'); 7 | 8 | /* 9 | * Public interface 10 | */ 11 | exports.sprintf = jsSprintf; 12 | exports.printf = jsPrintf; 13 | exports.fprintf = jsFprintf; 14 | 15 | /* 16 | * Stripped down version of s[n]printf(3c). We make a best effort to throw an 17 | * exception when given a format string we don't understand, rather than 18 | * ignoring it, so that we won't break existing programs if/when we go implement 19 | * the rest of this. 20 | * 21 | * This implementation currently supports specifying 22 | * - field alignment ('-' flag), 23 | * - zero-pad ('0' flag) 24 | * - always show numeric sign ('+' flag), 25 | * - field width 26 | * - conversions for strings, decimal integers, and floats (numbers). 27 | * - argument size specifiers. These are all accepted but ignored, since 28 | * Javascript has no notion of the physical size of an argument. 29 | * 30 | * Everything else is currently unsupported, most notably precision, unsigned 31 | * numbers, non-decimal numbers, and characters. 32 | */ 33 | function jsSprintf(ofmt) 34 | { 35 | var regex = [ 36 | '([^%]*)', /* normal text */ 37 | '%', /* start of format */ 38 | '([\'\\-+ #0]*?)', /* flags (optional) */ 39 | '([1-9]\\d*)?', /* width (optional) */ 40 | '(\\.([1-9]\\d*))?', /* precision (optional) */ 41 | '[lhjztL]*?', /* length mods (ignored) */ 42 | '([diouxXfFeEgGaAcCsSp%jr])' /* conversion */ 43 | ].join(''); 44 | 45 | var re = new RegExp(regex); 46 | 47 | /* variadic arguments used to fill in conversion specifiers */ 48 | var args = Array.prototype.slice.call(arguments, 1); 49 | /* remaining format string */ 50 | var fmt = ofmt; 51 | 52 | /* components of the current conversion specifier */ 53 | var flags, width, precision, conversion; 54 | var left, pad, sign, arg, match; 55 | 56 | /* return value */ 57 | var ret = ''; 58 | 59 | /* current variadic argument (1-based) */ 60 | var argn = 1; 61 | /* 0-based position in the format string that we've read */ 62 | var posn = 0; 63 | /* 1-based position in the format string of the current conversion */ 64 | var convposn; 65 | /* current conversion specifier */ 66 | var curconv; 67 | 68 | mod_assert.equal('string', typeof (fmt), 69 | 'first argument must be a format string'); 70 | 71 | while ((match = re.exec(fmt)) !== null) { 72 | ret += match[1]; 73 | fmt = fmt.substring(match[0].length); 74 | 75 | /* 76 | * Update flags related to the current conversion specifier's 77 | * position so that we can report clear error messages. 78 | */ 79 | curconv = match[0].substring(match[1].length); 80 | convposn = posn + match[1].length + 1; 81 | posn += match[0].length; 82 | 83 | flags = match[2] || ''; 84 | width = match[3] || 0; 85 | precision = match[4] || ''; 86 | conversion = match[6]; 87 | left = false; 88 | sign = false; 89 | pad = ' '; 90 | 91 | if (conversion == '%') { 92 | ret += '%'; 93 | continue; 94 | } 95 | 96 | if (args.length === 0) { 97 | throw (jsError(ofmt, convposn, curconv, 98 | 'has no matching argument ' + 99 | '(too few arguments passed)')); 100 | } 101 | 102 | arg = args.shift(); 103 | argn++; 104 | 105 | if (flags.match(/[\' #]/)) { 106 | throw (jsError(ofmt, convposn, curconv, 107 | 'uses unsupported flags')); 108 | } 109 | 110 | if (precision.length > 0) { 111 | throw (jsError(ofmt, convposn, curconv, 112 | 'uses non-zero precision (not supported)')); 113 | } 114 | 115 | if (flags.match(/-/)) 116 | left = true; 117 | 118 | if (flags.match(/0/)) 119 | pad = '0'; 120 | 121 | if (flags.match(/\+/)) 122 | sign = true; 123 | 124 | switch (conversion) { 125 | case 's': 126 | if (arg === undefined || arg === null) { 127 | throw (jsError(ofmt, convposn, curconv, 128 | 'attempted to print undefined or null ' + 129 | 'as a string (argument ' + argn + ' to ' + 130 | 'sprintf)')); 131 | } 132 | ret += doPad(pad, width, left, arg.toString()); 133 | break; 134 | 135 | case 'd': 136 | arg = Math.floor(arg); 137 | /*jsl:fallthru*/ 138 | case 'f': 139 | sign = sign && arg > 0 ? '+' : ''; 140 | ret += sign + doPad(pad, width, left, 141 | arg.toString()); 142 | break; 143 | 144 | case 'x': 145 | ret += doPad(pad, width, left, arg.toString(16)); 146 | break; 147 | 148 | case 'j': /* non-standard */ 149 | if (width === 0) 150 | width = 10; 151 | ret += mod_util.inspect(arg, false, width); 152 | break; 153 | 154 | case 'r': /* non-standard */ 155 | ret += dumpException(arg); 156 | break; 157 | 158 | default: 159 | throw (jsError(ofmt, convposn, curconv, 160 | 'is not supported')); 161 | } 162 | } 163 | 164 | ret += fmt; 165 | return (ret); 166 | } 167 | 168 | function jsError(fmtstr, convposn, curconv, reason) { 169 | mod_assert.equal(typeof (fmtstr), 'string'); 170 | mod_assert.equal(typeof (curconv), 'string'); 171 | mod_assert.equal(typeof (convposn), 'number'); 172 | mod_assert.equal(typeof (reason), 'string'); 173 | return (new Error('format string "' + fmtstr + 174 | '": conversion specifier "' + curconv + '" at character ' + 175 | convposn + ' ' + reason)); 176 | } 177 | 178 | function jsPrintf() { 179 | var args = Array.prototype.slice.call(arguments); 180 | args.unshift(process.stdout); 181 | jsFprintf.apply(null, args); 182 | } 183 | 184 | function jsFprintf(stream) { 185 | var args = Array.prototype.slice.call(arguments, 1); 186 | return (stream.write(jsSprintf.apply(this, args))); 187 | } 188 | 189 | function doPad(chr, width, left, str) 190 | { 191 | var ret = str; 192 | 193 | while (ret.length < width) { 194 | if (left) 195 | ret += chr; 196 | else 197 | ret = chr + ret; 198 | } 199 | 200 | return (ret); 201 | } 202 | 203 | /* 204 | * This function dumps long stack traces for exceptions having a cause() method. 205 | * See node-verror for an example. 206 | */ 207 | function dumpException(ex) 208 | { 209 | var ret; 210 | 211 | if (!(ex instanceof Error)) 212 | throw (new Error(jsSprintf('invalid type for %%r: %j', ex))); 213 | 214 | /* Note that V8 prepends "ex.stack" with ex.toString(). */ 215 | ret = 'EXCEPTION: ' + ex.constructor.name + ': ' + ex.stack; 216 | 217 | if (ex.cause && typeof (ex.cause) === 'function') { 218 | var cex = ex.cause(); 219 | if (cex) { 220 | ret += '\nCaused by: ' + dumpException(cex); 221 | } 222 | } 223 | 224 | return (ret); 225 | } 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extsprintf", 3 | "version": "1.4.1", 4 | "description": "extended POSIX-style sprintf", 5 | "main": "./lib/extsprintf.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/davepacheco/node-extsprintf.git" 9 | }, 10 | "engines": [ 11 | "node >=0.6.0" 12 | ], 13 | "license": "MIT" 14 | } 15 | -------------------------------------------------------------------------------- /test/tst.basic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * tst.basic.js: tests various valid invocation 3 | */ 4 | 5 | var mod_assert = require('assert'); 6 | var mod_extsprintf = require('../lib/extsprintf'); 7 | var mod_path = require('path'); 8 | var sprintf = mod_extsprintf.sprintf; 9 | 10 | var testcases = [ { 11 | 'name': 'empty string', 12 | 'args': [ '' ], 13 | 'result': '' 14 | }, { 15 | 'name': '%s: basic', 16 | 'args': [ '%s', 'foo' ], 17 | 'result': 'foo' 18 | }, { 19 | 'name': '%s: not first', 20 | 'args': [ 'hello %s\n', 'world' ], 21 | 'result': 'hello world\n' 22 | }, { 23 | 'name': '%s: right-aligned', 24 | 'args': [ 'hello %10s\n', 'world' ], 25 | 'result': 'hello world\n' 26 | }, { 27 | 'name': '%s: left-aligned', 28 | 'args': [ 'hello %-10sagain\n', 'world' ], 29 | 'result': 'hello world again\n' 30 | }, { 31 | 'name': '%d: basic, positive', 32 | 'args': [ '%d', 17 ], 33 | 'result': '17' 34 | }, { 35 | 'name': '%d: basic, zero', 36 | 'args': [ '%d', 0 ], 37 | 'result': '0' 38 | }, { 39 | 'name': '%d: basic, floating point value', 40 | 'args': [ '%d', 17.3 ], 41 | 'result': '17' 42 | }, { 43 | 'name': '%d: basic, negative', 44 | 'args': [ '%d', -3 ], 45 | 'result': '-3' 46 | }, { 47 | 'name': '%d: right-aligned', 48 | 'args': [ '%4d', 17 ], 49 | 'result': ' 17' 50 | }, { 51 | 'name': '%d: right-aligned, zero-padded', 52 | 'args': [ '%04d', 17 ], 53 | 'result': '0017' 54 | }, { 55 | 'name': '%d: left-aligned', 56 | 'args': [ '%-4d', 17 ], 57 | 'result': '17 ' 58 | }, { 59 | 'name': '%x: basic', 60 | 'args': [ '%x', 18], 61 | 'result': '12' 62 | }, { 63 | 'name': '%x: zero-padded, right-aligned', 64 | 'args': [ '%08x', 0xfeedface ], 65 | 'result': 'feedface' 66 | }, { 67 | 'name': '%d: with plus sign', 68 | 'args': [ '%+d', 17 ], 69 | 'result': '+17' 70 | }, { 71 | 'name': '%f: basic', 72 | 'args': [ '%f', 3.2 ], 73 | 'result': '3.2' 74 | }, { 75 | 'name': '%f: right-aligned', 76 | 'args': [ '%5f', 3.2 ], 77 | 'result': ' 3.2' 78 | }, { 79 | 'name': '%%: basic', 80 | 'args': [ '%%' ], 81 | 'result': '%' 82 | }, { 83 | 'name': 'complex', 84 | 'args': [ 'one %s %8s %-3d bytes past 0x%04x, which was %6f%%%s%5s', 85 | 'program', 'wrote', -2, 0x30, 3.7, ' plus', 'over' ], 86 | 'result': 'one program wrote -2 bytes past 0x0030, which was ' + 87 | '3.7% plus over' 88 | } ]; 89 | 90 | function main(verbose) { 91 | /* 92 | * Create one test case with a very large input string. 93 | */ 94 | var input = '1234'; 95 | while (input.length < 100 * 1024) { 96 | input += input; 97 | } 98 | testcases.push({ 99 | 'name': 'long string argument (' + input.length + ' characters)', 100 | 'args': [ '%s', input ], 101 | 'result': input 102 | }); 103 | 104 | testcases.forEach(function (tc) { 105 | var result; 106 | console.error('test case: %s', tc.name); 107 | result = sprintf.apply(null, tc.args); 108 | if (verbose) { 109 | console.error(' args: %s', JSON.stringify(tc.args)); 110 | console.error(' result: %s', result); 111 | } 112 | mod_assert.equal(tc.result, result); 113 | }); 114 | 115 | console.log('%s tests passed', mod_path.basename(__filename)); 116 | } 117 | 118 | main(process.argv.length > 2 && process.argv[2] == '-v'); 119 | -------------------------------------------------------------------------------- /test/tst.invalid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * tst.invalid.js: tests invalid invocations 3 | */ 4 | 5 | var mod_assert = require('assert'); 6 | var mod_extsprintf = require('../lib/extsprintf'); 7 | var mod_path = require('path'); 8 | var sprintf = mod_extsprintf.sprintf; 9 | 10 | var testcases = [ { 11 | 'name': 'missing all arguments', 12 | 'args': [], 13 | 'errmsg': /first argument must be a format string$/ 14 | }, { 15 | 'name': 'missing argument for format specifier (first char and specifier)', 16 | 'args': [ '%s' ], 17 | 'errmsg': new RegExp( 18 | 'format string "%s": conversion specifier "%s" at character 1 ' + 19 | 'has no matching argument \\(too few arguments passed\\)') 20 | }, { 21 | 'name': 'missing argument for format specifier (later in string)', 22 | 'args': [ 'hello %s world %13d', 'big' ], 23 | 'errmsg': new RegExp( 24 | 'format string "hello %s world %13d": conversion specifier "%13d" at ' + 25 | 'character 16 has no matching argument \\(too few arguments passed\\)') 26 | }, { 27 | 'name': 'printing null as string', 28 | 'args': [ '%d cookies %3s', 15, null ], 29 | 'errmsg': new RegExp( 30 | 'format string "%d cookies %3s": conversion specifier "%3s" at ' + 31 | 'character 12 attempted to print undefined or null as a string ' + 32 | '\\(argument 3 to sprintf\\)') 33 | }, { 34 | 'name': 'printing undefined as string', 35 | 'args': [ '%d cookies %3s ah %d', 15, undefined, 7 ], 36 | 'errmsg': new RegExp( 37 | 'format string "%d cookies %3s ah %d": conversion specifier "%3s" at ' + 38 | 'character 12 attempted to print undefined or null as a string ' + 39 | '\\(argument 3 to sprintf\\)') 40 | }, { 41 | 'name': 'unsupported format character', 42 | 'args': [ 'do not use %X', 13 ], 43 | 'errmsg': new RegExp( 44 | 'format string "do not use %X": conversion ' + 45 | 'specifier "%X" at character 12 is not supported$') 46 | }, { 47 | 'name': 'unsupported flags', 48 | 'args': [ '%#x', 13 ], 49 | 'errmsg': new RegExp( 50 | 'format string "%#x": conversion ' + 51 | 'specifier "%#x" at character 1 uses unsupported flags$') 52 | } ]; 53 | 54 | function main(verbose) { 55 | testcases.forEach(function (tc) { 56 | var error; 57 | console.error('test case: %s', tc.name); 58 | if (verbose) { 59 | console.error(' args: %s', JSON.stringify(tc.args)); 60 | } 61 | mod_assert.throws(function () { 62 | try { 63 | sprintf.apply(null, tc.args); 64 | } catch (ex) { 65 | error = ex; 66 | throw (ex); 67 | } 68 | }, tc.errmsg); 69 | 70 | if (verbose && error) { 71 | console.error(' error: %s', error.message); 72 | } 73 | }); 74 | 75 | console.log('%s tests passed', mod_path.basename(__filename)); 76 | } 77 | 78 | main(process.argv.length > 2 && process.argv[2] == '-v'); 79 | --------------------------------------------------------------------------------