Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
minhqdao committed Apr 24, 2024
0 parents commit 28f0da6
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
Empty file added .github/workflows/ci.yml
Empty file.
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -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.
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
75 changes: 75 additions & 0 deletions ci.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions example/ex1.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
program ex_1
use run_f, only: run
implicit none

character(:), allocatable :: output

output = run('whoami')
print *, output
end
16 changes: 16 additions & 0 deletions example/ex2.f90
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions fpm.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name = "run-f"
version = "0.1.0"
license = "MIT"
author = "Minh Dao"
maintainer = "[email protected]"
copyright = "Copyright 2024, Minh Dao"

[build]
module-naming = true
93 changes: 93 additions & 0 deletions src/run_f.f90
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions test/run_f_test.f90
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 28f0da6

Please sign in to comment.