11

A common scenario is having a zip file in a directory with other work:

me@work ~/my_working_folder $ ls
downloaded.zip  workfile1  workfile2  workfile3

I want to unzip downloaded.zip, but I don't know if it will make a mess or if it nicely creates its own directory. My ongoing workaround is to create a temporary folder and unzip it there:

me@work ~/my_working_folder $ mkdir temp && cp downloaded.zip temp && cd temp
me@work ~/my_working_folder/temp $ ls
downloaded.zip
me@work ~/my_working_folder/temp $ unzip downloaded.zip 
Archive:  downloaded.zip
   creating: nice_folder/

This prevents my_working_folder from being populated with lots of zip file contents.

My question is: Is there a better way to determine if a zip file contains only one folder before unzipping?

user1717828
  • 3,542
  • @Christopher, could you turn it into a complete answer? AFAICT, the flags just display the file list but require manual inspection of (possibly many, many) files to see if they're all in a subdirectory. The test case I just ran it on actually overfilled the terminal history. – user1717828 Oct 20 '16 at 16:47
  • 2
    This reminds me of The Unarchiver for Mac OS X. One of the options is, "Create a new folder for the extracted files," with the choices: "Always," "Never," or "Only if there is more than one top-level item." :) – Wildcard Oct 20 '16 at 21:09

5 Answers5

7

Well, you could just extract into a subdirectory unconditionally and get rid of it afterwards if it ends up containing only a single item.

But why go for a sane and easy solution (courtesy of ilkkachu), when you can use awk instead? :)

sunzip ()
{
    if [ $# -ne 1 ] || ! [ -f "$1" ]
    then
        printf '%s\n' "Expected a filename as the first (and only) argument. Aborting."
        return 1
    fi

    extract_dir="."

    # Strip the leading and trailing information about the zip file (leaving
    # only the lines with filenames), then check to make sure *all* filenames
    # contain a /.
    # If any file doesn't contain a / (i.e. is not located in a directory or is
    # a directory itself), exit with a failure code to trigger creating a new
    # directory for the extraction.
    if ! unzip -l "$1" | tail -n +4 | head -n -2 | awk 'BEGIN {lastprefix = ""} {if (match($4, /[^/]+/)) {prefix=substr($4, RSTART, RLENGTH); if (lastprefix != "" && prefix != lastprefix) {exit 1}; lastprefix=prefix}}'
    then
        extract_dir="${1%.zip}"
    fi

    unzip -d "$extract_dir" "$1"
}

Quick'n'dirty. Works with InfoZIP's unzip v6.0.
You might want to adapt it to your needs, e.g. to accept or automatically use additional parameters for unzip, or to use a different name for the extraction subdirectory (which is currently determined from the name of the zip file).


Oh, and I just noticed that this workaround correctly deals with the two most common situations (1. ZIP file contains a single directory with contents, 2. ZIP file contains lots of individual files and/or directories), but doesn't create a subdirectory when the ZIP file's root contains multiple directories but no files…

Edit: Fixed. The awk script now stores the first component ("prefix") of each path contained in the ZIP file, and aborts as soon as it detects a prefix that differs from the previous one. This catches both multiple files and multiple directories (since both are bound to have different names), while ignoring ZIP files where everything is contained in the same subdirectory.

n.st
  • 8,128
  • 3
    Note that if the file names contain whitespace, this will only find the slashes in the part before the first space. Though it'll just create the useless extra directory then. – ilkkachu Oct 20 '16 at 16:11
  • Ugh, I hate whitespace issues. You're right of course, @ilkkachu. – n.st Oct 20 '16 at 16:13
  • Well done, though. I would've gone with just unconditionally unpacking into a new directory, and counting the files that appeared. Possibly renaming the directory as necessary in the end. Doesn't matter with zip files since they actually have an index, but with tar files it would save re-reading the whole archive.. – ilkkachu Oct 20 '16 at 16:13
  • 2
    @ilkkachu Why go for a sane and easy solution when one can use awk instead? :) – n.st Oct 20 '16 at 16:15
5

From the manual...

[-d exdir]

An optional directory to which to extract files. By default, all files and subdirectories are recreated in the current directory; the -d option allows extraction in an arbitrary directory (always assuming one has permission to write to the directory). This option need not appear at the end of the command line; it is also accepted before the zipfile specification (with the normal options), immediately after the zipfile specification, or between the file(s) and the -x option. The option and directory may be concatenated without any white space between them, but note that this may cause normal shell behavior to be suppressed. In particular, -d ~ (tilde) is expanded by Unix C shells into the name of the user's home directory, but -d~ is treated as a literal subdirectory ~ of the current directory.

So...

unzip -d new_dir zipfile.zip

This creates a directory, new_dir, and extracts the archive within it, which avoids the potential mess every time even without looking first. It is also very useful to look at man unzip. More help for manual pages.

Christopher
  • 15,911
5

Instead of checking the archive contents before extracting it, you could also extract it using unar which will provide the behaviour you’re after: by default, if the archive contains a single top-level directory, unar extracts it as-is, otherwise it creates a directory named after the archive and extracts it there. You can force the creation of the directory in all cases with the -d option.

unar also has the advantage of supporting a wide variety of archive formats.

Stephen Kitt
  • 434,908
2

You can use my script using mv.

mkdir downloaded

mv downloaded.zip downloaded && cd downloaded

unzip downloaded.zip && cd

You'll create a folder (downloaded) and you'll unzip it in the folder you created.

Shader
  • 23
1

This is a nice bash one liner using default unzip:

 while read -r line; do (unzip -d "$(basename "$line" .zip)" "$line"); done < <(find . | grep '.zip')

Run this in the directory that has the zip files for unzipping.

  • The < < feeds the results of the find . | grep '.zip' into the while loop.
  • The filenames returned by find are then stored as line
  • The while loop iterates over the file names in line and feeds them into the do
  • This do creates a directory using the basename of the filename from $line
    • The .zip extension is specified as the text to be stripped off the string
    • This leaves only the name of the file without an extension (basename!)
  • The -d argument uses the basename of the .zip to create the new directory
    • /folder/archive.zip then becomes /folder/archive/unzipped_file.txt etc.
    • This is a lot like Windows / 7zip "extract to" option
  • The $line variable is then used as the target to unzip.

This one liner can be adapted for all kinds of things too! Moving files, zipping them, copying them... Find is recursive so it's super handy!

Shrout1
  • 451