// Copyright 2024 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::fs::OpenOptions; use std::io::Read as _; use std::io::Write as _; use std::path::PathBuf; use std::process::ExitCode; use bstr::ByteSlice as _; use clap::Parser; use itertools::Itertools as _; #[cfg(unix)] use nix::sys::signal; /// A fake code formatter, useful for testing /// /// `fake-formatter` is similar to `cat`. /// `fake-formatter --reverse` is similar to `rev` (not `tac`). /// `fake-formatter --stdout foo` is similar to `echo foo`. /// `fake-formatter --stdout foo --stderr bar --fail` is similar to /// `echo foo; echo bar >&2; false`. /// `fake-formatter --tee foo` is similar to `tee foo`). /// /// This program acts as a portable alternative to that class of shell commands. #[derive(Parser, Debug)] struct Args { /// Exit with non-successful status. #[arg(long, default_value_t = false)] fail: bool, /// Abort instead of exiting #[arg(long, default_value_t = false, conflicts_with = "fail")] abort: bool, /// Reverse the characters in each line when reading stdin. #[arg(long, default_value_t = false)] reverse: bool, /// Convert all characters to uppercase when reading stdin. #[arg(long, default_value_t = false)] uppercase: bool, /// Convert all characters to lowercase when reading stdin. #[arg(long, default_value_t = false)] lowercase: bool, /// Adds a line to the end of the file #[arg(long)] append: Option, /// Write this string to stdout, and ignore stdin. #[arg(long)] stdout: Option, /// Write this string to stderr. #[arg(long)] stderr: Option, /// Duplicate stdout into this file. #[arg(long)] tee: Vec, /// Read one byte at a time, and send one byte to the stdout before reading /// the next byte. #[arg(long, default_value_t = false)] byte_mode: bool, /// Format lines from first to last inclusive [f, l] (1-indexed) /// For example `--line-ranges 1-2,4-5` or `--line-ranges 1-2 --line-ranges /// 4-5` formats lines 1 and 2, and lines 4 and 5. #[arg(long, value_delimiter = ',')] line_ranges: Vec, /// Split lines with an even number of characters into two lines. /// Used only with `--line-ranges`. /// Replicating formatters that expand beyond the line ranges. /// For example, "abcd" becomes "ab\ncd". #[arg(long, default_value_t = false, requires = "line_ranges")] split_even_length_lines: bool, } /// Represents an inclusive range of lines. #[derive(Debug)] struct LineRange { first: usize, last: usize, } impl LineRange { /// Creates a new line range from a string slice. fn from_str(s: &str) -> Self { let (first, last) = s.split_once('-').unwrap(); Self { first: first.parse().unwrap(), last: last.parse().unwrap(), } } /// Checks if the line range contains the given line number (1-indexed and /// inclusive). fn contains(&self, line_num: usize) -> bool { line_num >= self.first && line_num <= self.last } } /// If a line has an even number of characters (excluding the trailing newline /// if there is one), split it in half into two lines. This simulates a /// formatter that writes to a larger piece of the file than the line range /// it was given. fn split_even_length_line(line: &str) -> String { let line_len = line.strip_suffix('\n').unwrap_or(line).len(); if line_len.is_multiple_of(2) { let (first, second) = line.split_at(line_len / 2); format!("{first}\n{second}") } else { line.to_owned() } } fn main() -> ExitCode { let args: Args = Args::parse(); // Code formatters tend to print errors before printing the result. if let Some(data) = args.stderr { eprint!("{data}"); } let stdout = if let Some(data) = args.stdout { // Other content-altering flags don't apply to --stdout. assert!(!args.reverse); assert!(!args.uppercase); assert!(!args.lowercase); assert!(args.append.is_none()); print!("{data}"); data } else if args.byte_mode { assert!(!args.reverse); assert!(args.append.is_none()); let mut stdout = vec![]; #[expect(clippy::unbuffered_bytes)] for byte in std::io::stdin().bytes() { let byte = byte.expect("Failed to read from stdin"); let output = if args.uppercase { byte.to_ascii_uppercase() } else if args.lowercase { assert!(!args.uppercase); byte.to_ascii_lowercase() } else { byte }; stdout.push(output); std::io::stdout() .write_all(&[output]) .expect("Failed to write to stdout"); } stdout .to_str() .expect("Output is not a valid UTF-8 string") .to_owned() } else { let line_ranges = if !args.line_ranges.is_empty() { args.line_ranges .iter() .map(|range| LineRange::from_str(range)) .collect_vec() } else { vec![LineRange { first: 1, last: usize::MAX, }] }; let mut input = vec![]; std::io::stdin() .read_to_end(&mut input) .expect("Failed to read from stdin"); let mut stdout = input .lines_with_terminator() .enumerate() .map(|(i, line)| { let line = line .to_str() .expect("The input is not valid UTF-8 string") .to_owned(); let line_num = i + 1; let in_range = line_ranges .iter() .any(|line_range| line_range.contains(line_num)); if !in_range { return line; } let line = if args.reverse { line.chars().rev().collect() } else { line }; let line = if args.split_even_length_lines { split_even_length_line(&line) } else { line }; if args.uppercase { assert!(!args.lowercase); line.to_uppercase() } else if args.lowercase { assert!(!args.uppercase); line.to_lowercase() } else { line } }) .join(""); if let Some(line) = args.append { stdout.push_str(&line); } print!("{stdout}"); stdout }; for path in args.tee { let mut file = OpenOptions::new() .create(true) .append(true) .open(path) .unwrap(); write!(file, "{stdout}").unwrap(); } if args.abort { // Coredump generation varies by UNIX and is irrelevant to tests // Prefer raising SIGTERM to crash without dumping core #[cfg(unix)] let _ = signal::raise(signal::Signal::SIGTERM); std::process::abort() } else if args.fail { ExitCode::FAILURE } else { ExitCode::SUCCESS } }