0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

1ファイルにC/C++/Objective-C/Fortranのソースコードとコンパイル手順を記述して直接実行可能なシェルスクリプトに

Last updated at Posted at 2023-01-24

動機

ちょっとしたコードの試し書きや小さいプログラムを使い回す際に、環境によってコンパイラの違いなどを気にしてコマンドラインでオプションをつけて叩くのは面倒である。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での編集で言語に応じてモードが選択されるようにしたい。(ただし、これは手動で書き換える必要がある。)

ファイル置き場

使い方(手順)

  1. シェルスクリプトのテンプレート(cornercut-compile_header.sh or cornercut-compile_header-short.sh)を、適宜名前を変えてコピーする。この際に新しいファイル名の拡張子(サフィックス)を、記述言語のデフォルト拡張子(たとえば.c,.ccなど)+.shする。 デフォルト拡張子としては、GCC manualに準じている。2つのテンプレートファイル(cornercut-compile_header.shcornercut-compile_header-short.sh)の違いは、冗長なコメントがついているかいないかだけで、動作に違いはない。
実行例1
% cp -ai 'cornercut-compile_header.sh' 'example.cc.sh'

もしくは

実行例2
% cp -ai 'cornercut-compile_header-short.sh' 'example.cc.sh'
  1. (オプション)コピーしたファイルの2行目のコメント行を、テキストエディタで記述言語に合わせたモード(とくにemacsのメジャーモード)になるように修正する。(これがないと、emacsではファイルネームからshell-script-modeになってしまうので、後のソースコードの部分の編集で不便).3. 必要に応じて、コピーしたファイルのシェル変数を編集する。たとえば、コンパイル時にライブラリーファイルのリンクオプション(-l??)を与える必要がある場合など。
  2. コピーしたファイルの末尾に実際のソースコードを追記する
  3. シェルスクリプトを実行する。シェルスクリプトに与えられたコマンドライン引数は、コンパイルされたファイルの実行時にそのまま渡される。
実行例
% ./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つのコードがプリプロセッサで選択されてコンパイルされるようなコードになっている。)
サンプルファイル:実体は1つでシンボリックリンクで名前(拡張子)だけが違う。
% 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言語でコンパイルされて実行される。
cornercut-compile-example.c-->cornercut-compile-example
% ./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言語でコンパイルされて実行される。
cornercut-compile-example.m-->cornercut-compile-example
% ./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++言語でコンパイルされて実行される。
cornercut-compile-example.cc-->cornercut-compile-example
% ./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++言語でコンパイルされて実行される。
cornercut-compile-example.mm-->cornercut-compile-example
% ./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言語でコンパイルされて実行される。
cornercut-compile-example.F-->cornercut-compile-example
% ./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が使われる。
(default)-->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

前節のスクリプトファイルの中身

cornercut-compile-example.sh
#!/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 */
0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?