safe-rm 3.1.2 → 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.
Files changed (3) hide show
  1. package/bin/rm.sh +51 -21
  2. package/package.json +1 -1
  3. package/test/cases.js +132 -0
package/bin/rm.sh CHANGED
@@ -429,17 +429,24 @@ remove(){
429
429
 
430
430
 
431
431
  recursive_remove(){
432
+ local dir=$1
432
433
  local path
433
-
434
- # use `ls -A` instead of `for` to list hidden files.
435
- # and `for $1/*` is also weird if `$1` is neithor a dir nor existing that will print "$1/*" directly and rudely.
436
- # never use `find $1`, for the searching order is not what we want
437
- local list=$(ls -A "$1")
438
-
439
- [[ -n $list ]] && for path in $list; do
440
- debug "$LINENO: recursively remove: $1/$path"
441
-
442
- 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"
443
450
  done
444
451
  }
445
452
 
@@ -521,12 +528,19 @@ applescript_trash(){
521
528
 
522
529
  [[ "$OPT_VERBOSE" == 1 ]] && list_files "$target"
523
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
+
524
539
  debug "$LINENO: osascript delete $target"
525
540
 
526
541
  osascript -e "tell application \"Finder\" to delete (POSIX file \"$target\" as alias)" &> /dev/null
527
542
 
528
- # TODO: handle osascript errors
529
- return 0
543
+ return $?
530
544
  }
531
545
 
532
546
 
@@ -610,11 +624,12 @@ mac_trash(){
610
624
 
611
625
  debug "$LINENO: mv $move to $trash_path"
612
626
  mv "$move" "$trash_path"
627
+ local status=$?
613
628
 
614
- [[ "$_traveled" == 1 ]] && cd $__DIRNAME &> /dev/null
629
+ [[ "$_traveled" == 1 ]] && cd "$__DIRNAME" &> /dev/null
615
630
 
616
- # default status
617
- return 0
631
+ # Propagate mv status; otherwise callers may treat move failures as success.
632
+ return $status
618
633
  }
619
634
 
620
635
 
@@ -668,6 +683,13 @@ linux_trash(){
668
683
  # Move the target into the trash
669
684
  debug "$LINENO: mv $move to $trash_path"
670
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
671
693
 
672
694
  # Save linux trash info
673
695
  local info_path="$SAFE_RM_TRASH/info/$base.trashinfo"
@@ -677,10 +699,11 @@ linux_trash(){
677
699
  Path=$move
678
700
  DeletionDate=$trash_time
679
701
  EOF
702
+ local info_status=$?
680
703
 
681
- [[ "$_traveled" == 1 ]] && cd $__DIRNAME &> /dev/null
704
+ [[ "$_traveled" == 1 ]] && cd "$__DIRNAME" &> /dev/null
682
705
 
683
- return 0
706
+ return $info_status
684
707
  }
685
708
 
686
709
 
@@ -689,15 +712,22 @@ EOF
689
712
  # 'coz `find` act a inward searching, unlike rm -v
690
713
  list_files(){
691
714
  if [[ -d "$1" ]]; then
692
- 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"
693
723
  local f
694
724
 
695
- [[ -n $list ]] && for f in $list; do
696
- list_files "$1/$f"
725
+ for f in "${list[@]}"; do
726
+ list_files "$f"
697
727
  done
698
728
  fi
699
729
 
700
- echo $1
730
+ echo "$1"
701
731
  }
702
732
 
703
733
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safe-rm",
3
- "version": "3.1.2",
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"
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
 
@@ -315,4 +316,135 @@ remove ${dir}? y
315
316
 
316
317
  t.false(await pathExists(dir), 'directory should be removed')
317
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
+ })
318
450
  }