-
Notifications
You must be signed in to change notification settings - Fork 13
/
unpack
executable file
·297 lines (261 loc) · 8.17 KB
/
unpack
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
296
297
#!/bin/bash
#
# Unpacks one of several different types of archive to the current
# directory. If there is more than one entry in the top-level
# directory, automatically creates a new subdirectory and unpacks to
# that, in order to avoid filling the current directory with crap.
# In every case, informs the user where the unpacked contents ended up.
#
# Adam Spiers <shell-hacks@adamspiers.org>
me="`basename $0`"
warn () {
echo >&2 "$*"
}
die () {
warn "$*"
exit 1
}
abort () {
die "$*; aborting."
}
interactive_mode () {
[ -t 1 ]
}
progress () {
if interactive_mode; then
echo "$@"
fi
}
usage () {
cat <<EOF >&2
Usage: $me ARCHIVE [ARCHIVE ...]
Unpacks the given archive(s). Supported formats include:
tar (incl. compressed with gzip, bzip2, xz), zip, 7z, rar,
apk, xpi, jar, class, gem, box, rpm, egg
Guarantees that extraction will always be to a newly-created directory
rather than polluting an existing one with multiple entries.
If STDOUT is not a tty, only the name of the new directory containing
the unpacked archive will be output to STDOUT. This can then be
captured and reused programmatically in other scripts.
EOF
exit 1
}
parse_options () {
if [ "$1" == -h ] || [ "$1" == --help ] || [ -z "$1" ]; then
usage
fi
ARGV=( "$@" )
}
unpack () {
archive_path="$1"
archive="`basename \"$1\"`"
get_base_dest_dir
extract
cd "$saved_dir"
ensure_single_dir
progress "Look inside $dest_dir"
if ! [ -t 1 ]; then
echo "$dest_dir"
fi
}
# Calculate a destination directory for the extracted archive which is
# based on the basename of the archive filename (i.e. with the suffix
# stripped off).
#
# We do this because if we're extracting an archive file called
# "server27-logs.tar.xz" then the user would probably prefer the
# resulting top-level directory to be called "server27-logs" which is
# more informative than just "var", assuming that the archive only
# contained files under the var/log/ hierarchy. This is especially
# helpful if the user is also unpacking server28-logs.tar.xz and
# server29-logs.tar.xz at the same time, since they can't all be
# extracted to the same "var" directory.
#
# On the other hand, we won't always use this destination directory.
# Sometimes the user prefers to use the singleton top-level directory
# within the archive. For example if we were extracting an archive
# file called "linux.tar.bz2", we would prefer the resulting top-level
# directory to be called "linux-4.9-rc5" which is more informative
# than just "linux".
#
# Unfortunately we can't always be sure which the user would prefer.
# But a fairly decent heuristic is to pick the longer of the two
# options, since that will usually be more informative and/or more
# likely to uniquely identify the extracted contents.
get_base_dest_dir () {
base_dest_dir=
case "$archive" in
*.tar|*.tgz|*.tbz|*.txz|*.zip|*.Zip|*.ZIP|\
*.7z|*.apk|*.rar|*.xpi|*.jar|*.sar|*.class|\
*.job|*.pylib|*.gem|*.box|*.rpm|*.egg|*.cpio)
base_dest_dir="${archive%.*}"
;;
*.tar.gz)
base_dest_dir="${archive%.tar.gz}"
;;
*.tar.bz2)
base_dest_dir="${archive%.tar.bz2}"
;;
*.tar.xz)
base_dest_dir="${archive%.tar.xz}"
;;
*)
abort "$archive does not have a supported file extension"
;;
esac
}
# Runs external commands, redirecting output as required.
run () {
if interactive_mode; then
"$@"
else
"$@" >&2
fi
}
extract () {
if ! tmpdir=`mktemp -d "$base_dest_dir.tmp.XXXXXXXX"`; then
die "mktemp failed: $!"
fi
case "$archive" in
*.tar)
run tar -C "$tmpdir" -xvf "$archive_path"
;;
*.tar.gz|*.tgz|*.box)
run tar -C "$tmpdir" -zxvf "$archive_path"
;;
*.tar.bz2|*.tbz)
run tar -C "$tmpdir" -jxvf "$archive_path"
;;
*.tar.xz|*.txz)
run tar -C "$tmpdir" -Jxvf "$archive_path"
;;
*.zip|*.ZIP|*.Zip|*.xpi|*.jar|*.class|*.sar|*.job|*.pylib|*.apk|*.egg)
run unzip -d "$tmpdir" "$archive_path"
;;
*.7z)
run 7za x -o"$tmpdir" "$archive_path"
;;
*.gem)
run gem unpack --target="$tmpdir" "$archive_path"
;;
*.rar)
archive_abspath=$( abs "$archive_path" )
pushd "$tmpdir"
run unrar x "$archive_abspath"
popd
;;
*.cpio)
cpio_name="$(basename -s .cpio $archive_path)"
unpack_cpio "$archive_path" "$cpio_name" "$tmpdir"
;;
*.rpm)
rpm_name="$(basename -s .rpm $archive_path)"
cpio="$rpm_name.cpio"
progress -n "Extracting cpio to $tmpdir/$cpio ... "
rpm2cpio "$rpm" > "$tmpdir/$cpio"
progress "done."
unpack_cpio "$tmpdir" "$cpio" "$tmpdir/$rpm_name"
rm "$tmpdir/$cpio"
;;
*)
abort "$archive is not a supported archive format"
;;
esac
if [ $? != 0 ]; then
abort "Unpack of $archive_path failed"
fi
}
# Takes a cpio $cpio currently sitting in $cpio_dir and unpacks it into
# $dest
unpack_cpio () {
cpio_dir="$1"
cpio="$2"
dest_dir="$3"
mkdir "$tmpdir/$cpio_name"
pushd "$tmpdir/$cpio_name" >/dev/null
progress "Unpacking contents of $cpio into $tmpdir ... "
cpio -id < ../"$cpio"
ret=$?
if [ $ret != 0 ]; then
popd
return $ret
fi
progress "done."
popd >/dev/null
return 0
}
ensure_single_dir () {
num_dirents=$( ls -A "$tmpdir" | wc -l )
if [ "$num_dirents" -eq 0 ]; then
die "$archive was empty? Aborting"
fi
if [ "$num_dirents" -gt 1 ]; then
# Naughty archive creator! Would cause a mess if unpacked to cwd.
progress "$archive had more than one top-level entry"
unpacked_dir="$tmpdir"
else
top_dir=$( ls -A "$tmpdir" )
progress "$archive is clean; everything under a single top-level directory $top_dir/"
unpacked_dir="$tmpdir/$top_dir"
if [ -e "$top_dir" ]; then
warn "$top_dir already exists; won't unpack into that."
# Try to unpack to base destination
else
# We can either keep the existing top-level directory, or
# try to rename to the base destination directory. As per
# the lengthy comment above, let's try a heuristic which
# chooses the longer of the two options.
if [ "${#top_dir}" -gt "${#base_dest_dir}" ]; then
if mv "$unpacked_dir" .; then
dest_dir="$top_dir"
rmdir "$tmpdir"
return
else
abort "mv $unpacked_dir . failed"
fi
fi
fi
fi
# If we got this far, we decided not to keep the top-level
# directory from within the archive, so try to rename to the base
# destination directory.
calc_dest_dir
rename_to_dest_dir
}
calc_dest_dir () {
if ! [ -e "$base_dest_dir" ]; then
# safe to use base destination directory
dest_dir="$base_dest_dir"
else
warn "Destination $base_dest_dir already exists"
dest_dir="$archive.unpacked"
if [ -e "$dest_dir" ]; then
warn "$dest_dir also already exists"
# Couldn't find a better place to move unpacked directory to, so
# leave it where it already is.
dest_dir="$unpacked_dir"
fi
fi
}
rename_to_dest_dir () {
if [ "$unpacked_dir" != "$dest_dir" ]; then
#progress "mv $unpacked_dir $dest_dir OK"
if ! mv "$unpacked_dir" "$dest_dir"; then
abort "mv $unpacked_dir $dest_dir failed"
fi
[ -d "$tmpdir" ] && rmdir "$tmpdir"
fi
}
main () {
parse_options "$@"
saved_dir="`pwd`"
for archive in "${ARGV[@]}"; do
if ! [ -e "$archive" ]; then
abort "$archive does not exist"
fi
done
for archive in "${ARGV[@]}"; do
unpack "$archive"
done
}
main "$@"