summaryrefslogtreecommitdiff
path: root/src/Propellor/Property/ConfFile.hs
blob: 95e2c2d56ea31eef000fe639448e32c4a5b56413 (plain)
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])
		(++ [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