diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..bcd5e07
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,3 @@
+# .cargo/config.toml
+#
+# Formatting requires nightly rustfmt — use ./scripts/fmt (see rustfmt.toml).
diff --git a/.cursor/rules/rust-standards.mdc b/.cursor/rules/rust-standards.mdc
new file mode 100644
index 0000000..5c0edd5
--- /dev/null
+++ b/.cursor/rules/rust-standards.mdc
@@ -0,0 +1,51 @@
+---
+description: Rust coding standards for shwild.Rust (aligned with Synesis Information Systems' internal project standards)
+globs: **/*.rs
+alwaysApply: false
+---
+
+# Rust Software Quality Rules
+
+
+## DOC_76 (The 76-Rule for Documentation Comments)
+
+- Documentation comments (`///`, `//!`) on **public** constructs must be at
+ most **76 characters** per line, including leading indentation;
+- Wrap text **greedily** (pack to maximum width) — do not wrap early if the
+ next word fits on the current line;
+- Code blocks inside doc comments (triple backticks) are exempt;
+- List items in doc comments should end with a semicolon;
+- Inline comments (`//`) inside functions are limited to **100 characters**
+ per line, wrapped greedily;
+- Private/non-`pub` constructs are exempt from DOC_76 for their doc
+ comments;
+
+Run `./scripts/check_doc_76.py` to verify compliance.
+
+
+## DERIVE_LAYOUT (Derive Layout & Formatting)
+
+Multi-trait `#[derive(...)]` macros must be split into separate,
+single-trait lines, ordered alphabetically by trait name. Tightly coupled
+traits may remain on one line: `#[derive(Eq, PartialEq)]`,
+`#[derive(Ord, PartialOrd)]`.
+
+`rustfmt.toml` sets `merge_derives = false` to preserve this layout.
+
+Run `./scripts/check_derives.py` to verify compliance.
+
+
+## RUST_TEST_NAMING
+
+All test methods must use `SHOUTING_SNAKE_CASE` - e.g. `TEST_PARSING()` -
+except where a word in the snake case represents a specific construct (such
+as a struct, enum, type, function, macro, or field name), in which case that
+word must preserve its exact correct case - e.g.
+`TEST_AutoBuffer_WITH_INTERNAL_SIZE()`, `TEST_doom_scope_1()`,
+`TEST_type_name_only_WITH_SomeCustomType()`.
+
+When a construct is embedded as a SHOUTING_SNAKE_CASE constant (or other
+construct name), use an extra underscore on each side as a delimiter - e.g.
+`TEST_MatcherSequence_WITH_Range_HAVING__IGNORE_CASE__1()`.
+
+Run `./scripts/check_test_names.py` to verify compliance.
diff --git a/.gitattributes b/.gitattributes
index e69de29..7beaa80 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+
+*.rs linguist-language=Rust
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5084a8c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,72 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ - boilerplate
+ pull_request:
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ check:
+ name: Test, Clippy, Fmt, Checkers
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy
+
+ - name: Install nightly rustfmt
+ run: rustup toolchain install nightly --profile minimal --component rustfmt
+
+ - uses: Swatinem/rust-cache@v2
+
+ - name: cargo test
+ run: cargo test --locked
+
+ - name: cargo clippy
+ run: cargo clippy --all-targets --locked -- -D warnings
+
+ - name: cargo build (examples)
+ run: cargo build --examples --locked
+
+ - name: cargo build (character-play example)
+ run: cargo build --example character-play --features test-regex --locked
+
+ - name: cargo doc
+ run: cargo doc --no-deps --locked
+
+ - name: rustfmt
+ run: ./scripts/fmt --check
+
+ - name: DOC_76 checker
+ run: python3 scripts/check_doc_76.py
+
+ - name: RUST_TEST_NAMING checker
+ run: python3 scripts/check_test_names.py
+
+ - name: DERIVE_LAYOUT checker
+ run: python3 scripts/check_derives.py
+
+ msrv:
+ name: MSRV (1.79)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@1.79.0
+
+ - uses: Swatinem/rust-cache@v2
+
+ - name: cargo check (library)
+ # Full `cargo test` needs dev-deps (criterion → clap_lex 2024 edition),
+ # which exceeds MSRV; the stable job runs the full test suite.
+ # MSRV 1.79: default `lookup-ranges` → collect-rs → base-traits uses
+ # `CStr::count_bytes()` (stabilized in Rust 1.79).
+ run: cargo check --lib --locked
diff --git a/.gitignore b/.gitignore
index 3589c67..8ea0d84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
# directories (by name)
/_build/
+/scripts/__pycache__/
/scratch/
/target/
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..d4484d1
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,57 @@
+# shwild.Rust - CHANGES
+
+
+## 0.1.4 - 8th July 2026
+
+* added **CHANGES.md** (back-filled) and **NEWS.md**;
+* added **EXAMPLES.md** and per-example documentation;
+* **README.md** badges, dependency links, related projects, and `null-feature` documentation;
+* added crate-level `//!` documentation;
+* added CI (`.github/workflows/ci.yml`) and quality scripts (`scripts/fmt`, checkers);
+* added `rust-version` (MSRV 1.79);
+* renamed `.rustfmt.toml` => **rustfmt.toml**; updated formatting settings;
+* added `.gitattributes`;
+* shortened `description` in **Cargo.toml**;
+* Clippy and test-naming fixes for CI; `check_test_names.py` allows `__CONSTRUCT__` padding;
+* upgraded **criterion** from 0.5 => 0.8;
+
+
+## 0.1.3 - 28th March 2025
+
+* crates.io packaging metadata — `categories`, `keywords`, `documentation`, and expanded `description`;
+* added `exclude` for `target` and `.github` in **Cargo.toml**;
+* added **TODO.md**;
+* **character-play** changed from `[[bin]]` to `[[example]]`;
+
+
+## 0.1.2 - 3rd November 2024
+
+* added `test-regex` feature — optional **regex** dependency for benchmarks and scratch programs;
+* enabled `lookup-ranges` in default features;
+* added **cw-regex** benchmark;
+* added **character-play** scratch program;
+* added `regex_comparision_tests` unit tests (gated on `test-regex`);
+
+
+## 0.1.1 - 3rd November 2024
+
+* added `lookup-ranges` feature — optional **collect-rs** dependency for `UnicodePointMap`-based range matching;
+* added **Cargo.lock**;
+* README: added Features section; clarified Wild-1 escape behaviour in pattern elements;
+
+
+## 0.1.0 - 3rd November 2024
+
+* first public release;
+* added `matches()` and `shwild_matches!()`;
+* added `CompiledMatcher`;
+* added `Error`, `Result`, and `IGNORE_CASE`;
+* added example programs **list-matching-files** and **list-matching-files-compiled**;
+* added benchmarks **range_string-creation_functions**, **shwild-compiled_matcher**, and **shwild-matches**;
+* added **README.md**;
+
+
+All history before this day is moot!
+
+
+
diff --git a/Cargo.lock b/Cargo.lock
index be726ec..7962111 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 4
+version = 3
[[package]]
name = "aho-corasick"
@@ -11,6 +11,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "alloca"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "anes"
version = "0.1.6"
@@ -35,18 +44,22 @@ version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556031355dea4fc81a56aa4850032c9b433c926e7296c30780f63ece19ecf082"
-[[package]]
-name = "bumpalo"
-version = "3.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
-
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+[[package]]
+name = "cc"
+version = "1.2.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -116,25 +129,22 @@ dependencies = [
[[package]]
name = "criterion"
-version = "0.5.1"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
+ "alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
- "is-terminal",
"itertools",
"num-traits",
- "once_cell",
"oorandom",
- "plotters",
- "rayon",
+ "page_size",
"regex",
"serde",
- "serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
@@ -142,39 +152,14 @@ dependencies = [
[[package]]
name = "criterion-plot"
-version = "0.5.0"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
"cast",
"itertools",
]
-[[package]]
-name = "crossbeam-deque"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
-dependencies = [
- "crossbeam-epoch",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.9.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
-
[[package]]
name = "crunchy"
version = "0.2.2"
@@ -187,6 +172,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
[[package]]
name = "half"
version = "2.4.1"
@@ -197,28 +188,11 @@ dependencies = [
"crunchy",
]
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
-
-[[package]]
-name = "is-terminal"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
-dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.52.0",
-]
-
[[package]]
name = "itertools"
-version = "0.10.5"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@@ -229,26 +203,11 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
-[[package]]
-name = "js-sys"
-version = "0.3.72"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
-dependencies = [
- "wasm-bindgen",
-]
-
[[package]]
name = "libc"
-version = "0.2.161"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
-
-[[package]]
-name = "log"
-version = "0.4.22"
+version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "memchr"
@@ -265,12 +224,6 @@ dependencies = [
"autocfg",
]
-[[package]]
-name = "once_cell"
-version = "1.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
-
[[package]]
name = "oorandom"
version = "11.1.4"
@@ -278,31 +231,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
[[package]]
-name = "plotters"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
-dependencies = [
- "num-traits",
- "plotters-backend",
- "plotters-svg",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "plotters-backend"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
-
-[[package]]
-name = "plotters-svg"
-version = "0.3.7"
+name = "page_size"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
- "plotters-backend",
+ "libc",
+ "winapi",
]
[[package]]
@@ -323,26 +258,6 @@ dependencies = [
"proc-macro2",
]
-[[package]]
-name = "rayon"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
-dependencies = [
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
-dependencies = [
- "crossbeam-deque",
- "crossbeam-utils",
-]
-
[[package]]
name = "regex"
version = "1.11.1"
@@ -419,9 +334,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
[[package]]
name = "shwild"
-version = "0.1.3"
+version = "0.1.4"
dependencies = [
"collect-rs",
"criterion",
@@ -476,69 +397,20 @@ dependencies = [
]
[[package]]
-name = "wasm-bindgen"
-version = "0.2.95"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
-dependencies = [
- "cfg-if",
- "once_cell",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.95"
+name = "winapi"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
- "bumpalo",
- "log",
- "once_cell",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.95"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.95"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
]
[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.95"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
-
-[[package]]
-name = "web-sys"
-version = "0.3.72"
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
@@ -546,17 +418,14 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys",
]
[[package]]
-name = "windows-sys"
-version = "0.52.0"
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
-dependencies = [
- "windows-targets",
-]
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
diff --git a/Cargo.toml b/Cargo.toml
index cb9475b..019941d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,11 +14,7 @@ categories = [
"development-tools",
"text-processing",
]
-description = """
-**shwild** is a small, standalone library, implemented in C++ with a C and a C++ API, that provides shell-compatible wildcard matching.
-
-**shwild.Rust** is a **Rust** port, with minimal API differences. The design emphasis is on simplicity-of-use, modularity, and performance.
-"""
+description = "Shell-compatible wildcard matching library"
documentation = "https://docs.rs/shwild"
edition = "2021"
exclude = [
@@ -35,7 +31,8 @@ license = "BSD-3-Clause"
name = "shwild"
readme = "README.md"
repository = "https://github.com/synesissoftware/shwild.Rust"
-version = "0.1.3"
+rust-version = "1.79"
+version = "0.1.4"
# ##########################################################
@@ -115,14 +112,19 @@ test-regex = [
[dependencies]
-collect-rs = { version = "0.2", optional = true }
-regex = { version = "1.11", optional = true }
+collect-rs = { version = "0.2", optional = true, default-features = false, features = [
+]}
+regex = { version = "1.11", optional = true, default-features = false, features = [
+]}
[dev-dependencies]
-criterion = { version = "0.5" }
-test_help-rs = { version = "0.1" }
+criterion = { version = "0.8", default-features = false, features = [
+ "html_reports",
+]}
+test_help-rs = { version = "0.1", default-features = false, features = [
+]}
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/EXAMPLES.md b/EXAMPLES.md
new file mode 100644
index 0000000..e1f50c6
--- /dev/null
+++ b/EXAMPLES.md
@@ -0,0 +1,11 @@
+# shwild.Rust Examples
+
+|Name|Source & Description|Summary|
+|---|---|---|
+|**list-matching-files**|[examples/list-matching-files/main.rs](/examples/list-matching-files/main.rs)
[examples/list-matching-files.md](/examples/list-matching-files.md)|Lists files in the current directory whose paths match one or more shell wildcard pattern(s), using `shwild::matches()`.|
+|**list-matching-files-compiled**|[examples/list-matching-files-compiled/main.rs](/examples/list-matching-files-compiled/main.rs)
[examples/list-matching-files-compiled.md](/examples/list-matching-files-compiled.md)|Same as **list-matching-files**, but patterns are compiled once into `CompiledMatcher` instances before matching.|
+|**character-play**|[test/scratch/character-play/main.rs](/test/scratch/character-play/main.rs)
[examples/character-play.md](/examples/character-play.md)|Scratch program exercising **regex** crate Unicode matching behaviour (requires feature `"test-regex"`).|
+
+
+
+
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 0000000..6d66629
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,12 @@
+# shwild.Rust - NEWS
+
+| Date | News Item |
+| --------------------- | ----------------------------------------- |
+| 8th July 2026 | shwild.Rust 0.1.4 released |
+| 28th March 2025 | shwild.Rust 0.1.3 released |
+| 3rd November 2024 | shwild.Rust 0.1.2 released |
+| 3rd November 2024 | shwild.Rust 0.1.1 released |
+| 3rd November 2024 | shwild.Rust 0.1.0 released |
+
+
+
diff --git a/README.md b/README.md
index 20649fa..cdd359e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,15 @@
-# shwild
-
-**SH**ell-compatible **WILD**cards, for **Rust**.
+# shwild.Rust
+
+[](https://opensource.org/licenses/BSD-3-Clause)
[](https://crates.io/crates/shwild)
+[](https://github.com/synesissoftware/shwild.Rust/releases/latest)
+
+[](https://github.com/synesissoftware/shwild.Rust/actions/workflows/ci.yml)
+[](https://github.com/synesissoftware/shwild.Rust/commits/master)
+[](https://docs.rs/shwild)
+
+**SH**ell-compatible **WILD**cards, for **Rust** — part of the cross-language **shwild** family.
## Table of Contents
@@ -30,9 +37,7 @@
## Introduction
-**shwild** is a small, standalone library, implemented in C++ with a C and a C++ API, that provides shell-compatible wildcard matching.
-
-**shwild.Rust** is a **Rust** port, with minimal API differences. The design emphasis is on simplicity-of-use, modularity, and performance.
+**shwild** is a small, standalone library, implemented in C++ with a C and a C++ API, that provides shell-compatible wildcard matching. **shwild.Rust** is a **Rust** port, with minimal API differences. The design emphasis is on simplicity-of-use, modularity, and performance.
```Rust
let pattern = r"Where are the* [🐼🐻]s\?";
@@ -64,7 +69,7 @@ The library (and other **shwild** variants) support the following pattern elemen
Reference in **Cargo.toml** in the usual way:
```toml
-shwild = { version = "~0.1" }
+shwild = { version = "0.1" }
```
@@ -104,6 +109,7 @@ The following crate features are defined:
| Name | Effect | Is `"default"`? | Dependent feature(s) |
| --------------------------- | ------------------------------------- | --------------- | ------------------------------------- |
| `"lookup-ranges"` | Causes match/non-match ranges to be implemented in terms of `UnicodePointMap` (from **collect-rs** crate), resulting in significant performance improvements in parsing and matching | Yes | |
+| `"null-feature"` | A feature that has no effect (and, thus, is useful for simplifying driver scripts) | **No** | |
| `"test-regex"` | Introduces a dependency to **regex** crate to support benchmark/example program(s) | **No** | |
@@ -167,7 +173,7 @@ No public traits are defined at this time.
## Examples
-T.B.C.
+Examples are provided in the ```examples``` directory, along with a markdown description for each. A detailed list TOC of them is provided in [EXAMPLES.md](./EXAMPLES.md).
## Project Information
@@ -186,8 +192,8 @@ Defect reports, feature requests, and pull requests are welcome on https://githu
**shwild.Rust** has two dependencies, both optional:
-* [**collect-rs**]() - required, for more efficient range matching, if feature `"lookup-ranges"` is specified;
-* [**regex**]() - required, by some benchmark/example programs only, if feature `"test-regex"` is specified;
+* [**collect-rs**](https://github.com/synesissoftware/collect-rs) - required, for more efficient range matching, if feature `"lookup-ranges"` is specified;
+* [**regex**](https://github.com/rust-lang/regex) - required, by some benchmark/example programs only, if feature `"test-regex"` is specified;
#### Dev Dependencies
@@ -200,7 +206,9 @@ Crates upon which **shwild** has development dependencies:
### Related projects
-None at this time.
+* [**shwild**](https://github.com/synesissoftware/shwild/);
+* [**shwild.Go**](https://github.com/synesissoftware/shwild.Go/);
+* [**collect-rs**](https://github.com/synesissoftware/collect-rs/);
### License
diff --git a/TODO.md b/TODO.md
index 57628ec..90c33f3 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,21 +3,101 @@
## Table of Contents
+- [0.1.4 boilerplate checklist](#014-boilerplate-checklist)
+- [Source layout (deferred)](#source-layout-deferred)
- [Functional improvements](#functional-improvements)
- [Performance improvements](#performance-improvements)
+## 0.1.4 boilerplate checklist
+
+### Cargo packaging
+
+* [x] `default-features = false` on optional and dev dependencies;
+* [x] `rust-version` (MSRV 1.79);
+* [x] `version = "0.1.4"`;
+* [x] upgraded **criterion** from 0.5 => 0.8;
+* [x] ~~narrow `exclude` (drop `.github`)~~ — **won't fix** (keep `exclude = ["target", ".github"]` since 0.1.3);
+
+### CI
+
+* [x] `.github/workflows/ci.yml` (test, clippy, examples, doc, fmt, checkers);
+* [x] Clippy, DOC_76, and test-naming fixes;
+* [x] MSRV job on 1.79 (`cargo check --lib --locked`);
+* [x] `check_test_names.py` allows `__CONSTRUCT__` padding around construct names;
+
+### Documentation
+
+* [x] back-filled **CHANGES.md** (0.1.0–0.1.3) and **0.1.4** entry;
+* [x] crate-level `//!` documentation;
+* [x] **EXAMPLES.md** and per-example `.md` files;
+* [x] **NEWS.md** release table (through 0.1.4);
+* [x] **README.md** badges, dependency links, related projects, `null-feature`;
+* [x] shortened `description` in **Cargo.toml**;
+
+### Formatting and quality tooling
+
+* [x] `.cargo/config.toml`;
+* [x] `.cursor/rules/rust-standards.mdc`;
+* [x] `.gitattributes`;
+* [x] `.gitignore` (`/scripts/__pycache__/`);
+* [x] `scripts/check_derives.py`, `check_doc_76.py`, `check_test_names.py`, `fmt`;
+* [x] renamed `.rustfmt.toml` => **rustfmt.toml**;
+
+### Layout and structure
+
+* [x] ~~flatten examples to `examples/*.rs`~~ — **won't fix** (keep `examples/*/main.rs`);
+* [x] ~~move **character-play** from `[[example]]` to `[[bin]]`~~ — **won't fix** (since 0.1.3);
+* [x] ~~normalise bench filenames to snake_case~~ — **won't fix** (keep current names);
+
+### Release
+
+* [ ] merge `boilerplate` branch and tag **0.1.4** on GitHub;
+* [ ] publish **0.1.4** to crates.io;
+
+
+## Source layout (deferred)
+
+Split `src/lib.rs` (~3,450 lines) into multiple source files. **Defer past 0.1.4**
+— behaviour-neutral refactor; ship as a dedicated follow-up change.
+
+**API unit tests remain in `src/lib.rs`** (`#[cfg(test)] mod tests { ... }` and
+`regex_comparision_tests`).
+
+Proposed module layout:
+
+```
+src/
+ lib.rs # crate docs, re-exports, matches(), shwild_matches! macro;
+ # #[cfg(test)] API unit tests (stay here)
+ constants.rs # IGNORE_CASE
+ error.rs # Error + Display/Error trait impls
+ traits.rs # Match trait
+ types.rs # CharacterRangeType
+ match_structures.rs # MatchLiteral, MatchRange, MatchWildN, ... + Match impls
+ utils.rs # MatcherSequence, parsing helpers, prepare_range_string
+ compiled_matcher.rs # CompiledMatcher, ParseState, parse logic
+```
+
+Suggested order of work:
+
+1. Extract internal modules (`match_structures`, `utils`, `compiled_matcher`, …)
+ — largest line-count reduction with lowest risk;
+2. Extract `error`, `constants`, `traits`, `types` — thin `lib.rs` façade
+ (Diagnosticism-style);
+3. Leave API tests in `lib.rs` (no `tests/mod.rs`).
+
+
## Functional improvements
* [ ] **no-std**;
-* [ ] ...;
+* [ ] unit-test macros;
## Performance improvements
* [ ] special cases (for compiled only) such as `"*brown*"` could just be `strstr()`;
-* [ ] ...;
+* [ ] thorough optimisation review (including optional "unsafe");
-
diff --git a/benches/cw-regex.rs b/benches/cw-regex.rs
index e890801..f4011af 100644
--- a/benches/cw-regex.rs
+++ b/benches/cw-regex.rs
@@ -2,17 +2,14 @@
#![allow(non_snake_case)]
-use shwild;
-
-use regex::Regex;
-
use criterion::{
- black_box,
criterion_group,
criterion_main,
Criterion,
};
+use regex::Regex;
+use std::hint::black_box;
mod constants {
pub(crate) const S_TQBFJOTLD : &str = "The quick brown fox jumps over the lazy dog";
diff --git a/benches/range_string-creation_functions.rs b/benches/range_string-creation_functions.rs
index a366992..a6e5fc0 100644
--- a/benches/range_string-creation_functions.rs
+++ b/benches/range_string-creation_functions.rs
@@ -1,18 +1,16 @@
// benches/range_string-creation_functions.rs : evaluates performance of different range-string creation-function approaches
#![allow(non_snake_case)]
-#![feature(ascii_char)]
-
-use shwild;
use criterion::{
- black_box,
criterion_group,
criterion_main,
// BenchmarkId,
Criterion,
};
+use std::hint::black_box;
+
mod constants {
#![allow(non_upper_case_globals)]
@@ -68,7 +66,7 @@ mod utils {
chars.dedup();
chars.into_iter().collect() // appears to be (marginally) faster for `Vec<>`, although it's very close
- // chars.as_slice().iter().collect()
+ // chars.as_slice().iter().collect()
}
pub(super) fn range_string_from_slice_1(
@@ -134,7 +132,7 @@ mod utils {
chars.dedup();
chars.into_iter().collect() // appears to be (marginally) faster for `Vec<>`, although it's very close
- // chars.as_slice().iter().collect()
+ // chars.as_slice().iter().collect()
}
pub(super) fn range_string_from_slice_3(
@@ -241,37 +239,6 @@ mod utils {
// chars.as_slice().iter().collect()
}
- #[allow(unexpected_cfgs)]
- #[cfg(feature = "auto-buffer")]
- pub(super) fn range_string_from_slice_6(
- chars : &[char],
- flags : i64,
- ) -> String {
- let mut chars = if 0 != (shwild::IGNORE_CASE & flags) {
- let mut ci_chars = auto_buffer::AutoBuffer::::with_capacity(chars.len() * 2);
-
- for (ix, c) in chars.iter().enumerate() {
- if c.is_alphabetic() {
- ci_chars.push(c.to_ascii_uppercase());
-
- ci_chars.push(c.to_ascii_lowercase());
- } else {
- ci_chars.push(*c);
- }
- }
-
- ci_chars
- } else {
- chars.into()
- };
-
- chars.sort_unstable();
-
- chars.dedup();
-
- chars.into_iter().collect()
- }
-
pub(super) fn range_string_from_slice_7(
chars : &[char],
flags : i64,
diff --git a/benches/shwild-compiled_matcher.rs b/benches/shwild-compiled_matcher.rs
index 8674850..bc7da1c 100644
--- a/benches/shwild-compiled_matcher.rs
+++ b/benches/shwild-compiled_matcher.rs
@@ -2,15 +2,14 @@
#![allow(non_snake_case)]
-use shwild;
-
use criterion::{
- black_box,
criterion_group,
criterion_main,
Criterion,
};
+use std::hint::black_box;
+
mod constants {
#![allow(non_upper_case_globals)]
diff --git a/benches/shwild-matches.rs b/benches/shwild-matches.rs
index e59e051..a2dec17 100644
--- a/benches/shwild-matches.rs
+++ b/benches/shwild-matches.rs
@@ -2,15 +2,14 @@
#![allow(non_snake_case)]
-use shwild;
-
use criterion::{
- black_box,
criterion_group,
criterion_main,
Criterion,
};
+use std::hint::black_box;
+
mod constants {
#![allow(non_upper_case_globals)]
diff --git a/examples/character-play.md b/examples/character-play.md
new file mode 100644
index 0000000..3266700
--- /dev/null
+++ b/examples/character-play.md
@@ -0,0 +1,99 @@
+# shwild.Rust Example - **character-play**
+
+## Summary
+
+A scratch program that exercises **regex** crate matching behaviour for Unicode text, including emoji and combining characters. It is intended to support development of the `"test-regex"` feature and the `regex_comparision_tests` unit tests; it does not call **shwild** APIs directly.
+
+
+## Source
+
+```Rust
+// test/scratch/character-play/main.rs : Unicode matching experiments with **regex**
+
+use regex::Regex;
+
+fn main() {
+ {
+ let re = Regex::new("abc").unwrap();
+
+ assert!(re.is_match("abc"));
+ assert!(re.is_match("abcd"));
+ }
+
+ {
+ let re = Regex::new("abc$").unwrap();
+
+ assert!(re.is_match("abc"));
+ assert!(!re.is_match("abcd"));
+ }
+
+ {
+ let re = Regex::new("a🐻c").unwrap();
+
+ assert!(re.is_match("a🐻c"));
+ assert!(!re.is_match("abc"));
+ assert!(re.is_match("a🐻cd"));
+ }
+
+ {
+ let re = Regex::new("aéc").unwrap();
+
+ assert!(re.is_match("aéc"));
+ assert!(!re.is_match("abc"));
+ assert!(re.is_match("aécd"));
+ }
+
+ {
+ let re = Regex::new("aéc").unwrap();
+
+ assert!(re.is_match("aéc"));
+ assert!(!re.is_match("abc"));
+ assert!(re.is_match("aécd"));
+ }
+
+ {
+ let re = Regex::new("a[🐻👀🛑]c").unwrap();
+
+ assert!(re.is_match("a🐻c"));
+ assert!(re.is_match("a👀c"));
+ assert!(re.is_match("a🛑c"));
+ assert!(!re.is_match("abc"));
+ assert!(re.is_match("a🛑cd"));
+ }
+
+ {
+ let re = Regex::new("a👁️c").unwrap();
+
+ assert!(!re.is_match("a🐻c"));
+ assert!(!re.is_match("a👀c"));
+ assert!(!re.is_match("a🛑c"));
+ assert!(re.is_match("a👁️c"));
+ assert!(!re.is_match("abc"));
+ assert!(!re.is_match("a🛑cd"));
+ }
+
+ {
+ let re = Regex::new("a[🐻👀🛑]c").unwrap();
+
+ assert!(re.is_match("a🐻c"));
+ assert!(re.is_match("a👀c"));
+ assert!(re.is_match("a🛑c"));
+ assert!(!re.is_match("abc"));
+ assert!(re.is_match("a🛑cd"));
+ }
+}
+```
+
+
+## Running and output
+
+When executed, as in:
+
+```bash
+$ cargo run --example character-play --features test-regex
+```
+
+it runs to completion with no output on success (all assertions pass).
+
+
+
diff --git a/examples/list-matching-files-compiled.md b/examples/list-matching-files-compiled.md
new file mode 100644
index 0000000..e426bff
--- /dev/null
+++ b/examples/list-matching-files-compiled.md
@@ -0,0 +1,113 @@
+# shwild.Rust Example - **list-matching-files-compiled**
+
+## Summary
+
+An example using **shwild.Rust**'s `CompiledMatcher` to list files in the current directory whose paths match one or more shell wildcard pattern(s) given on the command-line. Each pattern is parsed once up front; this is preferable when the same pattern(s) will be matched repeatedly. When no patterns are specified, `"*"` is assumed.
+
+
+## Source
+
+```Rust
+// examples/list-matching-files-compiled/main.rs : filter files using `CompiledMatcher`
+
+use std::{
+ env as std_env,
+ fs as std_fs,
+ process as std_process,
+};
+
+
+fn main() {
+ let directory = ".";
+
+ let patterns = {
+ let r = std_env::args().skip(1).collect::>();
+
+ if r.is_empty() {
+ vec!["*".into()]
+ } else {
+ r
+ }
+ };
+ let matchers = patterns
+ .iter()
+ .map(|pattern| {
+ shwild::CompiledMatcher::from_pattern_and_flags(&pattern, 0).unwrap_or_else(|e| {
+ eprintln!("failed to parse pattern '{pattern}': {e}");
+
+ std_process::exit(1);
+ })
+ })
+ .collect::>();
+
+ println!("searching in '{directory}' with pattern(s) {:?}", patterns);
+
+ match std_fs::read_dir(directory) {
+ Ok(entries) => {
+ // for each file in the directory ...
+ for entry in entries {
+ match entry {
+ Ok(entry) => {
+ let path = entry.path();
+ let path_s = format!("{}", path.display());
+
+ // ... check against ...
+ for matcher in &matchers {
+ // ... each pattern ...
+ if matcher.matches(&path_s) {
+ // ... and print when it matches any one.
+ println!("\t{path_s}");
+
+ break;
+ }
+ }
+ },
+ Err(e) => {
+ eprintln!("failed to read file in '{directory}': {e}");
+ },
+ };
+ }
+ },
+ Err(e) => {
+ eprintln!("failed to read files in '{directory}': {e}");
+ },
+ };
+}
+```
+
+
+## Running and output
+
+When executed, as in:
+
+```bash
+$ cargo run --example list-matching-files-compiled
+```
+
+it gives output similar to:
+
+```
+searching in '.' with pattern(s) ["*"]
+ ./Cargo.toml
+ ./LICENSE
+ ./README.md
+ ...
+```
+
+With pattern argument(s), as in:
+
+```bash
+$ cargo run --example list-matching-files-compiled -- '*.md'
+```
+
+it gives output similar to:
+
+```
+searching in '.' with pattern(s) ["*.md"]
+ ./README.md
+ ./CHANGES.md
+ ./TODO.md
+```
+
+
+
diff --git a/examples/list-matching-files-compiled/main.rs b/examples/list-matching-files-compiled/main.rs
index 1ea0aa2..edeb487 100644
--- a/examples/list-matching-files-compiled/main.rs
+++ b/examples/list-matching-files-compiled/main.rs
@@ -23,7 +23,7 @@ fn main() {
let matchers = patterns
.iter()
.map(|pattern| {
- shwild::CompiledMatcher::from_pattern_and_flags(&pattern, 0).unwrap_or_else(|e| {
+ shwild::CompiledMatcher::from_pattern_and_flags(pattern, 0).unwrap_or_else(|e| {
eprintln!("failed to parse pattern '{pattern}': {e}");
std_process::exit(1);
diff --git a/examples/list-matching-files.md b/examples/list-matching-files.md
new file mode 100644
index 0000000..93a84ca
--- /dev/null
+++ b/examples/list-matching-files.md
@@ -0,0 +1,109 @@
+# shwild.Rust Example - **list-matching-files**
+
+## Summary
+
+An example using **shwild.Rust**'s `matches()` function to list files in the current directory whose paths match one or more shell wildcard pattern(s) given on the command-line. When no patterns are specified, `"*"` is assumed.
+
+
+## Source
+
+```Rust
+// examples/list-matching-files/main.rs : filter files by name using `matches()`
+
+use std::{
+ env as std_env,
+ fs as std_fs,
+};
+
+
+fn main() {
+ let directory = ".";
+
+ let patterns = {
+ let r = std_env::args().skip(1).collect::>();
+
+ if r.is_empty() {
+ vec!["*".into()]
+ } else {
+ r
+ }
+ };
+
+ println!("searching in '{directory}' with pattern(s) {patterns:?}");
+
+ match std_fs::read_dir(directory) {
+ Ok(entries) => {
+ // for each file in the directory ...
+ for entry in entries {
+ match entry {
+ Ok(entry) => {
+ let path = entry.path();
+ let path_s = format!("{}", path.display());
+
+ // ... check against ...
+ for pattern in &patterns {
+ // ... each pattern ...
+ match shwild::matches(pattern, &path_s, 0) {
+ Ok(is_matched) => {
+ if is_matched {
+ // ... and print when it matches any one.
+ println!("\t{path_s}");
+
+ break;
+ }
+ },
+ Err(e) => {
+ eprintln!("failed to match against '{path_s}': {e}");
+ },
+ };
+ }
+ },
+ Err(e) => {
+ eprintln!("failed to read file in '{directory}': {e}");
+ },
+ };
+ }
+ },
+ Err(e) => {
+ eprintln!("failed to read files in '{directory}': {e}");
+ },
+ };
+}
+```
+
+
+## Running and output
+
+When executed, as in:
+
+```bash
+$ cargo run --example list-matching-files
+```
+
+it gives output similar to:
+
+```
+searching in '.' with pattern(s) ["*"]
+ ./Cargo.toml
+ ./LICENSE
+ ./README.md
+ ...
+```
+
+With pattern argument(s), as in:
+
+```bash
+$ cargo run --example list-matching-files -- '*.md'
+```
+
+it gives output similar to:
+
+```
+searching in '.' with pattern(s) ["*.md"]
+ ./README.md
+ ./CHANGES.md
+ ./TODO.md
+```
+
+
+
diff --git a/.rustfmt.toml b/rustfmt.toml
similarity index 92%
rename from .rustfmt.toml
rename to rustfmt.toml
index 41189d4..c08d358 100644
--- a/.rustfmt.toml
+++ b/rustfmt.toml
@@ -1,22 +1,24 @@
# rustfmt.toml for shwild.Rust
#
+# Requires nightly rustfmt with unstable features enabled — use ./scripts/fmt
+#
# configured for cargo-fmt 1.84.0
# array_width=60 # deprecated
# attr_fn_like_width=70 # deprecated
binop_separator="Front"
blank_lines_lower_bound=0
-blank_lines_upper_bound=2
+blank_lines_upper_bound=3
brace_style="SameLineWhere"
# chain_width=60 # deprecated
color="Auto"
combine_control_expr=false
-comment_width=100
+comment_width=76
condense_wildcard_suffixes=false
control_brace_style="AlwaysSameLine"
disable_all_formatting=false
-edition="2018"
+edition="2021"
empty_item_single_line=false
enum_discrim_align_threshold=0
error_on_line_overflow=false
@@ -75,7 +77,7 @@ tab_spaces=4 # Q: do we want to move to 2??
trailing_comma="Vertical"
trailing_semicolon=true
type_punctuation_density="Wide"
-# unstable_features=false
+unstable_features=true
use_field_init_shorthand=true
use_small_heuristics="Default"
use_try_shorthand=true
diff --git a/scripts/check_derives.py b/scripts/check_derives.py
new file mode 100755
index 0000000..5c595ec
--- /dev/null
+++ b/scripts/check_derives.py
@@ -0,0 +1,129 @@
+#! /usr/bin/env python3
+"""
+Verify DERIVE_LAYOUT: multi-trait `#[derive(...)]` macros must be split
+into separate single-trait lines, ordered alphabetically by trait name,
+except tightly coupled groups (Eq/PartialEq, Ord/PartialOrd).
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+
+COUPLED_TRAIT_GROUPS = [
+ ["Eq", "PartialEq"],
+ ["Ord", "PartialOrd"],
+]
+
+PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅
+FAIL = "\N{CROSS MARK}" # ❌
+
+
+def lint_file(filepath: Path) -> list[str]:
+ errors: list[str] = []
+
+ lines = filepath.read_text(encoding="utf-8").splitlines()
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ if not re.match(r"^\s*#\[derive\(", line):
+ i += 1
+ continue
+
+ derive_block: list[tuple[int, str]] = []
+ start_line_num = i + 1
+
+ while i < len(lines) and re.match(r"^\s*#\[derive\(", lines[i]):
+ derive_block.append((i + 1, lines[i]))
+ i += 1
+
+ parsed_lines: list[tuple[int, str, str]] = []
+ block_has_error = False
+
+ for line_num, line_str in derive_block:
+ match = re.search(r"#\[derive\((.*?)\)\]", line_str)
+
+ if not match:
+ continue
+
+ traits = [
+ t.strip()
+ for t in match.group(1).split(",")
+ if t.strip()
+ ]
+
+ if len(traits) > 1:
+ if traits not in COUPLED_TRAIT_GROUPS:
+ block_has_error = True
+ allowed = ", ".join(
+ f"'{', '.join(group)}'"
+ for group in COUPLED_TRAIT_GROUPS
+ )
+ errors.append(
+ f"{filepath}:{line_num}: multi-trait derive "
+ f"'{line_str.strip()}' is not allowed "
+ f"(except coupled groups: {allowed})",
+ )
+ elif len(traits) == 0:
+ block_has_error = True
+ errors.append(
+ f"{filepath}:{line_num}: empty derive attribute "
+ f"'{line_str.strip()}'",
+ )
+
+ sort_key = traits[0] if traits else ""
+ parsed_lines.append((line_num, line_str, sort_key))
+
+ if not block_has_error and len(parsed_lines) > 1:
+ sort_keys = [item[2] for item in parsed_lines]
+
+ if sort_keys != sorted(sort_keys):
+ actual = [item[1].strip() for item in parsed_lines]
+ expected = [
+ item[1].strip()
+ for item in sorted(parsed_lines, key=lambda x: x[2])
+ ]
+ errors.append(
+ f"{filepath}:{start_line_num}: derive attributes not "
+ f"sorted alphabetically\n"
+ f" actual: {actual}\n"
+ f" expected: {expected}",
+ )
+
+ return errors
+
+
+def main() -> int:
+ root = Path(__file__).resolve().parents[1]
+ errors: list[str] = []
+
+ for directory in ("src", "examples", "benches", "test"):
+ base = root / directory
+
+ if not base.is_dir():
+ continue
+
+ for path in sorted(base.rglob("*.rs")):
+ if "target" in path.parts:
+ continue
+
+ errors.extend(lint_file(path))
+
+ if errors:
+ print(
+ f"{FAIL} DERIVE_LAYOUT violations:",
+ file=sys.stderr,
+ )
+ print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr)
+ return 1
+
+ print(f"{PASS} DERIVE_LAYOUT: ok")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/check_doc_76.py b/scripts/check_doc_76.py
new file mode 100644
index 0000000..2a10562
--- /dev/null
+++ b/scripts/check_doc_76.py
@@ -0,0 +1,66 @@
+#! /usr/bin/env python3
+"""
+Verify DOC_76: public documentation comment lines are at most 76 characters.
+
+Code blocks inside doc comments (``` ... ```) are exempt, matching Synesis
+Information Systems' internal project standards.
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+
+DOC_LINE = re.compile(r"^\s*(//!|///)")
+
+PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅
+FAIL = "\N{CROSS MARK}" # ❌
+
+
+def iter_doc_violations(path: Path) -> list[str]:
+ violations: list[str] = []
+ in_codeblock = False
+
+ for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
+ stripped = line.rstrip()
+
+ if DOC_LINE.match(stripped) and re.search(r"\s*```\s*$", stripped):
+ in_codeblock = not in_codeblock
+ continue
+
+ if in_codeblock or not DOC_LINE.match(stripped):
+ continue
+
+ if len(stripped) > 76:
+ violations.append(
+ f"{path}:{line_no} ({len(stripped)} chars): {stripped}"
+ )
+
+ return violations
+
+
+def main() -> int:
+ root = Path(__file__).resolve().parents[1]
+ errors: list[str] = []
+
+ for path in sorted(root.rglob("*.rs")):
+ if "target" in path.parts:
+ continue
+ errors.extend(iter_doc_violations(path))
+
+ if errors:
+ print(
+ f"{FAIL} DOC_76 violations (doc comment lines must be <= 76 characters):",
+ file=sys.stderr,
+ )
+ print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr)
+ return 1
+
+ print(f"{PASS} DOC_76: ok")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/check_test_names.py b/scripts/check_test_names.py
new file mode 100644
index 0000000..f2084a9
--- /dev/null
+++ b/scripts/check_test_names.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+"""
+Verify RUST_TEST_NAMING: test functions and test modules use TEST_ prefix
+and SHOUTING_SNAKE_CASE, except words that name a specific Rust construct
+(type, function, macro, field, etc.) which must preserve exact case.
+
+When a construct name is embedded as a SHOUTING_SNAKE_CASE constant (or
+PascalCase construct), it may be delimited with an extra underscore on
+each side — e.g. HAVING__IGNORE_CASE__1.
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+
+TEST_ATTR = re.compile(r"^\s*#\[(\w+::)?test(\(\))?\]")
+FN_DEF = re.compile(r"^\s*fn\s+(\w+)")
+MOD_DEF = re.compile(r"^\s*mod\s+(\w+)")
+SNAKE_PART = re.compile(r"^[a-z][a-z0-9]*$")
+CONSECUTIVE_UPPER = re.compile(r"[A-Z]{2,}")
+
+PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅
+FAIL = "\N{CROSS MARK}" # ❌
+
+
+def is_pascal_case_atom(atom: str) -> bool:
+ return (
+ atom[0].isupper()
+ and any(c.islower() for c in atom)
+ and not CONSECUTIVE_UPPER.search(atom)
+ and atom.isalnum()
+ )
+
+
+def atom_violation(atom: str) -> str | None:
+ if atom.isupper() or atom.isdigit():
+ return None
+
+ if atom[0].islower() and all(SNAKE_PART.match(part) for part in atom.split("_")):
+ return None
+
+ if is_pascal_case_atom(atom):
+ return None
+
+ return (
+ f"segment '{atom}' must be SHOUTING_SNAKE_CASE, a PascalCase construct name, "
+ "or a Rust snake_case identifier"
+ )
+
+
+def parse_padded_construct(
+ segments: list[str], start: int
+) -> tuple[str | None, int, list[str]]:
+ """Parse __CONSTRUCT__ padding around a shouting or PascalCase atom."""
+ violations: list[str] = []
+ i = start
+ while i < len(segments) and not segments[i]:
+ i += 1
+ if i >= len(segments):
+ return None, i, [f"empty segment padding without construct"]
+
+ seg = segments[i]
+ atom: str | None = None
+
+ if seg.isupper():
+ parts = [seg]
+ i += 1
+ while i < len(segments) and segments[i] and segments[i].isupper():
+ parts.append(segments[i])
+ i += 1
+ atom = "_".join(parts)
+ reason = atom_violation(atom)
+ if reason:
+ violations.append(reason)
+ elif seg[0].isupper() and is_pascal_case_atom(seg):
+ atom = seg
+ reason = atom_violation(atom)
+ if reason:
+ violations.append(reason)
+ i += 1
+ else:
+ return None, start, [f"empty segment padding without construct"]
+
+ while i < len(segments) and not segments[i]:
+ i += 1
+
+ return atom, i, violations
+
+
+def parse_name_atoms(rest: str) -> tuple[list[str], list[str]]:
+ """Split a test name body into atoms; return (atoms, violations)."""
+ atoms: list[str] = []
+ violations: list[str] = []
+ segments = rest.split("_")
+ i = 0
+
+ while i < len(segments):
+ seg = segments[i]
+ if not seg:
+ atom, i, viols = parse_padded_construct(segments, i)
+ violations.extend(viols)
+ if atom:
+ atoms.append(atom)
+ elif not viols:
+ violations.append(f"empty segment in '{rest}'")
+ continue
+
+ if seg.isupper() or seg.isdigit():
+ reason = atom_violation(seg)
+ if reason:
+ violations.append(reason)
+ else:
+ atoms.append(seg)
+ i += 1
+ continue
+
+ if seg[0].isupper():
+ reason = atom_violation(seg)
+ if reason:
+ violations.append(reason)
+ else:
+ atoms.append(seg)
+ i += 1
+ continue
+
+ if SNAKE_PART.match(seg):
+ parts = [seg]
+ i += 1
+ while i < len(segments) and SNAKE_PART.match(segments[i]):
+ parts.append(segments[i])
+ i += 1
+ atom = "_".join(parts)
+ reason = atom_violation(atom)
+ if reason:
+ violations.append(reason)
+ else:
+ atoms.append(atom)
+ continue
+
+ violations.append(
+ f"segment '{seg}' must be SHOUTING_SNAKE_CASE, a PascalCase construct name, "
+ "or a Rust snake_case identifier"
+ )
+ i += 1
+
+ return atoms, violations
+
+
+def iter_name_violations(name: str) -> list[str]:
+ if not name.startswith("TEST_"):
+ return ["must start with 'TEST_'"]
+
+ rest = name[len("TEST_") :]
+ if not rest:
+ return ["must have a name after 'TEST_'"]
+
+ _, violations = parse_name_atoms(rest)
+ return violations
+
+
+def iter_test_results(path: Path, root: Path) -> list[tuple[bool, str]]:
+ results: list[tuple[bool, str]] = []
+ pending_test = False
+ display = path.relative_to(root)
+
+ for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
+ stripped = line.rstrip()
+
+ if TEST_ATTR.match(stripped):
+ pending_test = True
+ continue
+
+ if pending_test and stripped.startswith("#["):
+ continue
+
+ fn_match = FN_DEF.match(stripped)
+ if fn_match:
+ name = fn_match.group(1)
+ if pending_test:
+ pending_test = False
+ reasons = iter_name_violations(name)
+ label = f"{display}:{line_no}: test function '{name}'"
+ if reasons:
+ for reason in reasons:
+ results.append((False, f"{label}: {reason}"))
+ else:
+ results.append((True, label))
+ continue
+
+ mod_match = MOD_DEF.match(stripped)
+ if mod_match:
+ pending_test = False
+ name = mod_match.group(1)
+ if name.startswith("TEST_"):
+ reasons = iter_name_violations(name)
+ label = f"{display}:{line_no}: test module '{name}'"
+ if reasons:
+ for reason in reasons:
+ results.append((False, f"{label}: {reason}"))
+ else:
+ results.append((True, label))
+ continue
+
+ if stripped and not stripped.startswith("#") and stripped.endswith("{"):
+ pending_test = False
+
+ return results
+
+
+def main() -> int:
+ root = Path(__file__).resolve().parents[1]
+ results: list[tuple[bool, str]] = []
+
+ for path in sorted(root.rglob("*.rs")):
+ if "target" in path.parts:
+ continue
+ results.extend(iter_test_results(path, root))
+
+ failures = [line for ok, line in results if not ok]
+ if failures:
+ print(
+ f"{FAIL} RUST_TEST_NAMING violations "
+ "(test functions and modules must use TEST_ + SHOUTING_SNAKE_CASE):",
+ file=sys.stderr,
+ )
+ for ok, line in results:
+ mark = PASS if ok else FAIL
+ print(f" {mark} {line}", file=sys.stderr)
+ return 1
+
+ print(f"{PASS} RUST_TEST_NAMING: ok")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/fmt b/scripts/fmt
new file mode 100755
index 0000000..13731a1
--- /dev/null
+++ b/scripts/fmt
@@ -0,0 +1,12 @@
+#! /usr/bin/env bash
+set -euo pipefail
+
+RUSTFMT="$(rustup which --toolchain nightly rustfmt 2>/dev/null || true)"
+if [[ -z "${RUSTFMT}" ]]; then
+ echo "error: nightly rustfmt is required (see rustfmt.toml)" >&2
+ echo " rustup component add rustfmt --toolchain nightly" >&2
+ exit 1
+fi
+
+export RUSTFMT
+exec cargo fmt -- --unstable-features "$@"
diff --git a/src/lib.rs b/src/lib.rs
index 3812d38..51167f4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,51 @@
+//! Shell-compatible wildcard matching for Rust — part of the cross-language
+//! **shwild** family.
+//!
+//! **shwild** provides shell-style pattern matching: literals, `?` (any one
+//! character), `*` (any number of characters), `[`…`]` ranges, `[^`…`]`
+//! not-ranges, escapes, and case flags. **shwild.Rust** is a port with
+//! minimal API differences from the original **C/C++** library; the design
+//! emphasis is simplicity-of-use, modularity, and performance.
+//!
+//! (See [**shwild.Go**][sg] for the Go implementation.)
+//!
+//! # Installation
+//!
+//! Reference in **Cargo.toml** in the usual way:
+//!
+//! ```toml
+//! shwild = { version = "0.1" }
+//! ```
+//!
+//! # Components
+//!
+//! * [`matches()`] — parse `pattern` and test `input` in one step;
+//! * [`shwild_matches!`] — shorthand for [`matches()`] (2- or 3-arg);
+//! * [`CompiledMatcher`] — parse once, match many times;
+//! * [`Error`] and [`Result`] — parse/match error reporting;
+//! * [`IGNORE_CASE`] — flag for case-insensitive matching;
+//!
+//! # Features
+//!
+//! * `lookup-ranges` (default) — range matching via **collect-rs**
+//! `UnicodePointMap`;
+//! * `null-feature` — no effect; useful for driver scripts;
+//! * `test-regex` — optional **regex** dependency for benchmarks and
+//! scratch programs;
+//!
+//! # Examples
+//!
+//! ```
+//! use shwild::shwild_matches;
+//!
+//! assert_eq!(Ok(true), shwild_matches!("*.rs", "lib.rs"));
+//! ```
+//!
+//! Further examples are in the repository **examples** directory and in
+//! the project [README](https://github.com/synesissoftware/shwild.Rust).
+//!
+//! [sg]: https://github.com/synesissoftware/shwild.Go
+
// src/lib.rs : Definition of the shwild Rust package
// ///////////////////////////////////////////////
@@ -101,7 +149,6 @@ mod traits {
/// # Returns:
/// - `true` - indicates a full match; or
/// - `false` - if not a full match.
-
fn matches(
&self,
slice : &str,
@@ -300,11 +347,7 @@ mod match_structures {
let slice_starts_with_literal = slice.starts_with(&self.literal)
|| match &self.literal_uppercase {
Some(literal_uppercase) => {
- if slice.len() >= literal_uppercase.len() {
- slice.to_uppercase().starts_with(literal_uppercase)
- } else {
- false
- }
+ slice.len() >= literal_uppercase.len() && slice.to_uppercase().starts_with(literal_uppercase)
},
None => false,
};
@@ -426,13 +469,12 @@ mod match_structures {
mod tests {
#![allow(non_snake_case)]
+ #[cfg(not(feature = "lookup-ranges"))]
+ use super::super::utils::prepare_range_string;
#[cfg(feature = "lookup-ranges")]
use super::super::utils::prepare_range_upm_from_slice;
use super::{
- super::{
- traits::Match,
- utils::prepare_range_string,
- },
+ super::traits::Match,
MatchEnd,
MatchLiteral,
MatchNotRange,
@@ -1384,7 +1426,10 @@ pub type Result = std_result::Result;
/// # Examples:
///
/// ```
-/// let matcher = shwild::CompiledMatcher::from_pattern_and_flags("a[bc]c?", shwild::IGNORE_CASE).unwrap();
+/// let matcher = shwild::CompiledMatcher::from_pattern_and_flags(
+/// "a[bc]c?",
+/// shwild::IGNORE_CASE,
+/// ).unwrap();
///
/// assert!(matcher.matches("abcd"));
/// assert!(matcher.matches("accd"));
@@ -1889,7 +1934,6 @@ mod tests {
#![allow(non_snake_case)]
use crate as shwild;
- use crate::shwild_matches;
mod TEST_CompiledMatcher_PARSING {
@@ -3106,10 +3150,10 @@ mod tests {
assert!(!matcher.matches(""));
assert!(!matcher.matches("Where are the bears?"));
- assert_eq!(true, matcher.matches("Where are the 🐻s?"));
- assert_eq!(true, matcher.matches("Where are the 🐼s?"));
- assert_eq!(true, matcher.matches("Where are their 🐻s?"));
- assert_eq!(true, matcher.matches("Where are the big brown 🐻s?"));
+ assert!(matcher.matches("Where are the 🐻s?"));
+ assert!(matcher.matches("Where are the 🐼s?"));
+ assert!(matcher.matches("Where are their 🐻s?"));
+ assert!(matcher.matches("Where are the big brown 🐻s?"));
assert!(!matcher.matches("Where are the teddy-🐻s?"));
}
}