-
Notifications
You must be signed in to change notification settings - Fork 2
/
file.carp
302 lines (247 loc) · 9.95 KB
/
file.carp
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
(relative-include "file_helper.h")
(load "src/reader.carp")
(load "src/writer.carp")
(defmodule Dir
(register open (Fn [(Ptr CChar)] (Ptr DIR)) "opendir")
(register close (Fn [(Ptr DIR)] ()) "closedir")
(register read (Fn [(Ptr DIR)] (Ptr DirEntry)) "readdir")
(register d-name (Fn [(Ptr DirEntry)] (Ptr CChar)) "Dir_d_name")
)
(deftype File [name String, mode String, file (Ptr FILE)])
(deftype WalkOptions [
recursive? Bool,
follow-links? Bool,
dotfiles? Bool,
match-dirs? Bool,
])
(defmacro until-expr [bs expr body]
(list 'while true
(list 'let bs
(list 'if expr
'(break)
(list 'do body)))))
(doc File "A simple file abstraction for Carp.
## Installation
You can obtain this library like so:
```
(load \"[email protected]:carpentry-org/[email protected]\")
```
## Usage
The main type involved in working with this library is `File`, naturally. All
file operations depend on the file being opened.
```
; returns a Result containing a File you can write to
(File.open \"example.txt\")
; returns a Result containing a File you append to
(File.open-with \"example.txt\" \"a\")
```
If the file couldn’t be opened due to it not existing or file permission errors,
a `Result.Error` with an error message is returned.
The file permissions follow the [file modes](https://www.tutorialspoint.com/c_standard_library/c_function_fopen.htm)
in UNIX. The default is `\"a+\"`, or writing/reading. Generally, files have a
`name`, `mode`, and `file` property; you can read safely from them, but writing
to them directly is discouraged.
Before you end your operations on the file, it is good practice to close the
file again. In Carp, we do this using `close`.
```
(close f)
```
You can `read` from the file—or `read-all`, if you don’t care about length—,
`write` to it, `remove` it, or `rewind` the file buffer.
```
(write &f \"hi\")
(rewind &f)
(IO.println &(read-all &f))
```
All of these will check whether the files are actually readable and/or writable
before performing any IO actions and return a `Result.Error` if they can’t.
You can also ask about the modes of the file, using the functions `readable?`,
`writable?`, or `binary-mode?`.")
(defmodule File
(private stat)
(hidden stat)
(register stat (Fn [(Ref String)] Int) "File_stat")
(private is-link)
(hidden is-link)
(register is-link (Fn [Int] Bool) "S_ISLNK")
(private is-dir)
(hidden is-dir)
(register is-dir (Fn [Int] Bool) "S_ISDIR")
(hidden init)
(hidden file)
(hidden set-mode)
(hidden set-file)
(hidden set-name)
(hidden set-mode!)
(hidden set-file!)
(hidden set-name!)
(hidden update-mode)
(hidden update-file)
(hidden update-name)
; the actual low-level implementation of File.walk
(defn walk-recur [dname op spec]
(let [dir (Dir.open (cstr dname))
res (Result.Success 0)]
(if (null? dir)
(Result.Error (fmt "Can’t open '%s'" dname))
(do
(until-expr
[dent (Dir.read dir)]
(null? dent)
(let [name &(from-cstr (Dir.d-name dent))]
(cond
(and (not @(WalkOptions.dotfiles? spec)) (starts-with? name ".")) ()
(or (= name ".") (= name "..")) ()
(let-do [f (fmt "%s/%s" dname name)
lres (stat &f)]
(cond
(= -1 lres)
(do
(set! res (Result.Error (fmt "Can’t stat '%s'" &f)))
(break))
(and (is-link lres) (not @(WalkOptions.follow-links? spec))) ()
(is-dir lres)
(do
(when @(WalkOptions.recursive? spec)
(walk-recur &f op spec))
(when @(WalkOptions.match-dirs? spec)
(~op &f)))
(~op &f))))))
(Dir.close dir)
res))))
(hidden walk-recur)
(private walk-recur)
(doc default-walk "is the default options used by [`walk`](#walk).
Initially set to recursive, not following links, and not matching directories
or dotfiles.")
(def default-walk (WalkOptions.init true false false false))
(doc walk-mode "constructs a mode for [`walk-with`](#walk-with).")
(defn walk-mode [recursive? follow-links? dotfiles? match-dirs?]
(WalkOptions.init recursive? follow-links? dotfiles? match-dirs?))
(doc walk-with "walks a directory with a custom [walk mode](#walk-mode).
Returns a result containing either nothing, or an error if the directory
couldn’t be opened due to permission errors or if the user tried to open a
nonexistant directory.")
(defn walk-with [s callback options]
(walk-recur s callback options))
(doc walk "walks a directory with the default walk mode (see also
[`default-walk`](#default-walk)).
Returns a result containing either nothing, or an error if the directory
couldn’t be opened due to permission errors or if the user tried to open a
nonexistant directory.")
(defn walk [s callback]
(walk-with s callback &default-walk))
(doc map-with "walks a directory with a custom [walk mode](#walk-mode).
Returns a result containing either the results of the walker function as an
array or an error if the directory couldn’t be opened due to permission errors
or if the user tried to open a nonexistant directory.")
(defn map-with [s callback options]
(let [res []
res-ref &res]
(Result.map
(walk-with s &(fn [e] (Array.push-back! res-ref (~callback e))) options)
&(fn [_] @res-ref))))
(doc map "walks a directory with the default walk mode (see also
[`default-walk`](#default-walk)).
Returns a result containing either the results of the walker function as an
array or an error if the directory couldn’t be opened due to permission errors
or if the user tried to open a nonexistant directory.")
(defn map [s callback]
(map-with s callback &default-walk))
(doc contents-with "walks a directory with a custom [walk mode](#walk-mode).
Returns a result containing either the directory contents or an error if the
directory couldn’t be opened due to permission errors or if the user tried to
open a nonexistant directory.")
(defn contents-with [s options]
(map-with s © options))
(doc contents "walks a directory with the default walk mode (see also
[`default-walk`](#default-walk)).
Returns a result containing either the contents of that directory or an error
if the directory couldn’t be opened due to permission errors or if the user
tried to open a nonexistant directory.")
(defn contents [s]
(contents-with s &default-walk))
(doc default-mode "is the default file mode, used by [`open`](#open).
Initially set to `a+`.")
(def default-mode "a+")
(doc readable? "checks whether a file is readable by examining its mode.")
(defn readable? [f]
(or (> (String.index-of (mode f) \+) -1)
(> (String.index-of (mode f) \r) -1)))
(doc writable? "checks whether a file is writable by examining its mode.")
(defn writable? [f]
(or (> (String.index-of (mode f) \w) -1)
(> (String.index-of (mode f) \a) -1)))
(doc binary-mode? "checks whether a file is in binary mode by examining its
mode.")
(defn binary-mode? [f]
(> (String.index-of (mode f) \b) -1))
(doc open-with "opens a file with custom file mode.
Returns a result containing either a file, or an error if the file couldn’t be
opened due to permission errors or if the user tried to read from a nonexistant
file.")
(defn open-with [name mode]
(let [f (IO.Raw.fopen name mode)]
(if (null? f)
(Result.Error (fmt "File “%s” could not be opened!" name))
(Result.Success (init @name @mode (IO.Raw.fopen name mode))))))
(doc open "opens a file with the default file mode (see also
[`default-mode`](#default-mode)).
Returns a result containing either a file, or an error if the file couldn’t be
opened due to permission errors or if the user tried to read from a nonexistant
file.")
(defn open [name]
(open-with name default-mode))
(doc close "closes a file and takes ownership.")
(defn close [f]
(ignore (IO.Raw.fclose @(file &f))))
(doc remove "removes a file from the file system.")
(defn remove [f]
(ignore (IO.Raw.unlink (name f))))
(doc write
"Writes a value to a file. "
""
("This function is generic in its input. You can " false)
("provide a custom reader implementation by implementing the " false)
"write-to-file interface")
(defn write [f obj]
(if (writable? f)
(write-to-file f obj)
(Result.Error (fmt "The file “%s” is not writable" (name f)))))
(doc read
"Reads len values from a file."
""
("This function is generic in its return argument. You can " false)
("provide a custom reader implementation by implementing the " false)
"read-from-file interface."
""
("The surrounding context typically determine's the return type." false)
("if no implementation of read-from-file exists for the type, you'll " false)
"need to provide one. You can also use `the` to specify the return type."
"For example (to read the first 3 bytes of the file):"
"```"
"(match (File.open-with \"test-file.txt\" \"r\")"
" (Result.Success f) (the (Result (Array Byte) String) (File.read &f 3))"
" (Result.Error x) (Result.Error x))))"
"```")
(defn read [f len]
(if (readable? f)
(read-from-file f len)
(Result.Error (fmt "The file “%s” is not readable" (name f)))))
(doc read-all "reads the entire file content of a file.
Returns a result containing the string on success and an error if the file is
not readable.")
(defn read-all [f]
(let-do [fd @(file f)]
(ignore (IO.Raw.fseek fd 0 IO.SEEK-END))
(let-do [len (IO.Raw.ftell fd)]
(ignore (IO.Raw.fseek fd 0 IO.SEEK-SET))
(read f len))))
(doc rewind "rewinds a file.")
(defn rewind [f]
(IO.Raw.rewind @(file f)))
)
(load "src/byte-reader.carp")
(load "src/string-reader.carp")
(load "src/byte-writer.carp")
(load "src/string-writer.carp")