1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
module Propellor.Property.ConfFile (
-- * Generic conffiles with sections
SectionStart,
SectionPast,
AdjustSection,
InsertSection,
adjustSection,
-- * Windows .ini files
IniSection,
IniKey,
containsIniSetting,
lacksIniSetting,
hasIniSection,
lacksIniSection,
iniFileContains,
-- * Conffiles that are actually shell scripts setting env vars
ShellKey,
containsShellSetting,
lacksShellSetting,
) where
import Propellor.Base
import Propellor.Property.File
import Data.List (isPrefixOf, foldl')
-- | find the line that is the start of the wanted section (eg, == "<Foo>")
type SectionStart = Line -> Bool
-- | find a line that indicates we are past the section
-- (eg, a new section header)
type SectionPast = Line -> Bool
-- | run on all lines in the section, including the SectionStart line;
-- can add, delete, and modify lines, or even delete entire section
type AdjustSection = [Line] -> [Line]
-- | if SectionStart does not find the section in the file, this is used to
-- insert the section somewhere within it
type InsertSection = [Line] -> [Line]
-- | Adjusts a section of conffile.
adjustSection
:: Desc
-> SectionStart
-> SectionPast
-> AdjustSection
-> InsertSection
-> FilePath
-> Property UnixLike
adjustSection desc start past adjust insert = fileProperty desc go
where
go ls = let (pre, wanted, post) = foldl' find ([], [], []) ls
in if null wanted
then insert ls
else pre ++ adjust wanted ++ post
find (pre, wanted, post) l
| null wanted && null post && (not . start) l =
(pre ++ [l], wanted, post)
| (start l && null wanted && null post)
|| ((not . null) wanted && null post && (not . past) l) =
(pre, wanted ++ [l], post)
| otherwise = (pre, wanted, post ++ [l])
-- | Name of a section of an .ini file. This value is put
-- in square braces to generate the section header.
type IniSection = String
-- | Name of a configuration setting within a .ini file.
type IniKey = String
iniHeader :: IniSection -> String
iniHeader header = '[' : header ++ "]"
adjustIniSection
:: Desc
-> IniSection
-> AdjustSection
-> InsertSection
-> FilePath
-> Property UnixLike
adjustIniSection desc header =
adjustSection
desc
(== iniHeader header)
("[" `isPrefixOf`)
-- | Ensures that a .ini file exists and contains a section
-- with a key=value setting.
containsIniSetting :: FilePath -> (IniSection, IniKey, String) -> Property UnixLike
containsIniSetting f (header, key, value) = adjustIniSection
(f ++ " section [" ++ header ++ "] contains " ++ key ++ "=" ++ value)
header
go
(++ [confheader, confline]) -- add missing section at end
f
where
confheader = iniHeader header
confline = key ++ "=" ++ value
go [] = [confline]
go (l:ls) = if isKeyVal l then confline : ls else l : go ls
isKeyVal x = (filter (/= ' ') . takeWhile (/= '=')) x `elem` [key, '#':key]
-- | Removes a key=value setting from a section of an .ini file.
-- Note that the section heading is left in the file, so this is not a
-- perfect reversion of containsIniSetting.
lacksIniSetting :: FilePath -> (IniSection, IniKey, String) -> Property UnixLike
lacksIniSetting f (header, key, value) = adjustIniSection
(f ++ " section [" ++ header ++ "] lacks " ++ key ++ "=" ++ value)
header
(filter (/= confline))
id
f
where
confline = key ++ "=" ++ value
-- | Ensures that a .ini file exists and contains a section
-- with a given key=value list of settings.
hasIniSection :: FilePath -> IniSection -> [(IniKey, String)] -> Property UnixLike
hasIniSection f header keyvalues = adjustIniSection
("set " ++ f ++ " section [" ++ header ++ "]")
header
go
(++ confheader : conflines) -- add missing section at end
f
where
confheader = iniHeader header
conflines = map (\(key, value) -> key ++ "=" ++ value) keyvalues
go _ = confheader : conflines
-- | Ensures that a .ini file does not contain the specified section.
lacksIniSection :: FilePath -> IniSection -> Property UnixLike
lacksIniSection f header = adjustIniSection
(f ++ " lacks section [" ++ header ++ "]")
header
(const []) -- remove all lines of section
id -- add no lines if section is missing
f
-- | Specifies the whole content of a .ini file.
--
-- Revertijg this causes the file not to exist.
iniFileContains :: FilePath -> [(IniSection, [(IniKey, String)])] -> RevertableProperty UnixLike UnixLike
iniFileContains f l = f `hasContent` content <!> notPresent f
where
content = concatMap sectioncontent l
sectioncontent (section, keyvalues) = iniHeader section :
map (\(key, value) -> key ++ "=" ++ value) keyvalues
-- | Key for a shell conffile property. Conventionally uppercase letters and
-- numbers with underscores for separators. See files in </etc/default>.
type ShellKey = String
-- | Ensures a shell conffile (like those in </etc/default>) exists and has a
-- key=value pair.
--
-- Comments out any further settings of that key further down the
-- file, to avoid those taking precedence.
containsShellSetting :: FilePath -> (ShellKey, String) -> Property UnixLike
containsShellSetting f (k, v) = adjust `before` dedup
where
adjust = adjustSection
(f ++ " contains " ++ k ++ "=" ++ v)
isline
(not . isline)
(const [line])
(const [line])
f
dedup = fileProperty "" dedup' f
dedup' ls = let (pre, wanted, post) = foldl' find ([], [], []) ls
in pre ++ wanted ++ map commentIfIsline post
find (pre, wanted, post) l
| null wanted && (not . isline) l = (pre ++ [l], wanted, post)
| null wanted && isline l = (pre, [l], post)
| otherwise = (pre, wanted, post ++ [l])
-- some /etc/default files comment settings lines with '# '
-- and some use '#'; one advantage of just using '#' is that
-- it distinguishes settings values from prose comments
commentIfIsline l
| isline l = '#':l
| otherwise = l
isline s = (k ++ "=") `isPrefixOf` s
line = k ++ "=" ++ shellEscape v
-- | Comments out a key=value pair in a shell conffile.
--
-- Does not delete the file if empty, and does not uncomment any
-- lines, so not a perfect reversion of 'containsShellSetting'.
lacksShellSetting :: FilePath -> (ShellKey, String) -> Property UnixLike
lacksShellSetting f (k, v) =
fileProperty (f ++ "lacks shell setting " ++ k ++ "=" ++ v) go f
where
go ls = map commentOut ls
commentOut l
| (k ++ "=") `isPrefixOf` l = '#':l
| otherwise = l
|