44Update the rustc manpage "-O" description to match `rustc --help`.
55
66Usage (dry-run by default):
7- ./src/tools/update-rustc-man-opt-level.py --man-file src/doc/man/rustc.1
7+ ./src/tools/update-rustc-man-opt-level.py \
8+ --man-file src/doc/man/rustc.1
89
910Apply changes (creates a timestamped backup):
10- ./src/tools/update-rustc-man-opt-level.py --man-file src/doc/man/rustc.1 --apply
11+ ./src/tools/update-rustc-man-opt-level.py \
12+ --man-file src/doc/man/rustc.1 --apply
1113
1214Force a level instead of querying rustc:
13- ./src/tools/update-rustc-man-opt-level.py --man-file ... --expected-level 3 --apply
15+ ./src/tools/update-rustc-man-opt-level.py \
16+ --man-file ... --expected-level 3 --apply
1417"""
18+
1519from __future__ import annotations
1620
1721import argparse
2226import sys
2327import re
2428from pathlib import Path
25- from typing import Tuple
2629
2730DEFAULT_RUSTC = "rustc"
2831
@@ -45,105 +48,172 @@ def colorize(line: str, color: str, enabled: bool) -> str:
4548def get_rustc_opt_level (rustc_cmd : str = DEFAULT_RUSTC ) -> int :
4649 """Query `rustc --help` and parse the opt-level mapped to -O."""
4750 try :
48- proc = subprocess .run ([rustc_cmd , "--help" ], capture_output = True , text = True , check = True )
51+ proc = subprocess .run (
52+ [rustc_cmd , "--help" ], capture_output = True , text = True , check = True
53+ )
4954 except FileNotFoundError :
5055 raise RuntimeError (f"rustc not found at '{ rustc_cmd } '" )
5156 except subprocess .CalledProcessError as e :
5257 stderr = (e .stderr or "" ).strip ()
5358 raise RuntimeError (f"rustc --help failed: { stderr or e } " ) from e
5459
5560 help_text = (proc .stdout or "" ) + "\n " + (proc .stderr or "" )
56- m = re .search (r'-O[^\n]*opt(?:\\-)?level\s*=\s*(\d+)' , help_text , flags = re .IGNORECASE )
61+ # Fixed: removed unnecessary \\ escape before the hyphen
62+ m = re .search (r"-O[^\n]*opt(?:-)?level\s*=\s*(\d+)" , help_text , flags = re .IGNORECASE )
5763 if not m :
58- m2 = re .search (r'Equivalent to\s+-C\s+opt(?:-)?level\s*=\s*(\d+)' , help_text , flags = re .IGNORECASE )
64+ m2 = re .search (
65+ r"Equivalent to\s+-C\s+opt(?:-)?level\s*=\s*(\d+)" ,
66+ help_text ,
67+ flags = re .IGNORECASE ,
68+ )
5969 if not m2 :
60- raise RuntimeError ("Could not find '-O' opt-level mapping in rustc --help output" )
70+ raise RuntimeError (
71+ "Could not find '-O' opt-level mapping in rustc --help output"
72+ )
6173 return int (m2 .group (1 ))
6274 return int (m .group (1 ))
6375
6476
65- def find_and_replace_manpage_content (content : str , new_level : int ) -> Tuple [str , int ]:
66- """
67- Replace opt-level numbers in 'Equivalent to ... opt-level=N.' sentences tied to -O.
77+ def find_and_replace_manpage_content (
78+ content : str , new_level : int
79+ ) -> tuple [str , int , bool ]:
80+ r"""
81+ Replace opt-level numbers in 'Equivalent to ... opt-level=N.'
82+ sentences tied to -O.
6883
6984 Conservative heuristic:
70- - Locate sentences starting with 'Equivalent to' up to the next period.
71- - Ensure the sentence mentions opt-level (accepting escaped '\-').
72- - Confirm a -O header appears within a lookback window before the sentence.
85+ - Locate sentences starting with 'Equivalent to' up to the
86+ next period.
87+ - Ensure the sentence mentions opt-level (accepting escaped
88+ '\-').
89+ - Confirm a -O header appears within a lookback window before
90+ the sentence.
91+
92+ Returns:
93+ tuple of (new_content, replacements_made, found_any_patterns)
7394 """
7495 replacements = 0
96+ found_patterns = False
7597 out_parts = []
7698 last_index = 0
7799
78- sentence_pattern = re .compile (r'Equivalent to([^\n\.]{0,800}?)\.' , flags = re .IGNORECASE )
100+ # More conservative limit of 200 chars instead of 800
101+ sentence_pattern = re .compile (
102+ r"Equivalent to([^\n\.]{0,200}?)\." , flags = re .IGNORECASE
103+ )
79104
80105 for m in sentence_pattern .finditer (content ):
81106 start , end = m .span ()
82107 sentence = m .group (0 )
83108
84- if not re .search (r' opt(?:\\-)?level' , sentence , flags = re .IGNORECASE ):
109+ if not re .search (r" opt(?:\\-)?level" , sentence , flags = re .IGNORECASE ):
85110 continue
86111
87- num_match = re .search (r' (\d+)' , sentence )
112+ num_match = re .search (r" (\d+)" , sentence )
88113 if not num_match :
89114 continue
90115 old_level = int (num_match .group (1 ))
91- if old_level == new_level :
92- continue
93116
94117 window_start = max (0 , start - 1200 )
95118 window = content [window_start :start ]
96119
97- if not (
98- re .search (r'(^|\n)\s*-O\b' , window )
99- or re .search (r'\\fB\\-?O\\fR' , window )
100- or re .search (r'\\-O\b' , window )
101- or re .search (r'\.B\s+-O\b' , window )
102- or re .search (r'\\fB-?O\\fP' , window )
120+ # Use any() for better readability
121+ if not any (
122+ [
123+ re .search (r"(^|\n)\s*-O\b" , window ),
124+ re .search (r"\\fB\\-?O\\fR" , window ),
125+ re .search (r"\\-O\b" , window ),
126+ re .search (r"\.B\s+-O\b" , window ),
127+ re .search (r"\\fB-?O\\fP" , window ),
128+ ]
103129 ):
104130 continue
105131
106- new_sentence = re .sub (r'(\d+)' , str (new_level ), sentence , count = 1 )
132+ # We found at least one -O entry with opt-level
133+ found_patterns = True
134+
135+ if old_level == new_level :
136+ continue
137+
138+ # More robust: replace only the number after opt-level=
139+ new_sentence = re .sub (
140+ r"(opt(?:\\-)?level\s*=\s*)\d+" ,
141+ rf"\g<1>{ new_level } " ,
142+ sentence ,
143+ count = 1 ,
144+ )
107145 out_parts .append (content [last_index :start ])
108146 out_parts .append (new_sentence )
109147 last_index = end
110148 replacements += 1
111149
112150 out_parts .append (content [last_index :])
113- return "" .join (out_parts ), replacements
151+ return "" .join (out_parts ), replacements , found_patterns
114152
115153
116154def show_colored_diff (old : str , new : str , filename : str , color : bool ) -> None :
117- old_lines = old .splitlines (keepends = True )
118- new_lines = new .splitlines (keepends = True )
119- diff_iter = difflib .unified_diff (old_lines , new_lines , fromfile = filename , tofile = filename + " (updated)" , lineterm = "" )
155+ # Use keepends=False and lineterm="\n" for consistency
156+ old_lines = old .splitlines (keepends = False )
157+ new_lines = new .splitlines (keepends = False )
158+ diff_iter = difflib .unified_diff (
159+ old_lines ,
160+ new_lines ,
161+ fromfile = filename ,
162+ tofile = filename + " (updated)" ,
163+ lineterm = "\n " ,
164+ )
120165 for line in diff_iter :
121166 if line .startswith ("---" ) or line .startswith ("+++" ):
122- print (colorize (line . rstrip ( " \n " ) , "bold" , color ))
167+ print (colorize (line , "bold" , color ))
123168 elif line .startswith ("@@" ):
124- print (colorize (line . rstrip ( " \n " ) , "yellow" , color ))
169+ print (colorize (line , "yellow" , color ))
125170 elif line .startswith ("+" ):
126- # avoid coloring the file header lines that also start with +++
127- print (colorize (line .rstrip ("\n " ), "green" , color ))
171+ print (colorize (line , "green" , color ))
128172 elif line .startswith ("-" ):
129- print (colorize (line . rstrip ( " \n " ) , "red" , color ))
173+ print (colorize (line , "red" , color ))
130174 else :
131- print (line . rstrip ( " \n " ) )
175+ print (line )
132176
133177
134178def backup_file (path : Path ) -> Path :
135- ts = datetime .datetime .now ().strftime ("%Y%m%dT%H%M%S" )
179+ # Added microseconds to avoid collision if run multiple times
180+ # per second
181+ ts = datetime .datetime .now ().strftime ("%Y%m%dT%H%M%S%f" )
136182 backup = path .with_name (path .name + f".bak.{ ts } " )
137183 shutil .copy2 (path , backup )
138184 return backup
139185
140186
141187def parse_args () -> argparse .Namespace :
142- p = argparse .ArgumentParser (description = "Update rustc man page -O opt-level to match rustc --help" )
143- p .add_argument ("--man-file" , "-m" , required = True , help = "Path to rustc man page file to update (e.g. src/doc/man/rustc.1)" )
144- p .add_argument ("--rustc-cmd" , default = DEFAULT_RUSTC , help = "rustc binary to query (default: rustc)" )
145- p .add_argument ("--expected-level" , "-e" , type = int , help = "Use this level instead of querying rustc" )
146- p .add_argument ("--apply" , action = "store_true" , help = "Write changes to the man file (creates a backup). Without this flag runs in dry-run mode and prints a diff." )
188+ p = argparse .ArgumentParser (
189+ description = ("Update rustc man page -O opt-level to match rustc --help" )
190+ )
191+ p .add_argument (
192+ "--man-file" ,
193+ "-m" ,
194+ required = True ,
195+ help = ("Path to rustc man page file to update (e.g. src/doc/man/rustc.1)" ),
196+ )
197+ p .add_argument (
198+ "--rustc-cmd" ,
199+ default = DEFAULT_RUSTC ,
200+ help = "rustc binary to query (default: rustc)" ,
201+ )
202+ p .add_argument (
203+ "--expected-level" ,
204+ "-e" ,
205+ type = int ,
206+ help = "Use this level instead of querying rustc" ,
207+ )
208+ p .add_argument (
209+ "--apply" ,
210+ action = "store_true" ,
211+ help = (
212+ "Write changes to the man file (creates a backup). "
213+ "Without this flag runs in dry-run mode and prints "
214+ "a diff."
215+ ),
216+ )
147217 p .add_argument ("--no-color" , action = "store_true" , help = "Disable colored output" )
148218 return p .parse_args ()
149219
@@ -158,7 +228,11 @@ def main() -> int:
158228 return 2
159229
160230 try :
161- new_level = args .expected_level if args .expected_level is not None else get_rustc_opt_level (args .rustc_cmd )
231+ new_level = (
232+ args .expected_level
233+ if args .expected_level is not None
234+ else get_rustc_opt_level (args .rustc_cmd )
235+ )
162236 except RuntimeError as e :
163237 print (f"Error determining rustc opt-level: { e } " , file = sys .stderr )
164238 return 3
@@ -169,10 +243,20 @@ def main() -> int:
169243 print (f"Error reading man file { man_path } : { e } " , file = sys .stderr )
170244 return 4
171245
172- new_content , replacements = find_and_replace_manpage_content (content , new_level )
246+ new_content , replacements , found_patterns = find_and_replace_manpage_content (
247+ content , new_level
248+ )
173249
174250 if replacements == 0 :
175- print ("No replacements necessary (either already up-to-date or -O entry not found near an 'Equivalent to -C opt-level=' line)." )
251+ if found_patterns :
252+ print (f"✓ Manpage is already up-to-date (opt-level={ new_level } )." )
253+ else :
254+ print (
255+ "Warning: Could not find -O entry with "
256+ "'Equivalent to -C opt-level=' pattern in manpage." ,
257+ file = sys .stderr ,
258+ )
259+ return 6
176260 return 0
177261
178262 header = f"Found { replacements } replacement(s). Proposed changes:"
@@ -183,7 +267,8 @@ def main() -> int:
183267 try :
184268 backup = backup_file (man_path )
185269 man_path .write_text (new_content , encoding = "utf-8" )
186- print (colorize (f"\n Applied changes to { man_path } . Backup saved to { backup } " , "green" , color ))
270+ msg = f"\n Applied changes to { man_path } . Backup saved to { backup } "
271+ print (colorize (msg , "green" , color ))
187272 except Exception as e :
188273 print (f"Error writing updated man file: { e } " , file = sys .stderr )
189274 return 5
0 commit comments