【Excel】Mac版VBAでShellを使用する方法

# はじめに
Excelでwebブラウザの操作を自動化する最小機能のみ実装したフレームワークの「[TinySeleniumVBA](https://github.com/uezo/TinySeleniumVBA/blob/main/README.ja.md)」があります。
これは今のところActiveXを使用しているため、Windows専用になっています。ActiveXの部分を取り除き代替部分を[VBA-tools](https://github.com/VBA-tools)ライブラリー群を使えば、Mac上のExcelで動作するかもと取り組みました。
VBA-toolsを用いてWindows上では動作するようになったのですが、残念ながらMacではエラーで動作しませんでした。

調査した結果、WebDriver(chromedriver or msedgedriver)のプロセスが起動していないことが分かりました。
Shell関数の戻り値 0 にはなっていないため、エラーは出ていません。

```vb
' Start WebDriver executable
If Shell(driverPath, vbMinimizedNoFocus) = 0 Then
    MsgBox "Failed in starting WebDriver"
    End
End If
```

# 環境
* Mac Book Pro(2.3 GHz 8コアIntel Core i9) Monterey 12.2.1
* Microsoft Office Home and Student 2019

# MacのShell関数は使えない
>Macintoshコンピュータでは、MacID関数を使用して、アプリケーションの名前の代わりにアプリケーションの署名を指定できます。次の例では、Microsoft Wordの署名を使用しています。Shell MacID("MSWD"）
https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/shell-function

MacIDって検索してみたけど、よく分からない。他の方法を模索する。

# Shellの代替案 
## MacScript関数を利用する方法
ネットで検索するとShell関数の代わりにMacScript関数を使用する方法が散見される。
しかし、MacScript関数は廃止され、サポートされなくなりました。

>MacScript について
以前のバージョンの Office for Mac には、インラインの Applescript をサポートしている MacScript と呼ばれるコマンドが実装されていました。 このコマンドは、Office 2016 for Mac でもまだ存在していますが、MacScript は使用されなくなりました。 サンドボックス制限のため、MacScript コマンドは、Finder などのほかのアプリケーションを Office 2016 for Mac で呼び出すことができません。 Office 2016 for Mac のアプリでは、MacScript コマンドの代わりに AppleScriptTask コマンドを使用することをお勧めします。
https://docs.microsoft.com/ja-jp/office/vba/office-mac/applescripttask

エラーにはならないが、常にFALSEが返ってくるとのこと。

>Excel2011ではVBA内にAppleScriptをインラインで記述し、MacScript関数で実行する事ができましたが、Excel2016以降、セキュリティ上の観点から廃止されました。Excel2019で実行するとエラーにはなりませんが、常にFALSEが帰ってきます。
[Excel 2019 for Mac OS判定とAppleScriptTask利用方法](https://qiita.com/EugenesWorks/items/f802a8680836de951ab8) 

## AppleScriptTask関数を利用する方法
Office 2016 for Mac 以降では、MacScript関数の代わりに AppleScriptTask関数を使用する。
先述したTinySeleniumVBAのShell関数は、AppleScriptTask関数に置き換えることで対応できた。

AppleScriptTask関数を利用する手順は２段階となります。

1. スクリプトファイルを「~/Library/Application Scripts/com.microsoft.Excel/」に保存する。
2. VBA内でAppleScriptTaskからスクリプトファイルを呼び出す。

VBAではライブラリーフォルダにアクセスできないので配布するとなると少し面倒です。

最初、Shellが同期した状態で動いたため次のプロセスを終了するまで先に進まなかったのですが、下記サイトの方法で「" &"」を付けることで非同期で動かすことが出来ました。

* [do shell script without waiting](https://discussions.apple.com/thread/323661)
* [(Linux) &を付けてコマンドをバックグラウンド実行する](https://hara-chan.com/it/programming/linux-command-background/)
* [いい加減覚えよう。 command > /dev/null 2>&1の意味](https://qiita.com/ritukiii/items/b3d91e97b71ecd41d4ea)

【2022/05/05追記】
~~出力結果を捨ててしまうため、"> /dev/null 2>&1"を付けるのをやめました。~~ 2022/05/21 非同期のみ付け直しました。
shell.scptとして、同期と非同期のハンドラ(関数)に分けました。

```applescript:shell.scpt
on synchandler(param)
    try
        set ret to do shell script param
        return ret
    on error number n
        return "error " & n
    end try
end synchandler

on ansynchandler(param)
    try
        set ret to do shell script param & "> /dev/null 2>&1 &"
        return ret
    on error number n
        return "error " & n
    end try
end ansynchandler
```

```vb:Example.bas
Public Sub main()
    ' Start WebDriver 
    Driver.Chrome "/usr/local/bin/chromedriver"
```

```vb:WebDriver.cls
' Start WebDriver
Public Sub Start(ByVal driverPath As String, ByVal driverUrl As String, Optional ByVal driverName As String = vbNullString)
    'Start WebDriver executable
    #If Mac Then
        Dim scriptResult As String
        scriptResult = AppleScriptTask("shell.scpt", "ansynchandler", driverPath)
    #Else
        If Shell(driverPath, vbMinimizedNoFocus) = 0 Then
            MsgBox "Failed in starting WebDriver"
        End If
    #End If
```

実行することで、chromedriverが起動していることが分かります。

```bash
yaju@hi ~ % ps aux | grep chromedriver  
yaju         12435   0.0  0.0 34132100    916 s001  S+    7:39PM   0:00.00 grep chromedriver
yaju         11642   0.0  0.0 67343788   4852   ??  S     7:33PM   0:00.02 /usr/local/bin/chromedriver
```

## C ライブラリの popen 関数を利用する方法
[VBA-WEB](https://github.com/VBA-tools/VBA-Web)ライブラリーの中にも、curlを使用するために`ExecuteInShell`関数が用意されています。

* [Excel for Mac (VBA) から HTTP GET する方法](https://qiita.com/asakaguchi/items/2c0b73e8bae218a77d21)
* [How do I issue an HTTP GET from Excel VBA for Mac - stackoverflow](https://stackoverflow.com/questions/15981960/how-do-i-issue-an-http-get-from-excel-vba-for-mac)

```vb:WebHelper.bas
#If Mac Then
#If VBA7 Then
Private Declare PtrSafe Function web_popen Lib "/usr/lib/libc.dylib" Alias "popen" (ByVal web_Command As String, ByVal web_Mode As String) As LongPtr
Private Declare PtrSafe Function web_pclose Lib "/usr/lib/libc.dylib" Alias "pclose" (ByVal web_File As LongPtr) As LongPtr
Private Declare PtrSafe Function web_fread Lib "/usr/lib/libc.dylib" Alias "fread" (ByVal web_OutStr As String, ByVal web_Size As LongPtr, ByVal web_Items As LongPtr, ByVal web_Stream As LongPtr) As LongPtr
Private Declare PtrSafe Function web_feof Lib "/usr/lib/libc.dylib" Alias "feof" (ByVal web_File As LongPtr) As LongPtr
#Else
Private Declare Function web_popen Lib "libc.dylib" Alias "popen" (ByVal web_Command As String, ByVal web_Mode As String) As Long
Private Declare Function web_pclose Lib "libc.dylib" Alias "pclose" (ByVal web_File As Long) As Long
Private Declare Function web_fread Lib "libc.dylib" Alias "fread" (ByVal web_OutStr As String, ByVal web_Size As Long, ByVal web_Items As Long, ByVal web_Stream As Long) As Long
Private Declare Function web_feof Lib "libc.dylib" Alias "feof" (ByVal web_File As Long) As Long
#End If
#End If

'
' Execute the given command
'
' @internal
' @method ExecuteInShell
' @param {String} Command
' @return {ShellResult}
''
Public Function ExecuteInShell(web_Command As String) As ShellResult
#If Mac Then
#If VBA7 Then
    Dim web_File As LongPtr
#Else
    Dim web_File As Long
#End If

    Dim web_Chunk As String
    Dim web_Read As Long

    On Error GoTo web_Cleanup

    web_File = web_popen(web_Command, "r")

    If web_File = 0 Then
        ' TODO Investigate why this could happen and what should be done if it happens
        Exit Function
    End If

    Do While web_feof(web_File) = 0
        web_Chunk = VBA.Space$(50)
        web_Read = CLng(web_fread(web_Chunk, 1, Len(web_Chunk) - 1, web_File))
        If web_Read > 0 Then
            web_Chunk = VBA.Left$(web_Chunk, web_Read)
            ExecuteInShell.Output = ExecuteInShell.Output & web_Chunk
        End If
    Loop

web_Cleanup:

    ExecuteInShell.ExitCode = CLng(web_pclose(web_File))
#End If
End Function
```

この方法で`/usr/local/bin/chromedriver`を起動しようとしたが出来ませんでした。
しかし、lsやcurlやPerlなどのコマンドは`ExecuteInShell`関数で動作します。

Perlが動くならとターミナル上でPerlのワンライナーで`chromedriver`が起動できるのを確認後、VBA側で同じように`chromedriver`を起動させようと下記プログラムを試しました。しかし、エラーにならず処理は通るものの`chromedriver`は起動されませんでした。

```vb
web_Command = "perl -e 'system(""/usr/local/bin/chromedriver &"")'"
scriptResult = WebHelpers.ExecuteInShell(web_Command)
```

lsやcurlやPerlなどが格納されている/usr/binフォルダにあるものはセキュリティ上は動作可能になっているのかも知れませんね。

# 最後に
配布に問題があるものの、`chromedriver`などの外部プロセスを動かすには、AppleScriptTask コマンドを使用しAppleScript スクリプト側で動作させるしかなさそうです。