Help us understand the problem. What is going on with this article?

Cでプロンプト機能を実装する

More than 3 years have passed since last update.

はじめに

概要

LinuxのTerminalの機能をCで実装する方法を解説します。
正確にはTerminal上でTerminalのような動作をする(真似る?)Cプログラムの解説です。

コマンド入力を受け付ける事を示す > とかが表示されて
Enterを押すと改行して入力受付の表示(プロンプト)が続いていくようなイメージです。
↓↓↓

Terminal > (Enter)
Terminal > (Enter)
Terminal > (Enter)
   :

ざっくり以下の流れで実装しています。
 1. 矢印キーの画面非表示
 2. エコー機能の実装
 3. 強制終了の回避

目的

以下のような用途に使える(かもしれない)。

  • コマンド処理を行うプログラムを開発する際のシミュレータ (exp.Linuxサーバ、NW機器)
  • LinuxでCLI機能を持ったアプライアンス的なものを開発する場合

矢印キーの画面非表示

特に何も設定などしていない場合、Terminal上でプログラムを動作させている最中に
矢印キーを押下すると^[[Aだったり^[[Bが表示されるはずです。

[user@localhost home]# php sleep.php 
^[[A^[[B^[[A^[[D^[[C

これはTerminal側がユーザの入力を全てエコー表示する設定になっており、
矢印キーも同様にエコー表示されるからだと思います。
但し、矢印キーは特殊文字なのでよく読めない表示になってしまいます。

エコー表示はコマンド受付機能を実装する場合には都合良いですが、特殊文字はエコーの除外としたいですね。
ターミナル属性をいじる事で設定変更できそうですが、
特殊文字だけ除外する都合の良い設定方法はよくわかりませんでした。
そこでエコー表示そのものを廃止する設定を以下で行っています。

注意:以下のプログラムを実行すると以降エコー表示が無効化されTerminalの表示が崩れます。
(再ログインで元に戻ります)

disable_echo.c
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>

int main (int argc, char *argv[]) {
    struct termios io_conf;

    memset(&io_conf, 0x00, sizeof(struct termios));

    tcgetattr(0, &io_conf);
    io_conf.c_lflag &= ~(ECHO);  // disabled echo
    io_conf.c_cc[VMIN]  = 0;
    io_conf.c_cc[VTIME] = 1;
    tcsetattr( 0 , TCSAFLUSH , &io_conf );

    return 0;
}

エコー機能の実装

さて、エコー表示を無効化してしまいましたので、コマンド入力時に不便な仕様となってしまいます。
パスワード入力時のように入力した文字が表示されない動作をしてしまいます。

ですのでエコー表示機能はCプログラム側で独自実装する事になります。
方法としてはユーザが入力した文字(特殊文字含む)をプログラム側で取得し
エコーするかを判別して(エコーが必要なら)エコー表示します。

ですが、ちょっとわがままを言うとそれだけではなくて、
上下キーを押下してコマンドの履歴を表示したりTabキーを押下してコマンド補完をやりたいですね。
そのためにはユーザが何かしらキーを入力したと同時にキーの内容を取得する必要がありますが、
Terminal上の入力文字取得はデフォルトではEnterが押されてからでないと
プログラム側で入力値を取得できないみたいです(行単位の取得)。
これだと上下キーを押下し、さらにEnterを押してからでないとコマンド履歴機能を実装できず不便ですね。

カノニカルモードの廃止

ユーザ入力に関する設定はカノニカルモードで設定が可能です。
このモードを廃止する事でリアルタイムに入力キーの取得が可能になります。
以下のコードではカノニカルモードを廃止しています。

disable_canon.c
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>

int main (int argc, char *argv[]) {
    struct termios io_conf;

    memset(&io_conf, 0x00, sizeof(struct termios));

    tcgetattr(0, &io_conf);
    io_conf.c_lflag &= ~(ICANON);  // disabled canonical
    io_conf.c_cc[VMIN]  = 0;
    io_conf.c_cc[VTIME] = 1;
    tcsetattr( 0 , TCSAFLUSH , &io_conf );

    return 0;
}

カーソル移動

ここで言うエコー表示とははユーザーがキーを押下した時にTerminal上に同じ内容を表示させる事を指します。
但し矢印キーが押下された場合はそれに応じた適切な処理を行わなければなりません。
以下に各矢印キーが押下された際の動作内容を示します。

方向キー 取得文字列 動作内容
ESC[1A コマンドの履歴を表示
(現在表示しているコマンドから1つ前を表示)
ESC[1B コマンドの履歴を表示
(現在表示しているコマンドから1つ後を表示)
ESC[1C カーソルを1文字分右に移動
(但し既に表示しているコマンドの最後尾まで移動している場合を除く)
ESC[1D カーソルを1文字分左に移動
(但し既にプロンプトの手前まで移動している場合を除く)

コマンド履歴は今回は割愛します。
カーソル移動の方法は画面に特殊文字を出力するだけです。

cursor_sample.c
printf("\x1b[1C");  // shifts right by 1 character
printf("\x1b[1D");  // shifts left by 1 character

その他の特殊文字

ちなみに矢印キー以外の特殊文字としては以下があリます。

種別 文字列 意味
文字色 ESC[31m
ESC[32m
ESC[33m
ESC[34m
ESC[39m




元に戻す(文字カラー終了)
その他 ESC[1m
ESC[4m
ESC[0m
太字
下線
元に戻す(文字装飾終了)

赤い文字を表示する例は以下になります。

color_sample.c
printf( "¥x1b[31mRED\x1b[39m" );

エコー機能

(途中)
カーソル移動なら移動、Enter押下ならプロンプト表示、そうでないならエコー表示
的な事をやっています(多分..)。

echo.c
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>

int main( void ){
  char key    = 0;
  int ctl_cnt = 0;
  unsigned int cur_pos   = 0;
  unsigned int max_pos   = 0;
  char *pmp_str = "command > ";

  cur_pos = strlen( pmp_str );
  max_pos = cur_pos;
  printf( "%s" , pmp_str );

  while(1){
    key = fgetc( stdin );
    if( -1 == key ){
      continue;
    }
    if( 0 != ctl_cnt ){
      ctl_cnt++;
      if( ('A' == key) || ('B' == key) ){
        ctl_cnt = 0;
      }else if( 'C' == key ){
        // right
        if( max_pos > cur_pos ){
          printf( "\x1b[1C" );
          cur_pos++;
        }
        ctl_cnt = 0;
      }else if( 'D' == key ){
        // left
        if( cur_pos > strlen( pmp_str ) ){
          printf( "\x1b[1D" );
          cur_pos--;
        }
        ctl_cnt = 0;
      }
      continue;
    }
    switch( key ){
      case 0x00:
        break;
      case 0x1b:
        ctl_cnt = 1;
        break;
      case 0x0a:
        printf( "\n%s" , pmp_str );
        cur_pos = strlen( pmp_str );
        max_pos = cur_pos;
        break;
      case 0x08:
        if( cur_pos > strlen( pmp_str ) ){
          printf( "%c \b" , key );
          cur_pos--;
        }
        break;
      default:
        printf( "%c" , key );
        max_pos++;
        cur_pos++;
        break;
    }
  }

  return 0;
}

強制終了の回避

エコー表示機能が動作している最中にCtrl+Cなどが押下された時にプログラムが終了し、
Terminalのプロンプトに戻ってしまいます。これを回避するためには

出来るだけ終了させないようにする

を参考頂けるとうまく回避できるはずです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away