Fortranの標準手続きを用いてコマンドライン引数を取得する

Fortranのプログラム中で，コマンドライン引数を取得する方法をまとめます．
ネット上にいくつか記事が見られますが，まだ非標準の手続きを使っている場合もあります．本記事では，Fortran 2003から追加された標準手続きを利用します．Fortran2018から少し変わっている部分もあるので，それについても言及します．

以前に[記事の付録](https://qiita.com/implicit_none/items/3cde9f429be705ccc080#%E4%BB%98%E9%8C%B21-fortran%E6%A8%99%E6%BA%96%E3%81%AE%E6%89%8B%E7%B6%9A%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E5%BC%95%E6%95%B0%E3%81%AE%E5%8F%96%E5%BE%97)としてまとめていますが，改めて一つの記事として切り出しています．

# 概要
- Fortranでコマンドライン引数を取得するには，サブルーチン`get_command_argument`を利用する．
- 定石として，`get_command_argument(number,length)`でn番目の引数の長さを取得して文字列を割付けた後，`get_command_argument(number,value)`で引数の文字列を取得する．
- サブルーチン実行の成否は整数（0なら成功，それ以外なら失敗）として取得できるが，Fortran2018からはエラーメッセージも取得できるようになっている．

# 環境
- gfortran 8.1.0 (Windows, Linux)
- intel fortran 2021.1 (Windows, Linux)
- nvfortran 20.11-0 (Linux)

# Fortran標準の手続によるコマンドライン引数の取得

Fortran 2003から，コマンドライン引数に関する手続き（サブルーチンまたは関数）が追加されました．
コマンドライン引数全体を取得するサブルーチン`get_command()`，引数の個数を取得する`command_argument_count()`関数，各引数を取得するサブルーチン`get_command_argument()`です．


|関数|機能|
|:--|:--|
|`get_command([command = コマンドライン引数, length = コマンドライン引数の長さ, status=取得状態])`|コマンドライン引数全体を取得する<br>`command`引数が指定された場合，実行コマンドも含む全てのコマンドライン引数を文字列として取得する<br>`length`引数が指定された場合，コマンドライン引数の文字列の長さを取得する<br>`status`は，コマンドライン引数を正常に取得できれば0，文字列の変数がコマンドライン引数よりも短ければ-1，それ以外で正常に取得できなかった場合は正の値|
|`command_argument_count()`|実行コマンドを**含まない**引数の個数を標準種別の整数型で返す|
|`get_command_argumet(number=引数番号, [value = 引数の値（文字列）, length = 引数の長さ, status=取得状態])`|指定された番号（`0`$\le$`number`$\le$`command_argument_count()`）の引数を取得する（0は実行コマンド）<br>`value`引数が指定された場合，指定された番号の引数を文字列として取得する<br>`length`引数が指定された場合，指定された番号の引数の文字列長さを取得する<br>`status`は，引数を正常に取得できれば0，文字列の変数が引数よりも短ければ-1，それ以外で正常に取得できなかった場合は正の値|

`get_command`や`get_command_argument`の引数`command`, `length`, `value`は省略可能ですが，現実的な使い方としてすべてを省略することはないでしょう．

個別の引数は，下記の手順で取得すると無駄がありません．使う変数名などは異なりますが，他の記事でも同様の手順で引数が取得されており，定石といってよいでしょう．

1. `command_argumet_count()`での個数を取得
2. `do`ループの中で`get_command_argument()`によって各引数の長さを取得
3. 取得した長さの文字列を動的に割り付けて`get_command_argument()`で引数を取得

```Fortran
program main
    use, intrinsic :: iso_fortran_env
    implicit none

    type :: arguments
        character(:), allocatable :: v
    end type

    integer(int32) :: argc
    type(arguments), allocatable :: arg(:)

    argc = command_argument_count() ! 実行コマンドを含まない引数の個数
    allocate (arg(0:argc))          ! 実行コマンド+引数を格納するための文字列配列

    get_arguments: block
        integer(int32) :: n, length_nth_arg
        do n = 0, argc
            ! 第n引数の長さを取得
            call get_command_argument(number=n, length=length_nth_arg)

            ! 第n引数の長さと同じ長さの文字列を割付
            allocate (character(length_nth_arg) :: arg(n)%v)

            ! 第n引数の値を文字列で取得
            call get_command_argument(number=n, value=arg(n)%v)

            print *, n, arg(n)%v
        end do
    end block get_arguments
end program main
```

上記のプログラムでは，C言語の流儀に従って，コマンドライン引数の個数を`argc`，引数の文字列を`argv`と表現するようにしています．引数の文字列の格納には，自動割付文字列を成分に持つ構造体`arguments`型を作成し，その`arguments`型の配列を利用しています．

上記のプログラムをコンパイルして実行すると，実行コマンドを含む引数の番号とその文字列が出力されます．

```console
 $ ./a.out --help --version
           0 ./a.out
           1 --help
           2 --version
```

# エラー処理
サブルーチンで引数を正しく取得できているか否かを確認するには，`status`引数として整数型変数を渡します．サブルーチン実行後にその値を確認し，その値に応じて処理を切り分けます．

```Fortran
program main
    use, intrinsic :: iso_fortran_env
    implicit none

! 中略

    integer(int32) :: stat

! 中略

            call get_command_argument(number=n, value=arg(n)%v, status=stat)
            if (stat /= 0) then
                write (error_unit, '(*(g0))') "error in get_command_argument: &
                                              &unable to get correctly the string of argument : ", n
                stop
            end if
! 中略
end program main
```

`status`の値を確認することで，実行の成否が確認できます．したがって，`status`はエラーコードと見ることも可能ですが，エラーコードと生じているエラーの対応関係は，規格では規定されていないようです．
Intel Fortranの実行時エラーについては，[一覧](https://software.intel.com/content/www/us/en/develop/documentation/fortran-compiler-oneapi-dev-guide-and-reference/top/compiler-reference/error-handling/handling-run-time-errors/list-of-run-time-error-messages.html)が存在しています．

# Fortran 2018での拡張
上述したように，サブルーチンの引数`status`でエラーコードを取得できますが，エラーコードと生じているエラーの対応を調べるのも一苦労です．

Fortran 2018からは，`get_command`と`get_command_argument`に新たな引数`errmsg`が追加されました．この引数を利用すると，引数として渡した文字列にエラーメッセージが書き込まれます．`status`と併せて利用することで，`status`が`0`でない場合にエラーの内容を表示できます．

```Fortran
program main
    use, intrinsic :: iso_fortran_env
    implicit none

    type :: arguments
        character(:), allocatable :: v
    end type

    integer(int32) :: argc, stat
    type(arguments), allocatable :: arg(:)

    integer(int32), parameter :: len_buffer = 255
    character(len_buffer) :: error_message

    argc = command_argument_count() ! 実行コマンドを含まない引数の個数
    allocate (arg(0:argc))          ! 実行コマンド+引数を格納するための文字列配列

    get_arguments: block
        integer(int32) :: n, length_nth_arg
        do n = 0, argc
            ! 第n引数の長さを取得
            call get_command_argument(number=n, length=length_nth_arg, status=stat, errmsg=error_message)
            if (stat /= 0) then
                write (error_unit, '(*(g0))') trim(error_message)
                stop
            end if

            ! 第n引数の長さと同じ長さの文字列を割付
            allocate (character(length_nth_arg) :: arg(n)%v)

            ! 第n引数の値を文字列で取得
            call get_command_argument(number=n, value=arg(n)%v, status=stat, errmsg=error_message)
            if (stat /= 0) then
                write (error_unit, '(*(g0))') trim(error_message)
                stop
            end if

            print *, n, arg(n)%v
        end do
    end block get_arguments
end program main
```

例えば，存在しない引数の番号を指定してしまった場合には

```console
Empty arg in retrieved command
```

というエラーメッセージが表示されます．

残念ながら，Fortran 2018相当の`get_command`と`get_command_argument`が利用できるのは，テストした限りではIntel Fortranのみです．

Fortranでは，文字列に`allocatable`属性を付けると，自動割付文字列という扱いになり，文字列にリテラルなどを**代入**する際に，その長さが自動で変化します．しかし，この自動割付は代入時にしか有効ではないので，引数`errmsg`に未割付の文字列を渡すと，実行時エラーとなります．そのため，上記のプログラムでは，`errmsg`に相当する文字列`error_message`は長さを固定し，出力の際に`trim`関数で文字列後部の空白を消しています．

Fortranの次の規格（おそらくFortran 2023）では，自動割付は**代入時のみ**という制約が外れるので，今後はもっと使いやすくなると予想されます．