9
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 5 years have passed since last update.

おにぎり万太郎Advent Calendar 2019

Day 14

slコマンドのソースコードリーディング

Posted at

#slコマンド
みなさん、slコマンドはご存知ですか?
ファイルやディレクトリの情報を示すlsコマンドに対するジョークコマンドで、lsを焦ってslと打ってしまった場合に、落ち着きなさいよ、という意味合いでターミナル上にslが走る、という大変愉快なコマンドです。
#インストール
macを対象にします

$ brew install sl
$ alias ls='sl -aF'

これでslと打つと画面上にslが走り出します。

今回はそんなslコマンドのソースを読んでみようと思います。
参照したリポジトリはこちらです。

#ソースを読む
##sl.c
とりあえずmainから

sl.c
int main(int argc, char *argv[])
{
    int x, i;

    for (i = 1; i < argc; ++i) {
        if (*argv[i] == '-') {
            option(argv[i] + 1);
        }
    }
    ...
}

C言語におけるmain関数の引数のひとつargcは引数の個数を示しています。そして、argvは引数の文字列を示すポインタの配列になっています。

test.c
include <stdio.h>
int main(int argc, char* argv[]) {
  int i;
  for( i = 0; i < argc; i++ ) {
    printf("%d番目の引数: %s\n", i, argv[i]);
  }
}

これをコンパイルして実行します。

.shell
$ gcc -o test test.c
$ ./test a b c
0番目の引数: ./test
1番目の引数: a
2番目の引数: b
3番目の引数: c

argv[0]に実行したファイル名、argv[1]から順に渡した引数が入力されていることが理解できます。

ソースに戻ります。

sl.c
int main(int argc, char *argv[])
{
    int x, i;

    for (i = 1; i < argc; ++i) {
        if (*argv[i] == '-') {
            option(argv[i] + 1);
        }
    }
    ...
}

もう理解できるはずです。
つまり、引数の個数だけforで回しつつ、ハイフン(-)があれば、その次の文字(のポインタ)をoptionへ渡していますね。
argvは引数の文字列のポインタなので、*がついていることが要注意です。

optionの中は、以下のようになっていて、オプションに対応するフラグを立てていることがわかります。

sl.c
void option(char *str)
{
    extern int ACCIDENT, LOGO, FLY, C51;

    while (*str != '\0') {
        switch (*str++) {
            case 'a': ACCIDENT = 1; break;
            case 'F': FLY      = 1; break;
            case 'l': LOGO     = 1; break;
            case 'c': C51      = 1; break;
            default:                break;
        }
    }
}

ではもう少し進みます。

sl.c
int main(int argc, char *argv[]) {
    ...

    initscr();
    signal(SIGINT, SIG_IGN);
    noecho();
    curs_set(0);
    nodelay(stdscr, TRUE);
    leaveok(stdscr, TRUE);
    scrollok(stdscr, FALSE);

    ...
}

なるほど・・・?:thinking: :thinking: :thinking:

落ち着いて1行ずつ読んでいきましょう。

initscr()をソースコードでgrepすると定義元が見つからないので、外部からincludeした関数だということがわかります。調べると、文字に色を付けたり、画面上の文字列を動かすことなどに使われるncursesというライブラリに定義されている、端末の初期化を行う関数だということがわかりました。
ncursescurses.hとして取り込まれることで利用可能となる関数で、実際にこれがincludeされていることを確認できました。

signalは、1つ目の引数にシグナルの番号、2つめの引数にその番号へのアクションを渡します。
SIGINTはキーボードからの割り込みを管理する番号を指しています。よく、コマンドをCtrl+Cで中断するかと思いますが、あのときに発せられているシグナルがSIGINTです。(SIGnal INTerrupt の略)
SIG_IGNは、その番号へのシグナルを無視する、というアクションです。(SIGnal IGNore)
つまり、slコマンドの入力中にキーボードからの入力を無視してね、という命令を与えているのです。

その他、挙動をコメントとしてまとめました。noechoからscrollokまですべてncursesの関数です。

sl.c
int main(int argc, char* argv[]) {
    ...
    # 端末の初期化
    initscr();
    # キーボード割り込みを無効化
    signal(SIGINT, SIG_IGN);
    # キーボードから入力された文字をターミナルへ表示されないようにする
    noecho();
    # ターミナル中のカーソルの非表示
    curs_set(0);
    # ターミナルへの入力されるバッファが空の場合にエラーが返るように設定
    nodelay(stdscr, TRUE);
    # ターミナル中のカーソルの非表示(なんで2回目?)
    leaveok(stdscr, TRUE);
    # ターミナルのスクロールを無効化
    scrollok(stdscr, FALSE);
    ...
}

コメントも合わせて読めば、なんとなくイメージが湧くと思います。
curs_set(0)leaveokで2回カーソルの非表示を関数として呼んでいるのかはよくわかりませんでした。。

読み進めましょう。

sl.c
int main(int argc, char* argv[]) {
    ...
    for (x = COLS - 1; ; --x) {
        if (LOGO == 1) {
            if (add_sl(x) == ERR) break;
        }
        else if (C51 == 1) {
            if (add_C51(x) == ERR) break;
        }
        else {
            if (add_D51(x) == ERR) break;
        }
        getch();
        refresh();
        usleep(40000);
    }
    mvcur(0, COLS - 1, LINES - 1, 0);
    endwin();

    return 0;

}

とりあえず最後まで来ました。
COLSという定数はncureseが提供する定数で、initscr()することで起動しているターミナルのウィンドウサイズ(横幅)を保持しています。縦幅はLINESが持っていて、これも後ほど利用します。
COLSに対してforで回しているので、ターミナルの横幅を走査する形でadd_*という関数を走らせて、SLの描画をしているんだということが雰囲気でわかります。

returnまで読んでしまいましょう。
getch()はキーボードから入力された1文字をそのまま返す関数らしいですが、なぜここで呼ばれてるのかについてはこれも分かりませんでした。コメントアウトしてコンパイルして実行しても問題なく動いてるっぽいです。誰か教えて下さい。

refresh()することで画面が描画されるようです。つまりadd_()メソッドで、端末の各アドレスにsl情報を書き込んでおいて、それをrefresh()することで描写している、ということです。

肝心のslを描画しているadd_*についてですが、ここはぜひみなさんの目でコードを見てみてください。基本的には引数として渡している、ターミナルのx座標の情報を使って、ヘッダファイルに定義されているSLの文字列を呼び出して各アドレスに書き込んでいるって感じです。書き込んだ情報はrefresh()によってターミナルへ描画されます。

( 決して説明が面倒になったわけではないです )

まとめ

sl.c
int main(int argc, char *argv[])
{
    int x, i;

    // オプションの解釈とフラグを立てるところ
    for (i = 1; i < argc; ++i) {
        if (*argv[i] == '-') {
            option(argv[i] + 1);
        }
    }
    // 初期処理

    // 端末の初期化
    initscr();
    // キーボード割り込みを無効化
    signal(SIGINT, SIG_IGN);
    // キーボードから入力された文字をターミナルへ表示されないようにする
    noecho();
    // ターミナル中のカーソルの非表示
    curs_set(0);
    // ターミナルへの入力されるバッファが空の場合にエラーが返るように設定
    nodelay(stdscr, TRUE);
    // ターミナル中のカーソルの非表示(なんで2回目?)
    leaveok(stdscr, TRUE);
    // ターミナルのスクロールを無効化
    scrollok(stdscr, FALSE);

    // ターミナルのアドレスからオプションに対応したSL情報を各アドレスに書き込む
    for (x = COLS-1; ; --x) {
        if (LOGO == 1) {
            if (add_sl(x) == ERR) break;
        }
        else if (C51 == 1) {
            if (add_C51(x) == ERR) break;
        }
        else {
            if (add_D51(x) == ERR) break;
        }
        // 謎い
        getch();
        // 描画する
        refresh();
        // 寝かす
        usleep(40000);
    }
    // 謎い
    // 終了前にadd_*関数で移動させたアドレスを元に戻す、てきな?
    mvcur(0, COLS - 1, LINES - 1, 0);
    // 終了処理(メモリの解放とかとか)
    endwin();

    return 0;
}

感想

やっぱりC言語は高級言語。

9
1
0

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
9
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?