Merge: Activate C warnings in FFI code and revert `NativeString` to `char *`
[nit.git] / lib / standard / file.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2004-2008 Jean Privat <jean@pryen.org>
4 # Copyright 2008 Floréal Morandat <morandat@lirmm.fr>
5 # Copyright 2008 Jean-Sébastien Gélinas <calestar@gmail.com>
6 #
7 # This file is free software, which comes along with NIT. This software is
8 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
9 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 # PARTICULAR PURPOSE. You can modify it is you want, provided this header
11 # is kept unaltered, and a notification of the changes is added.
12 # You are allowed to redistribute it and sell it, alone or is a part of
13 # another product.
14
15 # File manipulations (create, read, write, etc.)
16 module file
17
18 intrude import stream
19 intrude import ropes
20 import string_search
21 import time
22 import gc
23
24 in "C Header" `{
25 #include <dirent.h>
26 #include <string.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #include <unistd.h>
30 #include <stdio.h>
31 #include <poll.h>
32 #include <errno.h>
33 `}
34
35 # `Stream` used to interact with a File or FileDescriptor
36 abstract class FileStream
37 super Stream
38 # The path of the file.
39 var path: nullable String = null
40
41 # The FILE *.
42 private var file: nullable NativeFile = null
43
44 # The status of a file. see POSIX stat(2).
45 #
46 # var f = new FileReader.open("/etc/issue")
47 # assert f.file_stat.is_file
48 #
49 # Return null in case of error
50 fun file_stat: nullable FileStat
51 do
52 var stat = _file.file_stat
53 if stat.address_is_null then return null
54 return new FileStat(stat)
55 end
56
57 # File descriptor of this file
58 fun fd: Int do return _file.fileno
59
60 redef fun close
61 do
62 if _file == null then return
63 if _file.address_is_null then
64 if last_error != null then return
65 last_error = new IOError("Cannot close unopened file")
66 return
67 end
68 var i = _file.io_close
69 if i != 0 then
70 last_error = new IOError("Close failed due to error {sys.errno.strerror}")
71 end
72 _file = null
73 end
74
75 # Sets the buffering mode for the current FileStream
76 #
77 # If the buf_size is <= 0, its value will be 512 by default
78 #
79 # The mode is any of the buffer_mode enumeration in `Sys`:
80 # - buffer_mode_full
81 # - buffer_mode_line
82 # - buffer_mode_none
83 fun set_buffering_mode(buf_size, mode: Int) do
84 if buf_size <= 0 then buf_size = 512
85 if _file.set_buffering_type(buf_size, mode) != 0 then
86 last_error = new IOError("Error while changing buffering type for FileStream, returned error {sys.errno.strerror}")
87 end
88 end
89 end
90
91 # `Stream` that can read from a File
92 class FileReader
93 super FileStream
94 super BufferedReader
95 super PollableReader
96 # Misc
97
98 # Open the same file again.
99 # The original path is reused, therefore the reopened file can be a different file.
100 #
101 # var f = new FileReader.open("/etc/issue")
102 # var l = f.read_line
103 # f.reopen
104 # assert l == f.read_line
105 fun reopen
106 do
107 if not eof and not _file.address_is_null then close
108 last_error = null
109 _file = new NativeFile.io_open_read(path.to_cstring)
110 if _file.address_is_null then
111 last_error = new IOError("Error: Opening file at '{path.as(not null)}' failed with '{sys.errno.strerror}'")
112 end_reached = true
113 return
114 end
115 end_reached = false
116 buffer_reset
117 end
118
119 redef fun close
120 do
121 super
122 buffer_reset
123 end_reached = true
124 end
125
126 redef fun fill_buffer
127 do
128 var nb = _file.io_read(_buffer, _buffer_capacity)
129 if nb <= 0 then
130 end_reached = true
131 nb = 0
132 end
133 _buffer_length = nb
134 _buffer_pos = 0
135 end
136
137 # End of file?
138 redef var end_reached = false
139
140 # Open the file at `path` for reading.
141 #
142 # var f = new FileReader.open("/etc/issue")
143 # assert not f.end_reached
144 # f.close
145 #
146 # In case of error, `last_error` is set
147 #
148 # f = new FileReader.open("/fail/does not/exist")
149 # assert f.end_reached
150 # assert f.last_error != null
151 init open(path: String)
152 do
153 self.path = path
154 prepare_buffer(10)
155 _file = new NativeFile.io_open_read(path.to_cstring)
156 if _file.address_is_null then
157 last_error = new IOError("Error: Opening file at '{path}' failed with '{sys.errno.strerror}'")
158 end_reached = true
159 end
160 end
161
162 # Creates a new File stream from a file descriptor
163 #
164 # This is a low-level method.
165 init from_fd(fd: Int) do
166 self.path = ""
167 prepare_buffer(1)
168 _file = fd.fd_to_stream(read_only)
169 if _file.address_is_null then
170 last_error = new IOError("Error: Converting fd {fd} to stream failed with '{sys.errno.strerror}'")
171 end_reached = true
172 end
173 end
174 end
175
176 # `Stream` that can write to a File
177 class FileWriter
178 super FileStream
179 super Writer
180
181 redef fun write_bytes(s) do
182 if last_error != null then return
183 if not _is_writable then
184 last_error = new IOError("cannot write to non-writable stream")
185 return
186 end
187 write_native(s.items, s.length)
188 end
189
190 redef fun write(s)
191 do
192 if last_error != null then return
193 if not _is_writable then
194 last_error = new IOError("cannot write to non-writable stream")
195 return
196 end
197 for i in s.substrings do write_native(i.to_cstring, i.length)
198 end
199
200 redef fun write_byte(value)
201 do
202 if last_error != null then return
203 if not _is_writable then
204 last_error = new IOError("Cannot write to non-writable stream")
205 return
206 end
207 if _file.address_is_null then
208 last_error = new IOError("Writing on a null stream")
209 _is_writable = false
210 return
211 end
212
213 var err = _file.write_byte(value)
214 if err != 1 then
215 # Big problem
216 last_error = new IOError("Problem writing a byte: {err}")
217 end
218 end
219
220 redef fun close
221 do
222 super
223 _is_writable = false
224 end
225 redef var is_writable = false
226
227 # Write `len` bytes from `native`.
228 private fun write_native(native: NativeString, len: Int)
229 do
230 if last_error != null then return
231 if not _is_writable then
232 last_error = new IOError("Cannot write to non-writable stream")
233 return
234 end
235 if _file.address_is_null then
236 last_error = new IOError("Writing on a null stream")
237 _is_writable = false
238 return
239 end
240 var err = _file.io_write(native, len)
241 if err != len then
242 # Big problem
243 last_error = new IOError("Problem in writing : {err} {len} \n")
244 end
245 end
246
247 # Open the file at `path` for writing.
248 init open(path: String)
249 do
250 _file = new NativeFile.io_open_write(path.to_cstring)
251 self.path = path
252 _is_writable = true
253 if _file.address_is_null then
254 last_error = new IOError("Error: Opening file at '{path}' failed with '{sys.errno.strerror}'")
255 is_writable = false
256 end
257 end
258
259 # Creates a new File stream from a file descriptor
260 init from_fd(fd: Int) do
261 self.path = ""
262 _file = fd.fd_to_stream(wipe_write)
263 _is_writable = true
264 if _file.address_is_null then
265 last_error = new IOError("Error: Opening stream from file descriptor {fd} failed with '{sys.errno.strerror}'")
266 _is_writable = false
267 end
268 end
269 end
270
271 redef class Int
272 # Creates a file stream from a file descriptor `fd` using the file access `mode`.
273 #
274 # NOTE: The `mode` specified must be compatible with the one used in the file descriptor.
275 private fun fd_to_stream(mode: NativeString): NativeFile is extern "file_int_fdtostream"
276 end
277
278 # Constant for read-only file streams
279 private fun read_only: NativeString do return once "r".to_cstring
280
281 # Constant for write-only file streams
282 #
283 # If a stream is opened on a file with this method,
284 # it will wipe the previous file if any.
285 # Else, it will create the file.
286 private fun wipe_write: NativeString do return once "w".to_cstring
287
288 ###############################################################################
289
290 # Standard input stream.
291 #
292 # The class of the default value of `sys.stdin`.
293 class Stdin
294 super FileReader
295
296 init do
297 _file = new NativeFile.native_stdin
298 path = "/dev/stdin"
299 prepare_buffer(1)
300 end
301
302 redef fun poll_in is extern "file_stdin_poll_in"
303 end
304
305 # Standard output stream.
306 #
307 # The class of the default value of `sys.stdout`.
308 class Stdout
309 super FileWriter
310 init do
311 _file = new NativeFile.native_stdout
312 path = "/dev/stdout"
313 _is_writable = true
314 set_buffering_mode(256, sys.buffer_mode_line)
315 end
316 end
317
318 # Standard error stream.
319 #
320 # The class of the default value of `sys.stderr`.
321 class Stderr
322 super FileWriter
323 init do
324 _file = new NativeFile.native_stderr
325 path = "/dev/stderr"
326 _is_writable = true
327 end
328 end
329
330 ###############################################################################
331
332 redef class Writable
333 # Like `write_to` but take care of creating the file
334 fun write_to_file(filepath: String)
335 do
336 var stream = new FileWriter.open(filepath)
337 write_to(stream)
338 stream.close
339 end
340 end
341
342 # Utility class to access file system services
343 #
344 # Usually created with `Text::to_path`.
345 class Path
346
347 private var path: String
348
349 # Path to this file
350 redef fun to_s do return path
351
352 # Name of the file name at `to_s`
353 #
354 # ~~~
355 # var path = "/tmp/somefile".to_path
356 # assert path.filename == "somefile"
357 # ~~~
358 var filename: String = path.basename("") is lazy
359
360 # Does the file at `path` exists?
361 fun exists: Bool do return stat != null
362
363 # Information on the file at `self` following symbolic links
364 #
365 # Returns `null` if there is no file at `self`.
366 #
367 # assert "/etc/".to_path.stat.is_dir
368 # assert "/etc/issue".to_path.stat.is_file
369 # assert "/fail/does not/exist".to_path.stat == null
370 #
371 # ~~~
372 # var p = "/tmp/".to_path
373 # var stat = p.stat
374 # if stat != null then # Does `p` exist?
375 # print "It's size is {stat.size}"
376 # if stat.is_dir then print "It's a directory"
377 # end
378 # ~~~
379 fun stat: nullable FileStat
380 do
381 var stat = path.to_cstring.file_stat
382 if stat.address_is_null then return null
383 return new FileStat(stat)
384 end
385
386 # Information on the file or link at `self`
387 #
388 # Do not follow symbolic links.
389 fun link_stat: nullable FileStat
390 do
391 var stat = path.to_cstring.file_lstat
392 if stat.address_is_null then return null
393 return new FileStat(stat)
394 end
395
396 # Delete a file from the file system, return `true` on success
397 fun delete: Bool do return path.to_cstring.file_delete
398
399 # Copy content of file at `path` to `dest`
400 #
401 # Require: `exists`
402 fun copy(dest: Path)
403 do
404 var input = open_ro
405 var output = dest.open_wo
406
407 while not input.eof do
408 var buffer = input.read(1024)
409 output.write buffer
410 end
411
412 input.close
413 output.close
414 end
415
416 # Open this file for reading
417 #
418 # Require: `exists and not link_stat.is_dir`
419 fun open_ro: FileReader
420 do
421 # TODO manage streams error when they are merged
422 return new FileReader.open(path)
423 end
424
425 # Open this file for writing
426 #
427 # Require: `not exists or not stat.is_dir`
428 fun open_wo: FileWriter
429 do
430 # TODO manage streams error when they are merged
431 return new FileWriter.open(path)
432 end
433
434 # Read all the content of the file
435 #
436 # ~~~
437 # var content = "/etc/issue".to_path.read_all
438 # print content
439 # ~~~
440 #
441 # See `Reader::read_all` for details.
442 fun read_all: String do return read_all_bytes.to_s
443
444 fun read_all_bytes: Bytes
445 do
446 var s = open_ro
447 var res = s.read_all_bytes
448 s.close
449 return res
450 end
451
452 # Read all the lines of the file
453 #
454 # ~~~
455 # var lines = "/etc/passwd".to_path.read_lines
456 #
457 # print "{lines.length} users"
458 #
459 # for l in lines do
460 # var fields = l.split(":")
461 # print "name={fields[0]} uid={fields[2]}"
462 # end
463 # ~~~
464 #
465 # See `Reader::read_lines` for details.
466 fun read_lines: Array[String]
467 do
468 var s = open_ro
469 var res = s.read_lines
470 s.close
471 return res
472 end
473
474 # Return an iterator on each line of the file
475 #
476 # ~~~
477 # for l in "/etc/passwd".to_path.each_line do
478 # var fields = l.split(":")
479 # print "name={fields[0]} uid={fields[2]}"
480 # end
481 # ~~~
482 #
483 # Note: the stream is automatically closed at the end of the file (see `LineIterator::close_on_finish`)
484 #
485 # See `Reader::each_line` for details.
486 fun each_line: LineIterator
487 do
488 var s = open_ro
489 var res = s.each_line
490 res.close_on_finish = true
491 return res
492 end
493
494
495 # Lists the name of the files contained within the directory at `path`
496 #
497 # Require: `exists and is_dir`
498 fun files: Array[Path]
499 do
500 var files = new Array[Path]
501 for filename in path.files do
502 files.add new Path(path / filename)
503 end
504 return files
505 end
506
507 # Delete a directory and all of its content, return `true` on success
508 #
509 # Does not go through symbolic links and may get stuck in a cycle if there
510 # is a cycle in the file system.
511 fun rmdir: Bool
512 do
513 var ok = true
514 for file in self.files do
515 var stat = file.link_stat
516 if stat.is_dir then
517 ok = file.rmdir and ok
518 else
519 ok = file.delete and ok
520 end
521 end
522
523 # Delete the directory itself
524 if ok then ok = path.to_cstring.rmdir and ok
525
526 return ok
527 end
528
529 redef fun ==(other) do return other isa Path and path.simplify_path == other.path.simplify_path
530 redef fun hash do return path.simplify_path.hash
531 end
532
533 # Information on a file
534 #
535 # Created by `Path::stat` and `Path::link_stat`.
536 #
537 # The information within this class is gathered when the instance is initialized
538 # it will not be updated if the targeted file is modified.
539 class FileStat
540 super Finalizable
541
542 # TODO private init
543
544 # The low-level status of a file
545 #
546 # See: POSIX stat(2)
547 private var stat: NativeFileStat
548
549 private var finalized = false
550
551 redef fun finalize
552 do
553 if not finalized then
554 stat.free
555 finalized = true
556 end
557 end
558
559 # Returns the last access time in seconds since Epoch
560 fun last_access_time: Int
561 do
562 assert not finalized
563 return stat.atime
564 end
565
566 # Returns the last access time
567 #
568 # alias for `last_access_time`
569 fun atime: Int do return last_access_time
570
571 # Returns the last modification time in seconds since Epoch
572 fun last_modification_time: Int
573 do
574 assert not finalized
575 return stat.mtime
576 end
577
578 # Returns the last modification time
579 #
580 # alias for `last_modification_time`
581 fun mtime: Int do return last_modification_time
582
583
584 # Size of the file at `path`
585 fun size: Int
586 do
587 assert not finalized
588 return stat.size
589 end
590
591 # Is self a regular file and not a device file, pipe, socket, etc.?
592 fun is_file: Bool
593 do
594 assert not finalized
595 return stat.is_reg
596 end
597
598 # Alias for `is_file`
599 fun is_reg: Bool do return is_file
600
601 # Is this a directory?
602 fun is_dir: Bool
603 do
604 assert not finalized
605 return stat.is_dir
606 end
607
608 # Is this a symbolic link?
609 fun is_link: Bool
610 do
611 assert not finalized
612 return stat.is_lnk
613 end
614
615 # FIXME Make the following POSIX only? or implement in some other way on Windows
616
617 # Returns the last status change time in seconds since Epoch
618 fun last_status_change_time: Int
619 do
620 assert not finalized
621 return stat.ctime
622 end
623
624 # Returns the last status change time
625 #
626 # alias for `last_status_change_time`
627 fun ctime: Int do return last_status_change_time
628
629 # Returns the permission bits of file
630 fun mode: Int
631 do
632 assert not finalized
633 return stat.mode
634 end
635
636 # Is this a character device?
637 fun is_chr: Bool
638 do
639 assert not finalized
640 return stat.is_chr
641 end
642
643 # Is this a block device?
644 fun is_blk: Bool
645 do
646 assert not finalized
647 return stat.is_blk
648 end
649
650 # Is this a FIFO pipe?
651 fun is_fifo: Bool
652 do
653 assert not finalized
654 return stat.is_fifo
655 end
656
657 # Is this a UNIX socket
658 fun is_sock: Bool
659 do
660 assert not finalized
661 return stat.is_sock
662 end
663 end
664
665 redef class Text
666 # Access file system related services on the path at `self`
667 fun to_path: Path do return new Path(to_s)
668 end
669
670 redef class String
671 # return true if a file with this names exists
672 fun file_exists: Bool do return to_cstring.file_exists
673
674 # The status of a file. see POSIX stat(2).
675 fun file_stat: nullable FileStat
676 do
677 var stat = to_cstring.file_stat
678 if stat.address_is_null then return null
679 return new FileStat(stat)
680 end
681
682 # The status of a file or of a symlink. see POSIX lstat(2).
683 fun file_lstat: nullable FileStat
684 do
685 var stat = to_cstring.file_lstat
686 if stat.address_is_null then return null
687 return new FileStat(stat)
688 end
689
690 # Remove a file, return true if success
691 fun file_delete: Bool do return to_cstring.file_delete
692
693 # Copy content of file at `self` to `dest`
694 fun file_copy_to(dest: String) do to_path.copy(dest.to_path)
695
696 # Remove the trailing extension `ext`.
697 #
698 # `ext` usually starts with a dot but could be anything.
699 #
700 # assert "file.txt".strip_extension(".txt") == "file"
701 # assert "file.txt".strip_extension("le.txt") == "fi"
702 # assert "file.txt".strip_extension("xt") == "file.t"
703 #
704 # if `ext` is not present, `self` is returned unmodified.
705 #
706 # assert "file.txt".strip_extension(".tar.gz") == "file.txt"
707 fun strip_extension(ext: String): String
708 do
709 if has_suffix(ext) then
710 return substring(0, length - ext.length)
711 end
712 return self
713 end
714
715 # Extract the basename of a path and remove the extension
716 #
717 # assert "/path/to/a_file.ext".basename(".ext") == "a_file"
718 # assert "path/to/a_file.ext".basename(".ext") == "a_file"
719 # assert "path/to".basename(".ext") == "to"
720 # assert "path/to/".basename(".ext") == "to"
721 # assert "path".basename("") == "path"
722 # assert "/path".basename("") == "path"
723 # assert "/".basename("") == "/"
724 # assert "".basename("") == ""
725 fun basename(ext: String): String
726 do
727 var l = length - 1 # Index of the last char
728 while l > 0 and self.chars[l] == '/' do l -= 1 # remove all trailing `/`
729 if l == 0 then return "/"
730 var pos = chars.last_index_of_from('/', l)
731 var n = self
732 if pos >= 0 then
733 n = substring(pos+1, l-pos)
734 end
735 return n.strip_extension(ext)
736 end
737
738 # Extract the dirname of a path
739 #
740 # assert "/path/to/a_file.ext".dirname == "/path/to"
741 # assert "path/to/a_file.ext".dirname == "path/to"
742 # assert "path/to".dirname == "path"
743 # assert "path/to/".dirname == "path"
744 # assert "path".dirname == "."
745 # assert "/path".dirname == "/"
746 # assert "/".dirname == "/"
747 # assert "".dirname == "."
748 fun dirname: String
749 do
750 var l = length - 1 # Index of the last char
751 while l > 0 and self.chars[l] == '/' do l -= 1 # remove all trailing `/`
752 var pos = chars.last_index_of_from('/', l)
753 if pos > 0 then
754 return substring(0, pos)
755 else if pos == 0 then
756 return "/"
757 else
758 return "."
759 end
760 end
761
762 # Return the canonicalized absolute pathname (see POSIX function `realpath`)
763 fun realpath: String do
764 var cs = to_cstring.file_realpath
765 var res = cs.to_s_with_copy
766 # cs.free_malloc # FIXME memory leak
767 return res
768 end
769
770 # Simplify a file path by remove useless `.`, removing `//`, and resolving `..`
771 #
772 # * `..` are not resolved if they start the path
773 # * starting `.` is simplified unless the path is empty
774 # * starting `/` is not removed
775 # * trailing `/` is removed
776 #
777 # Note that the method only work on the string:
778 #
779 # * no I/O access is performed
780 # * the validity of the path is not checked
781 #
782 # ~~~
783 # assert "some/./complex/../../path/from/../to/a////file//".simplify_path == "path/to/a/file"
784 # assert "../dir/file".simplify_path == "../dir/file"
785 # assert "dir/../../".simplify_path == ".."
786 # assert "dir/..".simplify_path == "."
787 # assert "//absolute//path/".simplify_path == "/absolute/path"
788 # assert "//absolute//../".simplify_path == "/"
789 # assert "/".simplify_path == "/"
790 # assert "../".simplify_path == ".."
791 # assert "./".simplify_path == "."
792 # assert "././././././".simplify_path == "."
793 # assert "./../dir".simplify_path == "../dir"
794 # assert "./dir".simplify_path == "dir"
795 # ~~~
796 fun simplify_path: String
797 do
798 var a = self.split_with("/")
799 var a2 = new Array[String]
800 for x in a do
801 if x == "." and not a2.is_empty then continue # skip `././`
802 if x == "" and not a2.is_empty then continue # skip `//`
803 if x == ".." and not a2.is_empty and a2.last != ".." then
804 if a2.last == "." then # do not skip `./../`
805 a2.pop # reduce `./../` in `../`
806 else # reduce `dir/../` in `/`
807 a2.pop
808 continue
809 end
810 else if not a2.is_empty and a2.last == "." then
811 a2.pop # reduce `./dir` in `dir`
812 end
813 a2.push(x)
814 end
815 if a2.is_empty then return "."
816 if a2.length == 1 and a2.first == "" then return "/"
817 return a2.join("/")
818 end
819
820 # Correctly join two path using the directory separator.
821 #
822 # Using a standard "{self}/{path}" does not work in the following cases:
823 #
824 # * `self` is empty.
825 # * `path` starts with `'/'`.
826 #
827 # This method ensures that the join is valid.
828 #
829 # assert "hello".join_path("world") == "hello/world"
830 # assert "hel/lo".join_path("wor/ld") == "hel/lo/wor/ld"
831 # assert "".join_path("world") == "world"
832 # assert "hello".join_path("/world") == "/world"
833 # assert "hello/".join_path("world") == "hello/world"
834 # assert "hello/".join_path("/world") == "/world"
835 #
836 # Note: You may want to use `simplify_path` on the result.
837 #
838 # Note: This method works only with POSIX paths.
839 fun join_path(path: String): String
840 do
841 if path.is_empty then return self
842 if self.is_empty then return path
843 if path.chars[0] == '/' then return path
844 if self.last == '/' then return "{self}{path}"
845 return "{self}/{path}"
846 end
847
848 # Convert the path (`self`) to a program name.
849 #
850 # Ensure the path (`self`) will be treated as-is by POSIX shells when it is
851 # used as a program name. In order to do that, prepend `./` if needed.
852 #
853 # assert "foo".to_program_name == "./foo"
854 # assert "/foo".to_program_name == "/foo"
855 # assert "".to_program_name == "./" # At least, your shell will detect the error.
856 fun to_program_name: String do
857 if self.has_prefix("/") then
858 return self
859 else
860 return "./{self}"
861 end
862 end
863
864 # Alias for `join_path`
865 #
866 # assert "hello" / "world" == "hello/world"
867 # assert "hel/lo" / "wor/ld" == "hel/lo/wor/ld"
868 # assert "" / "world" == "world"
869 # assert "/hello" / "/world" == "/world"
870 #
871 # This operator is quite useful for chaining changes of path.
872 # The next one being relative to the previous one.
873 #
874 # var a = "foo"
875 # var b = "/bar"
876 # var c = "baz/foobar"
877 # assert a/b/c == "/bar/baz/foobar"
878 fun /(path: String): String do return join_path(path)
879
880 # Returns the relative path needed to go from `self` to `dest`.
881 #
882 # assert "/foo/bar".relpath("/foo/baz") == "../baz"
883 # assert "/foo/bar".relpath("/baz/bar") == "../../baz/bar"
884 #
885 # If `self` or `dest` is relative, they are considered relatively to `getcwd`.
886 #
887 # In some cases, the result is still independent of the current directory:
888 #
889 # assert "foo/bar".relpath("..") == "../../.."
890 #
891 # In other cases, parts of the current directory may be exhibited:
892 #
893 # var p = "../foo/bar".relpath("baz")
894 # var c = getcwd.basename("")
895 # assert p == "../../{c}/baz"
896 #
897 # For path resolution independent of the current directory (eg. for paths in URL),
898 # or to use an other starting directory than the current directory,
899 # just force absolute paths:
900 #
901 # var start = "/a/b/c/d"
902 # var p2 = (start/"../foo/bar").relpath(start/"baz")
903 # assert p2 == "../../d/baz"
904 #
905 #
906 # Neither `self` or `dest` has to be real paths or to exist in directories since
907 # the resolution is only done with string manipulations and without any access to
908 # the underlying file system.
909 #
910 # If `self` and `dest` are the same directory, the empty string is returned:
911 #
912 # assert "foo".relpath("foo") == ""
913 # assert "foo/../bar".relpath("bar") == ""
914 #
915 # The empty string and "." designate both the current directory:
916 #
917 # assert "".relpath("foo/bar") == "foo/bar"
918 # assert ".".relpath("foo/bar") == "foo/bar"
919 # assert "foo/bar".relpath("") == "../.."
920 # assert "/" + "/".relpath(".") == getcwd
921 fun relpath(dest: String): String
922 do
923 var cwd = getcwd
924 var from = (cwd/self).simplify_path.split("/")
925 if from.last.is_empty then from.pop # case for the root directory
926 var to = (cwd/dest).simplify_path.split("/")
927 if to.last.is_empty then to.pop # case for the root directory
928
929 # Remove common prefixes
930 while not from.is_empty and not to.is_empty and from.first == to.first do
931 from.shift
932 to.shift
933 end
934
935 # Result is going up in `from` with ".." then going down following `to`
936 var from_len = from.length
937 if from_len == 0 then return to.join("/")
938 var up = "../"*(from_len-1) + ".."
939 if to.is_empty then return up
940 var res = up + "/" + to.join("/")
941 return res
942 end
943
944 # Create a directory (and all intermediate directories if needed)
945 #
946 # Return an error object in case of error.
947 #
948 # assert "/etc/".mkdir != null
949 fun mkdir: nullable Error
950 do
951 var dirs = self.split_with("/")
952 var path = new FlatBuffer
953 if dirs.is_empty then return null
954 if dirs[0].is_empty then
955 # it was a starting /
956 path.add('/')
957 end
958 var error: nullable Error = null
959 for d in dirs do
960 if d.is_empty then continue
961 path.append(d)
962 path.add('/')
963 var res = path.to_s.to_cstring.file_mkdir
964 if not res and error == null then
965 error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
966 end
967 end
968 return error
969 end
970
971 # Delete a directory and all of its content, return `true` on success
972 #
973 # Does not go through symbolic links and may get stuck in a cycle if there
974 # is a cycle in the filesystem.
975 #
976 # Return an error object in case of error.
977 #
978 # assert "/fail/does not/exist".rmdir != null
979 fun rmdir: nullable Error
980 do
981 var res = to_path.rmdir
982 if res then return null
983 var error = new IOError("Cannot change remove `{self}`: {sys.errno.strerror}")
984 return error
985 end
986
987 # Change the current working directory
988 #
989 # "/etc".chdir
990 # assert getcwd == "/etc"
991 # "..".chdir
992 # assert getcwd == "/"
993 #
994 # Return an error object in case of error.
995 #
996 # assert "/etc".chdir == null
997 # assert "/fail/does no/exist".chdir != null
998 # assert getcwd == "/etc" # unchanger
999 fun chdir: nullable Error
1000 do
1001 var res = to_cstring.file_chdir
1002 if res then return null
1003 var error = new IOError("Cannot change directory to `{self}`: {sys.errno.strerror}")
1004 return error
1005 end
1006
1007 # Return right-most extension (without the dot)
1008 #
1009 # Only the last extension is returned.
1010 # There is no special case for combined extensions.
1011 #
1012 # assert "file.txt".file_extension == "txt"
1013 # assert "file.tar.gz".file_extension == "gz"
1014 #
1015 # For file without extension, `null` is returned.
1016 # Hoever, for trailing dot, `""` is returned.
1017 #
1018 # assert "file".file_extension == null
1019 # assert "file.".file_extension == ""
1020 #
1021 # The starting dot of hidden files is never considered.
1022 #
1023 # assert ".file.txt".file_extension == "txt"
1024 # assert ".file".file_extension == null
1025 fun file_extension: nullable String
1026 do
1027 var last_slash = chars.last_index_of('.')
1028 if last_slash > 0 then
1029 return substring( last_slash+1, length )
1030 else
1031 return null
1032 end
1033 end
1034
1035 # Returns entries contained within the directory represented by self.
1036 #
1037 # var files = "/etc".files
1038 # assert files.has("issue")
1039 #
1040 # Returns an empty array in case of error
1041 #
1042 # files = "/etc/issue".files
1043 # assert files.is_empty
1044 #
1045 # TODO find a better way to handle errors and to give them back to the user.
1046 fun files: Array[String]
1047 do
1048 var res = new Array[String]
1049 var d = new NativeDir.opendir(to_cstring)
1050 if d.address_is_null then return res
1051
1052 loop
1053 var de = d.readdir
1054 if de.address_is_null then break
1055 var name = de.to_s_with_copy
1056 if name == "." or name == ".." then continue
1057 res.add name
1058 end
1059 d.closedir
1060
1061 return res
1062 end
1063 end
1064
1065 redef class NativeString
1066 private fun file_exists: Bool is extern "string_NativeString_NativeString_file_exists_0"
1067 private fun file_stat: NativeFileStat is extern "string_NativeString_NativeString_file_stat_0"
1068 private fun file_lstat: NativeFileStat `{
1069 struct stat* stat_element;
1070 int res;
1071 stat_element = malloc(sizeof(struct stat));
1072 res = lstat(self, stat_element);
1073 if (res == -1) return NULL;
1074 return stat_element;
1075 `}
1076 private fun file_mkdir: Bool is extern "string_NativeString_NativeString_file_mkdir_0"
1077 private fun rmdir: Bool `{ return !rmdir(self); `}
1078 private fun file_delete: Bool is extern "string_NativeString_NativeString_file_delete_0"
1079 private fun file_chdir: Bool is extern "string_NativeString_NativeString_file_chdir_0"
1080 private fun file_realpath: NativeString is extern "file_NativeString_realpath"
1081 end
1082
1083 # This class is system dependent ... must reify the vfs
1084 private extern class NativeFileStat `{ struct stat * `}
1085 # Returns the permission bits of file
1086 fun mode: Int is extern "file_FileStat_FileStat_mode_0"
1087 # Returns the last access time
1088 fun atime: Int is extern "file_FileStat_FileStat_atime_0"
1089 # Returns the last status change time
1090 fun ctime: Int is extern "file_FileStat_FileStat_ctime_0"
1091 # Returns the last modification time
1092 fun mtime: Int is extern "file_FileStat_FileStat_mtime_0"
1093 # Returns the size
1094 fun size: Int is extern "file_FileStat_FileStat_size_0"
1095
1096 # Returns true if it is a regular file (not a device file, pipe, sockect, ...)
1097 fun is_reg: Bool `{ return S_ISREG(self->st_mode); `}
1098 # Returns true if it is a directory
1099 fun is_dir: Bool `{ return S_ISDIR(self->st_mode); `}
1100 # Returns true if it is a character device
1101 fun is_chr: Bool `{ return S_ISCHR(self->st_mode); `}
1102 # Returns true if it is a block device
1103 fun is_blk: Bool `{ return S_ISBLK(self->st_mode); `}
1104 # Returns true if the type is fifo
1105 fun is_fifo: Bool `{ return S_ISFIFO(self->st_mode); `}
1106 # Returns true if the type is a link
1107 fun is_lnk: Bool `{ return S_ISLNK(self->st_mode); `}
1108 # Returns true if the type is a socket
1109 fun is_sock: Bool `{ return S_ISSOCK(self->st_mode); `}
1110 end
1111
1112 # Instance of this class are standard FILE * pointers
1113 private extern class NativeFile `{ FILE* `}
1114 fun io_read(buf: NativeString, len: Int): Int is extern "file_NativeFile_NativeFile_io_read_2"
1115 fun io_write(buf: NativeString, len: Int): Int is extern "file_NativeFile_NativeFile_io_write_2"
1116 fun write_byte(value: Int): Int `{
1117 unsigned char b = (unsigned char)value;
1118 return fwrite(&b, 1, 1, self);
1119 `}
1120 fun io_close: Int is extern "file_NativeFile_NativeFile_io_close_0"
1121 fun file_stat: NativeFileStat is extern "file_NativeFile_NativeFile_file_stat_0"
1122 fun fileno: Int `{ return fileno(self); `}
1123 # Flushes the buffer, forcing the write operation
1124 fun flush: Int is extern "fflush"
1125 # Used to specify how the buffering will be handled for the current stream.
1126 fun set_buffering_type(buf_length: Int, mode: Int): Int is extern "file_NativeFile_NativeFile_set_buffering_type_0"
1127
1128 new io_open_read(path: NativeString) is extern "file_NativeFileCapable_NativeFileCapable_io_open_read_1"
1129 new io_open_write(path: NativeString) is extern "file_NativeFileCapable_NativeFileCapable_io_open_write_1"
1130 new native_stdin is extern "file_NativeFileCapable_NativeFileCapable_native_stdin_0"
1131 new native_stdout is extern "file_NativeFileCapable_NativeFileCapable_native_stdout_0"
1132 new native_stderr is extern "file_NativeFileCapable_NativeFileCapable_native_stderr_0"
1133 end
1134
1135 # Standard `DIR*` pointer
1136 private extern class NativeDir `{ DIR* `}
1137
1138 # Open a directory
1139 new opendir(path: NativeString) `{ return opendir(path); `}
1140
1141 # Close a directory
1142 fun closedir `{ closedir(self); `}
1143
1144 # Read the next directory entry
1145 fun readdir: NativeString `{
1146 struct dirent *de;
1147 de = readdir(self);
1148 if (!de) return NULL;
1149 return de->d_name;
1150 `}
1151 end
1152
1153 redef class Sys
1154
1155 # Standard input
1156 var stdin: PollableReader = new Stdin is protected writable, lazy
1157
1158 # Standard output
1159 var stdout: Writer = new Stdout is protected writable, lazy
1160
1161 # Standard output for errors
1162 var stderr: Writer = new Stderr is protected writable, lazy
1163
1164 # Enumeration for buffer mode full (flushes when buffer is full)
1165 fun buffer_mode_full: Int is extern "file_Sys_Sys_buffer_mode_full_0"
1166 # Enumeration for buffer mode line (flushes when a `\n` is encountered)
1167 fun buffer_mode_line: Int is extern "file_Sys_Sys_buffer_mode_line_0"
1168 # Enumeration for buffer mode none (flushes ASAP when something is written)
1169 fun buffer_mode_none: Int is extern "file_Sys_Sys_buffer_mode_none_0"
1170
1171 # returns first available stream to read or write to
1172 # return null on interruption (possibly a signal)
1173 protected fun poll( streams : Sequence[FileStream] ) : nullable FileStream
1174 do
1175 var in_fds = new Array[Int]
1176 var out_fds = new Array[Int]
1177 var fd_to_stream = new HashMap[Int,FileStream]
1178 for s in streams do
1179 var fd = s.fd
1180 if s isa FileReader then in_fds.add( fd )
1181 if s isa FileWriter then out_fds.add( fd )
1182
1183 fd_to_stream[fd] = s
1184 end
1185
1186 var polled_fd = intern_poll( in_fds, out_fds )
1187
1188 if polled_fd == null then
1189 return null
1190 else
1191 return fd_to_stream[polled_fd]
1192 end
1193 end
1194
1195 private fun intern_poll(in_fds: Array[Int], out_fds: Array[Int]) : nullable Int is extern import Array[Int].length, Array[Int].[], Int.as(nullable Int) `{
1196 int in_len, out_len, total_len;
1197 struct pollfd *c_fds;
1198 int i;
1199 int first_polled_fd = -1;
1200 int result;
1201
1202 in_len = Array_of_Int_length( in_fds );
1203 out_len = Array_of_Int_length( out_fds );
1204 total_len = in_len + out_len;
1205 c_fds = malloc( sizeof(struct pollfd) * total_len );
1206
1207 /* input streams */
1208 for ( i=0; i<in_len; i ++ ) {
1209 int fd;
1210 fd = Array_of_Int__index( in_fds, i );
1211
1212 c_fds[i].fd = fd;
1213 c_fds[i].events = POLLIN;
1214 }
1215
1216 /* output streams */
1217 for ( i=0; i<out_len; i ++ ) {
1218 int fd;
1219 fd = Array_of_Int__index( out_fds, i );
1220
1221 c_fds[i].fd = fd;
1222 c_fds[i].events = POLLOUT;
1223 }
1224
1225 /* poll all fds, unlimited timeout */
1226 result = poll( c_fds, total_len, -1 );
1227
1228 if ( result > 0 ) {
1229 /* analyse results */
1230 for ( i=0; i<total_len; i++ )
1231 if ( c_fds[i].revents & c_fds[i].events || /* awaited event */
1232 c_fds[i].revents & POLLHUP ) /* closed */
1233 {
1234 first_polled_fd = c_fds[i].fd;
1235 break;
1236 }
1237
1238 return Int_as_nullable( first_polled_fd );
1239 }
1240 else if ( result < 0 )
1241 fprintf( stderr, "Error in Stream:poll: %s\n", strerror( errno ) );
1242
1243 return null_Int();
1244 `}
1245
1246 end
1247
1248 # Print `objects` on the standard output (`stdout`).
1249 fun printn(objects: Object...)
1250 do
1251 sys.stdout.write(objects.plain_to_s)
1252 end
1253
1254 # Print an `object` on the standard output (`stdout`) and add a newline.
1255 fun print(object: Object)
1256 do
1257 sys.stdout.write(object.to_s)
1258 sys.stdout.write("\n")
1259 end
1260
1261 # Print `object` on the error output (`stderr` or a log system)
1262 fun print_error(object: Object)
1263 do
1264 sys.stderr.write object.to_s
1265 sys.stderr.write "\n"
1266 end
1267
1268 # Read a character from the standard input (`stdin`).
1269 fun getc: Char
1270 do
1271 var c = sys.stdin.read_char
1272 if c == null then return '\1'
1273 return c
1274 end
1275
1276 # Read a line from the standard input (`stdin`).
1277 fun gets: String
1278 do
1279 return sys.stdin.read_line
1280 end
1281
1282 # Return the working (current) directory
1283 fun getcwd: String do return file_getcwd.to_s
1284 private fun file_getcwd: NativeString is extern "string_NativeString_NativeString_file_getcwd_0"