The Source for Java Technology Collaboration
User: Password:



Osvaldo Pinali Doederlein's Blog

September 2007 Archives


No tabs? Yes, you ARE nuts!

Posted by opinali on September 26, 2007 at 10:28 AM | Permalink | Comments (21)

Quote: (WHAT? NO TABS? ... yes, no tabs ... ARE YOU NUTS? ... at times ... WHY? ... because tabs create a source display problem ... BUT IT WORKS FINE FOR ME IN VI/EMACS! ... yes, but what about everybody else?)

Tabs are only a problem if you mix tabs and spaces before the first non-whitespace char, or indent some lines with tabs and others with spaces, or if you use tabs after the first non-whitespace char. If you have discipline to use tabs the way God intended when He created the ASCII charset; that is, using tabs (and only tabs) for indentation only - then tabs have the advantage of allowing each programmer to pick his or her preferred indentation size, and none of the claimed disadvantages.

I have written a small Java utility that will scan a directory tree with text files, and fix tab/whitespace usage automatically. I wrote this utility after searching, and not finding, anything similar on the Internet - it seems there are some utilities (created from programmers from the dark side of the force) that will do the reverse operation of expanding tabs to spaces. I think only Jalopy would do what I wanted, but at the cost of forcing me to run a full reformat.

The code is very small, so I'm just including it in this blog; enjoy if you find it useful. It's released to the public domain. The program is easy to use and efficient, and I won't claim that any code is bug-free but I've been using it for a couple years without any issues. I fear however, that this code may be confused by filesystems with links (limitation of java.io), or arbitrary Unicode data (I assume 8-bit chars). And I didn't have time to add really cool features like IDE integrations. Clearly it's not a sufficiently advanced program to make me the next famous open source project leader, so I'm posting this here just to help other programmers who are in the Right side of the Tab Wars... yeah, you know what you're supposed to do now: just wait till all your coworkers go home next Friday night; checkout the entire company repositories' sources; run FixTabs from the root; commit. ;-)

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.regex.Pattern;

/**
 * Fixes usage of physical tabs in text files: Tabs are mandatory for indentation,
 * and only allowed for that purpose (forbidden after first non-blank in line).
 * 
 * @author osvaldo
 */
public class FixTabs
{
	private static int spacesPerTab = 4;
	private static final ByteArrayOutputStream baos = new ByteArrayOutputStream();
	private static final String[] defaultIncludes = { ".*\\.java", ".*\\.properties" };
	private static final String[] defaultExcludes =
	{
		"\\..*",     // .svn, Unix, Eclipse hidden directories
		"CVS",       // CVS
		"bin",       // Common output directory for Eclipse
		"dist",      // Common output directory for NetBeans
		"build",     // Common output directory for NetBeans
		"nbproject", // Common output directory for NetBeans
		"target",    // Common output directory for Maven
	};

	public static void main (final String[] args)
	{
		if (args.length > 3 ||
				(args.length > 0 && ("-?".equals(args[0]) || "-help".equals(args[1]))))
			help();
		
		final File root = new File(args.length < 1 ? "." : args[0]);
		
		if (args.length >= 2) try
		{
			spacesPerTab = Integer.parseInt(args[1]);
		}
		catch (NumberFormatException e)
		{
			help();
		}
		
		final ArrayList includeList = new ArrayList(); 
		final ArrayList excludeList = new ArrayList(); 

		for (int i = 2; i < args.length; ++i)
		{
			if (args[i].startsWith("+"))
				includeList.add(Pattern.compile(args[i].substring(1)));
			else if (args[i].startsWith("-"))
				excludeList.add(Pattern.compile(args[i].substring(1)));
			else
				help();
		}
		
		if (includeList.isEmpty()) for (final String p: defaultIncludes)
			includeList.add(Pattern.compile(p));
		
		if (excludeList.isEmpty()) for (final String p: defaultExcludes)
			excludeList.add(Pattern.compile(p));

		final int count = fixDirectory(root,
			includeList.toArray(new Pattern[includeList.size()]),
			excludeList.toArray(new Pattern[excludeList.size()]));
		System.out.println("Fixed: " + count);
	}
	
	private static void help ()
	{
		System.out.println(
			"FixTabs [root [spacesPerTab [+includePattern*] [-excludePattern*]]]\n" +
			"Ex.: FixTabs . 4 +.*\\.java +.*\\.properties -CVS");
	}

	/**
	 * Processes a directory recursively.
	 */
	private static int fixDirectory (final File dir, final Pattern[] includeFiles,
		final Pattern[] excludeDirs)
	{
		System.out.print(dir.getAbsolutePath() + " ... ");
		final File[] files = dir.listFiles();
		if (files == null) return 0;
		int count = 0;
		int changed = 0;
		
		for (int i = 0; i < files.length; ++i)
		{
			final File file = files[i];
			
			if (file.canRead() && file.canWrite() && !file.isHidden() &&
					matchesAny(includeFiles, file.getAbsolutePath()))
			{
				if (fixFile(file)) ++changed;
				++count;
			}
		}
		
		System.out.println(Integer.toString(changed) + '/' + count);
		
		for (int i = 0; i < files.length; ++i)
		{
			final File file = files[i];
			
			if (file.isDirectory() && !matchesAny(excludeDirs, file.getName()))
				changed += fixDirectory(file, includeFiles, excludeDirs);
		}
		
		return changed;
	}

	/**
	 * Checks a string against a set of patterns; returns true if any pattern matches.
	 */
	private static boolean matchesAny (final Pattern[] patterns, final String s)
	{
		for (Pattern p: patterns)
			if (p.matcher(s).matches())
				return true;
		
		return false;
	}

	/**
	 * Processes a single file.
	 * 
	 * @param file The file to process.
	 * @return true if file was touched; otherwise, it was already okay.
	 */
	private static boolean fixFile (final File file)
	{
		RandomAccessFile raf = null;
		
		try
		{
			raf = new RandomAccessFile(file, "rw");
			final byte[] originalData = new byte[(int)raf.length()];
			raf.readFully(originalData);
			final byte[] fixedData = fix(originalData);
			if (fixedData == null) return false;
			raf.seek(0);
			raf.write(fixedData);
			raf.setLength(fixedData.length);
			raf.close();
			return true;
		}
		catch (IOException e)
		{
			System.err.println(e);
			return false;
		}
		finally
		{
			if (raf != null) try { raf.close(); } catch (IOException e) {}
		}
	}

	/**
	 * Fixes tabs & spaces, if needed.
	 * 
	 * @param data Original data in ASCII format (1 byte = 1 char).
	 * @return Fixed data, or null if no fix was necessary.
	 */
	private static byte[] fix (final byte[] data)
	{
		baos.reset();
		boolean changed = false;
		boolean indenting = true;
		int position = 0;
		
		for (int read = 0; read < data.length; ++read)
		{
			final byte c = data[read];
			
			if (c == '\n')
			{
				indenting = true;
				position = 0;
				baos.write(c);
			}
			else if (c == '\r')
				baos.write(c);
			else if (indenting)
			{
				if (c == ' ')
				{
					if (++position % spacesPerTab == 0)
					{
						changed = true;
						baos.write('\t');
					}
				}
				else if (c == '\t')
				{
					baos.write(c);
					position += (spacesPerTab - position % spacesPerTab);
				}
				else
				{
					for (int i = 0; i < position % spacesPerTab; ++i)
						baos.write(' ');
					
					baos.write(c);
					indenting = false;
					++position;
				}
			}
			else
			{
				if (c == '\t')
				{
					changed = true;
					
					do
					{
						baos.write(' ');
					}
					while (++position % spacesPerTab != 0);
				}
				else
				{
					baos.write(c);
					++position;
				}
			}
		}
		
		if (indenting) for (int i = 0; i < position % spacesPerTab; ++i)
			baos.write(' ');

		return changed ? baos.toByteArray() : null;
	}
}





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds