-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathapt-undo
executable file
·295 lines (232 loc) · 7.18 KB
/
apt-undo
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/env bash
# MIT license (c) 2021-2024 https://github.com/slowpeek
# Homepage: https://github.com/slowpeek/apt-undo
# About: Undo install events from apt log
case "$BASH_VERSION" in
[2-3].*|4.[0-2]*)
echo "This script requires bash 4.3, yours is $BASH_VERSION"
exit 1 ;;
esac
set -eu -o pipefail
SCRIPT_VERSION=1.0+git
if [[ -t 2 ]]; then
t_green=$'\e[32m'
t_red=$'\e[31m'
t_yellow=$'\e[33m'
t_reset=$'\e(B\e[m'
else
t_green=
t_red=
t_yellow=
t_reset=
fi
bye() {
echo "${t_red}error:${t_reset}" "$1" >&2
exit 1
}
version() {
echo "apt-undo ${SCRIPT_VERSION}"
exit
}
usage() {
cat <<EOF
Usage: apt-undo [options]
Without any options, show numbered summaries for all install events from the
default log.
Options:
-f path Use a custom log (plaintext or gz-compressed)
instead of the default one. -z is a special case of
-f tailored for gzipped rotated logs. '-f -' stands
for stdin
-h, --help Show usage
-i N Show list of packages for N'th install event
-l When called without -i, print full package lists
instead of summaries
-q Be less verbose: suppress package count with -i and
side-by-side comparison with -t
-t Requires -i. Check what would apt remove undoing
the install event. By default (without -q), print
side-by-side comparison of requested vs actual
removal lists to stderr. If there are any
unrequested packages to be removed, exit with error
without printing anything to stdout. This option
assumes the log's origin is the system apt-undo
runs on. It is pointless to use -t on foreign logs
-V, --version Show version
-z N Operate on N'th gzipped rotated log in the default
location
-z and -f are mutually exclusive.
Homepage https://github.com/slowpeek/apt-undo
EOF
exit
}
is_natural() {
[[ $1 == [1-9]* ]] && test "$1" -eq "$1" 2>/dev/null
}
check_empty() {
[[ -n "$2" ]] || bye "Empty $1 value"
}
check_natural() {
is_natural "$2" || bye "$1 value must be a whole number over zero"
}
log_lines() {
tac | sed -nE '/^Install:/{s/^\S+ //;s/ \([^)]+\),?//g;p}'
}
pkg_list_summary() {
local l=76 total=$# pool=()
while (( $# )); do
if (( (l -= ${#1} + 1) < 0 )); then
pool+=("+$(( total - ${#pool[@]} ))")
break
fi
pool+=("$1")
shift
done
echo "${pool[@]}"
}
_pkg_to_remove() {
LC_ALL=C apt-get remove -o APT::Get::Show-Versions=false -s "$@" |
awk '/^The following packages will be REMOVED:/ {p=1; next} p && /^[^ ]/ {exit} p'
}
pkg_to_remove() {
local arch
arch=$(dpkg --print-architecture)
local pkg
for pkg in $(_pkg_to_remove "$@"); do
[[ $pkg == *:* ]] || pkg+=:$arch
echo "$pkg"
done
}
# Upper: l1, l2, flip, items
add_items() {
local -n l
(( flip^=1 )) && l=l1 || l=l2
(( ${#1} <= l )) || l=${#1}
items+=("${2:-<n>}${1}</>")
}
# Upper: col1, col2, quiet
compare_col1_col2() {
local i=0 j=0 m=${#col1[@]} n=${#col2[@]}
local l1=0 l2=0
local flip=0 delta=0
while (( i<m && j<n )); do
if [[ ${col1[i]} == "${col2[j]}" ]]; then
add_items "${col1[i++]}" '<g>'
add_items "${col2[j++]}" '<g>'
elif [[ ${col1[i]} < "${col2[j]}" ]]; then
add_items "${col1[i++]}" '<y>'
add_items --
else
(( ++delta ))
add_items --
add_items "${col2[j++]}" '<r>'
fi
done
for (( ; i<m; i++ )); do
add_items "${col1[i]}" '<y>'
add_items --
done
for (( ; j<n; j++ )); do
(( ++delta ))
add_items --
add_items "${col2[j]}" '<r>'
done
if [[ $quiet == n ]]; then
local sed_colorize=(
'<g>' "${t_green}"
'<r>' "${t_red}"
'<y>' "${t_yellow}"
'<n>' ''
'</>' "${t_reset}"
)
echo
printf "%-$((l1+7))s %-$((l2+6))s\n" "${items[@]}" |
sed -E "$(printf 's,%s,%s,g;' "${sed_colorize[@]}")"
echo
fi
(( ! delta )) || return 1
}
main() {
# In termux everything is under $PREFIX/
local prefix=${PREFIX-}/var/log/apt
local opts
# jetopt f: hhelp i: l q t Vversion z:
opts=$(getopt -o f:hi:lqtVz: -l help,version -- "$@") || exit
eval set -- "$opts"
local log=$prefix/history.log quiet=n index=0 long=n test=n
local mask=0
while (( $# )); do
case $1 in
-h|--help)
usage ;;
-V|--version)
version ;;
--)
shift
break ;;
-f)
(( mask |= 1 ))
check_empty "$1" "$2"
log=$2
shift 2 ;;
-i)
check_natural "$1" "$2"
index=$2
shift 2 ;;
-l)
long=y
shift ;;
-q)
quiet=y
shift ;;
-t)
test=y
shift ;;
-z)
(( mask |= 2 ))
check_natural "$1" "$2"
log=$prefix/history.log.$2.gz
shift 2 ;;
esac
done
(( mask < 3 )) || bye '-z and -f are mutually exclusive'
[[ $test == n ]] || (( index )) || bye '-t requires -i value set'
(( ! $# )) || bye 'This tool does not accept any non-option args'
if [[ $log != - ]]; then
[[ -f $log ]] || bye "No such file: ${log}"
[[ -r $log ]] || bye "Not readable: ${log}"
exec < "$log"
fi
# --
{
local line
if (( index )); then
{
read -r line
read -r _ ||
bye "-i value is too big, there are only ${line} records in the log"
} < <(sed -n "${index}p;$=")
[[ $quiet == y ]] ||
echo "Package count: $(wc -w <<< "$line")" >&2
if [[ $test == y ]]; then
local col1
# shellcheck disable=SC2086
readarray -t col1 < <(printf '%s\n' $line | sort)
local col2
# shellcheck disable=SC2086
readarray -t col2 < <(pkg_to_remove $line | sort)
compare_col1_col2 >&2 ||
bye 'Extra packages to be removed'
fi
echo "$line"
else
local printer=pkg_list_summary
[[ $long == n ]] || printer='echo'
while read -r line; do
# shellcheck disable=SC2086
"$printer" $line
done | nl -w3 -nln -s ' '
fi
} < <(zcat -f | log_lines)
}
[[ ! ${BASH_SOURCE[0]##*/} == "${0##*/}" ]] || main "$@"