diff --git a/build/build.properties b/build/build.properties
index 0da6631da..28c490072 100644
--- a/build/build.properties
+++ b/build/build.properties
@@ -13,14 +13,14 @@ dependencies.dir=${basedir}/lib
cfml.version=5.3.10.120
cfml.extensions=8D7FB0DF-08BB-1589-FE3975678F07DB17
box.bunndled.modules=commandbox-update-check,commandbox-cfconfig,commandbox-dotenv
-cfml.loader.version=2.8.3
+cfml.loader.version=2.8.4
cfml.cli.version=${cfml.loader.version}.${cfml.version}
lucee.version=${cfml.version}
# Don't bump this version. Need to remove this dependency from cfmlprojects.org
lucee.config.version=5.2.4.37
-jre.version=jdk-11.0.18+10
+jre.version=jdk-11.0.19+7
launch4j.version=3.14
-runwar.version=4.8.3
+runwar.version=4.8.5
jline.version=3.21.0
jansi.version=2.3.2
jgit.version=5.13.0.202109080827-r
@@ -75,3 +75,70 @@ tests.run.url=http\://${server.host}\:${runwar.port}${war.contextpath}/tests/
runwar.cfml.dirlist=${src.dir}
## installs a custom error and 404 handler if set to true
cfmlexception.install=false
+
+java.opens=java.base/sun.nio.ch \
+ java.base/sun.nio.cs \
+ java.base/java.io \
+ java.base/java.lang \
+ java.base/java.lang.annotation \
+ java.base/java.lang.invoke \
+ java.base/java.lang.module \
+ java.base/java.lang.ref \
+ java.base/java.lang.reflect \
+ java.base/java.math \
+ java.base/java.net \
+ java.base/java.net.spi \
+ java.base/java.nio \
+ java.base/java.nio.channels \
+ java.base/java.nio.channels.spi \
+ java.base/java.nio.charset \
+ java.base/java.nio.charset.spi \
+ java.base/java.nio.file \
+ java.base/java.nio.file.attribute \
+ java.base/java.nio.file.spi \
+ java.base/java.security \
+ java.base/java.security.acl \
+ java.base/java.security.cert \
+ java.base/java.security.interfaces \
+ java.base/java.security.spec \
+ java.base/java.text \
+ java.base/java.text.spi \
+ java.base/java.time \
+ java.base/java.time.chrono \
+ java.base/java.time.format \
+ java.base/java.time.temporal \
+ java.base/java.time.zone \
+ java.base/java.util \
+ java.base/java.util.concurrent \
+ java.base/java.util.concurrent.atomic \
+ java.base/java.util.concurrent.locks \
+ java.base/java.util.function \
+ java.base/java.util.jar \
+ java.base/java.util.regex \
+ java.base/java.util.spi \
+ java.base/java.util.stream \
+ java.base/java.util.zip \
+ java.base/javax.crypto \
+ java.base/javax.crypto.interfaces \
+ java.base/javax.crypto.spec \
+ java.base/javax.net \
+ java.base/javax.net.ssl \
+ java.base/javax.security.auth \
+ java.base/javax.security.auth.callback \
+ java.base/javax.security.auth.login \
+ java.base/javax.security.auth.spi \
+ java.base/javax.security.auth.x500 \
+ java.base/javax.security.cert \
+ java.base/sun.net.www.protocol.https \
+ java.rmi/sun.rmi.transport \
+ java.base/sun.security.rsa \
+ java.base/sun.security.pkcs \
+ java.base/sun.security.x509 \
+ java.base/sun.security.util \
+ java.base/sun.util.cldr \
+ java.base/sun.util.locale.provider \
+ java.desktop/com.sun.java.swing.plaf.nimbus \
+ java.desktop/com.sun.java.swing.plaf.motif \
+ java.desktop/com.sun.java.swing.plaf.nimbus \
+ java.desktop/com.sun.java.swing.plaf.windows \
+ java.management/sun.management
\ No newline at end of file
diff --git a/build/build.xml b/build/build.xml
index 5fd7c1154..e827d012f 100644
--- a/build/build.xml
+++ b/build/build.xml
@@ -16,8 +16,8 @@ External Dependencies:
-
-
+
+
@@ -394,7 +394,9 @@ External Dependencies:
+
+
@@ -431,7 +433,9 @@ External Dependencies:
+
+
@@ -446,7 +450,9 @@ External Dependencies:
+
+
@@ -459,7 +465,9 @@ External Dependencies:
+
+
diff --git a/readme.md b/readme.md
index 6cc9e5025..45187317a 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
-``` bash
+```bash
_____ _ ____
/ ____| | | _ \
| | ___ _ __ ___ _ __ ___ __ _ _ __ __| | |_) | _____ __
@@ -49,7 +49,7 @@ CommandBox is maintained under the Semantic Versioning guidelines as much as pos
Releases will be numbered with the following format:
-``` bash
+```bash
..
```
diff --git a/src/bin/box.sh b/src/bin/box.sh
index 073d7bb76..4ae5a6243 100755
--- a/src/bin/box.sh
+++ b/src/bin/box.sh
@@ -17,14 +17,6 @@ done
# Get the location of the running script
this_script=`which "$0"`
-# Append to a class path
-cp=$this_script
-
-# Append box_classpath if set to cp
-if [ -n "$BOX_CLASSPATH" ]
-then
- cp="$cp:$BOX_CLASSPATH"
-fi
# Prepare Java arguments
java_args="$BOX_JAVA_ARGS -client"
@@ -35,10 +27,6 @@ java_args="$BOX_JAVA_ARGS -client"
# Cleanup paths for Cygwin.
case "`uname`" in
-CYGWIN*)
- cp=`cygpath --windows --mixed --path "$cp"`
- ;;
-# Add Java Arguments for Mac
Darwin)
if [ -e /System/Library/Frameworks/JavaVM.framework ]
then
@@ -56,9 +44,6 @@ Darwin)
;;
esac
-CLASSPATH="$cp"
-export CLASSPATH
-
##############################################################################
## JAVA DETERMINATION ##
##############################################################################
@@ -84,5 +69,5 @@ fi
## EXECUTION
##############################################################################
-exec "$java" $java_args cliloader.LoaderCLIMain "$@"
+exec "$java" $java_args -jar $this_script "$@"
exit
diff --git a/src/cfml/system/Shell.cfc b/src/cfml/system/Shell.cfc
index 52acff883..9b70b7d89 100644
--- a/src/cfml/system/Shell.cfc
+++ b/src/cfml/system/Shell.cfc
@@ -63,6 +63,10 @@ component accessors="true" singleton {
*/
property name="shellPrompt";
/**
+ * The default terminal window title
+ */
+ property name="windowTitle";
+ /**
* This value is either "interactive" meaning the shell stays open waiting for user input
* or "command" which means a single command will be run and then the shell will be exiting.
* This differentiation may be useful for commands who want to be careful not to leave threads running
@@ -111,6 +115,7 @@ component accessors="true" singleton {
variables.pwd = "";
variables.reader = "";
variables.shellPrompt = "";
+ variables.windowTitle = "";
variables.userDir = arguments.userDir;
variables.tempDir = arguments.tempDir;
@@ -135,9 +140,13 @@ component accessors="true" singleton {
* Finish configuring the shell
**/
function onDIComplete() {
+ // When the shell first starts, the current working dir doesn't always contain the trailing slash
+ variables.pwd = fileSystem.resolvePath( variables.pwd );
+
// Create reader console and setup the default shell Prompt
- variables.reader = readerFactory.getInstance( argumentCollection = variables.initArgs );
- variables.shellPrompt = print.green( "CommandBox> ");
+ variables.reader = readerFactory.getInstance( argumentCollection = variables.initArgs );
+ setPrompt();
+ setWindowTitle();
// Create temp dir & set
setTempDir( variables.tempdir );
@@ -151,9 +160,6 @@ component accessors="true" singleton {
);
getModuleService().configure();
- // When the shell first starts, the current working dir doesn't always contain the trailing slash
- variables.pwd = fileSystem.resolvePath( variables.pwd );
-
getModuleService().activateAllModules();
// load commands
@@ -251,6 +257,27 @@ component accessors="true" singleton {
return this;
}
+ /**
+ * Sets the window title
+ * @text.hint window title text to set, if empty we use the default title
+ **/
+ Shell function setWindowTitle( text="" ) {
+ if( !len( arguments.text ) ){
+ var homeDir = fileSystem.normalizeSlashes( fileSystem.resolvePath( '~' ) );
+ var thisFullpath = fileSystem.normalizeSlashes( getPWD() );
+ var thisPath = thisFullpath.listLast( '\/' ) & '/';
+
+ // Shortcut for home dir
+ if( thisFullpath contains homeDir ) {
+ thisPath = thisFullpath.replaceNoCase( homeDir, '~/' );
+ }
+ variables.windowTitle = "CommandBox: #thisPath#";
+ } else {
+ variables.windowTitle = arguments.text;
+ }
+ return this;
+ }
+
/**
* ask the user a question and wait for response
* @message.hint message to prompt the user with
@@ -509,6 +536,7 @@ component accessors="true" singleton {
request.lastCWD = arguments.directory;
// Update prompt to reflect directory change
setPrompt();
+ setWindowTitle();
return variables.pwd;
}
@@ -553,7 +581,7 @@ component accessors="true" singleton {
try {
- var interceptData = { prompt : variables.shellPrompt };
+ var interceptData = { prompt : variables.shellPrompt, windowTitle: variables.windowTitle };
getInterceptorService().announceInterception( 'prePrompt', interceptData );
if( arguments.silent ) {
@@ -576,6 +604,8 @@ component accessors="true" singleton {
if( arguments.silent ) {
line = variables.reader.readLine( interceptData.prompt, javacast( "char", ' ' ) );
} else {
+ // set the window title before reading a line
+ variables.reader.getTerminal().writer().print( chr( 27 ) & ']2;' & interceptData.windowTitle & chr( 7 ) );
line = variables.reader.readLine( interceptData.prompt );
}
}
diff --git a/src/cfml/system/endpoints/Java.cfc b/src/cfml/system/endpoints/Java.cfc
index cedf0e943..4db49b95c 100644
--- a/src/cfml/system/endpoints/Java.cfc
+++ b/src/cfml/system/endpoints/Java.cfc
@@ -310,20 +310,11 @@ component accessors=true implements="IEndpoint" singleton {
'version' : '',
'type' : 'jre',
'arch' : javaService.getCurrentCPUArch(),
- 'os' : '',
+ 'os' : javaService.getCurrentOS(),
'jvm-implementation' : ( ID.findNoCase( 'openj9' ) ? 'openj9' : 'hotspot' ),
'release' : 'latest'
};
-
- if( fileSystemUtil.isMac() ) {
- results.os = 'mac';
- } else if( fileSystemUtil.isLinux() ) {
- results.os = 'linux';
- } else {
- results.os = 'windows';
- }
-
var tokens = ID.listToArray( '_' );
var first = true;
for( var token in tokens ) {
diff --git a/src/cfml/system/modules/string-similarity/readme.md b/src/cfml/system/modules/string-similarity/readme.md
index 839f17097..d5c32a398 100644
--- a/src/cfml/system/modules/string-similarity/readme.md
+++ b/src/cfml/system/modules/string-similarity/readme.md
@@ -6,7 +6,7 @@ Read more about how it works here:
http://www.codersrevolution.com/blog/ColdFusion-Levenshtein-Distance-String-comparison-and-highlighting
-``` html
+```html
numVersions ) {
- var thisVersion = '';
- } else {
- var thisVersion = versions[thisIndex];
- if( satisfyingVersion == thisVersion ) {
- format = 'boldwhiteOnGreen';
- } else if ( matches.find( thisVersion ) ) {
- format = 'boldWhite';
- }
- }
- print.text( padRight( thisVersion, colWdith ), format );
+ print.columns( versions, (thisVersion)=>{
+ if( satisfyingVersion == thisVersion ) {
+ return 'boldwhiteOnGreen';
+ } else if ( matches.find( thisVersion ) ) {
+ return 'boldWhite';
}
- print.line()
- }
+ return 'grey';
+ } );
print.line()
.greyLine( 'Unmatched Version' )
.boldWhiteLine( 'Version matching semver range' )
@@ -189,27 +173,4 @@ component {
return [];
}
-
- /**
- * Adds characters to the right of a string until the string reaches a certain length.
- * If the text is already greater than or equal to the maxWidth, the text is returned unchanged.
- * @text The text to pad.
- * @maxWidth The number of characters to pad up to.
- * @padChar The character to use to pad the text.
- */
- private string function padRight( required string text, required numeric maxWidth, string padChar = " " ) {
- var textLength = len( arguments.text );
- if ( textLength == arguments.maxWidth ) {
- return arguments.text;
- } else if( textLength > arguments.maxWidth ) {
- if( arguments.maxWidth < 4 ) {
- return left( text, arguments.maxWidth );
- } else {
- return left( text, arguments.maxWidth-3 )&'...';
- }
- }
- arguments.text &= repeatString( arguments.padChar, arguments.maxWidth-textLength );
- return arguments.text;
- }
-
}
diff --git a/src/cfml/system/modules_app/package-commands/commands/package/list.cfc b/src/cfml/system/modules_app/package-commands/commands/package/list.cfc
index b6f622bcf..3de64572c 100644
--- a/src/cfml/system/modules_app/package-commands/commands/package/list.cfc
+++ b/src/cfml/system/modules_app/package-commands/commands/package/list.cfc
@@ -30,6 +30,7 @@ component aliases="list" {
processingdirective pageEncoding='UTF-8';
property name="packageService" inject="PackageService";
+ property name="_print" inject="print";
/**
* @verbose Outputs additional information about each package
@@ -63,41 +64,27 @@ component aliases="list" {
}
// normal output
print.green( 'Dependency Hierarchy for ' ).boldCyanLine( "#tree.name# (#tree.version#)" );
- printDependencies( tree, '', arguments.verbose );
+ print.tree( buildDependencies( tree, arguments.verbose ) );
}
- /**
- * Pretty print dependencies
- */
- private function printDependencies( required struct parent, string prefix, boolean verbose ) {
- var i = 0;
- var depCount = structCount( arguments.parent.dependencies );
+ // Massage dependency tree into just the bits we want for tree outout
+ private function buildDependencies( required struct parent, boolean verbose ) {
+ var deps = [:];
for( var dependencyName in arguments.parent.dependencies ) {
var dependency = arguments.parent.dependencies[ dependencyName ];
- var childDepCount = structCount( dependency.dependencies );
- i++;
- var isLast = ( i == depCount );
- var branch = ( isLast ? '└' : '├' ) & '─' & ( childDepCount ? '┬' : '─' );
- var branchCont = ( isLast ? ' ' : '│' ) & ' ' & ( childDepCount ? '│' : ' ' );
-
- print.text( prefix & branch & ' ' );
-
- print[ ( dependency.dev ? 'boldYellowline' : 'boldLine' ) ]( '#dependencyName# (#dependency.packageVersion#)' );
-
+ var thisName = _print.bold( '#dependencyName# (#dependency.packageVersion#)', ( dependency.dev ? 'yellow' : '' ) );
if( arguments.verbose ) {
if( len( dependency.name ) ) {
- print.text( prefix & branchCont & ' ' );
- print[ ( dependency.dev ? 'yellowLine' : 'line' ) ]( dependency.name );
+ thisName &= chr(10) & _print.text( dependency.name, ( dependency.dev ? 'yellow' : '' ) );
}
if( len( dependency.shortDescription ) ) {
- print.text( prefix & branchCont & ' ' );
- print[ ( dependency.dev ? 'yellowLine' : 'line' ) ]( dependency.shortDescription );
+ thisName &= chr(10) & _print.text( dependency.shortDescription, ( dependency.dev ? 'yellow' : '' ) );
}
} // end verbose?
-
- printDependencies( dependency, prefix & ( isLast ? ' ' : '│ ' ), arguments.verbose );
+ deps[ thisName ] = buildDependencies( dependency, arguments.verbose );
}
+ return deps;
}
}
diff --git a/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc b/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc
index 983cbd3af..642b35f44 100644
--- a/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc
+++ b/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc
@@ -54,7 +54,7 @@ component aliases='java search' {
function run(
version,
jvm = 'hotspot',
- os,
+ os = javaService.getCurrentOS(),
arch = javaService.getCurrentCPUArch(),
type = 'jre',
release = 'latest',
@@ -104,16 +104,6 @@ component aliases='java search' {
version = '[#version#,#version+1#)';
}
- if( isNull( os) ) {
- if( fileSystemUtil.isMac() ) {
- os = 'mac';
- } else if( fileSystemUtil.isLinux() ) {
- os = 'linux';
- } else {
- os = 'windows';
- }
- }
-
var APIURLCheck = 'https://api.adoptium.net/v3/assets/version/#encodeForURL(version)#?page_size=100&release_type=ga&vendor=eclipse&project=jdk&heap_size=normal';
if( jvm.len() ) {
diff --git a/src/cfml/system/modules_app/system-commands/commands/clipboard.cfc b/src/cfml/system/modules_app/system-commands/commands/clipboard.cfc
new file mode 100644
index 000000000..b4cff91e2
--- /dev/null
+++ b/src/cfml/system/modules_app/system-commands/commands/clipboard.cfc
@@ -0,0 +1,49 @@
+/**
+ * Stores text inputted into OS clipboard.
+ * .
+ * {code:bash}
+ * echo "Hello World!" | clipboard
+ * {code}
+ * .
+ * Output the text and copy it to the clipboard with the --echo flag
+ * .
+ * {code:bash}
+ * ls --tree | clipboard --echo
+ * {code}
+ **/
+component {
+
+ /**
+ * @text The text to store on clipboard
+ * @echo Echo text out to console as well
+ **/
+ function run( String text="", Boolean echo=false ) {
+ if( FileSystemUtil.isWindows() ) {
+ var binary = 'clip';
+ } else if( FileSystemUtil.isMac() ) {
+ var binary = 'pbcopy';
+ } else if( FileSystemUtil.isLinux() ) {
+ try {
+ var whichXclip = command( '!which xclip' ).run( returnOutput=true );
+ } catch( any e ) {
+ var whichXclip = '';
+ }
+ if( len( trim( whichXclip ) ) ) {
+ var binary = 'xclip';
+ } else {
+ error( 'xclip binary not installed for Linux. Please install xclip and try again.' );
+ }
+ } else {
+ error( 'Unsupported OS for "clipboard" [#getSystemSetting( 'os.name' )#]' );
+ }
+ command( '!' & binary )
+ .run( piped=print.unansi( text ) );
+
+ if( echo ) {
+ print.text( text );
+ } else {
+ print.greenLine( 'Copied to clipboard!' );
+ }
+ }
+
+}
diff --git a/src/cfml/system/modules_app/system-commands/commands/dir.cfc b/src/cfml/system/modules_app/system-commands/commands/dir.cfc
index c0d4220d4..f9c97c26b 100644
--- a/src/cfml/system/modules_app/system-commands/commands/dir.cfc
+++ b/src/cfml/system/modules_app/system-commands/commands/dir.cfc
@@ -40,8 +40,14 @@ component aliases="ls,ll,directory" {
* @recurse Include nested files and folders
* @simple Output only path names and nothing else.
* @full Output absolute file path, not just relative to current working directory
+ * @tree Output ASCII file tree
**/
- function run( Globber paths=globber( getCWD() ), sort='directory, type, name', string excludePaths='', boolean recurse=false, boolean simple=false, boolean full=false ) {
+ function run( Globber paths=globber( getCWD() ), sort='directory, type, name', string excludePaths='', boolean recurse=false, boolean simple=false, boolean full=false, boolean tree=false ) {
+
+ // If we're doign a tree view, then recurse is sort of assumed, otherwse it would be a very boring tree!
+ if( tree ) {
+ recurse = true;
+ }
// Backwards compat for old parameter name
if( arguments.keyExists( 'directory' ) && arguments.directory.len() ) {
@@ -67,18 +73,53 @@ component aliases="ls,ll,directory" {
if( directoryExists( p ) ){
if( !p.endsWith( '/' ) && !p.endsWith( '\' ) ) {
p &= '/';
- }
+ }
return p &= '*' & ( recurse ? '*' : '' );
}
return p;
} );
-
+
var results = paths
.setExcludePattern( excludePaths )
.asQuery()
.withSort( sort )
.matches();
-
+
+ if( tree ) {
+ var treeData = [:];
+ results = results.reduce( (acc,p)=>acc.append(p), [] );
+ results = results.map( (p)=>{
+ var name = p.name & ( p.type == "Dir" ? "/" : "" );
+ var path = cleanRecursiveDir( paths.getBaseDir(), p.directory & '/', full );
+ var segmentLength = 0;
+ if( len( path ) ) {
+ path = '["' & path.listChangeDelims( '/"]["', '/' ) & '/"]';
+ segmentLength = path.listLen( '/' )
+ }
+ return { 'segmentLength' : ++segmentLength, 'path' : path & '["#name#"]' }
+ } );
+ // We must add the keys to our ordered structs from the bottom level up to preserve the original sorting of the globber
+ loop from=1 to=results.reduce( (max,p)=>max(max,p.segmentLength), 0 ) index="local.thisLen" {
+ results
+ .filter( (p)=>p.segmentLength==thisLen )
+ .each( (p)=>{
+ evaluate( 'treeData#p.path#=[:]' );
+ } );
+ }
+
+ var treeFormatUDF = (s,a)=>{
+ if( s.endsWith( '/' ) ) {
+ return getPathColor( '', 'dir' )
+ } else {
+ return getPathColor( a.last(), 'file' )
+ }
+ };
+
+ print.tree( treeData, treeFormatUDF );
+
+ return;
+ }
+
for( var x=1; x lte results.recordcount; x++ ) {
if( simple ) {
@@ -126,16 +167,20 @@ component aliases="ls,ll,directory" {
*/
private function cleanRecursiveDir( required directory, required incoming, boolean full ){
if( full ) {
- return incoming;
+ return fileSystemUtil.normalizeSlashes( incoming );
}
var prefix = ( replacenocase( fileSystemUtil.normalizeSlashes( arguments.incoming ), fileSystemUtil.normalizeSlashes( arguments.directory ), "" ) );
return ( len( prefix ) ? reReplace( prefix, "^(/|\\)", "" ) : "" );
}
private function colorPath( name, type ){
+ print.line( name, getPathColor( name, type ) );
+ }
+
+ private function getPathColor( name, type ){
if( type == 'Dir' ) {
- print.BlueLine( name );
+ return 'Blue';
} else {
var ext = name.listLast( '.' );
if( name.startsWith( '.' ) ) { ext = 'hidden'; }
@@ -143,43 +188,36 @@ component aliases="ls,ll,directory" {
switch( ext ) {
// Binary/executable
case "exe": case "jar": case "com": case "bat": case "msi": case "lar": case "lco": case "class": case "dll": case "war": case "eot": case "svg": case "ttf": case "woff": case "woff2":
- print.AquaLine( name );
- break;
+ return 'Aqua';
// Compressed
case "zip": case "tar": case "gz":
- print.Gold3Line( name );
- break;
+ return 'Gold3';
// Code
case "cfml": case "cfm": case "cfc": case "html": case "htm": case "js": case "css": case "java": case "boxr": case "sh": case "sql":
- print.CyanLine( name );
- break;
+ return 'Cyan';
// Images
case "gif": case "jpg": case "bmp": case "jpeg": case "png": case "ico":
- print.YellowLine( name );
- break;
+ return 'Yellow';
// Design/browser assets
case "js": case "css": case "less": case "sass":
- print.OliveLine( name );
- break;
+ return 'Olive';
// Plain text/data
case "json": case "txt": case "properties": case "yml": case "yaml": case "xml": case "xsd": case "log": case "ini": case "conf":
- print.LimeLine( name );
- break;
+ return 'Lime';
// Documents
case "pdf": case "doc": case "docx": case "xls": case "csv": case "md": case "ppt": case "pptx": case "xlsx": case "odp": case "ods": case "rtf":
- print.RedLine( name );
- break;
+ return 'Red';
// Hidden files
case "hidden":
- print.MaroonLine( name );
- break;
+ return 'Maroon';
default:
- print.Line( name );
+ return '';
}
}
}
+
/**
* Returns file size in either b, kb, mb, gb, or tb
*
diff --git a/src/cfml/system/modules_app/system-commands/commands/help.cfc b/src/cfml/system/modules_app/system-commands/commands/help.cfc
index db05145f9..1e7690e2a 100644
--- a/src/cfml/system/modules_app/system-commands/commands/help.cfc
+++ b/src/cfml/system/modules_app/system-commands/commands/help.cfc
@@ -116,9 +116,7 @@ component aliases="h,/?,?,--help,-help" {
print.line();
// Show them
- for( var command in commands ) {
- print.line( commandRootSpaces & ' ' & command );
- }
+ print.columns( commands.map( (command)=>commandRootSpaces & ' ' & command ) );
print.line();
@@ -132,9 +130,7 @@ component aliases="h,/?,?,--help,-help" {
print.line();
// Show them
- for( var namespace in namespaces ) {
- print.line( commandRootSpaces & ' ' & namespace );
- }
+ print.columns( namespaces.map( (namespace)=>commandRootSpaces & ' ' & namespace ) );
print.line();
diff --git a/src/cfml/system/modules_app/system-commands/commands/printColumns.cfc b/src/cfml/system/modules_app/system-commands/commands/printColumns.cfc
new file mode 100644
index 000000000..1c296cc1a
--- /dev/null
+++ b/src/cfml/system/modules_app/system-commands/commands/printColumns.cfc
@@ -0,0 +1,40 @@
+/**
+ * Prints a list or array of information as columns
+ *
+ * JSON or list data can be passed in the first param or piped into the command:
+ * {code:bash}
+ * printColumns [1,2,3]
+ * ls --simple | printColumns
+ * {code}
+ */
+component {
+
+ /**
+ * Outputs a list of items in column form
+ * @data JSON serialized array or list
+ * @delimiter List delimiter (default to new line)
+ */
+
+ public string function run(
+ String data='',
+ String delimiter=chr(13)&chr(10)
+ ) {
+
+ data = print.unAnsi( data );
+
+ //deserialize data if in a JSON format
+ if( isJSON( data) ) {
+ data = deserializeJSON( data );
+ if( isSimpleValue( data ) ) {
+ data = [ data ];
+ } else if( !isArray( data ) ) {
+ error( 'Only JSON arrays can be used with the printColumn command' );
+ }
+ } else {
+ data = data.listToArray( delimiter );
+ }
+
+ print.columns( data );
+ }
+
+}
diff --git a/src/cfml/system/modules_app/system-commands/commands/run.cfc b/src/cfml/system/modules_app/system-commands/commands/run.cfc
index 677aa5f52..7b0d341b7 100644
--- a/src/cfml/system/modules_app/system-commands/commands/run.cfc
+++ b/src/cfml/system/modules_app/system-commands/commands/run.cfc
@@ -192,14 +192,6 @@ component{
terminal.resume();
}
- // Put the terminal title back on Windows
- if( fileSystemUtil.isWindows() && nativeShell contains 'cmd' ) {
- var commandArray = [ nativeShell,'/a','/c', 'Title CommandBox is a ColdFusion (CFML) CLI, Package Manager, Server and REPL' ];
- createObject( "java", "java.lang.ProcessBuilder" ).init( commandArray )
- .inheritIO()
- .start();
- }
-
checkInterrupted();
}
diff --git a/src/cfml/system/modules_app/system-commands/commands/unansi.cfc b/src/cfml/system/modules_app/system-commands/commands/unansi.cfc
new file mode 100644
index 000000000..61fdfbb7a
--- /dev/null
+++ b/src/cfml/system/modules_app/system-commands/commands/unansi.cfc
@@ -0,0 +1,18 @@
+/**
+ * Outputs the text entered but with ANSI formatting stripped. Useful when you want to pipe output into a command which doesn't understand or need ANSI formatting.
+ * .
+ * {code:bash}
+ * package show | unansi
+ * {code}
+ *
+ **/
+component {
+
+ /**
+ * @text.hint The text to output without ANSI formatting
+ **/
+ function run( String text="" ) {
+ print.text( print.unansi( arguments.text ) );
+ }
+
+}
diff --git a/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc b/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc
index 6dd6c9a6a..a00a195da 100644
--- a/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc
+++ b/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc
@@ -295,13 +295,18 @@ component {
memento = deserializeJSON( arguments.rawResults )
);
+ arguments.outputFile = resolvePath( arguments.outputFile );
// Build out the output formats in async fashion
arguments.outputFormats
.listToArray()
.each( ( format ) => {
// Build out the targetFile
- var targetFile = getCWD() & outputFile & getOutputExtension( arguments.format );
+ var targetFile = outputFile & getOutputExtension( arguments.format );
// write out the JSON
+ var targetDir = getDirectoryFromPath( targetFile );
+ if( !directoryExists( targetDir ) ) {
+ directoryCreate( targetDir );
+ }
fileWrite(
targetFile,
testbox
diff --git a/src/cfml/system/services/JavaService.cfc b/src/cfml/system/services/JavaService.cfc
index e85930e99..6b176e3fc 100644
--- a/src/cfml/system/services/JavaService.cfc
+++ b/src/cfml/system/services/JavaService.cfc
@@ -141,4 +141,25 @@ component accessors="true" singleton {
return 'x64';
}
}
+
+ /**
+ * Guess the current OS
+ */
+ function getCurrentOS() {
+ if( fileSystemUtil.isMac() ) {
+ return 'mac';
+ } else if( fileSystemUtil.isLinux() ) {
+ try {
+ if( fileRead( '/proc/version' ) contains 'alpine' ) {
+ return 'alpine-linux';
+ }
+ } catch( any e ) {
+ // /proc/version may not exist or may not have permissions
+ }
+ return 'linux';
+ } else {
+ return 'windows';
+ }
+ }
+
}
diff --git a/src/cfml/system/services/PackageService.cfc b/src/cfml/system/services/PackageService.cfc
index d242bf474..a445c33fe 100644
--- a/src/cfml/system/services/PackageService.cfc
+++ b/src/cfml/system/services/PackageService.cfc
@@ -296,6 +296,13 @@ component accessors="true" singleton {
// Set variable to allow interceptor-based skipping of package install
var skipInstall = interceptData.skipInstall;
+ // See if the containing package has an override for this type
+ if( !len( installDirectory ) && len( packageType ) && containerBoxJSON.keyExists( 'installPathConventions' ) && containerBoxJSON.installPathConventions.keyExists( packageType ) ) {
+ job.addWarnLog( "Overriding install path for [#packageType#] based on box.json installPathConventions" );
+ // Can be relative or absolute
+ installDirectory = fileSystemUtil.resolvePath( containerBoxJSON.installPathConventions[ packageType ] );
+ }
+
// Else, use package type convention
if( !len( installDirectory ) && len( packageType ) ) {
// If this is a CommandBox command
@@ -599,7 +606,7 @@ component accessors="true" singleton {
} catch( any e ) {
rethrow;
}
-
+
// full ID with endpoint and package like file:/opt/files/foo.zip
if( endpointName != 'forgebox' ) {
var ID = detail;
diff --git a/src/cfml/system/services/ServerService.cfc b/src/cfml/system/services/ServerService.cfc
index 6e21308ef..0ac1a94d2 100644
--- a/src/cfml/system/services/ServerService.cfc
+++ b/src/cfml/system/services/ServerService.cfc
@@ -361,7 +361,7 @@ component accessors="true" singleton {
systemSettings.expandDeepSystemSettings( defaults );
// Mix in environment variable overrides like BOX_SERVER_PROFILE
- loadOverrides( serverJSON, serverInfo, serverProps.verbose ?: serverJSON.verbose ?: defaults.verbose ?: false );
+ loadOverrides( serverJSON, serverInfo, serverProps.verbose ?: serverProps.debug ?: serverJSON.verbose ?: defaults.verbose ?: false );
// Load up our fully-realized server.json-specific env vars into CommandBox's environment
systemSettings.setDeepSystemSettings( serverDetails.serverJSON.env ?: {}, '', '_' );
@@ -1855,6 +1855,87 @@ component accessors="true" singleton {
args.prepend( fileSystemUtil.getNativeShell() );
}
+
+ // Setting this via env var so it doesn't blow up Java 8 and I don't have to try and detect what version of Java the server is using.
+ var javaOpens = [
+ 'java.base/sun.nio.ch',
+ 'java.base/sun.nio.cs',
+ 'java.base/java.io',
+ 'java.base/java.lang',
+ 'java.base/java.lang.annotation',
+ 'java.base/java.lang.invoke',
+ 'java.base/java.lang.module',
+ 'java.base/java.lang.ref',
+ 'java.base/java.lang.reflect',
+ 'java.base/java.math',
+ 'java.base/java.net',
+ 'java.base/java.net.spi',
+ 'java.base/java.nio',
+ 'java.base/java.nio.channels',
+ 'java.base/java.nio.channels.spi',
+ 'java.base/java.nio.charset',
+ 'java.base/java.nio.charset.spi',
+ 'java.base/java.nio.file',
+ 'java.base/java.nio.file.attribute',
+ 'java.base/java.nio.file.spi',
+ 'java.base/java.security',
+ 'java.base/java.security.cert',
+ 'java.base/java.security.interfaces',
+ 'java.base/java.security.spec',
+ 'java.base/java.text',
+ 'java.base/java.text.spi',
+ 'java.base/java.time',
+ 'java.base/java.time.chrono',
+ 'java.base/java.time.format',
+ 'java.base/java.time.temporal',
+ 'java.base/java.time.zone',
+ 'java.base/java.util',
+ 'java.base/java.util.concurrent',
+ 'java.base/java.util.concurrent.atomic',
+ 'java.base/java.util.concurrent.locks',
+ 'java.base/java.util.function',
+ 'java.base/java.util.jar',
+ 'java.base/java.util.regex',
+ 'java.base/java.util.spi',
+ 'java.base/java.util.stream',
+ 'java.base/java.util.zip',
+ 'java.base/javax.crypto',
+ 'java.base/javax.crypto.interfaces',
+ 'java.base/javax.crypto.spec',
+ 'java.base/javax.net',
+ 'java.base/javax.net.ssl',
+ 'java.base/javax.security.auth',
+ 'java.base/javax.security.auth.callback',
+ 'java.base/javax.security.auth.login',
+ 'java.base/javax.security.auth.spi',
+ 'java.base/javax.security.auth.x500',
+ 'java.base/javax.security.cert',
+ 'java.base/sun.net.www.protocol.https',
+ 'java.desktop/com.sun.java.swing.plaf.nimbus',
+ 'java.desktop/com.sun.java.swing.plaf.motif',
+ 'java.desktop/com.sun.java.swing.plaf.nimbus',
+ 'java.desktop/com.sun.java.swing.plaf.windows',
+ 'java.desktop/sun.java2d',
+ 'java.rmi/sun.rmi.transport',
+ 'java.base/sun.security.rsa',
+ 'java.base/sun.security.pkcs',
+ 'java.base/sun.security.x509',
+ 'java.base/sun.security.util',
+ 'java.base/sun.util.cldr',
+ 'java.base/sun.util',
+ 'java.base/sun.util.locale.provider',
+ 'java.management/sun.management'
+ ].reduce( (opens='',o)=>opens &= ' --add-opens=#o#=ALL-UNNAMED' );
+
+ var javaExports = [
+ 'java.desktop/sun.java2d',
+ 'java.base/sun.util'
+ ].reduce( (exports='',o)=>exports &= ' --add-exports=#o#=ALL-UNNAMED' );
+
+ systemSettings.setSystemSetting( 'JDK_JAVA_OPTIONS', systemSettings.getSystemSetting( 'JDK_JAVA_OPTIONS', javaOpens & ' ' & javaExports ) );
+ systemSettings.setSystemSetting( 'COMMANDBOX_HOME', systemSettings.getSystemSetting( 'COMMANDBOX_HOME', expandPath( '/commandbox-home' ) ) );
+ systemSettings.setSystemSetting( 'COMMANDBOX_VERSION', systemSettings.getSystemSetting( 'COMMANDBOX_VERSION', shell.getVersion() ) );
+
// At this point all command line arguments are in place, announce this
var interceptData = {
commandLineArguments=args,
@@ -1881,7 +1962,6 @@ component accessors="true" singleton {
return;
}
-
if( fileSystemUtil.isWindows() ) {
args = args.map( (a)=>replace( a, '"', '\"', 'all' ) );
}
@@ -1901,17 +1981,6 @@ component accessors="true" singleton {
}
}
- // Add COMMANDBOX_HOME env var to the server if not already there
- if ( !currentEnv.containsKey( 'COMMANDBOX_HOME' ) ) {
- currentEnv.put( 'COMMANDBOX_HOME', expandPath( '/commandbox-home' ) );
- }
-
- // Add COMMANDBOX_VERSION env var to the server if not already there
- if ( !currentEnv.containsKey( 'COMMANDBOX_VERSION' ) ) {
- currentEnv.put( 'COMMANDBOX_VERSION', shell.getVersion() );
- }
-
-
// Conjoin standard error and output for convenience.
processBuilder.redirectErrorStream( true );
// Kick off actual process
@@ -3157,7 +3226,6 @@ component accessors="true" singleton {
debugMessages.each( (l)=>job.addLog( l ) );
job.complete( verbose );
}
-
JSONService.mergeData( serverJSON, overrides );
}
diff --git a/src/cfml/system/util/CommandDSL.cfc b/src/cfml/system/util/CommandDSL.cfc
index d531ea6b2..3d0beef0e 100644
--- a/src/cfml/system/util/CommandDSL.cfc
+++ b/src/cfml/system/util/CommandDSL.cfc
@@ -127,7 +127,7 @@ component accessors=true {
if( isStruct( paramStruct[ param ] ?: '' ) ) {
paramStruct[ param ].each( (k,v)=>processedParams.append( '#param#:#k#="#parser.escapeArg( v ?: '' )#"' ) );
} else {
- processedParams.append( '#param#="#parser.escapeArg( paramStruct[ param ] ?: '' )#"' );
+ processedParams.append( '#param#="#parser.escapeArg( paramStruct[ param ] ?: '' )#"' );
}
}
}
@@ -262,7 +262,7 @@ component accessors=true {
try {
if( !isNull( getPipedInput() ) ) {
- var result = shell.callCommand( getTokens(), getReturnOutput(), getPipedInput(), getCommandString() );
+ var result = shell.callCommand( command=getTokens(), returnOutput=getReturnOutput(), piped=getPipedInput(), line=getCommandString() );
} else {
var result = shell.callCommand( command=getTokens(), returnOutput=getReturnOutput(), line=getCommandString() );
}
diff --git a/src/cfml/system/util/ConsolePainter.cfc b/src/cfml/system/util/ConsolePainter.cfc
index 50d91c605..fe1b8c39f 100644
--- a/src/cfml/system/util/ConsolePainter.cfc
+++ b/src/cfml/system/util/ConsolePainter.cfc
@@ -16,11 +16,11 @@ component singleton accessors=true {
property name="job" inject="InteractiveJob";
property name='shell' inject='shell';
property name='multiSelect';
-
+
property name='active' type='boolean' default='false';
property name='taskScheduler';
property name='future';
-
+
function onDIComplete() {
variables.attr = createObject( 'java', 'org.jline.utils.AttributedString' );
setTaskScheduler( wirebox.getTaskScheduler() );
@@ -31,14 +31,14 @@ component singleton accessors=true {
/**
* Starts up the scheduled painting thread if not already started
*
- */
+ */
function start() {
-
+
// If we have a dumb terminal or are running inside a CI server, skip the screen redraws all together.
if( !shell.isTerminalInteractive() || terminal.getWidth() == 0 ) {
return;
}
-
+
if( !getActive() ) {
lock timeout="20" name="ConsolePainter" type="exclusive" {
if( !getActive() ) {
@@ -47,29 +47,29 @@ component singleton accessors=true {
.every( 200 )
.start()
);
-
- setActive( true );
- }
- }
+
+ setActive( true );
+ }
+ }
}
}
-
+
/**
* Stops the scheduled painting thread if no jobs or progress bars are active and it's not already stopped
*
- */
+ */
function stop() {
-
+
// Check if all jobs and progress bars are finished
- if(
- progressBarGeneric.getActive()
- || progressBar.getActive()
- || job.getActive()
+ if(
+ progressBarGeneric.getActive()
+ || progressBar.getActive()
+ || job.getActive()
|| ( !isNull( multiSelect ) && multiSelect.getActive() )
) {
return;
}
-
+
if( getActive() ) {
lock timeout="20" name="ConsolePainter" type="exclusive" {
if( getActive() ) {
@@ -81,36 +81,36 @@ component singleton accessors=true {
}
setActive( false );
clear();
- }
- }
+ }
+ }
}
-
-
+
+
}
-
+
/**
* Stops up the scheduled painting thread and forces any active jobs to error and any active progress bars to clear
*
- */
+ */
function forceStop( string message='' ) {
job.errorRemaining( message );
progressBarGeneric.clear();
progressBar.clear();
stop();
}
-
+
/**
* Draw the lines to the console
*
- */
+ */
function paint() {
try {
var height = max( terminal.getHeight()-2, 0 );
display.resize( terminal.getHeight(), terminal.getWidth() );
-
+
var lines = [];
cursorPosInt = 0;
-
+
// Future enhancement: allow paintable things to be registered with the ConsolePainter intead of these hard coded references.
lines.append( job.getLines(), true );
lines.append( progressBar.getLines(), true );
@@ -121,34 +121,33 @@ component singleton accessors=true {
lines.append( multiSelect.getLines(), true );
}
// /Future enhancement
-
- lines.append( attr.init( ' ' ) );
- lines.append( attr.init( ' ' ) );
+
+ lines.append( attr.init( repeatString( ' ', terminal.getWidth() ) ) );
// Trim to terminal height so the screen doesn't go all jumpy
// If there is more output than screen, the user just doesn't get to see the rest
if( lines.len() > height ) {
lines = lines.slice( lines.len()-height, lines.len()-(lines.len()-height) );
}
-
+
// Add to console and flush
display.update(
lines,
cursorPosInt
);
-
+
} catch( any e ) {
if( !(e.type contains 'interrupt') ) {
systemoutput( e.message & ' ' & e.detail, 1 );
systemoutput( "#e.tagContext[1].template#: line #e.tagContext[1].line#", 1 );
- rethrow;
- }
+ rethrow;
+ }
}
}
-
+
/**
* Clear the console
- */
+ */
function clear() {
display.resize( terminal.getHeight(), terminal.getWidth() );
sleep(100)
@@ -156,6 +155,6 @@ component singleton accessors=true {
[ attr.init( ' ' ) ,attr.init( ' ' ) ,attr.init( ' ' ) ,attr.init( ' ' ) ],
0
);
-
+
}
}
\ No newline at end of file
diff --git a/src/cfml/system/util/MultiSelect.cfc b/src/cfml/system/util/MultiSelect.cfc
index 82de8534f..b318b6fe3 100644
--- a/src/cfml/system/util/MultiSelect.cfc
+++ b/src/cfml/system/util/MultiSelect.cfc
@@ -112,6 +112,9 @@ component accessors=true {
try {
draw();
+ while( isOversize() ) {
+ sleep( 500 );
+ }
while( ( var key = shell.waitForKey() ) != chr( 13 ) || !checkRequired() ) {
if( isUp( key ) ) {
@@ -303,15 +306,39 @@ component accessors=true {
}
function getCursorPosition() {
+ if( isOversize() ) {
+ return {
+ 'row' : 0,
+ 'col' : 0
+ };
+ }
return {
'row' : activeOption-viewportStart+3,
'col' : 3
};
}
+ function isOversize() {
+ return terminal.getHeight() < 10;
+ }
+
function getLines() {
var width=terminal.getWidth();
var height=terminal.getHeight();
+ if( isOversize() ) {
+ viewportStart = 1;
+ lines = [ aStr.fromAnsi( print.red( 'Terminal is too small for multi-select to render!' ) ) ];
+ if( height > 3 ){
+ loop times=height-3 {
+ lines.append( aStr.init( repeatString( ' ', width ) ) );
+ }
+ }
+ return lines;
+ }
+ viewportLength=min(getOptions().len(),height-9);
+ if( viewportStart + viewportLength > getOptions().len()+1 ){
+ viewportStart = 1;
+ }
var i = viewportStart-1;
return getOptions()
.slice( viewportStart, viewportLength )
@@ -328,8 +355,7 @@ component accessors=true {
.prepend( aStr.init( getQuestion() ) )
.prepend( aStr.init( ' ' ) )
.append( aStr.fromAnsi( ( getOptions().len()+1>viewportStart+viewportLength ? print.red( ' << #getOptions().len()+1-(viewportStart+viewportLength)# more below...>>' ) : ' ' ) ) )
- .append( aStr.fromAnsi( print.yellow( ' Use to toggle selections, to submit.' ) ) )
- .append( aStr.init( ' ' ) );
+ .append( aStr.fromAnsi( print.yellow( ' Use to toggle selections, to submit.' ) ) );
}
function checkRequired() {
diff --git a/src/cfml/system/util/Print.cfc b/src/cfml/system/util/Print.cfc
index 747079322..b507ba14d 100644
--- a/src/cfml/system/util/Print.cfc
+++ b/src/cfml/system/util/Print.cfc
@@ -33,6 +33,8 @@
*/
component {
+ processingdirective pageEncoding='UTF-8';
+
property name='cr' inject='cr@constants';
property name='shell' inject='shell';
property name='colors256Data' inject='colors256Data@constants';
@@ -73,6 +75,10 @@ component {
*
**/
function onMissingMethod( missingMethodName, missingMethodArguments ) {
+ return _onMissingMethod( argumentCollection=arguments );
+ }
+
+ private function _onMissingMethod( missingMethodName, missingMethodArguments ) {
// Check for Ctrl-C
shell.checkInterrupted();
@@ -88,7 +94,7 @@ component {
// TODO: Actually use a string buffer
var ANSIString = "";
-
+
var foundANSI = false;
// Text needing formatting
@@ -327,4 +333,86 @@ component {
return ' ' & replaceNoCase( arguments.text, cr, cr & ' ', 'all' );
}
-}
+ public String function columns( required array items, formatUDF=()=>'' ) {
+ var numItems = items.len();
+ var widestItem = items.map( (v)=>len( v ) ).max();
+ var colWdith = widestItem + 4;
+ var termWidth = shell.getTermWidth()-1;
+ var numCols = max( termWidth\colWdith, 1 );
+ var numRows = ceiling( numItems/numCols );
+ var columnText = createObject( 'java', 'java.lang.StringBuilder' ).init( '' );
+
+ loop from=1 to=numRows index="local.row" {
+ loop from=1 to=numCols index="local.col" {
+ var thisIndex = row+((col-1)*numRows);
+ if( thisIndex > numItems ) {
+ var thisItem = '';
+ } else {
+ var thisItem = items[thisIndex];
+ }
+ columnText.append( _onMissingMethod( 'text', [ padRight( thisItem, colWdith ), formatUDF( thisItem, row, col ) ] ) );
+ }
+ columnText.append( cr );
+ }
+ return columnText.toString();
+ }
+
+ /**
+ * Adds characters to the right of a string until the string reaches a certain length.
+ * If the text is already greater than or equal to the maxWidth, the text is returned unchanged.
+ * @text The text to pad.
+ * @maxWidth The number of characters to pad up to.
+ * @padChar The character to use to pad the text.
+ */
+ private string function padRight( required string text, required numeric maxWidth, string padChar = " " ) {
+ var textLength = len( arguments.text );
+ if ( textLength == arguments.maxWidth ) {
+ return arguments.text;
+ } else if( textLength > arguments.maxWidth ) {
+ if( arguments.maxWidth < 4 ) {
+ return left( text, arguments.maxWidth );
+ } else {
+ return left( text, arguments.maxWidth-3 )&'...';
+ }
+ }
+ arguments.text &= repeatString( arguments.padChar, arguments.maxWidth-textLength );
+ return arguments.text;
+ }
+
+ /**
+ * Print a struct of structs as a tree
+ *
+ * @data top level struct
+ * @formatUDF A UDF receiving both a string-concatenated prefix of keys, and an array of the same data. Returns string of special formating for that node of the tree
+ */
+ function tree( required struct data, formatUDF=()=>'' ) {
+ var treeSB = createObject( 'java', 'java.lang.StringBuilder' ).init( '' );
+ _tree( parent=data, prefix='', formatUDF=formatUDF, treeSB=treeSB );
+ return treeSB.toString();
+ }
+
+ private function _tree( required struct parent, required string prefix, required formatUDF, required any treeSB, Array keyPath=[] ) {
+ var i = 0;
+ var keyCount = structCount( arguments.parent );
+ for( var keyName in arguments.parent ) {
+ keyPath.append( keyName );
+ var child = arguments.parent[ keyName ];
+ var childKeyCount = isStruct( child ) ? structCount( child ) : 0;
+ i++;
+ var isLast = ( i == keyCount );
+ var branch = ( isLast ? '└' : '├' ) & '─' & ( childKeyCount ? '┬' : '─' );
+ var branchCont = ( isLast ? ' ' : '│' ) & ' ' & ( childKeyCount ? '│' : ' ' );
+
+ // If the key name has line breaks, output each individually
+ keyName.listToArray( chr(13)&chr(10) ).each( ( l, i )=>{
+ treeSB.append( prefix & ( i == 1 ? branch : branchCont ) & ' ' );
+ treeSB.append( _onMissingMethod( 'line', [ l, formatUDF( keyPath.toList( '' ), keyPath ) ] ) );
+ } );
+
+ if( isStruct( child ) ) {
+ _tree( parent=child, prefix=prefix & ( isLast ? ' ' : '│ ' ), formatUDF=formatUDF, treeSB=treeSB, keyPath=keyPath );
+ }
+ keyPath.deleteAt( keyPath.len() );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/cfml/system/util/PrintBuffer.cfc b/src/cfml/system/util/PrintBuffer.cfc
index 484974b9b..1fa9ae597 100644
--- a/src/cfml/system/util/PrintBuffer.cfc
+++ b/src/cfml/system/util/PrintBuffer.cfc
@@ -37,7 +37,7 @@ component accessors="true" extends="Print"{
clear();
}
- // Once we get the text to print above, we can release the lock while we actually print it.
+ // Once we get the text to print above, we can release the lock while we actually print it.
// If there is an active job, print our output through it
if( job.getActive() ) {
job.addLog( thingToPrint );
@@ -58,7 +58,7 @@ component accessors="true" extends="Print"{
// Proxy through any methods to the actual print helper
function onMissingMethod( missingMethodName, missingMethodArguments ){
var result = super.onMissingMethod( arguments.missingMethodName, arguments.missingMethodArguments );
-
+
// Don't modify the buffer if it's being printed, exclusive because StringBuilder is not thread-safe
lock name='printBuffer-#getObjectID()#' type="exclusive" timeout="20" {
variables.result.append( result );
@@ -81,10 +81,32 @@ component accessors="true" extends="Print"{
boolean debug=false
){
// Don't modify the buffer if it's being printed
- lock name='printBuffer-#getObjectID()#' type="readonly" timeout="20" {
+ lock name='printBuffer-#getObjectID()#' type="exclusive" timeout="20" {
variables.result.append( super.table( argumentCollection=arguments ) );
return this;
}
}
+ function columns( required array items, formatUDF=(s)=>'' ) {
+ // Don't modify the buffer if it's being printed
+ lock name='printBuffer-#getObjectID()#' type="exclusive" timeout="20" {
+ variables.result.append( super.columns( argumentCollection=arguments ) );
+ return this;
+ }
+ }
+
+ /**
+ * Print a struct of structs as a tree
+ *
+ * @data top level struct
+ * @formatUDF A UDF receiving both a string-concatenated prefix of keys, and an array of the same data. Returns string of special formating for that node of the tree
+ */
+ function tree( required struct data, formatUDF=()=>'' ) {
+ // Don't modify the buffer if it's being printed
+ lock name='printBuffer-#getObjectID()#' type="exclusive" timeout="20" {
+ variables.result.append( super.tree( argumentCollection=arguments ) );
+ return this;
+ }
+ }
+
}
diff --git a/src/cfml/system/util/REPLParser.cfc b/src/cfml/system/util/REPLParser.cfc
index c8031b779..286356c69 100644
--- a/src/cfml/system/util/REPLParser.cfc
+++ b/src/cfml/system/util/REPLParser.cfc
@@ -21,6 +21,22 @@ component accessors="true" singleton {
return this;
}
+ private boolean function hasOpenQuotes( required string command ) {
+ var charArray = arguments.command.toCharArray();
+ var isQuoteOpen = false;
+ var quoteType = "";
+ for ( var char in charArray ) {
+ if (!isQuoteOpen && ( char == "'" || char == '"' ) ) {
+ isQuoteOpen = true;
+ quoteType = char;
+ }
+ else if ( isQuoteOpen && char == quoteType ) {
+ isQuoteOpen = false;
+ }
+ }
+ return isQuoteOpen;
+ }
+
/**
* Clears existing command lines and signal we are starting a new command.
**/
@@ -61,21 +77,31 @@ component accessors="true" singleton {
}
}
+
/**
* Returns true if the command is complete and is ready to be executed.
**/
function isCommandComplete() {
- var cfml = getCommandAsString();
- cfml = reReplaceNoCase( cfml, "[""'].*[""']", """""", "all" );
+ var commandString = getCommandAsString();
+ var cfml = reReplaceNoCase( commandString, "[""'].*[""']", """""", "all" );
var numberOfCurlyBrackets = reMatchNoCase( "[{}]", cfml ).len();
var numberOfParenthesis = reMatchNoCase( "[\(\)]", cfml ).len();
- //var numberOfDoubleQuotations = reMatchNoCase( """", cfml ).len();
- //var numberOfSingleQuotations = reMatchNoCase( "'", cfml ).len();
+ var numberOfQuotes = reMatchNoCase( "[""']", commandString ).len();
+ var numberOfBrackets = reMatchNoCase( "[\[\]]", cfml ).len();
var numberOfNonTerminatingEqualSigns = reMatchNoCase( "=\s*$", cfml ).len();
var numberOfMultilineCommentBlocks = reMatchNoCase( "^\s*/\*|\*/", cfml ).len();
var numberOfHashSigns = reMatchNoCase( "##", cfml ).len();
- if ( numberOfCurlyBrackets % 2 == 0 && numberOfParenthesis % 2 == 0 && numberOfNonTerminatingEqualSigns == 0 && numberOfHashSigns % 2 == 0 && numberOfMultilineCommentBlocks % 2 == 0 ) {
+
+ if (
+ numberOfBrackets % 2 == 0
+ && numberOfCurlyBrackets % 2 == 0
+ && numberOfParenthesis % 2 == 0
+ && numberOfNonTerminatingEqualSigns == 0
+ && numberOfHashSigns % 2 == 0
+ && numberOfMultilineCommentBlocks % 2 == 0
+ && ( numberOfQuotes == 0 || !hasOpenQuotes( commandString ) )
+ ) {
return true;
}
return false;
diff --git a/src/java/cliloader/LoaderCLIMain.java b/src/java/cliloader/LoaderCLIMain.java
index 44ec862f8..8e3ccd439 100644
--- a/src/java/cliloader/LoaderCLIMain.java
+++ b/src/java/cliloader/LoaderCLIMain.java
@@ -601,9 +601,13 @@ public static ArrayList< String > initialize( String[] arguments ) throws IOExce
arguments = removeElement( arguments, "-clidebug" );
}
- log.debug( "CLI Java Version:" + System.getProperty( "java.vm.version", System.getProperty( "java.version", "Unknown" ) ) );
- log.debug( "CLI Java Home:" + System.getProperty( "java.home", "Unknown" ) );
- log.debug( "CLI Java Vendor:" + System.getProperty( "java.vendor", "Unknown" ) );
+ if( debug ) {
+ log.debug( "CLI Java Version: " + System.getProperty( "java.vm.version", System.getProperty( "java.version", "Unknown" ) ) );
+ log.debug( "CLI Java Home: " + System.getProperty( "java.home", "Unknown" ) );
+ log.debug( "CLI Java Vendor: " + System.getProperty( "java.vendor", "Unknown" ) );
+ log.debug( "box binary version: " + Util.getResourceAsString( CFML_VERSION_PATH ) );
+ log.debug( "box binary loader version: " + Util.getResourceAsString( VERSION_PROPERTIES_PATH ).split( "=" )[1] );
+ }
System.setProperty( "cfml.cli.debug", debug.toString() );
try {