diff options
author | Nicholas Bellinger <nab@risingtidesystems.com> | 2011-05-04 20:47:59 +0000 |
---|---|---|
committer | Nicholas Bellinger <nab@risingtidesystems.com> | 2011-05-04 20:47:59 +0000 |
commit | ce5bb566c2f3332911e986fada91836d2ef09479 (patch) | |
tree | 808b495f51fad553cac0b51fc4d83cefff452fcb | |
download | configshell-fb-ce5bb566c2f3332911e986fada91836d2ef09479.tar.gz |
Initial configshell commit0.9
Signed-off-by: Nicholas A. Bellinger <nab@risingtidesystems.com>
-rw-r--r-- | COPYING | 661 | ||||
-rw-r--r-- | Makefile | 85 | ||||
-rw-r--r-- | README | 24 | ||||
-rwxr-xr-x | bin/clean | 30 | ||||
-rwxr-xr-x | bin/gen_changelog | 46 | ||||
-rwxr-xr-x | bin/gen_changelog_cleanup | 20 | ||||
-rwxr-xr-x | bin/gendoc | 31 | ||||
-rw-r--r-- | configshell/__init__.py | 28 | ||||
-rw-r--r-- | configshell/console.py | 399 | ||||
-rw-r--r-- | configshell/log.py | 168 | ||||
-rw-r--r-- | configshell/node.py | 1664 | ||||
-rw-r--r-- | configshell/prefs.py | 149 | ||||
-rw-r--r-- | configshell/shell.py | 983 | ||||
-rw-r--r-- | debian/README.Debian | 13 | ||||
-rw-r--r-- | debian/compat | 1 | ||||
-rw-r--r-- | debian/configshell-doc.docs | 5 | ||||
-rw-r--r-- | debian/control | 24 | ||||
-rw-r--r-- | debian/copyright | 13 | ||||
-rw-r--r-- | debian/python-configshell.dirs | 1 | ||||
-rw-r--r-- | debian/python-configshell.docs | 3 | ||||
-rw-r--r-- | debian/python-configshell.install | 1 | ||||
-rwxr-xr-x | debian/python-configshell.postinst | 17 | ||||
-rwxr-xr-x | debian/python-configshell.preinst | 3 | ||||
-rwxr-xr-x | debian/python-configshell.prerm | 8 | ||||
-rw-r--r-- | debian/pyversions | 1 | ||||
-rwxr-xr-x | debian/rules | 48 | ||||
-rwxr-xr-x | examples/myshell | 181 | ||||
-rwxr-xr-x | setup.py | 43 |
28 files changed, 4650 insertions, 0 deletions
@@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<http://www.gnu.org/licenses/>. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55b6cdd --- /dev/null +++ b/Makefile @@ -0,0 +1,85 @@ +# This file is part of ConfigShell Community Edition. +# Copyright (c) 2011 by RisingTide Systems LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 (AGPLv3). +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +NAME = configshell +LIB = /usr/share +DOC = ${LIB}/doc/ +SETUP = ./setup.py +CLEAN = ./bin/clean +GENDOC = ./bin/gendoc + +all: usage +usage: + @echo "Usage:" + @echo " make install - Install configshell" + @echo " make installdocs - Install the documentation" + @echo "Developer targets:" + @echo " make packages - Generate the Debian and RPM packages" + @echo " make doc - Generate the documentation" + @echo " make clean - Cleanup the local repository" + @echo " make sdist - Build the source tarball" + @echo " make bdist - Build the installable tarball" + +install: + ${SETUP} install + +doc: + ./bin/gen_changelog + ${GENDOC} + +installdocs: doc + @test -e ${DOC} || \ + echo "Could not find ${DOC}; check the makefile variables." + @test -e ${DOC} + cp -r doc/* ${DOC}/${NAME}/ + +clean: + ${CLEAN} + ./bin/gen_changelog_cleanup + +packages: clean doc + dpkg-buildpackage -rfakeroot | tee dpkg-buildpackage.log + ./bin/gen_changelog_cleanup + grep "source version" dpkg-buildpackage.log | awk '{print $$4}' > dpkg-buildpackage.version + @test -e dist || mkdir dist + mv ../${NAME}_$$(cat dpkg-buildpackage.version).dsc dist + mv ../${NAME}_$$(cat dpkg-buildpackage.version)_*.changes dist + mv ../${NAME}_$$(cat dpkg-buildpackage.version).tar.gz dist + mv ../*${NAME}*$$(cat dpkg-buildpackage.version)*.deb dist + @test -e build || mkdir build + cd build; alien --scripts -k -g -r ../dist/configshell-doc_$$(cat ../dpkg-buildpackage.version)_all.deb + cd build/configshell-doc-*; mkdir usr/share/doc/packages + cd build/configshell-doc-*; mv usr/share/doc/configshell-doc usr/share/doc/packages/ + cd build/configshell-doc-*; perl -pi -e "s,/usr/share/doc/configshell-doc,/usr/share/doc/packages/configshell-doc,g" *.spec + cd build/configshell-doc-*; perl -pi -e "s,%%{ARCH},noarch,g" *.spec + cd build/configshell-doc-*; perl -pi -e "s,%post,%posttrans,g" *.spec + cd build/configshell-doc-*; rpmbuild --buildroot $$PWD -bb *.spec + cd build; alien --scripts -k -g -r ../dist/python-configshell_$$(cat ../dpkg-buildpackage.version)_all.deb; cd .. + cd build/python-configshell-*; mkdir usr/share/doc/packages + cd build/python-configshell-*; mv usr/share/doc/python-configshell usr/share/doc/packages/ + cd build/python-configshell-*; perl -pi -e "s,/usr/share/doc/python-configshell,/usr/share/doc/packages/python-configshell,g" *.spec + cd build/python-configshell-*; perl -pi -e 's/Group:/Requires: python >= 2.5\nGroup:/g' *.spec + cd build/python-configshell-*; perl -pi -e "s,%%{ARCH},noarch,g" *.spec + cd build/python-configshell-*; perl -pi -e "s,%post,%posttrans,g" *.spec + cd build/python-configshell-*; rpmbuild --buildroot $$PWD -bb *.spec + mv build/*.rpm dist + rm dpkg-buildpackage.log dpkg-buildpackage.version + +sdist: clean doc + ${SETUP} sdist + +bdist: clean doc + ${SETUP} bdist + @@ -0,0 +1,24 @@ +# This file is part of ConfigShell Community Edition. +# Copyright (c) 2011 by RisingTide Systems LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 (AGPLv3). +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +ConfigShell Community Edition is a python library that provides a framework +for building simple but nice CLI-based applications. + +For more information, see the configshell API reference, available in both html +and pdf formats as a separate package. + +To run the example shell from this directory use: +PYTHONPATH=. ./examples/myshell + diff --git a/bin/clean b/bin/clean new file mode 100755 index 0000000..6692f6c --- /dev/null +++ b/bin/clean @@ -0,0 +1,30 @@ +#!/bin/bash + +# This file is part of ConfigShell Community Edition. +# Copyright (c) 2011 by RisingTide Systems LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 (AGPLv3). +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +rm -v configshell/*.pyc configshell/*.html 2>/dev/null +rm -rvf doc doc/*.pdf doc/html doc/pdf doc/*.html 2>/dev/null +rm -rfv configshell.egg-info MANIFEST build dist 2>/dev/null +rm -rvf pdf html 2>/dev/null +rm -rvf debian/tmp 2>/dev/null +rm -v build-stamp 2>/dev/null +rm -v dpkg-buildpackage.log dpkg-buildpackage.version 2>/dev/null +rm -rfv *.rpm 2>/dev/null +rm -v debian/files debian/*.log debian/*.substvars 2>/dev/null +rm -rv debian/configshell-doc/ debian/python2.5-configshell/ debian/python2.6-configshell/ debian/python-configshell/ 2>/dev/null +rm -rv results 2>/dev/null +echo "Finished cleanup." + diff --git a/bin/gen_changelog b/bin/gen_changelog new file mode 100755 index 0000000..617a7cd --- /dev/null +++ b/bin/gen_changelog @@ -0,0 +1,46 @@ +#!/bin/bash + +# This file is part of ConfigShell Community Edition. +# Copyright (c) 2011 by RisingTide Systems LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 (AGPLv3). +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +NAME=$(git config --get user.name) +EMAIL=$(git config --get user.email) +DATE=$(date +'%a, %d %b %Y %H:%M:%S %z') + +SRCPKG=$(cat debian/control | grep ^Source | awk '{print $2}') + +LAST_TAG=$(git tag | tail -1) +if [ -z $LAST_TAG ]; then + LAST_TAG='0.0' +fi + +COMMIT=$(git log ${LAST_TAG}..HEAD | head -1 | colrm 1 7 | colrm 8) +TS=$(date +%Y%m%d%H%M%S) + +if [ -z $COMMIT ]; then + VERSION="${LAST_TAG}" +else + VERSION="${LAST_TAG}-${TS}.${COMMIT}" +fi + +sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" ${SRCPKG}/__init__.py + +echo "${SRCPKG} (${VERSION}) unstable; urgency=low" > debian/changelog +echo >> debian/changelog +echo " * Generated package." >> debian/changelog +echo >> debian/changelog +echo " -- ${NAME} <${EMAIL}> ${DATE}" >> debian/changelog + diff --git a/bin/gen_changelog_cleanup b/bin/gen_changelog_cleanup new file mode 100755 index 0000000..ee6565e --- /dev/null +++ b/bin/gen_changelog_cleanup @@ -0,0 +1,20 @@ +#!/bin/bash + +# This file is part of ConfigShell Community Edition. +# Copyright (c) 2011 by RisingTide Systems LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 (AGPLv3). +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +SRCPKG=$(cat debian/control | grep ^Source | awk '{print $2}') +rm -f debian/changelog +sed -i "s/__version__ = .*/__version__ = 'GIT_VERSION'/g" ${SRCPKG}/__init__.py diff --git a/bin/gendoc b/bin/gendoc new file mode 100755 index 0000000..317e1e7 --- /dev/null +++ b/bin/gendoc @@ -0,0 +1,31 @@ +#!/bin/bash + +# This file is part of ConfigShell Community Edition. +# Copyright (c) 2011 by RisingTide Systems LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 (AGPLv3). +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +path=$PWD +options='-n configshell configshell/*.py' + +rm -rf doc &>/dev/null +mkdir doc +epydoc --no-sourcecode --pdf -v $options +mkdir doc/pdf 2>/dev/null +mv pdf/api.pdf doc/pdf/configshell-API-reference.pdf +rm pdf -rf +epydoc --no-sourcecode --html $options +mv html doc/ +perl -pi -e "s/<\?/<!/g" doc/html/*.html +perl -pi -e "s/\?>/>/g" doc/html/*.html +cp README COPYING doc/ diff --git a/configshell/__init__.py b/configshell/__init__.py new file mode 100644 index 0000000..9bfcdb3 --- /dev/null +++ b/configshell/__init__.py @@ -0,0 +1,28 @@ +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +from log import Log +from console import Console +from shell import ConfigShell +from node import ConfigNode, ExecutionError +from prefs import Prefs + +__version__ = 'GIT_VERSION' +__author__ = "Jerome Martin <jxm@risingtidesystems.com>" +__url__ = "http://www.risingtidesystems.com" +__description__ = "A framework to implement simple but nice CLIs." +__license__ = __doc__ diff --git a/configshell/console.py b/configshell/console.py new file mode 100644 index 0000000..52558dd --- /dev/null +++ b/configshell/console.py @@ -0,0 +1,399 @@ +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import re +import sys +import tty +import fcntl +import prefs +import struct +import termios +import textwrap +import epydoc.markup.epytext + +class Console(object): + ''' + Implements various utility methods providing a console UI support toolkit, + most notably an epytext-to-console text renderer using ANSI escape + sequences. It uses the Borg pattern to share state between instances. + ''' + _max_width = 80 + _escape = '\033[' + _ansi_format = _escape + '%dm%s' + _ansi_reset = _escape + '0m' + _re_ansi_seq = re.compile('(\033\[..?m)') + + _ansi_styles = {'bold': 1, + 'underline': 4, + 'blink': 5, + 'reverse': 7, + 'concealed': 8} + + colors = ['black', 'red', 'green', 'yellow', + 'blue', 'magenta', 'cyan', 'white'] + + _ansi_fgcolors = dict(zip(colors, range(30, 38))) + _ansi_bgcolors = dict(zip(colors, range(40, 48))) + + __borg_state = {} + + def __init__(self, stdin=sys.stdin, stdout=sys.stdout): + ''' + Initializes a Console instance. + @param stdin: The console standard input. + @type stdin: file object + @param stdout: The console standard output. + @type stdout: file object + ''' + self.__dict__ = self.__borg_state + self._stdout = stdout + self._stdin = stdin + self.prefs = prefs.Prefs() + + # Public methods + + def escape(self, sequence, reply_terminator=None): + ''' + Sends an escape sequence to the console, and reads the reply terminated + by reply_terminator. If reply_terminator is not specified, the reply + will not be read. + @type sequence: str + @param reply_terminator: The expected end-of-reply marker. + @type reply_terminator: str + ''' + attributes = termios.tcgetattr(self._stdin) + tty.setraw(self._stdin) + try: + self.raw_write(self._escape + sequence) + if reply_terminator is not None: + reply = '' + while reply[-len(reply_terminator):] != reply_terminator: + reply += self._stdin.read(1) + finally: + termios.tcsetattr(self._stdin, termios.TCSADRAIN, attributes) + if reply_terminator is not None: + reply = reply[:-len(reply_terminator)] + reply = reply.replace(self._escape, '').split(';') + return reply + + def get_width(self): + ''' + Returns the console width + ''' + width = struct.unpack("HHHH", + fcntl.ioctl(self._stdout.fileno(), + termios.TIOCGWINSZ, + struct.pack("HHHH", 0, 0, 0, 0)))[1] + if width > self._max_width: + return self._max_width + else: + return width + + def get_cursor_xy(self): + ''' + Get the current text cursor x, y coordinates. + ''' + coords = [int(coord) for coord in self.escape("6n", "R")] + coords.reverse() + return coords + + def set_cursor_xy(self, xpos, ypos): + ''' + Set the cursor x, y coordinates. + @param xpos: The x coordinate of the cursor. + @type xpos: int + @param ypos: The y coordinate of the cursor. + @type ypos: int + ''' + self.escape("%d;%dH" % (ypos, xpos)) + + def raw_write(self, text): + ''' + Raw console printing function. + @param text: The text to print. + @type text: str + ''' + self._stdout.write(text) + self._stdout.flush() + + def display(self, text, no_lf=False): + ''' + Display a text with a default style. + @param text: Text to display + @type text: str + @param no_lf: Do not display a line feed. + @type no_lf: bool + ''' + text = self.render_text(text) + self.raw_write(text) + if not no_lf: + self.raw_write('\n') + + def epy_write(self, text): + ''' + Renders and print and epytext-formatted text on the console. + ''' + text = self.dedent(text) + try: + dom_tree = epydoc.markup.epytext.parse(text, None) + except: + self.display(text) + raise + text = self.render_domtree(dom_tree) + # We need to remove the last line feed, but there might be + # escape characters after it... + clean_text = '' + for index in range(1, len(text)): + if text[-index] == '\n': + clean_text = text[:-index] + if index != 1: + clean_text += text[-index+1:] + break + else: + clean_text = text + self.raw_write(clean_text) + + def indent(self, text, margin=2): + ''' + Indents text by margin space. + @param text: The text to be indented. + @type text: str + ''' + output = '' + for line in text.split('\n'): + output += margin * ' ' + line + '\n' + return output + + def dedent(self, text): + ''' + A convenience function to easily write multiline text blocks that + will be later assembled in to a unique epytext string. + It removes heading newline chars and common indentation. + ''' + for i in range(len(text)): + if text[i] != '\n': + break + text = text[i:] + text = textwrap.dedent(text) + text = '\n' * i + text + + return text + + def render_text(self, text, fgcolor=None, bgcolor=None, styles=None, + open_end=False, todefault=False): + ''' + Renders some text with ANSI console colors and attributes. + @param fgcolor: ANSI color to use for text: + black, red, green, yellow, blue, magenta. cyan. white + @type fgcolor: str + @param bgcolor: ANSI color to use for background: + black, red, green, yellow, blue, magenta. cyan. white + @type bgcolor: str + @param styles: List of ANSI styles to use: + bold, underline, blink, reverse, concealed + @type styles: list of str + @param open_end: Do not reset text style at the end ot the output. + @type open_end: bool + @param todefault: Instead of resetting style at the end of the + output, reset to default color. Only if not open_end. + @type todefault: bool + ''' + if self.prefs['color_mode']: + if fgcolor is None: + if self.prefs['color_default']: + fgcolor = self.prefs['color_default'] + if fgcolor is not None: + text = self._ansi_format % (self._ansi_fgcolors[fgcolor], text) + if bgcolor is not None: + text = self._ansi_format % (self._ansi_bgcolors[bgcolor], text) + if styles is not None: + for style in styles: + text = self._ansi_format % (self._ansi_styles[style], text) + if not open_end: + text += self._ansi_reset + if todefault and fgcolor is not None: + if self.prefs['color_default']: + text += self._ansi_format \ + % (self._ansi_fgcolors[ + self.prefs['color_default']], '') + return text + + def wordwrap(self, text, indent=0, startindex=0, splitchars=''): + ''' + Word-wrap the given string. I.e., add newlines to the string such + that any lines that are longer than terminal width or max_width + are broken into shorter lines (at the first whitespace sequence that + occurs before the limit. If the given string contains newlines, they + will I{not} be removed. Any lines that begin with whitespace will not + be wordwrapped. + + This version takes into account ANSI escape characters: + - stop escape sequence styling at the end of a split line + - start it again on the next line if needed after the indent + - do not account for the length of the escape sequences when + wrapping + + @param indent: If specified, then indent each line by this number + of spaces. + @type indent: C{int} + @param startindex: If specified, then assume that the first line + is already preceeded by C{startindex} characters. + @type startindex: C{int} + @param splitchars: A list of non-whitespace characters which can + be used to split a line. (E.g., use '/\\' to allow path names + to be split over multiple lines.) + @rtype: C{str} + ''' + right = self.get_width() + if splitchars: + chunks = re.split(r'( +|\n|[^ \n%s]*[%s])' % + (re.escape(splitchars), re.escape(splitchars)), + text.expandtabs()) + else: + chunks = re.split(r'( +|\n)', text.expandtabs()) + result = [' '*(indent-startindex)] + charindex = max(indent, startindex) + current_style = '' + for chunknum, chunk in enumerate(chunks): + chunk_groups = re.split(self._re_ansi_seq, chunk) + chunk_text = '' + next_style = current_style + + for group in chunk_groups: + if re.match(self._re_ansi_seq, group) is None: + chunk_text += group + else: + next_style += group + + chunk_len = len(chunk_text) + if (charindex + chunk_len > right and charindex > 0) \ + or chunk == '\n': + result[-1] = result[-1].rstrip() + result.append(self.render_text( + '\n' + ' '*indent + current_style, open_end=True)) + charindex = indent + if chunk[:1] not in ('\n', ' '): + result.append(chunk) + charindex += chunk_len + else: + result.append(chunk) + charindex += chunk_len + + current_style = next_style.split(self._ansi_reset)[-1] + + return ''.join(result).rstrip()+'\n' + + def render_domtree(self, tree, indent=0, seclevel=0): + ''' + Convert a DOM document encoding epytext to an 8-bits ascii string with + ANSI formating for simpler styles. + + @param tree: A DOM document encoding of an epytext string. + @type tree: C{Element} + @param indent: The indentation for the string representation of + C{tree}. Each line of the returned string will begin with + C{indent} space characters. + @type indent: C{int} + @param seclevel: The section level that C{tree} appears at. This + is used to generate section headings. + @type seclevel: C{int} + @return: The formated string. + @rtype: C{string} + ''' + if isinstance(tree, basestring): + return tree + + if tree.tag == 'section': + seclevel += 1 + + # Figure out the child indent level. + if tree.tag == 'epytext': + cindent = indent + elif tree.tag == 'li' and tree.attribs.get('bullet'): + cindent = indent + 1 + len(tree.attribs.get('bullet')) + else: + cindent = indent + 2 + + variables = [self.render_domtree(c, cindent, seclevel) + for c in tree.children] + childstr = ''.join(variables) + + if tree.tag == 'para': + text = self.render_text(childstr) + text = self.wordwrap(text, indent)+'\n' + elif tree.tag == 'li': + # We should be able to use getAttribute here; but there's no + # convenient way to test if an element has an attribute.. + bullet = tree.attribs.get('bullet') or '-' + text = indent*' ' + bullet + ' ' + childstr.lstrip() + elif tree.tag == 'heading': + text = ((indent-2)*' ' + self.render_text( + childstr, styles=['bold'], todefault=True) \ + + '\n') + elif tree.tag == 'doctestblock': + lines = [(indent+2)*' '+line for line in childstr.split('\n')] + text = '\n'.join(lines) + '\n\n' + elif tree.tag == 'literalblock': + lines = [(indent+1)*' '+ self.render_text( + line, todefault=True) + for line in childstr.split('\n')] + text = '\n'.join(lines) + '\n\n' + elif tree.tag == 'fieldlist': + text = childstr + elif tree.tag == 'field': + numargs = 0 + while tree.children[numargs+1].tag == 'arg': + numargs += 1 + args = variables[1:1+numargs] + body = variables[1+numargs:] + text = (indent)*' '+'@'+variables[0] + if args: + text += '(' + ', '.join(args) + ')' + text = text + ':\n' + ''.join(body) + elif tree.tag == 'uri': + if len(variables) != 2: + raise ValueError('Bad URI ') + elif variables[0] == variables[1]: + text = self.render_text( + '%s' % variables[1], + 'blue', styles=['underline'], todefault=True) + else: + text = '%r<%s>' % (variables[0], variables[1]) + elif tree.tag == 'link': + if len(variables) != 2: + raise ValueError('Bad Link') + text = '%s' % variables[0] + elif tree.tag in ('olist', 'ulist'): + text = childstr.replace('\n\n', '\n')+'\n' + elif tree.tag == 'bold': + text = self.render_text( + childstr, styles=['bold'], todefault=True) + elif tree.tag == 'italic': + text = self.render_text( + childstr, styles=['underline'], todefault=True) + elif tree.tag == 'symbol': + text = '%s' \ + % epydoc.markup.epytext.SYMBOL_TO_PLAINTEXT.get( + childstr, childstr) + elif tree.tag == 'graph': + text = '<<%s graph: %s>>' \ + % (variables[0], ', '.join(variables[1:])) + else: + # Assume that anything else can be passed through. + text = self.render_text(childstr) + + return text diff --git a/configshell/log.py b/configshell/log.py new file mode 100644 index 0000000..ffceb7f --- /dev/null +++ b/configshell/log.py @@ -0,0 +1,168 @@ +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import os +import sys +import time +import prefs +import inspect +import console +import traceback + +class Log(object): + ''' + Implements a file and console logger using python's logging facility. + Log levels are, in raising criticality: + - debug + - info + - warning + - error + - critical + It uses configshell's Prefs() backend for storing some of its parameters, + who can then be read/changed by other objects using Prefs() + ''' + __borg_state = {} + levels = ['critical', 'error', 'warning', 'info', 'debug'] + colors = {'critical': 'red', 'error': 'red', 'warning': 'red', + 'info': 'green', 'debug': 'blue'} + + def __init__(self, console_level=None, + logfile=None, file_level=None): + ''' + This class implements the Borg pattern. + @param console_level: Console log level, defaults to 'info' + @type console_level: str + @param logfile: Optional logfile. + @type logfile: str + @param file_level: File log level, defaults to 'debug'. + @type file_level: str + ''' + self.__dict__ = self.__borg_state + self.con = console.Console() + self.prefs = prefs.Prefs() + + if console_level: + self.prefs['loglevel_console'] = console_level + elif not self.prefs['loglevel_console']: + self.prefs['loglevel_console'] = 'info' + + if file_level: + self.prefs['loglevel_file'] = file_level + elif not self.prefs['loglevel_file']: + self.prefs['loglevel_file'] = 'debug' + + if logfile: + self.prefs['logfile'] = logfile + + # Private methods + + def _append(self, msg, level): + ''' + Just appends the message to the logfile if it exists, prefixing it with + the current time and level. + @param msg: The message to log + @type msg: str + @param level: The debug level to prefix the message with. + @type level: str + ''' + date_fields = time.localtime() + date = "%d-%02d-%02d %02d:%02d:%02d" \ + % (date_fields[0], date_fields[2], date_fields[1], + date_fields[3], date_fields[4], date_fields[5]) + + if self.prefs['logfile']: + path = os.path.expanduser(self.prefs['logfile']) + handle = open(path, 'a') + try: + handle.write("[%s] %s %s\n" % (level, date, msg)) + finally: + handle.close() + + def _log(self, level, msg): + ''' + Do the actual logging. + @param level: The log level of the message. + @type level: str + @param msg: The message to log. + @type msg: str + ''' + if self.levels.index(self.prefs['loglevel_file']) \ + >= self.levels.index(level): + self._append(msg, level.upper()) + + if self.levels.index(self.prefs['loglevel_console']) \ + >= self.levels.index(level): + if self.prefs["color_mode"]: + msg = self.con.render_text(msg, self.colors[level]) + else: + msg = "%s: %s" % (level.capitalize(), msg) + self.con.display(msg) + + # Public methods + + def debug(self, msg): + ''' + Logs a debug message. + @param msg: The message to log. + @type msg: str + ''' + caller = inspect.stack()[1] + msg = "%s:%d %s() %s" % (caller[1], caller[2], caller[3], msg) + self._log('debug', msg) + + def exception(self, msg=None): + ''' + Logs an error message and dumps a full stack trace. + @param msg: The message to log. + @type msg: str + ''' + trace = traceback.format_exc().rstrip() + if msg: + trace += '\n%s' % msg + self._log('error', trace) + + def info(self, msg): + ''' + Logs an info message. + @param msg: The message to log. + @type msg: str + ''' + self._log('info', msg) + + def warning(self, msg): + ''' + Logs a warning message. + @param msg: The message to log. + @type msg: str + ''' + self._log('warning', msg) + + def error(self, msg): + ''' + Logs an error message. + @param msg: The message to log. + @type msg: str + ''' + self._log('error', msg) + + def critical(self, msg): + ''' + Logs a critical message. + @param msg: The message to log. + @type msg: str + ''' + self._log('critical', msg) diff --git a/configshell/node.py b/configshell/node.py new file mode 100644 index 0000000..cf3d9b6 --- /dev/null +++ b/configshell/node.py @@ -0,0 +1,1664 @@ +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import re +import log +import copy +import prefs +import console +import inspect + +class ExecutionError(Exception): + pass + +class ConfigNode(object): + ''' + The ConfigNode class defines a common skeleton to be used by specific + implementation. It is "purely virtual" (sorry for using non-pythonic + vocabulary there ;-) ). + ''' + _path_separator = '/' + _path_current = '.' + _path_previous = '..' + + ui_command_method_prefix = "ui_command_" + ui_complete_method_prefix = "ui_complete_" + ui_setgroup_method_prefix = "ui_setgroup_" + ui_getgroup_method_prefix = "ui_getgroup_" + + def __init__(self): + self._name = 'config node' + self._children = set([]) + self._parent = None + + self.prefs = prefs.Prefs() + self.log = log.Log() + self.con = console.Console() + + self._configuration_groups = {} + self._configuration_groups['global'] = \ + {'tree_round_nodes': \ + [self.ui_type_onoff, + 'Tree node display style.'], + + 'tree_status_mode': \ + [self.ui_type_onoff, + 'Whether or not to display status in tree.'], + + 'tree_max_depth': \ + [self.ui_type_number, + 'Maximum depth of displayed node tree.'], + + 'tree_show_root': \ + [self.ui_type_onoff, + 'Whether or not to disply tree root.'], + + 'color_mode': \ + [self.ui_type_onoff, + 'Console color display mode.'], + + 'loglevel_console': \ + [self.ui_type_loglevel, + 'Log level for messages going to the console.'], + + 'loglevel_file': \ + [self.ui_type_loglevel, + 'Log level for messages going to the log file.'], + + 'logfile': \ + [self.ui_type_string, + 'Logfile to use.'], + + 'color_default': \ + [self.ui_type_colordefault, + 'Default text display color.'], + + 'color_path': \ + [self.ui_type_color, + 'Color to use for path completions'], + + 'color_command': \ + [self.ui_type_color, + 'Color to use for command completions.'], + + 'color_parameter': \ + [self.ui_type_color, + 'Color to use for parameter completions.'], + + 'color_keyword': \ + [self.ui_type_color, + 'Color to use for keyword completions.'], + + 'completions_in_columns': \ + [self.ui_type_onoff, + 'If B{on}, completions are displayed in columns, ' \ + + 'else in lines.'], + + 'prompt_length': \ + [self.ui_type_number, + 'Maximum length of the shell prompt path, 0 means infinite.'] + } + + if self.prefs['bookmarks'] is None: + self.prefs['bookmarks'] = {} + + # User interface types + + def ui_type_number(self, value=None, enum=False, reverse=False): + ''' + UI parameter type helper for number parameter type. + @param value: Value to check against the type. + @type value: anything + @param enum: Has a meaning only if value is omitted. If set, returns + a list of the possible values for the type, or [] if this is not + possible. If not set, returns a text description of the type format. + @type enum: bool + @param reverse: If set, translates an internal value to its UI + string representation. + @type reverse: bool + @return: c.f. parameter enum description. + @rtype: str|list|None + @raise ValueError: If the value does not check ok against the type. + ''' + if reverse: + if value is not None: + return str(value) + else: + return 'n/a' + + type_enum = [] + syntax = "NUMBER" + if value is None: + if enum: + return type_enum + else: + return syntax + elif not value: + return None + else: + try: + value = int(value) + except ValueError: + raise ValueError("Syntax error, '%s' is not a %s." \ + % (value, syntax)) + else: + return value + + def ui_type_string(self, value=None, enum=False, reverse=False): + ''' + UI parameter type helper for string parameter type. + @param value: Value to check against the type. + @type value: anything + @param enum: Has a meaning only if value is omitted. If set, returns + a list of the possible values for the type, or [] if this is not + possible. If not set, returns a text description of the type format. + @type enum: bool + @param reverse: If set, translates an internal value to its UI + string representation. + @type reverse: bool + @return: c.f. parameter enum description. + @rtype: str|list|None + @raise ValueError: If the value does not check ok against the type. + ''' + if reverse: + if value is not None: + return value + else: + return 'n/a' + + type_enum = [] + syntax = "STRING_OF_TEXT" + if value is None: + if enum: + return type_enum + else: + return syntax + elif not value: + return None + else: + try: + value = str(value) + except ValueError: + raise ValueError("Syntax error, '%s' is not a %s." \ + % (value, syntax)) + else: + return value + + def ui_type_onoff(self, value=None, enum=False, reverse=False): + ''' + UI parameter type helper for on/off parameter type. + @param value: Value to check against the type. + @type value: anything + @param enum: Has a meaning only if value is omitted. If set, returns + a list of the possible values for the type, or None if this is not + possible. If not set, returns a text description of the type format. + @type enum: bool + @param reverse: If set, translates an internal value to its UI + string representation. + @type reverse: bool + @return: c.f. parameter enum description. + @rtype: str|list|None + @raise ValueError: If the value does not check ok againts the type. + ''' + if reverse: + if value: + return 'on' + else: + return 'off' + type_enum = ['on', 'off'] + syntax = '|'.join(type_enum) + if value is None: + if enum: + return type_enum + else: + return syntax + elif value == 'on': + return True + elif value == 'off': + return False + else: + raise ValueError("Syntax error, '%s' is not %s." \ + % (value, syntax)) + + def ui_type_loglevel(self, value=None, enum=False, reverse=False): + ''' + UI parameter type helper for log level parameter type. + @param value: Value to check against the type. + @type value: anything + @param enum: Has a meaning only if value is omitted. If set, returns + a list of the possible values for the type, or None if this is not + possible. If not set, returns a text description of the type format. + @type enum: bool + @param reverse: If set, translates an internal value to its UI + string representation. + @type reverse: bool + @return: c.f. parameter enum description. + @rtype: str|list|None + @raise ValueError: If the value does not check ok againts the type. + ''' + if reverse: + if value is not None: + return value + else: + return 'n/a' + + type_enum = self.log.levels + syntax = '|'.join(type_enum) + if value is None: + if enum: + return type_enum + else: + return syntax + elif value in type_enum: + return value + else: + raise ValueError("Syntax error, '%s' is not %s" \ + % (value, syntax)) + + def ui_type_color(self, value=None, enum=False, reverse=False): + ''' + UI parameter type helper for color parameter type. + @param value: Value to check against the type. + @type value: anything + @param enum: Has a meaning only if value is omitted. If set, returns + a list of the possible values for the type, or None if this is not + possible. If not set, returns a text description of the type format. + @type enum: bool + @param reverse: If set, translates an internal value to its UI + string representation. + @type reverse: bool + @return: c.f. parameter enum description. + @rtype: str|list|None + @raise ValueError: If the value does not check ok againts the type. + ''' + if reverse: + if value is not None: + return value + else: + return 'default' + + type_enum = self.con.colors + ['default'] + syntax = '|'.join(type_enum) + if value is None: + if enum: + return type_enum + else: + return syntax + elif not value or value == 'default': + return None + elif value in type_enum: + return value + else: + raise ValueError("Syntax error, '%s' is not %s" \ + % (value, syntax)) + + def ui_type_colordefault(self, value=None, enum=False, reverse=False): + ''' + UI parameter type helper for default color parameter type. + @param value: Value to check against the type. + @type value: anything + @param enum: Has a meaning only if value is omitted. If set, returns + a list of the possible values for the type, or None if this is not + possible. If not set, returns a text description of the type format. + @type enum: bool + @param reverse: If set, translates an internal value to its UI + string representation. + @type reverse: bool + @return: c.f. parameter enum description. + @rtype: str|list|None + @raise ValueError: If the value does not check ok againts the type. + ''' + if reverse: + if value is not None: + return value + else: + return 'none' + + type_enum = self.con.colors + ['none'] + syntax = '|'.join(type_enum) + if value is None: + if enum: + return type_enum + else: + return syntax + elif not value or value == 'none': + return None + elif value in type_enum: + return value + else: + raise ValueError("Syntax error, '%s' is not %s" \ + % (value, syntax)) + + + # User interface get/set methods + + def ui_setgroup_global(self, parameter, value): + ''' + This is the backend method for setting parameters in configuration + group 'global'. It simply uses the Prefs() backend to store the global + preferences for the shell. Some of these group parameters are shared + using the same Prefs() object by the Log() and Console() classes, so + this backend should not be changed without taking this into + consideration. + + The parameters getting to us have already been type-checked and casted + by the type-check methods registered in the config group via the ui set + command, and their existence in the group has also been checked. Thus + our job is minimal here. Also, it means that overhead when called with + generated arguments (as opposed to user-supplied) gets minimal + overhead, and allows setting new parameters without error. + + @param parameter: The parameter to set. + @type parameter: str + @param value: The value + @type value: arbitrary + ''' + self.prefs[parameter] = value + + def ui_getgroup_global(self, parameter): + ''' + This is the backend method for getting configuration parameters out of + the B{global} configuration group. It gets the values from the Prefs() + backend. Eventual casting to str for UI display is handled by the ui + get command, for symmetry with the pendant ui_setgroup method. + Existence of the parameter in the group should have already been + checked by the ui get command, so we go blindly about this. This might + allow internal client code to get a None value if the parameter does + not exist, as supported by Prefs(). + + @param parameter: The parameter to get the value of. + @type parameter: str + @return: The parameter's value + @rtype: arbitrary + ''' + return self.prefs[parameter] + + # User interface commands + + def ui_command_set(self, group=None, **parameter): + ''' + Sets one or more configuration parameters in the given group. + The B{global} group contains all global CLI preferences. + Other groups are specific to the current path. + + Run with no parameter nor group to list all available groups, or + with just a group name to list all available parameters within that + group. + + Example: B{set global color_mode=on loglevel_console=info} + + SEE ALSO + ======== + get + ''' + if group is None: + self.con.epy_write(''' + AVAILABLE CONFIGURATION GROUPS + ============================== + %s + ''' % ' '.join(self._configuration_groups)) + elif not parameter: + if group in self._configuration_groups: + section = "%s PARAMETERS" % group.upper() + underline1 = ''.ljust(len(section), '=') + parameters = '' + for parameter, param_def \ + in self._configuration_groups[group].iteritems(): + (type_helper, description) = param_def + parameter += '=I{' + type_helper() + '}' + underline2 = ''.ljust(len(parameter), '-') + parameters += '%s\n%s\n%s\n\n' \ + % (parameter, underline2, description) + + self.con.epy_write('''%s\n%s\n%s\n''' \ + % (section, underline1, parameters)) + else: + self.log.error("Unknown configuration group: %s" % group) + elif group in self._configuration_groups: + for param, value in parameter.iteritems(): + if param in self._configuration_groups[group]: + type_helper = self._configuration_groups[group][param][0] + try: + value = type_helper(value) + except ValueError, msg: + self.log.error("Not setting %s! %s" % (param, msg)) + else: + group_setter = self.get_group_setter(group) + group_setter(param, value) + group_getter = self.get_group_getter(group) + value = group_getter(param) + value = type_helper(value, reverse=True) + self.con.display("Parameter %s has been set to '%s'." \ + % (param, value)) + else: + self.log.error( + "There is no parameter named '%s' in group '%s'." \ + % (param, group)) + else: + self.log.error("Unknown configuration group: %s" % group) + + def ui_complete_set(self, parameters, text, current_param): + ''' + Parameter auto-completion method for user command set. + @param parameters: Parameters on the command line. + @type parameters: dict + @param text: Current text of parameter being typed by the user. + @type text: str + @param current_param: Name of parameter to complete. + @type current_param: str + @return: Possible completions + @rtype: list of str + ''' + completions = [] + + self.log.debug("Called with parameters=%s, text='%s', current='%s'" \ + % (str(parameters), text, current_param)) + + if current_param == 'group': + completions = [group for group in self._configuration_groups + if group.startswith(text)] + elif 'group' in parameters: + group = parameters['group'] + if group in self._configuration_groups: + group_params = self._configuration_groups[group] + if current_param in group_params: + type_method = group_params[current_param][0] + type_enum = type_method(enum=True) + if type_enum is not None: + type_enum = [item for item in type_enum + if item.startswith(text)] + completions.extend(type_enum) + else: + group_params = ([param + '=' for param in group_params + if param.startswith(text) + if param not in parameters + ]) + if group_params: + completions.extend(group_params) + + if len(completions) == 1 and not completions[0].endswith('='): + completions = [completions[0] + ' '] + + self.log.debug("Returning completions %s." % str(completions)) + return completions + + def ui_command_get(self, group=None, *parameter): + ''' + Gets the value of one or more configuration parameters in the given + group. + + Run with no parameter nor group to list all available groups, or + with just a group name to list all available parameters within that + group. + + Example: B{get global color_mode loglevel_console} + + SEE ALSO + ======== + set + ''' + if group is None: + self.con.epy_write(''' + AVAILABLE CONFIGURATION GROUPS + ============================== + %s + ''' % ' '.join(self._configuration_groups)) + elif not parameter: + if group in self._configuration_groups: + section = "%s PARAMETERS" % group.upper() + underline1 = ''.ljust(len(section), '=') + parameters = '' + params = self._configuration_groups[group].items() + params.sort() + for parameter, param_def in params: + description = param_def[1] + group_getter = self.get_group_getter(group) + value = group_getter(parameter) + group_params = self._configuration_groups[group] + type_method = group_params[parameter][0] + value = type_method(value, reverse=True) + parameter = parameter + '=' + value + underline2 = ''.ljust(len(parameter), '-') + parameters += '%s\n%s\n%s\n\n' \ + % (parameter, underline2, description) + + self.con.epy_write('''%s\n%s\n%s\n''' \ + % (section, underline1, parameters)) + else: + self.log.error("Unknown configuration group: %s" % group) + elif group in self._configuration_groups: + for param in parameter: + if param in self._configuration_groups[group]: + self.log.debug("About to get the parameter's value.") + group_getter = self.get_group_getter(group) + value = group_getter(param) + group_params = self._configuration_groups[group] + type_method = group_params[param][0] + value = type_method(value, reverse=True) + self.con.display("%s=%s" % (param, value)) + else: + self.log.error( + "There is no parameter named '%s' in group '%s'." \ + % (param, group)) + else: + self.log.error("Unknown configuration group: %s" % group) + + def ui_complete_get(self, parameters, text, current_param): + ''' + Parameter auto-completion method for user command get. + @param parameters: Parameters on the command line. + @type parameters: dict + @param text: Current text of parameter being typed by the user. + @type text: str + @param current_param: Name of parameter to complete. + @type current_param: str + @return: Possible completions + @rtype: list of str + ''' + completions = [] + + self.log.debug("Called with parameters=%s, text='%s', current='%s'" \ + % (str(parameters), text, current_param)) + + if current_param == 'group': + completions = [group for group in self._configuration_groups + if group.startswith(text)] + elif 'group' in parameters: + group = parameters['group'] + if group in self._configuration_groups: + group_params = self._configuration_groups[group] + group_params = ([param for param in group_params + if param.startswith(text) + if param not in parameters + ]) + if group_params: + completions.extend(group_params) + + if len(completions) == 1 and not completions[0].endswith('='): + completions = [completions[0] + ' '] + + self.log.debug("Returning completions %s." % str(completions)) + return completions + + def ui_command_ls(self, path=None, depth=None): + ''' + Display either the nodes tree relative to path or to the current node. + + PARAMETERS + ========== + + I{path} + ------- + The I{path} to display the nodes tree of. Can be an absolute path, a + relative path or a bookmark. + + I{depth} + -------- + The I{depth} parameter limits the maximum depth of the tree to display. + If set to 0, then the complete tree will be displayed (the default). + + SEE ALSO + ======== + cd bookmarks + ''' + try: + target = self.get_node(path) + except ValueError, msg: + self.log.error(msg) + return + + if depth is None: + depth = self.prefs['tree_max_depth'] + try: + depth = int(depth) + except ValueError: + self.log.error('The tree depth must be a number.') + else: + if depth == 0: + depth = None + tree = self._render_tree(target, depth=depth) + self.con.display(tree) + + def _render_tree(self, root, margin=None, depth=None, do_list=False): + ''' + Renders an ascii representation of a tree of ConfigNodes. + @param root: The root node of the tree + @type root: ConfigNode + @param margin: Format of the left margin to use for children. + True results in a pipe, and False results in no pipe. + Used for recursion only. + @type margin: list + @param depth: The maximum depth of nodes to display, None means + infinite. + @type depth: None or int + @param do_list: Return two lists, one with each line text + representation, the other with the corresponding paths. + @type do_list: bool + @return: An ascii tree representation or (lines, paths). + @rtype: str + ''' + lines = [] + paths = [] + + node_length = 2 + node_shift = 2 + level = root.path.rstrip('/').count('/') + if margin is None: + margin = [0] + root_call = True + else: + root_call = False + + if do_list: + color = None + elif not level % 3: + color = None + elif not (level - 1) % 3: + color = 'blue' + else: + color = 'magenta' + + if do_list: + styles = None + elif root_call: + styles = ['bold', 'underline'] + else: + styles = ['bold'] + + if do_list: + name = root.name + else: + name = self.con.render_text(root.name, color, styles=styles) + name_len = len(root.name) + + (description, is_healthy) = root.summary() + if not description: + if is_healthy is True: + description = "OK" + elif is_healthy is False: + description = "ERROR" + else: + description = "..." + + description_len = len(description) + 3 + + if do_list: + summary = '[' + else: + summary = self.con.render_text(' [', styles=['bold']) + + if is_healthy is True: + if do_list: + summary += description + else: + summary += self.con.render_text(description, 'green') + elif is_healthy is False: + if do_list: + summary += description + else: + summary += self.con.render_text(description, 'red', + styles=['bold']) + else: + summary += description + + if do_list: + summary += ']' + else: + summary += self.con.render_text(']', styles=['bold']) + + children = list(root.children) + children.sort(key=lambda child: str(child)) + line = "" + + for pipe in margin[:-1]: + if pipe: + line = line + "|".ljust(node_shift) + else: + line = line + ''.ljust(node_shift) + + if self.prefs['tree_round_nodes']: + node_char = 'o' + else: + node_char = '+' + line += node_char.ljust(node_length, '-') + margin_len = len(line) + + pad = (self.con.get_width() - 1 + - description_len + - margin_len + - name_len) * '.' + if not do_list: + pad = self.con.render_text(pad, color) + + line += name + if self.prefs['tree_status_mode']: + line += ' %s%s' % (pad, summary) + + lines.append(line) + paths.append(root.path) + + if root_call and not self.prefs['tree_show_root'] and not do_list: + tree = '' + for child in children: + tree = tree + self._render_tree(child, [False], depth) + else: + tree = line + '\n' + if depth is None or depth > 0: + if depth is not None: + depth = depth - 1 + for i in range(len(children)): + margin.append(i<len(children)-1) + if do_list: + new_lines, new_paths = \ + self._render_tree(children[i], margin, depth, + do_list=True) + lines.extend(new_lines) + paths.extend(new_paths) + else: + tree = tree \ + + self._render_tree(children[i], margin, depth) + margin.pop() + + if root_call: + if do_list: + return (lines, paths) + else: + return tree[:-1] + else: + if do_list: + return (lines, paths) + else: + return tree + + + def ui_complete_ls(self, parameters, text, current_param): + ''' + Parameter auto-completion method for user command ls. + @param parameters: Parameters on the command line. + @type parameters: dict + @param text: Current text of parameter being typed by the user. + @type text: str + @param current_param: Name of parameter to complete. + @type current_param: str + @return: Possible completions + @rtype: list of str + ''' + if current_param == 'path': + (basedir, slash, partial_name) = text.rpartition('/') + basedir = basedir + slash + target = self.get_node(basedir) + names = [child.name for child in target.children] + completions = [] + for name in names: + num_matches = 0 + if name.startswith(partial_name): + num_matches += 1 + if num_matches == 1: + completions.append("%s%s/" % (basedir, name)) + else: + completions.append("%s%s" % (basedir, name)) + if len(completions) == 1: + if not self.get_node(completions[0]).children: + completions[0] = completions[0].rstrip('/') + ' ' + + # Bookmarks + bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks'] + if ('@' + bookmark).startswith(text)] + self.log.debug("Found bookmarks %s." % str(bookmarks)) + if bookmarks: + completions.extend(bookmarks) + + self.log.debug("Completions are %s." % str(completions)) + return completions + + elif current_param == 'depth': + if text: + try: + int(text.strip()) + except ValueError: + self.log.debug("Text is not a number.") + return [] + return [ text + number for number + in [str(num) for num in range(10)] + if (text + number).startswith(text)] + + def ui_command_cd(self, path=None): + ''' + Change current work path to path. + + The path is constructed just like a unix path, with B{/} as separator + character, B{.} for the current node, B{..} for the parent node. + + Suppose the nodes tree looks like this:: + +-/ + +-a0 (1) + | +-b0 (*) + | +-c0 + +-a1 (3) + +-b0 + +-c0 + +-d0 (2) + + Suppose the current node is the one marked (*) at the beginning of all + the following examples: + - B{cd ..} takes you to the node marked (1) + - B{cd .} makes you stay in (*) + - B{cd /a1/b0/c0/d0} takes you to the node marked (2) + - B{cd ../../a1} takes you to the node marked (3) + - B{cd /a1} also takes you to the node marked (3) + - B{cd /} takes you to the root node B{/} + - B{cd /a0/b0/./c0/../../../a1/.} takes you to the node marked (3) + + You can also navigate the path history with B{<} and B{>}: + - B{cd <} takes you back one step in the path history + - B{cd >} takes you one step forward in the path history + + SEE ALSO + ======== + ls cd + ''' + self.log.debug("Changing current node to '%s'." % path) + + if self.prefs['path_history'] is None: + self.prefs['path_history'] = [self.path] + self.prefs['path_history_index'] = 0 + + # Go back in history to the last existing path + if path == '<': + if self.prefs['path_history_index'] == 0: + self.log.info("Reached begining of path history.") + return self + exists = False + while not exists: + if self.prefs['path_history_index'] > 0: + self.prefs['path_history_index'] = \ + self.prefs['path_history_index'] - 1 + index = self.prefs['path_history_index'] + path = self.prefs['path_history'][index] + try: + target_node = self.get_node(path) + except ValueError: + pass + else: + exists = True + else: + path = '/' + self.prefs['path_history_index'] = 0 + self.prefs['path_history'][0] = '/' + exists = True + self.log.info('Taking you back to %s.' % path) + return self.get_node(path) + + # Go forward in history + if path == '>': + if self.prefs['path_history_index'] == \ + len(self.prefs['path_history']) - 1: + self.log.info("Reached the end of path history.") + return self + exists = False + while not exists: + if self.prefs['path_history_index'] \ + < len(self.prefs['path_history']) - 1: + self.prefs['path_history_index'] = \ + self.prefs['path_history_index'] + 1 + index = self.prefs['path_history_index'] + path = self.prefs['path_history'][index] + try: + target_node = self.get_node(path) + except ValueError: + pass + else: + exists = True + else: + path = self.path + self.prefs['path_history_index'] \ + = len(self.prefs['path_history']) + self.prefs['path_history'].append(path) + exists = True + self.log.info('Taking you back to %s.' % path) + return self.get_node(path) + + # Use an urwid walker to select the path + if path is None: + lines, paths = self._render_tree(self.get_root(), do_list=True) + start_pos = paths.index(self.path) + selected = self._lines_walker(lines, start_pos=start_pos) + path = paths[selected] + + # Normal path + try: + target_node = self.get_node(path) + except ValueError, msg: + self.log.error(msg) + return None + else: + index = self.prefs['path_history_index'] + if target_node.path != self.prefs['path_history'][index]: + # Truncate the hostory to retain current path as last one + self.prefs['path_history'] = \ + self.prefs['path_history'][:index+1] + # Append the new path and update the index + self.prefs['path_history'].append(target_node.path) + self.prefs['path_history_index'] = index + 1 + self.log.debug("After cd, path history is: %s, index is %d" \ + % (str(self.prefs['path_history']), + self.prefs['path_history_index'])) + return target_node + + def _lines_walker(self, lines, start_pos): + ''' + Using the curses urwid library, displays all lines passed as argument, + and after allowing selection of one line using up, down and enter keys, + returns its index. + @param lines: The lines to display and select from. + @type lines: list of str + @param start_pos: The index of the line to select initially. + @type start_pos: int + @return: the index of the selected line. + @rtype: int + ''' + import urwid + + class Selected(Exception): + pass + + palette = [('header', 'white', 'black'), + ('reveal focus', 'black', 'yellow', 'standout')] + + content = urwid.SimpleListWalker( + [urwid.AttrMap(w, None, 'reveal focus') + for w in [urwid.Text(line) for line in lines]]) + + listbox = urwid.ListBox(content) + frame = urwid.Frame(listbox) + + def handle_input(input, raw): + for key in input: + widget, pos = content.get_focus() + if unicode(key) == 'up': + if pos > 0: + content.set_focus(pos-1) + elif unicode(key) == 'down': + content.set_focus(pos+1) + elif unicode(key) == 'enter': + raise Selected(pos) + + content.set_focus(start_pos) + loop = urwid.MainLoop(frame, palette, input_filter=handle_input) + try: + loop.run() + except Selected, pos: + return int(str(pos)) + + def ui_complete_cd(self, parameters, text, current_param): + ''' + Parameter auto-completion method for user command cd. + @param parameters: Parameters on the command line. + @type parameters: dict + @param text: Current text of parameter being typed by the user. + @type text: str + @param current_param: Name of parameter to complete. + @type current_param: str + @return: Possible completions + @rtype: list of str + ''' + if current_param == 'path': + completions = self.ui_complete_ls(parameters, text, current_param) + completions.extend([nav for nav in ['<', '>'] + if nav.startswith(text)]) + return completions + + def ui_command_man(self, topic=None): + ''' + Displays the manual page for a topic, or list available topics. + ''' + commands = self.list_commands() + if topic is None: + msg = self.con.epy_write(''' + COMMAND MANUALS + =============== + %s + + ''' % ' '.join(commands)) + elif topic in commands: + syntax, comments, defaults = self.get_command_syntax(topic) + msg = self.con.dedent(''' + SYNTAX + ====== + %s + + ''' % syntax) + for comment in comments: + msg += comment + '\n' + + if defaults: + msg += self.con.dedent(''' + DEFAULT VALUES + ============== + %s + + ''' % defaults) + msg += self.con.dedent(''' + DESCRIPTION + =========== + ''') + msg += self.get_command_description(topic) + self.con.epy_write(msg) + else: + self.log.error("Cannot find help topic %s." % topic) + + def ui_complete_man(self, parameters, text, current_param): + ''' + Parameter auto-completion method for user command man. + @param parameters: Parameters on the command line. + @type parameters: dict + @param text: Current text of parameter being typed by the user. + @type text: str + @param current_param: Name of parameter to complete. + @type current_param: str + @return: Possible completions + @rtype: list of str + ''' + if current_param == 'topic': + # TODO Add other types of topics + topics = self.list_commands() + completions = [topic for topic in topics + if topic.startswith(text)] + else: + completions = [] + + if len(completions) == 1: + return [completions[0] + ' '] + else: + return completions + + def ui_command_exit(self): + ''' + Exits the command line interface. + ''' + return 'EXIT' + + def ui_command_bookmarks(self, action, bookmark=None): + ''' + Manage your bookmarks. + + Note that you can also access your bookmarks with the + B{cd} command. For instance, the following commands + are equivalent: + - B{cd mybookmark} + - C{bookmarks go mybookmark} + + You can also use bookmarks anywhere where you would use + a normal path: + - B{@mybookmark ls} would perform the B{ls} command + in the bookmarked path. + - B{ls @mybookmark} would show you the objects tree from + the bookmarked path. + + + PARAMETERS + ========== + + I{action} + --------- + The I{action} is one of: + - B{add} adds the current path to your bookmarks. + - B{del} deletes a bookmark. + - B{go} takes you to a bookmarked path. + - B{show} shows you all your bookmarks. + + I{bookmark} + ----------- + This is the name of the bookmark. + + SEE ALSO + ======== + ls cd + ''' + if action == 'add' and bookmark: + if bookmark in self.prefs['bookmarks']: + self.log.error("Bookmark %s already exists." % bookmark) + else: + self.prefs['bookmarks'][bookmark] = self.path + # No way Prefs is going to account for that :-( + self.prefs.save() + self.log.info("Bookmarked %s as %s." % (self.path, bookmark)) + elif action == 'del' and bookmark: + if bookmark in self.prefs['bookmarks']: + del self.prefs['bookmarks'][bookmark] + # No way Prefs is going to account for that deletion + self.prefs.save() + self.log.info("Deleted bookmark %s." % bookmark) + else: + self.log.error("No such bookmark %s." % bookmark) + elif action == 'go' and bookmark: + if bookmark in self.prefs['bookmarks']: + return self.ui_command_cd(self.prefs['bookmarks'][bookmark]) + else: + self.log.error("No such bookmark %s." % bookmark) + elif action == 'show': + bookmarks = self.con.dedent(''' + BOOKMARKS + ========= + + ''') + if not self.prefs['bookmarks']: + bookmarks += "No bookmarks yet.\n" + else: + for (bookmark, path) \ + in self.prefs['bookmarks'].iteritems(): + if len(bookmark) == 1: + bookmark += '\0' + underline = ''.ljust(len(bookmark), '-') + bookmarks += "%s\n%s\n%s\n\n" % (bookmark, underline, path) + self.con.epy_write(bookmarks) + else: + self.log.error("Syntax error, see 'man bookmarks'.") + + def ui_complete_bookmarks(self, parameters, text, current_param): + ''' + Parameter auto-completion method for user command bookmarks. + @param parameters: Parameters on the command line. + @type parameters: dict + @param text: Current text of parameter being typed by the user. + @type text: str + @param current_param: Name of parameter to complete. + @type current_param: str + @return: Possible completions + @rtype: list of str + ''' + if current_param == 'action': + completions = [action for action in ['add', 'del', 'go', 'show'] + if action.startswith(text)] + elif current_param == 'bookmark': + if 'action' in parameters: + if parameters['action'] not in ['show', 'add']: + completions = [mark for mark in self.prefs['bookmarks'] + if mark.startswith(text)] + else: + completions = [] + + if len(completions) == 1: + return [completions[0] + ' '] + else: + return completions + + def ui_command_pwd(self): + ''' + Displays the current working path. + + SEE ALSO + ======== + ls cd + ''' + self.con.display(self.path) + + # Private methods + + def __str__(self): + if self.is_root(): + return '/' + else: + return self.name + + def _get_parent(self): + ''' + Get this node's parent. + @return: The node's parent. + @rtype: ConfigNode + ''' + return self._parent + + def _set_parent(self, parent): + ''' + Sets the node's parent. Works only if it does not already have one. + @param parent: The parent node to assign. + @type parent: ConfigNode + ''' + if self.is_root(): + self._parent = parent + else: + raise AttributeError("Node %s already has a parent" % self._name) + + def _get_name(self): + ''' + @return: The node's name. + @rtype: str + ''' + return self._name + + def _set_name(self, name): + ''' + Sets the node's name. + @param name: The new node name. + @type name: str + ''' + self._name = str(name) + + def _get_path(self): + ''' + @returns: The absolute path for this node. + @rtype: str + ''' + subpath = self._path_separator + self.name + if self.is_root(): + return self._path_separator + elif self._parent.is_root(): + return subpath + else: + return self._parent.path + subpath + + def _list_children(self): + ''' + Lists the children of this node. + @return: The set of children nodes. + @rtype: set of ConfigNode + ''' + return self._children + + # Public methods + + def summary(self): + ''' + Returns a tuple with a status/description string for this node and a + health flag, to be displayed along the node's name in object trees, + etc. + @returns: (description, is_healthy) + @rtype: (str, bool or None) + ''' + return ('', None) + + def execute_command(self, command, pparams=[], kparams={}): + ''' + Execute a user command on the node. This works by finding out which is + the support command method, using ConfigNode naming convention: + The support method's name is 'PREFIX_COMMAND', where PREFIX is defined + by ConfigNode.ui_command_method_prefix and COMMAND is the commands's + name as seen by the user. + @param command: Name of the command. + @type command: str + @param pparams: The positional parameters to use. + @type pparams: list + @param kparams: The keyword=value parameters to use. + @type kparams: dict + @return: The support method's return value. + See ConfigShell._execute_command() for expected return values and how + they are interpreted by ConfigShell. + @rtype: str or ConfigNode or None + ''' + self.log.debug("Executing command %s " % command \ + + "with pparams %s " % str(pparams) \ + + "and kparams %s." % str(kparams)) + + if command in self.list_commands(): + method = self.get_command_method(command) + else: + self.log.error("Command not found %s" % command) + return + + try: + result = method(*pparams, **kparams) + except TypeError, msg: + # TODO Find a cleaner way to do this + msg = str(msg) + if "takes at most" in msg \ + or "takes exactly" in msg \ + or "takes at least" in msg \ + or "unexpected keyword" in msg: + self.log.error("Wrong parameters for %s, see 'man %s'."\ + % (command, command)) + else: + raise + except ExecutionError, msg: + self.log.error(msg) + else: + return result + + def list_commands(self): + ''' + @return: The list of user commands available for this node. + @rtype: list of str + ''' + prefix = self.ui_command_method_prefix + prefix_len = len(prefix) + return tuple([name[prefix_len:] for name in dir(self) + if name.startswith(prefix) and name != prefix + and inspect.ismethod(getattr(self, name))]) + + def get_group_getter(self, group): + ''' + @param group: A valid configuration group + @type group: str + @return: The getter method for the configuration group. + @rtype: method object + ''' + prefix = self.ui_getgroup_method_prefix + return getattr(self, '%s%s' % (prefix, group)) + + def get_group_setter(self, group): + ''' + @param group: A valid configuration group + @type group: str + @return: The setter method for the configuration group. + @rtype: method object + ''' + prefix = self.ui_setgroup_method_prefix + return getattr(self, '%s%s' % (prefix, group)) + + def get_command_method(self, command): + ''' + @param command: The command to get the method for. + @type command: str + @return: The user command support method. + @rtype: method + @raise ValueError: If the command is not found. + ''' + prefix = self.ui_command_method_prefix + if command in self.list_commands(): + return getattr(self, '%s%s' % (prefix, command)) + else: + self.log.debug('No command named %s in %s (%s)' \ + % (command, self.name, self.path)) + raise ValueError('No command named "%s".' % command) + + def get_completion_method(self, command): + ''' + @return: A user command's completion method or None. + @rtype: method or None + @param command: The command to get the completion method for. + @type command: str + ''' + prefix = self.ui_complete_method_prefix + try: + method = getattr(self, '%s%s' % (prefix, command)) + except AttributeError: + return None + else: + return method + + def get_command_description(self, command): + ''' + @return: An description string for a user command. + @rtype: str + @param command: The command to describe. + @type command: str + ''' + doc = self.get_command_method(command).__doc__ + if not doc: + doc = "No description available." + return self.con.dedent(doc) + + def get_command_syntax(self, command): + ''' + @return: A list of formatted syntax descriptions for the command: + - (syntax, comments, default_values) + - syntax is the syntax definition line. + - comments is a list of additionnal comments about the syntax. + - default_values is a string with the default parameters values. + @rtype: (str, [str...], str) + @param command: The command to document. + @type command: str + ''' + method = self.get_command_method(command) + parameters, args, kwargs, default = inspect.getargspec(method) + parameters = parameters[1:] + if default is None: + num_defaults = 0 + else: + num_defaults = len(default) + + if num_defaults != 0: + required_parameters = parameters[:-num_defaults] + optional_parameters = parameters[-num_defaults:] + else: + required_parameters = parameters + optional_parameters = [] + + self.log.debug("Required: %s" % str(required_parameters)) + self.log.debug("Optional: %s" % str(optional_parameters)) + + syntax = "B{%s} " % command + + required_parameters_str = '' + for param in required_parameters: + required_parameters_str += "I{%s} " % param + syntax += required_parameters_str + + optional_parameters_str = '' + for param in optional_parameters: + optional_parameters_str += "[I{%s}] " % param + syntax += optional_parameters_str + + comments = [] + #if optional_parameters: + # comments.append(self.con.dedent( + # ''' + # %s - These are optional parameters that can either be + # specified in the above order as positional parameters, or in + # any order at the end of the line as keyword=value parameters. + # ''' % optional_parameters_str[:-1])) + + if args is not None: + syntax += "[I{%s}...] " % args + # comments.append(self.con.dedent( + # ''' + # [I{%s}...] - This command accepts an arbitrary number of + # parameters before any keyword=value parameter. In order to use + # them, you must fill in all previous positional parameters if + # any. See B{DESCRIPTION} below. + # ''' % args)) + + if kwargs is not None: + syntax += "[I{%s=value}...] " % (kwargs) + # comments.append(self.con.dedent( + # ''' + # This command also accepts an arbitrary number of + # keyword=value parameters. See B{DESCRIPTION} below. + # ''')) + + default_values = '' + if num_defaults > 0: + index = 0 + for param in optional_parameters: + if default[index] is not None: + default_values += "%s=%s " % (param, str(default[index])) + + return syntax, comments, default_values + + def get_command_signature(self, command): + ''' + Get a command's signature. + @param command: The command to get the signature of. + @type command: str + @return: (parameters, free_pparams, free_kparams) where parameters is a + list of all the command's parameters and free_pparams and free_kparams + booleans set to True is the command accepts an arbitrary number of, + respectively, pparams and kparams. + @rtype: ([str...], bool, bool) + ''' + method = self.get_command_method(command) + parameters, args, kwargs, default = inspect.getargspec(method) + parameters = parameters[1:] + if args is not None: + free_pparams = args + else: + free_pparams = False + if kwargs is not None: + free_kparams = kwargs + else: + free_kparams = False + self.log.debug("Signature is %s, %s, %s." \ + % (str(parameters), \ + str(free_pparams), \ + str(free_kparams))) + return parameters, free_pparams, free_kparams + + def get_root(self): + ''' + @return: The root node of the nodes tree. + @rtype: ConfigNode + ''' + if self.is_root(): + return self + else: + return self.parent.get_root() + + name = property(_get_name, + _set_name, + doc="Gets or sets the node's name.") + + path = property(_get_path, + doc="Gets the node's path.") + + children = property(_list_children, + doc="Lists the node's children.") + + parent = property(_get_parent, + _set_parent, + doc="Gets or sets for the first time the node's parent.") + + def is_root(self): + ''' + @return: Wether or not we are a root node. + @rtype: bool + ''' + if self._parent is None: + return True + else: + return False + + def get_child(self, name): + ''' + @param name: The child's name. + @type name: str + @return: Our child named by name. + @rtype: ConfigNode + @raise ValueError: If there is no child named by name. + ''' + for child in self._children: + if child.name == name: + return child + else: + raise ValueError("No such path %s/%s" \ + % (self.path.rstrip('/'), name)) + + def get_node(self, path): + ''' + Looks up a node by path in the nodes tree. + @param path: The node's path. + @type path: str + @return: The node that has the given path. + @rtype: ConfigNode + @raise ValueError: If there is no node with that path. + ''' + def adjacent_node(name): + ''' + Returns an adjacent node or ourself. + ''' + if name == self._path_current: + return self + elif name == self._path_previous: + if self._parent is not None: + return self._parent + else: + return self + else: + return self.get_child(name) + + + # Cleanup the path + if path is None or path == '': + path = '.' + + # Is it a bookmark ? + if path.startswith('@'): + bookmark = path.lstrip('@').strip() + if bookmark in self.prefs['bookmarks']: + path = self.prefs['bookmarks'][bookmark] + else: + raise ValueError("No such bookmark %s" % bookmark) + + # More cleanup + path = re.sub('%s+' % self._path_separator, self._path_separator, path) + if len(path) > 1: + path = path.rstrip(self._path_separator) + self.log.debug("Looking for path '%s'" % path) + + + # Absolute path - make relative and pass on to root node + if path.startswith(self._path_separator): + next_node = self.get_root() + next_path = path.lstrip(self._path_separator) + if next_path: + return next_node.get_node(next_path) + else: + return next_node + + # Relative path + if self._path_separator in path: + next_node_name, next_path = path.split(self._path_separator, 1) + next_node = adjacent_node(next_node_name) + return next_node.get_node(next_path) + + # Path is just one of our children + return adjacent_node(path) + + def add_child(self, child, name=None): + ''' + Adds a new child to the node. + Performs necessary checks to enforce our hierarchy conventions: + - A node cannot be its own child + - A new node must not already have a parent + - Our children names must be unique + - We must not be a child of this child + @param child: The new child node. + @type child: A ConfigNode object. + @param name: If specified, the new child name, else uses the one from + child. + @type name: str + @raise ValueError: if the node breaks our hierarchy conventions. + ''' + if child == self: + raise ValueError("A node cannot be it's own child.") + + if not child.is_root(): + raise ValueError("Child node already has a parent.") + + for grandchild in child.children: + if grandchild == self: + raise ValueError("Refusing to add cyclic parent link.") + + if name is not None: + child.name = name + + if child.name in [ourchild.name for ourchild in self.children]: + raise ValueError("Node already has a child named %s" % name) + else: + child.parent = self + self._children.add(child) + + def del_child(self, child): + ''' + Deletes a child from the node. + @param child: The new child node. + @type child: A ConfigNode object. + ''' + if child in self._children: + self._children.remove(child) + else: + raise ValueError("Cannot delete: no such child.") + diff --git a/configshell/prefs.py b/configshell/prefs.py new file mode 100644 index 0000000..9ddb420 --- /dev/null +++ b/configshell/prefs.py @@ -0,0 +1,149 @@ +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import cPickle + +class Prefs(object): + ''' + This is a preferences backend object used to: + - Hold the ConfigShell preferences + - Handle persistent storage and retrieval of these preferences + - Share the preferences between the ConfigShell and ConfigNode objects + + As it is inherently destined to be shared between objects, this is a Borg. + ''' + _prefs = {} + filename = None + autosave = False + __borg_state = {} + + def __init__(self, filename=None): + ''' + Instanciates the ConfigShell preferences object. + @param filename: File to store the preferencces to. + @type filename: str + ''' + self.__dict__ = self.__borg_state + if filename is not None: + self.filename = filename + + def __getitem__(self, key): + ''' + Proxies dict-like references to prefs. + One specific behavior, though, is that if the key does not exists, + we will return None instead of raising an exception. + @param key: The preferences dictionnary key to get. + @type key: any valid dict key + @return: The key value + @rtype: n/a + ''' + if key in self._prefs: + return self._prefs[key] + else: + return None + + def __setitem__(self, key, value): + ''' + Proxies dict-like references to prefs. + @param key: The preferences dictionnary key to set. + @type key: any valid dict key + ''' + self._prefs[key] = value + if self.autosave: + self.save() + + def __contains__(self, key): + ''' + Do the preferences contain key ? + @param key: The preferences dictionnary key to check. + @type key: any valid dict key + ''' + if key in self._prefs: + return True + else: + return False + + def __delitem__(self, key): + ''' + Deletes a preference key. + @param key: The preference to delete. + @type key: any valid dict key + ''' + del self._prefs[key] + if self.autosave: + self.save() + + def __iter__(self): + ''' + Generic iterator for the preferences. + ''' + return self._prefs.__iter__() + + # Public methods + + def keys(self): + ''' + @return: Returns the list of keys in preferences. + @rtype: list + ''' + return self._prefs.keys() + + def items(self): + ''' + @return: Returns the list of items in preferences. + @rtype: list of (key, value) tuples + ''' + return self._prefs.items() + + def iteritems(self): + ''' + @return: Iterates on the items in preferences. + @rtype: yields items that are (key, value) pairs + ''' + return self._prefs.iteritems() + + def save(self, filename=None): + ''' + Saves the preferences to disk. If filename is not specified, + use the default one if it is set, else do nothing. + @param filename: Optional alternate file to use. + @type filename: str + ''' + if filename is None: + filename = self.filename + + if filename is not None: + fsock = open(filename, 'wb') + try: + cPickle.dump(self._prefs, fsock, 2) + finally: + fsock.close() + + def load(self, filename=None): + ''' + Loads the preferences from file. Use either the supplied filename, + or the default one if set. Else, do nothing. + ''' + if filename is None: + filename = self.filename + + if filename is not None: + fsock = open(filename, 'rb') + try: + self._prefs = cPickle.load(fsock) + finally: + fsock.close() diff --git a/configshell/shell.py b/configshell/shell.py new file mode 100644 index 0000000..22eba21 --- /dev/null +++ b/configshell/shell.py @@ -0,0 +1,983 @@ +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import os +import sys +import readline +import simpleparse.parser +import simpleparse.dispatchprocessor + +import configshell.log as log +import configshell.prefs as prefs +import configshell.console as console + +# A fix for frozen packages +import signal +def handle_sigint(signum, frame): + ''' + Raise KeyboardInterrupt when we get a SIGINT. + This is normally done by python, but even after patching + pyinstaller 1.4 to ignore SIGINT in the C wrapper code, we + still have to do the translation ourselves. + ''' + raise KeyboardInterrupt +signal.signal(signal.SIGINT, handle_sigint) + +class ConfigShell(object): + ''' + This is a simple CLI command interpreter that can be used both in + interactive or non-interactive modes. + It is based on a tree of ConfigNode objects, which can be navigated. + + The ConfigShell object itself provides global navigation commands. + It also handles the parsing of local commands (specific to a certain + ConfigNode) according to the ConfigNode commands definitions. + If the ConfigNode provides hooks for possible parameter values in a given + context, then the ConfigShell will also provide command-line completion + using the TAB key. If no completion hooks are available from the + ConfigNode, the completion function will still be able to display some help + and general syntax advice (as much as the ConfigNode will provide). + + Interactive sessions can be saved/loaded automatically by ConfigShell is a + writable session directory is supplied. This includes command-line history, + current node and global parameters. + ''' + + default_prefs = {'color_path': 'magenta', + 'color_command': 'cyan', + 'color_parameter': 'magenta', + 'color_keyword': 'cyan', + 'completions_in_columns': True, + 'logfile': None, + 'loglevel_console': 'info', + 'loglevel_file': 'debug9', + 'color_mode': True, + 'prompt_length': 30, + 'tree_max_depth': 0, + 'tree_status_mode': True, + 'tree_round_nodes': True, + 'tree_show_root': True + } + + _completion_help_topic = '' + _current_parameter = '' + _current_token = '' + _current_completions = [] + complete_key = 'tab' + grammar = \ + r''' + <eol> := '\n' + <space> := ' '+ + line := (linepath)/(space?, command), parameters?, eol? + >linepath< := space?, path, space?, command? + command := [a-zA-Z0-9_]+ + >parameters< := (space, kparam/pparam)+ + pparam := var + kparam := keyword, '=', value + >keyword< := [a-zA-Z0-9_\-]+ + >value< := var? + path := bookmark/pathstd/[0-9]+/'..'/'.'/'*' + <pathstd> := ([a-zA-Z0-9:_.-]*, '/', [a-zA-Z0-9:_./-]*, '*'?) + <var> := [a-zA-Z0-9_\\+/.<>~@:-]+ + <bookmark> := '@', var? + ''' + + def __init__(self, root_node, preferences_dir=None): + ''' + Creates a new ConfigShell. + @param root_node: The root ConfigNode object + @type root_node: ConfigNode + @param preferences_dir: Directory to load/save preferences from/to + @type preferences_dir: str + ''' + self._current_node = root_node + self._root_node = root_node + self._exit = False + + self._parser = simpleparse.parser.Parser(self.grammar, root='line') + readline.set_completer_delims('\t\n ~!#$%^&()[{]}\|;\'",?') + + self.log = log.Log() + + if preferences_dir is not None: + preferences_dir = os.path.expanduser(preferences_dir) + if not os.path.exists(preferences_dir): + os.makedirs(preferences_dir) + self._prefs_file = preferences_dir + '/prefs.bin' + self.prefs = prefs.Prefs(self._prefs_file) + self._cmd_history = preferences_dir + '/history.txt' + self._save_history = True + if not os.path.isfile(self._cmd_history): + try: + open(self._cmd_history, 'w').close() + except: + self.log.warning("Cannot create history file %s, " \ + % self._cmd_history \ + + "command history will not be saved.") + self._save_history = False + + if os.path.isfile(self._cmd_history): + try: + readline.read_history_file(self._cmd_history) + except IOError: + self.log.warning("Cannot read command history file %s." \ + % self._cmd_history) + + if self.prefs['logfile'] is None: + self.prefs['logfile'] = preferences_dir + '/' + 'log.txt' + + self.prefs.autosave = True + + else: + self.prefs = prefs.Prefs() + self._save_history = False + + try: + self.prefs.load() + except IOError: + self.log.warning("Could not load preferences file %s." \ + % self._prefs_file) + + for pref, value in self.default_prefs.iteritems(): + if pref not in self.prefs: + self.prefs[pref] = value + + self.con = console.Console() + + # Private methods + + def _set_readline_display_matches(self): + ''' + In order to stay compatible with python versions < 2.6, + we are not using readline.set_completion_display_matches_hook() but + instead use ctypes there to bind to the C readline hook if needed. + This hooks a callback function to display readline completions. + ''' + if 'set_completion_display_matches_hook' in dir(readline): + readline.set_completion_display_matches_hook( + self._display_completions_python) + else: + from ctypes import cdll, CFUNCTYPE, POINTER + from ctypes import c_char_p, c_int, c_void_p, cast + libreadline = None + try: + libreadline = cdll.LoadLibrary('libreadline.so') + except OSError: + try: + libreadline = cdll.LoadLibrary('libreadline.so.5') + except OSError: + try: + libreadline = cdll.LoadLibrary('libreadline.so.6') + except OSError: + self.log.critical( + "Could not find readline shared library.") + if libreadline: + completion_func_type = \ + CFUNCTYPE(None, POINTER(c_char_p), c_int, c_int) + hook = completion_func_type(self._display_completions) + ptr = c_void_p.in_dll(libreadline, + 'rl_completion_display_matches_hook') + ptr.value = cast(hook, c_void_p).value + + def _display_completions_python(self, substitution, matches, max_length): + ''' + A wrapper to be used with python>=2.6 readline display completion hook. + ''' + self.log.debug("Using python >=2.6 readline display hook.") + matches = [substitution] + matches + self._display_completions(matches, len(matches)-1, max_length) + + def _display_completions(self, matches, num_matches, max_length): + ''' + Hooked up to readline, this method displays: + - Possible completions for path/command/parameters + - Some help about the command being written + - The current parameter name and type hovering above the cursor + @param matches: Passed by readline, indexed from 1 to num_matches+1. + @type matches: a C list + @param num_matches: The number of matches. + @type num_matches: int + @param max_length: Length of the longer matches item. + @type max_length: int + ''' + (x_orig, y_pos) = self.con.get_cursor_xy() + self.con.raw_write("\n") + width = self.con.get_width() + max_length += 2 + + def just(text): + ''' + Justifies the text to the max match length. + ''' + return text.ljust(max_length, " ") + + # Sort the matches + if self._current_parameter: + keywords = [] + values = [] + for index in range(1, num_matches+1): + match = matches[index] + if match.endswith('='): + keywords.append( + self.con.render_text( + just(match), self.prefs['color_keyword'])) + else: + (keyword, sep, value) = match.partition('=') + if sep: + values.append( + self.con.render_text( + just(value), self.prefs['color_parameter'])) + else: + values.append( + self.con.render_text( + just(match), self.prefs['color_parameter'])) + matches = [''] + values + keywords + else: + paths = [] + commands = [] + for index in range(1, num_matches+1): + match = matches[index] + if '/' in match or match.startswith('@') or '*' in match: + paths.append( + self.con.render_text( + just(match), self.prefs['color_path'])) + else: + commands.append( + self.con.render_text( + just(match), self.prefs['color_command'])) + matches = [''] + paths + commands + + # Display the possible completions + + if num_matches: + + if max_length < width: + num_per_line = width / max_length + if num_per_line * max_length == width: + num_per_line -= 1 + else: + num_per_line = 1 + + if not self.prefs['completions_in_columns']: + index_match = 0 + done = False + while not done: + for index_col in range(num_per_line): + index_match += 1 + self.con.raw_write(matches[index_match]) + if index_match == num_matches: + done = True + break + self.con.raw_write('\n') + else: + count = (num_matches + num_per_line - 1) / num_per_line + if len > 0 and len <= num_per_line: + count = 1 + for i in range(1, count+1): + l = i + for j in range(num_per_line): + if l > num_matches or matches[l] == 0: + break + else: + self.con.raw_write(matches[l]) + l += count + self.con.raw_write("\n") + + # Draw the "hovering" hint with currently filled parameter name + line = "%s%s" % (self._get_prompt(), readline.get_line_buffer()) + line_offset = len(self._get_prompt()) + begidx = readline.get_begidx() + line_offset + endidx = readline.get_endidx() + line_offset + text = line[begidx:endidx] + (keyword, sep, value) = text.partition('=') + if sep: + paramidx = begidx + len(keyword) + 1 + else: + paramidx = begidx + self.con.display("%s%s" % ("".rjust(paramidx, "."), + self._current_token)) + self.con.raw_write("%s" % line) + + # Move the cursor where it should be + y_pos = self.con.get_cursor_xy()[1] + self.con.set_cursor_xy(x_orig, y_pos) + + def _complete_token_command(self, text, path, command, pparams, kparams): + ''' + Completes a partial command token, which could also be the beginning + of a path. + @param path: Path of the target ConfigNode. + @type path: str + @param command: The command (if any) found by the parser. + @type command: str + @param pparams: Positional parameters from commandline. + @type pparams: list of str + @param kparams: Keyword parameters from commandline. + @type kparams: dict of str:str + @param text: Current text being typed by the user. + @type text: str + @return: Possible completions for the token. + @rtype: list of str + ''' + completions = [] + target = self._current_node.get_node(path) + commands = target.list_commands() + self.log.debug("Completing command token among %s" % str(commands)) + + # Start with the possible commands + for command in commands: + if command.startswith(text): + completions.append(command) + if len(completions) == 1: + completions[0] = completions[0] + ' ' + + # No identified path yet on the command line, this might be it + if not path: + path_completions = [child.name + '/' + for child in self._current_node.children + if child.name.startswith(text)] + if not text: + path_completions.append('/') + if len(self._current_node.children) > 1: + path_completions.append('* ') + + if path_completions: + if completions: + self._current_token = \ + self.con.render_text( + 'path', self.prefs['color_path']) \ + + '|' \ + + self.con.render_text( + 'command', self.prefs['color_command']) + else: + self._current_token = \ + self.con.render_text( + 'path', self.prefs['color_path']) + else: + self._current_token = \ + self.con.render_text( + 'command', self.prefs['color_command']) + if len(path_completions) == 1 and \ + not path_completions[0][-1] in [' ', '*'] and \ + not self._current_node.get_node(path_completions[0]).children: + path_completions[0] = path_completions[0] + ' ' + completions.extend(path_completions) + else: + self._current_token = \ + self.con.render_text( + 'command', self.prefs['color_command']) + + # Even a bookmark + bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks'] + if bookmark.startswith("%s" % text.lstrip('@'))] + self.log.debug("Found bookmarks %s." % str(bookmarks)) + if bookmarks: + completions.extend(bookmarks) + + + # We are done + return completions + + def _complete_token_path(self, text, path, command, pparams, kparams): + ''' + Completes a partial path token. + @param path: Path of the target ConfigNode. + @type path: str + @param command: The command (if any) found by the parser. + @type command: str + @param pparams: Positional parameters from commandline. + @type pparams: list of str + @param kparams: Keyword parameters from commandline. + @type kparams: dict of str:str + @param text: Current text being typed by the user. + @type text: str + @return: Possible completions for the token. + @rtype: list of str + ''' + completions = [] + if text.endswith('.'): + text = text + '/' + (basedir, slash, partial_name) = text.rpartition('/') + self.log.debug("Got basedir=%s, partial_name=%s" \ + % (basedir, partial_name)) + basedir = basedir + slash + target = self._current_node.get_node(basedir) + names = [child.name for child in target.children] + + # Iterall path completion + if names and partial_name in ['', '*']: + # Not suggesting iterall to end a path that has only one + # child allows for fast TAB action to add the only child's + # name. + if len(names) > 1: + completions.append("%s* " % basedir) + + for name in names: + num_matches = 0 + if name.startswith(partial_name): + num_matches += 1 + if num_matches == 1: + completions.append("%s%s/" % (basedir, name)) + else: + completions.append("%s%s" % (basedir, name)) + + # Bookmarks + bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks'] + if bookmark.startswith("%s" % text.lstrip('@'))] + self.log.debug("Found bookmarks %s." % str(bookmarks)) + if bookmarks: + completions.extend(bookmarks) + + if len(completions) == 1: + self.log.debug("One completion left.") + if not completions[0].endswith("* "): + if not self._current_node.get_node(completions[0]).children: + completions[0] = completions[0].rstrip('/') + ' ' + + self._current_token = \ + self.con.render_text( + 'path', self.prefs['color_path']) + return completions + + def _complete_token_pparam(self, text, path, command, pparams, kparams): + ''' + Completes a positional parameter token, which can also be the keywork + part of a kparam token, as before the '=' sign is on the line, the + parser cannot know better. + @param path: Path of the target ConfigNode. + @type path: str + @param command: The command (if any) found by the parser. + @type command: str + @param pparams: Positional parameters from commandline. + @type pparams: list of str + @param kparams: Keyword parameters from commandline. + @type kparams: dict of str:str + @param text: Current text being typed by the user. + @type text: str + @return: Possible completions for the token. + @rtype: list of str + ''' + completions = [] + target = self._current_node.get_node(path) + cmd_params, free_pparams, free_kparams = \ + target.get_command_signature(command) + current_parameters = {} + for index in range(len(pparams)): + if index < len(cmd_params): + current_parameters[cmd_params[index]] = pparams[index] + for key, value in kparams.iteritems(): + current_parameters[key] = value + self._completion_help_topic = command + completion_method = target.get_completion_method(command) + self.log.debug("Command %s accepts parameters %s." \ + % (command, cmd_params)) + + # Do we still accept positional params ? + pparam_ok = True + for index in range(len(cmd_params)): + param = cmd_params[index] + if param in kparams: + if index <= len(pparams): + pparam_ok = False + self.log.debug( + "No more possible pparams (because of kparams).") + break + elif (text.strip() == '' and len(pparams) == len(cmd_params)) \ + or (len(pparams) > len(cmd_params)): + pparam_ok = False + self.log.debug("No more possible pparams.") + break + else: + if len(cmd_params) == 0: + pparam_ok = False + self.log.debug("No more possible pparams (none exists)") + + # If we do, find out which one we are completing + if pparam_ok: + if not text: + pparam_index = len(pparams) + else: + pparam_index = len(pparams) - 1 + self._current_parameter = cmd_params[pparam_index] + self.log.debug("Completing pparam %s." % self._current_parameter) + if completion_method: + pparam_completions = completion_method( + current_parameters, text, self._current_parameter) + if pparam_completions is not None: + completions.extend(pparam_completions) + + # Add the keywords for parameters not already on the line + if text: + offset = 1 + else: + offset = 0 + keyword_completions = [param + '=' \ + for param in cmd_params[len(pparams)-offset:] \ + if param not in kparams \ + if param.startswith(text)] + + self.log.debug("Possible pparam values are %s." \ + % str(completions)) + self.log.debug("Possible kparam keywords are %s." \ + % str(keyword_completions)) + + if keyword_completions: + if self._current_parameter: + self._current_token = \ + self.con.render_text( + self._current_parameter, \ + self.prefs['color_parameter']) \ + + '|' \ + + self.con.render_text( + 'keyword=', self.prefs['color_keyword']) + else: + self._current_token = \ + self.con.render_text( + 'keyword=', self.prefs['color_keyword']) + else: + if self._current_parameter: + self._current_token = \ + self.con.render_text( + self._current_parameter, + self.prefs['color_parameter']) + else: + self._current_token = '' + + completions.extend(keyword_completions) + + if free_kparams or free_pparams: + self.log.debug("Command has free [kp]params.") + if completion_method: + self.log.debug("Calling completion method for free params.") + free_completions = completion_method( + current_parameters, text, '*') + do_free_pparams = False + do_free_kparams = False + for free_completion in free_completions: + if free_completion.endswith("="): + do_free_kparams = True + else: + do_free_pparams = True + + if do_free_pparams: + self._current_token = \ + self.con.render_text( + free_pparams, self.prefs['color_parameter']) \ + + '|' + self._current_token + self._current_token = self._current_token.rstrip('|') + if not self._current_parameter: + self._current_parameter = 'free_parameter' + + if do_free_kparams: + if not 'keyword=' in self._current_token: + self._current_token = \ + self.con.render_text( + 'keyword=', self.prefs['color_keyword']) \ + + '|' + self._current_token + self._current_token = self._current_token.rstrip('|') + if not self._current_parameter: + self._current_parameter = 'free_parameter' + + completions.extend(free_completions) + + self.log.debug("Found completions %s." % str(completions)) + return completions + + def _complete_token_kparam(self, text, path, command, pparams, kparams): + ''' + Completes a keyword=value parameter token. + @param path: Path of the target ConfigNode. + @type path: str + @param command: The command (if any) found by the parser. + @type command: str + @param pparams: Positional parameters from commandline. + @type pparams: list of str + @param kparams: Keyword parameters from commandline. + @type kparams: dict of str:str + @param text: Current text being typed by the user. + @type text: str + @return: Possible completions for the token. + @rtype: list of str + ''' + self.log.debug("Called for text='%s'" % text) + target = self._current_node.get_node(path) + cmd_params = target.get_command_signature(command)[0] + self.log.debug("Command %s accepts parameters %s." \ + % (command, cmd_params)) + + (keyword, sep, current_value) = text.partition('=') + self.log.debug("Completing '%s' for kparam %s" \ + % (current_value, keyword)) + + self._current_parameter = keyword + current_parameters = {} + for index in range(len(pparams)): + current_parameters[cmd_params[index]] = pparams[index] + for key, value in kparams.iteritems(): + current_parameters[key] = value + completion_method = target.get_completion_method(command) + if completion_method: + completions = completion_method( + current_parameters, current_value, keyword) + if completions is None: + completions = [] + + self._current_token = \ + self.con.render_text( + self._current_parameter, self.prefs['color_parameter']) + + self.log.debug("Found completions %s." % str(completions)) + + return ["%s=%s" % (keyword, completion) for completion in completions] + + def _complete(self, text, state): + ''' + Text completion method, directly called by readline. + Finds out what token the user wants completion for, and calls the + _dispatch_completion() to get the possible completions. + Then implements the state system needed by readline to return those + possible completions to readline. + @param text: The text to complete. + @type text: str + @returns: The next possible completion for text. + @rtype: str + ''' + self._set_readline_display_matches() + + if state == 0: + cmdline = readline.get_line_buffer() + self._current_completions = [] + self._completion_help_topic = '' + self._current_parameter = '' + + (result_trees, path, command, pparams, kparams) = \ + self._parse_cmdline(cmdline) + + beg = readline.get_begidx() + end = readline.get_endidx() + if beg == end: + # No text under the cursor, fake it so that the parser + # result_trees gives us a token name on a second parser call + self.log.debug("Faking text entry on commandline.") + result_trees = self._parse_cmdline(cmdline + 'x')[0] + end += 1 + + for tree in result_trees: + if beg == tree[1] and end == tree[2]: + current_token = tree[0] + break + + self._current_completions = \ + self._dispatch_completion(path, command, + pparams, kparams, + text, current_token) + + self.log.debug("Returning completions %s to readline." \ + % str(self._current_completions)) + + if state < len(self._current_completions): + return self._current_completions[state] + else: + return None + + def _dispatch_completion(self, path, command, + pparams, kparams, text, current_token): + ''' + This method takes care of dispatching the current completion request + from readline (via the _complete() method) to the relevant token + completion methods. It has to cope with the fact that the commandline + being incomplete yet, + Of course, as the command line is still unfinished, the parser can + only do so much of a job. For instance, until the '=' sign is on the + command line, there is no way to distinguish a positional parameter + from the begining of a keyword=value parameter. + @param path: Path of the target ConfigNode. + @type path: str + @param command: The command (if any) found by the parser. + @type command: str + @param pparams: Positional parameters from commandline. + @type pparams: list of str + @param kparams: Keyword parameters from commandline. + @type kparams: dict of str:str + @param text: Current text being typed by the user. + @type text: str + @param current_token: Name of token to complete. + @type current_token: str + @return: Possible completions for the token. + @rtype: list of str + ''' + completions = [] + + self.log.debug("Dispatching completion for %s token. " \ + % current_token \ + + "text='%s', path='%s', command='%s', " \ + % (text, path, command) \ + + "pparams=%s, kparams=%s" \ + % (str(pparams), str(kparams))) + + (path, iterall) = path.partition('*')[:2] + if iterall: + try: + target = self._current_node.get_node(path) + except ValueError: + cpl_path = path + else: + children = target.children + if children: + cpl_path = children[0].path + else: + cpl_path = path + + + if current_token == 'command': + completions = self._complete_token_command(text, cpl_path, command, + pparams, kparams) + elif current_token == 'path': + completions = \ + self._complete_token_path(text, path, command, + pparams, kparams) + elif current_token == 'pparam': + completions = \ + self._complete_token_pparam(text, cpl_path, command, + pparams, kparams) + elif current_token == 'kparam': + completions = \ + self._complete_token_kparam(text, cpl_path, command, + pparams, kparams) + else: + self.log.debug('Cannot complete unknown token %s.' \ + % current_token) + + return completions + + def _get_prompt(self): + ''' + Returns the command prompt string. + ''' + prompt_path = self._current_node.path + prompt_length = self.prefs['prompt_length'] + + if prompt_length and prompt_length < len(prompt_path): + half = (prompt_length-3)/2 + prompt_path = "%s...%s" \ + % (prompt_path[:half], prompt_path[-half:]) + + if 'prompt_msg' in dir(self._current_node): + return "%s%s> " % (self._current_node.prompt_msg(), + prompt_path) + else: + return "%s> " % prompt_path + + def _cli_loop(self): + ''' + Starts the configuration shell interactive loop, that: + - Goes to the last current path + - Displays the prompt + - Waits for user input + - Runs user command + ''' + while not self._exit: + try: + readline.parse_and_bind("%s: complete" % self.complete_key) + readline.set_completer(self._complete) + cmdline = raw_input(self._get_prompt()).strip() + except EOFError: + self.con.raw_write('\n') + cmdline = "cd .." + self.run_cmdline(cmdline) + if self._save_history: + try: + readline.write_history_file(self._cmd_history) + except IOError, msg: + self.log.warning( + "Cannot write to command history file %s." \ + % self._cmd_history) + self.log.warning( + "Saving command history has been disabled!") + self._save_history = False + + def _parse_cmdline(self, line): + ''' + Parses the command line entered by the user. This is a wrapper around + the actual simpleparse parsed that pre-chews the result trees to + cleanly extract the tokens we care for (parameters, path, command). + @param line: The command line to parse. + @type line: str + @return: (result_trees, path, command, pparams, kparams), + pparams being positional parameters and kparams the keyword=value. + @rtype: (list of tuple, str, str, list, dict) For the exact + result_trees tuple format, c.f. the simpleparse documentation. + ''' + self.log.debug("Parsing commandline.") + path = '' + command = '' + pparams = [] + kparams = {} + (success, result_trees, next_character) = self._parser.parse(line) + if success: + self.log.debug(str(result_trees)) + for tree in result_trees: + token = tree[0] + value = line[tree[1]:tree[2]] + self.log.debug("Found %s token %s." % (token, value)) + if token == 'path': + path = value + elif token == 'command': + command = value + elif token == 'pparam': + pparams.append(value) + elif token == 'kparam': + (keyword, sep, value) = value.partition('=') + kparams[keyword] = value + + self.log.debug("Parse gave path='%s' command='%s' " % (path, command) \ + + "pparams=%s " % str(pparams) \ + + "kparams=%s" % str(kparams)) + return (result_trees, path, command, pparams, kparams) + + def _execute_command(self, path, command, pparams, kparams): + ''' + Calls the target node to execute a command. + Behavior depends on the target node command's result: + - An 'EXIT' string will trigger shell exit. + - None will do nothing. + - A ConfigNode object will trigger a current_node change. + @param path: Path of the target node. + @type path: str + @param command: The command to call. + @type command: str + @param pparams: The positional parameters to use. + @type pparams: list + @param kparams: The keyword=value parameters to use. + @type kparams: dict + ''' + if path.endswith('*'): + path = path.rstrip('*') + iterall = True + else: + iterall = False + + if not path: + path = '.' + + if not command: + if iterall: + command = 'ls' + else: + command = 'cd' + pparams = ['.'] + + try: + target = self._current_node.get_node(path) + except ValueError, msg: + self.log.error(msg) + else: + result = None + if not iterall: + targets = [target] + else: + targets = target.children + for target in targets: + if iterall: + self.con.display("[%s]" % target.path) + result = target.execute_command(command, pparams, kparams) + if result is 'EXIT': + self._exit = True + elif result is not None: + self._current_node = result + + # Public methods + + def run_cmdline(self, cmdline): + ''' + Runs the specified command. Global commands are checked first, + then local commands from the current node. + + Command syntax is: + [PATH] COMMAND [POSITIONAL_PARAMETER]+ [PARAMETER=VALUE]+ + + @param cmdline: The command line to run + @type cmdline: str + ''' + if cmdline: + self.log.debug("Running command line '%s'." % cmdline) + path, command, pparams, kparams = self._parse_cmdline(cmdline)[1:] + self._execute_command(path, command, pparams, kparams) + + def run_script(self, script_path, exit_on_error=True): + ''' + Runs the script located at script_path. + Script runs always start from the root context. + @param script_path: File path of the script to run + @type script_path: str + @param exit_on_error: If True, stops the run if an error occurs + @type exit_on_error: bool + ''' + try: + script_fd = open(script_path, 'r') + self.run_stdin(script_fd, exit_on_error) + except IOError, msg: + raise IOError(msg) + finally: + script_fd.close() + + def run_stdin(self, file_descriptor=sys.stdin, exit_on_error=True): + ''' + Reads commands to be run from a file descriptor, stdin by default. + The run always starts from the root context. + @param file_descriptor: The file descriptor to read commands from + @type file_descriptor: file object + @param exit_on_error: If True, stops the run if an error occurs + @type exit_on_error: bool + ''' + self._current_node = self._root_node + for cmdline in file_descriptor: + try: + self.run_cmdline(cmdline) + except Exception, msg: + self.log.error(msg) + if exit_on_error is True: + self.log.exception("Aborting run on error.") + break + else: + self.log.exception("Keep running after an error.") + + def run_interactive(self): + ''' + Starts interactive CLI mode. + ''' + history = self.prefs['path_history'] + index = self.prefs['path_history_index'] + if history and index: + if index < len(history): + try: + target = self._root_node.get_node(history[index]) + except ValueError: + self._current_node = self._root_node + else: + self._current_node = target + try: + old_completer = readline.get_completer() + self._cli_loop() + except KeyboardInterrupt: + self.con.raw_write('\n') + self.run_interactive() + except Exception: + self.log.exception() + self.run_interactive() + finally: + readline.set_completer(old_completer) diff --git a/debian/README.Debian b/debian/README.Debian new file mode 100644 index 0000000..374bba0 --- /dev/null +++ b/debian/README.Debian @@ -0,0 +1,13 @@ +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/configshell-doc.docs b/debian/configshell-doc.docs new file mode 100644 index 0000000..37ad01a --- /dev/null +++ b/debian/configshell-doc.docs @@ -0,0 +1,5 @@ +README +COPYING +doc/pdf +doc/html +examples/myshell diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..69f19bc --- /dev/null +++ b/debian/control @@ -0,0 +1,24 @@ +Source: configshell +Section: python +Priority: optional +Maintainer: Jerome Martin <jxm@risingtidesystems.com> +Build-Depends: debhelper(>= 7.0.1), python2.6, python-epydoc, python-simpleparse +Standards-Version: 3.8.1 + +Package: python-configshell +Architecture: all +Depends: python (>= 2.6)|python2.6, python-epydoc, python-simpleparse, python-urwid +Suggests: configshell-doc +Description: Framework to create CLI interfaces. + . + This package contains the configshell library. + +Package: configshell-doc +Section: doc +Architecture: all +Recommends: iceweasel | www-browser +Description: official API documentation for the configshell library + . + This package contains the API reference and usage information for configshell, + a framework to create CLI interfaces. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..80d4fbd --- /dev/null +++ b/debian/copyright @@ -0,0 +1,13 @@ +This package was originally debianized by Jerome Martin <jxm@risingtidesystems.com> +on Thu Sep 09 23:13:01 CET 2010. It is currently maintained by Jerome Martin +<jxm@risingtidesystems.com>. + +Upstream Author: Jerome Martin <jxm@risingtidesystems.com> + +Copyright: + + Copyright (c) 2010 by RisingTide Systems LLC. + All rights reserved. + For licensing information, please contact us. + Not for redistribution. + diff --git a/debian/python-configshell.dirs b/debian/python-configshell.dirs new file mode 100644 index 0000000..145429c --- /dev/null +++ b/debian/python-configshell.dirs @@ -0,0 +1 @@ +usr/share/python-support diff --git a/debian/python-configshell.docs b/debian/python-configshell.docs new file mode 100644 index 0000000..8b39665 --- /dev/null +++ b/debian/python-configshell.docs @@ -0,0 +1,3 @@ +README +COPYING +examples/myshell diff --git a/debian/python-configshell.install b/debian/python-configshell.install new file mode 100644 index 0000000..69bb674 --- /dev/null +++ b/debian/python-configshell.install @@ -0,0 +1 @@ +lib/configshell usr/share/python-support diff --git a/debian/python-configshell.postinst b/debian/python-configshell.postinst new file mode 100755 index 0000000..b56990a --- /dev/null +++ b/debian/python-configshell.postinst @@ -0,0 +1,17 @@ +#!/bin/sh +for lib in lib lib64; do + for python in python2.6; do + if [ -e /usr/${lib}/${python} ]; then + if [ ! -e /usr/${lib}/${python}/configshell ]; then + mkdir /usr/${lib}/${python}/configshell + for source in /usr/share/python-support/configshell/configshell/*.py; do + ln -sf ${source} /usr/${lib}/${python}/configshell/ + done + python_path=$(which ${python} 2>/dev/null) + if [ ! -z $python_path ]; then + ${python} -c "import compileall; compileall.compile_dir('/usr/${lib}/${python}/configshell', force=1)" + fi + fi + fi + done +done diff --git a/debian/python-configshell.preinst b/debian/python-configshell.preinst new file mode 100755 index 0000000..b96bf44 --- /dev/null +++ b/debian/python-configshell.preinst @@ -0,0 +1,3 @@ +#!/bin/sh +rm -f /usr/share/python-support/configshell/configshell/*.pyc +rm -f /usr/share/python-support/configshell/configshell/*.pyo diff --git a/debian/python-configshell.prerm b/debian/python-configshell.prerm new file mode 100755 index 0000000..866a5c2 --- /dev/null +++ b/debian/python-configshell.prerm @@ -0,0 +1,8 @@ +#!/bin/sh +for lib in lib lib64; do + for python in python2.6; do + if [ -e /usr/${lib}/${python}/configshell ]; then + rm -rf /usr/${lib}/${python}/configshell + fi + done +done diff --git a/debian/pyversions b/debian/pyversions new file mode 100644 index 0000000..0c043f1 --- /dev/null +++ b/debian/pyversions @@ -0,0 +1 @@ +2.6- diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..d8cd635 --- /dev/null +++ b/debian/rules @@ -0,0 +1,48 @@ +#!/usr/bin/make -f + +build_dir = build +install_dir = debian/tmp +setup = /usr/bin/python ./setup.py --quiet + +binary: binary-indep + +binary-arch: + +binary-indep: build install + dh_testdir + dh_testroot + dh_installchangelogs + dh_installdocs + dh_installman + dh_install --list-missing --sourcedir $(install_dir) + dh_fixperms + dh_compress -X.py + dh_installdeb + dh_gencontrol + dh_md5sums + dh_builddeb + +install: build + dh_testdir + dh_testroot + dh_installdirs + +build: build-stamp +build-stamp: + dh_testdir + $(setup) build --build-base $(build_dir) install --no-compile --install-purelib $(install_dir)/lib/configshell --install-scripts $(install_dir)/bin + echo "2.6" > $(install_dir)/lib/configshell/.version + touch build-stamp + +clean: + dh_testdir + dh_testroot + rm -f build-stamp + $(setup) clean + find . -name "*.pyc" | xargs rm -f + find . -name "*.pyo" | xargs rm -f + rm -rf $(build_dir) $(install_dir) + dh_clean + +.PHONY: binary binary-indep install build clean + diff --git a/examples/myshell b/examples/myshell new file mode 100755 index 0000000..d5c9c69 --- /dev/null +++ b/examples/myshell @@ -0,0 +1,181 @@ +#!/usr/bin/python +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import os +import configshell + +class MySystemRoot(configshell.node.ConfigNode): + def __init__(self): + configshell.node.ConfigNode.__init__(self) + for child in Interpreters(), System(): + self.add_child(child) + target = self + for level in range(1,20): + child = configshell.node.ConfigNode() + target.add_child(child, "level%d" % level) + target = child + +class Interpreters(configshell.node.ConfigNode): + def __init__(self): + configshell.node.ConfigNode.__init__(self) + self.name = 'interpreters' + + def ui_command_python(self): + ''' + python - an interpreted object-oriented programming language + ''' + os.system("python") + + def ui_command_ipython(self): + ''' + ipython - An Enhanced Interactive Python + ''' + os.system("ipython") + + def ui_command_bash(self): + ''' + bash - GNU Bourne-Again SHell + ''' + os.system("bash") + +class System(configshell.node.ConfigNode): + def __init__(self): + configshell.node.ConfigNode.__init__(self) + self.name = 'system' + for child in Processes(), Storage(): + self.add_child(child) + + def ui_command_uname(self): + ''' + Displays the system uname information. + ''' + os.system("uname -a") + + def ui_command_lsmod(self): + ''' + lsmod - program to show the status of modules in the Linux Kernel + ''' + os.system("lsmod") + + def ui_command_lspci(self): + ''' + lspci - list all PCI devices + ''' + os.system("lspci") + + def ui_command_lsusb(self): + ''' + lsusb - list USB devices + ''' + os.system("lsusb") + + def ui_command_lscpu(self): + ''' + lscpu - CPU architecture information helper + ''' + os.system("lscpu") + + def ui_command_uptime(self): + ''' + uptime - Tell how long the system has been running. + ''' + os.system("uptime") + + +class Storage(configshell.node.ConfigNode): + def __init__(self): + configshell.node.ConfigNode.__init__(self) + self.name = 'storage' + + def ui_command_lsscsi(self): + ''' + lsscsi - list SCSI devices (or hosts) and their attributes + ''' + os.system("lsscsi") + + def ui_command_du(self): + ''' + du - estimate file space usage + ''' + os.system("du -hs /") + + def ui_command_df(self): + ''' + df - report file system disk space usage + ''' + os.system("df -h") + + def ui_command_uuidgen(self): + ''' + uuidgen - command-line utility to create a new UUID value. + ''' + os.system("uuidgen") + +class Processes(configshell.node.ConfigNode): + def __init__(self): + configshell.node.ConfigNode.__init__(self) + self.name = 'processes' + + def ui_command_top(self): + ''' + top - display Linux tasks + ''' + os.system("top") + + def ui_command_ps(self): + ''' + ps - report a snapshot of the current processes. + ''' + os.system("ps aux") + + def ui_command_pstree(self): + ''' + pstree - display a tree of processes + ''' + os.system("pstree") + +class Users(configshell.node.ConfigNode): + def __init__(self): + configshell.node.ConfigNode.__init__(self) + self.name = 'users' + + def ui_command_who(self): + ''' + who - show who is logged on + ''' + os.system("who") + + def ui_command_whoami(self): + ''' + whoami - print effective userid + ''' + os.system("whoami") + + def ui_command_users(self): + ''' + users - print the user names of users currently logged in. + ''' + os.system("users") + +def main(): + root_node = MySystemRoot() + shell = configshell.shell.ConfigShell(root_node, '~/.myshell') + shell.run_interactive() + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4459832 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +#! /usr/bin/env python + +''' +This file is part of ConfigShell Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +import re +from distutils.core import setup +import configshell + +PKG = configshell +VERSION = str(PKG.__version__) +(AUTHOR, EMAIL) = re.match('^(.*?)\s*<(.*)>$', PKG.__author__).groups() +URL = PKG.__url__ +LICENSE = PKG.__license__ +SCRIPTS = [] +DESCRIPTION = PKG.__description__ + +setup( + name=PKG.__name__, + description=DESCRIPTION, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + license=LICENSE, + url=URL, + scripts=SCRIPTS, + packages=[PKG.__name__], + package_data = {'':[]} + ) |