23
\$\begingroup\$

Sometimes I need to search for dependencies in the classpath entries in the manifest of jar files. A jar manifest with classpaths looks like this:

Manifest-Version: 1.0
Created-By: 1.6.0_29 (Sun Microsystems Inc.)
Class-Path: spring-aop-3.2.0.jar spring-aspects-3.2.0.jar spring-beans
 -3.2.0.jar spring-context-3.2.0.jar spring-context-support-3.2.0.jar
 spring-core-3.2.0.jar spring-expression-3.2.0.jar spring-instrument-3
 .2.0.jar spring-instrument-tomcat-3.2.0.jar spring-jdbc-3.2.0.jar spr
 ing-jms-3.2.0.jar spring-orm-3.2.0.jar spring-oxm-3.2.0.jar spring-st
 ruts-3.2.0.jar spring-test-3.2.0.jar spring-tx-3.2.0.jar spring-web-3
 .2.0.jar spring-webmvc-3.2.0.jar spring-webmvc-portlet-3.2.0.jar

Searching for jars in a manifest is troublesome:

  1. Extract the META-INF/MANIFEST.MF file from the jar
  2. Re-join the split lines to make searching humanely possible

Step 2 is necessary, because of the line breaks. For example, I won't find "struts" in the above, because the word is broken in the middle.

I use this shell script to make this process easier, I call it jar-manifest-classpath.sh:

#!/bin/bash

if test -d "$TMPDIR"; then
    :
elif test -d "$TMP"; then
    TMPDIR=$TMP
elif test -d /var/tmp; then
    TMPDIR=/var/tmp
else
    TMPDIR=/tmp
fi
workdir=$TMPDIR/$(basename "$0")-work-$$

cleanup() {
    rm -fr "$workdir"
}

mkdir -p "$workdir"
trap 'cleanup' 1 2 3 15

for jar; do
    if ! test -f $jar; then
        echo warning: not a file: $jar
        continue
    fi
    [[ $jar = /* ]] || jar=$PWD/$jar
    (
        cd "$workdir" || exit 1
        jar xf "$jar"
        sed -ne '/^Class-Path:/,$p' META-INF/MANIFEST.MF | sed -e 's/^Class-Path: //' -e 's/^ //' | tr -d '\n' | tr ' ' '\n'
    )
done

cleanup

For a jar file with the manifest as above, this will output:

spring-aop-3.2.0.jar
spring-aspects-3.2.0.jar
spring-beans-3.2.0.jar
spring-context-3.2.0.jar
spring-context-support-3.2.0.jar
spring-core-3.2.0.jar
spring-expression-3.2.0.jar
spring-instrument-3.2.0.jar
spring-instrument-tomcat-3.2.0.jar
spring-jdbc-3.2.0.jar
spring-jms-3.2.0.jar
spring-orm-3.2.0.jar
spring-oxm-3.2.0.jar
spring-struts-3.2.0.jar
spring-test-3.2.0.jar
spring-tx-3.2.0.jar
spring-web-3.2.0.jar
spring-webmvc-3.2.0.jar
spring-webmvc-portlet-3.2.0.jar

This is much more pleasent, and perfectly grep-able.

Let me know if you have any ideas to improve or simplify! (The script is part of my collection on GitHub)

\$\endgroup\$
1
  • \$\begingroup\$ It would be much easier to do this in Java, since the standard library has classes to handle JAR manifest already. \$\endgroup\$
    – user20574
    Commented Dec 20, 2014 at 8:51

7 Answers 7

8
\$\begingroup\$

I would create the trap for cleanup before an action is done that needs cleanup, so change the sequence of commands. Also I would prefer signal names instead of numbers. If you add EXIT to the signal list then cleanup is called when the shell is exited, so you can remove the last line of your script (cleanup)

trap 'cleanup' EXIT SIGHUP SIGINT SIGQUIT SIGTERM
mkdir -p "$workdir"

If the script is called without argument or with-? a usage message should be displayed.

Warnings and error messages should be written to stderr(2) and not to stdout(1). This is Unix standard behaviour for a program. Otherwise you will not see the error message if you pipe the output through a grep command.

\$\endgroup\$
1
  • \$\begingroup\$ Excellent points, thank you! I wish you came to Code Review more often! \$\endgroup\$
    – janos
    Commented Feb 19, 2015 at 12:23
7
\$\begingroup\$

Bash may not be the best tool for the job, as evidenced by the difficulty with extraction and text processing. Although the jar command doesn't support extracting contents to standard output, Java does have built-in support for parsing manifest files. Consider writing a minimal shell wrapper to invoke a Java program:

public static void main(String[] args) throws IOException {
    Manifest mf = (args.length > 0) ? (new JarFile(args[0])).getManifest()
                                    : (new JarInputStream(System.in)).getManifest();
    String classPath = mf.getMainAttributes().getValue("Class-Path");
    if (classPath != null) {
        for (String dependency : classPath.split(" ")) {
            System.out.println(dependency);
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ A related example using this versatile approach is cited here. \$\endgroup\$
    – trashgod
    Commented Dec 20, 2014 at 3:43
6
\$\begingroup\$

You are working quite hard to create a temp folder, but you should instead just use the right tool: mktemp

workdir=$(mktemp -d)

That's the safe way to create it in a way that's not going to have race conditions.

Note that mktemp uses the value in $TMPDIR (if set) as the location in which to create the folder, so it will use whatver value you have set in the code before running mktemp

\$\endgroup\$
4
  • 1
    \$\begingroup\$ +1 mktemp is the right tool, but $TMPDIR is the default. so mktemp -dshould be used instead \$\endgroup\$
    – miracle173
    Commented Dec 18, 2014 at 18:33
  • \$\begingroup\$ Without -t mktemp doesn't use $TMPDIR though does it? \$\endgroup\$ Commented Dec 19, 2014 at 0:35
  • \$\begingroup\$ @EtanReisner - The man page says it does: " If TEMPLATE is not specified, use tmp.XXXXXXXXXX, and --tmpdir is implied." \$\endgroup\$
    – rolfl
    Commented Dec 19, 2014 at 0:42
  • \$\begingroup\$ For GNU/coreutils mktemp perhaps. Not for BSD mktemp (at least according to the docs I see). \$\endgroup\$ Commented Dec 19, 2014 at 2:38
5
\$\begingroup\$

When working with Jar files you should remember that they are just zip files with some restrictions.

As a result, there's no need to go through the whole extract-to-temp folder, and instead you can do the 'simple route':

unzip -q -c $jar META-INF/MANIFEST.MF

That will extract just the MANIFEST.MF file from the zip archive, and output it to standard output.

You can thus skip the whole temp-dir process entirely.

\$\endgroup\$
5
\$\begingroup\$

Although this script works fine with the given example and with classpath jars in general (jars containing only a manifest and no classes), it will be very inefficient with jars that contain classes. The culprit is the step that extracts the jar:

    [[ $jar = /* ]] || jar=$PWD/$jar
    (
        cd "$workdir" || exit 1
        jar xf "$jar"
        ^^^^ not so good

The jar xf step like this will extract the entire jar file, when the manifest file would be enough, like this:

        jar xf "$jar" META-INF/MANIFEST.MF

There is another obscure bug: although in the example the Class-Path field is the last, it's not guaranteed to be that way. For example this is a perfectly valid manifest, and some implementations may generate the manifest this way:

Manifest-Version: 1.0
Class-Path: spring-aop-3.2.0.jar spring-aspects-3.2.0.jar spring-beans
 -3.2.0.jar spring-context-3.2.0.jar spring-context-support-3.2.0.jar
 spring-core-3.2.0.jar spring-expression-3.2.0.jar
Created-By: 1.6.0_29 (Sun Microsystems Inc.)

The sed script in the question won't handle this case well, and the content of the Created-By line will end up in the output. Although this doesn't break your ability to grep for names in the classpath, the added lines are garbage, and clearly not as intended.


Instead of the tedious multiple if-else, a more elegant and equivalent way to find a suitable temp directory:

for TMPDIR in "$TMPDIR" "$TMP" /var/tmp /tmp; do
    test -d "$TMPDIR" && break
done

(This technique was suggested in another answer on my other related question, by @frostschutz)

\$\endgroup\$
0
3
\$\begingroup\$

I find this line:

sed -ne '/^Class-Path:/,$p' META-INF/MANIFEST.MF | sed -e 's/^Class-Path: //' -e 's/^ //' | tr -d '\n' | tr ' ' '\n'

Incredibly confusing to read. Surely, there has to be a simpler / less hacky way of doing this? I don't know bash but it jumps out as a code smell anyways...

\$\endgroup\$
3
\$\begingroup\$

JAR extraction

A lot of the headache comes from handling the temporary file. You have to find a temporary directory (which should be done using mktemp(1)) and clean up the mess afterwards, even in case of failure.

All you want is to process the contents of META-INF/MANIFEST.MF. Ideally, you should extract the text to standard output to be piped for further processing. Unfortunately, Java's jar command has no such capability.

Fortunately, the JAR File Specification says that the format is based on the popular ZIP file format. Therefore, you can use unzip(1), which does support a -c option to extract to standard output.

Text processing

Your code prints every word after Class-Path: until the end of the file. If other entries appear in MANIFEST.MF after the Class-Path line, their contents will be printed too!

I suggest writing a short AWK script to unfold the continuation lines. Then find the Class-Path entry, split it into one line per word, and omit the first line. You could try to do everything in one AWK script, but I recommend separating concerns.

Suggested solution

unzip -q -c "$jar" META-INF/MANIFEST.MF |

        # Unfold continuation lines
        # (This prepends an empty line, but that won't matter)
        awk '/^[^ ]/ { print BUF; BUF=$0 }
             END     { print BUF }
             /^ /    { sub(" *", ""); BUF = BUF $0 }' |

        # Get Class-Path entry
        grep '^Class-Path: ' |

        # Split into one line per word
        tr ' ' '\n' |

        # Omit the "Class-Path:" entry name itself
        tail +2
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.