diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd6e97d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# IDE +.idea/ +.settings/ + +# Keys and credentials +.songe.key* +.songe.trust* +config.cfg + +# Build files +release/ +build/ +out/ + +# Database +base/ +login-list.cfg + +# Other exec files +lunasql.sh +lunasql-srv.sh +other/ +history + +# Temp files +*~ + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..99a22c0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,520 @@ + + CeCILL FREE SOFTWARE LICENSE AGREEMENT + +Version 2.1 dated 2013-06-21 + + + Notice + +This Agreement is a Free Software license agreement that is the result +of discussions between its authors in order to ensure compliance with +the two main principles guiding its drafting: + + * firstly, compliance with the principles governing the distribution + of Free Software: access to source code, broad rights granted to users, + * secondly, the election of a governing law, French law, with which it + is conformant, both as regards the law of torts and intellectual + property law, and the protection that it offers to both authors and + holders of the economic rights over software. + +The authors of the CeCILL (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) +license are: + +Commissariat à l'énergie atomique et aux énergies alternatives - CEA, a +public scientific, technical and industrial research establishment, +having its principal place of business at 25 rue Leblanc, immeuble Le +Ponant D, 75015 Paris, France. + +Centre National de la Recherche Scientifique - CNRS, a public scientific +and technological establishment, having its principal place of business +at 3 rue Michel-Ange, 75794 Paris cedex 16, France. + +Institut National de Recherche en Informatique et en Automatique - +Inria, a public scientific and technological establishment, having its +principal place of business at Domaine de Voluceau, Rocquencourt, BP +105, 78153 Le Chesnay cedex, France. + + + Preamble + +The purpose of this Free Software license agreement is to grant users +the right to modify and redistribute the software governed by this +license within the framework of an open source distribution model. + +The exercising of this right is conditional upon certain obligations for +users so as to preserve this status for all subsequent redistributions. + +In consideration of access to the source code and the rights to copy, +modify and redistribute granted by the license, users are provided only +with a limited warranty and the software's author, the holder of the +economic rights, and the successive licensors only have limited liability. + +In this respect, the risks associated with loading, using, modifying +and/or developing or reproducing the software by the user are brought to +the user's attention, given its Free Software status, which may make it +complicated to use, with the result that its use is reserved for +developers and experienced professionals having in-depth computer +knowledge. Users are therefore encouraged to load and test the +suitability of the software as regards their requirements in conditions +enabling the security of their systems and/or data to be ensured and, +more generally, to use and operate it in the same conditions of +security. This Agreement may be freely reproduced and published, +provided it is not altered, and that no provisions are either added or +removed herefrom. + +This Agreement may apply to any or all software for which the holder of +the economic rights decides to submit the use thereof to its provisions. + +Frequently asked questions can be found on the official website of the +CeCILL licenses family (http://www.cecill.info/index.en.html) for any +necessary clarification. + + + Article 1 - DEFINITIONS + +For the purpose of this Agreement, when the following expressions +commence with a capital letter, they shall have the following meaning: + +Agreement: means this license agreement, and its possible subsequent +versions and annexes. + +Software: means the software in its Object Code and/or Source Code form +and, where applicable, its documentation, "as is" when the Licensee +accepts the Agreement. + +Initial Software: means the Software in its Source Code and possibly its +Object Code form and, where applicable, its documentation, "as is" when +it is first distributed under the terms and conditions of the Agreement. + +Modified Software: means the Software modified by at least one +Contribution. + +Source Code: means all the Software's instructions and program lines to +which access is required so as to modify the Software. + +Object Code: means the binary files originating from the compilation of +the Source Code. + +Holder: means the holder(s) of the economic rights over the Initial +Software. + +Licensee: means the Software user(s) having accepted the Agreement. + +Contributor: means a Licensee having made at least one Contribution. + +Licensor: means the Holder, or any other individual or legal entity, who +distributes the Software under the Agreement. + +Contribution: means any or all modifications, corrections, translations, +adaptations and/or new functions integrated into the Software by any or +all Contributors, as well as any or all Internal Modules. + +Module: means a set of sources files including their documentation that +enables supplementary functions or services in addition to those offered +by the Software. + +External Module: means any or all Modules, not derived from the +Software, so that this Module and the Software run in separate address +spaces, with one calling the other when they are run. + +Internal Module: means any or all Module, connected to the Software so +that they both execute in the same address space. + +GNU GPL: means the GNU General Public License version 2 or any +subsequent version, as published by the Free Software Foundation Inc. + +GNU Affero GPL: means the GNU Affero General Public License version 3 or +any subsequent version, as published by the Free Software Foundation Inc. + +EUPL: means the European Union Public License version 1.1 or any +subsequent version, as published by the European Commission. + +Parties: mean both the Licensee and the Licensor. + +These expressions may be used both in singular and plural form. + + + Article 2 - PURPOSE + +The purpose of the Agreement is the grant by the Licensor to the +Licensee of a non-exclusive, transferable and worldwide license for the +Software as set forth in Article 5 <#scope> hereinafter for the whole +term of the protection granted by the rights over said Software. + + + Article 3 - ACCEPTANCE + +3.1 The Licensee shall be deemed as having accepted the terms and +conditions of this Agreement upon the occurrence of the first of the +following events: + + * (i) loading the Software by any or all means, notably, by + downloading from a remote server, or by loading from a physical medium; + * (ii) the first time the Licensee exercises any of the rights granted + hereunder. + +3.2 One copy of the Agreement, containing a notice relating to the +characteristics of the Software, to the limited warranty, and to the +fact that its use is restricted to experienced users has been provided +to the Licensee prior to its acceptance as set forth in Article 3.1 +<#accepting> hereinabove, and the Licensee hereby acknowledges that it +has read and understood it. + + + Article 4 - EFFECTIVE DATE AND TERM + + + 4.1 EFFECTIVE DATE + +The Agreement shall become effective on the date when it is accepted by +the Licensee as set forth in Article 3.1 <#accepting>. + + + 4.2 TERM + +The Agreement shall remain in force for the entire legal term of +protection of the economic rights over the Software. + + + Article 5 - SCOPE OF RIGHTS GRANTED + +The Licensor hereby grants to the Licensee, who accepts, the following +rights over the Software for any or all use, and for the term of the +Agreement, on the basis of the terms and conditions set forth hereinafter. + +Besides, if the Licensor owns or comes to own one or more patents +protecting all or part of the functions of the Software or of its +components, the Licensor undertakes not to enforce the rights granted by +these patents against successive Licensees using, exploiting or +modifying the Software. If these patents are transferred, the Licensor +undertakes to have the transferees subscribe to the obligations set +forth in this paragraph. + + + 5.1 RIGHT OF USE + +The Licensee is authorized to use the Software, without any limitation +as to its fields of application, with it being hereinafter specified +that this comprises: + + 1. permanent or temporary reproduction of all or part of the Software + by any or all means and in any or all form. + + 2. loading, displaying, running, or storing the Software on any or all + medium. + + 3. entitlement to observe, study or test its operation so as to + determine the ideas and principles behind any or all constituent + elements of said Software. This shall apply when the Licensee + carries out any or all loading, displaying, running, transmission or + storage operation as regards the Software, that it is entitled to + carry out hereunder. + + + 5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS + +The right to make Contributions includes the right to translate, adapt, +arrange, or make any or all modifications to the Software, and the right +to reproduce the resulting software. + +The Licensee is authorized to make any or all Contributions to the +Software provided that it includes an explicit notice that it is the +author of said Contribution and indicates the date of the creation thereof. + + + 5.3 RIGHT OF DISTRIBUTION + +In particular, the right of distribution includes the right to publish, +transmit and communicate the Software to the general public on any or +all medium, and by any or all means, and the right to market, either in +consideration of a fee, or free of charge, one or more copies of the +Software by any means. + +The Licensee is further authorized to distribute copies of the modified +or unmodified Software to third parties according to the terms and +conditions set forth hereinafter. + + + 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION + +The Licensee is authorized to distribute true copies of the Software in +Source Code or Object Code form, provided that said distribution +complies with all the provisions of the Agreement and is accompanied by: + + 1. a copy of the Agreement, + + 2. a notice relating to the limitation of both the Licensor's warranty + and liability as set forth in Articles 8 and 9, + +and that, in the event that only the Object Code of the Software is +redistributed, the Licensee allows effective access to the full Source +Code of the Software for a period of at least three years from the +distribution of the Software, it being understood that the additional +acquisition cost of the Source Code shall not exceed the cost of the +data transfer. + + + 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE + +When the Licensee makes a Contribution to the Software, the terms and +conditions for the distribution of the resulting Modified Software +become subject to all the provisions of this Agreement. + +The Licensee is authorized to distribute the Modified Software, in +source code or object code form, provided that said distribution +complies with all the provisions of the Agreement and is accompanied by: + + 1. a copy of the Agreement, + + 2. a notice relating to the limitation of both the Licensor's warranty + and liability as set forth in Articles 8 and 9, + +and, in the event that only the object code of the Modified Software is +redistributed, + + 3. a note stating the conditions of effective access to the full source + code of the Modified Software for a period of at least three years + from the distribution of the Modified Software, it being understood + that the additional acquisition cost of the source code shall not + exceed the cost of the data transfer. + + + 5.3.3 DISTRIBUTION OF EXTERNAL MODULES + +When the Licensee has developed an External Module, the terms and +conditions of this Agreement do not apply to said External Module, that +may be distributed under a separate license agreement. + + + 5.3.4 COMPATIBILITY WITH OTHER LICENSES + +The Licensee can include a code that is subject to the provisions of one +of the versions of the GNU GPL, GNU Affero GPL and/or EUPL in the +Modified or unmodified Software, and distribute that entire code under +the terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL. + +The Licensee can include the Modified or unmodified Software in a code +that is subject to the provisions of one of the versions of the GNU GPL, +GNU Affero GPL and/or EUPL and distribute that entire code under the +terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL. + + + Article 6 - INTELLECTUAL PROPERTY + + + 6.1 OVER THE INITIAL SOFTWARE + +The Holder owns the economic rights over the Initial Software. Any or +all use of the Initial Software is subject to compliance with the terms +and conditions under which the Holder has elected to distribute its work +and no one shall be entitled to modify the terms and conditions for the +distribution of said Initial Software. + +The Holder undertakes that the Initial Software will remain ruled at +least by this Agreement, for the duration set forth in Article 4.2 <#term>. + + + 6.2 OVER THE CONTRIBUTIONS + +The Licensee who develops a Contribution is the owner of the +intellectual property rights over this Contribution as defined by +applicable law. + + + 6.3 OVER THE EXTERNAL MODULES + +The Licensee who develops an External Module is the owner of the +intellectual property rights over this External Module as defined by +applicable law and is free to choose the type of agreement that shall +govern its distribution. + + + 6.4 JOINT PROVISIONS + +The Licensee expressly undertakes: + + 1. not to remove, or modify, in any manner, the intellectual property + notices attached to the Software; + + 2. to reproduce said notices, in an identical manner, in the copies of + the Software modified or not. + +The Licensee undertakes not to directly or indirectly infringe the +intellectual property rights on the Software of the Holder and/or +Contributors, and to take, where applicable, vis-à-vis its staff, any +and all measures required to ensure respect of said intellectual +property rights of the Holder and/or Contributors. + + + Article 7 - RELATED SERVICES + +7.1 Under no circumstances shall the Agreement oblige the Licensor to +provide technical assistance or maintenance services for the Software. + +However, the Licensor is entitled to offer this type of services. The +terms and conditions of such technical assistance, and/or such +maintenance, shall be set forth in a separate instrument. Only the +Licensor offering said maintenance and/or technical assistance services +shall incur liability therefor. + +7.2 Similarly, any Licensor is entitled to offer to its licensees, under +its sole responsibility, a warranty, that shall only be binding upon +itself, for the redistribution of the Software and/or the Modified +Software, under terms and conditions that it is free to decide. Said +warranty, and the financial terms and conditions of its application, +shall be subject of a separate instrument executed between the Licensor +and the Licensee. + + + Article 8 - LIABILITY + +8.1 Subject to the provisions of Article 8.2, the Licensee shall be +entitled to claim compensation for any direct loss it may have suffered +from the Software as a result of a fault on the part of the relevant +Licensor, subject to providing evidence thereof. + +8.2 The Licensor's liability is limited to the commitments made under +this Agreement and shall not be incurred as a result of in particular: +(i) loss due the Licensee's total or partial failure to fulfill its +obligations, (ii) direct or consequential loss that is suffered by the +Licensee due to the use or performance of the Software, and (iii) more +generally, any consequential loss. In particular the Parties expressly +agree that any or all pecuniary or business loss (i.e. loss of data, +loss of profits, operating loss, loss of customers or orders, +opportunity cost, any disturbance to business activities) or any or all +legal proceedings instituted against the Licensee by a third party, +shall constitute consequential loss and shall not provide entitlement to +any or all compensation from the Licensor. + + + Article 9 - WARRANTY + +9.1 The Licensee acknowledges that the scientific and technical +state-of-the-art when the Software was distributed did not enable all +possible uses to be tested and verified, nor for the presence of +possible defects to be detected. In this respect, the Licensee's +attention has been drawn to the risks associated with loading, using, +modifying and/or developing and reproducing the Software which are +reserved for experienced users. + +The Licensee shall be responsible for verifying, by any or all means, +the suitability of the product for its requirements, its good working +order, and for ensuring that it shall not cause damage to either persons +or properties. + +9.2 The Licensor hereby represents, in good faith, that it is entitled +to grant all the rights over the Software (including in particular the +rights set forth in Article 5 <#scope>). + +9.3 The Licensee acknowledges that the Software is supplied "as is" by +the Licensor without any other express or tacit warranty, other than +that provided for in Article 9.2 <#good-faith> and, in particular, +without any warranty as to its commercial value, its secured, safe, +innovative or relevant nature. + +Specifically, the Licensor does not warrant that the Software is free +from any error, that it will operate without interruption, that it will +be compatible with the Licensee's own equipment and software +configuration, nor that it will meet the Licensee's requirements. + +9.4 The Licensor does not either expressly or tacitly warrant that the +Software does not infringe any third party intellectual property right +relating to a patent, software or any other property right. Therefore, +the Licensor disclaims any and all liability towards the Licensee +arising out of any or all proceedings for infringement that may be +instituted in respect of the use, modification and redistribution of the +Software. Nevertheless, should such proceedings be instituted against +the Licensee, the Licensor shall provide it with technical and legal +expertise for its defense. Such technical and legal expertise shall be +decided on a case-by-case basis between the relevant Licensor and the +Licensee pursuant to a memorandum of understanding. The Licensor +disclaims any and all liability as regards the Licensee's use of the +name of the Software. No warranty is given as regards the existence of +prior rights over the name of the Software or as regards the existence +of a trademark. + + + Article 10 - TERMINATION + +10.1 In the event of a breach by the Licensee of its obligations +hereunder, the Licensor may automatically terminate this Agreement +thirty (30) days after notice has been sent to the Licensee and has +remained ineffective. + +10.2 A Licensee whose Agreement is terminated shall no longer be +authorized to use, modify or distribute the Software. However, any +licenses that it may have granted prior to termination of the Agreement +shall remain valid subject to their having been granted in compliance +with the terms and conditions hereof. + + + Article 11 - MISCELLANEOUS + + + 11.1 EXCUSABLE EVENTS + +Neither Party shall be liable for any or all delay, or failure to +perform the Agreement, that may be attributable to an event of force +majeure, an act of God or an outside cause, such as defective +functioning or interruptions of the electricity or telecommunications +networks, network paralysis following a virus attack, intervention by +government authorities, natural disasters, water damage, earthquakes, +fire, explosions, strikes and labor unrest, war, etc. + +11.2 Any failure by either Party, on one or more occasions, to invoke +one or more of the provisions hereof, shall under no circumstances be +interpreted as being a waiver by the interested Party of its right to +invoke said provision(s) subsequently. + +11.3 The Agreement cancels and replaces any or all previous agreements, +whether written or oral, between the Parties and having the same +purpose, and constitutes the entirety of the agreement between said +Parties concerning said purpose. No supplement or modification to the +terms and conditions hereof shall be effective as between the Parties +unless it is made in writing and signed by their duly authorized +representatives. + +11.4 In the event that one or more of the provisions hereof were to +conflict with a current or future applicable act or legislative text, +said act or legislative text shall prevail, and the Parties shall make +the necessary amendments so as to comply with said act or legislative +text. All other provisions shall remain effective. Similarly, invalidity +of a provision of the Agreement, for any reason whatsoever, shall not +cause the Agreement as a whole to be invalid. + + + 11.5 LANGUAGE + +The Agreement is drafted in both French and English and both versions +are deemed authentic. + + + Article 12 - NEW VERSIONS OF THE AGREEMENT + +12.1 Any person is authorized to duplicate and distribute copies of this +Agreement. + +12.2 So as to ensure coherence, the wording of this Agreement is +protected and may only be modified by the authors of the License, who +reserve the right to periodically publish updates or new versions of the +Agreement, each with a separate number. These subsequent versions may +address new issues encountered by Free Software. + +12.3 Any Software distributed under a given version of the Agreement may +only be subsequently distributed under the same version of the Agreement +or a subsequent version, subject to the provisions of Article 5.3.4 +<#compatibility>. + + + Article 13 - GOVERNING LAW AND JURISDICTION + +13.1 The Agreement is governed by French law. The Parties agree to +endeavor to seek an amicable solution to any disagreements or disputes +that may arise during the performance of the Agreement. + +13.2 Failing an amicable solution within two (2) months as from their +occurrence, and unless emergency proceedings are necessary, the +disagreements or disputes shall be referred to the Paris Courts having +jurisdiction, by the more diligent Party. + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2aea5d3 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +lunasql +======= + +## Description + +AAnother naive but positive and fun Java JDBC SQL shell client. Designed to simply query databases by JDBC, or automate database tasks and prototype applications before real implementation. + +This is an old personal project that I used to develop occasionally, and that I just put on GitHub under an opensource license, but the development started in 2009! So please bear with me as to the quality of the code. It works, and it can be useful! + +## Features + + * connects to any database by JDBC driver, + * sends SQL commands to database server, + * adds predefined useful commands and macros, + * is highly configurable by command line or file, + * accepts user-defined macros and command plugins, + * evaluates expressions by JSR-223, + * embeds a minimalistic graphical IDE, + * includes an HTTP server for remote querying, + * secures scripts execution by digital signature, + * provides useful javascript scripting libraries, + * sources recently released on GitHub, + * only one (small) jar file! + +Enjoy! + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f2bc9a3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +# Security Policy + +If you discover a security vulnerability, please contact me. Please do not post the vulnerability publicly, as it could be exploited by a hacker. I can also provide a PGP key. Thank you. + diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..5eee811 --- /dev/null +++ b/build.xml @@ -0,0 +1,1402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..7f5832c --- /dev/null +++ b/init.sql @@ -0,0 +1,21 @@ +/* ************* + * INIT et TESTS + * *************/ +-- +need 4.8 + +def tests-unit exec "LunaSQL-4.9/misc/tests-unit.sql" +def tests-perf exec "LunaSQL-4.9/misc/tests-perf.sql" +def pok,pko {print ok} {print ko} + +opt :ON_INIT {print "Bonjour"} +opt :ON_QUIT {print "Au revoir"} + +return + + + +/* $$ BEGIN SIGNATURE $$ +DJk6bXiYW92tz1vhCMG+iAuVGQKs3KntVsLRfyWgh8IBeJg6fTTFQNIr5T3YzaIaNvAc +zEyBo/Had//OYoyiGIgzCR5mvXDoOepRuKZLnYAV6elNcJnxoEDDp4gFBOcJLO5MbjQA + * $$ END SIGNATURE $$ */ diff --git a/lunasql.iml b/lunasql.iml new file mode 100644 index 0000000..369ee75 --- /dev/null +++ b/lunasql.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/CmdEchoPlugin.class b/plugins/CmdEchoPlugin.class new file mode 100755 index 0000000..0c485f1 Binary files /dev/null and b/plugins/CmdEchoPlugin.class differ diff --git a/plugins/CmdEchoPlugin.java b/plugins/CmdEchoPlugin.java new file mode 100755 index 0000000..1a5e572 --- /dev/null +++ b/plugins/CmdEchoPlugin.java @@ -0,0 +1,33 @@ +import lunasql.lib.Contexte; +import lunasql.cmd.Instruction; + +/** + * Commande ECHO
+ * Copie de la commande PRINT pour test des greffons + * + * Utilisation : + * SQL> plugin echo CmdEchoPlugin + * SQL> echo ?a fonctionne + * + * @author M.P. + */ +public class CmdEchoPlugin extends Instruction { + + public CmdEchoPlugin(Contexte cont) { + super(cont, TYPE_CMDPLG, "ECHO", null); + } + + public int execute() { + if (cont.getVerbose() >= Contexte.VERB_AFF) cont.println(getSCommand(1)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + cont.setValeur(null); // ne retourne rien + return RET_CONTINUE; + } + public String getDesc(){ + return " echo Affiche un message\n"; + } + public String getHelp(){ + return "ECHO msg : affiche le message msg"; + } +}// class + diff --git a/scripts/Calendar.bsh b/scripts/Calendar.bsh new file mode 100644 index 0000000..bd20508 --- /dev/null +++ b/scripts/Calendar.bsh @@ -0,0 +1,116 @@ +/* + * MyCalendar.java a simple program to print the calendar of a month / year. + * This program gets two integers from user: + * - the index of the month (1-12) + * - the year number + * + * For example, the user would enter + * 7 2010 + * for July 2010. + * Then, this program will print the calendar of that month. + * + * En LunaSQL: +def calendar { + if $(arg_nb)==1 { + def month $(arg1) + def year $(_DAY_DATE_F&0-3) + } elseif $(arg_nb)==2 { + def month $(arg1) + def year $(arg2) + } else { + def month $(_DAY_DATE_F&4-5) + def year $(_DAY_DATE_F&0-3) + } + exec calendar.bsh $(month) $(year) + undef month year +} + */ + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; + +/* + * prints a calendar month based on month / year info + */ +private static void printCalendarMonthYear(int month, int year) { + // create a new GregorianCalendar object + Calendar cal = new GregorianCalendar(); + // set its date to the first day of the month/year given by user + cal.clear(); + cal.set(year, month - 1, 1); + // print calendar header + System.out.println("\n" + cal.getDisplayName(Calendar.MONTH, Calendar.LONG, + Locale.US) + " " + cal.get(Calendar.YEAR)); + // obtain the weekday of the first day of month. + int firstWeekdayOfMonth = cal.get(Calendar.DAY_OF_WEEK); + // obtain the number of days in month. + int numberOfMonthDays = cal.getActualMaximum(Calendar.DAY_OF_MONTH); + // print anonymous calendar month based on the weekday of the first + // day of the month and the number of days in month: + printCalendar(numberOfMonthDays, firstWeekdayOfMonth); +} + +/* + * prints an anonymous calendar month based on the weekday of the first + * day of the month and the number of days in month: + */ +private static void printCalendar(int numberOfMonthDays, int firstWeekdayOfMonth) { + // reset index of weekday + int weekdayIndex = 0; + // print calendar weekday header + System.out.println("Su Mo Tu We Th Fr Sa"); + // leave/skip weekdays before the first day of month + for (int day = 1; day < firstWeekdayOfMonth; day++) { + System.out.print(" "); + weekdayIndex++; + } + // print the days of month in tabular format. + for (int day = 1; day <= numberOfMonthDays; day++) { + // print day + System.out.printf("%1$2d", day); + // next weekday + weekdayIndex++; + // if it is the last weekday + if (weekdayIndex == 7) { + // reset it + weekdayIndex = 0; + // and go to next line + System.out.println(); + } else { // otherwise + // print space + System.out.print(" "); + } + } + // print a final new-line. + System.out.println(); +} + +/* + * The main program asks users for month and years, + * then it evaluates the weekday of the first day + * of that month as well as the number of days in that + * month. + */ +// represents the month (1-12) +String monthText = scr_args.get(0); +// represents the year +String yearText = scr_args.get(1); +try { + // convert month and year to integer. + // throws NumberFormatException if not convertible. + // It would be caught below: + int month = Integer.parseInt(monthText); + int year = Integer.parseInt(yearText); + // check if it is a valid month + if (month < 1 || month > 12) + throw new Exception("Invalid index for month: " + month); + // print the calendar for the given month/year. + printCalendarMonthYear(month, year); +} catch (NumberFormatException e) { + // handles NumberFormatException + System.err.println("Number Format Error: " + e.getMessage()); +} catch (Exception e) { + // handles any other Exception + System.err.println(e.getMessage()); +} diff --git a/scripts/creation.sql b/scripts/creation.sql new file mode 100644 index 0000000..a430390 --- /dev/null +++ b/scripts/creation.sql @@ -0,0 +1,16 @@ + +drop table commandes +drop table rapports +drop table produits +drop table clients +drop table typespro + +create table clients (id identity not null primary key, nom varchar) +create table typespro (id identity not null primary key, lib varchar) +create table produits (id identity not null primary key, lib varchar, prix real, + idtyp bigint not null references typespro(id)) +create table rapports (id identity not null primary key, montant real) + idpro bigint not null references produits(id), +create table commandes (idpro bigint not null references produits(id), + idcli bigint not null references clients(id), + datecmd datetime not null) diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..bb42e7d --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: lunasql.Main + diff --git a/src/jline/ANSIBuffer.java b/src/jline/ANSIBuffer.java new file mode 100755 index 0000000..1bbd4ef --- /dev/null +++ b/src/jline/ANSIBuffer.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +/** + * A buffer that can contain ANSI text. + * + * @author Marc Prud'hommeaux + */ +public class ANSIBuffer { + private boolean ansiEnabled = true; + private final StringBuffer ansiBuffer = new StringBuffer(); + private final StringBuffer plainBuffer = new StringBuffer(); + + public ANSIBuffer() { + } + + public ANSIBuffer(final String str) { + append(str); + } + + public void setAnsiEnabled(final boolean ansi) { + this.ansiEnabled = ansi; + } + + public boolean getAnsiEnabled() { + return this.ansiEnabled; + } + + public String getAnsiBuffer() { + return ansiBuffer.toString(); + } + + public String getPlainBuffer() { + return plainBuffer.toString(); + } + + public String toString(final boolean ansi) { + return ansi ? getAnsiBuffer() : getPlainBuffer(); + } + + public String toString() { + return toString(ansiEnabled); + } + + public ANSIBuffer append(final String str) { + ansiBuffer.append(str); + plainBuffer.append(str); + + return this; + } + + public ANSIBuffer attrib(final String str, final int code) { + ansiBuffer.append(ANSICodes.attrib(code)).append(str) + .append(ANSICodes.attrib(ANSICodes.OFF)); + plainBuffer.append(str); + + return this; + } + + public ANSIBuffer red(final String str) { + return attrib(str, ANSICodes.FG_RED); + } + + public ANSIBuffer blue(final String str) { + return attrib(str, ANSICodes.FG_BLUE); + } + + public ANSIBuffer green(final String str) { + return attrib(str, ANSICodes.FG_GREEN); + } + + public ANSIBuffer black(final String str) { + return attrib(str, ANSICodes.FG_BLACK); + } + + public ANSIBuffer yellow(final String str) { + return attrib(str, ANSICodes.FG_YELLOW); + } + + public ANSIBuffer magenta(final String str) { + return attrib(str, ANSICodes.FG_MAGENTA); + } + + public ANSIBuffer cyan(final String str) { + return attrib(str, ANSICodes.FG_CYAN); + } + + public ANSIBuffer bold(final String str) { + return attrib(str, ANSICodes.BOLD); + } + + public ANSIBuffer underscore(final String str) { + return attrib(str, ANSICodes.UNDERSCORE); + } + + public ANSIBuffer blink(final String str) { + return attrib(str, ANSICodes.BLINK); + } + + public ANSIBuffer reverse(final String str) { + return attrib(str, ANSICodes.REVERSE); + } + + public static class ANSICodes { + static final int OFF = 0; + static final int BOLD = 1; + static final int UNDERSCORE = 4; + static final int BLINK = 5; + static final int REVERSE = 7; + static final int CONCEALED = 8; + static final int FG_BLACK = 30; + static final int FG_RED = 31; + static final int FG_GREEN = 32; + static final int FG_YELLOW = 33; + static final int FG_BLUE = 34; + static final int FG_MAGENTA = 35; + static final int FG_CYAN = 36; + static final int FG_WHITE = 37; + static final char ESC = 27; + + /** + * Constructor is private since this is a utility class. + */ + private ANSICodes() { + } + + /** + * Sets the screen mode. The mode will be one of the following values: + *
+          * mode     description
+          * ----------------------------------------
+          *   0      40 x 148 x 25 monochrome (text)
+          *   1      40 x 148 x 25 color (text)
+          *   2      80 x 148 x 25 monochrome (text)
+          *   3      80 x 148 x 25 color (text)
+          *   4      320 x 148 x 200 4-color (graphics)
+          *   5      320 x 148 x 200 monochrome (graphics)
+          *   6      640 x 148 x 200 monochrome (graphics)
+          *   7      Enables line wrapping
+          *  13      320 x 148 x 200 color (graphics)
+          *  14      640 x 148 x 200 color (16-color graphics)
+          *  15      640 x 148 x 350 monochrome (2-color graphics)
+          *  16      640 x 148 x 350 color (16-color graphics)
+          *  17      640 x 148 x 480 monochrome (2-color graphics)
+          *  18      640 x 148 x 480 color (16-color graphics)
+          *  19      320 x 148 x 200 color (256-color graphics)
+          * 
+ */ + public static String setmode(final int mode) { + return ESC + "[=" + mode + "h"; + } + + /** + * Same as setmode () except for mode = 7, which disables line + * wrapping (useful for writing the right-most column without + * scrolling to the next line). + */ + public static String resetmode(final int mode) { + return ESC + "[=" + mode + "l"; + } + + /** + * Clears the screen and moves the cursor to the home postition. + */ + public static String clrscr() { + return ESC + "[2J"; + } + + /** + * Removes all characters from the current cursor position until + * the end of the line. + */ + public static String clreol() { + return ESC + "[K"; + } + + /** + * Moves the cursor n positions to the left. If n is greater or + * equal to the current cursor column, the cursor is moved to the + * first column. + */ + public static String left(final int n) { + return ESC + "[" + n + "D"; + } + + /** + * Moves the cursor n positions to the right. If n plus the current + * cursor column is greater than the rightmost column, the cursor + * is moved to the rightmost column. + */ + public static String right(final int n) { + return ESC + "[" + n + "C"; + } + + /** + * Moves the cursor n rows up without changing the current column. + * If n is greater than or equal to the current row, the cursor is + * placed in the first row. + */ + public static String up(final int n) { + return ESC + "[" + n + "A"; + } + + /** + * Moves the cursor n rows down. If n plus the current row is greater + * than the bottom row, the cursor is moved to the bottom row. + */ + public static String down(final int n) { + return ESC + "[" + n + "B"; + } + + /* + * Moves the cursor to the given row and column. (1,1) represents + * the upper left corner. The lower right corner of a usual DOS + * screen is (25, 80). + */ + public static String gotoxy(final int row, final int column) { + return ESC + "[" + row + ";" + column + "H"; + } + + /** + * Saves the current cursor position. + */ + public static String save() { + return ESC + "[s"; + } + + /** + * Restores the saved cursor position. + */ + public static String restore() { + return ESC + "[u"; + } + + /** + * Sets the character attribute. It will be + * one of the following character attributes: + * + *
+          * Text attributes
+          *    0    All attributes off
+          *    1    Bold on
+          *    4    Underscore (on monochrome display adapter only)
+          *    5    Blink on
+          *    7    Reverse video on
+          *    8    Concealed on
+          *
+          *   Foreground colors
+          *    30    Black
+          *    31    Red
+          *    32    Green
+          *    33    Yellow
+          *    34    Blue
+          *    35    Magenta
+          *    36    Cyan
+          *    37    White
+          *
+          *   Background colors
+          *    40    Black
+          *    41    Red
+          *    42    Green
+          *    43    Yellow
+          *    44    Blue
+          *    45    Magenta
+          *    46    Cyan
+          *    47    White
+          * 
+ * + * The attributes remain in effect until the next attribute command + * is sent. + */ + public static String attrib(final int attr) { + return ESC + "[" + attr + "m"; + } + + /** + * Sets the key with the given code to the given value. code must be + * derived from the following table, value must + * be any semicolon-separated + * combination of String (enclosed in double quotes) and numeric values. + * For example, to set F1 to the String "Hello F1", followed by a CRLF + * sequence, one can use: ANSI.setkey ("0;59", "\"Hello F1\";13;10"). + * Heres's the table of key values: + *
+          * Key                       Code      SHIFT+code  CTRL+code  ALT+code
+          * ---------------------------------------------------------------
+          * F1                        0;59      0;84        0;94       0;104
+          * F2                        0;60      0;85        0;95       0;105
+          * F3                        0;61      0;86        0;96       0;106
+          * F4                        0;62      0;87        0;97       0;107
+          * F5                        0;63      0;88        0;98       0;108
+          * F6                        0;64      0;89        0;99       0;109
+          * F7                        0;65      0;90        0;100      0;110
+          * F8                        0;66      0;91        0;101      0;111
+          * F9                        0;67      0;92        0;102      0;112
+          * F10                       0;68      0;93        0;103      0;113
+          * F11                       0;133     0;135       0;137      0;139
+          * F12                       0;134     0;136       0;138      0;140
+          * HOME (num keypad)         0;71      55          0;119      --
+          * UP ARROW (num keypad)     0;72      56          (0;141)    --
+          * PAGE UP (num keypad)      0;73      57          0;132      --
+          * LEFT ARROW (num keypad)   0;75      52          0;115      --
+          * RIGHT ARROW (num keypad)  0;77      54          0;116      --
+          * END (num keypad)          0;79      49          0;117      --
+          * DOWN ARROW (num keypad)   0;80      50          (0;145)    --
+          * PAGE DOWN (num keypad)    0;81      51          0;118      --
+          * INSERT (num keypad)       0;82      48          (0;146)    --
+          * DELETE  (num keypad)      0;83      46          (0;147)    --
+          * HOME                      (224;71)  (224;71)    (224;119)  (224;151)
+          * UP ARROW                  (224;72)  (224;72)    (224;141)  (224;152)
+          * PAGE UP                   (224;73)  (224;73)    (224;132)  (224;153)
+          * LEFT ARROW                (224;75)  (224;75)    (224;115)  (224;155)
+          * RIGHT ARROW               (224;77)  (224;77)    (224;116)  (224;157)
+          * END                       (224;79)  (224;79)    (224;117)  (224;159)
+          * DOWN ARROW                (224;80)  (224;80)    (224;145)  (224;154)
+          * PAGE DOWN                 (224;81)  (224;81)    (224;118)  (224;161)
+          * INSERT                    (224;82)  (224;82)    (224;146)  (224;162)
+          * DELETE                    (224;83)  (224;83)    (224;147)  (224;163)
+          * PRINT SCREEN              --        --          0;114      --
+          * PAUSE/BREAK               --        --          0;0        --
+          * BACKSPACE                 8         8           127        (0)
+          * ENTER                     13        --          10         (0
+          * TAB                       9         0;15        (0;148)    (0;165)
+          * NULL                      0;3       --          --         --
+          * A                         97        65          1          0;30
+          * B                         98        66          2          0;48
+          * C                         99        66          3          0;46
+          * D                         100       68          4          0;32
+          * E                         101       69          5          0;18
+          * F                         102       70          6          0;33
+          * G                         103       71          7          0;34
+          * H                         104       72          8          0;35
+          * I                         105       73          9          0;23
+          * J                         106       74          10         0;36
+          * K                         107       75          11         0;37
+          * L                         108       76          12         0;38
+          * M                         109       77          13         0;50
+          * N                         110       78          14         0;49
+          * O                         111       79          15         0;24
+          * P                         112       80          16         0;25
+          * Q                         113       81          17         0;16
+          * R                         114       82          18         0;19
+          * S                         115       83          19         0;31
+          * T                         116       84          20         0;20
+          * U                         117       85          21         0;22
+          * V                         118       86          22         0;47
+          * W                         119       87          23         0;17
+          * X                         120       88          24         0;45
+          * Y                         121       89          25         0;21
+          * Z                         122       90          26         0;44
+          * 1                         49        33          --         0;120
+          * 2                         50        64          0          0;121
+          * 3                         51        35          --         0;122
+          * 4                         52        36          --         0;123
+          * 5                         53        37          --         0;124
+          * 6                         54        94          30         0;125
+          * 7                         55        38          --         0;126
+          * 8                         56        42          --         0;126
+          * 9                         57        40          --         0;127
+          * 0                         48        41          --         0;129
+          * -                         45        95          31         0;130
+          * =                         61        43          ---        0;131
+          * [                         91        123         27         0;26
+          * ]                         93        125         29         0;27
+          *                           92        124         28         0;43
+          * ;                         59        58          --         0;39
+          * '                         39        34          --         0;40
+          * ,                         44        60          --         0;51
+          * .                         46        62          --         0;52
+          * /                         47        63          --         0;53
+          * `                         96        126         --         (0;41)
+          * ENTER (keypad)            13        --          10         (0;166)
+          * / (keypad)                47        47          (0;142)    (0;74)
+          * * (keypad)                42        (0;144)     (0;78)     --
+          * - (keypad)                45        45          (0;149)    (0;164)
+          * + (keypad)                43        43          (0;150)    (0;55)
+          * 5 (keypad)                (0;76)    53          (0;143)    --
+          */
+        public static String setkey(final String code, final String value) {
+            return ESC + "[" + code + ";" + value + "p";
+        }
+    }
+
+    public static void main(final String[] args) throws Exception {
+        // sequence, one can use: ANSI.setkey ("0;59", "\"Hello F1\";13;10").
+        BufferedReader reader =
+            new BufferedReader(new InputStreamReader(System.in));
+        System.out.print(ANSICodes.setkey("97", "97;98;99;13")
+                         + ANSICodes.attrib(ANSICodes.OFF));
+        System.out.flush();
+
+        String line;
+
+        while ((line = reader.readLine()) != null) {
+            System.out.println("GOT: " + line);
+        }
+    }
+}
diff --git a/src/jline/ArgumentCompletor.java b/src/jline/ArgumentCompletor.java
new file mode 100755
index 0000000..5493ad8
--- /dev/null
+++ b/src/jline/ArgumentCompletor.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
+ *
+ * This software is distributable under the BSD license. See the terms of the
+ * BSD license in the documentation provided with this software.
+ */
+package jline;
+
+import java.util.*;
+
+/**
+ *  A {@link Completor} implementation that invokes a child completor
+ *  using the appropriate separator argument. This
+ *  can be used instead of the individual completors having to
+ *  know about argument parsing semantics.
+ *  

+ * Example 1: Any argument of the command line can + * use file completion. + *

+ *

+ *        consoleReader.addCompletor (new ArgumentCompletor (
+ *                new {@link FileNameCompletor} ()))
+ *  
+ *

+ * Example 2: The first argument of the command line + * can be completed with any of "foo", "bar", or "baz", and remaining + * arguments can be completed with a file name. + *

+ *

+ *        consoleReader.addCompletor (new ArgumentCompletor (
+ *                new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"})));
+ *        consoleReader.addCompletor (new ArgumentCompletor (
+ *                new {@link FileNameCompletor} ()));
+ *  
+ * + *

+ * When the argument index is past the last embedded completors, the last + * completors is always used. To disable this behavior, have the last + * completor be a {@link NullCompletor}. For example: + *

+ * + *
+ *        consoleReader.addCompletor (new ArgumentCompletor (
+ *                new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}),
+ *                new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}),
+ *                new {@link NullCompletor}
+ *                ));
+ *        
+ *

+ * TODO: handle argument quoting and escape characters + *

+ * + * @author Marc Prud'hommeaux + */ +public class ArgumentCompletor implements Completor { + final Completor[] completors; + final ArgumentDelimiter delim; + boolean strict = true; + + /** + * Constuctor: create a new completor with the default + * argument separator of " ". + * + * @param completor the embedded completor + */ + public ArgumentCompletor(final Completor completor) { + this(new Completor[] { + completor + }); + } + + /** + * Constuctor: create a new completor with the default + * argument separator of " ". + * + * @param completors the List of completors to use + */ + public ArgumentCompletor(final List completors) { + this((Completor[]) completors.toArray(new Completor[completors.size()])); + } + + /** + * Constuctor: create a new completor with the default + * argument separator of " ". + * + * @param completors the embedded argument completors + */ + public ArgumentCompletor(final Completor[] completors) { + this(completors, new WhitespaceArgumentDelimiter()); + } + + /** + * Constuctor: create a new completor with the specified + * argument delimiter. + * + * @param completor the embedded completor + * @param delim the delimiter for parsing arguments + */ + public ArgumentCompletor(final Completor completor, + final ArgumentDelimiter delim) { + this(new Completor[] { + completor + }, delim); + } + + /** + * Constuctor: create a new completor with the specified + * argument delimiter. + * + * @param completors the embedded completors + * @param delim the delimiter for parsing arguments + */ + public ArgumentCompletor(final Completor[] completors, + final ArgumentDelimiter delim) { + this.completors = completors; + this.delim = delim; + } + + /** + * If true, a completion at argument index N will only succeed + * if all the completions from 0-(N-1) also succeed. + */ + public void setStrict(final boolean strict) { + this.strict = strict; + } + + /** + * Returns whether a completion at argument index N will succees + * if all the completions from arguments 0-(N-1) also succeed. + */ + public boolean getStrict() { + return this.strict; + } + + public int complete(final String buffer, final int cursor, + final List candidates) { + ArgumentList list = delim.delimit(buffer, cursor); + int argpos = list.getArgumentPosition(); + int argIndex = list.getCursorArgumentIndex(); + + if (argIndex < 0) { + return -1; + } + + final Completor comp; + + // if we are beyond the end of the completors, just use the last one + if (argIndex >= completors.length) { + comp = completors[completors.length - 1]; + } else { + comp = completors[argIndex]; + } + + // ensure that all the previous completors are successful before + // allowing this completor to pass (only if strict is true). + for (int i = 0; getStrict() && (i < argIndex); i++) { + Completor sub = + completors[(i >= completors.length) ? (completors.length - 1) : i]; + String[] args = list.getArguments(); + String arg = ((args == null) || (i >= args.length)) ? "" : args[i]; + + List subCandidates = new LinkedList(); + + if (sub.complete(arg, arg.length(), subCandidates) == -1) { + return -1; + } + + if (subCandidates.size() == 0) { + return -1; + } + } + + int ret = comp.complete(list.getCursorArgument(), argpos, candidates); + + if (ret == -1) { + return -1; + } + + int pos = ret + (list.getBufferPosition() - argpos); + + /** + * Special case: when completing in the middle of a line, and the + * area under the cursor is a delimiter, then trim any delimiters + * from the candidates, since we do not need to have an extra + * delimiter. + * + * E.g., if we have a completion for "foo", and we + * enter "f bar" into the buffer, and move to after the "f" + * and hit TAB, we want "foo bar" instead of "foo bar". + */ + if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) { + for (int i = 0; i < candidates.size(); i++) { + String val = candidates.get(i).toString(); + + while ((val.length() > 0) + && delim.isDelimiter(val, val.length() - 1)) { + val = val.substring(0, val.length() - 1); + } + + candidates.set(i, val); + } + } + + ConsoleReader.debug("Completing " + buffer + "(pos=" + cursor + ") " + + "with: " + candidates + ": offset=" + pos); + + return pos; + } + + /** + * The {@link ArgumentCompletor.ArgumentDelimiter} allows custom + * breaking up of a {@link String} into individual arguments in + * order to dispatch the arguments to the nested {@link Completor}. + * + * @author Marc Prud'hommeaux + */ + public static interface ArgumentDelimiter { + /** + * Break the specified buffer into individual tokens + * that can be completed on their own. + * + * @param buffer the buffer to split + * @param argumentPosition the current position of the + * cursor in the buffer + * @return the tokens + */ + ArgumentList delimit(String buffer, int argumentPosition); + + /** + * Returns true if the specified character is a whitespace + * parameter. + * + * @param buffer the complete command buffer + * @param pos the index of the character in the buffer + * @return true if the character should be a delimiter + */ + boolean isDelimiter(String buffer, int pos); + } + + /** + * Abstract implementation of a delimiter that uses the + * {@link #isDelimiter} method to determine if a particular + * character should be used as a delimiter. + * + * @author Marc Prud'hommeaux + */ + public abstract static class AbstractArgumentDelimiter + implements ArgumentDelimiter { + private char[] quoteChars = new char[] { '\'', '"' }; + private char[] escapeChars = new char[] { '\\' }; + + public void setQuoteChars(final char[] quoteChars) { + this.quoteChars = quoteChars; + } + + public char[] getQuoteChars() { + return this.quoteChars; + } + + public void setEscapeChars(final char[] escapeChars) { + this.escapeChars = escapeChars; + } + + public char[] getEscapeChars() { + return this.escapeChars; + } + + public ArgumentList delimit(final String buffer, final int cursor) { + List args = new LinkedList(); + StringBuffer arg = new StringBuffer(); + int argpos = -1; + int bindex = -1; + + for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) { + // once we reach the cursor, set the + // position of the selected index + if (i == cursor) { + bindex = args.size(); + // the position in the current argument is just the + // length of the current argument + argpos = arg.length(); + } + + if ((i == buffer.length()) || isDelimiter(buffer, i)) { + if (arg.length() > 0) { + args.add(arg.toString()); + arg.setLength(0); // reset the arg + } + } else { + arg.append(buffer.charAt(i)); + } + } + + return new ArgumentList((String[]) args. + toArray(new String[args.size()]), bindex, argpos, cursor); + } + + /** + * Returns true if the specified character is a whitespace + * parameter. Check to ensure that the character is not + * escaped by any of + * {@link #getQuoteChars}, and is not escaped by ant of the + * {@link #getEscapeChars}, and returns true from + * {@link #isDelimiterChar}. + * + * @param buffer the complete command buffer + * @param pos the index of the character in the buffer + * @return true if the character should be a delimiter + */ + public boolean isDelimiter(final String buffer, final int pos) { + if (isQuoted(buffer, pos)) { + return false; + } + + if (isEscaped(buffer, pos)) { + return false; + } + + return isDelimiterChar(buffer, pos); + } + + public boolean isQuoted(final String buffer, final int pos) { + return false; + } + + public boolean isEscaped(final String buffer, final int pos) { + if (pos <= 0) { + return false; + } + + for (int i = 0; (escapeChars != null) && (i < escapeChars.length); + i++) { + if (buffer.charAt(pos) == escapeChars[i]) { + return !isEscaped(buffer, pos - 1); // escape escape + } + } + + return false; + } + + /** + * Returns true if the character at the specified position + * if a delimiter. This method will only be called if the + * character is not enclosed in any of the + * {@link #getQuoteChars}, and is not escaped by ant of the + * {@link #getEscapeChars}. To perform escaping manually, + * override {@link #isDelimiter} instead. + */ + public abstract boolean isDelimiterChar(String buffer, int pos); + } + + /** + * {@link ArgumentCompletor.ArgumentDelimiter} + * implementation that counts all + * whitespace (as reported by {@link Character#isWhitespace}) + * as being a delimiter. + * + * @author Marc Prud'hommeaux + */ + public static class WhitespaceArgumentDelimiter + extends AbstractArgumentDelimiter { + /** + * The character is a delimiter if it is whitespace, and the + * preceeding character is not an escape character. + */ + public boolean isDelimiterChar(String buffer, int pos) { + return Character.isWhitespace(buffer.charAt(pos)); + } + } + + /** + * The result of a delimited buffer. + * + * @author Marc Prud'hommeaux + */ + public static class ArgumentList { + private String[] arguments; + private int cursorArgumentIndex; + private int argumentPosition; + private int bufferPosition; + + /** + * @param arguments the array of tokens + * @param cursorArgumentIndex the token index of the cursor + * @param argumentPosition the position of the cursor in the + * current token + * @param bufferPosition the position of the cursor in + * the whole buffer + */ + public ArgumentList(String[] arguments, int cursorArgumentIndex, + int argumentPosition, int bufferPosition) { + this.arguments = arguments; + this.cursorArgumentIndex = cursorArgumentIndex; + this.argumentPosition = argumentPosition; + this.bufferPosition = bufferPosition; + } + + public void setCursorArgumentIndex(int cursorArgumentIndex) { + this.cursorArgumentIndex = cursorArgumentIndex; + } + + public int getCursorArgumentIndex() { + return this.cursorArgumentIndex; + } + + public String getCursorArgument() { + if ((cursorArgumentIndex < 0) + || (cursorArgumentIndex >= arguments.length)) { + return null; + } + + return arguments[cursorArgumentIndex]; + } + + public void setArgumentPosition(int argumentPosition) { + this.argumentPosition = argumentPosition; + } + + public int getArgumentPosition() { + return this.argumentPosition; + } + + public void setArguments(String[] arguments) { + this.arguments = arguments; + } + + public String[] getArguments() { + return this.arguments; + } + + public void setBufferPosition(int bufferPosition) { + this.bufferPosition = bufferPosition; + } + + public int getBufferPosition() { + return this.bufferPosition; + } + } +} diff --git a/src/jline/CandidateCycleCompletionHandler.java b/src/jline/CandidateCycleCompletionHandler.java new file mode 100755 index 0000000..a0bf208 --- /dev/null +++ b/src/jline/CandidateCycleCompletionHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + *

+ * A {@link CompletionHandler} that deals with multiple distinct completions + * by cycling through each one every time tab is pressed. This + * mimics the behavior of the + * editline + * library. + *

+ *

This class is currently a stub; it does nothing

+ * @author Marc Prud'hommeaux + */ +public class CandidateCycleCompletionHandler implements CompletionHandler { + public boolean complete(final ConsoleReader reader, final List candidates, + final int position) throws IOException { + throw new IllegalStateException("CandidateCycleCompletionHandler unimplemented"); + } +} diff --git a/src/jline/CandidateListCompletionHandler.java b/src/jline/CandidateListCompletionHandler.java new file mode 100755 index 0000000..d9d43c2 --- /dev/null +++ b/src/jline/CandidateListCompletionHandler.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.text.MessageFormat; +import java.util.*; + +/** + *

+ * A {@link CompletionHandler} that deals with multiple distinct completions + * by outputting the complete list of possibilities to the console. This + * mimics the behavior of the + * readline + * library. + *

+ * + * TODO: + * + * + * @author Marc Prud'hommeaux + */ +public class CandidateListCompletionHandler implements CompletionHandler { + private static ResourceBundle loc = ResourceBundle. + getBundle(CandidateListCompletionHandler.class.getName()); + + private boolean eagerNewlines = true; + + public void setAlwaysIncludeNewline(boolean eagerNewlines) { + this.eagerNewlines = eagerNewlines; + } + + public boolean complete(final ConsoleReader reader, final List candidates, + final int pos) throws IOException { + CursorBuffer buf = reader.getCursorBuffer(); + + // if there is only one completion, then fill in the buffer + if (candidates.size() == 1) { + String value = candidates.get(0).toString(); + + // fail if the only candidate is the same as the current buffer + if (value.equals(buf.toString())) { + return false; + } + + setBuffer(reader, value, pos); + + return true; + } else if (candidates.size() > 1) { + String value = getUnambiguousCompletions(candidates); + String bufString = buf.toString(); + setBuffer(reader, value, pos); + } + + if (eagerNewlines) + reader.printNewline(); + printCandidates(reader, candidates, eagerNewlines); + + // redraw the current console buffer + reader.drawLine(); + + return true; + } + + public static void setBuffer(ConsoleReader reader, String value, int offset) + throws IOException { + while ((reader.getCursorBuffer().cursor > offset) + && reader.backspace()) { + ; + } + + reader.putString(value); + reader.setCursorPosition(offset + value.length()); + } + + /** + * Print out the candidates. If the size of the candidates + * is greated than the {@link getAutoprintThreshhold}, + * they prompt with aq warning. + * + * @param candidates the list of candidates to print + */ + public static final void printCandidates(ConsoleReader reader, + Collection candidates, boolean eagerNewlines) + throws IOException { + Set distinct = new HashSet(candidates); + + if (distinct.size() > reader.getAutoprintThreshhold()) { + if (!eagerNewlines) + reader.printNewline(); + reader.printString(MessageFormat.format + (loc.getString("display-candidates"), new Object[] { + new Integer(candidates .size()) + }) + " "); + + reader.flushConsole(); + + int c; + + String noOpt = loc.getString("display-candidates-no"); + String yesOpt = loc.getString("display-candidates-yes"); + + while ((c = reader.readCharacter(new char[] { + yesOpt.charAt(0), noOpt.charAt(0) })) != -1) { + if (noOpt.startsWith + (new String(new char[] { (char) c }))) { + reader.printNewline(); + return; + } else if (yesOpt.startsWith + (new String(new char[] { (char) c }))) { + break; + } else { + reader.beep(); + } + } + } + + // copy the values and make them distinct, without otherwise + // affecting the ordering. Only do it if the sizes differ. + if (distinct.size() != candidates.size()) { + Collection copy = new ArrayList(); + + for (Iterator i = candidates.iterator(); i.hasNext();) { + Object next = i.next(); + + if (!(copy.contains(next))) { + copy.add(next); + } + } + + candidates = copy; + } + + reader.printNewline(); + reader.printColumns(candidates); + } + + /** + * Returns a root that matches all the {@link String} elements + * of the specified {@link List}, or null if there are + * no commalities. For example, if the list contains + * foobar, foobaz, foobuz, the + * method will return foob. + */ + private final String getUnambiguousCompletions(final List candidates) { + if ((candidates == null) || (candidates.size() == 0)) { + return null; + } + + // convert to an array for speed + String[] strings = + (String[]) candidates.toArray(new String[candidates.size()]); + + String first = strings[0]; + StringBuffer candidate = new StringBuffer(); + + for (int i = 0; i < first.length(); i++) { + if (startsWith(first.substring(0, i + 1), strings)) { + candidate.append(first.charAt(i)); + } else { + break; + } + } + + return candidate.toString(); + } + + /** + * @return true is all the elements of candidates + * start with starts + */ + private final boolean startsWith(final String starts, + final String[] candidates) { + for (int i = 0; i < candidates.length; i++) { + if (!candidates[i].startsWith(starts)) { + return false; + } + } + + return true; + } +} diff --git a/src/jline/CandidateListCompletionHandler.properties b/src/jline/CandidateListCompletionHandler.properties new file mode 100755 index 0000000..18ee221 --- /dev/null +++ b/src/jline/CandidateListCompletionHandler.properties @@ -0,0 +1,5 @@ +display-candidates: Display all {0} possibilities? (y or n) +display-candidates-yes: y +display-candidates-no: n +display-more: --More-- + diff --git a/src/jline/ClassNameCompletor.java b/src/jline/ClassNameCompletor.java new file mode 100755 index 0000000..3ef5802 --- /dev/null +++ b/src/jline/ClassNameCompletor.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * A Completor implementation that completes java class names. By default, + * it scans the java class path to locate all the classes. + * + * @author Marc Prud'hommeaux + */ +public class ClassNameCompletor extends SimpleCompletor { + + /** + * Complete candidates using all the classes available in the + * java CLASSPATH. + */ + public ClassNameCompletor() throws IOException { + this(null); + } + + public ClassNameCompletor(final SimpleCompletorFilter filter) + throws IOException { + super(getClassNames(), filter); + setDelimiter("."); + } + + public static String[] getClassNames() throws IOException { + Set urls = new HashSet(); + + for (ClassLoader loader = ClassNameCompletor.class + .getClassLoader(); loader != null; + loader = loader.getParent()) { + if (!(loader instanceof URLClassLoader)) { + continue; + } + + urls.addAll(Arrays.asList(((URLClassLoader) loader).getURLs())); + } + + // Now add the URL that holds java.lang.String. This is because + // some JVMs do not report the core classes jar in the list of + // class loaders. + Class[] systemClasses = new Class[] { + String.class, javax.swing.JFrame.class + }; + + for (int i = 0; i < systemClasses.length; i++) { + URL classURL = systemClasses[i].getResource("/" + + systemClasses[i].getName() .replace('.', '/') + ".class"); + + if (classURL != null) { + URLConnection uc = (URLConnection) classURL.openConnection(); + + if (uc instanceof JarURLConnection) { + urls.add(((JarURLConnection) uc).getJarFileURL()); + } + } + } + + Set classes = new HashSet(); + + for (Iterator i = urls.iterator(); i.hasNext();) { + URL url = (URL) i.next(); + File file = new File(url.getFile()); + + if (file.isDirectory()) { + Set files = getClassFiles(file.getAbsolutePath(), + new HashSet(), file, new int[] { 200 }); + classes.addAll(files); + + continue; + } + + if ((file == null) || !file.isFile()) // TODO: handle directories + { + continue; + } + if (!file.toString().endsWith (".jar")) + continue; + + JarFile jf = new JarFile(file); + + for (Enumeration e = jf.entries(); e.hasMoreElements();) { + JarEntry entry = (JarEntry) e.nextElement(); + + if (entry == null) { + continue; + } + + String name = entry.getName(); + + if (!name.endsWith(".class")) // only use class files + { + continue; + } + + classes.add(name); + } + } + + // now filter classes by changing "/" to "." and trimming the + // trailing ".class" + Set classNames = new TreeSet(); + + for (Iterator i = classes.iterator(); i.hasNext();) { + String name = (String) i.next(); + classNames.add(name.replace('/', '.'). + substring(0, name.length() - 6)); + } + + return (String[]) classNames.toArray(new String[classNames.size()]); + } + + private static Set getClassFiles(String root, Set holder, File directory, + int[] maxDirectories) { + // we have passed the maximum number of directories to scan + if (maxDirectories[0]-- < 0) { + return holder; + } + + File[] files = directory.listFiles(); + + for (int i = 0; (files != null) && (i < files.length); i++) { + String name = files[i].getAbsolutePath(); + + if (!(name.startsWith(root))) { + continue; + } else if (files[i].isDirectory()) { + getClassFiles(root, holder, files[i], maxDirectories); + } else if (files[i].getName().endsWith(".class")) { + holder.add(files[i].getAbsolutePath(). + substring(root.length() + 1)); + } + } + + return holder; + } +} diff --git a/src/jline/CompletionHandler.java b/src/jline/CompletionHandler.java new file mode 100755 index 0000000..6f524da --- /dev/null +++ b/src/jline/CompletionHandler.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + * Handler for dealing with candidates for tab-completion. + * + * @author Marc Prud'hommeaux + */ +public interface CompletionHandler { + boolean complete(ConsoleReader reader, List candidates, int position) + throws IOException; +} diff --git a/src/jline/Completor.java b/src/jline/Completor.java new file mode 100755 index 0000000..ad29cf9 --- /dev/null +++ b/src/jline/Completor.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.util.*; + +/** + * A Completor is the mechanism by which tab-completion candidates + * will be resolved. + * + * @author Marc Prud'hommeaux + */ +public interface Completor { + /** + * Populates candidates with a list of possible + * completions for the buffer. The candidates + * list will not be sorted before being displayed to the + * user: thus, the complete method should sort the + * {@link List} before returning. + * + * + * @param buffer the buffer + * @param candidates the {@link List} of candidates to populate + * @return the index of the buffer for which + * the completion will be relative + */ + int complete(String buffer, int cursor, List candidates); +} diff --git a/src/jline/ConsoleOperations.java b/src/jline/ConsoleOperations.java new file mode 100755 index 0000000..16aa0e7 --- /dev/null +++ b/src/jline/ConsoleOperations.java @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.awt.event.KeyEvent; + +/** + * Symbolic constants for Console operations and virtual key bindings. + * @see KeyEvent + * + * @author Marc Prud'hommeaux + */ +public interface ConsoleOperations { + final String CR = System.getProperty("line.separator"); + final char BACKSPACE = '\b'; + final char RESET_LINE = '\r'; + final char KEYBOARD_BELL = '\07'; + final char CTRL_A = 1; + final char CTRL_B = 2; + final char CTRL_C = 3; + final char CTRL_D = 4; + final char CTRL_E = 5; + final char CTRL_F = 6; + final char CTRL_G = 7; + final static char CTRL_K = 11; + final static char CTRL_L = 12; + final char CTRL_N = 14; + final char CTRL_P = 16; + final static char CTRL_OB = 27; + final static char DELETE = 127; + final static char CTRL_QM = 127; + + + /** + * Logical constants for key operations. + */ + + /** + * Unknown operation. + */ + final short UNKNOWN = -99; + + /** + * Operation that moves to the beginning of the buffer. + */ + final short MOVE_TO_BEG = -1; + + /** + * Operation that moves to the end of the buffer. + */ + final short MOVE_TO_END = -3; + + /** + * Operation that moved to the previous character in the buffer. + */ + final short PREV_CHAR = -4; + + /** + * Operation that issues a newline. + */ + final short NEWLINE = -6; + + /** + * Operation that deletes the buffer from the current character to the end. + */ + final short KILL_LINE = -7; + + /** + * Operation that clears the screen. + */ + final short CLEAR_SCREEN = -8; + + /** + * Operation that sets the buffer to the next history item. + */ + final short NEXT_HISTORY = -9; + + /** + * Operation that sets the buffer to the previous history item. + */ + final short PREV_HISTORY = -11; + + /** + * Operation that redisplays the current buffer. + */ + final short REDISPLAY = -13; + + /** + * Operation that deletes the buffer from the cursor to the beginning. + */ + final short KILL_LINE_PREV = -15; + + /** + * Operation that deletes the previous word in the buffer. + */ + final short DELETE_PREV_WORD = -16; + + /** + * Operation that moves to the next character in the buffer. + */ + final short NEXT_CHAR = -19; + + /** + * Operation that moves to the previous character in the buffer. + */ + final short REPEAT_PREV_CHAR = -20; + + /** + * Operation that searches backwards in the command history. + */ + final short SEARCH_PREV = -21; + + /** + * Operation that repeats the character. + */ + final short REPEAT_NEXT_CHAR = -24; + + /** + * Operation that searches forward in the command history. + */ + final short SEARCH_NEXT = -25; + + /** + * Operation that moved to the previous whitespace. + */ + final short PREV_SPACE_WORD = -27; + + /** + * Operation that moved to the end of the current word. + */ + final short TO_END_WORD = -29; + + /** + * Operation that + */ + final short REPEAT_SEARCH_PREV = -34; + + /** + * Operation that + */ + final short PASTE_PREV = -36; + + /** + * Operation that + */ + final short REPLACE_MODE = -37; + + /** + * Operation that + */ + final short SUBSTITUTE_LINE = -38; + + /** + * Operation that + */ + final short TO_PREV_CHAR = -39; + + /** + * Operation that + */ + final short NEXT_SPACE_WORD = -40; + + /** + * Operation that + */ + final short DELETE_PREV_CHAR = -41; + + /** + * Operation that + */ + final short ADD = -42; + + /** + * Operation that + */ + final short PREV_WORD = -43; + + /** + * Operation that + */ + final short CHANGE_META = -44; + + /** + * Operation that + */ + final short DELETE_META = -45; + + /** + * Operation that + */ + final short END_WORD = -46; + + /** + * Operation that toggles insert/overtype + */ + final short INSERT = -48; + + /** + * Operation that + */ + final short REPEAT_SEARCH_NEXT = -49; + + /** + * Operation that + */ + final short PASTE_NEXT = -50; + + /** + * Operation that + */ + final short REPLACE_CHAR = -51; + + /** + * Operation that + */ + final short SUBSTITUTE_CHAR = -52; + + /** + * Operation that + */ + final short TO_NEXT_CHAR = -53; + + /** + * Operation that undoes the previous operation. + */ + final short UNDO = -54; + + /** + * Operation that moved to the next word. + */ + final short NEXT_WORD = -55; + + /** + * Operation that deletes the previous character. + */ + final short DELETE_NEXT_CHAR = -56; + + /** + * Operation that toggles between uppercase and lowercase. + */ + final short CHANGE_CASE = -57; + + /** + * Operation that performs completion operation on the current word. + */ + final short COMPLETE = -58; + + /** + * Operation that exits the command prompt. + */ + final short EXIT = -59; + + /** + * Operation that pastes the contents of the clipboard into the line + */ + final short PASTE = -60; + + /** + * Operation that moves the current History to the beginning. + */ + final static short START_OF_HISTORY = -61; + + /** + * Operation that moves the current History to the end. + */ + final static short END_OF_HISTORY = -62; + + /** + * Operation that clears whatever text is on the current line. + */ + final static short CLEAR_LINE = -63; + + /** + * Operation that aborts the current command (like searching) + */ + final static short ABORT = -64; + +} diff --git a/src/jline/ConsoleReader.java b/src/jline/ConsoleReader.java new file mode 100755 index 0000000..4bb00f5 --- /dev/null +++ b/src/jline/ConsoleReader.java @@ -0,0 +1,1844 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.ActionListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * A reader for console applications. It supports custom tab-completion, + * saveable command history, and command line editing. On some platforms, + * platform-specific commands will need to be issued before the reader will + * function properly. See {@link Terminal#initializeTerminal} for convenience + * methods for issuing platform-specific setup commands. + * + * @author Marc Prud'hommeaux + */ +public class ConsoleReader implements ConsoleOperations { + + final static int TAB_WIDTH = 4; + String prompt; + private boolean useHistory = true; + private boolean usePagination = false; + public static final String CR = System.getProperty("line.separator"); + private static ResourceBundle loc = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName()); + /** + * Map that contains the operation name to keymay operation mapping. + */ + public static SortedMap KEYMAP_NAMES; + + + static { + Map names = new TreeMap(); + + names.put("MOVE_TO_BEG", new Short(MOVE_TO_BEG)); + names.put("MOVE_TO_END", new Short(MOVE_TO_END)); + names.put("PREV_CHAR", new Short(PREV_CHAR)); + names.put("NEWLINE", new Short(NEWLINE)); + names.put("KILL_LINE", new Short(KILL_LINE)); + names.put("PASTE", new Short(PASTE)); + names.put("CLEAR_SCREEN", new Short(CLEAR_SCREEN)); + names.put("NEXT_HISTORY", new Short(NEXT_HISTORY)); + names.put("PREV_HISTORY", new Short(PREV_HISTORY)); + names.put("START_OF_HISTORY", new Short(START_OF_HISTORY)); + names.put("END_OF_HISTORY", new Short(END_OF_HISTORY)); + names.put("REDISPLAY", new Short(REDISPLAY)); + names.put("KILL_LINE_PREV", new Short(KILL_LINE_PREV)); + names.put("DELETE_PREV_WORD", new Short(DELETE_PREV_WORD)); + names.put("NEXT_CHAR", new Short(NEXT_CHAR)); + names.put("REPEAT_PREV_CHAR", new Short(REPEAT_PREV_CHAR)); + names.put("SEARCH_PREV", new Short(SEARCH_PREV)); + names.put("REPEAT_NEXT_CHAR", new Short(REPEAT_NEXT_CHAR)); + names.put("SEARCH_NEXT", new Short(SEARCH_NEXT)); + names.put("PREV_SPACE_WORD", new Short(PREV_SPACE_WORD)); + names.put("TO_END_WORD", new Short(TO_END_WORD)); + names.put("REPEAT_SEARCH_PREV", new Short(REPEAT_SEARCH_PREV)); + names.put("PASTE_PREV", new Short(PASTE_PREV)); + names.put("REPLACE_MODE", new Short(REPLACE_MODE)); + names.put("SUBSTITUTE_LINE", new Short(SUBSTITUTE_LINE)); + names.put("TO_PREV_CHAR", new Short(TO_PREV_CHAR)); + names.put("NEXT_SPACE_WORD", new Short(NEXT_SPACE_WORD)); + names.put("DELETE_PREV_CHAR", new Short(DELETE_PREV_CHAR)); + names.put("ADD", new Short(ADD)); + names.put("PREV_WORD", new Short(PREV_WORD)); + names.put("CHANGE_META", new Short(CHANGE_META)); + names.put("DELETE_META", new Short(DELETE_META)); + names.put("END_WORD", new Short(END_WORD)); + names.put("NEXT_CHAR", new Short(NEXT_CHAR)); + names.put("INSERT", new Short(INSERT)); + names.put("REPEAT_SEARCH_NEXT", new Short(REPEAT_SEARCH_NEXT)); + names.put("PASTE_NEXT", new Short(PASTE_NEXT)); + names.put("REPLACE_CHAR", new Short(REPLACE_CHAR)); + names.put("SUBSTITUTE_CHAR", new Short(SUBSTITUTE_CHAR)); + names.put("TO_NEXT_CHAR", new Short(TO_NEXT_CHAR)); + names.put("UNDO", new Short(UNDO)); + names.put("NEXT_WORD", new Short(NEXT_WORD)); + names.put("DELETE_NEXT_CHAR", new Short(DELETE_NEXT_CHAR)); + names.put("CHANGE_CASE", new Short(CHANGE_CASE)); + names.put("COMPLETE", new Short(COMPLETE)); + names.put("EXIT", new Short(EXIT)); + names.put("CLEAR_LINE", new Short(CLEAR_LINE)); + names.put("ABORT", new Short(ABORT)); + + KEYMAP_NAMES = new TreeMap(Collections.unmodifiableMap(names)); + } + /** + * The map for logical operations. + */ + private final short[] keybindings; + /** + * If true, issue an audible keyboard bell when appropriate. + */ + private boolean bellEnabled = true; + /** + * The current character mask. + */ + private Character mask = null; + /** + * The null mask. + */ + private static final Character NULL_MASK = new Character((char) 0); + /** + * The number of tab-completion candidates above which a warning will be + * prompted before showing all the candidates. + */ + private int autoprintThreshhold = Integer.getInteger( + "jline.completion.threshold", 100).intValue(); // same default as + + // bash + /** + * The Terminal to use. + */ + private final Terminal terminal; + private CompletionHandler completionHandler = new CandidateListCompletionHandler(); + InputStream in; + final Writer out; + final CursorBuffer buf = new CursorBuffer(); + static PrintWriter debugger; + History history = new History(); + final List completors = new LinkedList(); + private Character echoCharacter = null; + private Map triggeredActions = new HashMap(); + + private StringBuffer searchTerm = null; + private String previousSearchTerm = ""; + private int searchIndex = -1; + + /** + * Adding a triggered Action allows to give another course of action + * if a character passed the preprocessing. + * + * Say you want to close the application if the user enter q. + * addTriggerAction('q', new ActionListener(){ System.exit(0); }); + * would do the trick. + * + * @param c + * @param listener + */ + public void addTriggeredAction(char c, ActionListener listener) { + triggeredActions.put(new Character(c), listener); + } + + /** + * Create a new reader using {@link FileDescriptor#in} for input and + * {@link System#out} for output. {@link FileDescriptor#in} is used because + * it has a better chance of being unbuffered. + */ + public ConsoleReader() throws IOException { + this(new FileInputStream(FileDescriptor.in), + new PrintWriter( + new OutputStreamWriter(System.out, + System.getProperty("jline.WindowsTerminal.output.encoding", System.getProperty("file.encoding"))))); + } + + /** + * Create a new reader using the specified {@link InputStream} for input and + * the specific writer for output, using the default keybindings resource. + */ + public ConsoleReader(final InputStream in, final Writer out) + throws IOException { + this(in, out, null); + } + + public ConsoleReader(final InputStream in, final Writer out, + final InputStream bindings) throws IOException { + this(in, out, bindings, Terminal.getTerminal()); + } + + /** + * Create a new reader. + * + * @param in + * the input + * @param out + * the output + * @param bindings + * the key bindings to use + * @param term + * the terminal to use + */ + public ConsoleReader(InputStream in, Writer out, InputStream bindings, + Terminal term) throws IOException { + this.terminal = term; + setInput(in); + this.out = out; + if (bindings == null) { + try { + String bindingFile = System.getProperty("jline.keybindings", + new File(System.getProperty("user.home"), + ".jlinebindings.properties").getAbsolutePath()); + + if (new File(bindingFile).isFile()) { + bindings = new FileInputStream(new File(bindingFile)); + } + } catch (Exception e) { + // swallow exceptions with option debugging + if (debugger != null) { + e.printStackTrace(debugger); + } + } + } + + if (bindings == null) { + bindings = terminal.getDefaultBindings(); + } + + this.keybindings = new short[Character.MAX_VALUE * 2]; + + Arrays.fill(this.keybindings, UNKNOWN); + + /** + * Loads the key bindings. Bindings file is in the format: + * + * keycode: operation name + */ + if (bindings != null) { + Properties p = new Properties(); + p.load(bindings); + bindings.close(); + + for (Iterator i = p.keySet().iterator(); i.hasNext();) { + String val = (String) i.next(); + + try { + Short code = new Short(val); + String op = (String) p.getProperty(val); + + Short opval = (Short) KEYMAP_NAMES.get(op); + + if (opval != null) { + keybindings[code.shortValue()] = opval.shortValue(); + } + } catch (NumberFormatException nfe) { + consumeException(nfe); + } + } + + // hardwired arrow key bindings + // keybindings[VK_UP] = PREV_HISTORY; + // keybindings[VK_DOWN] = NEXT_HISTORY; + // keybindings[VK_LEFT] = PREV_CHAR; + // keybindings[VK_RIGHT] = NEXT_CHAR; + } + } + + public Terminal getTerminal() { + return this.terminal; + } + + /** + * Set the stream for debugging. Development use only. + */ + public void setDebug(final PrintWriter debugger) { + ConsoleReader.debugger = debugger; + } + + /** + * Set the stream to be used for console input. + */ + public void setInput(final InputStream in) { + this.in = in; + } + + /** + * Returns the stream used for console input. + */ + public InputStream getInput() { + return this.in; + } + + /** + * Read the next line and return the contents of the buffer. + */ + public String readLine() throws IOException { + return readLine((String) null); + } + + /** + * Read the next line with the specified character mask. If null, then + * characters will be echoed. If 0, then no characters will be echoed. + */ + public String readLine(final Character mask) throws IOException { + return readLine(null, mask); + } + + /** + * @param bellEnabled + * if true, enable audible keyboard bells if an alert is + * required. + */ + public void setBellEnabled(final boolean bellEnabled) { + this.bellEnabled = bellEnabled; + } + + /** + * @return true is audible keyboard bell is enabled. + */ + public boolean getBellEnabled() { + return this.bellEnabled; + } + + /** + * Query the terminal to find the current width; + * + * @see Terminal#getTerminalWidth + * @return the width of the current terminal. + */ + public int getTermwidth() { + return getTerminal().getTerminalWidth(); + } + + /** + * Query the terminal to find the current width; + * + * @see Terminal#getTerminalHeight + * + * @return the height of the current terminal. + */ + public int getTermheight() { + return getTerminal().getTerminalHeight(); + } + + /** + * @param autoprintThreshhold + * the number of candidates to print without issuing a warning. + */ + public void setAutoprintThreshhold(final int autoprintThreshhold) { + this.autoprintThreshhold = autoprintThreshhold; + } + + /** + * @return the number of candidates to print without issing a warning. + */ + public int getAutoprintThreshhold() { + return this.autoprintThreshhold; + } + + int getKeyForAction(short logicalAction) { + for (int i = 0; i < keybindings.length; i++) { + if (keybindings[i] == logicalAction) { + return i; + } + } + + return -1; + } + + /** + * Clear the echoed characters for the specified character code. + */ + int clearEcho(int c) throws IOException { + // if the terminal is not echoing, then just return... + if (!terminal.getEcho()) { + return 0; + } + + // otherwise, clear + int num = countEchoCharacters((char) c); + back(num); + drawBuffer(num); + + return num; + } + + int countEchoCharacters(char c) { + // tabs as special: we need to determine the number of spaces + // to cancel based on what out current cursor position is + if (c == 9) { + int tabstop = 8; // will this ever be different? + int position = getCursorPosition(); + + return tabstop - (position % tabstop); + } + + return getPrintableCharacters(c).length(); + } + + /** + * Return the number of characters that will be printed when the specified + * character is echoed to the screen. Adapted from cat by Torbjorn Granlund, + * as repeated in stty by David MacKenzie. + */ + StringBuffer getPrintableCharacters(char ch) { + StringBuffer sbuff = new StringBuffer(); + + if (ch >= 32) { + if (ch < 127) { + sbuff.append(ch); + } else if (ch == 127) { + sbuff.append('^'); + sbuff.append('?'); + } else { + sbuff.append('M'); + sbuff.append('-'); + + if (ch >= (128 + 32)) { + if (ch < (128 + 127)) { + sbuff.append((char) (ch - 128)); + } else { + sbuff.append('^'); + sbuff.append('?'); + } + } else { + sbuff.append('^'); + sbuff.append((char) (ch - 128 + 64)); + } + } + } else { + sbuff.append('^'); + sbuff.append((char) (ch + 64)); + } + + return sbuff; + } + + int getCursorPosition() { + // FIXME: does not handle anything but a line with a prompt + // absolute position + return getStrippedAnsiLength(prompt) + buf.cursor; + } + + /** + * Strips ANSI escape sequences starting with CSI and ending with char in range 64-126 + * @param ansiString String possibly containing ANSI codes, may be null + * @return length after stripping ANSI codes + */ + int getStrippedAnsiLength(String ansiString) { + if (ansiString == null) return 0; + boolean inAnsi = false; + int strippedLength = 0; + char[] chars = ansiString.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (!inAnsi && c == 27 && i < chars.length - 1 && chars[i+1] == '[') { + i++; // skip '[' + inAnsi = true; + } else if (inAnsi) { + if (64 <= c && c <= 126) { + inAnsi = false; + } + } else { + strippedLength++; + } + } + return strippedLength; + } + + public String readLine(final String prompt) throws IOException { + return readLine(prompt, null); + } + + /** + * The default prompt that will be issued. + */ + public void setDefaultPrompt(String prompt) { + this.prompt = prompt; + } + + /** + * The default prompt that will be issued. + */ + public String getDefaultPrompt() { + return prompt; + } + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt + * the prompt to issue to the console, may be null. + * @return a line that is read from the terminal, or null if there was null + * input (e.g., CTRL-D was pressed). + */ + public String readLine(final String prompt, final Character mask) + throws IOException { + this.mask = mask; + if (prompt != null) { + this.prompt = prompt; + } + + try { + terminal.beforeReadLine(this, this.prompt, mask); + + if ((this.prompt != null) && (this.prompt.length() > 0)) { + out.write(this.prompt); + out.flush(); + } + + // if the terminal is unsupported, just use plain-java reading + if (!terminal.isSupported()) { + return readLine(in); + } + + final int NORMAL = 1; + final int SEARCH = 2; + int state = NORMAL; + + boolean success = true; + + while (true) { + // Read next key and look up the command binding. + int[] next = readBinding(); + + if (next == null) { + return null; + } + + int c = next[0]; + int code = next[1]; + + if (c == -1) { + return null; + } + + // Search mode. + // + // Note that we have to do this first, because if there is a command + // not linked to a search command, we leave the search mode and fall + // through to the normal state. + if (state == SEARCH) { + switch (code) { + // This doesn't work right now, it seems CTRL-G is not passed + // down correctly. :( + case ABORT: + state = NORMAL; + break; + + case SEARCH_PREV: + if (searchTerm.length() == 0) { + searchTerm.append(previousSearchTerm); + } + + if (searchIndex == -1) { + searchIndex = history.searchBackwards(searchTerm.toString()); + } else { + searchIndex = history.searchBackwards(searchTerm.toString(), searchIndex); + } + break; + + case DELETE_PREV_CHAR: + if (searchTerm.length() > 0) { + searchTerm.deleteCharAt(searchTerm.length() - 1); + searchIndex = history.searchBackwards(searchTerm.toString()); + } + break; + + case UNKNOWN: + searchTerm.appendCodePoint(c); + searchIndex = history.searchBackwards(searchTerm.toString()); + break; + + default: + // Set buffer and cursor position to the found string. + if (searchIndex != -1) { + history.setCurrentIndex(searchIndex); + setBuffer(history.current()); + buf.cursor = history.current().indexOf(searchTerm.toString()); + } + state = NORMAL; + break; + } + + // if we're still in search mode, print the search status + if (state == SEARCH) { + if (searchTerm.length() == 0) { + printSearchStatus("", ""); + } else { + if (searchIndex == -1) { + beep(); + } else { + printSearchStatus(searchTerm.toString(), history.getHistory(searchIndex)); + } + } + } + // otherwise, restore the line + else { + restoreLine(); + } + } + + if (state == NORMAL) { + switch (code) { + case EXIT: // ctrl-d + + if (buf.buffer.length() == 0) { + return null; + } + else { + success = deleteCurrentCharacter(); + } + break; + + case COMPLETE: // tab + success = complete(); + break; + + case MOVE_TO_BEG: + success = setCursorPosition(0); + break; + + case KILL_LINE: // CTRL-K + success = killLine(); + break; + + case CLEAR_SCREEN: // CTRL-L + success = clearScreen(); + break; + + case KILL_LINE_PREV: // CTRL-U + success = resetLine(); + break; + + case NEWLINE: // enter + moveToEnd(); + printNewline(); // output newline + return finishBuffer(); + + case DELETE_PREV_CHAR: // backspace + success = backspace(); + break; + + case DELETE_NEXT_CHAR: // delete + success = deleteCurrentCharacter(); + break; + + case MOVE_TO_END: + success = moveToEnd(); + break; + + case PREV_CHAR: + success = moveCursor(-1) != 0; + break; + + case NEXT_CHAR: + success = moveCursor(1) != 0; + break; + + case NEXT_HISTORY: + success = moveHistory(true); + break; + + case PREV_HISTORY: + success = moveHistory(false); + break; + + case ABORT: + case REDISPLAY: + break; + + case PASTE: + success = paste(); + break; + + case DELETE_PREV_WORD: + success = deletePreviousWord(); + break; + + case PREV_WORD: + success = previousWord(); + break; + + case NEXT_WORD: + success = nextWord(); + break; + + case START_OF_HISTORY: + success = history.moveToFirstEntry(); + if (success) { + setBuffer(history.current()); + } + break; + + case END_OF_HISTORY: + success = history.moveToLastEntry(); + if (success) { + setBuffer(history.current()); + } + break; + + case CLEAR_LINE: + moveInternal(-(buf.buffer.length())); + killLine(); + break; + + case INSERT: + buf.setOvertyping(!buf.isOvertyping()); + break; + + case SEARCH_PREV: // CTRL-R + if (searchTerm != null) { + previousSearchTerm = searchTerm.toString(); + } + searchTerm = new StringBuffer(buf.buffer); + state = SEARCH; + if (searchTerm.length() > 0) { + searchIndex = history.searchBackwards(searchTerm.toString()); + if (searchIndex == -1) { + beep(); + } + printSearchStatus(searchTerm.toString(), + searchIndex > -1 ? history.getHistory(searchIndex) : ""); + } else { + searchIndex = -1; + printSearchStatus("", ""); + } + break; + + case UNKNOWN: + default: + if (c != 0) { // ignore null chars + ActionListener action = (ActionListener) triggeredActions.get(new Character((char) c)); + if (action != null) { + action.actionPerformed(null); + } else { + putChar(c, true); + } + } else { + success = false; + } + } + + if (!(success)) { + beep(); + } + + flushConsole(); + } + } + } finally { + terminal.afterReadLine(this, this.prompt, mask); + } + } + + private String readLine(InputStream in) throws IOException { + StringBuffer buf = new StringBuffer(); + + while (true) { + int i = in.read(); + + if ((i == -1) || (i == '\n') || (i == '\r')) { + return buf.toString(); + } + + buf.append((char) i); + } + + // return new BufferedReader (new InputStreamReader (in)).readLine (); + } + + /** + * Reads the console input and returns an array of the form [raw, key + * binding]. + */ + private int[] readBinding() throws IOException { + int c = readVirtualKey(); + + if (c == -1) { + return null; + } + + // extract the appropriate key binding + short code = keybindings[c]; + + if (debugger != null) { + // debug(" translated: " + (int) c + ": " + code); + } + + return new int[]{c, code}; + } + + /** + * Move up or down the history tree. + */ + private final boolean moveHistory(final boolean next) throws IOException { + if (next && !history.next()) { + return false; + } else if (!next && !history.previous()) { + return false; + } + + setBuffer(history.current()); + + return true; + } + + /** + * Paste the contents of the clipboard into the console buffer + * + * @return true if clipboard contents pasted + */ + public boolean paste() throws IOException { + Clipboard clipboard; + try { // May throw ugly exception on system without X + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } catch (Exception e) { + return false; + } + + if (clipboard == null) { + return false; + } + + Transferable transferable = clipboard.getContents(null); + + if (transferable == null) { + return false; + } + + try { + Object content = transferable.getTransferData(DataFlavor.plainTextFlavor); + + /* + * This fix was suggested in bug #1060649 at + * http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056 + * to get around the deprecated DataFlavor.plainTextFlavor, but it + * raises a UnsupportedFlavorException on Mac OS X + */ + if (content == null) { + try { + content = new DataFlavor().getReaderForText(transferable); + } catch (Exception e) { + } + } + + if (content == null) { + return false; + } + + String value; + + if (content instanceof Reader) { + // TODO: we might want instead connect to the input stream + // so we can interpret individual lines + value = ""; + + String line = null; + + for (BufferedReader read = new BufferedReader((Reader) content); (line = read.readLine()) != null;) { + if (value.length() > 0) { + value += "\n"; + } + + value += line; + } + } else { + value = content.toString(); + } + + if (value == null) { + return true; + } + + putString(value); + + return true; + } catch (UnsupportedFlavorException ufe) { + if (debugger != null) { + debug(ufe + ""); + } + + return false; + } + } + + /** + * Kill the buffer ahead of the current cursor position. + * + * @return true if successful + */ + public boolean killLine() throws IOException { + int cp = buf.cursor; + int len = buf.buffer.length(); + + if (cp >= len) { + return false; + } + + int num = buf.buffer.length() - cp; + clearAhead(num); + + for (int i = 0; i < num; i++) { + buf.buffer.deleteCharAt(len - i - 1); + } + + return true; + } + + /** + * Clear the screen by issuing the ANSI "clear screen" code. + */ + public boolean clearScreen() throws IOException { + if (!terminal.isANSISupported()) { + return false; + } + + // send the ANSI code to clear the screen + printANSISequence("2J"); + + // then send the ANSI code to go to position 1,1 + printANSISequence("1;1H"); + + redrawLine(); + + return true; + } + + /** + * Use the completors to modify the buffer with the appropriate completions. + * + * @return true if successful + */ + private final boolean complete() throws IOException { + // debug ("tab for (" + buf + ")"); + if (completors.size() == 0) { + return false; + } + + List candidates = new LinkedList(); + String bufstr = buf.buffer.toString(); + int cursor = buf.cursor; + + int position = -1; + + for (Iterator i = completors.iterator(); i.hasNext();) { + Completor comp = (Completor) i.next(); + + if ((position = comp.complete(bufstr, cursor, candidates)) != -1) { + break; + } + } + + // no candidates? Fail. + if (candidates.size() == 0) { + return false; + } + + return completionHandler.complete(this, candidates, position); + } + + public CursorBuffer getCursorBuffer() { + return buf; + } + + /** + * Output the specified {@link Collection} in proper columns. + * + * @param stuff + * the stuff to print + */ + public void printColumns(final Collection stuff) throws IOException { + if ((stuff == null) || (stuff.size() == 0)) { + return; + } + + int width = getTermwidth(); + int maxwidth = 0; + + for (Iterator i = stuff.iterator(); i.hasNext(); maxwidth = Math.max( + maxwidth, i.next().toString().length())) { + ; + } + + StringBuffer line = new StringBuffer(); + + int showLines; + + if (usePagination) { + showLines = getTermheight() - 1; // page limit + } else { + showLines = Integer.MAX_VALUE; + } + + for (Iterator i = stuff.iterator(); i.hasNext();) { + String cur = (String) i.next(); + + if ((line.length() + maxwidth) > width) { + printString(line.toString().trim()); + printNewline(); + line.setLength(0); + if (--showLines == 0) { // Overflow + printString(loc.getString("display-more")); + flushConsole(); + int c = readVirtualKey(); + if (c == '\r' || c == '\n') { + showLines = 1; // one step forward + } else if (c != 'q') { + showLines = getTermheight() - 1; // page forward + } + back(loc.getString("display-more").length()); + if (c == 'q') { + break; // cancel + } + } + } + + pad(cur, maxwidth + 3, line); + } + + if (line.length() > 0) { + printString(line.toString().trim()); + printNewline(); + line.setLength(0); + } + } + + /** + * Append toPad to the specified appendTo, as well as (toPad.length () - + * len) spaces. + * + * @param toPad + * the {@link String} to pad + * @param len + * the target length + * @param appendTo + * the {@link StringBuffer} to which to append the padded + * {@link String}. + */ + private final void pad(final String toPad, final int len, + final StringBuffer appendTo) { + appendTo.append(toPad); + + for (int i = 0; i < (len - toPad.length()); i++, appendTo.append(' ')) { + ; + } + } + + /** + * Add the specified {@link Completor} to the list of handlers for + * tab-completion. + * + * @param completor + * the {@link Completor} to add + * @return true if it was successfully added + */ + public boolean addCompletor(final Completor completor) { + return completors.add(completor); + } + + /** + * Remove the specified {@link Completor} from the list of handlers for + * tab-completion. + * + * @param completor + * the {@link Completor} to remove + * @return true if it was successfully removed + */ + public boolean removeCompletor(final Completor completor) { + return completors.remove(completor); + } + + /** + * Returns an unmodifiable list of all the completors. + */ + public Collection getCompletors() { + return Collections.unmodifiableList(completors); + } + + /** + * Erase the current line. + * + * @return false if we failed (e.g., the buffer was empty) + */ + final boolean resetLine() throws IOException { + if (buf.cursor == 0) { + return false; + } + + backspaceAll(); + + return true; + } + + /** + * Move the cursor position to the specified absolute index. + */ + public final boolean setCursorPosition(final int position) + throws IOException { + return moveCursor(position - buf.cursor) != 0; + } + + /** + * Set the current buffer's content to the specified {@link String}. The + * visual console will be modified to show the current buffer. + * + * @param buffer + * the new contents of the buffer. + */ + private final void setBuffer(final String buffer) throws IOException { + // don't bother modifying it if it is unchanged + if (buffer.equals(buf.buffer.toString())) { + return; + } + + // obtain the difference between the current buffer and the new one + int sameIndex = 0; + + for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1) && (i < l2); i++) { + if (buffer.charAt(i) == buf.buffer.charAt(i)) { + sameIndex++; + } else { + break; + } + } + + int diff = buf.cursor - sameIndex; + if (diff < 0) { // we can't backspace here so try from the end of the buffer + moveToEnd(); + diff = buf.buffer.length() - sameIndex; + } + + backspace(diff); // go back for the differences + killLine(); // clear to the end of the line + buf.buffer.setLength(sameIndex); // the new length + putString(buffer.substring(sameIndex)); // append the differences + } + + /** + * Clear the line and redraw it. + */ + public final void redrawLine() throws IOException { + printCharacter(RESET_LINE); + flushConsole(); + drawLine(); + } + + /** + * Output put the prompt + the current buffer + */ + public final void drawLine() throws IOException { + if (prompt != null) { + printString(prompt); + } + + printString(buf.buffer.toString()); + + if (buf.length() != buf.cursor) // not at end of line + { + back(buf.length() - buf.cursor - 1); // sync + } + } + + /** + * Output a platform-dependant newline. + */ + public final void printNewline() throws IOException { + printString(CR); + flushConsole(); + } + + /** + * Clear the buffer and add its contents to the history. + * + * @return the former contents of the buffer. + */ + final String finishBuffer() { + String str = buf.buffer.toString(); + + // we only add it to the history if the buffer is not empty + // and if mask is null, since having a mask typically means + // the string was a password. We clear the mask after this call + if (str.length() > 0) { + if (mask == null && useHistory) { + history.addToHistory(str); + } else { + mask = null; + } + } + + history.moveToEnd(); + + buf.buffer.setLength(0); + buf.cursor = 0; + + return str; + } + + /** + * Write out the specified string to the buffer and the output stream. + */ + public final void putString(final String str) throws IOException { + buf.write(str); + printString(str); + drawBuffer(); + } + + /** + * Output the specified string to the output stream (but not the buffer). + */ + public final void printString(final String str) throws IOException { + printCharacters(str.toCharArray()); + } + + /** + * Output the specified character, both to the buffer and the output stream. + */ + private final void putChar(final int c, final boolean print) + throws IOException { + buf.write((char) c); + + if (print) { + // no masking... + if (mask == null) { + printCharacter(c); + } // null mask: don't print anything... + else if (mask.charValue() == 0) { + ; + } // otherwise print the mask... + else { + printCharacter(mask.charValue()); + } + + drawBuffer(); + } + } + + /** + * Redraw the rest of the buffer from the cursor onwards. This is necessary + * for inserting text into the buffer. + * + * @param clear + * the number of characters to clear after the end of the buffer + */ + private final void drawBuffer(final int clear) throws IOException { + // debug ("drawBuffer: " + clear); + if (buf.cursor == buf.length() && clear == 0) { + return; + } + char[] chars = buf.buffer.substring(buf.cursor).toCharArray(); + if (mask != null) { + Arrays.fill(chars, mask.charValue()); + } + + printCharacters(chars); + clearAhead(clear); + if (terminal.isANSISupported()) { + if (chars.length > 0) { + // don't ask, it seems to work + back(Math.max(chars.length - 1, 1)); + } + } else { + back(chars.length); + } + flushConsole(); + } + + /** + * Redraw the rest of the buffer from the cursor onwards. This is necessary + * for inserting text into the buffer. + */ + private final void drawBuffer() throws IOException { + drawBuffer(0); + } + + /** + * Clear ahead the specified number of characters without moving the cursor. + */ + private final void clearAhead(final int num) throws IOException { + if (num == 0) { + return; + } + + if (terminal.isANSISupported()) { + printANSISequence("J"); + return; + } + + // debug ("clearAhead: " + num); + + // print blank extra characters + printCharacters(' ', num); + + // we need to flush here so a "clever" console + // doesn't just ignore the redundancy of a space followed by + // a backspace. + flushConsole(); + + // reset the visual cursor + back(num); + + flushConsole(); + } + + /** + * Move the visual cursor backwards without modifying the buffer cursor. + */ + private final void back(final int num) throws IOException { + if (num == 0) return; + if (terminal.isANSISupported()) { + int width = getTermwidth(); + int cursor = getCursorPosition(); + // debug("back: " + cursor + " + " + num + " on " + width); + int currRow = (cursor + num) / width; + int newRow = cursor / width; + int newCol = cursor % width + 1; + // debug(" old row: " + currRow + " new row: " + newRow); + if (newRow < currRow) { + printANSISequence((currRow - newRow) + "A"); + } + printANSISequence(newCol + "G"); + flushConsole(); + return; + } + printCharacters(BACKSPACE, num); + flushConsole(); + } + + /** + * Issue an audible keyboard bell, if {@link #getBellEnabled} return true. + */ + public final void beep() throws IOException { + if (!(getBellEnabled())) { + return; + } + + printCharacter(KEYBOARD_BELL); + // need to flush so the console actually beeps + flushConsole(); + } + + /** + * Output the specified character to the output stream without manipulating + * the current buffer. + */ + private final void printCharacter(final int c) throws IOException { + if (c == '\t') { + char cbuf[] = new char[TAB_WIDTH]; + Arrays.fill(cbuf, ' '); + out.write(cbuf); + return; + } + + out.write(c); + } + + /** + * Output the specified characters to the output stream without manipulating + * the current buffer. + */ + private final void printCharacters(final char[] c) throws IOException { + int len = 0; + for (int i = 0; i < c.length; i++) { + if (c[i] == '\t') { + len += TAB_WIDTH; + } else { + len++; + } + } + + char cbuf[]; + if (len == c.length) { + cbuf = c; + } else { + cbuf = new char[len]; + int pos = 0; + for (int i = 0; i < c.length; i++) { + if (c[i] == '\t') { + Arrays.fill(cbuf, pos, pos + TAB_WIDTH, ' '); + pos += TAB_WIDTH; + } else { + cbuf[pos] = c[i]; + pos++; + } + } + } + + out.write(cbuf); + } + + private final void printCharacters(final char c, final int num) + throws IOException { + if (num == 1) { + printCharacter(c); + } else { + char[] chars = new char[num]; + Arrays.fill(chars, c); + printCharacters(chars); + } + } + + /** + * Flush the console output stream. This is important for printout out + * single characters (like a backspace or keyboard) that we want the console + * to handle immedately. + */ + public final void flushConsole() throws IOException { + out.flush(); + } + + private final int backspaceAll() throws IOException { + return backspace(Integer.MAX_VALUE); + } + + /** + * Issue num backspaces. + * + * @return the number of characters backed up + */ + private final int backspace(final int num) throws IOException { + if (buf.cursor == 0) { + return 0; + } + + int count = 0; + int termwidth = getTermwidth(); + int lines = getCursorPosition() / termwidth; + count = moveCursor(-1 * num) * -1; + // debug ("Deleting from " + buf.cursor + " for " + count); + buf.buffer.delete(buf.cursor, buf.cursor + count); + if (getCursorPosition() / termwidth != lines) { + if (terminal.isANSISupported()) { + // debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines); + printANSISequence("J"); + flushConsole(); + } + } + drawBuffer(count); + + return count; + } + + /** + * Issue a backspace. + * + * @return true if successful + */ + public final boolean backspace() throws IOException { + return backspace(1) == 1; + } + + private final boolean moveToEnd() throws IOException { + return moveCursor(buf.length() - buf.cursor) > 0; + } + + /** + * Delete the character at the current position and redraw the remainder of + * the buffer. + */ + private final boolean deleteCurrentCharacter() throws IOException { + if (buf.length() == 0 || buf.cursor == buf.length()) { + return false; + } + + buf.buffer.deleteCharAt(buf.cursor); + drawBuffer(1); + return true; + } + + private final boolean previousWord() throws IOException { + while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { + ; + } + + while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { + ; + } + + return true; + } + + private final boolean nextWord() throws IOException { + while (isDelimiter(buf.current()) && (moveCursor(1) != 0)) { + ; + } + + while (!isDelimiter(buf.current()) && (moveCursor(1) != 0)) { + ; + } + + return true; + } + + private final boolean deletePreviousWord() throws IOException { + while (isDelimiter(buf.current()) && backspace()) { + ; + } + + while (!isDelimiter(buf.current()) && backspace()) { + ; + } + + return true; + } + + /** + * Move the cursor where characters. + * + * @param num + * if less than 0, move abs(num) to the left, + * otherwise move num to the right. + * + * @return the number of spaces we moved + */ + public final int moveCursor(final int num) throws IOException { + int where = num; + + if ((buf.cursor == 0) && (where <= 0)) { + return 0; + } + + if ((buf.cursor == buf.buffer.length()) && (where >= 0)) { + return 0; + } + + if ((buf.cursor + where) < 0) { + where = -buf.cursor; + } else if ((buf.cursor + where) > buf.buffer.length()) { + where = buf.buffer.length() - buf.cursor; + } + + moveInternal(where); + + return where; + } + + /** + * debug. + * + * @param str + * the message to issue. + */ + public static void debug(final String str) { + if (debugger != null) { + debugger.println(str); + debugger.flush(); + } + } + + /** + * Move the cursor where characters, withough checking the current + * buffer. + * + * @param where + * the number of characters to move to the right or left. + */ + private final void moveInternal(final int where) throws IOException { + // debug ("move cursor " + where + " (" + // + buf.cursor + " => " + (buf.cursor + where) + ")"); + buf.cursor += where; + + if (terminal.isANSISupported()) { + if (where < 0) { + back(Math.abs(where)); + } else { + int width = getTermwidth(); + int cursor = getCursorPosition(); + int oldLine = (cursor - where) / width; + int newLine = cursor / width; + if (newLine > oldLine) { + printANSISequence((newLine - oldLine) + "B"); + } + printANSISequence(1 +(cursor % width) + "G"); + } + flushConsole(); + return; + } + + char c; + + if (where < 0) { + int len = 0; + for (int i = buf.cursor; i < buf.cursor - where; i++) { + if (buf.getBuffer().charAt(i) == '\t') { + len += TAB_WIDTH; + } else { + len++; + } + } + + char cbuf[] = new char[len]; + Arrays.fill(cbuf, BACKSPACE); + out.write(cbuf); + + return; + } else if (buf.cursor == 0) { + return; + } else if (mask != null) { + c = mask.charValue(); + } else { + printCharacters(buf.buffer.substring(buf.cursor - where, buf.cursor).toCharArray()); + return; + } + + // null character mask: don't output anything + if (NULL_MASK.equals(mask)) { + return; + } + + printCharacters(c, Math.abs(where)); + } + + /** + * Read a character from the console. + * + * @return the character, or -1 if an EOF is received. + */ + public final int readVirtualKey() throws IOException { + int c = terminal.readVirtualKey(in); + + if (debugger != null) { + // debug("keystroke: " + c + ""); + } + + // clear any echo characters + clearEcho(c); + + return c; + } + + public final int readCharacter(final char[] allowed) throws IOException { + // if we restrict to a limited set and the current character + // is not in the set, then try again. + char c; + + Arrays.sort(allowed); // always need to sort before binarySearch + + while (Arrays.binarySearch(allowed, c = (char) readVirtualKey()) < 0); + + return c; + } + + /** + * Issue num deletes. + * + * @return the number of characters backed up + */ + private final int delete(final int num) + throws IOException { + /* Commented out beacuse of DWA-2949: + if (buf.cursor == 0) + return 0;*/ + + buf.buffer.delete(buf.cursor, buf.cursor + 1); + drawBuffer(1); + + return 1; + } + + public final boolean replace(int num, String replacement) { + buf.buffer.replace(buf.cursor - num, buf.cursor, replacement); + try { + moveCursor(-num); + drawBuffer(Math.max(0, num - replacement.length())); + moveCursor(replacement.length()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + /** + * Issue a delete. + * + * @return true if successful + */ + public final boolean delete() + throws IOException { + return delete(1) == 1; + } + + public void setHistory(final History history) { + this.history = history; + } + + public History getHistory() { + return this.history; + } + + public void setCompletionHandler(final CompletionHandler completionHandler) { + this.completionHandler = completionHandler; + } + + public CompletionHandler getCompletionHandler() { + return this.completionHandler; + } + + /** + *

+ * Set the echo character. For example, to have "*" entered when a password + * is typed: + *

+ * + *
+     * myConsoleReader.setEchoCharacter(new Character('*'));
+     * 
+ * + *

+ * Setting the character to + * + *

+     * null
+     * 
+ * + * will restore normal character echoing. Setting the character to + * + *
+     * new Character(0)
+     * 
+ * + * will cause nothing to be echoed. + *

+ * + * @param echoCharacter + * the character to echo to the console in place of the typed + * character. + */ + public void setEchoCharacter(final Character echoCharacter) { + this.echoCharacter = echoCharacter; + } + + /** + * Returns the echo character. + */ + public Character getEchoCharacter() { + return this.echoCharacter; + } + + /** + * No-op for exceptions we want to silently consume. + */ + private void consumeException(final Throwable e) { + } + + /** + * Checks to see if the specified character is a delimiter. We consider a + * character a delimiter if it is anything but a letter or digit. + * + * @param c + * the character to test + * @return true if it is a delimiter + */ + private boolean isDelimiter(char c) { + return !Character.isLetterOrDigit(c); + } + + private void printANSISequence(String sequence) throws IOException { + printCharacter(27); + printCharacter('['); + printString(sequence); + flushConsole(); + } + + /* + private int currentCol, currentRow; + + private void getCurrentPosition() { + // check for ByteArrayInputStream to disable for unit tests + if (terminal.isANSISupported() && !(in instanceof ByteArrayInputStream)) { + try { + printANSISequence("[6n"); + flushConsole(); + StringBuffer b = new StringBuffer(8); + // position is sent as [{ROW};{COLUMN}R + int r; + while((r = in.read()) > -1 && r != 'R') { + if (r != 27 && r != '[') { + b.append((char) r); + } + } + String[] pos = b.toString().split(";"); + currentRow = Integer.parseInt(pos[0]); + currentCol = Integer.parseInt(pos[1]); + } catch (Exception x) { + // no luck + currentRow = currentCol = -1; + } + } + } + */ + + /** + * Whether or not to add new commands to the history buffer. + */ + public void setUseHistory(boolean useHistory) { + this.useHistory = useHistory; + } + + /** + * Whether or not to add new commands to the history buffer. + */ + public boolean getUseHistory() { + return useHistory; + } + + /** + * Whether to use pagination when the number of rows of candidates exceeds + * the height of the temrinal. + */ + public void setUsePagination(boolean usePagination) { + this.usePagination = usePagination; + } + + /** + * Whether to use pagination when the number of rows of candidates exceeds + * the height of the temrinal. + */ + public boolean getUsePagination() { + return this.usePagination; + } + + public void printSearchStatus(String searchTerm, String match) throws IOException { + int i = match.indexOf(searchTerm); + printString("\r(reverse-i-search) `" + searchTerm + "': " + match + "\u001b[K"); + // FIXME: our ANSI using back() does not work here + printCharacters(BACKSPACE, match.length() - i); + flushConsole(); + } + + public void restoreLine() throws IOException { + printString("\u001b[2K"); // ansi/vt100 for clear whole line + redrawLine(); + flushConsole(); + } +} diff --git a/src/jline/ConsoleReaderInputStream.java b/src/jline/ConsoleReaderInputStream.java new file mode 100755 index 0000000..22a7b6d --- /dev/null +++ b/src/jline/ConsoleReaderInputStream.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + * An {@link InputStream} implementation that wraps a {@link ConsoleReader}. + * It is useful for setting up the {@link System#in} for a generic + * console. + * @author Marc Prud'hommeaux + */ +public class ConsoleReaderInputStream extends SequenceInputStream { + private static InputStream systemIn = System.in; + + public static void setIn() throws IOException { + setIn(new ConsoleReader()); + } + + public static void setIn(final ConsoleReader reader) { + System.setIn(new ConsoleReaderInputStream(reader)); + } + + /** + * Restore the original {@link System#in} input stream. + */ + public static void restoreIn() { + System.setIn(systemIn); + } + + public ConsoleReaderInputStream(final ConsoleReader reader) { + super(new ConsoleEnumeration(reader)); + } + + private static class ConsoleEnumeration implements Enumeration { + private final ConsoleReader reader; + private ConsoleLineInputStream next = null; + private ConsoleLineInputStream prev = null; + + public ConsoleEnumeration(final ConsoleReader reader) { + this.reader = reader; + } + + public Object nextElement() { + if (next != null) { + InputStream n = next; + prev = next; + next = null; + + return n; + } + + return new ConsoleLineInputStream(reader); + } + + public boolean hasMoreElements() { + // the last line was null + if ((prev != null) && (prev.wasNull == true)) { + return false; + } + + if (next == null) { + next = (ConsoleLineInputStream) nextElement(); + } + + return next != null; + } + } + + private static class ConsoleLineInputStream extends InputStream { + private final ConsoleReader reader; + private String line = null; + private int index = 0; + private boolean eol = false; + protected boolean wasNull = false; + + public ConsoleLineInputStream(final ConsoleReader reader) { + this.reader = reader; + } + + public int read() throws IOException { + if (eol) { + return -1; + } + + if (line == null) { + line = reader.readLine(); + } + + if (line == null) { + wasNull = true; + return -1; + } + + if (index >= line.length()) { + eol = true; + return '\n'; // lines are ended with a newline + } + + return line.charAt(index++); + } + } +} diff --git a/src/jline/ConsoleRunner.java b/src/jline/ConsoleRunner.java new file mode 100755 index 0000000..36356de --- /dev/null +++ b/src/jline/ConsoleRunner.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + *

+ * A pass-through application that sets the system input stream to a + * {@link ConsoleReader} and invokes the specified main method. + *

+ * @author Marc Prud'hommeaux + */ +public class ConsoleRunner { + private static ConsoleReader reader; + + public static ConsoleReader getReader() { return reader; } + + public static final String property = "jline.history"; + + public static void main(final String[] args) throws Exception { + String historyFileName = null; + + List argList = new ArrayList(Arrays.asList(args)); + + if (argList.size() == 0) { + usage(); + + return; + } + + historyFileName = System.getProperty(ConsoleRunner.property, null); + + // invoke the main() method + String mainClass = (String) argList.remove(0); + + // setup the inpout stream + reader = new ConsoleReader(); + + if (historyFileName != null) { + reader.setHistory(new History (new File + (System.getProperty("user.home"), + ".jline-" + mainClass + + "." + historyFileName + ".history"))); + } else { + reader.setHistory(new History(new File + (System.getProperty("user.home"), + ".jline-" + mainClass + ".history"))); + } + + String completors = System.getProperty + (ConsoleRunner.class.getName() + ".completors", ""); + List completorList = new ArrayList(); + + for (StringTokenizer tok = new StringTokenizer(completors, ","); + tok.hasMoreTokens();) { + completorList.add + ((Completor) Class.forName(tok.nextToken()).newInstance()); + } + + if (completorList.size() > 0) { + reader.addCompletor(new ArgumentCompletor(completorList)); + } + + ConsoleReaderInputStream.setIn(reader); + + try { + Class.forName(mainClass). + getMethod("main", new Class[] { String[].class }). + invoke(null, new Object[] { argList.toArray(new String[0]) }); + } finally { + // just in case this main method is called from another program + ConsoleReaderInputStream.restoreIn(); + } + } + + private static void usage() { + System.out.println("Usage: \n java " + "[-Djline.history='name'] " + + ConsoleRunner.class.getName() + + " [args]" + + "\n\nThe -Djline.history option will avoid history" + + "\nmangling when running ConsoleRunner on the same application." + + "\n\nargs will be passed directly to the target class name."); + } +} diff --git a/src/jline/CursorBuffer.java b/src/jline/CursorBuffer.java new file mode 100755 index 0000000..120d705 --- /dev/null +++ b/src/jline/CursorBuffer.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +/** + * A CursorBuffer is a holder for a {@link StringBuffer} that also contains the + * current cursor position. + * + * @author Marc Prud'hommeaux + */ +public class CursorBuffer { + public int cursor = 0; + + StringBuffer buffer = new StringBuffer(); + + private boolean overtyping = false; + + public int length() { + return buffer.length(); + } + + public char current() { + if (cursor <= 0) { + return 0; + } + + return buffer.charAt(cursor - 1); + } + + public boolean clearBuffer() { + if (buffer.length() == 0) { + return false; + } + + buffer.delete(0, buffer.length()); + cursor = 0; + return true; + } + + /** + * Write the specific character into the buffer, setting the cursor position + * ahead one. The text may overwrite or insert based on the current setting + * of isOvertyping(). + * + * @param c + * the character to insert + */ + public void write(final char c) { + buffer.insert(cursor++, c); + if (isOvertyping() && cursor < buffer.length()) { + buffer.deleteCharAt(cursor); + } + } + + /** + * Insert the specified {@link String} into the buffer, setting the cursor + * to the end of the insertion point. + * + * @param str + * the String to insert. Must not be null. + */ + public void write(final String str) { + if (buffer.length() == 0) { + buffer.append(str); + } else { + buffer.insert(cursor, str); + } + + cursor += str.length(); + + if (isOvertyping() && cursor < buffer.length()) { + buffer.delete(cursor, (cursor + str.length())); + } + } + + public String toString() { + return buffer.toString(); + } + + public boolean isOvertyping() { + return overtyping; + } + + public void setOvertyping(boolean b) { + overtyping = b; + } + + public StringBuffer getBuffer() { + return buffer; + } + + public void setBuffer(StringBuffer buffer) { + buffer.setLength(0); + buffer.append(this.buffer.toString()); + + this.buffer = buffer; + } + + +} diff --git a/src/jline/FileNameCompletor.java b/src/jline/FileNameCompletor.java new file mode 100755 index 0000000..19c4525 --- /dev/null +++ b/src/jline/FileNameCompletor.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + * A file name completor takes the buffer and issues a list of + * potential completions. + * + *

+ * This completor tries to behave as similar as possible to + * bash's file name completion (using GNU readline) + * with the following exceptions: + * + *

    + *
  • Candidates that are directories will end with "/"
  • + *
  • Wildcard regular expressions are not evaluated or replaced
  • + *
  • The "~" character can be used to represent the user's home, + * but it cannot complete to other users' homes, since java does + * not provide any way of determining that easily
  • + *
+ * + *

TODO

+ *
    + *
  • Handle files with spaces in them
  • + *
  • Have an option for file type color highlighting
  • + *
+ * + * @author Marc Prud'hommeaux + */ +public class FileNameCompletor implements Completor { + public int complete(final String buf, final int cursor, + final List candidates) { + String buffer = (buf == null) ? "" : buf; + + String translated = buffer; + + // special character: ~ maps to the user's home directory + if (translated.startsWith("~" + File.separator)) { + translated = System.getProperty("user.home") + + translated.substring(1); + } else if (translated.startsWith("~")) { + translated = new File(System.getProperty("user.home")).getParentFile() + .getAbsolutePath(); + } else if (!(translated.startsWith(File.separator))) { + translated = new File("").getAbsolutePath() + File.separator + + translated; + } + + File f = new File(translated); + + final File dir; + + if (translated.endsWith(File.separator)) { + dir = f; + } else { + dir = f.getParentFile(); + } + + final File[] entries = (dir == null) ? new File[0] : dir.listFiles(); + + try { + return matchFiles(buffer, translated, entries, candidates); + } finally { + // we want to output a sorted list of files + sortFileNames(candidates); + } + } + + protected void sortFileNames(final List fileNames) { + Collections.sort(fileNames); + } + + /** + * Match the specified buffer to the array of entries + * and enter the matches into the list of candidates. This method + * can be overridden in a subclass that wants to do more + * sophisticated file name completion. + * + * @param buffer the untranslated buffer + * @param translated the buffer with common characters replaced + * @param entries the list of files to match + * @param candidates the list of candidates to populate + * + * @return the offset of the match + */ + public int matchFiles(String buffer, String translated, File[] entries, + List candidates) { + if (entries == null) { + return -1; + } + + int matches = 0; + + // first pass: just count the matches + for (int i = 0; i < entries.length; i++) { + if (entries[i].getAbsolutePath().startsWith(translated)) { + matches++; + } + } + + // green - executable + // blue - directory + // red - compressed + // cyan - symlink + for (int i = 0; i < entries.length; i++) { + if (entries[i].getAbsolutePath().startsWith(translated)) { + String name = + entries[i].getName() + + (((matches == 1) && entries[i].isDirectory()) + ? File.separator : " "); + + /* + if (entries [i].isDirectory ()) + { + name = new ANSIBuffer ().blue (name).toString (); + } + */ + candidates.add(name); + } + } + + final int index = buffer.lastIndexOf(File.separator); + + return index + File.separator.length(); + } +} diff --git a/src/jline/History.java b/src/jline/History.java new file mode 100755 index 0000000..aefadc3 --- /dev/null +++ b/src/jline/History.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + * A command history buffer. + * + * @author Marc Prud'hommeaux + */ +public class History { + + private List history = new ArrayList(); + private PrintWriter output = null; + private int maxSize = 500; + private int currentIndex = 0; + + /** + * Construstor: initialize a blank history. + */ + public History() { + } + + /** + * Construstor: initialize History object the the specified {@link File} for + * storage. + */ + public History(final File historyFile) throws IOException { + setHistoryFile(historyFile); + } + + public void setHistoryFile(final File historyFile) throws IOException { + if (historyFile.isFile()) { + load(new FileInputStream(historyFile)); + } + + setOutput(new PrintWriter(new FileWriter(historyFile), true)); + flushBuffer(); + } + + /** + * Load the history buffer from the specified InputStream. + */ + public void load(final InputStream in) throws IOException { + load(new InputStreamReader(in)); + } + + /** + * Load the history buffer from the specified Reader. + */ + public void load(final Reader reader) throws IOException { + BufferedReader breader = new BufferedReader(reader); + List lines = new ArrayList(); + String line; + + while ((line = breader.readLine()) != null) { + lines.add(line); + } + + for (Iterator i = lines.iterator(); i.hasNext();) { + addToHistory((String) i.next()); + } + } + + public int size() { + return history.size(); + } + + /** + * Clear the history buffer + */ + public void clear() { + history.clear(); + currentIndex = 0; + } + + /** + * Add the specified buffer to the end of the history. The pointer is set to + * the end of the history buffer. + */ + public void addToHistory(final String buffer) { + // don't append duplicates to the end of the buffer + if ((history.size() != 0) && buffer.equals(history.get(history.size() - 1))) { + return; + } + + history.add(buffer); + + while (history.size() > getMaxSize()) { + history.remove(0); + } + + currentIndex = history.size(); + + if (getOutput() != null) { + getOutput().println(buffer); + getOutput().flush(); + } + } + + /** + * Flush the entire history buffer to the output PrintWriter. + */ + public void flushBuffer() throws IOException { + if (getOutput() != null) { + for (Iterator i = history.iterator(); i.hasNext(); getOutput().println((String) i.next())) { + ; + } + + getOutput().flush(); + } + } + + /** + * This moves the history to the last entry. This entry is one position + * before the moveToEnd() position. + * + * @return Returns false if there were no history entries or the history + * index was already at the last entry. + */ + public boolean moveToLastEntry() { + int lastEntry = history.size() - 1; + if (lastEntry >= 0 && lastEntry != currentIndex) { + currentIndex = history.size() - 1; + return true; + } + + return false; + } + + /** + * Move to the end of the history buffer. This will be a blank entry, after + * all of the other entries. + */ + public void moveToEnd() { + currentIndex = history.size(); + } + + /** + * Set the maximum size that the history buffer will store. + */ + public void setMaxSize(final int maxSize) { + this.maxSize = maxSize; + } + + /** + * Get the maximum size that the history buffer will store. + */ + public int getMaxSize() { + return this.maxSize; + } + + /** + * The output to which all history elements will be written (or null of + * history is not saved to a buffer). + */ + public void setOutput(final PrintWriter output) { + this.output = output; + } + + /** + * Returns the PrintWriter that is used to store history elements. + */ + public PrintWriter getOutput() { + return this.output; + } + + /** + * Returns the current history index. + */ + public int getCurrentIndex() { + return this.currentIndex; + } + + /** + * Return the content of the current buffer. + */ + public String current() { + if (currentIndex >= history.size()) { + return ""; + } + + return (String) history.get(currentIndex); + } + + /** + * Move the pointer to the previous element in the buffer. + * + * @return true if we successfully went to the previous element + */ + public boolean previous() { + if (currentIndex <= 0) { + return false; + } + + currentIndex--; + + return true; + } + + /** + * Move the pointer to the next element in the buffer. + * + * @return true if we successfully went to the next element + */ + public boolean next() { + if (currentIndex >= history.size()) { + return false; + } + + currentIndex++; + + return true; + } + + /** + * Returns an immutable list of the history buffer. + */ + public List getHistoryList() { + return Collections.unmodifiableList(history); + } + + /** + * Returns the standard {@link AbstractCollection#toString} representation + * of the history list. + */ + public String toString() { + return history.toString(); + } + + /** + * Moves the history index to the first entry. + * + * @return Return false if there are no entries in the history or if the + * history is already at the beginning. + */ + public boolean moveToFirstEntry() { + if (history.size() > 0 && currentIndex != 0) { + currentIndex = 0; + return true; + } + + return false; + } + + /** + * Search backward in history from a given position. + * + * @param searchTerm substring to search for. + * @param startIndex the index from which on to search + * @return index where this substring has been found, or -1 else. + */ + public int searchBackwards(String searchTerm, int startIndex) { + for (int i = startIndex - 1; i >= 0; i--) { + if (i >= size()) + continue; + if (getHistory(i).indexOf(searchTerm) != -1) { + return i; + } + } + return -1; + } + + /** + * Search backwards in history from the current position. + * + * @param searchTerm substring to search for. + * @return index where the substring has been found, or -1 else. + */ + public int searchBackwards(String s) { + return searchBackwards(s, getCurrentIndex()); + } + + /** + * Get the history string for the given index. + * + * @param index + * @return + */ + public String getHistory(int index) { + return (String) history.get(index); + } + + /** + * Set current index to given number. + * + * @param index + */ + public void setCurrentIndex(int index) { + if (index >= 0 && index < history.size()) + currentIndex = index; + } +} diff --git a/src/jline/LICENSE.txt b/src/jline/LICENSE.txt new file mode 100755 index 0000000..888f7e8 --- /dev/null +++ b/src/jline/LICENSE.txt @@ -0,0 +1,4 @@ +Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + +This software is distributable under the BSD license. See the terms of the +BSD license in the documentation provided with this software. diff --git a/src/jline/MultiCompletor.java b/src/jline/MultiCompletor.java new file mode 100755 index 0000000..e3cd4e3 --- /dev/null +++ b/src/jline/MultiCompletor.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.util.*; + +/** + *

+ * A completor that contains multiple embedded completors. This differs + * from the {@link ArgumentCompletor}, in that the nested completors + * are dispatched individually, rather than delimited by arguments. + *

+ * + * @author Marc Prud'hommeaux + */ +public class MultiCompletor implements Completor { + Completor[] completors = new Completor[0]; + + /** + * Construct a MultiCompletor with no embedded completors. + */ + public MultiCompletor() { + this(new Completor[0]); + } + + /** + * Construct a MultiCompletor with the specified list of + * {@link Completor} instances. + */ + public MultiCompletor(final List completors) { + this((Completor[]) completors.toArray(new Completor[completors.size()])); + } + + /** + * Construct a MultiCompletor with the specified + * {@link Completor} instances. + */ + public MultiCompletor(final Completor[] completors) { + this.completors = completors; + } + + public int complete(final String buffer, final int pos, final List cand) { + int[] positions = new int[completors.length]; + List[] copies = new List[completors.length]; + + for (int i = 0; i < completors.length; i++) { + // clone and save the candidate list + copies[i] = new LinkedList(cand); + positions[i] = completors[i].complete(buffer, pos, copies[i]); + } + + int maxposition = -1; + + for (int i = 0; i < positions.length; i++) { + maxposition = Math.max(maxposition, positions[i]); + } + + // now we have the max cursor value: build up all the + // candidate lists that have the same cursor value + for (int i = 0; i < copies.length; i++) { + if (positions[i] == maxposition) { + cand.addAll(copies[i]); + } + } + + return maxposition; + } + + public void setCompletors(final Completor[] completors) { + this.completors = completors; + } + + public Completor[] getCompletors() { + return this.completors; + } +} diff --git a/src/jline/NullCompletor.java b/src/jline/NullCompletor.java new file mode 100755 index 0000000..aa6cdf7 --- /dev/null +++ b/src/jline/NullCompletor.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.util.*; + +/** + *

+ * A completor that does nothing. Useful as the last item in an + * {@link ArgumentCompletor}. + *

+ * + * @author Marc Prud'hommeaux + */ +public class NullCompletor implements Completor { + /** + * Returns -1 always, indicating that the the buffer is never + * handled. + */ + public int complete(final String buffer, int cursor, List candidates) { + return -1; + } +} diff --git a/src/jline/SimpleCompletor.java b/src/jline/SimpleCompletor.java new file mode 100755 index 0000000..2d488b6 --- /dev/null +++ b/src/jline/SimpleCompletor.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + *

+ * A simple {@link Completor} implementation that handles a pre-defined + * list of completion words. + *

+ * + *

+ * Example usage: + *

+ *
+ *  myConsoleReader.addCompletor (new SimpleCompletor (new String [] { "now", "yesterday", "tomorrow" }));
+ *  
+ * + * @author Marc Prud'hommeaux + */ +public class SimpleCompletor implements Completor, Cloneable { + /** + * The list of candidates that will be completed. + */ + SortedSet candidates; + + /** + * A delimiter to use to qualify completions. + */ + String delimiter; + final SimpleCompletorFilter filter; + + /** + * Create a new SimpleCompletor with a single possible completion + * values. + */ + public SimpleCompletor(final String candidateString) { + this(new String[] { + candidateString + }); + } + + /** + * Create a new SimpleCompletor with a list of possible completion + * values. + */ + public SimpleCompletor(final String[] candidateStrings) { + this(candidateStrings, null); + } + + public SimpleCompletor(final String[] strings, + final SimpleCompletorFilter filter) { + this.filter = filter; + setCandidateStrings(strings); + } + + /** + * Complete candidates using the contents of the specified Reader. + */ + public SimpleCompletor(final Reader reader) throws IOException { + this(getStrings(reader)); + } + + /** + * Complete candidates using the whitespearated values in + * read from the specified Reader. + */ + public SimpleCompletor(final InputStream in) throws IOException { + this(getStrings(new InputStreamReader(in))); + } + + private static String[] getStrings(final Reader in) + throws IOException { + final Reader reader = + (in instanceof BufferedReader) ? in : new BufferedReader(in); + + List words = new LinkedList(); + String line; + + while ((line = ((BufferedReader) reader).readLine()) != null) { + for (StringTokenizer tok = new StringTokenizer(line); + tok.hasMoreTokens(); words.add(tok.nextToken())) { + ; + } + } + + return (String[]) words.toArray(new String[words.size()]); + } + + public int complete(final String buffer, final int cursor, final List clist) { + String start = (buffer == null) ? "" : buffer; + + SortedSet matches = candidates.tailSet(start); + + for (Iterator i = matches.iterator(); i.hasNext();) { + String can = (String) i.next(); + + if (!(can.startsWith(start))) { + break; + } + + if (delimiter != null) { + int index = can.indexOf(delimiter, cursor); + + if (index != -1) { + can = can.substring(0, index + 1); + } + } + + clist.add(can); + } + + if (clist.size() == 1) { + clist.set(0, ((String) clist.get(0)) + " "); + } + + // the index of the completion is always from the beginning of + // the buffer. + return (clist.size() == 0) ? (-1) : 0; + } + + public void setDelimiter(final String delimiter) { + this.delimiter = delimiter; + } + + public String getDelimiter() { + return this.delimiter; + } + + public void setCandidates(final SortedSet candidates) { + if (filter != null) { + TreeSet filtered = new TreeSet(); + + for (Iterator i = candidates.iterator(); i.hasNext();) { + String element = (String) i.next(); + element = filter.filter(element); + + if (element != null) { + filtered.add(element); + } + } + + this.candidates = filtered; + } else { + this.candidates = candidates; + } + } + + public SortedSet getCandidates() { + return Collections.unmodifiableSortedSet(this.candidates); + } + + public void setCandidateStrings(final String[] strings) { + setCandidates(new TreeSet(Arrays.asList(strings))); + } + + public void addCandidateString(final String candidateString) { + final String string = + (filter == null) ? candidateString : filter.filter(candidateString); + + if (string != null) { + candidates.add(string); + } + } + + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + /** + * Filter for elements in the completor. + * + * @author Marc Prud'hommeaux + */ + public static interface SimpleCompletorFilter { + /** + * Filter the specified String. To not filter it, return the + * same String as the parameter. To exclude it, return null. + */ + public String filter(String element); + } + + public static class NoOpFilter implements SimpleCompletorFilter { + public String filter(final String element) { + return element; + } + } +} diff --git a/src/jline/Terminal.java b/src/jline/Terminal.java new file mode 100755 index 0000000..6eecbe0 --- /dev/null +++ b/src/jline/Terminal.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; + +/** + * Representation of the input terminal for a platform. Handles + * any initialization that the platform may need to perform + * in order to allow the {@link ConsoleReader} to correctly handle + * input. + * + * @author Marc Prud'hommeaux + */ +public abstract class Terminal implements ConsoleOperations { + private static Terminal term; + + /** + * @see #setupTerminal + */ + public static Terminal getTerminal() { + return setupTerminal(); + } + + /** + * Reset the current terminal to null. + */ + public static void resetTerminal() { + term = null; + } + + /** + *

Configure and return the {@link Terminal} instance for the + * current platform. This will initialize any system settings + * that are required for the console to be able to handle + * input correctly, such as setting tabtop, buffered input, and + * character echo.

+ * + *

This class will use the Terminal implementation specified in the + * jline.terminal system property, or, if it is unset, by + * detecting the operating system from the os.name + * system property and instantiating either the + * {@link WindowsTerminalTest} or {@link UnixTerminal}. + * + * @see #initializeTerminal + */ + public static synchronized Terminal setupTerminal() { + if (term != null) { + return term; + } + + final Terminal t; + + String os = System.getProperty("os.name").toLowerCase(); + String termProp = System.getProperty("jline.terminal"); + + if ((termProp != null) && (termProp.length() > 0)) { + try { + t = (Terminal) Class.forName(termProp).newInstance(); + } catch (Exception e) { + throw (IllegalArgumentException) new IllegalArgumentException(e + .toString()).fillInStackTrace(); + } + } else if (os.indexOf("windows") != -1) { + t = new WindowsTerminal(); + } else { + t = new UnixTerminal(); + } + + try { + t.initializeTerminal(); + } catch (Exception e) { + e.printStackTrace(); + + return term = new UnsupportedTerminal(); + } + + return term = t; + } + + /** + * Returns true if the current console supports ANSI + * codes. + */ + public boolean isANSISupported() { + return true; + } + + /** + * Read a single character from the input stream. This might + * enable a terminal implementation to better handle nuances of + * the console. + */ + public int readCharacter(final InputStream in) throws IOException { + return in.read(); + } + + /** + * Reads a virtual key from the console. Typically, this will + * just be the raw character that was entered, but in some cases, + * multiple input keys will need to be translated into a single + * virtual key. + * + * @param in the InputStream to read from + * @return the virtual key (e.g., {@link ConsoleOperations#VK_UP}) + */ + public int readVirtualKey(InputStream in) throws IOException { + return readCharacter(in); + } + + /** + * Initialize any system settings + * that are required for the console to be able to handle + * input correctly, such as setting tabtop, buffered input, and + * character echo. + */ + public abstract void initializeTerminal() throws Exception; + + /** + * Returns the current width of the terminal (in characters) + */ + public abstract int getTerminalWidth(); + + /** + * Returns the current height of the terminal (in lines) + */ + public abstract int getTerminalHeight(); + + /** + * Returns true if this terminal is capable of initializing the + * terminal to use jline. + */ + public abstract boolean isSupported(); + + /** + * Returns true if the terminal will echo all characters type. + */ + public abstract boolean getEcho(); + + /** + * Invokes before the console reads a line with the prompt and mask. + */ + public void beforeReadLine(ConsoleReader reader, String prompt, + Character mask) { + } + + /** + * Invokes after the console reads a line with the prompt and mask. + */ + public void afterReadLine(ConsoleReader reader, String prompt, + Character mask) { + } + + /** + * Returns false if character echoing is disabled. + */ + public abstract boolean isEchoEnabled(); + + + /** + * Enable character echoing. This can be used to re-enable character + * if the ConsoleReader is no longer being used. + */ + public abstract void enableEcho(); + + + /** + * Disable character echoing. This can be used to manually re-enable + * character if the ConsoleReader has been disabled. + */ + public abstract void disableEcho(); + + public InputStream getDefaultBindings() { + // Mac bindings are slightly different from Unix/Linux. + // For instance, the Delete key behavior is different between them. + return Terminal.class.getResourceAsStream( + System.getProperty("os.name").toLowerCase().startsWith("mac") ? + "keybindings-mac.properties" : "keybindings.properties"); + } +} diff --git a/src/jline/UnixTerminal.java b/src/jline/UnixTerminal.java new file mode 100755 index 0000000..83f5194 --- /dev/null +++ b/src/jline/UnixTerminal.java @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; +import java.util.*; + +/** + *

+ * Terminal that is used for unix platforms. Terminal initialization + * is handled by issuing the stty command against the + * /dev/tty file to disable character echoing and enable + * character input. All known unix systems (including + * Linux and Macintosh OS X) support the stty), so this + * implementation should work for an reasonable POSIX system. + *

+ * + * @author Marc Prud'hommeaux + * @author Updates Dale Kemp 2005-12-03 + */ +public class UnixTerminal extends Terminal { + public static final short ARROW_START = 27; + public static final short ARROW_PREFIX = 91; + public static final short ARROW_LEFT = 68; + public static final short ARROW_RIGHT = 67; + public static final short ARROW_UP = 65; + public static final short ARROW_DOWN = 66; + public static final short O_PREFIX = 79; + public static final short HOME_CODE = 72; + public static final short END_CODE = 70; + + public static final short DEL_THIRD = 51; + public static final short DEL_SECOND = 126; + + private boolean echoEnabled; + private String ttyConfig; + private String ttyProps; + private long ttyPropsLastFetched; + private boolean backspaceDeleteSwitched = false; + private static String sttyCommand = + System.getProperty("jline.sttyCommand", "stty"); + + + String encoding = System.getProperty("input.encoding", "UTF-8"); + ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding); + InputStreamReader replayReader; + + public UnixTerminal() { + try { + replayReader = new InputStreamReader(replayStream, encoding); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected void checkBackspace(){ + String[] ttyConfigSplit = ttyConfig.split(":|="); + backspaceDeleteSwitched = ttyConfigSplit.length >= 7 && "7f".equals(ttyConfigSplit[6]); + } + + /** + * Remove line-buffered input by invoking "stty -icanon min 1" + * against the current terminal. + */ + public void initializeTerminal() throws IOException, InterruptedException { + // save the initial tty configuration + ttyConfig = stty("-g"); + + // sanity check + if ((ttyConfig.length() == 0) + || ((ttyConfig.indexOf("=") == -1) + && (ttyConfig.indexOf(":") == -1))) { + throw new IOException("Unrecognized stty code: " + ttyConfig); + } + + checkBackspace(); + + // set the console to be character-buffered instead of line-buffered + stty("-icanon min 1"); + + // disable character echoing + stty("-echo"); + echoEnabled = false; + + // at exit, restore the original tty configuration (for JDK 1.3+) + try { + Runtime.getRuntime().addShutdownHook(new Thread() { + public void start() { + try { + restoreTerminal(); + } catch (Exception e) { + consumeException(e); + } + } + }); + } catch (AbstractMethodError ame) { + // JDK 1.3+ only method. Bummer. + consumeException(ame); + } + } + + /** + * Restore the original terminal configuration, which can be used when + * shutting down the console reader. The ConsoleReader cannot be + * used after calling this method. + */ + public void restoreTerminal() throws Exception { + if (ttyConfig != null) { + stty(ttyConfig); + ttyConfig = null; + } + resetTerminal(); + } + + + + public int readVirtualKey(InputStream in) throws IOException { + int c = readCharacter(in); + + if (backspaceDeleteSwitched) + if (c == DELETE) + c = BACKSPACE; + else if (c == BACKSPACE) + c = DELETE; + + // in Unix terminals, arrow keys are represented by + // a sequence of 3 characters. E.g., the up arrow + // key yields 27, 91, 68 + if (c == ARROW_START && in.available() > 0) { + // Escape key is also 27, so we use InputStream.available() + // to distinguish those. If 27 represents an arrow, there + // should be two more chars immediately available. + while (c == ARROW_START) { + c = readCharacter(in); + } + if (c == ARROW_PREFIX || c == O_PREFIX) { + c = readCharacter(in); + if (c == ARROW_UP) { + return CTRL_P; + } else if (c == ARROW_DOWN) { + return CTRL_N; + } else if (c == ARROW_LEFT) { + return CTRL_B; + } else if (c == ARROW_RIGHT) { + return CTRL_F; + } else if (c == HOME_CODE) { + return CTRL_A; + } else if (c == END_CODE) { + return CTRL_E; + } else if (c == DEL_THIRD) { + c = readCharacter(in); // read 4th + return DELETE; + } + } + } + // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk + if (c > 128) { + // handle unicode characters longer than 2 bytes, + // thanks to Marc.Herbert@continuent.com + replayStream.setInput(c, in); +// replayReader = new InputStreamReader(replayStream, encoding); + c = replayReader.read(); + + } + + return c; + } + + /** + * No-op for exceptions we want to silently consume. + */ + private void consumeException(Throwable e) { + } + + public boolean isSupported() { + return true; + } + + public boolean getEcho() { + return false; + } + + /** + * Returns the value of "stty size" width param. + * + * Note: this method caches the value from the + * first time it is called in order to increase speed, which means + * that changing to size of the terminal will not be reflected + * in the console. + */ + public int getTerminalWidth() { + int val = -1; + + try { + val = getTerminalProperty("columns"); + } catch (Exception e) { + } + + if (val == -1) { + val = 80; + } + + return val; + } + + /** + * Returns the value of "stty size" height param. + * + * Note: this method caches the value from the + * first time it is called in order to increase speed, which means + * that changing to size of the terminal will not be reflected + * in the console. + */ + public int getTerminalHeight() { + int val = -1; + + try { + val = getTerminalProperty("rows"); + } catch (Exception e) { + } + + if (val == -1) { + val = 24; + } + + return val; + } + + private int getTerminalProperty(String prop) + throws IOException, InterruptedException { + // tty properties are cached so we don't have to worry too much about getting term widht/height + if (ttyProps == null || System.currentTimeMillis() - ttyPropsLastFetched > 1000) { + ttyProps = stty("-a"); + ttyPropsLastFetched = System.currentTimeMillis(); + } + // need to be able handle both output formats: + // speed 9600 baud; 24 rows; 140 columns; + // and: + // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0; + for (StringTokenizer tok = new StringTokenizer(ttyProps, ";\n"); + tok.hasMoreTokens();) { + String str = tok.nextToken().trim(); + + if (str.startsWith(prop)) { + int index = str.lastIndexOf(" "); + + return Integer.parseInt(str.substring(index).trim()); + } else if (str.endsWith(prop)) { + int index = str.indexOf(" "); + + return Integer.parseInt(str.substring(0, index).trim()); + } + } + + return -1; + } + + /** + * Execute the stty command with the specified arguments + * against the current active terminal. + */ + protected static String stty(final String args) + throws IOException, InterruptedException { + return exec("stty " + args + " < /dev/tty").trim(); + } + + /** + * Execute the specified command and return the output + * (both stdout and stderr). + */ + private static String exec(final String cmd) + throws IOException, InterruptedException { + return exec(new String[] { + "sh", + "-c", + cmd + }); + } + + /** + * Execute the specified command and return the output + * (both stdout and stderr). + */ + private static String exec(final String[] cmd) + throws IOException, InterruptedException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + Process p = Runtime.getRuntime().exec(cmd); + int c; + InputStream in = null; + InputStream err = null; + OutputStream out = null; + + try { + in = p.getInputStream(); + + while ((c = in.read()) != -1) { + bout.write(c); + } + + err = p.getErrorStream(); + + while ((c = err.read()) != -1) { + bout.write(c); + } + + out = p.getOutputStream(); + + p.waitFor(); + } finally { + try {in.close();} catch (Exception e) {} + try {err.close();} catch (Exception e) {} + try {out.close();} catch (Exception e) {} + } + + String result = new String(bout.toByteArray()); + + return result; + } + + /** + * The command to use to set the terminal options. Defaults + * to "stty", or the value of the system property "jline.sttyCommand". + */ + public static void setSttyCommand(String cmd) { + sttyCommand = cmd; + } + + /** + * The command to use to set the terminal options. Defaults + * to "stty", or the value of the system property "jline.sttyCommand". + */ + public static String getSttyCommand() { + return sttyCommand; + } + + public synchronized boolean isEchoEnabled() { + return echoEnabled; + } + + + public synchronized void enableEcho() { + try { + stty("echo"); + echoEnabled = true; + } catch (Exception e) { + consumeException(e); + } + } + + public synchronized void disableEcho() { + try { + stty("-echo"); + echoEnabled = false; + } catch (Exception e) { + consumeException(e); + } + } + + /** + * This is awkward and inefficient, but probably the minimal way to add + * UTF-8 support to JLine + * + * @author Marc Herbert + */ + static class ReplayPrefixOneCharInputStream extends InputStream { + byte firstByte; + int byteLength; + InputStream wrappedStream; + int byteRead; + + final String encoding; + + public ReplayPrefixOneCharInputStream(String encoding) { + this.encoding = encoding; + } + + public void setInput(int recorded, InputStream wrapped) throws IOException { + this.byteRead = 0; + this.firstByte = (byte) recorded; + this.wrappedStream = wrapped; + + byteLength = 1; + if (encoding.equalsIgnoreCase("UTF-8")) + setInputUTF8(recorded, wrapped); + else if (encoding.equalsIgnoreCase("UTF-16")) + byteLength = 2; + else if (encoding.equalsIgnoreCase("UTF-32")) + byteLength = 4; + } + + + public void setInputUTF8(int recorded, InputStream wrapped) throws IOException { + // 110yyyyy 10zzzzzz + if ((firstByte & (byte) 0xE0) == (byte) 0xC0) + this.byteLength = 2; + // 1110xxxx 10yyyyyy 10zzzzzz + else if ((firstByte & (byte) 0xF0) == (byte) 0xE0) + this.byteLength = 3; + // 11110www 10xxxxxx 10yyyyyy 10zzzzzz + else if ((firstByte & (byte) 0xF8) == (byte) 0xF0) + this.byteLength = 4; + else + throw new IOException("invalid UTF-8 first byte: " + firstByte); + } + + public int read() throws IOException { + if (available() == 0) + return -1; + + byteRead++; + + if (byteRead == 1) + return firstByte; + + return wrappedStream.read(); + } + + /** + * InputStreamReader is greedy and will try to read bytes in advance. We + * do NOT want this to happen since we use a temporary/"losing bytes" + * InputStreamReader above, that's why we hide the real + * wrappedStream.available() here. + */ + public int available() { + return byteLength - byteRead; + } + } +} diff --git a/src/jline/UnsupportedTerminal.java b/src/jline/UnsupportedTerminal.java new file mode 100755 index 0000000..2d87a18 --- /dev/null +++ b/src/jline/UnsupportedTerminal.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.IOException; + +/** + * A no-op unsupported terminal. + * + * @author Marc Prud'hommeaux + */ +public class UnsupportedTerminal extends Terminal { + private Thread maskThread = null; + + public void initializeTerminal() { + // nothing we need to do (or can do) for windows. + } + + public boolean getEcho() { + return true; + } + + + public boolean isEchoEnabled() { + return true; + } + + + public void enableEcho() { + } + + + public void disableEcho() { + } + + + /** + * Always returng 80, since we can't access this info on Windows. + */ + public int getTerminalWidth() { + return 80; + } + + /** + * Always returng 24, since we can't access this info on Windows. + */ + public int getTerminalHeight() { + return 80; + } + + public boolean isSupported() { + return false; + } + + public void beforeReadLine(final ConsoleReader reader, final String prompt, + final Character mask) { + if ((mask != null) && (maskThread == null)) { + final String fullPrompt = "\r" + prompt + + " " + + " " + + " " + + "\r" + prompt; + + maskThread = new Thread("JLine Mask Thread") { + public void run() { + while (!interrupted()) { + try { + reader.out.write(fullPrompt); + reader.out.flush(); + sleep(3); + } catch (IOException ioe) { + return; + } catch (InterruptedException ie) { + return; + } + } + } + }; + + maskThread.setPriority(Thread.MAX_PRIORITY); + maskThread.setDaemon(true); + maskThread.start(); + } + } + + public void afterReadLine(final ConsoleReader reader, final String prompt, + final Character mask) { + if ((maskThread != null) && maskThread.isAlive()) { + maskThread.interrupt(); + } + + maskThread = null; + } +} diff --git a/src/jline/WindowsTerminal.java b/src/jline/WindowsTerminal.java new file mode 100755 index 0000000..d2e72e9 --- /dev/null +++ b/src/jline/WindowsTerminal.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.io.*; + +//import jline.UnixTerminal.ReplayPrefixOneCharInputStream; + +/** + *

+ * Terminal implementation for Microsoft Windows. Terminal initialization in + * {@link #initializeTerminal} is accomplished by extracting the + * jline_version.dll, saving it to the system temporary + * directoy (determined by the setting of the java.io.tmpdir System + * property), loading the library, and then calling the Win32 APIs SetConsoleMode and + * GetConsoleMode to + * disable character echoing. + *

+ * + *

+ * By default, the {@link #readCharacter} method will attempt to test to see if + * the specified {@link InputStream} is {@link System#in} or a wrapper around + * {@link FileDescriptor#in}, and if so, will bypass the character reading to + * directly invoke the readc() method in the JNI library. This is so the class + * can read special keys (like arrow keys) which are otherwise inaccessible via + * the {@link System#in} stream. Using JNI reading can be bypassed by setting + * the jline.WindowsTerminal.directConsole system property + * to false. + *

+ * + * @author Marc Prud'hommeaux + */ +public class WindowsTerminal extends Terminal { + // constants copied from wincon.h + + /** + * The ReadFile or ReadConsole function returns only when a carriage return + * character is read. If this mode is disable, the functions return when one + * or more characters are available. + */ + private static final int ENABLE_LINE_INPUT = 2; + + /** + * Characters read by the ReadFile or ReadConsole function are written to + * the active screen buffer as they are read. This mode can be used only if + * the ENABLE_LINE_INPUT mode is also enabled. + */ + private static final int ENABLE_ECHO_INPUT = 4; + + /** + * CTRL+C is processed by the system and is not placed in the input buffer. + * If the input buffer is being read by ReadFile or ReadConsole, other + * control keys are processed by the system and are not returned in the + * ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also + * enabled, backspace, carriage return, and linefeed characters are handled + * by the system. + */ + private static final int ENABLE_PROCESSED_INPUT = 1; + + /** + * User interactions that change the size of the console screen buffer are + * reported in the console's input buffee. Information about these events + * can be read from the input buffer by applications using + * theReadConsoleInput function, but not by those using ReadFile + * orReadConsole. + */ + private static final int ENABLE_WINDOW_INPUT = 8; + + /** + * If the mouse pointer is within the borders of the console window and the + * window has the keyboard focus, mouse events generated by mouse movement + * and button presses are placed in the input buffer. These events are + * discarded by ReadFile or ReadConsole, even when this mode is enabled. + */ + private static final int ENABLE_MOUSE_INPUT = 16; + + /** + * When enabled, text entered in a console window will be inserted at the + * current cursor location and all text following that location will not be + * overwritten. When disabled, all following text will be overwritten. An OR + * operation must be performed with this flag and the ENABLE_EXTENDED_FLAGS + * flag to enable this functionality. + */ + private static final int ENABLE_PROCESSED_OUTPUT = 1; + + /** + * This flag enables the user to use the mouse to select and edit text. To + * enable this option, use the OR to combine this flag with + * ENABLE_EXTENDED_FLAGS. + */ + private static final int ENABLE_WRAP_AT_EOL_OUTPUT = 2; + + /** + * On windows terminals, this character indicates that a 'special' key has + * been pressed. This means that a key such as an arrow key, or delete, or + * home, etc. will be indicated by the next character. + */ + public static final int SPECIAL_KEY_INDICATOR = 224; + + /** + * On windows terminals, this character indicates that a special key on the + * number pad has been pressed. + */ + public static final int NUMPAD_KEY_INDICATOR = 0; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR, + * this character indicates an left arrow key press. + */ + public static final int LEFT_ARROW_KEY = 75; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates an + * right arrow key press. + */ + public static final int RIGHT_ARROW_KEY = 77; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates an up + * arrow key press. + */ + public static final int UP_ARROW_KEY = 72; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates an + * down arrow key press. + */ + public static final int DOWN_ARROW_KEY = 80; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates that + * the delete key was pressed. + */ + public static final int DELETE_KEY = 83; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates that + * the home key was pressed. + */ + public static final int HOME_KEY = 71; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates that + * the end key was pressed. + */ + public static final char END_KEY = 79; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates that + * the page up key was pressed. + */ + public static final char PAGE_UP_KEY = 73; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates that + * the page down key was pressed. + */ + public static final char PAGE_DOWN_KEY = 81; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR + * this character indicates that + * the insert key was pressed. + */ + public static final char INSERT_KEY = 82; + + /** + * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR, + * this character indicates that the escape key was pressed. + */ + public static final char ESCAPE_KEY = 0; + + private Boolean directConsole; + + private boolean echoEnabled; + + String encoding = System.getProperty("jline.WindowsTerminal.input.encoding", System.getProperty("file.encoding")); + ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding); + InputStreamReader replayReader; + + public WindowsTerminal() { + String dir = System.getProperty("jline.WindowsTerminal.directConsole"); + + if ("true".equals(dir)) { + directConsole = Boolean.TRUE; + } else if ("false".equals(dir)) { + directConsole = Boolean.FALSE; + } + + try { + replayReader = new InputStreamReader(replayStream, encoding); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + private native int getConsoleMode(); + + private native void setConsoleMode(final int mode); + + private native int readByte(); + + private native int getWindowsTerminalWidth(); + + private native int getWindowsTerminalHeight(); + + public int readCharacter(final InputStream in) throws IOException { + // if we can detect that we are directly wrapping the system + // input, then bypass the input stream and read directly (which + // allows us to access otherwise unreadable strokes, such as + // the arrow keys) + if (directConsole == Boolean.FALSE) { + return super.readCharacter(in); + } else if ((directConsole == Boolean.TRUE) + || ((in == System.in) || (in instanceof FileInputStream + && (((FileInputStream) in).getFD() == FileDescriptor.in)))) { + return readByte(); + } else { + return super.readCharacter(in); + } + } + + public void initializeTerminal() throws Exception { + loadLibrary("jline"); + + final int originalMode = getConsoleMode(); + + setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT); + + // set the console to raw mode + int newMode = originalMode + & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT + | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT); + echoEnabled = false; + setConsoleMode(newMode); + + // at exit, restore the original tty configuration (for JDK 1.3+) + try { + Runtime.getRuntime().addShutdownHook(new Thread() { + public void start() { + // restore the old console mode + setConsoleMode(originalMode); + } + }); + } catch (AbstractMethodError ame) { + // JDK 1.3+ only method. Bummer. + consumeException(ame); + } + } + + private void loadLibrary(final String name) throws IOException { + // store the DLL in the temporary directory for the System + String version = WindowsTerminal.class.getPackage().getImplementationVersion(); + + if (version == null) { + version = ""; + } + + version = version.replace('.', '_'); + + File f = new File(System.getProperty("java.io.tmpdir"), name + "_" + + version + ".dll"); + boolean exists = f.isFile(); // check if it already exists + + // extract the embedded jline.dll file from the jar and save + // it to the current directory + int bits = 32; + + // check for 64-bit systems and use to appropriate DLL + if (System.getProperty("os.arch").indexOf("64") != -1) + bits = 64; + + InputStream in = new BufferedInputStream(WindowsTerminal.class.getResourceAsStream(name + bits + ".dll")); + + OutputStream fout = null; + try { + fout = new BufferedOutputStream( + new FileOutputStream(f)); + byte[] bytes = new byte[1024 * 10]; + + for (int n = 0; n != -1; n = in.read(bytes)) { + fout.write(bytes, 0, n); + } + + } catch (IOException ioe) { + // We might get an IOException trying to overwrite an existing + // jline.dll file if there is another process using the DLL. + // If this happens, ignore errors. + if (!exists) { + throw ioe; + } + } finally { + if (fout != null) { + try { + fout.close(); + } catch (IOException ioe) { + // ignore + } + } + } + + // try to clean up the DLL after the JVM exits + f.deleteOnExit(); + + // now actually load the DLL + System.load(f.getAbsolutePath()); + } + + public int readVirtualKey(InputStream in) throws IOException { + int indicator = readCharacter(in); + + // in Windows terminals, arrow keys are represented by + // a sequence of 2 characters. E.g., the up arrow + // key yields 224, 72 + if (indicator == SPECIAL_KEY_INDICATOR + || indicator == NUMPAD_KEY_INDICATOR) { + int key = readCharacter(in); + + switch (key) { + case UP_ARROW_KEY: + return CTRL_P; // translate UP -> CTRL-P + case LEFT_ARROW_KEY: + return CTRL_B; // translate LEFT -> CTRL-B + case RIGHT_ARROW_KEY: + return CTRL_F; // translate RIGHT -> CTRL-F + case DOWN_ARROW_KEY: + return CTRL_N; // translate DOWN -> CTRL-N + case DELETE_KEY: + return CTRL_QM; // translate DELETE -> CTRL-? + case HOME_KEY: + return CTRL_A; + case END_KEY: + return CTRL_E; + case PAGE_UP_KEY: + return CTRL_K; + case PAGE_DOWN_KEY: + return CTRL_L; + case ESCAPE_KEY: + return CTRL_OB; // translate ESCAPE -> CTRL-[ + case INSERT_KEY: + return CTRL_C; + default: + return 0; + } + } else if (indicator > 128) { + // handle unicode characters longer than 2 bytes, + // thanks to Marc.Herbert@continuent.com + replayStream.setInput(indicator, in); + // replayReader = new InputStreamReader(replayStream, encoding); + indicator = replayReader.read(); + + } + + return indicator; + + } + + public boolean isSupported() { + return true; + } + + /** + * Windows doesn't support ANSI codes by default; disable them. + */ + public boolean isANSISupported() { + return false; + } + + public boolean getEcho() { + return false; + } + + /** + * Unsupported; return the default. + * + * @see Terminal#getTerminalWidth + */ + public int getTerminalWidth() { + return getWindowsTerminalWidth(); + } + + /** + * Unsupported; return the default. + * + * @see Terminal#getTerminalHeight + */ + public int getTerminalHeight() { + return getWindowsTerminalHeight(); + } + + /** + * No-op for exceptions we want to silently consume. + */ + private void consumeException(final Throwable e) { + } + + /** + * Whether or not to allow the use of the JNI console interaction. + */ + public void setDirectConsole(Boolean directConsole) { + this.directConsole = directConsole; + } + + /** + * Whether or not to allow the use of the JNI console interaction. + */ + public Boolean getDirectConsole() { + return this.directConsole; + } + + public synchronized boolean isEchoEnabled() { + return echoEnabled; + } + + public synchronized void enableEcho() { + // Must set these four modes at the same time to make it work fine. + setConsoleMode(getConsoleMode() | ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT + | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT); + echoEnabled = true; + } + + public synchronized void disableEcho() { + // Must set these four modes at the same time to make it work fine. + setConsoleMode(getConsoleMode() + & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT + | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT)); + echoEnabled = true; + } + + public InputStream getDefaultBindings() { + return WindowsTerminal.class.getResourceAsStream("windowsbindings.properties"); + } + + /** + * This is awkward and inefficient, but probably the minimal way to add + * UTF-8 support to JLine + * + * @author Marc Herbert + */ + static class ReplayPrefixOneCharInputStream extends InputStream { + byte firstByte; + int byteLength; + InputStream wrappedStream; + int byteRead; + + final String encoding; + + public ReplayPrefixOneCharInputStream(String encoding) { + this.encoding = encoding; + } + + public void setInput(int recorded, InputStream wrapped) throws IOException { + this.byteRead = 0; + this.firstByte = (byte) recorded; + this.wrappedStream = wrapped; + + byteLength = 1; + if (encoding.equalsIgnoreCase("UTF-8")) + setInputUTF8(recorded, wrapped); + else if (encoding.equalsIgnoreCase("UTF-16")) + byteLength = 2; + else if (encoding.equalsIgnoreCase("UTF-32")) + byteLength = 4; + } + + + public void setInputUTF8(int recorded, InputStream wrapped) throws IOException { + // 110yyyyy 10zzzzzz + if ((firstByte & (byte) 0xE0) == (byte) 0xC0) + this.byteLength = 2; + // 1110xxxx 10yyyyyy 10zzzzzz + else if ((firstByte & (byte) 0xF0) == (byte) 0xE0) + this.byteLength = 3; + // 11110www 10xxxxxx 10yyyyyy 10zzzzzz + else if ((firstByte & (byte) 0xF8) == (byte) 0xF0) + this.byteLength = 4; + else + throw new IOException("invalid UTF-8 first byte: " + firstByte); + } + + public int read() throws IOException { + if (available() == 0) + return -1; + + byteRead++; + + if (byteRead == 1) + return firstByte; + + return wrappedStream.read(); + } + + /** + * InputStreamReader is greedy and will try to read bytes in advance. We + * do NOT want this to happen since we use a temporary/"losing bytes" + * InputStreamReader above, that's why we hide the real + * wrappedStream.available() here. + */ + public int available() { + return byteLength - byteRead; + } + } + +} diff --git a/src/jline/jline32.dll b/src/jline/jline32.dll new file mode 100755 index 0000000..a0d3b11 Binary files /dev/null and b/src/jline/jline32.dll differ diff --git a/src/jline/jline64.dll b/src/jline/jline64.dll new file mode 100755 index 0000000..922d6b0 Binary files /dev/null and b/src/jline/jline64.dll differ diff --git a/src/jline/keybindings-mac.properties b/src/jline/keybindings-mac.properties new file mode 100755 index 0000000..6f13615 --- /dev/null +++ b/src/jline/keybindings-mac.properties @@ -0,0 +1,62 @@ +# Keybinding mapping for JLine. The format is: +# [key code]: [logical operation] + +# CTRL-B: move to the previous character +2: PREV_CHAR + +# CTRL-G: move to the previous word +7: PREV_WORD + +# CTRL-F: move to the next character +6: NEXT_CHAR + +# CTRL-A: move to the beginning of the line +1: MOVE_TO_BEG + +# CTRL-D: close out the input stream +4: EXIT + +# CTRL-E: move the cursor to the end of the line +5: MOVE_TO_END + +# BACKSPACE, CTRL-H: delete the previous character +# 8 is the ASCII code for backspace and therefor +# deleting the previous character +8: DELETE_PREV_CHAR + +# TAB, CTRL-I: signal that console completion should be attempted +9: COMPLETE + +# CTRL-J, CTRL-M: newline +10: NEWLINE + +# CTRL-K: erase the current line +11: KILL_LINE + +# ENTER: newline +13: NEWLINE + +# CTRL-L: clear screen +12: CLEAR_SCREEN + +# CTRL-N: scroll to the next element in the history buffer +14: NEXT_HISTORY + +# CTRL-P: scroll to the previous element in the history buffer +16: PREV_HISTORY + +# CTRL-R: redraw the current line +18: REDISPLAY + +# CTRL-U: delete all the characters before the cursor position +21: KILL_LINE_PREV + +# CTRL-V: paste the contents of the clipboard (useful for Windows terminal) +22: PASTE + +# CTRL-W: delete the word directly before the cursor +23: DELETE_PREV_WORD + +# DELETE, CTRL-?: delete the previous character +# 127 is the ASCII code for delete +127: DELETE_PREV_CHAR diff --git a/src/jline/keybindings.properties b/src/jline/keybindings.properties new file mode 100755 index 0000000..9585e3a --- /dev/null +++ b/src/jline/keybindings.properties @@ -0,0 +1,68 @@ +# Keybinding mapping for JLine. The format is: +# [key code]: [logical operation] + +# CTRL-A: move to the beginning of the line +1: MOVE_TO_BEG + +# CTRL-B: move to the previous character +2: PREV_CHAR + +# CTRL-D: close out the input stream +4: EXIT + +# CTRL-E: move the cursor to the end of the line +5: MOVE_TO_END + +# CTRL-F: move to the next character +6: NEXT_CHAR + +# CTRL-G: move to the previous word +7: ABORT + +# BACKSPACE, CTRL-H: delete the previous character +# 8 is the ASCII code for backspace and therefor +# deleting the previous character +8: DELETE_PREV_CHAR + +# TAB, CTRL-I: signal that console completion should be attempted +9: COMPLETE + +# CTRL-J, CTRL-M: newline +10: NEWLINE + +# CTRL-K: erase the current line +11: KILL_LINE + +# CTRL-L: clear screen +12: CLEAR_SCREEN + +# ENTER: newline +13: NEWLINE + +# CTRL-N: scroll to the next element in the history buffer +14: NEXT_HISTORY + +# CTRL-P: scroll to the previous element in the history buffer +16: PREV_HISTORY + +# CTRL-R: redraw the current line +18: SEARCH_PREV + +# CTRL-U: delete all the characters before the cursor position +21: KILL_LINE_PREV + +# CTRL-V: paste the contents of the clipboard (useful for Windows terminal) +22: PASTE + +# CTRL-W: delete the word directly before the cursor +23: DELETE_PREV_WORD + +# CTRL-X: temporary location for PREV_WORD to make tests pass +24: PREV_WORD + +# ESCAPE probably not intended this way, but it does the right thing for now +27: REDISPLAY + +# DELETE, CTRL-?: delete the next character +# 127 is the ASCII code for delete +127: DELETE_NEXT_CHAR diff --git a/src/jline/package.html b/src/jline/package.html new file mode 100755 index 0000000..c807431 --- /dev/null +++ b/src/jline/package.html @@ -0,0 +1,9 @@ + +

+The core JLine API. The central class is +jline.ConsoleReader}, which +is a reader for obtaining input from an arbitrary +InputStream (usually System.in). +

+ + diff --git a/src/jline/windowsbindings.properties b/src/jline/windowsbindings.properties new file mode 100755 index 0000000..d599c69 --- /dev/null +++ b/src/jline/windowsbindings.properties @@ -0,0 +1,68 @@ +# Keybinding mapping for JLine. The format is: +# [key code]: [logical operation] + +# CTRL-A: move to the beginning of the line +1: MOVE_TO_BEG + +# CTRL-B: move to the previous character +2: PREV_CHAR + +# CTRL-C: toggle overtype mode (frankly, I wasn't sure where to bind this) +3: INSERT + +# CTRL-D: close out the input stream +4: EXIT + +# CTRL-E: move the cursor to the end of the line +5: MOVE_TO_END + +# CTRL-F: move to the next character +6: NEXT_CHAR + +# CTRL-G: move to the previous word +7: ABORT + +# CTRL-H: delete the previous character +8: DELETE_PREV_CHAR + +# TAB, CTRL-I: signal that console completion should be attempted +9: COMPLETE + +# CTRL-J, CTRL-M: newline +10: NEWLINE + +# CTRL-K: Vertical tab - on windows we'll move to the start of the history +11: START_OF_HISTORY + +# CTRL-L: Form feed - on windows, we'll move to the end of the history +12: END_OF_HISTORY + +# ENTER: newline +13: NEWLINE + +# CTRL-N: scroll to the next element in the history buffer +14: NEXT_HISTORY + +# CTRL-P: scroll to the previous element in the history buffer +16: PREV_HISTORY + +# CTRL-R: search backwards in history +18: SEARCH_PREV + +# CTRL-U: delete all the characters before the cursor position +21: KILL_LINE_PREV + +# CTRL-V: paste the contents of the clipboard (useful for Windows terminal) +22: PASTE + +# CTRL-W: delete the word directly before the cursor +23: DELETE_PREV_WORD + +# CTRL-X: temporary location for PREV_WORD to make tests pass +24: PREV_WORD + +# CTRL-[: escape - clear the current line. +27: CLEAR_LINE + +# CTRL-?: delete the previous character +127: DELETE_NEXT_CHAR diff --git a/src/joptsimple/AbstractOptionSpec.java b/src/joptsimple/AbstractOptionSpec.java new file mode 100755 index 0000000..cc90e6f --- /dev/null +++ b/src/joptsimple/AbstractOptionSpec.java @@ -0,0 +1,153 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.*; + +import joptsimple.internal.Reflection; +import joptsimple.internal.ReflectionException; + +import static joptsimple.internal.Strings.*; + +/** + * @param represents the type of the arguments this option accepts + * @author Paul Holser + */ +abstract class AbstractOptionSpec implements OptionSpec, OptionDescriptor { + private final List options = new ArrayList(); + private final String description; + private boolean forHelp; + + protected AbstractOptionSpec( String option ) { + this( singletonList( option ), EMPTY ); + } + + protected AbstractOptionSpec( Collection options, String description ) { + arrangeOptions( options ); + + this.description = description; + } + + public final Collection options() { + return unmodifiableList( options ); + } + + public final List values( OptionSet detectedOptions ) { + return detectedOptions.valuesOf( this ); + } + + public final V value( OptionSet detectedOptions ) { + return detectedOptions.valueOf( this ); + } + + public String description() { + return description; + } + + public final AbstractOptionSpec forHelp() { + forHelp = true; + return this; + } + + public final boolean isForHelp() { + return forHelp; + } + + public boolean representsNonOptions() { + return false; + } + + protected abstract V convert( String argument ); + + protected V convertWith( ValueConverter converter, String argument ) { + try { + return Reflection.convertWith( converter, argument ); + } + catch ( ReflectionException ex ) { + throw new OptionArgumentConversionException( options(), argument, ex ); + } + catch ( ValueConversionException ex ) { + throw new OptionArgumentConversionException( options(), argument, ex ); + } + } + + protected String argumentTypeIndicatorFrom( ValueConverter converter ) { + if ( converter == null ) + return null; + + String pattern = converter.valuePattern(); + return pattern == null ? converter.valueType().getName() : pattern; + } + + abstract void handleOption( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions, + String detectedArgument ); + + private void arrangeOptions( Collection unarranged ) { + if ( unarranged.size() == 1 ) { + options.addAll( unarranged ); + return; + } + + List shortOptions = new ArrayList(); + List longOptions = new ArrayList(); + + for ( String each : unarranged ) { + if ( each.length() == 1 ) + shortOptions.add( each ); + else + longOptions.add( each ); + } + + sort( shortOptions ); + sort( longOptions ); + + options.addAll( shortOptions ); + options.addAll( longOptions ); + } + + @Override + public boolean equals( Object that ) { + if ( !( that instanceof AbstractOptionSpec ) ) + return false; + + AbstractOptionSpec other = (AbstractOptionSpec) that; + return options.equals( other.options ); + } + + @Override + public int hashCode() { + return options.hashCode(); + } + + @Override + public String toString() { + return options.toString(); + } +} diff --git a/src/joptsimple/AlternativeLongOptionSpec.java b/src/joptsimple/AlternativeLongOptionSpec.java new file mode 100755 index 0000000..1f51dcb --- /dev/null +++ b/src/joptsimple/AlternativeLongOptionSpec.java @@ -0,0 +1,51 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import static java.util.Collections.*; + +import static joptsimple.ParserRules.*; + +/** + * Represents the "-W" form of long option specification. + * + * @author Paul Holser + */ +class AlternativeLongOptionSpec extends ArgumentAcceptingOptionSpec { + AlternativeLongOptionSpec() { + super( singletonList( RESERVED_FOR_EXTENSIONS ), true, "Alternative form of long options" ); + + describedAs( "opt=value" ); + } + + @Override + protected void detectOptionArgument( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions ) { + if ( !arguments.hasMore() ) + throw new OptionMissingRequiredArgumentException( options() ); + + arguments.treatNextAsLongOption(); + } +} diff --git a/src/joptsimple/ArgumentAcceptingOptionSpec.java b/src/joptsimple/ArgumentAcceptingOptionSpec.java new file mode 100755 index 0000000..6a74850 --- /dev/null +++ b/src/joptsimple/ArgumentAcceptingOptionSpec.java @@ -0,0 +1,329 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; + +import static java.util.Collections.*; + +import static joptsimple.internal.Objects.*; +import static joptsimple.internal.Reflection.*; +import static joptsimple.internal.Strings.*; + +/** + *

Specification of an option that accepts an argument.

+ * + *

Instances are returned from {@link OptionSpecBuilder} methods to allow the formation of parser directives as + * sentences in a "fluent interface" language. For example:

+ * + *
+ *   
+ *   OptionParser parser = new OptionParser();
+ *   parser.accepts( "c" ).withRequiredArg().ofType( Integer.class );
+ *   
+ * 
+ * + *

If no methods are invoked on an instance of this class, then that instance's option will treat its argument as + * a {@link String}.

+ * + * @param represents the type of the arguments this option accepts + * @author Paul Holser + */ +public abstract class ArgumentAcceptingOptionSpec extends AbstractOptionSpec { + private static final char NIL_VALUE_SEPARATOR = '\u0000'; + + private boolean optionRequired; + private final boolean argumentRequired; + private ValueConverter converter; + private String argumentDescription = ""; + private String valueSeparator = String.valueOf( NIL_VALUE_SEPARATOR ); + private final List defaultValues = new ArrayList(); + + ArgumentAcceptingOptionSpec( String option, boolean argumentRequired ) { + super( option ); + + this.argumentRequired = argumentRequired; + } + + ArgumentAcceptingOptionSpec( Collection options, boolean argumentRequired, String description ) { + super( options, description ); + + this.argumentRequired = argumentRequired; + } + + /** + *

Specifies a type to which arguments of this spec's option are to be converted.

+ * + *

JOpt Simple accepts types that have either:

+ * + *
    + *
  1. a public static method called {@code valueOf} which accepts a single argument of type {@link String} + * and whose return type is the same as the class on which the method is declared. The {@code java.lang} + * primitive wrapper classes have such methods.
  2. + * + *
  3. a public constructor which accepts a single argument of type {@link String}.
  4. + *
+ * + *

This class converts arguments using those methods in that order; that is, {@code valueOf} would be invoked + * before a one-{@link String}-arg constructor would.

+ * + *

Invoking this method will trump any previous calls to this method or to + * {@link #withValuesConvertedBy(ValueConverter)}.

+ * + * @param represents the runtime class of the desired option argument type + * @param argumentType desired type of arguments to this spec's option + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws NullPointerException if the type is {@code null} + * @throws IllegalArgumentException if the type does not have the standard conversion methods + */ + public final ArgumentAcceptingOptionSpec ofType( Class argumentType ) { + return withValuesConvertedBy( findConverter( argumentType ) ); + } + + /** + *

Specifies a converter to use to translate arguments of this spec's option into Java objects. This is useful + * when converting to types that do not have the requisite factory method or constructor for + * {@link #ofType(Class)}.

+ * + *

Invoking this method will trump any previous calls to this method or to {@link #ofType(Class)}. + * + * @param represents the runtime class of the desired option argument type + * @param aConverter the converter to use + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws NullPointerException if the converter is {@code null} + */ + @SuppressWarnings( "unchecked" ) + public final ArgumentAcceptingOptionSpec withValuesConvertedBy( ValueConverter aConverter ) { + if ( aConverter == null ) + throw new NullPointerException( "illegal null converter" ); + + converter = (ValueConverter) aConverter; + return (ArgumentAcceptingOptionSpec) this; + } + + /** + *

Specifies a description for the argument of the option that this spec represents. This description is used + * when generating help information about the parser.

+ * + * @param description describes the nature of the argument of this spec's option + * @return self, so that the caller can add clauses to the fluent interface sentence + */ + public final ArgumentAcceptingOptionSpec describedAs( String description ) { + argumentDescription = description; + return this; + } + + /** + *

Specifies a value separator for the argument of the option that this spec represents. This allows a single + * option argument to represent multiple values for the option. For example:

+ * + *
+     *   
+     *   parser.accepts( "z" ).withRequiredArg()
+     *       .withValuesSeparatedBy( ',' );
+     *   OptionSet options = parser.parse( new String[] { "-z", "foo,bar,baz", "-z",
+     *       "fizz", "-z", "buzz" } );
+     *   
+     * 
+ * + *

Then {@code options.valuesOf( "z" )} would yield the list {@code [foo, bar, baz, fizz, buzz]}.

+ * + *

You cannot use Unicode U+0000 as the separator.

+ * + * @param separator a character separator + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws IllegalArgumentException if the separator is Unicode U+0000 + */ + public final ArgumentAcceptingOptionSpec withValuesSeparatedBy( char separator ) { + if ( separator == NIL_VALUE_SEPARATOR ) + throw new IllegalArgumentException( "cannot use U+0000 as separator" ); + + valueSeparator = String.valueOf( separator ); + return this; + } + + /** + *

Specifies a value separator for the argument of the option that this spec represents. This allows a single + * option argument to represent multiple values for the option. For example:

+ * + *
+     *   
+     *   parser.accepts( "z" ).withRequiredArg()
+     *       .withValuesSeparatedBy( ":::" );
+     *   OptionSet options = parser.parse( new String[] { "-z", "foo:::bar:::baz", "-z",
+     *       "fizz", "-z", "buzz" } );
+     *   
+     * 
+ * + *

Then {@code options.valuesOf( "z" )} would yield the list {@code [foo, bar, baz, fizz, buzz]}.

+ * + *

You cannot use Unicode U+0000 in the separator.

+ * + * @param separator a string separator + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws IllegalArgumentException if the separator contains Unicode U+0000 + */ + public final ArgumentAcceptingOptionSpec withValuesSeparatedBy( String separator ) { + if ( separator.indexOf( NIL_VALUE_SEPARATOR ) != -1 ) + throw new IllegalArgumentException( "cannot use U+0000 in separator" ); + + valueSeparator = separator; + return this; + } + + /** + * Specifies a set of default values for the argument of the option that this spec represents. + * + * @param value the first in the set of default argument values for this spec's option + * @param values the (optional) remainder of the set of default argument values for this spec's option + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws NullPointerException if {@code value}, {@code values}, or any elements of {@code values} are + * {@code null} + */ + public ArgumentAcceptingOptionSpec defaultsTo( V value, V... values ) { + addDefaultValue( value ); + defaultsTo( values ); + + return this; + } + + /** + * Specifies a set of default values for the argument of the option that this spec represents. + * + * @param values the set of default argument values for this spec's option + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws NullPointerException if {@code values} or any elements of {@code values} are {@code null} + */ + public ArgumentAcceptingOptionSpec defaultsTo( V[] values ) { + for ( V each : values ) + addDefaultValue( each ); + + return this; + } + + /** + * Marks this option as required. An {@link OptionException} will be thrown when + * {@link OptionParser#parse(java.lang.String...)} is called, if an option is marked as required and not specified + * on the command line. + * + * @return self, so that the caller can add clauses to the fluent interface sentence + */ + public ArgumentAcceptingOptionSpec required() { + optionRequired = true; + return this; + } + + public boolean isRequired() { + return optionRequired; + } + + private void addDefaultValue( V value ) { + ensureNotNull( value ); + defaultValues.add( value ); + } + + @Override + final void handleOption( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions, + String detectedArgument ) { + + if ( isNullOrEmpty( detectedArgument ) ) + detectOptionArgument( parser, arguments, detectedOptions ); + else + addArguments( detectedOptions, detectedArgument ); + } + + protected void addArguments( OptionSet detectedOptions, String detectedArgument ) { + StringTokenizer lexer = new StringTokenizer( detectedArgument, valueSeparator ); + if ( !lexer.hasMoreTokens() ) + detectedOptions.addWithArgument( this, detectedArgument ); + else { + while ( lexer.hasMoreTokens() ) + detectedOptions.addWithArgument( this, lexer.nextToken() ); + } + } + + protected abstract void detectOptionArgument( OptionParser parser, ArgumentList arguments, + OptionSet detectedOptions ); + + @Override + protected final V convert( String argument ) { + return convertWith( converter, argument ); + } + + protected boolean canConvertArgument( String argument ) { + StringTokenizer lexer = new StringTokenizer( argument, valueSeparator ); + + try { + while ( lexer.hasMoreTokens() ) + convert( lexer.nextToken() ); + return true; + } + catch ( OptionException ignored ) { + return false; + } + } + + protected boolean isArgumentOfNumberType() { + return converter != null && Number.class.isAssignableFrom( converter.valueType() ); + } + + public boolean acceptsArguments() { + return true; + } + + public boolean requiresArgument() { + return argumentRequired; + } + + public String argumentDescription() { + return argumentDescription; + } + + public String argumentTypeIndicator() { + return argumentTypeIndicatorFrom( converter ); + } + + public List defaultValues() { + return unmodifiableList( defaultValues ); + } + + @Override + public boolean equals( Object that ) { + if ( !super.equals( that ) ) + return false; + + ArgumentAcceptingOptionSpec other = (ArgumentAcceptingOptionSpec) that; + return requiresArgument() == other.requiresArgument(); + } + + @Override + public int hashCode() { + return super.hashCode() ^ ( argumentRequired ? 0 : 1 ); + } +} diff --git a/src/joptsimple/ArgumentList.java b/src/joptsimple/ArgumentList.java new file mode 100755 index 0000000..0ec79e9 --- /dev/null +++ b/src/joptsimple/ArgumentList.java @@ -0,0 +1,59 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import static joptsimple.ParserRules.*; + +/** + *

Wrapper for an array of command line arguments.

+ * + * @author Paul Holser + */ +class ArgumentList { + private final String[] arguments; + private int currentIndex; + + ArgumentList( String... arguments ) { + this.arguments = arguments.clone(); + } + + boolean hasMore() { + return currentIndex < arguments.length; + } + + String next() { + return arguments[ currentIndex++ ]; + } + + String peek() { + return arguments[ currentIndex ]; + } + + void treatNextAsLongOption() { + if ( HYPHEN_CHAR != arguments[ currentIndex ].charAt( 0 ) ) + arguments[ currentIndex ] = DOUBLE_HYPHEN + arguments[ currentIndex ]; + } +} diff --git a/src/joptsimple/BuiltinHelpFormatter.java b/src/joptsimple/BuiltinHelpFormatter.java new file mode 100755 index 0000000..94c8c5a --- /dev/null +++ b/src/joptsimple/BuiltinHelpFormatter.java @@ -0,0 +1,252 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import joptsimple.internal.Rows; +import joptsimple.internal.Strings; + +import static joptsimple.ParserRules.*; +import static joptsimple.internal.Classes.*; +import static joptsimple.internal.Strings.*; + +/** + *

A help formatter that allows configuration of overall row width and column separator width.

+ * + *

The formatter produces a two-column output. The left column is for the options, and the right column for their + * descriptions. The formatter will allow as much space as possible for the descriptions, by minimizing the option + * column's width, no greater than slightly less than half the overall desired width.

+ * + * @author Paul Holser + */ +public class BuiltinHelpFormatter implements HelpFormatter { + private final Rows nonOptionRows; + private final Rows optionRows; + + /** + * Makes a formatter with a pre-configured overall row width and column separator width. + */ + BuiltinHelpFormatter() { + this( 80, 2 ); + } + + /** + * Makes a formatter with a given overall row width and column separator width. + * + * @param desiredOverallWidth how many characters wide to make the overall help display + * @param desiredColumnSeparatorWidth how many characters wide to make the separation between option column and + * description column + */ + public BuiltinHelpFormatter( int desiredOverallWidth, int desiredColumnSeparatorWidth ) { + nonOptionRows = new Rows( desiredOverallWidth * 2, 0 ); + optionRows = new Rows( desiredOverallWidth, desiredColumnSeparatorWidth ); + } + + public String format( Map options ) { + Comparator comparator = + new Comparator() { + public int compare( OptionDescriptor first, OptionDescriptor second ) { + return first.options().iterator().next().compareTo( second.options().iterator().next() ); + } + }; + + Set sorted = new TreeSet( comparator ); + sorted.addAll( options.values() ); + + addRows( sorted ); + + return formattedHelpOutput(); + } + + private String formattedHelpOutput() { + StringBuilder formatted = new StringBuilder(); + String nonOptionDisplay = nonOptionRows.render(); + if ( !Strings.isNullOrEmpty( nonOptionDisplay ) ) + formatted.append( nonOptionDisplay ).append( LINE_SEPARATOR ); + formatted.append( optionRows.render() ); + + return formatted.toString(); + } + + private void addRows( Collection options ) { + addNonOptionsDescription( options ); + + if ( options.isEmpty() ) + optionRows.add( "No options specified", "" ); + else { + addHeaders( options ); + addOptions( options ); + } + + fitRowsToWidth(); + } + + private void addNonOptionsDescription( Collection options ) { + OptionDescriptor nonOptions = findAndRemoveNonOptionsSpec( options ); + if ( shouldShowNonOptionArgumentDisplay( nonOptions ) ) { + nonOptionRows.add( "Non-option arguments:", "" ); + nonOptionRows.add(createNonOptionArgumentsDisplay(nonOptions), ""); + } + } + + private boolean shouldShowNonOptionArgumentDisplay( OptionDescriptor nonOptions ) { + return !Strings.isNullOrEmpty( nonOptions.description() ) + || !Strings.isNullOrEmpty( nonOptions.argumentTypeIndicator() ) + || !Strings.isNullOrEmpty( nonOptions.argumentDescription() ); + } + + private String createNonOptionArgumentsDisplay(OptionDescriptor nonOptions) { + StringBuilder buffer = new StringBuilder(); + maybeAppendOptionInfo( buffer, nonOptions ); + maybeAppendNonOptionsDescription( buffer, nonOptions ); + + return buffer.toString(); + } + + private void maybeAppendNonOptionsDescription( StringBuilder buffer, OptionDescriptor nonOptions ) { + buffer.append( buffer.length() > 0 && !Strings.isNullOrEmpty( nonOptions.description() ) ? " -- " : "" ) + .append( nonOptions.description() ); + } + + private OptionDescriptor findAndRemoveNonOptionsSpec( Collection options ) { + for ( Iterator it = options.iterator(); it.hasNext(); ) { + OptionDescriptor next = it.next(); + if ( next.representsNonOptions() ) { + it.remove(); + return next; + } + } + + throw new AssertionError( "no non-options argument spec" ); + } + + private void addHeaders( Collection options ) { + if ( hasRequiredOption( options ) ) { + optionRows.add("Option (* = required)", "Description"); + optionRows.add("---------------------", "-----------"); + } else { + optionRows.add("Option", "Description"); + optionRows.add("------", "-----------"); + } + } + + private boolean hasRequiredOption( Collection options ) { + for ( OptionDescriptor each : options ) { + if ( each.isRequired() ) + return true; + } + + return false; + } + + private void addOptions( Collection options ) { + for ( OptionDescriptor each : options ) { + if ( !each.representsNonOptions() ) + optionRows.add( createOptionDisplay( each ), createDescriptionDisplay( each ) ); + } + } + + private String createOptionDisplay( OptionDescriptor descriptor ) { + StringBuilder buffer = new StringBuilder( descriptor.isRequired() ? "* " : "" ); + + for ( Iterator i = descriptor.options().iterator(); i.hasNext(); ) { + String option = i.next(); + buffer.append( option.length() > 1 ? DOUBLE_HYPHEN : HYPHEN ); + buffer.append( option ); + + if ( i.hasNext() ) + buffer.append( ", " ); + } + + maybeAppendOptionInfo( buffer, descriptor ); + + return buffer.toString(); + } + + private void maybeAppendOptionInfo( StringBuilder buffer, OptionDescriptor descriptor ) { + String indicator = extractTypeIndicator( descriptor ); + String description = descriptor.argumentDescription(); + if ( indicator != null || !isNullOrEmpty( description ) ) + appendOptionHelp( buffer, indicator, description, descriptor.requiresArgument() ); + } + + private String extractTypeIndicator( OptionDescriptor descriptor ) { + String indicator = descriptor.argumentTypeIndicator(); + + if ( !isNullOrEmpty( indicator ) && !String.class.getName().equals( indicator ) ) + return shortNameOf( indicator ); + + return null; + } + + private void appendOptionHelp( StringBuilder buffer, String typeIndicator, String description, boolean required ) { + if ( required ) + appendTypeIndicator( buffer, typeIndicator, description, '<', '>' ); + else + appendTypeIndicator( buffer, typeIndicator, description, '[', ']' ); + } + + private void appendTypeIndicator( StringBuilder buffer, String typeIndicator, String description, + char start, char end ) { + buffer.append( ' ' ).append( start ); + if ( typeIndicator != null ) + buffer.append( typeIndicator ); + + if ( !Strings.isNullOrEmpty( description ) ) { + if ( typeIndicator != null ) + buffer.append( ": " ); + + buffer.append( description ); + } + + buffer.append( end ); + } + + private String createDescriptionDisplay( OptionDescriptor descriptor ) { + List defaultValues = descriptor.defaultValues(); + if ( defaultValues.isEmpty() ) + return descriptor.description(); + + String defaultValuesDisplay = createDefaultValuesDisplay( defaultValues ); + return ( descriptor.description() + ' ' + surround( "default: " + defaultValuesDisplay, '(', ')' ) ).trim(); + } + + private String createDefaultValuesDisplay( List defaultValues ) { + return defaultValues.size() == 1 ? defaultValues.get( 0 ).toString() : defaultValues.toString(); + } + + private void fitRowsToWidth() { + nonOptionRows.fitToWidth(); + optionRows.fitToWidth(); + } +} diff --git a/src/joptsimple/HelpFormatter.java b/src/joptsimple/HelpFormatter.java new file mode 100755 index 0000000..3a62be4 --- /dev/null +++ b/src/joptsimple/HelpFormatter.java @@ -0,0 +1,45 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Map; + +/** + *

Represents objects charged with taking a set of option descriptions and producing some help text from them.

+ * + * @author Paul Holser + */ +public interface HelpFormatter { + /** + * Produces help text, given a set of option descriptors. + * + * @param options descriptors for the configured options of a parser + * @return text to be used as help + * @see OptionParser#printHelpOn(java.io.Writer) + * @see OptionParser#formatHelpWith(HelpFormatter) + */ + String format( Map options ); +} diff --git a/src/joptsimple/IllegalOptionSpecificationException.java b/src/joptsimple/IllegalOptionSpecificationException.java new file mode 100755 index 0000000..dceda90 --- /dev/null +++ b/src/joptsimple/IllegalOptionSpecificationException.java @@ -0,0 +1,46 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import static java.util.Collections.*; + +/** + * Thrown when the option parser is asked to recognize an option with illegal characters in it. + * + * @author Paul Holser + */ +class IllegalOptionSpecificationException extends OptionException { + private static final long serialVersionUID = -1L; + + IllegalOptionSpecificationException( String option ) { + super( singletonList( option ) ); + } + + @Override + public String getMessage() { + return singleOptionMessage() + " is not a legal option character"; + } +} diff --git a/src/joptsimple/LICENSE.txt b/src/joptsimple/LICENSE.txt new file mode 100755 index 0000000..f17b9f3 --- /dev/null +++ b/src/joptsimple/LICENSE.txt @@ -0,0 +1,22 @@ + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/joptsimple/MissingRequiredOptionException.java b/src/joptsimple/MissingRequiredOptionException.java new file mode 100755 index 0000000..362b42c --- /dev/null +++ b/src/joptsimple/MissingRequiredOptionException.java @@ -0,0 +1,46 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +/** + * Thrown when an option is marked as required, but not specified on the command line. + * + * @author Emils Solmanis + */ +class MissingRequiredOptionException extends OptionException { + private static final long serialVersionUID = -1L; + + protected MissingRequiredOptionException( Collection options ) { + super( options ); + } + + @Override + public String getMessage() { + return "Missing required option(s) " + multipleOptionMessage(); + } +} diff --git a/src/joptsimple/MultipleArgumentsForOptionException.java b/src/joptsimple/MultipleArgumentsForOptionException.java new file mode 100755 index 0000000..f5855f1 --- /dev/null +++ b/src/joptsimple/MultipleArgumentsForOptionException.java @@ -0,0 +1,46 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +/** + * Thrown when asking an {@link OptionSet} for a single argument of an option when many have been specified. + * + * @author Paul Holser + */ +class MultipleArgumentsForOptionException extends OptionException { + private static final long serialVersionUID = -1L; + + MultipleArgumentsForOptionException( Collection options ) { + super( options ); + } + + @Override + public String getMessage() { + return "Found multiple arguments for option " + multipleOptionMessage() + ", but you asked for only one"; + } +} diff --git a/src/joptsimple/NoArgumentOptionSpec.java b/src/joptsimple/NoArgumentOptionSpec.java new file mode 100755 index 0000000..923e971 --- /dev/null +++ b/src/joptsimple/NoArgumentOptionSpec.java @@ -0,0 +1,82 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.*; + +/** + * A specification for an option that does not accept arguments. + * + * @author Paul Holser + */ +class NoArgumentOptionSpec extends AbstractOptionSpec { + NoArgumentOptionSpec( String option ) { + this( singletonList( option ), "" ); + } + + NoArgumentOptionSpec( Collection options, String description ) { + super( options, description ); + } + + @Override + void handleOption( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions, + String detectedArgument ) { + + detectedOptions.add( this ); + } + + public boolean acceptsArguments() { + return false; + } + + public boolean requiresArgument() { + return false; + } + + public boolean isRequired() { + return false; + } + + public String argumentDescription() { + return ""; + } + + public String argumentTypeIndicator() { + return ""; + } + + @Override + protected Void convert( String argument ) { + return null; + } + + public List defaultValues() { + return emptyList(); + } +} diff --git a/src/joptsimple/NonOptionArgumentSpec.java b/src/joptsimple/NonOptionArgumentSpec.java new file mode 100755 index 0000000..45aa16c --- /dev/null +++ b/src/joptsimple/NonOptionArgumentSpec.java @@ -0,0 +1,170 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.List; + +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static joptsimple.internal.Reflection.*; + +/** + *

Specification of a command line's non-option arguments.

+ * + *

Instances are returned from {@link OptionParser} methods to allow the formation of parser directives as + * sentences in a "fluent interface" language. For example:

+ * + *
+ *   
+ *   OptionParser parser = new OptionParser();
+ *   parser.nonOptions( "files to be processed" ).ofType( File.class );
+ *   
+ * 
+ * + *

If no methods are invoked on an instance of this class, then that instance's option will treat the non-option + * arguments as {@link String}s.

+ * + * @param represents the type of the non-option arguments + * @author Paul Holser + */ +public class NonOptionArgumentSpec extends AbstractOptionSpec { + static final String NAME = "[arguments]"; + + private ValueConverter converter; + private String argumentDescription = ""; + + NonOptionArgumentSpec() { + this(""); + } + + NonOptionArgumentSpec( String description ) { + super( asList( NAME ), description ); + } + + /** + *

Specifies a type to which the non-option arguments are to be converted.

+ * + *

JOpt Simple accepts types that have either:

+ * + *
    + *
  1. a public static method called {@code valueOf} which accepts a single argument of type {@link String} + * and whose return type is the same as the class on which the method is declared. The {@code java.lang} + * primitive wrapper classes have such methods.
  2. + * + *
  3. a public constructor which accepts a single argument of type {@link String}.
  4. + *
+ * + *

This class converts arguments using those methods in that order; that is, {@code valueOf} would be invoked + * before a one-{@link String}-arg constructor would.

+ * + *

Invoking this method will trump any previous calls to this method or to + * {@link #withValuesConvertedBy(ValueConverter)}.

+ * + * @param represents the runtime class of the desired option argument type + * @param argumentType desired type of arguments to this spec's option + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws NullPointerException if the type is {@code null} + * @throws IllegalArgumentException if the type does not have the standard conversion methods + */ + @SuppressWarnings( "unchecked" ) + public NonOptionArgumentSpec ofType( Class argumentType ) { + converter = (ValueConverter) findConverter( argumentType ); + return (NonOptionArgumentSpec) this; + } + + /** + *

Specifies a converter to use to translate non-option arguments into Java objects. This is useful + * when converting to types that do not have the requisite factory method or constructor for + * {@link #ofType(Class)}.

+ * + *

Invoking this method will trump any previous calls to this method or to {@link #ofType(Class)}. + * + * @param represents the runtime class of the desired non-option argument type + * @param aConverter the converter to use + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws NullPointerException if the converter is {@code null} + */ + @SuppressWarnings( "unchecked" ) + public final NonOptionArgumentSpec withValuesConvertedBy( ValueConverter aConverter ) { + if ( aConverter == null ) + throw new NullPointerException( "illegal null converter" ); + + converter = (ValueConverter) aConverter; + return (NonOptionArgumentSpec) this; + } + + /** + *

Specifies a description for the non-option arguments that this spec represents. This description is used + * when generating help information about the parser.

+ * + * @param description describes the nature of the argument of this spec's option + * @return self, so that the caller can add clauses to the fluent interface sentence + */ + public NonOptionArgumentSpec describedAs( String description ) { + argumentDescription = description; + return this; + } + + @Override + protected final V convert( String argument ) { + return convertWith( converter, argument ); + } + + @Override + void handleOption( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions, + String detectedArgument ) { + + detectedOptions.addWithArgument( this, detectedArgument ); + } + + public List defaultValues() { + return emptyList(); + } + + public boolean isRequired() { + return false; + } + + public boolean acceptsArguments() { + return false; + } + + public boolean requiresArgument() { + return false; + } + + public String argumentDescription() { + return argumentDescription; + } + + public String argumentTypeIndicator() { + return argumentTypeIndicatorFrom( converter ); + } + + public boolean representsNonOptions() { + return true; + } +} diff --git a/src/joptsimple/OptionArgumentConversionException.java b/src/joptsimple/OptionArgumentConversionException.java new file mode 100755 index 0000000..dd98e14 --- /dev/null +++ b/src/joptsimple/OptionArgumentConversionException.java @@ -0,0 +1,50 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +/** + * Thrown when a problem occurs converting an argument of an option from {@link String} to another type. + * + * @author Paul Holser + */ +class OptionArgumentConversionException extends OptionException { + private static final long serialVersionUID = -1L; + + private final String argument; + + OptionArgumentConversionException( Collection options, String argument, Throwable cause ) { + super( options, cause ); + + this.argument = argument; + } + + @Override + public String getMessage() { + return "Cannot parse argument '" + argument + "' of option " + multipleOptionMessage(); + } +} diff --git a/src/joptsimple/OptionDeclarer.java b/src/joptsimple/OptionDeclarer.java new file mode 100755 index 0000000..302141b --- /dev/null +++ b/src/joptsimple/OptionDeclarer.java @@ -0,0 +1,110 @@ +package joptsimple; + +import java.util.Collection; + +/** + * Trains the option parser. This interface aids integration with other code which may expose declaration of options but + * not actual command-line parsing. + * + * @author Paul Holser + * @see OptionParser + */ +public interface OptionDeclarer { + /** + * Tells the parser to recognize the given option. + * + *

This method returns an instance of {@link OptionSpecBuilder} to allow the formation of parser directives + * as sentences in a fluent interface language. For example:

+ * + *

+     *   OptionDeclarer parser = new OptionParser();
+     *   parser.accepts( "c" ).withRequiredArg().ofType( Integer.class );
+     * 
+ * + *

If no methods are invoked on the returned {@link OptionSpecBuilder}, then the parser treats the option as + * accepting no argument.

+ * + * @param option the option to recognize + * @return an object that can be used to flesh out more detail about the option + * @throws OptionException if the option contains illegal characters + * @throws NullPointerException if the option is {@code null} + */ + OptionSpecBuilder accepts( String option ); + + /** + * Tells the parser to recognize the given option. + * + * @see #accepts(String) + * @param option the option to recognize + * @param description a string that describes the purpose of the option. This is used when generating help + * information about the parser. + * @return an object that can be used to flesh out more detail about the option + * @throws OptionException if the option contains illegal characters + * @throws NullPointerException if the option is {@code null} + */ + OptionSpecBuilder accepts( String option, String description ); + + /** + * Tells the parser to recognize the given options, and treat them as synonymous. + * + * @see #accepts(String) + * @param options the options to recognize and treat as synonymous + * @return an object that can be used to flesh out more detail about the options + * @throws OptionException if any of the options contain illegal characters + * @throws NullPointerException if the option list or any of its elements are {@code null} + */ + OptionSpecBuilder acceptsAll( Collection options ); + + /** + * Tells the parser to recognize the given options, and treat them as synonymous. + * + * @see #acceptsAll(Collection) + * @param options the options to recognize and treat as synonymous + * @param description a string that describes the purpose of the option. This is used when generating help + * information about the parser. + * @return an object that can be used to flesh out more detail about the options + * @throws OptionException if any of the options contain illegal characters + * @throws NullPointerException if the option list or any of its elements are {@code null} + * @throws IllegalArgumentException if the option list is empty + */ + OptionSpecBuilder acceptsAll( Collection options, String description ); + + /** + * Gives an object that represents an access point for non-option arguments on a command line. + * + * @return an object that can be used to flesh out more detail about the non-option arguments + */ + NonOptionArgumentSpec nonOptions(); + + /** + * Gives an object that represents an access point for non-option arguments on a command line. + * + * @see #nonOptions() + * @param description a string that describes the purpose of the non-option arguments. This is used when generating + * help information about the parser. + * @return an object that can be used to flesh out more detail about the non-option arguments + */ + NonOptionArgumentSpec nonOptions( String description ); + + /** + * Tells the parser whether or not to behave "POSIX-ly correct"-ly. + * + * @param setting {@code true} if the parser should behave "POSIX-ly correct"-ly + */ + void posixlyCorrect( boolean setting ); + + /** + *

Tells the parser to treat unrecognized options as non-option arguments.

+ * + *

If not called, then the parser raises an {@link OptionException} when it encounters an unrecognized + * option.

+ */ + void allowsUnrecognizedOptions(); + + /** + * Tells the parser either to recognize or ignore "-W"-style long options. + * + * @param recognize {@code true} if the parser is to recognize the special style of long options + */ + void recognizeAlternativeLongOptions( boolean recognize ); +} diff --git a/src/joptsimple/OptionDescriptor.java b/src/joptsimple/OptionDescriptor.java new file mode 100755 index 0000000..1bce1bc --- /dev/null +++ b/src/joptsimple/OptionDescriptor.java @@ -0,0 +1,101 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; +import java.util.List; + +/** + * Describes options that an option parser recognizes, in ways that might be useful to {@linkplain HelpFormatter + * help screens}. + * + * @author Paul Holser + */ +public interface OptionDescriptor { + /** + * A set of options that are mutually synonymous. + * + * @return synonymous options + */ + Collection options(); + + /** + * Description of this option's purpose. + * + * @return a description for the option + */ + String description(); + + /** + * What values will the option take if none are specified on the command line? + * + * @return any default values for the option + */ + List defaultValues(); + + /** + * Is this option {@linkplain ArgumentAcceptingOptionSpec#required() required} on a command line? + * + * @return whether the option is required + */ + boolean isRequired(); + + /** + * Does this option {@linkplain ArgumentAcceptingOptionSpec accept arguments}? + * + * @return whether the option accepts arguments + */ + boolean acceptsArguments(); + + /** + * Does this option {@linkplain OptionSpecBuilder#withRequiredArg() require an argument}? + * + * @return whether the option requires an argument + */ + boolean requiresArgument(); + + /** + * Gives a short {@linkplain ArgumentAcceptingOptionSpec#describedAs(String) description} of the option's argument. + * + * @return a description for the option's argument + */ + String argumentDescription(); + + /** + * Gives an indication of the {@linkplain ArgumentAcceptingOptionSpec#ofType(Class) expected type} of the option's + * argument. + * + * @return a description for the option's argument type + */ + String argumentTypeIndicator(); + + /** + * Tells whether this object represents the non-option arguments of a command line. + * + * @return {@code true} if this represents non-option arguments + */ + boolean representsNonOptions(); +} diff --git a/src/joptsimple/OptionException.java b/src/joptsimple/OptionException.java new file mode 100755 index 0000000..3635e04 --- /dev/null +++ b/src/joptsimple/OptionException.java @@ -0,0 +1,91 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import static java.util.Collections.*; + +import static joptsimple.internal.Strings.*; + +/** + * Thrown when a problem occurs during option parsing. + * + * @author Paul Holser + */ +public abstract class OptionException extends RuntimeException { + private static final long serialVersionUID = -1L; + + private final List options = new ArrayList(); + + protected OptionException( Collection options ) { + this.options.addAll( options ); + } + + protected OptionException( Collection options, Throwable cause ) { + super( cause ); + + this.options.addAll( options ); + } + + /** + * Gives the option being considered when the exception was created. + * + * @return the option being considered when the exception was created + */ + public Collection options() { + return unmodifiableCollection( options ); + } + + protected final String singleOptionMessage() { + return singleOptionMessage( options.get( 0 ) ); + } + + protected final String singleOptionMessage( String option ) { + return SINGLE_QUOTE + option + SINGLE_QUOTE; + } + + protected final String multipleOptionMessage() { + StringBuilder buffer = new StringBuilder( "[" ); + + for ( Iterator iter = options.iterator(); iter.hasNext(); ) { + buffer.append( singleOptionMessage( iter.next() ) ); + if ( iter.hasNext() ) + buffer.append( ", " ); + } + + buffer.append( ']' ); + + return buffer.toString(); + } + + static OptionException unrecognizedOption( String option ) { + return new UnrecognizedOptionException( option ); + } +} diff --git a/src/joptsimple/OptionMissingRequiredArgumentException.java b/src/joptsimple/OptionMissingRequiredArgumentException.java new file mode 100755 index 0000000..e35d624 --- /dev/null +++ b/src/joptsimple/OptionMissingRequiredArgumentException.java @@ -0,0 +1,46 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +/** + * Thrown when the option parser discovers an option that requires an argument, but that argument is missing. + * + * @author Paul Holser + */ +class OptionMissingRequiredArgumentException extends OptionException { + private static final long serialVersionUID = -1L; + + OptionMissingRequiredArgumentException( Collection options ) { + super( options ); + } + + @Override + public String getMessage() { + return "Option " + multipleOptionMessage() + " requires an argument"; + } +} diff --git a/src/joptsimple/OptionParser.java b/src/joptsimple/OptionParser.java new file mode 100755 index 0000000..03e34fd --- /dev/null +++ b/src/joptsimple/OptionParser.java @@ -0,0 +1,550 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import joptsimple.internal.AbbreviationMap; +import joptsimple.util.KeyValuePair; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.*; +import static joptsimple.OptionException.*; +import static joptsimple.OptionParserState.*; +import static joptsimple.ParserRules.*; + +/** + *

Parses command line arguments, using a syntax that attempts to take from the best of POSIX {@code getopt()} + * and GNU {@code getopt_long()}.

+ * + *

This parser supports short options and long options.

+ * + *
    + *
  • Short options begin with a single hyphen ("-") followed by a single letter or digit, + * or question mark ("?"), or dot (".").
  • + * + *
  • Short options can accept single arguments. The argument can be made required or optional. The option's + * argument can occur: + *
      + *
    • in the slot after the option, as in -d /tmp
    • + *
    • right up against the option, as in -d/tmp
    • + *
    • right up against the option separated by an equals sign ("="), as in -d=/tmp
    • + *
    + * To specify n arguments for an option, specify the option n times, once for each argument, + * as in -d /tmp -d /var -d /opt; or, when using the + * {@linkplain ArgumentAcceptingOptionSpec#withValuesSeparatedBy(char) "separated values"} clause of the "fluent + * interface" (see below), give multiple values separated by a given character as a single argument to the + * option.
  • + * + *
  • Short options can be clustered, so that -abc is treated as -a -b -c. If a short option + * in the cluster can accept an argument, the remaining characters are interpreted as the argument for that + * option.
  • + * + *
  • An argument consisting only of two hyphens ("--") signals that the remaining arguments are to be + * treated as non-options.
  • + * + *
  • An argument consisting only of a single hyphen is considered a non-option argument (though it can be an + * argument of an option). Many Unix programs treat single hyphens as stand-ins for the standard input or standard + * output streams.
  • + * + *
  • Long options begin with two hyphens ("--"), followed by multiple letters, digits, + * hyphens, question marks, or dots. A hyphen cannot be the first character of a long option specification when + * configuring the parser.
  • + * + *
  • You can abbreviate long options, so long as the abbreviation is unique.
  • + * + *
  • Long options can accept single arguments. The argument can be made required or optional. The option's + * argument can occur: + *
      + *
    • in the slot after the option, as in --directory /tmp
    • + *
    • right up against the option separated by an equals sign ("="), as in + * --directory=/tmp + *
    + * Specify multiple arguments for a long option in the same manner as for short options (see above).
  • + * + *
  • You can use a single hyphen ("-") instead of a double hyphen ("--") for a long + * option.
  • + * + *
  • The option -W is reserved. If you tell the parser to {@linkplain + * #recognizeAlternativeLongOptions(boolean) recognize alternative long options}, then it will treat, for example, + * -W foo=bar as the long option foo with argument bar, as though you had written + * --foo=bar.
  • + * + *
  • You can specify -W as a valid short option, or use it as an abbreviation for a long option, but + * {@linkplain #recognizeAlternativeLongOptions(boolean) recognizing alternative long options} will always supersede + * this behavior.
  • + * + *
  • You can specify a given short or long option multiple times on a single command line. The parser collects + * any arguments specified for those options as a list.
  • + * + *
  • If the parser detects an option whose argument is optional, and the next argument "looks like" an option, + * that argument is not treated as the argument to the option, but as a potentially valid option. If, on the other + * hand, the optional argument is typed as a derivative of {@link Number}, then that argument is treated as the + * negative number argument of the option, even if the parser recognizes the corresponding numeric option. + * For example: + *
    
    + *     OptionParser parser = new OptionParser();
    + *     parser.accepts( "a" ).withOptionalArg().ofType( Integer.class );
    + *     parser.accepts( "2" );
    + *     OptionSet options = parser.parse( "-a", "-2" );
    + *   
    + * In this case, the option set contains "a" with argument -2, not both "a" and + * "2". Swapping the elements in the args array gives the latter.
  • + *
+ * + *

There are two ways to tell the parser what options to recognize:

+ * + *
    + *
  1. A "fluent interface"-style API for specifying options, available since version 2. Sentences in this fluent + * interface language begin with a call to {@link #accepts(String) accepts} or {@link #acceptsAll(Collection) + * acceptsAll} methods; calls on the ensuing chain of objects describe whether the options can take an argument, + * whether the argument is required or optional, to what type arguments of the options should be converted if any, + * etc. Since version 3, these calls return an instance of {@link OptionSpec}, which can subsequently be used to + * retrieve the arguments of the associated option in a type-safe manner.
  2. + * + *
  3. Since version 1, a more concise way of specifying short options has been to use the special {@linkplain + * #OptionParser(String) constructor}. Arguments of options specified in this manner will be of type {@link String}. + * Here are the rules for the format of the specification strings this constructor accepts: + * + *
      + *
    • Any letter or digit is treated as an option character.
    • + * + *
    • An option character can be immediately followed by an asterisk (*) to indicate that the option is a + * "help" option.
    • + * + *
    • If an option character (with possible trailing asterisk) is followed by a single colon (":"), + * then the option requires an argument.
    • + * + *
    • If an option character (with possible trailing asterisk) is followed by two colons ("::"), + * then the option accepts an optional argument.
    • + * + *
    • Otherwise, the option character accepts no argument.
    • + * + *
    • If the option specification string begins with a plus sign ("+"), the parser will behave + * "POSIX-ly correct".
    • + * + *
    • If the option specification string contains the sequence "W;" (capital W followed by a + * semicolon), the parser will recognize the alternative form of long options.
    • + *
    + *
  4. + *
+ * + *

Each of the options in a list of options given to {@link #acceptsAll(Collection) acceptsAll} is treated as a + * synonym of the others. For example: + *

+ *     
+ *     OptionParser parser = new OptionParser();
+ *     parser.acceptsAll( asList( "w", "interactive", "confirmation" ) );
+ *     OptionSet options = parser.parse( "-w" );
+ *     
+ *   
+ * In this case, options.{@link OptionSet#has(String) has} would answer {@code true} when given arguments + * "w", "interactive", and "confirmation". The {@link OptionSet} would give the same + * responses to these arguments for its other methods as well.

+ * + *

By default, as with GNU {@code getopt()}, the parser allows intermixing of options and non-options. If, however, + * the parser has been created to be "POSIX-ly correct", then the first argument that does not look lexically like an + * option, and is not a required argument of a preceding option, signals the end of options. You can still bind + * optional arguments to their options using the abutting (for short options) or = syntax.

+ * + *

Unlike GNU {@code getopt()}, this parser does not honor the environment variable {@code POSIXLY_CORRECT}. + * "POSIX-ly correct" parsers are configured by either:

+ * + *
    + *
  1. using the method {@link #posixlyCorrect(boolean)}, or
  2. + * + *
  3. using the {@linkplain #OptionParser(String) constructor} with an argument whose first character is a plus sign + * ("+")
  4. + *
+ * + * @author Paul Holser + * @see The GNU C Library + */ +public class OptionParser implements OptionDeclarer { + private final AbbreviationMap> recognizedOptions; + private final Map, Set>> requiredIf; + private final Map, Set>> requiredUnless; + private OptionParserState state; + private boolean posixlyCorrect; + private boolean allowsUnrecognizedOptions; + private HelpFormatter helpFormatter = new BuiltinHelpFormatter(); + + /** + * Creates an option parser that initially recognizes no options, and does not exhibit "POSIX-ly correct" + * behavior. + */ + public OptionParser() { + recognizedOptions = new AbbreviationMap>(); + requiredIf = new HashMap, Set>>(); + requiredUnless = new HashMap, Set>>(); + state = moreOptions( false ); + + recognize( new NonOptionArgumentSpec() ); + } + + /** + * Creates an option parser and configures it to recognize the short options specified in the given string. + * + * Arguments of options specified this way will be of type {@link String}. + * + * @param optionSpecification an option specification + * @throws NullPointerException if {@code optionSpecification} is {@code null} + * @throws OptionException if the option specification contains illegal characters or otherwise cannot be + * recognized + */ + public OptionParser( String optionSpecification ) { + this(); + + new OptionSpecTokenizer( optionSpecification ).configure( this ); + } + + public OptionSpecBuilder accepts( String option ) { + return acceptsAll( singletonList( option ) ); + } + + public OptionSpecBuilder accepts( String option, String description ) { + return acceptsAll( singletonList( option ), description ); + } + + public OptionSpecBuilder acceptsAll( Collection options ) { + return acceptsAll( options, "" ); + } + + public OptionSpecBuilder acceptsAll( Collection options, String description ) { + if ( options.isEmpty() ) + throw new IllegalArgumentException( "need at least one option" ); + + ensureLegalOptions( options ); + + return new OptionSpecBuilder( this, options, description ); + } + + public NonOptionArgumentSpec nonOptions() { + NonOptionArgumentSpec spec = new NonOptionArgumentSpec(); + + recognize( spec ); + + return spec; + } + + public NonOptionArgumentSpec nonOptions( String description ) { + NonOptionArgumentSpec spec = new NonOptionArgumentSpec( description ); + + recognize( spec ); + + return spec; + } + + public void posixlyCorrect( boolean setting ) { + posixlyCorrect = setting; + state = moreOptions( setting ); + } + + boolean posixlyCorrect() { + return posixlyCorrect; + } + + public void allowsUnrecognizedOptions() { + allowsUnrecognizedOptions = true; + } + + boolean doesAllowsUnrecognizedOptions() { + return allowsUnrecognizedOptions; + } + + public void recognizeAlternativeLongOptions( boolean recognize ) { + if ( recognize ) + recognize( new AlternativeLongOptionSpec() ); + else + recognizedOptions.remove( String.valueOf( RESERVED_FOR_EXTENSIONS ) ); + } + + void recognize( AbstractOptionSpec spec ) { + recognizedOptions.putAll( spec.options(), spec ); + } + + /** + * Writes information about the options this parser recognizes to the given output sink. + * + * The output sink is flushed, but not closed. + * + * @param sink the sink to write information to + * @throws IOException if there is a problem writing to the sink + * @throws NullPointerException if {@code sink} is {@code null} + * @see #printHelpOn(Writer) + */ + public void printHelpOn( OutputStream sink ) throws IOException { + printHelpOn( new OutputStreamWriter( sink ) ); + } + + /** + * Writes information about the options this parser recognizes to the given output sink. + * + * The output sink is flushed, but not closed. + * + * @param sink the sink to write information to + * @throws IOException if there is a problem writing to the sink + * @throws NullPointerException if {@code sink} is {@code null} + * @see #printHelpOn(OutputStream) + */ + public void printHelpOn( Writer sink ) throws IOException { + sink.write( helpFormatter.format( recognizedOptions.toJavaUtilMap() ) ); + sink.flush(); + } + + /** + * Tells the parser to use the given formatter when asked to {@linkplain #printHelpOn(java.io.Writer) print help}. + * + * @param formatter the formatter to use for printing help + * @throws NullPointerException if the formatter is {@code null} + */ + public void formatHelpWith( HelpFormatter formatter ) { + if ( formatter == null ) + throw new NullPointerException(); + + helpFormatter = formatter; + } + + /** + * Retrieves all the options which have been configured for the parser. + * + * @return a {@link Map} containing all the configured options and their corresponding {@link OptionSpec} + */ + public Map> recognizedOptions() { + return new HashMap>( recognizedOptions.toJavaUtilMap() ); + } + + /** + * Parses the given command line arguments according to the option specifications given to the parser. + * + * @param arguments arguments to parse + * @return an {@link OptionSet} describing the parsed options, their arguments, and any non-option arguments found + * @throws OptionException if problems are detected while parsing + * @throws NullPointerException if the argument list is {@code null} + */ + public OptionSet parse( String... arguments ) { + ArgumentList argumentList = new ArgumentList( arguments ); + OptionSet detected = new OptionSet( recognizedOptions.toJavaUtilMap() ); + detected.add( recognizedOptions.get( NonOptionArgumentSpec.NAME ) ); + + while ( argumentList.hasMore() ) + state.handleArgument( this, argumentList, detected ); + + reset(); + + ensureRequiredOptions( detected ); + + return detected; + } + + private void ensureRequiredOptions( OptionSet options ) { + Collection missingRequiredOptions = missingRequiredOptions( options ); + boolean helpOptionPresent = isHelpOptionPresent( options ); + + if ( !missingRequiredOptions.isEmpty() && !helpOptionPresent ) + throw new MissingRequiredOptionException( missingRequiredOptions ); + } + + private Collection missingRequiredOptions( OptionSet options ) { + Collection missingRequiredOptions = new HashSet(); + + for ( AbstractOptionSpec each : recognizedOptions.toJavaUtilMap().values() ) { + if ( each.isRequired() && !options.has( each ) ) + missingRequiredOptions.addAll( each.options() ); + } + + for ( Map.Entry, Set>> eachEntry : requiredIf.entrySet() ) { + AbstractOptionSpec required = specFor( eachEntry.getKey().iterator().next() ); + + if ( optionsHasAnyOf( options, eachEntry.getValue() ) && !options.has( required ) ) { + missingRequiredOptions.addAll( required.options() ); + } + } + + for ( Map.Entry, Set>> eachEntry : requiredUnless.entrySet() ) { + AbstractOptionSpec required = specFor( eachEntry.getKey().iterator().next() ); + + if ( !optionsHasAnyOf( options, eachEntry.getValue() ) && !options.has( required ) ) { + missingRequiredOptions.addAll( required.options() ); + } + } + + return missingRequiredOptions; + } + + private boolean optionsHasAnyOf( OptionSet options, Collection> specs ) { + for ( OptionSpec each : specs ) { + if ( options.has( each ) ) + return true; + } + + return false; + } + + private boolean isHelpOptionPresent( OptionSet options ) { + boolean helpOptionPresent = false; + for ( AbstractOptionSpec each : recognizedOptions.toJavaUtilMap().values() ) { + if ( each.isForHelp() && options.has( each ) ) { + helpOptionPresent = true; + break; + } + } + return helpOptionPresent; + } + + void handleLongOptionToken( String candidate, ArgumentList arguments, OptionSet detected ) { + KeyValuePair optionAndArgument = parseLongOptionWithArgument( candidate ); + + if ( !isRecognized( optionAndArgument.key ) ) + throw unrecognizedOption( optionAndArgument.key ); + + AbstractOptionSpec optionSpec = specFor( optionAndArgument.key ); + optionSpec.handleOption( this, arguments, detected, optionAndArgument.value ); + } + + void handleShortOptionToken( String candidate, ArgumentList arguments, OptionSet detected ) { + KeyValuePair optionAndArgument = parseShortOptionWithArgument( candidate ); + + if ( isRecognized( optionAndArgument.key ) ) { + specFor( optionAndArgument.key ).handleOption( this, arguments, detected, optionAndArgument.value ); + } + else + handleShortOptionCluster( candidate, arguments, detected ); + } + + private void handleShortOptionCluster( String candidate, ArgumentList arguments, OptionSet detected ) { + char[] options = extractShortOptionsFrom( candidate ); + validateOptionCharacters( options ); + + for ( int i = 0; i < options.length; i++ ) { + AbstractOptionSpec optionSpec = specFor( options[ i ] ); + + if ( optionSpec.acceptsArguments() && options.length > i + 1 ) { + String detectedArgument = String.valueOf( options, i + 1, options.length - 1 - i ); + optionSpec.handleOption( this, arguments, detected, detectedArgument ); + break; + } + + optionSpec.handleOption( this, arguments, detected, null ); + } + } + + void handleNonOptionArgument( String candidate, ArgumentList arguments, OptionSet detectedOptions ) { + specFor( NonOptionArgumentSpec.NAME ).handleOption( this, arguments, detectedOptions, candidate ); + } + + void noMoreOptions() { + state = OptionParserState.noMoreOptions(); + } + + boolean looksLikeAnOption( String argument ) { + return isShortOptionToken( argument ) || isLongOptionToken( argument ); + } + + boolean isRecognized( String option ) { + return recognizedOptions.contains( option ); + } + + void requiredIf( Collection precedentSynonyms, String required ) { + requiredIf( precedentSynonyms, specFor( required ) ); + } + + void requiredIf( Collection precedentSynonyms, OptionSpec required ) { + putRequiredOption( precedentSynonyms, required, requiredIf ); + } + + void requiredUnless( Collection precedentSynonyms, String required ) { + requiredUnless( precedentSynonyms, specFor( required ) ); + } + + void requiredUnless( Collection precedentSynonyms, OptionSpec required ) { + putRequiredOption( precedentSynonyms, required, requiredUnless ); + } + + private void putRequiredOption( Collection precedentSynonyms, OptionSpec required, + Map, Set>> target ) { + + for ( String each : precedentSynonyms ) { + AbstractOptionSpec spec = specFor( each ); + if ( spec == null ) + throw new UnconfiguredOptionException( precedentSynonyms ); + } + + Set> associated = target.get( precedentSynonyms ); + if ( associated == null ) { + associated = new HashSet>(); + target.put( precedentSynonyms, associated ); + } + + associated.add( required ); + } + + private AbstractOptionSpec specFor( char option ) { + return specFor( String.valueOf( option ) ); + } + + private AbstractOptionSpec specFor( String option ) { + return recognizedOptions.get( option ); + } + + private void reset() { + state = moreOptions( posixlyCorrect ); + } + + private static char[] extractShortOptionsFrom( String argument ) { + char[] options = new char[ argument.length() - 1 ]; + argument.getChars( 1, argument.length(), options, 0 ); + + return options; + } + + private void validateOptionCharacters( char[] options ) { + for ( char each : options ) { + String option = String.valueOf( each ); + + if ( !isRecognized( option ) ) + throw unrecognizedOption( option ); + + if ( specFor( option ).acceptsArguments() ) + return; + } + } + + private static KeyValuePair parseLongOptionWithArgument( String argument ) { + return KeyValuePair.valueOf( argument.substring( 2 ) ); + } + + private static KeyValuePair parseShortOptionWithArgument( String argument ) { + return KeyValuePair.valueOf( argument.substring( 1 ) ); + } +} diff --git a/src/joptsimple/OptionParserState.java b/src/joptsimple/OptionParserState.java new file mode 100755 index 0000000..e7fb50e --- /dev/null +++ b/src/joptsimple/OptionParserState.java @@ -0,0 +1,76 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import static joptsimple.ParserRules.*; + +/** + * Abstraction of parser state; mostly serves to model how a parser behaves depending on whether end-of-options + * has been detected. + * + * @author Paul Holser + */ +abstract class OptionParserState { + static OptionParserState noMoreOptions() { + return new OptionParserState() { + @Override + protected void handleArgument( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions ) { + parser.handleNonOptionArgument( arguments.next(), arguments, detectedOptions ); + } + }; + } + + static OptionParserState moreOptions( final boolean posixlyCorrect ) { + return new OptionParserState() { + @Override + protected void handleArgument( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions ) { + String candidate = arguments.next(); + try { + if ( isOptionTerminator( candidate ) ) { + parser.noMoreOptions(); + return; + } else if ( isLongOptionToken( candidate ) ) { + parser.handleLongOptionToken( candidate, arguments, detectedOptions ); + return; + } else if ( isShortOptionToken( candidate ) ) { + parser.handleShortOptionToken( candidate, arguments, detectedOptions ); + return; + } + } catch ( UnrecognizedOptionException e ) { + if ( !parser.doesAllowsUnrecognizedOptions() ) + throw e; + } + + if ( posixlyCorrect ) + parser.noMoreOptions(); + + parser.handleNonOptionArgument( candidate, arguments, detectedOptions ); + } + }; + } + + protected abstract void handleArgument( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions ); +} diff --git a/src/joptsimple/OptionSet.java b/src/joptsimple/OptionSet.java new file mode 100755 index 0000000..f03c187 --- /dev/null +++ b/src/joptsimple/OptionSet.java @@ -0,0 +1,321 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.*; + +import static java.util.Collections.*; + +import static joptsimple.internal.Objects.*; + +/** + * Representation of a group of detected command line options, their arguments, and non-option arguments. + * + * @author Paul Holser + */ +public class OptionSet { + private final List> detectedSpecs; + private final Map> detectedOptions; + private final Map, List> optionsToArguments; + private final Map> recognizedSpecs; + private final Map> defaultValues; + + /* + * Package-private because clients don't create these. + */ + OptionSet( Map> recognizedSpecs ) { + detectedSpecs = new ArrayList>(); + detectedOptions = new HashMap>(); + optionsToArguments = new IdentityHashMap, List>(); + defaultValues = defaultValues( recognizedSpecs ); + this.recognizedSpecs = recognizedSpecs; + } + + /** + * Tells whether any options were detected. + * + * @return {@code true} if any options were detected + */ + public boolean hasOptions() { + return !detectedOptions.isEmpty(); + } + + /** + * Tells whether the given option was detected. + * + * @param option the option to search for + * @return {@code true} if the option was detected + * @see #has(OptionSpec) + */ + public boolean has( String option ) { + return detectedOptions.containsKey( option ); + } + + /** + * Tells whether the given option was detected. + * + *

This method recognizes only instances of options returned from the fluent interface methods.

+ * + *

Specifying a {@linkplain ArgumentAcceptingOptionSpec#defaultsTo(Object, Object[])} default argument value} + * for an option does not cause this method to return {@code true} if the option was not detected on the command + * line.

+ * + * @param option the option to search for + * @return {@code true} if the option was detected + * @see #has(String) + */ + public boolean has( OptionSpec option ) { + return optionsToArguments.containsKey( option ); + } + + /** + * Tells whether there are any arguments associated with the given option. + * + * @param option the option to search for + * @return {@code true} if the option was detected and at least one argument was detected for the option + * @see #hasArgument(OptionSpec) + */ + public boolean hasArgument( String option ) { + AbstractOptionSpec spec = detectedOptions.get( option ); + return spec != null && hasArgument( spec ); + } + + /** + * Tells whether there are any arguments associated with the given option. + * + *

This method recognizes only instances of options returned from the fluent interface methods.

+ * + *

Specifying a {@linkplain ArgumentAcceptingOptionSpec#defaultsTo(Object, Object[]) default argument value} + * for an option does not cause this method to return {@code true} if the option was not detected on the command + * line, or if the option can take an optional argument but did not have one on the command line.

+ * + * @param option the option to search for + * @return {@code true} if the option was detected and at least one argument was detected for the option + * @throws NullPointerException if {@code option} is {@code null} + * @see #hasArgument(String) + */ + public boolean hasArgument( OptionSpec option ) { + ensureNotNull( option ); + + List values = optionsToArguments.get( option ); + return values != null && !values.isEmpty(); + } + + /** + * Gives the argument associated with the given option. If the option was given an argument type, the argument + * will take on that type; otherwise, it will be a {@link String}. + * + *

Specifying a {@linkplain ArgumentAcceptingOptionSpec#defaultsTo(Object, Object[]) default argument value} + * for an option will cause this method to return that default value even if the option was not detected on the + * command line, or if the option can take an optional argument but did not have one on the command line.

+ * + * @param option the option to search for + * @return the argument of the given option; {@code null} if no argument is present, or that option was not + * detected + * @throws NullPointerException if {@code option} is {@code null} + * @throws OptionException if more than one argument was detected for the option + */ + public Object valueOf( String option ) { + ensureNotNull( option ); + + AbstractOptionSpec spec = detectedOptions.get( option ); + if ( spec == null ) { + List defaults = defaultValuesFor( option ); + return defaults.isEmpty() ? null : defaults.get( 0 ); + } + + return valueOf( spec ); + } + + /** + * Gives the argument associated with the given option. + * + *

This method recognizes only instances of options returned from the fluent interface methods.

+ * + * @param represents the type of the arguments the given option accepts + * @param option the option to search for + * @return the argument of the given option; {@code null} if no argument is present, or that option was not + * detected + * @throws OptionException if more than one argument was detected for the option + * @throws NullPointerException if {@code option} is {@code null} + * @throws ClassCastException if the arguments of this option are not of the expected type + */ + public V valueOf( OptionSpec option ) { + ensureNotNull( option ); + + List values = valuesOf( option ); + switch ( values.size() ) { + case 0: + return null; + case 1: + return values.get( 0 ); + default: + throw new MultipleArgumentsForOptionException( option.options() ); + } + } + + /** + *

Gives any arguments associated with the given option. If the option was given an argument type, the + * arguments will take on that type; otherwise, they will be {@link String}s.

+ * + * @param option the option to search for + * @return the arguments associated with the option, as a list of objects of the type given to the arguments; an + * empty list if no such arguments are present, or if the option was not detected + * @throws NullPointerException if {@code option} is {@code null} + */ + public List valuesOf( String option ) { + ensureNotNull( option ); + + AbstractOptionSpec spec = detectedOptions.get( option ); + return spec == null ? defaultValuesFor( option ) : valuesOf( spec ); + } + + /** + *

Gives any arguments associated with the given option. If the option was given an argument type, the + * arguments will take on that type; otherwise, they will be {@link String}s.

+ * + *

This method recognizes only instances of options returned from the fluent interface methods.

+ * + * @param represents the type of the arguments the given option accepts + * @param option the option to search for + * @return the arguments associated with the option; an empty list if no such arguments are present, or if the + * option was not detected + * @throws NullPointerException if {@code option} is {@code null} + * @throws OptionException if there is a problem converting the option's arguments to the desired type; for + * example, if the type does not implement a correct conversion constructor or method + */ + public List valuesOf( OptionSpec option ) { + ensureNotNull( option ); + + List values = optionsToArguments.get( option ); + if ( values == null || values.isEmpty() ) + return defaultValueFor( option ); + + AbstractOptionSpec spec = (AbstractOptionSpec) option; + List convertedValues = new ArrayList(); + for ( String each : values ) + convertedValues.add( spec.convert( each ) ); + + return unmodifiableList( convertedValues ); + } + + /** + * Gives the set of options that were detected, in the form of {@linkplain OptionSpec}s, in the order in which the + * options were found on the command line. + * + * @return the set of detected command line options + */ + public List> specs() { + List> specs = detectedSpecs; + specs.remove( detectedOptions.get( NonOptionArgumentSpec.NAME ) ); + + return unmodifiableList( specs ); + } + + /** + * Gives all declared options as a map of string to {@linkplain OptionSpec}. + * + * @return the declared options as a map + */ + public Map, List> asMap() { + Map, List> map = new HashMap, List>(); + for ( AbstractOptionSpec spec : recognizedSpecs.values() ) + if ( !spec.representsNonOptions() ) + map.put( spec, valuesOf( spec ) ); + return unmodifiableMap( map ); + } + + /** + * @return the detected non-option arguments + */ + public List nonOptionArguments() { + return unmodifiableList( valuesOf( detectedOptions.get( NonOptionArgumentSpec.NAME ) ) ); + } + + void add( AbstractOptionSpec spec ) { + addWithArgument( spec, null ); + } + + void addWithArgument( AbstractOptionSpec spec, String argument ) { + detectedSpecs.add( spec ); + + for ( String each : spec.options() ) + detectedOptions.put( each, spec ); + + List optionArguments = optionsToArguments.get( spec ); + + if ( optionArguments == null ) { + optionArguments = new ArrayList(); + optionsToArguments.put( spec, optionArguments ); + } + + if ( argument != null ) + optionArguments.add( argument ); + } + + @Override + public boolean equals( Object that ) { + if ( this == that ) + return true; + + if ( that == null || !getClass().equals( that.getClass() ) ) + return false; + + OptionSet other = (OptionSet) that; + Map, List> thisOptionsToArguments = + new HashMap, List>( optionsToArguments ); + Map, List> otherOptionsToArguments = + new HashMap, List>( other.optionsToArguments ); + return detectedOptions.equals( other.detectedOptions ) + && thisOptionsToArguments.equals( otherOptionsToArguments ); + } + + @Override + public int hashCode() { + Map, List> thisOptionsToArguments = + new HashMap, List>( optionsToArguments ); + return detectedOptions.hashCode() ^ thisOptionsToArguments.hashCode(); + } + + @SuppressWarnings( "unchecked" ) + private List defaultValuesFor( String option ) { + if ( defaultValues.containsKey( option ) ) + return (List) defaultValues.get( option ); + + return emptyList(); + } + + private List defaultValueFor( OptionSpec option ) { + return defaultValuesFor( option.options().iterator().next() ); + } + + private static Map> defaultValues( Map> recognizedSpecs ) { + Map> defaults = new HashMap>(); + for ( Map.Entry> each : recognizedSpecs.entrySet() ) + defaults.put( each.getKey(), each.getValue().defaultValues() ); + return defaults; + } +} diff --git a/src/joptsimple/OptionSpec.java b/src/joptsimple/OptionSpec.java new file mode 100755 index 0000000..a2701ed --- /dev/null +++ b/src/joptsimple/OptionSpec.java @@ -0,0 +1,98 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; +import java.util.List; + +/** + * Describes options that an option parser recognizes. + * + *

Instances of this interface are returned by the "fluent interface" methods to allow retrieval of option arguments + * in a type-safe manner. Here's an example:

+ * + *

+ *     OptionParser parser = new OptionParser();
+ *     OptionSpec<Integer> count =
+ *         parser.accepts( "count" ).withRequiredArg().ofType( Integer.class );
+ *     OptionSet options = parser.parse( "--count", "2" );
+ *     assert options.has( count );
+ *     int countValue = options.valueOf( count );
+ *     assert countValue == count.value( options );
+ *     List<Integer> countValues = options.valuesOf( count );
+ *     assert countValues.equals( count.values( options ) );
+ * 
+ * + * @param represents the type of the arguments this option accepts + * @author Paul Holser + */ +public interface OptionSpec { + /** + * Gives any arguments associated with the given option in the given set of detected options. + * + *

Specifying a {@linkplain ArgumentAcceptingOptionSpec#defaultsTo(Object, Object[]) default argument value} + * for this option will cause this method to return that default value even if this option was not detected on the + * command line, or if this option can take an optional argument but did not have one on the command line.

+ * + * @param detectedOptions the detected options to search in + * @return the arguments associated with this option; an empty list if no such arguments are present, or if this + * option was not detected + * @throws OptionException if there is a problem converting this option's arguments to the desired type; for + * example, if the type does not implement a correct conversion constructor or method + * @throws NullPointerException if {@code detectedOptions} is {@code null} + * @see OptionSet#valuesOf(OptionSpec) + */ + List values( OptionSet detectedOptions ); + + /** + * Gives the argument associated with the given option in the given set of detected options. + * + *

Specifying a {@linkplain ArgumentAcceptingOptionSpec#defaultsTo(Object, Object[]) default argument value} + * for this option will cause this method to return that default value even if this option was not detected on the + * command line, or if this option can take an optional argument but did not have one on the command line.

+ * + * @param detectedOptions the detected options to search in + * @return the argument of the this option; {@code null} if no argument is present, or that option was not detected + * @throws OptionException if more than one argument was detected for the option + * @throws NullPointerException if {@code detectedOptions} is {@code null} + * @throws ClassCastException if the arguments of this option are not of the expected type + * @see OptionSet#valueOf(OptionSpec) + */ + V value( OptionSet detectedOptions ); + + /** + * @return the string representations of this option + */ + Collection options(); + + /** + * Tells whether this option is designated as a "help" option. The presence of a "help" option on a command line + * means that missing "required" options will not cause parsing to fail. + * + * @return whether this option is designated as a "help" option + */ + boolean isForHelp(); +} diff --git a/src/joptsimple/OptionSpecBuilder.java b/src/joptsimple/OptionSpecBuilder.java new file mode 100755 index 0000000..b66f49a --- /dev/null +++ b/src/joptsimple/OptionSpecBuilder.java @@ -0,0 +1,195 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Allows callers to specify whether a given option accepts arguments (required or optional). + * + *

Instances are returned from {@link OptionParser#accepts(String)} to allow the formation of parser directives as + * sentences in a "fluent interface" language. For example:

+ * + *

+ *   OptionParser parser = new OptionParser();
+ *   parser.accepts( "c" ).withRequiredArg().ofType( Integer.class );
+ * 
+ * + *

If no methods are invoked on an instance of this class, then that instance's option will accept no argument.

+ * + *

Note that you should not use the fluent interface clauses in a way that would defeat the typing of option + * arguments:

+ * + *

+ *   OptionParser parser = new OptionParser();
+ *   ArgumentAcceptingOptionSpec<String> optionC =
+ *       parser.accepts( "c" ).withRequiredArg();
+ *   optionC.ofType( Integer.class );  // DON'T THROW AWAY THE TYPE!
+ *
+ *   String value = parser.parse( "-c", "2" ).valueOf( optionC );  // ClassCastException
+ * 
+ * + * @author Paul Holser + */ +public class OptionSpecBuilder extends NoArgumentOptionSpec { + private final OptionParser parser; + + OptionSpecBuilder( OptionParser parser, Collection options, String description ) { + super( options, description ); + + this.parser = parser; + attachToParser(); + } + + private void attachToParser() { + parser.recognize( this ); + } + + /** + * Informs an option parser that this builder's option requires an argument. + * + * @return a specification for the option + */ + public ArgumentAcceptingOptionSpec withRequiredArg() { + ArgumentAcceptingOptionSpec newSpec = + new RequiredArgumentOptionSpec( options(), description() ); + parser.recognize( newSpec ); + + return newSpec; + } + + /** + * Informs an option parser that this builder's option accepts an optional argument. + * + * @return a specification for the option + */ + public ArgumentAcceptingOptionSpec withOptionalArg() { + ArgumentAcceptingOptionSpec newSpec = + new OptionalArgumentOptionSpec( options(), description() ); + parser.recognize( newSpec ); + + return newSpec; + } + + /** + *

Informs an option parser that this builder's option is required if the given option is present on the command + * line.

+ * + *

For a given option, you should not mix this with {@link #requiredUnless(String, String...) + * requiredUnless} to avoid conflicts.

+ * + * @param dependent an option whose presence on a command line makes this builder's option required + * @param otherDependents other options whose presence on a command line makes this builder's option required + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws OptionException if any of the dependent options haven't been configured in the parser yet + */ + public OptionSpecBuilder requiredIf( String dependent, String... otherDependents ) { + List dependents = validatedDependents( dependent, otherDependents ); + for ( String each : dependents ) { + parser.requiredIf( options(), each ); + } + + return this; + } + + /** + *

Informs an option parser that this builder's option is required if the given option is present on the command + * line.

+ * + *

For a given option, you should not mix this with {@link #requiredUnless(OptionSpec, OptionSpec[]) + * requiredUnless} to avoid conflicts.

+ * + *

This method recognizes only instances of options returned from the fluent interface methods.

+ * + * @param dependent the option whose presence on a command line makes this builder's option required + * @param otherDependents other options whose presence on a command line makes this builder's option required + * @return self, so that the caller can add clauses to the fluent interface sentence + */ + public OptionSpecBuilder requiredIf( OptionSpec dependent, OptionSpec... otherDependents ) { + parser.requiredIf( options(), dependent ); + for ( OptionSpec each : otherDependents ) + parser.requiredIf( options(), each ); + + return this; + } + + /** + *

Informs an option parser that this builder's option is required if the given option is absent on the command + * line.

+ * + *

For a given option, you should not mix this with {@link #requiredIf(OptionSpec, OptionSpec[]) + * requiredIf} to avoid conflicts.

+ * + * @param dependent an option whose absence on a command line makes this builder's option required + * @param otherDependents other options whose absence on a command line makes this builder's option required + * @return self, so that the caller can add clauses to the fluent interface sentence + * @throws OptionException if any of the dependent options haven't been configured in the parser yet + */ + public OptionSpecBuilder requiredUnless( String dependent, String... otherDependents ) { + List dependents = validatedDependents( dependent, otherDependents ); + for ( String each : dependents ) { + parser.requiredUnless( options(), each ); + } + return this; + } + + /** + *

Informs an option parser that this builder's option is required if the given option is absent on the command + * line.

+ * + *

For a given option, you should not mix this with {@link #requiredIf(OptionSpec, OptionSpec[]) + * requiredIf} to avoid conflicts.

+ * + *

This method recognizes only instances of options returned from the fluent interface methods.

+ * + * @param dependent the option whose absence on a command line makes this builder's option required + * @param otherDependents other options whose absence on a command line makes this builder's option required + * @return self, so that the caller can add clauses to the fluent interface sentence + */ + public OptionSpecBuilder requiredUnless( OptionSpec dependent, OptionSpec... otherDependents ) { + parser.requiredUnless( options(), dependent ); + for ( OptionSpec each : otherDependents ) + parser.requiredUnless( options(), each ); + + return this; + } + + private List validatedDependents( String dependent, String... otherDependents ) { + List dependents = new ArrayList(); + dependents.add( dependent ); + Collections.addAll( dependents, otherDependents ); + + for ( String each : dependents ) { + if ( !parser.isRecognized( each ) ) + throw new UnconfiguredOptionException( each ); + } + + return dependents; + } +} diff --git a/src/joptsimple/OptionSpecTokenizer.java b/src/joptsimple/OptionSpecTokenizer.java new file mode 100755 index 0000000..1d6f910 --- /dev/null +++ b/src/joptsimple/OptionSpecTokenizer.java @@ -0,0 +1,126 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.NoSuchElementException; + +import static joptsimple.ParserRules.*; + +/** + * Tokenizes a short option specification string. + * + * @author Paul Holser + */ +class OptionSpecTokenizer { + private static final char POSIXLY_CORRECT_MARKER = '+'; + private static final char HELP_MARKER = '*'; + + private String specification; + private int index; + + OptionSpecTokenizer( String specification ) { + if ( specification == null ) + throw new NullPointerException( "null option specification" ); + + this.specification = specification; + } + + boolean hasMore() { + return index < specification.length(); + } + + AbstractOptionSpec next() { + if ( !hasMore() ) + throw new NoSuchElementException(); + + + String optionCandidate = String.valueOf( specification.charAt( index ) ); + index++; + + AbstractOptionSpec spec; + if ( RESERVED_FOR_EXTENSIONS.equals( optionCandidate ) ) { + spec = handleReservedForExtensionsToken(); + + if ( spec != null ) + return spec; + } + + ensureLegalOption( optionCandidate ); + + if ( hasMore() ) { + boolean forHelp = false; + if ( specification.charAt( index ) == HELP_MARKER ) { + forHelp = true; + ++index; + } + spec = hasMore() && specification.charAt( index ) == ':' + ? handleArgumentAcceptingOption( optionCandidate ) + : new NoArgumentOptionSpec( optionCandidate ); + if ( forHelp ) + spec.forHelp(); + } else + spec = new NoArgumentOptionSpec( optionCandidate ); + + return spec; + } + + void configure( OptionParser parser ) { + adjustForPosixlyCorrect( parser ); + + while ( hasMore() ) + parser.recognize( next() ); + } + + private void adjustForPosixlyCorrect( OptionParser parser ) { + if ( POSIXLY_CORRECT_MARKER == specification.charAt( 0 ) ) { + parser.posixlyCorrect( true ); + specification = specification.substring( 1 ); + } + } + + private AbstractOptionSpec handleReservedForExtensionsToken() { + if ( !hasMore() ) + return new NoArgumentOptionSpec( RESERVED_FOR_EXTENSIONS ); + + if ( specification.charAt( index ) == ';' ) { + ++index; + return new AlternativeLongOptionSpec(); + } + + return null; + } + + private AbstractOptionSpec handleArgumentAcceptingOption( String candidate ) { + index++; + + if ( hasMore() && specification.charAt( index ) == ':' ) { + index++; + return new OptionalArgumentOptionSpec( candidate ); + } + + return new RequiredArgumentOptionSpec( candidate ); + } +} diff --git a/src/joptsimple/OptionalArgumentOptionSpec.java b/src/joptsimple/OptionalArgumentOptionSpec.java new file mode 100755 index 0000000..d162700 --- /dev/null +++ b/src/joptsimple/OptionalArgumentOptionSpec.java @@ -0,0 +1,69 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +/** + * Specification of an option that accepts an optional argument. + * + * @param represents the type of the arguments this option accepts + * @author Paul Holser + */ +class OptionalArgumentOptionSpec extends ArgumentAcceptingOptionSpec { + OptionalArgumentOptionSpec( String option ) { + super( option, false ); + } + + OptionalArgumentOptionSpec( Collection options, String description ) { + super( options, false, description ); + } + + @Override + protected void detectOptionArgument( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions ) { + if ( arguments.hasMore() ) { + String nextArgument = arguments.peek(); + + if ( !parser.looksLikeAnOption( nextArgument ) ) + handleOptionArgument( parser, detectedOptions, arguments ); + else if ( isArgumentOfNumberType() && canConvertArgument( nextArgument ) ) + addArguments( detectedOptions, arguments.next() ); + else + detectedOptions.add( this ); + } + else + detectedOptions.add( this ); + } + + private void handleOptionArgument( OptionParser parser, OptionSet detectedOptions, ArgumentList arguments ) { + if ( parser.posixlyCorrect() ) { + detectedOptions.add( this ); + parser.noMoreOptions(); + } + else + addArguments( detectedOptions, arguments.next() ); + } +} diff --git a/src/joptsimple/ParserRules.java b/src/joptsimple/ParserRules.java new file mode 100755 index 0000000..0b8d1c3 --- /dev/null +++ b/src/joptsimple/ParserRules.java @@ -0,0 +1,84 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +import static java.lang.Character.*; + +/** + * Can tell whether or not options are well-formed. + * + * @author Paul Holser + */ +final class ParserRules { + static final char HYPHEN_CHAR = '-'; + static final String HYPHEN = String.valueOf( HYPHEN_CHAR ); + static final String DOUBLE_HYPHEN = "--"; + static final String OPTION_TERMINATOR = DOUBLE_HYPHEN; + static final String RESERVED_FOR_EXTENSIONS = "W"; + + private ParserRules() { + throw new UnsupportedOperationException(); + } + + static boolean isShortOptionToken( String argument ) { + return argument.startsWith( HYPHEN ) + && !HYPHEN.equals( argument ) + && !isLongOptionToken( argument ); + } + + static boolean isLongOptionToken( String argument ) { + return argument.startsWith( DOUBLE_HYPHEN ) && !isOptionTerminator( argument ); + } + + static boolean isOptionTerminator( String argument ) { + return OPTION_TERMINATOR.equals( argument ); + } + + static void ensureLegalOption( String option ) { + if ( option.startsWith( HYPHEN ) ) + throw new IllegalOptionSpecificationException( String.valueOf( option ) ); + + for ( int i = 0; i < option.length(); ++i ) + ensureLegalOptionCharacter( option.charAt( i ) ); + } + + static void ensureLegalOptions( Collection options ) { + for ( String each : options ) + ensureLegalOption( each ); + } + + private static void ensureLegalOptionCharacter( char option ) { + if ( !( isLetterOrDigit( option ) || isAllowedPunctuation( option ) ) ) + throw new IllegalOptionSpecificationException( String.valueOf( option ) ); + } + + private static boolean isAllowedPunctuation( char option ) { + String allowedPunctuation = "?." + HYPHEN_CHAR; + return allowedPunctuation.indexOf( option ) != -1; + } +} diff --git a/src/joptsimple/RequiredArgumentOptionSpec.java b/src/joptsimple/RequiredArgumentOptionSpec.java new file mode 100755 index 0000000..c78f020 --- /dev/null +++ b/src/joptsimple/RequiredArgumentOptionSpec.java @@ -0,0 +1,52 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +/** + * Specification of an option that accepts a required argument. + * + * @param represents the type of the arguments this option accepts + * @author Paul Holser + */ +class RequiredArgumentOptionSpec extends ArgumentAcceptingOptionSpec { + RequiredArgumentOptionSpec( String option ) { + super( option, true ); + } + + RequiredArgumentOptionSpec( Collection options, String description ) { + super( options, true, description ); + } + + @Override + protected void detectOptionArgument( OptionParser parser, ArgumentList arguments, OptionSet detectedOptions ) { + if ( !arguments.hasMore() ) + throw new OptionMissingRequiredArgumentException( options() ); + + addArguments( detectedOptions, arguments.next() ); + } +} diff --git a/src/joptsimple/UnacceptableNumberOfNonOptionsException.java b/src/joptsimple/UnacceptableNumberOfNonOptionsException.java new file mode 100755 index 0000000..164f4fb --- /dev/null +++ b/src/joptsimple/UnacceptableNumberOfNonOptionsException.java @@ -0,0 +1,54 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import static java.util.Collections.*; + +/** + * Thrown when the option parser detects an unacceptable number of {@code linkplain NonOptionArgumentSpec + * non-option arguments}. + * + * @author Paul Holser + */ +class UnacceptableNumberOfNonOptionsException extends OptionException { + private static final long serialVersionUID = -1L; + private final int minimum; + private final int maximum; + private final int actual; + + UnacceptableNumberOfNonOptionsException( int minimum, int maximum, int actual ) { + super( singletonList( NonOptionArgumentSpec.NAME ) ); + + this.minimum = minimum; + this.maximum = maximum; + this.actual = actual; + } + + @Override + public String getMessage() { + return String.format( "actual = %d, minimum = %d, maximum = %d", actual, minimum, maximum ); + } +} diff --git a/src/joptsimple/UnconfiguredOptionException.java b/src/joptsimple/UnconfiguredOptionException.java new file mode 100755 index 0000000..40d9705 --- /dev/null +++ b/src/joptsimple/UnconfiguredOptionException.java @@ -0,0 +1,52 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import java.util.Collection; + +import static java.util.Collections.*; + +/** + * Thrown when an option parser refers to an option that is not in fact configured already on the parser. + * + * @author Paul Holser + */ +class UnconfiguredOptionException extends OptionException { + private static final long serialVersionUID = -1L; + + UnconfiguredOptionException( String option ) { + this( singletonList( option ) ); + } + + UnconfiguredOptionException( Collection options ) { + super( options ); + } + + @Override + public String getMessage() { + return "Option " + multipleOptionMessage() + " has not been configured on this parser"; + } +} diff --git a/src/joptsimple/UnrecognizedOptionException.java b/src/joptsimple/UnrecognizedOptionException.java new file mode 100755 index 0000000..529bdb4 --- /dev/null +++ b/src/joptsimple/UnrecognizedOptionException.java @@ -0,0 +1,46 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +import static java.util.Collections.*; + +/** + * Thrown when the option parser encounters an unrecognized option. + * + * @author Paul Holser + */ +class UnrecognizedOptionException extends OptionException { + private static final long serialVersionUID = -1L; + + UnrecognizedOptionException( String option ) { + super( singletonList( option ) ); + } + + @Override + public String getMessage() { + return singleOptionMessage() + " is not a recognized option"; + } +} diff --git a/src/joptsimple/ValueConversionException.java b/src/joptsimple/ValueConversionException.java new file mode 100755 index 0000000..52be2cd --- /dev/null +++ b/src/joptsimple/ValueConversionException.java @@ -0,0 +1,54 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +/** + * Thrown by {@link ValueConverter}s when problems occur in converting string values to other Java types. + * + * @author Paul Holser + */ +public class ValueConversionException extends RuntimeException { + private static final long serialVersionUID = -1L; + + /** + * Creates a new exception with the specified detail message. + * + * @param message the detail message + */ + public ValueConversionException( String message ) { + this( message, null ); + } + + /** + * Creates a new exception with the specified detail message and cause. + * + * @param message the detail message + * @param cause the original exception + */ + public ValueConversionException( String message, Throwable cause ) { + super( message, cause ); + } +} diff --git a/src/joptsimple/ValueConverter.java b/src/joptsimple/ValueConverter.java new file mode 100755 index 0000000..09ca929 --- /dev/null +++ b/src/joptsimple/ValueConverter.java @@ -0,0 +1,58 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple; + +/** + * Instances of this interface are used to convert arguments of options into specific Java types. + * + * @param constraint on the type of values being converted to + * @author Paul Holser + */ +public interface ValueConverter { + /** + * Converts the given string value into a Java type. + * + * @param value the string to convert + * @return the converted value + * @throws ValueConversionException if a problem occurs while converting the value + */ + V convert( String value ); + + /** + * Gives the class of the type of values this converter converts to. + * + * @return the target class for conversion + */ + Class valueType(); + + /** + * Gives a string that describes the pattern of the values this converter expects, if any. For example, a date + * converter can respond with a {@link java.text.SimpleDateFormat date format string}. + * + * @return a value pattern, or {@code null} if there's nothing interesting here + */ + String valuePattern(); +} diff --git a/src/joptsimple/internal/AbbreviationMap.java b/src/joptsimple/internal/AbbreviationMap.java new file mode 100755 index 0000000..6b5dd60 --- /dev/null +++ b/src/joptsimple/internal/AbbreviationMap.java @@ -0,0 +1,234 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.util.Map; +import java.util.TreeMap; + +/** + *

A map whose keys are strings; when a key/value pair is added to the map, the longest unique abbreviations of that + * key are added as well, and associated with the value. Thus:

+ * + *
+ *   
+ *   abbreviations.put( "good", "bye" );
+ *   
+ * 
+ * + *

would make it such that you could retrieve the value {@code "bye"} from the map using the keys {@code "good"}, + * {@code "goo"}, {@code "go"}, and {@code "g"}. A subsequent invocation of:

+ *
+ *   
+ *   abbreviations.put( "go", "fish" );
+ *   
+ * 
+ * + *

would make it such that you could retrieve the value {@code "bye"} using the keys {@code "good"} and + * {@code "goo"}, and the value {@code "fish"} using the key {@code "go"}. The key {@code "g"} would yield + * {@code null}, since it would no longer be a unique abbreviation.

+ * + *

The data structure is much like a "trie".

+ * + * @param a constraint on the types of the values in the map + * @author Paul Holser + * @see Perl's Text::Abbrev module + */ +public class AbbreviationMap { + private String key; + private V value; + private final Map> children = new TreeMap>(); + private int keysBeyond; + + /** + *

Tells whether the given key is in the map, or whether the given key is a unique + * abbreviation of a key that is in the map.

+ * + * @param aKey key to look up + * @return {@code true} if {@code key} is present in the map + * @throws NullPointerException if {@code key} is {@code null} + */ + public boolean contains( String aKey ) { + return get( aKey ) != null; + } + + /** + *

Answers the value associated with the given key. The key can be a unique + * abbreviation of a key that is in the map.

+ * + * @param aKey key to look up + * @return the value associated with {@code aKey}; or {@code null} if there is no + * such value or {@code aKey} is not a unique abbreviation of a key in the map + * @throws NullPointerException if {@code aKey} is {@code null} + */ + public V get( String aKey ) { + char[] chars = charsOf( aKey ); + + AbbreviationMap child = this; + for ( char each : chars ) { + child = child.children.get( each ); + if ( child == null ) + return null; + } + + return child.value; + } + + /** + *

Associates a given value with a given key. If there was a previous + * association, the old value is replaced with the new one.

+ * + * @param aKey key to create in the map + * @param newValue value to associate with the key + * @throws NullPointerException if {@code aKey} or {@code newValue} is {@code null} + * @throws IllegalArgumentException if {@code aKey} is a zero-length string + */ + public void put( String aKey, V newValue ) { + if ( newValue == null ) + throw new NullPointerException(); + if ( aKey.length() == 0 ) + throw new IllegalArgumentException(); + + char[] chars = charsOf( aKey ); + add( chars, newValue, 0, chars.length ); + } + + /** + *

Associates a given value with a given set of keys. If there was a previous + * association, the old value is replaced with the new one.

+ * + * @param keys keys to create in the map + * @param newValue value to associate with the key + * @throws NullPointerException if {@code keys} or {@code newValue} is {@code null} + * @throws IllegalArgumentException if any of {@code keys} is a zero-length string + */ + public void putAll( Iterable keys, V newValue ) { + for ( String each : keys ) + put( each, newValue ); + } + + private boolean add( char[] chars, V newValue, int offset, int length ) { + if ( offset == length ) { + value = newValue; + boolean wasAlreadyAKey = key != null; + key = new String( chars ); + return !wasAlreadyAKey; + } + + char nextChar = chars[ offset ]; + AbbreviationMap child = children.get( nextChar ); + if ( child == null ) { + child = new AbbreviationMap(); + children.put( nextChar, child ); + } + + boolean newKeyAdded = child.add( chars, newValue, offset + 1, length ); + + if ( newKeyAdded ) + ++keysBeyond; + + if ( key == null ) + value = keysBeyond > 1 ? null : newValue; + + return newKeyAdded; + } + + /** + *

If the map contains the given key, dissociates the key from its value.

+ * + * @param aKey key to remove + * @throws NullPointerException if {@code aKey} is {@code null} + * @throws IllegalArgumentException if {@code aKey} is a zero-length string + */ + public void remove( String aKey ) { + if ( aKey.length() == 0 ) + throw new IllegalArgumentException(); + + char[] keyChars = charsOf( aKey ); + remove( keyChars, 0, keyChars.length ); + } + + private boolean remove( char[] aKey, int offset, int length ) { + if ( offset == length ) + return removeAtEndOfKey(); + + char nextChar = aKey[ offset ]; + AbbreviationMap child = children.get( nextChar ); + if ( child == null || !child.remove( aKey, offset + 1, length ) ) + return false; + + --keysBeyond; + if ( child.keysBeyond == 0 ) + children.remove( nextChar ); + if ( keysBeyond == 1 && key == null ) + setValueToThatOfOnlyChild(); + + return true; + } + + private void setValueToThatOfOnlyChild() { + Map.Entry> entry = children.entrySet().iterator().next(); + AbbreviationMap onlyChild = entry.getValue(); + value = onlyChild.value; + } + + private boolean removeAtEndOfKey() { + if ( key == null ) + return false; + + key = null; + if ( keysBeyond == 1 ) + setValueToThatOfOnlyChild(); + else + value = null; + + return true; + } + + /** + * Gives a Java map representation of this abbreviation map. + * + * @return a Java map corresponding to this abbreviation map + */ + public Map toJavaUtilMap() { + Map mappings = new TreeMap(); + addToMappings( mappings ); + return mappings; + } + + private void addToMappings( Map mappings ) { + if ( key != null ) + mappings.put( key, value ); + + for ( AbbreviationMap each : children.values() ) + each.addToMappings( mappings ); + } + + private static char[] charsOf( String aKey ) { + char[] chars = new char[ aKey.length() ]; + aKey.getChars( 0, aKey.length(), chars, 0 ); + return chars; + } +} diff --git a/src/joptsimple/internal/Classes.java b/src/joptsimple/internal/Classes.java new file mode 100755 index 0000000..544d096 --- /dev/null +++ b/src/joptsimple/internal/Classes.java @@ -0,0 +1,75 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Paul Holser + */ +public final class Classes { + private static final Map, Class> WRAPPERS = new HashMap, Class>( 13 ); + + static { + WRAPPERS.put( boolean.class, Boolean.class ); + WRAPPERS.put( byte.class, Byte.class ); + WRAPPERS.put( char.class, Character.class ); + WRAPPERS.put( double.class, Double.class ); + WRAPPERS.put( float.class, Float.class ); + WRAPPERS.put( int.class, Integer.class ); + WRAPPERS.put( long.class, Long.class ); + WRAPPERS.put( short.class, Short.class ); + WRAPPERS.put( void.class, Void.class ); + } + + private Classes() { + throw new UnsupportedOperationException(); + } + + /** + * Gives the "short version" of the given class name. Somewhat naive to inner classes. + * + * @param className class name to chew on + * @return the short name of the class + */ + public static String shortNameOf( String className ) { + return className.substring( className.lastIndexOf( '.' ) + 1 ); + } + + /** + * Gives the primitive wrapper class for the given class. If the given class is not + * {@linkplain Class#isPrimitive() primitive}, returns the class itself. + * + * @param generic class type + * @param clazz the class to check + * @return primitive wrapper type if {@code clazz} is primitive, otherwise {@code clazz} + */ + @SuppressWarnings( "unchecked" ) + public static Class wrapperOf( Class clazz ) { + return clazz.isPrimitive() ? (Class) WRAPPERS.get( clazz ) : clazz; + } +} diff --git a/src/joptsimple/internal/Columns.java b/src/joptsimple/internal/Columns.java new file mode 100755 index 0000000..15bc9d8 --- /dev/null +++ b/src/joptsimple/internal/Columns.java @@ -0,0 +1,107 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static java.text.BreakIterator.*; + +import static joptsimple.internal.Strings.*; + +/** + * @author Paul Holser + */ +class Columns { + private static final int INDENT_WIDTH = 2; + + private final int optionWidth; + private final int descriptionWidth; + + Columns( int optionWidth, int descriptionWidth ) { + this.optionWidth = optionWidth; + this.descriptionWidth = descriptionWidth; + } + + List fit( Row row ) { + List options = piecesOf( row.option, optionWidth ); + List descriptions = piecesOf( row.description, descriptionWidth ); + + List rows = new ArrayList(); + for ( int i = 0; i < Math.max( options.size(), descriptions.size() ); ++i ) + rows.add( new Row( itemOrEmpty( options, i ), itemOrEmpty( descriptions, i ) ) ); + + return rows; + } + + private static String itemOrEmpty( List items, int index ) { + return index >= items.size() ? "" : items.get( index ); + } + + private List piecesOf( String raw, int width ) { + List pieces = new ArrayList(); + + for ( String each : raw.trim().split( LINE_SEPARATOR ) ) + pieces.addAll( piecesOfEmbeddedLine( each, width ) ); + + return pieces; + } + + private List piecesOfEmbeddedLine( String line, int width ) { + List pieces = new ArrayList(); + + BreakIterator words = BreakIterator.getLineInstance( Locale.US ); + words.setText( line ); + + StringBuilder nextPiece = new StringBuilder(); + + int start = words.first(); + for ( int end = words.next(); end != DONE; start = end, end = words.next() ) + nextPiece = processNextWord( line, nextPiece, start, end, width, pieces ); + + if ( nextPiece.length() > 0 ) + pieces.add( nextPiece.toString() ); + + return pieces; + } + + private StringBuilder processNextWord( String source, StringBuilder nextPiece, int start, int end, int width, + List pieces ) { + StringBuilder augmented = nextPiece; + + String word = source.substring( start, end ); + if ( augmented.length() + word.length() > width ) { + pieces.add( augmented.toString().replaceAll( "\\s+$", "" ) ); + augmented = new StringBuilder( repeat( ' ', INDENT_WIDTH ) ).append( word ); + } + else + augmented.append( word ); + + return augmented; + } +} diff --git a/src/joptsimple/internal/ConstructorInvokingValueConverter.java b/src/joptsimple/internal/ConstructorInvokingValueConverter.java new file mode 100755 index 0000000..7fe884f --- /dev/null +++ b/src/joptsimple/internal/ConstructorInvokingValueConverter.java @@ -0,0 +1,56 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.lang.reflect.Constructor; + +import joptsimple.ValueConverter; + +import static joptsimple.internal.Reflection.*; + +/** + * @param constraint on the type of values being converted to + * @author Paul Holser + */ +class ConstructorInvokingValueConverter implements ValueConverter { + private final Constructor ctor; + + ConstructorInvokingValueConverter( Constructor ctor ) { + this.ctor = ctor; + } + + public V convert( String value ) { + return instantiate( ctor, value ); + } + + public Class valueType() { + return ctor.getDeclaringClass(); + } + + public String valuePattern() { + return null; + } +} diff --git a/src/joptsimple/internal/MethodInvokingValueConverter.java b/src/joptsimple/internal/MethodInvokingValueConverter.java new file mode 100755 index 0000000..548efec --- /dev/null +++ b/src/joptsimple/internal/MethodInvokingValueConverter.java @@ -0,0 +1,58 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.lang.reflect.Method; + +import joptsimple.ValueConverter; + +import static joptsimple.internal.Reflection.*; + +/** + * @param constraint on the type of values being converted to + * @author Paul Holser + */ +class MethodInvokingValueConverter implements ValueConverter { + private final Method method; + private final Class clazz; + + MethodInvokingValueConverter( Method method, Class clazz ) { + this.method = method; + this.clazz = clazz; + } + + public V convert( String value ) { + return clazz.cast( invoke( method, value ) ); + } + + public Class valueType() { + return clazz; + } + + public String valuePattern() { + return null; + } +} diff --git a/src/joptsimple/internal/Objects.java b/src/joptsimple/internal/Objects.java new file mode 100755 index 0000000..db0eeb3 --- /dev/null +++ b/src/joptsimple/internal/Objects.java @@ -0,0 +1,46 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +/** + * @author Paul Holser + */ +public final class Objects { + private Objects() { + throw new UnsupportedOperationException(); + } + + /** + * Rejects {@code null} references. + * + * @param target reference to check + * @throws NullPointerException if {@code target} is {@code null} + */ + public static void ensureNotNull( Object target ) { + if ( target == null ) + throw new NullPointerException(); + } +} diff --git a/src/joptsimple/internal/Reflection.java b/src/joptsimple/internal/Reflection.java new file mode 100755 index 0000000..6224bb5 --- /dev/null +++ b/src/joptsimple/internal/Reflection.java @@ -0,0 +1,146 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static java.lang.reflect.Modifier.*; + +import joptsimple.ValueConverter; + +import static joptsimple.internal.Classes.*; + +/** + * Helper methods for reflection. + * + * @author Paul Holser + */ +public final class Reflection { + private Reflection() { + throw new UnsupportedOperationException(); + } + + /** + * Finds an appropriate value converter for the given class. + * + * @param a constraint on the class object to introspect + * @param clazz class to introspect on + * @return a converter method or constructor + */ + public static ValueConverter findConverter( Class clazz ) { + Class maybeWrapper = wrapperOf( clazz ); + + ValueConverter valueOf = valueOfConverter( maybeWrapper ); + if ( valueOf != null ) + return valueOf; + + ValueConverter constructor = constructorConverter( maybeWrapper ); + if ( constructor != null ) + return constructor; + + throw new IllegalArgumentException( clazz + " is not a value type" ); + } + + private static ValueConverter valueOfConverter( Class clazz ) { + try { + Method valueOf = clazz.getDeclaredMethod( "valueOf", String.class ); + if ( meetsConverterRequirements( valueOf, clazz ) ) + return new MethodInvokingValueConverter( valueOf, clazz ); + + return null; + } + catch ( NoSuchMethodException ignored ) { + return null; + } + } + + private static ValueConverter constructorConverter( Class clazz ) { + try { + return new ConstructorInvokingValueConverter( clazz.getConstructor( String.class ) ); + } + catch ( NoSuchMethodException ignored ) { + return null; + } + } + + /** + * Invokes the given constructor with the given arguments. + * + * @param constraint on the type of the objects yielded by the constructor + * @param constructor constructor to invoke + * @param args arguments to hand to the constructor + * @return the result of invoking the constructor + * @throws ReflectionException in lieu of the gaggle of reflection-related exceptions + */ + public static T instantiate( Constructor constructor, Object... args ) { + try { + return constructor.newInstance( args ); + } + catch ( Exception ex ) { + throw reflectionException( ex ); + } + } + + /** + * Invokes the given static method with the given arguments. + * + * @param method method to invoke + * @param args arguments to hand to the method + * @return the result of invoking the method + * @throws ReflectionException in lieu of the gaggle of reflection-related exceptions + */ + public static Object invoke( Method method, Object... args ) { + try { + return method.invoke( null, args ); + } + catch ( Exception ex ) { + throw reflectionException( ex ); + } + } + + @SuppressWarnings( "unchecked" ) + public static V convertWith( ValueConverter converter, String raw ) { + return converter == null ? (V) raw : converter.convert( raw ); + } + + private static boolean meetsConverterRequirements( Method method, Class expectedReturnType ) { + int modifiers = method.getModifiers(); + return isPublic( modifiers ) && isStatic( modifiers ) && expectedReturnType.equals( method.getReturnType() ); + } + + private static RuntimeException reflectionException( Exception ex ) { + if ( ex instanceof IllegalArgumentException ) + return new ReflectionException( ex ); + if ( ex instanceof InvocationTargetException ) + return new ReflectionException( ex.getCause() ); + if ( ex instanceof RuntimeException ) + return (RuntimeException) ex; + + return new ReflectionException( ex ); + } +} diff --git a/src/joptsimple/internal/ReflectionException.java b/src/joptsimple/internal/ReflectionException.java new file mode 100755 index 0000000..0661ddf --- /dev/null +++ b/src/joptsimple/internal/ReflectionException.java @@ -0,0 +1,39 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +/** + * This unchecked exception wraps reflection-oriented exceptions. + * + * @author Paul Holser + */ +public class ReflectionException extends RuntimeException { + private static final long serialVersionUID = -2L; + + ReflectionException( Throwable cause ) { + super( cause ); + } +} diff --git a/src/joptsimple/internal/Row.java b/src/joptsimple/internal/Row.java new file mode 100755 index 0000000..7c6f6af --- /dev/null +++ b/src/joptsimple/internal/Row.java @@ -0,0 +1,55 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +/** + * @author Paul Holser + */ +class Row { + final String option; + final String description; + + Row( String option, String description ) { + this.option = option; + this.description = description; + } + + @Override + public boolean equals( Object that ) { + if ( that == this ) + return true; + if ( that == null || !getClass().equals( that.getClass() ) ) + return false; + + Row other = (Row) that; + return option.equals( other.option ) && description.equals( other.description ); + } + + @Override + public int hashCode() { + return option.hashCode() ^ description.hashCode(); + } +} diff --git a/src/joptsimple/internal/Rows.java b/src/joptsimple/internal/Rows.java new file mode 100755 index 0000000..63bf9d3 --- /dev/null +++ b/src/joptsimple/internal/Rows.java @@ -0,0 +1,102 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static java.lang.Math.*; + +import static joptsimple.internal.Strings.*; + +/** + * @author Paul Holser + */ +public class Rows { + private final int overallWidth; + private final int columnSeparatorWidth; + private final Set rows = new LinkedHashSet(); + private int widthOfWidestOption; + private int widthOfWidestDescription; + + public Rows( int overallWidth, int columnSeparatorWidth ) { + this.overallWidth = overallWidth; + this.columnSeparatorWidth = columnSeparatorWidth; + } + + public void add( String option, String description ) { + add( new Row( option, description ) ); + } + + private void add( Row row ) { + rows.add( row ); + widthOfWidestOption = max( widthOfWidestOption, row.option.length() ); + widthOfWidestDescription = max( widthOfWidestDescription, row.description.length() ); + } + + private void reset() { + rows.clear(); + widthOfWidestOption = 0; + widthOfWidestDescription = 0; + } + + public void fitToWidth() { + Columns columns = new Columns( optionWidth(), descriptionWidth() ); + + Set fitted = new LinkedHashSet(); + for ( Row each : rows ) + fitted.addAll( columns.fit( each ) ); + + reset(); + + for ( Row each : fitted ) + add( each ); + } + + public String render() { + StringBuilder buffer = new StringBuilder(); + + for ( Row each : rows ) { + pad( buffer, each.option, optionWidth() ).append( repeat( ' ', columnSeparatorWidth ) ); + pad( buffer, each.description, descriptionWidth() ).append( LINE_SEPARATOR ); + } + + return buffer.toString(); + } + + private int optionWidth() { + return min( ( overallWidth - columnSeparatorWidth ) / 2, widthOfWidestOption ); + } + + private int descriptionWidth() { + return min( ( overallWidth - columnSeparatorWidth ) / 2, widthOfWidestDescription ); + } + + private StringBuilder pad( StringBuilder buffer, String s, int length ) { + buffer.append( s ).append( repeat( ' ', length - s.length() ) ); + return buffer; + } +} diff --git a/src/joptsimple/internal/Strings.java b/src/joptsimple/internal/Strings.java new file mode 100755 index 0000000..a00bfe1 --- /dev/null +++ b/src/joptsimple/internal/Strings.java @@ -0,0 +1,117 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.internal; + +import java.util.Iterator; +import java.util.List; + +import static java.lang.System.*; +import static java.util.Arrays.*; + +/** + * @author Paul Holser + */ +public final class Strings { + public static final String EMPTY = ""; + public static final String SINGLE_QUOTE = "'"; + public static final String LINE_SEPARATOR = getProperty( "line.separator" ); + + private Strings() { + throw new UnsupportedOperationException(); + } + + /** + * Gives a string consisting of the given character repeated the given number of times. + * + * @param ch the character to repeat + * @param count how many times to repeat the character + * @return the resultant string + */ + public static String repeat( char ch, int count ) { + StringBuilder buffer = new StringBuilder(); + + for ( int i = 0; i < count; ++i ) + buffer.append( ch ); + + return buffer.toString(); + } + + /** + * Tells whether the given string is either {@code} or consists solely of whitespace characters. + * + * @param target string to check + * @return {@code true} if the target string is null or empty + */ + public static boolean isNullOrEmpty( String target ) { + return target == null || EMPTY.equals( target ); + } + + + /** + * Gives a string consisting of a given string prepended and appended with surrounding characters. + * + * @param target a string + * @param begin character to prepend + * @param end character to append + * @return the surrounded string + */ + public static String surround( String target, char begin, char end ) { + return begin + target + end; + } + + /** + * Gives a string consisting of the elements of a given array of strings, each separated by a given separator + * string. + * + * @param pieces the strings to join + * @param separator the separator + * @return the joined string + */ + public static String join( String[] pieces, String separator ) { + return join( asList( pieces ), separator ); + } + + /** + * Gives a string consisting of the string representations of the elements of a given array of objects, + * each separated by a given separator string. + * + * @param pieces the elements whose string representations are to be joined + * @param separator the separator + * @return the joined string + */ + public static String join( List pieces, String separator ) { + StringBuilder buffer = new StringBuilder(); + + for ( Iterator iter = pieces.iterator(); iter.hasNext(); ) { + buffer.append( iter.next() ); + + if ( iter.hasNext() ) + buffer.append( separator ); + } + + return buffer.toString(); + } +} diff --git a/src/joptsimple/util/DateConverter.java b/src/joptsimple/util/DateConverter.java new file mode 100755 index 0000000..afdf4bd --- /dev/null +++ b/src/joptsimple/util/DateConverter.java @@ -0,0 +1,100 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.util; + +import java.text.DateFormat; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Date; + +import joptsimple.ValueConversionException; +import joptsimple.ValueConverter; + +/** + * Converts values to {@link Date}s using a {@link DateFormat} object. + * + * @author Paul Holser + */ +public class DateConverter implements ValueConverter { + private final DateFormat formatter; + + /** + * Creates a converter that uses the given date formatter/parser. + * + * @param formatter the formatter/parser to use + * @throws NullPointerException if {@code formatter} is {@code null} + */ + public DateConverter( DateFormat formatter ) { + if ( formatter == null ) + throw new NullPointerException( "illegal null formatter" ); + + this.formatter = formatter; + } + + /** + * Creates a converter that uses a {@link SimpleDateFormat} with the given date/time pattern. The date formatter + * created is not {@link SimpleDateFormat#setLenient(boolean) lenient}. + * + * @param pattern expected date/time pattern + * @return the new converter + * @throws NullPointerException if {@code pattern} is {@code null} + * @throws IllegalArgumentException if {@code pattern} is invalid + */ + public static DateConverter datePattern( String pattern ) { + SimpleDateFormat formatter = new SimpleDateFormat( pattern ); + formatter.setLenient( false ); + + return new DateConverter( formatter ); + } + + public Date convert( String value ) { + ParsePosition position = new ParsePosition( 0 ); + + Date date = formatter.parse( value, position ); + if ( position.getIndex() != value.length() ) + throw new ValueConversionException( message( value ) ); + + return date; + } + + public Class valueType() { + return Date.class; + } + + public String valuePattern() { + return formatter instanceof SimpleDateFormat + ? ( (SimpleDateFormat) formatter ).toPattern() + : ""; + } + + private String message( String value ) { + String message = "Value [" + value + "] does not match date/time pattern"; + if ( formatter instanceof SimpleDateFormat ) + message += " [" + ( (SimpleDateFormat) formatter ).toPattern() + ']'; + + return message; + } +} diff --git a/src/joptsimple/util/InetAddressConverter.java b/src/joptsimple/util/InetAddressConverter.java new file mode 100755 index 0000000..f294508 --- /dev/null +++ b/src/joptsimple/util/InetAddressConverter.java @@ -0,0 +1,55 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import joptsimple.ValueConversionException; +import joptsimple.ValueConverter; + +/** + * Converts values to {@link java.net.InetAddress} using {@link InetAddress#getByName(String) getByName}. + * + * @author Raymund F\u00FCl\u00F6p + */ +public class InetAddressConverter implements ValueConverter { + public InetAddress convert( String value ) { + try { + return InetAddress.getByName( value ); + } catch ( UnknownHostException e ) { + throw new ValueConversionException( "Cannot convert value [" + value + " into an InetAddress", e ); + } + } + + public Class valueType() { + return InetAddress.class; + } + + public String valuePattern() { + return null; + } +} diff --git a/src/joptsimple/util/KeyValuePair.java b/src/joptsimple/util/KeyValuePair.java new file mode 100755 index 0000000..10f87a3 --- /dev/null +++ b/src/joptsimple/util/KeyValuePair.java @@ -0,0 +1,83 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.util; + +import static joptsimple.internal.Strings.*; + +/** + *

A simple string key/string value pair.

+ * + *

This is useful as an argument type for options whose values take on the form key=value, such as JVM + * command line system properties.

+ * + * @author Paul Holser + */ +public final class KeyValuePair { + public final String key; + public final String value; + + private KeyValuePair( String key, String value ) { + this.key = key; + this.value = value; + } + + /** + * Parses a string assumed to be of the form key=value into its parts. + * + * @param asString key-value string + * @return a key-value pair + * @throws NullPointerException if {@code stringRepresentation} is {@code null} + */ + public static KeyValuePair valueOf( String asString ) { + int equalsIndex = asString.indexOf( '=' ); + if ( equalsIndex == -1 ) + return new KeyValuePair( asString, EMPTY ); + + String aKey = asString.substring( 0, equalsIndex ); + String aValue = equalsIndex == asString.length() - 1 ? EMPTY : asString.substring( equalsIndex + 1 ); + + return new KeyValuePair( aKey, aValue ); + } + + @Override + public boolean equals( Object that ) { + if ( !( that instanceof KeyValuePair ) ) + return false; + + KeyValuePair other = (KeyValuePair) that; + return key.equals( other.key ) && value.equals( other.value ); + } + + @Override + public int hashCode() { + return key.hashCode() ^ value.hashCode(); + } + + @Override + public String toString() { + return key + '=' + value; + } +} diff --git a/src/joptsimple/util/RegexMatcher.java b/src/joptsimple/util/RegexMatcher.java new file mode 100755 index 0000000..0ddd1a6 --- /dev/null +++ b/src/joptsimple/util/RegexMatcher.java @@ -0,0 +1,83 @@ +/* + The MIT License + + Copyright (c) 2004-2014 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package joptsimple.util; + +import java.util.regex.Pattern; + +import static java.util.regex.Pattern.*; + +import joptsimple.ValueConversionException; +import joptsimple.ValueConverter; + +/** + * Ensures that values entirely match a regular expression. + * + * @author Paul Holser + */ +public class RegexMatcher implements ValueConverter { + private final Pattern pattern; + + /** + * Creates a matcher that uses the given regular expression, modified by the given flags. + * + * @param pattern the regular expression pattern + * @param flags modifying regex flags + * @throws IllegalArgumentException if bit values other than those corresponding to the defined match flags are + * set in {@code flags} + * @throws java.util.regex.PatternSyntaxException if the expression's syntax is invalid + */ + public RegexMatcher( String pattern, int flags ) { + this.pattern = compile( pattern, flags ); + } + + /** + * Gives a matcher that uses the given regular expression. + * + * @param pattern the regular expression pattern + * @return the new converter + * @throws java.util.regex.PatternSyntaxException if the expression's syntax is invalid + */ + public static ValueConverter regex( String pattern ) { + return new RegexMatcher( pattern, 0 ); + } + + public String convert( String value ) { + if ( !pattern.matcher( value ).matches() ) { + throw new ValueConversionException( + "Value [" + value + "] did not match regex [" + pattern.pattern() + ']' ); + } + + return value; + } + + public Class valueType() { + return String.class; + } + + public String valuePattern() { + return pattern.pattern(); + } +} diff --git a/src/lunasql/Config.java b/src/lunasql/Config.java new file mode 100644 index 0000000..120d500 --- /dev/null +++ b/src/lunasql/Config.java @@ -0,0 +1,72 @@ +/* + * Config.java + * Configuration et constantes publiques + * @author M.P. + */ +package lunasql; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +import lunasql.lib.Contexte; +import lunasql.sql.TypesSGBD; + +public class Config { + + /* À propos */ + public static final String APP_VERSION_NAME = "Quincella"; + public static final String APP_VERSION_NUM = "4.9.3.0"; // modèle: n.n.n.n + public static final String APP_DT_REVISION = "28 octobre 2022"; + public static final String APP_AUTHOR_NAME = "Micaël Paganotto"; + public static final String APP_AUTHOR_EMAIL = "keybase.io/espritlibredev"; + public static final String APP_AUTHOR_PGP = "aa77 7903 6281 d0e9 209b e8b9 2627 39eb a36c eb3e"; + + /* Comportement général */ + public static final int CF_MAX_CALL_DEEP = 200; // profondeur maximale pour ALIAS et EXEC + + /* Contexte d'exécution */ + public static final int CT_VERBOSE = Contexte.VERB_MSG; + public static final boolean CT_EXIT_ON_ERROR = true; // sortie sur erreur + public static final int CT_CONST_EDIT = 2; // constantes ':' éditables en console + public static final int CT_SQL_UPDATE = 1; // exécution de code SQL de mise-à-jour + public static final String CT_EVAL_ENGINE = "js"; // moteur d'évaluation par défaut + + /* Contexte d'exécution : répertoires de scripts par défaut (séparateur : ou ; selon plateforme) */ + public static final String CT_SCRIPTS_PATH = "scripts" + File.pathSeparator + "."; + public static final String CT_PLUGINS_PATH = "plugins" + File.pathSeparator + "."; + public static final String CT_SQL_EXT = "(?i)sql|lsql|luna"; // regexp des extensions des scripts LunaSQL + public static final String CT_INIT_FILE = "init.sql"; // fichier LunaSQL d'initialisation + public static final String CT_BASES_FILE = "login-list.cfg"; // fichier INI de définition des bases + public static final String CT_BASES_PATH = // chemin + System.getProperty("user.home") + File.separator + CT_BASES_FILE; + public static final String CT_CONFIG_FILE = "config.cfg"; // fichier de configuration + public static final boolean CT_SAVE_CONF = false; // si on sauve la config en quittant + public static final boolean CT_ALIAS_ARG = false; // Alias prennent args de la ligne + public static final boolean CT_INTERACT = false; // Opération sur les fichiers interactive + public static final boolean CT_ALLOW_RECUR = false; // Autorisation des alias récursifs /!\ + public static final boolean CT_ALLOW_REDEF = false; // Autorisation de redéfinition des commandes /!\ + public static final boolean CT_END_CMD_NL = true; // fin des commandes : nouvelle ligne + public static final int CT_COL_MAX_WIDTH = 100; // largeur max. des colonnes + public static final boolean CT_SELECT_ARR = true; // affichage tabulaire des SELECT + public static final boolean CT_ADD_ROW_NB = true; // ajout des numéros de ligne en tableau + public static final String CT_LOG_DIR = "."; // répertoire des logs d'erreur + public static final String CT_LOG_FILE = "crashes-" + + new SimpleDateFormat("yyyy-MM").format(new Date()) + ".log"; // Fichier des erreurs + public static final boolean CT_LIST_SUBST = false; // Support des substitutions par listes par § + public static final int CT_SIGN_POLICY = 0; // Politique de signature (0: rien, 1: signé, 2: confiance) + + /* Console lunasql */ + public static final int CS_HISTORY_SIZE = 1000; // Taille de l'historique + public static final String CS_HISTORY_FILE = "history"; // fichier d'historique de la console + public static final String CS_HISTORY_DBL = "2"; // gestion de l'historique + public static final String CS_PROMPT = "SQL"; // Prompt par défaut + public static final boolean CS_BEEP_ON = false; // Cloche (bip) qui ennerve sur erreur + public static final boolean CS_COLOR_ON = true; // Couleurs dans la console + + /* Serveur HTTP */ + public static final int SR_HTTP_PORT = 5862; // Port d'écoute par défaut pour serveur HTTP (LUNA) + + /* Base de données */ + public static final int DB_DEFAUT_TYPE = TypesSGBD.TYPE_H2DB; +} diff --git a/src/lunasql/Console.java b/src/lunasql/Console.java new file mode 100644 index 0000000..6f34ffe --- /dev/null +++ b/src/lunasql/Console.java @@ -0,0 +1,373 @@ +/* + * Console SQL + * @author M.P. + */ +package lunasql; + +import java.io.File; +import java.io.IOException; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import jline.ArgumentCompletor; +import jline.Completor; +import jline.ConsoleReader; +import jline.FileNameCompletor; +import jline.History; +import jline.MultiCompletor; +import jline.SimpleCompletor; +import lunasql.cmd.Instruction; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +//TODO : passer donc en Jline 3 https://github.com/jline/jline3 + +public class Console { + + private Contexte cont; + private ConsoleReader reader; + + /** + * Constructeur Console + * Pas d'affichage de la bannière. La faire en amont. + * + * @param cont le contexte d'exécution (connexion à la base et environnement) + */ + public Console(Contexte cont) { + // Contexte + this.cont = cont; + if (cont == null || !cont.hasSQLCnx()) + throw new IllegalArgumentException("Constructeur Console : contexte ou connexion SQL null"); + + compute(); + } + + /** + * Constructeur Console + * + * @param cont le contexte d'exécution (connexion à la base et environnement) + * @param path le chemin de connexion + * @param driver le driver de BDD + * @param login le nom d'utilisateur + * @param mdp le mot de passe de connexion + * @param banner si on affiche la bannière d'accueil + * @throws java.sql.SQLException si erreur SQL + */ + public Console(Contexte cont, String path, String driver, String login, String mdp, boolean banner) + throws SQLException { + createConsole(false, cont, 0, path, driver, null, login, mdp, null, null, 0, banner); + } + + /** + * Constructeur Console + * + * @param cont le contexte d'exécution (connexion à la base et environnement) + * @param type le type de BDD + * @param base le nom de la base + * @param login le nom d'utilisateur + * @param mdp le mot de passe de connexion + * @param opts les options des connexions (ex : crypt) + * @param host le serveur hébergeant la base + * @param port le port de connexion (0 si non renseigné) + * @param banner si on affiche la bannière d'accueil + * @throws SQLException si erreur SQL + */ + public Console(Contexte cont, int type, String base, String login, String mdp, String opts, + String host, int port, boolean banner) throws SQLException { + createConsole(true, cont, type, null, null, base, login, mdp, opts, host, port, banner); + } + + /** + * Création et lancement de la console + * @param mode mode de connexion : simplifié (avec type) ou normal (avec chaîne de connexion) + * @throws SQLException si erreur SQL + */ + private void createConsole(boolean mode, Contexte cont, int type, String path, String driver, + String base, String login, String mdp, String opts, String host, int port, boolean banner) + throws SQLException { + // Contexte + if (cont == null) throw new IllegalArgumentException("Constructeur Console : contexte null"); + this.cont = cont; + // Affichage + if (banner && cont.getVerbose() >= Contexte.VERB_MSG) dispBanner(cont); + + // Création de la console et connexion + if (!cont.hasSQLCnx()) { + String l = login, p = mdp; + if (l == null || p == null || (l.length() == 0 && p.length() == 0)) { + String[] lp = askConnUserPswd(); + if (lp == null) System.exit(-1); + l = lp[0]; p = lp[1]; + } + // Connexion si pas de connexion déjà en contexte + SQLCnx sqlx; + if (mode) sqlx = new SQLCnx(type, base, l, p, opts, host, port); // mode simplifié + else sqlx = new SQLCnx(path, driver, l, p); // mode normal + cont.setSQLCnx(sqlx); + cont.loadInitFile(); // Si présence d'un fichier INIT, exécution du fichier + } + compute(); + } + + /** + * Affichage du message d'accueil + * + * @param cont le contexte + */ + static void dispBanner(Contexte cont) { + cont.printlnX(" " + SQLCnx.frm("", 64, '='), Contexte.YELLOW); + cont.printlnX(" LunaSQL - Gestion d'une base de données en console et script", Contexte.GREEN); + cont.printlnX(" Version : " + Config.APP_VERSION_NUM + " - " + Config.APP_VERSION_NAME + + " - " + Config.APP_DT_REVISION, Contexte.GREEN); + cont.printlnX(" " + SQLCnx.frm("", 64, '=') + "\n", Contexte.YELLOW); + } + + /** + * *** Mode de saisie SQL en boucle *** + */ + private void compute() { + try { + reader = new ConsoleReader(); + cont.setConsole(this); + cont.setConsoleReader(reader); + reader.setBellEnabled(cont.getVar(Contexte.ENV_BEEP_ON).equals(Contexte.STATE_TRUE)); + + // Completors + setCompletors(); + + // Historique facultatif + History histo = null; + //FileHistory histo = null; + if (cont.getVar(Contexte.ENV_HIST_FILE).length() > 0) { + histo = new History(new File(cont.getVar(Contexte.ENV_HIST_FILE))); + //histo = new FileHistory(new File(cont.getVar(Contexte.ENV_HIST_FILE))); + histo.setMaxSize(Config.CS_HISTORY_SIZE); + cont.setHistory(histo); + reader.setHistory(histo); + reader.setUseHistory(false); // ajout manuel des commandes + } + + // Prompt SQL + boolean canCont = true; + StringBuilder prompt = new StringBuilder(); + Lecteur lec; + while (canCont) { + boolean hasColors = cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE); + String hdbl = cont.getVar(Contexte.ENV_HIST_DBL); // 0, 1 ou 2 + lec = new Lecteur(cont); + lec.setLecVar(Contexte.LEC_SUPER, ""); + lec.setLecVar(Contexte.LEC_THIS, "(console)"); + lec.setConsoleMode(); // seule utilisation + lec.setCheckWhile(false); + boolean rtmp = false; + prompt.setLength(0); + prompt.append('\n'); + if (hasColors) prompt.append("\u001b[32m"); + prompt.append(lec.substituteExt(cont.getVar(Contexte.ENV_PROMPT))).append("> "); + if (lec.getCmdState() != Instruction.RET_CONTINUE) { + cont.errprintln("Réinitialisation de l'option " + Contexte.ENV_PROMPT); + cont.setVar(Contexte.ENV_PROMPT, ""); + continue; + } + if (hasColors) prompt.append("\u001b[0m"); + + String line; + StringBuilder lines = new StringBuilder(); // doublage de l'historique + int np = 0, na = 0, nc = 0, nl = 2; + do { // boucle d'assemblage des multi-lignes + try { + line = reader.readLine(prompt.toString()); + } + catch (IndexOutOfBoundsException ex){ + // FIXME: correction de BUG JLine sur pression sur alors que curseur en commande + // Sera résolu (j'espère) avec JLine3 + cont.errprintln("\nBUG JLine 1 : soyez en fin de ligne avant de presser ", false); + line = null; // pour sortir + } + if (line == null) { + if (nl > 2) { + cont.errprintln("\nInterrompu"); + break; + } else { + if (cont.canExec()) cont.println("Bye!"); + else cont.println(""); + line = "QUIT;"; + if (histo != null) histo = null; // ne pas inscrire la ligne QUIT + } + } + else if (line.length() == 0) continue; + + rtmp = lec.add(line); + lines.append(line).append('\n'); + np = lec.getNP(); // nb de parenthèses ouvertes + na = lec.getNA(); // nb d'accolades ouvertes + nc = lec.getNC(); // nb de crochets ouverts + prompt.setLength(0); + if (hasColors) prompt.append("\u001b[33m"); + prompt.append(String.format("%02d ", nl++)); + prompt.append(np == 0 && na == 0 && nc == 0 ? lec.getMLineChar() : + (np > 0 ? String.format("%02d(%" + (np * 2) + "s", np, "") : + (na > 0 ? String.format("%02d{%" + (na * 2) + "s", na, "") : + (nc > 0 ? String.format("%02d[%" + (nc * 2) + "s", nc, "") : "#)")))); + if (hasColors) prompt.append("\u001b[0m"); + prompt.append(' '); + } while (!rtmp); + + if ((np != 0 || na != 0 || nc != 0) && cont.getVerbose() >= Contexte.VERB_MSG) + cont.errprintln("Parenthèses/accolades/crochets désappariés : " + (np + na + nc)); + canCont = (lec.getCmdState() != Instruction.RET_SHUTDOWN); + + if (histo != null) { + if (hdbl.equals("1")) histo.addToHistory(line); + else if (hdbl.equals("2")) histo.addToHistory(Tools.cleanSQLCode(cont, lines.toString(), -2)); + else if (hdbl.equals("3")) histo.addToHistory(Tools.cleanSQLCode(cont, lines.toString(), 2).trim()); + } + + // Affichage du retour de la commande + if (cont.getVerbose() >= Contexte.VERB_MSG) { + Valeur vr = cont.getValeur(); + String sub; + if (vr != null && (sub = vr.getSubValue()) != null) + cont.printlnX("=> " + sub, Contexte.BR_BLUE); + } + lec.fin(); + }// while + + // Fermeture + reader.flushConsole(); + //reader.flush(); + if (cont.getVar(Contexte.ENV_SAVE_CONF).equals(Contexte.STATE_TRUE)) cont.dumpConfigFile(); + } + catch (IOException ex) { + cont.exception("Console", "ERREUR IOException : " + ex.getMessage() + + "\nOuverture de la console interrompue", 1, ex); + } + finally { + if (cont.fermerConnex()) System.exit(cont.getQuitStat()); + // en console, on sort de toutes façons + } + } + + /** + * Fixe les différents moteurs de complètement + */ + @SuppressWarnings("unchecked") + public void setCompletors() { + // Suppression des anciens + Iterator it = reader.getCompletors().iterator(); + while (it.hasNext()) reader.removeCompletor(it.next()); + + // Création des nouveaux + List cpArg = new LinkedList<>(); + // Arg 1 : Compléter les commandes/macros/alias + HashMap cmds = cont.getAllCommands(); + List cpCmd = new ArrayList<>(cmds.size()); + for (Map.Entry me : cmds.entrySet()) + if (me.getKey() != null){ + cpCmd.add(me.getKey()); + cpCmd.add(me.getKey().toLowerCase()); + } + cpArg.add(new MultiCompletor(new Completor[]{ + new SimpleCompletor(cpCmd.toArray(new String[]{})), + new SimpleCompletor(getVariablesCol()) + })); + // Arg 2 et plus : Compléter les fichiers et les tables + cpArg.add(new MultiCompletor(new Completor[]{ + new FileNameCompletor(), + new SimpleCompletor(getTables()), + new SimpleCompletor(getVariablesDol()) + })); + reader.addCompletor(new ArgumentCompletor(cpArg)); + } + + /** + * Liste des tables en majuscule et en minuscule + * Types listés : "TABLE", "VIEW", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM" + * + * @return String[] de tables non system + */ + private String[] getTables() { + String[] ret = null; + try { + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + String[] typestbl = + {"TABLE", "VIEW", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"}; + ArrayList tables = new ArrayList<>(); + ResultSet result = dMeta.getTables(null, null, null, typestbl); + while (result.next()) { + tables.add(result.getString(3)); + } + ret = new String[tables.size() * 2]; + for (int i = 0; i < tables.size(); i++) { + ret[i * 2] = tables.get(i).toUpperCase(); + ret[i * 2 + 1] = tables.get(i).toLowerCase(); + } + result.close(); + } + catch (SQLException ex) { + cont.exception("Console", "ERREUR SQLException : " + ex.getMessage(), 1, ex); + } + return ret; + } + + /** + * Liste des variables et constantes définies + * + * @return String[] des variables + */ + private String[] getVariablesDol() { + Object[] o = cont.getAllVars().keySet().toArray(); + int l = o.length; + String[] r = new String[l * 2]; + for (int i = 0; i < l; i++) { + r[i] = (String) o[i]; + r[i + l] = "$(" + o[i] + ")"; + } + return r; + } + private String[] getVariablesCol() { + Object[] o = cont.getAllVars().keySet().toArray(); + int l = o.length; + String[] r = new String[l * 2]; + for (int i = 0; i < l; i++) { + r[i] = (String) o[i]; + r[i + l] = cont.isNonSys(r[i]) ? ":" + o[i] : ""; + } + return r; + } + + /** + * Demande des identifiants de connexion + * + * @return String[] tableau de deux éléments : le nom de connexion et le mot de passe + */ + static String[] askConnUserPswd() { + java.io.Console csl = System.console(); + if (csl != null) { + String l, p; + l = csl.readLine("Nom d'utilisateur : "); + if (l == null) { // saisie de null (Ctrl-D), on ->[] + System.out.println("\n\nBye"); + return null; + } + char [] cp = csl.readPassword("Mot de passe : "); + if (cp == null) { // saisie de null (Ctrl-D), on ->[] + System.out.println("\nBye"); + return null; + } + p = new String(cp); + return new String[]{l, p}; + } + return null; + } +}// class + diff --git a/src/lunasql/Main.java b/src/lunasql/Main.java new file mode 100644 index 0000000..4343da8 --- /dev/null +++ b/src/lunasql/Main.java @@ -0,0 +1,819 @@ +package lunasql; + +import static java.util.Arrays.asList; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.URL; +import java.sql.SQLException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.List; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.cmd.CmdHelp; +import lunasql.cmd.CmdNeed; +import lunasql.http.HttpConsole; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.sql.SQLCnx; +import lunasql.sql.TypesSGBD; +import lunasql.ui.FrmEditScript; + +/** + * Lancement général LunaSQL
+ * Created on 10 octobre 2010, 11:05 + * + * @author M.P. + */ +public class Main { + + private static Contexte contex; + private static boolean hasBase, hasContex, banner; + private static int port; + private static String user, mdp, base, dbtype, path, driver, host, opts, login; + private static File basefile; + + /** + * Main de lancement + * + * @param args the command line arguments + */ + public static void main(String[] args) { + // Rattrapage des exceptions imprévues + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); + System.setProperty("sun.awt.exception.handler", ExceptionHandler.class.getName()); + + try { readArgs(args); } + catch(OptionException ex) { + printErr("ERREUR OptionException : " + ex.getMessage()); + System.exit(-1); + } + } + + /* + * Lecture des arguments + */ + private static void readArgs(String[] args) throws OptionException { + OptionParser parser = new OptionParser(); + // Options + parser.acceptsAll(asList("u", "username"), "nom d'utilisateur de la base de données") + .withRequiredArg().ofType(String.class).describedAs("user"); + parser.acceptsAll(asList("p", "password"), "mot de passe de la base de données") + .withOptionalArg().ofType(String.class).defaultsTo("").describedAs("pswd"); + parser.acceptsAll(asList("t", "type"), "type de base de données") + .withRequiredArg().ofType(String.class).describedAs("type"); + parser.acceptsAll(asList("n", "name"), "nom de la base de données") + .withRequiredArg().ofType(String.class).describedAs("name"); + parser.acceptsAll(asList("H", "host"), "machine hôte de la base de données") + .withRequiredArg().ofType(String.class).describedAs("host"); + parser.acceptsAll(asList("R", "port"), "port de connexion à la base de données") + .withRequiredArg().ofType(Integer.class).describedAs("port"); + parser.acceptsAll(asList("D", "driver"), "driver de connexion à la base de données") + .withRequiredArg().ofType(String.class).describedAs("driver"); + parser.acceptsAll(asList("P", "path"), "chemin de connexion à la base de données") + .withRequiredArg().ofType(String.class).describedAs("path"); + parser.acceptsAll(asList("l", "login"), "connexion à une base sur alias de fichier") + .withRequiredArg().ofType(String.class).describedAs("path"); + parser.acceptsAll(asList("o", "db-options"), "options de connexion à la base de données") + .withRequiredArg().ofType(String.class).describedAs("db-options"); + parser.acceptsAll(asList("b", "verb-level"), "niveau de bavardage") + .withRequiredArg().ofType(String.class).describedAs("num|lib"); + parser.acceptsAll(asList("V", "need-version"), "numéro de version requise") + .withRequiredArg().ofType(String.class).describedAs("version"); + parser.accepts("exit-on-error", "interruption du script sur erreur") + .withOptionalArg().ofType(Integer.class).defaultsTo(1).describedAs("0|1"); + parser.accepts("defs", "variables d'initialisation de la console") + .withRequiredArg().ofType(String.class).describedAs("key1=val1,key2=val2") + .withValuesSeparatedBy(","); + parser.accepts("opts", "options d'initialisation de la console") + .withRequiredArg().ofType(String.class).describedAs("key1=val1,key2=val2") + .withValuesSeparatedBy(","); + parser.accepts("uses", "bibliothèques à charger au démarrage") + .withRequiredArg().ofType(String.class).describedAs("lib1,lib2") + .withValuesSeparatedBy(","); + parser.accepts("scripts-path", "chemins des répertoires de scripts") + .withRequiredArg().ofType(File.class).describedAs("rep1" + File.pathSeparator + "rep2") + .withValuesSeparatedBy(File.pathSeparator); + parser.accepts("plugins-path", "chemins des répertoires de greffons") + .withRequiredArg().ofType(File.class).describedAs("rep1" + File.pathSeparator + "rep2") + .withValuesSeparatedBy(File.pathSeparator); + parser.accepts("config-file", "fichier de configuration de la console") + .withRequiredArg().ofType(File.class).describedAs("file"); + parser.accepts("init-file", "fichier SQL d'initialisation de la console") + .withRequiredArg().ofType(File.class).describedAs("file"); + parser.accepts("history-file", "fichier de l'historique de la console") + .withRequiredArg().ofType(File.class).describedAs("file|-"); + parser.accepts("bases-file", "fichier XML des bases de connexion") + .withRequiredArg().ofType(File.class).describedAs("file"); + parser.accepts("log-dir", "répertoire des journaux d'erreur") + .withRequiredArg().ofType(File.class).describedAs("dir"); + parser.accepts("exec-args", "arguments de la commande 'exec'") + .withRequiredArg().ofType(String.class).describedAs("arg1;arg2"); + parser.accepts("http-port", "port d'écoute du serveur HTTP") + .withRequiredArg().ofType(Integer.class).describedAs("port"); + parser.acceptsAll(asList("A", "no-colors"), "sans coloration de la console"); + parser.acceptsAll(asList("B", "no-banner"), "sans la bannière d'accueil en console"); + parser.acceptsAll(asList("O", "deny-opt-command"), "interdit la commande OPT"); + parser.acceptsAll(asList("U", "deny-sql-update"), "interdit les commandes SQL de modification"); + + // commandes + parser.accepts("?", "aide sur les options"); + parser.acceptsAll(asList("h", "help"), "aide succinte sur le lancement"); + parser.acceptsAll(asList("a", "apropos"), "infos sur la version de l'application"); + parser.acceptsAll(asList("v", "version"), "numéro de version de l'application"); + parser.acceptsAll(asList("r", "run-sql"), "commande(s) SQL à exécuter puis sortie") + .withRequiredArg().ofType(String.class).describedAs("cmd1;cmd2;"); + parser.accepts("init-sql", "commande(s) SQL à exécuter avant lancement") + .withRequiredArg().ofType(String.class).describedAs("cmd1;cmd2;"); + parser.acceptsAll(asList("x", "exec"), "fichier SQL à exécuter puis sortie") + .withRequiredArg().ofType(String.class).describedAs("file|-:cmds|+:cmds"); + parser.acceptsAll(asList("c", "console"), "lancement de la console SQL"); + parser.acceptsAll(asList("i", "stdin"), "lecture de commandes depuis l'entrée standard"); + parser.accepts("editor", "ouverture de l'éditeur de texte") + .withOptionalArg().ofType(File.class).defaultsTo(new File("-")).describedAs("file"); + parser.acceptsAll(asList("e", "list-engines"), "liste des moteurs SE puis sortie"); + parser.accepts("http-console", "démarre le serveur HTTP"); + parser.accepts("test-jline3", "classe de test de JLine3"); + + //... autres options + OptionSet options = null; + try { options = parser.parse(args); } + catch (OptionException ex) { + printErr("ERREUR OptionException : " + ex.getMessage()); + try { parser.printHelpOn(System.err); } + catch(IOException ex2) {} + System.exit(-1); + } + + // *** Simples commandes hors contexte *** + // Aide générale + if (options.has("help")) { + System.out.println("Aide générale de la console LunaSQL\nUsage :"); + System.out.println(CmdHelp.getLaunching()); + System.out.println("\nPour toute interrogation, se rapprocher de"); + System.out.println("\t" + Config.APP_AUTHOR_NAME + " " + Config.APP_AUTHOR_EMAIL); + System.exit(0); + + // Commandes en ligne + } else if (options.has("?")) { + System.out.println("Aide générale de la console LunaSQL\nOptions :\n"); + try { parser.printHelpOn(System.out); } + catch (IOException ex) {} + System.exit(0); + + // Affichage des infos sur la version + } else if (options.has("apropos")) { + StringBuilder sb = new StringBuilder(); + sb.append("Console LunaSQL - Gestion d'une base de données SQL en console\n"); + sb.append("Version : ").append(Config.APP_VERSION_NUM); + sb.append(" (Nom de code : ").append(Config.APP_VERSION_NAME).append(")\n"); + sb.append("Auteur : ").append(Config.APP_AUTHOR_NAME); + sb.append("\n\tContact : ").append(Config.APP_AUTHOR_EMAIL); + sb.append("\nTourne sur la JVM version ").append(System.getProperty("java.version")) + .append(", ").append(System.getProperty("java.vm.info")); + System.out.println(sb.toString()); + System.exit(0); + + // Affichage du numéro de version seul + } else if (options.has("version")) { + System.out.println(Config.APP_VERSION_NUM); + System.exit(0); + } + + /*else if (options.has("test-jline3")) { + new TestJLine3(); + }*/ + + // *** Options dépendant du contexte *** + // Préparation des arguments par défaut + user = ""; + mdp = ""; + base = ""; + path = ""; + driver = ""; + host = ""; + port = 0; + login = ""; + basefile = null; + opts = null; + dbtype = TypesSGBD.getSTypeFromArg(Config.DB_DEFAUT_TYPE); + hasBase = false; + banner = true; + + // *** Options *** + // option : exit-on-error + if (options.has("exit-on-error")) { + creationContexte(); + contex.setVar(Contexte.ENV_EXIT_ERR, ((Integer)options.valueOf("exit-on-error"))==1 ? "1" : "0"); + } + + // option : need-version + if (options.has("need-version")) { + creationContexte(); + String v = (String)options.valueOf("need-version"); + if (v.matches(CmdNeed.VER_REG)) contex.executeCmd("NEED", v); + else { + printErr("numéro de version fourni invalide : " + v); + System.exit(-2); + } + } + + // option : bases-file + if (options.has("bases-file")) { + File f = (File)options.valueOf("bases-file"); + if (f.isFile() && f.canRead()) basefile = f; + else { + printErr("impossible de lire le fichier : " + f.getAbsolutePath()); + System.exit(-5); + } + } + + // option : connexion à une base + if (options.has("login")) { + login = (String)options.valueOf("login"); + setLogin(true); + } + + // option : username + if (options.has("username")) { + user = (String)options.valueOf("username"); + if (options.has("password")) mdp = (String)options.valueOf("password"); + else mdp = ""; + } + + // option : type + if (options.has("type")) { + if (!options.has("name")) { + printErr("l'option 'type' nécessite l'option 'name'"); + System.exit(-5); + } + dbtype = (String)options.valueOf("type"); + } + + // option : name + if (options.has("name")) { + if (!options.has("type")) { + printErr("l'option 'name' nécessite l'option 'type'"); + System.exit(-5); + } + base = (String)options.valueOf("name"); + if (!base.equals("-")) hasBase = true; + } + + // option : host et port + if (options.has("host")) { + if (!options.has("type")) { + printErr("l'option 'host' nécessite l'option 'type'"); + System.exit(-5); + } + host = (String)options.valueOf("host"); + if (options.has("port")) port = ((Integer)options.valueOf("port")); + } + + // option : driver + if (options.has("driver")) { + if (!options.has("path")) { + printErr("l'option 'driver' nécessite l'option 'path'"); + System.exit(-5); + } + if (options.has("type")) { + printErr("l'option 'type' est incompatible avec l'option 'driver'"); + System.exit(-5); + } + driver = (String)options.valueOf("driver"); + } + + // option : path et driver + if (options.has("path")) { + if (!options.has("driver")) { + printErr("l'option 'path' nécessite l'option 'driver'"); + System.exit(-5); + } + path = (String)options.valueOf("path"); + if (path.length() > 0) hasBase = true; + } + + // option : no-colors + if (options.has("no-colors")) { + creationContexte(false); + } + + // option : banner + if (options.has("no-banner")) { + banner = false; + } + + // option : options de connexion (selon compatibilité) + if (options.has("db-options")) { + creationContexte(); + opts = (String)options.valueOf("db-options"); + } + + // option : defs + if (options.has("defs")) { + creationContexte(); + List vars = options.valuesOf("defs"); + for (Object o : vars) { + String kv = o.toString(); + int i = kv.indexOf('='); + if (i < 1) continue; + String k = kv.substring(0, i), v = kv.substring(i + 1); + if (contex.valideKey(k)) contex.setVar(k, v); + else contex.errprintln(" * defs : affectation de variable invalide ou non autorisé : " + k); + } + } + + // option : opts + if (options.has("opts")) { + creationContexte(); + List vars = options.valuesOf("opts"); + for (Object o : vars) { + String kv = o.toString(); + int i = kv.indexOf('='); + if (i < 1) continue; + String k = kv.substring(0, i), v = kv.substring(i + 1); + if (contex.isSysUser(k)) contex.setVar(k, v); + else contex.errprintln(" * opts : non définie ou édition non autorisée : " + k); + } + } + + // option : uses + if (options.has("uses")) { + creationContexte(); + List vars = options.valuesOf("uses"); + String[] lcmd = new String[vars.size() + 1]; + lcmd[0] = "USE"; + for (int i = 0; i < vars.size(); i++) lcmd[i + 1] = vars.get(i).toString(); + contex.executeCmd(lcmd); + } + + // option : scripts-path + if (options.has("scripts-path")) { + creationContexte(); + List p = options.valuesOf("scripts-path"); + StringBuilder ss = new StringBuilder(); + for (Object o : p) { + File f = (File) o; + if (f.isDirectory()) ss.append(f.getAbsolutePath()).append(File.pathSeparator); + } + contex.setVar(Contexte.ENV_SCR_PATH, ss.toString()); + } + + // option : plugins-path + if (options.has("plugins-path")) { + creationContexte(); + List p = options.valuesOf("plugins-path"); + StringBuilder ss = new StringBuilder(); + for (Object o : p) { + File f = (File)o; + if (f.isDirectory()) ss.append(f.getAbsolutePath()).append(File.pathSeparator); + } + contex.setVar(Contexte.ENV_PLG_PATH, ss.toString()); + } + + // option : config-file + if (options.has("config-file")) { + creationContexte(); + File f = (File)options.valueOf("config-file"); + if (f.isFile() && f.canRead()) contex.setVar(Contexte.ENV_CONF_FILE, f.getAbsolutePath()); + } + + // option : init-file + if (options.has("init-file")) { + creationContexte(); + File f = (File)options.valueOf("init-file"); + if (f.isFile() && f.canRead()) contex.setVar(Contexte.ENV_INIT_FILE, f.getAbsolutePath()); + } + + // option : history-file + if (options.has("history-file")) { + creationContexte(); + File f = (File)options.valueOf("history-file"); + if (f.getName().equals("-")) // pas d'historisation des commandes + contex.setVar(Contexte.ENV_HIST_FILE, ""); + else { // fichier histo renseigné + if (!f.exists() || (f.isFile() && f.canRead())) + contex.setVar(Contexte.ENV_HIST_FILE, f.getAbsolutePath()); + } + } + + // option : init-sql + if (options.has("init-sql")) { + if (!hasBase) { + printErr("commande CONSOLE invalide si aucune base fournie"); + System.exit(-4); + } + execScript((String)options.valueOf("init-sql"), false); + execConsole(); + } + + // option : verb-level + if (options.has("verb-level")) { + creationContexte(); + contex.setVerbose((String)options.valueOf("verb-level")); + } + + // option : log-dir + if (options.has("log-dir")) { + System.setProperty(Contexte.PROP_LOG_DIR, + ((File) options.valueOf("log-dir")).getAbsolutePath()); + } + + // option : deny-opt-command + if (options.has("deny-opt-command")) { + creationContexte(); + contex.setVar(Contexte.ENV_CONST_EDIT, "1"); // 0: aucune, 1: locales, 2: toutes + } + + // option : deny-modify-sql + if (options.has("deny-sql-update")) { + creationContexte(); + contex.setVar(Contexte.ENV_SQL_UPDATE, Contexte.STATE_FALSE); // 0: non, 1: oui + } + + // *** Autres commandes dépendant du contexte *** + // Les commandes sont exclusives : une seule est permise + + // Aucune base fournie : recherche de login (sucre syntaxique à option --login) + if (!hasBase) { + List arglist = options.nonOptionArguments(); + if (!arglist.isEmpty()) login = (String)arglist.get(0); + setLogin(false); + } + + // Lancement simple de la console + if (options.has("console")) { + if (!hasBase) { + printErr("commande CONSOLE invalide si aucune base fournie"); + System.exit(-4); + } + execConsole(); + + // Exécution d'une commande SQL + } else if (options.has("run-sql")) { + if (!hasBase) { + printErr("commande SQL invalide si aucune base fournie"); + System.exit(-4); + } + System.exit(execScript((String)options.valueOf("run-sql"))); + + // Exécution d'un fichier LunaSQL ou ScriptEngine + } else if (options.has("exec")) { + if (!hasBase) { + printErr("commande EXEC invalide si aucune base fournie"); + System.exit(-4); + } + String f = (String)options.valueOf("exec"), exargs = ""; + if (options.has("exec-args")) exargs = (String)options.valueOf("exec-args"); + if (f.startsWith("-:") || f.startsWith("+:")) {// exec +:cmds arg1 args2 + System.exit(execScript("EXEC " + f.charAt(0) + " \"" + f.substring(2) + "\" " + + exargs.replace(';', ' '))); + } + else System.exit(execScript("EXEC \"" + f + "\" " + exargs.replace(';', ' '))); + + // Lecture des commandes depuis l'entrée standard + } else if (options.has("stdin")) { + if (!hasBase) { + printErr("commande STDIN invalide si aucune base fournie"); + System.exit(-4); + } + if (!hasContex) { + creationContexte(); + contex.loadConfigFile(); + hasContex = true; + } + int stat = 0; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { + setsql(); + contex.loadInitFile(); + // Lecture du script + String ln; + Lecteur lec = new Lecteur(contex); + lec.setLecVar(Contexte.LEC_THIS, "(stdin)"); + while ((ln = reader.readLine()) != null) lec.add(ln); + stat = Math.min(lec.getCmdState(), contex.getQuitStat()); + } + catch (IOException ex) { + printErr("ERREUR IOException : " + ex.getMessage()); + stat = -5; + } + finally { + contex.fermerConnex(); + System.exit(stat); + } + + // Ouverture de l'éditeur Swing + } else if (options.has("editor")) { + if (!hasBase) { + printErr("commande EDITOR invalide si aucune base fournie"); + System.exit(-4); + } + creationContexte(); + contex.loadConfigFile(); + banner(); + setsql(); + contex.loadInitFile(); + try { + FrmEditScript fed = new FrmEditScript(contex, true); + fed.setVisible(true); + File f = (File) options.valueOf("editor"); + if (!f.getName().isEmpty() && !f.getName().equals("-")) fed.openFile(f); + } catch (HeadlessException ex) { + printErr("ERREUR HeadlessException : " + ex.getMessage()); + } + + // Listage des moteurs de scripts diponibles depuis le classpath + } else if (options.has("list-engines")) { + creationContexte(); + contex.loadConfigFile(); + contex.executeCmd("ENGINE", "-l"); + System.exit(contex.getQuitStat()); + + // Lancement de la console HTTP (serveur) + } else if (options.has("http-console")) { + if (!hasBase) { + printErr("commande HTTP-CONSOLE invalide si aucune base fournie"); + System.exit(-4); + } + creationContexte(false); + contex.setHttpMode(true); + contex.loadConfigFile(); + banner(); + + if (user.length() == 0) { + String[] lp = Console.askConnUserPswd(); + if (lp == null) System.exit(-1); + user = lp[0]; + mdp = lp[1]; + } + int svport = options.has("http-port") ? (Integer) options.valueOf("http-port") + : Config.SR_HTTP_PORT; + try { + new HttpConsole(contex, TypesSGBD.getTypeFromArg(dbtype), base, user, mdp, opts, host, port, svport); + } + catch (IOException e) { + printErr("ERREUR IOException : " + e.getMessage()); + System.exit(-3); + } + catch (SQLException e) { + printErr("ERREUR SQLException : " + e.getMessage()); + System.exit(-3); + } + System.out.println("En écoute sur http://localhost:" + svport + " et http://localhost/api:" + svport + + ".\nTapez pour terminer le service.\n"); + // Ouverture du browser + Desktop desk = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; + if (desk != null && desk.isSupported(Desktop.Action.BROWSE)) { + try { desk.browse(new URL("http://localhost:" + svport + "/").toURI()); } + catch (Exception ex) { System.err.println("Erreur navigation : " + ex.getMessage()); } + } else System.err.println("Attention : ouverture du navigateur non supportée"); + // Sortie si touche "entrée" tappé + try { System.in.read(); } + catch (Throwable t) { } + finally { + contex.fermerConnex(); + System.exit(0); + } + } + // autres commandes... + + else { // aucune commande spécifiée : lancement de la console par défaut + setLogin(false); + if (!hasBase) { + printErr("commande CONSOLE invalide si aucune base fournie"); + System.exit(-4); + } + execConsole(); + } + } // readArgs + + /** + * Lancement par option Login + * Options par entrée : dbpath, driver, schema, passwd + * + * @param doFile si le fichier INI doit obligatoirement être présent + */ + private static void setLogin(boolean doFile) { + creationContexte(); + try { + if (basefile == null) basefile = new File(Config.CT_BASES_FILE); + if (!basefile.isFile()) basefile = new File(Config.CT_BASES_PATH); + if (!basefile.isFile()) { + if (!doFile) return; + printErr("fichier de définition de bases " + basefile.getCanonicalPath() + " introuvable" + + "\nIdentifiant de login demandé : " + (login.isEmpty() ? "(vide)" : login)); + System.exit(-5); + } + + contex.setVar(Contexte.ENV_BASES_FILE, basefile.getCanonicalPath()); + Properties prop = new Properties(); + prop.load(new FileReader(basefile)); + if (login.isEmpty()) login = prop.getProperty("default", ""); + path = prop.getProperty(login + ".dbpath", ""); + driver = prop.getProperty(login + ".driver", ""); + if (path.isEmpty() || driver.isEmpty()) { + printErr("entrée de fichier de bases invalide : " + (login.isEmpty() ? "(vide)" : login)); + System.exit(-5); + } + user = prop.getProperty(login + ".schema", ""); + mdp = prop.getProperty(login + ".passwd", ""); + hasBase = true; + } catch (IOException ex) { + printErr("ERREUR IOException : " + ex.getMessage()); + System.exit(-5); + } + } + + /** + * Création du contexte d'exécution et de connexion + */ + private static void creationContexte(boolean color) { + if (contex != null) return; + try { contex = new Contexte(color); } + catch (Exception ex) { + printErr("impossible de créer le contexte : " + ex.getMessage()); + } + } + private static void creationContexte() { + creationContexte(true); + } + + /** + * Exécution de code + */ + private static int execScript(String cmd){ + return execScript(cmd, true); + } + + private static int execScript(String cmd, boolean close) { + if (!hasContex) { + creationContexte(); + contex.loadConfigFile(); + hasContex = true; + } + + setsql(); + contex.loadInitFile(); + // Lecture du script + Lecteur lec = new Lecteur(contex, cmd + ';', + new HashMap() {{ put(Contexte.LEC_THIS, "(run-sql)"); }}); + if (close) contex.fermerConnex(); + return Math.min(lec.getCmdState(), contex.getQuitStat()); + } + + /** + * Exécution de console + */ + private static void execConsole() { + if (!hasContex) { + creationContexte(); + contex.loadConfigFile(); + hasContex = true; + } + // Lancement console + if (banner && contex.getVerbose() >= Contexte.VERB_MSG) Console.dispBanner(contex); + setsql(); + if (banner) contex.printlnX("\nPour de l'aide sur les commandes, tapez 'help'", Contexte.BR_WHITE); + contex.loadInitFile(); + new Console(contex); + } + + /** + * Création de la connexion + * Contexte ne doit pas être null + */ + private static void setsql() { + if (contex.hasSQLCnx()) return; + + // Test user/mdp + if (user.length() == 0) { + String[] lp = Console.askConnUserPswd(); + if (lp == null) System.exit(-1); + user = lp[0]; mdp = lp[1]; + } + + try { + if (banner) { + String bs = base.length() > 0 ? base : path; + bs = bs.substring(bs.lastIndexOf(File.separatorChar) + 1); + contex.print("Établissement de la connexion à " + bs + "... "); + } + SQLCnx sqlx = base.length() > 0 ? new SQLCnx( + TypesSGBD.getTypeFromArg(dbtype), base, user, mdp, opts, host, port) + : new SQLCnx(path, driver, user, mdp); + if (banner) contex.println("OK"); + contex.setSQLCnx(sqlx); + } catch (Exception e) { + printErr("impossible de créer la connexion : " + e.getMessage()); + System.exit(-1); + } + } + + /** + * Affichage d'un message d'erreur le cas échéant en rouge + */ + private static void printErr(String msg) { + if (contex != null && contex.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE)) { + System.err.println(Contexte.COLORS[Contexte.BR_RED] + "LunaSQL : " + msg + Contexte.COLORS[Contexte.NONE]); + } + else System.err.println("LunaSQL : " + msg); + } + + /** + * Affichage d'une banière + */ + private static void banner() { + if (contex.getVerbose() >= Contexte.VERB_AFF) { + contex.println("LunaSQL - Gestion d'une base de données en console et script"); + contex.println("Version : " + Config.APP_VERSION_NUM + " - " + Config.APP_VERSION_NAME + '\n'); + } + } + + /** + * Handles an exception thrown in the event-dispatch thread. Pratique pour ne pas planter le + * programme sur une exception de type Event + */ + public static class ExceptionHandler implements Thread.UncaughtExceptionHandler { + + public void handle(Throwable thrown) { + // for EDT exceptions + handleException(Thread.currentThread().getName(), thrown); + } + + @Override + public void uncaughtException(Thread thread, Throwable thrown) { + // for other uncaught exceptions + handleException(thread.getName(), thrown); + } + + void handleException(String tname, Throwable thrown) { + System.err.println("ERROR : Severe exception on thread : " + tname + " : " + + thrown.getMessage()); + System.err.println(getNStackString(thrown)); + + // Ecriture du fichier log des traces d'exception à l'exécution + File p = new File(System.getProperty(Contexte.PROP_LOG_DIR, Config.CT_LOG_DIR)), + f = new File(System.getProperty(Contexte.PROP_LOG_DIR, Config.CT_LOG_DIR) + File.separator + + Config.CT_LOG_FILE); + try { + if (!p.exists()) System.err.println("\nÉcriture du rapport en " + f.getParent() + " impossible !"); + else if (f.exists() || f.createNewFile()) { + // Ajout de l'information au fichier crashes + BufferedWriter out = new BufferedWriter(new FileWriter(f, true)); + out.write("==================== " + + new SimpleDateFormat("E dd/MM/yyyy HH:mm:ss").format(new Date()) + + " ====================\n"); + + out.write("--- Propriétés système ---"); + out.write("\nos.version : " + System.getProperty("os.name", "") + + " (version " + System.getProperty("os.version", "") + ") - " + + System.getProperty("os.arch", "")); + out.write("\njava.version : " + System.getProperty("java.version", "")); + out.write("\njava.home : " + System.getProperty("java.home", "")); + + out.write("\n\n--- Application LunaSQL ---"); + String cp = System.getProperty("java.class.path", ""); + if(cp.endsWith(File.pathSeparator)) cp = cp.substring(0, cp.length() - 2); + out.write("\nlunasql version : " + Config.APP_VERSION_NUM); + out.write("\njava.class.path :\n - " + cp.replaceAll(File.pathSeparator, "\n - ")); + + out.write("\n\n--- Trace de l'exception ---\n"); + thrown.printStackTrace(new PrintWriter(out)); + out.write("\n"); + out.close(); + // Affichage + System.out.println("\nRapport sauvé sous " + f.getCanonicalPath()); + } + } + catch (IOException ex) { + ex.printStackTrace(); + } + } + + private String getNStackString(Throwable e) { + StringBuilder sb = new StringBuilder(); + sb.append("Origine : ").append(e.toString()).append('\n'); + int i = 1; + sb.append("Trace de la pile :"); + for (StackTraceElement element : e.getStackTrace()) { + sb.append("\n ").append(element.toString()); + if (i++ >= 25) { // stop à 25 lignes + sb.append("...\n[stop à ").append(25).append(" lignes]\n"); + break; + } + } + return sb.toString(); + } + }// ExceptionHandler +} // class + diff --git a/src/lunasql/cmd/CmdAlias.java b/src/lunasql/cmd/CmdAlias.java new file mode 100644 index 0000000..3777f8f --- /dev/null +++ b/src/lunasql/cmd/CmdAlias.java @@ -0,0 +1,189 @@ +package lunasql.cmd; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.HashMap; + +import lunasql.Config; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande ALIAS
+ * (Interne) Ne pas utiliser, est appelée par getInstruction + * @author M.P. + */ +public class CmdAlias extends Instruction { + + private static int deep = 0; // compteur d'appels sur la pile + private static HashSet CMD_NAME = new HashSet<>(Arrays.asList("LIST", "DICT", "FILE", "TIME")); + private static HashSet CMD_SWITCH = new HashSet<>( + Arrays.asList("EACH", "EACHLN", "APPLY", "FILTER", "REDUCE", "ANY?", "ALL?", "AT", "AFTER", "REPEAT")); + + private final ArrayList lcmd0 = new ArrayList<>(); // pile des appels de macros + + public CmdAlias(Contexte cont){ + super(cont, TYPE_INVIS, "ALIAS", null); + } + + @Override + public int execute() { + // Contrôle de la profondeur de la pile pour réduire le risque de StackOverflow + if (deep > Config.CF_MAX_CALL_DEEP) { + cont.erreur("ALIAS", "Halte là ! Exécution circulaire irréfléchie dépassant " + + Config.CF_MAX_CALL_DEEP + " appels\n" + + "(sachez ce que vous faites quand vous jouez avec la récursivité !)" , lng); + return RET_EXIT_SCR; + } + + String cmd = getCommandName(); + boolean issym = false, denyrec = true; + if (cmd.charAt(0) == ':') { + issym = true; + cmd = cmd.substring(1); + } + else if (cmd.charAt(0) == '*') { + denyrec = false; + cmd = cmd.substring(1); + } + + // Recherche de référence circulaire d'appels de macros + if (denyrec && !cont.getVar(Contexte.ENV_ALLOW_REC).equals(Contexte.STATE_TRUE) && + !cont.isCtrlCircVar(cmd) && controlCirc(cmd)) { + StringBuilder sb = new StringBuilder(); + for (String s : lcmd0) sb.append(s).append(" -> "); + lcmd0.clear(); + return cont.erreur("ALIAS", "Référence circulaire : " + sb.substring(0, sb.length() - 4) + + "\n(positionnez " + Contexte.ENV_ALLOW_REC + " à " + Contexte.STATE_TRUE + + " pour jouer avec la récursivité)", lng); + } + + // Résolution de l'alias + String code = cont.getVar(cmd); // variable lecteur, puis globale + if (code == null) + return cont.erreur("ALIAS", "Pas de correspondance pour '" + cmd + "' en tant qu'alias", lng); + // Ajout des paramètres + if (issym || cont.getVar(Contexte.ENV_ALIAS_ARG).equals(Contexte.STATE_TRUE)) code = code + ' ' + getSCommand(1); + + // Exécution + // TODO : essayer d'intégrer ici la recherche de la macro en tampon de compilation + // Si existe, l'exécute directement par Lecteur.runCompiled(ArrayList), + // sinon, compilation préalable puis exécution du compilé + // if (bin = cont.getCompiled(cmd) != null) { + // Lecteur.runCompiled(bin); + // } else + if (code.length() > 0) { + // Préparation des variables de lecteur + deep++; + HashMap vars = new HashMap<>(); + if (!denyrec) vars.put(Contexte.ENV_ALLOW_REC, Contexte.STATE_TRUE); + vars.put(Contexte.LEC_VAR_NAME, cmd); + vars.put(Contexte.LEC_THIS, cmd); + vars.put(Contexte.LEC_SUPER, ""); + StringBuilder args = new StringBuilder(); + + vars.put(Contexte.LEC_VAR_ARG_NB, Integer.toString(getLength() - 1)); + for (int i = 1; i < getLength(); i++) { + vars.put(Contexte.LEC_VAR_ARG + i, getArg(i)); + args.append(Tools.putBraces(getArg(i))).append(' '); + } + if (args.length() > 1) args.deleteCharAt(args.length() - 1); + vars.put(Contexte.LEC_VAR_ARG_LS, args.toString()); + + // Exécution + Lecteur lec = new Lecteur(cont, code, vars); + Valeur vr = cont.getValeur(); + cont.setValeur(vr == null ? null : new ValeurDef(cont, null, vr.getSubValue())); + deep--; + return lec.getCmdState(); + } + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return null; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getHelp() { + return null; + } + + /** + * Recherche de référence circulaire dans la commande + * @param cmd nom de commande (alias) à tester + * @return true si une boubcle est trouvée + */ + private boolean controlCirc(String cmd) { + String key = cmd; + if (key == null || key.isEmpty()) return false; + + boolean r = false; + Lecteur lec = new Lecteur(cont, key, false); + for (ArrayList cmdi : lec.getCurrentCmd()) { + int l = cmdi.size(); + key = cmdi.get(0); + //cont.println("k=" + cmdi); + if (lcmd0.indexOf(key) >= 0) { // boucle trouvée, on ->[] + lcmd0.add(key); + return true; + } + + // Recherche en commande exécutant du code + int i; + String val, keyu = key.toUpperCase(); + if (keyu.equals("IF")) { // if 1 {} elseif 2 {} ... else {} + for (i = 2; i < l; i++) { + if ((i + 1) % 3 == 0 || i == l - 1) r = controlCirc(Tools.removeBracesIfAny(cmdi.get(i))); + } + } + else if (keyu.equals("CASE")) { // case x a {} b {} ... else {} + for (i = 3; i < l; i += 2) r = controlCirc(Tools.removeBracesIfAny(cmdi.get(i))); + } + else if (keyu.equals("FOR")) { // for [-sqf...] i iter {} + r = controlCirc(Tools.removeBracesIfAny(cmdi.get(l - 1))); + } + else if (keyu.equals("WHILE")) { // while else + for (i = 2; i < l; i += 2) r = controlCirc(Tools.removeBracesIfAny(cmdi.get(i))); + } + else if (keyu.equals("EVAL")) { // eval -tv {} [-c {}] [-f {}] + for (i = 1; i < l; i++) { + String c = cmdi.get(i); + if (!c.isEmpty() && c.charAt(0) != '-') r = controlCirc(Tools.removeBracesIfAny(c)); + } + } + else if (CMD_NAME.contains(keyu) && l > 3) { // commandes outils à bloc final + if (CMD_SWITCH.contains(cmdi.get(1).toUpperCase())) r = controlCirc(Tools.removeBraces(cmdi.get(3))); + } + // ... autres commandes à exécution de code + // S'il n'est pas possible de déterminer la position du bloc de code, penser parcourir les arg. + // et à reconnaître le bloc par ses {} + // for (i = 1; i < l; i++) { String s = cmdi.get(i); if (Tools.hasBraces(s)) r = controlCirc(Tools.removeBraces(s)); } + + // Recherche en variables + else if ((val = cont.getVar(key)) == null) { // bout de chaîne, on efface + if ((i = lcmd0.size() - 1) >= 0 && lcmd0.get(i).equals(key)) lcmd0.remove(i); + } + else { + // Sinon on continue le parcours en l'ajoutant à la liste + lcmd0.add(key); + r = controlCirc(val); + if (!r && lcmd0.get(i = (lcmd0.size() - 1)).equals(key)) lcmd0.remove(i); + } + } + return r; + } +}// class diff --git a/src/lunasql/cmd/CmdAlter.java b/src/lunasql/cmd/CmdAlter.java new file mode 100644 index 0000000..20b9fd9 --- /dev/null +++ b/src/lunasql/cmd/CmdAlter.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande ALTER
+ * (SQL) Modification de la structure d'un élément de base de données + * @author M.P. + */ +public class CmdAlter extends Instruction { + + public CmdAlter(Contexte cont){ + super(cont, TYPE_CMDSQL, "ALTER", null); + } + + @Override + public int execute() { + return executeUpdate("ALTER", "objet modifié"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " alter (+instr) Modifie la structure d'un objet de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdAppend.java b/src/lunasql/cmd/CmdAppend.java new file mode 100644 index 0000000..d665819 --- /dev/null +++ b/src/lunasql/cmd/CmdAppend.java @@ -0,0 +1,39 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande APPEND
+ * (Interne) Affecte à la valeur de retour _RET_VALUE les arguments de la commande en mode ajout + * @author M.P. + */ +public class CmdAppend extends Instruction { + + public CmdAppend(Contexte cont){ + super(cont, TYPE_CMDINT, "APPEND", null); + } + + @Override + public int execute() { + String ret = (getLength() == 1 ? null : getSCommand(1)), ancv; + Valeur anc = cont.getValeur(); + if (anc == null || (ancv = anc.getSubValue()) == null) { + if (ret == null) cont.setValeur(null); + else cont.setValeur(new ValeurDef(cont, null, ret)); + } + else if (ret != null) cont.setValeur(new ValeurDef(cont, null, ancv + ret)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " append Affecte en mode ajout la valeur de retour\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdArg.java b/src/lunasql/cmd/CmdArg.java new file mode 100644 index 0000000..706b53a --- /dev/null +++ b/src/lunasql/cmd/CmdArg.java @@ -0,0 +1,181 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.script.ScriptException; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; + +/** + * Commande ARG
+ * (Interne) Affectation de variables locales au sein d'une fonction + * @author M.P. + */ +public class CmdArg extends Instruction { + + Pattern opt_pattern; + + public CmdArg(Contexte cont){ + super(cont, TYPE_CMDINT, "ARG", null); + } + + @Override + public int execute() { + if (opt_pattern == null) opt_pattern = Pattern.compile( + "^(?s)(\\*|\\[)?(\\.?" + Contexte.KEY_PATTERN + ")(:((\\w+)|(`(.+)`)))?(\\s+(.*?))?\\]?$"); + //m 1 2 4 56 7 8 9 10 + //ex. [var:`pattern` val] [var:type val] *var + + String fname = cont.getLecVar(Contexte.LEC_SCR_NAME), // peut être null + vname = cont.getLecVar(Contexte.LEC_THIS), // ne doit pas être null + sname = cont.getLecVar(Contexte.LEC_SUPER); // peut être null + if (vname == null) vname = "script"; + boolean isrootscr = vname.equals(fname) && (sname == null || sname.isEmpty() || vname.equals(sname)), + hasgrp = false; + int nbprm = 0; + + List l = getCommand(1), noms = new ArrayList(); + for (int i = 0; i < l.size(); i++) { + String arg, key = l.get(i); + if (key.length() == 0) return cont.erreur("ARG", "nom de paramètre vide", lng); + + // Validation des arguments + if (key.equals("/") && i < l.size() - 1) { + String expr = getSCommand(i + 2); + try { + if (cont.evaluerBool(expr)) break; + else return cont.erreur("ARG", "précondition non remplie pour " + vname + " : " + expr, lng); + } + catch (ScriptException e) {// erreur prévisible > cont.erreur + return cont.erreur("ARG", "impossible d'évaluer l'expression :\n" + e.getMessage(), lng); + } + } + + // Paramètre optionel + Matcher m = opt_pattern.matcher(key); + if (m.matches()) { + //for (int j=1; j<=m.groupCount(); j++) cont.println("group(" + j + ") : " + m.group(j)); + nbprm++; + key = m.group(2); + boolean grp = "*".equals(m.group(1)) , opt = "[".equals(m.group(1)); + if (grp) hasgrp = true; + String typ = m.group(6), typre = m.group(8), val = null; + + if (grp) { // Paramètres groupés des valeurs finales + StringBuilder sb = new StringBuilder(); + while (cont.isLec(arg = (isrootscr ? Contexte.LEC_SCR_ARG : Contexte.LEC_VAR_ARG) + (++i))) { + val = cont.getLecVar(arg); + int r = verifType(typ, typre, key, val); + if (r > -1) return r; + sb.append(Tools.putBraces(val)).append(' '); + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + cont.setLecVar(key, sb.toString()); + break; + } + else { // Paramètres simples ou optionnel + if (cont.isLec(arg = (isrootscr ? Contexte.LEC_SCR_ARG : Contexte.LEC_VAR_ARG) + (i+1))) { + val = cont.getLecVar(arg); + } + else if (opt) { + int r; + Lecteur lec = cont.getCurrentLecteur(); + val = m.group(10) == null ? "" : lec.substituteExt(m.group(10)); + if ((r = lec.getCmdState()) != RET_CONTINUE) return r; + } + if (val == null) { + return cont.erreur("ARG", "paramètre '" + key + "' manquant " + + " (usage : " + vname + ' ' + getSCommand(1) + ')', lng); + } + // Vérification de type + int r = verifType(typ, typre, key, val); + if (r > -1) return r; + + // Types ok + if (noms.contains(key)) cont.errprintln("Attention : argument '" + key + "' dupliqué"); + else noms.add(key); + cont.setLecVar(key, val); + } + } else return cont.erreur("ARG", "affectation de variable invalide : " + key, lng); + } // for + + // Test des arguments en trop et message d'avertissement + if (!hasgrp && cont.isLec(isrootscr ? Contexte.LEC_SCR_ARG_NB : Contexte.LEC_VAR_ARG_NB)) { + int nbarg = Integer.parseInt(isrootscr ? cont.getLecVar(Contexte.LEC_SCR_ARG_NB) + : cont.getLecVar(Contexte.LEC_VAR_ARG_NB)); + if (nbprm < nbarg && cont.getVerbose() >= Contexte.VERB_AFF) { + String vn = cont.getLecVar(Contexte.LEC_THIS); + if (vn == null) vn = "script"; + cont.errprintln("Attention : trop d'arguments pour " + vn + + " (attendus : " + nbprm + ", reçus : " + nbarg + ')'); + } + } + + cont.setValeur(null); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + return RET_CONTINUE; + } + + /** + * Vérification de type + * @param typ le type + * @param typre sinon le patron de valeur + * @param key la clef + * @param val la valeur + * @return numéro d'erreur ou -1 si ok + */ + private int verifType(String typ, String typre, String key, String val) { + try { + boolean typok = true; + if (typ != null) { + if (typ.equals("int")) typok = val.matches("[+-]?\\d+"); + else if (typ.equals("nat")) typok = val.matches("\\d+"); + else if (typ.equals("num")) typok = val.matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); + else if (typ.equals("bool")) typok = val.matches("(?i)0|1|t(rue)?|f(alse)?"); + else if (typ.equals("yesno")) typok = val.matches("(?i)y(es)?|n(o(n)?)?|o(ui)?"); + else if (typ.equals("id")) typok = val.matches(Contexte.KEY_PATTERN); + else if (typ.equals("opt")) typok = val.matches("[_:][_A-Z]+"); + else if (typ.equals("idopt")) typok = val.matches("[_:]?" + Contexte.KEY_PATTERN); + else if (typ.equals("char")) typok = val.length() == 1; + else if (typ.equals("word")) typok = val.matches("\\w+"); + else if (typ.equals("date")) + typok = val.matches("(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\d\\d)"); + else if (typ.equals("datetime")) + typok = val.matches("(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\d\\d) ([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d"); + else if (typ.equals("dateus")) + typok = val.matches("(0?[1-9]|1[012])/(0?[1-9]|[12][0-9]|3[01])/((19|20)\\d\\d)"); + else if (typ.equals("datets")) + typok = val.matches("((19|20)\\d\\d)-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])"); + else if (typ.equals("alpha")) typok = val.matches("[A-Za-z]+"); + else if (typ.equals("alphanum")) typok = val.matches("[A-Za-z0-9]+"); + else if (typ.equals("alphauc")) typok = val.matches("[A-Z]+"); + else if (typ.equals("alphalc")) typok = val.matches("[a-z]+"); + else if (typ.equals("email")) typok = val.matches("(?i)([A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6})"); + else if (!typ.equals("str")) + return cont.erreur("ARG", "type de données inconnu : " + typ, lng); + } + else if (typre != null) typok = val.matches(typre); + if (!typok) return cont.erreur("ARG", "type invalide pour '" + key + + "' (usage : " + cont.getLecVar(Contexte.LEC_VAR_NAME) + ' ' + getSCommand(1) + ')', lng); + return -1; + } + catch (PatternSyntaxException ex) { + return cont.erreur("ARG", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " arg Affecte les arguments en variables locales\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdBreak.java b/src/lunasql/cmd/CmdBreak.java new file mode 100644 index 0000000..bc4c4bb --- /dev/null +++ b/src/lunasql/cmd/CmdBreak.java @@ -0,0 +1,48 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande BREAK
+ * (Interne) Sort d'une boucle for ou while en cours + * @author M.P. + */ +public class CmdBreak extends Instruction { + + public CmdBreak(Contexte cont){ + super(cont, TYPE_CMDINT, "BREAK", null); + } + + @Override + public int execute() { + if (cont.getLoopDeep() == 0) + return cont.erreur("BREAK", "BREAK appelée hors d'une boucle FOR ou WHILE", lng); + + // Profondeur fournie + int lo = 0; + if (getLength() == 2) { + try { lo = Integer.parseInt(getArg(1)); } + catch (NumberFormatException ex) {// erreur prévisible > cont.erreur + return cont.erreur("BREAK", "profondeur non numérique : " + getArg(1), lng); + } + } + // Programmation de la sortie sur profondeurs > 0 + if (lo > 0) { + cont.setLecVar(Contexte.LEC_LOOP_BREAK, "1"); // positionnement non null + cont.setLinkVar(Contexte.LEC_LOOP_BREAK, lo); + } + cont.setValeur(new ValeurDef(cont, null, Integer.toString(lo))); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_BREAK_LP; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " break Sort d'une boucle FOR ou WHILE\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdBuffer.java b/src/lunasql/cmd/CmdBuffer.java new file mode 100644 index 0000000..c5fa7a0 --- /dev/null +++ b/src/lunasql/cmd/CmdBuffer.java @@ -0,0 +1,239 @@ +package lunasql.cmd; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jline.ConsoleReader; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande BUFFER
+ * (SQL) Modification de la structure d'un élément de base de données + * @author M.P. + */ +public class CmdBuffer extends Instruction { + + private final OptionParser parser; + + public CmdBuffer(Contexte cont) { + super(cont, TYPE_CMDINT, "BUFFER", "+"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("c", "supprime le contenu du tampon"); + parser.accepts("l", "liste le contenu du tampon"); + parser.accepts("e", "exécute le contenu du tampon"); + parser.accepts("p", "supprime et retourne la dernière entrée"); + parser.accepts("r", "exécute la dernière entrée"); + parser.accepts("i", "exécute la i-ème entrée").withRequiredArg().ofType(Integer.class) + .describedAs("i"); + parser.accepts("n", "retourne le nombre d'entrées"); + parser.accepts("u", "exécute et supprime la dernière entrée"); + parser.accepts("a", "ajoute une commande au tampon"); + parser.accepts("h", "ajoute la dernière commande au tampon"); + parser.accepts("g", "entre en mode ligne"); + parser.accepts("d", "retourne le contenu"); + parser.accepts("o", "charge le contenu"); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lc = options.nonOptionArguments(); + String cmd = lc.isEmpty() ? "" : ((String) lc.get(0)).toUpperCase(); + Valeur vr = null; + if (options.has("c") || cmd.equals("CLEAR")) { + cont.setVar(Contexte.ENV_CMD_BUFFER, ""); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if ((options.has("l") || cmd.equals("LIST")) && cont.getVerbose() >= Contexte.VERB_AFF) { + String[] cmds = getBufferList(); + StringBuilder sb = new StringBuilder(); + int n = cmds.length; + if (n == 0) sb.append("Tampon de commandes vide\n"); + else { + for (int i = 0; i < cmds.length; i++) + sb.append(String.format("%03d", i + 1)).append(" | ").append(cmds[i]).append('\n'); + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr = new ValeurDef(cont, sb.toString(), Contexte.VERB_AFF, Integer.toString(n)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (options.has("d") || cmd.equals("DUMP")) { + vr = new ValeurDef(cont, null, cont.getVar(Contexte.ENV_CMD_BUFFER)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("o") || cmd.equals("LOAD")) { + String oldcmd = cont.getVar(Contexte.ENV_CMD_BUFFER), newcmd = listToString(lc, 1); + cont.setVar(Contexte.ENV_CMD_BUFFER, oldcmd.length() == 0 ? newcmd : oldcmd + ";;" + newcmd); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("e") || cmd.equals("RUNALL")) { + Lecteur lec = new Lecteur(cont, cont.getVar(Contexte.ENV_CMD_BUFFER)); + vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + return lec.getCmdState(); + } + else if (options.has("p") || cmd.equals("POP")) { + String[] cmds = getBufferList(); + String r = ""; + if (cmds.length > 0) { + r = cmds[cmds.length - 1]; + cmds[cmds.length - 1] = null; + setBufferList(cmds); + } + vr = new ValeurDef(cont, null, r); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("r") || cmd.equals("RUNLAST")) { + String[] cmds = getBufferList(); + if (cmds.length > 0) { + Lecteur lec = new Lecteur(cont, cmds[cmds.length - 1]); + vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + return lec.getCmdState(); + } + } + else if (options.has("i") || cmd.equals("RUNITH")) { + int i; + if (options.has("i")) i = (Integer) options.valueOf("i") - 1; + else if (lc.size() == 2) { + try { i = Integer.parseInt((String) lc.get(1)) - 1; } + catch (NumberFormatException ex) { + return cont.erreur("BUFFER", "nombre incorrect : " + ex.getMessage(), lng); + } + } + else return cont.erreur("BUFFER", "avec option -i : 1 nombre attendu", lng); + + String[] cmds = getBufferList(); + for (int j = 0; j < cmds.length; j++) { + if (i == j) { + Lecteur lec = new Lecteur(cont, cmds[i]); + vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + return lec.getCmdState(); + } + } + } + else if (options.has("n") || cmd.equals("SIZE")) { + String l = Integer.toString(getBufferList().length); + vr = new ValeurDef(cont, l, l); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (options.has("u") || cmd.equals("RUNPOP")) { + String[] cmds = getBufferList(); + String r = ""; + if (cmds.length > 0) { + r = cmds[cmds.length - 1]; + cmds[cmds.length - 1] = null; + setBufferList(cmds); + } + if (r.length() > 0) { + Lecteur lec = new Lecteur(cont, r); + vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + return lec.getCmdState(); + } + } + else if (options.has("a") || cmd.equals("ADD")) { + String cc = cmd.equals("ADD") ? listToString(lc, 1) : listToString(lc); + if (cc.isEmpty()) return cont.erreur("BUFFER", "une commande au moins est attendue", lng); + appendBuffer(cc); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("h") || cmd.equals("ADDLAST")) { + @SuppressWarnings("unchecked") + List lhisto = new ArrayList(cont.getHistory().getHistoryList()); + if (lhisto.size() >= 2) { + Collections.reverse(lhisto); + appendBuffer(lhisto.get(1)); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("g") || cmd.equals("LMODE")) { + ConsoleReader reader = cont.getConsoleReader(); + cont.println("Ajout en tampon en mode ligne. Pour sortir, +D ou ':q'"); // pas en vr + StringBuilder val = new StringBuilder(); + String s; + if(reader == null) { + BufferedReader reader2 = new BufferedReader(new InputStreamReader(System.in)); + while ((s = reader2.readLine()) != null && !s.equals(":q")) val.append(s).append(";;"); + reader2.close(); + } + else { + while ((s = reader.readLine("")) != null && !s.equals(":q")) val.append(s).append(";;"); + } + appendBuffer(val.toString()); + } + else return cont.erreur("BUFFER", "un nom de commande valide de buffer est attendu", lng); + + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("BUFFER", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("BUFFER", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + + /* + * Ajoute une commande au tampon + */ + private void appendBuffer(String txt) { + String cmds = cont.getVar(Contexte.ENV_CMD_BUFFER); + cont.setVar(Contexte.ENV_CMD_BUFFER, (cmds.isEmpty() ? "" : cmds + ";;") + txt); + } + + /* + * Retourne la liste des commandes du tampon sous forme de tableau String[] + */ + private String[] getBufferList() { + String[] cmds = cont.getVar(Contexte.ENV_CMD_BUFFER).split(";;"); + if(cmds.length == 1 && cmds[0].isEmpty()) return new String[0]; + return cmds; + } + + /* + * Fixe la liste des commandes du tampon + */ + private void setBufferList(String[] cmds) { + StringBuilder sb = new StringBuilder(); + for (String s : cmds) { + if (s != null && s.length() > 0) sb.append(s).append(";;"); + } + if (sb.length() > 2) sb.delete(sb.length() - 2, sb.length()); + cont.setVar(Contexte.ENV_CMD_BUFFER, sb.toString()); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " buffer, + Liste et modifie le tampon de commandes\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdCall.java b/src/lunasql/cmd/CmdCall.java new file mode 100644 index 0000000..0fa187b --- /dev/null +++ b/src/lunasql/cmd/CmdCall.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande CALL
+ * (SQL) Appelle une procédure ou toute expression SQL + * @author M.P. + */ +public class CmdCall extends Instruction { + + public CmdCall(Contexte cont){ + super(cont, TYPE_CMDSQL, "CALL", null); + } + + @Override + public int execute() { + return executeCall("CALL", "évaluation terminée"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " call (+proc) Appelle une procédure ou toute expression SQL\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdCase.java b/src/lunasql/cmd/CmdCase.java new file mode 100644 index 0000000..1e5078e --- /dev/null +++ b/src/lunasql/cmd/CmdCase.java @@ -0,0 +1,81 @@ +package lunasql.cmd; + +import java.util.regex.PatternSyntaxException; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.val.Valeur; + +/** + * Commande CASE
+ * (Interne) Conditionnel SWITCH CASE + * @author M.P. + */ +public class CmdCase extends Instruction { + + private static final String SYNERR = "syntaxe : CASE val [str|regexp bloc]* [ELSE bloc]"; + + public CmdCase(Contexte cont){ + super(cont, TYPE_CMDINT, "CASE", null); + } + + @Override + public int execute() { + try { + int lg = getLength(); + if (lg < 2 || lg % 2 != 0) return cont.erreur("CASE", SYNERR, lng); + + // Test des égalités regexp + int ret = RET_CONTINUE; + boolean exec = false; + Lecteur lec = new Lecteur(cont); + lec.setLecVar(Contexte.LEC_SUPER, "case"); + int i = 2; + String val = getArg(1); + while (!exec && i < lg - 1) { + String reg = getArg(i); + Tools.BQRet rbq = Tools.removeBQuotes(reg); + if (rbq.hadBQ) { // avec quotes > regexp + if (val.matches(rbq.value)) { + ret = cont.evaluerBlockIf(lec, getArg(i + 1)); + exec = true; + } + } + else { // sans quotes > exacte + if (val.equals(reg)) { + ret = cont.evaluerBlockIf(lec, getArg(i + 1)); + exec = true; + } + } + i += 2; + } + // Aucun match valide > else + if (!exec && lg > 3 && getArg(lg - 2).equalsIgnoreCase("ELSE")) { // exécution else + ret = cont.evaluerBlockIf(lec, getArg(lg - 1)); + exec = true; + } + lec.fin(); + + if (exec) { + Valeur vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + } + else cont.setValeur(null); // retour sans exécution + return ret; + } + catch (PatternSyntaxException ex) { + return cont.erreur("CASE", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " case Teste en chaîne une exp. reg. et exécute les arguments\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdComment.java b/src/lunasql/cmd/CmdComment.java new file mode 100644 index 0000000..ce03f2a --- /dev/null +++ b/src/lunasql/cmd/CmdComment.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande COMMENT
+ * (SQL) Ajout d'un commentaire + * @author M.P. + */ +public class CmdComment extends Instruction { + + public CmdComment(Contexte cont){ + super(cont, TYPE_CMDSQL, "COMMENT", null); + } + + @Override + public int execute() { + return executeUpdate("COMMENT", "commentaire ajouté"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " comment (+instr) Supprime un objet de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdCommit.java b/src/lunasql/cmd/CmdCommit.java new file mode 100644 index 0000000..372c3aa --- /dev/null +++ b/src/lunasql/cmd/CmdCommit.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande COMMIT
+ * (SQL) Valide les transactions danns le SGBD + * @author M.P. + */ +public class CmdCommit extends Instruction { + + public CmdCommit(Contexte cont){ + super(cont, TYPE_CMDSQL, "COMMIT", null); + } + + @Override + public int execute() { + return executeUpdate("COMMIT", "transactions validées"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " commit Valide les transactions effectuées\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdConfig.java b/src/lunasql/cmd/CmdConfig.java new file mode 100644 index 0000000..c4891f4 --- /dev/null +++ b/src/lunasql/cmd/CmdConfig.java @@ -0,0 +1,125 @@ +package lunasql.cmd; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; +//import jline.console.ConsoleReader; + +/** + * Commande CONFIG
+ * (Interne) Sauvegarde de la configuration (constantes de config., variables et greffons) + * @author M.P. + */ +public class CmdConfig extends Instruction { + + private final OptionParser parser; + + public CmdConfig(Contexte cont){ + super(cont, TYPE_CMDINT, "CONFIG", "CF"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("s", "sauvegarde des var. et options"); + parser.accepts("l", "chargement des var. et options"); + parser.accepts("o", "seulement les options"); + parser.nonOptions("fichier").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lf = options.nonOptionArguments(); + if (lf.size() > 2) + return cont.erreur("CONFIG", "au max. une cmd et un nom de fichier sont attendus", lng); + + Valeur vr = new ValeurDef(cont); + String cmd = lf.isEmpty() ? "LOAD" : ((String) lf.get(0)).toUpperCase(); + if (options.has("s") || cmd.equals("SAVE")) { // sauvegarde + try { + int n; + if (options.has("s")) n = saveVars(lf.isEmpty() ? + cont.getVar(Contexte.ENV_CONF_FILE) : (String) lf.get(0), vr, options.has("o")); + else n = saveVars(lf.size() == 1 ? + cont.getVar(Contexte.ENV_CONF_FILE) : (String) lf.get(1), vr, options.has("o")); + vr.setSubValue(Integer.toString(n)); + } catch (IOException ex) { + return cont.exception("CONFIG", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } else if (options.has("l") || cmd.equals("LOAD")) { // chargement + File f; + String v = null; + if (options.has("l") && lf.size() == 1) v = (String) lf.get(0); + else if (lf.size() == 2) v = (String) lf.get(1); + if (v == null) { + f = new File(cont.getVar(Contexte.ENV_CONF_FILE)); + if (!f.isFile()) + return cont.erreur("CONFIG", "le fichier " + Contexte.ENV_CONF_FILE + " '" + + f.getName() + "' n'existe pas", lng); + } else if ((f = new File(v)).isFile()) cont.setVar(Contexte.ENV_CONF_FILE, v); + else return cont.erreur("CONFIG", "le fichier '" + v + "' n'existe pas", lng); + + int nbl = cont.loadConfigFile(options.has("o")); + vr.setDispValue("Fichier de config. '" + f.getAbsolutePath() + "' chargé (" + nbl + " définitions)"); + vr.setSubValue("0"); + } else if (!options.has("l") && !options.has("s")) + return cont.erreur("CONFIG", "une option LOAD ou SAVE est attendue", lng); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("CONFIG", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex){ + return cont.exception("CONFIG", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /** + * Ecriture des variables utilisateur en fichier de configuration spécifié + * @param cfgfile le nom du fichier de configuration de destination + * @param sysonly si l'on doit sauver uniquement les var. systèmes + * @throws IOException si c'est le cas + */ + private int saveVars(String cfgfile, Valeur vr, boolean sysonly) throws IOException { + File fcfg = new File(cfgfile); + if (!cont.askWriteFile(fcfg)) { + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + // Ecriture + int nbl = cont.dumpConfigFile(fcfg, sysonly); + vr.setDispValue("Fichier de config. '" + fcfg.getCanonicalPath() + "' écrit (" + nbl + " définitions)", + Contexte.VERB_MSG); + return nbl; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " config, cf Gère la configuration actuelle (variables et greffons)\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdCreate.java b/src/lunasql/cmd/CmdCreate.java new file mode 100644 index 0000000..9f8300d --- /dev/null +++ b/src/lunasql/cmd/CmdCreate.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande CREATE
+ * (SQL) Création d'un objet de base de données (table, index, vue...) + * @author M.P. + */ +public class CmdCreate extends Instruction { + + public CmdCreate(Contexte cont){ + super(cont, TYPE_CMDSQL, "CREATE", null); + } + + @Override + public int execute() { + return executeUpdate("CREATE", "objet créé"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " create (+instr) Crée un objet dans la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdDef.java b/src/lunasql/cmd/CmdDef.java new file mode 100644 index 0000000..f079c57 --- /dev/null +++ b/src/lunasql/cmd/CmdDef.java @@ -0,0 +1,267 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande DEF
(Interne) Affectation d'une donnée ou d'un code à une variable + * de l'environnement d'exécution + * @author M.P. + */ +public class CmdDef extends Instruction { + + private final OptionParser parser; + + public CmdDef(Contexte cont) { + super(cont, TYPE_CMDINT, "DEF", ":"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("d", "test d'existence de la variable").withRequiredArg().ofType(String.class) + .describedAs("var").withValuesSeparatedBy(','); + parser.accepts("y", "test d'existence et si non null").withRequiredArg().ofType(String.class) + .describedAs("var").withValuesSeparatedBy(','); + parser.accepts("e", "évaluation du contenu").withRequiredArg().ofType(String.class) + .describedAs("vars"); + parser.accepts("l", "référence locale au lecteur en cours"); + parser.accepts("u", "référence en lecteur père").withRequiredArg().ofType(Integer.class) + .describedAs("niveau"); + parser.accepts("h", "ajout d'une aide de variable").withRequiredArg().ofType(String.class) + .describedAs("help"); + parser.accepts("a", "ajout à la variable existante"); + parser.accepts("c", "mise à jour du complètement"); + parser.accepts("r", "pas de contrôle de ref. circulaire"); + parser.nonOptions("nom_var définition_var").ofType(String.class); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#execute(lunasql.lib.Contexte) + */ + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lc = options.nonOptionArguments(); + boolean toret = true; // si false on ne retourne rien (option -e) + Valeur vr = new ValeurDef(cont); + boolean local = options.has("l"), link = options.has("u"); + + // Listage de toutes les variables + if (lc.isEmpty()) { + if (options.has("d")) { // test d'existence + boolean isset = true; + for (Object o : options.valuesOf("d")) { + String key = (String) o; + boolean isloc = false; + if (cont.isNonSys(key) && (cont.isSet(key) || (isloc = cont.isLec(key)))) { + vr.appendDispValue(SQLCnx.frm(key) + (isloc ? cont.getLecVar(key) : cont.getGlbVar(key)), + Contexte.VERB_BVR, true); + } else { + vr.appendDispValue(key + " non définie", Contexte.VERB_BVR, true); + isset = false; + } + } + vr.setSubValue(isset ? "1" : "0"); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } else if (options.has("y")) { // test d'existence et non vide + boolean isset = true; + for (Object o : options.valuesOf("y")) { + String key = (String) o; + if (cont.isNonSys(key)) { + String val = cont.getLecVar(key); + if (val == null) val = cont.getGlbVar(key); + isset = val != null && !val.isEmpty(); + vr.appendDispValue(SQLCnx.frm(key) + val, Contexte.VERB_BVR, true); + } else { + vr.appendDispValue(key + " non définie ou vide", Contexte.VERB_BVR, true); + isset = false; + } + } + vr.setSubValue(isset ? "1" : "0"); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } else if (options.has("e")) { + String key = (String) options.valueOf("e"); + toret = false; + if (cont.isNonSys(key) && (cont.isSet(key) || cont.isLec(key))) { + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_THIS, key); + vars.put(Contexte.LEC_SUPER, ""); + cont.addSubMode(); + new Lecteur(cont, cont.getVar(key), vars); + cont.remSubMode(); + } else return cont.erreur("DEF", "variable '" + key + "' non définie", lng); + } else if (options.has("c")) { + // Mise à jour du complètement + cont.setCompletors(); + } else { + int nb = 0; + StringBuilder sb = new StringBuilder(); + SortedSet sort; + Iterator iter; + // Variables lecteur + HashMap vlec = cont.getAllLecVars(); + if (vlec != null) { + sort = new TreeSet<>(vlec.keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + String key = iter.next(); + if (cont.isNonSys(key)) { + nb++; + sb.append(SQLCnx.frm(key + " *")).append(' ') + .append(link ? cont.getLinkVar(key, (Integer) options.valueOf("u")) : + cont.getLecVar(key)).append('\n'); + } + } + } + if (!local && !link) { // Variables globales + sort = new TreeSet<>(cont.getAllVars().keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + String key = iter.next(); + if (cont.isNonSys(key)) { + nb++; + sb.append(SQLCnx.frm(key)).append(' ').append(cont.getGlbVar(key)).append('\n'); + } + } + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(nb)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + } else if (lc.size() == 1) { // Affichage, exécution ou test d'une variable + // Simple affichage du contenu + try { + String key = Tools.removeBQuotes((String) lc.get(0)).value, val = null; + int nbv = 0; + StringBuilder sb = new StringBuilder(); + SortedSet sort; + Iterator iter; + Pattern ptnb = Pattern.compile(key); + + // Variables lecteur + HashMap vlec = cont.getAllLecVars(); + if (vlec != null) { + sort = new TreeSet<>(vlec.keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + String k = iter.next(); + if (cont.isNonSys(k) && ptnb.matcher(k).matches()) { + val = link ? cont.getLinkVar(k, (Integer) options.valueOf("u")) + : cont.getLecVar(k); + sb.append(SQLCnx.frm(k + " *")).append(' ').append(val).append('\n'); + nbv++; + if (!key.equals(k)) nbv++; + cont.setVar(Contexte.ENV_RET_VALUE, val); + } + } + } + if (!local && !link) { // Variables globales + sort = new TreeSet<>(cont.getAllVars().keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + String key2 = iter.next(); + if (cont.isNonSys(key2) && ptnb.matcher(key2).matches()) { + val = cont.getGlbVar(key2); + sb.append(SQLCnx.frm(key2)).append(' ').append(val).append('\n'); + nbv++; + if (!key.equals(key2)) nbv++; + cont.setVar(Contexte.ENV_RET_VALUE, val); + } + } + } + // Erreur si non définie + if (val == null) + return cont.erreur("DEF", "variable '" + key + "' non définie", lng); + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + if (nbv > 1) vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(val); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } catch (PatternSyntaxException ex) { + return cont.erreur("DEF", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } else { + // Affectation en variable + StringBuilder sb = new StringBuilder(); + String[] tkeys = ((String) lc.get(0)).split(","); + String val = "", help = null; + boolean append = options.has("a"), docirc = options.has("r"); + if (options.has("h")) help = (String) options.valueOf("h"); + for (int i = 0; i < tkeys.length; i++) { // pour chaque variable de la liste + val = (1 + i >= lc.size() ? "" : + (i < tkeys.length - 1 ? (String) lc.get(1 + i) : listToString(lc, 1 + i))); + if (cont.valideKey(tkeys[i]) || cont.isLec(tkeys[i])) { + if (append) { + String val0 = cont.getVar(tkeys[i]); + if (val0 != null) val = val0 + val; + } + if (link) cont.setLinkVar(tkeys[i], (Integer) options.valueOf("u")); + if (local || link) cont.setLecVar(tkeys[i], val); + else { + cont.setVar2(tkeys[i], val, docirc); + if (help != null) cont.addVarHelp(tkeys[i], help); + } + sb.append("-> variable fixée : ").append(tkeys[i]) + .append(help != null ? " (avec aide)" : "").append('\n'); + } else return cont.erreur("DEF", "affectation de variable invalide : " + + (tkeys[i].isEmpty() ? "(chaîne vide)" : "'" + tkeys[i] + "'"), lng); + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + + vr.setDispValue(sb.toString(), Contexte.VERB_BVR); + vr.setSubValue(val); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + // Mise à jour du complètement + if (options.has("c")) cont.setCompletors(); + } + + if (toret) { // si retour autorisé + vr.setRet(); + cont.setValeur(vr); + } + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("PRINT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex){ + return cont.exception("PRINT", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " def, : Affecte une valeur/fonction à une variable de console\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdDelete.java b/src/lunasql/cmd/CmdDelete.java new file mode 100644 index 0000000..75fa932 --- /dev/null +++ b/src/lunasql/cmd/CmdDelete.java @@ -0,0 +1,35 @@ +package lunasql.cmd; + +import java.sql.SQLException; + +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurExe; + +/** + * Commande DELETE
+ * (SQL) Suppression d'enregistrements dans une table de la base + * @author M.P. + */ +public class CmdDelete extends Instruction { + + public CmdDelete(Contexte cont){ + super(cont, TYPE_CMDSQL, "DELETE", null); + } + + @Override + public int execute() { + return executeUpdate("DELETE", " supprimée", true); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " delete (+instr) Supprime des données de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdDict.java b/src/lunasql/cmd/CmdDict.java new file mode 100644 index 0000000..66e5864 --- /dev/null +++ b/src/lunasql/cmd/CmdDict.java @@ -0,0 +1,263 @@ +package lunasql.cmd; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.util.*; + +import javax.script.ScriptException; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.val.ValeurDef; + +/** + * Commande DICT
+ * (Interne) Outils de gestion d'un disctionnaire + * @author M.P. + */ +public class CmdDict extends Instruction { + + private String dic; + + public CmdDict(Contexte cont){ + super(cont, TYPE_CMDINT, "DICT", null); + } + + @Override + public int execute() { + if (getLength() < 2) + return cont.erreur("DICT", "une commande de dictionnaire est requise", lng); + + int ret = RET_CONTINUE, r; + dic = ""; + String cmd = getArg(1).toUpperCase(); + if (cmd.equals("NEW")) { + int n = getLength(); + if (n % 2 != 0) return cont.erreur("DICT", "NEW : n (clef valeur) attendus", lng); + Properties prop = new Properties(); + for (int i = 2; i < n; i+=2) { + String k = getArg(i), v = getArg(i + 1); + if (k.isEmpty()) continue; + prop.put(k, v); + } + if ((r = dictStore(prop, "NEW")) >= 0) return r; + //dic = Integer.toString(cont.addVarDict(prop)); // ***NEW*** + } + else if (cmd.equals("GET")) { + int l = getLength(); + if (l < 4 || l > 5) return cont.erreur("DICT", "GET : 1 dict., 1 clef, 1 valeur attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "GET : dictionnaire invalide" , lng); + dic = prop.getProperty(getArg(3)); + if (dic == null) { // attribution de la valeur par défaut + if (l == 5) { + Lecteur lec = cont.getCurrentLecteur(); + dic = lec.substituteExt(getArg(4)); + if ((r = lec.getCmdState()) != RET_CONTINUE) return r; + } + else dic = ""; + } + } + else if (cmd.equals("HAS-KEY?")) { + if (getLength() != 4) return cont.erreur("DICT", "HAS-KEY? : 1 dict., 1 chaîne attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "HAS-KEY? : dictionnaire invalide" , lng); + dic = prop.containsKey(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("HAS-VAL?")) { + if (getLength() != 4) return cont.erreur("DICT", "HAS-VAL? : 1 dict., 1 chaîne attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "HAS-VAL? : dictionnaire invalide" , lng); + dic = prop.containsValue(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("SIZE")) { + if (getLength() != 3) return cont.erreur("DICT", "SIZE : 1 dict. attendu", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "SIZE : dictionnaire invalide" , lng); + dic = Integer.toString(prop.size()); + } + else if (cmd.equals("EMPTY?")) { + if (getLength() != 3) return cont.erreur("DICT", "EMPTY? : 1 dict. attendu", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "EMPTY? : dictionnaire invalide" , lng); + dic = prop.isEmpty() ? "1" : "0"; + } + else if (cmd.equals("PUT")) { + if (getLength() != 5) return cont.erreur("DICT", "PUT : 1 dict., 2 chaînes attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "PUT : dictionnaire invalide" , lng); + prop.put(getArg(3), getArg(4)); + if ((r = dictStore(prop, "PUT")) >= 0) return r; + } + else if (cmd.equals("REMOVE")) { + if (getLength() != 4) return cont.erreur("DICT", "REMOVE : 1 dict., 1 chaîne attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "REMOVE : dictionnaire invalide" , lng); + prop.remove(getArg(3)); + if ((r = dictStore(prop, "REMOVE")) >= 0) return r; + } + else if (cmd.equals("MERGE")) { + if (getLength() != 4) return cont.erreur("DICT", "MERGE : 2 dict. attendus", lng); + Properties prop1 = Tools.getProp(getArg(2)), prop2 = Tools.getProp(getArg(3)); + prop1.putAll(prop2); + if ((r = dictStore(prop1, "MERGE")) >= 0) return r; + } + else if (cmd.equals("KEYS")) { + if (getLength() != 3) return cont.erreur("DICT", "KEYS : 1 dict. attendu", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "KEYS : dictionnaire invalide" , lng); + List keys = new ArrayList<>(); + for (Enumeration e = prop.keys(); e.hasMoreElements();) + keys.add((String)e.nextElement()); + dic = Tools.arrayToString(keys); + } + else if (cmd.equals("VALUES")) { + if (getLength() != 3) return cont.erreur("DICT", "VALUES : 1 dict. attendu", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("DICT", "VALUES : dictionnaire invalide" , lng); + List vals = new ArrayList<>(); + for (Iterator e = prop.values().iterator(); e.hasNext();) + vals.add((String)e.next()); + dic = Tools.arrayToString(vals); + } + else if (cmd.equals("EACH")) { + if (getLength() != 4) return cont.erreur("DICT", "EACH : 1 dict., 1 fonction attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "dict each"); + Lecteur lec = new Lecteur(cont); + int nbi = 0; + for (Map.Entry me : prop.entrySet()) { + nbi++; + vars.put("arg1", (String) me.getKey()); + vars.put("arg2", (String) me.getValue()); + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + } + lec.fin(); + dic = Integer.toString(nbi); + } + else if (cmd.equals("APPLY")) { + if (getLength() != 4) return cont.erreur("DICT", "APPLY : 1 dict., 1 bloc attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "dict apply"); + Lecteur lec = new Lecteur(cont); + for (Map.Entry me : prop.entrySet()) { + vars.put("arg1", (String) me.getKey()); + vars.put("arg2", (String) me.getValue()); + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + prop.put(cont.getLecVar("arg1"), cont.getLecVar("arg2")); + } + lec.fin(); + if ((r = dictStore(prop, "APPLY")) >= 0) return r; + } + else if (cmd.equals("FILTER")) { + if (getLength() != 4) return cont.erreur("DICT", "FILTER : 1 dict., 1 bloc attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "dict filter"); + Properties prop2 = new Properties(); + for (Map.Entry me : prop.entrySet()) { + if (ret != RET_CONTINUE) break; + String key = (String) me.getKey(), val = (String) me.getValue(); + vars.put("arg1", key); + vars.put("arg2", val); + int[] re = cont.evaluerBoolLec(cmds, vars); + ret = re[1]; + if (re[0] == 1) prop2.put(key, val); + } + if ((r = dictStore(prop2, "FILTER")) >= 0) return r; + } + else if (cmd.equals("ANY?")) { + if (getLength() != 4) return cont.erreur("DICT", "ANY? : 1 dict., 1 bloc attendus", lng); + dic = Contexte.STATE_FALSE; + Properties prop = Tools.getProp(getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "dict any?"); + for (Map.Entry me : prop.entrySet()) { + if (ret != RET_CONTINUE) break; + String key = (String) me.getKey(), val = (String) me.getValue(); + vars.put("arg1", key); + vars.put("arg2", val); + int[] e = cont.evaluerBoolLec(cmds, vars); + ret = e[1]; + if (e[0] == 1) { + dic = Contexte.STATE_TRUE; + break; + } + } + } + else if (cmd.equals("ALL?")) { + if (getLength() != 4) return cont.erreur("DICT", "ALL? : 1 dict., 1 bloc attendus", lng); + dic = Contexte.STATE_TRUE; + Properties prop = Tools.getProp(getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "dict all?"); + for (Map.Entry me : prop.entrySet()) { + if (ret != RET_CONTINUE) break; + String key = (String) me.getKey(), val = (String) me.getValue(); + vars.put("arg1", key); + vars.put("arg2", val); + int[] e = cont.evaluerBoolLec(cmds, vars); + ret = e[1]; + if (e[0] == 0) { + dic = Contexte.STATE_FALSE; + break; + } + } + } + else if (cmd.equals("LOCAL")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("DICT", "LOCAL : 1 dict., [1 liste] attendus", lng); + Properties prop = Tools.getProp(getArg(2)); + List keep = l == 4 ? Arrays.asList(getArg(3).split(",")) : null; + for (Map.Entry me : prop.entrySet()) { + String key = (String) me.getKey(); + if (keep == null || keep.contains(key)) { // si liste de keys fournie : on ne garde qu'elles + if (cont.valideKey(key)) cont.setLecVar(key, (String) me.getValue()); + else return cont.erreur("DICT", "affectation de variable invalide : " + key, lng); + } + } + } + else return cont.erreur("DICT", cmd + " : commande inconnue", lng); + + cont.setValeur(new ValeurDef(cont, null, dic)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return ret; + } + + private int dictStore(Properties prop, String cmd) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + prop.store(os, null); + dic = "\n" + os.toString(cont.getVar(Contexte.ENV_FILE_ENC)); + // saut de ligne à conserver : pour échapper le # (dégroupeur d'arguments) + return -1; + } catch (IOException ex) { + return cont.erreur("DICT", cmd + " : " + ex.getMessage(), lng); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " dict Outils de traitement de dictionnaires de chaînes\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdDisp.java b/src/lunasql/cmd/CmdDisp.java new file mode 100644 index 0000000..1ef6df7 --- /dev/null +++ b/src/lunasql/cmd/CmdDisp.java @@ -0,0 +1,98 @@ +package lunasql.cmd; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.regex.PatternSyntaxException; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurReq; + +/** + * Commande DISP
+ * (Interne) Listage du contenu d'une table + * @author M.P. + */ +public class CmdDisp extends Instruction { + + public CmdDisp(Contexte cont) { + super(cont, TYPE_CMDINT, "DISP", "\\"); + } + + @Override + public int execute() { + if (getLength() >= 2) { + Lecteur lec = new Lecteur(cont); + String[] usertables = {"TABLE", "GLOBAL TEMPORARY", "VIEW"}; + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + boolean err = false; + int ret = RET_CONTINUE, nbl = 0; + String prem = null; + StringBuilder sb = new StringBuilder(); + + // Environnement + boolean addrownb = cont.getVar(Contexte.ENV_ADD_ROW_NB).equals(Contexte.STATE_TRUE), + colorson = cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE); + + // Boucle sur arguments + for (int i = 1; i < getLength(); i++) { + try { + ResultSet result = dMeta.getTables(null, null, null, usertables); + String tb; + boolean found = false; + while(result.next()) { + tb = result.getString(3); + if (tb.matches("(?i)" + Tools.removeBQuotes(getArg(i)).value)) { + found = true; + sb.append("Table user trouvée : ").append(tb).append('\n'); + SQLCnx.QueryResult v = cont.getConnex().getResultString("SELECT * FROM " + tb, + true, cont.getColMaxWidth(lng), cont.getRowMaxNumber(lng), addrownb, colorson); + sb.append(v.result).append('\n'); + if (v.istrunc) { + if (cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE)) + sb.append("Note : ").append(Contexte.COLORS[Contexte.BR_YELLOW]) + .append("d'autres lignes existent").append(Contexte.COLORS[Contexte.NONE]).append('\n'); + else + sb.append("Note : d'autres lignes existent\n"); + } + if (prem == null) prem = v.premVal; + nbl += v.nbLng; + } + } + if (!found) lec.add("SELECT * FROM " + getArg(i) + ';'); + + } + catch (SQLException ex) { + err = true; + ret = cont.exception("DISP", "ERREUR SQLException : " + ex.getMessage(), lng, ex); + } + catch (PatternSyntaxException ex) { + err = true; + ret = cont.erreur("DISP", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + }// for + lec.fin(); + if (err) return ret; + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + + cont.setValeur(new ValeurReq(cont, sb.toString(), prem, nbl, false)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return lec.getCmdState(); + } + else return cont.erreur("DISP", "un nom de table/vue au moins est requis", lng); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " disp, \\ Affiche le contenu de la (ou les) table(s) en paramètre\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdDrop.java b/src/lunasql/cmd/CmdDrop.java new file mode 100644 index 0000000..13ca72c --- /dev/null +++ b/src/lunasql/cmd/CmdDrop.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande DROP
+ * (SQL) Suppression d'un objet de base de données (table, index, vue...) + * @author M.P. + */ +public class CmdDrop extends Instruction { + + public CmdDrop(Contexte cont){ + super(cont, TYPE_CMDSQL, "DROP", null); + } + + @Override + public int execute() { + return executeUpdate("DROP", "objet supprimé"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " drop (+instr) Supprime un objet de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdEdit.java b/src/lunasql/cmd/CmdEdit.java new file mode 100644 index 0000000..772da57 --- /dev/null +++ b/src/lunasql/cmd/CmdEdit.java @@ -0,0 +1,132 @@ +package lunasql.cmd; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.ui.FrmEditScript; + +/** + * Commande EDITOR
(Interne) Affichage d'un éditeur swing + * @author M.P. + */ +public class CmdEdit extends Instruction { + + private final OptionParser parser; + private FrmEditScript fedit; + private int ret; + + public CmdEdit(Contexte cont) { + super(cont, TYPE_CMDINT, "EDIT", null); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("d", "ouvre l'éditeur système"); + parser.accepts("p", "ouvre l'éditeur en EDITOR_PATH"); + parser.accepts("t", "type de fichier script").withRequiredArg().ofType(String.class) + .describedAs("type"); + parser.nonOptions("fichier").ofType(String.class); + } + + @Override + public int execute() { + if (cont.isHttpMode()) + return cont.erreur("EDIT", "cette commande n'est pas autorisée en mode HTTP", lng); + + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + final Contexte c = cont; + List lf = options.nonOptionArguments(); + if (lf.size() > 1) + return cont.erreur("EDIT", "au maximum un nom de fichier est attendu", lng); + + // Lancement de l'editeur demandé + ret = RET_CONTINUE; + final String fname = (lf.size() == 1 ? (String) lf.get(0) : null); + java.awt.EventQueue.invokeLater(() -> { + File f = null; + if (fname != null) { + f = new File(fname); + if (!f.exists() || !f.canRead()) { + ret = cont.erreur("EDIT", "le fichier '" + f.getAbsolutePath() + "' est inaccessible", lng); + return; + } + } + // Ouverture + if (options.has("d")) { + if (Desktop.isDesktopSupported()) { + try { + Desktop dk = Desktop.getDesktop(); + if (dk.isSupported(Desktop.Action.EDIT)) dk.edit(f); + else ret = cont.erreur("EDIT", "Desktop EDIT non supporté", lng); + } catch (IOException ex) { + ret = cont.exception("EDIT", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } else ret = cont.erreur("EDIT", "Desktop non supporté sur cette plateforme", lng); + } else if (options.has("p")) { + try { + String edt = cont.getVar(Contexte.ENV_EDIT_PATH); + if (edt.isEmpty()) { + ret = cont.erreur("EDIT", "Option " + Contexte.ENV_EDIT_PATH + " non positionnée", lng); + return; + } + if (f == null) Runtime.getRuntime().exec(edt); + else Runtime.getRuntime().exec(new String[]{edt, f.getAbsolutePath()}); + } catch (IOException ex) { + ret = cont.exception("EDIT", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } else { + String type = null; + if (options.has("t")) { + type = (String) options.valueOf("t"); + String[] tsyn = FrmEditScript.getSyntaxes(); + if (!Arrays.asList(tsyn).contains(type)) { + ret = cont.erreur("EDIT", "Type syntaxique non supporté : " + type, lng); + return; + } + } + try { + if (fedit == null) fedit = new FrmEditScript(c, false, type); + fedit.setVisible(true); + if (f != null) fedit.openFile(f); + } catch (HeadlessException ex) { + ret = cont.exception("EDIT", "ERREUR HeadlessException : " + ex.getMessage(), lng, ex); + } + } + }); + cont.setValeur(null); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return ret; + } + catch (OptionException ex) { + return cont.exception("EDIT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex){ + return cont.exception("EDIT", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " edit Ouvre un éditeur de script Java Swing\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdElse.java b/src/lunasql/cmd/CmdElse.java new file mode 100644 index 0000000..f71232d --- /dev/null +++ b/src/lunasql/cmd/CmdElse.java @@ -0,0 +1,47 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande ELSE
+ * (Interne) Conditionnelle ELSE + * @author M.P. + */ +public class CmdElse extends Instruction { + + public CmdElse(Contexte cont){ + super(cont, TYPE_MOTC_WH, "ELSE", null); + } + + @Override + public int execute() { + if (getLength() == 1) { + if (cont.hasIf()) { + cont.invWhen(); + cont.setValeur(null); + return RET_CONTINUE; + } + else return cont.erreur("ELSE", "aucun WHEN correspondant", lng); + } + else return cont.erreur("ELSE", "aucun argument n'est attendu", lng); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return null; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getHelp(){ + return cont.getCommand("WHEN").getHelp(); + } +}// class + diff --git a/src/lunasql/cmd/CmdEnd.java b/src/lunasql/cmd/CmdEnd.java new file mode 100644 index 0000000..5ff66b8 --- /dev/null +++ b/src/lunasql/cmd/CmdEnd.java @@ -0,0 +1,47 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande END
+ * (Interne) Fin de commande conditionnelle IF + * @author M.P. + */ +public class CmdEnd extends Instruction { + + public CmdEnd(Contexte cont){ + super(cont, TYPE_MOTC_WH, "END", null); + } + + @Override + public int execute() { + if (getLength() == 1) { + if (cont.hasIf()) { + cont.endWhen(); + cont.setValeur(null); + return RET_CONTINUE; + } + else return cont.erreur("END", "WHEN/END : aucun WHEN correspondant", lng); + } + else return cont.erreur("END", "aucun argument attendu", lng); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return null; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getHelp(){ + return cont.getCommand("WHEN").getHelp(); + } +}// class + diff --git a/src/lunasql/cmd/CmdEngine.java b/src/lunasql/cmd/CmdEngine.java new file mode 100644 index 0000000..674b018 --- /dev/null +++ b/src/lunasql/cmd/CmdEngine.java @@ -0,0 +1,122 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.val.EvalEngine; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande ENGINE
+ * (Interne) Fixation du moteur d'évaluation de la console + * @author M.P. + */ +public class CmdEngine extends Instruction { + + private final OptionParser parser; + + public CmdEngine(Contexte cont){ + super(cont, TYPE_CMDINT, "ENGINE", "SE"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("l", "liste des engines disponibles"); + parser.nonOptions("engine").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + // Listage de tous les moteurs disponibles + if (options.has("l")) { + int nb = 0; + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : cont.getAllEngines().entrySet()) { + nb++; + ScriptEngineFactory factory = entry.getValue().getEngine().getFactory(); + sb.append("\n Code : ").append(entry.getKey()); + sb.append("\n Nom : ").append(factory.getEngineName()) + .append(" (").append(factory.getEngineVersion()).append(")\n"); + StringBuilder aliases = new StringBuilder(); + for (String n : factory.getNames()) aliases.append(n).append(", "); + aliases.delete(aliases.length() - 2, aliases.length()); + sb.append(" Alias : ").append(aliases.toString()).append('\n'); + sb.append(" Langage : ").append(factory.getLanguageName()).append(" (") + .append(factory.getLanguageVersion()).append(")\n"); + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(nb)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + // Sans argument: affichage de l'engine courant + else if (options.nonOptionArguments().isEmpty()) { + vr = new ValeurDef(cont, "Moteur d'évaluation actuel : " + cont.getEvalEngineName(), + Contexte.VERB_MSG, cont.getEvalEngineAlias()); + cont.setValeur(vr); + return RET_CONTINUE; + } + // Avec un argument: affectation d'un nouveau + else { + List lc = options.nonOptionArguments(); + if (lc.size() > 1) + return cont.erreur("ENGINE", "au maximum un nom de moteur SE est attendu", lng); + + String id = (String) lc.get(0); + ScriptEngine se; + try { + se = cont.getEvalEngine(Integer.parseInt(id)); + } catch (NumberFormatException ex) { + se = cont.getEvalEngine(id); + } + if (se == null) + return cont.erreur("ENGINE", "objet ScriptEngine '" + getArg(1) + "' inaccessible." + + "\nAvez-vous ajouté la bibliothèque correspondante au CLASSPATH ?", lng); + + cont.setEvalEngine(se); + vr.setDispValue("Nouveau moteur EVAL = " + cont.getEvalEngineName(), Contexte.VERB_BVR); + vr.setSubValue(cont.getEvalEngineAlias()); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("ENGINE", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex){ + return cont.exception("ENGINE", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " engine, se Attribue au système un nouveau moteur d'évaluation\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdErr.java b/src/lunasql/cmd/CmdErr.java new file mode 100644 index 0000000..0c8669c --- /dev/null +++ b/src/lunasql/cmd/CmdErr.java @@ -0,0 +1,48 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; + +/** + * Commande en erreur
+ * (Interne) Commande en erreur + * @author M.P. + */ +public class CmdErr extends Instruction { + + protected String msg; + + public CmdErr(Contexte cont){ + super(cont, TYPE_ERR, "ERR", null); + } + public CmdErr(Contexte cont, String name, String alias){ + super(cont, TYPE_ERR, name, alias); + } + + public void setMessage(String msg){ + this.msg = msg; + } + + @Override + public int execute() { + return cont.getVar(Contexte.ENV_EXIT_ERR).equals(Contexte.STATE_TRUE) ? RET_EXIT_SCR : RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return null; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getHelp(){ + return null; + } +} diff --git a/src/lunasql/cmd/CmdErrInc.java b/src/lunasql/cmd/CmdErrInc.java new file mode 100644 index 0000000..53fdcf8 --- /dev/null +++ b/src/lunasql/cmd/CmdErrInc.java @@ -0,0 +1,21 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande en erreur
+ * (Interne) Commande en erreur : inconnue + * @author M.P. + */ +public class CmdErrInc extends CmdErr { + + public CmdErrInc(Contexte cont){ + super(cont, "ERRINC", null); + } + + @Override + public int execute() { + return cont.erreur(getCommandName(), "commande inconnue ou expression invalide" + + (msg != null ? " :\n" + msg : ""), lng); + } +} diff --git a/src/lunasql/cmd/CmdErrSyn.java b/src/lunasql/cmd/CmdErrSyn.java new file mode 100644 index 0000000..929284f --- /dev/null +++ b/src/lunasql/cmd/CmdErrSyn.java @@ -0,0 +1,23 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande en erreur
+ * (Interne) Commande en erreur : syntaxe incorrecte + * @author M.P. + */ +public class CmdErrSyn extends CmdErr { + + public CmdErrSyn(Contexte cont){ + super(cont, "ERRSYN", null); + } + + @Override + public int execute() { + String s = getCommandName(); + return cont.erreur("Lecteur", "erreur de syntaxe (chaîne, parenthèse ou commentaire)" + + (s.length() > 0 ? " : " + getCommandName() : "") + + (msg != null ? " :\n" + msg : ""), lng); + } +} diff --git a/src/lunasql/cmd/CmdError.java b/src/lunasql/cmd/CmdError.java new file mode 100644 index 0000000..83e5b03 --- /dev/null +++ b/src/lunasql/cmd/CmdError.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande ERROR
+ * (SQL) Levée d'une erreur + * @author M.P. + */ +public class CmdError extends Instruction { + + public CmdError(Contexte cont){ + super(cont, TYPE_CMDINT, "ERROR", null); + } + + @Override + public int execute() { + return cont.erreur("ERROR", getSCommand(1), lng); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " error Lève une erreur d'exécution avec message\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdEval.java b/src/lunasql/cmd/CmdEval.java new file mode 100644 index 0000000..e81064e --- /dev/null +++ b/src/lunasql/cmd/CmdEval.java @@ -0,0 +1,132 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import com.sun.corba.se.pept.transport.ContactInfo; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +import javax.naming.Context; + +/** + * Commande EVAL
+ * (Interne) Évaluation d'une expression arithmétique ou d'une commande + * @author M.P. + */ +public class CmdEval extends Instruction { + + private final OptionParser parser; + + public CmdEval(Contexte cont){ + super(cont, TYPE_CMDINT, "EVAL", "="); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("t", "analyse du temps d'exécution du bloc"); + parser.accepts("n", "nom(s) de commande (si -c)").withRequiredArg().ofType(String.class) + .describedAs("noms"); + parser.accepts("c", "code à exécuter si erreur").requiredIf("n") + .withRequiredArg().ofType(String.class).describedAs("code"); + parser.accepts("f", "code à exécuter dans tous les cas (finally)") + .withRequiredArg().ofType(String.class).describedAs("code"); + parser.accepts("v", "dictionnaire de var. locales").withRequiredArg().ofType(String.class) + .describedAs("vars"); + parser.nonOptions("nom_var chaîne_à_évaluer").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lc = options.nonOptionArguments(); + if (lc.isEmpty()) + return cont.erreur("EVAL", "une commande (ou un bloc de code) est attendue", lng); + + // Variables locales + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "eval"); + if (options.has("v")) { // dict de vars fourni + Properties prop = Tools.getProp((String)options.valueOf("v")); + if (prop == null) return cont.erreur("EVAL", "dictionnaire invalide" , lng); + for (Map.Entry me : prop.entrySet()) { + String key = (String) me.getKey(), val = (String) me.getValue(); + if (cont.valideKey(key)) cont.setLecVar(key, val); + else return cont.erreur("EVAL", "affectation de variable invalide : " + key, lng); + } + } + if (options.has("c")) { // bloc code err fourni + vars.put(Contexte.LEC_CATCH_CODE, (String)options.valueOf("c")); + vars.put(Contexte.LEC_CATCH_LCMD, options.has("n") ? (String)options.valueOf("n") : ""); + } + + // Exécution sans retour (à charge de la commande) + int ret; + long tm = 0L; + boolean chrono = options.has("t"); + if (chrono) tm = System.currentTimeMillis(); + cont.addSubMode(); + Lecteur lec = new Lecteur(cont, listToString(lc), vars); + ret = lec.getCmdState() == RET_EV_CATCH ? RET_CONTINUE : lec.getCmdState(); + cont.remSubMode(); + if (chrono && cont.getVerbose() >= Contexte.VERB_AFF) { + tm = System.currentTimeMillis() - tm; + double nb = 5000.0 / tm; + // Affichage sans valeur de retour + String s = "-> temps d'exécution : " + SQLCnx.frmDur(tm) + " (" + String.format("%.2f", nb) + + " fois en 5 s)"; + Valeur vr = cont.getValeur(); + if (vr == null) vr = new ValeurDef(cont, s, null); + else vr.setDispValue(s); + cont.setValeur(vr); + } + + // Exécution du bloc finally (dans tous les cas) + if (options.has("f")) { // finally + vars.put(Contexte.LEC_SUPER, "eval:finally"); + vars.remove(Contexte.LEC_CATCH_CODE); + vars.remove(Contexte.LEC_CATCH_LCMD); + cont.addSubMode(); + Lecteur lec2 = new Lecteur(cont, (String)options.valueOf("f"), vars); + cont.remSubMode(); + if (ret == RET_CONTINUE) ret = lec2.getCmdState(); + } + return ret; + } + catch (OptionException ex) { + return cont.exception("EVAL", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex){ + return cont.exception("EVAL", "Erreur IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " eval, = Évalue une commande en espace confiné\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdExec.java b/src/lunasql/cmd/CmdExec.java new file mode 100644 index 0000000..bb9ea7c --- /dev/null +++ b/src/lunasql/cmd/CmdExec.java @@ -0,0 +1,392 @@ +package lunasql.cmd; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.zip.GZIPInputStream; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import lunasql.Config; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Security; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande EXEC
(Interne) Exécution d'un fichier de commandes + * @author M.P. + */ +public class CmdExec extends Instruction { + + private static int deep = 0; // compteur d'appels sur la pile + + public CmdExec(Contexte cont) { + super(cont, TYPE_CMDINT, "EXEC", "@"); + } + + public CmdExec(Contexte cont, String name, String alias) { + super(cont, TYPE_CMDINT, name, alias); + } + + @Override + public int execute() { + // Contrôle de la profondeur de la pile pour éviter StackOverflow + if (deep > Config.CF_MAX_CALL_DEEP) { + cont.erreur("EXEC", "Halte là ! Exécution circulaire irréfléchie dépassant " + + Config.CF_MAX_CALL_DEEP + " appels\n" + + "(sachez ce que vous faites quand vous jouez avec la récursivité !)", lng); + return RET_EXIT_SCR; // sortie script obligatoire + } + + if (getLength() < 2) + return cont.erreur("EXEC", "un nom de fichier SQL ou ScriptEngine au moins est requis\n" + + "\t\tou bien en ligne commandes : - (sql) ou + (SE)", lng); + + URL purl = null; + boolean cry = false, zip = false, jar = false; + String sname = getArg(1), pname = null, ext; + + if (sname.equals("-")) ext = "SQL"; // commande SQL en ligne + else if (sname.startsWith("+")) { + ext = sname.length() == 1 ? cont.getEvalEngineExtens() : sname.substring(1); + } // commande SE en ligne + else { + // Extraction de l'extension ext + ext = cont.getEvalEngineExtens(); // Javascript par défaut + int id = sname.lastIndexOf('.'); + if (id > 0) { + ext = sname.substring(id + 1).toUpperCase(); + if (ext.equals("GZ")) { // mise au format GZip + sname = sname.substring(0, id); + zip = true; + } + else if (ext.equals("CRY")) { + sname = sname.substring(0, id); + cry = true; + } + } + id = sname.lastIndexOf('.'); + if (id > 0 && id < sname.length() - 1) ext = sname.substring(id + 1); + if (zip) sname += ".gz"; + else if (cry) sname += ".cry"; + + // Détermination du chemin absolu pname + if (sname.matches("^jar:.*$")) { // resource en jar + sname = sname.substring(4); + pname = sname; + jar = true; + } else if (sname.matches("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]$")) { + pname = sname; + try { + purl = new URL(pname); + } catch (MalformedURLException ex) { + return cont.exception("EXEC", "ERREUR MalformedURLException : " + ex.getMessage(), lng, ex); + } + } else { + File fname = new File(sname); + if (fname.isFile()) pname = sname; // si script existe en rép., on le prend + else { // sinon, on va à la pêche dans ENV_SCR_PATH + String[] path = cont.getVar(Contexte.ENV_SCR_PATH).split(File.pathSeparator); + // Parcours du path, on s'arrête au 1er fichier trouvé + boolean hasf = false; + for (int i = 0; i < path.length && !hasf; i++) { + if (!path[i].endsWith(File.separator)) path[i] += File.separator; + fname = new File(path[i] + sname); + if (fname.isFile()) { // Existe + est un fichier + pname = fname.getAbsolutePath(); + hasf = true; + } + } + if (!hasf) { // aucun script trouvé + StringBuilder ss = new StringBuilder(); + ss.append("\n - ").append(new File(".").getAbsolutePath()); + for (String p : path) ss.append("\n - ").append(p); // Path suivant + return cont.erreur("EXEC", "aucun script '" + sname + "' trouvé dans les répertoires :" + + ss.toString(), lng); + } + } + try { + // Vérification de la signature du fichier + if (!verifySign(fname, zip)) + return cont.erreur("EXEC", "signature erronée du script '" + sname + "'", lng); + } catch (IllegalArgumentException ex) { + return cont.erreur("EXEC", "signature invalide du script '" + sname + + "' (" + ex.getMessage() + ")", lng); + } catch (IOException|NoSuchAlgorithmException|InvalidKeyException| + SignatureException ex) { + return cont.erreur("EXEC", "ERREUR" + ex.getClass().getSimpleName() + " : " + ex.getMessage(), + lng); + } + } + } + + /* + * Script LunaSQL (extension SQL) + */ + if (ext.matches(Config.CT_SQL_EXT)) { + deep++; + Valeur vr = new ValeurDef(cont); + Lecteur lec = null; + int ret = RET_CONTINUE; + if (sname.equals("-")) { // en ligne de commande + lec = new Lecteur(cont, getArg(2), setLecVars(3)); + Valeur vr2 = cont.getValeur(); + vr.setSubValue(vr2 == null ? null : vr2.getSubValue()); + } + else { // en fichier + InputStream inp = null; + try { + HashMap vars = setLecVars(cry ? 3 : 2); + vars.put(Contexte.LEC_SCR_NAME, sname); + vars.put(Contexte.LEC_THIS, sname); + vars.put(Contexte.LEC_SUPER, ""); + vars.put(Contexte.LEC_SCR_PATH, pname); + + if (purl == null) { + if (jar) { + inp = getClass().getResourceAsStream(sname); + if (inp == null) + return cont.erreur("EXEC", "Ressource jar '" + sname + "' non disponible", lng); + } + else inp = new FileInputStream(pname); + } + else inp = purl.openStream(); + + if (cry) { + if (getLength() < 3) + return cont.erreur("EXEC", "avec script chiffré, clef attendue en 1er argument", lng); + + byte[] iv = new byte[16], key = Security.hexdecode(getArg(2)); + if (key == null) + return cont.erreur("EXEC", "clef de déchiffrement fournie nulle", lng); + + inp.read(iv); + Cipher c = getCipher(key, iv); + inp = new CipherInputStream(inp, c); + } + if (zip || cry) inp = new GZIPInputStream(inp); + + long tm = System.currentTimeMillis(); + lec = new Lecteur(cont, new InputStreamReader(inp, cont.getVar(Contexte.ENV_FILE_ENC)), vars); + tm = System.currentTimeMillis() - tm; + vr.setDispValue("-> script '" + pname + "' exécuté en " + SQLCnx.frmDur(tm), Contexte.VERB_BVR); + Valeur vr2 = cont.getValeur(); + vr.setSubValue(vr2 == null ? null : vr2.getSubValue()); + } catch (IOException|NoSuchPaddingException|NoSuchAlgorithmException|InvalidAlgorithmParameterException| + InvalidKeyException ex) { + ret = cont.erreur("EXEC", "ERREUR" + ex.getClass().getSimpleName() + " : " + ex.getMessage(), + lng); + } + finally { + try { + if (inp != null) inp.close(); + } + catch (IOException e) {} + } + } + vr.setRet(); + cont.setValeur(vr); + deep--; + return lec == null ? ret : lec.getCmdState(); + + /* + * Script ScriptEngine + */ + } + else { + Valeur vr = new ValeurDef(cont); + ScriptEngine se = cont.getEvalEngine(ext); + if (se == null) + return cont.erreur("EXEC", "Objet ScriptEngine '" + ext + "' inaccessible." + + "\nAvez-vous ajouté la bibliothèque correspondante au CLASSPATH ?", lng); + + se.put("sql_connex", cont.getConnex()); + InputStream inp = null; + int ret = RET_CONTINUE; + try { + long tm = System.currentTimeMillis(); + Object o; + if (sname.startsWith("+")) { // en ligne de commande + se.put(Contexte.LEC_SCR_ARGS, getCommand(3)); + o = se.eval(getArg(2)); + } + else { // en fichier + se.put(Contexte.LEC_SCR_NAME, sname); + se.put(Contexte.LEC_SCR_PATH, pname); + se.put(Contexte.LEC_SCR_ARGS, getCommand(cry ? 3 : 2)); // si chiffré, clef en arg 2 + + if (purl == null) { + if (jar) { + inp = getClass().getResourceAsStream(sname); + if (inp == null) + return cont.erreur("EXEC", "Ressource jar '" + sname + "' non disponible", lng); + } + else inp = new FileInputStream(pname); + } + else inp = purl.openStream(); + + if (cry) { + if (getLength() < 3) + return cont.erreur("EXEC", "avec script chiffré, clef attendue en 1er argument", lng); + + byte[] iv = new byte[16], key = Security.hexdecode(getArg(2)); + if (key == null) + return cont.erreur("EXEC", "clef de déchiffrement fournie nulle", lng); + + inp.read(iv); + Cipher c = getCipher(key, iv); + inp = new CipherInputStream(inp, c); + } + if (zip || cry) inp = new GZIPInputStream(inp); + o = se.eval(new InputStreamReader(inp, cont.getVar(Contexte.ENV_FILE_ENC))); + } + if (o != null) { + vr.setSubValue(o.toString()); + if (o instanceof Integer) + vr.setDispValue("Objet entier returné : " + o); + else if (o instanceof Double) + vr.setDispValue("Objet double returné : " + o); + else if (o instanceof String) + vr.setDispValue("Objet chaîne returné : " + o); + else + vr.setDispValue("Objet returné (" + o.getClass().toString() + ") :\n" + o.toString()); + } + tm = System.currentTimeMillis() - tm; + vr.setDispValue("-> script '" + pname + "' exécuté en " + SQLCnx.frmDur(tm), + Contexte.VERB_BVR); + vr.setRet(); + cont.setValeur(vr); + } + catch (ScriptException ex) { + ret = cont.erreur("EXEC", "Erreur ScriptException :\n" + + ex.getMessage().replace(":", ":\n"), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("EXEC", ex.getMessage(), lng); + } + catch (IOException|NoSuchPaddingException|NoSuchAlgorithmException|InvalidAlgorithmParameterException| + InvalidKeyException ex) { + ret = cont.exception("EXEC", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + catch (Exception ex) { + ret = cont.exception("EXEC", "ERREUR Runtime : " + ex.getMessage(), lng, ex); + } + finally { + try { if (inp != null) inp.close(); } + catch (IOException ex) {} + } + return ret; + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " exec, @ Exécute le fichier de commandes SQL ou SE en paramètre\n"; + } + + /** + * Affectation des paramètres en mode SQL + */ + private HashMap setLecVars(int n) { + HashMap vars = new HashMap<>(); + StringBuilder args = new StringBuilder(); + vars.put(Contexte.LEC_SCR_ARG_NB, Integer.toString(getLength() - n)); + for (int i = n; i < getLength(); i++) { + vars.put(Contexte.LEC_SCR_ARG + (i - n + 1), getArg(i)); + args.append(getArg(i)).append(' '); + } + if (args.length() > 1) args.deleteCharAt(args.length() - 1); + vars.put(Contexte.LEC_SCR_ARG_LS, args.toString()); + return vars; + } + + /** + * Préparation du chiffre + */ + private Cipher getCipher(byte[] bkey, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(bkey, "AES"), new IvParameterSpec(iv)); + return c; + } + + /** + * Vérification de la signature du fichier + * @param f le fichier + * @param zip si le fichier est zippé (Gzip) + * @return true si signature ok, false sinon + */ + private boolean verifySign(File f, boolean zip) throws IOException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException { + String policy = cont.getVar(Contexte.ENV_SIGN_POLICY); + if (policy.equals("0")) return true; + + Security sec = new Security(); + String ms, pk; + File fsg = new File(f.getAbsolutePath() + ".sig"); + if (fsg.exists()) { // external signature + StringBuilder sbsg = new StringBuilder(); + for (String line : Files.readAllLines(fsg.toPath())) { + if (!(line = line.trim()).startsWith("#")) sbsg.append(line); + } + byte[] totsg = Security.b64decode(sbsg.toString()); + if (totsg == null || totsg.length != 102) // 32 + 6 + 64 + throw new IllegalArgumentException("signature nulle ou invalide"); + + byte[] bpk = Arrays.copyOfRange(totsg, 0, 32), bms = Arrays.copyOfRange(totsg, 32, 38), + bsig = Arrays.copyOfRange(totsg, 38, 102); + sec.setPublicKey(bpk); + if (!sec.verify(f, bms, bsig)) return false; + pk = Security.b64encode(bpk); + ms = Security.hexencode(bms); + } + else { // embedded signature + String[] pkarr = new String[2]; + if (zip) { + try (GZIPInputStream in = new GZIPInputStream(new FileInputStream(f)); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + int nb; + byte[] buffer = new byte[1024]; + while ((nb = in.read(buffer, 0, buffer.length)) > 0) out.write(buffer, 0, nb); + byte[] content = out.toByteArray(); + if (!sec.verifyPk(content, pkarr)) return false; + } + } + else if (!sec.verifyPk(f, pkarr)) return false; + pk = pkarr[0]; + ms = pkarr[1]; + } + cont.printSignInfos(pk, ms); + + // Confiance dans la clef + String[] tkey = cont.getVar(Contexte.ENV_SIGN_KEY).split("\\|"); + if (tkey.length < 2 || !tkey[0].equals(pk)) { + if (cont.getVar(Contexte.ENV_SIGN_TRUST).contains(pk)) return true; + if (policy.equals("1")) cont.errprintln("Note : signature du script valide mais clef non fiable"); + else return false; + } + return true; + } +}// class + diff --git a/src/lunasql/cmd/CmdExit.java b/src/lunasql/cmd/CmdExit.java new file mode 100644 index 0000000..6264858 --- /dev/null +++ b/src/lunasql/cmd/CmdExit.java @@ -0,0 +1,46 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande EXIT
+ * (Interne) Interruption de l'exécution d'un fichier de commandes + * @author M.P. + */ +public class CmdExit extends Instruction { + + public CmdExit(Contexte cont) { + super(cont, TYPE_CMDINT, "EXIT", null); + } + + @Override + public int execute() { + int errno = 0; + if (getLength() >= 2) { + try { + errno = Integer.parseInt(getArg(1)); + } + catch (NumberFormatException ex) {// erreur prévisible > cont.erreur + cont.erreur("EXIT", "valeur de sortie non numérique : " + getArg(1), lng); + } + } + if (cont.getVerbose() >= Contexte.VERB_AFF && getLength() >= 3) { + if (errno == 0) cont.println(getSCommand(2)); + else cont.errprintln(getSCommand(2)); // errprintln et non erreur + } + cont.setValeur(new ValeurDef(cont, null, Integer.toString(errno))); + cont.setVar(Contexte.ENV_CMD_STATE, errno == 0 ? Contexte.STATE_FALSE : Contexte.STATE_ERROR); + return RET_EXIT_SCR; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " exit Sort du fichier de commandes SQL en cours\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdExplain.java b/src/lunasql/cmd/CmdExplain.java new file mode 100644 index 0000000..e2b3aae --- /dev/null +++ b/src/lunasql/cmd/CmdExplain.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande EXPLAIN
+ * (SQL) Détail de l'exécution d'une requête + * @author M.P. + */ +public class CmdExplain extends Instruction { + + public CmdExplain(Contexte cont){ + super(cont, TYPE_CMDSQL, "EXPLAIN", null); + } + + @Override + public int execute() { + return executeCall("EXPLAIN", "exécution terminée"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " explain (+instr) Détaille l'exécution d'une requête\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdExport.java b/src/lunasql/cmd/CmdExport.java new file mode 100644 index 0000000..7d13f62 --- /dev/null +++ b/src/lunasql/cmd/CmdExport.java @@ -0,0 +1,524 @@ +package lunasql.cmd; + +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.regex.PatternSyntaxException; +import java.util.zip.GZIPOutputStream; + +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Security; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.ValeurDef; +import opencsv.CSVWriter; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +//import jline.console.ConsoleReader; + +/** + * Commande EXPORT
+ * (Interne) Exportation des données d'une table en fichier SQL, CSV, TXT, XML ou HTML + * @author M.P. + */ +public class CmdExport extends Instruction { + + private final OptionParser parser; + + public CmdExport(Contexte cont) { + super(cont, TYPE_CMDINT, "EXPORT", "EX"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("e", "extension (avec -f)").withRequiredArg().ofType(String.class) + .describedAs("ext"); + parser.accepts("f", "recherche de tables sur patron").requiredIf("e") + .withRequiredArg().ofType(String.class) + .describedAs("pattern"); + parser.accepts("k", "clef de chiffrement (si ext. cry)").withRequiredArg().ofType(String.class) + .describedAs("key"); + parser.accepts("o", "options d'export CSV").withRequiredArg().ofType(String.class) + .describedAs("qut,esc"); + parser.accepts("c", "exporte dans le presse-papier"); + parser.accepts("s", "exporte aussi la structure"); + parser.accepts("n", "valeurs nulles représentées par "); + parser.nonOptions("fichier table").ofType(String.class); + } + + @Override + public int execute() { // TODO: à réduire en utilisant des méthodes + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + boolean fltmod; // mode filtre de fichier + String fname, fbs, ext, ext1 = null; + String[] tables; + List lf = options.nonOptionArguments(); + // Option -find : modèle de nom de fichier + if (options.has("f")) { + fltmod = true; + String ftb = (String) options.valueOf("f"); + fname = Tools.removeBQuotes(ftb).value; + ext1 = options.has("e") ? (String) options.valueOf("e") : "csv"; + List ltbs = new ArrayList<>(); + try { + String[] usertables = {"TABLE", "GLOBAL TEMPORARY", "VIEW"}; + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + ResultSet result = dMeta.getTables(null, null, null, usertables); + String table; + while (result.next()) { + table = result.getString(3); + if (table.matches("(?i)" + fname)) ltbs.add(table); + } + result.close(); + } + catch (SQLException ex) { + return cont.exception("EXPORT", "ERREUR SQLException : " + ex.getMessage(), lng, ex); + } + catch (PatternSyntaxException ex) { + return cont.erreur("EXPORT", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + if (ltbs.isEmpty()) + return cont.erreur("EXPORT", "aucune table ne correspond au filtre : " + ftb, lng); + tables = ltbs.toArray(new String[]{}); + } + else { + if (lf.size() < 1) + return cont.erreur("EXPORT", + "un nom de fichier et une table/requête à exporter sont attendus", lng); + + fltmod = false; + tables = new String[]{ (String) lf.get(0) }; + } + + // Boucle de parcours des tables sélectionnées + int nbltot = 0; + StringBuilder ccl = new StringBuilder(); + for (String tb : tables) { + fname = fltmod ? tb + "." + ext1 : tb; + + boolean zip = false, cry = false, clip = options.has("c"); + if (clip) { + fbs = fname; + ext = ""; + } + else { + int id = fname.lastIndexOf('.'); + if (id > 0) { + ext = fname.substring(id + 1).toUpperCase(); + if (ext.equals("GZ")) { // mise au format GZip + fname = fname.substring(0, id); + zip = true; + } + else if (ext.equals("CRY")) { // chiffrement des données + fname = fname.substring(0, id); + cry = true; + } + } + else fname += ".csv"; + + // Extension normale + id = fname.lastIndexOf('.'); + if (id > 0 && id < fname.length() - 1) { + fbs = fname.substring(0, id).toUpperCase(); + ext = fname.substring(id + 1).toUpperCase(); + } + else { + fbs = fname; + ext = ""; + } + // Path separator + id = fbs.lastIndexOf(File.separatorChar); + if (id >= 0 && id < fbs.length() - 1) fbs = fbs.substring(id + 1); + + if (!ext.equals("SQL") && !ext.equals("CSV") && !ext.equals("TXT") + && !ext.equals("XML") && !ext.equals("HTML")) { + return cont.erreur("EXPORT", "format de fichier non supporté : " + + (ext.length() == 0 ? "(ext. vide)" : ext), lng); + } + } + + // Vérification de la clef de chiffrement en mode cry + byte[] bkey = null; + if (cry) { + if (!options.has("k")) + return cont.erreur("EXPORT", "option -k (clef) requise si format .cry", lng); + + try { + bkey = Security.hexdecode((String) options.valueOf("k")); + if (bkey == null) + return cont.erreur("EXPORT", "clef de déchiffrement fournie nulle", lng); + } catch (IllegalArgumentException ex) { + return cont.erreur("EXPORT", ex.getMessage(), lng); + } + } + + File file = null; + if (!clip) { + file = new File(fname + (zip ? ".gz" : (cry ? ".cry" : ""))); + if (!cont.askWriteFile(file)) { + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + } + + // Préparation de la table et requête d'insertion + long tm = System.currentTimeMillis(); + String req1, table; + if (fltmod || lf.size() == 1) { + table = fbs; + req1 = "SELECT * FROM " + table; + } + else { + req1 = listToString(lf, clip ? 0 : 1).trim(); + int j = req1.indexOf(" "); + table = (j > 1 ? req1.substring(0, j) : req1); + // TODO: faire une vraie analyse grâce à JSQLFarceur (si présent en CLASSPATH) + // problème: JSQLFarceur ne prend pas en charge CALL ou SELECT . + if (table.equalsIgnoreCase("SELECT")) { + j = req1.toUpperCase().indexOf(" FROM "); + if (j < 7) return cont.erreur("EXPORT", "format de requête d'export incorrect", lng); + int k = req1.toUpperCase().indexOf(" WHERE "); + table = (k > j ? req1.substring(j + 6, k) : req1.substring(j + 6)); + } + else { + req1 = "SELECT * FROM " + table; + } + } + + // Exécution de la requête + OutputStream outp = null; + int ret = RET_CONTINUE; + boolean err = false; + try { + if (cont.getVerbose() >= Contexte.VERB_DBG) cont.println("Requête : " + req1); + ResultSet rs = cont.getConnex().getResultset(req1); + ResultSetMetaData rsm = rs.getMetaData(); + int nbCol = rsm.getColumnCount(), nbl = 0; + boolean shownulls = options.has("n"); + + // Export format SQL + if (ext.equals("SQL")) { + if (!table.matches("[A-Za-z_][A-Za-z0-9._]*")) // nom de table unique ou composé + return cont.erreur("EXPORT", "nom de table incorrect pour un export SQL", lng); + outp = new FileOutputStream(file); + if (cry) { + byte[] iv = new byte[16]; + Cipher c = getCipher(bkey, iv); + outp.write(iv); + outp = new CipherOutputStream(outp, c); + } + if (zip || cry) outp = new GZIPOutputStream(outp, 4096); // chiffré est aussi zippé + PrintStream writer = new PrintStream(outp, false, cont.getVar(Contexte.ENV_FILE_ENC)); + StringBuilder s = new StringBuilder(); + s.append("/* ***************************************************************************** *"); + s.append("\n * Table exportée : ").append(table); + s.append("\n * Requête d'exportation : ").append(req1); + s.append("\n * Nom de base de données : ").append(cont.getConnex().getBase()); + s.append("\n * Date d'exportation : ").append(new java.util.Date().toString()); + s.append("\n * ***************************************************************************** */\n\n"); + writer.print(s.toString()); + s.setLength(0); + + // Construction de la requête avec noms de colonnes + s.append("INSERT INTO ").append(table).append(" ("); + for (int j = 1; j <= nbCol; j++) s.append(rsm.getColumnName(j)).append(','); + s.deleteCharAt(s.length() - 1).append(") VALUES ("); + String c = s.toString(); + s.setLength(0); + + // Exportation + if (options.has("s")) { + // Suppression de la table + s.append("DROP TABLE ").append(table).append(";\n\n"); + // Création de la table + s.append("CREATE TABLE ").append(table).append(" (\n"); + for (int j = 1; j <= nbCol; j++) { + s.append(" ").append(rsm.getColumnName(j)); // Nom de colonne + s.append(' ').append(rsm.getColumnTypeName(j)); // Type + s.append('(').append(rsm.getPrecision(j)).append(')'); // Taille + if (j < nbCol) s.append(','); + s.append('\n'); + } + s.append(");\n\n"); + } + // Suppression des données + s.append("DELETE FROM ").append(table).append(";\n\n"); + // Parcours des lignes de la table + while (rs.next()) { // pour chaque ligne + s.append(c); + for (int j = 1; j <= nbCol; j++) { // pour chaque colonne + String r = rs.getString(j); + if (r == null) { + s.append("NULL"); + } + else { + switch (rsm.getColumnType(j)) { + case Types.BOOLEAN: + case Types.BIT: + case Types.DECIMAL: + case Types.NUMERIC: + case Types.INTEGER: + case Types.BIGINT: + case Types.FLOAT: + case Types.DOUBLE: + case Types.REAL: + case Types.SMALLINT: + case Types.TINYINT: + case Types.NULL: + s.append(r); + break; + default: + s.append('\''); + s.append(r.replace("'", "''").replace("\n", "$")); + s.append('\''); + break; + } + } + s.append(','); + } + s.deleteCharAt(s.length() - 1).append(");\n"); + nbl++; + } + s.append("-- Nombre de lignes : ").append(nbl).append("\n\n"); + writer.print(s.toString()); + writer.close(); + } // SQL + + else if(ext.equals("HTML")) { // Export format : HTML + outp = new FileOutputStream(file); + if (cry) { + byte[] iv = new byte[16]; + Cipher c = getCipher(bkey, iv); + outp.write(iv); + outp = new CipherOutputStream(outp, c); + } + if (zip || cry) outp = new GZIPOutputStream(outp, 4096); + PrintStream writer = new PrintStream(outp, false, cont.getVar(Contexte.ENV_FILE_ENC)); + // introduction + writer.print("\n\n"); + writer.print("\nExportation requête LunaSQL\n\n\n"); + writer.print("

\n\n"); + writer.print("\n"); + writer.print("\n"); + writer.print("\n"); + writer.print("\n"); + writer.print("
Table exportée :" + table + "
Requête d'exportation :" + req1 + "
Nom de base de données :" + cont.getConnex().getBase() + "
Date d'exportation :" + new java.util.Date().toString() + "
\n

\n"); + writer.print("

\n\n"); + // table header + writer.print(""); + for (int j = 0; j < nbCol; j++) + writer.print("\n"); + writer.println(""); + // the data + while (rs.next()) { + nbl++; + writer.print(""); + for (int j = 1; j <= nbCol; j++) { + String value = rs.getString(j); + writer.print(""); + } + writer.println(""); + } + // conclusion + writer.print("
" + rsm.getColumnLabel(j + 1) + "
" + (value == null ? (shownulls ? "" : "") : value) + "
\n

\n"); + writer.print("

Nombre de lignes : " + nbl + "

"); + writer.print("\n\n"); + writer.close(); + }// HTML + + else if(ext.equals("XML")) { // Export format : XML + // Création du document + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.newDocument(); + Element results = doc.createElement(table + "_" + + new SimpleDateFormat("yyyyMMdd").format(new Date())); + doc.appendChild(results); + + // Lecture des données à exporter + while (rs.next()) { + Element row = doc.createElement("record"); + row.setAttribute("rownum", Integer.toString(++nbl)); + results.appendChild(row); + for (int j = 1; j <= nbCol; j++) { + String columnName = rsm.getColumnName(j), value = rs.getString(j); + Element node = doc.createElement(columnName); + node.appendChild(doc.createTextNode(value == null ? (shownulls ? "" : "") : value)); + row.appendChild(node); + } + } + rs.close(); + DOMSource domSource = new DOMSource(doc); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer trans = tf.newTransformer(); + trans.setOutputProperty(OutputKeys.VERSION, "1.0"); + trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + trans.setOutputProperty(OutputKeys.METHOD, "xml"); + trans.setOutputProperty(OutputKeys.ENCODING, cont.getVar(Contexte.ENV_FILE_ENC)); + trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + + // Génération du fichier xml + outp = new FileOutputStream(file); + if (cry) { + byte[] iv = new byte[16]; + Cipher c = getCipher(bkey, iv); + outp.write(iv); + outp = new CipherOutputStream(outp, c); + } + if (zip || cry) outp = new GZIPOutputStream(outp, 4096); + trans.transform(domSource, new StreamResult(outp)); + } // XML + + else { // Export format autres : CSV, TXT + // Options d'export + String[] csvopt; // 0:nol 1:qut 2:esc + if (options.has("o")) { + csvopt = ((String) options.valueOf("o")).split(","); + if (csvopt.length != 2 || csvopt[0].length() != 1 || csvopt[1].length() != 1) + return cont.erreur("EXPORT", "format des options d'export incorrect", lng); + } + else csvopt = new String[]{"\"", "\\"}; + + char sep = (clip || ext.equals("TXT") ? '\t' : ';'); + outp = clip ? new ByteArrayOutputStream() : new FileOutputStream(file); + if (cry) { + byte[] iv = new byte[16]; + Cipher c = getCipher(bkey, iv); + outp.write(iv); + outp = new CipherOutputStream(outp, c); + } + if (zip || cry) outp = new GZIPOutputStream(outp, 4096); + CSVWriter writer = new CSVWriter(new OutputStreamWriter(outp), + sep, csvopt[0].charAt(0), csvopt[1].charAt(0), "\n"); + String[] row = new String[nbCol]; + // Ecriture des noms de colonnes + for (int j = 0; j < nbCol; j++) row[j] = rsm.getColumnName(j + 1); + writer.writeNext(row); + // Ecriture des lignes + String v; + while (rs.next()){ + for (int j = 0; j < nbCol; j++) { + v = rs.getString(j + 1); + row[j] = v == null ? (shownulls ? "" : "") : v; + } + writer.writeNext(row); + nbl++; + } + //writer.writeAll(rs, true); + writer.close(); + if (clip){ + StringSelection ss = new StringSelection + (((ByteArrayOutputStream) outp).toString(cont.getVar(Contexte.ENV_FILE_ENC))); + Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard(); + c.setContents(ss, ss); + } + } + rs.close(); + + // Fermeture et Conclusion + tm = System.currentTimeMillis() - tm; + if (clip) ccl.append("Ecriture en presse-papier terminée\n"); + else ccl.append("Fichier '").append(file.getCanonicalPath()).append("' exporté\n"); + ccl.append("-> ").append((cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE) ? + Contexte.COLORS[Contexte.BR_CYAN] + nbl + Contexte.COLORS[Contexte.NONE] : nbl)) + .append(" ligne").append(nbl > 1 ? "s" : "") + .append(" écrite").append(nbl > 1 ? "s" : "") + .append(zip ? ", format zippé" : (cry ? ", format chiffré" : "")) + .append(" (").append(SQLCnx.frmDur(tm)).append(")"); + cont.setVar(Contexte.ENV_RET_NLINES, Integer.toString(nbl)); + nbltot += nbl; + } + catch (SQLException|ParserConfigurationException|TransformerException|NoSuchPaddingException| + NoSuchAlgorithmException|InvalidKeyException|InvalidAlgorithmParameterException ex) { + ret = cont.exception("EXPORT", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + err = true; + } + finally { + if (outp != null) outp.close(); + } + if (err) return ret; + }// for fnames + + cont.setValeur(new ValeurDef(cont, ccl.toString(), Integer.toString(nbltot), false)); + cont.setVar(Contexte.ENV_CMD_STATE, nbltot > 0 ? Contexte.STATE_TRUE : Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("EXPORT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("EXPORT", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + + /** + * Préparation du chiffre + */ + private Cipher getCipher(byte[] bkey, byte[] outiv) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + new SecureRandom().nextBytes(outiv); + c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(bkey, "AES"), new IvParameterSpec(outiv)); + return c; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " export, ex Exporte en fichier la table en paramètre selon format\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdFile.java b/src/lunasql/cmd/CmdFile.java new file mode 100644 index 0000000..564364c --- /dev/null +++ b/src/lunasql/cmd/CmdFile.java @@ -0,0 +1,667 @@ +package lunasql.cmd; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Properties; +import java.util.regex.PatternSyntaxException; +import java.util.zip.CRC32; +import java.util.zip.Checksum; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Security; +import lunasql.lib.Tools; +import lunasql.val.ValeurDef; + +/** + * Commande FILE
+ * (Interne) Commande d'utilitaires d'ouverture/fermeture de fichiers + * @author M.P. + */ +public class CmdFile extends Instruction { + + private String fil; + private int nbl; + + public CmdFile(Contexte cont){ + super(cont, TYPE_CMDINT, "FILE", null); + } + + @Override + public int execute() { + + if (getLength() < 2) + return cont.erreur("FILE", "une commande de fichier est requise", lng); + + int r; + String cmd = getArg(1).toUpperCase(), aff = null; + int ret = RET_CONTINUE; + fil = ""; nbl = 0; + + if (cmd.equals("EXISTS?")) { + if (getLength() != 3) return cont.erreur("FILE", "EXISTS? : 1 nom de fichier attendu", lng); + fil = new File(getArg(2)).exists() ? "1" : "0"; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("FILE?")) { + if (getLength() != 3) return cont.erreur("FILE", "FILE? : 1 nom de fichier attendu", lng); + fil = new File(getArg(2)).isFile() ? "1" : "0"; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("DIR?")) { + if (getLength() != 3) return cont.erreur("FILE", "DIR? : 1 nom de fichier attendu", lng); + fil = new File(getArg(2)).isDirectory() ? "1" : "0"; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("ABSOL?")) { + if (getLength() != 3) return cont.erreur("FILE", "ABSOL? : 1 nom de fichier attendu", lng); + fil = new File(getArg(2)).isAbsolute() ? "1" : "0"; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("ABSOL")) { + if (getLength() != 3) return cont.erreur("FILE", "ABSOL : 1 nom de fichier attendu", lng); + try { + fil = new File(getArg(2)).getCanonicalPath(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (IOException ex) { + return cont.erreur("FILE", "ABSOL : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("NAME")) { + if (getLength() != 3) return cont.erreur("FILE", "NAME : 1 nom de fichier attendu", lng); + fil = new File(getArg(2)).getName(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("PWD")) { + try { + fil = new File(".").getCanonicalPath(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (IOException ex) { + return cont.erreur("FILE", "PWD : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("PARENT")) { + if (getLength() != 3) return cont.erreur("FILE", "PARENT : 1 nom de fichier attendu", lng); + fil = new File(getArg(2)).getParent(); + if (fil == null) fil = ""; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("SIZE")) { + if (getLength() != 3) return cont.erreur("FILE", "SIZE : 1 nom de fichier attendu", lng); + fil = Long.toString(new File(getArg(2)).length()); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("COUNT")) { + if (getLength() < 3) return cont.erreur("FILE", "COUNT : au moins 1 nom de fichier attendu", lng); + if ((r = fileContent("LIST", 0)) >= 0) return r; + fil = Integer.toString(nbl); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("SEP")) { + fil = File.separator; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("PATHSEP")) { + fil = File.pathSeparator; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("LINESEP")) { + fil = Contexte.END_LINE; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("EXT")) { + if (getLength() != 3) return cont.erreur("FILE", "EXT : 1 nom de fichier attendu", lng); + String s = new File(getArg(2)).getName(); + int i = s.lastIndexOf('.'); + if (i >= 1) fil = s.substring(i + 1); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("CREATE")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("FILE", "CREATE : 1 nom de fichier, [1 chaîne] attendu", lng); + try { + File f; + if(!(f = new File(getArg(2))).createNewFile()) + return cont.erreur("FILE", "CREATE : création impossible de '" + f.getCanonicalPath() + "'", lng); + fil = f.getCanonicalPath(); + if (l == 4) { // Remplissage du fichier + BufferedWriter wr = new BufferedWriter(new FileWriter(f)); + wr.write(Tools.removeBracketsIfAny(getArg(3))); + wr.close(); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + catch (IOException ex) { + return cont.erreur("FILE", "CREATE : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("REMOVE")) { + if (getLength() != 3) return cont.erreur("FILE", "REMOVE : 1 nom de fichier attendu", lng); + File f; + if(!(f = new File(getArg(2))).delete()) + return cont.erreur("FILE", "REMOVE : suppression impossible de '" + f.getAbsolutePath() + + "'. Fichier introuvable ? Répertoire non vide ?", lng); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (cmd.equals("MKDIR")) { + if (getLength() != 3) return cont.erreur("FILE", "MKDIR : 1 nom de fichier attendu", lng); + File f; + if(!(f = new File(getArg(2))).mkdirs()) + return cont.erreur("FILE", "MKDIR : création impossible de '" + f.getAbsolutePath() + "'", lng); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (cmd.equals("MOVE")) { + if (getLength() != 4) return cont.erreur("FILE", "MOVE : 2 noms de fichier attendus", lng); + File f; + if(!(f = new File(getArg(2))).renameTo(new File(getArg(3)))) + return cont.erreur("FILE", "MOVE : déplacement impossible de '" + f.getAbsolutePath() + "'", lng); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (cmd.equals("COPY")) { + if (getLength() != 4) return cont.erreur("FILE", "COPY : 2 noms de fichier attendus", lng); + // Merci à http://www.roseindia.net/java/beginners/CopyFile.shtml + File f = new File(getArg(2)); + try { + InputStream is = new FileInputStream(f); + OutputStream os = new FileOutputStream(new File(getArg(3))); // pour append mettre true + byte[] buf = new byte[1024]; + int l; + while ((l = is.read(buf)) > 0) os.write(buf, 0, l); + is.close(); + os.close(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + catch (FileNotFoundException ex) { + return cont.erreur("FILE", "COPY : le fichier " + f.getAbsolutePath() + " n'existe pas", lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "COPY : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("GLOB")) { + int l = getLength(); + if (l < 2 || l > 4) + return cont.erreur("FILE", "GLOB : 1 nom de fichier, [1 rép., 1 regexp] attendus", lng); + final String rep = (l >= 3 ? getArg(2) : "."), + find = (l == 4 ? Tools.removeBQuotes(getArg(3)).value : ".*"); + String[] files = new File(rep).list((dir, fname) -> fname.matches(find)); + fil = files == null ? "" : Tools.arrayToString(Arrays.asList(files)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("LIST")) { + int l = getLength(); + if (l < 3) return cont.erreur("FILE", "LIST : au moins 1 nom de fichier attendu", lng); + + if ((r = fileContent("LIST", 0)) >= 0) return r; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("LINES")) { + if (getLength() < 3) return cont.erreur("FILE", "LINES : au moins 1 nom de fichier attendu", lng); + if ((r = fileContent("LINES", 1)) >= 0) return r; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("VIEW")) { + if (getLength() < 3) return cont.erreur("FILE", "VIEW : au moins 1 nom de fichier attendu", lng); + if ((r = fileContent("VIEW", 2)) >= 0) return r; + aff = fil; + fil = null; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("SCAN")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("FILE", "SCAN : 1 nom de fichier, [1 séq.] attendus", lng); + try { + String[] seq = (l == 4 ? getArg(3) : "1:-1").split(":"); + int deb = 1, fin; + switch (seq.length) { + case 1: + fin = Integer.parseInt(seq[0]); + if (fin < 0) { + deb = fin; + fin = -1; + } + break; + case 2: + deb = Integer.parseInt(seq[0]); + fin = Integer.parseInt(seq[1]); + break; + default: + return cont.erreur("FILE", "séquence attendue : début:fin", lng); + } + BufferedReader read; + if (fin < 0) { // Comptage du nb de ligne si fin<0 + int n = 0; + read = new BufferedReader(new FileReader(getArg(2))); + while (read.readLine() != null) n++; + read.close(); + fin = fin + n + 1; + if (deb < 0) deb = deb + n + 1; + } + String lu; int i = 1; + StringBuilder sb = new StringBuilder(); + read = new BufferedReader(new FileReader(getArg(2))); + while ((lu = read.readLine()) != null) { + if (i >= deb && i <= fin) sb.append(lu).append('\n'); + i++; + } + read.close(); + fil = sb.toString(); + } + catch (IOException ex) { + return cont.erreur("FILE", "SCAN : ERREUR IOException : " + ex.getMessage(), lng); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "paramètre de séquence invalide : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("TEMP")) { + if (getLength() < 2 || getLength() > 3) + return cont.erreur("FILE", "TEMP : [1 chaîne] attendue", lng); + try { + File f = File.createTempFile(getLength() == 3 ? getArg(2) : "lunasql-", null); + f.deleteOnExit(); + fil = f.getAbsolutePath(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (IOException ex) { + return cont.erreur("FILE", "TEMP : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("OPEN")) { + if (getLength() < 3 || getLength() > 4) + return cont.erreur("FILE", "OPEN : 1 nom de fichier, [1 mode] attendus", lng); + String f = getArg(2), m = getLength() == 4 && !getArg(3).isEmpty() ? getArg(3) : "r"; + try { fil = Integer.toString(cont.addFile(new File(f), m.charAt(0))); } + catch (IOException ex) { + return cont.erreur("FILE", "OPEN : ERREUR IOException : " + ex.getMessage(), lng); + } + cont.setVar(Contexte.ENV_CMD_STATE, m.equals("r") ? + Contexte.STATE_FALSE : Contexte.STATE_TRUE); + } + else if (cmd.equals("CLOSE")) { + if (getLength() != 3) return cont.erreur("FILE", "CLOSE : 1 index attendu", lng); + try { + int n = Integer.parseInt(getArg(2)); + cont.delFile(n); + fil = getArg(2); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "CLOSE : index incorrect : " + getArg(2), lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "CLOSE : ERREUR IOException : " + ex.getMessage(), lng); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (cmd.equals("READ")) { + if (getLength() < 3 || getLength() > 4) + return cont.erreur("FILE", "READ : 1 index, [1 nombre] attendu", lng); + try { + FileReader fr = cont.getFileR(Integer.parseInt(getArg(2))); + int nbCar = getLength() == 4 ? Integer.parseInt(getArg(3)) : 1; + if (nbCar == 1) { + if ((nbCar = fr.read()) < 0) fil = ""; + else fil = Character.toString((char)nbCar); + } + else { + char[] buf = new char[nbCar]; + fr.read(buf, 0, nbCar); + fil = new String(buf); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "READ : index incorrect : " + getArg(2), lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "READ : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("READLN")) { + if (getLength() != 3) return cont.erreur("FILE", "READLN : 1 index attendu", lng); + try { + FileReader fr = cont.getFileR(Integer.parseInt(getArg(2))); + int n; + StringBuilder sb = new StringBuilder(); + while ((n = fr.read()) > -1 && n != '\n') { + if (n != '\r') sb.append((char)n); // TODO: séparation des lignes pas super + } + if (n == -1 && sb.length() == 0) sb.append(""); + fil = sb.toString(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "READLN : index incorrect : " + getArg(2), lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "READLN : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("WRITE")) { + try { + if (getLength() != 4) return cont.erreur("FILE", "WRITE : 1 index, 1 chaîne attendus", lng); + FileWriter w = cont.getFileW(Integer.parseInt(getArg(2))); + String s = getArg(3); + w.write(s); + fil = Integer.toString(s.length()); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "WRITE : index incorrect : " + getArg(2), lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "WRITE : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("WRITELN")) { + try { + if (getLength() != 4) return cont.erreur("FILE", "WRITELN : 1 index, 1 chaîne attendus", lng); + FileWriter w = cont.getFileW(Integer.parseInt(getArg(2))); + String s = getArg(3); + w.write(s + Contexte.END_LINE); + fil = Integer.toString(s.length() + Contexte.END_LINE.length()); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "WRITELN : index incorrect : " + getArg(2), lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "WRITELN : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("EACH")) { + if (getLength() != 4) + return cont.erreur("FILE", "EACH : 1 nom de fichier, 1 bloc attendus", lng); + + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "file each"); + try { + BufferedReader read = new BufferedReader(new FileReader(getArg(2))); + Lecteur lec = new Lecteur(cont); + int lu; + int nbi = 0; + while ((lu = read.read()) != -1 && ret == RET_CONTINUE) { + nbi++; + vars.put("arg1", Character.toString((char)lu)); + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + ret = lec.getCmdState(); + } + read.close(); + lec.fin(); + fil = Integer.toString(nbi); + } + catch (IOException ex) { + return cont.erreur("FILE", "EACHLN : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("EACHLN")) { + if (getLength() != 4) + return cont.erreur("FILE", "EACHLN : 1 nom de fichier, 1 bloc attendus", lng); + + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "file eachln"); + try { + BufferedReader read = new BufferedReader(new FileReader(getArg(2))); + Lecteur lec = new Lecteur(cont); + String lu; + int nbi = 0; + while ((lu = read.readLine()) != null && ret == RET_CONTINUE) { + nbi++; + vars.put("arg1", lu); + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + ret = lec.getCmdState(); + } + read.close(); + lec.fin(); + fil = Integer.toString(nbi); + } + catch (IOException ex) { + return cont.erreur("FILE", "EACHLN : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("BYTES")) { + if (getLength() < 3 || getLength() > 4) + return cont.erreur("FILE", "BYTES : 1 nom de fichier, [1 nombre] attendu", lng); + + try { + File f = new File(getArg(2)); + if (f.length() > Math.pow(2,21)) // max 2 Mio + return cont.erreur("FILE", + "BYTES : le fichier " + getArg(2) + " est trop volumineux (> 2 Mio)", lng); + + fil = Security.hexencode(Files.readAllBytes(f.toPath())); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (NumberFormatException ex) { + return cont.erreur("FILE", "BYTES : nombre incorrect : " + getArg(3), lng); + } + catch (IOException ex) { + return cont.erreur("FILE", "BYTES : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("DUMP")) { + if (getLength() != 4) return cont.erreur("FILE", "DUMP : 1 nom de fichier, 1 chaîne attendus", lng); + + r = -1; + try { + File f = new File(getArg(2)); + if (cont.askWriteFile(f)) { + OutputStream os = new FileOutputStream(f); + if (f.getName().toUpperCase().endsWith(".GZ")) os = new GZIPOutputStream(os, 4096); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter( + os, cont.getVar(Contexte.ENV_FILE_ENC))); + out.write(getArg(3)); + out.close(); + fil = Long.toString(f.length()); + } + else fil = "0"; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + catch (IOException ex) { + r = cont.erreur("FILE", "DUMP : ERREUR IOException : " + ex.getMessage(), lng); + } + if (r >= 0) return r; + } + else if (cmd.equals("INFO")) { + if (getLength() != 3) return cont.erreur("FILE", "INFO : 1 nom de fichier attendu", lng); + + File f = new File(getArg(2)), f2; + if (!f.exists()) return cont.erreur("FILE", "INFO : le fichier " + f.getAbsolutePath() + + " n'existe pas", lng); + // Chargement des attributs + SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + try { + Properties prop = new Properties(); + prop.put("name", f.getName()); + prop.put("path", f.getAbsolutePath()); + prop.put("canon", f.getCanonicalPath()); + prop.put("size", Long.toString(f.length())); + prop.put("modified_f", df.format(new Date(f.lastModified()))); + prop.put("modified", Long.toString(f.lastModified())); + prop.put("parent", f.getParent() == null ? "null" : f.getParent()); + prop.put("parent2", (f2 = f.getParentFile()) == null ? "null" : f2.getCanonicalPath()); + prop.put("is-dir", f.isDirectory() ? "1" : "0"); + prop.put("is-file", f.isFile() ? "1" : "0"); + prop.put("is-absol", f.isAbsolute() ? "1" : "0"); + prop.put("is-hidden", f.isHidden() ? "1" : "0"); + prop.put("can-read", f.canRead() ? "1" : "0"); + prop.put("can-write", f.canWrite() ? "1" : "0"); + prop.put("can-exec", f.canExecute() ? "1" : "0"); + prop.put("URI", f.toURI().toASCIIString()); + prop.put("free-sp", Long.toString(f.getFreeSpace())); + prop.put("total-sp", Long.toString(f.getTotalSpace())); + prop.put("hash-sha1", new Security().getHashFile(f)); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + prop.store(os, "File : " + f.getCanonicalPath()); + fil = os.toString(cont.getVar(Contexte.ENV_FILE_ENC)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (IOException ex) { + return cont.erreur("FILE", "INFO : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("HASH")) { + if (getLength() != 3) return cont.erreur("FILE", "HASH : 1 nom de fichier attendu", lng); + + File f = new File(getArg(2)); + if (!f.canRead()) return cont.erreur("FILE", "HASH : le fichier " + f.getAbsolutePath() + + " est inaccessible", lng); + Checksum ck = new CRC32(); + byte[] tb = new byte[1024]; + int nbb; + r = -1; + try (InputStream in = new FileInputStream(f)) { + while ((nbb = in.read(tb)) != -1) ck.update(tb, 0, nbb); + fil = Long.toHexString(ck.getValue()); + } + catch (IOException ex) { + r = cont.erreur("FILE", "HASH : ERREUR IOException : " + ex.getMessage(), lng); + } + if (r >= 0) return r; + } + else if (cmd.equals("DIGEST")) { + if (getLength() < 3 || getLength() > 4) + return cont.erreur("FILE", "DIGEST : 1 nom de fichier attendu", lng); + + File f = new File(getArg(2)); + if (!f.canRead()) return cont.erreur("FILE", "DIGEST : le fichier " + f.getAbsolutePath() + + " est inaccessible", lng); + try { + fil = new Security().getHashFile(f, getLength() == 3 ? "hex" : getArg(3)); + } + catch (IOException ex) { + return cont.erreur("FILE", "DIGEST : ERREUR IOException : " + ex.getMessage(), lng); + } + } + else return cont.erreur("FILE", cmd + " : commande inconnue", lng); + + cont.setValeur(new ValeurDef(cont, aff, fil)); + return RET_CONTINUE; + } + + /** + * Lit le contenu intégral de la liste de fichiers getCommandA, selon le mode donné + * + * @param cmd le nom de la sous-commande + * @param mode le mode de retour : 0: retour simple, 1 : retour liste, 2 : affichage console + * @return -1 si tou va bien, code retour de erreur sinon + */ + private int fileContent(String cmd, int mode) { + String[] files = getCommandA(2); + StringBuilder sb = new StringBuilder(); + + for (String sf : files) { + String path; + String[] fnames; + try { + Tools.BQRet bqr = Tools.removeBQuotes(sf); + String fname = bqr.value; + if (bqr.hadBQ) { + final String fname0 = fname; + int id = fname.lastIndexOf(File.separator); + if (id >= 0) path = fname.substring(0, id); + else path = "."; + File dir = new File(path); + fnames = dir.list((directory, fileName) -> fileName.matches(fname0)); + } + else { + File f = new File(fname); + if (!f.exists()) return cont.erreur("FILE", "le fichier "+ sf +" n'existe pas", lng); + else if (f.length() > Math.pow(2,21)) // max 2 Mio + return cont.erreur("FILE", "le fichier " + sf + " est trop volumineux (> 2 Mio)", lng); + else { + fnames = new String[]{fname}; + path = "."; + } + } + + if (fnames == null || fnames.length == 0) // si pas de correspondance, erreur + return cont.erreur("FILE", "aucun fichier ne correspond au filtre : "+ sf, lng); + else + for (int j = 0; j < fnames.length; j++) { + fnames[j] = (path.equals(".") ? fnames[j] : path + File.separator + fnames[j]); + File file = new File(fnames[j]); + if (!file.isFile() || !file.canRead()) + return cont.erreur("FILE", "le fichier '"+ file.getCanonicalPath() + "' est inaccessible", lng); + else { + if (mode == 2) { + if (sb.length() > 0) sb.append('\n'); + sb.append("Fichier : ").append(file.getName()).append('\n'); + } + InputStream is = new FileInputStream(file); + if (fnames[j].toUpperCase().endsWith(".GZ")) is = new GZIPInputStream(is, 4096); + BufferedReader read = new BufferedReader(new InputStreamReader(is)); + String line; + int l = 0; + while ((line = read.readLine()) != null) { + l++; + switch (mode) { + case 0: + sb.append(line).append(Contexte.END_LINE); + break; + case 1: + sb.append('{') + .append(line.replace("{", "^{").replace("}", "^}")) + .append('}').append("\n"); + break; + case 2 : + sb.append(String.format("%04d", l)).append(" | ").append(line).append('\n'); + break; + } + }// Parcours du fichier + nbl += l; + if (mode == 2) sb.append("-> ").append(l).append(" ligne").append(l > 1 ? "s" : ""); + read.close(); + } + } + } + catch (IOException ex) { + return cont.exception("FILE", cmd + " : ERREUR IOException : " + ex.getMessage(), lng, ex); + } + catch (PatternSyntaxException ex) { + return cont.erreur("FILE", cmd + " : syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + }// for + fil = sb.toString(); + return -1; + } + + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " file Outils de gestion de fichier\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdFor.java b/src/lunasql/cmd/CmdFor.java new file mode 100644 index 0000000..3a69d51 --- /dev/null +++ b/src/lunasql/cmd/CmdFor.java @@ -0,0 +1,271 @@ +package lunasql.cmd; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.List; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.val.Valeur; + +/** + * Commande FOR
+ * (Interne) Parcours d'un resultset d'une requête SELECT/TABLE ou d'une liste + * @author M.P. + */ +public class CmdFor extends Instruction { + + private final OptionParser parser; + + public CmdFor(Contexte cont){ + super(cont, TYPE_CMDINT, "FOR", null); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("n", "avec -q, variables : noms des champs"); + parser.accepts("q", "boucle sur une table/requête").requiredIf("n"); + parser.accepts("r", "boucle sur une séquence"); + parser.accepts("f", "boucle sur les lignes d'un fichier"); + parser.accepts("s", "séparateur de valeurs en mode liste").withRequiredArg() + .ofType(String.class).describedAs("sep"); + parser.nonOptions("nom_var liste_valeurs liste_commandes").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lc = options.nonOptionArguments(); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "for"); + vars.put(Contexte.LEC_LOOP_DEEP, cont.incrLoopDeep()); // profondeur de boucle + int ret = RET_CONTINUE; + boolean exec = false; + String iter, cmds; + + if (options.has("q")) { // boucle sur requête/table + if (lc.size() < 2) + return cont.erreur("FOR", "avec -q, une requête/table et un bloc sont attendus", lng); + + iter = Tools.removeBracketsIfAny((String)lc.get(0)).trim(); + if (iter.isEmpty()) return cont.erreur("FOR", "requête SQL vide avec option -q", lng); + + cmds = (String) lc.get(1); + if (cmds.isEmpty()) { + cont.setValeur(null); + return RET_CONTINUE; + } + + if (!iter.matches("(?is)SELECT\\s+.*")) iter = "SELECT * FROM " + iter; + if (cont.getVerbose() >= Contexte.VERB_DBG) cont.println("Requête : " + iter); + // Ici on ne prend pas la méthode SQLCnx.getResultset() car exécution concurencielle dans evaluerBlockFor() + Statement statmt = cont.getConnex().getConnex().createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); + ResultSet rs = statmt.executeQuery(iter); + + ResultSetMetaData rsm = rs.getMetaData(); + int nbCol = rsm.getColumnCount(), nbl = 0; + vars.put("col_nb", Integer.toString(nbCol)); + for (int i = 1; i <= nbCol; i++) vars.put("col_n" + i, rsm.getColumnName(i)); + + // Parcours de la liste et exécution en Lecteur + Lecteur lec = new Lecteur(cont); + while (ret == RET_CONTINUE && rs.next()) { + // Variables de lecteur + vars.put("row_id", Integer.toString(++nbl)); + for (int i = 1; i <= nbCol; i++) vars.put("col" + i, rs.getString(i)); + if (options.has("n")) { + int nbExpr = 0; + for (int i = 1; i <= nbCol; i++) { + String k = rsm.getColumnName(i); + if (k == null || k.isEmpty()) vars.put("Expr" + String.format("%02d", ++nbExpr), rs.getString(i)); + else if (cont.valideKey(k)) vars.put(k, rs.getString(i)); + } + } + ret = cont.evaluerBlockFor(lec, cmds, vars); + exec = true; + } + lec.fin(); + rs.close(); + statmt.close(); + } + else if (options.has("r")) { // boucle sur séquence (range) + if (lc.size() != 3) + return cont.erreur("FOR", "avec -r, une variable, une séquence et un bloc sont attendus", lng); + + String key = (String) lc.get(0); + iter = Tools.removeBracketsIfAny((String) lc.get(1)); + cmds = (String) lc.get(2); + if (cmds.isEmpty()) { + cont.setValeur(null); + return RET_CONTINUE; + } + if (!cont.valideKey(key)) + return cont.erreur("FOR", "affectation de variable invalide", lng); + if (cont.getVerbose() >= Contexte.VERB_DBG) cont.println("Séquence : " + iter); + + String[] seq = iter.split(":"); + int deb = 0, fin, pas = 1; + try { + switch (seq.length) { + case 1: + fin = Integer.parseInt(seq[0]); + break; + case 2: + deb = Integer.parseInt(seq[0]); + fin = Integer.parseInt(seq[1]); + break; + case 3: + deb = Integer.parseInt(seq[0]); + fin = Integer.parseInt(seq[1]); + pas = Integer.parseInt(seq[2]); + break; + default: + return cont.erreur("FOR", "séquence attendue : début:fin:pas", lng); + } + if (pas == 0) + return cont.erreur("FOR", "séquence infinie non autorisée", lng); + } + catch (NumberFormatException ex) { + return cont.erreur("FOR", "paramètre de séquence invalide : " + ex.getMessage(), lng); + } + + // Boucle for sur séquence + Lecteur lec = new Lecteur(cont); + for (int i = deb; ret == RET_CONTINUE && (pas > 0 ? i < fin : i > fin); i += pas) { + // Variables de lecteur + vars.put(key, Integer.toString(i)); + ret = cont.evaluerBlockFor(lec, cmds, vars); + exec = true; + } + lec.fin(); + } + else if (options.has("f")) { // boucle sur lignes de fichier + if (lc.size() != 3) + return cont.erreur("FOR", + "avec -f, une variable, un fichier et un bloc sont attendus", lng); + + String key = (String) lc.get(0); + iter = (String) lc.get(1); + cmds = (String) lc.get(2); + if (cmds.isEmpty()) { + cont.setValeur(null); + return RET_CONTINUE; + } + if (!cont.valideKey(key) || !cont.isNonSys(key)) + return cont.erreur("FOR", "affectation de variable invalide", lng); + + // Fichier à itérer + File f = new File(iter); + try { + BufferedReader br = new BufferedReader(new FileReader(f)); + String line; + Lecteur lec = new Lecteur(cont); + while ((line = br.readLine()) != null) { + // Variables de lecteur + vars.put(key, line); + ret = cont.evaluerBlockFor(lec, cmds, vars); + exec = true; + } + lec.fin(); + br.close(); + } + catch (FileNotFoundException ex) { // IOException prévisible + return cont.erreur("FOR", "le fichier " + f.getAbsolutePath() + " n'existe pas", lng); + } + } + else { // boucle sur simple liste + if (lc.size() != 3) + return cont.erreur("FOR", + "une (ou des) var, une liste et un bloc sont attendus", lng); + + String[] keys = ((String)lc.get(0)).split(","); + if (keys.length == 0) return cont.erreur("FOR", "aucune variable de boucle fournie", lng); + + iter = (String) lc.get(1); + cmds = (String) lc.get(2); + if (cmds.isEmpty()) { + cont.setValeur(null); + return RET_CONTINUE; + } + + // Variables et itération + for (String key : keys) { + if (!cont.valideKey(key) || !cont.isNonSys(key)) + return cont.erreur("FOR", "affectation de variable invalide : " + key, lng); + vars.put(key, ""); + } + String[] tb; + if (options.has("s")) // support des "", [] et {} + tb = Tools.removeBracketsIfAny(iter).split((String) options.valueOf("s")); + else tb = Tools.blankSplitLec(cont, iter); // support des "" et {} + if (cont.getVerbose() >= Contexte.VERB_DBG) cont.println("Tableau : " + iter); + if (tb.length == 0) { + cont.setValeur(null); + return RET_CONTINUE; + } + + // Parcours de la liste et exécution en Lecteur + Lecteur lec = new Lecteur(cont); + for (int i = 0; ret == RET_CONTINUE && i < tb.length; i += keys.length) { + for (int j = 0; j < keys.length; j++) { + vars.put(keys[j], i + j < tb.length ? tb[i + j] : ""); + } + ret = cont.evaluerBlockFor(lec, cmds, vars); + exec = true; + } + lec.fin(); + } + + if (exec) { + Valeur vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + } else cont.setValeur(null); // retour sans exécution + + if (ret == RET_BREAK_LP) { + String lb = cont.getLecVar(Contexte.LEC_LOOP_BREAK); // si _LOOP_BREAK positionnée + return lb == null ? RET_CONTINUE : RET_BREAK_LP; + } + else return ret; + } + catch (SQLException e) {// erreur prévisible > cont.erreur + return cont.erreur("FOR", "Impossible d'exécuter la requête : " + e.getMessage(), lng); + } + catch (OptionException ex) { + return cont.exception("FOR", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("FOR", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " for Parcourt une liste ou le résultat d'une requête / table\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdGrant.java b/src/lunasql/cmd/CmdGrant.java new file mode 100644 index 0000000..6c0ea9e --- /dev/null +++ b/src/lunasql/cmd/CmdGrant.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande GRANT
+ * (SQL) Attribution de droits à un utilisateur de la base + * @author M.P. + */ +public class CmdGrant extends Instruction { + + public CmdGrant(Contexte cont) { + super(cont, TYPE_CMDSQL, "GRANT", null); + } + + @Override + public int execute() { + return executeUpdate("GRANT", "droits attribués"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " grant (+attr) Attribue des droits à un utilisateur\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdHelp.java b/src/lunasql/cmd/CmdHelp.java new file mode 100644 index 0000000..de55798 --- /dev/null +++ b/src/lunasql/cmd/CmdHelp.java @@ -0,0 +1,448 @@ +package lunasql.cmd; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.Config; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.sql.SQLCnx; +import lunasql.sql.TypesSGBD; + +/** + * Commande HELP
+ * (Interne) Affichage de l'aide de la console ou d'une commande + * @author M.P. + */ +public class CmdHelp extends Instruction { + + private final OptionParser parser; + + public CmdHelp(Contexte cont) { + super(cont, TYPE_CMDINT, "HELP", "?"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("a", "ajoute une aide à un alias (variable)"); + parser.accepts("t", "avec -a, l'aide n'est pas formatée en paragraphes"); + parser.accepts("d", "suppression d'une aide"); + parser.accepts("f", "exporte en fichier f toutes les aides").withRequiredArg().ofType(File.class) + .describedAs("file"); + parser.nonOptions("rubrique").ofType(String.class); + } + + @Override + public int execute() { + if (getLength() == 1) { + if (cont.getVerbose() >= Contexte.VERB_AFF) cont.printlnX(getIntro(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if (getLength() >= 2) { + StringBuilder sb = new StringBuilder(); + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + // Exportation de toutes les aides + if (options.has("f")) { + sb.append("spool \"").append(((File) options.valueOf("f")).getAbsolutePath()).append("\"; help; "); + String[] libs = {"launching", "syntax", "delimiters", "commands", "substitutes", "expressions", "variables", + "catching", "js-funct", "packages", "libraries", "etikette", "bonus", "licenses", "changelog"}; + for (String l : libs) sb.append("help ").append(l).append("; "); + sb.append("print; print \" -------------------------------- Bibliothèques chargées par défaut\";"); + sb.append("print; help-base; print; print; "); + sb.append("print \" ------------------------------------- Liste des commandes internes\";"); + // Parcours de toutes les aides des commandes + TreeMap cmds = cont.getAllCommandsTree(); + for (Map.Entry me : cmds.entrySet()) { + Instruction ins = me.getValue(); + if ((ins.getType() == Instruction.TYPE_CMDSQL || + ins.getType() == Instruction.TYPE_CMDINT || + ins.getType() == Instruction.TYPE_MOTC_WH) && ins.getDesc() != null) + sb.append("help ").append(ins.getName()).append("; print; "); + } + sb.append("spool off;"); + new Lecteur(cont, sb.toString()); + cont.setValeur(null); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lc = options.nonOptionArguments(); + + if (options.has("a")) { + if (lc.size() >= 2){ + String c = (String)lc.get(0), h = listToString(lc, 1); + if (cont.isSet(c) && cont.isNonSys(c)) { + cont.addVarHelp(c, h, !options.has("t")); + if (cont.getVerbose() >= Contexte.VERB_BVR) + cont.println("-> aide fixée pour la commande : " + c); + cont.setVar(Contexte.ENV_RET_VALUE, h); + } else + return cont.erreur("HELP", "affectation d'aide à variable non définie", lng); + } else + return cont.erreur("HELP", "avec l'option a, nom de variable et chaîne attendus", lng); + } else if (options.has("d")) { + if (lc.size() == 1) { + cont.removeVarHelp((String)lc.get(0)); + } else + return cont.erreur("HELP", "avec l'option d, nom de variable attendu", lng); + } else for (int i = 0; i < lc.size(); i++) { // pour chaque cmd + if (cont.getVerbose() == Contexte.VERB_SIL) { + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + sb.setLength(0); + String cmdname = ((String)lc.get(i)).toUpperCase(); + + // On commence par rechercher les COMMANDES et les VARIABLES/MACRO + Instruction cmd = cont.getCommand(cmdname); + String vh = cont.getVarHelp((String)lc.get(i)); + if (cmd != null) { + // Message si macro du même nom + if (vh != null && cont.getVerbose() >= Contexte.VERB_MSG) { + cont.errprintln("Note : nom de macro identique à une commande, la macro est ignorée"); + } + // Affichage de l'aide de la commande interne/plugin + String help = cmd.getHelp(); + sb.append("\n ").append(SQLCnx.frmI(" Commande " + cmd.getName(), 67, '-')); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + if (help == null) + cont.errprintln(" Commande à usage interne à l'application uniquement !"); + else cont.printlnX(help, Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } else if (vh != null) { + // Affichage de l'aide de la fonction utilisateur + sb.append("\n ").append(SQLCnx.frmI(" Variable/Macro " + cmdname, 67, '-')) + .append('\n'); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.printlnX(vh, Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + + // puis les rubriques d'aide + else if ("COMMANDS".startsWith(cmdname)) { + sb.append("\n ").append(SQLCnx.frmI(" Liste des commandes", 67, '-')).append("\n\n"); + TreeMap cmds = cont.getAllCommandsTree(); + sb.append("Commandes SQL :\n"); + for (Map.Entry me : cmds.entrySet()) { + Instruction ins = me.getValue(); + if (ins.getType() == Instruction.TYPE_CMDSQL && ins.getDesc() != null) + sb.append(ins.getDesc()); + } + sb.append("\nCommandes LunaSQL (et greffons éventuels) :\n"); + for (Map.Entry me : cmds.entrySet()) { + Instruction ins = me.getValue(); + int type = ins.getType(); + if ((type == Instruction.TYPE_CMDINT || type == Instruction.TYPE_CMDPLG || + type == Instruction.TYPE_MOTC_WH) && ins.getDesc() != null) + sb.append(ins.getDesc()); + } + TreeMap helps = cont.getAllVarHelpTree(); + if (helps.size()> 0) sb.append("\nVariables utilisateur :\n"); + for (Map.Entry me : helps.entrySet()) { + sb.append(" ").append(SQLCnx.frm(me.getKey(), 10, ' ')).append(" ") + .append(me.getValue()).append('\n'); + } + // autres aides... + sb.append("\nPour obtenir la syntaxe d'une commande : help nom_commande\n"); + sb.append("Les alias et macros se complètent par la touche . Pour\n"); + sb.append("ajouter un alias ou une macro en complétion : DEF -c"); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("LAUNCHING".startsWith(cmdname)) { + sb.append("\n ").append(SQLCnx.frmI(" Lancement de LunaSQL", 67, '-')).append('\n'); + sb.append(getLaunching()); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("SYNTAX".startsWith(cmdname)) { + sb.append(getFContent("Utilisation générale", "/lunasql/doc/syntax.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("DELIMITERS".startsWith(cmdname)) { + sb.append(getFContent("Délimitation des chaînes", "/lunasql/doc/delimiters.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("SUBSTITUTES".startsWith(cmdname)) { + sb.append(getFContent("Usage des substitutions", "/lunasql/doc/substitutes.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("EXPRESSIONS".startsWith(cmdname)) { + sb.append(getFContent("Expressions substituées", "/lunasql/doc/expressions.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("VARIABLES".startsWith(cmdname)) { + sb.append(getFContent("Propriétés des variables", "/lunasql/doc/variables.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("JS-FUNCT".startsWith(cmdname)) { + sb.append(getFContent("Liste des fonctions Javascript", "/lunasql/doc/js-funct.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("CATCHING".startsWith(cmdname)) { + sb.append(getFContent("Gestion à chaud des erreurs", "/lunasql/doc/catching.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("PACKAGES".startsWith(cmdname)) { + sb.append(getFContent("Modules disponibles à charger", "/lunasql/doc/packages.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("LIBRARIES".startsWith(cmdname)) { + sb.append(getFContent("À propos des bibliothèques Java", "/lunasql/doc/libraries.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("ETIKETTE".startsWith(cmdname)) { + sb.append(getFContent("Les bonnes pratiques en LunaSQL", "/lunasql/doc/etikette.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("BONUS".startsWith(cmdname)){ + sb.append(getFContent("Quelques bonus pour jouer", "/lunasql/doc/bonus.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if ("LICENSES".startsWith(cmdname)) { + // Affichage de la licence pour LunaSQL + sb.append(getFContent("Licence LunaSQL", "/lunasql/LICENSE.txt")).append('\n'); + // Affichage de la licence BSD pour JLine + sb.append('\n').append(getFContent("Licence JLine", "/jline/LICENSE.txt")); + sb.append("\nhttp://opensource.org/licenses/bsd-license.php"); + // Affichage de la licence MIT pour JOptsimple + sb.append('\n').append(getFContent("Licence JOptsimple", "/joptsimple/LICENSE.txt")); + sb.append("\nhttp://opensource.org/licenses/mit-license.php"); + // Affichage de la licence Apache 2 pour OpenCSV + sb.append('\n').append(getFContent("Licence OpenCSV", "/opencsv/LICENSE.txt")); + sb.append("\nhttp://opensource.org/licenses/Apache-2.0"); + // Affichage de la licence LGPL pour JTableView + sb.append("\n\n ").append(SQLCnx.frmI(" Licence JTableView", 67, '-')).append('\n'); + sb.append("http://opensource.org/licenses/LGPL-2.1"); + // Affichage de la licence pour NanoHTTPD + sb.append('\n').append(getFContent("Licence NanoHTTPD", "/lunasql/http/NanoHTTPD-LICENSE.txt")); + // Affichage de la licence pour Highlight.js + sb.append('\n').append(getFContent("Licence Highlight.js", "/lunasql/http/res/Highlightjs-LICENSE.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } else if("CHANGELOG".startsWith(cmdname)) { + sb.append(getFContent("Nouveautés des dernières versions", "/lunasql/doc/changelog.txt")); + cont.printlnX(sb.toString(), Contexte.BR_WHITE); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + // autres : erreur + } else return cont.erreur("HELP", "aucune rubrique d'aide pour la variable/macro '" + + cmdname + "'", lng); + }// for + } catch (OptionException ex) { + return cont.exception("HELP", "option incorrecte : " + ex.getMessage() , lng, ex); + } catch (IOException ex) { + return cont.exception("HELP", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + else return cont.erreur("HELP", "un nom de commande au maximum est attendu", lng); + cont.setValeur(null); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " help, ? Affiche différentes rubriques d'aide\n"; + } + + public static String getIntro() { + StringBuilder sb = new StringBuilder(); + sb.append("\n--------------------------------------------------------------\n"); + sb.append("\t\tAIDE DE LA CONSOLE LunaSQL"); + sb.append("\n--------------------------------------------------------------\n\n"); + sb.append("LunaSQL version ").append(Config.APP_VERSION_NUM).append(" - ").append(Config.APP_VERSION_NAME); + sb.append("\nClient SQL JDBC pour la gestion cosmique d'une base de données\n"); + sb.append(" 26950 lignes de code, 4041 lignes de doc interne\n\n"); + sb.append("* -- Liste des numéros d'employées depuis fichier\n"); + sb.append("* file eachln employees.dat { arg idemp\n"); + sb.append("* -- Recherche du nom (et du salaire) de l'employée en base\n"); + sb.append("* for -qn [SELECT NAME, SAL FROM EMPLOYEES WHERE ID=$idemp] {\n"); + sb.append("* if [$SAL >= 1000] {\n"); + sb.append("* print \"$NAME est riche, elle gagne $SAL !\"\n"); + sb.append("* }\n* }\n* }\n\n"); + sb.append("Rubriques d'aide disponibles (taper help ) :\n"); + sb.append(" launching practice substitutes commands catching js-funct packages\n"); + sb.append(" libraries bonus license changelog\n"); + sb.append("\n - Options de lancement : help launching\n"); + sb.append(" - Informations sur l'utilisation : help syntax\n"); + sb.append(" - Fonctionnement des chaînes de car. : help delimiters\n"); + sb.append(" - Liste complète des commandes : help commands\n"); + sb.append(" - Guide d'usage des substitutions : help substitutes\n"); + sb.append(" - Guide des expressions substituées : help expressions\n"); + sb.append(" - Quelques propriétés des variables : help variables\n"); + sb.append(" - Aide sur la commande : help \n"); + sb.append(" - Guide de gestion des erreurs : help catching\n"); + sb.append(" - Liste complète des fonctions js : help js-funct\n"); + sb.append(" - Liste complète des modules : help packages\n"); + sb.append(" - À propos des bibliothèques Java : help libraries\n"); + sb.append(" - Les bonnes pratiques en LunaSQL : help etikette\n"); + sb.append(" - Quelques bonus et exemples : help bonus\n"); + sb.append(" - Licence de LunaSQL et bibliothèques : help licenses\n"); + sb.append(" - Nouveautés des dernières versions : help changelog\n"); + sb.append(" - Aide sur le module de base : help-base\n"); + sb.append("\nLunaSQL fut créée pour répondre à un besoin d'interprète multi-SGBD\n"); + sb.append("de scripts SQL, puis fut poursuivie à visée pédagogique. Elle n'a pas\n"); + sb.append("la prétention d'être un « vrai » langage de programmation, mais...\n"); + sb.append("elle est amusante, élégante et bien utile tout de même !\n"); + sb.append("« Les cieux proclament la gloire de Dieu [...].\n"); + sb.append(" Un jour après l'autre fait jaillir le langage » - Ps. 19:1,2 \n"); + sb.append("\nUsage, modification et redistribution placés sous licence CeCILL v1\n"); + sb.append(" http://cecill.info/licences/Licence_CeCILL_V2.1-fr.html\n"); + sb.append("\nBugs et suggestions à rapporter à ").append(Config.APP_AUTHOR_NAME); + sb.append("\n(le maniaque derrière LunaSQL qui rédige ses chèques en héxadécimal)"); + sb.append("\nCourriel : ").append(Config.APP_AUTHOR_EMAIL); + sb.append("\nOpenPGP : ").append(Config.APP_AUTHOR_PGP); + // Proverbe sur la lune + sb.append("\n\n« Qui est plus utile, le soleil ou la lune ?"); + sb.append("\nLa lune, bien entendu. Elle brille quand il fait noir, alors que le"); + sb.append("\nsoleil brille uniquement quand il fait clair. »"); + sb.append("\n - Physicien et écrivain allemand Georg Christoph Lichtenberg"); + return sb.toString(); + } + + /* Aide de la console */ + public static String getLaunching() { + StringBuilder sb = new StringBuilder(); + sb.append("\n java -jar lunasql-x.x.x.jar "); + sb.append("\n ou"); + sb.append("\n java lunasql.Main \n"); + sb.append("\n Avec un seul argument, équivaut à : --login= --console\n"); + sb.append("\n Options (plusieurs possibles, elles peuvent être abrégées) :"); + sb.append("\n Pour la connexion :"); + sb.append("\n --type= (type : ACCESS ACCESSX UCACCESS HSQLDB H2DB MYSQL\n DERBY ORACLE SQLSERVER)"); + sb.append("\n pour UCanAccess cf. http://ucanaccess.sourceforge.net"); + sb.append("\n LunaSQL recommande la base H2, cf. la bibliothèque doc-h2"); + sb.append("\n --name= ('-' pour ne pas spécifier de base)"); + sb.append("\n --host=\n --port="); + sb.append("\n ou "); + sb.append("\n --driver="); + sb.append("\n Exemples de drivers (tout driver JDBC convient) :"); + sb.append("\n ODBC : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_ODBC]); + sb.append("\n ACCESS(X) : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_ACCESS]); + sb.append("\n UCACCESS : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_UCACCESS]); + sb.append("\n HSQLDB : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_HSQLDB]); + sb.append("\n H2DB : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_H2DB]); + sb.append("\n MYSQL : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_MYSQL]); + sb.append("\n DERBY : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_DERBY]); + sb.append("\n ORACLE : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_ORACLE]); + sb.append("\n SQLSERVER : ").append(TypesSGBD.DRIVERS[TypesSGBD.TYPE_SQLSERV]); + sb.append("\n --path "); + sb.append("\n Exemples de paths (tout SGBD convient si driver dispo.) :"); + sb.append("\n (Note : les ports peuvent être préfixés par un ':')"); + sb.append("\n ODBC : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_ODBC]); + sb.append("\n ACCESS : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_ACCESS]); + sb.append("\n ACCESSX : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_ACCESSX]); + sb.append("\n UCACCESS : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_UCACCESS]); + sb.append("\n HSQLDB : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_HSQLDB]); + sb.append("\n H2DB : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_H2DB]); + sb.append("\n MYSQL : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_MYSQL]); + sb.append("\n DERBY : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_DERBY]); + sb.append("\n ORACLE : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_ORACLE]); + sb.append("\n SQLSERVER : ").append(TypesSGBD.CHAINES[TypesSGBD.TYPE_SQLSERV]); + sb.append("\n ou "); + sb.append("\n --login="); + sb.append("\n avec pour chaque [base] les clefs : dbpath, driver, schema, passwd"); + sb.append("\n Sans base fournie, le login est le premier argument en commande."); + sb.append("\n Exemple à appeler par --login=foo :\n default=foo"); + sb.append("\n foo.dbpath=jdbc:h2:base/MyHDB\n foo.driver=org.h2.Driver\n foo.schema=sa"); + + sb.append("\n\n Options de connexion (selon compatibilité avec la base) :"); + sb.append("\n --username=\n --password="); + sb.append("\n --db-options= (selon compatibilité)"); + sb.append("\n\n Configuration de la console :"); + sb.append("\n --defs= (plusieurs --defs possibles)"); + sb.append("\n --opts= (plusieurs --opts possibles)"); + sb.append("\n (les options en fichier de config. écrasent ces définitions)"); + sb.append("\n --uses= (plusieurs --uses possibles)"); + sb.append("\n --need-version=num-version-min (erreur mais ne quitte pas)"); + sb.append("\n --exit-on-error[=<0 / 1 (defaut)>]\n --init-sql="); + sb.append("\n --scripts-path="); + sb.append("\n défaut : ").append(Config.CT_SCRIPTS_PATH); + sb.append("\n --plugins-path="); + sb.append("\n défaut : ").append(Config.CT_PLUGINS_PATH); + sb.append("\n --config-file="); + sb.append("\n défaut : ").append(Config.CT_CONFIG_FILE); + sb.append("\n --init-file="); + sb.append("\n défaut : ").append(Config.CT_INIT_FILE); + sb.append("\n --bases-file="); + sb.append("\n défaut : ").append(Config.CT_BASES_FILE).append(" en rép. courant"); + sb.append("\n ou bien : ").append(Config.CT_BASES_PATH); + sb.append("\n --history-file="); + sb.append("\n défaut : ").append(Config.CS_HISTORY_FILE); + sb.append("\n --verb-level="); + sb.append("\n défaut : ").append(Contexte.VERBLIB[Config.CT_VERBOSE]); + sb.append("\n --log-dir="); + sb.append("\n défaut : ").append(Config.CT_LOG_DIR); + sb.append("\n --http-port="); + sb.append("\n défaut : ").append(Config.SR_HTTP_PORT); + sb.append("\n --no-colors (pas de couleurs, utile sur certains systèmes en mousse"); + sb.append("\n ignorant l'ANSI, activé par défaut avec --http-console)"); + sb.append("\n --deny-opt-command (modification d'option par OPT interdite)"); + sb.append("\n --deny-sql-update (exécution de SQL de modification interdite)"); + sb.append("\n\n Commandes (une seule commande obligatoire) :"); + sb.append("\n --help\n Affiche cette aide et quitte"); + sb.append("\n --console\n Lance LunaSQL en mode interactif dans un terminal"); + sb.append("\n --run-sql=\n Lance LunaSQL pour exécuter la commande fournie, et quitte"); + sb.append("\n --stdin\n Lance LunaSQL pour exécuter le contenu en entrée standard (pipe)"); + sb.append("\n --exec= | -:cmds | +:cmds"); + sb.append("\n Lance LunaSQL pour exécuter le contenu du fichier (cf. EXEC)"); + sb.append("\n --list-engines\n Liste les moteurs d'exécution et quitte"); + sb.append("\n --editor[=]\n Ouvre le fichier scrupt par l'éditeur EDIT"); + sb.append("\n --http-console\n Lance un serveur HTTP pour exécuter du code LunaSQL à distance"); + sb.append("\n (port par défaut: ").append(Config.SR_HTTP_PORT).append(", peut être changé avec --http-port)"); + sb.append("\n http://localhost:").append(Config.SR_HTTP_PORT).append(" : application Web interactive"); + sb.append("\n http://localhost:").append(Config.SR_HTTP_PORT).append("/api : API GET|POST pour exécuter du "); + sb.append("code\n avec param|body: sqlquery= header: 'content-type: text/plain'"); + sb.append("\n Ex.: http://localhost:5862/api?sqlquery=select%20*%20from%20test"); + sb.append("\n --apropos\n Affiche des informations sur LunaSQL et quitte"); + sb.append("\n --version\n Affiche le numéro de la version courante et quitte"); + sb.append("\n\nBiblothèques optionnelles à ajouter au CLASSPATH :"); + sb.append("\n javamail.jar (commande MAIL), jsqlparser.jar (commande FRM),"); + sb.append("\n csvjdbc.jar (commande CSV)"); + + sb.append("\n\nOptions de lancement de la JVM :"); + sb.append("\n Il est possible d'adapter la mémoire et la pile par les options de"); + sb.append("\n lancement de la JVM -Xms (tas) et -Xss (pile). Exemple :"); + sb.append("\n java -Xms64m -Xss1m lunasql.Main [options de LunaSQL]"); + sb.append("\n\nNotes :\n - sous la console DOS de Windows, pour corriger le problème"); + sb.append("\n d'affichage des caractères accentués, utilisez en batch la commande"); + sb.append("\n CHCP. ex : 'chcp 28591 > nul' (code page ISO-8859-1)"); + sb.append("\n ou bien codez directement le fichier DOS en IBM850"); + sb.append("\n - pour certains SGBD, il peut être sage d'utiliser la console fournie"); + sb.append("\n par le SGBD, si elle existe (meilleure compatibilité)."); + sb.append("\n Exemple ORACLE : voir l'utilisation de SQLPlus*"); + + return sb.toString(); + }// affiche +} diff --git a/src/lunasql/cmd/CmdHist.java b/src/lunasql/cmd/CmdHist.java new file mode 100644 index 0000000..0b1157f --- /dev/null +++ b/src/lunasql/cmd/CmdHist.java @@ -0,0 +1,202 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import jline.History; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; +//import jline.console.history.FileHistory; +//import jline.console.history.History.Entry; + +/** + * Commande HIST
+ * (Interne) Retourne l'historique local de la console + * @author M.P. + */ +public class CmdHist extends Instruction { + + private final OptionParser parser; + + public CmdHist(Contexte cont) { + super(cont, TYPE_CMDINT, "HIST", "H"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("c", "supprime tout l'historique"); + parser.accepts("e", "exécute les entrées trouvées"); + parser.accepts("n", "affiche l'entrée n").withRequiredArg().ofType(Integer.class) + .describedAs("nb"); + parser.accepts("l", "liste les n dernières entrées n").withRequiredArg().ofType(Integer.class) + .describedAs("nb"); + parser.nonOptions("chaîne").ofType(String.class); + } + + @SuppressWarnings("unchecked") + @Override + public int execute() { + if (cont.isHttpMode()) + return cont.erreur("HIST", "cette commande n'est pas autorisée en mode HTTP", lng); + + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + History histo = cont.getHistory(); + Valeur vr = new ValeurDef(cont); + List lc = options.nonOptionArguments(); + + if (lc.isEmpty()) { + List lhisto = new ArrayList(histo.getHistoryList()); + Collections.reverse(lhisto); + int i; + StringBuilder sb = new StringBuilder(); + for (i = 0; i < lhisto.size(); i++) + sb.append(String.format("%03d : ", i)).append(lhisto.get(i)).append('\n'); + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(i)); + //int i = 0; + //for (Iterator lhisto = histo.iterator(); lhisto.hasNext(); i++) { + // cont.println(String.format("%03d : ", i) + lhisto.next().toString()); + //} + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else { + boolean exec = options.has("e"); // avec option n ou recherche + String cmd = lc.isEmpty() ? "" : ((String) lc.get(0)).toUpperCase(); + if (options.has("c") || cmd.equals("CLEAR")) { // suppression + if (exec) return cont.erreur("HIST", "exécution impossible avec option -c", lng); + histo.clear(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + + } + else if (options.has("n") || cmd.equals("NTH")) { + int i; + if (options.has("n")) i = (Integer)options.valueOf("n") + 1; + else if (lc.size() == 2) { + try { i = Integer.parseInt((String) lc.get(1)) + 1; } + catch (NumberFormatException ex) { + return cont.erreur("HIST", "nombre incorrect : " + ex.getMessage(), lng); + } + } + else return cont.erreur("HIST", "avec option -n : 1 nombre attendu", lng); + + if (i < 0 || i > histo.size()) + return cont.erreur("HIST", "index d'historique hors bornes : " + i, lng); + + String h = histo.getHistory(histo.size() - i - 1); + if (exec) new Lecteur(cont, h); + else { + vr.setDispValue(String.format("%03d : ", i) + h, Contexte.VERB_AFF); + vr.setSubValue(h); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + + } else if (options.has("l") || cmd.equals("LIST")) { + int i; + if (options.has("l")) i = (Integer) options.valueOf("l"); + else if (lc.size() == 2) { + try { i = Integer.parseInt((String) lc.get(1)); } + catch (NumberFormatException ex) { + return cont.erreur("HIST", "nombre incorrect : " + ex.getMessage(), lng); + } + } + else return cont.erreur("HIST", "avec option -l : 1 nombre attendu", lng); + if (i < 0) return cont.erreur("HIST", "Index d'historique hors bornes : " + i, lng); + List lhisto = new ArrayList(histo.getHistoryList()); + Collections.reverse(lhisto); + StringBuilder sb = new StringBuilder(); + if (exec) { + for (int j = 0; j < Math.min(lhisto.size(), i); j++) + sb.append(Tools.cleanSQLCodeDelHist(cont, lhisto.get(j))); + new Lecteur(cont, sb.toString()); + } + else { + int j; + for (j = 0; j < Math.min(lhisto.size(), i); j++) + sb.append(String.format("%03d : ", j)).append(lhisto.get(j)).append('\n'); + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(j)); + //int j = 0; + //for (Iterator lhisto = histo.iterator(); lhisto.hasNext() && j < Math.min(histo.size(), i); j++) { + // cont.println(String.format("%03d : ", j) + lhisto.next().toString()); + //} + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + } + else { + try { + List l = options.nonOptionArguments(); + if (l.size() != 1) + return cont.erreur("HIST", "au maximum une chaîne de recherche est attendue", lng); + List lhisto = new ArrayList(histo.getHistoryList()); + Collections.reverse(lhisto); + StringBuilder sb = new StringBuilder(); + Pattern ptnb = Pattern.compile(Tools.removeBQuotes((String) l.get(0)).value); + String h; + int i, n = 0; + for (i = 0; i < lhisto.size(); i++) { + h = lhisto.get(i); + if (ptnb.matcher(h).matches()) { + if (exec) { + // exclusion des commandes HIST pour éviter la StackOverflowError + sb.append(Tools.cleanSQLCodeDelHist(cont, h)); + } else { + n++; + sb.append(String.format("%03d : ", i)).append(h).append('\n'); + } + } + } + if (exec) new Lecteur(cont, sb.toString()); + else { + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(n)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + } + catch (PatternSyntaxException ex) { + return cont.erreur("HIST", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + } + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("HIST", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("HIST", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " hist, h Affiche l'historique des commandes en console\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdIf.java b/src/lunasql/cmd/CmdIf.java new file mode 100644 index 0000000..f2873be --- /dev/null +++ b/src/lunasql/cmd/CmdIf.java @@ -0,0 +1,85 @@ +package lunasql.cmd; + +import java.io.IOException; +import javax.script.ScriptException; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.val.Valeur; + +/** + * Commande IF
+ * (Interne) Conditionnel IF (ELSEIF ELSE) + * @author M.P. + */ +public class CmdIf extends Instruction { + + private static final String SYNERR = "syntaxe : IF expr bloc [ELSEIF expr bloc]+ [ELSE bloc]"; + + public CmdIf(Contexte cont){ + super(cont, TYPE_CMDINT, "IF", null); + } + + @Override + public int execute() { + try { + int lg = getLength(); + if (lg < 3) return cont.erreur("IF", SYNERR, lng); + + // Test des conditions booléennes + int ret = RET_CONTINUE; + boolean exec; + Lecteur lec = new Lecteur(cont); + lec.setLecVar(Contexte.LEC_SUPER, "if"); + String iter = getArg(1); + if (cont.evaluerBool(iter)) { + ret = cont.evaluerBlockIf(lec, getArg(2)); + exec = true; + } + else { // false + exec = false; + int i = 3; + + // Test des elseif + while (!exec && i < lg && getArg(i).equalsIgnoreCase("ELSEIF")) { + if (i + 2 >= lg) return cont.erreur("IF", SYNERR, lng); + if (cont.evaluerBool(getArg(i + 1))) { + ret = cont.evaluerBlockIf(lec, getArg(i + 2)); + exec = true; + } + i += 3; + } + + // Aucun elseif valide > else + if (!exec && i < lg && getArg(i).equalsIgnoreCase("ELSE")) { // exécution else + if (i + 1 >= lg) return cont.erreur("IF", SYNERR, lng); + ret = cont.evaluerBlockIf(lec, getArg(i + 1)); + exec = true; + } + } + lec.fin(); + + if (exec) { + Valeur vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + } + else cont.setValeur(null); // retour sans exécution + return ret; + } + catch (IllegalArgumentException e) {// erreur déjà traitée (cf. cont.evaluerBool) + return cont.erreur("IF", "Expression invalide :\n" + e.getMessage(), lng); + } + catch (ScriptException e) {// erreur prévisible > cont.erreur + return cont.erreur("IF", "Impossible d'évaluer l'expression :\n" + e.getMessage(), lng); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " if Teste une expression et exécute les arguments\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdImport.java b/src/lunasql/cmd/CmdImport.java new file mode 100644 index 0000000..bab7b72 --- /dev/null +++ b/src/lunasql/cmd/CmdImport.java @@ -0,0 +1,502 @@ +package lunasql.cmd; + +import java.awt.Toolkit; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.sql.*; +import java.text.ParseException; +import java.util.List; +import java.util.Properties; +import java.util.regex.PatternSyntaxException; +import java.util.zip.GZIPInputStream; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Security; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.ValeurDef; +import opencsv.CSVReader; + +/** + * Commande IMPORT
+ * (Interne) Importation d'un fichier formaté CSV ou TXT + * @author M.P. + */ +public class CmdImport extends Instruction { + + private final OptionParser parser; + + public CmdImport(Contexte cont) { + super(cont, TYPE_CMDINT, "IMPORT", "IM"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("f", "recherche de fichiers sur patron").withRequiredArg().ofType(String.class) + .describedAs("pattern"); + parser.accepts("k", "clef de chiffrement (si ext. cry)").withRequiredArg().ofType(String.class) + .describedAs("key"); + parser.accepts("o", "options d'import CSV").withRequiredArg().ofType(String.class) + .describedAs("lng,qut,esc"); + parser.accepts("t", "format de date personnalisé").withRequiredArg().ofType(String.class) + .describedAs("frm1,frm2").withValuesSeparatedBy(','); + parser.accepts("l", "avec -t, locale").withRequiredArg().ofType(String.class) + .describedAs("locale"); + parser.accepts("p", "propriétés du driver (avec -q)").withRequiredArg().ofType(String.class) + .describedAs("prop"); + parser.accepts("q", "requête SELECT de selection").requiredIf("p"); + parser.accepts("d", "pas de ligne d'entêtes"); + parser.accepts("h", "entêtes de colonnes").withRequiredArg().ofType(String.class) + .describedAs("headers"); + parser.accepts("c", "importe depuis le presse-papier"); + parser.nonOptions("fichier table").ofType(String.class); + } + + @Override + public int execute() { // TODO: à réduire en utilisant des méthodes + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lf = options.nonOptionArguments(); + + // Cas de l'import sur requeste SQL par la bibliothèque CsvJdbc + if (options.has("q")) { + try { + Class.forName("org.relique.jdbc.csv.CsvDriver"); + String path = (String) lf.get(0), table = (String) lf.get(1), + sql = listToString(lf, 2), zip = path.endsWith(".zip") ? "zip:" : ""; + Properties prop = new Properties(); + prop.put("separator", ";"); // options par défaut + if (options.has("p")) prop.putAll(Tools.getProp((String) options.valueOf("p"))); + + long tm = System.currentTimeMillis(); + Connection conn = DriverManager.getConnection("jdbc:relique:csv:" + zip + path, prop); + ResultSet rs = conn.createStatement().executeQuery(sql); + ResultSetMetaData rsSchema = rs.getMetaData(); + int nbCol = rsSchema.getColumnCount(), nbLig = 0; + + // Préparation de la requête d'insertion + StringBuilder sb = new StringBuilder(); + sb.append("INSERT INTO ").append(table).append(" VALUES ("); + for (int i = 1; i <= nbCol; i++) sb.append("?,"); + sb.deleteCharAt(sb.length() - 1).append(')'); + PreparedStatement stmt = cont.getConnex().getConnex().prepareStatement(sb.toString()); + + // Boucle sur les données CSV + while (rs.next()) { + nbLig++; + for (int i = 1; i <= nbCol; i++) stmt.setString(i, rs.getString(i)); + stmt.executeUpdate(); + } + stmt.close(); + tm = System.currentTimeMillis() - tm; + + // Conclusion + String ccl = new StringBuilder() + .append("-> ").append(nbLig).append(" ligne").append(nbLig > 1 ? "s":"") + .append(" insérée").append(nbLig > 1 ? "s":"") + .append(" (").append(SQLCnx.frmDur(tm)).append(")").toString(); + + cont.setValeur(new ValeurDef(cont, ccl, Integer.toString(nbLig), false)); + cont.setVar(Contexte.ENV_CMD_STATE, nbLig > 0 ? + Contexte.STATE_TRUE : Contexte.STATE_FALSE); + return RET_CONTINUE; + + } + catch (ClassNotFoundException ex) { + return cont.erreur("IMPORT", "impossible de charger le driver : " + ex.getMessage() + + "\n\tVérifiez que la bibliothèque csvjdbc.jar est disponible", lng); + } + catch (SQLException ex) { + return cont.erreur("CSV", "ERREUR SQLException : " + ex.getMessage() , lng); + } + } + + String fname, ext, fbs; + String[] fnames; + boolean fltmod = false; // mode filtre de fichier + + // Option -f : modèle regexp de nom de fichier + if (options.has("f")) { + fltmod = true; + String f = (String) options.valueOf("f"); + fname = Tools.removeBQuotes(f).value; + String path; + int id = fname.lastIndexOf(File.separator); + if (id >= 0) { + path = fname.substring(0, id); + fname = fname.substring(id + 1); + } + else path = "."; + + File dir = new File(path); + final String fname0 = fname; + try { + fnames = dir.list((directory, fileName) -> fileName.matches(fname0)); + } + catch(PatternSyntaxException ex){ + return cont.erreur("IMPORT", "syntaxe regexp incorrecte : "+ ex.getMessage(), lng); + } + if (fnames == null || fnames.length == 0) // si pas de correspondance, erreur + return cont.erreur("IMPORT", "aucun fichier ne correspond au filtre : "+ f, lng); + + for (int i = 0; i < fnames.length; i++) + fnames[i] = (path.equals(".") ? fnames[i] : path + File.separator + fnames[i]); + } + else { + if (lf.isEmpty() || lf.size() > 2) + return cont.erreur("IMPORT", "un nom de fichier et une table de destination sont attendus", lng); + fnames = new String[]{ (String) lf.get(0) }; + } + + // Boucle de parcours des fichiers sélectionnés + int nbltot = 0, nberrtot = 0; + int ret = RET_CONTINUE; + StringBuilder ccl = new StringBuilder(); + for (String fnm : fnames) { + fname = fnm; + + boolean zip = false, cry = false, clip = options.has("c"); + if (clip) { + fbs = fname; + ext = ""; + } + else { + int id = fname.lastIndexOf('.'); + if (id > 0) { + ext = fname.substring(id + 1).toUpperCase(); + if (ext.equals("GZ")){ // mise au format GZip + fname = fname.substring(0, id); + zip = true; + } + else if (ext.equals("CRY")) { // chiffrement des données + fname = fname.substring(0, id); + cry = true; + } + } + id = fname.lastIndexOf('.'); + if (id > 0 && id < fname.length() - 1) { + fbs = fname.substring(0, id).toUpperCase(); + ext = fname.substring(id + 1).toUpperCase(); + } else { + fbs = fname; + ext = ""; + } + id = fname.lastIndexOf(File.separator); + if (id > 0) fbs = fbs.substring(id + 1); + + // Si extension sql > renvoi à EXEC + if (ext.equals("SQL")) return cont.executeCmd("EXEC", fname); + if (!ext.equals("CSV") && !ext.equals("TXT")) { + return cont.erreur("IMPORT", "format de fichier non supporté : " + + (ext.length() == 0 ? "(ext. vide)" : ext), lng); + } + } + + // Vérification de la clef de chiffrement en mode cry + byte[] bkey = null; + if (cry) { + if (!options.has("k")) + return cont.erreur("IMPORT", "option k (clef) requise si format cry", lng); + + try { + bkey = Security.hexdecode((String) options.valueOf("k")); + if (bkey == null) + return cont.erreur("IMPORT", "clef de déchiffrement fournie nulle", lng); + } catch (IllegalArgumentException ex) { + return cont.erreur("IMPORT", ex.getMessage(), lng); + } + } + + String table = fltmod || lf.size() == 1 ? fbs : ((String) lf.get(1)).toUpperCase(); + char sep = (clip || ext.equals("TXT") ? '\t' : ';'); + + // Ouverture du fichier ou de l'url + URL purl = null; + if(!clip) { + if (fname.matches("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")){ + try { + purl = new URL(fname); + } + catch (MalformedURLException ex) { + return cont.erreur("IMPORT", "ERREUR MalformedURLException : " + ex.getMessage(), lng); + } + } + else { + File file = new File(fname + (zip ? ".gz" : (cry ? ".cry" : ""))); + if (file.isFile() && file.canRead()) fname = file.getAbsolutePath(); + else return cont.erreur("IMPORT", "le fichier '"+ file.getAbsolutePath() + + "' est inaccessible", lng); + } + } + + String[] line; + StringBuilder s = new StringBuilder(); + CSVReader reader = null; + try { + // Options d'import + String[] csvopt; // 0:nol 1:qut 2:esc + if (options.has("o")) { + csvopt = ((String) options.valueOf("o")).split(","); + if (csvopt.length != 3 || !csvopt[0].matches("^[0-9]+$") + || csvopt[1].length() != 1 || csvopt[2].length() != 1){ + return cont.erreur("IMPORT", "format des options d'import incorrect", lng); + } + } + else csvopt = new String[]{"0", "\"", "\\"}; + + // Ouverture du fichier + InputStream inp; + if (clip) { // lecture du presse-papier + byte[] data = ((String) Toolkit.getDefaultToolkit() + .getSystemClipboard().getData(DataFlavor.stringFlavor)).getBytes(); + inp = new ByteArrayInputStream(data) ; + } + else { + inp = (purl == null ? new FileInputStream(fname) : purl.openStream()); + if (cry) { + byte[] iv = new byte[16]; + inp.read(iv); + Cipher c = getCipher(bkey, iv); + inp = new CipherInputStream(inp, c); + } + if (zip || cry) inp = new GZIPInputStream(inp); + } + + boolean given = options.has("h"), fields = !given && !options.has("d"), first = true; + reader = new CSVReader(new InputStreamReader(inp, cont.getVar(Contexte.ENV_FILE_ENC)), + sep, csvopt[1].charAt(0), csvopt[2].charAt(0), Integer.parseInt(csvopt[0]), + false, true); + List dateexpr = null; + String sloc = null; + if (options.has("t")) { + dateexpr = options.valuesOf("t"); + if (options.has("l")) sloc = (String) options.valueOf("l"); + } + + // Préparation de la requête d'insertion + long tm = System.currentTimeMillis(); + PreparedStatement stmt = null; + s.append("INSERT INTO ").append(table); + + int nbfields = 0, nbl = 0, nberr = 0, csvlg = 0; + int[] types = null; + while ((line = reader.readNext()) != null) { + csvlg++; + if (line.length == 1 && line[0].length() == 0) continue; // ignore empty lines + if (line[0].trim().startsWith("#")) continue; // ignore comments + + try { + if (first) { // Création + nbfields = line.length; + types = new int[nbfields]; + StringBuilder styp = new StringBuilder(); + if (fields) { + s.append(" ("); + for (String ln : line) styp.append(ln).append(','); // Noms de champs + styp.deleteCharAt(styp.length() - 1); + s.append(styp).append(") VALUES ("); + + // Lecture des types de champs + styp.insert(0, "SELECT ").append(" FROM ").append(table); + ResultSet rs = cont.getConnex().getResultset(styp.toString()); + ResultSetMetaData rsmd = rs.getMetaData(); + for (int i=0; i= Contexte.VERB_DBG) { + cont.println("Requête : " + s.toString()); + cont.println("Types : "); + for (int i = 0; i < types.length; i++) cont.print((i + 1) + " : " + types[i] + ", "); + cont.println(); + } + stmt = cont.getConnex().getConnex().prepareStatement(s.toString()); + first = false; + if (fields) continue; + } + // Pas première ligne d'entêtes en fichier + int i = 0; + for (int j = 0; j < line.length; j++) { + // Traitement des NULL : champs numériques vides, et chaînes "NULL" + if (types != null && j < types.length && line[j].isEmpty()) { + switch (types[j]) { + case Types.BOOLEAN: + case Types.BIT: + case Types.DECIMAL: + case Types.NUMERIC: + case Types.INTEGER: + case Types.BIGINT: + case Types.FLOAT: + case Types.DOUBLE: + case Types.REAL: + case Types.SMALLINT: + case Types.TINYINT: + case Types.DATE: + case Types.TIME: + case Types.TIME_WITH_TIMEZONE: + case Types.TIMESTAMP: + case Types.TIMESTAMP_WITH_TIMEZONE: + case Types.NULL: + stmt.setNull(j + 1, types[j]); + break; + default: stmt.setString(j + 1, ""); + } + } + else if (line[j].equals("")) { + if (types != null && j < types.length) stmt.setNull(j + 1, types[j]); + else stmt.setNull(j + 1, Types.NULL); + } + else { + if (dateexpr != null && types != null && j < types.length) { + switch (types[j]) { + case Types.TIME: + case Types.TIME_WITH_TIMEZONE: + stmt.setTime(j + 1, new Time(CmdTime.parseDate(line[j], + (String) dateexpr.get(Math.min(i++, dateexpr.size() - 1)), sloc))); + break; + case Types.DATE: + stmt.setDate(j + 1, new java.sql.Date(CmdTime.parseDate(line[j], + (String) dateexpr.get(Math.min(i++, dateexpr.size() - 1)), sloc))); + break; + case Types.TIMESTAMP: + case Types.TIMESTAMP_WITH_TIMEZONE: + stmt.setTimestamp(j + 1, new Timestamp(CmdTime.parseDate(line[j], + (String) dateexpr.get(Math.min(i++, dateexpr.size() - 1)), sloc))); + break; + default: stmt.setString(j + 1, line[j]); + } + } + else stmt.setString(j + 1, line[j]); + } + } + for (int j = line.length; j < nbfields; j++) stmt.setString(j + 1, ""); + if (nbfields != line.length) { + if (cont.getVerbose() >= Contexte.VERB_BVR) + cont.println("Attention (" + fbs + (ext.length() > 0 ? "." : "") + ext + " ligne " + + csvlg + ") : " + nbfields + " entêtes pour " + line.length + " champs trouvés"); + } + nbl += stmt.executeUpdate(); + } + catch (SQLException ex) { // pas de retour + nberr++; + cont.exception("IMPORT", "ERREUR SQLException (" + fbs + + (ext.length() > 0 ? "." : "") + ext + " ligne " + csvlg + ") : " + ex.getMessage(), + lng, ex); + if (cont.getVar(Contexte.ENV_EXIT_ERR).equals("1")) break; + } + catch (ParseException ex) { // pas de retour + nberr++; + cont.exception("IMPORT", "Format de date incorrect (" + fbs + + (ext.length() > 0 ? "." : "") + ext + " ligne " + csvlg + ") : " + ex.getMessage(), + lng, ex); + if (cont.getVar(Contexte.ENV_EXIT_ERR).equals("1")) break; + } + }// while + + // Fermeture + if (stmt != null) try { stmt.close(); } + catch (SQLException ex){} + tm = System.currentTimeMillis() - tm; + reader.close(); + inp.close(); + + // Message de rapport + if (clip) ccl.append("Presse-papier "); + else ccl.append("Fichier '").append(fname).append("' "); + if (nbl == 0 && nberr == 0) ccl.append("vide\n"); + else ccl.append(nbl == 0 ? "non " : (nberr > 0 ? "partiellement " : "")).append("importé\n"); + ccl.append("-> ").append((cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE) ? + Contexte.COLORS[Contexte.BR_CYAN] + nbl + Contexte.COLORS[Contexte.NONE] : nbl)) + .append(" ligne").append(nbl > 1 ? "s" : "") + .append(" insérée").append(nbl > 1 ? "s" : "") + .append(zip ? ", format zippé" : (cry ? ", format chiffré" : "")) + .append(" (").append(SQLCnx.frmDur(tm)).append(")"); + if (nberr > 0) + ccl.append(" et ") + .append((cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE) ? + Contexte.COLORS[Contexte.BR_RED] + nberr + Contexte.COLORS[Contexte.NONE] : nberr)) + .append(" erreur").append(nberr > 1 ? "s" : ""); + + cont.setVar(Contexte.ENV_RET_NLINES, Integer.toString(nbl)); + nbltot += nbl; + nberrtot += nberr; + } + catch (UnsupportedFlavorException|NoSuchPaddingException|NoSuchAlgorithmException| + InvalidAlgorithmParameterException|InvalidKeyException ex) { + ret = cont.exception("IMPORT", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + finally { + if (reader != null) reader.close(); + } + }// for fnames + + cont.setValeur(new ValeurDef(cont, ccl.toString(), Integer.toString(nbltot), false)); + cont.setVar(Contexte.ENV_CMD_STATE, nberrtot > 0 ? Contexte.STATE_ERROR : + (nbltot > 0 ? Contexte.STATE_TRUE : Contexte.STATE_FALSE)); + return ret; + } + catch (OptionException ex) { + return cont.exception("IMPORT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.erreur("IMPORT", "ERREUR IOException : " + ex.getMessage() , lng); + } + } + + + /** + * Préparation du chiffre + */ + private Cipher getCipher(byte[] bkey, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(bkey, "AES"), new IvParameterSpec(iv)); + return c; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " import, im Importe le fichier de données en paramètre selon format\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdInfo.java b/src/lunasql/cmd/CmdInfo.java new file mode 100644 index 0000000..e501496 --- /dev/null +++ b/src/lunasql/cmd/CmdInfo.java @@ -0,0 +1,346 @@ +package lunasql.cmd; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.NumberFormat; +import java.util.*; +import java.util.regex.Pattern; + +import lunasql.lib.Contexte; +import lunasql.lib.Security; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande INFO
+ * (Interne) Affiche des informations sur le système + * @author M.P. + */ +public class CmdInfo extends Instruction { + + public CmdInfo(Contexte cont) { + super(cont, TYPE_CMDINT, "INFO", "&"); + } + + @Override + public int execute() { + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + StringBuilder sb; + switch (getLength()) { + case 1 : + sb = new StringBuilder(); + sb.append(SQLCnx.frm("Propriété", 35, ' ')).append(SQLCnx.frm("Valeur", 40, ' ')).append('\n'); + sb.append(SQLCnx.frm("", 75, '-')).append('\n'); + + // Liste des infos de connexion + sb.append(SQLCnx.frm("Connection path", 35, ' ')) + .append(SQLCnx.frm(cont.getConnex().getPath(), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Connection database", 35, ' ')) + .append(SQLCnx.frm(cont.getConnex().getBase(), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Connection driver", 35, ' ')) + .append(SQLCnx.frm(cont.getConnex().getDriver(), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Connection login", 35, ' ')) + .append(SQLCnx.frm(cont.getConnex().getLogin(), 35, ' ')).append('\n'); + // Liste des paramètres système + sb.append(SQLCnx.frm("Operating system name", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("os.name"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Operating system version", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("os.version"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Operating system arch.", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("os.arch"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Java Version", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("java.version"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Java VM", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("java.vm.info"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Java Home", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("java.home"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Java Vendor", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("java.vendor"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Java Vendor URL", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("java.vendor.url"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("User name", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("user.name"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("User home", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("user.home"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("User directory", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("user.dir"), 35, ' ')).append('\n'); + sb.append(SQLCnx.frm("Default file encoding", 35, ' ')) + .append(SQLCnx.frm(System.getProperty("file.encoding"), 35, ' ')).append('\n'); + + // System memory + System.gc(); //-- make sure we get almost reliable memory usage information. + NumberFormat numbfrm = NumberFormat.getNumberInstance(Locale.FRANCE); + numbfrm.setMinimumFractionDigits(2); + numbfrm.setMaximumFractionDigits(2); + Runtime rt = Runtime.getRuntime(); + double totmem = rt.totalMemory() / 1024; + double fremem = rt.freeMemory() / 1024; + double maxmem = rt.maxMemory() / 1024; + double usemem = totmem - fremem; + sb.append(SQLCnx.frm("Maximum memory [KB]", 35, ' ')) + .append(SQLCnx.frmI(numbfrm.format(maxmem), 15, ' ')).append('\n'); + sb.append(SQLCnx.frm("Allocated memory [KB]", 35, ' ')) + .append(SQLCnx.frmI(numbfrm.format(totmem), 15, ' ')).append('\n'); + sb.append(SQLCnx.frm("Free memory [KB]", 35, ' ')) + .append(SQLCnx.frmI(numbfrm.format(fremem), 15, ' ')).append('\n'); + sb.append(SQLCnx.frm("Used memory [KB]", 35, ' ')) + .append(SQLCnx.frmI(numbfrm.format(usemem), 15, ' ')); + + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue("20"); // c'est le nombre de propritétés affichées + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + break; + + case 2: + String[] tp = getArg(1).split("\\,"); + for (String p : tp) { + String val, p2 = p.toUpperCase(); + if (p2.equals("COMMANDS")) { + sb = new StringBuilder(); + TreeMap cmds = cont.getAllCommandsTree(); + for (Map.Entry me : cmds.entrySet()) { + Instruction ins = me.getValue(); + int t = ins.getType(); + if (t == Instruction.TYPE_CMDSQL || t == Instruction.TYPE_CMDINT || + t == Instruction.TYPE_CMDPLG || t == Instruction.TYPE_MOTC_WH) + sb.append(ins.getName()).append(' '); + } + val = sb.toString(); + } + else if (p2.equals("GLOBALS")) { + sb = new StringBuilder(); + HashMap vars = cont.getAllVars(); + for (String key : vars.keySet()) { + if (cont.isNonSys(key)) sb.append(key).append(' '); + } + val = sb.toString(); + } + else if (p2.equals("LOCALS")) { + sb = new StringBuilder(); + HashMap vars = cont.getAllLecVars(); + if (vars != null) { + for (String key : vars.keySet()) sb.append(key).append(' '); + } + val = sb.toString(); + } + else if (p2.equals("OPTIONS")) { + sb = new StringBuilder(); + HashMap vars = cont.getAllVars(); + for (String key : vars.keySet()) { + if (cont.isSys(key) || cont.isSysUser(key)) sb.append(key).append(' '); + } + val = sb.toString(); + } + else if (p2.equals("DATABASE")) { + try { + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + Properties prop = new Properties(); + prop.put("db-major", Integer.toString(dMeta.getDatabaseMajorVersion())); + prop.put("db-minor", Integer.toString(dMeta.getDatabaseMinorVersion())); + prop.put("product", dMeta.getDatabaseProductName()); + prop.put("product-vers", dMeta.getDatabaseProductVersion()); + prop.put("driver", dMeta.getDriverName()); + prop.put("driver-major", Integer.toString(dMeta.getDriverMajorVersion())); + prop.put("driver-minor", Integer.toString(dMeta.getDriverMinorVersion())); + prop.put("driver-vers", dMeta.getDriverVersion()); + prop.put("jdbc-major", Integer.toString(dMeta.getJDBCMajorVersion())); + prop.put("jdbc-minor", Integer.toString(dMeta.getJDBCMinorVersion())); + + // Autres resultsets + StringBuilder s = new StringBuilder(); + // Catalogs + ResultSet rscat = dMeta.getCatalogs(); + while (rscat.next()) s.append(rscat.getString("TABLE_CAT")).append(' '); + rscat.close(); + prop.put("db-catalogs", s.toString()); + s.setLength(0); + // Schémas + rscat = dMeta.getSchemas(); + while (rscat.next()) { + String cat = rscat.getString("TABLE_CATALOG"); + if (cat != null) s.append(cat).append('.'); + s.append(rscat.getString("TABLE_SCHEM")).append(' '); + } + rscat.close(); + prop.put("db-schemas", s.toString()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + prop.store(os, "Propriétés du SGBD"); + val = os.toString(cont.getVar(Contexte.ENV_FILE_ENC)); + } + catch (UnsupportedOperationException ex) { + return cont.erreur("INFO", "ERREUR UnsupportedOperationException : " + ex.getMessage(), lng); + } + catch (SQLException ex) { + return cont.erreur("INFO", "ERREUR SQLException : " + ex.getMessage(), lng); + } + catch (IOException ex) { + return cont.erreur("INFO", "ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (p2.equals("TABLES")) { + try { + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + ResultSet result = dMeta.getTables(null, null, null, new String[] { + "TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"}); + sb = new StringBuilder(); + while(result.next()) sb.append(result.getString(3)).append(' '); + val = sb.toString(); + } catch (SQLException ex) { + return cont.erreur("INFO", "ERREUR SQLException : " + ex.getMessage(), lng); + } + } + else if (p2.equals("SYSTABLES")) { + try { + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + ResultSet result = dMeta.getTables(null, null, null, new String[] {"SYSTEM TABLE"}); + sb = new StringBuilder(); + while(result.next()) sb.append(result.getString(3)).append(' '); + val = sb.toString(); + } catch (SQLException ex) { + return cont.erreur("INFO", "ERREUR SQLException : " + ex.getMessage(), lng); + } + } + else if (p2.equals("ENVIRON")) { + Map env = System.getenv(); + Properties prop = new Properties(); + prop.putAll(env); + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + prop.store(os, "Variables d'environnement"); + val = os.toString(cont.getVar(Contexte.ENV_FILE_ENC)); + } catch (IOException ex) { + return cont.erreur("INFO", "ERREUR IOException : " + ex.getMessage(), lng); + } + } + else if (p2.equals("STACK")) { + val = cont.getCallStack(); + } + else if (p2.equals("REDEFINED")) { + sb = new StringBuilder(); + HashMap vars = cont.getAllVars(); + for (String key : vars.keySet()) { + if (cont.isNonSys(key) && cont.getCommand(key.toUpperCase()) != null) sb.append(key).append(' '); + } + vars = cont.getAllLecVars(); + if (vars != null) { + for (String key : vars.keySet()) { + if (cont.getCommand(key.toUpperCase()) != null) sb.append(key).append(' '); + } + } + val = sb.toString(); + } + else if (p2.equals("NOCIRCCTRL")) { + sb = new StringBuilder(); + for (String s : cont.getAllCircVars()) sb.append(s).append(' '); + val = sb.toString(); + } + else if (p2.equals("NETWORK")) { + try { + sb = new StringBuilder(); + Enumeration linf = NetworkInterface.getNetworkInterfaces(); + while (linf.hasMoreElements()) { + NetworkInterface inf = linf.nextElement(); + Enumeration adrList = inf.getInetAddresses(); + while (adrList.hasMoreElements()) { + InetAddress adr = adrList.nextElement(); + sb.append('{').append(adr.getHostAddress()); + NetworkInterface net = NetworkInterface.getByInetAddress(adr); + if (net != null) sb.append(' ').append(Security.hexencode(net.getHardwareAddress())); + sb.append("}\n"); + } + } + val = sb.toString(); + } + catch (SocketException ex) { + return cont.erreur("INFO", "ERREUR SocketException : " + ex.getMessage(), lng); + } + } + else { + val = System.getProperty(p); + if (val == null) val = System.getenv(p); + if (val == null) return cont.erreur("INFO", + "nom de propriété système / environnement inconnu : " + p, lng); + } + // Affichage ? + if (tp.length > 1) vr.appendDispValue(SQLCnx.frm(p, 35, ' ') + SQLCnx.frm(val, 35, ' '), + Contexte.VERB_AFF, true); + vr.setSubValue(val); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + break; + + case 3: + String cmd = getArg(1).toUpperCase(), arg = getArg(2), val; + if (cmd.equals("WHATIS")) { + if (cont.getLecVar(arg) != null) { + char c0; + if ((c0 = arg.charAt(0)) == ':' || c0 == '_') val = "syslocal"; + else val = "local"; + } + else if (cont.getGlbVar(arg) != null) { + char c0; + if ((c0 = arg.charAt(0)) == ':' || c0 == '_') val = "sysglobal"; + else val = "global"; + } + else if (cont.getPlugin(arg.toUpperCase()) != null) val = "plugin"; + else if (cont.getCommand(arg.toUpperCase()) != null) val = "intern"; + else val = "nodef"; + } + else if (cmd.equals("FIND")) { + val = ""; + sb = new StringBuilder(); + Pattern ptv = Pattern.compile(".*" + arg + ".*", Pattern.DOTALL); + HashMap vlec = cont.getAllLecVars(); + for (Map.Entry e : vlec.entrySet()) { + String k = e.getKey(), v = e.getValue(); + if (cont.isNonSys(k) && ptv.matcher(v).matches()) { + sb.append(SQLCnx.frm(k + " *")).append(' ').append(v).append("\n"); + val = v; + } + } + HashMap vars = cont.getAllVars(); + for (Map.Entry e : vars.entrySet()) { + String k = e.getKey(), v = e.getValue(); + if (cont.isNonSys(k) && ptv.matcher(v).matches()) { + sb.append(SQLCnx.frm(k)).append(' ').append(v).append("\n"); + val = v; + } + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + } + else return cont.erreur("INFO", "nom de commande inconnu : " + cmd, lng); + + // Affichage + vr.setSubValue(val); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + break; + + default: return cont.erreur("INFO", "2 arguments maximum sont attendus", lng); + } + + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " info, & Affiche des informations sur le système\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdInput.java b/src/lunasql/cmd/CmdInput.java new file mode 100644 index 0000000..578f31a --- /dev/null +++ b/src/lunasql/cmd/CmdInput.java @@ -0,0 +1,189 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.PatternSyntaxException; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Tools; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; +//import jline.console.ConsoleReader; + +/** + * Commande INPUT
+ * (Interne) Demande d'une donnée à l'utilisateur + * @author M.P. + */ +public class CmdInput extends Instruction { + + private final OptionParser parser; + + public CmdInput(Contexte cont) { + super(cont, TYPE_CMDINT, "INPUT", ">"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("p", "saisie de mot de passe"); + parser.accepts("i", "invalidation des $"); + parser.accepts("t", "temps maximal d'attente (sec.)").withRequiredArg().ofType(Integer.class) + .describedAs("time"); + parser.accepts("f", "format de saisie attendu sur patron").withRequiredArg().ofType(String.class) + .describedAs("pattern"); + parser.accepts("d", "valeur par défaut si saisie vide").withRequiredArg().ofType(String.class) + .describedAs("str"); + parser.accepts("r", "valeur par défaut si interruption").withRequiredArg().ofType(String.class) + .describedAs("str"); + parser.nonOptions("nom_var invite").ofType(String.class); + } + + @Override + public int execute() { + if (cont.isHttpMode()) + return cont.erreur("INPUT", "cette commande n'est pas autorisée en mode HTTP", lng); + + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + List lt = options.nonOptionArguments(); + String val, prompt = listToString(lt); + String defd = options.has("d") ? (String) options.valueOf("d") : ""; + String defr = options.has("r") ? (String) options.valueOf("r") : ""; + Character echo = options.has("p") ? '*' : null; + // Timeout selon format + if (options.has("t")) { + Future res = Executors.newSingleThreadExecutor() + .submit(new ConsoleInput(prompt, echo)); + try { + val = res.get(((Integer) options.valueOf("t")), TimeUnit.SECONDS); + } + catch(TimeoutException ex) { + res.cancel(true); + cont.errprintln("\nInterrompu"); + val = defr; + } + } + else val = cont.getInput(prompt, echo); + if (val == null) { + cont.errprintln("\nInterrompu"); + val = defr; + } + else if (val.isEmpty() && defd.length() > 0) val = defd; + + // Test éventuel du format + if (options.has("f")) { + try { + boolean frmok; + Tools.BQRet rbq = Tools.removeBQuotes((String) options.valueOf("f")); + String frm = rbq.value; + if (rbq.hadBQ) { // recherche regexp + frmok = val.matches(frm); + } + else { + if (frm.equals("int")) + frmok = val.matches("[+-]?\\d+"); + else if (frm.equals("num")) + frmok = val.matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); + else if (frm.equals("bool")) + frmok = val.matches("(?i)0|1|t(rue)?|f(alse)?"); + else if (frm.equals("yesno")) + frmok = val.matches("(?i)(?i)y(es)?|n(o(n)?)?|o(ui)?"); + else if (frm.equals("id")) + frmok = val.matches("[A-Za-z][A-Za-z0-9_.-]*"); + else if (frm.equals("char")) + frmok = val.length() == 1; + else if (frm.equals("word")) + frmok = val.matches("\\w+"); + else if (frm.equals("date")) + frmok = val.matches("(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\d\\d)"); + else if (frm.equals("datetime")) + frmok = val.matches("(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\d\\d) ([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d"); + else if (frm.equals("dateus")) + frmok = val.matches("(0?[1-9]|1[012])/(0?[1-9]|[12][0-9]|3[01])/((19|20)\\d\\d)"); + else if (frm.equals("alpha")) + frmok = val.matches("[A-Za-z]+"); + else if (frm.equals("alphanum")) + frmok = val.matches("[A-Za-z0-9]+"); + else if (frm.equals("alphauc")) + frmok = val.matches("[A-Z]+"); + else if (frm.equals("alphalc")) + frmok = val.matches("[a-z]+"); + else if (frm.equals("email")) + frmok = val.matches("(?i)([A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6})"); + else if (frm.equals("str")) frmok = true; + else return cont.erreur("INPUT", "commande de format inconnue : " + frm, lng); + } + if (!frmok) + return cont.erreur("INPUT", "la saisie ne correspond pas au format : " + frm, lng); + } + catch (PatternSyntaxException ex) { + return cont.erreur("INPUT", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + + // Invalidation éventuelle des substitutions par # + if (options.has("i")) val = val.replace('$', '#'); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + // Retour + vr.setSubValue(val); + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("INPUT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException|ExecutionException|InterruptedException ex) { + return cont.exception("INPUT", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " input, > Affiche une invite pour saisie au clavier\n"; + } + + /** + * Classe de saisie en entrée standard avec TimeOut + * Merci à Heinz Kabutz pour l'idée (http://www.javaspecialists.eu/archive/Issue153.html) + */ + class ConsoleInput implements Callable { + private final String prompt; + private final Character echo; + + public ConsoleInput(String prompt, Character echo) { + this.prompt = prompt; + this.echo = echo; + } + + @Override + public String call() throws IOException { + return cont.getInput(prompt, echo); + } + } + +}// class diff --git a/src/lunasql/cmd/CmdInsert.java b/src/lunasql/cmd/CmdInsert.java new file mode 100644 index 0000000..221903f --- /dev/null +++ b/src/lunasql/cmd/CmdInsert.java @@ -0,0 +1,35 @@ +package lunasql.cmd; + +import java.sql.SQLException; + +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurExe; + +/** + * Commande INSERT
+ * (SQL) Insertion d'enregistrements dans une table de la base de données + * @author M.P. + */ +public class CmdInsert extends Instruction { + + public CmdInsert(Contexte cont) { + super(cont, TYPE_CMDSQL, "INSERT", null); + } + + @Override + public int execute() { + return executeUpdate("INSERT", " insérée", true); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " insert (+instr) Ajoute des données dans la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdLet.java b/src/lunasql/cmd/CmdLet.java new file mode 100644 index 0000000..ba6968c --- /dev/null +++ b/src/lunasql/cmd/CmdLet.java @@ -0,0 +1,41 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande LET
+ * (Interne) Évalue par SE une expression arithmétique + * @author M.P. + */ +public class CmdLet extends Instruction { + + public CmdLet(Contexte cont){ + super(cont, TYPE_CMDINT, "LET", "%"); + } + + @Override + public int execute() { + try { + Object r = cont.evaluerExpr(getSCommand(1)); + Valeur vr = r == null ? null : + new ValeurDef(cont, r.toString(), Contexte.VERB_BVR, Contexte.BR_BLUE, r.toString()); + cont.setValeur(vr); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (Exception ex) { + return cont.erreur("LET", "expression incorrecte : " + ex.getMessage() , lng); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " let, % Évalue une expression par SE\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdList.java b/src/lunasql/cmd/CmdList.java new file mode 100644 index 0000000..f26a7c1 --- /dev/null +++ b/src/lunasql/cmd/CmdList.java @@ -0,0 +1,645 @@ +package lunasql.cmd; + +import static lunasql.lib.Tools.blankSplitLec; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; +import opencsv.CSVWriter; + +/** + * Commande LIST
+ * (Interne) Commande d'utilitaires de traitement de liste de chaîne + * @author M.P. + */ +public class CmdList extends Instruction { + + public CmdList(Contexte cont){ + super(cont, TYPE_CMDINT, "LIST", null); + } + + @Override + public int execute() { + if (getLength() < 2) + return cont.erreur("LIST", "une commande de liste est requise", lng); + + int ret = RET_CONTINUE; + String cmd = getArg(1).toUpperCase(), lst = ""; + if (cmd.equals("NEW")) { + int l = getLength(); + if (l < 3) return cont.erreur("LIST", "NEW : listes attendues", lng); + List lt = new ArrayList(); + for (int i = 2; i < l; i++) lt.add(getArg(i)); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("FLAT")) { + if (getLength() < 3) return cont.erreur("LIST", "FLAT : listes attendues", lng); + lst = Tools.flatList(getSCommand(2)); + } + else if (cmd.equals("CONCAT")) { + int l = getLength(); + if (l < 3) return cont.erreur("LIST", "CONCAT : listes attendues", lng); + List lt = new ArrayList(); + for (int i = 2; i <= l; i++) lt.addAll(Arrays.asList(blankSplitLec(cont, getArg(i)))); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("SIZE")) { + if (getLength() != 3) return cont.erreur("LIST", "SIZE : 1 liste attendue", lng); + String[] t = blankSplitLec(cont, getArg(2)); + lst = Integer.toString(t.length); + } + else if (cmd.equals("GET")) { + if (getLength() != 4) return cont.erreur("LIST", "GET : 1 liste, 1 index attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + try { + int i = Integer.parseInt(getArg(3)); + if (i < 0) i = t.length + i; + if (i < 0 || i >= t.length) + return cont.erreur("LIST", "GET : indice incorrect : " + getArg(3), lng); + lst = t[i]; + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "GET : format de sous-liste incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("PUSH")) { + if (getLength() != 4) return cont.erreur("LIST", "PUSH : 1 liste, 1 valeur attendues", lng); + List lt = new ArrayList(Arrays.asList(blankSplitLec(cont, getArg(2)))); + lt.add(getArg(3)); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("INDEX")) { + if (getLength() != 4) return cont.erreur("LIST", "INDEX : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String c = getArg(3); + lst = "-1"; + for (int i = 0; i < t.length; i++) + if (t[i].equals(c)) { + lst = Integer.toString(i); break; + } + } + else if (cmd.equals("LINDEX")) { + if (getLength() != 4) return cont.erreur("LIST", "LINDEX : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String c = getArg(3); + lst = "-1"; + for (int i = t.length - 1; i >= 0; i--) + if (t[i].equals(c)) { + lst = Integer.toString(i); break; + } + } + else if (cmd.equals("INDEX-IC")) { + if (getLength() != 4) return cont.erreur("LIST", "INDEX-IC : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String c = getArg(3); + lst = "-1"; + for (int i = 0; i < t.length; i++) + if (t[i].equalsIgnoreCase(c)) { + lst = Integer.toString(i); break; + } + } + else if (cmd.equals("LINDEX-IC")) { + if (getLength() != 4) return cont.erreur("LIST", "LINDEX-IC : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String c = getArg(3); + lst = "-1"; + for (int i = t.length - 1; i >= 0; i--) + if (t[i].equalsIgnoreCase(c)) { + lst = Integer.toString(i); break; + } + } + else if (cmd.equals("HAS?")) { + if (getLength() != 4) return cont.erreur("LIST", "HAS? : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String c = getArg(3); + lst = "0"; + for (String e : t) + if (e.equals(c)) { + lst = "1"; break; + } + } + else if (cmd.equals("HAS-IC?")) { + if (getLength() != 4) return cont.erreur("LIST", "HAS-IC? : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String c = getArg(3); + lst = "0"; + for (String e : t) + if (e.equalsIgnoreCase(c)) { + lst = "1"; break; + } + } + else if (cmd.equals("SUBLST")) { + if (getLength() != 4) return cont.erreur("LIST", "SUBLST : 1 liste, 1 index attendus", lng); + try { lst = Tools.subList(cont, getArg(2), getArg(3)); } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "SUBLST : format de sous-liste incorrect : " + getArg(3), lng); + } + } + else if (cmd.equals("JOIN")) { + int l = getLength(); + if (l < 3 || l > 4) + return cont.erreur("LIST", "JOIN : 1 liste, 1 chaîne attendues", lng); + String[] t = blankSplitLec(cont, getArg(2)); + StringBuilder sb = new StringBuilder(); + String c = l == 4 ? getArg(3) : ""; + for (int i = 0; i < t.length - 1; i++) sb.append(t[i]).append(c); + if (t.length > 0) sb.append(t[t.length - 1]); + lst = sb.toString(); + } + else if (cmd.equals("FIND")) { + if (getLength() != 4) return cont.erreur("LIST", "FIND : 1 liste, 1 regexp attendues", lng); + try { + Pattern p = Pattern.compile(Tools.removeBQuotes(getArg(3)).value); + String[] t = blankSplitLec(cont, getArg(2)); + List lt = new ArrayList(); + for (String s : t) { + Matcher m = p.matcher(s); + if (m.find()) lt.add(s); + } + lst = Tools.arrayToString(lt); + } + catch (PatternSyntaxException ex) { + return cont.erreur("LIST", "FIND : syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("FIRST")) { + int l = getLength(); + if (l < 3 || l > 4) + return cont.erreur("LIST", "FIRST : 1 liste, [1 nombre] attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + int nb; + if (l == 4) { + try { + nb = Math.min(Integer.parseInt(getArg(3)), t.length); + if (nb < 0) throw new NumberFormatException("négatif : " + nb); + } + catch (NumberFormatException ex) { + return cont.erreur("LIST", "FIRST : nombre incorrect : " + ex.getMessage(), lng); + } + } + else nb = 1; + // Découpage + switch (nb) { + case 0 : lst = ""; break; + case 1 : lst = t[0]; break; + default: + List lt = new ArrayList<>(); + for (int i = 0; i < nb; i++) lt.add(t[i]); + lst = Tools.arrayToString(lt); + } + } + else if (cmd.equals("LAST")) { + int l = getLength(); + if (l < 3 || l > 4) + return cont.erreur("LIST", "LAST : 1 liste, [1 nombre] attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + int nb; + if (l == 4) { + try { + nb = Math.min(Integer.parseInt(getArg(3)), t.length); + if (nb < 0) throw new NumberFormatException("négatif : " + nb); + } + catch (NumberFormatException ex) { + return cont.erreur("LIST", "LAST : nombre incorrect : " + ex.getMessage(), lng); + } + } + else nb = 1; + // Découpage + switch (nb) { + case 0 : lst = ""; break; + case 1 : lst = t[t.length - 1]; break; + default: + List lt = new ArrayList<>(); + for (int i = t.length - nb; i < t.length; i++) lt.add(t[i]); + lst = Tools.arrayToString(lt); + } + } + else if (cmd.equals("SHIFT")) { + if (getLength() != 3) return cont.erreur("LIST", "SHIFT : 1 liste attendue", lng); + List lt = new ArrayList(Arrays.asList(blankSplitLec(cont, getArg(2)))); + if (!lt.isEmpty()) lt.remove(0); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("POP")) { + if (getLength() != 3) return cont.erreur("LIST", "POP : 1 liste attendue", lng); + List lt = new ArrayList(Arrays.asList(blankSplitLec(cont, getArg(2)))); + if (!lt.isEmpty()) lt.remove(lt.size() - 1); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("INSERT")) { + int l = getLength(); + if (l < 4 || l > 5) + return cont.erreur("LIST", "INSERT : 1 liste, 1 valeur, [1 index] attendus", lng); + List lt = new ArrayList<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + int idx = 0, ls = lt.size(); + if (l == 5) { + try { + idx = Integer.parseInt(getArg(4)); + if (idx < 0) idx = ls + idx + 1; + if (idx < 0 || idx > ls) + return cont.erreur("LIST", "INSERT : indice incorrect : " + getArg(4), lng); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "INSERT : nombre incorrect : " + getArg(4), lng); + } + } + lt.add(idx, getArg(3)); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("REMOVE")) { + if (getLength() != 4) return cont.erreur("LIST", "REMOVE : 1 liste, 1 index attendus", lng); + List lt = new ArrayList<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + int idx, l = lt.size(); + try { + idx = Integer.parseInt(getArg(3)); + if (idx < 0) idx = l + idx; + if (idx < 0 || idx >= l) + return cont.erreur("LIST", "REMOVE : indice incorrect : " + getArg(3), lng); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "REMOVE : nombre incorrect : " + getArg(3), lng); + } + lt.remove(idx); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("REPLACE")) { + if (getLength() != 5) + return cont.erreur("LIST", "REPLACE : 1 liste, 1 index, 1 valeur attendus", lng); + List lt = new ArrayList<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + int idx, l = lt.size(); + try { + idx = Integer.parseInt(getArg(3)); + if (idx < 0) idx = l + idx; + if (idx < 0 || idx >= l) + return cont.erreur("LIST", "REPLACE : indice incorrect : " + getArg(3), lng); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "REPLACE : nombre incorrect : " + getArg(3), lng); + } + lt.remove(idx); + lt.add(idx, getArg(4)); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("REVERSE")) { + if (getLength() != 3) return cont.erreur("LIST", "MAX : 1 liste attendue", lng); + List lt = new ArrayList<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + Collections.reverse(lt); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("MAX")) { + if (getLength() != 3) return cont.erreur("LIST", "MAX : 1 liste attendue", lng); + String[] tval = blankSplitLec(cont, getArg(2)); + double max = Double.MIN_VALUE; + for(String s : tval){ + try { max = Math.max(Double.parseDouble(s), max); } + catch (NumberFormatException ex) { + return cont.erreur("LIST", "MAX : nombre invalide : " + s, lng); + } + } + lst = (max == (int) max ? Integer.toString((int) max) : Double.toString(max)); + } + else if (cmd.equals("MIN")) { + if (getLength() != 3) return cont.erreur("LIST", "MIN : 1 liste attendue", lng); + String[] tval = blankSplitLec(cont, getArg(2)); + double min = Double.MAX_VALUE; + for(String s : tval){ + try { min = Math.min(Double.parseDouble(s), min); } + catch (NumberFormatException ex) { + return cont.erreur("LIST", "MIN : nombre invalide : " + s, lng); + } + } + lst = (min == (int) min ? Integer.toString((int) min) : Double.toString(min)); + } + else if (cmd.equals("SUM")) { + if (getLength() != 3) return cont.erreur("LIST", "SUM : 1 liste attendue", lng); + String[] tval = blankSplitLec(cont, getArg(2)); + double sum = 0.0; + for(String s : tval){ + try { sum += Double.parseDouble(s); } + catch (NumberFormatException ex) { + return cont.erreur("LIST", "SUM : nombre invalide : " + s, lng); + } + } + lst = (sum == (int) sum ? Integer.toString((int) sum) : Double.toString(sum)); + + } + else if (cmd.equals("SORT")) { + int l = getLength(); + if (l < 3 || l > 4) + return cont.erreur("LIST", "SORT : 1 liste, [nbr|nbr-rev|str|str-rev] attendus", lng); + Comparator cp = null; + List lt = Arrays.asList(blankSplitLec(cont, getArg(2))); + if (l == 4) { + final String mode = getArg(3); + cp = (a, b) -> { + try { + if (mode.equals("nbr")) return new Double(a).compareTo(new Double(b)); + else if (mode.equals("nbr-rev")) return new Double(b).compareTo(new Double(a)); + else if (mode.equals("str")) return a.compareTo(b); + else if (mode.equals("str-rev")) return b.compareTo(a); + } + catch (NumberFormatException ex) {} + return a.compareTo(b); + }; + } + Collections.sort(lt, cp); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("RANGE")) { + if (getLength() != 3) return cont.erreur("LIST", "RANGE : 1 séquence attendue", lng); + String[] seq = getArg(2).split(":"); + int deb = 0, fin, pas = 1; + try { + switch (seq.length) { + case 1: + fin = Integer.parseInt(seq[0]); + break; + case 2: + deb = Integer.parseInt(seq[0]); + fin = Integer.parseInt(seq[1]); + break; + case 3: + deb = Integer.parseInt(seq[0]); + fin = Integer.parseInt(seq[1]); + pas = Integer.parseInt(seq[2]); + break; + default: + return cont.erreur("LIST", "RANGE : séquence attendue : début:fin:pas", lng); + } + if (pas == 0) return cont.erreur("LIST", "RANGE : séquence infinie non autorisée", lng); + } + catch (NumberFormatException ex) { + return cont.erreur("LIST", "RANGE : paramètre de séquence invalide : " + ex.getMessage(), lng); + } + StringBuilder sb = new StringBuilder(); + for (int i = deb; (pas > 0 ? i < fin : i > fin); i += pas) sb.append(i).append(' '); + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + lst = sb.toString(); + } + else if (cmd.equals("NVL")) { + if (getLength() != 3) return cont.erreur("LIST", "NVL : 1 liste attendue", lng); + String[] t = blankSplitLec(cont, getArg(2)); + for (String s : t) + if (!s.isEmpty()) { + lst = s; + break; + } + } + else if (cmd.equals("PICK")) { + int l = getLength(); + if (l < 3 || l > 4) + return cont.erreur("LIST", "PICK : 1 liste, [1 nombre] attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + try { + int n = l == 4 ? Integer.parseInt(getArg(3)) : 1; + if (n < 0 || n > t.length) + return cont.erreur("LIST", "PICK : nombre incorrect : " + getArg(3), lng); + List lt = new ArrayList<>(); + for (int i = 0; i < n; i++) { + int r = (int)(Math.random() * t.length); + lt.add(t[r]); + } + lst = Tools.arrayToString(lt); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "PICK : nombre incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("RAND")) { + if (getLength() != 3) return cont.erreur("LIST", "RAND : 1 liste attendue", lng); + List lt = Arrays.asList(blankSplitLec(cont, getArg(2))); + Collections.shuffle(lt); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("UNIQ")) { + if (getLength() != 3) return cont.erreur("LIST", "UNIQ : 1 liste attendue", lng); + Set st = new LinkedHashSet<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + List lt = new ArrayList<>(st); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("MINUS")) { + if (getLength() != 4) return cont.erreur("LIST", "MINUS : 2 listes attendues", lng); + List lt = new ArrayList<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + lt.removeAll(Arrays.asList(blankSplitLec(cont, getArg(3)))); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("UNION")) { + if (getLength() != 4) return cont.erreur("LIST", "UNION : 2 listes attendues", lng); + Set t = new TreeSet<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + t.addAll(Arrays.asList(blankSplitLec(cont, getArg(3)))); + List lt = new ArrayList<>(t); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("INTER")) { + if (getLength() != 4) return cont.erreur("LIST", "INTER : 2 listes attendues", lng); + Set st = new TreeSet<>(Arrays.asList(blankSplitLec(cont, getArg(2)))); + st.retainAll(Arrays.asList(blankSplitLec(cont, getArg(3)))); + List lt = new ArrayList<>(st); + lst = Tools.arrayToString(lt); + } + else if (cmd.equals("EACH")) { + if (getLength() != 4) return cont.erreur("LIST", "EACH : 1 liste, 1 bloc attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "list each"); + Lecteur lec = new Lecteur(cont); + int nbi = 0; + for (int i = 0; ret == RET_CONTINUE && i < t.length; i++) { + nbi++; + vars.put("arg1", t[i]); + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + ret = lec.getCmdState(); + } + lec.fin(); + lst = Integer.toString(nbi); + } + else if (cmd.equals("APPLY")) { + if (getLength() != 4) return cont.erreur("LIST", "APPLY : 1 liste, 1 bloc attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "list apply"); + Lecteur lec = new Lecteur(cont); + StringBuilder sb = new StringBuilder(); + for (int i = 0; ret == RET_CONTINUE && i < t.length; i++) { + vars.put("arg1", t[i]); + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + ret = lec.getCmdState(); + Valeur vr = cont.getValeur(); + sb.append(Tools.putBraces(vr == null ? "" : vr.getSubValue())).append(' '); + } + lec.fin(); + lst = sb.toString(); + } + else if (cmd.equals("FILTER")) { + if (getLength() != 4) return cont.erreur("LIST", "FILTER : 1 liste, 1 bloc attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "list filter"); + StringBuilder sb = new StringBuilder(); + for (String s : t) { + if (ret != RET_CONTINUE) break; + vars.put("arg1", s); + int[] e = cont.evaluerBoolLec(cmds, vars); + ret = e[1]; + if (e[0] == 1) sb.append(Tools.putBraces(s)).append(' '); + } + lst = sb.toString(); + } + else if (cmd.equals("REDUCE")) { + int l = getLength(); + if (l < 4 || l > 5) + return cont.erreur("LIST", "REDUCE : 1 liste, 1 bloc, [1 valeur] attendus", lng); + String[] t = blankSplitLec(cont, getArg(2)); + boolean z = l == 5; // valeur init fournie + String red = z ? getArg(4) : (t.length > 0 ? t[0] : ""); // réduction + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "list reduce"); + Lecteur lec = new Lecteur(cont); + for (int i = (z ? 0 : 1); ret == RET_CONTINUE && i < t.length; i++) { + vars.put("arg1", red); // réduction + vars.put("arg2", t[i]); // élément + cont.addSubMode(); + lec.add(cmds, vars); + lec.doCheckWhen(); + cont.remSubMode(); + ret = lec.getCmdState(); + Valeur vr = cont.getValeur(); + red = vr == null ? "" : vr.getSubValue(); + } + lec.fin(); + lst = red; + } + else if (cmd.equals("ANY?")) { + if (getLength() != 4) return cont.erreur("LIST", "ANY? : 1 liste, 1 bloc attendus", lng); + lst = Contexte.STATE_FALSE; + String[] t = blankSplitLec(cont, getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "list any?"); + for (String s : t) { + vars.put("arg1", s); + int[] e = cont.evaluerBoolLec(cmds, vars); + ret = e[1]; + if (e[0] == 1) { + lst = Contexte.STATE_TRUE; + break; + } + } + } + else if (cmd.equals("ALL?")) { + if (getLength() != 4) return cont.erreur("LIST", "ALL? : 1 liste, 1 bloc attendus", lng); + lst = Contexte.STATE_TRUE; + String[] t = blankSplitLec(cont, getArg(2)); + String cmds = getArg(3); + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_SUPER, "list all?"); + for (String s : t) { + vars.put("arg1", s); + int[] e = cont.evaluerBoolLec(cmds, vars); + ret = e[1]; + if (e[0] == 0) { + lst = Contexte.STATE_FALSE; + break; + } + } + } + else if (cmd.equals("REPEAT")) { + if (getLength() != 4) return cont.erreur("LIST", "REPEAT : 1 chaîne, 1 nombre attendus", lng); + String s = Tools.putBraces(getArg(2)); + int n; + try { + n = Integer.parseInt(getArg(3)); + if (n < 0) return cont.erreur("LIST", "REPEAT : nombre incorrect : " + n, lng); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "REPEAT : nombre incorrect : " + getArg(3), lng); + } + lst = new String(new char[n]).replace("\0", s + " "); + } + else if (cmd.equals("ZEROS")) { + if (getLength() != 3) return cont.erreur("LIST", "ZEROS : 1 nombre attendu", lng); + int n; + try { + n = Integer.parseInt(getArg(2)); + if (n < 0) return cont.erreur("LIST", "ZEROS : nombre incorrect : " + n, lng); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "ZEROS : nombre incorrect : " + getArg(2), lng); + } + lst = new String(new char[n]).replace("\0", "0 "); + } + else if (cmd.equals("ONES")) { + if (getLength() != 3) return cont.erreur("LIST", "ONES : 1 nombre attendu", lng); + int n; + try { + n = Integer.parseInt(getArg(2)); + if (n < 0) return cont.erreur("LIST", "ONES : nombre incorrect : " + n, lng); + } + catch(NumberFormatException ex) { + return cont.erreur("LIST", "ONES : nombre incorrect : " + getArg(2), lng); + } + lst = new String(new char[n]).replace("\0", "1 "); + } + else if (cmd.equals("COMMAND?")) { + if (getLength() != 3) return cont.erreur("LIST", "COMMAND? : 1 liste attendue", lng); + String[] t = blankSplitLec(cont, getArg(2)); + if (t[0].length() > 1 && t[0].charAt(0) == ':') t[0] = t[0].substring(1); + lst = (cont.getCommand(t[0].toUpperCase()) != null || cont.isSet(t[0]) || cont.isLec(t[0])) ? "1" : "0"; + } + else if (cmd.equals("TOCSV")) { + if (getLength() != 3) return cont.erreur("LIST", "TOCSV : 1 liste attendue", lng); + try { + StringWriter sw = new StringWriter(); + CSVWriter writer = new CSVWriter(sw, ';', '"', '\\', "\n"); + String[] lines = getArg(2).split("\n"); + for (String l : lines) writer.writeNext(blankSplitLec(cont, l)); + writer.close(); + lst = sw.toString(); + } + catch (IOException ex) { + return cont.exception("LIST", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + else return cont.erreur("LIST", cmd + " : commande inconnue", lng); + + cont.setValeur(new ValeurDef(cont, null, lst)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return ret; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " list Outils de traitement de listes de chaînes\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdMerge.java b/src/lunasql/cmd/CmdMerge.java new file mode 100644 index 0000000..1d16564 --- /dev/null +++ b/src/lunasql/cmd/CmdMerge.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande MERGE
+ * (SQL) Fusion des données de deux sources + * @author M.P. + */ +public class CmdMerge extends Instruction { + + public CmdMerge(Contexte cont){ + super(cont, TYPE_CMDSQL, "MERGE", null); + } + + @Override + public int execute() { + return executeUpdate("MERGE", "fusion réalisée"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " merge (+instr) Fusionne les données de deux tables\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdNeed.java b/src/lunasql/cmd/CmdNeed.java new file mode 100644 index 0000000..3854eca --- /dev/null +++ b/src/lunasql/cmd/CmdNeed.java @@ -0,0 +1,126 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.Config; +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande NEED
+ * (Interne) Contrôle la version du programme, fait penser à la fonction 'require' de Perl + * @author M.P. + */ +public class CmdNeed extends Instruction { + + private final OptionParser parser; // pas besoin pour le moment + public static final String VER_REG = + "^([0-9]{1,3})(\\.([0-9]{1,3})(\\.([0-9]{1,3})(\\.([0-9]{1,3}))?)?)?$"; + + public CmdNeed(Contexte cont) { + super(cont, TYPE_CMDINT, "NEED", null); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("x", "version maximale requise").withRequiredArg().ofType(String.class) + .describedAs("version"); + parser.nonOptions("no_version").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List ln = options.nonOptionArguments(); + if (ln.size() != 1) + return cont.erreur("NEED", "un numéro de version minimale est attendu", lng); + + boolean bmax = options.has("x"); + String smin = (String) ln.get(0), smax = null; + int[] v0 = frmVersion(Config.APP_VERSION_NUM), vmin = frmVersion(smin), vmax = null; + if (bmax) { + smax = (String) options.valueOf("x"); + vmax = frmVersion(smax); + } + + // Contrôles + if (v0 == null) + throw new IllegalArgumentException("numéro de version APP_VERSION_NUM invalide : " + Config.APP_VERSION_NUM); + if (vmin == null) + return cont.erreur("NEED", "numéro de version min. invalide : " + smin, lng); + if (bmax && vmax == null) + return cont.erreur("NEED", "numéro de version max. invalide : " + smax, lng); + + long diffmin = 0, diffmax = 0; + for (int i = 0; i < 4; i++) diffmin += Math.pow(10, 3 * i) * (v0[i] - vmin[i]); + if (bmax) for (int i = 0; i < 4; i++) diffmax += Math.pow(10, 3 * i) * (vmax[i] - v0[i]); + if (diffmin < 0) { // Mauvaise version minimale : on sort du script + return cont.erreur("NEED", "version insuffisante : " + Config.APP_VERSION_NUM + + " (requise en appel : " + smin + ")", lng); + } + if (diffmax < 0) { // Mauvaise version maximale : on sort aussi du script + return cont.erreur("NEED", "version excédante : " + Config.APP_VERSION_NUM + + " (requise en appel : " + smax + ")", lng); + } + if(vmin[3] < 3) { + return cont.erreur("NEED", "version non supportée : " + Config.APP_VERSION_NUM + + " (requise en appel : " + smin + ")", lng); + } + + // Retour + cont.setValeur(new ValeurDef(cont, "Version requise OK : " + Config.APP_VERSION_NUM, + Contexte.VERB_BVR, Long.toString(diffmin + diffmax))); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("NEED", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("NEED", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " need Teste la version en cours par rapport à numéro donné\n"; + } + + /** + * Formate un numéro de version en 000.000.000.000 + * + * @param v la version + * @return formaté + */ + private int[] frmVersion(String v) { + Matcher m = Pattern.compile(VER_REG).matcher(v); + if (m.find()) { + int mj = m.group(1) == null ? 0 : Integer.parseInt(m.group(1)), + mn = m.group(3) == null ? 0 : Integer.parseInt(m.group(3)), + dt = m.group(5) == null ? 0 : Integer.parseInt(m.group(5)), + rv = m.group(7) == null ? 0 : Integer.parseInt(m.group(7)); + return new int[] { rv, dt, mn, mj }; + } else { + return null; + } + } +}// class diff --git a/src/lunasql/cmd/CmdNext.java b/src/lunasql/cmd/CmdNext.java new file mode 100644 index 0000000..8d264e8 --- /dev/null +++ b/src/lunasql/cmd/CmdNext.java @@ -0,0 +1,37 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande NEXT
+ * (Interne) Passe à l'itération suivante d'une boucle for ou while en cours + * @author M.P. + */ +public class CmdNext extends Instruction { + + public CmdNext(Contexte cont){ + super(cont, TYPE_CMDINT, "NEXT", null); + } + + @Override + public int execute() { + if (cont.getLoopDeep() == 0) + return cont.erreur("NEXT", "NEXT appelée hors d'une boucle FOR ou WHILE", lng); + if (getLength() > 1) + return cont.erreur("NEXT", "aucun argument attendu", lng); + + cont.setValeur(new ValeurDef(cont, null, "0")); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_NEXT_LP; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " next Saute à l'itération suivante (FOR ou WHILE)\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdNum.java b/src/lunasql/cmd/CmdNum.java new file mode 100644 index 0000000..abd71a9 --- /dev/null +++ b/src/lunasql/cmd/CmdNum.java @@ -0,0 +1,43 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande NUM
+ * (Interne) Commande à ne pas utiliser. Elle est appelée en cas d'alias expression (sortie de EVAL) + * @author M.P. + */ +public class CmdNum extends Instruction { + + public CmdNum(Contexte cont) { + super(cont, TYPE_INVIS, "NUM", null); + } + + @Override + public int execute() { + cont.setValeur(new ValeurDef(cont, "", Contexte.VERB_BVR, Contexte.BR_BLUE, + getCommandName())); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return null; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getHelp(){ + return null; + } +}// class + diff --git a/src/lunasql/cmd/CmdOpt.java b/src/lunasql/cmd/CmdOpt.java new file mode 100644 index 0000000..5034d5c --- /dev/null +++ b/src/lunasql/cmd/CmdOpt.java @@ -0,0 +1,203 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import lunasql.lib.Contexte; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande OPT
+ * (Interne) Affectation d'une donnée à une variable SYSTEME de l'environnement d'exécution + * @author M.P. + */ +public class CmdOpt extends Instruction { + + private final OptionParser parser; + + public CmdOpt(Contexte cont){ + super(cont, TYPE_CMDINT, "OPT", "_"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("l", "référence locale au lecteur en cours"); + parser.accepts("u", "référence en lecteur père").withRequiredArg().ofType(Integer.class) + .describedAs("niveau"); + parser.accepts("d", "test d'existence de l'option").withRequiredArg().ofType(String.class) + .describedAs("opt"); + parser.nonOptions("nom_opt définition_opt").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + List lc = options.nonOptionArguments(); + boolean local = options.has("l"), link = options.has("u"); + + // Listage de toutes les variables système + if (lc.isEmpty()) { + if (options.has("d")) { // test d'existence + boolean isset = true, isloc = false; + String key = (String) options.valueOf("d"); + if (!cont.isNonSys(key) && (cont.isSet(key) || (isloc = cont.isLec(key)))) { + vr.appendDispValue(SQLCnx.frm(key) + (isloc ? cont.getLecVar(key) : cont.getGlbVar(key)), + Contexte.VERB_BVR, true); + } else { + vr.appendDispValue(key + " non définie", Contexte.VERB_BVR, true); + isset = false; + } + vr.setSubValue(isset ? "1" : "0"); + } + else { + int nb = 0; + StringBuilder sb = new StringBuilder(); + SortedSet sort; + Iterator iter; + // Options lecteur + HashMap vlec = cont.getAllLecVars(); + if (vlec != null) { + sort = new TreeSet<>(vlec.keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + nb++; + String key = iter.next(); + if (cont.isSys(key) || cont.isSysUser(key)) + sb.append(SQLCnx.frm(key + " *")).append(' ') + .append(link ? cont.getLinkVar(key, (Integer) options.valueOf("u")) : + cont.getLecVar(key)).append('\n'); + } + } + + if (!local && !link) { // Options globales + sort = new TreeSet<>(cont.getAllVars().keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + nb++; + String key = iter.next(); + if (cont.isSys(key) || cont.isSysUser(key)) + sb.append(SQLCnx.frm(key)).append(' ').append(cont.getGlbVar(key)).append('\n'); + } + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(nb)); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + + // Listage d'une (ou plusieurs) variable(s) système + else if (lc.size() == 1) { + try { + String key = Tools.removeBQuotes((String) lc.get(0)).value, val = null; + int nbv = 0; + StringBuilder sb = new StringBuilder(); + SortedSet sort; + Iterator iter; + Pattern ptnb = Pattern.compile(key); + + // Options lecteur + HashMap vlec = cont.getAllLecVars(); + if (vlec != null) { + sort = new TreeSet<>(vlec.keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + String k = iter.next(); + if ((cont.isSys(k) || cont.isSysUser(k)) && ptnb.matcher(k).matches()) { + val = link ? cont.getLinkVar(k, (Integer) options.valueOf("u")) + : cont.getLecVar(k); + sb.append(SQLCnx.frm(k + " *")).append(' ').append(val).append('\n'); + nbv++; + if (!key.equals(k)) nbv++; + cont.setVar(Contexte.ENV_RET_VALUE, val); + } + } + } + if (!local && !link) { // Options globales + sort = new TreeSet<>(cont.getAllVars().keySet()); + iter = sort.iterator(); + while (iter.hasNext()) { + String k = iter.next(); + if ((cont.isSys(k) || cont.isSysUser(k)) && ptnb.matcher(k).matches()) { + val = cont.getGlbVar(k); + nbv++; + if (!key.equals(k)) nbv++; + sb.append(SQLCnx.frm(k)).append(' ').append(val).append('\n'); + } + } + } + if (val == null) + return cont.erreur("OPT", "option système '" + key + "' non définie", lng); + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + if (nbv > 1) vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(val); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + catch (PatternSyntaxException ex) { + return cont.erreur("OPT", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + else { // Modification d'une option + String allow = cont.getVar(Contexte.ENV_CONST_EDIT); + if ("0".equals(allow) || (!local && "1".equals(allow))) + return cont.erreur("OPT", "modification d'une option non autorisée\n" + + "(console lancée avec l'option --deny-opt-cmd. Cf documentation de OPT)", lng); + + String key = (String) lc.get(0); + if (cont.isSysUser(key)) { // var système uniquement + String val = listToString(lc, 1); + if (link) cont.setLinkVar(key, (Integer) options.valueOf("u")); + if (local || link) cont.setLecVar(key, val); + else cont.setVar(key, val); // pas setVar2 car système + vr.setDispValue("-> constante système fixée : " + key, Contexte.VERB_BVR); + vr.setSubValue(val); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else + return cont.erreur("OPT", "modification d'option système non autorisée", lng); + } + + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("OPT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("OPT", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " opt, _ Modifie la valeur d'une option système de config.\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdPlugin.java b/src/lunasql/cmd/CmdPlugin.java new file mode 100644 index 0000000..982d8ca --- /dev/null +++ b/src/lunasql/cmd/CmdPlugin.java @@ -0,0 +1,145 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.ret.RetPlugin; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande PLUGIN
+ * (Interne) Gestion des plugins de la console + * @author M.P. + */ +public class CmdPlugin extends Instruction { + + private final OptionParser parser; + + public CmdPlugin(Contexte cont) { + super(cont, TYPE_CMDINT, "PLUGIN", "PG"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("l", "liste les greffons enregistrés"); + parser.accepts("n", "affiche le nombre de greffons"); + parser.accepts("c", "supprime tous les greffons"); + parser.accepts("a", "affiche la classe du greffon").withRequiredArg().ofType(String.class) + .describedAs("nom"); + parser.accepts("r", "supprime le greffons").withRequiredArg().ofType(String.class) + .describedAs("nom"); + parser.nonOptions("nom_classe").ofType(String.class); + } + + @SuppressWarnings("unchecked") + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + int ret = RET_CONTINUE; + if (options.has("l")) { + int nb = 0; + StringBuilder sb = new StringBuilder(); + sb.append("Liste des greffons inscrits :\n"); + Set>> lplug = cont.getAllPlugins(); + for (Map.Entry> entry : lplug){ + nb++; + sb.append(" - ").append(entry.getKey()).append(" : ").append(entry.getValue()).append('\n'); + } + if (nb == 0) sb.append(" (aucun)\n"); + sb.append("Total : ").append(nb).append(" greffon(s)"); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(nb)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (options.has("n")){ + String lg = Integer.toString(cont.getPluginsNb()); + vr.setDispValue("Nombre de greffons : " + lg, Contexte.VERB_AFF); + vr.setSubValue(lg); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (options.has("c")){ + cont.clearAllPlugins(); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("r")){ + String pname = (String) options.valueOf("r"); + Class cl = cont.getPlugin(pname); + if(cl == null) return cont.erreur("PLUGIN", "le greffon " + pname + " n'existe pas", lng); + cont.removePlugin(pname); + vr.setSubValue(pname); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("a")){ + String pname = (String) options.valueOf("a"); + Class cl = cont.getPlugin(pname); + if(cl == null) return cont.erreur("PLUGIN", "le greffon " + pname + " n'existe pas", lng); + vr.setDispValue(pname + " : " + cl.toString(), Contexte.VERB_AFF); + vr.setSubValue(cl.getCanonicalName()); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else { + List lc = options.nonOptionArguments(); + if (lc.size() != 1) + return cont.erreur("PLUGIN", "une classe de greffon est attendue", lng); + String cmd = (String) lc.get(0); + try { + Class cl = Class.forName(cmd); + if (Instruction.class.isAssignableFrom(cl)) { + RetPlugin r = cont.addPlugin((Class) cl, lng); + ret = r.ret; // valeur retour + if (r.success == 1) { // succes ajout par cont.addPlugin + vr.setDispValue("-> greffon ajouté : " + cl.getCanonicalName() + + " en commande " + r.cmdName, Contexte.VERB_BVR); + vr.setSubValue(r.cmdName); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + } + else return cont.erreur("PLUGIN", "la classe " + cl + + " n'est pas sous-classe d'Instruction", lng); + } + catch (ClassNotFoundException ex) {// erreur prévisible > cont.erreur + return cont.erreur("PLUGIN", "classe " + cmd + " inaccessible", lng); + } + catch (NoClassDefFoundError er) { // ce n'est pas beau, mais ça rattrape d'autres erreurs + return cont.erreur("PLUGIN", "classe " + er.getMessage() + " inaccessible", lng); + } + } + + vr.setRet(); + cont.setValeur(vr); + return ret; + } + catch (OptionException ex) { + return cont.exception("PLUGIN", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("PLUGIN", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " plugin, pg Attribue au système un nouveau greffon\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdPrint.java b/src/lunasql/cmd/CmdPrint.java new file mode 100644 index 0000000..b0b9b65 --- /dev/null +++ b/src/lunasql/cmd/CmdPrint.java @@ -0,0 +1,98 @@ +package lunasql.cmd; + +import java.io.IOException; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande PRINT
+ * (Interne) Affichage d'un message sur la sortie standard de la console + * @author M.P. + */ +public class CmdPrint extends Instruction { + + private final OptionParser parser; + + public CmdPrint(Contexte cont) { + super(cont, TYPE_CMDINT, "PRINT", "<"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("n", "sans nouvelle ligne"); + parser.accepts("v", "selon niveau de verbose").withRequiredArg().ofType(Integer.class) + .describedAs("verb"); + parser.accepts("e", "écrit en mode erreur"); + parser.accepts("b", "émet un biip énervant"); + parser.accepts("s", "pas d'affichage en mode évaluation"); + parser.accepts("c", "couleur du texte").withRequiredArg().ofType(Integer.class) + .describedAs("couleur"); + parser.accepts("f", "efface la console"); + parser.nonOptions("texte_à_afficher").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + if (options.has("f")) cont.clearConsole(); + if (options.has("b")) cont.playBeep(); + int verb = options.has("v") ? ((Integer)options.valueOf("v")) : Contexte.VERB_AFF; + if (verb < Contexte.VERB_SIL || verb >= Contexte.VERB_NUMBER) + return cont.erreur("PRINT", "numéro de verbose invalide : " + verb, lng); + int coul = options.has("c") ? ((Integer)options.valueOf("c")) : 0; + if (coul < 0 || coul >= Contexte.COLORS.length) + return cont.erreur("PRINT", "numéro de couleur invalide : " + coul, lng); + + // Impression + Valeur vr = null; + if (options.has("s")) { + vr = new ValeurDef(cont); + vr.setDispValue(listToString(options.nonOptionArguments()), verb); + } + else { + if (options.has("e")) { + if (cont.getVerbose() >= verb) + cont.errprintln(listToString(options.nonOptionArguments()), !options.has("n")); + } + else { + if (cont.getVerbose() >= verb) + cont.printlnX(listToString(options.nonOptionArguments()), coul, !options.has("n")); + } + } + cont.setValeur(vr); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("PRINT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("PRINT", "ERREUR IOException : " + ex.getMessage() , lng, ex); + } + } + + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " print, < Affiche le message en paramètre sur la sortie standard\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdPut.java b/src/lunasql/cmd/CmdPut.java new file mode 100644 index 0000000..29db435 --- /dev/null +++ b/src/lunasql/cmd/CmdPut.java @@ -0,0 +1,37 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +import java.io.IOException; +import java.util.List; + +/** + * Commande PUT
+ * (Interne) Affecte à la valeur de retour _RET_VALUE les arguments de la commande + * @author M.P. + */ +public class CmdPut extends Instruction { + + public CmdPut(Contexte cont){ + super(cont, TYPE_CMDINT, "PUT", null); + } + + @Override + public int execute() { + String ret = (getLength() == 1 ? null : getSCommand(1)); + cont.setValeur(new ValeurDef(cont, null, ret)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " put Affecte simplement la valeur de retour\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdQuit.java b/src/lunasql/cmd/CmdQuit.java new file mode 100644 index 0000000..341ccbd --- /dev/null +++ b/src/lunasql/cmd/CmdQuit.java @@ -0,0 +1,52 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande QUIT
+ * (Interne) Fermeture de la console + * @author M.P. + */ +public class CmdQuit extends Instruction { + + public CmdQuit(Contexte cont) { + super(cont, TYPE_CMDINT, "QUIT", null); + } + + @Override + public int execute() { + if (getLength() == 1) { + cont.setQuitStat(0); + cont.setValeur(null); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else if (getLength() >= 2) { + String st = getArg(1); + int ist; + try { + ist = Integer.parseInt(st); + } + catch(NumberFormatException ex){ + cont.exception("QUIT", "valeur de sortie non numérique", lng, ex); + ist = -1; + } + cont.setQuitStat(ist); + if (getLength() >= 3 && cont.getVerbose() >= Contexte.VERB_AFF) cont.println(getSCommand(2)); + cont.setValeur(null); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_ERROR); + } + else { + cont.erreur("QUIT", "maximum une valeur de sortie est attendue", lng); + } + return RET_SHUTDOWN; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " quit Ferme de la connexion à la base et quitte (Ctrl+D)\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdRand.java b/src/lunasql/cmd/CmdRand.java new file mode 100644 index 0000000..1f700d6 --- /dev/null +++ b/src/lunasql/cmd/CmdRand.java @@ -0,0 +1,133 @@ +package lunasql.cmd; + +import static joptsimple.util.RegexMatcher.regex; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.UUID; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande RAND
+ * (Interne) Calcule un nombre aléatoire + * @author M.P. + */ +public class CmdRand extends Instruction { + + private final OptionParser parser; + private SecureRandom secrnd; + public static String CHAR_MAJ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + CHAR_MIN = "abcdefghijklmnopqrstuvwxyz", + CHAR_NB = "1234567890", + CHAR_SPE = "&#@$%|+-*/\\()[]<>~^.,;!?"; + + public CmdRand(Contexte cont) { + super(cont, TYPE_CMDINT, "RAND", null); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("m", "mode de génération").withRequiredArg().ofType(String.class) + .defaultsTo("string").describedAs("number|string") + .withValuesConvertedBy(regex("number|nbr|string|str|secure|sec|UUID|uuid")); + parser.accepts("d", "graine").withRequiredArg().ofType(Long.class) + .describedAs("seed"); + parser.accepts("i", "borne inf. (mode number)").withRequiredArg().ofType(Long.class) + .defaultsTo(0l).describedAs("borne"); + parser.accepts("s", "borne sup. (mode number)").withRequiredArg().ofType(Long.class) + .defaultsTo(1000000000l).describedAs("borne"); + parser.accepts("t", "taille (mode string ou secure)").withRequiredArg().ofType(Integer.class) + .defaultsTo(16).describedAs("taille"); + parser.accepts("c", "famille de caractères (mode string)").withRequiredArg().ofType(String.class) + .defaultsTo("a").describedAs("charset").withValuesConvertedBy(regex("[Aa0%]+")); + parser.accepts("r", "base (mode secure)").withRequiredArg().ofType(Integer.class) + .defaultsTo(16).describedAs("borne"); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + String mode = options.has("m") ? (String) options.valueOf("m") : "number", // par défaut + val = ""; + + if (mode.matches("number|nbr")) { // mode NOMBRE + long bMin = ((Long)options.valueOf("i")), + bMax = ((Long)options.valueOf("s")); + if (bMin >= bMax || bMin < 0) + return cont.erreur("RAND", + "mode number : borne inf. négative ou borne sup. inférieure à borne inf.", lng); + // Calucul du nombre aléatoire + val = Long.toString((long) (bMin + Math.random() * (bMax - bMin))); + } + else if (mode.matches("string|str")) { // mode CHAINE (défaut) + int size = ((Integer)options.valueOf("t")); // taille de la chaîne + String cset = (String)options.valueOf("c"); // famille de caractères + // Calucul de la chaîne de caractère aléatoire + + if(cset.indexOf('A') >= 0) val += CHAR_MAJ; + if(cset.indexOf('a') >= 0) val += CHAR_MIN; + if(cset.indexOf('0') >= 0) val += CHAR_NB; + if(cset.indexOf('%') >= 0) val += CHAR_SPE; + if(size < 1 || val.length() < 1) + return cont.erreur("RAND", + "mode string : taille de chaîne négative ou charset invalide", lng); + + StringBuilder sb = new StringBuilder(size); + for (int i = 0; i < size; i++) + sb.append(val.charAt((int) (Math.random() * val.length()))); + val = sb.toString(); + } + else if (mode.matches("secure|sec")) { // mode SECURE + if (secrnd == null) secrnd = new SecureRandom(); + if (options.has("d")) secrnd.setSeed(((Long)options.valueOf("d"))); + byte[] brnd = new byte[((Integer)options.valueOf("t"))]; + secrnd.nextBytes(brnd); + val = new BigInteger(brnd).abs().toString(((Integer)options.valueOf("r"))); + } + else if (mode.matches("UUID|uuid")) { // mode UUID + val = UUID.randomUUID().toString(); + } + // else... autres modes de génération + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + // Retour + vr.setSubValue(val); + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("RAND", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("RAND", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " rand Génère un nombre ou une chaîne aléatoire\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdReturn.java b/src/lunasql/cmd/CmdReturn.java new file mode 100644 index 0000000..1b2a969 --- /dev/null +++ b/src/lunasql/cmd/CmdReturn.java @@ -0,0 +1,34 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande RETURN
+ * (Interne) Interruption de l'exécution d'un fichier de commandes et retour d'un résultat + * @author M.P. + */ +public class CmdReturn extends Instruction { + + public CmdReturn(Contexte cont) { + super(cont, TYPE_CMDINT, "RETURN", "^"); + } + + @Override + public int execute() { + String ret = (getLength() == 1 ? null : getSCommand(1)); + cont.setValeur(new ValeurDef(cont, ret, Contexte.VERB_BVR, ret)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_RETR_SCR; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " return, ^ Sort d'un bloc de code en renvoyant une valeur\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdRevoke.java b/src/lunasql/cmd/CmdRevoke.java new file mode 100644 index 0000000..1b39d8f --- /dev/null +++ b/src/lunasql/cmd/CmdRevoke.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande REVOKE
+ * (SQL) Suppression de droits à un utilisateur de la base + * @author M.P. + */ +public class CmdRevoke extends Instruction { + + public CmdRevoke(Contexte cont) { + super(null, TYPE_CMDSQL, "REVOKE", null); + } + + @Override + public int execute() { + return executeUpdate("REVOKE", "droits retirés"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " revoke (+attr) Retire des droits à un utilisateur\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdRollback.java b/src/lunasql/cmd/CmdRollback.java new file mode 100644 index 0000000..592066f --- /dev/null +++ b/src/lunasql/cmd/CmdRollback.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande ROLLBACK
+ * (SQL) Annule les transactions danns le SGBD + * @author M.P. + */ +public class CmdRollback extends Instruction { + + public CmdRollback(Contexte cont) { + super(cont, TYPE_CMDSQL, "ROLLBACK", null); + } + + @Override + public int execute() { + return executeUpdate("ROLLBACK", "transactions annulées"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " rollback Annule les transactions effectuées\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdSeek.java b/src/lunasql/cmd/CmdSeek.java new file mode 100644 index 0000000..6940864 --- /dev/null +++ b/src/lunasql/cmd/CmdSeek.java @@ -0,0 +1,64 @@ +package lunasql.cmd; + +import java.sql.SQLException; + +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande SEEK
+ * (Interne) Evaluation d'une expression SQL + * @author M.P. + */ +public class CmdSeek extends Instruction { + + public CmdSeek(Contexte cont) { + super(cont, TYPE_CMDINT, "SEEK", "/"); + } + + @Override + public int execute() { + Valeur vr = new ValeurDef(cont); + String s = getSCommand(1), val, nbl; + int id = s.indexOf(' '); + String c = (id < 0 ? s : s.substring(0, id)).toUpperCase(); + try { + long tm = System.currentTimeMillis(); + if (c.equals("SELECT") || c.equals("CALL") || c.equals("EXPLAIN")) { + val = cont.getConnex().seek(s, cont.getVar(Contexte.ENV_FILE_ENC), cont.getRowMaxNumber(lng)); + tm = System.currentTimeMillis() - tm; + if (val == null) val = ""; + nbl = "1"; + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else { + int n = cont.getConnex().execute(s); + tm = System.currentTimeMillis() - tm; + val = "0"; + nbl = Integer.toString(n); + cont.setVar(Contexte.ENV_CMD_STATE, n > 0 ? Contexte.STATE_TRUE : Contexte.STATE_FALSE); + } + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + } + catch (SQLException ex) { + return cont.exception("SEEK", "ERREUR SQLException : " + ex.getMessage(), lng, ex); + } + + // Retour + vr.setSubValue(val); + vr.setRet(nbl); + cont.setValeur(vr); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " seek, / Évalue une commande SQL et retourne le résultat\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdSelect.java b/src/lunasql/cmd/CmdSelect.java new file mode 100644 index 0000000..ced1e86 --- /dev/null +++ b/src/lunasql/cmd/CmdSelect.java @@ -0,0 +1,63 @@ +package lunasql.cmd; + +import java.sql.SQLException; + +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.ValeurReq; + +/** + * Commande SELECT
+ * (SQL) Selection et affichage de données depuis les tables de la base + * @author M.P. + */ +public class CmdSelect extends Instruction { + + public CmdSelect(Contexte cont){ + super(cont, TYPE_CMDSQL, "SELECT", null); + } + + @Override + public int execute() { + try { + cont.showWheel(); + long tm = System.currentTimeMillis(); + ValeurReq vr = new ValeurReq(cont, cont.getConnex().getResultString(getSCommand(), + cont.getVar(Contexte.ENV_SELECT_ARR).equals(Contexte.STATE_TRUE), + cont.getColMaxWidth(lng), cont.getRowMaxNumber(lng), + cont.getVar(Contexte.ENV_ADD_ROW_NB).equals(Contexte.STATE_TRUE), + cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE))); + tm = System.currentTimeMillis() - tm; + cont.hideWheel(); + + int n = vr.getNbLines(); + if (cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE)) + vr.setDispValue("-> " + Contexte.COLORS[Contexte.BR_CYAN] + n + Contexte.COLORS[Contexte.NONE] + + " ligne" + (n > 1 ? "s" : "") + " récupérée" + (n > 1 ? "s" : "") + + (vr.isNblTrunc() ? Contexte.COLORS[Contexte.BR_YELLOW] + " mais d'autres lignes existent" + + Contexte.COLORS[Contexte.NONE] : "") + " (" + SQLCnx.frmDur(tm) + ")"); + else + vr.setDispValue("-> " + n + " ligne" + (n > 1 ? "s" : "") + " récupérée" + (n > 1 ? "s" : "") + + (vr.isNblTrunc() ? " mais d'autres lignes existent" : "") + " (" + SQLCnx.frmDur(tm) + ")"); + + cont.setValeur(vr); + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (SQLException e) { + cont.hideWheel(); + return cont.exception("SELECT", "ERREUR SQLException : " + e.getMessage(), lng, e); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " select (+instr) Consulte des données de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdSet.java b/src/lunasql/cmd/CmdSet.java new file mode 100644 index 0000000..a26a005 --- /dev/null +++ b/src/lunasql/cmd/CmdSet.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande SET
+ * (SQL) Affecte un paramètre interne de la base SQL + * @author M.P. + */ +public class CmdSet extends Instruction { + + public CmdSet(Contexte cont) { + super(cont, TYPE_CMDSQL, "SET", null); + } + + @Override + public int execute() { + return executeUpdate("SET", "paramètre fixé"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " set (+param) Fixe un paramètre de la base du SGBD\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdShell.java b/src/lunasql/cmd/CmdShell.java new file mode 100644 index 0000000..a1ce493 --- /dev/null +++ b/src/lunasql/cmd/CmdShell.java @@ -0,0 +1,123 @@ +package lunasql.cmd; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Tools; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande SHELL
+ * (Interne) Exécution d'une commande du shell externe + * @author M.P. + */ +public class CmdShell extends Instruction { + + private final OptionParser parser; + + public CmdShell(Contexte cont){ + super(cont, TYPE_CMDINT, "SHELL", "$"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("d", "ouverture en mode Desktop"); + parser.nonOptions("commande").ofType(String.class); + } + + @Override + public int execute() { + OptionSet options; + try { + options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lf = options.nonOptionArguments(); + if (lf.isEmpty()) + return cont.erreur("SHELL", "une commande Shell ou une ressource est attendue", lng); + + Valeur vr = new ValeurDef(cont); + if (options.has("d")) { + if (Desktop.isDesktopSupported()) { + Desktop dk = Desktop.getDesktop(); + String adr = (String) lf.get(0); + if (adr.matches("(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")){ + if (dk.isSupported(Desktop.Action.BROWSE)) dk.browse(new URI(adr)); + else return cont.erreur("SHELL", "Desktop mode BROWSE non supporté", lng); + } + else if(adr.matches("mailto:[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\?.*")) { + if (dk.isSupported(Desktop.Action.MAIL)) dk.mail(new URI(adr)); + else return cont.erreur("SHELL", "Desktop mode MAIL non supporté", lng); + } + else { + File fadr = new File(adr); + if (!fadr.isFile() || !fadr.canRead()){ + return cont.erreur("SHELL", "le fichier '" + fadr.getCanonicalPath() + + "' est inaccessible ou n'est pas une ressource valide", lng); + } + if (dk.isSupported(Desktop.Action.OPEN)) dk.open(fadr); + else return cont.erreur("SHELL", "Desktop mode OPEN non supporté", lng); + } + + vr.setDispValue("-> ressource ouverte : " + adr, Contexte.VERB_BVR); + vr.setSubValue(adr); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else return cont.erreur("SHELL", "Desktop non supporté sur cette plateforme", lng); + } + else { + if (((String) lf.get(0)).isEmpty()) return cont.erreur("SHELL", "Commande vide", lng); + else { + Process p = Runtime.getRuntime().exec(lf.toArray(new String[0])); + InputStream out = p.getInputStream(), err = p.getErrorStream(); + String s1 = Tools.streamToString(out), s2 = Tools.streamToString(err); + out.close(); + err.close(); + if (!s2.isEmpty()) cont.errprintln(s2); // contenu en STDERR pas forcément erreur + int status = p.waitFor(); + if (status != 0) return cont.erreur("SHELL", "Processus terminé avec code " + status, lng); + + vr.setDispValue("-> envoyé au shell : " + listToString(lf), Contexte.VERB_BVR); + vr.setSubValue(s1); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("SHELL", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException|URISyntaxException|IllegalArgumentException|InterruptedException ex) { + return cont.exception("SHELL", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " shell, $ Exécute la commande shell externe en paramètre\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdShow.java b/src/lunasql/cmd/CmdShow.java new file mode 100644 index 0000000..a67bcf8 --- /dev/null +++ b/src/lunasql/cmd/CmdShow.java @@ -0,0 +1,256 @@ +package lunasql.cmd; + +import static lunasql.sql.SQLCnx.frm; +import static lunasql.sql.SQLCnx.frmI; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.regex.PatternSyntaxException; + +import jline.ConsoleReader; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.sql.TypesSGBD; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; +//import jline.console.ConsoleReader; + +/** + * Commande SHOW
+ * (Interne) Affichage des caractéristiques des tables/objets de la base + * @author M.P. + */ +public class CmdShow extends Instruction { + + private final OptionParser parser; + + public CmdShow(Contexte cont) { + super(cont, TYPE_CMDINT, "SHOW", "#"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("t", "tables utilisateur"); + parser.accepts("v", "vues"); + parser.accepts("s", "tables système"); + parser.accepts("l", "comptage du nb de lignes"); + parser.accepts("c", "liste des catalogs"); + parser.accepts("m", "liste des schémas"); + } + + @Override + public int execute() { + OptionSet options; + try { + options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + List lc = options.nonOptionArguments(); + StringBuilder sb = new StringBuilder(); + + int[] rowCounts; + String schema = null; + ArrayList tables = new ArrayList<>(), + schemas = new ArrayList<>(), + remarks = new ArrayList<>(); + long tm = System.currentTimeMillis(); + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + + // Tables et vues + String[] types = new String[] {}; + if (options.has("v")) + types = Tools.arrayAppend(types, "VIEW"); + if (options.has("t")) + types = Tools.arrayAppend(types, "TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"); + if (options.has("s")) + types = Tools.arrayAppend(types, "SYSTEM TABLE"); + if (types.length == 0) types = new String[] { + "TABLE", "VIEW", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"}; + + ResultSet result = dMeta.getTables(null, null, null, types); + while(result.next()) { + schema = result.getString(2); // on sauvegarde le dernier + schemas.add(schema); + tables.add(result.getString(3)); + remarks.add(result.getString(5)); + } + result.close(); + rowCounts = new int[tables.size()]; + + // Catalogs + if (options.has("c")) { + ResultSet rscat = dMeta.getCatalogs(); + int n = 0; + sb.append(frm("Catalogs", 30, ' ')).append('\n').append(frm("", 30, '-')).append('\n'); + while (rscat.next()) { + n++; + sb.append(rscat.getString("TABLE_CAT")).append('\n'); + } + rscat.close(); + vr.setSubValue(Integer.toString(n)); + } + + // Schémas + else if (options.has("m")) { + ResultSet rscat = dMeta.getSchemas(); + int n = 0; + sb.append(frm("Schémas", 30, ' ')).append('\n').append(frm("", 30, '-')).append('\n'); + while (rscat.next()) { + n++; + String cat = rscat.getString("TABLE_CATALOG"); + if (cat != null) sb.append(cat).append('.'); + sb.append(rscat.getString("TABLE_SCHEM")).append('\n'); + } + rscat.close(); + vr.setSubValue(Integer.toString(n)); + } + + // Affichage de la structure globale des tables non sys + else if (lc.isEmpty() || options.has("t")) { + int t = cont.getConnex().getType(); + // Test du SGBD : si Oracle ou SQLServer (les usines à gaz), on demande confirmation + if (t == TypesSGBD.TYPE_ORACLE || t == TypesSGBD.TYPE_SQLSERV) { + try { + String p = "Nous sommes sur un SGBD sensible ! Continuer (O/N) ? ", val; + ConsoleReader reader = cont.getConsoleReader(); + if(reader == null){ + if (cont.getVerbose() >= Contexte.VERB_AFF) cont.print(p); + val = new BufferedReader(new InputStreamReader(System.in)).readLine(); + } else { + val = reader.readLine(p); + if (val == null) val = ""; + } + if (!val.equalsIgnoreCase("O")) return RET_CONTINUE; + } catch (IOException ex) { + return cont.exception("SHOW", "erreur IOException : " + ex.getMessage(), lng, ex); + } + } + sb.append(frm("Nom des tables", 20, ' ')).append(frm("Nb lignes", 11, ' ')) + .append(frm("Schéma", 20, ' ')).append(frm("Description", 25, ' ')) + .append('\n').append(frm("", 80, '-')).append('\n'); + for (int i = 0; i < tables.size(); i++) { + try { + String schp = schemas.get(i); + if (!options.has("l")) rowCounts[i] = 0; + else { + int n = cont.getConnex().seekNbl((schp == null ? "" : schp + ".") + tables.get(i)); + rowCounts[i] = n; + } + sb.append(frm(tables.get(i), 19, ' ')).append(' ') + .append(frmI("" + rowCounts[i], 10, ' ')).append(" ") + .append(frm(schemas.get(i), 24, ' ')).append(' ') + .append(remarks.get(i)).append('\n'); // desciption non formatée + } + catch (Exception ex) { + cont.exception("SHOW", "impossible d'obtenir la taille de " + tables.get(i), lng, ex); + } + } + tm = System.currentTimeMillis() - tm; + vr.setDispValue("(" + SQLCnx.frmDur(tm) + ")", Contexte.VERB_MSG); + vr.setSubValue(Integer.toString(tables.size())); + } + + // Affichage de la structure d'une table + else if (lc.size() >= 1) for (int i = 0; i < lc.size(); i++ ) { + String tblist = (String)lc.get(i), tbname = null; + boolean prem = i == 0; + // Recherche de la table (en ignorant la casse) ou de x tables nommées par regexp + for (String table : tables) { + if (table.matches("(?i)" + Tools.removeBQuotes(tblist).value)) { + tbname = table; + tm = System.currentTimeMillis(); + + // Affichage des informations + HashMap fk = new HashMap<>(); + ResultSet rsfk = dMeta.getExportedKeys(null, schema, tbname); + while (rsfk.next()) { + String k = rsfk.getString("PKCOLUMN_NAME"), + v = fk.get(rsfk.getString("PKCOLUMN_NAME")); + fk.put(k, (v == null ? "" : v + ", ") + + rsfk.getString("FKTABLE_NAME") + "(" + rsfk.getString("FKCOLUMN_NAME") + ")"); + } + rsfk.close(); + ResultSet rscol = dMeta.getColumns(null, schema, tbname, null); + sb.append(prem ? "" : "\n") + .append("Table trouvée : ").append(tbname).append('\n') + .append(frm("Colonne", 20)) + .append(frm("Type", 10)) + .append(frm("Taille", 10)) + .append(frm("Null?", 6)) + .append(frm("Référencé par", 30)) + .append(frm("Commentaire", 25)).append('\n') + .append(frm("", 99, '-')).append('\n'); + int nbcol = 0; + while (rscol.next()) { + nbcol++; + String col = rscol.getString(4); + sb.append(frm(col, 19, ' ')).append(' ') // Colonne + .append(frm(rscol.getString(6), 9, ' ')).append(' ') // Type + .append(frm(Integer.toString(rscol.getInt(7)), 9, ' ')).append(' ') // Taille + .append(rscol.getInt(11) != 0 ? "oui" : "non").append(" "); // Null + if (fk.containsKey(col)) sb.append(frm(fk.get(col), 30)); + sb.append(frm(rscol.getString(12), 24, ' ')).append(' ') // Commentaire + .append('\n'); + } + rscol.close(); + + // Count lignes si option -l + if (options.has("l")) { + tm = System.currentTimeMillis() - tm; + vr.setDispValue("Nombre de lignes : " + cont.getConnex().seekNbl(tbname) + + " (" + SQLCnx.frmDur(tm) + ")", Contexte.VERB_MSG); + } + vr.setSubValue(Integer.toString(nbcol)); + prem = false; + } + } + if (tbname == null) + return cont.erreur("SHOW", "Impossible de trouver la table '" + tblist + "'", lng); + } + + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_AFF); + vr.setRet(); + cont.setValeur(vr); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("EXPORT", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (PatternSyntaxException ex) { + return cont.erreur("SHOW", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + catch (IOException|SQLException ex) { + return cont.exception("SHOW", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " show, # Affiche le catalogue complet des objets de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdSize.java b/src/lunasql/cmd/CmdSize.java new file mode 100644 index 0000000..89ce9fd --- /dev/null +++ b/src/lunasql/cmd/CmdSize.java @@ -0,0 +1,92 @@ +package lunasql.cmd; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.regex.PatternSyntaxException; + +import lunasql.lib.Contexte; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande SIZE
+ * (Interne) Affichage du nombre de lignes d'une table de la base + * @author M.P. + */ +public class CmdSize extends Instruction { + + private int nbl; + private Valeur vr; + + public CmdSize(Contexte cont) { + super(cont, TYPE_CMDINT, "SIZE", "*"); + } + + @Override + public int execute() { + if(getLength() == 1) + return cont.erreur("SIZE", "un nom d'objet (table ou vue) au moins est attendu", lng); + + String[] usertables = {"TABLE", "GLOBAL TEMPORARY", "VIEW"}; + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + int ret = RET_CONTINUE; + nbl = 0; + vr = null; + for (int i = 1; i < getLength(); i++) { // pour chaque objet + try { + ResultSet result = dMeta.getTables(null, null, null, usertables); + String tb, nom = Tools.removeBQuotes(getArg(i)).value; + boolean found = false; + while(result.next()){ + tb = result.getString(3); + if (tb.matches("(?i)" + nom)) { + found = true; + seekSize(tb, true); + } + } + if (!found) seekSize(nom, false); + cont.setValeur(vr); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + + } + catch (SQLException ex) { + ret = cont.exception("SIZE", "ERREUR SQLException : "+ ex.getMessage(), lng, ex); + } + catch (PatternSyntaxException ex) { + ret = cont.erreur("SIZE", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + return ret; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " size, * Affiche la taille de la (ou les) table(s) en paramètre\n"; + } + + /* + * Construit la chaîne à afficher en fonction de la table donnée + * @param tb le nom de la table + * @return le nombre de lignes de la table tb + */ + private void seekSize(String tb, boolean f) throws SQLException { + long tm = System.currentTimeMillis(); + int v = cont.getConnex().seekNbl(tb); + tm = System.currentTimeMillis() - tm; + if (vr == null) vr = new ValeurDef(cont); + else vr.appendDispValue("\n", Contexte.VERB_AFF); + nbl += v; + vr.appendDispValue("- " + SQLCnx.frm(tb.toUpperCase() + ' ' + (f ? "(user)" : "(non user)"), 33, ' ') + + ' ' + v + " ligne" + (v > 1 ? "s" : "") + " (" + SQLCnx.frmDur(tm) + ")", Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(nbl)); + vr.setRet(Integer.toString(v)); + } +}// class + diff --git a/src/lunasql/cmd/CmdSpec.java b/src/lunasql/cmd/CmdSpec.java new file mode 100644 index 0000000..32d53c0 --- /dev/null +++ b/src/lunasql/cmd/CmdSpec.java @@ -0,0 +1,99 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurExe; +import lunasql.val.ValeurReq; + +/** + * Commande SPEC
+ * (SQL) Exécution d'une commande spécifique d'une base de données + * @author M.P. + */ +public class CmdSpec extends Instruction { + + private final OptionParser parser; + + public CmdSpec(Contexte cont) { + super(cont, TYPE_CMDSQL, "SPEC", null); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("s", "commande de sélection"); + parser.nonOptions("commande_sql").ofType(String.class); + } + + @Override + public int execute() { + OptionSet options; + try { + options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + List lc = options.nonOptionArguments(); + if (lc.isEmpty()) + return cont.erreur("SPEC", "une commande SQL est attendue", lng); + + long tm; + if (options.has("s")) { + tm = System.currentTimeMillis(); + cont.showWheel(); + ValeurReq vr = new ValeurReq(cont, cont.getConnex().getResultString(listToString(lc), + cont.getVar(Contexte.ENV_SELECT_ARR).equals(Contexte.STATE_TRUE), + cont.getColMaxWidth(lng), cont.getRowMaxNumber(lng), + cont.getVar(Contexte.ENV_ADD_ROW_NB).equals(Contexte.STATE_TRUE), + cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE))); + tm = System.currentTimeMillis() - tm; + cont.hideWheel(); + + int n = vr.getNbLines(); + vr.setDispValue("-> " + n + " ligne" + (n > 1 ? "s" : "") + " trouvée" + (n > 1 ? "s" : "") + " (" + + SQLCnx.frmDur(tm) + ")"); + cont.setValeur(vr); + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + else { + cont.showWheel(); + tm = System.currentTimeMillis(); + Valeur vr = new ValeurExe(cont, cont.getConnex().execute(listToString(lc))); + tm = System.currentTimeMillis() - tm; + cont.hideWheel(); + vr.setDispValue("-> commande exécutée (" + SQLCnx.frmDur(tm) + ")"); + cont.setValeur(vr); + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + return RET_CONTINUE; + } + catch (IOException|SQLException ex) { + cont.hideWheel(); + return cont.exception("SPEC", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " spec command Lance une commande spécifique à la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdSpool.java b/src/lunasql/cmd/CmdSpool.java new file mode 100644 index 0000000..a4a30f1 --- /dev/null +++ b/src/lunasql/cmd/CmdSpool.java @@ -0,0 +1,190 @@ +package lunasql.cmd; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.List; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; +//import jline.console.ConsoleReader; + +/** + * Commande SPOOL
+ * (Interne) Redirection de la sortie standard vers un fichier + * @author M.P. + */ +public class CmdSpool extends Instruction { + + private final OptionParser parser; + private PrintStream streamout, streamerr; + private String outname, errname; + + public CmdSpool(Contexte cont) { + super(cont, TYPE_CMDINT, "SPOOL", "!"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("f", "nom de fichier de sortie"); + parser.accepts("u", "nom de fichier d'erreur"); + parser.accepts("c", "ferme les redirections en cours"); + parser.accepts("s", "suspend les redirections en cours"); + parser.accepts("r", "reprend les redirections en cours"); + parser.accepts("a", "retour seul de l'état actuel"); + parser.accepts("w", "écrase le fichier si existe"); + parser.accepts("e", "redirection de la sortie des erreurs").withRequiredArg().ofType(File.class) + .describedAs("fichier"); + parser.nonOptions("fichier").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + List la = options.nonOptionArguments(); + if (la.size() > 1) + return cont.erreur("SPOOL", "une seule option ou commande ou un nom de fichier est attendu", lng); + + String cmd = la.isEmpty() ? "" : ((String) la.get(0)).toUpperCase(); + if (options.has("f") || cmd.equals("FILE")) { + vr.setSubValue(outname == null ? "" : outname); + } + else if (options.has("u") || cmd.equals("ERRFILE")) { + vr.setSubValue(errname == null ? "" : errname); + } + else if (options.has("c") || cmd.equals("OFF")) { + if (streamout == null && streamerr == null) + return cont.erreur("SPOOL", "aucune sortie vers fichier en cours", lng); + if (streamout != null) { + streamout.close(); + streamout = null; + outname = null; + } + if (streamerr != null) { + streamerr.close(); + streamerr = null; + errname = null; + } + cont.setWriter(null); + cont.setErrWriter(null); + vr.setDispValue(""); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("s") || cmd.equals("SUSPEND")) { + if (streamout == null && streamerr == null) + return cont.erreur("SPOOL", "aucune sortie vers fichier en cours", lng); + cont.setWriter(null); + cont.setErrWriter(null); + vr.setDispValue(""); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else if (options.has("r") || cmd.equals("RESUME")) { + if (streamout == null && streamerr == null) + return cont.erreur("SPOOL", "aucune sortie vers fichier en cours", lng); + if (cont.getWriter() != null && cont.getErrWriter() != null) { + System.err.println("La sortie vers fichier n'est pas suspendue"); // sur console + return cont.erreur("SPOOL", "la sortie vers fichier n'est pas suspendue", lng); + } + cont.setWriter(streamout); + cont.setErrWriter(streamerr); + vr.setDispValue(""); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else { + boolean iserr = options.has("e"); + if (la.size() == 1 || iserr) { // noms de fichier de redirection (out et/ou err) + // Ouverture + boolean isout = la.size() == 1, append = !options.has("w"); + if (isout) { + File fout = new File((String) la.get(0)); + if (cont.askWriteFile(fout)) { + if (streamout != null) streamout.close(); + outname = fout.getCanonicalPath(); + streamout = new PrintStream(new FileOutputStream(fout, append), true, + cont.getVar(Contexte.ENV_FILE_ENC)); + cont.setWriter(streamout, outname); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else isout = false; + } + if (iserr) { + if (streamerr != null) streamerr.close(); + File ferr = (File) options.valueOf("e"); + if (cont.askWriteFile(ferr)) { + errname = ferr.getCanonicalPath(); + streamerr = new PrintStream(new FileOutputStream(ferr, append), true, + cont.getVar(Contexte.ENV_FILE_ENC)); + cont.setErrWriter(streamerr, errname); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + else iserr = false; + } + // Rapport + if ((isout || iserr) && cont.getVerbose() >= Contexte.VERB_BVR) { + String comm = (outname == null ? "" : "Redirection de la sortie vers " + outname + "\n") + + (errname == null ? "" : "Redirection de l'erreur vers " + errname); + System.out.println(comm); + vr.setDispValue(comm, Contexte.VERB_BVR); + } + else { + vr.setDispValue(""); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + vr.setSubValue(null); + } + else { // sans option ni commande + String sw, ew; + // out + if (streamout == null) sw = "OFF"; + else if (cont.getWriter() == null) sw = "SUSPEND:" + outname; + else sw = cont.getWriterName(); + // err + if (streamerr == null) ew = "OFF"; + else if (cont.getErrWriter() == null) ew = "SUSPEND:" + errname; + else ew = cont.getErrWriterName(); + // Rapport + if (!options.has("a") && cont.getVerbose() >= Contexte.VERB_AFF) { + if (sw != null) System.out.println("Sortie normale : " + sw); // sur console uniquement + if (ew != null) System.out.println("Sortie erreurs : " + ew); // idem + } + vr.setDispValue(""); + vr.setSubValue(sw + (ew == null ? "" : ";" + ew)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + } + } + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("SPOOL", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("SPOOL", "ERREUR IOException : "+ getArg(1), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " spool, ! Redirige les sorties OUT et ERR vers le fichier indiqué\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdStart.java b/src/lunasql/cmd/CmdStart.java new file mode 100644 index 0000000..8432e64 --- /dev/null +++ b/src/lunasql/cmd/CmdStart.java @@ -0,0 +1,46 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +import java.util.ConcurrentModificationException; + +/** + * Commande START
+ * (Interne) Exécution d'un fichier de commandes Etend CmdExec pour en hériter + * les fonctions d'exécution + * @author M.P. + */ +public class CmdStart extends CmdExec { + + public int retValue; + + public CmdStart(Contexte cont) { + super(cont, "START", "@@"); + } + + @Override + public int execute() { + try { + //Contexte localctx = new Contexte(cont); // TODO: exécuter START en new Contexte : constructeur copie + (new Thread(() -> retValue = CmdStart.super.execute())).start(); + } + catch (ConcurrentModificationException ex) { + // On vous avait bien prévenu que START était risquée... + retValue = cont.exception("START", "ERREUR ConcurrentModificationException : " + ex.getMessage() + + "\n\tSachez ce que vous faites quand vous jouez avec la concurrence !", lng, ex); + //} catch (IOException ex) { + // retValue = cont.exception("START", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + return retValue; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " start, @@ Comme EXEC, mais l'exécution est faite en tâche de fond\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdStr.java b/src/lunasql/cmd/CmdStr.java new file mode 100644 index 0000000..6cd14ca --- /dev/null +++ b/src/lunasql/cmd/CmdStr.java @@ -0,0 +1,606 @@ +package lunasql.cmd; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.zip.CRC32; +import java.util.zip.Checksum; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Security; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.ValeurDef; +import opencsv.CSVReader; + +/** + * Commande STR
+ * (Interne) Commande d'utilitaires de traitement d'une chaîne de caractères + * @author M.P. + */ +public class CmdStr extends Instruction { + + public CmdStr(Contexte cont){ + super(cont, TYPE_CMDINT, "STR", null); + } + + @Override + public int execute() { + if (getLength() < 2) + return cont.erreur("STR", "une commande de chaîne est requise", lng); + + String cmd = getArg(1).toUpperCase(), str; + if (cmd.equals("NEW")) { + str = getSCommand(2); + } + else if (cmd.equals("LEN")) { + if (getLength() != 3) return cont.erreur("STR", "LEN : 1 chaîne attendue", lng); + str = Integer.toString(getArg(2).length()); + } + else if (cmd.equals("EQ?")) { + if (getLength() != 4) return cont.erreur("STR", "EQ? : 2 chaînes attendue", lng); + str = getArg(2).equals(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("EQ-IC?")) { + if (getLength() != 4) return cont.erreur("STR", "EQ-IC? : 2 chaînes attendue", lng); + str = getArg(2).equalsIgnoreCase(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("NEQ?")) { + if (getLength() != 4) return cont.erreur("STR", "NEQ? : 2 chaînes attendue", lng); + str = getArg(2).equals(getArg(3)) ? "0" : "1"; + } + else if (cmd.equals("NEQ-IC?")) { + if (getLength() != 4) return cont.erreur("STR", "NEQ-IC? : 2 chaînes attendue", lng); + str = getArg(2).equalsIgnoreCase(getArg(3)) ? "0" : "1"; + } + else if (cmd.equals("EMPTY?")) { + if (getLength() != 3) return cont.erreur("STR", "EMPTY? : 1 chaîne attendue", lng); + str = getArg(2).isEmpty() ? "1" : "0"; + } + else if (cmd.equals("NEMPTY?")) { + if (getLength() != 3) return cont.erreur("STR", "NEMPTY? : 1 chaîne attendue", lng); + str = getArg(2).isEmpty() ? "0" : "1"; + } + else if (cmd.equals("INDEX")) { + if (getLength() != 4) return cont.erreur("STR", "INDEX : 2 chaînes attendues", lng); + str = Integer.toString(getArg(2).indexOf(getArg(3))); + } + else if (cmd.equals("LINDEX")) { + if (getLength() != 4) return cont.erreur("STR", "LINDEX : 2 chaînes attendues", lng); + str = Integer.toString(getArg(2).lastIndexOf(getArg(3))); + } + else if (cmd.equals("SUBSTR")) { + if (getLength() != 4) return cont.erreur("STR", "SUBSTR : 1 chaîne, 1 index attendus", lng); + try { str = Tools.subString(getArg(2), getArg(3)); } + catch (NumberFormatException ex) { + return cont.erreur("STR", "SUBSTR : format de sous-chaîne incorrect : " + getArg(3), lng); + } + } + else if (cmd.equals("REPLACE")) { + if (getLength() != 5) return cont.erreur("STR", "REPLACE : 1 chaîne, 1 regexp, 1 chaîne attendues", lng); + try { str = getArg(2).replaceAll(Tools.removeBQuotes(getArg(3)).value, getArg(4)); } + catch (PatternSyntaxException ex) { + return cont.erreur("STR", "syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("CUT")) { + int l = getLength(); + if (l < 4 || l > 5) return cont.erreur("STR", "CUT : 1 chaîne, 1 nb, [1 chaîne] attendus", lng); + int nb; + try { + nb = Integer.parseInt(getArg(3)); + if (nb < 0) throw new NumberFormatException(nb + " (nombre positif attendu)"); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "CUT : nombre incorrect : " + ex.getMessage(), lng); + } + String s = getArg(2), c = l == 5 ? getArg(4) : ""; + if (nb >= s.length()) str = s; + else if (nb < c.length()) str = c.substring(0, nb); + else str = s.substring(0, nb - c.length()) + c; + } + else if (cmd.equals("HAS?")) { + if (getLength() != 4) return cont.erreur("STR", "HAS? : 2 chaînes attendues", lng); + str = getArg(2).contains(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("DIGIT?")) { + if (getLength() != 3) return cont.erreur("STR", "DIGIT? : 1 chaîne attendue", lng); + str = getArg(2).matches("\\d+") ? "1" : "0"; + } + else if (cmd.equals("NUM?")) { + if (getLength() != 3) return cont.erreur("STR", "NUMBER? : 1 chaîne attendue", lng); + str = getArg(2).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?") ? "1" : "0"; + } + else if (cmd.equals("ALPHA?")) { + if (getLength() != 3) return cont.erreur("STR", "ALPHA? : 1 chaîne attendue", lng); + str = getArg(2).matches("(?i)[A-Z]+") ? "1" : "0"; + } + else if (cmd.equals("ALNUM?")) { + if (getLength() != 3) return cont.erreur("STR", "ALNUM? : 1 chaîne attendue", lng); + str = getArg(2).matches("(?i)[A-Z0-9]+") ? "1" : "0"; + } + else if (cmd.equals("LIST?")) { + if (getLength() != 3) return cont.erreur("STR", "LIST? : 1 chaîne attendue", lng); + str = getArg(2).indexOf(' ') > 0 ? "1" : "0"; + } + else if (cmd.equals("DICT?")) { + if (getLength() != 3) return cont.erreur("STR", "DICT? : 1 chaîne attendue", lng); + Properties prop = Tools.getProp(getArg(2)); + if (prop == null) return cont.erreur("STR", "DICT? : dictionnaire incorrect", lng); + boolean isdict = !prop.isEmpty(); + for (Map.Entry kv : prop.entrySet()) { + String k = (String)kv.getKey(), v = (String)kv.getValue(); + isdict = !k.isEmpty() && !v.isEmpty(); + if (!isdict) break; + } + str = isdict ? "1" : "0"; + } + else if (cmd.equals("DATE?")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "DATE? : 1 chaîne, [1 format] attendu", lng); + try { + java.text.SimpleDateFormat df = new java.text.SimpleDateFormat(l == 3 ? "dd/MM/yyyy" : getArg(3)); + df.setLenient(false); + df.parse(getArg(2)); + str = "1"; + } catch (java.text.ParseException e) { + str = "0"; + } catch (IllegalArgumentException ex) { + return cont.erreur("STR", "DATE? : format de date incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("UPPER")) { + if (getLength() != 3) return cont.erreur("STR", "UPPER : 1 chaîne attendue", lng); + str = getArg(2).toUpperCase(); + } + else if (cmd.equals("LOWER")) { + if (getLength() != 3) return cont.erreur("STR", "LOWER : 1 chaîne attendue", lng); + str = getArg(2).toLowerCase(); + } + else if (cmd.equals("CAPIT")) { + if (getLength() != 3) return cont.erreur("STR", "CAPIT : 1 chaîne attendue", lng); + String s = getArg(2); + str = s.length() > 1 ? s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase() + : s.toUpperCase(); + } + else if (cmd.equals("TRIM")) { + if (getLength() != 3) return cont.erreur("STR", "TRIM : 1 chaîne attendue", lng); + str = getArg(2).trim(); + } + else if (cmd.equals("RTRIM")) { + if (getLength() != 3) return cont.erreur("STR", "RTRIM : 1 chaîne attendue", lng); + str = getArg(2).replaceAll("\\s+$",""); + } + else if (cmd.equals("LTRIM")) { + if (getLength() != 3) return cont.erreur("STR", "LTRIM : 1 chaîne attendue", lng); + str = getArg(2).replaceAll("^\\s+",""); + } + else if (cmd.equals("TRIMALL")) { + if (getLength() != 3) return cont.erreur("STR", "TRIMALL : 1 chaîne attendue", lng); + str = getArg(2).trim().replaceAll("\\s+"," "); + } + else if (cmd.equals("RPAD")) { + int l = getLength(); + if (l < 4 || l > 5) return cont.erreur("STR", "RPAD : 1 chaîne, 1 nb, [1 car.] attendus", lng); + int nb; + try { nb = Integer.parseInt(getArg(3)); } + catch (NumberFormatException ex) { + return cont.erreur("STR", "RPAD : nombre incorrect : " + ex.getMessage(), lng); + } + String s; + char c = l == 5 && !(s = getArg(4)).isEmpty() ? s.charAt(0) : ' '; + str = SQLCnx.frm(getArg(2), nb, c); + } + else if (cmd.equals("LPAD")) { + int l = getLength(); + if (l < 4 || l > 5) return cont.erreur("STR", "LPAD : 1 chaîne, 1 nb, [1 car.] attendus", lng); + int nb; + try { nb = Integer.parseInt(getArg(3)); } + catch (NumberFormatException ex) { + return cont.erreur("STR", "LPAD : nombre incorrect : " + ex.getMessage(), lng); + } + String s; + char c = l == 5 && !(s = getArg(4)).isEmpty() ? s.charAt(0) : ' '; + str = SQLCnx.frmI(getArg(2), nb, c); + } + else if (cmd.equals("STARTS?")) { + if (getLength() != 4) return cont.erreur("STR", "STARTS? : 2 chaînes attendues", lng); + str = getArg(2).startsWith(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("ENDS?")) { + if (getLength() != 4) return cont.erreur("STR", "ENDS? : 2 chaînes attendues", lng); + str = getArg(2).endsWith(getArg(3)) ? "1" : "0"; + } + else if (cmd.equals("COMP")) { + if (getLength() != 4) return cont.erreur("STR", "COMP : 2 chaînes attendues", lng); + str = Integer.toString(getArg(3).compareToIgnoreCase(getArg(2))); + } + else if (cmd.equals("COMP-IC")) { + if (getLength() != 4) return cont.erreur("STR", "COMP-IC : 2 chaînes attendues", lng); + str = Integer.toString(getArg(3).compareToIgnoreCase(getArg(2))); + } + else if (cmd.equals("MATCHES?")) { + if (getLength() != 4) return cont.erreur("STR", "MATCHES? : 1 chaîne, 1 regexp attendues", lng); + try { str = getArg(2).matches(Tools.removeBQuotes(getArg(3)).value) ? "1" : "0"; } + catch (PatternSyntaxException ex) { + return cont.erreur("STR", "MATCHES? : syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("SPLIT")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "SPLIT : 1 chaîne, [1 regexp] attendues", lng); + try { + List lt = Arrays.asList + (getArg(2).split(l == 3 ? " " : Tools.removeBQuotes(getArg(3)).value)); + str = Tools.arrayToString(lt); + } + catch (PatternSyntaxException ex) { + return cont.erreur("STR", "SPLIT : syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("CONCAT")) { + int l = getLength(); + if (l < 3) return cont.erreur("STR", "CONCAT : chaînes attendues", lng); + StringBuilder sb = new StringBuilder(); + for (int i = 2; i <= l; i++) sb.append(getArg(i)); + str = sb.toString(); + } + else if (cmd.equals("REPEAT")) { + if (getLength() != 4) return cont.erreur("STR", "REPEAT : 1 chaîne, 1 nb attendus", lng); + int nb; + try { nb = Integer.parseInt(getArg(3)); } + catch (NumberFormatException ex) { + return cont.erreur("STR", "REPEAT : nombre incorrect : " + ex.getMessage(), lng); + } + StringBuilder sb = new StringBuilder(); + for (int i = 1; i <= nb; i++) sb.append(getArg(2)); + str = sb.toString(); + } + else if (cmd.equals("FIRST")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "FIRST : 1 chaîne, [1 nombre] attendus", lng); + String s = getArg(2); + int nb; + if (l == 4) { + try { + nb = Math.min(Integer.parseInt(getArg(3)), s.length()); + if (nb < 0) throw new NumberFormatException("négatif : " + nb); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "FIRST : nombre incorrect : " + ex.getMessage(), lng); + } + } + else nb = 1; + str = s.isEmpty() || nb == 0 ? "" : s.substring(0, nb); + } + else if (cmd.equals("LAST")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "LAST : 1 chaîne, [1 nombre] attendus", lng); + String s = getArg(2); + int nb; + if (l == 4) { + try { + nb = Math.min(Integer.parseInt(getArg(3)), s.length()); + if (nb < 0) throw new NumberFormatException("négatif : " + nb); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "LAST : nombre incorrect : " + ex.getMessage(), lng); + } + } + else nb = 1; + str = s.isEmpty() || nb == 0 ? "" : s.substring(s.length() - nb); + } + else if (cmd.equals("CHOP")) { + if (getLength() != 3) return cont.erreur("STR", "CHOP : 1 chaîne attendue", lng); + String s = getArg(2); + str = s.length() > 0 ? s.substring(0, s.length() - 1) : s; + } + else if (cmd.equals("REVERSE")) { + if (getLength() != 3) return cont.erreur("STR", "REVERSE : 1 chaîne attendue", lng); + StringBuilder sb = new StringBuilder(getArg(2)); + str = sb.reverse().toString(); + } + else if (cmd.equals("FIND")) { + if (getLength() != 4) return cont.erreur("STR", "FIND : 1 chaîne, 1 regexp attendues", lng); + try { + Matcher m = Pattern.compile(Tools.removeBQuotes(getArg(3)).value).matcher(getArg(2)); + StringBuilder sb = new StringBuilder(); + while (m.find()) sb.append(m.start()).append(' '); + str = sb.toString(); + } + catch (PatternSyntaxException ex) { + return cont.erreur("STR", "FIND : syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("GROUPS")) { + if (getLength() != 4) return cont.erreur("STR", "GROUPS : 1 chaîne, 1 regexp attendues", lng); + try { + Matcher m = Pattern.compile(Tools.removeBQuotes(getArg(3)).value).matcher(getArg(2)); + Properties pr = new Properties(); + if (m.matches()) { + for (int i = 0; i <= m.groupCount(); i++) pr.put(Integer.toString(i), m.group(i)); + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + pr.store(os, null); + str = os.toString(cont.getVar(Contexte.ENV_FILE_ENC)); + } catch (PatternSyntaxException ex) { + return cont.erreur("STR", "GROUPS : syntaxe regexp incorrecte : " + ex.getMessage(), lng); + } catch (IOException ex) { + return cont.erreur("STR", "GROUPS : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("FORMAT")) { + int l = getLength(); + if (l < 4) return cont.erreur("STR", "FORMAT : 1 chaîne, x val attendues", lng); + l = l - 3; + Object[] t = new Object[l]; + try { + for (int i = 0; i < l; i++) { + String s = getArg(i + 3); + if (s.matches("[0-9]+")) t[i] = Integer.parseInt(s); + else if (s.matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?")) t[i] = Float.parseFloat(s); + else t[i] = s; + } + str = String.format(getArg(2), t); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "FORMAT : nombre incorrect : " + ex.getMessage(), lng); + } + catch (IllegalFormatException ex) { + return cont.erreur("STR", "FORMAT : format incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("CONVERT")) { + if (getLength() != 5) return cont.erreur("STR", "CONVERT : 1 chaîne, 2 ensembles attendues", lng); + char[] s = getArg(2).toCharArray(), r = Arrays.copyOfRange(s, 0, s.length), + seti = Tools.charRangeToSet(getArg(3)).toCharArray(), + seto = Tools.charRangeToSet(getArg(4)).toCharArray(); + if (seti.length != seto.length) + return cont.erreur("STR", "CONVERT : ensembles de tailles différentes (" + + seti.length + " contre " + seto.length + ")", lng); + // Parcours de la chaîne pour remplacement + for (int i = 0; i < s.length; i++) { + for (int j = 0; j < seti.length; j++) { + if (s[i] == seti[j]) { r[i] = seto[j]; break; } + } + } + str = new String(r); + } + else if (cmd.equals("CLEANSQL")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "CLEANSQL : 1 chaîne, [indent 0/1/2] attendus", lng); + int mode; // mode formaté et indenté par défaut + if (l > 3) { + if (getArg(3).equals("0")) mode = -2; // ni formaté, ni indenté + else if (getArg(3).equals("1")) mode = -1; // formaté, non indenté + else if (getArg(3).equals("2")) mode = 0; // formaté et indenté + else return cont.erreur("STR", "CLEANSQL : mode 0/1/2 attendu", lng); + } + else mode = 0; + str = Tools.cleanSQLCode(cont, getArg(2), mode); + } + else if (cmd.equals("WRAP")) { + int l = getLength(); + if (l < 3 || l > 6) + return cont.erreur("STR", "WRAP : 1 chaîne, [1 longueur, 1 chaîne, 1 mode] attendus", lng); + int lg = 70; + String mrg = ""; + int idt = 0; + try { + if (l > 3) { + lg = Integer.parseInt(getArg(3)); + if (l > 4) { + mrg = getArg(4); + if (l > 5) { + idt = Integer.parseInt(getArg(5)); + if (idt < -1 || idt > 1) + return cont.erreur("STR", "WRAP : mode de retrait incorrect : " + idt, lng); + } + } + } + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "WRAP : nombre incorrect : " + ex.getMessage(), lng); + } + str = Tools.textToLines(getArg(2), lg, mrg, idt); + } + else if (cmd.equals("ASC")) { + String s; + if (getLength() != 3 || (s = getArg(2)).isEmpty()) + return cont.erreur("STR", "ASC : 1 chaîne non vide attendue", lng); + str = Integer.toString(s.charAt(0)); + } + else if (cmd.equals("CHR")) { + if (getLength() != 3) return cont.erreur("STR", "CHR : 1 code car. attendu", lng); + try { + str = "" + (char)Integer.parseInt(getArg(2)); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "ORD : nombre incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("NEXT")) { + if (getLength() != 3) return cont.erreur("STR", "NEXT : 1 chaîne attendue", lng); + try { str = Lecteur.stringInc(getArg(2)); } + catch (IllegalArgumentException ex) { + return cont.erreur("STR", "NEXT : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("B64ENC")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "B64ENC : 1 chaîne, [1 nombre] attendus", lng); + try { + str = l == 3 ? Security.b64encode(getArg(2).getBytes()) : + Security.b64encode(getArg(2).getBytes(), Integer.parseInt(getArg(3))); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "B64ENC : nombre incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("B64DEC")) { + if (getLength() != 3) return cont.erreur("STR", "B64DEC : 1 chaîne attendue", lng); + try { + str = new String(Security.b64decode(getArg(2))); + } catch (IllegalArgumentException ex) { + return cont.erreur("STR", "B64DEC : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("HEXENC")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "HEXENC : 1 chaîne, [1 nombre] attendus", lng); + try { + str = l == 3 ? Security.hexencode(getArg(2).getBytes()) : + Security.hexencode(getArg(2).getBytes(), Integer.parseInt(getArg(3))); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "HEXENC : nombre incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("HEXDEC")) { + if (getLength() != 3) return cont.erreur("STR", "HEXDEC : 1 chaîne attendue", lng); + try { + str = new String(Security.hexdecode(getArg(2))); + } catch (IllegalArgumentException ex) { + return cont.erreur("STR", "HEXDEC : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("ZIPENC")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "ZIPENC : 1 chaîne, [1 nombre] attendue", lng); + try { + int nbc = l == 3 ? 0 : Integer.parseInt(getArg(3)); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + GZIPOutputStream gos = new GZIPOutputStream(bos, 4096); + gos.write(getArg(2).getBytes(cont.getVar(Contexte.ENV_FILE_ENC))); + gos.finish(); + str = Security.b64encode(bos.toByteArray(), nbc); + gos.close(); + } + catch (IOException ex) { + return cont.erreur("STR", "ZIPENC : " + ex.getMessage(), lng); + } + catch (NumberFormatException ex) { + return cont.erreur("STR", "ZIPENC : nombre incorrect : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("ZIPDEC")) { + if (getLength() != 3) return cont.erreur("STR", "ZIPDEC : 1 chaîne attendue", lng); + try { + ByteArrayInputStream bis = new ByteArrayInputStream(Security.b64decode(getArg(2))); + GZIPInputStream gis = new GZIPInputStream(bis, 4096); + // merci à http://hmkcode.com/java-convert-inputstream-to-string/ + StringBuilder sb = new StringBuilder(); + BufferedReader br = new BufferedReader(new InputStreamReader(gis)); + int c; + while((c = br.read()) != -1) sb.append((char)c); + br.close(); + str = sb.toString(); + } + catch (IOException|IllegalArgumentException ex) { + return cont.erreur("STR", "ZIPDEC : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("VIGENC")) { + if (getLength() != 3) return cont.erreur("STR", "VIGENC : 1 chaîne attendue", lng); + str = Security.brouille(getArg(2)); + } + else if (cmd.equals("VIGDEC")) { + if (getLength() != 3) return cont.erreur("STR", "VIGDEC : 1 chaîne attendue", lng); + str = Security.debrouille(getArg(2)); + } + else if (cmd.equals("CODE")) { + if (getLength() != 3) return cont.erreur("STR", "CODE : 1 chaîne attendue", lng); + str = Integer.toHexString(getArg(2).hashCode()); + } + else if (cmd.equals("HASH")) { + if (getLength() != 3) return cont.erreur("STR", "HASH : 1 chaîne attendue", lng); + byte[] tb = getArg(2).getBytes(); + Checksum ck = new CRC32(); + ck.update(tb, 0, tb.length); + str = Long.toHexString(ck.getValue()); + } + else if (cmd.equals("DIGEST")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "DIGEST : 1 chaîne, [hex|b64] attendu", lng); + str = new Security().getHashStr(getArg(2), l == 3 ? "hex" : getArg(3)); + } + else if (cmd.equals("LEVEN")) { + if (getLength() != 4) return cont.erreur("STR", "LEVEN : 2 chaînes attendue", lng); + str = Integer.toString(Tools.LevenshteinDistance(getArg(2), getArg(3))); + } + else if (cmd.equals("ESCAPE")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("STR", "ESCAPE : 1 chaîne, [1 car.] attendue", lng); + try { + str = getArg(2).replaceAll("'", l == 3 ? "\\\\'" : getArg(3) + "'"); + } + catch (IllegalArgumentException ex) { + return cont.erreur("STR", "ESCAPE : caractère invalide : " + getArg(3) + + " (essayer de l'échapper par '\\')", lng); + } + } + else if (cmd.equals("NORM")) { + if (getLength() != 3) return cont.erreur("STR", "NORM : 1 chaîne attendue", lng); + str = Security.sansAccent(getArg(2)); + } + else if (cmd.equals("FROMCSV")) { + if (getLength() != 3) return cont.erreur("STR", "FROMCSV : 1 chaîne attendue", lng); + CSVReader reader = new CSVReader(new StringReader(getArg(2)), ';', '"', '\\', 0, false, true); + List lt = new ArrayList<>(); + String[] line; + try { + while ((line = reader.readNext()) != null) + lt.add(Tools.arrayToString(Arrays.asList(line))); + reader.close(); + switch (lt.size()) { + case 0: str = ""; break; + case 1: str = lt.get(0); break; + default: + StringBuilder sb = new StringBuilder(); + for (String s : lt) sb.append('{').append(s).append("}\n"); + str = sb.toString(); + } + + } + catch (IOException ex) { + return cont.erreur("STR", "FROMCSV : " + ex.getMessage(), lng); + } + } + else if (cmd.equals("TEST")) { // sous commande cachée, pour tests uniquement + try { + // tests + System.out.println("nothing left to test"); + } catch (Exception ex) { ex.printStackTrace();} + str = ""; + } + else return cont.erreur("STR", cmd + " : commande inconnue", lng); + + cont.setValeur(new ValeurDef(cont, null, str)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " str Outils de traitement de chaînes\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdTime.java b/src/lunasql/cmd/CmdTime.java new file mode 100644 index 0000000..2b60429 --- /dev/null +++ b/src/lunasql/cmd/CmdTime.java @@ -0,0 +1,375 @@ +package lunasql.cmd; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.ValeurDef; + +/** + * Commande TIME
+ * (Interne) Commande d'utilitaires de date et heure + * @author M.P. + */ +public class CmdTime extends Instruction { + + private static final SimpleDateFormat + frmD = new SimpleDateFormat("dd/MM/yyyy"), + frmDT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + + public CmdTime(Contexte cont) { + super(cont, TYPE_CMDINT, "TIME", null); + } + + @Override + public int execute() { + if (getLength() < 2) + return cont.erreur("TIME", "une commande de date est requise", lng); + + String cmd = getArg(1).toUpperCase(), tim; + if (cmd.equals("NEW")) { + int l = getLength(); + if (l < 3 || l > 5) + return cont.erreur("TIME", "NEW : 1 date, [1 format attendus, 1 locale]", lng); + try { + long n; + if (l == 3) n = parseDate(getArg(2), null, null); + else if (l == 4) n = parseDate(getArg(2), getArg(3), null); + else n = parseDate(getArg(2), getArg(3), getArg(4)); + tim = Long.toString(n); + } + catch (ParseException ex) { + return cont.erreur("TIME", "NEW : date incorrecte : " + ex.getMessage(), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "NEW : format incorrect : " + getArg(3), lng); + } + } + else if (cmd.equals("NOW")) { + int l = getLength(); + if (l > 3) return cont.erreur("TIME", "NOW : 1 format attendu", lng); + try { + tim = l == 2 ? Long.toString(System.currentTimeMillis()) : + new SimpleDateFormat(Tools.removeQuotes(getArg(2))).format(new Date()); + } catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "NOW : format incorrect : " + getArg(2), lng); + } + } + else if (cmd.equals("FORMAT")) { + int l = getLength(); + if (l < 3 || l > 4) return cont.erreur("TIME", "FORMAT : 1 nombre ms, [1 format] attendus", lng); + try { + long n = Long.parseLong(getArg(2)); + tim = (l == 4 ? new SimpleDateFormat(Tools.removeQuotes(getArg(3))) : frmDT).format(new Date(n)); + } catch (NumberFormatException ex) { + return cont.erreur("TIME", "FORMAT : nombre incorrect : " + getArg(2), lng); + } catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "FORMAT : format incorrect : " + getArg(3), lng); + } + } + else if (cmd.equals("INIT")) { + if (getLength() != 2) return cont.erreur("TIME", "INIT : aucun arg. attendu", lng); + cont.setChrono(); + cont.setVar(Contexte.ENV_EXEC_TIME, "0"); + tim = "0"; + } + else if (cmd.equals("CHRON")) { // Mesure du temps qui passe... + if (getLength() != 2) return cont.erreur("TIME", "CHRON : aucun arg. attendu", lng); + long tm = cont.setChrono(); + tim = SQLCnx.frmDur(tm); + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + } + else if (cmd.equals("DATE")) { + try { + if (getLength() != 2) return cont.erreur("TIME", "DATE : aucun arg. attendu", lng); + tim = new SimpleDateFormat("dd/MM/yyyy").format(new Date()); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "DATE : format incorrect : " + getArg(2), lng); + } + } + else if (cmd.equals("TIME")) { + if (getLength() != 2) return cont.erreur("TIME", "TIME : aucun arg. attendu", lng); + tim = new SimpleDateFormat("HH:mm:ss").format(new Date()); + } + else if (cmd.equals("DATETIME")) { + if (getLength() != 2) return cont.erreur("TIME", "DATETIME : aucun arg. attendu", lng); + tim = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()); + } + else if (cmd.equals("DATETIMEMS")) { + if (getLength() != 2) return cont.erreur("TIME", "DATETIMEMS : aucun arg. attendu", lng); + tim = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss:SS").format(new Date()); + } + else if (cmd.equals("COMPACT")) { + if (getLength() != 2) return cont.erreur("TIME", "COMPACT : aucun arg. attendu", lng); + tim = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + } + else if (cmd.equals("ADD")) { + int l = getLength(); + if (l < 4 || l > 6) + return cont.erreur("TIME", "ADD : 1 date, 1 nombre ms, [1 format, 1 locale] attendus", lng); + try { + long n1; + if (l == 4) n1 = parseDate(getArg(2), null, null); + else if (l == 5) n1 = parseDate(getArg(2), getArg(4), null); + else n1 = parseDate(getArg(2), getArg(4), getArg(5)); + tim = Long.toString(n1 + Long.parseLong(getArg(3))); + } + catch (NumberFormatException ex) { + return cont.erreur("TIME", "ADD : nombre incorrect : " + ex.getMessage(), lng); + } + catch (ParseException ex) { + return cont.erreur("TIME", "ADD : date incorrecte : " + ex.getMessage(), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "ADD : format incorrect : " + getArg(4), lng); + } + } + else if (cmd.equals("DIFF")) { + int l = getLength(); + if (l < 4 || l > 6) return cont.erreur("TIME", "DIFF : 2 dates, [1 format, 1 locale] attendus", lng); + try { + long[] n = readTwoDates(l); + tim = Long.toString(n[1] - n[0]); + } + catch (ParseException ex) { + return cont.erreur("TIME", "DIFF : date incorrecte : " + ex.getMessage(), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "DIFF : format incorrect : " + getArg(4), lng); + } + } + else if (cmd.equals("AFTER?")) { + int l = getLength(); + if (l < 4 || l > 6) + return cont.erreur("TIME", "AFTER? : 2 dates, [1 format, 1 locale] attendus", lng); + try { + long[] n = readTwoDates(l); + tim = n[0] > n[1] ? "1" : "0"; + } + catch (ParseException ex) { + return cont.erreur("TIME", "AFTER? : date incorrecte : " + ex.getMessage(), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "AFTER? : format incorrect : " + getArg(4), lng); + } + } + else if (cmd.equals("BEFORE?")) { + int l = getLength(); + if (l < 4 || l > 6) + return cont.erreur("TIME", "BEFORE? : 2 dates, [1 format, 1 locale] attendus", lng); + try { + long[] n = readTwoDates(l); + tim = n[0] < n[1] ? "1" : "0"; + } + catch (ParseException ex) { + return cont.erreur("TIME", "BEFORE? : date incorrecte : " + ex.getMessage(), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "BEFORE? : format incorrect : " + getArg(4), lng); + } + } + else if (cmd.equals("AT")) { + int l = getLength(); + if (l < 4 || l > 6) + return cont.erreur("TIME", "AT : 1 date, [1 format, 1 locale], 1 bloc attendus", lng); + try { + long delai, n1; + if (l == 4) n1 = parseDate(getArg(2), null, null); + else if (l == 5) n1 = parseDate(getArg(2), getArg(3), null); + else n1 = parseDate(getArg(2), getArg(3), getArg(4)); + delai = n1 - System.currentTimeMillis(); + if (delai >= 0) runThread(delai, 1, getArg(l - 1), "time at"); + else cont.errprintln("Attention : la date indiquée est dépassée"); + tim = ""; + } + catch (NumberFormatException ex) { + return cont.erreur("TIME", "AT : date ms incorrecte : " + ex.getMessage(), lng); + } + catch (ParseException ex) { + return cont.erreur("TIME", "AT : date incorrecte : " + ex.getMessage(), lng); + } + catch (IllegalArgumentException ex) { + return cont.erreur("TIME", "AT : format incorrect : " + getArg(4), lng); + } + } + else if (cmd.equals("AFTER")) { + if (getLength() != 4) return cont.erreur("TIME", "AFTER : 1 délai, 1 bloc attendus", lng); + try { + long t = parsePeriod(getArg(2)); + if (t == 0) return cont.erreur("TIME", "AFTER : délai incorrect : " + getArg(2), lng); + runThread(t, 1, getArg(3), "time after"); + tim = ""; + } + catch (NumberFormatException ex) { + return cont.erreur("TIME", "AFTER : délai ms incorrect : " + getArg(2), lng); + } + } + else if (cmd.equals("REPEAT")) { + if (getLength() != 5) + return cont.erreur("TIME", "REPEAT : 1 période, 1 nombre, 1 bloc attendus", lng); + try { + long t = parsePeriod(getArg(2)); + if (t == 0) return cont.erreur("TIME", "REPEAT : délai incorrect : " + getArg(2), lng); + int nb = Integer.parseInt(getArg(3)); + if (nb <= 0) nb = Integer.MAX_VALUE; // "presque" infini ! + runThread(t, nb, getArg(4), "time repeat"); + tim = ""; + } + catch (NumberFormatException ex) { + return cont.erreur("TIME", "REPEAT : nombre répétitions incorrect : " + getArg(3), lng); + } + } + else if (cmd.equals("WAIT")) { + if (getLength() != 3) return cont.erreur("TIME", "WAIT : 1 période attendue", lng); + long t = parsePeriod(getArg(2)); + if (t == 0) return cont.erreur("TIME", "WAIT : délai incorrect : " + getArg(2), lng); + try { + cont.showWheel(); + Thread.sleep(t); // Attente du délai fourni + cont.hideWheel(); + tim = Long.toString(t); + } + catch (InterruptedException ex) { + cont.hideWheel(); + return cont.exception("WAIT", "erreur InterruptedException : ", lng, ex); + } + } + else if (cmd.equals("DELAY")) { + if (getLength() != 3) return cont.erreur("TIME", "DELAY : 1 période attendue", lng); + long t = parsePeriod(getArg(2)); + if (t == 0) return cont.erreur("TIME", "DELAY : délai incorrect : " + getArg(2), lng); + tim = Long.toString(t); + } + else return cont.erreur("TIME", cmd + " : commande inconnue", lng); + + cont.setValeur(new ValeurDef(cont, null, tim)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + /** + * Lit deux dates + * @param l nombre d'arguments + * @return les deux dates en long + * @throws ParseException + * @throws IllegalArgumentException + */ + private long[] readTwoDates(int l) throws ParseException, IllegalArgumentException { + long n1, n2; + if (l == 4) { + n1 = parseDate(getArg(2), null, null); + n2 = parseDate(getArg(3), null, null); + } + else if (l == 5) { + n1 = parseDate(getArg(2), getArg(4), null); + n2 = parseDate(getArg(3), getArg(4), null); + } + else { + n1 = parseDate(getArg(2), getArg(4), getArg(5)); + n2 = parseDate(getArg(3), getArg(4), getArg(5)); + } + return new long[] {n1, n2}; + } + + /** + * Analyse la date donnée en chaîne de caractères selon le format et la locale donnés + * + * @param sdate la date + * @param frm le format + * @param sloc la locale (FR, DE, IT, ZH, TW, CA, CAF (français), US, UK, JP, KO) + * @return la date en ms + * @throws ParseException + * @throws IllegalArgumentException + */ + protected static long parseDate(String sdate, String frm, String sloc) + throws ParseException, IllegalArgumentException { + Date dt; + String d = Tools.removeQuotes(sdate), f; + if (frm == null) dt = (d.length() == 10 ? frmD : frmDT).parse(d); + else { + f = Tools.removeQuotes(frm); + if (sloc == null) dt = new SimpleDateFormat(f).parse(d); + else { // Locale fournie + Locale loc; + switch (sloc) { + case "FR": loc = Locale.FRANCE; break; + case "DE": loc = Locale.GERMANY; break; + case "IT": loc = Locale.ITALY; break; + case "UK": loc = Locale.UK; break; + case "ZH": loc = Locale.CHINA; break; + case "TW": loc = Locale.TAIWAN; break; + case "CA": loc = Locale.CANADA; break; + case "CAF": loc = Locale.CANADA_FRENCH; break; + case "JP": loc = Locale.JAPAN; break; + case "KO": loc = Locale.KOREA; break; + case "US": + default: loc = Locale.US; + } + dt = new SimpleDateFormat(f, loc).parse(d); + } + } + return dt.getTime(); + } + + /** + * Exécute le bloc de code en nouveau Thread + * + * @param delai le delai entre deux exécutions + * @param nb le nombre d'exécutions + * @param code le bloc de code + * @param cmd la commande à executer + */ + private void runThread(final long delai, final int nb, final String code, final String cmd) { + new Thread(() -> { + try { + for (int i = 0; i < nb; i++) { + Thread.sleep(delai); + cont.addSubMode(); + new Lecteur(cont, code, new HashMap() {{ put(Contexte.LEC_SUPER, cmd); }}); + cont.remSubMode(); + } + } + catch (InterruptedException ex) { + cont.erreur("TIME", "Erreur InterruptedException : " + ex.getMessage(), lng); + } + }).start(); + } + + /** + * Analyse le délai pour each + * + * @param p la chaîne de délai ex: 1h35m20s + * @return le résultat en ms + */ + private long parsePeriod(String p) { + // 012 3 + Matcher m = Pattern.compile("(([1-9][0-9]*?)([hmsl]))+?").matcher(p); + long l = 0L; + //while (m.find()) for (int i=0; i + * (Interne) Affiche une représentation en arbre des clefs étangères d'une table + * TEST(ID) + * | + * +-- TEST2(IDTST) + * TEST2(ID) + * | + * +-- TEST3(IDTST) + * + * @author M.P. + */ +public class CmdTree extends Instruction { + + private final OptionParser parser; + private int deepMax; + private boolean modeDot; // mode arbre normal (false) ou DOT (true) + + public CmdTree(Contexte cont){ + super(cont, TYPE_CMDINT, "TREE", null);// Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("d", "génération au format GraphViz DOT"); + } + + @Override + public int execute() { + OptionSet options; + try { + options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + modeDot = options.has("d"); // digraph DOT + Valeur vr = new ValeurDef(cont); + List lc = options.nonOptionArguments(); + if (lc.isEmpty() || lc.size() > 2) + return cont.erreur("TREE", "une table, [une profondeur] attendues", lng); + + if (lc.size() == 2) { + try { + deepMax = Integer.parseInt((String)lc.get(1)) - 1; + if (deepMax < 0) throw new NumberFormatException("Profondeur négative invalide"); + } + catch (NumberFormatException ex) { + return cont.erreur("TREE", "profondeur incorrecte : " + ex.getMessage(), lng); + } + } + else deepMax = 2; + + String table = (String)lc.get(0); + DatabaseMetaData dMeta = cont.getConnex().getMetaData(); + + // Recherche de la table + boolean hastb = false; + ResultSet rs = dMeta.getTables(null, null, null, new String[] { + "TABLE", "VIEW", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"}); + while(rs.next()) { + if (table.equalsIgnoreCase(rs.getString(3))) { + hastb = true; + table = rs.getString(3); + } + } + rs.close(); + if (!hastb) return cont.erreur("TREE", "table '" + table + "' non trouvée", lng); + + StringBuilder ret = new StringBuilder(); + if (modeDot) ret.append("digraph {\n"); + ret.append(drawTree(dMeta, table, 0, "")); + if (modeDot) { + ret.append('}'); + vr.setSubValue(ret.toString()); + } + else { + vr.setDispValue(ret.length() == 0 ? "Aucune référence vers cette table" : ret.toString(), + Contexte.VERB_AFF); + } + cont.setValeur(vr); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("TREE", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException|SQLException ex) { + return cont.exception("TREE", "ERREUR " + ex.getClass().getSimpleName() + " : " + + ex.getMessage(), lng, ex); + } + } + + /** + * Descente récursive dans les tables par les clefs étrangères + * + * @param dMeta le DatabaseMetaData + * @param tbprnt le nom de la table parent + * @param deep la profondeur en cours + * @param tblist la list des tables rencontrées + * @return la construction + * @throws SQLException si erreur SQL + */ + public String drawTree(DatabaseMetaData dMeta, String tbprnt, int deep, String tblist) + throws SQLException { + StringBuilder sb = new StringBuilder(); + ResultSet rs = dMeta.getExportedKeys(null, null, tbprnt); + while (rs.next()) { + String flprnt = rs.getString("PKCOLUMN_NAME"), + tbenft = rs.getString("FKTABLE_NAME"), flenft = rs.getString("FKCOLUMN_NAME"); + if (modeDot) { + sb.append(" ").append(tbprnt).append(" -> ").append(tbenft); // tables + sb.append(" [label=\"").append(flprnt).append('=').append(flenft); // relations + sb.append("\", fontsize=12.0];\n"); + } else { + String rp = repeat(deep); + sb.append("\n"); + sb.append(rp).append(tbprnt).append('(').append(flprnt).append(")\n"); // parent + sb.append(rp).append("|\n").append(rp).append("+-- "); + sb.append(tbenft).append("(").append(flenft).append(")"); // enfant + } + if (tblist.contains(tbprnt + "." + flprnt)) sb.append(" (...)"); + else if (deep < deepMax) + sb.append(drawTree(dMeta, tbenft, deep + 1, tblist + "|" + tbprnt + "." + flprnt)); + } + rs.close(); + return sb.toString(); + } + + /** + * Fixe la variable d'instance deepMax (proondeur max) + * + * @param d la nouvelle valeur + */ + public void setDeepMax(int d) { + deepMax = d; + } + + /** + * Répétition de chaîne + * + * @param n le nb de fois + * @return str + */ + private static String repeat(int n) { + return new String(new char[n]).replace("\0", " "); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " tree Affiche un schéma de structure de la base\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdTruncate.java b/src/lunasql/cmd/CmdTruncate.java new file mode 100644 index 0000000..290342e --- /dev/null +++ b/src/lunasql/cmd/CmdTruncate.java @@ -0,0 +1,30 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande TRUNCATE
+ * (SQL) Suppression du contenu d'une table + * @author M.P. + */ +public class CmdTruncate extends Instruction { + + public CmdTruncate(Contexte cont){ + super(cont, TYPE_CMDSQL, "TRUNCATE", null); + } + + @Override + public int execute() { + return executeUpdate("TRUNCATE", "table purgée"); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " truncate (+instr) Supprime un objet de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdUndef.java b/src/lunasql/cmd/CmdUndef.java new file mode 100644 index 0000000..e190c56 --- /dev/null +++ b/src/lunasql/cmd/CmdUndef.java @@ -0,0 +1,124 @@ +package lunasql.cmd; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Tools; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande UNDEF
+ * (Interne) Suppression d'une variable de l'environnement d'exécution + * @author M.P. + */ +public class CmdUndef extends Instruction { + + private final OptionParser parser; + + public CmdUndef(Contexte cont) { + super(cont, TYPE_CMDINT, "UNDEF", "-"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("a", "supprime toutes variables"); + parser.accepts("f", "supprime les variables sur patron").withRequiredArg().ofType(String.class) + .describedAs("pattern"); + parser.accepts("d", "supprime si déclarée"); + parser.nonOptions("noms_variables").ofType(String.class); + } + + @Override + public int execute() { + OptionSet options; + try { + options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + Valeur vr = new ValeurDef(cont); + int ret = RET_CONTINUE, nbundef = 0; + boolean err = false; + if (options.has("a")) { // suppression de toutes variables + nbundef = cont.unsetAllVars(); + vr.setDispValue("Toutes variables supprimées", Contexte.VERB_BVR); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } else if (options.has("f")) { // suppression sur patron + SortedSet sort = new TreeSet<>(cont.getAllVars().keySet()); + Iterator iter = sort.iterator(); + Pattern ptnb = Pattern.compile(Tools.removeBQuotes((String) options.valueOf("f")).value); + StringBuilder sb = new StringBuilder(); + while (iter.hasNext()) { + String key = iter.next(); + if (cont.isNonSys(key) && ptnb.matcher(key).matches() && cont.unsetVar(key)) { + nbundef++; + sb.append('\'').append(key).append("' supprimée").append('\n'); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_BVR); + } else { + List lv = options.nonOptionArguments(); + if (lv.isEmpty()) + return cont.erreur("UNDEF", "un nom de variable au moins est attendu", lng); + + StringBuilder sb = new StringBuilder(); + for (Object o : lv) { // pour chaque var + String key = (String) o; + if (cont.isSys(key) || cont.isSysUser(key)) { + err = true; + ret = cont.erreur("UNDEF", "suppression d'option système interdite : " + key, lng); + } else { + if (cont.unsetVar(key)) { + nbundef++; + sb.append('\'').append(key).append("' supprimée").append('\n'); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } else if (!options.has("d")) { + err = true; + ret = cont.erreur("UNDEF", "variable globale '" + key + "' non définie", lng); + } + } + } + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + vr.setDispValue(sb.toString(), Contexte.VERB_BVR); + } + if (err) return ret; + + vr.setSubValue(Integer.toString(nbundef)); + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + catch (OptionException ex) { + return cont.exception("UNDEF", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("UNDEF", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " undef, - Supprime une variable de l'environnement de console\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdUpdate.java b/src/lunasql/cmd/CmdUpdate.java new file mode 100644 index 0000000..5b7192b --- /dev/null +++ b/src/lunasql/cmd/CmdUpdate.java @@ -0,0 +1,35 @@ +package lunasql.cmd; + +import java.sql.SQLException; + +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurExe; + +/** + * Commande UPDATE
+ * (SQL) Mise-à-jour des données d'une table de la base + * @author M.P. + */ +public class CmdUpdate extends Instruction { + + public CmdUpdate(Contexte cont){ + super(cont, TYPE_CMDSQL, "UPDATE", null); + } + + @Override + public int execute() { + return executeUpdate("UPDATE", " modifiée", true); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " update (+instr) Modifie des données de la base\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdUse.java b/src/lunasql/cmd/CmdUse.java new file mode 100644 index 0000000..39d09c4 --- /dev/null +++ b/src/lunasql/cmd/CmdUse.java @@ -0,0 +1,245 @@ +package lunasql.cmd; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import javax.script.ScriptException; + +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurDef; + +/** + * Commande USE
+ * (Interne) Chargement d'une bibliothèque en environnement + * @author M.P. + */ +public class CmdUse extends CmdExec { + + private final OptionParser parser; + + public CmdUse(Contexte cont){ + super(cont, "USE", "|"); + // Option parser + parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + parser.accepts("?", "aide sur la commande"); + parser.accepts("j", "invoque une classe d'un fichier jar").withRequiredArg().ofType(File.class) + .describedAs("jar"); + parser.accepts("m", "invoque une méthode").withRequiredArg().ofType(String.class) + .describedAs("method"); + parser.accepts("i", "fixe le mode d'appel").withRequiredArg().ofType(String.class) + .describedAs("new|static"); + parser.accepts("c", "types requis pour le constructeur si new").withRequiredArg().ofType(String.class) + .describedAs("type1,type2").withValuesSeparatedBy(','); + parser.accepts("r", "arguments pour le constructeur si new").withRequiredArg().ofType(String.class) + .describedAs("arg1,arg2").withValuesSeparatedBy(','); + parser.accepts("a", "arguments pour l'option m").withRequiredArg().ofType(String.class) + .describedAs("arg1,arg2").withValuesSeparatedBy(','); + parser.accepts("t", "types requis pour l'option m").withRequiredArg().ofType(String.class) + .describedAs("type1,type2").withValuesSeparatedBy(','); + parser.nonOptions("bibliothèques").ofType(String.class); + } + + @Override + public int execute() { + try { + OptionSet options = parser.parse(getCommandA1()); + // Aide sur les options + if (options.has("?")) { + parser.printHelpOn(cont.getWriterOrOut()); + cont.setValeur(null); + return RET_CONTINUE; + } + + // Exécution avec autres options + int ret = RET_CONTINUE; + Valeur vr = new ValeurDef(cont); + List lb = options.nonOptionArguments(); + if (options.has("i")) { // chargement d'une classe Java + if (lb.size() != 1) + return cont.erreur("USE", "un (et un seul) nom de classe Java est requis", lng); + + File jar = null; + if (options.has("j")) { + jar = (File)options.valueOf("j"); + if(!jar.canRead()) + return cont.erreur("USE", "le fichier '" + jar.getName() + "' n'est pas accessible", lng); + } + String c = (String) lb.get(0); + try { + // Chargement de la classe + Class cl; + if (jar == null) cl = Class.forName(c); + else { + ClassLoader loader = new URLClassLoader(new URL[]{jar.toURI().toURL()}); + cl = loader.loadClass(c); + } + + String mode = (String) options.valueOf("i"); + Object obj = null; + if (mode.equals("new")) { + List args, types; + if (options.has("r") && options.has("c")) { // constructeur avec signature non vide + args = options.valuesOf("r"); + types = options.valuesOf("c"); + if (types.size() != args.size()) + return cont.erreur("USE", "arguments et types incohérents pour new", lng); + } + else if (options.has("r") || options.has("c")) { + return cont.erreur("USE", "les options -c et -r sont interdépendantes", lng); + } + else { // constructeur sans arguments + args = new ArrayList(); + types = new ArrayList(); + } + Constructor ctr = cl.getConstructor(getTypes(types)); + obj = ctr.newInstance(args.isEmpty() ? null : args.toArray()); + } + + if (options.has("m")) {// méthode demandée + List args, types; + if (options.has("a") && options.has("t")) { // méthodes avec argument + args = options.valuesOf("a"); + types = options.valuesOf("t"); + if (types.size() != args.size()) + return cont.erreur("USE", "arguments et types incohérents pour l'option m", lng); + } + else if (options.has("t") || options.has("a")) { + return cont.erreur("USE", "les options -t et -a sont interdépendantes", lng); + } + else { // méthode sans argument + args = new ArrayList(); + types = new ArrayList(); + } + // Chargement de la méthode + Method met = cl.getDeclaredMethod((String) options.valueOf("m"), getTypes(types)); + obj = met.invoke(obj, args.isEmpty() ? null : args.toArray()); + vr.setSubValue(String.valueOf(obj)); + } + else { // pas de méthode : retourne la classe + vr.setDispValue("classe chargée : " + c, Contexte.VERB_BVR); + vr.setSubValue(String.valueOf(cl)); + } + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } + catch (MalformedURLException ex){ + return cont.exception("USE", "ERREUR MalformedURLException : " + ex.getMessage(), lng, ex); + } + catch (ClassNotFoundException ex) {// erreur prévisible > cont.erreur + return cont.erreur("USE", "classe '" + ex.getMessage() + "' inaccessible dans le " + + (jar == null ? "CLASSPATH" : "fichier JAR " + jar.getName()), lng); + } + catch (NullPointerException ex){ + return cont.erreur("USE", "méthode appelée sur un objet null ou déclaré static", lng); + } + catch (Exception ex) {// toute autre chose + return cont.exception("USE", "ERREUR Reflexion " + ex.getClass().getSimpleName() + + " : " + ex.getMessage(), lng, ex); + } + } + else { // chargement d'une bibliothèque LunaSQL normale + if (lb.isEmpty()) + return cont.erreur("USE", "un nom de bibliothèque au moins est requis", lng); + + Lecteur lec = null; + for (Object o : lb) { // pour chaque lib + String l = (String) o; + if (l.indexOf('.') < 0) l += ".sql"; + + // La lib est-elle déjà chargée ? + String llib = cont.getVar(Contexte.ENV_LOADED_LIBS); + String[] tllib = llib.split(File.pathSeparator); + if (Arrays.asList(tllib).contains(l)) { + if (cont.getVerbose() >= Contexte.VERB_BVR) + cont.println("Bibliothèque '" + l + "' déjà chargée"); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + vr.setSubValue("0"); + continue; // sortie + } + + // Mise-à-jour de la variable de liste des bibliothèques chargées + cont.setVar(Contexte.ENV_LOADED_LIBS, + (llib.length() == 0 ? "" : llib + File.pathSeparator) + l); + // Chargement en ressource stream : 1) /lunasql/misc/ 2) /lunasql/jsextras/ + if (cont.getVerbose() >= Contexte.VERB_BVR) + cont.println("Bibliothèque '" + l + "' en chargement"); + + InputStream is = getClass().getResourceAsStream("/lunasql/misc/" + l); + if (is == null) is = getClass().getResourceAsStream("/lunasql/jsextras/" + l); + // Si absente du CLASSPATH > passage à la commande EXEC + if (is == null) { + super.execute(); + Valeur vr2 = cont.getValeur(); + vr.setSubValue(vr2 == null ? null : vr2.getSubValue()); + } + else { // présente > lecteur en archive + HashMap vars = new HashMap<>(); + vars.put(Contexte.LEC_THIS, l); + vars.put(Contexte.LEC_SUPER, ""); + vars.put(Contexte.LEC_SCR_NAME, l); + long tm = System.currentTimeMillis(); + try (Reader read = new InputStreamReader(is, StandardCharsets.UTF_8)) { + if (l.endsWith(".sql")) lec = new Lecteur(cont, read, vars); + else cont.getEvalEngine().eval(read); + tm = System.currentTimeMillis() - tm; + vr.setDispValue("Exécuté en : " + SQLCnx.frmDur(tm), Contexte.VERB_BVR); + Valeur vr2 = cont.getValeur(); + vr.setSubValue(vr2 == null ? null : vr2.getSubValue()); + } + catch (IOException ex) { + ret = cont.erreur("USE", "IOException : " + ex.getMessage(), lng); + } + } + }// for + if (lec != null) ret = lec.getCmdState(); + } + + vr.setRet(); + cont.setValeur(vr); + return ret; + } + catch (ScriptException ex){ + return cont.erreur("USE", "erreur lecture script SE : " + ex.getMessage() , lng); + } + catch (OptionException ex) { + return cont.exception("USE", "erreur d'option : " + ex.getMessage(), lng, ex); + } + catch (IOException ex) { + return cont.exception("USE", "ERREUR IOException : " + ex.getMessage(), lng, ex); + } + } + + private Class[] getTypes(List types) throws ClassNotFoundException { + Class[] prmcl = new Class[types.size()]; + for (int i=0; i + * (Interne) Positionnement de l'état verbeux de la console + * @author M.P. + */ +public class CmdVerb extends Instruction { + + public CmdVerb(Contexte cont) { + super(cont, TYPE_CMDINT, "VERB", ","); + } + + @Override + public int execute() { + Valeur vr = new ValeurDef(cont); + + // Affichage de la valeur de verbose + switch (getLength()) { + case 1: + int nv = cont.getVerbose(); + vr.setDispValue("VERB = " + nv + " (" + Contexte.VERBLIB[nv] + ")", Contexte.VERB_AFF); + vr.setSubValue(Integer.toString(nv)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + break; + + case 2: + // Modification de la valeur de verbose + String sval; + if ((sval = cont.setVerbose(getArg(1))) != null) { + vr.setDispValue("Nouveau VERB = " + sval, Contexte.VERB_BVR); + vr.setSubValue(sval); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + } else { + return cont.erreur("VERB", "valeur ou libellé de verbose invalide : " + getArg(1), lng); + } + break; + + default: + return cont.erreur("VERB", "une valeur ou un libellé de verbose est attendu(e)", lng); + } + vr.setRet(); + cont.setValeur(vr); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " verb, , Attribue au système une valeur de verbose\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdView.java b/src/lunasql/cmd/CmdView.java new file mode 100644 index 0000000..23ed266 --- /dev/null +++ b/src/lunasql/cmd/CmdView.java @@ -0,0 +1,251 @@ +package lunasql.cmd; + +import java.awt.*; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; + +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.ValeurDef; +import sbrunner.gui.tableView.DefaultTableViewColumn; +import sbrunner.gui.tableView.DefaultTableViewModel; +import sbrunner.gui.tableView.TableView; +import sbrunner.gui.tableView.TableViewColumn; + +/** + * Commande VIEW
+ * (Interne) Affiche un resultset en JTable au lieu de la sortie standard + * @author M.P. + */ +public class CmdView extends Instruction { + + public CmdView(Contexte cont){ + super(cont, TYPE_CMDINT, "VIEW", null); + } + + @Override + public int execute() { + if (cont.isHttpMode()) + return cont.erreur("VIEW", "cette commande n'est pas autorisée en mode HTTP", lng); + if (getLength() < 2) + return cont.erreur("VIEW", "une commande SQL SELECT ou une table est attendue", lng); + + String s = getSCommand(1); + int id = s.indexOf(' '); + String c = (id < 0 ? s : s.substring(0, id)).toUpperCase(); + if (getLength() == 2) // nom de table fourni + s = "SELECT * FROM " + c; + else if (!c.equals("SELECT")) // commande SQL fournie + return cont.erreur("VIEW", "seule une commande SELECT (ou une table) est autorisée", lng); + + // Exécution + try { + long tm = System.currentTimeMillis(); + ResultSet rs = cont.getConnex().getResultset(s); + ResultSetMetaData rsSchema = rs.getMetaData(); + tm = System.currentTimeMillis() - tm; + int nbCol = rsSchema.getColumnCount(), nbLig = 0; + if (nbCol > 16) // 16 colonnes max car limite de fonctions en VarRow + return cont.erreur("VIEW", "la requête retourne " + nbCol + " colonnes (max. 16)", lng); + + TableViewColumn[] columns = new TableViewColumn[nbCol]; + List rows = new ArrayList<>(); + for (int i = 0; i < nbCol; i++) + columns[i] = new DefaultTableViewColumn(rsSchema.getColumnName(i + 1), + VarRow.class.getDeclaredMethod("getVar" + i, (Class[])null), + VarRow.class.getDeclaredMethod("setVar" + i, String.class) + ); + while (rs.next() && ++nbLig <= 200) { // limite de taille à 200 lignes + List l = new ArrayList<>(); + for (int i = 0; i < nbCol; i++) l.add(i, rs.getString(i + 1)); + rows.add(new VarRow(l)); + } + rs.close(); + + // Construction de la fenêtre de vue + try { + JFrame frame = new JFrame(s); + final TableView view = new TableView(new DefaultTableViewModel(columns, rows)); + view.setMakeIndex(true); + frame.getContentPane().setLayout(new BorderLayout()); + frame.getContentPane().add(new JScrollPane(view), BorderLayout.CENTER); + + frame.setSize(600, 400); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.validate(); + frame.setVisible(true); + } + catch (HeadlessException ex) { + return cont.exception("EDIT", "ERREUR HeadlessException : " + ex.getMessage(), lng, ex); + } + + cont.setValeur(new ValeurDef(cont, "-> " + nbLig + " ligne" + (nbLig > 1 ? "s" : "") + + " trouvée" + (nbLig > 1 ? "s" : "") + " (" + SQLCnx.frmDur(tm) + ")", + Contexte.VERB_MSG, Integer.toString(nbLig))); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch(SQLException ex){ + return cont.exception("VIEW", "ERREUR SQLException : " + ex.getMessage(), lng, ex); + } + catch (NoSuchMethodException ex) { + return cont.exception("VIEW", "ERREUR NoSuchMethodException : " + ex.getMessage(), lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " view Affiche le résultat d'une requête ou d'une table\n"; + } + + /** + * Classe représentant une ligne variable + * @author MP + * + */ + public static class VarRow { + private final List data; + + public VarRow(List d){ + data = d; + } + + public String getVar0(){ + return data.get(0); + } + + public void setVar0(String element){ + data.set(0, element); + } + + public String getVar1(){ + return data.get(1); + } + + public void setVar1(String element){ + data.set(1, element); + } + + public String getVar2(){ + return data.get(2); + } + + public void setVar2(String element){ + data.set(2, element); + } + + public String getVar3(){ + return data.get(3); + } + + public void setVar3(String element){ + data.set(3, element); + } + + public String getVar4(){ + return data.get(4); + } + + public void setVar4(String element){ + data.set(4, element); + } + + public String getVar5(){ + return data.get(5); + } + + public void setVar5(String element){ + data.set(5, element); + } + + public String getVar6(){ + return data.get(6); + } + + public void setVar6(String element){ + data.set(6, element); + } + + public String getVar7(){ + return data.get(7); + } + + public void setVar7(String element){ + data.set(7, element); + } + + public String getVar8(){ + return data.get(8); + } + + public void setVar8(String element){ + data.set(8, element); + } + + public String getVar9(){ + return data.get(9); + } + + public void setVar9(String element){ + data.set(9, element); + } + + public String getVar10(){ + return data.get(10); + } + + public void setVar10(String element){ + data.set(10, element); + } + + public String getVar11(){ + return data.get(11); + } + + public void setVar11(String element){ + data.set(11, element); + } + + public String getVar12(){ + return data.get(12); + } + + public void setVar12(String element){ + data.set(12, element); + } + + public String getVar13(){ + return data.get(13); + } + + public void setVar13(String element){ + data.set(13, element); + } + + public String getVar14(){ + return data.get(14); + } + + public void setVar14(String element){ + data.set(14, element); + } + + public String getVar15(){ + return data.get(15); + } + + public void setVar15(String element){ + data.set(15, element); + } + } +}// class diff --git a/src/lunasql/cmd/CmdVoid.java b/src/lunasql/cmd/CmdVoid.java new file mode 100644 index 0000000..1f44f3f --- /dev/null +++ b/src/lunasql/cmd/CmdVoid.java @@ -0,0 +1,31 @@ +package lunasql.cmd; + +import lunasql.lib.Contexte; + +/** + * Commande VOID
+ * (Interne) Commande qui ne fait absolument rien + * @author M.P. + */ +public class CmdVoid extends Instruction { + + public CmdVoid(Contexte cont){ + super(cont, TYPE_CMDINT, "VOID", "."); + } + + @Override + public int execute() { + cont.setValeur(null); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " void, . Ne fait absolument rien\n"; + } +}// class diff --git a/src/lunasql/cmd/CmdWait.java b/src/lunasql/cmd/CmdWait.java new file mode 100644 index 0000000..150d0ec --- /dev/null +++ b/src/lunasql/cmd/CmdWait.java @@ -0,0 +1,85 @@ +package lunasql.cmd; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import jline.ConsoleReader; +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; +//import jline.console.ConsoleReader; + +/** + * Commande WAIT
+ * (Interne) Attente d'un évènement et affichage éventuel d'un message + * @author M.P. + */ +public class CmdWait extends Instruction { + + public CmdWait(Contexte cont) { + super(cont, TYPE_CMDINT, "WAIT", "~"); + } + + @Override + public int execute() { + if (getLength() == 1) + return cont.erreur("WAIT", "une durée en ms et un message optionnel sont attendus", lng); + + try { + String t = getArg(1); + long delai; + if (t.length() > 2 && t.charAt(0) == '\'' && t.charAt(t.length() - 1) == '\'') { + Date d = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss:SS").parse(t.substring(1, t.length() - 1)); + delai = d.getTime() - System.currentTimeMillis(); + if (delai < 0) delai = 1L; + } + else delai = Long.parseLong(t); + if (delai <= 0) { + if (cont.isHttpMode()) + return cont.erreur("WAIT", "cette commande n'est pas autorisée en mode HTTP", lng); + + // Attente de la frappe sur ENTREE + if (cont.getVerbose() >= Contexte.VERB_AFF) cont.println(getLength() == 2 ? + "Veuillez appuyer sur pour continuer..." : getSCommand(2)); + ConsoleReader reader = cont.getConsoleReader(); + if (reader == null) new BufferedReader(new InputStreamReader(System.in)).readLine(); + else reader.readLine(""); + } + else { + cont.showWheel(); + Thread.sleep(delai); // Attente du délai fourni + cont.hideWheel(); + } + + cont.setValeur(new ValeurDef(cont, getSCommand(2), Contexte.VERB_AFF, Long.toString(delai))); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (ParseException ex) {// erreur prévisible > cont.erreur + return cont.erreur("WAIT", "format de date incorrect : " + getArg(1), lng); + } + catch (NumberFormatException ex) {// erreur prévisible > cont.erreur + return cont.erreur("WAIT", "délai incorrect : " + getArg(1), lng); + } + catch (IOException ex) { + return cont.exception("WAIT", "erreur IO sur entrée standard : ", lng, ex); + } + catch (InterruptedException ex) { + cont.hideWheel(); + return cont.exception("WAIT", "ERREUR InterruptedException : ", lng, ex); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " wait, ~ Attend le temps indiqué ou une frappe clavier\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdWhen.java b/src/lunasql/cmd/CmdWhen.java new file mode 100644 index 0000000..17c509a --- /dev/null +++ b/src/lunasql/cmd/CmdWhen.java @@ -0,0 +1,52 @@ +package lunasql.cmd; + +import java.io.IOException; + +import javax.script.ScriptException; +import static lunasql.cmd.Instruction.RET_CONTINUE; + +import lunasql.lib.Contexte; +import lunasql.val.ValeurDef; + +/** + * Commande WHEN
+ * (Interne) Conditionnelle WHEN par contexte d'exécution + * @author M.P. + */ +public class CmdWhen extends Instruction { + + public CmdWhen(Contexte cont){ + super(cont, TYPE_MOTC_WH, "WHEN", null); + } + + @Override + public int execute() { + if (getLength() >= 2) { + String s = getSCommand(1); + try { + boolean r = cont.canExec(); + if (r) r = (cont.evaluerBool(s)); + cont.addWhen(r); + cont.setValeur(new ValeurDef(cont, null, r ? "1" : "0")); + return RET_CONTINUE; + } + catch (IllegalArgumentException e) {// erreur déjà traitée (cf. cont.evaluerBool) + return cont.erreur("WHEN", "Expression invalide :\n" + e.getMessage(), lng); + } + catch (ScriptException e) {// erreur prévisible > cont.erreur + return cont.erreur("WHEN", "Impossible d'évaluer l'expression :\n" + e.getMessage(), lng); + } + } + else return cont.erreur("WHEN", "une expression est attendue", lng); + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc() { + return " when Teste une expression et fixe le contexte d'exécution\n"; + } +}// class + diff --git a/src/lunasql/cmd/CmdWhile.java b/src/lunasql/cmd/CmdWhile.java new file mode 100644 index 0000000..7ee67a1 --- /dev/null +++ b/src/lunasql/cmd/CmdWhile.java @@ -0,0 +1,77 @@ +package lunasql.cmd; + +import java.io.IOException; +import javax.script.ScriptException; +import static lunasql.cmd.Instruction.RET_CONTINUE; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.val.Valeur; + +/** + * Commande WHILE
+ * (Interne) Boucle WHILE + * @author M.P. + */ +public class CmdWhile extends Instruction { + + private static final String SYNERR = "syntaxe : WHILE expr bloc [ELSE bloc]"; + + public CmdWhile(Contexte cont){ + super(cont, TYPE_CMDINT, "WHILE", null); + } + + @Override + public int execute() { + int lg = getLength(); + if (lg < 3) return cont.erreur("WHILE", SYNERR, lng); + + try { + int ret = RET_CONTINUE; + boolean exec = false; + Lecteur lec = new Lecteur(cont); + lec.setLecVar(Contexte.LEC_SUPER, "while"); + lec.setLecVar(Contexte.LEC_LOOP_DEEP, cont.incrLoopDeep()); // profondeur de boucle + String iter = getArg(1), code = getArg(2); // copie necessaire pour imbrication de while + while (ret == RET_CONTINUE && cont.evaluerBool(iter)) { + ret = cont.evaluerBlockFor(lec, code, null); + exec = true; + } + + // Condition jamais valide > clause ELSE + if (!exec && lg > 3 && getArg(3).equalsIgnoreCase("ELSE")) { // exécution else + if (lg == 4) return cont.erreur("WHILE", SYNERR, lng); + ret = cont.evaluerBlockFor(lec, getArg(4), null); + exec = true; + } + lec.fin(); + + if (exec) { + Valeur vr = cont.getValeur(); + if (vr != null) vr.setDispValue(null, Contexte.VERB_SIL); + } + else cont.setValeur(null); // retour sans exécution + + if (ret == RET_BREAK_LP) { + String lb = cont.getLecVar(Contexte.LEC_LOOP_BREAK); // si _LOOP_BREAK positionnée + return lb == null ? RET_CONTINUE : RET_BREAK_LP; + } + else return ret; + } + catch (IllegalArgumentException e) {// erreur déjà traitée (cf. cont.evaluerBool) + return cont.erreur("WHILE", "Expression invalide :\n" + e.getMessage(), lng); + } + catch (ScriptException e) {// erreur prévisible > cont.erreur + return cont.erreur("WHILE", "Impossible d'évaluer l'expression :\n" + e.getMessage(), lng); + } + } + + /* + * (non-Javadoc) + * @see lunasql.cmd.Instruction#getHelp() + */ + @Override + public String getDesc(){ + return " while Teste une expression et boucle sur les arguments\n"; + } +}// class + diff --git a/src/lunasql/cmd/Instruction.java b/src/lunasql/cmd/Instruction.java new file mode 100644 index 0000000..e8f58d9 --- /dev/null +++ b/src/lunasql/cmd/Instruction.java @@ -0,0 +1,407 @@ +package lunasql.cmd; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import lunasql.lib.Contexte; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; +import lunasql.val.ValeurExe; +import lunasql.val.ValeurReq; + +/** + * Classe Instruction
+ * Modèle de commande pour les classes filles
+ * + * @author M.P. + */ +public abstract class Instruction { + + public static final int RET_CONTINUE = 0; + public static final int RET_NEXT_LP = RET_CONTINUE + 1; + public static final int RET_BREAK_LP = RET_NEXT_LP + 1; + public static final int RET_EV_CATCH = RET_BREAK_LP + 1; + public static final int RET_RETR_SCR = RET_EV_CATCH + 1; + public static final int RET_EXIT_SCR = RET_RETR_SCR + 1; + public static final int RET_SHUTDOWN = RET_EXIT_SCR + 1; + public static final int TYPE_VIDE = 0; + public static final int TYPE_ERR = TYPE_VIDE + 1; + public static final int TYPE_MOTC_WH = TYPE_ERR + 1; + public static final int TYPE_INVIS = TYPE_MOTC_WH + 1; + public static final int TYPE_CMDSQL = TYPE_INVIS + 1; + public static final int TYPE_CMDINT = TYPE_CMDSQL + 1; + public static final int TYPE_CMDPLG = TYPE_CMDINT + 1; + + protected int type; + protected String name; + protected String alias; + protected int lng; + protected ArrayList command; + protected Contexte cont; + + + /** + * Constructeur Instruction + * Impossible d'instancier une commande par ce constructeur : il sert pour super + * + * @param cont le contexte + * @param type le type d'instruction + * @param name le nom de l'instruction à reconnaître + * @param alias l'alias de l'instruction à reconnaître + */ + public Instruction(Contexte cont, int type, String name, String alias) { + this.cont = cont; + this.type = type; + this.name = name; + this.alias = alias; + } + + /** + * Retourne le nombre d'éléments de l'instruction complète + * + * @return int nombre d'éléments (tous arguments compris) + */ + public int getLength() { + return this.command.size(); + } + + /** + * Retourne l'instruction complète + * + * @return instruction en ArrayList + */ + public ArrayList getCommand() { + return this.command; + } + + /** + * Retourne l'instruction à partir du Nième arg + * + * @param n l'indice dans la commande + * @return instruction en ArrayList + */ + public ArrayList getCommand(int n) { + if (n < 0 || n >= command.size()) return new ArrayList<>(); + ArrayList a = new ArrayList<>(); + for (int i = n; i < this.command.size(); i++) a.add(command.get(i)); + return a; + } + + /** + * Retourne l'instruction en tableau de strings à partir du Nième arg + * + * @param n l'indice dans la commande + * @return instruction en String[] + */ + public String[] getCommandA(int n){ + return getCommand(n).toArray(new String[0]); + } + + /** + * Retourne les arguments de l'instruction en tableau de strings + * + * @return instruction en String[] + */ + public String[] getCommandA1(){ + return getCommand(1).toArray(new String[0]); + } + + /** + * Retourne la commande entière + * + * @return commande en String + */ + public String getSCommand() { + return getSCommand(0); + } + + /** + * Retourne la commande à partir du Nième arg + * + * @param n l'indice dans la commande + * @return commande en String + */ + public String getSCommand(int n) { + if (n < 0 || n >= command.size()) return ""; + StringBuilder args = new StringBuilder(); + for (int i = n; i < command.size(); i++) args.append(command.get(i)).append(' '); + if (args.length() > 0) args.deleteCharAt(args.length() - 1); + return args.toString(); + } + + /** + * Retourne le nom de la commande exécutée (position 0) + * + * @return nom en chaîne de caractères + */ + public String getCommandName() { + return getArg(0); + } + + /** + * Retourne l'argument de la commande de la position n + * + * @param n l'indice dans la commande + * @return arguments en chaîne de caractères + */ + public String getArg(int n) { + if (n < 0 || n >= command.size()) return ""; + return this.command.get(n); + } + + /** + * Retourne le type de commande + * + * @return le numéro du type de commande + */ + public int getType() { + return this.type; + } + + /** + * Retourne le nom de commande + * + * @return le nom de commande + */ + public String getName() { + return this.name; + } + + /** + * Retourne l'alias de commande + * + * @return l'alias de commande + */ + public String getAlias() { + return this.alias; + } + + /** + * Numéro de ligne + * + * @return numéro de ligne + */ + public int getNoLine() { + return this.lng; + } + + /** + * Fixe la commande entière + * + * @param command une ArrayList de la commande + */ + public void setCommand(ArrayList command) { + this.command = command; + } + + /** + * Fixe le type de commande + * + * @param type le numéro de type + */ + public void setType(int type) { + this.type = type; + } + + /** + * Fixe le numéro de ligne + * + * @param lng le numéro de ligne + */ + public void setNoLine(int lng) { + this.lng = lng; + } + + /** + * Obtention de la description courte de la commande
+ * + * @return l'aide + */ + public String getDesc(){ + return null; + } + + /** + * Obtention de l'aide de la commande
+ * + * @return l'aide + */ + public String getHelp(){ + String cl = getClass().getSimpleName(); + try { + return getFContent(null, "/lunasql/doc/" + cl.toLowerCase() + ".txt"); + } catch (IOException ex) { + return "\n ERREUR : impossible d'accéder à l'aide pour " + cl + + "\n Cause : " + ex.getMessage(); + } + } + + /** + * Obtention du contenu d'un fichier ressource en console + * + * @param nom le titre à afficher (ou null) + * @param fich le nom du fichier en fichier jar + * @return le contenu + * @throws java.io.IOException si fichier inrouvable + */ + public String getFContent (String nom, String fich) throws IOException { + StringBuilder sb = new StringBuilder(); + InputStream is = getClass().getResourceAsStream(fich); + if (is == null) throw new IOException("ressource introuvable"); + + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + String read; + if (nom == null) sb.append('\n'); + else sb.append("\n ").append(SQLCnx.frmI(" " + nom, 67, '-')).append("\n\n"); + while((read = br.readLine()) != null) sb.append(read).append('\n'); + sb.deleteCharAt(sb.length() - 1); + br.close(); + return sb.toString(); + } + + /** + * Exécution de la commande
+ * + * @return RET_CONTINUE, RET_EXIT_SCR ou RET_SHUTDOWN + */ + public abstract int execute(); + + /** + * Transforme une liste en chaîne de caractères + * + * @param list la liste à transformer + * @param deb à partir de cet indice d'argument inclus + * @param fin jusqu'à cet indice d'argument inclu. Si -1: dernier élément, -2: + * @return la châine formée séparée par des espaces + */ + public static String listToString(List list, int deb, int fin) { + if (list == null) return null; + if (fin < 0) fin = list.size() + fin; + else if (fin >= list.size()) fin = list.size() - 1; + + StringBuilder sb = new StringBuilder(); + for(int i = deb; i <= fin; i++) { + if (sb.length() > 0) sb.append(' '); + sb.append(list.get(i).toString()); + } + return sb.toString(); + } + + /** + * Transforme une liste en chaîne de caractères + * + * @param list la liste à transformer + * @param deb à partir de cet indice d'argument + * @return la châine formée séparée par des espaces + */ + public static String listToString(List list, int deb) { + return listToString(list, deb, -1); + } + + /** + * Transforme une liste en chaîne de caractères + * + * @param list la liste à transformer + * @return la châine formée séparée par des espaces + */ + public static String listToString(List list){ + return listToString(list, 0, -1); + } + + // Méthodes d'exécution pour les classes filles (commandes SQL) + + /** + * Exécute une commande SQL de modification + * + * @param cmd le nom de la commande + * @param comm le commentaire à afficher + * @return continue + */ + protected int executeUpdate(String cmd, String comm) { + return executeUpdate(cmd, comm, false); + } + + /** + * Exécute une commande SQL de modification + * + * @param cmd le nom de la commande + * @param comm le commentaire à afficher + * @param ligne si l'on affiche le résumé du nb de lignes affectées + * @return continue + */ + protected int executeUpdate(String cmd, String comm, boolean ligne) { + if (!cont.getVar(Contexte.ENV_SQL_UPDATE).equals(Contexte.STATE_TRUE)) + return cont.erreur(cmd, "exécution d'une commande SQL de modification non autorisée\n" + + "(console lancée avec l'option --deny-sql-update. Cf documentation de OPT)", lng); + + try { + cont.showWheel(); + long tm = System.currentTimeMillis(); + ValeurExe vr = new ValeurExe(cont, cont.getConnex().execute(getSCommand())); + tm = System.currentTimeMillis() - tm; + cont.hideWheel(); + int n = vr.getNbLines(); + if (ligne) comm = (cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE) ? + Contexte.COLORS[Contexte.BR_CYAN] + n + Contexte.COLORS[Contexte.NONE] : n) + + " ligne" + (n > 1 ? "s" : "") + comm + (n > 1 ? "s" : ""); + vr.setDispValue("-> " + comm + " (" + SQLCnx.frmDur(tm) + ")"); + cont.setValeur(vr); + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + cont.setVar(Contexte.ENV_CMD_STATE, n > 0 ? Contexte.STATE_TRUE : Contexte.STATE_FALSE); + return RET_CONTINUE; + } + catch (SQLException ex) { + cont.hideWheel(); + return cont.exception(cmd, "ERREUR SQLException : "+ ex.getMessage(), lng, ex); + } + } + + /** + * + * @param cmd le nom de la commande + * @param comm le commentaire à afficher + * @return l'état d'exécution + */ + protected int executeCall(String cmd, String comm) { + try { + cont.showWheel(); + long tm = System.currentTimeMillis(); + ValeurReq vr = new ValeurReq(cont, cont.getConnex().getResultString(getSCommand(), + cont.getVar(Contexte.ENV_SELECT_ARR).equals(Contexte.STATE_TRUE), + cont.getColMaxWidth(lng), cont.getRowMaxNumber(lng), + cont.getVar(Contexte.ENV_ADD_ROW_NB).equals(Contexte.STATE_TRUE), + cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE))); + tm = System.currentTimeMillis() - tm; + cont.hideWheel(); + + if (cont.getVar(Contexte.ENV_COLORS_ON).equals(Contexte.STATE_TRUE)) + vr.setDispValue("-> " + comm + + (vr.isNblTrunc() ? Contexte.COLORS[Contexte.BR_YELLOW] + " (d'autres lignes existent)" + + Contexte.COLORS[Contexte.NONE] : "") + " (" + SQLCnx.frmDur(tm) + ")"); + else + vr.setDispValue("-> " + comm + (vr.isNblTrunc() ? " (d'autres lignes existent)" : "") + + " (" + SQLCnx.frmDur(tm) + ")"); + + cont.setValeur(vr); + cont.setVar(Contexte.ENV_EXEC_TIME, Long.toString(tm)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_TRUE); + return RET_CONTINUE; + } + catch (SQLException ex) { + cont.hideWheel(); + return cont.exception(cmd, "ERREUR SQLException : "+ ex.getMessage(), lng, ex); + } + } + + @Override + public int hashCode() { + return this.getName().hashCode() * this.getLength(); + } +}// class diff --git a/src/lunasql/doc/bonus.txt b/src/lunasql/doc/bonus.txt new file mode 100644 index 0000000..aeb24a2 --- /dev/null +++ b/src/lunasql/doc/bonus.txt @@ -0,0 +1,172 @@ + *** Quelques informations bonus et exemples de codes *** + +Cette rubrique liste quelques échantillons de code LunaSQL pour en +apprécier la syntaxe élégante :) en dehors de tout code SQL ! +Toute ressemblance avec du code existant ou ayant existé est purement +fortuite. + +Vous trouverez également sur ce dépot d'autres échantillons de code : +https://github.com/CodeEspritLibre/lunasql-samples + + +/*-------------------------------- Petit jeu ----------------------*/ + +/* + * Jeu consistant à deviner un nombre entre 0 et 10 utilisant la + * récursivité ! C'est un exemple pédagogique, dans la vraie vie + * préférer bien sûr utiliser une boucle. + */ +opt :ALLOW_RECUR 1 +def jeu-devine { + print "Devinez un nombre entre 0 et 9" + def nbalea,nbcoups $[rand -m=nbr -s=9] 0 + devine +} +def devine { + def nbcoups,nbsaisi $(nbcoups#inc!) $[input "Nombre ? "] + if ['$(nbsaisi)'=='$(nbalea)'] { + print "Bravo, réussi en $(nbcoups) coups !" + undef nbalea nbsaisi nbcoups + } else { + if ['$(nbsaisi)'>'$(nbalea)'] { + print "Non, plus petit." + } else { + print "Non, plus grand." + } + devine + } +} +-- lancement du jeu +jeu-devine + + +/*---------------------- Itérateurs, séquences et listes ----------*/ + +/* + * Exemple de boucle while affichant les arguments saisis + */ +def repeat { + let i=0;while"i++<$(arg_nb)" { print"$(arg$[I(i)])" } +} +repeat 1 4 7 + +/* + * Création d'une séquence simple de nombres pour itérateur : + * de m à n, pas p + * Usage : seq [fin=10] [deb=0] [inc=1] + * @need 4.4 + */ +def seq { + arg [fin:int 10] [deb:int 0] [pas:int 1] + def -l r "" + for i -r $deb:$fin:$pas {def -u1 r "$r$i "} + return $r +} +print $[seq 5] + +/* + * Création d'une séquence élaborée de nombres pour itérateur : + * de m à n, pas p + * Usage : range nombre [deb=0] [inc=1] + * L'incrément est une variable JS + * Exemple pédagogique, préférer la commande : list range + */ +def range { + if !$arg_nb { + error "Usage : range nbre [deb=0] [inc=1]" + } + def -l nbre,deb,inc $arg1 $(*arg2?0) $(*arg3?1) + let [i = $deb; n = 0;] + def r "" + while [n++<$nbre] { + def r "$r $[I(i)]" + let [i = i + $inc;] + } + def -l r0 $r -- pour undef la var. r avant retour + undef r + return $[str ltrim $r0] +} +-- utilisation +print $[range 5] +for i $[range 5 10 2] {print $i} + +/* + * Affichage d'une séquence de nombre en couleurs + */ +def print-colored-seq { + def -l nmax $(*arg1?20) + let idx=0 + while [(idx=idx+1)<=$nmax] { + if [idx%2==0] { -- pairs en vert + print -n -c2 "$[I(idx)] " -- I() force le type int + } else { -- impairs en rouge + print -n -c3 "$[I(idx)] " + } + } + print +} +-- tirage d'une séquence +print-colored-seq +print-colored-seq 10 + + +/*-------------------- Programmation fonctionnelle ----------------*/ + +/* + * Fonction de haut niveau + */ +-- Ajoute 5 +defm add5 {n:int} {let $n+5} +-- Multiplie par 3 +defm mult3 {n:int} {let $n*3} + +-- Fonction d'appel +defm calc {func n:int} { + print "calc $n : $[$func $n]" +} + +calc add5 8 +calc mult3 12 + +/* + * Décorateurs + */ +-- Décorateur : encadre par deux chaînes +defm decor {func} { + -- fonction "décorée" retournée + put " + print ^"------------^" + $func + print ^"------------^" + " + -- ou $($func) pour intégrer le corps +} + +-- Fonction à décorer +def print_text { + print "Hello world!" +} + +-- Appel +def print_text_dec $[decor print_text] +print_text_dec + + +/*-------------------------------- Réseaux ------------------------*/ + +/* + * Affichage de l'adresse MAC de la première interface réseau avec + * séparateur tirets. Exemple pédagogique uniquement. + */ +def get-mac { + def -l mac,sep "" "" + for by $[str split $[list get $[list get $[info network] 0] 1] ""] { + if $(sep=) {def -u2 sep -} else {def -u2 sep ""} + def -u1 -a mac $(sep)$(by) + } + return $[str substr $mac 1:] +} + +/* + * Bon travail avec LunaSQL ! + */ diff --git a/src/lunasql/doc/catching.txt b/src/lunasql/doc/catching.txt new file mode 100644 index 0000000..389b92b --- /dev/null +++ b/src/lunasql/doc/catching.txt @@ -0,0 +1,29 @@ +En cas d'erreur, l'option :EXIT_ON_ERR permet ou non d'interrompre +l'exécution (bloc de code courant, ex. fichier sql) et de sortir. +Dans tous les cas, le contenu de l'option :ON_ERROR est évalué. + +Cas selon valeurs de :EXIT_ON_ERR (EOE) et bloc :ON_ERROR (BOE), +en cas d'erreur, que se passe-t-il ? +EOE BOE Effet +1 vide sortie simplement sur l'erreur +1 non vide sans exit BOE exécuté avec sortie sur l'erreur +1 non vide avec exit BOE exécuté avec sortie sur EXIT +0 vide rien (donc reprise après l'erreur) +0 non vide sans exit BOE exécuté puis reprise après l'erreur +0 non vide avec exit BOE exécuté puis sortie sur EXIT + +En bloc :ON_ERROR, les variables locales suivantes sont disponibles : + - err_lng : numéro de ligne de l'erreur + - err_cmd : commande ayant lancé l'erreur + - err_msg : message d'erreur complet + - err_stk : pile d'appels + +Un autre mécanisme de gestion d'erreur est implanté dans la commande +EVAL (cf. aide EVAL). Les deux mécanismes sont complémentaires. + +Notes : - :ON_ERROR n'est pas exécuté sur un appel à EXIT, ni en cas + d'erreur déjà rattrapée par EVAL -c + - les options _ERROR_CMD et _ERROR_MSG fournissent des + renseignements sur les erreurs non attrapées. + +« Dans le doute, reboote ; si ça rate, formate. » (mimi, 05/10/2017) diff --git a/src/lunasql/doc/changelog.txt b/src/lunasql/doc/changelog.txt new file mode 100644 index 0000000..3a03e98 --- /dev/null +++ b/src/lunasql/doc/changelog.txt @@ -0,0 +1,85 @@ + == Version 4.9 Quincella == + * Ajout des commandes APPEND, NET (GET, POST) + * Ajout de l'API en serveur HTTP pour requêter du code LunaSQL + * Ajout de la signature de fichiers/scripts par EdDSA et des + commandes SIGN (signature, génération de clef), VERIFY + * Sécurisation du chiffrement en CRYPT, EXPORT (mode CBC) + * Prise en charge des fichiers .cry en EXEC + * Ajout des options ALL?, ANY? en DICT; -t, -l en IMPORT; -c, -d en + CSV, -p en HASH, -f en CRYPT/DCRYPT, -o en CONFIG + * Ajout de la commande en bibliothèque REPORT + * Ajout des menus Signer en Vérifier en éditeur + * Ajout du paquet 'const' + * Correction de quelques bogues et diverses améliorations + + == Version 4.8 Pélissa == + * Ajout du préfixage des commandes par =, et des alias par * + * Ajout de l'option de lancement --deny-opt-command, de l'option + _SQL_UPDATE et --deny-sql-update, et de l'option --need-version + * Ajout des options REDEFINED, NOCIRCCTRL en INFO; -w, FILE (-f), + ERRFILE (-u), SUSPEND (-s), RESUME (-r), en SPOOL (renommage de + l'ancienne -s); -n en EXPORT; -a, -r en DEF; -q en XML; -y en DEF, + CHOP en STR, mode retrait -1 en STR WRAP, 'cancel' pour :ON_INIT + et :ON_QUIT + * Retrait de l'option INFO mac.address + * Ajout de l'option :ALLOW_REDEF et possibilité de redéfinir une + commande interne par un alias + * Renforcement du contrôle de ref. circulaire des alias/macros + * Suppression de la bibliothèque Jansi + * Correction de pas mal de bogues + + == Version 4.7 Olivia == + * Ajout de la commande NEXT + * Ajout des options -t en HELP, -e en HIST; TOLINES, VIGENC, VIGDEC, + CONVERT en STR; REDUCE, FIRST, LAST, EACH, REPEAT, ZEROS, ONES, + REVERSE en LIST; EACH, MERGE en DICT; EACH, EACHLN en FILE; REPEAT, + WAIT, DELAY en TIME; -v, -f en EVAL; FIND, STACK en INFO; -r/-d à + INPUT, -n en FOR + * Ajout de la validation des arguments en commande ARG + * Ajout des modif. de variable : fonction #flat + * Ajout de la commande XML (module xml) + * Ajout des variables locales this_scope, super_scope et scope_deep + en Lecteurs + * Ajout de l'option :ADD_ROW_NB + * Ajout de la bibliothèque obj + * Amélioration de l'aide de defmacro + * Amélioration de la gestion de :HISTORY_DBL (0, 1 ou 2) + * Ajout des substitutions de caractères : {}[]<>() + * Ajout des options :ON_INIT et :ON_QUIT + * Retrait des options :DEEP_SUBSTIT et :EASY_SUBSTIT + * Ménage fait dans les bibliothèques JS et dans les aides + * Quelques corrections de bogues, comme d'habitude + + == Version 4.6 Nathalia == + * Ajout de la commande PUT + * Ajout des options PUSH, TOCSV en LIST, FROMCSV en STR; -t, -n à + EVAL (suppression de -e); -l à OPT (options locales); NEW, + DATETIMEMS, ATDO, AFTERDO à TIME; 'date/heure' à WAIT; WHATIS, + TABLES, SYSTABLES à INFO + * Ajout de l'option _ERROR_CMD + * Ajout de la profondeur en commande BREAK + * Pour TIME, les dates et formats peuvent être encadrés par '' + * Ajout du support de [] pour les listes et FOR + * En mode :END_CMD_NL=0, les ; finaux en sous-lecteurs sont + facultatifs + * Ajout du forçage de résolution d'alias par préfixe ':' + * Ajout des commentaires simple-ligne actifs par --~ + * Ajout du choix du SE pour la notation '+' de EXEC et START + * Ajout des substitutions en paramètres optionnels pour ARG + * Ajout de fonctions sur tables en éditeur UI + + == Version 4.5 Marina == + * Ajout des commandes EXPLAIN, TREE + * Ajout de l'option :ROW_MAX_NB + * Ajout des options -h en DEF, PICK, en LIST, LOCAL en DICT; SCAN, + BYTES en FILE, NEMPTY?, ZIPENC, ZIPDEC en STR; -c, -m en SHOW + * FILE DUMP et LIST/LINES/VIEW peut lire/écrire en gzip + * Ajout du support des multi var. pour la commande FOR + * Retrait du support des chaînes dures !! !!, et les {} sont retirés + de la chaîne dure qu'ils encadrent + * Ajout de la césure d'arguments par préfixe # + * Adaptation du ScriptEngine à Nashorn (Java 8) et amélioration de + la gestion globale des SE + * Ajout des listes de tables et de variables en éditeur (EDIT) + * Modernisation de la console web http-console + diff --git a/src/lunasql/doc/cmdalter.txt b/src/lunasql/doc/cmdalter.txt new file mode 100644 index 0000000..7df3c08 --- /dev/null +++ b/src/lunasql/doc/cmdalter.txt @@ -0,0 +1,22 @@ + ALTER : Modifie la structure de la base + - Index : + ALTER INDEX RENAME TO + - Séquence : + ALTER SEQUENCE RESTART WITH + - Schéma : + ALTER SCHEMA RENAME TO + - Table (ajout de colonne) : + ALTER TABLE ADD [COLUMN] Datatype + [(columnSize[,precision])] [{DEFAULT | + GENERATED BY DEFAULT AS IDENTITY + (START WITH [, INCREMENT BY ])}] | + [[NOT] NULL] [IDENTITY] [PRIMARY KEY] + [BEFORE ] - Table (retrait de colonne) : + ALTER TABLE DROP [COLUMN] + - Table (modification de colonne) : + ALTER TABLE ALTER COLUMN {SET | + RENAME TO } + - User : + ALTER USER SET PASSWORD + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES diff --git a/src/lunasql/doc/cmdappend.txt b/src/lunasql/doc/cmdappend.txt new file mode 100644 index 0000000..e6138ef --- /dev/null +++ b/src/lunasql/doc/cmdappend.txt @@ -0,0 +1,20 @@ + APPEND : Affecte la valeur de retour *en mode ajout* sans sortir + APPEND [valeur] + Comme de la commande PUT, les arguments sont placés en valeur + de retour sans que l'exécution soit interrompue, mais l'ancienne + valeur est conservée, et la nouvelle est concaténée. Le résultat + est retourné pour la commande englobante $[] et placé en constante + _RET_VALUE, mais n'est pas affiché. + + Note : il n'est pas possible d'utiliser des substitutions de + commandes dans la mesure où elles fonctionnent sur le principe de + la commande PUT (donc elles écrasent la valeur précédente); + + Exemples : + str len hello -- puis: + APPEND hello -- la valeur de retour est '5hello' + APPEND $[str len hello] -- la valeur est '55' (retour de $ doublé) + put -- maintenant la valeur est vide + + Attribue à _RET_VALUE la valeur chaîne en paramètre, éventuellement + concaténé à la précédente valeur, sinon rien si aucun paramètre. diff --git a/src/lunasql/doc/cmdarg.txt b/src/lunasql/doc/cmdarg.txt new file mode 100644 index 0000000..f9585cd --- /dev/null +++ b/src/lunasql/doc/cmdarg.txt @@ -0,0 +1,78 @@ + ARG : Affecte les arguments passés en variables locales + Doit être appelé au sein d'un bloc lecteur à arguments de ligne + de commande positionnés : typiquement, ARG est positionnée en + début de macro (pour capter les arguments passés à la macro), ou + en début de fichier de script (pour ceux passés au fichier). + ARG var1:type var2:type [var3:type defaut] *var4:type / cond. + Types : exp. rég. encadrée par ` et `, ou commandes suivantes : + int (entier), nat (naturel), num (numérique), char, + bool (0,1,t(rue),f(alse)), yesno (y[es],n[o[n]],o[ui]), + id (nom de var.), opt (nom d'option), idopt (les deux), + word (lettre, chiffre, _), date (JJ/MM/AAAA), + datetime (JJ/MM/AAAA HH:MI:SS), dateus (MM/JJ/AAAA), + datets (AAAA-MM-JJ), alpha, alphanum, alphauc (maj.), + alphalc (min.), email, str (tout, défaut). + + Les arguments obligatoires sont déclarés par leur nom seul, les + optionnels encadrés par [], et le groupage précédé de *. La valeur + par défaut peut contenir des substitutions par '$', mais elles + doivent alors être échappées. + Les noms de paramètres (var. locales) peuvent commencer par '.' + Tout ce qui suit un '/' placé seul en arg. est considéré comme + une expression booléenne de validation des arguments. + + En cas de mauvais usage de la macro par rapport à la définition + donnée à ARG, une erreur est levée, sécurisant l'exécution. + ARG peut ainsi avantageusement remplacer des def -l et des if : + ARG a b [c 0] *d -- est l'équivalent de : + def -l a,b,c,d $arg1 $arg2 $(*arg3?0) $(args_ls@3:) + if $(a#empty?) {error "argument a vide"} -- etc etc + + Exemples : + def f {ARG .a .b; print $(.a) $(.b)}; f do fa + def f {ARG a b [c mi]; print $a $b $c}; f do ré --> do ré mi + def f {ARG a *b; print $a $b}; f do "ré mi" --> do {ré mi} + def f {ARG a [b]; print $a $b}; f do --> do + def f {ARG a:num [b:int 0]; print $a $b}; f 1.8 2 --> 1.8 2 + def f {ARG a:`\w+`; print $a}; f a123 --> a123 + def f {ARG [a:int ^$`?a`] [b ^$(uneVar)] [c ^$[call 6*7]]} + f --> a est demandé, b prend $(uneVar), c est calculé par CALL + -- donc avec defmacro : + defm f {[a:int ^$`?a`] [b $(uneVar)] [c ^$[call 6*7]]} { + -- code utilisant les paramètres a, b et c + } + -- arguments chaînés : b prend par défaut la valeur de a, c de b + def f {ARG [a 1] [b ^$a] [c ^$b]; print $a $b $c} + f 2 --> 2 2 2 + f 2 3 --> 2 3 3 + f 2 3 4 --> 2 3 4 + -- validation de arguments : ici b doit être supérieur à a + def f {ARG a:int [b:int 2] / {$b > $a}; print $a $b} + + Les arguments optionnels [x] ou englobants *x ne peuvent être + placés qu'en fin de liste. Ces appels lèvent une erreur : + def f {arg a [b] c}; f 1 2 -- 2 est pris par b + def g {arg a *b c}; g 1 2 3 -- 2 et 3 sont pris par b + + Pour implanter des arguments nommés, utiliser un dict. Exemple : + defm f [opts] { -- opts est un dict ou une chaîne vide + with $opts { -- with évite les appels $(opts,key) + def -u2 a $[list nvl [{$(*a)} {valA}]] -- valA par défaut + def -u2 b $[list nvl [{$(*b)} {valB}]] -- valB par défaut + -- notez bien "-u2" car le bloc with entre en 2 lecteurs + } + -- Utilisation de nos arguments nommés a et b + print "a=$a et b=$b" + } + f --> a=valA et b=valB + f { a=1// c=3 } --> a=1 et b=valB + f { a=1// b=2 } --> a=1 et b=2 + + Notes : + - pour déclarer une macro (avec ou sans arguments/aide), il est + préférable d'utiliser defmacro (qui utilise ARG) plutôt que def + - bien sûr, l'utilisation de ARG, bien que pratique, est un peu + plus gourmande que l'équivalent par $arg1, $arg2..., surtout en + cas d'usage de la validation. + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdbreak.txt b/src/lunasql/doc/cmdbreak.txt new file mode 100644 index 0000000..c5e0a46 --- /dev/null +++ b/src/lunasql/doc/cmdbreak.txt @@ -0,0 +1,17 @@ + BREAK : Sort d'une boucle FOR ou WHILE. + BREAK : Sort de la boucle la plus interne (équivaut à BREAK 0) + BREAK n : Sort de toutes les boucles jusqu'au niveau le lecteur n + + Exemples : + for -r i 5 { print $i; if $i==2 BREAK } -- sort au bout de 3 + i=0; while i++<10 {/* traitement */ BREAK} + i=0; while i++<5 {j=0; while j++<5 {BREAK 1}; print "non affiché"} + + Note : le niveau de lecteur n n'est pas forcément la profondeur des + boucles, mais la profondeur des lecteurs. Exemples : + for -r i 5 {for -r j 5 {BREAK 1}} -- sortie au premier i + for -r i 5 {for -r j 5 {if 1{BREAK 1}}} -- pas de sortie pour i ! + for -r i 5 {for -r j 5 {if 1{BREAK 2}}} -- ok + + Attribue à la constante _RET_VALUE la profondeur de sortie + (défaut 0) diff --git a/src/lunasql/doc/cmdbuffer.txt b/src/lunasql/doc/cmdbuffer.txt new file mode 100644 index 0000000..3f7a873 --- /dev/null +++ b/src/lunasql/doc/cmdbuffer.txt @@ -0,0 +1,24 @@ + BUFFER (+) : Gère une var. tampon pour des commandes à portée de + main (ajout, suppression, lecture/écriture en fichier). + BUFFER option|commande + Avec option(s) parmi (exécution dans cet ordre si multiples) : + -c | CLEAR : efface le contenu du tampon + -l | LIST : liste le contenu du tampon + -e | RUNALL : exécute le contenu du tampon + -p | POP : supprime et retourne la dernière entrée du tampon + -r | RUNLAST : exécute la dernière entrée du tampon + -i | RUNITH : exécute la i-ème entrée du tampon + -n | SIZE : retourne le nombre d'entrées du tampon + -u | RUNPOP : exécute et supprime la dernière entrée + -a | ADD : ajoute la commande c au tampon. Pour plusieurs + commandes, les séparer par ';;' + -h | ADDLAST : ajoute la dernière commande au tampon + -g | LMODE : ajoute du contenu au tampon en mode ligne + -o | LOAD : charge le contenu en paramètre en tampon + -d | DUMP : retourne simplement le contenu + Note : en cas de dépassement d'index (ex. tampon vide), aucune + erreur n'est levée. + + Les options suivantes modifient comme suit _RET_VALUE : + -ln (nb d'entrées), -eris (selon retour cmd), -p (dern. entrée), + -n (selon retour cmd). Les autres lui attribuent 0. diff --git a/src/lunasql/doc/cmdcall.txt b/src/lunasql/doc/cmdcall.txt new file mode 100644 index 0000000..62a4089 --- /dev/null +++ b/src/lunasql/doc/cmdcall.txt @@ -0,0 +1,5 @@ + CALL : Appelle une procédure ou évalue une expression SQL + L'expression est dépendante du SGBD + CALL expression|procédure + + Modifie les constantes _RET_VALUE et _RET_NLINES comme SELECT. diff --git a/src/lunasql/doc/cmdcase.txt b/src/lunasql/doc/cmdcase.txt new file mode 100644 index 0000000..4b23941 --- /dev/null +++ b/src/lunasql/doc/cmdcase.txt @@ -0,0 +1,13 @@ + CASE : Teste en chaîne une exp. reg. et exécute les arguments + CASE [ ]* [ELSE ] + 'bloc' doit être déclaré par {} ou "" car il doit former + un seul argument. Si la valeur est encadrée par ` et `, le test est + réalisé sur expression régulière. + + Exemples : + CASE $a 0 {print a=0} 1 {print a=1} else "print autre chose" + CASE abc `a.*` {print commence par a} `b|c` {print "b ou c"} + + Ne modifie pas les constantes _RET_VALUE et _CMD_STATE (sauf + erreur), mais les commandes exécutées peuvent les modifier. + Attribue 0 à _RET_VALUE si aucun bloc exécuté. diff --git a/src/lunasql/doc/cmdcomment.txt b/src/lunasql/doc/cmdcomment.txt new file mode 100644 index 0000000..31c40c3 --- /dev/null +++ b/src/lunasql/doc/cmdcomment.txt @@ -0,0 +1,23 @@ + COMMENT : Ajoute un commentaire à un objet de la base + - Colonne : + COMMENT ON COLUMN . IS + - Index : + COMMENT ON INDEX IS + - Table : + COMMENT ON TABLE
IS + - Rôle : + COMMENT ON ROLE IS + - Séquence : + COMMENT ON SEQUENCE IS + - Schema : + COMMENT ON SCHEMA IS + - Trigger : + COMMENT ON TRIGGER IS + - User : + COMMENT ON USER IS + - View : + COMMENT ON VIEW IS + - Contrainte : + COMMENT ON CONSTRAINT IS + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES diff --git a/src/lunasql/doc/cmdcommit.txt b/src/lunasql/doc/cmdcommit.txt new file mode 100644 index 0000000..f5d2d01 --- /dev/null +++ b/src/lunasql/doc/cmdcommit.txt @@ -0,0 +1,3 @@ + COMMIT : Valide une transaction SQL au niveau du SGBD + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. diff --git a/src/lunasql/doc/cmdconfig.txt b/src/lunasql/doc/cmdconfig.txt new file mode 100644 index 0000000..66c104b --- /dev/null +++ b/src/lunasql/doc/cmdconfig.txt @@ -0,0 +1,36 @@ + CONFIG (cf) : Charge ou sauvegarde la configuration actuelle en + fichier, à savoir variables, constantes et greffons + CONFIG -l | LOAD Charge le fichier f + CONFIG -s | SAVE Enregistre en fichier f + Sans nom de fichier, le fichier en _CONFIG_FILE est utilisé. Sans + aucun argument, équivalent à CONFIG -l. + Avec l'option -o, seules les options (variables començant par ':') + sont chargées ou sauvées. + + Exemple de configuration possible en fichier : + +-------------------------- code ----------------------------------- +# Constantes de paramétrage +\:ALIAS_ARG = 0 +\:PROMPT = EXoTiQ +# Variables et fonctions utilisateur +cmdstat = '$(_CMD_STATE)'=='E' ? 'ERREUR' : 'OK' +# Greffons java +?echo = org.plugins.CmdEchoPlugin +------------------------------------------------------------------- + + Notes : - l'option -s n'exporte que les options et les variables + globales (aucun paramètre système (_*) ni var. locale) + - l'opérateur d'affectation peut être '=' ou ':', donc + chaque ':' en identifiant doit être échappé par \ + - le code contenu en macro n'est pas interprété : ne pas + échapper les $ et caracères spéciaux (cf. cmdstat ci-dessus) + - si une bibliothèque est chargée en console (commande USE), + alors lors d'un SAVE, les options et variables associées seront + également exportées + - insérez un saut de ligne par la chaîne '\n'. Pour une entrée + sur plusieurs lignes, terminer chaque ligne par \ + - le moteur d'analyse est le format Properties de Java. Voir + https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html + + Attribue le nombre de var. écrites à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdcreate.txt b/src/lunasql/doc/cmdcreate.txt new file mode 100644 index 0000000..10b0708 --- /dev/null +++ b/src/lunasql/doc/cmdcreate.txt @@ -0,0 +1,52 @@ + CREATE : Crée un nouvel objet dans la base + - Alias (fonction Java externe appelable depuis son package) : + CREATE ALIAS FOR + - Index : + CREATE [UNIQUE] INDEX ON
( [DESC] [, ...]) + [DESC] + - Rôle : + CREATE ROLE + - Schéma : + CREATE SCHEMA AUTHORIZATION + [ [] [...] + - Sequence : + CREATE SEQUENCE [AS {INTEGER | BIGINT}] + [START WITH ] [INCREMENT BY ] + - Table : + CREATE [MEMORY | CACHED | [GLOBAL] TEMPORARY | TEMP | TEXT] TABLE + ( [, ...] [, ...] ) + [AS ] + [ON COMMIT {DELETE | PRESERVE} ROWS] + : + columnname [(columnSize[,precision])] + [{DEFAULT | GENERATED BY DEFAULT AS IDENTITY + (START WITH [, INCREMENT BY ])}] | [[NOT] NULL] [IDENTITY] + [PRIMARY KEY] + : + - alphanum. CHAR(n) Chaîne de longueur fixe n (n<16383) + - alphanum. VARCHAR(n) Chaîne de n caractères maximum (n<16383) + - num. NUMBER(n,[d]) Nombre de n chiffres [d après la virgule] + - num. SMALLINT Entier signé de 16 bits (-32768 à 32757) + - num. INTEGER Entier signé de 32 bits (-2E31 à 2E31-1) + - num. FLOAT Nombre à virgule flottante + - horaire DATE Date sous la forme 16/07/99 + - horaire TIME Heure sous la forme 12:54:24.85 + - horaire TIMESTAMP Date et Heure : + [CONSTRAINT ] + UNIQUE ( [,...]) | PRIMARY KEY ( + [,...]) | FOREIGN KEY ( [,...] ) + REFERENCES ( [,...]) [ON {DELETE | + UPDATE} {CASCADE | SET DEFAULT | SET NULL}] | + CHECK() + - Trigger : + CREATE TRIGGER {BEFORE | AFTER} {INSERT | UPDATE | DELETE} + ON
[FOR EACH ROW] [QUEUE n] [NOWAIT] CALL + - User : + CREATE USER PASSWORD [ADMIN] + - View : + CREATE VIEW [(,..) AS SELECT ... FROM ... + [WHERE Expression] + [ORDER BY orderExpression [, ...]] + [LIMIT [OFFSET ]] + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES diff --git a/src/lunasql/doc/cmdcrypt.txt b/src/lunasql/doc/cmdcrypt.txt new file mode 100644 index 0000000..c4646eb --- /dev/null +++ b/src/lunasql/doc/cmdcrypt.txt @@ -0,0 +1,40 @@ + CRYPT : Chiffre le message donné + CRYPT -k= -i= Chiffre le message msg par clef c + CRYPT -p=

-i= Chiffre le message msg par mot de passe p + CRYPT -f= -k/-p Chiffre le fichier f par clef k ou mdp p + + La clef doit être binaire (typiquement par commande HASH), ou bien + founie par -p (dérivation de mot de passe en clef de 128 bits). Avec + -k, seuls les 128 ou 256 premiers bits (forts) sont retenus pour la + clef (la clef peut faire 128 ou 256 bits). + Le vecteur d'initialisation (VI) doit être de 128 bits (typiquement + par commande rand -m=sec -t=16). + Attention: par sécurité, le VI doit être UNIQUE pour chaque clef ! + Le message est d'abord compressé par GZip puis chiffré par l'algo + AES 128/256 (selon la clef fournie) en mode CBC/PKCS5Padding. + Avec l'option -f, l'option -i n'est pas prise en compte car le VI est + généré et directement attaché au fichier. + + Options : -r= -- b64|hex|utf (défaut: hex) + -u= -- b64|hex|utf (défaut: hex) + -t= -- b64|hex|utf (défaut: hex) + -a= -- AES|Blowfish|DES|DESede (défaut: AES) + -s= -- nb de caractères par lignes en sortie + + Exemples : + def iv $[rand -m=sec -t=16] -- génération du VI + CRYPT -k=$[hash clef] -i=$iv "message secret" --> msg chiffré + CRYPT -p=password -i=$iv "message secret" --> msg chiffré + print $[CRYPT -k=$[hash $[input -p mdp?]] -i=$iv $message] + CRYPT -r=b64 -s=32 -a=AES -k=4QrcOUm6Wau+VuBX8g+IPg== -i=$iv ^ + "un message secret à chiffrer" + CRYPT -k=4Qrc... -r=b64 -f=data.txt --> taille de f + def key $[rand -m=sec -t=32] -- AES 256 + CRYPT -k=$key -f=data.txt -- DONE! + CRYPT -p=password -f=data.txt -- AES 128 + + Attention ! Bien que cette commande utilise un algorithme puissant, + pour les échanges de données sensibles il est préférable de choisir + un protocole dédié au chiffrement tel que NaCl/libsodium ou OpenPGP. + + Attribue le résultat à la constante _RET_VALUE ou 0 si erreur. diff --git a/src/lunasql/doc/cmdcsv.txt b/src/lunasql/doc/cmdcsv.txt new file mode 100644 index 0000000..587e91a --- /dev/null +++ b/src/lunasql/doc/cmdcsv.txt @@ -0,0 +1,36 @@ + CSV : Lit le contenu d'un CSV depuis requête SQL + CSV [-p {options}] + Chaque fichier du rép. constitue une table (omettre l'extension). + Le format de fichier se définit en options : + avec l'option -p, prend un dictionnaire {clef=valeurs}, selon + les clefs définies en doc http://csvjdbc.sourceforge.net/doc.html + La commande prend également en charge les fichiers compressés + (extension : .zip). + + Par défaut le ResultSet est affiché en console en mode formaté + (comme la commande SELECT), mais ce comportement peut être modifié : + avec l'option -d, une liste de dictionnaires est retournée + avec l'option -c=, un bloc de code est exécuté pour chaque + ligne avec comme var. locales les noms de champs de la ligne et + row_id le numéro de la ligne en cours. + + Exemples avec un fichier 'clients.csv' en dossier 'data/' : + def opts { + headerline = ID;LIB;COMM + separator = ; + trimValues = true + } + CSV -p=$opts data select * from clients -- simple affichage + CSV -p=$opts -d data select * from clients --> liste de dict. + CSV -p=$opts data select * from clients -c { + print "$row_id: $ID -> $LIB (commentaire: $COMM)" + } + + Notes : - requiert la bibliothèque csvjdbc.jar en CLASSPATH, à + charger par la commande : use csv + - pour éviter qu'un tiret dans la requête soit pris comme un + argument, il peut être utile de l'encadrer par "" + + Attribue la 1re valeur de la 1re ligne, ou avec -d la liste, ou avec + -c le retour de la dernière ligne à la constante _RET_VALUE et + le nombre de lignes retournées à la constante _RET_NLINES. diff --git a/src/lunasql/doc/cmddcrypt.txt b/src/lunasql/doc/cmddcrypt.txt new file mode 100644 index 0000000..6aa163d --- /dev/null +++ b/src/lunasql/doc/cmddcrypt.txt @@ -0,0 +1,31 @@ + DCRYPT : Déchiffre le message chiffré donné + DCRYPT -k= -i= Déchiffre le chiffré par clef c, VI i + DCRYPT -p=

-i= Déchiffre le chiffré par mot de passe p + DCRYPT -f= -k/-p Déchiffre le fichier f par c ou p + + La clef doit être binaire (typiquement par commande HASH), ou bien + founie par -p (dérivation de mot de passe en clef de 128 bits). Avec + -k, seuls les 128 ou 256 premiers bits (forts) sont retenus pour la + clef (la clef peut faire 128 ou 256 bits). + Le vecteur d'initialisation (VI) doit être de 128 bits (typiquement + par commande rand -m=sec -t=16). + Le chiffré doit être binaire (créé par commandes CRYPT ou EXPORT). + Avec l'option -f, l'option -i n'est pas fournie par le VI est + attaché au fichier. + + Options : -r= -- b64|hex|utf + -u= -- b64|hex|utf (défaut: hex) + -t= -- b64|hex|utf + -a= -- AES|Blowfish|DES|DESede + + Exemples : + def iv $[rand -m=sec -t=16] -- génération du VI + DCRYPT -k=$[hash clef] -i=$iv 5E28...BB197 --> déchiffré + DCRYPT -p=password -i=$iv 5E28...BB197 --> déchiffré + DCRYPT -k=$[hash $[input -p mdp?]] -i=$iv 5E28D9...EBB197 + DCRYPT -r=b64 -t=hex -a=AES -k=4QrcOUm6Wau+VuBX8g+IPg== ^ + -i=$iv 5E28D95F22DAFCC3...572F38637BEBB197 + DCRYPT -k=4Qrc... -r=b64 -f=data.txt.cry --> taille de f + DCRYPT -p=password -f=data.txt.cry + + Attribue le résultat à la constante _RET_VALUE ou 0 si erreur. diff --git a/src/lunasql/doc/cmddef.txt b/src/lunasql/doc/cmddef.txt new file mode 100644 index 0000000..36236fa --- /dev/null +++ b/src/lunasql/doc/cmddef.txt @@ -0,0 +1,225 @@ + DEF (:) : Affecte une valeur (chaîne de caractère) à une variable + de console (variable indépendante du moteur SE.) + DEF [-l|-u|-h|-d|-y|-a|-r] x,y,z val1 val2 val3 + Affecte à une ou plusieurs variable(s) les expressions listées en + paramètres, séparées par des espaces. Dans l'exemple ci-dessus, + affecte à x la valeur val1, à y la valeur val2, à z la valeur val3 + Les valeurs en trop sont passées à la dernière var., et les var. + en trop reçoivent la chaîne vide "". '~' découpe les arguments. + Caractères spéciaux autorisés dans le nom de variable : .-_ + Les accents sont autorisés, mais gare à l'encodage des fichiers ! + Exemples: + DEF s "value of s" + DEF s,t,u,v do ré mi fa -- chaque var. reçoit une valeur + DEF s,t,u do ré mi fa -- u vaut 44 45 + DEF l "do ré mi fa" -- liste + DEF s,t,u,v ~$l -- chaque var. reçoit une valeur de l + + Avec l'option -a, ajoute la valeur à la variable existante. La + recherche est effectuée en local puis en global. Si la variable + n'existe pas, elle est simplement créée avec la nouvelle valeur. + Exemple: + DEF s salut; DEF -a s " monde" -- s vaut "salut monde" + -- équivalent de : DEF s salut; DEF s "$s monde" + + Avec l'option -r, la (les) variables alias globales déclarées + seront exemptes du contrôle de référence circulaire lors de leur + exécution en tant qu'alias/macro, quelque soit :ALLOW_RECUR. + + Avec option -l, les variables sont déclarées localement au lecteur + en cours (fichier ou alias de commandes). En console (un lecteur + racine par instruction), les variables locales (déclarées par -l/u) + ont la portée de l'instruction en cours. Hors console, elles ont + la portée du bloc {} courant (cas des commandes if, while, for, + eval, list, dict...). + L'option -u= fonctionne comme -l, avec liaison de la var. aux + lecteurs parents, à raison de n niveaux de remontées (ainsi + l'option -u0 correspond simplement à -l). + Pour les var. locales (-l/u) déclarées par ARG et préfixées par + '.' il est toléré de les modifier à nouveau par DEF -l/u. + Sans affectation, -l liste les variables appartenant au lecteur + indiqué. Exemples : + eval {DEF -u=1 a 1; DEF -l b 2}; DEF -l a|b --> seule a existe + DEF f {arg .a; DEF -u=1 .a 2}; f 1 --> .a vaut 2 + + DEF sert typiquement aussi pour définir les alias et les macros, + bien que pour les macros il soit préférable d'utiliser defmacro. + - Un alias pourra être passé en commande et sa valeur remplacera le + nom de commande dans la ligne d'appel, comme un raccourci de + commande (requiert :ALIAS_ARG à 1). + Ex. DEF sel "select * from"; sel table1; + L'alias est déclaré par une chaîne "". + - Une macro pourra être passée en commande mais les arguments passés + seront accessibles uniquement par var. locales, et le code pourra + éventuellement retourner une valeur par RETURN (requiert :ALIAS_ARG + à 0 pour éviter l'ajout des arguments). + Référencement des arguments : + - par arguments d'exécution + $(arg1), $(arg2)... : Arguments éventuels passés en paramètre + $(arg_ls) : Liste des arguments + $(arg_nb) : Nombre d'arguments + ou $arg1, $arg2, $arg_ls, $arg_nb... + - par arguments de commande (paramètres de ligne précédente) + $(0) nom de la macro ; $(1), $(2)..., $(_l) la liste ; et $(_n) + le nombre. (Il est possible d'utiliser la syntaxe $1, $_l...). + Note : en déclaration de fonction, ces références doivent être + situées en 1ère ligne car toute commande intermédiaire les + écrase. Ex: DEF f {void; print $(1)}; f 123 --> erreur + Ex. DEF sel { select * from $(arg1) }; sel table1; + La macro est déclarée par une chaîne "" ou {} (substitutions + immédiates avec "" et différée avec {}). + Avec l'option -h, ajoute à la variable globale (alias/macro) une + chaîne d'aide accessible par la commande help. + + Une fois définis, alias et macros sont appelables comme commandes. + Un alias peut "aliaser" une commande interne (ou plugin) ou une + macro, mais le préfixe ':' n'est nécessaire que pour la commande. + Ex : DEF f,g g {print $arg1} -- f : alias de g, g : macro + f 1 -- pas de préfixe ':' car f est un alias de g, une macro + DEF f print -- f est un alias de print, une commande interne + :f 1 -- préfixe ':' nécessaire si :ALIAS_ARG à 0 + Par exemple, defm est un alias de la macro defmacro. + Notes : + - avec la syntaxe d'alias :f, ses arguments sont évalués deux + fois (passage supplémentaire pour :f) + - une alias peut aussi être appelé en tant que référence, auquel + cas il remplace seulement la commande correspondante + Ex. DEF f print; $f 1 2 3 + + Positionnez :ALLOW_RECUR à 1 pour les macros récursives (boucles), + mais attention à ce que vous faites ! Si :ALLOW_RECUR est à 0, un + contrôle de référence circulaire est fait au lancement. Par contre, + quelque soit :ALLOW_RECUR, si l'alias/macro est préfixé par "*", + alors le contrôle n'est pas effectué, et la récursion est permise + pour la macro et son exécution (portées filles). + Il y a donc 3 moyens de désactiver le contrôle de réf. circ. : + - positionnement de :ALLOW_RECUR à 1 en amont dans la portée + - déclaration de la macro par DEF -r (pour les var. globales) + - appel de la macro en la préfixant par "*" + + Si l'option :ALLOW_REDEF est positionnée à 1, alors un alias ou une + macro peut prendre le nom d'une commande ou d'un plugin. Attention + dans ce cas, pour utiliser une commande ou un plugin du même nom, il + faut préfixer la commande par '=' (sinon la commande est cachée par + l'alias, et l'exécution boucle). + Ex : DEF edit {print -e "ne pas appeler de commande graphique!"} + edit --> commande edit non exécutée, et message + DEF print { =print "args: $arg_ls" } -- notez le préfixe = + print toto --> args: toto + Note : le masquage des commandes est à vos risques et périls ! + + Autres usages de DEF : + DEF s + Affiche la valeur de la variable x sans aucune substitution + Recherche les var. (globales et locales) dont le nom correspond + à l'expr. régulière s et les affiche. Liste aussi les variables + locales (variables de lecteur) en les marquant par un '*'. + Avec l'option -l, la liste est limitée aux variables locales. + Attribue à _RET_VALUE la valeur de la dernière variable trouvée. + L'expression régulière s peut être encadrée par ` et `. + Exemples : DEF `ab.*` Affiche les var. commençant par 'ab' + DEF a|b|c Affiche les var. a,b et c si existent + Si une seule variable est trouvée, retour de la valeur seul. + + DEF -d=n + DEF -y=n + Avec d, détermine si la liste de var. utilisateur n sont toutes + définies, et retourne 1 si oui, 0 sinon. Avec y, teste en plus si + les var. sont toutes non vides. + Exemples : DEF a,b,d 1 2 "" + DEF -d=a,b -- ou DEF -d=a -d=b --> 1 + DEF -d=a,b,c --> 0 + DEF -y=a,b,d --> 0 + DEF -e=v + Évalue les commandes contenues en variable v + Note : l'option -e ne supporte pas le passage de paramètres + DEF -c + Met aussi à jour la liste de complètement en console. + + Quelques autres exemples simples : + DEF Affiche les valeurs de toutes les variables globales + DEF x "" Affecte la chaîne vide à x + DEF v,w "chaîne pour v" "chaîne pour w" -- affectation multiple + DEF x,y,z ~$[get-coord-3d] -- affectation multiple depuis macro + -- notez le ~ pour dissocier les 3 retours de get-coord-3d + DEF f {print "lorem ipsum"} -h "Affiche du texte"; help f + if $[DEF -d x] {print "'x' est définie !"} + DEF f,g g f -- si appel en macro, référence circulaire ! + + Notes : + - lors de la déclaration de macros par {}, attention à ne pas + insérer ou cacher un { ou } dans des commentaires en bloc : + Ex. DEF f {print a /* { */ b} -- /* et */ sont en bloc + Cet ex. appelle une autre ligne car /* et */ ne sont pas vus + en tant que commentaires au sein d'une chaîne {} + Une solution : DEF f "print a /* { */ b" -- ok + - il y a une différence entre $[def x], $[def -l x] et $x : + $[def x] : résolution de x globalement puis localement + $[def -l x] : résolution de x uniquement localement + $x : résolution de x localement puis globalement + Exemples : + eval { + DEF -l a foo; DEF a bar + print $[DEF a] --> bar (priorité au global) + print $[DEF -l a] --> foo (local seul) + print $a --> foo (priorité au local) + - les alias et macros sont appelés *après* les greffons, eux-mêmes + appelés après les commandes internes. + - les alias et macros sont sensibles à la casse majuscule/ + minuscule (ce n'est pas le cas des commandes). + - pour la déclaration d'alias ou de macros globales, utilisez + de préférence defmacro (ou defm). + + Exemples de déclaration d'une macro : + -- 1) Pas de nettoyage, substitutions différées à l'appel + DEF nom_fonc { -- peut s'étendre sur plusieurs lignes + cmd1 arg1 $(1) $(2) + cmd1 arg1 $(arg1) -- substitutions non échappées + cmd1 $(CONST) -- différée également + cmd2 arg2 ; cmd3 arg3 -- les ';' sont protégés + cmd4 arg4 -- et ainsi de suite... + } + -- 2) Pas de nettoyage, substitutions immédiates (= chaîne) + DEF nom_fonc " -- peut s'étendre sur plusieurs lignes + cmd1 arg1 ^$(1) ^$(2) + cmd1 arg1 ^$(arg1) -- échapper les substitutions à différer + cmd1 $(CONST) -- substitution immédiate (doit exister) + cmd2 arg2 ; cmd3 arg3 -- les ';' sont protégés + cmd4 arg4 -- et ainsi de suite... + " + + Déclaration de variables avec résolution paresseuse (lazy eval): + Il est possible de déclarer une expression (coûteuse) mais de faire + qu'elle soit évaluée seulement si besoin, en stoquant son résultat. + Ex: def e {/* évaluation lourde */ 1*2*3*4*5*6*7*8*9*10} + print $(e#!eval) -- évalué et réassigné avec le résultat + dict get {a=42} a ^$(e#!eval) -- non évalué + + Définition de modules (code factorisable entre macros) depuis DEF : + -- définition du module + def module1 { + def -u1 m1-macro1 {print "en macro 1 du module 1"} + def -u1 m1-macro2 {print "en macro 2 du module 1"} + } + -- utilisation du module 1 en fonction quelconque + def f { + module1 -- définition locale de m1-macro1 et m1-macro2 + m1-macro1 + m1-macro2 + } + + Comparatif des syntaxes au cours des versions : + -- 3.8 + var func (:var l ^$(_l)^; + when !^$(l#empty)^; print ^$(l@0)^; end^; unvar l^;:) + -- 3.9 + var func (: if ^$(arg_nb) (:print ^^$(arg1)^; :):) + -- 4.0, 4.1 + DEF func !! if $(arg_nb) {: print ^$(arg1); :}!! + -- 4.2 + DEF func { if $(arg_nb) { print $(arg1) } } + -- 4.4 + DEF func { if $(arg_nb) { print $(arg1) } } -- ou bien + DEF func { arg [a1]; if $(a1#len) { print $a1 } } + + Attribue la valeur de la variable fixée/listée à _RET_VALUE. diff --git a/src/lunasql/doc/cmddelete.txt b/src/lunasql/doc/cmddelete.txt new file mode 100644 index 0000000..4fe24d8 --- /dev/null +++ b/src/lunasql/doc/cmddelete.txt @@ -0,0 +1,4 @@ + DELETE : Supprime des données de la base + DELETE FROM table [WHERE Expression] + + Attribue le nombre de lignes insérées à _RET_VALUE et à _RET_NLINES diff --git a/src/lunasql/doc/cmddict.txt b/src/lunasql/doc/cmddict.txt new file mode 100644 index 0000000..b9026f7 --- /dev/null +++ b/src/lunasql/doc/cmddict.txt @@ -0,0 +1,96 @@ + DICT : Outils de gestion de dictionnaires de chaînes + DICT commande dictionnaire [params] + Le dictionnaire peut être encadré par "", [] ou {}. Mais il vaut + mieux préférer l'usage de {} (sauf en cas de substitutions en + dictionnaire). Le format supporté est Properties, cf doc + http://docs.oracle.com/javase/8/docs/api/java/util/Properties.html + Les paires chef=valeur sont séparables par un saut de ligne ou + bien // (échapable par \/\/). + + Commandes (insensibles à la casse) et valeurs de retour : + - NEW : crée un nouveau dictionnaire et le retourne + Ex: DICT new a do b ré c "mi fa sol" + - GET : -> valeur correspondant à la clef fournie, ou défaut + La valeur par défaut n'est évaluée que si clef non trouvée + Ex: DICT get {a:1// b:2} b --> 2 (*) + DICT get {a:1// b:2} c --> (vide) + DICT get {a:1// b:2} c 3 --> 3 + DICT get {a:1// b:2} b ^$[6*3]--> 2, 6*3 non calculé + - HAS-KEY? : -> 1 si le dict. contient la clef donnée, 0 sinon + - HAS-VAL? : -> 1 si le dict. contient la valeur donnée, 0 sinon + - EMPTY? : -> 1 si le dictionnaire est vide, 0 sinon + - SIZE : -> nombre de clefs dans le dictionnaire + - PUT : ajoute un couple clef-valeur au dictionnaire + Ex: DICT put $dict key1 value --> $dict pas modifié + DICT put "" a 123 --> création d'un nouveau dict. + - REMOVE : supprime un couple clef-valeur du dictionnaire + - MERGE : fusionne deux dictionnaires et retourne le résultat + -> (les clefs en doublon sont mises à jour par le 2ème) + Utile pour les macros à arguments nommés (DICT) par défaut + Ex: DICT merge {a:do//b:sol//d:fa} {a:ré//c:mi} + --> {a=ré//b=sol//c=mi//d=fa} + - KEYS : -> liste des clefs dans le dictionnaire + - VALUES : -> liste des valeurs dans le dictionnaire + - LOCAL : ajoute le dictionnaire à l'environnement local. Si une + liste (sép ',') de clefs est fournie, filtre l'ajout de var. + Ex: DICT local {a:do//b:ré} --> ajoute les var. a,b + DICT local {a:do//b:ré} a --> ajoute la var. a (pas b) + DICT local $[seek select * from books where id = 42] + - EACH : applique une macro à chaque couple, référencé par + $arg1 (clef) et $arg2 (valeur) dans la fonction, et + retourne le nombre d'itérations. + Ex: DICT each {1:do//2:sol} {print $arg1=>$arg2} + DICT each {1:do//2:sol} {arg k v; print $k=>$v} + - APPLY : applique une fonction à chaque couple, référencé par + $arg1 (clef) et $arg2 (valeur) dans la fonction, et retourne + un dictionnaire. Modifier explicitement la référence. + Ex: DICT apply {1:do//2:ré} {def -l arg2 $[str upper $arg2]} + DICT apply {1:do} {arg k v; def -l arg2 $[str upper $v]} + - FILTER : applique une fonction booléenne filtre à chaque couple, + référencé par $arg1 (key) et $arg2 (val) dans la fonction et + retourne un dictionnaire. + Ex: DICT filter {1:do//2:sol} {$[str len $arg2] > 2} + DICT filter {1:do//2:sol} {arg k v; $[str len $v] > 2} + Note : pour APPLY et FILTER, les var. locales déclarées en + boucle sont accessibles pour tous les tours de la boucle. + Notes pour APPLY, FILTER et EACH : + - les var. locales déclarées en boucle sont accessibles pour tous + les tours de la boucle + - la commande ARG est candidate (utilise les arguments argX). + Ex : def f { arg k v; print $k => $v }; DICT apply {a=42} f + - ANY? : cf. filter, mais retourne 1 si au moins un élément est vrai + Ex: DICT any? {1:do//2:sol} {$[str len $arg2] > 2} --> 1 + - ALL? : cf. filter, mais retourne 1 si aucun élément n'est faux + Ex: DICT all? {1:do//2:sol} {$[str len $arg2] > 2} --> 0 + + Note : (*) : '//' (séparateur de paires) est utile pour des commandes + sur une ligne et pour l'usage avec l'option :END_CMD_NL à 1 et avec + cleansql (ex. defmacro), mais en général il est préférable surtout + en script de conserver le format de Properties : + def opts { + # Comment + key1 = value 1 + key2 = value 2 + } -- au lieu de : def opts {key1 = value 1//key2 = value 2} + + Exemple d'utilisation typique : + -- Recherche de valeur de fruits + def fruits2 { + pomme=1 + banane=2 + poire=3 + fraise=4 + } + print "valeur de fraise : $(fruits2,fraise)" + + Exemple d'utilisation pour des arguments nommés par défaut: + defmacro func {[args]} { + -- args est un dictionnaire aux clefs optionnelles + def opts $[DICT merge $[DICT new a 10 b 11 c 12] $args] + print "a=$(opts,a), b=$(opts,b), c=$(opts,c)" + } + func {a=42} --> a=42, b=11, c=12 + + Note : les opérations fonctionnent sur les valeurs et non sur les + références. Il n'y a pas de modification de variable. + Attribue à _RET_VALUE la valeur générée par la sous commande diff --git a/src/lunasql/doc/cmddisp.txt b/src/lunasql/doc/cmddisp.txt new file mode 100644 index 0000000..1d9065e --- /dev/null +++ b/src/lunasql/doc/cmddisp.txt @@ -0,0 +1,9 @@ + DISP (\) : Affiche le contenu de la (ou les) table(s) en paramètre + DISP nom_table1 | nom_vue1 | pattern1 ... + DISP pattern Affiche le contenu des tables dont le nom correspond + à l'expression régulière pattern + L'expr. régulière pattern peut être encadrée par ` et `. + Exemple : DISP tb.* -- tables commençant par 'tb' + + Attribue la 1re valeur de la 1re ligne de la 1re table à _RET_VALUE + et le nombre de lignes total à la constante _RET_NLINES. diff --git a/src/lunasql/doc/cmddrop.txt b/src/lunasql/doc/cmddrop.txt new file mode 100644 index 0000000..98ae669 --- /dev/null +++ b/src/lunasql/doc/cmddrop.txt @@ -0,0 +1,19 @@ + DROP : Supprime un objet de la base + - Index : + DROP INDEX [IF EXISTS] + - Rôle : + DROP ROLE + - Séquence : + DROP SEQUENCE [IF EXISTS] [RESTRICT | CASCADE] + - Schéma : + DROP SCHEMA [RESTRICT | CASCADE] + - Table : + DROP TABLE

[IF EXISTS] [RESTRICT | CASCADE] + - Trigger : + DROP TRIGGER + - User : + DROP USER + - View : + DROP VIEW [IF EXISTS] [RESTRICT | CASCADE] + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES diff --git a/src/lunasql/doc/cmdedit.txt b/src/lunasql/doc/cmdedit.txt new file mode 100644 index 0000000..3388cd5 --- /dev/null +++ b/src/lunasql/doc/cmdedit.txt @@ -0,0 +1,76 @@ + EDIT : Ouvre un éditeur de script Java Swing + EDIT édite le fichier par l'éditeur Swing interne. + Si JSyntaxPane est dans le CLASSPATH, il est utilisé avec + comme type l'extension du fichier (ou l'option -t). + EDIT -d : ouvre avec l'éditeur par défaut du système + (si le type de fichier est associé à une action) + EDIT -p : ouvre avec l'éditeur configuré en :EDITOR_PATH + + L'éditeur par défaut (sans option -d ni -p) est un éditeur Swing très + simple mais bien adapté aux scripts et macros LunaSQL. + Il se compose de trois compartiments : dans le volet de gauche, la + liste des tables de la base de données, et la liste des variables + LunaSQL, et le panneau central qui contient l'éditeur proprement dit. + Si la bibliothèque JSyntaxPane est dans la CLASSPATH, l'éditeur se + bonifie de la coloration synaxique, des numéros de lignes, d'un + menu contextuel complet pour le développement et de fonctions de + recherche avancées. + Notes : - la version courante de JSyntaxPane (1.1.5) fonctionne sur + Java 8 mais mal sur Java 11, + - en mode HTTP, cette commande n'est pas autorisée. + + La liste des tables permet des opérations sur les tables de la base + (par clic-droit sur un nom de table) : + - insérer le nom à la position du curseur + - interrogation du nombre de lignes de la table (attention !) + - interrogation de la structure de la table (colonnes) + - interrogation des relations filles de "foreign key" + - prévisualisation du contenu de la table + - actualiser la liste des tables + Sous-menu SQL : + - insertion à la position du curseur d'une requête SELECT, INSERT, + UPDATE, DELETE, CEATE, DROP + Sous-menu Données : + - vider la table (commande TRUNCATE) (attention !) + - importer les données de la table (commande IMPORT) + - exporter les données de la table (commande EXPORT) + + La liste des variables permet des opérations sur les variables/macros + de l'environnement global de LunaSQL (par clic-droit sur un nom) : + - insérer au curseur le nom de la variable, ex: x + - insérer au curseur la référence de la variable, ex: $(x) + - insérer au curseur la valeur de la variable, ex: 42 + - inspecter la variable, c'est-à-dire afficher son contenu + - voir la documentation de la variable (commande HELP) + - ajouter une variable + - modifier une variable + - dupliquer une variable + - supprimer une variable + - actualiser la liste des variables + Cocher la case "Var. système" ajoute à la liste aussi les variables + système (commençant par '_' ou ':'). + + Le menu permet en outre des actions supplémentaires : + - Fichier : opérations sur le contenu de l'éditeur (ouvrir un + fichier, sauver, quitter l'éditeur) + - Édition : opérations d'édition classiques (annuler, rétablir, + copier, couper, coller, tout sélectionner) + - Script : changer de syntaxe de coloration, changer de moteur + d'exécution, exécuter l'intégralité du contenu de l'éditeur, + signer et vérifier la signature d'un script + - Code : opérations sur le code (compléter le mot sélectionné, + évaluer l'expression sélectionnée, inspecter la variable + sélectionnée, voir la documentation de la var. sélectionnée, + ouvrir le navigateur pour la doc. SQL) + Le complètement de mots fonctionne sur un mot-clef sélectionné + parmi les mots suivants : create, insert, select, update, delete, + if, when, case, for, while, eval, def, defm, defmacro, defr, + defrecord, with, Lorem. L'évaluation insère au curseur la valeur + retournée par l'expression sélectionnée. + Ex: "rand -m=uuid", "formatDate()", "select count(*) from test" + - Greffons : liste des greffons (ce sont des scripts LunaSQL ou SE + pris en charge, selon bibliothèque SE en CLASSPATH, et présents + dans le premier répertoire de la variable :SCRIPTS_PATH, ou si + vide, dans le répertoire courant). + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdengine.txt b/src/lunasql/doc/cmdengine.txt new file mode 100644 index 0000000..8cbde68 --- /dev/null +++ b/src/lunasql/doc/cmdengine.txt @@ -0,0 +1,93 @@ + ENGINE (se) : Liste ou attribue au systême un moteur d'évaluation + ENGINE Affiche le moteur actuel + ENGINE -l Affiche tous les moteurs disponibles (selon CLASSPATH) + ENGINE eng Fixe le nouveau moteur d'évaluation à eng + eng peut être le code du moteur, ou un de ses alias. + Variables java disponibles depuis le moteur : + - eval_engine (javax.script.ScriptEngine) : moteur d'évaluation SE + - connection (lunasql.lib.Contexte) : contexte d'exécution LunaSQL + Note : lors du passage d'un moteur à un autre, le contexte + d'exécution du moteur JavaScript est sauvegardé, mais les autres + sont volatiles (ni sauvegardés ni transférés). + + En plus des fonctions du langage SE, LunaSQL ajoute les objets Java + suivants (les exemples sont fournis avec la syntaxe de Javascript) : + + - lunasql (lunasql.lib.Coquille) : moteur d'évaluation LunaSQL + println(String s), errprintln(String s), eval(String s), + getVarOpt(String key), setVar(String key, String val), + setOpt(String key, String val), isVarOptSet(String key), + getVarHelp(String key) + + les fonctions SQL suivantes : + seek(sql,enc,rmax), seekNbl(tbl), execute(sql), getResultset(sql), + getResultArray(sql) List[][], getMetaData(), getConnex() + Exemple d'usage : + // SELECT + nbl = lunasql.seekNbl("TABLE NAME [+WHERE]") + val = lunasql.seek("SELECT COMMAND", "UTF-8") // 1ère cellule + rs = lunasql.getResultset("SELECT COMMAND") + // objet java.sql.ResultSet + ar = lunasql.getResultArray("SELECT COMMAND") + // objet java.util.ArrayList> + tb = lunasql.getResultString("SELECT ...", + mode, // true: tabulaire, false: linéaire + maxcl, // largeur max des colonnes (int) + maxlg, // nombre max de ligne à retourner(int) + rowno // affichage du num de ligne (boolean) + ) // retourne objet String[] à 3 valeurs : + // 0: résultat complet, 1: première valeur, 2: nb de lignes + // UPDATE + nbl = lunasql.execute("UPDATE COMMAND") + // Autres + lunasql.getMetaData() // objet java.sql.DatabaseMetaData + + - engine (javax.script.ScriptEngine) : moteur d'évaluation SE + docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngine.html + avec entre autres les fonctions : + eval(java.io.Reader flux), eval(String script), + get(String key), put(String key, Object value) + + Attribue le nom de l'engine fixé à _RET_VALUE, la valeur du SE + actuel si aucun paramètre, ou le nombre en cas de liste. + + Quelques liens et exemples de langages de script sur JVM : + + - Javascript (moteur par défaut inclus en distro JRE jusque 11) + https://developer.mozilla.org/en-US/docs/JavaScript/Guide + https://developer.mozilla.org/en-US/docs/JavaScript/Reference + Exemple : + var sql = "select code, valeur, actif from cfgappli" + var stmt = sql_connex.prepareStatement(sql) // sql_connex + var rs = stmt.executeQuery() + while(rs.next()) { + print(rs.getString(1)) + } + rs.close() + + - JRuby (l'énorme) + http://www.ruby-lang.org/fr/documentation/ (doc. en français !) + http://ruby-doc.org/ + http://jruby.org/documentation + Exemple : + return -1 if $sql_connex.nil? + sql = "select code, valeur, actif from cfgappli" + stmt = $sql_connex.prepare_statement(sql) # $sql_connex + rs = stmt.execute_query + while rs.next + puts rs.get_string(1) + end + rs.close + + Autres SE : + - Beanshell (c'est du Java) + http://www.beanshell.org/manual/contents.html + http://docs.oracle.com/javase/7/docs/api/ + - Jython + http://docs.python.org/3/ + http://www.jython.org/docs/ + - Groovy + http://groovy.codehaus.org/Documentation + - Clojure + http://clojure.org/documentation + - Renjin (R) + http://docs.renjin.org/en/latest/ diff --git a/src/lunasql/doc/cmderror.txt b/src/lunasql/doc/cmderror.txt new file mode 100644 index 0000000..d15b2af --- /dev/null +++ b/src/lunasql/doc/cmderror.txt @@ -0,0 +1,6 @@ + ERROR : Lève une erreur d'exécution + ERROR [msg] Lève une erreur avec le message msg + Cette erreur peut être gérée par :EXIT_ON_ERR et :ON_ERROR, et peut + être attrapée par EVAL -e/-c. + + Attribue 0 à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdeval.txt b/src/lunasql/doc/cmdeval.txt new file mode 100644 index 0000000..f0b599e --- /dev/null +++ b/src/lunasql/doc/cmdeval.txt @@ -0,0 +1,58 @@ + EVAL (=) : Évalue une commande LunaSQL ou une expression SE en + espace confiné et la retourne + EVAL cmd évalue la commande cmd et retourne son resultat + Si option -t : affiche le temps d'exécution. + Si option -v : accepte un dictionnaire de variables locales qui peut + être encadré par "", [] ou {} (cf. doc de la commande DICT). + + Si options -c= [-n=] : attrape l'erreur d'exécution si elle est + produite par une commande listée par le filtre f (ou si l'option n + est absente), puis exécute le code c (c et f peuvent être des chaînes + vides). Le filtre f est une liste de noms de commandes (ou macros) + pour lesquelles l'erreur doit être rattrapée. + Avec l'option -f= : bloc "finally" à exécuter dans tous les cas. + En bloc -c, les variables locales suivantes sont disponibles : + - err_lng : numéro de ligne de l'erreur + - err_cmd : commande ayant lancé l'erreur : + - nom en majuscules si commande interne ou un plugin + - nom identique si macro ou alias en :ALIAS_ARG 1 + - "ALIAS" si alias appelé par préfixe : + - expression complète si expression SE + - err_msg : message d'erreur complet + - err_stk : pile d'appels + Les variables locales déclarées en bloc principal sont accessibles + en bloc -c ("catch") et -f ("finally"). + + Exemples : + EVAL frm select * from test -- avoir chargé 'frm' avant + EVAL size TABLE1 --> 42 + EVAL {def -l a; print $a} -- $a est locale à EVAL + def PI $[EVAL 4*Math.atan(1)] --> 3.141592653589793 + -- bien que pour du calcul let soit à préférer + EVAL -v [a=42//b=54] {put $a} --> 42 + + EVAL sert aussi à exécuter du code en espace restreint : + EVAL {def -l a foo; print $a} -- a est confinée + EVAL {print $noexist} -c={print "Erreur: $err_msg"} + EVAL {risky} -c my-catch-macro -f my-finally-macro + EVAL {risky} -c "" -- erreur "ignorée" + -- erreur ici attrapée car commande def présente en filtre + EVAL {def noexist} -c={print "Commande: $err_cmd"} -n=def,opt + -- maintenant la commande à filtrer est une macro + def f {error "erreur en f"} -- la commande est une macro + EVAL f -c={print "attrapée"} -n=f + -- les blocs EVAL sont imbricables + EVAL {risky} -c { eval { def $noexist } -c={/* exécuté */} } + + Notes : + - L'évaluation normale d'une commande est réalisée par $[] + - Avec l'option -c (gestion d'erreur), en cas d'erreur le reste du + bloc est exécuté en fonction de la valeur de :EXIT_ON_ERR. + :ON_ERROR n'est pas évaluée si l'erreur est rattrapée par -c + - En bloc -c, une déclaration par def -u doit prendre en compte + une profondeur +1 (et encore +1 si eval exécute une macro) + + Ne modifie pas _RET_VALUE, car est modifiée par le bloc exécuté ou + par le bloc -c en cas d'erreur, selon :EXIT_ON_ERR (la dernière + expression évaluée est retournée). Avec l'option -c, attribue 0 à + _CMD_STATE diff --git a/src/lunasql/doc/cmdexec.txt b/src/lunasql/doc/cmdexec.txt new file mode 100644 index 0000000..5d51aa3 --- /dev/null +++ b/src/lunasql/doc/cmdexec.txt @@ -0,0 +1,102 @@ + EXEC (@) : Exécute le fichier de commandes SQL/SE en paramètre + Le choix SQL / ScriptEngine est fait sur l'extension ou du symbole + du premier argument ('-' : LunaSQL, '+[engine]' : SE). + Les scripts sont recherchés dans les répertoires de l'option + :SCRIPTS_PATH, ou bien renseignés en chemins absolus. Si la + politique de sécurité est activée (option :SIGN_POLICY à 1 ou 2), + les scripts doivent être signés numériquement (commande SIGN en + bibliothèque 'crypt'). La signature externe est d'abord recherchée + (extension .sig au fichier) puis, si absente, interne. + + Sont supportés : + - les fichiers scripts LunaSQL et SE (JS ou si bibliothèque en CP) + présents en répertoire :SCRIPTS_PATH, avec signature interne ou + non (selon :SIGN_POLICY). Notez que les signatures internes sont + encadrées par commentaires SQL/JS (/*...*/), donc ne sont pas + adaptées aux SE utilisant une autre syntaxe de commentaires. + - les adresses en réseau en http(s)://, ftp:// et file:// + - les adresses de ressources préfixées par 'jar:' + - les fichiers zippés .gz (ex. : extension .sql.gz), dont ceux + dont le fichier texte contient une signature interne + - les fichiers chiffrés .cry (ex. : extension .sql.cry), dont ceux + générés par les commandes CRYPT et EXPORT (sql), mais la + signature doit être externe (pas de lecture de signature interne + en fichier chiffré). La clef de déchiffrement (128/256 bits) doit + être le premier argument, au format hexadécimal. + + Attention aux références de script circulaires ! + + - Cas SQL (extensions .sql, .lsql et .luna) : + EXEC nom_script1.sql param1 param2 ... + Exemples : + EXEC /chemin/vers/script.sql -- chemin absolu + EXEC prog.luna -- recherche en :SCRIPTS_PATH + EXEC compressed.sql.gz -- le script est compressé + EXEC encrypted.sql.cry 1234...CDEF -- le script est chiffré + EXEC file:///Users/scripts/test.sql param1 param2 ... + EXEC https://domain.fr/scripts/test.sql param1 param2 ... + EXEC jar:/lunasql/misc/test.sql param1 param2 ... + -- notation '-' pour exécution des arguments de la commande + EXEC - "cmd1; cmd2; cmd3 ..." param1 param2... + Exemple : + EXEC - "print ^$(arg1)" 56 toto --> 56 + + En fichier, il est pratique de déclarer ainsi des macros. Le code + multiligne est alors encadré par "" ou mieux {} car alors les + substitutions sont différées. En outre pour la déclaration de + macros en production (bibliothèque de macros), utiliser plûtot la + commande USE, qui gère les dépendances (chargement unique). + + Variables disponibles (en plus des var. de portée) : + script_arg1, script_arg2... : Arguments éventuels passés + script_arg_ls : Liste des arguments + script_arg_nb : Nombre d'arguments + script_path : Chemin absolu du fichier script + script_name : Nom du fichier script + this_scope : Nom du fichier script + Ces variables sont bien sûr locales au Lecteur en cours, ou portée + (par exemple la fonction ou le script). + Exemple : + def info-user { -- déclaration d'une fonction en script + print "Infos sur $(arg1)" -- pas d'échappement du $ car en {} + select Code, NomPrenom "Nom", Fonction ^ + from Utilisateurs where Code='$(arg1)' + } + -- mieux : + defmacro info-user {user} { -- idem + print "Infos sur $(user)" -- pas d'échappement du $ car {} + select Code, NomPrenom "Nom", Fonction ^ + from Utilisateurs where Code='$(user)' + } + + Codage des caractères en fichier de script : + Le codage des fichiers par défaut est "UTF-8", mais peut être + spécifié en option :ENCODING. À noter que la BOM (Byte Order Mark, + ou séquence EF BB BF en début de fichier) n'est pas supportée. + + - Cas ScriptEngine (SE, toutes autres extensions) : + La bibliothèque doit être présente en CLASSPATH et membre de SE + EXEC nom_script param1 param2 ... + Exemples : + EXEC script2.rb param1 param2 -- SE JRuby + EXEC /chemin/vers/script.js -- chemin absolu + EXEC prog.js -- recherche en :SCRIPTS_PATH + EXEC compressed.js.gz -- le script est compressé + EXEC encrypted.js.cry 1234...CDEF -- le script est chiffré + -- notation '+' pour exécution des arguments de la commande + EXEC + "cmd1; cmd2; cmd3 ..." param1 param2... + EXEC +engine "cmd1_engine" param1 param2... + Exemples : EXEC + "print(scr_args.get(0)+'\n')" 56 toto -- 56 + EXEC +beanshell {fonction_bsh();} + EXEC +Clojure { (println "n =" (+ 5 2)) } + Moteur par défaut : js (selon constante _EVAL_ENGINE) + Variables disponibles : + sql_connex : (SQLCnx) Connexion à la base de données + script_args : (ArrayList) Arguments en commande + script_path : (String) Chemin absolu du fichier script + script_name : (String) Nom du fichier script + + Voir l'aide de la commande ENGINE pour des exemples plus complets. + Doc. Javascript : https://developer.mozilla.org/fr/docs/JavaScript + + Attribue la valeur de RETURN du script à la constante _RET_VALUE diff --git a/src/lunasql/doc/cmdexit.txt b/src/lunasql/doc/cmdexit.txt new file mode 100644 index 0000000..152f71e --- /dev/null +++ b/src/lunasql/doc/cmdexit.txt @@ -0,0 +1,10 @@ + EXIT : Sort des fichiers de commandes (ou les alias) SQL en + remontant toute la pile d'appel. + EXIT Sort normalement (équivalent à EXIT 0) + EXIT e Sort avec le code d'erreur positionné à e (nb entier) + EXIT e msg Sort avec le code d'erreur positionné à e en affichant + le message msg + Quelque soit le code d'erreur, cette interruption n'est pas gérée par + :EXIT_ON_ERR et :ON_ERROR. Cf. commande ERROR. + + Attribue à la constante _RET_VALUE le code d'erreur diff --git a/src/lunasql/doc/cmdexplain.txt b/src/lunasql/doc/cmdexplain.txt new file mode 100644 index 0000000..17f7ead --- /dev/null +++ b/src/lunasql/doc/cmdexplain.txt @@ -0,0 +1,3 @@ + EXPLAIN : Détaille l'exécution d'une requête par le SGBD + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES diff --git a/src/lunasql/doc/cmdexport.txt b/src/lunasql/doc/cmdexport.txt new file mode 100644 index 0000000..961e9b4 --- /dev/null +++ b/src/lunasql/doc/cmdexport.txt @@ -0,0 +1,44 @@ + EXPORT (EX) : Crée un fichier d'import de la table/req. en paramètre + EXPORT fichier table|requête + EXPORT fichier.ext table1 + EXPORT fichier.ext ^ + select col1,col2 from nom_table1 where col1>0 orber by machin + EXPORT nom_fichier.ext + Sans table/requête renseignée, le nom de fichier est choisi. + Les fichiers zippés (*.sql.gz) ou chiffrés (*.sql.cry) sont + supportés. Si extension cry (mode chiffrement), la clef est fournie + par l'option -k et peut faire 128 ou 256 bits (seuls les 128 ou 256 + premiers bits (forts) sont retenus pour la clef). Le message chiffré + est d'abord compressé par GZip puis chiffré par l'algo AES 128/256 + (selon la clef fournie) en mode CBC/PKCS5Padding. + + Formats acceptés : SQL, CSV, TXT, HTML, XML (selon extension). + En cas d'export d'une requête en fichier SQL, la table doit être un + nom unique coincé obligatoirement sans alias entre FROM et WHERE. + + EXPORT -c table|requête : exporte en format TXT dans le presse-papier + EXPORT -f s -e e : recherche les tables dont le nom correspond à exp. + régulière s et les exporte chacune en fichier correspondant + avec l'extension e (par défaut: csv). + L'expression s peut être encadrée par ``. + EXPORT -n : les valeurs NULL sont représentées par la chaîne "" + pour pouvoir être importées en valeurs nulles par IMPORT + EXPORT -o opts : exporte selon les options de format CSV spécifiées + opts : quote_char,sep_char,escape_char + Les 3 options doivent obligatoirement être spécifiées + Options par défaut : ",\ + + Exemples : EXPORT test.txt test -- export de la table test + EXPORT path/to/test.txt -- nom de fichier seul + EXPORT -f `.*_TB` -e csv.gz + EXPORT -f `.*_TB` -o ^",\ -- options par défaut + EXPORT -c select id,lib from test where id<10 + EXPORT test.csv.cry TB_TEST -k=$[hash clefsecrete] + Notes : + - en cas d'exportation en SQL, le nom de la table retenu est ce qui + se trouve en FROM et WHERE. Il faut un WHERE (fût-ce true ou 1=1). + - selon les SGBD, il est possible d'exporter facilement du CSV. + Ex. H2 : CALL CSVWRITE('fichier.csv', 'SELECT * FROM TEST'); + + Attribue le nombre de lignes du dernier export à _RET_NLINES + et le nombre total à _RET_VALUE diff --git a/src/lunasql/doc/cmdfile.txt b/src/lunasql/doc/cmdfile.txt new file mode 100644 index 0000000..a595623 --- /dev/null +++ b/src/lunasql/doc/cmdfile.txt @@ -0,0 +1,100 @@ + FILE : Outils de gestion de fichier et de chemins de fichiers. + FILE commande fichier [params] + + Commandes (insensibles à la casse) et valeurs de retour : + - EXISTS? : -> 1 si le fichier existe, 0 sinon + - FILE? : -> 1 si le fichier existe et est un fichier, 0 sinon + - DIR? : -> 1 si le fichier existe et est un rép., 0 sinon + - ABSOL? : -> 1 si le nom est absolu, 0 sinon + - ABSOL : -> nom absolu du fichier + Ex. FILE absol ./config.sql --> /home/.../config.sql + - NAME : -> nom relatif court du fichier + Ex. FILE name ./config.sql --> config.sql + - PWD : -> nom absolu du répertoire courant + - PARENT : -> répertoire parent du fichier + Ex. FILE name ./config.sql --> . + - SIZE : -> taille du fichier (en octets) + - COUNT : -> nombre de lignes (fichiers texte) + - SEP : -> séparateur de nom de chemin (selon OS) + - PATHSEP : -> séparateur de variable PATH (selon OS) + - LINESEP : -> séparateur de ligne (selon OS) + - EXT : -> extension du fichier (sans le .) + Ex. FILE name chemin/vers/config.sql --> sql + - CREATE : crée un nouveau fichier de nom donné vide ou rempli par + chaîne en 2ème argument (qui peut être encadrée par des []), + sans besoin d'OPEN/CLOSE + Ex. FILE create test.sql --> /home/.../test.sql + FILE create test.sql {...le contenu du fichier...} + - REMOVE : supprime le fichier ou répertoire de nom donné + Si c'est un répertoire, il doit être vide + - MKDIR : crée l'arborescence de chemin donné + - MOVE : déplace/renomme le fichier de nom donné + - COPY : copie le fichier 1 en fichier 2 + Ex. FILE copy init.sql save/init.sql + - GLOB : -> liste de tous les fichiers du rép. donné qui + correspondent au patron regexp donné. + Ex: FILE glob -- équivalent à FILE glob . `.*` + FILE glob scripts `.*\.txt` -- \ en patron échappe + - OPEN : ouvre un fichier -> flux texte (entier de 0 à 1999) + selon mode r (lecture), w (écriture) ou a (ajout) + Ex. def h $[FILE open db.cfg r] --> 1234 + - TEMP : -> nom complet d'un fichier temporaire, créé mais + supprimé à la fermeture. Le fichier n'est pas ouvert. + Ex. FILE temp -- équivaut à FILE temp "lunasql-" + FILE temp abc --> /tmp/abc2168...99.tmp + - READ : lit un ou plusieurs caractères d'un flux texte + -> car. lu ou '' en cas de fin de fichier + Ex. FILE read $h 50 + - READLN : lit une ligne du fichier texte (> fin de ligne) + -> ligne ou '' en cas de fin de fichier + Ex. FILE readln $h + - WRITE : écrit un ou plusieurs caractères dans un flux texte + Ex. FILE write $h message + - WRITELN : idem mais avec fin de ligne système + - CLOSE : ferme le flux texte désigné + - EACH : ouvre un fichier *texte* en lecture pour lister chaque + caractère et le passer à la fonction sous la référence $arg1 + (commande ARG candidate car utilise $argX). Ferme le fichier. + Ex. FILE each fich.txt {print $arg1} + FILE each fich.txt {arg c; print car=$c} + def f { arg car; print car }; FILE each fich.txt f + - EACHLN : idem mais liste chaque ligne. + Ex. FILE eachln fich.txt {print $arg1} + FILE eachln fich.txt {arg l; print ligne=$l} + def f { arg line; print $line }; FILE eachln fich.txt f + - SCAN : -> contenu d'un fichier *texte* selon séquence + de numéros de ligne début:fin (peuvent être négatifs) + (pas besoin d'OPEN/CLOSE) + Ex. FILE scan fich.txt 1:5 --> 5 premières lignes + FILE scan fich.txt 5 --> idem + FILE scan fich.txt 6:10 --> 5 lignes suivantes + FILE scan fich.txt -5:-1 --> 5 dernières lignes + FILE scan fich.txt -5 --> idem + FILE scan fich.txt --> comme FILE list fich.txt + - LIST : -> contenu de un ou plusieurs fichiers *texte* + (ou zip si extension .gz), sans besoin d'OPEN/CLOSE + Le nom peut être exact ou encadré par `` (regexp) + Ex. FILE list fich.txt fich.gz `.*[.]sql` + FILE dump f3 $[FILE list f1 f2] -- concat f1 f2 > f3 + - LINES : idem mais sous forme de liste de lignes du fichier + Ex. for l $[FILE lines fich.txt] {print $l} + - VIEW : idem mais affiche en console avec numéros de lignes + - BYTES : -> contenu du fichier au format héxadécimal + Ex. FILE bytes fich.bin -- sur 1 seule ligne + FILE bytes fich.bin 80 -- 80 car./ligne + - DUMP : écrit la chaîne en argument en fichier texte (ou zip si + extension .gz), sans besoin d'OPEN/CLOSE + -> le nombre d'octets écrits. Le fichier est écrasé. + Ex. FILE dump fich.txt "texte important" --> 15 + FILE dump fich.gz "texte important zippé" --> 42 + - INFO : -> dictionnaire d'informations sur le fichier + - CHECK : -> empreinte CRC32 héxadécimale du fichier + - DIGEST : -> empreinte MD5 du fichier selon format fourni + 'hex' (défaut), 'b64', sinon binaire + + Exemple de lecture de fichier par FILE OPEN et READ : + def h $[file open test.txt] + while { $[def c $[file read $h]; str nempty? $c] } { print $c } + file close $h; undef h c + + Attribue à _RET_VALUE la valeur générée par la sous commande diff --git a/src/lunasql/doc/cmdfor.txt b/src/lunasql/doc/cmdfor.txt new file mode 100644 index 0000000..d33ff6b --- /dev/null +++ b/src/lunasql/doc/cmdfor.txt @@ -0,0 +1,82 @@ + FOR : Parcours d'une liste ou du résultat d'une requête SELECT + ou du contenu d'une table de la base. + FOR + FOR [-s=] var {liste valeurs} {command1 param1a param1b ...} + La variable contient la (ou les) valeur(s) de la + boucle en cours (liste de variables séparées par ','). + L'option -s indique le séparateur (regexp). Avec l'option -s + la liste ne supporte pas les regroupements par "", [] ou {}. + La liste à itérer peut être encadrée par "", [] ou {}. + FOR -r + est une séquence au format début:fin:pas. + fin est exclue, début et pas sont facultatifs (début=0, pas=1) + La plage à itérer doit être encadrée par "", [] ou {} + FOR -q + La table/requête peut être encadrée par "", [] ou {}. + Les variables locales de boucle suivantes sont disponibles : + - row_id : numéro de l'enregistrement (de 1 à nb lignes) + - col_nb : nombre total de colonnes + - col_n : nom de la colonne i (i de 1 à nb col) + - col : val. de la ligne rowid, colonne i (de 1 à nb col) + Note : la variable col n'est pas définie pour un NULL. + Pour éviter une erreur, utiliser la structure $(*x). + Ex. si le champ 1 peut être NULL, utiliser $(*col1) + Avec l'option -n, ces variables locales de boucle sont + remplacés par les noms des champs (si ce sont des noms de + variables valides, bien sûr). + La requête peut tenir sur plusieurs lignes et contenir des + commentaires SQL, mais pas avant le mot-clef "SELECT ". + FOR -f + Parcours des lignes d'un fichier + Le bloc de commandes est encadré comme d'habitude par {} ou "", mais + de préférence {}. + + Notes : + - comme d'habitude, si non encadrées de chaines dures {}, les + substitutions $ seront peut-être réalisées *en amont* + cf. help substitutes selon usage de "", (), {}. + - les vars. locales déclarées en boucle sont accessibles pour + tous les tours de la boucle (mises-à-jour à chaque itération). + - sauf en cas d'usage de substitutions en liste, il est recommandé + d'utiliser les délimiteurs de liste {} + - l'environnement local contient les options suivantes : + _LOOP_DEEP : profondeur actuelle de boucle + _LOOP_BREAK : profondeur à l'appel à BREAK (fac.) + + Exemples : + Supposons que la table 'test' contienne 2 colonnes. + FOR -q test { + print ligne:$(row_id): $(col_n2)=$(col2) + } + FOR -q {select id,descr from test where id<5} { + print ligne:$(row_id): $(col_n2)=$(col2) + } -- fonctionne aussi en encadrant la req. par [] + FOR -q { select * from matable } { + insert into autretable values ($col1, '$col2') } + -- mettons que matable ait 2 champs : ID, LIB + FOR -qn { select ID, LIB from matable } { + insert into autretable values ($ID, '$LIB') } + + -- exemples avec tableaux simples + FOR i "1 2 3" "print ^$(i%*2)" -- affiche le double + -- notez les ^ pour échapper la substitution + FOR i {1 2 3} {print $(i%*2)} -- mieux ! + FOR k,v {a 1 b 2 c 3} {print "$k => $v"} -- multi var + def a,b,c 1 2 3; FOR i [$a $b $c] {print $i} -- sub ! + FOR i {1 {2 3}} {print $(i)} -- sans -s supporte les {} + FOR i -s, [1,2,3] {print $i} + FOR p -s: $[info PATH] {print $p} -- var. PATH (linux) + def -l a 1; FOR i "1 2 3" {print $(a#inc!)} -- incrémente a + -- cet exemple tordu combine les chiffres 1, 2 et 3 : + FOR i {1 2 3} { FOR j "1 2 3" { print $(i)$(j) } } + -- autre exemple tordu montrant l'exécution de code possible : + FOR -s=\| cd {print 1|print 2|print 3} { cd } + + -- exemples avec plages de nombres (ranges) : + FOR -r i 5 {print $i} -- boucle de 0 à 4 de pas 1 + FOR -r i 1:5 {print $i} -- boucle de 1 à 4 de pas 1 + FOR -r i 8:-2:-2 {print $i} -- boucle de 8 à 0 de pas -2 + + Ne modifie pas les constantes _RET_VALUE et _CMD_STATE (sauf erreur), + mais les commandes exécutées peuvent les modifier. Attribue 0 à + _RET_VALUE si aucun bloc exécuté. diff --git a/src/lunasql/doc/cmdfrm.txt b/src/lunasql/doc/cmdfrm.txt new file mode 100644 index 0000000..89fdfc9 --- /dev/null +++ b/src/lunasql/doc/cmdfrm.txt @@ -0,0 +1,6 @@ + FRM (f) : Formate la commande SQL en argument par JSQLParser + Notes : - requiert la bibliothèque jsqlparser.jar à charger par + la commande : use frm + Documentation : http://jsqlparser.sourceforge.net/example.php + + Attribue le code SQL formaté à _RET_VALUE. diff --git a/src/lunasql/doc/cmdgrant.txt b/src/lunasql/doc/cmdgrant.txt new file mode 100644 index 0000000..d26d0cf --- /dev/null +++ b/src/lunasql/doc/cmdgrant.txt @@ -0,0 +1,6 @@ + GRANT : Attribue les droits précisés à l'utilisateur + GRANT privilèges ON database_object TO ( PUBLIC | user_list ) + [ WITH GRANT OPTION ] + Exemple : GRANT SELECT ON TABLE MyTable TO toby + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. diff --git a/src/lunasql/doc/cmdhash.txt b/src/lunasql/doc/cmdhash.txt new file mode 100644 index 0000000..58435db --- /dev/null +++ b/src/lunasql/doc/cmdhash.txt @@ -0,0 +1,22 @@ + HASH : Calcule l'empreinte cryptographique du message + HASH Calcule le hash du message msg + HASH -p Calcule de hash par PBKDF2 SHA-1 (10000 itérations) + HASH -f Calcule le hash du fichier f + Options : -r= : b64|hex|utf (défaut: hex) + -a= : MD2|MD5|SHA-1|SHA-256|SHA-384|SHA-512 + (défaut: SHA-256). Ignorée avec l'option -p. + + Exemples : + HASH "Ceci est un test" -- retour du hash de la chaîne donnée + HASH -p "Ceci est un test" -- idem avec fonction PBKDF2 + HASH -f fichier.txt -- hash de fichier et retour de valeur + HASH -f config.cfg -r=b64 -a=SHA-512 -- idem mais super hash + print $[hash -f fichier.txt] + Astuce ! Comment hasher un MDP sans conserver de variable ? + def salt %Sel2Mère -- toujours ajouter un sel pour les MDP + print $[HASH $salt:$[input -p "mdp? "]] + eval { def -l m $[HASH -l $salt:$[input -p mdp?]] + print 1:$m + return 0}; print 2:$m -- m est alors non définie + + Attribue le résultat à la constante _RET_VALUE ou 0 si erreur. diff --git a/src/lunasql/doc/cmdhelp.txt b/src/lunasql/doc/cmdhelp.txt new file mode 100644 index 0000000..3d5348a --- /dev/null +++ b/src/lunasql/doc/cmdhelp.txt @@ -0,0 +1,29 @@ + HELP (?) : Aide de l'application, ou d'une (ou plus.) commande(s) + HELP : informations générales de l'application + HELP LAUNCHING : options de lancement en ligne de commande + HELP PRACTICE : informations sur l'utilisation et la syntaxe + HELP SUBSTITUTES : documentation des fonctions de substitution + HELP COMMANDS : liste des commandes et leur courte description + HELP : aide détaillée d'utilisation de la commande cmd + -? : détaille les options possibles + HELP CATCHING : guide de gestion des erreurs à l'exécution + HELP JS-FUNCT : liste des commandes du SE javascript + HELP PACKAGES : liste et aide des modules complémentaires + HELP LIBRARIES : liste et liens à propos des bibliothèques java + HELP BONUS : bonus croustillants et exemples intrigants + HELP LICENSES : texte de licence de LunaSQL et des bibliothèques + HELP CHANGELOG : liste des modifications des versions + Note : ces commandes peuvent être abrégées, ex. : help sub + + HELP -a : ajoute à l'aide des commandes l'aide h de la var. ou + macro v. La variable/macro v ne doit pas être locale. + Avec en plus -t, pas de formatage en paragraphe. + HELP -d : supprime l'aide pour la variable/macro v + HELP -f : exporte en fichier f le contenu de toutes les aides + + Note : bien qu'avec la définition de :ALLOW_REDEF à 1 les commandes + internes (et plugins) puissent être "cachées" par la définition + d'une variable de même nom, leur aide n'est pas remplaçable par + celle d'une variable. + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdhist.txt b/src/lunasql/doc/cmdhist.txt new file mode 100644 index 0000000..8557284 --- /dev/null +++ b/src/lunasql/doc/cmdhist.txt @@ -0,0 +1,41 @@ + HIST (HI) : Affiche l'historique de la console + HIST Affiche la totalité de l'historique (200 lignes) + HIST s Recherche et affiche les entrées correspondant à + l'expression régulière s + Exemple : hist insert.* -- entrées commençant par 'insert' + Options : + -c | CLEAR Supprime l'historique de la console courante + -n | NTH Affiche l'entrée n de l'historique. + Note: la ligne recherchée est n+1 car la commande saisie ajoute 1. + -l | LIST Affiche les n dernières entrées de l'historique + -e Avec options -l, -n ou recherche, exécute les + entrées trouvées au lieu de les afficher. + Attention ! Utilisation de -e à vos risques et périls ! + Risque d'exécution infinie de commande HIST avec levée de + StackOverflowError si une commande HIST est embarquée avec -e + Ex: print test; eval HIST -e .*test + + Raccourcis utiles (de la bibliothèque JLine) : + Note : ces raccourcis peuvent varier en fonction de la plateforme et + du terminal utilisés. + - CTRL+A / Origine : place le curseur en début de ligne + - CTRL+B / Flèche G : déplace le curseur d'un car. à gauche + - CTRL+D (ligne vide) : quitte le terminal + - CTRL+D (sur car.) : supprime le car. sous curseur + - CTRL+E / Fin : place le curseur en fin de ligne + - CTRL+F / Flèche D : déplace le curseur d'un car. à droite + - CTRL+G / Flèche D : déplace le curseur au mot précédent + - CTRL+H / Retour : supprime le car. devant le curseur + - CTRL+I / Tabulation : complète le mot (table/fichier) + - CTRL+J/M / Entrée : valide la ligne actuelle + - CTRL+K : supprime la ligne depuis le curseur + - CTRL+L : efface la console + - CTRL+N / Flèche bas : affiche la commande suivante + - CTRL+P / Flèche haut : affiche la commande précédente + - CTRL+R : ouvre une recherche par mot et exécute + - CTRL+U : supprime la ligne devant le curseur + - CTRL+V : colle le presse papier au curseur + - CTRL+W : supprime le mot avant le curseur + - CTRL+X : déplace le curseur d'un mot à gauche + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdif.txt b/src/lunasql/doc/cmdif.txt new file mode 100644 index 0000000..bbfdcd6 --- /dev/null +++ b/src/lunasql/doc/cmdif.txt @@ -0,0 +1,43 @@ + IF : Teste une expression et exécute les arguments + IF + [ELSEIF ]*n + [ELSE ] + 'expr_bool' est évaluée par le moteur SE courant : avec le SE par + défaut javascript, elle peut contenir && (et), || (ou), ! (non), + parenthèses... Elle peut être encadrée par "", [] ou {} pour grouper + l'expression en un seul argument. + 'bloc' doit être déclaré par {} ou "" car il doit aussi former un seul + argument. Chaque bloc ELSEIF est évalué si les 'expr_bool' précédentes + sont fausses. Finalement, le bloc ELSE est évalué si toutes les + 'expr_bool' sont fausses. + + Exemples : + IF 0 {print ko} else "print ok" + IF [1 > 0] "print ok" elseif [0 > 1] "print ko" + IF [1 > 0] { + print ok + } else { + print ko + } + def a x; IF '$(a)'=='x' { print "a vaut x" } + IF 1 {IF 1 {IF 1 {IF 1 {IF 1 {print ok}}}}} + var i=0 -- i est une variable du SE par défaut (js) + IF [i++ < 5] { print i=$[i]; print ok } + IF "i++ < 5" " print i=^$[i]; print ok " + IF $[call 3>4 or FALSE] {print "éval par SGBD ok"} + IF 1 {} -- retourne la valeur de l'évaluation 'expr_bool' (= 1) + def a 1 -- a est définie, pas b + IF {$a == 1} {print "a ok"} elseif {$b == 1} {print "b PAS ok"} + + Notes : + - les substitutions $ seront peut-être réalisées *en amont* + Voir l'aide substitutes. Selon l'usage de "", (), {}, prendre + l'habitude d'encadrer les blocs par {}. + - 'expr_bool' peut contenir des variables LunaSQL et peut aussi + être encadré par {} pour en différer les substitutions. + - en cas de blocs volumineux (ce qui devrait rarement se produire), + préférer la commande WHEN + + Ne modifie pas les constantes _RET_VALUE et _CMD_STATE (sauf en cas + d'erreur), mais les commandes exécutées peuvent les modifier. + Attribue 0 à _RET_VALUE si aucun bloc exécuté. diff --git a/src/lunasql/doc/cmdimport.txt b/src/lunasql/doc/cmdimport.txt new file mode 100644 index 0000000..84b6142 --- /dev/null +++ b/src/lunasql/doc/cmdimport.txt @@ -0,0 +1,64 @@ + IMPORT (IM) : Ajoute à la table en paramètre le fichier spécifié + IMPORT nom_fichier.ext nom_table1 + IMPORT nom_fichier.ext + IMPORT -f s Recherche les fichiers dont le nom correspond à + l'expression régulière s et les importe chacun en table ad hoc + L'expression s peut être encadrée par ``. + ex. tb.*[.]csv équivaut au shell tb*.csv + Sans table renseignée, le nom de fichier est pris pour table. + Les adresses en http(s)://, ftp:// et file:// sont supportées. + Les fichiers zippés (extension .csv.gz ou .csv.zip) ou chiffrés + (extension .csv.cry, algo AES128) sont supportés. Si extension cry + (mode chiffrement), la clef est fournie par l'option -k et peut faire + 128 ou 256 bits (seuls les 128 ou 256 premiers bits (forts) sont + retenus pour la clef, format hex obligatoire, ex. par commande HASH). + Formats acceptés : CSV (sép. ';') et TXT (le choix est fait selon + extension du fichier). Les commentaires, introduits par # en début + de ligne, sont supportés, et les lignes vides sont ignorées. + Il est concédé le support de SQL (renvoi à la commande EXEC). + + Options supplémentaires : + -c tb : importe en table tb le contenu TXT du presse-papier + -d : le fichier ne contient pas de noms de colonnes + -h head : idem mais on fournit les entêtes sous forme de liste SQL + -o opts : importe selon options de format CSV opts spécifié + avec opts : first_line,quote_char,escape_char + Les 3 options doivent obligatoirement être spécifiées + Options par défaut : 0,",\ + -t : spécifie les colonnes à importer au format date avec + conversion de la chaîne en date selon le format fourni + '#data' est remplacé par le nom de la colonne + '|' est séparateur d'expression, si plusieurs colonnes de date + Avec l'option -l, spécifiez une locale pour le format de date + parmi : FR, DE, IT, ZH, TW, CA, CAF (français), US, UK, JP, KO + -q : importe le résultat d'une requête SQL (cf. ci-dessous) + avec -q (et la bibliothèque CsvJdbc en CLASSPATH), l'option -p + passe à CsvJdbc le dictionnaire {clef=valeurs} (cf doc CsvJdbc). + Chaque fichier du rép. est une table (omettre l'extension). Le + format de fichier est CSV séparateur point-virgule, et il est + possible d'encadrer la requête par {}. + Documentation : http://csvjdbc.sourceforge.net/doc.html + + Notes : + - les champs numériques vides sont traités comme des NULL. De même, + une chaîne "" remplissant un champ est insérée en NULL + - en cas d'erreur 'IOException : Not in GZIP format', la clef de + déchiffrement est sûrement erronée + - selon les SGBD, il est possible d'importer facilement du CSV, + par exemple pour sélectionner les colonnes/lignes à importer. + Ex. H2 : SELECT * FROM CSVREAD('fichier.csv'); + + Exemples : IMPORT -f `tb.*[.]csv` + IMPORT -f `tb.*[.]csv` -o {3,",\} + IMPORT table.csv -- le fichier porte le nom de la table + IMPORT -d data.csv table -- le fichier n'a pas d'entêtes + IMPORT -h=ID,LIB data.csv table -- entêtes fournies + -- exemple sur H2 avec donnée '3 Feb 2001 03:05:06 GMT' + IMPORT table.csv table -t 'd MMM yyyy HH:mm:ss z' -l US + -- chiffrement + IMPORT test.csv.cry TB_TEST -k=$[hash clefsecrete] + -- import depuis CsvJdbc + IMPORT -q data/ TB_TEST select col1, col2 from csvfile + + Attribue le nombre de lignes du dernier import à _RET_NLINES + et le nombre total à _RET_VALUE diff --git a/src/lunasql/doc/cmdinfo.txt b/src/lunasql/doc/cmdinfo.txt new file mode 100644 index 0000000..68af725 --- /dev/null +++ b/src/lunasql/doc/cmdinfo.txt @@ -0,0 +1,41 @@ + INFO (&) : Affiche des informations sur le système + (propriétés système et mémoire) + INFO : liste les propriétés système de la JVM + + INFO prop : affiche la valeur de la propriété système prop + prop peut être une liste séparée par une virgule. Choisir parmi : + - les propriétés internes suivantes : + commands : liste des commandes internes LunaSQL, + globals : liste des variales globales (définies par def), + locals : liste des variables locales (définies par def -l/-u), + options : liste des options de configuration (:* et _*), + database : dictionnaire des propriétés de la base de données, + tables : liste des tables utilisateur de la base de données, + systables : liste des tables système de la base de données, + environ : liste des variables d'environnement du système hôte, + stack : pile d'exécution des macros/alias, + redefined : liste des commandes redéfinies par des macros, + nocircctrl : liste des macros exemptes du contrôle ref. circ., + network : liste des interfaces réseaux du système (IP MAC) + - les propriétés système de la JVM, comme : + os.name, os.version, os.arch, java.version, java.home, user.name, + user.home, user.dir, file.encoding, java.class.path + Cf. http://docs.oracle.com/javase/8/docs/api/java/lang/System.html + ou bien une variable d'environnement + + INFO rech arg : recherche de l'information rech avec arg, parmi : + - whatis cmd : retourne le type de la commande cmd, qui peut être + local, global, syslocal, sysglobal, plugin, intern, nodef, stack + - find str : retourne le contenu des variables (et leur nom s'il + y en a plusieurs), dont le contenu contient la chaîne str + + Exemples : + INFO locals --> toules les macros, alias et variables + INFO os.version --> version de l'OS, sans grande surprise + INFO user.name,user.home + INFO PATH --> variable d'environnement PATH, sép. système + INFO whatis def --> intern + INFO find exec --> reinit exec "$(_INIT_FILE)"; + + Attribue à _RET_VALUE la valeur de la dernière propriété, ou le + nombre si pas d'argument fourni. diff --git a/src/lunasql/doc/cmdinput.txt b/src/lunasql/doc/cmdinput.txt new file mode 100644 index 0000000..3bbb443 --- /dev/null +++ b/src/lunasql/doc/cmdinput.txt @@ -0,0 +1,30 @@ + INPUT (>) : Affiche une invite de saisie au clavier et retourne. + INPUT [opt] Invite de saisie + Options : + -p : saisie de mot de pâsse + -i : invalidation des $ par des # (empêche substitutions) + -f=frm : contrôle du format de saisi selon commande ou regexp + frm : exp. rég. encadrée par ` et `, ou commandes suivantes : + int, num, bool (0,1,t(rue),f(alse)), yesno (y[es],n[o[n]],o[ui]), + id (nom de var.), char, word (lettre, chiffre, _), date, + datetime, dateus (format US), alpha, alphanum, alphauc + (maj.), alphalc (min.), email, str (tout, défaut). + -t=sec : temps de réponse maximal autorisé (en secondes) + -d=str : valeur par défaut à retourner en cas de saisie vide + -r=str : valeur par défaut à retourner en cas d'interruption + (par Ctrl+C ou temps dépassé avec option -t) + + Notes : les $ saisis ne seront pas substitués. Mais en cas d'usage + ultérieur de la valeur saisie, et pour éviter toute exécution de + code exécutable potentiellement dangereux, utilisez l'option -i. + Les options avancées (-p, -t) ne sont disponibles qu'en appel par + console (c'est-à-dire LunaSQL exécutée avec l'option --console). + + Exemples : + INPUT -f=email "Votre adresse e-mail ? " + INPUT -i -f=`0\d\.(\d\d\.){3}\d\d` "Votre numéro de tél ? " + INPUT -p "Entrez le mot de pâsse: " + INPUT "Nom de variable ? " -f=id -t=10 -d=name + if [ '$[INPUT -r=]' == '' ] { print Annulation } + + Attribue le message saisi à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdinsert.txt b/src/lunasql/doc/cmdinsert.txt new file mode 100644 index 0000000..04c7f39 --- /dev/null +++ b/src/lunasql/doc/cmdinsert.txt @@ -0,0 +1,5 @@ + INSERT : Insère des données dans la base + INSERT INTO
[( column [,...] )] { VALUES(Expression [,...]) | + SelectStatement} + + Attribue le nombre de lignes insérées à _RET_VALUE et à _RET_NLINES. diff --git a/src/lunasql/doc/cmdlet.txt b/src/lunasql/doc/cmdlet.txt new file mode 100644 index 0000000..e7e1e1c --- /dev/null +++ b/src/lunasql/doc/cmdlet.txt @@ -0,0 +1,19 @@ + LET (%) : évalue une expression par le moteur Script Engine + LET + 'expr' est évaluée par le moteur SE courant (par défaut javascript), + et peut dans ce cas contenir && (et), || (ou), ! (non), parenthèses... + (selon la syntaxe du langage SE sélectionné). Elle peut être encadrée + par "", [] ou {} pour grouper une code multi-lignes ou échapper + des éléments syntaxiques du language. + Le SE rhino/nashorn fourni avec le JRE peut autoriser des structures + de données complexes : JSON, XML, fonctions ou création d'objets Java. + + Exemples : + engine nashorn + LET { obj = { key1: {val1: 123, val2: "hello"}} } + print $[obj.key1.val1] --> 123 + engine Clojure -- bibliothèque à placer en CLASSPATH + LET { (defn sum [a b] (+ a b)) (sum 3 2) } --> 5 + print $[(sum 30 12)] --> 42 + + Attribue le résultat à _RET_VALUE. diff --git a/src/lunasql/doc/cmdlist.txt b/src/lunasql/doc/cmdlist.txt new file mode 100644 index 0000000..8b945a0 --- /dev/null +++ b/src/lunasql/doc/cmdlist.txt @@ -0,0 +1,136 @@ + LIST : Outils de gestion de listes. + LIST commande liste [params] + La liste peut être encadrée par "", [] ou {}. Mais il vaut mieux + préférer l'usage de {} (sauf en cas de substitutions en liste). + + Commandes (insensibles à la casse) et valeurs de retour : + - NEW : -> nouvelle liste depuis éléments fournis + Ex: LIST new "do ré" "mi fa" --> {do ré} {mi fa} + - FLAT : -> nouvelle liste dont les {} ont été supprimées + Ex: LIST flat {do ré} {mi {fa sol}} --> do ré mi fa sol + - CONCAT : -> nouvelle liste en mettant à la plat les éléments + Ex: LIST concat {do ré} {mi {fa sol}} --> do ré mi {fa sol} + - SIZE : -> nombre d'éléments de la liste + - GET : -> élément à la position n + Ex: LIST get {do ré mi fa} 3 --> fa + - INDEX : -> index d'emplacement de l'élément dans la liste + - INDEX-IC : idem, mais insensible à la casse + - LINDEX : -> dernier index d'emplacem. de l'élément dans la liste + - LINDEX-IC : idem, mais insensible à la casse + - HAS? : -> 1 si la liste contient l'élément donné, 0 sinon + Ex: LIST has? {do ré mi fa} sol --> 0 + - HAS-IC? : idem, mais insensible à la casse + - SUBLST : -> sous-liste selon index fournis au format début:fin + Voir l'aide de la substitution $(x@d:f) + Ex: LIST sublst {do ré mi fa} 1:-1 --> {ré mi} + - JOIN : -> chaîne formée par les éléments joints par la chaîne, + défaut "" (chaîne vide) + Ex: LIST join {do ré mi fa} : --> do:ré:mi:fa + LIST join {do ré mi fa} --> dorémifa + - FIND : -> élément dans la liste du patron regexp fourni + Ex: LIST find {do ré mi fa} f.* --> fa + - FIRST : -> n premier(s) élément(s) de la liste, défaut 1 + Ex: LIST first {do ré mi fa} --> do + LIST first {do ré mi fa} 2 --> do ré + - LAST : -> n dernier(s) élément(s) de la liste, défaut 1 + - PUSH : -> nouvelle liste avec l'élément ajouté + Ex: LIST push {do {ré mi}} {fa sol} --> do {ré mi} {fa sol} + - SHIFT : -> liste sans son premier élément + Ex: LIST shift {do ré mi fa} --> {ré mi fa} + - POP : -> liste sans son dernier élément + Ex: LIST pop {do ré mi fa} --> {do ré mi} + - INSERT : -> liste avec l'élément fourni inséré en position n, + défaut 0 (première position) + Ex: LIST insert {do ré mi fa} sol 2 --> {do ré sol mi fa} + LIST insert {do ré mi fa} sol -1 --> {do ré mi fa sol} + LIST insert {do ré mi fa} sol --> {sol do ré mi fa} + - REMOVE : -> liste avec l'élément en position n supprimé + Ex: LIST remove {do ré mi fa} 2 --> {do ré fa} + LIST remove {do ré mi fa} -1 --> {do ré mi} + - REPLACE : -> liste avec l'élément n remplacé par le fourni + Ex: LIST replace {do ré mi fa} 2 sol --> {do ré sol fa} + LIST replace {do ré mi fa} -1 si --> {do ré mi si} + - REVERSE : -> liste dont l'ordre des éléments est inversé + Ex: LIST reverse {do ré mi fa} --> fa mi ré do + - MIN : -> plus petit élément (liste de nombres) + - MAX : -> plus grand élément (liste de nombres) + - SUM : -> somme des éléments (liste de nombres) + Ex: LIST sum {5 2 9 1} --> 17 + - SORT : -> liste rangée selon les ordres disponibles suivants : + nbr|nbr-rev|str|str-rev (défaut: str) + Ex: LIST sort {do ré mi fa} str --> {do fa mi ré} + LIST sort {3 1 2 10} --> {1 10 2 3} + LIST sort {3 1 2 10} nbr --> {1 2 3 10} + - RANGE : -> liste numérique définie par la séquence. Cf. FOR -r + - NVL : -> premier élément non vide de la liste + Ex: LIST nvl {{} 2 3} --> 2 + LIST nvl $arg_ls --> premier argument non vide + def a ""; LIST nvl [{$a} {val def}] --> val def + - PICK : -> liste de n éléments pris aléatoirement + Le même élément peut être repris plusieurs fois. + Ex: LIST pick {1 2 3 4 5} --> ex. 3 + LIST pick {1 2 3 4 5} 2 --> ex. 5 2 + - RAND : -> liste dont éléments sont rangés par ordre aléatoire + Ex: LIST rand {1 2 3 4 5} --> 3 2 5 4 1 (exemple) + - UNIQ : -> liste dont les éléments en doublons sont retirés + - MINUS : -> liste résultante de la liste 1 Moins la liste 2 + - UNION : -> liste union entre deux listes fournies + - INTER : -> liste intersection entre deux listes fournies + - EACH : applique une fonction à chaque élément, référencé par + $arg1 dans la fonction, et retourne le nombre d'itérations. + - APPLY : applique une fonction à chaque élément, référencé par + $arg1 dans la fonction, et retourne une liste. + Ex: LIST apply {do ré mi fa} {str upper $arg1} + --> DO RÉ MI FA + LIST apply $[LIST range -3:5] {arg x; $x*$x + 3*$x - 2} + --> -2 -4 -4 -2 2 8 16 26 + LIST apply {a bb ccc dddd} {'$[str len $arg1]:$arg1'} + --> 1:a 2:bb 3:ccc 4:dddd + - FILTER : applique une fonction booléenne filtre à chaque élément, + référencé par $arg1 dans la fonction, et retourne une liste. + Ex: LIST filter {do ré sol} {$[str len $arg1] > 2} + - REDUCE : applique une fonction de réduction sur la liste. + Un dernier arg. optionnel est la valeur init. + $arg1 référence le résultat précédent, $arg2 l'élément + Ex: LIST reduce {1 2 3 4 5} {$arg1 + $arg2} 10 -- comme sum + LIST reduce {2 4 6 8} {arg prod e; $prod * $e} + -- calcule le produit des éléments du tableau + LIST reduce {a b c d} { '$arg1/$arg2' } + -- comme à LIST join {a b c d} / + Notes pour APPLY, FILTER et REDUCE : + - les var. locales déclarées en boucle sont accessibles pour tous + les tours de la boucle + - la commande ARG est candidate (utilise les arguments argX). + Ex: def f { arg n; $n * $n }; LIST APPLY {2 3 4} f + - ANY? : cf. filter, mais retourne 1 si au moins un élément est vrai + Ex: LIST any? {1 3 4 7 0} {$arg1 % 2 == 0} --> 1 + - ALL? : cf. filter, mais retourne 1 si aucun élément n'est faux + Ex: LIST all? {1 3 4 7 0} {$arg1 % 2 == 0} --> 0 + - REPEAT : -> liste formée de n fois la chaîne donnée + Ex: LIST repeat {do ré} 2 --> {do ré} {do ré} + - ONES : -> liste formée de n fois "1" + - ZEROS : idem avec "0" + - COMMAND? : -> 1 si la liste est une commande valide, 0 sinon + Ne contrôle pas le nombre d'arguments pour une commande. + - TOCSV : -> une chaîne au format CSV de la liste en arg. + Ex: LIST tocsv {do ré {mi fa}} --> "do";"ré";"mi fa" + + Exemple d'utilisation typique : + -- Recherche de valeur de fruits (liste de listes) + def fruits { + {pomme 1} + {banane 2} + {poire 3} + {fraise 4} + } + def fruit-val { + for fruit $fruits { + def -l nom,val $[list sublst $fruit 0] $[list sublst $fruit 1] + if $[str starts? $nom $arg1] {return $val} + } + } + print "valeur de fraise : $[fruit-val fraise]" + + Note : les opérations fonctionnent sur les valeurs et non sur les + références. Il n'y a pas de modification de variable. + Attribue à _RET_VALUE la valeur générée par la sous commande diff --git a/src/lunasql/doc/cmdmail.txt b/src/lunasql/doc/cmdmail.txt new file mode 100644 index 0000000..8373328 --- /dev/null +++ b/src/lunasql/doc/cmdmail.txt @@ -0,0 +1,18 @@ + MAIL : Envoi d'un courrier électronique à un destinataire par mail + MAIL [options] -h= [-o=] [-u=] [-p=] + -d= -c= -i= + -e= -b= -f= + options : -a (authentification) avec -u=, -p= + -o=, -t (chiffrement TLS), -g (DEBUG) + -y (mode hypertexte HTLM) + + Exemple : MAIL -d=aa@bb.com,cc@ee.net -b=\"Réunion\" {Bonjour...} + + Notes : + - requiert la bibliothèque javamail.jar (et la bibl. 'mail') + - les valeurs des options h, o, u, p sont conservées en mémoire + - il peut y avoir plusieurs options d, c et i, et chacune peut + contenir des adresses séparées par , + - les adresses en 'Nom Prénom ' sont supportées + + Attribue 1 à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdmerge.txt b/src/lunasql/doc/cmdmerge.txt new file mode 100644 index 0000000..56b4ce7 --- /dev/null +++ b/src/lunasql/doc/cmdmerge.txt @@ -0,0 +1,13 @@ + MERGE : Fusionne les données de deux tables + Met à jour les lignes existantes, et insère les autres. + Vérifiez si MERGE est disponible sur votre SGBD. + MERGE INTO tableName USING tableRefName + ON (conditions) + WHEN MATCHED THEN + UPDATE SET table1.colonne1 = valeur1, table1.colonne2 = valeur2 + DELETE WHERE conditions2 + WHEN NOT MATCHED THEN + INSERT (colonnes1, colonne3) + VALUES (valeur1, valeur3) + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. diff --git a/src/lunasql/doc/cmdneed.txt b/src/lunasql/doc/cmdneed.txt new file mode 100644 index 0000000..4e472ba --- /dev/null +++ b/src/lunasql/doc/cmdneed.txt @@ -0,0 +1,13 @@ + NEED : Teste si la version en cours en compatible avec le requis + Si le num. requis est supérieur à celui du module, le script + SQL est interrompu. + NEED [-x ] + + Exemples : NEED 4.4.3.0 + NEED 4 -x 4.2 + + Note : en cas d'erreur de version signalée par NEED, le script (ou la + portée en cours d'exécution) est arrêté quelque soit la valeur de + :EXIT_ON_ERR. + + Attribue la différence (numérique) à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdnet.txt b/src/lunasql/doc/cmdnet.txt new file mode 100644 index 0000000..291130c --- /dev/null +++ b/src/lunasql/doc/cmdnet.txt @@ -0,0 +1,41 @@ + NET : Ouvre et envoie une requête HTTP GET, POST, PUT, DELETE + Permet d'envoyer des données à un serveur sous forme de requêtes + HTTP GET, POST, PUT et DELETE, avec la possibilité d'ajouter des + entêtes. Actuellement, seules les sous-commandes GET et POST sont + supportées. + + NET get [-h=] [-o=] + est une adresse sur un serveur HTTP valide (http(s)://) + Avec l'option -h, un dictionnaire de clefs key=value + définit les entêtes HTTP à envoyer au serveur (le dictionnaire + peut être encadré par "" ou {}). Voir la documentation sur + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers + Avec l'option -o, un nom de fichier de sortie est + accepté pour sauvegarder les données en fichier et non les + retourner en console (utile pour les fichiers binaires). + Exemples: + NET get http://server.net/resource.html + NET get https://server.net/binary.zip -o=archiv.zip + NET get https://server.net/login.php -h={ + User-Agent: Mozilla/5.0 + Content-Type: application/xml + Authorization: Basic bG9naW46cGFzc3dvcmQ= + } + -- API HTTP de LunaSQL, si serveur lancé sur localhost:5862 + -- passage de requête en paramètre d'URL avec codage d'URL + NET get http://localhost:5862/api?sqlquery=select%20*%20from%20test + + NET post [-h=] [-o=] + L'usage de , -h et -o est le même que la commande GET. + représente le corps de la requête, les données à envoyer + au serveur. C'est soit une simple chaîne (encadré par "" ou {}) + soit avec l'option -i le nom d'un fichier contenant les données. + Exemples: + NET post http://server.net/reader.php "login=mike&password=1234" + NET post http://server.net/reader.php -i=bodyToSend.dat + -- API HTTP de LunaSQL, si serveur lancé sur localhost:5862 + -- passage de requête en corps POST text/plain avec codage d'URL + NET post http://localhost:5862/api sqlquery=select%20*%20from%20test + + Attribue à la constante _RET_VALUE les données renvoyées par le + serveur, ou bien la taille du fichier écrit avec l'option -o. diff --git a/src/lunasql/doc/cmdnext.txt b/src/lunasql/doc/cmdnext.txt new file mode 100644 index 0000000..f0df453 --- /dev/null +++ b/src/lunasql/doc/cmdnext.txt @@ -0,0 +1,9 @@ + NEXT : Continue la plus interne boucle FOR ou WHILE. + N'est pas supportée par commandes de traitement de bloc comme + LIST, DICT, FILE + + Exemples : + for -r i 5 { print $i; NEXT; print "jamais affiché" } + i=0; while i++<10 { /* traitement */ NEXT; print "jamais affiché"} + + Attribue 0 à la constante _RET_VALUE diff --git a/src/lunasql/doc/cmdopt.txt b/src/lunasql/doc/cmdopt.txt new file mode 100644 index 0000000..728f1d4 --- /dev/null +++ b/src/lunasql/doc/cmdopt.txt @@ -0,0 +1,156 @@ + OPT (_) : Modifie la valeur d'une option (ou constante) système + de configuration de la console ou de l'application. + OPT X val Affecte à une option système X la valeur val + Exemple : OPT :PROMPT LunaSQL + Avec l'option -l : sans argument, liste les options définies + localement ; avec argument, affecte localement l'option (limitée au + lecteur en cours). + L'option -u fonctionne comme -l, avec liaison de l'option aux + lecteurs parents (cf. fonctionnement en commande DEF). + Une option commençant par ':' peut être modifiée (configuration), + mais une option système commençant par '_' est interne au système, et + n'est modifiable que par le programme. + Attention : il arrive que, pour des raisons de sécurité, la commande + OPT en modification soit verrouillée par l'option de lancement + --deny-opt-command. Dans ce cas, toute tentative d'édition d'une + option non locale (globale ou lecteurs pères) par OPT sera refusée + jusqu'à la fermeture de la console. En revanche, les définitions en + option de lancement par --opts et au niveau du fichier de config. + (et non du fichier d'init.) sont autorisées. + + OPT s Recherche les options dont le nom correspond à + l'expression régulière s et les affiche + L'expr. régulière s peut être encadrée par ` et `. + Exemple : OPT AB.* -- affiche les const. commençant par 'AB' + + Attribue la valeur de l'option fixée ou affichée à _RET_VALUE, + ou bien le nombre d'options affichées (si aucun argument). + + Options particulières modifiables directement par commande OPT : + + :ADD_ROW_NB : spécifie si (1/0) l'affichage des tableaux de + ResultSet comprend une colonne de numéros de lignes. + :ALIAS_ARG : spécifie si (1/0) les arguments d'une commande de + type alias sont ajoutés automatiquement (en plus de $(1)...). + Avec :ALIAS_ARG à 1, les arguments son passés à l'alias. + Avec :ALIAS_ARG à 0, il est possible de lancer un alias en + préfixant son nom par ':'. L'usage des alias n'est plus + recommandé : préférer les macros (avec :ALIAS_ARG à 1, certaines + macros peuvent ne plus fonctionner. + :ALLOW_RECUR : spécifie si (1/0) l'exécution récursive d'alias + est autorisée (références circulaires). Cette utilisation est + à vos risques et périls ! Ne concerne pas les boucles formées + par eval, $[]... mais seulement les macros en appel direct. + :ALLOW_REDEF : spécifie si (1/0) la redéfinition (masquage) de + commandes par un alias/macro du même nom est autorisé. Cette + utilisation est à vos risques et périls ! + :AUTOSAVE_CFG : spécifie si (1/0) on sauve la configuration à la + sortie (options et variables). + :BEEP_ON : spécifie si la console doit biper sur erreur (à + déclarer uniquement en fichier config). + :COL_MAX_WIDTH : spécifie la taille max. des colonnes affichées en + retour de commandes de type SELECT (doit être entier >= 5). + :COLORS_ON : spécifie si la console se pare de belles couleurs. + :EDITOR_PATH : éditeur de texte à utiliser à l'appel de EDIT -p + :ENCODING : spécifie l'encodage des caractères des fichiers. + Défaut : valeur de la propriété "file.encoding": UTF-8 (sans BOM) + :END_CMD_NL : spécifie si les commandes sont séparées par une + fin de ligne \n (1) ou par un point-virgule (0). Défaut : 1 + Note : le comportement en console est différent d'en fichier : + si (1) ';' est terminateur en console, séparateur en fichier. + :EXIT_ON_ERR : spécifie si (1/0) l'exécution d'un script ou d'une + chaîne de commandes (séparées par ';') se termine après une + commande générant une erreur. + :FILE_CONFIRM : spécifie si (1/0) les opérations sur les fichiers + doivent demander confirmation si le fichier existe. + :HISTORY_DBL : spécifie si l'historique des commandes doit + être alimenté par la commande saisie. + 0: non, 1: simple copie, 2: formatée sur 1 ligne, 3: formatée en + multilignes, défaut : 2. + :LIST_SUBSTIT : prise en charge de l'évaluation précoce par liste + par § plus de l'évaluation normale par $ + :ON_ERROR : détermine le code à exécuter en cas d'erreur. + :ON_INIT : détermine le code à exécuter au démarrage (juste + après la lecture du fichier d'initialisation). Peut être annulé + (quitter l'application) par positionnement de la variable locale + 'cancel' à 1. + :ON_QUIT : détermine le code à exécuter à la fermeture (par QUIT + ou par fin des instructions) + Peut être annulé (rester dans l'application) par positionnement + de la variable locale 'cancel' à 1. Dans ce cas, QUIT agira comme + EXIT, mais on quittera tout de même à la fin des instructions. + Ne pas trop compter sur cette annulation quand-même... + :PLUGINS_PATH : spécifie la liste des répertoires où rechercher + les plugins pour les commandes formées par plugin. Note : le + séparateur est ';' pour Windose et ':' pour Linux. + Défaut : .:/plugins/ + :PROMPT : spécifie la chaîne du prompt en cours. Défaut : SQL + Peut contenir des substitutions par '$'. Exemples : + opt :PROMPT "^$[time now HH:mm] SQL" + opt :PROMPT "Base ^$(_CNX_BASE)" + def i 0; opt :PROMPT "^$(_CNX_PATH):^$[str lpad ^$(i#inc!) 3 0]" + :ROW_MAX_NB : spécifie le nombre maximal de lignes à retourner + par une commande de type SELECT (SELECT, DISP, CALL...). + :SCRIPTS_PATH : spécifie la liste des répertoires où rechercher + les scripts pour les commandes EXEC et START. Le séparateur est + ';' pour Window$ et ':' pour Linux. Défaut : .:/scripts/ + :SELECT_ARRAY : spécifie si (1/0) le format d'affichage des + résultats SELECT doit être tabulaire. + :SIGN_KEY : couple de clefs publique|privée pour la signature + des scripts (ou autres fichiers/textes) par EdDSA (Ed25519). Les + clefs sont générées par la commande SIGN en bibliothèque 'crypt' + :SIGN_POLICY : spécifie la politique de sécurité d'éxécution des + scripts par signature numérique (0: pas de vérification, + 1: alerte si clef non fiable, 2: erreur si clef non fiable). + :SIGN_TRUST : liste des clefs publiques de confiance séparées par + des ',' pour la vérification des signatures numériques. + + Options particulières non modifiables directement : + + _BASES_FILE : nom ou chemin du fichier des définitions des bases. + _CMD_BUFFER : liste des commandes dans le tampon (buffer). + _CMD_STATE : état d'exécution de la commande précédente + (0 : pas de modif., 1 : modif. effectuée, E : erreur à + l'exécution). + _CNX_BASE : nom de la base de données en cours d'utilisation. + _CNX_DRIVER : nom du driver de connexion à la base de données. + _CNX_LOGIN : nom de connexion de l'utilisateur en cours. + _CNX_PATH : chemin jdbc complet de connexion à la base de données. + _CONFIG_FILE : nom ou chemin du fichier de config. clef:=valeur + _CONST_EDIT : indique si les options utilisateur (donc commençant + par ':') sont modifiables dans la session en cours. + Défaut : 2. L'option de lancement --deny-opt-cmd fixe à 1. + (0: aucune opt, 1: opt locales, 2: toutes opt) + _DAY_DATE : date du jour à la connexion (format JJ/MM/AAAA). + _DAY_DATE_F : date du jour à la connexion (format AAAAMMJJ, pour + être utilisée dans les fichiers, par exemple). + _DB_NTYPE : numéro du type de SGDB (0: ODBC, 1: ACCESS, + 2: ACCESSX, 3: UCACCESS, 4: HSQLDB, 5: H2DB, 6: MYSQL, 7: ORACLE, + 8: DERBY, 9: SQLSERVER). + _DB_TYPE : libellé du type de SGBD (ODBC ACCESS ACCESSX HSQLDB + H2DB MYSQL ORACLE DERBY SQLSERVER). + _ERROR_CMD : Nom de commande ayant généré la dernière erreur. + _ERROR_MSG : Message complet de la dernière erreur. + _ERROR_STK : Pile d'appels à la dernière erreur. + _EVAL_ENGINE : nom du moteur d'évaluation actuel. Modifiable + indirectement par la commande ENGINE. Défaut : js (JavaScript). + _EXEC_TIME : durée de l'exécution depuis dernier appel de CHRON. + _HISTORY_FILE : nom ou chemin du fich. d'historique des commandes. + _INIT_FILE : nom ou chemin du fichier d'init. de la console. + _LOADED_LIBS : liste des bibliothèques chargées (commande USE). + _LOGIN_MS : date de la connexion en ms (Epoch). + _NVERBOSE : numéro de l'état d'affichage de message en cours + (0: SILENCE, 1: DISPLAY, 2: MESSAGE, 3: CHATTY, 4: DEBUG). + _RET_NLINES : nombre de lignes affectées lors de la dernière + commande. + _RET_VALUE : valeur retournée lors de la dernière commande. + _SESSION_ID : identifiant unique de session de console. + _SQL_UPDATE : spécifie si (1/0) l'exécution des commandes SQL de + modification (UPDATE, INSERT, DELETE, CREATE, DROP...) sont + autorisées. + _VERBOSE : libellé de l'état d'affichage de message en cours. + (SILENCE, DISPLAY, MESSAGE, CHATTY, DEBUG). + _VERSION : numéro de version de l'application LunaSQL. + _VERS_NAME : nom de code de la version de l'application LunaSQL. + _WHEN_DEEP : profondeur de l'environnement conditionnel (WHEN). + _WORKING_DIR : répertoire de travail à la connexion. diff --git a/src/lunasql/doc/cmdplay.txt b/src/lunasql/doc/cmdplay.txt new file mode 100644 index 0000000..a791eb7 --- /dev/null +++ b/src/lunasql/doc/cmdplay.txt @@ -0,0 +1,8 @@ + PLAY : Joue le fichier son (format .wav) + PLAY Joue le fichier file (format doit être WAVE (.wav)). + La lecture est réalisée en asynchrone (arrière-plan). + + Exemple : + PLAY sounds/login.wav + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdplugin.txt b/src/lunasql/doc/cmdplugin.txt new file mode 100644 index 0000000..f3fc218 --- /dev/null +++ b/src/lunasql/doc/cmdplugin.txt @@ -0,0 +1,63 @@ + PLUGIN (pg) : Liste ou inscrit un greffon attaché à une commande + PLUGIN -l : liste les greffons enregistrés et leur commande + PLUGIN -c : supprime l'intégralité des greffons + PLUGIN -n : affiche le nombre de greffons enregistrés + PLUGIN -r : supprime le greffon name + PLUGIN -a : affiche la classe du greffon name + Note : le nom de greffon (nom de commande) est sensible à la casse + PLUGIN : ajoute le plugin correspondant à cette + classe full_class_name. Le nom de la commande est importé. + Exemple : PLUGIN com.command.CmdEchoPlugin --> ECHO + + Les classes doivent se trouver dans le CLASSPATH, et le nom attribué + sera utilisé comme nom de commande correspondant. + Les classes présentes dans les rép. désignés par :PLUGINS_PATH, et + figurer dans le fichier de config. _CONFIG_FILE préfixé par '?' + Ex. ?plugin = com.command.CmdEchoPlugin + Les greffons sont recherchés *après* les alias et les commandes. + Note : pour -a, -r : le nom doit être le même que déclaré en greffon + + Attribue le nom de la classe du plugin à _RET_VALUE si le traitement + est effectué, le nombre le lignes si liste, 0 si aucun traitement. + + Exemple simple de classe greffon : commande echo (semblable à print) + +----------------------- code ----------------------------------- + + import lunasql.lib.Contexte; + import lunasql.cmd.Instruction; + + public class CmdEchoPlugin extends Instruction { + + // Constructeur : utilise le contexte courant + public CmdEchoPlugin(Contexte cont) { + // Appel au super constructeur avec passage du contexte, + // du type de commande (plugin : TYPE_CMDPLG), et + // et du nom et raccourci de la commande. + super(cont, TYPE_CMDPLG, "ECHO", "E"); + } + + // Exécution. + // Retourne RET_CONTINUE, RET_EXIT_SCR ou RET_SHUTDOWN + public int execute() { + // Obtention de la connexion SQL : cont.getConnex() + if (cont.getVerbose() >= Contexte.VERB_BVR) + cont.println(getSCommand(1)); + cont.setVar(Contexte.ENV_CMD_STATE, Contexte.STATE_FALSE); + cont.setValeur(null); // ne retourne rien + return RET_CONTINUE; + } + + // Description de la commande, affichée sur HELP commands + public String getDesc() { + return " echo Affiche un message\n"; + // ces espaces et le \n final garantissent un bon affichage + } + + // Aide complète de la commande, affichée sur HELP + public String getHelp() { + return "ECHO : affiche le message "; + } + }// class + +---------------------------------------------------------------- diff --git a/src/lunasql/doc/cmdprint.txt b/src/lunasql/doc/cmdprint.txt new file mode 100644 index 0000000..5c22d3a --- /dev/null +++ b/src/lunasql/doc/cmdprint.txt @@ -0,0 +1,52 @@ + PRINT (<) : Affiche le message en paramêtre sur la sortie + PRINT + Options : + -e : imprime le message sur la sortie erreur + -n : n'ajoute pas de saut de ligne final + -s : pas d'affichage en mode évaluation (ex. encadré en $[]) + -f : efface la console (selon plateforme) + -b : émet un biip agaçant (ne fonctionne pas partout) + -v= : écrit le message selon niveau de verbosité + parmi : + 0 Silence + 1 Affiche + 2 Message + 3 Bavard + 4 Debug + -c= : écrit le message en couleur + parmi : + 0 None + 1 Black + 2 Red + 3 Green + 4 Yellow + 5 Blue + 6 Magenta + 7 Cyan + 8 White + 9 Bright Black + 10 Bright Red + 11 Bright Green + 12 Bright Yellow + 13 Bright Blue + 14 Bright Magenta + 15 Bright Cyan + 16 Bright White + + Notes : + - le système MS Windows (même en version 10) est réticent à + supporter l'ANSI par défaut. Pour en activer le support, éditer le + registre et ajouter (ou modifier) la clef suivante : + [HKEY_CURRENT_USER\Console] + "VirtualTerminalLevel"=dword:00000001 + - le paquet 'const' définit des constantes pour cette commande. + + Exemples : + PRINT "message libre qui devra s'afficher" + PRINT -n "pas de fin de ligne" + PRINT -be "un message d'erreur sur la sortie erreur !" + PRINT -n -c=2 "ceci est un pseudo message d'erreur" + Sans être obligatoire, il est recommandé d'encadrer la chaîne par + des guillemets. + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdput.txt b/src/lunasql/doc/cmdput.txt new file mode 100644 index 0000000..030c776 --- /dev/null +++ b/src/lunasql/doc/cmdput.txt @@ -0,0 +1,16 @@ + PUT : Affecte la valeur de retour sans sortir + PUT [valeur] + Comme de la commande RETURN, les arguments sont placés en valeur + de retour, mais sans que l'exécution soit interrompue. Le résultat + est retourné pour la commande englobante $[] et placé en constante + _RET_VALUE, mais n'est pas affiché. + + Exemples : + PUT 1 + -- un peu plus corsé : incrémentation de variable à la Tcl + def incr {PUT $($arg1#inc)} -- par fonction inc + def incr {PUT $($arg1%!+$(*arg2?1))} -- avec 2ème arg optionnel + -- cf aide de macro defmacro pour incr... + + Attribue à _RET_VALUE la valeur chaîne en paramètre, rien si aucun + paramètre. diff --git a/src/lunasql/doc/cmdquit.txt b/src/lunasql/doc/cmdquit.txt new file mode 100644 index 0000000..2f4ae42 --- /dev/null +++ b/src/lunasql/doc/cmdquit.txt @@ -0,0 +1,10 @@ + QUIT (Q) : Déconnecte de la base et quitte la console ou le fichier + QUIT Quitte normalement (équivalent à QUIT 0) + QUIT Quitte avec l'état d'erreur positionné à l'état e + QUIT Quitte avec l'état d'erreur positionné à l'état e en + affichant le message d'erreur msg + + Depuis la console, CTRL+D déclenche la commande QUIT. En mode HTTP, + cette commande est annulée. Elle peut être annulée par le + positionnement à 0 de la var. locale 'cancel' en :ON_QUIT. Annulée, + ella a le même effet que EXIT. diff --git a/src/lunasql/doc/cmdrand.txt b/src/lunasql/doc/cmdrand.txt new file mode 100644 index 0000000..46d831e --- /dev/null +++ b/src/lunasql/doc/cmdrand.txt @@ -0,0 +1,32 @@ + RAND : Génère un nombre ou une chaîne aléatoire selon bornes. + Modes : number (ou nbr), string (ou str), secure (ou sec) + ou mode UUID. Par défaut : string + RAND -m=nbr|number [-i=borne_inf] [-s=borne_sup (exclue)] + défaut : -i=0, -s=1000 + RAND -m=str|string [-t=taille] [-c=charset] + défaut : -t=16, -c=a. charset : A:A-Z, a:a-z, 0:0-9, %:spécial + RAND -m=sec|secure [-t=taille] [-r=base] + défaut : -t=16, -r=16 (de 2 à 36) + RAND -m=UUID + + Exemples : + RAND : Génère et retourne un nombre entre 0 et un milliard (exclu) + équivalent à: RAND -m=nbr -s=1000000000 + RAND -m=nbr -s=50 Idem mais nombre entre 0 et 50 + RAND -m=nbr -i=10 -s=50 Idem mais nombre entre 10 et 50 + RAND -m=str : Génère et retourne une chaîne de 16 car. minusc. + RAND -m=str -t=12 Idem mais chaîne fait 12 car. minusc. + RAND -m=str -t=12 -c=Aa0 Idem mais chaîne est maj/min/chiffre + RAND -m=sec -t=20 -r=32 Génère un nombre aléatoire sécurisé de 20 + octets et retourne le résultat en base 32. + Option : -d=seed : semence (long), ex. -d=$[time now] + RAND -m=UUID Génère et retourne un UUID + -- Génération de 10 nombres aléatoires : + i=0; while i++<10 { print $[RAND -m=nbr -i0 -s1000000] } + + Note : les générations sécurisées (modes sec et UUID) consomment + l'entropie du système ; il peut arriver que la commande RAND + (classe SecureRandom) attende que l'entropie soit suffisante + avant de retourner une valeur. + + Attribue l'aléa généré à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdreport.txt b/src/lunasql/doc/cmdreport.txt new file mode 100644 index 0000000..849ea79 --- /dev/null +++ b/src/lunasql/doc/cmdreport.txt @@ -0,0 +1,34 @@ + REPORT : Commande de génération de rapports simples en HTML + REPORT fichier_source fichier_généré + Le fichier source est typyquement un fichier HTML, mais tout ce qui + se trouve entre la balises <% et %> sera interprété par LunaSQL comme + ce serait le cas en console ou en fichier. Le résultat retourné par + chaque bloc se substituera aux balises. + + Cette commande peut servir pour générer des rapports basiques au + format HTML rapidement, mais à noter qu'il est impossible comme en + PHP d'imbriquer les blocs avec du code HTML. + + Exemple de modèle de rapport minimaliste "salaires.model" : + +----------------------- code ----------------------------------- + + + +

Date : <% put $_DAY_DATE %>

+

Salaires des employés du service info :

+
+ <% + put -- pour vider la valeur courante + for -qn {SELECT NAME, SAL FROM EMPLOYEES WHERE SERVICE='info'} { + append "" + } + %> +
$NAME$SAL €
+ + +---------------------------------------------------------------- + + REPORT salaires.model salaires.html --> nb de char. écrits + + Attribue le nombre de caractère écrits à _RET_VALUE. diff --git a/src/lunasql/doc/cmdreturn.txt b/src/lunasql/doc/cmdreturn.txt new file mode 100644 index 0000000..3f6f2df --- /dev/null +++ b/src/lunasql/doc/cmdreturn.txt @@ -0,0 +1,15 @@ + RETURN (^) : Sort du fichier de commandes ou du bloc de code en cours + RETURN [valeur] + Remonte d'un niveau dans la pile après l'appel à RETURN. Le résultat + est retourné pour la commande englobante $[] et placé en constante + _RET_VALUE, mais n'est pas affiché. + + Exemples : + RETURN 1 + -- un peu plus corsé : incrémentation de variable à la Tcl + def incr {RETURN $($arg1#inc)} -- par fonction inc + def incr {RETURN $($arg1%!+$(*arg2?1))} -- avec 2ème arg + -- cf aide de macro defmacro pour incr... + + Attribue à _RET_VALUE la valeur chaîne en paramètre, rien si aucun + paramètre. diff --git a/src/lunasql/doc/cmdrevoke.txt b/src/lunasql/doc/cmdrevoke.txt new file mode 100644 index 0000000..72a6560 --- /dev/null +++ b/src/lunasql/doc/cmdrevoke.txt @@ -0,0 +1,5 @@ + REVOKE : Retire les droits précisés à l'utilisateur + REVOKE [ GRANT OPTION FOR ] privilèges ON database_object + FROM ( PUBLIC | user_list ) + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. diff --git a/src/lunasql/doc/cmdrollback.txt b/src/lunasql/doc/cmdrollback.txt new file mode 100644 index 0000000..e87d1cb --- /dev/null +++ b/src/lunasql/doc/cmdrollback.txt @@ -0,0 +1,3 @@ + ROLLBACK : Annule une transaction SQL au niveau du SGBD + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. diff --git a/src/lunasql/doc/cmdseek.txt b/src/lunasql/doc/cmdseek.txt new file mode 100644 index 0000000..1d4ddd6 --- /dev/null +++ b/src/lunasql/doc/cmdseek.txt @@ -0,0 +1,13 @@ + SEEK (/) : Évalue une expression SQL et retourne le résultat + SEEK expr évalue une expression SQL expr + Avec commande SELECT ou CALL, si la commande SQL retourne plusieurs + colonnes, le résultat est un dictionnaire, si elle a plusieurs + lignes, c'est une liste (de valeurs ou de dictionnaires). + + Ex. : SEEK {select count(*) from test} --> 20 + SEEK insert into test values (12,'toto') --> 0 + SEEK drop table test --> 0 + print $[SEEK select * from test] --> (dictionnaire) + def data $[SEEK select id, libelle from test where id<4] + + Attribue la valeur de la variable fixée à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdselect.txt b/src/lunasql/doc/cmdselect.txt new file mode 100644 index 0000000..070ed90 --- /dev/null +++ b/src/lunasql/doc/cmdselect.txt @@ -0,0 +1,15 @@ + SELECT : Consulte des données de la base par requête + SELECT [{LIMIT | TOP }][ALL | DISTINCT] + { selectExpression | table.* | * } [, ...] + [INTO [CACHED | TEMP | TEXT] newTable] + FROM tableList + [WHERE Expression] + [GROUP BY Expression [, ...]] + [HAVING Expression] + [{ UNION [ALL | DISTINCT] | {MINUS [DISTINCT] | EXCEPT [DISTINCT] } + | INTERSECT [DISTINCT] } selectStatement] + [ORDER BY orderExpression [, ...]] + [LIMIT [OFFSET ]] + + Attribue la 1re valeur de la 1re ligne à la constante _RET_VALUE et + le nombre de lignes retournées à la constante _RET_NLINES diff --git a/src/lunasql/doc/cmdset.txt b/src/lunasql/doc/cmdset.txt new file mode 100644 index 0000000..4b32b8b --- /dev/null +++ b/src/lunasql/doc/cmdset.txt @@ -0,0 +1,7 @@ + SET : Fixe un paramêtre de la base SQL + SET x=valeur -- peut varier selon le SGBD + Exemples : SET variable = expression + SET AUTO COMMIT ( ON | OFF ) + SET SCHEMA schema_name + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. diff --git a/src/lunasql/doc/cmdshell.txt b/src/lunasql/doc/cmdshell.txt new file mode 100644 index 0000000..54d6e88 --- /dev/null +++ b/src/lunasql/doc/cmdshell.txt @@ -0,0 +1,23 @@ + SHELL ($) : Exécute la commande shell externe en paramètre + SHELL cmd prm1 prm2 ... + Ouvre un shell et y exécute cmd avec ses paramètres prm1, prm2... + Si un argument contient une espace, il faut l'encadrer par "". + Ce que le processus écrit sur SDTOUT est retourné, et ce qu'il + écrit sur STDERR est affiché en rouge (ce n'est pas forcément une + erreur). Si le processus emet un code de sortie non nul, une + erreur est levée. + + Exemples : + SHELL cat init.sql + SHELL rm "fichier inutile.txt" + SHELL curl http://remote-resource.io/fichier + -- curl écrit 'fichier' sur STDOUT et ses statistiques sur STDERR + SHELL test -f dontexist.txt --> erreur car code de sortie = 1 + + SHELL -d f + Ouvre le fichier f par Desktop, donc avec l'application par défaut, + ou bien si file est une adresse internet (http(s)://) ou un e-mail + (mailto:), ouvre la ressource ad hoc. + + Attribue le résultat de la commande à _RET_VALUE, ou son nom en cas + d'option -d. diff --git a/src/lunasql/doc/cmdshow.txt b/src/lunasql/doc/cmdshow.txt new file mode 100644 index 0000000..ed81db8 --- /dev/null +++ b/src/lunasql/doc/cmdshow.txt @@ -0,0 +1,33 @@ + SHOW (#) : Affiche le catalog des objets de la base + SHOW Affiche une description des tables de la base + SHOW pattern Affiche les colonnes des tables dont le nom correspond + aux expressions régulières pattern en paramètre. L'expr. régulière + peut être encadrée par ` et `. + Exemple : SHOW tb.* test.* -- tables commençant par 'tb' ou 'test' + SHOW [-l|-t|-s|-v|-c|-m] : sans argument, liste les objets : + -t : liste les tables utilisateur (défaut) + -s : liste les tables système + -v : liste les vues + -c : liste les catalogs + -m : liste les schémas + avec l'option suivante : + -l : calcul du nombre de lignes + + Attention : éviter de lancer SHOW sans argument sur un SGBD sensible + car il prend des ressources sur les tables système, particulièrement + avec l'option -l (donc *avec* comptage du nombre de lignes). + Sur SGBD sensible, par sécurité il est préférable d'exécuter : + > SELECT owner, table_name FROM all_tables; -- toutes tables + > SELECT table_name FROM user_tables; -- tables de l'utilisateur + > SELECT object_name FROM user_objects WHERE object_type='TABLE'; + + Il est aussi possible depuis le SGBD de lister d'autres objets, comme + les schémas, les séquences, les utilisateurs... + Lancer la commande SHOW -s, et chercher les tables correspondantes. + Exemple sur H2: + select * from information_schema.users -- liste utilisateurs + select * from information_schema.sequences -- liste séquences + La macro h2-help du module h2doc utilise information_schema + + Attribue le nombre de tables (si aucun arg.) ou de colonnes (si table + fournie) à _RET_VALUE diff --git a/src/lunasql/doc/cmdsign.txt b/src/lunasql/doc/cmdsign.txt new file mode 100644 index 0000000..7ebc667 --- /dev/null +++ b/src/lunasql/doc/cmdsign.txt @@ -0,0 +1,55 @@ + SIGN : Signe numériquement le message/fichier donné par EdDSA + SIGN -g Génère une nouvelle paire de clefs et chiffre la clef privée. + SIGN -p Modifie le mot de passe de protection de la clef privée. + SIGN Signe le message texte par la clef. + SIGN -f [-d] Signe le fichier par la clef. Avec l'option -d + le fichier peut être de tout type et la signature est sauvée + dans un fichier .sig, sinon, le fichier doit être un + fichier *texte* car la signature est ajoutée au fichier. + + La paire de clef est générée dans la session en cours (en option + :SIGN_KEY). Il en va de même en cas de changement de mot de passe. + Veillez donc à SAUVEGARDER les modifications en fichier de config ! + + L'algorithme de la paire de clefs est Ed25519 (cryptographie + asymétique à courbes elliptiques à clefs publique et privée de 256 + bits). Cf. https://fr.wikipedia.org/wiki/Curve25519 + La clef privée est chiffrée par AES-128 (dérivation de clef PKCS #12) + en mode ECB/NoPadding et la paire de clefs est ensuite sauvée en + option :SIGN_KEY. Si une paire de clefs est déjà présente, la + génération de la nouvelle clef échoue. + + Il est recommandé d'utiliser l'option -d (signature détachée) pour + les fichiers binaires, compressés, chiffrés ou les fichiers texte + volumineux (> 2 Mio). Les signatures internes sont typiquement + utilisées pour les fichiers script LunaSQL et SE (Javascript), mais + également les fichiers de contrôle d'intégrité (SHA1SUM/SHA256SUM). + Note : les signatures comprennent des fins de lignes LF (Unix). Dans + les signatures internes, modifier les fins de lignes d'un fichier + (Windows ou Mac) signé invaliderait la signature. + + La signature est composée par la concaténation des éléments suivants, + le tout codé en base64 : + - clef publique : la clef publique pour vérification (32 octets), + - horodatage : date/heure au format Epoch en ms (6 octets) ; il est + inclus dans le message signé sous forme hexadécimale, + - signature : la signature EdDSA (64 octets) + Le cartouche du signature ajouté à la fin d'un fichier *texte* dans + le cas d'une signature interne est de la forme : + /* $$ BEGIN SIGNATURE $$ + DJk6bXiYW92tz1vhCMG+iAuVGQKs3KntVsLRfyWgh8IBdTLwfQ28EZuvnM0lJKgA27kd + bLHXUCxnllHXuFAwbdwvV9z30sZ4zaf4uzn5uvBn/z4aWHSKblMPg5xPgupY5IC9gY8G + * $$ END SIGNATURE $$ */ + Dans le cas d'une signature détachée, les données de signature ne + sont pas encadrées (format simple base64 avec commentaire '#'). + La vérification de la signature d'un script excécutable (SQL ou SE) + n'ajoute que quelques millisecondes à son exécution. + + Exemples : + SIGN -g --> clef créée dans la session (à sauvegarder !) + def s $[SIGN "message important"] + SIGN -f script.sql --> signature incluse en script.sql + SIGN -f script.sql -d --> signature en script.sql.sig + + Attribue la signature en base64 à la constante _RET_VALUE en cas de + signature, et la clef publique en cas de génération. diff --git a/src/lunasql/doc/cmdsize.txt b/src/lunasql/doc/cmdsize.txt new file mode 100644 index 0000000..0ebe507 --- /dev/null +++ b/src/lunasql/doc/cmdsize.txt @@ -0,0 +1,9 @@ + SIZE (*) : Affiche la taille de la (ou les) table(s) en paramêtre + SIZE nom_table1 | nom_vue1 | pattern1 ... + SIZE pattern Affiche la taille des tables dont le nom correspond + à l'expression régulière pattern. + L'expr. régulière s peut être encadrée par ` et `. + Ex. SIZE tb.* -- tables commençant par 'tb' + + Attribue le nombre de lignes de la dernière table à _RET_NLINES + et le nombre total à _RET_VALUE. diff --git a/src/lunasql/doc/cmdspec.txt b/src/lunasql/doc/cmdspec.txt new file mode 100644 index 0000000..4e6132a --- /dev/null +++ b/src/lunasql/doc/cmdspec.txt @@ -0,0 +1,16 @@ + SPEC : Lance la commande SQL spécifique de la base en paramètre + Les commandes de sélection affichent le résultat en tableau et le + retournent, les autres retournent le nb le lignes affectées ou 0 + si DDL, DCL, TCL. + SPEC -s sql : sélection (DML : SELECT, CALL, EXPLAIN) + SPEC sql : mise-à-jour/traitement (DDL, DML, DCL, TCL) + + Détail des commandes (ex. Oracle) : + DDL : CREATE, ALTER, DROP, TRUNCATE, COMMENT, RENAME + DML : SELECT, INSERT, UPDATE, DELETE, MERGE, CALL, EXPLAIN PLAN, + LOCK TABLE + DCL : GRANT, REVOKE + TCL : COMMIT, SAVEPOINT, ROLLBACK, SET TRANSACTION + + Attribue le résultat (le cas échéant) à la constante _RET_VALUE + Attribue le nombre de lignes à la constante _RET_NLINES. diff --git a/src/lunasql/doc/cmdspool.txt b/src/lunasql/doc/cmdspool.txt new file mode 100644 index 0000000..f3d023a --- /dev/null +++ b/src/lunasql/doc/cmdspool.txt @@ -0,0 +1,17 @@ + SPOOL (!) : Redirige les sorties OUT ou ERR vers le fichier indiqué + SPOOL Affiche l'état spool actuel et le retourne + Avec option -s, pas d'affichage, état seulement retourné. + SPOOL [-e=] Ouvre la redirection vers le fichier + fich1 et optionnellement les erreurs vers le fichier fich2 + Avec option -w, l'écriture écrase le fichier s'il existe + (mode 'w'), sinon, le fichier est complété (mode 'a'). + Commandes de gestion : + -s | SUSPEND : suspend la redirection (ne ferme pas le fichier) + -r | RESUME : reprend la redirection vers le même fichier + -c | OFF : ferme le fichier et repasse en console + Commandes d'information : + -f | FILE : retourne le nom du fichier de redirection + -u | ERRFILE : retourne le nom du fichier de redirection d'erreur + + Sans argument ou avec une commande d'information, attribue l'état ou + le nom de fichier à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdstart.txt b/src/lunasql/doc/cmdstart.txt new file mode 100644 index 0000000..0a02a62 --- /dev/null +++ b/src/lunasql/doc/cmdstart.txt @@ -0,0 +1,23 @@ + START (@@) : Exécute le fichier script ou les commandes passés en + paramètre en tâche de fond (nouveau processus). + La syntaxe est celle de la commande EXEC (voir l'aide de EXEC). + + Attention : + La bonne gestion de la concurrence n'est pas du tout assurée car + le même contexte est alors partagé par tous les processus. De + plus la plupart des commandes gèrent leurs arguments depuis le + contexte, donc ne supportent pas la parallélisation. + L'exécution de commandes en batterie avec START peut donc donner + lieu à d'importants et étonnants effets de bords. + Commande à utiliser avec précaution et à vos risques et périls ! + + Exemple : + start - {print 1}; start - {print 2} --> affiche 2 2 + + Ici les arguments de print étant liés à un contexte commun, + l'argument "1" est écrasé par "2" ! (oui, c'est un bogue) + Il peut être utile de différer le deuxième appel, mais cela peut + aussi lever une ConcurrentModificationException. + + Example : + start - {print 1}; time wait 1l; start - {print 2} --> 1 2 diff --git a/src/lunasql/doc/cmdstr.txt b/src/lunasql/doc/cmdstr.txt new file mode 100644 index 0000000..fb72cab --- /dev/null +++ b/src/lunasql/doc/cmdstr.txt @@ -0,0 +1,154 @@ + STR : Outils de traitement de chaînes de caractères + STR commande chaîne [params] + + Commandes (insensibles à la casse) et valeurs de retour : + - NEW : -> chaîne en entrée, tout simplement + - LEN : -> longueur de la chaîne + - EQ? : -> 1 si la chaîne 1 est égale à la 2, 0 sinon + - EQ-IC? : idem, mais insensible à la casse + - NEQ? : -> 1 si la chaîne 1 est différente de la 2, 0 sinon + - NEQ-IC? : idem, mais insensible à la casse + - INDEX : -> index d'emplacement de la chaine 2 dans la 1 + Ex: STR index salut lu --> 2 + - LINDEX : -> dernier index d'emplacement de la chaine 2 dans la 1 + - SUBSTR : -> sous-chaîne selon index fournis au format début:fin + Voir l'aide de la substitution $(x&d:f) + Ex: STR substr dorémifa 1:3,5:7 --> orif + STR substr dorémifa 2:-4 --> ré + - REPLACE : -> chaîne à occurences remplacées selon patron regexp + et chaîne de remplacement. + Ex: STR replace doréeemi `rée+` sol --> dosolmi + - CUT : -> chaîne tronquée à la taille n (y ajoute la chaîne s) + Ex: STR trunc "la belle bleue" 11 "..." --> la belle... + - EMPTY? : -> 1 si la chaîne est vide, 0 sinon + - NEMPTY? : -> 1 si la chaîne est non vide, 0 sinon + - HAS? : -> 1 si la chaîne 1 contient la 2, 0 sinon + - DIGIT? : -> 1 si la chaîne ne contient que des chiffres, 0 sinon + - NUM? : -> 1 si la chaîne ressemble à un nombre, 0 sinon + Note : le patron recherché est le format anglosaxon. + - ALPHA? : -> 1 si la chaîne ne contient que des lettres, 0 sinon + - ALNUM? : idem mais pour des caractères alpha-numériques + - LIST? : -> 1 si la chaîne ressemble à une liste, 0 sinon + - DICT? : -> 1 si la chaîne ressemble à un dictionnaire, 0 sinon + - DATE? : -> 1 si la chaîne ressemble à une date au format indiqué, + 0 sinon. Par défaut : format dd/MM/yyyy. Documentation sur + https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html + Ex: STR date? 12/19/2019 --> 0 + STR date? 12/19/2019 MM/dd/yyyy --> 1 + - UPPER : -> chaîne en majuscules + - LOWER : -> chaîne en minuscules + - CAPIT : -> chaîne capitalisée (1 lettre en maj, le reste en min.) + - TRIM : -> chaîne sans les espaces initiales et terminales + - LTRIM : -> chaîne sans les espaces initiales + - RTRIM : -> chaîne sans les espaces terminales + - TRIMALL : -> chaîne sans les espaces initiales, terminales et une + seule espace restante entre les mots dans la chaîne + Ex: STR trimall " do ré mi " --> do ré mi + Les commandes *TRIM* suppriment espaces, tab, sauts de lignes + - LPAD : -> chaîne préfixée des car. fournis (ou ' ') jusqu'à n + Si n est plus petit que la chaîne, la chaîne est inchangée. + Ex: STR lpad 42 4 0 --> 0042 + STR lpad abc 5 " " --> " abc" + - RPAD : -> chaîne suffixée des car. fournis (ou ' ') jusqu'à n + Si n est plus petit que la chaîne, la chaîne est inchangée. + Ex: STR rpad abc 5 " " --> "abc " + - STARTS? : -> 1 si la chaîne 1 commence par la chaîne 2, 0 sinon + - ENDS? : -> 1 si la chaîne finit par la chaîne 2, 0 sinon + - COMP : -> résultat de la comparaison chaîne 1 <=> chaîne 2 + Ex: STR comp abc abd --> 1 + - COMP-IC : idem, mais insensible à la casse + - MATCHES? : -> 1 si la chaîne correspond au patron regexp fourni, + 0 sinon. Voir la documentation de la classe Pattern sur + https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html + Ex: STR matches? bonjour `.*our` --> 1 + - SPLIT : coupe la chaîne par le car. regexp fourni, -> list + Ex: STR split "do,ré mi,fa" , --> do {ré mi} fa + - CONCAT : concatène toutes les chaînes en argument + - REPEAT : répète la chaîne n fois (n fourni) + Ex: STR repeat "salut " 3 --> salut salut salut + - FIRST : -> n premier(s) caractère(s) de la chaîne, défaut 1 + Ex: STR first hello --> h + STR first hello 2 --> he + - LAST : -> n dernier(s) caractère(s) de la chaîne, défaut 1 + - CHOP : -> chaîne sans le dernier caractère + Ex: STR chop a,b,c, --> a,b,c + - REVERSE : -> chaîne inversée + - FIND : -> list d'index dans la chaîne du patron regexp fourni + Ex: STR find "moi et toi" `oi` --> 1 8 + - GROUPS : -> dict de tous les groupes du patron regexp fourni + le dict. contient les groupes numérotées de 1 à n plus 0 + (toute la correspondance) + Ex: STR groups "21/03/2012" `(\d{2})/(\d{2})/\d{4}` + - FORMAT : -> chaîne formatée sur le modèle de printf en C. Doc. + https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html + Ex: STR format "salut %s" toi --> salut toi + - CONVERT : -> chaîne dont les caractères de l'ensemble A sont + remplacés par ceux de l'ensemble B (peuvent être encadrés + par ``). Un ensemble est formé par des caractères (abcd) + ou une suite (a-d), qui peut être inversée (d-a). + Ex: STR convert "salut" `aeiou` `mnopq` --> smlqt + STR convert ebb1aa492faae7 `a-z` `A-Z` -- comme upper + STR convert $s `àéèêîôùû` `aeeeiouu` -- suppr. accents + - CLEANSQL : nettoie un code LunaSQL en lui retirant les espaces en + trop et les commentaires. De plus, si le 2ème arg. est : + 0 : le code est ni formaté (sauts de ligne) ni indenté + 1 : le code est formaté mais non indenté + 2 : le code est formaté et indenté (défaut) + Ex: STR cleansql "if 1 {print /* foo */ bar}" -- testez ! + - WRAP : -> chaîne formatée en paragraphes, défaut 70 car./ligne, + avec ou sans marge à gauche et retrait de coupure. + arguments : taille de ligne désirée, chaîne représentant la + marge (défaut: rien), mode de retrait de 1ère ligne + (0: aucun (défaut), 1: droite, -1: gauche). + Les sauts de lignes sont respectés. Si des espaces finissent + la chaîne, ils servent de modèle pour supprimer les retraits + (le retrait final est supprimé à chaque début de ligne). + Ex: STR wrap $longTexte -- lignes de 70 car. sans retrait + STR wrap $longTexte 60 $ 0 + Nunc vero inanes flatus quorundam vile esse quicquid + extra urbis pomerium nascitur aestimant praeter orbos et + caelibes, nec credi potest qua obsliberis Romae. + STR wrap $s 60 $ 1 + Nunc vero inanes flatus quorundam vile esse quicquid + extra urbis pomerium nascitur aestimant praeter orbos et + caelibes, nec credi potest qua obsliberis Romae. + STR wrap $s 60 $ -1 + Nunc vero inanes flatus quorundam vile esse quicquid extra + urbis pomerium nascitur aestimant praeter orbos et caelibes, + nec credi potest qua obsliberis Romae. + - ASC : -> code ASCII du caractère fourni + - CHR : -> caractère du code ASCII fourni + - NEXT : -> chaîne incrémentée. Ex 'ab' -> 'ac' + - B64ENC : -> chaîne encodée en base64. La longueur de ligne base64 + peut être fournie en 2ème arg (minimum 4). + Ex: STR b64enc "içi c'est l'été" --> acOnaSBjJ2VzdCBsJ8OpdMOp + Ex: STR b64enc "içi c'est l'été" 12 --> (idem sur 2 lignes) + - B64DEC : -> chaîne base64 décodée + - HEXENC : -> chaîne encodée en hexadécimal. La longueur de ligne + hexadécimale peut être fournie en 2ème arg (minimum 2). + - HEXDEC : -> chaîne hexadécimale décodée + - VIGENC : -> chaîne encodée par Vigenère (clef de 3 car. préfixée) + - VIGDEC : -> chaîne Vigenère décodée + Note : taux d'erreur vigenc/vigdec d'environ 8 pour 1000 + - ZIPENC : -> chaîne gzippée encodée en base64 (cf. B64ENC) + Ne prend pas en charge tous les caractères, ni \r\n\t + - ZIPDEC : -> chaîne gzippée décompressée (entrée en base64) + - CODE : -> hashcode Java hexadécimal de la chaîne (<= 32 bits) + - HASH : -> empreinte CRC32 hexadécimale de la chaîne (32 bits) + - DIGEST : -> empreinte SHA-1 de la chaîne selon format fourni + 'hex' (défaut), 'b64', sinon binaire (160 bits) + - LEVEN : -> distance de Levenstein entre deux chaînes + - NORM : -> chaîne normalisée (sans accents) + - ESCAPE : -> chaîne avec ' échappés par le 2nd arg. (défaut '\') + Ex: STR escape "c'est l'autre" --> c\'est l\'autre + STR escape "c'est l'autre" \\ --> idem (notez '\\') + STR escape "c'est l'autre" $ --> c^'est l^'autre + STR escape "c'est l'autre" \^$ --> c$'est l$'autre + - FROMCSV : -> liste correspondant à la (aux) ligne(s) CSV lue(s) + avec sép:; délim:" échap:\ début:0 + Ex: STR fromcsv {"do";"ré";"mi fa"} --> do ré {mi fa} + + Notes : les opérations fonctionnent sur les valeurs et non sur les + références. Il n'y a pas de modification de variable. + Il est possible et recommandé d'encadrer les regexp par ``. + Attribue à _RET_VALUE la valeur générée par la sous commande. diff --git a/src/lunasql/doc/cmdtest.txt b/src/lunasql/doc/cmdtest.txt new file mode 100644 index 0000000..578ac3f --- /dev/null +++ b/src/lunasql/doc/cmdtest.txt @@ -0,0 +1,59 @@ + TEST : Commande de test de nullité ou falsitude des arguments + Chaque argument est testé, et s'il est vide, égal à 0 (nul) ou + 'false', une erreur est levée. + TEST : test la nullité de la chaîne + TEST -n : affecte un numéro au test + TEST -d : affecte une description au test + Exemple : TEST -n=23 -d="va planter" 1 2 "" -- échoue ! + + Exemple complet d'utilisation sur une batterie de tests : + (Note : exemple pédagogique. Préférer l'utilisation de la macro + "run-tests" de la même bibliothèque) + +----------------------- code ----------------------------------- + + -- on veut tester la substitution de sous-chaîne par $(s&...) + def -l testx { + arg s v + let notest++ + print -n " - test $[notest] " + test -n=$[notest] -d="attendu : $v, obtenu : $s" $['$s'=='$v'] + print -c2 ok + } + + let notest=0 + def -l s abcdefgh + -- 01234567 l=8 + + /* 01 */ testx $(s&:) abcdefgh + /* 02 */ testx $(s&0:) abcdefgh + /* 03 */ testx $(s&1:) bcdefgh + /* 04 */ testx $(s&2:) cdefgh + /* 05 */ testx $(s&3:) defgh + /* 06 */ testx $(s&:4) abcd + /* 07 */ testx $(s&:6) abcdef + /* 08 */ testx $(s&:8) abcdefgh + /* 09 */ testx $(s&:10) abcdefgh + /* 10 */ testx $(s&1:7) bcdefg + /* 11 */ testx $(s&1:6) bcdef + /* 12 */ testx $(s&1:5) bcde + /* 13 */ testx $(s&2:-1) cdefg + /* 14 */ testx $(s&2:-2) cdef + /* 15 */ testx $(s&2:-10) cba + /* 16 */ testx $(s&0:10) abcdefgh + /* 17 */ testx $(s&-2:) gh + /* 18 */ testx $(s&-4:) efgh + /* 19 */ testx $(s&-2:-6) gfed + /* 20 */ testx $(s&-2:2) gfed + /* 21 */ testx $(s&-1:0) hgfedcb + /* 22 */ testx $(s&2:10) cdefgh + /* 23 */ testx $(s&6:2) gfed + /* 24 */ testx $(s&7:0) hgfedcb + /* 25 */ testx $(s&10:0) hgfedcb + /* 26 */ testx $(s&0,2,7) ach + + print -c2 "$Tous les tests OK" + +---------------------------------------------------------------- + + Attribue 1 à _RET_VALUE en cas de succès, lève une erreur sinon. diff --git a/src/lunasql/doc/cmdtime.txt b/src/lunasql/doc/cmdtime.txt new file mode 100644 index 0000000..34a905b --- /dev/null +++ b/src/lunasql/doc/cmdtime.txt @@ -0,0 +1,71 @@ + TIME : Outils de gestion/formatage de date et heure. + TIME commande [options] + Le format de date/heure ou la donnée au format date/heure peut + être encadré par rien (si aucun espace), "" ou '' (dates). + Le format par défaut pour NEW, FORMAT, DIFF, ADD, AFTER? et + BEFORE? est : 'dd/MM/yyyy' ou 'dd/MM/yyyy HH:mm:ss'. + Le cas échéant le préciser en argument, ainsi que la "locale" + parmi : FR, DE, IT, ZH, TW, CA, CAF (français), US, UK, JP, KO + Doc sur le format : https://docs.oracle.com/javase/8/docs/api/ + + Commandes (insensibles à la casse) et valeurs de retour : + - NEW : -> temps en ms de la date au format et locale donnés + Ex: TIME new '24/04/2015' --> 1429910000000 + TIME new '24/04/2015' 'dd/MM/yyyy' --> 1429910000000 + TIME new '24/04/2015' 'dd/MM/yyyy' FR --> 1429910000000 + - FORMAT : formate et retourne la date donnée selon format donné + (défaut : 'dd/MM/yyyy HH:mm:ss') + Ex : TIME format 1429910000000 --> 24/04/2015 23:13:20 + TIME format 1429910000000 'yyyy-MM-dd' --> 2015-04-24 + - NOW : -> temps système en ms (currentTimeMillis()) ou formaté + Ex: TIME now --> 1429910000000 + TIME now 'dd/MM/yyyy' --> 24/04/2015 + - INIT : déclenche un chronomètre (initialisation à 0) + - CHRON : déclenche et retourne un temps d'exécution. Affecte + le résultat en ms à _EXEC_TIME + - DATE : -> date courant au format JJ/MM/AAAA + équivalent à : TIME now 'dd/MM/yyyy' + - TIME : -> heure au format HH24:MM:SS + équivalent à : TIME now 'HH:mm:ss' + - DATETIME : -> date au format JJ/MM/AAAA HH:MM:SS + équivalent à : TIME now 'dd/MM/yyyy HH:mm:ss' + - DATETIMEMS : -> date au format JJ/MM/AAAA HH:MM:SS:MS + équivalent à : TIME now 'dd/MM/yyyy HH:mm:ss:SS' + - COMPACT : -> date au format JJMMAAAAHHMMSS (pour identifiants) + équivalent à : TIME now yyyyMMddHHmm + - ADD : -> ajout d'un nombre ms à une date au format spécifié + Ex: TIME add '15/01/2001' -259200000 --> 979254000000 + TIME add '01/15/2001' -259200000 'MM/dd/yyyy' + - DIFF : -> différence entre deux dates au format spécifié en ms + Ex: TIME diff 15/01/2001 15/02/2001 --> 2678400000 + TIME diff '15/01/2011 09:52' '15/01/2011 10:00' ^ + 'dd/MM/yyyy HH:mm' --> 480000 + - AFTER? : -> 1 si la 1re date est après la 2ème, 0 sinon + Ex: TIME after? '15/01/2001' '30/01/2001' --> 0 + TIME after? 15/01/2001 30/01/2001 'MM/dd/yyyy' --> 0 + - BEFORE? : -> 1 si la 1re date est avant la 2ème, 0 sinon + - AT : exécute en asynchrone le bloc de code à la date donnée + en ms. Si l'heure est passée, pas d'éxécution du bloc. + Ex: TIME at '08/09/2017 12:30' { print "Il est 12h30 !" } + - AFTER : exécute en asynchrone le bloc de code après le délai + exprimé en heure/min/sec/millisec selon h (heures), + m (minutes), s (secondes), l (millisecondes). + Ex: TIME after 1h25m30s { print "Temps écoulé !" } + - REPEAT : exécute en mode asynchrone le bloc de code après chaque + délai et n fois, délai s'exprimant comme AFTER + Si le délai est 0, l'exécution est (presque) illimitée + Ex: TIME repeat 5m35s400l 10 { print ok } + -- affiche "ok" toutes les 5min 35sec 400ms, 10 fois + - WAIT : attend le délai indiqué (synchrone) avant de poursuivre, + délai s'exprimant comme AFTER + Ex: time wait 1m30s + - DELAY : -> delai s'exprimant comme AFTER converti en ms + Ex: TIME delay 2h14m50s --> 8090000 + + À la différence de WAIT, AT, AFTER et REPEAT sont asynchrones et + ne retournent rien (car le bloc est exécuté dans un autre Thread). + Cf. la remarque en commande START pour l'asynchrone. + + Note : le paquet 'const' définit des constantes pour cette commande. + + Attribue à _RET_VALUE la valeur générée par la sous commande diff --git a/src/lunasql/doc/cmdtree.txt b/src/lunasql/doc/cmdtree.txt new file mode 100644 index 0000000..db908d1 --- /dev/null +++ b/src/lunasql/doc/cmdtree.txt @@ -0,0 +1,24 @@ + TREE : Affiche un arbre des clefs étangères d'une table + TREE table Affiche l'arbre en profondeur 3 + TREE table n Affiche l'arbre en profondeur n + + L'arbre de références des clefs étrangères est créé pour la table à + partir de ses clefs qui sont référencées dans d'autres tables. + + Avec l'option -d, génère un digraph au format DOT GraphViz + (voir http://graphviz.org). + En cas de référence circulaire, '(...)' est ajouté à la ligne. + Ne fonctionne pas sur tous les SGBD (ou bien c'est un bug ?). + + La représentation de base est du type (ex. TREE TYPESPRO): + TYPESPRO(ID) + | + +-- PRODUITS(IDTYP) + PRODUITS(ID) + | + +-- COMMANDES(IDPRO) + PRODUITS(ID) + | + +-- RAPPORTS(IDPRO) + + Ne modifie pas la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdtruncate.txt b/src/lunasql/doc/cmdtruncate.txt new file mode 100644 index 0000000..0dac6d9 --- /dev/null +++ b/src/lunasql/doc/cmdtruncate.txt @@ -0,0 +1,6 @@ + TRUNCATE : Purge le contenu d'une table + Vérifiez si TRUNCATE est disponible sur votre SGBD. + TRUNCATE TABLE + + Attribue 0 aux constantes _RET_VALUE et _RET_NLINES. + \ No newline at end of file diff --git a/src/lunasql/doc/cmdundef.txt b/src/lunasql/doc/cmdundef.txt new file mode 100644 index 0000000..3188853 --- /dev/null +++ b/src/lunasql/doc/cmdundef.txt @@ -0,0 +1,16 @@ + UNDEF (-) : Supprime une variable globale de l'environnement + UNDEF [opt] Supprime les var. de la liste liste_var + 0ptions : + -a Supprime toutes les var. globales non systèmes sans état + d'âme ni message de confirmation + -d Aucune erreur si la var. n'est pas déclarée + -f=s Recherche les variables dont le nom correspond à + l'expression régulière s et les supprime. + L'expr. régulière s peut être encadrée par ` et `. + La variable doit être interne. Aucun effet sur variables SE. + Une variable système (commençant par _) ne peut être supprimée + + Exemple : + UNDEF -f ab.* -- Supprime les var. commençant par 'ab' + + Attribue le nombre de var. supprimées à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdupdate.txt b/src/lunasql/doc/cmdupdate.txt new file mode 100644 index 0000000..88253af --- /dev/null +++ b/src/lunasql/doc/cmdupdate.txt @@ -0,0 +1,4 @@ + UPDATE : Met à jour des données de la base + UPDATE table SET column = Expression [, ...] [WHERE Expression] + + Attribue le nombre de lignes insérées à _RET_VALUE et à_RET_NLINES. diff --git a/src/lunasql/doc/cmduse.txt b/src/lunasql/doc/cmduse.txt new file mode 100644 index 0000000..047440f --- /dev/null +++ b/src/lunasql/doc/cmduse.txt @@ -0,0 +1,74 @@ + USE (|) : Charge une seule fois une bibliothèque en environnement + USE lib1 lib2... + Le fichier contient du code LunaSQL et porte l'extension .(l)sql + ou .luna : typiquement cette commande sert à charger des alias/macros + en env. global (définition par defmacro). + Il est aussi possible de charger une lib SE (javascript...), selon le + moteur SE en cours. + Elle est tout d'abord recherchée parmi les bibliothèques contenues + dans l'archive LunaSQL (/lunasql/misc/), puis dans le classpath + (/lunasql/jsextras/ dans un autre fichier jar), et enfin, comme la + commande EXEC, dans les répertoires de la constante :SCRIPTS_PATH + ou en chemins absolus. + Si la bib. est interne et sql, on peut omettre l'extension. Voir + l'aide PACKAGES pour les bibliothèques disponibles. Si la bib. est + externe, c'est la commande EXEC qui la charge finalement, mais dans + ce cas, un seul fichier est admis en argument. Voir l'aide de EXEC. + + Modules disponibles à charger : cf help packages + Variables disponibles en module (en plus des var. de portée) : + - script_name : nom du fichier script ( = avec .sql) + - this_scope : nom de la bibliothèque (comme script_name) + Exemples : USE crypt sys math-base + Les bib. chargées sont ajoutées à la constante _LOADED_LIBS, qui est + facilement interrogeable par la macro interne 'loaded'. + + Optionnellement, USE peut aussi appeler du code Java : + Charge des bibliothèques de classes en CLASSPATH, ou invoque une + méthode depuis la classe ou l'objet indiqué(e). + USE [-j=f.jar] class -i=new|static + [-c=tp -r=args] : avec -i=new, instantiation + [-m=methode [-a=args -t=tp]] : appel de méthode + charge depuis f.jar la classe class et invoque la méthode met + (peut être 'main') de la classe cl avec le mode new ou static. + La méthode prend les arguments args de types tp (sép: ',') + + Exemples complets : + + -- chargement de fichiers LunaSQL + USE crypt sys math-base + USE /home/teddy/path/to/file.sql + -- mettons que le fichier conf.sql renvoie un dict par PUT + def cfg $[USE conf.sql]; print $(cfg,key1) + -- notez que cet appel ne pourra se faire qu'une fois, car + -- au 2ème appel, USE renverra la valeur 0 + + -- chargement de code Java + -- ici on utilise JEval (jeval.sourceforge.net) + USE -j=jeval-0.9.4.jar net.sourceforge.jeval.Evaluator -i=new ^ + -m=evaluate -a=(6+3)*5 -t=java.lang.String --> 45 + USE java.lang.Integer -i=static -m=parseInt -a=8104 ^ + -t=java.lang.String --> 8104 + USE java.lang.System -i=static -m=currentTimeMillis + --> 1448649882807 (ex.) + USE java.io.File -i=new -r=config.cfg -c=java.lang.String ^ + -m=length --> 825 (ex.) + + Notes : + - les bib. internes renvoient 1 si la lib. est chargée avec succès + - si un fichier externe porte le nom d'une bib. interne, comme + test.sql (bib. test), c'est la bib. interne qui est chargée, + sauf si le fichier est appelé par son chemin (path/to/test.sql) + - les classes sont appelées par leur nom complet + ex: java.lang.Integer, java.lang.System + - un autre moyen d'utiliser la réflexion est de passer par le + moteur d'évaluation (SE) courant, s'il le permet (ex: JS) + ex: new java.io.File('config.cfg').length() -- est à préférer à : + USE java.io.File -i=new -r=config.cfg -c=java.lang.String ^ + -m=length + + Attribue la valeur de RETURN (ou de PUT en dernière commande, ou le + résultat de la dernière évaluation) du script LunaSQL à la constante + _RET_VALUE, mais lui attribue 0 si la bib. était déjà chargée. En cas + de chargement de code Java, attribue à _RET_VALUE la valeur retournée + par la fonction appelée (option -m). diff --git a/src/lunasql/doc/cmdverb.txt b/src/lunasql/doc/cmdverb.txt new file mode 100644 index 0000000..8e81a16 --- /dev/null +++ b/src/lunasql/doc/cmdverb.txt @@ -0,0 +1,10 @@ + VERB (,) : Attribue au systême une valeur de verbose ou l'affiche + VERB Affiche le niveau actuel de verbose + VERB n|lib Attribue au systême la valeur de verbose n ou + le libellé lib n entre 0 et 4, ou +1,-2... + lib parmi : SILENCE, DISPLAY, MESSAGE, CHATTY, DEBUG + Exemple : VERB CHATTY + + Note : le paquet 'const' définit des constantes pour cette commande. + + Attribue le numéro de verbose fixé/affiché à la constante _RET_VALUE. diff --git a/src/lunasql/doc/cmdverify.txt b/src/lunasql/doc/cmdverify.txt new file mode 100644 index 0000000..952b317 --- /dev/null +++ b/src/lunasql/doc/cmdverify.txt @@ -0,0 +1,31 @@ + VERIFY : Vérifie la signature numérique créée par la commande SIGN + VERIFY Vérifie le message avec le dictionnaire de + signature . + VERIFY -f [-d] Vérifie le fichier . Avec l'option -d + le fichier peut être de tout type et la signature est lue + depuis le fichier .sig, sinon, le fichier texte contenant + la signature est vérifié. + + La politique de sécurité contenue en option :SIGN_POLICY définit la + confiance accordée dans la clef de signature. Si la vérification + réussit, si :SIGN_POLICY vaut : + 0: la confiance n'est pas vérifiée, + 1: si la clef n'est pas de confiance, une alerte est imprimée, + 2: si la clef n'est pas de confiance, une erreur est levée. + Si la vérification échoue, une erreur est levée dans tous les cas. + + Pour ajouter ou supprimer une clef publique à la liste des cles de + confiance, il suffit d'éditer manuellement l'option :SIGN_TRUST + (le séparateur est la virgule ','). + + Exemples : + opt :SIGN_TRUST $(:SIGN_TRUST),Abc01...xyZ= -- ajout de clef + def s $[sign "message important"] + VERIFY "message important" $s --> 1601052179215 (OK) + VERIFY -f script.sql -- signature incluse en script.sql + VERIFY -f script.sql -d -- lecture de script.sql.sig + time format $[VERIFY -f script.sql -d] --> 26/09/2020 15:53:32 + -- ou erreur si la signature est erronée ou invalide + + Attribue la date de signature (en ms epoch) à la constante + _RET_VALUE si la vérification réussit, sinon lève une erreur. diff --git a/src/lunasql/doc/cmdview.txt b/src/lunasql/doc/cmdview.txt new file mode 100644 index 0000000..939375c --- /dev/null +++ b/src/lunasql/doc/cmdview.txt @@ -0,0 +1,15 @@ + VIEW : Affiche le résultat d'une requête SELECT ou d'une table en + JTable (avec composant Swing) + VIEW tbl : Liste le contenu de la table tbl et l'affiche + VIEW req : Exécute la requête SELECT req et l'affiche + Exemple : VIEW select * from test + + La taille des données retournées est limitée à 200 lignes et une + erreur est levée si plus de 16 colonnes. + Notes : - en mode HTTP, cette commande n'est pas autorisée, + - bien que les cellules soient éditables, il n'y a pas de + modification des données dans les tables. + + + Contrairement à SELECT, ne modifie pas la constante _RET_VALUE + Attribue le nombre de lignes retournées à la constante _RET_NLINES. diff --git a/src/lunasql/doc/cmdvoid.txt b/src/lunasql/doc/cmdvoid.txt new file mode 100644 index 0000000..cce579e --- /dev/null +++ b/src/lunasql/doc/cmdvoid.txt @@ -0,0 +1,10 @@ + VOID (.) : Ne fait absolument rien ! + Les sustitutions sont tout de même réalisées. Cela permet par + exemple d'exéuter du code silentieusement. + Exemple: + def reponse 42; VOID $(reponse#inc!) --> reponse = 43 + Comparer : + VOID $[return exécuté] + VOID $[print exécuté] + + Ne modifie pas la constante _RET_VALUE, mais les substitutions oui. diff --git a/src/lunasql/doc/cmdwait.txt b/src/lunasql/doc/cmdwait.txt new file mode 100644 index 0000000..4dce45e --- /dev/null +++ b/src/lunasql/doc/cmdwait.txt @@ -0,0 +1,22 @@ + WAIT (~) : Attend le temps indiqué ou une frappe au clavier en + affichant un message + WAIT délai [message] + WAIT 'date-heure' [message] + Le délai est exprimé en millisecondes. S'il est négatif ou nul, + une frappe de la touche ENTREE est attendue pour continuer. + Si le premier argument est au format 'dd/MM/yyyy HH:mm:ss:SS' + (avec apostrophes), le délai est calculé pour attendre jusqu'à + l'heure indiquée. + + Exemples : + WAIT 2500 "Traitement terminé" + WAIT 0 "Taper ENTREE pour continuer" + WAIT '05/02/2017 18:20:00:00' "On attend cette heure précise" + -- Exécution après un moment (version synchrone) : + def after {WAIT $arg1; eval $arg2}; after 1000 {print ok} + def at {WAIT '$arg1'; eval $arg2} -- $arg1 est une heure + -- Exécution après un moment (version asynchrone) : + def after {start - "WAIT $arg1; eval $arg2"} + -- préférez bien sûr utiliser TIME AFTERDO et TIME ATDO + + Attribue à _RET_VALUE le temps d'attente en ms diff --git a/src/lunasql/doc/cmdwhen.txt b/src/lunasql/doc/cmdwhen.txt new file mode 100644 index 0000000..7044192 --- /dev/null +++ b/src/lunasql/doc/cmdwhen.txt @@ -0,0 +1,36 @@ + WHEN : Teste une expression et positionne le contexte d'exécution + des commandes suivantes. Il est inversé par ELSE et terminé + par END. Les WHEN/ELSE/END peuvent être imbriqués. + WHEN ; + command1 param1a param1b ...; + ELSE; + command2 param2a param2b ...; + END; + 'expr_bool' est évaluée par le moteur SE courant : avec le SE par + défaut javascript, elle peut contenir && (et), || (ou), ! (non), + parenthèses... et peut être encadrée par [] ou {}. + + Différence avec la commande IF : + WHEN est un modificateur d'exécution basée sur le contexte. Un WHEN + évalué à faux interdit l'exécution des commandes avant un ELSE ou END. + WHEN ne crée par de nouveau lecteur : les var. locales sont dans le + même contexte. + Au contraire, IF est une commande qui prend un bloc code à exécuter, + et qui lance donc un nouveau lecteur (analyseur) pour chaque bloc + évalué. + + Notes : + - aucune substitution n'est réalisée dans un bloc WHEN non évalué + - 'expr_bool' peut contenir des substitutions par $ + - en bloc WHEN ne pas utiliser les commandes BREAK, RETURN et EXIT + - éviter d'ouvrir un WHEN pour le fermer en bloc différent... + + Exemples: + if 1 { if 1 { if 1 { print foo } } } -- une seule commande + WHEN 1; WHEN 1; WHEN 1; print foo; END; END; END; -- contexte + if 1 {def -l a 1}; print $a -- erreur : a n'est pas défini ! + WHEN 1; def -l a 1; END; print $a -- a est défini + WHEN 0; print $(indefinie); END -- pas d'erreur + WHEN 0; voilà du texte brut comme un commentaire; END + + WHEN/ELSE/END ne modifient pas _RET_VALUE ni _CMD_STATE. diff --git a/src/lunasql/doc/cmdwhile.txt b/src/lunasql/doc/cmdwhile.txt new file mode 100644 index 0000000..4bbd4f0 --- /dev/null +++ b/src/lunasql/doc/cmdwhile.txt @@ -0,0 +1,37 @@ + WHILE : Teste une expression et boucle sur les arguments + WHILE [ELSE ] + 'expr_bool' est évaluée par le moteur SE courant : avec le SE par + défaut javascript, elle peut contenir && (et), || (ou), ! (non), + parenthèses... Elle peut être encadrée par "", [] ou {} pour grouper + l'expression en un seul argument. + 'bloc' doit être déclaré par {} ou "" car il doit aussi former un seul + argument. Le bloc ELSE est évalué si 'expr_bool' est fausse dès la + première itération. + + Exemples : + var i -- i est une variable du SE par défaut (js) + i=0; WHILE "i++ < 5" { print i=$[i]; print ok } + i=5; WHILE [i++ < 5] { print ko } else { print i=$[i]; print ok } + def i 0; WHILE {$(i#inc!)<5} { print i=$i; print ok } + i=0; while i++<3 {j=0; while j++<3 {print $[i] $[j]}} + + Notes : + - les substitutions $ seront peut-être réalisées *en amont* + cf. help substitutes selon usage de "", (), {}, prendre l'habitude + d'encadrer les blocs par {}. + - 'expr_bool' peut contenir des variables LunaSQL, mais attention à + l'encadrer par {} pour différer la substitution. + - les vars. locales déclarées en boucle sont accessibles pour tous + les tours de la boucle. + - l'environnement local contient les options suivantes : + _LOOP_DEEP : profondeur actuelle de boucle + _LOOP_BREAK : profondeur à l'appel à BREAK (fac.) + + Attention : 'expr_bool' doit être évalué à chaque tour, ce qui n'est + pas le cas si utilisation d'une variable de LunaSQL (hors SE). + Exemple de boucle qui tue car '$(a)' n'est évalué qu'une fois : + def a 1 2 3; WHILE '$(a)'!='' {print $(a#pshift)} + + Ne modifie pas les constantes _RET_VALUE et _CMD_STATE (sauf erreur), + mais les commandes exécutées peuvent les modifier. Attribue 0 à + _RET_VALUE si aucun bloc exécuté. diff --git a/src/lunasql/doc/cmdxml.txt b/src/lunasql/doc/cmdxml.txt new file mode 100644 index 0000000..9b40390 --- /dev/null +++ b/src/lunasql/doc/cmdxml.txt @@ -0,0 +1,64 @@ + XML : Analyse un objet XML et le parcourt par XPath + XML -s|-f= Lit le code xml et applique XPath + XML -q= [] Exécute le code SQL (SELECT) ou liste la + table et sans retourne le XML ou applique XPath. + + Documentation pour les requêtes XPath : + https://docs.oracle.com/javase/tutorial/jaxp/xslt/xpath.html + Il est recommandé d'encadrer les données par {}. Les requêtes peuvent + être encadrées par {} ou par [], ou bien non-encadrées si elles + forment un seul argument sans caractère spécial. + En cas de requêtes multiples (groupées ou non), les résultats sont + retrournés sous forme de liste. + + Options : -s= document XML en chaîne + -f= document XML en fichier + -q= commande SQL SELECT ou nom de table à lister + -m plusieurs requêtes sont groupées en 1 argument + (liste(s) de requêtes) + + Avec l'option -q, la requête SQL est exécutée et le XML est généré + selon le modèle suivant (exemple listant le contenu d'une table TEST + à 4 champs : ID, NOM, AGE, SEX) : + + + + 1 + Zoé + 28 + F + + ... + + Il est ensuite possible de sauvegarder ce code XML en variable par + DEF pour un usage ultérieur avec XML -s, ou bien de l'utiliser en + place par une requête XPath, comme : + XML -q {SELECT * FROM TEST} [//recordset/record[@rownum="1"]/NOM] + --> Zoé + Note : pour requêter directement des données d'une base, préférer la + commande SEEK (ou CSV). + + Exemples : + XML -s={ + hellosalut + } //a/b[@l="fr"] --> salut + + XML -s={ + the bigred hat + } //a/b[@id="2"] //a/b[@id="1"] --> {red hat} {the big} + + XML -q={SELECT * FROM TEST} --> XML complet + XML -q={SELECT * FROM TEST} [//recordset/record[@rownum="1"]/NOM] + + Plus tordu, définition de variables par DEF sur multi-requêtes : + def r1,r2 ~$[ XML -s={ + the bigred hat + } //a/b[@id="2"] //a/b[@id="1"] ] + -- r1 <- red hat, r2 <- the big + -- notez bien l'utilisation de ~ pour dégrouper le retour de la + -- substitution $[] en deux arguments pour r1 et r2 + + XML -f=donnees.xml [//a/b[@l="de"]] -- req. encadrée par [] + XML -m -f=donnees.xml { //a/b[@l="fr"] //a/b[@l="en"] } -- ou {} + + Attribue le résultat à la constante _RET_VALUE. diff --git a/src/lunasql/doc/complwords.txt b/src/lunasql/doc/complwords.txt new file mode 100644 index 0000000..46dfc1e --- /dev/null +++ b/src/lunasql/doc/complwords.txt @@ -0,0 +1,31 @@ +# Complètement de mots pour l'éditeur FrmEditScript et HttpConsole +# Modèle de déclaration : mot-clef:texte ajouté au mot-clef + +# SQL +CREATE:TABLE myTab (id INT PRIMARY KEY, name VARCHAR(255) DEFAULT NULL) +create:table myTab (id int primary key, name varchar(255) DEFAULT NULL) +SELECT:col1 FROM tab1 WHERE conditions +select:col1 from tab1 where conditions +INSERT:INTO tab1 VALUES (1, 'val1') +insert:into tab1 values (1, 'val1') +UPDATE:tab1 SET id=1, name='val1' WHERE id=0 +update:tab1 set id=1, name='val1' where id=0 +DELETE:FROM tab1 WHERE id=0 +delete:from tab1 where id=0 + +# LunaSQL +if:[cond] {\n -- code\n} elseif [cond] {\n -- code\n} else {\n -- code\n} +when:[cond]\n -- code\nelse\n -- code\nend +case:$s v1 {\n -- code\n} v2 {\n -- code\n} else {\n -- code\n} +for:e {liste} {\n -- code $e\n} +while:[cond] {\n -- code\n} else {\n -- code\n} +eval:{\n -- code\n} -e=err -c {\n -- $err_cmd, $err_msg, $err_lng\n} +def:name val +defm:name {args} {\n -- code $args;\n \n} "\n description\n exemple\n " +defmacro:name {args} {\n -- code $args;\n \n} "\n description\n exemple\n " +defr:name {id:nat desc} +defrecord:name {id:nat desc} +with:[x = 1\n y = 2] {\n -- code x, y;\n} + +# Autres +Lorem:ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/src/lunasql/doc/delimiters.txt b/src/lunasql/doc/delimiters.txt new file mode 100644 index 0000000..4c2557e --- /dev/null +++ b/src/lunasql/doc/delimiters.txt @@ -0,0 +1,74 @@ +Délimitation de chaînes et blocs de code. Plusieurs façons de faire : + +- Chaînes dures +Les chaînes encadrées par { et } ou $$ et $$ sont dites "dures", +c'est-à-dire que rien n'est échappé ni substitué, mais les {} en début +et fin de chaîne sont supprimés. Les délimiteurs $$ sont conservés +(compatibilité avec délimiteurs H2), mais les {} encadrants sont +supprimés de la chaîne. +{} sont recommandés pour DEF, IF, FOR, WHILE et CASE car elles peuvent +être imbriquées et différent les substitutions : pour délimiter tout +bloc de code, utiliser {}, qui interdisent également les substitutions. +Les $$...$$ ne peuvent pas être imbriqués, et $$$$ est invalide +(il faut insérer au moins un caractère (ex $$ $$) ou bien placer +le triplet $$$ ($$a$$$b$$ -> $$a$$ $$b$$), ou bien encadrer {$$$$}. + ex. if [ 2>1 ] { print ok; print $(x) } -- $(x) est différé + +- Chaînes douces +Les chaînes douces (ou normales) sont encadrées par "", '', [] ou ``. +Elles sont substituées mais échappent les caractères hors $ : "";... +Pour délimiter un bloc de code (bien que de loin préférable par {}), +parmi les chaînes douces, seuls "" sont autorisés. + ex. if [ 2>1 ] "print ok; print ^$(x)" -- protéger $(x) +Il peut donc être nécessaire d'échapper les $, mais pas les ; +En chaîne normale, un ^ en fin de ligne échappe le saut de ligne. +Le ^ peut aussi servir en blocs [] ou {}. + ex. if $[str len ^^]] {print "^}"} + -- notez en $[] les ^^ et en {} l'encadrement par "" + ^ +Plus d'info : cf help "substitutes" + +- Parenthèses ( et ) +Un argument encadré par ( et ) sera nettoyé des commentaires, +coupé par les points-virgules et substitué avant exécution. Les () +peuvent être imbriquées. Il n'est pas du tout protégé : les éléments +du bloc sont simplement des arguments de la commande principale. +Ne sont pas autorisées pour délimiter un bloc de code, une liste ou +un dictionnaire. + ex. print ( foo^; bar ^$(x) ) -- protéger $(x) et ; + print (;) -- invalide : échapper le ; ! + if 1 ( print ko! ) -- ne fonctionne pas ! + +Rapport entre chaînes, délimiteurs et interprétation : + def c sub -- chaîne? délim? interp? + print "a /* comm */ b $(c)" -- oui non oui + print 'a /* comm */ b $(c)' -- oui oui oui + print (a /* comm */ b $(c)) -- non - oui + print [a /* comm */ b $(c)] -- oui oui oui + print {a /* comm */ b $(c)} -- oui non non + print -- non - oui + print `a /* comm */ b $(c)` -- oui oui oui + print $$a /* comm */ b $(c)$$ -- oui oui non +L'interprétation comprend les substitutions et échappements +(ex. les ^ de caractères ou fin de lignes). + +Groupage des arguments en fonction des délimiteurs : + def nbargs { print $(arg_nb) } -- nombre d'arguments + nbargs a"b""c"d --> 4 + nbargs a$$b$$ $$c$$d --> 4 + nbargs a$$b$$$c$$d --> 4 (notez les $$$ pour $$ $$) + -- en effet "" et $$ $$ séparent les arguments + nbargs a(b)(c)d --> 1 + nbargs a[b][c]d --> 1 + nbargs a{b}{c}d --> 4 car {} séparent les arguments + nbargs a'b''c'd --> 1 + nbargs a`b``c`d --> 1 + +La séparation d'arguments par {} et "" permet en fait les structures +suivantes : + print a"b"{c}d --> a b c d + print"ok" + defmacro f"a b c"{print $a $b $c}" Aide de f..." + defmacro f{a b c}{print $a $b $c}{ Aide de f...} + +Les chaînes douces et dures délimitent donc les arguments. Pour le +groupage des arguments substitués, cf. l'aide "substitutes". diff --git a/src/lunasql/doc/etikette.txt b/src/lunasql/doc/etikette.txt new file mode 100644 index 0000000..b1ad24f --- /dev/null +++ b/src/lunasql/doc/etikette.txt @@ -0,0 +1,57 @@ +L'ÉtiKette : Guide rapide des bonnes pratiques et des conventions + +Généralités : +- indentation : 2 espaces +- commandes internes : en minuscules +- commandes SQL : en majuscules +- noms de variables, de macros et de scripts sensés (pas x, tmp...) +- noms de macros et de scripts en minuscules, mots séparés par '-' +- noms de variables en minuscules, mots séparés par '_' ou non séparés +- noms de variables du SE selon règles du SE +- noms de constantes en majuscules, mots séparés par '_' +- :END_CMD_NL à 1, :EXIT_ON_ERR à 1, :ALLOW_RECUR à 0, :ALIAS_ARG à 0 + (sauf besoins particuliers et exceptionnels) + +Écriture de scripts et définition de macros : +- utiliser les commentaires Javadoc pour la présentation +- utiliser les noms de commandes longs (pas les raccourcis) +- favoriser l'usage de variables locales (portée la plus interne) +- remplacer les valeurs littérales par des constantes +- les alias/macros font généralement moins de 30 lignes +- les lignes font généralement moins de 80 caractères +- toujours retourner une valeur (au moins de succès) +Uniquement pour les macros : +- toujours documenter (par commentaires Javadoc et doc de macro) +- préférer l'usage de defmacro à def/arg/help +- fractionner le code au maximum en plusieurs macros +- déclarer les paramètres par un point initial, ex: .param + +Exemple complet : + /** + * exemple-macro : modèle de code LunaSQL + * une brève mais précise description ne gâche rien + * @auteur M.P. + * @creation 13/01/2019 + * @version 1.0 + */ + defmacro exemple-macro {.name [.age:int]} { + -- validation des paramètres + def -l mon_nom "Nom nom est $(.name)" + def -l la_reponse $[let 42 - $(.age)] + + -- traitements + if [$(.age) > $agelimit] { + INSERT INTO MaTable (Nom, Age) VALUES ('$mon_nom', $la_reponse) + SELECT * FROM MaTable + } + } " + Documentation de exemple-macro + Cette macro est un exemple de L'ÉtiKette en LunaSQL + " + + def agelimit 25 + def personne { + nom: Alice + age: 26 + } + exemple-macro $(personne,nom) $(personne,age) diff --git a/src/lunasql/doc/expressions.txt b/src/lunasql/doc/expressions.txt new file mode 100644 index 0000000..31294bd --- /dev/null +++ b/src/lunasql/doc/expressions.txt @@ -0,0 +1,135 @@ +Expressions de substitutions : + +Insertion d'un caractère : $ avec c le code caractère. +Insertion d'une expr./cmd : $[] avec l'expr. ou la commande +Insertion d'une variable : $() avec le nom de la var. (ou $v). + +Liste des formes substituables : + $ Remplace c par le caractère correspondant, parmi : + n:nouv ligne, t:tab, e:espace, g:", q:', Q:`, c:^, + d:$, y:}, Y:{, k:], K:[, s:>, S:<, p:), P:(, + r:return, b:backspace, f:formfeed, l:cloche + ou le code ascii décimal, ou une chaîne encadrée par ''. + Pour une liste complète des codes de caractères, cf. + http://www.asciitable.pro/ascii_table.htm + ex. print $$$ + Avec c*n : le caractère est répété n fois : + ex. print |$| --> | | + print $<'salut'*3> --> salutsalutsalut + $(x) Résout la variable x. + Les macros et alias sont aussi des variables. En tant + que commande, utiliser le nom de la commande seul. + Ex. def f {print ok}; f -- et non $(f) + Si le nom de la var. est alphanumérique, on peut omettre + les (), ex. print "2x=$[2 * $x]" -- $x pour $(x) + Singularité : def a a; print $($($(a))) --> a + $[expr] Évalue l'expression expr (expr est un code du SE) + Utilisée comme une COMMANDE, le $[] est omissible + Pour le SE js taper HELP JS-FUNCT pour plus d'information + $[cmd] Évalue la commande cmd, pour être embarquée dans une autre. + Peut être une commande SQL ou interne. La valeur de retour + de la commande est retournée pour être substituée et + affectée à _RET_VALUE. + Ex. print $[call 4*5] $[select toto from matable] + Avec :END_CMD_NL à 0, nécessite de finir par un ';'. + $[] retourne la dernière valeur évaluée. + $`prompt` Demande en console 'prompt' et substitue la saisie. + Si 'prompt' commence par '?', l'invite est améliorée, + ou par '*', les caractères saisis sont affichés en '*'. + Si plusieurs 'prompt' identiques sont à substituer, un + seul est demandé. Ex. print $`?n`+$`n` + Par contre les valeurs sont internes à un lecteur : + Ex. print $`?n`+$`n`=$[$`?n`+$`n`] -- 2 demandes. + Ici il est préférable d'utiliser des var. locales... + Note : quelque soit la saisie, le contenu ne sera pas + substitué (ni exécuté). Substitution non imbricable. + Ex: $`?val` (amélioré), $`?*val` (amélioré + MDP) + + Notes : + - Les substitutions sont faites une seule fois, même si la chaîne + retournée contient elle-même des formes substituables. + - Les caractères $ et § sont échappables par '^', ex. ^$(x), + et en chaîne dure (et an WHEN 0), la substitution par $ n'est + pas réalisée. + - Commande précédente ou appelante : $(1), $(2), $(3)... sont + les arguments, $(_l) la liste et $(_n) le nombre. + Ex: void foo; print $1 --> foo + def f {print 1=$1 2=$2 nb=$_n}; f do ré --> 1=do 2=ré nb=2 + Fonctionne aussi en 1re commande de script. + Attention : la commande précédente peut être interne ! + Ex: if 1 {print $0} --> if car c'est la dernière commande + put 1 2 3; print $0 --> put + Pour les arguments d'une commande, préférer $(arg1), $(arg_ls) + - Caractères spéciaux autorisés en nom de variable : .-_ + - Quand la variable n'existe pas, une erreur est levée, sauf + si le nom de variable est préfixé par '*', exemple : $(*x) + - Il est possible de retourner une valeur en cas de variable + non définie par '?'. Ne pas utiliser de modificateurs. + Ex. print $(*x?ND) -- * pour éviter l'erreur + - Ne concerne que les variables internes appelables par $(), + et non celles appartenant au SE (ex. js) appelables par $[] + - Il peut être nécessaire d'encadrer la substitution par "", en + particulier si le modificateur sépare l'argument. + Ex. "$(a= )", "$(x%*2 + 1)", "$(x/\d{2})" + - '$' ou '$$' seul en argument n'est pas substitué + - En cas de [ ou ] non fermé dans $[], même dans une chaîne, il + doit être échappé par deux '^' par profondeur (c'est comme ça). + Ex. print $[put "a^^]"] --> a] + print $[str lower $[put "a^^^^]"]] --> a] -- bof bof + Autre structure alambiquée à éviter : les "" imbriqués en $[] + print $[str lower "a-$[str upper "ouf"]-b"] -- erreur ! + -- si les "" sont indispensables, préférer cette structure : + def -l tmp $[str upper "ouf"]; print $[str lower "a-$tmp-b"] + + Modificateurs pour substitution de variable $() et §() : + - égalité (=) : $(x=foo) + - inégalité (~) : $(x~foo) + - valeur par défaut si non définition ou vide (?) : $(x?defaut) + Pour x non definie : $(*x?d) -> d, $(x?d) -> erreur x non def + Pour x definie "" : $(*x?d) -> "", $(x?d) -> d + - valeur définie en lecteur père au niveau n : $(x\n) + Ne s'applique qu'aux variables locales, mais si n='g' ou si + pas de locale n trouvée, force la recherche en globale. + - recherche de clef en dictionnaire (,) : $(x,k) + - arithmétique (%) : $(x%*2+1) $(x%<2) $(x%==2*5) + - commandes (#) : len : longueur (str), size* : taille (list), + inc! : incrémentation (int), dec! : décrémentation (int), + empty? : test vide (str), next : incrémentation (str), + shift* : décalage (list), shift!* : idem avec retour du 1er, + pop!* : retour du dernier (list), index|s| : index de s + (str), lstindex|s|* : dernier index de s (str), eval : + évaluation, sub : force un passage de substitution + * : s'appliquent à une liste (sép. : espace, tab, ligne ; + et encadreurs : rien, []). + Note : les séparateurs ne peuvent pas être échappés et sont + tous (saut de ligne, tab...) remplacés par ' ' + Les commandes terminant par ! modifient la variable. + |s| : les param. éventuels se déclarent entre | et | séparés + par des virgules (échappées par '^' au besoin). + Ex. print $(str#len) $(list#size) $(list#has?|abc|) + def x a ^"b c^"; $(x#size) --> 2 + - substitution de chaîne (:) : $(x:reg1=rep1,reg2=rep2...) + En cas de substitution de ',' ou '=', les échapper par '^' + - extraction de sous-liste (@) : $(x@2) ou $(x@3:) ou $(x@2:4) + (liste à encadrer par "" [], ou {} (préféré)) + - extraction de sous-chaîne (&) : $(x&1,2:3) + Pour @ et & : le dernier indice est exclu, les indices négatifs + pointent sur la fin de liste et si début > fin, la sous-chaîne + est inversée. Si l'indice fin dépasse, ramené au dernier car. + Ex: $(s&:), $(s&3:), $(s&:10), $(s&2:-2), $(s&6:2) + - comparaison avec regexp (/) : $(x/[A-Za-z_]) (ret. 0|1) + + '!' juste après le caractère de modification modifie la variable + ex. $(x%!+1) -- incrémente x de 1, comme $(x#inc!) + $(*x?!foo) -- affectation in situ + $(cmd#!eval) -- résolution paresseuse (cf. commande DEF) + Notes : + - s'il y a un espace ou un car. spécial (ex. {}) dans la + chaîne, il faut encadrer la var. entière par "" + - en cas de modification de var. *locale* (par ! ou #cmd!), + seuls le lecteur courant et ses fils sont modifiés. + Comparez : + def -l n 0; ={print $(n%!+1)}; print $n --> 1 0 + def -l n 0; ={def -u1 n $(n%+1); print $n}; print $n --> 1 1 + +Pour l'usage des substitutions, voir l'aide "substitutes". diff --git a/src/lunasql/doc/js-funct.txt b/src/lunasql/doc/js-funct.txt new file mode 100644 index 0000000..cf00aa9 --- /dev/null +++ b/src/lunasql/doc/js-funct.txt @@ -0,0 +1,91 @@ +La substitution $[] permet d'exécuter du code selon le ScriptEngine +en cours (par défaut : Javascript). Exemples pour le SE js : + SQL> print $['abc'.toUpperCase()] + SQL> 'abc'.toUpperCase() + SQL> (print ('ok')) -- les () ou "" forcent le choix de print de js +Le SE js fourni avec le JRE peut autoriser des structures de données +très complexes, ex. JSON (cf. documentation du JRE et commande LET). +Dans le cas du SE JS, en plus de toutes les fonctions du langage JS +implantées dans le JRE, LunaSQL ajoute les variables et fonctions +suivantes : + +Fonctions de base : (chargées par défaut) + include(f) : exécute/charge le fichier JS f + contexEval(s) : évalue le code LunaSQL s (ret: statut sortie) + isString(o), isArray(o), isInt(o), isFloat(o) : tests de type + isEmail(o), isUrl(o) : tests de format de chaîne + toJSON(obj,sp) : sérialise un objet au format JSON (avec/sans esp.) + toObject(str) : désérialise du texte JSON en objet + printObj(o) : affiche un objet au format JSON + I(n) : formate un nombre en supprimant les décimales + messageInfo(m,t), messageWarn(m,t), messageError(m,t) : dialogues +Manipulation de chaînes : + trim(s), ltrim(s), rtrim(s) : suppression d'espaces + lpad(s,p,l), rpad(s,p,l) : chaîne s plus l caractères p + starts(s,r), ends(s,r) : chaîne s commence/finit par regex r + upper(s), lower(s) : chaîne s en majuscules/minuscules + encodeHtml(s) : chaîne s dont éléments HTML protégés + stripHtml(s) : chaîne s dont balises HTML supprimées + truncate(s,l) : chaîne s tronquée à la longueur l + onlyLetters(s) : seulement les lettres de la chaîne s + onlyLettersNums(s) : seulement les lettres/num de la chaîne s +Manipulation de tableaux : + sortn(a) : tableau a rangé par ordre numérique + find(a,s) : teste si tableau contient s (reg, str ou num) + remove(a,v) : ôte l'élément v du tableau a + contains(a,v) : teste si a contient strictement v + map(a,f) : applique à chaque élément du tab. a la fonc. f + indexOf(a,n) : index de n dans a + lastIndexOf(a,n) : dernier index de n dans a + forEach(a,f) : exécute f pour chaque élément de a + insert(a,i,v) : insère un élément v à la position i de a + shuffle(a) : tab. a avec les éléments dans le désordre + unique(a) : tab. avec éléments uniques de a + concat(a,a2) : additionne une ou plusieurs tableaux + copy(a,a2) : copie les éléments du tab. a vers a2 + pop(a) : ôte et retourne le dernier élément de a + push(a) : ajoute un ou x éléments à la fin de a + shift(a) : ôte et retourne le premier élément de a + slice(a,a2,c) : copie et retourne plusieurs éléments c + splice(a,a2,c) : ôte ou remplace x élém. et les retourne + unshift(a,c) : ajoute c au début de a et retourne la taille + sortObjectsByProperty(a,f,r,p) : range un tab. par propriété + f:field, r:reverse, p:primer + cf http://4umi.com/web/javascript/array.php + +Fonctions diverses : bibliothèque 'sys' + variables : JV, OS, OSWin, WD (environnement), engine (SriptEngine), + lunasql (cf. aide de la commande ENGINE) + fonctions : ENV() + +Fonctions de manipulation de dates : bibliothèque 'time' + millis() : temps système en ms + date([f]) : date système selon format f + datefrm([d,f]) : date d selon format élaboré f + isoDateString(d) : convertit une date en ISO + + bibliothèque XDate* v0.8.2, cf https://arshaw.com/xdate/ + +Fonctions et constantes mathématiques : bibliothèque 'math' + E, PI : constantes mathématiques e et pi + sin(x), cos(x), tan(x), asin(s), acos(x), atan(x), atan2(y,x), + exp(x), log(x), ln(x), pow(x,y), ceil(x), floor(x), round(x), + sqrt(x), cbrt(s), rand(), randn(x) (package Math) + intToBase(number,ob,nb), logb(x,base), roundp(x,places), + fractApprox(x,maxDenom), fractReduce(numer,denom) + + bibl. jStat* v1.9.4, cf https://github.com/jstat/jstat + https://cdn.jsdelivr.net/npm/jstat@latest/dist/jstat.min.js + + bibl. Decision-Tree* v.0.2.1 (algo décisionnel ID3) + cf https://github.com/serendipious/nodejs-decision-tree-id3 + + bibl. DNN* v.0.1.0 cf https://github.com/junku901/dnn + +Fonctions de manipulations d'objets JS : bibliothèque 'obj' + Class.extend({ props }) : programmation "objet" en JS + + bibl. sugar* v2.0.4 cf. https://sugarjs.com/ + + bibl. underscore* v1.11.0, cf https://underscorejs.org/ + + bibl. taffy* cf. https://github.com/typicaljoe/taffydb + (le projet semble arrêté) + +Note : ces fonctions appartenant au moteur js, elles ne peuvent pas + bénéficier de l'auto-complètement, ni de l'aide des commandes. + Il est possible d'ajouter manuellement des bibliothèques, en la + plaçant en CLASSPATH sous une ressource /lunasql/jsextras/. diff --git a/src/lunasql/doc/libraries.txt b/src/lunasql/doc/libraries.txt new file mode 100644 index 0000000..783fe5f --- /dev/null +++ b/src/lunasql/doc/libraries.txt @@ -0,0 +1,40 @@ +Bibliothèques embarquées en jar LunaSQL : + - jline : émulation d'une console interactive + https://jline.github.io/ + - joptsimple : analyse des options de ligne de commande + https://github.com/jopt-simple/jopt-simple + - opencsv : lecture/écriture de données au format CSV + http://opencsv.sourceforge.net/project-info.html + - jtableview : affichage de données en JTable amélioré + http://sourceforge.net/projects/tableview/ + https://launchpad.net/~stephane-brunner/+archive/ppa + - nanohttpd : mini serveur HTTP embarqué + https://github.com/NanoHttpd/nanohttpd + - str4d : pure Java implementation of EdDSA + https://github.com/str4d/ed25519-java + +Bibliothèques Javascript en jar LunaSQL : + - taffy : fonctions de base de données en JS + https://github.com/typicaljoe/taffydb + - xdate : fonctions de manipulation de dates + http://arshaw.com/xdate/ + - sugarjs : fonctions de manipulation des objets JS + https://sugarjs.com/ + - underscore : fonctions de manipulation de tableaux + http://underscorejs.org + - decision : fonctions de prise de décision + https://github.com/serendipious/nodejs-decision-tree-id3 + - jstat : fonctions de calcul statistique + https://github.com/jstat/jstat/ + +Bibliothèques optionnelles (jar à ajouter au CLASSPATH) : + - jansi : console agrémentée de couleurs + https://github.com/fusesource/jansi + - javamail : commande mail (use mail) pour envoi de mails + https://javaee.github.io/javamail/ + - jsqlparser : commande frm (use frm) pour formatage SQL + https://github.com/JSQLParser/JSqlParser + - csvjdbc : commande csv (use csv) pour lecture CSV par SQL + http://csvjdbc.sourceforge.net/ + - jsyntaxpane : commande edit : édition de code en swing + https://github.com/nordfalk/jsyntaxpane diff --git a/src/lunasql/doc/packages.txt b/src/lunasql/doc/packages.txt new file mode 100644 index 0000000..c98f7c4 --- /dev/null +++ b/src/lunasql/doc/packages.txt @@ -0,0 +1,51 @@ +Les modules LunaSQL ou Javascript suivants sont disponibles, +chargeables via la commande USE (voir l'aide de USE). + + const : ajoute des constantes utiles + crypt : ajoute les commandes cryptographiques : hash, crypt, + dcrypt, sign et verify + csv : ajoute la commande de gestion de tables en CSV : csv + doc : documentation + - doc-h2 : documentation sur le SGBD H2 + - doc-sql : documentation sur le langage SQL + frm : ajoute la commande de formatage de requête : frm + geek : module inutile pour geeks invertébrés + mail : ajoute la commande d'envoi d'e-mails : mail + math : outils JS de calcul mathématique, contient : + - math-base : (js) trigonométrie et fonctions de base + - math-decision : (js) prise de décision par ID3 + - math-jstat : (js) fonctions et tests statistiques + obj : manipulations d'objets et structures de données + - obj-base : macros et fonctions JS de base + - obj-sugar : (js) gestion d'objets js natifs + - obj-taffy : (js) moteur de BDD en objet + - obj-underscore : (js) programmation fonctionnelle en js + optim : module de confort pour la plupart des usages + play : ajoute la commande de lecture de son : play + report : ajoute la commande de génération de rapports : report + strict : module de garantie de syntaxe stricte + sys : ajoute des macros et fonctions JS d'accès au système sur le + modèle de commandes shell (ls, cp, mv...) et des macros de + journalisation (log) + test : ajoute la commande d'assertions : test + ainsi que plusieurs macros de contrôle et de comparaison de + contenu de variables, et la macro run-tests de lancement + time : utilitaires de gestion de la date et l'heure, contient : + - time-base : ajoute les macros set-chrono, get-chrono + - time-xdate : (js) manipulation de dates + xml : ajoute la commande de requête XPATH : xml + +Pour importer d'autres bibliothèques js par la commande USE, placez +seulement les bibliothèques en archive ou dossier sous l'arborescence +/lunasql/jsextras/ (par exemple dans lunasql-js.jar), puis ajouter +bien sûr cette ressource dans le CLASSPATH. + +Notes : + - les macros définies dans certains de ces paquets sont exemptes + du contrôle de référence circulaire. Inutile, par exemple, de les + prefixer par '*' + - si l'option :SIGN_POLICY est fixée à 1 ou 2, les scripts externes + à LunaSQL doivent être signés pour permettre leur exécution (dans + tous les cas, le script est considéré comme chargé) + +Tapez help- pour de l'aide sur une bibliothèque diff --git a/src/lunasql/doc/substitutes.txt b/src/lunasql/doc/substitutes.txt new file mode 100644 index 0000000..2f84e10 --- /dev/null +++ b/src/lunasql/doc/substitutes.txt @@ -0,0 +1,70 @@ +Il existe deux types de substitutions : + +Les substitutions normales (différées) sont introduites par $. + +Elles sont groupées en *un seul* argument. +Pour l'usage des expressions à substituer, voir l'aide "expressions". +Il peut arriver qu'il faille pourtant "dégrouper" l'argument. Pour +cela préfixer l'argument par ~ (échappable par ^), ou bien utiliser +la substitution par liste (précoce par § si :LIST_SUBSTIT à 1). +Exemples : + def nbargs { print $(arg_nb) } -- nombre d'arguments + nbargs "do ré mi" --> 1 + nbargs "~do ré mi" --> 3 + nbargs "^~do ré mi" --> 1, car ~ est ici échappé + def f { return do ré mi } + nbargs $[f] --> 1 + nbargs ~$[f] --> 3 + opt :LIST_SUBSTIT 1; nbargs §[f] --> 3 +Notes : - aussi bien avec $ que ~$, on ne peut avoir nbargs = 0 dès + lors que l'on passe un argument + - attention à l'usage de ~ : il dégroupe aussi bien les substit. + que les chaînes de caractères ! + +Exemple : + def f {put} + nbargs $[f] --> 1 et non 0 + nbargs ~$[f] --> idem + nbargs §[f] --> 0, ok (si :LIST_SUBSTIT est à 1) +Mais : + print "~ cette /* non affiché ! */ + chaîne va être + dégroupée !" --> cette chaîne va être dégroupée ! + +Les substitutions précoces (ou par listes) sont introduites par §. + +Positionner :LIST_SUBSTIT à 1 pour les activer. +Elles sont utilisables sur le même modèle que $. La grosse différence +est que la substitution est précoce, *avant* l'analyse du code et donc +le groupage des arguments. C'est une sorte de pré-processeur. +Exemple d'application (voir aussi l'exemple ci-dessus) : + def l do ré mi + def nbargs { print $arg_nb } + nbargs $l --> 1 + nbargs §l --> 3 : passage par liste +Autre exemple tordu : + def c "/*" + print §c commentaire pas affiché */ +Notes : + - le texte en commentaire, ainsi que les chaînes dures sont + aussi concernés par cette substitution précoce ! + - les structures suivantes ne fonctionnent donc pas car § est + substitué de façon précoce (substitution avant éval. du bloc) : + def a 123; print §a -- une ligne en console = 1 bloc + def f { a 123; print §a } -- une macro = 1 bloc + +Les substitutions par listes sont utiles pour passer une liste +d'arguments car elles évitent le groupage. Elle peuvent être utiles +également pour insérer du code factorisable (sorte de modules). +Exemple : + def code { + --code partagé à déclarer au préalable + print code + } + def f { + -- fonction appelée ultérieurement + print début + §code -- ici code est inséré non groupé + print fin + } + diff --git a/src/lunasql/doc/syntax.txt b/src/lunasql/doc/syntax.txt new file mode 100644 index 0000000..605590c --- /dev/null +++ b/src/lunasql/doc/syntax.txt @@ -0,0 +1,83 @@ +Les requêtes SQL et commandes se tapent au prompt 'SQL>'. +Les commandes peuvent être appelées par leur nom ou leur raccourci +(ex. affichage des arguments : "PRINT" ou "<"), suivi des éventuels +arguments séparés par au moins une espace. + +Il existe 6 types de commandes, dans l'ordre de résolution : + - les commandes SQL : interprétées par Lecteur uniquement pour les + substitutions, chaînes "", elles sont évaluées par le SGBD. + Ex : SELECT, UPDATE, COMMIT + - les commandes internes : ce sont des fonctions compilées en Java, + intégrées au fichier exécutable lunasql.jar, évaluées par LunaSQL + Ex : PRINT, IMPORT, ARG + - les greffons de commandes : comme les commandes internes, mais + pas intégrées au fichier lunasql.jar + - les macros : ce sont des fonctions interprétées en code LunaSQL, + déclarées par DEF, supportant les arguments nommés + Ex. defmacro, loaded + - les alias : comme les macros, mais ne supportant pas les arguments + nommés, ils fonctionnent comme des "raccourcis", si l'option + :ALIAS_ARG est à 0 ou si son nom est préfixé par ':'. + Si le nom est préfixé par '=' et que l'option :ALLOW_REDEF est + positionnée à 1, par contre, la commande du même nom (et non + l'alias/macro) est recherchée. Avec le préfixe '*', la macro ne + bénéficie pas du contrôle de référence circulaire (aussi + désactivable par :ALLOW_RECUR à 1). + Ex : defm + - les expressions du ScriptEngine : comme les commandes SQL, + interprétées par lecteur pour les substitutions, chaînes "", + elles sont envoyées au moteur ScriptEngine (SE) courant (par + défaut : js). + +Commentaire simple-ligne préfixé par '--' et multi-ligne encadré par +/* */ ex. /* commentaire */. Commentaire actif introduit par --~ ou +tout ce qui suit -~ dans un commentaire simple ligne --. +Note : les démiliteurs /* et */ sont reconnus par modèle : + /* */ /**/ /*/ sont trois commentaires valides, mais + select /*comment*/* from test; -- n'est pas valide + select /*comment*/ * from test; -- mieux +Ex : /* commentaire multi-ligne, pas imbricable */ + -- commentaire simple-ligne normal + --~ commentaire pour SGBD, pas pour LunaSQL + -- commentaire -~ mais ceci est exécuté en LunaSQL + --~ print exon1 -- intron1 --~ exon2 -- intron2 + -- si LunaSQL -~ opt :END_CMD_NL 0 -- en script SGBD + +Le caractère '^' permet d'échapper les caractères spéciaux suivants : +'$' (substitution), fin de ligne, apostrophe, guillemets et ';'. +Notes : - ^ ne s'échappe pas lui-même. + - bien que ce ne soit pas recommandé de l'utiliser, ^ échappe + aussi la fin de ligne d'une fin de commentaire -- + (attention bug : toujours laisser une espace avant --) + +Exécution de script (SQL, ScriptEngine) également possible en console. +Exemples : + SQL> print "un texte simple"; -- commande LunaSQL + SQL> select * from ma_table; -- requête SQL au SGBD + SQL> import contenu.csv; -- commande LunaSQL + SQL> 6*Math.PI; -- évaluation SE + SQL> "function f(){...}"; -- déclaration SE js (notez les "") + SQL> exec script.sql; -- exécution d'un script en console + +Selon la constante :END_CMD_NL, une commande peut se terminer par ';'. +Cela est utile pour longue commande : positionner :END_CMD_NL à 0 +de façon à les devoir valider par un ';' (et non une fin de ligne). +Note : en console, si :END_CMD_NL est à 0, les commandes *doivent* +se terminer par ';'. En lecteur fils (script, sous commande...), le +';' n'est qu'un *séparateur* de commandes : il n'est pas requis en +fin de commande. +Ex. SQL> opt :END_CMD_NL 0 -- passage en terminateur ';' + SQL> print $[str len hello]; -- pas de ';' en commande STR + +Les parenthèses, crochets, accolades, et les guillemets simples, +doubles et inverses non refermés permettent l'écriture de la commande +sur une une nouvelle ligne (sans usage de ^). Les guillemets doubles et +les accolades sont supprimés après interprétation. +Les commandes peuvent accepter des options, commençant par un tiret. +Ex. print -c=3 "de la couleur!"; -- écrit ce texte en vert +Pour passer à la commande du texte commençant par '-' qui n'est pas +une option, précéder les arguments à échapper par "--" (avec les "). +Ex. print "--" -ceci -est -un -texte -avec -tirets; + +Pour plus d'information sur les délimitations de chaînes, voir l'aide +"delimiters". diff --git a/src/lunasql/doc/variables.txt b/src/lunasql/doc/variables.txt new file mode 100644 index 0000000..994f3cc --- /dev/null +++ b/src/lunasql/doc/variables.txt @@ -0,0 +1,35 @@ +Les variables en LunaSQL sont des chaînes de caractères référencées +par un nom de variable. Les macros, alias (définies par DEF ou bien +defmacro) et les options (définies par OPT) sont toutes des variables. + +Les variables peuvent être globales (par défaut) ou locales au lecteur +(ou bloc, ou scope) de déclaration (DEF -l ou OPT -l). + +Un mot sur la portée des variables : + +Les variables globales (déclarées par DEF) sont liées au contexte +global, donc sont accessibles n'importe où, quelque soit leur portée +d'exécution (Lecteur) ou leur fichier de déclaration. +Les variables locales (déclarées par DEF -l ou indice de boucle FOR) +sont liées à leur portée d'exécution (Lecteur) ou leur fichier de +déclaration. Elles sont donc accessibles dans cette portée et dans tous +les portées filles. Un bloc de déclaration peut-être : + - un fichier (commandes EXEC et USE) + - un bloc IF, FOR, WHILE ou EXEC + - un bloc itérateur (ex. LIST apply, filter). +Pour les blocs à itérations, les variables déclarées lors d'un tour +d'itération sont accessibles aux tours suivants. +Variables de Lecteur (ou portée) disponibles : + macro_name : Nom de la macro (le cas échéant) + script_name : Nom du fichier (le cas échéant) + this_scope : Nom de la portée active (fichier, console, macro) + super_scope : Nom de la portée supplémentaire (IF, FOR...) + scope_deep : Profondeur de Lecteurs (1: racine, ex. console) + +Exemple : + def f { + if 1 { + print $this_scope --> f + print $super_scope --> if + } + }; f diff --git a/src/lunasql/http/HttpConsole.java b/src/lunasql/http/HttpConsole.java new file mode 100644 index 0000000..6c8b3e6 --- /dev/null +++ b/src/lunasql/http/HttpConsole.java @@ -0,0 +1,231 @@ +package lunasql.http; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Properties; + +import lunasql.Config; +import lunasql.lib.Contexte; +import lunasql.lib.Lecteur; +import lunasql.lib.Security; +import lunasql.lib.Tools; +import lunasql.sql.SQLCnx; +import lunasql.val.Valeur; + +/** + * Console en HTTP + */ +public class HttpConsole extends NanoHTTPD { + + private final Contexte cont; + + /** + * Constructeur HttpConsole (Contexte, String, String, String, String) + * + * @param cont le contexte + * @param path la chaine entière de connexion + * @param driver le chemin de la classe du driver + * @param login nom d'utilisateur + * @param mdp mot de passe de connexion + * @param port le port d'écoute du serveur + * @throws java.io.IOException si erreur IO + * @throws java.sql.SQLException si erreur SQL + */ + public HttpConsole(Contexte cont, String path, String driver, String login, String mdp, int port) + throws IOException, SQLException, IllegalArgumentException { + super(port); + // Application du contexte et connexion + this.cont = cont; + this.cont.setSQLCnx(new SQLCnx(path, driver, login, mdp)); + this.cont.loadInitFile(); // Si présence d'un fichier INIT, exécution du fichier + } + + /** + * Constructeur HttpConsole (Contexte, int, String, String, String, String, String, int) + * + * @param cont le contexte + * @param type type de la base + * @param base nom ou chemin de la base de données + * @param login nom d'utilisateur + * @param mdp mot de passe de connexion + * @param opts les options des connexions (ex : crypt) + * @param host l'hôte + * @param dbport le port de la base de données + * @param svport le port d'écoute du serveur + * @throws java.io.IOException si erreur IO + * @throws java.sql.SQLException si erreur SQL + */ + public HttpConsole(Contexte cont, int type, String base, String login, String mdp, String opts, String host, + int dbport, int svport) throws IOException, SQLException { + super(svport); + // Application du contexte et connexion + this.cont = cont; + this.cont.setSQLCnx(new SQLCnx(type, base, login, mdp, opts, host, dbport)); + this.cont.loadInitFile(); // Si présence d'un fichier INIT, exécution du fichier + } + + @Override + public Response serve(String uri, String method, Properties header, Properties parms, Properties files) { + String sql = parms.getProperty("sqlquery"); + System.out.println("request: " + method + " at " + uri + ":\n" + sql); + //TODO: ne supporte pas les requêtes CORS + + // GET|POST '/' + if (uri.equals("/")) { + // TODO: ajouter attribut onkeydown='history()' pour l'historique + String frm = parms.getProperty("frmsql"); + HashMap htmlvars = new HashMap<>(); + htmlvars.put("version", Config.APP_VERSION_NUM); + htmlvars.put("lsqlcode", HTMLEntities(sql)); + htmlvars.put("frmchecked", frm == null ? "" : " checked"); + StringBuilder msg = new StringBuilder(); + if (sql != null && !sql.isEmpty()) { + if (frm != null) sql = Tools.cleanSQLCode(cont, sql); + // Zone de saisie + msg.append("

Commande SQL :

\n"); + msg.append("
\n
").append(HTMLEntities(sql));
+            msg.append("
\n
\n"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(), errbaos = new ByteArrayOutputStream(); + cont.setWriter(new PrintStream(baos)); + cont.setErrWriter(new PrintStream(errbaos)); + new Lecteur(cont, sql, new HashMap() {{ put(Contexte.LEC_THIS, "(http-console)"); }}); + + // Valeur de retour + Valeur vr = cont.getValeur(); + String sub = null; + if (vr != null) sub = vr.getSubValue(); + if (sub != null) + msg.append("

Retour : ") + .append(HTMLEntities(sub)).append("

\n"); + + String r = baos.toString(), e = errbaos.toString(); + msg.append("

Affichage en console :

\n"); + // Sortie erreurs + if (!e.trim().isEmpty()) + msg.append("
\n
").append(HTMLEntities(e)).append("
\n
\n"); + // Sortie standard + if (!r.trim().isEmpty()) + msg.append("
\n
").append(HTMLEntities(r)).append("
\n
\n"); + + // Pied + msg.append("

\n

 

\n

Recharger

\n"); + } + else { + msg.append("\n"); + msg.append("\n"); + } + htmlvars.put("returndata", msg.toString()); + return new NanoHTTPD.Response(HTTP_OK, MIME_HTML, getTxtFile("index.html", htmlvars)); + } + + // GET|POST '/api' + if (uri.equals("/api")) { + if (sql == null || sql.isEmpty()) + return new NanoHTTPD.Response(HTTP_BADREQUEST, MIME_PLAINTEXT, + "No SQL code provided. Please set the `sqlcode` property in the body content."); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(), errbaos = new ByteArrayOutputStream(); + cont.setWriter(new PrintStream(baos)); + cont.setErrWriter(new PrintStream(errbaos)); + new Lecteur(cont, sql, new HashMap() {{ put(Contexte.LEC_THIS, "(http-api)"); }}); + + // TODO get return value and send a JSON/XML object + String r = baos.toString(), e = errbaos.toString(); + if (!e.isEmpty()) r += '\n' + e; + return new NanoHTTPD.Response(HTTP_OK, MIME_PLAINTEXT, r) {{ + header = new Properties() {{ put("Content-Type", "text/plain; charset=UTF-8"); }}; + }}; + } + + return new NanoHTTPD.Response(HTTP_NOTFOUND, MIME_PLAINTEXT, uri + " not found"); + } + + /** + * Ouvre une ressource binaire (image) et en retourne l'URL base64 + * Merci à http://stackoverflow.com/questions/1264709/convert-inputstream-to-byte-array-in-java + * + * @param fich le nom du fichier + * @return la chaîne Base64 + */ + private String getImgFile(String fich) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + InputStream is = getClass().getResourceAsStream("/lunasql/http/res/" + fich); + if (is == null) throw new IOException("ressource introuvable"); + byte[] buf = new byte[1024]; + int b; + while ((b = is.read(buf, 0, buf.length)) != -1) out.write(buf, 0, b); + out.flush(); + } + catch (IOException ex) { + cont.errprintln("Erreur HttpConsole : " + ex.getMessage()); + } + return "data:image/gif;base64," + Security.b64encode(out.toByteArray()); + } + + /** + * Ouvre une ressource texte et en retourne le contenu en remplaçant les variables + * ${txtfile:xxx}, ${imgfile:xxx} et ${var:xxx} par la liste de varibles fournie + * + * @param fich le nom du fichier en fichier jar + * @param vars la liste de variables à remplacer + * @return le contenu + */ + public String getTxtFile(String fich, HashMap vars) { + StringBuilder sb = new StringBuilder(), sbvar = new StringBuilder(); + try { + InputStream is = getClass().getResourceAsStream("/lunasql/http/res/" + fich); + if (is == null) throw new IOException("ressource introuvable"); + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + int i; + while ((i = br.read()) != -1) { + if ((char) i == '$') { + if ((char) (i = br.read()) == '{') { + while ((i = br.read()) != -1 && i != '}') sbvar.append((char) i); + String var = sbvar.toString(), key = var.substring(4), val; + if (var.startsWith("var=")) { + if (vars == null || (val = vars.get(key)) == null) + sb.append("[undefined: ").append(key).append("]"); + else sb.append(val); + } else if (var.startsWith("txt=")) sb.append(getTxtFile(key, vars)); + else if (var.startsWith("img=")) sb.append(getImgFile(key)); + else sb.append("[invalid substitution: ").append(var).append("]"); + sbvar.setLength(0); + } + else sb.append('$').append((char) i); + } + else sb.append((char) i); + } + br.close(); + } + catch (IOException ex) { + cont.errprintln("Erreur HttpConsole : " + ex.getMessage()); + } + return sb.toString(); + } + + /** + * Remplacement de tous les caractères HTML bizarres en leut équivalent sécurisés + * @param s la chaîne à traiter + * @return la chaîne sécurisée + */ + public static String HTMLEntities(String s){ + if (s == null || s.isEmpty()) return ""; + return s.replace("&", "&").replace("<", "<").replace(">", ">") + .replace("\u001B[1;30m", "") + .replace("\u001B[1;31m", "") + .replace("\u001B[1;32m", "") + .replace("\u001B[1;33m", "") + .replace("\u001B[1;34m", "") + .replace("\u001B[1;37m", "") + .replace("\u001B[m", ""); + } +} + diff --git a/src/lunasql/http/NanoHTTPD-LICENSE.txt b/src/lunasql/http/NanoHTTPD-LICENSE.txt new file mode 100755 index 0000000..9c4abc0 --- /dev/null +++ b/src/lunasql/http/NanoHTTPD-LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (C) 2001,2005-2011 by Jarno Elonen +and Copyright (C) 2010 by Konstantinos Togias + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. Redistributions in +binary form must reproduce the above copyright notice, this list of +conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. The name of the author may not +be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.; diff --git a/src/lunasql/http/NanoHTTPD.java b/src/lunasql/http/NanoHTTPD.java new file mode 100644 index 0000000..748822e --- /dev/null +++ b/src/lunasql/http/NanoHTTPD.java @@ -0,0 +1,1065 @@ +package lunasql.http; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLEncoder; +import java.util.Date; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.Vector; + +/** + * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java + * + *

NanoHTTPD version 1.24, + * Copyright © 2001,2005-2011 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) + * and Copyright © 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr) + * + *

Features + limitations:

    + * + *
  • Only one Java file
  • + *
  • Java 1.1 compatible
  • + *
  • Released as open source, Modified BSD licence
  • + *
  • No fixed config files, logging, authorization etc. (Implement yourself if you need them.)
  • + *
  • Supports parameter parsing of GET and POST methods
  • + *
  • Supports both dynamic content and file serving
  • + *
  • Supports file upload (since version 1.2, 2010)
  • + *
  • Supports partial content (streaming)
  • + *
  • Supports ETags
  • + *
  • Never caches anything
  • + *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • + *
  • Default code serves files and shows all HTTP parameters and headers
  • + *
  • File server supports directory listing, index.html and index.htm
  • + *
  • File server supports partial content (streaming)
  • + *
  • File server supports ETags
  • + *
  • File server does the 301 redirection trick for directories without '/'
  • + *
  • File server supports simple skipping for files (continue download)
  • + *
  • File server serves also very long files without memory overhead
  • + *
  • Contains a built-in list of most common mime types
  • + *
  • All header names are converted lowercase so they don't vary between browsers/clients
  • + * + *
+ * + *

Ways to use:

    + * + *
  • Run as a standalone app, serves files and shows requests
  • + *
  • Subclass serve() and embed to your own program
  • + *
  • Call serveFile() from serve() with your own base directory
  • + * + *
+ * + * Homepage: http://iki.fi/elonen/code/nanohttpd/ + * + * Copyright (C) 2001,2005-2011 by Jarno Elonen \n + and Copyright (C) 2010 by Konstantinos Togias \n + Redistribution and use in source and binary forms, with or without\n + modification, are permitted provided that the following conditions\n + are met: + Redistributions of source code must retain the above copyright notice,\n + this list of conditions and the following disclaimer. Redistributions in\n + binary form must reproduce the above copyright notice, this list of\n + conditions and the following disclaimer in the documentation and/or other\n + materials provided with the distribution. The name of the author may not\n + be used to endorse or promote products derived from this software without\n + specific prior written permission. + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.; + */ +public class NanoHTTPD { + // ================================================== + // API parts + // ================================================== + + /** + * Override this to customize the server.

+ * + * (By default, this delegates to serveFile() and allows directory listing.) + * + * @param uri Percent-decoded URI without parameters, for example "/index.cgi" + * @param method "GET", "POST" etc. + * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. + * @param header Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + public Response serve(String uri, String method, Properties header, Properties parms, Properties files) { + System.out.println(method + " '" + uri + "' "); + + Enumeration e = header.propertyNames(); + while (e.hasMoreElements()) { + String value = (String) e.nextElement(); + System.out.println(" HDR: '" + value + "' = '" + + header.getProperty(value) + "'"); + } + e = parms.propertyNames(); + while (e.hasMoreElements()) { + String value = (String) e.nextElement(); + System.out.println(" PRM: '" + value + "' = '" + + parms.getProperty(value) + "'"); + } + e = files.propertyNames(); + while (e.hasMoreElements()) { + String value = (String) e.nextElement(); + System.out.println(" UPLOADED: '" + value + "' = '" + + files.getProperty(value) + "'"); + } + + return serveFile(uri, header, myRootDir, true); + } + + /** + * HTTP response. + * Return one of these from serve(). + */ + public class Response { + + /** + * Default constructor: response = HTTP_OK, data = mime = 'null' + */ + public Response() { + this.status = HTTP_OK; + } + + /** + * Basic constructor. + * @param status + * @param mimeType + * @param data + */ + public Response(String status, String mimeType, InputStream data) { + this.status = status; + this.mimeType = mimeType; + this.data = data; + } + + /** + * Convenience method that makes an InputStream out of + * given text. + * @param status + * @param mimeType + * @param txt + */ + public Response(String status, String mimeType, String txt) { + this.status = status; + this.mimeType = mimeType; + try { + this.data = new ByteArrayInputStream(txt.getBytes("UTF-8")); + } catch (java.io.UnsupportedEncodingException uee) { + uee.printStackTrace(); + } + } + + /** + * Adds given line to the header. + * @param name + * @param value + */ + public void addHeader(String name, String value) { + header.put(name, value); + } + /** + * HTTP status code after processing, e.g. "200 OK", HTTP_OK + */ + public String status; + /** + * MIME type of content, e.g. "text/html" + */ + public String mimeType; + /** + * Data of the response, may be null. + */ + public InputStream data; + /** + * Headers for the HTTP response. Use addHeader() + * to add lines. + */ + public Properties header = new Properties(); + } + /** + * Some HTTP response status codes + */ + public static final String HTTP_OK = "200 OK", + HTTP_PARTIALCONTENT = "206 Partial Content", + HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable", + HTTP_REDIRECT = "301 Moved Permanently", + HTTP_FORBIDDEN = "403 Forbidden", + HTTP_NOTFOUND = "404 Not Found", + HTTP_BADREQUEST = "400 Bad Request", + HTTP_INTERNALERROR = "500 Internal Server Error", + HTTP_NOTIMPLEMENTED = "501 Not Implemented"; + /** + * Common mime types for dynamic content + */ + public static final String MIME_PLAINTEXT = "text/plain", + MIME_HTML = "text/html", + MIME_DEFAULT_BINARY = "application/octet-stream", + MIME_XML = "text/xml"; + + // ================================================== + // Socket & server code + // ================================================== + /** + * Starts a HTTP server to given port.

+ * Throws an IOException if the socket is already in use + * @param port + * @param wwwroot + * @throws java.io.IOException + */ + public NanoHTTPD(int port, File wwwroot) throws IOException { + myTcpPort = port; + this.myRootDir = wwwroot; + myServerSocket = new ServerSocket(myTcpPort); + myThread = new Thread(new Runnable() { + + @Override + public void run() { + try { + while (true) { + new HTTPSession(myServerSocket.accept()); + } + } catch (IOException ioe) { + } + } + }); + myThread.setDaemon(true); + myThread.start(); + } + + /** + * Starts a HTTP server to given port.

+ * Throws an IOException if the socket is already in use. + * Starts serving files from current directory. + * @param port + * @throws java.io.IOException + */ + public NanoHTTPD(int port) throws IOException { + this(port, new File(".").getAbsoluteFile()); + } + + /** + * Stops the server. + */ + public void stop() { + try { + myServerSocket.close(); + myThread.join(); + } catch (IOException ioe) { + } catch (InterruptedException e) { + } + } + + /** + * Starts as a standalone file server and waits for Enter. + * + public static void main(String[] args) { + System.out.println("NanoHTTPD 1.24 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" + + "(Command line options: [-p port] [-d root-dir] [--licence])\n"); + + // Defaults + int port = 80; + File wwwroot = new File(".").getAbsoluteFile(); + + // Show licence if requested + for (int i = 0; i < args.length; ++i) { + if (args[i].equalsIgnoreCase("-p")) { + port = Integer.parseInt(args[i + 1]); + } else if (args[i].equalsIgnoreCase("-d")) { + wwwroot = new File(args[i + 1]).getAbsoluteFile(); + } else if (args[i].toLowerCase().endsWith("licence")) { + System.out.println(LICENCE + "\n"); + break; + } + } + + try { + new NanoHTTPD(port, wwwroot); + } catch (IOException ioe) { + System.err.println("Couldn't start server:\n" + ioe); + System.exit(-1); + } + + System.out.println("Now serving files in port " + port + " from \"" + wwwroot + "\""); + System.out.println("Hit Enter to stop.\n"); + + try { + System.in.read(); + } catch (Throwable t) { + } + }*/ + + /** + * Handles one session, i.e. parses the HTTP request + * and returns the response. + */ + private class HTTPSession implements Runnable { + + public HTTPSession(Socket s) { + mySocket = s; + Thread t = new Thread(this); + t.setDaemon(true); + t.start(); + } + + @Override + public void run() { + try { + InputStream is = mySocket.getInputStream(); + if (is == null) { + return; + } + + // Read the first 8192 bytes. + // The full header should fit in here. + // Apache's default header limit is 8KB. + int bufsize = 8192; + byte[] buf = new byte[bufsize]; + int rlen = is.read(buf, 0, bufsize); + if (rlen <= 0) { + return; + } + + // Create a BufferedReader for parsing the header. + ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen); + BufferedReader hin = new BufferedReader(new InputStreamReader(hbis)); + Properties pre = new Properties(); + Properties parms = new Properties(); + Properties header = new Properties(); + Properties files = new Properties(); + + // Decode the header into parms and header java properties + decodeHeader(hin, pre, parms, header); + String method = pre.getProperty("method"); + String uri = pre.getProperty("uri"); + + long size = 0x7FFFFFFFFFFFFFFFl; + String contentLength = header.getProperty("content-length"); + if (contentLength != null) { + try { + size = Integer.parseInt(contentLength); + } catch (NumberFormatException ex) { + } + } + + // We are looking for the byte separating header from body. + // It must be the last byte of the first two sequential new lines. + int splitbyte = 0; + boolean sbfound = false; + while (splitbyte < rlen) { + if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && + buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') { + sbfound = true; + break; + } + splitbyte++; + } + splitbyte++; + + // Write the part of body already read to ByteArrayOutputStream f + ByteArrayOutputStream f = new ByteArrayOutputStream(); + if (splitbyte < rlen) { + f.write(buf, splitbyte, rlen - splitbyte); + } + + // While Firefox sends on the first read all the data fitting + // our buffer, Chrome and Opera sends only the headers even if + // there is data for the body. So we do some magic here to find + // out whether we have already consumed part of body, if we + // have reached the end of the data to be sent or we should + // expect the first byte of the body at the next read. + if (splitbyte < rlen) { + size -= rlen - splitbyte + 1; + } else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl) { + size = 0; + } + + // Now read all the body and write it to f + buf = new byte[512]; + while (rlen >= 0 && size > 0) { + rlen = is.read(buf, 0, 512); + size -= rlen; + if (rlen > 0) { + f.write(buf, 0, rlen); + } + } + + // Get the raw body as a byte [] + byte[] fbuf = f.toByteArray(); + + // Create a BufferedReader for easily reading it as string. + ByteArrayInputStream bin = new ByteArrayInputStream(fbuf); + BufferedReader in = new BufferedReader(new InputStreamReader(bin)); + + // If the method is POST, there may be parameters + // in data section, too, read it: + if (method.equalsIgnoreCase("POST")) { + String contentType = ""; + String contentTypeHeader = header.getProperty("content-type"); + if (contentTypeHeader == null) { + sendError(HTTP_BADREQUEST, "BAD REQUEST: Content type is missing."); + } + StringTokenizer st = new StringTokenizer(contentTypeHeader, "; "); + if (st.hasMoreTokens()) { + contentType = st.nextToken(); + } + + if (contentType.equalsIgnoreCase("multipart/form-data")) { + // Handle multipart/form-data + if (!st.hasMoreTokens()) { + sendError(HTTP_BADREQUEST, + "BAD REQUEST: Content type is multipart/form-data but boundary missing. " + + "Usage: GET /example/file.html"); + } + String boundaryExp = st.nextToken(); + st = new StringTokenizer(boundaryExp, "="); + if (st.countTokens() != 2) { + sendError(HTTP_BADREQUEST, + "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. " + + "Usage: GET /example/file.html"); + } + st.nextToken(); + String boundary = st.nextToken(); + + decodeMultipartData(boundary, fbuf, in, parms, files); + } else { + // Handle application/x-www-form-urlencoded + String postLine = ""; + char pbuf[] = new char[512]; + int read = in.read(pbuf); + while (read >= 0 && !postLine.endsWith("\r\n")) { + postLine += String.valueOf(pbuf, 0, read); + read = in.read(pbuf); + } + postLine = postLine.trim(); + decodeParms(postLine, parms); + } + } + + // Ok, now do the serve() + Response r = serve(uri, method, header, parms, files); + if (r == null) { + sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + sendResponse(r.status, r.mimeType, r.header, r.data); + } + + in.close(); + is.close(); + } catch (IOException ioe) { + try { + sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } catch (Throwable t) { + } + } catch (InterruptedException ie) { + // Thrown by sendError, ignore and exit the thread. + } + } + + /** + * Decodes the sent headers and loads the data into + * java Properties' key - value pairs + **/ + private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header) + throws InterruptedException { + try { + // Read the request line + String inLine = in.readLine(); + if (inLine == null) { + return; + } + StringTokenizer st = new StringTokenizer(inLine); + if (!st.hasMoreTokens()) { + sendError(HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } + + String method = st.nextToken(); + pre.put("method", method); + + if (!st.hasMoreTokens()) { + sendError(HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); + } + + String uri = st.nextToken(); + + // Decode parameters from the URI + int qmi = uri.indexOf('?'); + if (qmi >= 0) { + decodeParms(uri.substring(qmi + 1), parms); + uri = decodePercent(uri.substring(0, qmi)); + } else { + uri = decodePercent(uri); + } + + // If there's another token, it's protocol version, + // followed by HTTP headers. Ignore version but parse headers. + // NOTE: this now forces header names lowercase since they are + // case insensitive and vary by client. + if (st.hasMoreTokens()) { + String line = in.readLine(); + while (line != null && line.trim().length() > 0) { + int p = line.indexOf(':'); + if (p >= 0) { + header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim()); + } + line = in.readLine(); + } + } + + pre.put("uri", uri); + } catch (IOException ioe) { + sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } + } + + /** + * Decodes the Multipart Body data and put it + * into java Properties' key - value pairs. + **/ + private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, + Properties parms, Properties files) throws InterruptedException { + try { + int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); + int boundarycount = 1; + String mpline = in.readLine(); + while (mpline != null) { + if (mpline.indexOf(boundary) == -1) { + sendError(HTTP_BADREQUEST, + "BAD REQUEST: Content type is multipart/form-data but next chunk does not " + + "start with boundary. Usage: GET /example/file.html"); + } + boundarycount++; + Properties item = new Properties(); + mpline = in.readLine(); + while (mpline != null && mpline.trim().length() > 0) { + int p = mpline.indexOf(':'); + if (p != -1) { + item.put(mpline.substring(0, p).trim().toLowerCase(), mpline.substring(p + 1).trim()); + } + mpline = in.readLine(); + } + if (mpline != null) { + String contentDisposition = item.getProperty("content-disposition"); + if (contentDisposition == null) { + sendError(HTTP_BADREQUEST, + "BAD REQUEST: Content type is multipart/form-data but no content-disposition " + + "info found. Usage: GET /example/file.html"); + } + StringTokenizer st = new StringTokenizer(contentDisposition, "; "); + Properties disposition = new Properties(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + int p = token.indexOf('='); + if (p != -1) { + disposition.put(token.substring(0, p).trim().toLowerCase(), + token.substring(p + 1).trim()); + } + } + String pname = disposition.getProperty("name"); + pname = pname.substring(1, pname.length() - 1); + + String value = ""; + if (item.getProperty("content-type") == null) { + while (mpline != null && mpline.indexOf(boundary) == -1) { + mpline = in.readLine(); + if (mpline != null) { + int d = mpline.indexOf(boundary); + if (d == -1) { + value += mpline; + } else { + value += mpline.substring(0, d - 2); + } + } + } + } else { + if (boundarycount > bpositions.length) { + sendError(HTTP_INTERNALERROR, "Error processing request"); + } + int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); + String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); + files.put(pname, path); + value = disposition.getProperty("filename"); + value = value.substring(1, value.length() - 1); + do { + mpline = in.readLine(); + } while (mpline != null && mpline.indexOf(boundary) == -1); + } + parms.put(pname, value); + } + } + } catch (IOException ioe) { + sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } + } + + /** + * Find the byte positions where multipart boundaries start. + **/ + public int[] getBoundaryPositions(byte[] b, byte[] boundary) { + int matchcount = 0; + int matchbyte = -1; + Vector matchbytes = new Vector(); + for (int i = 0; i < b.length; i++) { + if (b[i] == boundary[matchcount]) { + if (matchcount == 0) { + matchbyte = i; + } + matchcount++; + if (matchcount == boundary.length) { + matchbytes.addElement(new Integer(matchbyte)); + matchcount = 0; + matchbyte = -1; + } + } else { + i -= matchcount; + matchcount = 0; + matchbyte = -1; + } + } + int[] ret = new int[matchbytes.size()]; + for (int i = 0; i < ret.length; i++) ret[i] = matchbytes.elementAt(i).intValue(); + return ret; + } + + /** + * Retrieves the content of a sent file and saves it + * to a temporary file. + * The full path to the saved file is returned. + **/ + private String saveTmpFile(byte[] b, int offset, int len) { + String path = ""; + if (len > 0) { + String tmpdir = System.getProperty("java.io.tmpdir"); + try { + File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir)); + OutputStream fstream = new FileOutputStream(temp); + fstream.write(b, offset, len); + fstream.close(); + path = temp.getAbsolutePath(); + } catch (Exception e) { // Catch exception if any + System.err.println("Error: " + e.getMessage()); + } + } + return path; + } + + /** + * It returns the offset separating multipart file headers + * from the file's data. + **/ + private int stripMultipartHeaders(byte[] b, int offset) { + int i = 0; + for (i = offset; i < b.length; i++) { + if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n') { + break; + } + } + return i + 1; + } + + /** + * Decodes the percent encoding scheme.
+ * For example: "an+example%20string" -> "an example string" + */ + private String decodePercent(String str) throws InterruptedException { + try { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + switch (c) { + case '+': + sb.append(' '); + break; + case '%': + sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16)); + i += 2; + break; + default: + sb.append(c); + break; + } + } + return sb.toString(); + } catch (Exception e) { + sendError(HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding."); + return null; + } + } + + /** + * Decodes parameters in percent-encoded URI-format + * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and + * adds them to given Properties. NOTE: this doesn't support multiple + * identical keys due to the simplicity of Properties -- if you need multiples, + * you might want to replace the Properties with a Hashtable of Vectors or such. + */ + private void decodeParms(String parms, Properties p) + throws InterruptedException { + if (parms == null) { + return; + } + + StringTokenizer st = new StringTokenizer(parms, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + if (sep >= 0) { + p.put(decodePercent(e.substring(0, sep)).trim(), + decodePercent(e.substring(sep + 1))); + } + } + } + + /** + * Returns an error message as a HTTP response and + * throws InterruptedException to stop further request processing. + */ + private void sendError(String status, String msg) throws InterruptedException { + sendResponse(status, MIME_PLAINTEXT, null, new ByteArrayInputStream(msg.getBytes())); + throw new InterruptedException(); + } + + /** + * Sends given response to the socket. + */ + private void sendResponse(String status, String mime, Properties header, InputStream data) { + try { + if (status == null) { + throw new Error("sendResponse(): Status can't be null."); + } + + OutputStream out = mySocket.getOutputStream(); + PrintWriter pw = new PrintWriter(out); + pw.print("HTTP/1.0 " + status + " \r\n"); + + if (mime != null) { + pw.print("Content-Type: " + mime + "\r\n"); + } + + if (header == null || header.getProperty("Date") == null) { + pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); + } + + if (header != null) { + Enumeration e = header.keys(); + while (e.hasMoreElements()) { + String key = (String) e.nextElement(); + String value = header.getProperty(key); + pw.print(key + ": " + value + "\r\n"); + } + } + + pw.print("\r\n"); + pw.flush(); + + if (data != null) { + int maxBuff = 2048; + int pending = data.available(); // This is to support partial sends, see serveFile() + byte[] buff = new byte[Math.max(pending, maxBuff)]; + while (pending > 0) { + int read = data.read(buff, 0, (Math.min(pending, maxBuff))); + if (read <= 0) { + break; + } + out.write(buff, 0, read); + pending -= read; + } + } + out.flush(); + out.close(); + if (data != null) { + data.close(); + } + } catch (IOException ioe) { + // Couldn't write? No can do. + try { + mySocket.close(); + } catch (Throwable t) { + } + } + } + private Socket mySocket; + } + + /** + * URL-encodes everything between "/"-characters. + * Encodes spaces as '%20' instead of '+'. + */ + private String encodeUri(String uri) { + String newUri = ""; + StringTokenizer st = new StringTokenizer(uri, "/ ", true); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equals("/")) { + newUri += "/"; + } else if (tok.equals(" ")) { + newUri += "%20"; + } else { + // newUri += URLEncoder.encode(tok); + // For Java 1.4 you'll want to use this instead: + try { newUri += URLEncoder.encode( tok, "UTF-8" ); } + catch ( java.io.UnsupportedEncodingException uee ) {} + } + } + return newUri; + } + private int myTcpPort; + private final ServerSocket myServerSocket; + private Thread myThread; + private File myRootDir; + + // ================================================== + // File server code + // ================================================== + /** + * Serves file from homeDir and its' subdirectories (only). + * Uses only URI, ignores all headers and HTTP parameters. + */ + public Response serveFile(String uri, Properties header, File homeDir, + boolean allowDirectoryListing) { + Response res = null; + + // Make sure we won't die of an exception later + if (!homeDir.isDirectory()) { + res = new Response(HTTP_INTERNALERROR, MIME_HTML, + "INTERNAL ERRROR: serveFile(): " + + "given homeDir is not a directory."); + } + + if (res == null) { + // Remove URL arguments + uri = uri.trim().replace(File.separatorChar, '/'); + if (uri.indexOf('?') >= 0) { + uri = uri.substring(0, uri.indexOf('?')); + } + + // Prohibit getting out of current directory + if (uri.startsWith("..") || uri.endsWith("..") || uri.contains("../")) { + res = new Response(HTTP_FORBIDDEN, MIME_HTML, + "FORBIDDEN: Won't serve ../ " + + "for security reasons."); + } + } + + File f = new File(homeDir, uri); + if (res == null && !f.exists()) { + res = new Response(HTTP_NOTFOUND, MIME_HTML, + "Error 404, file not found."); + } + + // List the directory, if necessary + if (res == null && f.isDirectory()) { + // Browsers get confused without '/' after the + // directory, send a redirect. + if (!uri.endsWith("/")) { + uri += "/"; + res = new Response(HTTP_REDIRECT, MIME_HTML, + "Redirected: " + + uri + ""); + res.addHeader("Location", uri); + } + + if (res == null) { + // First try index.html and index.htm + if (new File(f, "index.html").exists()) { + f = new File(homeDir, uri + "/index.html"); + } else if (new File(f, "index.htm").exists()) { + f = new File(homeDir, uri + "/index.htm"); + } // No index file, list the directory if it is readable + else if (allowDirectoryListing && f.canRead()) { + String[] files = f.list(); + String msg = "

Directory " + uri + "


"; + + if (uri.length() > 1) { + String u = uri.substring(0, uri.length() - 1); + int slash = u.lastIndexOf('/'); + if (slash >= 0 && slash < u.length()) { + msg += "..
"; + } + } + + if (files != null) { + for (int i = 0; i < files.length; ++i) { + File curFile = new File(f, files[i]); + boolean dir = curFile.isDirectory(); + if (dir) { + msg += ""; + files[i] += "/"; + } + + msg += "" + + files[i] + ""; + + // Show file size + if (curFile.isFile()) { + long len = curFile.length(); + msg += "  ("; + if (len < 1024) { + msg += len + " bytes"; + } else if (len < 1024 * 1024) { + msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB"; + } else { + msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB"; + } + + msg += ")"; + } + msg += "
"; + if (dir) { + msg += "
"; + } + } + } + msg += ""; + res = new Response(HTTP_OK, MIME_HTML, msg); + } else { + res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, + "FORBIDDEN: No directory listing."); + } + } + } + + try { + if (res == null) { + // Get MIME type from file name extension, if possible + String mime = null; + int dot = f.getCanonicalPath().lastIndexOf('.'); + if (dot >= 0) { + mime = (String) theMimeTypes.get(f.getCanonicalPath().substring(dot + 1).toLowerCase()); + } + if (mime == null) { + mime = MIME_DEFAULT_BINARY; + } + + // Calculate etag + String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()) + .hashCode()); + + // Support (simple) skipping: + long startFrom = 0; + long endAt = -1; + String range = header.getProperty("range"); + if (range != null) { + if (range.startsWith("bytes=")) { + range = range.substring("bytes=".length()); + int minus = range.indexOf('-'); + try { + if (minus > 0) { + startFrom = Long.parseLong(range.substring(0, minus)); + endAt = Long.parseLong(range.substring(minus + 1)); + } + } catch (NumberFormatException nfe) { + } + } + } + + // Change return code and add Content-Range header when skipping is requested + long fileLen = f.length(); + if (range != null && startFrom >= 0) { + if (startFrom >= fileLen) { + res = new Response(HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, ""); + res.addHeader("Content-Range", "bytes 0-0/" + fileLen); + res.addHeader("ETag", etag); + } else { + if (endAt < 0) { + endAt = fileLen - 1; + } + long newLen = endAt - startFrom + 1; + if (newLen < 0) { + newLen = 0; + } + + final long dataLen = newLen; + FileInputStream fis = new FileInputStream(f) { + + @Override + public int available() throws IOException { + return (int) dataLen; + } + }; + fis.skip(startFrom); + + res = new Response(HTTP_PARTIALCONTENT, mime, fis); + res.addHeader("Content-Length", "" + dataLen); + res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); + res.addHeader("ETag", etag); + } + } else { + res = new Response(HTTP_OK, mime, new FileInputStream(f)); + res.addHeader("Content-Length", "" + fileLen); + res.addHeader("ETag", etag); + } + } + } catch (IOException ioe) { + res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed."); + } + + // Announce that the file server accepts partial content requestes + res.addHeader("Accept-Ranges", "bytes"); + return res; + } + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + private static Hashtable theMimeTypes = new Hashtable(); + + static { + StringTokenizer st = new StringTokenizer( + "css text/css " + + "htm text/html " + + "html text/html " + + "xml text/xml " + + "txt text/plain " + + "asc text/plain " + + "gif image/gif " + + "jpg image/jpeg " + + "jpeg image/jpeg " + + "png image/png " + + "mp3 audio/mpeg " + + "m3u audio/mpeg-url " + + "mp4 video/mp4 " + + "ogv video/ogg " + + "flv video/x-flv " + + "mov video/quicktime " + + "swf application/x-shockwave-flash " + + "js application/javascript " + + "pdf application/pdf " + + "doc application/msword " + + "ogg application/x-ogg " + + "zip application/octet-stream " + + "exe application/octet-stream " + + "class application/octet-stream "); + while (st.hasMoreTokens()) { + theMimeTypes.put(st.nextToken(), st.nextToken()); + } + } + /** + * GMT date formatter + */ + private static java.text.SimpleDateFormat gmtFrmt; + + static { + gmtFrmt = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + } +} diff --git a/src/lunasql/http/res/Highlightjs-LICENSE.txt b/src/lunasql/http/res/Highlightjs-LICENSE.txt new file mode 100755 index 0000000..422deb7 --- /dev/null +++ b/src/lunasql/http/res/Highlightjs-LICENSE.txt @@ -0,0 +1,24 @@ +Copyright (c) 2006, Ivan Sagalaev +All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of highlight.js nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/lunasql/http/res/default.css b/src/lunasql/http/res/default.css new file mode 100755 index 0000000..f1bfade --- /dev/null +++ b/src/lunasql/http/res/default.css @@ -0,0 +1,99 @@ +/* + +Original highlight.js style (c) Ivan Sagalaev + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #F0F0F0; +} + + +/* Base color: saturation 0; */ + +.hljs, +.hljs-subst { + color: #444; +} + +.hljs-comment { + color: #888888; +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold; +} + + +/* User color: hue: 0 */ + +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060; +} + + +/* Language color: hue: 90; */ + +.hljs-literal { + color: #78A960; +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/src/lunasql/http/res/highlight.min.js b/src/lunasql/http/res/highlight.min.js new file mode 100755 index 0000000..9cf40f3 --- /dev/null +++ b/src/lunasql/http/res/highlight.min.js @@ -0,0 +1,2 @@ +/*! highlight.js v9.3.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function i(e){var n,t,r,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",t=/\blang(?:uage)?-([\w-]+)\b/i.exec(i))return w(t[1])?t[1]:"no-highlight";for(i=i.split(/\s+/),n=0,r=i.length;r>n;n++)if(w(i[n])||a(i[n]))return i[n]}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3==i.nodeType?a+=i.nodeValue.length:1==i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!=r[0].offset?e[0].offset"}function u(e){f+=""}function c(e){("start"==e.event?o:u)(e.node)}for(var s=0,f="",l=[];e.length||r.length;){var g=i();if(f+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g==e){l.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g==e&&g.length&&g[0].offset==s);l.reverse().forEach(o)}else"start"==g[0].event?l.push(g[0].node):l.pop(),c(g.splice(0,1)[0])}return f+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):Object.keys(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"==e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var f=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=f.length?t(f.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){for(var t=0;t";return i+=e+'">',i+n+o}function h(){if(!k.k)return n(M);var e="",t=0;k.lR.lastIndex=0;for(var r=k.lR.exec(M);r;){e+=n(M.substr(t,r.index-t));var a=g(k,r);a?(B+=a[1],e+=p(a[0],n(r[0]))):e+=n(r[0]),t=k.lR.lastIndex,r=k.lR.exec(M)}return e+n(M.substr(t))}function d(){var e="string"==typeof k.sL;if(e&&!R[k.sL])return n(M);var t=e?f(k.sL,M,!0,y[k.sL]):l(M,k.sL.length?k.sL:void 0);return k.r>0&&(B+=t.r),e&&(y[k.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=void 0!==k.sL?d():h(),M=""}function v(e,n){L+=e.cN?p(e.cN,"",!0):"",k=Object.create(e,{parent:{value:k}})}function m(e,n){if(M+=e,void 0===n)return b(),0;var t=o(n,k);if(t)return t.skip?M+=n:(t.eB&&(M+=n),b(),t.rB||t.eB||(M=n)),v(t,n),t.rB?0:n.length;var r=u(k,n);if(r){var a=k;a.skip?M+=n:(a.rE||a.eE||(M+=n),b(),a.eE&&(M=n));do k.cN&&(L+=""),k.skip||(B+=k.r),k=k.parent;while(k!=r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,k))throw new Error('Illegal lexeme "'+n+'" for mode "'+(k.cN||"")+'"');return M+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var x,k=i||N,y={},L="";for(x=k;x!=N;x=x.parent)x.cN&&(L=p(x.cN,"",!0)+L);var M="",B=0;try{for(var C,j,I=0;;){if(k.t.lastIndex=I,C=k.t.exec(t),!C)break;j=m(t.substr(I,C.index-I),C[0]),I=C.index+j}for(m(t.substr(I)),x=k;x.parent;x=x.parent)x.cN&&(L+="");return{r:B,value:L,language:e,top:k}}catch(O){if(-1!=O.message.indexOf("Illegal"))return{r:0,value:n(t)};throw O}}function l(e,t){t=t||E.languages||Object.keys(R);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,n){return n.replace(/\t/g,E.tabReplace)})),E.useBR&&(e=e.replace(/\n/g,"
")),e}function p(e,n,t){var r=n?x[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function h(e){var n=i(e);if(!a(n)){var t;E.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var r=t.textContent,o=n?f(n,r,!0):l(r),s=u(t);if(s.length){var h=document.createElementNS("http://www.w3.org/1999/xhtml","div");h.innerHTML=o.value,o.value=c(s,u(h),r)}o.value=g(o.value),e.innerHTML=o.value,e.className=p(e.className,n,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function d(e){E=o(E,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,h)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=R[n]=t(e);r.aliases&&r.aliases.forEach(function(e){x[e]=n})}function N(){return Object.keys(R)}function w(e){return e=(e||"").toLowerCase(),R[e]||R[x[e]]}var E={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},R={},x={};return e.highlight=f,e.highlightAuto=l,e.fixMarkup=g,e.highlightBlock=h,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}}); \ No newline at end of file diff --git a/src/lunasql/http/res/index.html b/src/lunasql/http/res/index.html new file mode 100644 index 0000000..47441c7 --- /dev/null +++ b/src/lunasql/http/res/index.html @@ -0,0 +1,61 @@ + + + + + +Console LunaSQL HTTP + + + + + + + + +

Console LunaSQL HTTP

+
+

SQL :

+

+ +
${txt=sql-links.html} + +
+ + +Formater le codeExécution en cours... + +
+ + +${var=returndata} + + diff --git a/src/lunasql/http/res/jquery-1.12.3.min.js b/src/lunasql/http/res/jquery-1.12.3.min.js new file mode 100644 index 0000000..5a0ac9f --- /dev/null +++ b/src/lunasql/http/res/jquery-1.12.3.min.js @@ -0,0 +1,6 @@ +/*! jQuery v1.12.3 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; +}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("