Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Streamly/Coreutils/Ls.hs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ ls f dir = do
-- Stream.unfoldIterateDfs unfoldOne
-- BFS avoids opening too many file descriptors but may accumulate
-- more data in memory.
Stream.unfoldIterateBfs unfoldOne
Stream.bfsUnfoldIterate unfoldOne
-- $ Stream.parConcatIterate id streamOne
-- $ Stream.parConcatIterate (Stream.ordered True) streamOne
$ Stream.fromPure (Left dir)
Expand Down
284 changes: 284 additions & 0 deletions src/Streamly/Coreutils/Sh.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
-- |
-- Module : Streamly.Coreutils.Sh
-- Copyright : (c) 2021 Composewell Technologies
-- License : Apache-2.0
-- Maintainer : streamly@composewell.com
-- Stability : experimental
-- Portability : GHC
--
-- Use shell scripts in your Haskell programs, interfacing via standard input
-- and output. The functions in this module are just convenience wrappers over
-- "Streamly.System.Process" to run shell commands using "\/bin/sh" as the
-- shell.
--
-- >>> :{
-- toBytes "echo hello"
-- & pipeBytes "tr [a-z] [A-Z]"
-- & Stdio.putBytes
-- :}
-- HELLO
--

module Streamly.Coreutils.Sh
(
-- * Generation
toBytes
, toChunks
, toChars
, toLines

-- * Effects
, toString
, toStdout
, toNull

-- * Transformation
, pipeBytes
, pipeChars
, pipeChunks

-- * Helpers
, runWith
, streamWith
, pipeWith
)
where

import Control.Monad.Catch (MonadCatch)
import Data.Word (Word8)
import Streamly.Data.Array (Array)
import Streamly.Data.Fold (Fold)
import Streamly.Data.Stream.Prelude (Stream, MonadAsync)

import qualified Streamly.Internal.System.Process as Process

-- The APIs are named/designed such that we can replace the sh module with
-- another module for bash or any other shells without requiring API name
-- changes.

-- $setup
-- >>> :set -XFlexibleContexts
-- >>> :set -package streamly
-- >>> :set -package streamly-core
-- >>> :set -package streamly-process
-- >>> import Data.Function ((&))
-- >>> import qualified Streamly.Internal.Console.Stdio as Stdio
-- >>> import qualified Streamly.Data.Fold as Fold
-- >>> import qualified Streamly.Data.Stream as Stream
-- >>> import qualified Streamly.Internal.System.Process as Process
-- >>> import qualified Streamly.Coreutils.Sh as Sh
-- >>> import qualified Streamly.Unicode.Stream as Unicode

-- | A modifier for stream generation APIs in "Streamly.System.Process" to
-- generate streams from shell scripts with "\/bin/sh" as the shell
-- interpreter. Defined as:
--
-- >>> streamWith f cmd = f "/bin/sh" ["-c", cmd]
--
-- For example:
--
-- >>> streamWith Process.toBytes "echo hello" & Stdio.putBytes
-- hello
-- >>> streamWith Process.toChunks "echo hello" & Stdio.putChunks
-- hello
--
streamWith :: (FilePath -> [String] -> Stream m a) -> String -> Stream m a
streamWith f cmd = f "/bin/sh" ["-c", cmd]

-- | A modifier for process running APIs in "Streamly.System.Process" to run
-- shell commands.
--
-- For example:
--
-- >>> runWith Process.toString "echo hello"
-- "hello\n"
-- >>> runWith Process.toStdout "echo hello"
-- hello
--
-- /Internal/
{-# INLINE runWith #-}
runWith :: (FilePath -> [String] -> m a) -> String -> m a
runWith f cmd = f "/bin/sh" ["-c", cmd]

-- | A modifier for process piping APIs in "Streamly.System.Process" to pipe
-- data through shell scripts:
--
-- For example:
--
-- >>> :{
-- toChunks "echo hello"
-- & pipeWith Process.pipeChunks "tr [a-z] [A-Z]"
-- & Stdio.putChunks
-- :}
--HELLO
--
-- /Internal/
pipeWith ::
(FilePath -> [String] -> Stream m a -> Stream m b)
-> String
-> Stream m a
-> Stream m b
pipeWith f cmd = f "/bin/sh" ["-c", cmd]

-- | @pipeChunks command input@ runs the executable with arguments specified by
-- @command@ and supplying @input@ stream as its standard input. Returns the
-- standard output of the executable as a stream of byte arrays.
--
-- If only the name of an executable file is specified instead of its path then
-- the file name is searched in the directories specified by the PATH
-- environment variable.
--
-- 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'
-- exception is raised.
--
-- The following code is equivalent to the shell command @echo "hello world" |
-- tr [a-z] [A-Z]@:
--
-- >>> :{
-- toChunks "echo hello world"
-- & pipeChunks "tr [a-z] [A-Z]"
-- & Stdio.putChunks
-- :}
--HELLO WORLD
--
-- /Pre-release/
{-# INLINE pipeChunks #-}
pipeChunks :: (MonadAsync m, MonadCatch m) =>
String -> Stream m (Array Word8) -> Stream m (Array Word8)
pipeChunks = pipeWith Process.pipeChunks

-- | Like 'pipeChunks' except that it works on a stream of bytes instead of
-- a stream of chunks.
--
-- >>> :{
-- toBytes "echo hello world"
-- & pipeBytes "tr [a-z] [A-Z]"
-- & Stdio.putBytes
-- :}
--HELLO WORLD
--
-- /Pre-release/
{-# INLINE pipeBytes #-}
pipeBytes :: (MonadAsync m, MonadCatch m) =>
String -> Stream m Word8 -> Stream m Word8
pipeBytes = pipeWith Process.pipeBytes

-- | Like 'pipeChunks' except that it works on a stream of chars instead of
-- a stream of chunks.
--
-- >>> :{
-- toChars "echo hello world"
-- & pipeChars "tr [a-z] [A-Z]"
-- & Stdio.putChars
-- :}
--HELLO WORLD
--
-- /Pre-release/
{-# INLINE pipeChars #-}
pipeChars :: (MonadAsync m, MonadCatch m) =>
String -> Stream m Char -> Stream m Char
pipeChars = pipeWith Process.pipeChars

-------------------------------------------------------------------------------
-- Generation
-------------------------------------------------------------------------------

-- |
--
-- >>> toBytes = streamWith Process.toBytes
--
-- >>> toBytes "echo hello world" & Stdio.putBytes
--hello world
-- >>> toBytes "echo hello\\ world" & Stdio.putBytes
--hello world
-- >>> toBytes "echo 'hello world'" & Stdio.putBytes
--hello world
-- >>> toBytes "echo \"hello world\"" & Stdio.putBytes
--hello world
--
-- /Pre-release/
toBytes :: (MonadAsync m, MonadCatch m) => String -> Stream m Word8
toBytes = streamWith Process.toBytes

-- |
--
-- >>> toChunks = streamWith Process.toChunks
--
-- >>> toChunks "echo hello world" & Stdio.putChunks
--hello world
--
-- /Pre-release/
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
--
-- /Pre-release/
{-# INLINE toChars #-}
toChars :: (MonadAsync m, MonadCatch m) => String -> Stream m Char
toChars = streamWith Process.toChars

-- |
-- >>> toLines f = streamWith (Process.toLines f)
--
-- >>> toLines Fold.toList "/bin/echo -e hello\\\\nworld" & Stream.fold Fold.toList
-- ["hello","world"]
--
-- /Pre-release/
{-# INLINE toLines #-}
toLines ::
(MonadAsync m, MonadCatch m)
=> Fold m Char a
-> String -- ^ Command
-> Stream m a -- ^ Output Stream
toLines f = streamWith (Process.toLines f)

-- |
-- >>> toString = runWith Process.toString
--
-- >>> toString "echo hello world"
--"hello world\n"
--
-- /Pre-release/
{-# INLINE toString #-}
toString ::
(MonadAsync m, MonadCatch m)
=> String -- ^ Command
-> m String
toString = runWith Process.toString

-- |
-- >>> toStdout = runWith Process.toStdout
--
-- >>> toStdout "echo hello world"
-- hello world
--
-- /Pre-release/
{-# INLINE toStdout #-}
toStdout ::
(MonadAsync m, MonadCatch m)
=> String -- ^ Command
-> m ()
toStdout = runWith Process.toStdout

-- |
-- >>> toNull = runWith Process.toNull
--
-- >>> toNull "echo hello world"
--
-- /Pre-release/
{-# INLINE toNull #-}
toNull ::
(MonadAsync m, MonadCatch m)
=> String -- ^ Command
-> m ()
toNull = runWith Process.toNull
21 changes: 11 additions & 10 deletions stack.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
resolver: lts-22.33
packages:
- '.'
#extra-deps:
# - streamly-core-0.3.0
# - streamly-0.11.0

extra-deps:
- github: composewell/streamly
commit: "e57585dfac5d9d1b57c52d8f3a69ed3809a6ae63"
- github: composewell/streamly
commit: "e57585dfac5d9d1b57c52d8f3a69ed3809a6ae63"
subdirs:
- core
- streamly-core-0.3.0
- streamly-0.11.0
- streamly-process-0.4.0

#extra-deps:
#- github: composewell/streamly
# commit: "e57585dfac5d9d1b57c52d8f3a69ed3809a6ae63"
#- github: composewell/streamly
# commit: "e57585dfac5d9d1b57c52d8f3a69ed3809a6ae63"
# subdirs:
# - core

# Look at https://stackoverflow.com/questions/70045586/could-not-find-module-system-console-mintty-win32-when-compiling-test-framework
flags:
Expand Down
16 changes: 9 additions & 7 deletions streamly-coreutils.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ library
base >= 4.8 && < 5
, exceptions >= 0.8 && < 0.11
-- We use internal modules of streamly.
, streamly >= 0.9 && < 0.12
, streamly >= 0.11 && < 0.12
, streamly-core >= 0.3 && < 0.4
, streamly-process >= 0.4 && < 0.5
, time >= 1.9 && < 1.15
, directory >= 1.2.2 && < 1.4
, filepath >= 1.4 && < 1.6
Expand All @@ -120,20 +121,21 @@ library
, Streamly.Coreutils.Cp
, Streamly.Coreutils.Directory
, Streamly.Coreutils.Dirname
, Streamly.Coreutils.Mv
, Streamly.Coreutils.Stat
, Streamly.Coreutils.Mkdir
, Streamly.Coreutils.FileTest
, Streamly.Coreutils.Ln
, Streamly.Coreutils.Ls
, Streamly.Coreutils.Rm
, Streamly.Coreutils.String
, Streamly.Coreutils.Mkdir
, Streamly.Coreutils.Mv
, Streamly.Coreutils.ReadLink
, Streamly.Coreutils.Rm
, Streamly.Coreutils.Sh
, Streamly.Coreutils.ShellWords
, Streamly.Coreutils.Sleep
, Streamly.Coreutils.Stat
, Streamly.Coreutils.String
, Streamly.Coreutils.Touch
, Streamly.Coreutils.Uniq
, Streamly.Coreutils.Which
, Streamly.Coreutils.Ln

default-language: Haskell2010

Expand Down
Loading