Showing preview only (1,136K chars total). Download the full file or copy to clipboard to get everything.
Repository: idank/explainshell
Branch: master
Commit: cfa685e7ada2
Files: 69
Total size: 1.1 MB
Directory structure:
gitextract_h90spds8/
├── .github/
│ └── workflows/
│ ├── build-docker-image.yml
│ ├── build-test.yml
│ └── lint-docker.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── dump/
│ └── explainshell/
│ ├── classifier.bson
│ └── system.indexes.bson
├── explainshell/
│ ├── __init__.py
│ ├── algo/
│ │ ├── __init__.py
│ │ ├── classifier.py
│ │ └── features.py
│ ├── config.py
│ ├── errors.py
│ ├── fixer.py
│ ├── helpconstants.py
│ ├── manager.py
│ ├── manpage.py
│ ├── matcher.py
│ ├── options.py
│ ├── store.py
│ ├── util.py
│ └── web/
│ ├── __init__.py
│ ├── debugviews.py
│ ├── helpers.py
│ ├── static/
│ │ ├── css/
│ │ │ ├── bootstrap-responsive.css
│ │ │ ├── bootstrap.css
│ │ │ ├── es.css
│ │ │ └── font-awesome.css
│ │ ├── font/
│ │ │ └── FontAwesome.otf
│ │ └── js/
│ │ ├── bootstrap.js
│ │ ├── d3.v3.js
│ │ ├── es.js
│ │ ├── jquery.js
│ │ └── underscore.js
│ ├── templates/
│ │ ├── about.html
│ │ ├── base.html
│ │ ├── debug.html
│ │ ├── errors/
│ │ │ ├── error.html
│ │ │ ├── missingmanpage.html
│ │ │ └── parsingerror.html
│ │ ├── explain.html
│ │ ├── index.html
│ │ ├── macros.html
│ │ ├── options.html
│ │ └── tagger.html
│ └── views.py
├── misc/
│ ├── crontab
│ ├── logrotate/
│ │ └── explainshell
│ ├── nginx/
│ │ └── explainshell.conf
│ ├── setup.bash
│ └── supervisord/
│ └── uwsgi.conf
├── requirements.txt
├── runserver.py
├── tests/
│ ├── __init__.py
│ ├── helpers.py
│ ├── test-fixer.py
│ ├── test-integration.py
│ ├── test-manager.py
│ ├── test-manpage.py
│ ├── test-matcher.py
│ └── test-options.py
└── tools/
├── dlgzlist
├── extractgzlist
├── shellbuiltins.py
└── w3mman2html.cgi
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build-docker-image.yml
================================================
name: Build & Publish Docker Image
on:
workflow_dispatch:
push:
branches:
- master
- main
permissions:
packages: write
contents: read
env:
IMAGE_NAME: ${{ github.repository }}
REGISTRY: ghcr.io
jobs:
build:
timeout-minutes: 15
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate Docker Metadata
id: meta
uses: docker/metadata-action@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
images: |
ghcr.io/${{ github.actor }}/${{ github.repository }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Set up QEMU To support build amd64 and arm64 images
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to the Github Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
id: docker_build
with:
push: ${{ github.event_name != 'pull_request' && ( github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' ) }}
context: .
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Output image, digest and metadata to summary
run: |
{
echo imageid: "${{ steps.docker_build.outputs.imageid }}"
echo digest: "${{ steps.docker_build.outputs.digest }}"
echo labels: "${{ steps.meta.outputs.labels }}"
echo tags: "${{ steps.meta.outputs.tags }}"
echo version: "${{ steps.meta.outputs.version }}"
} >> "$GITHUB_STEP_SUMMARY"
================================================
FILE: .github/workflows/build-test.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Build and Test Python Package
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
with:
python-version: "2.7"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Spin up MongoDB for running tests
uses: supercharge/mongodb-github-action@1.8.0
with:
mongodb-version: "5"
- name: Run nosetests
run: |
mongorestore dump/explainshell && mongorestore -d explainshell_tests dump/explainshell
make tests
================================================
FILE: .github/workflows/lint-docker.yml
================================================
name: "Lint Dockerfile"
on:
pull_request:
paths:
- "**/Dockerfile.*"
- "**/docker-compose.*"
branches:
- main
- master
workflow_dispatch:
jobs:
hadolint:
name: "Lint Docker with hadolint"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- name: run tflint
uses: reviewdog/action-hadolint@master
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-review
fail_on_error: "false"
level: warning
filter_mode: "nofilter" # Optional. Check all files, not just the diff
================================================
FILE: .gitignore
================================================
*.pyc
*.swp
.coverage
.vagrant
application.log
================================================
FILE: Dockerfile
================================================
FROM python:2.7
RUN sed -i 's|deb.debian.org/debian|archive.debian.org/debian|g' /etc/apt/sources.list \
&& sed -i 's|security.debian.org/debian-security|archive.debian.org/debian-security|g' /etc/apt/sources.list \
&& sed -i '/buster-updates/d' /etc/apt/sources.list \
&& apt-get update \
&& apt-get install man-db -y \
&& apt-get clean
ADD ./requirements.txt /tmp/requirements.txt
RUN pip install --upgrade pip \
&& python --version \
&& pip install -r /tmp/requirements.txt \
&& rm -rf ~/.cache/pip/*
ADD ./ /opt/webapp/
WORKDIR /opt/webapp
EXPOSE 5000
CMD ["make", "serve"]
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU 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 <http://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
<http://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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
================================================
FILE: Makefile
================================================
tests:
nosetests --exe --with-doctest tests/ explainshell/
serve:
python runserver.py
.PHONY: tests
================================================
FILE: README.md
================================================
# [explainshell.com](http://www.explainshell.com) - match command-line arguments to their help text
explainshell is a tool (with a web interface) capable of parsing man pages, extracting options and
explaining a given command-line by matching each argument to the relevant help text in the man page.
## How?
explainshell is built from the following components:
1. man page reader which converts a given man page from raw format to html (manpage.py)
2. classifier which goes through every paragraph in the man page and classifies
it as contains options or not (algo/classifier.py)
3. an options extractor that scans classified paragraphs and looks for options (options.py)
4. a storage backend that saves processed man pages to mongodb (store.py)
5. a matcher that walks the command's AST (parsed by [bashlex](https://github.com/idank/bashlex)) and contextually matches each node
to the relevant help text (matcher.py)
When querying explainshell, it:
1. parses the query into an AST
2. visits interesting nodes in the AST, such as:
- command nodes - these nodes represent a simple command
- shell related nodes - these nodes represent something the shell
interprets such as '|', '&&'
3. for every command node we check if we know how to explain the current program,
and then go through the rest of the tokens, trying to match each one to the
list of known options
4. returns a list of matches that are rendered with Flask
## Manpages
> [!IMPORTANT]
>
> explainshell is actively maintained in terms of keeping it healthy and functional -- issues are addressed, and the core remains stable.
>
> However, please note that the **manpages are outdated**. The previous system for generating them was unsustainable, and they haven’t been updated in some time. There are currently **no active plans** to revise this mechanism.
>
> If you're relying on manpages, be aware that they may not reflect the latest behavior. Contributions in this area are welcome but would require rethinking the documentation pipeline.
Right now explainshell.com contains the entire [archive of Ubuntu](http://manpages.ubuntu.com/). It's not
possible to directly add a missing man page to the live site (it might be in the future).
## Running explainshell locally
Setup a working environment that lets you run the web interface locally using docker:
```ShellSession
# download db dump
$ curl -L -o /tmp/dump.gz https://github.com/idank/explainshell/releases/download/db-dump/dump.gz
# Clone Repository
$ git clone https://github.com/idank/explainshell.git
# start containers, load man pages from dump
$ docker-compose build
$ docker-compose up
$ docker-compose exec -T db mongorestore --archive --gzip < /tmp/dump.gz
# run tests
$ docker-compose exec -T web make tests
..SSSSSSSSS.....................................................................
----------------------------------------------------------------------
Ran 80 tests in 0.041s
OK (SKIP=9)
# open http://localhost:5001 to view the ui
```
### Processing a man page
Use the manager to parse and save a gzipped man page in raw format:
```ShellSession
$ docker-compose exec -T web bash -c "PYTHONPATH=. python explainshell/manager.py --log info /usr/share/man/man1/echo.1.gz"
INFO:explainshell.store:creating store, db = 'explainshell_tests', host = 'mongodb://localhost'
INFO:explainshell.algo.classifier:train on 994 instances
INFO:explainshell.manager:handling manpage echo (from /tmp/es/manpages/1/echo.1.gz)
INFO:explainshell.store:looking up manpage in mapping with src 'echo'
INFO:explainshell.manpage:executing '/tmp/es/tools/w3mman2html.cgi local=%2Ftmp%2Fes%2Fmanpages%2F1%2Fecho.1.gz'
INFO:explainshell.algo.classifier:classified <paragraph 3, DESCRIPTION: '-n do not output the trailing newlin'> (0.991381) as an option paragraph
INFO:explainshell.algo.classifier:classified <paragraph 4, DESCRIPTION: '-e enable interpretation of backslash escape'> (0.996904) as an option paragraph
INFO:explainshell.algo.classifier:classified <paragraph 5, DESCRIPTION: '-E disable interpretation of backslash escapes (default'> (0.998640) as an option paragraph
INFO:explainshell.algo.classifier:classified <paragraph 6, DESCRIPTION: '--help display this help and exi'> (0.999215) as an option paragraph
INFO:explainshell.algo.classifier:classified <paragraph 7, DESCRIPTION: '--version'> (0.999993) as an option paragraph
INFO:explainshell.store:inserting mapping (alias) echo -> echo (52207a1fa9b52e42fb59df36) with score 10
successfully added echo
```
Note that if you've setup using the docker instructions above, echo will already be in the database.
================================================
FILE: docker-compose.yml
================================================
services:
db:
image: mongo
web:
build: .
command: make serve
environment:
- MONGO_URI=mongodb://db
- HOST_IP=0.0.0.0
volumes:
- .:/opt/webapp
ports:
- "5001:5000"
depends_on:
- db
================================================
FILE: explainshell/__init__.py
================================================
================================================
FILE: explainshell/algo/__init__.py
================================================
import explainshell.algo.features
import explainshell.util
================================================
FILE: explainshell/algo/classifier.py
================================================
import itertools, collections, logging
import nltk
import nltk.metrics
import nltk.classify
import nltk.classify.maxent
from explainshell import algo, config
logger = logging.getLogger(__name__)
def get_features(paragraph):
features = {}
ptext = paragraph.cleantext()
assert ptext
features['starts_with_hyphen'] = algo.features.starts_with_hyphen(ptext)
features['is_indented'] = algo.features.is_indented(ptext)
features['par_length'] = algo.features.par_length(ptext)
for w in ('=', '--', '[', '|', ','):
features['first_line_contains_%s' % w] = algo.features.first_line_contains(ptext, w)
features['first_line_length'] = algo.features.first_line_length(ptext)
features['first_line_word_count'] = algo.features.first_line_word_count(ptext)
features['is_good_section'] = algo.features.is_good_section(paragraph)
features['word_count'] = algo.features.word_count(ptext)
return features
class classifier(object):
'''classify the paragraphs of a man page as having command line options
or not'''
def __init__(self, store, algo, **classifier_args):
self.store = store
self.algo = algo
self.classifier_args = classifier_args
self.classifier = None
def train(self):
if self.classifier:
return
manpages = self.store.trainingset()
# flatten the manpages so we get a list of (manpage-name, paragraph)
def flatten_manpages(manpage):
l = []
for para in manpage.paragraphs:
l.append(para)
return l
paragraphs = itertools.chain(*[flatten_manpages(m) for m in manpages])
training = list(paragraphs)
negids = [p for p in training if not p.is_option]
posids = [p for p in training if p.is_option]
negfeats = [(get_features(p), False) for p in negids]
posfeats = [(get_features(p), True) for p in posids]
negcutoff = len(negfeats)*3/4
poscutoff = len(posfeats)*3/4
trainfeats = negfeats[:negcutoff] + posfeats[:poscutoff]
self.testfeats = negfeats[negcutoff:] + posfeats[poscutoff:]
logger.info('train on %d instances', len(trainfeats))
if self.algo == 'maxent':
c = nltk.classify.maxent.MaxentClassifier
elif self.algo == 'bayes':
c = nltk.classify.NaiveBayesClassifier
else:
raise ValueError('unknown classifier')
self.classifier = c.train(trainfeats, **self.classifier_args)
def evaluate(self):
self.train()
refsets = collections.defaultdict(set)
testsets = collections.defaultdict(set)
for i, (feats, label) in enumerate(self.testfeats):
refsets[label].add(i)
guess = self.classifier.prob_classify(feats)
observed = guess.max()
testsets[observed].add(i)
#if label != observed:
# print 'label:', label, 'observed:', observed, feats
print 'pos precision:', nltk.metrics.precision(refsets[True], testsets[True])
print 'pos recall:', nltk.metrics.recall(refsets[True], testsets[True])
print 'neg precision:', nltk.metrics.precision(refsets[False], testsets[False])
print 'neg recall:', nltk.metrics.recall(refsets[False], testsets[False])
print self.classifier.show_most_informative_features(10)
def classify(self, manpage):
self.train()
for item in manpage.paragraphs:
features = get_features(item)
guess = self.classifier.prob_classify(features)
option = guess.max()
certainty = guess.prob(option)
if option:
if certainty < config.CLASSIFIER_CUTOFF:
pass
else:
logger.info('classified %s (%f) as an option paragraph', item, certainty)
item.is_option = True
yield certainty, item
================================================
FILE: explainshell/algo/features.py
================================================
import re
def extract_first_line(paragraph):
'''
>>> extract_first_line('a b cd')
'a b'
>>> extract_first_line('a b cd')
'a b cd'
>>> extract_first_line(' a b cd')
'a b cd'
>>> extract_first_line(' a b cd')
'a b'
'''
lines = paragraph.splitlines()
first = lines[0].strip()
spaces = list(re.finditer(r'(\s+)', first))
# handle options that have their description in the first line by trying
# to treat it as two lines (looking at spaces between option and the rest
# of the text)
if spaces:
longest = max(spaces, key=lambda m: m.span()[1] - m.span()[0])
if longest and longest.start() > 1 and longest.end() - longest.start() > 1:
first = first[:longest.start()]
return first
def starts_with_hyphen(paragraph):
return paragraph.lstrip()[0] == '-'
def is_indented(paragraph):
return paragraph != paragraph.lstrip()
def par_length(paragraph):
return round(len(paragraph.strip()), -1) / 2
def first_line_contains(paragraph, what):
l = paragraph.splitlines()[0]
return what in l
def first_line_length(paragraph):
first = extract_first_line(paragraph)
return round(len(first), -1) / 2
def first_line_word_count(paragraph):
first = extract_first_line(paragraph)
splitted = [s for s in first.split() if len(s) > 1]
return round(len(splitted), -1)
def is_good_section(paragraph):
if not paragraph.section:
return False
s = paragraph.section.lower()
if 'options' in s:
return True
if s in ('description', 'function letters'):
return True
return False
def word_count(text):
return round(len(re.findall(r'\w+', text)), -1)
def has_bold(html):
return '<b>' in html
================================================
FILE: explainshell/config.py
================================================
import os
_currdir = os.path.dirname(os.path.dirname(__file__))
MANPAGEDIR = os.path.join(_currdir, 'manpages')
CLASSIFIER_CUTOFF = 0.7
TOOLSDIR = os.path.join(_currdir, 'tools')
MAN2HTML = os.path.join(TOOLSDIR, 'w3mman2html.cgi')
# host to pass into Flask's app.run.
HOST_IP = os.getenv('HOST_IP', False)
MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost')
DEBUG = True
LOGGING_DICT = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
},
},
'handlers': {
'console': {
'level' : 'INFO',
'class' : 'logging.StreamHandler',
'formatter': 'standard',
},
'file': {
'class': 'logging.FileHandler',
'level': 'INFO',
'formatter': 'standard',
'filename': 'application.log',
'mode': 'a',
},
},
'loggers': {
'explainshell': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False
}
}
}
================================================
FILE: explainshell/errors.py
================================================
class ProgramDoesNotExist(Exception):
pass
class EmptyManpage(Exception):
pass
================================================
FILE: explainshell/fixer.py
================================================
import textwrap, logging
from explainshell import util
class basefixer(object):
'''The base fixer class which other fixers inherit from.
Subclasses override the base methods in order to fix manpage content during
different parts of the parsing/classifying/saving process.'''
runbefore = []
runlast = False
def __init__(self, mctx):
self.mctx = mctx
self.run = True
self.logger = logging.getLogger(self.__class__.__name__)
def pre_get_raw_manpage(self):
pass
def pre_parse_manpage(self):
pass
def post_parse_manpage(self):
pass
def pre_classify(self):
pass
def post_classify(self):
pass
def post_option_extraction(self):
pass
def pre_add_manpage(self):
pass
fixerscls = []
fixerspriority = {}
class runner(object):
'''The runner coordinates the fixers.'''
def __init__(self, mctx):
self.mctx = mctx
self.fixers = [f(mctx) for f in fixerscls]
def disable(self, name):
before = len(self.fixers)
self.fixers = [f for f in self.fixers if f.__class__.__name__ != name]
if before == len(self.fixers):
raise ValueError('fixer %r not found' % name)
def _fixers(self):
return (f for f in self.fixers if f.run)
def pre_get_raw_manpage(self):
for f in self._fixers():
f.pre_get_raw_manpage()
def pre_parse_manpage(self):
for f in self._fixers():
f.pre_parse_manpage()
def post_parse_manpage(self):
for f in self._fixers():
f.post_parse_manpage()
def pre_classify(self):
for f in self._fixers():
f.pre_classify()
def post_classify(self):
for f in self._fixers():
f.post_classify()
def post_option_extraction(self):
for f in self._fixers():
f.post_option_extraction()
def pre_add_manpage(self):
for f in self._fixers():
f.pre_add_manpage()
def register(fixercls):
fixerscls.append(fixercls)
for f in fixercls.runbefore:
if not hasattr(f, '_parents'):
f._parents = []
f._parents.append(fixercls)
return fixercls
@register
class bulletremover(basefixer):
'''remove list bullets from paragraph start, see mysqlslap.1'''
def post_parse_manpage(self):
toremove = []
for i, p in enumerate(self.mctx.manpage.paragraphs):
try:
idx = p.text.index('\xc2\xb7')
p.text = p.text[:idx] + p.text[idx+2:]
if not p.text.strip():
toremove.append(i)
except ValueError:
pass
for i in reversed(toremove):
del self.mctx.manpage.paragraphs[i]
@register
class leadingspaceremover(basefixer):
'''go over all known option paragraphs and remove their leading spaces
by the amount of spaces in the first line'''
def post_option_extraction(self):
for i, p in enumerate(self.mctx.manpage.options):
text = self._removewhitespace(p.text)
p.text = text
def _removewhitespace(self, text):
'''
>>> f = leadingspaceremover(None)
>>> f._removewhitespace(' a\\n b ')
'a\\n b'
>>> f._removewhitespace('\\t a\\n\\t \\tb')
'a\\n\\tb'
'''
return textwrap.dedent(text).rstrip()
@register
class tarfixer(basefixer):
def __init__(self, *args):
super(tarfixer, self).__init__(*args)
self.run = self.mctx.name == 'tar'
def pre_add_manpage(self):
self.mctx.manpage.partialmatch = True
@register
class paragraphjoiner(basefixer):
runbefore = [leadingspaceremover]
maxdistance = 5
def post_option_extraction(self):
options = [p for p in self.mctx.manpage.paragraphs if p.is_option]
self._join(self.mctx.manpage.paragraphs, options)
def _join(self, paragraphs, options):
def _paragraphsbetween(op1, op2):
assert op1.idx < op2.idx
r = []
start = None
for i, p in enumerate(paragraphs):
if op1.idx < p.idx < op2.idx:
if not r:
start = i
r.append(p)
return r, start
totalmerged = 0
for curr, next in util.pairwise(options):
between, start = _paragraphsbetween(curr, next)
if curr.section == next.section and 1 <= len(between) < self.maxdistance:
self.logger.info('merging paragraphs %d through %d (inclusive)', curr.idx, next.idx-1)
newdesc = [curr.text.rstrip()]
newdesc.extend([p.text.rstrip() for p in between])
curr.text = '\n\n'.join(newdesc)
del paragraphs[start:start+len(between)]
totalmerged += len(between)
return totalmerged
@register
class optiontrimmer(basefixer):
runbefore = [paragraphjoiner]
d = {'git-rebase' : (50, -1)}
def __init__(self, mctx):
super(optiontrimmer, self).__init__(mctx)
self.run = self.mctx.name in self.d
def post_classify(self):
start, end = self.d[self.mctx.name]
classifiedoptions = [p for p in self.mctx.manpage.paragraphs if p.is_option]
assert classifiedoptions
if end == -1:
end = classifiedoptions[-1].idx
else:
assert start > end
for p in classifiedoptions:
if not (start <= p.idx <= end):
p.is_option = False
self.logger.info('removing option %r', p)
def _parents(fixercls):
p = getattr(fixercls, '_parents', [])
last = fixercls.runlast
if last and p:
raise ValueError("%s can't be last and also run before someone else" % fixercls.__name__)
if last:
return [f for f in fixerscls if f is not fixercls]
return p
fixerscls = util.toposorted(fixerscls, _parents)
================================================
FILE: explainshell/helpconstants.py
================================================
# -*- coding: utf-8 -*-
import textwrap
NOSYNOPSIS = 'no synopsis found'
PIPELINES = textwrap.dedent(''' <b>Pipelines</b>
A <u>pipeline</u> is a sequence of one or more commands separated by one of the control operators <b>|</b> or <b>|&</b>. The
format for a pipeline is:
[<b>time</b> [<b>-p</b>]] [ ! ] <u>command</u> [ [<b>|</b>⎪<b>|&</b>] <u>command2</u> ... ]
The standard output of <u>command</u> is connected via a pipe to the standard input of <u>command2</u>. This
connection is performed before any redirections specified by the command (see <b>REDIRECTION</b> below). If <b>|&</b>
is used, the standard error of <u>command</u> is connected to <u>command2</u>'s standard input through the pipe; it is
shorthand for <b>2>&1</b> <b>|</b>. This implicit redirection of the standard error is performed after any
redirections specified by the command.
The return status of a pipeline is the exit status of the last command, unless the <b>pipefail</b> option is
enabled. If <b>pipefail</b> is enabled, the pipeline's return status is the value of the last (rightmost)
command to exit with a non-zero status, or zero if all commands exit successfully. If the reserved word
<b>!</b> precedes a pipeline, the exit status of that pipeline is the logical negation of the exit status as
described above. The shell waits for all commands in the pipeline to terminate before returning a value.
If the <b>time</b> reserved word precedes a pipeline, the elapsed as well as user and system time consumed by
its execution are reported when the pipeline terminates. The <b>-p</b> option changes the output format to that
specified by POSIX. When the shell is in <u>posix</u> <u>mode</u>, it does not recognize <b>time</b> as a reserved word if
the next token begins with a `-'. The <b>TIMEFORMAT</b> variable may be set to a format string that specifies
how the timing information should be displayed; see the description of <b>TIMEFORMAT</b> under <b>Shell</b> <b>Variables</b>
below.
When the shell is in <u>posix</u> <u>mode</u>, <b>time</b> may be followed by a newline. In this case, the shell displays the
total user and system time consumed by the shell and its children. The <b>TIMEFORMAT</b> variable may be used
to specify the format of the time information.
Each command in a pipeline is executed as a separate process (i.e., in a subshell).''')
OPSEMICOLON = textwrap.dedent(''' Commands separated by a <b>;</b> are executed sequentially; the shell waits for each command to terminate in turn. The
return status is the exit status of the last command executed.''')
OPBACKGROUND = textwrap.dedent(''' If a command is terminated by the control operator <b>&</b>, the shell executes the command in the <u>background</u> in
a subshell. The shell does not wait for the command to finish, and the return status is 0.''')
OPANDOR = textwrap.dedent(''' AND and OR lists are sequences of one of more pipelines separated by the <b>&&</b> and <b>||</b> control operators,
respectively. AND and OR lists are executed with left associativity. An AND list has the form
<u>command1</u> <b>&&</b> <u>command2</u>
<u>command2</u> is executed if, and only if, <u>command1</u> returns an exit status of zero.
An OR list has the form
<u>command1</u> <b>||</b> <u>command2</u>
<u>command2</u> is executed if and only if <u>command1</u> returns a non-zero exit status. The return status of AND
and OR lists is the exit status of the last command executed in the list.''')
OPERATORS = {';' : OPSEMICOLON, '&' : OPBACKGROUND, '&&' : OPANDOR, '||' : OPANDOR}
REDIRECTION = textwrap.dedent(''' Before a command is executed, its input and output may be <u>redirected</u> using a special notation interpreted
by the shell. Redirection may also be used to open and close files for the current shell execution
environment. The following redirection operators may precede or appear anywhere within a <u>simple</u> <u>command</u>
or may follow a <u>command</u>. Redirections are processed in the order they appear, from left to right.''')
REDIRECTING_INPUT = textwrap.dedent(''' <b>Redirecting</b> <b>Input</b>
Redirection of input causes the file whose name results from the expansion of <u>word</u> to be opened for
reading on file descriptor <u>n</u>, or the standard input (file descriptor 0) if <u>n</u> is not specified.
The general format for redirecting input is:
[<u>n</u>]<b><</b><u>word</u>''')
REDIRECTING_OUTPUT = textwrap.dedent(''' <b>Redirecting</b> <b>Output</b>
Redirection of output causes the file whose name results from the expansion of <u>word</u> to be opened for
writing on file descriptor <u>n</u>, or the standard output (file descriptor 1) if <u>n</u> is not specified. If the
file does not exist it is created; if it does exist it is truncated to zero size.
The general format for redirecting output is:
[<u>n</u>]<b>></b><u>word</u>
If the redirection operator is <b>></b>, and the <b>noclobber</b> option to the <b>set</b> builtin has been enabled, the
redirection will fail if the file whose name results from the expansion of <u>word</u> exists and is a regular
file. If the redirection operator is <b>>|</b>, or the redirection operator is <b>></b> and the <b>noclobber</b> option to
the <b>set</b> builtin command is not enabled, the redirection is attempted even if the file named by <u>word</u>
exists.''')
APPENDING_REDIRECTED_OUTPUT = textwrap.dedent(''' <b>Appending</b> <b>Redirected</b> <b>Output</b>
Redirection of output in this fashion causes the file whose name results from the expansion of <u>word</u> to be
opened for appending on file descriptor <u>n</u>, or the standard output (file descriptor 1) if <u>n</u> is not
specified. If the file does not exist it is created.
The general format for appending output is:
[<u>n</u>]<b>>></b><u>word</u>''')
REDIRECTING_OUTPUT_ERROR = textwrap.dedent(''' <b>Redirecting</b> <b>Standard</b> <b>Output</b> <b>and</b> <b>Standard</b> <b>Error</b>
This construct allows both the standard output (file descriptor 1) and the standard error output (file
descriptor 2) to be redirected to the file whose name is the expansion of <u>word</u>.
There are two formats for redirecting standard output and standard error:
<b>&></b><u>word</u>
and
<b>>&</b><u>word</u>
Of the two forms, the first is preferred. This is semantically equivalent to
<b>></b><u>word</u> 2<b>>&</b>1''')
APPENDING_OUTPUT_ERROR = textwrap.dedent(''' <b>Appending</b> <b>Standard</b> <b>Output</b> <b>and</b> <b>Standard</b> <b>Error</b>
This construct allows both the standard output (file descriptor 1) and the standard error output (file
descriptor 2) to be appended to the file whose name is the expansion of <u>word</u>.
The format for appending standard output and standard error is:
<b>&>></b><u>word</u>
This is semantically equivalent to
<b>>></b><u>word</u> 2<b>>&</b>1''')
HERE_DOCUMENTS = textwrap.dedent(''' <b>Here</b> <b>Documents</b>
This type of redirection instructs the shell to read input from the current source until a line
containing only <u>delimiter</u> (with no trailing blanks) is seen. All of the lines read up to that point are
then used as the standard input for a command.
The format of here-documents is:
<b><<</b>[<b>-</b>]<u>word</u>
<u>here-document</u>
<u>delimiter</u>
No parameter expansion, command substitution, arithmetic expansion, or pathname expansion is performed on
<u>word</u>. If any characters in <u>word</u> are quoted, the <u>delimiter</u> is the result of quote removal on <u>word</u>, and
the lines in the here-document are not expanded. If <u>word</u> is unquoted, all lines of the here-document are
subjected to parameter expansion, command substitution, and arithmetic expansion. In the latter case,
the character sequence <b>\<newline></b> is ignored, and <b>\</b> must be used to quote the characters <b>\</b>, <b>$</b>, and <b>`</b>.
If the redirection operator is <b><<-</b>, then all leading tab characters are stripped from input lines and the
line containing <u>delimiter</u>. This allows here-documents within shell scripts to be indented in a natural
fashion.
<b>Here</b> <b>Strings</b>
A variant of here documents, the format is:
<b><<<</b><u>word</u>
The <u>word</u> is expanded and supplied to the command on its standard input.''')
REDIRECTION_KIND = {'<' : REDIRECTING_INPUT,
'>' : REDIRECTING_OUTPUT,
'>>' : APPENDING_REDIRECTED_OUTPUT,
'&>' : REDIRECTING_OUTPUT_ERROR,
'>&' : REDIRECTING_OUTPUT_ERROR,
'&>>' : APPENDING_OUTPUT_ERROR,
'<<' : HERE_DOCUMENTS,
'<<<' : HERE_DOCUMENTS}
ASSIGNMENT = textwrap.dedent(''' A <u>variable</u> may be assigned to by a statement of the form
<u>name</u>=[<u>value</u>]
If <u>value</u> is not given, the variable is assigned the null string. All <u>values</u> undergo tilde expansion,
parameter and variable expansion, command substitution, arithmetic expansion, and quote removal (see
<b>EXPANSION</b> below). If the variable has its <b>integer</b> attribute set, then <u>value</u> is evaluated as an
arithmetic expression even if the $((...)) expansion is not used (see <b>Arithmetic</b> <b>Expansion</b> below). Word
splitting is not performed, with the exception of <b>"$@"</b> as explained below under <b>Special</b> <b>Parameters</b>.
Pathname expansion is not performed. Assignment statements may also appear as arguments to the <b>alias</b>,
<b>declare</b>, <b>typeset</b>, <b>export</b>, <b>readonly</b>, and <b>local</b> builtin commands.
In the context where an assignment statement is assigning a value to a shell variable or array index, the
+= operator can be used to append to or add to the variable's previous value. When += is applied to a
variable for which the <u>integer</u> attribute has been set, <u>value</u> is evaluated as an arithmetic expression and
added to the variable's current value, which is also evaluated. When += is applied to an array variable
using compound assignment (see <b>Arrays</b> below), the variable's value is not unset (as it is when using =),
and new values are appended to the array beginning at one greater than the array's maximum index (for
indexed arrays) or added as additional key-value pairs in an associative array. When applied to a
string-valued variable, <u>value</u> is expanded and appended to the variable's value.''')
_group = textwrap.dedent(''' { <u>list</u>; }
<u>list</u> is simply executed in the current shell environment. <u>list</u> must be terminated with a newline
or semicolon. This is known as a <u>group</u> <u>command</u>. The return status is the exit status of <u>list</u>.
Note that unlike the metacharacters <b>(</b> and <b>)</b>, <b>{</b> and <b>}</b> are <u>reserved</u> <u>words</u> and must occur where a
reserved word is permitted to be recognized. Since they do not cause a word break, they must be
separated from <u>list</u> by whitespace or another shell metacharacter.''')
_subshell = textwrap.dedent(''' (<u>list</u>) <u>list</u> is executed in a subshell environment (see <b>COMMAND</b> <b>EXECUTION</b> <b>ENVIRONMENT</b> below). Variable
assignments and builtin commands that affect the shell's environment do not remain in effect after
the command completes. The return status is the exit status of <u>list</u>.''')
_negate = '''If the reserved word <b>!</b> precedes a pipeline, the exit status of that pipeline is the logical negation of the
exit status as described above.'''
_if = textwrap.dedent(''' <b>if</b> <u>list</u>; <b>then</b> <u>list;</u> [ <b>elif</b> <u>list</u>; <b>then</b> <u>list</u>; ] ... [ <b>else</b> <u>list</u>; ] <b>fi</b>
The <b>if</b> <u>list</u> is executed. If its exit status is zero, the <b>then</b> <u>list</u> is executed. Otherwise, each
<b>elif</b> <u>list</u> is executed in turn, and if its exit status is zero, the corresponding <b>then</b> <u>list</u> is
executed and the command completes. Otherwise, the <b>else</b> <u>list</u> is executed, if present. The exit
status is the exit status of the last command executed, or zero if no condition tested true.''')
_for = textwrap.dedent(''' <b>for</b> <u>name</u> [ [ <b>in</b> [ <u>word</u> <u>...</u> ] ] ; ] <b>do</b> <u>list</u> ; <b>done</b>
The list of words following <b>in</b> is expanded, generating a list of items. The variable <u>name</u> is set
to each element of this list in turn, and <u>list</u> is executed each time. If the <b>in</b> <u>word</u> is omitted,
the <b>for</b> command executes <u>list</u> once for each positional parameter that is set (see <b>PARAMETERS</b>
below). The return status is the exit status of the last command that executes. If the expansion
of the items following <b>in</b> results in an empty list, no commands are executed, and the return
status is 0.''')
_whileuntil = textwrap.dedent(''' <b>while</b> <u>list-1</u>; <b>do</b> <u>list-2</u>; <b>done</b>
<b>until</b> <u>list-1</u>; <b>do</b> <u>list-2</u>; <b>done</b>
The <b>while</b> command continuously executes the list <u>list-2</u> as long as the last command in the list
<u>list-1</u> returns an exit status of zero. The <b>until</b> command is identical to the <b>while</b> command,
except that the test is negated; <u>list-2</u> is executed as long as the last command in <u>list-1</u> returns
a non-zero exit status. The exit status of the <b>while</b> and <b>until</b> commands is the exit status of the
last command executed in <u>list-2</u>, or zero if none was executed.''')
_select = textwrap.dedent(''' <b>select</b> <u>name</u> [ <b>in</b> <u>word</u> ] ; <b>do</b> <u>list</u> ; <b>done</b>
The list of words following <b>in</b> is expanded, generating a list of items. The set of expanded words
is printed on the standard error, each preceded by a number. If the <b>in</b> <u>word</u> is omitted, the
positional parameters are printed (see <b>PARAMETERS</b> below). The <b>PS3</b> prompt is then displayed and a
line read from the standard input. If the line consists of a number corresponding to one of the
displayed words, then the value of <u>name</u> is set to that word. If the line is empty, the words and
prompt are displayed again. If EOF is read, the command completes. Any other value read causes
<u>name</u> to be set to null. The line read is saved in the variable <b>REPLY</b>. The <u>list</u> is executed after
each selection until a <b>break</b> command is executed. The exit status of <b>select</b> is the exit status of
the last command executed in <u>list</u>, or zero if no commands were executed.''')
RESERVEDWORDS = {
'!' : _negate,
'{' : _group,
'}' : _group,
'(' : _subshell,
')' : _subshell,
';' : OPSEMICOLON,
}
def _addwords(key, text, *words):
for word in words:
COMPOUNDRESERVEDWORDS.setdefault(key, {})[word] = text
COMPOUNDRESERVEDWORDS = {}
_addwords('if', _if, 'if', 'then', 'elif', 'else', 'fi', ';')
_addwords('for', _for, 'for', 'in', 'do', 'done', ';')
_addwords('while', _whileuntil, 'while', 'do', 'done', ';')
_addwords('until', _whileuntil, 'until', 'do', 'done')
_addwords('select', _select, 'select', 'in', 'do', 'done')
_function = textwrap.dedent(''' A shell function is an object that is called like a simple command and executes a compound command with a
new set of positional parameters. Shell functions are declared as follows:
<u>name</u> () <u>compound-command</u> [<u>redirection</u>]
<b>function</b> <u>name</u> [()] <u>compound-command</u> [<u>redirection</u>]
This defines a function named <u>name</u>. The reserved word <b>function</b> is optional. If the <b>function</b>
reserved word is supplied, the parentheses are optional. The <u>body</u> of the function is the compound
command <u>compound-command</u> (see <b>Compound</b> <b>Commands</b> above). That command is usually a <u>list</u> of
commands between { and }, but may be any command listed under <b>Compound</b> <b>Commands</b> above.
<u>compound-command</u> is executed whenever <u>name</u> is specified as the name of a simple command. Any
redirections (see <b>REDIRECTION</b> below) specified when a function is defined are performed when the
function is executed. The exit status of a function definition is zero unless a syntax error
occurs or a readonly function with the same name already exists. When executed, the exit status
of a function is the exit status of the last command executed in the body. (See <b>FUNCTIONS</b> below.)''')
_functioncall = 'call shell function %r'
_functionarg = 'argument for shell function %r'
COMMENT = textwrap.dedent('''<b>COMMENTS</b>
In a non-interactive shell, or an interactive shell in which the <b>interactive_comments</b> option to the <b>shopt</b>
builtin is enabled (see <b>SHELL</b> <b>BUILTIN</b> <b>COMMANDS</b> below), a word beginning with <b>#</b> causes that word and all
remaining characters on that line to be ignored. An interactive shell without the <b>interactive_comments</b>
option enabled does not allow comments. The <b>interactive_comments</b> option is on by default in interactive
shells.''')
parameters = {
'*' : 'star',
'@' : 'at',
'#' : 'pound',
'?' : 'question',
'-' : 'hyphen',
'$' : 'dollar',
'!' : 'exclamation',
'0' : 'zero',
'_' : 'underscore',
}
================================================
FILE: explainshell/manager.py
================================================
import sys, os, argparse, logging, glob
from explainshell import options, store, fixer, manpage, errors, util, config
from explainshell.algo import classifier
logger = logging.getLogger('explainshell.manager')
class managerctx(object):
def __init__(self, classifier, store, manpage):
self.classifier = classifier
self.store = store
self.manpage = manpage
self.name = manpage.name
self.classifiermanpage = None
self.optionsraw = None
self.optionsextracted = None
self.aliases = None
class manager(object):
'''the manager uses all parts of the system to read, classify, parse, extract
and write a man page to the database'''
def __init__(self, dbhost, dbname, paths, overwrite=False, drop=False):
self.paths = paths
self.overwrite = overwrite
self.store = store.store(dbname, dbhost)
self.classifier = classifier.classifier(self.store, 'bayes')
self.classifier.train()
if drop:
self.store.drop(True)
def ctx(self, m):
return managerctx(self.classifier, self.store, m)
def _read(self, ctx, frunner):
frunner.pre_get_raw_manpage()
ctx.manpage.read()
ctx.manpage.parse()
assert len(ctx.manpage.paragraphs) > 1
ctx.manpage = store.manpage(ctx.manpage.shortpath, ctx.manpage.name,
ctx.manpage.synopsis, ctx.manpage.paragraphs, list(ctx.manpage.aliases))
frunner.post_parse_manpage()
def _classify(self, ctx, frunner):
ctx.classifiermanpage = store.classifiermanpage(ctx.name, ctx.manpage.paragraphs)
frunner.pre_classify()
_ = list(ctx.classifier.classify(ctx.classifiermanpage))
frunner.post_classify()
def _extract(self, ctx, frunner):
options.extract(ctx.manpage)
frunner.post_option_extraction()
if not ctx.manpage.options:
logger.warn("couldn't find any options for manpage %s", ctx.manpage.name)
def _write(self, ctx, frunner):
frunner.pre_add_manpage()
return ctx.store.addmanpage(ctx.manpage)
def _update(self, ctx, frunner):
frunner.pre_add_manpage()
return ctx.store.updatemanpage(ctx.manpage)
def process(self, ctx):
frunner = fixer.runner(ctx)
self._read(ctx, frunner)
self._classify(ctx, frunner)
self._extract(ctx, frunner)
m = self._write(ctx, frunner)
return m
def edit(self, m, paragraphs=None):
ctx = self.ctx(m)
frunner = fixer.runner(ctx)
if paragraphs:
m.paragraphs = paragraphs
frunner.disable('paragraphjoiner')
frunner.post_option_extraction()
else:
self._extract(ctx, frunner)
m = self._update(ctx, frunner)
return m
def run(self):
added = []
exists = []
for path in self.paths:
try:
m = manpage.manpage(path)
logger.info('handling manpage %s (from %s)', m.name, path)
try:
mps = self.store.findmanpage(m.shortpath[:-3])
mps = [mp for mp in mps if m.shortpath == mp.source]
if mps:
assert len(mps) == 1
mp = mps[0]
if not self.overwrite or mp.updated:
logger.info('manpage %r already in the data store, not overwriting it', m.name)
exists.append(m)
continue
except errors.ProgramDoesNotExist:
pass
# the manpage is not in the data store; process and add it
ctx = self.ctx(m)
m = self.process(ctx)
if m:
added.append(m)
except errors.EmptyManpage, e:
logger.error('manpage %r is empty!', e.args[0])
except ValueError:
logger.fatal('uncaught exception when handling manpage %s', path)
except KeyboardInterrupt:
raise
except:
logger.fatal('uncaught exception when handling manpage %s', path)
raise
if not added:
logger.warn('no manpages added')
else:
self.findmulticommands()
return added, exists
def findmulticommands(self):
manpages = {}
potential = []
for _id, m in self.store.names():
if '-' in m:
potential.append((m.split('-'), _id))
else:
manpages[m] = _id
mappings = set([x[0] for x in self.store.mappings()])
mappingstoadd = []
multicommands = {}
for p, _id in potential:
if ' '.join(p) in mappings:
continue
if p[0] in manpages:
mappingstoadd.append((' '.join(p), _id))
multicommands[p[0]] = manpages[p[0]]
for src, dst in mappingstoadd:
self.store.addmapping(src, dst, 1)
logger.info('inserting mapping (multicommand) %s -> %s', src, dst)
for multicommand, _id in multicommands.iteritems():
self.store.setmulticommand(_id)
logger.info('making %r a multicommand', multicommand)
return mappingstoadd, multicommands
def main(files, dbname, dbhost, overwrite, drop, verify):
if verify:
s = store.store(dbname, dbhost)
ok = s.verify()
return 0 if ok else 1
if drop:
if raw_input('really drop db (y/n)? ').strip().lower() != 'y':
drop = False
else:
overwrite = True # if we drop, no need to take overwrite into account
gzs = set()
for path in files:
if os.path.isdir(path):
gzs.update([os.path.abspath(f) for f in glob.glob(os.path.join(path, '*.gz'))])
else:
gzs.add(os.path.abspath(path))
m = manager(dbhost, dbname, gzs, overwrite, drop)
added, exists = m.run()
for mp in added:
print 'successfully added %s' % mp.source
if exists:
print 'these manpages already existed and werent overwritten: \n\n%s' % '\n'.join([m.path for m in exists])
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='process man pages and save them in the store')
parser.add_argument('--log', type=str, default='ERROR', help='use log as the logger log level')
parser.add_argument('--overwrite', action='store_true', default=False, help='overwrite man pages that already exist in the store')
parser.add_argument('--drop', action='store_true', default=False, help='delete all existing man pages')
parser.add_argument('--db', default='explainshell', help='mongo db name')
parser.add_argument('--host', default=config.MONGO_URI, help='mongo host')
parser.add_argument('--verify', action='store_true', default=False, help='verify db integrity')
parser.add_argument('files', nargs='*')
args = parser.parse_args()
logging.basicConfig(level=getattr(logging, args.log.upper()))
sys.exit(main(args.files, args.db, args.host, args.overwrite, args.drop, args.verify))
================================================
FILE: explainshell/manpage.py
================================================
import os, subprocess, re, logging, collections, urllib
from explainshell import config, store, errors
devnull = open(os.devnull, 'w')
SPLITSYNOP = re.compile(r'([^ ]+) - (.*)$')
ENV = dict(os.environ)
ENV["W3MMAN_MAN"] = "man --no-hyphenation"
ENV["MAN_KEEP_FORMATTING"] = "1"
ENV["MANWIDTH"] = "115"
ENV["LC_ALL"] = "en_US.UTF-8"
logger = logging.getLogger(__name__)
def extractname(gzname):
'''
>>> extractname('ab.1.gz')
'ab'
>>> extractname('ab.1.1.gz')
'ab.1'
>>> extractname('ab.1xyz.gz')
'ab'
>>> extractname('ab.1.1xyz.gz')
'ab.1'
>>> extractname('a/b/c/ab.1.1xyz.gz')
'ab.1'
'''
if '/' in gzname:
gzname = os.path.basename(gzname)
return gzname.rsplit('.', 2)[0]
def bold(l):
'''
>>> bold('a')
([], ['a'])
>>> bold('<b>a</b>')
(['a'], [])
>>> bold('a<b>b</b>c')
(['b'], ['a', 'c'])
>>> bold('<b>first</b> <b>second:</b>')
(['first', 'second:'], [])
'''
inside = []
for m in _section.finditer(l):
inside.append(m.span(0))
current = 0
outside = []
for start, end in inside:
outside.append((current, start))
current = end
outside.append((current, len(l)))
inside = [l[s:e] for s, e in inside]
inside = [s.replace('<b>', '').replace('</b>', '') for s in inside]
outside = [l[s:e] for s, e in outside]
outside = [l for l in outside if l and not l.isspace()]
return inside, outside
# w3mman2html.cgi (the tool we're using to output html from a man page) does
# some strange escaping which causes it to output invalid utf8. we look these
# up and fix them manually
_replacementsprefix = [
('\xe2\x80\xe2\x80\x98', None, True), # left single quote
('\xe2\x80\xe2\x80\x99', None, True), # right single quote
('\xe2\x80\xe2\x80\x9c', None, True), # left double quote
('\xe2\x80\xe2\x80\x9d', None, True), # right double quote
('\xe2\x94\xe2\x94\x82', '|', False), # pipe
('\xe2\x8e\xe2\x8e\xaa', None, False), # pipe 2
('\xe2\x80\xe2\x80\x90', None, True), # hyphen
('\xe2\x80\xe2\x80\x94', None, True), # hyphen 2
('\xe2\x80\xc2\xbd', None, True), # half
('\xe2\x88\xe2\x88\x97', None, True), # asterisk
('\xe2\x86\xe2\x86\x92', None, True), # right arrow
('\xe2\x88\xe2\x88\x92', None, True), # minus sign
('\xe2\x80\xe2\x80\x93', None, True), # en dash
('\xe2\x80\xe2\x80\xb2', None, False), # prime
('\xe2\x88\xe2\x88\xbc', None, False), # tilde operator
('\xe2\x86\xe2\x86\xb5', None, False), # downwards arrow with corner leftwards
('\xef\xbf\xef\xbf\xbd', None, False) # replacement char
]
_replacements = []
for searchfor, replacewith, underline in _replacementsprefix:
if replacewith is None:
replacewith = searchfor[2:]
_replacements.append((searchfor, replacewith))
if underline:
x = list(replacewith)
x.insert(1, '</u>')
x = ''.join(x)
_replacements.append((x, '%s</u>' % replacewith))
_replacementsnoprefix = ['\xc2\xb7', # bullet
'\xc2\xb4', # apostrophe
'\xc2\xa0', # no break space
'\xc3\xb8', '\xe4\xbd\xa0', '\xe5\xa5\xbd', # gibberish
'\xc2\xa7', # section sign
'\xef\xbf\xbd', # replacement char
'\xc2\xa4', # latin small letter a with diaeresis
'\xc3\xa4', # latin small letter a with diaeresis
'\xc4\xa4', # latin small letter a with diaeresis
'\xc3\xaa', # latin small letter e with circumflex
]
for s in _replacementsnoprefix:
x = list(s)
x.insert(1, '</u>')
x = ''.join(x)
_replacements.append((x, '%s</u>' % s))
_href = re.compile(r'<a href="file:///[^\?]*\?([^\(]*)\(([^\)]*)\)">')
_section = re.compile(r'<b>([^<]+)</b>')
def _parsetext(lines):
paragraphlines = []
section = None
i = 0
for l in lines:
l = re.sub(_href, r'<a href="http://manpages.ubuntu.com/manpages/precise/en/man\2/\1.\2.html">', l)
for lookfor, replacewith in _replacements:
l = re.sub(lookfor, replacewith, l)
# confirm the line is valid utf8
lreplaced = l.decode('utf8', 'ignore').encode('utf8')
if lreplaced != l:
logger.error('line %r contains invalid utf8', l)
l = lreplaced
raise ValueError
if l.startswith('<b>'): # section
section = re.sub(_section, r'\1', l)
else:
foundsection = False
if l.strip().startswith('<b>'):
inside, outside = bold(l.strip())
if not outside and inside[-1][-1] == ':':
foundsection = True
section = ' '.join(inside)[:-1]
if not foundsection:
if not l.strip() and paragraphlines:
yield store.paragraph(i, '\n'.join(paragraphlines), section, False)
i += 1
paragraphlines = []
elif l.strip():
paragraphlines.append(l)
if paragraphlines:
yield store.paragraph(i, '\n'.join(paragraphlines), section, False)
def _parsesynopsis(base, synopsis):
'''
>>> _parsesynopsis('/a/b/c', '/a/b/c: "p-r+o++g - foo bar."')
('p-r+o++g', 'foo bar')
'''
synopsis = synopsis[len(base)+3:-1]
if synopsis[-1] == '.':
synopsis = synopsis[:-1]
return SPLITSYNOP.match(synopsis).groups()
class manpage(object):
'''read the man page at path by executing w3mman2html.cgi and find its
synopsis with lexgrog
since some man pages share the same name (different versions), each
alias of a man page has a score that's determined in this simple fashion:
- name of man page source file is given a score of 10
- all other names found for a particular man page are given a score of 1
(other names are found by scanning the output of lexgrog)
'''
def __init__(self, path):
self.path = path
self.shortpath = os.path.basename(self.path)
self.name = extractname(self.path)
self.aliases = set([self.name])
self.synopsis = None
self.paragraphs = None
self._text = None
def read(self):
'''Read the content from a local manpage file and store it in usable formats
on the class instance.'''
cmd = [config.MAN2HTML, urllib.urlencode({'local' : os.path.abspath(self.path)})]
logger.info('executing %r', ' '.join(cmd))
self._text = subprocess.check_output(cmd, stderr=devnull, env=ENV)
try:
self.synopsis = subprocess.check_output(['lexgrog', self.path], stderr=devnull).rstrip()
except subprocess.CalledProcessError:
logger.error('failed to extract synopsis for %s', self.name)
def parse(self):
self.paragraphs = list(_parsetext(self._text.splitlines()[7:-3]))
if not self.paragraphs:
raise errors.EmptyManpage(self.shortpath)
if self.synopsis:
self.synopsis = [_parsesynopsis(self.path, l) for l in self.synopsis.splitlines()]
# figure out aliases from the synopsis
d = collections.OrderedDict()
for prog, text in self.synopsis:
d.setdefault(text, []).append(prog)
text, progs = d.items()[0]
self.synopsis = text
self.aliases.update(progs)
self.aliases.remove(self.name)
# give the name of the man page the highest score
self.aliases = [(self.name, 10)] + [(x, 1) for x in self.aliases]
================================================
FILE: explainshell/matcher.py
================================================
import collections, logging, itertools
import bashlex.parser
import bashlex.ast
from explainshell import errors, util, helpconstants
class matchgroup(object):
'''a class to group matchresults together
we group all shell results in one group and create a new group for every
command'''
def __init__(self, name):
self.name = name
self.results = []
def __repr__(self):
return '<matchgroup %r with %d results>' % (self.name, len(self.results))
class matchresult(collections.namedtuple('matchresult', 'start end text match')):
@property
def unknown(self):
return self.text is None
matchwordexpansion = collections.namedtuple('matchwordexpansion',
'start end kind')
logger = logging.getLogger(__name__)
class matcher(bashlex.ast.nodevisitor):
'''parse a command line and return a list of matchresults describing
each token.
'''
def __init__(self, s, store):
self.s = s.encode('latin1', 'replace')
self.store = store
self._prevoption = self._currentoption = None
self.groups = [matchgroup('shell')]
# a list of matchwordexpansions where expansions happened during word
# expansion
self.expansions = []
# a stack to manage nested command groups: whenever a new simple
# command is started, we push a tuple with:
# - the node that started this group. this is used to find it when
# a command ends (see visitnodeend)
# - its matchgroup. new matchresults will be added to it.
# - a word used to end the top-most command. this is used when a flag
# starts a new command, e.g. find -exec.
self.groupstack = [(None, self.groups[-1], None)]
# keep a stack of the currently visited compound command (if/for..)
# to provide context when matching reserved words, since for example
# the keyword 'done' can appear in a for, while..
self.compoundstack = []
# a set of functions defined in the current input, we will try to match
# commands against them so if one refers to defined function, it won't
# show up as unknown or be taken from the db
self.functions = set()
def _generatecommandgroupname(self):
existing = len([g for g in self.groups if g.name.startswith('command')])
return 'command%d' % existing
@property
def matches(self):
'''return the list of results from the most recently created group'''
return self.groupstack[-1][1].results
@property
def allmatches(self):
return list(itertools.chain.from_iterable(g.results for g in self.groups))
@property
def manpage(self):
group = self.groupstack[-1][1]
# we do not have a manpage if the top of the stack is the shell group.
# this can happen if the first argument is a command substitution
# and we're not treating it as a "man page not found"
if group.name != 'shell':
return group.manpage
def find_option(self, opt):
self._currentoption = self.manpage.find_option(opt)
logger.debug('looking up option %r, got %r', opt, self._currentoption)
return self._currentoption
def findmanpages(self, prog):
prog = prog.decode('latin1')
logger.info('looking up %r in store', prog)
manpages = self.store.findmanpage(prog)
logger.info('found %r in store, got: %r, using %r', prog, manpages, manpages[0])
return manpages
def unknown(self, token, start, end):
logger.debug('nothing to do with token %r', token)
return matchresult(start, end, None, None)
def visitreservedword(self, node, word):
# first try the compound reserved words
helptext = None
if self.compoundstack:
currentcompound = self.compoundstack[-1]
helptext = helpconstants.COMPOUNDRESERVEDWORDS.get(currentcompound, {}).get(word)
# try these if we don't have anything specific
if not helptext:
helptext = helpconstants.RESERVEDWORDS[word]
self.groups[0].results.append(matchresult(node.pos[0], node.pos[1], helptext, None))
def visitoperator(self, node, op):
helptext = None
if self.compoundstack:
currentcompound = self.compoundstack[-1]
helptext = helpconstants.COMPOUNDRESERVEDWORDS.get(currentcompound, {}).get(op)
if not helptext:
helptext = helpconstants.OPERATORS[op]
self.groups[0].results.append(matchresult(node.pos[0], node.pos[1], helptext, None))
def visitpipe(self, node, pipe):
self.groups[0].results.append(
matchresult(node.pos[0], node.pos[1], helpconstants.PIPELINES, None))
def visitredirect(self, node, input, type, output, heredoc):
helptext = [helpconstants.REDIRECTION]
if type == '>&' and isinstance(output, int):
type = type[:-1]
if type in helpconstants.REDIRECTION_KIND:
helptext.append(helpconstants.REDIRECTION_KIND[type])
self.groups[0].results.append(
matchresult(node.pos[0], node.pos[1], '\n\n'.join(helptext), None))
# the output might contain a wordnode, visiting it will confuse the
# matcher who'll think it's an argument, instead visit the expansions
# directly, if we have any
if isinstance(output, bashlex.ast.node):
for part in output.parts:
self.visit(part)
return False
def visitcommand(self, node, parts):
assert parts
# look for the first WordNode, which might not be at parts[0]
idxwordnode = bashlex.ast.findfirstkind(parts, 'word')
if idxwordnode == -1:
logger.info('no words found in command (probably contains only redirects)')
return
wordnode = parts[idxwordnode]
# check if this refers to a previously defined function
if wordnode.word in self.functions:
logger.info('word %r is a function, not trying to match it or its '
'arguments', wordnode)
# first, add a matchresult for the function call
mr = matchresult(wordnode.pos[0], wordnode.pos[1],
helpconstants._functioncall % wordnode.word, None)
self.matches.append(mr)
# this is a bit nasty: if we were to visit the command like we
# normally do it would try to match it against a manpage. but
# we don't have one here, we just want to take all the words and
# consider them part of the function call
for part in parts:
# maybe it's a redirect...
if part.kind != 'word':
self.visit(part)
else:
# this is an argument to the function
if part is not wordnode:
mr = matchresult(part.pos[0], part.pos[1],
helpconstants._functionarg % wordnode.word,
None)
self.matches.append(mr)
# visit any expansions in there
for ppart in part.parts:
self.visit(ppart)
# we're done with this commandnode, don't visit its children
return False
self.startcommand(node, parts, None)
def visitif(self, *args):
self.compoundstack.append('if')
def visitfor(self, node, parts):
self.compoundstack.append('for')
for part in parts:
# don't visit words since they're not part of the current command,
# instead consider them part of the for construct
if part.kind == 'word':
mr = matchresult(part.pos[0], part.pos[1], helpconstants._for, None)
self.groups[0].results.append(mr)
# but we do want to visit expanions
for ppart in part.parts:
self.visit(ppart)
else:
self.visit(part)
return False
def visitwhile(self, *args):
self.compoundstack.append('while')
def visituntil(self, *args):
self.compoundstack.append('until')
def visitnodeend(self, node):
if node.kind == 'command':
# it's possible for visitcommand/end to be called without a command
# group being pushed if it contains only redirect nodes
if len(self.groupstack) > 1:
logger.info('visitnodeend %r, groups %d', node, len(self.groupstack))
while self.groupstack[-1][0] is not node:
logger.info('popping groups that are a result of nested commands')
self.endcommand()
self.endcommand()
elif node.kind in ('if', 'for', 'while', 'until'):
kind = self.compoundstack.pop()
assert kind == node.kind
def startcommand(self, commandnode, parts, endword, addgroup=True):
logger.info('startcommand commandnode=%r parts=%r, endword=%r, addgroup=%s',
commandnode, parts, endword, addgroup)
idxwordnode = bashlex.ast.findfirstkind(parts, 'word')
assert idxwordnode != -1
wordnode = parts[idxwordnode]
if wordnode.parts:
logger.info('node %r has parts (it was expanded), no point in looking'
' up a manpage for it', wordnode)
if addgroup:
mg = matchgroup(self._generatecommandgroupname())
mg.manpage = None
mg.suggestions = None
self.groups.append(mg)
self.groupstack.append((commandnode, mg, endword))
return False
startpos, endpos = wordnode.pos
try:
mps = self.findmanpages(wordnode.word)
# we consume this node here, pop it from parts so we
# don't visit it again as an argument
parts.pop(idxwordnode)
except errors.ProgramDoesNotExist, e:
if addgroup:
# add a group for this command, we'll mark it as unknown
# when visitword is called
logger.info('no manpage found for %r, adding a group for it',
wordnode.word)
mg = matchgroup(self._generatecommandgroupname())
mg.error = e
mg.manpage = None
mg.suggestions = None
self.groups.append(mg)
self.groupstack.append((commandnode, mg, endword))
return False
manpage = mps[0]
idxnextwordnode = bashlex.ast.findfirstkind(parts, 'word')
# check the next word for a possible multicommand if:
# - the matched manpage says so
# - we have another word node
# - the word node has no expansions in it
if manpage.multicommand and idxnextwordnode != -1 and not parts[idxnextwordnode].parts:
nextwordnode = parts[idxnextwordnode]
try:
multi = '%s %s' % (wordnode.word, nextwordnode.word)
logger.info('%r is a multicommand, trying to get another token and look up %r', manpage, multi)
mps = self.findmanpages(multi)
manpage = mps[0]
# we consume this node here, pop it from parts so we
# don't visit it again as an argument
parts.pop(idxnextwordnode)
endpos = nextwordnode.pos[1]
except errors.ProgramDoesNotExist:
logger.info('no manpage %r for multicommand %r', multi, manpage)
# create a new matchgroup for the current command
mg = matchgroup(self._generatecommandgroupname())
mg.manpage = manpage
mg.suggestions = mps[1:]
self.groups.append(mg)
self.groupstack.append((commandnode, mg, endword))
self.matches.append(matchresult(startpos, endpos,
manpage.synopsis or helpconstants.NOSYNOPSIS, None))
return True
def endcommand(self):
'''end the most recently created command group by popping it from the
group stack. groups are created by visitcommand or a nested command'''
assert len(self.groupstack) >= 2, 'groupstack must contain shell and command groups'
g = self.groupstack.pop()
logger.info('ending group %s', g)
def visitcommandsubstitution(self, node, command):
kind = self.s[node.pos[0]]
substart = 2 if kind == '$' else 1
# start the expansion after the $( or `
self.expansions.append(matchwordexpansion(node.pos[0] + substart,
node.pos[1] - 1,
'substitution'))
# do not try to match the child nodes
return False
def visitprocesssubstitution(self, node, command):
# don't include opening <( and closing )
self.expansions.append(matchwordexpansion(node.pos[0] + 2,
node.pos[1] - 1,
'substitution'))
# do not try to match the child nodes
return False
def visitassignment(self, node, word):
helptext = helpconstants.ASSIGNMENT
self.groups[0].results.append(matchresult(node.pos[0], node.pos[1], helptext, None))
def visitword(self, node, word):
def attemptfuzzy(chars):
m = []
if chars[0] == '-':
tokens = [chars[0:2]] + list(chars[2:])
considerarg = True
else:
tokens = list(chars)
considerarg = False
pos = node.pos[0]
prevoption = None
for i, t in enumerate(tokens):
op = t if t[0] == '-' else '-' + t
option = self.find_option(op)
if option:
if considerarg and not m and option.expectsarg:
logger.info('option %r expected an arg, taking the rest too', option)
# reset the current option if we already took an argument,
# this prevents the next word node to also consider itself
# as an argument
self._currentoption = None
return [matchresult(pos, pos+len(chars), option.text, None)]
mr = matchresult(pos, pos+len(t), option.text, None)
m.append(mr)
# if the previous option expected an argument and we couldn't
# match the current token, take the rest as its argument, this
# covers a series of short options where the last one has an argument
# with no space between it, such as 'xargs -r0n1'
elif considerarg and prevoption and prevoption.expectsarg:
pmr = m[-1]
mr = matchresult(pmr.start, pmr.end+(len(tokens)-i), pmr.text, None)
m[-1] = mr
# reset the current option if we already took an argument,
# this prevents the next word node to also consider itself
# as an argument
self._currentoption = None
break
else:
m.append(self.unknown(t, pos, pos+len(t)))
pos += len(t)
prevoption = option
return m
def _visitword(node, word):
if not self.manpage:
logger.info('inside an unknown command, giving up on %r', word)
self.matches.append(self.unknown(word, node.pos[0], node.pos[1]))
return
logger.info('trying to match token: %r', word)
self._prevoption = self._currentoption
if word.startswith('--'):
word = word.split('=', 1)[0]
option = self.find_option(word)
if option:
logger.info('found an exact match for %r: %r', word, option)
mr = matchresult(node.pos[0], node.pos[1], option.text, None)
self.matches.append(mr)
# check if we splitted the word just above, if we did then reset
# the current option so the next word doesn't consider itself
# an argument
if word != node.word:
self._currentoption = None
else:
word = node.word
# check if we're inside a nested command and this word marks the end
if isinstance(self.groupstack[-1][-1], list) and word in self.groupstack[-1][-1]:
logger.info('token %r ends current nested command', word)
self.endcommand()
mr = matchresult(node.pos[0], node.pos[1], self.matches[-1].text, None)
self.matches.append(mr)
elif word != '-' and word.startswith('-') and not word.startswith('--'):
logger.debug('looks like a short option')
if len(word) > 2:
logger.info("trying to split it up")
self.matches.extend(attemptfuzzy(word))
else:
self.matches.append(self.unknown(word, node.pos[0], node.pos[1]))
elif self._prevoption and self._prevoption.expectsarg:
logger.info("previous option possibly expected an arg, and we can't"
" find an option to match the current token, assuming it's an arg")
ea = self._prevoption.expectsarg
possibleargs = ea if isinstance(ea, list) else []
take = True
if possibleargs and word not in possibleargs:
take = False
logger.info('token %r not in list of possible args %r for %r',
word, possibleargs, self._prevoption)
if take:
if self._prevoption.nestedcommand:
logger.info('option %r can nest commands', self._prevoption)
if self.startcommand(None, [node], self._prevoption.nestedcommand, addgroup=False):
self._currentoption = None
return
pmr = self.matches[-1]
mr = matchresult(pmr.start, node.pos[1], pmr.text, None)
self.matches[-1] = mr
else:
self.matches.append(self.unknown(word, node.pos[0], node.pos[1]))
else:
if self.manpage.partialmatch:
logger.info('attemping to do a partial match')
m = attemptfuzzy(word)
if not any(mm.unknown for mm in m):
logger.info('found a match for everything, taking it')
self.matches.extend(m)
return
if self.manpage.arguments:
if self.manpage.nestedcommand:
logger.info('manpage %r can nest commands', self.manpage)
if self.startcommand(None, [node], self.manpage.nestedcommand, addgroup=False):
self._currentoption = None
return
d = self.manpage.arguments
k = list(d.keys())[0]
logger.info('got arguments, using %r', k)
text = d[k]
mr = matchresult(node.pos[0], node.pos[1], text, None)
self.matches.append(mr)
return
# if all of that failed, we can't explain it so mark it unknown
self.matches.append(self.unknown(word, node.pos[0], node.pos[1]))
_visitword(node, word)
def visitfunction(self, node, name, body, parts):
self.functions.add(name.word)
def _iscompoundopenclosecurly(compound):
first, last = compound.list[0], compound.list[-1]
if (first.kind == 'reservedword' and last.kind == 'reservedword' and
first.word == '{' and last.word == '}'):
return True
# if the compound command we have there is { }, let's include the
# {} as part of the function declaration. normally it would be
# treated as a group command, but that seems less informative in this
# context
if _iscompoundopenclosecurly(body):
# create a matchresult until after the first {
mr = matchresult(node.pos[0], body.list[0].pos[1],
helpconstants._function, None)
self.groups[0].results.append(mr)
# create a matchresult for the closing }
mr = matchresult(body.list[-1].pos[0], body.list[-1].pos[1],
helpconstants._function, None)
self.groups[0].results.append(mr)
# visit anything in between the { }
for part in body.list[1:-1]:
self.visit(part)
else:
beforebody = bashlex.ast.findfirstkind(parts, 'compound') - 1
assert beforebody > 0
beforebody = parts[beforebody]
# create a matchresult ending at the node before body
mr = matchresult(node.pos[0], beforebody.pos[1],
helpconstants._function, None)
self.groups[0].results.append(mr)
self.visit(body)
return False
def visittilde(self, node, value):
self.expansions.append(matchwordexpansion(node.pos[0], node.pos[1],
'tilde'))
def visitparameter(self, node, value):
try:
int(value)
kind = 'digits'
except ValueError:
kind = helpconstants.parameters.get(value, 'param')
self.expansions.append(matchwordexpansion(node.pos[0], node.pos[1],
'parameter-%s' % kind))
def match(self):
logger.info('matching string %r', self.s)
# limit recursive parsing to a depth of 1
self.ast = bashlex.parser.parsesingle(self.s, expansionlimit=1,
strictmode=False)
if self.ast:
self.visit(self.ast)
assert len(self.groupstack) == 1, 'groupstack should contain only shell group after matching'
# if we only have one command in there and no shell results/expansions,
# reraise the original exception
if (len(self.groups) == 2 and not self.groups[0].results and
self.groups[1].manpage is None and not self.expansions):
raise self.groups[1].error
else:
logger.warn('no AST generated for %r', self.s)
def debugmatch():
s = '\n'.join(['%d) %r = %r' % (i, self.s[m.start:m.end], m.text) for i, m in enumerate(self.allmatches)])
return s
self._markunparsedunknown()
# fix each matchgroup seperately
for group in self.groups:
if group.results:
if getattr(group, 'manpage', None):
# ensure that the program part isn't unknown (i.e. it has
# something as its synopsis)
assert not group.results[0].unknown
group.results = self._mergeadjacent(group.results)
# add matchresult.match to existing matches
for i, m in enumerate(group.results):
assert m.end <= len(self.s), '%d %d' % (m.end, len(self.s))
portion = self.s[m.start:m.end].decode('latin1')
group.results[i] = matchresult(m.start, m.end, m.text, portion)
logger.debug('%r matches:\n%s', self.s, debugmatch())
# not strictly needed, but doesn't hurt
self.expansions.sort()
return self.groups
def _markunparsedunknown(self):
'''the parser may leave a remainder at the end of the string if it doesn't
match any of the rules, mark them as unknowns'''
parsed = [False]*len(self.s)
# go over all existing matches to see if we've covered the
# current position
for start, end, _, _ in self.allmatches:
for i in range(start, end):
parsed[i] = True
for i in range(len(parsed)):
c = self.s[i]
# whitespace is always 'unparsed'
if c.isspace():
parsed[i] = True
# the parser ignores comments but we can use a trick to see if this
# starts a comment and is beyond the ending index of the parsed
# portion of the inpnut
if (not self.ast or i > self.ast.pos[1]) and c == '#':
comment = matchresult(i, len(parsed), helpconstants.COMMENT, None)
self.groups[0].results.append(comment)
break
if not parsed[i]:
# add unparsed results to the 'shell' group
self.groups[0].results.append(self.unknown(c, i, i+1))
# there are no overlaps, so sorting by the start is enough
self.groups[0].results.sort(key=lambda mr: mr.start)
def _resultindex(self):
'''return a mapping of matchresults to their index among all
matches, sorted by the start position of the matchresult'''
d = {}
i = 0
for result in sorted(self.allmatches, key=lambda mr: mr.start):
d[result] = i
i += 1
return d
def _mergeadjacent(self, matches):
merged = []
resultindex = self._resultindex()
sametext = itertools.groupby(matches, lambda m: m.text)
for text, ll in sametext:
for l in util.groupcontinuous(ll, key=lambda m: resultindex[m]):
if len(l) == 1:
merged.append(l[0])
else:
start = l[0].start
end = l[-1].end
endindex = resultindex[l[-1]]
for mr in l:
del resultindex[mr]
merged.append(matchresult(start, end, text, None))
resultindex[merged[-1]] = endindex
return merged
================================================
FILE: explainshell/options.py
================================================
import re, collections, logging
from explainshell import store
tokenstate = collections.namedtuple('tokenstate', 'startpos endpos token')
logger = logging.getLogger(__name__)
def extract(manpage):
'''extract options from all paragraphs that have been classified as containing
options'''
for i, p in enumerate(manpage.paragraphs):
if p.is_option:
s, l = extract_option(p.cleantext())
if s or l:
expectsarg = any(x.expectsarg for x in s + l)
s = [x.flag for x in s]
l = [x.flag for x in l]
manpage.paragraphs[i] = store.option(p, s, l, expectsarg)
else:
logger.error("no options could be extracted from paragraph %r", p)
opt_regex = re.compile(r'''
(?P<opt>--?(?:\?|\#|(?:\w+-)*\w+)) # option starts with - or -- and can have - in the middle but not at the end, also allow '-?'
(?:
(?:\s?(=)?\s?) # -a=
(?P<argoptional>[<\[])? # -a=< or -a=[
(?:\s?(=)?\s?) # or maybe -a<=
(?P<arg>
(?(argoptional) # if we think we have an arg (we saw [ or <)
[^\]>]+ # either read everything until the closing ] or >
|
(?(2)
[-a-zA-Z]+ # or if we didn't see [ or < but just saw =, read all letters, e.g. -a=abc
|
[A-Z]+ # but if we didn't have =, only allow uppercase letters, e.g. -a FOO
)
)
)
(?(argoptional)(?P<argoptionalc>[\]>])) # read closing ] or > if we have an arg
)? # the whole arg thing is optional
(?P<ending>,\s*|\s+|\Z|/|\|)''', re.X) # read any trailing whitespace or the end of the string
opt2_regex = re.compile(r'''
(?P<opt>\w+) # an option that doesn't start with any of the usual characters, e.g. options from 'dd' like bs=BYTES
(?:
(?:\s*=\s*) # an optional arg, e.g. bs=BYTES
(?P<arg>\w+)
)
(?:,\s*|\s+|\Z)''', re.X) # end with , or whitespace or the end of the string
def _flag(s, pos=0):
'''
>>> _flag('a=b').groupdict()
{'opt': 'a', 'arg': 'b'}
>>> bool(_flag('---c-d'))
False
>>> bool(_flag('foobar'))
False
'''
m = opt2_regex.match(s, pos)
return m
def _option(s, pos=0):
'''
>>> bool(_option('-'))
False
>>> bool(_option('--'))
False
>>> bool(_option('---'))
False
>>> bool(_option('-a-'))
False
>>> bool(_option('--a-'))
False
>>> bool(_option('--a-b-'))
False
>>> sorted(_option('-a').groupdict().iteritems())
[('arg', None), ('argoptional', None), ('argoptionalc', None), ('ending', ''), ('opt', '-a')]
>>> sorted(_option('--a').groupdict().iteritems())
[('arg', None), ('argoptional', None), ('argoptionalc', None), ('ending', ''), ('opt', '--a')]
>>> sorted(_option('-a<b>').groupdict().iteritems())
[('arg', 'b'), ('argoptional', '<'), ('argoptionalc', '>'), ('ending', ''), ('opt', '-a')]
>>> sorted(_option('-a=[foo]').groupdict().iteritems())
[('arg', 'foo'), ('argoptional', '['), ('argoptionalc', ']'), ('ending', ''), ('opt', '-a')]
>>> sorted(_option('-a=<foo>').groupdict().iteritems())
[('arg', 'foo'), ('argoptional', '<'), ('argoptionalc', '>'), ('ending', ''), ('opt', '-a')]
>>> sorted(_option('-a=<foo bar>').groupdict().iteritems())
[('arg', 'foo bar'), ('argoptional', '<'), ('argoptionalc', '>'), ('ending', ''), ('opt', '-a')]
>>> sorted(_option('-a=foo').groupdict().iteritems())
[('arg', 'foo'), ('argoptional', None), ('argoptionalc', None), ('ending', ''), ('opt', '-a')]
>>> bool(_option('-a=[foo>'))
False
>>> bool(_option('-a=[foo bar'))
False
>>> _option('-a foo').end(0)
3
'''
m = opt_regex.match(s, pos)
if m:
if m.group('argoptional'):
c = m.group('argoptional')
cc = m.group('argoptionalc')
if (c == '[' and cc == ']') or (c == '<' and cc == '>'):
return m
else:
return
return m
_eatbetweenregex = re.compile(r'\s*(?:or|,|\|)\s*')
def _eatbetween(s, pos):
'''
>>> _eatbetween('foo', 0)
0
>>> _eatbetween('a, b', 1)
3
>>> _eatbetween('a|b', 1)
2
>>> _eatbetween('a or b', 1)
5
'''
m = _eatbetweenregex.match(s, pos)
if m:
return m.end(0)
return pos
class extractedoption(collections.namedtuple('extractedoption', 'flag expectsarg')):
def __eq__(self, other):
if isinstance(other, str):
return self.flag == other
else:
return super(extractedoption, self).__eq__(other)
def __str__(self):
return self.flag
def extract_option(txt):
'''this is where the magic is (suppose) to happen. try and find options
using a regex'''
startpos = currpos = len(txt) - len(txt.lstrip())
short, long = [], []
m = _option(txt, currpos)
# keep going as long as options are found
while m:
s = m.group('opt')
po = extractedoption(s, m.group('arg'))
if s.startswith('--'):
long.append(po)
else:
short.append(po)
currpos = m.end(0)
currpos = _eatbetween(txt, currpos)
if m.group('ending') == '|':
m = _option(txt, currpos)
if not m:
startpos = currpos
while currpos < len(txt) and not txt[currpos].isspace():
if txt[currpos] == '|':
short.append(extractedoption(txt[startpos:currpos], None))
startpos = currpos
currpos += 1
leftover = txt[startpos:currpos]
if leftover:
short.append(extractedoption(leftover, None))
else:
m = _option(txt, currpos)
if currpos == startpos:
m = _flag(txt, currpos)
while m:
s = m.group('opt')
po = extractedoption(s, m.group('arg'))
long.append(po)
currpos = m.end(0)
currpos = _eatbetween(txt, currpos)
m = _flag(txt, currpos)
return short, long
================================================
FILE: explainshell/store.py
================================================
'''data objects to save processed man pages to mongodb'''
import pymongo, collections, re, logging
from explainshell import errors, util, helpconstants, config
logger = logging.getLogger(__name__)
class classifiermanpage(collections.namedtuple('classifiermanpage', 'name paragraphs')):
'''a man page that had its paragraphs manually tagged as containing options
or not'''
@staticmethod
def from_store(d):
m = classifiermanpage(d['name'], [paragraph.from_store(p) for p in d['paragraphs']])
return m
def to_store(self):
return {'name' : self.name,
'paragraphs' : [p.to_store() for p in self.paragraphs]}
class paragraph(object):
'''a paragraph inside a man page is text that ends with two new lines'''
def __init__(self, idx, text, section, is_option):
self.idx = idx
self.text = text
self.section = section
self.is_option = is_option
def cleantext(self):
t = re.sub(r'<[^>]+>', '', self.text)
t = re.sub('<', '<', t)
t = re.sub('>', '>', t)
return t
@staticmethod
def from_store(d):
p = paragraph(d.get('idx', 0), d['text'].encode('utf8'), d['section'], d['is_option'])
return p
def to_store(self):
return {'idx' : self.idx, 'text' : self.text, 'section' : self.section,
'is_option' : self.is_option}
def __repr__(self):
t = self.cleantext()
t = t[:min(20, t.find('\n'))].lstrip()
return '<paragraph %d, %s: %r>' % (self.idx, self.section, t)
def __eq__(self, other):
if not other:
return False
return self.__dict__ == other.__dict__
class option(paragraph):
'''a paragraph that contains extracted options
short - a list of short options (-a, -b, ..)
long - a list of long options (--a, --b)
expectsarg - specifies if one of the short/long options expects an additional argument
argument - specifies if to consider this as positional arguments
nestedcommand - specifies if the arguments to this option can start a nested command
'''
def __init__(self, p, short, long, expectsarg, argument=None, nestedcommand=False):
paragraph.__init__(self, p.idx, p.text, p.section, p.is_option)
self.short = short
self.long = long
self._opts = self.short + self.long
self.argument = argument
self.expectsarg = expectsarg
self.nestedcommand = nestedcommand
if nestedcommand:
assert expectsarg, 'an option that can nest commands must expect an argument'
@property
def opts(self):
return self._opts
@classmethod
def from_store(cls, d):
p = paragraph.from_store(d)
return cls(p, d['short'], d['long'], d['expectsarg'], d['argument'],
d.get('nestedcommand'))
def to_store(self):
d = paragraph.to_store(self)
assert d['is_option']
d['short'] = self.short
d['long'] = self.long
d['expectsarg'] = self.expectsarg
d['argument'] = self.argument
d['nestedcommand'] = self.nestedcommand
return d
def __str__(self):
return '(%s)' % ', '.join([str(x) for x in self.opts])
def __repr__(self):
return '<options for paragraph %d: %s>' % (self.idx, str(self))
class manpage(object):
'''processed man page
source - the path to the original source man page
name - the name of this man page as extracted by manpage.manpage
synopsis - the synopsis of this man page as extracted by manpage.manpage
paragraphs - a list of paragraphs (and options) that contain all of the text and options
extracted from this man page
aliases - a list of aliases found for this man page
partialmatch - allow interperting options without a leading '-'
multicommand - consider sub commands when explaining a command with this man page,
e.g. git -> git commit
updated - whether this man page was manually updated
nestedcommand - specifies if positional arguments to this program can start a nested command,
e.g. sudo, xargs
'''
def __init__(self, source, name, synopsis, paragraphs, aliases,
partialmatch=False, multicommand=False, updated=False,
nestedcommand=False):
self.source = source
self.name = name
self.synopsis = synopsis
self.paragraphs = paragraphs
self.aliases = aliases
self.partialmatch = partialmatch
self.multicommand = multicommand
self.updated = updated
self.nestedcommand = nestedcommand
def removeoption(self, idx):
for i, p in self.paragraphs:
if p.idx == idx:
if not isinstance(p, option):
raise ValueError("paragraph %d isn't an option" % idx)
self.paragraphs[i] = paragraph(p.idx, p.text, p.section, False)
return
raise ValueError('idx %d not found' % idx)
@property
def namesection(self):
name, section = util.namesection(self.source[:-3])
return '%s(%s)' % (name, section)
@property
def section(self):
name, section = util.namesection(self.source[:-3])
return section
@property
def options(self):
return [p for p in self.paragraphs if isinstance(p, option)]
@property
def arguments(self):
# go over all paragraphs and look for those with the same 'argument'
# field
groups = collections.OrderedDict()
for opt in self.options:
if opt.argument:
groups.setdefault(opt.argument, []).append(opt)
# merge all the paragraphs under the same argument to a single string
for k, l in groups.iteritems():
groups[k] = '\n\n'.join([p.text for p in l])
return groups
@property
def synopsisnoname(self):
return re.match(r'[\w|-]+ - (.*)$', self.synopsis).group(1)
def find_option(self, flag):
for option in self.options:
for o in option.opts:
if o == flag:
return option
def to_store(self):
return {'source' : self.source, 'name' : self.name, 'synopsis' : self.synopsis,
'paragraphs' : [p.to_store() for p in self.paragraphs],
'aliases' : self.aliases, 'partialmatch' : self.partialmatch,
'multicommand' : self.multicommand, 'updated' : self.updated,
'nestedcommand' : self.nestedcommand}
@staticmethod
def from_store(d):
paragraphs = []
for pd in d.get('paragraphs', []):
pp = paragraph.from_store(pd)
if pp.is_option == True and 'short' in pd:
pp = option.from_store(pd)
paragraphs.append(pp)
synopsis = d['synopsis']
if synopsis:
synopsis = synopsis.encode('utf8')
else:
synopsis = helpconstants.NOSYNOPSIS
return manpage(d['source'], d['name'], synopsis, paragraphs,
[tuple(x) for x in d['aliases']], d['partialmatch'],
d['multicommand'], d['updated'], d.get('nestedcommand'))
@staticmethod
def from_store_name_only(name, source):
return manpage(source, name, None, [], [], None, None, None)
def __repr__(self):
return '<manpage %r(%s), %d options>' % (self.name, self.section, len(self.options))
class store(object):
'''read/write processed man pages from mongodb
we use three collections:
1) classifier - contains manually tagged paragraphs from man pages
2) manpage - contains a processed man page
3) mapping - contains (name, manpageid, score) tuples
'''
def __init__(self, db='explainshell', host=config.MONGO_URI):
logger.info('creating store, db = %r, host = %r', db, host)
self.connection = pymongo.MongoClient(host)
self.db = self.connection[db]
self.classifier = self.db['classifier']
self.manpage = self.db['manpage']
self.mapping = self.db['mapping']
def close(self):
self.connection.disconnect()
self.classifier = self.manpage = self.mapping = self.db = None
def drop(self, confirm=False):
if not confirm:
return
logger.info('dropping mapping, manpage, collections')
self.mapping.drop()
self.manpage.drop()
def trainingset(self):
for d in self.classifier.find():
yield classifiermanpage.from_store(d)
def __contains__(self, name):
c = self.mapping.find({'src' : name}).count()
return c > 0
def __iter__(self):
for d in self.manpage.find():
yield manpage.from_store(d)
def findmanpage(self, name):
'''find a man page by its name, everything following the last dot (.) in name,
is taken as the section of the man page
we return the man page found with the highest score, and a list of
suggestions that also matched the given name (only the first item
is prepopulated with the option data)'''
if name.endswith('.gz'):
logger.info('name ends with .gz, looking up an exact match by source')
d = self.manpage.find_one({'source':name})
if not d:
raise errors.ProgramDoesNotExist(name)
m = manpage.from_store(d)
logger.info('returning %s', m)
return [m]
section = None
origname = name
# don't try to look for a section if it's . (source)
if name != '.':
splitted = name.rsplit('.', 1)
name = splitted[0]
if len(splitted) > 1:
section = splitted[1]
logger.info('looking up manpage in mapping with src %r', name)
cursor = self.mapping.find({'src' : name})
count = cursor.count()
if not count:
raise errors.ProgramDoesNotExist(name)
dsts = dict(((d['dst'], d['score']) for d in cursor))
cursor = self.manpage.find({'_id' : {'$in' : list(dsts.keys())}}, {'name' : 1, 'source' : 1})
if cursor.count() != len(dsts):
logger.error('one of %r mappings is missing in manpage collection '
'(%d mappings, %d found)', dsts, len(dsts), cursor.count())
results = [(d.pop('_id'), manpage.from_store_name_only(**d)) for d in cursor]
results.sort(key=lambda x: dsts.get(x[0], 0), reverse=True)
logger.info('got %s', results)
if section is not None:
if len(results) > 1:
results.sort(key=lambda (oid, m): m.section == section, reverse=True)
logger.info(r'sorting %r so %s is first', results, section)
if not results[0][1].section == section:
raise errors.ProgramDoesNotExist(origname)
results.extend(self._discovermanpagesuggestions(results[0][0], results))
oid = results[0][0]
results = [x[1] for x in results]
results[0] = manpage.from_store(self.manpage.find_one({'_id' : oid}))
return results
def _discovermanpagesuggestions(self, oid, existing):
'''find suggestions for a given man page
oid is the objectid of the man page in question,
existing is a list of (oid, man page) of suggestions that were
already discovered
'''
skip = set([oid for oid, m in existing])
cursor = self.mapping.find({'dst' : oid})
# find all srcs that point to oid
srcs = [d['src'] for d in cursor]
# find all dsts of srcs
suggestionoids = self.mapping.find({'src' : {'$in' : srcs}}, {'dst' : 1})
# remove already discovered
suggestionoids = [d['dst'] for d in suggestionoids if d['dst'] not in skip]
if not suggestionoids:
return []
# get just the name and source of found suggestions
suggestionoids = self.manpage.find({'_id' : {'$in' : suggestionoids}},
{'name' : 1, 'source' : 1})
return [(d.pop('_id'), manpage.from_store_name_only(**d)) for d in suggestionoids]
def addmapping(self, src, dst, score):
self.mapping.insert({'src' : src, 'dst' : dst, 'score' : score})
def addmanpage(self, m):
'''add m into the store, if it exists first remove it and its mappings
each man page may have aliases besides the name determined by its
basename'''
d = self.manpage.find_one({'source' : m.source})
if d:
logger.info('removing old manpage %s (%s)', m.source, d['_id'])
self.manpage.remove(d['_id'])
# remove old mappings if there are any
c = self.mapping.count()
self.mapping.remove({'dst' : d['_id']})
c -= self.mapping.count()
logger.info('removed %d mappings for manpage %s', c, m.source)
o = self.manpage.insert(m.to_store())
for alias, score in m.aliases:
self.addmapping(alias, o, score)
logger.info('inserting mapping (alias) %s -> %s (%s) with score %d', alias, m.name, o, score)
return m
def updatemanpage(self, m):
'''update m and add new aliases if necessary
change updated attribute so we don't overwrite this in the future'''
logger.info('updating manpage %s', m.source)
m.updated = True
self.manpage.update({'source' : m.source}, m.to_store())
_id = self.manpage.find_one({'source' : m.source}, fields={'_id':1})['_id']
for alias, score in m.aliases:
if alias not in self:
self.addmapping(alias, _id, score)
logger.info('inserting mapping (alias) %s -> %s (%s) with score %d', alias, m.name, _id, score)
else:
logger.debug('mapping (alias) %s -> %s (%s) already exists', alias, m.name, _id)
return m
def verify(self):
# check that everything in manpage is reachable
mappings = list(self.mapping.find())
reachable = set([m['dst'] for m in mappings])
manpages = set([m['_id'] for m in self.manpage.find(fields={'_id':1})])
ok = True
unreachable = manpages - reachable
if unreachable:
logger.error('manpages %r are unreachable (nothing maps to them)', unreachable)
unreachable = [self.manpage.find_one({'_id' : u})['name'] for u in unreachable]
ok = False
notfound = reachable - manpages
if notfound:
logger.error('mappings to inexisting manpages: %r', notfound)
ok = False
return ok, unreachable, notfound
def names(self):
cursor = self.manpage.find(fields={'name':1})
for d in cursor:
yield d['_id'], d['name']
def mappings(self):
cursor = self.mapping.find(fields={'src':1})
for d in cursor:
yield d['src'], d['_id']
def setmulticommand(self, manpageid):
self.manpage.update({'_id' : manpageid}, {'$set' : {'multicommand' : True}})
================================================
FILE: explainshell/util.py
================================================
import itertools
from operator import itemgetter
def consecutive(l, fn):
'''yield consecutive items from l that fn returns True for them
>>> even = lambda x: x % 2 == 0
>>> list(consecutive([], even))
[]
>>> list(consecutive([1], even))
[[1]]
>>> list(consecutive([1, 2], even))
[[1], [2]]
>>> list(consecutive([2, 4], even))
[[2, 4]]
>>> list(consecutive([1, 2, 4], even))
[[1], [2, 4]]
>>> list(consecutive([1, 2, 4, 5, 7, 8, 10], even))
[[1], [2, 4], [5], [7], [8, 10]]
'''
it = iter(l)
ll = []
try:
while True:
x = it.next()
if fn(x):
ll.append(x)
else:
if ll:
yield ll
ll = []
yield [x]
except StopIteration:
if ll:
yield ll
def groupcontinuous(l, key=None):
'''
>>> list(groupcontinuous([1, 2, 4, 5, 7, 8, 10]))
[[1, 2], [4, 5], [7, 8], [10]]
>>> list(groupcontinuous(range(5)))
[[0, 1, 2, 3, 4]]
'''
if key is None:
key = lambda x: x
for k, g in itertools.groupby(enumerate(l), lambda (i, x): i-key(x)):
yield map(itemgetter(1), g)
def toposorted(graph, parents):
"""
Returns vertices of a DAG in topological order.
Arguments:
graph -- vetices of a graph to be toposorted
parents -- function (vertex) -> vertices to preceed
given vertex in output
"""
result = []
used = set()
def use(v, top):
if id(v) in used:
return
for parent in parents(v):
if parent is top:
raise ValueError('graph is cyclical', graph)
use(parent, v)
used.add(id(v))
result.append(v)
for v in graph:
use(v, v)
return result
def pairwise(iterable):
a, b = itertools.tee(iterable)
next(b, None)
return itertools.izip(a, b)
class peekable(object):
'''
>>> it = peekable(iter('abc'))
>>> it.index, it.peek(), it.index, it.peek(), it.next(), it.index, it.peek(), it.next(), it.next(), it.index
(0, 'a', 0, 'a', 'a', 1, 'b', 'b', 'c', 3)
>>> it.peek()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
>>> it.peek()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
>>> it.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
'''
def __init__(self, it):
self.it = it
self._peeked = False
self._peekvalue = None
self._idx = 0
def __iter__(self):
return self
def next(self):
if self._peeked:
self._peeked = False
self._idx += 1
return self._peekvalue
n = self.it.next()
self._idx += 1
return n
def hasnext(self):
try:
self.peek()
return True
except StopIteration:
return False
def peek(self):
if self._peeked:
return self._peekvalue
else:
self._peekvalue = self.it.next()
self._peeked = True
return self._peekvalue
@property
def index(self):
'''return the index of the next item returned by next()'''
return self._idx
def namesection(path):
assert '.gz' not in path
name, section = path.rsplit('.', 1)
return name, section
class propertycache(object):
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, type=None):
result = self.func(obj)
self.cachevalue(obj, result)
return result
def cachevalue(self, obj, value):
setattr(obj, self.name, value)
================================================
FILE: explainshell/web/__init__.py
================================================
from flask import Flask
app = Flask(__name__)
from explainshell.web import views
from explainshell import store, config
if config.DEBUG:
from explainshell.web import debugviews
app.config.from_object(config)
================================================
FILE: explainshell/web/debugviews.py
================================================
import logging
from flask import render_template, request, abort, redirect, url_for, json
from explainshell import manager, config, store
from explainshell.web import app, helpers
logger = logging.getLogger(__name__)
@app.route('/debug')
def debug():
s = store.store('explainshell', config.MONGO_URI)
d = {'manpages' : []}
for mp in s:
synopsis = ''
if mp.synopsis:
synopsis = mp.synopsis[:20]
dd = {'name' : mp.name, 'synopsis' : synopsis}
l = []
for o in mp.options:
l.append(str(o))
dd['options'] = ', '.join(l)
d['manpages'].append(dd)
d['manpages'].sort(key=lambda d: d['name'].lower())
return render_template('debug.html', d=d)
def _convertvalue(value):
if isinstance(value, list):
return [s.strip() for s in value]
elif value.lower() == 'true':
return True
elif value:
return value.strip()
return False
@app.route('/debug/tag/<source>', methods=['GET', 'POST'])
def tag(source):
mngr = manager.manager(config.MONGO_URI, 'explainshell', [], False, False)
s = mngr.store
m = s.findmanpage(source)[0]
assert m
if 'paragraphs' in request.form:
paragraphs = json.loads(request.form['paragraphs'])
mparagraphs = []
for d in paragraphs:
idx = d['idx']
text = d['text']
section = d['section']
short = [s.strip() for s in d['short']]
long = [s.strip() for s in d['long']]
expectsarg = _convertvalue(d['expectsarg'])
nestedcommand = _convertvalue(d['nestedcommand'])
if isinstance(nestedcommand, str):
nestedcommand = [nestedcommand]
elif nestedcommand is True:
logger.error('nestedcommand %r must be a string or list', nestedcommand)
abort(503)
argument = d['argument']
if not argument:
argument = None
p = store.paragraph(idx, text, section, d['is_option'])
if d['is_option'] and (short or long or argument):
p = store.option(p, short, long, expectsarg, argument, nestedcommand)
mparagraphs.append(p)
if request.form.get('nestedcommand', '').lower() == 'true':
m.nestedcommand = True
else:
m.nestedcommand = False
m = mngr.edit(m, mparagraphs)
if m:
return redirect(url_for('explain', cmd=m.name))
else:
abort(503)
else:
helpers.convertparagraphs(m)
for p in m.paragraphs:
if isinstance(p, store.option):
if isinstance(p.expectsarg, list):
p.expectsarg = ', '.join(p.expectsarg)
if isinstance(p.nestedcommand, list):
p.nestedcommand = ', '.join(p.nestedcommand)
return render_template('tagger.html', m=m)
================================================
FILE: explainshell/web/helpers.py
================================================
from explainshell import util
def convertparagraphs(manpage):
for p in manpage.paragraphs:
p.text = p.text.decode('utf-8')
return manpage
def suggestions(matches, command):
'''enrich command matches with links to other man pages with the
same name'''
for m in matches:
if 'name' in m and 'suggestions' in m:
before = command[:m['start']]
after = command[m['end']:]
newsuggestions = []
for othermp in sorted(m['suggestions'], key=lambda mp: mp.section):
mid = '%s.%s' % (othermp.name, othermp.section)
newsuggestions.append({'cmd' : ''.join([before, mid, after]),
'text' : othermp.namesection})
m['suggestions'] = newsuggestions
================================================
FILE: explainshell/web/static/css/bootstrap-responsive.css
================================================
/*!
* Bootstrap Responsive v2.3.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
line-height: 0;
content: "";
}
.clearfix:after {
clear: both;
}
.hide-text {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.input-block-level {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
@-ms-viewport {
width: device-width;
}
.hidden {
display: none;
visibility: hidden;
}
.visible-phone {
display: none !important;
}
.visible-tablet {
display: none !important;
}
.hidden-desktop {
display: none !important;
}
.visible-desktop {
display: inherit !important;
}
@media (min-width: 768px) and (max-width: 979px) {
.hidden-desktop {
display: inherit !important;
}
.visible-desktop {
display: none !important ;
}
.visible-tablet {
display: inherit !important;
}
.hidden-tablet {
display: none !important;
}
}
@media (max-width: 767px) {
.hidden-desktop {
display: inherit !important;
}
.visible-desktop {
display: none !important;
}
.visible-phone {
display: inherit !important;
}
.hidden-phone {
display: none !important;
}
}
.visible-print {
display: none !important;
}
@media print {
.visible-print {
display: inherit !important;
}
.hidden-print {
display: none !important;
}
}
@media (min-width: 1200px) {
.row {
margin-left: -30px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
line-height: 0;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 30px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 1170px;
}
.span12 {
width: 1170px;
}
.span11 {
width: 1070px;
}
.span10 {
width: 970px;
}
.span9 {
width: 870px;
}
.span8 {
width: 770px;
}
.span7 {
width: 670px;
}
.span6 {
width: 570px;
}
.span5 {
width: 470px;
}
.span4 {
width: 370px;
}
.span3 {
width: 270px;
}
.span2 {
width: 170px;
}
.span1 {
width: 70px;
}
.offset12 {
margin-left: 1230px;
}
.offset11 {
margin-left: 1130px;
}
.offset10 {
margin-left: 1030px;
}
.offset9 {
margin-left: 930px;
}
.offset8 {
margin-left: 830px;
}
.offset7 {
margin-left: 730px;
}
.offset6 {
margin-left: 630px;
}
.offset5 {
margin-left: 530px;
}
.offset4 {
margin-left: 430px;
}
.offset3 {
margin-left: 330px;
}
.offset2 {
margin-left: 230px;
}
.offset1 {
margin-left: 130px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
line-height: 0;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 30px;
margin-left: 2.564102564102564%;
*margin-left: 2.5109110747408616%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 2.564102564102564%;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.45299145299145%;
*width: 91.39979996362975%;
}
.row-fluid .span10 {
width: 82.90598290598291%;
*width: 82.8527914166212%;
}
.row-fluid .span9 {
width: 74.35897435897436%;
*width: 74.30578286961266%;
}
.row-fluid .span8 {
width: 65.81196581196582%;
*width: 65.75877432260411%;
}
.row-fluid .span7 {
width: 57.26495726495726%;
*width: 57.21176577559556%;
}
.row-fluid .span6 {
width: 48.717948717948715%;
*width: 48.664757228587014%;
}
.row-fluid .span5 {
width: 40.17094017094017%;
*width: 40.11774868157847%;
}
.row-fluid .span4 {
width: 31.623931623931625%;
*width: 31.570740134569924%;
}
.row-fluid .span3 {
width: 23.076923076923077%;
*width: 23.023731587561375%;
}
.row-fluid .span2 {
width: 14.52991452991453%;
*width: 14.476723040552828%;
}
.row-fluid .span1 {
width: 5.982905982905983%;
*width: 5.929714493544281%;
}
.row-fluid .offset12 {
margin-left: 105.12820512820512%;
*margin-left: 105.02182214948171%;
}
.row-fluid .offset12:first-child {
margin-left: 102.56410256410257%;
*margin-left: 102.45771958537915%;
}
.row-fluid .offset11 {
margin-left: 96.58119658119658%;
*margin-left: 96.47481360247316%;
}
.row-fluid .offset11:first-child {
margin-left: 94.01709401709402%;
*margin-left: 93.91071103837061%;
}
.row-fluid .offset10 {
margin-left: 88.03418803418803%;
*margin-left: 87.92780505546462%;
}
.row-fluid .offset10:first-child {
margin-left: 85.47008547008548%;
*margin-left: 85.36370249136206%;
}
.row-fluid .offset9 {
margin-left: 79.48717948717949%;
*margin-left: 79.38079650845607%;
}
.row-fluid .offset9:first-child {
margin-left: 76.92307692307693%;
*margin-left: 76.81669394435352%;
}
.row-fluid .offset8 {
margin-left: 70.94017094017094%;
*margin-left: 70.83378796144753%;
}
.row-fluid .offset8:first-child {
margin-left: 68.37606837606839%;
*margin-left: 68.26968539734497%;
}
.row-fluid .offset7 {
margin-left: 62.393162393162385%;
*margin-left: 62.28677941443899%;
}
.row-fluid .offset7:first-child {
margin-left: 59.82905982905982%;
*margin-left: 59.72267685033642%;
}
.row-fluid .offset6 {
margin-left: 53.84615384615384%;
*margin-left: 53.739770867430444%;
}
.row-fluid .offset6:first-child {
margin-left: 51.28205128205128%;
*margin-left: 51.175668303327875%;
}
.row-fluid .offset5 {
margin-left: 45.299145299145295%;
*margin-left: 45.1927623204219%;
}
.row-fluid .offset5:first-child {
margin-left: 42.73504273504273%;
*margin-left: 42.62865975631933%;
}
.row-fluid .offset4 {
margin-left: 36.75213675213675%;
*margin-left: 36.645753773413354%;
}
.row-fluid .offset4:first-child {
margin-left: 34.18803418803419%;
*margin-left: 34.081651209310785%;
}
.row-fluid .offset3 {
margin-left: 28.205128205128204%;
*margin-left: 28.0987452264048%;
}
.row-fluid .offset3:first-child {
margin-left: 25.641025641025642%;
*margin-left: 25.53464266230224%;
}
.row-fluid .offset2 {
margin-left: 19.65811965811966%;
*margin-left: 19.551736679396257%;
}
.row-fluid .offset2:first-child {
margin-left: 17.094017094017094%;
*margin-left: 16.98763411529369%;
}
.row-fluid .offset1 {
margin-left: 11.11111111111111%;
*margin-left: 11.004728132387708%;
}
.row-fluid .offset1:first-child {
margin-left: 8.547008547008547%;
*margin-left: 8.440625568285142%;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 30px;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 1156px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 1056px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 956px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 856px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 756px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 656px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 556px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 456px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 356px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 256px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 156px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 56px;
}
.thumbnails {
margin-left: -30px;
}
.thumbnails > li {
margin-left: 30px;
}
.row-fluid .thumbnails {
margin-left: 0;
}
}
@media (min-width: 768px) and (max-width: 979px) {
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
line-height: 0;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 20px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 724px;
}
.span12 {
width: 724px;
}
.span11 {
width: 662px;
}
.span10 {
width: 600px;
}
.span9 {
width: 538px;
}
.span8 {
width: 476px;
}
.span7 {
width: 414px;
}
.span6 {
width: 352px;
}
.span5 {
width: 290px;
}
.span4 {
width: 228px;
}
.span3 {
width: 166px;
}
.span2 {
width: 104px;
}
.span1 {
width: 42px;
}
.offset12 {
margin-left: 764px;
}
.offset11 {
margin-left: 702px;
}
.offset10 {
margin-left: 640px;
}
.offset9 {
margin-left: 578px;
}
.offset8 {
margin-left: 516px;
}
.offset7 {
margin-left: 454px;
}
.offset6 {
margin-left: 392px;
}
.offset5 {
margin-left: 330px;
}
.offset4 {
margin-left: 268px;
}
.offset3 {
margin-left: 206px;
}
.offset2 {
margin-left: 144px;
}
.offset1 {
margin-left: 82px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
line-height: 0;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 30px;
margin-left: 2.7624309392265194%;
*margin-left: 2.709239449864817%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 2.7624309392265194%;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.43646408839778%;
*width: 91.38327259903608%;
}
.row-fluid .span10 {
width: 82.87292817679558%;
*width: 82.81973668743387%;
}
.row-fluid .span9 {
width: 74.30939226519337%;
*width: 74.25620077583166%;
}
.row-fluid .span8 {
width: 65.74585635359117%;
*width: 65.69266486422946%;
}
.row-fluid .span7 {
width: 57.18232044198895%;
*width: 57.12912895262725%;
}
.row-fluid .span6 {
width: 48.61878453038674%;
*width: 48.56559304102504%;
}
.row-fluid .span5 {
width: 40.05524861878453%;
*width: 40.00205712942283%;
}
.row-fluid .span4 {
width: 31.491712707182323%;
*width: 31.43852121782062%;
}
.row-fluid .span3 {
width: 22.92817679558011%;
*width: 22.87498530621841%;
}
.row-fluid .span2 {
width: 14.3646408839779%;
*width: 14.311449394616199%;
}
.row-fluid .span1 {
width: 5.801104972375691%;
*width: 5.747913483013988%;
}
.row-fluid .offset12 {
margin-left: 105.52486187845304%;
*margin-left: 105.41847889972962%;
}
.row-fluid .offset12:first-child {
margin-left: 102.76243093922652%;
*margin-left: 102.6560479605031%;
}
.row-fluid .offset11 {
margin-left: 96.96132596685082%;
*margin-left: 96.8549429881274%;
}
.row-fluid .offset11:first-child {
margin-left: 94.1988950276243%;
*margin-left: 94.09251204890089%;
}
.row-fluid .offset10 {
margin-left: 88.39779005524862%;
*margin-left: 88.2914070765252%;
}
.row-fluid .offset10:first-child {
margin-left: 85.6353591160221%;
*margin-left: 85.52897613729868%;
}
.row-fluid .offset9 {
margin-left: 79.8342541436464%;
*margin-left: 79.72787116492299%;
}
.row-fluid .offset9:first-child {
margin-left: 77.07182320441989%;
*margin-left: 76.96544022569647%;
}
.row-fluid .offset8 {
margin-left: 71.2707182320442%;
*margin-left: 71.16433525332079%;
}
.row-fluid .offset8:first-child {
margin-left: 68.50828729281768%;
*margin-left: 68.40190431409427%;
}
.row-fluid .offset7 {
margin-left: 62.70718232044199%;
*margin-left: 62.600799341718584%;
}
.row-fluid .offset7:first-child {
margin-left: 59.94475138121547%;
*margin-left: 59.838368402492065%;
}
.row-fluid .offset6 {
margin-left: 54.14364640883978%;
*margin-left: 54.037263430116376%;
}
.row-fluid .offset6:first-child {
margin-left: 51.38121546961326%;
*margin-left: 51.27483249088986%;
}
.row-fluid .offset5 {
margin-left: 45.58011049723757%;
*margin-left: 45.47372751851417%;
}
.row-fluid .offset5:first-child {
margin-left: 42.81767955801105%;
*margin-left: 42.71129657928765%;
}
.row-fluid .offset4 {
margin-left: 37.01657458563536%;
*margin-left: 36.91019160691196%;
}
.row-fluid .offset4:first-child {
margin-left: 34.25414364640884%;
*margin-left: 34.14776066768544%;
}
.row-fluid .offset3 {
margin-left: 28.45303867403315%;
*margin-left: 28.346655695309746%;
}
.row-fluid .offset3:first-child {
margin-left: 25.69060773480663%;
*margin-left: 25.584224756083227%;
}
.row-fluid .offset2 {
margin-left: 19.88950276243094%;
*margin-left: 19.783119783707537%;
}
.row-fluid .offset2:first-child {
margin-left: 17.12707182320442%;
*margin-left: 17.02068884448102%;
}
.row-fluid .offset1 {
margin-left: 11.32596685082873%;
*margin-left: 11.219583872105325%;
}
.row-fluid .offset1:first-child {
margin-left: 8.56353591160221%;
*margin-left: 8.457152932878806%;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 20px;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 710px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 648px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 586px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 524px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 462px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 400px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 338px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 276px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 214px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 152px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 90px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 28px;
}
}
@media (max-width: 767px) {
body {
padding-right: 20px;
padding-left: 20px;
}
.navbar-fixed-top,
.navbar-fixed-bottom,
.navbar-static-top {
margin-right: -20px;
margin-left: -20px;
}
.container-fluid {
padding: 0;
}
.dl-horizontal dt {
float: none;
width: auto;
clear: none;
text-align: left;
}
.dl-horizontal dd {
margin-left: 0;
}
.container {
width: auto;
}
.row-fluid {
width: 100%;
}
.row,
.thumbnails {
margin-left: 0;
}
.thumbnails > li {
float: none;
margin-left: 0;
}
[class*="span"],
.uneditable-input[class*="span"],
.row-fluid [class*="span"] {
display: block;
float: none;
width: 100%;
margin-left: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.span12,
.row-fluid .span12 {
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="offset"]:first-child {
margin-left: 0;
}
.input-large,
.input-xlarge,
.input-xxlarge,
input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.input-prepend input,
.input-append input,
.input-prepend input[class*="span"],
.input-append input[class*="span"] {
display: inline-block;
width: auto;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 0;
}
.modal {
position: fixed;
top: 20px;
right: 20px;
left: 20px;
width: auto;
margin: 0;
}
.modal.fade {
top: -100px;
}
.modal.fade.in {
top: 20px;
}
}
@media (max-width: 480px) {
.nav-collapse {
-webkit-transform: translate3d(0, 0, 0);
}
.page-header h1 small {
display: block;
line-height: 20px;
}
input[type="checkbox"],
input[type="radio"] {
border: 1px solid #ccc;
}
.form-horizontal .control-label {
float: none;
width: auto;
padding-top: 0;
text-align: left;
}
.form-horizontal .controls {
margin-left: 0;
}
.form-horizontal .control-list {
padding-top: 0;
}
.form-horizontal .form-actions {
padding-right: 10px;
padding-left: 10px;
}
.media .pull-left,
.media .pull-right {
display: block;
float: none;
margin-bottom: 10px;
}
.media-object {
margin-right: 0;
margin-left: 0;
}
.modal {
top: 10px;
right: 10px;
left: 10px;
}
.modal-header .close {
padding: 10px;
margin: -10px;
}
.carousel-caption {
position: static;
}
}
@media (max-width: 979px) {
body {
padding-top: 0;
}
.navbar-fixed-top,
.navbar-fixed-bottom {
position: static;
}
.navbar-fixed-top {
margin-bottom: 20px;
}
.navbar-fixed-bottom {
margin-top: 20px;
}
.navbar-fixed-top .navbar-inner,
.navbar-fixed-bottom .navbar-inner {
padding: 5px;
}
.navbar .container {
width: auto;
padding: 0;
}
.navbar .brand {
padding-right: 10px;
padding-left: 10px;
margin: 0 0 0 -5px;
}
.nav-collapse {
clear: both;
}
.nav-collapse .nav {
float: none;
margin: 0 0 10px;
}
.nav-collapse .nav > li {
float: none;
}
.nav-collapse .nav > li > a {
margin-bottom: 2px;
}
.nav-collapse .nav > .divider-vertical {
display: none;
}
.nav-collapse .nav .nav-header {
color: #777777;
text-shadow: none;
}
.nav-collapse .nav > li > a,
.nav-collapse .dropdown-menu a {
padding: 9px 15px;
font-weight: bold;
color: #777777;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.nav-collapse .btn {
padding: 4px 10px 4px;
font-weight: normal;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.nav-collapse .dropdown-menu li + li a {
margin-bottom: 2px;
}
.nav-collapse .nav > li > a:hover,
.nav-collapse .nav > li > a:focus,
.nav-collapse .dropdown-menu a:hover,
.nav-collapse .dropdown-menu a:focus {
background-color: #f2f2f2;
}
.navbar-inverse .nav-collapse .nav > li > a,
.navbar-inverse .nav-collapse .dropdown-menu a {
color: #999999;
}
.navbar-inverse .nav-collapse .nav > li > a:hover,
.navbar-inverse .nav-collapse .nav > li > a:focus,
.navbar-inverse .nav-collapse .dropdown-menu a:hover,
.navbar-inverse .nav-collapse .dropdown-menu a:focus {
background-color: #111111;
}
.nav-collapse.in .btn-group {
padding: 0;
margin-top: 5px;
}
.nav-collapse .dropdown-menu {
position: static;
top: auto;
left: auto;
display: none;
float: none;
max-width: none;
padding: 0;
margin: 0 15px;
background-color: transparent;
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.nav-collapse .open > .dropdown-menu {
display: block;
}
.nav-collapse .dropdown-menu:before,
.nav-collapse .dropdown-menu:after {
display: none;
}
.nav-collapse .dropdown-menu .divider {
display: none;
}
.nav-collapse .nav > li > .dropdown-menu:before,
.nav-collapse .nav > li > .dropdown-menu:after {
display: none;
}
.nav-collapse .navbar-form,
.nav-collapse .navbar-search {
float: none;
padding: 10px 15px;
margin: 10px 0;
border-top: 1px solid #f2f2f2;
border-bottom: 1px solid #f2f2f2;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
}
.navbar-inverse .nav-collapse .navbar-form,
.navbar-inverse .nav-collapse .navbar-search {
border-top-color: #111111;
border-bottom-color: #111111;
}
.navbar .nav-collapse .nav.pull-right {
float: none;
margin-left: 0;
}
.nav-collapse,
.nav-collapse.collapse {
height: 0;
overflow: hidden;
}
.navbar .btn-navbar {
display: block;
}
.navbar-static .navbar-inner {
padding-right: 10px;
padding-left: 10px;
}
}
@media (min-width: 980px) {
.nav-collapse.collapse {
height: auto !important;
overflow: visible !important;
}
}
================================================
FILE: explainshell/web/static/css/bootstrap.css
================================================
/*!
* Bootstrap v2.3.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
line-height: 0;
content: "";
}
.clearfix:after {
clear: both;
}
.hide-text {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.input-block-level {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section {
display: block;
}
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
audio:not([controls]) {
display: none;
}
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
a:focus {
outline: thin dotted #333;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
a:hover,
a:active {
outline: 0;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
img {
width: auto\9;
height: auto;
max-width: 100%;
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
}
#map_canvas img,
.google-maps img {
max-width: none;
}
button,
input,
select,
textarea {
margin: 0;
font-size: 100%;
vertical-align: middle;
}
button,
input {
*overflow: visible;
line-height: normal;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
cursor: pointer;
-webkit-appearance: button;
}
label,
select,
button,
input[type="button"],
input[type="reset"],
input[type="submit"],
input[type="radio"],
input[type="checkbox"] {
cursor: pointer;
}
input[type="search"] {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
textarea {
overflow: auto;
vertical-align: top;
}
@media print {
* {
color: #000 !important;
text-shadow: none !important;
background: transparent !important;
box-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
@page {
margin: 0.5cm;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333333;
background-color: #ffffff;
}
a {
color: #0088cc;
text-decoration: none;
}
a:hover,
a:focus {
color: #005580;
text-decoration: underline;
}
.img-rounded {
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
.img-polaroid {
padding: 4px;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.img-circle {
-webkit-border-radius: 500px;
-moz-border-radius: 500px;
border-radius: 500px;
}
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
line-height: 0;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 20px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 940px;
}
.span12 {
width: 940px;
}
.span11 {
width: 860px;
}
.span10 {
width: 780px;
}
.span9 {
width: 700px;
}
.span8 {
width: 620px;
}
.span7 {
width: 540px;
}
.span6 {
width: 460px;
}
.span5 {
width: 380px;
}
.span4 {
width: 300px;
}
.span3 {
width: 220px;
}
.span2 {
width: 140px;
}
.span1 {
width: 60px;
}
.offset12 {
margin-left: 980px;
}
.offset11 {
margin-left: 900px;
}
.offset10 {
margin-left: 820px;
}
.offset9 {
margin-left: 740px;
}
.offset8 {
margin-left: 660px;
}
.offset7 {
margin-left: 580px;
}
.offset6 {
margin-left: 500px;
}
.offset5 {
margin-left: 420px;
}
.offset4 {
margin-left: 340px;
}
.offset3 {
margin-left: 260px;
}
.offset2 {
margin-left: 180px;
}
.offset1 {
margin-left: 100px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
line-height: 0;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 30px;
margin-left: 2.127659574468085%;
*margin-left: 2.074468085106383%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 2.127659574468085%;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.48936170212765%;
*width: 91.43617021276594%;
}
.row-fluid .span10 {
width: 82.97872340425532%;
*width: 82.92553191489361%;
}
.row-fluid .span9 {
width: 74.46808510638297%;
*width: 74.41489361702126%;
}
.row-fluid .span8 {
width: 65.95744680851064%;
*width: 65.90425531914893%;
}
.row-fluid .span7 {
width: 57.44680851063829%;
*width: 57.39361702127659%;
}
.row-fluid .span6 {
width: 48.93617021276595%;
*width: 48.88297872340425%;
}
.row-fluid .span5 {
width: 40.42553191489362%;
*width: 40.37234042553192%;
}
.row-fluid .span4 {
width: 31.914893617021278%;
*width: 31.861702127659576%;
}
.row-fluid .span3 {
width: 23.404255319148934%;
*width: 23.351063829787233%;
}
.row-fluid .span2 {
width: 14.893617021276595%;
*width: 14.840425531914894%;
}
.row-fluid .span1 {
width: 6.382978723404255%;
*width: 6.329787234042553%;
}
.row-fluid .offset12 {
margin-left: 104.25531914893617%;
*margin-left: 104.14893617021275%;
}
.row-fluid .offset12:first-child {
margin-left: 102.12765957446808%;
*margin-left: 102.02127659574467%;
}
.row-fluid .offset11 {
margin-left: 95.74468085106382%;
*margin-left: 95.6382978723404%;
}
.row-fluid .offset11:first-child {
margin-left: 93.61702127659574%;
*margin-left: 93.51063829787232%;
}
.row-fluid .offset10 {
margin-left: 87.23404255319149%;
*margin-left: 87.12765957446807%;
}
.row-fluid .offset10:first-child {
margin-left: 85.1063829787234%;
*margin-left: 84.99999999999999%;
}
.row-fluid .offset9 {
margin-left: 78.72340425531914%;
*margin-left: 78.61702127659572%;
}
.row-fluid .offset9:first-child {
margin-left: 76.59574468085106%;
*margin-left: 76.48936170212764%;
}
.row-fluid .offset8 {
margin-left: 70.2127659574468%;
*margin-left: 70.10638297872339%;
}
.row-fluid .offset8:first-child {
margin-left: 68.08510638297872%;
*margin-left: 67.9787234042553%;
}
.row-fluid .offset7 {
margin-left: 61.70212765957446%;
*margin-left: 61.59574468085106%;
}
.row-fluid .offset7:first-child {
margin-left: 59.574468085106375%;
*margin-left: 59.46808510638297%;
}
.row-fluid .offset6 {
margin-left: 53.191489361702125%;
*margin-left: 53.085106382978715%;
}
.row-fluid .offset6:first-child {
margin-left: 51.063829787234035%;
*margin-left: 50.95744680851063%;
}
.row-fluid .offset5 {
margin-left: 44.68085106382979%;
*margin-left: 44.57446808510638%;
}
.row-fluid .offset5:first-child {
margin-left: 42.5531914893617%;
*margin-left: 42.4468085106383%;
}
.row-fluid .offset4 {
margin-left: 36.170212765957444%;
*margin-left: 36.06382978723405%;
}
.row-fluid .offset4:first-child {
margin-left: 34.04255319148936%;
*margin-left: 33.93617021276596%;
}
.row-fluid .offset3 {
margin-left: 27.659574468085104%;
*margin-left: 27.5531914893617%;
}
.row-fluid .offset3:first-child {
margin-left: 25.53191489361702%;
*margin-left: 25.425531914893618%;
}
.row-fluid .offset2 {
margin-left: 19.148936170212764%;
*margin-left: 19.04255319148936%;
}
.row-fluid .offset2:first-child {
margin-left: 17.02127659574468%;
*margin-left: 16.914893617021278%;
}
.row-fluid .offset1 {
margin-left: 10.638297872340425%;
*margin-left: 10.53191489361702%;
}
.row-fluid .offset1:first-child {
margin-left: 8.51063829787234%;
*margin-left: 8.404255319148938%;
}
[class*="span"].hide,
.row-fluid [class*="span"].hide {
display: none;
}
[class*="span"].pull-right,
.row-fluid [class*="span"].pull-right {
float: right;
}
.container {
margin-right: auto;
margin-left: auto;
*zoom: 1;
}
.container:before,
.container:after {
display: table;
line-height: 0;
content: "";
}
.container:after {
clear: both;
}
.container-fluid {
padding-right: 20px;
padding-left: 20px;
*zoom: 1;
}
.container-fluid:before,
.container-fluid:after {
display: table;
line-height: 0;
content: "";
}
.container-fluid:after {
clear: both;
}
p {
margin: 0 0 10px;
}
.lead {
margin-bottom: 20px;
font-size: 21px;
font-weight: 200;
line-height: 30px;
}
small {
font-size: 85%;
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
cite {
font-style: normal;
}
.muted {
color: #999999;
}
a.muted:hover,
a.muted:focus {
color: #808080;
}
.text-warning {
color: #c09853;
}
a.text-warning:hover,
a.text-warning:focus {
color: #a47e3c;
}
.text-error {
color: #b94a48;
}
a.text-error:hover,
a.text-error:focus {
color: #953b39;
}
.text-info {
color: #3a87ad;
}
a.text-info:hover,
a.text-info:focus {
color: #2d6987;
}
.text-success {
color: #468847;
}
a.text-success:hover,
a.text-success:focus {
color: #356635;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 10px 0;
font-family: inherit;
font-weight: bold;
line-height: 20px;
color: inherit;
text-rendering: optimizelegibility;
}
h1 small,
h2 small,
h3 small,
h4 small,
h5 small,
h6 small {
font-weight: normal;
line-height: 1;
color: #999999;
}
h1,
h2,
h3 {
line-height: 40px;
}
h1 {
font-size: 38.5px;
}
h2 {
font-size: 31.5px;
}
h3 {
font-size: 24.5px;
}
h4 {
font-size: 17.5px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 11.9px;
}
h1 small {
font-size: 24.5px;
}
h2 small {
font-size: 17.5px;
}
h3 small {
font-size: 14px;
}
h4 small {
font-size: 14px;
}
.page-header {
padding-bottom: 9px;
margin: 20px 0 30px;
border-bottom: 1px solid #eeeeee;
}
ul,
ol {
padding: 0;
margin: 0 0 10px 25px;
}
ul ul,
ul ol,
ol ol,
ol ul {
margin-bottom: 0;
}
li {
line-height: 20px;
}
ul.unstyled,
ol.unstyled {
margin-left: 0;
list-style: none;
}
ul.inline,
ol.inline {
margin-left: 0;
list-style: none;
}
ul.inline > li,
ol.inline > li {
display: inline-block;
*display: inline;
padding-right: 5px;
padding-left: 5px;
*zoom: 1;
}
dl {
margin-bottom: 20px;
}
dt,
dd {
line-height: 20px;
}
dt {
font-weight: bold;
}
dd {
margin-left: 10px;
}
.dl-horizontal {
*zoom: 1;
}
.dl-horizontal:before,
.dl-horizontal:after {
display: table;
line-height: 0;
content: "";
}
.dl-horizontal:after {
clear: both;
}
.dl-horizontal dt {
float: left;
width: 160px;
overflow: hidden;
clear: left;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.dl-horizontal dd {
margin-left: 180px;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px solid #eeeeee;
border-bottom: 1px solid #ffffff;
}
abbr[title],
abbr[data-original-title] {
cursor: help;
border-bottom: 1px dotted #999999;
}
abbr.initialism {
font-size: 90%;
text-transform: uppercase;
}
blockquote {
padding: 0 0 0 15px;
margin: 0 0 20px;
border-left: 5px solid #eeeeee;
}
blockquote p {
margin-bottom: 0;
font-size: 17.5px;
font-weight: 300;
line-height: 1.25;
}
blockquote small {
display: block;
line-height: 20px;
color: #999999;
}
blockquote small:before {
content: '\2014 \00A0';
}
blockquote.pull-right {
float: right;
padding-right: 15px;
padding-left: 0;
border-right: 5px solid #eeeeee;
border-left: 0;
}
blockquote.pull-right p,
blockquote.pull-right small {
text-align: right;
}
blockquote.pull-right small:before {
content: '';
}
blockquote.pull-right small:after {
content: '\00A0 \2014';
}
q:before,
q:after,
blockquote:before,
blockquote:after {
content: "";
}
address {
display: block;
margin-bottom: 20px;
font-style: normal;
line-height: 20px;
}
code,
pre {
padding: 0 3px 2px;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
font-size: 12px;
color: #333333;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
code {
padding: 2px 4px;
color: #d14;
white-space: nowrap;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
}
pre {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
line-height: 20px;
word-break: break-all;
word-wrap: break-word;
white-space: pre;
white-space: pre-wrap;
background-color: #f5f5f5;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.15);
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
pre.prettyprint {
margin-bottom: 20px;
}
pre code {
padding: 0;
color: inherit;
white-space: pre;
white-space: pre-wrap;
background-color: transparent;
border: 0;
}
.pre-scrollable {
max-height: 340px;
overflow-y: scroll;
}
form {
margin: 0 0 20px;
}
fieldset {
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: 20px;
font-size: 21px;
line-height: 40px;
color: #333333;
border: 0;
border-bottom: 1px solid #e5e5e5;
}
legend small {
font-size: 15px;
color: #999999;
}
label,
input,
button,
select,
textarea {
font-size: 14px;
font-weight: normal;
line-height: 20px;
}
input,
button,
select,
textarea {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
label {
display: block;
margin-bottom: 5px;
}
select,
textarea,
input[type="text"],
input[type="password"],
input[type="datetime"],
input[type="datetime-local"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="week"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="search"],
input[type="tel"],
input[type="color"],
.uneditable-input {
display: inline-block;
height: 20px;
padding: 4px 6px;
margin-bottom: 10px;
font-size: 14px;
line-height: 20px;
color: #555555;
vertical-align: middle;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
input,
textarea,
.uneditable-input {
width: 206px;
}
textarea {
height: auto;
}
textarea,
input[type="text"],
input[type="password"],
input[type="datetime"],
input[type="datetime-local"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="week"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="search"],
input[type="tel"],
input[type="color"],
.uneditable-input {
background-color: #ffffff;
border: 1px solid #cccccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-o-transition: border linear 0.2s, box-shadow linear 0.2s;
transition: border linear 0.2s, box-shadow linear 0.2s;
}
textarea:focus,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="datetime"]:focus,
input[type="datetime-local"]:focus,
input[type="date"]:focus,
input[type="month"]:focus,
input[type="time"]:focus,
input[type="week"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="url"]:focus,
input[type="search"]:focus,
input[type="tel"]:focus,
input[type="color"]:focus,
.uneditable-input:focus {
border-color: rgba(82, 168, 236, 0.8);
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
input[type="radio"],
input[type="checkbox"] {
margin: 4px 0 0;
margin-top: 1px \9;
*margin-top: 0;
line-height: normal;
}
input[type="file"],
input[type="image"],
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="radio"],
input[type="checkbox"] {
width: auto;
}
select,
input[type="file"] {
height: 30px;
/* In IE7, the height of the select element cannot be changed by height, only font-size */
*margin-top: 4px;
/* For IE7, add top margin to align select with labels */
line-height: 30px;
}
select {
width: 220px;
background-color: #ffffff;
border: 1px solid #cccccc;
}
select[multiple],
select[size] {
height: auto;
}
select:focus,
input[type="file"]:focus,
input[type="radio"]:focus,
input[type="checkbox"]:focus {
outline: thin dotted #333;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
.uneditable-input,
.uneditable-textarea {
color: #999999;
cursor: not-allowed;
background-color: #fcfcfc;
border-color: #cccccc;
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
-moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
}
.uneditable-input {
overflow: hidden;
white-space: nowrap;
}
.uneditable-textarea {
width: auto;
height: auto;
}
input:-moz-placeholder,
textarea:-moz-placeholder {
color: #999999;
}
input:-ms-input-placeholder,
textarea:-ms-input-placeholder {
color: #999999;
}
input::-webkit-input-placeholder,
textarea::-webkit-input-placeholder {
color: #999999;
}
.radio,
.checkbox {
min-height: 20px;
padding-left: 20px;
}
.radio input[type="radio"],
.checkbox input[type="checkbox"] {
float: left;
margin-left: -20px;
}
.controls > .radio:first-child,
.controls > .checkbox:first-child {
padding-top: 5px;
}
.radio.inline,
.checkbox.inline {
display: inline-block;
padding-top: 5px;
margin-bottom: 0;
vertical-align: middle;
}
.radio.inline + .radio.inline,
.checkbox.inline + .checkbox.inline {
margin-left: 10px;
}
.input-mini {
width: 60px;
}
.input-small {
width: 90px;
}
.input-medium {
width: 150px;
}
.input-large {
width: 210px;
}
.input-xlarge {
width: 270px;
}
.input-xxlarge {
width: 530px;
}
input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input[class*="span"],
.row-fluid input[class*="span"],
.row-fluid select[class*="span"],
.row-fluid textarea[class*="span"],
.row-fluid .uneditable-input[class*="span"] {
float: none;
margin-left: 0;
}
.input-append input[class*="span"],
.input-append .uneditable-input[class*="span"],
.input-prepend input[class*="span"],
.input-prepend .uneditable-input[class*="span"],
.row-fluid input[class*="span"],
.row-fluid select[class*="span"],
.row-fluid textarea[class*="span"],
.row-fluid .uneditable-input[class*="span"],
.row-fluid .input-prepend [class*="span"],
.row-fluid .input-append [class*="span"] {
display: inline-block;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 20px;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 926px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 846px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 766px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 686px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 606px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 526px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 446px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 366px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 286px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 206px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 126px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 46px;
}
.controls-row {
*zoom: 1;
}
.controls-row:before,
.controls-row:after {
display: table;
line-height: 0;
content: "";
}
.controls-row:after {
clear: both;
}
.controls-row [class*="span"],
.row-fluid .controls-row [class*="span"] {
float: left;
}
.controls-row .checkbox[class*="span"],
.controls-row .radio[class*="span"] {
padding-top: 5px;
}
input[disabled],
select[disabled],
textarea[disabled],
input[readonly],
select[readonly],
textarea[readonly] {
cursor: not-allowed;
background-color: #eeeeee;
}
input[type="radio"][disabled],
input[type="checkbox"][disabled],
input[type="radio"][readonly],
input[type="checkbox"][readonly] {
background-color: transparent;
}
.control-group.warning .control-label,
.control-group.warning .help-block,
.control-group.warning .help-inline {
color: #c09853;
}
.control-group.warning .checkbox,
.control-group.warning .radio,
.control-group.warning input,
.control-group.warning select,
.control-group.warning textarea {
color: #c09853;
}
.control-group.warning input,
.control-group.warning select,
.control-group.warning textarea {
border-color: #c09853;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.control-group.warning input:focus,
.control-group.warning select:focus,
.control-group.warning textarea:focus {
border-color: #a47e3c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
}
.control-group.warning .input-prepend .add-on,
.control-group.warning .input-append .add-on {
color: #c09853;
background-color: #fcf8e3;
border-color: #c09853;
}
.control-group.error .control-label,
.control-group.error .help-block,
.control-group.error .help-inline {
color: #b94a48;
}
.control-group.error .checkbox,
.control-group.error .radio,
.control-group.error input,
.control-group.error select,
.control-group.error textarea {
color: #b94a48;
}
.control-group.error input,
.control-group.error select,
.control-group.error textarea {
border-color: #b94a48;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.control-group.error input:focus,
.control-group.error select:focus,
.control-group.error textarea:focus {
border-color: #953b39;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
}
.control-group.error .input-prepend .add-on,
.control-group.error .input-append .add-on {
color: #b94a48;
background-color: #f2dede;
border-color: #b94a48;
}
.control-group.success .control-label,
.control-group.success .help-block,
.control-group.success .help-inline {
color: #468847;
}
.control-group.success .checkbox,
.control-group.success .radio,
.control-group.success input,
.control-group.success select,
.control-group.success textarea {
color: #468847;
}
.control-group.success input,
.control-group.success select,
.control-group.success textarea {
border-color: #468847;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.control-group.success input:focus,
.control-group.success select:focus,
.control-group.success textarea:focus {
border-color: #356635;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
}
.control-group.success .input-prepend .add-on,
.control-group.success .input-append .add-on {
color: #468847;
background-color: #dff0d8;
border-color: #468847;
}
.control-group.info .control-label,
.control-group.info .help-block,
.control-group.info .help-inline {
color: #3a87ad;
}
.control-group.info .checkbox,
.control-group.info .radio,
.control-group.info input,
.control-group.info select,
.control-group.info textarea {
color: #3a87ad;
}
.control-group.info input,
.control-group.info select,
.control-group.info textarea {
border-color: #3a87ad;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0,
gitextract_h90spds8/
├── .github/
│ └── workflows/
│ ├── build-docker-image.yml
│ ├── build-test.yml
│ └── lint-docker.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── dump/
│ └── explainshell/
│ ├── classifier.bson
│ └── system.indexes.bson
├── explainshell/
│ ├── __init__.py
│ ├── algo/
│ │ ├── __init__.py
│ │ ├── classifier.py
│ │ └── features.py
│ ├── config.py
│ ├── errors.py
│ ├── fixer.py
│ ├── helpconstants.py
│ ├── manager.py
│ ├── manpage.py
│ ├── matcher.py
│ ├── options.py
│ ├── store.py
│ ├── util.py
│ └── web/
│ ├── __init__.py
│ ├── debugviews.py
│ ├── helpers.py
│ ├── static/
│ │ ├── css/
│ │ │ ├── bootstrap-responsive.css
│ │ │ ├── bootstrap.css
│ │ │ ├── es.css
│ │ │ └── font-awesome.css
│ │ ├── font/
│ │ │ └── FontAwesome.otf
│ │ └── js/
│ │ ├── bootstrap.js
│ │ ├── d3.v3.js
│ │ ├── es.js
│ │ ├── jquery.js
│ │ └── underscore.js
│ ├── templates/
│ │ ├── about.html
│ │ ├── base.html
│ │ ├── debug.html
│ │ ├── errors/
│ │ │ ├── error.html
│ │ │ ├── missingmanpage.html
│ │ │ └── parsingerror.html
│ │ ├── explain.html
│ │ ├── index.html
│ │ ├── macros.html
│ │ ├── options.html
│ │ └── tagger.html
│ └── views.py
├── misc/
│ ├── crontab
│ ├── logrotate/
│ │ └── explainshell
│ ├── nginx/
│ │ └── explainshell.conf
│ ├── setup.bash
│ └── supervisord/
│ └── uwsgi.conf
├── requirements.txt
├── runserver.py
├── tests/
│ ├── __init__.py
│ ├── helpers.py
│ ├── test-fixer.py
│ ├── test-integration.py
│ ├── test-manager.py
│ ├── test-manpage.py
│ ├── test-matcher.py
│ └── test-options.py
└── tools/
├── dlgzlist
├── extractgzlist
├── shellbuiltins.py
└── w3mman2html.cgi
SYMBOL INDEX (789 symbols across 26 files)
FILE: explainshell/algo/classifier.py
function get_features (line 12) | def get_features(paragraph):
class classifier (line 29) | class classifier(object):
method __init__ (line 32) | def __init__(self, store, algo, **classifier_args):
method train (line 38) | def train(self):
method evaluate (line 76) | def evaluate(self):
method classify (line 96) | def classify(self, manpage):
FILE: explainshell/algo/features.py
function extract_first_line (line 3) | def extract_first_line(paragraph):
function starts_with_hyphen (line 26) | def starts_with_hyphen(paragraph):
function is_indented (line 29) | def is_indented(paragraph):
function par_length (line 32) | def par_length(paragraph):
function first_line_contains (line 35) | def first_line_contains(paragraph, what):
function first_line_length (line 39) | def first_line_length(paragraph):
function first_line_word_count (line 43) | def first_line_word_count(paragraph):
function is_good_section (line 49) | def is_good_section(paragraph):
function word_count (line 59) | def word_count(text):
function has_bold (line 62) | def has_bold(html):
FILE: explainshell/errors.py
class ProgramDoesNotExist (line 1) | class ProgramDoesNotExist(Exception):
class EmptyManpage (line 4) | class EmptyManpage(Exception):
FILE: explainshell/fixer.py
class basefixer (line 5) | class basefixer(object):
method __init__ (line 13) | def __init__(self, mctx):
method pre_get_raw_manpage (line 18) | def pre_get_raw_manpage(self):
method pre_parse_manpage (line 21) | def pre_parse_manpage(self):
method post_parse_manpage (line 24) | def post_parse_manpage(self):
method pre_classify (line 27) | def pre_classify(self):
method post_classify (line 30) | def post_classify(self):
method post_option_extraction (line 33) | def post_option_extraction(self):
method pre_add_manpage (line 36) | def pre_add_manpage(self):
class runner (line 42) | class runner(object):
method __init__ (line 44) | def __init__(self, mctx):
method disable (line 48) | def disable(self, name):
method _fixers (line 54) | def _fixers(self):
method pre_get_raw_manpage (line 57) | def pre_get_raw_manpage(self):
method pre_parse_manpage (line 61) | def pre_parse_manpage(self):
method post_parse_manpage (line 65) | def post_parse_manpage(self):
method pre_classify (line 69) | def pre_classify(self):
method post_classify (line 73) | def post_classify(self):
method post_option_extraction (line 77) | def post_option_extraction(self):
method pre_add_manpage (line 81) | def pre_add_manpage(self):
function register (line 85) | def register(fixercls):
class bulletremover (line 94) | class bulletremover(basefixer):
method post_parse_manpage (line 96) | def post_parse_manpage(self):
class leadingspaceremover (line 110) | class leadingspaceremover(basefixer):
method post_option_extraction (line 114) | def post_option_extraction(self):
method _removewhitespace (line 119) | def _removewhitespace(self, text):
class tarfixer (line 130) | class tarfixer(basefixer):
method __init__ (line 131) | def __init__(self, *args):
method pre_add_manpage (line 135) | def pre_add_manpage(self):
class paragraphjoiner (line 139) | class paragraphjoiner(basefixer):
method post_option_extraction (line 143) | def post_option_extraction(self):
method _join (line 147) | def _join(self, paragraphs, options):
class optiontrimmer (line 172) | class optiontrimmer(basefixer):
method __init__ (line 177) | def __init__(self, mctx):
method post_classify (line 181) | def post_classify(self):
function _parents (line 195) | def _parents(fixercls):
FILE: explainshell/helpconstants.py
function _addwords (line 237) | def _addwords(key, text, *words):
FILE: explainshell/manager.py
class managerctx (line 8) | class managerctx(object):
method __init__ (line 9) | def __init__(self, classifier, store, manpage):
class manager (line 20) | class manager(object):
method __init__ (line 23) | def __init__(self, dbhost, dbname, paths, overwrite=False, drop=False):
method ctx (line 35) | def ctx(self, m):
method _read (line 38) | def _read(self, ctx, frunner):
method _classify (line 48) | def _classify(self, ctx, frunner):
method _extract (line 54) | def _extract(self, ctx, frunner):
method _write (line 60) | def _write(self, ctx, frunner):
method _update (line 64) | def _update(self, ctx, frunner):
method process (line 68) | def process(self, ctx):
method edit (line 78) | def edit(self, m, paragraphs=None):
method run (line 91) | def run(self):
method findmulticommands (line 132) | def findmulticommands(self):
function main (line 162) | def main(files, dbname, dbhost, overwrite, drop, verify):
FILE: explainshell/manpage.py
function extractname (line 16) | def extractname(gzname):
function bold (line 33) | def bold(l):
function _parsetext (line 118) | def _parsetext(lines):
function _parsesynopsis (line 151) | def _parsesynopsis(base, synopsis):
class manpage (line 161) | class manpage(object):
method __init__ (line 171) | def __init__(self, path):
method read (line 180) | def read(self):
method parse (line 191) | def parse(self):
FILE: explainshell/matcher.py
class matchgroup (line 8) | class matchgroup(object):
method __init__ (line 13) | def __init__(self, name):
method __repr__ (line 17) | def __repr__(self):
class matchresult (line 20) | class matchresult(collections.namedtuple('matchresult', 'start end text ...
method unknown (line 22) | def unknown(self):
class matcher (line 30) | class matcher(bashlex.ast.nodevisitor):
method __init__ (line 34) | def __init__(self, s, store):
method _generatecommandgroupname (line 63) | def _generatecommandgroupname(self):
method matches (line 68) | def matches(self):
method allmatches (line 73) | def allmatches(self):
method manpage (line 77) | def manpage(self):
method find_option (line 85) | def find_option(self, opt):
method findmanpages (line 90) | def findmanpages(self, prog):
method unknown (line 97) | def unknown(self, token, start, end):
method visitreservedword (line 101) | def visitreservedword(self, node, word):
method visitoperator (line 114) | def visitoperator(self, node, op):
method visitpipe (line 125) | def visitpipe(self, node, pipe):
method visitredirect (line 129) | def visitredirect(self, node, input, type, output, heredoc):
method visitcommand (line 150) | def visitcommand(self, node, parts):
method visitif (line 197) | def visitif(self, *args):
method visitfor (line 199) | def visitfor(self, node, parts):
method visitwhile (line 217) | def visitwhile(self, *args):
method visituntil (line 219) | def visituntil(self, *args):
method visitnodeend (line 222) | def visitnodeend(self, node):
method startcommand (line 237) | def startcommand(self, commandnode, parts, endword, addgroup=True):
method endcommand (line 312) | def endcommand(self):
method visitcommandsubstitution (line 319) | def visitcommandsubstitution(self, node, command):
method visitprocesssubstitution (line 331) | def visitprocesssubstitution(self, node, command):
method visitassignment (line 340) | def visitassignment(self, node, word):
method visitword (line 344) | def visitword(self, node, word):
method visitfunction (line 479) | def visitfunction(self, node, name, body, parts):
method visittilde (line 520) | def visittilde(self, node, value):
method visitparameter (line 524) | def visitparameter(self, node, value):
method match (line 534) | def match(self):
method _markunparsedunknown (line 582) | def _markunparsedunknown(self):
method _resultindex (line 614) | def _resultindex(self):
method _mergeadjacent (line 624) | def _mergeadjacent(self, matches):
FILE: explainshell/options.py
function extract (line 9) | def extract(manpage):
function _flag (line 52) | def _flag(s, pos=0):
function _option (line 64) | def _option(s, pos=0):
function _eatbetween (line 112) | def _eatbetween(s, pos):
class extractedoption (line 128) | class extractedoption(collections.namedtuple('extractedoption', 'flag ex...
method __eq__ (line 129) | def __eq__(self, other):
method __str__ (line 135) | def __str__(self):
function extract_option (line 138) | def extract_option(txt):
FILE: explainshell/store.py
class classifiermanpage (line 8) | class classifiermanpage(collections.namedtuple('classifiermanpage', 'nam...
method from_store (line 12) | def from_store(d):
method to_store (line 16) | def to_store(self):
class paragraph (line 20) | class paragraph(object):
method __init__ (line 22) | def __init__(self, idx, text, section, is_option):
method cleantext (line 28) | def cleantext(self):
method from_store (line 35) | def from_store(d):
method to_store (line 39) | def to_store(self):
method __repr__ (line 43) | def __repr__(self):
method __eq__ (line 48) | def __eq__(self, other):
class option (line 53) | class option(paragraph):
method __init__ (line 62) | def __init__(self, p, short, long, expectsarg, argument=None, nestedco...
method opts (line 74) | def opts(self):
method from_store (line 78) | def from_store(cls, d):
method to_store (line 84) | def to_store(self):
method __str__ (line 94) | def __str__(self):
method __repr__ (line 97) | def __repr__(self):
class manpage (line 100) | class manpage(object):
method __init__ (line 116) | def __init__(self, source, name, synopsis, paragraphs, aliases,
method removeoption (line 129) | def removeoption(self, idx):
method namesection (line 139) | def namesection(self):
method section (line 144) | def section(self):
method options (line 149) | def options(self):
method arguments (line 153) | def arguments(self):
method synopsisnoname (line 168) | def synopsisnoname(self):
method find_option (line 171) | def find_option(self, flag):
method to_store (line 177) | def to_store(self):
method from_store (line 185) | def from_store(d):
method from_store_name_only (line 204) | def from_store_name_only(name, source):
method __repr__ (line 207) | def __repr__(self):
class store (line 210) | class store(object):
method __init__ (line 218) | def __init__(self, db='explainshell', host=config.MONGO_URI):
method close (line 226) | def close(self):
method drop (line 230) | def drop(self, confirm=False):
method trainingset (line 238) | def trainingset(self):
method __contains__ (line 242) | def __contains__(self, name):
method __iter__ (line 246) | def __iter__(self):
method findmanpage (line 250) | def findmanpage(self, name):
method _discovermanpagesuggestions (line 303) | def _discovermanpagesuggestions(self, oid, existing):
method addmapping (line 326) | def addmapping(self, src, dst, score):
method addmanpage (line 329) | def addmanpage(self, m):
method updatemanpage (line 352) | def updatemanpage(self, m):
method verify (line 368) | def verify(self):
method names (line 388) | def names(self):
method mappings (line 393) | def mappings(self):
method setmulticommand (line 398) | def setmulticommand(self, manpageid):
FILE: explainshell/util.py
function consecutive (line 4) | def consecutive(l, fn):
function groupcontinuous (line 37) | def groupcontinuous(l, key=None):
function toposorted (line 49) | def toposorted(graph, parents):
function pairwise (line 73) | def pairwise(iterable):
class peekable (line 78) | class peekable(object):
method __init__ (line 96) | def __init__(self, it):
method __iter__ (line 101) | def __iter__(self):
method next (line 103) | def next(self):
method hasnext (line 111) | def hasnext(self):
method peek (line 117) | def peek(self):
method index (line 125) | def index(self):
function namesection (line 129) | def namesection(path):
class propertycache (line 134) | class propertycache(object):
method __init__ (line 135) | def __init__(self, func):
method __get__ (line 139) | def __get__(self, obj, type=None):
method cachevalue (line 144) | def cachevalue(self, obj, value):
FILE: explainshell/web/debugviews.py
function debug (line 11) | def debug():
function _convertvalue (line 27) | def _convertvalue(value):
function tag (line 37) | def tag(source):
FILE: explainshell/web/helpers.py
function convertparagraphs (line 3) | def convertparagraphs(manpage):
function suggestions (line 8) | def suggestions(matches, command):
FILE: explainshell/web/static/js/bootstrap.js
function removeElement (line 115) | function removeElement() {
function clearMenus (line 739) | function clearMenus() {
function getParent (line 745) | function getParent($this) {
function removeWithAnimation (line 1278) | function removeWithAnimation() {
function ScrollSpy (line 1549) | function ScrollSpy(element, options) {
function next (line 1760) | function next() {
FILE: explainshell/web/static/js/d3.v3.js
function d3_number (line 71) | function d3_number(x) {
function d3_zipLength (line 141) | function d3_zipLength(d) {
function d3_range_integerScale (line 182) | function d3_range_integerScale(x) {
function d3_class (line 187) | function d3_class(ctor, properties) {
function d3_Map (line 204) | function d3_Map() {}
function map (line 254) | function map(mapType, array, depth) {
function entries (line 278) | function entries(map, depth) {
function d3_Set (line 320) | function d3_Set() {}
function d3_rebind (line 354) | function d3_rebind(target, source, method) {
function d3_dispatch (line 365) | function d3_dispatch() {}
function d3_dispatch_event (line 380) | function d3_dispatch_event(dispatch) {
function d3_eventCancel (line 403) | function d3_eventCancel() {
function d3_eventSource (line 407) | function d3_eventSource() {
function d3_eventDispatch (line 412) | function d3_eventDispatch(target) {
function d3_mousePoint (line 433) | function d3_mousePoint(container, e) {
function d3_arrayCopy (line 457) | function d3_arrayCopy(pseudoarray) {
function d3_arraySlice (line 462) | function d3_arraySlice(pseudoarray) {
function drag (line 485) | function drag() {
function mousedown (line 488) | function mousedown() {
function d3_selection (line 543) | function d3_selection(groups) {
function d3_selection_selector (line 585) | function d3_selection_selector(selector) {
function d3_selection_selectorAll (line 603) | function d3_selection_selectorAll(selector) {
function d3_selection_attr (line 641) | function d3_selection_attr(name, value) {
function d3_collapse (line 665) | function d3_collapse(s) {
function d3_selection_classedRe (line 689) | function d3_selection_classedRe(name) {
function d3_selection_classed (line 692) | function d3_selection_classed(name, value) {
function d3_selection_classedName (line 705) | function d3_selection_classedName(name) {
function d3_selection_style (line 731) | function d3_selection_style(name, value, priority) {
function d3_selection_property (line 752) | function d3_selection_property(name, value) {
function append (line 787) | function append() {
function appendNS (line 790) | function appendNS() {
function insert (line 798) | function insert(d, i) {
function insertNS (line 801) | function insertNS(d, i) {
function bind (line 823) | function bind(group, groupData) {
function d3_selection_dataNode (line 894) | function d3_selection_dataNode(data) {
function d3_selection_filter (line 916) | function d3_selection_filter(selector) {
function d3_selection_sortComparator (line 937) | function d3_selection_sortComparator(comparator) {
function d3_noop (line 943) | function d3_noop() {}
function d3_selection_on (line 957) | function d3_selection_on(type, listener, capture) {
function d3_selection_onListener (line 994) | function d3_selection_onListener(listener, argumentz) {
function d3_selection_onFilter (line 1006) | function d3_selection_onFilter(listener, argumentz) {
function d3_selection_each (line 1020) | function d3_selection_each(groups, callback) {
function d3_selection_enter (line 1045) | function d3_selection_enter(selection) {
function zoom (line 1095) | function zoom() {
function location (line 1131) | function location(p) {
function point (line 1134) | function point(l) {
function scaleTo (line 1137) | function scaleTo(s) {
function translateTo (line 1140) | function translateTo(p, l) {
function rescale (line 1145) | function rescale() {
function dispatch (line 1153) | function dispatch(event) {
function mousedown (line 1162) | function mousedown() {
function mousewheel (line 1181) | function mousewheel() {
function mousemove (line 1187) | function mousemove() {
function dblclick (line 1190) | function dblclick() {
function touchstart (line 1196) | function touchstart() {
function touchmove (line 1214) | function touchmove() {
function d3_Color (line 1236) | function d3_Color() {}
function d3_hsl (line 1243) | function d3_hsl(h, s, l) {
function d3_Hsl (line 1246) | function d3_Hsl(h, s, l) {
function d3_hsl_rgb (line 1263) | function d3_hsl_rgb(h, s, l) {
function d3_sgn (line 1284) | function d3_sgn(x) {
function d3_acos (line 1287) | function d3_acos(x) {
function d3_asin (line 1290) | function d3_asin(x) {
function d3_sinh (line 1293) | function d3_sinh(x) {
function d3_cosh (line 1296) | function d3_cosh(x) {
function d3_haversin (line 1299) | function d3_haversin(x) {
function d3_hcl (line 1305) | function d3_hcl(h, c, l) {
function d3_Hcl (line 1308) | function d3_Hcl(h, c, l) {
function d3_hcl_lab (line 1323) | function d3_hcl_lab(h, c, l) {
function d3_lab (line 1329) | function d3_lab(l, a, b) {
function d3_Lab (line 1332) | function d3_Lab(l, a, b) {
function d3_lab_rgb (line 1349) | function d3_lab_rgb(l, a, b) {
function d3_lab_hcl (line 1356) | function d3_lab_hcl(l, a, b) {
function d3_lab_xyz (line 1359) | function d3_lab_xyz(x) {
function d3_xyz_lab (line 1362) | function d3_xyz_lab(x) {
function d3_xyz_rgb (line 1365) | function d3_xyz_rgb(r) {
function d3_rgb (line 1371) | function d3_rgb(r, g, b) {
function d3_Rgb (line 1374) | function d3_Rgb(r, g, b) {
function d3_rgb_hex (line 1399) | function d3_rgb_hex(v) {
function d3_rgb_parse (line 1402) | function d3_rgb_parse(format, rgb, hsl) {
function d3_rgb_hsl (line 1439) | function d3_rgb_hsl(r, g, b) {
function d3_rgb_lab (line 1450) | function d3_rgb_lab(r, g, b) {
function d3_rgb_xyz (line 1457) | function d3_rgb_xyz(r) {
function d3_rgb_parseNumber (line 1460) | function d3_rgb_parseNumber(c) {
function d3_functor (line 1616) | function d3_functor(v) {
function d3_identity (line 1622) | function d3_identity(d) {
function respond (line 1630) | function respond() {
function d3_xhr_fixCallback (line 1684) | function d3_xhr_fixCallback(callback) {
function d3_dsv (line 1689) | function d3_dsv(delimiter, mimeType) {
function d3_timer_step (line 1815) | function d3_timer_step() {
function d3_timer_flush (line 1843) | function d3_timer_flush() {
function d3_formatPrefix (line 1871) | function d3_formatPrefix(d, i) {
function d3_format_precision (line 1984) | function d3_format_precision(x, p) {
function d3_format_typeDefault (line 1987) | function d3_format_typeDefault(x) {
function d3_geo_streamGeometry (line 2011) | function d3_geo_streamGeometry(geometry, listener) {
function d3_geo_streamLine (line 2056) | function d3_geo_streamLine(coordinates, listener, closed) {
function d3_geo_streamPolygon (line 2062) | function d3_geo_streamPolygon(coordinates, listener) {
function d3_geo_areaRingStart (line 2091) | function d3_geo_areaRingStart() {
function d3_geo_bounds (line 2111) | function d3_geo_bounds(projectStream) {
function d3_geo_centroidPoint (line 2169) | function d3_geo_centroidPoint(λ, φ) {
function d3_geo_centroidRingStart (line 2178) | function d3_geo_centroidRingStart() {
function d3_geo_centroidLineStart (line 2193) | function d3_geo_centroidLineStart() {
function d3_geo_centroidLineEnd (line 2217) | function d3_geo_centroidLineEnd() {
function d3_geo_cartesian (line 2220) | function d3_geo_cartesian(spherical) {
function d3_geo_cartesianDot (line 2224) | function d3_geo_cartesianDot(a, b) {
function d3_geo_cartesianCross (line 2227) | function d3_geo_cartesianCross(a, b) {
function d3_geo_cartesianAdd (line 2230) | function d3_geo_cartesianAdd(a, b) {
function d3_geo_cartesianScale (line 2235) | function d3_geo_cartesianScale(vector, k) {
function d3_geo_cartesianNormalize (line 2238) | function d3_geo_cartesianNormalize(d) {
function d3_true (line 2244) | function d3_true() {
function d3_geo_spherical (line 2247) | function d3_geo_spherical(cartesian) {
function d3_geo_sphericalEqual (line 2250) | function d3_geo_sphericalEqual(a, b) {
function d3_geo_clipPolygon (line 2253) | function d3_geo_clipPolygon(segments, compare, inside, interpolate, list...
function d3_geo_clipPolygonLinkCircular (line 2339) | function d3_geo_clipPolygonLinkCircular(array) {
function d3_geo_clip (line 2350) | function d3_geo_clip(pointVisible, clipLine, interpolate) {
function d3_geo_clipSegmentLength1 (line 2439) | function d3_geo_clipSegmentLength1(segment) {
function d3_geo_clipBufferListener (line 2442) | function d3_geo_clipBufferListener() {
function d3_geo_clipAreaRing (line 2463) | function d3_geo_clipAreaRing(ring, invisible) {
function d3_geo_clipSort (line 2479) | function d3_geo_clipSort(a, b) {
function d3_geo_clipAntimeridianLine (line 2483) | function d3_geo_clipAntimeridianLine(listener) {
function d3_geo_clipAntimeridianIntersect (line 2522) | function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) {
function d3_geo_clipAntimeridianInterpolate (line 2526) | function d3_geo_clipAntimeridianInterpolate(from, to, direction, listene...
function d3_geo_clipCircle (line 2549) | function d3_geo_clipCircle(radius) {
function d3_geo_clipView (line 2646) | function d3_geo_clipView(x0, y0, x1, y1) {
function d3_geo_clipViewT (line 2786) | function d3_geo_clipViewT(num, denominator, t) {
function d3_geo_compose (line 2798) | function d3_geo_compose(a, b) {
function d3_geo_resample (line 2807) | function d3_geo_resample(project) {
function d3_geo_projection (line 2877) | function d3_geo_projection(project) {
function d3_geo_projectionMutator (line 2882) | function d3_geo_projectionMutator(projectAt) {
function d3_geo_projectionRadiansRotate (line 2947) | function d3_geo_projectionRadiansRotate(rotate, stream) {
function d3_geo_equirectangular (line 2970) | function d3_geo_equirectangular(λ, φ) {
function forward (line 2978) | function forward(coordinates) {
function d3_geo_rotation (line 2988) | function d3_geo_rotation(δλ, δφ, δγ) {
function d3_geo_forwardRotationλ (line 2991) | function d3_geo_forwardRotationλ(δλ) {
function d3_geo_rotationλ (line 2996) | function d3_geo_rotationλ(δλ) {
function d3_geo_rotationφγ (line 3001) | function d3_geo_rotationφγ(δφ, δγ) {
function circle (line 3015) | function circle() {
function d3_geo_circleInterpolate (line 3045) | function d3_geo_circleInterpolate(radius, precision) {
function d3_geo_circleAngle (line 3062) | function d3_geo_circleAngle(cr, point) {
function graticule (line 3075) | function graticule() {
function lines (line 3081) | function lines() {
function d3_geo_graticuleX (line 3147) | function d3_geo_graticuleX(y0, y1, dy) {
function d3_geo_graticuleY (line 3155) | function d3_geo_graticuleY(x0, x1, dx) {
function d3_source (line 3163) | function d3_source(d) {
function d3_target (line 3166) | function d3_target(d) {
function greatArc (line 3171) | function greatArc() {
function d3_geo_interpolate (line 3198) | function d3_geo_interpolate(x0, y0, x1, y1) {
function d3_geo_lengthLineStart (line 3223) | function d3_geo_lengthLineStart() {
function d3_geo_conic (line 3238) | function d3_geo_conic(projectAt) {
function d3_geo_conicEqualArea (line 3246) | function d3_geo_conicEqualArea(φ0, φ1) {
function albersUsa (line 3267) | function albersUsa(coordinates) {
function projection (line 3270) | function projection(point) {
function d3_geo_albersUsaInvert (line 3299) | function d3_geo_albersUsaInvert(projection, extent) {
function d3_geo_pathAreaRingStart (line 3324) | function d3_geo_pathAreaRingStart() {
function d3_geo_pathBuffer (line 3338) | function d3_geo_pathBuffer() {
function d3_geo_pathCentroidPoint (line 3396) | function d3_geo_pathCentroidPoint(x, y) {
function d3_geo_pathCentroidLineStart (line 3402) | function d3_geo_pathCentroidLineStart() {
function d3_geo_pathCentroidLineEnd (line 3422) | function d3_geo_pathCentroidLineEnd() {
function d3_geo_pathCentroidRingStart (line 3425) | function d3_geo_pathCentroidRingStart() {
function d3_geo_pathContext (line 3446) | function d3_geo_pathContext(context) {
function path (line 3488) | function path(object) {
function d3_geo_pathCircle (line 3522) | function d3_geo_pathCircle(radius) {
function d3_geo_pathProjectStream (line 3525) | function d3_geo_pathProjectStream(project) {
function d3_geo_azimuthal (line 3556) | function d3_geo_azimuthal(scale, angle) {
function d3_geo_conicConformal (line 3582) | function d3_geo_conicConformal(φ0, φ1) {
function d3_geo_conicEquidistant (line 3600) | function d3_geo_conicEquidistant(φ0, φ1) {
function d3_geo_mercator (line 3622) | function d3_geo_mercator(λ, φ) {
function d3_geo_mercatorProjection (line 3628) | function d3_geo_mercatorProjection(project) {
function d3_geo_transverseMercator (line 3669) | function d3_geo_transverseMercator(λ, φ) {
function d3_svg_line (line 3681) | function d3_svg_line(projection) {
function d3_svg_lineX (line 3729) | function d3_svg_lineX(d) {
function d3_svg_lineY (line 3732) | function d3_svg_lineY(d) {
function d3_svg_lineLinear (line 3753) | function d3_svg_lineLinear(points) {
function d3_svg_lineLinearClosed (line 3756) | function d3_svg_lineLinearClosed(points) {
function d3_svg_lineStepBefore (line 3759) | function d3_svg_lineStepBefore(points) {
function d3_svg_lineStepAfter (line 3764) | function d3_svg_lineStepAfter(points) {
function d3_svg_lineCardinalOpen (line 3769) | function d3_svg_lineCardinalOpen(points, tension) {
function d3_svg_lineCardinalClosed (line 3772) | function d3_svg_lineCardinalClosed(points, tension) {
function d3_svg_lineCardinal (line 3776) | function d3_svg_lineCardinal(points, tension) {
function d3_svg_lineHermite (line 3779) | function d3_svg_lineHermite(points, tangents) {
function d3_svg_lineCardinalTangents (line 3806) | function d3_svg_lineCardinalTangents(points, tension) {
function d3_svg_lineBasis (line 3816) | function d3_svg_lineBasis(points) {
function d3_svg_lineBasisOpen (line 3838) | function d3_svg_lineBasisOpen(points) {
function d3_svg_lineBasisClosed (line 3858) | function d3_svg_lineBasisClosed(points) {
function d3_svg_lineBundle (line 3877) | function d3_svg_lineBundle(points, tension) {
function d3_svg_lineDot4 (line 3890) | function d3_svg_lineDot4(a, b) {
function d3_svg_lineBasisBezier (line 3894) | function d3_svg_lineBasisBezier(path, x, y) {
function d3_svg_lineSlope (line 3897) | function d3_svg_lineSlope(p0, p1) {
function d3_svg_lineFiniteDifferences (line 3900) | function d3_svg_lineFiniteDifferences(points) {
function d3_svg_lineMonotoneTangents (line 3908) | function d3_svg_lineMonotoneTangents(points) {
function d3_svg_lineMonotone (line 3932) | function d3_svg_lineMonotone(points) {
function hull (line 3938) | function hull(data) {
function d3_geom_hullCCW (line 4017) | function d3_geom_hullCCW(i1, i2, i3, v) {
function d3_geom_polygonInside (line 4076) | function d3_geom_polygonInside(p, a, b) {
function d3_geom_polygonIntersect (line 4079) | function d3_geom_polygonIntersect(c, d, a, b) {
function voronoi (line 4107) | function voronoi(data) {
function d3_geom_voronoiTessellate (line 4230) | function d3_geom_voronoiTessellate(points, callback) {
function quadtree (line 4512) | function quadtree(data) {
function d3_geom_quadtreeCompatX (line 4603) | function d3_geom_quadtreeCompatX(d) {
function d3_geom_quadtreeCompatY (line 4606) | function d3_geom_quadtreeCompatY(d) {
function d3_geom_quadtreeNode (line 4609) | function d3_geom_quadtreeNode() {
function d3_geom_quadtreeVisit (line 4618) | function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) {
function d3_interpolateRgb (line 4628) | function d3_interpolateRgb(a, b) {
function d3_transform (line 4644) | function d3_transform(m) {
function d3_transformDot (line 4660) | function d3_transformDot(a, b) {
function d3_transformNormalize (line 4663) | function d3_transformNormalize(a) {
function d3_transformCombine (line 4671) | function d3_transformCombine(a, b, k) {
function d3_interpolateNumber (line 4685) | function d3_interpolateNumber(a, b) {
function d3_interpolateTransform (line 4692) | function d3_interpolateTransform(a, b) {
function d3_interpolateObject (line 4745) | function d3_interpolateObject(a, b) {
function d3_interpolateString (line 4765) | function d3_interpolateString(a, b) {
function d3_interpolate (line 4829) | function d3_interpolate(a, b) {
function d3_interpolateByName (line 4834) | function d3_interpolateByName(name) {
function d3_interpolateArray (line 4847) | function d3_interpolateArray(a, b) {
function d3_ease_clamp (line 4898) | function d3_ease_clamp(f) {
function d3_ease_reverse (line 4903) | function d3_ease_reverse(f) {
function d3_ease_reflect (line 4908) | function d3_ease_reflect(f) {
function d3_ease_quad (line 4913) | function d3_ease_quad(t) {
function d3_ease_cubic (line 4916) | function d3_ease_cubic(t) {
function d3_ease_cubicInOut (line 4919) | function d3_ease_cubicInOut(t) {
function d3_ease_poly (line 4925) | function d3_ease_poly(e) {
function d3_ease_sin (line 4930) | function d3_ease_sin(t) {
function d3_ease_exp (line 4933) | function d3_ease_exp(t) {
function d3_ease_circle (line 4936) | function d3_ease_circle(t) {
function d3_ease_elastic (line 4939) | function d3_ease_elastic(a, p) {
function d3_ease_back (line 4947) | function d3_ease_back(s) {
function d3_ease_bounce (line 4953) | function d3_ease_bounce(t) {
function d3_interpolateHcl (line 4957) | function d3_interpolateHcl(a, b) {
function d3_interpolateHsl (line 4967) | function d3_interpolateHsl(a, b) {
function d3_interpolateLab (line 4977) | function d3_interpolateLab(a, b) {
function d3_interpolateRound (line 4986) | function d3_interpolateRound(a, b) {
function d3_uninterpolateNumber (line 4992) | function d3_uninterpolateNumber(a, b) {
function d3_uninterpolateClamp (line 4998) | function d3_uninterpolateClamp(a, b) {
function d3_layout_bundlePath (line 5012) | function d3_layout_bundlePath(link) {
function d3_layout_bundleAncestors (line 5025) | function d3_layout_bundleAncestors(node) {
function d3_layout_bundleLeastCommonAncestor (line 5035) | function d3_layout_bundleLeastCommonAncestor(a, b) {
function relayout (line 5047) | function relayout() {
function resort (line 5113) | function resort() {
function repulse (line 5160) | function repulse(node) {
function position (line 5325) | function position(dimension, size) {
function neighbor (line 5330) | function neighbor() {
function dragmove (line 5357) | function dragmove(d) {
function d3_layout_forceDragstart (line 5363) | function d3_layout_forceDragstart(d) {
function d3_layout_forceDragend (line 5366) | function d3_layout_forceDragend(d) {
function d3_layout_forceMouseover (line 5369) | function d3_layout_forceMouseover(d) {
function d3_layout_forceMouseout (line 5373) | function d3_layout_forceMouseout(d) {
function d3_layout_forceAccumulate (line 5376) | function d3_layout_forceAccumulate(quad, alpha, charges) {
function recurse (line 5406) | function recurse(node, depth, nodes) {
function revalue (line 5425) | function revalue(node, depth) {
function hierarchy (line 5436) | function hierarchy(d) {
function d3_layout_hierarchyRebind (line 5462) | function d3_layout_hierarchyRebind(object, hierarchy) {
function d3_layout_hierarchyChildren (line 5468) | function d3_layout_hierarchyChildren(d) {
function d3_layout_hierarchyValue (line 5471) | function d3_layout_hierarchyValue(d) {
function d3_layout_hierarchySort (line 5474) | function d3_layout_hierarchySort(a, b) {
function d3_layout_hierarchyLinks (line 5477) | function d3_layout_hierarchyLinks(nodes) {
function position (line 5489) | function position(node, x, dx, dy) {
function depth (line 5504) | function depth(node) {
function partition (line 5512) | function partition(d, i) {
function pie (line 5526) | function pie(data) {
function stack (line 5575) | function stack(data, index) {
function d3_layout_stackX (line 5629) | function d3_layout_stackX(d) {
function d3_layout_stackY (line 5632) | function d3_layout_stackY(d) {
function d3_layout_stackOut (line 5635) | function d3_layout_stackOut(d, y0, y) {
function d3_layout_stackOrderDefault (line 5702) | function d3_layout_stackOrderDefault(data) {
function d3_layout_stackOffsetZero (line 5705) | function d3_layout_stackOffsetZero(data) {
function d3_layout_stackMaxIndex (line 5710) | function d3_layout_stackMaxIndex(array) {
function d3_layout_stackReduceSum (line 5720) | function d3_layout_stackReduceSum(d) {
function d3_layout_stackSum (line 5723) | function d3_layout_stackSum(p, d) {
function histogram (line 5728) | function histogram(data, i) {
function d3_layout_histogramBinSturges (line 5772) | function d3_layout_histogramBinSturges(range, values) {
function d3_layout_histogramBinFixed (line 5775) | function d3_layout_histogramBinFixed(range, n) {
function d3_layout_histogramRange (line 5780) | function d3_layout_histogramRange(values) {
function tree (line 5785) | function tree(d, i) {
function d3_layout_treeSeparation (line 5884) | function d3_layout_treeSeparation(a, b) {
function d3_layout_treeLeft (line 5887) | function d3_layout_treeLeft(node) {
function d3_layout_treeRight (line 5891) | function d3_layout_treeRight(node) {
function d3_layout_treeSearch (line 5895) | function d3_layout_treeSearch(node, compare) {
function d3_layout_treeRightmost (line 5907) | function d3_layout_treeRightmost(a, b) {
function d3_layout_treeLeftmost (line 5910) | function d3_layout_treeLeftmost(a, b) {
function d3_layout_treeDeepest (line 5913) | function d3_layout_treeDeepest(a, b) {
function d3_layout_treeVisitAfter (line 5916) | function d3_layout_treeVisitAfter(node, callback) {
function d3_layout_treeShift (line 5931) | function d3_layout_treeShift(node) {
function d3_layout_treeMove (line 5940) | function d3_layout_treeMove(ancestor, node, shift) {
function d3_layout_treeAncestor (line 5950) | function d3_layout_treeAncestor(vim, node, ancestor) {
function pack (line 5955) | function pack(d, i) {
function d3_layout_packSort (line 5990) | function d3_layout_packSort(a, b) {
function d3_layout_packInsert (line 5993) | function d3_layout_packInsert(a, b) {
function d3_layout_packSplice (line 6000) | function d3_layout_packSplice(a, b) {
function d3_layout_packIntersects (line 6004) | function d3_layout_packIntersects(a, b) {
function d3_layout_packSiblings (line 6008) | function d3_layout_packSiblings(node) {
function d3_layout_packLink (line 6072) | function d3_layout_packLink(node) {
function d3_layout_packUnlink (line 6075) | function d3_layout_packUnlink(node) {
function d3_layout_packTransform (line 6079) | function d3_layout_packTransform(node, x, y, k) {
function d3_layout_packPlace (line 6089) | function d3_layout_packPlace(a, b, c) {
function cluster (line 6105) | function cluster(d, i) {
function d3_layout_clusterY (line 6137) | function d3_layout_clusterY(children) {
function d3_layout_clusterX (line 6142) | function d3_layout_clusterX(children) {
function d3_layout_clusterLeft (line 6147) | function d3_layout_clusterLeft(node) {
function d3_layout_clusterRight (line 6151) | function d3_layout_clusterRight(node) {
function scale (line 6157) | function scale(children, k) {
function squarify (line 6164) | function squarify(node) {
function stickify (line 6191) | function stickify(node) {
function worst (line 6208) | function worst(row, u) {
function position (line 6219) | function position(row, u, rect, flush) {
function treemap (line 6249) | function treemap(d) {
function padFunction (line 6268) | function padFunction(node) {
function padConstant (line 6272) | function padConstant(node) {
function d3_layout_treemapPadNull (line 6303) | function d3_layout_treemapPadNull(node) {
function d3_layout_treemapPad (line 6311) | function d3_layout_treemapPad(node, padding) {
function d3_scaleExtent (line 6357) | function d3_scaleExtent(domain) {
function d3_scaleRange (line 6361) | function d3_scaleRange(scale) {
function d3_scale_bilinear (line 6364) | function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {
function d3_scale_nice (line 6370) | function d3_scale_nice(domain, nice) {
function d3_scale_polylinear (line 6382) | function d3_scale_polylinear(domain, range, uninterpolate, interpolate) {
function d3_scale_linear (line 6400) | function d3_scale_linear(domain, range, interpolate, clamp) {
function d3_scale_linearRebind (line 6452) | function d3_scale_linearRebind(scale, linear) {
function d3_scale_linearNice (line 6455) | function d3_scale_linearNice(dx) {
function d3_scale_linearTickRange (line 6466) | function d3_scale_linearTickRange(domain, m) {
function d3_scale_linearTicks (line 6474) | function d3_scale_linearTicks(domain, m) {
function d3_scale_linearTickFormat (line 6477) | function d3_scale_linearTickFormat(domain, m, format) {
function d3_scale_log (line 6486) | function d3_scale_log(linear, base, log, pow) {
function d3_scale_logp (line 6541) | function d3_scale_logp(x) {
function d3_scale_powp (line 6544) | function d3_scale_powp(x) {
function d3_scale_logn (line 6547) | function d3_scale_logn(x) {
function d3_scale_pown (line 6550) | function d3_scale_pown(x) {
function d3_scale_logNice (line 6553) | function d3_scale_logNice(base) {
function d3_scale_pow (line 6570) | function d3_scale_pow(linear, exponent) {
function d3_scale_powPow (line 6604) | function d3_scale_powPow(e) {
function d3_scale_ordinal (line 6618) | function d3_scale_ordinal(domain, ranger) {
function d3_scale_quantile (line 6713) | function d3_scale_quantile(domain, range) {
function d3_scale_quantize (line 6748) | function d3_scale_quantize(x0, x1, range) {
function d3_scale_threshold (line 6777) | function d3_scale_threshold(domain, range) {
function d3_scale_identity (line 6799) | function d3_scale_identity(domain) {
function arc (line 6822) | function arc() {
function d3_svg_arcInnerRadius (line 6854) | function d3_svg_arcInnerRadius(d) {
function d3_svg_arcOuterRadius (line 6857) | function d3_svg_arcOuterRadius(d) {
function d3_svg_arcStartAngle (line 6860) | function d3_svg_arcStartAngle(d) {
function d3_svg_arcEndAngle (line 6863) | function d3_svg_arcEndAngle(d) {
function d3_svg_lineRadial (line 6872) | function d3_svg_lineRadial(points) {
function d3_svg_area (line 6883) | function d3_svg_area(projection) {
function chord (line 6973) | function chord(d, i) {
function subgroup (line 6977) | function subgroup(self, f, d, i) {
function equals (line 6987) | function equals(a, b) {
function arc (line 6990) | function arc(r, p, a) {
function curve (line 6993) | function curve(r0, p0, r1, p1) {
function d3_svg_chordRadius (line 7023) | function d3_svg_chordRadius(d) {
function diagonal (line 7028) | function diagonal(d, i) {
function d3_svg_diagonalProjection (line 7056) | function d3_svg_diagonalProjection(d) {
function d3_svg_diagonalRadialProjection (line 7066) | function d3_svg_diagonalRadialProjection(projection) {
function symbol (line 7074) | function symbol(d, i) {
function d3_svg_symbolSize (line 7089) | function d3_svg_symbolSize() {
function d3_svg_symbolType (line 7092) | function d3_svg_symbolType() {
function d3_svg_symbolCircle (line 7095) | function d3_svg_symbolCircle(size) {
function d3_transition (line 7124) | function d3_transition(groups, id) {
function d3_transition_tween (line 7198) | function d3_transition_tween(groups, name, value, tween) {
function attrNull (line 7212) | function attrNull() {
function attrNullNS (line 7215) | function attrNullNS() {
function attrString (line 7219) | function attrString() {
function attrStringNS (line 7225) | function attrStringNS() {
function attrTween (line 7236) | function attrTween(d, i) {
function attrTweenNS (line 7242) | function attrTweenNS(d, i) {
function styleNull (line 7261) | function styleNull() {
function styleString (line 7265) | function styleString() {
function d3_transition_text (line 7286) | function d3_transition_text(b) {
function d3_transitionNode (line 7355) | function d3_transitionNode(node, i, id, inherit) {
function axis (line 7408) | function axis(g) {
function d3_svg_axisX (line 7545) | function d3_svg_axisX(selection, x) {
function d3_svg_axisY (line 7550) | function d3_svg_axisY(selection, y) {
function d3_svg_axisSubdivide (line 7555) | function d3_svg_axisSubdivide(scale, ticks, m) {
function brush (line 7574) | function brush(g) {
function redraw (line 7604) | function redraw(g) {
function redrawX (line 7609) | function redrawX(g) {
function redrawY (line 7613) | function redrawY(g) {
function brushstart (line 7617) | function brushstart() {
function d3_time_utc (line 7800) | function d3_time_utc() {
function d3_time_interval (line 7868) | function d3_time_interval(local, step, number) {
function d3_time_interval_utc (line 7916) | function d3_time_interval_utc(method) {
function format (line 7979) | function format(date) {
function d3_time_parse (line 8015) | function d3_time_parse(date, template, string, j) {
function d3_time_formatRe (line 8029) | function d3_time_formatRe(names) {
function d3_time_formatLookup (line 8032) | function d3_time_formatLookup(names) {
function d3_time_formatPad (line 8037) | function d3_time_formatPad(value, fill, width) {
function d3_time_parseWeekdayAbbrev (line 8134) | function d3_time_parseWeekdayAbbrev(date, string, i) {
function d3_time_parseWeekday (line 8139) | function d3_time_parseWeekday(date, string, i) {
function d3_time_parseMonthAbbrev (line 8144) | function d3_time_parseMonthAbbrev(date, string, i) {
function d3_time_parseMonth (line 8149) | function d3_time_parseMonth(date, string, i) {
function d3_time_parseLocaleFull (line 8154) | function d3_time_parseLocaleFull(date, string, i) {
function d3_time_parseLocaleDate (line 8157) | function d3_time_parseLocaleDate(date, string, i) {
function d3_time_parseLocaleTime (line 8160) | function d3_time_parseLocaleTime(date, string, i) {
function d3_time_parseFullYear (line 8163) | function d3_time_parseFullYear(date, string, i) {
function d3_time_parseYear (line 8168) | function d3_time_parseYear(date, string, i) {
function d3_time_expandYear (line 8173) | function d3_time_expandYear(d) {
function d3_time_parseMonthNumber (line 8176) | function d3_time_parseMonthNumber(date, string, i) {
function d3_time_parseDay (line 8181) | function d3_time_parseDay(date, string, i) {
function d3_time_parseHour24 (line 8186) | function d3_time_parseHour24(date, string, i) {
function d3_time_parseMinutes (line 8191) | function d3_time_parseMinutes(date, string, i) {
function d3_time_parseSeconds (line 8196) | function d3_time_parseSeconds(date, string, i) {
function d3_time_parseMilliseconds (line 8201) | function d3_time_parseMilliseconds(date, string, i) {
function d3_time_parseAmPm (line 8207) | function d3_time_parseAmPm(date, string, i) {
function d3_time_zone (line 8215) | function d3_time_zone(d) {
function format (line 8221) | function format(date) {
function d3_time_formatIsoNative (line 8245) | function d3_time_formatIsoNative(date) {
function d3_time_scale (line 8292) | function d3_time_scale(linear, methods, format) {
function d3_time_scaleExtent (line 8330) | function d3_time_scaleExtent(domain) {
function d3_time_scaleDate (line 8334) | function d3_time_scaleDate(t) {
function d3_time_scaleFormat (line 8337) | function d3_time_scaleFormat(formats) {
function d3_time_scaleSetYear (line 8344) | function d3_time_scaleSetYear(y) {
function d3_time_scaleGetYear (line 8349) | function d3_time_scaleGetYear(d) {
function d3_time_scaleUTCSetYear (line 8396) | function d3_time_scaleUTCSetYear(y) {
function d3_time_scaleUTCGetYear (line 8401) | function d3_time_scaleUTCGetYear(d) {
function d3_text (line 8414) | function d3_text(request) {
function d3_json (line 8420) | function d3_json(request) {
function d3_html (line 8426) | function d3_html(request) {
function d3_xml (line 8434) | function d3_xml(request) {
FILE: explainshell/web/static/js/es.js
function specialparam (line 74) | function specialparam(text) {
function eslinkgroup (line 190) | function eslinkgroup(clazz, options, mid) {
function eslink (line 199) | function eslink(clazz, option, mid, color) {
function espath (line 260) | function espath() {
function swapNodes (line 270) | function swapNodes(a, b) {
function reorder (line 278) | function reorder(lefteslinks) {
function helpselector (line 298) | function helpselector(commandselector) {
function optionsselector (line 305) | function optionsselector(pres, spans) {
function initialize (line 325) | function initialize() {
function handlesynopsis (line 436) | function handlesynopsis() {
function assigncolors (line 446) | function assigncolors() {
function commandunknowns (line 460) | function commandunknowns() {
function affixon (line 482) | function affixon() {
function drawgrouplines (line 490) | function drawgrouplines(commandselector, options) {
function clear (line 886) | function clear() {
function commandlinetourl (line 895) | function commandlinetourl(s) {
function adjustcommandfontsize (line 907) | function adjustcommandfontsize() {
function navigation (line 928) | function navigation() {
function inview (line 1043) | function inview(viewtop, viewbottom, $el) {
function drawvisible (line 1094) | function drawvisible() {
function draw (line 1118) | function draw() {
function setTheme (line 1126) | function setTheme(theme) {
FILE: explainshell/web/static/js/jquery.js
function isArraylike (line 951) | function isArraylike( obj ) {
function createOptions (line 974) | function createOptions( options ) {
function internalData (line 1551) | function internalData( elem, name, data, pvt /* Internal Use Only */ ){
function internalRemoveData (line 1645) | function internalRemoveData( elem, name, pvt ) {
function dataAttr (line 1841) | function dataAttr( elem, key, data ) {
function isEmptyDataObject (line 1873) | function isEmptyDataObject( obj ) {
function returnTrue (line 2702) | function returnTrue() {
function returnFalse (line 2706) | function returnFalse() {
function isNative (line 3841) | function isNative( fn ) {
function createCache (line 3851) | function createCache() {
function markFunction (line 3869) | function markFunction( fn ) {
function assert (line 3878) | function assert( fn ) {
function Sizzle (line 3891) | function Sizzle( selector, context, results, seed ) {
function siblingCheck (line 4449) | function siblingCheck( a, b ) {
function createInputPseudo (line 4471) | function createInputPseudo( type ) {
function createButtonPseudo (line 4479) | function createButtonPseudo( type ) {
function createPositionalPseudo (line 4487) | function createPositionalPseudo( fn ) {
function tokenize (line 5014) | function tokenize( selector, parseOnly ) {
function toSelector (line 5081) | function toSelector( tokens ) {
function addCombinator (line 5091) | function addCombinator( matcher, combinator, base ) {
function elementMatcher (line 5141) | function elementMatcher( matchers ) {
function condense (line 5155) | function condense( unmatched, map, filter, context, xml ) {
function setMatcher (line 5176) | function setMatcher( preFilter, selector, matcher, postFilter, postFinde...
function matcherFromTokens (line 5269) | function matcherFromTokens( tokens ) {
function matcherFromGroupMatchers (line 5321) | function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
function multipleContexts (line 5449) | function multipleContexts( selector, contexts, results ) {
function select (line 5458) | function select( selector, context, results, seed ) {
function setFilters (line 5527) | function setFilters() {}
function sibling (line 5680) | function sibling( cur, dir ) {
function winnow (line 5788) | function winnow( elements, qualifier, keep ) {
function createSafeFragment (line 5821) | function createSafeFragment( document ) {
function findOrAppend (line 6202) | function findOrAppend( elem, tag ) {
function disableScript (line 6207) | function disableScript( elem ) {
function restoreScript (line 6212) | function restoreScript( elem ) {
function setGlobalEval (line 6223) | function setGlobalEval( elems, refElements ) {
function cloneCopyEvent (line 6231) | function cloneCopyEvent( src, dest ) {
function fixCloneNodeIssues (line 6259) | function fixCloneNodeIssues( src, dest ) {
function getAll (line 6352) | function getAll( context, tag ) {
function fixDefaultChecked (line 6375) | function fixDefaultChecked( elem ) {
function vendorPropName (line 6640) | function vendorPropName( style, name ) {
function isHidden (line 6662) | function isHidden( elem, el ) {
function showHide (line 6669) | function showHide( elements, show ) {
function setPositiveNumber (line 7016) | function setPositiveNumber( elem, value, subtract ) {
function augmentWidthOrHeight (line 7024) | function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
function getWidthOrHeight (line 7063) | function getWidthOrHeight( elem, name, extra ) {
function css_defaultDisplay (line 7107) | function css_defaultDisplay( nodeName ) {
function actualDisplay (line 7139) | function actualDisplay( name, doc ) {
function buildParams (line 7368) | function buildParams( prefix, obj, traditional, add ) {
function addToPrefiltersOrTransports (line 7466) | function addToPrefiltersOrTransports( structure ) {
function inspectPrefiltersOrTransports (line 7498) | function inspectPrefiltersOrTransports( structure, options, originalOpti...
function ajaxExtend (line 7525) | function ajaxExtend( target, src ) {
function done (line 7991) | function done( status, nativeStatusText, responses, headers ) {
function ajaxHandleResponses (line 8117) | function ajaxHandleResponses( s, jqXHR, responses ) {
function ajaxConvert (line 8178) | function ajaxConvert( s, response ) {
function createStandardXHR (line 8439) | function createStandardXHR() {
function createActiveXHR (line 8445) | function createActiveXHR() {
function createFxNow (line 8684) | function createFxNow() {
function createTweens (line 8691) | function createTweens( animation, props ) {
function Animation (line 8706) | function Animation( elem, properties, options ) {
function propFilter (line 8810) | function propFilter( props, specialEasing ) {
function defaultPrefilter (line 8877) | function defaultPrefilter( elem, props, opts ) {
function Tween (line 9004) | function Tween( elem, options, prop, end, easing ) {
function genFx (line 9230) | function genFx( type, includeWidth ) {
function getWindow (line 9526) | function getWindow( elem ) {
FILE: explainshell/web/views.py
function index (line 14) | def index():
function about (line 18) | def about():
function explain (line 22) | def explain():
function explainold (line 58) | def explainold(section, program):
function explainprogram (line 77) | def explainprogram(program, store):
function _makematch (line 100) | def _makematch(start, end, match, commandclass, helpclass):
function explaincommand (line 104) | def explaincommand(command, store):
function formatmatch (line 193) | def formatmatch(d, m, expansions):
function _substitutionmarkup (line 249) | def _substitutionmarkup(cmd):
function _checkoverlaps (line 260) | def _checkoverlaps(s, matches):
FILE: tests/helpers.py
class mockstore (line 3) | class mockstore(object):
method __init__ (line 4) | def __init__(self):
method findmanpage (line 34) | def findmanpage(self, x, section=None):
FILE: tests/test-fixer.py
class test_fixer (line 6) | class test_fixer(unittest.TestCase):
method setUp (line 7) | def setUp(self):
method tearDown (line 10) | def tearDown(self):
method test_changes (line 13) | def test_changes(self):
method test_paragraphjoiner (line 25) | def test_paragraphjoiner(self):
FILE: tests/test-integration.py
class test_integration (line 6) | class test_integration(unittest.TestCase):
method test (line 7) | def test(self):
FILE: tests/test-manager.py
class test_manager (line 6) | class test_manager(unittest.TestCase):
method setUp (line 7) | def setUp(self):
method _getmanager (line 10) | def _getmanager(self, names, **kwargs):
method test (line 18) | def test(self):
method test_verify (line 36) | def test_verify(self):
method test_aliases (line 61) | def test_aliases(self):
method test_overwrite (line 76) | def test_overwrite(self):
method test_multicommand (line 101) | def test_multicommand(self):
method test_edit (line 108) | def test_edit(self):
method test_samename (line 128) | def test_samename(self):
method test_samename_samesection (line 138) | def test_samename_samesection(self):
FILE: tests/test-manpage.py
class test_manpage (line 5) | class test_manpage(unittest.TestCase):
method test_first_paragraph_no_section (line 6) | def test_first_paragraph_no_section(self):
method test_sections (line 11) | def test_sections(self):
method test_no_synopsis (line 37) | def test_no_synopsis(self):
FILE: tests/test-matcher.py
class test_matcher (line 10) | class test_matcher(unittest.TestCase):
method assertMatchSingle (line 11) | def assertMatchSingle(self, what, expectedmanpage, expectedresults):
method test_unknown_prog (line 18) | def test_unknown_prog(self):
method test_unicode (line 21) | def test_unicode(self):
method test_no_options (line 28) | def test_no_options(self):
method test_known_arg (line 32) | def test_known_arg(self):
method test_arg_in_fuzzy_with_expected_value (line 40) | def test_arg_in_fuzzy_with_expected_value(self):
method test_partialmatch_with_arguments (line 57) | def test_partialmatch_with_arguments(self):
method test_reset_current_option_if_argument_taken (line 65) | def test_reset_current_option_if_argument_taken(self):
method test_arg_with_expected_value (line 92) | def test_arg_with_expected_value(self):
method test_arg_with_expected_value_from_list (line 100) | def test_arg_with_expected_value_from_list(self):
method test_arg_with_expected_value_clash (line 116) | def test_arg_with_expected_value_clash(self):
method test_arg_with_expected_value_no_clash (line 126) | def test_arg_with_expected_value_no_clash(self):
method test_unknown_arg (line 138) | def test_unknown_arg(self):
method test_merge_same_match (line 155) | def test_merge_same_match(self):
method test_known_and_unknown_arg (line 159) | def test_known_and_unknown_arg(self):
method test_long (line 166) | def test_long(self):
method test_arg_no_dash (line 175) | def test_arg_no_dash(self):
method test_multicommand (line 185) | def test_multicommand(self):
method test_multiple_matches (line 201) | def test_multiple_matches(self):
method test_arguments (line 212) | def test_arguments(self):
method test_arg_is_dash (line 223) | def test_arg_is_dash(self):
method test_nested_command (line 233) | def test_nested_command(self):
method test_nested_option (line 248) | def test_nested_option(self):
method test_multiple_nests (line 311) | def test_multiple_nests(self):
method test_nested_command_is_unknown (line 326) | def test_nested_command_is_unknown(self):
method test_unparsed (line 338) | def test_unparsed(self):
method test_known_and_unknown_program (line 344) | def test_known_and_unknown_program(self):
method test_pipe (line 359) | def test_pipe(self):
method test_subshells (line 369) | def test_subshells(self):
method test_redirect_first_word_of_command (line 383) | def test_redirect_first_word_of_command(self):
method test_comsub (line 402) | def test_comsub(self):
method test_comsub_as_arg (line 425) | def test_comsub_as_arg(self):
method test_comsub_as_first_word (line 440) | def test_comsub_as_first_word(self):
method test_procsub (line 452) | def test_procsub(self):
method test_if (line 468) | def test_if(self):
method test_nested_controlflows (line 483) | def test_nested_controlflows(self):
method test_for_expansion (line 500) | def test_for_expansion(self):
method test_assignment_with_expansion (line 516) | def test_assignment_with_expansion(self):
method test_assignment_as_first_word (line 527) | def test_assignment_as_first_word(self):
method test_expansion_limit (line 538) | def test_expansion_limit(self):
method test_functions (line 559) | def test_functions(self):
method test_function_reference (line 588) | def test_function_reference(self):
method test_comment (line 609) | def test_comment(self):
method test_heredoc_at_eof (line 630) | def test_heredoc_at_eof(self):
method test_no_synopsis (line 643) | def test_no_synopsis(self):
FILE: tests/test-options.py
class test_options (line 5) | class test_options(unittest.TestCase):
method test_simple (line 6) | def test_simple(self):
method test_option_arg (line 35) | def test_option_arg(self):
method test_pipe_separator (line 56) | def test_pipe_separator(self):
method test_multiline_options (line 65) | def test_multiline_options(self):
method test_multiline_desc (line 70) | def test_multiline_desc(self):
method test_not_an_option (line 74) | def test_not_an_option(self):
method test_no_hyphen (line 77) | def test_no_hyphen(self):
method test_hyphen_in_arg (line 81) | def test_hyphen_in_arg(self):
method test_extract (line 90) | def test_extract(self):
method test_help (line 109) | def test_help(self):
FILE: tools/shellbuiltins.py
function _add (line 18) | def _add(names, synopsis, options):
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,197K chars).
[
{
"path": ".github/workflows/build-docker-image.yml",
"chars": 2194,
"preview": "name: Build & Publish Docker Image\n\non:\n workflow_dispatch:\n push:\n branches:\n - master\n - main\n\npermissi"
},
{
"path": ".github/workflows/build-test.yml",
"chars": 1427,
"preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
},
{
"path": ".github/workflows/lint-docker.yml",
"chars": 664,
"preview": "name: \"Lint Dockerfile\"\n\non:\n pull_request:\n paths:\n - \"**/Dockerfile.*\"\n - \"**/docker-compose.*\"\n bran"
},
{
"path": ".gitignore",
"chars": 46,
"preview": "*.pyc\n*.swp\n.coverage\n.vagrant\napplication.log"
},
{
"path": "Dockerfile",
"chars": 599,
"preview": "FROM python:2.7\n\nRUN sed -i 's|deb.debian.org/debian|archive.debian.org/debian|g' /etc/apt/sources.list \\\n && sed -i 's"
},
{
"path": "LICENSE",
"chars": 35147,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "Makefile",
"chars": 104,
"preview": "tests:\n\tnosetests --exe --with-doctest tests/ explainshell/\n\nserve:\n\tpython runserver.py\n\n.PHONY: tests\n"
},
{
"path": "README.md",
"chars": 4647,
"preview": "# [explainshell.com](http://www.explainshell.com) - match command-line arguments to their help text\n\nexplainshell is a t"
},
{
"path": "docker-compose.yml",
"chars": 242,
"preview": "services:\n db:\n image: mongo\n web:\n build: .\n command: make serve\n environment:\n - MONGO_URI=mongodb:"
},
{
"path": "explainshell/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "explainshell/algo/__init__.py",
"chars": 59,
"preview": "import explainshell.algo.features\nimport explainshell.util\n"
},
{
"path": "explainshell/algo/classifier.py",
"chars": 3988,
"preview": "import itertools, collections, logging\n\nimport nltk\nimport nltk.metrics\nimport nltk.classify\nimport nltk.classify.maxent"
},
{
"path": "explainshell/algo/features.py",
"chars": 1759,
"preview": "import re\n\ndef extract_first_line(paragraph):\n '''\n >>> extract_first_line('a b cd')\n 'a b'\n >>> extract_fi"
},
{
"path": "explainshell/config.py",
"chars": 1127,
"preview": "import os\n\n_currdir = os.path.dirname(os.path.dirname(__file__))\n\nMANPAGEDIR = os.path.join(_currdir, 'manpages')\nCLASSI"
},
{
"path": "explainshell/errors.py",
"chars": 88,
"preview": "class ProgramDoesNotExist(Exception):\n pass\n\nclass EmptyManpage(Exception):\n pass\n"
},
{
"path": "explainshell/fixer.py",
"chars": 6002,
"preview": "import textwrap, logging\n\nfrom explainshell import util\n\nclass basefixer(object):\n '''The base fixer class which othe"
},
{
"path": "explainshell/helpconstants.py",
"chars": 19314,
"preview": "# -*- coding: utf-8 -*-\n\nimport textwrap\n\nNOSYNOPSIS = 'no synopsis found'\n\nPIPELINES = textwrap.dedent(''' <b>Pipelin"
},
{
"path": "explainshell/manager.py",
"chars": 7255,
"preview": "import sys, os, argparse, logging, glob\n\nfrom explainshell import options, store, fixer, manpage, errors, util, config\nf"
},
{
"path": "explainshell/manpage.py",
"chars": 7602,
"preview": "import os, subprocess, re, logging, collections, urllib\n\nfrom explainshell import config, store, errors\n\ndevnull = open("
},
{
"path": "explainshell/matcher.py",
"chars": 26896,
"preview": "import collections, logging, itertools\n\nimport bashlex.parser\nimport bashlex.ast\n\nfrom explainshell import errors, util,"
},
{
"path": "explainshell/options.py",
"chars": 6226,
"preview": "import re, collections, logging\n\nfrom explainshell import store\n\ntokenstate = collections.namedtuple('tokenstate', 'star"
},
{
"path": "explainshell/store.py",
"chars": 15180,
"preview": "'''data objects to save processed man pages to mongodb'''\nimport pymongo, collections, re, logging\n\nfrom explainshell im"
},
{
"path": "explainshell/util.py",
"chars": 3810,
"preview": "import itertools\nfrom operator import itemgetter\n\ndef consecutive(l, fn):\n '''yield consecutive items from l that fn "
},
{
"path": "explainshell/web/__init__.py",
"chars": 215,
"preview": "from flask import Flask\napp = Flask(__name__)\n\nfrom explainshell.web import views\nfrom explainshell import store, config"
},
{
"path": "explainshell/web/debugviews.py",
"chars": 2945,
"preview": "import logging\n\nfrom flask import render_template, request, abort, redirect, url_for, json\n\nfrom explainshell import man"
},
{
"path": "explainshell/web/helpers.py",
"chars": 792,
"preview": "from explainshell import util\n\ndef convertparagraphs(manpage):\n for p in manpage.paragraphs:\n p.text = p.text."
},
{
"path": "explainshell/web/static/css/bootstrap-responsive.css",
"chars": 22111,
"preview": "/*!\n * Bootstrap Responsive v2.3.1\n *\n * Copyright 2012 Twitter, Inc\n * Licensed under the Apache License v2.0\n * http:/"
},
{
"path": "explainshell/web/static/css/bootstrap.css",
"chars": 127247,
"preview": "/*!\n * Bootstrap v2.3.1\n *\n * Copyright 2012 Twitter, Inc\n * Licensed under the Apache License v2.0\n * http://www.apache"
},
{
"path": "explainshell/web/static/css/es.css",
"chars": 3289,
"preview": "html {\n overflow-y: scroll;\n}\n\nbody {\n font-family: \"Courier New\", Courier, Monaco, Menlo, Consolas, monospace;\n}\n"
},
{
"path": "explainshell/web/static/css/font-awesome.css",
"chars": 27232,
"preview": "/*!\n * Font Awesome 3.2.1\n * the iconic font designed for Bootstrap\n * ----------------------------------------------"
},
{
"path": "explainshell/web/static/js/bootstrap.js",
"chars": 61752,
"preview": "/* ===================================================\n * bootstrap-transition.js v2.3.1\n * http://twitter.github.com/bo"
},
{
"path": "explainshell/web/static/js/d3.v3.js",
"chars": 292405,
"preview": "d3 = function() {\n var d3 = {\n version: \"3.1.4\"\n };\n if (!Date.now) Date.now = function() {\n return +new Date()"
},
{
"path": "explainshell/web/static/js/es.js",
"chars": 43770,
"preview": "jQuery.fn.reverse = [].reverse;\n\nvar debug = false;\n\nvar themeCookieName = 'theme';\n\nif (!debug) {\n console = console"
},
{
"path": "explainshell/web/static/js/jquery.js",
"chars": 268381,
"preview": "/*!\n * jQuery JavaScript Library v1.9.1\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Cop"
},
{
"path": "explainshell/web/static/js/underscore.js",
"chars": 41426,
"preview": "// Underscore.js 1.4.4\n// ===================\n\n// > http://underscorejs.org\n// > (c) 2009-2013 Jeremy Ashkenas, Document"
},
{
"path": "explainshell/web/templates/about.html",
"chars": 1195,
"preview": "{% extends \"base.html\" %}\n{% block title %} - about{% endblock %}\n {% block content -%}\n <div id=\""
},
{
"path": "explainshell/web/templates/base.html",
"chars": 3781,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>explainshell.com{% block title %}{% endblock %}</title>\n <meta n"
},
{
"path": "explainshell/web/templates/debug.html",
"chars": 920,
"preview": "{% extends \"base.html\" %}\n\t{% block content %}\n {% if d %}\n <div class=\"small-push\"></div>\n "
},
{
"path": "explainshell/web/templates/errors/error.html",
"chars": 404,
"preview": "{% extends \"base.html\" %}\n {% block content -%}\n <div class=\"push\"></div>\n <div class=\""
},
{
"path": "explainshell/web/templates/errors/missingmanpage.html",
"chars": 138,
"preview": "{% extends \"errors/error.html\" %}\n{% block message -%}\nNo man page found for <span class=\"program-text\">{{ e|e }}</span>"
},
{
"path": "explainshell/web/templates/errors/parsingerror.html",
"chars": 233,
"preview": "{% extends \"errors/error.html\" %}\n{% block message -%}\n<div class=\"push\">{{ e }}</div>\n<pre style=\"overflow: auto; white"
},
{
"path": "explainshell/web/templates/explain.html",
"chars": 3558,
"preview": "{% extends \"base.html\" %}\n{% block title %} - {{ getargs|e }}{% endblock %}\n\t{% block content %}\n <div id=\"na"
},
{
"path": "explainshell/web/templates/index.html",
"chars": 2510,
"preview": "{% extends \"base.html\" %}\n{% import 'macros.html' as macros -%}\n{% block menu %}{% endblock %}\n{% block title %} - match"
},
{
"path": "explainshell/web/templates/macros.html",
"chars": 2145,
"preview": "{% macro outputcommand(mp, suggestions) -%}\n {% if suggestions|length == 0 %}\n <a href=\"http://manpages.ubuntu.com"
},
{
"path": "explainshell/web/templates/options.html",
"chars": 971,
"preview": "{% extends \"base.html\" %}\n{% set t = mp.program|e %}\n{% if mp.synopsis %}\n{% set t = mp.program|e + ' - ' + mp.synopsis|"
},
{
"path": "explainshell/web/templates/tagger.html",
"chars": 7688,
"preview": "{% extends \"base.html\" %}\n\t{% block content %}\n <div class=\"small-push\"></div>\n <div class=\"push\">"
},
{
"path": "explainshell/web/views.py",
"chars": 9329,
"preview": "import logging, itertools, urllib\nimport markupsafe\n\nfrom flask import render_template, request, redirect\n\nimport bashle"
},
{
"path": "misc/crontab",
"chars": 27,
"preview": "0 0 1,15 * * certbot renew\n"
},
{
"path": "misc/logrotate/explainshell",
"chars": 355,
"preview": "/home/idan/logs/uwsgi.log {\n copytruncate\n daily\n rotate 30\n size 10M\n compress\n delaycompress\n missingok\n notif"
},
{
"path": "misc/nginx/explainshell.conf",
"chars": 1029,
"preview": "log_format timed_combined '$remote_addr - $remote_user [$time_local] '\n '\"$request\" $status $body_bytes_sent '\n '"
},
{
"path": "misc/setup.bash",
"chars": 395,
"preview": "set -e\n\n# TODO(idank): test this on a clean machine\n\nsudo apt-get install nginx git python-pip mongodb supervisor virtua"
},
{
"path": "misc/supervisord/uwsgi.conf",
"chars": 365,
"preview": "[program:explainshell]\ncommand=/usr/local/bin/uwsgi\n --socket /tmp/explainshell.sock\n --logto /home/idan/logs/uwsgi.lo"
},
{
"path": "requirements.txt",
"chars": 84,
"preview": "Flask==0.12\nnltk==3.4.5\nMarkupSafe==1.1.1\nnose==1.3.0\npymongo==3.13.0\nbashlex==0.12\n"
},
{
"path": "runserver.py",
"chars": 289,
"preview": "from explainshell import config\nfrom explainshell.web import app\n\nimport logging.config\nlogging.config.dictConfig(config"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/helpers.py",
"chars": 1774,
"preview": "from explainshell import matcher, store, errors, options, helpconstants\n\nclass mockstore(object):\n def __init__(self)"
},
{
"path": "tests/test-fixer.py",
"chars": 2101,
"preview": "import unittest\nimport copy\n\nfrom explainshell import fixer, options, store\n\nclass test_fixer(unittest.TestCase):\n de"
},
{
"path": "tests/test-integration.py",
"chars": 1289,
"preview": "import unittest, subprocess, pymongo, os\n\nfrom explainshell import manager, config, matcher\n\n@unittest.skip(\"nltk usage "
},
{
"path": "tests/test-manager.py",
"chars": 5091,
"preview": "import unittest, os\n\nfrom explainshell import manager, config, store, errors\n\n@unittest.skip(\"nltk usage is broken due t"
},
{
"path": "tests/test-manpage.py",
"chars": 1206,
"preview": "import unittest, os, subprocess\n\nfrom explainshell import manpage, store\n\nclass test_manpage(unittest.TestCase):\n def"
},
{
"path": "tests/test-matcher.py",
"chars": 25908,
"preview": "import unittest\n\nimport bashlex.errors, bashlex.ast\n\nfrom explainshell import matcher, errors, helpconstants\nfrom tests "
},
{
"path": "tests/test-options.py",
"chars": 4063,
"preview": "import unittest\n\nfrom explainshell import options, store, errors\n\nclass test_options(unittest.TestCase):\n def test_si"
},
{
"path": "tools/dlgzlist",
"chars": 244,
"preview": "#!/bin/bash -e\n\nwhile read url; do\n\tif [ -f $(printf \"$url\" | cut -d / -f 7) ]; then\n\t\tcontinue\n\tfi\n\n\tpython -c 'import "
},
{
"path": "tools/extractgzlist",
"chars": 420,
"preview": "#!/bin/bash\n\nrelease=\"precise\"\nsection=\"8\"\nman1list=\"http://manpages.ubuntu.com/manpages/$release/en/man$section/\"\ngzroo"
},
{
"path": "tools/shellbuiltins.py",
"chars": 5874,
"preview": "# -*- coding: utf-8 -*-\n\n'''manually define and add shell builtins into the store\n\nunfortunately the bash section for bu"
},
{
"path": "tools/w3mman2html.cgi",
"chars": 5895,
"preview": "#!/usr/bin/env perl\n\n$MAN = $ENV{'W3MMAN_MAN'} || '@MAN@';\n$QUERY = $ENV{'QUERY_STRING'} || $ARGV[0];\n$SCRIPT_NAME = $EN"
}
]
// ... and 3 more files (download for full content)
About this extraction
This page contains the full source code of the idank/explainshell GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (1.1 MB), approximately 319.8k tokens, and a symbol index with 789 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.