Add --strict so failures report a non-zero exit code

Every error path did printStackTrace() then System.exit(0), making a
failed run indistinguishable from a successful one to any caller checking
$?. pipekit works around this by grepping stdout for stack-trace text
(_detect_silent_failure); 112 /opt/sync scripts run under `set -e` and
cannot detect a jrunner failure at all.

All 14 error exits now route through die(), which honours --strict.
Opt-in rather than default: flipping it unconditionally would change the
behaviour of those 112 scripts at once. --strict is pre-scanned from argv
so it applies to failures during argument parsing too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-08-10 09:01:07 -04:00
parent e3f624f4d8
commit 61b3967a36

View File

@ -70,6 +70,7 @@ public class jrunner {
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
msg = msg + nl + "-f output format (csv, tsv, table, json) - default: csv";
msg = msg + nl + "--passfile path to the connection alias file - default: ~/.jrunnerpass";
msg = msg + nl + "--strict exit 1 on failure (default exits 0, for legacy callers)";
msg = msg + nl + "--help info";
msg = msg + nl + "";
msg = msg + nl + "Prefer -sc/-dc aliases over -scp/-dcp: a password given on the";
@ -83,6 +84,12 @@ public class jrunner {
//---------------------------------------parse args into variables-------------------------------------------------
// Pre-scan: -sq can fail while the loop is still running, and it must
// honour --strict regardless of where the flag sits in argv.
for (String a : args) {
if (a.equals("--strict")) strictExit = true;
}
for (int i = 0; i < args.length; i = i +1 ){
switch (args[i]) {
//source connection alias
@ -126,7 +133,7 @@ public class jrunner {
catch (Exception e) {
//System.out.println(nl + "error reasing source sql file: " + printStackTrace());
e.printStackTrace();
System.exit(0);
die();
return;
}
break;
@ -150,6 +157,9 @@ public class jrunner {
case "--passfile":
passFilePath = args[i+1];
break;
//exit non-zero on failure (handled in the pre-scan above)
case "--strict":
break;
case "-v":
System.out.println(msg);
return;
@ -223,7 +233,7 @@ public class jrunner {
Class.forName("com.ibm.as400.access.AS400JDBCDriver");
} catch (ClassNotFoundException cnf) {
System.out.println("The AS400 JDBC driver did not load");
System.exit(0);
die();
}
//-------------------------------------------establish connections-------------------------------------------------
@ -245,7 +255,7 @@ public class jrunner {
} catch (SQLException e) {
System.out.println("issue connecting to source:");
e.printStackTrace();
System.exit(0);
die();
}
if (!queryMode) {
System.out.println(" ✅ source database");
@ -257,7 +267,7 @@ public class jrunner {
} catch (SQLException e) {
System.out.println("issue connecting to destination:");
e.printStackTrace();
System.exit(0);
die();
}
System.out.println(" ✅ destination database");
}
@ -276,7 +286,7 @@ public class jrunner {
} catch (SQLException e) {
System.out.println("issue retrieving rows from source:");
e.printStackTrace();
System.exit(0);
die();
}
//---------------------------------------build meta---------------------------------------------------------------
@ -290,7 +300,7 @@ public class jrunner {
dtn = new String[cols + 1];
} catch (SQLException e) {
e.printStackTrace();
System.exit(0);
die();
}
try {
for (int i = 1; i <= cols; i++){
@ -301,7 +311,7 @@ public class jrunner {
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(0);
die();
}
//-------------------------clear the target table if requeted----------------------------------------------------
if (!queryMode && clear) {
@ -314,7 +324,7 @@ public class jrunner {
} catch (SQLException e) {
e.printStackTrace();
System.out.println(sql);
System.exit(0);
die();
}
}
if (queryMode) {
@ -323,7 +333,7 @@ public class jrunner {
outputQueryResults(rs, cols, dtn, outputFormat);
} catch (SQLException e) {
e.printStackTrace();
System.exit(0);
die();
}
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:sqlserver:")) {
//-------------------------------bulk copy (SQL Server dest)-------------------------------------------------
@ -347,7 +357,7 @@ public class jrunner {
System.out.print("\r" + src.rowsWritten());
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
die();
}
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:postgresql:")) {
//-------------------------------bulk copy (COPY, Postgres dest)--------------------------------------------
@ -389,7 +399,7 @@ public class jrunner {
System.out.print("\r" + rows);
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
die();
}
} else {
System.out.println("------------row count-------------------------------------");
@ -523,7 +533,7 @@ public class jrunner {
} catch (SQLException e) {
e.printStackTrace();
System.out.println(sql);
System.exit(0);
die();
}
sql = "";
}
@ -538,12 +548,12 @@ public class jrunner {
} catch (SQLException e) {
e.printStackTrace();
System.out.println(sql);
System.exit(0);
die();
}
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(0);
die();
}
}
//System.out.println(sql);
@ -767,6 +777,17 @@ public class jrunner {
return value;
}
// Historically every error path did printStackTrace() then System.exit(0),
// so a failed run was indistinguishable from a successful one to any
// caller checking $?. 112 /opt/sync scripts run under `set -e` against
// that behaviour, so flipping it unconditionally would change them all at
// once; --strict makes the correct exit code opt-in until they're audited.
private static boolean strictExit = false;
private static void die() {
System.exit(strictExit ? 1 : 0);
}
private static String escapeTSV(String value) {
return value.replaceAll("\t", " ").replaceAll("\n", " ").replaceAll("\r", " ");
}