Functional Weave
Code in TypeScript

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

  • parsePortList(22) → 22 a single port
  • parsePortList(443,22,80) → 443, 22, 80 a list keeps the order given, not sorted
  • parsePortList(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.

export function parsePortList(text: string): readonly number[]
textstringcomma-separated ports and inclusive ranges low-high; spaces around items are allowed
returnsint[]each port once, in the order first given

Your code names it in one line, in the file that uses it

import { parsePortList } from "#fune/net.port-list@^1";
impl/typescript.ts · 53 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { isPort } from "./net_port.ts";  ← from net.port ^1.0.0 · built alongside by fune

/** Spaces and tabs only: a newline inside a port list is a mistake, not padding. */
function trimBlank(s: string): string {
  return s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
}

/** Whole ASCII digits with no leading zero: "80", not "080", "+80" or "٨٠". */
function isNumber(s: string): boolean {
  return /^[0-9]+$/.test(s) && (s.length === 1 || s[0] !== "0");
}

function port(token: string): number {
  // More than five digits cannot be a port, and would overflow elsewhere.
  const value = token.length > 5 ? 0 : Number(token);
  if (!isPort(value)) throw new RangeError(`port "${token}" is out of range 1-65535`);
  return 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.
 */
export function parsePortList(text: string): readonly number[] {
  if (typeof text !== "string") throw new TypeError("text must be a string");
  if (trimBlank(text) === "") throw new RangeError("port list is empty");
  const out: number[] = [];
  const seen = new Set<number>();
  const add = (p: number) => {
    if (!seen.has(p)) {
      seen.add(p);
      out.push(p);
    }
  };
  for (const raw of text.split(",")) {
    const item = trimBlank(raw);
    if (item === "") throw new RangeError("empty item in port list");
    if (item.includes("-")) {
      const ends = item.split("-");
      if (ends.length !== 2 || !isNumber(ends[0]) || !isNumber(ends[1])) {
        throw new RangeError(`invalid port range "${item}"`);
      }
      const low = port(ends[0]);
      const high = port(ends[1]);
      if (low > high) throw new RangeError(`port range "${item}" runs backwards`);
      for (let p = low; p <= high; p++) add(p);
    } else {
      if (!isNumber(item)) throw new RangeError(`invalid port "${item}"`);
      add(port(item));
    }
  }
  return out;
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript 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. Or pin a range in fune.project and build in one step:

fune add net.port-list
Download for TypeScript net.port-list-1.0.0-typescript.fune · 7,949 bytes sha256 bae33156dbed49340ad51ec42077b11a8aaa17a4a0ab191639054d6d6032f72d

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.port-list-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.port-list@1.0.0:typescript.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,407
impl/python.py1,927
impl/rust.rs2,412
impl/typescript.ts1,903
vectors.json2,753