Getty Images

Tip

How to debug Java programs with CLI scripting

Yes, you can have the ease of scripts and the structure of a static-typed language. Here's how to automate Java workflows using scripts, such as debugging Java code in the CLI.

Working in the command line offers many benefits, but the main one is the ability to automate tasks with scripts, thereby streamlining workflows. This tutorial explores how to apply simple scripts in the CLI to debug Java programs

Why use the CLI vs. GUI?

CLIs offer several advantages over graphical user interfaces, including speed, resource efficiency and the ability to automate tasks. They are much quicker at executing commands -- especially with automation -- than navigating through menus. They also require fewer computing resources to execute commands, making them suitable for systems with limited hardware capabilities or remote server environments. Additionally, CLIs excel in automating repetitive tasks through scripting, which helps to save time and reduce errors.

Automation through scripts is arguably the main advantage of CLIs over GUIs. Most program GUIs -- such as those in IDEs or Microsoft Office -- lack comparable task automation features. For those that do have this feature, it is often extremely limited -- and mostly achieved through a proprietary scripting language that is generally incompatible with other scripting languages. For example, the internal scripting language VBA -- used with Microsoft Office programs -- only works with Office programs.

We could argue the same is true with shell languages, which have been incompatible among different platforms for eons -- although there have been efforts to create new scripting languages that are portable among different platforms. Python, for example, was a shell language created for the Amoeba distributed OS, but it is now the most popular general-purpose programming language and "lingua franca" in AI. Similarly, JavaScript was created as a scripting language to be used in web pages and is now "lingua franca" on the web.

Many other portable scripting languages exist nowadays; the newest example is nushell.

Why program with simple scripts?

Scripting languages are easy to use and easy to write scripts with. Thus, they are desirable to use for task automation.

Simple scripting languages are arguably better suited to task automation than Python and other complex scripting languages. Python is used to automate tasks, but has become a more intricate language than most scripting languages. Python is undoubtedly suitable for writing programs quickly, but scripting languages are not, in principle, meant to replace programming languages.

In A Plea for Lean Software, Niklaus Wirth, the creator of Pascal and Modula-2, argues that easy-to-program languages generally don't promote high-quality software and are partially responsible for software bloat. Moreover, he argues, languages that don't enforce strict static typing, such as C/C++, particularly in structured or object-oriented languages, contribute to the bloat:

Without type checking, the notion of abstraction in programming languages remains hollow and academic. Abstraction can work only with languages that postulate strict, static typing of every variable and function. In this respect, C fails -- it resembles assembler code where everything goes.

And also:

Given this situation, programmers struggle with a language that discourages structured thinking and disciplined program construction (and denies basic compiler support). They also resort to makeshift tools that chiefly add to software's bulk.

All this is particularly true in scripting languages, which are easy to program with and don't have strict static type checking. This contrasts with languages, such as Java, that are not only strictly static-typed but also encourage structured thinking and disciplined program construction.

Automation with scripts: Simplicity vs. portability

To be clear, I do not encourage readers to write software entirely in scripting languages. Instead, developers should consider using simple scripting languages to automate certain tasks -- ones that can be boring or error-prone in CLI contexts and that are used in tandem with programming languages that embrace structured thinking and strict static types.

To prioritize simplicity, we could relax the need for portability, which many modern scripting languages offer in addition to OS independence. Typically, though, interpreters for portable languages are not bundled with OSes. When simplicity is paramount, particularly to address a broad public, Windows batch scripts are probably the simplest and most popular option due to the Windows platform's ubiquity. It's also extremely easy to convert these scripts to other scripting languages, such as Linux bash.

Debugging in the CLI with Java jdb

The JDK includes a debugger called jdb. Using jdb in the CLI can be cumbersome to set up, especially when done manually. Even copying and pasting statements can be error-prone and confusing for users. The ideal solution here is to automate the entire procedure using scripts.

The problem to automate here is that jdb uses two Windows consoles: the main window where the program to be debugged is called, and a second where jdb CLI commands are actually typed. This setup enables the program to print to the console where the program runs; that is, where the program to be debugged was initially called. This procedure is also useful for remote debugging. Although we are not stressing remote debugging here, this way to use jdb is preferable because it can be used in all situations.

Let us suppose that the name of the class containing the main method is ClassName. We must first run and suspend the program to be debugged, as follows:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 ClassName

This statement launches the JVM that will load ClassName class, but suspends the program rather than runs it. We then run jdb in another console to connect to the port 5005, where ClassName's JVM is listening:

jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=5005

Right after connecting to the application's JVM, jdb shows a prompt that looks like this:

VM Started: > No frames on the current call stack

main[1]

Where main[1] is the prompt waiting for a command to be typed.

Creating a test program

Consider a class ClassName that has dependencies we want to debug. For the sake of simplicity, let's suppose that ClassName.java looks like this:

import static utils.SomeTools.someTool;
import utils.SomeClass;

public class ClassName {

    public static void main(String[] args) {
        SomeClass var = someTool(args[0]);
        System.out.println(var.s);
    }

}

In directory utils, we would then have the files SomeTools.java and SomeClass.java. Let's suppose SomeClass.java is something along these lines:

package utils;
public class SomeClass {
public String s;
 
SomeClass(String s) {
this.s = s;
}
}

Let's now suppose that SomeTools.java has only the function used in ClassName.java:

package utils;
public class SomeTools {
public static SomeClass someTool(String s) {
return new SomeClass(s);
}
}

We will need to compile these classes with -g option first to have the symbols of the classes used by the program accessible by jdb:

javac -g utils\SomeClass.java
javac -g utils\SomeTools.java
javac -g ClassName.java

Automating jdb setup and jdb call

To automate the jdb setup, we need a batch file that prepares the files needed to debug the class ClassName and then somehow call java and jdb as shown above, one in each window.

Let's devise a not-too-fancy batch file called debug.bat to compile the source files, handle jdb setup, call java and call jdb:

@ECHO OFF

javac -g utils\SomeClass.java
if %ERRORLEVEL% NEQ 0 (
    EXIT /B 1
)

javac -g utils\SomeTools.java
if %ERRORLEVEL% NEQ 0 (
    EXIT /B 1
)

javac -g ClassName.java
if %ERRORLEVEL% NEQ 0 (
    EXIT /B 1
)

@ECHO "**========================================================================**"
@ECHO "**                            COMMANDS                                    **"
@ECHO "**========================================================================**"
@ECHO "**  stop at ClassName:<line number> - sets bkp at <line number>          **"
@ECHO "**  run - runs the program and stops at bkp (called only at the start)   **"
@ECHO "**  cont - continue to execute the program (after run was called)        **"
@ECHO "**  locals - prints local variables                                       **"
@ECHO "**  step - steps into                                                      **"
@ECHO "**  next - steps out                                                       **"
@ECHO "**  print ClassName.<variable> - displays class <variable>               **"
@ECHO "**  dump <object> - display all variables in <object>                    **"
@ECHO "**  list - shows source code and bkp line                                 **"
@ECHO "**========================================================================**"

start cmd /k "rdb"

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 ClassName "This is a test text"

The rdb.bat batch file contains only the statement to connect the debugger to the port 5005. In other words, it will look like this:

jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=5005

Although it might seem strange to call rdb.bat before we call java, we are obliged to do that because the program must be suspended right after calling java to make JVM listen to port 5005 for commands to be sent from jdb. Therefore, debug.bat is blocked and cannot execute any statements that might appear after calling java.

On Windows, the new Windows console and rdb.bat will be executed in another process. There is overhead to create the new process and Windows console, and to run rdb.bat. Meanwhile, debug.bat will execute its last line, which will run and suspend the program while JVM listens to port 5005, waiting for jdb commands.

List jdb commands for quick reference

As seen above, file debug.bat also contains a series of @ECHO statements, which are just a brief summary of jdb commands. In this way, one can copy one of them from this list and paste it in the jdb window to minimize typing.

This list can be extended with more commands as needed. If the program must print many lines before it reaches the point where the bug occurs, it's better to redirect System.out to a file using >. In this case, the last line of debug.bat can be modified as follows:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 ClassName "This is a test text" > out.txt

This keeps the jdb command list easily accessible and prevents jdb's Windows console from reaching the buffer size limit. In any case, one can always check the file's content with a text editor, such as Notepad, by double-clicking out.txt in an Explorer window. Even better in this case is Notepad++, because when something else is written in the file during the debug session, Notepad++ will propose to reload the file.

Finally, the compilations of the Java source files at the beginning are each followed by this statement:

if %ERRORLEVEL% NEQ 0 (

EXIT /B 1

)

That statement verifies the exit status of the last statement executed in the script, which was the compilation.

If the compiler finds errors in a Java source file, it will return an error code that is different than zero. If so, the condition shown in the if statement will be true, and the batch will exit with an error code 1. Therefore, these if statements will abort the script and prevent execution of any statements after a compilation error is detected.

An overview of jdb debugging commands

The commands listed by the echo statements are just a few from a large set of jdb commands. Here's an overview of some of them:

Conclusion

An important advantage of using the CLI vs. an IDE and GUIs is automation using scripts. Many professional developers prefer to use the CLI because scripts enable them to streamline workflows.

When automating programming tasks using scripts, the simpler the scripting language is, the easier it is to port it to different machines. Some might prefer to use a portable scripting language, such as Python or nushell. However, to emphasize simplicity, it is desirable to use simple scripts with the CLI.

Nilo Stolte has extensive experience in computer graphics, computer architecture and parallel processing, as well as high-level programming languages including C, C++ and Java.

 

Dig Deeper on Application development and design