0

I make a script javaFilesConcatenator using cat and sed, that concatenate multiple .java files:

#!/bin/bash
########################################################################################################################
# This file create a "MainFileName.java.output" file concatenating all java file in this directory.
# Run this script as '$ ./javaFilesConcatenator MainFileName' when "MainFileName.java" contains the main method.
########################################################################################################################

cat *.java | sed -e 's/public class/class/g;s/public interface/interface/g' >$1.java.output
sed -i "s/class $1/public class $1/g" $1.java.output

For running this script I take main class name as input argument from user, because of main class should be public and others aren't. I replace all public class ClassName to class ClassName except the main class.

Now, I want to modify my script so that it is more friendly to user, so that it detect the main class without providing main class name. I guess it would be possible, because of main class is different from others. It contains a main method.

And, another requirement is that it has to place all import(i.g. import java.util.Scanner;) statement at the beginning of the document by cutting from different position(each file has some import statement at beginning, after concatenating these are not all at beginning.) of the output file.

Another, optional requirement is it removes all package com.alhelal.texpad; like line but not comments having word package.

How, can I fulfill my requirements?

  • Do you have one class per file? Then you can grep for void main() and handle that file differently. For a verifyable script you should provide a minimal example including all possible problems. – Philippos Oct 26 '17 at 5:40
1

This sed script should do all steps, but I couldn't test it, because I don't have test data:

sed '/^[^/]*package/d
     /import  *java\./{p;d;}
     H;/void main()/{g;s/.*\n[[:space:]]*public/& public/;h;}
     $!d;g;s/public class/class/g' *.java

The main concepts of the script:

  • You can pass multiple filenames to sed; they are read one after another, line by line. sed will not know when one file ands and another starts, but we don't need that here anyhow.
  • As sed works line by line, we need some memory to store lines that we need later. That's required for resorting the file (all imports at the top of the output). In sed, this memory is the hold space; think of it like a clip board. While the pattern space gets overwritten with the new line with every cycle of the script, the hold buffer remains untouched. You can copy the pattern space to the hold space with h or copy back with g or exchange both with x. With H the contents of the pattern space are appended to the hold space (with a newline in between), with g vice versa. Thus, lines we want at the top of the output are printed immediately (with p), while all other lines are appended to the hold space. After we reach the end of all lines, the lines collected in the hold space can be processed.
  • All public should be removed except one. I do this with a trick: As soon as I meet the main method, I know that the last class is the one to remain public, so I need to mark it as such. But how? The trick: I add a second public there, so we get public class fooClass ... public public class thisIsTheMainClass ... public class barClass, so if we later replace all public class with class, the main class will still have one public attribute left. The adding of the second public takes places in the third line (we need to copy the hold space to the pattern space to work with it and later copy it back).
  • One thing to keep in mind: You can preceed sed commands with an "address", so they are executed only for lines that match the address. Addresses can be a regexp pattern enclosed in slahes to match the line; this is used in the first three lines of the script. Or it can be a line number or $ for the last line, like in the fourth line of the script, where the ! inverts the selected address, so the delete is executed for all lines but the last one. If the address is followed by commands enclosed with {}, all commands inside are executed only for this address, like in the second and third line of the script.

The script in detail:

  • /^[^/]*package/d deletes all lines with the word package and no slashes before. I hope this is what you want
  • /import *java\./{p;d;} prints all lines with java imports immediately and stops further processing.
  • H collects all other lines in the hold space instead of printing them. This is how we collect all imports at the beginning of the output
  • /void main()/{g;s/.*\n[[:space:]]*public/& public/;h;}: When meeting the main method, g transfers the lines collected so far to the pattern space, the s command searches for the last occurence of public at the beginning of a line and appends another public. The h transfers the modified buffer back to the hold space
  • $!d If this not the last line, no further actions
  • g;s/public class/class/g' moves all not-import lines we collected to the pattern space and removes all public from classes. As we added a second public to the main class, the main class will remain public

Note that you can usually break scripts like this with certain input (keywords in identifiers or commentaries).

  • thank you, I like your effort. although you described, if you describe each option i.g p, d in {p;d;} it would be clear to me. Because this script is more sensitive and will guarantee my users that it doesn't make your. use this data – alhelal Oct 26 '17 at 9:30
  • p is print, d is delete. Those are described well in man sed. Your samples contain neither import lines nor a main method, as far as I can see. – Philippos Oct 26 '17 at 10:25
  • I did expand your samples at github (see pull requests) and checked the script with it. Seems to work like intended. Everything understood so far? – Philippos Oct 26 '17 at 14:42
  • I guess your solution correct, but not tell something confidently because I don't understand 3rd and 4th line. Would you describe why you collect in hold space(describe H and h), and why put two public in main what you do in last line. sorry, I didn't accept your answer, yet, but voted up your answer. – alhelal Oct 26 '17 at 15:51
  • I added more explanation, including some basic sed concepts. I hope it's more clear now. – Philippos Oct 27 '17 at 6:12
0

First, you want to find the main method, the file basename is the class name

CLASS=$(basename -s .java $(grep -l 'public static void main' *.java))

Then, extract all the imports, sort them, and use uniq. Assuming there are no wildcard imports that match other imports.

sed -n '/^[ \t]*import /p' *.java | sort | uniq > $CLASS.java.output

Then process Java files, extracting imports and packages:

grep -hv '^[ \t]*import \|^[ \t]*package' *.java | sed -e 's/public class/class/g;s/public interface/interface/g' >>$CLASS.java.output

another way, pure sed:

sed -e '/^[ \t]*\(import\|package\)/d;s/public class/class/g;s/public interface/interface/g' .java >>$CLASS.java.output

Then you want to revert your public class

sed -i "s/class $CLASS/public class $CLASS/g" $CLASS.java.output

Note that you do not have to use bash in your shebang (#!/bin/sh is good enough), because you do not have any bashisms, here.

Also note, you do not have to cat-pipe to sed, sed -e 's/.../.../' *.java is the same as cat *.java | sed -e 's/.../.../', however, you only have sed running in the first solution, not cat and sed - fewer processes is always better.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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