替换 rm 命令
2021-07-30/2021-07-30
目的:防止直接删除文件
通过自定义脚本,将文件移到回收站,而不是直接删除。
脚本地址:https://github.com/kaelzhang/shell-safe-rm
将脚本保存在 ~/mysh/rf.sh
中
配置别名 alias rf='~/mysh/rf.sh -rf'
即可通过 rf 命令删除文件。
附录脚本:
1#!/bin/bash
2
3# You could modify these environment variables to change the default constants
4# Default to ~/.Trash on Mac, ~/.local/share/Trash/files on Linux.
5DEFAULT_TRASH="$HOME/.Trash"
6if [[ "$(uname -s)" == "Linux" ]]; then
7 DEFAULT_TRASH="$HOME/.local/share/Trash/files"
8fi
9
10
11SAFE_RM_TRASH=${SAFE_RM_TRASH:="$DEFAULT_TRASH"}
12
13
14# Print debug info or not
15SAFE_RM_DEBUG=${SAFE_RM_DEBUG:=}
16# -------------------------------------------------------------------------------
17
18# Simple basename: /bin/rm -> rm
19COMMAND=${0##*/}
20
21# pwd
22__DIRNAME=$(pwd)
23
24GUID=0
25TIME=
26date_time(){
27 TIME=$(date +%Y-%m-%d_%H:%M:%S)-$GUID
28 (( GUID += 1 ))
29}
30
31# tools
32debug(){
33 if [[ -n "$SAFE_RM_DEBUG" ]]; then
34 echo "[D] $@" >&2
35 fi
36}
37
38
39# parse argv -------------------------------------------------------------------------------
40
41invalid_option(){
42 # if there's an invalid option, `rm` only takes the second char of the option string
43 # case:
44 # rm -c
45 # -> rm: illegal option -- c
46 echo "rm: illegal option -- ${1:1:1}"
47 usage
48}
49
50usage(){
51 echo "usage: rm [-f | -i | -I] [-dPRrvW] file ..."
52 echo " unlink file"
53
54 # if has an invalid option, exit with 64
55 exit 64
56}
57
58
59if [[ "$#" = 0 ]]; then
60 echo "safe-rm"
61 usage
62fi
63
64ARG_END=
65FILE_NAME=
66ARG=
67
68file_i=0
69arg_i=0
70
71split_push_arg(){
72 # remove leading '-' and split combined short options
73 # -vif -> vif -> v, i, f
74 split=`echo ${1:1} | fold -w1`
75
76 local arg
77 for arg in ${split[@]}; do
78 ARG[arg_i]="-$arg"
79 ((arg_i += 1))
80 done
81}
82
83push_arg(){
84 ARG[arg_i]=$1
85 ((arg_i += 1))
86}
87
88push_file(){
89 FILE_NAME[file_i]=$1
90 ((file_i += 1))
91}
92
93# pre-parse argument vector
94while [[ -n $1 ]]; do
95 # case:
96 # rm -v abc -r --force
97 # -> -r will be ignored
98 # -> args: ['-v'], files: ['abc', '-r', 'force']
99 if [[ -n $ARG_END ]]; then
100 push_file "$1"
101
102 else
103 case $1 in
104
105 # case:
106 # rm -v -f -i a b
107
108 # case:
109 # rm -vf -ir a b
110
111 # ATTENTION:
112 # Regex in bash is not perl regex,
113 # in which `'*'` means "anything" (including nothing)
114 -[a-zA-Z]*)
115 split_push_arg $1; debug "short option $1"
116 ;;
117
118 # rm --force a
119 --[a-zA-Z]*)
120 push_arg $1; debug "option $1"
121 ;;
122
123 # rm -- -a
124 --)
125 ARG_END=1; debug "divider"
126 ;;
127
128 # case:
129 # rm -
130 # -> args: [], files: ['-']
131 *)
132 push_file "$1"; debug "file $1"
133 ARG_END=1
134 ;;
135 esac
136 fi
137
138 shift
139done
140
141# flags
142OPT_FORCE=
143OPT_INTERACTIVE=
144OPT_INTERACTIVE_ONCE=
145OPT_RECURSIVE=
146OPT_VERBOSE=
147
148# global exit code, default to 0
149EXIT_CODE=0
150
151# parse options
152for arg in ${ARG[@]}; do
153 case $arg in
154
155 # There's no --help|-h option for rm on Mac OS
156 # [hH]|--[hH]elp)
157 # help
158 # shift
159 # ;;
160
161 -f|--force)
162 OPT_FORCE=1; debug "force : $arg"
163 ;;
164
165 # interactive=always
166 -i|--interactive|--interactive=always)
167 OPT_INTERACTIVE=1; debug "interactive : $arg"
168 OPT_INTERACTIVE_ONCE=
169 ;;
170
171 # interactive=once. interactive=once and interactive=always are exclusive
172 -I|--interactive=once)
173 OPT_INTERACTIVE_ONCE=1; debug "interactive_once : $arg"
174 OPT_INTERACTIVE=;
175 ;;
176
177 # both r and R is allowed
178 -[rR]|--[rR]ecursive)
179 OPT_RECURSIVE=1; debug "recursive : $arg"
180 ;;
181
182 # only lowercase v is allowed
183 -v|--verbose)
184 OPT_VERBOSE=1; debug "verbose : $arg"
185 ;;
186
187 *)
188 invalid_option $arg
189 ;;
190 esac
191done
192# /parse argv -------------------------------------------------------------------------------
193
194
195# make sure recycled bin exists
196if [[ ! -e $SAFE_RM_TRASH ]]; then
197 echo "Directory \"$SAFE_RM_TRASH\" does not exist, do you want create it?"
198 echo -n "(yes/no): "
199
200 read answer
201 if [[ $answer = "yes" || ! -n $anwser ]]; then
202 mkdir -p "$SAFE_RM_TRASH"
203 else
204 echo "Canceled!"
205 exit 1
206 fi
207fi
208
209# try to remove a file or directory
210remove(){
211 local file=$1
212
213 # if is dir
214 if [[ -d $file ]]; then
215
216 # if a directory, and without '-r' option
217 if [[ ! -n $OPT_RECURSIVE ]]; then
218 debug "$LINENO: $file: is a directory"
219 echo "$COMMAND: $file: is a directory"
220 return 1
221 fi
222
223 if [[ $file = './' ]]; then
224 echo "$COMMAND: $file: Invalid argument"
225 return 1
226 fi
227
228 if [[ $OPT_INTERACTIVE = 1 ]]; then
229 echo -n "examine files in directory $file? "
230 read answer
231
232 # actually, as long as the answer start with 'y', the file will be removed
233 # default to no remove
234 if [[ ${answer:0:1} =~ [yY] ]]; then
235
236 # if choose to examine the dir, recursively check files first
237 recursive_remove "$file"
238
239 # interact with the dir at last
240 echo -n "remove $file? "
241 read answer
242 if [[ ${answer:0:1} =~ [yY] ]]; then
243 [[ $(ls -A "$file") ]] && {
244 echo "$COMMAND: $file: Directory not empty"
245
246 return 1
247
248 } || {
249 trash "$file"
250 debug "$LINENO: trash returned status $?"
251 }
252 fi
253 fi
254 else
255 trash "$file"
256 debug "$LINENO: trash returned status $?"
257 fi
258
259 # if is a file
260 else
261 if [[ "$OPT_INTERACTIVE" = 1 ]]; then
262 echo -n "remove $file? "
263 read answer
264 if [[ ${answer:0:1} =~ [yY] ]]; then
265 :
266 else
267 return 0
268 fi
269 fi
270
271 trash "$file"
272 debug "$LINENO: trash returned status $?"
273 fi
274}
275
276
277recursive_remove(){
278 local path
279
280 # use `ls -A` instead of `for` to list hidden files.
281 # and `for $1/*` is also weird if `$1` is neithor a dir nor existing that will print "$1/*" directly and rudely.
282 # never use `find $1`, for the searching order is not what we want
283 local list=$(ls -A "$1")
284
285 [[ -n $list ]] && for path in "$list"; do
286 remove "$1/$path"
287 done
288}
289
290
291# trash a file or dir directly
292trash(){
293 debug "trash $1"
294
295 # origin file path
296 local file=$1
297
298 # the first parameter to be passed to `mv`
299 local move=$file
300 local base=$(basename "$file")
301 local travel=
302
303 # basename ./ -> .
304 # basename ../ -> ..
305 # basename ../abc -> abc
306 # basename ../.abc -> .abc
307 if [[ -d "$file" && ${base:0:1} = '.' ]]; then
308 # then file must be a relative dir
309 cd $file
310
311 # pwd can't be piped?
312 move=$(pwd)
313 move=$(basename "$move")
314 cd ..
315 travel=1
316 fi
317
318 local trash_name=$SAFE_RM_TRASH/$base
319
320 # if already in the trash
321 if [[ -e "$trash_name" ]]; then
322 # renew $TIME
323 date_time
324 trash_name="$trash_name-$TIME"
325 fi
326
327 [[ "$OPT_VERBOSE" = 1 ]] && list_files "$file"
328
329 debug "mv $move to $trash_name"
330 mv "$move" "$trash_name"
331
332 [[ "$travel" = 1 ]] && cd $__DIRNAME &> /dev/null
333
334 #default status
335 return 0
336}
337
338# list all files and maintain outward sequence
339# we can't just use `find $file`, 'coz `find` act a inward searching, unlike rm -v
340list_files(){
341 if [[ -d "$1" ]]; then
342 local list=$(ls -A "$1")
343 local f
344
345 [[ -n $list ]] && for f in "$list"; do
346 list_files "$1/$f"
347 done
348 fi
349
350 echo $1
351}
352
353
354# debug: get $FILE_NAME array length
355debug "${#FILE_NAME[@]} files or directory to process: ${FILE_NAME[@]}"
356
357# test remove interactive_once: ask for 3 or more files or with recorsive option
358if [[ (${#FILE_NAME[@]} > 2 || $OPT_RECURSIVE = 1) && $OPT_INTERACTIVE_ONCE = 1 ]]; then
359 echo -n "$COMMAND: remove all arguments? "
360 read answer
361
362 # actually, as long as the answer start with 'y', the file will be removed
363 # default to no remove
364 if [[ ! ${answer:0:1} =~ [yY] ]]; then
365 debug "EXIT_CODE $EXIT_CODE"
366 exit $EXIT_CODE
367 fi
368fi
369
370for file in "${FILE_NAME[@]}"; do
371 debug "result file $file"
372
373 if [[ $file = "/" ]]; then
374 echo "it is dangerous to operate recursively on /"
375 echo "are you insane?"
376 EXIT_CODE=1
377
378 # Exit immediately
379 debug "EXIT_CODE $EXIT_CODE"
380 exit $EXIT_CODE
381 fi
382
383 if [[ $file = "." || $file = ".." ]]; then
384 echo "$COMMAND: \".\" and \"..\" may not be removed"
385 EXIT_CODE=1
386 continue
387 fi
388
389 #the same check also apply on /. /..
390 if [[ $(basename $file) = "." || $(basename $file) = ".." ]]; then
391 echo "$COMMAND: \".\" and \"..\" may not be removed"
392 EXIT_CODE=1
393 continue
394 fi
395
396 # deal with wildcard and also, redirect error output
397 ls_result=$(ls -d "$file" 2> /dev/null)
398
399 # debug
400 debug "ls_result: $ls_result"
401
402 if [[ -n "$ls_result" ]]; then
403 for file in "$ls_result"; do
404 remove "$file"
405 status=$?
406 debug "remove returned status: $status"
407
408 if [[ ! $status == 0 ]]; then
409 EXIT_CODE=1
410 fi
411 done
412 else
413 echo "$COMMAND: $file: No such file or directory"
414 EXIT_CODE=1
415 fi
416done
417
418debug "EXIT_CODE $EXIT_CODE"
419exit $EXIT_CODE