├── .gitignore ├── LICENSE ├── README.md ├── nginxfmt.py ├── pyproject.toml ├── test-files ├── not-formatted-1.conf ├── umlaut-latin1.conf └── umlaut-utf8.conf └── test_nginxfmt.py /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | examples/ 141 | deploy*.sh 142 | 143 | poetry.lock 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *nginx* config file formatter/beautifier 2 | 3 | *nginx* config file formatter/beautifier written in Python with no additional dependencies. 4 | It can be used as a library or a standalone script. 5 | It formats *nginx* configuration files in a consistent way, described below: 6 | 7 | * All lines are indented uniformly, with 4 spaces per level. The number of spaces is customizable. 8 | * Neighboring empty lines are collapsed to at most two. 9 | * Curly brace placement follows the Java convention. 10 | * Whitespace is collapsed, except in comments and within quotation marks. 11 | * Newline characters are normalized to the operating system default (LF or CRLF), but this can be overridden. 12 | 13 | 14 | ## Installation 15 | 16 | Python 3.4 or later is needed to run this program. 17 | The easiest way is to download the package from PyPI: 18 | 19 | ```bash 20 | pip install nginxfmt 21 | ``` 22 | 23 | 24 | ### Manual installation 25 | 26 | The simplest form of installation is copying `nginxfmt.py` to your scripts' directory. 27 | It has no third-party dependencies. 28 | 29 | You can also clone the repository and symlink the executable: 30 | 31 | ``` 32 | cd 33 | git clone https://github.com/slomkowski/nginx-config-formatter.git 34 | ln -s ~/nginx-config-formatter/nginxfmt.py ~/bin/nginxfmt.py 35 | ``` 36 | 37 | 38 | ## Usage as standalone script 39 | 40 | It can format one or several files. 41 | By default, the result is saved to the original file, but it can be redirected to *stdout*. 42 | It can also function in piping mode, using the `--pipe` or `-` switch. 43 | 44 | ``` 45 | usage: nginxfmt.py [-h] [-v] [-] [-p | -b] [-i INDENT] [--line-endings {auto,unix,windows,crlf,lf}] [config_files ...] 46 | 47 | Formats nginx configuration files in consistent way. 48 | 49 | positional arguments: 50 | config_filesconfiguration files to format 51 | 52 | options: 53 | -h, --helpshow this help message and exit 54 | -v, --verbose show formatted file names 55 | -, --pipe reads content from standard input, prints result to stdout 56 | -p, --print-resultprints result to stdout, original file is not changed 57 | -b, --backup-original 58 | backup original config file as filename.conf~ 59 | 60 | formatting options: 61 | -i, --indent INDENT specify number of spaces for indentation 62 | --line-endings {auto,unix,windows,crlf,lf} 63 | specify line ending style: 'unix' or 'lf' for \n, 'windows' or 'crlf' for \r\n. When not provided, system-default is used 64 | ``` 65 | 66 | 67 | ## Using as library 68 | 69 | The main logic is within the `Formatter` class, which can be used in third-party code. 70 | 71 | ```python 72 | import nginxfmt 73 | 74 | # initializing with standard FormatterOptions 75 | f = nginxfmt.Formatter() 76 | 77 | # format from string 78 | formatted_text = f.format_string(unformatted_text) 79 | 80 | # format file and save result to the same file 81 | f.format_file(unformatted_file_path) 82 | 83 | # format file and save result to the same file, original unformatted content is backed up 84 | f.format_file(unformatted_file_path, backup_path) 85 | ``` 86 | 87 | Customizing formatting options: 88 | 89 | ```python 90 | import nginxfmt 91 | 92 | fo = nginxfmt.FormatterOptions() 93 | fo.indentation = 2# 2 spaces instead of default 4 94 | fo.line_endings = '\n'# force Unix line endings 95 | 96 | # initialize with standard FormatterOptions 97 | f = nginxfmt.Formatter(fo) 98 | ``` 99 | 100 | 101 | ## Reporting bugs 102 | 103 | Please create an issue at https://github.com/slomkowski/nginx-config-formatter/issues. 104 | Be sure to include config snippets to reproduce the issue, preferably: 105 | 106 | * Snippet to be formatted 107 | * Actual result with the invalid formatting 108 | * Desired result 109 | 110 | ## Credits 111 | 112 | Copyright 2021 Michał Słomkowski. 113 | License: Apache 2.0. 114 | Previously published under https://github.com/1connect/nginx-config-formatter. 115 | -------------------------------------------------------------------------------- /nginxfmt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """nginx config file formatter/beautifier with no additional dependencies. 5 | 6 | Originally published under https://github.com/1connect/nginx-config-formatter, 7 | then moved to https://github.com/slomkowski/nginx-config-formatter. 8 | """ 9 | 10 | import argparse 11 | import contextlib 12 | import io 13 | import logging 14 | import os 15 | import pathlib 16 | import re 17 | import sys 18 | 19 | __author__ = "Michał Słomkowski" 20 | __license__ = "Apache 2.0" 21 | __version__ = "1.3.0" 22 | 23 | 24 | class FormatterOptions: 25 | """Class holds the formatting options. For now, only indentation supported.""" 26 | indentation = 4 27 | line_endings = os.linesep 28 | 29 | 30 | class Formatter: 31 | """nginx formatter. Can format config loaded from file or string.""" 32 | _TEMPLATE_VARIABLE_OPENING_TAG = '___TEMPLATE_VARIABLE_OPENING_TAG___' 33 | _TEMPLATE_VARIABLE_CLOSING_TAG = '___TEMPLATE_VARIABLE_CLOSING_TAG___' 34 | 35 | _TEMPLATE_BRACKET_OPENING_TAG = '___TEMPLATE_BRACKET_OPENING_TAG___' 36 | _TEMPLATE_BRACKET_CLOSING_TAG = '___TEMPLATE_BRACKET_CLOSING_TAG___' 37 | 38 | def __init__(self, 39 | options: FormatterOptions = FormatterOptions(), 40 | logger: logging.Logger = None): 41 | self.logger = logger if logger is not None else logging.getLogger(__name__) 42 | self.options = options 43 | 44 | def format_string(self, 45 | contents: str) -> str: 46 | """Accepts the string containing nginx configuration and returns formatted one. Adds newline at the end.""" 47 | ls = self.options.line_endings 48 | lines = contents.splitlines() 49 | lines = self._apply_bracket_template_tags(lines) 50 | lines = self._clean_lines(lines) 51 | lines = self._join_opening_bracket(lines) 52 | lines = self._perform_indentation(lines) 53 | 54 | text = ls.join(lines) 55 | text = self._strip_bracket_template_tags(text) 56 | 57 | for pattern, substitute in ( 58 | (r'%s{3,}' % ls, '%s%s%s' % (ls, ls, ls)), 59 | (r'^%s' % ls, ''), 60 | (r'%s$' % ls, '') 61 | ): 62 | text = re.sub(pattern, substitute, text) 63 | 64 | return text + ls 65 | 66 | def get_formatted_string_from_file(self, 67 | file_path: pathlib.Path) -> str: 68 | """Loads nginx config from file, performs formatting and returns contents as string. 69 | :param file_path: path to original nginx configuration file.""" 70 | 71 | _, original_file_content = self._load_file_content(file_path) 72 | return self.format_string(original_file_content) 73 | 74 | def format_file(self, 75 | file_path: pathlib.Path, 76 | original_backup_file_path: pathlib.Path = None): 77 | """Performs the formatting on the given file. The function tries to detect file encoding first. 78 | :param file_path: path to original nginx configuration file. This file will be overridden. 79 | :param original_backup_file_path: optional path, where original file will be backed up.""" 80 | 81 | chosen_encoding, original_file_content = self._load_file_content(file_path) 82 | 83 | with file_path.open('w', encoding=chosen_encoding) as wfp: 84 | wfp.write(self.format_string(original_file_content)) 85 | 86 | self.logger.info("Formatted content written to original file.") 87 | 88 | if original_backup_file_path: 89 | with original_backup_file_path.open('w', encoding=chosen_encoding) as wfp: 90 | wfp.write(original_file_content) 91 | self.logger.info("Original content saved to '%s'.", original_backup_file_path) 92 | 93 | def _load_file_content(self, 94 | file_path: pathlib.Path) -> (str, str): 95 | """Determines the encoding of the input file and loads its content to string. 96 | :param file_path: path to original nginx configuration file.""" 97 | 98 | encodings = ('utf-8', 'latin1') 99 | encoding_failures = [] 100 | chosen_encoding = None 101 | original_file_content = None 102 | 103 | for enc in encodings: 104 | try: 105 | with file_path.open('r', encoding=enc) as rfp: 106 | original_file_content = rfp.read() 107 | chosen_encoding = enc 108 | break 109 | except ValueError as e: 110 | encoding_failures.append(e) 111 | 112 | if chosen_encoding is None: 113 | raise Exception('none of encodings %s are valid for file %s. Errors: %s' 114 | % (encodings, file_path, [str(e) for e in encoding_failures])) 115 | 116 | self.logger.info("Loaded file '%s' (detected encoding %s).", file_path, chosen_encoding) 117 | 118 | assert original_file_content is not None 119 | return chosen_encoding, original_file_content 120 | 121 | @staticmethod 122 | def _strip_line(single_line): 123 | """Strips the line and replaces neighbouring whitespaces with single space (except when within quotation 124 | marks). """ 125 | single_line = single_line.strip() 126 | if single_line.startswith('#'): 127 | return single_line 128 | 129 | within_quotes = False 130 | quote_char = None 131 | result = [] 132 | for char in single_line: 133 | if char in ['"', "'"]: 134 | if within_quotes: 135 | if char == quote_char: 136 | within_quotes = False 137 | quote_char = None 138 | else: 139 | within_quotes = True 140 | quote_char = char 141 | result.append(char) 142 | elif not within_quotes and re.match(r'\s', char): 143 | if result[-1] != ' ': 144 | result.append(' ') 145 | else: 146 | result.append(char) 147 | return ''.join(result) 148 | 149 | @staticmethod 150 | def _count_multi_semicolon(single_line): 151 | """Count multi semicolon (except when within quotation marks).""" 152 | single_line = single_line.strip() 153 | if single_line.startswith('#'): 154 | return 0, 0 155 | 156 | within_quotes = False 157 | quote_char = None 158 | q = 0 159 | c = 0 160 | for char in single_line: 161 | if char in ['"', "'"]: 162 | if within_quotes: 163 | if char == quote_char: 164 | within_quotes = False 165 | quote_char = None 166 | else: 167 | within_quotes = True 168 | quote_char = char 169 | q = 1 170 | elif not within_quotes and char == ';': 171 | c += 1 172 | return q, c 173 | 174 | def _multi_semicolon(self, single_line): 175 | """Break multi semicolon into multiline (except when within quotation marks).""" 176 | single_line = single_line.strip() 177 | if single_line.startswith('#'): 178 | return single_line 179 | 180 | within_quotes = False 181 | quote_char = None 182 | result = [] 183 | for char in single_line: 184 | if char in ['"', "'"]: 185 | if within_quotes: 186 | if char == quote_char: 187 | within_quotes = False 188 | quote_char = None 189 | else: 190 | within_quotes = True 191 | quote_char = char 192 | result.append(char) 193 | elif not within_quotes and char == ';': 194 | result.append(";%s" % self.options.line_endings) 195 | else: 196 | result.append(char) 197 | return ''.join(result) 198 | 199 | def _apply_variable_template_tags(self, line: str) -> str: 200 | """Replaces variable indicators ${ and } with tags, so subsequent formatting is easier.""" 201 | return re.sub(r'\${\s*(\w+)\s*}', 202 | self._TEMPLATE_VARIABLE_OPENING_TAG + r"\1" + self._TEMPLATE_VARIABLE_CLOSING_TAG, 203 | line, 204 | flags=re.UNICODE) 205 | 206 | def _strip_variable_template_tags(self, line: str) -> str: 207 | """Replaces tags back with ${ and } respectively.""" 208 | return re.sub(self._TEMPLATE_VARIABLE_OPENING_TAG + r'\s*(\w+)\s*' + self._TEMPLATE_VARIABLE_CLOSING_TAG, 209 | r'${\1}', 210 | line, 211 | flags=re.UNICODE) 212 | 213 | def _apply_bracket_template_tags(self, lines): 214 | """ Replaces bracket { and } with tags, so subsequent formatting is easier.""" 215 | formatted_lines = [] 216 | 217 | for line in lines: 218 | formatted_line = "" 219 | in_quotes = False 220 | last_char = "" 221 | 222 | if line.startswith('#'): 223 | formatted_line += line 224 | else: 225 | for char in line: 226 | if (char == "\'" or char == "\"") and last_char != "\\": 227 | in_quotes = self._reverse_in_quotes_status(in_quotes) 228 | 229 | if in_quotes: 230 | if char == "{": 231 | formatted_line += self._TEMPLATE_BRACKET_OPENING_TAG 232 | elif char == "}": 233 | formatted_line += self._TEMPLATE_BRACKET_CLOSING_TAG 234 | else: 235 | formatted_line += char 236 | else: 237 | formatted_line += char 238 | 239 | last_char = char 240 | 241 | formatted_lines.append(formatted_line) 242 | 243 | return formatted_lines 244 | 245 | @staticmethod 246 | def _reverse_in_quotes_status(status: bool) -> bool: 247 | if status: 248 | return False 249 | return True 250 | 251 | def _strip_bracket_template_tags(self, content: str) -> str: 252 | """ Replaces tags back with { and } respectively.""" 253 | content = content.replace(self._TEMPLATE_BRACKET_OPENING_TAG, "{", -1) 254 | content = content.replace(self._TEMPLATE_BRACKET_CLOSING_TAG, "}", -1) 255 | return content 256 | 257 | def _clean_lines(self, orig_lines) -> list: 258 | """Strips the lines and splits them if they contain curly brackets.""" 259 | cleaned_lines = [] 260 | for line in orig_lines: 261 | line = self._strip_line(line) 262 | line = self._apply_variable_template_tags(line) 263 | if line == "": 264 | cleaned_lines.append("") 265 | elif line == "};": 266 | cleaned_lines.append("}") 267 | elif line.startswith("#"): 268 | cleaned_lines.append(self._strip_variable_template_tags(line)) 269 | else: 270 | q, c = self._count_multi_semicolon(line) 271 | if q == 1 and c > 1: 272 | ml = self._multi_semicolon(line) 273 | cleaned_lines.extend(self._clean_lines(ml.splitlines())) 274 | elif q != 1 and c > 1: 275 | newlines = line.split(";") 276 | lines_to_add = self._clean_lines(["".join([ln, ";"]) for ln in newlines if ln != ""]) 277 | cleaned_lines.extend(lines_to_add) 278 | else: 279 | if line.startswith("rewrite"): 280 | cleaned_lines.append(self._strip_variable_template_tags(line)) 281 | else: 282 | cleaned_lines.extend( 283 | [self._strip_variable_template_tags(ln).strip() for ln in re.split(r"([{}])", line) if 284 | ln != ""]) 285 | return cleaned_lines 286 | 287 | @staticmethod 288 | def _join_opening_bracket(lines): 289 | """When opening curly bracket is in it's own line (K&R convention), it's joined with precluding line (Java).""" 290 | modified_lines = [] 291 | for i in range(len(lines)): 292 | if i > 0 and lines[i] == "{": 293 | modified_lines[-1] += " {" 294 | else: 295 | modified_lines.append(lines[i]) 296 | return modified_lines 297 | 298 | def _perform_indentation(self, lines): 299 | """Indents the lines according to their nesting level determined by curly brackets.""" 300 | indented_lines = [] 301 | current_indent = 0 302 | indentation_str = ' ' * self.options.indentation 303 | for line in lines: 304 | if not line.startswith("#") and line.endswith('}') and current_indent > 0: 305 | current_indent -= 1 306 | 307 | if line != "": 308 | indented_lines.append(current_indent * indentation_str + line) 309 | else: 310 | indented_lines.append("") 311 | 312 | if not line.startswith("#") and line.endswith('{'): 313 | current_indent += 1 314 | 315 | return indented_lines 316 | 317 | 318 | @contextlib.contextmanager 319 | def _redirect_stdout_to_stderr(): 320 | """Redirects stdout to stderr for argument parsing. This is to don't pollute the stdout 321 | when --print-result is used.""" 322 | old_stdout = sys.stdout 323 | sys.stdout = sys.stderr 324 | try: 325 | yield 326 | finally: 327 | sys.stdout = old_stdout 328 | 329 | 330 | def _aname(action) -> str: 331 | """Converts argument name to string to be consistent with argparse.""" 332 | if action.option_strings: 333 | return '/'.join(action.option_strings) 334 | return action.dest 335 | 336 | 337 | def _standalone_run(program_arguments): 338 | arg_parser = argparse.ArgumentParser(description="Formats nginx configuration files in consistent way.") 339 | 340 | arg_parser.add_argument("-v", "--verbose", action="store_true", help="show formatted file names") 341 | 342 | pipe_arg = arg_parser.add_argument("-", "--pipe", 343 | action="store_true", 344 | help="reads content from standard input, prints result to stdout") 345 | 346 | pipe_xor_backup_group = arg_parser.add_mutually_exclusive_group() 347 | print_result_arg = pipe_xor_backup_group.add_argument("-p", "--print-result", 348 | action="store_true", 349 | help="prints result to stdout, original file is not changed") 350 | pipe_xor_backup_group.add_argument("-b", "--backup-original", 351 | action="store_true", 352 | help="backup original config file as filename.conf~") 353 | 354 | arg_parser.add_argument("config_files", 355 | nargs='*', 356 | help="configuration files to format") 357 | 358 | formatter_options_group = arg_parser.add_argument_group("formatting options") 359 | formatter_options_group.add_argument("-i", 360 | "--indent", 361 | action="store", 362 | default=4, 363 | type=int, 364 | help="specify number of spaces for indentation") 365 | formatter_options_group.add_argument( 366 | "--line-endings", 367 | choices=["auto", "unix", "windows", "crlf", "lf"], 368 | default="auto", 369 | help=( 370 | "specify line ending style: 'unix' or 'lf' for \\n, " 371 | "'windows' or 'crlf' for \\r\\n. When not provided, system-default is used" 372 | ) 373 | ) 374 | 375 | with _redirect_stdout_to_stderr(): 376 | args = arg_parser.parse_args(program_arguments) 377 | 378 | logging.basicConfig( 379 | level=logging.INFO if args.verbose else logging.ERROR, 380 | format='%(levelname)s: %(message)s') 381 | 382 | try: 383 | if args.pipe and len(args.config_files) != 0: 384 | raise Exception("if %s is enabled, no file can be passed as input" % _aname(pipe_arg)) 385 | if args.pipe and args.backup_original: 386 | raise Exception("cannot create backup file when %s is enabled" % _aname(pipe_arg)) 387 | if args.print_result and len(args.config_files) > 1: 388 | raise Exception("if %s is enabled, only one file can be passed as input" % _aname(print_result_arg)) 389 | if len(args.config_files) == 0 and not args.pipe: 390 | raise Exception("no input files provided, specify at least one file or use %s" % _aname(pipe_arg)) 391 | except Exception as e: 392 | arg_parser.error(str(e)) 393 | 394 | format_options = FormatterOptions() 395 | format_options.indentation = args.indent 396 | 397 | if args.line_endings in ["unix", "lf"]: 398 | format_options.line_endings = '\n' 399 | elif args.line_endings in ["windows", "crlf"]: 400 | format_options.line_endings = '\r\n' 401 | else: 402 | format_options.line_endings = os.linesep 403 | 404 | formatter = Formatter(format_options) 405 | 406 | if args.pipe: 407 | original_content = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') 408 | print(formatter.format_string(original_content.read()), end="") 409 | elif args.print_result: 410 | print(formatter.get_formatted_string_from_file(pathlib.Path(args.config_files[0])), end="") 411 | else: 412 | for config_file_path in args.config_files: 413 | backup_file_path = pathlib.Path(config_file_path + '~') if args.backup_original else None 414 | formatter.format_file(pathlib.Path(config_file_path), backup_file_path) 415 | 416 | 417 | def main(): 418 | _standalone_run(sys.argv[1:]) 419 | 420 | 421 | if __name__ == "__main__": 422 | main() 423 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nginxfmt" 3 | version = "1.3.0" 4 | description = "nginx config file formatter/beautifier with no additional dependencies." 5 | authors = ["Michał Słomkowski "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/slomkowski/nginx-config-formatter" 9 | repository = "https://github.com/slomkowski/nginx-config-formatter" 10 | keywords = ["nginx", "formatter", "formatting", "beautifier"] 11 | 12 | 13 | [tool.poetry.dependencies] 14 | python = ">=3.4" 15 | 16 | [tool.poetry.dev-dependencies] 17 | 18 | [tool.poetry.scripts] 19 | nginxfmt = "nginxfmt:main" 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /test-files/not-formatted-1.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name example.com; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test-files/umlaut-latin1.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slomkowski/nginx-config-formatter/6d891e6d5c570af00e21b230dd14ec4a5823d4ec/test-files/umlaut-latin1.conf -------------------------------------------------------------------------------- /test-files/umlaut-utf8.conf: -------------------------------------------------------------------------------- 1 | http { 2 | server { 3 | listen 80 default_server; 4 | server_name example.com; 5 | 6 | # redirect auf https://www.... 7 | location / { 8 | return 301 https://www.example.com$request_uri; 9 | } 10 | 11 | # Statusseite für Monitoring freigeben 12 | # line above contains german umlaut causing problems 13 | location /nginx_status { 14 | stub_status on; 15 | access_log off; 16 | allow 127.0.0.1; 17 | deny all; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test_nginxfmt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Unit tests for nginxfmt module.""" 5 | import contextlib 6 | import io 7 | import logging 8 | import pathlib 9 | import shutil 10 | import tempfile 11 | import unittest 12 | 13 | import nginxfmt 14 | 15 | __author__ = "Michał Słomkowski" 16 | __license__ = "Apache 2.0" 17 | 18 | 19 | class TestFormatter(unittest.TestCase): 20 | 21 | def __init__(self, method_name: str = ...) -> None: 22 | super().__init__(method_name) 23 | logging.basicConfig(level=logging.DEBUG) # todo fix logging in debug 24 | 25 | fmt_options = nginxfmt.FormatterOptions() 26 | fmt_options.line_endings = '\n' # force Unix LF for tests 27 | self.fmt = nginxfmt.Formatter(fmt_options) 28 | 29 | fmt_options_crlf = nginxfmt.FormatterOptions() 30 | fmt_options_crlf.line_endings = '\r\n' # CRLF formatter 31 | self.fmt_crlf = nginxfmt.Formatter(fmt_options_crlf) 32 | 33 | def check_formatting(self, original_text: str, formatted_text: str, formatter=None): 34 | self.assertMultiLineEqual(formatted_text, (formatter if formatter else self.fmt).format_string(original_text)) 35 | 36 | def check_stays_the_same(self, text: str): 37 | self.assertMultiLineEqual(text, self.fmt.format_string(text)) 38 | 39 | def _check_variable_tags_symmetry(self, text): 40 | self.assertMultiLineEqual(text, 41 | self.fmt._strip_variable_template_tags(self.fmt._apply_variable_template_tags(text))) 42 | 43 | def test_collapse_variable1(self): 44 | self.check_formatting(" lorem ipsum ${ dol } amet", "lorem ipsum ${dol} amet\n") 45 | 46 | def test_join_opening_parenthesis(self): 47 | self.assertEqual(["foo", "bar {", "johan {", "tee", "ka", "}"], 48 | self.fmt._join_opening_bracket(("foo", "bar {", "johan", "{", "tee", "ka", "}"))) 49 | 50 | def test_clean_lines(self): 51 | self.assertEqual(["ala", "ma", "{", "kota", "}", "to;", "", "ook"], 52 | self.fmt._clean_lines(("ala", "ma {", "kota", "}", "to;", "", "ook"))) 53 | 54 | self.assertEqual(["ala", "ma", "{", "{", "kota", "}", "to", "}", "ook"], 55 | self.fmt._clean_lines(("ala", "ma {{", "kota", "}", "to}", "ook"))) 56 | 57 | self.assertEqual(["{", "ala", "ma", "{", "{", "kota", "}", "to", "}"], 58 | self.fmt._clean_lines(("{", "ala ", "ma {{", " kota ", "}", " to} "))) 59 | 60 | self.assertEqual(["{", "ala", "# ma {{", "kota", "}", "to", "}", "# }"], 61 | self.fmt._clean_lines(("{", "ala ", "# ma {{", " kota ", "}", " to} ", "# }"))) 62 | 63 | self.assertEqual(["{", "ala", "# ma {{", r"rewrite /([\d]{2}) /up/$1.html last;", "}", "to", "}"], 64 | self.fmt._clean_lines( 65 | ("{", "ala ", "# ma {{", r" rewrite /([\d]{2}) /up/$1.html last; ", "}", " to", "}"))) 66 | 67 | self.assertEqual(["{", "ala", "# ma {{", "aa last;", "bb to;", "}"], 68 | self.fmt._clean_lines(("{", "ala ", "# ma {{", " aa last; bb to; ", "}"))) 69 | 70 | self.assertEqual(["{", "aa;", "b b \"cc; dd; ee \";", "ssss;", "}"], 71 | self.fmt._clean_lines(("{", "aa; b b \"cc; dd; ee \"; ssss;", "}"))) 72 | 73 | self.assertEqual([r"location ~ /\.ht", "{"], self.fmt._clean_lines([r"location ~ /\.ht {", ])) 74 | 75 | def test_perform_indentation(self): 76 | self.assertEqual([ 77 | "foo bar {", 78 | " fizz bazz;", 79 | "}"], self.fmt._perform_indentation(("foo bar {", "fizz bazz;", "}"))) 80 | 81 | self.assertEqual([ 82 | "foo bar {", 83 | " fizz bazz {", 84 | " lorem ipsum;", 85 | " asdf asdf;", 86 | " }", 87 | "}"], self.fmt._perform_indentation(("foo bar {", "fizz bazz {", "lorem ipsum;", "asdf asdf;", "}", "}"))) 88 | 89 | self.assertEqual([ 90 | "foo bar {", 91 | " fizz bazz {", 92 | " lorem ipsum;", 93 | " # }", 94 | " }", 95 | "}", 96 | "}", 97 | "foo {"], 98 | self.fmt._perform_indentation(("foo bar {", "fizz bazz {", "lorem ipsum;", "# }", "}", "}", "}", "foo {"))) 99 | 100 | self.assertEqual([ 101 | "foo bar {", 102 | " fizz bazz {", 103 | " lorem ipsum;", 104 | " }", 105 | "}", 106 | "}", 107 | "foo {"], 108 | self.fmt._perform_indentation(("foo bar {", "fizz bazz {", "lorem ipsum;", "}", "}", "}", "foo {"))) 109 | 110 | def test_strip_line(self): 111 | self.assertEqual("foo", self.fmt._strip_line(" foo ")) 112 | self.assertEqual("bar foo", self.fmt._strip_line(" bar foo ")) 113 | self.assertEqual("bar foo", self.fmt._strip_line(" bar \t foo ")) 114 | self.assertEqual('lorem ipsum " foo bar zip "', self.fmt._strip_line(' lorem ipsum " foo bar zip " ')) 115 | self.assertEqual('lorem ipsum " foo bar zip " or " dd aa " mi', 116 | self.fmt._strip_line(' lorem ipsum " foo bar zip " or \t " dd aa " mi')) 117 | 118 | def test_apply_bracket_template_tags(self): 119 | self.assertEqual( 120 | "\"aaa___TEMPLATE_BRACKET_OPENING_TAG___dd___TEMPLATE_BRACKET_CLOSING_TAG___bbb\"".splitlines(), 121 | self.fmt._apply_bracket_template_tags("\"aaa{dd}bbb\"".splitlines())) 122 | self.assertEqual( 123 | "\"aaa___TEMPLATE_BRACKET_OPENING_TAG___dd___TEMPLATE_BRACKET_CLOSING_TAG___bbb\"cc{cc}cc\"dddd___TEMPLATE_BRACKET_OPENING_TAG___eee___TEMPLATE_BRACKET_CLOSING_TAG___fff\"".splitlines(), 124 | self.fmt._apply_bracket_template_tags("\"aaa{dd}bbb\"cc{cc}cc\"dddd{eee}fff\"".splitlines())) 125 | 126 | def test_strip_bracket_template_tags1(self): 127 | self.assertEqual("\"aaa{dd}bbb\"", self.fmt._strip_bracket_template_tags( 128 | "\"aaa___TEMPLATE_BRACKET_OPENING_TAG___dd___TEMPLATE_BRACKET_CLOSING_TAG___bbb\"")) 129 | 130 | def test_apply_bracket_template_tags1(self): 131 | self.assertEqual( 132 | "\"aaa___TEMPLATE_BRACKET_OPENING_TAG___dd___TEMPLATE_BRACKET_CLOSING_TAG___bbb\"cc{cc}cc\"dddd___TEMPLATE_BRACKET_OPENING_TAG___eee___TEMPLATE_BRACKET_CLOSING_TAG___fff\"".splitlines(), 133 | self.fmt._apply_bracket_template_tags("\"aaa{dd}bbb\"cc{cc}cc\"dddd{eee}fff\"".splitlines())) 134 | 135 | def test_variable_template_tags(self): 136 | self.assertEqual("foo bar ___TEMPLATE_VARIABLE_OPENING_TAG___myvar___TEMPLATE_VARIABLE_CLOSING_TAG___", 137 | self.fmt._apply_variable_template_tags("foo bar ${myvar}")) 138 | self._check_variable_tags_symmetry("lorem ipsum ${dolor} $amet") 139 | self._check_variable_tags_symmetry("lorem ipsum ${dolor} $amet\nother $var and ${var_name2}") 140 | 141 | def test_umlaut_in_string(self): 142 | self.check_formatting( 143 | "# Statusseite für Monitoring freigeben \n" + 144 | "# line above contains german umlaut causing problems \n" + 145 | "location /nginx_status {\n" + 146 | " stub_status on;\n" + 147 | " access_log off;\n" + 148 | " allow 127.0.0.1;\n" + 149 | " deny all;\n" + 150 | "}", 151 | "# Statusseite für Monitoring freigeben\n" + 152 | "# line above contains german umlaut causing problems\n" + 153 | "location /nginx_status {\n" + 154 | " stub_status on;\n" + 155 | " access_log off;\n" + 156 | " allow 127.0.0.1;\n" + 157 | " deny all;\n" + 158 | "}\n" 159 | ) 160 | 161 | def test_empty_lines_removal(self): 162 | self.check_formatting( 163 | "\n foo bar {\n" + 164 | " lorem ipsum;\n" + 165 | "}\n\n\n", 166 | "foo bar {\n" + 167 | " lorem ipsum;\n" + 168 | "}\n") 169 | 170 | self.check_formatting( 171 | "\n foo bar {\n\n\n\n\n\n" + 172 | " lorem ipsum;\n" + 173 | "}\n\n\n", 174 | "foo bar {\n\n\n" + 175 | " lorem ipsum;\n" + 176 | "}\n") 177 | 178 | self.check_formatting( 179 | " foo bar {\n" + 180 | " lorem ipsum;\n" + 181 | " kee {\n" + 182 | "caak; \n" + 183 | "}}", 184 | "foo bar {\n" + 185 | " lorem ipsum;\n" + 186 | " kee {\n" + 187 | " caak;\n" + 188 | " }\n" + 189 | "}\n") 190 | 191 | def test_template_variables_with_dollars1(self): 192 | self.check_formatting('server {\n' + 193 | ' # commented ${line} should not be touched\n' + 194 | 'listen 80 default_server;\n' + 195 | 'server_name localhost;\n' + 196 | 'location / {\n' + 197 | 'proxy_set_header X-User-Auth "In ${cookie_access_token} ${ other}";\n' + 198 | 'proxy_set_header X-User-Other "foo ${bar}";\n' + 199 | '}\n' + 200 | '}', 201 | 'server {\n' + 202 | ' # commented ${line} should not be touched\n' + 203 | ' listen 80 default_server;\n' + 204 | ' server_name localhost;\n' + 205 | ' location / {\n' + 206 | ' proxy_set_header X-User-Auth "In ${cookie_access_token} ${ other}";\n' + 207 | ' proxy_set_header X-User-Other "foo ${bar}";\n' + 208 | ' }\n' + 209 | '}\n') 210 | 211 | def test_template_variables_with_dollars2(self): 212 | self.check_formatting(' some_tag { with_templates "my ${var} and other ${ invalid_variable_use } "; }\n' + 213 | '# in my line\n', 214 | 'some_tag {\n' + 215 | ' with_templates "my ${var} and other ${ invalid_variable_use } ";\n' + 216 | '}\n' + 217 | '# in my line\n') 218 | 219 | def test_backslash3(self): 220 | self.check_formatting('location ~ /\.ht {\n' + 221 | 'deny all;\n' + 222 | '}', 223 | 'location ~ /\.ht {\n' + 224 | ' deny all;\n' + 225 | '}\n') 226 | 227 | def test_backslash2(self): 228 | """If curly braces are withing quotation marks, we treat them as part of the string, not syntax structure. 229 | Writing '${ var }' is not valid in nginx anyway, so we slip collapsing these altogether. May be changed in 230 | the future. """ 231 | self.check_formatting( 232 | ' tag { wt ~ /\.ht \t "my ${some some} and ~ /\.ht \tother ${comething in curly braces } "; }\n' + 233 | '# in my line\n', 234 | 235 | 'tag {\n' + 236 | ' wt ~ /\.ht "my ${some some} and ~ /\.ht \tother ${comething in curly braces } ";\n' + 237 | '}\n' + 238 | '# in my line\n') 239 | 240 | def test_multi_semicolon(self): 241 | self.check_formatting('location /a { \n' + 242 | 'allow 127.0.0.1; allow 10.0.0.0/8; deny all; \n' + 243 | '}\n', 244 | 'location /a {\n' + 245 | ' allow 127.0.0.1;\n' + 246 | ' allow 10.0.0.0/8;\n' + 247 | ' deny all;\n' + 248 | '}\n') 249 | 250 | def test_quotes1(self): 251 | self.check_formatting( 252 | '''add_header Alt-Svc 'h3-25=":443"; ma=86400'; add_header Alt-Svc 'h3-29=":443"; ma=86400';''', 253 | '''add_header Alt-Svc 'h3-25=":443"; ma=86400';\n''' + 254 | '''add_header Alt-Svc 'h3-29=":443"; ma=86400';\n''') 255 | 256 | def test_quotes2(self): 257 | self.check_formatting( 258 | '''add_header Alt-Svc "h3-23=':443'; ma=86400"; add_header Alt-Svc 'h3-29=":443"; ma=86400';''', 259 | '''add_header Alt-Svc "h3-23=':443'; ma=86400";\n''' + 260 | '''add_header Alt-Svc 'h3-29=":443"; ma=86400';\n''') 261 | 262 | def test_loading_utf8_file(self): 263 | tmp_file = pathlib.Path(tempfile.mkstemp('utf-8')[1]) 264 | try: 265 | shutil.copy('test-files/umlaut-utf8.conf', tmp_file) 266 | self.fmt.format_file(tmp_file) 267 | # todo perform some tests on result file 268 | finally: 269 | tmp_file.unlink() 270 | 271 | def test_loading_latin1_file(self): 272 | tmp_file = pathlib.Path(tempfile.mkstemp('latin1')[1]) 273 | try: 274 | shutil.copy('test-files/umlaut-latin1.conf', tmp_file) 275 | self.fmt.format_file(tmp_file) 276 | # todo perform some tests on result file 277 | finally: 278 | tmp_file.unlink() 279 | 280 | def test_issue_15(self): 281 | self.check_formatting( 282 | 'section { server_name "~^(?[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\.a\.b\.com$"; }', 283 | 'section {\n server_name "~^(?[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\.a\.b\.com$";\n}\n') 284 | 285 | def test_issue_11(self): 286 | self.check_formatting(" # 3 spaces\n" + 287 | "# 2 spaces\n" + 288 | " # 1 space", 289 | "# 3 spaces\n" + 290 | "# 2 spaces\n" + 291 | "# 1 space\n") 292 | 293 | # everything after # is left as is (except trimming trailing whitespaces) 294 | self.check_formatting(""" #if (!-f $request_filename) { 295 | # rewrite ^/static/?(.*)$ /static.php?resource=$1 last; 296 | #""", 297 | "#if (!-f $request_filename) {\n" + 298 | "# rewrite ^/static/?(.*)$ /static.php?resource=$1 last;\n" + 299 | "#\n") 300 | 301 | def test_issue_20_1(self): 302 | self.check_stays_the_same("# comment 1\n" + 303 | "tag {\n" + 304 | " # comment 2\n" + 305 | " code;\n" + 306 | " # comment 3\n" + 307 | " subtag {\n" + 308 | " code;\n" + 309 | " # comment 4\n" + 310 | " #\n" + 311 | " }\n" + 312 | " # comment 5\n" + 313 | "}\n") 314 | 315 | def test_issue_20_2(self): 316 | self.check_formatting( 317 | "location /nginx_status {\n" + 318 | "# Don't break \n" + 319 | " stub_status on;\n" + 320 | " access_log off;\n" + 321 | " allow 127.0.0.1;\n" + 322 | " deny all;\n" + 323 | "}", 324 | "location /nginx_status {\n" + 325 | " # Don't break\n" + 326 | " stub_status on;\n" + 327 | " access_log off;\n" + 328 | " allow 127.0.0.1;\n" + 329 | " deny all;\n" + 330 | "}\n" 331 | ) 332 | self.check_formatting( 333 | "location /nginx_status {\n" + 334 | "# Don\"t break \n" + 335 | " stub_status on;\n" + 336 | " access_log off;\n" + 337 | " allow 127.0.0.1;\n" + 338 | " deny all;\n" + 339 | "}", 340 | "location /nginx_status {\n" + 341 | " # Don\"t break\n" + 342 | " stub_status on;\n" + 343 | " access_log off;\n" + 344 | " allow 127.0.0.1;\n" + 345 | " deny all;\n" + 346 | "}\n" 347 | ) 348 | 349 | def test_issue_16(self): 350 | self.check_formatting( 351 | "location /example { allow 192.168.0.0/16; deny all; }", 352 | "location /example {\n" 353 | " allow 192.168.0.0/16;\n" 354 | " deny all;\n" 355 | "}\n") 356 | 357 | def test_issue_9(self): 358 | self.check_formatting( 359 | ( 360 | """http {\n""" 361 | """ log_format le_json '{"time":"$time_iso8601", '\n""" 362 | """ '"client_agent":"$client_agent",\n""" 363 | """ '"user_agent":"$http_user_agent"}';\n""" 364 | """}\n""" 365 | ), 366 | ( 367 | """http {\n""" 368 | """ log_format le_json '{"time":"$time_iso8601", '\n""" 369 | """ '"client_agent":"$client_agent",\n""" 370 | """ '"user_agent":"$http_user_agent"}';\n""" 371 | """}\n""" 372 | ), 373 | ) 374 | 375 | def test_custom_indentation(self): 376 | fo = nginxfmt.FormatterOptions() 377 | fo.indentation = 2 378 | fmt2 = nginxfmt.Formatter(fo) 379 | self.assertMultiLineEqual("{\n" 380 | " foo bar;\n" 381 | "}\n", 382 | fmt2.format_string( 383 | " { \n" 384 | " foo bar;\n" 385 | "}\n")) 386 | 387 | def test_crlf_1(self): 388 | self.check_formatting( 389 | "location /example \r\n{\r\nallow 192.168.0.0/16; deny all;\r\n}\n", 390 | "location /example {\n" 391 | " allow 192.168.0.0/16;\n" 392 | " deny all;\n" 393 | "}\n") 394 | 395 | self.check_formatting( 396 | "location /example \r\n{\r\nallow 192.168.0.0/16; deny all;\r\n}\n", 397 | "location /example {\r\n" 398 | " allow 192.168.0.0/16;\r\n" 399 | " deny all;\r\n" 400 | "}\r\n", 401 | formatter=self.fmt_crlf) 402 | 403 | 404 | class TestStandaloneRun(unittest.TestCase): 405 | 406 | def __init__(self, method_name: str = ...) -> None: 407 | super().__init__(method_name) 408 | logging.basicConfig(level=logging.DEBUG) # todo fix logging in debug 409 | 410 | @contextlib.contextmanager 411 | def input_test_file(self, file_name): 412 | tmp_file = pathlib.Path(tempfile.mkstemp('utf-8')[1]) 413 | try: 414 | logging.debug("Input file saved to", tmp_file) 415 | shutil.copy('test-files/' + file_name, tmp_file) 416 | yield str(tmp_file) 417 | # todo perform some tests on result file 418 | finally: 419 | tmp_file.unlink() 420 | 421 | # todo better tests of standalone mode? 422 | def test_print_result(self): 423 | with self.input_test_file('not-formatted-1.conf') as input_file: 424 | nginxfmt._standalone_run(['-p', input_file]) 425 | 426 | def test_print_result_line_endings_windows(self): 427 | f = io.StringIO() 428 | with self.input_test_file('not-formatted-1.conf') as input_file: 429 | with contextlib.redirect_stdout(f): 430 | nginxfmt._standalone_run(['--line-endings=windows', '-p', input_file]) 431 | output = f.getvalue() 432 | self.assertEqual(output.count('\r\n'), 5) 433 | 434 | def test_print_result_line_endings_unix(self): 435 | f = io.StringIO() 436 | with self.input_test_file('not-formatted-1.conf') as input_file: 437 | with contextlib.redirect_stdout(f): 438 | nginxfmt._standalone_run(['--line-endings=unix', '-p', input_file]) 439 | output = f.getvalue() 440 | self.assertEqual(output.count('\r\n'), 0) 441 | self.assertEqual(output.count('\n'), 5) 442 | 443 | 444 | if __name__ == '__main__': 445 | unittest.main() 446 | --------------------------------------------------------------------------------