initial commit

This commit is contained in:
raf 2025-05-24 16:03:32 +03:00
commit f4cb9fe9a1
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 469 additions and 0 deletions

202
src/main.rs Executable file
View file

@ -0,0 +1,202 @@
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use clap::Parser;
use anyhow::{Context, Result};
use thiserror::Error;
#[derive(Error, Debug)]
enum FormatterError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Invalid file: {0}")]
InvalidFile(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
}
#[derive(Debug, Clone, Copy)]
enum IndentStyle {
Tabs,
Spaces,
}
impl IndentStyle {
fn format(&self, level: usize, spaces_per_level: usize) -> String {
match self {
Self::Tabs => "\t".repeat(level),
Self::Spaces => " ".repeat(spaces_per_level * level),
}
}
}
impl std::str::FromStr for IndentStyle {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"tabs" | "tab" => Ok(Self::Tabs),
"spaces" | "space" => Ok(Self::Spaces),
_ => Err(format!("Invalid indent style: {}. Use 'tabs' or 'spaces'", s)),
}
}
}
#[derive(Parser, Debug)]
#[command(
name = "nff",
version = "0.1.0",
about = "A high-quality nftables formatter and beautifier",
long_about = "nff (nftables formatter) is a tool for formatting and beautifying nftables configuration files with proper indentation and structure."
)]
struct Args {
/// nftables config file (e.g: /etc/nftables.conf)
#[arg(short, long, value_name = "FILE")]
file: String,
/// Type of indentation
#[arg(short, long, default_value = "tabs", value_parser = clap::value_parser!(IndentStyle))]
indent: IndentStyle,
/// Output file (writes to stdout if not specified)
#[arg(short, long, value_name = "FILE")]
output: Option<String>,
/// Optimize output by removing excessive empty lines
#[arg(long)]
optimize: bool,
/// Number of spaces per indentation level (only used with --indent=spaces)
#[arg(long, default_value = "2", value_name = "N")]
spaces: usize,
}
struct NftablesFormatter {
indent_style: IndentStyle,
spaces_per_level: usize,
optimize: bool,
}
impl NftablesFormatter {
fn new(indent_style: IndentStyle, spaces_per_level: usize, optimize: bool) -> Self {
Self {
indent_style,
spaces_per_level,
optimize,
}
}
fn format_lines(&self, lines: Vec<String>) -> Vec<String> {
let mut output_lines = Vec::new();
let mut level = 0;
let mut prev_was_empty = false;
for (i, line) in lines.iter().enumerate() {
let line = line.trim();
// Handle empty lines
if line.is_empty() {
if self.optimize {
if prev_was_empty {
continue;
}
prev_was_empty = true;
} else {
prev_was_empty = false;
}
output_lines.push(String::new());
continue;
} else {
prev_was_empty = false;
}
// Skip lines that contain both opening and closing braces (single-line blocks)
if line.contains('{') && line.contains('}') {
continue;
}
// Adjust indentation level before formatting if this line closes a block
if line.ends_with('}') || line == "}" {
if level > 0 {
level -= 1;
}
}
// Generate indentation
let indentation = self.indent_style.format(level, self.spaces_per_level);
// Format the line
let formatted_line = format!("{}{}", indentation, line);
// Skip empty lines before closing braces if optimizing
if self.optimize && i > 0 && lines[i-1].trim().is_empty() {
if line.ends_with('}') || line == "}" {
// Remove the last empty line
if let Some(last) = output_lines.last() {
if last.trim().is_empty() {
output_lines.pop();
}
}
}
}
output_lines.push(formatted_line);
// Adjust indentation level after formatting if this line opens a block
if line.ends_with('{') {
level += 1;
}
}
output_lines
}
}
fn process_nftables_config(args: Args) -> Result<()> {
let path = Path::new(&args.file);
if !path.exists() {
return Err(FormatterError::FileNotFound(args.file).into());
}
if !path.is_file() {
return Err(FormatterError::InvalidFile("Not a regular file".to_string()).into());
}
let file = File::open(&args.file)
.with_context(|| format!("Failed to open file: {}", args.file))?;
let reader = BufReader::new(file);
let lines: Result<Vec<String>, io::Error> = reader.lines().collect();
let lines = lines.with_context(|| "Failed to read file contents")?;
let formatter = NftablesFormatter::new(args.indent, args.spaces, args.optimize);
let formatted_lines = formatter.format_lines(lines);
// Create output content
let output_content = formatted_lines.join("\n");
let output_content = if !output_content.ends_with('\n') && !output_content.is_empty() {
format!("{}\n", output_content)
} else {
output_content
};
// Write output
match &args.output {
Some(output_file) => {
fs::write(output_file, output_content)
.with_context(|| format!("Failed to write to output file: {}", output_file))?;
println!("Formatted output written to: {}", output_file);
}
None => {
io::stdout().write_all(output_content.as_bytes())
.with_context(|| "Failed to write to stdout")?;
}
}
Ok(())
}
fn main() -> Result<()> {
let args = Args::parse();
process_nftables_config(args)
}