Skip to content

feat: Add pdsh-style hostlist expression support (range expansion) #98

@inureyes

Description

@inureyes

Summary

Implement pdsh-style hostlist expression syntax for specifying multiple hosts using range notation. This allows compact specification of host lists like node[01-10] instead of listing each host individually.

Parent Issue

Part of #91 (pdsh compatibility mode) - Phase 3: Hostlist Expression Support

Prerequisites

  • Phase 2 Core Infrastructure must be complete
  • The hostlist parser can be used in both pdsh compat mode and native bssh mode

Background

pdsh uses a powerful hostlist expression syntax that allows:

  • Range expansion: node[01-05] -> node01, node02, node03, node04, node05
  • Multiple ranges: node[1-3,7,9-10] -> node1, node2, node3, node7, node9, node10
  • Prefix/suffix: rack[1-2]-node[1-3] -> all combinations
  • Exclusion: node[1-10] -x node[3-5]
  • File input: ^/path/to/hostfile

Proposed Implementation

Hostlist Syntax

hostlist     = host_term (',' host_term)*
host_term    = prefix range_expr suffix
range_expr   = '[' range_list ']'
range_list   = range_item (',' range_item)*
range_item   = NUMBER | NUMBER '-' NUMBER
prefix       = STRING (any characters before '[')
suffix       = STRING (any characters after ']', may include nested ranges)

Examples

# Simple range
node[1-5]                    # -> node1, node2, node3, node4, node5

# Zero-padded range
node[01-05]                  # -> node01, node02, node03, node04, node05

# Multiple values
node[1,3,5]                  # -> node1, node3, node5

# Mixed ranges and values
node[1-3,7,9-10]             # -> node1, node2, node3, node7, node9, node10

# Multiple range groups
rack[1-2]-node[1-3]          # -> rack1-node1, rack1-node2, rack1-node3,
                             #   rack2-node1, rack2-node2, rack2-node3

# With domain suffix
web[1-3].example.com         # -> web1.example.com, web2.example.com, web3.example.com

# File input (pdsh style)
^/etc/hosts.cluster          # Read hosts from file, one per line

Module Structure

// src/hostlist/mod.rs
pub mod parser;
pub mod expander;

pub fn expand_hostlist(expr: &str) -> Result<Vec<String>, HostlistError>;
pub fn parse_hostfile(path: &Path) -> Result<Vec<String>, HostlistError>;

Native bssh -H Option Integration

The hostlist expression syntax will be available in native bssh mode through the -H (hosts) option. This extends the current comma-separated host list syntax to support range expressions.

Current -H Option Syntax

# Current syntax (comma-separated hosts)
bssh -H "user@host1:22,user@host2:22,user@host3:22" "uptime"

Extended -H Option Syntax with Range Expansion

# Simple range expansion
bssh -H "node[1-5]" "uptime"                    
# Expands to: node1, node2, node3, node4, node5

# Zero-padded range
bssh -H "node[01-05]" "uptime"                  
# Expands to: node01, node02, node03, node04, node05

# With user and port specification
bssh -H "admin@web[1-3].example.com:22" "uptime"
# Expands to: admin@web1.example.com:22, admin@web2.example.com:22, admin@web3.example.com:22

# Multiple ranges in single host pattern
bssh -H "rack[1-2]-node[1-3]" "uptime"          
# Expands to: rack1-node1, rack1-node2, rack1-node3, rack2-node1, rack2-node2, rack2-node3

# Multiple host patterns (comma-separated)
bssh -H "web[1-3],db[1-2]" "uptime"             
# Expands to: web1, web2, web3, db1, db2

# Complex combination with user/port
bssh -H "admin@app[01-03].prod.example.com:2222,root@db[1-2].prod.example.com" "uptime"
# Expands to: admin@app01.prod.example.com:2222, admin@app02.prod.example.com:2222, 
#             admin@app03.prod.example.com:2222, root@db1.prod.example.com, root@db2.prod.example.com

# File input
bssh -H "^/etc/cluster/hosts" "uptime"          
# Reads hosts from file

# Mixed: explicit hosts and range expressions
bssh -H "gateway,node[1-3],backup" "uptime"     
# Expands to: gateway, node1, node2, node3, backup

-H Option Parsing Flow

Input: "admin@web[1-3].example.com:22"
                    ↓
          Parse user prefix: "admin@"
                    ↓
          Parse hostname with range: "web[1-3].example.com"
                    ↓
          Expand range: ["web1.example.com", "web2.example.com", "web3.example.com"]
                    ↓
          Parse port suffix: ":22"
                    ↓
Output: ["admin@web1.example.com:22", "admin@web2.example.com:22", "admin@web3.example.com:22"]

Integration with Existing -H Features

The range expansion should work seamlessly with existing -H option features:

Feature Example Description
--filter bssh -H "node[1-10]" --filter "node[1-5]" Expand both, then filter
--exclude / -x bssh -H "node[1-10]" -x "node[3-5]" Expand both, then exclude
Jump hosts bssh -H "node[1-3]" -J "bastion" Apply jump host to all expanded hosts
Output dir bssh -H "node[1-5]" -o ./results Create output files for each expanded host

Implementation Tasks

  • Create src/hostlist/mod.rs module
  • Implement range parser ([1-5] syntax)
  • Implement zero-padding preservation ([01-05] -> 01, 02, ...)
  • Implement comma-separated values ([1,3,5])
  • Implement mixed ranges and values ([1-3,7,9-10])
  • Implement multiple range groups (cartesian product)
  • Implement file input with ^filename syntax
  • Integrate with -H option in native mode
    • Parse user prefix (user@) before expansion
    • Parse port suffix (:port) after expansion
    • Handle comma-separated host patterns
    • Integrate with --filter option
    • Integrate with --exclude / -x option
  • Integrate with -w option in pdsh compat mode
  • Add comprehensive unit tests
  • Add integration tests with actual command execution
  • Update documentation
  • Update CLI help text with range expansion examples

Acceptance Criteria

  • node[1-5] expands to 5 hosts
  • node[01-05] preserves zero-padding
  • node[1,3,5] expands to 3 specific hosts
  • node[1-3,7] combines ranges and individual values
  • rack[1-2]-node[1-3] produces 6 hosts (cartesian product)
  • ^/path/to/file reads hosts from file
  • -H "user@host[1-3]:port" correctly parses user, expands hostname, preserves port
  • -H "host[1-3],host[5-7]" expands multiple patterns
  • -H "node[1-10]" -x "node[3-5]" correctly excludes expanded range
  • Invalid syntax produces clear error messages
  • Empty ranges are handled gracefully
  • Large ranges work efficiently (e.g., [1-1000])

Edge Cases to Handle

  • Empty brackets: node[] -> error
  • Reversed range: node[5-1] -> error or reverse iteration?
  • Invalid numbers: node[a-z] -> error
  • Unclosed brackets: node[1-5 -> error
  • Nested brackets: node[[1-2]] -> error
  • Overlapping ranges: node[1-5,3-7] -> deduplicate?
  • User/port with range: user@host[1-3]:22 -> proper parsing
  • Range in username: user[1-3]@host -> should this be supported?
  • Range in port: host:22[01-03] -> should this be an error?
  • IPv6 addresses: [2001:db8::1] vs range [1-3] -> disambiguation

Technical Considerations

  • Consider using nom or pest for parsing
  • Cartesian product can produce large host lists - consider limits
  • Zero-padding detection: compare digit counts in range bounds
  • User/port parsing must happen before/after range expansion
  • IPv6 literal brackets [...] must not conflict with range brackets

Notes

  • This feature can be enabled in native bssh mode too, not just pdsh compat
  • Consider making hostlist expansion a reusable library
  • The -H option integration makes this feature immediately useful without requiring pdsh compat mode

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions