📘

「コマンドラインシェル??? 誰でも作れますよ」

2020/12/31に公開

「まさか、あなた…作れないんですか?」(不安を煽ってゆく商法)

縛り条件

  • インタラクティブなシェルを作る
  • 自分で作った nyagos 向けの既存パッケージは使わない
  • 他人が作った既存パッケージは使ってよい(作成を依頼してはダメ)
  • 自分のコードは go run コマンドで動かせる範囲のみ(つまり1ファイル)
  • 動くこと!

必要最小限のミニマムな骨格のフレームのスケルトンなイメージの青写真

while ( true ){
 text = readline()// 一行入力:最悪 gets でもよい
 system(text) /// 一行実行:最悪 CMD.EXE とかに丸投げしてもよい
}

最初は「最悪」だけでいいんです

今回のポリシー的な方向性についての方針

  • 一行入力は、"github.com/mattn/go-tty" についてた ttyutil の ReadLine を拝借しよう
    • 最初はgets相当をやろうかと思ったが、bufio.NewScannerとか、かえってめんどいように思った
    • が、実際はそんなことないくらい、ttyのインスタンス用意で行数消費した
  • 一行実行くらいは "os/exec" で軽く実装しよう

カタカタカタ

package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/mattn/go-tty"
	"github.com/mattn/go-tty/ttyutil"
)

func mains() error {
	fmt.Println("Tiny Shell. Type Ctrl-D to quit.")

	tty1, err := tty.Open()
	if err != nil {
		return err
	}
	defer tty1.Close()

	for {
		clean, err := tty1.Raw()
		if err != nil {
			return err
		}
		fmt.Println("Ready")
		text, err := ttyutil.ReadLine(tty1)
		clean()
		if err != nil {
			return err
		}

		fields := strings.Fields(text)
		if len(fields) <= 0 {
			continue
		}
		cmd := exec.Command(fields[0], fields[1:]...)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		cmd.Stdin = os.Stdin
		cmd.Run()
	}
}

func main() {
	if err := mains(); err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
}

説明しよう!

  • main()の中からmains()errorを呼ぶのは、エラー処理を簡略化する、わたくしのスタイルなんで、あんまり気にしないでいいです。
  • よく見ると、半分くらい https://github.com/zetamatta/go-readline-ny/#example2go の改変物だが、誰もコピペがダメとは言っちゃあいない(キタねぇぞ!
  • プログラムの上半分は一行入力を行う(=ReadLine を呼ぶ)ための、準備作業(1文字キー入力の初期化)に費やされている。ここは go-tty の仕様だからシヨウがない。。
    • go-tty はそもそものはコンソール入力のライブラリで、これを使うと Enter キーなしのキー取得が可能になる。ttyutil.ReadLine はそのサンプルとして提供されているもの
    • go-tty はUNIXの端末操作のラッパーを模している(Windowsではラッパーではなくエミュレーションしている)。UNIX系端末には Cocked モードと Raw モードがあり、前者は Enterが押下されるまで端末任せの編集が行われるが、この状態では Backspace など簡単な訂正が可能なだけで、まともな編集機能があるとはいいがたい。Emacs-like な複雑な編集を行うには Rawモードといって、入力文字をそのままアプリケーションに送信するモードに切り替えて、アプリケーションで細かくキーの解釈・カーソルの移動を行わなくてはいけない。今回はそういうめんどくさいことは ReadLine に丸投げだ!
  • プロンプトだが、どうも ttyutil.ReadLine だと上書きされるのか、うまくインラインに書けなかったので、今回は F-BASIC風に「Ready」をプロンプトにして、8bit風味にしてごまかした。
  • 後半は入力した一行を単語単位に分割して、それを exec.Command にくべているだけ

「ね、簡単でしょ?」

TO BE CONTINUED...?

  • 環境変数は参照も変更もできません
  • カレントディレクトリの変更もできません
  • パイプラインもリダイレクトもできません(「甘ったれるな!」「逆ギレ?」)
  • スクリプトもありません。Lua?なんですかそれは

なんとかせんといかんのでは?(気が向いたら、続きを書きましょう)

GitHubで編集を提案

Discussion