initial commit
This commit is contained in:
commit
f4cb9fe9a1
4 changed files with 469 additions and 0 deletions
202
src/main.rs
Executable file
202
src/main.rs
Executable 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue