You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1080 lines
34 KiB

#!/usr/bin/env python3
"""
c_util - C code navigation utility
Commands:
toc - Show table of contents for all files
description <names...> - Show selected items with comments
show <name1> [name2] - Show code for functions/structs/enums
edit <file> <line> <checksums> - Edit file with checksum verification
"""
import sys
import re
C_KEYWORDS = {
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if',
'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static',
'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while',
'_Alignas', '_Alignof', '_Atomic', '_Bool', '_Complex', '_Generic', '_Imaginary',
'_Noreturn', '_Static_assert', '_Thread_local'
}
CONTROL_FLOW = {'if', 'for', 'while', 'switch', 'return', 'else'}
def read_filelist(filename='filelist.txt'):
"""Read list of files to process."""
with open(filename, 'r') as f:
return [line.strip() for line in f if line.strip()]
def load_files(files):
"""Load contents of all files."""
contents = {}
for f in files:
try:
with open(f, 'r') as fd:
contents[f] = fd.read().splitlines()
except Exception as e:
print(f"Warning: could not read {f}: {e}", file=sys.stderr)
return contents
def preprocess_lines(lines):
"""
Preprocess lines:
- Remove preprocessor directives (#...)
- Remove comments (/* */ and //)
- Track pre_lines (comments before declarations)
Returns list of tuples: (original_line, is_code, pre_lines)
"""
result = []
multi_comment = False
pre_lines = []
for line in lines:
stripped = line.strip()
# Handle multi-line comments
if multi_comment:
if '*/' in line:
multi_comment = False
pre_lines.append(line)
result.append((line, False, []))
continue
# Start of multi-line comment
if '/*' in line:
end_pos = line.find('*/')
if end_pos == -1:
# Comment continues to next line
multi_comment = True
pre_lines.append(line)
result.append((line, False, []))
continue
else:
# Single-line block comment
pre_lines.append(line)
result.append((line, False, []))
continue
# Single-line comment
if stripped.startswith('//'):
pre_lines.append(line)
result.append((line, False, []))
continue
# Preprocessor directive - remove but don't add to pre_lines
if stripped.startswith('#'):
result.append((line, False, []))
continue
# Empty line - keep as separator but not code
if not stripped:
result.append((line, False, []))
continue
# This is actual code
current_pre = pre_lines[:]
pre_lines = []
result.append((line, True, current_pre))
return result
def compute_brace_levels(lines_info):
"""
Compute brace nesting level for each line.
Returns list of levels (level before processing the line).
"""
levels = []
brace_level = 0
for line, is_code, _ in lines_info:
levels.append(brace_level)
if not is_code:
continue
stripped = line.strip()
# Skip preprocessor (already filtered but double-check)
if stripped.startswith('#'):
continue
# Skip extern "C" { blocks (C++ linkage)
if 'extern "C"' in stripped:
# Count braces in extern "C" line separately
if '{' in stripped:
# Don't count this brace as it opens/closes C linkage block
pass
continue
# Skip do { ... } while(0) - not real nesting
if stripped.startswith('do {'):
continue
if stripped.startswith('}') and 'while(0)' in stripped:
continue
# Count braces
brace_level += line.count('{') - line.count('}')
return levels
def extract_identifier_before_paren(line):
"""
Extract function name before first '('.
Returns (name, return_type) or (None, None) if invalid.
"""
paren_pos = line.find('(')
if paren_pos <= 0:
return None, None
before = line[:paren_pos].strip()
# Split into parts (handle pointers, qualifiers, etc.)
# e.g., "static int * func_name" -> ["static", "int", "*", "func_name"]
parts = before.split()
if not parts:
return None, None
# Last part should be function name
name = parts[-1].strip('*').strip()
# Check if it's a valid identifier
if not name.isidentifier():
return None, None
# Check if it's a keyword
if name in C_KEYWORDS or name in CONTROL_FLOW:
return None, None
# Return type is everything except name
ret_type = ' '.join(parts[:-1]) if len(parts) > 1 else 'void'
return name, ret_type
def find_matching_brace(lines_info, start_idx, start_level):
"""
Find line index of matching closing brace.
Starts from start_idx, looking for level to return to start_level.
"""
level = start_level
for i in range(start_idx, len(lines_info)):
line, is_code, _ = lines_info[i]
if not is_code:
continue
stripped = line.strip()
# Skip preprocessor
if stripped.startswith('#'):
continue
# Skip do-while
if stripped.startswith('do {'):
continue
if stripped.startswith('}') and 'while(0)' in stripped:
continue
# Check current level before this line
if level == start_level and i > start_idx and '}' in line:
return i
# Update level
level += line.count('{') - line.count('}')
return len(lines_info) - 1
def extract_declarations(lines):
"""
Extract all function, struct, and enum declarations.
Returns (functions, structs, enums).
"""
functions = []
structs = []
enums = []
# Preprocess
lines_info = preprocess_lines(lines)
# Compute brace levels
brace_levels = compute_brace_levels(lines_info)
i = 0
n = len(lines_info)
while i < n:
line, is_code, pre_lines = lines_info[i]
if not is_code:
i += 1
continue
stripped = line.strip()
level = brace_levels[i]
# Only process declarations at level 0
if level != 0:
i += 1
continue
# Check for typedef struct/enum/union
if stripped.startswith('typedef struct'):
name, decl_end = parse_typedef_struct(lines_info, i)
if name:
structs.append({
'name': name,
'pre': pre_lines,
'start': i,
'end': decl_end,
'line_count': decl_end - i + 1
})
i = decl_end + 1
continue
if stripped.startswith('typedef enum'):
name, decl_end = parse_typedef_enum(lines_info, i)
if name:
enums.append({
'name': name,
'pre': pre_lines,
'start': i,
'end': decl_end,
'line_count': decl_end - i + 1
})
i = decl_end + 1
continue
# Check for simple struct/enum declarations
if stripped.startswith('struct ') and '{' in stripped:
name, decl_end = parse_simple_struct(lines_info, i)
if name:
structs.append({
'name': name,
'pre': pre_lines,
'start': i,
'end': decl_end,
'line_count': decl_end - i + 1
})
i = decl_end + 1
continue
if stripped.startswith('enum ') and '{' in stripped:
name, decl_end = parse_simple_enum(lines_info, i)
if name:
enums.append({
'name': name,
'pre': pre_lines,
'start': i,
'end': decl_end,
'line_count': decl_end - i + 1
})
i = decl_end + 1
continue
# Check for function declarations
if '(' in stripped:
func_info = parse_function(lines_info, i)
if func_info:
functions.append(func_info)
i = func_info['end'] + 1
continue
i += 1
return functions, structs, enums
def parse_typedef_struct(lines_info, start_idx):
"""Parse typedef struct { ... } name;"""
# Collect lines until we have complete declaration
decl_lines = []
brace_count = 0
for i in range(start_idx, len(lines_info)):
line, is_code, _ = lines_info[i]
decl_lines.append(line)
if not is_code:
continue
stripped = line.strip()
# Count braces (skip do-while)
if not stripped.startswith('do {'):
if not (stripped.startswith('}') and 'while(0)' in stripped):
brace_count += line.count('{') - line.count('}')
# If we've closed all braces and found semicolon, we're done
if brace_count == 0 and stripped.endswith(';'):
# Extract name from "} name;"
decl_text = ' '.join([l.strip() for l in decl_lines])
close_brace = decl_text.rfind('}')
semi = decl_text.find(';', close_brace)
if close_brace > 0 and semi > close_brace:
name = decl_text[close_brace + 1:semi].strip()
return name, i
return None, i
return None, len(lines_info) - 1
def parse_typedef_enum(lines_info, start_idx):
"""Parse typedef enum { ... } name;"""
# Same logic as typedef struct
decl_lines = []
brace_count = 0
for i in range(start_idx, len(lines_info)):
line, is_code, _ = lines_info[i]
decl_lines.append(line)
if not is_code:
continue
stripped = line.strip()
if not stripped.startswith('do {'):
if not (stripped.startswith('}') and 'while(0)' in stripped):
brace_count += line.count('{') - line.count('}')
if brace_count == 0 and stripped.endswith(';'):
decl_text = ' '.join([l.strip() for l in decl_lines])
close_brace = decl_text.rfind('}')
semi = decl_text.find(';', close_brace)
if close_brace > 0 and semi > close_brace:
name = decl_text[close_brace + 1:semi].strip()
return name, i
return None, i
return None, len(lines_info) - 1
def parse_simple_struct(lines_info, start_idx):
"""Parse struct name { ... };"""
line, is_code, _ = lines_info[start_idx]
stripped = line.strip()
# Extract name: "struct name {"
match = re.match(r'struct\s+(\w+)\s*\{', stripped)
if not match:
return None, start_idx
name = match.group(1)
# Find closing brace
brace_count = 1
for i in range(start_idx + 1, len(lines_info)):
line, is_code, _ = lines_info[i]
if not is_code:
continue
stripped = line.strip()
if not stripped.startswith('do {'):
if not (stripped.startswith('}') and 'while(0)' in stripped):
brace_count += line.count('{') - line.count('}')
if brace_count == 0:
return name, i
return name, len(lines_info) - 1
def parse_simple_enum(lines_info, start_idx):
"""Parse enum name { ... };"""
line, is_code, _ = lines_info[start_idx]
stripped = line.strip()
# Extract name: "enum name {"
match = re.match(r'enum\s+(\w+)\s*\{', stripped)
if not match:
return None, start_idx
name = match.group(1)
# Find closing brace
brace_count = 1
for i in range(start_idx + 1, len(lines_info)):
line, is_code, _ = lines_info[i]
if not is_code:
continue
stripped = line.strip()
if not stripped.startswith('do {'):
if not (stripped.startswith('}') and 'while(0)' in stripped):
brace_count += line.count('{') - line.count('}')
if brace_count == 0:
return name, i
return name, len(lines_info) - 1
def parse_function(lines_info, start_idx):
"""Parse function declaration at level 0."""
line, is_code, pre_lines = lines_info[start_idx]
stripped = line.strip()
# Extract function name
name, ret_type = extract_identifier_before_paren(stripped)
if not name:
return None
# Check if this is a forward declaration (ends with ;)
if stripped.endswith(';'):
return None
# Find opening brace - function body must start with {
# Check current line and next line only
decl_end = start_idx
found_brace = False
# Check current line first - look for { after )
paren_end = stripped.rfind(')')
if paren_end > 0:
after_paren = stripped[paren_end:]
if '{' in after_paren:
decl_end = start_idx
found_brace = True
# If not found, check next few lines (up to 3) for {
# But stop if we encounter ; (end of prototype)
if not found_brace:
for offset in range(1, 4): # Check next 3 lines
if start_idx + offset >= len(lines_info):
break
check_line, check_is_code, _ = lines_info[start_idx + offset]
if not check_is_code:
continue
check_stripped = check_line.strip()
# If we hit a semicolon, this is a prototype, not a definition
if check_stripped.endswith(';'):
return None
# If we found opening brace, this is the function body
# Check if '{' is present in the line (not necessarily at start due to formatting)
if '{' in check_line:
decl_end = start_idx + offset
found_brace = True
break
if not found_brace:
# No body - probably forward declaration or macro, skip
return None
# Find closing brace
closing = find_matching_brace(lines_info, decl_end + 1, 0)
# Extract arguments - need to collect all lines from start to decl_end
# to handle multi-line function signatures
sig_lines = []
for idx in range(start_idx, decl_end + 1):
l, is_code, _ = lines_info[idx]
if is_code:
sig_lines.append(l)
sig_text = ' '.join(sig_lines)
paren_start = sig_text.find('(')
paren_end = sig_text.rfind(')')
if paren_start > 0 and paren_end > paren_start:
args = sig_text[paren_start + 1:paren_end].strip()
else:
args = ''
return {
'type': 'function',
'name': name,
'args': args,
'ret': ret_type,
'pre': pre_lines,
'start': start_idx,
'end': closing,
'decl_end': decl_end, # Store the line with opening brace
'line_count': closing - start_idx + 1
}
def line_checksum(line):
"""Calculate checksum: sum of all character codes modulo 256, as 2 hex digits."""
checksum = sum(ord(c) for c in line) % 256
return f"{checksum:02X}"
def autolearn_indentation(lines, context_start=0, context_end=None):
"""
Analyze indentation style in the file.
Returns dict with:
- indent_char: '\t' or ' '
- indent_size: number of spaces per level (for spaces) or 1 (for tabs)
- base_indent: base indentation string for the context
"""
if context_end is None:
context_end = len(lines)
# Count tab vs space indented lines
tab_lines = 0
space_lines = 0
space_counts = []
for i in range(context_start, min(context_end, len(lines))):
line = lines[i]
if not line.strip():
continue
leading = line[:len(line) - len(line.lstrip())]
if '\t' in leading:
tab_lines += 1
elif ' ' in leading:
space_lines += 1
# Count leading spaces
space_count = len(leading)
if space_count > 0:
space_counts.append(space_count)
# Determine indent character
if tab_lines > space_lines:
indent_char = '\t'
indent_size = 1
else:
indent_char = ' '
# Calculate most common indent size (GCD of space counts)
if space_counts:
def gcd(a, b):
while b:
a, b = b, a % b
return a
indent_size = space_counts[0]
for count in space_counts[1:]:
indent_size = gcd(indent_size, count)
if indent_size == 1:
break
# Default to 4 if we couldn't determine
if indent_size < 2:
indent_size = 4
else:
indent_size = 4
return {
'indent_char': indent_char,
'indent_size': indent_size,
'indent_str': indent_char * indent_size if indent_char == ' ' else '\t'
}
def normalize_whitespace(text):
"""Normalize whitespace: replace multiple spaces/newlines with single space."""
# Replace all whitespace (spaces, tabs, newlines) with single space
text = re.sub(r'\s+', ' ', text)
# Strip leading/trailing whitespace
return text.strip()
def get_function_signature(lines, func):
"""Get normalized function signature (like prototype)."""
# Get all lines from start to the line with opening brace
start = func['start']
end = func.get('decl_end', func['start']) # We need to track decl_end in parse_function
# Collect signature lines
sig_lines = []
for i in range(start, min(end + 1, len(lines))):
line = lines[i]
# Remove comments
if '//' in line:
line = line[:line.index('//')]
sig_lines.append(line)
# Join and normalize
sig = ' '.join(sig_lines)
sig = normalize_whitespace(sig)
# Remove the opening brace if present
if '{' in sig:
sig = sig[:sig.index('{')].strip()
# Ensure it ends with semicolon
if not sig.endswith(';'):
sig += ';'
return sig
def get_declaration_text(lines, decl, include_comments=False):
"""Get normalized declaration text for struct/enum."""
start = decl['start']
end = decl['end']
# Collect all lines
decl_lines = []
for i in range(start, min(end + 1, len(lines))):
line = lines[i]
if not include_comments:
# Remove comments
if '//' in line:
line = line[:line.index('//')]
decl_lines.append(line)
if include_comments:
# Normalize: one tab indent, remove empty lines, normalize whitespace
result_lines = []
for line in decl_lines:
# Skip empty lines
if not line.strip():
continue
# Normalize whitespace and use one tab for indentation
normalized = normalize_whitespace(line)
if normalized:
result_lines.append('\t' + normalized)
return '\n'.join(result_lines)
else:
# Join and normalize for toc mode
text = ' '.join(decl_lines)
text = normalize_whitespace(text)
return text
def get_function_comments(pre_lines):
"""Extract function comments from pre_lines.
Returns:
- Last /* */ block if found (including single-line /* */)
- Or consecutive // lines (without empty lines or non-comment lines)
- Empty string if no suitable comments
"""
if not pre_lines:
return ""
# Look for last /* */ block
last_block_comment = ""
in_block = False
block_start = -1
for i, line in enumerate(pre_lines):
stripped = line.strip()
if stripped.startswith('/*'):
# Check if it's a single-line comment /* ... */
if stripped.endswith('*/'):
# Single-line block comment
last_block_comment = line
else:
# Start of multi-line block comment
in_block = True
block_start = i
elif in_block and stripped.endswith('*/'):
in_block = False
# Extract the block
block_lines = pre_lines[block_start:i+1]
last_block_comment = '\n'.join(block_lines)
elif stripped.startswith('*/'):
in_block = False
if last_block_comment:
return last_block_comment
# Look for consecutive // lines (from the end, backwards)
comment_lines = []
for line in reversed(pre_lines):
stripped = line.strip()
if stripped.startswith('//'):
comment_lines.insert(0, stripped)
elif not stripped: # Skip empty lines
continue
else: # Non-comment line breaks the sequence
break
return '\n'.join(comment_lines) if comment_lines else ""
def main():
if len(sys.argv) < 2:
print("Usage: c_util toc | description <name1> [name2] ... | show <name1> [name2] ... | edit <file> <line> <checksums> ...")
sys.exit(1)
cmd = sys.argv[1]
files = read_filelist()
contents = load_files(files)
# Parse all files
project_functions = {}
project_structs = {}
project_enums = {}
for f, lines in contents.items():
funcs, strs, enums = extract_declarations(lines)
project_functions[f] = funcs
project_structs[f] = strs
project_enums[f] = enums
if cmd == 'toc':
first_file = True
for f in files:
if f not in contents:
continue
# Get all declarations for this file
declarations = []
# Add functions
for func in project_functions.get(f, []):
sig = get_function_signature(contents[f], func)
declarations.append((func['start'], func['end'], sig))
# Add structs
for s in project_structs.get(f, []):
text = get_declaration_text(contents[f], s)
declarations.append((s['start'], s['end'], text))
# Add enums
for e in project_enums.get(f, []):
text = get_declaration_text(contents[f], e)
declarations.append((e['start'], e['end'], text))
if not declarations:
continue
# Sort by line number
declarations.sort(key=lambda x: x[0])
# Print empty line between files (except before first)
if not first_file:
print()
first_file = False
# Print filename header
print(f"{f}:")
# Print in new format
for start, end, text in declarations:
print(f"[{start + 1}-{end + 1}] {text}")
elif cmd == 'description':
# Get list of names to look for
if len(sys.argv) < 3:
print("Usage: c_util description <name1> [name2] ...")
sys.exit(1)
target_names = set(sys.argv[2:])
first_file = True
for f in files:
if f not in contents:
continue
# Collect all matching declarations with full info
declarations = []
# Check functions
for func in project_functions.get(f, []):
if func['name'] in target_names:
# Get signature
sig = get_function_signature(contents[f], func)
# Get comments
comments = get_function_comments(func['pre'])
declarations.append((func['start'], func['end'], 'function', sig, comments))
# Check structs
for s in project_structs.get(f, []):
if s['name'] in target_names:
# Get full text with formatting and comments
text = get_declaration_text(contents[f], s, include_comments=True)
declarations.append((s['start'], s['end'], 'struct', s['name'], text))
# Check enums
for e in project_enums.get(f, []):
if e['name'] in target_names:
# Get full text with formatting and comments
text = get_declaration_text(contents[f], e, include_comments=True)
declarations.append((e['start'], e['end'], 'enum', e['name'], text))
if not declarations:
continue
# Sort by line number
declarations.sort(key=lambda x: x[0])
# Print empty line between files (except before first)
if not first_file:
print()
first_file = False
# Print filename header
print(f"{f}:")
# Print declarations with full formatting
for start, end, decl_type, content, extra in declarations:
if decl_type == 'function':
# Print comments first if any (without line numbers)
if extra:
print(extra)
# Print function signature with line number and checksum
lines = contents[f]
for line_idx in range(start, end + 1):
if line_idx < len(lines):
line = lines[line_idx]
line_num = line_idx + 1
checksum = line_checksum(line)
print(f"{line_num} {checksum}: {line}")
else:
# struct or enum - print with line numbers and checksums
lines = contents[f]
for line_idx in range(start, end + 1):
if line_idx < len(lines):
line = lines[line_idx]
line_num = line_idx + 1
checksum = line_checksum(line)
print(f"{line_num} {checksum}: {line}")
elif cmd == 'show':
if len(sys.argv) < 3:
print("Usage: c_util show <name1> [name2] ...")
sys.exit(1)
target_names = set(sys.argv[2:])
first_file = True
found_any = False
for f in files:
if f not in contents:
continue
# Collect all matching items
items = []
# Check functions
for func in project_functions.get(f, []):
if func['name'] in target_names:
items.append(('function', func))
found_any = True
# Check structs
for s in project_structs.get(f, []):
if s['name'] in target_names:
items.append(('struct', s))
found_any = True
# Check enums
for e in project_enums.get(f, []):
if e['name'] in target_names:
items.append(('enum', e))
found_any = True
if not items:
continue
# Sort by line number
items.sort(key=lambda x: x[1]['start'])
# Print empty line between files (except before first)
if not first_file:
print()
first_file = False
# Print filename header
print(f"{f}:")
# Print items with line numbers and checksums
for item_type, item in items:
lines = contents[f]
start = item['start']
end = item['end']
# Print pre lines (comments) without line numbers
pre = item['pre']
if pre:
for pre_line in pre:
print(pre_line)
# Print body with line numbers and checksums
for line_idx in range(start, end + 1):
if line_idx < len(lines):
line = lines[line_idx]
line_num = line_idx + 1
checksum = line_checksum(line)
print(f"{line_num} {checksum}: {line}")
if not found_any:
print("Not found")
sys.exit(1)
elif cmd == 'edit':
# Format: c_util edit path/file.c start_line checksum1 [checksum2 ...] <<'EOF'
if len(sys.argv) < 5:
print("Usage: c_util edit <file> <start_line> <checksum1> [checksum2 ...]")
print("Then provide new code block via stdin or heredoc")
sys.exit(1)
file_path = sys.argv[2]
# Load the file if not already loaded
if file_path not in contents:
try:
with open(file_path, 'r') as f:
contents[file_path] = f.read().splitlines()
except Exception as e:
print(f"Error reading file {file_path}: {e}", file=sys.stderr)
sys.exit(1)
file_lines = contents[file_path]
try:
start_line = int(sys.argv[3])
except ValueError:
print(f"Invalid line number: {sys.argv[3]}")
sys.exit(1)
# Get expected checksums
expected_checksums = []
for checksum_str in sys.argv[4:]:
try:
expected_checksums.append(int(checksum_str, 16))
except ValueError:
print(f"Invalid checksum format: {checksum_str}")
sys.exit(1)
num_lines_to_replace = len(expected_checksums)
# Verify line numbers are valid
if start_line < 1 or start_line > len(file_lines):
print(f"Line number {start_line} out of range (1-{len(file_lines)})")
sys.exit(1)
if start_line + num_lines_to_replace - 1 > len(file_lines):
print(f"Block extends beyond end of file")
sys.exit(1)
# Verify checksums
actual_checksums = []
for i in range(num_lines_to_replace):
line_idx = start_line - 1 + i
actual_checksum = sum(ord(c) for c in file_lines[line_idx]) % 256
actual_checksums.append(actual_checksum)
expected = expected_checksums[i]
if actual_checksum != expected:
print(f"Checksum mismatch at line {line_idx + 1}:")
print(f" Expected: {expected:02X}")
print(f" Actual: {actual_checksum:02X}")
print(f" Line: {repr(file_lines[line_idx])}")
sys.exit(1)
# Read new code from stdin
print("Reading new code block from stdin...", file=sys.stderr)
new_code_lines = sys.stdin.read().splitlines()
# Determine indentation style from file
indent_info = autolearn_indentation(file_lines, max(0, start_line - 10), min(len(file_lines), start_line + num_lines_to_replace + 10))
# Calculate base indentation from the first line being replaced
first_old_line = file_lines[start_line - 1]
base_indent = first_old_line[:len(first_old_line) - len(first_old_line.lstrip())]
# Determine indentation level from surrounding code
# Count braces before the replacement block
brace_level = 0
for i in range(max(0, start_line - 20), start_line - 1):
line = file_lines[i]
# Skip preprocessor and comments
stripped = line.strip()
if stripped.startswith('#'):
continue
# Count braces (simple approach)
brace_level += line.count('{') - line.count('}')
# Apply indentation to new code
indented_new_lines = []
current_level = brace_level
for i, line in enumerate(new_code_lines):
stripped = line.strip()
# Empty line - keep as is
if not stripped:
indented_new_lines.append('')
continue
# Preprocessor directive - no indentation
if stripped.startswith('#'):
indented_new_lines.append(stripped)
continue
# Check for closing brace - reduce level before this line
if stripped.startswith('}'):
current_level -= 1
# Calculate indentation
if indent_info['indent_char'] == '\t':
indent = '\t' * current_level
else:
indent = ' ' * (indent_info['indent_size'] * current_level)
indented_new_lines.append(indent + stripped)
# Check for opening brace - increase level for next lines
if stripped.endswith('{'):
current_level += 1
# Replace the block
start_idx = start_line - 1
end_idx = start_idx + num_lines_to_replace
new_file_lines = file_lines[:start_idx] + indented_new_lines + file_lines[end_idx:]
# Write back to file
try:
with open(file_path, 'w') as f:
f.write('\n'.join(new_file_lines))
if new_file_lines and not new_file_lines[-1].endswith('\n'):
f.write('\n')
print(f"Successfully edited {file_path}")
print(f"Replaced {num_lines_to_replace} lines with {len(indented_new_lines)} lines")
except Exception as e:
print(f"Error writing file {file_path}: {e}", file=sys.stderr)
sys.exit(1)
else:
print("Unknown command")
sys.exit(1)
if __name__ == "__main__":
main()