-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathapt-clean
executable file
·241 lines (206 loc) · 8.49 KB
/
apt-clean
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
#!/bin/bash
# Simplify and clean up redundant or incorrect APT package state.
# TODO: add commands to query the database, and display sorted based
# on name/status/component/section etc.
abort() { local x="$1"; shift; echo >&2 "$@"; exit "$x"; }
NATIVE="$(dpkg --print-architecture)"
# Save to $STATE.new - we can't save to $STATE directly because dialog provides
# no way to distinguish between empty output vs operation cancelled.
save_state() {
if grep -q '^\['"$1"'\]' "$STATE"; then
# blank between "[$1]" and "[!$1]||<EOF>"
awk '(/\[/ && !/\['"$1"'\]/ || 0){f=0} !f; /\['"$1"'\]/{f=1}' "$STATE" > "$STATE.tmp"
sed -i -e '/\['"$1"'\]/r'<(cat; echo;) "$STATE.tmp"
else
cat "$STATE" <(echo "[$1]"; cat; echo;) >> "$STATE.tmp"
fi
mv "$STATE.tmp" "$STATE.new"
}
commit_state() {
cat <"$STATE.new" >"$STATE" && rm -f "$STATE.new"
}
read_state() {
# output between "[$1]" and "[!$1]||<EOF>"
awk '/\['"$1"'\]/{f=1} (/\[/ && !/\['"$1"'\]/ || 0){f=0} f' "$STATE"
}
# Either output &1, or (if the first item is "_") $1 \ &1.
maybe_invert() {
read x
if [ "$x" = "_" ]; then
comm -13 <(sort) "$1"
else
{ echo "$x"; cat; } | sort
fi
}
# Execute a query and ask the user to select a subset to "keep" i.e. do nothing
# with. This selection is saved to STATE, and everything else is output for
# further processing.
#
# old stdout (tty) must be saved to fd 3 before calling, e.g. with `exec 3>&1`
user_select() {
local tag="$1" query="$2" filter="$3" title="$4" sel="$5" sweep="$6"
aptitude search --disable-columns -F '%p %d' "!~v $query" | sort -k1 | {
local argsY=()
local argsN=()
local pkgsf=$(mktemp --tmpdir=/dev/shm apt-clean.XXXXXXXXXX)
trap 'excode=$?; '"rm $pkgsf"'; trap - EXIT; exit $excode' EXIT HUP INT QUIT PIPE TERM
while read pkg desc; do
if ! eval "$filter" >/dev/null 2>/dev/null; then
continue
fi
if read_state "$tag" | grep -qxF "$pkg"; then
argsY+=( "$pkg" "$desc" "on" )
else
argsN+=( "$pkg" "$desc" "off" )
fi
echo "$pkg" >> "$pkgsf"
done
sort "$pkgsf" -o "$pkgsf"
if ! [ -s "$pkgsf" ]; then return; fi
# skip dialog if everything already selected
if $SKIP_FULL && [ "${#argsN[@]}" -eq 0 ]; then
# discard previously-selected entries that are no longer valid i.e.
# didn't show up in the query. the `git diff` will reveal them anyway.
comm -12 "$pkgsf" <(read_state "$tag" | sort) | save_state "$tag"
commit_state
return
fi
rm -f "$STATE.tmp"
if dialog --output-fd 4 --separate-output \
--title "$title" --checklist "Select items that you would like to $sel.\n\nEverything else will be $sweep." \
43 132 40 "_" "INVERT SELECTION" "" "${argsN[@]}" "${argsY[@]}" 4> >(maybe_invert "$pkgsf" | save_state "$tag") >&3; then
# sleep because save_state happens in bg
while [ -f "$STATE.tmp" ]; do sleep 0.1; done
commit_state
# aptitude sorts differently
comm -23 "$pkgsf" <(read_state "$tag" | sort)
fi
}
}
user_run() {
exec 3>&1 # save stdout for user_select
xargs --verbose -r -a <(user_select "${@:1:6}" | tee "$STATE.round") "${@:7}"
local X=$?
if $AUTO_NEXT; then
echo "step \"$1\" complete."
else
read -p "step \"$1\" complete; examine in sub-shell? [y/N] " x
if [ "$x" = "y" ]; then "$SHELL"; fi
fi
if [ "$X" = 0 -a -s "$STATE.round" ]; then
return 119 # a code that xargs does not use
else
return $X
fi
}
git_commit_state() {
local q="$1"
local msg="$2"
shift 2
read -p "$q [y/N] " x
if [ "$x" = "y" ]; then
"$@"
git add "$STATE"
git commit -m "apt-clean: $msg"
fi
}
ESSENTIAL='~prequired|~pimportant|~E'
TOP_LEVEL='!~Rpredepends:~i !~Rdepends:~i !~Rrecommends:~i'
ABS_TOP_LEVEL="$TOP_LEVEL"' !~Rsuggests:~i'
CUR_ARCH='~rnative'
LOCALITY_CAVEAT="This shouldn't affect other packages not listed here - i.e. the aptitude prompt that follows should not contain any non-trivial {a} entries. If it does, then you should have selected extra packages - abort the command by selecting NO, and we will schedule another round for you to fix the problem"
USAGE="Usage: $0 [-a|-s|-h] <STATE>"
# runtime vars
SKIP_FULL=false
AUTO_NEXT=false
USING_GIT=false
while getopts ash o; do
case $o in
a ) AUTO_NEXT=true;;
s ) SKIP_FULL=true;;
h )
cat <<-EOF
$USAGE
Simplify and clean up redundant or incorrect APT package state.
Arguments:
-a Auto mode; don't ask to drop to shell after each step.
-s Skip the selection dialog if all available entries are
already selected.
-h Show this help.
EOF
exit 1
;;
esac
done
shift `expr $OPTIND - 1`
STATE=${1:-apt-clean.txt}
touch "$STATE" "$STATE.tmp" "$STATE.round" || abort 3 "cannot write to one of $STATE{,.tmp,.round}"
cleanup() { rm -f "$STATE.tmp" "$STATE.round" 2>/dev/null; exit; }
trap 'excode=$?; cleanup; trap - EXIT; exit $excode' EXIT HUP INT QUIT PIPE TERM
test -z "$(aptitude search "(~prequired|~E) $CUR_ARCH !~i")" || abort 4 "Your system appears to be FUBAR, try \`aptitude install '(~prequired|~E) $CUR_ARCH'\`."
test -n "$(git ls-tree HEAD -- "$STATE")" && USING_GIT=true
if ! $USING_GIT; then
echo >&2 "warning: $STATE is not controlled by git"
git_commit_state "automatically add to existing (or else new) git repo?" "initialise" git init
read -p "press any key to continue" x
test -n "$(git ls-tree HEAD -- "$STATE")" && USING_GIT=true
fi
while $imperfect; do
imperfect=false
user_run 'block-rec' '~RBrecommends:~i !~i' true 'broken recommends' \
"keep uninstalled, even if recommended by another installed package.\n - You might do this if e.g. it takes up too much space, or it conflicts with another installed package" \
"installed" \
aptitude --prompt install \
|| imperfect=true
user_run 'block-imp' "~pimportant $CUR_ARCH !~i" true 'uninstalled important packages' \
"keep uninstalled, which should only be packages where a functional alternative (e.g. a different version or flavour) is already installed" \
"installed" \
aptitude --prompt install \
|| imperfect=true
user_run 'manual-sub' "~i !($ESSENTIAL) !($TOP_LEVEL) !~M" true 'not top-level, non-automatic' \
"keep installed, even if (theoretically, in the future) nothing else depends on it" \
"marked automatic, which means it will be removed if no longer required, but otherwise kept" \
aptitude --prompt markauto \
|| imperfect=true
user_run 'auto-abstop' "~i !($ESSENTIAL) $ABS_TOP_LEVEL ~M" true 'abs-top-level, automatic' \
"be automatically removed. They will no longer show up here next time, which is what you want" \
"marked manual, which means it will be retained even if nothing else depends on it" \
aptitude --prompt unmarkauto \
|| imperfect=true
user_run 'manual-top' "~i !($ESSENTIAL) $TOP_LEVEL !~M" true 'top-level, non-automatic' \
"keep installed, even if nothing else depends on it" \
"removed, which may benignly cause other autoremovals and another round to be executed" \
aptitude --prompt remove \
|| imperfect=true
user_run 'auto-top' "~i !($ESSENTIAL) $TOP_LEVEL ~M" true 'top-level, automatic' \
"keep automatically installed as a result of a suggestion.\n - If you want to make these unconditionally manually installed, you should 'aptitude unmarkauto' these later, and re-run this script" \
"uninstalled.\n - $LOCALITY_CAVEAT" \
aptitude --prompt remove \
|| imperfect=true
user_run 'foreign-arch' '~rforeign ~i' '! dpkg-query -W "${pkg%:*}:$NATIVE"' 'installed-foreign without installed-native' \
"keep installed, even if the corresponding native package isn't installed" \
"removed, which might trigger aptitude to try to install the native package" \
aptitude --prompt remove \
|| imperfect=true
user_run 'keep-config' '~c' true 'uninstalled but with configuration' \
"keep config files for" \
"purged" \
aptitude --prompt purge \
|| imperfect=true
user_run 'maybe-dummy' "~i !($ESSENTIAL) $TOP_LEVEL (~ddummy|~dtransitional|~d\"safely removed\")" true 'possibly transitional or dummy packages' \
"keep installed, i.e. for which the heuristic (simple regex) we use here is incorrect" \
"purged, since it is simply a dummy or transitional package.\n - $LOCALITY_CAVEAT" \
aptitude --prompt purge \
|| imperfect=true
if $imperfect; then
echo "This round caused some changes and/or exposed some problems; running one final cleanup."
echo "It should not do anything unexpected; if it does, then abort and re-run this script."
aptitude --prompt install
echo "Running another round to iron out newly-uncovered issues and/or to fix those problems."
fi
done
echo "apt-clean done."
if $USING_GIT && ! git diff --quiet; then
git diff
git_commit_state "commit changes?" "$STATE: update package state"
fi