Use turtle if you want to write light-weight and maintainable shell
scripts.

turtle embeds shell scripting directly within Haskell for three main
reasons:

Haskell code is easy to refactor and maintain because the language is
statically typed

Haskell is syntactically lightweight, thanks to global type inference

Haskell programs can be type-checked and interpreted very rapidly (< 1
second)

These features make Haskell ideal for scripting, particularly for replacing
large and unwieldy Bash scripts.

This tutorial introduces how to use the turtle library to write Haskell
scripts. This assumes no prior knowledge of Haskell, but does assume prior
knowledge of Bash or a similar shell scripting language.

If you are already proficient with Haskell, then you can get quickly up to
speed by reading the Quick Start guide at the top of Turtle.Prelude.

In Haskell you can use -- to comment out the rest of a line. The above
example uses comments to show the equivalent Bash script side-by-side with
the Haskell script.

You can execute the above code by saving it to the file example.hs. If you
are copying and pasting the code, then remove the leading 1-space indent.
After you save the file, make the script executable and run the script:

$ chmod u+x example.hs
$ ./example.hs
Hello, world!

If you delete the first line of the program, you can also compile the above
code to generate a native executable which will have a much faster startup
time and improved performance:

From now on I'll omit ghci's linker output in tutorial examples. You can
also silence this linker output by passing the -v0 flag to ghci.

Comparison

You'll already notice a few differences between the Haskell code and Bash
code.

First, the Haskell code requires two additional lines of overhead to import
the turtle library and enable overloading of string literals. This
overhead is mostly unavoidable.

Second, the Haskell echo explicitly quotes its string argument whereas the
Bash echo does not. In Bash every token is a string by default and you
distinguish variables by prepending a dollar sign to them. In Haskell
the situation is reversed: every token is a variable by default and you
distinguish strings by quoting them. The following example highlights the
difference:

Third, you have to explicitly assign a subroutine to main to specify which
subroutine to run when your program begins. This is because Haskell lets you
define things out of order. For example, we could have written our original
program this way instead:

Notice how the above program defines str after main, which is valid.
Haskell does not care in what order you define top-level values or functions
(using the = sign). However, the top level of a Haskell program only
permits definitions. If you were to insert a statement at the top-level:

Some commands can return a value, and you can store the result of a command
using the <- symbol. For example, the following program prints the
creation time of the current working directory by storing two intermediate
results:

The <- symbol is overloaded and its meaning is context-dependent; in this
context it just means "store the current result"

The = symbol is not overloaded and always means that the two sides of the
equality are interchangeable

do notation lets you combine smaller subroutines into larger subroutines.
For example, we could refactor the above code to split the first two commands
into their own smaller subroutine and then invoke that smaller subroutine
within a larger subroutine:

However, keep in mind that the return statement is something of a misnomer
since it does not break or exit from the surrounding subroutine. All it
does is create a trivial subroutine that has no side effects and returns its
argument as its result. If you return an expression, you're just giving
it a new name:

$ ./example.hs
example.hs:8:10:
Couldn't match expected type `Text' with actual type `UTCTime'
In the first argument of `echo', namely `time'
In a stmt of a 'do' block: echo time
In the expression:
do { dir <- pwd;
time <- datefile dir;
echo time }

The error points to the last line of our program: (example.hs:8:10) means
line 8, column 10 of our program. If you study the error message closely
you'll see that the echo function expects a Text value, but we passed it
'time', which was a UTCTime value. Although the error is at the end of
our script, Haskell catches this error before even running the script. When
we "interpret" a Haskell script the Haskell compiler actually compiles the
script without any optimizations to generate a temporary executable and then
runs the executable, much like Perl does for Perl scripts.

You might wonder: "where are the types?" None of the above programs had
any type signatures or type annotations, yet the compiler still detected type
errors correctly. This is because Haskell uses "global type inference" to
detect errors, meaning that the compiler can infer the types of expressions
within the program without any assistance from the programmer.

You can even ask the compiler what the type of an expression is using ghci.
Let's open up the REPL and import this library so that we can study the types
and deduce why our program failed:

$ ghci
Prelude> import Turtle
Prelude Turtle>

You can interrogate the REPL for an expression's type using the :type
command:

Whenever you see something of the form (x :: t), that means that 'x'
is a value of type 't'. The REPL says that pwd is a subroutine (IO)
that returns a FilePath. The Turtle prefix before
FilePath is just the module name since the FilePath
exported by the turtle library conflicts with the default FilePath
exported by Haskell's Prelude. The compiler uses the fully qualified name,
Turtle.FilePath, to avoid ambiguity.

The above type says that echo is a function whose argument is a value of
type Text and whose result is a subroutine (IO) with an empty return
value (denoted '()').

Now we can understand the type error: echo expects a Text argument but
datefile returns a UTCTime, which is not the same thing. Unlike Bash,
not everything is Text in Haskell and the compiler will not cast or coerce
types for you.

The reason print worked is because print has a more general type than
echo:

This type signature says that print can display any value of type 'a'
so long as 'a' implements the Show interface. In this case UTCTime
does implement the Show interface, so everything works out when we use
print.

This library provides a helper function that lets you convert any type that
implements Show into a Text value:

-- This behaves like Python's `repr` function
repr :: Show a => a -> Text

You can also optionally configure ghci to run the first two commands every
time you launch ghci. Just create a .ghci within your current directory
with these two lines:

:set -XOverloadedStrings
import Turtle

The following ghci examples will all assume that you run these two commands
at the beginning of every session, either manually or automatically. You can
even enable those two commands permanently by adding the above .ghci file
to your home directory.

Within ghci you can run a subroutine and ghci will print the
subroutine's value if it is not empty:

Type signatures

Haskell performs global type inference, meaning that the compiler never
requires any type signatures. When you add type signatures, they are purely
for the benefit of the programmer and behave like machine-checked
documentation.

These type annotations do not assist the compiler. Instead, the compiler
independently infers the type and then checks whether it matches the
documented type. If there is a mismatch the compiler will raise a type
error.

$ ./example.hs
example.hs:8:7:
No instance for (IsString Int)
arising from the literal `"Hello, world!"'
Possible fix: add an instance declaration for (IsString Int)
In the expression: "Hello, world!"
In an equation for `str': str = "Hello, world!"
example.hs:11:13:
Couldn't match expected type `Text' with actual type `Int'
In the first argument of `echo', namely `str'
In the expression: echo str
In an equation for `main': main = echo str

The first error message relates to the OverloadedStrings extensions. When
we enable OverloadedStrings the compiler overloads string literals,
interpreting them as any type that implements the IsString interface. The
error message says that Int does not implement the IsString interface so
the compiler cannot interpret a string literal as an Int. On the other
hand the Text and FilePath types do implement IsString, which
is why we can interpret string literals as Text or FilePath
values.

The second error message says that echo expects a Text value, but we
declared str to be an Int, so the compiler aborts compilation, requiring
us to either fix or delete our type signature.

Notice that there is nothing wrong with the program other than the type
signature we added. If we were to delete the type signature the program
would compile and run correctly. The sole purpose of this type signature is
for us to communicate our expectations to the compiler so that the compiler
can alert us if the code does not match our expectations.

Let's also try reversing the type error, providing a number where we expect
a string:

$ ./example.hs
example.hs:8:7:
No instance for (Num Text)
arising from the literal `4'
Possible fix: add an instance declaration for (Num Text)
In the expression: 4
In an equation for `str': str = 4

Haskell also automatically overloads numeric literals, too. The compiler
interprets integer literals as any type that implements the Num interface.
The Text type does not implement the Num interface, so we cannot
interpret integer literals as Text strings.

System

You can invoke arbitrary shell commands using the shell command. For
example, we can write a program that creates an empty directory and then
uses a shell command to archive the directory:

The compiler deduces that the above Format string requires one argument of
type Text to satisfy the s at the beginning of the format string and
another argument of type Int to satisfy the d at the end of the format
string.

If you are interested in this feature, check out the Turtle.Format module
for more details.

Streams

The turtle library provides support for streaming computations, just like
Bash. The primitive turtle streams are little more verbose than their
Bash counterparts, but turtle streams can be built and combined in more
ways.

The key type for streams is the Shell type, which represents a stream of
values. For example, the ls function has a streaming result:

That type says that ls takes a single FilePath as its argument
(the directory to list) and the result is a Shell stream of
FilePaths (the immediate children of that directory).

You can't run a Shell stream directly within ghci. You will get a type
error like this if you try:

Prelude Turtle> ls "/tmp"
<interactive>:2:1:
No instance for (Show (Shell Turtle.FilePath))
arising from a use of `print'
Possible fix:
add an instance declaration for (Show (Shell Turtle.FilePath))
In a stmt of an interactive GHCi command: print it

Instead, you must consume the stream as it is generated and the simplest way
to consume a Shell stream is view:

Exception Safety

Sometimes you may want to acquire resources and ensure they get released
correctly if there are any exceptions. You can use Managed resources to
acquire things safely within a Shell.

You can think of a Managed resource as some resource that needs to be
acquired and then released afterwards. Example: you want to create a
temporary file and then guarantee it's deleted afterwards, even if the
program fails with an exception.

MonadIO

If you are sick of having type liftIO everywhere, you can omit it. This
is because all subroutines in turtle are overloaded using the MonadIO
type class, like our original pwd command where we first encountered the
the MonadIO type:

These instances represent the overloaded functions associated with Shell
and we can see from the list that Shell implements MonadIO so we can
use pwd (or any other subroutine in this library) within a Shell.

However, not all subroutines in the Haskell ecosystem are overloaded in this
way (such as print), so you will still occasionally need to wrap
subroutines in liftIO.

Command line options

The Turtle.Options module lets you easily parse command line arguments,
using either flags or positional arguments.

For example, if you want to write a cp-like script that takes two
positional arguments for the source and destination file, you can write:

We can also specify arguments on the command lines using flags instead of
specifying them positionally. Let's change our example to specify the
input and output using the --src and --dest flags, using -s and -d
as short-hands for the flags:

See the Turtle.Options module for more details and utilities related to
parsing command line options. This module is built on top of the
optparse-applicative library, which provides even more extensive
functionality.

Conclusion

By this point you should be able to write basic shell scripts in Haskell. If
you would like to learn more advanced tricks, take the time to read the
documentation in these modules:

This library provides an extended suite of Unix-like utilities, but would
still benefit from adding more utilities for better parity with the Unix
ecosystem. Pull requests to add new utilities are highly welcome!