safe-rm 3.1.3 → 3.1.5
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 +105 -41
- package/package.json +1 -1
- package/test/cases.js +409 -24
- package/test/helper.js +11 -4
package/bin/rm.sh
CHANGED
|
@@ -330,7 +330,7 @@ if [[ ! -e $SAFE_RM_TRASH ]]; then
|
|
|
330
330
|
echo -n "(yes/no): "
|
|
331
331
|
|
|
332
332
|
read answer
|
|
333
|
-
if [[ $answer == "yes" || ! -n $
|
|
333
|
+
if [[ $answer == "yes" || ! -n $answer ]]; then
|
|
334
334
|
mkdir -p "$SAFE_RM_TRASH"
|
|
335
335
|
else
|
|
336
336
|
echo "Canceled!"
|
|
@@ -455,7 +455,7 @@ trash(){
|
|
|
455
455
|
local target=$1
|
|
456
456
|
|
|
457
457
|
if [[ -n $SAFE_RM_PERM_DEL_FILES_IN_TRASH ]]; then
|
|
458
|
-
if
|
|
458
|
+
if is_in_trash "$target"; then
|
|
459
459
|
# If the target is already in the trash, delete it permanently
|
|
460
460
|
/bin/rm -rf "$target"
|
|
461
461
|
else
|
|
@@ -490,7 +490,48 @@ do_trash(){
|
|
|
490
490
|
|
|
491
491
|
|
|
492
492
|
get_absolute_path(){
|
|
493
|
-
|
|
493
|
+
local dir
|
|
494
|
+
local base
|
|
495
|
+
dir=$(cd "$(dirname -- "$1")" && pwd)
|
|
496
|
+
base=$(basename -- "$1")
|
|
497
|
+
printf '%s/%s\n' "$dir" "$base"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
encode_trashinfo_path(){
|
|
502
|
+
local path=$1
|
|
503
|
+
local out=
|
|
504
|
+
local i
|
|
505
|
+
local char
|
|
506
|
+
local hex
|
|
507
|
+
local LC_ALL=C
|
|
508
|
+
|
|
509
|
+
for ((i = 0; i < ${#path}; i += 1)); do
|
|
510
|
+
char=${path:i:1}
|
|
511
|
+
|
|
512
|
+
case "$char" in
|
|
513
|
+
[a-zA-Z0-9._~/-])
|
|
514
|
+
out="$out$char"
|
|
515
|
+
;;
|
|
516
|
+
*)
|
|
517
|
+
hex=$(printf '%s' "$char" | od -An -tx1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')
|
|
518
|
+
out="$out%$hex"
|
|
519
|
+
;;
|
|
520
|
+
esac
|
|
521
|
+
done
|
|
522
|
+
|
|
523
|
+
printf '%s\n' "$out"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
is_in_trash(){
|
|
528
|
+
local target_abs
|
|
529
|
+
local trash_abs
|
|
530
|
+
|
|
531
|
+
target_abs=$(get_absolute_path "$1") || return 1
|
|
532
|
+
trash_abs=$(cd "$SAFE_RM_TRASH" && pwd) || return 1
|
|
533
|
+
|
|
534
|
+
[[ "$target_abs" == "$trash_abs" || "$target_abs" == "$trash_abs"/* ]]
|
|
494
535
|
}
|
|
495
536
|
|
|
496
537
|
|
|
@@ -555,16 +596,22 @@ check_mac_trash_path(){
|
|
|
555
596
|
local ext=$2
|
|
556
597
|
local full_path="$path$ext"
|
|
557
598
|
|
|
558
|
-
|
|
559
|
-
if [[ -e "$full_path" ]]; then
|
|
560
|
-
debug "$LINENO: $full_path already exists"
|
|
561
|
-
|
|
562
|
-
# renew $_short_time_ret
|
|
563
|
-
short_time
|
|
564
|
-
check_mac_trash_path "$path $_short_time_ret" "$ext"
|
|
565
|
-
else
|
|
599
|
+
if [[ ! -e "$full_path" ]]; then
|
|
566
600
|
_mac_trash_path_ret=$full_path
|
|
601
|
+
return
|
|
567
602
|
fi
|
|
603
|
+
|
|
604
|
+
debug "$LINENO: $full_path already exists"
|
|
605
|
+
|
|
606
|
+
short_time
|
|
607
|
+
full_path="$path $_short_time_ret$ext"
|
|
608
|
+
|
|
609
|
+
while [[ -e "$full_path" ]]; do
|
|
610
|
+
debug "$LINENO: $full_path already exists"
|
|
611
|
+
full_path="${full_path}X"
|
|
612
|
+
done
|
|
613
|
+
|
|
614
|
+
_mac_trash_path_ret=$full_path
|
|
568
615
|
}
|
|
569
616
|
|
|
570
617
|
|
|
@@ -601,7 +648,7 @@ check_target_to_move(){
|
|
|
601
648
|
mac_trash(){
|
|
602
649
|
check_target_to_move "$1"
|
|
603
650
|
local move=$_to_move
|
|
604
|
-
local base=$(basename "$move")
|
|
651
|
+
local base=$(basename -- "$move")
|
|
605
652
|
|
|
606
653
|
# foo.jpg => "foo" + ".jpg"
|
|
607
654
|
# foo => "foo" + ""
|
|
@@ -623,7 +670,7 @@ mac_trash(){
|
|
|
623
670
|
[[ "$OPT_VERBOSE" == 1 ]] && list_files "$1"
|
|
624
671
|
|
|
625
672
|
debug "$LINENO: mv $move to $trash_path"
|
|
626
|
-
mv "$move" "$trash_path"
|
|
673
|
+
mv -- "$move" "$trash_path"
|
|
627
674
|
local status=$?
|
|
628
675
|
|
|
629
676
|
[[ "$_traveled" == 1 ]] && cd "$__DIRNAME" &> /dev/null
|
|
@@ -647,16 +694,35 @@ check_linux_trash_base(){
|
|
|
647
694
|
|
|
648
695
|
local max_n=0
|
|
649
696
|
local num=
|
|
697
|
+
local restore_nullglob=$(shopt -p nullglob)
|
|
698
|
+
local restore_dotglob=$(shopt -p dotglob)
|
|
699
|
+
shopt -s nullglob dotglob
|
|
700
|
+
local list=("$trash"/*)
|
|
701
|
+
eval "$restore_nullglob"
|
|
702
|
+
eval "$restore_dotglob"
|
|
703
|
+
local file
|
|
704
|
+
local name
|
|
705
|
+
local suffix
|
|
706
|
+
|
|
707
|
+
for file in "${list[@]}"; do
|
|
708
|
+
name=$(basename -- "$file")
|
|
709
|
+
|
|
710
|
+
if [[ "$name" != "$base".* ]]; then
|
|
711
|
+
continue
|
|
712
|
+
fi
|
|
713
|
+
|
|
714
|
+
suffix=${name#"$base".}
|
|
650
715
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
# Remove leading zeros and make sure the number is in base 10
|
|
654
|
-
num=$((10#${BASH_REMATCH[1]}))
|
|
655
|
-
if ((num > max_n)); then
|
|
656
|
-
max_n=$num
|
|
657
|
-
fi
|
|
716
|
+
if [[ ! "$suffix" =~ ^[0-9]+$ ]]; then
|
|
717
|
+
continue
|
|
658
718
|
fi
|
|
659
|
-
|
|
719
|
+
|
|
720
|
+
# Remove leading zeros and make sure the number is in base 10
|
|
721
|
+
num=$((10#$suffix))
|
|
722
|
+
if ((num > max_n)); then
|
|
723
|
+
max_n=$num
|
|
724
|
+
fi
|
|
725
|
+
done
|
|
660
726
|
|
|
661
727
|
(( max_n += 1 ))
|
|
662
728
|
|
|
@@ -670,9 +736,15 @@ check_linux_trash_base(){
|
|
|
670
736
|
# trash a file or dir directly for linux
|
|
671
737
|
# - move the target into
|
|
672
738
|
linux_trash(){
|
|
739
|
+
local original_path
|
|
740
|
+
local trashinfo_path_value
|
|
741
|
+
|
|
742
|
+
original_path=$(get_absolute_path "$1") || return 1
|
|
743
|
+
trashinfo_path_value=$(encode_trashinfo_path "$original_path") || return 1
|
|
744
|
+
|
|
673
745
|
check_target_to_move "$1"
|
|
674
746
|
local move=$_to_move
|
|
675
|
-
local base=$(basename "$move")
|
|
747
|
+
local base=$(basename -- "$move")
|
|
676
748
|
|
|
677
749
|
base=$(check_linux_trash_base "$base")
|
|
678
750
|
|
|
@@ -682,7 +754,7 @@ linux_trash(){
|
|
|
682
754
|
|
|
683
755
|
# Move the target into the trash
|
|
684
756
|
debug "$LINENO: mv $move to $trash_path"
|
|
685
|
-
mv "$move" "$trash_path"
|
|
757
|
+
mv -- "$move" "$trash_path"
|
|
686
758
|
local move_status=$?
|
|
687
759
|
|
|
688
760
|
if [[ $move_status -ne 0 ]]; then
|
|
@@ -696,7 +768,7 @@ linux_trash(){
|
|
|
696
768
|
local trash_time=$(date +%Y-%m-%dT%H:%M:%S)
|
|
697
769
|
cat > "$info_path" <<EOF
|
|
698
770
|
[Trash Info]
|
|
699
|
-
Path=$
|
|
771
|
+
Path=$trashinfo_path_value
|
|
700
772
|
DeletionDate=$trash_time
|
|
701
773
|
EOF
|
|
702
774
|
local info_status=$?
|
|
@@ -735,7 +807,7 @@ list_files(){
|
|
|
735
807
|
debug "$LINENO: ${#FILE_NAME[@]} files or directory to process: ${FILE_NAME[@]}"
|
|
736
808
|
|
|
737
809
|
# test remove interactive_once: ask for 3 or more files or with recursive option
|
|
738
|
-
if [[ (${#FILE_NAME[@]} >
|
|
810
|
+
if [[ (${#FILE_NAME[@]} > 3 || $OPT_RECURSIVE == 1) && $OPT_INTERACTIVE_ONCE == 1 ]]; then
|
|
739
811
|
echo -n "$COMMAND: remove all arguments? "
|
|
740
812
|
read answer
|
|
741
813
|
|
|
@@ -763,28 +835,20 @@ for file in "${FILE_NAME[@]}"; do
|
|
|
763
835
|
fi
|
|
764
836
|
|
|
765
837
|
# the same check also apply on /. /..
|
|
766
|
-
if [[ $(basename "$file") == "." || $(basename "$file") == ".." ]]; then
|
|
838
|
+
if [[ $(basename -- "$file") == "." || $(basename -- "$file") == ".." ]]; then
|
|
767
839
|
error "$COMMAND: \".\" and \"..\" may not be removed"
|
|
768
840
|
EXIT_CODE=1
|
|
769
841
|
continue
|
|
770
842
|
fi
|
|
771
843
|
|
|
772
|
-
|
|
773
|
-
|
|
844
|
+
if [[ -e "$file" || -L "$file" ]]; then
|
|
845
|
+
remove "$file"
|
|
846
|
+
status=$?
|
|
847
|
+
debug "$LINENO: remove returned status: $status"
|
|
774
848
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if [[ -n "$ls_result" ]]; then
|
|
779
|
-
for file in "$ls_result"; do
|
|
780
|
-
remove "$file"
|
|
781
|
-
status=$?
|
|
782
|
-
debug "$LINENO: remove returned status: $status"
|
|
783
|
-
|
|
784
|
-
if [[ ! $status == 0 ]]; then
|
|
785
|
-
EXIT_CODE=1
|
|
786
|
-
fi
|
|
787
|
-
done
|
|
849
|
+
if [[ ! $status == 0 ]]; then
|
|
850
|
+
EXIT_CODE=1
|
|
851
|
+
fi
|
|
788
852
|
elif [[ -z "$OPT_FORCE" ]]; then
|
|
789
853
|
error "$COMMAND: $file: No such file or directory" >&2
|
|
790
854
|
EXIT_CODE=1
|
package/package.json
CHANGED
package/test/cases.js
CHANGED
|
@@ -20,6 +20,9 @@ const is_as = type => type === 'safe-rm-as'
|
|
|
20
20
|
// We skip testing trash dir for vanilla rm
|
|
21
21
|
const should_skip_test_trash_dir = type => is_rm(type) || is_as(type)
|
|
22
22
|
|
|
23
|
+
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
24
|
+
const encodeTrashInfoPath = value => value.split('/').map(encodeURIComponent).join('/')
|
|
25
|
+
|
|
23
26
|
module.exports = (
|
|
24
27
|
test,
|
|
25
28
|
{
|
|
@@ -111,32 +114,24 @@ module.exports = (
|
|
|
111
114
|
.sort((a, b) => a.length - b.length)
|
|
112
115
|
|
|
113
116
|
if (IS_MACOS) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
f => {
|
|
121
|
-
const base = path.basename(f)
|
|
122
|
-
|
|
123
|
-
return base.slice(0, base.length - ext.length)
|
|
124
|
-
}
|
|
117
|
+
const baseNames = files.map(file => path.basename(file))
|
|
118
|
+
const timestamped = baseNames.filter(base => base !== full_name)
|
|
119
|
+
const timestampedRe = new RegExp(
|
|
120
|
+
ext
|
|
121
|
+
? `^${escapeRegExp(filename)} \\d{2}\\.\\d{2}\\.\\d{2}${escapeRegExp(ext)}X*$`
|
|
122
|
+
: `^${escapeRegExp(filename)} \\d{2}\\.\\d{2}\\.\\d{2}X*$`
|
|
125
123
|
)
|
|
126
124
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
t.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
t.is(fbs3[1], time)
|
|
139
|
-
t.is(fbs3[2], time)
|
|
125
|
+
t.is(baseNames.filter(base => base === full_name).length, 1)
|
|
126
|
+
t.is(timestamped.length, 2)
|
|
127
|
+
t.true(
|
|
128
|
+
timestamped.every(base => timestampedRe.test(base)),
|
|
129
|
+
'timestamped duplicates should follow Finder naming'
|
|
130
|
+
)
|
|
131
|
+
t.true(
|
|
132
|
+
timestamped.some(base => ext ? base.endsWith(ext) : !base.endsWith('X')),
|
|
133
|
+
'should include a plain timestamped duplicate'
|
|
134
|
+
)
|
|
140
135
|
} else {
|
|
141
136
|
// /path/to/foo[.jpg]
|
|
142
137
|
// /path/to/foo[.jpg].1
|
|
@@ -193,6 +188,276 @@ module.exports = (
|
|
|
193
188
|
t.is(files.length, 0, 'should be already removed')
|
|
194
189
|
})
|
|
195
190
|
|
|
191
|
+
!is_rm(type) && !is_as(type) && test(`permanent delete only applies to real trash descendants`, async t => {
|
|
192
|
+
const {
|
|
193
|
+
createDir,
|
|
194
|
+
createFile,
|
|
195
|
+
runRm,
|
|
196
|
+
pathExists,
|
|
197
|
+
lsFileInTrash
|
|
198
|
+
} = t.context
|
|
199
|
+
|
|
200
|
+
const sibling = await createDir({
|
|
201
|
+
name: `${path.basename(t.context.trash_path)}-other`,
|
|
202
|
+
under: path.dirname(t.context.trash_path)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const filepath = await createFile({
|
|
206
|
+
name: uuid(),
|
|
207
|
+
under: sibling
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const result = await runRm([filepath], {
|
|
211
|
+
env: {
|
|
212
|
+
SAFE_RM_PERM_DEL_FILES_IN_TRASH: 'yes'
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
assertEmptySuccess(t, result)
|
|
217
|
+
t.false(await pathExists(filepath), 'file should be removed from the sibling dir')
|
|
218
|
+
|
|
219
|
+
const files = await lsFileInTrash(filepath)
|
|
220
|
+
|
|
221
|
+
t.is(files.length, 1, 'file should be moved into trash instead of being hard-deleted')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
!is_rm(type) && !is_as(type) && !IS_MACOS && test(`linux duplicate naming treats regex chars literally`, async t => {
|
|
225
|
+
const {
|
|
226
|
+
trash_path,
|
|
227
|
+
createDir,
|
|
228
|
+
createFile,
|
|
229
|
+
runRm,
|
|
230
|
+
pathExists,
|
|
231
|
+
lsFileInTrash
|
|
232
|
+
} = t.context
|
|
233
|
+
|
|
234
|
+
const filename = 'a+b'
|
|
235
|
+
const trashFilesDir = await createDir({
|
|
236
|
+
name: 'files',
|
|
237
|
+
under: trash_path
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const existing0 = await createFile({
|
|
241
|
+
name: filename,
|
|
242
|
+
under: trashFilesDir,
|
|
243
|
+
content: 'existing-0'
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const existing1 = await createFile({
|
|
247
|
+
name: `${filename}.1`,
|
|
248
|
+
under: trashFilesDir,
|
|
249
|
+
content: 'existing-1'
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const filepath = await createFile({
|
|
253
|
+
name: filename,
|
|
254
|
+
content: 'incoming'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const result = await runRm([filepath])
|
|
258
|
+
|
|
259
|
+
assertEmptySuccess(t, result)
|
|
260
|
+
t.false(await pathExists(filepath), 'source file should be removed')
|
|
261
|
+
|
|
262
|
+
const files = (await lsFileInTrash(filename))
|
|
263
|
+
.map(file => path.basename(file))
|
|
264
|
+
.sort()
|
|
265
|
+
|
|
266
|
+
t.deepEqual(files, [filename, `${filename}.1`, `${filename}.2`])
|
|
267
|
+
t.is(await fs.readFile(existing0, 'utf8'), 'existing-0')
|
|
268
|
+
t.is(await fs.readFile(existing1, 'utf8'), 'existing-1')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
!is_rm(type) && !is_as(type) && !IS_MACOS && test(`linux .trashinfo stores an encoded absolute original path`, async t => {
|
|
272
|
+
const {
|
|
273
|
+
source_path,
|
|
274
|
+
trash_path,
|
|
275
|
+
createFile,
|
|
276
|
+
runRm,
|
|
277
|
+
pathExists
|
|
278
|
+
} = t.context
|
|
279
|
+
|
|
280
|
+
const filename = 'space name'
|
|
281
|
+
const filepath = await createFile({
|
|
282
|
+
name: filename
|
|
283
|
+
})
|
|
284
|
+
const canonicalFilepath = await fs.realpath(filepath)
|
|
285
|
+
|
|
286
|
+
const result = await runRm([`./${filename}`], {
|
|
287
|
+
cwd: source_path
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
assertEmptySuccess(t, result)
|
|
291
|
+
t.false(await pathExists(filepath), 'source file should be removed')
|
|
292
|
+
|
|
293
|
+
const infoPath = path.join(trash_path, 'info', `${filename}.trashinfo`)
|
|
294
|
+
const info = await fs.readFile(infoPath, 'utf8')
|
|
295
|
+
const expectedPath = encodeTrashInfoPath(canonicalFilepath)
|
|
296
|
+
|
|
297
|
+
t.true(info.includes(`[Trash Info]`))
|
|
298
|
+
t.true(info.includes(`Path=${expectedPath}`))
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
!is_rm(type) && !is_as(type) && IS_MACOS && test(`mac fallback duplicate naming matches Finder without extension`, async t => {
|
|
302
|
+
const {
|
|
303
|
+
root,
|
|
304
|
+
createDir,
|
|
305
|
+
createFile,
|
|
306
|
+
runRm,
|
|
307
|
+
lsFileInTrash
|
|
308
|
+
} = t.context
|
|
309
|
+
|
|
310
|
+
const mockBin = await createDir({
|
|
311
|
+
name: 'mock-bin-noext',
|
|
312
|
+
under: root
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const mockDate = await createFile({
|
|
316
|
+
name: 'date',
|
|
317
|
+
under: mockBin,
|
|
318
|
+
content: '#!/usr/bin/env bash\necho 12.34.56\n'
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
await fs.chmod(mockDate, 0o755)
|
|
322
|
+
|
|
323
|
+
const filename = `finder-noext-${uuid()}`
|
|
324
|
+
|
|
325
|
+
for (const content of ['1', '2', '3', '4']) {
|
|
326
|
+
const filepath = await createFile({
|
|
327
|
+
name: filename,
|
|
328
|
+
content
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const result = await runRm([filepath], {
|
|
332
|
+
env: {
|
|
333
|
+
PATH: `${mockBin}:${process.env.PATH}`
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
assertEmptySuccess(t, result)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const files = (await lsFileInTrash(filename))
|
|
341
|
+
.map(file => path.basename(file))
|
|
342
|
+
.sort((a, b) => a.length - b.length)
|
|
343
|
+
|
|
344
|
+
t.deepEqual(files, [
|
|
345
|
+
filename,
|
|
346
|
+
`${filename} 12.34.56`,
|
|
347
|
+
`${filename} 12.34.56X`,
|
|
348
|
+
`${filename} 12.34.56XX`
|
|
349
|
+
])
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
!is_rm(type) && !is_as(type) && IS_MACOS && test(`mac fallback duplicate naming matches Finder with extensions`, async t => {
|
|
353
|
+
const {
|
|
354
|
+
root,
|
|
355
|
+
createDir,
|
|
356
|
+
createFile,
|
|
357
|
+
runRm,
|
|
358
|
+
lsFileInTrash
|
|
359
|
+
} = t.context
|
|
360
|
+
|
|
361
|
+
const mockBin = await createDir({
|
|
362
|
+
name: 'mock-bin-ext',
|
|
363
|
+
under: root
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const mockDate = await createFile({
|
|
367
|
+
name: 'date',
|
|
368
|
+
under: mockBin,
|
|
369
|
+
content: '#!/usr/bin/env bash\necho 12.34.56\n'
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
await fs.chmod(mockDate, 0o755)
|
|
373
|
+
|
|
374
|
+
const filename = `finder-ext-${uuid()}.jpg`
|
|
375
|
+
|
|
376
|
+
for (const content of ['1', '2', '3', '4']) {
|
|
377
|
+
const filepath = await createFile({
|
|
378
|
+
name: filename,
|
|
379
|
+
content
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const result = await runRm([filepath], {
|
|
383
|
+
env: {
|
|
384
|
+
PATH: `${mockBin}:${process.env.PATH}`
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
assertEmptySuccess(t, result)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const files = (await lsFileInTrash(filename))
|
|
392
|
+
.map(file => path.basename(file))
|
|
393
|
+
.sort((a, b) => a.length - b.length)
|
|
394
|
+
|
|
395
|
+
const baseName = path.basename(filename, '.jpg')
|
|
396
|
+
|
|
397
|
+
t.deepEqual(files, [
|
|
398
|
+
filename,
|
|
399
|
+
`${baseName} 12.34.56.jpg`,
|
|
400
|
+
`${baseName} 12.34.56.jpgX`,
|
|
401
|
+
`${baseName} 12.34.56.jpgXX`
|
|
402
|
+
])
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
!is_rm(type) && !is_as(type) && test(`-I only prompts for more than three files`, async t => {
|
|
406
|
+
const {
|
|
407
|
+
createFile,
|
|
408
|
+
runRm,
|
|
409
|
+
pathExists
|
|
410
|
+
} = t.context
|
|
411
|
+
|
|
412
|
+
const files3 = await Promise.all(['a', 'b', 'c'].map(name => createFile({name})))
|
|
413
|
+
const result3 = await runRm(['-I', ...files3])
|
|
414
|
+
|
|
415
|
+
assertEmptySuccess(t, result3)
|
|
416
|
+
t.true(
|
|
417
|
+
(await Promise.all(files3.map(file => pathExists(file)))).every(exists => !exists),
|
|
418
|
+
'three files should be removed without a once-interactive prompt'
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
const files4 = await Promise.all(['d', 'e', 'f', 'g'].map(name => createFile({name})))
|
|
422
|
+
const result4 = await runRm(['-I', ...files4], {
|
|
423
|
+
input: ['n']
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
t.is(result4.code, 0, 'exit code should be 0 when declining the once-interactive prompt')
|
|
427
|
+
t.true(result4.stdout.includes('remove all arguments?'), 'stdout should contain the once-interactive prompt')
|
|
428
|
+
t.true(
|
|
429
|
+
(await Promise.all(files4.map(file => pathExists(file)))).every(Boolean),
|
|
430
|
+
'files should remain after declining the once-interactive prompt'
|
|
431
|
+
)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
!is_rm(type) && !is_as(type) && IS_MACOS && test(`declining trash directory creation aborts removal`, async t => {
|
|
435
|
+
const {
|
|
436
|
+
root,
|
|
437
|
+
createFile,
|
|
438
|
+
runRm,
|
|
439
|
+
pathExists
|
|
440
|
+
} = t.context
|
|
441
|
+
|
|
442
|
+
const filepath = await createFile({
|
|
443
|
+
name: 'needs-trash'
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const trashPath = path.join(root, 'missing-trash')
|
|
447
|
+
const result = await runRm([filepath], {
|
|
448
|
+
input: ['no'],
|
|
449
|
+
env: {
|
|
450
|
+
SAFE_RM_TRASH: trashPath
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
t.is(result.code, 1, 'exit code should be 1 when declining trash creation')
|
|
455
|
+
t.true(result.stdout.includes(`Directory "${trashPath}" does not exist`))
|
|
456
|
+
t.true(result.stdout.includes('Canceled!'))
|
|
457
|
+
t.true(await pathExists(filepath), 'file should remain in place')
|
|
458
|
+
t.false(await pathExists(trashPath), 'trash directory should not be created')
|
|
459
|
+
})
|
|
460
|
+
|
|
196
461
|
test(`#22 exit code with -f option`, async t => {
|
|
197
462
|
const {
|
|
198
463
|
source_path,
|
|
@@ -447,4 +712,124 @@ fi
|
|
|
447
712
|
t.false(await pathExists(link), 'symlink should be removed')
|
|
448
713
|
t.true(await pathExists(target), 'target file should remain')
|
|
449
714
|
})
|
|
715
|
+
|
|
716
|
+
!is_as(type) && test(`removes a dangling symlink`, async t => {
|
|
717
|
+
const {
|
|
718
|
+
source_path,
|
|
719
|
+
runRm
|
|
720
|
+
} = t.context
|
|
721
|
+
|
|
722
|
+
const missingTarget = path.join(source_path, 'missing-target')
|
|
723
|
+
const link = path.join(source_path, 'dangling_sym')
|
|
724
|
+
await fs.symlink(missingTarget, link)
|
|
725
|
+
|
|
726
|
+
const before = await fs.lstat(link)
|
|
727
|
+
t.true(before.isSymbolicLink(), 'should create symlink before remove')
|
|
728
|
+
|
|
729
|
+
const result = await runRm([link])
|
|
730
|
+
|
|
731
|
+
t.is(result.code, 0, 'exit code should be 0')
|
|
732
|
+
await t.throwsAsync(async () => fs.lstat(link), {
|
|
733
|
+
code: 'ENOENT'
|
|
734
|
+
})
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
!is_as(type) && test(`-r should not dereference a directory symlink`, async t => {
|
|
738
|
+
const {
|
|
739
|
+
createDir,
|
|
740
|
+
createFile,
|
|
741
|
+
runRm,
|
|
742
|
+
pathExists
|
|
743
|
+
} = t.context
|
|
744
|
+
|
|
745
|
+
const realDir = await createDir({
|
|
746
|
+
name: 'real-dir'
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
const keep = await createFile({
|
|
750
|
+
name: 'keep',
|
|
751
|
+
under: realDir
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
const link = path.join(path.dirname(realDir), 'real-dir-sym')
|
|
755
|
+
await fs.symlink(realDir, link)
|
|
756
|
+
|
|
757
|
+
const result = await runRm(['-r', link])
|
|
758
|
+
|
|
759
|
+
t.is(result.code, 0, 'exit code should be 0')
|
|
760
|
+
await t.throwsAsync(async () => fs.lstat(link), {
|
|
761
|
+
code: 'ENOENT'
|
|
762
|
+
})
|
|
763
|
+
t.true(await pathExists(realDir), 'target directory should remain')
|
|
764
|
+
t.true(await pathExists(keep), 'target content should remain')
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
!is_as(type) && test(`-ri should not dereference a directory symlink`, async t => {
|
|
768
|
+
const {
|
|
769
|
+
createDir,
|
|
770
|
+
createFile,
|
|
771
|
+
runRm,
|
|
772
|
+
pathExists
|
|
773
|
+
} = t.context
|
|
774
|
+
|
|
775
|
+
const realDir = await createDir({
|
|
776
|
+
name: 'real-dir-i'
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
const keep = await createFile({
|
|
780
|
+
name: 'keep-i',
|
|
781
|
+
under: realDir
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
const link = path.join(path.dirname(realDir), 'real-dir-sym-i')
|
|
785
|
+
await fs.symlink(realDir, link)
|
|
786
|
+
|
|
787
|
+
const result = await runRm(['-ri', link], {
|
|
788
|
+
input: ['y']
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
t.is(result.code, 0, 'exit code should be 0')
|
|
792
|
+
await t.throwsAsync(async () => fs.lstat(link), {
|
|
793
|
+
code: 'ENOENT'
|
|
794
|
+
})
|
|
795
|
+
t.true(await pathExists(realDir), 'target directory should remain')
|
|
796
|
+
t.true(await pathExists(keep), 'target content should remain')
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
!is_as(type) && test(`removes a file prefixed with "-" after --`, async t => {
|
|
800
|
+
const {
|
|
801
|
+
source_path,
|
|
802
|
+
createFile,
|
|
803
|
+
runRm,
|
|
804
|
+
pathExists
|
|
805
|
+
} = t.context
|
|
806
|
+
|
|
807
|
+
const name = '-dash-file'
|
|
808
|
+
await createFile({name})
|
|
809
|
+
|
|
810
|
+
const result = await runRm(['--', name], {
|
|
811
|
+
cwd: source_path
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
t.is(result.code, 0, 'exit code should be 0')
|
|
815
|
+
t.false(await pathExists(name), 'file should be removed')
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
!is_as(type) && test(`removes a file with newline in filename`, async t => {
|
|
819
|
+
const {
|
|
820
|
+
createFile,
|
|
821
|
+
runRm,
|
|
822
|
+
pathExists
|
|
823
|
+
} = t.context
|
|
824
|
+
|
|
825
|
+
const filename = 'line1\nline2'
|
|
826
|
+
const filepath = await createFile({
|
|
827
|
+
name: filename
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
const result = await runRm([filepath])
|
|
831
|
+
|
|
832
|
+
t.is(result.code, 0, 'exit code should be 0')
|
|
833
|
+
t.false(await pathExists(filepath), 'file should be removed')
|
|
834
|
+
})
|
|
450
835
|
}
|
package/test/helper.js
CHANGED
|
@@ -12,6 +12,8 @@ const IS_MACOS = process.platform === 'darwin'
|
|
|
12
12
|
// For linux mock testing
|
|
13
13
|
&& !process.env.SAFE_RM_DEBUG_LINUX
|
|
14
14
|
|
|
15
|
+
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
16
|
+
|
|
15
17
|
const generateContextMethods = (
|
|
16
18
|
rm_command,
|
|
17
19
|
rm_command_env
|
|
@@ -60,7 +62,8 @@ const generateContextMethods = (
|
|
|
60
62
|
function runRm (args, {
|
|
61
63
|
input = [],
|
|
62
64
|
command = rm_command,
|
|
63
|
-
env: arg_env = {}
|
|
65
|
+
env: arg_env = {},
|
|
66
|
+
cwd
|
|
64
67
|
} = {}) {
|
|
65
68
|
return new Promise((resolve, reject) => {
|
|
66
69
|
const env = {
|
|
@@ -73,7 +76,8 @@ const generateContextMethods = (
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
const child = spawn(command, args, {
|
|
76
|
-
env
|
|
79
|
+
env,
|
|
80
|
+
cwd
|
|
77
81
|
})
|
|
78
82
|
let stdout = ''
|
|
79
83
|
let stderr = ''
|
|
@@ -81,7 +85,7 @@ const generateContextMethods = (
|
|
|
81
85
|
child.stdout.on('data', data => {
|
|
82
86
|
stdout += data.toString()
|
|
83
87
|
|
|
84
|
-
if (
|
|
88
|
+
if (/[?:]\s*$/.test(stdout)) {
|
|
85
89
|
if (!child.stdin) {
|
|
86
90
|
reject(new Error('Child process does not support stdin'))
|
|
87
91
|
return
|
|
@@ -142,9 +146,12 @@ const generateContextMethods = (
|
|
|
142
146
|
const _filename = path.basename(filepath)
|
|
143
147
|
const ext = path.extname(_filename)
|
|
144
148
|
const filename = path.basename(_filename, ext)
|
|
149
|
+
const pattern = ext
|
|
150
|
+
? new RegExp(`^${escapeRegExp(filename)}(?: \\d{2}\\.\\d{2}\\.\\d{2})?${escapeRegExp(ext)}X*$`)
|
|
151
|
+
: new RegExp(`^${escapeRegExp(filename)}(?: \\d{2}\\.\\d{2}\\.\\d{2}X*)?$`)
|
|
145
152
|
|
|
146
153
|
const filtered = files.filter(
|
|
147
|
-
f =>
|
|
154
|
+
f => pattern.test(f)
|
|
148
155
|
).map(f => path.join(trash_path, f))
|
|
149
156
|
|
|
150
157
|
return filtered
|