Merge: introduce plain_to_s
[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 not removed
774 # * trailing "/" is removed
775 #
776 # Note that the method only work on the string:
777 #
778 # * no I/O access is performed
779 # * the validity of the path is not checked
780 #
781 # ~~~
782 # assert "some/./complex/../../path/from/../to/a////file//".simplify_path == "path/to/a/file"
783 # assert "../dir/file".simplify_path == "../dir/file"
784 # assert "dir/../../".simplify_path == ".."
785 # assert "dir/..".simplify_path == "."
786 # assert "//absolute//path/".simplify_path == "/absolute/path"
787 # assert "//absolute//../".simplify_path == "/"
788 # ~~~
789 fun simplify_path: String
790 do
791 var a = self.split_with("/")
792 var a2 = new Array[String]
793 for x in a do
794 if x == "." then continue
795 if x == "" and not a2.is_empty then continue
796 if x == ".." and not a2.is_empty and a2.last != ".." then
797 a2.pop
798 continue
799 end
800 a2.push(x)
801 end
802 if a2.is_empty then return "."
803 if a2.length == 1 and a2.first == "" then return "/"
804 return a2.join("/")
805 end
806
807 # Correctly join two path using the directory separator.
808 #
809 # Using a standard "{self}/{path}" does not work in the following cases:
810 #
811 # * `self` is empty.
812 # * `path` starts with `'/'`.
813 #
814 # This method ensures that the join is valid.
815 #
816 # assert "hello".join_path("world") == "hello/world"
817 # assert "hel/lo".join_path("wor/ld") == "hel/lo/wor/ld"
818 # assert "".join_path("world") == "world"
819 # assert "hello".join_path("/world") == "/world"
820 # assert "hello/".join_path("world") == "hello/world"
821 # assert "hello/".join_path("/world") == "/world"
822 #
823 # Note: You may want to use `simplify_path` on the result.
824 #
825 # Note: This method works only with POSIX paths.
826 fun join_path(path: String): String
827 do
828 if path.is_empty then return self
829 if self.is_empty then return path
830 if path.chars[0] == '/' then return path
831 if self.last == '/' then return "{self}{path}"
832 return "{self}/{path}"
833 end
834
835 # Convert the path (`self`) to a program name.
836 #
837 # Ensure the path (`self`) will be treated as-is by POSIX shells when it is
838 # used as a program name. In order to do that, prepend `./` if needed.
839 #
840 # assert "foo".to_program_name == "./foo"
841 # assert "/foo".to_program_name == "/foo"
842 # assert "".to_program_name == "./" # At least, your shell will detect the error.
843 fun to_program_name: String do
844 if self.has_prefix("/") then
845 return self
846 else
847 return "./{self}"
848 end
849 end
850
851 # Alias for `join_path`
852 #
853 # assert "hello" / "world" == "hello/world"
854 # assert "hel/lo" / "wor/ld" == "hel/lo/wor/ld"
855 # assert "" / "world" == "world"
856 # assert "/hello" / "/world" == "/world"
857 #
858 # This operator is quite useful for chaining changes of path.
859 # The next one being relative to the previous one.
860 #
861 # var a = "foo"
862 # var b = "/bar"
863 # var c = "baz/foobar"
864 # assert a/b/c == "/bar/baz/foobar"
865 fun /(path: String): String do return join_path(path)
866
867 # Returns the relative path needed to go from `self` to `dest`.
868 #
869 # assert "/foo/bar".relpath("/foo/baz") == "../baz"
870 # assert "/foo/bar".relpath("/baz/bar") == "../../baz/bar"
871 #
872 # If `self` or `dest` is relative, they are considered relatively to `getcwd`.
873 #
874 # In some cases, the result is still independent of the current directory:
875 #
876 # assert "foo/bar".relpath("..") == "../../.."
877 #
878 # In other cases, parts of the current directory may be exhibited:
879 #
880 # var p = "../foo/bar".relpath("baz")
881 # var c = getcwd.basename("")
882 # assert p == "../../{c}/baz"
883 #
884 # For path resolution independent of the current directory (eg. for paths in URL),
885 # or to use an other starting directory than the current directory,
886 # just force absolute paths:
887 #
888 # var start = "/a/b/c/d"
889 # var p2 = (start/"../foo/bar").relpath(start/"baz")
890 # assert p2 == "../../d/baz"
891 #
892 #
893 # Neither `self` or `dest` has to be real paths or to exist in directories since
894 # the resolution is only done with string manipulations and without any access to
895 # the underlying file system.
896 #
897 # If `self` and `dest` are the same directory, the empty string is returned:
898 #
899 # assert "foo".relpath("foo") == ""
900 # assert "foo/../bar".relpath("bar") == ""
901 #
902 # The empty string and "." designate both the current directory:
903 #
904 # assert "".relpath("foo/bar") == "foo/bar"
905 # assert ".".relpath("foo/bar") == "foo/bar"
906 # assert "foo/bar".relpath("") == "../.."
907 # assert "/" + "/".relpath(".") == getcwd
908 fun relpath(dest: String): String
909 do
910 var cwd = getcwd
911 var from = (cwd/self).simplify_path.split("/")
912 if from.last.is_empty then from.pop # case for the root directory
913 var to = (cwd/dest).simplify_path.split("/")
914 if to.last.is_empty then to.pop # case for the root directory
915
916 # Remove common prefixes
917 while not from.is_empty and not to.is_empty and from.first == to.first do
918 from.shift
919 to.shift
920 end
921
922 # Result is going up in `from` with ".." then going down following `to`
923 var from_len = from.length
924 if from_len == 0 then return to.join("/")
925 var up = "../"*(from_len-1) + ".."
926 if to.is_empty then return up
927 var res = up + "/" + to.join("/")
928 return res
929 end
930
931 # Create a directory (and all intermediate directories if needed)
932 #
933 # Return an error object in case of error.
934 #
935 # assert "/etc/".mkdir != null
936 fun mkdir: nullable Error
937 do
938 var dirs = self.split_with("/")
939 var path = new FlatBuffer
940 if dirs.is_empty then return null
941 if dirs[0].is_empty then
942 # it was a starting /
943 path.add('/')
944 end
945 var error: nullable Error = null
946 for d in dirs do
947 if d.is_empty then continue
948 path.append(d)
949 path.add('/')
950 var res = path.to_s.to_cstring.file_mkdir
951 if not res and error == null then
952 error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
953 end
954 end
955 return error
956 end
957
958 # Delete a directory and all of its content, return `true` on success
959 #
960 # Does not go through symbolic links and may get stuck in a cycle if there
961 # is a cycle in the filesystem.
962 #
963 # Return an error object in case of error.
964 #
965 # assert "/fail/does not/exist".rmdir != null
966 fun rmdir: nullable Error
967 do
968 var res = to_path.rmdir
969 if res then return null
970 var error = new IOError("Cannot change remove `{self}`: {sys.errno.strerror}")
971 return error
972 end
973
974 # Change the current working directory
975 #
976 # "/etc".chdir
977 # assert getcwd == "/etc"
978 # "..".chdir
979 # assert getcwd == "/"
980 #
981 # Return an error object in case of error.
982 #
983 # assert "/etc".chdir == null
984 # assert "/fail/does no/exist".chdir != null
985 # assert getcwd == "/etc" # unchanger
986 fun chdir: nullable Error
987 do
988 var res = to_cstring.file_chdir
989 if res then return null
990 var error = new IOError("Cannot change directory to `{self}`: {sys.errno.strerror}")
991 return error
992 end
993
994 # Return right-most extension (without the dot)
995 #
996 # Only the last extension is returned.
997 # There is no special case for combined extensions.
998 #
999 # assert "file.txt".file_extension == "txt"
1000 # assert "file.tar.gz".file_extension == "gz"
1001 #
1002 # For file without extension, `null` is returned.
1003 # Hoever, for trailing dot, `""` is returned.
1004 #
1005 # assert "file".file_extension == null
1006 # assert "file.".file_extension == ""
1007 #
1008 # The starting dot of hidden files is never considered.
1009 #
1010 # assert ".file.txt".file_extension == "txt"
1011 # assert ".file".file_extension == null
1012 fun file_extension: nullable String
1013 do
1014 var last_slash = chars.last_index_of('.')
1015 if last_slash > 0 then
1016 return substring( last_slash+1, length )
1017 else
1018 return null
1019 end
1020 end
1021
1022 # Returns entries contained within the directory represented by self.
1023 #
1024 # var files = "/etc".files
1025 # assert files.has("issue")
1026 #
1027 # Returns an empty array in case of error
1028 #
1029 # files = "/etc/issue".files
1030 # assert files.is_empty
1031 #
1032 # TODO find a better way to handle errors and to give them back to the user.
1033 fun files: Array[String]
1034 do
1035 var res = new Array[String]
1036 var d = new NativeDir.opendir(to_cstring)
1037 if d.address_is_null then return res
1038
1039 loop
1040 var de = d.readdir
1041 if de.address_is_null then break
1042 var name = de.to_s_with_copy
1043 if name == "." or name == ".." then continue
1044 res.add name
1045 end
1046 d.closedir
1047
1048 return res
1049 end
1050 end
1051
1052 redef class NativeString
1053 private fun file_exists: Bool is extern "string_NativeString_NativeString_file_exists_0"
1054 private fun file_stat: NativeFileStat is extern "string_NativeString_NativeString_file_stat_0"
1055 private fun file_lstat: NativeFileStat `{
1056 struct stat* stat_element;
1057 int res;
1058 stat_element = malloc(sizeof(struct stat));
1059 res = lstat(self, stat_element);
1060 if (res == -1) return NULL;
1061 return stat_element;
1062 `}
1063 private fun file_mkdir: Bool is extern "string_NativeString_NativeString_file_mkdir_0"
1064 private fun rmdir: Bool `{ return !rmdir(self); `}
1065 private fun file_delete: Bool is extern "string_NativeString_NativeString_file_delete_0"
1066 private fun file_chdir: Bool is extern "string_NativeString_NativeString_file_chdir_0"
1067 private fun file_realpath: NativeString is extern "file_NativeString_realpath"
1068 end
1069
1070 # This class is system dependent ... must reify the vfs
1071 private extern class NativeFileStat `{ struct stat * `}
1072 # Returns the permission bits of file
1073 fun mode: Int is extern "file_FileStat_FileStat_mode_0"
1074 # Returns the last access time
1075 fun atime: Int is extern "file_FileStat_FileStat_atime_0"
1076 # Returns the last status change time
1077 fun ctime: Int is extern "file_FileStat_FileStat_ctime_0"
1078 # Returns the last modification time
1079 fun mtime: Int is extern "file_FileStat_FileStat_mtime_0"
1080 # Returns the size
1081 fun size: Int is extern "file_FileStat_FileStat_size_0"
1082
1083 # Returns true if it is a regular file (not a device file, pipe, sockect, ...)
1084 fun is_reg: Bool `{ return S_ISREG(self->st_mode); `}
1085 # Returns true if it is a directory
1086 fun is_dir: Bool `{ return S_ISDIR(self->st_mode); `}
1087 # Returns true if it is a character device
1088 fun is_chr: Bool `{ return S_ISCHR(self->st_mode); `}
1089 # Returns true if it is a block device
1090 fun is_blk: Bool `{ return S_ISBLK(self->st_mode); `}
1091 # Returns true if the type is fifo
1092 fun is_fifo: Bool `{ return S_ISFIFO(self->st_mode); `}
1093 # Returns true if the type is a link
1094 fun is_lnk: Bool `{ return S_ISLNK(self->st_mode); `}
1095 # Returns true if the type is a socket
1096 fun is_sock: Bool `{ return S_ISSOCK(self->st_mode); `}
1097 end
1098
1099 # Instance of this class are standard FILE * pointers
1100 private extern class NativeFile `{ FILE* `}
1101 fun io_read(buf: NativeString, len: Int): Int is extern "file_NativeFile_NativeFile_io_read_2"
1102 fun io_write(buf: NativeString, len: Int): Int is extern "file_NativeFile_NativeFile_io_write_2"
1103 fun write_byte(value: Int): Int `{
1104 unsigned char b = (unsigned char)value;
1105 return fwrite(&b, 1, 1, self);
1106 `}
1107 fun io_close: Int is extern "file_NativeFile_NativeFile_io_close_0"
1108 fun file_stat: NativeFileStat is extern "file_NativeFile_NativeFile_file_stat_0"
1109 fun fileno: Int `{ return fileno(self); `}
1110 # Flushes the buffer, forcing the write operation
1111 fun flush: Int is extern "fflush"
1112 # Used to specify how the buffering will be handled for the current stream.
1113 fun set_buffering_type(buf_length: Int, mode: Int): Int is extern "file_NativeFile_NativeFile_set_buffering_type_0"
1114
1115 new io_open_read(path: NativeString) is extern "file_NativeFileCapable_NativeFileCapable_io_open_read_1"
1116 new io_open_write(path: NativeString) is extern "file_NativeFileCapable_NativeFileCapable_io_open_write_1"
1117 new native_stdin is extern "file_NativeFileCapable_NativeFileCapable_native_stdin_0"
1118 new native_stdout is extern "file_NativeFileCapable_NativeFileCapable_native_stdout_0"
1119 new native_stderr is extern "file_NativeFileCapable_NativeFileCapable_native_stderr_0"
1120 end
1121
1122 # Standard `DIR*` pointer
1123 private extern class NativeDir `{ DIR* `}
1124
1125 # Open a directory
1126 new opendir(path: NativeString) `{ return opendir(path); `}
1127
1128 # Close a directory
1129 fun closedir `{ closedir(self); `}
1130
1131 # Read the next directory entry
1132 fun readdir: NativeString `{
1133 struct dirent *de;
1134 de = readdir(self);
1135 if (!de) return NULL;
1136 return de->d_name;
1137 `}
1138 end
1139
1140 redef class Sys
1141
1142 # Standard input
1143 var stdin: PollableReader = new Stdin is protected writable, lazy
1144
1145 # Standard output
1146 var stdout: Writer = new Stdout is protected writable, lazy
1147
1148 # Standard output for errors
1149 var stderr: Writer = new Stderr is protected writable, lazy
1150
1151 # Enumeration for buffer mode full (flushes when buffer is full)
1152 fun buffer_mode_full: Int is extern "file_Sys_Sys_buffer_mode_full_0"
1153 # Enumeration for buffer mode line (flushes when a `\n` is encountered)
1154 fun buffer_mode_line: Int is extern "file_Sys_Sys_buffer_mode_line_0"
1155 # Enumeration for buffer mode none (flushes ASAP when something is written)
1156 fun buffer_mode_none: Int is extern "file_Sys_Sys_buffer_mode_none_0"
1157
1158 # returns first available stream to read or write to
1159 # return null on interruption (possibly a signal)
1160 protected fun poll( streams : Sequence[FileStream] ) : nullable FileStream
1161 do
1162 var in_fds = new Array[Int]
1163 var out_fds = new Array[Int]
1164 var fd_to_stream = new HashMap[Int,FileStream]
1165 for s in streams do
1166 var fd = s.fd
1167 if s isa FileReader then in_fds.add( fd )
1168 if s isa FileWriter then out_fds.add( fd )
1169
1170 fd_to_stream[fd] = s
1171 end
1172
1173 var polled_fd = intern_poll( in_fds, out_fds )
1174
1175 if polled_fd == null then
1176 return null
1177 else
1178 return fd_to_stream[polled_fd]
1179 end
1180 end
1181
1182 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) `{
1183 int in_len, out_len, total_len;
1184 struct pollfd *c_fds;
1185 sigset_t sigmask;
1186 int i;
1187 int first_polled_fd = -1;
1188 int result;
1189
1190 in_len = Array_of_Int_length( in_fds );
1191 out_len = Array_of_Int_length( out_fds );
1192 total_len = in_len + out_len;
1193 c_fds = malloc( sizeof(struct pollfd) * total_len );
1194
1195 /* input streams */
1196 for ( i=0; i<in_len; i ++ ) {
1197 int fd;
1198 fd = Array_of_Int__index( in_fds, i );
1199
1200 c_fds[i].fd = fd;
1201 c_fds[i].events = POLLIN;
1202 }
1203
1204 /* output streams */
1205 for ( i=0; i<out_len; i ++ ) {
1206 int fd;
1207 fd = Array_of_Int__index( out_fds, i );
1208
1209 c_fds[i].fd = fd;
1210 c_fds[i].events = POLLOUT;
1211 }
1212
1213 /* poll all fds, unlimited timeout */
1214 result = poll( c_fds, total_len, -1 );
1215
1216 if ( result > 0 ) {
1217 /* analyse results */
1218 for ( i=0; i<total_len; i++ )
1219 if ( c_fds[i].revents & c_fds[i].events || /* awaited event */
1220 c_fds[i].revents & POLLHUP ) /* closed */
1221 {
1222 first_polled_fd = c_fds[i].fd;
1223 break;
1224 }
1225
1226 return Int_as_nullable( first_polled_fd );
1227 }
1228 else if ( result < 0 )
1229 fprintf( stderr, "Error in Stream:poll: %s\n", strerror( errno ) );
1230
1231 return null_Int();
1232 `}
1233
1234 end
1235
1236 # Print `objects` on the standard output (`stdout`).
1237 fun printn(objects: Object...)
1238 do
1239 sys.stdout.write(objects.plain_to_s)
1240 end
1241
1242 # Print an `object` on the standard output (`stdout`) and add a newline.
1243 fun print(object: Object)
1244 do
1245 sys.stdout.write(object.to_s)
1246 sys.stdout.write("\n")
1247 end
1248
1249 # Print `object` on the error output (`stderr` or a log system)
1250 fun print_error(object: Object)
1251 do
1252 sys.stderr.write object.to_s
1253 sys.stderr.write "\n"
1254 end
1255
1256 # Read a character from the standard input (`stdin`).
1257 fun getc: Char
1258 do
1259 var c = sys.stdin.read_char
1260 if c == null then return '\1'
1261 return c
1262 end
1263
1264 # Read a line from the standard input (`stdin`).
1265 fun gets: String
1266 do
1267 return sys.stdin.read_line
1268 end
1269
1270 # Return the working (current) directory
1271 fun getcwd: String do return file_getcwd.to_s
1272 private fun file_getcwd: NativeString is extern "string_NativeString_NativeString_file_getcwd_0"