Use Groovy to create quick change dir scripts


index: abstract, background, implementation, to do, extensions, conclusion, Further Reading

Abstract

Templating in the Groovy language is used to create scripts that provide a quick change directory utility at the command line based on a database of key to path assignments.

Background

On a prior job, I was often using the command line shell on Windows, Linux, and Unix.  One thing that I found inefficient was changing directory locations, the “cd” command.  When one needs to change to deeply nested paths this can get tedious.  Sure, tab completion (available on Windows, *nix) helps and one could also create aliases or use other techniques.  I guess this is enough of an issue that there are many utilities to make “cd” use “better”.

Recently, again I needed to do a lot of change directories in a command shell.  This time I looked at those projects that offer tools to do this.  I tried one and it didn’t work on Windows 7.  It ran, created a database of paths, but perhaps it does not handle file junctions in Windows, it could not locate a simple path.  It was time to revisit my previous solution to this.

At another prior job, on Windows I created a simple batch file, go.cmd, that accepted one argument that mapped to a directory path.  The script simply did a cd to that path.  The advantage with this method is that any string could be used for the path, whereas many tools do a search for a matching path. Thus, I could set foo=very/long/path and then just execute ‘go foo’. I wouldn’t have to find “path” or be presented with a list of partial matches.

I could just reuse that approach.  One drawback though, was that one had to edit a batch file to add or remove a new path target.  Not good.  If a script is working, changing it would require full use of a software development lifecycle. Yes, scripts should be treated with the same care any code gets. See for example, “Testing Matters, even with shell scripts”.

So, I wanted to do the same thing:  A utility that would take a key string, find the corresponding path in a database, and perform a cd.  This utility had to work on different OS’s and it had to use a high-level language.  Java would be adequate (had reasons for not using Perl, Python, PowerShell, etc), however there are issues with attempting to change directory from a JVM instance.  But, we could use “source code generation”, i.e. generate the required batch files.  This is a standard practice, even used as part of install systems.  Further, to make this easy, we could use the Groovy dynamic programming language that runs on the JVM.

Implementation

The Simple Approach

Temporary batch creation and use

If we store the desired key to path mapping in a Java Properties file. We can look up the key specified then generate a batch file that does the required CD. However, this process must itself be driven by a batch file.

Example run:

c:\Users\jbetancourt\workspace\GoScript\src\batch>go.cmd sync
c:\batch\sync>

File go.cmd the driver script:

@echo off
set OLDDIR=%CD%
call groovy Go.groovy %*
call goto.cmd
del %OLDDIR%\goto.cmd

File: goMappings.txt, the key to path mappings:

	java : c:\\java
	projects : c:\\Users\\jbetancourt\\Documents\\projects
	sync : c:\\batch\\sync

Now the Groovy script is simply:

class Go {
	static main(args) {
		if(args.length != 1){
			return // TODO: better handling
		}

		def db = new Properties()
		db.load(new FileReader(new File("goMappings.txt")))
		def f = new File("Goto.cmd")
		f.write("cd " + db.get(args[0]))
	}
}

One neat we could add here is that we could use a regexp to get the closest match, and if there are multiple give the user the option to pick a specific path. We could even search the stored paths in addition to the tag names.

A Complex Approach

Using a command pattern

As shown in listing one (oops, some lines are cut off in this view), the solution is simple. Download source

Listing 1

/**
 * GoGenerator.groovy
 * Copyright  2010 Josef Betancourt 20100312-17:47
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package batch

/*@Grapes([
@Grab(group='ch.qos.logback', module='logback-core', version='0.9.18'),
@Grab(group='ch.qos.logback', module='logback-classic', version='0.9.18'),
@Grab(group='org.slf4j', module='slf4j-api', version='1.5.10')])

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
*/

/**
 * Command line utility class to generate an OS specific
 * quick change directory script file.
 *
 * Syntax: GoGenerator [<file name>]
 * If target file name is not specified, output goes to std out.
 * Example: groovy GoGenerator.groovy
 * Example: groovy GoGenerator.groovy go.cmd
 *
 * Other utilities of this type are:
 *   Quick Change Directory: http://www.stevemiller.net/qcd/
 *   WCD:  http://www.xs4all.nl/~waterlan/
 *   List of directory changers:  http://www.xs4all.nl/~waterlan/other.htm
 *
 * Currently only Windows builder is implemented below.
 * Tested on Groovy 1.7.0, java version "1.6.0_18", Windows 7
 *
 * On Ubuntu:
 * /development$ sudo groovy -cp ./slf4j-api-1.5.11.jar:
 *    ./slf4j-simple-1.5.11.jar:./ant.jar:./ant-launcher.jar:.
 *    GoGenerator.groovy go.sh
 *
 * @author jbetancourt
 */
class GoGenerator {
	// Map of the location targets for the script utility.
	def static winDb = [
		"java" : "c:\\java",
		"work" : "\\Users\\jbetancourt\\work",
		"projects" : "c:\\Users\\jbetancourt\\Documents\\projects",
		"concurrent" : "c:\\Users\\jbetancourt\\Documents\\projects\\dev\\ConcurrentGroovy-1",
		"sync" : "c:\\batch\\sync"
	]

	def static linuxDb = [:]

	/**
	 * main entry point
	 * @param args
	 */
	static main(args) {
		def fileName
		if(args.length > 0){
			fileName = args[0]
		}

		def builder = createBuilder(fileName)
		builder.generate()
	}

	/**
	 * factory method to create an OS specific go builder.
	 *
	 * @param db map of folders
	 * @param fileName target file name or null
	 * @return the builder object
	 */
	static createBuilder(fileName){
		def ant = new AntBuilder()
		ant.condition(property:"winOS"){
			os(family:"windows")
		}
		ant.condition(property:"linuxOS"){
			os(family:"unix")
		}

		def builder
		// get first property entry whose key ends in "OS"
		def entry = ant.project.getProperties().find{ it.key ==~ /.*OS$/}

		switch(entry ? entry.key : "xyz"){
		  case 'winOS':
			builder = new WinGoBuilder(winDb, fileName)
			break
		  case 'linuxOS':
			builder = new LinuxGoBuilder(linuxDb, fileName)
			break
		  case 'xyz' :
			builder = new ErrorGoBuilder()
			break
		}

		return builder
	}

} // end class GoGenerator

/**
 * Creates go scripts.
 * Superclass of OS specific builders
 *
 * @author jbetancourt
 */
private class GoBuilder {
    def script = '''echo TODO!''' // template
    def db  // stores the data binding
    def fileName  // the target script file to create

    /**
     * Create the target script file or if
     * no filename send to stdout
     */
    def generate(){
		def engine = new groovy.text.SimpleTemplateEngine()
		def template = engine.createTemplate(script)
		def result = template.make(["db":db])
		if(fileName){
			File file = new File(fileName)
			file.write(result.toString())
		}else{
			println result
		}
    }
} // end class GoBuilder

/**
 * Creates an error message output builder.
 *
 * @author jbetancourt
 */
private class ErrorGoBuilder extends GoBuilder {
    def os = System.getProperty("os.name")
} // end class ErrorGoBuilder

/**
 * Creates a Windows specific go script.
 *
 * @author jbetancourt
 *
 */
private class WinGoBuilder extends GoBuilder  {
	    def winScript =
'''
@ECHO OFF
rem Go.bat
rem DO NOT EDIT THIS FILE.  It is generated using GoGenerator.groovy script.
rem purpose:   aid rapid directory setting at command line
rem example:  go full
rem if "%OS%"=="Windows_NT" setlocal
if "%1."=="." goto ERROR
<% db.each { %>
if NOT %1==$it.key goto ${it.key.toUpperCase()}-END
<% if(it.value ==~ /^\\S:.*/) {
print(it.value.substring(0,1) + ":")
}%>
cd $it.value
goto FINISH
: ${it.key.toUpperCase()}-END
<% } %>
:ERROR
echo GO.BAT: Huh?  [%1] Syntax is: go keyword
echo Didn't locate keyword: [%1]
echo Targets are
<% db.each { %>
echo <% print("\t" + it.key + "=" + it.value) %>
echo -------------------------------------
<% } %>
:FINISH
rem if "%OS%"=="Windows_NT" endlocal
'''
	    /**
	     * Constructor
	     */
	    WinGoBuilder(db, fileName){
	    	this.db = db
	    	this.fileName = fileName
	    	script = winScript
	    }

} // end class WinGoBuilder

/**
 * Creates a Linux specific go script builder
 *
 * @author jbetancourt
 */
private class LinuxGoBuilder  extends GoBuilder {
	def linuxScript = ''''''

    /** Constructor */
    LinuxGoBuilder(fileName){
    	this.fileName = fileName
    }

} // end class LinuxGoBuilder

// end of source GoGenerator.groovy

The code illustrates the use of two features of Groovy: Templates and the AntBuilder.  The AntBuilder allows easy reuse of Ant tasks.  The Ant Condition task which has an OS detection method is reused here.  Why reinvent OS detection code?  The code,

 

def ant = new AntBuilder()
ant.condition(property:"winOS"){
	os(family:"windows")
}

def builder
def entry = ant.project.getProperties().find{ it.key ==~ /.*OS$/}

switch(entry.key){
 case 'winOS':
	builder = new WinGoBuilder(db, fileName)
	break
}

return builder

creates an Ant task that sets a new Ant property, "winOS", if the OS family is windows.  Later, if this property is found, using a regular expression, a Windows specific builder is created passing in the map of keyword to folder pairs.  Notice that a "switch" in Groovy can use more then just ints as in Java.

The builder is more of a builder pattern, not the more powerful Groovy Builder concept.  The builder creates a data binding and using the stored Groovy Template, creates the target script text which is then stored in a target file or printed to console if no file was specified.  A map is used to store the keyword to directory path pairs.  An alternative was to store this data in an external properties file, of course.  For the intended purpose, that was not important.

The template format is similar to many other template languages such JSP or ASP.  Of course, most systems allow templating; even Ant has low level support for token replacement using filterchains. Within the template, Groovy code is embedded to iterate over the db map:

<% db.each { %>
if NOT %1==$it.key goto ${it.key.toUpperCase()}-END
<% if(it.value ==~ /^\\S:.*/) {
print(it.value.substring(0,1) + ":")
}%>
cd $it.value
goto FINISH
: ${it.key.toUpperCase()}-END
<% } %>

This section of code ultimately produces something like this, for Windows:
if NOT %1==java goto JAVA-END
c:
cd c:\java
goto FINISH
: JAVA-END

Not expert level batch programming, but effective.

To Do

  • Since this code was written using the Universal Admin Development Model, it is not Unit test friendly. Rewrite so that unit tests can be created.
  • The DB map being used is at the class level. This means it is shared by all builders. Not very useful since paths on each OS not only would have different formats but would not be the same destinations.
  • I still have not figured out how to include backslashes in a template. \\ nor \\\\ work. Good thing I didn't need them yet.
  • The switch should be within a conditional. Using a trick to avoid a match with switch(entry ? entry.key : "xyz") still forces the switch to do a lookup.

Extensions

One is tempted to embellish a simple utility, like adding a GUI for managing the resources. I resisted that. However, on Linux I also used two other scripts that I created. These allowed me to traverse a dir hierarchy, either up or down. For example, to go up the tree 4 levels, cdu 4. To go down, one indicates the destination folder, cdd drivers. They were implemented using Linux tools like "find". I wonder if they could be added to the go generator? That would be complex using DOS batch files. A better approach is to generate Powershell scripts.

Conclusion


Shown was an approach for the generation of batch file scripts using Groovy language. The implementation illustrated the use of Groovy Templates, AntBuilder, Regexp, and powerful switch statement.
This was a simple exercise in Groovy scripting and on the whole was successful. The code has some issues, such as embedded data, and the OS detection and builder creation could be improved.

 

Updates

July 25, 2011: Did Groovy 1.8* change and now requires an external Ant in the classpath? I had to change my quick change batch file and my Groovy script did not work until I included ant.jar and ant-Launcher.jar.

Top

Further Reading

Updated code: Groovy change dir batch generator

Minar, Igor, "Testing matters, even with shell scripts", link

Groovy, link

"Builder Pattern", link

A. Glover, S. Davis, "Practically Groovy: MVC programming with Groovy templates", 15 Feb 2005, accessed on 14 Mar 2010 at link

PowerShell Team, "The Admin Development Model and Send-Snippet", [Weblog entry.] Windows PowerShell Blog. 1 Jan 2007. (link). 14 Mar 2010.

"Using Ant from Groovy", accessed on 14 Mar 2010 at link

"Groovy Templates", accessed on 14 Mar 2010 at link

PowerShell, link

"Code generation done right", link

About these ads

2 Responses to Use Groovy to create quick change dir scripts

  1. Mauro Zallocco says:

    If you are using bash, use this to go to a directory
    containing the parameter.

    function goto () {
    `find . -name “$1″ -print | sed “s/^/cd /”`
    }

    e.g. if you have the following directory structure:
    foo
    foo/bar
    foo/bar/tar

    then
    $>goto tar
    $foo/bar/tar>

    Will it run on Windows?
    Sure, just install cygwin first.

    Mauro.

    • josefbetancourt says:

      Mauro:
      That is what I was using. Yes, I use cygwin, even on a USB stick. However, cygwin is not always available or allowed on every system. That’s one reason I was looking for a generative approach.

      Note that the “find” approach won’t help when your not at the root of a tree, which would then require also optionally passing the root to ‘goto’.

      Thanks for the advice. BTW, my bash mojo needs refreshing, how would you do the goup a specified amount in the tree in bash script?

      – Josef

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: