safe-rm 1.0.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "safe-rm",
3
- "version": "1.0.8",
3
+ "version": "3.0.0",
4
4
  "description": "A much safer replacement of bash rm with nearly full functionalities and options of the rm command!",
5
5
  "bin": {
6
6
  "safe-rm": "./bin/rm.sh"
7
7
  },
8
8
  "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "test": "ava --timeout=10s --verbose",
10
+ "test:dev": "NODE_DEBUG=safe-rm npm run test",
11
+ "test:debug": "SAFE_RM_DEBUG=1 npm run test:dev",
12
+ "test:mock-linux": "SAFE_RM_DEBUG_LINUX=1 npm run test",
13
+ "test:mock-linux:debug": "SAFE_RM_DEBUG=1 SAFE_RM_DEBUG_LINUX=1 npm run test:dev",
14
+ "lint": "eslint .",
15
+ "fix": "eslint . --fix"
10
16
  },
11
17
  "repository": {
12
18
  "type": "git",
@@ -14,15 +20,32 @@
14
20
  },
15
21
  "keywords": [
16
22
  "safe-rm",
23
+ "cli",
17
24
  "rm",
18
25
  "linux",
19
26
  "alternative",
20
27
  "trash"
21
28
  ],
29
+ "ava": {
30
+ "files": [
31
+ "test/*.test.js"
32
+ ]
33
+ },
22
34
  "author": "kael",
23
35
  "license": "MIT",
24
36
  "bugs": {
25
37
  "url": "https://github.com/kaelzhang/shell-safe-rm/issues"
26
38
  },
27
- "homepage": "https://github.com/kaelzhang/shell-safe-rm#readme"
39
+ "homepage": "https://github.com/kaelzhang/shell-safe-rm#readme",
40
+ "devDependencies": {
41
+ "@ostai/eslint-config": "^4.0.0",
42
+ "ava": "^6.2.0",
43
+ "delay": "^5.0.0",
44
+ "eslint": "^8.57.0",
45
+ "eslint-plugin-import": "^2.31.0",
46
+ "fs-extra": "^11.2.0",
47
+ "home": "^2.0.0",
48
+ "tmp": "^0.2.3",
49
+ "uuid": "^11.0.3"
50
+ }
28
51
  }
package/test/cases.js ADDED
@@ -0,0 +1,275 @@
1
+ const path = require('path')
2
+ const {v4: uuid} = require('uuid')
3
+ const delay = require('delay')
4
+
5
+ const {
6
+ generateContextMethods,
7
+ assertEmptySuccess,
8
+ IS_MACOS
9
+ } = require('./helper')
10
+
11
+ const SAFE_RM_PATH = path.join(__dirname, '..', 'bin', 'rm.sh')
12
+
13
+ const is_rm = type => type === 'rm'
14
+ // const is_safe_rm = type => type === 'safe-rm'
15
+
16
+ // Whether it is a test spec for AppleScript version of safe-rm
17
+ const is_as = type => type === 'safe-rm-as'
18
+
19
+ // We skip testing trash dir for vanilla rm
20
+ const should_skip_test_trash_dir = type => is_rm(type) || is_as(type)
21
+
22
+ module.exports = (
23
+ test,
24
+ {
25
+ type,
26
+ // test_trash_dir,
27
+ command = SAFE_RM_PATH,
28
+ env = {}
29
+ } = {}
30
+ ) => {
31
+ // Setup before each test
32
+ test.beforeEach(generateContextMethods(command, env))
33
+
34
+ test(`removes a single file`, async t => {
35
+ const {
36
+ createFile,
37
+ runRm,
38
+ pathExists,
39
+ lsFileInTrash
40
+ } = t.context
41
+
42
+ const filepath = await createFile()
43
+ const result = await runRm([filepath])
44
+
45
+ t.is(result.code, 0, 'exit code should be 0')
46
+ t.false(await pathExists(filepath), 'file should be removed')
47
+
48
+ if (should_skip_test_trash_dir(type)) {
49
+ return
50
+ }
51
+
52
+ const files = await lsFileInTrash(filepath)
53
+
54
+ t.is(files.length, 1, 'should be one in the trash')
55
+ t.is(
56
+ path.basename(files[0]),
57
+ path.basename(filepath),
58
+ 'file name should match'
59
+ )
60
+ })
61
+
62
+ const EXTs = [
63
+ '',
64
+ '.jpg'
65
+ ]
66
+
67
+ EXTs.forEach(ext => {
68
+ const extra = ext
69
+ ? ` with ext "${ext}"`
70
+ : ''
71
+
72
+ test(`removes multiple files of the same name${extra}`, async t => {
73
+ const {
74
+ createFile,
75
+ runRm,
76
+ pathExists,
77
+ lsFileInTrash
78
+ } = t.context
79
+
80
+ const filename = uuid()
81
+ const full_name = filename + ext
82
+
83
+ const now = Date.now()
84
+ const to_next_second = 1000 - now % 1000
85
+ await delay(to_next_second)
86
+
87
+ const filepath1 = await createFile({name: full_name, content: '1'})
88
+ const result1 = await runRm([filepath1])
89
+
90
+ const filepath2 = await createFile({name: full_name, content: '2'})
91
+ const result2 = await runRm([filepath2])
92
+
93
+ const filepath3 = await createFile({name: full_name, content: '3'})
94
+ const result3 = await runRm([filepath3])
95
+
96
+ assertEmptySuccess(t, result1)
97
+ t.false(await pathExists(filepath1), 'file 1 should be removed')
98
+
99
+ assertEmptySuccess(t, result2)
100
+ t.false(await pathExists(filepath2), 'file 2 should be removed')
101
+
102
+ assertEmptySuccess(t, result3)
103
+ t.false(await pathExists(filepath3), 'file 3 should be removed')
104
+
105
+ if (should_skip_test_trash_dir(type)) {
106
+ return
107
+ }
108
+
109
+ const files = (await lsFileInTrash(full_name))
110
+ .sort((a, b) => a.length - b.length)
111
+
112
+ if (IS_MACOS) {
113
+ // /path/to/foo[.jpg]
114
+ // /path/to/foo 12.58.23[.jpg]
115
+ // /path/to/foo 12.58.23 12.58.23[.jpg]
116
+ const [f1, f2, f3] = files
117
+
118
+ const [fb1, fb2, fb3] = [f1, f2, f3].map(
119
+ f => {
120
+ const base = path.basename(f)
121
+
122
+ return base.slice(0, base.length - ext.length)
123
+ }
124
+ )
125
+
126
+ const [fbs1, fbs2, fbs3] = [fb1, fb2, fb3].map(f => f.split(' '))
127
+
128
+ const time = fbs2[1]
129
+
130
+ t.true(files.every(f => f.endsWith(ext)), 'should have the same ext')
131
+
132
+ t.is(fb1, filename)
133
+ t.is(fbs1[0], filename)
134
+ t.is(fbs2[0], filename)
135
+ t.is(fbs3[0], filename)
136
+
137
+ t.is(fbs3[1], time)
138
+ t.is(fbs3[2], time)
139
+ } else {
140
+ // /path/to/foo[.jpg]
141
+ // /path/to/foo[.jpg].1
142
+ // /path/to/foo[.jpg].2
143
+ const nums = []
144
+ const bases = []
145
+ const re = /(\.(\d+))?$/
146
+
147
+ for (const file of files) {
148
+ const match = file.match(re)
149
+ const n = match[2]
150
+ ? parseInt(match[2], 10)
151
+ : 0
152
+
153
+ const f = file.slice(0, match.index)
154
+
155
+ nums.push(n)
156
+ bases.push(f)
157
+ }
158
+
159
+ t.true(bases.every(f => f.endsWith(ext)), 'should have the same ext')
160
+ t.is(nums[0], 0)
161
+ t.is(nums[1], 1)
162
+ t.is(nums[2], 2)
163
+ }
164
+ })
165
+ })
166
+
167
+ test(`removes a single file in trash permanently`, async t => {
168
+ const {
169
+ trash_path,
170
+ createFile,
171
+ runRm,
172
+ pathExists,
173
+ lsFileInTrash
174
+ } = t.context
175
+
176
+ const filepath = await createFile({name: path.join(trash_path, uuid())})
177
+ const result = await runRm([filepath], {
178
+ env: {
179
+ SAFE_RM_PERM_DEL_FILES_IN_TRASH: 'yes'
180
+ }
181
+ })
182
+
183
+ assertEmptySuccess(t, result)
184
+ t.false(await pathExists(filepath), 'file should be removed')
185
+
186
+ if (should_skip_test_trash_dir(type)) {
187
+ return
188
+ }
189
+
190
+ const files = await lsFileInTrash(filepath)
191
+
192
+ t.is(files.length, 0, 'should be already removed')
193
+ })
194
+
195
+ test(`#22 exit code with -f option`, async t => {
196
+ const {
197
+ source_path,
198
+ runRm
199
+ } = t.context
200
+
201
+ const filepath = path.join(source_path, uuid())
202
+ const result = await runRm(['-f', filepath])
203
+
204
+ assertEmptySuccess(t, result, ', if rm -f a non-existing file')
205
+
206
+ const result_no_f = await runRm([filepath])
207
+
208
+ t.is(result_no_f.code, 1, 'exit code should be 1 without -f')
209
+ t.is(result_no_f.stdout, '', 'stdout should be empty')
210
+
211
+ t.true(
212
+ // The stderr of different rm distributions may vary:
213
+ // - Linux rm: "rm: cannot remove 'nonexistent.txt': No such file or directory"
214
+ // - Mac rm: "rm: nonexistent.txt: No such file or directory"
215
+ // So we just check if the stderr includes "No such file or directory"
216
+ result_no_f.stderr.includes('No such file or directory'), `stderr should include "No such file or directory": ${
217
+ result_no_f.stderr
218
+ }`)
219
+ })
220
+
221
+ test(`removes an empty directory: -d`, async t => {
222
+ const {
223
+ createDir,
224
+ runRm,
225
+ pathExists
226
+ } = t.context
227
+
228
+ const dirpath = await createDir()
229
+ const result1 = await runRm([dirpath])
230
+
231
+ t.is(result1.code, 1, 'exit code should be 1')
232
+ t.true(result1.stderr.includes('a directory'), 'stderr should include "a directory"')
233
+
234
+ const result2 = await runRm(['-d', dirpath])
235
+
236
+ assertEmptySuccess(t, result2)
237
+ t.false(await pathExists(dirpath), 'directory should be removed')
238
+ })
239
+
240
+ // Only test for safe-rm
241
+ !is_rm(type) && test(`protected rules`, async t => {
242
+ const {
243
+ createDir,
244
+ createFile,
245
+ runRm,
246
+ pathExists
247
+ } = t.context
248
+
249
+ const config_root = await createDir({
250
+ name: '.safe-rm'
251
+ })
252
+
253
+ const dir = await createDir()
254
+ const filepath = await createFile({
255
+ under: dir
256
+ })
257
+
258
+ await createFile({
259
+ name: '.gitignore',
260
+ under: config_root,
261
+ content: `${dir}
262
+ `
263
+ })
264
+
265
+ const result = await runRm([filepath], {
266
+ env: {
267
+ SAFE_RM_CONFIG_ROOT: config_root
268
+ }
269
+ })
270
+
271
+ t.is(result.code, 1, 'exit code should be 1')
272
+ t.true(result.stderr.includes('protected'), 'stderr should include "protected"')
273
+ t.true(await pathExists(filepath), 'file should not be removed')
274
+ })
275
+ }
package/test/helper.js ADDED
@@ -0,0 +1,178 @@
1
+ const path = require('path')
2
+ const fs = require('fs').promises
3
+ const {spawn} = require('child_process')
4
+ const tmp = require('tmp')
5
+ const fse = require('fs-extra')
6
+ const {v4: uuid} = require('uuid')
7
+ const log = require('util').debuglog('safe-rm')
8
+
9
+ const TEST_DIR = path.join(tmp.dirSync().name, 'safe-rm-tests')
10
+
11
+ const IS_MACOS = process.platform === 'darwin'
12
+ // For linux mock testing
13
+ && !process.env.SAFE_RM_DEBUG_LINUX
14
+
15
+ const generateContextMethods = (
16
+ rm_command,
17
+ rm_command_env
18
+ ) => async t => {
19
+ const root_path = path.join(TEST_DIR, uuid())
20
+ t.context.root = await fse.ensureDir(root_path)
21
+
22
+ const source_path = t.context.source_path = path.join(root_path, 'source')
23
+ const trash = rm_command_env.SAFE_RM_TRASH
24
+ ? rm_command_env.SAFE_RM_TRASH
25
+ : path.join(root_path, 'trash')
26
+
27
+ t.context.trash_path = process.platform === 'darwin'
28
+ ? trash
29
+ : path.join(trash, 'files')
30
+
31
+ await Promise.all([
32
+ fse.ensureDir(source_path),
33
+ fse.ensureDir(t.context.trash_path)
34
+ ])
35
+
36
+ // Helper function to create a temporary directory
37
+ async function createDir ({
38
+ name = uuid(),
39
+ under
40
+ } = {}) {
41
+ const dirpath = path.resolve(under || t.context.source_path, name)
42
+ await fse.ensureDir(dirpath)
43
+
44
+ return dirpath
45
+ }
46
+
47
+ // Helper function to create a temporary file
48
+ async function createFile ({
49
+ name = uuid(),
50
+ content = 'test content',
51
+ under
52
+ } = {}) {
53
+ const filepath = path.resolve(under || t.context.source_path, name)
54
+ await fs.writeFile(filepath, content)
55
+
56
+ return filepath
57
+ }
58
+
59
+ // Helper function to run rm commands
60
+ function runRm (args, {
61
+ input = '',
62
+ command = rm_command,
63
+ env: arg_env = {}
64
+ } = {}) {
65
+ return new Promise(resolve => {
66
+ const env = {
67
+ // ...process.env,
68
+ ...{
69
+ SAFE_RM_TRASH: t.context.trash_path
70
+ },
71
+ ...rm_command_env,
72
+ ...arg_env
73
+ }
74
+
75
+ const child = spawn(command, args, {
76
+ env
77
+ })
78
+ let stdout = ''
79
+ let stderr = ''
80
+
81
+ child.stdout.on('data', data => {
82
+ stdout += data.toString()
83
+ })
84
+
85
+ child.stderr.on('data', data => {
86
+ stderr += data.toString()
87
+ })
88
+
89
+ if (input) {
90
+ if (!child.stdin) {
91
+ throw new Error('Child process does not support stdin')
92
+ }
93
+
94
+ child.stdin.write(input)
95
+ child.stdin.end()
96
+ }
97
+
98
+ child.on('close', code => {
99
+ const resolved = {
100
+ code,
101
+ stdout,
102
+ stderr
103
+ }
104
+
105
+ log(command, 'result:', resolved)
106
+
107
+ resolve(resolved)
108
+ })
109
+ })
110
+ }
111
+
112
+ // Helper function to check if path exists
113
+ async function pathExists (filepath) {
114
+ const realpath = path.resolve(t.context.source_path, filepath)
115
+
116
+ try {
117
+ await fs.access(realpath)
118
+ return true
119
+ } catch (e) {
120
+ return false
121
+ }
122
+ }
123
+
124
+ async function lsFileInMacTrash (filepath) {
125
+ const {trash_path} = t.context
126
+
127
+ const files = await fs.readdir(trash_path)
128
+
129
+ const _filename = path.basename(filepath)
130
+ const ext = path.extname(_filename)
131
+ const filename = path.basename(_filename, ext)
132
+
133
+ const filtered = files.filter(
134
+ f => f.endsWith(ext) && f.startsWith(filename)
135
+ ).map(f => path.join(trash_path, f))
136
+
137
+ return filtered
138
+ }
139
+
140
+ async function lsFileInLinuxTrash (filepath) {
141
+ const {trash_path: _trash_path} = t.context
142
+ const trash_path = path.join(_trash_path, 'files')
143
+
144
+ const filename = path.basename(filepath)
145
+
146
+ const files = await fs.readdir(trash_path)
147
+
148
+ return files
149
+ .filter(f => f.startsWith(filename))
150
+ .map(f => path.join(trash_path, f))
151
+ }
152
+
153
+ async function lsFileInTrash (filepath) {
154
+ return IS_MACOS
155
+ ? lsFileInMacTrash(filepath)
156
+ : lsFileInLinuxTrash(filepath)
157
+ }
158
+
159
+ Object.assign(t.context, {
160
+ createDir,
161
+ createFile,
162
+ runRm,
163
+ pathExists,
164
+ lsFileInTrash
165
+ })
166
+ }
167
+
168
+ const assertEmptySuccess = (t, result, a = '', b = '', c = '') => {
169
+ t.is(result.code, 0, `exit code should be 0${a}`)
170
+ t.is(result.stdout, '', `stdout should be empty${b}`)
171
+ t.is(result.stderr, '', `stderr should be empty${c}`)
172
+ }
173
+
174
+ module.exports = {
175
+ generateContextMethods,
176
+ assertEmptySuccess,
177
+ IS_MACOS
178
+ }
@@ -0,0 +1,8 @@
1
+ const test = require('ava')
2
+
3
+ const run = require('./cases')
4
+
5
+ run(test, {
6
+ type: 'rm',
7
+ command: '/bin/rm'
8
+ })
@@ -0,0 +1,11 @@
1
+ const test = require('ava')
2
+ const home = require('home')
3
+
4
+ const run = require('./cases')
5
+
6
+ run(test, {
7
+ type: 'safe-rm-as',
8
+ env: {
9
+ SAFE_RM_TRASH: home.resolve('~/.Trash')
10
+ }
11
+ })
@@ -0,0 +1,7 @@
1
+ const test = require('ava')
2
+
3
+ const run = require('./cases')
4
+
5
+ run(test, {
6
+ type: 'safe-rm'
7
+ })
package/test/argument.sh DELETED
@@ -1,5 +0,0 @@
1
- #!/bin/bash
2
-
3
- n=1
4
-
5
- echo ${!n}
package/test/fold.sh DELETED
@@ -1,39 +0,0 @@
1
- #!/bin/bash
2
-
3
- n=-abcde
4
-
5
- split_echo(){
6
- split=`echo $1 | fold -w1`
7
-
8
- local s
9
-
10
- for s in ${split[@]}
11
- do
12
- echo $s
13
- done
14
- }
15
-
16
- split_echo $n
17
-
18
- echo $s
19
-
20
- cut_echo(){
21
- echo "remove leading char ${1:1}"
22
- }
23
-
24
- cut_echo abcdefg
25
-
26
-
27
- match(){
28
- if [[ ${1:0:1} =~ [aA] ]]; then
29
- echo start with a
30
- else
31
- echo no
32
- fi
33
- }
34
-
35
- match abc
36
-
37
- match Acbdfd
38
- match ddd
39
-
package/test/for.sh DELETED
@@ -1,22 +0,0 @@
1
- #!/bin/bash
2
-
3
-
4
- print(){
5
- for path in $1/*
6
- do
7
- echo $path
8
- done
9
- }
10
-
11
-
12
- print $1
13
-
14
- n=
15
- a=2
16
-
17
- [[ $n ]] && echo "n set" || {
18
- echo "n not set"
19
- a=1
20
- }
21
-
22
- echo $a
@@ -1,72 +0,0 @@
1
- #!/bin/bash
2
-
3
-
4
- print(){
5
- case $1 in
6
- 1[0-9][0-9]*)
7
- echo 1
8
- ;;
9
-
10
- 1[0-9]*)
11
- echo 3
12
- ;;
13
-
14
- 1)
15
- echo 4
16
- ;;
17
-
18
- *)
19
- echo 5
20
- ;;
21
- esac
22
- }
23
-
24
- print_2(){
25
- case $1 in
26
- 1[0-9]+)
27
- echo 1
28
- ;;
29
-
30
- 1[0-9]*)
31
- echo 2
32
- ;;
33
- *)
34
- echo 3
35
- ;;
36
- esac
37
- }
38
-
39
- print_3(){
40
- case $1 in
41
- .|..)
42
- echo 1
43
- ;;
44
-
45
- a)
46
- echo 2
47
- ;;
48
-
49
- aa)
50
- echo 3
51
- ;;
52
- esac
53
- }
54
-
55
- print 1234
56
- print 123
57
- print 12
58
- print 1
59
-
60
- echo ---------
61
-
62
- print_2 1234
63
- print_2 123
64
- print_2 12
65
- print_2 1
66
-
67
- echo ---------
68
-
69
- print_3 a
70
- print_3 aa
71
- print_3 ..
72
- print_3 .