packages feed

gitignore-ospath (empty) → 0.1.0

raw patch · 6 files changed

+767/−0 lines, 6 filesdep +basedep +filepathdep +gitignore-ospath

Dependencies added: base, filepath, gitignore-ospath, hspec, os-string, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for gitignore-ospath++## 0.1.0 -- 2026-01-02++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,201 @@+                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright [yyyy] [name of copyright owner]++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ gitignore-ospath.cabal view
@@ -0,0 +1,53 @@+cabal-version:      3.0+name:               gitignore-ospath+version:            0.1.0+synopsis:           A compact Haskell library for reading `.gitignore` files and filtering paths.+description:        A compact Haskell library for reading `.gitignore` files and filtering paths.+                    .+                    The library works with `.gitignore`-like formats (e.g., `.dockerignore`) and uses+                    'System.OsPath.OsPath' for path handling. Parsing and filtering rules follow the+                    Git documentation.+homepage:           https://github.com/federicotdn/gitignore-ospath+bug-reports:        https://github.com/federicotdn/gitignore-ospath/issues+license:            Apache-2.0+license-file:       LICENSE+author:             Federico Tedin+maintainer:         federicotedin@gmail.com+category:           Development+build-type:         Simple+extra-doc-files:    CHANGELOG.md++source-repository head+  type:     git+  location: https://github.com/federicotdn/gitignore-ospath++common warnings+    ghc-options: -Wall++common shared-deps+    default-language: Haskell2010+    build-depends:    base >=4.14.0.0 && <5+                    , os-string >=2.0.0 && < 3+                    , filepath >=1.5.0.0 && < 2+                    , text >=2.1.0 && < 3+    default-extensions: OverloadedStrings++library+    import:           warnings+                    , shared-deps+    exposed-modules:  Ignore+    other-modules:    Ignore.Internal+    hs-source-dirs:   src++test-suite gitignore-ospath-test+    import:           warnings+                    , shared-deps+    type:             exitcode-stdio-1.0+    hs-source-dirs:   test+                    , src+    main-is:          IgnoreSpec.hs+    other-modules:    Ignore+                    , Ignore.Internal+    build-depends:    base >=4.14.0.0 && <5+                    , hspec ^>=2.11.16+                    , gitignore-ospath
+ src/Ignore.hs view
@@ -0,0 +1,10 @@+-- | Gitignore pattern matching for 'System.OsPath.OsPath'.+module Ignore+  ( Ignore (..),+    parse,+    ignores,+    ignores',+  )+where++import Ignore.Internal
+ src/Ignore/Internal.hs view
@@ -0,0 +1,277 @@+module Ignore.Internal where++import Data.List (tails)+import Data.Maybe (isJust, mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.IO.Encoding (utf16le, utf8)+import System.OsPath (OsPath, splitDirectories)+import System.OsString (OsChar, encodeWith)+import qualified System.OsString as OS+import Text.ParserCombinators.ReadP (ReadP)+import qualified Text.ParserCombinators.ReadP as R++data GlobClassPart+  = ClassSingle OsChar+  | ClassRange (OsChar, OsChar)+  | ClassSep+  deriving (Show, Eq)++data GlobPart+  = Wildcard Bool+  | Single OsChar+  | Class [GlobClassPart]+  | Noop+  deriving (Show, Eq)++data Segment+  = DAsterisk+  | Asterisk+  | Literal OsPath+  | Prefix OsPath+  | Suffix OsPath+  | Glob [GlobPart]+  deriving (Show, Eq)++data Pattern = Pattern+  { pSegments :: [Segment],+    pDir :: Bool,+    pNegated :: Bool,+    pAnchored :: Bool+  }+  deriving (Show, Eq)++-- | The parsed contents of a gitignore file.+--+-- Multiple 'Ignore' values can be combined using '<>' to merge their patterns.+-- This is useful for combining patterns from multiple gitignore files (e.g.,+-- a global gitignore and a repository-specific one). Later patterns take+-- precedence when determining if a path should be ignored.+newtype Ignore = Ignore [Pattern] deriving (Show, Eq)++-- | Combine gitignore files by appending their patterns.+--+-- When combining @ignore1 <> ignore2@, patterns from @ignore2@ are appended+-- after @ignore1@, meaning later patterns take precedence for matching.+instance Semigroup Ignore where+  (Ignore p1) <> (Ignore p2) = Ignore (p1 <> p2)++-- TODO: This is probably overkill for encoding just one char.+encodeChar :: Char -> Maybe OsChar+encodeChar ch =+  let str = [ch]+      encoded = OS.unpack <$> encodeWith utf8 utf16le str+   in case encoded of+        Left _ -> Nothing+        Right [] -> Nothing+        Right (osch : _) -> Just osch++getOsChar :: ReadP (Char, OsChar)+getOsChar = do+  ch <- R.get+  let osch = encodeChar ch+  case osch of+    Just osch' -> pure (ch, osch')+    Nothing -> R.pfail++finishRange :: (OsChar, OsChar) -> [GlobClassPart]+finishRange (start, end) =+  if start < end+    then [ClassRange (start, end), ClassSep]+    else [ClassSingle start, ClassSep]++parseGlobInner :: [GlobPart] -> Maybe [GlobClassPart] -> ReadP [GlobPart]+parseGlobInner curr class_ = do+  atEnd <- null <$> R.look+  case class_ of+    -- We are outside a class definition e.g. ab|cd[efg]+    Nothing ->+      if atEnd+        then return curr -- Finished!+        else do+          (ch, osch) <- getOsChar+          case ch of+            '[' -> contWith Noop (Just []) -- Start building a class.+            '?' -> contWith (Wildcard False) Nothing+            '*' -> contWith (Wildcard True) Nothing+            '\\' -> do+              -- Maybe read one more char.+              (_, osch') <- getOsChar+              contWith (Single osch') Nothing+            _ -> contWith (Single osch) Nothing -- Includes ']' case.++    -- We are inside a class definition e.g. abcd[e|fg]+    Just class_' -> do+      -- The line below will fail if there's nothing to read, i.e.+      -- the class was opened but never closed.+      (ch, osch) <- getOsChar+      case ch of+        ']' -> do+          let classParts = filter (/= ClassSep) class_'+          contWith (if null classParts then Noop else Class classParts) Nothing+        '-' -> do+          case class_' of+            [] -> contWith Noop $ Just [ClassSingle osch]+            elems -> case last elems of+              ClassRange (start, _) -> contWith Noop $ Just (init class_' ++ finishRange (start, osch))+              -- Start building a range.+              ClassSingle cs -> contWith Noop $ Just (init class_' ++ [ClassRange (cs, cs)])+              ClassSep -> contWith Noop $ Just (class_' ++ [ClassSingle osch]) -- Just add '-'.+        _ -> do+          -- Maybe read one more char.+          (_, osch') <- if ch == '\\' then getOsChar else pure (ch, osch)+          case class_' of+            [] -> contWith Noop $ Just [ClassSingle osch']+            elems -> case last elems of+              ClassRange (start, _) -> contWith Noop $ Just (init class_' ++ finishRange (start, osch'))+              _ -> contWith Noop $ Just (class_' ++ [ClassSingle osch'])+  where+    contWith p = parseGlobInner (curr ++ [p])++parseGlob :: Text -> Maybe Segment+parseGlob source = do+  let result = R.readP_to_S (parseGlobInner [] Nothing) (T.unpack source)+  case result of+    [] -> Nothing+    (r : _) -> do+      let parts = filter (/= Noop) $ fst r+      case parts of+        [] -> Nothing+        _ -> Just (Glob parts)++parseSegment :: Text -> Maybe Segment+parseSegment source+  | source == "*" = Just Asterisk+  | T.length source == acount = Just DAsterisk -- '**' or more asterisks+  | scount == 0 && acount == 1 && aprefix = Suffix . OS.tail <$> encoded+  | scount == 0 && acount == 1 && asuffix = Prefix . OS.init <$> encoded+  | scount > 0 || acount > 0 = parseGlob source+  | otherwise = Literal <$> encoded+  where+    aprefix = "*" `T.isPrefixOf` source+    asuffix = "*" `T.isSuffixOf` source+    acount = T.count "*" source+    scount = T.length (T.filter (`elem` ['[', ']', '?', '\\']) source)+    encoded = either (const Nothing) Just $ encodeWith utf8 utf16le (T.unpack source)++parsePattern :: Text -> Maybe Pattern+parsePattern source =+  let (source', negated) =+        if not (T.null source) && T.head source == '!'+          then (T.tail source, True)+          else (source, False)+      segments = filter (not . T.null) $ T.splitOn "/" source'+      segments' = mapMaybe parseSegment segments+      dir = "/" `T.isSuffixOf` source'+      anchored = "/" `T.isPrefixOf` source' || length segments' > 1+   in if null segments'+        then Nothing+        else+          Just+            Pattern+              { pSegments = segments',+                pDir = dir,+                pNegated = negated,+                pAnchored = anchored+              }++-- | Parse a complete gitignore file content into an 'Ignore' collection.+--+-- Strips whitespace, removes comments (lines starting with @#@), and parses+-- each remaining line as a pattern.+parse :: Text -> Ignore+parse source =+  let sourceLines = filter (not . T.null) $ map T.strip $ T.lines source+      patterns = filter (\l -> T.head l /= '#') sourceLines+   in Ignore (mapMaybe parsePattern patterns)++globClassMatches :: OsChar -> GlobClassPart -> Bool+globClassMatches curr part =+  case part of+    ClassSingle osch -> osch == curr+    ClassRange (start, end) -> (start <= curr) && (curr <= end)+    ClassSep -> False++matchesEmpty :: GlobPart -> Bool+matchesEmpty (Wildcard True) = True+matchesEmpty Noop = True+matchesEmpty _ = False++globMatches :: [GlobPart] -> OsPath -> Bool+globMatches parts path =+  if OS.null path+    then null parts || all matchesEmpty parts+    else case parts of+      [] -> False+      (part : rest) ->+        let pathTail = OS.tail path+            cont = globMatches rest pathTail+            curr = OS.head path+         in case part of+              Wildcard True -> cont || globMatches parts pathTail || globMatches rest path+              Wildcard False -> cont+              Single osch -> if osch == curr then cont else False+              Noop -> globMatches rest path+              Class classParts ->+                if any (globClassMatches curr) classParts+                  then cont+                  else False++segmentMatches :: Segment -> OsPath -> Bool+segmentMatches target path = case target of+  Asterisk -> True+  Literal val -> path == val+  Suffix val -> val `OS.isSuffixOf` path+  Prefix val -> val `OS.isPrefixOf` path+  Glob parts -> globMatches parts path+  -- NOTE: DAsterisk is already handled in 'patternIgnoresInner'.+  DAsterisk -> error "Unhandled ** pattern"++patternIgnoresInner :: Pattern -> [OsPath] -> Maybe Bool+patternIgnoresInner pat@Pattern {pNegated = True} splitPath =+  not <$> patternIgnoresInner pat {pNegated = False} splitPath+patternIgnoresInner pat@Pattern {pSegments = segs} splitPath =+  case splitPath of+    [] ->+      -- Exhausted path without returning False, meaning we might+      -- have an ignore-match. This depends on whether there is more+      -- pattern to match.+      if null segs then Just True else Nothing+    (pathHead : pathTail) -> case segs of+      [] -> Nothing+      (DAsterisk : rest) ->+        if null rest+          then Just True+          else+            let matches = mapMaybe (patternIgnoresInner (pat {pSegments = rest})) (init $ tails splitPath)+             in if null matches then Nothing else Just (or matches)+      (seg : rest) ->+        let match = segmentMatches seg pathHead+            continued = patternIgnoresInner (pat {pSegments = rest}) pathTail+            retry = if null pathTail then Nothing else patternIgnoresInner pat pathTail+         in if pAnchored pat+              then if match then continued else Nothing+              else (if match && isJust continued then continued else retry)++patternIgnores :: Pattern -> [OsPath] -> Bool -> Maybe Bool+patternIgnores pat splitPath dir =+  if (pDir pat && not dir) || null splitPath+    then Nothing+    else patternIgnoresInner pat splitPath++ignoresInner :: Ignore -> [OsPath] -> Bool -> Bool+ignoresInner (Ignore patterns) splitPath dir =+  last $ False : mapMaybe (\pat -> patternIgnores pat splitPath dir) patterns++-- | Check if an 'Ignore' collection ignores the given path.+--+-- The third parameter indicates whether the path is a directory.+ignores :: Ignore -> OsPath -> Bool -> Bool+ignores ign path = ignoresInner ign (splitDirectories path)++-- | Check if an 'Ignore' collection ignores the given pre-split path.+--+-- Like 'ignores', but takes a path already split into components. The third+-- parameter indicates whether the path is a directory.+ignores' :: Ignore -> [OsPath] -> Bool -> Bool+ignores' = ignoresInner
+ test/IgnoreSpec.hs view
@@ -0,0 +1,221 @@+module Main (main) where++import Data.Text (Text)+import qualified Data.Text as T+import Ignore+import Ignore.Internal+import System.OsString (OsChar, OsString, unsafeEncodeUtf, unsafeFromChar)+import Test.Hspec++os :: String -> OsString+os = unsafeEncodeUtf++osc :: Char -> OsChar+osc = unsafeFromChar++main :: IO ()+main = hspec spec++path :: String -> Segment+path p = Literal (os p)++parseln :: [Text] -> Ignore+parseln patterns = parse (T.intercalate "\n" patterns)++pat :: [Segment] -> Bool -> Bool -> Bool -> Pattern+pat segs dir negated anchored =+  Pattern+    { pSegments = segs,+      pDir = dir,+      pNegated = negated,+      pAnchored = anchored+    }++spec :: Spec+spec = do+  describe "parse" $ do+    it "parses an ignore correctly" $ do+      parse "" `shouldBe` Ignore []+      parse "\n  \n\t\t\n \n" `shouldBe` Ignore []+      parse "# test" `shouldBe` Ignore []+      parse "  # test " `shouldBe` Ignore []+      parse "  # test\n#test" `shouldBe` Ignore []+      parse "path" `shouldBe` Ignore [pat [path "path"] False False False]+      parse "!path" `shouldBe` Ignore [pat [path "path"] False True False]+      parse "!!path" `shouldBe` Ignore [pat [path "!path"] False True False]+      parse "path/" `shouldBe` Ignore [pat [path "path"] True False False]+      parse "path///" `shouldBe` Ignore [pat [path "path"] True False False]+      parse "/path/" `shouldBe` Ignore [pat [path "path"] True False True]+      parse "/path/foo/" `shouldBe` Ignore [pat [path "path", path "foo"] True False True]+      parse "/path///foo/" `shouldBe` Ignore [pat [path "path", path "foo"] True False True]+      parse "**/path/" `shouldBe` Ignore [pat [DAsterisk, path "path"] True False True]+      parse "/path/**" `shouldBe` Ignore [pat [path "path", DAsterisk] False False True]+      parse "/path/**/" `shouldBe` Ignore [pat [path "path", DAsterisk] True False True]+      parse "a?" `shouldBe` Ignore [pat [Glob [Single (osc 'a'), Wildcard False]] False False False]+      parse "*a*" `shouldBe` Ignore [pat [Glob [Wildcard True, Single (osc 'a'), Wildcard True]] False False False]+      parse "a]" `shouldBe` Ignore [pat [Glob [Single (osc 'a'), Single (osc ']')]] False False False]+      parse "\\a" `shouldBe` Ignore [pat [Glob [Single (osc 'a')]] False False False]+      parse "a\\?" `shouldBe` Ignore [pat [Glob [Single (osc 'a'), Single (osc '?')]] False False False]+      parse "[ab]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc 'b')]]] False False False]+      parse "[a\\b]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc 'b')]]] False False False]+      parse "[a\\]]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc ']')]]] False False False]+      parse "[-]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '-')]]] False False False]+      parse "[--]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc '-', osc '-')]]] False False False]+      parse "[---]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '-')]]] False False False]+      parse "[a-]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'a')]]] False False False]+      parse "[a-z]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z')]]] False False False]+      parse "[a--z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc 'z')]]] False False False]+      parse "[1a-z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '1'), ClassRange (osc 'a', osc 'z')]]] False False False]+      parse "[a-z2]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z'), ClassSingle (osc '2')]]] False False False]+      parse "[a-zA-Z]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z'), ClassRange (osc 'A', osc 'Z')]]] False False False]+      parse "[-a-z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '-'), ClassRange (osc 'a', osc 'z')]]] False False False]+      parse "[a-z-]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z'), ClassSingle (osc '-')]]] False False False]+      parse "[z-a]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'z')]]] False False False]+      parse "[z-z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'z')]]] False False False]+      parse "[z-a-m]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'z'), ClassSingle (osc '-'), ClassSingle (osc 'm')]]] False False False]+      parse "[]]" `shouldBe` Ignore [pat [Glob [Single (osc ']')]] False False False]+      parse "[[]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '[')]]] False False False]+      parse "a[" `shouldBe` Ignore [] -- Unclosed class+      parse "a[b" `shouldBe` Ignore [] -- Unclosed class+      parse "[b[" `shouldBe` Ignore [] -- Unclosed class+      parse "[]" `shouldBe` Ignore [] -- Empty+      parse "[a\\]" `shouldBe` Ignore [] -- Unclosed class+      parse "\\" `shouldBe` Ignore []++  describe "ignores" $ do+    it "ignores paths correctly (single pattern)" $ do+      ignores (parse "") (os "") False `shouldBe` False+      ignores (parse "") (os "foo") False `shouldBe` False+      ignores (parse "!") (os "foo") False `shouldBe` False+      ignores (parse "!*") (os "foo") False `shouldBe` False+      ignores (parse "bar") (os "foo") False `shouldBe` False+      ignores (parse "!bar") (os "foo") False `shouldBe` False+      ignores (parse "foo") (os "foo") False `shouldBe` True+      ignores (parse "/") (os "foo") False `shouldBe` False+      ignores (parse "/") (os "foo") True `shouldBe` False+      ignores (parse "/") (os "") False `shouldBe` False+      ignores (parse "!foo") (os "foo") False `shouldBe` False+      ignores (parse "!!foo") (os "foo") False `shouldBe` False+      ignores (parse "foo") (os "foo") True `shouldBe` True+      ignores (parse "foo/") (os "foo") False `shouldBe` False+      ignores (parse "foo/") (os "foo") True `shouldBe` True+      ignores (parse "/foo") (os "foo") False `shouldBe` True+      ignores (parse "foo/bar") (os "foo/bar") False `shouldBe` True+      ignores (parse "foo/bar") (os "foo/baz") False `shouldBe` False+      ignores (parse "/foo/bar") (os "foo/bar") False `shouldBe` True+      ignores (parse "/foo/bar") (os "baz/foo/bar") False `shouldBe` False+      ignores (parse "bar") (os "foo/bar") False `shouldBe` True+      ignores (parse "bar/") (os "foo/bar") True `shouldBe` True+      ignores (parse "/bar") (os "foo/bar") False `shouldBe` False+      ignores (parse "foo/bar") (os "baz/foo/bar") False `shouldBe` False+      ignores (parse "*") (os "foo") False `shouldBe` True+      ignores (parse "*") (os "foo") True `shouldBe` True+      ignores (parse "*") (os "foo/bar/baz") False `shouldBe` True+      ignores (parse "/*") (os "foo") False `shouldBe` True+      ignores (parse "/*") (os "foo/bar") False `shouldBe` False+      ignores (parse "*/*") (os "foo/bar") False `shouldBe` True+      ignores (parse "*/*") (os "foo/bar/baz") False `shouldBe` False+      ignores (parse "*/*") (os "foo") False `shouldBe` False+      ignores (parse "foo/bar") (os "foo/x/bar") False `shouldBe` False+      ignores (parse "foo/*/bar") (os "foo/x/bar") False `shouldBe` True+      ignores (parse "foo/*/bar") (os "foo/x/baz") False `shouldBe` False+      ignores (parse "foo/*/bar") (os "foo/x") False `shouldBe` False+      ignores (parse "**") (os "foo") False `shouldBe` True+      ignores (parse "**") (os "foo/bar/baz") False `shouldBe` True+      ignores (parse "**/bar") (os "x/foo/bar") False `shouldBe` True+      ignores (parse "**/foo/bar") (os "x/foo/bar") False `shouldBe` True+      ignores (parse "**/foo/baz") (os "x/foo/bar") False `shouldBe` False+      ignores (parse "bar/**") (os "bar/a") False `shouldBe` True+      ignores (parse "bar/***") (os "bar/a") False `shouldBe` True+      ignores (parse "*/**") (os "bar/a") False `shouldBe` True+      ignores (parse "bar/**") (os "bar") False `shouldBe` False+      ignores (parse "bar/**/x") (os "bar/x") False `shouldBe` True+      ignores (parse "bar/**/x") (os "bar/1/2/3/x") False `shouldBe` True+      ignores (parse "bar/**/x/") (os "bar/1/2/3/x") False `shouldBe` False+      ignores (parse "bar/**/x/") (os "bar/1/2/3/x") True `shouldBe` True+      ignores (parse "bar/***/x/") (os "bar/1/2/3/x") True `shouldBe` True+      ignores (parse "bar/**/") (os "bar/test") True `shouldBe` True+      ignores (parse "bar/**/") (os "bar/test") False `shouldBe` False+      ignores (parse "**/x") (os "foo") False `shouldBe` False+      ignores (parse "**/**") (os "foo") False `shouldBe` True+      ignores (parse "**/**") (os "foo/bar") False `shouldBe` True+      ignores (parse "!**/foo") (os "bar/foo") False `shouldBe` False+      ignores (parse "a/**/b/**/c") (os "a/x/b/y/c") False `shouldBe` True+      ignores (parse "a/**/m/**/c") (os "a/x/b/y/c") False `shouldBe` False+      ignores (parse "/**") (os "foo") False `shouldBe` True+      ignores (parse "/**") (os "foo/yyy") False `shouldBe` True+      ignores (parse "/**/foo") (os "foo") False `shouldBe` True+      ignores (parse "/**/foo") (os "x/foo") False `shouldBe` True+      ignores (parse "/**/foo") (os "x/y/foo") False `shouldBe` True+      ignores (parse "/***/foo") (os "x/y/foo") False `shouldBe` True+      ignores (parse "!foo/") (os "foo") True `shouldBe` False+      ignores (parse "!foo") (os "foo") True `shouldBe` False+      ignores (parse ".*") (os "foo") False `shouldBe` False+      ignores (parse ".*") (os ".foo") False `shouldBe` True+      ignores (parse ".*") (os "bar/.foo") False `shouldBe` True+      ignores (parse ".*") (os "bar/.") False `shouldBe` True+      ignores (parse "*.py") (os "test.py") False `shouldBe` True+      ignores (parse "*.py") (os "test.c") False `shouldBe` False+      ignores (parse "*.py") (os "foo/test.py") False `shouldBe` True+      ignores (parse "foo/*.py") (os "test.py") False `shouldBe` False+      ignores (parse "foo/*.py") (os "foo/test.py") False `shouldBe` True+      ignores (parse "**/*.py") (os "a/b/foo/test.py") False `shouldBe` True+      ignores (parse "**/*.py") (os "test.py") False `shouldBe` True+      ignores (parse "**/") (os "test") True `shouldBe` True+      ignores (parse "**/") (os "test/dir/foo") True `shouldBe` True+      ignores (parse "**/") (os "test/file") False `shouldBe` False+      ignores (parse "/*/") (os "test") True `shouldBe` True+      ignores (parse "/*/") (os "test/foo") True `shouldBe` False+      ignores (parse "**/*.py") (os "zzz") False `shouldBe` False+      ignores (parse "!**/*.py") (os "zzz") False `shouldBe` False+      ignores (parse "/baz/**") (os "a/baz/foo") False `shouldBe` False+      -- Globs+      ignores (parse "??") (os "ab") False `shouldBe` True+      ignores (parse "??") (os "abc") False `shouldBe` False+      ignores (parse "?ab*") (os "aabcd") False `shouldBe` True+      ignores (parse "a*c") (os "abc") False `shouldBe` True+      ignores (parse "a*c") (os "ac") False `shouldBe` True+      ignores (parse "a*cb") (os "acb") False `shouldBe` True+      ignores (parse "a*c") (os "abx") False `shouldBe` False+      ignores (parse "foo]") (os "foo]") False `shouldBe` True+      ignores (parse "\\?") (os "?") False `shouldBe` True+      ignores (parse "[]") (os "a") False `shouldBe` False+      ignores (parse "[-]") (os "-") False `shouldBe` True+      ignores (parse "[ab]") (os "a") False `shouldBe` True+      ignores (parse "[ab]") (os "c") False `shouldBe` False+      ignores (parse "[a-z]") (os "c") False `shouldBe` True+      ignores (parse "[a-z]") (os "0") False `shouldBe` False+      ignores (parse "[a-zA-Z]") (os "A") False `shouldBe` True+      ignores (parse "[z-a]") (os "z") False `shouldBe` True+      ignores (parse "*.?[a-z][-]\\?\\**") (os "abc.xa-?*-") False `shouldBe` True+      ignores (parse "*.?[a-z][-]\\?\\**") (os "abcxa-?*-") False `shouldBe` False++      -- From https://git-scm.com/docs/gitignore:+      ignores (parse "doc/frotz/") (os "doc/frotz") True `shouldBe` True+      ignores (parse "doc/frotz/") (os "a/doc/frotz") True `shouldBe` False+      ignores (parse "frotz/") (os "frotz") True `shouldBe` True+      ignores (parse "frotz/") (os "a/frotz") True `shouldBe` True+      ignores (parse "**/foo") (os "a/foo") False `shouldBe` True+      ignores (parse "**/foo/bar") (os "a/b/foo/bar") False `shouldBe` True+      ignores (parse "abc/**") (os "abc/foo/bar") False `shouldBe` True+      ignores (parse "a/**/b") (os "a/b") False `shouldBe` True+      ignores (parse "a/**/b") (os "a/x/b") False `shouldBe` True+      ignores (parse "a/**/b") (os "a/x/y/b") False `shouldBe` True+      ignores (parse "foo/*") (os "foo/test.json") False `shouldBe` True+      ignores (parse "foo/*") (os "foo/bar") True `shouldBe` True+      ignores (parse "foo/*") (os "foo/bar/hello.c") False `shouldBe` False++    it "ignores paths correctly (multiple patterns)" $ do+      ignores (parseln ["foo", "!foo"]) (os "foo") False `shouldBe` False+      ignores (parseln ["!foo", "!!foo"]) (os "foo") False `shouldBe` False+      ignores (parseln ["*", "!foo"]) (os "foo") False `shouldBe` False+      ignores (parseln ["*", "!*"]) (os "foo") False `shouldBe` False+      ignores (parseln ["*", "!bar"]) (os "foo") False `shouldBe` True+      ignores (parseln ["*", "!foo", "foo"]) (os "foo") False `shouldBe` True+      ignores (parseln ["test/**", "!test/foo"]) (os "test/foo") False `shouldBe` False+      ignores (parseln ["*.py", "!test.py"]) (os "test.py") False `shouldBe` False+      ignores (parseln ["*", "!*/"]) (os "foo/bar") True `shouldBe` False+      ignores (parseln ["*", "!*/"]) (os "foo/bar") False `shouldBe` True+      ignores (parseln ["*", "!*/", "!**/*.py"]) (os "bar.py") False `shouldBe` False+      ignores (parseln ["*", "!*/", "!**/*.py"]) (os "foo/bar.py") False `shouldBe` False+      ignores (parseln ["*", "!*/", "!*.py"]) (os "foo/bar.py") False `shouldBe` False