« ◇rustで時刻処理をするcargo,crateプロジェクト | トップページ | ■イチョウ嵐 »

◇RUSTで標準入力からキーを読む

rustで標準入力から1文字ずつ読み込んで標準出力に出すプログラムを示します。

rustのインストール法については rustコンパイラ・メモ、プロジェクトを作成しライブラリを参照する方法については ◇rustで時刻処理をするcargo,crateプロジェクトを参照してください。

文字(キー入力)が主眼ですが、関連するいくつかの要素の説明ともなっています。

項番内容
01コンパイル時の無意味なwarningの抑止:#![allow(xxx)]
02利用する参照ネストをuseで指定し、後の記述を短くする
03クレート(ライブラリ)の参照
04配列定数の宣言と初期値の与え方
05構造体の定義
06構造体に置く参照体のライフタイム指定
07標準出力の汎用型
08配列の添え字数値の型:usize
09関数の引数に構造体の参照を使う
10文字のVectorをStringにする
11文字列⇒数値変換
12数値⇒日付時刻文字列変換
13出力先指定の文字出力
14配列のクリア
15Result内容取り出し:unwrap()
16Keyの種別判断とコード取り出し:match
17backspaceキー:Ctrl('h')
18backspace動作
19ResultのOk/Errチェック:match
20クラス化する

 コード

コードを示します。

関数executeが一文字ずつ読み込む繰り返し処理を行っています。
バックスペースに関する処理も入れてあります。
tty出力では"16"から始まる10桁の10進数を日付時刻に変換して出力します。16から始まる数値列は一旦内部に貯められるため数値終了まで画面に応答されません。16以外場合は変換対象とならず画面に応答されます。
ファイル出力の場合は10桁の数値であれば制限なく日付変換するようにしています。

#![allow(non_snake_case)]             // 01
#![allow(unused_must_use)]
#![allow(unused_variables)]
#![allow(bare_trait_objects)]
use std::io::{stdin, stdout};         // 02
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;
extern crate chrono;                  // 03
use chrono::prelude::*;

// 時刻数値開始パターン"16":2020-09-13 21:26:40~2023-11-15 07:13:19
counted_array!(pub const TOP: [char; _] = ['1','6']); // 04
// 状態を保持する
struct Values<'a> {                  // 05,06
   stdout   : &'a mut std::io::Write,// 06,07
   numbers  : &'a mut Vec<char>,
   top_idx  : usize,                 // 08
   top_len  : usize,
   prev_num : bool,
   }
// 数値文字列の並びを時刻文字列にして出力
fn printNumber(values_:&mut Values){ // 09
   if values_.numbers.len() > 0 {
      let _num: String = values_.numbers.iter().collect(); // 10
      if values_.numbers.len()==10 {
         let _timestamp:i64 = _num.parse::<i64>().unwrap(); // 11
         let _naive:NaiveDateTime
             = NaiveDateTime::from_timestamp(_timestamp+9*3600,0);// 12
         write!(values_.stdout,"{}",_naive); // 13
         }
      else{
         write!(values_.stdout,"{}",_num);
         }
      }
   values_.numbers.clear(); // 14
   }
// 1文字を受け、特定数値の並びを配列に追加、それ以外を出力
fn charEvent(c_:char,values_:&mut Values){
   let mut _is_num:bool=false;
   if c_ >= '0' && c_ <= '9'{
      if !values_.prev_num || values_.top_idx!=0 {
         // 前が数値ではない または 数値取得中
         if values_.top_idx<values_.top_len {
            if TOP[values_.top_idx] == c_ { _is_num=true; }
            }
         else if values_.top_idx<=10 { _is_num=true; }
         }
      values_.prev_num=true;
      if _is_num {
         values_.numbers.push(c_);
         values_.top_idx=values_.top_idx+1;
         }
      }
   else { values_.prev_num=false; }
   if !_is_num {
      printNumber(values_);
      write!(values_.stdout,"{}",c_);
      values_.stdout.flush();
      values_.top_idx=0;
      }
   }
// 標準入力から1文字ずつ読み込み、文字/バックスペース/終了処理を行う
fn execute(values_:&mut Values){
   let _stdin = stdin();
   for _result_ in _stdin.keys() {
      let _c:termion::event::Key=_result_.unwrap(); //15 
      match _c { // 16
         Key::Ctrl('k') | Key::Ctrl('c') | Key::Ctrl('d') => break,
         Key::Backspace | Key::Ctrl('h') => { // 17
            write!(values_.stdout,"{}{}"
                                 ,termion::cursor::Left(1) // 18
                                 ,termion::clear::UntilNewline );
            values_.stdout.flush();
            },
         Key::Char(_c) => { charEvent(_c,values_) }
            _ => (),
         }
      }
   printNumber(values_);
   }
// raw_mode出力用にstdoutを参照し、処理を実行する
fn to_raw(){
   let mut _stdout=stdout().into_raw_mode().unwrap();
   let mut _numbers:Vec<char>=Vec::new();
   let mut _values= Values{ stdout:&mut _stdout,numbers:&mut _numbers,
                            top_idx:0,prev_num:false,top_len:TOP.len() };
   execute(&mut _values);
   }
// file出力用にstdoutを参照し、処理を実行する
fn to_file(){
   let mut _stdout=stdout();
   let mut _numbers:Vec<char>=Vec::new();
   let mut _values= Values{ stdout:&mut _stdout,numbers:&mut _numbers,
                            top_idx:0,prev_num:false,top_len:0 };
   execute(&mut _values);
   }
// stdouがraw_modeが可能かどうかをチェックし、処理を呼び分ける
fn main() {
   let _r_  = stdout().into_raw_mode();
   match _r_ { // 19
      Ok(_) => to_raw(),
      Err(_)=> to_file(),
      };
   }

Cargo.tmlは次のものです

[package]
name = "u2t"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
termion = "1.5.2"
chrono = "0.4"
counted-array = "0.1.2"

 注意点

 初期値を持つ配列はマクロcounted_array!を使う

信じられないことに初期値を持つ配列を定義する時、初期値の長さが配列の長さとなることはありません。

const TOP:[char;2]=['1','6']; // ['1','6']では配列長を示せない

rust標準の形式は用いるべきではなく、counted_array!マクロを使用します。

#[macro_use] extern crate counted_array;
counted_array!(pub const TOP: [char; _] = ['1','6']);
--- Cargo.toml
[dependencies]
counted-array = "0.1.2"

大昔のFORTRANでは文字列を表すのに長さを明示することが必要で、"abc"を表すには 3habc と書かなければなりませんでした。
rustの仕様はタイムマシンで古代世界に戻った感じです。

 出力先がファイルの場合into_raw_mode()エラーとなる

出力先が端末の場合はinto_raw_mode()を用いることができますが、ファイルの場合エラーとなります。

次のコードでは、一旦into_raw_mode()を動かし、正常に動く場合と、異常になる場合で処理を分けています。
なお、正常の場合Ok(_raw_io_)などで受け、_raw_io_を利用したかったのですが、残念ながら関数に引き渡すことができず、改めて関数内でinto_raw_mode()呼んでいます。

fn main() {
   let _r_  = stdout().into_raw_mode();
   match _r_ { // 19
      Ok(_) => to_raw(),
      Err(_)=> to_file(),
      };
   }

 match手続きは異なる型を返せない

次のコードは

   let _sdtio = match _r_ {
      Ok(_raw_io_) => _raw_io_,
      Err(_)       => stdout(),
      };

`match` arms have incompatible typesというエラーとなります。
* _raw_io_はfound to be of type `RawTerminal`
* stdout()はexpected struct `RawTerminal`, found struct `Stdout`
となります。

受け取る変数の問題かとlet _stdioに色々型情報を付けてみましたがだめでした。

Valuesのstdout要素初期値設定ではどちらの型も可能ですので、Valuesの初期値として与えてみましたが、だめでした。
matchが異なる型を許さないようです。

   let mut _numbers:Vec=Vec::new();
   let mut _values= Values{ 
      stdout:&mut match _r_ {
         Ok(_raw_io_) => _raw_io_,
         Err(_)       => stdout(),
         },
      numbers:&mut _numbers,
      top_idx:0,prev_num:false
      };
error[E0308]: `match` arms have incompatible types

castを試みましたがエラーとなります。

   let _sdtio = match _r_ {
      Ok(_raw_io_) => _raw_io_ as std::io::Write,
      Err(_)       => stdout() as std::io::Write,
      };
error[E0620]: cast to unsized type: `RawTerminal` as `dyn std::io::Write`

 stdinのバックスペース

キー入力はtermion::event::Keyデータで得られます。

バックスペースはKey::Backspaceとなることになっていますが、Key::Ctrl('h')でしか判断できませんでした。

また、キー操作のある別コマンドをパイプで繋いだ場合はbackspaceキーイベントが得られず、1文字削除された行文字が得られます。

  # u2tが本プログラム、mongoはキー操作のあるコマンド、
  # mongoの出力の内unixEpochのみ時刻表示しようとした
  $ mongo | u2t
  ざんねんながらbackspaceはうまく表示に反映されない

 stdoutのバックスペース

次の処理はcursor_pos()がコンパイラを通りません。

   let (x, y) = values_.stdout.cursor_pos().unwrap();
   write!(values_.stdout,"{}{}"
                        , termion::cursor::Goto(x-1, y)
                        , termion::clear::UntilNewline );

次のコードで出力の1文字を戻せます。

    write!(values_.stdout,"{}{}"
                         ,termion::cursor::Left(1)
                         ,termion::clear::UntilNewline );
    values_.stdout.flush();

 unrwap()によるioエラーpanic

io関数はResultを返します。エラーの場合も同じです。Resultに対しunwrap()を呼ぶとエラー時にパニックを起こさせプログラムを終了させることができます。

         write!(values_.stdout,"{}",_naive).unwrap();

ネットでみる多くのサンプルがunwrap()を呼んでいますが、推奨されているものかどうか分かりません。本記事では省略しました。

 ソースのダウンロード

次のリンクでソースセットをダウンロードできます。
u2t.zip

内容は次のものです。

u2t
|-- A00_clear.sh
|-- A01_build.sh
|-- A05_test.sh
|-- Cargo.lock
|-- Cargo.toml
|-- reference.txt
`-- src
    `-- main.rs

 クラス風にする(参考)//20

structに関連関数を定義しクラス風の形にできます。

// クラス名がU2T,lifetimeをaとして
struct U2T<'a> {
   // なんだかんだ インスタンス変数
   }
impl<'a> U2T<'a> {
   // なんだかんだクラス定数とメソッド
   }

メソッドの引数にはmut&selfが必要でこれにstructが入ってきます。
変数にはself.変数名でアクセスできます。
クラス定数へのアクセスはSelf::定数名です。selfではなくSelfである点に注意が必要です。

先のプログラムをクラス化したものを示します。

|

« ◇rustで時刻処理をするcargo,crateプロジェクト | トップページ | ■イチョウ嵐 »