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 {