pax_global_header 0000666 0000000 0000000 00000000064 15171717125 0014521 g ustar 00root root 0000000 0000000 52 comment=33229fa6a8c4df7a4882fc5e556698f2f3ee84ef
pixl-tools-2.0.3/ 0000775 0000000 0000000 00000000000 15171717125 0013635 5 ustar 00root root 0000000 0000000 pixl-tools-2.0.3/.npmignore 0000664 0000000 0000000 00000000031 15171717125 0015626 0 ustar 00root root 0000000 0000000 .gitignore
node_modules/
pixl-tools-2.0.3/LICENSE.md 0000664 0000000 0000000 00000002121 15171717125 0015235 0 ustar 00root root 0000000 0000000 # License
**The MIT License (MIT)**
*Copyright (c) 2015 - 2025 Joseph Huckaby*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
pixl-tools-2.0.3/README.md 0000664 0000000 0000000 00000156300 15171717125 0015121 0 ustar 00root root 0000000 0000000 Table of Contents
- [Overview](#overview)
- [Usage](#usage)
- [Function List](#function-list)
* [timeNow](#timenow)
* [generateUniqueID](#generateuniqueid)
* [generateUniqueBase64](#generateuniquebase64)
* [generateShortID](#generateshortid)
* [digestHex](#digesthex)
* [digestBase64](#digestbase64)
* [numKeys](#numkeys)
* [firstKey](#firstkey)
* [hashKeysToArray](#hashkeystoarray)
* [hashValuesToArray](#hashvaluestoarray)
* [isaHash](#isahash)
* [isaArray](#isaarray)
* [copyHash](#copyhash)
* [copyHashRemoveKeys](#copyhashremovekeys)
* [copyHashRemoveProto](#copyhashremoveproto)
* [mergeHashes](#mergehashes)
* [mergeHashInto](#mergehashinto)
* [parseQueryString](#parsequerystring)
* [composeQueryString](#composequerystring)
* [findObjectsIdx](#findobjectsidx)
* [findObjectIdx](#findobjectidx)
* [findObject](#findobject)
* [findObjects](#findobjects)
* [findObjectDeep](#findobjectdeep)
* [findObjectsDeep](#findobjectsdeep)
* [deleteObject](#deleteobject)
* [deleteObjects](#deleteobjects)
* [alwaysArray](#alwaysarray)
* [sub](#sub)
* [setPath](#setpath)
* [getPath](#getpath)
* [deletePath](#deletepath)
* [getDateArgs](#getdateargs)
* [getTimeFromArgs](#gettimefromargs)
* [normalizeTime](#normalizetime)
* [formatDate](#formatdate)
* [getTextFromBytes](#gettextfrombytes)
* [getBytesFromText](#getbytesfromtext)
* [commify](#commify)
* [shortFloat](#shortfloat)
* [pct](#pct)
* [zeroPad](#zeropad)
* [clamp](#clamp)
* [lerp](#lerp)
* [getTextFromSeconds](#gettextfromseconds)
* [getSecondsFromText](#getsecondsfromtext)
* [getNiceRemainingTime](#getniceremainingtime)
* [randArray](#randarray)
* [pluralize](#pluralize)
* [escapeRegExp](#escaperegexp)
* [ucfirst](#ucfirst)
* [getErrorDescription](#geterrordescription)
* [bufferSplit](#buffersplit)
* [fileEachLine](#fileeachline)
* [getpwnam](#getpwnam)
* [getgrnam](#getgrnam)
* [tween](#tween)
* [findFiles](#findfiles)
* [findFilesSync](#findfilessync)
* [walkDir](#walkdir)
* [walkDirSync](#walkdirsync)
* [glob](#glob)
* [globSync](#globsync)
* [rimraf](#rimraf)
* [rimrafSync](#rimrafsync)
* [mkdirp](#mkdirp)
* [mkdirpSync](#mkdirpsync)
* [writeFileAtomic](#writefileatomic)
* [writeFileAtomicSync](#writefileatomicsync)
* [parseJSON](#parsejson)
* [findBin](#findbin)
* [findBinSync](#findbinsync)
* [sortBy](#sortby)
* [includesAny](#includesany)
* [includesAll](#includesall)
* [stripANSI](#stripansi)
* [noop](#noop)
- [Misc](#misc)
* [async](#async)
* [isLinux](#islinux)
* [isMac](#ismac)
* [isWindows](#iswindows)
* [NEVER_MATCH](#never_match)
* [MATCH_ANSI](#match_ansi)
* [MATCH_BAD_KEY](#match_bad_key)
- [License](#license)
# Overview
This module contains a set of miscellaneous utility functions that don't fit into any particular category.
# Usage
Use [npm](https://www.npmjs.com/) to install the module:
```sh
npm install pixl-tools
```
Then use `require()` to load it in your code:
```js
const Tools = require('pixl-tools');
```
Then call the function of your choice:
```js
let id = Tools.generateUniqueID();
```
# Function List
Here are all the functions included in the tools library, with full descriptions and examples:
## timeNow
```
NUMBER timeNow( FLOOR )
```
This function returns the current time expressed as [Epoch Seconds](http://en.wikipedia.org/wiki/Unix_time). Pass `true` if you want the value floored to the nearest integer.
```js
let epoch = Tools.timeNow();
let floored = Tools.timeNow(true);
```
## generateUniqueID
```
STRING generateUniqueID( LENGTH, SALT )
```
This function generates a cryptographically secure alphanumeric (lower-case hexadecimal) ID. It is *extremely* fast, as it uses a local 32K cache and only calls [crypto.randomBytes](https://nodejs.org/api/crypto.html#cryptorandombytessize-callback) to refill it. The default length is 64 characters, but you can pass in any lesser length to chop it.
```js
let id = Tools.generateUniqueID();
// Example: "1ee5de6aae098087d74e79b70a6796400d5b5fb9c8d53581d17cdd560892a14a"
let id = Tools.generateUniqueID( 32 );
// Example: "507d935eff6fbc502ad1156c728641b6"
let id = Tools.generateUniqueID( 16 );
// Example: "3b71219d2bfa2b0c"
```
## generateUniqueBase64
```
STRING generateUniqueBase64( BYTES, SALT )
```
This function generates a cryptographically secure URL-safe Base64 ID string. It is *extremely* fast, as it uses a local 32K cache and only calls [crypto.randomBytes](https://nodejs.org/api/crypto.html#cryptorandombytessize-callback) to refill it. The default length is 32 bytes (which results in a ~43 character Base64 string), but you can pass in any lesser byte length to chop it (e.g. 16 or 8).
```js
let id = Tools.generateUniqueBase64();
// Example: "q7CLMg_FBD9gYDlqPADYtg7VX1VVxOGKn_HgZBE-H54"
let id = Tools.generateUniqueBase64( 16 );
// Example: "jNEHRduwVcqcijGVAKVZQg"
let id = Tools.generateUniqueBase64( 8 );
// Example: "YrIjBy5x_sU"
```
## generateShortID
```
STRING generateShortID( PREFIX )
```
This function generates a short, semi-unique pseudo-random sortable alphanumeric ID using high-resolution server time, a static counter, and crypto random bytes. It is *extremely* fast, as it uses a local 32K cache and only calls [crypto.randomBytes](https://nodejs.org/api/crypto.html#cryptorandombytessize-callback) to refill it. The IDs are [Base-36](https://en.wikipedia.org/wiki/Base36) encoded (lower-case alphanumeric), and 16 characters in length (including an optional string prefix if provided). Example:
```js
let id = Tools.generateShortID('z');
// Example: "zmfopulv7m5o4dz6"
```
The IDs are constructed from the following parts:
- A user-provided prefix string (optional, custom length)
- The Unix clock time in milliseconds, converted to Base-36 (+8 characters)
- A static global counter, also in Base-36, which counts up to 1,296 (`zz`) then resets (+2 characters)
- Crypto random bytes in Base-36 (enough to fill out the ID to 16 characters total)
Use these with caution, as collisions *can* happen when many IDs are generated in sequence at an **extremely** high rate. However, in practice this will be very, very rare indeed. In order to produce a collision, the following has to occur: More than 1,296 IDs have to be generated within the same server clock millisecond (which flips the static counter back to zero), **and** the crypto random portion of the ID (usually around 5 - 6 characters) have to collide with the ID with the matching static counter value from the previous loop.
**Note:** Unix clock time in Base-36 will rollover from 8 to 9 characters in the year 2059 A.D. When this happens, the short IDs will still be 16 characters total (it compensates), but the time portion will take up 1 extra character, which will cause a "resort" if you are relying on these being alphabetically sortable. However, this library will likely be obsolete by then (and I'll probably be dead), so I don't care. No, I don't want to add a padding character.
## digestHex
```
STRING digestHex( PLAINTEXT, [ALGO], [LEN] )
```
This function is a simple wrapper around Node's [SHA-256](http://en.wikipedia.org/wiki/SHA-2) or other hashing algorithms. The default is SHA-256, in which case it returns a 64-character hexadecimal hash of the given string. You can pass a lesser length as the 3rd argument to chop it.
```js
let sig = Tools.digestHex( "my plaintext string" );
// --> "6b4fdfd705d05b11a56b8c3020058b666359d3939b6eda354f529ebad77695c2"
```
To specify the algorithm, include it as the second argument. It should be a string set to `md5`, `sha256`, etc. On recent releases of OpenSSL, typing `openssl list-message-digest-algorithms` will display the available digest algorithms. Example (MD5):
```js
let sig = Tools.digestHex( "my plaintext string", "md5" );
// --> "659a30fb5d9958326b15c17e8444c123"
```
## digestBase64
```
STRING digestBase64( PLAINTEXT, [ALGO], [BYTES] )
```
This function is a simple wrapper around Node's [SHA-256](http://en.wikipedia.org/wiki/SHA-2) or other hashing algorithms. The default is SHA-256, in which case it returns a URL-safe Base64 digest of the given string. The default digest buffer size is 32 bytes (which results in a ~43 character Base64 string), but you can pass a lesser byte length as the 3rd argument to reduce the output length.
```js
let sig = Tools.digestBase64( "my plaintext string" );
// --> "a0_f1wXQWxGla4wwIAWLZmNZ05Obbto1T1Keutd2lcI"
```
To specify the algorithm, include it as the second argument. It should be a string set to `md5`, `sha256`, etc. On recent releases of OpenSSL, typing `openssl list-message-digest-algorithms` will display the available digest algorithms. Example (MD5):
```js
let sig = Tools.digestBase64( "my plaintext string", "md5" );
// --> "ZZow-12ZWDJrFcF-hETBIw"
```
Here is an example of reducing the digest to only 8 bytes (64 bits), which results in a much shorter Base64 string:
```js
let sig = Tools.digestBase64( "my plaintext string", "sha256", 8 );
// --> "a0_f1wXQWxE"
```
## numKeys
```
INTEGER numKeys( OBJECT )
```
This function returns the number of keys in the specified hash.
```js
let my_hash = { foo: "bar", baz: 12345 };
let num = Tools.numKeys( my_hash ); // 2
```
## firstKey
```
STRING firstKey( OBJECT )
```
This function returns the first key of the hash when iterating over it. Note that hash keys are stored in an undefined order.
```js
let my_hash = { foo: "bar", baz: 12345 };
let key = Tools.firstKey( my_hash ); // foo or baz
```
## hashKeysToArray
```
ARRAY hashKeysToArray( OBJECT )
```
This function returns all the hash keys as an array. The values are discarded. Useful for sorting and then iterating over the sorted list.
```js
let my_hash = { foo: "bar", baz: 12345 };
let keys = Tools.hashKeysToArray( my_hash ).sort();
for (let idx = 0, len = keys.length; idx < len; idx++) {
let key = keys[idx];
// do something with key and my_hash[key]
}
```
## hashValuesToArray
```
ARRAY hashValuesToArray( OBJECT )
```
This function returns all the hash values as an array. The keys are discarded.
```js
let my_hash = { foo: "bar", baz: 12345 };
let values = Tools.hashValuesToArray( my_hash );
for (let idx = 0, len = values.length; idx < len; idx++) {
let value = values[idx];
// do something with value
}
```
## isaHash
```
BOOLEAN isaHash( MIXED )
```
This function returns `true` if the provided argument is a hash (object), `false` otherwise.
```js
let my_hash = { foo: "bar", baz: 12345 };
let is_hash = Tools.isaHash( my_hash );
```
## isaArray
```
BOOLEAN isaArray( MIXED )
```
This function returns `true` if the provided argument is an array (or is array-like), `false` otherwise.
```js
let my_arr = [ "foo", "bar", 12345 ];
let is_arr = Tools.isaArray( my_arr );
```
## copyHash
```
OBJECT copyHash( OBJECT, DEEP )
```
This function performs a shallow copy of the specified hash, and returns the copy. Pass `true` as the 2nd argument to perform a *deep copy*, which uses JSON parse/stringify.
```js
let my_hash = { foo: "bar", baz: 12345 };
let my_copy = Tools.copyHash( my_hash );
```
## copyHashRemoveKeys
```
OBJECT copyHashRemoveKeys( OBJECT, REMOVE )
```
This function performs a shallow copy of the specified hash, and returns the copy, but *omits* any keys you specify in a separate hash.
```js
let my_hash = { foo: "bar", baz: 12345 };
let omit_these = { baz: true };
let my_copy = Tools.copyHashRemoveKeys( my_hash, omit_these );
```
## copyHashRemoveProto
```
OBJECT copyHashRemoveProto( OBJECT )
```
This function performs a shallow copy of the specified hash, and returns the copy, but ensures that the copy is a "pure" object with no prototype, constructor, or any of the special properties that all standard Objects implicitly have.
```js
let my_hash = { foo: "bar", baz: 12345 };
let clean_copy = Tools.copyHashRemoveProto( my_hash );
```
## mergeHashes
```
OBJECT mergeHashes( OBJECT_A, OBJECT_B )
```
This function merges two hashes (objects) together, and returns a new hash which contains the combination of the two keys (shallow copy). The 2nd hash takes precedence over the first, in the event of duplicate keys.
```js
let hash1 = { foo: "bar" };
let hash2 = { baz: 12345 };
let combo = Tools.mergeHashes( hash1, hash2 );
```
## mergeHashInto
```
VOID mergeHashInto( OBJECT_A, OBJECT_B )
```
This function shallow-merges `OBJECT_B` into `OBJECT_A`. There is no return value. Existing keys are replaced in `OBJECT_A`.
```js
let hash1 = { foo: "bar" };
let hash2 = { baz: 12345 };
Tools.mergeHashInto( hash1, hash2 );
```
## parseQueryString
```
OBJECT parseQueryString( URL )
```
This function parses a standard URL query string, and returns a hash with key/value pairs for every query parameter. Duplicate params are clobbered, the latter prevails. Values are URL-unescaped, and all of them are strings. The function accepts a full URL, or just the query string portion.
```js
let url = 'http://something.com/hello.html?foo=bar&baz=12345';
let query = Tools.parseQueryString( url );
let foo = query.foo; // "bar"
let baz = query.baz; // "12345"
```
Please note that this is a very simple function, and you should probably use the built-in Node.js [querystring](http://nodejs.org/api/querystring.html) module instead.
## composeQueryString
```
STRING composeQueryString( OBJECT )
```
This function takes a hash of key/value pairs, and constructs a URL query string out of it. Values are URL-escaped.
```js
let my_hash = { foo: "bar", baz: 12345 };
let qs = Tools.composeQueryString( my_hash );
// --> "?foo=bar&baz=12345"
```
Please note that this is a very simple function, and you should probably use the built-in Node.js [querystring](http://nodejs.org/api/querystring.html) module instead.
## findObjectsIdx
```
ARRAY findObjectsIdx( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and returns all the array indexes whose objects have keys which match a given criteria hash.
```js
let list = [
{ id: 12345, name: "Joe", eyes: "blue" },
{ id: 12346, name: "Frank", eyes: "brown" },
{ id: 12347, name: "Cynthia", eyes: "blue" }
];
let criteria = { eyes: "blue" };
let idxs = Tools.findObjectsIdx( list, criteria );
// --> [0, 2]
```
## findObjectIdx
```
INTEGER findObjectIdx( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and returns the first array index whose object has keys which match a given criteria hash. If no objects match, `-1` is returned.
```js
let list = [
{ id: 12345, name: "Joe", eyes: "blue" },
{ id: 12346, name: "Frank", eyes: "brown" },
{ id: 12347, name: "Cynthia", eyes: "blue" }
];
let criteria = { eyes: "blue" };
let idx = Tools.findObjectIdx( list, criteria );
// --> 0
```
## findObject
```
OBJECT findObject( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and returns the first item whose object has keys which match a given criteria hash. If no objects match, `null` is returned.
```js
let list = [
{ id: 12345, name: "Joe", eyes: "blue" },
{ id: 12346, name: "Frank", eyes: "brown" },
{ id: 12347, name: "Cynthia", eyes: "blue" }
];
let criteria = { eyes: "blue" };
let obj = Tools.findObject( list, criteria );
// --> { id: 12345, name: "Joe", eyes: "blue" }
```
## findObjects
```
ARRAY findObjects( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and returns all the items whose objects have keys which match a given criteria hash.
```js
let list = [
{ id: 12345, name: "Joe", eyes: "blue" },
{ id: 12346, name: "Frank", eyes: "brown" },
{ id: 12347, name: "Cynthia", eyes: "blue" }
];
let criteria = { eyes: "blue" };
let objs = Tools.findObjects( list, criteria );
// --> [{ id: 12345, name: "Joe", eyes: "blue" }, { id: 12347, name: "Cynthia", eyes: "blue" }]
```
## findObjectDeep
```
OBJECT findObjectDeep( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and returns the first item whose object has deep properties which match a given criteria hash with `dot.path.properties`. If no objects match, `null` is returned. The format of the criteria properties should be compatible with [getPath()](#getpath).
```js
let list = [
{ id: 12345, params: { name: "Joe", eyes: "blue" } },
{ id: 12346, params: { name: "Frank", eyes: "brown" } },
{ id: 12347, params: { name: "Cynthia", eyes: "blue" } }
];
let criteria = { 'params.eyes': "blue" };
let obj = Tools.findObjecDeep( list, criteria );
// --> { id: 12345, params: { name: "Joe", eyes: "blue" } }
```
## findObjectsDeep
```
ARRAY findObjectsDeep( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and returns all the items whose objects have deep properties which match a given criteria hash with `dot.path.properties`. If none match, an empty array is returned. The format of the criteria properties should be compatible with [getPath()](#getpath).
```js
let list = [
{ id: 12345, params: { name: "Joe", eyes: "blue" } },
{ id: 12346, params: { name: "Frank", eyes: "brown" } },
{ id: 12347, params: { name: "Cynthia", eyes: "blue" } }
];
let criteria = { 'params.eyes': "blue" };
let objs = Tools.findObjectsDeep( list, criteria );
// --> [{ id: 12345, params: { name: "Joe", eyes: "blue" } }, { id: 12347, params: { name: "Cynthia", eyes: "blue" } }]
```
## deleteObject
```
BOOLEAN deleteObject( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and deletes the first item whose object has keys which match a given criteria hash. It returns `true` for success or `false` if no matching object could be found.
```js
let list = [
{ id: 12345, name: "Joe", eyes: "blue" },
{ id: 12346, name: "Frank", eyes: "brown" },
{ id: 12347, name: "Cynthia", eyes: "blue" }
];
let criteria = { eyes: "blue" };
Tools.deleteObject( list, criteria );
// list will now contain only Frank and Cynthia
```
## deleteObjects
```
INTEGER deleteObjects( ARRAY, CRITERIA )
```
This function iterates over an array of hashes, and deletes all items whose objects have keys which match a given criteria hash. It returns the number of objects deleted.
```js
let list = [
{ id: 12345, name: "Joe", eyes: "blue" },
{ id: 12346, name: "Frank", eyes: "brown" },
{ id: 12347, name: "Cynthia", eyes: "blue" }
];
let criteria = { eyes: "blue" };
Tools.deleteObjects( list, criteria );
// list will now contain only Frank
```
## alwaysArray
```
ARRAY alwaysArray( MIXED )
```
This function will wrap anything passed to it into an array and return the array, unless the item passed is already an array, in which case it is simply returned verbatim.
```js
let arr = Tools.alwaysArray( maybe_array );
```
## sub
```
STRING sub( TEMPLATE, ARGS, [FATAL, [FALLBACK, [FILTER]]] )
```
This function performs placeholder substitution on a string, using square bracket delimited placeholders which may contain simple keys or even paths. The paths can use either slash notation or dot notation. Example:
```js
let tree = {
folder1: {
file1: "foo",
folder2: {
file2: "bar"
}
}
};
let template = "Hello, I would like [/folder1/folder2/file2] and also [/folder1/file1] please!";
let str = Tools.sub( template, tree );
// --> "Hello, I would like bar and also foo please!"
```
You can omit the leading slashes if you are doing single-level hash lookups.
The three arguments at the end are all optional:
- If you pass true for the `FATAL` argument, the function will return `null` if any variable lookups fail. The default behavior is to preserve the original formatting (with placeholders and all) if the lookup fails.
- If you pass a string for the `FALLBACK` argument, it will be used as a fallback substitution value if the path lookup fails (only applies if `FATAL` is false).
- If you pass a function for the `FILTER` argument, all string values will be passed through that function before they are inserted into the template. For example, pass the global `encodeURIComponent` function to URL-encode all substituted values.
## setPath
```
BOOLEAN setPath( OBJECT, PATH, VALUE )
```
This function will set a property value inside a hash/array tree, by first traversing a directory-style path. Will auto-create new objects if needed. You can use either `dir/slash/syntax` or `dot.path.syntax`. Returns `true` on success or `false` on failure.
```js
let tree = {
folder1: {
file1: "foo"
}
};
Tools.setPath( tree, "folder1/folder2/file2", "bar" );
```
For walking through arrays, simply provide the index number of the element you want.
## getPath
```
MIXED getPath( OBJECT, PATH )
```
This function will perform a directory-style path lookup on a hash/array tree, returning whatever object or value is pointed to, or `undefined` if not found. You can use either `dir/slash/syntax` or `dot.path.syntax`.
```js
let tree = {
folder1: {
file1: "foo",
folder2: {
file2: "bar"
}
}
};
let file = Tools.getPath( tree, "folder1/folder2/file2" );
// --> "bar"
let file = Tools.getPath( tree, "folder1.folder2.file2" );
// --> "bar"
```
For walking into arrays, simply provide the index number of the element you want.
## deletePath
```
BOOLEAN deletePath( OBJECT, PATH )
```
This function will delete a property value inside a hash/array tree, by first traversing a directory-style path. You can use either `dir/slash/syntax` or `dot.path.syntax`. Returns `true` on success or `false` on failure.
```js
let tree = {
folder1: {
file1: "foo",
file2: "bar"
}
};
Tools.deletePath( tree, "folder1/file1" );
```
For walking through arrays, simply provide the index number of the element you want. However, note that the final element in your path should not be an array index, as that cannot be deleted using this API.
## getDateArgs
```
OBJECT getDateArgs( MIXED )
```
This function parses any date string, Epoch timestamp or Date object, and produces a hash with the following keys (all localized to the current timezone):
| Key | Sample Value | Description |
| --- | ------------ | ----------- |
| `year` | 2015 | Full year as integer. |
| `yy` | "15" | 2-digit year as string, with padded zeros if needed. |
| `yyyy` | "2015" | 4-digit year as string. |
| `mon` | 3 | Month of year as integer (1 - 12). |
| `mm` | "03" | 2-digit month as string with padded zeros if needed. |
| `mmm` | "Mar" | Month name abbreviated to first three letters. |
| `mmmm` | "March" | Full month name. |
| `mday` | 6 | Day of month as integer (1 - 31). |
| `dd` | "06" | 2-digit day as string with padded zeros if needed. |
| `wday` | 4 | Day of week as integer (0 - 6), starting with Sunday. |
| `ddd` | "Thu" | Weekday name abbreviated to first three letters. |
| `dddd` | "Thursday" | Full weekday name. |
| `hour` | 9 | Hour of day as integer (0 - 23). |
| `hour12` | 9 | Hour expressed in 12-hour time (i.e. 3 PM = 3). |
| `hh` | "09" | 2-digit hour as string with padded zeros if needed. |
| `min` | 2 | Minute of hour as integer (0 - 59). |
| `mi` | "02" | 2-digit minute as string with padded zeros if needed. |
| `sec` | 10 | Second of minute as integer (0 - 59). |
| `ss` | "10" | 2-digit second as string with padded zeros if needed. |
| `msec` | 999 | Millisecond of second as integer (0 - 999). |
| `ampm` | "am" | String representing ante meridiem (`am`) or post meridiem (`pm`). |
| `AMPM` | "AM" | Upper-case version of `ampm`. |
| `yyyy_mm_dd` | "2015/03/06" | Formatted string representing date in `YYYY/MM/DD` format. |
| `hh_mi_ss` | "09:02:10" | Formatted string representing local time in `HH:MI:SS` format. |
| `epoch` | 1425661330 | Epoch seconds used to generate all the date properties. |
| `offset` | -8 | Local offset from GMT/UTC in hours. |
| `tz` | "GMT-8" | Formatted GMT hour offset string. |
Example usage:
```js
let args = Tools.getDateArgs( new Date() );
let date_str = args.yyyy + '/' + args.mm + '/' + args.dd;
```
## getTimeFromArgs
```
INTEGER getTimeFromArgs( OBJECT )
```
This function will recalculate a date given an `args` object as returned from [getDateArgs()](#getdateargs). It allows you to manipulate the `year`, `mon`, `mday`, `hour`, `min` and/or `sec` properties, and will return the computed Epoch seconds from the new set of values. Example:
```js
let args = Tools.getDateArgs( new Date() );
args.mday = 15;
let epoch = Tools.getTimeFromArgs(args);
```
This example would return the Epoch seconds from the 15th day of the current month, in the current year, and using the current time of day.
## normalizeTime
```
INTEGER normalizeTime( INTEGER, OBJECT )
```
This function will "normalize" (i.e. quantize) an Epoch value to the nearest minute, hour, day, month, or year. Meaning, you can pass in an Epoch time value, and have it return a value of the start of the current hour, midnight on the current day, the 1st of the month, etc. To do this, pass in an object containing any keys you wish to change, e.g. `year`, `mon`, `mday`, `hour`, `min` and/or `sec`. Example:
```js
let midnight = Tools.normalizeTime( Tools.timeNow(), { hour: 0, min: 0, sec: 0 } );
```
You can actually set the values to non-zero. For example, to return the Epoch time of exactly noon today:
```js
let noon = Tools.normalizeTime( Tools.timeNow(), { hour: 12, min: 0, sec: 0 } );
```
## formatDate
```
STRING formatDate( MIXED, STRING )
```
This function parses any date string, Epoch timestamp or Date object, and produces a formatted date/time string according to a custom template, and in the local timezone. The template is populated using [sub()](#sub) (i.e. square bracket syntax) and can use any of the date/time properties returned by [getDateArgs()](#getdateargs). Examples:
```js
let now = new Date();
let str = Tools.formatDate( now, "[yyyy]/[mm]/[dd]" );
// 2019/03/22
let str = Tools.formatDate( now, "[dddd], [mmmm] [mday], [yyyy]" );
// Friday, March 22, 2019
let str = Tools.formatDate( now, "[hour12]:[mi] [ampm]" );
// 10:43 am
```
## getTextFromBytes
```
STRING getTextFromBytes( BYTES, PRECISION = 10, UNIT = 1024 )
```
This function generates a human-friendly text string given a number of bytes. It reduces the units to K, MB, GB or TB as needed, and allows a configurable amount of precision after the decimal point. The default is one decimal of precision (specify as `1`, `10`, `100`, etc.).
```js
let str = Tools.getTextFromBytes( 0 ); // "0 bytes"
let str = Tools.getTextFromBytes( 1023 ); // "1023 bytes"
let str = Tools.getTextFromBytes( 1024 ); // "1 K"
let str = Tools.getTextFromBytes( 1126 ); // "1.1 K"
let str = Tools.getTextFromBytes( 1599078, 1 ); // "1 MB"
let str = Tools.getTextFromBytes( 1599078, 10 ); // "1.5 MB"
let str = Tools.getTextFromBytes( 1599078, 100 ); // "1.52 MB"
let str = Tools.getTextFromBytes( 1599078, 1000 ); // "1.525 MB"
```
Note that by default, the "unit" is set to 1,024, meaning it considers 1,024 bytes to be 1K, 1,048,576 bytes to be 1MB, and so on. These are known as "binary units". However, if you would like the output to be in "decimal units" instead, set the unit to `1000`. Example:
```js
let str = Tools.getTextFromBytes( 500000, 10, 1000 ); // "500 K"
```
## getBytesFromText
```
INTEGER getBytesFromText( STRING, UNIT = 1024 )
```
This function parses a string containing a human-friendly size count (e.g. `45 bytes` or `1.5 MB`) and converts it to raw bytes.
```js
let bytes = Tools.getBytesFromText( "0 bytes" ); // 0
let bytes = Tools.getBytesFromText( "1023 bytes" ); // 1023
let bytes = Tools.getBytesFromText( "1 K" ); // 1024
let bytes = Tools.getBytesFromText( "1.1k" ); // 1126
let bytes = Tools.getBytesFromText( "1.525 MB" ); // 1599078
```
Note that by default, the "unit" is set to 1,024, meaning it treats 1KB as exactly 1,024 bytes, 1MB as exactly 1,024KB, and so on. These are known as "binary units". However, if your text is in "decimal units" instead, set the unit to `1000`. Example:
```js
let bytes = Tools.getBytesFromText( "1MB", 1000 ); // 1000000
```
## commify
```
STRING commify( INTEGER )
```
This function adds international symbols to long numbers following the current server locale formatting rules (for e.g. in the US this will add comma every 3 digits counting from right side).
```js
let c = Tools.commify( 123 ); // "123"
let c = Tools.commify( 1234 ); // "1,234"
let c = Tools.commify( 1234567890 ); // "1,234,567,890"
```
## shortFloat
```
NUMBER shortFloat( NUMBER, [PLACES] )
```
This function "shortens" a floating point number by only allowing up to `N` digits after the decimal point (defaults to `2`). You can customize this by passing an optional 2nd argument. Examples:
```js
let num1 = Tools.shortFloat( 0.12345 ); // 0.12
let num2 = Tools.shortFloat( 0.00001 ); // 0.0
let num3 = Tools.shortFloat( 0.00123, 3 ); // 0.001
```
## pct
```
STRING pct( AMOUNT, MAX, FLOOR )
```
This function calculates a percentage given an arbitrary numerical amount and a maximum value, and returns a formatted string with a '%' symbol. Pass `true` as the 3rd argument to floor the percentage to the nearest integer. Otherwise the value is shortened with `shortFloat()`.
```js
let p = Tools.pct( 5, 10 ); // "50%"
let p = Tools.pct( 0, 1 ); // "0%"
let p = Tools.pct( 751, 1000 ); // "75.1%"
let p = Tools.pct( 751, 1000, true ); // "75%"
```
## zeroPad
```
STRING zeroPad( NUMBER, MAX )
```
This function adds zeros to the left side of a number, until the total string length meets a specified maximum (up to 10 characters). The return value is a string, not a number.
```js
let padded = Tools.zeroPad( 5, 1 ); // "5"
let padded = Tools.zeroPad( 5, 2 ); // "05"
let padded = Tools.zeroPad( 5, 3 ); // "005"
let padded = Tools.zeroPad( 100, 3 ); // "100"
let padded = Tools.zeroPad( 100, 4 ); // "0100"
let padded = Tools.zeroPad( 100, 5 ); // "00100"
```
## clamp
```
NUMBER clamp( NUMBER, MIN, MAX )
```
This function performs a simple mathematical "clamp" operation, restricting a value between a defined range. This is just a convenience method, which can save you a few keystrokes. Example:
```js
let clamped = Tools.clamp( 50, 0, 10 );
// --> 10
```
## lerp
```
NUMBER lerp( START, END, AMOUNT )
```
This function performs linear interpolation between two values and a specified amount between `0.0` and `1.0`. This is just a convenience method, which can save you a few keystrokes. Example:
```js
let lerped = Tools.lerp( 0, 50, 0.25 );
// --> 12.5
```
## getTextFromSeconds
```
STRING getTextFromSeconds( NUMBER, ABBREVIATE, SHORTEN )
```
This function generates a human-friendly time string given a number of seconds. It reduces the units to minutes, hours or days as needed. You can also abbreviate the output, and shorten the extra precision.
```js
let str = Tools.getTextFromSeconds( 0 ); // "0 seconds"
let str = Tools.getTextFromSeconds( 86400 ); // "1 day"
let str = Tools.getTextFromSeconds( 90 ); // "1 minute, 30 seconds"
let str = Tools.getTextFromSeconds( 90, true ); // "1 min, 30 sec"
let str = Tools.getTextFromSeconds( 90, false, true ); // "1 minute"
let str = Tools.getTextFromSeconds( 90, true, true ); // "1 min"
```
## getSecondsFromText
```
INTEGER getSecondsFromText( STRING )
```
This function parses a string containing a human-friendly time (e.g. `45 minutes` or `7 days`) and converts it to raw seconds. It accepts seconds, minutes, hours, days and/or weeks. It does not interpret "months" or "years" because those are non-exact measurements.
```js
let sec = Tools.getSecondsFromText( "1 second" ); // 1
let sec = Tools.getSecondsFromText( "2min" ); // 120
let sec = Tools.getSecondsFromText( "30m" ); // 1800
let sec = Tools.getSecondsFromText( "12 HOURS" ); // 43200
let sec = Tools.getSecondsFromText( "1day" ); // 86400
```
## getNiceRemainingTime
```
STRING getNiceRemainingTime( ELAPSED, COUNTER, MAX, ABBREV, SHORTEN )
```
This function calculates the estimated remaining time on a job in progress, given the elapsed time in seconds, an arbitrary counter representing the job's progress, and a maximum value for the counter.
```js
let remain = Tools.getNiceRemainingTime( 45, 0.75, 1.0 );
// --> "15 seconds"
let remain = Tools.getNiceRemainingTime( 3640, 0.75, 1.0 );
// --> "20 minutes, 13 seconds"
let remain = Tools.getNiceRemainingTime( 3640, 0.75, 1.0, true );
// --> "20 min, 13 sec"
let remain = Tools.getNiceRemainingTime( 3640, 0.75, 1.0, false, true );
// --> "20 minutes"
let remain = Tools.getNiceRemainingTime( 3640, 0.75, 1.0, true, true );
// --> "20 min"
```
Note that this works best when the job's progress is somewhat constant. If it proceeds at a varying pace, the remaining time may appear to go too fast or too slow at times. It always computes the average speed over the course of the time elapsed, versus the current progress.
## randArray
```
MIXED randArray( ARRAY )
```
This function picks a random element from the given array, and returns it.
```js
let fruit = ['apple', 'orange', 'banana'];
let rand = Tools.randArray( fruit );
```
## pluralize
```
STRING pluralize( STRING, NUMBER )
```
This function pluralizes a string using US-English rules, given an arbitrary number. This is useful when constructing human-friendly sentences containing a quantity of things, and you wish to say either "thing" or "things" depending on the number.
```js
let list = ['apple', 'orange', 'banana'];
let text = "You have " + list.length + Tools.pluralize(" item", list.length) + " in your list.";
// --> "You have 3 items in your list.";
```
## escapeRegExp
```
STRING escapeRegExp( STRING )
```
This function escapes a string so that it can be used inside a regular expression. Meaning, any regular expression metacharacters are prefixed with a backslash, so they are interpreted literally. It was taken from the [MDN Regular Expression Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).
## ucfirst
```
STRING ucfirst( STRING )
```
The function upper-cases the first character of a string, and lower-cases the rest. This is very similar to the Perl core function of the same name. Example:
```js
let first_name = Tools.ucfirst( 'george' );
// --> "George"
```
## getErrorDescription
```
STRING getErrorDescription( ERROR )
```
This function takes a standard Node.js [System Error](https://nodejs.org/api/errors.html#class-systemerror) object, such as one emitted when a filesystem or network error occurs, and produces a prettier and more verbose string description. It uses the 3rd party [errno](https://www.npmjs.com/package/errno) package, and adds its own decorations as well. Example:
```js
require('fs').readFile( '/bad/file.txt', function(err, data) {
if (err) {
console.log( "Native Error: " + err.message );
console.log( "Better Error: " + Tools.getErrorDescription(err) );
}
} );
// Outputs:
// Native Error: ENOENT, open '/bad/file.txt'
// Better Error: No such file or directory (ENOENT, open '/bad/file.txt')
```
Basically it resolves the Node.js error codes such as `ENOENT` to a human-readable string (i.e. `No such file or directory`), but also appends the raw native error message in parenthesis as well.
## bufferSplit
```
ARRAY bufferSplit( BUFFER, SEPARATOR )
```
This function splits a buffer into an array of chunks, given a separator (string or buffer). It works similarly to the [String.split](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) core function, with two main differences. First, the separator cannot be a regular expression (it must be a string or another buffer), and second, the returned split buffer chunks will occupy the same memory space as the original buffer. Example:
```js
const EOL = require('os').EOL;
let data = require('fs').readFileSync( 'some_file.csv' );
let lines = Tools.bufferSplit( data, EOL );
```
## fileEachLine
```
VOID fileEachLine( FILE, OPTS, ITERATOR, CALLBACK )
```
This function iterates over a file line by line, firing `ITERATOR` for each. This is done in asynchronous fashion, akin to the [async](http://caolan.github.io/async/) module. Your `ITERATOR` function is passed the line (encoded string or buffer) and a callback to fire. When all the lines are completed, the main `CALLBACK` is fired once, including an error or not. This is designed to handle huge files without using much memory at all.
The `OPTS` object may include:
| Property Name | Default Value | Description |
|---------------|---------------|-------------|
| `buffer_size` | `1024` | How many bytes to read from the file at a time. |
| `eol` | os.EOL | The end-of-line separator, defaults to the current system EOL. |
| `encoding` | `utf8` | The encoding to use for each line, set to `null` if you want buffers. |
Example:
```js
Tools.fileEachLine( "my_large_spreadsheet.csv",
function(line, callback) {
// this is fired for each line
let columns = line.split(/\,\s*/);
// do something with the data here, possibly async
// fire callback for next line, pass error to abort
callback();
},
function(err) {
// all lines are complete
if (err) throw err;
}
);
```
## getpwnam
```
OBJECT getpwnam( USERNAME, [USE_CACHE] )
```
This function fetches local user account information, give a username or numerical UID. This is similar to the POSIX [getpwnam](http://man7.org/linux/man-pages/man3/getpwnam.3.html) function, which is missing from Node.js core. This function works on Linux and OS X only. It runs in synchronous mode, and returns an object with the following properties, or `null` on error:
| Property Name | Sample Value | Description |
|---------------|---------------|-------------|
| `username` | `jhuckaby` | The username of the account. |
| `password` | `****` | The hashed password of the account (often masked). |
| `uid` | `501` | The numerical UID (User ID) of the account. |
| `gid` | `501` | The numeric GID (Group ID) of the account. |
| `name` | `Joseph Huckaby` | The full name of the user. |
| `dir` | `/home/jhuckaby` | The home directory path of the user. |
| `shell` | `/bin/bash` | The login shell used by the user. |
If you pass `true` as the 2nd argument, the user information will be cached in RAM for future queries on the same username or UID. Example use:
```js
let info = Tools.getpwnam( "jhuckaby", true );
if (info) {
process.chdir( info.dir );
}
```
## getgrnam
```
OBJECT getgrnam( GROUP, [USE_CACHE] )
```
This function fetches local group account information, give a name or numerical GID. This is similar to the POSIX [getgrnam](http://man7.org/linux/man-pages/man3/getgrnam.3.html) function, which is missing from Node.js core. This function works on Linux and OS X only. It runs in synchronous mode, and returns an object with the following properties, or `null` on error:
| Property Name | Sample Value | Description |
|---------------|---------------|-------------|
| `name` | `games` | The name of the group. |
| `gid` | `20` | The numeric GID (Group ID) of the group. |
If you pass `true` as the 2nd argument, the group information will be cached in RAM for future queries on the same name or GID. Example use:
```js
let info = Tools.getgrnam( "games", true );
if (info) {
console.log( "GID: ", info.gid );
}
```
## tween
```
NUMBER tween( START, END, AMOUNT, MODE, ALGORITHM )
```
This function calculates a [tween](https://en.wikipedia.org/wiki/Inbetweening) between two numbers, and returns the in-between value. For example, this can be used to control animation with "easing" (i.e. ease-in, ease-out), and also custom mathematical curves like quadratic, quintic, etc. Example use:
```js
let x = Tools.tween( 0, 150, 0.5, 'EaseOut', 'Quadratic' );
```
The output will be somewhere between `0` and `150`, controlled by the `EaseOut` mode and `Quadratic` algorithm. If you had selected the `Linear` algorithm, this would be exactly `75` (halfway between the start and end).
Here is a more detailed list of the function arguments:
| Argument | Description |
|----------|-------------|
| `START` | The starting value for the property (any number). |
| `END` | The ending value for the property (any number). |
| `AMOUNT` | This value should be between `0.0` and `1.0`, and sets the position along the animation path. |
| `MODE` | The animation mode as string, one of `EaseIn`, `EaseOut` or `EaseInOut`. |
| `ALGORITHM` | The algorithm name as string, one of `Linear`, `Quadratic`, `Cubic`, `Quartetic`, `Quintic`, `Sine` or `Circular`. |
## findFiles
```
VOID findFiles( DIR, [OPTS], CALLBACK )
```
The `findFiles()` function will recursively scan for files on the filesystem, and can include several filters for customization. You need to specify a starting directory path, an object containing options (see below), and a callback to receive the list of files. Your callback will be called with two arguments: an error if any, and an array of files. The options object can include:
| Property | Type | Description |
|----------|------|-------------|
| `filespec` | RegExp / String | An optional regular expression or string to match against filenames (not paths). Defaults to `/.+/`. |
| `recurse` | Boolean | Recurse into nested subdirectories, defaults to `true`. Set this to `false` to only scan the outermost directory. |
| `all` | Boolean | Normally, dotfiles are skipped. When this is set to `true`, dotfiles will be included (unless filtered out by `filespec`). |
| `filter` | Function | Optional custom filter function, called for each file. See example below for usage. |
| `dirs` | Boolean | Optionally return directories as well as files, if they match the filespec. |
| `stats` | Boolean | Optionally return an object for each file, containing `path`, `size` and `mtime` properties. |
Here is a simple example that finds all image files:
```js
Tools.findFiles( "/path/to/starting/dir", {
filespec: /\.(jpg|png|gif)$/i
},
function(err, files) {
console.log("All the images: ", files);
});
```
Here is an example of using a custom filter function:
```js
Tools.findFiles( "/path/to/starting/dir", {
filter: function(file, stats) {
return stats.size <= 32768; // only include files 32K or less
}
},
function(err, files) {
console.log("All files 32K or less: ", files);
});
```
If you just want all the files, you can omit the options object:
```js
Tools.findFiles( "/path/to/starting/dir", function(err, files) {
console.log("All the files: ", files);
});
```
Please note that this function specifically returns *files*, not directories (unless you set the `dirs` option to `true`). Also, for more low-level control over this process, see [walkDir()](#walkdir) below, which this function calls internally.
## findFilesSync
```
ARRAY findFilesSync( DIR, [OPTS] )
```
The `findFilesSync()` function will recursively scan for files on the filesystem, and can include several filters for customization. This is the synchronous version of the function, so there is no callback. You need to specify a starting directory path, and an object containing options (see below). The return value will be an array of files. The options object can include:
| Property | Type | Description |
|----------|------|-------------|
| `filespec` | RegExp / String | An optional regular expression or string to match against filenames (not paths). Defaults to `/.+/`. |
| `recurse` | Boolean | Recurse into nested subdirectories, defaults to `true`. Set this to `false` to only scan the outermost directory. |
| `all` | Boolean | Normally, dotfiles are skipped. When this is set to `true`, dotfiles will be included (unless filtered out by `filespec`). |
| `filter` | Function | Optional custom filter function, called for each file. See example below for usage. |
| `dirs` | Boolean | Optionally return directories as well as files, if they match the filespec. |
| `stats` | Boolean | Optionally return an object for each file, containing `path`, `size` and `mtime` properties. |
Here is a simple example that finds all image files:
```js
let files = Tools.findFilesSync( "/path/to/starting/dir", {
filespec: /\.(jpg|png|gif)$/i
});
console.log("All the images: ", files);
```
Here is an example of using a custom filter function:
```js
let files = Tools.findFilesSync( "/path/to/starting/dir", {
filter: function(file, stats) {
return stats.size <= 32768; // only include files 32K or less
}
});
console.log("All files 32K or less: ", files);
```
If you just want all the files, you can omit the options object:
```js
let files = Tools.findFiles( "/path/to/starting/dir");
console.log("All the files: ", files);
```
Please note that this function specifically returns *files*, not directories (unless you set the `dirs` option to `true`). Also, for more low-level control over this process, see [walkDirSync()](#walkdirsync) below, which this function calls internally.
## walkDir
```
VOID walkDir( DIR, ITERATOR, CALLBACK )
```
The `walkDir()` function recursively walks a directory on the filesystem, including all subdirectories, and fires a custom iterator function for each file or directory encountered. Your iterator function is passed the file path, an [fs.Stats](https://nodejs.org/api/fs.html#class-fsstats) object, and a callback. It needs to fire the callback function, and pass `true` to recurse for directories, or `false` to skip it. When the full directory tree is walked, the final callback is fired. Example:
```js
Tools.walkDir( "/path/to/starting/dir",
function(file, stats, callback) {
// called for each file and directory
if (stats.isDirectory()) callback(true); // recurse into
else {
console.log("Found file: " + file);
callback();
}
},
function() {
// all done!
console.log("Walk complete!");
}
);
```
## walkDirSync
```
VOID walkDirSync( DIR, ITERATOR )
```
The `walkDirSync()` function recursively walks a directory on the filesystem, including all subdirectories, and fires a custom iterator function for each file or directory encountered. This is the synchronous version of the function, so there is no callback. Your iterator function is passed the file path, and an [fs.Stats](https://nodejs.org/api/fs.html#class-fsstats) object. It can return `true` to recurse for directories, or `false` to skip. Example:
```js
Tools.walkDirSync( "/path/to/starting/dir",
function(file, stats) {
// called for each file and directory
if (stats.isDirectory()) return true; // recurse into
else {
console.log("Found file: " + file);
}
}
);
console.log("Walk complete!");
```
## glob
```
VOID glob( FILESPEC, CALLBACK )
```
The `glob()` function searches for files using a [glob pattern](https://en.wikipedia.org/wiki/Glob_%28programming%29). Example:
```js
Tools.glob( "/path/to/files/*.jpg", function(err, files) {
if (err) throw err;
console.log("Found files: ", files);
});
```
## globSync
```
VOID globSync( FILESPEC )
```
The `globSync()` function searches for files using a [glob pattern](https://en.wikipedia.org/wiki/Glob_%28programming%29). This is the synchronous version of the function, so there is no callback. Example:
```js
let files = Tools.globSync( "/path/to/files/*.jpg");
console.log("Found files: ", files);
```
This function is also available via `glob.sync`, for convenience.
## rimraf
```
VOID rimraf( FILESPEC, CALLBACK )
```
The `rimraf()` function recursively deletes files and folders using a [glob pattern](https://en.wikipedia.org/wiki/Glob_%28programming%29). The name comes from the standard Linux `rm -rf` shell command. It will not fail if no files were found. Example use:
```js
Tools.rimraf( "/path/to/files/*.jpg", function(err) {
if (err) throw err;
});
```
## rimrafSync
```
VOID rimrafSync( FILESPEC )
```
The `rimrafSync()` function recursively deletes files and folders using a [glob pattern](https://en.wikipedia.org/wiki/Glob_%28programming%29). The name comes from the standard Linux `rm -rf` shell command. This is the synchronous version of the function, so there is no callback. It will not fail if no files were found. Example use:
```js
Tools.rimrafSync( "/path/to/files/*.jpg");
```
This function is also available via `rimraf.sync`, for convenience.
## mkdirp
```
VOID mkdirp( PATH, CALLBACK )
```
The `mkdirp()` function creates a directory, and all parent directories as needed. The name comes from the standard Linux `mkdir -p` shell command. It will not fail if the directory already exists. Example use:
```js
Tools.mkdirp( "/path/to/my/dir", function(err) {
if (err) throw err;
});
```
## mkdirpSync
```
VOID mkdirpSync( PATH )
```
The `mkdirpSync()` function creates a directory, and all parent directories as needed. The name comes from the standard Linux `mkdir -p` shell command. It will not fail if the directory already exists. This is the synchronous version of the function, so there is no callback. Example use:
```js
Tools.mkdirpSync( "/path/to/my/dir");
```
This function is also available via `mkdirp.sync`, for convenience.
## writeFileAtomic
```
VOID writeFileAtomic( FILE, DATA, OPTS, CALLBACK )
```
This function writes a file *atomically*. That is, it writes to a temp file first, and then renames that file atop the original. This ensures that no corruption can occur with multiple threads or processes writing to the same file at the same time. In this case the latter prevails. The temp file is created in the same directory to ensure the same filesystem (cross-FS renames are **not** atomic), and is named with a `.tmp.[UNIQUE]` file extension. It accepts the same arguments as [fs.writeFile()](https://nodejs.org/api/fs.html#fswritefilefile-data-options-callback). Example:
```js
Tools.writeFileAtomic( "/path/to/my/file.json", data, function(err) {
if (err) throw err;
});
```
## writeFileAtomicSync
```
VOID writeFileAtomicSync( FILE, DATA, OPTS )
```
This function writes a file *atomically* and synchronously. That is, it writes to a temp file first, and then renames that file atop the original. This ensures that no corruption can occur with multiple threads or processes writing to the same file at the same time. In this case the latter prevails. The temp file is created in the same directory to ensure the same filesystem (cross-FS renames are **not** atomic), and is named with a `.tmp.[UNIQUE]` file extension. It accepts the same arguments as [fs.writeFileSync()](https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options). Example:
```js
try {
Tools.writeFileAtomicSync( "/path/to/my/file.json", data );
}
catch (err) {
throw err;
}
```
## parseJSON
```
OBJECT parseJSON( TEXT )
```
This function is a wrapper around the built-in `JSON.parse()`. It works in exactly the same way, except that it throws improved error messages in the event of parser errors. Specifically, it specifies the exact line number and column of the error in the source JSON. This is mainly useful for multi-line (i.e. pretty-printed) JSON files. Here is an example:
```js
let bad_json = `{
"good_property_name": 12345,
bad_missing_quotes: 67890
}`;
let obj = Tools.parseJSON(bad_json);
// Error: Unexpected token b in JSON on line 3 column 2
```
## findBin
```
VOID findBin( FILENAME, CALLBACK )
```
This function locates the path to a binary executable given a filename and a callback. It searches all directories in the current environment `PATH`, as well as a number of known common locations (`/usr/local/bin`, `/usr/bin`, `/bin`, `/usr/sbin`, and `/sbin`). Your callback is invoked with an error (or `false` on success), and the path to the first binary executable found. Example use:
```js
Tools.findBin( 'lsof', function(err, file) {
if (err) throw err;
console.log("Found: " + file);
} );
```
## findBinSync
```
STRING findBinSync( FILENAME )
```
A synchronous version of [findBin](#findbin). This returns the binary path, or `false` if none was found. It will not throw.
## sortBy
```
ARRAY sortBy( ARRAY, KEY, OPTS )
```
This function sorts an array of objects by a specific named property inside each object. The options object may include the following:
| Property Name | Type | Description |
|---------------|------|-------------|
| `type` | String | Specify a numerical (`number`) or locale-aware string (`string`) sort. The default is `string`. |
| `dir` | Number | Specify an ascending (`1`) or descending (`-1`) sort direction. The default is ascending (`1`). |
| `copy` | Boolean | Set this to `true` to return a shallow copy of the sorted array (and don't touch the original). The default (`false`) is to sort the original array in place. |
Here is an example:
```js
let list = [
{ username: 'joe', date: 1654987195.435 },
{ username: 'fred', date: 1473634873 },
{ username: 'nancy', date: 1883476393.2 },
{ username: 'jane', date: 1289898989 },
];
let sorted = Tools.sortBy( list, "date", { type: "number", dir: 1, copy: true } );
```
## includesAny
```
BOOLEAN includesAny( HAYSTACK, NEEDLES )
```
Returns true if `haystack` contains any `needles`, false otherwise. Both arguments must be arrays. This is similar to [Array.includes]() except that it searches the first array (haystack) for **any** matches in the second array (needles). Example use:
```js
var haystack = ['red', 'green', 'yellow', 'blue', 'purple', 'black'];
var matched = Tools.includesAny(haystack, ['red', 'white', 'blue']); // true
```
## includesAll
```
BOOLEAN includesAll( HAYSTACK, NEEDLES )
```
Returns true if `haystack` contains all `needles` (in any order), false otherwise. Both arguments must be arrays. This is similar to [Array.includes]() except that it searches the first array (haystack) for **all** matches in the second array (needles). Example use:
```js
var haystack = ['red', 'green', 'yellow', 'blue', 'purple', 'black'];
var matched = Tools.includesAll(haystack, ['red', 'white', 'blue']); // false
var matched = Tools.includesAll(haystack, [ 'black', 'purple', 'blue', 'yellow', 'green', 'red' ]); // true
```
## stripANSI
```
STRING stripANSI( STRING )
```
Safely strips all [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) from a given string, returning a new string. We are borrowing the regular expression from [ansi-regex](https://www.npmjs.com/package/ansi-regex) for this work. Example use:
```js
let text = '\u001B[4mcake\u001B[0m';
let stripped = Tools.stripANSI(text);
// --> cake
```
The raw regexp itself is also available at `Tools.MATCH_ANSI` should you need it.
## noop
This is a no-op function which does nothing.
# Misc
Here are a few non-functions available in the `Tools` object.
## async
This is a reference to the extremely awesome [async](https://npmjs.com/package/async) package from NPM. I use this so frequently that I decided to include in tools. Access it like this:
```js
const async = Tools.async;
```
## isLinux
This boolean will be true if the current platform is Linux.
## isMac
This boolean will be true if the current platform is macOS (Darwin).
## isWindows
This boolean will be true if the current platform is Windows.
## NEVER_MATCH
This pre-compiled regular expression will *never match* no matter what.
## MATCH_ANSI
This pre-compiled regular expression will match any ANSI escape codes. Used by [stripANSI](#stripansi).
## MATCH_BAD_KEY
This pre-compiled regular expression will match any of the "hidden" keys that exist in all objects, e.g. `__proto__`.
# License
**The MIT License**
*Copyright (c) 2015 - 2025 Joseph Huckaby.*
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
pixl-tools-2.0.3/package.json 0000664 0000000 0000000 00000001164 15171717125 0016125 0 ustar 00root root 0000000 0000000 {
"name": "pixl-tools",
"version": "2.0.3",
"description": "A set of miscellaneous utility functions for Node.js.",
"author": "Joseph Huckaby ",
"homepage": "https://github.com/jhuckaby/pixl-tools",
"license": "MIT",
"main": "tools.js",
"repository": {
"type": "git",
"url": "https://github.com/jhuckaby/pixl-tools"
},
"bugs": {
"url": "https://github.com/jhuckaby/pixl-tools/issues"
},
"keywords": [
"utilities",
"miscellaneous",
"misc"
],
"dependencies": {
"errno": "0.1.7",
"async": "2.6.4",
"picomatch": "4.0.4"
},
"devDependencies": {}
}
pixl-tools-2.0.3/tools.js 0000664 0000000 0000000 00000127111 15171717125 0015336 0 ustar 00root root 0000000 0000000 // Misc Tools for Node.js
// Copyright (c) 2015 - 2025 Joseph Huckaby
// Released under the MIT License
const fs = require('fs');
const Path = require('path');
const cp = require('child_process');
const crypto = require('crypto');
const ErrNo = require('errno');
const os = require('os');
const hostname = os.hostname();
const picomatch = require('picomatch');
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December' ];
const SHORT_MONTH_NAMES = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May',
'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' ];
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday'];
const SHORT_DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const EASE_ALGOS = {
Linear: function(_amount) { return _amount; },
Quadratic: function(_amount) { return Math.pow(_amount, 2); },
Cubic: function(_amount) { return Math.pow(_amount, 3); },
Quartetic: function(_amount) { return Math.pow(_amount, 4); },
Quintic: function(_amount) { return Math.pow(_amount, 5); },
Sine: function(_amount) { return 1 - Math.sin((1 - _amount) * Math.PI / 2); },
Circular: function(_amount) { return 1 - Math.sin(Math.acos(_amount)); }
};
const EASE_MODES = {
EaseIn: function(_amount, _algo) { return EASE_ALGOS[_algo](_amount); },
EaseOut: function(_amount, _algo) { return 1 - EASE_ALGOS[_algo](1 - _amount); },
EaseInOut: function(_amount, _algo) {
return (_amount <= 0.5) ? EASE_ALGOS[_algo](2 * _amount) / 2 : (2 - EASE_ALGOS[_algo](2 * (1 - _amount))) / 2;
}
};
const BIN_DIRS = [
'/bin',
'/sbin',
'/usr/bin',
'/usr/sbin',
'/usr/local/bin',
'/usr/local/sbin'
];
if (process.env.HOME) {
BIN_DIRS.push( Path.join(process.env.HOME, '.local', 'bin') );
}
const MATCH_ANSI = (function() {
// Borrowed from https://github.com/chalk/ansi-regex (MIT)
const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)';
const pattern = [
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
].join('|');
return new RegExp(pattern, 'g');
})();
const INTL_NUM_FMT = new Intl.NumberFormat();
module.exports = {
"async": require('async'),
picomatch: picomatch,
hostname: hostname,
user_cache: {},
group_cache: {},
_uniqueIDCounter: 0,
_shortIDCounter: Math.floor( Math.random() * Math.pow(36, 2) ),
NEVER_MATCH: /(?!)/,
MATCH_ANSI: MATCH_ANSI,
MATCH_BAD_KEY: /^(constructor|__defineGetter__|__defineSetter__|hasOwnProperty|__lookupGetter__|__lookupSetter__|isPrototypeOf|propertyIsEnumerable|toString|valueOf|__proto__|toLocaleString|0)$/,
isWindows: !!process.platform.match(/^win/),
isLinux: !!process.platform.match(/^linux/),
isMac: !!process.platform.match(/^darwin/),
randomPool: {
// provide INSANELY fast access to small amounts of randomBytes
// about 1000X faster than calling crypto.randomBytes each time
// does not allocate pool until first call
pool: Buffer.alloc(0),
size: 32768,
offset: 0,
getBytes: function(len) {
// suck len bytes from the pool, refill if necessary
if (len > this.size) this.len = this.size;
if (this.offset + len > this.pool.length) {
this.pool = crypto.randomBytes( this.size );
this.offset = 0;
}
const out = this.pool.subarray(this.offset, this.offset + len);
this.offset += len;
return out;
}
},
noop: function() {},
timeNow: function(floor) {
// return current epoch time
var epoch = Date.now() / 1000;
return floor ? Math.floor(epoch) : epoch;
},
getRandomEntropy(salt) {
// get random string using some readily-available bits of entropy
// included for legacy only -- not used for IDs anymore
return [
'SALT_7fb1b7485647b1782c715474fba28fd1',
this.timeNow(),
Math.random(),
hostname,
process.pid,
this._uniqueIDCounter++,
salt || ''
].join('-');
},
generateUniqueID: function(len = 64) {
// generate cryptographically secure unique hexadecimal string ID
const buf = this.randomPool.getBytes( Math.ceil(len / 2) );
return buf.toString('hex').slice(0, len);
},
generateUniqueBase64: function(bytes = 32) {
// generate cryptographically secure unique URL-Safe Base64 ID
const buf = this.randomPool.getBytes( bytes );
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
},
generateShortID: function(prefix = '', len = 16) {
// generate sortable short id using high-res server time, a static counter, and crypto random bytes
// FUTURE: beware the Base36 time rollover coming in year 2059, which will cause a "resort"
this._shortIDCounter++;
if (this._shortIDCounter >= Math.pow(36, 2)) this._shortIDCounter = 0;
let id = prefix + Date.now().toString(36) + this._shortIDCounter.toString(36);
if (id.length < len) {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
const needed = len - id.length;
const arr = this.randomPool.getBytes(needed);
for (let idx = 0; idx < needed; idx++) {
id += chars[ arr[idx] % chars.length ];
}
}
return id;
},
digestHex: function(str, algo, len) {
// digest string using SHA256 (default) or other algo, return hex hash
var output = crypto.createHash( algo || 'sha256' ).update( str ).digest('hex');
return len ? output.substring(0, len) : output;
},
digestBase64: function(str, algo, bytes) {
// digest string using SHA256 (default) or other algo, return url-safe base64 string
var buf = crypto.createHash( algo || 'sha256' ).update( str ).digest();
var output = bytes ? buf.slice(0, bytes).toString('base64') : buf.toString('base64');
return output.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // url-safe
},
numKeys: function(hash) {
// count keys in hash
// Object.keys(hash).length may be faster, but this uses far less memory
var count = 0;
for (var key in hash) { count++; }
return count;
},
firstKey: function(hash) {
// return first key in hash (key order is undefined)
for (var key in hash) return key;
return null; // no keys in hash
},
hashKeysToArray: function(hash) {
// convert hash keys to array (discard values)
var arr = [];
for (var key in hash) arr.push(key);
return arr;
},
hashValuesToArray: function(hash) {
// convert hash values to array (discard keys)
var arr = [];
for (var key in hash) arr.push( hash[key] );
return arr;
},
isaHash: function(arg) {
// determine if arg is a hash or hash-like
return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) == 'undefined') );
},
isaArray: function(arg) {
// determine if arg is an array or is array-like
return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) != 'undefined') );
},
copyHash: function(hash, deep) {
// copy hash to new one, with optional deep mode (uses JSON)
if (deep) {
// deep copy
return JSON.parse( JSON.stringify(hash) );
}
else {
// shallow copy
var output = {};
for (var key in hash) {
output[key] = hash[key];
}
return output;
}
},
copyHashRemoveKeys: function(hash, remove) {
// shallow copy hash, excluding some keys
var output = {};
for (var key in hash) {
if (!remove[key]) output[key] = hash[key];
}
return output;
},
copyHashRemoveProto: function(hash) {
// shallow copy hash, but remove __proto__ and family from copy
var output = Object.create(null);
for (var key in hash) {
output[key] = hash[key];
}
return output;
},
mergeHashes: function(a, b) {
// shallow-merge keys from a and b into c and return c
// b has precedence over a
if (!a) a = {};
if (!b) b = {};
var c = {};
for (var key in a) c[key] = a[key];
for (var key in b) c[key] = b[key];
return c;
},
mergeHashInto: function(a, b) {
// shallow-merge keys from b into a
for (var key in b) a[key] = b[key];
},
parseQueryString: function(url) {
// parse query string into key/value pairs and return as object
var query = {};
url.replace(/^.*\?/, '').replace(/([^\=]+)\=([^\&]*)\&?/g, function(match, key, value) {
query[key] = decodeURIComponent(value);
if (query[key].match(/^\-?\d+$/)) query[key] = parseInt(query[key]);
else if (query[key].match(/^\-?\d*\.\d+$/)) query[key] = parseFloat(query[key]);
return '';
} );
return query;
},
composeQueryString: function(query) {
// compose key/value pairs into query string
var qs = '';
for (var key in query) {
qs += (qs.length ? '&' : '?') + key + '=' + encodeURIComponent(query[key]);
}
return qs;
},
findObjectsIdx: function(arr, crit, max) {
// find idx of all objects that match crit keys/values
var idxs = [];
var num_crit = 0;
for (var a in crit) num_crit++;
for (var idx = 0, len = arr.length; idx < len; idx++) {
var matches = 0;
for (var key in crit) {
if (arr[idx][key] == crit[key]) matches++;
}
if (matches == num_crit) {
idxs.push(idx);
if (max && (idxs.length >= max)) return idxs;
}
} // foreach elem
return idxs;
},
findObjectIdx: function(arr, crit) {
// find idx of first matched object, or -1 if not found
var idxs = this.findObjectsIdx(arr, crit, 1);
return idxs.length ? idxs[0] : -1;
},
findObject: function(arr, crit) {
// return first found object matching crit keys/values, or null if not found
var idx = this.findObjectIdx(arr, crit);
return (idx > -1) ? arr[idx] : null;
},
findObjects: function(arr, crit) {
// find and return all objects that match crit keys/values
var idxs = this.findObjectsIdx(arr, crit);
var objs = [];
for (var idx = 0, len = idxs.length; idx < len; idx++) {
objs.push( arr[idxs[idx]] );
}
return objs;
},
findObjectsDeep: function(arr, crit, max) {
// find and return all objects that match crit paths/values
var results = [];
var num_crit = 0;
for (var a in crit) num_crit++;
for (var idx = 0, len = arr.length; idx < len; idx++) {
var matches = 0;
for (var key in crit) {
if (this.getPath(arr[idx], key) == crit[key]) matches++;
}
if (matches == num_crit) {
results.push(arr[idx]);
if (max && (results.length >= max)) return results;
}
} // foreach elem
return results;
},
findObjectDeep: function(arr, crit) {
// return first found object matching crit paths/values, or null if not found
var results = this.findObjectsDeep(arr, crit, 1);
return results.length ? results[0] : null;
},
deleteObject: function(arr, crit) {
// walk array looking for nested object matching criteria object
// delete first object found
var idx = this.findObjectIdx(arr, crit);
if (idx > -1) {
arr.splice( idx, 1 );
return true;
}
return false;
},
deleteObjects: function(arr, crit) {
// delete all objects in obj array matching criteria
// FUTURE: This is not terribly efficient -- could use a rewrite.
var count = 0;
while (this.deleteObject(arr, crit)) count++;
return count;
},
alwaysArray: function(obj) {
// if obj is not an array, wrap it in one and return it
return this.isaArray(obj) ? obj : [obj];
},
lookupPath: function(path, obj) {
// LEGACY METHOD, included for backwards compatibility only -- use getPath() instead
// walk through object tree, psuedo-XPath-style
// supports arrays as well as objects
// return final object or value
// always start query with a slash, i.e. /something/or/other
path = path.replace(/\/$/, ""); // strip trailing slash
while (/\/[^\/]+/.test(path) && (typeof(obj) == 'object')) {
// find first slash and strip everything up to and including it
var slash = path.indexOf('/');
path = path.substring( slash + 1 );
// find next slash (or end of string) and get branch name
slash = path.indexOf('/');
if (slash == -1) slash = path.length;
var name = path.substring(0, slash);
// advance obj using branch
if ((typeof(obj.length) == 'undefined') || name.match(/\D/)) {
// obj is probably a hash
if (typeof(obj[name]) != 'undefined') obj = obj[name];
else return null;
}
else {
// obj is array
var idx = parseInt(name, 10);
if (isNaN(idx)) return null;
if (typeof(obj[idx]) != 'undefined') obj = obj[idx];
else return null;
}
} // while path contains branch
return obj;
},
sub: function(text, args, fatal, fallback, filter) {
// perform simple [placeholder] substitution using supplied
// args object and return transformed text
var self = this;
var result = true;
var value = '';
if (typeof(text) == 'undefined') text = '';
text = '' + text;
if (!args) args = {};
if (fallback && filter) fallback = filter(fallback);
text = text.replace(/\[([^\]]+)\]/g, function(m_all, name) {
value = self.getPath(args, name);
if (value === undefined) {
result = false;
return fallback || m_all;
}
else if (filter) return filter(value);
else return value;
} );
if (!result && fatal) return null;
else return text;
},
substitute: function(text, args, fatal) {
// LEGACY METHOD, included for backwards compatibility only -- use sub() instead
// perform simple [placeholder] substitution using supplied
// args object and return transformed text
if (typeof(text) == 'undefined') text = '';
text = '' + text;
if (!args) args = {};
while (text.indexOf('[') > -1) {
var open_bracket = text.indexOf('[');
var close_bracket = text.indexOf(']');
if (close_bracket < open_bracket) {
// error, mismatched brackets, we must abort
return fatal ? null : text.replace(/__APLB__/g, '[').replace(/__APRB__/g, ']');
}
var before = text.substring(0, open_bracket);
var after = text.substring(close_bracket + 1, text.length);
var name = text.substring( open_bracket + 1, close_bracket );
var value = '';
// prevent infinite loop with nested open brackets
name = name.replace(/\[/g, '__APLB__');
if (name.indexOf('/') == 0) {
value = this.lookupPath(name, args);
if (value === null) {
if (fatal) return null;
else value = '__APLB__' + name + '__APRB__';
}
}
else if (typeof(args[name]) != 'undefined') value = args[name];
else {
if (fatal) return null;
else value = '__APLB__' + name + '__APRB__';
}
text = before + value + after;
} // while text contains [
return text.replace(/__APLB__/g, '[').replace(/__APRB__/g, ']');
},
setPath: function(target, path, value) {
// set path using dir/slash/syntax or dot.path.syntax
// support inline dots and slashes if backslash-escaped
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
} ) : path.split(/[\.\/]/);
var key = parts.pop();
// traverse path
while (parts.length) {
var part = parts.shift();
if (part) {
if (!(part in target)) {
// auto-create nodes
target[part] = {};
}
if (typeof(target[part]) != 'object') {
// path runs into non-object
return false;
}
target = target[part];
}
}
target[key] = value;
return true;
},
deletePath: function(target, path) {
// delete path using dir/slash/syntax or dot.path.syntax
// support inline dots and slashes if backslash-escaped
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
} ) : path.split(/[\.\/]/);
var key = parts.pop();
// traverse path
while (parts.length) {
var part = parts.shift();
if (part) {
if (!(part in target)) {
// path runs into non-existent object
return false;
}
if (typeof(target[part]) != 'object') {
// path runs into non-object
return false;
}
target = target[part];
}
}
delete target[key];
return true;
},
getPath: function(target, path) {
// get path using dir/slash/syntax or dot.path.syntax
// support inline dots and slashes if backslash-escaped
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
} ) : path.split(/[\.\/]/);
var key = parts.pop();
// traverse path
while (parts.length) {
var part = parts.shift();
if (part) {
if (typeof(target[part]) != 'object') {
// path runs into non-object
return undefined;
}
target = target[part];
}
}
return target[key];
},
formatDate: function(thingy, template) {
// format date using get_date_args
// e.g. '[yyyy]/[mm]/[dd]' or '[dddd], [mmmm] [mday], [yyyy]' or '[hour12]:[mi] [ampm]'
return this.sub( template, this.getDateArgs(thingy) );
},
getDateArgs: function(thingy) {
// return hash containing year, mon, mday, hour, min, sec
// given epoch seconds, date object or date string
if (!thingy) thingy = new Date();
var date = (typeof(thingy) == 'object') ? thingy : (new Date( (typeof(thingy) == 'number') ? (thingy * 1000) : thingy ));
var args = {
epoch: Math.floor( date.getTime() / 1000 ),
year: date.getFullYear(),
mon: date.getMonth() + 1,
mday: date.getDate(),
wday: date.getDay(),
hour: date.getHours(),
min: date.getMinutes(),
sec: date.getSeconds(),
msec: date.getMilliseconds(),
offset: 0 - (date.getTimezoneOffset() / 60)
};
args.yyyy = '' + args.year;
args.yy = args.year % 100;
if (args.yy < 10) args.yy = "0" + args.yy; else args.yy = '' + args.yy;
if (args.mon < 10) args.mm = "0" + args.mon; else args.mm = '' + args.mon;
if (args.mday < 10) args.dd = "0" + args.mday; else args.dd = '' + args.mday;
if (args.hour < 10) args.hh = "0" + args.hour; else args.hh = '' + args.hour;
if (args.min < 10) args.mi = "0" + args.min; else args.mi = '' + args.min;
if (args.sec < 10) args.ss = "0" + args.sec; else args.ss = '' + args.sec;
if (args.hour >= 12) {
args.ampm = 'pm';
args.hour12 = args.hour - 12;
if (!args.hour12) args.hour12 = 12;
}
else {
args.ampm = 'am';
args.hour12 = args.hour;
if (!args.hour12) args.hour12 = 12;
}
args.AMPM = args.ampm.toUpperCase();
args.yyyy_mm_dd = args.yyyy + '/' + args.mm + '/' + args.dd;
args.hh_mi_ss = args.hh + ':' + args.mi + ':' + args.ss;
args.tz = 'GMT' + (args.offset >= 0 ? '+' : '') + args.offset;
// add formatted month and weekdays
args.mmm = SHORT_MONTH_NAMES[ args.mon - 1 ];
args.mmmm = MONTH_NAMES[ args.mon - 1];
args.ddd = SHORT_DAY_NAMES[ args.wday ];
args.dddd = DAY_NAMES[ args.wday ];
return args;
},
getTimeFromArgs: function(args) {
// return epoch given args like those returned from getDateArgs()
var then = new Date(
args.year,
args.mon - 1,
args.mday,
args.hour,
args.min,
args.sec,
0
);
return Math.floor( then.getTime() / 1000 );
},
normalizeTime: function(epoch, zero_args) {
// quantize time into any given precision
// examples:
// hour: { min:0, sec:0 }
// day: { hour:0, min:0, sec:0 }
var args = this.getDateArgs(epoch);
for (var key in zero_args) args[key] = zero_args[key];
// mday is 1-based
if (!args['mday']) args['mday'] = 1;
return this.getTimeFromArgs(args);
},
getTextFromBytes: function(bytes, precision, unit) {
// convert raw bytes to english-readable format
// set precision to 1 for ints, 10 for 1 decimal point (default), 100 for 2, etc.
bytes = Math.floor(bytes);
if (!precision) precision = 10;
if (!unit) unit = 1024;
if (bytes >= unit) {
bytes = Math.floor( (bytes / unit) * precision ) / precision;
if (bytes >= unit) {
bytes = Math.floor( (bytes / unit) * precision ) / precision;
if (bytes >= unit) {
bytes = Math.floor( (bytes / unit) * precision ) / precision;
if (bytes >= unit) {
bytes = Math.floor( (bytes / unit) * precision ) / precision;
return bytes + ' TB';
}
else return bytes + ' GB';
}
else return bytes + ' MB';
}
else return bytes + ' K';
}
else return bytes + this.pluralize(' byte', bytes);
},
getBytesFromText: function(text, unit) {
// parse text into raw bytes, e.g. "1 K" --> 1024 (or custom)
if (text.toString().match(/^\d+$/)) return parseInt(text); // already in bytes
if (!unit) unit = 1024;
var multipliers = {
b: 1,
k: unit,
m: unit * unit,
g: unit * unit * unit,
t: unit * unit * unit * unit
};
var bytes = 0;
text = text.toString().replace(/([\d\.]+)\s*(\w)\w*\s*/g, function(m_all, m_g1, m_g2) {
var mult = multipliers[ m_g2.toLowerCase() ] || 0;
bytes += (parseFloat(m_g1) * mult);
return '';
} );
return Math.floor(bytes);
},
commify: function(number) {
// add international formatting to integer, like 1,234,567 in the US
return INTL_NUM_FMT.format(number || 0);
},
shortFloat: function(value, places) {
// Shorten floating-point decimal to N places max
if (!places) places = 2;
var mult = Math.pow(10, places);
return( Math.floor(parseFloat(value || 0) * mult) / mult );
},
pct: function(count, max, floor) {
// Return formatted percentage given a number along a sliding scale from 0 to 'max'
var pct = (count * 100) / (max || 1);
if (!pct.toString().match(/^\d+(\.\d+)?$/)) { pct = 0; }
return '' + (floor ? Math.floor(pct) : this.shortFloat(pct)) + '%';
},
zeroPad: function(value, len) {
// Pad a number with zeroes to achieve a desired total length (max 10)
return ('0000000000' + value).slice(0 - len);
},
clamp: function(val, min, max) {
// simple math clamp implementation
return Math.max(min, Math.min(max, val));
},
lerp: function(start, end, amount) {
// simple linear interpolation algo
return start + ((end - start) * this.clamp(amount, 0, 1));
},
getTextFromSeconds: function(sec, abbrev, no_secondary) {
// convert raw seconds to human-readable relative time
var neg = '';
sec = Math.floor(sec);
if (sec<0) { sec =- sec; neg = '-'; }
var p_text = abbrev ? "sec" : "second";
var p_amt = sec;
var s_text = "";
var s_amt = 0;
if (sec > 59) {
var min = Math.floor(sec / 60);
sec = sec % 60;
s_text = abbrev ? "sec" : "second";
s_amt = sec;
p_text = abbrev ? "min" : "minute";
p_amt = min;
if (min > 59) {
var hour = Math.floor(min / 60);
min = min % 60;
s_text = abbrev ? "min" : "minute";
s_amt = min;
p_text = abbrev ? "hr" : "hour";
p_amt = hour;
if (hour > 23) {
var day = Math.floor(hour / 24);
hour = hour % 24;
s_text = abbrev ? "hr" : "hour";
s_amt = hour;
p_text = "day";
p_amt = day;
} // hour>23
} // min>59
} // sec>59
var text = p_amt + " " + p_text;
if ((p_amt != 1) && !abbrev) text += "s";
if (s_amt && !no_secondary) {
text += ", " + s_amt + " " + s_text;
if ((s_amt != 1) && !abbrev) text += "s";
}
return(neg + text);
},
getSecondsFromText: function(text) {
// parse text into raw seconds, e.g. "1 minute" --> 60
if (text.toString().match(/^\d+$/)) return parseInt(text); // already in seconds
var multipliers = {
s: 1,
m: 60,
h: 60 * 60,
d: 60 * 60 * 24,
w: 60 * 60 * 24 * 7,
y: 60 * 60 * 24 * 365
};
var seconds = 0;
text = text.toString().replace(/([\d\.]+)\s*(\w)\w*\s*/g, function(m_all, m_g1, m_g2) {
var mult = multipliers[ m_g2.toLowerCase() ] || 0;
seconds += (parseFloat(m_g1) * mult);
return '';
} );
return Math.floor(seconds);
},
getNiceRemainingTime: function(elapsed, counter, counter_max, abbrev, shorten) {
// estimate remaining time given starting epoch, a counter and the
// counter maximum (i.e. percent and 100 would work)
// return in english-readable format
if (counter == counter_max) return 'Complete';
if (counter == 0) return 'n/a';
var sec_remain = Math.floor(((counter_max - counter) * elapsed) / counter);
return this.getTextFromSeconds( sec_remain, abbrev, shorten );
},
randArray: function(arr) {
// return random element from array
return arr[ Math.floor(Math.random() * arr.length) ];
},
pluralize: function(word, num) {
// apply english pluralization to word if 'num' is not equal to 1
if (num != 1) {
return word.replace(/y$/, 'ie') + 's';
}
else return word;
},
escapeRegExp: function(text) {
// escape text for regular expression
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
},
ucfirst: function(text) {
// capitalize first character only, lower-case rest
return text.substring(0, 1).toUpperCase() + text.substring(1, text.length).toLowerCase();
},
getErrorDescription: function(err) {
// attempt to get better error description using 'errno' module
var msg = err.message;
if (err.errno && ErrNo.code[err.errno]) {
msg = this.ucfirst(ErrNo.code[err.errno].description) + " (" + err.message + ")";
}
else if (err.code && ErrNo.code[err.code]) {
msg = this.ucfirst(ErrNo.code[err.code].description) + " (" + err.message + ")";
}
return msg;
},
bufferSplit: function(buf, chunk) {
// Split a buffer like string split (no reg exp support tho)
// WARNING: Splits use SAME MEMORY SPACE as original buffer
var idx = -1;
var lines = [];
while ((idx = buf.indexOf(chunk)) > -1) {
lines.push( buf.subarray(0, idx) );
buf = buf.subarray( idx + chunk.length, buf.length );
}
lines.push(buf);
return lines;
},
fileEachLine: function(file, opts, iterator, callback) {
// asynchronously process file line by line, using very little memory
var self = this;
if (!callback && (typeof(opts) == 'function')) {
// support 3-arg convention: file, iterator, callback
callback = iterator;
iterator = opts;
opts = {};
}
if (!opts) opts = {};
if (!opts.buffer_size) opts.buffer_size = 1024;
if (!opts.eol) opts.eol = os.EOL;
if (!('encoding' in opts)) opts.encoding = 'utf8';
var chunk = Buffer.alloc(opts.buffer_size);
var lastChunk = null;
var processNextLine = null;
var processChunk = null;
var readNextChunk = null;
var lines = [];
fs.open(file, "r", function(err, fh) {
if (err) {
if ((err.code == 'ENOENT') && (opts.ignore_not_found)) return callback();
else return callback(err);
}
processNextLine = function() {
// process single line from buffer
var line = lines.shift();
if (opts.encoding) line = line.toString( opts.encoding );
iterator(line, function(err) {
if (err) {
fs.close(fh, function() {});
return callback(err);
}
// if (lines.length) setImmediate( processNextLine ); // ensure async
if (lines.length) process.nextTick( processNextLine );
else readNextChunk();
});
};
processChunk = function(err, num_bytes, chunk) {
if (err) {
fs.close(fh, function() {});
return callback(err);
}
var eof = (num_bytes != opts.buffer_size);
var data = chunk.subarray(0, num_bytes);
if (lastChunk && lastChunk.length) {
data = Buffer.concat([lastChunk, data], lastChunk.length + data.length);
lastChunk = null;
}
if (data.length) {
lines = self.bufferSplit( data, opts.eol );
// see if data ends on EOL -- if not, we have a partial block
// fill buffer for next read (unless at EOF)
if (data.subarray(0 - opts.eol.length).toString() == opts.eol) {
lines.pop(); // remove empty line caused by split
}
else if (!eof) {
// more to read, save excess for next loop iteration
var line = lines.pop();
lastChunk = Buffer.from(line);
}
if (lines.length) processNextLine();
else readNextChunk();
}
else {
// close file and complete
fs.close(fh, callback);
}
};
readNextChunk = function() {
// read chunk from file
fs.read(fh, chunk, 0, opts.buffer_size, null, processChunk);
};
// begin reading
readNextChunk();
}); // fs.open
},
getpwnam: function(username, use_cache) {
// Simulate POSIX getpwnam by querying getent on linux, or /usr/bin/id on darwin / OSX.
// Accepts username or uid, and can optionally cache results for repeat queries for same user.
// Response keys: username, password, uid, gid, name, dir, shell
var user = null;
// sanitize username to prevent abuse
username = username.toString().replace(/[^\w\-\.]/g, '');
if (use_cache && this.user_cache[username]) {
return this.copyHash( this.user_cache[username] );
}
if (process.platform === 'linux') {
// use getent on linux
var cols = null;
var getent = this.findBinSync('getent');
if (!getent) return null; // no getent!
var opts = { timeout: 1000, encoding: 'utf8' };
try { cols = cp.execSync(getent + ' passwd ' + username, opts).trim().split(':'); }
catch (err) { return null; }
if ((username == cols[0]) || (username == Number(cols[2]))) {
user = {
username: cols[0],
password: cols[1],
uid: Number(cols[2]),
gid: Number(cols[3]),
name: cols[4] && cols[4].split(',')[0],
dir: cols[5],
shell: cols[6]
};
}
else {
// user not found
return null;
}
}
else if (process.platform === 'darwin') {
// use /usr/bin/id on darwin / OSX
var cols = null;
var opts = { timeout: 1000, encoding: 'utf8', stdio: 'pipe' };
try { cols = cp.execSync('/usr/bin/id -P ' + username, opts).trim().split(':'); }
catch (err) { return null; }
if ((username == cols[0]) || (username == Number(cols[2]))) {
user = {
username: cols[0],
password: cols[1],
uid: Number(cols[2]),
gid: Number(cols[3]),
name: cols[7],
dir: cols[8],
shell: cols[9]
};
}
else {
// something went wrong
return null;
}
}
else {
// unsupported platform
return null;
}
if (use_cache) {
this.user_cache[ user.username ] = user;
this.user_cache[ user.uid ] = user;
return this.copyHash( user );
}
else {
return user;
}
},
getgrnam: function(name, use_cache) {
// Simulate POSIX getgrnam by querying getent on linux, or /etc/group on darwin / OSX.
// Accepts group name or gid, and can optionally cache results for repeat queries for same group.
// Response keys: name, gid
var group = null;
// sanitize group name to prevent abuse
name = name.toString().replace(/[^\w\-\.]/g, '');
if (use_cache && this.group_cache[name]) {
return this.copyHash( this.group_cache[name] );
}
if (process.platform === 'linux') {
// use getent on linux
var lines = null;
var cols = null;
var getent = this.findBinSync('getent');
if (!getent) return null; // no getent!
var opts = { timeout: 1000, encoding: 'utf8' };
try { cols = cp.execSync(getent + ' group ' + name, opts).trim().split(':'); }
catch (err) { return null; }
if ((name == cols[0]) || (name == Number(cols[2]))) {
group = {
name: cols[0],
gid: Number(cols[2])
};
}
else {
// group not found
return null;
}
}
else if (process.platform === 'darwin') {
// use /etc/group on darwin / OSX
if (!fs.existsSync('/etc/group')) return null; // no /etc/group!
var lines = fs.readFileSync('/etc/group', 'utf8').trim().split(/\n/);
for (var idx = 0, len = lines.length; idx < len; idx++) {
var cols = lines[idx].split(':');
if ((name == cols[0]) || (name == Number(cols[2]))) {
group = {
name: cols[0],
gid: Number(cols[2])
};
idx = len;
}
}
return group;
}
else {
// unsupported platform
return null;
}
if (use_cache) {
this.group_cache[ group.name ] = group;
this.group_cache[ group.gid ] = group;
return this.copyHash( group );
}
else {
return group;
}
},
tween: function(start, end, amount, mode, algo) {
// Calculate the "tween" (value between two other values) using a variety of algorithms.
// Useful for computing positions for animation frames.
// Omit mode and algo for 'lerp' (simple linear interpolation).
if (!mode) mode = 'EaseOut';
if (!algo) algo = 'Linear';
amount = this.clamp( amount, 0.0, 1.0 );
return start + (EASE_MODES[mode]( amount, algo ) * (end - start));
},
findFiles: function(dir, opts, callback) {
// find all files matching filespec, optionally recurse into subdirs
// opts: { filespec, recurse, all (dotfiles), filter, dirs, stats }
var files = [];
if (!callback) { callback = opts; opts = {}; }
if (!opts) opts = {};
if (!opts.filespec) opts.filespec = /.+/;
else if (typeof(opts.filespec) == 'string') opts.filespec = new RegExp(opts.filespec);
if (!("recurse" in opts)) opts.recurse = true;
this.walkDir( dir,
function(file, stats, callback) {
var filename = Path.basename(file);
if (!opts.all && filename.match(/^\./)) return callback(false); // skip dotfiles
var info = { path: file, size: stats.size, mtime: stats.mtimeMs / 1000 };
if (stats.isDirectory()) {
info.dir = true;
if (opts.dirs && filename.match(opts.filespec)) files.push( opts.stats ? info : file );
return callback( opts.recurse );
}
else {
if (filename.match( opts.filespec )) {
if (opts.filter && (opts.filter(file, stats) === false)) return callback(false); // user skip
else files.push( opts.stats ? info : file );
}
}
callback();
},
function(err) {
callback(err, files);
}
); // walkDir
},
findFilesSync: function(dir, opts) {
// find all files matching filespec sync, optionally recurse into subdirs
// opts: { filespec, recurse, all (dotfiles), filter, dirs, stats }
var files = [];
if (!opts) opts = {};
if (!opts.filespec) opts.filespec = /.+/;
else if (typeof(opts.filespec) == 'string') opts.filespec = new RegExp(opts.filespec);
if (!("recurse" in opts)) opts.recurse = true;
this.walkDirSync(dir, function(file, stats) {
var filename = Path.basename(file);
if (!opts.all && filename.match(/^\./)) return false; // skip dotfiles
var info = { path: file, size: stats.size, mtime: stats.mtimeMs / 1000 };
if (stats.isDirectory()) {
info.dir = true;
if (opts.dirs && filename.match(opts.filespec)) files.push( opts.stats ? info : file );
return opts.recurse;
}
else {
if (filename.match( opts.filespec )) {
if (opts.filter && (opts.filter(file, stats) === false)) return false; // user skip
else files.push( opts.stats ? info : file );
}
}
}); // walkDirSync
return files;
},
walkDir: function(dir, iterator, callback) {
// walk directory tree, fire iterator for every file, then callback at end
// iterator is passed: (path, stats, callback)
// pass false to iterator callback to prevent descending into a dir
var self = this;
fs.readdir(dir, function(err, files) {
if (err) return callback(err);
if (!files || !files.length) return callback();
self.async.eachSeries( files,
function(filename, callback) {
var file = Path.join( dir, filename );
fs.stat( file, function(err, stats) {
if (err) return callback();
iterator( file, stats, function(cont) {
// recurse for dir
if (stats.isDirectory() && (cont !== false)) {
self.walkDir( file, iterator, callback );
}
else callback();
} );
} );
}, callback
); // eachSeries
} ); // fs.readdir
},
walkDirSync: function(dir, iterator) {
// walk directory tree sync, fire iterator for every file
// iterator is passed: (path, stats)
// return false from iterator to prevent descending into a dir
var self = this;
var files = fs.readdirSync(dir);
if (!files || !files.length) return;
files.forEach( function(filename) {
var file = Path.join( dir, filename );
var stats = null;
try { stats = fs.statSync(file); }
catch(e) { return; }
var cont = iterator(file, stats);
if (stats.isDirectory() && (cont !== false)) {
self.walkDirSync( file, iterator );
}
}); // forEach
},
glob: function(filespec, opts, callback) {
// find files using glob pattern
if (!callback) { callback = opts; opts = {}; }
if (!opts) opts = {};
if (this.isWindows) {
opts.windows = true; // allow backslash dir seps
filespec = filespec.replace(/\\/g, '/'); // convert backs to fronts in pattern
}
var pmatch = picomatch(filespec, opts);
var pinfo = picomatch.scan(filespec);
var dir = pinfo.base || '.';
if (dir === filespec) dir = Path.dirname(dir);
var recurse = !!pinfo.glob.match(/(\*\*|\/)/);
this.findFiles( dir, { recurse, dirs: true }, function(err, files) {
if (err) return callback(err);
callback(null, files.filter( function(file) { return pmatch(file); } ));
}); // findFiles
},
globSync: function(filespec, opts) {
// find files using glob pattern, sync
if (!opts) opts = {};
if (this.isWindows) {
opts.windows = true; // allow backslash dir seps
filespec = filespec.replace(/\\/g, '/'); // convert backs to fronts in pattern
}
var pmatch = picomatch(filespec, opts);
var pinfo = picomatch.scan(filespec);
var dir = pinfo.base || '.';
if (dir === filespec) dir = Path.dirname(dir);
var recurse = !!pinfo.glob.match(/(\*\*|\/)/);
var files = this.findFilesSync( dir, { recurse, dirs: true } );
return files.filter( function(file) { return pmatch(file); } );
},
rimraf: function(filespec, opts, callback) {
// multi-recursive delete (rm -rf)
if (!callback) { callback = opts; opts = {}; }
var self = this;
this.glob(filespec, function(err, files) {
if (err) return callback(err);
self.async.eachSeries(files, function(file, callback) {
fs.rm( file, { force: true, recursive: true }, callback );
}, callback );
}); // glob
},
rimrafSync: function(filespec) {
// multi-recursive delete (rm -rf) sync
this.glob.sync(filespec).forEach( function(file) {
fs.rmSync( file, { force: true, recursive: true } );
}); // glob
},
mkdirp: function(path, opts, callback) {
// Recursively create directories
if (!callback) {
callback = opts;
opts = null;
}
if (!opts) opts = { mode: 0o777 };
if (typeof(opts) == 'number') opts = { mode: opts };
opts.recursive = true;
fs.mkdir( path, opts, callback );
},
mkdirpSync: function(path, opts) {
// Recursively create directories, sync
if (!opts) opts = { mode: 0o777 };
if (typeof(opts) == 'number') opts = { mode: opts };
opts.recursive = true;
return fs.mkdirSync( path, opts );
},
writeFileAtomic: function(file, data, opts, callback) {
// write a file atomically
var temp_file = file + '.tmp.' + process.pid + '.' + this.generateShortID();
if (!callback) {
// opts is optional
callback = opts;
opts = {};
}
fs.writeFile( temp_file, data, opts, function(err) {
if (err) return callback(err);
fs.rename( temp_file, file, function(err) {
if (err) {
// cleanup temp file before returning
fs.unlink( temp_file, function() { callback(err); } );
}
else callback();
});
}); // fs.writeFile
},
writeFileAtomicSync: function(file, data, opts) {
// write a file atomically and synchronously
var temp_file = file + '.tmp.' + process.pid + '.' + this.generateShortID();
if (!opts) opts = {};
fs.writeFileSync( temp_file, data, opts );
try {
fs.renameSync( temp_file, file );
}
catch (err) {
// try to cleanup temp file before throwing
fs.unlinkSync( temp_file );
throw err;
}
},
parseJSON: function(text) {
// parse JSON with improved error messages (i.e. line numbers)
text = text.toString().replace(/\r\n/g, "\n"); // Unix line endings
var json = null;
try { json = JSON.parse(text); }
catch (err) {
var lines = text.split(/\n/).map( function(line) { return line + "\n"; } );
var err_msg = (err.message || err.toString()).replace(/\bat\s+position\s+(\d+)/, function(m_all, m_g1) {
var pos = parseInt(m_g1);
var offset = 0;
var loc = null;
for (var idx = 0, len = lines.length; idx < len; idx++) {
offset += lines[idx].length;
if (offset >= pos) {
loc = { line: idx + 1 };
offset -= lines[idx].length;
loc.column = (pos - offset) + 1;
idx = len;
}
}
if (loc) {
return "on line " + loc.line + " column " + loc.column;
}
else return m_all;
});
throw new Error(err_msg);
}
return json;
},
findBin: function(bin, callback) {
// locate binary executable using PATH and known set of common dirs
var dirs = (process.env.PATH || '').split(/\:/).concat(BIN_DIRS).filter( function(item) {
return item.match(/\S/);
} );
var found = false;
this.async.eachSeries( dirs,
function(dir, callback) {
var file = Path.join(dir, bin);
fs.stat( file, function(err, stats) {
if (!err && stats) {
found = file;
return callback("ABORT");
}
callback();
} ); // fs.stat
},
function() {
if (found) callback( false, found );
else callback( new Error("Binary executable not found: " + bin) );
}
); // eachSeries
},
findBinSync: function(bin) {
// locate binary executable using PATH and known set of common dirs
var dirs = (process.env.PATH || '').split(/\:/).concat(BIN_DIRS).filter( function(item) {
return item.match(/\S/);
} );
for (var idx = 0, len = dirs.length; idx < len; idx++) {
var file = Path.join(dirs[idx], bin);
if (fs.existsSync(file)) return file;
}
return false;
},
sortBy: function(orig, key, opts) {
// sort array of objects by key, asc or desc, and optionally return NEW array
// opts: { dir, type, copy }
if (!opts) opts = {};
if (!opts.dir) opts.dir = 1;
if (!opts.type) opts.type = 'string';
var arr = opts.copy ? Array.from(orig) : orig;
arr.sort( function(a, b) {
switch(opts.type) {
case 'string':
return( (''+a[key]).localeCompare(b[key]) * opts.dir );
break;
case 'number':
return (a[key] - b[key]) * opts.dir;
break;
}
} );
return arr;
},
includesAny: function(haystack, needles) {
// return true if haystack contains any needles
// (like Array.includes, but searches first array for ANY matches in second array)
for (var idx = 0, len = needles.length; idx < len; idx++) {
if (haystack.includes(needles[idx])) return true;
}
return false;
},
includesAll: function(haystack, needles) {
// return true if haystack contains ALL needles, false otherwise
for (var idx = 0, len = needles.length; idx < len; idx++) {
if (!haystack.includes(needles[idx])) return false;
}
return true;
},
stripANSI: function(str) {
// strip ansi characters from a string
return str.replace(MATCH_ANSI, '');
},
toTitleCase(str) {
// capitalize each word
return String(str).toLowerCase().replace(/\b\w/g, function (txt) { return txt.toUpperCase(); });
},
stableStringify: function(node) {
// deep-serialize JSON with sorted keys, for comparison purposes
var self = this;
if (node === null) return 'null';
if (this.isaHash(node)) {
var json = '{';
Object.keys(node).sort().forEach( function(key, idx) {
if (idx) json += ',';
json += JSON.stringify(key) + ":" + self.stableStringify(node[key]);
} );
json += '}';
return json;
}
else if (this.isaArray(node)) {
var json = '[';
node.forEach( function(item, idx) {
if (idx) json += ',';
json += self.stableStringify(item);
} );
json += ']';
return json;
}
else return JSON.stringify(node);
},
stablePrettyStringify: function(node) {
// generate stable (alphabetized keys) pretty-printed json
return JSON.stringify( JSON.parse( this.stableStringify(node) ), null, "\t" );
}
}; // module.exports
// some utility functions need to be fully portable
module.exports.glob = module.exports.glob.bind(module.exports);
module.exports.rimraf = module.exports.rimraf.bind(module.exports);
module.exports.mkdirp = module.exports.mkdirp.bind(module.exports);
// *.sync calling conventions, also portable
module.exports.glob.sync = module.exports.globSync.bind(module.exports);
module.exports.rimraf.sync = module.exports.rimrafSync.bind(module.exports);
module.exports.mkdirp.sync = module.exports.mkdirpSync.bind(module.exports);