packages feed

sloane 1.8.2 → 1.9.1

raw patch · 7 files changed

+463/−269 lines, 7 filesdep +cerealdep +containersdep +download-curldep −timedep ~optparse-applicativedep ~text

Dependencies added: cereal, containers, download-curl

Dependencies removed: time

Dependency ranges changed: optparse-applicative, text

Files

− CHANGELOG.md
@@ -1,18 +0,0 @@-# Changelog--## sloane 1.8--Use optparse-applicative instead of cmdargs for command line parsing.-Deprecate '-u', '-?', '-V', '--limit', and '--keys'.--## sloane 1.7.1 - April 30, 2014--* Clean up the README / man page.--## sloane 1.7 - April 20, 2014--* Enable filtering against a local cache of sequences.--## sloane 1.6 - March 18, 2014--* Properly handle multiple search terms.
README.md view
@@ -1,6 +1,6 @@ ----title: SLOANE(1) Sloane User Manual | Version 1.8.2-date: July 3, 2014+title: SLOANE(1) Sloane User Manual | Version 1.9.1+date: 6 Aug 2014 ---  # NAME@@ -10,42 +10,64 @@  # SYNOPSIS -sloane [-a | --all | -k *keys* | --url] [-n *entries*] *search-terms* ...  -sloane (--update | --version | --help)  -sloane [--invert]+sloane [lookup | grep] +  [-a | --all | -k *keys* | --url] [-n *entries*] *terms* ...  +sloane filter [--invert]  +sloane update  +sloane version    # DESCRIPTION -The sloane command searches Sloane's On-Line Encyclopedia of Integer-Sequences (OEIS). The search terms are typically the leading term of a-sequence. For example,+The `sloane lookup` command searches Sloane's On-Line Encyclopedia of+Integer Sequences (OEIS). The search terms are typically the leading+term of a sequence. For example, +    sloane lookup 1,1,2,5,15,52,203,877,4140++returns entry A000110 (Bell numbers), and four more entries. If no+command is given sloane will fall back to the lookup command, so the+above query can more simply be given as+     sloane 1,1,2,5,15,52,203,877,4140 -returns entry A000110 (Bell numbers), and four more entries. One can-also search by sequence id (A-number), or even search for arbitrary-words. See the **EXAMPLES** section.+One can also search by sequence id (A-number), or even search for+arbitrary words. See the **EXAMPLES** section. -If no search terms are specified the standard input is read-line-by-line. In this mode the search is done locally against a-downloaded list of known sequences. If the sequence is in OEIS, then-it is returned to the standard output; if not, it is ignored. This way-sloane can quickly filter out the sequences from the input that are in-OEIS. Assuming that *FILE* contains one sequence per line,+Alternatively, using the `sloane grep` command, the search can be done+locally against a downloaded list of known sequences. This mode works by+"grepping" for the query in the sequence field. -    sloane <FILE+To check a large number of sequences one can use the `sloane filter`+command.  It reads the standard input line-by-line, if the sequence read+is in the local database, then it is returned to the standard output; if+not, it is ignored. This way onw can quickly filter out the sequences+from the input that are in the local database. In other words, assuming+that *FILE* contains one sequence per line, -returns the subset of the sequences in *FILE* that are in OEIS. To-also look-up the names of those sequences in OEIS one could, for+    sloane filter <FILE++returns the subset of the sequences in *FILE* that are in the local+database. To also look-up the names of those sequences one could, for instance, run -    sloane <FILE | xargs -L1 --verbose sloane+    sloane filter <FILE | xargs -L1 --verbose sloane grep  Sloane normally crops long lines to fit the widths of the terminal. If-this is unwanted, pipe the output through cat.+this is unwanted, pipe the output through cat or less: +    sloane lookup -a id:A000110 | less -R+ # OPTIONS +--help+:   Display a short help message++# COMMANDS++## `lookup`++Lookup a sequence, or other search term, in OEIS+ -a, --all :   Print all fields @@ -58,23 +80,33 @@ -n *entries* :   Fetch at most this many entries (default: 5) ++## `grep`++Grep for a sequence in the local database. Same options as for the+`lookup` command apply.++## `filter`++Read sequences from stdin and return those that are in the local+database.+ --invert-:   When used as a filter, return sequences *not* in OEIS+:   Return sequences *not* in the database. ---update-:   Update the local sequence cache+## `update` ---version-:   Print version information+Update the local database. ---help-:   Display a short help message+## `version` +Print version information.+ # EXAMPLES  The most common search is for entries matching a sequence of consecutive terms: -    sloane 1,3,19,183,2371,38703+    sloane lookup 1,3,19,183,2371,38703  At the time of writing this particular query would return @@ -86,21 +118,21 @@ option. For instance, the following search shows the sequence, name, comments, and formula fields of the sequence whose A-number is A006531: -    sloane -k SNCF id:A006531+    sloane lookup -k SNCF id:A006531  The next example returns at most 3 results of a free text search: -    sloane -n 3 "(2+2)-free posets"+    sloane lookup -n 3 "(2+2)-free posets"  To view the full entries of these 3 results in a browser (e.g., Firefox) one can use the url option: -    firefox `sloane --url -n 3 "(2+2)-free posets"`+    firefox `sloane lookup --url -n 3 "(2+2)-free posets"`  In the final example the local cache is used to filter out sequences from the standard input that are in OEIS: -    sloane <<END+    sloane filter <<END     1,2,3,6,11,23,47,106,235           # Comma separated integers     1 2 444 90 120                     # Space separated integers     '(3 9 27 88 123)                   # S-expression
+ Sloane/Config.hs view
@@ -0,0 +1,43 @@+-- |+-- Copyright   : Anders Claesson 2014+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--+module Sloane.Config (Config (..), defaultConfig) where++import System.Console.Terminal.Size (width, size)+import System.FilePath ((</>))+import System.Directory++type URL = String++data Config = Config+    { name      :: String+    , home      :: FilePath+    , sloaneDir :: FilePath+    , sloaneDB  :: FilePath+    , oeisHost  :: URL+    , oeisURL   :: URL+    , sURL      :: URL+    , nURL      :: URL+    , termWidth :: Int+    }++defaultConfig :: IO Config+defaultConfig = do+    w <- maybe maxBound width `fmap` size+    h <- getHomeDirectory+    let dsloane = h </> ".sloane"+    return Config+        { name      = "sloane 1.9.1"+        , home      = h+        , sloaneDir = dsloane+        , sloaneDB  = dsloane </> "sloane.db"+        , oeisHost  = oeisorg+        , oeisURL   = oeisorg ++ "search?fmt=text"+        , sURL      = oeisorg ++ "stripped.gz"+        , nURL      = oeisorg ++ "names.gz"+        , termWidth = w+        }+  where+    oeisorg = "https://oeis.org/"
+ Sloane/DB.hs view
@@ -0,0 +1,150 @@+-- |+-- Copyright   : Anders Claesson 2014+-- Maintainer  : Anders Claesson <anders.claesson@gmail.com>+-- License     : BSD-3+--++module Sloane.DB+    ( DB+    , Reply+    , initDB+    , readDB+    , writeDB+    , putDB+    , null+    , insert+    , lookup+    , grep+    , take+    , aNumbers+    , parseOEISEntries+    ) where++import           Prelude                    hiding (lookup, null, take)+import qualified Prelude                    as P+import           Data.List                  (intersect)+import           Data.ByteString            (ByteString)+import qualified Data.ByteString.Lazy       as BL+import           Data.Map                   (Map, (!))+import qualified Data.Map                   as M+import           Data.Serialize+import           Data.Text                  (Text)+import qualified Data.Text                  as T+import qualified Data.Text.IO               as IO+import           Data.Text.Encoding         (decodeUtf8, encodeUtf8)+import           Control.Monad              (forM_, unless)+import qualified Codec.Compression.GZip     as GZip+import           Network.Curl.Download.Lazy (openLazyURI)+import           System.Console.ANSI+import           Sloane.Config+import           System.Directory++type ANumber = Text+type Key     = Char+type Entry   = Text+type Reply   = Map Key Entry++type DB      = Map ANumber Reply+type DBRaw   = Map ByteString (Map Char ByteString)++encodeDB :: DB -> DBRaw+encodeDB = M.mapKeys encodeUtf8 . M.map (M.map encodeUtf8)++decodeDB :: DBRaw -> DB+decodeDB = M.mapKeys decodeUtf8 . M.map (M.map decodeUtf8)++compress :: ByteString -> BL.ByteString+compress = GZip.compress . BL.fromStrict++compressDB :: DB -> BL.ByteString+compressDB = compress . encode . encodeDB++decompress :: BL.ByteString -> ByteString+decompress = BL.toStrict . GZip.decompress++decompressDB :: BL.ByteString -> Either String DB+decompressDB = fmap decodeDB . decode . decompress++initDB :: Config -> IO ()+initDB cfg = do+    createDirectoryIfMissing False (sloaneDir cfg)+    putStrLn $ "Downloading " ++ sURL cfg+    dbS <- openLazyURI (sURL cfg) >>= either error (return . mkDB 'S')+    putStrLn $ "Downloading " ++ nURL cfg+    dbN <- openLazyURI (nURL cfg) >>= either error (return . mkDB 'N')+    putStrLn "Building database"+    writeDB cfg $ unionDB dbS dbN+    putStrLn "Done."+  where+    unionDB = M.unionWith M.union+    mkDB key = mkMap key . decodeUtf8 . decompress+    mkMap key = M.fromList . map (aNumberAndReply key) . drop 4 . T.lines+    mkReply key = M.singleton key . T.dropWhile (==',') . T.drop 8+    aNumberAndReply key line = (T.take 7 line, mkReply key line)++readDB :: Config -> IO DB+readDB cfg = doesFileExist (sloaneDB cfg) >>= \updated ->+    if updated+        then BL.readFile (sloaneDB cfg) >>= either error return . decompressDB+        else error $ "No local database found. " +++                     "You need to run \"sloane update\" first."++writeDB :: Config -> DB -> IO ()+writeDB cfg = BL.writeFile (sloaneDB cfg) . compressDB++null :: DB -> Bool+null = M.null++insert :: ANumber -> Reply -> DB -> DB+insert = M.insert++lookup :: ANumber -> DB -> Maybe Reply+lookup = M.lookup++grep :: Text -> DB -> DB+grep q = M.filter $ \reply -> q `T.isInfixOf` (reply ! 'S')++take :: Int -> DB -> DB+take n = M.fromList . P.take n . M.toList++aNumbers :: DB -> [ANumber]+aNumbers = M.keys++unions :: [DB] -> DB+unions = M.unionsWith . M.unionWith $ \s t ->+    (s `T.append` T.pack "\n") `T.append` t++singleton :: ANumber -> Key -> Entry -> DB+singleton aNum key entry = M.singleton aNum $ M.singleton key entry++parseOEISEntries :: Text -> DB+parseOEISEntries = unions . map parseLine . trim+  where+    trim = map (T.drop 1) . reverse . drop 2 . reverse . drop 5 . T.lines+    parseLine = parseWords . T.words+    parseWords (key:aNum:rest) = singleton aNum (T.head key) (T.unwords rest)+    parseWords _ = M.empty++putDB :: Config -> [Key] -> DB -> IO ()+putDB cfg keys db = do+    unless (null db) $ putStrLn ""+    forM_ (M.toList db) $ \(aNum, reply) -> do+        forM_ (keys `intersect` M.keys reply) $ \key -> do+            let entry = reply ! key+            forM_ (T.lines entry) $ \line -> do+                setSGR [ SetColor Foreground Dull Green ]+                putStr [key]+                setSGR [ SetColor Foreground Dull Yellow ]+                putStr " " >> IO.putStr aNum+                setSGR []+                putStr " " >> IO.putStrLn (crop key (termWidth cfg - 10) line)+        putStrLn ""++crop :: Key -> Int -> Text -> Text+crop key =+    let cropText f maxLen s = if maxLen < T.length s then f maxLen s else s+    in if key `elem` ['S'..'X']+           then cropText $ \maxLen ->+               T.reverse . T.dropWhile (/= ',') . T.reverse . T.take maxLen+           else cropText $ \maxLen s ->+               T.take (maxLen-2) s `T.append` T.pack ".."
sloane.1 view
@@ -1,68 +1,101 @@-.TH "SLOANE" "1" "July 3, 2014" "Sloane User Manual" "Version 1.8.2"+.TH "SLOANE" "1" "6 Aug 2014" "Sloane User Manual" "Version 1.9.1" .SH NAME .PP sloane \- a command line interface to Sloane\[aq]s On\-Line Encyclopedia of Integer Sequences <http://oeis.org> .SH SYNOPSIS .PP-sloane [\-a | \-\-all | \-k \f[I]keys\f[] | \-\-url] [\-n-\f[I]entries\f[]] \f[I]search\-terms\f[] ...+sloane [lookup | grep] [\-a | \-\-all | \-k \f[I]keys\f[] | \-\-url]+[\-n \f[I]entries\f[]] \f[I]terms\f[] ... .PD 0 .P .PD-sloane (\-\-update | \-\-version | \-\-help)+sloane filter [\-\-invert] .PD 0 .P .PD-sloane [\-\-invert]+sloane update+.PD 0+.P+.PD+sloane version .SH DESCRIPTION .PP-The sloane command searches Sloane\[aq]s On\-Line Encyclopedia of-Integer Sequences (OEIS).+The \f[C]sloane\ lookup\f[] command searches Sloane\[aq]s On\-Line+Encyclopedia of Integer Sequences (OEIS). The search terms are typically the leading term of a sequence. For example, .IP .nf \f[C]-sloane\ 1,1,2,5,15,52,203,877,4140+sloane\ lookup\ 1,1,2,5,15,52,203,877,4140 \f[] .fi .PP returns entry A000110 (Bell numbers), and four more entries.+If no command is given sloane will fall back to the lookup command, so+the above query can more simply be given as+.IP+.nf+\f[C]+sloane\ 1,1,2,5,15,52,203,877,4140+\f[]+.fi+.PP One can also search by sequence id (A\-number), or even search for arbitrary words. See the \f[B]EXAMPLES\f[] section. .PP-If no search terms are specified the standard input is read-line\-by\-line.-In this mode the search is done locally against a downloaded list of-known sequences.-If the sequence is in OEIS, then it is returned to the standard output;-if not, it is ignored.-This way sloane can quickly filter out the sequences from the input that-are in OEIS.-Assuming that \f[I]FILE\f[] contains one sequence per line,+Alternatively, using the \f[C]sloane\ grep\f[] command, the search can+be done locally against a downloaded list of known sequences.+This mode works by "grepping" for the query in the sequence field.+.PP+To check a large number of sequences one can use the+\f[C]sloane\ filter\f[] command.+It reads the standard input line\-by\-line, if the sequence read is in+the local database, then it is returned to the standard output; if not,+it is ignored.+This way onw can quickly filter out the sequences from the input that+are in the local database.+In other words, assuming that \f[I]FILE\f[] contains one sequence per+line, .IP .nf \f[C]-sloane\ <FILE+sloane\ filter\ <FILE \f[] .fi .PP-returns the subset of the sequences in \f[I]FILE\f[] that are in OEIS.-To also look\-up the names of those sequences in OEIS one could, for-instance, run+returns the subset of the sequences in \f[I]FILE\f[] that are in the+local database.+To also look\-up the names of those sequences one could, for instance,+run .IP .nf \f[C]-sloane\ <FILE\ |\ xargs\ \-L1\ \-\-verbose\ sloane+sloane\ filter\ <FILE\ |\ xargs\ \-L1\ \-\-verbose\ sloane\ grep \f[] .fi .PP Sloane normally crops long lines to fit the widths of the terminal.-If this is unwanted, pipe the output through cat.+If this is unwanted, pipe the output through cat or less:+.IP+.nf+\f[C]+sloane\ lookup\ \-a\ id:A000110\ |\ less\ \-R+\f[]+.fi .SH OPTIONS .TP+.B \-\-help+Display a short help message+.RS+.RE+.SH COMMANDS+.SS \f[C]lookup\f[]+.PP+Lookup a sequence, or other search term, in OEIS+.TP .B \-a, \-\-all Print all fields .RS@@ -82,26 +115,25 @@ Fetch at most this many entries (default: 5) .RS .RE+.SS \f[C]grep\f[]+.PP+Grep for a sequence in the local database.+Same options as for the \f[C]lookup\f[] command apply.+.SS \f[C]filter\f[]+.PP+Read sequences from stdin and return those that are in the local+database. .TP .B \-\-invert-When used as a filter, return sequences \f[I]not\f[] in OEIS-.RS-.RE-.TP-.B \-\-update-Update the local sequence cache-.RS-.RE-.TP-.B \-\-version-Print version information-.RS-.RE-.TP-.B \-\-help-Display a short help message+Return sequences \f[I]not\f[] in the database. .RS .RE+.SS \f[C]update\f[]+.PP+Update the local database.+.SS \f[C]version\f[]+.PP+Print version information. .SH EXAMPLES .PP The most common search is for entries matching a sequence of consecutive@@ -109,7 +141,7 @@ .IP .nf \f[C]-sloane\ 1,3,19,183,2371,38703+sloane\ lookup\ 1,3,19,183,2371,38703 \f[] .fi .PP@@ -130,7 +162,7 @@ .IP .nf \f[C]-sloane\ \-k\ SNCF\ id:A006531+sloane\ lookup\ \-k\ SNCF\ id:A006531 \f[] .fi .PP@@ -138,7 +170,7 @@ .IP .nf \f[C]-sloane\ \-n\ 3\ "(2+2)\-free\ posets"+sloane\ lookup\ \-n\ 3\ "(2+2)\-free\ posets" \f[] .fi .PP@@ -147,7 +179,7 @@ .IP .nf \f[C]-firefox\ `sloane\ \-\-url\ \-n\ 3\ "(2+2)\-free\ posets"`+firefox\ `sloane\ lookup\ \-\-url\ \-n\ 3\ "(2+2)\-free\ posets"` \f[] .fi .PP@@ -156,7 +188,7 @@ .IP .nf \f[C]-sloane\ <<END+sloane\ filter\ <<END 1,2,3,6,11,23,47,106,235\ \ \ \ \ \ \ \ \ \ \ #\ Comma\ separated\ integers 1\ 2\ 444\ 90\ 120\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ Space\ separated\ integers \[aq](3\ 9\ 27\ 88\ 123)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ S\-expression
sloane.cabal view
@@ -1,8 +1,10 @@ Name:                sloane-Version:             1.8.2-Synopsis:            A command line interface to Sloane's On-Line Encyclopedia of Integer Sequences-Description:         A command line interface to Sloane's On-Line Encyclopedia of Integer Sequences.-                     For usage see <http://github.com/akc/sloane>.+Version:             1.9.1+Synopsis:            A command line interface to Sloane's On-Line Encyclopedia+                     of Integer Sequences+Description:         A command line interface to Sloane's On-Line Encyclopedia+                     of Integer Sequences. For usage see+                     <http://github.com/akc/sloane>. Homepage:            http://github.com/akc/sloane License:             BSD3 License-file:        LICENSE@@ -10,9 +12,9 @@ Maintainer:          anders.claesson@gmail.com Category:            Math Build-type:          Custom-Extra-Source-Files:  README.md sloane.1 CHANGELOG.md+Extra-Source-Files:  README.md sloane.1 -Cabal-version:       >=1.6+Cabal-version:       >=1.9.2  source-repository head   type:                git@@ -20,16 +22,20 @@  Executable sloane   Main-is:             sloane.hs+  Other-Modules:       Sloane.Config+                       Sloane.DB   ghc-options:         -Wall   Build-depends:       base >=3 && <5,-                       optparse-applicative >=0.9,+                       containers >=0.5,+                       optparse-applicative >=0.8,+                       download-curl >= 0.1.4,                        HTTP >=4000.0.9,+                       cereal >=0.4,                        network >=2.4,                        bytestring >=0.9,-                       text >=1.1,+                       text >=0.11,                        ansi-terminal >=0.6,                        terminal-size >=0.2,                        filepath >=1.3,                        directory >=1.2,-                       zlib >=0.5,-                       time >=1.4+                       zlib >=0.5
sloane.hs view
@@ -3,202 +3,151 @@ -- Maintainer  : Anders Claesson <anders.claesson@gmail.com> -- License     : BSD-3 ---import qualified Codec.Compression.GZip       as GZip-import           Control.Monad                (unless, when, liftM2)-import qualified Data.ByteString              as B-import qualified Data.ByteString.Lazy         as BL import           Data.Text                    (Text) import qualified Data.Text                    as T import qualified Data.Text.IO                 as IO import           Data.Text.Encoding           (decodeUtf8)-import           Data.Maybe                   (fromJust)-import           Data.Time                    (diffUTCTime, getCurrentTime)-import           Network.HTTP-import           Network.URI                  (parseURI)+import           Network.HTTP                 (urlEncodeVars)+import           Network.Curl.Download        (openURI) import           Options.Applicative-import           System.Console.ANSI-import           System.Console.Terminal.Size (Window (..), size)-import           System.Directory-import           System.FilePath              ((</>))-import           System.IO                    (stderr)--type OEISEntries = [String]-type ANumbers = [String]-type Query = String-type Keys = String--data Args = Args-    Bool     -- a: Print all fields?-    String   -- k: Keys of fields to print-    Int      -- n: Fetch at most this many entries-    Bool     -- invert: Return sequences NOT in OEIS-    Bool     -- update: Update the local sequence cache-    Bool     -- url: Print URLs of found entries-    Bool     -- version-    [String] -- search terms--name, oeisHost, oeisURL, cacheDir, cacheFile, cacheURL :: String--name = "sloane 1.8.2"--oeisHost  = "http://oeis.org/"-oeisURL   = oeisHost ++ "search?fmt=text"--cacheDir  = ".sloane"-cacheFile = "stripped.gz"-cacheURL  = oeisHost ++ cacheFile--msgDownloadingCache, msgCacheIsUpToDate, msgNoCache, msgOldCache :: String--msgDownloadingCache = unlines-    [ "Downloading " ++ cacheURL-    , "This may take a minute or two ..."-    ]-msgCacheIsUpToDate = unlines-    [ "The sequence cache is now up-to-date"-    ]-msgNoCache = unlines-    [ "No sequence cache found. You need to run \"sloane --update\""-    ]-msgOldCache = unlines-    [ "The sequence cache is more than 100 days old"-    , "You may want to run \"sloane --update\""-    ]--select :: Keys -> OEISEntries -> OEISEntries-select ks = filter (\line -> null line || head line `elem` ks)--aNumbers :: OEISEntries -> ANumbers-aNumbers es = [ words ids !! 1 | ids@(_:_) <- select "I" es ]--urls :: OEISEntries -> String-urls = unlines . map (oeisHost ++ ) . aNumbers--get :: HStream b => String -> IO b-get uri = simpleHTTP (defaultGETRequest_ uri') >>= getResponseBody-  where-    uri' = fromJust $ parseURI uri--searchOEIS :: Int -> Query -> IO OEISEntries-searchOEIS n s = trim `fmap` get uri-  where-    trim = map (drop 1) . reverse . drop 2 . reverse . drop 5 . lines-    uri = oeisURL ++ "&" ++ urlEncodeVars [("n", show n), ("q", s)]+import           Sloane.Config+import           Sloane.DB                    hiding (null)+import qualified Sloane.DB                    as DB -cropStr :: (Int -> String -> String) -> Int -> String -> String-cropStr f maxLen s = if maxLen < length s then f maxLen s else s+type URL = String+type Seq = Text -cropSeq :: Int -> String -> String-cropSeq = cropStr $ \maxLen ->-              reverse . dropWhile (/= ',') . reverse . take maxLen+data Visibility = Visible | Internal -cropLine :: Int -> String -> String-cropLine = cropStr $ \maxLen s -> take (maxLen-2) s ++ ".."+data Options+    = Cmd Command+    | IntOpts SearchOpts -- Internal opts for fallback to lookup -getWidth :: IO Int-getWidth = maybe maxBound width `fmap` size+data Command+    = Lookup SearchOpts+    | Grep   SearchOpts+    | Filter FilterOpts+    | Update+    | Version -put :: String -> IO ()-put = IO.putStr . T.pack+data SearchOpts = SearchOpts+    { full   :: Bool     -- Print all fields?+    , keys   :: String   -- Keys of fields to print+    , limit  :: Int      -- Fetch at most this many entries+    , url    :: Bool     -- Print URLs of found entries+    , terms  :: [String] -- Search terms+    } -putErr :: String -> IO ()-putErr = IO.hPutStr stderr . T.pack+data FilterOpts = FilterOpts+    { invert :: Bool     -- Return sequences NOT in DB+    } -newline :: IO ()-newline = IO.putStrLn T.empty+oeisKeys :: String+oeisKeys = "ISTUVWXNDHFYAOEeptoKC" -- Valid OEIS keys -putEntries :: Int -> OEISEntries -> IO ()-putEntries ncols = mapM_ $ \line -> do-    case words line of-        []  -> return ()-        [w] -> put w -- Should never be reached-        (key:aNum:rest) -> do-            setSGR [ SetColor Foreground Dull Green ]-            put key-            setSGR [ SetColor Foreground Dull Yellow ]-            put $ ' ' : aNum-            setSGR []-            let crop = if key == "S" then cropSeq else cropLine-            put $ ' ' : crop ncols (unwords rest)-    newline+oeisUrls :: Config -> DB -> [URL]+oeisUrls cfg = map ((oeisHost cfg ++) . T.unpack) . aNumbers -updateCache :: FilePath -> IO ()-updateCache home = do-    createDirectoryIfMissing False dir-    put msgDownloadingCache-    get cacheURL >>= BL.writeFile (dir </> cacheFile)-    put msgCacheIsUpToDate+oeisLookup :: SearchOpts -> Config -> IO DB+oeisLookup opts cfg =+    (parseOEISEntries . decodeUtf8 . either error id) <$>+    openURI (oeisURL cfg ++ "&" ++ urlEncodeVars [("n", show n), ("q", q)])   where-    dir = home </> cacheDir+    n = limit opts+    q = unwords $ terms opts -readCache :: FilePath -> IO Text-readCache home = do-    updated <- doesFileExist fname-    if updated-        then do-            age <- liftM2 diffUTCTime getCurrentTime (getModificationTime fname)-            when (age > 100*day) $ putErr msgOldCache-            (dropPreamble . decompress) `fmap` BL.readFile fname-        else-            error msgNoCache+grepDB :: SearchOpts -> DB -> DB+grepDB opts = DB.take n . DB.grep (T.pack q)   where-    day = 60*60*24-    fname = home </> cacheDir </> cacheFile-    decompress = decodeUtf8 . B.concat . BL.toChunks . GZip.decompress-    dropPreamble = T.unlines . drop 4 . T.lines+    n = limit opts+    q = unwords $ terms opts -seqs :: Text -> [Text]-seqs = filter (not . T.null) . map mkSeq . T.lines+filterDB :: FilterOpts -> DB -> IO [Seq]+filterDB opts db = filter match . parseSeqs <$> IO.getContents   where+    match q = (if invert opts then id else not) (DB.null $ DB.grep q db)+    parseSeqs = filter (not . T.null) . map mkSeq . T.lines     mkSeq = normalize . dropComment     dropComment = T.takeWhile (/= '#')-    normalize   = T.intercalate (T.pack ",") . T.words . clean . T.map tr-    tr c  = if c `elem` ";," then ' ' else c+    normalize = T.intercalate (T.pack ",") . T.words . clean . T.map tr+    tr c = if c `elem` ";," then ' ' else c     clean = T.filter (`elem` " 0123456789-") -filterSeqs :: Bool -> FilePath -> IO ()-filterSeqs invert home = do-    cache <- readCache home-    let f q = (if invert then not else id) (q `T.isInfixOf` cache)-    IO.getContents >>= mapM_ IO.putStrLn . filter f . seqs--args :: Parser Args-args = Args-    <$> switch (short 'a' <> long "all" <> help "Print all fields")+searchOptionsParser :: Visibility -> Parser SearchOpts+searchOptionsParser visibility = hiddenHelp <*> (SearchOpts+    <$> switch+        ( short 'a'+       <> long "all"+       <> help "Print all fields"+       <> f )     <*> strOption         ( short 'k'        <> metavar "KEYS"        <> value "SN"-       <> help "Keys of fields to print [default: SN]" )+       <> help "Keys of fields to print [default: SN]"+       <> f )     <*> option         ( short 'n'        <> metavar "N"        <> value 5-       <> help "Fetch at most this many entries [default: 5]" )+       <> help "Fetch at most this many entries [default: 5]"+       <> f )     <*> switch-        ( long "invert"-       <> help "When used as a filter, return sequences NOT in OEIS" )-    <*> switch (long "update" <> help "Update the local sequence cache")-    <*> switch (long "url" <> help "Print URLs of found entries")-    <*> switch (hidden <> long "version")-    <*> many (argument str (metavar "TERMS..."))+        ( long "url"+       <> help "Print URLs of found entries"+       <> f )+    <*> some (argument str (metavar "TERMS...")))+  where+    f = case visibility of {Visible -> idm; Internal -> internal} -sloane :: Args -> IO ()-sloane (Args _ _    _ _   _    _   True _ ) = put name >> newline-sloane (Args _ _    _ _   True _   _    _ ) = getHomeDirectory >>= updateCache-sloane (Args _ _    _ inv _    _   _    []) = getHomeDirectory >>= filterSeqs inv-sloane (Args a keys n _   _    url _    ts) = do-    ncols <- getWidth-    hits  <- searchOEIS n (unwords ts)-    let pick = if a then id else select keys-    unless (null hits) $ do-        newline-        if url-            then put (urls hits)-            else putEntries (ncols - 10) (pick hits)-        newline+filterOptionsParser :: Parser FilterOpts+filterOptionsParser = FilterOpts+    <$> switch (long "invert" <> help "Return sequences NOT in the database") +commandParser :: Parser Command+commandParser = subparser+    ( command "lookup" (info (Lookup <$> searchOptionsParser Visible)+      ( progDesc "Lookup a sequence, or other search term, in OEIS" ))+   <> command "grep" (info (Grep <$> searchOptionsParser Visible)+      ( progDesc "Grep for a sequence in the local database" ))+   <> command "filter" (info (Filter <$> filterOptionsParser)+      ( progDesc ("Read sequences from stdin and "+               ++ "return those that are in the local database")))+   <> command "update" (info (pure Update)+      ( progDesc "Update the local database" ))+   <> command "version" (info (pure Version)+      ( progDesc "Show version info" ))+    )++optionsParser :: Parser Options+optionsParser =+    (Cmd <$> commandParser) <|> (IntOpts <$> searchOptionsParser Internal)++runSearch :: (SearchOpts -> Config -> IO DB) -> SearchOpts -> Config -> IO ()+runSearch f opts cfg = f opts cfg >>=+    if url opts+        then putStr . unlines . oeisUrls cfg+        else putDB cfg (if full opts then oeisKeys else keys opts)++runCmd :: Command -> Config -> IO ()+runCmd (Lookup opts) = runSearch oeisLookup opts+runCmd (Grep   opts) = runSearch (\o cfg -> grepDB o <$> readDB cfg) opts+runCmd (Filter opts) = \c -> readDB c >>= filterDB opts >>= mapM_ IO.putStrLn+runCmd Update        = initDB+runCmd Version       = putStrLn . name++hiddenHelp :: Parser (a -> a)+hiddenHelp = abortOption ShowHelpText $ hidden <> short 'h' <> long "help"+ main :: IO ()-main = execParser (info (h <*> args) (fullDesc <> header name)) >>= sloane+main = do+    conf <- defaultConfig+    opts <- customExecParser preferences (info parser description)+    case opts of+        (Cmd cmd)   -> runCmd cmd conf+        (IntOpts o) -> runCmd (Lookup o) conf -- Fallback to 'lookup'   where-    h = abortOption ShowHelpText $ hidden <> long "help"+    parser = hiddenHelp <*> optionsParser+    preferences = prefs showHelpOnError+    description = fullDesc <> footer+        "Run 'sloane COMMAND --help' for help on a specific command."