-
Notifications
You must be signed in to change notification settings - Fork 0
/
mv++
executable file
·209 lines (176 loc) · 6.16 KB
/
mv++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env bash
#
# Waring: Obviusly this script is only interactive and shoud not be used in background scripts
#
set -eu
shopt -s nullglob dotglob
IFS=''
echoerr(){ echo >&2 "$@"; }
exit-dry(){ exit "$@"; } # function is changed when --drry-run argument passed
TODO(){ echoerr $'\e[1;33mTODO:\e[m' "$@"; exit-dry 5; }
err(){ echoerr $'\e[1;31mERROR:\e[m' "$1"; [[ ${2:-''} = 0 ]] || exit-dry "${2:-1}"; }
warn(){ echoerr $'\e[1;36mWARNING:\e[m' "$@"; }
info(){ echoerr $'\e[1;34mINFO:\e[m' "$@"; }
yn='[y/n]' # used for string replacement bellow
confirm(){ # confirm(prompt_questin, default=y)
local p
read -p "$1 ${yn^^${2:-y}} " p &&
# [[ $p = [Yy]* || ( ! $2 && ! $p ) ]]
if [[ ${2:-y} = [Nn]* ]]
then [[ $p = [Yy]* ]]
else [[ $p = ?([Yy]*) ]]
fi
return $?
}
v=--verbose
i=--interactive
debug=0
# __mvPP_dryrun is keept from the envirometn
tmp=.mv++_$$~ # temp name for swapping files
for _i; do
case $_i in
--) shift;break;;
--help) TODO write help message;;
--debug|--dbg|-x) debug=1;shift;;
--dry-run|--dry|-d) __mvPP_dryrun=1; shift;;
--no-verbose|-V) v=; shift;; # is this even usefull ?, TODO: consider dropping it
--no-interactive|-I) i=; shift;;
--verbose|-v) v=--verbose; shift;;
--interactive|-i) i=--interactive; shift;;
[^-]*|'') break;; # stop when no more arguments # TODO: dont stop and use `shify $index`
--*) echoerr "invalid option ${_i@Q}"; exit 1;;
-*) echoerr "unrecognized option -- ${_i@Q}"; exit 1;;
# TODO: support multiple arguments with 1 dash, example: -vid => --vefbose --interactive --dey-run
esac
done
(( $# )) || TODO write help message
: ${__mvPP_dryrun:=0} # get it from arguments OR enviroment and set to false if not set
d(){ dirname -- "$@";}
b(){ basename -- "$@";}
mv-(){ mv $v $i -- "$@";}
# mvt-(){ mv $v $i --target-directory="${@:$#:1}" -- "${@:1:$#-1}"; }
mvT-(){ mv $v $i --no-target-directory -- "$@"; }
mkdir-(){ mkdir $v --parents -- "$@"; }
rmdir-(){ rmdir $v --parents --ignore-fail-on-non-empty -- "$@"; }
isonlydir(){ # checks if $2 contain only $1 (sub directory included) # todo: berrer explain this fn
local a=("${2%/}"/*) # shopt -s nullglob dotglob
[[ ${#a[@]} = 1 && ${1%/} = "${a#./}"?(/*) ]]
}
isemptydir(){ # checks is the directory empty
local a=("${1%/}"/*) # shopt -s nullglob dotglob
[[ -d $1 && ${#a[@]} = 0 && ! -e $t ]]
}
crmdir(){ # Confirm to Rmdir Dirname
local t=$(d "$1")
local m=
isemptydir "$t" || return 0
[[ $t = . ]] && t=../${PWD##*/} && m=' (curretn directory)'
confirm "delete LEFTOVER dirnames of SOURCE$m" || return 0 # dry-run replace relyes on this line bellow
rmdir- "$t"
}
(( __mvPP_dryrun )) &&
eval 'function '{rmdir,mv,mkdir,exit-dry}' () { echo " $ ${FUNCNAME%-dry}" "${@@Q}"; }; ' &&
eval "$(_i=$(declare -f crmdir); echo "${_i/'isemptydir "$t"'/'warn "$FUNCNAME fn may differ in not dry-run"; isonlydir "$1" "$t" && [[ "$t" != "$(d "$2")" ]]'}")"
(( debug )) && echoerr "${@@A}" && set -x
[[ $# != 2 ]] && err 'only 1 SOURCE argument is supported'
if [[ $1 = @(?(*/).?(.)?(/*)) || $2 = ?(*/).?(.)?(/*) ]]; then # if contain .. or . in SOURCE or TARGET
info "using readlink because relarive path '..' or '.' found"
source=$(readlink -e -- "$1") &&
target=$(readlink -f -- "$2") &&
set -- "$source${1##*[^/]}" "$target${2##*[^/]}" ||
err 'readling failed'
# TODO: test `readlink -e` and '-f'
else
set -- "${1##+(./)}" "${2##+(./)}" # remove './' example: ./foo OR ./././foo --> foo
set -- "${@//\/\//\/}" # replace '//' with single '/'
fi
# when many arguments: [[ "$(printf '%c' "$@")" = @(+(/)|+([^/]) ]] || {
[[ ${1:0:1}${2:0:1} = @(//|[^/][^/]) ]] || {
err 'SOURCE and TARGET are with different type of path /absolute and ./relative' 0
confirm 'Use readlink?' n && {
source=$(readlink -e -- "$1") &&
target=$(readlink -f -- "$2") &&
set -- "$source${1##*[^/]}" "$target${2##*[^/]}" ||
{ err 'readling failed' 0; false; }
} ||
confirm 'Try anyway' n || exit 1
}
[[ ! -e $1 ]] && err 'SOURCE file must exist'
[[ ${1%/} = "$2" && ( $2 != */ || ${2%/*}/${1##*/} = "$2" ) ]] && err 'SOURCE and TARGET are the same file'
inner_dir(){
mvT- "$1" "${1%/}$tmp" &&
mkdir- "${2%/*}" &&
if [[ $2 = */ ]]
then mvT- "${1%/}$tmp" "${2%/*}/$(b "$1")" # using basename to remove $tmp # todo: "${2%/*}/" instead of "$2/" - foo//bar is the same as foo/bar ?
else mvT- "${1%/}$tmp" "$2"
fi
}
upper_dir(){
mvT- "$1" "${2%/}$tmp" &&
rmdir- "$(d "${1#./}")" &&
mvT- "${2%/}$tmp" "$2"
}
merge(){
TODO "implement $FUNCNAME function"
}
swap(){
mvT- "$1" "${1%/}$tmp" &&
mvT- "$2" "$1" &&
mvT- "${1%/}$tmp" "$2"
}
target(){
mkdir- "${2%/*}" &&
mv- "$@"
}
main(){
# TODO: check which "mv" needs --target-directory
[[ $2 = "${1%/}"/* ]] && confirm 'move in INNER directory with saame name?' && {
inner_dir "$@"
return $?
}
[[ $1 = "${2%/}"/* ]] && confirm 'move in UPPER directory with saame name?' && {
isonlydir "$@" || {
warn 'SOURCE is not directly only content in TARGET directory'
confirm 'try anyway?' n
} && {
upper_dir "$@"
return $?
}
}
[[ -e $2 && ${1##*[^/]} = "${2##*[^/]}" ]] && {
# will not match if SOUCE or TARGET end with 2 or more at end //
[[ -d $1 && -d $2 ]] && confirm 'MERGE directoryes? (no for swap files)' && {
merge "$@" || return $?
crmdir "$@"
return $?
}
confirm 'SWAP files?' && {
swap "$@"
return $?
}
}
# requires '/' to be in TARGET argument, (just put it at end if you want to force creating TARGET dir)
# dont use dirname since it can be target directory ( when ends with '/' | mv++ foo 'bar/' )
[[ $2 = */* && ! -d ${2%/*} ]] && confirm 'make TARGET dirname and use regular MV?' && {
target "$@" || return $?
crmdir "$@"
return $?
}
confirm 'use regular MV?' && {
mv- "$@" || return $?
crmdir "$@"
return $?
}
err 'there is nothing to do' 4
return 4
}
main "$@"
exit $?
# TODO:
# rmdit:
# todo: is this bad practice for absolute path ?
# WARNING: this is not good for absolute path # AND also for ../../parrent-dirs ( not anymore, because uses readlink )
# `[[ $@ = . ]] && rmdir ...` was used before readlink # '.' is current directory and shoud not try to remove it?
# TODO: check if $() keeps tabs spaces and NEW LINE
# test -e # FILE exists
# test -d # FILE exists and is a directory