diff --git a/README.md b/README.md
index c2388cb..1e7d439 100644
--- a/README.md
+++ b/README.md
@@ -1109,6 +1109,7 @@ var sh = fs.Shell();
* [sh.cat(files, callback)](#cat)
* [sh.rm(path, [options], callback)](#rm)
* [sh.tempDir(callback)](#tempDir)
+* [sh.mkdirp(path, callback)](#mkdirp)
#### sh.cd(path, callback)
@@ -1298,3 +1299,19 @@ sh.tempDir(function(err, tmp) {
// tmp is now '/temporary', and /temporary exists
});
```
+
+#### sh.mkdirp(callback)
+
+Recursively creates the directory at the provided path. If the
+directory already exists, no error is returned. All parents must
+be valid directories (not files).
+
+Example:
+
+```javascript
+// Default empty filesystem
+sh.mkdirp('/test/mkdirp', function(err) {
+ if(err) throw err;
+ // the root '/' now contains a directory 'test' containing the directory 'mkdirp'
+});
+```
diff --git a/src/shell.js b/src/shell.js
index 2de68ee..0beb865 100644
--- a/src/shell.js
+++ b/src/shell.js
@@ -353,6 +353,75 @@ define(function(require) {
});
};
+ /**
+ * Recursively creates the directory at `path`. If the parent
+ * of `path` does not exist, it will be created.
+ * Based off EnsureDir by Sam X. Xu
+ * https://www.npmjs.org/package/ensureDir
+ * MIT License
+ */
+ Shell.prototype.mkdirp = function(path, callback) {
+ var fs = this.fs;
+ callback = callback || function(){};
+
+ if(!path) {
+ callback(new Errors.EInvalid());
+ return;
+ }
+ else if (path === '/'){
+ callback();
+ return;
+ }
+ function _mkdirp(path, callback){
+ fs.stat(path, function (err, stat) {
+ //doesn't exist
+ if (err && err.code === 'ENOENT') {
+ var parent = Path.dirname(path);
+ if(parent === '/'){ //parent is root
+ fs.mkdir(path, function (err) {
+ if (err && err.code != 'EEXIST') {
+ callback(err);
+ return;
+ }
+ callback();
+ return;
+ });
+ }
+ else { //parent is not root, make parent first
+ _mkdirp(parent, function (err) {
+ if (err) return callback(err);
+ fs.mkdir(path, function (err) { //then make dir
+ if (err && err.code != 'EEXIST') {
+ callback(err);
+ return;
+ }
+ callback();
+ return;
+ });
+ });
+ }
+ }
+ //other error
+ else if (err){
+ callback(err);
+ return;
+ }
+ //already exists
+ else if (stat.type === 'DIRECTORY') {
+ callback();
+ return;
+ }
+ //not a directory
+ else if (stat.type === 'FILE') {
+ callback(new Errors.ENotDirectory());
+ return;
+ }
+ });
+ }
+
+ _mkdirp(path, callback);
+ };
+
return Shell;
});
diff --git a/tests/spec/shell/mkdirp.spec.js b/tests/spec/shell/mkdirp.spec.js
new file mode 100644
index 0000000..afca2f7
--- /dev/null
+++ b/tests/spec/shell/mkdirp.spec.js
@@ -0,0 +1,97 @@
+define(["Filer", "util"], function(Filer, util) {
+
+ describe('FileSystemShell.mkdirp', function() {
+ beforeEach(util.setup);
+ afterEach(util.cleanup);
+
+ it('should be a function', function() {
+ var shell = util.shell();
+ expect(shell.mkdirp).to.be.a('function');
+ });
+
+ it('should fail without a path provided', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+
+ shell.mkdirp(null, function(err) {
+ expect(err).to.exist;
+ expect(err.code).to.equal('EINVAL');
+ done();
+ });
+ });
+
+ it('should succeed if provided path is root', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+ shell.mkdirp('/', function(err) {
+ expect(err).to.not.exist;
+ done();
+ });
+ });
+
+ it('should succeed if the directory exists', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+ fs.mkdir('/test', function(err){
+ expect(err).to.not.exist;
+ shell.mkdirp('/test',function(err) {
+ expect(err).to.not.exist;
+ done();
+ });
+ });
+ });
+
+ it('fail if a file name is provided', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+ fs.writeFile('/test.txt', 'test', function(err){
+ expect(err).to.not.exist;
+ shell.mkdirp('/test.txt', function(err) {
+ expect(err).to.exist;
+ expect(err.code).to.equal('ENOTDIR');
+ done();
+ });
+ });
+ });
+
+ it('should succeed on a folder on root (\'/test\')', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+ shell.mkdirp('/test', function(err) {
+ expect(err).to.not.exist;
+ fs.exists('/test', function(dir){
+ expect(dir).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it('should succeed on a folder with a nonexistant parent (\'/test/test\')', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+ shell.mkdirp('/test/test', function(err) {
+ expect(err).to.not.exist;
+ fs.exists('/test', function(dir1){
+ expect(dir1).to.be.true;
+ fs.exists('/test/test', function(dir2){
+ expect(dir2).to.be.true;
+ done();
+ });
+ });
+ });
+ });
+
+ it('should fail on a folder with a file for its parent (\'/test.txt/test\')', function(done) {
+ var fs = util.fs();
+ var shell = fs.Shell();
+ fs.writeFile('/test.txt', 'test', function(err){
+ expect(err).to.not.exist;
+ shell.mkdirp('/test.txt/test', function(err) {
+ expect(err).to.exist;
+ expect(err.code).to.equal('ENOTDIR');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/tests/test-manifest.js b/tests/test-manifest.js
index 5e99bf1..85fc016 100644
--- a/tests/test-manifest.js
+++ b/tests/test-manifest.js
@@ -55,6 +55,7 @@ define([
"spec/shell/ls.spec",
"spec/shell/rm.spec",
"spec/shell/env.spec",
+ "spec/shell/mkdirp.spec",
// Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test)
"spec/node-js/simple/test-fs-mkdir",