なでしこ3で簡単なcatコマンドを作る

[なでしこ3](https://nadesi.com/doc3/) に入門しています。

```
$ cnako3 -v    
3.2.30
```

新しくプログラミング言語を覚えるとき、入出力や反復などの基本的な機能の下調べも兼ねて、最近はよく cat コマンドを書いています。なでしこでもやってみます。

- 参考: [【 cat 】コマンド――設定ファイルの内容を簡単に確認する：Linux基本コマンドTips（1） - ＠IT](https://atmarkit.itmedia.co.jp/ait/articles/1602/25/news034.html)

ちなみに、なでしこ3は Node.js ベースなので Linux でも使えます。この記事も Docker コンテナ内で動かしながら書いています。

- [ローカルPCでバッチ処理に使う - 日本語プログラミング言語「なでしこ3」](https://nadesi.com/doc3/index.php?%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%ABPC%E3%81%A7%E3%83%90%E3%83%83%E3%83%81%E5%87%A6%E7%90%86%E3%81%AB%E4%BD%BF%E3%81%86)
    - `npm install -g nadesiko3` でインストール

- [Chromebookになでしこをインストールする - Qiita](https://qiita.com/kujirahand/items/2a940c14fd2b5ac0db29)






# 標準出力に出力する

まずは基本ということで。

```
「こんにちは」と表示。
```

これは簡単。チュートリアルの最初に出てきますね。

- [なでしこさん マニュアル - チュートリアル](https://nadesi.com/v3/doc/index.php?%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB&show)






# 標準入力から読む

マニュアルで探して `標準入力取得時` という命令を見つけました。

- [なでしこさん マニュアル - plugin_node/標準入力取得時](https://nadesi.com/v3/doc/index.php?plugin_node%2F%E6%A8%99%E6%BA%96%E5%85%A5%E5%8A%9B%E5%8F%96%E5%BE%97%E6%99%82&show)

使ってみます。

```
# cat1.nako3

標準入力取得時には（行）
    行を表示
ここまで
```

- [なでしこさん マニュアル - 文法/無名関数 / 「…には」構文 / 暗黙的な無名関数の指定](https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E7%84%A1%E5%90%8D%E9%96%A2%E6%95%B0&show)

動かしてみます。

```
$ cat cat1.nako3 | cnako3 cat1.nako3
標準入力取得時には（行）
標準入力取得時には（行）
    行を表示
    行を表示
ここまで
ここまで
$
```

各行が2回表示されていますが、これはインストールしたなでしこに含まれている `src/plugin_node.js` を修正して createInterface のオプションとして `terminal: false` を指定することで解消できました。

```
$ cat cat1.nako3 | cnako3 cat1.nako3 | cat
標準入力取得時には（行）
    行を表示
ここまで
$ 
```

参考:

- https://github.com/kujirahand/nadesiko3/blob/3.2.30/src/plugin_node.js
- [readline duplicates input sent from the console/terminal · Issue #30510 · nodejs/node](https://github.com/nodejs/node/issues/30510)
- [Readline | Node.js v17.2.0 Documentation / readlinePromises.createInterface(options)](https://nodejs.org/api/readline.html#readlinepromisescreateinterfaceoptions)




はい、これで cat コマンドっぽいものができました。簡単ですね。


----

さすがにこれだけだとさびしいので、もうちょっとあれこれやってみます。




# 末尾に改行を付けない版の「表示」が使いたい

＜追記 2022-06-05＞
v3.2.34 で「継続表示」命令が追加されたようです。

- [Comparing 3.2.33...3.2.34 · kujirahand/nadesiko3](https://github.com/kujirahand/nadesiko3/compare/3.2.33...3.2.34)
- [なでしこさん マニュアル - plugin_system/継続表示](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E7%B6%99%E7%B6%9A%E8%A1%A8%E7%A4%BA&show)

＜追記ここまで＞


[なでしこ v1 には「継続表示」という命令があり](https://www.nadesi.com/man/index.php?%E7%B6%99%E7%B6%9A%E8%A1%A8%E7%A4%BA)、末尾に改行を付けずに出力できるようなのですが、v3 では用意されていないようです（v3.2.30 時点）。

cat コマンドの基本の動作としては入力の内容を変えずにそのまま出力したいのですが、「表示」命令だと末尾に改行がない入力が来た場合に内容が変わってしまいます。

そこで、「JSメソッド実行」という命令を利用して「継続表示」関数を作ってみました。

- [なでしこさん マニュアル - plugin_system/JSメソッド実行](https://nadesi.com/v3/doc/index.php?plugin_system%2FJS%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AE%9F%E8%A1%8C&show)

```
●（値を）継続表示とは
    「process.stdout」の「write」を["{値}"]でJSメソッド実行
ここまで

123を継続表示
「継続表示の」を継続表示
「テスト」を継続表示
「です。{LF}」を継続表示
```

```
$ cnako3 sample_keizoku_hyouji.nako3
123継続表示のテストです。
$ 
```

よしよし。ちなみに `stdout` を `stderr` にすれば標準エラー出力に出力できます。

このように、なでしこ自体に命令が用意されていない場合でも JavaScript（Node.js）の機能を使うことで自分でなんとかすることができます。

この例では「JSメソッド実行」を使いましたが、他にも似た命令として「JS実行」などがあります。

- [なでしこさん マニュアル - plugin_system/特殊命令](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E7%89%B9%E6%AE%8A%E5%91%BD%E4%BB%A4&show)


----

余談ですが eval がありますね！

- [なでしこさん マニュアル - plugin_system/ナデシコ](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%83%8A%E3%83%87%E3%82%B7%E3%82%B3&show)

↓のようなことができるそうです。

```
AAA=0
『AAA=123』をナデシコする
AAAを表示
```



# 標準入力から全部読みたい

`標準入力取得時` だと `EOF` （入力ストリームの終端）が検出できないため、「標準入力からとりあえず全部読んで何かしたい」「標準入力から読み終わったタイミングで何かしたい」といったことができないようでした。

なでしこの機能だけで簡単に済ます方法を思いつけなかったので、標準入力からの入力をシェルスクリプトで一時ファイルに出力して、なでしこではその一時ファイルから読む、という方式にしてみました。
ファイル名は `stdin` で決め打ち。

```sh
#!/bin/bash

STDIN_FILE=stdin

if [ -e $STDIN_FILE ]; then
  rm $STDIN_FILE
fi

if [ -p /dev/stdin ]; then
  # 標準入力の内容をすべてファイルに保存
  cat > $STDIN_FILE
fi

cnako3 "$@"
```

これを `cnako3.sh` として保存して、以降は `cnako3` コマンドを置き換える形で

```
$ ls | ./cnako3.sh sample.nako3
```

のように使っていきます。


参考:

https://qiita.com/xtetsuji/items/d9800864c23af47c1443

なでしこ側では普通にファイルを読めばよいですね。

```
# cat2.nako3

●標準入力全部読とは
    もし、「stdin」が存在するならば
        「stdin」を読んで戻す
    違えば
        「標準入力からの読み込みに失敗」のエラー発生
    ここまで
ここまで

標準入力全部読んでテキストに代入
テキストを表示
```

`標準入力全部読んでテキストに代入` したあとは、普通の文字列の処理です。

```
$ cat sample_end_with_newline.txt | ./cnako3.sh cat2.nako3 
UTF-8 テキストファイル
ファイル終端の改行あり
行末にスペース→  

$ 
```





# 改造してみる

cat コマンドなんか作って何が面白いんや？ と思われるかもしれませんが、 cat ができるとそれを改造していろいろ遊べます。

<!--
し、痒いところに手が届く自分用の実用的なツールが生まれることもたまにあります
し、痒いところに手が届く自分用の実用的なツールが生まれることもあります
-->


## LF を "＜改行＞" + LF に置換してファイル終端も表示

```
# cat3.nako3
# 継続表示、標準入力全部読 は同じなので省略

標準入力全部読んでテキストに代入

テキストのLFを「＜改行＞{LF}」に置換して継続表示
「＜ファイル終端＞」を表示
```

入力の最後に改行がある場合:

```
$ cat sample_end_with_newline.txt | ./cnako3.sh cat3.nako3 
UTF-8 テキストファイル＜改行＞
ファイルの最後に改行あり＜改行＞
行末にスペース→  ＜改行＞
＜ファイル終端＞
$ 
```

入力の最後に改行がない場合:

```
$ cat sample_end_without_newline.txt | ./cnako3.sh cat3.nako3 
UTF-8 テキストファイル＜改行＞
ファイルの最後に改行なし＜改行＞
行末にスペース→  ＜ファイル終端＞
$ 
```

これで行末の余分なスペースを見つけやすくなりますね！
`cat --show-all` みたいな感じ。
最後に `＜ファイル終端＞` を付けて、最後の改行やスペースがあるかどうかも分かりやすくしてみました。


- [なでしこさん マニュアル - plugin_system/置換](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E7%BD%AE%E6%8F%9B&show)
- [なでしこさん マニュアル - plugin_system/システム定数](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%AE%9A%E6%95%B0&show)
    - `LF` はシステム定数として定義されています




## 行番号を表示

`cat -n` みたいなの。

```
# cat4.nako3
# 継続表示、標準入力全部読 は同じなので省略

標準入力全部読んでテキストに代入

テキストをLFで区切って行リストに代入
行数 = 行リストの配列要素数

行番号を1から（行数）まで繰り返す
　　「{行番号}: {行リスト@(行番号 - 1)}」を表示
ここまで
```

```
$ cat sample_end_with_newline.txt | ./cnako3.sh cat4.nako3 
1: UTF-8 テキストファイル
2: ファイルの最後に改行あり
3: 行末にスペース→  
4: 
$ 
```

行番号が分かってべんり！

- [なでしこさん マニュアル - 文法/配列変数](https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E9%85%8D%E5%88%97%E5%A4%89%E6%95%B0&show)



## 1文字ずつ処理

1文字ずつ処理するバージョンも書いてみました。

```
# cat5.nako3
# 継続表示、標準入力全部読 は同じなので省略

●（テキストから位置の）文字取得
    文字配列 = テキストを文字列分解
    文字配列@位置を戻す
ここまで

標準入力全部読んでテキストに代入

テキスト文字数はテキストの文字数
位置は0
位置が（テキスト文字数 - 1）以下の間繰り返す
    テキストから位置の文字取得して文字に代入
    文字を継続表示
    位置 = 位置 + 1
ここまで
```

「文字取得」関数を呼び出すたびに文字列分解していて効率悪いですが、おためしなので気にしない。


```
$ cat sample_end_with_newline.txt | ./cnako3.sh cat5.nako3 
UTF-8 テキストファイル
ファイルの最後に改行あり
行末にスペース→  
$ 
```




## 行をソート

cat コマンドを改造すると、たとえば行をソートするコマンドなんかも作れるようになります。

```
# sort.nako3
# 継続表示、標準入力全部読 は同じなので省略

標準入力全部読んでテキストに代入
テキストをLFで区切って行リストに代入
行リストを配列ソートしてソート済行リストに代入

ソート済行リストを反復
    対象を表示
ここまで
```

```
$ cat sort.nako3 | ./cnako3.sh sort.nako3 




        「stdin」を読んで戻す
        「標準入力からの読み込みに失敗」のエラー発生
    「process.stdout」の「write」を["{値}"]でJSメソッド実行
    ここまで
    もし、「stdin」が存在するならば
    対象を表示
    違えば
●標準入力全部読とは
●（値を）継続表示とは
ここまで
ここまで
ここまで
ソート済行リストを反復
テキストをLFで区切って行リストに代入
標準入力全部読んでテキストに代入
行リストを配列ソートしてソート済行リストに代入
$ 
```

## コンパイラ

（この節は 2022-06-05 に追記しました）

cat コマンドを改造して育てていくとコンパイラも作れます。

<!--
なでしこ3でかんたんな自作言語のコンパイラを書いた - Qiita
-->
https://qiita.com/sonota88/items/54c65e97aa563009137e

……というのは半分冗談ですが :sweat:  半分はほんとで、コンパイラがやっていることは

-  (1) 入力を読む
-  (2) 読んだデータを加工する
-  (3) 加工したデータを出力する

のように改造 cat コマンドと同じですし、作るときも実際にまず cat コマンド（のようなもの）を作ってちょっとずつ改造を加える形で作っていました。




---

というわけで cat コマンドっぽいものを作っていろいろ試してみました。この記事は以上です。






# この記事を読んだ人は（ひょっとしたら）こちらも読んでいます


<!--
Zig: 1バイトごとに読み書きするだけのcatコマンドを書いてみた - memo88
-->
https://memo88.hatenablog.com/entry/2020/09/19/084602

<!--
Deno: 標準入力を読んで行ごとに処理 - memo88
-->
https://memo88.hatenablog.com/entry/2020/07/18/154534

<!--
Kotlin: 標準入力を読んで行ごとに処理 - memo88
-->
https://memo88.hatenablog.com/entry/20200711_kotlin_read_stdin_each_line