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.
- package/bin/rm.sh +51 -21
- package/package.json +1 -1
- 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
|
-
|
|
435
|
-
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
617
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
696
|
-
list_files "$
|
|
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
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
|
}
|