diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,7 @@
+### 3.1.16.0
+
+- Add `tokens'` and `string'` combinators which don't consume the prefix.
+
 ### 3.1.15.0
 
 - Add `INLINABLE` pragmas to most overloaded combinators
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -19,9 +19,9 @@
   thanks to Microsoft Research).
 * [Using Parsec](http://book.realworldhaskell.org/read/using-parsec.html),
   chapter 16 of [Real World Haskell](http://book.realworldhaskell.org/).
-* [An introduction to the Parsec library](http://kunigami.wordpress.com/2014/01/21/an-introduction-to-the-parsec-library)
+* [An introduction to the Parsec library](https://www.kuniga.me/blog/2014/01/21/an-introduction-to-the-parsec-library.html)
   on Kunigami's blog.
-* [An introduction to parsing text in Haskell with Parsec](http://unbui.lt/#!/post/haskell-parsec-basics) on Wilson's blog.
+* [An introduction to parsing text in Haskell with Parsec](https://jsdw.me/posts/haskell-parsec-basics/) on Wilson's blog.
 * Differences between Parsec and
   [Attoparsec](http://hackage.haskell.org/package/attoparsec)
   (Haskell's other prominent parser library) as explained in
diff --git a/parsec.cabal b/parsec.cabal
--- a/parsec.cabal
+++ b/parsec.cabal
@@ -1,6 +1,6 @@
 cabal-version:  1.12
 name:           parsec
-version:        3.1.15.1
+version:        3.1.16.0
 
 synopsis:       Monadic parser combinators
 description:    Parsec is designed from scratch as an industrial-strength parser
@@ -64,7 +64,7 @@
         Text.ParserCombinators.Parsec.Token
 
     build-depends:
-        base       >= 4.5.1.0 && < 4.18,
+        base       >= 4.5.1.0 && < 4.19,
         mtl        >= 2.1.3.1 && < 2.4,
         bytestring >= 0.9.2.1 && < 0.12,
         text      (>= 1.2.3.0  && < 1.3)
@@ -97,12 +97,13 @@
         if impl(ghc >= 7.10)
             ghc-options: -fno-warn-trustworthy-safe
 
-    if impl(ghc >= 9.0)
-        -- these flags may abort compilation with GHC-8.10
-        -- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/3295
-        ghc-options: -Winferred-safe-imports -Wmissing-safe-haskell-mode
+    -- these flags may abort compilation with GHC-8.10
+    -- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/3295
+    -- https://gitlab.haskell.org/ghc/ghc/-/issues/22728
+    -- if impl(ghc >= 9.0)
+    --    -- ghc-options: -Winferred-safe-imports -Wmissing-safe-haskell-mode
 
-test-suite parsec.
+test-suite parsec-tests
     type: exitcode-stdio-1.0
 
     hs-source-dirs: test
@@ -115,6 +116,7 @@
         Bugs.Bug35
         Features
         Features.Feature80
+        Features.Feature150
         Util
 
     build-depends:
diff --git a/src/Text/Parsec/Char.hs b/src/Text/Parsec/Char.hs
--- a/src/Text/Parsec/Char.hs
+++ b/src/Text/Parsec/Char.hs
@@ -167,12 +167,24 @@
                                 (\pos c _cs -> updatePosChar pos c)
                                 (\c -> if f c then Just c else Nothing)
 
--- | @string s@ parses a sequence of characters given by @s@. Returns
+-- | @'string' s@ parses a sequence of characters given by @s@. Returns
 -- the parsed string (i.e. @s@).
 --
 -- >  divOrMod    =   string "div"
 -- >              <|> string "mod"
+--
+-- Consider using 'string''.
 
 string :: (Stream s m Char) => String -> ParsecT s u m String
 {-# INLINABLE string #-}
 string s            = tokens show updatePosString s
+
+-- | @'string'' s@ parses a sequence of characters given by @s@.
+-- Doesn't consume matching prefix.
+--
+-- >  carOrCdr    =   string' "car"
+-- >              <|> string' "cdr"
+
+string' :: (Stream s m Char) => String -> ParsecT s u m String
+{-# INLINABLE string' #-}
+string' s            = tokens' show updatePosString s
diff --git a/src/Text/Parsec/Error.hs b/src/Text/Parsec/Error.hs
--- a/src/Text/Parsec/Error.hs
+++ b/src/Text/Parsec/Error.hs
@@ -189,12 +189,12 @@
 
       showExpect      = showMany msgExpecting expect
       showUnExpect    = showMany msgUnExpected unExpect
-      showSysUnExpect | not (null unExpect) ||
-                        null sysUnExpect = ""
-                      | null firstMsg    = msgUnExpected ++ " " ++ msgEndOfInput
-                      | otherwise        = msgUnExpected ++ " " ++ firstMsg
-          where
-              firstMsg  = messageString (head sysUnExpect)
+      showSysUnExpect
+          | not (null unExpect)      = ""
+          | []      <- sysUnExpect   = ""
+          | msg : _ <- sysUnExpect
+          , null (messageString msg) = msgUnExpected ++ " " ++ msgEndOfInput
+          | msg : _ <- sysUnExpect   = msgUnExpected ++ " " ++ messageString msg
 
       showMessages      = showMany "" messages
 
diff --git a/src/Text/Parsec/Prim.hs b/src/Text/Parsec/Prim.hs
--- a/src/Text/Parsec/Prim.hs
+++ b/src/Text/Parsec/Prim.hs
@@ -53,6 +53,7 @@
     , lookAhead
     , Stream(..)
     , tokens
+    , tokens'
     , try
     , token
     , tokenPrim
@@ -511,6 +512,44 @@
             Nothing                 -> cerr $ errEof
             Just (x,xs) | t == x    -> walk ts xs
                         | otherwise -> cerr $ errExpect x
+
+        ok rs = let pos' = nextposs pos tts
+                    s' = State rs pos' u
+                in cok tts s' (newErrorUnknown pos')
+    in do
+        sr <- uncons input
+        case sr of
+            Nothing         -> eerr $ errEof
+            Just (x,xs)
+                | tok == x  -> walk toks xs
+                | otherwise -> eerr $ errExpect x
+
+-- | Like 'tokens', but doesn't consume matching prefix.
+tokens' :: (Stream s m t, Eq t)
+       => ([t] -> String)      -- Pretty print a list of tokens
+       -> (SourcePos -> [t] -> SourcePos)
+       -> [t]                  -- List of tokens to parse
+       -> ParsecT s u m [t]
+{-# INLINE tokens' #-}
+tokens' _ _ []
+    = ParsecT $ \s _ _ eok _ ->
+      eok [] s $ unknownError s
+tokens' showTokens nextposs tts@(tok:toks)
+    = ParsecT $ \(State input pos u) cok cerr _eok eerr ->
+    let
+        errEof = (setErrorMessage (Expect (showTokens tts))
+                  (newErrorMessage (SysUnExpect "") pos))
+
+        errExpect x = (setErrorMessage (Expect (showTokens tts))
+                       (newErrorMessage (SysUnExpect (showTokens [x])) pos))
+
+        walk []     rs = ok rs
+        walk (t:ts) rs = do
+          sr <- uncons rs
+          case sr of
+            Nothing                 -> eerr $ errEof
+            Just (x,xs) | t == x    -> walk ts xs
+                        | otherwise -> eerr $ errExpect x
 
         ok rs = let pos' = nextposs pos tts
                     s' = State rs pos' u
diff --git a/test/Features.hs b/test/Features.hs
--- a/test/Features.hs
+++ b/test/Features.hs
@@ -5,8 +5,10 @@
 import Test.Tasty
 
 import qualified Features.Feature80
+import qualified Features.Feature150
 
 features :: [TestTree]
-features = [
-             Features.Feature80.main
-           ]
+features =
+    [ Features.Feature80.main
+    , Features.Feature150.main
+    ]
diff --git a/test/Features/Feature150.hs b/test/Features/Feature150.hs
new file mode 100644
--- /dev/null
+++ b/test/Features/Feature150.hs
@@ -0,0 +1,46 @@
+module Features.Feature150 ( main ) where
+
+import           Control.Applicative ((*>))
+import           Control.Monad.Identity
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Text.Parsec
+
+main :: TestTree
+main =
+  testCase "string' (#150)" $ do
+    parseString (boot <|> bool) "boot" @?= "boot"
+    parseFail   (boot <|> bool) "bool" @?= "no parse"
+    parseFail   (boot <|> bool) "booz" @?= "no parse"
+
+    parseString (boot' <|> bool') "boot" @?= "boot"
+    parseString (boot' <|> bool') "bool" @?= "bool"
+    parseFail   (boot' <|> bool') "booz" @?= "no parse"
+
+    parseString (boot' <|> bool' <|> char 'b' *> many anyChar) "boomerang" @?= "oomerang"
+
+ where
+    boot :: ParsecT String () Identity String
+    boot = string "boot"
+
+    bool :: ParsecT String () Identity String
+    bool = string "bool"
+
+    boot' :: ParsecT String () Identity String
+    boot' = string' "boot"
+
+    bool' :: ParsecT String () Identity String
+    bool' = string' "bool"
+
+    parseString :: ParsecT String () Identity String -> String -> String
+    parseString p input =
+      case parse p "Example" input of
+        Left{}    -> error "Parse failure"
+        Right str -> str
+
+    parseFail :: ParsecT String () Identity String -> String -> String
+    parseFail p input =
+      case parse p "Example" input of
+        Left{}  -> "no parse"
+        Right _ -> error "Parsed but shouldn't"
