safe-rm 3.1.5 → 3.2.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/README.md +25 -0
- package/bin/rm.sh +107 -2
- package/package.json +1 -1
- package/test/cases.js +143 -0
package/README.md
CHANGED
|
@@ -161,6 +161,31 @@ export SAFE_RM_TRASH=/path/to/trash
|
|
|
161
161
|
export SAFE_RM_PERM_DEL_FILES_IN_TRASH=yes
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
### Restrict Removals To A Safe Directory Scope
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
export SAFE_RM_SCOPE="$HOME"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
When `SAFE_RM_SCOPE` is set, `safe-rm` only removes targets under that directory.
|
|
171
|
+
Targets outside the configured scope will be skipped and reported as unsafe.
|
|
172
|
+
|
|
173
|
+
For example:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
export SAFE_RM_SCOPE="$HOME"
|
|
177
|
+
safe-rm -rf /
|
|
178
|
+
# rm: target '/' skipped, unsafe directory scope
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
You could also use a relative scope such as `.`:
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
SAFE_RM_SCOPE=. safe-rm -rf ./foo
|
|
185
|
+
SAFE_RM_SCOPE=. safe-rm -rf ../
|
|
186
|
+
# rm: target '../' skipped, unsafe directory scope
|
|
187
|
+
```
|
|
188
|
+
|
|
164
189
|
### Protect Files And Directories From Deleting
|
|
165
190
|
|
|
166
191
|
If you want to protect some certain files or directories from deleting by mistake, you could create a `.gitignore` file under the `"~/.safe-rm/"` directory, you could write [.gitignore rules](https://git-scm.com/docs/gitignore) inside the file.
|
package/bin/rm.sh
CHANGED
|
@@ -20,10 +20,21 @@ fi
|
|
|
20
20
|
# ```
|
|
21
21
|
SAFE_RM_CONFIG=${SAFE_RM_CONFIG:="$SAFE_RM_CONFIG_ROOT/config"}
|
|
22
22
|
|
|
23
|
+
SAFE_RM_SCOPE_ENV_DEFINED=
|
|
24
|
+
SAFE_RM_SCOPE_ENV_VALUE=
|
|
25
|
+
if [[ ${SAFE_RM_SCOPE+x} ]]; then
|
|
26
|
+
SAFE_RM_SCOPE_ENV_DEFINED=1
|
|
27
|
+
SAFE_RM_SCOPE_ENV_VALUE=$SAFE_RM_SCOPE
|
|
28
|
+
fi
|
|
29
|
+
|
|
23
30
|
if [[ -f "$SAFE_RM_CONFIG" ]]; then
|
|
24
31
|
source "$SAFE_RM_CONFIG"
|
|
25
32
|
fi
|
|
26
33
|
|
|
34
|
+
if [[ -n "$SAFE_RM_SCOPE_ENV_DEFINED" ]]; then
|
|
35
|
+
SAFE_RM_SCOPE=$SAFE_RM_SCOPE_ENV_VALUE
|
|
36
|
+
fi
|
|
37
|
+
|
|
27
38
|
# Print debug info or not
|
|
28
39
|
SAFE_RM_DEBUG=${SAFE_RM_DEBUG:=}
|
|
29
40
|
|
|
@@ -351,6 +362,8 @@ check_return_status(){
|
|
|
351
362
|
remove(){
|
|
352
363
|
local file=$1
|
|
353
364
|
|
|
365
|
+
ensure_safe_scope "$file" || return 1
|
|
366
|
+
|
|
354
367
|
# if is dir
|
|
355
368
|
if [[ -d "$file" && ! -L "$file" ]]; then
|
|
356
369
|
|
|
@@ -492,9 +505,56 @@ do_trash(){
|
|
|
492
505
|
get_absolute_path(){
|
|
493
506
|
local dir
|
|
494
507
|
local base
|
|
495
|
-
|
|
508
|
+
if [[ "$1" == "/" ]]; then
|
|
509
|
+
printf '/\n'
|
|
510
|
+
return
|
|
511
|
+
fi
|
|
512
|
+
|
|
513
|
+
dir=$(cd "$(dirname -- "$1")" && pwd) || return 1
|
|
496
514
|
base=$(basename -- "$1")
|
|
497
|
-
|
|
515
|
+
|
|
516
|
+
case "$base" in
|
|
517
|
+
.)
|
|
518
|
+
printf '%s\n' "$dir"
|
|
519
|
+
;;
|
|
520
|
+
|
|
521
|
+
..)
|
|
522
|
+
dir=$(cd "$dir/.." && pwd) || return 1
|
|
523
|
+
printf '%s\n' "$dir"
|
|
524
|
+
;;
|
|
525
|
+
|
|
526
|
+
*)
|
|
527
|
+
printf '%s/%s\n' "$dir" "$base"
|
|
528
|
+
;;
|
|
529
|
+
esac
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
expand_home_path(){
|
|
534
|
+
case $1 in
|
|
535
|
+
"~")
|
|
536
|
+
printf '%s\n' "$HOME"
|
|
537
|
+
;;
|
|
538
|
+
|
|
539
|
+
"~/"*)
|
|
540
|
+
printf '%s/%s\n' "$HOME" "${1#~/}"
|
|
541
|
+
;;
|
|
542
|
+
|
|
543
|
+
*)
|
|
544
|
+
printf '%s\n' "$1"
|
|
545
|
+
;;
|
|
546
|
+
esac
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
resolve_directory_path(){
|
|
551
|
+
local path
|
|
552
|
+
path=$(expand_home_path "$1")
|
|
553
|
+
|
|
554
|
+
(
|
|
555
|
+
cd "$path" &> /dev/null || exit 1
|
|
556
|
+
pwd
|
|
557
|
+
)
|
|
498
558
|
}
|
|
499
559
|
|
|
500
560
|
|
|
@@ -535,6 +595,44 @@ is_in_trash(){
|
|
|
535
595
|
}
|
|
536
596
|
|
|
537
597
|
|
|
598
|
+
SAFE_RM_SCOPE_ROOT=
|
|
599
|
+
init_safe_rm_scope(){
|
|
600
|
+
if [[ -z "$SAFE_RM_SCOPE" ]]; then
|
|
601
|
+
return 0
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
SAFE_RM_SCOPE_ROOT=$(resolve_directory_path "$SAFE_RM_SCOPE") || {
|
|
605
|
+
error "$COMMAND: invalid SAFE_RM_SCOPE '$SAFE_RM_SCOPE': not an existing directory"
|
|
606
|
+
return 1
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
debug "$LINENO: safe rm scope enabled: $SAFE_RM_SCOPE_ROOT"
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
is_in_scope(){
|
|
614
|
+
local target_abs
|
|
615
|
+
|
|
616
|
+
if [[ -z "$SAFE_RM_SCOPE_ROOT" ]]; then
|
|
617
|
+
return 0
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
target_abs=$(get_absolute_path "$1") || return 1
|
|
621
|
+
|
|
622
|
+
[[ "$target_abs" == "$SAFE_RM_SCOPE_ROOT" || "$target_abs" == "$SAFE_RM_SCOPE_ROOT"/* ]]
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
ensure_safe_scope(){
|
|
627
|
+
if is_in_scope "$1"; then
|
|
628
|
+
return 0
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
error "$COMMAND: target '$1' skipped, unsafe directory scope"
|
|
632
|
+
return 1
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
538
636
|
# Returns
|
|
539
637
|
# - 0: the target is protected
|
|
540
638
|
# - 1: the target is not protected
|
|
@@ -806,6 +904,8 @@ list_files(){
|
|
|
806
904
|
# debug: get $FILE_NAME array length
|
|
807
905
|
debug "$LINENO: ${#FILE_NAME[@]} files or directory to process: ${FILE_NAME[@]}"
|
|
808
906
|
|
|
907
|
+
init_safe_rm_scope || do_exit $LINENO 1
|
|
908
|
+
|
|
809
909
|
# test remove interactive_once: ask for 3 or more files or with recursive option
|
|
810
910
|
if [[ (${#FILE_NAME[@]} > 3 || $OPT_RECURSIVE == 1) && $OPT_INTERACTIVE_ONCE == 1 ]]; then
|
|
811
911
|
echo -n "$COMMAND: remove all arguments? "
|
|
@@ -821,6 +921,11 @@ fi
|
|
|
821
921
|
for file in "${FILE_NAME[@]}"; do
|
|
822
922
|
debug "$LINENO: result file $file"
|
|
823
923
|
|
|
924
|
+
if ! ensure_safe_scope "$file"; then
|
|
925
|
+
EXIT_CODE=1
|
|
926
|
+
continue
|
|
927
|
+
fi
|
|
928
|
+
|
|
824
929
|
if [[ $file == "/" ]]; then
|
|
825
930
|
error "it is dangerous to operate recursively on /"
|
|
826
931
|
error "are you insane?"
|
package/package.json
CHANGED
package/test/cases.js
CHANGED
|
@@ -402,6 +402,149 @@ module.exports = (
|
|
|
402
402
|
])
|
|
403
403
|
})
|
|
404
404
|
|
|
405
|
+
!is_rm(type) && test(`scope "." allows removing targets under cwd only`, async t => {
|
|
406
|
+
const {
|
|
407
|
+
root,
|
|
408
|
+
source_path,
|
|
409
|
+
createDir,
|
|
410
|
+
createFile,
|
|
411
|
+
runRm,
|
|
412
|
+
pathExists
|
|
413
|
+
} = t.context
|
|
414
|
+
|
|
415
|
+
const trash = await createDir({
|
|
416
|
+
name: 'scope-trash-dot',
|
|
417
|
+
under: root
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
const cwd = await createDir({
|
|
421
|
+
name: 'scope-cwd',
|
|
422
|
+
under: source_path
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const inside = await createDir({
|
|
426
|
+
name: 'foo',
|
|
427
|
+
under: cwd
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
await createFile({
|
|
431
|
+
name: 'bar',
|
|
432
|
+
under: inside
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
const resultInside = await runRm(['-rf', 'foo'], {
|
|
436
|
+
cwd,
|
|
437
|
+
env: {
|
|
438
|
+
SAFE_RM_SCOPE: '.',
|
|
439
|
+
SAFE_RM_TRASH: trash
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
assertEmptySuccess(t, resultInside)
|
|
444
|
+
t.false(await pathExists(inside), 'in-scope target should be removed')
|
|
445
|
+
|
|
446
|
+
const resultOutside = await runRm(['-rf', '../'], {
|
|
447
|
+
cwd,
|
|
448
|
+
env: {
|
|
449
|
+
SAFE_RM_SCOPE: '.',
|
|
450
|
+
SAFE_RM_TRASH: trash
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
t.is(resultOutside.code, 1, 'out-of-scope target should fail')
|
|
455
|
+
t.true(resultOutside.stderr.includes('unsafe directory scope'))
|
|
456
|
+
t.true(await pathExists(source_path), 'parent directory should remain')
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
!is_rm(type) && test(`scope "~" expands HOME and blocks targets outside home`, async t => {
|
|
460
|
+
const {
|
|
461
|
+
root,
|
|
462
|
+
createDir,
|
|
463
|
+
createFile,
|
|
464
|
+
runRm,
|
|
465
|
+
pathExists
|
|
466
|
+
} = t.context
|
|
467
|
+
|
|
468
|
+
const home = await createDir({
|
|
469
|
+
name: 'scope-home',
|
|
470
|
+
under: root
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
const trash = await createDir({
|
|
474
|
+
name: 'scope-trash-home',
|
|
475
|
+
under: root
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const inside = await createFile({
|
|
479
|
+
name: 'inside-home',
|
|
480
|
+
under: home
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
const outside = await createFile({
|
|
484
|
+
name: 'outside-home'
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const env = {
|
|
488
|
+
HOME: home,
|
|
489
|
+
SAFE_RM_SCOPE: '~',
|
|
490
|
+
SAFE_RM_TRASH: trash
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const resultInside = await runRm([inside], {
|
|
494
|
+
env
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
assertEmptySuccess(t, resultInside)
|
|
498
|
+
t.false(await pathExists(inside), 'home-scoped target should be removed')
|
|
499
|
+
|
|
500
|
+
const resultOutside = await runRm([outside], {
|
|
501
|
+
env
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
t.is(resultOutside.code, 1, 'target outside home scope should fail')
|
|
505
|
+
t.true(resultOutside.stderr.includes('unsafe directory scope'))
|
|
506
|
+
t.true(await pathExists(outside), 'out-of-scope file should remain')
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
!is_rm(type) && test(`scope checks symlink path instead of symlink target`, async t => {
|
|
510
|
+
const {
|
|
511
|
+
root,
|
|
512
|
+
createDir,
|
|
513
|
+
createFile,
|
|
514
|
+
runRm,
|
|
515
|
+
pathExists
|
|
516
|
+
} = t.context
|
|
517
|
+
|
|
518
|
+
const trash = await createDir({
|
|
519
|
+
name: 'scope-trash-link',
|
|
520
|
+
under: root
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const cwd = await createDir({
|
|
524
|
+
name: 'scope-link-cwd',
|
|
525
|
+
under: root
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const outsideTarget = await createFile({
|
|
529
|
+
name: 'scope-link-target'
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
const link = path.join(cwd, 'scope-link')
|
|
533
|
+
await fs.symlink(outsideTarget, link)
|
|
534
|
+
|
|
535
|
+
const result = await runRm(['scope-link'], {
|
|
536
|
+
cwd,
|
|
537
|
+
env: {
|
|
538
|
+
SAFE_RM_SCOPE: '.',
|
|
539
|
+
SAFE_RM_TRASH: trash
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
assertEmptySuccess(t, result)
|
|
544
|
+
t.false(await pathExists(link), 'symlink should be removed')
|
|
545
|
+
t.true(await pathExists(outsideTarget), 'symlink target should remain')
|
|
546
|
+
})
|
|
547
|
+
|
|
405
548
|
!is_rm(type) && !is_as(type) && test(`-I only prompts for more than three files`, async t => {
|
|
406
549
|
const {
|
|
407
550
|
createFile,
|