89

I have a script that extracts a tar.gz-file to a specified subdirectory mysubfolder:

mkdir mysubfolder; tar --extract --file=sourcefile.tar.gz --strip-components=1 --directory=mysubfolder;

Is there any equivalent way of doing this with a zip-file?

Run5k
  • 15,723
  • 24
  • 49
  • 63
Fredrik
  • 1,015
  • 1
  • 7
  • 6

4 Answers4

40

As Mathias said, unzip has no such option. But a one-liner bash script can do the job.

Problem is: the best approach depends on your archive layout. A solution that assumes a single top-level dir will fail miserably if the content is directly in the archive root: think about an archive containing /a/foo, /b/foo, /foo, and the chaos of stripping /a and /b.

And the same fail happens with tar --strip-component. There is no one-size-fits-all solution.

So, to strip the root dir, assuming there is one (and only one):

unzip -d "$dest" "$zip" && f=("$dest"/*) && mv "$dest"/*/* "$dest" && rmdir "${f[@]}"

Just make sure that second-level files/dirs do not have the same name of the top-level parent (for example, /foo/foo). But /foo/bar/foo and /foo/bar/bar are ok. If they do, or you just want to be safe, you can use a temp dir for extraction:

temp=$(mktemp -d) && unzip -d "$temp" "$zip" && mkdir -p "$dest" &&
mv "$temp"/*/* "$dest" && rmdir "$temp"/* "$temp"

If you're using Bash, you can test if top level is a single dir or not using:

f=("$temp"/*); (( ${#f[@]} == 1 )) && [[ -d "${f[0]}" ]] && echo "Single dir!"

Speaking of Bash, you should turn on dotglob to include hidden files, and you can wrap everything in a single, handy function:

# unzip featuring an enhanced version of tar's --strip-components=1
# Usage: unzip-strip ARCHIVE [DESTDIR] [EXTRA_cp_OPTIONS]
# Derive DESTDIR to current dir and archive filename or toplevel dir
unzip-strip() (
    set -eu
    local archive=$1
    local destdir=${2:-}
    shift; shift || :
    local tmpdir=$(mktemp -d)
    trap 'rm -rf -- "$tmpdir"' EXIT
    unzip -qd "$tmpdir" -- "$archive"
    shopt -s dotglob
    local files=("$tmpdir"/*) name i=1
    if (( ${#files[@]} == 1 )) && [[ -d "${files[0]}" ]]; then
        name=$(basename "${files[0]}")
        files=("$tmpdir"/*/*)
    else
        name=$(basename "$archive"); name=${archive%.*}
        files=("$tmpdir"/*)
    fi
    if [[ -z "$destdir" ]]; then
        destdir=./"$name"
    fi
    while [[ -f "$destdir" ]]; do destdir=${destdir}-$((i++)); done
    mkdir -p "$destdir"
    cp -ar "$@" -t "$destdir" -- "${files[@]}"
)

Now put that in your ~/.bashrc and you'll never have to worry about it again. Simply use as:

unzip-strip sourcefile.zip [mysubfolder] [OPTIONS]

This little beast will:

  • Create mysubfolder for you if it does not exist
  • Automatically detect if if your zip archive contains a single top-level directory and handle the extraction accordingly.
  • mysubfolder is optional. If blank it will extract to a subdir of the current directory (not necessarily the archive directory!), named after:
    • The single top-level directory in the archive, if there is one
    • or archive file name, without the (presumably .zip) extension
  • If destination path, given or derived, already exists as a file, increment the name until a suitable one is found (new path or existing directory).
  • By default this will:
    • be silent
    • overwrite any existing files
    • preserve links and attributes (mode, timestamps, etc)
  • You can pass extra OPTIONS to cp. Useful options are:
    • -v|--verbose: output each copied file, just like unzip does
    • -n|--no-clobber: do not overwrite existing files
    • -u|--update: only overwrite files that are newer than the destination
  • Such extra OPTIONS are the 3rd argument onward. Craft a proper argument parser if needed.
  • It will use double the extracted disk space during the operation, due to "extract to temp dir and copy" approach. No way around this without losing some of its flexibility/features.
MestreLion
  • 2,467
  • 4
  • 27
  • 21
  • This will not unzip into an existing directory structure as I'd hoped (I tried to use . in place of mysubfolder). I ended up just unzipping (unzip zip-with-top-dir.zip) and then copying (cp -rv extracted-top-zip-dir/* .). – catgofire Oct 25 '16 at 21:14
  • Your good design saved me from having to re-download 2 _big_ archives. I tarted in `zipsdir`. `$ ls` #output: `ar1.zip ar2.zip ar3.zip` ; `$ dest="."; thezip="ar1.zip"; unzip -qq -d "$dest" "$thezip" && f=("$dest"/*) && mv "$dest"/*/* "$dest" && rmdir "${f[@]}"` #output: `rmdir: failed to remove './ar1.zip': Not a directory` \n `rmdir: failed to remove './ar1_file1': Not a directory` \n `rmdir: failed to remove './ar2.zip': Not a directory` \n `rmdir: failed to remove './ar3.zip': Not a directory` ; #RESULT: `$ ls` #output: `ar1.zip ar1_file1 ar1_subdir1 ar1_subdir2 ar2.zip ar3.zip` Thx4 rmdir – bballdave025 Jun 30 '20 at 00:28
  • @catgofire: now it does ;-) – MestreLion Sep 16 '20 at 20:06
13

You can use -j to junk paths (do not make directories). This is only recommended for somewhat common single-level archives. Archives with multi level directory structures will be flattened - this might even lead to name clashes for the files to extract.

From the man page of unzip:

   -j     junk  paths.   The  archive's directory structure is not recreated; all files are deposited in the
          extraction directory (by default, the current one).
Pedro Rodrigues
  • 239
  • 2
  • 5
  • 1
    This answer would've been more useful to me if it also explained how to make it output stuff to a named directory instead of the current directory. – Boris Verkhovskiy Mar 26 '22 at 20:17
8

As others have noted, unzip does not support this. However, bsdtar can extract zip files as well.

bsdtar xvf app.zip --strip-components=1 -C /opt/some-app

bsdtar can also extract streams (which unzip does not support).

curl -sSL https://... | bsdtar xvf - --strip-components=1 -C /opt/some-app
fnkr
  • 730
  • 8
  • 12
  • This is great, however there is a slight flaw in that `bsdtar` does not mark executables as executable, but `unzip` does. I tried `--fflags` and `--insecure` but it made no difference. Testing with [this zip](https://github.com/Z3Prover/z3/releases/download/z3-4.12.1/z3-4.12.1-x64-glibc-2.35.zip). – Timmmm Jan 31 '23 at 17:16
6

I couldn’t find such an option in the manual pages for unzip, so I’m afraid this is impossible. :(

However, (depending on the situation) you could work around it. For example, if you’re sure the only top-level directory in the zip file is named foo- followed by a version number, you could do something like this:

cd /tmp
unzip /path/to/file.zip
cd foo-*
cp -r . /path/to/destination/folder
Mathias Bynens
  • 2,441
  • 5
  • 31
  • 39
  • 1
    Nice approach, but a bit incomplete: you will still have foo* dir with the full extracted content. – MestreLion Mar 28 '13 at 05:44
  • Yes, I didn’t add `rm -rf foo-*` on purpose as that’s potentially dangerous. What if there already was a folder named `foo-bar`? Note that the extraction is being done within the `/tmp` folder, which gets emptied automatically every now and then. – Mathias Bynens Mar 28 '13 at 10:59
  • That's why I chained operations using `&&`: a given step only happens if the previous step was successful, so the last one (the `rm`) only runs if *all* steps completed with no error. – MestreLion Mar 28 '13 at 15:21
  • 4
    That's also why one should never user `/tmp/some-hardcoded-folder-name` as a temp folder, but instead should use `mktemp` for that: it guarantees there will be no such existing folder. Check my answer below. – MestreLion Mar 28 '13 at 15:23