autoclean.sh: abide by POSIX shebang
[pacman-ng.git] / contrib / paccache.in
blobda65f476ca63cd928ebabbb617e467b068be015b
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 -r myname='paccache'
24 declare -r myver='@PACKAGE_VERSION@'
26 declare -a candidates=() cmdopts=() whitelist=() blacklist=()
27 declare -i delete=0 dryrun=0 filecount=0 move=0 needsroot=0 totalsaved=0 verbose=0
28 declare cachedir=@localstatedir@/cache/pacman/pkg delim=$'\n' keep=3 movedir= scanarch=
30 msg() {
31 local mesg=$1; shift
32 printf "==> $mesg\n" "$@"
33 } >&2
35 error() {
36 local mesg=$1; shift
37 printf "==> ERROR: $mesg\n" "$@"
38 } >&2
40 die() {
41 error "$@"
42 exit 1
45 # reads a list of files on stdin and prints out deletion candidates
46 pkgfilter() {
47 # there's whitelist and blacklist parameters passed to this
48 # script after the block of awk.
50 awk -v keep="$1" -v scanarch="$2" '
51 function parse_filename(filename, parts, count, i, pkgname, arch) {
53 count = split(filename, parts, "-")
55 i = 1
56 pkgname = parts[i++]
57 while (i <= count - 3) {
58 pkgname = pkgname "-" parts[i++]
61 arch = substr(parts[count], 1, index(parts[count], ".") - 1)
63 # filter on whitelist or blacklist
64 if (wlen && !whitelist[pkgname]) return
65 if (blen && blacklist[pkgname]) return
67 if ("" == packages[pkgname,arch]) {
68 packages[pkgname,arch] = filename
69 } else {
70 packages[pkgname,arch] = packages[pkgname,arch] SUBSEP filename
74 BEGIN {
75 # create whitelist
76 wlen = ARGV[1]; delete ARGV[1]
77 for (i = 2; i < 2 + wlen; i++) {
78 whitelist[ARGV[i]] = 1
79 delete ARGV[i]
82 # create blacklist
83 blen = ARGV[i]; delete ARGV[i]
84 while (i++ < ARGC) {
85 blacklist[ARGV[i]] = 1
86 delete ARGV[i]
89 # read package filenames
90 while (getline < "/dev/stdin") {
91 parse_filename($0)
94 for (pkglist in packages) {
95 # idx[1,2] = idx[pkgname,arch]
96 split(pkglist, idx, SUBSEP)
98 # enforce architecture match if specified
99 if (!scanarch || scanarch == idx[2]) {
100 count = split(packages[idx[1], idx[2]], pkgs, SUBSEP)
101 for(i = 1; i <= count - keep; i++) {
102 print pkgs[i]
106 }' "${@:3}"
109 size_to_human() {
110 awk -v size="$1" '
111 BEGIN {
112 suffix[1] = "B"
113 suffix[2] = "KiB"
114 suffix[3] = "MiB"
115 suffix[4] = "GiB"
116 suffix[5] = "TiB"
117 count = 1
119 while (size > 1024) {
120 size /= 1024
121 count++
124 sizestr = sprintf("%.2f", size)
125 sub(/\.?0+$/, "", sizestr)
126 printf("%s %s", sizestr, suffix[count])
130 runcmd() {
131 if (( needsroot )); then
132 msg "Privilege escalation required"
133 if sudo -v &>/dev/null && sudo -l &>/dev/null; then
134 sudo "$@"
135 else
136 printf '%s ' 'root'
137 su -c "$(printf '%q ' "$@")"
139 else
140 "$@"
144 summarize() {
145 local -i filecount=$1; shift
146 local seenarch= seen= arch= name=
147 local -r pkg_re='(.+)-[^-]+-[0-9]+-([^.]+)\.pkg.*'
149 if (( delete )); then
150 printf -v output 'finished: %d packages removed' "$filecount"
151 elif (( move )); then
152 printf -v output "finished: %d packages moved to \`%s'" "$filecount" "$movedir"
153 elif (( dryrun )); then
154 if (( verbose )); then
155 msg "Candidate packages:"
156 while read -r pkg; do
157 if (( verbose >= 3 )); then
158 [[ $pkg =~ $pkg_re ]] && name=${BASH_REMATCH[1]} arch=${BASH_REMATCH[2]}
159 if [[ -z $seen || $seenarch != "$arch" || $seen != "$name" ]]; then
160 seen=$name seenarch=$arch
161 printf '%s (%s):\n' "$name" "$arch"
163 printf ' %s\n' "$pkg"
164 elif (( verbose >= 2 )); then
165 printf "$PWD/%s$delim" "$pkg"
166 else
167 printf "%s$delim" "$pkg"
169 done < <(printf '%s\n' "$@" | pacsort)
171 printf -v output 'finished dry run: %d candidates' "$filecount"
174 printf '\n' >&2
175 msg "$output (diskspace saved: %s)" "$(size_to_human "$totalsaved")"
178 usage() {
179 cat <<EOF
180 usage: $myname <operation> [options] [targets...]
182 $myname is a flexible pacman cache cleaning utility, which has numerous
183 options to help control how much, and what, is deleted from any directory
184 containing pacman package tarballs.
186 Operations:
187 -d perform a dry run, only finding candidate packages.
188 -m <movedir> move candidate packages to 'movedir'.
189 -r remove candidate packages.
191 Options:
192 -a <arch> scan for 'arch' (default: all architectures).
193 -c <cachedir> scan 'cachedir' for packages (default: @localstatedir@/cache/pacman/pkg).
194 -f apply force to mv(1) and rm(1) operations.
195 -h display this help message.
196 -i <pkgs> ignore 'pkgs', which is a comma separated. Alternatively,
197 specify '-' to read package names from stdin, newline delimited.
198 -k <num> keep 'num' of each package in 'cachedir' (default: 3).
199 -u target uninstalled packages.
200 -v increase verbosity. specify up to 3 times.
201 -z use null delimiters for candidate names (only with -v and -vv)
206 version() {
207 printf "%s %s\n" "$myname" "$myver"
208 echo 'Copyright (C) 2011 Dave Reisner <dreisner@archlinux.org>'
211 if (( ! UID )); then
212 error "Do not run this script as root. You will be prompted for privilege escalation."
213 exit 42
216 # TODO: remove this workaround and use a sane command line parser (like the
217 # parse_options library from scripts/) here
218 if [[ $1 = -@(h|-help) ]]; then
219 usage
220 exit 0
221 elif [[ $1 = -@(V|-version) ]]; then
222 version
223 exit 0
226 while getopts ':a:c:dfi:k:m:rsuvz' opt; do
227 case $opt in
228 a) scanarch=$OPTARG ;;
229 c) cachedir=$OPTARG ;;
230 d) dryrun=1 ;;
231 f) cmdopts=(-f) ;;
232 i) if [[ $OPTARG = '-' ]]; then
233 [[ ! -t 0 ]] && IFS=$'\n' read -r -d '' -a ign
234 else
235 IFS=',' read -r -a ign <<< "$OPTARG"
237 blacklist+=("${ign[@]}")
238 unset i ign ;;
239 k) keep=$OPTARG
240 if [[ -z $keep || -n ${keep//[0-9]/} ]]; then
241 die 'argument to option -k must be a non-negative integer'
242 else
243 keep=$(( 10#$keep ))
244 fi ;;
245 m) move=1 movedir=$OPTARG ;;
246 r) delete=1 ;;
247 u) IFS=$'\n' read -r -d '' -a ign < <(pacman -Qq)
248 blacklist+=("${ign[@]}")
249 unset ign ;;
250 v) (( ++verbose )) ;;
251 z) delim='\0' ;;
252 :) die "option '--%s' requires an argument" "$OPTARG" ;;
253 ?) die "invalid option -- '%s'" "$OPTARG" ;;
254 esac
255 done
256 shift $(( OPTIND - 1 ))
258 # remaining args are a whitelist
259 whitelist=("$@")
261 # sanity checks
262 case $(( dryrun+delete+move )) in
263 0) die "no operation specified (use -h for help)" ;;
264 [^1]) die "only one operation may be used at a time" ;;
265 esac
267 [[ -d $cachedir ]] ||
268 die "cachedir \`%s' does not exist or is not a directory" "$cachedir"
270 [[ $movedir && ! -d $movedir ]] &&
271 die "move-to directory \`%s' does not exist or is not a directory" "$movedir"
273 if (( move || delete )); then
274 # make it an absolute path since we're about to chdir
275 [[ ${movedir:0:1} != '/' ]] && movedir=$PWD/$movedir
276 [[ ! -w $cachedir || ( $movedir && ! -w $movedir ) ]] && needsroot=1
279 # unlikely that this will fail, but better make sure
280 cd "$cachedir" >/dev/null || die "failed to chdir to \`%s'" "$cachedir"
282 # note that these results are returned in an arbitrary order from awk, but
283 # they'll be resorted (in summarize) iff we have a verbosity level set.
284 IFS=$'\n' read -r -d '' -a candidates < \
285 <(printf '%s\n' *.pkg.tar?(.+([^.])) | pacsort |
286 pkgfilter "$keep" "$scanarch" \
287 "${#whitelist[*]}" "${whitelist[@]}" \
288 "${#blacklist[*]}" "${blacklist[@]}")
290 if (( ! ${#candidates[*]} )); then
291 msg 'no candidate packages found for pruning'
292 exit 1
295 # grab this prior to signature scavenging
296 pkgcount=${#candidates[*]}
298 # copy the list, merging in any found sigs
299 for cand in "${candidates[@]}"; do
300 candtemp+=("$cand")
301 [[ -f $cand.sig ]] && candtemp+=("$cand.sig")
302 done
303 candidates=("${candtemp[@]}")
304 unset candtemp
306 # do this before we destroy anything
307 totalsaved=$(@SIZECMD@ "${candidates[@]}" | awk '{ sum += $1 } END { print sum }')
309 # crush. kill. destroy.
310 (( verbose )) && cmdopts+=(-v)
311 if (( delete )); then
312 runcmd rm "${cmdopts[@]}" "${candidates[@]}"
313 elif (( move )); then
314 runcmd mv "${cmdopts[@]}" "${candidates[@]}" "$movedir"
317 summarize "$pkgcount" "${candidates[@]}"
319 # vim: set ts=2 sw=2 noet: