匿名希望のおでんFortranツヴァイさん太郎

生き物、Fortran、川について書く

コマンド ライン引数を用いる利点と Fortran における使用例

本稿にはコマンド ライン引数を用いることの利点、 ならびに Fortran におけるコマンド ライン引数使用の例を示しています。

私が考えるコマンド ライン引数を利用する利点は以下の通りです。

  • 使いまわしやすいプログラムを書ける
  • バッチ処理が可能になる

入力ファイル、出力ファイル、パラメータなどを替えて同じプログラムを走らせたいとき、これらの値をハード コーディングしていると、毎回違う値を入力してコンパイルし直す必要があります。しかし、これらをコマンド ライン引数として与えるプログラムにしていれば、コンパイルし直すことなく使用することができます。

バッチ ファイルにまとめて書くことで、複数の処理をまとめて実行することもできます。例えば、Fortran で計算し、Python で可視化する、という処理を一括で行えます。 何を入力したかをバッチ ファイルに残すことができるので、計算を再現するための条件を残しておくこともできます。また、バッチ ファイル自体がプログラムの使用例になります。

Fortran に限らず、コマンド ライン引数を用いたプログラムにおける処理の基本的な流れは以下の通りです。

  1. コマンド ライン引数を受け取ったら、構文の解析を行う。

    • 引数の個数、値の範囲、型などが正しいか分析します。
  2. 構文にエラーがあれば、ユーザーに使用方法を通知する。

    • 例外を投げるだけだと、ユーザーには意味不明の文が流れるので、 きちんと使用方法を提示します。
  3. 意味がわかる名前の変数に、受け取った引数の値をすぐに代入する。

    • args(1)、args(2)... のような名前のまま使うと、 何を格納した変数かわからないためです。
    • スコープ最小の原則にそむくように見えますが、 スコープ最小の原則の目的は覚えておくべきことを最小にすることです。 すぐに意味のある変数に代入することで、args(1)、args(2)... が何であるかを 覚えておく必要がなくなります。

コマンド ライン引数を Fortran で用いるには get_command_argument*1 を用います。 get_command_argument はそのままでは使いにくいので、command_getter クラスを作りました。

この command_getter を用いたプログラムの例を示します。

メイン プログラムは以下の通りです*2

! コマンド ライン引数取得のテストです。
! 名前、年齢、体重をコマンド ライン引数で与えます。
! 標準出力にコマンド ライン引数で与えた名前、年齢、体重を表示します。
program main_program
    use, intrinsic :: iso_fortran_env
    use class_command_getter
    implicit none

    ! 名前
    character(:), allocatable :: name
    ! 年齢
    integer(int32) age
    ! 体重
    real(real64) weight

    ! コマンド ライン引数を取得するクラス
    type(command_getter) args_getter


    ! コマンド ライン引数を解析します。
    if(args_getter%get_size() /= 3) then
        ! エラーを通知してプログラムを終了します。
        write(error_unit, *) "There must be following command line arguments."
        write(error_unit, *) "  - name: character"
        write(error_unit, *) "  - age: integer"
        write(error_unit, *) "  - weight (kg): real number or integer"
        write(error_unit, *) "(e.g.) command_getter_test.exe Rikishi 25 150.6"
        write(error_unit, *) "Program ended."
        stop
    end if
    
    ! 名前を決定します。
    name = args_getter%get(1)
    ! 年齢を決定します。
    age = args_getter%to_int32(2)
    ! 体重を決定します。
    weight = args_getter%to_real64(3)
    
    ! 名前、年齢、体重を標準出力に表示します。
    print'(A, A)', "Name: ", name
    print'(A, I3)', "Age: ", age
    print'(A, F5.1, A)', "Weight: ", weight, " kg"
end program

class_command_getter は以下の通りです。

! コマンド ライン引数を取得するクラスです。
module class_command_getter
    use, intrinsic :: iso_fortran_env
    implicit none
    
    private
    
    ! コマンド ライン引数を表す構造体です。
    type, private :: command_line_argument
        character(:), private, allocatable :: value
    end type

    
    ! コマンド ライン引数を取得するクラスです。
    type, public :: command_getter
        ! コマンド ライン引数を取得していれば true、そうでなければ false
        logical, private :: has_values = .false.
        ! コマンド ライン引数
        type(command_line_argument), private, allocatable :: value(:)

        contains

        procedure, private, pass :: get_all_command_line_argument
        procedure, public, pass :: get
        procedure, public, pass :: get_size
        procedure, public, pass :: to_int32
        procedure, public, pass :: to_real64
    end type


    contains


    ! i 番目のコマンド ライン引数を返します。戻り値の型は character です。
    !
    ! @param i コマンド ライン引数の順番 (1 始まり、int32)
    ! @return i 番目のコマンド ライン引数
    function get(this, i) result(arg)
        ! 引数
        class(command_getter), intent(inout) :: this
        integer(int32), intent(in) :: i
        ! 戻り値
        character(:), allocatable :: arg

        ! コマンド ライン引数を取得していなければ、コマンド ライン引数を全て取得します。
        if(.not. this%has_values) then
            call this%get_all_command_line_argument()
        end if

        arg = this%value(i)%value
    end function


    ! コマンド ライン引数の個数を返します。
    !
    ! @return コマンド ライン引数の個数
    function get_size(this) result(number_of_args)
        ! 引数
        class(command_getter), intent(inout) :: this
        ! 戻り値
        integer(int32) number_of_args

        ! コマンド ライン引数を取得していなければ、コマンド ライン引数を全て取得します。
        if(.not. this%has_values) then
            call this%get_all_command_line_argument()
        end if

        number_of_args = size(this%value)
    end function


    ! 指定されているコマンド ライン引数を全て取得します。
    subroutine get_all_command_line_argument(this)
        ! 引数
        class(command_getter), intent(inout) :: this
        ! コマンド ライン引数
        character(:), allocatable :: arg
        
        ! コマンド ライン引数の個数
        integer(int32) number_of_args
        ! コマンド ライン引数の文字列長を格納する配列
        integer(int32) length
        ! コマンド ライン引数取得時の状態
        integer(int32) status
        ! ループ カウンタ
        integer(int32) i
        
        ! コマンド ライン引数の個数を取得します。
        number_of_args = command_argument_count()
        
        ! コマンド ライン引数を格納する配列を生成します。
        allocate(this%value(number_of_args))
        
        do i = 1, number_of_args
            ! i 番目のコマンド ライン引数の長さを取得します。
            call get_command_argument(i, length=length, status=status)
            
            ! コマンド ライン引数の取得に失敗すれば、プログラムを終了します。
            if (status /= 0) then
                write(error_unit, *) "Command line argument retrieval failed."
                write(error_unit, *) "Program ended."
                stop
            end if
            
            ! i 番目のコマンド ライン引数を取得します。
            allocate(character(length) :: arg)
            call get_command_argument(i, arg, status=status)
            
            ! コマンド ライン引数の取得に失敗すれば、プログラムを終了します。
            if (status /= 0) then
                write(error_unit, *) "Command line argument retrieval failed."
                write(error_unit, *) "Program ended."
                stop
            end if

            ! コマンド ライン引数を格納します。
            this%value(i)%value = arg
            
            deallocate(arg)
        end do

        this%has_values = .true.
    end subroutine


    ! i 番目のコマンド ライン引数を int32 に変換します。
    !
    ! @param i int32 に変換したいコマンド ライン引数のインデックス
    ! @return int32 の整数
    function to_int32(this, i) result(output_integer)
        ! 引数
        class(command_getter), intent(inout) :: this
        integer(int32), intent(in) :: i
        ! 戻り値
        integer(int32) output_integer
        ! iostat 指定子
        integer(int32) iostat

        ! コマンド ライン引数を取得していなければ、コマンド ライン引数を全て取得します。
        if(.not. this%has_values) then
            call this%get_all_command_line_argument()
        end if

        ! コマンド ライン引数を int32 に変換します。
        read(this%value(i)%value, *, iostat=iostat) output_integer

        ! 変換にエラーがあればプログラムを終了します。
        if(iostat > 0) then
            write(error_unit, *) "An error occurred in to_int32 in class_command_getter."
            write(error_unit, *) "Program ended."
            stop
        end if
    end function


    ! i 番目のコマンド ライン引数を real64 に変換します。
    !
    ! @param i real64 に変換したいコマンド ライン引数のインデックス
    ! @return real64 の実数
    function to_real64(this, i) result(output_real_value)
        ! 引数
        class(command_getter), intent(inout) :: this
        integer(int32), intent(in) :: i
        ! 戻り値
        real(real64) output_real_value
        ! iostat 指定子
        integer(int32) iostat

        ! コマンド ライン引数を取得していなければ、コマンド ライン引数を全て取得します。
        if(.not. this%has_values) then
            call this%get_all_command_line_argument()
        end if

        ! コマンド ライン引数を real64 に変換します。
        read(this%value(i)%value, *, iostat=iostat) output_real_value

        ! 変換にエラーがあればプログラムを終了します。
        if(iostat > 0) then
            write(error_unit, *) "An error occurred in to_real64 in class_command_getter."
            write(error_unit, *) "Program ended."
            stop
        end if
    end function
end module

*1:https://gcc.gnu.org/onlinedocs/gfortran/GET_005fCOMMAND_005fARGUMENT.html

*2:型チェックの良い方法が思いつかなかったので、今回はコマンド ライン引数の数だけを解析しています。