sr-extra (empty) → 1.46.3
raw patch · 25 files changed
+1607/−0 lines, 25 filesdep +HUnitdep +QuickCheckdep +Unixutilssetup-changed
Dependencies added: HUnit, QuickCheck, Unixutils, base, bytestring, bzlib, containers, directory, filepath, mtl, network, network-uri, old-locale, old-time, pretty, process, pureMD5, random, regex-compat, time, unix, zlib
Files
- COPYING +36/−0
- Extra/Bool.hs +7/−0
- Extra/CIO.hs +308/−0
- Extra/Either.hs +20/−0
- Extra/Exit.hs +15/−0
- Extra/Files.hs +253/−0
- Extra/GPGSign.hs +72/−0
- Extra/HughesPJ.hs +11/−0
- Extra/IO.hs +20/−0
- Extra/IOThread.hs +41/−0
- Extra/List.hs +95/−0
- Extra/Lock.hs +63/−0
- Extra/Misc.hs +155/−0
- Extra/Net.hs +20/−0
- Extra/SSH.hs +114/−0
- Extra/Terminal.hs +25/−0
- Extra/Time.hs +65/−0
- Extra/URI.hs +91/−0
- Extra/URIQuery.hs +39/−0
- Setup.hs +5/−0
- Test/QUnit.hs +66/−0
- Test/QuickCheck/Properties.hs +9/−0
- cbits/gwinsz.c +9/−0
- cbits/gwinsz.h +1/−0
- sr-extra.cabal +67/−0
+ COPYING view
@@ -0,0 +1,36 @@+This package was debianized by David Fox+<ddssff@gmail.com> on September 18, 2007.++Copyright (c) 2007, David Fox+Copyright (c) Jeremy Shaw 2007+Copyright (c) Stefan O'Rear 2006++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * 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.++ * The names of contributors may not be used to endorse or promote+ products derived from this software without specific prior+ written permission. ++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 COPYRIGHT+OWNER 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.
+ Extra/Bool.hs view
@@ -0,0 +1,7 @@+module Extra.Bool+ ( cond+ ) where++cond :: a -> a -> Bool -> a+cond t _ True = t+cond _ f False = f
+ Extra/CIO.hs view
@@ -0,0 +1,308 @@+-- |CIO is a type class for the TIO monad, which tracks the cursor+-- position of the console so that indentation and prefixes can be+-- added to the output. TIO also has a style component which lets you+-- control the output verbosity and the appearance of the prefix.+-- There is an instance for the regular IO monad which doesn't use any+-- of these features, to allow functions which do not use the TIO+-- monad call functions in the Debian library.+--+-- NOTE: a copy of this library is in the Extra library as+-- well. Please update both locations.+-- +-- This code is provided for backwards compatibility, I don't+-- endorse its use in new projects.+module Extra.CIO {-# DEPRECATED "Use System.Unix.QIO in Unixutils." #-}+ ( -- * The CIO class+ CIO(..)++ -- * Style constructors and transformers+ , TStyle(..)+ , defStyle+ , withStyle++ , setVerbosity+ , addVerbosity+ , setPrefix+ , addPrefix+ , appPrefix+ , setPrefixes+ , addPrefixes+ , appPrefixes+ , hGetPrefix++ -- * Output functions+ , putStr+ , ePutStr+ , vPutStr+ , vEPutStr++ , hPutChar+ , putChar+ , ePutChar+ , vHPutChar+ , vPutChar+ , vEPutChar++ , hPutStrBl+ , putStrBl+ , ePutStrBl+ , vHPutStrBl+ , vPutStrBl+ , vEPutStrBl++ , hPutStrLn+ , putStrLn+ , ePutStrLn+ , vHPutStrLn+ , vPutStrLn+ , vEPutStrLn++ , bol+ , eBOL+ , vHBOL+ , vBOL+ , vEBOL++ , hColor+ , blue+ , green+ , red+ , magenta+ ) where++import Prelude hiding (putStr, putChar, putStrLn)+import qualified System.IO as IO+import Control.Monad.Trans+import Control.Exception++-- |Class representing ways of doing console (terminal?) output.+class MonadIO m => CIO m where+ -- |Write output to a handle.+ hPutStr :: IO.Handle -> String -> m ()+ -- |If we are not already at the beginning of a line, move the cursor+ -- to the beginning of the next one.+ hBOL :: IO.Handle -> m ()+ -- |Return the \"effective verbosity\" for a task. If the argument+ -- is 2 it means the caller is computing ev for a task that+ -- normally does output when the verbosity level is 2 or higher.+ -- If the verbosity of the current style is 1, then the ev or+ -- effective verbosity is 2-1 = -1, so the output should be+ -- quieter.+ ev :: Int -> m Int+ -- |Modify the current output style.+ setStyle :: (TStyle -> TStyle) -> m a -> m a+ -- |Implementation of try for this monad+ tryCIO :: m a -> m (Either SomeException a)++-- |A record used to hold the output style information for a task.+-- This The prefixes that will appear at the beginning of each line,+-- and the desired verbosity level. Suggested verbosity level policy:+--+-- * -1: No output of any kind, as if you were directing all output to /dev/null+-- +-- * 0: Error output only, suitable for a run whose log you might examine later+-- +-- * 1: casual progress reporting - if you were running on a console but didn't+-- expect anything to go wrong+--+-- * 2: detailed progress reporting - show more progress, particularly things+-- that might fail during the normal operation of the autobuilder: patches+-- that fail to apply, dpkg-buildpackage runs that return errors, etc.+--+-- * 3: Debugging output - use this level or higher if you suspect the+-- autobuilder itself is failing, or you are doing development work on+-- the autobuilder.+data TStyle+ = TStyle { prefix :: String -- ^ Add this string at the beginning of each line+ , verbosity :: Int -- ^ Ignore v functions whose argument is more than this+ , hPrefix :: [(IO.Handle, String)] -- ^ Per-handle prefix+ } deriving Show++defStyle :: TStyle+defStyle = TStyle { prefix = ": "+ , hPrefix = []+ , verbosity = 0+ }++-- |Use a new style for the TIO action+withStyle :: CIO m => TStyle -> m a -> m a+withStyle newStyle = setStyle (const newStyle)++setVerbosity :: Int -> TStyle -> TStyle+setVerbosity n style = style {verbosity = n}+addVerbosity :: Int -> TStyle -> TStyle+addVerbosity n style = setVerbosity (n + verbosity style) style++-- |Set the output style for a handle to prefixed.+setPrefix :: String -> TStyle -> TStyle+setPrefix prefix style = style {prefix = prefix}++-- | Prepend some text to the prefix.+addPrefix :: String -> TStyle -> TStyle+addPrefix newPrefix style = style {prefix = newPrefix ++ prefix style}++-- | Append some text to the prefix.+appPrefix :: String -> TStyle -> TStyle+appPrefix newPrefix style = style {prefix = prefix style ++ newPrefix}++hSetPrefix :: IO.Handle -> String -> TStyle -> TStyle+hSetPrefix handle string style =+ style {hPrefix = (handle, string) : filter ((/= handle) . fst) (hPrefix style)}++hAddPrefix :: IO.Handle -> String -> TStyle -> TStyle+hAddPrefix handle string style =+ hSetPrefix handle (string ++ prefix) style+ where prefix = maybe "" id (lookup handle (hPrefix style))++hAppPrefix :: IO.Handle -> String -> TStyle -> TStyle+hAppPrefix handle string style =+ hSetPrefix handle (prefix ++ string) style+ where prefix = maybe "" id (lookup handle (hPrefix style))++-- |Get the current prefix for a particular handle+hGetPrefix :: IO.Handle -> TStyle -> String+hGetPrefix handle style = prefix style ++ maybe "" id (lookup handle (hPrefix style))++-- |Set the output style for the stdout and stderr handle to prefixed,+-- using whatever prefixes were most recently set (default is [1] and [2].)+setPrefixes :: String -> String -> TStyle -> TStyle+setPrefixes stdoutPrefix stderrPrefix style =+ hSetPrefix IO.stdout stdoutPrefix . hSetPrefix IO.stderr stderrPrefix $ style++-- |Switch to prefixed mode and modify both the stdout and stderr prefixes.+addPrefixes :: String -> String -> TStyle -> TStyle+addPrefixes oPrefix ePrefix style =+ hAddPrefix IO.stdout oPrefix . hAddPrefix IO.stderr ePrefix $ style++appPrefixes :: String -> String -> TStyle -> TStyle+appPrefixes oPrefix ePrefix style =+ hAppPrefix IO.stdout oPrefix . hAppPrefix IO.stderr ePrefix $ style++-- |Perform an action if the effective verbosity level is >= 0,+-- otherwise return the default value d.+vIO :: CIO m => Int -> a -> m a -> m a+vIO v d f =+ do v' <- ev v+ if v' >= 0 then f else return d++-- |Write a string to stdout.+putStr :: CIO m => String -> m ()+putStr = hPutStr IO.stdout++-- |Write a string to stderr.+ePutStr :: CIO m => String -> m ()+ePutStr = hPutStr IO.stderr++-- |Verbosity controlled version of ePutStr+vEPutStr :: CIO m => Int -> String -> m ()+vEPutStr = vHPutStr IO.stderr++-- |Write a string to stdout depending on the verbosity level.+vPutStr :: CIO m => Int -> String -> m ()+vPutStr = vHPutStr IO.stdout++-- |Write a character.+hPutChar :: CIO m => IO.Handle -> Char -> m ()+hPutChar h c = hPutStr h [c]++-- |Write a character to stdout.+putChar :: CIO m => Char -> m ()+putChar = hPutChar IO.stdout++-- |Write a character to stderr.+ePutChar :: CIO m => Char -> m ()+ePutChar = hPutChar IO.stderr++-- |Verbosity controlled version of hPutStr+vHPutStr :: CIO m => IO.Handle -> Int -> String -> m ()+vHPutStr h v s = vIO v () (hPutStr h s)++-- |Verbosity controlled version of hPutChar.+vHPutChar :: CIO m => IO.Handle -> Int -> Char -> m ()+vHPutChar h v c = vHPutStr h v [c]++-- |Verbosity controlled version of putChar+vPutChar :: CIO m => Int -> Char -> m ()+vPutChar = vHPutChar IO.stdout++-- |Verbosity controlled version of ePutChar+vEPutChar :: CIO m => Int -> Char -> m ()+vEPutChar = vHPutChar IO.stderr++-- |Move to beginning of next line (if necessary) and output a string.+hPutStrBl :: CIO m => IO.Handle -> String -> m ()+hPutStrBl h s = hBOL h >> hPutStr h s++-- |hPutStrBl to stdout.+putStrBl :: CIO m => String -> m ()+putStrBl = hPutStrBl IO.stdout++-- |hPutStrBl to stderr.+ePutStrBl :: CIO m => String -> m ()+ePutStrBl = hPutStrBl IO.stderr++-- |Verbosity controlled version of hPutStrBl+vHPutStrBl :: CIO m => IO.Handle -> Int -> String -> m ()+vHPutStrBl h v s = vHBOL h v >> vHPutStr h v s++-- |Verbosity controlled version of putStrBl+vPutStrBl :: CIO m => Int -> String -> m ()+vPutStrBl = vHPutStrBl IO.stdout ++-- |Verbosity controlled version of ePutStrBl+vEPutStrBl :: CIO m => Int -> String -> m ()+vEPutStrBl = vHPutStrBl IO.stderr++-- |Write a newline character and a string.+hPutStrLn :: CIO m => IO.Handle -> String -> m ()+hPutStrLn h s = hBOL h >> hPutStr h s++-- |hPutStrLn to stdout.+putStrLn :: CIO m => String -> m ()+putStrLn s = hPutStrLn IO.stdout s++-- |hPutStrLn to stderr.+ePutStrLn :: CIO m => String -> m ()+ePutStrLn = hPutStrLn IO.stderr++-- |Verbosity controlled version of hPutStrLn.+vHPutStrLn :: CIO m => IO.Handle -> Int -> String -> m ()+vHPutStrLn h v s = vHBOL h v >> vHPutStr h v s++-- |Verbosity controlled version of putStrLn+vPutStrLn :: CIO m => Int -> String -> m ()+vPutStrLn = vHPutStrLn IO.stdout ++-- |Verbosity controlled version of ePutStrLn+vEPutStrLn :: CIO m => Int -> String -> m ()+vEPutStrLn = vHPutStrLn IO.stderr++-- |hBOL to stdout.+bol :: CIO m => m ()+bol = hBOL IO.stdout++-- |hBOL to stderr.+eBOL :: CIO m => m ()+eBOL = hBOL IO.stderr++vHBOL :: CIO m => IO.Handle -> Int -> m ()+vHBOL h v = vIO v () (hBOL h)++-- |Verbosity controlled version of BOL+vBOL :: CIO m => Int -> m ()+vBOL = vHBOL IO.stdout++-- |Verbosity controlled version of eBOL+vEBOL :: CIO m => Int -> m ()+vEBOL = vHBOL IO.stderr++-- These only work in a terminal, not in an emacs shell.+hColor h s = case h of+ _ | h == IO.stdout -> green s+ _ | h == IO.stdout -> red s+ _ -> magenta s++blue s = "\ESC[34m" ++ s ++ "\ESC[30m"+green s = "\ESC[32m" ++ s ++ "\ESC[30m"+red s = "\ESC[31m" ++ s ++ "\ESC[30m"+magenta s = "\ESC[35m" ++ s ++ "\ESC[30m"
+ Extra/Either.hs view
@@ -0,0 +1,20 @@+module Extra.Either where++import Data.Either (partitionEithers, rights)++isRight (Right _) = True+isRight (Left _) = False++isLeft = not . isRight++-- |Turn a list of eithers into an either of lists+concatEithers :: [Either a b] -> Either [a] [b]+concatEithers xs =+ case partitionEithers xs of + ([], rs) -> Right rs+ (ls, _) -> Left ls++{-# DEPRECATED rightOnly "Use rights" #-}+rightOnly = rights+{-# DEPRECATED eitherFromList "Use concatEithers" #-}+eitherFromList = concatEithers
+ Extra/Exit.hs view
@@ -0,0 +1,15 @@+module Extra.Exit where++import Extra.HughesPJ+import System.Environment+import System.Exit+import System.IO+import Text.PrettyPrint.HughesPJ++-- |exitFailure with nicely formatted help text on stderr+exitWithHelp :: (String -> Doc) -- ^ generate help text, the argument is the result of getProgName+ -> IO a -- ^ no value is returned, this function always calls exitFailure+exitWithHelp helpText =+ do progName <- getProgName+ hPutStrLn stderr =<< renderWidth (helpText progName)+ exitFailure
+ Extra/Files.hs view
@@ -0,0 +1,253 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- |Some extra operations on files. The functions here generally+-- return (Right ()) on success, Left [messages] on failure, and throw+-- an exception when a failure leaves things in an inconsistant state.+-- An example of an inconsistant state would be if we got a failure+-- when writing out a file, but were unable to restore the original+-- file to its original position.+module Extra.Files + ( getSubDirectories+ , renameAlways+ , renameMissing+ , deleteMaybe+ , installFiles+ , writeAndZipFileWithBackup+ , writeAndZipFile+ , backupFile+ , writeFileIfMissing+ , maybeWriteFile -- writeFileUnlessSame+ , createSymbolicLinkIfMissing+ , prepareSymbolicLink+ , forceRemoveLink+ , replaceFile+ ) where++import qualified Codec.Compression.GZip as GZip+import qualified Codec.Compression.BZip as BZip+import Control.Exception+import Control.Monad+import qualified Data.ByteString.Lazy as B+import Data.List+import Data.Maybe+import Extra.Misc+import System.Unix.Directory+import System.Directory+import System.IO.Error hiding (try, catch)+import System.Posix.Files++-- | Return the list of subdirectories, omitting . and .. and ignoring+-- symbolic links.+getSubDirectories :: FilePath -> IO [String]+getSubDirectories path =+ getDirectoryContents path >>=+ return . filter (not . (flip elem) [".", ".."]) >>=+ filterM isRealDirectory+ where+ isRealDirectory name = getSymbolicLinkStatus (path ++ "/" ++ name) >>= return . not . isSymbolicLink++-- |Atomically install a list of files. Returns a list of what went+-- wrong on failure. Will throw an error if it fails and is unable to+-- restore the original files to their original states.+installFiles :: [(FilePath, FilePath)] -> IO (Either [String] ())+installFiles pairs =+ do backedUp <- mapM (uncurry renameAlways) (zip originalFiles backupFiles)+ case lefts backedUp of+ [] -> + do renamed <- mapM (uncurry renameAlways) (zip replacementFiles originalFiles)+ case lefts renamed of+ [] -> return $ Right ()+ _ ->+ -- We failed after all the original files were+ -- renamed and maybe some of the replacement+ -- files were installed. Move all the renamed+ -- files back into place.+ do restored <- mapM (uncurry renameAlways) (zip backupFiles originalFiles)+ case lefts restored of+ -- We succeeded in failing.+ [] -> return . Left . concat . lefts $ renamed+ -- Restore failed. Throw an exception.+ _ -> error ("installFiles: Couldn't restore original files after write failure:" +++ concat (map message (zip3 replacementFiles originalFiles renamed)) +++ concat (map message (zip3 originalFiles backupFiles restored)))+ _ ->+ -- We failed after renaming all original files, but+ -- before any of the replacement files were installed.+ -- Restore the backup for any missing original files.+ do restored <- mapM (uncurry renameMissing) (zip backupFiles originalFiles)+ case lefts restored of+ -- We succeeded in failing.+ [] -> return . Left . concat . lefts $ backedUp+ -- Restore failed. Throw an exception.+ _ -> error ("installFiles: Couldn't restore original files after write failure: " +++ concat (map message (zip3 originalFiles backupFiles backedUp)) +++ concat (map message (zip3 backupFiles originalFiles restored)))+ where+ replacementFiles = map fst pairs+ originalFiles = map snd pairs+ backupFiles = map (++ "~") originalFiles+ message (path1, path2, Left problems) =+ "\n " ++ path1 ++ " -> " ++ path2 ++ ": " ++ concat (intersperse ", " (map show problems))+ message (_, _, Right ()) = ""++lefts :: [Either a b] -> [a]+lefts xs = catMaybes $ map (either Just (const Nothing)) xs++-- |Change a file's name only if the new name doesn't exist.+renameMissing :: FilePath -> FilePath -> IO (Either [String] ())+renameMissing old new =+ do exists <- fileExist new+ case exists of+ True -> return $ Right ()+ False -> renameAlways old new++-- |Change a file's name, removing any existing file with the new name.+renameAlways :: FilePath -> FilePath -> IO (Either [String] ())+renameAlways old new =+ do deleted <- deleteMaybe new+ case deleted of+ Right () ->+ try (rename old new) >>=+ return . either (\ (e :: SomeException) -> Left ["Couldn't rename " ++ old ++ " -> " ++ new ++ ": " ++ show e]) (\ _ -> Right ())+ x -> return x++-- |Change a file's name if it exists.+renameMaybe :: FilePath -> FilePath -> IO (Either [String] ())+renameMaybe old new =+ do exists <- fileExist old+ case exists of+ False -> return $ Right ()+ True -> renameAlways old new++-- |Delete a file if it exists+deleteMaybe :: FilePath -> IO (Either [String] ())+deleteMaybe path =+ do exists <- fileExist path+ case exists of+ False -> return $ Right ()+ True ->+ do status <- getSymbolicLinkStatus path+ -- To do: should we remove the directory contents?+ let rm = if isDirectory status then removeDirectory else removeLink+ try (rm path) >>= return . either (\ (e :: SomeException) -> Left ["Couldn't remove " ++ path ++ ": " ++ show e]) (const . Right $ ())++-- |Create or update gzipped and bzip2-ed versions of a file.+zipFile :: FilePath -> IO (Either [String] ())+zipFile path =+ try (do forceRemoveLink gz+ forceRemoveLink bz2+ B.readFile path >>= B.writeFile gz . {- t1 . -} GZip.compress+ B.readFile path >>= B.writeFile bz2 . {- t2 . -} BZip.compress) >>=+ return . either (\ (e :: SomeException) -> Left ["Failure writing and zipping " ++ path, show e]) Right+ where+ gz = path ++ ".gz"+ bz2 = path ++ ".bz2"+ --t1 s = trace ("Size of " ++ gz ++ " text: " ++ show (L.length s)) s+ --t2 s = trace ("Size of " ++ bz2 ++ " text: " ++ show (L.length s)) s+ --writeMessage (command, output) =+ -- case exitCodeOnly output of+ -- (ExitFailure n : _) ->+ -- command ++ " -> " ++ show n ++ ":\n " ++ L.unpack (stderrOnly output)+ -- _ -> ""++-- |like removeLink, but does not fail if link did not exist+forceRemoveLink :: FilePath -> IO ()+forceRemoveLink fp = removeLink fp `Control.Exception.catch` (\e -> unless (isDoesNotExistError e) (ioError e))+ +-- | Write out three versions of a file, regular, gzipped, and bzip2ed.+writeAndZipFileWithBackup :: FilePath -> B.ByteString -> IO (Either [String] ())+writeAndZipFileWithBackup path text =+ backupFile path >>=+ either (\ e -> return (Left ["Failure renaming " ++ path ++ " -> " ++ path ++ "~: " ++ show e]))+ (\ _ -> try (B.writeFile path text) >>=+ either (\ (e :: SomeException) ->+ restoreBackup path >>=+ either (\ e -> error ("Failed to restore backup: " ++ path ++ "~ -> " ++ path ++ ": " ++ show e))+ (\ _ -> return (Left ["Failure writing " ++ path ++ ": " ++ show e])))+ (\ _ -> zipFile path))++-- | Write out three versions of a file, regular, gzipped, and bzip2ed.+-- This new version assumes the files are written to temporary locations,+-- so any existing file there can be removed.+writeAndZipFile :: FilePath -> B.ByteString -> IO (Either [String] ())+writeAndZipFile path text =+ deleteMaybe path >>=+ either (\ e -> return (Left ["Failure removing " ++ path ++ ": " ++ show e]))+ (\ _ -> try (B.writeFile path text) >>=+ either (\ (e :: SomeException) -> return (Left ["Failure writing " ++ path ++ ": " ++ show e]))+ (\ _ -> zipFile path))++-- Turn a file into a backup file if it exists.+backupFile :: FilePath -> IO (Either [String] ())+backupFile path = renameMaybe path (path ++ "~")++restoreBackup :: FilePath -> IO (Either [String] ())+restoreBackup path = renameMaybe (path ++ "~") path++-- | Like writeFile, but if the file already exists don't touch it.+-- Example: writeFileIfMissing True \"\/var\/lib\/dpkg\/status\" \"\"+writeFileIfMissing :: Bool -> FilePath -> String -> IO ()+writeFileIfMissing mkdirs path text =+ do+ exists <- doesFileExist path+ case exists of+ False ->+ do+ if mkdirs then+ createDirectoryIfMissing True (parentPath path) else+ return ()+ replaceFile path text+ True ->+ return ()++-- | Write a file if its content is different from the given text.+maybeWriteFile :: FilePath -> String -> IO ()+maybeWriteFile path text =+ try (readFile path) >>= maybeWrite+ where+ maybeWrite (Left (e :: IOException)) | isDoesNotExistError e = writeFile path text+ maybeWrite (Left e) = error ("maybeWriteFile: " ++ show e)+ maybeWrite (Right old) | old == text = return ()+ maybeWrite (Right _old) = + --hPutStrLn stderr ("Old text: " ++ show old) >>+ --hPutStrLn stderr ("New text: " ++ show text) >>+ replaceFile path text++-- |Add-on for System.Posix.Files+createSymbolicLinkIfMissing :: String -> FilePath -> IO ()+createSymbolicLinkIfMissing text path =+ try (getSymbolicLinkStatus path) >>=+ either (\ (_ :: SomeException) -> createSymbolicLink text path) (\ _ -> return ())++prepareSymbolicLink :: FilePath -> FilePath -> IO ()+prepareSymbolicLink name path =+ checkExists >>= checkType >>= checkContent+ where+ checkExists = doesDirectoryExist path >>= orCreate+ checkType False = return False+ checkType True = getSymbolicLinkStatus path >>= return . isSymbolicLink >>= orReplace+ checkContent False = return ()+ checkContent True = readSymbolicLink path >>= return . (== name) >>= orReplace >> return ()+ orReplace True = return True+ orReplace False = do removeRecursiveSafely path; orCreate False+ orCreate True = return True+ orCreate False = do createSymbolicLink name path; return False++-- Replace a file's contents, accounting for the possibility that the+-- old contents of the file may still be being read. Apparently there+-- is a race condition in the file system so we may get one or more+-- isAlreadyBusyError exceptions before the writeFile succeeds.+replaceFile :: FilePath -> String -> IO ()+replaceFile path text =+ --tries 100 10 f -- There is now a fix for this problem, see ghc ticket 2122.+ f+ where+ f :: IO ()+ f = removeFile path `Control.Exception.catch` (\ e -> if isDoesNotExistError e then return () else ioError e) >> writeFile path text++-- Try something n times, returning the first Right or the last Left+-- if it never succeeds. Sleep between tries.+--tries :: Int -> Int -> (IO a) -> IO (Either Exception a)+{-+tries _ 1 f = try f >>= either (return . Left) (return . Right)+tries usec count f = try f >>= either (\ (_ :: SomeException) -> usleep usec >> tries usec (count - 1) f) (return . Right)+-}
+ Extra/GPGSign.hs view
@@ -0,0 +1,72 @@+module Extra.GPGSign+ ( sign+ , PGPKey(..)+ , pgpSignFiles+ , pgpSignFile+ , cd+ ) where++import System.Process+import System.IO+import System.Exit+import Extra.Misc++_test :: PGPKey'' -> [FilePath] -> IO [FilePath]+_test key files =+ mapM (sign key) files++type PGPKey'' = String+++sign :: PGPKey'' -> FilePath -> IO FilePath+sign keyname path =+ do (_, _,err,pid) <- runInteractiveProcess cmd args workingDir env+ status <- waitForProcess pid+ case status of+ ExitSuccess -> return outputPath+ ExitFailure _ ->+ do gpgerr <- hGetContents err+ hPutStr stderr gpgerr+ exitWith status+ where+ cmd = "/usr/bin/gpg"+ args = [ "--batch"+ , "--yes"+ , "--default-key", keyname+ , "-o", outputPath+ , "--clearsign"+ , path+ ]+ outputPath = path ++ ".gpg"+ workingDir = Nothing -- Just (dirName path)+ env = Nothing++data PGPKey = Key String | Default deriving Show++pgpSignFiles :: FilePath -> PGPKey -> [FilePath] -> IO [Bool]+pgpSignFiles root key files = cd root $ mapM (pgpSignFile key) files++pgpSignFile :: PGPKey -> FilePath -> IO Bool+pgpSignFile keyname path =+ do (_, _,err,pid) <- runInteractiveProcess cmd args workingDir env+ status <- waitForProcess pid+ case status of+ ExitSuccess -> return True+ ExitFailure _code ->+ do gpgerr <- hGetContents err+ hPutStr stderr gpgerr+ return False+ where+ cmd = "/usr/bin/gpg"+ args = defaultKey +++ [ "--batch"+ , "--yes"+ , "-o", outputPath+ , "--armor"+ , "--detach-sign"+ , path+ ]+ defaultKey = case keyname of Key name -> ["--default-key", name]; Default -> []+ outputPath = path ++ ".gpg"+ workingDir = Nothing -- Just (dirName path)+ env = Nothing
+ Extra/HughesPJ.hs view
@@ -0,0 +1,11 @@+module Extra.HughesPJ where++import Data.Maybe+import Extra.Terminal+import Text.PrettyPrint.HughesPJ++-- |render a Doc using the current terminal width+renderWidth :: Doc -> IO String +renderWidth doc =+ do columns <- return . fromMaybe 80 =<< getWidth+ return $ renderStyle (Style PageMode columns 1.0) doc
+ Extra/IO.hs view
@@ -0,0 +1,20 @@+module Extra.IO {-# DEPRECATED "CIO is deprecated" #-} where++import Extra.CIO+import qualified System.IO as IO+import Control.Exception (try)++-- |Use this module to call functions in the CIO module from the+-- regular IO monad. This instance ignores all style and state+-- information. The verbosity controlled output functions will ignore+-- any calls when v is greater than zero. This allows you to call the+-- functions in the haskell-debian package from the regular IO monad.+-- +-- This is in a separate module from CIO so you don't accidentally do+-- a liftIO of some other CIO operation and get this instance.+instance CIO IO where+ hPutStr h s = IO.hPutStr h s+ hBOL h = IO.hPutStr h "\n"+ ev v = return (- v)+ setStyle _ f = f+ tryCIO = try
+ Extra/IOThread.hs view
@@ -0,0 +1,41 @@+-- |this module provides a simple mechanism for adding IO operations+-- to a queue and running them in a single thread. This is useful if+-- the IO operations have side-effects which could collide if run from+-- multiple threads. For example, creating an image thumbnail and+-- storing it on disk, running latex, etc.+module Extra.IOThread where++import Control.Concurrent (ThreadId, forkIO)+import Control.Concurrent.Chan (Chan,newChan, readChan, writeChan)+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, readMVar)+import Control.Exception+import Control.Monad (forever)++newtype IOThread a b = IOThread (Chan (a, MVar (Either SomeException b)))++-- |start the IO thread.+startIOThread :: (a -> IO b) -- ^ the IO function that does all the work+ -> IO (ThreadId, IOThread a b) -- ^ a ThreadId which can be used to kill the IOThread, and a handle that can be used to issue requests to the thread.+startIOThread f =+ do c <- newChan+ tid <- forkIO $ ioThread f c+ return (tid, IOThread c)+ where+ ioThread f c =+ forever $ do (a, mvar) <- readChan c+ b <- try $ f a+ putMVar mvar b++-- |issue a request to the IO thread and get back the result+-- if the thread function throws an exception 'ioRequest' will rethrow the exception.+ioRequest :: (IOThread a b) -- ^ handle to the IOThread+ -> a -- ^ argument to the function in the IOThread+ -> IO b -- ^ value returned by the function in the IOThread+ioRequest (IOThread chan) a =+ do resp <- newEmptyMVar + writeChan chan (a, resp)+ e <- readMVar resp+ case e of+ (Right r) -> return r+ (Left err) -> throwIO err+
+ Extra/List.hs view
@@ -0,0 +1,95 @@+module Extra.List+ ( consperse+ , surround+ , changePrefix+ , dropPrefix+ , cartesianProduct+ , wordsBy+ , empty+ , sortByMapped+ , sortByMappedM+ , partitionM+ , listIntersection+ , isSublistOf+ ) where++import Control.Monad+import Data.List++{-# DEPRECATED consperse "Use intercalate" #-}+consperse :: [a] -> [[a]] -> [a]+-- ^ The mighty consperse function - e.g. consperse "," ["a", "b"] -> "a,b"+-- consperse = MissingH.List.join+consperse s l = concat . intersperse s $ l++surround :: [a] -> [a] -> [[a]] -> [a]+-- ^ surround each element of a list - e.g. surround "(" ")" ["a", "b"] -> ["(a)(b)"]+surround prefix suffix items = concat $ map ((prefix ++) . (++ suffix)) items++-- |Replace the prefix of s, return Nothing if it doesn't match.+changePrefix :: (Eq a) => [a] -> [a] -> [a] -> Maybe [a]+changePrefix old new s = maybe Nothing (Just . (new ++)) (dropPrefix old s)++-- |Remove a prefix of s, return nothing if it doesn't match.+dropPrefix :: (Eq a) => [a] -> [a] -> Maybe [a]+dropPrefix prefix s =+ case isPrefixOf prefix s of+ True -> Just (drop (length prefix) s)+ False -> Nothing++cartesianProduct :: [[a]] -> [[a]]+-- ^ cartesianProduct [[1,2,3], [4,5],[6]] -> [[1,4,6],[1,5,6],[2,4,6],[2,5,6],[3,4,6],[3,5,6]]+cartesianProduct [] = []+cartesianProduct [xs] = map (: []) xs+cartesianProduct (xs : yss) =+ distribute xs (cartesianProduct yss)+ where distribute xs yss = concat (map (\ x -> map (x :) yss) xs)++-- |FIXME: implement for a string+wordsBy :: Eq a => (a -> Bool) -> [a] -> [[a]]+wordsBy p s = + case (break p s) of+ (s, []) -> [s]+ (h, t) -> h : wordsBy p (drop 1 t)++-- |Like maybe, but with empty vs. non-empty list+empty :: b -> ([a] -> b) -> [a] -> b+empty e _ [] = e+empty _ f l = f l++-- |Sort a list using the compare function on the list elements mapped+-- over f. This is like "sortBy (\ a b -> compare (f a) (f b))"+-- except that f is applied O(n) times instead of O(n log n)+sortByMapped :: (a -> b) -> (b -> b -> Ordering) -> [a] -> [a]+sortByMapped f compare list =+ map fst sorted+ where+ sorted = sortBy (\ (_, x) (_, y) -> compare x y) pairs+ pairs = zip list (map f list)++-- |Monadic version of sortByMapped+sortByMappedM :: (a -> IO b) -> (b -> b -> Ordering) -> [a] -> IO [a]+sortByMappedM f compare list =+ do+ pairs <- mapM f list >>= return . (zip list)+ let sorted = sortBy (\ (_, x) (_, y) -> compare x y) pairs+ return (map fst sorted)++partitionM :: (Monad m) => (a -> m Bool) -> [a] -> m ([a], [a])+partitionM p xs =+ foldM f ([], []) xs+ where f (a, b) x = p x >>= (\ flag -> return $ if flag then (x : a, b) else (a, x : b))++listIntersection :: Eq a => [[a]] -> [a]+listIntersection [] = []+listIntersection (first : rest) = foldr intersect first rest++isSublistOf :: Eq a => [a] -> [a] -> Maybe Int+isSublistOf sub lst =+ maybe Nothing (\ s -> Just (length s - length sub))+ (find (isSuffixOf sub) (inits lst))++{-+lookups :: (Eq a) => a -> [(a, b)] -> [b]+lookups a = map snd . filter ((a ==) . fst)+-}
+ Extra/Lock.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Extra.Lock+ ( withLock+ , awaitLock+ ) where++import Control.Exception+import Control.Monad.RWS+import Prelude hiding (catch)+import System.Directory+import System.IO+import System.IO.Error hiding (try, catch)+import System.Posix.Files+import System.Posix.IO+import System.Posix.Unistd++withLock :: (MonadIO m) => FilePath -> m a -> m a+withLock path task =+ liftIO (checkLock >> takeLock) >> task >>= \ result -> liftIO dropLock >> return result+ where+ -- Return True if file is locked by a running process, false otherwise+ --checkLock :: IO (Either Exception ())+ checkLock = readFile path `catch` checkReadError >>= processRunning . lines+ checkReadError :: IOError -> IO String+ checkReadError e | isDoesNotExistError e = return ""+ checkReadError e = throw e+ processRunning :: [String] -> IO ()+ processRunning (pid : _) =+ do exists <- doesDirectoryExist ("/proc/" ++ pid)+ case exists of+ True -> throw (lockedBy pid path)+ False -> breakLock+ processRunning [] = breakLock+ breakLock = removeFile path `catch` checkBreakError+ checkBreakError (e :: IOException) | isDoesNotExistError e = return ()+ checkBreakError e = throw e+ takeLock :: IO ()+ takeLock =+ -- Try to create the lock file in exclusive mode, if this+ -- succeeds then we have a lock. Then write the process ID+ -- into the lock and close.+ openFd path ReadWrite (Just 0o600) (defaultFileFlags {exclusive = True, trunc = True}) >>=+ fdToHandle >>= \ h -> processID >>= hPutStrLn h >> hClose h+ dropLock = removeFile path `catch` checkDrop+ checkDrop (e :: IOException) | isDoesNotExistError e = return ()+ checkDrop e = throw e++-- |Like withLock, but instead of giving up immediately, try n times+-- with a wait between each.+--awaitLock :: (MonadIO m) => Int -> Int -> FilePath -> m a -> m (Either Exception a)+awaitLock tries usecs path task =+ attempt 0+ where + attempt n | n >= tries = error "awaitLock: too many failures"+ attempt n = withLock path task `catch` checkLockError+ where+ checkLockError e | isAlreadyInUseError e = liftIO (usleep usecs) >> attempt (n + 1)+ checkLockError e = throw e++processID :: IO String+processID = readSymbolicLink "/proc/self"++lockedBy pid path = mkIOError alreadyInUseErrorType ("Locked by " ++ pid) Nothing (Just path)
+ Extra/Misc.hs view
@@ -0,0 +1,155 @@+module Extra.Misc+ (+ -- * String functions+ columns+ , justify+ -- * Tuple functions+ , mapSnd+ -- * FilePath functions+ , parentPath+ , canon+ -- * Map and Set functions+ , listMap+ , listDiff+ -- * System.Posix+ , checkSuperUser+ , md5sum+ , sameInode+ , sameMd5sum+ , tarDir+ -- * Processes+ -- , splitOutput+ -- * ByteString+ , cd+ -- * Debugging+ , read'+ ) where++import Control.Exception+import qualified Data.ByteString.Lazy.Char8 as B+import qualified Data.Digest.Pure.MD5+import Data.List+import qualified Data.Map as Map+import Data.Maybe+import qualified Data.Set as Set+import Extra.List+import System.Exit+import System.FilePath+import System.Directory+import System.Posix.Files+import System.Posix.User (getEffectiveUserID)+import System.Process (readProcessWithExitCode)+-- import System.Process.Progress (keepStdout, keepStderr, keepResult)+import Text.Regex++mapSnd :: (b -> c) -> (a, b) -> (a, c)+mapSnd f (a, b) = (a, f b)++-- Control file stuff++-- |Pad strings so the columns line up. The argument and return value+-- elements are the rows of a table. Do not pad the rightmost column.+columns :: [[String]] -> [[String]]+columns rows =+ map (map pad . zip (widths ++ [0])) rows+ where+ widths = map (fromJust . listMax) . transpose . map (map length . init) $ rows+ listMax l = foldl (\ a b -> Just . maybe b (max b) $ a) Nothing l+ pad (width, field) = field ++ replicate (max 0 (width - length field)) ' '++-- |Group words into lines of length n or less.+justify :: String -> Int -> [[String]]+justify s n =+ foldr doWord [[]] (words s)+ where doWord w [] = [[w]]+ doWord w (ws : etc) = + if length (concat (intersperse " " (w:ws))) <= n then+ (w : ws) : etc else+ [w] : ws : etc++-- |dirname+parentPath :: FilePath -> FilePath+parentPath path = fst (splitFileName path)++-- |Turn a list of (k, a) pairs into a map from k -> [a]. The order of the elements in+-- the a list is preserved.+listMap :: (Ord k) => [(k, a)] -> Map.Map k [a]+listMap pairs =+ foldl insertPair Map.empty (reverse pairs)+ where insertPair m (k,a) = Map.insert k (a : (Map.findWithDefault [] k m)) m++-- Return the difference of two lists. Order is not preserved.+listDiff :: Ord a => [a] -> [a] -> [a]+listDiff a b = Set.toList (Set.difference (Set.fromList a) (Set.fromList b))++-- | Weak attempt at canonicalizing a file path.+canon :: FilePath -> FilePath+canon path =+ let re = mkRegex "/" in+ let names = splitRegex re path in+ concat (intersperse "/" (merge names))+ where+ merge (".." : xs) = ".." : (merge xs)+ merge ("." : xs) = "." : (merge xs)+ merge (_ : ".." : xs) = (merge xs)+ merge (x : "." : xs) = (merge (x : xs))+ merge (x : xs) = x : merge xs+ merge [] = []++{-# DEPRECATED md5sum "Use Data.ByteString.Lazy.Char8.readFile path >>= return . show . Data.Digest.Pure.MD5.md5" #-}+-- | Run md5sum on a file and return the resulting checksum as text.+md5sum :: FilePath -> IO String+md5sum path = B.readFile path >>= return . show . Data.Digest.Pure.MD5.md5++-- | Predicate to decide if two files have the same inode.+sameInode :: FilePath -> FilePath -> IO Bool+sameInode a b =+ do+ aStatus <- getFileStatus a+ bStatus <- getFileStatus b+ return (deviceID aStatus == deviceID bStatus && fileID aStatus == fileID bStatus)++-- | Predicate to decide if two files have the same md5 checksum.+sameMd5sum :: FilePath -> FilePath -> IO Bool+sameMd5sum a b =+ do+ asum <- md5sum a+ bsum <- md5sum b+ return (asum == bsum)++{-+splitOutput :: [Output B.ByteString] -> (B.ByteString, B.ByteString, [ExitCode])+splitOutput output = (B.concat (keepStdout output), B.concat (keepStderr output), keepResult output)+-}++-- |A version of read with a more helpful error message.+read' s =+ case reads s of+ [] -> error $ "read - no parse: " ++ show s+ ((x, _s) : _) -> x++checkSuperUser :: IO Bool+checkSuperUser = getEffectiveUserID >>= return . (== 0)++-- | Given a tarball, return the name of the top directory.+tarDir :: FilePath -> IO (Maybe String)+tarDir path =+ readProcessWithExitCode "tar" ["tfz", path] "" >>= \ (code, out, _) ->+ case code of+ ExitSuccess -> return . dir . lines $ out+ _ -> return Nothing+ where+ dir [] = Nothing+ dir (file : _) = case wordsBy (== '/') file of+ [] -> Nothing+ ("" : _) -> Nothing+ (s : _) -> Just s++cd :: FilePath -> IO a -> IO a+cd name m = + bracket + (do cwd <- getCurrentDirectory+ setCurrentDirectory name+ return cwd)+ (\oldwd -> do setCurrentDirectory oldwd)+ (const m)
+ Extra/Net.hs view
@@ -0,0 +1,20 @@+module Extra.Net+ ( webServerDirectoryContents+ ) where++import qualified Data.ByteString.Lazy.Char8 as L+import Data.Maybe+import Text.Regex++-- | Parse the text returned when a directory is listed by a web+-- server. This is currently only known to work with Apache.+-- NOTE: there is a second copy of this function in+-- debian:Debian.URI. Please update both locations if you make+-- changes.+webServerDirectoryContents :: L.ByteString -> [String]+webServerDirectoryContents text =+ catMaybes . map (second . matchRegex re) . lines . L.unpack $ text+ where+ re = mkRegex "( <A HREF|<a href)=\"([^/][^\"]*)/\""+ second (Just [_, b]) = Just b+ second _ = Nothing
+ Extra/SSH.hs view
@@ -0,0 +1,114 @@+module Extra.SSH+ ( sshVerify+ , sshExportDeprecated+ , sshCopy+ ) where++import System.Cmd+import System.Directory+import System.Posix.User+import System.Environment+import System.Exit+import System.IO+import System.Process (readProcessWithExitCode, showCommandForUser)+-- |Set up access to destination (user\@host).+sshExportDeprecated :: String -> Maybe Int -> IO (Either String ())+sshExportDeprecated dest port =+ generatePublicKey >>=+ either (return . Left) (testAccess dest port) >>=+ either (return . Left) (openAccess dest port)++-- parseURI "ssh://dsf@server:22"+-- URI {uriScheme = "ssh:", uriAuthority = Just (URIAuth {uriUserInfo = "dsf@", uriRegName = "server", uriPort = ":22"}), uriPath = "", uriQuery = "", uriFragment = ""}++-- |Make sure there is a public key for the local account+generatePublicKey :: IO (Either String FilePath)+generatePublicKey =+ do user <- getEffectiveUserID+ home <- getUserEntryForID user >>= return . homeDirectory+ let cmd = "yes '' | ssh-keygen -t rsa 2>&1 >/dev/null"+ let keypath = home ++ "/.ssh/id_rsa.pub"+ exists <- doesFileExist keypath+ case exists of+ True -> return . Right $ keypath+ False ->+ do hPutStrLn stderr $ "generatePublicKey " ++ " -> " ++ keypath+ code <- system cmd+ case code of+ ExitFailure n ->+ return . Left $ "Failure: " ++ show cmd ++ " -> " ++ show n+ _ -> return . Right $ keypath++-- |See if we already have access to the destination (user\@host).+sshVerify :: String -> Maybe Int -> IO Bool+sshVerify dest port =+ do result <- system (sshTestCmd dest port)+ return $ case result of+ ExitSuccess -> True -- We do+ ExitFailure _ -> False -- We do not+ where+ sshTestCmd dest port =+ ("ssh -o 'PreferredAuthentications hostbased,publickey' " +++ (maybe "" (("-p " ++) . show) port) ++ " " ++ show dest ++ " pwd > /dev/null && exit 0")++testAccess :: String -> Maybe Int -> FilePath -> IO (Either String (Maybe FilePath))+testAccess dest port keypath =+ do flag <- sshVerify dest port+ case flag of+ True -> return . Right $ Nothing+ False -> return . Right . Just $ keypath++-- |Try to set up the keys so we have access to the account. I don't+-- think we use this any more, and I don't think you should either.+openAccess :: String -> Maybe Int -> Maybe FilePath -> IO (Either String ())+openAccess _ _ Nothing = return . Right $ ()+openAccess dest port (Just keypath) =+ do hPutStrLn stderr $ "openAccess " ++ show dest ++ " " ++ show port ++ " " ++ show keypath+ let args = maybe [] (\ x -> ["-p", show x]) port ++ [show dest, sshOpenRemoteCmd]+ (code, out, err) <- readFile keypath >>= readProcessWithExitCode "ssh" args+ case code of+ ExitFailure n -> return . Left $ "Failure: " ++ showCommandForUser "ssh" args ++ " -> " ++ show n +++ "\n\nstdout: " ++ out ++ "\n\nstderr: " ++ err+ _ -> return . Right $ ()+ where+ sshOpenRemoteCmd =+ ("chmod g-w . && " ++ -- Ssh will not work if the permissions aren't just so+ "chmod o-rwx . && " +++ "mkdir -p .ssh && " +++ "chmod 700 .ssh && " +++ "cat >> .ssh/authorized_keys2 && " ++ -- Add the key to the authorized key list+ "chmod 600 .ssh/authorized_keys2")++-- This used to be main.+{-+test =+ getDest >>=+ either (return . Left) (uncurry sshExport) >>=+ either (error . show) (const . exitWith $ ExitSuccess)++-- |Get the destination account info from the command line+getDest :: IO (Either String (String, Maybe Int))+getDest =+ getArgs >>= checkArgs+ where checkArgs [dest] =+ return $ case parseURI ("ssh://" ++ dest) of+ Just (URI {uriAuthority = Just (URIAuth {uriUserInfo = user, uriRegName = host, uriPort = ""})}) ->+ Right (user ++ host, Nothing)+ Just (URI {uriAuthority = Just (URIAuth {uriUserInfo = user, uriRegName = host, uriPort = port})}) ->+ case reads (dropWhile (== ':') port) :: [(Int, String)] of+ [] -> Left $ "Invalid destination: " ++ dest ++ " (" ++ port ++ ")"+ ((n, _) : _) -> Right (user ++ host, Just n)+ _ -> Left $ "Invalid destination: " ++ dest+ checkArgs args = return . Left $ "Usage: sshexport user@host"+-}++-- |Copy the ssh configuration from $HOME to the \/root directory of a+-- changeroot.+sshCopy :: FilePath -> IO ExitCode+sshCopy root =+ do exists <- doesDirectoryExist "~/.ssh"+ home <- getEnv "HOME"+ case exists of+ True -> system ("rsync -aHxSpDt --delete " ++ home ++ "/.ssh/ " ++ root ++ "/root/.ssh && " +++ "chown -R root.root " ++ root ++ "/root/.ssh")+ False -> system "mkdir -p /root/.ssh"
+ Extra/Terminal.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE ForeignFunctionInterface #-}+-- Copyright Stefan O'Rear 2006+-- Copyright Jeremy Shaw 2007+module Extra.Terminal where++import System.Posix.Env+import Foreign.C.Types++foreign import ccall "gwinsz.h c_get_window_size" c_get_window_size :: IO CLong++-- get the number of rows and columns using ioctl (0, TIOCGWINSZ, &w)+-- @see also: getWidth+getWinSize :: IO (Int,Int)+getWinSize = do (a,b) <- (`divMod` 65536) `fmap` c_get_window_size+ return (fromIntegral b, fromIntegral a)++-- get the number of colums.+-- First tries getWinSize, if that returns 0, then try the COLUMNS+-- shell variable.+getWidth :: IO (Maybe Int)+getWidth =+ do (cols, _) <- getWinSize+ case cols of+ 0 -> return . fmap read =<< getEnv "COLUMNS"+ _ -> return (Just cols)
+ Extra/Time.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE CPP #-}+module Extra.Time+ ( formatDebianDate+ , myTimeDiffToString+ ) where++import Control.Exception+import Data.List+import Data.Time+#if MIN_VERSION_time(1,5,0)+import qualified System.Locale as Old (defaultTimeLocale)+import qualified System.Time as Old (formatTimeDiff, tdPicosec)+#else+import System.Locale as Old+import System.Time as Old+#endif+import Text.Printf++{- This function is so complicated because there seems to be no way+ to get the Data.Time to format seconds without the fractional part,+ which seems not to be allowed in the RFC822 format.+ The mysterious 'take 2' pulls in just the integral part.++ Note the getCurrentTime :: IO UTCTime, so this will always be in UTC time+ with the resulting string ending in "UTC".+ -}+++formatDebianDate t =+ prefix ++ seconds ++ suffix+ where prefix = formatTime defaultTimeLocale prefixFormat t+ seconds = take 2 $ formatTime defaultTimeLocale secondsFormat t+ suffix = formatTime defaultTimeLocale suffixFormat t+ prefixFormat = "%a, %d %b %Y %H:%M:"+ secondsFormat = "%S"+ suffixFormat = " %Z"+ format = "%a, %d %b %Y %H:%M:%S %Z"+ _test = assert (format == prefixFormat ++ secondsFormat ++ suffixFormat)++_test=+ do tz <- getCurrentTimeZone+ let ut = localTimeToUTC tz testtime+ return $ teststring == formatDebianDate ut+ where testtime = LocalTime {localDay=fromGregorian testyear testmonth testday,+ localTimeOfDay=TimeOfDay{todHour=testhour,todMin=testminute,todSec=testsecond}}+ testyear = 2006+ testmonth = 12+ testday = 19+ testhour = 12+ testminute = 19+ testsecond = 15.29+ teststring = "Tue, 19 Dec 2006 17:19:15 UTC"++myTimeDiffToString diff =+ do+ case () of+ _ | isPrefixOf "00:00:0" s -> drop 7 s ++ printf ".%03d" ms ++ " s."+ _ | isPrefixOf "00:00:" s -> drop 6 s ++ printf ".%03d" ms ++ " s."+ _ | isPrefixOf "00:" s -> drop 3 s+ _ -> s+ where+ s = Old.formatTimeDiff Old.defaultTimeLocale "%T" diff+ ms = ps2ms ps+ ps2ms ps = quot (ps + 500000000) 1000000000+ ps = Old.tdPicosec diff
+ Extra/URI.hs view
@@ -0,0 +1,91 @@+-- |Make URI an instance of Read and Ord, and add functions to+-- manipulate the uriQuery.+module Extra.URI+ ( module Network.URI+ , relURI+ , setURIPort+ , parseURIQuery+ , modifyURIQuery+ , setURIQuery+ , setURIQueryAttr+ , deleteURIQueryAttr+ ) where++import Network.URI -- (URIAuth(..), URI(..), parseURI, uriToString, escapeURIString, isUnreserved, unEscapeString)+import Data.List(intersperse, groupBy, inits)+import Data.Maybe(isJust, isNothing, catMaybes)+import Control.Arrow(second)++-- |Create a relative URI with the given query.+relURI :: FilePath -> [(String, String)] -> URI+relURI path pairs = URI {uriScheme = "",+ uriAuthority = Nothing,+ uriPath = path,+ uriQuery = formatURIQuery pairs,+ uriFragment = ""}++-- |Set the port number in the URI authority, creating it if necessary.+setURIPort port uri =+ uri {uriAuthority = Just auth'}+ where+ auth' = auth {uriPort = port}+ auth = maybe nullAuth id (uriAuthority uri)+ nullAuth = URIAuth {uriUserInfo = "", uriRegName = "", uriPort = ""}++-- |Return the pairs in a URI's query+parseURIQuery :: URI -> [(String, String)]+parseURIQuery uri =+ case uriQuery uri of+ "" -> []+ '?' : attrs ->+ map (second (unEscapeString . tail) . break (== '='))+ (filter (/= "&") (groupBy (\ a b -> a /= '&' && b /= '&') attrs))+ x -> error $ "Invalid URI query: " ++ x++-- |Modify a URI's query by applying a function to the pairs+modifyURIQuery :: ([(String, String)] -> [(String, String)]) -> URI -> URI+modifyURIQuery f uri = uri {uriQuery = formatURIQuery (f (parseURIQuery uri))}++setURIQuery :: [(String, String)] -> URI -> URI+setURIQuery pairs = modifyURIQuery (const pairs)++setURIQueryAttr :: String -> String -> URI -> URI+setURIQueryAttr name value uri =+ modifyURIQuery f uri+ where f pairs = (name, value) : filter ((/= name) . fst) pairs++deleteURIQueryAttr :: String -> URI -> URI+deleteURIQueryAttr name uri =+ modifyURIQuery f uri+ where f pairs = filter ((/= name) . fst) pairs++-- |Turn a list of attribute value pairs into a uriQuery.+formatURIQuery :: [(String, String)] -> String+formatURIQuery [] = ""+formatURIQuery attrs = '?' : concat (intersperse "&" (map (\ (a, b) -> a ++ "=" ++ escapeURIForQueryValue b) attrs))++-- |Escape a value so it can safely appear on the RHS of an element of+-- the URI query. The isUnreserved predicate is the set of characters+-- that can appear in a URI which don't have any special meaning.+-- Everything else gets escaped.+escapeURIForQueryValue = escapeURIString isUnreserved++-- Make URI an instance of Read. This will throw an error if no+-- prefix up to ten characters long of the argument string looks like+-- a URI. If such a prefix is found, it will continue trying longer+-- and longer prefixes until the result no longer looks like a URI.+instance Read URI where+ readsPrec _ s =+ let allURIs = map parseURI (inits s) in+ -- If nothing in the first ten characters looks like a URI, give up+ case catMaybes (take 10 allURIs) of+ [] -> fail "read URI: no parse"+ -- Return the last element that still looks like a URI+ _ ->+ [(longestURI, drop (length badURIs + length goodURIs - 1) s)]+ where+ longestURI = case reverse (catMaybes goodURIs) of+ [] -> error $ "Invalid URI: " ++ s+ (a : _) -> a+ goodURIs = takeWhile isJust moreURIs+ (badURIs, moreURIs) = span isNothing allURIs
+ Extra/URIQuery.hs view
@@ -0,0 +1,39 @@+module Extra.URIQuery+ ( modify+ , del+ , put+ , copy+ ) where++import Data.List (partition)+import Data.Maybe (listToMaybe)+import Extra.URI++-- |Modify an individual URI query attributes.+modify :: String -> (Maybe String -> Maybe String) -> URI -> URI+modify a vf uri =+ let (vs, other) = partition ((== a) . fst) (parseURIQuery uri) in+ setURIQuery (case vf (listToMaybe (map snd vs)) of+ Just v' -> (a, v') : other+ Nothing -> other) uri++-- |Replace a query attribute with Nothing.+del :: String -> URI -> URI+del a uri = modify a (const Nothing) uri++-- |Replace a query attribute with something.+put :: String -> String -> URI -> URI +put a v uri = modify a (const (Just v)) uri++-- |Copy an attribute from one query to another+copy :: String -> URI -> URI -> URI+copy a src dst = modify a (const (lookup a (parseURIQuery src))) dst++-- Apply f to all of the URI's pairs+{-+modifyAll :: (String -> Maybe String -> Maybe String) -> URI -> URI+modifyAll f uri =+ foldr (\ (a, _) uri -> modify a (f a) uri) uri pairs+ where+ pairs = parseURIQuery uri+-}
+ Setup.hs view
@@ -0,0 +1,5 @@+#!/usr/bin/runhaskell++import Distribution.Simple++main = defaultMainWithHooks simpleUserHooks
+ Test/QUnit.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}+-----------------------------------------------------------------------------+-- |+-- Module : Test.QUnit+-- Copyright : (c) Koen Claessen, John Hughes 2001, Jeremy Shaw 2008+-- License : BSD-style (see the file libraries\/base\/LICENSE)+-- +-- Maintainer : jeremy@n-heptane.com+-- Stability : experimental+-- Portability : portable+--+-- Some glue code for running QuickCheck tests using the HUnit framework.+--+-- This module provides an instance of Test.HUnit.Testable for+-- Test.QuickCheck.Property, which makes it trivial to use QuickCheck+-- properties in the HUnit framework:+--+-- @+-- import Test.HUnit+-- import Test.HUnit.Text+-- import Test.QuickCheck+-- import Test.QUnit+--+-- runTestTT $ (\"x \/= x\" ~: property (\x -> x /= x))+-- @+--+-- The QuickCheck Property will be run using+-- Test.QuickCheck.defaultConfig. If you need to specific an+-- alternate config, then use 'testQuickCheck' like this:+--+-- @+-- runTestTT $ (\"x \/= x\" ~: testQuickCheck myConfig (\x -> x /= x))+-- @+-----------------------------------------------------------------------------+module Test.QUnit (testQuickCheck) where++import qualified Test.HUnit as HU+import qualified Test.QuickCheck as QC++-- |an instance of Test.HUnit.Testable for Test.QuickCheck.Property+--+-- Note: I did not add an instance:+--+-- instance (QC.Testable a) => (HU.Testable a)+--+-- Because it results in undeciable instances. For example, there is+-- an instance of 'Bool' for QC.Testable and HU.Testable already.+instance HU.Testable QC.Property where+ test qc = testQuickCheck QC.stdArgs qc++-- |turns the quickcheck test into an hunit test+--+-- Use this if you want to provide a custom 'Config' instead of+-- 'defaultConfig'.+testQuickCheck :: (QC.Testable a) => + QC.Args -- ^ quickcheck config+ -> a -- ^ quickcheck property+ -> HU.Test+testQuickCheck args prop =+ HU.TestCase $+ do result <- QC.quickCheckWithResult args prop+ case result of+ QC.Success{} -> return ()+ QC.GaveUp{} -> let ntest = QC.numTests result in HU.assertFailure $ "Arguments exhausted after" ++ show ntest ++ (if ntest == 1 then " test." else " tests.")+ QC.Failure{} -> let reason = QC.reason result in HU.assertFailure reason+ QC.NoExpectedFailure{} -> HU.assertFailure $ "No Expected Failure"
+ Test/QuickCheck/Properties.hs view
@@ -0,0 +1,9 @@+module Test.QuickCheck.Properties where++import Test.QuickCheck++isIdempotentBy :: (Arbitrary a, Eq a, Show a) => (a -> a) -> Gen a -> Property+isIdempotentBy f src = forAll src $ \a -> f a == f (f a)++isIdempotent :: (Arbitrary a, Eq a, Show a) => (a -> a) -> Property+isIdempotent f = isIdempotentBy f arbitrary
+ cbits/gwinsz.c view
@@ -0,0 +1,9 @@+#include <sys/ioctl.h>++unsigned long c_get_window_size(void) {+ struct winsize w;+ if (ioctl (0, TIOCGWINSZ, &w) >= 0)+ return (w.ws_row << 16) + w.ws_col;+ else+ return 0x190050;+}
+ cbits/gwinsz.h view
@@ -0,0 +1,1 @@+unsigned long c_get_window_size(void);
+ sr-extra.cabal view
@@ -0,0 +1,67 @@+Name: sr-extra+Version: 1.46.3+License: BSD3+License-File: COPYING+Author: David Fox+Category: Unclassified+Description: A hodge-podge of functions and modules that do not have a better home+Maintainer: David Fox <david@seereason.com>+Homepage: http://src.seereason.com/haskell-extra+Synopsis: A grab bag of modules.+Build-Type: Simple+Cabal-Version: >= 1.2+flag network-uri+ Description: Get Network.URI from the network-uri package+ Default: True++Library+ Build-Depends:+ base < 5,+ bytestring,+ bzlib,+ containers,+ directory,+ filepath,+ HUnit,+ mtl,+ old-locale,+ old-time,+ pretty,+ process,+ pureMD5,+ QuickCheck >= 2 && < 3,+ random,+ regex-compat,+ time >= 1.1,+ unix,+ Unixutils >= 1.51,+ zlib+ if flag(network-uri)+ Build-Depends: network-uri >= 2.6+ else+ Build-Depends: network >= 2.4+ ghc-options: -O2 -W+ C-Sources: cbits/gwinsz.c+ Include-Dirs: cbits+ Install-Includes: gwinsz.h+ Exposed-modules:+ Extra.Bool,+ Extra.CIO,+ Extra.Either,+ Extra.Exit,+ Extra.Files,+ Extra.GPGSign,+ Extra.List,+ Extra.HughesPJ,+ Extra.Lock,+ Extra.Misc,+ Extra.Net,+ Extra.SSH,+ Extra.Time,+ Extra.Terminal,+ Extra.IO,+ Extra.URI,+ Extra.URIQuery,+ Test.QUnit,+ Test.QuickCheck.Properties,+ Extra.IOThread