packages feed

streamly-process 0.3.0 → 0.3.1

raw patch · 11 files changed

+531/−289 lines, 11 filesdep ~streamlydep ~streamly-core

Dependency ranges changed: streamly, streamly-core

Files

Benchmark/System/Process.hs view
@@ -23,7 +23,6 @@  -- Internal imports import qualified Streamly.Internal.FileSystem.Handle as FH-import qualified Streamly.Internal.System.Process as Proc  -- XXX replace with streamly versions once they are fixed {-# INLINE rights #-}
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog +## 0.3.1 (Dec 2023)++* Allow streamly-0.10.0 and streamly-core-0.2.0+* Fix a bug in quote escaping in the Command module+* Add APIs in System.Process and System.Command module with ability to set+  process attributes.+ ## 0.3.0 (Apr 2023)  * Added a `Streamly.System.Command` module
README.md view
@@ -33,8 +33,8 @@    Command.toBytes [str|echo "hello world"|] -- Stream IO Word8  & Command.pipeBytes [str|tr [a-z] [A-Z]|]   -- Stream IO Word8  & Stream.fold Stdio.write                   -- IO ()- :}- HELLO WORLD+:}+HELLO WORLD ```  ## Shell commands as streaming functions@@ -45,8 +45,8 @@ >>> :{    Command.toBytes [str|sh "-c" "echo 'hello world' | tr [a-z] [A-Z]"|] -- Stream IO Word8  & Stream.fold Stdio.write                                              -- IO ()- :}- HELLO WORLD+:}+HELLO WORLD ```  ## Running Commands Concurrently@@ -61,7 +61,7 @@    Command.toBytes [str|grep -H "pattern" #{file}|]             -- Stream IO Word8  & Stream.handle (\(_ :: Command.ProcessFailure) -> Stream.nil) -- Stream IO Word8  & Stream.foldMany (Fold.takeEndBy (== 10) Array.write)         -- Stream IO (Array Word8)- :}+:}  >>> :{ pgrep =
+ src/DocTestCommand.hs view
@@ -0,0 +1,20 @@+{- $setup++>>> :set -XFlexibleContexts+>>> :set -XQuasiQuotes+>>> import Data.Char (toUpper)+>>> import Data.Function ((&))+>>> import Streamly.Unicode.String (str)+>>> import qualified Streamly.Console.Stdio as Stdio+>>> import qualified Streamly.Data.Array as Array+>>> import qualified Streamly.Data.Fold as Fold+>>> import qualified Streamly.Data.Stream.Prelude as Stream+>>> import qualified Streamly.System.Command as Command+>>> import qualified Streamly.Unicode.Stream as Unicode++For APIs that have not been released yet.++>>> import qualified Streamly.Internal.Console.Stdio as Stdio (putBytes, putChars, putChunks)+>>> import qualified Streamly.Internal.FileSystem.Dir as Dir (readFiles)+>>> import qualified Streamly.Internal.System.Process as Process+-}
+ src/DocTestProcess.hs view
@@ -0,0 +1,20 @@+{- $setup++>>> :set -XFlexibleContexts+>>> :set -XScopedTypeVariables+>>> import Data.Char (toUpper)+>>> import Data.Function ((&))+>>> import qualified Streamly.Console.Stdio as Stdio+>>> import qualified Streamly.Data.Array as Array+>>> import qualified Streamly.Data.Fold as Fold+>>> import qualified Streamly.Data.Stream.Prelude as Stream+>>> import qualified Streamly.System.Process as Process+>>> import qualified Streamly.Unicode.Stream as Unicode++For APIs that have not been released yet.++>>> import qualified Streamly.Internal.Console.Stdio as Stdio (putChars, putChunks)+>>> import qualified Streamly.Internal.FileSystem.Dir as Dir (readFiles)+>>> import qualified Streamly.Internal.System.Process as Process+>>> import qualified Streamly.Internal.Unicode.Stream as Unicode (lines)+-}
src/Streamly/Internal/System/Command.hs view
@@ -15,6 +15,7 @@     -- * Generation       toBytes     , toChunks+    , toChunksWith     , toChars     , toLines @@ -27,8 +28,15 @@     , pipeBytes     , pipeChars     , pipeChunks+    , pipeChunksWith +    -- * Standalone Processes+    , standalone+    , foreground+    , daemon+     -- * Helpers+    , quotedWord     , runWith     , streamWith     , pipeWith@@ -42,6 +50,9 @@ import Streamly.Data.Fold (Fold) import Streamly.Data.Parser (Parser) import Streamly.Data.Stream.Prelude (MonadAsync, Stream)+import Streamly.Internal.System.Process (Config)+import System.Exit (ExitCode(..))+import System.Process (ProcessHandle)  import qualified Streamly.Data.Fold as Fold import qualified Streamly.Data.Parser as Parser@@ -50,23 +61,18 @@  -- Keep it synced with the released module --- $setup--- >>> :set -XFlexibleContexts--- >>> :set -XQuasiQuotes--- >>> import Data.Char (toUpper)--- >>> import Data.Function ((&))--- >>> import Streamly.Unicode.String (str)--- >>> import qualified Streamly.Data.Array as Array--- >>> import qualified Streamly.Console.Stdio as Stdio--- >>> import qualified Streamly.Data.Fold as Fold--- >>> import qualified Streamly.Data.Stream.Prelude as Stream--- >>> import qualified Streamly.System.Command as Command--- >>> import qualified Streamly.Unicode.Stream as Unicode------ >>> import qualified Streamly.Internal.System.Process as Process--- >>> import qualified Streamly.Internal.Console.Stdio as Stdio--- >>> import qualified Streamly.Internal.FileSystem.Dir as Dir+#include "DocTestCommand.hs" +-- | Posix compliant quote escaping:+--+-- $ echo 'hello\\"world'+-- hello\\"world+--+-- $ echo "hello\"\\w\'orld"+-- hello"\w\'orld+--+-- $ echo 'hello\'+-- hello\ {-# INLINE quotedWord #-} quotedWord :: MonadCatch m => Parser Char m String quotedWord =@@ -75,7 +81,15 @@                 '"' -> Just x                 '\'' -> Just x                 _ -> Nothing-        trEsc q x = if q == x then Just x else Nothing+        -- Inside ",+        -- \\ is translated to \+        -- \" is translated to "+        trEsc '"' x =+            case x of+                '\\' -> Just '\\'+                '"' -> Just '"'+                _ -> Nothing+        trEsc _ _ = Nothing      in Parser.wordWithQuotes False trEsc '\\' toRQuote isSpace Fold.toList  -- | A modifier for stream generation APIs in "Streamly.System.Process" to@@ -153,6 +167,15 @@             y:ys -> return $ f y ys input             _ -> error "streamWith: empty command" +-- | Like 'pipeChunks' but use the specified configuration to run the process.+{-# INLINE pipeChunksWith #-}+pipeChunksWith ::+    (MonadCatch m, MonadAsync m)+    => (Config -> Config)      -- ^ Config modifier+    -> String                  -- ^ Command+    -> Stream m (Array Word8)  -- ^ Input stream+    -> Stream m (Array Word8)  -- ^ Output stream+pipeChunksWith modifier = pipeWith (Process.pipeChunksWith modifier)  -- | @pipeChunks command input@ runs the executable with arguments specified by -- @command@ and supplying @input@ stream as its standard input.  Returns the@@ -165,7 +188,7 @@ -- If the input stream throws an exception or if the output stream is garbage -- collected before it could finish then the process is terminated with SIGTERM. ----- If the process terminates with a non-zero exit code then a 'ProcessFailure'+-- If the process terminates with a non-zero exit code then a 'Process.ProcessFailure' -- exception is raised. -- -- The following code is equivalent to the shell command @echo "hello world" |@@ -220,10 +243,9 @@ -- Generation ------------------------------------------------------------------------------- --- |--- -- >>> toBytes = streamWith Process.toBytes---++-- | -- >>> toBytes "echo hello world" & Stdio.putBytes --hello world -- >>> toBytes "echo hello\\ world" & Stdio.putBytes@@ -238,10 +260,18 @@ toBytes :: (MonadAsync m, MonadCatch m) => String -> Stream m Word8 toBytes = streamWith Process.toBytes --- |---+-- | Like 'toChunks' but use the specified configuration to run the process.+{-# INLINE toChunksWith #-}+toChunksWith ::+    (MonadCatch m, MonadAsync m)+    => (Config -> Config)     -- ^ Config modifier+    -> String                 -- ^ Command+    -> Stream m (Array Word8) -- ^ Output stream+toChunksWith modifier = streamWith (Process.toChunksWith modifier)+ -- >>> toChunks = streamWith Process.toChunks---++-- | -- >>> toChunks "echo hello world" & Stdio.putChunks --hello world --@@ -250,9 +280,9 @@ toChunks :: (MonadAsync m, MonadCatch m) => String -> Stream m (Array Word8) toChunks = streamWith Process.toChunks --- | -- >>> toChars = streamWith Process.toChars---++-- | -- >>> toChars "echo hello world" & Stdio.putChars --hello world --@@ -261,9 +291,9 @@ toChars :: (MonadAsync m, MonadCatch m) => String -> Stream m Char toChars = streamWith Process.toChars --- | -- >>> toLines f = streamWith (Process.toLines f)---++-- | -- >>> toLines Fold.toList "echo -e hello\\\\nworld" & Stream.fold Fold.toList -- ["hello","world"] --@@ -276,9 +306,9 @@     -> Stream m a -- ^ Output Stream toLines f = streamWith (Process.toLines f) --- | -- >>> toString = runWith Process.toString---++-- | -- >>> toString "echo hello world" --"hello world\n" --@@ -290,9 +320,9 @@     -> m String toString = runWith Process.toString --- | -- >>> toStdout = runWith Process.toStdout---++-- | -- >>> toStdout "echo hello world" -- hello world --@@ -304,9 +334,9 @@     -> m () toStdout = runWith Process.toStdout --- | -- >>> toNull = runWith Process.toNull---++-- | -- >>> toNull "echo hello world" -- -- /Pre-release/@@ -316,3 +346,62 @@     => String -- ^ Command     -> m () toNull = runWith Process.toNull++-------------------------------------------------------------------------------+-- Processes not interacting with the parent process+-------------------------------------------------------------------------------++-- | Launch a standlone process i.e. the process does not have a way to attach+-- the IO streams with other processes. The IO streams stdin, stdout, stderr+-- can either be inherited from the parent or closed.+--+-- This API is more powerful than 'interactive' and 'daemon' and can be used to+-- implement both of these. However, it should be used carefully e.g. if you+-- inherit the IO streams and parent is not waiting for the child process to+-- finish then both parent and child may use the IO streams resulting in+-- garbled IO if both are reading/writing simultaneously.+--+-- If the parent chooses to wait for the process an 'ExitCode' is returned+-- otherwise a 'ProcessHandle' is returned which can be used to terminate the+-- process, send signals to it or wait for it to finish.+{-# INLINE standalone #-}+standalone ::+       Bool -- ^ Wait for process to finish?+    -> (Bool, Bool, Bool) -- ^ close (stdin, stdout, stderr)+    -> (Config -> Config)+    -> String -- ^ Command+    -> IO (Either ExitCode ProcessHandle)+standalone wait streams modCfg =+    runWith (Process.standalone wait streams modCfg)++-- | Launch a process interfacing with the user. User interrupts are sent to+-- the launched process and ignored by the parent process. The launched process+-- inherits stdin, stdout, and stderr from the parent, so that the user can+-- interact with the process. The parent waits for the child process to exit,+-- an 'ExitCode' is returned when the process finishes.+--+-- This is the same as the common @system@ function found in other libraries+-- used to execute commands.+--+-- On Windows you can pass @setSession NewConsole@ to create a new console.+--+{-# INLINE foreground #-}+foreground ::+       (Config -> Config)+    -> String -- ^ Command+    -> IO ExitCode+foreground modCfg = runWith (Process.foreground modCfg)++-- | Launch a daemon process. Closes stdin, stdout and stderr, creates a new+-- session, detached from the terminal, the parent does not wait for the+-- process to finish.+--+-- The 'ProcessHandle' returned can be used to terminate the daemon or send+-- signals to it.+--+{-# INLINE daemon #-}+daemon ::+       (Config -> Config)+    -> String -- ^ Command+    -> IO ProcessHandle+daemon modCfg = runWith (Process.daemon modCfg)
src/Streamly/Internal/System/Process.hs view
@@ -60,17 +60,18 @@     -}     , closeFiles     , newProcessGroup+    , Session (..)     , setSession      -- * Posix Only Options     -- | These options have no effect on Windows.-    , parentIgnoresInterrupt+    , interruptChildOnly     , setUserId     , setGroupId      -- * Windows Only Options     -- | These options have no effect on Posix.-    , waitForChildTree+    , waitForDescendants      -- * Internal     , inheritStdin@@ -111,11 +112,14 @@     , pipeChunksEitherWith      -- * Standalone Processes-    , standalone-    , interactive+    , foreground     , daemon+    , standalone      -- * Deprecated+    , parentIgnoresInterrupt+    , waitForChildTree+    , interactive     , processBytes     , processChunks     )@@ -170,17 +174,7 @@ import qualified Streamly.Unicode.Stream as Unicode import qualified Streamly.Internal.Unicode.Stream as Unicode (lines) --- $setup--- >>> :set -XFlexibleContexts--- >>> import Data.Char (toUpper)--- >>> import Data.Function ((&))--- >>> import qualified Streamly.Internal.Console.Stdio as Stdio--- >>> import qualified Streamly.Data.Fold as Fold--- >>> import qualified Streamly.Data.Stream.Prelude as Stream--- >>> import qualified Streamly.Internal.System.Process as Process--- >>> import qualified Streamly.Unicode.Stream as Unicode--- >>> import qualified Streamly.Internal.Data.Stream as Stream--- >>> import qualified Streamly.Internal.Unicode.Stream as Unicode+#include "DocTestProcess.hs"  ------------------------------------------------------------------------------- -- Config@@ -271,7 +265,7 @@ -- working directory is inherited from the parent process. -- -- Default is 'Nothing' - inherited from the parent process.-setCwd :: Maybe (FilePath) -> Config -> Config+setCwd :: Maybe FilePath -> Config -> Config setCwd path (Config cfg) = Config $ cfg { cwd = path }  -- | Set the environment variables for the new process. When 'Nothing', the@@ -336,7 +330,9 @@ -- | If 'True' the new process starts a new process group, becomes a process -- group leader, its pid becoming the process group id. ----- See the POSIX @setpgid@ man page.+-- See the POSIX+-- <https://man7.org/linux/man-pages/man2/setpgid.2.html setpgid>+-- man page. -- -- Default is 'False', the new process belongs to the parent's process group. newProcessGroup :: Bool -> Config -> Config@@ -346,7 +342,9 @@ -- parent process. This is the default. -- -- 'NewSession' makes the new process start with a new session without a--- controlling terminal. On POSIX, @setsid@ is used to create a new process+-- controlling terminal. On POSIX,+-- <https://man7.org/linux/man-pages/man2/setsid.2.html setsid>+-- is used to create a new process -- group and session, the pid of the new process is the session id and process -- group id as well. On Windows @DETACHED_PROCESS@ flag is used to detach the -- process from inherited console session.@@ -356,10 +354,12 @@ -- nothing. -- -- For Windows see+-- -- * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags -- * https://learn.microsoft.com/en-us/windows/console/creation-of-a-console . ----- For POSIX see, @setsid@ man page.+-- For POSIX see, <https://man7.org/linux/man-pages/man2/setsid.2.html setsid>+-- man page. data Session =       InheritSession -- ^ Inherit the parent session     | NewSession -- ^ Detach process from the current session@@ -376,11 +376,15 @@             NewSession -> cfg { new_session = True}             NewConsole -> cfg {create_new_console = True} --- | Use the POSIX @setuid@ call to set the user id of the new process before+-- | Use the POSIX+-- <https://man7.org/linux/man-pages/man2/setuid.2.html setuid>+-- call to set the user id of the new process before -- executing the command. The parent process must have sufficient privileges to -- set the user id. ----- POSIX only. See the POSIX @setuid@ man page.+-- POSIX only. See the POSIX+-- <https://man7.org/linux/man-pages/man2/setuid.2.html setuid>+-- man page. -- -- Default is 'Nothing' - inherit from the parent. setUserId :: Maybe Word32 -> Config -> Config@@ -392,11 +396,15 @@     Config $ cfg { child_user = CUid <$> x } #endif --- | Use the POSIX @setgid@ call to set the group id of the new process before+-- | Use the POSIX+-- <https://man7.org/linux/man-pages/man2/setgid.2.html setgid>+-- call to set the group id of the new process before -- executing the command. The parent process must have sufficient privileges to -- set the group id. ----- POSIX only. See the POSIX @setgid@ man page.+-- POSIX only. See the POSIX+-- <https://man7.org/linux/man-pages/man2/setgid.2.html setgid>+-- man page. -- -- Default is 'Nothing' - inherit from the parent. setGroupId :: Maybe Word32 -> Config -> Config@@ -424,15 +432,23 @@ -- until the child exits. -- -- POSIX only. Default is 'False'.+interruptChildOnly :: Bool -> Config -> Config+interruptChildOnly x (Config cfg) = Config $ cfg { delegate_ctlc = x }++{-# DEPRECATED parentIgnoresInterrupt "Use interruptChildOnly instead." #-} parentIgnoresInterrupt :: Bool -> Config -> Config-parentIgnoresInterrupt x (Config cfg) = Config $ cfg { delegate_ctlc = x }+parentIgnoresInterrupt = interruptChildOnly --- | On Windows, the parent waits for the entire tree of process i.e. including--- processes that are spawned by the child process.+-- | On Windows, the parent waits for the entire descendant tree of process+-- i.e. including processes that are spawned by the child process. -- -- Default is 'True'.+waitForDescendants :: Bool -> Config -> Config+waitForDescendants x (Config cfg) = Config $ cfg { use_process_jobs = x }++{-# DEPRECATED waitForChildTree "Use waitForDescendants instead." #-} waitForChildTree :: Bool -> Config -> Config-waitForChildTree x (Config cfg) = Config $ cfg { use_process_jobs = x }+waitForChildTree = waitForDescendants  pipeStdErr :: Config -> Config pipeStdErr (Config cfg) = Config $ cfg { std_err = CreatePipe }@@ -595,6 +611,8 @@      alloc = createProc' modCfg path args +-- | Like 'pipeChunksEither' but use the specified configuration to run the+-- process. {-# INLINE pipeChunksEitherWith #-} pipeChunksEitherWith ::     (MonadCatch m, MonadAsync m)@@ -614,6 +632,8 @@             `parallel` fmap Right (toChunksClose stdoutH)     run _ = error "pipeChunksEitherWith: Not reachable" +-- | Like 'pipeChunks' but also includes stderr as 'Left' stream in the+-- 'Either' output. {-# INLINE pipeChunksEither #-} pipeChunksEither ::     (MonadCatch m, MonadAsync m)@@ -657,6 +677,7 @@         rightRdr = fmap Right Array.reader      in Stream.unfoldMany (Unfold.either leftRdr rightRdr) output +-- | Like 'pipeChunks' but use the specified configuration to run the process. {-# INLINE pipeChunksWith #-} pipeChunksWith ::     (MonadCatch m, MonadAsync m)@@ -790,6 +811,8 @@ -- Generation ------------------------------------------------------------------------------- +-- | Like 'toChunksEither' but use the specified configuration to run the+-- process. {-# INLINE toChunksEitherWith #-} toChunksEitherWith ::     (MonadCatch m, MonadAsync m)@@ -807,6 +830,7 @@             `parallel` fmap Right (toChunksClose stdoutH)     run _ = error "toChunksEitherWith: Not reachable" +-- | Like 'toChunks' but use the specified configuration to run the process. {-# INLINE toChunksWith #-} toChunksWith ::     (MonadCatch m, MonadAsync m)@@ -873,7 +897,7 @@     let output = toChunks path args      in Stream.unfoldMany Array.reader output --- | Like 'toBytes' but generates a stream of @Array Word8@ instead of a stream+-- | Like 'toBytesEither' but generates a stream of @Array Word8@ instead of a stream -- of @Word8@. -- -- >>> :{@@ -886,7 +910,7 @@ -- -- >>> toChunksEither = toChunksEitherWith id ----- Prefer 'toChunksEither over 'toBytesEither when performance matters.+-- Prefer 'toChunksEither' over 'toBytesEither' when performance matters. -- -- /Pre-release/ {-# INLINE toChunksEither #-}@@ -980,9 +1004,24 @@ toNull path args = toChunks path args & Stream.fold Fold.drain  ---------------------------------------------------------------------------------- Process not interacting with the parent process+-- Processes not interacting with the parent process ------------------------------------------------------------------------------- +-- XXX Make the return type ExitCode/ProcessHandle depend on the wait argument?++-- | Launch a standalone process i.e. the process does not have a way to attach+-- the IO streams with other processes. The IO streams stdin, stdout, stderr+-- can either be inherited from the parent or closed.+--+-- This API is more powerful than 'interactive' and 'daemon' and can be used to+-- implement both of these. However, it should be used carefully e.g. if you+-- inherit the IO streams and parent is not waiting for the child process to+-- finish then both parent and child may use the IO streams resulting in+-- garbled IO if both are reading/writing simultaneously.+--+-- If the parent chooses to wait for the process an 'ExitCode' is returned+-- otherwise a 'ProcessHandle' is returned which can be used to terminate the+-- process, send signals to it or wait for it to finish. {-# INLINE standalone #-} standalone ::        Bool -- ^ Wait for process to finish?@@ -998,7 +1037,7 @@      postCreate _ _ _ procHandle =         if wait-        then fmap Left $ waitForProcess procHandle+        then Left <$> waitForProcess procHandle         else return $ Right procHandle      cfg =@@ -1008,52 +1047,62 @@             s_err = if close_stderr then NoStream else Inherit         in c {std_in = s_in, std_out = s_out, std_err = s_err} --- | Inherits stdin, stdout, and stderr from the parent, so that the user can--- interact with the process, user interrupts are handled by the child process,--- the parent waits for the child process to exit.+-- | Launch a process interfacing with the user. User interrupts are sent to+-- the launched process and ignored by the parent process. The launched process+-- inherits stdin, stdout, and stderr from the parent, so that the user can+-- interact with the process. The parent waits for the child process to exit,+-- an 'ExitCode' is returned when the process finishes. ----- This is same as the common @system@ function found in other libraries used--- to execute commands.+-- This is the same as the common @system@ function found in other libraries+-- used to execute commands. -- -- On Windows you can pass @setSession NewConsole@ to create a new console. --+{-# INLINE foreground #-}+foreground ::+       (Config -> Config)+    -> FilePath -- ^ Executable name or path+    -> [String] -- ^ Arguments+    -> IO ExitCode+foreground modCfg path args =+    let r =+            standalone+                True+                (False, False, False)+                (parentIgnoresInterrupt True . modCfg)+                path args+     in fmap (either id undefined) r++{-# DEPRECATED interactive "Use foreground instead." #-} {-# INLINE interactive #-} interactive ::        (Config -> Config)     -> FilePath -- ^ Executable name or path     -> [String] -- ^ Arguments     -> IO ExitCode-interactive modCfg path args =-    withCreateProcess cfg (\_ _ _ p -> waitForProcess p)--    where--    -- let child handle SIGINT/QUIT-    modCfg1 = (parentIgnoresInterrupt True . modCfg)--    cfg =-        let Config c = modCfg1 $ mkConfig path args-        in c {std_in = Inherit, std_out = Inherit, std_err = Inherit}+interactive = foreground  -- XXX ProcessHandle can be used to terminate the process. re-export -- terminateProcess? --- | Closes stdin, stdout and stderr, creates a new session, detached from the--- terminal, the parent does not wait for the process to finish.+-- | Launch a daemon process. Closes stdin, stdout and stderr, creates a new+-- session, detached from the terminal, the parent does not wait for the+-- process to finish. --+-- The 'ProcessHandle' returned can be used to terminate the daemon or send+-- signals to it.+-- {-# INLINE daemon #-} daemon ::        (Config -> Config)     -> FilePath -- ^ Executable name or path     -> [String] -- ^ Arguments     -> IO ProcessHandle-daemon modCfg path args = withCreateProcess cfg (\_ _ _ p -> return p)--    where--    -- Detach terminal-    modCfg1 = (setSession NewSession . modCfg)--    cfg =-        let Config c = modCfg1 $ mkConfig path args-        in c {std_in = NoStream, std_out = NoStream, std_err = NoStream}+daemon modCfg path args =+    let r =+            standalone+                False+                (True, True, True)+                (setSession NewSession . modCfg)+                path args+     in fmap (either undefined id) r
src/Streamly/System/Command.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} -- | -- Module      : Streamly.System.Command -- Copyright   : (c) 2023 Composewell Technologies@@ -6,97 +7,18 @@ -- Stability   : experimental -- Portability : GHC ----- Use command strings to execute OS processes. These processes can be used--- just like native Haskell functions - to generate, transform or consume--- streams. It provides a powerful way to write high-level Haskell scripts to--- perform tasks similar to shell scripts without requiring the shell.--- Moreover, the Haskell scripts provide C-like performance.------ This module is a wrapper over the "Streamly.System.Process" module.------ See also: "Streamly.Internal.System.Command".----module Streamly.System.Command-    (-    -- * Setup-    -- | To execute the code examples provided in this module in ghci, please-    -- run the following commands first.-    ---    -- $setup--    -- * Overview-    -- $overview--    -- * Types-      ProcessFailure (..)--    -- * Generation-    , toBytes-    , toChunks-    , toChars-    , toLines--    -- * Effects-    , toString-    , toStdout-    , toNull--    -- * Transformation-    , pipeBytes-    , pipeChars-    , pipeChunks--    -- -- * Helpers-    -- , runWith-    -- , streamWith-    -- , pipeWith-    )-where--import Streamly.Internal.System.Command-import Streamly.Internal.System.Process (ProcessFailure (..))---- Keep it synced with the Internal module---- $setup--- >>> :set -XFlexibleContexts--- >>> :set -XQuasiQuotes--- >>> import Data.Char (toUpper)--- >>> import Data.Function ((&))--- >>> import Streamly.Unicode.String (str)--- >>> import qualified Streamly.Data.Array as Array--- >>> import qualified Streamly.Console.Stdio as Stdio--- >>> import qualified Streamly.Data.Fold as Fold--- >>> import qualified Streamly.Data.Stream.Prelude as Stream--- >>> import qualified Streamly.System.Command as Command--- >>> import qualified Streamly.Unicode.Stream as Unicode------ >>> import qualified Streamly.Internal.System.Process as Process--- >>> import qualified Streamly.Internal.Console.Stdio as Stdio--- >>> import qualified Streamly.Internal.FileSystem.Dir as Dir---- Note: Commands are not executed using shell------ You can use this module to execute system commands and compose them with--- Haskell.  It does not use the system shell to execute commands, they are--- executed as independent processes. This provides a convenient and powerful--- way to replace shell scripting with Haskell. Instead of composing commands--- using shell you can use Haskell Streamly streaming APIs to compose them with--- better efficiency and type safety.------ Normally, you should not need the system shell but if you want to use shell--- scripts in your program then you can take a look at the @streamly-shell@--- package which provides convenient wrapper over "Streamly.System.Process" to--- execute shell scripts, commands.---- $overview------ Please see the "Streamly.System.Process" for basics.+-- This module provides a way to invoke external executables and use them+-- seamlessly in a Haskell program, in a streaming fashion. This enables you to+-- write high-level Haskell scripts to perform tasks similar to shell scripts+-- without requiring the shell. Moreover, Haskell scripts provide C-like+-- performance. ----- "Streamly.System.Process" module requires specifying the command executable--- name and its arguments separately (e.g. "ls" "-al") whereas using this--- module we can specify the executable and its arguments more conveniently as--- a single command string e.g.  we can execute "ls -al".+-- Please see the "Streamly.System.Process" for basics. This module is a+-- wrapper over that module. "Streamly.System.Process" requires+-- specifying a command executable name and its arguments separately (e.g.+-- "ls" "-al") whereas using this module we can specify the executable and its+-- arguments more conveniently as a single command string e.g.  we can execute+-- "ls -al". -- -- A command string is parsed in the same way as a posix shell would parse it. -- A command string consists of whitespace separated tokens with the first@@ -130,7 +52,9 @@ -- -- = Shell commands as functions ----- If you want to execute the same command using the shell:+-- We recommend using streamly to compose commands natively in Haskell rather+-- than using the shell as shown in the previous example. However, if for some+-- reason you want to execute commands using the shell: -- -- >>> :{ --    Command.toBytes [str|sh "-c" "echo 'hello world' | tr [a-z] [A-Z]"|]@@ -140,7 +64,8 @@ -- -- = Running Commands Concurrently ----- Running @grep@ concurrently on many files:+-- This example shows the power of composing in Haskell rather than using the+-- shell. Running @grep@ concurrently on many files: -- -- >>> :{ -- grep file =@@ -156,3 +81,103 @@ --  & Stream.fold Stdio.writeChunks -- :} --+-- = Experimental APIs+--+-- See "Streamly.Internal.System.Command" for unreleased APIs.+--++module Streamly.System.Command+    (+    -- * Setup+    -- | To execute the code examples provided in this module in ghci, please+    -- run the following commands first.+    --+    -- $setup++    -- * Exceptions+      Process.ProcessFailure (..)++    -- * Process Configuration+    -- | Use the config modifiers to modify the default config.+    , Process.Config++    -- ** Common Modifiers+    -- | These options apply to both POSIX and Windows.+    , Process.setCwd+    , Process.setEnv+    , Process.closeFiles+    , Process.newProcessGroup+    , Process.Session (..)+    , Process.setSession++    -- ** Posix Only Modifiers+    -- | These options have no effect on Windows.+    , Process.interruptChildOnly+    , Process.setUserId+    , Process.setGroupId++    -- ** Windows Only Modifiers+    -- | These options have no effect on Posix.+    , Process.waitForDescendants++    -- * Generation+    , toBytes+    , toChunks+    , toChunksWith+    , toChars+    , toLines++    -- * Effects+    , toString+    , toStdout+    , toNull++    -- * Transformation+    , pipeChunks+    , pipeChunksWith+    , pipeBytes+    , pipeChars++    -- -- * Including Stderr Stream+    -- | Like other "Generation" routines but along with stdout, stderr is also+    -- included in the output stream. stdout is converted to 'Right' values in+    -- the output stream and stderr is converted to 'Left' values.+    -- , toBytesEither+    -- , toChunksEither+    -- , toChunksEitherWith+    -- , pipeBytesEither+    -- , pipeChunksEither+    -- , pipeChunksEitherWith++    -- * Non-streaming Processes+    -- | These processes do not attach the IO streams with other processes.+    , foreground+    , daemon+    , standalone++    -- -- * Helpers+    -- , runWith+    -- , streamWith+    -- , pipeWith+    )+where++import Streamly.Internal.System.Command+import qualified Streamly.Internal.System.Process as Process -- (ProcessFailure (..))++-- Keep it synced with the Internal module++#include "DocTestCommand.hs"+-- Note: Commands are not executed using shell+--+-- You can use this module to execute system commands and compose them with+-- Haskell.  It does not use the system shell to execute commands, they are+-- executed as independent processes. This provides a convenient and powerful+-- way to replace shell scripting with Haskell. Instead of composing commands+-- using shell you can use Haskell Streamly streaming APIs to compose them with+-- better efficiency and type safety.+--+-- Normally, you should not need the system shell but if you want to use shell+-- scripts in your program then you can take a look at the @streamly-shell@+-- package which provides convenient wrapper over "Streamly.System.Process" to+-- execute shell scripts, commands.
src/Streamly/System/Process.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} -- | -- Module      : Streamly.System.Process -- Copyright   : (c) 2020 Composewell Technologies@@ -6,98 +7,20 @@ -- Stability   : experimental -- Portability : GHC ----- Use OS processes just like native Haskell functions - to generate, transform--- or consume streams.------ See "Streamly.System.Command" module for a higher level wrapper over this--- module.------ See also: "Streamly.Internal.System.Process" for unreleased functions.----module Streamly.System.Process-    (-    -- * Setup-    -- | To execute the code examples provided in this module in ghci, please-    -- run the following commands first.-    ---    -- $setup--    -- * Overview-    -- $overview--    -- * Exceptions-    -- | Since we are composing using Streamly's streaming pipeline there is-    -- nothing special about exception handling, it works the same as in-    -- Streamly.  Like the @pipefail@ option in shells, exceptions are-    -- propagated if any of the stages fail.-      ProcessFailure (..)--    -- * Process Configuration-    , Config--    {--    -- ** Common Config Options-    -- | These options apply to both POSIX and Windows.-    , setCwd-    , setEnv-    , closeFiles-    , newProcessGroup-    , setSession--    -- * Posix Only Options-    -- | These options have no effect on Windows.-    , parentIgnoresInterrupt-    , setUserId-    , setGroupId--    -- * Windows Only Options-    -- | These options have no effect on Posix.-    , waitForChildTree-    -}--    -- * Generation-    , toChunks-    , toBytes--    -- * Transformation-    , pipeChunks-    , pipeBytes--    -- * Deprecated-    , processChunks-    , processBytes-    )-where--import Streamly.Internal.System.Process---- $setup--- >>> :set -XFlexibleContexts--- >>> :set -XScopedTypeVariables--- >>> import Data.Char (toUpper)--- >>> import Data.Function ((&))--- >>> import qualified Streamly.Console.Stdio as Stdio--- >>> import qualified Streamly.Data.Array as Array--- >>> import qualified Streamly.Data.Fold as Fold--- >>> import qualified Streamly.Data.Stream.Prelude as Stream--- >>> import qualified Streamly.System.Process as Process--- >>> import qualified Streamly.Unicode.Stream as Unicode------ >>> import qualified Streamly.Internal.FileSystem.Dir as Dir--- >>> import qualified Streamly.Internal.Data.Stream as Stream---- $overview--- -- This module provides functions to run operating system processes as stream--- source, sink or transformation functions. Thus OS processes can be used in--- the same way as Haskell functions and all the streaming combinators in--- streamly can be used to combine them. This allows you to seamlessly--- integrate external programs into your Haskell program.+-- producers, consumers or stream transformation functions. Thus OS processes+-- can be used in the same way as Haskell functions and all the streaming+-- combinators in streamly can be used to combine them. This allows you to+-- seamlessly integrate external binary executables into your Haskell program. ----- We recommend using Haskell functions with Streamly threads for performing--- tasks whenever possible. This approach offers a simpler programming model--- compared to system processes, which also have a larger performance overhead.+-- However, we recommend native Haskell functions with Streamly threads over+-- using system processes whenever possible. This approach offers a simpler+-- programming model compared to system processes, which also have a larger+-- performance overhead. --+-- Prefer "Streamly.System.Command" module as a higher level wrapper over this+-- module.+-- -- = Executables as functions -- -- Processes can be composed in a streaming pipeline just like a Posix shell@@ -150,3 +73,90 @@ --  & Stream.parConcatMap id grep --  & Stream.fold Stdio.writeChunks -- :}+--+-- = Experimental APIs+--+-- See "Streamly.Internal.System.Process" for unreleased functions.+--++module Streamly.System.Process+    (+    -- * Setup+    -- | To execute the code examples provided in this module in ghci, please+    -- run the following commands first.+    --+    -- $setup++    -- * Exceptions+    -- | Since we are composing using Streamly's streaming pipeline there is+    -- nothing special about exception handling, it works the same as in+    -- Streamly.  Like the @pipefail@ option in shells, exceptions are+    -- propagated if any of the stages fail.+      ProcessFailure (..)++    -- * Process Configuration+    -- | Use the config modifiers to modify the default config.+    , Config++    -- ** Common Modifiers+    -- | These options apply to both POSIX and Windows.+    , setCwd+    , setEnv+    , closeFiles+    , newProcessGroup+    , Session (..)+    , setSession++    -- ** Posix Only Modifiers+    -- | These options have no effect on Windows.+    , interruptChildOnly+    , setUserId+    , setGroupId++    -- ** Windows Only Modifiers+    -- | These options have no effect on Posix.+    , waitForDescendants++    -- * Generation+    , toChunks+    , toChunksWith+    , toBytes+    , toChars+    , toLines++    -- * Effects+    , toString+    , toStdout+    , toNull++    -- * Transformation+    , pipeChunks+    , pipeChunksWith+    , pipeBytes++    -- * Including Stderr Stream+    -- | Like other "Generation" routines but along with stdout, stderr is also+    -- included in the output stream. stdout is converted to 'Right' values in+    -- the output stream and stderr is converted to 'Left' values.+    , toBytesEither+    , toChunksEither+    , toChunksEitherWith+    , pipeBytesEither+    , pipeChunksEither+    , pipeChunksEitherWith++    -- * Non-streaming Processes+    -- | These processes do not attach the IO streams with other processes.+    , foreground+    , daemon+    , standalone++    -- * Deprecated+    , processChunks+    , processBytes+    )+where++import Streamly.Internal.System.Process++#include "DocTestProcess.hs"
streamly-process.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                streamly-process-version:             0.3.0+version:             0.3.1 synopsis:            Use OS processes as stream transformation functions description:   Use operating system (OS) commands in Haskell programs as if they were@@ -30,6 +30,8 @@   NOTICE   README.md   design/proposal.md+  src/DocTestCommand.hs+  src/DocTestProcess.hs   test/data/failExec.bat   test/data/failExec.sh   test/data/passExec.bat@@ -81,6 +83,7 @@  library   import: compile-options, optimization-options+  include-dirs: src   hs-source-dirs: src   exposed-modules:     Streamly.System.Process@@ -94,8 +97,8 @@       base              >= 4.8   && < 5     , exceptions        >= 0.8   && < 0.11     -- Uses internal APIs-    , streamly          == 0.9.0.*-    , streamly-core     == 0.1.0+    , streamly          >= 0.9 && < 0.10+    , streamly-core     >= 0.1 && < 0.2   if !flag(use-native)     build-depends: process >= 1.0 && < 1.7   else@@ -123,7 +126,7 @@     , base              >= 4.8   && < 5     , directory         >= 1.2.2 && < 1.4     -- Uses internal APIs-    , streamly-core     == 0.1.0.*+    , streamly-core     , tasty-bench >= 0.2.5 && < 0.4    if flag(fusion-plugin) && !impl(ghc < 8.6)@@ -146,4 +149,4 @@     , hspec             >= 2.0   && < 3     , QuickCheck        >= 2.10  && < 2.15     -- Uses internal APIs-    , streamly-core     == 0.1.0.*+    , streamly-core
test/Streamly/System/Process.hs view
@@ -16,7 +16,7 @@ import System.Directory (removeFile, findExecutable, doesFileExist) import System.Exit (exitSuccess) import System.IO (IOMode(..), openFile, hClose)-import Test.Hspec (hspec, describe)+import Test.Hspec (describe, hspec, it, shouldBe) import Test.Hspec.QuickCheck import Test.QuickCheck     ( forAll@@ -31,10 +31,10 @@ import qualified Streamly.Data.Fold as Fold import qualified Streamly.Data.Stream as S import qualified Streamly.System.Process as Proc+import qualified Streamly.Data.Stream as Stream  import qualified Streamly.Internal.FileSystem.Handle as FH (putBytes, read)-import qualified Streamly.Internal.System.Process as Proc-    (pipeChunksEither, pipeBytesEither, toChunksEither, toBytesEither)+import qualified Streamly.Internal.System.Command as Cmd (quotedWord)  newtype SimpleError = SimpleError String     deriving Show@@ -466,6 +466,14 @@      checkFailAction = catch action failAction +quotedWordTest :: String -> [String] -> IO ()+quotedWordTest inp expected = do+    res <-+        Stream.fold Fold.toList+            $ Stream.catRights+            $ Stream.parseMany Cmd.quotedWord $ Stream.fromList inp+    res `shouldBe` expected+ main :: IO () main = do     -- Nix does not have "/usr/bin/env", so the execution of test executables@@ -474,7 +482,7 @@     -- https://github.com/haskell/cabal/issues/7577 .     r <- doesFileExist "/usr/bin/env"     unless r $ do-        putStrLn $ "/usr/bin/env does not exist, skipping tests."+        putStrLn "/usr/bin/env does not exist, skipping tests."         exitSuccess      hspec $ do@@ -522,3 +530,15 @@             describe "toBytesEither" $ do                 prop "toBytesEither cat = FH.toBytes" toBytes1                 prop "toBytesEither on failing executable" toBytes2++            describe "quotedWord" $ do+                it "Single quote test" $+                   quotedWordTest "'hello\\\\\"world'" ["hello\\\\\"world"]+                it "Double quote test" $+                   quotedWordTest+                       "\"hello\\\"\\\\w\\'orld\""+                       ["hello\"\\w\\'orld"]+                -- TODO: We need to let the escape character be at the end+                -- "wordWithQuotes" needs to be fixed!+                -- it "Double quote test" $+                --    quotedWordTest "'hello\'" ["hello\\"]