Skip to content

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

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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions