251 lines · 7.8 KB · Rust
RawDownload
// 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<String>,     /// Write this string to stdout, and ignore stdin.    #[arg(long)]    stdout: Option<String>,     /// Write this string to stderr.    #[arg(long)]    stderr: Option<String>,     /// Duplicate stdout into this file.    #[arg(long)]    tee: Vec<PathBuf>,     /// 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<String>,     /// 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    }}