safe-rm 3.1.1 → 3.1.3

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/bin/rm.sh CHANGED
@@ -43,7 +43,7 @@ debug(){
43
43
 
44
44
 
45
45
  error(){
46
- echo $@ >&2
46
+ echo "$@" >&2
47
47
  }
48
48
 
49
49
 
@@ -389,6 +389,8 @@ remove(){
389
389
  read answer
390
390
  if [[ ${answer:0:1} =~ [yY] ]]; then
391
391
  [[ $(ls -A "$file") ]] && {
392
+ debug "$LINENO: $file: Directory not empty: $(ls -A "$file")"
393
+
392
394
  echo "$COMMAND: $file: Directory not empty"
393
395
 
394
396
  return 1
@@ -427,15 +429,24 @@ remove(){
427
429
 
428
430
 
429
431
  recursive_remove(){
432
+ local dir=$1
430
433
  local path
431
-
432
- # use `ls -A` instead of `for` to list hidden files.
433
- # and `for $1/*` is also weird if `$1` is neithor a dir nor existing that will print "$1/*" directly and rudely.
434
- # never use `find $1`, for the searching order is not what we want
435
- local list=$(ls -A "$1")
436
-
437
- [[ -n $list ]] && for path in "$list"; do
438
- remove "$1/$path"
434
+ local restore_nullglob=$(shopt -p nullglob)
435
+ local restore_dotglob=$(shopt -p dotglob)
436
+
437
+ # Avoid `ls -A` + unquoted `for` iteration, which splits names by IFS
438
+ # and breaks paths containing spaces (e.g. "a b" -> "a" + "b").
439
+ # Use glob expansion with arrays to keep each entry as one element.
440
+ # dotglob includes hidden files, nullglob avoids a literal "$dir/*" token.
441
+ shopt -s nullglob dotglob
442
+ local list=("$dir"/*)
443
+ eval "$restore_nullglob"
444
+ eval "$restore_dotglob"
445
+
446
+ for path in "${list[@]}"; do
447
+ debug "$LINENO: recursively remove: $path"
448
+
449
+ remove "$path"
439
450
  done
440
451
  }
441
452
 
@@ -517,12 +528,19 @@ applescript_trash(){
517
528
 
518
529
  [[ "$OPT_VERBOSE" == 1 ]] && list_files "$target"
519
530
 
531
+ # #47: Finder alias resolves symlinks to their targets.
532
+ # For symbolic links, fallback to `mv`-based trash to remove the link itself.
533
+ if [[ -L "$target" ]]; then
534
+ debug "$LINENO: symlink detected, fallback to mac_trash: $target"
535
+ mac_trash "$target"
536
+ return $?
537
+ fi
538
+
520
539
  debug "$LINENO: osascript delete $target"
521
540
 
522
541
  osascript -e "tell application \"Finder\" to delete (POSIX file \"$target\" as alias)" &> /dev/null
523
542
 
524
- # TODO: handle osascript errors
525
- return 0
543
+ return $?
526
544
  }
527
545
 
528
546
 
@@ -606,11 +624,12 @@ mac_trash(){
606
624
 
607
625
  debug "$LINENO: mv $move to $trash_path"
608
626
  mv "$move" "$trash_path"
627
+ local status=$?
609
628
 
610
- [[ "$_traveled" == 1 ]] && cd $__DIRNAME &> /dev/null
629
+ [[ "$_traveled" == 1 ]] && cd "$__DIRNAME" &> /dev/null
611
630
 
612
- # default status
613
- return 0
631
+ # Propagate mv status; otherwise callers may treat move failures as success.
632
+ return $status
614
633
  }
615
634
 
616
635
 
@@ -630,7 +649,7 @@ check_linux_trash_base(){
630
649
  local num=
631
650
 
632
651
  while IFS= read -r file; do
633
- if [[ $file =~ ${filename}\.([0-9]+)$ ]]; then
652
+ if [[ $file =~ ${base}\.([0-9]+)$ ]]; then
634
653
  # Remove leading zeros and make sure the number is in base 10
635
654
  num=$((10#${BASH_REMATCH[1]}))
636
655
  if ((num > max_n)); then
@@ -664,6 +683,13 @@ linux_trash(){
664
683
  # Move the target into the trash
665
684
  debug "$LINENO: mv $move to $trash_path"
666
685
  mv "$move" "$trash_path"
686
+ local move_status=$?
687
+
688
+ if [[ $move_status -ne 0 ]]; then
689
+ # Keep failure visible to remove()/EXIT_CODE and skip writing .trashinfo.
690
+ [[ "$_traveled" == 1 ]] && cd "$__DIRNAME" &> /dev/null
691
+ return $move_status
692
+ fi
667
693
 
668
694
  # Save linux trash info
669
695
  local info_path="$SAFE_RM_TRASH/info/$base.trashinfo"
@@ -673,10 +699,11 @@ linux_trash(){
673
699
  Path=$move
674
700
  DeletionDate=$trash_time
675
701
  EOF
702
+ local info_status=$?
676
703
 
677
- [[ "$_traveled" == 1 ]] && cd $__DIRNAME &> /dev/null
704
+ [[ "$_traveled" == 1 ]] && cd "$__DIRNAME" &> /dev/null
678
705
 
679
- return 0
706
+ return $info_status
680
707
  }
681
708
 
682
709
 
@@ -685,15 +712,22 @@ EOF
685
712
  # 'coz `find` act a inward searching, unlike rm -v
686
713
  list_files(){
687
714
  if [[ -d "$1" ]]; then
688
- local list=$(ls -A "$1")
715
+ local restore_nullglob=$(shopt -p nullglob)
716
+ local restore_dotglob=$(shopt -p dotglob)
717
+ # Keep traversal behavior aligned with recursive_remove(): no word splitting
718
+ # for names with spaces, and include dotfiles for rm -v output.
719
+ shopt -s nullglob dotglob
720
+ local list=("$1"/*)
721
+ eval "$restore_nullglob"
722
+ eval "$restore_dotglob"
689
723
  local f
690
724
 
691
- [[ -n $list ]] && for f in "$list"; do
692
- list_files "$1/$f"
725
+ for f in "${list[@]}"; do
726
+ list_files "$f"
693
727
  done
694
728
  fi
695
729
 
696
- echo $1
730
+ echo "$1"
697
731
  }
698
732
 
699
733
 
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "safe-rm",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
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": "ava --timeout=10s --verbose",
9
+ "test:rm": "ava --timeout=10s --verbose test/safe-rm.test.js",
10
+ "test:as": "ava --timeout=10s --verbose test/safe-rm-as.test.js",
11
+ "test": "npm run test:rm && npm run test:as",
10
12
  "test:dev": "NODE_DEBUG=safe-rm npm run test",
11
13
  "test:debug": "SAFE_RM_DEBUG=1 npm run test:dev",
12
14
  "test:mock-linux": "SAFE_RM_DEBUG_LINUX=1 npm run test",
package/test/cases.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const path = require('path')
2
+ const fs = require('fs').promises
2
3
  const {v4: uuid} = require('uuid')
3
4
  const delay = require('delay')
4
5
 
@@ -272,4 +273,178 @@ module.exports = (
272
273
  t.true(result.stderr.includes('protected'), 'stderr should include "protected"')
273
274
  t.true(await pathExists(filepath), 'file should not be removed')
274
275
  })
276
+
277
+ !is_as(type) && test(`#44: recursively and interactively`, async t => {
278
+ const {
279
+ createDir,
280
+ createFile,
281
+ runRm,
282
+ pathExists
283
+ } = t.context
284
+
285
+ const name = 'foo'
286
+
287
+ const dir = await createDir({
288
+ name
289
+ })
290
+
291
+ const fileA = await createFile({
292
+ name: 'a',
293
+ under: dir
294
+ })
295
+
296
+ const fileB = await createFile({
297
+ name: 'b',
298
+ under: dir
299
+ })
300
+
301
+ const result = await runRm(['-ri', dir], {
302
+ input: ['y', 'y', 'y', 'y']
303
+ })
304
+
305
+ t.is(result.stderr, '', 'stderr should be empty')
306
+ t.is(result.code, 0, 'exit code should be 0')
307
+ t.is(
308
+ result.stdout,
309
+ `examine files in directory ${dir}? y
310
+ remove ${fileA}? y
311
+ remove ${fileB}? y
312
+ remove ${dir}? y
313
+ `,
314
+ 'stdout does not match'
315
+ )
316
+
317
+ t.false(await pathExists(dir), 'directory should be removed')
318
+ })
319
+
320
+ !is_as(type) && test(`recursively and interactively with spaces`, async t => {
321
+ // Regression guard: recursive interactive remove must keep "a b"
322
+ // as one path and still remove the directory successfully.
323
+ const {
324
+ createDir,
325
+ createFile,
326
+ runRm,
327
+ pathExists
328
+ } = t.context
329
+
330
+ const dir = await createDir({
331
+ name: 'foo'
332
+ })
333
+
334
+ const fileA = await createFile({
335
+ name: 'a b',
336
+ under: dir
337
+ })
338
+
339
+ const fileB = await createFile({
340
+ name: 'c',
341
+ under: dir
342
+ })
343
+
344
+ const result = await runRm(['-ri', dir], {
345
+ input: ['y', 'y', 'y', 'y']
346
+ })
347
+
348
+ t.is(result.stderr, '', 'stderr should be empty')
349
+ t.is(result.code, 0, 'exit code should be 0')
350
+ t.is(
351
+ result.stdout,
352
+ `examine files in directory ${dir}? y
353
+ remove ${fileA}? y
354
+ remove ${fileB}? y
355
+ remove ${dir}? y
356
+ `,
357
+ 'stdout does not match'
358
+ )
359
+
360
+ t.false(await pathExists(dir), 'directory should be removed')
361
+ })
362
+
363
+ !is_as(type) && test(`#47: removes symlink itself instead of target`, async t => {
364
+ const {
365
+ createFile,
366
+ runRm,
367
+ pathExists
368
+ } = t.context
369
+
370
+ const target = await createFile({
371
+ name: 'file1'
372
+ })
373
+
374
+ const link = path.join(path.dirname(target), 'file1_sym')
375
+ await fs.symlink(target, link)
376
+
377
+ const result = await runRm([link])
378
+
379
+ t.is(result.code, 0, 'exit code should be 0')
380
+ t.false(await pathExists(link), 'symlink should be removed')
381
+ t.true(await pathExists(target), 'target file should remain')
382
+ })
383
+
384
+ !is_as(type) && test(`#47: applescript path should also remove symlink itself`, async t => {
385
+ const {
386
+ root,
387
+ createDir,
388
+ createFile,
389
+ runRm,
390
+ pathExists
391
+ } = t.context
392
+
393
+ const home = await createDir({
394
+ name: 'home',
395
+ under: root
396
+ })
397
+
398
+ await createDir({
399
+ name: '.Trash',
400
+ under: home
401
+ })
402
+
403
+ const mockBin = await createDir({
404
+ name: 'mock-bin',
405
+ under: root
406
+ })
407
+
408
+ // Simulate Finder alias behavior: if safe-rm calls osascript with a symlink,
409
+ // this mock deletes the resolved target. safe-rm should bypass this for symlinks.
410
+ const mockOsa = await createFile({
411
+ name: 'osascript',
412
+ under: mockBin,
413
+ content: `#!/usr/bin/env bash
414
+ expr=$2
415
+ target=$(printf '%s' "$expr" | cut -d '"' -f2)
416
+
417
+ if [[ -L "$target" ]]; then
418
+ resolved=$(readlink "$target")
419
+ if [[ "$resolved" != /* ]]; then
420
+ resolved="$(cd "$(dirname "$target")" && pwd)/$resolved"
421
+ fi
422
+ /bin/rm -f "$resolved"
423
+ else
424
+ /bin/rm -rf "$target"
425
+ fi
426
+ `
427
+ })
428
+
429
+ await fs.chmod(mockOsa, 0o755)
430
+
431
+ const target = await createFile({
432
+ name: 'file2'
433
+ })
434
+
435
+ const link = path.join(path.dirname(target), 'file2_sym')
436
+ await fs.symlink(target, link)
437
+
438
+ const result = await runRm([link], {
439
+ env: {
440
+ HOME: home,
441
+ PATH: `${mockBin}:${process.env.PATH}`,
442
+ SAFE_RM_TRASH: ''
443
+ }
444
+ })
445
+
446
+ t.is(result.code, 0, 'exit code should be 0')
447
+ t.false(await pathExists(link), 'symlink should be removed')
448
+ t.true(await pathExists(target), 'target file should remain')
449
+ })
275
450
  }
package/test/helper.js CHANGED
@@ -58,13 +58,13 @@ const generateContextMethods = (
58
58
 
59
59
  // Helper function to run rm commands
60
60
  function runRm (args, {
61
- input = '',
61
+ input = [],
62
62
  command = rm_command,
63
63
  env: arg_env = {}
64
64
  } = {}) {
65
- return new Promise(resolve => {
65
+ return new Promise((resolve, reject) => {
66
66
  const env = {
67
- // ...process.env,
67
+ ...process.env,
68
68
  ...{
69
69
  SAFE_RM_TRASH: t.context.trash_path
70
70
  },
@@ -80,21 +80,33 @@ const generateContextMethods = (
80
80
 
81
81
  child.stdout.on('data', data => {
82
82
  stdout += data.toString()
83
+
84
+ if (/\?\s*$/.test(stdout)) {
85
+ if (!child.stdin) {
86
+ reject(new Error('Child process does not support stdin'))
87
+ return
88
+ }
89
+
90
+ const this_input = input.shift()
91
+
92
+ if (!this_input) {
93
+ reject(new Error('Need more input'))
94
+ return
95
+ }
96
+
97
+ child.stdin.write(`${this_input}\n`)
98
+ stdout += `${this_input}\n`
99
+
100
+ if (input.length === 0) {
101
+ child.stdin.end()
102
+ }
103
+ }
83
104
  })
84
105
 
85
106
  child.stderr.on('data', data => {
86
107
  stderr += data.toString()
87
108
  })
88
109
 
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
110
  child.on('close', code => {
99
111
  const resolved = {
100
112
  code,
@@ -105,6 +117,7 @@ const generateContextMethods = (
105
117
  log(command, 'result:', resolved)
106
118
 
107
119
  resolve(resolved)
120
+ child.kill()
108
121
  })
109
122
  })
110
123
  }
package/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- language: bash
2
-
3
- script:
4
- - make test