mod error;
pub mod expander;
mod parser;
pub use error::HostlistError;
pub use expander::{expand_host_spec, expand_host_specs, expand_hostlist};
pub use parser::{HostPattern, parse_host_pattern, parse_hostfile};
pub fn is_hostlist_expression(pattern: &str) -> bool {
if !pattern.contains('[') || !pattern.contains(']') {
return false;
}
let mut in_bracket = false;
let mut bracket_content = String::new();
for ch in pattern.chars() {
match ch {
'[' if !in_bracket => {
in_bracket = true;
bracket_content.clear();
}
']' if in_bracket => {
if looks_like_hostlist_range(&bracket_content) {
return true;
}
in_bracket = false;
}
_ if in_bracket => {
bracket_content.push(ch);
}
_ => {}
}
}
false
}
pub fn looks_like_hostlist_range(content: &str) -> bool {
if content.is_empty() {
return false;
}
for part in content.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if part.contains('-') {
let parts: Vec<&str> = part.splitn(2, '-').collect();
if parts.len() == 2 {
if parts[0].chars().all(|c| c.is_ascii_digit())
&& parts[1].chars().all(|c| c.is_ascii_digit())
{
return true;
}
}
} else {
if part.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
false
}
pub fn expand_hostlist_patterns(expr: &str) -> Result<Vec<String>, HostlistError> {
if expr.is_empty() {
return Ok(Vec::new());
}
if let Some(path) = expr.strip_prefix('^') {
return parse_hostfile(std::path::Path::new(path));
}
let patterns = split_patterns(expr)?;
let mut all_hosts = Vec::new();
for pattern in patterns {
let pattern = pattern.trim();
if pattern.is_empty() {
continue;
}
if let Some(path) = pattern.strip_prefix('^') {
let file_hosts = parse_hostfile(std::path::Path::new(path))?;
all_hosts.extend(file_hosts);
} else {
let expanded = expand_hostlist(pattern)?;
all_hosts.extend(expanded);
}
}
deduplicate_hosts(all_hosts)
}
fn split_patterns(expr: &str) -> Result<Vec<String>, HostlistError> {
let mut patterns = Vec::new();
let mut current = String::new();
let mut bracket_depth = 0;
for ch in expr.chars() {
match ch {
'[' => {
bracket_depth += 1;
current.push(ch);
}
']' => {
if bracket_depth == 0 {
return Err(HostlistError::UnmatchedBracket {
expression: expr.to_string(),
});
}
bracket_depth -= 1;
current.push(ch);
}
',' if bracket_depth == 0 => {
if !current.is_empty() {
patterns.push(current);
current = String::new();
}
}
_ => current.push(ch),
}
}
if bracket_depth != 0 {
return Err(HostlistError::UnclosedBracket {
expression: expr.to_string(),
});
}
if !current.is_empty() {
patterns.push(current);
}
Ok(patterns)
}
fn deduplicate_hosts(hosts: Vec<String>) -> Result<Vec<String>, HostlistError> {
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for host in hosts {
if seen.insert(host.clone()) {
result.push(host);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_hostlist_patterns_empty() {
let result = expand_hostlist_patterns("").unwrap();
assert!(result.is_empty());
}
#[test]
fn test_expand_hostlist_patterns_single() {
let result = expand_hostlist_patterns("node[1-3]").unwrap();
assert_eq!(result, vec!["node1", "node2", "node3"]);
}
#[test]
fn test_expand_hostlist_patterns_multiple() {
let result = expand_hostlist_patterns("web[1-2],db[1-2]").unwrap();
assert_eq!(result, vec!["web1", "web2", "db1", "db2"]);
}
#[test]
fn test_expand_hostlist_patterns_with_whitespace() {
let result = expand_hostlist_patterns("web[1-2], db[1-2]").unwrap();
assert_eq!(result, vec!["web1", "web2", "db1", "db2"]);
}
#[test]
fn test_expand_hostlist_patterns_deduplication() {
let result = expand_hostlist_patterns("node[1-3],node[2-4]").unwrap();
assert_eq!(result, vec!["node1", "node2", "node3", "node4"]);
}
#[test]
fn test_split_patterns_simple() {
let patterns = split_patterns("a,b,c").unwrap();
assert_eq!(patterns, vec!["a", "b", "c"]);
}
#[test]
fn test_split_patterns_with_brackets() {
let patterns = split_patterns("node[1,2,3],web[1-3]").unwrap();
assert_eq!(patterns, vec!["node[1,2,3]", "web[1-3]"]);
}
#[test]
fn test_split_patterns_unclosed_bracket() {
let result = split_patterns("node[1-3");
assert!(result.is_err());
}
#[test]
fn test_split_patterns_unmatched_bracket() {
let result = split_patterns("node]1-3[");
assert!(result.is_err());
}
}