diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for hledger-makeitso
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is 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.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  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.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  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 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. Use with the GNU Affero General Public License.
+
+  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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 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 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 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 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  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 GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/README.org b/README.org
new file mode 100644
--- /dev/null
+++ b/README.org
@@ -0,0 +1,387 @@
+#+STARTUP: showall
+#+PROPERTY: header-args:sh :prologue exec 2>&1 :epilogue echo :
+
+* hledger: Make It So
+
+  Inspired by the structure and ideas of [[https://github.com/adept/full-fledged-hledger/wiki][adept's Full-fledged Hledger]].
+
+  I've given [[https://pauley.org.za/functional-finance-hledger/][a talk]] at [[https://www.meetup.com/lambda-luminaries/events/qklkvpyxmbnb/][Lambda Luminaries Johannesburg]] featuring hledger and hledger-makeitso.
+
+* Who should use this?
+
+  =hledger-makeitso= is intended for you if:
+
+  - You are interested in getting started with [[http://hledger.org/][hledger]] and you wouldn't mind pointers to the right docs along the way.
+  - You want a way to organise your finances into a structure that will be maintainable over the long term.
+  - You want to be able to automate as much as possible when dealing with your financial life.
+  - You don't mind writing some scripts when needed, as long as it saves you time over the long term.
+  - You want the ability to model your entire financial life in one tool, as opposed to just the parts that some online tool currently supports.
+  - You appreciate the fact that all your financial information stays within your control.
+
+  At the moment this software is in a fairly stable beta.
+  It is still possible that I'll make a breaking change to enable some feature, but I hope not.
+
+  Let me know if you are actively using this - I'm more likely to proceed with caution if I know I'm not the only active user.
+
+* After Cloning This Repository
+
+  This repository has some submodules included, mostly related to the examples in the documentation.
+
+  You need to initialise and update the submodules:
+  #+BEGIN_SRC sh
+  git submodule init
+  git submodule update
+  #+END_SRC
+
+* Build Instructions
+
+  You need a recent version of [[https://docs.haskellstack.org/en/stable/README/][stack]] installed.
+
+  Then run:
+  #+NAME: stack-build
+  #+BEGIN_SRC sh :results none :exports both
+  stack test
+  stack install
+  #+END_SRC
+
+  Which should end with this:
+  #+BEGIN_SRC org
+  Copied executables to ~/.local/bin:
+  - hledger-makeitso
+  #+END_SRC
+
+  Ensure that =${HOME}/.local/bin= is in your =PATH=.
+
+  Usually this means adding this to your =~/.bashrc=:
+  #+BEGIN_SRC sh :results none :exports both
+  PATH="${HOME}/.local/bin:${PATH}"
+  #+END_SRC
+
+* How to Use It
+
+** Overview of the Basic Workflow
+
+   1. Save an input CSV file to a [[https://github.com/apauley/hledger-makeitso#input-files][specific directory]].
+   2. Add an hledger [[https://github.com/apauley/hledger-makeitso#rules-files][rules file]]. Include some classification rules if you want.
+   3. Run =hledger-makeitso import=
+
+   Add all your files to your favourite version control system.
+
+   The generated journal that you most likely want to use as your =LEDGER_FILE= is called =all-years.journal=.
+   This has include directives to all the automatically imported journals, as well as includes for your
+   own manually managed journal entries.
+
+   In a typical software project we don't add generated files to version control, but in this case I think it is a good idea
+   to add all the generated files to version control as well - when you inevitably change something, e.g. how you classify transactions
+   in your rules file, then you can easily see if your change had the desired effect by looking at a diff.
+
+** Detailed Step-By-Step Guide
+
+   Have a look at the [[file:docs/README.org][detailed step-by-step instructions]] and the files in the [[file:docs/][documentation directory]].
+
+   For a visual overview, check out the slide show version of the same step-by-step instructions:
+
+   https://pauley.org.za/hledger-makeitso/
+
+   You can see the example imported financial transactions as it was generated by the step-by-step
+   instructions here:
+
+   https://github.com/apauley/hledger-makeitso-example-finances
+
+* Feature Reference
+
+** Input Files
+
+   Your input files will probably be CSV files with a line for each transaction,
+   although other file types will work fine if you use a =preprocess= or a =construct= script that can read them.
+   These scripts are explained later.
+
+   We mostly use conventions based on a predefined directory structure for your input statements.
+
+   For example, assuming you have a =savings= account at =mybank=, you'll put your first CSV statement here:
+   =import/john/mybank/savings/1-in/2018/123456789_2018-06-30.csv=.
+
+   Some people may want to include accounts belonging to their spouse as part of the household finances:
+   =import/spouse/otherbank/checking/1-in/2018/987654321_2018-06-30.csv=.
+
+*** More About Input Files
+
+    All files and directories under the =import= directory is related to the automatic importing and classification of transactions.
+
+    The directory directly under =import= is meant to indicate the owner or custodian of the accounts below it.
+    It mostly has an impact on reporting. You may want to have separate reports for =import/mycompany= and =import/personal=.
+
+    Below the directory for the owner we can indicate where an account is held.
+    For a bank account you may choose to name it =import/john/mybank=.
+
+    If your underground bunker filled with gold has CSV statements linked to it,
+    then you can absolutely create =import/john/secret-treasure-room=.
+
+    Under the directory for the financial institution, you'll have a directory for each account
+    at that institution, e.g. =import/mycompany/bigbankinc/customer-deposits= and
+    =import/mycompany/bigbankinc/expense-account=.
+
+    Next you'll create a directory named =1-in=.
+    This is to distinguish it from =2-preprocessed= and =3-journal= which will be auto-generated later.
+
+    Under =1-in= you'll create a directory for the year, e.g. =2018=, and within that you can copy the statements for that year:
+    =import/john/mybank/savings/1-in/2018/123456789_2018-06-30.csv=
+
+** Rules Files
+
+   If your input file is in CSV format, or converted to CSV by your =preprocess= script,
+   then you'll need an [[http://hledger.org/csv.html][hledger rules file]].
+
+   =hledger-makeitso= will try to find a rules file for each statement in a few places.
+   The same rules file is typically used for all statements of a specific account, or even for all accounts
+   of the same specific bank.
+
+   - A global rules file for any =mybank= statement can be saved here: =import/mybank.rules=
+   - A rules file for all statements of a specific account: =import/spouse/bigbankinc/savings/bigbankinc-savings.rules=
+
+*** Statement-specific Rules Files
+
+    What happens if some of the statements for an account has a different format than the others?
+
+    This can happen if you normally get your statements directly from your bank, but some
+    statements you had to download from somewhere else, like Mint, because your bank is being daft
+    with older statements.
+
+    In order to tell =hledger-makeitso= that you want to override the rules file for a specific statement,
+    you need to add a suffix, separated by an underscore (=_=) and starting with the letters =rfo= (rules file override)
+    to the filename of that statement.
+
+    For example: assuming you've named your statement =99966633_20171223_1844_rfo-mint.csv=.
+
+    =hledger-makeitso= will look for a rules file named =rfo-mint.rules= in the following places:
+      - in the import directory, e.g. =import/rfo-mint.rules=
+      - in the bank directory, e.g. =import/john/mybank/rfo-mint.rules=
+      - in the account directory, e.g. =import/john/mybank/savings/rfo-mint.rules=
+
+*** Example rules file usage
+
+    A common scenario is multiple accounts that share the same file format, but have different =account1= directives.
+
+    One possible approach would be to include a shared rules file in your account-specific rules file.
+
+    If you are lucky enough that all statements at =mybank= share a common format across all accounts,
+    then you can =include= a rules file that just defines the parts that are shared across accounts.
+
+    Two accounts at =mybank= may have rules files similar to these.
+
+    A checking account at mybank:
+    #+BEGIN_SRC hledger
+    # Saved as: import/john/mybank/checking/mybank-checking.rules
+    include ../../../mybank-shared.rules
+    account1 Assets:Current:John:MyBank:Checking
+    #+END_SRC
+
+    Another account at mybank:
+    #+BEGIN_SRC hledger
+    # Saved as: import/alice/mybank/savings/mybank-savings.rules
+    include ../../../mybank-shared.rules
+    account1 Assets:Current:Alice:MyBank:Savings
+    #+END_SRC
+
+    Where =import/mybank-shared.rules= may define some shared attributes:
+    #+BEGIN_SRC hledger
+    skip 1
+
+    fields date, description, amount, balance
+
+    date-format %Y-%m-%d
+    currency $
+    #+END_SRC
+
+    Another possible approach could be to use your =preprocess= script to write out a CSV file
+    that has extra fields for =account1= and =account2=.
+
+    You could then create the above mentioned global =import/mybank.rules= with the fields defined more or less like this:
+    #+BEGIN_SRC hledger
+    fields date, description, amount, balance, account1, account2
+    #+END_SRC
+
+** Opening and Closing Balances
+
+*** Opening Balances
+
+   =hledger-makeitso= looks for a file named =YEAR-opening.journal= in each account directory, where =YEAR=
+   corresponds to an actual year directory, eg. *1983*
+   (if you have electronic statements [[https://en.wikipedia.org/wiki/Online_banking#First_online_banking_services_in_the_United_States][dating back to 1983]]).
+   Example: =import/john/mybank/savings/1983-opening.journal=
+
+   If it exists the file will automatically be included at the beginning of the generated journal include file for that year.
+
+   You need to edit this file for each account to specify the opening balance at the date of the first available transaction.
+
+   An opening balance may look something like this:
+   #+BEGIN_SRC hledger
+   2018-06-01 Savings Account Opening Balance
+   assets:Current:MyBank:Savings               $102.01
+   equity:Opening Balances:MyBank:Savings
+   #+END_SRC
+
+*** Closing Balances
+
+   Similar to opening balances, =hledger-makeitso= looks for an optional file named =YEAR-closing.journal= in each account directory.
+   Example: =import/john/mybank/savings/1983-closing.journal=
+
+   If it exists the file will automatically be included at the end of the generated journal include file for that year.
+
+   A closing balance may look something like this:
+   #+BEGIN_SRC hledger
+   2018-06-01 Savings Account Closing Balance
+   assets:Current:MyBank:Savings               $-234.56 = $0.00
+   equity:Closing Balances:MyBank:Savings
+   #+END_SRC
+
+*** Example Opening and Closing Journal Files
+
+   As an example, assuming that the relevant year is =2019= and =hledger-makeitso= is about to generate
+   =import/john/mybank/savings/2019-include.journal=, then one or both of the following files will be added to the include file if they exist:
+
+   1. =import/john/mybank/savings/2019-opening.journal=
+   2. =import/john/mybank/savings/2019-closing.journal=
+
+   The =opening.journal= will be included just before the other included entries, while the =closing.journal=
+   will be included just after the other entries in that include file.
+
+   An include file may look like this:
+
+   #+BEGIN_SRC sh
+   cat import/john/mybank/savings/2019-include.journal
+   #+END_SRC
+
+   #+BEGIN_SRC hledger
+   ### Generated by hledger-makeitso - DO NOT EDIT ###
+
+   !include 2019-opening.journal
+   !include 3-journal/2019/123456789_2019-01-30
+   !include 2019-closing.journal
+   #+END_SRC
+
+** The =preprocess= Script
+
+    Sometimes the statements you get from your bank is [[https://github.com/apauley/fnb-csv-demoronizer][less than suitable]] for automatic processing.
+    Or maybe you just want to make it easier for the hledger rules file to do its thing by adding some useful columns.
+
+    If you put a script called =preprocess= in the account directory, e.g. =import/john/mybank/savings/preprocess=,
+    then =hledger-makeitso= will call that script for each input statement.
+
+    The =preprocess= script will be called with 4 positional parameters:
+     1. The path to the input statement, e.g. =import/john/mybank/savings/1-in/2018/123456789_2018-06-30.csv=
+     2. The path to an output file that can be sent to =hledger=, e.g. =import/john/mybank/savings/2-preprocessed/2018/123456789_2018-06-30.csv=
+     3. The name of the bank, e.g. =mybank=
+     4. The name of the account, e.g. =savings=
+     5. The name of the owner, e.g. =john=
+
+    Your =preprocess= script is expected to:
+     - read the input file
+     - write a new output file at the supplied path that works with your rules file
+     - be idempotent. Running =preprocess= multiple times on the same files will produce the same result.
+
+** The =construct= Script
+
+    If you need even more power and flexibility than what you can get from the =preprocess= script and =hledger='s CSV import functionality,
+    then you can create your own custom script to =construct= transactions exactly as you need them.
+
+    At the expense of more construction work for you, of course.
+
+    As an example, =hledger='s CSV import currently [[https://github.com/simonmichael/hledger/issues/627][only supports two postings per transaction]], even though =hledger= itself
+    is perfectly happy with transactions containing more than two postings, e.g.:
+
+    #+BEGIN_SRC hledger
+    2019-02-01 Mortgage Payment
+    Liabilities:Mortgage                                1,000.00
+    Expenses:Interest:Real Estate                         833.33
+    Assets:Cash                                         -1833.33
+    #+END_SRC
+
+    The =construct= script can be used in addition to the =preprocess= script, or on it's own.
+    But since the =construct= script is more powerful than the =preprocess= script, you could tell your =construct= script to do
+    anything that the =preprocess= script would have done.
+
+    Save your =construct= script in the account directory, e.g. =import/john/mybank/savings/construct=.
+
+    =hledger-makeitso= will call your =construct= script with 4 positional parameters:
+     1. The path to the input statement, e.g. =import/john/mybank/savings/1-in/2018/123456789_2018-06-30.csv=
+     2. A "-" (indicating that output should be sent to =stdout=)
+     3. The name of the bank, e.g. =mybank=
+     4. The name of the account, e.g. =savings=
+     5. The name of the owner, e.g. =john=
+
+    Your =construct= script is expected to:
+     - read the input file
+     - generate your own =hledger= journal transactions
+     - be idempotent. Running =construct= multiple times on the same files should produce the same result.
+     - send all output to =stdout=. =hledger-makeitso= will pipe your output into =hledger= which will format it and save it to an output file.
+
+
+** Manually Managed Journals
+
+   Not every transaction in your life comes with CSV statements.
+
+   Sometimes you just need to add a transaction for that time you loaned a friend some money.
+
+   =hledger-makeitso= looks for =pre-import= and =post-import= files related to each generated include file as part of the import.
+
+   You can enter your own transactions manually into these files.
+
+   You can run =hledger-makeitso import --verbose= to see exactly which files are being looked for.
+
+   As an example, assuming that the relevant year is =2019= and =hledger-makeitso= is about to generate
+   =import/john/2019-include.journal=, then one or both of the following files will be added to the include file if they exist:
+
+   1. =import/john/_manual_/2019/pre-import.journal=
+   2. =import/john/_manual_/2019/post-import.journal=
+
+   The =pre-import.journal= will be included just before the other included entries, while the =post-import.journal=
+   will be included just after the other entries in that include file.
+
+   An include file may look like this:
+
+   #+BEGIN_SRC sh
+   cat import/john/2019-include.journal
+   #+END_SRC
+
+   #+BEGIN_SRC hledger
+   ### Generated by hledger-makeitso - DO NOT EDIT ###
+
+   !include _manual_/2019/pre-import.journal
+   !include mybank/2019-include.journal
+   !include otherbank/2019-include.journal
+   !include _manual_/2019/post-import.journal
+   #+END_SRC
+
+* Compatibility with Ledger
+
+  When writing out the journal include files, =hledger-makeitso= sorts the include statements by filename.
+
+  [[https://www.ledger-cli.org/][Ledger]] fails any balance assertions when the transactions aren't included in chronological order.
+
+  An easy way around this is to name your input files so that March's statement is listed before December's statement.
+
+  Another option is to add =--permissive= to any [[https://www.ledger-cli.org/][ledger]] command.
+
+  So you should easily be able to use both =ledger= and =hledger= on these journals.
+
+* Project Goals
+
+  My =hledger= files started to collect a bunch of supporting code that weren't really
+  specific to my financial situation.
+
+  I want to extract and share as much as possible of that supporting code.
+
+  Adept's goals also resonated with me:
+
+   - Tracking expenses should take as little time, effort and manual work as possible
+   - Eventual consistency should be achievable: even if I can't record something precisely right now,
+     maybe I would be able to do it later, so I should be able to leave things half-done and pick them up later
+   - Ability to refactor is a must. I want to be able to go back and change the way I am doing things,
+     with as little effort as possible and without fear of irrevocably breaking things.
+
+* Still To Be Done
+
+  I add ideas and thoughts in [[file:TODO.org][TODO.org]]
+
+  Let me know if you can think of some improvements.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn)
+import qualified Hledger.MakeItSo.Import.Types as IT
+import qualified Hledger.MakeItSo.Report.Types as RT
+import Hledger.MakeItSo.Common
+import Hledger.MakeItSo.Reports
+import Hledger.MakeItSo.CSVImport
+
+type SubcommandParams = (Maybe FilePath, Bool)
+data Command = Import SubcommandParams | Report SubcommandParams deriving (Show)
+
+main :: IO ()
+main = do
+  cmd <- options "An hledger workflow focusing on automated statement import and classification:\nhttps://github.com/apauley/hledger-makeitso#readme" parser
+  case cmd of
+    Import subParams -> toImportOptions subParams >>= importCSVs
+    Report subParams -> toReportOptions subParams >>= generateReports
+
+toImportOptions :: SubcommandParams -> IO IT.ImportOptions
+toImportOptions (maybeBaseDir, verbose) = do
+  bd <- dirOrPwd maybeBaseDir
+  return IT.ImportOptions {IT.baseDir = bd, IT.verbose = verbose}
+
+toReportOptions :: SubcommandParams -> IO RT.ReportOptions
+toReportOptions (maybeBaseDir, verbose) = do
+  bd <- dirOrPwd maybeBaseDir
+  return RT.ReportOptions {RT.baseDir = bd, RT.verbose = verbose}
+
+parser :: Parser Command
+parser = fmap Import (subcommand "import" "Converts CSV transactions into categorised journal files" subcommandParser)
+     <|> fmap Report (subcommand "report" "Generate Reports" subcommandParser)
+
+subcommandParser :: Parser SubcommandParams
+subcommandParser = (,) <$> optional (argPath "basedir" "The hledger-makeitso base directory")
+                       <*> switch  "verbose" 'v' "Print more verbose output"
diff --git a/hledger-makeitso.cabal b/hledger-makeitso.cabal
new file mode 100644
--- /dev/null
+++ b/hledger-makeitso.cabal
@@ -0,0 +1,88 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.1.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 59ade1a7ba88d7f66d95d1181ca0947b0c46281f3b7909755578659ead785124
+
+name:           hledger-makeitso
+version:        0.9.0.1
+synopsis:       An hledger workflow focusing on automated statement import and classification.
+description:    Please see the README on GitHub at <https://github.com/apauley/hledger-makeitso#readme>
+category:       Finance, Console
+homepage:       https://github.com/apauley/hledger-makeitso#readme
+bug-reports:    https://github.com/apauley/hledger-makeitso/issues
+author:         Andreas Pauley <andreas@pauley.org.za>
+maintainer:     Andreas Pauley <andreas@pauley.org.za>
+copyright:      2018 Andreas Pauley
+license:        GPL-3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.org
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/apauley/hledger-makeitso
+
+library
+  exposed-modules:
+      Hledger.MakeItSo.Common
+      Hledger.MakeItSo.CSVImport
+      Hledger.MakeItSo.Import.Types
+      Hledger.MakeItSo.Report.Types
+      Hledger.MakeItSo.Reports
+      Hledger.MakeItSo.Types
+  other-modules:
+      Paths_hledger_makeitso
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      base >=4.7 && <5
+    , containers
+    , foldl
+    , stm
+    , text
+    , time
+    , turtle
+  default-language: Haskell2010
+
+executable hledger-makeitso
+  main-is: Main.hs
+  other-modules:
+      Paths_hledger_makeitso
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
+  build-depends:
+      base >=4.7 && <5
+    , hledger-makeitso
+    , text
+    , turtle
+  default-language: Haskell2010
+
+test-suite hledger-makeitso-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Common.Unit
+      CSVImport.Integration
+      CSVImport.Unit
+      TestHelpers
+      Paths_hledger_makeitso
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HUnit
+    , base >=4.7 && <5
+    , containers
+    , foldl
+    , hledger-makeitso
+    , stm
+    , text
+    , turtle
+  default-language: Haskell2010
diff --git a/src/Hledger/MakeItSo/CSVImport.hs b/src/Hledger/MakeItSo/CSVImport.hs
new file mode 100644
--- /dev/null
+++ b/src/Hledger/MakeItSo/CSVImport.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hledger.MakeItSo.CSVImport
+    ( importCSVs
+    ) where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn, take)
+import qualified Data.Text as T
+import qualified Data.List.NonEmpty as NonEmpty
+import Hledger.MakeItSo.Types (LogMessage)
+import Hledger.MakeItSo.Import.Types
+import Hledger.MakeItSo.Common
+import Control.Concurrent.STM
+
+importCSVs :: ImportOptions -> IO ()
+importCSVs opts = sh (
+  do
+    ch <- liftIO newTChanIO
+    logHandle <- fork $ consoleChannelLoop ch
+    liftIO $ logVerbose opts ch "Starting import"
+    (journals, diff) <- time $ liftIO $ importCSVs' opts ch
+    liftIO $ channelOut ch $ format ("Imported "%d%" journals in "%s) (length journals) $ repr diff
+    liftIO $ terminateChannelLoop ch
+    wait logHandle
+  )
+
+importCSVs' :: ImportOptions -> TChan LogMessage -> IO [FilePath]
+importCSVs' opts ch = do
+  channelOut ch "Collecting input files..."
+  (inputFiles, diff) <- time $ single . shellToList . onlyFiles $ find (has (suffix "1-in/")) $ baseDir opts
+  let fileCount = length inputFiles
+  if (fileCount == 0) then
+    do
+      let msg = format ("I couldn't find any input files underneath "%fp
+                        %"\n\nhledger-makitso expects to find its input files in specifically\nnamed directories.\n\n"%
+                        "Have a look at the documentation for a detailed explanation:\n"%s) (dirname (baseDir opts) </> "import/") (docURL "input-files")
+      stderr $ select $ textToLines msg
+      exit $ ExitFailure 1
+    else
+    do
+      logVerbose opts ch $ format ("Found "%d%" input files in "%s%". Proceeding with import...") fileCount (repr diff)
+      let actions = map (extractAndImport opts ch) inputFiles :: [IO FilePath]
+      importedJournals <- single . shellToList $ parallel actions
+      sh $ writeIncludesUpTo opts ch "import" importedJournals
+      return importedJournals
+
+extractAndImport :: ImportOptions -> TChan LogMessage -> FilePath -> IO FilePath
+extractAndImport opts ch inputFile = do
+  case extractImportDirs inputFile of
+    Right importDirs -> importCSV opts ch importDirs inputFile
+    Left errorMessage -> do
+      stderr $ select $ textToLines errorMessage
+      exit $ ExitFailure 1
+
+importCSV :: ImportOptions -> TChan LogMessage -> ImportDirs -> FilePath -> IO FilePath
+importCSV opts ch importDirs srcFile = do
+  let preprocessScript = accountDir importDirs </> "preprocess"
+  let constructScript = accountDir importDirs </> "construct"
+  let bankName = importDirLine bankDir importDirs
+  let accountName = importDirLine accountDir importDirs
+  let ownerName = importDirLine ownerDir importDirs
+  csvFile <- preprocessIfNeeded opts ch preprocessScript bankName accountName ownerName srcFile
+  doCustomConstruct <- verboseTestFile opts ch constructScript
+  let importFun = if doCustomConstruct
+        then customConstruct opts ch constructScript bankName accountName ownerName
+        else hledgerImport opts ch
+  let journalOut = changePathAndExtension "3-journal" "journal" csvFile
+  mktree $ directory journalOut
+  importFun csvFile journalOut
+
+preprocessIfNeeded :: ImportOptions -> TChan LogMessage -> FilePath -> Line -> Line -> Line -> FilePath -> IO FilePath
+preprocessIfNeeded opts ch script bank account owner src = do
+  shouldPreprocess <- verboseTestFile opts ch script
+  if shouldPreprocess
+    then preprocess opts ch script bank account owner src
+    else return src
+
+preprocess :: ImportOptions -> TChan LogMessage -> FilePath -> Line -> Line -> Line -> FilePath -> IO FilePath
+preprocess opts ch script bank account owner src = do
+  let csvOut = changePathAndExtension "2-preprocessed" "csv" src
+  mktree $ directory csvOut
+  let script' = format fp script :: Text
+  let action = procs script' [format fp src, format fp csvOut, lineToText bank, lineToText account, lineToText owner] empty
+  let relScript = relativeToBase opts script
+  let relSrc = relativeToBase opts src
+  let msg = format ("executing '"%fp%"' on '"%fp%"'") relScript relSrc
+  _ <- logVerboseTime opts ch msg action
+  return csvOut
+
+hledgerImport :: ImportOptions -> TChan LogMessage -> FilePath -> FilePath -> IO FilePath
+hledgerImport opts ch csvSrc journalOut = do
+  case extractImportDirs csvSrc of
+    Right importDirs -> hledgerImport' opts ch importDirs csvSrc journalOut
+    Left errorMessage -> do
+      stderr $ select $ textToLines errorMessage
+      exit $ ExitFailure 1
+
+hledgerImport' :: ImportOptions -> TChan LogMessage -> ImportDirs -> FilePath -> FilePath -> IO FilePath
+hledgerImport' opts ch importDirs csvSrc journalOut = do
+  let candidates = rulesFileCandidates csvSrc importDirs
+  maybeRulesFile <- firstExistingFile candidates
+  let relCSV = relativeToBase opts csvSrc
+  case maybeRulesFile of
+    Just rf -> do
+      let relRules = relativeToBase opts rf
+      let action = procs "hledger" ["print", "--rules-file", format fp rf, "--file", format fp csvSrc, "--output-file", format fp journalOut] empty
+      let msg = format ("importing '"%fp%"' using rules file '"%fp%"'") relCSV relRules
+      _ <- logVerboseTime opts ch msg action
+      return journalOut
+    Nothing ->
+      do
+        let relativeCandidates = map (relativeToBase opts) candidates
+        let candidatesTxt = T.intercalate "\n" $ map (format fp) relativeCandidates
+        let msg = format ("I couldn't find an hledger rules file while trying to import\n"%fp
+                          %"\n\nI will happily use the first rules file I can find from any one of these "%d%" files:\n"%s
+                          %"\n\nHere is a bit of documentation about rules files that you may find helpful:\n"%s)
+                  relCSV (length candidates) candidatesTxt (docURL "rules-files")
+        stderr $ select $ textToLines msg
+        exit $ ExitFailure 1
+
+rulesFileCandidates :: FilePath -> ImportDirs -> [FilePath]
+rulesFileCandidates csvSrc importDirs = statementSpecificRulesFiles csvSrc importDirs ++ generalRulesFiles importDirs
+
+importDirLines :: (ImportDirs -> FilePath) -> ImportDirs -> [Line]
+importDirLines dirFun importDirs = NonEmpty.toList $ textToLines $ format fp $ dirname $ dirFun importDirs
+
+importDirLine :: (ImportDirs -> FilePath) -> ImportDirs -> Line
+importDirLine dirFun importDirs = foldl (<>) "" $ importDirLines dirFun importDirs
+
+generalRulesFiles :: ImportDirs -> [FilePath]
+generalRulesFiles importDirs = do
+  let bank = importDirLines bankDir importDirs
+  let account = importDirLines accountDir importDirs
+  let accountRulesFile = accountDir importDirs </> buildFilename (bank ++ account) "rules"
+
+  let bankRulesFile = importDir importDirs </> buildFilename bank "rules"
+  [accountRulesFile, bankRulesFile]
+
+statementSpecificRulesFiles :: FilePath -> ImportDirs -> [FilePath]
+statementSpecificRulesFiles csvSrc importDirs = do
+  let srcSuffix = snd $ T.breakOnEnd "_" (format fp (basename csvSrc))
+
+  if ((T.take 3 srcSuffix) == "rfo")
+    then
+    do
+      let srcSpecificFilename = fromText srcSuffix <.> "rules"
+      map (</> srcSpecificFilename) [accountDir importDirs, bankDir importDirs, importDir importDirs]
+    else []
+
+customConstruct :: ImportOptions -> TChan LogMessage -> FilePath -> Line -> Line -> Line -> FilePath -> FilePath -> IO FilePath
+customConstruct opts ch constructScript bank account owner csvSrc journalOut = do
+  let script = format fp constructScript :: Text
+  let importOut = inproc script [format fp csvSrc, "-", lineToText bank, lineToText account, lineToText owner] empty
+  let action = procs "hledger" ["print", "--ignore-assertions", "--file", "-", "--output-file", format fp journalOut] importOut
+  let relScript = relativeToBase opts constructScript
+  let relSrc = relativeToBase opts csvSrc
+  let msg = format ("executing '"%fp%"' on '"%fp%"'") relScript relSrc
+  _ <- logVerboseTime opts ch msg action
+
+  return journalOut
diff --git a/src/Hledger/MakeItSo/Common.hs b/src/Hledger/MakeItSo/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Hledger/MakeItSo/Common.hs
@@ -0,0 +1,341 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hledger.MakeItSo.Common
+    ( docURL
+    , showCmdArgs
+    , consoleChannelLoop
+    , terminateChannelLoop
+    , channelOut
+    , channelErr
+    , logVerbose
+    , logVerboseTime
+    , verboseTestFile
+    , relativeToBase
+    , relativeToBase'
+    , lsDirs
+    , onlyFiles
+    , onlyDirs
+    , filterPaths
+    , changePathAndExtension
+    , basenameLine
+    , buildFilename
+    , shellToList
+    , firstExistingFile
+    , groupValuesBy
+    , groupIncludeFiles
+    , allYearIncludeFiles
+    , yearsIncludeMap
+    , extraIncludesForFile
+    , groupPairs
+    , pairBy
+    , includePreamble
+    , toIncludeFiles
+    , toIncludeLine
+    , groupAndWriteIncludeFiles
+    , writeIncludesUpTo
+    , dirOrPwd
+    , extractImportDirs
+    ) where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import qualified GHC.IO.Handle.FD as H
+
+import Data.Maybe
+import qualified Control.Foldl as Fold
+import qualified Data.Map.Strict as Map
+import Data.Time.LocalTime
+
+import Data.Function (on)
+import qualified Data.List as List (nub, sort, sortBy, groupBy)
+import Data.Ord (comparing)
+import Hledger.MakeItSo.Types
+import qualified Hledger.MakeItSo.Import.Types as IT
+import Control.Concurrent.STM
+
+showCmdArgs :: [Text] -> Text
+showCmdArgs args = T.intercalate " " (map escapeArg args)
+
+escapeArg :: Text -> Text
+escapeArg a = if (T.count " " a > 0) then "'" <> a <> "'" else a
+
+channelOut :: TChan LogMessage -> Text -> IO ()
+channelOut ch txt = atomically $ writeTChan ch $ StdOut txt
+
+channelErr :: TChan LogMessage -> Text -> IO ()
+channelErr ch txt = atomically $ writeTChan ch $ StdErr txt
+
+timestampPrefix :: Text -> IO Text
+timestampPrefix txt = do
+  t <- getZonedTime
+  return $ format (s%"\thledger-makeitso "%s) (repr t) txt
+
+logToChannel :: TChan LogMessage -> Text -> IO ()
+logToChannel ch msg = do
+  ts <- timestampPrefix msg
+  channelErr ch ts
+
+consoleChannelLoop :: TChan LogMessage -> IO ()
+consoleChannelLoop ch = do
+  logMsg <- atomically $ readTChan ch
+  case logMsg of
+    StdOut msg -> do
+      T.hPutStrLn H.stdout msg
+      consoleChannelLoop ch
+    StdErr msg -> do
+      T.hPutStrLn H.stderr msg
+      consoleChannelLoop ch
+    Terminate  -> return ()
+
+terminateChannelLoop :: TChan LogMessage -> IO ()
+terminateChannelLoop ch = atomically $ writeTChan ch Terminate
+
+logVerbose :: HasVerbosity o => o -> TChan LogMessage -> Text -> IO ()
+logVerbose opts ch msg = if (verbose opts) then logToChannel ch msg else return ()
+
+logVerboseTime :: HasVerbosity o => o -> TChan LogMessage -> Text -> IO a -> IO (a, NominalDiffTime)
+logVerboseTime opts ch msg action = do
+  logVerbose opts ch $ format ("Begin: "%s) msg
+  (result, diff) <- time action
+  logVerbose opts ch $ format ("End:   "%s%" ("%s%")") msg $ repr diff
+  return (result, diff)
+
+verboseTestFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> FilePath -> IO Bool
+verboseTestFile opts ch p = do
+  fileExists <- testfile p
+  let rel = relativeToBase opts p
+  if fileExists
+    then logVerbose opts ch $ format ("Found a "       %fp%" file at '"%fp%"'") (basename rel) rel
+    else logVerbose opts ch $ format ("Did not find a "%fp%" file at '"%fp%"'") (basename rel) rel
+  return fileExists
+
+relativeToBase :: HasBaseDir o => o -> FilePath -> FilePath
+relativeToBase opts = relativeToBase' (baseDir opts)
+
+relativeToBase' :: FilePath -> FilePath -> FilePath
+relativeToBase' bd p = fromMaybe p $ stripPrefix (forceTrailingSlash bd) p
+
+groupPairs' :: (Eq a, Ord a) => [(a, b)] -> [(a, [b])]
+groupPairs' = map (\ll -> (fst . head $ ll, map snd ll)) . List.groupBy ((==) `on` fst)
+              . List.sortBy (comparing fst)
+
+groupPairs :: (Eq a, Ord a) => [(a, b)] -> Map.Map a [b]
+groupPairs = Map.fromList . groupPairs'
+
+pairBy :: (a -> b) -> [a] -> [(b, a)]
+pairBy keyFun = map (\v -> (keyFun v, v))
+
+groupValuesBy :: (Ord k, Ord v) => (v -> k) -> [v] -> Map.Map k [v]
+groupValuesBy keyFun = groupPairs . pairBy keyFun
+
+initialIncludeFilePath :: FilePath -> FilePath
+initialIncludeFilePath p = (parent . parent . parent) p </> includeFileName p
+
+parentIncludeFilePath :: FilePath -> FilePath
+parentIncludeFilePath p = (parent . parent) p </> (filename p)
+
+allYearsPath :: FilePath -> FilePath
+allYearsPath p = directory p </> "all-years.journal"
+
+groupIncludeFiles :: [FilePath] -> (Map.Map FilePath [FilePath], Map.Map FilePath [FilePath])
+groupIncludeFiles = allYearIncludeFiles . groupIncludeFilesPerYear
+
+groupIncludeFilesPerYear :: [FilePath] -> Map.Map FilePath [FilePath]
+groupIncludeFilesPerYear [] = Map.empty
+groupIncludeFilesPerYear ps@(p:_) = case extractImportDirs p of
+    Right _ -> (groupValuesBy initialIncludeFilePath) ps
+    Left  _ -> (groupValuesBy parentIncludeFilePath)  ps
+
+allYearIncludeFiles :: Map.Map FilePath [FilePath] -> (Map.Map FilePath [FilePath], Map.Map FilePath [FilePath])
+allYearIncludeFiles m = (m, yearsIncludeMap $ Map.keys m)
+
+yearsIncludeMap :: [FilePath] -> Map.Map FilePath [FilePath]
+yearsIncludeMap = groupValuesBy allYearsPath
+
+docURL :: Line -> Text
+docURL = format ("https://github.com/apauley/hledger-makeitso#"%l)
+
+lsDirs :: FilePath -> Shell FilePath
+lsDirs = onlyDirs . ls
+
+onlyDirs :: Shell FilePath -> Shell FilePath
+onlyDirs = excludeHiddenFiles . excludeWeirdPaths . filterPathsByFileStatus isDirectory
+
+onlyFiles :: Shell FilePath -> Shell FilePath
+onlyFiles = excludeHiddenFiles . filterPathsByFileStatus isRegularFile
+
+filterPathsByFileStatus :: (FileStatus -> Bool) -> Shell FilePath -> Shell FilePath
+filterPathsByFileStatus filepred files = do
+  files' <- shellToList files
+  filtered <- filterPathsByFileStatus' filepred [] files'
+  select filtered
+
+filterPathsByFileStatus' :: (FileStatus -> Bool) -> [FilePath] -> [FilePath] -> Shell [FilePath]
+filterPathsByFileStatus' _ acc [] = return acc
+filterPathsByFileStatus' filepred acc (file:files) = do
+  filestat <- stat file
+  let filtered = if (filepred filestat) then file:acc else acc
+  filterPathsByFileStatus' filepred filtered files
+
+filterPaths :: (FilePath -> IO Bool) -> [FilePath] -> Shell [FilePath]
+filterPaths = filterPaths' []
+
+filterPaths' :: [FilePath] -> (FilePath -> IO Bool) -> [FilePath] -> Shell [FilePath]
+filterPaths' acc _ [] = return acc
+filterPaths' acc filepred (file:files) = do
+  shouldInclude <- liftIO $ filepred file
+  let filtered = if shouldInclude then file:acc else acc
+  filterPaths' filtered filepred files
+
+excludeHiddenFiles :: Shell FilePath -> Shell FilePath
+excludeHiddenFiles paths = do
+  p <- paths
+  case (match (prefix ".") $ format fp $ filename p) of
+    [] -> select [p]
+    _  -> select []
+
+excludeWeirdPaths :: Shell FilePath -> Shell FilePath
+excludeWeirdPaths = findtree (suffix $ noneOf "_")
+
+firstExistingFile :: [FilePath] -> IO (Maybe FilePath)
+firstExistingFile files = do
+  case files of
+    []   -> return Nothing
+    file:fs -> do
+      exists <- testfile file
+      if exists then return (Just file) else firstExistingFile fs
+
+basenameLine :: FilePath -> Shell Line
+basenameLine path = case (textToLine $ format fp $ basename path) of
+  Nothing -> die $ format ("Unable to determine basename from path: "%fp%"\n") path
+  Just bn -> return bn
+
+buildFilename :: [Line] -> Text -> FilePath
+buildFilename identifiers ext = fromText (T.intercalate "-" (map lineToText identifiers)) <.> ext
+
+shellToList :: Shell a -> Shell [a]
+shellToList files = fold files Fold.list
+
+includeFileName :: FilePath -> FilePath
+includeFileName = (<.> "journal") . fromText . (format (fp%"-include")) . dirname
+
+toIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> Map.Map FilePath [FilePath] -> Shell (Map.Map FilePath Text)
+toIncludeFiles opts ch m = do
+  preMap  <- extraIncludes opts ch (Map.keys m) ["opening.journal"] ["pre-import.journal"]
+  postMap <- extraIncludes opts ch (Map.keys m) ["closing.journal"] ["post-import.journal"]
+  return $ (addPreamble . toIncludeFiles' preMap postMap) m
+
+extraIncludes :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [FilePath] -> [Text] -> [FilePath] -> Shell (Map.Map FilePath [FilePath])
+extraIncludes opts ch = extraIncludes' opts ch Map.empty
+
+extraIncludes' :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> Map.Map FilePath [FilePath] -> [FilePath] -> [Text] -> [FilePath] -> Shell (Map.Map FilePath [FilePath])
+extraIncludes' _ _ acc [] _ _ = return acc
+extraIncludes' opts ch acc (file:files) extraSuffixes manualFiles = do
+  extra <- extraIncludesForFile opts ch file extraSuffixes manualFiles
+  extraIncludes' opts ch (Map.unionWith (++) acc extra) files extraSuffixes manualFiles
+
+extraIncludesForFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> FilePath -> [Text] -> [FilePath] -> Shell (Map.Map FilePath [FilePath])
+extraIncludesForFile opts ch file extraSuffixes manualFiles = do
+  let dirprefix = fromText $ fst $ T.breakOn "-" $ format fp $ basename file
+  let fileNames = map (\suff -> fromText $ format (fp%"-"%s) dirprefix suff) extraSuffixes
+  let suffixFiles = map (directory file </>) fileNames
+  let suffixDirFiles = map (directory file </> "_manual_" </> dirprefix </>) manualFiles
+  let extraFiles = suffixFiles ++ suffixDirFiles
+  filtered <- filterPaths testfile extraFiles
+  let logMsg = format ("Looking for possible extra include files for '"%fp%"' among these "%d%" options: "%s%". Found "%d%": "%s)
+               (relativeToBase opts file) (length extraFiles) (repr $ relativeFilesAsText opts extraFiles)
+               (length filtered) (repr $ relativeFilesAsText opts filtered)
+  liftIO $ logVerbose opts ch logMsg
+  return $ Map.fromList [(file, filtered)]
+
+relativeFilesAsText :: HasBaseDir o => o -> [FilePath] -> [Text]
+relativeFilesAsText opts ps = map ((format fp) . (relativeToBase opts)) ps
+
+toIncludeFiles' :: Map.Map FilePath [FilePath] -> Map.Map FilePath [FilePath] -> Map.Map FilePath [FilePath] -> Map.Map FilePath Text
+toIncludeFiles' preMap postMap = Map.mapWithKey $ generatedIncludeText preMap postMap
+
+addPreamble :: Map.Map FilePath Text -> Map.Map FilePath Text
+addPreamble = Map.map (\txt -> includePreamble <> "\n" <> txt)
+
+toIncludeLine :: FilePath -> FilePath -> Text
+toIncludeLine base file = format ("!include "%fp) $ relativeToBase' base file
+
+generatedIncludeText :: Map.Map FilePath [FilePath] -> Map.Map FilePath [FilePath] -> FilePath -> [FilePath] -> Text
+generatedIncludeText preMap postMap outputFile fs = do
+  let preFiles = fromMaybe [] $ Map.lookup outputFile preMap
+  let files = List.nub . List.sort $ fs
+  let postFiles = fromMaybe [] $ Map.lookup outputFile postMap
+  let lns = map (toIncludeLine $ directory outputFile) $ preFiles ++ files ++ postFiles
+  T.intercalate "\n" $ lns ++ [""]
+
+includePreamble :: Text
+includePreamble = "### Generated by hledger-makeitso - DO NOT EDIT ###\n"
+
+groupAndWriteIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [FilePath] -> Shell [FilePath]
+groupAndWriteIncludeFiles opts ch = writeFileMap opts ch . groupIncludeFiles
+
+writeFiles :: Shell (Map.Map FilePath Text) -> Shell [FilePath]
+writeFiles fileMap = do
+  m <- fileMap
+  writeFiles' m
+
+writeFiles' :: Map.Map FilePath Text -> Shell [FilePath]
+writeFiles' fileMap = do
+  liftIO $ writeTextMap fileMap
+  return $ Map.keys fileMap
+
+writeTextMap :: Map.Map FilePath Text -> IO ()
+writeTextMap = Map.foldlWithKey (\a k v -> a <> writeTextFile k v) (return ())
+
+writeFileMap :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> (Map.Map FilePath [FilePath], Map.Map FilePath [FilePath]) -> Shell [FilePath]
+writeFileMap opts ch (m, allYears) = do
+  _ <- writeFiles' $ (addPreamble . toIncludeFiles' Map.empty Map.empty) allYears
+  writeFiles . (toIncludeFiles opts ch) $ m
+
+writeIncludesUpTo :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> FilePath -> [FilePath] -> Shell [FilePath]
+writeIncludesUpTo _ _ _ [] = return []
+writeIncludesUpTo opts ch stopAt paths = do
+  let shouldStop = any (\dir -> dir == stopAt) $ map dirname paths
+  if shouldStop
+    then groupAndWriteIncludeFiles opts ch paths else
+    do
+      newPaths <- groupAndWriteIncludeFiles opts ch paths
+      writeIncludesUpTo opts ch stopAt newPaths
+
+changeExtension :: Text -> FilePath -> FilePath
+changeExtension ext path = (dropExtension path) <.> ext
+
+changePathAndExtension :: FilePath -> Text -> FilePath -> FilePath
+changePathAndExtension newOutputLocation newExt = (changeOutputPath newOutputLocation) . (changeExtension newExt)
+
+changeOutputPath :: FilePath -> FilePath -> FilePath
+changeOutputPath newOutputLocation srcFile = mconcat $ map changeSrcDir $ splitDirectories srcFile
+  where changeSrcDir file = if (file == "1-in/" || file == "2-preprocessed/") then newOutputLocation else file
+
+dirOrPwd :: Maybe FilePath -> IO FilePath
+dirOrPwd maybeBaseDir = fmap forceTrailingSlash (fromMaybe pwd $ fmap realpath maybeBaseDir)
+
+forceTrailingSlash :: FilePath -> FilePath
+forceTrailingSlash p = directory (p </> "temp")
+
+importDirBreakdown ::  FilePath -> [FilePath]
+importDirBreakdown = importDirBreakdown' []
+
+importDirBreakdown' :: [FilePath] -> FilePath -> [FilePath]
+importDirBreakdown' acc path = do
+  let dir = directory path
+  if (dirname dir == "import" || (dirname dir == ""))
+    then dir:acc
+    else importDirBreakdown' (dir:acc) $ parent dir
+
+extractImportDirs :: FilePath -> Either Text IT.ImportDirs
+extractImportDirs inputFile = do
+  case importDirBreakdown inputFile of
+    [bd,owner,bank,account,filestate,year] -> Right $ IT.ImportDirs bd owner bank account filestate year
+    _ -> do
+      Left $ format ("I couldn't find the right number of directories between \"import\" and the input file:\n"%fp
+                      %"\n\nhledger-makeitso expects to find input files in this structure:\n"%
+                      "import/owner/bank/account/filestate/year/trxfile\n\n"%
+                      "Have a look at the documentation for a detailed explanation:\n"%s) inputFile (docURL "input-files")
diff --git a/src/Hledger/MakeItSo/Import/Types.hs b/src/Hledger/MakeItSo/Import/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Hledger/MakeItSo/Import/Types.hs
@@ -0,0 +1,23 @@
+module Hledger.MakeItSo.Import.Types
+where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn)
+import Hledger.MakeItSo.Types
+
+data ImportOptions = ImportOptions { baseDir :: FilePath, verbose :: Bool }
+  deriving (Show)
+
+instance HasVerbosity ImportOptions where
+  verbose (ImportOptions _ v) = v
+
+instance HasBaseDir ImportOptions where
+  baseDir (ImportOptions bd _) = bd
+
+data ImportDirs = ImportDirs { importDir  :: FilePath
+                             , ownerDir   :: FilePath
+                             , bankDir    :: FilePath
+                             , accountDir :: FilePath
+                             , stateDir   :: FilePath
+                             , yearDir    :: FilePath
+                             } deriving (Show)
diff --git a/src/Hledger/MakeItSo/Report/Types.hs b/src/Hledger/MakeItSo/Report/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Hledger/MakeItSo/Report/Types.hs
@@ -0,0 +1,15 @@
+module Hledger.MakeItSo.Report.Types
+where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn)
+import Hledger.MakeItSo.Types
+
+data ReportOptions = ReportOptions { baseDir :: FilePath, verbose :: Bool }
+  deriving (Show)
+
+instance HasVerbosity ReportOptions where
+  verbose (ReportOptions _ v) = v
+
+instance HasBaseDir ReportOptions where
+  baseDir (ReportOptions bd _) = bd
diff --git a/src/Hledger/MakeItSo/Reports.hs b/src/Hledger/MakeItSo/Reports.hs
new file mode 100644
--- /dev/null
+++ b/src/Hledger/MakeItSo/Reports.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hledger.MakeItSo.Reports
+    ( generateReports
+    ) where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn)
+import Hledger.MakeItSo.Types
+import Hledger.MakeItSo.Report.Types
+import Hledger.MakeItSo.Common
+import Control.Concurrent.STM
+
+generateReports :: ReportOptions -> IO ()
+generateReports opts = sh (
+  do
+    ch <- liftIO newTChanIO
+    logHandle <- fork $ consoleChannelLoop ch
+    (reports, diff) <- time $ liftIO $ generateReports' opts ch
+    liftIO $ channelOut ch $ format ("Generated "%d%" reports in "%s) (length reports) $ repr diff
+    liftIO $ terminateChannelLoop ch
+    wait logHandle
+  )
+
+generateReports' :: ReportOptions -> TChan LogMessage -> IO [FilePath]
+generateReports' opts ch = do
+  logVerbose opts ch "Something will be here Real Soon Now (tm)"
+  channelOut ch "Report generation has not been implemented. Yet. https://github.com/apauley/hledger-makeitso/pull/4"
+  return []
diff --git a/src/Hledger/MakeItSo/Types.hs b/src/Hledger/MakeItSo/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Hledger/MakeItSo/Types.hs
@@ -0,0 +1,13 @@
+module Hledger.MakeItSo.Types
+where
+
+import Turtle
+import Prelude hiding (FilePath, putStrLn)
+
+data LogMessage = StdOut Text | StdErr Text | Terminate deriving (Show)
+
+class HasVerbosity a where
+  verbose :: a -> Bool
+
+class HasBaseDir a where
+  baseDir :: a -> FilePath
diff --git a/test/CSVImport/Integration.hs b/test/CSVImport/Integration.hs
new file mode 100644
--- /dev/null
+++ b/test/CSVImport/Integration.hs
@@ -0,0 +1,231 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+
+module CSVImport.Integration (tests) where
+
+import Test.HUnit
+import Turtle
+import Prelude hiding (FilePath)
+import qualified Data.Map.Strict as Map
+import qualified Control.Foldl as Fold
+import qualified Data.Text as T
+import qualified Data.List as List (sort)
+
+import TestHelpers
+import Hledger.MakeItSo.Import.Types
+import Hledger.MakeItSo.Common
+import Control.Concurrent.STM
+
+testHiddenFiles = TestCase (
+  sh (
+      do
+        tmpdir <- using (mktempdir "." "makeitso")
+        let tmpJournals = map (tmpdir </>) journalFiles :: [FilePath]
+        let tmpExtras = map (tmpdir </>) extraFiles :: [FilePath]
+        let tmpHidden = map (tmpdir </>) hiddenFiles :: [FilePath]
+        let onDisk = List.sort $ tmpJournals ++ tmpExtras ++ tmpHidden
+        touchAll onDisk
+        filtered <- (fmap List.sort) $ shellToList $ onlyFiles $ select onDisk
+        let expected = List.sort $ tmpExtras ++ tmpJournals
+        liftIO $ assertEqual "Hidden files should be excluded" expected filtered
+     )
+  )
+
+testDirOrPwd = TestCase (
+  sh (
+      do
+        currentDir <- fmap (\p -> directory (p </> "t")) pwd
+        tmpdir <- using (mktempdir "." "makeitso")
+        let fooDir = collapse $ currentDir </> tmpdir </> "foo/"
+        let barDir = collapse $ currentDir </> tmpdir </> "bar/"
+        mkdir fooDir
+        mkdir barDir
+        d1 <- liftIO $ dirOrPwd Nothing
+        liftIO $ assertEqual "dirOrPwd returns pwd as a fallback" currentDir d1
+        liftIO $ assertEqual "dirOrPwd assumes the fallback is a directory" (directory d1) d1
+        d2 <- liftIO $ dirOrPwd $ Just $ tmpdir </> "foo"
+        liftIO $ assertEqual "dirOrPwd returns the supplied dir - no trailing slash supplied" fooDir d2
+        liftIO $ assertEqual "dirOrPwd assumes the supplied dir is a directory - no trailing slash supplied" (directory d2) d2
+        d3 <- liftIO $ dirOrPwd $ Just $ tmpdir </> "bar/"
+        liftIO $ assertEqual "dirOrPwd returns the supplied dir - trailing slash supplied" barDir d3
+        liftIO $ assertEqual "dirOrPwd assumes the supplied dir is a directory - trailing slash supplied" (directory d3) d3
+     )
+  )
+
+testFilterPaths = TestCase (
+  sh (
+      do
+        tmpdir <- using (mktempdir "." "makeitso")
+        let tmpJournals = map (tmpdir </>) journalFiles :: [FilePath]
+        let tmpExtras = map (tmpdir </>) extraFiles :: [FilePath]
+        let tmpHidden = map (tmpdir </>) hiddenFiles :: [FilePath]
+        let onDisk = List.sort $ tmpJournals ++ tmpExtras ++ tmpHidden
+        touchAll onDisk
+
+        let nonExistant = map (tmpdir </>) ["where", "is", "my", "mind"]
+        let toFilter = nonExistant ++ onDisk
+        filtered <- single $ filterPaths testfile toFilter
+        let actual = List.sort filtered
+        liftIO $ assertEqual "The filtered paths should exclude files not actually on disk" onDisk actual
+     )
+  )
+
+testExtraIncludesForFile = TestCase (
+  sh (
+      do
+        tmpdir <- using (mktempdir "." "makeitso")
+        let importedJournals = map (tmpdir </>) journalFiles :: [FilePath]
+        let accountDir = "import/john/bogartbank/savings"
+        let opening = tmpdir </> accountDir </> "2017-opening.journal"
+        let closing = tmpdir </> accountDir </> "2017-closing.journal"
+        let hidden = map (tmpdir </>) hiddenFiles :: [FilePath]
+        touchAll $ importedJournals ++ hidden
+
+        let accountInclude = tmpdir </> accountDir </> "2017-include.journal"
+        let expectedEmpty = [(accountInclude, [])]
+
+        ch <- liftIO newTChanIO
+
+        extraOpening1 <- extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["opening.journal"] []
+        liftIO $ assertEqual "The opening journal should not be included when it is not on disk" expectedEmpty extraOpening1
+
+        extraClosing1 <- extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["closing.journal"] []
+        liftIO $ assertEqual "The closing journal should not be included when it is not on disk" expectedEmpty extraClosing1
+
+        touchAll [opening, closing]
+
+        extraOpening2 <- extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["opening.journal"] []
+        liftIO $ assertEqual "The opening journal should be included when it is on disk" [(accountInclude, [opening])] extraOpening2
+
+        extraClosing2 <- extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["closing.journal"] []
+        liftIO $ assertEqual "The closing journal should be included when it is on disk" [(accountInclude, [closing])] extraClosing2
+     ))
+
+testIncludesPrePost = TestCase (
+  sh (
+      do
+        tmpdir <- using (mktempdir "." "makeitso")
+        let ownerDir = tmpdir </> "import/john"
+        let includeFile = ownerDir </> "2019-include.journal"
+        let pre  = ownerDir </> "_manual_" </> "2019" </> "pre-import.journal"
+        let post = ownerDir </> "_manual_" </> "2019" </> "post-import.journal"
+        touchAll [pre, post]
+
+        let includeMap = Map.singleton includeFile [ownerDir </> "bank1" </> "2019-include.journal",
+                                                    ownerDir </> "bank2" </> "2019-include.journal"]
+
+        ch <- liftIO newTChanIO
+        fileMap <- toIncludeFiles (defaultOpts tmpdir) ch includeMap
+        let expectedText = includePreamble <> "\n"
+              <> "!include _manual_/2019/pre-import.journal\n"
+              <> "!include bank1/2019-include.journal\n"
+              <> "!include bank2/2019-include.journal\n"
+              <> "!include _manual_/2019/post-import.journal\n"
+        let expectedMap = Map.singleton includeFile expectedText
+        liftIO $ assertEqual "All pre/post files on disk should be included" expectedMap fileMap
+     ))
+
+testIncludesOpeningClosing = TestCase (
+  sh (
+      do
+        tmpdir <- using (mktempdir "." "makeitso")
+        let ownerDir = tmpdir </> "import/john"
+        let accountDir = ownerDir </> "bank1" </> "savings"
+        let includeFile = accountDir </> "2019-include.journal"
+        let opening = accountDir </> "2019-opening.journal"
+        let closing = accountDir </> "2019-closing.journal"
+        touchAll [opening, closing]
+
+        let includeMap = Map.singleton includeFile [accountDir </> "3-journal" </> "2019" </> "2019-01-30.journal"]
+
+        ch <- liftIO newTChanIO
+        fileMap <- toIncludeFiles (defaultOpts tmpdir) ch includeMap
+        let expectedText = includePreamble <> "\n"
+              <> "!include 2019-opening.journal\n"
+              <> "!include 3-journal/2019/2019-01-30.journal\n"
+              <> "!include 2019-closing.journal\n"
+        let expectedMap = Map.singleton includeFile expectedText
+        liftIO $ assertEqual "All pre/post files on disk should be included" expectedMap fileMap
+     ))
+
+testWriteIncludeFiles = TestCase (
+  sh (
+      do
+        tmpdir <- using (mktempdir "." "makeitso")
+        let importedJournals = map (tmpdir </>) journalFiles :: [FilePath]
+        let extras = map (tmpdir </>) extraFiles :: [FilePath]
+        let hidden = map (tmpdir </>) hiddenFiles :: [FilePath]
+        touchAll $ importedJournals ++ extras ++ hidden
+
+        let jane1 = tmpdir </> "import/jane/bogartbank/checking/2018-include.journal"
+        let jane2 = tmpdir </> "import/jane/bogartbank/checking/2019-include.journal"
+        let jane3 = tmpdir </> "import/jane/bogartbank/savings/2017-include.journal"
+        let jane4 = tmpdir </> "import/jane/bogartbank/savings/2018-include.journal"
+        let jane5 = tmpdir </> "import/jane/otherbank/creditcard/2017-include.journal"
+        let jane6 = tmpdir </> "import/jane/otherbank/creditcard/2018-include.journal"
+        let jane7 = tmpdir </> "import/jane/otherbank/investments/2018-include.journal"
+        let jane8 = tmpdir </> "import/jane/otherbank/investments/2019-include.journal"
+
+        let john1 = tmpdir </> "import/john/bogartbank/checking/2018-include.journal"
+        let john2 = tmpdir </> "import/john/bogartbank/checking/2019-include.journal"
+        let john3 = tmpdir </> "import/john/bogartbank/savings/2017-include.journal"
+        let john4 = tmpdir </> "import/john/bogartbank/savings/2018-include.journal"
+        let john5 = tmpdir </> "import/john/otherbank/creditcard/2017-include.journal"
+        let john6 = tmpdir </> "import/john/otherbank/creditcard/2018-include.journal"
+        let john7 = tmpdir </> "import/john/otherbank/investments/2018-include.journal"
+        let john8 = tmpdir </> "import/john/otherbank/investments/2019-include.journal"
+        let expectedIncludes = [jane1, jane2, jane3, jane4, jane5, jane6, jane7, jane8,
+                                john1, john2, john3, john4, john5, john6, john7, john8]
+
+        ch <- liftIO newTChanIO
+        reportedAsWritten <- single $ groupAndWriteIncludeFiles (defaultOpts tmpdir) ch importedJournals
+        liftIO $ assertEqual "groupAndWriteIncludeFiles should return which files it wrote" expectedIncludes reportedAsWritten
+
+        let allYears = [tmpdir </> "import/jane/bogartbank/checking/all-years.journal",
+                        tmpdir </> "import/jane/bogartbank/savings/all-years.journal",
+                        tmpdir </> "import/jane/otherbank/creditcard/all-years.journal",
+                        tmpdir </> "import/jane/otherbank/investments/all-years.journal",
+                        tmpdir </> "import/john/bogartbank/checking/all-years.journal",
+                        tmpdir </> "import/john/bogartbank/savings/all-years.journal",
+                        tmpdir </> "import/john/otherbank/creditcard/all-years.journal",
+                        tmpdir </> "import/john/otherbank/investments/all-years.journal"]
+        let expectedOnDisk = List.sort $ reportedAsWritten ++ extras ++ importedJournals ++ allYears
+        allFilesOnDisk <- single $ sort $ onlyFiles $ lstree tmpdir
+        liftIO $ assertEqual "The actual files on disk should match what groupAndWriteIncludeFiles reported" expectedOnDisk allFilesOnDisk
+
+        let expectedJohn1Contents = includePreamble <> "\n"
+              <> "!include 3-journal/2018/2018-10-30.journal\n"
+              <> "!include 3-journal/2018/2018-11-30.journal\n"
+              <> "!include 3-journal/2018/2018-12-30.journal\n"
+        actualJohn1Contents <- liftIO $ readTextFile john1
+        liftIO $ assertEqual "John1: The include file contents should be the journal files" expectedJohn1Contents actualJohn1Contents
+
+        let expectedJohn2Contents = includePreamble <> "\n"
+              <> "!include 3-journal/2019/2019-01-30.journal\n"
+              <> "!include 3-journal/2019/2019-02-30.journal\n"
+        actualJohn2Contents <- liftIO $ readTextFile john2
+        liftIO $ assertEqual "John2: The include file contents should be the journal files" expectedJohn2Contents actualJohn2Contents
+
+        let expectedJohn3Contents = includePreamble <> "\n"
+              <> "!include 2017-opening.journal\n"
+              <> "!include 3-journal/2017/2017-11-30.journal\n"
+              <> "!include 3-journal/2017/2017-12-30.journal\n"
+        actualJohn3Contents <- liftIO $ readTextFile john3
+        liftIO $ assertEqual "John3: The include file contents should be the journal files" expectedJohn3Contents actualJohn3Contents
+
+        let expectedJohn4Contents = includePreamble <> "\n"
+              <> "!include 3-journal/2018/2018-01-30.journal\n"
+              <> "!include 3-journal/2018/2018-02-30.journal\n"
+        actualJohn4Contents <- liftIO $ readTextFile john4
+        liftIO $ assertEqual "John4: The include file contents should be the journal files" expectedJohn4Contents actualJohn4Contents
+
+        let expectedJane7Contents = includePreamble <> "\n"
+              <> "!include 3-journal/2018/2018-12-30.journal\n"
+        actualJane7Contents <- liftIO $ readTextFile jane7
+        liftIO $ assertEqual "Jane7: The include file contents should be the journal files" expectedJane7Contents actualJane7Contents
+
+     )
+  )
+
+tests = TestList [testDirOrPwd, testExtraIncludesForFile, testIncludesPrePost, testIncludesOpeningClosing,
+                  testHiddenFiles, testFilterPaths, testWriteIncludeFiles]
diff --git a/test/CSVImport/Unit.hs b/test/CSVImport/Unit.hs
new file mode 100644
--- /dev/null
+++ b/test/CSVImport/Unit.hs
@@ -0,0 +1,411 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+
+module CSVImport.Unit where
+
+import Test.HUnit
+import Turtle
+import Prelude hiding (FilePath)
+import qualified Data.Map.Strict as Map
+import qualified Control.Foldl as Fold
+import qualified Data.Text as T
+import qualified CSVImport.Integration
+import Control.Concurrent.STM
+
+import TestHelpers
+import Hledger.MakeItSo.Common
+
+groupedJaneBogart :: Map.Map FilePath [FilePath]
+groupedJaneBogart = [
+  ("./import/jane/bogartbank/checking/2018-include.journal",
+   ["import/jane/bogartbank/checking/3-journal/2018/2018-12-30.journal"]),
+  ("./import/jane/bogartbank/checking/2019-include.journal",
+   ["import/jane/bogartbank/checking/3-journal/2019/2019-01-30.journal"]),
+  ("./import/jane/bogartbank/savings/2017-include.journal",
+   ["import/jane/bogartbank/savings/3-journal/2017/2017-12-30.journal"]),
+  ("./import/jane/bogartbank/savings/2018-include.journal",
+   ["import/jane/bogartbank/savings/3-journal/2018/2018-01-30.journal"])]
+
+groupedJaneOther :: Map.Map FilePath [FilePath]
+groupedJaneOther = [
+  ("./import/jane/otherbank/creditcard/2017-include.journal",
+   ["import/jane/otherbank/creditcard/3-journal/2017/2017-12-30.journal"]),
+  ("./import/jane/otherbank/creditcard/2018-include.journal",
+   ["import/jane/otherbank/creditcard/3-journal/2018/2018-01-30.journal"]),
+  ("./import/jane/otherbank/investments/2018-include.journal",
+   ["import/jane/otherbank/investments/3-journal/2018/2018-12-30.journal"]),
+  ("./import/jane/otherbank/investments/2019-include.journal",
+   ["import/jane/otherbank/investments/3-journal/2019/2019-01-30.journal"])]
+
+groupedJohnBogart :: Map.Map FilePath [FilePath]
+groupedJohnBogart = [
+  ("./import/john/bogartbank/checking/2018-include.journal",
+   ["import/john/bogartbank/checking/3-journal/2018/2018-11-30.journal",
+    "import/john/bogartbank/checking/3-journal/2018/2018-10-30.journal",
+    "import/john/bogartbank/checking/3-journal/2018/2018-12-30.journal"]),
+  ("./import/john/bogartbank/checking/2019-include.journal",
+   ["import/john/bogartbank/checking/3-journal/2019/2019-01-30.journal",
+    "import/john/bogartbank/checking/3-journal/2019/2019-02-30.journal"]),
+  ("./import/john/bogartbank/savings/2017-include.journal",
+   ["import/john/bogartbank/savings/3-journal/2017/2017-11-30.journal",
+    "import/john/bogartbank/savings/3-journal/2017/2017-12-30.journal"]),
+  ("./import/john/bogartbank/savings/2018-include.journal",
+   ["import/john/bogartbank/savings/3-journal/2018/2018-02-30.journal",
+    "import/john/bogartbank/savings/3-journal/2018/2018-01-30.journal"])]
+
+groupedJohnOther :: Map.Map FilePath [FilePath]
+groupedJohnOther = [
+  ("./import/john/otherbank/creditcard/2017-include.journal",
+   ["import/john/otherbank/creditcard/3-journal/2017/2017-12-30.journal"]),
+  ("./import/john/otherbank/creditcard/2018-include.journal",
+   ["import/john/otherbank/creditcard/3-journal/2018/2018-01-30.journal"]),
+  ("./import/john/otherbank/investments/2018-include.journal",
+   ["import/john/otherbank/investments/3-journal/2018/2018-12-30.journal"]),
+  ("./import/john/otherbank/investments/2019-include.journal",
+   ["import/john/otherbank/investments/3-journal/2019/2019-01-30.journal"])]
+
+groupedIncludeFiles :: Map.Map FilePath [FilePath]
+groupedIncludeFiles = groupedJaneBogart <> groupedJaneOther <>
+                      groupedJohnBogart <> groupedJohnOther
+
+testYearsIncludeMap = TestCase (
+  do
+    let maps = allYearIncludeFiles groupedJohnOther
+    let yearGrouping = [("./import/john/otherbank/creditcard/all-years.journal",
+                         ["./import/john/otherbank/creditcard/2017-include.journal",
+                          "./import/john/otherbank/creditcard/2018-include.journal"]),
+                        ("./import/john/otherbank/investments/all-years.journal",
+                         ["./import/john/otherbank/investments/2018-include.journal",
+                          "./import/john/otherbank/investments/2019-include.journal"])]
+    let expected = (groupedJohnOther, yearGrouping)
+    assertEqual "An augmented map with grouped years per level added" expected maps
+  )
+
+testYearsIncludeGrouping = TestCase (
+  do
+    let yearsMap = yearsIncludeMap (Map.keys groupedJohnOther)
+    let expected = [("./import/john/otherbank/creditcard/all-years.journal",
+                     ["./import/john/otherbank/creditcard/2017-include.journal",
+                      "./import/john/otherbank/creditcard/2018-include.journal"]),
+                    ("./import/john/otherbank/investments/all-years.journal",
+                     ["./import/john/otherbank/investments/2018-include.journal",
+                      "./import/john/otherbank/investments/2019-include.journal"])]
+    assertEqual "A basic map with grouped years per level" expected yearsMap
+  )
+
+testGroupIncludeFilesTinySet = TestCase (
+  do
+    let journals1 = [   "import/jane/bogartbank/savings/3-journals/2017/2017-12-30.journal"]
+    let expected1 = [("./import/jane/bogartbank/savings/2017-include.journal", journals1)] :: Map.Map FilePath [FilePath]
+    let expectedAllYears1 = [("./import/jane/bogartbank/savings/all-years.journal", ["./import/jane/bogartbank/savings/2017-include.journal"])]
+    let (group1, allYears1) = groupIncludeFiles journals1
+    assertEqual "groupIncludeFiles small allYears 1" expectedAllYears1 allYears1
+    assertEqual "groupIncludeFiles small set 1" expected1 group1
+
+    let journals2 = [(fst . head . Map.toList) expected1] :: [FilePath]
+    let expected2 = [("./import/jane/bogartbank/2017-include.journal", journals2)] :: Map.Map FilePath [FilePath]
+    let expectedAllYears2 = [("./import/jane/bogartbank/all-years.journal", ["./import/jane/bogartbank/2017-include.journal"])]
+    let (group2, allYears2) = groupIncludeFiles journals2
+    assertEqual "groupIncludeFiles small allYears 2" expectedAllYears2 allYears2
+    assertEqual "groupIncludeFiles small set 2" expected2 group2
+
+    let journals3 = [(fst . head . Map.toList) expected2] :: [FilePath]
+    let expected3 = [("./import/jane/2017-include.journal", journals3)] :: Map.Map FilePath [FilePath]
+    let expectedAllYears3 = [("./import/jane/all-years.journal", ["./import/jane/2017-include.journal"])]
+    let (group3, allYears3) = groupIncludeFiles journals3
+    assertEqual "groupIncludeFiles small allYears 3" expectedAllYears3 allYears3
+    assertEqual "groupIncludeFiles small set 3" expected3 group3
+  )
+
+testGroupIncludeFilesSmallSet = TestCase (
+  do
+    let (group1, allYears1) = groupIncludeFiles (toJournals inputJaneBogart)
+    let expectedAllYears1 = [
+          ("./import/jane/bogartbank/checking/all-years.journal",
+           ["./import/jane/bogartbank/checking/2018-include.journal",
+            "./import/jane/bogartbank/checking/2019-include.journal"]),
+          ("./import/jane/bogartbank/savings/all-years.journal",
+           ["./import/jane/bogartbank/savings/2017-include.journal",
+            "./import/jane/bogartbank/savings/2018-include.journal"])]
+
+    assertEqual "groupIncludeFiles Jane AllYears 1" expectedAllYears1 allYears1
+    assertEqual "groupIncludeFiles Jane 1" groupedJaneBogart group1
+
+    let (group2, allYears2) = groupIncludeFiles (Map.keys group1)
+    let expectedAllYears2 = [
+          ("./import/jane/bogartbank/all-years.journal",
+           ["./import/jane/bogartbank/2017-include.journal",
+            "./import/jane/bogartbank/2018-include.journal",
+            "./import/jane/bogartbank/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 2" expectedAllYears2 allYears2
+    let expectedGroup2 = [
+          ("./import/jane/bogartbank/2017-include.journal",
+           ["./import/jane/bogartbank/savings/2017-include.journal"]),
+          ("./import/jane/bogartbank/2018-include.journal",
+           ["./import/jane/bogartbank/checking/2018-include.journal",
+            "./import/jane/bogartbank/savings/2018-include.journal"]),
+          ("./import/jane/bogartbank/2019-include.journal",
+           ["./import/jane/bogartbank/checking/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane 2" expectedGroup2 group2
+
+    let (group3, allYears3) = groupIncludeFiles (Map.keys group2)
+    let expectedAllYears3 = [
+          ("./import/jane/all-years.journal",
+           ["./import/jane/2017-include.journal",
+            "./import/jane/2018-include.journal",
+            "./import/jane/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 3" expectedAllYears3 allYears3
+    let expectedGroup3 = [
+          ("./import/jane/2017-include.journal",
+           ["./import/jane/bogartbank/2017-include.journal"]),
+          ("./import/jane/2018-include.journal",
+           ["./import/jane/bogartbank/2018-include.journal"]),
+          ("./import/jane/2019-include.journal",
+           ["./import/jane/bogartbank/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane 3" expectedGroup3 group3
+
+    let (group4, allYears4) = groupIncludeFiles (Map.keys group3)
+    let expectedAllYears4 = [
+          ("./import/all-years.journal",
+           ["./import/2017-include.journal",
+            "./import/2018-include.journal",
+            "./import/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 4" expectedAllYears4 allYears4
+    let expectedGroup4 = [
+          ("./import/2017-include.journal",
+           ["./import/jane/2017-include.journal"]),
+          ("./import/2018-include.journal",
+           ["./import/jane/2018-include.journal"]),
+          ("./import/2019-include.journal",
+           ["./import/jane/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane 4" expectedGroup4 group4
+
+    let (group5, allYears5) = groupIncludeFiles (Map.keys group4)
+    let expectedAllYears5 = [("./all-years.journal", ["./2017-include.journal", "./2018-include.journal", "./2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 5" expectedAllYears5 allYears5
+    let expectedGroup5 = [
+          ("./2017-include.journal",
+           ["./import/2017-include.journal"]),
+          ("./2018-include.journal",
+           ["./import/2018-include.journal"]),
+          ("./2019-include.journal",
+           ["./import/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane 5" expectedGroup5 group5
+ )
+
+testGroupIncludeFiles = TestCase (
+  do
+    let (group1, allYears1) = groupIncludeFiles journalFiles
+    let expectedAllYears1 = [
+          ("./import/jane/bogartbank/checking/all-years.journal",
+            ["./import/jane/bogartbank/checking/2018-include.journal",
+             "./import/jane/bogartbank/checking/2019-include.journal"]),
+          ("./import/jane/bogartbank/savings/all-years.journal",
+            ["./import/jane/bogartbank/savings/2017-include.journal",
+             "./import/jane/bogartbank/savings/2018-include.journal"]),
+          ("./import/jane/otherbank/creditcard/all-years.journal",
+            ["./import/jane/otherbank/creditcard/2017-include.journal",
+             "./import/jane/otherbank/creditcard/2018-include.journal"]),
+          ("./import/jane/otherbank/investments/all-years.journal",
+            ["./import/jane/otherbank/investments/2018-include.journal",
+             "./import/jane/otherbank/investments/2019-include.journal"]),
+          ("./import/john/bogartbank/checking/all-years.journal",
+            ["./import/john/bogartbank/checking/2018-include.journal",
+             "./import/john/bogartbank/checking/2019-include.journal"]),
+          ("./import/john/bogartbank/savings/all-years.journal",
+            ["./import/john/bogartbank/savings/2017-include.journal",
+             "./import/john/bogartbank/savings/2018-include.journal"]),
+          ("./import/john/otherbank/creditcard/all-years.journal",
+            ["./import/john/otherbank/creditcard/2017-include.journal",
+             "./import/john/otherbank/creditcard/2018-include.journal"]),
+          ("./import/john/otherbank/investments/all-years.journal",
+            ["./import/john/otherbank/investments/2018-include.journal",
+             "./import/john/otherbank/investments/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 1" expectedAllYears1 allYears1
+    assertEqual "groupIncludeFiles 1" groupedIncludeFiles group1
+
+    let (group2, allYears2) = groupIncludeFiles (Map.keys group1)
+    let expectedAllYears2 = [
+          ("./import/jane/bogartbank/all-years.journal",
+            ["./import/jane/bogartbank/2017-include.journal",
+             "./import/jane/bogartbank/2018-include.journal",
+             "./import/jane/bogartbank/2019-include.journal"]),
+          ("./import/jane/otherbank/all-years.journal",
+            ["./import/jane/otherbank/2017-include.journal",
+             "./import/jane/otherbank/2018-include.journal",
+             "./import/jane/otherbank/2019-include.journal"]),
+          ("./import/john/bogartbank/all-years.journal",
+            ["./import/john/bogartbank/2017-include.journal",
+             "./import/john/bogartbank/2018-include.journal",
+             "./import/john/bogartbank/2019-include.journal"]),
+          ("./import/john/otherbank/all-years.journal",
+            ["./import/john/otherbank/2017-include.journal",
+             "./import/john/otherbank/2018-include.journal",
+             "./import/john/otherbank/2019-include.journal"])]
+
+    assertEqual "groupIncludeFiles Jane AllYears 2" expectedAllYears2 allYears2
+    let expectedGroup2 = [("./import/jane/bogartbank/2017-include.journal",
+                           ["./import/jane/bogartbank/savings/2017-include.journal"]),
+                          ("./import/jane/bogartbank/2018-include.journal",
+                           ["./import/jane/bogartbank/checking/2018-include.journal",
+                            "./import/jane/bogartbank/savings/2018-include.journal"]),
+                          ("./import/jane/bogartbank/2019-include.journal",
+                           ["./import/jane/bogartbank/checking/2019-include.journal"]),
+
+                          ("./import/jane/otherbank/2017-include.journal",
+                           ["./import/jane/otherbank/creditcard/2017-include.journal"]),
+                          ("./import/jane/otherbank/2018-include.journal",
+                           ["./import/jane/otherbank/creditcard/2018-include.journal",
+                            "./import/jane/otherbank/investments/2018-include.journal"]),
+                          ("./import/jane/otherbank/2019-include.journal",
+                           ["./import/jane/otherbank/investments/2019-include.journal"]),
+
+                          ("./import/john/bogartbank/2017-include.journal",
+                           ["./import/john/bogartbank/savings/2017-include.journal"]),
+                          ("./import/john/bogartbank/2018-include.journal",
+                           ["./import/john/bogartbank/checking/2018-include.journal",
+                            "./import/john/bogartbank/savings/2018-include.journal"]),
+                          ("./import/john/bogartbank/2019-include.journal",
+                           ["./import/john/bogartbank/checking/2019-include.journal"]),
+
+                          ("./import/john/otherbank/2017-include.journal",
+                           ["./import/john/otherbank/creditcard/2017-include.journal"]),
+                          ("./import/john/otherbank/2018-include.journal",
+                           ["./import/john/otherbank/creditcard/2018-include.journal",
+                            "./import/john/otherbank/investments/2018-include.journal"]),
+                          ("./import/john/otherbank/2019-include.journal",
+                           ["./import/john/otherbank/investments/2019-include.journal"])]
+    assertEqual "groupIncludeFiles 2 - diff 1" [] (expectedGroup2 Map.\\ group2)
+    assertEqual "groupIncludeFiles 2 - diff 2" [] (group2 Map.\\ expectedGroup2)
+    assertEqual "groupIncludeFiles 2" expectedGroup2 group2
+
+    let (group3, allYears3) = groupIncludeFiles (Map.keys group2)
+    let expectedAllYears3 = [
+          ("./import/jane/all-years.journal",
+            ["./import/jane/2017-include.journal",
+             "./import/jane/2018-include.journal",
+             "./import/jane/2019-include.journal"]),
+          ("./import/john/all-years.journal",
+            ["./import/john/2017-include.journal",
+             "./import/john/2018-include.journal",
+             "./import/john/2019-include.journal"])]
+
+    assertEqual "groupIncludeFiles Jane AllYears 3" expectedAllYears3 allYears3
+    let expectedGroup3 = [("./import/jane/2017-include.journal",
+                           ["./import/jane/bogartbank/2017-include.journal",
+                            "./import/jane/otherbank/2017-include.journal"]),
+                          ("./import/jane/2018-include.journal",
+                           ["./import/jane/bogartbank/2018-include.journal",
+                            "./import/jane/otherbank/2018-include.journal"]),
+                          ("./import/jane/2019-include.journal",
+                           ["./import/jane/bogartbank/2019-include.journal",
+                            "./import/jane/otherbank/2019-include.journal"]),
+                          ("./import/john/2017-include.journal",
+                           ["./import/john/bogartbank/2017-include.journal",
+                            "./import/john/otherbank/2017-include.journal"]),
+                          ("./import/john/2018-include.journal",
+                           ["./import/john/bogartbank/2018-include.journal",
+                            "./import/john/otherbank/2018-include.journal"]),
+                          ("./import/john/2019-include.journal",
+                           ["./import/john/bogartbank/2019-include.journal",
+                            "./import/john/otherbank/2019-include.journal"])]
+    assertEqual "groupIncludeFiles 3 - diff 1" [] (expectedGroup3 Map.\\ group3)
+    assertEqual "groupIncludeFiles 3 - diff 2" [] (group3 Map.\\ expectedGroup3)
+    assertEqual "groupIncludeFiles 3" expectedGroup3 group3
+
+    let (group4, allYears4) = groupIncludeFiles (Map.keys group3)
+    let expectedAllYears4 = [
+          ("./import/all-years.journal",
+            ["./import/2017-include.journal",
+             "./import/2018-include.journal",
+             "./import/2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 4" expectedAllYears4 allYears4
+    let expectedGroup4 = [("./import/2017-include.journal",
+                           ["./import/jane/2017-include.journal",
+                            "./import/john/2017-include.journal"]),
+                          ("./import/2018-include.journal",
+                           ["./import/jane/2018-include.journal",
+                            "./import/john/2018-include.journal"]),
+                          ("./import/2019-include.journal",
+                           ["./import/jane/2019-include.journal",
+                            "./import/john/2019-include.journal"])]
+    assertEqual "groupIncludeFiles 4 - diff 1" [] (expectedGroup4 Map.\\ group4)
+    assertEqual "groupIncludeFiles 4 - diff 2" [] (group4 Map.\\ expectedGroup4)
+    assertEqual "groupIncludeFiles 4" expectedGroup4 group4
+
+    let (group5, allYears5) = groupIncludeFiles (Map.keys group4)
+    let expectedAllYears5 = [("./all-years.journal", ["./2017-include.journal", "./2018-include.journal", "./2019-include.journal"])]
+    assertEqual "groupIncludeFiles Jane AllYears 5" expectedAllYears5 allYears5
+    let expectedGroup5 = [
+          ("./2017-include.journal",
+           ["./import/2017-include.journal"]),
+          ("./2018-include.journal",
+           ["./import/2018-include.journal"]),
+          ("./2019-include.journal",
+           ["./import/2019-include.journal"])]
+    assertEqual "groupIncludeFiles 5 - diff 1" [] (expectedGroup5 Map.\\ group5)
+    assertEqual "groupIncludeFiles 5 - diff 2" [] (group5 Map.\\ expectedGroup5)
+    assertEqual "groupIncludeFiles 5" expectedGroup5 group5
+  )
+
+testRelativeToBase = TestCase (
+  do
+    let expected = "file1.journal"
+    let relativeWithTrailingSlash = relativeToBase' "./base/dir/" "./base/dir/file1.journal"
+    assertEqual "relative base dir with trailing slash" expected relativeWithTrailingSlash
+
+    let relativeNoTrailingSlash = relativeToBase' "./base/dir" "./base/dir/file1.journal"
+    assertEqual "relative base dir without a trailing slash" expected relativeNoTrailingSlash
+
+    let absoluteWithTrailingSlash = relativeToBase' "/base/dir/" "/base/dir/file1.journal"
+    assertEqual "absolute base dir with trailing slash" expected absoluteWithTrailingSlash
+
+    let absoluteNoTrailingSlash = relativeToBase' "/base/dir" "/base/dir/file1.journal"
+    assertEqual "absolute base dir without a trailing slash" expected absoluteNoTrailingSlash
+
+    let mismatch = relativeToBase' "/base/dir" "/unrelated/dir/file1.journal"
+    assertEqual "A basedir with no shared prefix should return the supplied file unchanged" "/unrelated/dir/file1.journal" mismatch
+  )
+
+testToIncludeLine = TestCase (
+  do
+    let expected = "!include file1.journal"
+    let relativeWithTrailingSlash = toIncludeLine "./base/dir/" "./base/dir/file1.journal"
+    assertEqual "Include line - relative base dir with trailing slash" expected relativeWithTrailingSlash
+
+    let relativeNoTrailingSlash = toIncludeLine "./base/dir" "./base/dir/file1.journal"
+    assertEqual "Include line - relative base dir without a trailing slash" expected relativeNoTrailingSlash
+
+    let absoluteWithTrailingSlash = toIncludeLine "/base/dir/" "/base/dir/file1.journal"
+    assertEqual "Include line - absolute base dir with trailing slash" expected absoluteWithTrailingSlash
+
+    let absoluteNoTrailingSlash = toIncludeLine "/base/dir" "/base/dir/file1.journal"
+    assertEqual "Include line - absolute base dir without a trailing slash" expected absoluteNoTrailingSlash
+  )
+
+testToIncludeFiles = TestCase (
+  do
+    let expected = [
+          ("./import/john/bogartbank/checking/2018-include.journal",
+           "### Generated by hledger-makeitso - DO NOT EDIT ###\n\n" <>
+           "!include import/john/bogartbank/checking/3-journal/2018/2018-10-30.journal\n" <>
+           "!include import/john/bogartbank/checking/3-journal/2018/2018-11-30.journal\n" <>
+           "!include import/john/bogartbank/checking/3-journal/2018/2018-12-30.journal\n"),
+          ("./import/john/bogartbank/checking/2019-include.journal",
+           "### Generated by hledger-makeitso - DO NOT EDIT ###\n\n" <>
+           "!include import/john/bogartbank/checking/3-journal/2019/2019-01-30.journal\n" <>
+           "!include import/john/bogartbank/checking/3-journal/2019/2019-02-30.journal\n"),
+          ("./import/john/bogartbank/savings/2017-include.journal",
+           "### Generated by hledger-makeitso - DO NOT EDIT ###\n\n" <>
+           "!include import/john/bogartbank/savings/3-journal/2017/2017-11-30.journal\n" <>
+           "!include import/john/bogartbank/savings/3-journal/2017/2017-12-30.journal\n"),
+          ("./import/john/bogartbank/savings/2018-include.journal",
+           "### Generated by hledger-makeitso - DO NOT EDIT ###\n\n" <>
+           "!include import/john/bogartbank/savings/3-journal/2018/2018-01-30.journal\n" <>
+           "!include import/john/bogartbank/savings/3-journal/2018/2018-02-30.journal\n")]
+
+    ch <- liftIO newTChanIO
+    txt <- single $ toIncludeFiles (defaultOpts ".") ch groupedJohnBogart
+    assertEqual "Convert a grouped map of paths, to a map with text contents for each file" expected txt)
+
+tests = TestList [testYearsIncludeMap, testYearsIncludeGrouping, testGroupIncludeFilesTinySet, testGroupIncludeFilesSmallSet, testGroupIncludeFiles, testRelativeToBase, testToIncludeLine, testToIncludeFiles]
diff --git a/test/Common/Unit.hs b/test/Common/Unit.hs
new file mode 100644
--- /dev/null
+++ b/test/Common/Unit.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+
+module Common.Unit where
+
+import Test.HUnit
+import Turtle
+import Prelude hiding (FilePath)
+
+import TestHelpers
+import Hledger.MakeItSo.Common
+
+testShowCmdArgs = TestCase (
+  do
+    let options = ["--number", "/tmp/file with spaces"]
+    let expected = "--number '/tmp/file with spaces'"
+    let actual = showCmdArgs options
+    assertEqual "Convert command-line arguments to text" expected actual)
+
+tests = TestList [testShowCmdArgs]
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+
+module Main where
+
+import Test.HUnit
+import Turtle
+import Prelude hiding (FilePath)
+
+import qualified Common.Unit
+import qualified CSVImport.Unit
+import qualified CSVImport.Integration
+
+tests = TestList [Common.Unit.tests, CSVImport.Unit.tests, CSVImport.Integration.tests]
+
+main :: IO Counts
+main = do
+  counts <- runTestTT tests
+  if (errors counts > 0 || failures counts > 0)
+    then exit $ ExitFailure 1
+    else return counts
diff --git a/test/TestHelpers.hs b/test/TestHelpers.hs
new file mode 100644
--- /dev/null
+++ b/test/TestHelpers.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+
+module TestHelpers where
+
+import Test.HUnit
+import Turtle
+import Prelude hiding (FilePath)
+import qualified Data.Map.Strict as Map
+import qualified Control.Foldl as Fold
+import qualified Data.Text as T
+import qualified Data.List as List (sort)
+
+import Hledger.MakeItSo.Import.Types
+import Hledger.MakeItSo.Common
+
+inputJohnBogart = [
+  "import/john/bogartbank/savings/1-in/2017/2017-11-30.csv",
+  "import/john/bogartbank/savings/1-in/2017/2017-12-30.csv",
+  "import/john/bogartbank/savings/1-in/2018/2018-02-30.csv",
+  "import/john/bogartbank/savings/1-in/2018/2018-01-30.csv",
+  "import/john/bogartbank/checking/1-in/2018/2018-11-30.csv",
+  "import/john/bogartbank/checking/1-in/2018/2018-10-30.csv",
+  "import/john/bogartbank/checking/1-in/2018/2018-12-30.csv",
+  "import/john/bogartbank/checking/1-in/2019/2019-01-30.csv",
+  "import/john/bogartbank/checking/1-in/2019/2019-02-30.csv"] :: [FilePath]
+
+inputJohnOther = [
+  "import/john/otherbank/creditcard/1-in/2017/2017-12-30.csv",
+  "import/john/otherbank/creditcard/1-in/2018/2018-01-30.csv",
+  "import/john/otherbank/investments/1-in/2018/2018-12-30.csv",
+  "import/john/otherbank/investments/1-in/2019/2019-01-30.csv"] :: [FilePath]
+
+inputJaneBogart = [
+  "import/jane/bogartbank/savings/1-in/2017/2017-12-30.csv",
+  "import/jane/bogartbank/savings/1-in/2018/2018-01-30.csv",
+  "import/jane/bogartbank/checking/1-in/2018/2018-12-30.csv",
+  "import/jane/bogartbank/checking/1-in/2019/2019-01-30.csv"] :: [FilePath]
+
+inputJaneOther = [
+  "import/jane/otherbank/creditcard/1-in/2017/2017-12-30.csv",
+  "import/jane/otherbank/creditcard/1-in/2018/2018-01-30.csv",
+  "import/jane/otherbank/investments/1-in/2018/2018-12-30.csv",
+  "import/jane/otherbank/investments/1-in/2019/2019-01-30.csv"] :: [FilePath]
+
+inputFiles = inputJohnBogart <> inputJohnOther <> inputJaneBogart <> inputJaneOther
+
+journalFiles = toJournals inputFiles :: [FilePath]
+extraFiles = ["import/john/bogartbank/savings/2017-opening.journal"] :: [FilePath]
+hiddenFiles = [".hiddenfile", "checking/.DS_Store", "import/john/bogartbank/savings/1-in/.anotherhiddenfile", "import/john/bogartbank/checking/1-in/2018/.hidden"] :: [FilePath]
+
+defaultOpts :: FilePath -> ImportOptions
+defaultOpts bd = ImportOptions bd False
+
+toJournals :: [FilePath] -> [FilePath]
+toJournals = map (changePathAndExtension "3-journal" "journal")
+
+touchAll :: [FilePath] -> Shell ()
+touchAll = foldl (\acc file -> acc <> superTouch file) (return ())
+
+superTouch :: FilePath -> Shell ()
+superTouch file = do
+  mktree $ directory file
+  touch file
