diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,16 @@
 # Changelog for lens-regex-pcre
 
+# 1.0.1.0
+
+### *BREAKING CHANGES*
+This release fixes a pretty major bugs surrounding the behaviour of optional groups. It's **unlikely** but still possible that the change to grouping behaviour has changed the behaviour of your application, but most likely it just fixed some bugs you didn't know you had yet...
+
+- Handle optional or alternated groups like `pcre-heavy`. This may change group behaviour on regular expressions which had groups with optional groups. E.g.:
+    - `A(x)?(B)`
+    - `(A)|(B)|(C)`
+- Switch `groups` from `IndexedTraversal'` to `IndexedLens'`. Since all lenses are valid traversals this shouldn't cause any breakages.
+- Add `namedGroups` and `namedGroup`
+
 # 1.0.0.0
 - Add `regexing` and `makeRegexTraversalQQ`
 - Replace `regex` traversal maker with `regex` QuasiQuoter
diff --git a/lens-regex-pcre.cabal b/lens-regex-pcre.cabal
--- a/lens-regex-pcre.cabal
+++ b/lens-regex-pcre.cabal
@@ -4,17 +4,17 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 6af08c0ccc1056deeb2c3dd469eca9c04a2fd667f31e9039181fc2acc991f398
+-- hash: d1c8fcf51e5b49738911f4885c246d4f3a9e709234fad2feefcd25246e284ac2
 
 name:           lens-regex-pcre
-version:        1.0.0.1
+version:        1.1.0.0
 synopsis:       A lensy interface to regular expressions
 description:    Please see the README on GitHub at <https://github.com/ChrisPenner/lens-regex-pcre#readme>
 category:       Regex
 homepage:       https://github.com/ChrisPenner/lens-regex-pcre#readme
 bug-reports:    https://github.com/ChrisPenner/lens-regex-pcre/issues
 author:         Chris Penner
-maintainer:     example@example.com
+maintainer:     christopher.penner@gmail.com
 copyright:      2019 Chris Penner
 license:        BSD3
 license-file:   LICENSE
@@ -39,8 +39,10 @@
   build-depends:
       base >=4.7 && <5
     , bytestring
+    , containers
     , lens
     , pcre-heavy
+    , pcre-light >=0.4.1.0
     , template-haskell
     , text
   default-language: Haskell2010
@@ -58,10 +60,12 @@
   build-depends:
       base >=4.7 && <5
     , bytestring
+    , containers
     , hspec
     , lens
     , lens-regex-pcre
     , pcre-heavy
+    , pcre-light >=0.4.1.0
     , template-haskell
     , text
   default-language: Haskell2010
@@ -77,10 +81,12 @@
   build-depends:
       base >=4.7 && <5
     , bytestring
+    , containers
     , gauge
     , lens
     , lens-regex-pcre
     , pcre-heavy
+    , pcre-light >=0.4.1.0
     , template-haskell
     , text
   default-language: Haskell2010
diff --git a/src/Control/Lens/Regex/ByteString.hs b/src/Control/Lens/Regex/ByteString.hs
--- a/src/Control/Lens/Regex/ByteString.hs
+++ b/src/Control/Lens/Regex/ByteString.hs
@@ -14,6 +14,7 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE UndecidableInstances #-}
 {-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE TupleSections #-}
 
 module Control.Lens.Regex.ByteString
     (
@@ -22,6 +23,8 @@
     , match
     , groups
     , group
+    , namedGroups
+    , namedGroup
     , matchAndGroups
 
     -- * Compiling regexes to Traversals
@@ -37,12 +40,15 @@
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.ByteString.Builder as BS
 import qualified Text.Regex.PCRE.Heavy as PCRE
+import qualified Text.Regex.PCRE.Light as PCRE
 import Control.Lens hiding (re)
 import Data.Bifunctor
 import qualified Language.Haskell.TH.Quote as TH
 import qualified Language.Haskell.TH.Syntax as TH
 import qualified Language.Haskell.TH as TH
 import GHC.TypeLits
+import qualified Data.Map as M
+import Data.Tuple (swap)
 
 -- $setup
 -- >>> :set -XQuasiQuotes
@@ -57,8 +63,12 @@
 type GroupRanges = [(Int, Int)]
 
 -- | Match represents an opaque regex match.
--- You can drill into it using 'match', 'groups', 'group' or 'matchAndGroups'
-newtype Match = Match [Either BS.Builder BS.Builder]
+-- You can drill into it using 'match', 'groups', 'group', 'namedGroup', 'namedGroups' or 'matchAndGroups'
+data Match =
+    Match { _chunks    :: [Either BS.Builder BS.Builder]
+          , _matchRegex :: PCRE.Regex
+          }
+makeLensesFor [("_chunks", "chunks")] ''Match
 
 instance TypeError
   ('Text "You're trying to 'show' a raw 'Match' object."
@@ -66,9 +76,6 @@
   => Show Match where
   show _ = "This is a raw Match object, did you miss a 'match' or 'groups' or 'group' call after your 'regex'?"
 
-chunks :: Iso' Match [Either BS.Builder BS.Builder]
-chunks = coerced
-
 unBuilder :: BS.Builder -> BS.ByteString
 unBuilder = BL.toStrict . BS.toLazyByteString
 
@@ -78,7 +85,6 @@
 -- | Access all groups of a match as a list. Stashes the full match text as the index in case
 -- you need it.
 --
--- Note that you can edit the groups through this traversal,
 -- Changing the length of the list has behaviour similar to 'partsOf'.
 --
 -- Get all matched groups:
@@ -112,10 +118,10 @@
 --
 -- >>> "one-two" & [regex|(\w+)-(\w+)|] . groups <. traversed %@~ \mtch grp -> grp <> ":(" <> mtch <> ")"
 -- "one:(one-two)-two:(one-two)"
-groups :: IndexedTraversal' BS.ByteString Match [BS.ByteString]
+groups :: IndexedLens' BS.ByteString Match [BS.ByteString]
 groups = conjoined groupsT (reindexed (view match) selfIndex <. groupsT)
     where
-      groupsT :: Traversal' Match [BS.ByteString]
+      groupsT :: Lens' Match [BS.ByteString]
       groupsT = chunks . partsOf (traversed . _Right . building)
 
 -- | Access a specific group of a match. Numbering starts at 0.
@@ -140,6 +146,87 @@
 group :: Int -> IndexedTraversal' BS.ByteString Match BS.ByteString
 group n = groups <. ix n
 
+-- | Access all the named groups of a match as a 'M.Map'. Stashes the full match text as the index in case
+-- you need it.
+--
+-- Note that you can edit the groups through this lens, but the behaviour is undefined when editing inner elements of __nested__ groups.
+-- Behaviour is undefined if groups are removed from the map (so don't do that).
+--
+-- NOTE: There's currently some strange behaviour in pcre-heavy where trailing unmatched optional groups are omitted, I'm looking into getting that patched, but for now, note the difference in behaviour:
+--
+-- >>> "A" ^? [regex|(?<a>A)|(?<b>B)|] . namedGroups
+-- Just (fromList [("a","A")])
+--
+-- >>> "B" ^? [regex|(?<a>A)|(?<b>B)|] . namedGroups
+-- Just (fromList [("a",""),("b","B")])
+--
+-- Get all matched groups:
+--
+-- >>> "raindrops on roses and whiskers on kittens" ^.. [regex|(?<first>\w+) on (?<second>\w+)|] . namedGroups
+-- [fromList [("first","raindrops"),("second","roses")],fromList [("first","whiskers"),("second","kittens")]]
+--
+-- You can access a specific group combining with 'ix', or just use 'namedGroup' instead
+--
+-- >>> "raindrops on roses and whiskers on kittens" ^.. [regex|(?<first>\w+) on (?<second>\w+)|] . namedGroups .  ix "second"
+-- ["roses","kittens"]
+--
+-- Editing groups:
+--
+-- >>> "raindrops on roses and whiskers on kittens" & [regex|(?<first>\w+) on (?<second>\w+)|] . namedGroups . ix "second" %~ Char8.map toUpper
+-- "raindrops on ROSES and whiskers on KITTENS"
+--
+-- Use indexed helpers to access the full match when operating on a group.
+--
+-- This replaces the "first" group with the full match text wrapped in parens:
+--
+-- >>> "one-two" & [regex|(?<first>\w+)-(\w+)|] . namedGroups <. ix "first" %@~ \mtch grp -> grp <> ":(" <> mtch <> ")"
+-- "one:(one-two)-two"
+namedGroups :: IndexedLens' BS.ByteString Match (M.Map BS.ByteString BS.ByteString)
+namedGroups = conjoined stepOne (reindexed (view match) selfIndex <. stepOne)
+    where
+      -- stepOne :: Traversal' Match (M.Map BS.ByteString BS.ByteString)
+      stepOne :: Lens' Match (M.Map BS.ByteString BS.ByteString)
+      stepOne f m = m & (groups . zipT . converterT (_matchRegex m) . partsOf (traversed . _Right) . mapL) %%~ f
+      zipT :: Iso' [a]  [(Int, a)]
+      zipT = iso (zip [0..]) (fmap snd)
+      converterT :: PCRE.Regex -> Lens' [(Int, BS.ByteString)] [Either (Int, BS.ByteString) (BS.ByteString, BS.ByteString)]
+      converterT pattern f xs =
+          f (converter pattern xs) <&> itraversed %@~ \i l -> either id ((i,) . snd) l
+      converter :: PCRE.Regex -> [(Int, BS.ByteString)] -> [Either (Int, BS.ByteString) (BS.ByteString, BS.ByteString)]
+      converter pattern = fmap $ \(i, s) ->
+          case M.lookup i (names pattern) of
+              Nothing -> Left (i, s)
+              Just n -> Right (n, s)
+      mapL :: Lens' [(BS.ByteString, BS.ByteString)] (M.Map BS.ByteString BS.ByteString)
+      mapL = lens M.fromList setter
+        where
+          setter :: [(BS.ByteString, BS.ByteString)] -> M.Map BS.ByteString BS.ByteString -> [(BS.ByteString, BS.ByteString)]
+          setter xs m = xs <&> \(k, _) -> (k, M.findWithDefault "" k m)
+      names :: PCRE.Regex -> M.Map Int BS.ByteString
+      names pattern = M.fromList . fmap swap $ PCRE.captureNames pattern
+
+-- | Access a specific named group of a match
+--
+-- See 'namedGroups' for caveats and more info.
+--
+-- Stashes the full match text as the index in case you need it.
+--
+-- >>> "key:value, a:b" ^.. [regex|(?<first>\w+):(?<second>\w+)|] . namedGroup "first"
+-- ["key","a"]
+--
+-- >>> "key:value, a:b" ^.. [regex|(?<first>\w+):(?<second>\w+)|] . namedGroup "second"
+-- ["value","b"]
+--
+-- >>> "key:value, a:b" & [regex|(?<first>\w+):(?<second>\w+)|] . namedGroup "second" %~ Char8.map toUpper
+-- "key:VALUE, a:B"
+--
+-- Replace the first capture group with the full match:
+--
+-- >>> "a, b" & [regex|(?<first>\w+), (?<second>\w+)|] . namedGroup "first" .@~ \i -> "(" <> i <> ")"
+-- "(a, b), b"
+namedGroup :: BS.ByteString -> IndexedTraversal' BS.ByteString Match BS.ByteString
+namedGroup name = namedGroups <. ix name
+
 -- | Traverse each match
 --
 -- Stashes any matched groups into the index in case you need them.
@@ -177,7 +264,13 @@
 --
 -- Also see 'mkRegexTraversalQQ'
 regexing :: PCRE.Regex -> IndexedTraversal' Int BS.ByteString Match
-regexing pattern = conjoined (regexT pattern) (indexing (regexT pattern)) . from chunks
+regexing pattern = conjoined (regexT pattern) (indexing (regexT pattern)) . asMatch
+  where
+    -- Unlawful iso, but since the Regex field of Match isn't exported it's fine.
+    asMatch :: Iso' [Either BS.Builder BS.Builder] Match
+    asMatch = iso to' from'
+    to' xs = Match xs pattern
+    from' (Match xs _) = xs
 
 -- | Base regex traversal helper
 regexT :: PCRE.Regex -> Traversal' BS.ByteString [Either BS.Builder BS.Builder]
@@ -198,9 +291,9 @@
 
 -- | Builds a traversal over text using a Regex pattern
 --
--- It's a 'QuasiQuoter' which creates a Traversal out of the given regex string.
--- It's equivalent to calling 'regexing' on a 'Regex' created using the
--- 're' QuasiQuoter.
+-- It's a 'TH.QuasiQuoter' which creates a Traversal out of the given regex string.
+-- It's equivalent to calling 'regexing' on a 'PCRE.Regex' created using the
+-- 'PCRE.re' QuasiQuoter.
 --
 -- The "real" type is:
 --
@@ -267,12 +360,12 @@
 -- >>> "Monday: 29, Tuesday: 99, Wednesday: 3" & partsOf ([regex|\d+|] . match . from packedChars . _Show @Int) %~ sort
 -- "Monday: 3, Tuesday: 29, Wednesday: 99"
 --
--- To alter behaviour of the regex you may wish to pass 'PCREOption's when compiling it.
+-- To alter behaviour of the regex you may wish to pass 'PCRE.PCREOption's when compiling it.
 -- The default behaviour may seem strange in certain cases; e.g. it operates in 'single-line'
--- mode. You can 'compile' the 'Regex' separately and add any options you like, then pass the resulting
--- 'Regex' into 'regex';
+-- mode. You can 'PCRE.compile' the 'PCRE.Regex' separately and add any options you like, then pass the resulting
+-- 'PCRE.Regex' into 'regex';
 -- Alternatively can make your own version of the QuasiQuoter with any options you want embedded
--- by using 'mkRegexQQ'.
+-- by using 'PCRE.mkRegexQQ'.
 regex :: TH.QuasiQuoter
 regex = PCRE.re{TH.quoteExp=quoter}
   where
@@ -299,6 +392,7 @@
 
 groupSplit :: BS.ByteString -> Int -> GroupRanges -> [Either BS.Builder BS.Builder]
 groupSplit txt _ [] = [Left $ BS.byteString txt]
+groupSplit txt offset ((-1, -1) : rest) = Right "" : groupSplit txt offset rest
 groupSplit txt offset ((grpStart, grpEnd) : rest) | offset == grpStart =
     let (prefix, suffix) = BS.splitAt (grpEnd - offset) txt
      in Right (BS.byteString prefix) : groupSplit suffix grpEnd rest
diff --git a/src/Control/Lens/Regex/Text.hs b/src/Control/Lens/Regex/Text.hs
--- a/src/Control/Lens/Regex/Text.hs
+++ b/src/Control/Lens/Regex/Text.hs
@@ -19,6 +19,8 @@
     , match
     , groups
     , group
+    , namedGroups
+    , namedGroup
     , matchAndGroups
 
     -- * Compiling regexes to Traversals
@@ -35,6 +37,7 @@
 import qualified Data.Text.Encoding.Error as T
 import qualified Data.ByteString as BS
 import qualified Text.Regex.PCRE.Heavy as PCRE
+import qualified Data.Map as M
 import Control.Lens hiding (re, matching)
 import qualified Language.Haskell.TH as TH
 import qualified Language.Haskell.TH.Quote as TH
@@ -54,15 +57,15 @@
 
 -- | Builds a traversal over text using a Regex pattern
 --
--- It's a 'QuasiQuoter' which creates a Traversal out of the given regex string.
--- It's equivalent to calling 'regexing' on a 'Regex' created using the
--- 're' QuasiQuoter.
+-- It's a 'TH.QuasiQuoter' which creates a Traversal out of the given regex string.
+-- It's equivalent to calling 'regexing' on a 'PCRE.Regex' created using the
+-- 'PCRE.re' QuasiQuoter.
 --
 -- The "real" type is:
 --
 -- > regex :: Regex -> IndexedTraversal' Int T.Text Match
 --
--- It's a traversal which selects 'Match'es; compose it with 'match' or 'groups'
+-- It's a traversal which selects 'RBS.Match'es; compose it with 'match' or 'groups'
 -- to get the relevant parts of your match.
 --
 -- >>> txt = "raindrops on roses and whiskers on kittens"
@@ -123,12 +126,12 @@
 -- >>> "Monday: 29, Tuesday: 99, Wednesday: 3" & partsOf ([regex|\d+|] . match . unpacked . _Show @Int) %~ sort
 -- "Monday: 3, Tuesday: 29, Wednesday: 99"
 --
--- To alter behaviour of the regex you may wish to pass 'PCREOption's when compiling it.
+-- To alter behaviour of the regex you may wish to pass 'PCRE.PCREOption's when compiling it.
 -- The default behaviour may seem strange in certain cases; e.g. it operates in 'single-line'
--- mode. You can 'compile' the 'Regex' separately and add any options you like, then pass the resulting
--- 'Regex' into 'regex';
+-- mode. You can 'PCRE.compile' the 'PCRE.Regex' separately and add any options you like, then pass the resulting
+-- 'PCRE.Regex' into 'regex';
 -- Alternatively can make your own version of the QuasiQuoter with any options you want embedded
--- by using 'mkRegexQQ'.
+-- by using 'PCRE.mkRegexQQ'.
 -- regex :: Regex -> IndexedTraversal' Int T.Text RBS.Match
 regex :: TH.QuasiQuoter
 regex = PCRE.re{TH.quoteExp=quoter}
@@ -157,7 +160,7 @@
 -- | Access all groups of a match as a list. Also keeps full match text as the index in case
 -- you need it.
 --
--- Note that you can edit the groups through this traversal,
+-- Note that you can edit the groups through this lens,
 -- Changing the length of the list has behaviour similar to 'partsOf'.
 --
 -- Get all matched groups:
@@ -189,7 +192,7 @@
 --
 -- >>> "one-two" & [regex|(\w+)-(\w+)|] . groups <. traversed %@~ \mtch grp -> grp <> ":(" <> mtch <> ")"
 -- "one:(one-two)-two:(one-two)"
-groups :: IndexedTraversal' T.Text RBS.Match [T.Text]
+groups :: IndexedLens' T.Text RBS.Match [T.Text]
 groups = reindexed (view $ from utf8) RBS.groups <. mapping (from utf8)
 
 -- | Access a specific group of a match. Numbering starts at 0.
@@ -216,6 +219,69 @@
 -- "(a, b), b"
 group :: Int -> IndexedTraversal' T.Text RBS.Match T.Text
 group n = groups <. ix n
+
+-- | Access all the named groups of a match as a 'M.Map'. Stashes the full match text as the index in case
+-- you need it.
+--
+-- Note that you can edit the groups through this lens, but the behaviour is undefined when editing inner elements of __nested__ groups.
+-- Behaviour is undefined if groups are removed from the map (so don't do that).
+--
+-- NOTE: There's currently some strange behaviour in pcre-heavy where trailing unmatched optional groups are omitted, I'm looking into getting that patched, but for now, note the difference in behaviour:
+--
+-- >>> "A" ^? [regex|(?<a>A)|(?<b>B)|] . namedGroups
+-- Just (fromList [("a","A")])
+--
+-- >>> "B" ^? [regex|(?<a>A)|(?<b>B)|] . namedGroups
+-- Just (fromList [("a",""),("b","B")])
+--
+-- Get all matched groups:
+--
+-- >>> "raindrops on roses and whiskers on kittens" ^.. [regex|(?<first>\w+) on (?<second>\w+)|] . namedGroups
+-- [fromList [("first","raindrops"),("second","roses")],fromList [("first","whiskers"),("second","kittens")]]
+--
+-- You can access a specific group combining with 'ix', or just use 'namedGroup' instead
+--
+-- >>> "raindrops on roses and whiskers on kittens" ^.. [regex|(?<first>\w+) on (?<second>\w+)|] . namedGroups .  ix "second"
+-- ["roses","kittens"]
+--
+-- Editing groups:
+--
+-- >>> "raindrops on roses and whiskers on kittens" & [regex|(?<first>\w+) on (?<second>\w+)|] . namedGroups . ix "second" %~ T.toUpper
+-- "raindrops on ROSES and whiskers on KITTENS"
+--
+-- Use indexed helpers to access the full match when operating on a group.
+--
+-- This replaces the "first" group with the full match text wrapped in parens:
+--
+-- >>> "one-two" & [regex|(?<first>\w+)-(\w+)|] . namedGroups <. ix "first" %@~ \mtch grp -> grp <> ":(" <> mtch <> ")"
+-- "one:(one-two)-two"
+namedGroups :: IndexedLens' T.Text RBS.Match (M.Map T.Text T.Text)
+namedGroups = reindexed (view $ from utf8) RBS.namedGroups <. mapAsTxt
+  where
+    mapAsTxt :: Iso' (M.Map BS.ByteString BS.ByteString) (M.Map T.Text T.Text)
+    mapAsTxt = iso (M.mapKeys (review utf8)) (M.mapKeys (view utf8)) . mapping (from utf8)
+
+-- | Access a specific named group of a match
+--
+-- See 'namedGroups' for caveats and more info.
+--
+-- Stashes the full match text as the index in case you need it.
+--
+-- >>> "key:value, a:b" ^.. [regex|(?<first>\w+):(?<second>\w+)|] . namedGroup "first"
+-- ["key","a"]
+--
+-- >>> "key:value, a:b" ^.. [regex|(?<first>\w+):(?<second>\w+)|] . namedGroup "second"
+-- ["value","b"]
+--
+-- >>> "key:value, a:b" & [regex|(?<first>\w+):(?<second>\w+)|] . namedGroup "second" %~ T.toUpper
+-- "key:VALUE, a:B"
+--
+-- Replace the first capture group with the full match:
+--
+-- >>> "a, b" & [regex|(?<first>\w+), (?<second>\w+)|] . namedGroup "first" .@~ \i -> "(" <> i <> ")"
+-- "(a, b), b"
+namedGroup :: T.Text -> IndexedTraversal' T.Text RBS.Match T.Text
+namedGroup name = namedGroups <. ix name
 
 -- | Traverse each match
 --
diff --git a/test/ByteString.hs b/test/ByteString.hs
--- a/test/ByteString.hs
+++ b/test/ByteString.hs
@@ -6,11 +6,16 @@
 import Control.Lens.Regex.ByteString
 import qualified Data.ByteString.Char8 as C8 hiding (index)
 import Data.Char
+import qualified Data.Map as M
 import Test.Hspec
 
 spec :: Spec
 spec = do
     describe "regex" $ do
+        xdescribe "pcre-heavy-compat" $ do
+            it "should handle crazy nested groups" $ do
+                "abcdefhijklm" ^? [regex|^(?:a(b(c)))(?:d(e(f)))(?:h(i(j)))(?:k(l(m)))$|] . matchAndGroups
+                    `shouldBe` Just ("abcdefhijklm", ["bc", "c", "ef", "f", "ij", "j", "lm", "m"])
         describe "match" $ do
             describe "getting" $ do
                 it "should find one match" $ do
@@ -89,9 +94,11 @@
                 ("one two three four" ^.. [regex|(\w+) (\w+)|] . groups . ix 1)
                 `shouldBe` ["two", "four"]
 
-            xit "should handle weird group alternation" $ do
-                "1:2 a=b" ^.. [regex|(\d):(\d)|(\w)=(\w)|] . groups
-                `shouldBe` [[ "not entirely sure what I expect this to be yet" ]]
+            it "should handle weird group alternation" $ do
+                ("AB" ^.. [regex|A(x)?(B)|] . groups `shouldBe` [["", "B"]])
+                ("B" ^.. [regex|(A)|(B)|] . groups `shouldBe` [["", "B"]])
+                -- This behaviour is consistent with pcre-heavy
+                ("A" ^.. [regex|(A)|(B)|] . groups `shouldBe` [["A"]])
 
         describe "setting" $ do
             it "should allow setting groups as a list" $ do
@@ -135,6 +142,40 @@
             it "should compose indices with matches" $ do
                 ("one two three four" ^.. ([regex|(\w+) (\w+)|] <.> groups . traversed) . withIndex)
                 `shouldBe` [((0, 0), "one"), ((0, 1), "two"), ((1, 0), "three"), ((1, 1), "four")]
+
+    describe "namedGroups" $ do
+        describe "getting" $ do
+            it "should get named groups" $ do
+                "a b c" ^.. [regex|(?<mygroup>\w)|] . namedGroups
+                `shouldBe` [M.fromList [("mygroup", "a")], M.fromList [("mygroup", "b")], M.fromList [("mygroup", "c")]]
+
+            it "should get multiple named groups" $ do
+                "raindrops on roses and whiskers on kittens" ^.. [regex|(?<one>\w+) on (?<two>\w+)|] . namedGroups
+                `shouldBe` [M.fromList [("one", "raindrops"), ("two", "roses")], M.fromList [("one", "whiskers"), ("two", "kittens")]]
+
+            it "should allow getting a specific named group" $ do
+                ("raindrops on roses and whiskers on kittens" ^.. [regex|(?<one>\w+) on (?<two>\w+)|] . namedGroups . ix "two")
+                `shouldBe` ["roses", "kittens"]
+
+            it "should handle weird group alternation" $ do
+                ("AB" ^.. [regex|A(?<opt>x)?(?<always>B)|] . namedGroups `shouldBe` [M.fromList [("opt", ""), ("always", "B")]])
+                ("B" ^.. [regex|(?<a>A)|(?<b>B)|] . namedGroups `shouldBe` [M.fromList [("a", ""), ("b", "B")]])
+                -- This is the behaviour of pcre-heavy, it's a bit unfortunate
+                ("A" ^.. [regex|(?<a>A)|(?<b>B)|] . namedGroups `shouldBe` [M.fromList [("a", "A")]])
+
+        describe "setting" $ do
+            it "should allow setting groups as a map" $ do
+                ("one two three" & [regex|(?<a>\w+) (?<b>\w+)|] . namedGroups .~ M.fromList [("a", "1"), ("b", "2")])
+                `shouldBe` "1 2 three"
+
+    describe "namedGroup" $ do
+        it "should get a single named group" $ do
+                "a:b c:d" ^.. [regex|(?<before>\w):(?<after>\w)|] . namedGroup "after"
+                `shouldBe` ["b", "d"]
+
+        it "should set a single group" $ do
+                "a:b c:d" & [regex|(\w):(?<after>\w)|] . namedGroup "after" %~ C8.map toUpper
+                `shouldBe` "a:B c:D"
 
     describe "matchAndGroups" $ do
         it "should get match and groups" $ do
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE OverloadedStrings #-}
 import Test.Hspec
 import Text
 import ByteString
diff --git a/test/Text.hs b/test/Text.hs
--- a/test/Text.hs
+++ b/test/Text.hs
@@ -5,6 +5,7 @@
 import Control.Lens
 import Control.Lens.Regex.Text
 import qualified Data.Text as T
+import qualified Data.Map as M
 import Test.Hspec
 
 spec :: Spec
@@ -98,9 +99,11 @@
                 ("one two three four" ^.. [regex|(\w+) (\w+)|] . groups . ix 1)
                 `shouldBe` ["two", "four"]
 
-            xit "should handle weird group alternation" $ do
-                "1:2 a=b" ^.. [regex|(\d):(\d)|(\w)=(\w)|] . groups
-                `shouldBe` [[ "not entirely sure what I expect this to be yet" ]]
+            it "should handle weird group alternation" $ do
+                ("AB" ^.. [regex|A(x)?(B)|] . groups `shouldBe` [["", "B"]])
+                ("B" ^.. [regex|(A)|(B)|] . groups `shouldBe` [["", "B"]])
+                -- This behaviour is consistent with pcre-heavy
+                ("A" ^.. [regex|(A)|(B)|] . groups `shouldBe` [["A"]])
 
         describe "setting" $ do
             it "should allow setting groups as a list" $ do
@@ -144,6 +147,40 @@
             it "should compose indices with matches" $ do
                 ("one two three four" ^.. ([regex|(\w+) (\w+)|] <.> groups . traversed) . withIndex)
                 `shouldBe` [((0, 0), "one"), ((0, 1), "two"), ((1, 0), "three"), ((1, 1), "four")]
+
+    describe "namedGroups" $ do
+        describe "getting" $ do
+            it "should get named groups" $ do
+                "a b c" ^.. [regex|(?<mygroup>\w)|] . namedGroups
+                `shouldBe` [M.fromList [("mygroup", "a")], M.fromList [("mygroup", "b")], M.fromList [("mygroup", "c")]]
+
+            it "should get multiple named groups" $ do
+                "raindrops on roses and whiskers on kittens" ^.. [regex|(?<one>\w+) on (?<two>\w+)|] . namedGroups
+                `shouldBe` [M.fromList [("one", "raindrops"), ("two", "roses")], M.fromList [("one", "whiskers"), ("two", "kittens")]]
+
+            it "should allow getting a specific named group" $ do
+                ("raindrops on roses and whiskers on kittens" ^.. [regex|(?<one>\w+) on (?<two>\w+)|] . namedGroups . ix "two")
+                `shouldBe` ["roses", "kittens"]
+
+            it "should handle weird group alternation" $ do
+                ("AB" ^.. [regex|A(?<opt>x)?(?<always>B)|] . namedGroups `shouldBe` [M.fromList [("opt", ""), ("always", "B")]])
+                ("B" ^.. [regex|(?<a>A)|(?<b>B)|] . namedGroups `shouldBe` [M.fromList [("a", ""), ("b", "B")]])
+                -- This is the behaviour of pcre-heavy, it's a bit unfortunate
+                ("A" ^.. [regex|(?<a>A)|(?<b>B)|] . namedGroups `shouldBe` [M.fromList [("a", "A")]])
+
+        describe "setting" $ do
+            it "should allow setting groups as a map" $ do
+                ("one two three" & [regex|(?<a>\w+) (?<b>\w+)|] . namedGroups .~ M.fromList [("a", "1"), ("b", "2")])
+                `shouldBe` "1 2 three"
+
+    describe "namedGroup" $ do
+        it "should get a single named group" $ do
+                "a:b c:d" ^.. [regex|(?<before>\w):(?<after>\w)|] . namedGroup "after"
+                `shouldBe` ["b", "d"]
+
+        it "should set a single group" $ do
+                "a:b c:d" & [regex|(\w):(?<after>\w)|] . namedGroup "after" %~ T.toUpper
+                `shouldBe` "a:B c:D"
 
     describe "matchAndGroups" $ do
         it "should get match and groups" $ do
