shell-conduit
Write shell scripts with Conduit. Still in the experimental phase.
Haddock API documentation.
Examples
Cloning and initializing a repo
import Control.Monad.IO.Class
import Data.Conduit.Shell
import System.Directory
main =
run (do exists <- liftIO (doesDirectoryExist "fpco")
if exists
then rm "fpco/.hsenvs" "-rf"
else git "clone" "git@github.com:fpco/fpco.git"
liftIO (setCurrentDirectory "fpco")
shell "./dev-scripts/update-repo.sh"
shell "./dev-scripts/build-all.sh"
alertDone)
Piping
Piping of processes and normal conduits is possible:
λ> run (ls $| grep ".*" $| shell "cat" $| conduit (CL.map (S8.map toUpper)))
DIST
EXAMPLES
LICENSE
README.MD
SETUP.HS
SHELL-CONDUIT.CABAL
SRC
TAGS
TODO.ORG
Running actions in sequence and piping
Results are outputted to stdout unless piped into other processes:
λ> run (do shell "echo sup"; shell "echo hi")
sup
hi
λ> run (do shell "echo sup" $| sed "s/u/a/"; shell "echo hi")
sap
hi
Streaming
Live streaming between pipes like in normal shell scripting is
possible:
λ> run (do tail' "/tmp/example.txt" "-f" $| grep "--line-buffered" "Hello")
Hello, world!
Oh, hello!
(Remember that grep
needs --line-buffered
if it is to output things
line-by-line).
Handling exit failures
Process errors can be ignored by using the Alternative instance.
import Control.Applicative
import Control.Monad.Fix
import Data.Conduit.Shell
main =
run (do ls
echo "Restarting server ... ?"
killall name "-q" <|> return ()
fix (\loop ->
do echo "Waiting for it to terminate ..."
sleep "1"
(ps "-C" name >> loop) <|> return ())
shell "dist/build/ircbrowse/ircbrowse ircbrowse.conf")
where name = "ircbrowse"
Running custom things
You can run processes directly:
λ> run (proc "ls" [])
dist LICENSE Setup.hs src TODO.org
examples README.md shell-conduit.cabal TAGS
Or shell commands:
λ> run (shell "ls")
dist LICENSE Setup.hs src TODO.org
examples README.md shell-conduit.cabal TAGS
Or conduits:
λ> run (cat $| conduit (awaitForever yield))
hello
hello
Interrupted.
Keyboard configuration
import Data.Conduit.Shell
main =
run (do xmodmap ".xmodmap"
xset "r" "rate" "150" "50")
How it works
All executable names in the PATH
at compile-time are brought into
scope as runnable process conduits e.g. ls
or grep
.
All processes are bound as variadic process calling functions, like this:
rmdir :: ProcessType r => r
ls :: ProcessType r => r
But ultimately the types end up being:
rmdir "foo" :: Segment r
ls :: Segment r
ls "." :: Segment r
Etc.
Run all shell scripts with
run :: Segment r -> IO r
The Segment
type has a handy Alternative
instance.
String types
If using OverloadedStrings
so that you can use Text
for arguments,
then also enable ExtendedDefaultRules
, otherwise you'll get
ambiguous type errors.
{-# LANGUAGE ExtendedDefaultRules #-}
But this isn't necessary if you don't need to use Text
yet. Strings
literals will be interpreted as String
. Though you can pass a value
of type Text
or any instance of CmdArg
without needing conversions.
Other modules
You might want to import the regular Conduit modules qualified, too:
import qualified Data.Conduit.List as CL
Which contains handy functions for working on streams in a
list-like way. See the rest of the handy modules for Conduit in
conduit-extra.
Also of interest is
csv-conduit,
html-conduit, and
http-conduit.
Finally, see the Conduit category on Hackage for other useful
libraries: http://hackage.haskell.org/packages/#cat:Conduit
All of these general purpose Conduits can be used in shell
scripting.