nginx-lens 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
analyzer/duplicates.py CHANGED
@@ -3,15 +3,16 @@ from typing import List, Dict, Any
3
3
 
4
4
  def find_duplicate_directives(tree) -> List[Dict[str, Any]]:
5
5
  """
6
- Находит дублирующиеся директивы внутри одного блока.
7
- Возвращает список: [{block, directive, count}]
6
+ Находит дублирующиеся директивы внутри одного блока (без вложенности).
7
+ Возвращает список: [{block, directive, count, location}]
8
8
  """
9
9
  analyzer = Analyzer(tree)
10
10
  duplicates = []
11
11
  for d, parent in analyzer.walk():
12
12
  if 'directives' in d:
13
+ # Считаем только прямые дочерние директивы (без вложенных блоков)
13
14
  seen = {}
14
- for sub, _ in analyzer.walk(d['directives'], d):
15
+ for sub in d['directives']:
15
16
  if 'directive' in sub:
16
17
  key = (sub['directive'], str(sub.get('args')))
17
18
  seen[key] = seen.get(key, 0) + 1
@@ -21,6 +22,7 @@ def find_duplicate_directives(tree) -> List[Dict[str, Any]]:
21
22
  'block': d,
22
23
  'directive': directive,
23
24
  'args': args,
24
- 'count': count
25
+ 'count': count,
26
+ 'location': d.get('arg')
25
27
  })
26
28
  return duplicates
commands/analyze.py CHANGED
@@ -13,6 +13,34 @@ from analyzer.dead_locations import find_dead_locations
13
13
  app = typer.Typer()
14
14
  console = Console()
15
15
 
16
+ # Карта советов и критичности для issue_type
17
+ ISSUE_META = {
18
+ 'location_conflict': ("Возможное пересечение location. Это не всегда ошибка: порядок и типы location могут быть корректны. Проверьте, что порядок и типы location соответствуют вашим ожиданиям. Если всё ок — игнорируйте предупреждение.", "medium"),
19
+ 'duplicate_directive': ("Оставьте только одну директиву с нужным значением в этом блоке.", "medium"),
20
+ 'empty_block': ("Удалите или заполните пустой блок.", "low"),
21
+ 'proxy_pass_no_scheme': ("Добавьте http:// или https:// в proxy_pass.", "medium"),
22
+ 'autoindex_on': ("Отключите autoindex, если не требуется публикация файлов.", "medium"),
23
+ 'if_block': ("Избегайте if внутри location, используйте map/try_files.", "medium"),
24
+ 'server_tokens_on': ("Отключите server_tokens для безопасности.", "low"),
25
+ 'ssl_missing': ("Укажите путь к SSL-сертификату/ключу.", "high"),
26
+ 'ssl_protocols_weak': ("Отключите устаревшие протоколы TLS.", "high"),
27
+ 'ssl_ciphers_weak': ("Используйте современные шифры.", "high"),
28
+ 'listen_443_no_ssl': ("Добавьте ssl к listen 443.", "high"),
29
+ 'listen_443_no_http2': ("Добавьте http2 к listen 443 для производительности.", "low"),
30
+ 'no_limit_req_conn': ("Добавьте limit_req/limit_conn для защиты от DDoS.", "medium"),
31
+ 'missing_security_header': ("Добавьте security-заголовок.", "medium"),
32
+ 'deprecated': ("Замените устаревшую директиву.", "medium"),
33
+ 'limit_too_small': ("Увеличьте лимит до рекомендуемого значения.", "medium"),
34
+ 'limit_too_large': ("Уменьшите лимит до разумного значения.", "medium"),
35
+ 'unused_variable': ("Удалите неиспользуемую переменную.", "low"),
36
+ 'listen_servername_conflict': ("Измените listen/server_name для устранения конфликта.", "high"),
37
+ 'rewrite_cycle': ("Проверьте rewrite на циклические правила.", "high"),
38
+ 'rewrite_conflict': ("Проверьте порядок и уникальность rewrite.", "medium"),
39
+ 'rewrite_no_flag': ("Добавьте last/break/redirect/permanent к rewrite.", "low"),
40
+ 'dead_location': ("Удалите неиспользуемый location или используйте его.", "low"),
41
+ }
42
+ SEVERITY_COLOR = {"high": "red", "medium": "orange3", "low": "yellow"}
43
+
16
44
  def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
17
45
  """
18
46
  Анализирует конфигурацию Nginx на типовые проблемы и best practices.
@@ -30,7 +58,14 @@ def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf"
30
58
  Пример:
31
59
  nginx-lens analyze /etc/nginx/nginx.conf
32
60
  """
33
- tree = parse_nginx_config(config_path)
61
+ try:
62
+ tree = parse_nginx_config(config_path)
63
+ except FileNotFoundError:
64
+ console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
65
+ return
66
+ except Exception as e:
67
+ console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
68
+ return
34
69
  conflicts = find_location_conflicts(tree)
35
70
  dups = find_duplicate_directives(tree)
36
71
  empties = find_empty_blocks(tree)
@@ -43,30 +78,58 @@ def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf"
43
78
  table = Table(show_header=True, header_style="bold blue")
44
79
  table.add_column("issue_type")
45
80
  table.add_column("issue_description")
81
+ table.add_column("solution")
82
+
83
+ def add_row(issue_type, desc):
84
+ solution, severity = ISSUE_META.get(issue_type, ("", "low"))
85
+ color = SEVERITY_COLOR.get(severity, "yellow")
86
+ table.add_row(f"[{color}]{issue_type}[/{color}]", desc, f"[{color}]{solution}[/{color}]")
46
87
 
47
88
  for c in conflicts:
48
- table.add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
89
+ add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
49
90
  for d in dups:
50
- table.add_row("duplicate_directive", f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}")
91
+ loc = d.get('location')
92
+ add_row("duplicate_directive", f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}{' location: '+str(loc) if loc else ''}")
51
93
  for e in empties:
52
- table.add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
94
+ add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
53
95
  for w in warnings:
54
- if w['type'] == 'proxy_pass_no_scheme':
55
- table.add_row("proxy_pass_no_scheme", f"proxy_pass без схемы: {w['value']}")
56
- elif w['type'] == 'autoindex_on':
57
- table.add_row("autoindex_on", f"autoindex on в блоке {w['context'].get('block','')}")
58
- elif w['type'] == 'if_block':
59
- table.add_row("if_block", f"директива if внутри блока {w['context'].get('block','')}")
60
- elif w['type'] == 'server_tokens_on':
61
- table.add_row("server_tokens_on", f"server_tokens on в блоке {w['context'].get('block','')}")
96
+ t = w['type']
97
+ if t == 'proxy_pass_no_scheme':
98
+ add_row(t, f"proxy_pass без схемы: {w['value']}")
99
+ elif t == 'autoindex_on':
100
+ add_row(t, f"autoindex on в блоке {w['context'].get('block','')}")
101
+ elif t == 'if_block':
102
+ add_row(t, f"директива if внутри блока {w['context'].get('block','')}")
103
+ elif t == 'server_tokens_on':
104
+ add_row(t, f"server_tokens on в блоке {w['context'].get('block','')}")
105
+ elif t == 'ssl_missing':
106
+ add_row(t, f"{w['directive']} не указан")
107
+ elif t == 'ssl_protocols_weak':
108
+ add_row(t, f"ssl_protocols содержит устаревшие протоколы: {w['value']}")
109
+ elif t == 'ssl_ciphers_weak':
110
+ add_row(t, f"ssl_ciphers содержит слабые шифры: {w['value']}")
111
+ elif t == 'listen_443_no_ssl':
112
+ add_row(t, f"listen без ssl: {w['value']}")
113
+ elif t == 'listen_443_no_http2':
114
+ add_row(t, f"listen 443 без http2: {w['value']}")
115
+ elif t == 'no_limit_req_conn':
116
+ add_row(t, f"server без limit_req/limit_conn")
117
+ elif t == 'missing_security_header':
118
+ add_row(t, f"отсутствует security header: {w['value']}")
119
+ elif t == 'deprecated':
120
+ add_row(t, f"устаревшая директива: {w['directive']} — {w['value']}")
121
+ elif t == 'limit_too_small':
122
+ add_row(t, f"слишком маленькое значение: {w['directive']} = {w['value']}")
123
+ elif t == 'limit_too_large':
124
+ add_row(t, f"слишком большое значение: {w['directive']} = {w['value']}")
62
125
  for v in unused_vars:
63
- table.add_row("unused_variable", v['name'])
126
+ add_row("unused_variable", v['name'])
64
127
  for c in listen_conflicts:
65
- table.add_row("listen_servername_conflict", f"server1: {c['server1'].get('arg','')} server2: {c['server2'].get('arg','')} listen: {','.join(c['listen'])} server_name: {','.join(c['server_name'])}")
128
+ add_row("listen_servername_conflict", f"server1: {c['server1'].get('arg','')} server2: {c['server2'].get('arg','')} listen: {','.join(c['listen'])} server_name: {','.join(c['server_name'])}")
66
129
  for r in rewrite_issues:
67
- table.add_row(r['type'], r['value'])
130
+ add_row(r['type'], r['value'])
68
131
  for l in dead_locations:
69
- table.add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
132
+ add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
70
133
 
71
134
  if table.row_count == 0:
72
135
  console.print("[green]Проблем не найдено[/green]")
commands/cli.py CHANGED
@@ -7,8 +7,8 @@ from commands.diff import diff
7
7
  from commands.route import route
8
8
  from commands.include import include_tree
9
9
  from commands.graph import graph
10
- from commands.logs import app as logs_app
11
- from commands.syntax import app as syntax_app
10
+ from commands.logs import logs
11
+ from commands.syntax import syntax
12
12
 
13
13
  app = typer.Typer(help="nginx-lens — анализ и диагностика конфигураций Nginx")
14
14
  console = Console()
@@ -20,8 +20,8 @@ app.command()(diff)
20
20
  app.command()(route)
21
21
  app.command()(include_tree)
22
22
  app.command()(graph)
23
- app.add_typer(logs_app, name="logs")
24
- app.add_typer(syntax_app, name="syntax")
23
+ app.command()(logs)
24
+ app.command()(syntax)
25
25
 
26
26
  if __name__ == "__main__":
27
27
  app()
commands/diff.py CHANGED
@@ -3,6 +3,7 @@ from rich.console import Console
3
3
  from rich.table import Table
4
4
  from analyzer.diff import diff_trees
5
5
  from parser.nginx_parser import parse_nginx_config
6
+ import difflib
6
7
 
7
8
  app = typer.Typer()
8
9
  console = Console()
@@ -12,28 +13,39 @@ def diff(
12
13
  config2: str = typer.Argument(..., help="Второй nginx.conf")
13
14
  ):
14
15
  """
15
- Сравнивает две конфигурации Nginx и выводит отличия side-by-side.
16
+ Сравнивает две конфигурации Nginx и выводит отличия построчно side-by-side с подсветкой.
16
17
 
17
18
  Пример:
18
19
  nginx-lens diff /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
19
20
  """
20
- tree1 = parse_nginx_config(config1)
21
- tree2 = parse_nginx_config(config2)
22
- diffs = diff_trees(tree1, tree2)
23
- if not diffs:
24
- console.print("[green]Конфигурации идентичны[/green]")
21
+ try:
22
+ with open(config1) as f1, open(config2) as f2:
23
+ lines1 = f1.readlines()
24
+ lines2 = f2.readlines()
25
+ except FileNotFoundError as e:
26
+ console.print(f"[red]Файл {e.filename} не найден. Проверьте путь к конфигу.[/red]")
25
27
  return
28
+ except Exception as e:
29
+ console.print(f"[red]Ошибка при чтении файлов: {e}[/red]")
30
+ return
31
+ maxlen = max(len(lines1), len(lines2))
32
+ # Выравниваем длины
33
+ lines1 += [''] * (maxlen - len(lines1))
34
+ lines2 += [''] * (maxlen - len(lines2))
26
35
  table = Table(show_header=True, header_style="bold blue")
27
- table.add_column("Config 1", style="red")
28
- table.add_column("Config 2", style="green")
29
- for d in diffs:
30
- path = "/".join(d['path'])
31
- if d['type'] == 'added':
32
- table.add_row("", f"+ {path}")
33
- elif d['type'] == 'removed':
34
- table.add_row(f"- {path}", "")
35
- elif d['type'] == 'changed':
36
- v1 = str(d['value1'])
37
- v2 = str(d['value2'])
38
- table.add_row(f"! {path}\n{v1}", f"! {path}\n{v2}")
36
+ table.add_column("", style="dim", width=4)
37
+ table.add_column("Config 1", style="white")
38
+ table.add_column("№", style="dim", width=4)
39
+ table.add_column("Config 2", style="white")
40
+ for i in range(maxlen):
41
+ l1 = lines1[i].rstrip('\n')
42
+ l2 = lines2[i].rstrip('\n')
43
+ n1 = str(i+1) if l1 else ''
44
+ n2 = str(i+1) if l2 else ''
45
+ if l1 == l2:
46
+ table.add_row(n1, l1, n2, l2)
47
+ else:
48
+ style1 = "red" if l1 else "on red"
49
+ style2 = "green" if l2 else "on green"
50
+ table.add_row(f"[bold]{n1}[/bold]" if l1 else n1, f"[{style1}]{l1}[/{style1}]" if l1 or l2 else '', f"[bold]{n2}[/bold]" if l2 else n2, f"[{style2}]{l2}[/{style2}]" if l1 or l2 else '')
39
51
  console.print(table)
commands/graph.py CHANGED
@@ -2,25 +2,118 @@ import typer
2
2
  from rich.console import Console
3
3
  from parser.nginx_parser import parse_nginx_config
4
4
  from exporter.graph import tree_to_dot, tree_to_mermaid
5
+ from rich.text import Text
5
6
 
6
7
  app = typer.Typer()
7
8
  console = Console()
8
9
 
9
10
  def graph(
10
- config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
11
- format: str = typer.Option("dot", help="Формат: dot или mermaid")
11
+ config_path: str = typer.Argument(..., help="Путь к nginx.conf")
12
12
  ):
13
13
  """
14
- Генерирует схему маршрутизации nginx (dot/mermaid).
14
+ Показывает все возможные маршруты nginx в виде цепочек server → location → proxy_pass → upstream → server.
15
15
 
16
16
  Пример:
17
- nginx-lens graph /etc/nginx/nginx.conf --format dot
18
- nginx-lens graph /etc/nginx/nginx.conf --format mermaid
17
+ nginx-lens graph /etc/nginx/nginx.conf
19
18
  """
20
19
  tree = parse_nginx_config(config_path)
21
- if format == "dot":
22
- console.print(tree_to_dot(tree.directives))
23
- elif format == "mermaid":
24
- console.print(tree_to_mermaid(tree.directives))
25
- else:
26
- console.print("[red]Неизвестный формат: выберите dot или mermaid[/red]")
20
+ routes = []
21
+ # Для каждого server/location строим маршрут
22
+ def walk(d, chain, upstreams):
23
+ if d.get('block') == 'server':
24
+ srv = d.get('arg','') or '[no arg]'
25
+ for sub in d.get('directives', []):
26
+ walk(sub, chain + [('server', srv)], upstreams)
27
+ elif d.get('block') == 'location':
28
+ loc = d.get('arg','')
29
+ for sub in d.get('directives', []):
30
+ walk(sub, chain + [('location', loc)], upstreams)
31
+ elif d.get('directive') == 'proxy_pass':
32
+ val = d.get('args','')
33
+ # ищем, есть ли такой upstream
34
+ up_name = None
35
+ if val.startswith('http://') or val.startswith('https://'):
36
+ up = val.split('://',1)[1].split('/',1)[0]
37
+ if up in upstreams:
38
+ up_name = up
39
+ if up_name:
40
+ for srv in upstreams[up_name]:
41
+ routes.append(chain + [('proxy_pass', val), ('upstream', up_name), ('upstream_server', srv)])
42
+ else:
43
+ routes.append(chain + [('proxy_pass', val)])
44
+ elif d.get('upstream'):
45
+ # собираем upstream-ы
46
+ upstreams[d['upstream']] = d.get('servers',[])
47
+ # рекурсивно по всем директивам
48
+ for sub in d.get('directives', []):
49
+ walk(sub, chain, upstreams)
50
+ # Собираем upstream-ы
51
+ upstreams = {}
52
+ for d in tree.directives:
53
+ if d.get('upstream'):
54
+ upstreams[d['upstream']] = d.get('servers',[])
55
+ # Строим маршруты
56
+ for d in tree.directives:
57
+ walk(d, [], upstreams)
58
+ if not routes:
59
+ console.print("[yellow]Не найдено ни одного маршрута[/yellow]")
60
+ return
61
+ # Красивый вывод
62
+ seen = set()
63
+ for route in routes:
64
+ if not route or route[0][0] != 'server':
65
+ continue
66
+ key = tuple(route)
67
+ if key in seen:
68
+ continue
69
+ seen.add(key)
70
+ t = Text()
71
+ # Получаем label для server
72
+ server_val = route[0][1]
73
+ server_block = None
74
+ # Найти сам блок server по arg
75
+ for d in tree.directives:
76
+ if d.get('block') == 'server' and (d.get('arg','') == server_val or (not d.get('arg') and (server_val == '[no arg]' or not server_val))):
77
+ server_block = d
78
+ break
79
+ label = get_server_label(server_block) if server_block else (server_val or 'default')
80
+ t.append(f"[", style="white")
81
+ t.append(f"server: {label}", style="bold blue")
82
+ t.append("]", style="white")
83
+ for i, (typ, val) in enumerate(route[1:]):
84
+ if typ == 'location':
85
+ t.append(f" -> [", style="white")
86
+ t.append(f"location: {val}", style="yellow")
87
+ t.append("]", style="white")
88
+ elif typ == 'proxy_pass':
89
+ t.append(f" -> proxy_pass: {val}", style="green")
90
+ elif typ == 'upstream':
91
+ t.append(f" -> [", style="white")
92
+ t.append(f"upstream: {val}", style="magenta")
93
+ t.append("]", style="white")
94
+ elif typ == 'upstream_server':
95
+ t.append(f" -> [", style="white")
96
+ t.append(f"server: {val}", style="grey50")
97
+ t.append("]", style="white")
98
+ console.print(t)
99
+
100
+ def get_server_label(server_block):
101
+ arg = server_block.get('arg')
102
+ if arg and arg != '[no arg]':
103
+ return arg
104
+ # Ищем server_name
105
+ names = []
106
+ listens = []
107
+ for sub in server_block.get('directives', []):
108
+ if sub.get('directive') == 'server_name':
109
+ names += sub.get('args', '').split()
110
+ if sub.get('directive') == 'listen':
111
+ listens.append(sub.get('args', ''))
112
+ if names:
113
+ return ' '.join(names)
114
+ if listens:
115
+ return ','.join(listens)
116
+ return 'default'
117
+
118
+ if __name__ == "__main__":
119
+ app()
commands/include.py CHANGED
@@ -18,7 +18,14 @@ def include_tree(
18
18
  nginx-lens include-tree /etc/nginx/nginx.conf
19
19
  nginx-lens include-tree /etc/nginx/nginx.conf --directive server_name
20
20
  """
21
- tree = build_include_tree(config_path)
21
+ try:
22
+ tree = build_include_tree(config_path)
23
+ except FileNotFoundError:
24
+ console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
25
+ return
26
+ except Exception as e:
27
+ console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
28
+ return
22
29
  rich_tree = Tree(f"[bold blue]{config_path}[/bold blue]")
23
30
  def _add(node, t):
24
31
  for k, v in t.items():
commands/logs.py CHANGED
@@ -9,7 +9,6 @@ console = Console()
9
9
 
10
10
  log_line_re = re.compile(r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^\"]+" (?P<status>\d{3})')
11
11
 
12
- @app.command()
13
12
  def logs(
14
13
  log_path: str = typer.Argument(..., help="Путь к access.log или error.log"),
15
14
  top: int = typer.Option(10, help="Сколько топ-значений выводить")
commands/route.py CHANGED
@@ -3,12 +3,14 @@ from rich.console import Console
3
3
  from rich.panel import Panel
4
4
  from analyzer.route import find_route
5
5
  from parser.nginx_parser import parse_nginx_config
6
+ import glob
7
+ import os
6
8
 
7
9
  app = typer.Typer()
8
10
  console = Console()
9
11
 
10
12
  def route(
11
- config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
13
+ config_path: str = typer.Argument(None, help="Путь к nginx.conf (если не указан — поиск по всем .conf в /etc/nginx)", show_default=False),
12
14
  url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)")
13
15
  ):
14
16
  """
@@ -16,18 +18,36 @@ def route(
16
18
 
17
19
  Пример:
18
20
  nginx-lens route /etc/nginx/nginx.conf http://example.com/api/v1
21
+ nginx-lens route http://example.com/api/v1
19
22
  """
20
- tree = parse_nginx_config(config_path)
21
- res = find_route(tree, url)
22
- if not res:
23
- console.print(Panel(f"Не найден подходящий server для {url}", style="red"))
24
- return
25
- server = res['server']
26
- location = res['location']
27
- proxy_pass = res['proxy_pass']
28
- text = f"[bold]Server:[/bold] {server.get('arg','') or '[no arg]'}\n"
29
- if location:
30
- text += f"[bold]Location:[/bold] {location.get('arg','')}\n"
31
- if proxy_pass:
32
- text += f"[bold]proxy_pass:[/bold] {proxy_pass}\n"
33
- console.print(Panel(text, title="Route", style="green"))
23
+ configs = []
24
+ if config_path:
25
+ configs = [config_path]
26
+ else:
27
+ configs = glob.glob("/etc/nginx/**/*.conf", recursive=True)
28
+ if not configs:
29
+ console.print(Panel("Не найдено ни одного .conf файла в /etc/nginx", style="red"))
30
+ return
31
+ for conf in configs:
32
+ try:
33
+ tree = parse_nginx_config(conf)
34
+ except FileNotFoundError:
35
+ console.print(f"[red]Файл {conf} не найден. Проверьте путь к конфигу.[/red]")
36
+ continue
37
+ except Exception as e:
38
+ console.print(f"[red]Ошибка при разборе {conf}: {e}[/red]")
39
+ continue
40
+ res = find_route(tree, url)
41
+ if res:
42
+ server = res['server']
43
+ location = res['location']
44
+ proxy_pass = res['proxy_pass']
45
+ text = f"[bold]Config:[/bold] {conf}\n"
46
+ text += f"[bold]Server:[/bold] {server.get('arg','') or '[no arg]'}\n"
47
+ if location:
48
+ text += f"[bold]Location:[/bold] {location.get('arg','')}\n"
49
+ if proxy_pass:
50
+ text += f"[bold]proxy_pass:[/bold] {proxy_pass}\n"
51
+ console.print(Panel(text, title="Route", style="green"))
52
+ return
53
+ console.print(Panel(f"Ни один ваш конфиг в /etc/nginx не обрабатывает этот URL", style="red"))
commands/syntax.py CHANGED
@@ -8,11 +8,10 @@ import re
8
8
  app = typer.Typer(help="Проверка синтаксиса nginx-конфига через nginx -t с подсветкой ошибок.")
9
9
  console = Console()
10
10
 
11
- ERROR_RE = re.compile(r'in (.+?):(\d+)')
11
+ ERRORS_RE = re.compile(r'in (.+?):(\d+)(?:\s*\n)?(.+?)(?=\nin |$)', re.DOTALL)
12
12
 
13
- @app.command()
14
13
  def syntax(
15
- config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
14
+ config_path: str = typer.Option(None, "-c", "--config", help="Путь к кастомному nginx.conf"),
16
15
  nginx_path: str = typer.Option("nginx", help="Путь к бинарю nginx (по умолчанию 'nginx')")
17
16
  ):
18
17
  """
@@ -21,26 +20,46 @@ def syntax(
21
20
  В случае ошибки показывает место в виде таблицы с контекстом.
22
21
 
23
22
  Пример:
24
- nginx-lens syntax /etc/nginx/nginx.conf
25
- nginx-lens syntax /etc/nginx/nginx.conf --nginx-path /usr/local/sbin/nginx
23
+ nginx-lens syntax -c ./mynginx.conf
24
+ nginx-lens syntax
26
25
  """
26
+ if not config_path:
27
+ candidates = [
28
+ "/etc/nginx/nginx.conf",
29
+ "/usr/local/etc/nginx/nginx.conf",
30
+ "./nginx.conf"
31
+ ]
32
+ config_path = next((p for p in candidates if os.path.isfile(p)), None)
33
+ if not config_path:
34
+ console.print("[red]Не удалось найти nginx.conf. Укажите путь через -c.[/red]")
35
+ return
36
+ if not os.path.isfile(config_path):
37
+ console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
38
+ return
27
39
  cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
28
40
  try:
29
41
  result = subprocess.run(cmd, capture_output=True, text=True, check=False)
30
42
  if result.returncode == 0:
31
43
  console.print("[green]Синтаксис nginx-конфига корректен[/green]")
44
+ return
32
45
  else:
33
46
  console.print("[red]Ошибка синтаксиса![/red]")
34
- console.print(result.stdout)
35
- console.print(result.stderr)
36
- # Парсим ошибку
47
+ console.print(result.stdout)
48
+ console.print(result.stderr)
49
+ # Парсим все ошибки
37
50
  err = result.stderr or result.stdout
38
- m = ERROR_RE.search(err)
39
- if m:
40
- file, line = m.group(1), int(m.group(2))
41
- msg = err.strip().split('\n')[-1]
51
+ errors = list(ERRORS_RE.finditer(err))
52
+ if not errors:
53
+ console.print("[red]Не удалось определить файл и строку ошибки[/red]")
54
+ return
55
+ table = Table(title="Ошибки синтаксиса", show_header=True, header_style="bold red")
56
+ table.add_column("config_file")
57
+ table.add_column("issue_message")
58
+ table.add_column("context")
59
+ for m in errors:
60
+ file, line, msg = m.group(1), int(m.group(2)), m.group(3).strip().split('\n')[0]
42
61
  # Читаем контекст
43
- context = []
62
+ context_lines = []
44
63
  try:
45
64
  with open(file) as f:
46
65
  lines = f.readlines()
@@ -48,16 +67,10 @@ def syntax(
48
67
  end = min(len(lines), line+2)
49
68
  for i in range(start, end):
50
69
  mark = "->" if i+1 == line else " "
51
- context.append((str(i+1), mark, lines[i].rstrip()))
70
+ context_lines.append(f"{mark} {lines[i].rstrip()}")
52
71
  except Exception:
53
- context = []
54
- table = Table(title="Ошибка синтаксиса", show_header=True, header_style="bold red")
55
- table.add_column("File")
56
- table.add_column("Line")
57
- table.add_column("Message")
58
- table.add_column("Context")
59
- for ln, mark, code in context:
60
- table.add_row(file, ln, msg if mark == "->" else "", f"{mark} {code}")
61
- console.print(table)
72
+ context_lines = []
73
+ table.add_row(file, msg, '\n'.join(context_lines))
74
+ console.print(table)
62
75
  except FileNotFoundError:
63
76
  console.print(f"[red]Не найден бинарь nginx: {nginx_path}[/red]")
commands/tree.py CHANGED
@@ -34,7 +34,14 @@ def tree(
34
34
  nginx-lens tree /etc/nginx/nginx.conf --markdown
35
35
  nginx-lens tree /etc/nginx/nginx.conf --html
36
36
  """
37
- tree_obj = parse_nginx_config(config_path)
37
+ try:
38
+ tree_obj = parse_nginx_config(config_path)
39
+ except FileNotFoundError:
40
+ console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
41
+ return
42
+ except Exception as e:
43
+ console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
44
+ return
38
45
  root = RichTree(f"[bold blue]nginx.conf[/bold blue]")
39
46
  _build_tree(tree_obj.directives, root)
40
47
  if markdown:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -3,7 +3,7 @@ analyzer/base.py,sha256=oGKg78BfMVmuzYafc08oq9p31-jEgYolGjLkUcIdkN8,607
3
3
  analyzer/conflicts.py,sha256=NSNZc8e2x51K41dflSUvuwlDq-rzBXU5ITi6WfxFbfU,2796
4
4
  analyzer/dead_locations.py,sha256=uvMu5qBGTVi0Nn960x3WpRvTljGbQuVFivU4nfe36oY,1435
5
5
  analyzer/diff.py,sha256=idvXnoLzBVUYgKi_s3uDu0v2GNMV3B8aDqTROXcdQdo,1749
6
- analyzer/duplicates.py,sha256=VUjbM19Y_Wwty8GEOu_7nNzOH6mr071p_z1MAkp1kBA,1012
6
+ analyzer/duplicates.py,sha256=jpy_6k-BzWxaXFt2Wb3rlulIXUEzbFe9xYRm7rWR50U,1215
7
7
  analyzer/empty_blocks.py,sha256=7Zu4-5I5PS3bjhH0Ppq1CvM7rMTeRIc4fHx5n5vkMIw,517
8
8
  analyzer/include.py,sha256=FhKR4VsogLknykjLD2N8jX9OtwxZcWik5oPpvp-_luE,2465
9
9
  analyzer/rewrite.py,sha256=-jSLLG1jqmGU-dXWvU6NHCW6muB8Lfro6fXX1tDCHCQ,1834
@@ -11,16 +11,16 @@ analyzer/route.py,sha256=2xxQooQEsfn10tzGCZUoP32T0OnTMnPB6qRgBR6not8,2345
11
11
  analyzer/unused.py,sha256=Ixzv0bPsw9IafblVwLiAOgugdg2dGu1MJDtuoqzPZiY,1066
12
12
  analyzer/warnings.py,sha256=zC36QMvegA2eQPvZ-P1eysrX_kXHx5A1MUKHKKNvG5c,5784
13
13
  commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- commands/analyze.py,sha256=ZpChHk6QXut3S1PvslcX_4IeqdaaXtUXX-ogkB-a6GQ,3618
15
- commands/cli.py,sha256=0uzz71nMXTGlY1odZkEGwtVOjf7uq7762qRLXQaQURo,780
16
- commands/diff.py,sha256=VeZsUu1BYrDubDFrmM5xC4DUWZvWUjFRHfddXm3-I3c,1373
17
- commands/graph.py,sha256=xEDMAOZ9Z5gaOGqLhSIGeRlDvRfSborWwUVjk-XLCsk,930
14
+ commands/analyze.py,sha256=W6begSgXNjgKJGoGeguR3WKgHPLkClWXxxpDcqvsJdc,8343
15
+ commands/cli.py,sha256=9HwDJ-po5al0ceb4Wkyw5F2wzqxbJTo0CbHQ2AQ8obo,722
16
+ commands/diff.py,sha256=C7gRIWh6DNWHzjiQBPVTn-rZ40m2KCY75Zd6Q4URJIE,2076
17
+ commands/graph.py,sha256=fvYDUvj_BXdIAm8eg52v5VxhkZijb2YDkCw7D21ZK-M,4650
18
18
  commands/health.py,sha256=d2bBui0qauQtO4Ll9cjniKR1Y5dYJBQzG9CECDnsUQ4,1365
19
- commands/include.py,sha256=zPqJpbbSU_6S3L4ntFncPmyFWba8smvdCRog_SFuAFI,1907
20
- commands/logs.py,sha256=NkBJAfKGXvZQDEjHwqdfYikKFMyCrEtjfDeD_MJ9ycg,3435
21
- commands/route.py,sha256=Rnm-zjIbos0NJjUZ0gRs27dCmUfF1tKWKVzdcFcl9hA,1224
22
- commands/syntax.py,sha256=TM9uOZ7MsxF78iJlajybWzouJgH2YeAbUXo8i3GxPVo,2624
23
- commands/tree.py,sha256=NEhNU66_e0JCsD4xh4315TM-xwo8NkPwc00lZ4saPzE,1844
19
+ commands/include.py,sha256=5PTYG5C00-AlWfIgpQXLq9E7C9yTFSv7HrZkM5ogDps,2224
20
+ commands/logs.py,sha256=8tnGsgNy_B97S3O0D_6bvOVfNvAyqeUNotlDOdjltgw,3420
21
+ commands/route.py,sha256=H_xMk9KoqwesutzT9ewiTsakfdjmu01kka9ZQc14faY,2222
22
+ commands/syntax.py,sha256=gABzFvml92bHqYa6YdRx78bmrtd9v2ORT_Hz-gfQu1U,3336
23
+ commands/tree.py,sha256=mDfx0Aeg1EDQSYQoJ2nJIkSd_uP7ZR7pEqy7Cw3clQ0,2161
24
24
  exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  exporter/graph.py,sha256=WYUrqUgCaK6KihgxAcRHaQn4oMo6b7ybC8yb_36ZIsA,3995
26
26
  exporter/html.py,sha256=uquEM-WvBt2aV9GshgaI3UVhYd8sD0QQ-OmuNtvYUdU,798
@@ -29,8 +29,8 @@ parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  parser/nginx_parser.py,sha256=JqZ3clNy4Nf-bmbsx_rJUL7EgRoB79b87eEu_isMeqg,3577
30
30
  upstream_checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  upstream_checker/checker.py,sha256=9-6CMUTN7gXUACP8EwX722QogfujZyV-WWWUeM3a79k,455
32
- nginx_lens-0.1.4.dist-info/METADATA,sha256=MkC1F3wihoxkMKrTJqH0DMrgrZmtTg4L8VqbY85J0jg,476
33
- nginx_lens-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- nginx_lens-0.1.4.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
35
- nginx_lens-0.1.4.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
36
- nginx_lens-0.1.4.dist-info/RECORD,,
32
+ nginx_lens-0.1.6.dist-info/METADATA,sha256=FhO8QQOa9YfJzMUtNq313YcaNQAhHr2UEMNHXDIJkBU,476
33
+ nginx_lens-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ nginx_lens-0.1.6.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
35
+ nginx_lens-0.1.6.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
36
+ nginx_lens-0.1.6.dist-info/RECORD,,