packages feed

ShellCheck (empty) → 0.3.0

raw patch · 9 files changed

+5356/−0 lines, 9 filesdep +basedep +containersdep +directorysetup-changed

Dependencies added: base, containers, directory, json, mtl, parsec, regex-compat

Files

+ LICENSE view
@@ -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/>.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ShellCheck.cabal view
@@ -0,0 +1,34 @@+Name:             ShellCheck+Version:          0.3.0+Synopsis:         Shell script analysis tool+License:          OtherLicense+License-file:     LICENSE+Category:         Static Analysis+Author:           Vidar Holen+Maintainer:       vidar@vidarholen.net+Homepage:         http://www.shellcheck.net/+Build-Type:       Simple+Cabal-Version:    >= 1.6+Bug-reports:      https://github.com/koalaman/shellcheck/issues+Description:+  The goals of ShellCheck are:+  .+  * To point out and clarify typical beginner's syntax issues,+    that causes a shell to give cryptic error messages.+  .+  * To point out and clarify typical intermediate level semantic problems,+    that causes a shell to behave strangely and counter-intuitively.+  .+  * To point out subtle caveats, corner cases and pitfalls, that may cause an+    advanced user's otherwise working script to fail under future circumstances.++source-repository head+    type: git+    location: git://github.com/koalaman/shellcheck.git++library+    build-depends: base >= 4, base < 5, parsec, containers, regex-compat, mtl, directory, json+    exposed-modules: ShellCheck.AST, ShellCheck.Data, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple++executable shellcheck+    main-is: shellcheck.hs
+ ShellCheck/AST.hs view
@@ -0,0 +1,344 @@+{-+    This file is part of ShellCheck.+    http://www.vidarholen.net/contents/shellcheck++    ShellCheck 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.++    ShellCheck 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/>.+-}+module ShellCheck.AST where++import Control.Monad+import Control.Monad.Identity+import qualified Text.Regex as Re++data Id = Id Int deriving (Show, Eq, Ord)++data Quoted = Quoted | Unquoted deriving (Show, Eq)+data Dashed = Dashed | Undashed deriving (Show, Eq)+data AssignmentMode = Assign | Append deriving (Show, Eq)++data Token =+    TA_Base Id String Token+    | TA_Binary Id String Token Token+    | TA_Expansion Id Token+    | TA_Literal Id String+    | TA_Sequence Id [Token]+    | TA_Trinary Id Token Token Token+    | TA_Unary Id String Token+    | TA_Variable Id String+    | TC_And Id ConditionType String Token Token+    | TC_Binary Id ConditionType String Token Token+    | TC_Group Id ConditionType Token+    | TC_Noary Id ConditionType Token+    | TC_Or Id ConditionType String Token Token+    | TC_Unary Id ConditionType String Token+    | T_AND_IF Id+    | T_AndIf Id (Token) (Token)+    | T_Arithmetic Id Token+    | T_Array Id [Token]+    | T_Assignment Id AssignmentMode String (Maybe Token) Token+    | T_Backgrounded Id Token+    | T_Backticked Id [Token]+    | T_Bang Id+    | T_Banged Id Token+    | T_BraceExpansion Id String+    | T_BraceGroup Id [Token]+    | T_CLOBBER Id+    | T_Case Id+    | T_CaseExpression Id Token [([Token],[Token])]+    | T_Condition Id ConditionType Token+    | T_DGREAT Id+    | T_DLESS Id+    | T_DLESSDASH Id+    | T_DSEMI Id+    | T_Do Id+    | T_DollarArithmetic Id Token+    | T_DollarBraced Id Token+    | T_DollarBracket Id Token+    | T_DollarDoubleQuoted Id [Token]+    | T_DollarExpansion Id [Token]+    | T_DollarSingleQuoted Id String+    | T_Done Id+    | T_DoubleQuoted Id [Token]+    | T_EOF Id+    | T_Elif Id+    | T_Else Id+    | T_Esac Id+    | T_Extglob Id String [Token]+    | T_FdRedirect Id String Token+    | T_Fi Id+    | T_For Id+    | T_ForArithmetic Id Token Token Token [Token]+    | T_ForIn Id String [Token] [Token]+    | T_Function Id String Token+    | T_GREATAND Id+    | T_Glob Id String+    | T_Greater Id+    | T_HereDoc Id Dashed Quoted String [Token]+    | T_HereString Id Token+    | T_If Id+    | T_IfExpression Id [([Token],[Token])] [Token]+    | T_In  Id+    | T_IoFile Id Token Token+    | T_LESSAND Id+    | T_LESSGREAT Id+    | T_Lbrace Id+    | T_Less Id+    | T_Literal Id String+    | T_Lparen Id+    | T_NEWLINE Id+    | T_NormalWord Id [Token]+    | T_OR_IF Id+    | T_OrIf Id (Token) (Token)+    | T_Pipeline Id [Token]+    | T_ProcSub Id String [Token]+    | T_Rbrace Id+    | T_Redirecting Id [Token] Token+    | T_Rparen Id+    | T_Script Id String [Token]+    | T_Select Id+    | T_SelectIn Id String [Token] [Token]+    | T_Semi Id+    | T_SimpleCommand Id [Token] [Token]+    | T_SingleQuoted Id String+    | T_Subshell Id [Token]+    | T_Then Id+    | T_Until Id+    | T_UntilExpression Id [Token] [Token]+    | T_While Id+    | T_WhileExpression Id [Token] [Token]+    | T_Annotation Id [Annotation] Token+    deriving (Show)++data Annotation = DisableComment Integer deriving (Show, Eq)+data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)++-- I apologize for nothing!+lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"+instance Eq Token where+    (==) a b = (lolHax a) == (lolHax b)+++analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token+analyze f g i t =+    round t+  where+    round t = do+        f t+        newT <- delve t+        g t+        return . i $ newT+    roundAll = mapM round++    roundMaybe Nothing = return Nothing+    roundMaybe (Just v) = do+        s <- round v+        return (Just s)++    dl l v = do+        x <- roundAll l+        return $ v x+    dll l m v = do+        x <- roundAll l+        y <- roundAll m+        return $ v x m+    d1 t v = do+        x <- round t+        return $ v x+    d2 t1 t2 v = do+        x <- round t1+        y <- round t2+        return $ v x y++    delve (T_NormalWord id list) = dl list $ T_NormalWord id+    delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id+    delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id+    delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id+    delve (T_Backticked id list) = dl list $ T_Backticked id+    delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id+    delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id+    delve (T_IoFile id op file) = d2 op file $ T_IoFile id+    delve (T_HereString id word) = d1 word $ T_HereString id+    delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v+    delve (T_Assignment id mode var index value) = do+        a <- roundMaybe index+        b <- round value+        return $ T_Assignment id mode var a b+    delve (T_Array id t) = dl t $ T_Array id+    delve (T_Redirecting id redirs cmd) = do+        a <- roundAll redirs+        b <- round cmd+        return $ T_Redirecting id a b+    delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id+    delve (T_Pipeline id l) = dl l $ T_Pipeline id+    delve (T_Banged id l) = d1 l $ T_Banged id+    delve (T_AndIf id t u) = d2 t u $ T_AndIf id+    delve (T_OrIf id t u) = d2 t u $ T_OrIf id+    delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id+    delve (T_Subshell id l) = dl l $ T_Subshell id+    delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ+    delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id+    delve (T_IfExpression id conditions elses) = do+        newConds <- mapM (\(c, t) -> do+                            x <- mapM round c+                            y <- mapM round t+                            return (x,y)+                    ) conditions+        newElses <- roundAll elses+        return $ T_IfExpression id newConds newElses+    delve (T_BraceGroup id l) = dl l $ T_BraceGroup id+    delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id+    delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id+    delve (T_ForIn id v w l) = dll w l $ T_ForIn id v+    delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v+    delve (T_CaseExpression id word cases) = do+        newWord <- round word+        newCases <- mapM (\(c, t) -> do+                            x <- mapM round c+                            y <- mapM round t+                            return (x,y)+                        ) cases+        return $ T_CaseExpression id newWord newCases++    delve (T_ForArithmetic id a b c group) = do+        x <- round a+        y <- round b+        z <- round c+        list <- mapM round group+        return $ T_ForArithmetic id x y z list++    delve (T_Script id s l) = dl l $ T_Script id s+    delve (T_Function id name body) = d1 body $ T_Function id name+    delve (T_Condition id typ token) = d1 token $ T_Condition id typ+    delve (T_Extglob id str l) = dl l $ T_Extglob id str+    delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id+    delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str++    delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str+    delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str+    delve (TC_Group id typ token) = d1 token $ TC_Group id typ+    delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op+    delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op+    delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ++    delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op+    delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op+    delve (TA_Sequence id l) = dl l $ TA_Sequence id+    delve (TA_Trinary id t1 t2 t3) = do+        a <- round t1+        b <- round t2+        c <- round t3+        return $ TA_Trinary id a b c+    delve (TA_Expansion id t) = d1 t $ TA_Expansion id+    delve (TA_Base id b t) = d1 t $ TA_Base id b+    delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns+    delve t = return t++getId t = case t of+        T_AND_IF id  -> id+        T_OR_IF id  -> id+        T_DSEMI id  -> id+        T_Semi id  -> id+        T_DLESS id  -> id+        T_DGREAT id  -> id+        T_LESSAND id  -> id+        T_GREATAND id  -> id+        T_LESSGREAT id  -> id+        T_DLESSDASH id  -> id+        T_CLOBBER id  -> id+        T_If id  -> id+        T_Then id  -> id+        T_Else id  -> id+        T_Elif id  -> id+        T_Fi id  -> id+        T_Do id  -> id+        T_Done id  -> id+        T_Case id  -> id+        T_Esac id  -> id+        T_While id  -> id+        T_Until id  -> id+        T_For id  -> id+        T_Select id  -> id+        T_Lbrace id  -> id+        T_Rbrace id  -> id+        T_Lparen id  -> id+        T_Rparen id  -> id+        T_Bang id  -> id+        T_In  id  -> id+        T_NEWLINE id  -> id+        T_EOF id  -> id+        T_Less id  -> id+        T_Greater id  -> id+        T_SingleQuoted id _  -> id+        T_Literal id _  -> id+        T_NormalWord id _  -> id+        T_DoubleQuoted id _  -> id+        T_DollarExpansion id _  -> id+        T_DollarBraced id _  -> id+        T_DollarArithmetic id _  -> id+        T_BraceExpansion id _  -> id+        T_IoFile id _ _  -> id+        T_HereDoc id _ _ _ _ -> id+        T_HereString id _  -> id+        T_FdRedirect id _ _  -> id+        T_Assignment id _ _ _ _  -> id+        T_Array id _  -> id+        T_Redirecting id _ _  -> id+        T_SimpleCommand id _ _  -> id+        T_Pipeline id _  -> id+        T_Banged id _  -> id+        T_AndIf id _ _ -> id+        T_OrIf id _ _ -> id+        T_Backgrounded id _  -> id+        T_IfExpression id _ _  -> id+        T_Subshell id _  -> id+        T_BraceGroup id _  -> id+        T_WhileExpression id _ _  -> id+        T_UntilExpression id _ _  -> id+        T_ForIn id _ _ _  -> id+        T_SelectIn id _ _ _  -> id+        T_CaseExpression id _ _ -> id+        T_Function id _ _  -> id+        T_Arithmetic id _  -> id+        T_Script id _ _  -> id+        T_Condition id _ _  -> id+        T_Extglob id _ _ -> id+        T_Backticked id _ -> id+        TC_And id _ _ _ _  -> id+        TC_Or id _ _ _ _  -> id+        TC_Group id _ _  -> id+        TC_Binary id _ _ _ _  -> id+        TC_Unary id _ _ _  -> id+        TC_Noary id _ _  -> id+        TA_Binary id _ _ _  -> id+        TA_Unary id _ _  -> id+        TA_Sequence id _  -> id+        TA_Variable id _  -> id+        TA_Trinary id _ _ _  -> id+        TA_Expansion id _  -> id+        TA_Literal id _ -> id+        TA_Base id _ _ -> id+        T_ProcSub id _ _ -> id+        T_Glob id _ -> id+        T_ForArithmetic id _ _ _ _ -> id+        T_DollarSingleQuoted id _ -> id+        T_DollarDoubleQuoted id _ -> id+        T_DollarBracket id _ -> id+        T_Annotation id _ _ -> id++blank :: Monad m => Token -> m ()+blank = const $ return ()+doAnalysis f t = analyze f blank id t+doStackAnalysis startToken endToken t = analyze startToken endToken id t+doTransform i t = runIdentity $ analyze blank blank i t+
+ ShellCheck/Analytics.hs view
@@ -0,0 +1,1985 @@+{-+    This file is part of ShellCheck.+    http://www.vidarholen.net/contents/shellcheck++    ShellCheck 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.++    ShellCheck 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/>.+-}+module ShellCheck.Analytics where++import ShellCheck.AST+import ShellCheck.Data+import ShellCheck.Parser+import Control.Monad+import Control.Monad.State+import qualified Data.Map as Map+import Data.Char+import Data.Functor+import Data.List+import Data.Maybe+import Debug.Trace+import Text.Regex+import Data.Maybe++data Shell = Ksh | Zsh | Sh | Bash+    deriving (Show, Eq)++genericChecks = [+    runBasicAnalysis (\x -> mapM_ (flip ($) x) basicChecks)+    ,runBasicTreeAnalysis treeChecks+    ,subshellAssignmentCheck+    ,checkSpacefulness+    ,checkQuotesInLiterals+    ,checkShebang+    ,checkFunctionsUsedExternally+    ,checkUnusedAssignments+    ,checkWrongArithmeticAssignment+    ]++checksFor Sh = map runBasicAnalysis [+    checkBashisms+    ,checkTimeParameters+    ,checkCdAndBack Sh+    ]+checksFor Ksh = map runBasicAnalysis [+    checkEchoSed+    ,checkCdAndBack Ksh+    ]+checksFor Zsh = map runBasicAnalysis [+    checkTimeParameters+    ,checkEchoSed+    ,checkCdAndBack Zsh+    ]+checksFor Bash = map runBasicAnalysis [+    checkTimeParameters+    ,checkBraceExpansionVars+    ,checkEchoSed+    ,checkCdAndBack Bash+    ]++runAllAnalytics root m = addToMap notes m+    where shell = determineShell root+          notes =  checkList ((checksFor shell) ++ genericChecks) root++checkList l t = concatMap (\f -> f t) l+addToMap list map = foldr (\(id,note) m -> Map.adjust (\(Metadata pos notes) -> Metadata pos (note:notes)) id m) map list++prop_determineShell0 = determineShell (T_Script (Id 0) "#!/bin/sh" []) == Sh+prop_determineShell1 = determineShell (T_Script (Id 0) "#!/usr/bin/env ksh" []) == Ksh+prop_determineShell2 = determineShell (T_Script (Id 0) "" []) == Bash+determineShell (T_Script _ shebang _) = normalize $ shellFor shebang+    where shellFor s | "/env " `isInfixOf` s = head ((drop 1 $ words s)++[""])+          shellFor s = reverse . takeWhile (/= '/') . reverse $ s+          normalize "sh" = Sh+          normalize "ksh" = Ksh+          normalize "zsh" = Zsh+          normalize "bash" = Bash+          normalize _ = Bash++runBasicAnalysis f t = snd $ runState (doAnalysis f t) []+basicChecks = [+    checkUuoc+    ,checkPipePitfalls+    ,checkForInQuoted+    ,checkForInLs+    ,checkShorthandIf+    ,checkDollarStar+    ,checkUnquotedDollarAt+    ,checkStderrRedirect+    ,checkUnquotedN+    ,checkNumberComparisons+    ,checkSingleBracketOperators+    ,checkDoubleBracketOperators+    ,checkNoaryWasBinary+    ,checkConstantNoary+    ,checkForDecimals+    ,checkDivBeforeMult+    ,checkArithmeticDeref+    ,checkArithmeticBadOctal+    ,checkComparisonAgainstGlob+    ,checkPrintfVar+    ,checkCommarrays+    ,checkOrNeq+    ,checkEchoWc+    ,checkConstantIfs+    ,checkTr+    ,checkPipedAssignment+    ,checkAssignAteCommand+    ,checkUuoe+    ,checkFindNameGlob+    ,checkGrepRe+    ,checkNeedlessCommands+    ,checkQuotedCondRegex+    ,checkForInCat+    ,checkFindExec+    ,checkValidCondOps+    ,checkGlobbedRegex+    ,checkTrapQuotes+    ,checkTestRedirects+    ,checkIndirectExpansion+    ,checkSudoRedirect+    ,checkPS1Assignments+    ,checkBackticks+    ,checkInexplicablyUnquoted+    ,checkTildeInQuotes+    ,checkLonelyDotDash+    ,checkSpuriousExec+    ,checkSpuriousExpansion+    ,checkUnusedEchoEscapes+    ,checkDollarBrackets+    ,checkSshHereDoc+    ,checkSshCommandString+    ,checkGlobsAsOptions+    ,checkWhileReadPitfalls+    ,checkArithmeticOpCommand+    ,checkCharRangeGlob+    ]+treeChecks = [+    checkUnquotedExpansions+    ,checkSingleQuotedVariables+    ,checkRedirectToSame+    ,checkPrefixAssignmentReference+    ]+++runBasicTreeAnalysis checks token =+    checkList (map runTree checks) token+  where+    parentTree = getParentTree token+    runTree f t = runBasicAnalysis (flip f $ parentTree) t++filterByAnnotation token metadataMap =+    Map.mapWithKey removeVals metadataMap+  where+    removeVals id (Metadata pos notes) =+        Metadata pos $ filter (not . shouldIgnore id . numFor) notes+    numFor (Note _ code _) = code+    shouldIgnore id num =+        any (shouldIgnoreFor num) $ getPath parents (T_Bang id)+    shouldIgnoreFor num (T_Annotation _ anns _) =+        any hasNum anns+      where+        hasNum (DisableComment ts) = num == ts+    shouldIgnoreFor _ _ = False+    parents = getParentTree token++addNoteFor id note = modify ((id, note):)+warn id code note = addNoteFor id $ Note WarningC code $ note+err id code note = addNoteFor id $ Note ErrorC code $ note+info id code note = addNoteFor id $ Note InfoC code $ note+style id code note = addNoteFor id $ Note StyleC code $ note++isVariableStartChar x = x == '_' || x >= 'a' && x <= 'z' || x >= 'A' && x <= 'Z'+isVariableChar x = isVariableStartChar x || x >= '0' && x <= '9'++prop_isVariableName1 = isVariableName "_fo123"+prop_isVariableName2 = not $ isVariableName "4"+prop_isVariableName3 = not $ isVariableName "test: "+isVariableName (x:r) = isVariableStartChar x && all isVariableChar r+isVariableName _ = False++willSplit x =+  case x of+    T_DollarBraced _ _ -> True+    T_DollarExpansion _ _ -> True+    T_Backticked _ _ -> True+    T_BraceExpansion _ s -> True+    T_Glob _ _ -> True+    T_Extglob _ _ _ -> True+    T_NormalWord _ l -> any willSplit l+    _ -> False++isGlob (T_Extglob _ _ _) = True+isGlob (T_Glob _ _) = True+isGlob (T_NormalWord _ l) = any isGlob l+isGlob _ = False++wouldHaveBeenGlob s = '*' `elem` s++isConfusedGlobRegex ('*':_) = True+isConfusedGlobRegex [x,'*'] | x /= '\\' = True+isConfusedGlobRegex _ = False++isPotentiallyConfusedGlobRegex =+    let re = mkRegex "[a-z1-9]\\*" in+        isJust . matchRegex re++matches string regex = isJust $ matchRegex regex string++headOrDefault _ (a:_) = a+headOrDefault def _ = def++isConstant token =+    case token of+        T_NormalWord _ l   -> all isConstant l+        T_DoubleQuoted _ l -> all isConstant l+        T_SingleQuoted _ _ -> True+        T_Literal _ _ -> True+        _ -> False++isEmpty token =+    case token of+        T_NormalWord _ l   -> all isEmpty l+        T_DoubleQuoted _ l -> all isEmpty l+        T_SingleQuoted _ "" -> True+        T_Literal _ "" -> True+        _ -> False++makeSimple (T_NormalWord _ [f]) = f+makeSimple (T_Redirecting _ _ f) = f+makeSimple (T_Annotation _ _ f) = f+makeSimple t = t+simplify = doTransform makeSimple++deadSimple (T_NormalWord _ l) = [concat (concatMap (deadSimple) l)]+deadSimple (T_DoubleQuoted _ l) = [(concat (concatMap (deadSimple) l))]+deadSimple (T_SingleQuoted _ s) = [s]+deadSimple (T_DollarBraced _ _) = ["${VAR}"]+deadSimple (T_DollarArithmetic _ _) = ["${VAR}"]+deadSimple (T_DollarExpansion _ _) = ["${VAR}"]+deadSimple (T_Backticked _ _) = ["${VAR}"]+deadSimple (T_Glob _ s) = [s]+deadSimple (T_Pipeline _ [x]) = deadSimple x+deadSimple (T_Literal _ x) = [x]+deadSimple (T_SimpleCommand _ vars words) = concatMap (deadSimple) words+deadSimple (T_Redirecting _ _ foo) = deadSimple foo+deadSimple (T_DollarSingleQuoted _ s) = [s]+deadSimple (T_Annotation _ _ s) = deadSimple s+deadSimple _ = []++(!!!) list i =+    case drop i list of+        [] -> Nothing+        (r:_) -> Just r++verify f s = checkBasic f s == Just True+verifyNot f s = checkBasic f s == Just False+verifyFull f s = checkFull f s == Just True+verifyNotFull f s = checkFull f s == Just False+verifyTree f s = checkTree f s == Just True+verifyNotTree f s = checkTree f s == Just False++checkBasic f s = checkFull (runBasicAnalysis f) s+checkTree f s = checkFull (runBasicTreeAnalysis [f]) s+checkFull f s = case parseShell "-" s of+        (ParseResult (Just (t, m)) _) -> Just . not . null $ f t+        _ -> Nothing+++prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"+checkEchoWc (T_Pipeline id [a, b]) =+    when (acmd == ["echo", "${VAR}"]) $+        case bcmd of+            ["wc", "-c"] -> countMsg+            ["wc", "-m"] -> countMsg+            _ -> return ()+  where+    acmd = deadSimple a+    bcmd = deadSimple b+    countMsg = style id 2000 $ "See if you can use ${#variable} instead."+checkEchoWc _ = return ()++prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"+prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"+checkEchoSed (T_Pipeline id [a, b]) =+    when (acmd == ["echo", "${VAR}"]) $+        case bcmd of+            ["sed", v] -> checkIn v+            ["sed", "-e", v] -> checkIn v+            _ -> return ()+  where+    sedRe = mkRegex "^s(.)(.*)\\1(.*)\\1g?$"+    acmd = deadSimple a+    bcmd = deadSimple b+    checkIn s =+        case matchRegex sedRe s of+                Just _ -> style id 2001 $ "See if you can use ${variable//search/replace} instead."+                _        -> return ()+checkEchoSed _ = return ()++prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo"+prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo"+prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo"+checkPipedAssignment (T_Pipeline _ (T_Redirecting _ _ (T_SimpleCommand id (_:_) []):_:_)) =+    warn id 2036 "If you wanted to assign the output of the pipeline, use a=$(b | c) ."+checkPipedAssignment _ = return ()++prop_checkAssignAteCommand1 = verify checkAssignAteCommand "A=ls -l"+prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo"+prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar"+prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l"+prop_checkAssignAteCommand5 = verifyNot checkAssignAteCommand "PAGER=cat grep bar"+checkAssignAteCommand (T_SimpleCommand id ((T_Assignment _ _ _ _ assignmentTerm):[]) (firstWord:_)) =+    when ("-" `isPrefixOf` (concat $ deadSimple firstWord) ||+        (isCommonCommand (getLiteralString assignmentTerm) && not (isCommonCommand (getLiteralString firstWord)))) $+            warn id 2037 "To assign the output of a command, use var=$(cmd) ."+  where+    isCommonCommand (Just s) = s `elem` commonCommands+    isCommonCommand _ = False+checkAssignAteCommand _ = return ()++prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1"+prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2"+prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts"+checkArithmeticOpCommand (T_SimpleCommand id ((T_Assignment _ _ _ _ _):[]) (firstWord:_)) =+    fromMaybe (return ()) $ check <$> getGlobOrLiteralString firstWord+  where+    check op =+        when (op `elem` ["+", "-", "*", "/"]) $+            warn (getId firstWord) 2099 $+                "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"+checkArithmeticOpCommand _ = return ()++prop_checkWrongArit = verifyFull checkWrongArithmeticAssignment "i=i+1"+prop_checkWrongArit2 = verifyFull checkWrongArithmeticAssignment "n=2; i=n*2"+checkWrongArithmeticAssignment t = runBasicAnalysis f t+  where+    regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$"+    flow = getVariableFlow t+    references = foldl (flip ($)) Map.empty (map insertRef flow)+    insertRef (Assignment (_, _, name, _)) =+        Map.insert name ()+    insertRef _ = id++    getNormalString (T_NormalWord _ words) = do+        parts <- foldl (liftM2 (\x y -> x ++ [y])) (Just []) $ map getLiterals words+        return $ concat parts+    getNormalString _ = Nothing++    getLiterals (T_Literal _ s) = return s+    getLiterals (T_Glob _ s) = return s+    getLiterals _ = Nothing++    f (T_SimpleCommand id ((T_Assignment _ _ _ _ val):[]) []) =+      fromMaybe (return ()) $ do+        str <- getNormalString val+        match <- matchRegex regex str+        var <- match !!! 0+        op <- match !!! 1+        Map.lookup var references+        return $ do+            warn (getId val) 2100 $+                "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"+    f _ = return ()++prop_checkUuoc1 = verify checkUuoc "cat foo | grep bar"+prop_checkUuoc2 = verifyNot checkUuoc "cat * | grep bar"+prop_checkUuoc3 = verify checkUuoc "cat $var | grep bar"+prop_checkUuoc4 = verifyNot checkUuoc "cat $var"+checkUuoc (T_Pipeline _ ((T_Redirecting _ _ cmd):_:_)) = checkCommand "cat" f cmd+  where+    f [word] = when (isSimple word) $+        style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead."+    f _ = return ()+    isSimple (T_NormalWord _ parts) = all isSimple parts+    isSimple (T_DollarBraced _ _) = True+    isSimple x = not $ willSplit x+checkUuoc _ = return ()++prop_checkNeedlessCommands = verify checkNeedlessCommands "foo=$(expr 3 + 2)"+prop_checkNeedlessCommands2 = verify checkNeedlessCommands "foo=`echo \\`expr 3 + 2\\``"+prop_checkNeedlessCommands3 = verifyNot checkNeedlessCommands "foo=$(expr foo : regex)"+checkNeedlessCommands cmd@(T_SimpleCommand id _ _) |+        cmd `isCommand` "expr" && (not $ ":" `elem` deadSimple cmd) =+    style id 2003 "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."+checkNeedlessCommands _ = return ()++prop_checkPipePitfalls3 = verify checkPipePitfalls "ls | grep -v mp3"+prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0 foo"+prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo"+prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo"+checkPipePitfalls (T_Pipeline id commands) = do+    for ["find", "xargs"] $+        \(find:xargs:_) -> let args = deadSimple xargs in+            when (not $ hasShortParameter args '0') $+                warn (getId find) 2038 "Use either 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow for non-alphanumeric filenames."++    for ["?", "echo"] $+        \(_:echo:_) -> info (getId echo) 2008 "echo doesn't read from stdin, are you sure you should be piping to it?"++    for' ["ps", "grep"] $+        \x -> info x 2009 "Consider using pgrep instead of grepping ps output."++    didLs <- liftM or . sequence $ [+        for' ["ls", "grep"] $+            \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.",+        for' ["ls", "xargs"] $+            \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames."+        ]+    when (not didLs) $ do+        for ["ls", "?"] $+            \(ls:_) -> (when (not $ hasShortParameter (deadSimple ls) 'N') $+                info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames.")+        return ()+  where+    for l f =+        let indices = indexOfSublists l (map (headOrDefault "" . deadSimple) commands)+        in do+            mapM_ f (map (\n -> take (length l) $ drop n $ commands) indices)+            return . not . null $ indices+    for' l f = for l (first f)+    first func (x:_) = func (getId x)+    first _ _ = return ()+    hasShortParameter list char = any (\x -> "-" `isPrefixOf` x && char `elem` x) list+checkPipePitfalls _ = return ()++indexOfSublists sub all = f 0 all+  where+    f _ [] = []+    f n a@(r:rest) =+        let others = f (n+1) rest in+            if match sub a+              then n:others+              else others+    match ("?":r1) (_:r2) = match r1 r2+    match (x1:r1) (x2:r2) | x1 == x2 = match r1 r2+    match [] _ = True+    match _ _ = False+++bracedString l = concat $ deadSimple l+isMagicInQuotes (T_DollarBraced _ l) | '@' `elem` (bracedString l) = True+isMagicInQuotes _ = False++prop_checkShebang1 = verifyFull checkShebang "#!/usr/bin/env bash -x\necho cow"+prop_checkShebang2 = verifyNotFull checkShebang "#! /bin/sh  -l "+checkShebang (T_Script id sb _) =+    if (length $ words sb) > 2 then+        let note = Note ErrorC 2096 $ "On most OS, shebangs can only specify a single parameter."+        in [(id, note)]+    else []++prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"+prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"+prop_checkBashisms3 = verify checkBashisms "echo $((i++))"+prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"+prop_checkBashisms5 = verify checkBashisms "source file"+prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"+prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"+prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"+prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"+prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"+prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"+prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"+prop_checkBashisms13= verify checkBashisms "exec -c env"+prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""+prop_checkBashisms15= verify checkBashisms "let n++"+prop_checkBashisms16= verify checkBashisms "echo $RANDOM"+prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"+prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"+checkBashisms = bashism+  where+    errMsg id s = err id 2040 $ "#!/bin/sh was specified, so " ++ s ++ " is not supported, even when sh is actually bash."+    warnMsg id s = warn id 2039 $ "#!/bin/sh was specified, but " ++ s ++ " is not standard."+    bashism (T_ProcSub id _ _) = errMsg id "process substitution"+    bashism (T_Extglob id _ _) = warnMsg id "extglob"+    bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..'"+    bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\""+    bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loop"+    bashism (T_Arithmetic id _) = warnMsg id "standalone ((..))"+    bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..))"+    bashism (T_SelectIn id _ _ _) = warnMsg id "select loop"+    bashism (T_BraceExpansion id _) = warnMsg id "brace expansion"+    bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]]"+    bashism (T_HereString id _) = warnMsg id "here-string"+    bashism (TC_Binary id SingleBracket op _ _)+        | op `elem` [ "-nt", "-ef", "\\<", "\\>", "==" ] =+            warnMsg id op+    bashism (TA_Unary id op _)+        | op `elem` [ "|++", "|--", "++|", "--|"] =+            warnMsg id (filter (/= '|') op)+    bashism t@(T_SimpleCommand id _ _)+        | t `isCommand` "source" =+            warnMsg id "'source' in place of '.'"+    bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&>"+    bashism (T_DollarBraced id token) =+        mapM_ check expansion+      where+        str = concat $ deadSimple token+        check (regex, feature) =+            when (isJust $ matchRegex regex str) $ warnMsg id feature++    bashism t@(T_SimpleCommand _ _ (cmd:arg:_))+        | t `isCommand` "echo" && "-" `isPrefixOf` argString =+            when (not $ "--" `isPrefixOf` argString) $ -- echo "-------"+                warnMsg (getId arg) "echo flag"+      where argString = (concat $ deadSimple arg)+    bashism t@(T_SimpleCommand _ _ (cmd:arg:_))+        | t `isCommand` "exec" && "-" `isPrefixOf` (concat $ deadSimple arg) =+            warnMsg (getId arg) "exec flag"+    bashism t@(T_SimpleCommand id _ _)+        | t `isCommand` "let" = warnMsg id "'let'"+    bashism t@(TA_Variable id "RANDOM") =+        warnMsg id "RANDOM"++    bashism _ = return()++    varChars="_0-9a-zA-Z"+    expansion = let re = mkRegex in [+        (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references"),+        (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion"),+        (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefix"),+        (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing"),+        (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement"),+        (re $ "^RANDOM$", "$RANDOM")+        ]++prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"+prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done"+prop_checkForInQuoted2a = verifyNot checkForInQuoted "for f in *.mp3; do echo foo; done"+prop_checkForInQuoted2b = verify checkForInQuoted "for f in \"*.mp3\"; do echo foo; done"+prop_checkForInQuoted3 = verify checkForInQuoted "for f in 'find /'; do true; done"+prop_checkForInQuoted4 = verify checkForInQuoted "for f in 1,2,3; do true; done"+prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done"+checkForInQuoted (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =+    when (any (\x -> willSplit x && not (isMagicInQuotes x)) list+            || (getLiteralString word >>= (return . wouldHaveBeenGlob)) == Just True) $+        err id 2066 $ "Since you double quoted this, it will not word split, and the loop will only run once."+checkForInQuoted (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id s]] _) =+    warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")."+checkForInQuoted (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) =+    if ',' `elem` s+      then warn id 2042 $ "Use spaces, not commas, to separate loop elements."+      else warn id 2043 $ "This loop will only run once, with " ++ f ++ "='" ++ s ++ "'."+checkForInQuoted _ = return ()++prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"+prop_checkForInCat1a= verify checkForInCat "for f in `cat foo`; do stuff; done"+prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done"+prop_checkForInCat2a= verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done"+prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done"+checkForInCat (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w+  where+    checkF (T_DollarExpansion id [T_Pipeline _ r])+        | all isLineBased r =+            info id 2013 "To read lines rather than words, pipe/redirect to a 'while read' loop."+    checkF (T_Backticked id cmds) = checkF (T_DollarExpansion id cmds)+    checkF _ = return ()+    isLineBased cmd = any (cmd `isCommand`)+                        ["grep", "fgrep", "egrep", "sed", "cat", "awk", "cut", "sort"]+checkForInCat _ = return ()++prop_checkForInLs = verify checkForInLs "for f in $(ls *.mp3); do mplayer \"$f\"; done"+prop_checkForInLs2 = verify checkForInLs "for f in `ls *.mp3`; do mplayer \"$f\"; done"+prop_checkForInLs3 = verify checkForInLs "for f in `find / -name '*.mp3'`; do mplayer \"$f\"; done"+checkForInLs t = try t+  where+   try (T_ForIn _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =+        check id f x+   try (T_ForIn _ f [T_NormalWord _ [T_Backticked id [x]]] _) =+        check id f x+   try _ = return ()+   check id f x =+    case deadSimple x of+      ("ls":n) ->+        let warntype = if any ("-" `isPrefixOf`) n then warn else err in+          warntype id 2045 $ "Iterate over globs whenever possible (e.g. 'for f in */*.wav'), as for loops over ls will fail for filenames like 'my file*.txt'."+      ("find":_) -> warn id 2044 $ "Use find -exec or a while read loop instead, as for loops over find will fail for filenames like 'my file*.txt'."+      _ -> return ()+++prop_checkFindExec1 = verify checkFindExec "find / -name '*.php' -exec rm {};"+prop_checkFindExec2 = verify checkFindExec "find / -exec touch {} && ls {} \\;"+prop_checkFindExec3 = verify checkFindExec "find / -execdir cat {} | grep lol +"+prop_checkFindExec4 = verifyNot checkFindExec "find / -name '*.php' -exec foo {} +"+prop_checkFindExec5 = verifyNot checkFindExec "find / -execdir bash -c 'a && b' \\;"+prop_checkFindExec6 = verify checkFindExec "find / -type d -execdir rm *.jpg \\;"+checkFindExec cmd@(T_SimpleCommand _ _ t@(h:r)) | cmd `isCommand` "find" = do+    c <- broken r False+    when c $ do+        let wordId = getId $ last t in+            err wordId 2067 "Missing ';' or + terminating -exec. You can't use |/||/&&, and ';' has to be a separate, quoted argument."++  where+    broken [] v = return v+    broken (w:r) v = do+        when v $ (mapM_ warnFor $ fromWord w)+        case getLiteralString w of+            Just "-exec" -> broken r True+            Just "-execdir" -> broken r True+            Just "+" -> broken r False+            Just ";" -> broken r False+            _ -> broken r v++    shouldWarn x =+      case x of+        T_DollarExpansion _ _ -> True+        T_Backticked _ _ -> True+        T_Glob _ _ -> True+        T_Extglob _ _ _ -> True+        _ -> False++    warnFor x =+        if shouldWarn x+        then info (getId x) 2014 "This will expand once before find runs, not per file found."+        else return ()++    fromWord (T_NormalWord _ l) = l+    fromWord _ = []+checkFindExec _ = return ()+++prop_checkUnquotedExpansions1 = verifyTree checkUnquotedExpansions "rm $(ls)"+prop_checkUnquotedExpansions1a= verifyTree checkUnquotedExpansions "rm `ls`"+prop_checkUnquotedExpansions2 = verifyTree checkUnquotedExpansions "rm foo$(date)"+prop_checkUnquotedExpansions3 = verifyTree checkUnquotedExpansions "[ $(foo) == cow ]"+prop_checkUnquotedExpansions3a= verifyTree checkUnquotedExpansions "[ ! $(foo) ]"+prop_checkUnquotedExpansions4 = verifyNotTree checkUnquotedExpansions "[[ $(foo) == cow ]]"+prop_checkUnquotedExpansions5 = verifyNotTree checkUnquotedExpansions "for f in $(cmd); do echo $f; done"+prop_checkUnquotedExpansions6 = verifyNotTree checkUnquotedExpansions "$(cmd)"+prop_checkUnquotedExpansions7 = verifyNotTree checkUnquotedExpansions "cat << foo\n$(ls)\nfoo"+checkUnquotedExpansions t tree =+    check t+  where+    check t@(T_DollarExpansion _ _) = examine t+    check t@(T_Backticked _ _) = examine t+    check _ = return ()+    examine t =+        unless (inUnquotableContext tree t || usedAsCommandName tree t) $+            warn (getId t) 2046 "Quote this to prevent word splitting."+++prop_checkRedirectToSame = verifyTree checkRedirectToSame "cat foo > foo"+prop_checkRedirectToSame2 = verifyTree checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol"+prop_checkRedirectToSame3 = verifyNotTree checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol"+prop_checkRedirectToSame4 = verifyNotTree checkRedirectToSame "foo /dev/null > /dev/null"+prop_checkRedirectToSame5 = verifyNotTree checkRedirectToSame "foo > bar 2> bar"+checkRedirectToSame s@(T_Pipeline _ list) parents =+    mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurences x) l) (getAllRedirs list))) list+  where checkOccurences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =+            when (exceptId /= newId+                    && x == y+                    && not (isOutput t && isOutput u)+                    && not (special t)) $ do+                let note = Note InfoC 2094 $ "Make sure not to read and write the same file in the same pipeline."+                addNoteFor newId $ note+                addNoteFor exceptId $ note+        checkOccurences _ _ = return ()+        getAllRedirs l = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls) l+        getRedirs (T_FdRedirect _ _ (T_IoFile _ op file)) =+                case op of T_Greater _ -> [file]+                           T_Less _    -> [file]+                           T_DGREAT _  -> [file]+                           _ -> []+        getRedirs _ = []+        special x = "/dev/" `isPrefixOf` (concat $ deadSimple x)+        isOutput t =+            case drop 1 $ getPath parents t of+                (T_IoFile _ op _):_ ->+                    case op of+                        T_Greater _  -> True+                        T_DGREAT _ -> True+                        _ -> False+                _ -> False+checkRedirectToSame _ _ = return ()+++prop_checkShorthandIf  = verify checkShorthandIf "[[ ! -z file ]] && scp file host || rm file"+prop_checkShorthandIf2 = verifyNot checkShorthandIf "[[ ! -z file ]] && { scp file host || echo 'Eek'; }"+prop_checkShorthandIf3 = verifyNot checkShorthandIf "foo && bar || echo baz"+prop_checkShorthandIf4 = verifyNot checkShorthandIf "foo && a=b || a=c"+checkShorthandIf (T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ t)))+        | not $ isOk t =+    info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true."+  where+    isOk [t] = isAssignment t || (fromMaybe False $ do+        name <- getCommandBasename t+        return $ name `elem` ["echo", "exit"])+    isOk _ = False+checkShorthandIf _ = return ()+++prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"+checkDollarStar (T_NormalWord _ [(T_DollarBraced id l)]) | (bracedString l) == "*"  =+    warn id 2048 $ "Use \"$@\" (with quotes) to prevent whitespace problems."+checkDollarStar _ = return ()+++prop_checkUnquotedDollarAt = verify checkUnquotedDollarAt "ls $@"+prop_checkUnquotedDollarAt1= verifyNot checkUnquotedDollarAt "ls ${#@}"+prop_checkUnquotedDollarAt2 = verify checkUnquotedDollarAt "ls ${foo[@]}"+prop_checkUnquotedDollarAt3 = verifyNot checkUnquotedDollarAt "ls ${#foo[@]}"+prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\""+prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }"+checkUnquotedDollarAt (T_NormalWord _ [T_DollarBraced id l]) =+    let string = bracedString l+        failing = err id 2068 $ "Add double quotes around ${" ++ string ++ "}, otherwise it's just like $* and breaks on spaces."+        in do+            when ("@" `isPrefixOf` string) failing+            when (not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string) failing+checkUnquotedDollarAt _ = return ()++prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow"+prop_checkStderrRedirect2 = verifyNot checkStderrRedirect "test > cow 2>&1"+checkStderrRedirect (T_Redirecting _ [+    T_FdRedirect id "2" (T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "1"])),+    T_FdRedirect _ _ (T_IoFile _ op _)+    ] _) = case op of+            T_Greater _ -> error+            T_DGREAT _ -> error+            _ -> return ()+         where error = err id 2069 $ "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."+checkStderrRedirect _ = return ()++lt x = trace ("FAILURE " ++ (show x)) x+ltt t x = trace ("FAILURE " ++ (show t)) x+++prop_checkSingleQuotedVariables  = verifyTree checkSingleQuotedVariables "echo '$foo'"+prop_checkSingleQuotedVariables2 = verifyTree checkSingleQuotedVariables "echo 'lol$1.jpg'"+prop_checkSingleQuotedVariables3 = verifyNotTree checkSingleQuotedVariables "sed 's/foo$/bar/'"+prop_checkSingleQuotedVariables3a= verifyTree checkSingleQuotedVariables "sed 's/${foo}/bar/'"+prop_checkSingleQuotedVariables3b= verifyTree checkSingleQuotedVariables "sed 's/$(echo cow)/bar/'"+prop_checkSingleQuotedVariables3c= verifyTree checkSingleQuotedVariables "sed 's/$((1+foo))/bar/'"+prop_checkSingleQuotedVariables4 = verifyNotTree checkSingleQuotedVariables "awk '{print $1}'"+prop_checkSingleQuotedVariables5 = verifyNotTree checkSingleQuotedVariables "trap 'echo $SECONDS' EXIT"+checkSingleQuotedVariables t@(T_SingleQuoted id s) parents =+            case matchRegex re s of+                Just [] -> unless (probablyOk t) $ info id 2016 $ "Expressions don't expand in single quotes, use double quotes for that."+                _          -> return ()+  where+    probablyOk t = fromMaybe False $ do+        cmd <- getClosestCommand parents t+        name <- getCommandBasename cmd+        return $ name `elem` [+                "trap"+                ,"sh"+                ,"bash"+                ,"ksh"+                ,"zsh"+                ,"ssh"+                ]+            || "awk" `isSuffixOf` name+            || "perl" `isPrefixOf` name++    re = mkRegex "\\$[{(0-9a-zA-Z_]"+checkSingleQuotedVariables _ _ = return ()+++prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"+prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"+prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"+checkUnquotedN (T_Condition _ SingleBracket (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t]))) | willSplit t =+       err id 2070 "Always true because you failed to quote. Use [[ ]] instead."+checkUnquotedN _ = return ()++prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"+prop_checkNumberComparisons2 = verify checkNumberComparisons "[[ 0 >= $(cmd) ]]"+prop_checkNumberComparisons3 = verifyNot checkNumberComparisons "[[ $foo ]] > 3"+prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]"+prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]"+prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 = $foo ]]"+checkNumberComparisons (TC_Binary id typ op lhs rhs) = do+    when (op `elem` ["<", ">", "<=", ">=", "\\<", "\\>", "\\<=", "\\>="]) $ do+        when (isNum lhs || isNum rhs) $ err id 2071 $ "\"" ++ op ++ "\" is for string comparisons. Use " ++ (eqv op) ++" ."+        mapM_ checkDecimals [lhs, rhs]++    when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq", "=", "=="]) $ do+        mapM_ checkDecimals [lhs, rhs]++  where+      checkDecimals hs = when (isFraction hs) $ err (getId hs) 2072 $ decimalError+      decimalError = "Decimals are not supported. Either use integers only, or use bc or awk to compare."+      isNum t = case deadSimple t of [v] -> all isDigit v+                                     _ -> False+      isFraction t = case deadSimple t of [v] -> isJust $ matchRegex floatRegex v+                                          _ -> False+      eqv ('\\':s) = eqv s+      eqv "<" = "-lt"+      eqv ">" = "-gt"+      eqv "<=" = "-le"+      eqv ">=" = "-ge"+      eqv _ = "the numerical equivalent"+      floatRegex = mkRegex "^[0-9]+\\.[0-9]+$"+checkNumberComparisons _ = return ()++prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"+prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]"+prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]"+prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done"+checkSingleBracketOperators (TC_Binary id typ op lhs rhs)+    | typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] =+        err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."+checkSingleBracketOperators (TC_Binary id typ op lhs rhs)+    | typ == SingleBracket && op == "=~" =+        err id 2074 $ "Can't use " ++ op ++" in [ ]. Use [[..]] instead."+checkSingleBracketOperators _ = return ()++prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]"+prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]"+checkDoubleBracketOperators x@(TC_Binary id typ op lhs rhs)+    | typ == DoubleBracket && op `elem` ["\\<", "\\>", "\\<=", "\\>="] =+        err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]"+checkDoubleBracketOperators _ = return ()++prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"+prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ 'cow' ]]"+prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"+checkQuotedCondRegex (TC_Binary _ _ "=~" _ rhs) =+    case rhs of+        T_NormalWord id [T_DoubleQuoted _ _] -> error id+        T_NormalWord id [T_SingleQuoted _ _] -> error id+        _ -> return ()+  where+    error id = err id 2076 $ "Don't quote rhs of =~, it'll match literally rather than as a regex."+checkQuotedCondRegex _ = return ()++prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"+prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"+prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"+prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"+prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"+checkGlobbedRegex (TC_Binary _ DoubleBracket "=~" _ rhs) =+    let s = concat $ deadSimple rhs in+        if isConfusedGlobRegex s+            then warn (getId rhs) 2049 $ "=~ is for regex. Use == for globs."+            else return ()+checkGlobbedRegex _ = return ()+++prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"+prop_checkConstantIfs2 = verify checkConstantIfs "[[ n -le 4 ]]"+prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n -ge 2 ]]"+prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"+prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"+checkConstantIfs (TC_Binary id typ op lhs rhs)+    | op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] = do+        when (isJust lLit && isJust rLit) $ warn id 2050 $ "This expression is constant. Did you forget the $ on a variable?"+    where+        lLit = getLiteralString lhs+        rLit = getLiteralString rhs+checkConstantIfs _ = return ()++prop_checkNoaryWasBinary = verify checkNoaryWasBinary "[[ a==$foo ]]"+prop_checkNoaryWasBinary2 = verify checkNoaryWasBinary "[ $foo=3 ]"+prop_checkNoaryWasBinary3 = verify checkNoaryWasBinary "[ $foo!=3 ]"+checkNoaryWasBinary (TC_Noary _ _ t@(T_NormalWord id l)) | not $ isConstant t = do+    let str = concat $ deadSimple t+    when ('=' `elem` str) $ err id 2077 $ "You need spaces around the comparison operator."+checkNoaryWasBinary _ = return ()++prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]"+prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]"+prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]"+prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]"+checkConstantNoary (TC_Noary _ _ t@(T_NormalWord id _)) | isConstant t = do+    err id 2078 $ "This expression is constant. Did you forget a $ somewhere?"+checkConstantNoary _ = return ()++prop_checkBraceExpansionVars = verify checkBraceExpansionVars "echo {1..$n}"+checkBraceExpansionVars (T_BraceExpansion id s) | '$' `elem` s =+    warn id 2051 $ "Bash doesn't support variables in brace expansions."+checkBraceExpansionVars _ = return ()++prop_checkForDecimals = verify checkForDecimals "((3.14*c))"+checkForDecimals (TA_Literal id s) | any (== '.') s = do+    err id 2079 $ "(( )) doesn't support decimals. Use bc or awk."+checkForDecimals _ = return ()++prop_checkDivBeforeMult = verify checkDivBeforeMult "echo $((c/n*100))"+prop_checkDivBeforeMult2 = verifyNot checkDivBeforeMult "echo $((c*100/n))"+checkDivBeforeMult (TA_Binary _ "*" (TA_Binary id "/" _ _) _) = do+    info id 2017 $ "Increase precision by replacing a/b*c with a*c/b."+checkDivBeforeMult _ = return ()++prop_checkArithmeticDeref = verify checkArithmeticDeref "echo $((3+$foo))"+prop_checkArithmeticDeref2 = verify checkArithmeticDeref "cow=14; (( s+= $cow ))"+prop_checkArithmeticDeref3 = verifyNot checkArithmeticDeref "cow=1/40; (( s+= ${cow%%/*} ))"+prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))"+prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))"+prop_checkArithmeticDeref6 = verifyNot checkArithmeticDeref "(( ${a[$i]} ))"+checkArithmeticDeref (TA_Expansion _ (T_DollarBraced id l)) | not . excepting $ bracedString l =+    style id 2004 $ "Don't use $ on variables in (( ))."+  where+    excepting [] = True+    excepting s = (any (`elem` "/.:#%?*@[]") s) || (isDigit $ head s)+checkArithmeticDeref _ = return ()++prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))"+prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))"+prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"+checkArithmeticBadOctal (TA_Base id "0" (TA_Literal _ str)) | '9' `elem` str || '8' `elem` str =+    err id 2080 $ "Numbers with leading 0 are considered octal."+checkArithmeticBadOctal _ = return ()++prop_checkComparisonAgainstGlob = verify checkComparisonAgainstGlob "[[ $cow == $bar ]]"+prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow == \"$bar\" ]]"+prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"+prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"+checkComparisonAgainstGlob (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _])) | op == "=" || op == "==" =+    warn id 2053 $ "Quote the rhs of = in [[ ]] to prevent glob interpretation."+checkComparisonAgainstGlob (TC_Binary _ SingleBracket op _ word)+        | (op == "=" || op == "==") && isGlob word =+    err (getId word) 2081 $ "[ .. ] can't match globs. Use [[ .. ]] or grep."+checkComparisonAgainstGlob _ = return ()++prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)"+prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)"+prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)"+checkCommarrays (T_Array id l) =+    if any ("," `isSuffixOf`) (concatMap deadSimple l) || (length $ filter (==',') (concat $ concatMap deadSimple l)) > 1+    then warn id 2054 "Use spaces, not commas, to separate array elements."+    else return ()+checkCommarrays _ = return ()++prop_checkOrNeq1 = verify checkOrNeq "if [[ $lol -ne cow || $lol -ne foo ]]; then echo foo; fi"+prop_checkOrNeq2 = verify checkOrNeq "(( a!=lol || a!=foo ))"+prop_checkOrNeq3 = verify checkOrNeq "[ \"$a\" != lol || \"$a\" != foo ]"+prop_checkOrNeq4 = verifyNot checkOrNeq "[ a != $cow || b != $foo ]"+-- This only catches the most idiomatic cases. Fixme?+checkOrNeq (TC_Or id typ op (TC_Binary _ _ op1 word1 _) (TC_Binary _ _ op2 word2 _))+    | word1 == word2 && (op1 == op2 && (op1 == "-ne" || op1 == "!=")) =+        warn id 2055 $ "You probably wanted " ++ (if typ == SingleBracket then "-a" else "&&") ++ " here."+checkOrNeq (TA_Binary id "||" (TA_Binary _ "!=" word1 _) (TA_Binary _ "!=" word2 _))+    | word1 == word2 =+        warn id 2056 "You probably wanted && here."+checkOrNeq _ = return ()+++allModifiedVariables t = snd $ runState (doAnalysis (\x -> modify $ (++) (getModifiedVariables x)) t) []++prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]"+prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]"+prop_checkValidCondOps2a= verifyNot checkValidCondOps "[ 3 \\> 2 ]"+prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]"+prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]"+checkValidCondOps (TC_Binary id _ s _ _)+    | not (s `elem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="]) =+        warn id 2057 "Unknown binary operator."+checkValidCondOps (TC_Unary id _ s _)+    | not (s `elem`  [ "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-o", "-v", "-R"]) =+        warn id 2058 "Unknown unary operator."+checkValidCondOps _ = return ()++--- Context seeking++getParentTree t =+    snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)+  where+    pre t = modify (\(l, m) -> (t:l, m))+    post t = do+        ((_:rest), map) <- get+        case rest of [] -> put (rest, map)+                     (x:_) -> put (rest, Map.insert (getId t) x map)++getTokenMap t =+    snd $ runState (doAnalysis f t) (Map.empty)+  where+    f t = modify (Map.insert (getId t) t)+++inUnquotableContext tree t =+    case t of+        TC_Noary _ DoubleBracket _ -> True+        TC_Unary _ DoubleBracket _ _ -> True+        TC_Binary _ DoubleBracket _ _ _ -> True+        TA_Unary _ _ _ -> True+        TA_Binary _ _ _ _ -> True+        TA_Trinary _ _ _ _ -> True+        TA_Expansion _ _ -> True+        T_Assignment _ _ _ _ _ -> True+        T_Redirecting _ _ _ ->+            any (isCommand t) ["local", "declare", "typeset", "export"]+        T_DoubleQuoted _ _ -> True+        T_CaseExpression _ _ _ -> True+        T_HereDoc _ _ _ _ _ -> True+        T_ForIn _ _ _ _ -> True -- Pragmatically assume it's desirable here+        x -> case Map.lookup (getId x) tree of+                Nothing -> False+                Just parent -> inUnquotableContext tree parent++isParamTo tree cmd t =+    go t+  where+    go x = case Map.lookup (getId x) tree of+                Nothing -> False+                Just parent -> check parent+    check t =+        case t of+            T_SingleQuoted _ _ -> go t+            T_DoubleQuoted _ _ -> go t+            T_NormalWord _ _ -> go t+            T_SimpleCommand _ _ _ -> isCommand t cmd+            T_Redirecting _ _ _ -> isCommand t cmd+            _ -> False++getClosestCommand tree t =+    msum . map getCommand $ getPath tree t+  where+    getCommand t@(T_Redirecting _ _ _) = return t+    getCommand _ = Nothing++usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)+  where+    go currentId ((T_NormalWord id [word]):rest)+        | currentId == (getId word) = go id rest+    go currentId ((T_DoubleQuoted id [word]):rest)+        | currentId == (getId word) = go id rest+    go currentId ((T_SimpleCommand _ _ (word:_)):_)+        | currentId == (getId word) = True+    go _ _ = False++-- A list of the element and all its parents+getPath tree t = t :+    case Map.lookup (getId t) tree of+        Nothing -> []+        Just parent -> getPath tree parent++--- Command specific checks++checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =+    if t `isCommand` str then f rest else return ()+checkCommand _ _ _ = return ()++checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =+    if t `isUnqualifiedCommand` str then f rest else return ()+checkUnqualifiedCommand _ _ _ = return ()++getLiteralString = getLiteralStringExt (const Nothing)++getGlobOrLiteralString = getLiteralStringExt f+  where+    f (T_Glob _ str) = return str+    f _ = Nothing++getLiteralStringExt more t = g t+  where+    allInList l = let foo = map g l in if all isJust foo then return $ concat (catMaybes foo) else Nothing+    g s@(T_DoubleQuoted _ l) = allInList l+    g s@(T_DollarDoubleQuoted _ l) = allInList l+    g s@(T_NormalWord _ l) = allInList l+    g (T_SingleQuoted _ s) = return s+    g (T_Literal _ s) = return s+    g x = more x++isLiteral t = isJust $ getLiteralString t++isCommand token str = isCommandMatch token (\cmd -> cmd  == str || ("/" ++ str) `isSuffixOf` cmd)+isUnqualifiedCommand token str = isCommandMatch token (\cmd -> cmd  == str)++isCommandMatch token matcher = fromMaybe False $ do+    cmd <- getCommandName token+    return $ matcher cmd++getCommandName (T_Redirecting _ _ w) =+    getCommandName w+getCommandName (T_SimpleCommand _ _ (w:_)) =+    getLiteralString w+getCommandName (T_Annotation _ _ t) = getCommandName t+getCommandName _ = Nothing++getCommandBasename = liftM basename . getCommandName+basename = reverse . (takeWhile (/= '/')) . reverse++isAssignment (T_Annotation _ _ w) = isAssignment w+isAssignment (T_Redirecting _ _ w) = isAssignment w+isAssignment (T_SimpleCommand _ (w:_) []) = True+isAssignment _ = False++prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""+prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"+prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"+prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"+checkPrintfVar = checkUnqualifiedCommand "printf" f where+    f (dashv:var:rest) | getLiteralString dashv == (Just "-v") = f rest+    f (format:params) = check format+    f _ = return ()+    check format =+        if '%' `elem` (concat $ deadSimple format) || isLiteral format+          then return ()+          else warn (getId format) 2059 $+                "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."++prop_checkUuoe1 = verify checkUuoe "echo $(date)"+prop_checkUuoe1a= verify checkUuoe "echo `date`"+prop_checkUuoe2 = verify checkUuoe "echo \"$(date)\""+prop_checkUuoe2a= verify checkUuoe "echo \"`date`\""+prop_checkUuoe3 = verifyNot checkUuoe "echo \"The time is $(date)\""+checkUuoe = checkUnqualifiedCommand "echo" f where+    msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."+    f [T_NormalWord id [(T_DollarExpansion _ _)]] = msg id+    f [T_NormalWord id [T_DoubleQuoted _ [(T_DollarExpansion _ _)]]] = msg id+    f [T_NormalWord id [(T_Backticked _ _)]] = msg id+    f [T_NormalWord id [T_DoubleQuoted _ [(T_Backticked _ _)]]] = msg id+    f _ = return ()++prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"+prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"+prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"+prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"+prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"+prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"+prop_checkTr4 = verifyNot checkTr "ls [a-z]"+prop_checkTr5 = verify checkTr "tr foo bar"+prop_checkTr6 = verify checkTr "tr 'hello' 'world'"+prop_checkTr8 = verifyNot checkTr "tr aeiou _____"+prop_checkTr9 = verifyNot checkTr "a-z n-za-m"+prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"+prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"+checkTr = checkCommand "tr" (mapM_ f)+  where+    f w | isGlob w = do -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?+        warn (getId w) 2060 $ "Quote parameters to tr to prevent glob expansion."+    f word = case getLiteralString word of+                Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."+                Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."++                Just s -> do  -- Eliminate false positives by only looking for dupes in SET2?+                            when ((not $ "-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $+                                info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."++                            unless ("[:" `isPrefixOf` s) $+                                when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && (not $ '*' `elem` s)) $+                                    info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."+                Nothing -> return ()++    duplicated s =+        let relevant = filter isAlpha s+        in not $ relevant == nub relevant+++prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"+prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"+prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"+checkFindNameGlob = checkCommand "find" f where+    acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]+    acceptsGlob _ = False+    f [] = return ()+    f [x] = return ()+    f (a:b:r) = do+        when (acceptsGlob (getLiteralString a) && isGlob b) $ do+            let (Just s) = getLiteralString a+            warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."+        f (b:r)+++prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"+prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"+prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"+prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"+prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v  --regex=moo *"+prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"+prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"+prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"+prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"++checkGrepRe = checkCommand "grep" f where+    -- --regex=*(extglob) doesn't work. Fixme?+    skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s+    skippable _ = False+    f [] = return ()+    f (x:r) | skippable (getLiteralString x) = f r+    f (re:_) = do+        when (isGlob re) $ do+            warn (getId re) 2062 $ "Quote the grep pattern so the shell won't interpret it."+        let string = concat $ deadSimple re+        if isConfusedGlobRegex string then+            warn (getId re) 2063 $ "Grep uses regex, but this looks like a glob."+          else+            if (isPotentiallyConfusedGlobRegex string)+            then info (getId re) 2022 "Note that c* does not mean \"c followed by anything\" in regex."+            else return ()+++prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"+prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"+prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"+prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"+checkTrapQuotes = checkCommand "trap" f where+    f (x:_) = checkTrap x+    f _ = return ()+    checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs+    checkTrap _ = return ()+    warning id = warn id 2064 $ "Use single quotes, otherwise this expands now rather than when signalled."+    checkExpansions (T_DollarExpansion id _) = warning id+    checkExpansions (T_Backticked id _) = warning id+    checkExpansions (T_DollarBraced id _) = warning id+    checkExpansions (T_DollarArithmetic id _) = warning id+    checkExpansions _ = return ()++prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"+prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"+prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"+checkTimeParameters = checkUnqualifiedCommand "time" f where+    f (x:_) = let s = concat $ deadSimple x in+                if "-" `isPrefixOf` s && s /= "-p" then+                    info (getId x) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."+                  else return ()+    f _ = return ()++prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"+prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"+prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo"+checkTestRedirects (T_Redirecting id redirs@(redir:_) cmd) | cmd `isCommand` "test" =+    warn (getId redir) 2065 $ "This is interpretted as a shell file redirection, not a comparison."+checkTestRedirects _ = return ()++prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"+prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"+prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"+prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"+prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"+prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"+prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"+checkSudoRedirect (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" =+    mapM_ warnAbout redirs+  where+    warnAbout (T_FdRedirect _ s (T_IoFile id op file))+        | (s == "" || s == "&") && (not $ special file) =+        case op of+            T_Less _ ->+              info (getId op) 2024 $+                "sudo doesn't affect redirects. Use sudo cat file | .."+            T_Greater _ ->+              warn (getId op) 2024 $+                "sudo doesn't affect redirects. Use ..| sudo tee file"+            T_DGREAT _ ->+              warn (getId op) 2024 $+                "sudo doesn't affect redirects. Use .. | sudo tee -a file"+            _ -> return ()+    warnAbout _ = return ()+    special file = (concat $ deadSimple file) == "/dev/null"+checkSudoRedirect _ = return ()++prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"+prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"+prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"+prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"+prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"+prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"+prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"+prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"+prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"+prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"+checkPS1Assignments (T_Assignment _ _ "PS1" _ word) = warnFor word+  where+    warnFor word =+        let contents = concat $ deadSimple word in+            when (containsUnescaped contents) $+                info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"+    containsUnescaped s =+        let unenclosed = subRegex enclosedRegex s "" in+           isJust $ matchRegex escapeRegex unenclosed+    enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager+    escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"+checkPS1Assignments _ = return ()++prop_checkBackticks1 = verify checkBackticks "echo `foo`"+prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"+checkBackticks (T_Backticked id _) =+    style id 2006 "Use $(..) instead of deprecated `..`"+checkBackticks _ = return ()++prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"+prop_checkIndirectExpansion2 = verifyNot checkIndirectExpansion "${foo//$n/lol}"+prop_checkIndirectExpansion3 = verify checkIndirectExpansion "${$#}"+prop_checkIndirectExpansion4 = verify checkIndirectExpansion "${var${n}_$((i%2))}"+prop_checkIndirectExpansion5 = verifyNot checkIndirectExpansion "${bar}"+checkIndirectExpansion (T_DollarBraced i (T_NormalWord _ contents)) =+    when (isIndirection contents) $+        err i 2082 "To expand via indirection, use name=\"foo$n\"; echo \"${!name}\"."+  where+    isIndirection vars =+        let list = catMaybes (map isIndirectionPart vars) in+            not (null list) && all id list+    isIndirectionPart t =+        case t of T_DollarExpansion _ _ ->  Just True+                  T_Backticked _ _ ->       Just True+                  T_DollarBraced _ _ ->     Just True+                  T_DollarArithmetic _ _ -> Just True+                  T_Literal _ s -> if all isVariableChar s+                                    then Nothing+                                    else Just False+                  _ -> Just False++checkIndirectExpansion _ = return ()++prop_checkInexplicablyUnquoted1 = verify checkInexplicablyUnquoted "echo 'var='value';'"+prop_checkInexplicablyUnquoted2 = verifyNot checkInexplicablyUnquoted "'foo'*"+prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --user-agent='something'"+checkInexplicablyUnquoted (T_NormalWord id tokens) = mapM_ check (tails tokens)+  where+    check ((T_SingleQuoted _ _):(T_Literal id str):_)+        | all isAlphaNum str =+        info id 2026 $ "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "++    check ((T_DoubleQuoted _ _):trapped:(T_DoubleQuoted _ _):_) =+        case trapped of+            T_DollarExpansion id _ -> warnAbout id+            T_DollarBraced id _ -> warnAbout id+            _ -> return ()++    check _ = return ()+    warnAbout id =+        info id 2027 $ "Surrounding quotes actually unquotes this (\"inside\"$outside\"inside\"). Did you forget your quote level?"+checkInexplicablyUnquoted _ = return ()++prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""+prop_checkTildeInQuotes2 = verify checkTildeInQuotes "foo > '~/dir'"+prop_checkTildeInQuotes4 = verifyNot checkTildeInQuotes "~/file"+prop_checkTildeInQuotes5 = verifyNot checkTildeInQuotes "echo '/~foo/cow'"+prop_checkTildeInQuotes6 = verifyNot checkTildeInQuotes "awk '$0 ~ /foo/'"+checkTildeInQuotes = check+  where+    verify id ('~':_) = warn id 2088 "Note that ~ does not expand in quotes."+    verify _ _ = return ()+    check (T_NormalWord _ ((T_SingleQuoted id str):_)) =+        verify id str+    check (T_NormalWord _ ((T_DoubleQuoted _ ((T_Literal id str):_)):_)) =+        verify id str+    check _ = return ()++prop_checkLonelyDotDash1 = verify checkLonelyDotDash "./ file"+prop_checkLonelyDotDash2 = verifyNot checkLonelyDotDash "./file"+checkLonelyDotDash t@(T_Redirecting id _ _)+    | isUnqualifiedCommand t "./" =+        err id 2083 "Don't add spaces after the slash in './file'."+checkLonelyDotDash _ = return ()+++prop_checkSpuriousExec1 = verify checkSpuriousExec "exec foo; true"+prop_checkSpuriousExec2 = verify checkSpuriousExec "if a; then exec b; exec c; fi"+prop_checkSpuriousExec3 = verifyNot checkSpuriousExec "echo cow; exec foo"+prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi"+prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"+prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"+prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"+checkSpuriousExec = doLists+  where+    doLists (T_Script _ _ cmds) = doList cmds+    doLists (T_BraceGroup _ cmds) = doList cmds+    doLists (T_WhileExpression _ _ cmds) = doList cmds+    doLists (T_UntilExpression _ _ cmds) = doList cmds+    doLists (T_ForIn _ _ _ cmds) = doList cmds+    doLists (T_IfExpression _ thens elses) = do+        mapM_ (\(_, l) -> doList l) thens+        doList elses+    doLists _ = return ()++    stripCleanup = reverse . dropWhile cleanup . reverse+    cleanup (T_Pipeline _ [cmd]) =+        isCommandMatch cmd (`elem` ["echo", "exit"])+    cleanup _ = False++    doList = doList' . stripCleanup+    doList' t@(current:following:_) = do+        commentIfExec current+        doList (tail t)+    doList' _ = return ()++    commentIfExec (T_Pipeline id list) =+      mapM_ commentIfExec $ take 1 list+    commentIfExec (T_Redirecting _ _ f@(+      T_SimpleCommand id _ (cmd:arg:_))) =+        when (f `isUnqualifiedCommand` "exec") $+          warn (id) 2093 $+            "Remove \"exec \" if script should continue after this command."+    commentIfExec _ = return ()+++prop_checkSpuriousExpansion1 = verify checkSpuriousExpansion "if $(true); then true; fi"+prop_checkSpuriousExpansion2 = verify checkSpuriousExpansion "while \"$(cmd)\"; do :; done"+prop_checkSpuriousExpansion3 = verifyNot checkSpuriousExpansion "$(cmd) --flag1 --flag2"+prop_checkSpuriousExpansion4 = verify checkSpuriousExpansion "$((i++))"+checkSpuriousExpansion (T_SimpleCommand _ _ [T_NormalWord _ [word]]) = check word+  where+    check word = case word of+        T_DollarExpansion id _ ->+            warn id 2091 "Remove surrounding $() to avoid executing output."+        T_Backticked id _ ->+            warn id 2092 "Remove backticks to avoid executing output."+        T_DollarArithmetic id _ ->+            err id 2084 "Remove '$' or use '_=$((expr))' to avoid executing output."+        T_DoubleQuoted id [subword] -> check subword+        _ -> return ()+checkSpuriousExpansion _ = return ()+++prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"+prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"+prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""+prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"+checkUnusedEchoEscapes = checkCommand "echo" f+  where+    isDashE = mkRegex "^-.*e"+    hasEscapes = mkRegex "\\\\[rnt]"+    f (arg:_) | (concat $ deadSimple arg) `matches` isDashE = return ()+    f args = mapM_ checkEscapes args++    checkEscapes (T_NormalWord _ args) =+        mapM_ checkEscapes args+    checkEscapes (T_DoubleQuoted id args) =+        mapM_ checkEscapes args+    checkEscapes (T_Literal id str) = examine id str+    checkEscapes (T_SingleQuoted id str) = examine id str+    checkEscapes _ = return ()++    examine id str =+        when (str `matches` hasEscapes) $+            info id 2028 "echo won't expand escape sequences. Consider printf."+++prop_checkDollarBrackets1 = verify checkDollarBrackets "echo $[1+2]"+prop_checkDollarBrackets2 = verifyNot checkDollarBrackets "echo $((1+2))"+checkDollarBrackets (T_DollarBracket id _) =+    style id 2007 "Use $((..)) instead of deprecated $[..]"+checkDollarBrackets _ = return ()++prop_checkSshHereDoc1 = verify checkSshHereDoc "ssh host << foo\necho $PATH\nfoo"+prop_checkSshHereDoc2 = verifyNot checkSshHereDoc "ssh host << 'foo'\necho $PATH\nfoo"+checkSshHereDoc (T_Redirecting _ redirs cmd)+        | cmd `isCommand` "ssh" =+    mapM_ checkHereDoc redirs+  where+    hasVariables = mkRegex "[`$]"+    checkHereDoc (T_FdRedirect _ _ (T_HereDoc id _ Unquoted token tokens))+        | not (all isConstant tokens) =+        warn id 2087 $ "Quote '" ++ token ++ "' to make here document expansions happen on the server side rather than on the client."+    checkHereDoc _ = return ()+checkSshHereDoc _ = return ()++-- This is hard to get right without properly parsing ssh args+prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""+prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""+prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""+checkSshCommandString = checkCommand "ssh" f+  where+    nonOptions args =+        filter (\x -> not $ "-" `isPrefixOf` (concat $ deadSimple x)) args+    f args =+        case nonOptions args of+            (hostport:r@(_:_)) -> checkArg $ last r+            _ -> return ()+    checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =+        case filter (not . isConstant) parts of+            [] -> return ()+            (x:_) -> info (getId x) 2029 $+                "Note that, unescaped, this expands on the client side."+    checkArg _ = return ()+++--- Subshell detection+prop_subshellAssignmentCheck = verifyFull     subshellAssignmentCheck "cat foo | while read bar; do a=$bar; done; echo \"$a\""+prop_subshellAssignmentCheck2 = verifyNotFull subshellAssignmentCheck "while read bar; do a=$bar; done < file; echo \"$a\""+prop_subshellAssignmentCheck3 = verifyFull    subshellAssignmentCheck "( A=foo; ); rm $A"+prop_subshellAssignmentCheck4 = verifyNotFull subshellAssignmentCheck "( A=foo; rm $A; )"+prop_subshellAssignmentCheck5 = verifyFull    subshellAssignmentCheck "cat foo | while read cow; do true; done; echo $cow;"+prop_subshellAssignmentCheck6 = verifyFull    subshellAssignmentCheck "( export lol=$(ls); ); echo $lol;"+prop_subshellAssignmentCheck6a= verifyFull    subshellAssignmentCheck "( typeset -a lol=a; ); echo $lol;"+prop_subshellAssignmentCheck7 = verifyFull    subshellAssignmentCheck "cmd | while read foo; do (( n++ )); done; echo \"$n lines\""+prop_subshellAssignmentCheck8 = verifyFull    subshellAssignmentCheck "n=3 & echo $((n++))"+prop_subshellAssignmentCheck9 = verifyFull    subshellAssignmentCheck "read n & n=foo$n"+prop_subshellAssignmentCheck10 = verifyFull    subshellAssignmentCheck "(( n <<= 3 )) & (( n |= 4 )) &"+prop_subshellAssignmentCheck11 = verifyFull subshellAssignmentCheck "cat /etc/passwd | while read line; do let n=n+1; done\necho $n"+prop_subshellAssignmentCheck12 = verifyFull subshellAssignmentCheck "cat /etc/passwd | while read line; do let ++n; done\necho $n"+subshellAssignmentCheck t =+    let flow = getVariableFlow t+        check = findSubshelled flow [("oops",[])] Map.empty+    in snd $ runState check []+++data Scope = SubshellScope String | NoneScope deriving (Show, Eq)+data StackData =+    StackScope Scope+    | StackScopeEnd+    -- (Base expression, specific position, var name, assigned values)+    | Assignment (Token, Token, String, DataSource)+    | Reference (Token, Token, String)+  deriving (Show, Eq)+data DataSource = DataFrom [Token] | DataExternal+  deriving (Show, Eq)++data VariableState = Dead Token String | Alive deriving (Show, Eq)++leadType t =+    case t of+        T_DollarExpansion _ _  -> SubshellScope "$(..) expansion"+        T_Backticked _ _  -> SubshellScope "`..` expansion"+        T_Backgrounded _ _  -> SubshellScope "backgrounding &"+        T_Subshell _ _  -> SubshellScope "(..) group"+        -- This considers the whole pipeline one subshell. Consider fixing.+        T_Pipeline _ (_:_:[])  -> SubshellScope "pipeline"+        _ -> NoneScope+++getModifiedVariables t =+    case t of+        T_SimpleCommand _ vars [] ->+            concatMap (\x -> case x of+                                T_Assignment id _ name _ w ->+                                    [(x, x, name, DataFrom [w])]+                                _ -> []+                      ) vars+        c@(T_SimpleCommand _ _ _) ->+            getModifiedVariableCommand c++        TA_Unary _ "++|" (TA_Variable id name) -> [(t, t, name, DataFrom [t])]+        TA_Unary _ "|++" (TA_Variable id name) -> [(t, t, name, DataFrom [t])]+        TA_Binary _ op (TA_Variable id name) rhs ->+            if any (==op) ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]+                then [(t, t, name, DataFrom [rhs])]+                else []++        --Points to 'for' rather than variable+        T_ForIn id str words _ -> [(t, t, str, DataFrom words)]+        T_SelectIn id str words _ -> [(t, t, str, DataFrom words)]+        _ -> []++-- Consider 'export' a reference, since it makes the var available+getReferencedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) =+    case x of+        "export" -> concatMap getReference rest+        _ -> []+  where+    getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]+    getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]+    getReference _ = []++getReferencedVariableCommand _ = []++getModifiedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) =+   filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $+    case x of+        "read" ->  concatMap getLiteral rest+        "let" -> concatMap letParamToLiteral rest++        "export" -> concatMap getModifierParam rest+        "declare" -> concatMap getModifierParam rest+        "typeset" -> concatMap getModifierParam rest+        "local" -> concatMap getModifierParam rest++        _ -> []+  where+    stripEquals s = let rest = dropWhile (/= '=') s in+        if rest == "" then "" else tail rest+    stripEqualsFrom (T_NormalWord id1 ((T_Literal id2 s):rs)) =+        (T_NormalWord id1 ((T_Literal id2 (stripEquals s)):rs))+    stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =+        (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]])+    stripEqualsFrom t = t++    getLiteral t@(T_NormalWord _ [T_Literal _ s]) =+        [(base, t, s, DataExternal)]+    getLiteral t@(T_NormalWord _ [T_SingleQuoted _ s]) =+        [(base, t, s, DataExternal)]+    getLiteral t@(T_NormalWord _ [T_DoubleQuoted _ [T_Literal id s]]) =+        [(base, t, s, DataExternal)]+    getLiteral x = []++    getModifierParam t@(T_Assignment _ _ name _ value) =+        [(base, t, name, DataFrom [value])]+    getModifierParam _ = []++    letParamToLiteral token =+          if var == ""+            then []+            else [(base, token, var, DataFrom [stripEqualsFrom token])]+        where var = takeWhile (isVariableChar) $ dropWhile (\x -> x `elem` "+-") $ concat $ deadSimple token+getModifiedVariableCommand _ = []++-- TODO:+getBracedReference s = takeWhile (\x -> not $ x `elem` ":[#%/^,") $ dropWhile (`elem` "#!") s++getReferencedVariables t =+    case t of+        T_DollarBraced id l -> map (\x -> (t, t, x)) $ [getBracedReference $ bracedString l]+        TA_Variable id str -> [(t, t, str)]+        T_Assignment id Append str _ _ -> [(t, t, str)]+        x -> getReferencedVariableCommand x++getVariableFlow t =+    let (_, stack) = runState (doStackAnalysis startScope endScope t) []+    in reverse stack+  where+    startScope t =+        let scopeType = leadType t+        in do+            when (scopeType /= NoneScope) $ modify ((StackScope scopeType):)+            if assignFirst t then setWritten t else return ()++    endScope t =+        let scopeType = leadType t+        in do+            setRead t+            if assignFirst t then return () else setWritten t+            when (scopeType /= NoneScope) $ modify ((StackScopeEnd):)++    assignFirst (T_ForIn _ _ _ _) = True+    assignFirst (T_SelectIn _ _ _ _) = True+    assignFirst _ = False++    setRead t =+        let read    = getReferencedVariables t+        in mapM_ (\v -> modify ((Reference v):)) read++    setWritten t =+        let written = getModifiedVariables t+        in mapM_ (\v -> modify ((Assignment v):)) written++findSubshelled [] _ _ = return ()+findSubshelled ((Assignment x@(_, _, str, _)):rest) ((reason,scope):lol) deadVars =+    findSubshelled rest ((reason, x:scope):lol) $ Map.insert str Alive deadVars+findSubshelled ((Reference (_, readToken, str)):rest) scopes deadVars = do+    case Map.findWithDefault Alive str deadVars of+        Alive -> return ()+        Dead writeToken reason -> do+                    info (getId writeToken) 2030 $ "Modification of " ++ str ++ " is local (to subshell caused by "++ reason ++")."+                    info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost."+    findSubshelled rest scopes deadVars++findSubshelled ((StackScope (SubshellScope reason)):rest) scopes deadVars =+    findSubshelled rest ((reason,[]):scopes) deadVars++findSubshelled ((StackScopeEnd):rest) ((reason, scope):oldScopes) deadVars =+    findSubshelled rest oldScopes $+        foldl (\m (_, token, var, _) ->+            Map.insert var (Dead token reason) m) deadVars scope++doVariableFlowAnalysis readFunc writeFunc empty t = fst $ runState (+    foldM (\list x -> do { l <- doFlow x;  return $ l ++ list; }) [] flow+    ) empty+  where+    flow = getVariableFlow t+    doFlow (Reference (base, token, name)) =+        readFunc base token name+    doFlow (Assignment (base, token, name, values)) =+        writeFunc base token name values+    doFlow _ = return []++---- Check whether variables could have spaces/globs+prop_checkSpacefulness0 = verifyFull checkSpacefulness "for f in *.mp3; do echo $f; done"+prop_checkSpacefulness1 = verifyFull checkSpacefulness "a='cow moo'; echo $a"+prop_checkSpacefulness2 = verifyNotFull checkSpacefulness "a='cow moo'; [[ $a ]]"+prop_checkSpacefulness3 = verifyNotFull checkSpacefulness "a='cow*.mp3'; echo \"$a\""+prop_checkSpacefulness4 = verifyFull checkSpacefulness "for f in *.mp3; do echo $f; done"+prop_checkSpacefulness4a= verifyNotFull checkSpacefulness "foo=3; foo=$(echo $foo)"+prop_checkSpacefulness5 = verifyFull checkSpacefulness "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"+prop_checkSpacefulness6 = verifyFull checkSpacefulness "a=foo$(lol); echo $a"+prop_checkSpacefulness7 = verifyFull checkSpacefulness "a=foo\\ bar; rm $a"+prop_checkSpacefulness8 = verifyNotFull checkSpacefulness "a=foo\\ bar; a=foo; rm $a"+prop_checkSpacefulnessA = verifyFull checkSpacefulness "rm $1"+prop_checkSpacefulnessB = verifyFull checkSpacefulness "rm ${10//foo/bar}"+prop_checkSpacefulnessC = verifyNotFull checkSpacefulness "(( $1 + 3 ))"+prop_checkSpacefulnessD = verifyNotFull checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"+prop_checkSpacefulnessE = verifyNotFull checkSpacefulness "foo=$3 env"+prop_checkSpacefulnessF = verifyNotFull checkSpacefulness "local foo=$1"+prop_checkSpacefulnessG = verifyNotFull checkSpacefulness "declare foo=$1"+prop_checkSpacefulnessH = verifyFull checkSpacefulness "echo foo=$1"+prop_checkSpacefulnessI = verifyNotFull checkSpacefulness "$1 --flags"+prop_checkSpacefulnessJ = verifyFull checkSpacefulness "echo $PWD"++checkSpacefulness t =+    doVariableFlowAnalysis readF writeF (Map.fromList defaults) t+  where+    defaults = zip variablesWithoutSpaces (repeat False)++    hasSpaces name = do+        map <- get+        return $ Map.findWithDefault True name map++    setSpaces name bool = do+        modify $ Map.insert name bool++    readF _ token name = do+        spaced <- hasSpaces name+        if spaced+          && (not $ "@" `isPrefixOf` name) -- There's another warning for this+          && (not $ isCounting token)+          && (not $ inUnquotableContext parents token)+          && (not $ usedAsCommandName parents token)+            then return [(getId token, Note InfoC 2086 warning)]+            else return []+      where+        warning = "Double quote to prevent globbing and word splitting."++    writeF _ _ name DataExternal = do+        setSpaces name True+        return []++    writeF _ _ name (DataFrom vals) = do+        map <- get+        setSpaces name+            (isSpacefulWord (\x -> Map.findWithDefault True x map) vals)+        return []++    parents = getParentTree t++    isCounting (T_DollarBraced id token) =+        case concat $ deadSimple token of+            '#':_ -> True+            _ -> False+    isCounting _ = False++    isSpacefulWord :: (String -> Bool) -> [Token] -> Bool+    isSpacefulWord f words = any (isSpaceful f) words+    isSpaceful :: (String -> Bool) -> Token -> Bool+    isSpaceful spacefulF x =+        case x of+          T_DollarExpansion _ _ -> True+          T_Backticked _ _ -> True+          T_Glob _ _         -> True+          T_Extglob _ _ _    -> True+          T_Literal _ s      -> s `containsAny` globspace+          T_SingleQuoted _ s -> s `containsAny` globspace+          T_DollarBraced _ l -> spacefulF $ getBracedReference $ bracedString l+          T_NormalWord _ w   -> isSpacefulWord spacefulF w+          T_DoubleQuoted _ w -> isSpacefulWord spacefulF w+          _ -> False+      where+        globspace = "*? \t\n"+        containsAny s chars = any (\c -> c `elem` s) chars+++prop_checkQuotesInLiterals1 = verifyFull checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"+prop_checkQuotesInLiterals1a= verifyFull checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"+prop_checkQuotesInLiterals2 = verifyNotFull checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""+prop_checkQuotesInLiterals3 =verifyNotFull checkQuotesInLiterals "param=('--foo='); app \"${param[@]}\""+prop_checkQuotesInLiterals4 = verifyNotFull checkQuotesInLiterals "param=\"don't bother with this one\"; app $param"+prop_checkQuotesInLiterals5 = verifyNotFull checkQuotesInLiterals "param=\"--foo='lolbar'\"; eval app $param"+checkQuotesInLiterals t =+    doVariableFlowAnalysis readF writeF Map.empty t+  where+    getQuotes name = get >>= (return . Map.lookup name)+    setQuotes name ref = modify $ Map.insert name ref+    deleteQuotes = modify . Map.delete+    parents = getParentTree t+    quoteRegex = mkRegex "\"|([= ]|^)'|'( |$)"+    containsQuotes s = isJust $ matchRegex quoteRegex s++    -- Just catch the most blatant cases of foo='--cow="lol bert"'; cmd $foo, since that's 99%+    writeF _ _ name (DataFrom values) = do+        let quotedVars = filter (\v -> containsQuotes (concat $ deadSimple v)) values+        case quotedVars of+            [] -> deleteQuotes name+            x:_ -> setQuotes name (getId x)+        return []+    writeF _ _ _ _ = return []++    readF _ expr name = do+        assignment <- getQuotes name+        if isJust assignment+            && not (isParamTo parents "eval" expr)+            && not (inUnquotableContext parents expr)+            then return [+                    (fromJust assignment,+                        Note WarningC 2089 "Word splitting will treat quotes as literals. Use an array."),+                    (getId expr,+                        Note WarningC 2090 "Embedded quotes in this variable will not be respected.")+                    ]+            else return []+++prop_checkFunctionsUsedExternally1 =+  verifyFull checkFunctionsUsedExternally "foo() { :; }; sudo foo"+prop_checkFunctionsUsedExternally2 =+  verifyFull checkFunctionsUsedExternally "alias f='a'; xargs -n 1 f"+prop_checkFunctionsUsedExternally3 =+  verifyNotFull checkFunctionsUsedExternally "f() { :; }; echo f"+checkFunctionsUsedExternally t =+    runBasicAnalysis checkCommand t+  where+    invokingCmds = [+        "chroot",+        "find",+        "screen",+        "ssh",+        "su",+        "sudo",+        "xargs"+        ]+    checkCommand t@(T_SimpleCommand _ _ (cmd:args)) =+        let name = fromMaybe "" $ getCommandBasename t in+          when (name `elem` invokingCmds) $+            mapM_ (checkArg name) args+    checkCommand _ = return ()++    functions = Map.fromList $ runBasicAnalysis findFunctions t+    findFunctions (T_Function id name _) = modify ((name, id):)+    findFunctions t@(T_SimpleCommand id _ (_:args))+        | t `isUnqualifiedCommand` "alias" = mapM_ getAlias args+    findFunctions _ = return ()+    getAlias arg =+        let string = concat $ deadSimple arg+        in when ('=' `elem` string) $+            modify ((takeWhile (/= '=') string, getId arg):)+    checkArg cmd arg =+        case Map.lookup (concat $ deadSimple arg) functions of+            Nothing -> return ()+            Just id -> do+              warn (getId arg) 2033 $+                "Shell functions can't be passed to external commands."+              info id 2032 $+                "Use own script or sh -c '..' to run this from " ++ cmd ++ "."++prop_checkUnused0 = verifyNotFull checkUnusedAssignments "var=foo; echo $var"+prop_checkUnused1 = verifyFull checkUnusedAssignments "var=foo; echo $bar"+prop_checkUnused2 = verifyNotFull checkUnusedAssignments "var=foo; export var;"+prop_checkUnused3 = verifyFull checkUnusedAssignments "for f in *; do echo '$f'; done"+prop_checkUnused4 = verifyFull checkUnusedAssignments "local i=0"+prop_checkUnused5 = verifyNotFull checkUnusedAssignments "read lol; echo $lol"+prop_checkUnused6 = verifyNotFull checkUnusedAssignments "var=4; (( var++ ))"+prop_checkUnused7 = verifyNotFull checkUnusedAssignments "var=2; $((var))"+prop_checkUnused8 = verifyFull checkUnusedAssignments "var=2; var=3;"+prop_checkUnused9 = verifyNotFull checkUnusedAssignments "read ''"+prop_checkUnused10= verifyNotFull checkUnusedAssignments "read -p 'test: '"+prop_checkUnused11= verifyNotFull checkUnusedAssignments "bar=5; export foo[$bar]=3"+prop_checkUnused12= verifyNotFull checkUnusedAssignments "read foo; echo ${!foo}"+checkUnusedAssignments t = snd $ runState (mapM_ checkAssignment flow) []+  where+    flow = getVariableFlow t+    references = foldl (flip ($)) defaultMap (map insertRef flow)+    insertRef (Reference (base, token, name)) =+        Map.insert name ()+    insertRef _ = id++    checkAssignment (Assignment (_, token, name, _)) | isVariableName name =+        case Map.lookup name references of+            Just _ -> return ()+            Nothing -> do+                info (getId token) 2034 $+                    name ++ " appears unused. Verify it or export it."+    checkAssignment _ = return ()++    defaultMap = Map.fromList $ zip internalVariables $ repeat ()++prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"+prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"+prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"+checkGlobsAsOptions (T_SimpleCommand _ _ args) =+    mapM_ check $ takeWhile (not . isEndOfArgs) args+  where+    check v@(T_NormalWord _ ((T_Glob id s):_)) | s == "*" || s == "?" =+        info id 2035 $+            "Use ./" ++ (concat $ deadSimple v)+                ++ " so names with dashes won't become options."+    check _ = return ()++    isEndOfArgs t =+        case concat $ deadSimple t of+            "--" -> True+            ":::" -> True+            "::::" -> True+            _ -> False++checkGlobsAsOptions _ = return ()+++prop_checkWhileReadPitfalls1 = verify checkWhileReadPitfalls "while read foo; do ssh $foo uptime; done < file"+prop_checkWhileReadPitfalls2 = verifyNot checkWhileReadPitfalls "while read -u 3 foo; do ssh $foo uptime; done 3< file"+prop_checkWhileReadPitfalls3 = verifyNot checkWhileReadPitfalls "while true; do ssh host uptime; done"+prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo; do ssh $foo hostname < /dev/null; done"+prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done"+prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"++checkWhileReadPitfalls (T_WhileExpression id [command] contents)+        | isStdinReadCommand command = do+    mapM_ checkMuncher contents+  where+    munchers = [ "ssh", "ffmpeg", "mplayer" ]++    isStdinReadCommand (T_Pipeline _ [T_Redirecting id redirs cmd]) =+        let plaintext = deadSimple cmd+        in head (plaintext ++ [""]) == "read"+            && (not $ "-u" `elem` plaintext)+            && all (not . stdinRedirect) redirs+    isStdinReadCommand _ = False++    checkMuncher (T_Pipeline _ ((T_Redirecting _ redirs cmd):_)) = do+        let name = fromMaybe "" $ getCommandBasename cmd+        when ((not . any stdinRedirect $ redirs) && (name `elem` munchers)) $ do+            info id 2095 $+                name ++ " may swallow stdin, preventing this loop from working properly."+            warn (getId cmd) 2095 $+                "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin."+    checkMuncher _ = return ()++    stdinRedirect (T_FdRedirect _ fd _)+        | fd == "" || fd == "0" = True+    stdinRedirect _ = False+checkWhileReadPitfalls _ = return ()+++prop_checkPrefixAssign1 = verifyTree checkPrefixAssignmentReference "var=foo echo $var"+prop_checkPrefixAssign2 = verifyNotTree checkPrefixAssignmentReference "var=$(echo $var) cmd"+checkPrefixAssignmentReference t@(T_DollarBraced id value) tree =+    check path+  where+    name = getBracedReference $ bracedString value+    path = getPath tree t+    idPath = map getId path++    check [] = return ()+    check (t:rest) =+        case t of+            T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars+            otherwise -> check rest+    checkVar (T_Assignment aId mode aName Nothing value) |+            aName == name && (not $ aId `elem` idPath) = do+        warn aId 2097 "This assignment is only seen by the forked process."+        warn id 2098 "This expansion will not see the mentioned assignment."+    checkVar _ = return ()++checkPrefixAssignmentReference _ _ = return ()++prop_checkCharRangeGlob1 = verify checkCharRangeGlob "ls *[:digit:].jpg"+prop_checkCharRangeGlob2 = verifyNot checkCharRangeGlob "ls *[[:digit:]].jpg"+prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]"+prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]"+checkCharRangeGlob (T_Glob id str) | isCharClass str =+    if ":" `isPrefixOf` contents+        && ":" `isSuffixOf` contents+        && contents /= ":"+    then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]."+    else+        if (not $ '[' `elem` contents) && hasDupes+        then info id 2102 "Ranges can only match single chars (mentioned due to duplicates)."+        else return ()+  where+    isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str+    contents = drop 1 . take ((length str) - 1) $ str+    hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents+checkCharRangeGlob _ = return ()++++prop_checkCdAndBack1 = verify (checkCdAndBack Sh) "for f in *; do cd $f; git pull; cd ..; done"+prop_checkCdAndBack2 = verifyNot (checkCdAndBack Sh) "for f in *; do cd $f || continue; git pull; cd ..; done"+prop_checkCdAndBack3 = verifyNot (checkCdAndBack Sh) "while [[ $PWD != / ]]; do cd ..; done"+checkCdAndBack shell = doLists+  where+    doLists (T_ForIn _ _ _ cmds) = doList cmds+    doLists (T_WhileExpression _ _ cmds) = doList cmds+    doLists (T_UntilExpression _ _ cmds) = doList cmds+    doLists (T_IfExpression _ thens elses) = do+        mapM_ (\(_, l) -> doList l) thens+        doList elses+    doLists _ = return ()++    isCdRevert t =+        case deadSimple t of+            ["cd", p] -> p `elem` ["..", "-"]+            _ -> False++    getCmd (T_Annotation id _ x) = getCmd x+    getCmd (T_Pipeline id [x]) = getCommandName x+    getCmd _ = Nothing++    doList list =+        let cds = filter ((== Just "cd") . getCmd) list in+            when (length cds >= 2 && isCdRevert (last cds)) $+               warn (getId $ head cds) 2103 message++    message =+        if shell == Bash || shell == Zsh+        then "Consider using ( subshell ), 'cd foo||exit', or pushd/popd instead."+        else "Consider using ( subshell ) or 'cd foo||exit' instead."
+ ShellCheck/Data.hs view
@@ -0,0 +1,74 @@+module ShellCheck.Data where++internalVariables = [+    -- Generic+    "", "_",++    -- Bash+    "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",+    "BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",+    "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",+    "BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",+    "COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",+    "COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",+    "HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",+    "OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",+    "RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",+    "SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",+    "COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",+    "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",+    "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",+    "IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",+    "LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",+    "OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",+    "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",+    "TMOUT", "TMPDIR", "auto_resume", "histchars",++    -- Zsh+    "ARGV0", "BAUD", "cdpath", "COLUMNS", "CORRECT_IGNORE",+    "DIRSTACKSIZE", "ENV", "FCEDIT", "fignore", "fpath", "histchars",+    "HISTCHARS", "HISTFILE", "HISTSIZE", "HOME", "IFS", "KEYBOARD_HACK",+    "KEYTIMEOUT", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",+    "LC_MESSAGES", "LC_NUMERIC", "LC_TIME", "LINES", "LISTMAX",+    "LOGCHECK", "MAIL", "MAILCHECK", "mailpath", "manpath", "module_path",+    "NULLCMD", "path", "POSTEDIT", "PROMPT", "PROMPT2", "PROMPT3",+    "PROMPT4", "prompt", "PROMPT_EOL_MARK", "PS1", "PS2", "PS3", "PS4",+    "psvar", "READNULLCMD", "REPORTTIME", "REPLY", "reply", "RPROMPT",+    "RPS1", "RPROMPT2", "RPS2", "SAVEHIST", "SPROMPT", "STTY", "TERM",+    "TERMINFO", "TIMEFMT", "TMOUT", "TMPPREFIX", "watch", "WATCHFMT",+    "WORDCHARS", "ZBEEP", "ZDOTDIR", "ZLE_LINE_ABORTED",+    "ZLE_REMOVE_SUFFIX_CHARS", "ZLE_SPACE_SUFFIX_CHARS"+  ]++variablesWithoutSpaces = [+    "$", "-", "?", "!", +    "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",+    "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",+    "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"+  ]++commonCommands = [ +    "admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",+    "bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",+    "chmod", "chown", "cksum", "cmp", "colon", "comm", "command",+    "compress", "continue", "cp", "crontab", "csplit", "ctags", "cut",+    "cxref", "date", "dd", "delta", "df", "diff", "dirname", "dot",+    "du", "echo", "ed", "env", "eval", "ex", "exec", "exit", "expand",+    "export", "expr", "fc", "fg", "file", "find", "fold", "fort77",+    "fuser", "gencat", "get", "getconf", "getopts", "grep", "hash",+    "head", "iconv", "ipcrm", "ipcs", "jobs", "join", "kill", "lex",+    "link", "ln", "locale", "localedef", "logger", "logname", "lp",+    "ls", "m4", "mailx", "make", "man", "mesg", "mkdir", "mkfifo",+    "more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste",+    "patch", "pathchk", "pax", "pr", "printf", "prs", "ps", "pwd",+    "qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls",+    "qselect", "qsig", "qstat", "qsub", "read", "readonly", "renice",+    "return", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "set",+    "sh", "shift", "sleep", "sort", "split", "strings", "strip", "stty",+    "tabs", "tail", "talk", "tee", "test", "time", "times", "touch",+    "tput", "tr", "trap", "tsort", "tty", "type", "ulimit", "umask",+    "unalias", "uname", "uncompress", "unexpand", "unget", "uniq",+    "unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux",+    "val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",+    "zcat" +  ]
+ ShellCheck/Parser.hs view
@@ -0,0 +1,1919 @@+{-+    This file is part of ShellCheck.+    http://www.vidarholen.net/contents/shellcheck++    ShellCheck 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.++    ShellCheck 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/>.+-}+{-# LANGUAGE NoMonomorphismRestriction #-}++module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), notesFromMap, Metadata(..), sortNotes) where++import ShellCheck.AST+import ShellCheck.Data+import Text.Parsec+import Debug.Trace+import Control.Monad+import Data.Char+import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)+import qualified Data.Map as Map+import qualified Control.Monad.State as Ms+import Data.Maybe+import Prelude hiding (readList)+import System.IO+import Text.Parsec.Error+import GHC.Exts (sortWith)++lastError = 1074++backslash = char '\\'+linefeed = (optional carriageReturn) >> char '\n'+singleQuote = char '\'' <|> unicodeSingleQuote+doubleQuote = char '"' <|> unicodeDoubleQuote+variableStart = upper <|> lower <|> oneOf "_"+variableChars = upper <|> lower <|> digit <|> oneOf "_"+functionChars = variableChars <|> oneOf ":+-.?"+specialVariable = oneOf "@*#?-$!"+tokenDelimiter = oneOf "&|;<> \t\n\r" <|> nbsp+quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars+quotable = nbsp <|> unicodeDoubleQuote <|> oneOf quotableChars+bracedQuotable = oneOf "}\"$`'"+doubleQuotableChars = "\"$`\x201C\x201D"+doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars+whitespace = oneOf " \t\n" <|> carriageReturn <|> nbsp+linewhitespace = oneOf " \t" <|> nbsp++extglobStartChars = "?*@!+"+extglobStart = oneOf extglobStartChars++prop_spacing = isOk spacing "  \\\n # Comment"+spacing = do+    x <- many (many1 linewhitespace <|> (try $ string "\\\n"))+    optional readComment+    return $ concat x++prop_allspacing = isOk allspacing "#foo"+prop_allspacing2 = isOk allspacing " #foo\n # bar\n#baz\n"+prop_allspacing3 = isOk allspacing "#foo\n#bar\n#baz\n"+allspacing = do+    s <- spacing+    more <- option False (linefeed >> return True)+    if more then do+        rest <- allspacing+        return $ s ++ "\n" ++ rest+      else+        return s++allspacingOrFail = do+    s <- allspacing+    when (null s) $ fail "Expected spaces"++unicodeDoubleQuote = do+    pos <- getPosition+    char '\x201C' <|> char '\x201D'+    parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it."+    return '"'++unicodeSingleQuote = do+    pos <- getPosition+    char '\x2018' <|> char '\x2019'+    parseProblemAt pos WarningC 1016 "This is a unicode single quote. Delete and retype it."+    return '"'++carriageReturn = do+    parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."+    char '\r'++nbsp = do+    parseNote ErrorC 1018 "This is a &nbsp;. Delete it and retype as space."+    char '\xA0'+    return ' '++--------- Message/position annotation on top of user state+data Note = Note Severity Code String deriving (Show, Eq)+data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq)+data Metadata = Metadata SourcePos [Note] deriving (Show)+data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)+data Context = ContextName SourcePos String | ContextAnnotation [Annotation]+type Code = Integer++codeForNote (Note _ code _) = code+codeForParseNote (ParseNote _ _ code _) = code++initialState = (Id $ -1, Map.empty, [])++getInitialMeta pos = Metadata pos []++getLastId = do+    (id, _, _) <- getState+    return id++getNextIdAt sourcepos = do+    (id, map, notes) <- getState+    let newId = incId id+    let newMap = Map.insert newId (getInitialMeta sourcepos) map+    putState (newId, newMap, notes)+    return newId+  where incId (Id n) = (Id $ n+1)++getNextId = do+    pos <- getPosition+    getNextIdAt pos++modifyMap f = do+    (id, map, parsenotes) <- getState+    putState (id, f map, parsenotes)++getMap = do+    (_, map, _) <- getState+    return map++getParseNotes = do+    (_, _, notes) <- getState+    return notes++addParseNote n = do+    irrelevant <- shouldIgnoreCode (codeForParseNote n)+    when (not irrelevant) $ do+        (a, b, notes) <- getState+        putState (a, b, n:notes)++shouldIgnoreCode code = do+    context <- getCurrentContexts+    return $ any disabling context+  where+    disabling (ContextAnnotation list) =+        any disabling' list+    disabling _ = False+    disabling' (DisableComment n) = code == n++-- Store potential parse problems outside of parsec+parseProblem level code msg = do+    pos <- getPosition+    parseProblemAt pos level code msg++setCurrentContexts c = do+    Ms.modify (\(list, _) -> (list, c))++getCurrentContexts = do+    (_, context) <- Ms.get+    return context++popContext = do+    v <- getCurrentContexts+    if not $ null v+        then do+            let (a:r) = v+            setCurrentContexts r+            return [a]+        else+            return []++pushContext c = do+    v <- getCurrentContexts+    setCurrentContexts (c:v)++parseProblemAt pos level code msg = do+    irrelevant <- shouldIgnoreCode code+    when (not irrelevant) $+        Ms.modify (\(list, current) -> ((ParseNote pos level code msg):list, current))++-- Store non-parse problems inside+addNoteFor id note = modifyMap $ Map.adjust (\(Metadata pos notes) -> Metadata pos (note:notes)) id++addNote note = do+    id <- getLastId+    addNoteFor id note++parseNote c l a = do+    pos <- getPosition+    parseNoteAt pos c l a++parseNoteAt pos c l a = addParseNote $ ParseNote pos c l a++--------- Convenient combinators+thenSkip main follow = do+    r <- main+    optional follow+    return r++unexpecting s p = try $ do+    (try p >> unexpected s) <|> return ()++notFollowedBy2 = unexpecting "keyword/token"++disregard x = x >> return ()++reluctantlyTill p end = do+    (lookAhead ((disregard $ try end) <|> eof) >> return []) <|> do+        x <- p+        more <- reluctantlyTill p end+        return $ x:more+      <|> return []++reluctantlyTill1 p end = do+    notFollowedBy2 end+    x <- p+    more <- reluctantlyTill p end+    return $ x:more++attempting rest branch = do+    ((try branch) >> rest) <|> rest++orFail parser stuff = do+    try (disregard parser) <|> (disregard stuff >> fail "nope")++wasIncluded p = option False (p >> return True)++acceptButWarn parser level code note = do+    optional $ try (do+        pos <- getPosition+        parser+        parseProblemAt pos level code note+      )++withContext entry p = do+    pushContext entry+    do+        v <- p+        popContext+        return v+     <|> do -- p failed without consuming input, abort context+        popContext+        fail $ ""++called s p = do+    pos <- getPosition+    withContext (ContextName pos s) p++withAnnotations anns p =+    withContext (ContextAnnotation anns) p++readConditionContents single = do+    readCondContents `attempting` (lookAhead $ do+                                pos <- getPosition+                                s <- many1 letter+                                when (s `elem` commonCommands) $+                                    parseProblemAt pos WarningC 1009 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")++  where+    typ = if single then SingleBracket else DoubleBracket+    readCondBinaryOp = try $ do+        id <- getNextId+        op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp+        hardCondSpacing+        return op+      where+        tryOp s = try $ do+            id <- getNextId+            string s+            return $ TC_Binary id typ s+        otherOp = try $ do+            id <- getNextId+            s <- readOp+            when (s == "-a" || s == "-o") $ fail "Wrong operator"+            return $ TC_Binary id typ s++    readCondUnaryExp = do+      op <- readCondUnaryOp+      pos <- getPosition+      (do+          arg <- readCondWord+          return $ op arg)+        <|> (do+              parseProblemAt pos ErrorC 1019 $ "Expected this to be an argument to the unary condition."+              fail "oops")++    readCondUnaryOp = try $ do+        id <- getNextId+        s <- readOp+        hardCondSpacing+        return $ TC_Unary id typ s++    readOp = try $ do+        char '-'+        s <- many1 letter+        return ('-':s)++    readCondWord = do+        notFollowedBy2 (try (spacing >> (string "]")))+        x <- readNormalWord+        pos <- getPosition+        when (endedWith "]" x) $ do+            parseProblemAt pos ErrorC 1020 $+                "You need a space before the " ++ (if single then "]" else "]]") ++ "."+        when (single && endedWith ")" x) $ do+            parseProblemAt pos ErrorC 1021 $+                "You need a space before the \\)"+        disregard spacing+        return x+      where endedWith str (T_NormalWord id s@(_:_)) =+                case (last s) of T_Literal id s -> str `isSuffixOf` s+                                 _ -> False+            endedWith _ _ = False++    readCondAndOp = do+        id <- getNextId+        x <- try (string "&&" <|> string "-a")+        when (single && x == "&&") $ addNoteFor id $ Note ErrorC 1022 "You can't use && inside [..]. Use [[..]] instead."+        when (not single && x == "-a") $ addNoteFor id $ Note ErrorC 1023 "In [[..]], use && instead of -a."+        softCondSpacing+        return $ TC_And id typ x++    readCondOrOp = do+        id <- getNextId+        x <- try (string "||" <|> string "-o")+        when (single && x == "||") $ addNoteFor id $ Note ErrorC 1024 "You can't use || inside [..]. Use [[..]] instead."+        when (not single && x == "-o") $ addNoteFor id $ Note ErrorC 1025 "In [[..]], use || instead of -o."+        softCondSpacing+        return $ TC_Or id typ x++    readCondNoaryOrBinary = do+      id <- getNextId+      x <- readCondWord `attempting` (do+              pos <- getPosition+              lookAhead (char '[')+              parseProblemAt pos ErrorC 1026 $ if single+                  then "If grouping expressions inside [..], use \\( ..\\)."+                  else "If grouping expressions inside [[..]], use ( .. )."+            )+      (do+            pos <- getPosition+            isRegex <- regexOperatorAhead+            op <- readCondBinaryOp+            y <- if isRegex+                    then readRegex+                    else  readCondWord <|> ( (parseProblemAt pos ErrorC 1027 $ "Expected another argument for this operator.") >> mzero)+            return (x `op` y)+          ) <|> (return $ TC_Noary id typ x)++    readCondGroup = do+          id <- getNextId+          pos <- getPosition+          lparen <- try $ string "(" <|> string "\\("+          when (single && lparen == "(") $ parseProblemAt pos ErrorC 1028 "In [..] you have to escape (). Use [[..]] instead."+          when (not single && lparen == "\\(") $ parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ()."+          if single then hardCondSpacing else disregard spacing+          x <- readCondContents+          cpos <- getPosition+          rparen <- string ")" <|> string "\\)"+          if single then hardCondSpacing else disregard spacing+          when (single && rparen == ")") $ parseProblemAt cpos ErrorC 1030 "In [..] you have to escape (). Use [[..]] instead."+          when (not single && rparen == "\\)") $ parseProblemAt cpos ErrorC 1031 "In [[..]] you shouldn't escape ()."+          when (isEscaped lparen `xor` isEscaped rparen) $ parseProblemAt pos ErrorC 1032 "Did you just escape one half of () but not the other?"+          return $ TC_Group id typ x+      where+        isEscaped ('\\':_) = True+        isEscaped _ = False+        xor x y = x && not y || not x && y++    -- Currently a bit of a hack since parsing rules are obscure+    regexOperatorAhead = (lookAhead $ do+        try (string "=~") <|> try (string "~=")+        return True)+          <|> return False+    readRegex = called "regex" $ do+        id <- getNextId+        parts <- many1 (+                readGroup <|>+                readSingleQuoted <|>+                readDoubleQuoted <|>+                readDollarExpression <|>+                readNormalLiteral "( " <|>+                readPipeLiteral <|>+                readGlobLiteral)+        disregard spacing+        return $ T_NormalWord id parts+      where+        readGlobLiteral = do+            id <- getNextId+            s <- many1 (extglobStart <|> oneOf "[]$")+            return $ T_Literal id s+        readGroup = called "regex grouping" $ do+            id <- getNextId+            char '('+            parts <- many (readGroup <|> readSingleQuoted <|> readDoubleQuoted <|> readDollarExpression <|> readRegexLiteral <|> readGlobLiteral)+            char ')'+            return $ T_NormalWord id parts+        readRegexLiteral = do+            id <- getNextId+            str <- readGenericLiteral1 (singleQuote <|> doubleQuotable <|> oneOf "()")+            return $ T_Literal id str+        readPipeLiteral = do+            id <- getNextId+            str <- string "|"+            return $ T_Literal id str++    readCondTerm = readCondNot <|> readCondExpr+    readCondNot = do+        id <- getNextId+        char '!'+        softCondSpacing+        expr <- readCondExpr+        return $ TC_Unary id typ "!" expr++    readCondExpr =+      readCondGroup <|> readCondUnaryExp <|> readCondNoaryOrBinary++    readCondOr = chainl1 readCondAnd readCondAndOp+    readCondAnd = chainl1 readCondTerm readCondOrOp+    readCondContents = readCondOr++++prop_a1 = isOk readArithmeticContents " n++ + ++c"+prop_a2 = isOk readArithmeticContents "$N*4-(3,2)"+prop_a3 = isOk readArithmeticContents "n|=2<<1"+prop_a4 = isOk readArithmeticContents "n &= 2 **3"+prop_a5 = isOk readArithmeticContents "1 |= 4 && n >>= 4"+prop_a6 = isOk readArithmeticContents " 1 | 2 ||3|4"+prop_a7 = isOk readArithmeticContents "3*2**10"+prop_a8 = isOk readArithmeticContents "3"+prop_a9 = isOk readArithmeticContents "a^!-b"+prop_aA = isOk readArithmeticContents "! $?"+prop_aB = isOk readArithmeticContents "10#08 * 16#f"+prop_aC = isOk readArithmeticContents "\"$((3+2))\" + '37'"+prop_aD = isOk readArithmeticContents "foo[9*y+x]++"+readArithmeticContents =+    readSequence+  where+    spacing = many whitespace++    splitBy x ops = chainl1 x (readBinary ops)+    readBinary ops = readComboOp ops TA_Binary+    readComboOp op token = do+        id <- getNextId+        op <- choice (map (\x -> try $ do+                                        s <- string x+                                        notFollowedBy2 $ oneOf "&|<>="+                                        return s+                            ) op)+        spacing+        return $ token id op++    readVar = do+        id <- getNextId+        x <- readVariableName+        y <- readArrayIndex <|> return ""+        optional spacing+        return $ TA_Variable id (x ++ y)++    -- Doesn't help with foo[foo]+    readArrayIndex = do+        char '['+        x <- many1 $ noneOf "]"+        char ']'+        return $ "[" ++ x ++ "]"++    readExpansion = do+        id <- getNextId+        x <- readNormalDollar+        spacing+        return $ TA_Expansion id x++    readGroup = do+        char '('+        s <- readSequence+        char ')'+        spacing+        return s++    readNumber = do+        id <- getNextId+        num <- many1 $ oneOf "0123456789."+        return $ TA_Literal id (num)++    readBased = getArbitrary <|> getHex <|> getOct+      where+        getThing prefix litchars = try $ do+            id <- getNextId+            x <- prefix+            t <- readExpansion <|> (do+                i <- getNextId+                stuff <- many1 litchars+                return $ TA_Literal i stuff)+            return $ TA_Base id x t++        getArbitrary = getThing arbitrary variableChars+        getHex = getThing hex hexDigit+        getOct = getThing oct digit++        arbitrary = try $ do+            b <- many1 digit+            s <- char '#'+            return (b ++ [s])+        hex = try $ do+            z <- char '0'+            x <- oneOf "xX"+            return (z:x:[])+        oct = string "0"++    readArithTerm = readBased <|> readArithTermUnit+    readArithTermUnit = readGroup <|> readExpansion <|> readQuoted <|> readNumber <|> readVar++    readQuoted = readDoubleQuoted <|> readSingleQuoted++    readSequence = do+        spacing+        id <- getNextId+        l <- readAssignment `sepBy` (char ',' >> spacing)+        return $ TA_Sequence id l++    readAssignment = readTrinary `splitBy` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]+    readTrinary = do+        let part = readLogicalOr+        x <- part+        do+            id <- getNextId+            string "?"+            spacing+            y <- part+            string ":"+            spacing+            z <- part+            return $ TA_Trinary id x y z+         <|>+          return x++    readLogicalOr  = readLogicalAnd `splitBy` ["||"]+    readLogicalAnd = readBitOr `splitBy` ["&&"]+    readBitOr  = readBitXor `splitBy` ["|"]+    readBitXor = readBitAnd `splitBy` ["^"]+    readBitAnd = readEquated `splitBy` ["&"]+    readEquated = readCompared `splitBy` ["==", "!="]+    readCompared = readShift `splitBy` ["<=", ">=", "<", ">"]+    readShift = readAddition `splitBy` ["<<", ">>"]+    readAddition = readMultiplication `splitBy` ["+", "-"]+    readMultiplication = readExponential `splitBy` ["*", "/", "%"]+    readExponential = readAnyNegated `splitBy` ["**"]++    readAnyNegated = readNegated <|> readAnySigned+    readNegated = do+        id <- getNextId+        op <- oneOf "!~"+        spacing+        x <- readAnySigned+        return $ TA_Unary id [op] x++    readAnySigned = readSigned <|> readAnycremented+    readSigned = do+        id <- getNextId+        op <- choice (map readSignOp "+-")+        spacing+        x <- readAnycremented+        return $ TA_Unary id [op] x+     where+        readSignOp c = try $ do+            char c+            notFollowedBy2 $ char c+            spacing+            return c++    readAnycremented = readNormalOrPostfixIncremented <|> readPrefixIncremented+    readPrefixIncremented = do+        id <- getNextId+        op <- try $ string "++" <|> string "--"+        spacing+        x <- readArithTerm+        return $ TA_Unary id (op ++ "|") x++    readNormalOrPostfixIncremented = do+        x <- readArithTerm+        spacing+        do+            id <- getNextId+            op <- try $ string "++" <|> string "--"+            spacing+            return $ TA_Unary id ("|" ++ op) x+         <|>+            return x++++prop_readCondition = isOk readCondition "[ \\( a = b \\) -a \\( c = d \\) ]"+prop_readCondition2 = isOk readCondition "[[ (a = b) || (c = d) ]]"+prop_readCondition3 = isOk readCondition "[[ $c = [[:alpha:].~-] ]]"+prop_readCondition4 = isOk readCondition "[[ $c =~ *foo* ]]"+prop_readCondition5 = isOk readCondition "[[ $c =~ f( ]] )* ]]"+prop_readCondition5a= isOk readCondition "[[ $c =~ a(b) ]]"+prop_readCondition5b= isOk readCondition "[[ $c =~ f( ($var ]]) )* ]]"+prop_readCondition6 = isOk readCondition "[[ $c =~ ^[yY]$ ]]"+prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"+prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"+prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"+readCondition = called "test expression" $ do+    opos <- getPosition+    id <- getNextId+    open <- (try $ string "[[") <|> (string "[")+    let single = open == "["+    condSpacingMsg False $ if single+          then "You need spaces after the opening [ and before the closing ]."+          else "You need spaces after the opening [[ and before the closing ]]."+    condition <- readConditionContents single++    cpos <- getPosition+    close <- (try $ string "]]") <|> (string "]")+    when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"+    when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"+    spacing+    many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?+    return $ T_Condition id (if single then SingleBracket else DoubleBracket) condition+++hardCondSpacing = condSpacingMsg False "You need a space here."+softCondSpacing = condSpacingMsg True "You need a space here."+condSpacingMsg soft msg = do+  pos <- getPosition+  space <- spacing+  when (null space) $ (if soft then parseNoteAt else parseProblemAt) pos ErrorC 1035 msg++readAnnotationPrefix = do+    char '#'+    many linewhitespace+    string "shellcheck"++prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n"+prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n"+readAnnotation = called "shellcheck annotation" $ do+    try readAnnotationPrefix+    many1 linewhitespace+    values <- many1 (readDisable)+    linefeed+    many linewhitespace+    return $ concat values+  where+    readDisable = forKey "disable" $ do+        readCode `sepBy` char ','+      where+        readCode = do+            optional $ string "SC"+            int <- many1 digit+            return $ DisableComment (read int)+    forKey s p = do+        try $ string s+        char '='+        value <- p+        many linewhitespace+        return value++readAnnotations = do+    annotations <- many (readAnnotation `thenSkip` allspacing)+    return $ concat annotations++readComment = do+    unexpecting "shellcheck annotation" readAnnotationPrefix+    char '#'+    many $ noneOf "\r\n"++prop_readNormalWord = isOk readNormalWord "'foo'\"bar\"{1..3}baz$(lol)"+prop_readNormalWord2 = isOk readNormalWord "foo**(foo)!!!(@@(bar))"+prop_readNormalWord3 = isOk readNormalWord "foo#"+prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'"+readNormalWord = readNormalishWord ""++readNormalishWord end = do+    id <- getNextId+    pos <- getPosition+    x <- many1 (readNormalWordPart end)+    checkPossibleTermination pos x+    return $ T_NormalWord id x++checkPossibleTermination pos [T_Literal _ x] =+    if x `elem` ["do", "done", "then", "fi", "esac", "}"]+        then parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."+        else return ()+checkPossibleTermination _ _ = return ()++readNormalWordPart end = do+    checkForParenthesis+    readSingleQuoted <|> readDoubleQuoted <|> readGlob <|> readNormalDollar <|> readBraced <|> readBackTicked <|> readProcSub <|> (readNormalLiteral end)+  where+    checkForParenthesis = do+        return () `attempting` do+            pos <- getPosition+            lookAhead $ char '('+            parseProblemAt pos ErrorC 1036 "'(' is invalid here. Did you forget to escape it?"+++readSpacePart = do+    id <- getNextId+    x <- many1 whitespace+    return $ T_Literal id x++readDollarBracedWord = do+    id <- getNextId+    list <- many readDollarBracedPart+    return $ T_NormalWord id list++readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readBackTicked <|> readDollarBracedLiteral++readDollarBracedLiteral = do+    id <- getNextId+    vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable+    return $ T_Literal id $ concat vars++prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)"+prop_readProcSub2 = isOk readProcSub "<(  if true; then true; fi )"+readProcSub = called "process substitution" $ do+    id <- getNextId+    dir <- try $ do+                    x <- oneOf "<>"+                    char '('+                    return [x]+    allspacing+    list <- readCompoundList+    allspacing+    char ')'+    return $ T_ProcSub id dir list++prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'"+prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'"+prop_readsingleQuoted3 = isWarning readSingleQuoted "\x2018hello\x2019"+readSingleQuoted = called "single quoted string" $ do+    id <- getNextId+    singleQuote+    s <- readSingleQuotedPart `reluctantlyTill` singleQuote+    pos <- getPosition+    singleQuote <?> "end of single quoted string"++    let string = concat s+    return (T_SingleQuoted id string) `attempting` do+        x <- lookAhead anyChar+        when (isAlpha x && not (null string) && isAlpha (last string)) $ parseProblemAt pos WarningC 1011 "This apostrophe terminated the single quoted string!"++readSingleQuotedLiteral = do+    singleQuote+    strs <- many1 readSingleQuotedPart+    singleQuote+    return $ concat strs++readSingleQuotedPart =+    readSingleEscaped+    <|> (many1 $ noneOf "'\\\x2018\x2019")++prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"+prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"+readBackTicked = called "backtick expansion" $ do+    id <- getNextId+    pos <- getPosition+    char '`'+    subStart <- getPosition+    subString <- readGenericLiteral "`"+    char '`'+    -- Result positions may be off due to escapes+    result <- subParse subStart readCompoundList (unEscape subString)+    return $ T_Backticked id result+  where+    unEscape [] = []+    unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest+    unEscape ('\\':'\n':rest) = unEscape rest+    unEscape (c:rest) = c : unEscape rest++subParse pos parser input = do+    lastPosition <- getPosition+    lastInput <- getInput+    setPosition pos+    setInput input+    result <- parser+    setInput lastInput+    setPosition lastPosition+    return result++prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\""+prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\""+prop_readDoubleQuoted3 = isWarning readDoubleQuoted "\x201Chello\x201D"+readDoubleQuoted = called "double quoted string" $ do+    id <- getNextId+    doubleQuote+    x <- many doubleQuotedPart+    doubleQuote <?> "end of double quoted string"+    return $ T_DoubleQuoted id x++doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked++readDoubleQuotedLiteral = do+    doubleQuote+    x <- readDoubleLiteral+    doubleQuote+    return x++readDoubleLiteral = do+    id <- getNextId+    s <- many1 readDoubleLiteralPart+    return $ T_Literal id (concat s)++readDoubleLiteralPart = do+    x <- many1 $ (readDoubleEscaped <|> (many1 $ noneOf ('\\':doubleQuotableChars)))+    return $ concat x++readNormalLiteral end = do+    id <- getNextId+    s <- many1 (readNormalLiteralPart end)+    return $ T_Literal id (concat s)++prop_readGlob1 = isOk readGlob "*"+prop_readGlob2 = isOk readGlob "[^0-9]"+prop_readGlob3 = isOk readGlob "[a[:alpha:]]"+prop_readGlob4 = isOk readGlob "[[:alnum:]]"+prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"+readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral+    where+        readSimple = do+            id <- getNextId+            c <- oneOf "*?"+            return $ T_Glob id [c]+        -- Doesn't handle weird things like [^]a] and [$foo]. fixme?+        readClass = try $ do+            id <- getNextId+            char '['+            s <- many1 (predefined <|> (liftM return $ letter <|> digit <|> oneOf globchars))+            char ']'+            return $ T_Glob id $ "[" ++ (concat s) ++ "]"+          where+           globchars = "^-_:?*.,!~@#$%=+{}/~"+           predefined = do+              try $ string "[:"+              s <- many1 letter+              string ":]"+              return $ "[:" ++ s ++ ":]"++        readGlobbyLiteral = do+            id <- getNextId+            c <- extglobStart <|> char '['+            return $ T_Literal id [c]++readNormalLiteralPart end = do+    readNormalEscaped <|> (many1 $ noneOf (end ++ quotableChars ++ extglobStartChars ++ "["))++readNormalEscaped = called "escaped char" $ do+    pos <- getPosition+    backslash+    do+        next <- (quotable <|> oneOf "?*@!+[]{}.,")+        return $ if next == '\n' then "" else [next]+      <|>+        do+            next <- anyChar+            case escapedChar next of+                Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use \"$(printf \"\\" ++ [next] ++ "\")\"."+                Nothing -> parseNoteAt pos InfoC 1001 $ "This \\" ++ [next] ++ " will be a regular '" ++ [next] ++ "' in this context."+            return [next]+  where+    escapedChar 'n' = Just "line feed"+    escapedChar 't' = Just "tab"+    escapedChar 'r' = Just "carriage return"+    escapedChar _ = Nothing+++prop_readExtglob1 = isOk readExtglob "!(*.mp3)"+prop_readExtglob2 = isOk readExtglob "!(*.mp3|*.wmv)"+prop_readExtglob4 = isOk readExtglob "+(foo \\) bar)"+prop_readExtglob5 = isOk readExtglob "+(!(foo *(bar)))"+prop_readExtglob6 = isOk readExtglob "*(((||))|())"+prop_readExtglob7 = isOk readExtglob "*(<>)"+prop_readExtglob8 = isOk readExtglob "@(|*())"+readExtglob = called "extglob" $ do+    id <- getNextId+    c <- try $ do+            f <- extglobStart+            char '('+            return f+    contents <- readExtglobPart `sepBy` (char '|')+    char ')'+    return $ T_Extglob id [c] contents++readExtglobPart = do+    id <- getNextId+    x <- many (readExtglobGroup <|> readNormalWordPart "" <|> readSpacePart <|> readExtglobLiteral)+    return $ T_NormalWord id x+  where+    readExtglobGroup = do+        id <- getNextId+        char '('+        contents <- readExtglobPart `sepBy` (char '|')+        char ')'+        return $ T_Extglob id "" contents+    readExtglobLiteral = do+        id <- getNextId+        str <- many1 (oneOf "<>#;&")+        return $ T_Literal id str+++readSingleEscaped = do+    s <- backslash+    let attempt level code p msg = do { try $ parseNote level code msg; x <- p; return [s,x]; }++    do {+        x <- lookAhead singleQuote;+        parseProblem InfoC 1003 "Are you trying to escape that single quote? echo 'You'\\''re doing it wrong'.";+        return [s];+    }+        <|> attempt InfoC 1004 linefeed "You don't break lines with \\ in single quotes, it results in literal backslash-linefeed."+        <|> do+            x <- anyChar+            return [s,x]+++readDoubleEscaped = do+    bs <- backslash+    (linefeed >> return "")+        <|> (doubleQuotable >>= return . return)+        <|> (anyChar >>= (return . \x -> [bs, x]))++readBraceEscaped = do+    bs <- backslash+    (linefeed >> return "")+        <|> (bracedQuotable >>= return . return)+        <|> (anyChar >>= (return . \x -> [bs, x]))+++readGenericLiteral endChars = do+    strings <- many (readGenericEscaped <|> (many1 $ noneOf ('\\':endChars)))+    return $ concat strings++readGenericLiteral1 endExp = do+    strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp+    return $ concat strings++readGenericEscaped = do+    backslash+    x <- anyChar+    return $ if x == '\n' then [] else ['\\', x]++prop_readBraced = isOk readBraced "{1..4}"+prop_readBraced2 = isOk readBraced "{foo,bar,\"baz lol\"}"+readBraced = try $ do+    let strip (T_Literal _ s) = return ("\"" ++ s ++ "\"")+    id <- getNextId+    char '{'+    str <- many1 ((readDoubleQuotedLiteral >>= (strip)) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))+    char '}'+    return $ T_BraceExpansion id $ concat str++readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely+readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely+readDollarExpression = readDollarArithmetic <|> readDollarBracket <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable++prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"+readDollarSingleQuote = called "$'..' expression" $ do+    id <- getNextId+    try $ string "$'"+    str <- readGenericLiteral "'"+    char '\''+    return $ T_DollarSingleQuoted id str++prop_readDollarDoubleQuote = isOk readDollarDoubleQuote "$\"hello\""+readDollarDoubleQuote = do+    lookAhead . try $ string "$\""+    id <- getNextId+    char '$'+    doubleQuote+    x <- many doubleQuotedPart+    doubleQuote <?> "end of translated double quoted string"+    return $ T_DollarDoubleQuoted id x+++prop_readDollarArithmetic = isOk readDollarArithmetic "$(( 3 * 4 +5))"+prop_readDollarArithmetic2 = isOk readDollarArithmetic "$(((3*4)+(1*2+(3-1))))"+readDollarArithmetic = called "$((..)) expression" $ do+    id <- getNextId+    try (string "$((")+    c <- readArithmeticContents+    string "))"+    return (T_DollarArithmetic id c)++readDollarBracket = called "$[..] expression" $ do+    id <- getNextId+    try (string "$[")+    c <- readArithmeticContents+    string "]"+    return (T_DollarBracket id c)++readArithmeticExpression = called "((..)) command" $ do+    id <- getNextId+    try (string "((")+    c <- readArithmeticContents+    string "))"+    return (T_Arithmetic id c)++prop_readDollarBraced1 = isOk readDollarBraced "${foo//bar/baz}"+prop_readDollarBraced2 = isOk readDollarBraced "${foo/'{cow}'}"+prop_readDollarBraced3 = isOk readDollarBraced "${foo%%$(echo cow})}"+prop_readDollarBraced4 = isOk readDollarBraced "${foo#\\}}"+readDollarBraced = called "parameter expansion" $ do+    id <- getNextId+    try (string "${")+    word <- readDollarBracedWord+    char '}'+    return $ T_DollarBraced id word++prop_readDollarExpansion = isOk readDollarExpansion "$(echo foo; ls\n)"+readDollarExpansion = called "command expansion" $ do+    id <- getNextId+    try (string "$(")+    cmds <- readCompoundList+    char ')' <?> "end of $(..) expression"+    return $ (T_DollarExpansion id cmds)++prop_readDollarVariable = isOk readDollarVariable "$@"+readDollarVariable = do+    id <- getNextId+    let singleCharred p = do+        n <- p+        value <- wrap [n]+        return (T_DollarBraced id value) `attempting` do+            pos <- getPosition+            num <- lookAhead $ many1 p+            parseNoteAt pos ErrorC 1037 $ "$" ++ (n:num) ++ " is equivalent to ${" ++ [n] ++ "}"++ num ++"."++    let positional = singleCharred digit+    let special = singleCharred specialVariable++    let regular = do+        name <- readVariableName+        value <- wrap name+        return $ T_DollarBraced id value++    try $ char '$' >> (positional <|> special <|> regular)++  where+    wrap s = do+        x <- getNextId+        y <- getNextId+        return $ T_NormalWord x [T_Literal y s]++readVariableName = do+    f <- variableStart+    rest <- many variableChars+    return (f:rest)++readDollarLonely = do+    id <- getNextId+    pos <- getPosition+    char '$'+    n <- lookAhead (anyChar <|> (eof >> return '_'))+    when (n /= '\'') $ parseNoteAt pos StyleC 1000 "$ is not used specially and should therefore be escaped."+    return $ T_Literal id "$"++prop_readHereDoc = isOk readHereDoc "<< foo\nlol\ncow\nfoo"+prop_readHereDoc2 = isWarning readHereDoc "<<- EOF\n  cow\n  EOF"+prop_readHereDoc3 = isOk readHereDoc "<< foo\n$\"\nfoo"+prop_readHereDoc4 = isOk readHereDoc "<< foo\n`\nfoo"+prop_readHereDoc5 = isOk readHereDoc "<<- !foo\nbar\n!foo"+prop_readHereDoc6 = isOk readHereDoc "<< foo\\ bar\ncow\nfoo bar"+readHereDoc = called "here document" $ do+    fid <- getNextId+    pos <- getPosition+    try $ string "<<"+    dashed <- (char '-' >> return Dashed) <|> return Undashed+    tokenPosition <- getPosition+    sp <- spacing+    optional $ do+        try . lookAhead $ char '('+        let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'."+        parseProblemAt pos ErrorC 1038 message+    hid <- getNextId+    (quoted, endToken) <-+            (readDoubleQuotedLiteral >>= return . (\x -> (Quoted, stripLiteral x)))+            <|> (readSingleQuotedLiteral >>= return . (\x -> (Quoted, x)))+            <|> (readToken >>= (\x -> return (Unquoted, x)))+    spacing++    startPos <- getPosition+    hereData <- anyChar `reluctantlyTill` do+                    linefeed+                    spacing+                    string endToken+                    disregard linefeed  <|> eof++    do+        linefeed+        spaces <- spacing+        verifyHereDoc dashed quoted spaces hereData+        string endToken+        parsedData <- parseHereData quoted startPos hereData+        return $ T_FdRedirect fid "" $ T_HereDoc hid dashed quoted endToken parsedData+     `attempting` (eof >> debugHereDoc tokenPosition endToken hereData)++  where+    stripLiteral (T_Literal _ x) = x+    stripLiteral (T_SingleQuoted _ x) = x++    readToken = do+        liftM concat $ many1 (escaped <|> quoted <|> normal)+      where+        quoted = liftM stripLiteral readDoubleQuotedLiteral <|> readSingleQuotedLiteral+        normal = anyChar `reluctantlyTill1` (whitespace <|> oneOf ";&)'\"\\")+        escaped = do -- surely the user must be doing something wrong at this point+            char '\\'+            c <- anyChar+            return [c]++    parseHereData Quoted startPos hereData = do+        id <- getNextIdAt startPos+        return $ [T_Literal id hereData]++    parseHereData Unquoted startPos hereData = do+        subParse startPos readHereData hereData++    readHereData = many $ try readNormalDollar <|> try readBackTicked <|> readHereLiteral++    readHereLiteral = do+        id <- getNextId+        chars <- many1 $ noneOf "`$"+        return $ T_Literal id chars++    verifyHereDoc dashed quoted spacing hereInfo = do+        when (dashed == Undashed && spacing /= "") $+            parseNote ErrorC 1039 "Use <<- instead of << if you want to indent the end token."+        when (dashed == Dashed && filter (/= '\t') spacing /= "" ) $+            parseNote ErrorC 1040 "When using <<-, you can only indent with tabs."+        return ()++    debugHereDoc pos endToken doc =+        if endToken `isInfixOf` doc+            then+                  let lookAt line = when (endToken `isInfixOf` line) $+                            parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")+                  in do+                        parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")+                        mapM_ lookAt (lines doc)+            else if (map toLower endToken) `isInfixOf` (map toLower doc)+                then parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")+                else parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")+++readFilename = readNormalWord+readIoFileOp = choice [g_LESSAND, g_GREATAND, g_DGREAT, g_LESSGREAT, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ]++prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\""+readIoFile = called "redirection" $ do+    id <- getNextId+    op <- readIoFileOp+    spacing+    file <- readFilename+    return $ T_FdRedirect id "" $ T_IoFile id op file++readIoNumber = try $ do+    x <- many1 digit <|> string "&"+    lookAhead readIoFileOp+    return x++prop_readIoNumberRedirect = isOk readIoNumberRedirect "3>&2"+prop_readIoNumberRedirect2 = isOk readIoNumberRedirect "2> lol"+prop_readIoNumberRedirect3 = isOk readIoNumberRedirect "4>&-"+prop_readIoNumberRedirect4 = isOk readIoNumberRedirect "&> lol"+readIoNumberRedirect = do+    id <- getNextId+    n <- readIoNumber+    op <- readHereString <|> readHereDoc <|> readIoFile+    let actualOp = case op of T_FdRedirect _ "" x -> x+    spacing+    return $ T_FdRedirect id n actualOp++readIoRedirect = choice [ readIoNumberRedirect, readHereString, readHereDoc, readIoFile ] `thenSkip` spacing++readRedirectList = many1 readIoRedirect++prop_readHereString = isOk readHereString "<<< \"Hello $world\""+readHereString = called "here string" $ do+    id <- getNextId+    try $ string "<<<"+    spacing+    id2 <- getNextId+    word <- readNormalWord+    return $ T_FdRedirect id "" $ T_HereString id2 word++readNewlineList = many1 ((newline <|> carriageReturn) `thenSkip` spacing)+readLineBreak = optional readNewlineList++prop_roflol = isWarning readScript "a &; b"+prop_roflol2 = isOk readScript "a & b"+readSeparatorOp = do+    notFollowedBy2 (g_AND_IF <|> g_DSEMI)+    notFollowedBy2 (string "&>")+    f <- (try $ do+                    char '&'+                    spacing+                    pos <- getPosition+                    char ';'+                    parseProblemAt pos ErrorC 1045 "It's not 'foo &; bar', just 'foo & bar'."+                    return '&'+            ) <|> char ';' <|> char '&'+    spacing+    return f++readSequentialSep = (disregard $ g_Semi >> readLineBreak) <|> (disregard readNewlineList)+readSeparator =+    do+        separator <- readSeparatorOp+        readLineBreak+        return separator+     <|>+        do+            readNewlineList+            return '\n'++makeSimpleCommand id1 id2 prefix cmd suffix =+    let+        (preAssigned, preRest) = partition assignment prefix+        (preRedirected, preRest2) = partition redirection preRest+        (postRedirected, postRest) = partition redirection suffix++        redirs = preRedirected ++ postRedirected+        assigns = preAssigned+        args = cmd ++ preRest2 ++ postRest+    in+        T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args+  where+    assignment (T_Assignment _ _ _ _ _) = True+    assignment _ = False+    redirection (T_FdRedirect _ _ _) = True+    redirection _ = False+++prop_readSimpleCommand = isOk readSimpleCommand "echo test > file"+prop_readSimpleCommand2 = isOk readSimpleCommand "cmd &> file"+prop_readSimpleCommand3 = isOk readSimpleCommand "export foo=(bar baz)"+prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)"+prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"+prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"+readSimpleCommand = called "simple command" $ do+    id1 <- getNextId+    id2 <- getNextId+    prefix <- option [] readCmdPrefix+    cmd <- option Nothing $ do { f <- readCmdName; return $ Just f; }+    when (null prefix && isNothing cmd) $ fail "No command"+    case cmd of+      Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []+      Just cmd -> do+            suffix <- option [] $+                        if isModifierCommand cmd+                        then readModifierSuffix+                        else if isTimeCommand cmd+                             then readTimeSuffix+                             else readCmdSuffix+            return $ makeSimpleCommand id1 id2 prefix [cmd] suffix+  where+    isModifierCommand (T_NormalWord _ [T_Literal _ s]) =+        s `elem` ["declare", "export", "local", "readonly", "typeset"]+    isModifierCommand _ = False+    -- Might not belong in T_SimpleCommand. Fixme?+    isTimeCommand (T_NormalWord _ [T_Literal _ "time"]) = True+    isTimeCommand _ = False++prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"+prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"+prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"+readPipeline = do+    unexpecting "keyword/token" readKeyword+    do+        (T_Bang id) <- g_Bang+        pipe <- readPipeSequence+        return $ T_Banged id pipe+      <|> do+        readPipeSequence++prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"+prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"+prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo"+readAndOr = do+    aid <- getNextId+    annotations <- readAnnotations++    andOr <- withAnnotations annotations $ do+        chainr1 readPipeline $ do+            op <- g_AND_IF <|> g_OR_IF+            readLineBreak+            return $ case op of T_AND_IF id -> T_AndIf id+                                T_OR_IF  id -> T_OrIf id++    return $ if null annotations+                then andOr+                else T_Annotation aid annotations andOr++readTerm = do+    allspacing+    m <- readAndOr+    readTerm' m++readTerm' current =+    do+        id <- getNextId+        sep <- readSeparator+        more <- (option (T_EOF id) readAndOr)+        case more of (T_EOF _) -> return [transformWithSeparator id sep current]+                     _         -> do+                                list <- readTerm' more+                                return $ (transformWithSeparator id sep current : list)+      <|>+        return [current]++transformWithSeparator i '&' = T_Backgrounded i+transformWithSeparator i _  = id+++readPipeSequence = do+    id <- getNextId+    list <- readCommand `sepBy1` (readPipe `thenSkip` (spacing >> readLineBreak))+    spacing+    return $ T_Pipeline id list++readPipe = do+    notFollowedBy2 g_OR_IF+    char '|' `thenSkip` spacing++readCommand = (readCompoundCommand <|> readSimpleCommand)++readCmdName = do+    f <- readNormalWord+    spacing+    return f++readCmdWord = do+    f <- readNormalWord+    spacing+    return f++prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi"+prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"+prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"+readIfClause = called "if expression" $ do+    id <- getNextId+    pos <- getPosition+    (condition, action) <- readIfPart+    elifs <- many readElifPart+    elses <- option [] readElsePart++    g_Fi `orFail` do+        parseProblemAt pos ErrorC 1046 "Couldn't find 'fi' for this 'if'."+        parseProblem ErrorC 1047 "Expected 'fi' matching previously mentioned 'if'."++    return $ T_IfExpression id ((condition, action):elifs) elses+++verifyNotEmptyIf s =+    optional (do+                emptyPos <- getPosition+                try . lookAhead $ (g_Fi <|> g_Elif <|> g_Else)+                parseProblemAt emptyPos ErrorC 1048 $ "Can't have empty " ++ s ++ " clauses (use 'true' as a no-op).")+readIfPart = do+    pos <- getPosition+    g_If+    allspacing+    condition <- readTerm++    optional (do+        try . lookAhead $ g_Fi+        parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'if'?")++    called "then clause" $ do+        g_Then `orFail` parseProblem ErrorC 1050 "Expected 'then'."++        acceptButWarn g_Semi ErrorC 1051 "No semicolons directly after 'then'."+        allspacing+        verifyNotEmptyIf "then"++        action <- readTerm+        return (condition, action)++readElifPart = called "elif clause" $ do+    pos <- getPosition+    g_Elif+    allspacing+    condition <- readTerm+    g_Then+    acceptButWarn g_Semi ErrorC 1052 "No semicolons directly after 'then'."+    allspacing+    verifyNotEmptyIf "then"+    action <- readTerm+    return (condition, action)++readElsePart = called "else clause" $ do+    g_Else+    acceptButWarn g_Semi ErrorC 1053 "No semicolons directly after 'else'."+    allspacing+    verifyNotEmptyIf "else"+    readTerm++prop_readSubshell = isOk readSubshell "( cd /foo; tar cf stuff.tar * )"+readSubshell = called "explicit subshell" $ do+    id <- getNextId+    char '('+    allspacing+    list <- readCompoundList+    allspacing+    char ')'+    return $ T_Subshell id list++prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"+prop_readBraceGroup2 = isWarning readBraceGroup "{foo;}"+readBraceGroup = called "brace group" $ do+    id <- getNextId+    char '{'+    allspacingOrFail <|> parseProblem ErrorC 1054 "You need a space after the '{'."+    optional $ do+        pos <- getPosition+        lookAhead $ char '}'+        parseProblemAt pos ErrorC 1055 "You need at least one command here. Use 'true;' as a no-op."+    list <- readTerm+    char '}' <|> do+        parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."+        fail "Unable to parse"+    return $ T_BraceGroup id list++prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"+readWhileClause = called "while loop" $ do+    pos <- getPosition+    (T_While id) <- g_While+    condition <- readTerm+    statements <- readDoGroup pos+    return $ T_WhileExpression id condition statements++prop_readUntilClause = isOk readUntilClause "until kill -0 $PID; do sleep 1; done"+readUntilClause = called "until loop" $ do+    pos <- getPosition+    (T_Until id) <- g_Until+    condition <- readTerm+    statements <- readDoGroup pos+    return $ T_UntilExpression id condition statements++readDoGroup loopPos = do+    pos <- getPosition+    optional (do+                try . lookAhead $ g_Done+                parseProblemAt loopPos ErrorC 1057 "Did you forget the 'do' for this loop?")++    g_Do `orFail` parseProblem ErrorC 1058 "Expected 'do'."++    acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."+    allspacing++    optional (do+                try . lookAhead $ g_Done+                parseProblemAt loopPos ErrorC 1060 "Can't have empty do clauses (use 'true' as a no-op).")++    commands <- readCompoundList+    g_Done `orFail` do+            parseProblemAt pos ErrorC 1061 "Couldn't find 'done' for this 'do'."+            parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."+    return commands+++prop_readForClause = isOk readForClause "for f in *; do rm \"$f\"; done"+prop_readForClause3 = isOk readForClause "for f; do foo; done"+prop_readForClause4 = isOk readForClause "for((i=0; i<10; i++)); do echo $i; done"+prop_readForClause5 = isOk readForClause "for ((i=0;i<10 && n>x;i++,--n))\ndo \necho $i\ndone"+prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone"+prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"+prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone"+prop_readForClause9 = isOk readForClause "for i do true; done"+readForClause = called "for loop" $ do+    pos <- getPosition+    (T_For id) <- g_For+    spacing+    typ <- (readRegular <|> readArithmetic)+    group <- readDoGroup pos+    typ id group+  where+    readArithmetic = called "arithmetic for condition" $ do+        try $ string "(("+        x <- readArithmeticContents+        char ';' >> spacing+        y <- readArithmeticContents+        char ';' >> spacing+        z <- readArithmeticContents+        spacing+        string "))"+        spacing+        optional $ readSequentialSep >> spacing+        return $ \id group -> (return $ T_ForArithmetic id x y z group)++    readRegular = do+        name <- readVariableName+        spacing+        values <- readInClause <|> (optional readSequentialSep >> return [])+        return $ \id group -> (return $ T_ForIn id name values group)++prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done"+prop_readSelectClause2 = isOk readSelectClause "select foo; do echo $foo; done"+readSelectClause = called "select loop" $ do+    pos <- getPosition+    (T_Select id) <- g_Select+    spacing+    typ <- readRegular+    group <- readDoGroup pos+    typ id group+  where+    readRegular = do+        name <- readVariableName+        spacing+        values <- readInClause <|> (readSequentialSep >> return [])+        return $ \id group -> (return $ T_SelectIn id name values group)++readInClause = do+    g_In+    things <- (readCmdWord) `reluctantlyTill`+                (disregard (g_Semi) <|> disregard linefeed <|> disregard g_Do)++    do {+        lookAhead (g_Do);+        parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'.";+    } <|> do {+        optional $ g_Semi;+        disregard allspacing;+    }++    return things++prop_readCaseClause = isOk readCaseClause "case foo in a ) lol; cow;; b|d) fooo; esac"+prop_readCaseClause2 = isOk readCaseClause "case foo\n in * ) echo bar;; esac"+readCaseClause = called "case expression" $ do+    id <- getNextId+    g_Case+    word <- readNormalWord+    allspacing+    g_In+    readLineBreak+    list <- readCaseList+    g_Esac+    return $ T_CaseExpression id word list++readCaseList = many readCaseItem++readCaseItem = called "case item" $ do+    notFollowedBy2 g_Esac+    optional g_Lparen+    spacing+    pattern <- readPattern+    g_Rparen+    readLineBreak+    list <- ((lookAhead g_DSEMI >> return []) <|> readCompoundList)+    (g_DSEMI <|> lookAhead (readLineBreak >> g_Esac)) `attempting` do+        pos <- getPosition+        lookAhead g_Rparen+        parseProblemAt pos ErrorC 1074+            "Did you forget the ;; after the previous case item?"+    readLineBreak+    return (pattern, list)++prop_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }"+prop_readFunctionDefinition1 = isOk readFunctionDefinition "foo   (){ command foo --lol \"$@\"; }"+prop_readFunctionDefinition2 = isWarning readFunctionDefinition "function foo() { command foo --lol \"$@\"; }"+prop_readFunctionDefinition3 = isWarning readFunctionDefinition "function foo { lol; }"+prop_readFunctionDefinition4 = isWarning readFunctionDefinition "foo(a, b) { true; }"+prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}"+prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"+prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"+prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"+readFunctionDefinition = called "function" $ do+    id <- getNextId+    name <- try readFunctionSignature+    allspacing+    (disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.")+    group <- readBraceGroup <|> readSubshell+    return $ T_Function id name group+++readFunctionSignature = do+    readWithFunction <|> readWithoutFunction+  where+    readWithFunction = do+        pos <- getPosition+        try $ do+            string "function"+            whitespace+        parseProblemAt pos InfoC 1005 "Drop the keyword 'function'. It's optional in Bash but invalid in other shells."+        spacing+        name <- readFunctionName+        optional spacing+        pos <- getPosition+        readParens <|> do+            parseProblemAt pos InfoC 1006 "Include '()' after the function name (in addition to dropping 'function')."+        return name++    readWithoutFunction = try $ do+        name <- readFunctionName+        optional spacing+        readParens+        return name++    readParens = do+        g_Lparen+        optional spacing+        g_Rparen <|> do+            parseProblem ErrorC 1065 "Trying to declare parameters? Don't. Use () and refer to params as $1, $2.."+            many $ noneOf "\n){"+            g_Rparen+        return ()++    readFunctionName = many1 functionChars++++readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)+++prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"+readCompoundCommand = do+    id <- getNextId+    cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition]+    optional spacing+    redirs <- many readIoRedirect+    when (not . null $ redirs) $ optional $ do+        lookAhead $ try (spacing >> needsSeparator)+        parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."+    return $ T_Redirecting id redirs $ cmd+  where+    needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]+++readCompoundList = readTerm++readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord)+readCmdSuffix = many1 (readIoRedirect <|> readCmdWord)+readModifierSuffix = many1 (readIoRedirect <|> readAssignmentWord <|> readCmdWord)+readTimeSuffix = do+    flags <- many readFlag+    pipeline <- readPipeline+    return $ flags ++ [pipeline]+  where+    -- This fails for quoted variables and such. Fixme?+    readFlag = do+        lookAhead $ char '-'+        readCmdWord++prop_readAssignmentWord = isOk readAssignmentWord "a=42"+prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"+prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"+prop_readAssignmentWord4 = isWarning readAssignmentWord "b = $(lol)"+prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"+prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"+prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"+prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"+prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "+prop_readAssignmentWord0 = isWarning readAssignmentWord "foo$n=42"+readAssignmentWord = try $ do+    id <- getNextId+    pos <- getPosition+    optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")+    variable <- readVariableName+    optional (readNormalDollar >> parseNoteAt pos ErrorC+                                1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")+    index <- optionMaybe readArrayIndex+    space <- spacing+    pos <- getPosition+    op <- readAssignmentOp+    space2 <- spacing+    if space == "" && space2 /= ""+      then do+        when (variable /= "IFS") $+            parseNoteAt pos InfoC 1007 $ "Note that 'var= value' (with space after equals sign) is similar to 'var=\"\"; value'."+        value <- readEmptyLiteral+        return $ T_Assignment id op variable index value+      else do+        when (space /= "" || space2 /= "") $+            parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments."+        value <- readArray <|> readNormalWord+        spacing+        return $ T_Assignment id op variable index value+  where+    readAssignmentOp =+        (string "+=" >> return Append) <|> (string "=" >> return Assign)+    readEmptyLiteral = do+        id <- getNextId+        return $ T_Literal id ""++-- This is only approximate. Fixme?+-- * Bash allows foo[' ' "" $(true) 2 ``]=var+-- * foo[bar] dereferences bar+readArrayIndex = do+    char '['+    optional space+    x <- readNormalishWord "]"+    optional space+    char ']'+    return x++readArray = called "array assignment" $ do+    id <- getNextId+    char '('+    allspacing+    words <- (readNormalWord `thenSkip` allspacing) `reluctantlyTill` (char ')')+    char ')'+    return $ T_Array id words++tryToken s t = try $ do+    id <- getNextId+    string s+    spacing+    return $ t id++redirToken c t = try $ do+    id <- getNextId+    char c+    notFollowedBy2 $ char '('+    return $ t id++tryWordToken s t = tryParseWordToken (string s) t `thenSkip` spacing+tryParseWordToken parser t = try $ do+    id <- getNextId+    parser+    optional (do+        try . lookAhead $ char '['+        parseProblem ErrorC 1069 "You need a space before the [.")+    try $ lookAhead (keywordSeparator)+    return $ t id++g_AND_IF = tryToken "&&" T_AND_IF+g_OR_IF = tryToken "||" T_OR_IF+g_DSEMI = tryToken ";;" T_DSEMI+g_DLESS = tryToken "<<" T_DLESS+g_DGREAT = tryToken ">>" T_DGREAT+g_LESSAND = tryToken "<&" T_LESSAND+g_GREATAND = tryToken ">&" T_GREATAND+g_LESSGREAT = tryToken "<>" T_LESSGREAT+g_DLESSDASH = tryToken "<<-" T_DLESSDASH+g_CLOBBER = tryToken ">|" T_CLOBBER+g_OPERATOR = g_AND_IF <|> g_OR_IF <|> g_DSEMI <|> g_DLESSDASH <|> g_DLESS <|> g_DGREAT <|> g_LESSAND <|> g_GREATAND <|> g_LESSGREAT++g_If = tryWordToken "if" T_If+g_Then = tryWordToken "then" T_Then+g_Else = tryWordToken "else" T_Else+g_Elif = tryWordToken "elif" T_Elif+g_Fi = tryWordToken "fi" T_Fi+g_Do = tryWordToken "do" T_Do+g_Done = tryWordToken "done" T_Done+g_Case = tryWordToken "case" T_Case+g_Esac = tryWordToken "esac" T_Esac+g_While = tryWordToken "while" T_While+g_Until = tryWordToken "until" T_Until+g_For = tryWordToken "for" T_For+g_Select = tryWordToken "select" T_Select+g_In = tryWordToken "in" T_In+g_Lbrace = tryWordToken "{" T_Lbrace+g_Rbrace = tryWordToken "}" T_Rbrace++g_Lparen = tryToken "(" T_Lparen+g_Rparen = tryToken ")" T_Rparen+g_Bang = do+    id <- getNextId+    char '!'+    softCondSpacing+    return $ T_Bang id+++g_Semi = do+    notFollowedBy2 g_DSEMI+    tryToken ";" T_Semi++keywordSeparator =+    eof <|> disregard whitespace <|> (disregard $ oneOf ";()[<>&|")++readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ]++ifParse p t f = do+    (lookAhead (try p) >> t) <|> f++readShebang = do+    try $ string "#!"+    str <- many $ noneOf "\r\n"+    optional carriageReturn+    optional linefeed+    return str++prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n"+prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"+prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"+prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("+readScript = do+    id <- getNextId+    pos <- getPosition+    sb <- option "" readShebang+    verifyShell pos (getShell sb)+    if (isValidShell $ getShell sb) /= Just False+      then+        do {+            allspacing;+            commands <- readTerm;+            eof <|> (parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors.");+            return $ T_Script id sb commands;+        } <|> do {+            parseProblem WarningC 1014 "Couldn't read any commands.";+            return $ T_Script id sb $ [T_EOF id];+        }+      else do+        many anyChar+        return $ T_Script id sb $ [T_EOF id];++  where+    basename s = reverse . takeWhile (/= '/') . reverse $ s+    getShell sb =+        case words sb of+            [] -> ""+            [x] -> basename x+            (first:second:_) ->+                if basename first == "env"+                    then second+                    else basename first++    verifyShell pos s =+        case isValidShell s of+            Just True -> return ()+            Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports Bourne based shell scripts, sorry!"+            Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles Bourne based shells."++    isValidShell s =+        let good = s == "" || any (`isPrefixOf` s) goodShells+            bad = any (`isPrefixOf` s) badShells+        in+            if good+                then Just True+                else if bad+                        then Just False+                        else Nothing++    goodShells = [+        "sh",+        "bash",+        "ksh",+        "zsh"+        ]+    badShells = [+        "awk",+        "csh",+        "perl",+        "python",+        "ruby",+        "tcsh"+        ]++rp p filename contents = Ms.runState (runParserT p initialState filename contents) ([], [])++isWarning p s = (fst cs) && (not . null . snd $ cs) where cs = checkString p s+isOk p s = (fst cs) && (null . snd $ cs) where cs = checkString p s++checkString parser string =+    case rp (parser >> eof >> getState) "-" string of+        (Right (tree, map, notes), (problems, _)) -> (True, (notesFromMap map) ++ notes ++ problems)+        (Left _, (n, _)) -> (False, n)++parseWithNotes parser = do+    item <- parser+    map <- getMap+    parseNotes <- getParseNotes+    return (item, map, nub . sortNotes $ parseNotes)++toParseNotes (Metadata pos list) = map (\(Note level code note) -> ParseNote pos level code note) list+notesFromMap map = Map.fold (\x -> (++) (toParseNotes x)) [] map++getAllNotes result = (concatMap (notesFromMap . snd) (maybeToList . parseResult $ result)) ++ (parseNotes result)++compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)+sortNotes = sortBy compareNotes+++data ParseResult = ParseResult { parseResult :: Maybe (Token, Map.Map Id Metadata), parseNotes :: [ParseNote] } deriving (Show)++makeErrorFor parsecError =+    ParseNote (errorPos parsecError) ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError++getStringFromParsec errors =+        case map snd $ sortWith fst $ map f errors of+            r -> (intercalate " " $ take 1 $ nub r)  ++ " Fix any mentioned problems and try again."+    where f err =+            case err of+                UnExpect s    -> (1, unexpected s)+                SysUnExpect s -> (2, unexpected s)+                Expect s      -> (3, "Expected " ++ s ++ ".")+                Message s     -> (4, s ++ ".")+          wut "" = "eof"+          wut x = x+          unexpected s = "Unexpected " ++ (wut s) ++ "."++parseShell filename contents = do+    case rp (parseWithNotes readScript) filename contents of+        (Right (script, map, notes), (parsenotes, _)) -> ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes)+        (Left err, (p, context)) -> ParseResult Nothing (nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err]))++  where+    isName (ContextName _ _) = True+    isName _ = False+    notesForContext list = zipWith ($) [first, second] $ filter isName list+    first (ContextName pos str) = ParseNote pos ErrorC 1073 $+        "Couldn't parse this " ++ str ++ "."+    second (ContextName pos str) = ParseNote pos InfoC 1009 $+        "The mentioned parser error was in this " ++ str ++ "."++lt x = trace (show x) x+ltt t x = trace (show t) x
+ ShellCheck/Simple.hs view
@@ -0,0 +1,67 @@+{-+    This file is part of ShellCheck.+    http://www.vidarholen.net/contents/shellcheck++    ShellCheck 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.++    ShellCheck 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/>.+-}+module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage) where++import ShellCheck.Parser+import ShellCheck.Analytics+import Data.Maybe+import Text.Parsec.Pos+import Data.List+++prop_findsParseIssue =+    let comments = shellCheck "echo \"$12\"" in+        (length comments) == 1 && (scCode $ head comments) == 1037+prop_commentDisablesParseIssue1 =+    null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\""+prop_commentDisablesParseIssue2 =+    null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\""++prop_findsAnalysisIssue =+    let comments = shellCheck "echo $1" in+        (length comments) == 1 && (scCode $ head comments) == 2086+prop_commentDisablesAnalysisIssue1 =+    null $ shellCheck "#shellcheck disable=SC2086\necho $1"+prop_commentDisablesAnalysisIssue2 =+    null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1"++shellCheck :: String -> [ShellCheckComment]+shellCheck script =+    let (ParseResult result notes) = parseShell "-" script in+        let allNotes = notes ++ (concat $ maybeToList $ do+            (tree, map) <- result+            let newMap = runAllAnalytics tree map+            return $ notesFromMap $ filterByAnnotation tree newMap+            )+        in+            map formatNote $ nub $ sortNotes allNotes++data ShellCheckComment = ShellCheckComment { scLine :: Int, scColumn :: Int, scSeverity :: String, scCode :: Int, scMessage :: String }++instance Show ShellCheckComment where+    show c = concat ["(", show $ scLine c, ",", show $ scColumn c, ") ", scSeverity c, ": ", show (scCode c), " ", scMessage c]++severityToString s =+    case s of+        ErrorC -> "error"+        WarningC -> "warning"+        InfoC -> "info"+        StyleC -> "style"++formatNote (ParseNote pos severity code text) =+    ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
+ shellcheck.hs view
@@ -0,0 +1,270 @@+{-+    This file is part of ShellCheck.+    http://www.vidarholen.net/contents/shellcheck++    ShellCheck 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.++    ShellCheck 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 Control.Exception+import Control.Monad+import Data.Char+import GHC.Exts+import GHC.IO.Device+import Prelude hiding (catch)+import ShellCheck.Simple+import System.Console.GetOpt+import System.Directory+import System.Environment+import System.Exit+import System.IO+import Text.JSON+import qualified Data.Map as Map++data Flag = Flag String String++header = "Usage: shellcheck [OPTIONS...] FILES..."+options = [+    Option ['f'] ["format"]+        (ReqArg (Flag "format") "FORMAT") "output format",+    Option ['e'] ["exclude"]+        (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings"+    ]++printErr = hPutStrLn stderr++syntaxFailure = ExitFailure 3+supportFailure = ExitFailure 4++instance JSON ShellCheckComment where+  showJSON c = makeObj [+      ("line", showJSON $ scLine c),+      ("column", showJSON $ scColumn c),+      ("level", showJSON $ scSeverity c),+      ("code", showJSON $ scCode c),+      ("message", showJSON $ scMessage c)+      ]+  readJSON = undefined++parseArguments argv =+    case getOpt Permute options argv of+        (opts, files, []) ->+            if not $ null files+              then+                return $ Just (opts, files)+              else do+                printErr "No files specified.\n"+                printErr $ usageInfo header options+                exitWith syntaxFailure++        (_, _, errors) -> do+            printErr $ (concat errors) ++ "\n" ++ usageInfo header options+            exitWith syntaxFailure++formats = Map.fromList [+    ("json", forJson),+    ("gcc", forGcc),+    ("checkstyle", forCheckstyle),+    ("tty", forTty)+    ]++forTty options files = do+    output <- mapM doFile files+    return $ and output+  where+    clear = ansi 0+    ansi n = "\x1B[" ++ (show n) ++ "m"++    colorForLevel "error" = 31 -- red+    colorForLevel "warning" = 33 -- yellow+    colorForLevel "info" = 32 -- green+    colorForLevel "style" = 32 -- green+    colorForLevel "message" = 1 -- bold+    colorForLevel "source" = 0 -- none+    colorForLevel _ = 0 -- none++    colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear++    doFile path = do+        contents <- readContents path+        doInput path contents++    doInput filename contents = do+        let fileLines = lines contents+        let lineCount = length fileLines+        let comments = getComments options contents+        let groups = groupWith scLine comments+        colorFunc <- getColorFunc+        mapM_ (\x -> do+            let lineNum = scLine (head x)+            let line = if lineNum < 1 || lineNum > lineCount+                            then ""+                            else fileLines !! (lineNum - 1)+            putStrLn ""+            putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")+            putStrLn (colorFunc "source" line)+            mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x+            putStrLn ""+          ) groups+        return $ null comments++    cuteIndent comment =+        (replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (code $ scCode comment) ++ ": " ++ (scMessage comment)++    code code = "SC" ++ (show code)++    getColorFunc = do+        term <- hIsTerminalDevice stdout+        return $ if term then colorComment else const id++-- This totally ignores the filenames. Fixme?+forJson options files = do+    comments <- liftM concat $ mapM (commentsFor options) files+    putStrLn $ encodeStrict $ comments+    return . null $ comments++-- Mimic GCC "file:line:col: (error|warning|note): message" format+forGcc options files = do+    files <- mapM process files+    return $ and files+  where+    process file = do+        contents <- readContents file+        let comments = makeNonVirtual (getComments options contents) contents+        mapM_ (putStrLn . format file) comments+        return $ null comments++    format filename c = concat [+            filename, ":",+            show $ scLine c, ":",+            show $ scColumn c, ": ",+            case scSeverity c of+                "error" -> "error"+                "warning" -> "warning"+                _ -> "note",+            ": ",+            concat . lines $ scMessage c,+            " [SC", show $ scCode c, "]"+      ]++-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies+forCheckstyle options files = do+    putStrLn "<?xml version='1.0' encoding='UTF-8'?>"+    putStrLn "<checkstyle version='4.3'>"+    statuses <- mapM (\x -> process x `catch` report) files+    putStrLn "</checkstyle>"+    return $ and statuses+  where+    process file = do+        comments <- commentsFor options file+        putStrLn (formatFile file comments)+        return $ null comments+    report error = do+        printErr $ show (error :: SomeException)+        return False++    severity "error" = "error"+    severity "warning" = "warning"+    severity _ = "info"+    attr s v = concat [ s, "='", escape v, "' " ]+    escape msg = concatMap escape' msg+    escape' c = if isOk c then [c] else "&#" ++ (show $ ord c) ++ ";"+    isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]++    formatFile name comments = concat [+        "<file ", attr "name" name, ">\n",+            concatMap format comments,+        "</file>"+        ]++    format c = concat [+        "<error ",+        attr "line" $ show . scLine $ c,+        attr "column" $ show . scColumn $ c,+        attr "severity" $ severity . scSeverity $ c,+        attr "message" $ scMessage c,+        attr "source" $ "ShellCheck.SC" ++ (show $ scCode c),+        "/>\n"+        ]++commentsFor options file =+    liftM (getComments options) $ readContents file++getComments options contents =+    excludeCodes (getExclusions options) $ shellCheck contents++readContents file = if file == "-" then getContents else readFile file++-- Realign comments from a tabstop of 8 to 1+makeNonVirtual comments contents =+    map fix comments+  where+    ls = lines contents+    fix c = c { scColumn = real (ls !! (scLine c - 1)) 0 0 (scColumn c) }+    real _ r v target | target <= v = r+    real [] r v _ = r -- should never happen+    real ('\t':rest) r v target =+        real rest (r+1) (v + 8 - (v `mod` 8)) target+    real (_:rest) r v target = real rest (r+1) (v+1) target++getOption [] _ def = def+getOption ((Flag var val):_) name _ | name == var = val+getOption (_:rest) flag def = getOption rest flag def++getOptions options name =+    map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options++split char str =+    split' str []+  where+    split' (a:rest) element =+        if a == char+        then (reverse element) : split' rest []+        else split' rest (a:element)+    split' [] element = [reverse element]++getExclusions options =+    let elements = concatMap (split ',') $ getOptions options "exclude"+        clean = dropWhile (not . isDigit)+    in+        map (Prelude.read . clean) elements :: [Int]++excludeCodes codes comments =+    filter (not . hasCode) comments+  where+    hasCode c = scCode c `elem` codes++main = do+    args <- getArgs+    parsedArgs <- parseArguments args+    code <- do+        status <- process parsedArgs+        return $ if status then ExitSuccess else ExitFailure 1+     `catch` return+     `catch` \err -> do+        printErr $ show (err :: SomeException)+        return $ ExitFailure 2+    exitWith code++process Nothing = return False+process (Just (options, files)) =+  let format = getOption options "format" "tty" in+    case Map.lookup format formats of+        Nothing -> do+            printErr $ "Unknown format " ++ format+            printErr $ "Supported formats:"+            mapM_ (printErr . write) $ Map.keys formats+            exitWith supportFailure+          where write s = "  " ++ s+        Just f -> do+            f options files+