/src/lib/io/low_level/basic_directory.e
Specman e | 711 lines | 539 code | 45 blank | 127 comment | 38 complexity | 53777f334ba48cd9e4bde263b54d844b MD5 | raw file
1-- This file is part of a Liberty Eiffel library. 2-- See the full copyright at the end. 3-- 4expanded class BASIC_DIRECTORY 5 -- 6 -- Very low-level basic tools for file-system directory handling and file path manipulation. This class 7 -- intended to be platform independent as much as possible. In order to remove from the client side the 8 -- burden of file path computation, this class tries to compute automatically the system file notation 9 -- using argument(s) of some of the very first call(s). As soon as the system notation has been properly 10 -- detected, the result is internally memorized for all objects of type BASIC_DIRECTORY in a common private 11 -- buffer. Besides the low-level nature of operations one can found in this class, all file path 12 -- manipulations are done in a smart way (except when the system file path notation has not been detected 13 -- automatically, which is quite uncommon). As an example, even if the directory separator is internally 14 -- detected, this information is _intentionally_ kept private to avoid low-level manipulation from the 15 -- client side. Finally, this class is expanded in order to avoid as much as possible memory allocations. 16 -- 17 -- Also consider high level facade class DIRECTORY if you don't want to deal directly with low level 18 -- directory streams. 19 -- 20 21feature {} 22 directory_stream: POINTER 23 -- This pointer memorize the current directory stream being scanned (used to compute `is_connected'). 24 25 current_entry: POINTER 26 -- When `is_connected', memorize the current entry in the current `directory_stream'. 27 28feature {ANY} -- State of `Current' basic directory stream: 29 is_connected: BOOLEAN 30 -- Is `Current' connected to some directory stream ? 31 do 32 Result := directory_stream.is_not_null 33 end 34 35 end_of_input: BOOLEAN 36 -- Is end of input reached ? 37 require 38 is_connected 39 do 40 Result := current_entry.is_null 41 end 42 43feature {ANY} -- Connect and disconnect: 44 connect_to (directory_path: ABSTRACT_STRING) 45 -- Try to connect `Current' to some existing `directory_path'. After this call, the client 46 -- supposed to use `is_connected' to check that the stream is ready to be used. 47 require 48 not is_connected 49 not directory_path.is_empty 50 common_buffer_protection: last_entry /= directory_path 51 local 52 path_pointer: POINTER 53 do 54 path_pointer := directory_path.to_external 55 directory_stream := directory_open(path_pointer) 56 current_entry := directory_stream 57 last_entry.clear_count 58 ensure 59 is_connected implies not end_of_input 60 end 61 62 connect_with (some_path: ABSTRACT_STRING) 63 -- Try to connect `Current' to some directory using `some_path' which may be either an existing 64 -- directory path or some arbitrary file path name. When `some_path' is the path of some readable 65 -- existing directory, this directory is opened and the effect of `connect_with' is equivalent to 66 -- `connect_to'. When `some_path' is not an existing readable directory path, `connect_with' tries to 67 -- open the directory which may contains `some_path' viewed as a file path name. After this call, the 68 -- client is supposed to use `is_connected' to check that the stream is ready to be used and the 69 -- `last_entry' buffer to know about the corresponding opened directory path. Whatever the result, 70 -- `some_path' is left unchanged. 71 require 72 not is_connected 73 not some_path.is_empty 74 common_buffer_protection: last_entry /= some_path 75 local 76 p: POINTER 77 do 78 connect_to(some_path) 79 if is_connected then 80 last_entry.copy(some_path.out) 81 else 82 compute_parent_directory_of(some_path) 83 if last_entry.count > 0 then 84 p := last_entry.to_external 85 directory_stream := directory_open(p) 86 current_entry := directory_stream 87 if directory_stream.is_null then 88 last_entry.clear_count 89 end 90 else 91 last_entry.clear_count 92 end 93 end 94 ensure 95 is_connected implies not end_of_input 96 end 97 98 connect_to_current_working_directory 99 -- Try to connect `Current' to the current working directory. After this call, the client is supposed 100 -- to use `is_connected' to check that the stream is ready to be used and the `last_entry' buffer to 101 -- know about the name of the current working directory. 102 require 103 not is_connected 104 local 105 path: POINTER 106 do 107 path := directory_current_working_directory 108 if path.is_not_null then 109 last_entry.from_external_copy(path) 110 directory_stream := directory_open(path) 111 current_entry := directory_stream 112 if directory_stream.is_null then 113 last_entry.clear_count 114 else 115 if not system_notation_detected then 116 set_notation_using(last_entry) 117 end 118 system_notation.to_directory_path(last_entry) 119 end 120 else 121 last_entry.clear_count 122 end 123 ensure 124 is_connected implies not end_of_input 125 end 126 127 disconnect 128 -- Do not forget to call this feature when you have finished with some previously opened directory 129 -- stream. 130 require 131 is_connected 132 local 133 null: POINTER 134 do 135 if directory_close(directory_stream) then 136 directory_stream := null 137 current_entry := null 138 end 139 ensure 140 not is_connected 141 end 142 143feature {ANY} -- Scanning: 144 last_entry: STRING 145 -- Unique global buffer (once object) to get the last information computed by many routines of this 146 -- class: `read_entry', `connect_with' `connect_to_current_working_directory', 147 -- `compute_parent_directory_of', ... 148 once 149 create Result.make(256) 150 end 151 152 read_entry 153 -- Read the next entry name and update `last_entry' and `end_of_input' accordingly. 154 require 155 is_connected 156 not end_of_input 157 local 158 name: POINTER 159 do 160 current_entry := directory_read_entry(directory_stream) 161 if current_entry.is_not_null then 162 name := directory_get_entry_name(current_entry) 163 last_entry.from_external_copy(name) 164 end 165 end 166 167feature {ANY} -- File path handling tools: 168 compute_parent_directory_of (some_path: ABSTRACT_STRING) 169 -- Using `some_path' (which may be either a file path or a directory path) tries to compute in the 170 -- `last_entry' buffer the parent directory of `some_path'. When `some_path' is a path with no parent 171 -- directory, the `last_entry' buffer `is_empty' after this call. This operation does not perform any 172 -- disk access. 173 require 174 not some_path.is_empty 175 common_buffer_protection: last_entry /= some_path 176 do 177 if system_notation /= Void then 178 last_entry.copy(some_path.out) 179 system_notation.to_parent_directory(last_entry) 180 else 181 set_notation_using(some_path) 182 if system_notation /= Void then 183 last_entry.copy(some_path.out) 184 system_notation.to_parent_directory(last_entry) 185 else 186 last_entry.clear_count 187 end 188 end 189 end 190 191 compute_subdirectory_with (parent_path, entry_name: ABSTRACT_STRING) 192 -- Try to compute in the `last_entry' buffer the new subdirectory path obtained when trying to 193 -- concatenate smartly `parent_path' with some `entry_name'. When this fails the `last_entry' buffer 194 -- `is_empty' after this call. This operation does not perform any disk access. Whatever the result, 195 -- `parent_path' and `entry_name' are left unchanged. 196 require 197 not parent_path.is_empty 198 not entry_name.is_empty 199 common_buffer_protection1: last_entry /= parent_path 200 common_buffer_protection2: last_entry /= entry_name 201 do 202 if system_notation /= Void then 203 last_entry.copy(parent_path.out) 204 system_notation.to_subdirectory_with(last_entry, entry_name.out) 205 else 206 set_notation_using(parent_path.out) 207 if system_notation /= Void then 208 last_entry.copy(parent_path.out) 209 system_notation.to_subdirectory_with(last_entry, entry_name.out) 210 else 211 last_entry.clear_count 212 end 213 end 214 end 215 216 compute_file_path_with (parent_path, file_name: ABSTRACT_STRING) 217 -- Try to compute in the `last_entry' buffer the new file path obtained when trying to concatenate 218 -- smartly `parent_path' with some `file_name'. When this fails the `last_entry' buffer `is_empty' 219 -- after this call. This operation does not perform any disk access. Whatever the result, 220 -- `parent_path' and `file_name' are left unchanged. 221 require 222 not parent_path.is_empty 223 not file_name.is_empty 224 common_buffer_protection1: last_entry /= parent_path 225 common_buffer_protection2: last_entry /= file_name 226 do 227 if system_notation /= Void then 228 last_entry.copy(parent_path.out) 229 system_notation.to_file_path_with(last_entry, file_name.out) 230 else 231 set_notation_using(parent_path) 232 if system_notation /= Void then 233 last_entry.copy(parent_path.out) 234 system_notation.to_file_path_with(last_entry, file_name.out) 235 else 236 last_entry.clear_count 237 end 238 end 239 end 240 241 compute_absolute_file_path_with (path: ABSTRACT_STRING) 242 -- Try to compute an absolute path equivalent to `path' and store it in `last_entry'. When this fails 243 -- the `last_entry' buffer `is_empty' after this call. This operation does not perform any disk 244 -- access. Whatever the result, `path' is left unchanged. 245 require 246 valid_path(path) 247 common_buffer_protection: last_entry /= path 248 do 249 if system_notation /= Void then 250 last_entry.copy(current_working_directory.out) 251 system_notation.to_absolute_path_in(last_entry, path.out) 252 else 253 set_notation_using(path) 254 if system_notation /= Void then 255 last_entry.copy(current_working_directory.out) 256 system_notation.to_absolute_path_in(last_entry, path.out) 257 else 258 last_entry.clear_count 259 end 260 end 261 ensure 262 last_entry.is_empty or else system_notation.is_absolute_path(last_entry.out) 263 end 264 265 compute_short_name_of (path: ABSTRACT_STRING) 266 -- Try to find the short name of the file or directory given by its `path' and store it in 267 -- `last_entry'. When this fails the `last_entry' buffer `is_empty' after the call. This operation 268 -- does not perform any disk access. Whatever the result, `path' is left unchanged. 269 do 270 if system_notation /= Void then 271 last_entry.copy(current_working_directory.out) 272 system_notation.to_short_name_in(last_entry, path.out) 273 else 274 set_notation_using(path) 275 if system_notation /= Void then 276 last_entry.copy(current_working_directory.out) 277 system_notation.to_short_name_in(last_entry, path.out) 278 else 279 last_entry.clear_count 280 end 281 end 282 end 283 284 valid_path (path: ABSTRACT_STRING): BOOLEAN 285 -- Is the syntax of `path' valid for the system notation? 286 do 287 if system_notation_detected then 288 Result := system_notation.is_valid_path(path.out) 289 else 290 Result := not path.is_empty 291 end 292 ensure 293 system_notation_detected implies Result = system_notation.is_valid_path(path.out) 294 not system_notation_detected implies Result = not path.is_empty 295 end 296 297 change_current_working_directory (directory_path: ABSTRACT_STRING) 298 -- Try to change the current working directory using some `directory_path'. When the operation 299 -- possible, the `last_entry' buffer is updated with the new current working directory path, 300 -- otherwise, when the modification is not possible the `last_entry' buffer `is_empty' after this 301 -- call. Whatever the result, `directory_path' is left unchanged. 302 require 303 not is_connected 304 common_buffer_protection1: last_entry /= directory_path 305 local 306 p: POINTER 307 do 308 p := directory_path.to_external 309 if directory_chdir(p) then 310 connect_to_current_working_directory 311 if is_connected then 312 disconnect 313 check 314 not last_entry.is_empty 315 end 316 else 317 last_entry.clear_count 318 end 319 else 320 last_entry.clear_count 321 end 322 ensure 323 not is_connected 324 end 325 326 current_working_directory: FIXED_STRING 327 -- The current working directory. Always returns the same once STRING. 328 local 329 path: POINTER; cwd: STRING 330 do 331 path := directory_current_working_directory 332 if path.is_not_null then 333 cwd := once "" 334 cwd.from_external_copy(path) 335 if not system_notation_detected then 336 set_notation_using(cwd) 337 end 338 system_notation.to_directory_path(cwd) 339 Result := cwd.intern 340 end 341 end 342 343 ensure_system_notation 344 local 345 dummy: BOOLEAN 346 once 347 dummy := require_system_notation 348 ensure 349 system_notation /= Void 350 end 351 352 require_system_notation: BOOLEAN 353 -- Same as `ensure_system_notation', useful for contracts 354 once 355 Result := current_working_directory /= Void 356 ensure 357 system_notation /= Void 358 Result 359 end 360 361feature {ANY} -- Disk modification: 362 create_new_directory (directory_path: ABSTRACT_STRING): BOOLEAN 363 -- Try to create a new directory using the `directory_path' name. 364 -- Returns True on success. 365 require 366 not is_connected 367 local 368 p: POINTER 369 do 370 p := directory_path.to_external 371 Result := directory_mkdir(p) 372 ensure 373 not is_connected 374 end 375 376 remove_directory (directory_path: ABSTRACT_STRING): BOOLEAN 377 -- Try to remove directory `directory_path' which must be empty. 378 -- Returns True on success. 379 require 380 not is_connected 381 local 382 p: POINTER 383 do 384 p := directory_path.to_external 385 Result := directory_rmdir(p) 386 ensure 387 not is_connected 388 end 389 390 remove_files_of (directory_path: ABSTRACT_STRING) 391 -- Try to remove all files (not subdirectories) of directory specified by `directory_path'. 392 require 393 not is_connected 394 local 395 ft: FILE_TOOLS 396 do 397 connect_to(directory_path) 398 if is_connected then 399 from 400 read_entry 401 until 402 end_of_input 403 loop 404 tmp_path.copy(last_entry) 405 compute_file_path_with(directory_path, tmp_path) 406 tmp_path.copy(last_entry) 407 if ft.is_file(tmp_path) then 408 ft.delete(tmp_path) 409 end 410 read_entry 411 end 412 disconnect 413 end 414 ensure 415 not is_connected 416 end 417 418 remove_recursively (directory_path: ABSTRACT_STRING): BOOLEAN 419 -- Try to remove all files and all subdirectories of directory specified by `directory_path'. 420 require 421 not is_connected 422 local 423 ft: FILE_TOOLS 424 do 425 connect_to(directory_path) 426 Result := is_connected 427 if Result then 428 from 429 read_entry 430 until 431 end_of_input 432 loop 433 if not system_notation.is_current_directory(last_entry) and then not system_notation.is_parent_directory(last_entry) then 434 tmp_path.copy(last_entry) 435 compute_file_path_with(directory_path, tmp_path) 436 tmp_path.copy(last_entry) 437 if ft.is_directory(tmp_path) then 438 disconnect 439 Result := Result and remove_recursively(tmp_path.twin) 440 connect_to(directory_path) 441 else 442 ft.delete(tmp_path) 443 end 444 end 445 read_entry 446 end 447 disconnect 448 end 449 Result := Result and remove_directory(directory_path) 450 ensure 451 not is_connected 452 end 453 454feature {ANY} -- Miscellaneous: 455 is_case_sensitive: BOOLEAN 456 local 457 bd: like Current 458 do 459 if system_notation = Void then 460 -- Try to create system_notation 461 bd.connect_to_current_working_directory 462 if bd.is_connected then 463 if not last_entry.is_empty then 464 set_notation_using(last_entry) 465 if system_notation /= Void then 466 Result := system_notation.is_case_sensitive 467 end 468 end 469 bd.disconnect 470 end 471 else 472 Result := system_notation.is_case_sensitive 473 end 474 end 475 476 system_notation: DIRECTORY_NOTATION 477 do 478 Result := system_notation_buffer.item 479 end 480 481feature {DIRECTORY_NOTATION_HANDLER} 482 system_notation_buffer: REFERENCE[DIRECTORY_NOTATION] 483 -- Unique common buffer to memorize the system path notation. 484 once 485 create Result 486 end 487 488 system_notation_detected: BOOLEAN 489 do 490 Result := system_notation /= Void 491 end 492 493 unix_notation: BOOLEAN 494 -- The Unix like file path notation looks like: 495 -- /LibertyEiffel/sys/system.se 496 do 497 Result := {UNIX_DIRECTORY_NOTATION} ?:= system_notation 498 end 499 500 windows_notation: BOOLEAN 501 -- The Windows like file path notation looks like: 502 -- C:\LibertyEiffel\sys\system.se 503 do 504 Result := {WINDOWS_DIRECTORY_NOTATION} ?:= system_notation 505 end 506 507 cygwin_notation: BOOLEAN 508 -- The Cygwin like file path notation looks like: 509 -- //C/LibertyEiffel/sys/system.se 510 do 511 Result := {CYGWIN_DIRECTORY_NOTATION} ?:= system_notation 512 end 513 514 openvms_notation: BOOLEAN 515 -- The VMS file path notation looks like: 516 -- DISK:[LibertyEiffel.sys]system.se 517 -- The current working directory notation is: 518 -- DISK:[] 519 -- The equivalent of Unix .. is : 520 -- [-] 521 -- The equivalent of Unix ../.. is : 522 -- [-.-] 523 -- 524 do 525 Result := {OPENVMS_DIRECTORY_NOTATION} ?:= system_notation 526 end 527 528 set_notation_using (some_path: ABSTRACT_STRING) 529 -- Try to detect automatically the file system notation. 530 require 531 not some_path.is_empty 532 not system_notation_detected 533 do 534 inspect 535 some_path.first 536 when '/', '.', '~' then 537 if some_path.count >= 4 and then some_path.item(2) = '/' and then some_path.item(4) = '/' then 538 system_notation_buffer.set_item(create {CYGWIN_DIRECTORY_NOTATION}) 539 else 540 system_notation_buffer.set_item(create {UNIX_DIRECTORY_NOTATION}) 541 end 542 when '\' then 543 system_notation_buffer.set_item(create {WINDOWS_DIRECTORY_NOTATION}) 544 when '[' then 545 system_notation_buffer.set_item(create {OPENVMS_DIRECTORY_NOTATION}) 546 when 'a' .. 'z', 'A' .. 'Z' then 547 if some_path.count >= 2 then 548 inspect 549 some_path.item(2) 550 when ':' then 551 if some_path.count = 2 then 552 system_notation_buffer.set_item(create {WINDOWS_DIRECTORY_NOTATION}) 553 elseif some_path.has('\') then 554 system_notation_buffer.set_item(create {WINDOWS_DIRECTORY_NOTATION}) 555 elseif some_path.has('/') then 556 system_notation_buffer.set_item(create {CYGWIN_DIRECTORY_NOTATION}) 557 end 558 when 'a' .. 'z', 'A' .. 'Z' then 559 if some_path.has('[') then 560 system_notation_buffer.set_item(create {OPENVMS_DIRECTORY_NOTATION}) 561 elseif some_path.has(':') then 562 if some_path.has('[') then 563 system_notation_buffer.set_item(create {OPENVMS_DIRECTORY_NOTATION}) 564 end 565 elseif some_path.has('/') then 566 system_notation_buffer.set_item(create {UNIX_DIRECTORY_NOTATION}) 567 elseif some_path.has('\') then 568 --|*** This looks weird <FM-23/03/2005> 569 system_notation_buffer.set_item(create {UNIX_DIRECTORY_NOTATION}) 570 end 571 else 572 end 573 end 574 else 575 end 576 end 577 578 reset_notation_using (some_path: ABSTRACT_STRING) 579 -- Try to detect automatically the file system notation. 580 do 581 system_notation_buffer.set_item(Void) 582 set_notation_using(some_path) 583 end 584 585 reset_notation 586 do 587 reset_notation_using(current_working_directory) 588 end 589 590feature {} 591 tmp_path: STRING 592 once 593 create Result.make(256) 594 end 595 596 directory_open (path_pointer: POINTER): POINTER 597 -- Try to open some existing directory using `path'. When `Result' `is_not_null', the directory 598 -- correctly opened and `Result' is a valid handle for this directory. Using `Result', one can then 599 -- scan the content of the directory using function `basic_directory_read_entry' and 600 -- `basic_directory_get_entry_name'. Finally, a `is_not_null' directory must be closed using function 601 -- `basic_directory_close'. 602 require 603 path_pointer.is_not_null 604 external "plug_in" 605 alias "{ 606 location: "${sys}/plugins/io" 607 module_name: "directory" 608 feature_name: "directory_open" 609 }" 610 end 611 612 directory_read_entry (dirstream: POINTER): POINTER 613 -- Read an return a new entry using the directory handle `dirstream' obtained with function 614 -- `basic_directory_open'. When there is no more entry, the `Result' becomes `is_null'. 615 require 616 dirstream.is_not_null 617 external "plug_in" 618 alias "{ 619 location: "${sys}/plugins/io" 620 module_name: "directory" 621 feature_name: "directory_read_entry" 622 }" 623 end 624 625 directory_get_entry_name (entry: POINTER): POINTER 626 -- Read an return a new entry using the directory handle `dirstream' obtained with function 627 -- `basic_directory_open'. When there is no more entry, the `Result' becomes `is_null'. 628 require 629 entry.is_not_null 630 external "plug_in" 631 alias "{ 632 location: "${sys}/plugins/io" 633 module_name: "directory" 634 feature_name: "directory_get_entry_name" 635 }" 636 end 637 638 directory_close (dirstream: POINTER): BOOLEAN 639 -- Try to close some opened `dirstream' directory. A True result indicates that the directory 640 -- correctly closed. 641 require 642 dirstream.is_not_null 643 external "plug_in" 644 alias "{ 645 location: "${sys}/plugins/io" 646 module_name: "directory" 647 feature_name: "directory_close" 648 }" 649 end 650 651 directory_current_working_directory: POINTER 652 -- Try to get the current working directory path. 653 external "plug_in" 654 alias "{ 655 location: "${sys}/plugins/io" 656 module_name: "directory" 657 feature_name: "directory_current_working_directory" 658 }" 659 end 660 661 directory_chdir (destination: POINTER): BOOLEAN 662 -- Try to change the current working directory using `destination'. 663 external "plug_in" 664 alias "{ 665 location: "${sys}/plugins/io" 666 module_name: "directory" 667 feature_name: "directory_chdir" 668 }" 669 end 670 671 directory_mkdir (directory_path: POINTER): BOOLEAN 672 -- Try to create a new directory using `directory_path'. 673 external "plug_in" 674 alias "{ 675 location: "${sys}/plugins/io" 676 module_name: "directory" 677 feature_name: "directory_mkdir" 678 }" 679 end 680 681 directory_rmdir (directory_path: POINTER): BOOLEAN 682 -- Try to remove `directory_path'. 683 external "plug_in" 684 alias "{ 685 location: "${sys}/plugins/io" 686 module_name: "directory" 687 feature_name: "directory_rmdir" 688 }" 689 end 690 691end -- class BASIC_DIRECTORY 692-- 693-- Copyright (C) 2009-2017: by all the people cited in the AUTHORS file. 694-- 695-- Permission is hereby granted, free of charge, to any person obtaining a copy 696-- of this software and associated documentation files (the "Software"), to deal 697-- in the Software without restriction, including without limitation the rights 698-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 699-- copies of the Software, and to permit persons to whom the Software is 700-- furnished to do so, subject to the following conditions: 701-- 702-- The above copyright notice and this permission notice shall be included in 703-- all copies or substantial portions of the Software. 704-- 705-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 706-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 707-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 708-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 709-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 710-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 711-- THE SOFTWARE.