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 +0/−18
- README.md +66/−34
- Sloane/Config.hs +43/−0
- Sloane/DB.hs +150/−0
- sloane.1 +76/−44
- sloane.cabal +16/−10
- sloane.hs +112/−163
− 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."