spy (empty) → 0.4
raw patch · 7 files changed
+425/−0 lines, 7 filesdep +HUnitdep +QuickCheckdep +basesetup-changed
Dependencies added: HUnit, QuickCheck, base, cmdargs, directory, filemanip, filepath, hfsevents, json, process, test-framework, test-framework-hunit, test-framework-quickcheck2
Files
- LICENSE +27/−0
- README.md +89/−0
- Setup.hs +3/−0
- spy.cabal +62/−0
- src/Main.hs +46/−0
- src/Spy/Watcher.hs +142/−0
- tests/Tests.hs +56/−0
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright 2012 Stefan Saasen++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 REGENTS 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 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.
+ README.md view
@@ -0,0 +1,89 @@+Spy+===++Spy is a compact file system watcher for Mac OS X using the [File System Events API](https://developer.apple.com/library/mac/#documentation/Darwin/Conceptual/FSEvents_ProgGuide/Introduction/Introduction.html) via [hfsevents](https://github.com/luite/hfsevents).+++Usage+=====++Spy expects a single argument, the directory or a single file to watch. Spy currently supports two different modes: watch and run mode.++Watch mode+-----------++In output mode Spy will print the path to a modified file to STDOUT (followed by a newline) whenever a file modification (new file added, file modified, file deleted) occurs.++ $> spy watch .+ path/to/modified/file++Because watch is the default mode you can omit the `watch` command if you want:++ $> spy .++It's possible to watch a single file (this obviously only shows changes to that particular file):++ $> spy /path/to/file++The default format is the full path to the modified file followed by a newline. To make it easier to parse the output, the `--format=json` changes the output to be printed formatted as a JSON object (again followed by a newline).++ $> spy watch --format=json .+ {"path": "path/to/modified/file", "flags": ["ItemModified"], "id": 123143234}++For directories the following options apply:++An optional second argument can be used to filter the files in the given directory using a glob pattern:++ $> spy watch /path/to/directory "*.md"+++Run mode+--------++In run mode Spy will execute a given command whenever a file modification occurs without printing modifications to stdout. The command will be executed with the path to the modified file as the last argument.++ $> spy run "./run-build.sh"++In the example above the shell script `run-build.sh` would be executed with the path to the modified file as the first argument.++If the command to be executed does not expect any (additional) arguments the `--notify-only` flag can be used. This will cause spy to execute the command without passing the path as an argument:++ $> spy run --notify-only "rake test" .+++Installation+============++Spy only works on Mac OS X >= 10.7 (Lion and above)!++Binary distribution+-------------------++The binary distribution contains a 64bit binary compiled for Mac OS X > 10.7.++Download the tarball and run "make install" to copy the binary and the man page into the correct target directories:++ $> curl -OL https://bitbucket.org/ssaasen/spy/downloads/spy-osx-x86_64-v0.4.tar.gz+ $> tar xfz spy-osx-x86_64-v0.4.tar.gz+ $> cd spy+ $> make install++The user manual should now be available via `man spy` and the `spy` executable should be on your `$PATH`.+++Source distribution+-------------------++To install spy from source you need the Haskell platform installed and cabal-install available on your $PATH:++ $> cabal install --only-dependencies+ $> cabal configure+ $> cabal build++This will create the spy binary in the ./dist/build/spy directory.++To copy the spy binary to the cabal bin directory (which should be available on your PATH) use:++ $> cabal copy++
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple+main = defaultMain+
+ spy.cabal view
@@ -0,0 +1,62 @@+name: spy+version: 0.4+license: BSD3+license-file: LICENSE+author: Stefan Saasen+maintainer: stefan@saasen.me+synopsis: A compact file system watcher for Mac OS X+category: Apple, Development+description: Spy can be used to watch for file changes and to either report the modified files or run a command if files change. It can be used to trigger compilation, to run tests or start a deployment.+homepage: https://bitbucket.org/ssaasen/spy+bug-reports: https://bitbucket.org/ssaasen/spy/issues+cabal-version: >= 1.8+build-type: Simple+extra-source-files: README.md++source-repository head+ type: git+ location: git@bitbucket.org:ssaasen/spy.git++flag small_base+ description: Choose the new, split-up base package.++test-suite spy-testsuite+ type: exitcode-stdio-1.0+ main-is: Tests.hs+ hs-source-dirs: tests, src+ build-depends: base < 5 && >= 3,+ test-framework >= 0.3.3,+ test-framework-quickcheck2 >= 0.2.9,+ test-framework-hunit,+ HUnit,+ QuickCheck >= 2.4.0.1,+ hfsevents >= 0.1.3,+ cmdargs >= 0.10,+ filepath >= 1.3,+ filemanip >= 0.3.6.2,+ process >= 1.1,+ json >= 0.7,+ directory >= 1.1++executable spy+ main-is: Main.hs+ other-modules: Spy.Watcher+ build-depends:+ base < 5 && >= 3,+ hfsevents >= 0.1.3,+ cmdargs >= 0.10,+ filepath >= 1.3,+ filemanip >= 0.3.6.2,+ process >= 1.1,+ json >= 0.7,+ directory >= 1.1++ ghc-options:+ -Wall+ -rtsopts+ -fno-warn-unused-do-bind+ -fno-warn-missing-signatures++ hs-source-dirs:+ src+
+ src/Main.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE DeriveDataTypeable #-}+module Main where++import System.Console.CmdArgs+import System.Directory (canonicalizePath)+import Spy.Watcher++version :: String+version = "spy v0.4, (C) Stefan Saasen"++hiddenOpts x = x &= help "Set to true if hidden files/directories should be included" &= name "i" &= typ "BOOL"++watch :: Spy+watch = Watch+ {hidden = hiddenOpts False+ ,dir = "." &= argPos 0 &= typ "FILE/DIR"+ ,format = Just plainFormat &= name "f" &= help "Specify the output format ('json', 'plain')"+ ,glob = Nothing &= args &= typ "GLOB"+ }+ &= help "Watch a directory (or file) for file changes"+ &= auto++run :: Spy+run = Run+ {hidden = hiddenOpts False+ ,command = def &= argPos 0 &= typ "CMD"+ ,dir = "." &= argPos 1 &= typ "FILE/DIR"+ ,glob = Nothing &= args &= typ "GLOB"+ ,notifyOnly = False &= name "n" &= name "notify-only" &= typ "BOOL"+ }+ &= help "Run a command whenever a file changes"+++mode :: Mode (CmdArgs Spy)+mode = cmdArgsMode $ modes [watch,run]+ &= help "spy watches for file changes on the file system"+ &= program "spy" &= summary version+++main :: IO ()+main = do+ config <- cmdArgsRun mode+ canonicalizedDir <- canonicalizePath $ dir config+ spy config { dir = canonicalizedDir }+ putStrLn "No eyes on the target"+
+ src/Spy/Watcher.hs view
@@ -0,0 +1,142 @@+{-# LANGUAGE DeriveDataTypeable,RecordWildCards #-}++module Spy.Watcher+(+ spy+,Format+,plainFormat+,Spy(..)+,skipEvent+,matchesFile+,containsHiddenPathElement+) where++import System.OSX.FSEvents+import System.Console.CmdArgs+import System.Cmd+import System.Exit+import System.IO (stderr, hPrint)+import System.FilePath.GlobPattern+import System.FilePath (splitDirectories, takeFileName)+import Control.Monad (unless)+import Control.Exception (bracket)+import Data.Maybe (fromMaybe, maybeToList)+import Data.Bits+import Data.Word+import Text.JSON++-- | The output format when Spy prints out changes to STDOUT+data Format = Json | Plain deriving (Show, Eq, Data, Typeable)++-- | Spy run modes.+data Spy = Watch {+ dir :: FilePath+ ,glob :: Maybe GlobPattern+ ,format :: Maybe Format+ ,hidden :: Bool+} | Run {+ dir :: FilePath+ ,command :: String+ ,glob :: Maybe GlobPattern+ ,hidden :: Bool+ ,notifyOnly :: Bool+} deriving (Data,Typeable,Show,Eq)++-- | Return the Plain format.+plainFormat :: Format+plainFormat = Plain++-- | Register for FS events using the given Spy config.+spy :: Spy -> IO String+spy config = bracket+ (eventStreamCreate [dir config] 1.0 True True True $ handleEvent config)+ eventStreamDestroy+ (\_ -> getLine)++-- | Handle the FS event based on the current Spy run configuration+handleEvent :: Spy -> Event -> IO ()+handleEvent config@Run{..} event =+ unless (skipEvent config event) $+ runCommand command pathAsArg >>=+ \exit -> case exit of+ ExitSuccess -> return ()+ ExitFailure i -> hPrint stderr $ "Failed to execute " ++ command ++ " - exit code: " ++ show i+ where pathAsArg = if notifyOnly then+ Nothing+ else Just (eventPath event)++handleEvent config@Watch{..} event =+ unless (skipEvent config event) $+ putStrLn $ (outputHandler $ fromMaybe Plain format) event++-- =================================================================================++runCommand :: Command -> Maybe FilePath -> IO ExitCode+runCommand cmd maybePath = rawSystem p $ mergeArgs args'+ where (p, args') = case words cmd of+ (x:xs) -> (x, xs)+ _ -> ("", [])+ mergeArgs defaultArgs = defaultArgs ++ maybeToList maybePath++-- =================================================================================++type Printer = (Event -> String)+type Command = String++outputHandler :: Format -> Printer+outputHandler Json = \event -> encode $ makeObj [+ ("path", showJSON $ eventPath event),+ ("id", showJSON $ eventId event),+ ("flags", showJSONs $ showEventFlags $ eventFlags event)]+outputHandler Plain = eventPath+++-- | Skip events based on the configuration given+skipEvent :: Spy -> Event -> Bool+skipEvent config event = skipHidden || skipNonMatchingGlob+ where skipHidden = let includeHiddenfiles = hidden config+ in not includeHiddenfiles && containsHiddenPathElement path+ skipNonMatchingGlob = maybe False (not . matchesFile path) $ glob config+ path = eventPath event+++matchesFile :: FilePath -> GlobPattern -> Bool+matchesFile path glob' = takeFileName path ~~ glob'++containsHiddenPathElement :: FilePath -> Bool+containsHiddenPathElement path = any isHidden paths+ where paths = splitDirectories path+ isHidden name' = case name' of+ (x:_) -> x == '.'+ _ -> False++-- =================================================================================+-- The following code is taken from:+-- https://github.com/luite/hfsevents/blob/master/test/trace.hs+-- Copyright (c) 2012, Luite Stegeman+showEventFlags :: Word64 -> [String]+showEventFlags fl = map fst . filter hasFlag $ flagList+ where hasFlag (_,f) = fl .&. f /= 0+++flagList :: [(String, Word64)]+flagList = [ ("MustScanSubDirs" , 0x00000001)+ , ("UserDropped" , 0x00000002)+ , ("KernelDropped" , 0x00000004)+ , ("EventIdsWrapped" , 0x00000008)+ , ("HistoryDone" , 0x00000010)+ , ("RootChanged" , 0x00000020)+ , ("Mount" , 0x00000040)+ , ("Unmount" , 0x00000080)+ , ("ItemCreated" , 0x00000100)+ , ("ItemRemoved" , 0x00000200)+ , ("ItemInodeMetaMod" , 0x00000400)+ , ("ItemRenamed" , 0x00000800)+ , ("ItemModified" , 0x00001000)+ , ("ItemFinderInfoMod" , 0x00002000)+ , ("ItemChangeOwner" , 0x00004000)+ , ("ItemXattrMod" , 0x00008000)+ , ("ItemIsFile" , 0x00010000)+ , ("ItemIsDir" , 0x00020000)+ , ("ItemIsSymlink" , 0x00040000)+ ]
+ tests/Tests.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import qualified Test.HUnit as H+import Data.Maybe+import System.OSX.FSEvents+import Spy.Watcher+import Test.QuickCheck hiding ((.&.))+import Test.Framework (Test, defaultMain, testGroup)+import Test.Framework.Providers.QuickCheck2 (testProperty)+import Test.Framework.Providers.HUnit+++test_globMatchesFile = H.assertBool+ "*.hs should match a Haskell source file"+ (matchesFile "/path/to/Main.hs" "*.hs")++test_globDoesNotMatchFile = H.assertBool+ "*.hs should not match a C source file"+ (not $ matchesFile "/path/to/Main.c" "*.hs")++test_skipEventHidden = H.assertBool+ "Skip path if hidden directory and showing hidden files is not enabled"+ (skipEvent (mockWatch {hidden = False}) (mockEvent "/a/b/.git/refs"))++test_containsHiddenPathElement = H.assertBool+ "Should identify hidden directory"+ (containsHiddenPathElement "/a/b/.git/info/exclude")++-- ===========================================================+mockWatch :: Spy+mockWatch = Watch { dir = ".", glob = Nothing, format = Nothing, hidden = False }++mockEvent :: FilePath -> Event+mockEvent path = Event path 0x00000001 0x00000001+------------------------------------------------------------------------+-- Test harness++main :: IO ()+main = defaultMain tests++tests :: [Test]+tests =+ [ testGroup "Watcher"+ [+ testCase "watcher/shouldMatchFilePath" test_globMatchesFile,+ testCase "watcher/shouldNotMatchFilePath" test_globDoesNotMatchFile+ ],+ testGroup "Watcher opts"+ [+ testCase "watcher/optHidden" test_skipEventHidden,+ testCase "watcher/containsHiddenPathElement" test_containsHiddenPathElement+ ]+ ]+