shh (empty) → 0.1.0.0
raw patch · 10 files changed
+832/−0 lines, 10 filesdep +asyncdep +basedep +deepseqsetup-changed
Dependencies added: async, base, deepseq, directory, filepath, mtl, process, shh, split, tasty, tasty-hunit, tasty-quickcheck, template-haskell, unix
Files
- ChangeLog.md +5/−0
- LICENSE +30/−0
- README.md +118/−0
- Setup.hs +2/−0
- app/Example.hs +15/−0
- shh.cabal +61/−0
- src/Shh.hs +55/−0
- src/Shh/Example.hs +7/−0
- src/Shh/Internal.hs +460/−0
- test/Test.hs +79/−0
+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Revision history for execpath++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2017, Luke Clifton++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.++ * Neither the name of Luke Clifton nor the names of other+ contributors may 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.
+ README.md view
@@ -0,0 +1,118 @@+# Shh++Shh is a library to enable convinient shell-like programming in Haskell.+It works well in scripts, and from GHCi, allowing you to use GHCi as a shell.++It supports++ * Automatically defining a function for each executable on your `$PATH`+ using template Haskell.++ * Redirction of stdout and stderr+ + -- Redirect stdout+ λ echo "Hello" &> StdErr+ λ echo "Hello" &> Truncate ".tmp_file"++ -- Redirect stderr+ λ echo "Hello" &!> Append "/dev/null"+ λ echo "Hello" &!> StdOut+++ * Piping stdout or stderr to the input of a chained process+ + λ cat "/dev/urandom" |> xxd |> head "-n" 5++ * Multiple processes sequentially feeding a single process++ λ (echo 1 >> echo 2) |> cat++ * Use of Haskells concurrency primitives.++ λ race (sleep 1) $ curl "http://this_needs_to_timeout_after_1_second"++ λ d <- readTrim $ mktemp "-d"+ λ :{+ | System.Directory.withCurrentDirectory d $ do+ | mapConcurrently_ (curl "-LOJs")+ | [ "https://raw.githubusercontent.com/luke-clifton/shh/master/shell.nix"+ | , "https://raw.githubusercontent.com/luke-clifton/shh/master/shh.cabal"+ | ]+ | ls+ | :}++ * Capturing of process output++ λ loggedIn <- nub . words <$> readProc users+ λ putStrLn $ "Logged in users: " ++ show loggedIn++ λ mapM_ putStrLn =<< readSplit0 (Shh.Example.find "-maxdepth" 1 "-print0")++ * Capturing infinite output of a process lazily++ λ withRead (cat "/dev/urandom" |> xxd) $ mapM_ putStrLn . take 3 . lines+ 00000000: 8fcb ebee 9228 a897 3bfc 1d05 491d aceb .....(..;...I...+ 00000010: 47de 3ea3 2788 44ac 9b85 0a0f a458 b949 G.>.'.D......X.I+ 00000020: 5308 ddfe 5790 5a5f 39e3 bbb6 b689 2b03 S...W.Z_9.....+.++ * Write strings to stdin of a process.++ λ writeProc cat "Hello\n"+ Hello++ λ "Hello" >>> shasum+ f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0 -++ λ shasum <<< "Hello"+ f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0 -++ * Proper exceptions, when a process exits with a failure code, an exception+ is thrown. You can catch these normally. The exception includes the error+ code, the command, and all it's arguments.++ λ false "Ha, it died"+ *** Exception: Command `false "Ha, it died"` failed [exit 1]++ λ catchCode false+ 1++## Mnemonics ++Shh has many symbols that might seem intimidating at first, but there+is a simple mnemonic for them.++ | Piping. Looks like a pipe, same as in POSIX shells.+ & Redirection, think of the shell `2>&1`+ >,< The direction of flow of a command+ ! Operate on stderr instead of stdout++So, for example,++ ls |> cat Pipe the stdout of `ls` into stdin of `cat`+ cat <| ls Same as above+ ls &> StdErr Redirect stdout of `ls` to wherever stderr is going.+ StdErr <& ls Same as above+ ls &!> StdOut Redirect stderr of `ls` to wherever stdout is going.+ StdOut <!& ls Same as above++## Usage++Enable Temlpate Haskell and load the environment++ {-# LANGUAGE TemplateHaskell #-}+ $(loadEnv)++You now have all your executables available as simple to read+Haskell functions.++### Usage in GHCi++If you want `^D` to be recognised as a EOF marker (when running commands+that read from stdin) when running in GHCi, you will need to run the+`initInteractive` function. This sets the line buffering appropriately and+ensures the terminal is in canonical mode.++### Script Usage++TODO: Fill this in once on Hackage.+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Example.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ExtendedDefaultRules #-}++module Main where++import Control.Monad+import Shh+import Data.Monoid+import Data.Char+import Data.List++$(loadEnv)++main :: IO ()+main = (sleep 1 >> echo "Hello" >> sleep 2) |> cat
+ shh.cabal view
@@ -0,0 +1,61 @@+name: shh+version: 0.1.0.0+synopsis: Simple shell scripting from Haskell+description: Provides a shell scripting environment for Haskell. It+ helps you all external binaries, and allows you to+ perform many shell-like functions, such as piping+ and redirection.+license: BSD3+license-file: LICENSE+author: Luke Clifton+maintainer: lukec@themk.net+copyright: (c) 2018 Luke Clifton+category: System+build-type: Simple+extra-source-files: ChangeLog.md, README.md+cabal-version: >=1.10++source-repository head+ type: git+ location: https://github.com/luke-clifton/shh++library+ exposed-modules: Shh+ , Shh.Example+ , Shh.Internal+ build-depends: base >=4.9 && <4.12+ , async+ , deepseq+ , directory+ , filepath+ , mtl+ , process+ , split+ , template-haskell+ , unix+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall++executable shh-example+ ghc-options: -threaded -with-rtsopts=-N+ default-language: Haskell2010+ build-depends:+ base >=4.9,+ shh+ hs-source-dirs: app+ main-is: Example.hs+ ++test-suite shh-tests+ ghc-options: -threaded -with-rtsopts=-N+ default-language: Haskell2010+ build-depends:+ base >=4.9,+ tasty,+ tasty-quickcheck,+ tasty-hunit,+ shh+ hs-source-dirs: test+ main-is: Test.hs+ type: exitcode-stdio-1.0
+ src/Shh.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE GADTs #-}+-- | Shh provides a shell-like environment for Haskell.+module Shh+ ( initInteractive+ -- | == Constructing a `Proc`+ -- You will rarely have to use these as most of the time these are+ -- created for you by using the `loadEnv` template Haskell function.+ , exe+ , mkProc+ , runProc+ , Proc()+ -- | == Piping and Redirection+ , PipeResult(..)+ , (<|)+ , Stream(..)+ , devNull+ -- | == Reading @stdout@+ , readProc+ , withRead+ , trim+ , readTrim+ , split0+ , readSplit0+ , readLines+ , readAuto+ -- | == Writing to @stdin@+ , writeProc+ , (<<<), (>>>)+ , readWriteProc+ , mapP+ -- | == Exceptions+ -- If any exception is allowed to propagate out of a pipeline, all the+ -- processes comprising the pipeline will be terminated. This is contrary+ -- to how a shell normally works (even with @-o pipefail@!).+ , Failure(..)+ , ignoreFailure+ , catchFailure+ , catchCode+ -- | == Constructing Arguments+ , ExecArg(..)+ -- | == Template Haskell helpers+ , loadEnv+ , loadAnnotatedEnv+ , loadExe+ , loadExeAs+ ) where++import Shh.Internal
+ src/Shh/Example.hs view
@@ -0,0 +1,7 @@+{-# LANGUAGE TemplateHaskell #-}++module Shh.Example where++import Shh++$(loadAnnotatedEnv (\s -> s ++ "_"))
+ src/Shh/Internal.hs view
@@ -0,0 +1,460 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE GADTs #-}++-- | See documentation for "Shh".+module Shh.Internal where+++import Control.Concurrent.Async+import Control.DeepSeq (rnf)+import Control.Exception as C+import Control.Monad+import Control.Monad.IO.Class+import Data.Char (isLower, isSpace, isAlphaNum)+import Data.List (nub, dropWhileEnd, intercalate)+import Data.List.Split (endBy, splitOn)+import Data.Maybe (mapMaybe)+import Language.Haskell.TH+import System.Directory (doesDirectoryExist, getDirectoryContents)+import System.Environment (getEnv)+import System.Exit (ExitCode(..))+import System.IO+import System.Posix.Signals+import System.Process++-- | This function needs to be called in order to use the library succesfully+-- from GHCi.+initInteractive :: IO ()+initInteractive = do+ hSetBuffering stdin LineBuffering++-- | When a process exits with a non-zero exit code+-- we throw this Failure exception.+--+-- The only exception to this is when a process is terminated+-- by SIGPIPE in a pipeline, in which case we ignore it.+data Failure = Failure+ { failureProg :: String+ , failureArgs :: [String]+ , failureCode :: Int+ } deriving (Eq, Ord)++instance Show Failure where+ show f = concat $+ [ "Command `"+ ]+ ++ [intercalate " " (failureProg f : map show (failureArgs f))]+ +++ [ "` failed [exit "+ , show (failureCode f)+ , "]"+ ]++instance Exception Failure++-- | This class is used to allow most of the operators in Shh to be+-- polymorphic in their return value. This makes using them in an `IO`+-- context easier (we can avoid having to prepend everything with a+-- `runProc`).+class PipeResult f where+ -- | Use this to send the output of on process into the input of another.+ -- This is just like a shells `|` operator.+ --+ -- The result is polymorphic in it's output, and can result in either+ -- another `Proc a` or an `IO a` depending on the context in which it is+ -- used.+ --+ -- >>> echo "Hello" |> wc+ -- 1 1 6+ (|>) :: Proc a -> Proc a -> f a++ -- | Similar to `|!>` except that it connects stderr to stdin of the+ -- next process in the chain.+ --+ -- NB: The next command to be `|>` on will recapture the stdout of+ -- both preceding processes, because they are both going to the same+ -- handle!+ -- + -- This is probably not what you want, see the `&>` and `&!>` operators+ -- for redirection.+ (|!>) :: Proc a -> Proc a -> f a++ -- | Redirect stdout of this process to another location+ --+ -- > ls &> Append "/dev/null"+ (&>) :: Proc a -> Stream -> f a++ -- | Redirect stderr of this process to another location+ --+ -- > ls &!> StdOut+ (&!>) :: Proc a -> Stream -> f a++-- | Flipped version of `|>`+(<|) :: PipeResult f => Proc a -> Proc a -> f a+(<|) = flip (|>)++instance PipeResult IO where+ a |> b = runProc $ a |> b+ a |!> b = runProc $ a |!> b+ a &> s = runProc $ a &> s+ a &!> s = runProc $ a &!> s++-- | Create a pipe, and close both ends on exception.+withPipe :: (Handle -> Handle -> IO a) -> IO a+withPipe k =+ bracket+ createPipe+ (\(r,w) -> hClose r `finally` hClose w)+ (\(r,w) -> k r w)++instance PipeResult Proc where+ (Proc a) |> (Proc b) = Proc $ \i o e pl pw ->+ withPipe $ \r w -> do+ a' <- async $ a i w e (pure ()) (hClose w)+ b' <- async $ b r o e (pure ()) (hClose r)+ link2 a' b'+ (_, br) <- (pl >> waitBoth a' b') `finally` pw+ pure br++ (Proc a) |!> (Proc b) = Proc $ \i o e pl pw -> do+ withPipe $ \r w -> do+ a' <- async $ a i o w (pure ()) (hClose w)+ b' <- async $ b r o e (pure ()) (hClose r)+ link2 a' b'+ (_, br) <- (pl >> waitBoth a' b') `finally` pw+ pure br++ p &> StdOut = p+ (Proc f) &> StdErr = Proc $ \i _ e pl pw -> f i e e pl pw+ (Proc f) &> (Truncate path) = Proc $ \i _ e pl pw ->+ withBinaryFile path WriteMode $ \h -> f i h e pl pw+ (Proc f) &> (Append path) = Proc $ \i _ e pl pw ->+ withBinaryFile path AppendMode $ \h -> f i h e pl pw++ p &!> StdErr = p+ (Proc f) &!> StdOut = Proc $ \i o _ pl pw -> f i o o pl pw+ (Proc f) &!> (Truncate path) = Proc $ \i o _ pl pw ->+ withBinaryFile path WriteMode $ \h -> f i o h pl pw+ (Proc f) &!> (Append path) = Proc $ \i o _ pl pw ->+ withBinaryFile path AppendMode $ \h -> f i o h pl pw+ ++-- | Type used to represent destinations for redirects. @`Truncate` file@+-- is like @> file@ in a shell, and @`Append` file@ is like @>> file@.+data Stream = StdOut | StdErr | Truncate FilePath | Append FilePath++-- | Shortcut for @`Truncate` "\/dev\/null"@+devNull :: Stream+devNull = Truncate "/dev/null"++-- | Type representing a series or pipeline (or both) of shell commands.+newtype Proc a = Proc (Handle -> Handle -> Handle -> IO () -> IO () -> IO a)+ deriving Functor++instance MonadIO Proc where+ liftIO a = Proc $ \_ _ _ pl pw -> do+ (pl >> a) `finally` pw++-- | The `Semigroup` instance for `Proc` pipes the stdout of one process+-- into the stdin of the next. However, consider using `|>` instead which+-- behaves when used in an `IO` context. If you use `<>` in an IO monad+-- you will be using the `IO` instance of semigroup which is a sequential+-- execution. `|>` prevents that error.+instance Semigroup (Proc a) where+ (<>) = (|>)++instance (a ~ ()) => Monoid (Proc a) where+ mempty = Proc $ \_ _ _ pl pw -> pl `finally` pw++instance Applicative Proc where+ pure a = Proc $ \_ _ _ pw pl -> do+ pw `finally` pl+ pure a++ f <*> a = do+ f' <- f+ a' <- a+ pure (f' a')+ ++instance Monad Proc where+ (Proc a) >>= f = Proc $ \i o e pl pw -> do+ ar <- a i o e pl (pure ())+ let+ Proc f' = f ar+ f' i o e (pure ()) pw++-- | Run's a `Proc` in `IO`. This is usually not required, as most+-- commands in Shh are polymorphic in their return type, and work+-- just fine in `IO` directly.+runProc :: Proc a -> IO a+runProc (Proc f) = f stdin stdout stderr (pure ()) (pure ())++-- | Create a `Proc` from a command and a list of arguments.+mkProc :: String -> [String] -> Proc ()+mkProc cmd args = Proc $ \i o e pl pw -> do+ bracket+ (createProcess_ cmd (proc cmd args)+ { std_in = UseHandle i+ , std_out = UseHandle o+ , std_err = UseHandle e+ , close_fds = True+ }+ )+ (\(_,_,_,ph) -> terminateProcess ph)+ $ \(_,_,_,ph) -> do+ pl+ (waitProc cmd args ph `onException` terminateProcess ph) `finally` pw+++-- | Read the stdout of a `Proc` in any `MonadIO` (including other `Proc`s).+-- This is strict, so the whole output is read into a `String`. See `withRead`+-- for a lazy version that can be used for streaming.+readProc :: MonadIO io => Proc a -> io String+readProc (Proc f) = liftIO $ do+ (r,w) <- createPipe+ (_,o) <- concurrently+ (f stdin w stderr (pure ()) (hClose w))+ (do+ output <- hGetContents r+ C.evaluate $ rnf output+ hClose r+ pure output+ )+ pure o++-- | Run a process and capture it's output lazily. Once the continuation+-- is completed, the handles are closed, and the process is terminated.+withRead :: MonadIO io => Proc a -> (String -> IO b) -> io b+withRead (Proc f) k = liftIO $ + withPipe $ \r w -> do+ withAsync (f stdin w stderr (pure ()) (hClose w)) $ \_ ->+ (hGetContents r >>= k) `finally` hClose r+++-- | Read and write to a `Proc`...+readWriteProc :: MonadIO io => Proc a -> String -> io String+readWriteProc (Proc f) input = liftIO $ do+ (ri,wi) <- createPipe+ (ro,wo) <- createPipe+ (_,o) <- concurrently+ (concurrently+ (f ri wo stderr (pure ()) (hClose wo `finally` hClose ri))+ (hPutStr wi input `finally` hClose wi)+ ) (do+ output <- hGetContents ro+ C.evaluate $ rnf output+ hClose ro+ pure output+ )+ pure o++-- | Some as `readWriteProc`. Map a `Proc` over a `String`.+--+-- >>> mapP shasum "Hello"+-- "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0 -\n"+mapP :: MonadIO io => Proc a -> String -> io String+mapP = readWriteProc++-- | Provide the stdin of a `Proc` from a `String`+writeProc :: MonadIO io => Proc a -> String -> io a+writeProc (Proc f) input = liftIO $ do+ (r,w) <- createPipe+ fst <$> concurrently+ (f r stdout stderr (pure ()) (hClose r))+ (hPutStr w input `finally` hClose w)++-- | Flipped, infix version of `writeProc`+(>>>) :: MonadIO io => String -> Proc a -> io a+(>>>) = flip writeProc+++-- | Infix version of `writeProc`+(<<<) :: MonadIO io => Proc a -> String -> io a+(<<<) = writeProc++-- | What on a given `ProcessHandle`, and throw an exception of+-- type `Failure` if it's exit code is non-zero (ignoring SIGPIPE)+waitProc :: String -> [String] -> ProcessHandle -> IO ()+waitProc cmd arg ph = waitForProcess ph >>= \case+ ExitFailure c+ | fromIntegral c == negate sigPIPE -> pure ()+ | otherwise -> throwIO $ Failure cmd arg c+ ExitSuccess -> pure ()++-- | Trim leading and tailing whitespace.+trim :: String -> String+trim = dropWhileEnd isSpace . dropWhile isSpace++-- | Allow us to catch `Failure` exceptions in `IO` and `Proc`+class ProcFailure m where+ -- | Run a `Proc` action, catching an `Failure` exceptions+ -- and returning them.+ catchFailure :: Proc a -> m (Either Failure a)++instance ProcFailure Proc where+ catchFailure (Proc f) = Proc $ \i o e pl pw -> do+ try $ f i o e pl pw++instance ProcFailure IO where+ catchFailure = runProc . catchFailure++-- | Run a `Proc` action, ignoring any `Failure` exceptions.+-- This can be used to prevent a process from interrupting a whole pipeline.+--+-- >>> false `|>` (sleep 2 >> echo 1)+-- *** Exception: Command `false` failed [exit 1]+--+-- >>> (ignoreFailure false) `|>` (sleep 2 >> echo 1)+-- 1+ignoreFailure :: (Functor m, ProcFailure m) => Proc a -> m ()+ignoreFailure = void . catchFailure++-- | Run an `Proc` action returning the return code if an+-- exception was thrown, and 0 if it wasn't.+catchCode :: (Functor m, ProcFailure m) => Proc a -> m Int+catchCode = fmap getCode . catchFailure+ where+ getCode (Right _) = 0+ getCode (Left f) = failureCode f++-- | Like `readProc`, but trim leading and tailing whitespace.+readTrim :: MonadIO io => Proc a -> io String+readTrim = fmap trim . readProc+++-- | A class for things that can be converted to arguments on the command+-- line. The default implementation is to use `show`.+class ExecArg a where+ asArg :: a -> [String]+ default asArg :: Show a => a -> [String]+ asArg a = [show a]++instance ExecArg String where+ asArg s = [s]++-- TODO: Determine if `Char` flags should be prepended with a '-' or not. +-- instance ExecArg Char where+-- asArg c = [['-', c]]++instance ExecArg Int+instance ExecArg Integer+instance ExecArg Word++-- | A class for building up a command+class ExecArgs a where+ toArgs :: [String] -> a++instance ExecArgs (Proc ()) where+ toArgs (cmd:args) = mkProc cmd args+ toArgs _ = error "The impossible happened. How did you construct this?"++instance (ExecArg b, ExecArgs a) => ExecArgs (b -> a) where+ toArgs f i = toArgs $ f ++ asArg i++-- | Commands can be executed directly in IO+instance ExecArgs (IO ()) where+ toArgs = runProc . toArgs++-- | Force a `()` result.+class Unit a+instance {-# OVERLAPPING #-} Unit b => Unit (a -> b)+instance {-# OVERLAPPABLE #-} a ~ () => Unit (m a)++-- | Get all files in a directory on your `$PATH`.+--+-- TODO: Check for executability.+pathBins :: IO [FilePath]+pathBins = do+ pathsVar <- splitOn ":" <$> getEnv "PATH"+ paths <- filterM doesDirectoryExist pathsVar+ nub . concat <$> mapM getDirectoryContents paths++-- | Execute the given command. Further arguments can be passed in.+--+-- > exe "ls" "-l"+--+-- See also `loadExe` and `loadEnv`.+exe :: (Unit a, ExecArgs a) => String -> a+exe s = toArgs [s]++-- | Create a function for the executable named+loadExe :: String -> Q [Dec]+loadExe s = loadExeAs s s++-- | @$(loadExeAs fnName executable)@ defines a function called @fnName@+-- which executes the path in @executable@.+loadExeAs :: String -> String -> Q [Dec]+loadExeAs fnName executable =+ -- TODO: Can we place haddock markup in TH generated functions.+ -- TODO: Can we palce the man page for each function in there xD+ -- https://ghc.haskell.org/trac/ghc/ticket/5467+ let+ name = mkName $ fnName+ impl = valD (varP name) (normalB [|+ exe executable+ |]) []+ typn = mkName "a"+ typ = SigD name (ForallT [PlainTV typn] [AppT (ConT ''Unit) (VarT typn), AppT (ConT ''ExecArgs) (VarT typn)] (VarT typn))+ in do+ i <- impl+ return $ [typ,i]++-- | Checks if a String is a valid Haskell identifier.+validIdentifier :: String -> Bool+validIdentifier "" = False+validIdentifier ident = isValidInit (head ident) && all isValidC ident && isNotIdent+ where+ isValidInit c = isLower c || c `elem` "_"+ isValidC c = isAlphaNum c || c `elem` "_'"+ isNotIdent = not $ ident `elem`+ [ "import", "if", "else", "then", "do", "in", "let", "type"+ , "as", "case", "of", "class", "data", "default", "deriving"+ , "instance", "forall", "foreign", "hiding", "infix", "infixl"+ , "infixr", "mdo", "module", "newtype", "proc", "qualified"+ , "rec", "type", "where"]++-- | Scans your '$PATH' environment variable and creates a function for each+-- executable found. Binaries that would not create valid Haskell identifiers+-- are ignored.+loadEnv :: Q [Dec]+loadEnv = loadAnnotatedEnv id++-- | Like `loadEnv`, but allows you to modify the function name that would+-- be generated.+loadAnnotatedEnv :: (String -> String) -> Q [Dec]+loadAnnotatedEnv f = do+ bins <- runIO pathBins+ let pairs = mapMaybe getAnnotation bins+ fmap join $ mapM (uncurry loadExeAs) pairs++ where+ getAnnotation :: String -> Maybe (String,String)+ getAnnotation s+ | validIdentifier (f s) = Just (f s, s)+ | otherwise = Nothing++-- | Function that splits '\0' seperated list of strings.+split0 :: String -> [String]+split0 = endBy "\0"++-- | A convinience function for reading in a @"\\NUL"@ seperated list of+-- strings. This is commonly used when dealing with paths.+--+-- > readSplit0 $ find "-print0"+readSplit0 :: Proc () -> IO [String]+readSplit0 p = split0 <$> readProc p++-- | A convinience function for reading the output lines of a `Proc`.+readLines :: Proc () -> IO [String]+readLines p = lines <$> readProc p++-- | Like `readProc`, but attempts to `Prelude.read` the result.+readAuto :: Read a => Proc () -> IO a+readAuto p = read <$> readProc p
+ test/Test.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ExtendedDefaultRules #-}+module Main where++import Shh+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++$(loadEnv)++main = do+ putStrLn "################################################"+ putStrLn " These tests require that certain binaries"+ putStrLn " exist on your $PATH. If you are getting"+ putStrLn " failures, please check that it's not because"+ putStrLn " they are missing."+ putStrLn "################################################"+ defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [unitTests, properties]++properties :: TestTree+properties = testGroup "Properties"+ [ testProperty "trim = trim . trim" $ \l -> trim l == trim (trim l)+ ]++unitTests :: TestTree+unitTests = testGroup "Unit tests"+ [ testCase "Read stdout" $ do+ l <- readProc $ echo "test"+ l @?= "test\n"+ , testCase "Redirect to /dev/null" $ do+ l <- readProc $ echo "test" &> devNull+ l @?= ""+ , testCase "Redirect to file (Truncate)" $ do+ t <- trim <$> readProc mktemp+ echo "test" &> Truncate t+ r <- readProc $ cat t+ rm t+ "test\n" @?= r+ , testCase "Redirect to file (Append)" $ do+ t <- readTrim mktemp+ echo "test" &> Truncate t+ echo "test" &> Append t+ r <- readProc $ cat t+ rm t+ "test\ntest\n" @?= r+ , testCase "Long pipe" $ do+ r <- readProc $ echo "test" |> shasum |> shasum |> shasum+ r @?= "3f18e7bc4021e72e52fc1395ece85d82f912a74a -\n"+ , testCase "Pipe stderr" $ do+ r <- readProc $ echo "test" &> StdErr |!> cat+ r @?= "test\n"+ , testCase "Lazy read" $ do+ withRead (cat "/dev/urandom" |> xxd) $ \s -> do+ take 10 s @?= "00000000: "+ , testCase "Multiple outputs" $ do+ l <- readProc $ (echo (1 :: Int) >> echo (2 :: Int)) |> cat+ l @?= "1\n2\n"+ , testCase "Terminate upstream processes" $ do+ Left x <- catchFailure (mkProc "false" ["dummy"] |> (sleep 1 >> false "Didn't kill"))+ x @?= Shh.Failure "false" ["dummy"] 1+ , testCase "Write to process" $ do+ t <- readTrim mktemp+ writeProc (cat &> Truncate t) "Hello"+ r <- readProc (cat t)+ r @?= "Hello"+ writeProc (cat &> Truncate t) "Goodbye"+ r <- readProc (cat t)+ r @?= "Goodbye"+ , testCase "mapP" $ do+ r <- mapP shasum "test"+ r @?= "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 -\n"+ , testCase "ignoreFailure" $ do+ r <- readProc $ ignoreFailure false |> echo "Hello"+ r @?= "Hello\n"+ ]