Skip to content

Commit

Permalink
add cli script
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacs committed Jan 10, 2023
1 parent 0c82d74 commit d4eec2e
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 0 deletions.
25 changes: 25 additions & 0 deletions README.md
Expand Up @@ -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 <path> [<path> ...]
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=<type> 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=<path> Folder to hold temp files for 'windows' implementation
--max-retries=<n> maxRetries for the 'native' implementation
--retry-delay=<n> retryDelay for the 'native' implementation
```

## mkdirp

If you need to _create_ a directory recursively, check out
Expand Down
126 changes: 126 additions & 0 deletions 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 <path> [<path> ...]
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=<type> 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=<path> Folder to hold temp files for 'windows' implementation
--max-retries=<n> maxRetries for the 'native' implementation
--retry-delay=<n> 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)
})
}
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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 <i@izs.me> (http://blog.izs.me/)",
"license": "ISC",
Expand Down
191 changes: 191 additions & 0 deletions 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=<path>', 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/)
})

0 comments on commit d4eec2e

Please sign in to comment.