packages feed

aura-2.0.2: exec/Flags.hs

{-# LANGUAGE OverloadedStrings #-}

module Flags
  ( Program(..), opts
  , PacmanOp( Sync ), SyncOp( SyncUpgrade ), MiscOp
  , AuraOp(..), _AurSync, _AurIgnore, _AurIgnoreGroup
  , AurOp(..), BackupOp(..), CacheOp(..), LogOp(..), OrphanOp(..)
  ) where

import           Aura.Cache (defaultPackageCache)
import           Aura.Pacman (defaultLogFile, pacmanConfFile)
import           Aura.Settings
import           Aura.Types
import           BasePrelude hiding (Version, exp, log, option)
import qualified Data.List.NonEmpty as NEL
import qualified Data.Set as S
import           Data.Set.NonEmpty (NESet)
import qualified Data.Set.NonEmpty as NES
import qualified Data.Text as T
import           Lens.Micro
import           Options.Applicative
import           System.Path (Absolute, Path, fromAbsoluteFilePath, toFilePath)

---

-- | A description of a run of Aura to attempt.
data Program = Program {
  -- ^ Whether Aura handles everything, or the ops and input are just passed down to Pacman.
  _operation   :: Either (PacmanOp, S.Set MiscOp) AuraOp
  -- ^ Settings common to both Aura and Pacman.
  , _commons   :: CommonConfig
  -- ^ Settings specific to building packages.
  , _buildConf :: BuildConfig
  -- ^ The human language of text output.
  , _language  :: Maybe Language } deriving (Show)

-- | Inherited operations that are fed down to Pacman.
data PacmanOp = Database (Either DatabaseOp (NESet PkgName))
              | Files    (S.Set FilesOp)
              | Query    (Either QueryOp (S.Set QueryFilter, S.Set PkgName))
              | Remove   (S.Set RemoveOp) (NESet PkgName)
              | Sync     (Either SyncOp (S.Set PkgName)) (S.Set SyncSwitch)
              | TestDeps (NESet T.Text)
              | Upgrade  (S.Set UpgradeSwitch) (NESet PkgName)
              deriving (Show)

instance Flagable PacmanOp where
  asFlag (Database (Left o))      = "-D" : asFlag o
  asFlag (Database (Right fs))    = "-D" : asFlag fs
  asFlag (Files os)               = "-F" : asFlag os
  asFlag (Query (Left o))         = "-Q" : asFlag o
  asFlag (Query (Right (fs, ps))) = "-Q" : asFlag ps ++ asFlag fs
  asFlag (Remove os ps)           = "-R" : asFlag os ++ asFlag ps
  asFlag (Sync (Left o) ss)       = "-S" : asFlag ss ++ asFlag o
  asFlag (Sync (Right ps) ss)     = "-S" : asFlag ss ++ asFlag ps
  asFlag (TestDeps ps)            = "-T" : asFlag ps
  asFlag (Upgrade s ps)           = "-U" : asFlag s ++ asFlag ps

data DatabaseOp = DBCheck
                | DBAsDeps     (NESet T.Text)
                | DBAsExplicit (NESet T.Text)
                deriving (Show)

instance Flagable DatabaseOp where
  asFlag DBCheck           = ["--check"]
  asFlag (DBAsDeps ps)     = "--asdeps" : asFlag ps
  asFlag (DBAsExplicit ps) = "--asexplicit" : asFlag ps

data FilesOp = FilesList  (NESet T.Text)
             | FilesOwns   T.Text
             | FilesSearch T.Text
             | FilesRegex
             | FilesRefresh
             | FilesMachineReadable
             deriving (Eq, Ord, Show)

instance Flagable FilesOp where
  asFlag (FilesList fs)       = "--list" : asFlag fs
  asFlag (FilesOwns f)        = ["--owns", f]
  asFlag (FilesSearch f)      = ["--search", f]
  asFlag FilesRegex           = ["--regex"]
  asFlag FilesRefresh         = ["--refresh"]
  asFlag FilesMachineReadable = ["--machinereadable"]

data QueryOp = QueryChangelog (NESet T.Text)
             | QueryGroups    (NESet T.Text)
             | QueryInfo      (NESet T.Text)
             | QueryCheck     (NESet T.Text)
             | QueryList      (NESet T.Text)
             | QueryOwns      (NESet T.Text)
             | QueryFile      (NESet T.Text)
             | QuerySearch     T.Text
             deriving (Show)

instance Flagable QueryOp where
  asFlag (QueryChangelog ps) = "--changelog" : asFlag ps
  asFlag (QueryGroups ps)    = "--groups" : asFlag ps
  asFlag (QueryInfo ps)      = "--info" : asFlag ps
  asFlag (QueryCheck ps)     = "--check" : asFlag ps
  asFlag (QueryList ps)      = "--list" : asFlag ps
  asFlag (QueryOwns ps)      = "--owns" : asFlag ps
  asFlag (QueryFile ps)      = "--file" : asFlag ps
  asFlag (QuerySearch t)     = ["--search", t]

data QueryFilter = QueryDeps
                 | QueryExplicit
                 | QueryForeign
                 | QueryNative
                 | QueryUnrequired
                 | QueryUpgrades
                 deriving (Eq, Ord, Show)

instance Flagable QueryFilter where
  asFlag QueryDeps       = ["--deps"]
  asFlag QueryExplicit   = ["--explicit"]
  asFlag QueryForeign    = ["--foreign"]
  asFlag QueryNative     = ["--native"]
  asFlag QueryUnrequired = ["--unrequired"]
  asFlag QueryUpgrades   = ["--upgrades"]

data RemoveOp = RemoveCascade
              | RemoveNoSave
              | RemoveRecursive
              | RemoveUnneeded
              deriving (Eq, Ord, Show)

instance Flagable RemoveOp where
  asFlag RemoveCascade   = ["--cascade"]
  asFlag RemoveNoSave    = ["--nosave"]
  asFlag RemoveRecursive = ["--recursive"]
  asFlag RemoveUnneeded  = ["--unneeded"]

data SyncOp = SyncClean
            | SyncGroups   (NESet T.Text)
            | SyncInfo     (NESet T.Text)
            | SyncList      T.Text
            | SyncSearch    T.Text
            | SyncUpgrade  (S.Set T.Text)
            | SyncDownload (NESet T.Text)
            deriving (Show)

instance Flagable SyncOp where
  asFlag SyncClean         = ["--clean"]
  asFlag (SyncGroups gs)   = "--groups" : asFlag gs
  asFlag (SyncInfo ps)     = "--info" : asFlag ps
  asFlag (SyncList r)      = ["--list", r]
  asFlag (SyncSearch s)    = ["--search", s]
  asFlag (SyncUpgrade ps)  = "--sysupgrade" : asFlag ps
  asFlag (SyncDownload ps) = "--downloadonly" : asFlag ps

data SyncSwitch = SyncRefresh
                | SyncIgnore      (S.Set PkgName)
                | SyncIgnoreGroup (S.Set PkgGroup)
                deriving (Eq, Ord, Show)

instance Flagable SyncSwitch where
  asFlag SyncRefresh          = ["--refresh"]
  asFlag (SyncIgnore ps)      = ["--ignore", T.intercalate "," $ asFlag ps ]
  asFlag (SyncIgnoreGroup gs) = ["--ignoregroup" , T.intercalate "," $ asFlag gs ]

data UpgradeSwitch = UpgradeAsDeps
                   | UpgradeAsExplicit
                   | UpgradeIgnore      (S.Set PkgName)
                   | UpgradeIgnoreGroup (S.Set PkgGroup)
                   deriving (Eq, Ord, Show)

instance Flagable UpgradeSwitch where
  asFlag UpgradeAsDeps           = ["--asdeps"]
  asFlag UpgradeAsExplicit       = ["--asexplicit"]
  asFlag (UpgradeIgnore ps)      = ["--ignore", T.intercalate "," $ asFlag ps ]
  asFlag (UpgradeIgnoreGroup gs) = ["--ignoregroup", T.intercalate "," $ asFlag gs ]

-- | Flags common to several Pacman operations.
data MiscOp = MiscArch    (Path Absolute)
            | MiscAssumeInstalled T.Text
            | MiscColor   T.Text
            | MiscConfirm
            | MiscDBOnly
            | MiscDBPath  (Path Absolute)
            | MiscGpgDir  (Path Absolute)
            | MiscHookDir (Path Absolute)
            | MiscNoDeps
            | MiscNoProgress
            | MiscNoScriptlet
            | MiscPrint
            | MiscPrintFormat T.Text
            | MiscRoot    (Path Absolute)
            | MiscVerbose
            deriving (Eq, Ord, Show)

instance Flagable MiscOp where
  asFlag (MiscArch p)            = ["--arch", T.pack $ toFilePath p]
  asFlag (MiscAssumeInstalled p) = ["--assume-installed", p]
  asFlag (MiscColor c)           = ["--color", c]
  asFlag (MiscDBPath p)          = ["--dbpath", T.pack $ toFilePath p]
  asFlag (MiscGpgDir p)          = ["--gpgdir", T.pack $ toFilePath p]
  asFlag (MiscHookDir p)         = ["--hookdir", T.pack $ toFilePath p]
  asFlag (MiscPrintFormat s)     = ["--print-format", s]
  asFlag (MiscRoot p)            = ["--root", T.pack $ toFilePath p]
  asFlag MiscConfirm             = ["--confirm"]
  asFlag MiscDBOnly              = ["--dbonly"]
  asFlag MiscNoDeps              = ["--nodeps"]
  asFlag MiscNoProgress          = ["--noprogressbar"]
  asFlag MiscNoScriptlet         = ["--noscriptlet"]
  asFlag MiscPrint               = ["--print"]
  asFlag MiscVerbose             = ["--verbose"]

-- | Operations unique to Aura.
data AuraOp = AurSync (Either AurOp (NESet PkgName)) (S.Set AurSwitch)
            | Backup  (Maybe  BackupOp)
            | Cache   (Either CacheOp (NESet PkgName))
            | Log     (Maybe  LogOp)
            | Orphans (Maybe  OrphanOp)
            | Version
            | Languages
            | ViewConf
            deriving (Show)

_AurSync :: Traversal' AuraOp (S.Set AurSwitch)
_AurSync f (AurSync o s) = AurSync o <$> f s
_AurSync _ x             = pure x

data AurOp = AurDeps     (NESet PkgName)
           | AurInfo     (NESet PkgName)
           | AurPkgbuild (NESet PkgName)
           | AurSearch    T.Text
           | AurUpgrade  (S.Set PkgName)
           | AurJson     (NESet PkgName)
           deriving (Show)

data AurSwitch = AurIgnore      (S.Set PkgName)
               | AurIgnoreGroup (S.Set PkgGroup)
               deriving (Eq, Ord, Show)

_AurIgnore :: Traversal' AurSwitch (S.Set PkgName)
_AurIgnore f (AurIgnore s) = AurIgnore <$> f s
_AurIgnore _ x             = pure x

_AurIgnoreGroup :: Traversal' AurSwitch (S.Set PkgGroup)
_AurIgnoreGroup f (AurIgnoreGroup s) = AurIgnoreGroup <$> f s
_AurIgnoreGroup _ x                  = pure x

data BackupOp = BackupClean Word | BackupRestore | BackupList deriving (Show)

data CacheOp = CacheBackup (Path Absolute) | CacheClean Word | CacheCleanNotSaved | CacheSearch T.Text deriving (Show)

data LogOp = LogInfo (NESet PkgName) | LogSearch T.Text deriving (Show)

data OrphanOp = OrphanAbandon | OrphanAdopt (NESet PkgName) deriving (Show)

opts :: ParserInfo Program
opts = info (program <**> helper) (fullDesc <> header "Aura - Package manager for Arch Linux and the AUR.")

program :: Parser Program
program = Program
  <$> (fmap Right aurOps <|> (curry Left <$> pacOps <*> misc))
  <*> commonConfig
  <*> buildConfig
  <*> optional language
  where aurOps = aursync <|> backups <|> cache <|> log <|> orphans <|> version' <|> languages <|> viewconf
        pacOps = database <|> files <|> queries <|> remove <|> sync <|> testdeps <|> upgrades

aursync :: Parser AuraOp
aursync = bigA *>
  (AurSync
   <$> (fmap (Right . NES.fromList . fmap (PkgName . T.toLower) . NES.toList) someArgs <|> fmap Left mods)
   <*> (S.fromList <$> many switches)
  )
  where bigA    = flag' () (long "aursync" <> short 'A' <> help "Install packages from the AUR.")
        mods    = ds <|> ainfo <|> pkgb <|> search <|> upgrade <|> aur
        ds      = AurDeps <$> (flag' () (long "deps" <> short 'd' <> hidden <> help "View dependencies of an AUR package.") *> somePkgs')
        ainfo   = AurInfo <$> (flag' () (long "info" <> short 'i' <> hidden <> help "View AUR package information.") *> somePkgs')
        pkgb    = AurPkgbuild <$> (flag' () (long "pkgbuild" <> short 'p' <> hidden <> help "View an AUR package's PKGBUILD file.") *> somePkgs')
        search  = AurSearch <$> strOption (long "search" <> short 's' <> metavar "STRING" <> hidden <> help "Search the AUR via a search string.")
        upgrade = AurUpgrade <$> (flag' () (long "sysupgrade" <> short 'u' <> hidden <> help "Upgrade all installed AUR packages.") *> fmap (S.map PkgName) manyArgs')
        aur     = AurJson <$> (flag' () (long "json" <> hidden <> help "Retrieve package JSON straight from the AUR.") *> somePkgs')
        switches = ign <|> igg
        ign  = AurIgnore . S.fromList . map PkgName . T.split (== ',') <$>
          strOption (long "ignore" <> metavar "PKG(,PKG,...)" <> hidden <> help "Ignore given packages.")
        igg  = AurIgnoreGroup . S.fromList . map PkgGroup . T.split (== ',') <$>
          strOption (long "ignoregroup" <> metavar "PKG(,PKG,...)" <> hidden <> help "Ignore packages from the given groups.")

backups :: Parser AuraOp
backups = bigB *> (Backup <$> optional mods)
  where bigB = flag' () (long "save" <> short 'B' <> help "Save a package state.")
        mods = clean <|> restore <|> lst
        clean = BackupClean <$> option auto (long "clean" <> short 'c' <> metavar "N" <> hidden <> help "Keep the most recent N states, delete the rest.")
        restore = flag' BackupRestore (long "restore" <> short 'r' <> hidden <> help "Restore a previous package state.")
        lst = flag' BackupList (long "list" <> short 'l' <> hidden <> help "Show all saved package state filenames.")

cache :: Parser AuraOp
cache = bigC *> (Cache <$> (fmap Left mods <|> fmap Right somePkgs))
  where bigC = flag' () (long "downgrade" <> short 'C' <> help "Interact with the package cache.")
        mods = backup <|> clean <|> clean' <|> search
        backup = CacheBackup . fromAbsoluteFilePath <$> strOption (long "backup" <> short 'b' <> metavar "PATH" <> help "Backup the package cache to a given directory." <> hidden)
        clean  = CacheClean  <$> option auto (long "clean" <> short 'c' <> metavar "N" <> help "Save the most recent N versions of a package in the cache, deleting the rest." <> hidden)
        clean' = flag' CacheCleanNotSaved (long "notsaved" <> help "Clean out any cached package files which doesn't appear in any saved state." <> hidden)
        search = CacheSearch <$> strOption (long "search" <> short 's' <> metavar "STRING" <> help "Search the package cache via a search string." <> hidden)

log :: Parser AuraOp
log = bigL *> (Log <$> optional mods)
  where bigL = flag' () (long "viewlog" <> short 'L' <> help "View the Pacman log.")
        mods = inf <|> sch
        inf  = LogInfo <$> (flag' () (long "info" <> short 'i' <> help "Display the installation history for given packages." <> hidden) *> somePkgs')
        sch  = LogSearch <$> strOption (long "search" <> short 's' <> metavar "STRING" <> help "Search the Pacman log via a search string." <> hidden)

orphans :: Parser AuraOp
orphans = bigO *> (Orphans <$> optional mods)
  where bigO    = flag' () (long "orphans" <> short 'O' <> help "Display all orphan packages.")
        mods    = abandon <|> adopt
        abandon = flag' OrphanAbandon (long "abandon" <> short 'j' <> hidden <> help "Uninstall all orphan packages.")
        adopt   = OrphanAdopt <$> (flag' () (long "adopt" <> hidden <> help "Mark some packages' install reason as 'Explicit'.") *> somePkgs')

version' :: Parser AuraOp
version' = flag' Version (long "version" <> short 'V' <> help "Display Aura's version.")

languages :: Parser AuraOp
languages = flag' Languages (long "languages" <> help "Show all human languages available for output.")

viewconf :: Parser AuraOp
viewconf = flag' ViewConf (long "viewconf" <> help "View the Pacman config file.")

buildConfig :: Parser BuildConfig
buildConfig = BuildConfig <$> makepkg <*> bp <*> optional bu <*> trunc <*> buildSwitches
  where makepkg = S.fromList <$> many (ia <|> as <|> si)
        ia      = flag' IgnoreArch (long "ignorearch" <> hidden <> help "Exposed makepkg flag.")
        as      = flag' AllSource (long "allsource" <> hidden <> help "Exposed makepkg flag.")
        si      = flag' SkipInteg (long "skipinteg" <> hidden <> help "Skip all makepkg integrity checks.")
        bp      = fmap fromAbsoluteFilePath (strOption (long "build" <> metavar "PATH" <> hidden <> help "Directory in which to build packages."))
                  <|> pure defaultBuildDir
        bu      = User <$> strOption (long "builduser" <> metavar "USER" <> hidden <> help "User account to build as.")
        trunc   = fmap Head (option auto (long "head" <> metavar "N" <> hidden <> help "Only show top N search results."))
          <|> fmap Tail (option auto (long "tail" <> metavar "N" <> hidden <> help "Only show last N search results."))
          <|> pure None

buildSwitches :: Parser (S.Set BuildSwitch)
buildSwitches = S.fromList <$> many (lv <|> dmd <|> dsm <|> dpb <|> rbd <|> he <|> ucp <|> dr <|> sa <|> fo <|> npc)
  where dmd = flag' DeleteMakeDeps (long "delmakedeps" <> short 'a' <> hidden <> help "Uninstall makedeps after building.")
        dsm = flag' DontSuppressMakepkg (long "unsuppress" <> short 'x' <> hidden <> help "Unsuppress makepkg output.")
        dpb = flag' DiffPkgbuilds (long "diff" <> short 'k' <> hidden <> help "Show PKGBUILD diffs.")
        rbd = flag' RebuildDevel (long "devel" <> hidden <> help "Rebuild all git/hg/svn/darcs-based packages.")
        he  = flag' HotEdit (long "hotedit" <> hidden <> help "Edit a PKGBUILD before building.")
        ucp = flag' UseCustomizepkg (long "custom" <> hidden <> help "Run customizepkg before building.")
        dr  = flag' DryRun (long "dryrun" <> hidden <> help "Run dependency checks and PKGBUILD diffs, but don't build.")
        sa  = flag' SortAlphabetically (long "abc" <> hidden <> help "Sort search results alphabetically.")
        lv  = flag' LowVerbosity (long "quiet" <> short 'q' <> hidden <> help "Display less information.")
        fo  = flag' ForceBuilding (long "force" <> hidden <> help "Always (re)build specified packages.")
        npc = flag' NoPkgbuildCheck (long "noanalysis" <> hidden <> help "Do not analyse PKGBUILDs for security flaws.")

commonConfig :: Parser CommonConfig
commonConfig = CommonConfig <$> cap <*> cop <*> lfp <*> commonSwitches
  where cap = fmap (Right . fromAbsoluteFilePath)
                   (strOption (long "cachedir" <> hidden <> help "Use an alternate package cache location."))
              <|> pure (Left defaultPackageCache)
        cop = fmap (Right . fromAbsoluteFilePath)
                   (strOption (long "config"   <> hidden <> help "Use an alternate Pacman config file."))
              <|> pure (Left pacmanConfFile)
        lfp = fmap (Right . fromAbsoluteFilePath)
                   (strOption (long "logfile"  <> hidden <> help "Use an alternate Pacman log."))
              <|> pure (Left defaultLogFile)

commonSwitches :: Parser (S.Set CommonSwitch)
commonSwitches = S.fromList <$> many (nc <|> no <|> dbg <|> clr)
  where nc  = flag' NoConfirm  (long "noconfirm" <> hidden <> help "Never ask for Aura or Pacman confirmation.")
        no  = flag' NeededOnly (long "needed"    <> hidden <> help "Don't rebuild/reinstall up-to-date packages.")
        dbg = flag' Debug      (long "debug"     <> hidden <> help "Print useful debugging info.")
        clr = Colour . f <$> strOption (long "color" <> metavar "WHEN" <> hidden <> help "Colourize the output.")
        f :: String -> ColourMode
        f "never"  = Never
        f "always" = Always
        f _        = Auto

database :: Parser PacmanOp
database = bigD *> (Database <$> (fmap Right somePkgs <|> fmap Left mods))
  where bigD   = flag' () (long "database" <> short 'D' <> help "Interact with the package database.")
        mods   = check <|> asdeps <|> asexp
        check  = flag' DBCheck (long "check" <> short 'k' <> hidden <> help "Test local database validity.")
        asdeps = DBAsDeps <$> (flag' () (long "asdeps" <> hidden <> help "Mark packages as being dependencies.") *> someArgs')
        asexp  = DBAsExplicit <$> (flag' () (long "asexplicit" <> hidden <> help "Mark packages as being explicitely installed.") *> someArgs')

files :: Parser PacmanOp
files = bigF *> (Files <$> fmap S.fromList (many mods))
  where bigF = flag' () (long "files" <> short 'F' <> help "Interact with the file database.")
        mods = lst <|> own <|> sch <|> rgx <|> rfr <|> mch
        lst  = FilesList <$> (flag' () (long "list" <> short 'l' <> hidden <> help "List the files owned by given packages.") *> someArgs')
        own  = FilesOwns <$> strOption (long "owns" <> short 'o' <> metavar "FILE" <> hidden <> help "Query the package that owns FILE.")
        sch  = FilesSearch <$> strOption (long "search" <> short 's' <> metavar "FILE" <> hidden <> help "Find package files that match the given FILEname.")
        rgx  = flag' FilesRegex (long "regex" <> short 'x' <> hidden <> help "Interpret the input of -Fs as a regex.")
        rfr  = flag' FilesRefresh (long "refresh" <> short 'y' <> hidden <> help "Download fresh package databases.")
        mch  = flag' FilesMachineReadable (long "machinereadable" <> hidden <> help "Produce machine-readable output.")

queries :: Parser PacmanOp
queries = bigQ *> (Query <$> (fmap Right query <|> fmap Left mods))
  where bigQ  = flag' () (long "query" <> short 'Q' <> help "Interact with the local package database.")
        query = curry (second (S.map PkgName)) <$> queryFilters <*> manyArgs
        mods  = chl <|> gps <|> inf <|> lst <|> own <|> fls <|> sch <|> chk
        chl   = QueryChangelog <$> (flag' () (long "changelog" <> short 'c' <> hidden <> help "View a package's changelog.") *> someArgs')
        gps   = QueryGroups <$> (flag' () (long "groups" <> short 'g' <> hidden <> help "View all members of a package group.") *> someArgs')
        inf   = QueryInfo <$> (flag' () (long "info" <> short 'i' <> hidden <> help "View package information.") *> someArgs')
        lst   = QueryList <$> (flag' () (long "list" <> short 'l' <> hidden <> help "List files owned by a package.") *> someArgs')
        chk = QueryCheck <$> (flag' () (long "check" <> short 'k' <> hidden <> help "Check that package files exist.") *> someArgs')
        own   = QueryOwns <$> (flag' () (long "owns" <> short 'o' <> hidden <> help "Find the package some file belongs to.") *> someArgs')
        fls   = QueryFile <$> (flag' () (long "file" <> short 'p' <> hidden <> help "Query a package file.") *> someArgs')
        sch   = QuerySearch <$> strOption (long "search" <> short 's' <> metavar "REGEX" <> hidden <> help "Search the local database.")

queryFilters :: Parser (S.Set QueryFilter)
queryFilters = S.fromList <$> many (dps <|> exp <|> frg <|> ntv <|> urq <|> upg)
  where dps = flag' QueryDeps (long "deps" <> short 'd' <> hidden <> help "[filter] Only list packages installed as deps.")
        exp = flag' QueryExplicit (long "explicit" <> short 'e' <> hidden <> help "[filter] Only list explicitly installed packages.")
        frg = flag' QueryForeign (long "foreign" <> short 'm' <> hidden <> help "[filter] Only list AUR packages.")
        ntv = flag' QueryNative (long "native" <> short 'n' <> hidden <> help "[filter] Only list official packages.")
        urq = flag' QueryUnrequired (long "unrequired" <> short 't' <> hidden <> help "[filter] Only list packages not required as a dependency to any other.")
        upg = flag' QueryUpgrades (long "upgrades" <> short 'u' <> hidden <> help "[filter] Only list outdated packages.")

remove :: Parser PacmanOp
remove = bigR *> (Remove <$> mods <*> somePkgs)
  where bigR     = flag' () (long "remove" <> short 'R' <> help "Uninstall packages.")
        mods     = S.fromList <$> many (cascade <|> nosave <|> recurse <|> unneeded)
        cascade  = flag' RemoveCascade (long "cascade" <> short 'c' <> hidden <> help "Remove packages and all others that depend on them.")
        nosave   = flag' RemoveNoSave (long "nosave" <> short 'n' <> hidden <> help "Remove configuration files as well.")
        recurse  = flag' RemoveRecursive (long "recursive" <> short 's' <> hidden <> help "Remove unneeded dependencies.")
        unneeded = flag' RemoveUnneeded (long "unneeded" <> short 'u' <> hidden <> help "Remove unneeded packages.")

sync :: Parser PacmanOp
sync = bigS *> (Sync <$> (fmap (Right . S.map PkgName) manyArgs <|> fmap Left mods) <*> (S.fromList <$> many (ref <|> ign <|> igg)))
  where bigS = flag' () (long "sync" <> short 'S' <> help "Install official packages.")
        ref  = flag' SyncRefresh (long "refresh" <> short 'y' <> hidden <> help "Update the package database.")
        mods = cln <|> gps <|> inf <|> lst <|> sch <|> upg <|> dnl
        cln  = flag' SyncClean (long "clean" <> short 'c' <> hidden <> help "Remove old packages from the cache.")
        gps  = SyncGroups <$> (flag' () (long "groups" <> short 'g' <> hidden <> help "View members of a package group.") *> someArgs')
        inf  = SyncInfo <$> (flag' () (long "info" <> short 'i' <> hidden <> help "View package information.") *> someArgs')
        lst  = SyncList <$> strOption (long "list" <> short 'l' <> metavar "REPO" <> hidden <> help "List the packages in a REPO.")
        sch  = SyncSearch <$> strOption (long "search" <> short 's' <> metavar "REGEX" <> hidden <> help "Search the official package repos.")
        upg  = SyncUpgrade <$> (flag' () (long "sysupgrade" <> short 'u' <> hidden <> help "Upgrade installed packages.") *> manyArgs')
        dnl  = SyncDownload <$> (flag' () (long "downloadonly" <> short 'w' <> hidden <> help "Download package tarballs.") *> someArgs')
        ign  = SyncIgnore . S.fromList . map PkgName . T.split (== ',') <$>
          strOption (long "ignore" <> metavar "PKG(,PKG,...)" <> hidden <> help "Ignore given packages.")
        igg  = SyncIgnoreGroup . S.fromList . map PkgGroup . T.split (== ',') <$>
          strOption (long "ignoregroup" <> metavar "PKG(,PKG,...)" <> hidden <> help "Ignore packages from the given groups.")

misc :: Parser (S.Set MiscOp)
misc = S.fromList <$> many (ar <|> dbp <|> roo <|> ver <|> gpg <|> hd <|> con <|> dbo <|> nop <|> nos <|> pf <|> nod <|> prt <|> asi)
  where ar  = MiscArch . fromAbsoluteFilePath
              <$> strOption (long "arch" <> metavar "ARCH" <> hidden <> help "Use an alternate architecture.")
        dbp = MiscDBPath . fromAbsoluteFilePath
              <$> strOption (long "dbpath" <> short 'b' <> metavar "PATH" <> hidden <> help "Use an alternate database location.")
        roo = MiscRoot . fromAbsoluteFilePath
              <$> strOption (long "root" <> short 'r' <> metavar "PATH" <> hidden <> help "Use an alternate installation root.")
        ver = flag' MiscVerbose (long "verbose" <> short 'v' <> hidden <> help "Be more verbose.")
        gpg = MiscGpgDir . fromAbsoluteFilePath
              <$> strOption (long "gpgdir" <> metavar "PATH" <> hidden <> help "Use an alternate GnuGPG directory.")
        hd  = MiscHookDir . fromAbsoluteFilePath
              <$> strOption (long "hookdir" <> metavar "PATH" <> hidden <> help "Use an alternate hook directory.")
        con = flag' MiscConfirm (long "confirm" <> hidden <> help "Always ask for confirmation.")
        dbo = flag' MiscDBOnly (long "dbonly" <> hidden <> help "Only modify database entries, not package files.")
        nop = flag' MiscNoProgress (long "noprogressbar" <> hidden <> help "Don't show a progress bar when downloading.")
        nos = flag' MiscNoScriptlet (long "noscriptlet" <> hidden <> help "Don't run available install scriptlets.")
        pf  = MiscPrintFormat <$> strOption (long "print-format" <> metavar "STRING" <> hidden <> help "Specify how targets should be printed.")
        nod = flag' MiscNoDeps (long "nodeps" <> short 'd' <> hidden <> help "Skip dependency version checks.")
        prt = flag' MiscPrint (long "print" <> short 'p' <> hidden <> help "Print the targets instead of performing the operation.")
        asi = MiscAssumeInstalled <$> strOption (long "assume-installed" <> metavar "<package=version>" <> hidden <> help "Add a virtual package to satisfy dependencies.")

testdeps :: Parser PacmanOp
testdeps = bigT *> (TestDeps <$> someArgs)
  where bigT = flag' () (long "deptest" <> short 'T' <> help "Test dependencies - useful for scripts.")

upgrades :: Parser PacmanOp
upgrades = bigU *> (Upgrade <$> (S.fromList <$> many mods) <*> somePkgs)
  where bigU = flag' () (long "upgrade" <> short 'U' <> help "Install given package files.")
        mods = asd <|> ase <|> ign <|> igg
        asd = flag' UpgradeAsDeps (long "asdeps" <> hidden)
        ase = flag' UpgradeAsExplicit (long "asexplicit" <> hidden)
        ign  = UpgradeIgnore . S.fromList . map PkgName . T.split (== ',') <$>
          strOption (long "ignore" <> metavar "PKG(,PKG,...)" <> hidden <> help "Ignore given packages.")
        igg  = UpgradeIgnoreGroup . S.fromList . map PkgGroup . T.split (== ',') <$>
          strOption (long "ignoregroup" <> metavar "PKG(,PKG,...)" <> hidden <> help "Ignore packages from the given groups.")

somePkgs :: Parser (NESet PkgName)
somePkgs = NES.fromList . fromJust . NEL.nonEmpty . map PkgName <$> some (argument str (metavar "PACKAGES"))

-- | Same as `someArgs`, but the help message "brief display" won't show PACKAGES.
somePkgs' :: Parser (NESet PkgName)
somePkgs' = NES.fromList . fromJust . NEL.nonEmpty . map PkgName <$> some (argument str (metavar "PACKAGES" <> hidden))

-- | One or more arguments.
someArgs :: Parser (NESet T.Text)
someArgs = NES.fromList . fromJust . NEL.nonEmpty <$> some (argument str (metavar "PACKAGES"))

-- | Same as `someArgs`, but the help message "brief display" won't show PACKAGES.
someArgs' :: Parser (NESet T.Text)
someArgs' = NES.fromList . fromJust . NEL.nonEmpty <$> some (argument str (metavar "PACKAGES" <> hidden))

-- | Zero or more arguments.
manyArgs :: Parser (S.Set T.Text)
manyArgs = S.fromList <$> many (argument str (metavar "PACKAGES"))

-- | Zero or more arguments.
manyArgs' :: Parser (S.Set T.Text)
manyArgs' = S.fromList <$> many (argument str (metavar "PACKAGES" <> hidden))

language :: Parser Language
language = foldr1 (<|>) $ map (\(f, v) -> flag' v (long f <> hidden)) langs
  where langs = [ ( "japanese",   Japanese ),   ( "日本語",     Japanese )
                , ( "polish",     Polish ),     ( "polski",    Polish )
                , ( "croatian",   Croatian ),   ( "hrvatski",  Croatian )
                , ( "swedish",    Swedish ),    ( "svenska",   Swedish )
                , ( "german",     German ),     ( "deutsch",   German )
                , ( "spanish",    Spanish ),    ( "español",   Spanish )
                , ( "portuguese", Portuguese ), ( "português", Portuguese )
                , ( "french",     French),      ( "français",  French )
                , ( "russian",    Russian ),    ( "русский",   Russian )
                , ( "italian",    Italian ),    ( "italiano",  Italian )
                , ( "serbian",    Serbian ),    ( "српски",    Serbian )
                , ( "norwegian",  Norwegian ),  ( "norsk",     Norwegian )
                , ( "indonesian", Indonesia )
                , ( "chinese",    Chinese ),    ( "中文",       Chinese )
                , ( "esperanto",  Esperanto ) ]