diff --git a/src/shell.js b/src/shell.js index 6a386e4..1497ea7 100644 --- a/src/shell.js +++ b/src/shell.js @@ -159,6 +159,88 @@ define(function(require) { }); }; + /** + * Get the listing of a directory, returning an array of + * file entries in the following form: + * + * { + * path: the basename of the directory entry + * links: the number of links to the entry + * size: the size in bytes of the entry + * modified: the last modified date/time + * type: the type of the entry + * contents: an optional array of child entries + * } + * + * By default ls() gives a shallow listing. If you want + * to follow directories as they are encountered, use + * the `recursive=true` option. + */ + Shell.prototype.ls = function(dir, options, callback) { + if(!dir) { + callback(new Error("Missing dir argument")); + return; + } + + var fs = this.fs; + if(typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + callback = callback || function(){}; + + function list(path, callback) { + var pathname = Path.resolve(this.cwd, path); + var result = []; + + fs.readdir(pathname, function(error, entries) { + if(error) { + callback(error); + return; + } + + function getDirEntry(name, callback) { + name = Path.join(pathname, name); + fs.stat(name, function(error, stats) { + if(error) { + callback(error); + return; + } + var entry = { + path: Path.basename(name), + links: stats.nlinks, + size: stats.size, + modified: stats.mtime, + type: stats.type + }; + + if(options.recursive && stats.type === 'DIRECTORY') { + list(Path.join(pathname, entry.path), function(error, items) { + if(error) { + callback(error); + return; + } + entry.contents = items; + result.push(entry); + callback(); + }); + } else { + result.push(entry); + callback(); + } + }); + } + + async.each(entries, getDirEntry, function(error) { + callback(error, result); + }); + }); + } + + list(dir, callback); + }; + return Shell; }); diff --git a/tests/spec/shell/ls.spec.js b/tests/spec/shell/ls.spec.js new file mode 100644 index 0000000..9c51c30 --- /dev/null +++ b/tests/spec/shell/ls.spec.js @@ -0,0 +1,186 @@ +define(["Filer", "util"], function(Filer, util) { + + describe('FileSystemShell.ls', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be a function', function() { + var shell = util.shell(); + expect(shell.ls).to.be.a('function'); + }); + + it('should fail when dirs argument is absent', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + shell.cat(null, function(error, list) { + expect(error).to.exist; + expect(list).not.to.exist; + done(); + }); + }); + + it('should return the contents of a simple dir', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + var contents2 = "bb"; + + fs.writeFile('/file', contents, function(err) { + if(err) throw err; + + fs.writeFile('/file2', contents2, function(err) { + if(err) throw err; + + shell.ls('/', function(err, list) { + expect(err).not.to.exist; + expect(list.length).to.equal(2); + + var item0 = list[0]; + expect(item0.path).to.equal('file'); + expect(item0.links).to.equal(1); + expect(item0.size).to.equal(1); + expect(item0.modified).to.be.a('number'); + expect(item0.type).to.equal('FILE'); + expect(item0.contents).not.to.exist; + + var item1 = list[1]; + expect(item1.path).to.equal('file2'); + expect(item1.links).to.equal(1); + expect(item1.size).to.equal(2); + expect(item1.modified).to.be.a('number'); + expect(item1.type).to.equal('FILE'); + expect(item0.contents).not.to.exist; + + done(); + }); + }); + }); + }); + + it('should return the shallow contents of a dir tree', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + fs.mkdir('/dir/dir2', function(err) { + if(err) throw err; + + fs.writeFile('/dir/file', contents, function(err) { + if(err) throw err; + + fs.writeFile('/dir/file2', contents, function(err) { + if(err) throw err; + + shell.ls('/dir', function(err, list) { + expect(err).not.to.exist; + expect(list.length).to.equal(3); + + // We shouldn't rely on the order we'll get the listing + list.forEach(function(item, i, arr) { + switch(item.path) { + case 'dir2': + expect(item.links).to.equal(1); + expect(item.size).to.be.a('number'); + expect(item.modified).to.be.a('number'); + expect(item.type).to.equal('DIRECTORY'); + expect(item.contents).not.to.exist; + break; + case 'file': + case 'file2': + expect(item.links).to.equal(1); + expect(item.size).to.equal(1); + expect(item.modified).to.be.a('number'); + expect(item.type).to.equal('FILE'); + expect(item.contents).not.to.exist; + break; + default: + // shouldn't happen + expect(true).to.be.false; + break; + } + + if(i === arr.length -1) { + done(); + } + }); + }); + }); + }); + }); + }); + }); + + it('should return the deep contents of a dir tree', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + fs.mkdir('/dir/dir2', function(err) { + if(err) throw err; + + fs.writeFile('/dir/dir2/file', contents, function(err) { + if(err) throw err; + + fs.writeFile('/dir/file', contents, function(err) { + if(err) throw err; + + fs.writeFile('/dir/file2', contents, function(err) { + if(err) throw err; + + shell.ls('/dir', { recursive: true }, function(err, list) { + expect(err).not.to.exist; + expect(list.length).to.equal(3); + + // We shouldn't rely on the order we'll get the listing + list.forEach(function(item, i, arr) { + switch(item.path) { + case 'dir2': + expect(item.links).to.equal(1); + expect(item.size).to.be.a('number'); + expect(item.modified).to.be.a('number'); + expect(item.type).to.equal('DIRECTORY'); + expect(item.contents).to.exist; + expect(item.contents.length).to.equal(1); + var contents0 = item.contents[0]; + expect(contents0.path).to.equal('file'); + expect(contents0.links).to.equal(1); + expect(contents0.size).to.equal(1); + expect(contents0.modified).to.be.a('number'); + expect(contents0.type).to.equal('FILE'); + expect(contents0.contents).not.to.exist; + break; + case 'file': + case 'file2': + expect(item.links).to.equal(1); + expect(item.size).to.equal(1); + expect(item.modified).to.be.a('number'); + expect(item.type).to.equal('FILE'); + expect(item.contents).not.to.exist; + break; + default: + // shouldn't happen + expect(true).to.be.false; + break; + } + + if(i === arr.length -1) { + done(); + } + }); + }); + }); + }); + }); + }); + }); + }); + + }); +}); diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 32758c0..d0efa70 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -47,6 +47,7 @@ define([ "spec/shell/touch.spec", "spec/shell/exec.spec", "spec/shell/cat.spec", + "spec/shell/ls.spec", // Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test) "spec/node-js/simple/test-fs-mkdir",