替换 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
评论
发表评论
       
       
取消