├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src └── lib.rs ├── tests └── glob-std.rs └── triagebot.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "tempdir" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install Rust (rustup) 22 | run: rustup update nightly --no-self-update && rustup default nightly 23 | - name: Run release-plz 24 | uses: MarcoIeni/release-plz-action@v0.5 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | CARGO_TERM_VERBOSE: true 5 | RUSTDOCFLAGS: -Dwarnings 6 | RUSTFLAGS: -Dwarnings 7 | 8 | on: 9 | pull_request: 10 | push: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | test: 16 | name: Tests 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | channel: 21 | - stable 22 | - nightly 23 | - 1.63.0 # MSRV of test dependencies 24 | os: 25 | - macos-13 # x86 MacOS 26 | - macos-15 # Arm MacOS 27 | - windows-2025 28 | - ubuntu-24.04 29 | include: 30 | - channel: beta 31 | os: ubuntu-24.04 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v4 36 | 37 | - name: Update rust 38 | run: | 39 | rustup default ${{ matrix.channel }} 40 | rustup update --no-self-update 41 | 42 | - run: cargo test --all 43 | 44 | clippy: 45 | name: Clippy 46 | runs-on: ubuntu-24.04 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | 51 | - name: Update rust 52 | run: | 53 | # use beta since it gives us near-latest fixes but isn't as volatile as nightly 54 | rustup default beta 55 | rustup component add clippy 56 | rustup update --no-self-update 57 | 58 | # FIXME(msrv): suggestions do not work in 1.23, nor dows `#![allow(clippy::...)]` 59 | - run: cargo clippy --all -- -Aclippy::while_let_loop 60 | 61 | msrv: 62 | name: Check building with the MSRV 63 | runs-on: ubuntu-24.04 64 | steps: 65 | - name: Checkout repository 66 | uses: actions/checkout@v4 67 | 68 | - name: Update rust 69 | run: | 70 | rustup default 1.23.0 71 | rustup update --no-self-update 72 | 73 | - run: cargo build 74 | 75 | rustfmt: 76 | name: Rustfmt 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@master 80 | - name: Install Rust 81 | run: | 82 | rustup default nightly 83 | rustup update --no-self-update 84 | rustup component add rustfmt 85 | - run: cargo fmt -- --check 86 | 87 | success: 88 | needs: 89 | - test 90 | - clippy 91 | - msrv 92 | - rustfmt 93 | runs-on: ubuntu-latest 94 | # GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency 95 | # failed" as success. So we have to do some contortions to ensure the job fails if any of its 96 | # dependencies fails. 97 | if: always() # make sure this is never "skipped" 98 | steps: 99 | # Manually check the status of all dependencies. `if: failure()` does not work. 100 | - name: check if any dependency failed 101 | run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.3.2](https://github.com/rust-lang/glob/compare/v0.3.1...v0.3.2) - 2024-12-28 8 | 9 | ## What's Changed 10 | * Add fs::symlink_metadata to detect broken symlinks by @kyoheiu in https://github.com/rust-lang/glob/pull/105 11 | * Add support for windows verbatim disk paths by @nico-abram in https://github.com/rust-lang/glob/pull/112 12 | * Respect `require_literal_leading_dot` option in `glob_with` method for path components by @JohnTitor in https://github.com/rust-lang/glob/pull/128 13 | * Harden tests for symlink by @JohnTitor in https://github.com/rust-lang/glob/pull/127 14 | * Remove "extern crate" directions from README by @zmitchell in https://github.com/rust-lang/glob/pull/131 15 | * Add FIXME for tempdir by @JohnTitor in https://github.com/rust-lang/glob/pull/126 16 | * Cache information about file type by @Kobzol in https://github.com/rust-lang/glob/pull/135 17 | * Document the behaviour of ** with files by @Wilfred in https://github.com/rust-lang/glob/pull/138 18 | * Add dependabot by @oriontvv in https://github.com/rust-lang/glob/pull/139 19 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/rust-lang/glob/pull/140 20 | * Check only (no longer test) at the MSRV by @tgross35 in https://github.com/rust-lang/glob/pull/151 21 | * Add release-plz for automated releases by @tgross35 in https://github.com/rust-lang/glob/pull/150 22 | 23 | ## New Contributors 24 | * @kyoheiu made their first contribution in https://github.com/rust-lang/glob/pull/105 25 | * @nico-abram made their first contribution in https://github.com/rust-lang/glob/pull/112 26 | * @zmitchell made their first contribution in https://github.com/rust-lang/glob/pull/131 27 | * @Kobzol made their first contribution in https://github.com/rust-lang/glob/pull/135 28 | * @Wilfred made their first contribution in https://github.com/rust-lang/glob/pull/138 29 | * @oriontvv made their first contribution in https://github.com/rust-lang/glob/pull/139 30 | * @dependabot made their first contribution in https://github.com/rust-lang/glob/pull/140 31 | * @tgross35 made their first contribution in https://github.com/rust-lang/glob/pull/151 32 | 33 | **Full Changelog**: https://github.com/rust-lang/glob/compare/0.3.1...0.3.2 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "glob" 3 | version = "0.3.2" 4 | authors = ["The Rust Project Developers"] 5 | license = "MIT OR Apache-2.0" 6 | homepage = "https://github.com/rust-lang/glob" 7 | repository = "https://github.com/rust-lang/glob" 8 | documentation = "https://docs.rs/glob" 9 | description = """ 10 | Support for matching file paths against Unix shell style patterns. 11 | """ 12 | categories = ["filesystem"] 13 | rust-version = "1.23.0" 14 | 15 | [dev-dependencies] 16 | # FIXME: This should be replaced by `tempfile` 17 | tempdir = "0.3" 18 | doc-comment = "0.3" 19 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | glob 2 | ==== 3 | 4 | Support for matching file paths against Unix shell style patterns. 5 | 6 | [![Continuous integration](https://github.com/rust-lang/glob/actions/workflows/rust.yml/badge.svg)](https://github.com/rust-lang/glob/actions/workflows/rust.yml) 7 | 8 | [Documentation](https://docs.rs/glob) 9 | 10 | ## Usage 11 | 12 | To use `glob`, add this to your `Cargo.toml`: 13 | 14 | ```toml 15 | [dependencies] 16 | glob = "0.3.2" 17 | ``` 18 | 19 | If you're using Rust 1.30 or earlier, or edition 2015, add this to your crate root: 20 | 21 | ```rust 22 | extern crate glob; 23 | ``` 24 | 25 | ## Examples 26 | 27 | Print all jpg files in /media/ and all of its subdirectories. 28 | 29 | ```rust 30 | use glob::glob; 31 | 32 | for entry in glob("/media/**/*.jpg").expect("Failed to read glob pattern") { 33 | match entry { 34 | Ok(path) => println!("{:?}", path.display()), 35 | Err(e) => println!("{:?}", e), 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | //! Support for matching file paths against Unix shell style patterns. 12 | //! 13 | //! The `glob` and `glob_with` functions allow querying the filesystem for all 14 | //! files that match a particular pattern (similar to the libc `glob` function). 15 | //! The methods on the `Pattern` type provide functionality for checking if 16 | //! individual paths match a particular pattern (similar to the libc `fnmatch` 17 | //! function). 18 | //! 19 | //! For consistency across platforms, and for Windows support, this module 20 | //! is implemented entirely in Rust rather than deferring to the libc 21 | //! `glob`/`fnmatch` functions. 22 | //! 23 | //! # Examples 24 | //! 25 | //! To print all jpg files in `/media/` and all of its subdirectories. 26 | //! 27 | //! ```rust,no_run 28 | //! use glob::glob; 29 | //! 30 | //! for entry in glob("/media/**/*.jpg").expect("Failed to read glob pattern") { 31 | //! match entry { 32 | //! Ok(path) => println!("{:?}", path.display()), 33 | //! Err(e) => println!("{:?}", e), 34 | //! } 35 | //! } 36 | //! ``` 37 | //! 38 | //! To print all files containing the letter "a", case insensitive, in a `local` 39 | //! directory relative to the current working directory. This ignores errors 40 | //! instead of printing them. 41 | //! 42 | //! ```rust,no_run 43 | //! use glob::glob_with; 44 | //! use glob::MatchOptions; 45 | //! 46 | //! let options = MatchOptions { 47 | //! case_sensitive: false, 48 | //! require_literal_separator: false, 49 | //! require_literal_leading_dot: false, 50 | //! }; 51 | //! for entry in glob_with("local/*a*", options).unwrap() { 52 | //! if let Ok(path) = entry { 53 | //! println!("{:?}", path.display()) 54 | //! } 55 | //! } 56 | //! ``` 57 | 58 | #![doc( 59 | html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png", 60 | html_favicon_url = "https://www.rust-lang.org/favicon.ico", 61 | html_root_url = "https://docs.rs/glob/0.3.1" 62 | )] 63 | #![deny(missing_docs)] 64 | 65 | #[cfg(test)] 66 | #[macro_use] 67 | extern crate doc_comment; 68 | 69 | #[cfg(test)] 70 | doctest!("../README.md"); 71 | 72 | use std::cmp; 73 | use std::cmp::Ordering; 74 | use std::error::Error; 75 | use std::fmt; 76 | use std::fs; 77 | use std::fs::DirEntry; 78 | use std::io; 79 | use std::ops::Deref; 80 | use std::path::{self, Component, Path, PathBuf}; 81 | use std::str::FromStr; 82 | 83 | use CharSpecifier::{CharRange, SingleChar}; 84 | use MatchResult::{EntirePatternDoesntMatch, Match, SubPatternDoesntMatch}; 85 | use PatternToken::AnyExcept; 86 | use PatternToken::{AnyChar, AnyRecursiveSequence, AnySequence, AnyWithin, Char}; 87 | 88 | /// An iterator that yields `Path`s from the filesystem that match a particular 89 | /// pattern. 90 | /// 91 | /// Note that it yields `GlobResult` in order to report any `IoErrors` that may 92 | /// arise during iteration. If a directory matches but is unreadable, 93 | /// thereby preventing its contents from being checked for matches, a 94 | /// `GlobError` is returned to express this. 95 | /// 96 | /// See the `glob` function for more details. 97 | #[derive(Debug)] 98 | pub struct Paths { 99 | dir_patterns: Vec, 100 | require_dir: bool, 101 | options: MatchOptions, 102 | todo: Vec>, 103 | scope: Option, 104 | } 105 | 106 | /// Return an iterator that produces all the `Path`s that match the given 107 | /// pattern using default match options, which may be absolute or relative to 108 | /// the current working directory. 109 | /// 110 | /// This may return an error if the pattern is invalid. 111 | /// 112 | /// This method uses the default match options and is equivalent to calling 113 | /// `glob_with(pattern, MatchOptions::new())`. Use `glob_with` directly if you 114 | /// want to use non-default match options. 115 | /// 116 | /// When iterating, each result is a `GlobResult` which expresses the 117 | /// possibility that there was an `IoError` when attempting to read the contents 118 | /// of the matched path. In other words, each item returned by the iterator 119 | /// will either be an `Ok(Path)` if the path matched, or an `Err(GlobError)` if 120 | /// the path (partially) matched _but_ its contents could not be read in order 121 | /// to determine if its contents matched. 122 | /// 123 | /// See the `Paths` documentation for more information. 124 | /// 125 | /// # Examples 126 | /// 127 | /// Consider a directory `/media/pictures` containing only the files 128 | /// `kittens.jpg`, `puppies.jpg` and `hamsters.gif`: 129 | /// 130 | /// ```rust,no_run 131 | /// use glob::glob; 132 | /// 133 | /// for entry in glob("/media/pictures/*.jpg").unwrap() { 134 | /// match entry { 135 | /// Ok(path) => println!("{:?}", path.display()), 136 | /// 137 | /// // if the path matched but was unreadable, 138 | /// // thereby preventing its contents from matching 139 | /// Err(e) => println!("{:?}", e), 140 | /// } 141 | /// } 142 | /// ``` 143 | /// 144 | /// The above code will print: 145 | /// 146 | /// ```ignore 147 | /// /media/pictures/kittens.jpg 148 | /// /media/pictures/puppies.jpg 149 | /// ``` 150 | /// 151 | /// If you want to ignore unreadable paths, you can use something like 152 | /// `filter_map`: 153 | /// 154 | /// ```rust 155 | /// use glob::glob; 156 | /// use std::result::Result; 157 | /// 158 | /// for path in glob("/media/pictures/*.jpg").unwrap().filter_map(Result::ok) { 159 | /// println!("{}", path.display()); 160 | /// } 161 | /// ``` 162 | /// Paths are yielded in alphabetical order. 163 | pub fn glob(pattern: &str) -> Result { 164 | glob_with(pattern, MatchOptions::new()) 165 | } 166 | 167 | /// Return an iterator that produces all the `Path`s that match the given 168 | /// pattern using the specified match options, which may be absolute or relative 169 | /// to the current working directory. 170 | /// 171 | /// This may return an error if the pattern is invalid. 172 | /// 173 | /// This function accepts Unix shell style patterns as described by 174 | /// `Pattern::new(..)`. The options given are passed through unchanged to 175 | /// `Pattern::matches_with(..)` with the exception that 176 | /// `require_literal_separator` is always set to `true` regardless of the value 177 | /// passed to this function. 178 | /// 179 | /// Paths are yielded in alphabetical order. 180 | pub fn glob_with(pattern: &str, options: MatchOptions) -> Result { 181 | #[cfg(windows)] 182 | fn check_windows_verbatim(p: &Path) -> bool { 183 | match p.components().next() { 184 | Some(Component::Prefix(ref p)) => { 185 | // Allow VerbatimDisk paths. std canonicalize() generates them, and they work fine 186 | p.kind().is_verbatim() 187 | && if let std::path::Prefix::VerbatimDisk(_) = p.kind() { 188 | false 189 | } else { 190 | true 191 | } 192 | } 193 | _ => false, 194 | } 195 | } 196 | #[cfg(not(windows))] 197 | fn check_windows_verbatim(_: &Path) -> bool { 198 | false 199 | } 200 | 201 | #[cfg(windows)] 202 | fn to_scope(p: &Path) -> PathBuf { 203 | // FIXME handle volume relative paths here 204 | p.to_path_buf() 205 | } 206 | #[cfg(not(windows))] 207 | fn to_scope(p: &Path) -> PathBuf { 208 | p.to_path_buf() 209 | } 210 | 211 | // make sure that the pattern is valid first, else early return with error 212 | let _ = Pattern::new(pattern)?; 213 | 214 | let mut components = Path::new(pattern).components().peekable(); 215 | loop { 216 | match components.peek() { 217 | Some(&Component::Prefix(..)) | Some(&Component::RootDir) => { 218 | components.next(); 219 | } 220 | _ => break, 221 | } 222 | } 223 | let rest = components.map(|s| s.as_os_str()).collect::(); 224 | let normalized_pattern = Path::new(pattern).iter().collect::(); 225 | let root_len = normalized_pattern.to_str().unwrap().len() - rest.to_str().unwrap().len(); 226 | let root = if root_len > 0 { 227 | Some(Path::new(&pattern[..root_len])) 228 | } else { 229 | None 230 | }; 231 | 232 | if root_len > 0 && check_windows_verbatim(root.unwrap()) { 233 | // FIXME: How do we want to handle verbatim paths? I'm inclined to 234 | // return nothing, since we can't very well find all UNC shares with a 235 | // 1-letter server name. 236 | return Ok(Paths { 237 | dir_patterns: Vec::new(), 238 | require_dir: false, 239 | options, 240 | todo: Vec::new(), 241 | scope: None, 242 | }); 243 | } 244 | 245 | let scope = root.map_or_else(|| PathBuf::from("."), to_scope); 246 | let scope = PathWrapper::from_path(scope); 247 | 248 | let mut dir_patterns = Vec::new(); 249 | let components = 250 | pattern[cmp::min(root_len, pattern.len())..].split_terminator(path::is_separator); 251 | 252 | for component in components { 253 | dir_patterns.push(Pattern::new(component)?); 254 | } 255 | 256 | if root_len == pattern.len() { 257 | dir_patterns.push(Pattern { 258 | original: "".to_string(), 259 | tokens: Vec::new(), 260 | is_recursive: false, 261 | }); 262 | } 263 | 264 | let last_is_separator = pattern.chars().next_back().map(path::is_separator); 265 | let require_dir = last_is_separator == Some(true); 266 | let todo = Vec::new(); 267 | 268 | Ok(Paths { 269 | dir_patterns, 270 | require_dir, 271 | options, 272 | todo, 273 | scope: Some(scope), 274 | }) 275 | } 276 | 277 | /// A glob iteration error. 278 | /// 279 | /// This is typically returned when a particular path cannot be read 280 | /// to determine if its contents match the glob pattern. This is possible 281 | /// if the program lacks the appropriate permissions, for example. 282 | #[derive(Debug)] 283 | pub struct GlobError { 284 | path: PathBuf, 285 | error: io::Error, 286 | } 287 | 288 | impl GlobError { 289 | /// The Path that the error corresponds to. 290 | pub fn path(&self) -> &Path { 291 | &self.path 292 | } 293 | 294 | /// The error in question. 295 | pub fn error(&self) -> &io::Error { 296 | &self.error 297 | } 298 | 299 | /// Consumes self, returning the _raw_ underlying `io::Error` 300 | pub fn into_error(self) -> io::Error { 301 | self.error 302 | } 303 | } 304 | 305 | impl Error for GlobError { 306 | #[allow(deprecated)] 307 | fn description(&self) -> &str { 308 | self.error.description() 309 | } 310 | 311 | #[allow(unknown_lints, bare_trait_objects)] 312 | fn cause(&self) -> Option<&Error> { 313 | Some(&self.error) 314 | } 315 | } 316 | 317 | impl fmt::Display for GlobError { 318 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 319 | write!( 320 | f, 321 | "attempting to read `{}` resulted in an error: {}", 322 | self.path.display(), 323 | self.error 324 | ) 325 | } 326 | } 327 | 328 | #[derive(Debug)] 329 | struct PathWrapper { 330 | path: PathBuf, 331 | is_directory: bool, 332 | } 333 | 334 | impl PathWrapper { 335 | fn from_dir_entry(path: PathBuf, e: DirEntry) -> Self { 336 | let is_directory = e 337 | .file_type() 338 | .ok() 339 | .and_then(|file_type| { 340 | // We need to use fs::metadata to resolve the actual path 341 | // if it's a symlink. 342 | if file_type.is_symlink() { 343 | None 344 | } else { 345 | Some(file_type.is_dir()) 346 | } 347 | }) 348 | .or_else(|| fs::metadata(&path).map(|m| m.is_dir()).ok()) 349 | .unwrap_or(false); 350 | Self { path, is_directory } 351 | } 352 | fn from_path(path: PathBuf) -> Self { 353 | let is_directory = fs::metadata(&path).map(|m| m.is_dir()).unwrap_or(false); 354 | Self { path, is_directory } 355 | } 356 | 357 | fn into_path(self) -> PathBuf { 358 | self.path 359 | } 360 | } 361 | 362 | impl Deref for PathWrapper { 363 | type Target = Path; 364 | 365 | fn deref(&self) -> &Self::Target { 366 | self.path.deref() 367 | } 368 | } 369 | 370 | impl AsRef for PathWrapper { 371 | fn as_ref(&self) -> &Path { 372 | self.path.as_ref() 373 | } 374 | } 375 | 376 | /// An alias for a glob iteration result. 377 | /// 378 | /// This represents either a matched path or a glob iteration error, 379 | /// such as failing to read a particular directory's contents. 380 | pub type GlobResult = Result; 381 | 382 | impl Iterator for Paths { 383 | type Item = GlobResult; 384 | 385 | fn next(&mut self) -> Option { 386 | // the todo buffer hasn't been initialized yet, so it's done at this 387 | // point rather than in glob() so that the errors are unified that is, 388 | // failing to fill the buffer is an iteration error construction of the 389 | // iterator (i.e. glob()) only fails if it fails to compile the Pattern 390 | if let Some(scope) = self.scope.take() { 391 | if !self.dir_patterns.is_empty() { 392 | // Shouldn't happen, but we're using -1 as a special index. 393 | assert!(self.dir_patterns.len() < std::usize::MAX); 394 | 395 | fill_todo(&mut self.todo, &self.dir_patterns, 0, &scope, self.options); 396 | } 397 | } 398 | 399 | loop { 400 | if self.dir_patterns.is_empty() || self.todo.is_empty() { 401 | return None; 402 | } 403 | 404 | let (path, mut idx) = match self.todo.pop().unwrap() { 405 | Ok(pair) => pair, 406 | Err(e) => return Some(Err(e)), 407 | }; 408 | 409 | // idx -1: was already checked by fill_todo, maybe path was '.' or 410 | // '..' that we can't match here because of normalization. 411 | if idx == std::usize::MAX { 412 | if self.require_dir && !path.is_directory { 413 | continue; 414 | } 415 | return Some(Ok(path.into_path())); 416 | } 417 | 418 | if self.dir_patterns[idx].is_recursive { 419 | let mut next = idx; 420 | 421 | // collapse consecutive recursive patterns 422 | while (next + 1) < self.dir_patterns.len() 423 | && self.dir_patterns[next + 1].is_recursive 424 | { 425 | next += 1; 426 | } 427 | 428 | if path.is_directory { 429 | // the path is a directory, so it's a match 430 | 431 | // push this directory's contents 432 | fill_todo( 433 | &mut self.todo, 434 | &self.dir_patterns, 435 | next, 436 | &path, 437 | self.options, 438 | ); 439 | 440 | if next == self.dir_patterns.len() - 1 { 441 | // pattern ends in recursive pattern, so return this 442 | // directory as a result 443 | return Some(Ok(path.into_path())); 444 | } else { 445 | // advanced to the next pattern for this path 446 | idx = next + 1; 447 | } 448 | } else if next == self.dir_patterns.len() - 1 { 449 | // not a directory and it's the last pattern, meaning no 450 | // match 451 | continue; 452 | } else { 453 | // advanced to the next pattern for this path 454 | idx = next + 1; 455 | } 456 | } 457 | 458 | // not recursive, so match normally 459 | if self.dir_patterns[idx].matches_with( 460 | { 461 | match path.file_name().and_then(|s| s.to_str()) { 462 | // FIXME (#9639): How do we handle non-utf8 filenames? 463 | // Ignore them for now; ideally we'd still match them 464 | // against a * 465 | None => continue, 466 | Some(x) => x, 467 | } 468 | }, 469 | self.options, 470 | ) { 471 | if idx == self.dir_patterns.len() - 1 { 472 | // it is not possible for a pattern to match a directory 473 | // *AND* its children so we don't need to check the 474 | // children 475 | 476 | if !self.require_dir || path.is_directory { 477 | return Some(Ok(path.into_path())); 478 | } 479 | } else { 480 | fill_todo( 481 | &mut self.todo, 482 | &self.dir_patterns, 483 | idx + 1, 484 | &path, 485 | self.options, 486 | ); 487 | } 488 | } 489 | } 490 | } 491 | } 492 | 493 | /// A pattern parsing error. 494 | #[derive(Debug)] 495 | #[allow(missing_copy_implementations)] 496 | pub struct PatternError { 497 | /// The approximate character index of where the error occurred. 498 | pub pos: usize, 499 | 500 | /// A message describing the error. 501 | pub msg: &'static str, 502 | } 503 | 504 | impl Error for PatternError { 505 | fn description(&self) -> &str { 506 | self.msg 507 | } 508 | } 509 | 510 | impl fmt::Display for PatternError { 511 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 512 | write!( 513 | f, 514 | "Pattern syntax error near position {}: {}", 515 | self.pos, self.msg 516 | ) 517 | } 518 | } 519 | 520 | /// A compiled Unix shell style pattern. 521 | /// 522 | /// - `?` matches any single character. 523 | /// 524 | /// - `*` matches any (possibly empty) sequence of characters. 525 | /// 526 | /// - `**` matches the current directory and arbitrary 527 | /// subdirectories. To match files in arbitrary subdiretories, use 528 | /// `**/*`. 529 | /// 530 | /// This sequence **must** form a single path component, so both 531 | /// `**a` and `b**` are invalid and will result in an error. A 532 | /// sequence of more than two consecutive `*` characters is also 533 | /// invalid. 534 | /// 535 | /// - `[...]` matches any character inside the brackets. Character sequences 536 | /// can also specify ranges of characters, as ordered by Unicode, so e.g. 537 | /// `[0-9]` specifies any character between 0 and 9 inclusive. An unclosed 538 | /// bracket is invalid. 539 | /// 540 | /// - `[!...]` is the negation of `[...]`, i.e. it matches any characters 541 | /// **not** in the brackets. 542 | /// 543 | /// - The metacharacters `?`, `*`, `[`, `]` can be matched by using brackets 544 | /// (e.g. `[?]`). When a `]` occurs immediately following `[` or `[!` then it 545 | /// is interpreted as being part of, rather then ending, the character set, so 546 | /// `]` and NOT `]` can be matched by `[]]` and `[!]]` respectively. The `-` 547 | /// character can be specified inside a character sequence pattern by placing 548 | /// it at the start or the end, e.g. `[abc-]`. 549 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] 550 | pub struct Pattern { 551 | original: String, 552 | tokens: Vec, 553 | is_recursive: bool, 554 | } 555 | 556 | /// Show the original glob pattern. 557 | impl fmt::Display for Pattern { 558 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 559 | self.original.fmt(f) 560 | } 561 | } 562 | 563 | impl FromStr for Pattern { 564 | type Err = PatternError; 565 | 566 | fn from_str(s: &str) -> Result { 567 | Self::new(s) 568 | } 569 | } 570 | 571 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 572 | enum PatternToken { 573 | Char(char), 574 | AnyChar, 575 | AnySequence, 576 | AnyRecursiveSequence, 577 | AnyWithin(Vec), 578 | AnyExcept(Vec), 579 | } 580 | 581 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 582 | enum CharSpecifier { 583 | SingleChar(char), 584 | CharRange(char, char), 585 | } 586 | 587 | #[derive(Copy, Clone, PartialEq)] 588 | enum MatchResult { 589 | Match, 590 | SubPatternDoesntMatch, 591 | EntirePatternDoesntMatch, 592 | } 593 | 594 | const ERROR_WILDCARDS: &str = "wildcards are either regular `*` or recursive `**`"; 595 | const ERROR_RECURSIVE_WILDCARDS: &str = "recursive wildcards must form a single path \ 596 | component"; 597 | const ERROR_INVALID_RANGE: &str = "invalid range pattern"; 598 | 599 | impl Pattern { 600 | /// This function compiles Unix shell style patterns. 601 | /// 602 | /// An invalid glob pattern will yield a `PatternError`. 603 | pub fn new(pattern: &str) -> Result { 604 | let chars = pattern.chars().collect::>(); 605 | let mut tokens = Vec::new(); 606 | let mut is_recursive = false; 607 | let mut i = 0; 608 | 609 | while i < chars.len() { 610 | match chars[i] { 611 | '?' => { 612 | tokens.push(AnyChar); 613 | i += 1; 614 | } 615 | '*' => { 616 | let old = i; 617 | 618 | while i < chars.len() && chars[i] == '*' { 619 | i += 1; 620 | } 621 | 622 | let count = i - old; 623 | 624 | match count.cmp(&2) { 625 | Ordering::Greater => { 626 | return Err(PatternError { 627 | pos: old + 2, 628 | msg: ERROR_WILDCARDS, 629 | }) 630 | } 631 | Ordering::Equal => { 632 | // ** can only be an entire path component 633 | // i.e. a/**/b is valid, but a**/b or a/**b is not 634 | // invalid matches are treated literally 635 | let is_valid = if i == 2 || path::is_separator(chars[i - count - 1]) { 636 | // it ends in a '/' 637 | if i < chars.len() && path::is_separator(chars[i]) { 638 | i += 1; 639 | true 640 | // or the pattern ends here 641 | // this enables the existing globbing mechanism 642 | } else if i == chars.len() { 643 | true 644 | // `**` ends in non-separator 645 | } else { 646 | return Err(PatternError { 647 | pos: i, 648 | msg: ERROR_RECURSIVE_WILDCARDS, 649 | }); 650 | } 651 | // `**` begins with non-separator 652 | } else { 653 | return Err(PatternError { 654 | pos: old - 1, 655 | msg: ERROR_RECURSIVE_WILDCARDS, 656 | }); 657 | }; 658 | 659 | if is_valid { 660 | // collapse consecutive AnyRecursiveSequence to a 661 | // single one 662 | 663 | let tokens_len = tokens.len(); 664 | 665 | if !(tokens_len > 1 666 | && tokens[tokens_len - 1] == AnyRecursiveSequence) 667 | { 668 | is_recursive = true; 669 | tokens.push(AnyRecursiveSequence); 670 | } 671 | } 672 | } 673 | Ordering::Less => tokens.push(AnySequence), 674 | } 675 | } 676 | '[' => { 677 | if i + 4 <= chars.len() && chars[i + 1] == '!' { 678 | match chars[i + 3..].iter().position(|x| *x == ']') { 679 | None => (), 680 | Some(j) => { 681 | let chars = &chars[i + 2..i + 3 + j]; 682 | let cs = parse_char_specifiers(chars); 683 | tokens.push(AnyExcept(cs)); 684 | i += j + 4; 685 | continue; 686 | } 687 | } 688 | } else if i + 3 <= chars.len() && chars[i + 1] != '!' { 689 | match chars[i + 2..].iter().position(|x| *x == ']') { 690 | None => (), 691 | Some(j) => { 692 | let cs = parse_char_specifiers(&chars[i + 1..i + 2 + j]); 693 | tokens.push(AnyWithin(cs)); 694 | i += j + 3; 695 | continue; 696 | } 697 | } 698 | } 699 | 700 | // if we get here then this is not a valid range pattern 701 | return Err(PatternError { 702 | pos: i, 703 | msg: ERROR_INVALID_RANGE, 704 | }); 705 | } 706 | c => { 707 | tokens.push(Char(c)); 708 | i += 1; 709 | } 710 | } 711 | } 712 | 713 | Ok(Self { 714 | tokens, 715 | original: pattern.to_string(), 716 | is_recursive, 717 | }) 718 | } 719 | 720 | /// Escape metacharacters within the given string by surrounding them in 721 | /// brackets. The resulting string will, when compiled into a `Pattern`, 722 | /// match the input string and nothing else. 723 | pub fn escape(s: &str) -> String { 724 | let mut escaped = String::new(); 725 | for c in s.chars() { 726 | match c { 727 | // note that ! does not need escaping because it is only special 728 | // inside brackets 729 | '?' | '*' | '[' | ']' => { 730 | escaped.push('['); 731 | escaped.push(c); 732 | escaped.push(']'); 733 | } 734 | c => { 735 | escaped.push(c); 736 | } 737 | } 738 | } 739 | escaped 740 | } 741 | 742 | /// Return if the given `str` matches this `Pattern` using the default 743 | /// match options (i.e. `MatchOptions::new()`). 744 | /// 745 | /// # Examples 746 | /// 747 | /// ```rust 748 | /// use glob::Pattern; 749 | /// 750 | /// assert!(Pattern::new("c?t").unwrap().matches("cat")); 751 | /// assert!(Pattern::new("k[!e]tteh").unwrap().matches("kitteh")); 752 | /// assert!(Pattern::new("d*g").unwrap().matches("doog")); 753 | /// ``` 754 | pub fn matches(&self, str: &str) -> bool { 755 | self.matches_with(str, MatchOptions::new()) 756 | } 757 | 758 | /// Return if the given `Path`, when converted to a `str`, matches this 759 | /// `Pattern` using the default match options (i.e. `MatchOptions::new()`). 760 | pub fn matches_path(&self, path: &Path) -> bool { 761 | // FIXME (#9639): This needs to handle non-utf8 paths 762 | path.to_str().map_or(false, |s| self.matches(s)) 763 | } 764 | 765 | /// Return if the given `str` matches this `Pattern` using the specified 766 | /// match options. 767 | pub fn matches_with(&self, str: &str, options: MatchOptions) -> bool { 768 | self.matches_from(true, str.chars(), 0, options) == Match 769 | } 770 | 771 | /// Return if the given `Path`, when converted to a `str`, matches this 772 | /// `Pattern` using the specified match options. 773 | pub fn matches_path_with(&self, path: &Path, options: MatchOptions) -> bool { 774 | // FIXME (#9639): This needs to handle non-utf8 paths 775 | path.to_str() 776 | .map_or(false, |s| self.matches_with(s, options)) 777 | } 778 | 779 | /// Access the original glob pattern. 780 | pub fn as_str(&self) -> &str { 781 | &self.original 782 | } 783 | 784 | fn matches_from( 785 | &self, 786 | mut follows_separator: bool, 787 | mut file: std::str::Chars, 788 | i: usize, 789 | options: MatchOptions, 790 | ) -> MatchResult { 791 | for (ti, token) in self.tokens[i..].iter().enumerate() { 792 | match *token { 793 | AnySequence | AnyRecursiveSequence => { 794 | // ** must be at the start. 795 | debug_assert!(match *token { 796 | AnyRecursiveSequence => follows_separator, 797 | _ => true, 798 | }); 799 | 800 | // Empty match 801 | match self.matches_from(follows_separator, file.clone(), i + ti + 1, options) { 802 | SubPatternDoesntMatch => (), // keep trying 803 | m => return m, 804 | }; 805 | 806 | while let Some(c) = file.next() { 807 | if follows_separator && options.require_literal_leading_dot && c == '.' { 808 | return SubPatternDoesntMatch; 809 | } 810 | follows_separator = path::is_separator(c); 811 | match *token { 812 | AnyRecursiveSequence if !follows_separator => continue, 813 | AnySequence 814 | if options.require_literal_separator && follows_separator => 815 | { 816 | return SubPatternDoesntMatch 817 | } 818 | _ => (), 819 | } 820 | match self.matches_from( 821 | follows_separator, 822 | file.clone(), 823 | i + ti + 1, 824 | options, 825 | ) { 826 | SubPatternDoesntMatch => (), // keep trying 827 | m => return m, 828 | } 829 | } 830 | } 831 | _ => { 832 | let c = match file.next() { 833 | Some(c) => c, 834 | None => return EntirePatternDoesntMatch, 835 | }; 836 | 837 | let is_sep = path::is_separator(c); 838 | 839 | if !match *token { 840 | AnyChar | AnyWithin(..) | AnyExcept(..) 841 | if (options.require_literal_separator && is_sep) 842 | || (follows_separator 843 | && options.require_literal_leading_dot 844 | && c == '.') => 845 | { 846 | false 847 | } 848 | AnyChar => true, 849 | AnyWithin(ref specifiers) => in_char_specifiers(specifiers, c, options), 850 | AnyExcept(ref specifiers) => !in_char_specifiers(specifiers, c, options), 851 | Char(c2) => chars_eq(c, c2, options.case_sensitive), 852 | AnySequence | AnyRecursiveSequence => unreachable!(), 853 | } { 854 | return SubPatternDoesntMatch; 855 | } 856 | follows_separator = is_sep; 857 | } 858 | } 859 | } 860 | 861 | // Iter is fused. 862 | if file.next().is_none() { 863 | Match 864 | } else { 865 | SubPatternDoesntMatch 866 | } 867 | } 868 | } 869 | 870 | // Fills `todo` with paths under `path` to be matched by `patterns[idx]`, 871 | // special-casing patterns to match `.` and `..`, and avoiding `readdir()` 872 | // calls when there are no metacharacters in the pattern. 873 | fn fill_todo( 874 | todo: &mut Vec>, 875 | patterns: &[Pattern], 876 | idx: usize, 877 | path: &PathWrapper, 878 | options: MatchOptions, 879 | ) { 880 | // convert a pattern that's just many Char(_) to a string 881 | fn pattern_as_str(pattern: &Pattern) -> Option { 882 | let mut s = String::new(); 883 | for token in &pattern.tokens { 884 | match *token { 885 | Char(c) => s.push(c), 886 | _ => return None, 887 | } 888 | } 889 | 890 | Some(s) 891 | } 892 | 893 | let add = |todo: &mut Vec<_>, next_path: PathWrapper| { 894 | if idx + 1 == patterns.len() { 895 | // We know it's good, so don't make the iterator match this path 896 | // against the pattern again. In particular, it can't match 897 | // . or .. globs since these never show up as path components. 898 | todo.push(Ok((next_path, std::usize::MAX))); 899 | } else { 900 | fill_todo(todo, patterns, idx + 1, &next_path, options); 901 | } 902 | }; 903 | 904 | let pattern = &patterns[idx]; 905 | let is_dir = path.is_directory; 906 | let curdir = path.as_ref() == Path::new("."); 907 | match pattern_as_str(pattern) { 908 | Some(s) => { 909 | // This pattern component doesn't have any metacharacters, so we 910 | // don't need to read the current directory to know where to 911 | // continue. So instead of passing control back to the iterator, 912 | // we can just check for that one entry and potentially recurse 913 | // right away. 914 | let special = "." == s || ".." == s; 915 | let next_path = if curdir { 916 | PathBuf::from(s) 917 | } else { 918 | path.join(&s) 919 | }; 920 | let next_path = PathWrapper::from_path(next_path); 921 | if (special && is_dir) 922 | || (!special 923 | && (fs::metadata(&next_path).is_ok() 924 | || fs::symlink_metadata(&next_path).is_ok())) 925 | { 926 | add(todo, next_path); 927 | } 928 | } 929 | None if is_dir => { 930 | let dirs = fs::read_dir(path).and_then(|d| { 931 | d.map(|e| { 932 | e.map(|e| { 933 | let path = if curdir { 934 | PathBuf::from(e.path().file_name().unwrap()) 935 | } else { 936 | e.path() 937 | }; 938 | PathWrapper::from_dir_entry(path, e) 939 | }) 940 | }) 941 | .collect::, _>>() 942 | }); 943 | match dirs { 944 | Ok(mut children) => { 945 | if options.require_literal_leading_dot { 946 | children 947 | .retain(|x| !x.file_name().unwrap().to_str().unwrap().starts_with('.')); 948 | } 949 | children.sort_by(|p1, p2| p2.file_name().cmp(&p1.file_name())); 950 | todo.extend(children.into_iter().map(|x| Ok((x, idx)))); 951 | 952 | // Matching the special directory entries . and .. that 953 | // refer to the current and parent directory respectively 954 | // requires that the pattern has a leading dot, even if the 955 | // `MatchOptions` field `require_literal_leading_dot` is not 956 | // set. 957 | if !pattern.tokens.is_empty() && pattern.tokens[0] == Char('.') { 958 | for &special in &[".", ".."] { 959 | if pattern.matches_with(special, options) { 960 | add(todo, PathWrapper::from_path(path.join(special))); 961 | } 962 | } 963 | } 964 | } 965 | Err(e) => { 966 | todo.push(Err(GlobError { 967 | path: path.to_path_buf(), 968 | error: e, 969 | })); 970 | } 971 | } 972 | } 973 | None => { 974 | // not a directory, nothing more to find 975 | } 976 | } 977 | } 978 | 979 | fn parse_char_specifiers(s: &[char]) -> Vec { 980 | let mut cs = Vec::new(); 981 | let mut i = 0; 982 | while i < s.len() { 983 | if i + 3 <= s.len() && s[i + 1] == '-' { 984 | cs.push(CharRange(s[i], s[i + 2])); 985 | i += 3; 986 | } else { 987 | cs.push(SingleChar(s[i])); 988 | i += 1; 989 | } 990 | } 991 | cs 992 | } 993 | 994 | fn in_char_specifiers(specifiers: &[CharSpecifier], c: char, options: MatchOptions) -> bool { 995 | for &specifier in specifiers.iter() { 996 | match specifier { 997 | SingleChar(sc) => { 998 | if chars_eq(c, sc, options.case_sensitive) { 999 | return true; 1000 | } 1001 | } 1002 | CharRange(start, end) => { 1003 | // FIXME: work with non-ascii chars properly (issue #1347) 1004 | if !options.case_sensitive && c.is_ascii() && start.is_ascii() && end.is_ascii() { 1005 | let start = start.to_ascii_lowercase(); 1006 | let end = end.to_ascii_lowercase(); 1007 | 1008 | let start_up = start.to_uppercase().next().unwrap(); 1009 | let end_up = end.to_uppercase().next().unwrap(); 1010 | 1011 | // only allow case insensitive matching when 1012 | // both start and end are within a-z or A-Z 1013 | if start != start_up && end != end_up { 1014 | let c = c.to_ascii_lowercase(); 1015 | if c >= start && c <= end { 1016 | return true; 1017 | } 1018 | } 1019 | } 1020 | 1021 | if c >= start && c <= end { 1022 | return true; 1023 | } 1024 | } 1025 | } 1026 | } 1027 | 1028 | false 1029 | } 1030 | 1031 | /// A helper function to determine if two chars are (possibly case-insensitively) equal. 1032 | fn chars_eq(a: char, b: char, case_sensitive: bool) -> bool { 1033 | if cfg!(windows) && path::is_separator(a) && path::is_separator(b) { 1034 | true 1035 | } else if !case_sensitive && a.is_ascii() && b.is_ascii() { 1036 | // FIXME: work with non-ascii chars properly (issue #9084) 1037 | a.eq_ignore_ascii_case(&b) 1038 | } else { 1039 | a == b 1040 | } 1041 | } 1042 | 1043 | /// Configuration options to modify the behaviour of `Pattern::matches_with(..)`. 1044 | #[allow(missing_copy_implementations)] 1045 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 1046 | pub struct MatchOptions { 1047 | /// Whether or not patterns should be matched in a case-sensitive manner. 1048 | /// This currently only considers upper/lower case relationships between 1049 | /// ASCII characters, but in future this might be extended to work with 1050 | /// Unicode. 1051 | pub case_sensitive: bool, 1052 | 1053 | /// Whether or not path-component separator characters (e.g. `/` on 1054 | /// Posix) must be matched by a literal `/`, rather than by `*` or `?` or 1055 | /// `[...]`. 1056 | pub require_literal_separator: bool, 1057 | 1058 | /// Whether or not paths that contain components that start with a `.` 1059 | /// will require that `.` appears literally in the pattern; `*`, `?`, `**`, 1060 | /// or `[...]` will not match. This is useful because such files are 1061 | /// conventionally considered hidden on Unix systems and it might be 1062 | /// desirable to skip them when listing files. 1063 | pub require_literal_leading_dot: bool, 1064 | } 1065 | 1066 | impl MatchOptions { 1067 | /// Constructs a new `MatchOptions` with default field values. This is used 1068 | /// when calling functions that do not take an explicit `MatchOptions` 1069 | /// parameter. 1070 | /// 1071 | /// This function always returns this value: 1072 | /// 1073 | /// ```rust,ignore 1074 | /// MatchOptions { 1075 | /// case_sensitive: true, 1076 | /// require_literal_separator: false, 1077 | /// require_literal_leading_dot: false 1078 | /// } 1079 | /// ``` 1080 | /// 1081 | /// # Note 1082 | /// The behavior of this method doesn't match `default()`'s. This returns 1083 | /// `case_sensitive` as `true` while `default()` does it as `false`. 1084 | // FIXME: Consider unity the behavior with `default()` in a next major release. 1085 | pub fn new() -> Self { 1086 | Self { 1087 | case_sensitive: true, 1088 | require_literal_separator: false, 1089 | require_literal_leading_dot: false, 1090 | } 1091 | } 1092 | } 1093 | 1094 | #[cfg(test)] 1095 | mod test { 1096 | use super::{glob, MatchOptions, Pattern}; 1097 | use std::path::Path; 1098 | 1099 | #[test] 1100 | fn test_pattern_from_str() { 1101 | assert!("a*b".parse::().unwrap().matches("a_b")); 1102 | assert!("a/**b".parse::().unwrap_err().pos == 4); 1103 | } 1104 | 1105 | #[test] 1106 | fn test_wildcard_errors() { 1107 | assert!(Pattern::new("a/**b").unwrap_err().pos == 4); 1108 | assert!(Pattern::new("a/bc**").unwrap_err().pos == 3); 1109 | assert!(Pattern::new("a/*****").unwrap_err().pos == 4); 1110 | assert!(Pattern::new("a/b**c**d").unwrap_err().pos == 2); 1111 | assert!(Pattern::new("a**b").unwrap_err().pos == 0); 1112 | } 1113 | 1114 | #[test] 1115 | fn test_unclosed_bracket_errors() { 1116 | assert!(Pattern::new("abc[def").unwrap_err().pos == 3); 1117 | assert!(Pattern::new("abc[!def").unwrap_err().pos == 3); 1118 | assert!(Pattern::new("abc[").unwrap_err().pos == 3); 1119 | assert!(Pattern::new("abc[!").unwrap_err().pos == 3); 1120 | assert!(Pattern::new("abc[d").unwrap_err().pos == 3); 1121 | assert!(Pattern::new("abc[!d").unwrap_err().pos == 3); 1122 | assert!(Pattern::new("abc[]").unwrap_err().pos == 3); 1123 | assert!(Pattern::new("abc[!]").unwrap_err().pos == 3); 1124 | } 1125 | 1126 | #[test] 1127 | fn test_glob_errors() { 1128 | assert!(glob("a/**b").err().unwrap().pos == 4); 1129 | assert!(glob("abc[def").err().unwrap().pos == 3); 1130 | } 1131 | 1132 | // this test assumes that there is a /root directory and that 1133 | // the user running this test is not root or otherwise doesn't 1134 | // have permission to read its contents 1135 | #[cfg(all(unix, not(target_os = "macos")))] 1136 | #[test] 1137 | fn test_iteration_errors() { 1138 | use std::io; 1139 | let mut iter = glob("/root/*").unwrap(); 1140 | 1141 | // GlobErrors shouldn't halt iteration 1142 | let next = iter.next(); 1143 | assert!(next.is_some()); 1144 | 1145 | let err = next.unwrap(); 1146 | assert!(err.is_err()); 1147 | 1148 | let err = err.err().unwrap(); 1149 | assert!(err.path() == Path::new("/root")); 1150 | assert!(err.error().kind() == io::ErrorKind::PermissionDenied); 1151 | } 1152 | 1153 | #[test] 1154 | fn test_absolute_pattern() { 1155 | assert!(glob("/").unwrap().next().is_some()); 1156 | assert!(glob("//").unwrap().next().is_some()); 1157 | 1158 | // assume that the filesystem is not empty! 1159 | assert!(glob("/*").unwrap().next().is_some()); 1160 | 1161 | #[cfg(not(windows))] 1162 | fn win() {} 1163 | 1164 | #[cfg(windows)] 1165 | fn win() { 1166 | use std::env::current_dir; 1167 | use std::path::Component; 1168 | 1169 | // check windows absolute paths with host/device components 1170 | let root_with_device = current_dir() 1171 | .ok() 1172 | .and_then(|p| match p.components().next().unwrap() { 1173 | Component::Prefix(prefix_component) => { 1174 | let path = Path::new(prefix_component.as_os_str()).join("*"); 1175 | Some(path.to_path_buf()) 1176 | } 1177 | _ => panic!("no prefix in this path"), 1178 | }) 1179 | .unwrap(); 1180 | // FIXME (#9639): This needs to handle non-utf8 paths 1181 | assert!(glob(root_with_device.as_os_str().to_str().unwrap()) 1182 | .unwrap() 1183 | .next() 1184 | .is_some()); 1185 | } 1186 | win() 1187 | } 1188 | 1189 | #[test] 1190 | fn test_wildcards() { 1191 | assert!(Pattern::new("a*b").unwrap().matches("a_b")); 1192 | assert!(Pattern::new("a*b*c").unwrap().matches("abc")); 1193 | assert!(!Pattern::new("a*b*c").unwrap().matches("abcd")); 1194 | assert!(Pattern::new("a*b*c").unwrap().matches("a_b_c")); 1195 | assert!(Pattern::new("a*b*c").unwrap().matches("a___b___c")); 1196 | assert!(Pattern::new("abc*abc*abc") 1197 | .unwrap() 1198 | .matches("abcabcabcabcabcabcabc")); 1199 | assert!(!Pattern::new("abc*abc*abc") 1200 | .unwrap() 1201 | .matches("abcabcabcabcabcabcabca")); 1202 | assert!(Pattern::new("a*a*a*a*a*a*a*a*a") 1203 | .unwrap() 1204 | .matches("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); 1205 | assert!(Pattern::new("a*b[xyz]c*d").unwrap().matches("abxcdbxcddd")); 1206 | } 1207 | 1208 | #[test] 1209 | fn test_recursive_wildcards() { 1210 | let pat = Pattern::new("some/**/needle.txt").unwrap(); 1211 | assert!(pat.matches("some/needle.txt")); 1212 | assert!(pat.matches("some/one/needle.txt")); 1213 | assert!(pat.matches("some/one/two/needle.txt")); 1214 | assert!(pat.matches("some/other/needle.txt")); 1215 | assert!(!pat.matches("some/other/notthis.txt")); 1216 | 1217 | // a single ** should be valid, for globs 1218 | // Should accept anything 1219 | let pat = Pattern::new("**").unwrap(); 1220 | assert!(pat.is_recursive); 1221 | assert!(pat.matches("abcde")); 1222 | assert!(pat.matches("")); 1223 | assert!(pat.matches(".asdf")); 1224 | assert!(pat.matches("/x/.asdf")); 1225 | 1226 | // collapse consecutive wildcards 1227 | let pat = Pattern::new("some/**/**/needle.txt").unwrap(); 1228 | assert!(pat.matches("some/needle.txt")); 1229 | assert!(pat.matches("some/one/needle.txt")); 1230 | assert!(pat.matches("some/one/two/needle.txt")); 1231 | assert!(pat.matches("some/other/needle.txt")); 1232 | assert!(!pat.matches("some/other/notthis.txt")); 1233 | 1234 | // ** can begin the pattern 1235 | let pat = Pattern::new("**/test").unwrap(); 1236 | assert!(pat.matches("one/two/test")); 1237 | assert!(pat.matches("one/test")); 1238 | assert!(pat.matches("test")); 1239 | 1240 | // /** can begin the pattern 1241 | let pat = Pattern::new("/**/test").unwrap(); 1242 | assert!(pat.matches("/one/two/test")); 1243 | assert!(pat.matches("/one/test")); 1244 | assert!(pat.matches("/test")); 1245 | assert!(!pat.matches("/one/notthis")); 1246 | assert!(!pat.matches("/notthis")); 1247 | 1248 | // Only start sub-patterns on start of path segment. 1249 | let pat = Pattern::new("**/.*").unwrap(); 1250 | assert!(pat.matches(".abc")); 1251 | assert!(pat.matches("abc/.abc")); 1252 | assert!(!pat.matches("ab.c")); 1253 | assert!(!pat.matches("abc/ab.c")); 1254 | } 1255 | 1256 | #[test] 1257 | fn test_lots_of_files() { 1258 | // this is a good test because it touches lots of differently named files 1259 | glob("/*/*/*/*").unwrap().skip(10000).next(); 1260 | } 1261 | 1262 | #[test] 1263 | fn test_range_pattern() { 1264 | let pat = Pattern::new("a[0-9]b").unwrap(); 1265 | for i in 0..10 { 1266 | assert!(pat.matches(&format!("a{}b", i))); 1267 | } 1268 | assert!(!pat.matches("a_b")); 1269 | 1270 | let pat = Pattern::new("a[!0-9]b").unwrap(); 1271 | for i in 0..10 { 1272 | assert!(!pat.matches(&format!("a{}b", i))); 1273 | } 1274 | assert!(pat.matches("a_b")); 1275 | 1276 | let pats = ["[a-z123]", "[1a-z23]", "[123a-z]"]; 1277 | for &p in pats.iter() { 1278 | let pat = Pattern::new(p).unwrap(); 1279 | for c in "abcdefghijklmnopqrstuvwxyz".chars() { 1280 | assert!(pat.matches(&c.to_string())); 1281 | } 1282 | for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars() { 1283 | let options = MatchOptions { 1284 | case_sensitive: false, 1285 | ..MatchOptions::new() 1286 | }; 1287 | assert!(pat.matches_with(&c.to_string(), options)); 1288 | } 1289 | assert!(pat.matches("1")); 1290 | assert!(pat.matches("2")); 1291 | assert!(pat.matches("3")); 1292 | } 1293 | 1294 | let pats = ["[abc-]", "[-abc]", "[a-c-]"]; 1295 | for &p in pats.iter() { 1296 | let pat = Pattern::new(p).unwrap(); 1297 | assert!(pat.matches("a")); 1298 | assert!(pat.matches("b")); 1299 | assert!(pat.matches("c")); 1300 | assert!(pat.matches("-")); 1301 | assert!(!pat.matches("d")); 1302 | } 1303 | 1304 | let pat = Pattern::new("[2-1]").unwrap(); 1305 | assert!(!pat.matches("1")); 1306 | assert!(!pat.matches("2")); 1307 | 1308 | assert!(Pattern::new("[-]").unwrap().matches("-")); 1309 | assert!(!Pattern::new("[!-]").unwrap().matches("-")); 1310 | } 1311 | 1312 | #[test] 1313 | fn test_pattern_matches() { 1314 | let txt_pat = Pattern::new("*hello.txt").unwrap(); 1315 | assert!(txt_pat.matches("hello.txt")); 1316 | assert!(txt_pat.matches("gareth_says_hello.txt")); 1317 | assert!(txt_pat.matches("some/path/to/hello.txt")); 1318 | assert!(txt_pat.matches("some\\path\\to\\hello.txt")); 1319 | assert!(txt_pat.matches("/an/absolute/path/to/hello.txt")); 1320 | assert!(!txt_pat.matches("hello.txt-and-then-some")); 1321 | assert!(!txt_pat.matches("goodbye.txt")); 1322 | 1323 | let dir_pat = Pattern::new("*some/path/to/hello.txt").unwrap(); 1324 | assert!(dir_pat.matches("some/path/to/hello.txt")); 1325 | assert!(dir_pat.matches("a/bigger/some/path/to/hello.txt")); 1326 | assert!(!dir_pat.matches("some/path/to/hello.txt-and-then-some")); 1327 | assert!(!dir_pat.matches("some/other/path/to/hello.txt")); 1328 | } 1329 | 1330 | #[test] 1331 | fn test_pattern_escape() { 1332 | let s = "_[_]_?_*_!_"; 1333 | assert_eq!(Pattern::escape(s), "_[[]_[]]_[?]_[*]_!_".to_string()); 1334 | assert!(Pattern::new(&Pattern::escape(s)).unwrap().matches(s)); 1335 | } 1336 | 1337 | #[test] 1338 | fn test_pattern_matches_case_insensitive() { 1339 | let pat = Pattern::new("aBcDeFg").unwrap(); 1340 | let options = MatchOptions { 1341 | case_sensitive: false, 1342 | require_literal_separator: false, 1343 | require_literal_leading_dot: false, 1344 | }; 1345 | 1346 | assert!(pat.matches_with("aBcDeFg", options)); 1347 | assert!(pat.matches_with("abcdefg", options)); 1348 | assert!(pat.matches_with("ABCDEFG", options)); 1349 | assert!(pat.matches_with("AbCdEfG", options)); 1350 | } 1351 | 1352 | #[test] 1353 | fn test_pattern_matches_case_insensitive_range() { 1354 | let pat_within = Pattern::new("[a]").unwrap(); 1355 | let pat_except = Pattern::new("[!a]").unwrap(); 1356 | 1357 | let options_case_insensitive = MatchOptions { 1358 | case_sensitive: false, 1359 | require_literal_separator: false, 1360 | require_literal_leading_dot: false, 1361 | }; 1362 | let options_case_sensitive = MatchOptions { 1363 | case_sensitive: true, 1364 | require_literal_separator: false, 1365 | require_literal_leading_dot: false, 1366 | }; 1367 | 1368 | assert!(pat_within.matches_with("a", options_case_insensitive)); 1369 | assert!(pat_within.matches_with("A", options_case_insensitive)); 1370 | assert!(!pat_within.matches_with("A", options_case_sensitive)); 1371 | 1372 | assert!(!pat_except.matches_with("a", options_case_insensitive)); 1373 | assert!(!pat_except.matches_with("A", options_case_insensitive)); 1374 | assert!(pat_except.matches_with("A", options_case_sensitive)); 1375 | } 1376 | 1377 | #[test] 1378 | fn test_pattern_matches_require_literal_separator() { 1379 | let options_require_literal = MatchOptions { 1380 | case_sensitive: true, 1381 | require_literal_separator: true, 1382 | require_literal_leading_dot: false, 1383 | }; 1384 | let options_not_require_literal = MatchOptions { 1385 | case_sensitive: true, 1386 | require_literal_separator: false, 1387 | require_literal_leading_dot: false, 1388 | }; 1389 | 1390 | assert!(Pattern::new("abc/def") 1391 | .unwrap() 1392 | .matches_with("abc/def", options_require_literal)); 1393 | assert!(!Pattern::new("abc?def") 1394 | .unwrap() 1395 | .matches_with("abc/def", options_require_literal)); 1396 | assert!(!Pattern::new("abc*def") 1397 | .unwrap() 1398 | .matches_with("abc/def", options_require_literal)); 1399 | assert!(!Pattern::new("abc[/]def") 1400 | .unwrap() 1401 | .matches_with("abc/def", options_require_literal)); 1402 | 1403 | assert!(Pattern::new("abc/def") 1404 | .unwrap() 1405 | .matches_with("abc/def", options_not_require_literal)); 1406 | assert!(Pattern::new("abc?def") 1407 | .unwrap() 1408 | .matches_with("abc/def", options_not_require_literal)); 1409 | assert!(Pattern::new("abc*def") 1410 | .unwrap() 1411 | .matches_with("abc/def", options_not_require_literal)); 1412 | assert!(Pattern::new("abc[/]def") 1413 | .unwrap() 1414 | .matches_with("abc/def", options_not_require_literal)); 1415 | } 1416 | 1417 | #[test] 1418 | fn test_pattern_matches_require_literal_leading_dot() { 1419 | let options_require_literal_leading_dot = MatchOptions { 1420 | case_sensitive: true, 1421 | require_literal_separator: false, 1422 | require_literal_leading_dot: true, 1423 | }; 1424 | let options_not_require_literal_leading_dot = MatchOptions { 1425 | case_sensitive: true, 1426 | require_literal_separator: false, 1427 | require_literal_leading_dot: false, 1428 | }; 1429 | 1430 | let f = |options| { 1431 | Pattern::new("*.txt") 1432 | .unwrap() 1433 | .matches_with(".hello.txt", options) 1434 | }; 1435 | assert!(f(options_not_require_literal_leading_dot)); 1436 | assert!(!f(options_require_literal_leading_dot)); 1437 | 1438 | let f = |options| { 1439 | Pattern::new(".*.*") 1440 | .unwrap() 1441 | .matches_with(".hello.txt", options) 1442 | }; 1443 | assert!(f(options_not_require_literal_leading_dot)); 1444 | assert!(f(options_require_literal_leading_dot)); 1445 | 1446 | let f = |options| { 1447 | Pattern::new("aaa/bbb/*") 1448 | .unwrap() 1449 | .matches_with("aaa/bbb/.ccc", options) 1450 | }; 1451 | assert!(f(options_not_require_literal_leading_dot)); 1452 | assert!(!f(options_require_literal_leading_dot)); 1453 | 1454 | let f = |options| { 1455 | Pattern::new("aaa/bbb/*") 1456 | .unwrap() 1457 | .matches_with("aaa/bbb/c.c.c.", options) 1458 | }; 1459 | assert!(f(options_not_require_literal_leading_dot)); 1460 | assert!(f(options_require_literal_leading_dot)); 1461 | 1462 | let f = |options| { 1463 | Pattern::new("aaa/bbb/.*") 1464 | .unwrap() 1465 | .matches_with("aaa/bbb/.ccc", options) 1466 | }; 1467 | assert!(f(options_not_require_literal_leading_dot)); 1468 | assert!(f(options_require_literal_leading_dot)); 1469 | 1470 | let f = |options| { 1471 | Pattern::new("aaa/?bbb") 1472 | .unwrap() 1473 | .matches_with("aaa/.bbb", options) 1474 | }; 1475 | assert!(f(options_not_require_literal_leading_dot)); 1476 | assert!(!f(options_require_literal_leading_dot)); 1477 | 1478 | let f = |options| { 1479 | Pattern::new("aaa/[.]bbb") 1480 | .unwrap() 1481 | .matches_with("aaa/.bbb", options) 1482 | }; 1483 | assert!(f(options_not_require_literal_leading_dot)); 1484 | assert!(!f(options_require_literal_leading_dot)); 1485 | 1486 | let f = |options| Pattern::new("**/*").unwrap().matches_with(".bbb", options); 1487 | assert!(f(options_not_require_literal_leading_dot)); 1488 | assert!(!f(options_require_literal_leading_dot)); 1489 | } 1490 | 1491 | #[test] 1492 | fn test_matches_path() { 1493 | // on windows, (Path::new("a/b").as_str().unwrap() == "a\\b"), so this 1494 | // tests that / and \ are considered equivalent on windows 1495 | assert!(Pattern::new("a/b").unwrap().matches_path(Path::new("a/b"))); 1496 | } 1497 | 1498 | #[test] 1499 | fn test_path_join() { 1500 | let pattern = Path::new("one").join(Path::new("**/*.rs")); 1501 | assert!(Pattern::new(pattern.to_str().unwrap()).is_ok()); 1502 | } 1503 | } 1504 | -------------------------------------------------------------------------------- /tests/glob-std.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2014 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | // ignore-windows TempDir may cause IoError on windows: #10462 12 | 13 | #![cfg_attr(test, deny(warnings))] 14 | 15 | extern crate glob; 16 | extern crate tempdir; 17 | 18 | use glob::{glob, glob_with}; 19 | use std::env; 20 | use std::fs; 21 | use std::path::PathBuf; 22 | use tempdir::TempDir; 23 | 24 | #[test] 25 | fn main() { 26 | fn mk_file(path: &str, directory: bool) { 27 | if directory { 28 | fs::create_dir(path).unwrap(); 29 | } else { 30 | fs::File::create(path).unwrap(); 31 | } 32 | } 33 | 34 | fn mk_symlink_file(original: &str, link: &str) { 35 | #[cfg(unix)] 36 | { 37 | use std::os::unix::fs::symlink; 38 | symlink(original, link).unwrap(); 39 | } 40 | #[cfg(windows)] 41 | { 42 | use std::os::windows::fs::symlink_file; 43 | symlink_file(original, link).unwrap(); 44 | } 45 | } 46 | 47 | fn mk_symlink_dir(original: &str, link: &str) { 48 | #[cfg(unix)] 49 | { 50 | use std::os::unix::fs::symlink; 51 | symlink(original, link).unwrap(); 52 | } 53 | #[cfg(windows)] 54 | { 55 | use std::os::windows::fs::symlink_dir; 56 | symlink_dir(original, link).unwrap(); 57 | } 58 | } 59 | 60 | fn glob_vec(pattern: &str) -> Vec { 61 | glob(pattern).unwrap().map(|r| r.unwrap()).collect() 62 | } 63 | 64 | fn glob_with_vec(pattern: &str, options: glob::MatchOptions) -> Vec { 65 | glob_with(pattern, options) 66 | .unwrap() 67 | .map(|r| r.unwrap()) 68 | .collect() 69 | } 70 | 71 | let root = TempDir::new("glob-tests"); 72 | let root = root.ok().expect("Should have created a temp directory"); 73 | assert!(env::set_current_dir(root.path()).is_ok()); 74 | 75 | mk_file("aaa", true); 76 | mk_file("aaa/apple", true); 77 | mk_file("aaa/orange", true); 78 | mk_file("aaa/tomato", true); 79 | mk_file("aaa/tomato/tomato.txt", false); 80 | mk_file("aaa/tomato/tomoto.txt", false); 81 | mk_file("bbb", true); 82 | mk_file("bbb/specials", true); 83 | mk_file("bbb/specials/!", false); 84 | // a valid symlink 85 | mk_symlink_file("aaa/apple", "aaa/green_apple"); 86 | // a broken symlink 87 | mk_symlink_file("aaa/setsuna", "aaa/kazusa"); 88 | 89 | // windows does not allow `*` or `?` characters to exist in filenames 90 | if env::consts::FAMILY != "windows" { 91 | mk_file("bbb/specials/*", false); 92 | mk_file("bbb/specials/?", false); 93 | } 94 | 95 | mk_file("bbb/specials/[", false); 96 | mk_file("bbb/specials/]", false); 97 | mk_file("ccc", true); 98 | mk_file("xyz", true); 99 | mk_file("xyz/x", false); 100 | mk_file("xyz/y", false); 101 | mk_file("xyz/z", false); 102 | 103 | mk_file("r", true); 104 | mk_file("r/current_dir.md", false); 105 | mk_file("r/one", true); 106 | mk_file("r/one/a.md", false); 107 | mk_file("r/one/another", true); 108 | mk_file("r/one/another/a.md", false); 109 | mk_file("r/one/another/deep", true); 110 | mk_file("r/one/another/deep/spelunking.md", false); 111 | mk_file("r/another", true); 112 | mk_file("r/another/a.md", false); 113 | mk_file("r/two", true); 114 | mk_file("r/two/b.md", false); 115 | mk_file("r/three", true); 116 | mk_file("r/three/c.md", false); 117 | 118 | mk_file("dirsym", true); 119 | mk_symlink_dir(root.path().join("r").to_str().unwrap(), "dirsym/link"); 120 | 121 | assert_eq!( 122 | glob_vec("dirsym/**/*.md"), 123 | vec!( 124 | PathBuf::from("dirsym/link/another/a.md"), 125 | PathBuf::from("dirsym/link/current_dir.md"), 126 | PathBuf::from("dirsym/link/one/a.md"), 127 | PathBuf::from("dirsym/link/one/another/a.md"), 128 | PathBuf::from("dirsym/link/one/another/deep/spelunking.md"), 129 | PathBuf::from("dirsym/link/three/c.md"), 130 | PathBuf::from("dirsym/link/two/b.md") 131 | ) 132 | ); 133 | 134 | // all recursive entities 135 | assert_eq!( 136 | glob_vec("r/**"), 137 | vec!( 138 | PathBuf::from("r/another"), 139 | PathBuf::from("r/one"), 140 | PathBuf::from("r/one/another"), 141 | PathBuf::from("r/one/another/deep"), 142 | PathBuf::from("r/three"), 143 | PathBuf::from("r/two") 144 | ) 145 | ); 146 | 147 | // std-canonicalized windows verbatim disk paths should work 148 | if env::consts::FAMILY == "windows" { 149 | let r_verbatim = PathBuf::from("r").canonicalize().unwrap(); 150 | assert_eq!( 151 | glob_vec(&format!("{}\\**", r_verbatim.display().to_string())) 152 | .into_iter() 153 | .map(|p| p.strip_prefix(&r_verbatim).unwrap().to_owned()) 154 | .collect::>(), 155 | vec!( 156 | PathBuf::from("another"), 157 | PathBuf::from("one"), 158 | PathBuf::from("one\\another"), 159 | PathBuf::from("one\\another\\deep"), 160 | PathBuf::from("three"), 161 | PathBuf::from("two") 162 | ) 163 | ); 164 | } 165 | 166 | // collapse consecutive recursive patterns 167 | assert_eq!( 168 | glob_vec("r/**/**"), 169 | vec!( 170 | PathBuf::from("r/another"), 171 | PathBuf::from("r/one"), 172 | PathBuf::from("r/one/another"), 173 | PathBuf::from("r/one/another/deep"), 174 | PathBuf::from("r/three"), 175 | PathBuf::from("r/two") 176 | ) 177 | ); 178 | 179 | assert_eq!( 180 | glob_vec("r/**/*"), 181 | vec!( 182 | PathBuf::from("r/another"), 183 | PathBuf::from("r/another/a.md"), 184 | PathBuf::from("r/current_dir.md"), 185 | PathBuf::from("r/one"), 186 | PathBuf::from("r/one/a.md"), 187 | PathBuf::from("r/one/another"), 188 | PathBuf::from("r/one/another/a.md"), 189 | PathBuf::from("r/one/another/deep"), 190 | PathBuf::from("r/one/another/deep/spelunking.md"), 191 | PathBuf::from("r/three"), 192 | PathBuf::from("r/three/c.md"), 193 | PathBuf::from("r/two"), 194 | PathBuf::from("r/two/b.md") 195 | ) 196 | ); 197 | 198 | // followed by a wildcard 199 | assert_eq!( 200 | glob_vec("r/**/*.md"), 201 | vec!( 202 | PathBuf::from("r/another/a.md"), 203 | PathBuf::from("r/current_dir.md"), 204 | PathBuf::from("r/one/a.md"), 205 | PathBuf::from("r/one/another/a.md"), 206 | PathBuf::from("r/one/another/deep/spelunking.md"), 207 | PathBuf::from("r/three/c.md"), 208 | PathBuf::from("r/two/b.md") 209 | ) 210 | ); 211 | 212 | // followed by a precise pattern 213 | assert_eq!( 214 | glob_vec("r/one/**/a.md"), 215 | vec!( 216 | PathBuf::from("r/one/a.md"), 217 | PathBuf::from("r/one/another/a.md") 218 | ) 219 | ); 220 | 221 | // followed by another recursive pattern 222 | // collapses consecutive recursives into one 223 | assert_eq!( 224 | glob_vec("r/one/**/**/a.md"), 225 | vec!( 226 | PathBuf::from("r/one/a.md"), 227 | PathBuf::from("r/one/another/a.md") 228 | ) 229 | ); 230 | 231 | // followed by two precise patterns 232 | assert_eq!( 233 | glob_vec("r/**/another/a.md"), 234 | vec!( 235 | PathBuf::from("r/another/a.md"), 236 | PathBuf::from("r/one/another/a.md") 237 | ) 238 | ); 239 | 240 | assert_eq!(glob_vec(""), Vec::::new()); 241 | assert_eq!(glob_vec("."), vec!(PathBuf::from("."))); 242 | assert_eq!(glob_vec(".."), vec!(PathBuf::from(".."))); 243 | 244 | assert_eq!(glob_vec("aaa"), vec!(PathBuf::from("aaa"))); 245 | assert_eq!(glob_vec("aaa/"), vec!(PathBuf::from("aaa"))); 246 | assert_eq!(glob_vec("a"), Vec::::new()); 247 | assert_eq!(glob_vec("aa"), Vec::::new()); 248 | assert_eq!(glob_vec("aaaa"), Vec::::new()); 249 | 250 | assert_eq!(glob_vec("aaa/apple"), vec!(PathBuf::from("aaa/apple"))); 251 | assert_eq!(glob_vec("aaa/apple/nope"), Vec::::new()); 252 | 253 | // windows should support both / and \ as directory separators 254 | if env::consts::FAMILY == "windows" { 255 | assert_eq!(glob_vec("aaa\\apple"), vec!(PathBuf::from("aaa/apple"))); 256 | } 257 | 258 | assert_eq!( 259 | glob_vec("???/"), 260 | vec!( 261 | PathBuf::from("aaa"), 262 | PathBuf::from("bbb"), 263 | PathBuf::from("ccc"), 264 | PathBuf::from("xyz") 265 | ) 266 | ); 267 | 268 | assert_eq!( 269 | glob_vec("aaa/tomato/tom?to.txt"), 270 | vec!( 271 | PathBuf::from("aaa/tomato/tomato.txt"), 272 | PathBuf::from("aaa/tomato/tomoto.txt") 273 | ) 274 | ); 275 | 276 | assert_eq!( 277 | glob_vec("xyz/?"), 278 | vec!( 279 | PathBuf::from("xyz/x"), 280 | PathBuf::from("xyz/y"), 281 | PathBuf::from("xyz/z") 282 | ) 283 | ); 284 | 285 | assert_eq!(glob_vec("a*"), vec!(PathBuf::from("aaa"))); 286 | assert_eq!(glob_vec("*a*"), vec!(PathBuf::from("aaa"))); 287 | assert_eq!(glob_vec("a*a"), vec!(PathBuf::from("aaa"))); 288 | assert_eq!(glob_vec("aaa*"), vec!(PathBuf::from("aaa"))); 289 | assert_eq!(glob_vec("*aaa"), vec!(PathBuf::from("aaa"))); 290 | assert_eq!(glob_vec("*aaa*"), vec!(PathBuf::from("aaa"))); 291 | assert_eq!(glob_vec("*a*a*a*"), vec!(PathBuf::from("aaa"))); 292 | assert_eq!(glob_vec("aaa*/"), vec!(PathBuf::from("aaa"))); 293 | 294 | assert_eq!( 295 | glob_vec("aaa/*"), 296 | vec!( 297 | PathBuf::from("aaa/apple"), 298 | PathBuf::from("aaa/green_apple"), 299 | PathBuf::from("aaa/kazusa"), 300 | PathBuf::from("aaa/orange"), 301 | PathBuf::from("aaa/tomato"), 302 | ) 303 | ); 304 | 305 | assert_eq!( 306 | glob_vec("aaa/*a*"), 307 | vec!( 308 | PathBuf::from("aaa/apple"), 309 | PathBuf::from("aaa/green_apple"), 310 | PathBuf::from("aaa/kazusa"), 311 | PathBuf::from("aaa/orange"), 312 | PathBuf::from("aaa/tomato") 313 | ) 314 | ); 315 | 316 | assert_eq!( 317 | glob_vec("*/*/*.txt"), 318 | vec!( 319 | PathBuf::from("aaa/tomato/tomato.txt"), 320 | PathBuf::from("aaa/tomato/tomoto.txt") 321 | ) 322 | ); 323 | 324 | assert_eq!( 325 | glob_vec("*/*/t[aob]m?to[.]t[!y]t"), 326 | vec!( 327 | PathBuf::from("aaa/tomato/tomato.txt"), 328 | PathBuf::from("aaa/tomato/tomoto.txt") 329 | ) 330 | ); 331 | 332 | assert_eq!(glob_vec("./aaa"), vec!(PathBuf::from("aaa"))); 333 | assert_eq!(glob_vec("./*"), glob_vec("*")); 334 | assert_eq!(glob_vec("*/..").pop().unwrap(), PathBuf::from("xyz/..")); 335 | assert_eq!(glob_vec("aaa/../bbb"), vec!(PathBuf::from("aaa/../bbb"))); 336 | assert_eq!(glob_vec("nonexistent/../bbb"), Vec::::new()); 337 | assert_eq!(glob_vec("aaa/tomato/tomato.txt/.."), Vec::::new()); 338 | 339 | assert_eq!(glob_vec("aaa/tomato/tomato.txt/"), Vec::::new()); 340 | 341 | // Ensure to find a broken symlink. 342 | assert_eq!(glob_vec("aaa/kazusa"), vec!(PathBuf::from("aaa/kazusa"))); 343 | 344 | assert_eq!(glob_vec("aa[a]"), vec!(PathBuf::from("aaa"))); 345 | assert_eq!(glob_vec("aa[abc]"), vec!(PathBuf::from("aaa"))); 346 | assert_eq!(glob_vec("a[bca]a"), vec!(PathBuf::from("aaa"))); 347 | assert_eq!(glob_vec("aa[b]"), Vec::::new()); 348 | assert_eq!(glob_vec("aa[xyz]"), Vec::::new()); 349 | assert_eq!(glob_vec("aa[]]"), Vec::::new()); 350 | 351 | assert_eq!(glob_vec("aa[!b]"), vec!(PathBuf::from("aaa"))); 352 | assert_eq!(glob_vec("aa[!bcd]"), vec!(PathBuf::from("aaa"))); 353 | assert_eq!(glob_vec("a[!bcd]a"), vec!(PathBuf::from("aaa"))); 354 | assert_eq!(glob_vec("aa[!a]"), Vec::::new()); 355 | assert_eq!(glob_vec("aa[!abc]"), Vec::::new()); 356 | 357 | assert_eq!( 358 | glob_vec("bbb/specials/[[]"), 359 | vec!(PathBuf::from("bbb/specials/[")) 360 | ); 361 | assert_eq!( 362 | glob_vec("bbb/specials/!"), 363 | vec!(PathBuf::from("bbb/specials/!")) 364 | ); 365 | assert_eq!( 366 | glob_vec("bbb/specials/[]]"), 367 | vec!(PathBuf::from("bbb/specials/]")) 368 | ); 369 | 370 | mk_file("i", true); 371 | mk_file("i/qwe", true); 372 | mk_file("i/qwe/.aaa", false); 373 | mk_file("i/qwe/.bbb", true); 374 | mk_file("i/qwe/.bbb/ccc", false); 375 | mk_file("i/qwe/.bbb/.ddd", false); 376 | mk_file("i/qwe/eee", false); 377 | 378 | let options = glob::MatchOptions { 379 | case_sensitive: false, 380 | require_literal_separator: true, 381 | require_literal_leading_dot: true, 382 | }; 383 | assert_eq!(glob_with_vec("i/**/*a*", options), Vec::::new()); 384 | assert_eq!(glob_with_vec("i/**/*c*", options), Vec::::new()); 385 | assert_eq!(glob_with_vec("i/**/*d*", options), Vec::::new()); 386 | assert_eq!( 387 | glob_with_vec("i/**/*e*", options), 388 | vec!(PathBuf::from("i/qwe"), PathBuf::from("i/qwe/eee")) 389 | ); 390 | 391 | if env::consts::FAMILY != "windows" { 392 | assert_eq!( 393 | glob_vec("bbb/specials/[*]"), 394 | vec!(PathBuf::from("bbb/specials/*")) 395 | ); 396 | assert_eq!( 397 | glob_vec("bbb/specials/[?]"), 398 | vec!(PathBuf::from("bbb/specials/?")) 399 | ); 400 | } 401 | 402 | if env::consts::FAMILY == "windows" { 403 | assert_eq!( 404 | glob_vec("bbb/specials/[![]"), 405 | vec!( 406 | PathBuf::from("bbb/specials/!"), 407 | PathBuf::from("bbb/specials/]") 408 | ) 409 | ); 410 | 411 | assert_eq!( 412 | glob_vec("bbb/specials/[!]]"), 413 | vec!( 414 | PathBuf::from("bbb/specials/!"), 415 | PathBuf::from("bbb/specials/[") 416 | ) 417 | ); 418 | 419 | assert_eq!( 420 | glob_vec("bbb/specials/[!!]"), 421 | vec!( 422 | PathBuf::from("bbb/specials/["), 423 | PathBuf::from("bbb/specials/]") 424 | ) 425 | ); 426 | } else { 427 | assert_eq!( 428 | glob_vec("bbb/specials/[![]"), 429 | vec!( 430 | PathBuf::from("bbb/specials/!"), 431 | PathBuf::from("bbb/specials/*"), 432 | PathBuf::from("bbb/specials/?"), 433 | PathBuf::from("bbb/specials/]") 434 | ) 435 | ); 436 | 437 | assert_eq!( 438 | glob_vec("bbb/specials/[!]]"), 439 | vec!( 440 | PathBuf::from("bbb/specials/!"), 441 | PathBuf::from("bbb/specials/*"), 442 | PathBuf::from("bbb/specials/?"), 443 | PathBuf::from("bbb/specials/[") 444 | ) 445 | ); 446 | 447 | assert_eq!( 448 | glob_vec("bbb/specials/[!!]"), 449 | vec!( 450 | PathBuf::from("bbb/specials/*"), 451 | PathBuf::from("bbb/specials/?"), 452 | PathBuf::from("bbb/specials/["), 453 | PathBuf::from("bbb/specials/]") 454 | ) 455 | ); 456 | 457 | assert_eq!( 458 | glob_vec("bbb/specials/[!*]"), 459 | vec!( 460 | PathBuf::from("bbb/specials/!"), 461 | PathBuf::from("bbb/specials/?"), 462 | PathBuf::from("bbb/specials/["), 463 | PathBuf::from("bbb/specials/]") 464 | ) 465 | ); 466 | 467 | assert_eq!( 468 | glob_vec("bbb/specials/[!?]"), 469 | vec!( 470 | PathBuf::from("bbb/specials/!"), 471 | PathBuf::from("bbb/specials/*"), 472 | PathBuf::from("bbb/specials/["), 473 | PathBuf::from("bbb/specials/]") 474 | ) 475 | ); 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /triagebot.toml: -------------------------------------------------------------------------------- 1 | [assign] 2 | --------------------------------------------------------------------------------