プロジェクトを作らずにファイル単体で書ける F# スクリプトは重宝しますが、コンパイルしたときと F# Interactive で動かすときとでコマンドライン引数が変わります。それをすり合わせました。
F# スクリプトを手軽に扱うには VS Code + Ionide がお勧めです。
状況確認
コマンドライン引数の扱いを確認します。
printfn "%A" (System.Environment.GetCommandLineArgs())
>fsi args1.fsx abc def
[|"fsi"; "args1.fsx"; "abc"; "def"|]
>fsc args1.fsx
Microsoft (R) F# Compiler バージョン 10.6.0.0 for F# 4.7
Copyright (C) Microsoft Corporation. All rights reserved.
>args1 abc def
[|"args1"; "abc"; "def"|]
$ fsharpi args1.fsx abc def
[|"/usr/lib/mono/fsharp/fsi.exe"; "--exename:fsharpi"; "args1.fsx"; "abc"; "def"|]
$ fsharpc args1.fsx
Microsoft (R) F# Compiler version 10.2.3 for F# 4.5
Copyright (c) Microsoft Corporation. All Rights Reserved.
$ mono args1.exe abc def
[|"/home/xxx/test/args1.exe"; "abc"; "def"|]
fsi の存在が見えています。Mono ではオプションも見えています。
プログラムに引数として渡したい abc
def
の位置が、コンパイルした時とずれています。
対策
fsi から起動されているかどうかを確認して、引数の開始位置をずらします。
let args =
let args = System.Environment.GetCommandLineArgs()
match System.IO.Path.GetFileNameWithoutExtension args.[0] with
| "fsi" | "fsiAnyCpu" ->
args.[1..] |> Array.skipWhile (fun s -> s.StartsWith("--"))
| _ -> args
printfn "%A" args
>fsi args2.fsx abc def
[|"args2.fsx"; "abc"; "def"|]
>fsc args2.fsx
Microsoft (R) F# Compiler バージョン 10.6.0.0 for F# 4.7
Copyright (C) Microsoft Corporation. All rights reserved.
>args2 abc def
[|"args2"; "abc"; "def"|]
$ fsharpi args2.fsx abc def
[|"args2.fsx"; "abc"; "def"|]
$ fsharpc args2.fsx
Microsoft (R) F# Compiler version 10.2.3 for F# 4.5
Copyright (c) Microsoft Corporation. All Rights Reserved.
$ mono args2.exe abc def
[|"/home/xxx/test/args2.exe"; "abc"; "def"|]
fsi の存在が隠れて、コンパイルした時と同様に扱えるようになりました。
合わせ技
最初に起動したスクリプトかどうかを判定する方法が以下の記事にあります。
これを今回の技と組み合わせます。
do
let frames = System.Diagnostics.StackTrace().GetFrames()
|> Array.map (fun frame -> frame.GetMethod().Name)
if frames.[0] <> "main@" || Array.contains "EvalParsedSourceFiles" frames then () else
let args =
let args = System.Environment.GetCommandLineArgs()
match System.IO.Path.GetFileNameWithoutExtension args.[0] with
| "fsi" | "fsiAnyCpu" ->
args.[1..] |> Array.skipWhile (fun s -> s.StartsWith("--"))
| _ -> args
printfn "%A" args
ここでやりたいことは Python でよく見かける以下と同じことです。
import sys
if __name__ == '__main__':
print(sys.argv)
※ Python に比べると過剰な感は否めません。
EntryPoint
最初は EntryPoint を使えば良いのではないかと思ったのですが、コンパイルしたときしか実行されません。
open System
[<EntryPoint>]
let main args =
printfn "%A" args
0
$ fsharpi entry.fsx abc def
$ fsharpc entry.fsx
Microsoft (R) F# Compiler version 10.2.3 for F# 4.5
Copyright (c) Microsoft Corporation. All Rights Reserved.
$ mono entry.exe abc def
[|"abc"; "def"|]
EntryPoint に渡される引数には実行ファイルの名前は含まれませんが、それを知りたいことがあるため、今回の記事の方法では敢えて残しています。
経緯
以前使っていた MonoDevelop では F# スクリプトもコンパイルして実行していたので、あまり気にしていませんでした。しかし最近使っている VS Code + Ionide ではコンパイルせずに実行するため、挙動の違いが無視できなくなりました。