├── README.md ├── dedup.js └── lib ├── logger.js ├── progressbar.js └── termcolors.js /README.md: -------------------------------------------------------------------------------- 1 | node-dedup 2 | =========== 3 | 4 | ### Performs a poor man's file deduplication recursively on a directory. Deletes duplicate files, and creates symbolic links in their place. 5 | 6 | About 7 | ======== 8 | 9 | **node-dedup** performs a *poor* mans file deduplication on a base directory and recursively walks down. It loops through starting at the base directory and constructs a *SHA256* hash for each file *(excluding .DS_Store files and node-dedup-db directories)*. It then sorts all the hashs, and if a hash exists more than once, it deletes the duplicate files and creates a symbolic link in its place. 10 | 11 | How To Use 12 | ======== 13 | 14 | **From the command line:** 15 | 16 | $ node dedup.js /some/path/here 17 | 18 | ![alt node-dedup](http://i.imgur.com/UnWY7.png "node-dedup") 19 | 20 | $ node dedup.js /some/path/here --dryrun 21 | 22 | ![alt node-dedup-dryrun](http://i.imgur.com/460CJ.png "node-dedup-dryrun") 23 | 24 | $ node dedup.js --version 25 | 0.0.3 26 | 27 | 28 | Why 29 | ======== 30 | 31 | Surprisingly, we have lots of duplicate files on our systems, and we were looking for a solution to search and find duplicates and symlink them instead of having multiple copies wasting disk space. 32 | 33 | **Some example use cases are music libraries, pictures, and videos. Static content that inst changed often makes the most sense.** 34 | 35 | 36 | Warning 37 | ============ 38 | 39 | **BE VERY CAREFUL. NODE-DEDUP IS STILL UNDER DEVELOPMENT, AND THINGS CAN GO VERY WRONG, VERY QUICKLY IF YOUR NOT CAREFUL. WE RECOMMEND, EITHER SETTING UP A TEST ENVIRONMENT AT FIRST, AND MANUALLY COPYING FILES INTO THE TEST ENVIRONMENT OR RUNNING NODE-DEDUP IN DRY-RUN MODE. RUNNING NODE-DEDUP ON YOUR ENTIRE DISK, WOULD PROBABLY BE A VERY BAD IDEA.** 40 | 41 | 42 | Database 43 | ========= 44 | 45 | **node-dedup** keeps a basic **json** database log file of every deduplication performed. The name of the file is the UNIX timestamp and .json extension. The database file provides exactly what files link to what in case you ever need to manually revert. In the future, we would like to build the ability for node-dedup to read a database log file, and 'undo' the duplication; i.e. delete the symbolic links, and create copies of the files. 46 | 47 | **Example database log file:** 48 | 49 | [ 50 | { 51 | "hash": "b8a434ad9deddbb2bb246e0e403fdca3a8ca0a67e052a6583cdfa68d2965a344", 52 | "path": "/Users/justin/test/vegas-1.jpg", 53 | "linksto": null 54 | }, 55 | { 56 | "hash": "b8a434ad9deddbb2bb246e0e403fdca3a8ca0a67e052a6583cdfa68d2965a344", 57 | "path": "/Users/justin/test/vegas-2.jpg", 58 | "linksto": "/Users/justin/test/vegas-1.jpg" 59 | }, 60 | { 61 | "hash": "b8a434ad9deddbb2bb246e0e403fdca3a8ca0a67e052a6583cdfa68d2965a344", 62 | "path": "/Users/justin/test/vegas-3.jpg", 63 | "linksto": "/Users/justin/test/vegas-1.jpg" 64 | } 65 | ] 66 | 67 | 68 | To Do 69 | =========== 70 | 71 | * The file listing should sort by **date modified desc**, instead of name, that way the newest file modified gets to be link too 72 | * Somehow cache hashes, instead of calculating them everytime (naughty... slow) 73 | * Read in a database file, and 'undo' a node-dedup; i.e. remove symbolic links and copy back files 74 | * Move from flat .json files as the database to Redis or MongoDB 75 | 76 | Change Log / Version History 77 | =========== 78 | 79 | * 0.0.3 (12/01/2011) 80 | + Added option to child.process.exec() { maxBuffer: (200*10240) } which should prevent buffer overflow errors, unless reading a massive amount of files. 81 | 82 | * 0.0.2 (12/01/2011) 83 | + Added flag '--dryrun' which does not delete files and does not create symbolic links. Use for testing. 84 | * Modified database structure, changed 'link' property to 'linksto' for clearification. 85 | * Improved logging to screen to show number of files read and number of files deduplciated. 86 | 87 | * 0.0.1 (11/28/2011) 88 | 89 | Author / Contact 90 | ============ 91 | 92 | Created and coded by the **NodeSocket** team. 93 | 94 | _Website: _ 95 | 96 | _Twitter: _ 97 | 98 | Problems? Bugs? Feature Requests? __ 99 | 100 | *(c) 2011 NodeSocket LLC. All Rights Reserved.* 101 | 102 | License & Legal 103 | ============== 104 | 105 | *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:* 106 | 107 | *The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.* 108 | 109 | *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.* -------------------------------------------------------------------------------- /dedup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * node-dedup.js 3 | * 4 | * @version 0.0.3 5 | * @date last modified 12/01/2011 6 | * @author NodeSocket 7 | * @copyright (c) 2011 NodeSocket. All Rights Reserved. 8 | */ 9 | 10 | /* 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 12 | * associated documentation files (the "Software"), to deal in the Software without restriction, including 13 | * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 15 | * following conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all copies or substantial 18 | * portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 23 | * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | * DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | //// 28 | // Requires 29 | //// 30 | var colors = require('./lib/termcolors').colors; 31 | var logger = require('./lib/logger'); 32 | var progress_hash = require('./lib/progressbar').create(process.stdout); 33 | var progress_deduplicate = require('./lib/progressbar').create(process.stdout); 34 | var exec = require('child_process').exec; 35 | var fs = require('fs'); 36 | var crypto = require('crypto'); 37 | var path = require('path'); 38 | 39 | //// 40 | // dedup 41 | //// 42 | var dedup = module.exports = { 43 | //Verion of node-dedup 44 | version: '0.0.3', 45 | 46 | //Dryrun, don't acutally delete and create the symbolic links, used for testing 47 | dryrun: null, 48 | 49 | //Base directory path to start working from 50 | base: null, 51 | 52 | //Array of objects; each object contains a hash, path, and linksto 53 | files: [], 54 | 55 | //// 56 | // init 57 | // parameters: p_base (String) base directory path to start from 58 | //// 59 | init: function(p_base, p_is_dryrun) { 60 | //Confirm that p_base directory exists 61 | path.exists(p_base, function(p_exists) { 62 | //Base directory path exists 63 | if(p_exists) { 64 | //Set global dryrun equal to p_is_dryrun 65 | dedup.dryrun = p_is_dryrun; 66 | 67 | //Show the log that its a dryrun 68 | if(dedup.dryrun) { 69 | logger.log("Notice: DRY RUN. No files will be deleted or symbolic links created.", 'notice'); 70 | } 71 | 72 | //Set global base equal to p_base, removes trailing slashes if it exists 73 | dedup.base = p_base.replace(/\/$/, ''); 74 | 75 | logger.log("Notice: Deduplicating from '" + dedup.base + "'.", 'notice'); 76 | 77 | //Check if node-dedup database directory exists 78 | path.exists(dedup.base + '/node-dedup-db', function(p_exists) { 79 | //Database directory does not exist 80 | if(!p_exists) { 81 | logger.log("Notice: No database directory found in '" + dedup.base + "', automatically creating it.", 'notice'); 82 | 83 | //Create database directory 84 | fs.mkdir(dedup.base + '/node-dedup-db', function(p_err) { 85 | if(p_err) { 86 | logger.log(p_err.message, 'error'); 87 | } else { 88 | //Call getFiles 89 | dedup.getFiles(dedup.base); 90 | } 91 | }); 92 | } 93 | //Database directory exists 94 | else { 95 | //Call getFiles 96 | dedup.getFiles(dedup.base); 97 | } 98 | }); 99 | } 100 | //Base directory path does not exist 101 | else { 102 | logger.log("Error: Provided base directory '" + p_base + "' does not exist.", 'error'); 103 | process.exit(-2); 104 | } 105 | }); 106 | }, 107 | //// 108 | // getFiles 109 | // parameters: p_base (String) base directory path to start from 110 | //// 111 | getFiles: function(p_base) { 112 | logger.log("Reading files...", 'info'); 113 | 114 | //// 115 | // todo: 116 | // It may be faster to use native node and fs.readdir() recursively instead of find. Needs benchmarking. 117 | //// 118 | //Find files, exclude .DS_Store and everything inside node-dedup-db directory 119 | exec('find ' + p_base + ' -type f \\( -not -iname ".DS_Store" -and -not -ipath "' + dedup.base + '/node-dedup-db/*" \\) | sort', { maxBuffer: (200*10240) }, function(p_err, p_stdout, p_stderr) { 120 | if(p_err) { 121 | logger.log(p_err.message, 'error'); 122 | } else if(p_stderr) { 123 | logger.log(p_stderr, 'error'); 124 | } else { 125 | //Split the paths on newline into an array 126 | var paths = p_stdout.split('\n'); 127 | 128 | //Remove the last element if its empty 129 | if(paths[paths.length - 1] === '') { 130 | paths.pop(); 131 | } 132 | 133 | logger.log("Discovered [" + colors.green(paths.length, true) + "] total files in '" + p_base + "'.", 'info'); 134 | 135 | //Call generateHashes 136 | dedup.generateHashes(paths); 137 | } 138 | }); 139 | }, 140 | //// 141 | // generateHashes 142 | // parameters: p_paths (Array) list of file paths to generate hashes for 143 | //// 144 | generateHashes: function(p_paths) { 145 | //Tracks callbacks 146 | var counter = 0; 147 | 148 | if(p_paths.length > 0) { 149 | logger.log("Generating hashes... (Can take a while).", 'info'); 150 | } 151 | 152 | //Loop through file paths 153 | for(var i = 0; i < p_paths.length; i++) { 154 | var hash = crypto.createHash('sha256'); 155 | var stream = fs.ReadStream(p_paths[i]); 156 | 157 | //OnData 158 | stream.on('data', function(p_data) { 159 | this.hash.update(p_data); 160 | }.bind({ 161 | hash: hash 162 | })); 163 | 164 | //OnEnd 165 | stream.on('end', function() { 166 | //Add file object to the global files array 167 | dedup.files.push({ 168 | hash: this.hash.digest('hex'), 169 | path: this.path, 170 | linksto: null 171 | }); 172 | 173 | //Increment counter 174 | counter++; 175 | 176 | //Update progress bar 177 | progress_hash.update(counter / p_paths.length); 178 | 179 | //Done looping through paths 180 | if(counter === p_paths.length) { 181 | //Call findDuplicates 182 | dedup.findDuplicates(); 183 | } 184 | }.bind({ 185 | hash: hash, 186 | path: p_paths[i] 187 | })); 188 | } 189 | }, 190 | //// 191 | // findDuplicates 192 | // parameters: none 193 | //// 194 | findDuplicates: function() { 195 | //Sort files array by hash, neat trick; thanks stackoverflow. 196 | dedup.files.sort(function(p_a, p_b) { 197 | return p_a.hash.localeCompare(p_b.hash); 198 | }); 199 | 200 | //Storage for duplicates 201 | var duplicate_storage = []; 202 | 203 | //A pointer to array objects for previous, current, and the file that is kept 204 | var prev = null; 205 | var current = null; 206 | var keep = null; 207 | 208 | if(dedup.files.length > 0) { 209 | logger.log("\n\tDeduplicating...", 'info'); 210 | } 211 | 212 | //Loop through files array 213 | for(var i = 0; i < dedup.files.length; i++) { 214 | //Set previous, assuming not the first time through the loop 215 | if(i > 0) { 216 | prev = dedup.files[i - 1]; 217 | } 218 | 219 | //Set current 220 | current = dedup.files[i]; 221 | 222 | //Set keep to current, if its the first time through the loop 223 | if(prev === null) { 224 | keep = current; 225 | //Previous equals current 226 | } else if(prev.hash === current.hash) { 227 | duplicate_storage.push({ 228 | keep: keep, 229 | del: current 230 | }); 231 | } 232 | //New file, set keep to current 233 | else { 234 | keep = current; 235 | } 236 | } 237 | 238 | //Keeps track of the number of callbacks so we know when we are done 239 | var callbacks = 0; 240 | 241 | //Loop through storage 242 | for(var dup in duplicate_storage) { 243 | //Call deleteAndSymlink 244 | dedup.deleteAndSymlink(duplicate_storage[dup].keep, duplicate_storage[dup].del, function() { 245 | //Increment callbacks 246 | callbacks++; 247 | 248 | //Update progress bar 249 | progress_deduplicate.update(callbacks / duplicate_storage.length); 250 | 251 | //Done with all deletes and symlinks 252 | if(callbacks === duplicate_storage.length) { 253 | logger.log("\n\tDeduplicated [" + colors.green(duplicate_storage.length, true) + "] files in '" + dedup.base + "'.", 'info'); 254 | 255 | //Call writeDatabaseFile 256 | dedup.writeDatabaseFile(); 257 | } 258 | }); 259 | } 260 | }, 261 | //// 262 | // deleteAndSymlink 263 | // parameters: p_keep (Object) File to link too 264 | // p_delete (Object) File to delete and create symbolic link from 265 | // p_callback (Function) Called on complete 266 | //// 267 | deleteAndSymlink: function(p_keep, p_delete, p_callback) { 268 | //Not a dryrun, do the delete and create the sumbolic link 269 | if(!dedup.dryrun) { 270 | //Delete p_delete file 271 | fs.unlink(p_delete.path, function(p_err) { 272 | if(p_err) { 273 | logger.log(p_err.message, 'error'); 274 | } else { 275 | //Create the symbolic link from p_keep path to the p_delete path 276 | fs.symlink(p_keep.path, p_delete.path, function(p_err) { 277 | if(p_err) { 278 | logger.log(p_err.message, 'error'); 279 | } else { 280 | //Update p_delete linksto to p_keep's path 281 | p_delete.linksto = p_keep.path; 282 | 283 | //Call the callback, if it exists 284 | typeof p_callback === "function" ? p_callback.call() : null; 285 | } 286 | }); 287 | } 288 | }); 289 | } 290 | //A dryrun, don't do the delete and create of the symbolic link 291 | else { 292 | //Update p_delete link to p_keep's path 293 | p_delete.linksto = p_keep.path; 294 | 295 | //Call the callback, if it exists 296 | typeof p_callback === "function" ? p_callback.call() : null; 297 | } 298 | }, 299 | //// 300 | // writeDatabaseFile 301 | // parameters: none 302 | //// 303 | writeDatabaseFile: function() { 304 | var db_file_name = Math.round((new Date()).getTime() / 1000) + '.json'; 305 | 306 | logger.log("Notice: Writing database file '" + db_file_name + "' to database directory '" + dedup.base + "/node-dedup-db'", 'notice'); 307 | 308 | //Write the database file in base directory path with the filename being UNIX timestamp.json 309 | fs.writeFile(dedup.base + "/node-dedup-db/" + db_file_name, JSON.stringify(dedup.files), 'utf8', function(p_err) { 310 | if(p_err) { 311 | logger.log(p_err.message, 'error'); 312 | } else { 313 | logger.log(colors.bg_green("Complete!", true), 'info'); 314 | 315 | //Exit nicely 316 | process.exit(0); 317 | } 318 | }); 319 | } 320 | } 321 | 322 | //// 323 | // Execution begins 324 | //// 325 | //argv[2] undefined, show error and exit 326 | if(typeof process.argv[2] === "undefined") { 327 | logger.log("Usage: node dedup.js /Users/john { --dryrun }", 'info'); 328 | process.exit(-1); 329 | } 330 | //argv[2] is set 331 | else { 332 | if(process.argv[2] === '-v' || process.argv[2] === '--version' ) { 333 | console.log(dedup.version); 334 | } else { 335 | if(process.argv[3] === '--dryrun') { 336 | //Call init, passing argv[2] as the base directory path and true as dryrun flag 337 | dedup.init(process.argv[2], true); 338 | } else { 339 | //Call init, passing argv[2] as the base directory path and false as dryrun flag 340 | dedup.init(process.argv[2], false); 341 | } 342 | } 343 | } -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * logger.js 3 | * 4 | * @version 0.0.1 5 | * @date last modified 11/28/2011 6 | * @author NodeSocket 7 | * @copyright (c) 2011 NodeSocket. All Rights Reserved. 8 | */ 9 | 10 | /* 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 12 | * associated documentation files (the "Software"), to deal in the Software without restriction, including 13 | * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 15 | * following conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all copies or substantial 18 | * portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 23 | * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | * DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | var colors = require('./termcolors').colors; 28 | 29 | var loger = module.exports = { 30 | log: function(message, level) { 31 | if(typeof level === "undefined") { 32 | level = 'notice'; 33 | } 34 | 35 | if(level === 'error') { 36 | console.log(colors.bg_red("\t" + message, true)); 37 | } else if(level === 'notice') { 38 | console.log(colors.bg_lblue("\t" + message, true)); 39 | } else if(level === 'info') { 40 | console.log("\t" + message); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /lib/progressbar.js: -------------------------------------------------------------------------------- 1 | var tty = require('tty'); 2 | 3 | /** 4 | * Creates a ProgressBar. 5 | * 6 | * @constructor 7 | * @this {ProgressBar} 8 | * @param {stdout} output The output interface for the progress bar. Use process.stdout. 9 | * @param {number} width The width of the progress bar in characters, borders excluded. 10 | */ 11 | function ProgressBar(output, width){ 12 | if (width){ 13 | this.width = width; 14 | } 15 | this._output = output; 16 | } 17 | 18 | ProgressBar.prototype = { 19 | /** 20 | * The output interface of the progress bar. 21 | * 22 | * @private 23 | */ 24 | _output: null, 25 | /** 26 | * {Object} Object containing the symbol instructions for the progress bar. 27 | */ 28 | symbols: { 29 | leftBorder: '[', 30 | rightBorder: ']', 31 | loaded: '#', 32 | notLoaded: '-' 33 | }, 34 | /** 35 | * {number} Width of the progress bar in characters, borders excluded. 36 | */ 37 | width: 70, 38 | /** 39 | * {Boolean} Boolean determining whether the bar progresses from left to right. 40 | */ 41 | leftToRight: true, 42 | /** 43 | * {String} The format to write on the stdout. 44 | */ 45 | format: '\t$bar; $percentage;%', 46 | /** 47 | * {number} Progress value (0.0 - 1.0) of the progress bar. 48 | */ 49 | progress: 0, 50 | /** 51 | * {RegExp} The RegExp the bar uses to interpret the format string. 52 | */ 53 | interpreter: /\$([a-z]+)(\s?,\s?([0-9]+)(\s?:\s?(.))?)?;/ig, 54 | /** 55 | * Updates the progress bar graphical representation. 56 | * 57 | * @param {number} value The new value for progress. (Optional) 58 | */ 59 | update: function(value){ 60 | if (typeof value !== 'undefined'){ 61 | value = +value; 62 | if (value < 0 || value > 1){ 63 | throw new RangeError('Value out of bounds.'); 64 | } else if (isNaN(value)) { 65 | throw new TypeError('Value not a number.'); 66 | } else { 67 | this.progress = value; 68 | } 69 | } 70 | value = this.progress; 71 | var percentage = Math.floor(value * 100), 72 | done = Math.floor(value * this.width), 73 | notDone = this.width - done, 74 | graphStart = Array(done + 1).join(this.symbols.loaded), 75 | graphEnd = Array(notDone + 1).join(this.symbols.notLoaded), 76 | graph = this.leftToRight ? graphStart + graphEnd : graphEnd + graphStart; 77 | 78 | graph = this.symbols.leftBorder + 79 | graph + 80 | this.symbols.rightBorder; 81 | 82 | this.clear(); 83 | 84 | var line = this.format.replace(this.interpreter, function(str, valueName, padding, pad){ 85 | padding = arguments[3] || 0; 86 | pad = arguments[5] || ' '; 87 | var val; 88 | switch (valueName.toLowerCase()){ 89 | case 'percentage': 90 | val = percentage; 91 | break; 92 | case 'progress': 93 | val = value; 94 | break; 95 | case 'bar': 96 | val = graph; 97 | break; 98 | default: 99 | return str; 100 | } 101 | val = String(val); 102 | if (padding){ 103 | var paddingNeeded = Number(padding) - val.length + 1; 104 | val = Array(paddingNeeded > 0 ? paddingNeeded : 1).join(pad) + val; 105 | } 106 | return val; 107 | }); 108 | this._output.write(line); 109 | }, 110 | /** 111 | * Clears the progress bar off the screen. 112 | */ 113 | clear: function(){ 114 | this._output.cursorTo(0); 115 | this._output.clearLine(1); 116 | } 117 | } 118 | 119 | exports.ProgressBar = ProgressBar; 120 | /** 121 | * A shorthand function to create a new ProgressBar instance. 122 | */ 123 | exports.create = function(a,b,c){ 124 | return new ProgressBar(a,b,c); 125 | } -------------------------------------------------------------------------------- /lib/termcolors.js: -------------------------------------------------------------------------------- 1 | var i, colors = exports.colors = {}, 2 | format = function(color) { 3 | return "\033[" + colors.fg[color] + "m"; 4 | }, 5 | formatbg = function(color) { 6 | return "\033[" + colors.bg[color] + "m"; 7 | }; 8 | 9 | colors.fg = { 10 | black: '30', 11 | dgray: '1;30', 12 | red: '31', 13 | lred: '1;31', 14 | green: '32', 15 | lgreen: '1;32', 16 | brown: '33', 17 | yellow: '1;33', 18 | blue: '34', 19 | lblue: '1;34', 20 | purple: '35', 21 | lpurple: '1;35', 22 | cyan: '36', 23 | lcyan: '1;36', 24 | lgray: '37', 25 | white: '1;37', 26 | none: '' 27 | }; 28 | colors.bg = { 29 | darkgray: 40, 30 | red: 41, 31 | green: 42, 32 | yellow: 43, 33 | lblue: 44, 34 | purple: 45, 35 | lcyan: 46, 36 | lgray: 47 37 | }; 38 | 39 | 40 | for (i in colors.fg) { 41 | colors[i] = (function(color) { 42 | return function(str, n) { 43 | str = str || ''; 44 | n = (n) ? format('none') : ''; 45 | return format(color) + str + n; 46 | } 47 | })(i); 48 | } 49 | 50 | for (i in colors.bg) { 51 | colors['bg_' + i] = (function(color) { 52 | return function(str) { 53 | return formatbg(color) + str + format('none'); 54 | } 55 | })(i); 56 | } 57 | 58 | 59 | colors.bold = function(str) { 60 | return "\033[1m" + str + "\033[0m" 61 | }; --------------------------------------------------------------------------------