動機
ちょっとしたコードの試し書きや小さいプログラムを使い回す際に、環境によってコンパイラの違いなどを気にしてコマンドラインでオプションをつけて叩くのは面倒である。Makefile
を書けばいいのだが、1つのディレクトリに複数のソースファイルを置くことを考えると、「Makefile
の内容をマージするのか、Makefile
のファイル名を変えて併存させるのか」といったことを気にしないといけない。そもそも、ほんのちょっとしたプログラムを別の環境に持っていくだけなのに、依存ファイル(この場合Makefile
)をコピーするとか、そのためのディレクトリに小分けにするのはちょっと大袈裟すぎる気がする。この手間を減らせないか?
ヒント
たまたま見つけた@lo48576氏の記事やブログ記事にヒントがあった。ソースコードにコンパイルするシェルスクリプトをで記述するというアイデアをいただいて、スクリプトを書くことにした。
実装方針
私は極端にものぐさな人間なので、「何よりもCLIで実行時に楽をする」ことを最大目標に、下記の方針で実装した。(とにかく使ってみたい方は次節に)
- 単体で実行可能なシェルスクリプトにする。このためファイルの先頭行はシェバンにする必要があるので、参考例での「ソースコードとしてそのままコンパイル可能」ということは実現できない。ファイルの内容をフィルタリングしてコンパイラに渡すことになる。
- (基本的にほとんど書き換える必要のない)定型のシェルスクリプトを用意して、その末尾に直接ソースコードの内容を記述するればよい、というものにする。参考例の様に、シェルスクリプトとして実行可能な内容を含んでいるものに、
.c
,.cc
,.m
,...といったソースコードの拡張子の名前のファイルとするのは、トロイの木馬感があり若干気がひけるので、.c.sh
,.cc.sh
,.m.sh
,...というファイル名とする。(そのままでコンパイルできないし。) - 実行環境による違いはなるべく気にしなくて良いようにする。主にmacOSとCentOSを想定して、
clang
があればそれを使い、なければgcc
を使う。(macOSでのFortranは、macportsでインストールされたgfortran
を想定) - 使用言語を気にせずに使いまわせるようにする。C/C++/Objective-C/Objective-C++/gfortranの典型的なコンパイル方法を書いておく。使用言語はファイル名から類推する。(わからなかったらc++だと思う。)
- シェルスクリプトが呼ばれると、スクリプトファイル自身の末尾を抽出したものをコンパイルし、正常にコンパイルできたら、作成された実行可能ファイルをシェルスクリプトの引数を渡して実行し、実行後に実行可能ファイル消す。
- 中間ファイルとして作成される実行可能ファイルのファイル名は、シェルスクリプトから類推(拡張子を2つ除いたもの)もしくは
a.out
。 - 環境変数に応じて、「コンパイルのみ実行」「コンパイル後の実行ファイルを保存」「単体で実行可能なソースコードの保存」ができるようにする。
-
emacs
での編集で言語に応じてモードが選択されるようにしたい。(ただし、これは手動で書き換える必要がある。)
ファイル置き場
使い方(手順)
- シェルスクリプトのテンプレート(
cornercut-compile_header.sh
orcornercut-compile_header-short.sh
)を、適宜名前を変えてコピーする。この際に新しいファイル名の拡張子(サフィックス)を、記述言語のデフォルト拡張子(たとえば.c,.ccなど)+.sh
にする。 デフォルト拡張子としては、GCC manualに準じている。2つのテンプレートファイル(cornercut-compile_header.sh
とcornercut-compile_header-short.sh
)の違いは、冗長なコメントがついているかいないかだけで、動作に違いはない。
% cp -ai 'cornercut-compile_header.sh' 'example.cc.sh'
もしくは
% cp -ai 'cornercut-compile_header-short.sh' 'example.cc.sh'
- (オプション)コピーしたファイルの2行目のコメント行を、テキストエディタで記述言語に合わせたモード(とくに
emacs
のメジャーモード)になるように修正する。(これがないと、emacs
ではファイルネームからshell-script-mode
になってしまうので、後のソースコードの部分の編集で不便).3. 必要に応じて、コピーしたファイルのシェル変数を編集する。たとえば、コンパイル時にライブラリーファイルのリンクオプション(-l??
)を与える必要がある場合など。 - コピーしたファイルの末尾に実際のソースコードを追記する
- シェルスクリプトを実行する。シェルスクリプトに与えられたコマンドライン引数は、コンパイルされたファイルの実行時にそのまま渡される。
% ./example.cc.sh [arg1] [arg2] ...
コンパイル済みのバイナリファイルを残したい場合には、環境変数keep_bin
を非0値にする。また、もしファイルのコンパイルだけを行ないたい場合には、環境変数autorun
を0に設定する。
% env autorun=0 keep_bin=1 ./example.cc.sh
% ls -l example
もし純粋なソースコードの部分のみを抽出した場合には環境変数keep_src
を非0値にする。
% env autorun=0 keep_src=1 ./example.cc.sh
% ls -l example.cc
もし、コンパイルで生成される実行ファイルと同じ名前のファイルがある場合には、ファイルを上書きしないようにスクリプトの実行は中断される。もしファイルの上書きを許す場合には、環境変数force_build
を非0値にする。
% ls -l example
% env force_build=1 ./example.cc.sh
これらの環境変数は、前述の通りスプリプとのはじめの方でハードコードすることも可能である。
使用例
- スクリプトの使用例(
cornercut-compile-example.sh
)が、前述のレポジトリのexample
ディレクトリにある。この末尾には、C
,C++
,Objective-C
,Objective-C++
,fortran
で書かれた、"Hello world!"とコマンドライン引数のリストを表示するプログラムである。各言語で記述された5つのコードがプリプロセッサで選択されてコンパイルされるようなコードになっている。)
% ls -l example/
-rwxr-xr-x ...... cornercut-compile-example.sh
lrwxr-xr-x ...... cornercut-compile-example.c.sh -> cornercut-compile-example.sh
lrwxr-xr-x ...... cornercut-compile-example.m.sh -> cornercut-compile-example.sh
lrwxr-xr-x ...... cornercut-compile-example.cc.sh -> cornercut-compile-example.sh
lrwxr-xr-x ...... cornercut-compile-example.mm.sh -> cornercut-compile-example.sh
lrwxr-xr-x ...... cornercut-compile-example.F.sh -> cornercut-compile-example.sh
-
*.c.sh
を実行すると、C
言語でコンパイルされて実行される。
% ./example/cornercut-compile-example.c.sh arg1 arg2 arg3
Hello, world! (C)
0 ./example/cornercut-compile-example
1 arg1
2 arg2
3 arg3
-
*.m.sh
を実行すると、Objective-C
言語でコンパイルされて実行される。
% ./example/cornercut-compile-example.m.sh arg1 arg2 arg3
Hello, World! (Objective-C)
0 /.../.../example/cornercut-compile-example
1 arg1
2 arg2
3 arg3
-
*.cc.sh
を実行すると、C++
言語でコンパイルされて実行される。
% ./example/cornercut-compile-example.cc.sh arg1 arg2 arg3
Hello, world! (C++)
0 ./example/cornercut-compile-example
1 arg1
2 arg2
3 arg3
-
*.mm.sh
を実行すると、Objective-C++
言語でコンパイルされて実行される。
% ./example/cornercut-compile-example.mm.sh arg1 arg2 arg3
Hello, World! (Objective-C++)
0 /.../.../example/cornercut-compile-example
1 arg1
2 arg2
3 arg3
-
*.F.sh
を実行すると、Fortran
言語でコンパイルされて実行される。
% ./example/cornercut-compile-example.F.sh arg1 arg2 arg3
Hello, world! (Fortran)
0 ./example/cornercut-compile-example
1 arg1
2 arg2
3 arg3
- ファイルの拡張子から記述言語が判定できない場合には
C++
言語でコンパイルされ、実行ファイル名はa.out
が使われる。
% ./example/cornercut-compile-example.sh arg1 arg2 arg3
[Warning: cornercut-compile-example.sh] Can not identify language. C++ is assumed
Hello, world! (C++)
0 ./example/a.out
1 arg1
2 arg2
3 arg3
前節のスクリプトファイルの中身
#!/bin/bash
# /* -*- mode: C++ ; coding: utf-8 ; truncate-lines: t -*- */
# /* <-- change mode string manually: C | C++ | Objc | Fortran | f90 | Shell-script */
# /*
# cornercut-compile: Nanigashi Uji (53845049+nanigashi-uji@users.noreply.github.com)
#
this="${0}"; this_bn="$(basename "${this}")"
#
# Variables for compiler options
# (usual Environmental variables also work: CC, CFLAGS, CPPFLAGS, CXX, CXXFLAGS, FC, FFLAGS, LDFLAGS, LDLIBS)
#
# Compilier options: Adding include path, macro options, compiler warning options and so on.
cmpflgs_add='-I'"$(dirname "${this}")"
#
# Linker options: Adding library path, required library file and so on.
ldflgs_add='-Wl,-rpath,'"$(dirname "${this}")"' -Wl,-L,'"$(dirname "${this}")"
ldlibs_add=
# Example: ldlibs_add='-lm'
#
# Variables to change this script behavior: Change it below or Give it as envriomental variable, if necessary
# autorun=1 keep_bin=0 keep_src=0 force_build=0 (default) : Compile code for each run and clean up. Stop compiling if the binary is already exists.
# autorun=0 keep_bin=1 : Compile code only. do not execute it.
# autorun=0 keep_src=1 : Check code compile and generate pure source code if succeeded.
#
# autorun : 0 = compile only (useful to use with keep_bin=1 and/or keep_src=1)
# 1 = execute the program with given commandline arguments after compile succeeded (default)
#autorun=1
#
# keep_bin : 0 = remove the executable binary file (i.e. compiler output) (default)
# 1 = keep the executable binary file (i.e. compiler output)
#keep_bin=0
#
# keep_src : 0 = Do not generate the pure source code file (i.e. compiler input) (default)
# 1 = Generate the pure source code file (i.e. compiler input)
#keep_src=0
#
# force_rebuild : 0 = Do not override the executable binary file. (If it already exists, this script will be aborted.) (Default.)
# 1 = Do not check if the executable binary is already exists. (If it already exists, it will be overriden.)
#force_rebuild=0
#
# Default compiler determination
which "clang" 1>/dev/null 2>&1 && dfltcc="clang" || dfltcc="gcc"
which "clang++" 1>/dev/null 2>&1 && dfltcxx="clang++" || dfltcxx="g++"
dfltfc="gfortran"
#
[ "${this_bn}" != "${this_bn%.*.*}" ] && exectbl="${this_bn%.*.*}" ; exectbl="$(dirname "${this}")/${exectbl:-a.out}"
[ "${this_bn}" != "${this_bn%.*}" ] && src="${this_bn%.*}" ; src="$(dirname "${this}")/${src:-${this_bn}.cxx}"
_extglob="$(shopt -p extglob)"; shopt -s extglob
case "${this}" in
*.c.sh) cmplr="${CC:-${dfltcc:-cc}}" ; cmplr_flg="${CFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="c" ;;
*.i.sh) cmplr="${CC:-${dfltcc:-cc}}" ; cmplr_flg="${CFLAGS}" ; cpp_flg="-fpreprocessed" ; lang="c" ;;
*.m.sh) cmplr="${CC:-${dfltcc:-cc}}" ; cmplr_flg="${CFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="objective-c" ; ldlibs_add="-framework Foundation ${ldlibs_add}" ;;
*.mi.sh) cmplr="${CC:-${dfltcc:-cc}}" ; cmplr_flg="${CFLAGS}" ; cpp_flg="-fpreprocessed" ; lang="objective-c" ; ldlibs_add="-framework Foundation ${ldlibs_add}" ;;
*.@(mm|M).sh) cmplr="${CXX:-${dfltcxx:-c++}}" ; cmplr_flg="${CXXFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="objective-c++" ; ldlibs_add="-framework Foundation ${ldlibs_add}" ;;
*.mii.sh) cmplr="${CXX:-${dfltcxx:-c++}}" ; cmplr_flg="${CXXFLAGS}" ; cpp_flg="-fpreprocessed" ; lang="objective-c++" ; ldlibs_add="-framework Foundation ${ldlibs_add}" ;;
*.ii.sh) cmplr="${CXX:-${dfltcxx:-c++}}" ; cmplr_flg="${CXXFLAGS}" ; cpp_flg="-fpreprocessed" ; lang="c++" ;;
*.@(cc|cp|cxx|cpp|CPP|c++|C).sh) cmplr="${CXX:-${dfltcxx:-c++}}" ; cmplr_flg="${CXXFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="c++" ;;
*.@(f|for|ftn).sh) cmplr="${FC:-${dfltfc:-gfortran}}" ; cmplr_flg="${FFLAGS}" ; cpp_flg="-fpreprocessed" ; lang="f77" ldlibs_add="-lgfortran ${ldlibs_add}";;
*.@(F|FOR|fpp|FPP|FTN).sh) cmplr="${FC:-${dfltfc:-gfortran}}" ; cmplr_flg="${FFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="f77-cpp-input" ldlibs_add="-lgfortran ${ldlibs_add}";;
*.@(f90|f95|f03|f08).sh) cmplr="${FC:-${dfltfc:-gfortran}}" ; cmplr_flg="${FFLAGS}" ; cpp_flg="-fpreprocessed" ; lang="f95" ldlibs_add="-lgfortran ${ldlibs_add}";;
*.@(F90|F95|F03|F08).sh) cmplr="${FC:-${dfltfc:-gfortran}}" ; cmplr_flg="${FFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="f95-cpp-input" ldlibs_add="-lgfortran ${ldlibs_add}";;
*) cmplr="${CXX:-${dfltcxx:-c++}}" ; cmplr_flg="${CXXFLAGS}" ; cpp_flg="${CPPFLAGS}" ; lang="c++" ; echo "[Warning: ${this_bn}] Can not identify language. C++ is assumed" 1>&2 ;;
esac #
if [ -e "${exectbl}" -a ${force_rebuild:-0} -eq 0 ]; then
exec echo "[Error: ${this_bn} ] ${exectbl} is already exist. (Compile is aborted.)" 1>&2
fi
#echo "${cmplr}" -x "${lang}" ${cmplr_flg} ${cpp_flg} ${cmpflgs_add} -o "${exectbl}" ${CCFLAGS} - ${LDFLAGS} ${ldflgs_add} ${LDLIBS} ${ldlibs_add}
sedcmd='/^#line 0 ____FILE____/,$ { s/^(#line +0 +)____FILE____/\1"'"${this_bn%.*}"'"/ ; p ; }'
${SED:-sed} -nE -e "${sedcmd}" "${this}" |
"${cmplr}" -x "${lang}" ${cmplr_flg} ${cpp_flg} ${cmpflgs_add} -o "${exectbl}" ${CCFLAGS} - ${LDFLAGS} ${ldflgs_add} ${LDLIBS} ${ldlibs_add} \
&& { if [ ${autorun:-1} -ne 0 ] ; then "${exectbl}" "${@}" ; fi ; \
if [ ${keep_bin:-0} -eq 0 ] ; then rm "${exectbl}" ; fi ; \
if [ ${keep_src:-0} -ne 0 ] ; then ${SED:-sed} -nE -e "${sedcmd}" "${this}" 1> "${src}" ; fi ; }
exit
# The source code will be implemented the after the line : '^#line 0 ...'
# */
#line 0 ____FILE____
#if defined(__cplusplus) && (__cplusplus != 0)
#if defined(__OBJC__) && (__OBJC__ != 0)
/* Objective-C++ */
#import <Foundation/Foundation.h>
#include <iostream>
int main(){
NSString *buf = @"Hello, World! (Objective-C++)";
NSArray *args = [[NSProcessInfo processInfo] arguments];
std::cout << [buf UTF8String] << std::endl;
for (NSInteger i=0;i<[args count];++i){
NSString *argv = [args objectAtIndex:i];
std::cout << i << " " << [argv UTF8String] << std::endl;
}
return 0;
}
#else /* __OBJC__ */
/* C++ */
#include <iostream>
int main(int argc, char* argv[]) {
std::cout << "Hello, world! (C++)" << std::endl;
for (int i=0;i<argc;++i){
std::cout << i << " " << argv[i] << std::endl;
}
return 0;
}
#endif /* __OBJC__ */
#elif defined(__STDC__) && (__STDC__ != 0) /* __cplusplus */
#if defined(__OBJC__) && (__OBJC__ != 0)
/* Objective-C */
#import <Foundation/Foundation.h>
#include <stdio.h>
int main(){
NSInteger i;
NSString *argv;
NSString *buf = @"Hello, World! (Objective-C)";
NSArray *args = [[NSProcessInfo processInfo] arguments];
printf ("%s\n", [buf UTF8String]);
for (i=0;i<[args count];++i){
argv = [args objectAtIndex:i];
printf("%-2ld %s\n", i, [argv UTF8String]);
}
return 0;
}
#else /* __OBJC__ */
/* C */
#include <stdio.h>
int main(int argc, char* argv[]){
printf ("Hello, world! (C) \n");
for (int i=0;i<argc;++i){
printf("%-2d %s\n", i, argv[i]);
}
return 0;
}
#endif /* __OBJC__ */
#else /* __STDC__ */ /* __cplusplus */
c /* Fortran */
PROGRAM TRIAL
IMPLICIT NONE
INTEGER i
CHARACTER*70 sargv
WRITE (*,*) 'Hello, world! (Fortran)'
DO i=0, iargc()
CALL getarg(i, sargv)
WRITE (*,'(i2,1X,A)') i,sargv
ENDDO
END PROGRAM
#endif /* __STDC__ */ /* __cplusplus */