diff --git a/README.md b/README.md index e754b5f3..d9c8760c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,31 @@ as the folder being deleted, or else the operation will fail. Synchronous form of `rimraf.windows()` +### Command Line Interface + +``` +Usage: rimraf [ ...] +Deletes all files and folders at "path", recursively. + +Options: + -- Treat all subsequent arguments as paths + -h --help Display this usage info + --preserve-root Do not remove '/' (default) + --no-preserve-root Do not treat '/' specially + + --impl= Specify the implementationt to use. + rimraf: choose the best option + native: the C++ implementation in Node.js + manual: the platform-specific JS implementation + posix: the Posix JS implementation + windows: the Windows JS implementation + +Implementation-specific options: + --tmp= Folder to hold temp files for 'windows' implementation + --max-retries= maxRetries for the 'native' implementation + --retry-delay= retryDelay for the 'native' implementation +``` + ## mkdirp If you need to _create_ a directory recursively, check out diff --git a/lib/bin.js b/lib/bin.js new file mode 100755 index 00000000..16433606 --- /dev/null +++ b/lib/bin.js @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +const rimraf = require('./index.js') + +const { version } = require('../package.json') + +const runHelpForUsage = () => + console.error('run `rimraf --help` for usage information') + +const help = `rimraf version ${version} + +Usage: rimraf [ ...] +Deletes all files and folders at "path", recursively. + +Options: + -- Treat all subsequent arguments as paths + -h --help Display this usage info + --preserve-root Do not remove '/' (default) + --no-preserve-root Do not treat '/' specially + + --impl= Specify the implementationt to use. + rimraf: choose the best option + native: the C++ implementation in Node.js + manual: the platform-specific JS implementation + posix: the Posix JS implementation + windows: the Windows JS implementation + +Implementation-specific options: + --tmp= Folder to hold temp files for 'windows' implementation + --max-retries= maxRetries for the 'native' implementation + --retry-delay= retryDelay for the 'native' implementation +` + +const { resolve, parse } = require('path') + +const main = async (...args) => { + if (process.env.__RIMRAF_TESTING_BIN_FAIL__ === '1') + throw new Error('simulated rimraf failure') + + const opts = {} + const paths = [] + let dashdash = false + let impl = rimraf + + for (const arg of args) { + if (dashdash) { + paths.push(arg) + continue + } + if (arg === '--') { + dashdash = true + continue + } else if (arg === '-h' || arg === '--help') { + console.log(help) + return 0 + } else if (arg === '--preserve-root') { + opts.preserveRoot = true + continue + } else if (arg === '--no-preserve-root') { + opts.preserveRoot = false + continue + } else if (/^--tmp=/.test(arg)) { + const val = arg.substr('--tmp='.length) + opts.tmp = val + continue + } else if (/^--max-retries=/.test(arg)) { + const val = +arg.substr('--max-retries='.length) + opts.maxRetries = val + continue + } else if (/^--retry-delay=/.test(arg)) { + const val = +arg.substr('--retry-delay='.length) + opts.retryDelay = val + continue + } else if (/^--impl=/.test(arg)) { + const val = arg.substr('--impl='.length) + switch (val) { + case 'rimraf': + impl = rimraf + continue + case 'native': + case 'manual': + case 'posix': + case 'windows': + impl = rimraf[val] + continue + default: + console.error(`unknown implementation: ${val}`) + runHelpForUsage() + return 1 + } + } else if (/^-/.test(arg)) { + console.error(`unknown option: ${arg}`) + runHelpForUsage() + return 1 + } else + paths.push(arg) + } + + if (opts.preserveRoot !== false) { + for (const path of paths.map(p => resolve(p))) { + if (path === parse(path).root) { + console.error(`rimraf: it is dangerous to operate recursively on '/'`) + console.error('use --no-preserve-root to override this failsafe') + return 1 + } + } + } + + if (!paths.length) { + console.error('rimraf: must provide a path to remove') + runHelpForUsage() + return 1 + } + + await impl(paths, opts) + return 0 +} + +module.exports = Object.assign(main, { help }) +if (module === require.main) { + const args = process.argv.slice(2) + main(...args).then(code => process.exit(code), er => { + console.error(er) + process.exit(1) + }) +} diff --git a/package-lock.json b/package-lock.json index 5990b09e..6801206c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "rimraf", "version": "4.0.0-0", "license": "ISC", + "bin": { + "rimraf": "lib/bin.js" + }, "devDependencies": { "@npmcli/lint": "^1.0.0", "mkdirp": "1", diff --git a/package.json b/package.json index 60804017..341ce792 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "rimraf", "version": "4.0.0-0", "main": "lib/index.js", + "bin": "lib/bin.js", "description": "A deep deletion module for node (like `rm -rf`)", "author": "Isaac Z. Schlueter (http://blog.izs.me/)", "license": "ISC", diff --git a/test/bin.js b/test/bin.js new file mode 100644 index 00000000..23b9b4da --- /dev/null +++ b/test/bin.js @@ -0,0 +1,191 @@ +const t = require('tap') + +t.test('basic arg parsing stuff', t => { + const LOGS = [] + const ERRS = [] + const { log: consoleLog, error: consoleError } = console + t.teardown(() => { + console.log = consoleLog + console.error = consoleError + }) + console.log = (...msg) => LOGS.push(msg) + console.error = (...msg) => ERRS.push(msg) + + const CALLS = [] + const rimraf = async (path, opt) => + CALLS.push(['rimraf', path, opt]) + const bin = t.mock('../lib/bin.js', { + '../lib/index.js': Object.assign(rimraf, { + native: async (path, opt) => + CALLS.push(['native', path, opt]), + manual: async (path, opt) => + CALLS.push(['manual', path, opt]), + posix: async (path, opt) => + CALLS.push(['posix', path, opt]), + windows: async (path, opt) => + CALLS.push(['windows', path, opt]), + }), + }) + + t.afterEach(() => { + LOGS.length = 0 + ERRS.length = 0 + CALLS.length = 0 + }) + + t.test('helpful output', t => { + const cases = [ + ['-h'], + ['--help'], + ['a', 'b', '--help', 'c'], + ] + for (const c of cases) { + t.test(c.join(' '), async t => { + t.equal(await bin(...c), 0) + t.same(LOGS, [[bin.help]]) + t.same(ERRS, []) + t.same(CALLS, []) + }) + } + t.end() + }) + + t.test('no paths', async t => { + t.equal(await bin(), 1) + t.same(LOGS, []) + t.same(ERRS, [ + ['rimraf: must provide a path to remove'], + ['run `rimraf --help` for usage information'], + ]) + }) + + t.test('dashdash', async t => { + t.equal(await bin('--', '-h'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['-h'], {}]]) + }) + + t.test('no preserve root', async t => { + t.equal(await bin('--no-preserve-root', 'foo'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['foo'], { preserveRoot: false }]]) + }) + t.test('yes preserve root', async t => { + t.equal(await bin('--preserve-root', 'foo'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['foo'], { preserveRoot: true }]]) + }) + t.test('yes preserve root, remove root', async t => { + t.equal(await bin('/'), 1) + t.same(LOGS, []) + t.same(ERRS, [ + [`rimraf: it is dangerous to operate recursively on '/'`], + ['use --no-preserve-root to override this failsafe'], + ]) + t.same(CALLS, []) + }) + t.test('no preserve root, remove root', async t => { + t.equal(await bin('/', '--no-preserve-root'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['/'], { preserveRoot: false }]]) + }) + + t.test('--tmp=', async t => { + t.equal(await bin('--tmp=some-path', 'foo'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['foo'], { tmp: 'some-path' }]]) + }) + + t.test('--max-retries=n', async t => { + t.equal(await bin('--max-retries=100', 'foo'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['foo'], { maxRetries: 100 }]]) + }) + + t.test('--retry-delay=n', async t => { + t.equal(await bin('--retry-delay=100', 'foo'), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [['rimraf', ['foo'], { retryDelay: 100 }]]) + }) + + t.test('--uknown-option', async t => { + t.equal(await bin('--unknown-option=100', 'foo'), 1) + t.same(LOGS, []) + t.same(ERRS, [ + ['unknown option: --unknown-option=100'], + ['run `rimraf --help` for usage information'], + ]) + t.same(CALLS, []) + }) + + t.test('--impl=asdf', async t => { + t.equal(await bin('--impl=asdf', 'foo'), 1) + t.same(LOGS, []) + t.same(ERRS, [ + ['unknown implementation: asdf'], + ['run `rimraf --help` for usage information'], + ]) + t.same(CALLS, []) + }) + + const impls = ['rimraf', 'native', 'manual', 'posix', 'windows'] + for (const impl of impls) { + t.test(`--impl=${impl}`, async t => { + t.equal(await bin('foo', `--impl=${impl}`), 0) + t.same(LOGS, []) + t.same(ERRS, []) + t.same(CALLS, [ + [impl, ['foo'], {}], + ]) + }) + } + + t.end() +}) + +t.test('actually delete something with it', async t => { + const path = t.testdir({ + a: { + b: { + c: '1', + }, + }, + }) + + const bin = require.resolve('../lib/bin.js') + const { spawnSync } = require('child_process') + const res = spawnSync(process.execPath, [bin, path]) + const { statSync } = require('fs') + t.throws(() => statSync(path)) + t.equal(res.status, 0) +}) + +t.test('print failure when impl throws', async t => { + const path = t.testdir({ + a: { + b: { + c: '1', + }, + }, + }) + + const bin = require.resolve('../lib/bin.js') + const { spawnSync } = require('child_process') + const res = spawnSync(process.execPath, [bin, path], { + env: { + ...process.env, + __RIMRAF_TESTING_BIN_FAIL__: '1', + }, + }) + const { statSync } = require('fs') + t.equal(statSync(path).isDirectory(), true) + t.equal(res.status, 1) + t.match(res.stderr.toString(), /^Error: simulated rimraf failure/) +})