#ifdef UNDEF // 從這裡開始的內容會被 C++ 編譯器所忽略
ap=$(mktemp -u)
c++ -Wall -o $ap "$0" && $ap $@
rc=$?
test -f $ap && rm $ap
exit $rc # 腳本命令到這裡被強制跳離,因此以下所有檔案內容不受 shell 解析處理
#endif // 直到此之前的內容會被 C++ 編譯器所忽略
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Running CPP file as script...\n");
printf("Arguments (argc=%d):\n", argc);
for(int i = 0; i < argc; ++i)
printf(" argv[%d]: \"%s\"\n", i, argv[i]);
int rc = 7;
printf("Return the result code: %d\n", rc);
return rc;
}
把 C++ 程式碼當直譯腳本執行
這次分享一個程式小技巧,能把 C++ 程式碼檔案當成腳本來執行, 或許在合適的情況下會挺好用的喔!
有時候我們就是會有一些瑣碎的事情需要即時做個小工具來處理。 我們通常不會太苛求這種工具的功能完整嚴謹與全面的覆蓋性, 因為作為使用者兼設計師的我們可能是隨時監看並能依據當下條件即時調整修改程式的。 對於計算效能多半也是無所謂的態度,因為對於或許是一次性的工具來說, 通常使用更少的時間與消耗更少的頭腦精力去把東西製造出來的效益會遠大於仔細推敲打磨它。 這時候我們通常就會傾向使用腳本語言來進行這些工作, 比如我自己就經常寫些很簡單的 shell script,稍微複雜困難一點的工作就寫個 Python 來處理。
但是吧,有些工作使用這些常見的腳本語言來做就是存在一些不容易。 可能雖然工作是一次性的,但資料量相當龐大,也不是真的這麼不在意直譯程式的效能; 可能是有一些底層的資料格式需要分析處理,例如位元組、無號定長整數的計算處理等, 使用 C/C++ 真的反而就是比較簡單方便; 也可能其實當下流行的腳本語言並非沒有能力去實現出這些功能,只是因為個人學習鑽研道行實在不足。 無論如何,對於一個數十年與 C/C++ 日夜相伴的程序員來說, 有時候就是會有想要使用 C++ 去製做這些工具的想法。
那麼做就做吧,產品程式都能造出來了,製做個人的工具程式有什麼困擾呢? 通常的糾結也許就在於這種編譯式的語言所產生出來或所需要依賴的檔案數量較多, 對於不是以軟體專案本身當主角,而是在周邊小工具的用途上當配角去服務其它檔案的話, 在檔案的儲存、管理、和命名等事情上就會帶來比較大的龐雜感; 特別是對於慣用 IDE 工具來進行程式開發的人來說, 身為一個小小龍套配角的工具程式其檔案數量可能會更加驚人!
使用腳本編譯執行
至此對於本篇所探討的 C++ 程式的使用情境已經明瞭了, 就是以簡單小工具為定位,以最少的檔案管理負擔為訴求, 為此目的可以容許對於執行效能的一定程度下降為代價。
從前我的做法是就在 C++ 程式碼旁邊建立一個 shell script 檔案,用來編譯並執行旁邊的程式碼檔。 不使用 Automake 或 CMake 這些龐大複雜的建置系統,當然更不可能使用 IDE 專案。 就只有一個簡單的腳本,不檢查檔案時間戳也不產生中間檔,每次執行時就是從頭全部重新編譯。 當然以配角小工具的用途來說一般程式碼沒有什麼太大的規模,程式碼的檔案數量也就只有一個, 每次執行時都重編譯過一次也並不會消耗什麼有感的時間; 而其實這就是把 C++ 拿來當直譯腳本語言使用的概念。
在這樣的使用方式下,檔案的數量減少到只剩下 2 到 3 個, 分別是主要的 C++ 程式碼,用來編譯並執行程式的腳本,以及使用後可留可刪的執行檔。 雖然已經只剩下 2、3 個檔案,但是終究還是複數, 與只需要單一一個檔案就能做事的真正腳本語言相較還是多了些負擔。 那麼能不能夠將檔案再進一步縮減到只剩下一個呢?
要進一步精減檔案數量,首先可以排除掉的就是執行檔了。 既然已經寫了個腳本去做編譯,順便編譯完後立刻執行, 那麼稍微再多寫一些步驟就能夠在執行完成後將產生的執行檔給刪除,做法並不困難。 因此真正的挑戰在於如何將 C++ 程式碼檔案與建置腳本檔案給二合一, 而這就是本文真正要探討的技巧主題了……
利用 Preprocessor
將兩個檔案合併為一個檔案的問題在於,兩個檔案內容的格式規格並不相同, 一個是 C++ 而另一個是 shell script。 要將它們合併在一起, 就必然要想個方法讓同一個檔案既能夠被 C++ 編譯器正常處理,也能被 shell 正常解析, 讓檔案內的兩個部份互相不干預對方。 關於腳本的部份倒好處理,反正 shell 是一行一行分析執行, 所以只要把腳本的部份寫在檔案開頭,然後在腳本命令最後放一個 exit 命令結束並離開, 那麼再往下的內容長成什麼鬼樣子(站在 shell parser 的角度)就完全不造成影響了! 所以真正的問題在於如何讓 C++ 編譯器去忽略掉檔案開頭的那些腳本命令?
這裡採用的方法是利用 C++ 前置處理器的識別符號「#」。
剛好在 shell script 裡面這個井字號是註解符號,
所以任何 C++ 前置處理命令在 shell 裡面都會被忽略掉,因此可以放心使用。
只剩下腳本開頭的 Shebang (也就是通常在腳本開頭出現的那個「#!」)會在 C++ 編譯的時候導致錯誤,
好在當代的 Linux 環境大部份在沒有 Shebang 的情況下也是把檔案內容當成 shell 命令在處理,
因此這裡就放心的直接捨棄掉正規 shell script 在檔案開頭的 Shebang 了。
按上述想法所產生的範例程式如下:
這樣就可以直接把 C++ 程式碼檔案當成腳本來執行(當然檔案屬性需要設定執行權限):
./cppscript-use-preprocessor.cpp qui tollis peccata mundi; echo "RC=$?"
上面的執行命令最終產生了如下的結果, 可以注意到我把程式的啟動參數傳遞以及返回值的傳遞都處理妥當了:
Running CPP file as script...
Arguments (argc=5):
argv[0]: "/tmp/tmp.KU6vOCid49"
argv[1]: "qui"
argv[2]: "tollis"
argv[3]: "peccata"
argv[4]: "mundi"
Return the result code: 7
RC=7
這樣其實就已經達到了一開始所要的目的,將全部檔案整合為一個檔案,把 C++ 當直譯腳本來用。 當然上面的範例設想的還是比較多的,如果你自己使用情況下並不需要取得程式回傳值, 或者不需要傳輸啟動參數,或者甚至可以直接指定一個固定的執行檔名稱而不是像上面隨機產生一個暫時檔, 那麼有關建置與執行的命令還可以再刪減的更加精簡。
利用單行註解
理論上使用上面的方法已經達成了將程式碼與腳本合併只剩一個檔案,
並且能把 C++ 檔案當直譯程式來執行的目的。
只不過對於檔案開頭好多行的腳本內容還是覺得有些礙眼,
特別是雖然那些命令已經被包含在 #ifdef 裡面,但可能有些文字編輯器還是會嘗試去分析裡面的內容,
而因為這些內容都不是合法的 C++ 語法,
因此雖然實際不會產生錯誤,但在編寫 C++ 的時候可能會顯示出許多較為「醒目」的語法著色顯示。
於是我好奇有沒有能人還有更好的想法?結果一查欸還真有!
[1]
其中一種方式就是利用 C++ 的單行註解符號「//」。
而斜線「/」同時也是路徑的分隔符號,單獨的或起頭的斜線則在 POSIX 相容系統上表示根目錄,
而兩個連著的斜線路徑分隔符其實等同於只有一個。
因此檔案的起頭必須指定一個絕對路徑,也就是說檔案一開始的第一個命令就是要執行一支程式。
上面參考網站的內容直接使用絕對路徑執行編譯器,
然而因為我在編譯前還想先設定變數內容,因此無法像他這樣簡單的直接呼叫命令。
我的做法是將第一個命令給虛化,它不需要做實事,
我只不過是利用它的路徑起頭符號將整行文字被 C++ 編譯器判定為註解而已。
因此我選擇首先執行 true 命令,基本等於沒有任何實際效果,
然後在分號後面再去執行我真正要執行的工作。
按照這樣的想法,我將前面的那個範例修改如下 (至於命令的執行和輸出的結果就不貼了,反正是一樣的):
//bin/true; ap=$(mktemp -u); (c++ -Wall -o $ap "$0" && $ap $@); rc=$?; (test -f $ap && rm $ap); exit $rc
#include <stdio.h>
int main(int argc, char *argv[])
{
......
}
就這樣把全部的命令塞在一行 C++ 註解裡面,看著更加清爽也少了視覺干擾。 只不過缺點也很明顯,這一行文字挺長的, 如果編譯的命令再複雜一點,比如說連結一些程式庫、加入更多編譯巨集等,那長度又會更加延伸; 不過相對的,如果你的使用並不需要回傳值、程式參數這些東西,甚至也不想刪除編譯出來的執行檔的話, 那麼這個寫法就會變得相當簡潔,相當合適! 例如像下面這樣,看是不是好多了:
//bin/c++ -Wall -o $PWD/example "$0" && $PWD/example $@
#include <stdio.h>
int main(int argc, char *argv[])
{
......
}
利用多行註解
前一個做法的缺點就是在編譯參數較多的時候,檔案第一行會變得很長,
有些人可能覺得無所謂,有些人可能覺得受不了。
那如果無法忍受過長的文字行的話,就還是得把命令寫成多行來表達,
按照延續的思路,我們可以用 C++ 的多行註解來處理這樣的需求。
其巧思之處在於 C++ 多行註解的開頭「/*」,在 shell 下的意義就是根目錄下的任一個檔案或資料匣,
那麼我就取用其中的隨便哪一個,然後再返回上一層,看兜了一圈不就等於又回來了!
按此思路修改前面範例,則為:
/*/../bin/true
ap=$(mktemp -u)
c++ -Wall -o $ap "$0" && $ap $@
rc=$?
test -f $ap && rm $ap
exit $rc
*/
#include <stdio.h>
int main(int argc, char *argv[])
{
......
}
這樣就兼顧了可以在合理的版面空間內容納較多較複雜的命令, 同時又把命令全塞進 C++ 註解內減少在編寫程式時的干擾,幾乎完美解決了所有問題。 因此最終,在想要把 C++ 程式碼當成直譯腳本來使用的需求下, 我推薦最後面介紹的這兩種使用註解的寫法, 在編譯與執行命令較為簡單簡潔時推薦使用單行註解方式, 而在相關命令比較繁多時則推薦使用多行註解的方式。
下一篇:「執行緒同步 1:簡介」