1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

Merge pull request #7078 from jfinkels/tsort-split-whitespace

tsort: split edge data on any whitespace chars
This commit is contained in:
Daniel Hofstetter 2025-01-05 15:27:43 +01:00 committed by GitHub
commit e3acd5ab07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 46 deletions

View file

@ -5,13 +5,11 @@
//spell-checker:ignore TAOCP //spell-checker:ignore TAOCP
use clap::{crate_version, Arg, Command}; use clap::{crate_version, Arg, Command};
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt::Write; use std::fmt::Display;
use std::fs::File;
use std::io::{stdin, BufReader, Read};
use std::path::Path; use std::path::Path;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError}; use uucore::error::{UError, UResult};
use uucore::{format_usage, help_about, help_usage}; use uucore::{format_usage, help_about, help_usage, show};
const ABOUT: &str = help_about!("tsort.md"); const ABOUT: &str = help_about!("tsort.md");
const USAGE: &str = help_usage!("tsort.md"); const USAGE: &str = help_usage!("tsort.md");
@ -20,6 +18,43 @@ mod options {
pub const FILE: &str = "file"; pub const FILE: &str = "file";
} }
#[derive(Debug)]
enum TsortError {
/// The input file is actually a directory.
IsDir(String),
/// The number of tokens in the input data is odd.
///
/// The list of edges must be even because each edge has two
/// components: a source node and a target node.
NumTokensOdd(String),
/// The graph contains a cycle.
Loop(String),
/// A particular node in a cycle. (This is mainly used for printing.)
LoopNode(String),
}
impl std::error::Error for TsortError {}
impl UError for TsortError {}
impl Display for TsortError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::IsDir(d) => write!(f, "{d}: read error: Is a directory"),
Self::NumTokensOdd(i) => write!(
f,
"{}: input contains an odd number of tokens",
i.maybe_quote()
),
Self::Loop(i) => write!(f, "{i}: input contains a loop:"),
Self::LoopNode(v) => write!(f, "{v}"),
}
}
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?; let matches = uu_app().try_get_matches_from(args)?;
@ -28,61 +63,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.get_one::<String>(options::FILE) .get_one::<String>(options::FILE)
.expect("Value is required by clap"); .expect("Value is required by clap");
let mut stdin_buf; let data = if input == "-" {
let mut file_buf; let stdin = std::io::stdin();
let mut reader = BufReader::new(if input == "-" { std::io::read_to_string(stdin)?
stdin_buf = stdin();
&mut stdin_buf as &mut dyn Read
} else { } else {
let path = Path::new(&input); let path = Path::new(&input);
if path.is_dir() { if path.is_dir() {
return Err(USimpleError::new( return Err(TsortError::IsDir(input.to_string()).into());
1,
format!("{input}: read error: Is a directory"),
));
} }
file_buf = File::open(path).map_err_context(|| input.to_string())?; std::fs::read_to_string(path)?
&mut file_buf as &mut dyn Read };
});
let mut input_buffer = String::new(); // Create the directed graph from pairs of tokens in the input data.
reader.read_to_string(&mut input_buffer)?;
let mut g = Graph::default(); let mut g = Graph::default();
for ab in data.split_whitespace().collect::<Vec<&str>>().chunks(2) {
for line in input_buffer.lines() { match ab {
let tokens: Vec<_> = line.split_whitespace().collect(); [a, b] => g.add_edge(a, b),
if tokens.is_empty() { _ => return Err(TsortError::NumTokensOdd(input.to_string()).into()),
break;
}
for ab in tokens.chunks(2) {
match ab.len() {
2 => g.add_edge(ab[0], ab[1]),
_ => {
return Err(USimpleError::new(
1,
format!(
"{}: input contains an odd number of tokens",
input.maybe_quote()
),
))
}
}
} }
} }
match g.run_tsort() { match g.run_tsort() {
Err(cycle) => { Err(cycle) => {
let mut error_message = format!( show!(TsortError::Loop(input.to_string()));
"{}: {}: input contains a loop:\n",
uucore::util_name(),
input
);
for node in &cycle { for node in &cycle {
writeln!(error_message, "{}: {}", uucore::util_name(), node).unwrap(); show!(TsortError::LoopNode(node.to_string()));
} }
eprint!("{}", error_message);
println!("{}", cycle.join("\n")); println!("{}", cycle.join("\n"));
Err(USimpleError::new(1, "")) Ok(())
} }
Ok(ordering) => { Ok(ordering) => {
println!("{}", ordering.join("\n")); println!("{}", ordering.join("\n"));

View file

@ -75,3 +75,11 @@ fn test_error_on_dir() {
.fails() .fails()
.stderr_contains("tsort: tsort_test_dir: read error: Is a directory"); .stderr_contains("tsort: tsort_test_dir: read error: Is a directory");
} }
#[test]
fn test_split_on_any_whitespace() {
new_ucmd!()
.pipe_in("a\nb\n")
.succeeds()
.stdout_only("a\nb\n");
}