diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ac6b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..09963b4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Minh Dao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3c3c6a --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# run-f + +This simple Fortran library allows you to execute a command in the command line and save its result as a string without the need for a temporary file. + +It was inspired by the work of [Jacob Williams](https://degenerateconic.com/fortran-c-interoperability.html) and uses `iso_c_binding` to call `popen`, `fgets` and `pclose` from the C standard library. + +## Usage + +First, import the `run_f` module into your Fortran code: + +```fortran +use run_f, only: run +``` + +Then you can use the `run` function to execute a command and save its result as a string: + +```fortran +character(len=:), allocatable :: output + +output = run("whoami") +``` + +### Error Handling + +### Print Command + +### Ignore Errors + +You can also use the `optional` arguments to add error handling or print the command to the console before executing it: + +```fortran +character(:), allocatable :: output +character(*), parameter :: command = "whoami" +logical :: has_error + +output = run(command, has_error, print_cmd=.true.) + +if (has_error) then + print *, "An error occurred while executing the command: ", command + stop 1 +end if +``` + +## Install + +### fpm + +Using [fpm](https://fpm.fortran-lang.org/en/index.html), you can simply add this package as a dependency to your `fpm.toml` file: + +```toml +[dependencies] + +[dependencies.run-f] +git = "https://github.com/minhqdao/run-f.git" +tag = "v0.1.0" +``` + +Then import the `run_f` module into your Fortran code: + +```fortran +use run_f, only: run +``` + +Run `fpm build` to download the dependency. + +## Tests + +Run tests with: + +```bash +fpm test +``` + +## Formatting + +The CI will fail if the code is not formatted correctly. Please configure your editor to use [fprettify](https://pypi.org/project/fprettify/) and use an indentation width of 2 or run `fprettify -i 2 -r .` before committing. + +## Contribute + +Feel free to [create an issue](https://github.com/minhqdao/run-f/issues) in case you found a bug, have any questions or want to propose further improvements. Please stick to the existing coding style when opening a pull request. + +## License + +You can use, redistribute and/or modify the code under the terms of the [MIT License](https://github.com/minhqdao/run-f/blob/main/LICENSE). diff --git a/ci.yml b/ci.yml new file mode 100644 index 0000000..7cfe914 --- /dev/null +++ b/ci.yml @@ -0,0 +1,75 @@ +name: CI +on: push + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + toolchain: + - { compiler: gcc, version: 13 } + - { compiler: intel, version: 2024.1 } + - { compiler: intel-classic, version: 2021.10 } + - { compiler: lfortran, version: 0.33.0 } + exclude: + - os: macos-latest + toolchain: { compiler: intel, version: 2024.1 } + include: + - os: ubuntu-latest + toolchain: { compiler: nvidia-hpc, version: 23.11 } + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: fortran-lang/setup-fortran@v1 + id: setup-fortran + with: + compiler: ${{ matrix.toolchain.compiler }} + version: ${{ matrix.toolchain.version }} + - uses: fortran-lang/setup-fpm@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - run: fpm test + + msys2: + runs-on: windows-latest + strategy: + matrix: + msystem: [msys, ucrt64, mingw64, clang64] + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + - uses: fortran-lang/setup-fortran@v1 + with: + compiler: gcc + version: 13 + - uses: fortran-lang/setup-fpm@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - run: fpm test + + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Check formatting + run: | + pip install fprettify + + diff=$(fprettify -i 2 -r . -d) + + if [[ $diff ]]; then + red="\033[0;31m" + cyan="\033[0;36m" + reset="\033[0m" + printf -- "$diff\n" + printf "${red}The code is not correctly formatted. Please run: ${reset}\n" + printf "${cyan}fprettify -i 2 -r .${reset}\n" + exit 1 + fi diff --git a/example/ex1.f90 b/example/ex1.f90 new file mode 100644 index 0000000..bc160e6 --- /dev/null +++ b/example/ex1.f90 @@ -0,0 +1,9 @@ +program ex_1 + use run_f, only: run + implicit none + + character(:), allocatable :: output + + output = run('whoami') + print *, output +end diff --git a/example/ex2.f90 b/example/ex2.f90 new file mode 100644 index 0000000..9c9b313 --- /dev/null +++ b/example/ex2.f90 @@ -0,0 +1,16 @@ +program ex_2 + use run_f, only: run + implicit none + + character(:), allocatable :: output + character(*), parameter :: command = 'xyzabc' + logical :: has_error + + output = run(command, has_error, print_cmd=.true.) + + if (has_error) then + print *, 'Error executing command: ', command + stop 1 + end if + +end diff --git a/fpm.toml b/fpm.toml new file mode 100644 index 0000000..a8b6641 --- /dev/null +++ b/fpm.toml @@ -0,0 +1,9 @@ +name = "run-f" +version = "0.1.0" +license = "MIT" +author = "Minh Dao" +maintainer = "hello@minhdao.de" +copyright = "Copyright 2024, Minh Dao" + +[build] +module-naming = true diff --git a/src/run_f.f90 b/src/run_f.f90 new file mode 100644 index 0000000..21a4f32 --- /dev/null +++ b/src/run_f.f90 @@ -0,0 +1,93 @@ +module run_f + use, intrinsic :: iso_c_binding, only: c_ptr, c_char, c_int, c_null_ptr, & + & c_null_char, c_associated + implicit none + private + + public :: run + + interface + function popen(cmd, mode) bind(C, name='popen') + import :: c_char, c_ptr + character(kind=c_char), dimension(*) :: cmd + character(kind=c_char), dimension(*) :: mode + type(c_ptr) :: popen + end + + function fgets(str, size, stream) bind(C, name='fgets') + import :: c_char, c_ptr, c_int + character(kind=c_char), dimension(*) :: str + integer(kind=c_int), value :: size + type(c_ptr), value :: stream + type(c_ptr) :: fgets + end + + function pclose(stream) bind(C, name='pclose') + import :: c_ptr, c_int + type(c_ptr), value :: stream + integer(c_int) :: pclose + end + end interface + +contains + + !> Convert a C string to a Fortran string. + pure function c2f(c) result(f) + character(len=*), intent(in) :: c + character(len=:), allocatable :: f + + integer :: i + + i = index(c, c_null_char) + + if (i <= 0) then + f = c + else if (i == 1) then + f = '' + else if (i > 1) then + f = c(1:i - 1) + end if + end + + !> Run a command in the command line and return the result as a string. + function run(cmd, has_error, print_cmd) result(str) + !> The command to run in the command line. + character(len=*), intent(in) :: cmd + + !> True if running the command results in an error. + logical, optional, intent(out) :: has_error + + !> Whether the command is printed to the command line before it is executed. + logical, optional, intent(in) :: print_cmd + + !> Stores the result of the command. + character(len=:), allocatable :: str + + integer, parameter :: buffer_length = 1024 + + type(c_ptr) :: handle + integer(c_int) :: istat + character(kind=c_char, len=buffer_length) :: line + + if (present(print_cmd)) then + if (print_cmd) print *, 'Running command: ', cmd + end if + + if (present(has_error)) has_error = .false. + + str = '' + handle = c_null_ptr + handle = popen(cmd//c_null_char, 'r'//c_null_char) + + if (.not. c_associated(handle)) then + if (present(has_error)) has_error = .true.; return + end if + + do while (c_associated(fgets(line, buffer_length, handle))) + str = str//c2f(line) + end do + + istat = pclose(handle) + if (present(has_error)) has_error = istat /= 0 + end +end diff --git a/test/run_f_test.f90 b/test/run_f_test.f90 new file mode 100644 index 0000000..e2f48a2 --- /dev/null +++ b/test/run_f_test.f90 @@ -0,0 +1,48 @@ +program run_f_test + use run_f, only: run + implicit none + + character(:), allocatable :: str + logical :: has_error + + str = run('') + if (str /= '') call fail('Empty command has output (no error handling).') + + str = run('', has_error) + if (str /= '') call fail('Empty command has output.') + if (has_error) call fail('Empty command has error.') + + str = run('whoami') + if (str == '') call fail('whoami has no output (no error handling).') + + str = run('whoami', has_error) + if (str == '') call fail('whoami has no output.') + if (has_error) call fail('whoami failed.') + + str = run('whoami', has_error, print_cmd=.false.) + if (str == '') call fail('whoami has no output (no print).') + if (has_error) call fail('whoami failed.') + + str = run('whoami', has_error, print_cmd=.true.) + if (str == '') call fail('whoami has no output (with print).') + if (has_error) call fail('whoami failed.') + + str = run('whoami -x', has_error) + if (.not. has_error) call fail('whoami with invalid option did not fail.') + + str = run('whoami xyz', has_error) + if (.not. has_error) call fail('whoami with invalid argument did not fail.') + + str = run('abcdefg', has_error) + if (.not. has_error) call fail('Invalid command did not fail.') + + print *, achar(10)//achar(27)//'[92m All tests passed.'//achar(27) + +contains + + subroutine fail(msg) + character(*), intent(in) :: msg + print *, achar(27)//'[31m'//'Test failed: '//msg//achar(27)//'[0m' + stop 1 + end +end