66
77import sys
88import re
9+ import argparse
910from pathlib import Path
1011from typing import List , Tuple
1112
13+ # Validation thresholds and limits
14+ MINIMUM_BASH_EXAMPLES = 2
15+ MINIMUM_EXPECTED_OUTPUT_SECTIONS = 1
16+ MINIMUM_TROUBLESHOOTING_ISSUES = 2
17+ SUMMARY_SEPARATOR_LENGTH = 60
18+
19+ # Section search offsets
20+ SECTION_SEARCH_OFFSET = 1
21+ SECTION_NOT_FOUND = - 1
22+
23+ # Exit codes
24+ EXIT_SUCCESS = 0
25+ EXIT_ERROR = 1
26+
1227# Required sections in order
1328REQUIRED_SECTIONS = [
1429 ("# " , "Title with plugin name" ),
@@ -86,11 +101,11 @@ def validate_parameter_tables(content: str) -> List[str]:
86101 # Check for required parameters section
87102 if '### Required parameters' in content :
88103 section_start = content .index ('### Required parameters' )
89- section_end = content .find ('\n ###' , section_start + 1 )
90- if section_end == - 1 :
91- section_end = content .find ('\n ##' , section_start + 1 )
104+ section_end = content .find ('\n ###' , section_start + SECTION_SEARCH_OFFSET )
105+ if section_end == SECTION_NOT_FOUND :
106+ section_end = content .find ('\n ##' , section_start + SECTION_SEARCH_OFFSET )
92107
93- section_content = content [section_start :section_end ] if section_end != - 1 else content [section_start :]
108+ section_content = content [section_start :section_end ] if section_end != SECTION_NOT_FOUND else content [section_start :]
94109
95110 if 'required' not in section_content .lower ():
96111 errors .append ("Required parameters section should indicate which parameters are required" )
@@ -103,8 +118,8 @@ def validate_examples(content: str) -> List[str]:
103118
104119 # Check for bash code examples
105120 bash_examples = re .findall (r'```bash(.*?)```' , content , re .DOTALL )
106- if len (bash_examples ) < 2 :
107- errors .append (f"Should have at least 2 bash code examples (found { len (bash_examples )} )" )
121+ if len (bash_examples ) < MINIMUM_BASH_EXAMPLES :
122+ errors .append (f"Should have at least { MINIMUM_BASH_EXAMPLES } bash code examples (found { len (bash_examples )} )" )
108123
109124 # Check for influxdb3 commands in examples
110125 has_create_trigger = any ('influxdb3 create trigger' in ex for ex in bash_examples )
@@ -120,8 +135,8 @@ def validate_examples(content: str) -> List[str]:
120135
121136 # Check for expected output
122137 expected_output_count = content .count ('### Expected output' ) + content .count ('**Expected output' )
123- if expected_output_count < 1 :
124- errors .append ("Should include at least one 'Expected output' section in examples" )
138+ if expected_output_count < MINIMUM_EXPECTED_OUTPUT_SECTIONS :
139+ errors .append (f "Should include at least { MINIMUM_EXPECTED_OUTPUT_SECTIONS } 'Expected output' section in examples" )
125140
126141 return errors
127142
@@ -153,8 +168,8 @@ def validate_troubleshooting(content: str) -> List[str]:
153168
154169 if '## Troubleshooting' in content :
155170 section_start = content .index ('## Troubleshooting' )
156- section_end = content .find ('\n ##' , section_start + 1 )
157- section_content = content [section_start :section_end ] if section_end != - 1 else content [section_start :]
171+ section_end = content .find ('\n ##' , section_start + SECTION_SEARCH_OFFSET )
172+ section_content = content [section_start :section_end ] if section_end != SECTION_NOT_FOUND else content [section_start :]
158173
159174 # Check for common subsections
160175 if '### Common issues' not in section_content :
@@ -164,8 +179,8 @@ def validate_troubleshooting(content: str) -> List[str]:
164179 issue_count = section_content .count ('#### Issue:' ) + section_content .count ('**Issue:' )
165180 solution_count = section_content .count ('**Solution:' ) + section_content .count ('Solution:' )
166181
167- if issue_count < 2 :
168- errors .append ("Troubleshooting should include at least 2 documented issues" )
182+ if issue_count < MINIMUM_TROUBLESHOOTING_ISSUES :
183+ errors .append (f "Troubleshooting should include at least { MINIMUM_TROUBLESHOOTING_ISSUES } documented issues" )
169184 if issue_count > solution_count :
170185 errors .append ("Each troubleshooting issue should have a corresponding solution" )
171186
@@ -177,8 +192,8 @@ def validate_code_overview(content: str) -> List[str]:
177192
178193 if '## Code overview' in content :
179194 section_start = content .index ('## Code overview' )
180- section_end = content .find ('\n ##' , section_start + 1 )
181- section_content = content [section_start :section_end ] if section_end != - 1 else content [section_start :]
195+ section_end = content .find ('\n ##' , section_start + SECTION_SEARCH_OFFSET )
196+ section_content = content [section_start :section_end ] if section_end != SECTION_NOT_FOUND else content [section_start :]
182197
183198 # Check for required subsections
184199 if '### Files' not in section_content :
@@ -249,22 +264,123 @@ def validate_readme(readme_path: Path) -> Tuple[List[str], List[str]]:
249264
250265 return errors , warnings
251266
267+ def parse_arguments ():
268+ """Parse command line arguments."""
269+ parser = argparse .ArgumentParser (
270+ description = 'Validates plugin README files against the standard template.' ,
271+ formatter_class = argparse .RawDescriptionHelpFormatter ,
272+ epilog = '''
273+ Examples:
274+ python scripts/validate_readme.py # Validate all plugins
275+ python scripts/validate_readme.py --plugins basic_transformation,downsampler
276+ python scripts/validate_readme.py --list # List available plugins
277+ python scripts/validate_readme.py --quiet # Show only errors
278+
279+ Validation Rules:
280+ - Checks for required sections in correct order
281+ - Validates emoji metadata format
282+ - Ensures parameter tables are properly formatted
283+ - Verifies code examples include required commands
284+ - Validates troubleshooting content structure
285+ '''
286+ )
287+
288+ parser .add_argument (
289+ '--plugins' ,
290+ type = str ,
291+ help = 'Comma-separated list of specific plugins to validate (e.g., "basic_transformation,downsampler")'
292+ )
293+
294+ parser .add_argument (
295+ '--list' ,
296+ action = 'store_true' ,
297+ help = 'List all available plugins and exit'
298+ )
299+
300+ parser .add_argument (
301+ '--quiet' ,
302+ action = 'store_true' ,
303+ help = 'Show only errors, suppress warnings and success messages'
304+ )
305+
306+ parser .add_argument (
307+ '--errors-only' ,
308+ action = 'store_true' ,
309+ help = 'Exit with success code even if warnings are found (only fail on errors)'
310+ )
311+
312+ return parser .parse_args ()
313+
314+ def list_available_plugins ():
315+ """List all available plugins and exit."""
316+ influxdata_dir = Path ('influxdata' )
317+
318+ if not influxdata_dir .exists ():
319+ print ("❌ Error: 'influxdata' directory not found. Run this script from the influxdb3_plugins root directory." )
320+ sys .exit (EXIT_ERROR )
321+
322+ readme_files = list (influxdata_dir .glob ('*/README.md' ))
323+
324+ if not readme_files :
325+ print ("❌ No plugins found in influxdata/ subdirectories" )
326+ sys .exit (EXIT_ERROR )
327+
328+ print (f"Available plugins ({ len (readme_files )} found):" )
329+ for readme_path in sorted (readme_files ):
330+ plugin_name = readme_path .parent .name
331+ print (f" - { plugin_name } " )
332+
333+ sys .exit (EXIT_SUCCESS )
334+
335+ def filter_plugins_by_name (readme_files : List [Path ], plugin_names : str ) -> List [Path ]:
336+ """Filter README files by specified plugin names."""
337+ requested_plugins = [name .strip () for name in plugin_names .split (',' )]
338+ filtered_files = []
339+
340+ for readme_path in readme_files :
341+ plugin_name = readme_path .parent .name
342+ if plugin_name in requested_plugins :
343+ filtered_files .append (readme_path )
344+ requested_plugins .remove (plugin_name )
345+
346+ # Report any plugins that weren't found
347+ if requested_plugins :
348+ print (f"⚠️ Warning: The following plugins were not found: { ', ' .join (requested_plugins )} " )
349+ available_plugins = [f .parent .name for f in readme_files ]
350+ print (f"Available plugins: { ', ' .join (sorted (available_plugins ))} " )
351+
352+ return filtered_files
353+
252354def main ():
253355 """Main validation function."""
356+ args = parse_arguments ()
357+
358+ # Handle list option
359+ if args .list :
360+ list_available_plugins ()
361+
254362 # Find all plugin READMEs
255363 influxdata_dir = Path ('influxdata' )
256364
257365 if not influxdata_dir .exists ():
258366 print ("❌ Error: 'influxdata' directory not found. Run this script from the influxdb3_plugins root directory." )
259- sys .exit (1 )
367+ sys .exit (EXIT_ERROR )
260368
261369 readme_files = list (influxdata_dir .glob ('*/README.md' ))
262370
263371 if not readme_files :
264372 print ("❌ No README files found in influxdata/ subdirectories" )
265- sys .exit (1 )
373+ sys .exit (EXIT_ERROR )
266374
267- print (f"Validating { len (readme_files )} plugin README files...\n " )
375+ # Filter by specific plugins if requested
376+ if args .plugins :
377+ readme_files = filter_plugins_by_name (readme_files , args .plugins )
378+ if not readme_files :
379+ print ("❌ No matching plugins found" )
380+ sys .exit (EXIT_ERROR )
381+
382+ if not args .quiet :
383+ print (f"Validating { len (readme_files )} plugin README files...\n " )
268384
269385 all_valid = True
270386 error_count = 0
@@ -278,24 +394,47 @@ def main():
278394 error_count += len (errors )
279395 warning_count += len (warnings )
280396
281- print (format_validation_result (readme_path , errors , warnings ))
397+ result = format_validation_result (readme_path , errors , warnings )
398+
399+ # Apply quiet mode filtering
400+ if args .quiet :
401+ # Only show files with errors in quiet mode
402+ if errors :
403+ print (result )
404+ else :
405+ print (result )
282406
283407 # Print summary
284- print ("\n " + "=" * 60 )
285- print ("VALIDATION SUMMARY" )
286- print ("=" * 60 )
287- print (f"Total files validated: { len (readme_files )} " )
288- print (f"Errors found: { error_count } " )
289- print (f"Warnings found: { warning_count } " )
290-
291- if all_valid :
292- print ("\n ✅ All README files are valid!" )
293- sys .exit (0 )
408+ if not args .quiet :
409+ print ("\n " + "=" * SUMMARY_SEPARATOR_LENGTH )
410+ print ("VALIDATION SUMMARY" )
411+ print ("=" * SUMMARY_SEPARATOR_LENGTH )
412+ print (f"Total files validated: { len (readme_files )} " )
413+ print (f"Errors found: { error_count } " )
414+ print (f"Warnings found: { warning_count } " )
415+
416+ # Determine exit status
417+ has_errors = error_count > 0
418+ has_warnings = warning_count > 0
419+
420+ if not has_errors and not has_warnings :
421+ if not args .quiet :
422+ print ("\n ✅ All README files are valid!" )
423+ sys .exit (EXIT_SUCCESS )
424+ elif not has_errors and has_warnings :
425+ if not args .quiet :
426+ print (f"\n ⚠️ Validation completed with { warning_count } warning(s) but no errors" )
427+ # If --errors-only is specified, exit successfully even with warnings
428+ exit_code = EXIT_SUCCESS if args .errors_only else EXIT_ERROR
429+ sys .exit (exit_code )
294430 else :
295- print (f"\n ❌ Validation failed with { error_count } error(s)" )
296- print ("\n Please fix the errors above and ensure all READMEs follow the template." )
297- print ("See README_TEMPLATE.md for the correct structure." )
298- sys .exit (1 )
431+ if not args .quiet :
432+ print (f"\n ❌ Validation failed with { error_count } error(s)" )
433+ if has_warnings :
434+ print (f"Also found { warning_count } warning(s)" )
435+ print ("\n Please fix the errors above and ensure all READMEs follow the template." )
436+ print ("See README_TEMPLATE.md for the correct structure." )
437+ sys .exit (EXIT_ERROR )
299438
300439if __name__ == "__main__" :
301440 main ()
0 commit comments