diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -72,15 +72,15 @@
 Binaries
 
 - epub-tools is available for Arch Linux [from the AUR](http://aur.archlinux.org/packages/epub-tools/)
-- Download binary distributions for generic Linux, OSX and Windows [from this directory](https://github.com/dino-/epub-tools/releases/download/3.2)
+- Download binary distributions for Linux and Windows [from this directory](https://codeberg.org/dinofp/epub-tools/releases/tag/4.0)
 
 
 Getting source for development
 
 - Download the cabalized source package [from Hackage](http://hackage.haskell.org/package/epub-tools)
-- Get the source with git: `$ git clone https://github.com/dino-/epub-tools.git`
+- Get the source with git: `$ git clone https://codeberg.org/dinofp/epub-tools.git`
 - Get the source with stack: `$ stack unpack epub-tools`
-- If you're just looking, [browse the source](https://github.com/dino-/epub-tools)
+- If you're just looking, [browse the source](https://codeberg.org/dinofp/epub-tools)
 
 
 Once you have source, building the usual way:
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,8 +1,5 @@
 #! /usr/bin/env runhaskell
 
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 import Distribution.Simple
 
 
diff --git a/TODO.md b/TODO.md
--- a/TODO.md
+++ b/TODO.md
@@ -8,27 +8,25 @@
 - Windows binary docs:
    - make INSTALL more nicely worded
    - add information or maybe a batch file for assisting with globs and cmd
-- Use Paths_epub_tools version instead of hard-coded version in the Opts modules. Test this on Windows.
 - Would like to way to be able to insert a publishing year tag into the OPF data
    - like these:
       - `<dc:date opf:event="publication">2011</dc:date>`
       - `<dc:date opf:event="original-publication">2011</dc:date>`
       - `<dc:subject>anthology</dc:subject>`
    - Perhaps as part of epubmeta?
-- Perform util/win-dist.sh behavior in post-build step of Setup.hs? I think it makes sense.
 
 
 ## epubmeta
 
-- When a backup is made during `-e[SUF]`, make a note of it on stdout
+- When a backup is made during `-b|--backup SUF`, make a note of it on stdout
 - Maybe more options for modifying metadata from the command-line
-- Ability to dump the actual Haskell Metadata structure out. Could be used to make unit tests for epubname.
+- Ability to dump the actual Haskell Metadata structure out. Could be used to make unit tests for epubname. Would want to use a pretty printer library for this as opposed to just the Show instancing.
 
 
 ## epubname
 
-- Switch from using awful String everywhere to nice, Unicode Text. This may alleviate some of the special typographic character problems I have from time to time.
-- in usage, encourage users to contact the developer
+- Switch from using String everywhere to Text. This may alleviate some of the special typographic character problems I have from time to time.
+- In usage, encourage users to contact the developer
 - Add info in the usage about submitting metadata to me using epubmeta when there are problems.
 - Rethink that Publisher business, maybe get rid of it.
 - I'd like to see the author name formatting be more flexible so you can opt to not have it call scrubString as is mandatory now.
@@ -43,6 +41,12 @@
          Test/
             Compile
             Test
+
+2024-02-28
+
+- Redesign epubname.hs, Main.hs. These modules are poorly named and too much weird stuff is lumped together in this area of the code.
+- Change one or two date examples in the usage to just YYYY
+- When does exitProcessingFailure actually happen? Seems like never. Examine what processBook does more closely.
 
 
 ## epubzip
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,22 @@
+4.0 (2026-06-14)
+
+  * Moved stackage resolver up to lts-22.6
+  * Adjusted all build-depends bounds
+  * Updated README with new Codeberg locations
+  * Completely overhauled options parsing
+  * Made temp file creation for epub edit safer
+  * Editing metadata can be aborted without modification
+  * Fixed a bug with the logic for removing the source file after naming
+  * Documented epubname modes better in usage
+  * Switched from HUnit alone to Test.Tasty and Test.Tasty.HUnit
+  * epubname now supports linking to multiple dest directories
+  * epubname now defaulting to hard links for the rename
+  * Redesigned how rules file loading works
+  * Dead code clean-up
+  * Reorganized exports
+  * Fixed date options parsing
+
+
 3.2 (2024-01-22)
 
   * Updated to build against recent changes and bug fixes in
diff --git a/epub-tools.cabal b/epub-tools.cabal
--- a/epub-tools.cabal
+++ b/epub-tools.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.2
 
 name:         epub-tools
-version:      3.2
+version:      4.0
 synopsis:     Command line utilities for working with epub files
 description:  A suite of command-line utilities for creating and manipulating epub book files. Included are: epubmeta, epubname, epubzip
 category:     Application, Console
@@ -12,10 +12,9 @@
 license:      ISC
 license-file: LICENSE
 build-type:   Simple
-tested-with:  GHC >= 9.2.6
+tested-with:  GHC >= 9.8.4
 extra-source-files:
   .gitignore
-  changelog.md
   doc/dev/notes.md
   doc/hcar/epubtoolsCommandlineepubU-De.tex
   doc/INSTALL
@@ -26,6 +25,8 @@
   util/all-books.hs
   util/install.sh
   util/win-dist.sh
+extra-doc-files:
+  changelog.md
 
 source-repository head
    type: git
@@ -34,10 +35,11 @@
 common lang
   default-language: Haskell2010
   build-depends:
-      directory
-    , epub-metadata >= 5.3
-    , filepath
-    , mtl
+      directory >= 1.3.8 && < 1.5
+    , epub-metadata >= 5.4 && < 6
+    , filepath >= 1.4.100 && < 1.6
+    , heredoc >= 0.2.0 && < 0.4
+    , mtl >= 2.3.1 && < 2.5
   ghc-options:
     -fwarn-tabs
     -Wall
@@ -60,16 +62,18 @@
   main-is: epubmeta.hs
   build-depends:
       base >= 3 && < 5
-    , bytestring
-    , process
-    , zip-archive
+    , bytestring >= 0.11.5 && < 0.14
+    , optparse-applicative-dex >= 1.0.1 && < 2
+    , process >= 1.6.17 && < 1.8
+    , text >= 2.0.2 && < 2.3
+    , zip-archive >= 0.4.3 && < 0.6
   other-modules:
-    EpubTools.EpubMeta.Display
     EpubTools.EpubMeta.Edit
     EpubTools.EpubMeta.Export
     EpubTools.EpubMeta.Import
     EpubTools.EpubMeta.Opts
     EpubTools.EpubMeta.Util
+    EpubTools.EpubMeta.View
     Paths_epub_tools
 
 executable epubname
@@ -79,9 +83,12 @@
   main-is: epubname.hs
   build-depends:
       base >= 3 && < 5
-    , containers
-    , parsec
-    , regex-compat
+    , containers >= 0.6.7 && < 0.8
+    , optparse-applicative-dex >= 1.0.1 && < 2
+    , parsec >= 3.1.16 && < 3.3
+    , regex-compat >= 0.95.2 && < 0.97
+    , text >= 2.0.2 && < 2.3
+    , unix >= 2.8.1.0 && < 3
   other-modules:
     EpubTools.EpubName.Doc.Dsl
     EpubTools.EpubName.Doc.Rules
@@ -90,6 +97,7 @@
     EpubTools.EpubName.Format.Format
     EpubTools.EpubName.Format.PubYear
     EpubTools.EpubName.Format.Util
+    EpubTools.EpubName.Common
     EpubTools.EpubName.Main
     EpubTools.EpubName.Opts
     EpubTools.EpubName.Prompt
@@ -98,17 +106,22 @@
 
 test-suite epubname-tests
   import: lang
-  import: rts
   type: exitcode-stdio-1.0
-  hs-source-dirs: src/app src/tests
-  main-is: test-epubname.hs
+  hs-source-dirs: src/app src/test
+  main-is: EpubTools/Test/EpubName/Main.hs
+  ghc-options:
+    -rtsopts
+    -threaded
+    -with-rtsopts=-N
   build-depends:
       base >= 3 && < 5
-    , containers
-    , HUnit
-    , parsec
-    , regex-compat
+    , containers >= 0.6.7 && < 0.8
+    , parsec >= 3.1.16 && < 3.3
+    , regex-compat >= 0.95.2 && < 0.97
+    , tasty >= 1.4.3 && < 1.6
+    , tasty-hunit >= 0.10.1 && < 0.12
   other-modules:
+    EpubTools.EpubName.Common
     EpubTools.EpubName.Doc.Rules
     EpubTools.EpubName.Format.Author
     EpubTools.EpubName.Format.Compile
@@ -116,7 +129,6 @@
     EpubTools.EpubName.Format.PubYear
     EpubTools.EpubName.Format.Util
     EpubTools.EpubName.Main
-    EpubTools.EpubName.Opts
     EpubTools.EpubName.Util
     EpubTools.Test.EpubName.Format
     EpubTools.Test.EpubName.PubYear
@@ -128,10 +140,13 @@
   main-is: epubzip.hs
   build-depends:
       base >= 3 && < 5
-    , containers
-    , parsec
-    , regex-compat
+    , containers >= 0.6.7 && < 0.8
+    , optparse-applicative-dex >= 1.0.1 && < 2
+    , parsec >= 3.1.16 && < 3.3
+    , regex-compat >= 0.95.2 && < 0.97
+    , text >= 2.0.2 && < 2.3
   other-modules:
+    EpubTools.EpubName.Common
     EpubTools.EpubName.Doc.Rules
     EpubTools.EpubName.Format.Author
     EpubTools.EpubName.Format.Compile
@@ -139,7 +154,6 @@
     EpubTools.EpubName.Format.PubYear
     EpubTools.EpubName.Format.Util
     EpubTools.EpubName.Main
-    EpubTools.EpubName.Opts
     EpubTools.EpubName.Util
     EpubTools.EpubZip.Opts
     Paths_epub_tools
diff --git a/src/app/EpubTools/EpubMeta/Display.hs b/src/app/EpubTools/EpubMeta/Display.hs
deleted file mode 100644
--- a/src/app/EpubTools/EpubMeta/Display.hs
+++ /dev/null
@@ -1,26 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubMeta.Display
-   where
-
-import Codec.Epub
-import Control.Monad ( when )
-
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
-
-
-display :: Options -> FilePath -> EM ()
-display opts zipPath = do
-   xml <- getPkgXmlFromZip zipPath
-
-   pkg <- format `fmap` getPackage xml
-   meta <- format `fmap` getMetadata xml
-   liftIO $ mapM_ putStr [pkg, meta]
-
-   when (optVerbose opts) $ do
-      mf <- format `fmap` getManifest xml
-      spine <- format `fmap` getSpine xml
-      guide <- format `fmap` getGuide xml
-      liftIO $ mapM_ putStr [mf, spine, guide]
diff --git a/src/app/EpubTools/EpubMeta/Edit.hs b/src/app/EpubTools/EpubMeta/Edit.hs
--- a/src/app/EpubTools/EpubMeta/Edit.hs
+++ b/src/app/EpubTools/EpubMeta/Edit.hs
@@ -1,83 +1,79 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 {-# LANGUAGE ScopedTypeVariables #-}
 
 module EpubTools.EpubMeta.Edit
-   ( edit )
-   where
-
-import Control.Exception
-import Control.Monad
-import System.Directory
-import System.FilePath
-import System.Environment
-import System.Exit
-import System.Process ( system )
-import Text.Printf
-
-import EpubTools.EpubMeta.Export
-import EpubTools.EpubMeta.Import
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
+  ( edit )
+  where
 
+import Control.Applicative (asum)
+import Data.Maybe (fromJust)
+import System.Directory (copyFile, findExecutable, getFileSize,
+  removeFile)
+import System.FilePath ((</>), (<.>))
+import System.Environment (lookupEnv)
+import System.Exit (ExitCode (ExitSuccess))
+import System.Process (getCurrentPid, system)
+import Text.Printf (printf)
 
-{- Turn an (IO a) action into a (Maybe a) with a value of Nothing
-   if an IO exception is raised
--}
-ioMaybe :: IO a -> IO (Maybe a)
-ioMaybe action = do
-   result :: (Either SomeException a) <- try action
-   either (const . return $ Nothing) (return . Just) result
+import EpubTools.EpubMeta.Export (exportOpf)
+import EpubTools.EpubMeta.Import (importOpf)
+import EpubTools.EpubMeta.Opts (Backup (BackupSuffix, NoBackup),
+  EpubPath (..), ImportPath (..), Output (OutputFilename))
+import EpubTools.EpubMeta.Util (EM, liftIO, throwError)
 
 
-{- Look through the user's environment looking for their preferred 
-   editor. Use /usr/bin/vi as a default if none found
+{- Look through the user's environment for their preferred editor.
+   Use vi wherever it may be on the PATH as a default if none found.
 -}
 findEditor :: EM FilePath
 findEditor = do
-   mbEditor <- liftIO $ foldr (liftM2 mplus) (findExecutable "vi")
-      [ ioMaybe $ getEnv "EDITOR"
-      , ioMaybe $ getEnv "VISUAL"
-      ]
-
-   maybe (throwError "epubmeta: ERROR: Could not find a suitable editor in your EDITOR or VISUAL environment variables, and could not find the vi binary. Fix this situation or use epubmeta export/import.") return mbEditor
+  mbEditor <- liftIO $ asum <$> sequence
+    [ lookupEnv "EDITOR"
+    , lookupEnv "VISUAL"
+    , findExecutable "vi"
+    ]
+  maybe (throwError "epubmeta: Could not find a suitable editor in your EDITOR or VISUAL environment variables, and could not find the vi binary. Fix this situation or use epubmeta export/import.") return mbEditor
 
 
-edit :: Options -> FilePath -> EM ()
-edit opts = edit' $ optEdit opts
+{- Modern Linux systems with systemd provide per-user temp space
+-}
+locateTempDir :: IO FilePath
+locateTempDir = fromJust . asum <$> sequence
+  [ lookupEnv "TMPDIR"           -- Often unset, but use if we can
+  , lookupEnv "XDG_RUNTIME_DIR"  -- Should always exist on any systemd-based distro
+  , pure . Just $ "/tmp"         -- Every system should have this
+  ]
 
 
-edit' :: Edit -> FilePath -> EM ()
-
-edit' NoBackup zipPath = do
-   -- Make a temp path for the OPF document
-   tempDir <- liftIO $ getTemporaryDirectory
-   let opfPath = tempDir </> "epubmeta-temp-content.opf"
+edit :: Backup -> EpubPath -> EM ()
 
-   -- Dump the OPF document to this path
-   exportOpf (defaultOptions { optExport = ToPath opfPath }) zipPath
+edit NoBackup epubPath = do
+  -- Make a temp path for the OPF document
+  tempDir <- liftIO $ locateTempDir
+  pid <- liftIO $ getCurrentPid
+  let opfPath = tempDir </> ("epubmeta-" <> show pid <> "-content") <.> "opf"
 
-   -- Open this file in the user's editor and wait for it
-   editor <- findEditor
-   ec <- liftIO $ system $ printf "%s %s" editor opfPath
+  -- Dump the OPF document to this path
+  exportOpf (OutputFilename opfPath) epubPath
 
-   case ec of
-      ExitSuccess -> do
-         -- Modify the book with the now-edited OPF data
-         importOpf (defaultOptions { optImport = Just opfPath }) zipPath
+  -- Open this file in the user's editor and wait for it
+  editor <- findEditor
+  ec <- liftIO $ system $ printf "%s %s" editor opfPath
+  tempFileEmpty <- liftIO $ (== 0) <$> getFileSize opfPath
 
-         -- Delete the temp file
-         liftIO $ removeFile opfPath
+  case (ec, tempFileEmpty) of
+    (ExitSuccess, False) -> do
+      -- Modify the book with the now-edited OPF data
+      importOpf (ImportPath opfPath) NoBackup epubPath
 
-      ExitFailure _ -> do
-         -- Delete the temp file
-         liftIO $ removeFile opfPath
+      -- Delete the temp file
+      liftIO $ removeFile opfPath
 
-         throwError "epubmeta: ERROR: Something went wrong during editing, your file has not been changed."
+    _ -> do
+      -- Delete the temp file
+      liftIO $ removeFile opfPath
 
-edit' (Backup suffix) zipPath = do
-   liftIO $ copyFile zipPath (zipPath ++ suffix)
-   edit' NoBackup zipPath
+      throwError "epubmeta: ERROR: Edit of metadata aborted, your file has not been changed."
 
-edit' NotEditing _ = undefined
+edit (BackupSuffix suffix) epubPath@(EpubPath zipPath) = do
+  liftIO $ copyFile zipPath (zipPath ++ suffix)
+  edit NoBackup epubPath
diff --git a/src/app/EpubTools/EpubMeta/Export.hs b/src/app/EpubTools/EpubMeta/Export.hs
--- a/src/app/EpubTools/EpubMeta/Export.hs
+++ b/src/app/EpubTools/EpubMeta/Export.hs
@@ -1,35 +1,31 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubMeta.Export
-   ( exportOpf )
-   where
+  ( exportOpf )
+  where
 
-import Codec.Epub.IO
-import System.FilePath
+import Codec.Epub.IO (getPkgPathXmlFromZip, getPkgXmlFromZip)
+import System.FilePath (takeFileName)
 
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
+import EpubTools.EpubMeta.Opts (EpubPath (..),
+  Output (ExistingName, OutputFilename, StdOut))
+import EpubTools.EpubMeta.Util (EM, liftIO)
 
 
 write :: FilePath -> String -> EM ()
 write path contents = liftIO $ do
-   writeFile path contents
-   putStrLn $ "OPF XML saved as: " ++ path
-
-
-exportOpf :: Options -> FilePath -> EM ()
-exportOpf opts = export' (optExport opts)
+  writeFile path contents
+  putStrLn $ "OPF XML saved as: " ++ path
 
 
-export' :: Export -> FilePath -> EM ()
+exportOpf :: Output -> EpubPath -> EM ()
 
-export' Existing zipPath = do
-   (fullPath, contents) <- getPkgPathXmlFromZip zipPath
-   write (takeFileName fullPath) contents
+exportOpf (OutputFilename path) (EpubPath zipPath) = do
+  contents <- getPkgXmlFromZip zipPath
+  write path contents
 
-export' (ToPath path) zipPath = do
-   contents <- getPkgXmlFromZip zipPath
-   write path contents
+exportOpf ExistingName (EpubPath zipPath) = do
+  (fullPath, contents) <- getPkgPathXmlFromZip zipPath
+  write (takeFileName fullPath) contents
 
-export' NoExport _ = undefined
+exportOpf StdOut (EpubPath zipPath) = do
+  (_, contents) <- getPkgPathXmlFromZip zipPath
+  liftIO $ putStrLn contents
diff --git a/src/app/EpubTools/EpubMeta/Import.hs b/src/app/EpubTools/EpubMeta/Import.hs
--- a/src/app/EpubTools/EpubMeta/Import.hs
+++ b/src/app/EpubTools/EpubMeta/Import.hs
@@ -1,28 +1,23 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubMeta.Import
-   ( importOpf )
-   where
+  ( importOpf )
+  where
 
-import Codec.Archive.Zip ( Entry (..), addEntryToArchive, findEntryByPath
-  , readEntry, toArchive )
+import Codec.Archive.Zip (Entry (..), addEntryToArchive, findEntryByPath,
+  readEntry, toArchive)
 import Codec.Epub.IO ( getPkgPathXmlFromBS, writeArchive )
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as BL
-import Data.List ( foldl' )
-import Data.Maybe ( fromJust )
-import System.Directory ( removeFile )
+import Data.List (foldl')
+import System.Directory (copyFile, removeFile)
 
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
+import EpubTools.EpubMeta.Opts (Backup (BackupSuffix, NoBackup),
+  EpubPath (..), ImportPath (..))
+import EpubTools.EpubMeta.Util (EM, liftIO, throwError)
 
 
-importOpf :: Options -> FilePath -> EM ()
-importOpf opts zipPath = do
-  -- The modified file we want to insert into the book
-  let pathToNewOpf = fromJust . optImport $ opts
+importOpf :: ImportPath -> Backup -> EpubPath -> EM ()
 
+importOpf (ImportPath pathToNewOpf) NoBackup (EpubPath zipPath) = do
   -- Make new file into a Zip Entry
   tempEntry <- liftIO $ readEntry [] pathToNewOpf
 
@@ -53,3 +48,7 @@
 
   -- Write the new archive out
   liftIO $ writeArchive zipPath newArchive
+
+importOpf importPath (BackupSuffix suffix) epubPath@(EpubPath zipPath) = do
+  liftIO $ copyFile zipPath (zipPath ++ suffix)
+  importOpf importPath NoBackup epubPath
diff --git a/src/app/EpubTools/EpubMeta/Opts.hs b/src/app/EpubTools/EpubMeta/Opts.hs
--- a/src/app/EpubTools/EpubMeta/Opts.hs
+++ b/src/app/EpubTools/EpubMeta/Opts.hs
@@ -1,111 +1,163 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
 
 module EpubTools.EpubMeta.Opts
-   ( Edit (..)
-   , Export (..)
-   , Options (..), defaultOptions
-   , parseOpts, usageText
-   )
-   where
+  ( Backup (..)
+  , EpubPath (..)
+  , ImportPath (..)
+  , Mode (..)
+  , Options (..)
+  , Output (..)
+  , Verbose (..)
+  , parseOpts
+  )
+  where
 
-import Data.Version ( showVersion )
-import Paths_epub_tools ( version )
-import System.Console.GetOpt
+import Data.Text.Lazy (Text, pack)
+import Data.Version (showVersion)
+import Options.Applicative.Dex
+import Paths_epub_tools (version)
+import Text.Heredoc (here)
 
-import EpubTools.EpubMeta.Util
 
+newtype Verbose = Verbose Bool
 
--- There are three states for this, representing with a new type
-data Edit
-   = Backup String
-   | NoBackup
-   | NotEditing
-   deriving Eq
+data Output
+  = OutputFilename FilePath
+  | ExistingName
+  | StdOut
 
+data Backup
+  = BackupSuffix FilePath
+  | NoBackup
 
-maybeToEdit :: Maybe String -> Edit
-maybeToEdit (Just suffix) = Backup suffix
-maybeToEdit Nothing       = NoBackup
+newtype ImportPath = ImportPath FilePath
 
+data Mode
+  = View Verbose
+  | Export Output
+  | Edit Backup
+  | Import ImportPath Backup
 
--- There are three states for this, representing with a new type
-data Export
-   = ToPath String   -- User specified a file to export to
-   | Existing        -- User wants export to use existing name
-   | NoExport        -- This arg wasn't used at all
-   deriving Eq
+newtype EpubPath = EpubPath FilePath
 
+data Options = Options
+  { mode :: Mode
+  , inFile :: EpubPath
+  }
 
-maybeToExport :: Maybe String -> Export
-maybeToExport (Just file) = ToPath file
-maybeToExport Nothing     = Existing
 
+parseOpts :: IO Options
+parseOpts = parseOpts' parser
+  "View or edit epub book metadata"
+  footerContent version
 
-data Options = Options
-   { optHelp :: Bool
-   , optEdit :: Edit
-   , optImport :: Maybe String
-   , optExport :: Export
-   , optVerbose :: Bool
-   }
 
+parseView :: Parser Mode
+parseView = View
+  <$> ( flag' ()
+        (  long "view"
+        <> short 'V'
+        <> help "View EPUB metadata, this is the default mode"
+        )
+        *>  ( Verbose <$> switch
+              (  long "verbose"
+              <> short 'v'
+              <> help "Display all OPF package info, including manifest, spine and guide"
+              )
+            )
+      )
 
-defaultOptions :: Options
-defaultOptions = Options
-   { optHelp = False
-   , optEdit = NotEditing
-   , optImport = Nothing
-   , optExport = NoExport
-   , optVerbose = False
-   }
+parseEdit :: Parser Mode
+parseEdit = Edit
+  <$> ( flag' ()
+        (  long "edit"
+        <> short 'E'
+        <> help "Edit a book's OPF XML data in a text editor"
+        )
+        *>  ( maybe NoBackup BackupSuffix <$> (optional $ strOption
+              (  long "backup"
+              <> short 'b'
+              <> metavar "SUF"
+              )
+            ))
+      )
 
+parseExport :: Parser Mode
+parseExport = Export
+  <$> ( flag' ()
+        (  long "export"
+        <> short 'X'
+        <> help "Export the book's OPF XML metadata, defaults to stdout unless -o or -e are used"
+        )
+        *>  ( ( OutputFilename <$> strOption
+                (  long "output"
+                <> short 'o'
+                <> metavar "FILE"
+                <> help "Filename to use for exporting the OPF XML metadata document"
+                )
+              )
+              <|>
+              ( flag' ExistingName
+                (  long "existing"
+                <> short 'e'
+                <> help "Export OPF XML metadata document with the same filename used in the book archive"
+                )
+              )
+              <|>
+              pure StdOut
+            )
+      )
 
-options :: [OptDescr (Options -> Options)]
-options =
-   [ Option ['h'] ["help"] 
-      (NoArg (\opts -> opts { optHelp = True } ))
-      "This help text"
-   , Option ['e'] ["edit-opf"]
-      (OptArg (\f opts -> opts { optEdit = maybeToEdit f }) "SUF")
-      "Edit a book's OPF XML data in a text editor. If an optional suffix is supplied, a backup of the book will be created with that suffix"
-   , Option ['x'] ["export"]
-      (OptArg (\f opts -> opts { optExport = maybeToExport f }) "FILE")
-      "Export the book's OPF XML metadata to a file. If no file name is given, the name of the file in the book will be used"
-   , Option ['i'] ["import"]
-      (ReqArg (\f opts -> opts { optImport = Just f }) "FILE")
-      "Import OPF metadata from the supplied XML file"
-   , Option ['v'] ["verbose"]
-      (NoArg (\opts -> opts { optVerbose = True } )) 
-      "Display all OPF package info, including manifest, spine and guide"
-   ]
+parseImport :: Parser Mode
+parseImport = Import
+  <$> ( ImportPath <$> strOption
+        (  long "import"
+        <> short 'I'
+        <> metavar "FILE"
+        <> help "Import OPF metadata from the supplied XML file"
+        )
+      )
+  <*> ( ( maybe NoBackup BackupSuffix <$> (optional $ strOption
+          (  long "backup"
+          <> short 'b'
+          <> metavar "SUF"
+          <> help "Make a backup of the EPUB file with this suffix appended to the filename"
+          )
+        ))
+      )
 
+-- This defaults to `-V EPUBFILE` behavior when invoked with just EPUBFILE
+parseNoMode :: Parser Mode
+parseNoMode = pure (View (Verbose False))
 
-parseOpts :: [String] -> EM (Options, [String])
-parseOpts argv = 
-   case getOpt Permute options argv of
-      (o,n,[]  ) -> return (foldl (flip id) defaultOptions o, n)
-      (_,_,errs) -> throwError $ concat errs ++ usageText
+parseExplicitMode :: Parser Mode
+parseExplicitMode = parseView <|> parseEdit <|> parseExport <|> parseImport
 
+parser :: Parser Options
+parser = Options
+  <$> ( parseNoMode <|> parseExplicitMode )
+  <*> ( EpubPath <$> argument str
+        (  metavar "EPUBFILE"
+        <> help "Path to EPUB file"
+        )
+      )
 
-usageText :: String
-usageText = (usageInfo header options) ++ "\n" ++ footer
-   where
-      header = init $ unlines
-         [ "Usage: epubmeta [OPTIONS] EPUBFILE"
-         , "View or edit epub book metadata"
-         , ""
-         , "Note that 'edit' here means you get dumped into an editor with the XML document and are on your own."
-         , ""
-         , "Options:"
-         ]
-      footer = init $ unlines
-         [ "The -e feature will look for an editor in this order: the EDITOR environment variable, the VISUAL environment variable, vi"
-         , ""
-         , "For more information on the epub format:"
-         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
-         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
-         , ""
-         , ""
-         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
-         ]
+
+footerContent :: Text
+footerContent = [here|This software works in one of four modes. The modes each have additional options as shown above.
+
+view    (-V|--view)    Output a human-readable report of the book's metadata (Default mode)
+edit    (-E|--edit)    Edit the book's OPF metadata XML in an editor
+export  (-X|--export)  Export the book's OPF metadata XML to a file
+import  (-I|--import)  Import an OPF metadata XML file into the book
+
+edit mode will look for an editor in this order: the EDITOR environment variable, the VISUAL environment variable, vi
+
+IMPORTANT: To abort editing without changing the book file, delete all lines in the editor, save and exit. This is exactly how Git works, so will be familiar to some.
+
+For more information on the epub format:
+  epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm
+  epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html
+
+
+Version |] <> (pack $ showVersion version) <> "  Dino Morelli <dino@ui3.info>"
diff --git a/src/app/EpubTools/EpubMeta/Util.hs b/src/app/EpubTools/EpubMeta/Util.hs
--- a/src/app/EpubTools/EpubMeta/Util.hs
+++ b/src/app/EpubTools/EpubMeta/Util.hs
@@ -1,14 +1,12 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubMeta.Util
-   ( EM , runEM
-   , liftIO
-   , throwError
-   )
-   where
+  ( EM , runEM
+  , liftIO
+  , throwError
+  )
+  where
 
-import Control.Monad.Except
+import Control.Monad.Except (ExceptT, runExceptT, throwError)
+import Control.Monad.Trans (liftIO)
 
 
 type EM a = (ExceptT String IO) a
diff --git a/src/app/EpubTools/EpubMeta/View.hs b/src/app/EpubTools/EpubMeta/View.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/View.hs
@@ -0,0 +1,25 @@
+module EpubTools.EpubMeta.View
+  ( view )
+  where
+
+import Codec.Epub (format, getGuide, getManifest, getMetadata, getPackage,
+  getPkgXmlFromZip, getSpine)
+import Control.Monad (when)
+
+import EpubTools.EpubMeta.Opts (EpubPath (..), Verbose (..))
+import EpubTools.EpubMeta.Util (EM, liftIO)
+
+
+view :: Verbose -> EpubPath -> EM ()
+view (Verbose verbose) (EpubPath zipPath) = do
+  xml <- getPkgXmlFromZip zipPath
+
+  pkg <- format `fmap` getPackage xml
+  meta <- format `fmap` getMetadata xml
+  liftIO $ mapM_ putStr [pkg, meta]
+
+  when verbose $ do
+    mf <- format `fmap` getManifest xml
+    spine <- format `fmap` getSpine xml
+    guide <- format `fmap` getGuide xml
+    liftIO $ mapM_ putStr [mf, spine, guide]
diff --git a/src/app/EpubTools/EpubName/Common.hs b/src/app/EpubTools/EpubName/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Common.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+
+module EpubTools.EpubName.Common
+  ( BookFiles (..), InteractiveSwitch (..), MoveSwitch (..)
+  , NoActionSwitch (..), Options (..), PublisherSwitch (..), PubYear (..)
+  , RulesLocation (..), RulesLocations (..)
+  , TargetDirs (..), VerbosityLevel (..)
+  , defaultOptions
+  , defaultRulesLocations
+  , intToVerbosity
+  )
+  where
+
+import Data.List.NonEmpty (NonEmpty, fromList, singleton)
+import System.FilePath ((</>), (<.>))
+
+
+data PubYear
+   = AnyDate      -- Default
+      -- Use any date found in this order: Issued, Created, Epub (date element), Modified
+   | NoModified   -- Don't use Modified date: Issued, Created, Epub (date element)
+   | NoDate       -- Don't do publication date at all
+
+newtype InteractiveSwitch = InteractiveSwitch { v :: Bool }
+
+newtype NoActionSwitch = NoActionSwitch { v :: Bool }
+
+newtype PublisherSwitch = PublisherSwitch { v :: Bool }
+
+newtype RulesLocations = RulesLocations (NonEmpty RulesLocation)
+
+data RulesLocation
+  = RulesPath FilePath
+  | RulesViaEnv String FilePath
+  | BuiltinRules
+
+newtype TargetDirs = TargetDirs { v :: NonEmpty FilePath }
+
+newtype MoveSwitch = MoveSwitch { v :: Bool }
+
+data VerbosityLevel = Normal | ShowFormatter | ShowBookInfo
+  deriving (Eq, Ord)
+
+newtype BookFiles = BookFiles { v :: NonEmpty FilePath }
+
+data Options = Options
+  { pubYear           :: PubYear
+  , interactive       :: InteractiveSwitch
+  , noAction          :: NoActionSwitch
+  , includePublisher  :: PublisherSwitch
+  , rulesPaths        :: RulesLocations
+  , targetDirs        :: TargetDirs
+  , move              :: MoveSwitch
+  , verbosityLevel    :: VerbosityLevel
+  , bookFiles         :: BookFiles
+  }
+
+
+intToVerbosity :: Int -> VerbosityLevel
+intToVerbosity 0 = Normal
+intToVerbosity 1 = ShowFormatter
+intToVerbosity _ = ShowBookInfo
+
+
+defaultRulesFile :: FilePath
+defaultRulesFile = "default" <.> "rules"
+
+
+defaultRulesLocations :: RulesLocations
+defaultRulesLocations = RulesLocations $ fromList
+  [ RulesViaEnv "HOME" $ ".config" </> "epubtools" </> defaultRulesFile   -- UNIX-like
+  , RulesViaEnv "HOME" $ ".epubtools" </> defaultRulesFile                -- UNIX-like, old-school
+  , RulesViaEnv "APPDATA" $ "epubtools" </> defaultRulesFile              -- Windows
+  , BuiltinRules
+  ]
+
+
+defaultOptions :: Options
+defaultOptions = Options
+  { pubYear           = AnyDate
+  , interactive       = InteractiveSwitch False
+  , noAction          = NoActionSwitch False
+  , includePublisher  = PublisherSwitch False
+  , rulesPaths        = defaultRulesLocations
+  , targetDirs        = TargetDirs (singleton ".")
+  , move              = MoveSwitch False
+  , verbosityLevel    = Normal
+  , bookFiles         = BookFiles (singleton "dummy-filename")
+  }
diff --git a/src/app/EpubTools/EpubName/Doc/Dsl.hs b/src/app/EpubTools/EpubName/Doc/Dsl.hs
--- a/src/app/EpubTools/EpubName/Doc/Dsl.hs
+++ b/src/app/EpubTools/EpubName/Doc/Dsl.hs
@@ -1,183 +1,182 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
+{-# LANGUAGE QuasiQuotes #-}
 
 module EpubTools.EpubName.Doc.Dsl
    ( docs
    )
    where
 
+import Text.Heredoc (here)
 
+
 docs :: String
-docs = unlines
-   [ "epubname rules documentation"
-   , "----------------------------"
-   , ""
-   , "Book naming is controlled by a set of rules loaded from a file "
-   , ""
-   , "At runtime, a rules file is searched for, first in the user's home directory and then in the installed files for epubname. The paths of these files specific to your system are listed in --help output."
-   , ""
-   , "The rules are checked, from first to last, for each file being renamed. The first rule that succeeds is used."
-   , ""
-   , ""
-   , "Rules have the following format:"
-   , ""
-   , "   label"
-   , "      authorMatch \"...\""
-   , "      subjectMatch \"...\""
-   , "      titlePat \"...\""
-   , "      name \"...\""
-   , ""
-   , "label is to identify what the rule is for to the user and also for verbose output (helpful for debugging rules)"
-   , ""
-   , "The indented fields are rule commands and consist of:"
-   , ""
-   , "   authorMatch - Optional. If present, it's a regexp pattern that must"
-   , "      match an author in the book's metadata. If it doesn't match, this"
-   , "      rule is rejected. This may be a partial match, \"bar\" will match if"
-   , "      any author looks like \"foo bar baz\""
-   , ""
-   , "   subjectMatch - Optional. If present, it's a regexp pattern that must"
-   , "      match a subject in the book's metadata. If it doesn't match, this"
-   , "      rule is rejected. This may be a partial match, \"bar\" will match if"
-   , "      any subject looks like \"foo bar baz\""
-   , ""
-   , "   titlePat - a regular expression for picking apart the title string in"
-   , "      the book's metadata. The pieces in capture groups ( ) are exposed "
-   , "      as numbered matches in the name construction field. If this match"
-   , "      fails, this rule is rejected."
-   , ""
-   , "   name - instructions for building up a new book filename, most often"
-   , "      using the parts extracted in titlePat. More on this below"
-   , ""
-   , ""
-   , "In the name command, there are several functions that can be used. These functions are delimited with parenthesis in the name command's expression."
-   , ""
-   , "   authors - Get formatted authors from the metadata. If there are any"
-   , "      authors, a hyphen will be placed after them in the resulting string."
-   , ""
-   , "      syntax: (authors)"
-   , ""
-   , "   INT - Retrieve the text of a specific numbered item from the title"
-   , "      pattern match results"
-   , ""
-   , "      syntax: (INT)"
-   , ""
-   , "   monthNum INDEX - Construct a numeric month from a variety of English"
-   , "      months strings. Including abbreviations and ranges"
-   , ""
-   , "      syntax: (monthNum INT)"
-   , ""
-   , "      examples:"
-   , "         \"January\"           -> \"01\""
-   , "         \"Feb\"               -> \"02\""
-   , "         \"Mar-Apr\"           -> \"03_04\""
-   , "         \"September/October\" -> \"09_10\""
-   , ""
-   , "   pad WIDTH INDEX - Pad a numeric value from the pattern match results"
-   , ""
-   , "      syntax: (pad INT INT)"
-   , ""
-   , "      examples:"
-   , "         2 \"3\"    -> \"03\""
-   , "         3 \"10\"   -> \"010\""
-   , ""
-   , "   publisher - Get the formatted publisher file-as string from the"
-   , "      metadata"
-   , ""
-   , "      syntax: (publisher)"
-   , ""
-   , "   scrub INDEX - Clean up an item from the pattern match results. A"
-   , "      large number of punctuation is removed and/or converted into"
-   , "      other characters, and the string is CamelCased. In the end, only"
-   , "      alphanumerics, - and _ are used."
-   , ""
-   , "      syntax: (scrub INT)"
-   , ""
-   , "   wordNum WIDTH INDEX - Construct a numeric string from an English word"
-   , "      for a number, with padding. Works on strings from \"One\" to \"Twenty\""
-   , ""
-   , "      syntax: (wordNum INT INT)"
-   , ""
-   , "      example:"
-   , "         2 \"One\"    -> \"01\""
-   , "         3 \"Twenty\" -> \"020\""
-   , ""
-   , "   year - Get the formatted publication year from the metadata"
-   , ""
-   , "      syntax: (year)"
-   , ""
-   , "   Anything else, not in parenthesis, is a string literal to be used at"
-   , "   that position in the resulting string."
-   , ""
-   , ""
-   , "This probably seems confusing, some examples may help."
-   , ""
-   , "This one is a fictitious magazine with issue and date info in the title:"
-   , ""
-   , "   original book metadata:"
-   , "      title: Adventurer's Weekly Issue 53, May 2008"
-   , ""
-   , "   epubname rule:"
-   , "      magAdventureWeekly"
-   , "         titlePat \"(Adventurer's Weekly) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$\""
-   , "         name \"(scrub 1)Magazine(pad 3 2)_(4)-(monthNum 3)\""
-   , ""
-   , "   resulting title:"
-   , "      \"AdventurersWeeklyMagazine053_2008-05\""
-   , ""
-   , ""
-   , "Here's one using some more functions:"
-   , ""
-   , "   original book metadata:"
-   , "      title: Thrilling Journeys Three"
-   , "      creator"
-   , "         role: aut"
-   , "         text: The Thrilling Authors"
-   , ""
-   , "   epubname rule:"
-   , "      compThrillingJourneys"
-   , "         author \"Thrilling Authors\""
-   , "         titlePat \"(Thrilling Journeys) (.*)\""
-   , "         name \"(scrub 1)(wordNum 2 2)\""
-   , ""
-   , "   resulting title:"
-   , "      \"ThrillingJourneys03\""
-   , ""
-   , ""
-   , "And here's what happens when an ordinary (non-periodical) book is formatted:"
-   , ""
-   , "   original book metadata:"
-   , "      title: Springtime in the Village"
-   , "      creator"
-   , "         role: aut"
-   , "         file-as: Dandridge, Suzanne"
-   , "         text: Suzanne Dandridge"
-   , "      date"
-   , "         event: publication"
-   , "         text: 2012-10-09"
-   , ""
-   , "   epubname rule:"
-   , "      ordinary_book"
-   , "         titlePat \"(.*)\""
-   , "         name \"(authors)(scrub 1)(year)\""
-   , ""
-   , "   resulting title:"
-   , "      \"DandridgeSuzanne-SpringtimeInTheVillage_2012\""
-   , ""
-   , ""
-   , "The last rule in the built-in rules, ordinary_book is very important, and looks like this:"
-   , ""
-   , "   ordinary_book"
-   , "      titlePat \"(.*)\""
-   , "      name \"(authors)(scrub 1)(year)\""
-   , ""
-   , "This rule will match any book and is used to format all simple author-title situations. This rules is responsible for the basic epubname behavior and should always be present and always be last."
-   , ""
-   , ""
-   , "Preceding a rule label with ! will \"switch off\" the entire rule. It's a simple way to comment out a single rule in its entirety. example:"
-   , ""
-   , "   !compThrillingJourneys"
-   , "      author \"Thrilling Authors\""
-   , "      titlePat \"(Thrilling Journeys) (.*)\""
-   , "      name \"(scrub 1)(wordNum 2 2)\""
-   ]
+docs = [here|epubname rules documentation
+----------------------------
+
+Book naming is controlled by a set of rules loaded from a file 
+
+At runtime, a rules file is searched for, first in the user's home directory and then in the installed files for epubname. The paths of these files specific to your system are listed in --help output.
+
+The rules are checked, from first to last, for each file being renamed. The first rule that succeeds is used.
+
+
+Rules have the following format:
+
+   label
+      authorMatch "..."
+      subjectMatch "..."
+      titlePat "..."
+      name "..."
+
+label is to identify what the rule is for to the user and also for verbose output (helpful for debugging rules)
+
+The indented fields are rule commands and consist of:
+
+   authorMatch - Optional. If present, it's a regexp pattern that must
+      match an author in the book's metadata. If it doesn't match, this
+      rule is rejected. This may be a partial match, "bar" will match if
+      any author looks like "foo bar baz"
+
+   subjectMatch - Optional. If present, it's a regexp pattern that must
+      match a subject in the book's metadata. If it doesn't match, this
+      rule is rejected. This may be a partial match, "bar" will match if
+      any subject looks like "foo bar baz"
+
+   titlePat - a regular expression for picking apart the title string in
+      the book's metadata. The pieces in capture groups ( ) are exposed 
+      as numbered matches in the name construction field. If this match
+      fails, this rule is rejected.
+
+   name - instructions for building up a new book filename, most often
+      using the parts extracted in titlePat. More on this below
+
+
+In the name command, there are several functions that can be used. These functions are delimited with parenthesis in the name command's expression.
+
+   authors - Get formatted authors from the metadata. If there are any
+      authors, a hyphen will be placed after them in the resulting string.
+
+      syntax: (authors)
+
+   INT - Retrieve the text of a specific numbered item from the title
+      pattern match results
+
+      syntax: (INT)
+
+   monthNum INDEX - Construct a numeric month from a variety of English
+      months strings. Including abbreviations and ranges
+
+      syntax: (monthNum INT)
+
+      examples:
+         "January"           -> "01"
+         "Feb"               -> "02"
+         "Mar-Apr"           -> "03_04"
+         "September/October" -> "09_10"
+
+   pad WIDTH INDEX - Pad a numeric value from the pattern match results
+
+      syntax: (pad INT INT)
+
+      examples:
+         2 "3"    -> "03"
+         3 "10"   -> "010"
+
+   publisher - Get the formatted publisher file-as string from the
+      metadata
+
+      syntax: (publisher)
+
+   scrub INDEX - Clean up an item from the pattern match results. A
+      large number of punctuation is removed and/or converted into
+      other characters, and the string is CamelCased. In the end, only
+      alphanumerics, - and _ are used.
+
+      syntax: (scrub INT)
+
+   wordNum WIDTH INDEX - Construct a numeric string from an English word
+      for a number, with padding. Works on strings from "One" to "Twenty"
+
+      syntax: (wordNum INT INT)
+
+      example:
+         2 "One"    -> "01"
+         3 "Twenty" -> "020"
+
+   year - Get the formatted publication year from the metadata
+
+      syntax: (year)
+
+   Anything else, not in parenthesis, is a string literal to be used at
+   that position in the resulting string.
+
+
+This probably seems confusing, some examples may help.
+
+This one is a fictitious magazine with issue and date info in the title:
+
+   original book metadata:
+      title: Adventurer's Weekly Issue 53, May 2008
+
+   epubname rule:
+      magAdventureWeekly
+         titlePat "(Adventurer's Weekly) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$"
+         name "(scrub 1)Magazine(pad 3 2)_(4)-(monthNum 3)"
+
+   resulting title:
+      "AdventurersWeeklyMagazine053_2008-05"
+
+
+Here's one using some more functions:
+
+   original book metadata:
+      title: Thrilling Journeys Three
+      creator
+         role: aut
+         text: The Thrilling Authors
+
+   epubname rule:
+      compThrillingJourneys
+         author "Thrilling Authors"
+         titlePat "(Thrilling Journeys) (.*)"
+         name "(scrub 1)(wordNum 2 2)"
+
+   resulting title:
+      "ThrillingJourneys03"
+
+
+And here's what happens when an ordinary (non-periodical) book is formatted:
+
+   original book metadata:
+      title: Springtime in the Village
+      creator
+         role: aut
+         file-as: Dandridge, Suzanne
+         text: Suzanne Dandridge
+      date
+         event: publication
+         text: 2012-10-09
+
+   epubname rule:
+      ordinary_book
+         titlePat "(.*)"
+         name "(authors)(scrub 1)(year)"
+
+   resulting title:
+      "DandridgeSuzanne-SpringtimeInTheVillage_2012"
+
+
+The last rule in the built-in rules, ordinary_book is very important, and looks like this:
+
+   ordinary_book
+      titlePat "(.*)"
+      name "(authors)(scrub 1)(year)"
+
+This rule will match any book and is used to format all simple author-title situations. This rules is responsible for the basic epubname behavior and should always be present and always be last.
+
+
+Preceding a rule label with ! will "switch off" the entire rule. It's a simple way to comment out a single rule in its entirety. example:
+
+   !compThrillingJourneys
+      author "Thrilling Authors"
+      titlePat "(Thrilling Journeys) (.*)"
+      name "(scrub 1)(wordNum 2 2)"|]
diff --git a/src/app/EpubTools/EpubName/Doc/Rules.hs b/src/app/EpubTools/EpubName/Doc/Rules.hs
--- a/src/app/EpubTools/EpubName/Doc/Rules.hs
+++ b/src/app/EpubTools/EpubName/Doc/Rules.hs
@@ -1,137 +1,136 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
+{-# LANGUAGE QuasiQuotes #-}
 
 module EpubTools.EpubName.Doc.Rules
    ( defaults
    )
    where
 
+import Text.Heredoc (here)
 
+
 defaults :: String
-defaults = unlines
-   [ "magAeon"
-   , "   titlePat \"^A[eE]on ([^ ]+)$\""
-   , "   name \"AeonMagazine(wordNum 2 1)\""
-   , ""
-   , "magApex"
-   , "   titlePat \"^(Apex)[^0-9]*([0-9]{1,3}).*\""
-   , "   name \"(scrub 1)Magazine(pad 3 2)\""
-   , ""
-   , "magAnalog"
-   , "   titlePat \"^(A[^ ]*).*, ([^ ]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)SF(3)-(monthNum 2)\""
-   , ""
-   , "magBcs"
-   , "   titlePat \"(Beneath Ceaseless.*) #([0-9]+).*\""
-   , "   name \"(scrub 1)_Issue(pad 3 2)\""
-   , ""
-   , "magBlackStatic"
-   , "   titlePat \"^(Black Static Horror Magazine)[^0-9]*([0-9]+)$\""
-   , "   name \"(scrub 1)(pad 2 2)\""
-   , ""
-   , "magChallengingDestiny"
-   , "   titlePat \"^(Challenging Destiny) #([0-9]+).*\""
-   , "   name \"(scrub 1)Magazine(pad 3 2)\""
-   , ""
-   , "magClarkesworld"
-   , "   titlePat \"^(Clarkesworld)[^0-9]*([0-9]+)$\""
-   , "   name \"(1)(pad 3 2)\""
-   , ""
-   , "magFantasyMag"
-   , "   titlePat \"^(Fantasy Magazine)[^0-9]+([0-9]+).*\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magEclipse_word"
-   , "   titlePat \"^(Eclipse) +([a-zA-Z]+)$\""
-   , "   name \"(1)(wordNum 2 2)\""
-   , ""
-   , "magEclipse_num"
-   , "   titlePat \"^(Eclipse) +([0-9]+):.*\""
-   , "   name \"(1)(pad 2 2)\""
-   , ""
-   , "magFsf"
-   , "   authorMatch \"Spilogale\""
-   , "   titlePat \".* ([^ ]+) ([0-9]{4})$\""
-   , "   name \"FantasyScienceFiction(2)-(monthNum 1)\""
-   , ""
-   , "magFutureOrbits"
-   , "   titlePat \"(Future Orbits) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)Magazine(pad 2 2)_(4)-(monthNum 3)\""
-   , ""
-   , "magGud"
-   , "   authorMatch \"GUD Magazine Authors\""
-   , "   titlePat \".* Magazine Issue ([0-9]+) ::.*\""
-   , "   name \"GUDMagazine(pad 2 1)\""
-   , ""
-   , "magInterzone"
-   , "   titlePat \"^(Interzone|INTERZONE)[^0-9]+([0-9]+).*\""
-   , "   name \"(scrub 1)SFF(pad 3 2)\""
-   , ""
-   , "magLightspeed_date"
-   , "   titlePat \"(Lightspeed) Magazine, (.*) (.*)\""
-   , "   name \"(1)(3)-(monthNum 2)\""
-   , ""
-   , "magLightspeed_issue"
-   , "   titlePat \"(Lightspeed).* Issue ([^ ]+)(.*)\""
-   , "   name \"(1)(pad 3 2)\""
-   , ""
-   , "magLunaStationQuarterly"
-   , "   titlePat \"(Luna Station Quarterly) - Issue ([0-9]+)\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magNemesis"
-   , "   titlePat \"(Nemesis Mag)azine #([0-9]+).*\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magRageMachine"
-   , "   titlePat \"(Rage Machine.*)--([^ ]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)_(3)-(monthNum 2)\""
-   , ""
-   , "magSomethingWicked"
-   , "   titlePat \"^(Something Wicked)[^0-9]*([0-9]+)\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magWeirdTales"
-   , "   titlePat \"^(Weird Tales)[^0-9]*([0-9]+)$\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magLocus"
-   , "   titlePat \"^(Locus), ([a-zA-Z]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)Magazine(3)-(monthNum 2)\""
-   , ""
-   , "magGenericWithIssue"
-   , "   titlePat \"(.* Magazine):? Issue ([0-9]+).*\""
-   , "   name \"(scrub 1)(pad 2 2)\""
-   , ""
-   , "magGenericSubjWithIssue"
-   , "   subjectMatch \"magazine\""
-   , "   titlePat \"(.*) Issue ([0-9]+).*\""
-   , "   name \"(scrub 1)Magazine(pad 2 2)\""
-   , ""
-   , "magGenericVolNo3"
-   , "   titlePat \"(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{3}).*\""
-   , "   name \"(scrub 1)Vol(pad 2 3)No(pad 3 5)\""
-   , ""
-   , "magGenericVolNo2"
-   , "   titlePat \"(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{1,2})[^0-9]?.*\""
-   , "   name \"(scrub 1)Vol(pad 2 3)No(pad 2 5)\""
-   , ""
-   , "nonficWomensInstituteLibrary"
-   , "   authorMatch \"Woman's Institute\""
-   , "   titlePat \"(.*)\""
-   , "   name \"(scrub 1)\""
-   , ""
-   , "anthology_date"
-   , "   subjectMatch \"anthology\""
-   , "   titlePat \"(.*) - ([^ ]+) ([0-9]{4})\""
-   , "   name \"(scrub 1)(3)-(monthNum 2)\""
-   , ""
-   , "anthology"
-   , "   subjectMatch \"anthology\""
-   , "   titlePat \"(.*)\""
-   , "   name \"(scrub 1)(year)\""
-   , ""
-   , "ordinary_book"
-   , "   titlePat \"(.*)\""
-   , "   name \"(authors)(scrub 1)(year)\""
-   ]
+defaults = [here|magAeon
+   titlePat "^A[eE]on ([^ ]+)$"
+   name "AeonMagazine(wordNum 2 1)"
+
+magApex
+   titlePat "^(Apex)[^0-9]*([0-9]{1,3}).*"
+   name "(scrub 1)Magazine(pad 3 2)"
+
+magAnalog
+   titlePat "^(A[^ ]*).*, ([^ ]+) ([0-9]{4})$"
+   name "(scrub 1)SF(3)-(monthNum 2)"
+
+magBcs
+   titlePat "(Beneath Ceaseless.*) #([0-9]+).*"
+   name "(scrub 1)_Issue(pad 3 2)"
+
+magBlackStatic
+   titlePat "^(Black Static Horror Magazine)[^0-9]*([0-9]+)$"
+   name "(scrub 1)(pad 2 2)"
+
+magChallengingDestiny
+   titlePat "^(Challenging Destiny) #([0-9]+).*"
+   name "(scrub 1)Magazine(pad 3 2)"
+
+magClarkesworld
+   titlePat "^(Clarkesworld)[^0-9]*([0-9]+)$"
+   name "(1)(pad 3 2)"
+
+magFantasyMag
+   titlePat "^(Fantasy Magazine)[^0-9]+([0-9]+).*"
+   name "(scrub 1)(pad 3 2)"
+
+magEclipse_word
+   titlePat "^(Eclipse) +([a-zA-Z]+)$"
+   name "(1)(wordNum 2 2)"
+
+magEclipse_num
+   titlePat "^(Eclipse) +([0-9]+):.*"
+   name "(1)(pad 2 2)"
+
+magFsf
+   authorMatch "Spilogale"
+   titlePat ".* ([^ ]+) ([0-9]{4})$"
+   name "FantasyScienceFiction(2)-(monthNum 1)"
+
+magFutureOrbits
+   titlePat "(Future Orbits) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$"
+   name "(scrub 1)Magazine(pad 2 2)_(4)-(monthNum 3)"
+
+magGud
+   authorMatch "GUD Magazine Authors"
+   titlePat ".* Magazine Issue ([0-9]+) ::.*"
+   name "GUDMagazine(pad 2 1)"
+
+magInterzone
+   titlePat "^(Interzone|INTERZONE)[^0-9]+([0-9]+).*"
+   name "(scrub 1)SFF(pad 3 2)"
+
+magLightspeed_date
+   titlePat "(Lightspeed) Magazine, (.*) (.*)"
+   name "(1)(3)-(monthNum 2)"
+
+magLightspeed_issue
+   titlePat "(Lightspeed).* Issue ([^ ]+)(.*)"
+   name "(1)(pad 3 2)"
+
+magLunaStationQuarterly
+   titlePat "(Luna Station Quarterly) - Issue ([0-9]+)"
+   name "(scrub 1)(pad 3 2)"
+
+magNemesis
+   titlePat "(Nemesis Mag)azine #([0-9]+).*"
+   name "(scrub 1)(pad 3 2)"
+
+magRageMachine
+   titlePat "(Rage Machine.*)--([^ ]+) ([0-9]{4})$"
+   name "(scrub 1)_(3)-(monthNum 2)"
+
+magSomethingWicked
+   titlePat "^(Something Wicked)[^0-9]*([0-9]+)"
+   name "(scrub 1)(pad 3 2)"
+
+magWeirdTales
+   titlePat "^(Weird Tales)[^0-9]*([0-9]+)$"
+   name "(scrub 1)(pad 3 2)"
+
+magLocus
+   titlePat "^(Locus), ([a-zA-Z]+) ([0-9]{4})$"
+   name "(scrub 1)Magazine(3)-(monthNum 2)"
+
+magGenericWithIssue
+   titlePat "(.* Magazine):? Issue ([0-9]+).*"
+   name "(scrub 1)(pad 2 2)"
+
+magGenericSubjWithIssue
+   subjectMatch "magazine"
+   titlePat "(.*) Issue ([0-9]+).*"
+   name "(scrub 1)Magazine(pad 2 2)"
+
+magGenericVolNo3
+   titlePat "(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{3}).*"
+   name "(scrub 1)Vol(pad 2 3)No(pad 3 5)"
+
+magGenericVolNo2
+   titlePat "(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{1,2})[^0-9]?.*"
+   name "(scrub 1)Vol(pad 2 3)No(pad 2 5)"
+
+nonficWomensInstituteLibrary
+   authorMatch "Woman's Institute"
+   titlePat "(.*)"
+   name "(scrub 1)"
+
+anthology_date
+   subjectMatch "anthology"
+   titlePat "(.*) - ([^ ]+) ([0-9]{4})"
+   name "(scrub 1)(3)-(monthNum 2)"
+
+anthology
+   subjectMatch "anthology"
+   titlePat "(.*)"
+   name "(scrub 1)(year)"
+
+ordinary_book
+   titlePat "(.*)"
+   name "(authors)(scrub 1)(year)"|]
diff --git a/src/app/EpubTools/EpubName/Format/Author.hs b/src/app/EpubTools/EpubName/Format/Author.hs
--- a/src/app/EpubTools/EpubName/Format/Author.hs
+++ b/src/app/EpubTools/EpubName/Format/Author.hs
@@ -1,21 +1,18 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubName.Format.Author
    ( extractAuthors
    , authorMatches
    )
    where
 
-import Codec.Epub.Data.Metadata
-import Control.Monad
+import Codec.Epub.Data.Metadata (Creator (..), Metadata (metaCreators))
+import Control.Monad ((>=>), mplus, unless)
 import Data.List ( intercalate )
 import Data.Maybe ( isJust )
 import Prelude hiding ( last )
-import Text.Printf
-import Text.Regex
+import Text.Printf (printf)
+import Text.Regex (matchRegex, mkRegex, splitRegex)
 
-import EpubTools.EpubName.Format.Util
+import EpubTools.EpubName.Format.Util (EN, asks, gMetadata, scrubString, throwError)
 
 
 extractAuthors :: EN String
diff --git a/src/app/EpubTools/EpubName/Format/Compile.hs b/src/app/EpubTools/EpubName/Format/Compile.hs
--- a/src/app/EpubTools/EpubName/Format/Compile.hs
+++ b/src/app/EpubTools/EpubName/Format/Compile.hs
@@ -1,16 +1,15 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
 
 module EpubTools.EpubName.Format.Compile
    where
 
-import Data.Maybe ( catMaybes )
+import Data.Maybe (catMaybes)
 import Text.ParserCombinators.Parsec
 
-import EpubTools.EpubName.Format.Author
-import EpubTools.EpubName.Format.Format
+import EpubTools.EpubName.Format.Author (authorMatches)
+import EpubTools.EpubName.Format.Format (Formatter (..), ReplF, authors,
+  extractTitle, index, literal, monthNum, pad, publisher, scrub,
+  subjectMatches, wordNum, year)
 
 
 parseRules :: SourceName -> String -> Either ParseError [Formatter]
diff --git a/src/app/EpubTools/EpubName/Format/Format.hs b/src/app/EpubTools/EpubName/Format/Format.hs
--- a/src/app/EpubTools/EpubName/Format/Format.hs
+++ b/src/app/EpubTools/EpubName/Format/Format.hs
@@ -1,7 +1,4 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleContexts, OverloadedRecordDot #-}
 
 module EpubTools.EpubName.Format.Format
    ( ReplF
@@ -14,17 +11,23 @@
    )
    where
 
-import Codec.Epub.Data.Metadata
-import Control.Monad.Except
+import Codec.Epub.Data.Metadata (Creator (..), Metadata (..), titleText)
+import Control.Monad (mplus, unless)
+import Control.Monad.Except (MonadError)
+import Control.Monad.Trans (MonadIO)
 import Data.List ( isPrefixOf )
 import Data.Maybe ( isJust, listToMaybe )
-import Text.Printf
-import Text.Regex
+import Text.Printf (printf)
+import Text.Regex (matchRegex, mkRegex, mkRegexWithOpts)
 
-import EpubTools.EpubName.Format.Author
-import EpubTools.EpubName.Format.PubYear
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Opts
+import EpubTools.EpubName.Common
+  ( Options (includePublisher)
+  , PublisherSwitch (..)
+  )
+import EpubTools.EpubName.Format.Author (extractAuthors)
+import EpubTools.EpubName.Format.PubYear (getPubYear)
+import EpubTools.EpubName.Format.Util (EN, Globals (gMetadata, gOpts), asks,
+  runEN, sanitizeString, scrubString, throwError)
 
 
 type ReplF = [String] -> EN String
@@ -113,7 +116,8 @@
 publisher :: ReplF
 publisher _ = do
    md <- asks gMetadata
-   fmap (extractPublisher md) $ asks $ optPublisher . gOpts
+   opts <- asks gOpts
+   pure $ extractPublisher md opts.includePublisher.v
 
 
 -- Clean up an item from the pattern match results
diff --git a/src/app/EpubTools/EpubName/Format/PubYear.hs b/src/app/EpubTools/EpubName/Format/PubYear.hs
--- a/src/app/EpubTools/EpubName/Format/PubYear.hs
+++ b/src/app/EpubTools/EpubName/Format/PubYear.hs
@@ -1,35 +1,35 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubName.Format.PubYear
-   ( extractYear
-   , getPubYear
-   )
-   where
+  ( extractYear
+  , getPubYear
+  )
+  where
 
-import Codec.Epub.Data.Metadata ( DateValue (..), DateEvent (Created, Date,
-  Epub, Issued, Modified), Metadata (metaDates) )
-import Control.Monad ( (<=<) )
+import Control.Applicative (asum)
+import Codec.Epub.Data.Metadata (DateValue (..), DateEvent (Created, Date,
+  Epub, Issued, Modified), Metadata (metaDates))
+import Control.Monad ((<=<))
 import qualified Data.Map.Strict as Map
-import Data.Monoid ( First (..), getFirst )
-import Text.Regex ( matchRegex, mkRegex )
+import Text.Regex (matchRegex, mkRegex)
 
-import EpubTools.EpubName.Opts
-import EpubTools.EpubName.Format.Util
+import EpubTools.EpubName.Common
+  ( Options (pubYear)
+  , PubYear (AnyDate, NoDate, NoModified)
+  )
+import EpubTools.EpubName.Format.Util (EN, Globals (gMetadata, gOpts), asks)
 
 
 {- Look for publication date, return "" if none found
 -}
 getPubYear :: EN String
 getPubYear = do
-  yearHandling <- asks $ optPubYear . gOpts
+  yearHandling <- asks $ pubYear . gOpts
   let events = case yearHandling of
         AnyDate -> [Issued, Created, Date, Epub, Modified]
         NoModified -> [Issued, Created, Date, Epub]
         NoDate -> []
   datesMap <- asks (metaDates . gMetadata)
   let fs = map (flip Map.lookup $ datesMap) events
-  pure . maybe "" ('_' :) . (extractYear <=< getFirst) . mconcat . map First $ fs
+  pure . maybe "" ('_' :) . (extractYear <=< asum) $ fs
 
 
 extractYear :: DateValue -> Maybe String
diff --git a/src/app/EpubTools/EpubName/Format/Util.hs b/src/app/EpubTools/EpubName/Format/Util.hs
--- a/src/app/EpubTools/EpubName/Format/Util.hs
+++ b/src/app/EpubTools/EpubName/Format/Util.hs
@@ -1,26 +1,26 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubName.Format.Util
-   ( Globals (..)
-   , EN , runEN
-   , throwError
-   , asks
-   , scrubString
-   , sanitizeString
-   )
-   where
+  ( EN
+  , Globals (..)
+  , runEN
+  , scrubString
+  , sanitizeString
 
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
-import Control.Monad.Except
-import Control.Monad.Identity
-import Control.Monad.Reader
-import Data.Char
-import Data.List ( foldl' )
-import Text.Regex
+  -- re-exporting
+  , asks
+  , throwError
+  )
+  where
 
-import EpubTools.EpubName.Opts
+import Codec.Epub.Data.Metadata (Metadata)
+import Codec.Epub.Data.Package (Package)
+import Control.Monad.Except (ExceptT, runExceptT, throwError)
+import Control.Monad.Identity (Identity, runIdentity)
+import Control.Monad.Reader (ReaderT, asks, runReaderT)
+import Data.Char (toLower, toUpper)
+import Data.List (foldl')
+import Text.Regex (mkRegex, subRegex)
+
+import EpubTools.EpubName.Common (Options)
 
 
 data Globals = Globals
diff --git a/src/app/EpubTools/EpubName/Main.hs b/src/app/EpubTools/EpubName/Main.hs
--- a/src/app/EpubTools/EpubName/Main.hs
+++ b/src/app/EpubTools/EpubName/Main.hs
@@ -1,45 +1,68 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleContexts, TupleSections #-}
 
 module EpubTools.EpubName.Main
-   ( initialize
-   )
-   where
+  ( initialize
+  )
+  where
 
-import Control.Monad.Except
-import System.Directory ( doesFileExist )
-import System.Exit
-import Text.Printf
+import Control.Applicative (asum)
+import Control.Monad.Except (MonadError, throwError)
+import Control.Monad.Trans (MonadIO, liftIO)
+import Data.List.NonEmpty hiding (map)
+import Data.Maybe (fromMaybe)
+import System.Directory (doesFileExist)
+import System.Environment (lookupEnv)
+import System.Exit (ExitCode)
+import System.FilePath ((</>))
+import System.IO.Error (tryIOError)
+import Text.Printf (PrintfArg, PrintfType, printf)
 
+import EpubTools.EpubName.Common
+  ( Options (rulesPaths, verbosityLevel)
+  , RulesLocation (BuiltinRules, RulesPath, RulesViaEnv), RulesLocations (..)
+  , VerbosityLevel (Normal)
+  )
 import qualified EpubTools.EpubName.Doc.Rules as Rules
-import EpubTools.EpubName.Format.Compile
-import EpubTools.EpubName.Format.Format
-import EpubTools.EpubName.Opts
-import EpubTools.EpubName.Util
+import EpubTools.EpubName.Format.Compile (parseRules)
+import EpubTools.EpubName.Format.Format (Formatter)
+import EpubTools.EpubName.Util (exitInitFailure)
 
 
 initialize :: (MonadError ExitCode m, MonadIO m) =>
    Options -> m [Formatter]
-initialize opts = (locateRules $ optRulesPaths opts)
-   >>= (loadFormatters $ optVerbose opts)
+initialize opts = do
+  erls <- liftIO . tryIOError . locateRules . rulesPaths $ opts
+  rls <- case erls of
+    Right rls' -> pure rls'
+    Left err   -> do
+      _ <- liftIO $ print err
+      throwError exitInitFailure
+  parseFormatters (verbosityLevel opts) rls
 
 
-locateRules :: (MonadError ExitCode m, MonadIO m) =>
-   [FilePath] -> m (String, String)
-locateRules paths = do
-   mbResult <- liftIO $ foldl (liftM2 mplus) (return Nothing)
-      $ map mbExists paths
-   maybe (return ("built-in", Rules.defaults))
-      loadRules mbResult
+locateRules :: RulesLocations -> IO (String, String)
+locateRules (RulesLocations rls) = do
+  let loadActions = map loadRuleFile . toList $ rls
+  mbLoadedFiles <- sequence loadActions
+  pure $ fromMaybe undefined . asum $ mbLoadedFiles
 
-   where
-      loadRules p = do
-         contents <- liftIO $ readFile p
-         return (p, contents)
 
+loadRuleFile :: RulesLocation -> IO (Maybe (String, String))
 
+loadRuleFile (RulesPath filePath) = do
+  mbExistFile <- mbExists filePath
+  case mbExistFile of
+    Nothing -> pure Nothing
+    (Just path) -> (Just . (filePath, )) <$> readFile path
+
+loadRuleFile (RulesViaEnv varName pathSuffix) = do
+  mbPathPrefix <- lookupEnv varName
+  let mbFullPath = (</> pathSuffix) <$> mbPathPrefix
+  maybe (pure Nothing) (loadRuleFile . RulesPath) mbFullPath
+
+loadRuleFile BuiltinRules = pure $ Just ("built-in", Rules.defaults)
+
+
 mbExists :: FilePath -> IO (Maybe FilePath)
 mbExists p = do
    e <- doesFileExist p
@@ -48,9 +71,9 @@
       else return Nothing
 
 
-loadFormatters :: (MonadError ExitCode m, MonadIO m) =>
-   (Maybe Int) -> (String, String) -> m [Formatter]
-loadFormatters verbosity (name, contents) = case parseRules name contents of
+parseFormatters :: (MonadError ExitCode m, MonadIO m) =>
+   VerbosityLevel -> (String, String) -> m [Formatter]
+parseFormatters verbosity (name, contents) = case parseRules name contents of
    Left err  -> do
       _ <- liftIO $ printf "%s:\n%s" name (show err)
       throwError exitInitFailure
@@ -59,7 +82,8 @@
       return fs
 
 
-showRulesSource :: (Monad m, PrintfType (m ()), PrintfArg t, Num a, Eq a) =>
-   Maybe a -> t -> m ()
-showRulesSource (Just 1) name = printf "Rules loaded from: %s\n" name
-showRulesSource _        _    = return ()
+showRulesSource :: (Monad m, PrintfType (m ()), PrintfArg t) =>
+   VerbosityLevel -> t -> m ()
+showRulesSource verbosity name
+  | verbosity > Normal = printf "Rules loaded from: %s\n" name
+  | otherwise = return ()
diff --git a/src/app/EpubTools/EpubName/Opts.hs b/src/app/EpubTools/EpubName/Opts.hs
--- a/src/app/EpubTools/EpubName/Opts.hs
+++ b/src/app/EpubTools/EpubName/Opts.hs
@@ -1,228 +1,213 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
 
 module EpubTools.EpubName.Opts
-   ( PubYear (..), Options (..)
-   , defaultOptions
-   , parseOpts, usageText
-   )
-   where
+  ( parseOpts
+  )
+  where
 
-import Control.Monad.Except
-import Data.Maybe
-import Data.Version ( showVersion )
-import Paths_epub_tools ( version )
-import System.Console.GetOpt
-import System.Environment
-import System.Exit
-import System.FilePath
+import Data.List.NonEmpty ((<|), fromList, singleton)
+import Data.Text.Lazy (Text, pack)
+import Data.Version (showVersion)
+import Options.Applicative.Dex
+import Paths_epub_tools (version)
+import Text.Heredoc (here)
 
-import EpubTools.EpubName.Util
+import EpubTools.EpubName.Common
+  ( BookFiles (..)
+  , InteractiveSwitch (..)
+  , MoveSwitch (..)
+  , NoActionSwitch (..)
+  , Options (Options)
+  , PublisherSwitch (..)
+  , PubYear (AnyDate, NoDate, NoModified)
+  , RulesLocation (RulesPath), RulesLocations (..)
+  , TargetDirs (..)
+  , defaultRulesLocations
+  , intToVerbosity
+  )
+import qualified EpubTools.EpubName.Doc.Dsl as Dsl
+import qualified EpubTools.EpubName.Doc.Rules as Rules
 
 
-data PubYear
-   = AnyDate      -- Default
-      -- Use any date found in this order: Issued, Created, Epub (date element), Modified
-   | NoModified   -- Don't use Modified date: Issued, Created, Epub (date element)
-   | NoDate       -- Don't do publication date at all
+parseOpts :: IO Options
+parseOpts = parseOpts' (parser <**> parseDumpRules <**> parseHelpRules)
+  "Rename epub book files with clear names based on their metadata"
+  footerContent version
 
 
-data Options = Options
-   { optDumpRules    :: Bool
-   , optHelp         :: Bool
-   , optHelpRules    :: Bool
-   , optInteractive  :: Bool
-   , optNoAction     :: Bool
-   , optPublisher    :: Bool
-   , optRulesPaths   :: [FilePath]
-   , optTargetDir    :: FilePath
-   , optVerbose      :: Maybe Int
-   , optPubYear      :: PubYear
-   }
+prependRulesLocation :: RulesLocations -> Maybe FilePath -> RulesLocations
+prependRulesLocation rulesLocations Nothing = rulesLocations
+prependRulesLocation (RulesLocations rulesLocList) (Just filePath) =
+  RulesLocations ((RulesPath filePath) <| rulesLocList)
 
 
-defaultRulesFile :: FilePath
-defaultRulesFile = "default.rules"
+setTargetDirs :: [FilePath] -> TargetDirs
+setTargetDirs [] = TargetDirs $ singleton "."
+setTargetDirs ds = TargetDirs $ fromList ds
 
 
-userRulesPath :: IO (Maybe FilePath)
-userRulesPath = foldl (liftM2 mplus) (return Nothing) $ map mkdir
-   [ ("HOME", ".epubtools")      -- UNIX-like
-   , ("APPDATA", "epubtools")    -- Windows
-   ]
+parser :: Parser Options
+parser = Options
+  <$> ( ( flag AnyDate NoModified
+          (  long "no-modified-date"
+          <> short 'M'
+          <> help "Don't use modified date for publication year"
+          )
+        )
+        <|>
+        ( flag AnyDate NoDate
+          (  long "no-date"
+          <> short 'D'
+          <> help "Suppress inclusion of any publication year"
+          )
+        )
+      )
+  <*> ( InteractiveSwitch <$> switch
+        (  long "interactive"
+        <> short 'i'
+        <> help "Prompt user interactively"
+        )
+      )
+  <*> ( NoActionSwitch <$> switch
+        (  long "no-action"
+        <> short 'n'
+        <> help "Display what would be done, but do nothing"
+        )
+      )
+  <*> ( PublisherSwitch <$> switch
+        (  long "publisher"
+        <> short 'p'
+        <> help "Include book publisher if present. See below"
+        )
+      )
+  <*> ( prependRulesLocation defaultRulesLocations <$> (optional $ strOption
+        (  long "rules"
+        <> short 'r'
+        <> metavar "FILE"
+        <> help "Specify a rules file for naming the books"
+        )
+      ))
+  <*> ( setTargetDirs <$> (many $ strOption
+        (  long "target-dir"
+        <> short 't'
+        <> metavar "DIR"
+        <> help "Target directory for successfully renamed books. Specify multiple times for additional directories (default: \".\")"
+        )
+      ))
+  <*> ( MoveSwitch <$> switch
+        (  long "move"
+        <> help "Remove the original file after linking (or copying) to target directories"
+        )
+      )
+  <*> ( intToVerbosity <$> option auto
+        (  long "verbose"
+        <> short 'v'
+        <> metavar "NUM"
+        <> help "Verbosity level: 0, 1 or 2, see VERBOSITY LEVELS below"
+        <> showDefault
+        <> value 0
+        )
+      )
+  <*> ( BookFiles . fromList <$> (some $ strArgument
+        (  metavar "FILES"
+        <> help "One or more files to rename/move"
+        )
+      ))
 
-   where
-      mkdir (var, subdir) = do
-         homeDir <- lookupEnv var
-         return $ (</> subdir </> defaultRulesFile) `fmap` homeDir
 
+parseDumpRules :: Parser (a -> a)
+parseDumpRules =
+  infoOption Rules.defaults $ mconcat
+  [ long "dump-rules"
+  , help "Output built-in rules definitions, use to make your own"
+  ]
 
-defaultOptions :: IO Options
-defaultOptions = do
-   urp <- userRulesPath
 
-   return Options
-      { optDumpRules       = False
-      , optHelp            = False
-      , optHelpRules       = False
-      , optInteractive     = False
-      , optNoAction        = False
-      , optPublisher       = False
-      , optRulesPaths      = maybeToList urp
-      , optTargetDir       = "."
-      , optVerbose         = Nothing
-      , optPubYear         = AnyDate
-      }
+parseHelpRules :: Parser (a -> a)
+parseHelpRules =
+  infoOption Dsl.docs $ mconcat
+  [ long "help-rules"
+  , help "Help on the naming rules domain specific language"
+  ]
 
 
-options :: [OptDescr (Options -> Options)]
-options =
-   [ Option ['M'] ["no-modified-date"]
-      (NoArg (\opts -> opts { optPubYear = NoModified } ))
-      "Don't use modified date for publication year"
-   , Option ['D'] ["no-date"]
-      (NoArg (\opts -> opts { optPubYear = NoDate } )) 
-      "Suppress inclusion of any publication year"
-   , Option [] ["dump-rules"] 
-      (NoArg (\opts -> opts { optDumpRules = True } ))
-      "Output built-in rules definitions, use to make your own"
-   , Option ['h'] ["help"] 
-      (NoArg (\opts -> opts { optHelp = True } ))
-      "This help text"
-   , Option [] ["help-rules"] 
-      (NoArg (\opts -> opts { optHelpRules = True } ))
-      "Help on the naming rules domain specific language"
-   , Option ['i'] ["interactive"] 
-      (NoArg (\opts -> opts { optInteractive = True } ))
-      "Prompt user interactively"
-   , Option ['n'] ["no-action"]
-      (NoArg (\opts -> opts { optNoAction = True } )) 
-      "Display what would be done, but do nothing"
-   , Option ['p'] ["publisher"]
-      (NoArg (\opts -> opts { optPublisher = True } )) 
-      "Include book publisher if present. See below"
-   , Option ['r'] ["rules"] (ReqArg prependRulesPath "FILE")
-      "Specify a rules file for naming the books"
-   , Option ['t'] ["target-dir"]
-      ( ReqArg
-         (\p opts -> opts { optTargetDir = p } )
-         "DIR")
-      "Target directory for successfully renamed books. Default: ."
-   , Option ['v'] ["verbose"]
-      ( OptArg
-         ((\n opts -> opts { optVerbose = Just (read n)}) . fromMaybe "1")
-         "LEVEL")
-      "Verbosity level: 1, 2"
-   ]
+footerContent :: Text
+footerContent = [here|VERBOSITY LEVELS:
 
+  0 - Normal verbosity
+  1 - Output which formatter processed the file
+  2 - Output the OPF Package and Metadata info
 
-prependRulesPath :: FilePath -> Options -> Options
-prependRulesPath p opts = opts { optRulesPaths = newlist }
-   where
-      newlist = p : optRulesPaths opts
+EXIT CODES:
 
+  0 - success
+  1 - bad options
+  2 - failed to process one or more of the files given
 
-parseOpts :: (MonadError ExitCode m, MonadIO m) =>
-   [String] -> m (Options, [String])
-parseOpts argv = do
-   dos <- liftIO defaultOptions
-   case getOpt Permute options argv of
-      (o,n,[]  ) -> return (foldl (flip id) dos o, n)
-      (_,_,errs) -> do
-         liftIO $ do
-            ut <- usageText
-            putStrLn $ concat errs ++ ut
-         throwError exitInitFailure
+SIMPLE NAMING:
 
+Book names are constructed by examining parts of the OPF Package metadata such as the title, creators, contributors and dates.
 
-usageText :: IO String
-usageText = return $ (usageInfo header options) ++ "\n" ++ footer
-   where
-      header = init $ unlines
-         [ "USAGE: epubname [OPTIONS] FILES"
-         , "Rename epub book files with clear names based on their metadata"
-         , ""
-         , "OPTIONS:"
-         , ""
-         ]
-      footer = init $ unlines
-         [ "VERBOSITY LEVELS:"
-         , ""
-         , "   1 - Output which formatter processed the file"
-         , "   2 - Output the OPF Package and Metadata info"
-         , ""
-         , "EXIT CODES:"
-         , ""
-         , "   0 - success"
-         , "   1 - bad options"
-         , "   2 - failed to process one or more of the files given"
-         , ""
-         , "SIMPLE NAMING:"
-         , ""
-         , "Book names are constructed by examining parts of the OPF Package metadata such as the title, creators, contributors and dates."
-         , ""
-         , "Strings from the OPF metadata fields are stripped of punctuation, CamelCased and stripped of spaces. Resulting file names look like this:"
-         , ""
-         , "For books with a single author:"
-         , "   LastFirst[Middle]-TitleText[_year][_publisher].epub"
-         , ""
-         , "For books with multiple authors:"
-         , "   Last1_Last2[_Last3...]-TitleText[_year][_publisher].epub"
-         , ""
-         , "For books that have no clear authors, such as compilations:"
-         , "   TitleText[_year][_publisher].epub"
-         , ""
-         , "Only creator tags with either a role attribute of 'aut' or no role at all are considered authors. If a file-as attribute is present, this will be the preferred string. If not, the program tries to do some intelligent parsing of the name."
-         , ""
-         , "PUBLICATION DATE:"
-         , ""
-         , "This software will attempt to locate a publication date to use at the end of the filename. This can be suppressed with the --no-date switch. How the date is found depends on the document epub version:"
-         , ""
-         , "epub2: These date values are looked for in this order"
-         , "  issued    <dc:date opf:event='original-publication'>2013-09-17</dc:date>"
-         , "  created   <dc:date opf:event='publication'>2013-09-17T12:00:00Z</dc:date>"
-         , "  epub      <dc:date>2013-09-17</dc:date>"
-         , "  modified  <meta property='dcterms:modified'>2013-09-17</meta>"
-         , ""
-         , "epub3: These date values are looked for in this order"
-         , "  issued    <meta property='dcterms:issued'>2013-09-17</meta>"
-         , "  created   <meta property='dcterms:created'>2013-09-17T12:00:00Z</created>"
-         , "  date      <meta property='dcterms:date'>2013-09-18</meta>"
-         , "  epub      <dc:date>2013-09-17T12:00:00Z</dc:date>"
-         , "  modified  <meta property='dcterms:modified'>2013-09-17</meta>"
-         , ""
-         , "This software will fish the year out of whatever string it can find. Use the examples above as a guide for adding/editing these values yourself."
-         , ""
-         , "PUBLISHER:"
-         , ""
-         , "I wanted to provide a way to have multiple copies of the same book produced by different publishers and name them sort-of unambiguously. I came up with the idea of expecting a contributor tag with role attribute of 'bkp' (so far, this is fairly normal). And then use a file-as attribute on that tag to contain a small string to be used in the filename. The idea here is short and sweet for the file-as."
-         , ""
-         , "MORE COMPLICATED NAMING: MAGAZINES AND COLLECTIONS"
-         , ""
-         , "Magazines are kind of a sticky problem in that it's often desireable to have edition and/or date info in the filename. There's a lot of chaos out there with titling the epub editions of magazines. The solution in this software is to do some pattern matching on multiple fields in the magazine's metadata combined with custom naming rules for specific magazines."
-         , ""
-         , "This software ships with a set of rules to properly name over 20 magazines and compilations that are commonly purchased by the developer. These default rules can be extended to add new publications, and used as an example."
-         , ""
-         , "Another case is anthologies, which is to say: collections of works by multiple authors but not necessarily in periodical form. There is an anthology rule in the default rules that's set to look for any <dc:subject>anthology</dc:subject> tag in the metadata. In this case, the authors will be ignored and the title along with a publication year will be used (see above re: dc:date)"
-         , ""
-         , "epubname will search the following locations for a rules file, in this order:"
-         , ""
-         , "   Path specified in a --rules switch"
-         , "   $HOME/.epubtools/default.rules  -- or:"
-         , "   %APPDATA%\\epubtools\\default.rules  -- in Windows"
-         , "   Built-in rules, a comprehensive stock set of naming rules"
-         , ""
-         , "Use the built-in rules as a model for a custom file. See --dump-rules above"
-         , ""
-         , "Please see --help-rules for more information on the syntax of the DSL in the rules file."
-         , ""
-         , ""
-         , "For more information on the epub format:"
-         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
-         , "   epub3: https://www.w3.org/TR/epub/"
-         , ""
-         , ""
-         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
-         ]
+Strings from the OPF metadata fields are stripped of punctuation, CamelCased and stripped of spaces. Resulting file names look like this:
+
+For books with a single author:
+  LastFirst[Middle]-TitleText[_year][_publisher].epub
+
+For books with multiple authors:
+  Last1_Last2[_Last3...]-TitleText[_year][_publisher].epub
+
+For books that have no clear authors, such as compilations:
+  TitleText[_year][_publisher].epub
+
+Only creator tags with either a role attribute of 'aut' or no role at all are considered authors. If a file-as attribute is present, this will be the preferred string. If not, the program tries to do some intelligent parsing of the name.
+
+PUBLICATION DATE:
+
+This software will attempt to locate a publication date to use at the end of the filename. This can be suppressed with the --no-date switch. How the date is found depends on the document epub version:
+
+epub2: These date values are looked for in this order
+  issued    <dc:date opf:event='original-publication'>2013-09-17</dc:date>
+  created   <dc:date opf:event='publication'>2013-09-17T12:00:00Z</dc:date>
+  epub      <dc:date>2013-09-17</dc:date>
+  modified  <meta property='dcterms:modified'>2013-09-17</meta>
+
+epub3: These date values are looked for in this order
+  issued    <meta property='dcterms:issued'>2013-09-17</meta>
+  created   <meta property='dcterms:created'>2013-09-17T12:00:00Z</created>
+  date      <meta property='dcterms:date'>2013-09-18</meta>
+  epub      <dc:date>2013-09-17T12:00:00Z</dc:date>
+  modified  <meta property='dcterms:modified'>2013-09-17</meta>
+
+This software will fish the year out of whatever string it can find. Use the examples above as a guide for adding/editing these values yourself.
+
+PUBLISHER:
+
+I wanted to provide a way to have multiple copies of the same book produced by different publishers and name them sort-of unambiguously. I came up with the idea of expecting a contributor tag with role attribute of 'bkp' (so far, this is fairly normal). And then use a file-as attribute on that tag to contain a small string to be used in the filename. The idea here is short and sweet for the file-as.
+
+MORE COMPLICATED NAMING: MAGAZINES AND COLLECTIONS
+
+Magazines are kind of a sticky problem in that it's often desireable to have edition and/or date info in the filename. There's a lot of chaos out there with titling the epub editions of magazines. The solution in this software is to do some pattern matching on multiple fields in the magazine's metadata combined with custom naming rules for specific magazines.
+
+This software ships with a set of rules to properly name over 20 magazines and compilations that are commonly purchased by the developer. These default rules can be extended to add new publications, and used as an example.
+
+Another case is anthologies, which is to say: collections of works by multiple authors but not necessarily in periodical form. There is an anthology rule in the default rules that's set to look for any <dc:subject>anthology</dc:subject> tag in the metadata. In this case, the authors will be ignored and the title along with a publication year will be used (see above re: dc:date)
+
+epubname will search the following locations for a rules file, in this order:
+
+  1. Path specified in the optional --rules switch
+  2. $HOME/.config/epubtools/default.rules   Modern UNIX-like location
+  3. $HOME/.epubtools/default.rules          Older UNIX-like location
+  4. %%APPDATA%%\\epubtools\\default.rules   Windows
+  5. Built-in rules, a comprehensive stock set of naming rules
+
+Use the built-in rules as a model for a custom file. See --dump-rules above
+
+Please see --help-rules for more information on the syntax of the DSL in the rules file.
+
+
+For more information on the epub format:
+  epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm
+  epub3: https://www.w3.org/TR/epub/
+
+
+Version |] <> (pack $ showVersion version) <> "  Dino Morelli <dino@ui3.info>"
diff --git a/src/app/EpubTools/EpubName/Prompt.hs b/src/app/EpubTools/EpubName/Prompt.hs
--- a/src/app/EpubTools/EpubName/Prompt.hs
+++ b/src/app/EpubTools/EpubName/Prompt.hs
@@ -1,37 +1,34 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubName.Prompt
-   ( PromptResult (..)
-   , prompt
-   , continue
-   )
-   where
+  ( PromptResult (..)
+  , prompt
+  , continue
+  )
+  where
 
-import Data.Char ( toLower )
+import Data.Char (toLower)
 
 
 data PromptResult
-   = Yes
-   | No
-   | Quit
-   deriving Eq
+  = Yes
+  | No
+  | Quit
+  deriving Eq
 
 
 prompt :: IO PromptResult
 prompt = do
-   putStr "Rename this file? [Ynq], or ? for more options: "
-   c <- getChar
-   putStr "\n"
+  putStr "Link new filename into dest directories? [Ynq], or ? for more options: "
+  c <- getChar
+  putStr "\n"
 
-   case (toLower c) of
-      'y'  -> return Yes
-      ' '  -> return Yes
-      '\n' -> return Yes
-      'n'  -> return No
-      'q'  -> return Quit
-      '?'  -> more >> prompt
-      _    -> putStrLn "Unknown choice" >> prompt
+  case (toLower c) of
+    'y'  -> return Yes
+    ' '  -> return Yes
+    '\n' -> return Yes
+    'n'  -> return No
+    'q'  -> return Quit
+    '?'  -> more >> prompt
+    _    -> putStrLn "Unknown choice" >> prompt
 
 
 continue :: PromptResult -> Bool
@@ -41,8 +38,8 @@
 
 more :: IO ()
 more = putStr . unlines $
-   [ "y: rename this file"
-   , "n: don't rename this file"
-   , "q: cancel now, don't rename any files beyond this point"
-   , "<space> or <enter>: accept the default (which is capitalized)"
-   ]
+  [ "y: rename this file"
+  , "n: don't rename this file"
+  , "q: cancel now, don't rename any files beyond this point"
+  , "<space> or <enter>: accept the default (which is capitalized)"
+  ]
diff --git a/src/app/EpubTools/EpubName/Util.hs b/src/app/EpubTools/EpubName/Util.hs
--- a/src/app/EpubTools/EpubName/Util.hs
+++ b/src/app/EpubTools/EpubName/Util.hs
@@ -1,10 +1,10 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 module EpubTools.EpubName.Util
-   where
+  ( exitInitFailure
+  , exitProcessingFailure
+  )
+  where
 
-import System.Exit
+import System.Exit (ExitCode (ExitFailure))
 
 
 exitInitFailure, exitProcessingFailure :: ExitCode
diff --git a/src/app/EpubTools/EpubZip/Opts.hs b/src/app/EpubTools/EpubZip/Opts.hs
--- a/src/app/EpubTools/EpubZip/Opts.hs
+++ b/src/app/EpubTools/EpubZip/Opts.hs
@@ -1,76 +1,70 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
 
 module EpubTools.EpubZip.Opts
-   ( Options (..)
-   , parseOpts, usageText
-   )
-   where
+  ( DirOrName (..)
+  , Options (..)
+  , Overwrite (..)
+  , parseOpts
+  )
+  where
 
-import Data.Version ( showVersion )
-import Paths_epub_tools ( version )
-import System.Console.GetOpt
+import Data.Text.Lazy (Text, pack)
+import Data.Version (showVersion)
+import Options.Applicative.Dex
+import Paths_epub_tools (version)
+import Text.Heredoc (here)
 
 
+newtype Overwrite = Overwrite Bool
+
+newtype DirOrName = DirOrName FilePath
+
 data Options = Options
-   { optHelp :: Bool
-   , optOverwrite :: Bool
-   }
+  { overwrite :: Overwrite
+  , dirOrName :: DirOrName
+  }
 
 
-defaultOptions :: Options
-defaultOptions = Options
-   { optHelp = False
-   , optOverwrite = False
-   }
+parseOpts :: IO Options
+parseOpts = parseOpts' parser
+  "Construct an EPUB zip archive from files in the current directory"
+  footerContent version
 
 
-options :: [OptDescr (Options -> Options)]
-options =
-   [ Option ['h'] ["help"] 
-      (NoArg (\opts -> opts { optHelp = True } ))
-      "This help text"
-   , Option ['o'] ["overwrite"] 
-      (NoArg (\opts -> opts { optOverwrite = True } ))
-      "Force overwrite if dest file exists"
-   ]
+parser :: Parser Options
+parser = Options
+  <$> ( Overwrite <$> switch
+        (  long "overwrite"
+        <> short 'o'
+        <> help "Force overwrite if dest file exists"
+        )
+      )
+  <*> ( DirOrName <$> argument str
+        (  metavar "(DIR | DIR/FILE)"
+        <> help "Directory or filename for the new EPUB file"
+        )
+      )
 
 
-parseOpts :: [String] -> IO (Options, [String])
-parseOpts argv = 
-   case getOpt Permute options argv of
-      (o,n,[]  ) -> return (foldl (flip id) defaultOptions o, n)
-      (_,_,errs) -> ioError $ userError (concat errs ++ usageText)
+footerContent :: Text
+footerContent = [here|Construct an EPUB zip archive from files in the current directory
 
+If run with DIR alone, epubzip will try to construct a name from the OPF package data for this book (see epubname). If run with DIR/FILE, epubzip will use that name for the destination file.
 
-usageText :: String
-usageText = (usageInfo header options) ++ "\n" ++ footer
-   where
-      header = init $ unlines
-         [ "Usage: epubzip [OPTIONS] DESTDIR"
-         , "       epubzip [OPTIONS] DESTDIR/EPUBFILE"
-         , "Construct an epub zip archive from files in the current directory"
-         , ""
-         , "Options:"
-         ]
-      footer = init $ unlines
-         [ "If run with DESTDIR alone, epubzip will try to construct a name from the OPF package data for this book (see epubname). If run with DESTDIR/EPUBFILE, epubzip will use that name for the destination file."
-         , ""
-         , "You may have noticed that there is no epubunzip utility. Truth is, epubs are just zip files and you barely need epubzip either if you have the normal zip/unzip utilities installed. While not as fancy with file naming and leaving out dotfiles, this works for zipping:"
-         , ""
-         , "   $ cd DIR"
-         , "   $ zip -Xr9D ../EPUBFILE mimetype *"
-         , ""
-         , "And for unzipping, it's really just as easy:"
-         , ""
-         , "   $ mkdir TEMPDIR"
-         , "   $ cd TEMPDIR"
-         , "   $ unzip EPUBFILE"
-         , ""
-         , "For more information on the epub format:"
-         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
-         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
-         , ""
-         , ""
-         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
-         ]
+You may have noticed that there is no epubunzip utility. Truth is, EPUBs are just zip files and you barely need epubzip either if you have the normal zip/unzip utilities installed. While not as fancy with file naming and leaving out dotfiles, this works for zipping:
+
+    $ cd DIR
+    $ zip -Xr9D ../EPUBFILE mimetype *
+
+And for unzipping, it's really just as easy:
+
+    $ mkdir TEMPDIR
+    $ cd TEMPDIR
+    $ unzip EPUBFILE
+
+For more information on the EPUB format:
+  EPUB2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm
+  EPUB3: http://www.idpf.org/epub/30/spec/epub30-publications.html
+
+
+Version |] <> (pack $ showVersion version) <> "  Dino Morelli <dino@ui3.info>"
diff --git a/src/app/epubmeta.hs b/src/app/epubmeta.hs
--- a/src/app/epubmeta.hs
+++ b/src/app/epubmeta.hs
@@ -1,33 +1,28 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-import Data.Maybe
-import System.Environment ( getArgs )
-import System.Exit
+import System.Exit (ExitCode (ExitFailure, ExitSuccess), exitWith)
 
-import EpubTools.EpubMeta.Display
-import EpubTools.EpubMeta.Edit
-import EpubTools.EpubMeta.Export
-import EpubTools.EpubMeta.Import
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
+import EpubTools.EpubMeta.Edit (edit)
+import EpubTools.EpubMeta.Export (exportOpf)
+import EpubTools.EpubMeta.Import (importOpf)
+import EpubTools.EpubMeta.Opts (Mode (Edit, Export, Import, View),
+  Options (..), parseOpts)
+import EpubTools.EpubMeta.Util (EM, runEM)
+import EpubTools.EpubMeta.View (view)
 
 
-dispatch :: Options -> [FilePath] -> EM ()
-dispatch opts (f:[]) | optEdit opts /= NotEditing = edit opts f
-dispatch opts (f:[]) | isJust . optImport $ opts  = importOpf opts f
-dispatch opts (f:[]) | optExport opts /= NoExport = exportOpf opts f
-dispatch opts (f:[])                              = display opts f
-dispatch _    _                                   = throwError usageText
+dispatch :: Options -> EM ()
+dispatch (Options (View verbose) epubPath) = view verbose epubPath
+dispatch (Options (Export output) epubPath) = exportOpf output epubPath
+dispatch (Options (Edit backup) epubPath) = edit backup epubPath
+dispatch (Options (Import importPath backup) epubPath) = importOpf importPath backup epubPath
 
 
 main :: IO ()
 main = do
-   result <- runEM $
-      liftIO getArgs >>= parseOpts >>= uncurry dispatch
-   either exitFail (const (exitWith ExitSuccess)) result
+  opts <- parseOpts
+  result <- runEM $ dispatch opts
+  either exitFail (const (exitWith ExitSuccess)) result
 
-   where
-      exitFail msg = do
-         putStrLn msg
-         exitWith $ ExitFailure 1
+  where
+    exitFail msg = do
+      putStrLn msg
+      exitWith $ ExitFailure 1
diff --git a/src/app/epubname.hs b/src/app/epubname.hs
--- a/src/app/epubname.hs
+++ b/src/app/epubname.hs
@@ -1,35 +1,44 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
 -- This is for GHC 7.8/7.10 compatibility with the
 -- Control.Applicative import below
 {-# OPTIONS_GHC -fno-warn-unused-imports #-}
 
-{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleContexts, OverloadedRecordDot #-}
 
-import Codec.Epub
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
+import Codec.Epub (format, getMetadata, getPackage, getPkgXmlFromZip)
+import Codec.Epub.Data.Metadata (Metadata)
+import Codec.Epub.Data.Package (Package)
 import Control.Applicative
-import Control.Monad
-import Control.Monad.Except
-import System.Directory ( doesDirectoryExist, doesFileExist, renameFile )
-import System.Environment ( getArgs )
-import System.FilePath
-import System.Exit
+import Control.Exception (try)
+import Control.Monad (when)
+import Control.Monad.Except (runExceptT)
+import Control.Monad.Trans (MonadIO, liftIO)
+import Data.List.NonEmpty (toList)
+import GHC.IO.Exception (IOException)
+import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
+import System.FilePath ((</>))
+import System.Exit (ExitCode (ExitSuccess), exitWith)
 import System.IO ( BufferMode ( NoBuffering )
                  , hSetBuffering, stdin, stdout, stderr 
                  )
-import Text.Printf
+import System.Posix (createLink, removeLink)
+import Text.Printf (printf)
 
-import qualified EpubTools.EpubName.Doc.Dsl as Dsl
-import qualified EpubTools.EpubName.Doc.Rules as Rules
+import EpubTools.EpubName.Common
+  ( BookFiles (..)
+  , InteractiveSwitch (..)
+  , MoveSwitch (..)
+  , NoActionSwitch (..)
+  , Options (bookFiles, interactive, move, noAction, targetDirs, verbosityLevel)
+  , TargetDirs (..)
+  , VerbosityLevel (Normal, ShowFormatter, ShowBookInfo)
+  )
+import qualified EpubTools.EpubName.Common (VerbosityLevel (Normal, ShowBookInfo, ShowFormatter))
 import EpubTools.EpubName.Format.Format ( Formatter (..), tryFormatting )
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Main
-import EpubTools.EpubName.Opts ( Options (..), parseOpts, usageText )
+import EpubTools.EpubName.Format.Util (Globals (..), throwError)
+import EpubTools.EpubName.Main (initialize)
+import EpubTools.EpubName.Opts (parseOpts)
 import EpubTools.EpubName.Prompt ( PromptResult (..), prompt, continue )
-import EpubTools.EpubName.Util
+import EpubTools.EpubName.Util (exitProcessingFailure)
 
 
 {- Construct additional verbose output
@@ -45,15 +54,15 @@
 
 {- Format and display output for a book that was processed
 -}
-displayResults :: MonadIO m => Options
+displayResults :: MonadIO m => VerbosityLevel
    -> FilePath -> FilePath -> String -> Package -> Metadata -> m ()
-displayResults opts oldPath newPath fmtUsed pkg md =
-   liftIO $ printf "%s -> %s%s\n" oldPath newPath
-      (additional (optVerbose opts) (fmtUsed, pkg, md))
+displayResults verbosityLevel' origPath newName fmtUsed pkg md =
+   liftIO $ printf "%s -> %s%s\n" origPath newName
+      (additional verbosityLevel' (fmtUsed, pkg, md))
    where
-      additional Nothing  = const ""
-      additional (Just 1) = formatF
-      additional _        = formatFM
+      additional Normal = const ""
+      additional ShowFormatter = formatF
+      additional ShowBookInfo = formatFM
 
 
 {- Process an individual epub book file
@@ -70,7 +79,7 @@
 processBook opts formatters (oldPath:paths) _     priRes = do
    result <- runExceptT $ do
       {- Parse the book's metadata
-         The reason for the nested runErrorT block is, if there is
+         The reason for the nested runExceptT block is, if there is
          failure during parsing, we need to mark the result up with the
          file path. Failures here will otherwise get lost in the output
          when multiple books are processed at once.
@@ -83,23 +92,28 @@
             $ printf "ERROR: File %s: %s" oldPath msg
          ) return epm
 
-      (fmtUsed, shortPath) <-
+      (fmtUsed, newName) <-
          tryFormatting (Globals opts pkg md) formatters oldPath
 
-      let newPath = optTargetDir opts </> shortPath
+      displayResults opts.verbosityLevel oldPath newName fmtUsed pkg md
 
-      fileExists <- liftIO $ doesFileExist newPath
-      when fileExists $ throwError $ 
-         printf "File %s already exists. No change." newPath
+      promptResult <- liftIO $ case opts.interactive.v of
+        True  -> prompt
+        False -> return Yes
 
-      displayResults opts oldPath newPath fmtUsed pkg md
+      liftIO $ when ((promptResult == Yes) && (not opts.noAction.v)) $ do
+        -- We need enough Data.List functions inside this block that it's
+        -- easier to convert the NonEmpty a back into a [a]
+        let destDirs = toList opts.targetDirs.v
 
-      promptResult <- liftIO $ case (optInteractive opts) of
-         True  -> prompt
-         False -> return Yes
+        linkOutcomes <- sequence . map (doOneDest oldPath newName) $ destDirs
 
-      when ((promptResult == Yes) && (optNoAction opts == False)) $
-         liftIO $ renameFile oldPath newPath
+        when opts.move.v $
+          if all (== True) linkOutcomes
+            then do
+              putStrLn "All links successfully created, removing original file"
+              removeLink oldPath
+            else putStrLn "One or more links failed, NOT removing original file"
 
       return $ continue promptResult
 
@@ -117,47 +131,50 @@
    processBook opts formatters paths cont newRes
 
 
+doOneDest :: FilePath -> FilePath -> FilePath -> IO Bool
+doOneDest srcPath newName destDir = do
+  let destPath = destDir </> newName
+  fileExists <- liftIO $ doesFileExist destPath
+  if fileExists
+    then do
+      printf "File %s already exists. No change.\n" destPath
+      pure False
+    else
+      tryHardLink srcPath destPath
+
+
+tryHardLink :: FilePath -> FilePath -> IO Bool
+tryHardLink srcFp destFp = do
+  el <- try $ createLink srcFp destFp
+  either tryCopy (const $ pure True) el
+  where
+    tryCopy :: IOException -> IO Bool
+    tryCopy _ = do
+      printf "Hard link at %s failed, attempting to copy instead\n" destFp
+      ec <- try $ copyFile srcFp destFp
+      either failureHandler (const $ pure True) ec
+
+    failureHandler :: IOException -> IO Bool
+    failureHandler _ = do
+      printf "Copy to %s failed\n" destFp
+      pure False
+
+
 main :: IO ()
 main = do
    -- No buffering, it messes with the order of output
    mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr, stdin ]
 
    either exitWith exitWith =<< (runExceptT $ do
-      -- Parse command-line arguments
-      (opts, paths) <- (liftIO getArgs) >>= parseOpts
-
-      -- User asked for rules help, this is a special termination case
-      when (optHelpRules opts) $ do
-         liftIO $ putStrLn Dsl.docs
-         throwError ExitSuccess
-
-      -- User asked for a dump of the built-in rules,
-      -- this is a special termination case
-      when (optDumpRules opts) $ do
-         liftIO $ putStr Rules.defaults
-         throwError ExitSuccess
-
-      -- User asked for help, this is a special termination case
-      when ((optHelp opts) || (null paths)) $ do
-         liftIO $ usageText >>= putStrLn
-         throwError ExitSuccess
-
-      let targetDir = optTargetDir opts
-      targetDirExists <- liftIO $ doesDirectoryExist targetDir
-      unless targetDirExists $ do
-         _ <- liftIO $ printf
-            "ERROR: Target directory doesn't exist: %s\n" targetDir
-         throwError exitInitFailure
+      opts <- liftIO parseOpts
 
-      -- Locate the rules file, load it and parse into a list of
-      -- formatters
+      -- Locate the rules file, load it and parse into a list of formatters
       fs <- initialize opts
 
-      when (optNoAction opts) $ liftIO
-         $ putStrLn "No-action specified"
+      when opts.noAction.v $ liftIO $ putStrLn "No-action specified"
 
       -- Perform the formatting operation on the books
-      code <- liftIO $ processBook opts fs paths True True
+      code <- liftIO $ processBook opts fs (toList opts.bookFiles.v) True True
       case code of
          True  -> return ExitSuccess
          False -> throwError exitProcessingFailure
diff --git a/src/app/epubzip.hs b/src/app/epubzip.hs
--- a/src/app/epubzip.hs
+++ b/src/app/epubzip.hs
@@ -1,79 +1,69 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-import Codec.Epub
-import Control.Monad
-import Control.Monad.Except
-import System.Directory
-import System.Environment
-import System.Exit
-import System.FilePath
-import System.IO ( BufferMode ( NoBuffering )
-                 , hSetBuffering, stdout, stderr 
-                 )
-import Text.Printf
+import Codec.Epub (getMetadata, getPackage, getPkgPathXmlFromDir,
+  mkEpubArchive, writeArchive)
+import Control.Monad.Except (runExceptT)
+import Control.Monad.Trans (liftIO)
+import System.Directory (doesDirectoryExist, doesFileExist, removeFile)
+import System.Exit (ExitCode (ExitFailure, ExitSuccess), exitWith)
+import System.FilePath ((</>))
+import System.IO (BufferMode ( NoBuffering ), hSetBuffering, stdout, stderr)
+import Text.Printf (printf)
 
-import EpubTools.EpubName.Format.Format ( tryFormatting )
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Main
-import qualified EpubTools.EpubName.Opts as EN
-import EpubTools.EpubZip.Opts
+import EpubTools.EpubName.Format.Format (tryFormatting)
+import EpubTools.EpubName.Format.Util (Globals (..), throwError)
+import EpubTools.EpubName.Main (initialize)
+import qualified EpubTools.EpubName.Common as EN
+import EpubTools.EpubZip.Opts (DirOrName (..),
+  Options (dirOrName, overwrite), Overwrite (..), parseOpts)
 
 
 main :: IO ()
 main = do
-   -- No buffering, it messes with the order of output
-   mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr ]
-
-   either exitWith exitWith =<< (runExceptT $ do
-      -- Parse command-line arguments
-      (opts, paths) <- liftIO $ getArgs >>= parseOpts
+  -- No buffering, it messes with the order of output
+  mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr ]
 
-      -- User asked for help, this is a special termination case
-      when ((optHelp opts) || (null paths)) $ do
-         liftIO $ putStrLn usageText
-         throwError ExitSuccess
+  either exitWith exitWith =<< (runExceptT $ do
+    opts <- liftIO parseOpts
 
-      let inputPath = head paths
-      isDir <- liftIO $ doesDirectoryExist inputPath
+    let (DirOrName inputPath) = dirOrName opts
+    isDir <- liftIO $ doesDirectoryExist inputPath
 
-      en <- if isDir
+    en <- if isDir
             then do
-               dos <- liftIO EN.defaultOptions
-               fs <- initialize dos
-               runExceptT $ do
-                  xml <- snd `fmap` getPkgPathXmlFromDir "."
-                  pkg <- getPackage xml
-                  metadata <- getMetadata xml
+              let dos = EN.defaultOptions
+              fs <- initialize dos
+              runExceptT $ do
+                xml <- snd `fmap` getPkgPathXmlFromDir "."
+                pkg <- getPackage xml
+                metadata <- getMetadata xml
 
-                  (_, newPath) <- tryFormatting
-                     (Globals dos pkg metadata) fs
-                     "CURRENT DIRECTORY"
+                (_, newPath) <- tryFormatting
+                  (Globals dos pkg metadata) fs
+                  "CURRENT DIRECTORY"
 
-                  return $ inputPath </> newPath
+                return $ inputPath </> newPath
             else return . Right $ inputPath
 
-      case en of
-         Right zipPath -> do
-            exists <- liftIO $ doesFileExist zipPath
+    case en of
+      Right zipPath -> do
+        exists <- liftIO $ doesFileExist zipPath
 
-            case ((optOverwrite opts), exists) of
-               (False, True) -> do
-                  _ <- liftIO $ printf
-                     "File %s exists, use --overwrite to force\n"
-                     zipPath
-                  throwError $ ExitFailure 1
-               (True,  True) -> liftIO $ removeFile zipPath
-               (_   ,  _   ) -> return ()
+        case ((overwrite opts), exists) of
+          (Overwrite False, True) -> do
+            _ <- liftIO $ printf
+              "File %s exists, use --overwrite to force\n"
+              zipPath
+            throwError $ ExitFailure 1
+          (Overwrite True,  True) -> liftIO $ removeFile zipPath
+          (_             ,  _   ) -> return ()
 
-            _ <- liftIO $ do
-               archive <- mkEpubArchive "."
-               writeArchive zipPath archive
-               printf "Book created: %s\n" zipPath
+        _ <- liftIO $ do
+          archive <- mkEpubArchive "."
+          writeArchive zipPath archive
+          printf "Book created: %s\n" zipPath
 
-            return ExitSuccess
+        return ExitSuccess
 
-         Left errorMsg -> do
-            liftIO $ putStrLn errorMsg
-            throwError $ ExitFailure 1
-      )
+      Left errorMsg -> do
+        liftIO $ putStrLn errorMsg
+        throwError $ ExitFailure 1
+    )
diff --git a/src/test/EpubTools/Test/EpubName/Format.hs b/src/test/EpubTools/Test/EpubName/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/test/EpubTools/Test/EpubName/Format.hs
@@ -0,0 +1,1659 @@
+module EpubTools.Test.EpubName.Format
+  ( formatTests
+  )
+  where
+
+import Codec.Epub.Data.Metadata (Creator (..), DateEvent (..), DateValue (..),
+  Metadata (..), Title (..), emptyMetadata)
+import Codec.Epub.Data.Package (Package (..))
+import Control.Monad.Except (runExceptT)
+import qualified Data.Map.Strict as Map
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit ((@=?), testCase)
+
+import EpubTools.EpubName.Common
+  ( Options (includePublisher, pubYear)
+  , PublisherSwitch (..)
+  , PubYear (NoDate, NoModified)
+  )
+import EpubTools.EpubName.Format.Format (Formatter, tryFormatting)
+import EpubTools.EpubName.Format.Util (Globals (..))
+
+
+formatTests :: Options -> [Formatter] -> TestTree
+formatTests opts fs = testGroup "Format" $
+  map (\f -> f ((Globals opts (Package "3.3" "") emptyMetadata), fs))
+    [ testAuthorMinimal
+    , testAuthorOneName
+    , testAuthorRole
+    , testAuthorFileas
+    , testAuthorFileasParens
+    , testAuthorFull
+    , testAuthorMiddle
+    , testMultiAutCreators
+    , testMultiAutOneString
+    , testNoAuthor
+    , testColon
+    , testCreatorsNoAuthor
+    , testCreatorsNoAuthorPubDate
+    , testCreatorLastFirst
+    , testTitleCaps
+    , testTitleBracket
+    , testTitleNone
+    , testTitleMultiline
+    , testTitleHyphen
+    , testTitleHyphenDates
+    , testTitleRomanNum
+    , testAllPunctuation
+    , testPubYearNoDatesPresent
+    , testPubYearAnyIssued
+    , testPubYearAnyCreated
+    , testPubYearAnyDate
+    , testPubYearAnyEpub
+    , testPubYearAnyModified
+    , testPubYearAnyAllPresent
+    , testPubYearNoModified
+    , testPubYearNoDate
+    , testMagAeon
+    , testMagAEon
+    , testMagApexPound
+    , testMagApexIssue
+    , testMagApexLong
+    , testChallengingDestinyShort
+    , testChallengingDestinyLong
+    , testAnalogSingle
+    , testAnalogDouble
+    , testAsimovs
+    , testFantasyMagazine
+    , testFsfShort
+    , testFsfShortComma
+    , testFsfLong
+    , testFsfAmpersand
+    , testFsfAmpersandSpaces
+    , testFsfVeryLong
+    , testMagFutureOrbits
+    , testGudShort
+    , testGudLong
+    , testGudVeryLong
+    , testInterzone
+    , testInterzoneCaps
+    , testInterzoneOldLong
+    , testInterzoneOldShort
+    , testNemesisShort
+    , testNemesisLong
+    , testMagSomethingWicked
+    , testMagSomethingWickedMonth
+    , testSFBestOf
+    , testBestSF
+    , testYearsBest
+    , testMagBlackStatic
+    , testRageMachineMag
+    , testEclipseMagWord
+    , testEclipseMagNum
+    , testBcs
+    , testBkpFileAs
+    , testBkpText
+    , testBkpMissing
+    , testMagUniverse
+    , testMagClarkesworld
+    , testLightspeedDate
+    , testLightspeedMagIssue
+    , testLightspeedIssue
+    , testLightspeedMagIssueDate
+    , testMagWeirdTales
+    , testMagGalaxysEdge
+    , testMagPlasmaFreq
+    , testMagPlasmaFreqMonth
+    , testMagPunchinello1800s
+    , testMagGenericVolNo3
+    , testMagSubjWithIssue
+    , testWomensInstituteLib
+    , testAnthologyDate
+    , testAnthology
+    , testMagLunaStation
+    ]
+
+
+assertNewName :: Globals -> [Formatter] -> String
+  -> (String, FilePath) -> TestTree
+assertNewName globals fs desc expected = testCase desc $ do
+  result <- runExceptT $ tryFormatting globals fs ""
+  let actual = either (\em -> ("NO FORMATTER", em)) id result
+  expected @=? actual
+
+
+testAuthorMinimal :: (Globals, [Formatter]) -> TestTree
+testAuthorMinimal (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "minimal author" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing
+        "Herman Melville"]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick.epub"
+      )
+
+
+testAuthorOneName :: (Globals, [Formatter]) -> TestTree
+testAuthorOneName (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "author is a single word" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing
+        "Melville"]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "Melville-MobyDick.epub"
+      )
+
+
+testAuthorRole :: (Globals, [Formatter]) -> TestTree
+testAuthorRole (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "author with role" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Herman Melville"]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick.epub"
+      )
+
+
+testAuthorFileas :: (Globals, [Formatter]) -> TestTree
+testAuthorFileas (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "author with file-as" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing
+        (Just "Melville, Herman")
+        Nothing
+        "Herman Melville"]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick.epub"
+      )
+
+
+testAuthorFileasParens :: (Globals, [Formatter]) -> TestTree
+testAuthorFileasParens (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "author with file-as and pesky parenthesized full names" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing
+        (Just "Fletcher, J. S. (Joseph Smith)")
+        Nothing
+        "J. S. Fletcher"]
+      , metaTitles = [Title Nothing Nothing Nothing "The Middle of Things"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "FletcherJS-TheMiddleOfThings.epub"
+      )
+
+
+testAuthorFull :: (Globals, [Formatter]) -> TestTree
+testAuthorFull (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "author fully filled out" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Melville, Herman")
+        Nothing
+        "Herman Melville"]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick.epub"
+      )
+
+
+testAuthorMiddle :: (Globals, [Formatter]) -> TestTree
+testAuthorMiddle (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "author with middle-name" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Wallace, David Foster")
+        Nothing
+        "David Foster Wallace"]
+      , metaTitles = [Title Nothing Nothing Nothing "Infinite Jest"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "WallaceDavidFoster-InfiniteJest.epub"
+      )
+
+
+testMultiAutCreators :: (Globals, [Formatter]) -> TestTree
+testMultiAutCreators (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "several authors" expected
+  where
+    meta = emptyMetadata
+      { metaCreators =
+        [ Creator (Just "aut") Nothing
+          Nothing "James Patrick Kelly"
+        , Creator (Just "aut") (Just "Kessel, John")
+          Nothing "John Kessel"
+        , Creator (Just "aut") Nothing
+          Nothing "Jonathan Lethem"
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Ninety Percent of Everything"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "Kelly_Kessel_Lethem-NinetyPercentOfEverything.epub"
+      )
+
+
+testMultiAutOneString :: (Globals, [Formatter]) -> TestTree
+testMultiAutOneString (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "more than one author separated by & and/or and" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing
+        "Yvonne Q. Anderson & Eva Tunglewacker and Jefferson Milner"]
+      , metaTitles = [Title Nothing Nothing Nothing "Big Trouble"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "Anderson_Tunglewacker_Milner-BigTrouble.epub"
+      )
+
+
+testNoAuthor :: (Globals, [Formatter]) -> TestTree
+testNoAuthor (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "no creator(s) at all" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = []
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Some Collection of Fine Stories, Volume 1"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "SomeCollectionOfFineStoriesVolume1.epub"
+      )
+
+
+testCreatorsNoAuthor :: (Globals, [Formatter]) -> TestTree
+testCreatorsNoAuthor (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "creators, but no author(s) at all" expected
+  where
+    meta = emptyMetadata
+      { metaCreators =
+        [ Creator (Just "edt") Nothing
+          Nothing "Graham Spindlewest"
+        , Creator (Just "ill") Nothing
+          Nothing "Eva Tunglewacker"
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Some Collection of Fine Stories, Volume 1"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "SomeCollectionOfFineStoriesVolume1.epub"
+      )
+
+
+testColon :: (Globals, [Formatter]) -> TestTree
+testColon (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "colon becomes underscore" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing
+        "Ed Howdershelt"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Book 1: 3rd World Products, Inc."]
+      }
+    expected =
+      ( "ordinary_book"
+      , "HowdersheltEd-Book1_3rdWorldProductsInc.epub"
+      )
+
+
+testCreatorsNoAuthorPubDate :: (Globals, [Formatter]) -> TestTree
+testCreatorsNoAuthorPubDate (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "creators, but no author(s) at all, with pub date" expected
+  where
+    meta = emptyMetadata
+      { metaCreators =
+        [ Creator (Just "edt") Nothing
+          Nothing "Graham Spindlewest"
+        , Creator (Just "ill") Nothing
+          Nothing "Eva Tunglewacker"
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Some Collection of Fine Stories, Volume 1"]
+      , metaDates = Map.fromList [(Created, DateValue "2008")]
+      }
+    expected =
+      ( "ordinary_book"
+      , "SomeCollectionOfFineStoriesVolume1_2008.epub"
+      )
+
+
+testCreatorLastFirst :: (Globals, [Formatter]) -> TestTree
+testCreatorLastFirst (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "creator text (not file-as) is last-name-first" expected
+  where
+    meta = emptyMetadata
+      { metaCreators =
+        [ Creator Nothing Nothing
+          Nothing "Spindlewest, Graham"
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "A Midnight's Horror Story"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "SpindlewestGraham-AMidnightsHorrorStory.epub"
+      )
+
+
+testTitleCaps :: (Globals, [Formatter]) -> TestTree
+testTitleCaps (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "title all caps" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Greg Bear"]
+      , metaTitles = [Title Nothing Nothing Nothing "EON"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "BearGreg-Eon.epub"
+      )
+
+
+testTitleBracket :: (Globals, [Formatter]) -> TestTree
+testTitleBracket (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "title with brackets" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Mercedes Lackey"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "SKitty [Shipscat series #1]"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "LackeyMercedes-Skitty_ShipscatSeries1.epub"
+      )
+
+
+testTitleNone :: (Globals, [Formatter]) -> TestTree
+testTitleNone (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "missing title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Nobody McCrankypants"]
+      }
+    expected =
+      ( "NO FORMATTER"
+      , " [ERROR No formatter found]"
+      )
+
+
+testTitleMultiline :: (Globals, [Formatter]) -> TestTree
+testTitleMultiline (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "multiline title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator
+        Nothing
+        (Just "Castleman, Virginia Carter")
+        Nothing
+        "Virginia Carter Castleman"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Pocahontas.\nA Poem"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "CastlemanVirginiaCarter-Pocahontas_APoem.epub"
+      )
+
+
+testTitleHyphen :: (Globals, [Formatter]) -> TestTree
+testTitleHyphen (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "title with hyphen" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator
+        Nothing
+        (Just "Spinoza, Benedictus de")
+        Nothing
+        "Benedictus de Spinoza"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Theologico-Political Treatise"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "SpinozaBenedictusDe-Theologico-PoliticalTreatise.epub"
+      )
+
+
+testTitleHyphenDates :: (Globals, [Formatter]) -> TestTree
+testTitleHyphenDates (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "title with hyphen separating dates" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator
+        Nothing
+        (Just "Anonymous")
+        Nothing
+        "Anonymous"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Queen Victoria\nStory of Her Life and Reign, 1819-1901"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "Anonymous-QueenVictoria_StoryOfHerLifeAndReign1819-1901.epub"
+      )
+
+testTitleRomanNum :: (Globals, [Formatter]) -> TestTree
+testTitleRomanNum (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "title with Roman numerals" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator
+        Nothing
+        (Just "Hauptmann, Gerhart")
+        Nothing
+        "Gerhart Hauptmann"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "The Dramatic Works of Gerhart Hauptmann\nVolume III"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "HauptmannGerhart-TheDramaticWorksOfGerhartHauptmann_VolumeIII.epub"
+      )
+
+
+testAllPunctuation :: (Globals, [Formatter]) -> TestTree
+testAllPunctuation (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "big test of all punctuation" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Dino Morelli"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "The *crazy*: Sand-box. Of Smedley's discontent/loathing, fear & Malnourishment? (Maybe not!); [Part #2]"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MorelliDino-TheCrazy_Sand-BoxOfSmedleysDiscontentLoathingFearAndMalnourishmentMaybeNot_Part2.epub"
+      )
+
+
+testPubYearNoDatesPresent :: (Globals, [Formatter]) -> TestTree
+testPubYearNoDatesPresent (gs, fs) =
+  assertNewName gs { gMetadata = meta} fs
+    "book with no dates at all" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory.epub"
+      )
+
+
+testPubYearAnyIssued :: (Globals, [Formatter]) -> TestTree
+testPubYearAnyIssued (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "book with only issued date" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList [(Issued, DateValue "2003")]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory_2003.epub"
+      )
+
+
+testPubYearAnyCreated :: (Globals, [Formatter]) -> TestTree
+testPubYearAnyCreated (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "book with only created date" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList [(Created, DateValue "2003")]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory_2003.epub"
+      )
+
+
+testPubYearAnyDate :: (Globals, [Formatter]) -> TestTree
+testPubYearAnyDate (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "book with only created date" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList [(Date, DateValue "2003")]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory_2003.epub"
+      )
+
+
+testPubYearAnyEpub :: (Globals, [Formatter]) -> TestTree
+testPubYearAnyEpub (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "book with only the simple (date element) date" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList [(Epub, DateValue "2003")]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory_2003.epub"
+      )
+
+
+testPubYearAnyModified :: (Globals, [Formatter]) -> TestTree
+testPubYearAnyModified (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "book with only modified date" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList [(Modified, DateValue "2003")]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory_2003.epub"
+      )
+
+
+testPubYearAnyAllPresent :: (Globals, [Formatter]) -> TestTree
+testPubYearAnyAllPresent (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "book with all dates" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList
+        [ (Epub, DateValue "2001")
+        , (Date, DateValue "2005")
+        , (Modified, DateValue "2004")
+        , (Created, DateValue "2003")
+        , (Issued, DateValue "2002")
+        ]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory_2002.epub"
+      )
+
+
+testPubYearNoModified :: (Globals, [Formatter]) -> TestTree
+testPubYearNoModified (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "book with only modified date, --no-modified-date switch" expected
+  where
+    testOpts = (gOpts gs) { pubYear = NoModified }
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList [ (Modified, DateValue "2004") ]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory.epub"
+      )
+
+
+testPubYearNoDate :: (Globals, [Formatter]) -> TestTree
+testPubYearNoDate (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "epub book with lots of dates but we don't want"
+    expected
+  where
+    testOpts = (gOpts gs) { pubYear = NoDate }
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+      , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+      , metaDates = Map.fromList
+        [ (Epub, DateValue "2001")
+        , (Date, DateValue "2005")
+        , (Modified, DateValue "2004")
+        , (Created, DateValue "2003")
+        , (Issued, DateValue "2002")
+        ]
+      }
+    expected =
+      ( "ordinary_book"
+      , "JonesJim-ATimelessStory.epub"
+      )
+
+
+testMagAeon :: (Globals, [Formatter]) -> TestTree
+testMagAeon (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "Aeon magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "Aeon Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing "Aeon Eight"]
+      }
+    expected =
+      ( "magAeon"
+      , "AeonMagazine08.epub"
+      )
+
+
+testMagAEon :: (Globals, [Formatter]) -> TestTree
+testMagAEon (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "AEon magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing "AEon Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing "Aeon Thirteen"]
+      }
+    expected =
+      ( "magAeon"
+      , "AeonMagazine13.epub"
+      )
+
+
+testMagApexPound :: (Globals, [Formatter]) -> TestTree
+testMagApexPound (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Apex Magazine, pound sign and issue number" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Apex Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Apex Magazine #16"]
+      }
+    expected =
+      ( "magApex"
+      , "ApexMagazine016.epub"
+      )
+
+
+testMagApexIssue :: (Globals, [Formatter]) -> TestTree
+testMagApexIssue (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Apex Magazine, Issue number" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Apex Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Apex Magazine Issue 17"]
+      }
+    expected =
+      ( "magApex"
+      , "ApexMagazine017.epub"
+      )
+
+
+testMagApexLong :: (Globals, [Formatter]) -> TestTree
+testMagApexLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Apex Magazine, title with month year" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Apex Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Apex Magazine: Issue 101, October 2017"]
+      }
+    expected =
+      ( "magApex"
+      , "ApexMagazine101.epub"
+      )
+
+
+testChallengingDestinyShort :: (Globals, [Formatter]) -> TestTree
+testChallengingDestinyShort (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Challenging Destiny Magazine, short title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Crystalline Sphere Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Challenging Destiny #23"]
+      }
+    expected =
+      ( "magChallengingDestiny"
+      , "ChallengingDestinyMagazine023.epub"
+      )
+
+
+testChallengingDestinyLong :: (Globals, [Formatter]) -> TestTree
+testChallengingDestinyLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Challenging Destiny Magazine, long title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "Crystalline Sphere Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Challenging Destiny #24: August 2007"]
+      }
+    expected =
+      ( "magChallengingDestiny"
+      , "ChallengingDestinyMagazine024.epub"
+      )
+
+
+testAnalogSingle :: (Globals, [Formatter]) -> TestTree
+testAnalogSingle (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "Analog, single month" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Dell Magazine Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Analog SFF, November 2010"]
+      }
+    expected =
+      ( "magAnalog"
+      , "AnalogSF2010-11.epub"
+      )
+
+
+testAnalogDouble :: (Globals, [Formatter]) -> TestTree
+testAnalogDouble (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "Analog, double month" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Dell Magazine Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Analog SFF, July-August 2003"]
+      }
+    expected =
+      ( "magAnalog"
+      , "AnalogSF2003-07_08.epub"
+      )
+
+
+testAsimovs :: (Globals, [Formatter]) -> TestTree
+testAsimovs (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "Asimovs" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Dell Magazine Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Asimov's SF, August 2003"]
+      }
+    expected =
+      ( "magAnalog"
+      , "AsimovsSF2003-08.epub"
+      )
+
+testFantasyMagazine :: (Globals, [Formatter]) -> TestTree
+testFantasyMagazine (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "Fantasy Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators =
+        [ Creator (Just "aut") (Just "Howard, Kat & Tanzer, Molly & Beagle, Peter S. & Howard, Jonathan L. & Valentine, Genevieve & Vaughn, Carrie & Pilinovsky, Helen") Nothing "Kat Howard"
+        , Creator (Just "aut") Nothing Nothing "Molly Tanzer"
+        , Creator (Just "aut") Nothing Nothing "Peter S. Beagle"
+        , Creator (Just "aut") Nothing Nothing "Jonathan L. Howard"
+        , Creator (Just "aut") Nothing Nothing "Genevieve Valentine"
+        , Creator (Just "aut") Nothing Nothing "Carrie Vaughn"
+        , Creator (Just "aut") Nothing Nothing "Helen Pilinovsky"
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Fantasy Magazine Issue 49"]
+      }
+    expected =
+      ( "magFantasyMag"
+      , "FantasyMagazine049.epub"
+      )
+
+
+testFsfShort :: (Globals, [Formatter]) -> TestTree
+testFsfShort (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "FSF Magazine, short" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Spilogale Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing "FSF Oct/Nov 2003"]
+      }
+    expected =
+      ( "magFsf"
+      , "FantasyScienceFiction2003-10_11.epub"
+      )
+
+
+testFsfShortComma :: (Globals, [Formatter]) -> TestTree
+testFsfShortComma (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "FSF Magazine, short, comma" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Spilogale Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing "FSF, April 2008"]
+      }
+    expected =
+      ( "magFsf"
+      , "FantasyScienceFiction2008-04.epub"
+      )
+
+
+testFsfLong :: (Globals, [Formatter]) -> TestTree
+testFsfLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "FSF Magazine, long" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "Spilogale Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "FSF Magazine, April 2006"]
+      }
+    expected =
+      ( "magFsf"
+      , "FantasyScienceFiction2006-04.epub"
+      )
+
+
+testFsfAmpersand :: (Globals, [Formatter]) -> TestTree
+testFsfAmpersand (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "FSF Magazine, ampersand" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Spilogale"]
+      , metaTitles = [Title Nothing Nothing Nothing "F&SF, Apr 2004"]
+      }
+    expected =
+      ( "magFsf"
+      , "FantasyScienceFiction2004-04.epub"
+      )
+
+
+testFsfAmpersandSpaces :: (Globals, [Formatter]) -> TestTree
+testFsfAmpersandSpaces (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "FSF Magazine, ampersand, spaces" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Spilogale"]
+      , metaTitles = [Title Nothing Nothing Nothing "F & SF Dec 2003"]
+      }
+    expected =
+      ( "magFsf"
+      , "FantasyScienceFiction2003-12.epub"
+      )
+
+
+testFsfVeryLong :: (Globals, [Formatter]) -> TestTree
+testFsfVeryLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "FSF Magazine, very long name" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing
+        Nothing "Spilogale Inc."]
+      , metaTitles = [Title Nothing Nothing Nothing "Fantasy & Science Fiction, November/December 2017"]
+      }
+    expected =
+      ( "magFsf"
+      , "FantasyScienceFiction2017-11_12.epub"
+      )
+
+
+testMagFutureOrbits :: (Globals, [Formatter]) -> TestTree
+testMagFutureOrbits (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "testMagFutureOrbits" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "Vander Neut Publications, L.L.C."]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Future Orbits Issue 5, June/July 2002"]
+      }
+    expected =
+      ( "magFutureOrbits"
+      , "FutureOrbitsMagazine05_2002-06_07.epub"
+      )
+
+
+testGudShort :: (Globals, [Formatter]) -> TestTree
+testGudShort (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Gud Magazine, short" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "GUD Magazine Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "GUD Magazine Issue 0 :: Spring 2007"]
+      }
+    expected =
+      ( "magGud"
+      , "GUDMagazine00.epub"
+      )
+
+
+testGudLong :: (Globals, [Formatter]) -> TestTree
+testGudLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Gud Magazine, long" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "GUD Magazine Authors, Jeff Somers, Jeremy Shipp"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "GUD Magazine Issue 2 :: Spring 2008"]
+      }
+    expected =
+      ( "magGud"
+      , "GUDMagazine02.epub"
+      )
+
+
+testGudVeryLong :: (Globals, [Formatter]) -> TestTree
+testGudVeryLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Gud Magazine, very long" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "GUD Magazine Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Greatest Uncommon Denominator Magazine Issue 4 :: Spring 2009"]
+      }
+    expected =
+      ( "magGud"
+      , "GUDMagazine04.epub"
+      )
+
+
+testInterzone :: (Globals, [Formatter]) -> TestTree
+testInterzone (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Interzone Magazine, Smashwords style" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "TTA Press"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Interzone 233 Mar - Apr 2011"]
+      }
+    expected =
+      ( "magInterzone"
+      , "InterzoneSFF233.epub"
+      )
+
+
+testInterzoneCaps :: (Globals, [Formatter]) -> TestTree
+testInterzoneCaps (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Interzone Magazine, started in 2014" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing (Just "Interzone 249")
+        Nothing "TTA Press"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "INTERZONE #249 SCIENCE FICTION & FANTASY MAGAZINE (NOVâ\x80\x93DEC 2013)"]
+      }
+    expected =
+      ( "magInterzone"
+      , "InterzoneSFF249.epub"
+      )
+
+
+testInterzoneOldLong :: (Globals, [Formatter]) -> TestTree
+testInterzoneOldLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Interzone Magazine, old style, long" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "TTA Press Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Interzone Science Fiction and Fantasy Magazine #216"]
+      }
+    expected =
+      ( "magInterzone"
+      , "InterzoneSFF216.epub"
+      )
+
+
+testInterzoneOldShort :: (Globals, [Formatter]) -> TestTree
+testInterzoneOldShort (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Interzone Magazine, old style, short" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "TTA Press Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing 
+        "Interzone SFF #212"]
+      }
+    expected =
+      ( "magInterzone"
+      , "InterzoneSFF212.epub"
+      )
+
+
+testNemesisShort :: (Globals, [Formatter]) -> TestTree
+testNemesisShort (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Nemesis Magazine, short" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "Stephen Adams"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Nemesis Magazine #2"]
+      }
+    expected =
+      ( "magNemesis"
+      , "NemesisMag002.epub"
+      )
+
+
+testNemesisLong :: (Globals, [Formatter]) -> TestTree
+testNemesisLong (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Nemesis Magazine, long" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "Stephen Adams"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Nemesis Magazine #7: Featuring Victory Rose in Death Stalks the Ruins"]
+      }
+    expected =
+      ( "magNemesis"
+      , "NemesisMag007.epub"
+      )
+
+
+testMagSomethingWicked :: (Globals, [Formatter]) -> TestTree
+testMagSomethingWicked (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Something Wicked Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing Nothing
+        "Something Wicked Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Something Wicked SF and Horror Magazine #5"]
+      }
+    expected =
+      ( "magSomethingWicked"
+      , "SomethingWicked005.epub"
+      )
+
+
+testMagSomethingWickedMonth :: (Globals, [Formatter]) -> TestTree
+testMagSomethingWickedMonth (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Something Wicked Magazine, month in title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Authors, Something Wicked")
+        Nothing
+        "Something Wicked Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Something Wicked #14 (October 2011)"]
+      }
+    expected =
+      ( "magSomethingWicked"
+      , "SomethingWicked014.epub"
+      )
+
+
+testSFBestOf :: (Globals, [Formatter]) -> TestTree
+testSFBestOf (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Science Fiction: The Best of the Year" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "Rich Horton"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Science Fiction: The Best of the Year, 2007 Edition"]
+      , metaSubjects = ["anthology"]
+      }
+    expected =
+      ( "anthology"
+      , "ScienceFiction_TheBestOfTheYear2007Edition.epub"
+      )
+
+
+testBestSF :: (Globals, [Formatter]) -> TestTree
+testBestSF (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "The Best Science Fiction and Fantasy of the Year" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Strahan, Jonathan")
+        Nothing
+        "Jonathan Strahan"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "The Best Science Fiction and Fantasy of the Year: Volume 2"]
+      , metaSubjects = ["anthology"]
+      }
+    expected =
+      ( "anthology"
+      , "TheBestScienceFictionAndFantasyOfTheYear_Volume2.epub"
+      )
+
+
+testYearsBest :: (Globals, [Formatter]) -> TestTree
+testYearsBest (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "The Year's Best SF" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing Nothing
+        "Rich Horton, Michael Swanwick, Karen Joy Fowler"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "The Year's Best Science Fiction: 2008 Edition"]
+      , metaSubjects = ["anthology"]
+      }
+    expected =
+      ( "anthology"
+      , "TheYearsBestScienceFiction_2008Edition.epub"
+      )
+
+
+testMagBlackStatic :: (Globals, [Formatter]) -> TestTree
+testMagBlackStatic (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Black Static Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing Nothing 
+        Nothing "TTA Press Authors"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Black Static Horror Magazine #5"]
+      }
+    expected =
+      ( "magBlackStatic"
+      , "BlackStaticHorrorMagazine05.epub"
+      )
+
+
+testRageMachineMag :: (Globals, [Formatter]) -> TestTree
+testRageMachineMag (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Rage Machine Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "edt") Nothing 
+        Nothing "G. W. Thomas"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Rage Machine Magazine #1--December 2005"]
+      }
+    expected =
+      ( "magRageMachine"
+      , "RageMachineMagazine1_2005-12.epub"
+      )
+
+
+testEclipseMagWord :: (Globals, [Formatter]) -> TestTree
+testEclipseMagWord (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Eclipse Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Strahan, Jonathan")
+        Nothing
+        "Jonathan Strahan"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Eclipse One"]
+      }
+    expected =
+      ( "magEclipse_word"
+      , "Eclipse01.epub"
+      )
+
+
+testEclipseMagNum :: (Globals, [Formatter]) -> TestTree
+testEclipseMagNum (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Eclipse Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Strahan, Jonathan")
+        Nothing
+        "Jonathan Strahan"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Eclipse 4: New Science Fiction and Fantasy"]
+      }
+    expected =
+      ( "magEclipse_num"
+      , "Eclipse04.epub"
+      )
+
+
+testBcs :: (Globals, [Formatter]) -> TestTree
+testBcs (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Beneath Ceaseless Skies Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = 
+        [ Creator (Just "aut")
+          (Just "Tidwell, Erin A.")
+          Nothing
+          "Hoover, Kenneth Mark"
+        , Creator (Just "aut")
+          (Just "Tidwell, Erin A.")
+          Nothing
+          "Tidwell, Erin A."
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Beneath Ceaseless Skies #32"]
+      }
+    expected =
+      ( "magBcs"
+      , "BeneathCeaselessSkies_Issue032.epub"
+      )
+
+
+testBkpFileAs :: (Globals, [Formatter]) -> TestTree
+testBkpFileAs (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "book publisher suffix requested and present in file-as"
+    expected
+  where
+    testOpts = (gOpts gs) { includePublisher = PublisherSwitch True }
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Herman Melville"]
+      , metaContributors = [Creator (Just "bkp") (Just "acme") 
+        Nothing "Acme Publishing, Inc."]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick_acme.epub"
+      )
+
+
+testBkpText :: (Globals, [Formatter]) -> TestTree
+testBkpText (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "book publisher suffix requested and present in text"
+    expected
+  where
+    testOpts = (gOpts gs) { includePublisher = PublisherSwitch True }
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Herman Melville"]
+      , metaContributors = [Creator (Just "bkp") Nothing
+        Nothing "Acme Publishing, Inc."]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick.epub"
+      )
+
+
+testBkpMissing :: (Globals, [Formatter]) -> TestTree
+testBkpMissing (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "book publisher suffix requested and not present"
+    expected
+  where
+    testOpts = (gOpts gs) { includePublisher = PublisherSwitch True }
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Herman Melville"]
+      , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+      }
+    expected =
+      ( "ordinary_book"
+      , "MelvilleHerman-MobyDick.epub"
+      )
+
+
+testMagUniverse :: (Globals, [Formatter]) -> TestTree
+testMagUniverse (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "Jim Baen's Universe Magazine" expected
+  where
+    testOpts = (gOpts gs) { includePublisher = PublisherSwitch True }
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing
+        Nothing "Jim Baen's Universe"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Jim Baen's Universe-Vol 4 Num 6"]
+      }
+    expected =
+      ( "magGenericVolNo2"
+      , "JimBaensUniverse-Vol04No06.epub"
+      )
+
+
+testMagClarkesworld :: (Globals, [Formatter]) -> TestTree
+testMagClarkesworld (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Clarkesworld Magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut")
+        (Just "Kowal, Mary Robinette") Nothing
+        "Mary Robinette Kowal"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Clarkesworld Magazine - Issue 21"]
+      }
+    expected =
+      ( "magClarkesworld"
+      , "Clarkesworld021.epub"
+      )
+
+
+testLightspeedDate :: (Globals, [Formatter]) -> TestTree
+testLightspeedDate (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Lightspeed Magazine, date in title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") Nothing Nothing
+        "Edited by John Joseph Adams"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Lightspeed Magazine, June 2010"]
+      }
+    expected =
+      ( "magLightspeed_date"
+      , "Lightspeed2010-06.epub"
+      )
+
+
+testLightspeedMagIssue :: (Globals, [Formatter]) -> TestTree
+testLightspeedMagIssue (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Lightspeed Magazine, issue number in title" expected
+  where
+    meta = emptyMetadata
+      { metaCreators = [Creator (Just "aut") (Just "Magazine, Lightspeed & Clark, Maggie & Valentine, Genevieve & Baxter, Stephen & Okorafor, Nnedi & Wilison, Daniel H. & Reed, Robert & Sedia, Ekaterina") Nothing "Lightspeed Magazine"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Lightspeed Magazine Issue 10"]
+      }
+    expected =
+      ( "magLightspeed_issue"
+      , "Lightspeed010.epub"
+      )
+
+
+testLightspeedIssue :: (Globals, [Formatter]) -> TestTree
+testLightspeedIssue (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Lightspeed Magazine, issue number in title" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Lightspeed Issue 33"]
+      }
+    expected =
+      ( "magLightspeed_issue"
+      , "Lightspeed033.epub"
+      )
+
+
+testLightspeedMagIssueDate :: (Globals, [Formatter]) -> TestTree
+testLightspeedMagIssueDate (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Lightspeed Magazine, both issue and date in title" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Lightspeed Magazine, Issue 62 (July 2015)"]
+      }
+    expected =
+      ( "magLightspeed_issue"
+      , "Lightspeed062.epub"
+      )
+
+
+testMagWeirdTales :: (Globals, [Formatter]) -> TestTree
+testMagWeirdTales (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Weird Tales magazine" expected
+  where
+    meta = emptyMetadata
+      { metaCreators =
+        [ Creator (Just "aut") Nothing Nothing "VanderMeer"
+        , Creator (Just "aut") Nothing Nothing "Ann"
+        , Creator (Just "aut") Nothing Nothing "Spinrad"
+        , Creator (Just "aut") Nothing Nothing "Norman"
+        ]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Weird Tales #350"]
+      }
+    expected =
+      ( "magWeirdTales"
+      , "WeirdTales350.epub"
+      )
+
+
+testMagGalaxysEdge :: (Globals, [Formatter]) -> TestTree
+testMagGalaxysEdge (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Galaxy's Edge magazine" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Galaxy's Edge Magazine: Issue 1 March 2013"]
+      }
+    expected =
+      ( "magGenericWithIssue"
+      , "GalaxysEdgeMagazine01.epub"
+      )
+
+
+testMagPlasmaFreq :: (Globals, [Formatter]) -> TestTree
+testMagPlasmaFreq (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Plasma Frequency magazine" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Plasma Frequency Magazine Issue 1"]
+      }
+    expected =
+      ( "magGenericWithIssue"
+      , "PlasmaFrequencyMagazine01.epub"
+      )
+
+
+testMagPlasmaFreqMonth :: (Globals, [Formatter]) -> TestTree
+testMagPlasmaFreqMonth (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Plasma Frequency magazine, incl month/year in title" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Plasma Frequency Magazine: Issue 8 October/November 2013"]
+      }
+    expected =
+      ( "magGenericWithIssue"
+      , "PlasmaFrequencyMagazine08.epub"
+      )
+
+
+testMagPunchinello1800s :: (Globals, [Formatter]) -> TestTree
+testMagPunchinello1800s (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "Punchinello magazine from the 1800s" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Punchinello, Volume 1, No. 06, May 7, 1870"]
+      }
+    expected =
+      ( "magGenericVolNo2"
+      , "PunchinelloVol01No06.epub"
+      )
+
+
+testMagGenericVolNo3 :: (Globals, [Formatter]) -> TestTree
+testMagGenericVolNo3 (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "generic magazine with volume and 3-digit number" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Blackwood's Edinburgh Magazine Volume 53, No. 327, January, 1843"]
+      }
+    expected =
+      ( "magGenericVolNo3"
+      , "BlackwoodsEdinburghMagazineVol53No327.epub"
+      )
+
+
+testMagSubjWithIssue :: (Globals, [Formatter]) -> TestTree
+testMagSubjWithIssue (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "with magazine subject and issue" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing "The Dark Issue 1"]
+      , metaCreators =
+        [ Creator (Just "aut") (Just "Okorafor, Nnedi & Swirsky, Rachel & Slatter, Angela & Hannett, Lisa L.") Nothing
+          "Nnedi Okorafor"
+        , Creator (Just "aut") Nothing Nothing
+          "Rachel Swirsky"
+        ]
+      , metaDates = Map.fromList [(Created, DateValue "2013-10-01")]
+      , metaSubjects =
+        [ "magazine"
+        , "horror"
+        , "dark fantasy"
+        ]
+      }
+    expected =
+      ( "magGenericSubjWithIssue"
+      , "TheDarkMagazine01.epub"
+      )
+
+
+testWomensInstituteLib :: (Globals, [Formatter]) -> TestTree
+testWomensInstituteLib (gs, fs) =
+  assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+    "Women's Institute Library" expected
+  where
+    testOpts = (gOpts gs) { includePublisher = PublisherSwitch True }
+    meta = emptyMetadata
+      { metaCreators = [Creator Nothing
+        (Just "Woman's Institute of Domestic Arts and Sciences")
+        Nothing "Woman's Institute of Domestic Arts and Sciences"]
+      , metaTitles = [Title Nothing Nothing Nothing
+        "Woman's Institute Library of Cookery\nVolume 1: Essentials of Cookery; Cereals; Bread; Hot Breads"]
+      }
+    expected =
+      ( "nonficWomensInstituteLibrary"
+      , "WomansInstituteLibraryOfCookery_Volume1_EssentialsOfCookeryCerealsBreadHotBreads.epub"
+      )
+
+
+testAnthologyDate :: (Globals, [Formatter]) -> TestTree
+testAnthologyDate (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs
+    "anthology with date in title" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing "Flash Fiction Online - June 2015"]
+      , metaCreators =
+        [ Creator (Just "art") (Just "Bijelac, Dario") Nothing
+          "Dario Bijelac"
+        , Creator (Just "aut") (Just "Pearlman, Laura") Nothing
+          "Laura Pearlman"
+        ]
+      , metaDates = Map.fromList [(Epub, DateValue "2015-05-30T18:47:28.000000+00:00")]
+      , metaSubjects = ["anthology"]
+      }
+    expected =
+      ( "anthology_date"
+      , "FlashFictionOnline2015-06.epub"
+      )
+
+
+testAnthology :: (Globals, [Formatter]) -> TestTree
+testAnthology (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "anthology" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing "Creepy Secrets"]
+      , metaCreators =
+        [ Creator (Just "aut") (Just "Snively, Mortimer") Nothing
+          "Mortimer Snively"
+        , Creator (Just "aut") (Just "Baxter, Joanne") Nothing
+          "Joanne Baxter"
+        ]
+      , metaDates = Map.fromList [(Created, DateValue "2010-11-01")]
+      , metaSubjects = ["fantasy and horror anthology"]
+      }
+    expected =
+      ( "anthology"
+      , "CreepySecrets_2010.epub"
+      )
+
+
+testMagLunaStation :: (Globals, [Formatter]) -> TestTree
+testMagLunaStation (gs, fs) =
+  assertNewName gs { gMetadata = meta } fs "Luna Station Quarterly magazine" expected
+  where
+    meta = emptyMetadata
+      { metaTitles = [Title Nothing Nothing Nothing
+        "Luna Station Quarterly - Issue 028"]
+      }
+    expected =
+      ( "magLunaStationQuarterly"
+      , "LunaStationQuarterly028.epub"
+      )
diff --git a/src/test/EpubTools/Test/EpubName/Main.hs b/src/test/EpubTools/Test/EpubName/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/test/EpubTools/Test/EpubName/Main.hs
@@ -0,0 +1,24 @@
+import Control.Monad.Except (runExceptT)
+import System.Exit (exitWith)
+import Test.Tasty (defaultMain, testGroup)
+
+import EpubTools.EpubName.Common (Options, defaultOptions)
+import EpubTools.EpubName.Format.Format (Formatter)
+import EpubTools.EpubName.Main (initialize)
+import EpubTools.Test.EpubName.Format (formatTests)
+import EpubTools.Test.EpubName.PubYear (pubYearTests)
+
+
+main :: IO ()
+main = do
+  -- Can use this def to adjust the options with defaultOptions { ... }
+  let testOpts = defaultOptions
+  ir <- runExceptT $ initialize testOpts
+  either (exitWith) (runTests testOpts) ir
+
+
+runTests :: Options -> [Formatter] -> IO ()
+runTests opts fs = defaultMain $ testGroup " EpubName tests"
+  [ formatTests opts fs
+  , pubYearTests
+  ]
diff --git a/src/test/EpubTools/Test/EpubName/PubYear.hs b/src/test/EpubTools/Test/EpubName/PubYear.hs
new file mode 100644
--- /dev/null
+++ b/src/test/EpubTools/Test/EpubName/PubYear.hs
@@ -0,0 +1,30 @@
+module EpubTools.Test.EpubName.PubYear
+  ( pubYearTests
+  )
+  where
+
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit ((@=?), testCase)
+
+import Codec.Epub.Data.Metadata (DateValue (..))
+import EpubTools.EpubName.Format.PubYear (extractYear)
+
+
+pubYearTests :: TestTree
+pubYearTests = testGroup "PubYear"
+  [ testExtractYear "2001-03-25T00:00:00" "2001"
+  , testExtractYear "2007" "2007"
+  , testExtractYear "Sat Nov 03 18:49:50 +0100 2007" "2007"
+  , testExtractYear "2001-03-25 05:00:00+00:00" "2001"
+  , testExtractYear "2009-11-19T07:00:00+00:00" "2009"
+  , testExtractYear "2010-09-21" "2010"
+  , testExtractYear "2010-02-14T21:47:47.682882+00:00" "2010"
+  , testExtractYear "2011-11-16T09:19:50-0500" "2011"
+  , testExtractYear "2011-11-16T14:19:52Z" "2011"
+  , testExtractYear "Tue, 29 Nov 2011 06:21:19 +0000" "2011"
+  ]
+
+
+testExtractYear :: String -> String -> TestTree
+testExtractYear dateStr expected = testCase dateStr $
+  (Just expected) @=? (extractYear . DateValue $ dateStr)
diff --git a/src/tests/EpubTools/Test/EpubName/Format.hs b/src/tests/EpubTools/Test/EpubName/Format.hs
deleted file mode 100644
--- a/src/tests/EpubTools/Test/EpubName/Format.hs
+++ /dev/null
@@ -1,1657 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.Test.EpubName.Format
-   ( formatTests
-   )
-   where
-
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
-import Control.Monad.Except
-import qualified Data.Map.Strict as Map
-import Test.HUnit ( Test (..), assertEqual )
-import Test.HUnit.Base ( Assertion )
-
-import EpubTools.EpubName.Format.Format
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Opts
-
-
-formatTests :: Options -> [Formatter] -> Test
-formatTests opts fs = TestLabel "Format" $ TestList $
-   map (\f -> f ((Globals opts (Package "3.3" "") emptyMetadata), fs))
-      [ testAuthorMinimal
-      , testAuthorOneName
-      , testAuthorRole
-      , testAuthorFileas
-      , testAuthorFileasParens
-      , testAuthorFull
-      , testAuthorMiddle
-      , testMultiAutCreators
-      , testMultiAutOneString
-      , testNoAuthor
-      , testColon
-      , testCreatorsNoAuthor
-      , testCreatorsNoAuthorPubDate
-      , testCreatorLastFirst
-      , testTitleCaps
-      , testTitleBracket
-      , testTitleNone
-      , testTitleMultiline
-      , testTitleHyphen
-      , testTitleHyphenDates
-      , testTitleRomanNum
-      , testAllPunctuation
-      , testPubYearNoDatesPresent
-      , testPubYearAnyIssued
-      , testPubYearAnyCreated
-      , testPubYearAnyDate
-      , testPubYearAnyEpub
-      , testPubYearAnyModified
-      , testPubYearAnyAllPresent
-      , testPubYearNoModified
-      , testPubYearNoDate
-      , testMagAeon
-      , testMagAEon
-      , testMagApexPound
-      , testMagApexIssue
-      , testMagApexLong
-      , testChallengingDestinyShort
-      , testChallengingDestinyLong
-      , testAnalogSingle
-      , testAnalogDouble
-      , testAsimovs
-      , testFantasyMagazine
-      , testFsfShort
-      , testFsfShortComma
-      , testFsfLong
-      , testFsfAmpersand
-      , testFsfAmpersandSpaces
-      , testFsfVeryLong
-      , testMagFutureOrbits
-      , testGudShort
-      , testGudLong
-      , testGudVeryLong
-      , testInterzone
-      , testInterzoneCaps
-      , testInterzoneOldLong
-      , testInterzoneOldShort
-      , testNemesisShort
-      , testNemesisLong
-      , testMagSomethingWicked
-      , testMagSomethingWickedMonth
-      , testSFBestOf
-      , testBestSF
-      , testYearsBest
-      , testMagBlackStatic
-      , testRageMachineMag
-      , testEclipseMagWord
-      , testEclipseMagNum
-      , testBcs
-      , testBkpFileAs
-      , testBkpText
-      , testBkpMissing
-      , testMagUniverse
-      , testMagClarkesworld
-      , testLightspeedDate
-      , testLightspeedMagIssue
-      , testLightspeedIssue
-      , testLightspeedMagIssueDate
-      , testMagWeirdTales
-      , testMagGalaxysEdge
-      , testMagPlasmaFreq
-      , testMagPlasmaFreqMonth
-      , testMagPunchinello1800s
-      , testMagGenericVolNo3
-      , testMagSubjWithIssue
-      , testWomensInstituteLib
-      , testAnthologyDate
-      , testAnthology
-      , testMagLunaStation
-      ]
-
-
-assertNewName :: Globals -> [Formatter] -> String
-   -> (String, String) -> Assertion
-assertNewName globals fs desc expected = do
-   result <- runExceptT $ tryFormatting globals fs ""
-   let actual = either (\em -> ("NO FORMATTER", em)) id result
-   assertEqual desc expected actual
-
-
-testAuthorMinimal :: (Globals, [Formatter]) -> Test
-testAuthorMinimal (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "minimal author" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorOneName :: (Globals, [Formatter]) -> Test
-testAuthorOneName (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "author is a single word" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Melville-MobyDick.epub"
-         )
-
-
-testAuthorRole :: (Globals, [Formatter]) -> Test
-testAuthorRole (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author with role" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorFileas :: (Globals, [Formatter]) -> Test
-testAuthorFileas (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author with file-as" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing
-            (Just "Melville, Herman")
-            Nothing
-            "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorFileasParens :: (Globals, [Formatter]) -> Test
-testAuthorFileasParens (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "author with file-as and pesky parenthesized full names" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing
-            (Just "Fletcher, J. S. (Joseph Smith)")
-            Nothing
-            "J. S. Fletcher"]
-         , metaTitles = [Title Nothing Nothing Nothing "The Middle of Things"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "FletcherJS-TheMiddleOfThings.epub"
-         )
-
-
-testAuthorFull :: (Globals, [Formatter]) -> Test
-testAuthorFull (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author fully filled out" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Melville, Herman")
-            Nothing
-            "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorMiddle :: (Globals, [Formatter]) -> Test
-testAuthorMiddle (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author with middle-name" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Wallace, David Foster")
-            Nothing
-            "David Foster Wallace"]
-         , metaTitles = [Title Nothing Nothing Nothing "Infinite Jest"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "WallaceDavidFoster-InfiniteJest.epub"
-         )
-
-
-testMultiAutCreators :: (Globals, [Formatter]) -> Test
-testMultiAutCreators (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "several authors" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "aut") Nothing
-               Nothing "James Patrick Kelly"
-            , Creator (Just "aut") (Just "Kessel, John")
-               Nothing "John Kessel"
-            , Creator (Just "aut") Nothing
-               Nothing "Jonathan Lethem"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Ninety Percent of Everything"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Kelly_Kessel_Lethem-NinetyPercentOfEverything.epub"
-         )
-
-
-testMultiAutOneString :: (Globals, [Formatter]) -> Test
-testMultiAutOneString (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "more than one author separated by & and/or and" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Yvonne Q. Anderson & Eva Tunglewacker and Jefferson Milner"]
-         , metaTitles = [Title Nothing Nothing Nothing "Big Trouble"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Anderson_Tunglewacker_Milner-BigTrouble.epub"
-         )
-
-
-testNoAuthor :: (Globals, [Formatter]) -> Test
-testNoAuthor (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "no creator(s) at all" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = []
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Some Collection of Fine Stories, Volume 1"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SomeCollectionOfFineStoriesVolume1.epub"
-         )
-
-
-testCreatorsNoAuthor :: (Globals, [Formatter]) -> Test
-testCreatorsNoAuthor (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "creators, but no author(s) at all" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "edt") Nothing
-               Nothing "Graham Spindlewest"
-            , Creator (Just "ill") Nothing
-               Nothing "Eva Tunglewacker"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Some Collection of Fine Stories, Volume 1"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SomeCollectionOfFineStoriesVolume1.epub"
-         )
-
-
-testColon :: (Globals, [Formatter]) -> Test
-testColon (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "colon becomes underscore" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Ed Howdershelt"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Book 1: 3rd World Products, Inc."]
-         }
-      expected =
-         ( "ordinary_book"
-         , "HowdersheltEd-Book1_3rdWorldProductsInc.epub"
-         )
-
-
-testCreatorsNoAuthorPubDate :: (Globals, [Formatter]) -> Test
-testCreatorsNoAuthorPubDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "creators, but no author(s) at all, with pub date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "edt") Nothing
-               Nothing "Graham Spindlewest"
-            , Creator (Just "ill") Nothing
-               Nothing "Eva Tunglewacker"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Some Collection of Fine Stories, Volume 1"]
-         , metaDates = Map.fromList [(Created, DateValue "2008")]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SomeCollectionOfFineStoriesVolume1_2008.epub"
-         )
-
-
-testCreatorLastFirst :: (Globals, [Formatter]) -> Test
-testCreatorLastFirst (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "creator text (not file-as) is last-name-first" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator Nothing Nothing
-               Nothing "Spindlewest, Graham"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "A Midnight's Horror Story"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SpindlewestGraham-AMidnightsHorrorStory.epub"
-         )
-
-
-testTitleCaps :: (Globals, [Formatter]) -> Test
-testTitleCaps (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "title all caps" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Greg Bear"]
-         , metaTitles = [Title Nothing Nothing Nothing "EON"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "BearGreg-Eon.epub"
-         )
-
-
-testTitleBracket :: (Globals, [Formatter]) -> Test
-testTitleBracket (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with brackets" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Mercedes Lackey"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "SKitty [Shipscat series #1]"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "LackeyMercedes-Skitty_ShipscatSeries1.epub"
-         )
-
-
-testTitleNone :: (Globals, [Formatter]) -> Test
-testTitleNone (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "missing title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Nobody McCrankypants"]
-         }
-      expected =
-         ( "NO FORMATTER"
-         , " [ERROR No formatter found]"
-         )
-
-
-testTitleMultiline :: (Globals, [Formatter]) -> Test
-testTitleMultiline (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "multiline title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Castleman, Virginia Carter")
-            Nothing
-            "Virginia Carter Castleman"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Pocahontas.\nA Poem"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "CastlemanVirginiaCarter-Pocahontas_APoem.epub"
-         )
-
-
-testTitleHyphen :: (Globals, [Formatter]) -> Test
-testTitleHyphen (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with hyphen" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Spinoza, Benedictus de")
-            Nothing
-            "Benedictus de Spinoza"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Theologico-Political Treatise"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SpinozaBenedictusDe-Theologico-PoliticalTreatise.epub"
-         )
-
-
-testTitleHyphenDates :: (Globals, [Formatter]) -> Test
-testTitleHyphenDates (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with hyphen separating dates" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Anonymous")
-            Nothing
-            "Anonymous"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Queen Victoria\nStory of Her Life and Reign, 1819-1901"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Anonymous-QueenVictoria_StoryOfHerLifeAndReign1819-1901.epub"
-         )
-
-testTitleRomanNum :: (Globals, [Formatter]) -> Test
-testTitleRomanNum (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with Roman numerals" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Hauptmann, Gerhart")
-            Nothing
-            "Gerhart Hauptmann"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The Dramatic Works of Gerhart Hauptmann\nVolume III"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "HauptmannGerhart-TheDramaticWorksOfGerhartHauptmann_VolumeIII.epub"
-         )
-
-
-testAllPunctuation :: (Globals, [Formatter]) -> Test
-testAllPunctuation (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "big test of all punctuation" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dino Morelli"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The *crazy*: Sand-box. Of Smedley's discontent/loathing, fear & Malnourishment? (Maybe not!); [Part #2]"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MorelliDino-TheCrazy_Sand-BoxOfSmedleysDiscontentLoathingFearAndMalnourishmentMaybeNot_Part2.epub"
-         )
-
-
-testPubYearNoDatesPresent :: (Globals, [Formatter]) -> Test
-testPubYearNoDatesPresent (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta} fs
-      "book with no dates at all" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testPubYearAnyIssued :: (Globals, [Formatter]) -> Test
-testPubYearAnyIssued (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with only issued date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList [(Issued, DateValue "2003")]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearAnyCreated :: (Globals, [Formatter]) -> Test
-testPubYearAnyCreated (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with only created date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList [(Created, DateValue "2003")]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearAnyDate :: (Globals, [Formatter]) -> Test
-testPubYearAnyDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with only created date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList [(Date, DateValue "2003")]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearAnyEpub :: (Globals, [Formatter]) -> Test
-testPubYearAnyEpub (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with only the simple (date element) date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList [(Epub, DateValue "2003")]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearAnyModified :: (Globals, [Formatter]) -> Test
-testPubYearAnyModified (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with only modified date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList [(Modified, DateValue "2003")]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearAnyAllPresent :: (Globals, [Formatter]) -> Test
-testPubYearAnyAllPresent (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with all dates" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList
-            [ (Epub, DateValue "2001")
-            , (Date, DateValue "2005")
-            , (Modified, DateValue "2004")
-            , (Created, DateValue "2003")
-            , (Issued, DateValue "2002")
-            ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2002.epub"
-         )
-
-
-testPubYearNoModified :: (Globals, [Formatter]) -> Test
-testPubYearNoModified (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book with only modified date, --no-modified-date switch" expected
-   where
-      testOpts = (gOpts gs) { optPubYear = NoModified }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList [ (Modified, DateValue "2004") ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testPubYearNoDate :: (Globals, [Formatter]) -> Test
-testPubYearNoDate (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "epub book with lots of dates but we don't want"
-      expected
-   where
-      testOpts = (gOpts gs) { optPubYear = NoDate }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = Map.fromList
-            [ (Epub, DateValue "2001")
-            , (Date, DateValue "2005")
-            , (Modified, DateValue "2004")
-            , (Created, DateValue "2003")
-            , (Issued, DateValue "2002")
-            ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testMagAeon :: (Globals, [Formatter]) -> Test
-testMagAeon (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Aeon magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Aeon Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "Aeon Eight"]
-         }
-      expected =
-         ( "magAeon"
-         , "AeonMagazine08.epub"
-         )
-
-
-testMagAEon :: (Globals, [Formatter]) -> Test
-testMagAEon (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "AEon magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "AEon Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "Aeon Thirteen"]
-         }
-      expected =
-         ( "magAeon"
-         , "AeonMagazine13.epub"
-         )
-
-
-testMagApexPound :: (Globals, [Formatter]) -> Test
-testMagApexPound (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Apex Magazine, pound sign and issue number" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Apex Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Apex Magazine #16"]
-         }
-      expected =
-         ( "magApex"
-         , "ApexMagazine016.epub"
-         )
-
-
-testMagApexIssue :: (Globals, [Formatter]) -> Test
-testMagApexIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Apex Magazine, Issue number" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Apex Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Apex Magazine Issue 17"]
-         }
-      expected =
-         ( "magApex"
-         , "ApexMagazine017.epub"
-         )
-
-
-testMagApexLong :: (Globals, [Formatter]) -> Test
-testMagApexLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Apex Magazine, title with month year" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Apex Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Apex Magazine: Issue 101, October 2017"]
-         }
-      expected =
-         ( "magApex"
-         , "ApexMagazine101.epub"
-         )
-
-
-testChallengingDestinyShort :: (Globals, [Formatter]) -> Test
-testChallengingDestinyShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Challenging Destiny Magazine, short title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Crystalline Sphere Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Challenging Destiny #23"]
-         }
-      expected =
-         ( "magChallengingDestiny"
-         , "ChallengingDestinyMagazine023.epub"
-         )
-
-
-testChallengingDestinyLong :: (Globals, [Formatter]) -> Test
-testChallengingDestinyLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Challenging Destiny Magazine, long title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Crystalline Sphere Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Challenging Destiny #24: August 2007"]
-         }
-      expected =
-         ( "magChallengingDestiny"
-         , "ChallengingDestinyMagazine024.epub"
-         )
-
-
-testAnalogSingle :: (Globals, [Formatter]) -> Test
-testAnalogSingle (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Analog, single month" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dell Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Analog SFF, November 2010"]
-         }
-      expected =
-         ( "magAnalog"
-         , "AnalogSF2010-11.epub"
-         )
-
-
-testAnalogDouble :: (Globals, [Formatter]) -> Test
-testAnalogDouble (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Analog, double month" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dell Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Analog SFF, July-August 2003"]
-         }
-      expected =
-         ( "magAnalog"
-         , "AnalogSF2003-07_08.epub"
-         )
-
-
-testAsimovs :: (Globals, [Formatter]) -> Test
-testAsimovs (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Asimovs" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dell Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Asimov's SF, August 2003"]
-         }
-      expected =
-         ( "magAnalog"
-         , "AsimovsSF2003-08.epub"
-         )
-
-testFantasyMagazine :: (Globals, [Formatter]) -> Test
-testFantasyMagazine (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Fantasy Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "aut") (Just "Howard, Kat & Tanzer, Molly & Beagle, Peter S. & Howard, Jonathan L. & Valentine, Genevieve & Vaughn, Carrie & Pilinovsky, Helen") Nothing "Kat Howard"
-            , Creator (Just "aut") Nothing Nothing "Molly Tanzer"
-            , Creator (Just "aut") Nothing Nothing "Peter S. Beagle"
-            , Creator (Just "aut") Nothing Nothing "Jonathan L. Howard"
-            , Creator (Just "aut") Nothing Nothing "Genevieve Valentine"
-            , Creator (Just "aut") Nothing Nothing "Carrie Vaughn"
-            , Creator (Just "aut") Nothing Nothing "Helen Pilinovsky"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Fantasy Magazine Issue 49"]
-         }
-      expected =
-         ( "magFantasyMag"
-         , "FantasyMagazine049.epub"
-         )
-
-
-testFsfShort :: (Globals, [Formatter]) -> Test
-testFsfShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "FSF Oct/Nov 2003"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2003-10_11.epub"
-         )
-
-
-testFsfShortComma :: (Globals, [Formatter]) -> Test
-testFsfShortComma (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, short, comma" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "FSF, April 2008"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2008-04.epub"
-         )
-
-
-testFsfLong :: (Globals, [Formatter]) -> Test
-testFsfLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Spilogale Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "FSF Magazine, April 2006"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2006-04.epub"
-         )
-
-
-testFsfAmpersand :: (Globals, [Formatter]) -> Test
-testFsfAmpersand (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, ampersand" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale"]
-         , metaTitles = [Title Nothing Nothing Nothing "F&SF, Apr 2004"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2004-04.epub"
-         )
-
-
-testFsfAmpersandSpaces :: (Globals, [Formatter]) -> Test
-testFsfAmpersandSpaces (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, ampersand, spaces" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale"]
-         , metaTitles = [Title Nothing Nothing Nothing "F & SF Dec 2003"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2003-12.epub"
-         )
-
-
-testFsfVeryLong :: (Globals, [Formatter]) -> Test
-testFsfVeryLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, very long name" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale Inc."]
-         , metaTitles = [Title Nothing Nothing Nothing "Fantasy & Science Fiction, November/December 2017"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2017-11_12.epub"
-         )
-
-
-testMagFutureOrbits :: (Globals, [Formatter]) -> Test
-testMagFutureOrbits (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "testMagFutureOrbits" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Vander Neut Publications, L.L.C."]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Future Orbits Issue 5, June/July 2002"]
-         }
-      expected =
-         ( "magFutureOrbits"
-         , "FutureOrbitsMagazine05_2002-06_07.epub"
-         )
-
-
-testGudShort :: (Globals, [Formatter]) -> Test
-testGudShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Gud Magazine, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "GUD Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "GUD Magazine Issue 0 :: Spring 2007"]
-         }
-      expected =
-         ( "magGud"
-         , "GUDMagazine00.epub"
-         )
-
-
-testGudLong :: (Globals, [Formatter]) -> Test
-testGudLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Gud Magazine, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "GUD Magazine Authors, Jeff Somers, Jeremy Shipp"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "GUD Magazine Issue 2 :: Spring 2008"]
-         }
-      expected =
-         ( "magGud"
-         , "GUDMagazine02.epub"
-         )
-
-
-testGudVeryLong :: (Globals, [Formatter]) -> Test
-testGudVeryLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Gud Magazine, very long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "GUD Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Greatest Uncommon Denominator Magazine Issue 4 :: Spring 2009"]
-         }
-      expected =
-         ( "magGud"
-         , "GUDMagazine04.epub"
-         )
-
-
-testInterzone :: (Globals, [Formatter]) -> Test
-testInterzone (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, Smashwords style" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Interzone 233 Mar - Apr 2011"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF233.epub"
-         )
-
-
-testInterzoneCaps :: (Globals, [Formatter]) -> Test
-testInterzoneCaps (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, started in 2014" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing (Just "Interzone 249")
-            Nothing "TTA Press"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "INTERZONE #249 SCIENCE FICTION & FANTASY MAGAZINE (NOVâ\x80\x93DEC 2013)"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF249.epub"
-         )
-
-
-testInterzoneOldLong :: (Globals, [Formatter]) -> Test
-testInterzoneOldLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, old style, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Interzone Science Fiction and Fantasy Magazine #216"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF216.epub"
-         )
-
-
-testInterzoneOldShort :: (Globals, [Formatter]) -> Test
-testInterzoneOldShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, old style, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Interzone SFF #212"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF212.epub"
-         )
-
-
-testNemesisShort :: (Globals, [Formatter]) -> Test
-testNemesisShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Nemesis Magazine, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Stephen Adams"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Nemesis Magazine #2"]
-         }
-      expected =
-         ( "magNemesis"
-         , "NemesisMag002.epub"
-         )
-
-
-testNemesisLong :: (Globals, [Formatter]) -> Test
-testNemesisLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Nemesis Magazine, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Stephen Adams"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Nemesis Magazine #7: Featuring Victory Rose in Death Stalks the Ruins"]
-         }
-      expected =
-         ( "magNemesis"
-         , "NemesisMag007.epub"
-         )
-
-
-testMagSomethingWicked :: (Globals, [Formatter]) -> Test
-testMagSomethingWicked (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Something Wicked Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Something Wicked Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Something Wicked SF and Horror Magazine #5"]
-         }
-      expected =
-         ( "magSomethingWicked"
-         , "SomethingWicked005.epub"
-         )
-
-
-testMagSomethingWickedMonth :: (Globals, [Formatter]) -> Test
-testMagSomethingWickedMonth (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Something Wicked Magazine, month in title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Authors, Something Wicked")
-            Nothing
-            "Something Wicked Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Something Wicked #14 (October 2011)"]
-         }
-      expected =
-         ( "magSomethingWicked"
-         , "SomethingWicked014.epub"
-         )
-
-
-testSFBestOf :: (Globals, [Formatter]) -> Test
-testSFBestOf (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Science Fiction: The Best of the Year" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Rich Horton"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Science Fiction: The Best of the Year, 2007 Edition"]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "ScienceFiction_TheBestOfTheYear2007Edition.epub"
-         )
-
-
-testBestSF :: (Globals, [Formatter]) -> Test
-testBestSF (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "The Best Science Fiction and Fantasy of the Year" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Strahan, Jonathan")
-            Nothing
-            "Jonathan Strahan"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The Best Science Fiction and Fantasy of the Year: Volume 2"]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "TheBestScienceFictionAndFantasyOfTheYear_Volume2.epub"
-         )
-
-
-testYearsBest :: (Globals, [Formatter]) -> Test
-testYearsBest (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "The Year's Best SF" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing Nothing
-            "Rich Horton, Michael Swanwick, Karen Joy Fowler"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The Year's Best Science Fiction: 2008 Edition"]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "TheYearsBestScienceFiction_2008Edition.epub"
-         )
-
-
-testMagBlackStatic :: (Globals, [Formatter]) -> Test
-testMagBlackStatic (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Black Static Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Black Static Horror Magazine #5"]
-         }
-      expected =
-         ( "magBlackStatic"
-         , "BlackStaticHorrorMagazine05.epub"
-         )
-
-
-testRageMachineMag :: (Globals, [Formatter]) -> Test
-testRageMachineMag (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Rage Machine Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "edt") Nothing 
-            Nothing "G. W. Thomas"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Rage Machine Magazine #1--December 2005"]
-         }
-      expected =
-         ( "magRageMachine"
-         , "RageMachineMagazine1_2005-12.epub"
-         )
-
-
-testEclipseMagWord :: (Globals, [Formatter]) -> Test
-testEclipseMagWord (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Eclipse Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Strahan, Jonathan")
-            Nothing
-            "Jonathan Strahan"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Eclipse One"]
-         }
-      expected =
-         ( "magEclipse_word"
-         , "Eclipse01.epub"
-         )
-
-
-testEclipseMagNum :: (Globals, [Formatter]) -> Test
-testEclipseMagNum (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Eclipse Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Strahan, Jonathan")
-            Nothing
-            "Jonathan Strahan"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Eclipse 4: New Science Fiction and Fantasy"]
-         }
-      expected =
-         ( "magEclipse_num"
-         , "Eclipse04.epub"
-         )
-
-
-testBcs :: (Globals, [Formatter]) -> Test
-testBcs (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Beneath Ceaseless Skies Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = 
-            [ Creator (Just "aut")
-               (Just "Tidwell, Erin A.")
-               Nothing
-               "Hoover, Kenneth Mark"
-            , Creator (Just "aut")
-               (Just "Tidwell, Erin A.")
-               Nothing
-               "Tidwell, Erin A."
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Beneath Ceaseless Skies #32"]
-         }
-      expected =
-         ( "magBcs"
-         , "BeneathCeaselessSkies_Issue032.epub"
-         )
-
-
-testBkpFileAs :: (Globals, [Formatter]) -> Test
-testBkpFileAs (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book publisher suffix requested and present in file-as"
-      expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaContributors = [Creator (Just "bkp") (Just "acme") 
-            Nothing "Acme Publishing, Inc."]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick_acme.epub"
-         )
-
-
-testBkpText :: (Globals, [Formatter]) -> Test
-testBkpText (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book publisher suffix requested and present in text"
-      expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaContributors = [Creator (Just "bkp") Nothing
-            Nothing "Acme Publishing, Inc."]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testBkpMissing :: (Globals, [Formatter]) -> Test
-testBkpMissing (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book publisher suffix requested and not present"
-      expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testMagUniverse :: (Globals, [Formatter]) -> Test
-testMagUniverse (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "Jim Baen's Universe Magazine" expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Jim Baen's Universe"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Jim Baen's Universe-Vol 4 Num 6"]
-         }
-      expected =
-         ( "magGenericVolNo2"
-         , "JimBaensUniverse-Vol04No06.epub"
-         )
-
-
-testMagClarkesworld :: (Globals, [Formatter]) -> Test
-testMagClarkesworld (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Clarkesworld Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Kowal, Mary Robinette") Nothing
-            "Mary Robinette Kowal"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Clarkesworld Magazine - Issue 21"]
-         }
-      expected =
-         ( "magClarkesworld"
-         , "Clarkesworld021.epub"
-         )
-
-
-testLightspeedDate :: (Globals, [Formatter]) -> Test
-testLightspeedDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, date in title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing Nothing
-            "Edited by John Joseph Adams"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Magazine, June 2010"]
-         }
-      expected =
-         ( "magLightspeed_date"
-         , "Lightspeed2010-06.epub"
-         )
-
-
-testLightspeedMagIssue :: (Globals, [Formatter]) -> Test
-testLightspeedMagIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, issue number in title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") (Just "Magazine, Lightspeed & Clark, Maggie & Valentine, Genevieve & Baxter, Stephen & Okorafor, Nnedi & Wilison, Daniel H. & Reed, Robert & Sedia, Ekaterina") Nothing "Lightspeed Magazine"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Magazine Issue 10"]
-         }
-      expected =
-         ( "magLightspeed_issue"
-         , "Lightspeed010.epub"
-         )
-
-
-testLightspeedIssue :: (Globals, [Formatter]) -> Test
-testLightspeedIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, issue number in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Issue 33"]
-         }
-      expected =
-         ( "magLightspeed_issue"
-         , "Lightspeed033.epub"
-         )
-
-
-testLightspeedMagIssueDate :: (Globals, [Formatter]) -> Test
-testLightspeedMagIssueDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, both issue and date in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Magazine, Issue 62 (July 2015)"]
-         }
-      expected =
-         ( "magLightspeed_issue"
-         , "Lightspeed062.epub"
-         )
-
-
-testMagWeirdTales :: (Globals, [Formatter]) -> Test
-testMagWeirdTales (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Weird Tales magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "aut") Nothing Nothing "VanderMeer"
-            , Creator (Just "aut") Nothing Nothing "Ann"
-            , Creator (Just "aut") Nothing Nothing "Spinrad"
-            , Creator (Just "aut") Nothing Nothing "Norman"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Weird Tales #350"]
-         }
-      expected =
-         ( "magWeirdTales"
-         , "WeirdTales350.epub"
-         )
-
-
-testMagGalaxysEdge :: (Globals, [Formatter]) -> Test
-testMagGalaxysEdge (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Galaxy's Edge magazine" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Galaxy's Edge Magazine: Issue 1 March 2013"]
-         }
-      expected =
-         ( "magGenericWithIssue"
-         , "GalaxysEdgeMagazine01.epub"
-         )
-
-
-testMagPlasmaFreq :: (Globals, [Formatter]) -> Test
-testMagPlasmaFreq (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Plasma Frequency magazine" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Plasma Frequency Magazine Issue 1"]
-         }
-      expected =
-         ( "magGenericWithIssue"
-         , "PlasmaFrequencyMagazine01.epub"
-         )
-
-
-testMagPlasmaFreqMonth :: (Globals, [Formatter]) -> Test
-testMagPlasmaFreqMonth (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Plasma Frequency magazine, incl month/year in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Plasma Frequency Magazine: Issue 8 October/November 2013"]
-         }
-      expected =
-         ( "magGenericWithIssue"
-         , "PlasmaFrequencyMagazine08.epub"
-         )
-
-
-testMagPunchinello1800s :: (Globals, [Formatter]) -> Test
-testMagPunchinello1800s (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Punchinello magazine from the 1800s" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Punchinello, Volume 1, No. 06, May 7, 1870"]
-         }
-      expected =
-         ( "magGenericVolNo2"
-         , "PunchinelloVol01No06.epub"
-         )
-
-
-testMagGenericVolNo3 :: (Globals, [Formatter]) -> Test
-testMagGenericVolNo3 (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "generic magazine with volume and 3-digit number" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Blackwood's Edinburgh Magazine Volume 53, No. 327, January, 1843"]
-         }
-      expected =
-         ( "magGenericVolNo3"
-         , "BlackwoodsEdinburghMagazineVol53No327.epub"
-         )
-
-
-testMagSubjWithIssue :: (Globals, [Formatter]) -> Test
-testMagSubjWithIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "with magazine subject and issue" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing "The Dark Issue 1"]
-         , metaCreators =
-            [ Creator (Just "aut") (Just "Okorafor, Nnedi & Swirsky, Rachel & Slatter, Angela & Hannett, Lisa L.") Nothing
-               "Nnedi Okorafor"
-            , Creator (Just "aut") Nothing Nothing
-               "Rachel Swirsky"
-            ]
-         , metaDates = Map.fromList [(Created, DateValue "2013-10-01")]
-         , metaSubjects =
-            [ "magazine"
-            , "horror"
-            , "dark fantasy"
-            ]
-         }
-      expected =
-         ( "magGenericSubjWithIssue"
-         , "TheDarkMagazine01.epub"
-         )
-
-
-testWomensInstituteLib :: (Globals, [Formatter]) -> Test
-testWomensInstituteLib (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "Women's Institute Library" expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing
-            (Just "Woman's Institute of Domestic Arts and Sciences")
-            Nothing "Woman's Institute of Domestic Arts and Sciences"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Woman's Institute Library of Cookery\nVolume 1: Essentials of Cookery; Cereals; Bread; Hot Breads"]
-         }
-      expected =
-         ( "nonficWomensInstituteLibrary"
-         , "WomansInstituteLibraryOfCookery_Volume1_EssentialsOfCookeryCerealsBreadHotBreads.epub"
-         )
-
-
-testAnthologyDate :: (Globals, [Formatter]) -> Test
-testAnthologyDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "anthology with date in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing "Flash Fiction Online - June 2015"]
-         , metaCreators =
-            [ Creator (Just "art") (Just "Bijelac, Dario") Nothing
-               "Dario Bijelac"
-            , Creator (Just "aut") (Just "Pearlman, Laura") Nothing
-               "Laura Pearlman"
-            ]
-         , metaDates = Map.fromList [(Epub, DateValue "2015-05-30T18:47:28.000000+00:00")]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology_date"
-         , "FlashFictionOnline2015-06.epub"
-         )
-
-
-testAnthology :: (Globals, [Formatter]) -> Test
-testAnthology (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "anthology" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing "Creepy Secrets"]
-         , metaCreators =
-            [ Creator (Just "aut") (Just "Snively, Mortimer") Nothing
-               "Mortimer Snively"
-            , Creator (Just "aut") (Just "Baxter, Joanne") Nothing
-               "Joanne Baxter"
-            ]
-         , metaDates = Map.fromList [(Created, DateValue "2010-11-01")]
-         , metaSubjects = ["fantasy and horror anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "CreepySecrets_2010.epub"
-         )
-
-
-testMagLunaStation :: (Globals, [Formatter]) -> Test
-testMagLunaStation (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Luna Station Quarterly magazine" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Luna Station Quarterly - Issue 028"]
-         }
-      expected =
-         ( "magLunaStationQuarterly"
-         , "LunaStationQuarterly028.epub"
-         )
diff --git a/src/tests/EpubTools/Test/EpubName/PubYear.hs b/src/tests/EpubTools/Test/EpubName/PubYear.hs
deleted file mode 100644
--- a/src/tests/EpubTools/Test/EpubName/PubYear.hs
+++ /dev/null
@@ -1,32 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.Test.EpubName.PubYear
-   ( pubYearTests
-   )
-   where
-
-import Test.HUnit ( Test (..), assertEqual )
-
-import Codec.Epub.Data.Metadata ( DateValue (..) )
-import EpubTools.EpubName.Format.PubYear
-
-
-pubYearTests :: Test
-pubYearTests = TestLabel "PubYear" $ TestList
-   [ testExtractYear "2001-03-25T00:00:00" "2001"
-   , testExtractYear "2007" "2007"
-   , testExtractYear "Sat Nov 03 18:49:50 +0100 2007" "2007"
-   , testExtractYear "2001-03-25 05:00:00+00:00" "2001"
-   , testExtractYear "2009-11-19T07:00:00+00:00" "2009"
-   , testExtractYear "2010-09-21" "2010"
-   , testExtractYear "2010-02-14T21:47:47.682882+00:00" "2010"
-   , testExtractYear "2011-11-16T09:19:50-0500" "2011"
-   , testExtractYear "2011-11-16T14:19:52Z" "2011"
-   , testExtractYear "Tue, 29 Nov 2011 06:21:19 +0000" "2011"
-   ]
-
-
-testExtractYear :: String -> String -> Test
-testExtractYear dateStr expected = TestCase $
-   assertEqual dateStr (extractYear . DateValue $ dateStr) (Just expected)
diff --git a/src/tests/test-epubname.hs b/src/tests/test-epubname.hs
deleted file mode 100644
--- a/src/tests/test-epubname.hs
+++ /dev/null
@@ -1,45 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module Main
-   where
-
-import Control.Monad.Except
-import System.Exit
-import Test.HUnit ( Counts (..), Test (..), runTestTT )
-
-import EpubTools.EpubName.Format.Format
-import EpubTools.EpubName.Main
-import EpubTools.EpubName.Opts
-import EpubTools.Test.EpubName.Format
-import EpubTools.Test.EpubName.PubYear
-
-
-main :: IO ()
-main = do
-   dos <- defaultOptions
-   let testOpts = dos  -- Can use this to adjust the options with dos { ... }
-   ir <- runExceptT $ initialize testOpts
-   either (exitWith) (runTests testOpts) ir
-   
-
-runTests :: Options -> [Formatter] -> IO ()
-runTests opts fs = do
-   counts <- runTestTT $ tests opts fs
-   exit $ testsPassed counts
-
-
-exit :: Bool -> IO ()
-exit True  = exitWith ExitSuccess
-exit False = exitWith $ ExitFailure 1
-
-
-testsPassed :: Counts -> Bool
-testsPassed (Counts _ _ e f) = (e == 0) && (f == 0)
-
-
-tests :: Options -> [Formatter] -> Test
-tests opts fs = TestList
-   [ formatTests opts fs
-   , pubYearTests
-   ]
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,7 +1,8 @@
-resolver: lts-20.12
+resolver: lts-22.6
 
 packages:
 - .
 
 extra-deps:
-- epub-metadata-5.3
+- epub-metadata-5.4
+- optparse-applicative-dex-1.0.1
diff --git a/stack.yaml.lock b/stack.yaml.lock
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -1,19 +1,26 @@
 # This file was autogenerated by Stack.
 # You should not edit this file by hand.
 # For more information, please see the documentation at:
-#   https://docs.haskellstack.org/en/stable/lock_files
+#   https://docs.haskellstack.org/en/stable/topics/lock_files
 
 packages:
 - completed:
-    hackage: epub-metadata-5.3@sha256:32599ce748a1e3ad626ff0e293b66fbaf51afc672d9afe6e3b3b38ed7ec06697,3460
+    hackage: epub-metadata-5.4@sha256:92aa8f8c9dd711724c9e328c48fceb6dc8009af63185ec076b5378030e4d4ad1,3460
     pantry-tree:
-      sha256: a6a4a8a3925578ee833bf2035ca98878c66c7e7bd824571ce98d154828b32de8
+      sha256: 60b6c563c23e493b524f4aed1b86ee20a58b6ba7ba2185cb4c6c47fc1c21d469
       size: 3660
   original:
-    hackage: epub-metadata-5.3
+    hackage: epub-metadata-5.4
+- completed:
+    hackage: optparse-applicative-dex-1.0.1@sha256:de325edef3720c65591cb34eafe313822a7db1249fa027d4487415f2ffbd8653,1251
+    pantry-tree:
+      sha256: cead9b701015d3b308d5c8a1a045bb279e6b7235cb3f4cb458a6eee017e46f65
+      size: 395
+  original:
+    hackage: optparse-applicative-dex-1.0.1
 snapshots:
 - completed:
-    sha256: af5d667f6096e535b9c725a72cffe0f6c060e0568d9f9eeda04caee70d0d9d2d
-    size: 649133
-    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/12.yaml
-  original: lts-20.12
+    sha256: 1b4c2669e26fa828451830ed4725e4d406acc25a1fa24fcc039465dd13d7a575
+    size: 714100
+    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/6.yaml
+  original: lts-22.6
