paccache: escape . in trimming of diskspace string
[pacman-ng.git] / contrib / paccache.in
blob8a8cad4b24bd5af74c8cccfd3a1604d994156b54
1 #!/bin/bash
3 # pacache - flexible pacman cache cleaning
5 # Copyright (C) 2011 Dave Reisner <dreisner@archlinux.org>
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 shopt -s extglob
23 declare -a candidates=() cmdopts=() whitelist=() blacklist=()
24 declare -i delete=0 dryrun=0 filecount=0 keep=3 move=0 totalsaved=0
25 declare cachedir=@localstatedir@/cache/pacman/pkg delim=$'\n' movedir= scanarch=
27 msg() {
28 local mesg=$1; shift
29 printf "==> $mesg\n" "$@"
30 } >&2
32 error() {
33 local mesg=$1; shift
34 printf "==> ERROR: $mesg\n" "$@"
35 } >&2
37 die() {
38 error "$@"
39 exit 1
42 # reads a list of files on stdin and prints out deletion candidates
43 pkgfilter() {
44 # there's whitelist and blacklist parameters passed to this
45 # script after the block of awk.
47 awk -v keep="$1" -v scanarch="$2" '
48 function parse_filename(filename, parts, count, i, pkgname, arch) {
50 count = split(filename, parts, "-")
52 i = 1
53 pkgname = parts[i++]
54 while (i <= count - 3) {
55 pkgname = pkgname "-" parts[i++]
58 arch = substr(parts[count], 1, index(parts[count], ".") - 1)
60 # filter on whitelist or blacklist
61 if (wlen && !whitelist[pkgname]) return
62 if (blen && blacklist[pkgname]) return
64 if ("" == packages[pkgname,arch]) {
65 packages[pkgname,arch] = filename
66 } else {
67 packages[pkgname,arch] = packages[pkgname,arch] SUBSEP filename
71 BEGIN {
72 # create whitelist
73 wlen = ARGV[1]; delete ARGV[1]
74 for (i = 2; i < 2 + wlen; i++) {
75 whitelist[ARGV[i]] = 1
76 delete ARGV[i]
79 # create blacklist
80 blen = ARGV[i]; delete ARGV[i]
81 while (i++ < ARGC) {
82 blacklist[ARGV[i]] = 1
83 delete ARGV[i]
86 # read package filenames
87 while (getline < "/dev/stdin") {
88 parse_filename($0)
91 for (pkglist in packages) {
92 # idx[1,2] = idx[pkgname,arch]
93 split(pkglist, idx, SUBSEP)
95 # enforce architecture match if specified
96 if (!scanarch || scanarch == idx[2]) {
97 count = split(packages[idx[1], idx[2]], pkgs, SUBSEP)
98 if (count > keep) {
99 for(i = 1; i <= count - keep; i++) {
100 print pkgs[i]
105 }' "${@:3}"
108 size_to_human() {
109 awk -v size="$1" '
110 BEGIN {
111 suffix[1] = "B"
112 suffix[2] = "KiB"
113 suffix[3] = "MiB"
114 suffix[4] = "GiB"
115 suffix[5] = "TiB"
116 count = 1
118 while (size > 1024) {
119 size /= 1024
120 count++
123 sizestr = sprintf("%.2f", size)
124 sub(/\.?0+$/, "", sizestr)
125 printf("%s %s", sizestr, suffix[count])
129 runcmd() {
130 if (( needsroot )); then
131 msg "Privilege escalation required"
132 if sudo -v &>/dev/null && sudo -l &>/dev/null; then
133 sudo "$@"
134 else
135 printf '%s ' 'root'
136 su -c "$(printf '%q ' "$@")"
138 else
139 "$@"
143 summarize() {
144 local -i filecount=$1; shift
145 local seenarch= seen= arch= name=
146 local -r pkg_re='(.+)-[^-]+-[0-9]+-([^.]+)\.pkg.*'
148 if (( delete )); then
149 printf -v output 'finished: %d packages removed' "$filecount"
150 elif (( move )); then
151 printf -v output "finished: %d packages moved to \`%s'" "$filecount" "$movedir"
152 elif (( dryrun )); then
153 if (( verbose )); then
154 msg "Candidate packages:"
155 while read -r pkg; do
156 if (( verbose >= 3 )); then
157 [[ $pkg =~ $pkg_re ]] && name=${BASH_REMATCH[1]} arch=${BASH_REMATCH[2]}
158 if [[ -z $seen || $seenarch != $arch || $seen != $name ]]; then
159 printf '%s (%s):\n' "$name" "$arch"
161 printf ' %s\n' "$pkg"
162 elif (( verbose >= 2 )); then
163 printf "$PWD/%s$delim" "$pkg"
164 else
165 printf "%s$delim" "$pkg"
167 done < <(printf '%s\n' "$@" | pacsort)
169 printf -v output 'finished dry run: %d candidates' "$filecount"
172 printf '\n' >&2
173 msg "$output (diskspace saved: %s)" "$(size_to_human "$totalsaved")"
176 usage() {
177 cat <<EOF
178 usage: ${0##*/} <operation> [options] [targets...]
180 ${0##*/} is a flexible pacman cache cleaning utility, which has numerous
181 options to help control how much, and what, is deleted from any directory
182 containing pacman package tarballs.
184 Operations:
185 -d perform a dry run, only finding candidate packages.
186 -m <movedir> move candidate packages to 'movedir'.
187 -r remove candidate packages.
189 Options:
190 -a <arch> scan for 'arch' (default: all architectures).
191 -c <cachedir> scan 'cachedir' for packages (default: @localstatedir@/cache/pacman/pkg).
192 -f apply force to mv(1) and rm(1) operations.
193 -h display this help message.
194 -i <pkgs> ignore 'pkgs', which is a comma separated. Alternatively,
195 specify '-' to read package names from stdin, newline delimited.
196 -k <num> keep 'num' of each package in 'cachedir' (default: 3).
197 -u target uninstalled packages.
198 -v increase verbosity. specify up to 3 times.
199 -z use null delimiters for candidate names (only with -v and -vv)
204 if (( ! UID )); then
205 error "Do not run this script as root. You will be prompted for privilege escalation."
206 exit 42
209 while getopts ':a:c:dfhi:k:m:rsuvz' opt; do
210 case $opt in
211 a) scanarch=$OPTARG ;;
212 c) cachedir=$OPTARG ;;
213 d) dryrun=1 ;;
214 f) cmdopts=(-f) ;;
215 h) usage
216 exit 0 ;;
217 i) if [[ $OPTARG = '-' ]]; then
218 [[ ! -t 0 ]] && IFS=$'\n' read -r -d '' -a ign
219 else
220 IFS=',' read -r -a ign <<< "$OPTARG"
222 blacklist+=("${ign[@]}")
223 unset i ign ;;
224 k) keep=$OPTARG
225 if [[ $keep != $OPTARG ]] || (( keep < 0 )); then
226 die 'argument to option -k must be a non-negative integer'
227 fi ;;
228 m) move=1 movedir=$OPTARG ;;
229 r) delete=1 ;;
230 u) IFS=$'\n' read -r -d '' -a ign < <(pacman -Qq)
231 blacklist+=("${ign[@]}")
232 unset ign ;;
233 v) (( ++verbose )) ;;
234 z) delim='\0' ;;
235 :) die "option '--%s' requires an argument" "$OPTARG" ;;
236 ?) die "invalid option -- '%s'" "$OPTARG" ;;
237 esac
238 done
239 shift $(( OPTIND - 1 ))
241 # remaining args are a whitelist
242 whitelist=("$@")
244 # sanity checks
245 case $(( dryrun+delete+move )) in
246 0) die "no operation specified (use -h for help)" ;;
247 [^1]) die "only one operation may be used at a time" ;;
248 esac
250 [[ -d $cachedir ]] ||
251 die "cachedir \`%s' does not exist or is not a directory" "$cachedir"
253 [[ $movedir && ! -d $movedir ]] &&
254 die "move-to directory \`%s' does not exist or is not a directory" "$movedir"
256 if (( move || delete )); then
257 # make it an absolute path since we're about to chdir
258 [[ ${movedir:0:1} != '/' ]] && movedir=$PWD/$movedir
259 [[ ! -w $cachedir || ( $movedir && ! -w $movedir ) ]] && needsroot=1
262 # unlikely that this will fail, but better make sure
263 cd "$cachedir" || die "failed to chdir to \`%s'" "$cachedir"
265 # note that these results are returned in an arbitrary order from awk, but
266 # they'll be resorted (in summarize) iff we have a verbosity level set.
267 IFS=$'\n' read -r -d '' -a candidates < \
268 <(printf '%s\n' *.pkg.tar?(.+([^.])) | pacsort |
269 pkgfilter "$keep" "$scanarch" \
270 "${#whitelist[*]}" "${whitelist[@]}" \
271 "${#blacklist[*]}" "${blacklist[@]}")
273 if (( ! ${#candidates[*]} )); then
274 msg 'no candidate packages found for pruning'
275 exit 1
278 # grab this prior to signature scavenging
279 pkgcount=${#candidates[*]}
281 # copy the list, merging in any found sigs
282 for cand in "${candidates[@]}"; do
283 candtemp+=("$cand")
284 [[ -f $cand.sig ]] && candtemp+=("$cand.sig")
285 done
286 candidates=("${candtemp[@]}")
287 unset candtemp
289 # do this before we destroy anything
290 totalsaved=$(@SIZECMD@ "${candidates[@]}" | awk '{ sum += $1 } END { print sum }')
292 # crush. kill. destroy.
293 (( verbose )) && cmdopts+=(-v)
294 if (( delete )); then
295 runcmd rm "${cmdopts[@]}" "${candidates[@]}"
296 elif (( move )); then
297 runcmd mv "${cmdopts[@]}" "${candidates[@]}" "$movedir"
300 summarize "$pkgcount" "${candidates[@]}"