Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ As it turns out, a lot of different citation styles omit various fields, and it'
Therefore, I created this tool (in Python, since that's what I know best), that can parse the entries and then performs
arbitrary (self-defined) invariant checks on them.

In my field the most used citation style is `IEEEtran` so this is how I've defined the default rules of the script.
I've written down the observations on which the rules are based [here](test/test_template/IEEEtran_observations.md).
In my field the most used citation style is `ieeetr` so this is how I've defined the default rules of the script.
I've written down the observations on which the rules are based [here](test/test_template/IEEEtr_observations.md).
These should not be confused with the LaTeX built-in `IEEEtran` citation style, for which I also developed rules.

It is however relatively easy to define your own [custom ruleset](#advanced-custom-rulesets), should the need arise.

Expand All @@ -45,6 +46,14 @@ The script will parse the file, perform the checks and print out the results.
> As the `bibtex_linter` returns exit code `0`, if all checks have passed and `1`, if violations were found,
> you could also use it in the CI of your LaTeX projects.

### Defined Rulesets
Currently, the following rulesets are shipped with the `bibtex_linter`:

- `bibtex_linter path/to/refs.bib ieeetr` (default): Citation style of some IEEE conferences (needs `IEEEtran.cls`)
- `bibtex_linter path/to/refs.bib IEEEtran`: LaTeX built-in IEEE citation style (via `\bibliographystyle{IEEEtran}`)

If you want to define your own rules, see the next section on how to do this:

### Advanced: Custom Rulesets

It is also possible to define your own rules inside a Python file.
Expand Down
File renamed without changes.
300 changes: 300 additions & 0 deletions bibtex_linter/ieeetran_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
from typing import List, Set
import re

from bibtex_linter.parser import BibTeXEntry
from bibtex_linter.verification import (
linter_rule,
check_required_fields,
check_required_field,
check_omitted_fields,
check_disallowed_field,
)


@linter_rule(entry_type=None)
def check_url_field(entry: BibTeXEntry) -> List[str]:
"""
Check that the `url` field is not set.
Additionally, if the `note` field is set, check that it conforms to the following schema:

```
[ONLINE]. Available: \\url{...}, Accessed: YYYY-mmm-dd
```
Note, that the backslash had to be escaped here and is only meant to be a single one.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
if "url" in entry.fields.keys():
invariant_violations.append(
f"Entry '{entry.name}' contains the non-allowed field: [url]. "
f"Move the content of the field into the [note] field."
)
if "note" in entry.fields.keys():
note_content: str = entry.fields["note"]
pattern = r"^\[ONLINE\]\. Available: \\url\{(.+?)\}, Accessed: (\d{4}-\d{2}-\d{2})$"
match = re.match(pattern, note_content)
if not match:
invariant_violations.append(
f"Entry '{entry.name}' contains a malformed field [note]. "
"Make sure the [note] field follows the following pattern: '[ONLINE]. Available: \\url{...}, "
"Accessed: YYYY-mmm-dd'"
)
return invariant_violations


@linter_rule(entry_type="article")
def check_article(entry: BibTeXEntry) -> List[str]:
"""
Check that the article entry type contains the required fields.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"journal",
"year"
}
))
return invariant_violations


@linter_rule(entry_type="conference")
def check_conference(entry: BibTeXEntry) -> List[str]:
"""
Check that conference entry type contains all required fields.
Additionally, check that 'publisher' and 'organization' are not duplicates of each other.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"booktitle",
"publisher",
"year",
"type",
}
))
invariant_violations.extend(check_required_field(
entry,
field="booktitle",
explanation="This should be the name of the conference.",
))
invariant_violations.extend(check_required_field(
entry,
field="publisher",
explanation="This should be the company that published the proceedings.",
))
invariant_violations.extend(check_required_field(
entry,
field="type",
explanation="This should describe the type of report/publication (e.g., “Conference Paper”).",
))
if entry.fields.get("organization") == entry.fields.get("publisher"):
invariant_violations.append(
f"Entry '{entry.name}' fields [organization] and [publisher] are the same. Remove field [organization]."
)
return invariant_violations


@linter_rule(entry_type="online")
def check_online(entry: BibTeXEntry) -> List[str]:
"""
Check that online entry type contains all required fields.
Additionally, check that 'author' and 'organization' are not duplicates of each other.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"year",
"howpublished",
}
))
invariant_violations.extend(check_required_field(
entry,
field="howpublished",
explanation="This should be something like: 'White paper', 'Blog post', 'GitHub repository', etc.",
))
if entry.fields.get("organization") == entry.fields.get("author"):
invariant_violations.append(
f"Entry '{entry.name}' fields [organization] and [author] are the same. Remove field [organization]."
)
return invariant_violations


@linter_rule(entry_type="book")
def check_book(entry: BibTeXEntry) -> List[str]:
"""
Check that book entry type contains all required fields.
Additionally, check that 'publisher' and 'editor' are not duplicates of each other.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"year",
"publisher",
}
))
if entry.fields.get("publisher") == entry.fields.get("editor"):
invariant_violations.append(
f"Entry '{entry.name}' fields [publisher] and [editor] are the same. Remove field [editor]."
)
return invariant_violations


@linter_rule(entry_type="inbook")
def check_in_book(entry: BibTeXEntry) -> List[str]:
"""
Check that inbook entry type contains all required fields.
Additionally check that the field `editor` is not present.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"year",
"publisher",
}
))
invariant_violations.extend(check_required_field(
entry,
field="title",
explanation="This should be the title of the book.",
))
invariant_violations.extend(check_disallowed_field(
entry,
field="editor",
explanation="This field is not rendered in IEEEtran-style.",
))
return invariant_violations


@linter_rule(entry_type="incollection")
def check_in_collection(entry: BibTeXEntry) -> List[str]:
"""
Check that incollection entry type contains all required fields.
Additionally, check that the field `type` is not set.
Furthermore, check that 'editor' and 'publisher' are not duplicates of each other.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"year",
"booktitle",
"publisher",
}
))
invariant_violations.extend(check_disallowed_field(
entry,
field="type",
explanation="If this field is set to (Article, Paper, Essay etc.), you should use a different entry type."
))
if entry.fields.get("editor") == entry.fields.get("publisher"):
invariant_violations.append(
f"Entry '{entry.name}' fields [editor] and [publisher] are the same. Remove field [editor]."
)
return invariant_violations


@linter_rule(entry_type="standard")
def check_standard(entry: BibTeXEntry) -> List[str]:
"""
Check that standard entry type contains all required fields.
Furthermore, check that 'author' and 'organization' are not duplicates of each other.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"title",
"organization",
"type",
"number",
"year",
}
))
invariant_violations.extend(check_required_field(
entry,
field="organization",
explanation="This should be the issuing body or standards organization.",
))
invariant_violations.extend(check_required_field(
entry,
field="type",
explanation="This should be something like "
"(Standard, Technical Report, Recommendation, Specification, Guideline, Draft Standard).",
))
if entry.fields.get("author") == entry.fields.get("organization"):
invariant_violations.append(
f"Entry '{entry.name}' fields [author] and [organization] are the same. Remove field [author]."
)
return invariant_violations


@linter_rule(entry_type="techreport")
def check_tech_report(entry: BibTeXEntry) -> List[str]:
"""
Disallow the use of the techreport entry type.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
return [f"Entry '{entry.name}' is of type 'TECHREPORT'. Please use a different entry type, such as 'STANDARD'."]


@linter_rule(entry_type="misc")
def check_misc(entry: BibTeXEntry) -> List[str]:
"""
Check that misc entry type contains all required fields.

:param entry: The BibTeXEntry
:return: A list of string descriptions of rule violations for this entry.
"""
invariant_violations: List[str] = []
invariant_violations.extend(check_required_fields(
entry,
fields={
"author",
"title",
"howpublished",
"year",
}
))
return invariant_violations
15 changes: 11 additions & 4 deletions bibtex_linter/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,25 @@ def main() -> None:
type=str,
nargs="?",
default=None,
help="Path to the rules.py that define the rules. If left empty, the default ruleset is used. "
"WARNING: Executes the Python code inside rules.py, so be sure that it's safe!")
help="Name (ieeetr, IEEEtran) of or path to the rules.py that define the rules. "
"If left empty, the default ruleset (ieeetr) is used. "
"WARNING: Executes the Python code inside rules.py, so be sure that it's safe! "
"See https://github.com/s-heppner/python-bibtex-linter for more information.")

args = parser.parse_args()

# Try to import the ruleset
if args.ruleset is None:
import bibtex_linter.default_rules
import bibtex_linter.ieeetr_rules
print("Using the default ruleset.")
else:
print(f"Importing rules from {args.ruleset}.")
import_from_path(args.ruleset)
if args.ruleset in ["default", "ieeetr"]:
import bibtex_linter.ieeetr_rules
elif args.ruleset == "IEEEtran":
import bibtex_linter.ieeetran_rules
else:
import_from_path(args.ruleset)

entries: List[BibTeXEntry] = parse_bibtex_file(args.filepath)
had_violations = False
Expand Down
Loading