feed-cli (empty) → 2008.5.3
raw patch · 9 files changed
+428/−0 lines, 9 filesdep +basedep +directorydep +feedsetup-changed
Dependencies added: base, directory, feed, old-locale, old-time, time, xml
Files
- FeedCLI.hs +180/−0
- LICENSE +31/−0
- Options.hs +105/−0
- README +40/−0
- Setup.hs +4/−0
- examples/getURL.sh +17/−0
- examples/piper.sh +20/−0
- examples/piper2.sh +6/−0
- feed-cli.cabal +25/−0
+ FeedCLI.hs view
@@ -0,0 +1,180 @@+{-++TODO / ideas:+- try using it for something :)+- --watch mode that runs feed-cli at a given interval+- attachments+- when adding an item to a non-existant feed, create a new feed file?+- use -c"command --arg" instead of --pipe-mode so we can get more information about a command being run?+- delete oldest feeds instead of front feeds+- implement very simple version? ./feed-cli title "this is description" or so?+- clean up parameter checks, -D others+- --format=atom, rss +- test it against feed verifyer - http://feedvalidator.org/++DONE:+- --pipe-mode = stdin becomes description, subject is command run, time, return value? echo items to screen? or maybe use -c"command" for more control?+- clean up num parser for better error+- input & output from files+- description input from file+- --update file instead of -i and -o - if -i and -o are the same, or if they use -u then create a temp file and update.+--limit=10 delete the oldest feeds so that only 10 remain - could stand alone as a command?+-}+module Main where++import Text.RSS.Syntax (RSS (..), RSSChannel (..), RSSItem (..), nullItem, nullRSS)+import Text.RSS.Export (xmlRSS)+import Text.RSS.Import (elementToRSS)++import Text.XML.Light.Output (showTopElement)+import Text.XML.Light.Input (parseXMLDoc)++import System.Environment(getArgs)+import System.Locale (defaultTimeLocale)+import System.IO (openTempFile, openFile, hGetContents+ , hClose, hPutStr, IOMode(..), stdin)+import System.Directory (renameFile)+import System.Directory(doesFileExist)++import Data.Maybe(fromJust, fromMaybe)+import Control.Monad(when)+import Data.Time.Clock (getCurrentTime, UTCTime)+import Data.Time.Format (formatTime)++import Options (Options(..), LimitType (..), DescType (..), parseOpts)++------------------------------------------------------------+-- * Helpers+------------------------------------------------------------++inetFormat :: UTCTime -> String+inetFormat t = formatTime defaultTimeLocale "%a, %d %b %Y %T GMT" t++notImplemented :: String -> a+notImplemented a = error $ a ++ " not implemented."++-- * Transformers+addFeedItem :: RSS -> RSSItem -> RSS+addFeedItem feed item+ = let oldChannel = rssChannel feed+ oldItems = rssItems oldChannel in+ feed{rssChannel=oldChannel{rssItems=item:oldItems}}++------------------------------------------------------------+-- * IO+------------------------------------------------------------++writeRSS :: Maybe FilePath -- If they haven't provided an argument, stdout.+ -> RSS+ -> Bool -- ^create a temporary file?+ -> IO ()+writeRSS (Just outFile) rss createTemp = do+ (fileToWrite, h) <- if createTemp+ then openTempFile "/tmp" ("rssGenTemp.xml") -- fix: portable?+ else do h' <- openFile outFile WriteMode+ return (outFile, h')+ hPutStr h (showTopElement $ xmlRSS rss)+ hClose h+ when createTemp (renameFile fileToWrite outFile)++writeRSS Nothing rss _ = putStrLn $ showTopElement $ xmlRSS rss++-- Throw some sane exceptions open file fail, etc. Creates basic empty feed if it doesn't exist+readRSS :: FilePath -> IO RSS+readRSS inFile = do+ exists <- doesFileExist inFile+ when (not exists) (+ (error ("input file doesn't exist: " ++ inFile+ ++ "\n(hint, use ./feed-cli new-feed)" ))+ )+ xmlStr <- readFile inFile+ let mXmlDoc = parseXMLDoc xmlStr+ return $ fromJust $ elementToRSS (fromJust mXmlDoc)+++------------------------------------------------------------+-- * Handling Commands & Main+------------------------------------------------------------++handleCommands :: Options+ -> [String]+ -> IO ()+handleCommands opts command = do+ case command of+ ["new-feed"] -> doNewFeed opts+ ["new-item"] -> doNewItem opts+ c -> error ("unknown command: " ++ (show c))++------------------------------------------------------------++-- |Take the given number of elements from the front. Don't compare time yet.+limitItems :: RSS -- ^ the feed to filter+ -> Integer -- ^Max items+ -> RSS -- ^the feed with max items+limitItems rss limit+ = let oldChannel = rssChannel rss+ oldItems = rssItems oldChannel+ newItems = take (fromIntegral limit) oldItems+ in rss{rssChannel=oldChannel{rssItems=newItems}}++doNewItem :: Options -> IO ()+doNewItem opts = do+ let mOutFile = optOutput opts+ let feedTitle = optTitle opts++ let mLink = optLink opts+ -- fix: These will become more flexible when we have more commands we can do:+ let inFile = fromMaybe (error "infile required") (optInput opts)+ let createTemp = Just inFile == mOutFile+ let mLimit = optLimit opts++ feedDescription <- getDescription (optDescription opts) (optPreDesc opts)++ readFeed <- readRSS inFile+ time <- getCurrentTime+ let emptyItem = nullItem feedTitle+ let newItem=emptyItem{rssItemPubDate=Just (inetFormat time)+ ,rssItemDescription = Just feedDescription+ ,rssItemLink = mLink+ }+ let feedWithItem = addFeedItem readFeed newItem+ let newFeed+ = case mLimit of+ NoLimit -> addFeedItem readFeed newItem+ KeepLimit -> limitItems feedWithItem+ (fromIntegral $ length $ rssItems $ rssChannel readFeed)+ NumLimit lim -> limitItems feedWithItem lim+ writeRSS mOutFile newFeed createTemp+++getDescription :: DescType+ -> Bool -- add <pre>description</pre> tags?+ -> IO String+getDescription oDescription pre = do+ desc <- case oDescription of+ DescCLI s -> return s+ DescFile d -> readFile d+ DescSTDIN -> hGetContents stdin+ return $ if pre then ("<pre>" ++ desc ++ "</pre>") else desc++++doNewFeed :: Options -> IO ()+doNewFeed opts = do+ let mOutFile = optOutput opts+ let title = optTitle opts+ desc <- getDescription (optDescription opts) (optPreDesc opts)+ let mLink = optLink opts+ let link = fromMaybe (error "--link required") mLink++ let newRSS = nullRSS title link+ let newChannel = (rssChannel newRSS){rssDescription = desc}++ writeRSS mOutFile newRSS{rssChannel=newChannel} False++------------------------------------------------------------++main :: IO ()+main = do args <- getArgs+ (opts, command) <- parseOpts args+ handleCommands opts command
+ LICENSE view
@@ -0,0 +1,31 @@+(c) 2008 Isaac Potoczny-Jones++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++1. Redistributions of source code must retain the above copyright+notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+notice, this list of conditions and the following disclaimer in the+documentation and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of his contributors+may be used to endorse or promote products derived from this software+without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.+~
+ Options.hs view
@@ -0,0 +1,105 @@+module Options where++import System.Console.GetOpt+import Data.Maybe(fromMaybe)++data LimitType = NumLimit Integer -- limit feed size to given number+ | NoLimit -- don't limit+ | KeepLimit -- limit feed size to current size (during add-item)+ deriving Show++data DescType = DescFile FilePath -- means -Dfoo+ | DescCLI String -- means -dFoo+ | DescSTDIN -- means --pipe-mode+ deriving Show++data Options = Options+ { optVerbose :: Bool+ , optShowVersion :: Bool+ , optPreDesc :: Bool+ , optOutput :: Maybe FilePath+ , optInput :: Maybe FilePath+ , optDescription :: DescType+ , optLimit :: LimitType+ , optTitle :: String+ , optLink :: Maybe String+ } deriving Show++defaultOptions :: Options+defaultOptions = Options+ { optVerbose = False+ , optShowVersion = False+ , optPreDesc = False+ , optOutput = Nothing+ , optInput = Nothing+ , optLimit = NoLimit+ , optTitle = ""+ , optDescription = DescCLI ""+ , optLink = Nothing+ }++options :: [OptDescr (Options -> Options)]+options =+ [ Option ['v'] ["verbose"]+ (NoArg (\ opts -> opts { optVerbose = True }))+ "chatty output on stderr"+ , Option ['V','?'] ["version"]+ (NoArg (\ opts -> opts { optShowVersion = True }))+ "show version number"++ , Option ['p'] ["pre"]+ (NoArg (\ opts -> opts { optPreDesc = True }))+ "add <pre> tag to description for html readers"++ , Option [] ["pipe-mode"]+ (NoArg (\ opts -> opts { optDescription = DescSTDIN }))+ "description pipe mode, suitable for piping commands into, or -d or -D"++ , Option ['D'] ["description-file"]+ (ReqArg ((\ f opts -> opts { optDescription = DescFile f })) "FILE")+ "read the description from given file. or -d or --pipe-mode"++ , Option ['d'] ["description"]+ (ReqArg (\ t opts -> opts { optDescription = DescCLI t }) "description")+ "feed item description from command line. or -D or --pipe-mode"++ , Option ['o'] ["output"]+ (ReqArg ((\ f opts -> opts { optOutput = Just f })) "FILE")+ "output to this file, or -u"++ , Option ['u'] ["update-file"]+ (ReqArg ((\ f opts -> opts { optInput = Just f+ , optOutput = Just f })) "FILE")+ "update-file FILE"+ , Option [] ["limit"]+ (OptArg ((\ m opts -> opts { optLimit = + case m of+ Nothing -> KeepLimit+ Just l -> NumLimit (fromMaybe (error ("--limit= expecting number, got " ++ show l)) (readInteger l))+ }))+ "NUM")+ "limit=NUM"+ , Option ['i'] ["input"]+ (ReqArg ((\ f opts -> opts { optInput = Just f })) "FILE")+ "input FILE"+ , Option ['l'] ["link"]+ (ReqArg ((\ f opts -> opts { optLink = Just f })) "URL")+ "link http://foo.com"+ , Option ['t'] ["title"]+ (ReqArg (\ t opts -> opts { optTitle = t }) "title")+ "feed item title"++ ]++readInteger :: String -> Maybe Integer+readInteger s =+ case reads s of+ [(n, "")] -> Just n+ _ -> Nothing++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 ++ usageInfo header options))+ where header = "Usage: ic [OPTION...] files..."
+ README view
@@ -0,0 +1,40 @@+See my announcement:+http://www.syntaxpolice.org/index.php/?q=node/425++Synopsis:++This program generates [RSS 2.0 http:\/\/www.rssboard.org\/rss-2-0-1-rv-6] +feeds based on command line arguments. Use it to create and update feeds+from shell scripts, build scripts, cron jobs, CGIs, or other programs instead+of using an RSS or Atom library.++Commands and args:++new-item: -iFILE (required, or -u): The feed to add this item to+ -oFILE (optional): otherwise, stdout+ -tTitle (required)+ -dDescription (required, or -D, or --pipe-mode)+ --pipe-mode: read description from STDIN+ -lhttp://example.com (optional): Item link+ -DFILE: read description from a file+ -uFILE (optional): Update this file, not valid with -i or -o. Same as -iFILE == -oFILE+ --limit (optional): Keep the number of items fixed. Drop the last item when adding a new item. Doesn't compare dates yet. Taken from the front since that's how we add them.+ --limit=NUM (optional): keep only the last NUM items.++new-feed: -oFILE (optional): otherwise, stdout+ -tTitle (required): feed title+ -dDescription (required): feed description+ -lhttp://example.com (required): Feed link++EXAMPLES:++See also the patterns directory.++# create an empty feed:+./feed-cli new-feed -tTitleOfFeed -d"Feed Description" -o/tmp/feed.xml -lhttp://www.syntaxpolice.org++# add an item to that feed +./feed-cli new-item -t"entry of the day" -d"This is a description of <b>this feed item</b>." -u/tmp/feed.xml -lhttp://www.syntaxpolice.org++# pipe a command into a feed item+ls -l | ./feed-cli new-item --pipe-mode --pre -t"another entry of the day" -u/tmp/feed.xml -lhttp://www.syntaxpolice.org
+ Setup.hs view
@@ -0,0 +1,4 @@+#!/usr/bin/env runghc++import Distribution.Simple+main = defaultMain
+ examples/getURL.sh view
@@ -0,0 +1,17 @@+#!/bin/sh++# use like ./getURL http://www.galois.com+# This pattern fetches a reguar web page using wget, and generates a+# feed entry for it.++FEEDFILE=/tmp/pageFeed.xml++# If the feed doesn't already exist, generate it with something like this:+# ./feed-cli new-feed -t"Page Updates" -o$FEEDFILE -lhttp://www.example.com++echo $1+wget $1 -O outfile.html++./dist/build/feed-cli/feed-cli new-item -u$FEEDFILE -t"[$1] @ `date`" -Doutfile.html -l$1++rm outfile.html
+ examples/piper.sh view
@@ -0,0 +1,20 @@+#!/bin/sh++CMD="head /etc/dictionaries-common/words"+#CMD="./setup build"++echo "<li>Command: $CMD" > cmdOut+echo "<li>Executed by $USER @ `date`" >> cmdOut+echo "<li>Command completed with return code: $?" >> cmdOut+echo "<li><b>Command output follows:</b>" >> cmdOut+++echo '<pre>' >> cmdOut+$CMD>>cmdOut+echo '</pre>' >> cmdOut++./dist/build/feed-cli/feed-cli new-item -u/tmp/piper.xml -t"[$?] > $CMD @ `date`" -DcmdOut++rm cmdOut++#./dist/build/feed-cli/feed-cli new-feed -o/tmp/piper.xml -t"Output from your commands" -lhttp://www.syntaxpolice.org
+ examples/piper2.sh view
@@ -0,0 +1,6 @@+#!/bin/sh++CMD="head /etc/dictionaries-common/words"+#CMD="./setup build"++$CMD | ./dist/build/feed-cli/feed-cli new-item -u/tmp/piper.xml -t"[$?] > $CMD @ `date`" --pipe-mode --pre
+ feed-cli.cabal view
@@ -0,0 +1,25 @@+Name: feed-cli+Version: 2008.5.3+Author: Isaac Potoczny-Jones <ijones@syntaxpolice.org>+Maintainer: Isaac Potoczny-Jones <ijones@syntaxpolice.org>+Copyright: (c) Isaac Potoczny-Jones, 2008+Category: Web, Text+HomePage: http://www.syntaxpolice.org/darcs_repos/feed-cli+License: BSD3+License-File: LICENSE+build-depends: feed, xml, base, time, old-locale, old-time, directory+data-files: README, examples/getURL.sh, examples/piper2.sh, examples/piper.sh+build-type: Simple+Synopsis: A simple command line interface for creating and updating feeds like RSS+Description:+ This program generates RSS 2.0 (http://www.rssboard.org/rss-2-0-1-rv-6)+ feeds based on command line arguments. Use it to easily create and update+ feeds from shell scripts, build scripts, cron jobs, CGIs, or other programs+ instead of using an RSS or Atom library.+ .+ eg: feed-cli new-item -t"entry of the day" -d"This is a description..." -u/tmp/feed.xml -lhttp://www.syntaxpolice.org++Executable: feed-cli+main-is: FeedCLI.hs+other-modules: Options+ghc-options: -Wall