net.port-list
Parse a port list such as 22,80,8000-8010 into port numbers, in order and without duplicates.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 27 tests, run in TypeScript, Python and Rust.
What it does
Parses a port list as people type it on a command line or in a config file, `22,80,8000-8010`, into port numbers: each port once, in the order it was first given (a scan of `443,22` probes 443 first). Ranges are inclusive, and every port is checked with `net.port`'s `isPort`, so 0 and 65536 are refused.
## Accepted
For example
parse_port_list(22)→ 22 a single portparse_port_list(443,22,80)→ 443, 22, 80 a list keeps the order given, not sortedparse_port_list(8000-8003)→ 8,000, 8,001, 8,002, 8,003 an inclusive range
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn parse_port_list(text: &str) -> Vec<i64>
| text | string | comma-separated ports and inclusive ranges low-high; spaces around items are allowed |
| returns | int[] | each port once, in the order first given |
Your code names it in one line, in the file that uses it
fune!(net.port-list@^1); // then call parse_port_list(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::net_port::is_port; ← from net.port ^1.0.0 · built alongside by fune
use std::collections::HashSet;
/// Spaces and tabs only: a newline inside a port list is a mistake, not padding.
fn trim_blank(s: &str) -> &str {
s.trim_matches(|c| c == ' ' || c == '\t')
}
/// Whole ASCII digits with no leading zero: "80", not "080", "+80" or "٨٠".
fn is_number(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) && (s.len() == 1 || !s.starts_with('0'))
}
fn port(token: &str) -> i64 {
// More than five digits cannot be a port, and would overflow an i64 parse.
let value = if token.len() > 5 { 0 } else { token.parse::<i64>().unwrap() };
if !is_port(value) {
panic!("port \"{}\" is out of range 1-65535", token);
}
value
}
/// Parse a port list such as "22,80,8000-8010" into port numbers: each port
/// once, in the order first given. Ranges are inclusive.
///
/// # Panics
/// Panics on an empty list or item, a malformed port or range, a port outside
/// 1..=65535, or a range that runs backwards.
pub fn parse_port_list(text: &str) -> Vec<i64> {
if trim_blank(text).is_empty() {
panic!("port list is empty");
}
let mut out = Vec::new();
let mut seen = HashSet::new();
let mut add = |p: i64| {
if seen.insert(p) {
out.push(p);
}
};
for raw in text.split(',') {
let item = trim_blank(raw);
if item.is_empty() {
panic!("empty item in port list");
}
if item.contains('-') {
let ends: Vec<&str> = item.split('-').collect();
if ends.len() != 2 || !is_number(ends[0]) || !is_number(ends[1]) {
panic!("invalid port range \"{}\"", item);
}
let low = port(ends[0]);
let high = port(ends[1]);
if low > high {
panic!("port range \"{}\" runs backwards", item);
}
for p in low..=high {
add(p);
}
} else {
if !is_number(item) {
panic!("invalid port \"{}\"", item);
}
add(port(item));
}
}
out
}
pub fn fune_vector(args: &[Value]) -> Value {
let text = match &args[0] {
Value::Str(s) => s.as_str(),
_ => panic!("text must be a string"),
};
Value::Arr(parse_port_list(text).into_iter().map(Value::Int).collect())
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add net.port-list
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./net.port-list-1.0.0-rust.fune, or fetch it from a terminal with fune pull net.port-list@1.0.0:rust.
The whole function, every language, is one file too: net.port-list-1.0.0.fune, 12,546 bytes, sha256 a9386de8ca0833c7dca907585ed78b1740aafad25d5a3d15532e62b3ff6119fa. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before net.port-list
after — your function gets the result and the arguments, and returns the final result.
// fune: after net.port-list
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace net.port in net.port-list
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.port-list --steps.
// fune: step net.port-list after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a single port | 22 | → | 22 |
| a list keeps the order given, not sorted | 443,22,80 | → | 443, 22, 80 |
| an inclusive range | 8000-8003 | → | 8,000, 8,001, 8,002, 8,003 |
| ports and ranges mixed | 22,80,8000-8002 | → | 22, 80, 8,000, 8,001, 8,002 |
| spaces and tabs around items are allowed | 22 , 80 , 8000-8001 | → | 22, 80, 8,000, 8,001 |
| duplicates are dropped, keeping the first | 80,22,80,22 | → | 80, 22 |
| a port inside an earlier range is not repeated | 20-23,22,19 | → | 20, 21, 22, 23, 19 |
| a one-port range | 443-443 | → | 443 |
| the extremes 1 and 65535 | 65535,1 | → | 65,535, 1 |
| empty text | → | error: port list is empty |
Show the other 17 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| only spaces | → | error: port list is empty | |
| an empty item between commas | 22,,80 | → | error: empty item in port list |
| a trailing comma | 22,80, | → | error: empty item in port list |
| a word is not a port | 22,ssh | → | error: invalid port "ssh" |
| a leading plus is refused | +22 | → | error: invalid port "+22" |
| a leading zero is refused | 080 | → | error: invalid port "080" |
| a trailing newline is not trimmed | 22,80 | → | error: invalid port "80 " |
| Arabic-Indic digits are not port numbers | ٨٠ | → | error: invalid port "٨٠" |
| 0 is not a port | 0 | → | error: port "0" is out of range 1-65535 |
| 65536 is not a port | 65536 | → | error: port "65536" is out of range 1-65535 |
| six digits is out of range | 100000 | → | error: port "100000" is out of range 1-65535 |
| a range that runs backwards | 8010-8000 | → | error: port range "8010-8000" runs backwards |
| a range with a missing end | 8000- | → | error: invalid port range "8000-" |
| a negative port reads as a malformed range | -22 | → | error: invalid port range "-22" |
| a range with an out-of-range end | 65000-70000 | → | error: port "70000" is out of range 1-65535 |
| a three-part range | 1-2-3 | → | error: invalid port range "1-2-3" |
| a number instead of text is refused | 22 | → | error: text must be a string |
More from the author
- Items separated by commas; spaces and tabs around an item are ignored. - An item is a port (`80`) or an inclusive range (`8000-8010`). - Duplicates, including ports already covered by an earlier range, are dropped silently, keeping the first.
## Refused, each with its own error
| input | error | |-------|-------| | `""`, `" "` | `port list is empty` | | `22,,80`, `22,` | `empty item in port list` | | `ssh`, `+22`, `080`, `80\n`, non-ASCII digits | `invalid port "..."` | | `0`, `65536`, `100000` | `port "..." is out of range 1-65535` | | `8000-`, `-22`, `1-2-3` | `invalid port range "..."` | | `8010-8000` | `port range "8010-8000" runs backwards` |
Leading zeros are refused rather than read as decimal, because some tools read `080` as octal. A newline is not trimmed: a list read from a file should be split into lines first. Service names (`ssh`) are not looked up; use `net.port`'s `portService` to go the other way.
A very wide range such as `1-65535` is allowed and returns 65535 ports; a caller that sweeps should cap what it is willing to probe.
Files
| Path | Bytes |
|---|---|
| README.md | 1,407 |
| impl/python.py | 1,927 |
| impl/rust.rs | 2,412 |
| impl/typescript.ts | 1,903 |
| vectors.json | 2,753 |