├── test ├── fixtures │ └── log.txt └── mtail.test.js ├── .gitignore ├── Makefile ├── bin └── mtail ├── package.json ├── LICENSE ├── doc └── man │ └── mtail.1 ├── Readme.md └── index.js /test/fixtures/log.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | tmp/* 4 | *.sw? 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./node_modules/.bin/mocha 3 | 4 | .PHONY: test 5 | -------------------------------------------------------------------------------- /bin/mtail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var mtail = require(__dirname + '/../index'); 4 | mtail.handleOptions(); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mtail", 3 | "description": "Tail multiple files", 4 | "version": "0.0.1", 5 | "author": "Alex R. Young ", 6 | "dependencies": { 7 | "commander": "0.4.1" 8 | }, 9 | "devDependencies": { 10 | "mocha": "latest" 11 | }, 12 | "directories": { 13 | "man": "./doc/man", 14 | "lib": "./lib", 15 | "bin": "./bin" 16 | }, 17 | "keywords": ["console", "command-line", "tail"], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/alexyoung/mtail" 21 | }, 22 | "bugs": { 23 | "url": "http://github.com/alexyoung/mtail/issues" 24 | }, 25 | "main": "index", 26 | "bin": { "mtail": "./bin/mtail" }, 27 | "engines": { "node": ">= 0.5.0 < 0.7.0" }, 28 | "scripts": { "test": "make test" }, 29 | "licenses": [ 30 | { 31 | "type": "MIT", 32 | "url": "http://github.com/alexyoung/mtail/raw/master/LICENSE" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012, Alex R. Young 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /doc/man/mtail.1: -------------------------------------------------------------------------------- 1 | .TH MTAIL "1" "2012" "" "" 2 | 3 | 4 | .SH "NAME" 5 | mtail \- Tail multiple files at once 6 | 7 | .SH SYNOPSIS 8 | 9 | 10 | .B mtail 11 | [ 12 | .B \-V 13 | ] 14 | [ 15 | .B \-e 16 | | 17 | .B \-\-encoding 18 | .I encoding 19 | ] 20 | [ 21 | .B \-n 22 | .I number 23 | ] 24 | [ 25 | .B \-p 26 | | 27 | .B \-\-print\-file 28 | .I file\-name 29 | ] 30 | [ 31 | .B \-s 32 | | 33 | .B \-\-sleep\-interval 34 | .I sleep\-interval 35 | ] 36 | [ 37 | .B \-t 38 | | 39 | .B \-\-truncate 40 | .I length 41 | ] 42 | [ 43 | .I file ... 44 | ] 45 | .br 46 | 47 | .SH DESCRIPTION 48 | 49 | The mtail utility displays the contents of multiple files. 50 | 51 | .SH OPTIONS 52 | .PP 53 | \-V, \-\-version 54 | .RS 4 55 | Prints the version of the program\&. 56 | .RE 57 | .PP 58 | \-\-help 59 | .RS 4 60 | Prints the synopsis and a list of the most commonly used commands\&. 61 | .RE 62 | .PP 63 | \-e, \-\-encoding 64 | .RS 4 65 | Sets the encoding of the input streams. Defaults to 'utf8'\&. 66 | .RE 67 | .PP 68 | \-n 69 | .RS 4 70 | Number of lines to print from the file. Defaults to 10\&. 71 | .RE 72 | .PP 73 | \-p, \-\-print\-file 74 | .RS 4 75 | Print the file name before each line\&. 76 | .RE 77 | .PP 78 | \-s, \-\-sleep\-interval 79 | .RS 4 80 | Sets the number of milliseconds to check for file changes on systems that don't support inotify. Defaults to 100\&. 81 | .RE 82 | .PP 83 | \-t, \-\-truncate 84 | .RS 4 85 | Truncates the file name to . If no length is supplied, the file's basename will be displayed\&. 86 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## mtail 2 | 3 | Tail multiple files. 4 | 5 | ### Usage 6 | 7 | Usage: mtail [options] 8 | 9 | Options: 10 | 11 | -h, --help output usage information 12 | -V, --version output the version number 13 | -e, --encoding [encoding] File encoding [utf8] 14 | -f, --follow Keep watching the file for changes 15 | -n [lines] Start location in number of lines [10] 16 | -s, --sleep-interval Sleep interval in milliseconds [100] 17 | -p, --print-file Print the name of each file 18 | -t, --truncate [length] Truncate filenames when printed with -p, defaults to truncating to basename 19 | 20 | ### Installation 21 | 22 | npm install mtail 23 | 24 | ### License 25 | 26 | (The MIT License) 27 | 28 | Copyright (C) 2012, Alex R. Young 29 | 30 | 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: 31 | 32 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 33 | 34 | 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. 35 | -------------------------------------------------------------------------------- /test/mtail.test.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec 2 | , spawn = require('child_process').spawn 3 | , mtail = require(__dirname + '/../index') 4 | , assert = require('assert'); 5 | 6 | before(function(done) { 7 | exec('> ' + __dirname + '/fixtures/log.txt', function(err, stdout, stderr) { 8 | done(); 9 | }); 10 | }); 11 | 12 | describe('truncate', function() { 13 | it('should truncate to basename by default', function() { 14 | assert.equal('index.js', mtail.util.truncate('../test/index.js', true)); 15 | }); 16 | 17 | it('should truncate to any length', function() { 18 | assert.equal('x.js', mtail.util.truncate('../test/index.js', 4)); 19 | }); 20 | }); 21 | 22 | 23 | describe('colour', function() { 24 | it('should colourise text', function() { 25 | assert.equal('\033[90mtest\033[0m', mtail.util.colour(0, 'test')); 26 | }); 27 | }); 28 | 29 | describe('tailFile', function() { 30 | it('should tail a single file', function(done) { 31 | var child = spawn(__dirname + '/../bin/mtail', ['-fn', 0, '-t', '10', __dirname + '/fixtures/log.txt']) 32 | , data = ''; 33 | 34 | child.stdout.on('data', function(buffer) { 35 | data += buffer.toString(); 36 | }); 37 | 38 | child.on('exit', function() { 39 | assert.equal('Logged result\n', data); 40 | done(); 41 | }); 42 | 43 | setTimeout(function() { 44 | exec('echo "Logged result" >> ' + __dirname + '/fixtures/log.txt', function(err, stdout, stderr) { 45 | if (err) console.error(err); 46 | setTimeout(function() { 47 | child.kill('SIGINT'); 48 | }, 110); 49 | }); 50 | }, 110); 51 | }); 52 | 53 | it('should handle file truncation', function(done) { 54 | var child = spawn(__dirname + '/../bin/mtail', ['-fn', '0', '-t', '10', __dirname + '/fixtures/log.txt']) 55 | , data = ''; 56 | 57 | child.stdout.on('data', function(buffer) { 58 | data += buffer.toString(); 59 | }); 60 | 61 | child.on('exit', function() { 62 | assert.equal('Logged result\n', data); 63 | done(); 64 | }); 65 | 66 | setTimeout(function() { 67 | exec('echo "Logged result" > ' + __dirname + '/fixtures/log.txt', function(err, stdout, stderr) { 68 | if (err) console.error(err); 69 | setTimeout(function() { 70 | child.kill('SIGINT'); 71 | }, 100); 72 | }); 73 | }, 110); 74 | }); 75 | 76 | it('should handle zero length files', function(done) { 77 | var child = spawn(__dirname + '/../bin/mtail', ['-fn', '0', '-t', '10', __dirname + '/fixtures/log.txt']) 78 | , data = ''; 79 | 80 | child.on('exit', function() { 81 | done(); 82 | }); 83 | 84 | child.stdout.on('data', function(buffer) { 85 | }); 86 | 87 | child.stderr.on('data', function(buffer) { 88 | assert.fail(buffer.toString()); 89 | }); 90 | 91 | 92 | setTimeout(function() { 93 | exec('echo "test" > ' + __dirname + '/fixtures/log.txt', function(err, stdout, stderr) { 94 | if (err) console.error(err); 95 | 96 | exec('> ' + __dirname + '/fixtures/log.txt', function(err, stdout, stderr) { 97 | if (err) console.error(err); 98 | setTimeout(function() { 99 | child.kill('SIGINT'); 100 | }, 100); 101 | }); 102 | }); 103 | }, 110); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , tty = require('tty') 3 | , isatty = tty.isatty(1) && tty.isatty(2) 4 | , path = require('path') 5 | , colours = [90, 31, 92, 91, 93, 36, 31, 32, 42, 41] 6 | , bufferSize = 4096 7 | , fileCount = 0; 8 | 9 | function colour(index, str) { 10 | if (!isatty) return str; 11 | index = index % (colours.length - 1); 12 | return '\033[' + colours[index] + 'm' + str + '\033[0m'; 13 | } 14 | 15 | function handleOptions() { 16 | var program = require('commander'); 17 | 18 | program 19 | .version(JSON.parse(fs.readFileSync(__dirname + '/package.json')).version) 20 | .option('-e, --encoding [encoding]', 'File encoding [utf8]', 'utf8') 21 | .option('-f, --follow', 'Keep watching the file for changes') 22 | .option('-n [lines]', 'Start location in number of lines [10]', parseInt) 23 | .option('-s, --sleep-interval', 'Sleep interval in milliseconds [100]', parseInt) 24 | .option('-p, --print-file', 'Print the name of each file') 25 | .option('-t, --truncate [length]', 'Truncate filenames when printed with -p, defaults to truncating to basename', parseInt) 26 | .usage('[options] ') 27 | .parse(process.argv); 28 | 29 | if (program.args) { 30 | program.args.forEach(function(file) { 31 | tailFile(file, program); 32 | }); 33 | } 34 | } 35 | 36 | function truncate(file, length) { 37 | return length === true ? path.basename(file) : file.slice(file.length - length); 38 | } 39 | 40 | function tailFile(file, options) { 41 | var fileText = options.truncate ? colour(fileCount, truncate(file, options.truncate)) + ': ' : colour(fileCount, file) + ': ' 42 | , charsWritten = 0 43 | , output = new Buffer(bufferSize); // TODO: Buffer size? 44 | 45 | fileCount++; 46 | 47 | function clear() { 48 | output.fill(0); 49 | charsWritten = 0; 50 | } 51 | 52 | function print(buffer) { 53 | options.printFile ? printFile(buffer.toString()) : process.stdout.write(buffer); 54 | } 55 | 56 | function showLinesWithLength(fd, n, length, fn) { 57 | var text = ''; 58 | if (length === 0) return fn(); 59 | 60 | function end() { 61 | var l = text.length, reversed = '', i; 62 | for (i = l - 2; i >= 0; i--) { 63 | reversed += text[i]; 64 | } 65 | clear(); 66 | print(reversed); 67 | fn(); 68 | } 69 | 70 | function read(p) { 71 | fs.read(fd, new Buffer(1, options.encoding), 0, 1, p, function(err, bytes, buffer) { 72 | var c = buffer.toString(); 73 | 74 | if (c === '\n') n--; 75 | text += c; 76 | 77 | if (n === 0) { 78 | end(); 79 | } else { 80 | p < 1 ? end() : read(p - 1); 81 | } 82 | }); 83 | } 84 | 85 | read(length - 1); 86 | } 87 | 88 | function showLines(fd, n, fn) { 89 | fs.stat(file, function(err, stat) { 90 | showLinesWithLength(fd, n + 1, stat.size, fn); 91 | }); 92 | } 93 | 94 | function printFile(text) { 95 | var i = text.indexOf('\n'); 96 | 97 | if (i >= 0) { 98 | output.write(text.substr(0, i), charsWritten); 99 | charsWritten += i; 100 | process.stdout.write(fileText + output.toString(options.encoding, 0, charsWritten) + '\n'); 101 | clear(); 102 | 103 | // Recursively print until the newlines have all been found 104 | if (i < text.length - 1) { 105 | return printFile(text.substr(i + 1)); 106 | } 107 | } else { 108 | output.write(text, charsWritten); 109 | charsWritten += text.length; 110 | 111 | // If the buffer gets to the end, print it and clear it 112 | if (charsWritten >= bufferSize) { 113 | process.stdout.write(fileText + output.toString(options.encoding, 0, charsWritten) + '\n'); 114 | clear(); 115 | } 116 | } 117 | } 118 | 119 | fs.open(file, 'r', function(err, fd) { 120 | var lastStat = fs.statSync(file); 121 | 122 | if (err) { 123 | console.error('Error opening file:', file); 124 | console.error(err); 125 | return; 126 | } 127 | 128 | function watch() { 129 | if (!options.follow) return; 130 | 131 | fs.watch(file, { interval: options.sleepInterval || 100 }, function(event) { 132 | if (event === 'change') { 133 | fs.stat(file, function(err, stat) { 134 | var delta = stat.size - lastStat.size 135 | , start = lastStat.size; 136 | 137 | if (delta <= 0) { 138 | delta = stat.size; 139 | start = 0; 140 | } 141 | 142 | lastStat = stat; 143 | 144 | if (stat.size === 0) return; 145 | 146 | fs.read(fd, new Buffer(delta, options.encoding), 0, delta, start, function(err, bytes, buffer) { 147 | if (err) { 148 | // TODO: Clean exit 149 | console.error('Error reading file:', file); 150 | console.error(err); 151 | process.exit(1); 152 | } 153 | 154 | print(buffer); 155 | }); 156 | }); 157 | } 158 | }); 159 | } 160 | 161 | options.N = typeof(options.N) === 'undefined' ? 10 : options.N; 162 | 163 | if (options.N) { 164 | showLines(fd, options.N, watch); 165 | } else { 166 | watch(); 167 | } 168 | }); 169 | } 170 | 171 | module.exports.handleOptions = handleOptions; 172 | module.exports.tailFile = tailFile; 173 | module.exports.util = { 174 | colour: colour 175 | , truncate: truncate 176 | }; 177 | --------------------------------------------------------------------------------