diff --git a/doc/specs/stdlib_system.md b/doc/specs/stdlib_system.md index c6c79fcea..95a7f8e41 100644 --- a/doc/specs/stdlib_system.md +++ b/doc/specs/stdlib_system.md @@ -646,6 +646,80 @@ Subroutine --- +## `get_cwd` - Gets the current working directory + +### Status + +Experimental + +### Description + +This subroutine retrieves the current working directory the running process is executing from. +It is designed to work across multiple platforms. On Windows, paths with both forward `/` and backward `\` slashes are accepted. + +### Syntax + +`call [[stdlib_system(module):get_cwd(subroutine)]] (cwd [, err])` + +### Class + +Subroutine + +### Arguments + +`cwd`: Shall be a character string for receiving the path of the current working directory (cwd). It is an `intent(out)` argument. + +`err`(optional): Shall be of type `state_type`, and is used for error handling. It is an `intent(out)` argument. + +### Return values + +`err` is an optional state return flag. On error if not requested, an `FS_ERROR` will trigger an error stop. + +### Example + +```fortran +{!example/system/example_cwd.f90!} +``` + +--- + +## `set_cwd` - Sets the current working directory + +### Status + +Experimental + +### Description + +This subrotine sets the current working directory the process is executing from. +It is designed to work across multiple platforms. On Windows, paths with both forward `/` and backward `\` slashes are accepted. + +### Syntax + +`call [[stdlib_system(module):set_cwd(subroutine)]] (path [, err])` + +### Class + +Subroutine + +### Arguments + +`path`: Shall be a character string containing the path of the directory. It is an `intent(in)` argument. + +`err`(optional): Shall be of type `state_type`, and is used for error handling. It is an `intent(out)` argument. + +### Return values + +`err` is an optional state return flag. On error if not requested, an `FS_ERROR` will trigger an error stop. + +### Example + +```fortran +{!example/system/example_cwd.f90!} +``` + +--- + ## `null_device` - Return the null device file path ### Status @@ -682,6 +756,8 @@ None. {!example/system/example_null_device.f90!} ``` +--- + ## `delete_file` - Delete a file ### Status @@ -723,6 +799,8 @@ The file is removed from the filesystem if the operation is successful. If the o {!example/system/example_delete_file.f90!} ``` +--- + ## `join_path` - Joins the provided paths according to the OS ### Status @@ -785,6 +863,8 @@ The result is an `allocatable` character string or `type(string_type)` {!example/system/example_path_join.f90!} ``` +--- + ## `split_path` - splits a path immediately following the last separator ### Status @@ -825,6 +905,8 @@ The splitted path. `head` and `tail`. {!example/system/example_path_split_path.f90!} ``` +--- + ## `base_name` - The last part of a path ### Status @@ -860,6 +942,8 @@ A character string or `type(string_type)`. {!example/system/example_path_base_name.f90!} ``` +--- + ## `dir_name` - Everything except the last part of the path ### Status diff --git a/example/system/CMakeLists.txt b/example/system/CMakeLists.txt index 142dad22a..3ede9e3cf 100644 --- a/example/system/CMakeLists.txt +++ b/example/system/CMakeLists.txt @@ -18,3 +18,4 @@ ADD_EXAMPLE(path_base_name) ADD_EXAMPLE(path_dir_name) ADD_EXAMPLE(make_directory) ADD_EXAMPLE(remove_directory) +ADD_EXAMPLE(cwd) diff --git a/example/system/example_cwd.f90 b/example/system/example_cwd.f90 new file mode 100644 index 000000000..b2f2817c0 --- /dev/null +++ b/example/system/example_cwd.f90 @@ -0,0 +1,32 @@ +! Illustrate the usage of `get_cwd`, `set_cwd` +program example_cwd + use stdlib_system, only: get_cwd, set_cwd + use stdlib_error, only: state_type + implicit none + + character(len=:), allocatable :: path + type(state_type) :: err + + call get_cwd(path, err) + + if (err%error()) then + print *, "Error getting current working directory: "//err%print() + end if + + print *, "CWD: "//path + + call set_cwd("./src", err) + + if (err%error()) then + print *, "Error setting current working directory: "//err%print() + end if + + call get_cwd(path, err) + + if (err%error()) then + print *, "Error getting current working directory after using set_cwd: "//err%print() + end if + + print *, "CWD: "//path +end program example_cwd + diff --git a/src/stdlib_system.F90 b/src/stdlib_system.F90 index bd6f9b001..90e61e05b 100644 --- a/src/stdlib_system.F90 +++ b/src/stdlib_system.F90 @@ -2,7 +2,7 @@ module stdlib_system use, intrinsic :: iso_c_binding, only : c_int, c_long, c_ptr, c_null_ptr, c_int64_t, c_size_t, & c_f_pointer use stdlib_kinds, only: int64, dp, c_bool, c_char -use stdlib_strings, only: to_c_char, find +use stdlib_strings, only: to_c_char, find, to_string use stdlib_string_type, only: string_type use stdlib_optval, only: optval use stdlib_error, only: state_type, STDLIB_SUCCESS, STDLIB_FS_ERROR @@ -156,6 +156,32 @@ module stdlib_system !! public :: remove_directory +!! version: experimental +!! +!! Gets the current working directory of the process +!! ([Specification](../page/specs/stdlib_system.html#get_cwd)) +!! +!! ### Summary +!! Gets the current working directory. +!! +!! ### Description +!! This subroutine gets the current working directory the process is executing from. +!! +public :: get_cwd + +!! version: experimental +!! +!! Sets the current working directory of the process +!! ([Specification](../page/specs/stdlib_system.html#set_cwd)) +!! +!! ### Summary +!! Changes the current working directory to the one specified. +!! +!! ### Description +!! This subroutine sets the current working directory the process is executing from. +!! +public :: set_cwd + !! version: experimental !! !! Deletes a specified file from the filesystem. @@ -896,6 +922,25 @@ end function stdlib_is_directory end function is_directory +! A Helper function to convert C character arrays to Fortran character strings +function to_f_char(c_str_ptr, len) result(f_str) + type(c_ptr), intent(in) :: c_str_ptr + ! length of the string excluding the null character + integer(kind=c_size_t), intent(in) :: len + character(:), allocatable :: f_str + + integer :: i + character(kind=c_char), pointer :: c_str(:) + + call c_f_pointer(c_str_ptr, c_str, [len]) + + allocate(character(len=len) :: f_str) + + do concurrent (i=1:len) + f_str(i:i) = c_str(i) + end do +end function to_f_char + ! A helper function to get the result of the C function `strerror`. ! `strerror` is a function provided by ``. ! It returns a string describing the meaning of `errno` in the C header `` @@ -911,18 +956,11 @@ end function strerror end interface type(c_ptr) :: c_str_ptr - integer(c_size_t) :: len, i - character(kind=c_char), pointer :: c_str(:) + integer(c_size_t) :: len c_str_ptr = strerror(len) - call c_f_pointer(c_str_ptr, c_str, [len]) - - allocate(character(len=len) :: str) - - do concurrent (i=1:len) - str(i:i) = c_str(i) - end do + str = to_f_char(c_str_ptr, len) end function c_get_strerror !! makes an empty directory @@ -1024,6 +1062,56 @@ end function stdlib_remove_directory end subroutine remove_directory +subroutine get_cwd(cwd, err) + character(:), allocatable, intent(out) :: cwd + type(state_type), optional, intent(out) :: err + type(state_type) :: err0 + + interface + type(c_ptr) function stdlib_get_cwd(len, stat) bind(C, name='stdlib_get_cwd') + import c_ptr, c_size_t + integer(c_size_t), intent(out) :: len + integer :: stat + end function stdlib_get_cwd + end interface + + type(c_ptr) :: c_str_ptr + integer(c_size_t) :: len + integer :: stat + + c_str_ptr = stdlib_get_cwd(len, stat) + + if (stat /= 0) then + err0 = FS_ERROR_CODE(stat, c_get_strerror()) + call err0%handle(err) + end if + + cwd = to_f_char(c_str_ptr, len) + +end subroutine get_cwd + +subroutine set_cwd(path, err) + character(len=*), intent(in) :: path + type(state_type), optional, intent(out) :: err + type(state_type) :: err0 + + interface + integer function stdlib_set_cwd(path) bind(C, name='stdlib_set_cwd') + import c_char + character(kind=c_char), intent(in) :: path(*) + end function stdlib_set_cwd + end interface + + integer :: code + + code = stdlib_set_cwd(to_c_char(trim(path))) + + if (code /= 0) then + err0 = FS_ERROR_CODE(code, c_get_strerror()) + call err0%handle(err) + end if +end subroutine set_cwd + !> Returns the file path of the null device for the current operating system. !> !> Version: Helper function. @@ -1042,21 +1130,13 @@ end function process_null_device end interface - integer(c_size_t) :: i, len + integer(c_size_t) :: len type(c_ptr) :: c_path_ptr - character(kind=c_char), pointer :: c_path(:) ! Call the C function to get the null device path and its length c_path_ptr = process_null_device(len) - call c_f_pointer(c_path_ptr,c_path,[len]) - ! Allocate the Fortran string with the length returned from C - allocate(character(len=len) :: path) - - do concurrent (i=1:len) - path(i:i) = c_path(i) - end do - + path = to_f_char(c_path_ptr, len) end function null_device !> Delete a file at the given path. diff --git a/src/stdlib_system.c b/src/stdlib_system.c index 0bef82b8c..81ff06a06 100644 --- a/src/stdlib_system.c +++ b/src/stdlib_system.c @@ -1,4 +1,6 @@ +#include #include +#include #include #include #include @@ -44,3 +46,48 @@ int stdlib_remove_directory(const char* path){ return (!code) ? 0 : errno; } + +// Wrapper to the platform's `getcwd`(get current working directory) call. +// Uses `getcwd` on unix, `_getcwd` on windows. +// Returns the cwd, sets the length of cwd and the `stat` of the operation. +char* stdlib_get_cwd(size_t* len, int* stat){ + *stat = 0; +#ifdef _WIN32 + char* buffer; + buffer = _getcwd(NULL, 0); + + if (buffer == NULL) { + *stat = errno; + return NULL; + } + + *len = strlen(buffer); + return buffer; +#else + char buffer[PATH_MAX + 1]; + if (!getcwd(buffer, sizeof(buffer))) { + *stat = errno; + } + + *len = strlen(buffer); + + char* res = malloc(*len); + strncpy(res, buffer, *len); + + return res; +#endif /* ifdef _WIN32 */ +} + +// Wrapper to the platform's `chdir`(change directory) call. +// Uses `chdir` on unix, `_chdir` on windows. +// Returns 0 if successful, otherwise returns the `errno`. +int stdlib_set_cwd(char* path) { + int code; +#ifdef _WIN32 + code = _chdir(path); +#else + code = chdir(path); +#endif /* ifdef _WIN32 */ + + return (code == -1) ? errno : 0; +} diff --git a/test/system/test_filesystem.f90 b/test/system/test_filesystem.f90 index af4bbedb6..1d06297f5 100644 --- a/test/system/test_filesystem.f90 +++ b/test/system/test_filesystem.f90 @@ -2,7 +2,7 @@ module test_filesystem use testdrive, only : new_unittest, unittest_type, error_type, check, skip_test use stdlib_system, only: is_directory, delete_file, FS_ERROR, FS_ERROR_CODE, & make_directory, remove_directory, make_directory_all, is_windows, OS_TYPE, & - OS_WINDOWS + OS_WINDOWS, get_cwd, set_cwd, operator(/) use stdlib_error, only: state_type, STDLIB_FS_ERROR implicit none @@ -25,7 +25,8 @@ subroutine collect_suite(testsuite) new_unittest("fs_make_dir_existing_dir", test_make_directory_existing), & new_unittest("fs_make_dir_all", test_make_directory_all), & new_unittest("fs_remove_dir", test_remove_directory), & - new_unittest("fs_remove_dir_non_existent", test_remove_directory_nonexistent) & + new_unittest("fs_remove_dir_non_existent", test_remove_directory_nonexistent), & + new_unittest("fs_cwd", test_cwd) & ] end subroutine collect_suite @@ -279,6 +280,56 @@ subroutine test_remove_directory_nonexistent(error) if (allocated(error)) return end subroutine test_remove_directory_nonexistent + subroutine test_cwd(error) + type(error_type), allocatable, intent(out) :: error + type(state_type) :: err + character(len=256) :: dir_name + integer :: ios,iocmd + character(len=512) :: msg + + character(:), allocatable :: pwd1, pwd2, abs_dir_name + + ! get the initial cwd + call get_cwd(pwd1, err) + call check(error, err%ok(), 'Could not get current working directory: '//err%print()) + if (allocated(error)) return + + ! create a temporary directory for use by `set_cwd` + dir_name = "test_directory" + + call execute_command_line('mkdir ' // dir_name, exitstat=ios, cmdstat=iocmd, cmdmsg=msg) + call check(error, ios==0 .and. iocmd==0, 'Cannot init cwd test: '//trim(msg)) + if (allocated(error)) return + + abs_dir_name = pwd1 / dir_name + call set_cwd(abs_dir_name, err) + call check(error, err%ok(), 'Could not set current working directory: '//err%print()) + if (allocated(error)) return + + ! get the new cwd -> should be same as (pwd1 / dir_name) + call get_cwd(pwd2, err) + call check(error, err%ok(), 'Could not get current working directory: '//err%print()) + if (allocated(error)) return + + call check(error, pwd2 == abs_dir_name, 'Working directory is wrong, & + & expected: '//abs_dir_name//" got: "//pwd2) + if (allocated(error)) return + + ! cleanup: set the cwd back to the initial value + call set_cwd(pwd1, err) + call check(error, err%ok(), 'Could not clean up cwd test, could not set the cwd back: '//err%print()) + if (allocated(error)) then + ! our cwd now is `./test_directory` + ! there is no way of removing the empty test directory + return + end if + + ! cleanup: remove the empty directory + call execute_command_line('rmdir ' // dir_name, exitstat=ios, cmdstat=iocmd, cmdmsg=msg) + call check(error, ios==0 .and. iocmd==0, 'Cannot cleanup cwd test, cannot remove empty dir: '//trim(msg)) + if (allocated(error)) return + end subroutine test_cwd + end module test_filesystem program tester