1 | {-# LANGUAGE CPP #-} |
2 | {-# LANGUAGE DeriveDataTypeable #-} |
3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} |
4 | {-# LANGUAGE OverloadedStrings #-} |
5 | |
6 | module Turtle.Line |
7 | ( Line |
8 | , lineToText |
9 | , textToLines |
10 | , linesToText |
11 | , textToLine |
12 | , unsafeTextToLine |
13 | , NewlineForbidden(..) |
14 | ) where |
15 | |
16 | import Data.Text (Text) |
17 | import qualified Data.Text as Text |
18 | #if __GLASGOW_HASKELL__ >= 708 |
19 | import Data.Coerce |
20 | #endif |
21 | import Data.List.NonEmpty (NonEmpty(..)) |
22 | import Data.String |
23 | #if __GLASGOW_HASKELL__ >= 710 |
24 | #else |
25 | import Data.Monoid |
26 | #endif |
27 | import Data.Maybe |
28 | import Data.Typeable |
29 | import Control.Exception |
30 | |
31 | import qualified Data.List.NonEmpty |
32 | |
33 | -- | The `NewlineForbidden` exception is thrown when you construct a `Line` |
34 | -- using an overloaded string literal or by calling `fromString` explicitly |
35 | -- and the supplied string contains newlines. This is a programming error to |
36 | -- do so: if you aren't sure that the input string is newline-free, do not |
37 | -- rely on the @`IsString` `Line`@ instance. |
38 | -- |
39 | -- When debugging, it might be useful to look for implicit invocations of |
40 | -- `fromString` for `Line`: |
41 | -- |
42 | -- > >>> sh (do { line <- "Hello\nWorld"; echo line }) |
43 | -- > *** Exception: NewlineForbidden |
44 | -- |
45 | -- In the above example, `echo` expects its argument to be a `Line`, thus |
46 | -- @line :: `Line`@. Since we bind @line@ in `Shell`, the string literal |
47 | -- @\"Hello\\nWorld\"@ has type @`Shell` `Line`@. The |
48 | -- @`IsString` (`Shell` `Line`)@ instance delegates the construction of a |
49 | -- `Line` to the @`IsString` `Line`@ instance, where the exception is thrown. |
50 | -- |
51 | -- To fix the problem, use `textToLines`: |
52 | -- |
53 | -- > >>> sh (do { line <- select (textToLines "Hello\nWorld"); echo line }) |
54 | -- > Hello |
55 | -- > World |
56 | data NewlineForbidden = NewlineForbidden |
57 | deriving (Show, Typeable) |
58 | |
59 | instance Exception NewlineForbidden |
60 | |
61 | -- | A line of text (does not contain newlines). |
62 | newtype Line = Line Text |
63 | deriving (Eq, Ord, Show, Monoid) |
64 | |
65 | #if __GLASGOW_HASKELL__ >= 804 |
66 | instance Semigroup Line where |
67 | (<>) = mappend |
68 | #endif |
69 | |
70 | instance IsString Line where |
71 | fromString = fromMaybe (throw NewlineForbidden) . textToLine . fromString |
72 | |
73 | -- | Convert a line to a text value. |
74 | lineToText :: Line -> Text |
75 | lineToText (Line t) = t |
76 | |
77 | -- | Split text into lines. The inverse of `linesToText`. |
78 | textToLines :: Text -> NonEmpty Line |
79 | textToLines = |
80 | #if __GLASGOW_HASKELL__ >= 708 |
81 | Data.List.NonEmpty.fromList . coerce (Text.splitOn "\n") |
82 | #else |
83 | Data.List.NonEmpty.fromList . map unsafeTextToLine . Text.splitOn "\n" |
84 | #endif |
85 | |
86 | -- | Merge lines into a single text value. |
87 | linesToText :: [Line] -> Text |
88 | linesToText = |
89 | #if __GLASGOW_HASKELL__ >= 708 |
90 | coerce Text.unlines |
91 | #else |
92 | Text.unlines . map lineToText |
93 | #endif |
94 | |
95 | -- | Try to convert a text value into a line. |
96 | -- Precondition (checked): the argument does not contain newlines. |
97 | textToLine :: Text -> Maybe Line |
98 | textToLine = fromSingleton . textToLines |
99 | where |
100 | fromSingleton (a :| []) = Just a |
101 | fromSingleton _ = Nothing |
102 | |
103 | -- | Convert a text value into a line. |
104 | -- Precondition (unchecked): the argument does not contain newlines. |
105 | unsafeTextToLine :: Text -> Line |
106 | unsafeTextToLine = Line |