packages feed

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 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