GShell 0.1.8 − コマンド入力用IME

開発:さてリモートシェル第3日目行きましょうか。

社長:ちょっと待った。さっきから猛烈に、IMEがやりたくなりました。

開発:何故に。

社長:変な名前のファイルができちゃったんで消したくなった時です。"??" とか。まあこれはエスケープすればいいんでしょうけど。日本語名のファイル名には昔から悩まされています。日本語部分を編集できなくても良いですが、日本語名のディレクトリに移動したいとか、ファイルにちょっとローマ字的にいじりたいとか。

基盤:それはよくありますね。

社長:それで、せっかくならalias とか ファイル名の展開も、IME という枠組みで統合してやれば良いと思うんです。全部IMEの辞書に入れちゃうわけです。!3 とか入れると3番目のコマンドに置き換えられる。*.html なんて入れると gsh-0.1.8.go.html になる。

基盤:当たり前のような。

社長:IMEの辞書として統合的に出来る点が新しいのです。

開発:alias とか history とか find の結果をがんがんIMEの辞書に入れる… まあ辞書を永続的な辞書と刹那的な辞書に分ける必要はありますね。

社長:優先度的な属性でも良いと思いますね。あと刹那というのは再来する文脈だと思うんです。消えてしまうものではなくて、時々そういう文脈になる。文脈毎に辞書を作るか、辞書項目に文脈の目印を入れると良いのかなと思います。

開発:ならいっそIMEで、ファイル名展開の構文で生成もできると良いかもしれませんね。あとふつうIMEでは複数の候補から一つを選択して確定するわけですが、複数の候補をそのまま展開して確定ちゃうという変換方法がないといけないです。

社長:うーん、面白そう。

開発:変換については、実際に置き換えるか、表示モードで切り替えるか、選べると良いかと思います。表示モード方式は、編集はコンパクトで、表示は展開してっていう形には利点があります。

社長:それ、プログラムのIDEでできるといいなあ。マクロを展開したらどうなるか、その場で見たいことって多いですよね。

基盤:ついにIDEにも手を出すと。

社長:いや、ただのエディタでもビューアでも良いのですが… とにかく、複数の表示方法を切り替えられることが重要と思います。複数のビュー。HTMLで言えば最終的な表示とソースコードビュー。

開発:ただ、現状、入力用のラインエディタ getline.c は2週間前にGshellを始めた時にCで書いたもので、これをGoにしないといけないですね。まああの当時はGoのことがほとんどわからなかったわけですが、今ならすんなり書けるかな。ちっちゃなプログラムだし。stty は Unix のコマンドでやっちゃえばOKだし。

社長:そうしましょう。あー、でも今日は外食に行きたいです。飯食って飲んで鋭気を養ってきます。

* * *

開発:あれ?目が醒めたらこんな時間に…

社長:よく寝ました。このところ飲んでなかったから酒に弱くなりましたかね。今日見かけた印象的な建造物。

基盤:なにか至近距離で花火が打ち上がってます。

経理:市から通知書が来てますね。

社長:どれどれログイン。おー、振込*キユウフキンツクバシトクベツテイガ 入100,000円とな!

開発:口座名がカタカナでしかも大きいひらがなだけってなんとかならないんですかね?もとが濁点別文字だとすると20文字で打ち切り。もう何十年もこの状態。

基盤:キヤノンみたいな。

開発:ウエルシアなんていう表記になっちゃてるのもそのせいですかね。

社長:半角カナにすら小さいカタカナはありますから、それよりもっと前に決められた文字コードを使ってるんじゃないですかね。

基盤:でもWikiには銀行もこれを使ってるって書いてますけどね。

半角カナ
金融機関のシステムにコンピュータが導入された当時は高速なコンピュータもネットワーク網もない時代で、日本のコンピュータ全体が、情報量が少なくて済む1バイトの半角カタカナを使っていた。故に、銀行のシステムも半角カタカナで統一され、全国の金融機関に定着していった。後に、コンピュータの進化とともに全角文字が使えるようになったものの、一度莫大な資金を投入して出来上がったシステムは簡単に変更出来ず、現在もシステムが未熟な金融機関に合わせ、共通する形式は半角カタカナのままであるケースが多い。

開発:ハンコ文化が廃れないのと同じ理由ですかね。

社長:識別子は口座番号ですしね。名前の無い電話番号よりマシなのでは。

開発:そういえば法人の識別子として法人番号ができたのって割と最近みたいですね。

基盤:人間番号より後でしたっけ?

* * *

開発:さてさて、ようやく思い腰を上げてラインエディタを Goで書き直しました。

社長:手間がかかりましたか?

開発:うーん、もとが500行程度のCのプログラムだったわけです。Goのプログラムとして通るようになるまでは、ほぼ機械的な書き換え作業ですので、1時間程度で終わりました。基本的にはCの標準的な関数、stdioとstring関係の関数、あとは system() ですが、これの互換関数的なものを定義してやりました。

開発:で、もとのCのプログラムは最初、ほぼ変更無しでやってました。書き換えたのは func と struct と、for 文くらいですね。これは構文が違うので仕方がありません。ていうかこれ、人間の手作業でやる必要はなくて、機械的に変換できるはずです。

基盤:goto 文も残ってますね 🙂

社長:Goto が通ったらすごいですね。外のモジュールに飛んじゃうとかw

開発:適切な goto は大変有効な機能ですからね。書きやすく、読みやすく、効率が良い。harmful だなんてとんでもありません。

社長:まああれも、魔女狩りにあった感じですね。今は言語の設計も様変わりにしてるのに、当時の悪いイメージがまだ残っている。今も教科書には使っちゃいけないって書いてあるんですかね?

基盤:なんだかカッコ悪いイメージはありますよね。

開発:あのころは玉ねぎ状の言語がカッコいいと思われてましたからね。一体何層になってるんだみたいな。そういう意味ではCはカッコ悪い言語だった。

開発:構文的にひっかかったと言えば、例の switch がデフォで break だというやつです。なんか動きが変だなと思ったら。Cでは複数のcase が一つの処理になる場合を良く書くので、あれに相当する振る舞いをするキーワードとかあると良いのですが。

社長:goto でやるとか。

開発:それで、最初はそのままでやってたんですが、やはりGoの可変長文字列を使わない手は無いということで、当初は固定長の []byte でやってたのを string に置き換えました。なんと言っても書きやすい。ここはCが一番苦手な部分ですし。ということで、もう元のCには戻れないプログラムになりました。

社長:まあどのくらいのオーバヘッドがあるのかわからないですが、何しろ人間の手入力を相手にしている部分ですしね。一桁くらい遅くなったって構わないでしょう。ミリ秒で処理できればOK。

開発:ということでGoにしてgsh.goに組み込んでしまったので、逆にコマンド行エディタから自由にgshの内部状態にアクセスできるようになりました。ヒストリを戻るとか超簡単になりました。

社長:それで、コマンドラインIME的にはどのような?

開発:移植作業でちょっと疲れてしまったので、簡単なのを一つだけ作りました。イツモ不便に思っていた 10進 / 16進変換支援です。%x/数値/ とすると数値が16進に、%d/数値/ とすると10進に変換されます。こんな感じ。まず入力をして。

開発:¥j と押すと変換されます。

基盤:うーん、これは普通に便利。ですが、¥j って、これも日本語変換っていう意味ですかw

開発:とりあえず流用してるだけです。¥ の後に意味を文字はかなり空いているので、色々できると思います。で、¥j は表示を変換するだけなので、ここで改行でコマンドを確定すると、こうなっちゃいます。

開発:なので、現在表示されている文字列で元の文字列を置き換える ¥i というのを入れました。¥i してから改行するとこうなります。

社長:コントロールキーもファンクションキーも使わないんですね。

開発:その辺はユーザが使いたいならそうバインディングすれば良いと思います。今はハードコーディングしていますが、これもコマンドキー用の辞書を作ってそれをインタープリッとするようにすれば良いと思います。

基盤:VNCではコントロールキーがうまく通らなくてひどい目に合いましたか。これで助かります。

開発:そういうことで、昨日の作業のまとめを終わります。最後に恒例のこれを。

社長:Hello -> こんにちはを辞書に入れとくとよいかもですね 🙂

開発:私はここにカンマを入れる作法の意味がよくわからないんです (^-^;

-- 2020-0823 SatoxITX

GShell 0.1.7 − 続リモートシェル化

開発:いやもう昨日はいったいどうなることかと。

社長:出だしの5ホールの時点ではものすごい結末もよぎりましたが。

開発:稀勢の里の連続優勝後のストーリーを思い出しちゃいましたよ。

基盤:4日間楽しめると良いですね。

* * *

社長:今日はどこから行きましょうか。

開発:優先度は、リモートホストでやることの多い作業の効率化に資する事ですね。かつ、なんか実装面で面白い事。

基盤:うちでは大半が、ローカルとの間のファイルのコピー作業ですね。あとはログの tail -f (^-^)。たまにコンテンツの編集。ごくまれに設定ファイルの編集。pingやtcpdumpもたまにやりますね。

開発:tail -f とかtcpdump以外は本来、手元で管理していて、ファイルをリモートコピーすれば出来ることではあります。ファイルをコピーして、あとはなんかプログラムを叩くだけ。tail -f みたいなのは、NFS的に実現するか、ログインして tail -fコマンドを実行するかですね。あるいは、HTTPで実現する。

基盤:tcpdumpとかはどうなるんでしょう?仮想的なファイルに見せるとか?

社長:まあ他に比べると、ログインして tail -f があまりにも簡単ですね。リモートシェルのキラー機能じゃないですかねw

開発:思うにFTPにそういう機能があればよいのだと思いますけどね。ストーリームからの入力というか。

基盤:named pipe に tail -f の結果を流すプログラムを貼り付けるとかすればできるんじゃないですかね。

社長:というか、コマンドを実行して標準出力をダウンロード・アップロードするという機能があれば、それだけでいいんじゃないですかね。

開発:その案、乗った!ftpコマンド風には、get {tail -f file} /dev/ttyxx みたいにできると良いですね。でこれをGoルーチンでバックグラウンドにやる。

基盤:get {tar cfz - dir} {tar xfz -} なんていうのも良いですね。put file.tar.gz {tar xfz -} とかも。

社長:うーん。今日はそれをまずやりますか。

* * *

開発:まず、昨日の作業のまとめす。

開発:単発のファイルのアップロードとダウンロードは出来ましたが、RTT 250msの遠隔とやるとオーバヘッドが1秒かかってしまいます。これは原理的にも、0.5秒を切ることはできないでしょう。リモートには100万個単位のファイルがあります。多数のファイルをサイト間でコピーするのにこれでは使い物になりません。

基盤:ns1.its-more.jp には147万個のファイルがありますね。

基盤:そのうち83万はカウンターのファイルですが。

基盤:なんでこんなに沢山あるんですかね?

社長:ああそれは、DeleGateが1つのURLごとにカウンタファイルを一つ作るからです。動的に生成するコンテンツもあるし、我社の所有する100近いドメイン名から同じコンテンツをアクセスされたのが、URLは違うのでそれぞれ違うカウンターファイルになる。80万ファイル程度で収まっているのは少ないくらいだと思います。

基盤:しかし$5ライトセールのinodeは250万しか無いようです。結構ヤバいかと。

社長:なんでハッシュファイルにしなかったんでしたっけ。

開発:クラッシュして壊れるのが嫌だったように思いますね。dbm とか ndbm とかの過渡期で移植性の問題もあったような。あとは手作業で個々のカウンターの中身を見られるということを優先したような気も。

基盤:しかも個々のファイルは256バイトの固定長です。今日びのファイルのブロックサイズは4KBか8KBだと思いますから、ほとんどフラグメントですね。95%以上無駄。

社長:個別ファイルのカウンターを dbm 型式に圧縮というかアーカイブして、過去のカウンターセットにアクセスできると良さそうですね。カウンターファイル間の演算ができれば、色んな期間のビューで見られるし。

開発:いずれにしても今後、DeleGate自体のカウンター機能に手を入れることはありません。やるなら、GoでDeleGateをラッピングするかフックするかして、Goで作ったカウンターで置き換えたいと思います。

社長:GShellの機能としてカウンターがあっても良さそうですね。でそれを呼び出す。ヒストリ機能も汎用化して作ると良いかもしれない。

開発:ftp的な機能の範疇ではないですが、カウターファイルをカウントアップするみたいなコマンドがあると良さそうに思います。

* * *

開発:で、昨日の作業のまとめです。

開発:まずファイル転送用のコマンドですが、rcp や scp にならって host:file 型式でファイルを指定できるようにしました。コマンド名は gcp。ポート番号も指定したいので、host:port:file というのも使えるようにしました。省略規則は [host]:[port:][path] です。アップロード先が前と同じホストの手元と同じファイル名なら : だけでOKです。

基盤:フルのURLが gsh://host:port/path だとすると、省略規則は [gsh:][//][host]:[[port]/]path とかですかね?

社長:ローカルファイルも前と同じみたいな指定ができると良いように思いますが。まあヒストリを参照すれば良いのかな。というか、ほとんどの場合、特定のひとつのサーバにログインしていて、そこにアップロードするんでしょうから、繰り返す場合にはアップロードは gput src [dst]、ダウンロードは gget dst [src] で良いように思いますね。

開発:まあそのほうが簡明ですね。

基盤:賛成。

開発:それで、最大の問題の遠隔へのアップロードですが。フランクフルト宛てではこう。

開発:一方ダウンロードではこうです。

開発:最速で、上り 1.5MB/s、下り 2.1MB/s というところです。

基盤:今日は下りも遅いですね。下りは 5MB/s 近く出ることも多いですが。

社長:ここはぜひ、UDPを使った当社の驚速化技術を投入したいところです。

開発:一方そのころ当社よろずサーバ jp1 @ライトセール東京。

開発:こちらは最速で上り40MB/s、下り24MB/s となっています。スピードは不安定ですが、20MB/s は確実に出ますね。

基盤:macOSにインストールされているscpは性能が安定しています。例の問題で、上り1MB/sしか出ません。

開発:この状態が放置されているというのは、信じがたいことです。Appleはこの問題を把握してないんでしょうか?あるいは我々が非常に特殊なネットワーク環境に居るのでしょうか?

社長:そもそも遠隔で scp とか使っている人少ないんでしょうかね…

* * *

開発:ああそれで、ログイン機能というか認証機能についてですが。

社長:やはり公開鍵認証ですかね。

開発:それはパッケージを使えば簡単に出来ると思うのですが、そもそも鍵での認証にも疑問というか不安はあるわけです。秘密鍵を盗まれちゃったらどうしようとか。鍵の管理も面倒。

開発:なので、ワンタイムパスワード的なものをやりたい。たとえばすごく素朴には、gshでサーバを起動する時に、その一時的なサーバにだけ有効なパスワードを指定する。あるいは頻繁に、gsh固有のパスワードを自動生成して変更して、それをいつもgshサーバとgshクライアントとの間で共有しておく。

社長:仮想的な永続セッションですね。

基盤:生成されたパスワードを共有できない環境にあるクライントだと不便なような。

開発:まあ一番最初は、認証中のgshクライアントに発行してもらって、手入力というかコピペさせてもらう、でいいんじゃないですかね。

開発:そもそも秘密鍵の問題は、それを安全に共有というか配るのが大変だということでは無いかと思うんです。だから頻繁には変えられなくて、それがまた危険を生じる。しかも人間が手入力する前提だから簡単なものになって、それもまた危険を生じる。ですが、こういう特定ユーザの特定用途のリモートシェルでは事情というか前提が違う。そもそもオンラインで常につながっているわけですから、配布というか共有はすごく簡単。配布の時の暗号化は Diffie-Hellman で十分だと思います。

社長:たとえばサーバもクライアントも膨大な過去のヒストリを持ってるから、そう例えば1GB程度のヒストリですが、いついつに実行したコマンドと結果はなんだっけ?あなたなら覚えてますよね?みたいな認証方法があっても良さそうですね。

開発:何ギガバイトあっても、ナマのヒストリデータに依拠するのはそれも盗まれた時の危険はあるでしょうから、ひねる必要はあると思います。

基盤:gshサーバからクライアントにメールでワンタイムパスワードを送って、それをgshクライアントがPOPかIMAPで受け取って、サーバに送り返すのでも良いのでは。あるいは携帯に4桁くらいの番号をSMSしてそれを入れる。

社長:うーん、それが今風でカッコいいかな…

開発:いや、でももっとシンプルに、物量作戦で行くとするとですよ。鍵は100GBくらいにする。

社長:2048ビット256バイトの鍵が40万個詰まってるみたいな感じですかね。

開発:使い方は自在かと。何バイト目から何バイト分を鍵として使うとかその場でネゴる。

開発:いまどきのHDDにとって100GBはへのようなもので、鍵保管にかかる費用は約200円。これにHDDととネットの最高速の100MB/sでアクセスできたとしても、盗み出すのに1000秒かかるわけです。というか、すごく遅いSDメモリとかに保管すれば読み出しに1日掛かりです。

社長:相手がクラウド上のサーバだったり携帯だったりすると、現状では巨大鍵の共有はチープではないでしょうね。まあでも、現状で10GBならありですかね…

基盤:思い切り遅い大容量のランダムアクセス可能な記憶があると良いですね。

開発:ランダムアクセスの応答はそこそこ速いけど、連続読み出ししようとするとすごく遅いディスクとかファイルとか。

社長:ソフト的に、連続読み出しをチョークする仕掛けをOSというかファイルシステムに入れてあれば良いようにも思いますね。

開発:ただ、OSにしてもソフトですし、ソフトは騙せますからねえ… ただのデータだけに。

基盤:そもそもFlashなら、すごい大容量で読み出しがすごく遅いのがありそうです。256バイトの読み出しはミリ秒でできるけど、100GB全部読むのに一週間がかりとか。

社長:それって、鍵を保存するのに1週間かかるということですか?

開発:本来は高速なFlashで、利用時のストレージとの物理的な接続をUSB-1でやるとか、SPIでやるとか… AppleTalk とかw

社長:10MbpsのEthernetハブとか、まだ売ってるかもですね。それ経由で鍵ファイルをNFSする。

基盤:人間でなくてユーザが使ってるマシンで認証するという手もありますね。

社長:まあ今どきのマシンならあれを積んでますよね。

開発:何にしても今の認証のインフラって、高速大容量のネットワーク、インターネット、プロセッサのセキュリティ機能が使えるって前提が無しに作られてる気はします。アプリケーションプロトコルにしたってアプリケーションにしたって、前提とか環境がまるで違う時代に作られたものが金科玉条になっちゃってるというか。昔は過去の遺産を活用することが実装のコスト減にも発展の速度にも寄与しましたけど、今は逆になっているんじゃないかって。

社長:まあ、ITの応用を一度まる洗いしたく候というのが当社の設立趣意ですから(笑)

社長:今の所うちの用途では急ぎでは無いですが、認証はぜひやりたいですね。なにか面白いのを。

* * *

開発:あー疲れた。昨日の版の整理をしていたら、もう4時をまわってしましまいした。午睡もなく。

社長:私なんて昨日今日と飲みにもいかず液体カロリーメイトですよ。ごくごく。

開発:そうこうするうちに今日はもうそろそろスタートです。

開発:しかしこの草っ原って、ロイヤルという名前が似合わなわいなぁ…

* * *

社長:それで tail -f ですが。

開発:私は、Unixのリダイレクションは良いのですが、shellレベルで入出力がファイルなら > < でプロセスだと | というのがいかがなものかと思うんです。これはプロセスだ、という特殊ファイル名的なもの、例えば < cmdfile{...} なんてすると、そのコマンドからの出力を pipe で受けるでもいいんじゃないかと。

社長:どういうメリットがあるんですかね。

開発:この cmdpath{...} の部分はOSが、というか open システムコールのユーザ空間のラッパーが解釈すると良いと思うんです。そうすると、shellによらず、動的なコンテンツを提供する仮想的なファイルが実現できる。プロセススペシャルファイル的な。全てのアプリからそれが利用できるわけです。

基盤:Windowsのショートカットとかってそういう感じのものじゃなかったでしたっけ。

開発:で、URLというのはまさにそういうものなわけですが、ファイルシステムでは無い。なので、NFSでこれをやりたいと思っているわけです。ただ、NFSだとファイルに対する機能要求が複雑すぎる。それで、FTPでやればどうかという気もするわけです。

社長:この話はごく最近やったような記憶もありますが… ああ、アプリは拡張子でファイルの型を判定することが多いから、最後は拡張子で終わるのが良い、って話でしたね。

開発:名前は filenmame{...}.ext でも {...} filename.ext でも良いですが。

* * *

社長:だいぶ厳しい結果に終わったようですね。

開発:どーんと落ち込んでがーんとバウンスバックすればいいんじゃないですかね。

基盤:明日は開発に集中できますねw

社長:で、{tail -f logfile} の件はどうなったでしょうか?

開発:できました。実際、ただpopen相当の事をsyscallでやるだけなので、そこは簡単だったのですが、{ ... } をパースするところでツボってしまいました。なんと、Go言語のスライスの基本中の基本を誤解してたのが原因した。Goのスライスの部分列を取り出すには配列[start:end] なのですが、何故か配列[start:length]だと思っていたのです。

基盤:よくそれでこれまで動いてましたねw

開発:なんだか不思議な怪奇現象に何度かあったことはあったような記憶がありますが、適当に回避してたんでしょうね。たぶん、配列[:length] と混同してたんだと思います。この場合、end == length ですしね。

社長:その end というのは実際の最終要素ではなくて、+1 だということですね。end という表記は紛らわしいと思います。

開発:あと、shell におけるカッコの解釈についてちょっと検索したんですが、ちょっとおもしろい記事がありました。

開発:shell が使用する特殊記号の中で、カーリーブラケットは一番既定がゆるいというか、解釈が文脈依存なんだと思います。出現する文脈によってはなにも解釈されないので、ユーザプログラムで使用することもできます。

社長:そう、$N と ${N:… } は学生の頃に作ったプログラムでも使ってました。その流れで、DeleGate はデータをくくるのに { } を使ってるんですよね。

基盤:ファイル名を *.{gif,jpg} なんていう感じでマッチするのに良く使いますね。

開発:そう、一般には検索、マッチングなんですが、データの生成機能があるようなんです。

社長:これは面白い。

開発:配列も、配列の配列、…といったこともできるし、当然配列要素の取り出しもできる。

基盤:知りませんでした。文字列を1文字ごとの配列にバラして加工したりマッチングしたり結合したりもできるんでしょうか?

開発:できるんじゃないですかね。それはそうと、zsh が { } になにやら厳しい解釈をしているようで、これで zsh のイメージがガクンと落ちてしまいました。

社長:なんにしても、他のshellとの互換な構文を採用しなければならないとなったら、GShell 固有で色々できるのは { } しかないんでしょうね。

* * *

開発:ということで今日のまとめです。

開発:今日の実装では、GShellのクライアントとサーバの間の、一つのtcpコネクション上にコマンド・レスポンスの制御と、ファイルとかの送受信データが一続きで流れるようになっています。ファイルのような静的なデータについては、最初に送信データの長さを知らせて、あとは生でドンと送っています。ギガだろうがテラだろうが。

社長:大小によらず一つのメッセージというかパケットなわけですね。

開発:最高速のデータ転送を実現する上で、CPUでの処理は最小限にしたい。ですのでこれは一つの理想形ではあるわけです。

基盤:10GB/sを実現したいならですけどね。現状では1ギガビット/sしか無いから、100MB/s で処理できれば十分だと思いますが…

開発:それで当然問題になるのが、プログラムとかでデータを生成して送る場合、送り始める時に長さが不定であるという点です。なので、どうやって受信側がデータの終了を判定して制御のやりとりに戻るか課題。

基盤:普通はパケット化しますよね。といっても、単に長さ情報のヘッダだけの可変長パケットで良いと思いますが。

開発:いや、パケット化しないモードがあっていいじゃないですか。というか、プログラムでやるにしても、下の層に分けてやるのがきれいだと思うのです。

社長:下の層でSSLを使う場合には、 SSLのパケット型式に便乗するとよいかも知れませんね。Notifyみたいのを使うとか。

開発:さてそれで実例です。リモートで tar cf したのをローカルで tar tf しています。もちろん tar xf してもOKですが。

開発:ディスク上に実在するファイルのtar の場合、無限長というわけではありませんが、何十何百ギガバイトにもなるかも知れない。あるいは作成に何分も何十分もかかるかも知れない。だからこそ作っては投げのパイプライン並列・ストリーム式が必要なわけです。

社長:tar 型式そのものをパケットというか、型式を理解して中継すれば、終端も分かりそうですね。tar 型式で終端ファイルを送るというか。

開発:ああそれで、上の例の中に出てきますように、データの送信が終わったら、ちょっと間を置いて、この例では100msですが、データ終端データを送っています。ユーザ側がそれまでのデータを受け取り追えていれば、ソケットから読み込んだデータの先頭にこの終端マークがあるはずだ、というわけです。

基盤:100msでも足りないかも知れないですね。膨大な数のデータを送る場合に、1つごとに1/10秒待たされるの実用的で無いと思います。

開発:ともかく、アプリケーション自身が持っているデータのフォーマット形式を利用して長さを規定したい、ただ転送のためにぶつぶつパケットだかチャンクだかに切るのに抵抗があるのです。途中にフィルターをかませる時にすごく害になります。

社長:受信側が受信状況を送信側に知らせてくると良いかもですね。送信側からのデータが途切れたら、今何バイト受け取りましたけど、まだ残りがありますか?無いならこういうデータを送って下さいみたいな。そしたら100ms待つ必要も無いでしょう。

基盤:フランクフルトとだと、そのネゴに100ms以上かかりそうですね。

開発:TCP上のOOBを使うというのもやはり候補ではあると思います。

開発:ただ何にしろ、本命の実装では、データ転送用のチャネルは制御用とは分離する。複数並列転送も可能にする。そういう方針で行きたいと思います。

社長:ラジャー。

開発:ああそれで、tail -f で遠隔のログファイルズルズルの例も。

基盤:これは… 普通に便利ですね。リモートログインする機会が激べりしそうです。

開発:あるいはshellスクリプトを送って、実行する。

基盤:便利過ぎる…

開発:あとは当面、頻繁に変更するGShell自身をアップロードしてリコンパイルして自分を新たにexecする機能ですね。これをやっとけば私もリモートログインする機会が激べりします。

社長:まあ不特定多数が安全に使うにはこのままではイケナイですけどね。でもそれは必要になったら検討しましょう。

社長:ところで、遠隔ユーザには普通のファイルのように見えて、実はアクティブなファイルだっていうのはどうやるんですかね。

開発:特殊な型式、ファイル名の拡張子か、内容の先頭のマジックで識別される特定の型式のプログラムならGShellスクリプトとして実行する、引数はシンボリックリンクか環境変数で与える、といった形では無いかと。

基盤:CGI互換の環境変数でも良いですよね。

開発:それは当然含めるべきだと思います。ただ、FTPクライアントからは、アプリケーションレベルのオプション情報を与えるのが難しいというか、ごく限定されるとは思いますが。

社長:macOSでは OS X デビューの時からFTPがmountできます。ぜひGShellをFTPサーバにもしたいですね。

開発:おっとー。いまちょっとプチ感動したのですが、ファイル転送でtopも普通に見えますね。

基盤:sshでは怒られちゃいますね。

開発:まあこのGShellサーバのプロセスは、端末から起動してますからね。そのtty情報が使われているのでしょう。

基盤:sshでコマンドだけ実行する時に入出力をttyだかpts経由にすることって出来ないんでうかね…

開発:ログインはしないけどtty入出力を想定したコマンドを使える、っていうモードは有って良い感じはしますね。うーん。まー、いずれかのユーザの名義にはなるんでしょうけど。guestとかanonymousでいいかなという。

社長:anonymous で GShell ログインして試用できる GShellのデモサイトがあると良いかもですね。

-- 2020-0821 SatoxITS

GShell 0.1.6 − リモートシェル化

開発:さて、今日はGShellのリモートシェル化に取り組みたいと思います。

社長:いよいよ本丸ですね。

開発:最大の目玉は、ローカルシェルとリモートシェルとの協調動作だと思います。ローカルはただリモートの入出力を端末にストリーム的に中継するだけでは無い。少くともこの2つはやりたい。

  • ヒストリ共有
  • ファイル共有

社長:コマンドの実行プログラムとファイル環境とかはリモートにあるんだけれど、コマンドの解釈まではローカルでやるという方法があり得ますね。

開発:特にリモートとの時間距離が遠い場合、手元のttyでインタラクションしながら実行する場合、その方法しか無いと思います。

基盤:フランクフルト支部での苦い体験が考えを変えましたね。

開発:あれで地球の大きさを実感しました。HDDのレイテンシ10msを許容基準に考えると、ホスト間250msのレイテンシは問題外です。そういう世界がまだあるというか、今後も本質的にあり続けるんだろうと実感したのです。

社長:ライトセールのお陰様でした。

基準:レイテンシが10msでもうんともすんとも動かないFUSEって何者なんでしょうね?

開発:4月にITSTPプロトコルを試作した際には、ファイル関係のシステムコールを全部リモートに飛ばすというナイーブな方法をやったわけですが、これはレイテンシーの高い環境では使い物にならない可能性が高いです。

社長:やはりアプリケーションレベルでの意味的な大きなまとまりでやらないとダメですね。そして処理とデータの局所性。

開発:ただファイルを共有できれば良いという意味でなら、リモートのファイルをローカルのファイルシステムの一部として見せるという通常のネットワークファイル共有があります。ですが、リモートにあるファイルに対して何らかの処理をする場合、その処理はそのファイルのあるリモートのプログラムで行わないと、まともな性能にならない。

社長:たとえば find コマンドのファイルがリモートにあれば find もリモートで実行するってことですね。

開発:一番シンプルな例が、同じホスト内でのファイルのコピーで、これをいちいちリモート経由でやっていると話になんらない。

基盤:ネットワークファイルシステムでは実際そういう事にんっちゃうわけですけど。

開発:ファイル関係のシステムコールに、コピーが無いのが問題と思いますね。それに、コピーする際の簡単な加工方法、圧縮とか指定できたら、世の中の無駄なトラフィックは大幅に減るかもしれません。

社長:システムコールとかNFSとか、ファイルの中身に微細にアクセスするレイヤでそれをやるのはやはり難しそうなので、FTPとかWebDAV的なファイルをひとつの塊としてしか見ないプロトコルにそういうコマンドを入れるのが適切でしょうね。

社長:ユーザにローカルとリモートをどう見せるかが重要と思います。両方を同時に見せたいわけですよね。

開発:シェルから見える仮想的なディレクトリにするのが自然と思います。で、カレントディレクトリと言うかワーキングディレクトリの移動によって、接続先のシェルを代える。chdir @host/path みたいな。リモートのファイルに対するアクセスを行うコマンドはリモートで実行する。

開発:両者にまたがるコマンドについては、どっちにしろ局所性が無いので、例えばファイルの単純コピーとかはどっちでやってもよいでしょね。実現のしやすさで言えばローカルで実行。処理を伴うもの、例えばリモートで圧縮した結果をローカルに書くとかだと、もちろんリモートで圧縮を実行するのが吉。

社長:そのへんの自動判定が難しくて面白いでしょうね。状況を見て途中で処理を交代するとかできたらブラボーですが。ランダムアクセスとかリアルタイムなアクセスが多いようなファイルの近くでコマンドを実行する。

開発:場合によっては、リモートに必要なコマンドが無い場合もあり得るので、その場合にはローカルまで持ってきては処理する。RPC的にリモートでopenしてローカルなファイルディスクリプタからアクセスする。ここはITSTPでなくても、Goのパッケージを使えば良いかなと最近は思います。

社長:あるいは、Goプログラム型式の処理コマンドを生成してリモートに送り込んで、リモートでコンパイルして実行する。

基盤:夢は広がりんぐですが、どこから手を付けるんでしょう?

開発:やっぱり chdir と which と find ですかね。ベースは、Remote Procedure Call 的な Remote GShell Commandじゃないかと。

社長:そういえば昔、rexec とか使ってたような記憶があります。

基盤:r系はなんで絶滅しちゃったんですかね?

開発:別のレイヤで実現されるべきセキュリティの話と混同されて魔女狩りにあったように思いますね。NFSとかtelnetなんかも。そもそも rlogin が telnet の上位互換に実現されなかった時点でアレ?って感じでしたが。

社長:OSIの7層モデルみたいのの上の層がインターネットには無かったですからね。セッション層みたいなのが。全部がアプリケーションプロトコルに要求されちゃってなんともまあ。

開発:その点SSLというかTLSはセキュアなレイヤをアプリと独立に挿しはさんで良かったと思うんです。なぜSSHが必要だったのかとか、普及したのかは良くわかりません。

社長:垂直統合的にワンセットにパッケージされててとっつきやすかったんだと思います。

* * *

開発:さて、今日の仕事を始める前に、現状の0.1.5をアーカイブして置きます。

開発:この版では、移動して来たディレクトリの履歴を @N で参照できるようにしました。

開発:N番目のコマンドを実行した時のディレクトリという参照もしたいので、これを !Nd というにすることにしました。一方、N番目のディレクトリに居た時に何をやったかを見たいことも多いので、これは history @N で表示できるようにしました。

社長:history コマンドに限らず、@Nh とかで、@N でのヒストリに展開するのが良いかもですね。

開発:そうですね。表記方法はまだ仮り置きですが、機能はそういう事にしましょう。

基盤:要は、各種の履歴を一旦普通の文字列の配列にしたら、あとは汎用のフィルタリングとかソートとか集計ができる、という感じですかね。

開発:汎用的な処理をした結果をまた、情報の出どころの属性に従って適切に表示できるというところが肝かなと思います。

社長:ところでGShellのコードの行数が2500行を超えましたが。

開発:色々試行錯誤中だったり、Goの事をよく知らないで非効率な書き方をしてたり、ロゴのイメージデータを含んでますからね。正味ではまだ2000行に達してないと思います。

基盤:リモートシェル機能を入れたらどっとデカくなるのでは。

開発:骨組の部分は本体に残しますが、部品的な部分は流石に外に出すことになると思います。

社長:わたし的には昔のPascalのコンパイラが3000行くらいだったとか、Unix v6 のカーネルが10000行足らずだったというのがすごく基準なんですよね。Unixのコードなんてコメントも多かったしドライバとかも含んでいたから、いわゆるカーネル部分はやっぱり数千行だと思うんです。たったそれだけでそれまで世に無かった新しい世界が創造できちゃう。感動的です。

開発:DeleGateは9.9.13で30万行に達しましたが、ネジ一本まで自作でしたし、今日手に入る部品を最大限に活用したら、10分の1くらいでできるかも知れません。

社長:昔はそのネジが世の中になかったんで、仕方がないですけどね。

* * *

広報:そういえばロゴですが、現行のは、Noto Sans の S がやせてて元気がないという指摘がありました。

広報:Courier New の Sが好ましいとのことでしたので、GShまではCourier という版を作ってみました。

社長:うーむ、Gがへなちょこ過ぎる感じですね…

開発:Courier New って大きくするとへなちょこなんですね。小さくても読みやすいというところにデザインの力点があるのかな。

基盤:というかこのメタフォント、やたら微妙で情報量が多そうな気がしますが…

社長:そもそもGは、GoのロゴのGにあやかりたいわけでして…

基盤:Sを画像にして横に引き伸ばしたらどうですかね?

広報:こんな感じでしょうか?

社長:これいい。今度からこれにしましょう。

基盤:画像にしちゃうと使いまわしが面倒にはなりますけどね。

広報:いずれ時間がとれましたら、フォントを自作したいと思います。ただ、よさげなフォント作成ソフトが安いのでも数千円はしまして…

社長:承認。

社長:そういえば、カッチカチだったメロンのお尻がようやく柔らかくなったので、冷蔵庫に入れて冷やします。

基盤:今日の打ち上げはメロン (^-^)/

* * *

社長:さて、どこから始めますかね。

開発:まずは、隣で動いてる GShell にちょっかいを掛けてコマンドを送って結果を得るということろだと思います。rexecですね。これはこないだウェブブラウザからHTTPでちょっかい掛けたのと大して違いません。ただし、結果は標準入出力というストリームだけでなく、実行結果のメタ情報的な標準結果出力的なものも得る。

社長:ちょっかいを受けるほうがgshdということですね。2種類の結果をどう受け取るかですが。単一コネクション上でシーケンシャルに受け取るか、もうひとつコネクションを張るか。シーケンシャルに受け取る場合にはコネクションを元からマルチプレックスして複数のチャネルを作るか、一つのストリームの中にtelnetプロトコルのように特殊なパケット的なものを返すか。ストリーム入出力はリアルタイムですが、実行結果のメタ情報は最後の付け足しというか、並列性はない場合が多いように思います。

社長:マルチプレックスするならDeleGateで作ったYYMUXも使えます。yysh は YYMUX を使って X Window とかファイル転送をバックグラウンドでやっていますね。GoはHTTP2対応なので、HTTP2のマルチプレクサみたいのも使えると思います。

開発:基本、屋上屋的なものはやめたいと思います。並列チャネルはそれぞれ実際に複数のTCPをつないでしまう。FTPのデータコネクション方式ですね。あれは野蛮だと思ってましたが、最近はあれが正しいような気がしています。

開発:経験上、アプリケーション層でのマルチプレックスは、本来意図されている通信のリアルタイム性を、マルチプレクサでのスケジューリングが邪魔をする危険性があります。特にスループットが必要な大きなデータの転送が、レスポンスが重要な小さいデータの転送の邪魔をする。

社長:まあそれはyysh/YYMUXでもありました。ファイル転送中はキーのエコーがとろくなるとか。

開発:あるいは、マルチプレックスのための処理が、ギガビットのデータ転送能力を殺してしまうかも知れない。

社長:out of band で追い越せるんじゃなかったでしたっけ?telnet ではやってますよね。

開発:あれは必ずしもうまくいかなかった記憶が。到達性とか。システム依存だったようにも思います。一方コネクションを用途別に分ければ、一つのコネクション上にオリジナルのひとつながりのデータが流れるので、中間での加工が必要な場合のような用途にも適しています。

社長:ただあれは、別チャネルにはサーバ側に別ポートが必要になりますね。まさかPORTみたいにサーバ側から接続するというのは、外部サーバでの使用を考えるとありえない気がしますが。PASVにしたって、ルータを動的に設定できないと、攻撃者の妨害を受ける危険性があります。

開発:ですのでそこは、同一のポートで受けたいと思います。通常の、ログイン的な主制御のコネクションを受けるし、副コネクションも同じポートで受ける。

社長:複数並列セッションを考えないなら良いですが、複数セッションのサーバが並列に動いていた時に、副コネクションをしかるべきサーバが受け取るのが難しいと思います。

開発:確かプロセス間でファイルディスクリプタを渡せた気がするんですよ。おそらくAF_UNIXのソケット経由で。だから、ポートの入り口の管理プロセス、デーモンが、主コネクションを受けたらGShellサーバプロセスを生成する、副コネクションを受けたらそれを適切なGShellサーバプロセスに渡す。これで良いのではないかと。最悪、入り口デーモンで中継しても良いです。

開発:そもそもこれはサーバ側の問題なので、サーバ側でのファイアウォールフレンドリ性はあまり考慮しなくて良いと思います。

社長:リモートログインの場合には、接続先がおうちのがっちりファイアウォール内ってこともありますよね。

開発:その場合はFTPのPORTコマンド式に、サーバからクライアントに接続すれば良いと思います。

基盤:サーバもクライアントもがっちりファイアウォール内ってこともありますね。

開発:その場合は、リフレクタ方式で対処しても良いですね。リフレクタで中継すれば良いと思います。

開発:なんにしろこれは、副チャネルではUDPを目一杯使う可能性もあるわけです。FTP的に言えば、制御コネクションはTCPで、データコネクションはUDPでっていう事もあり得る。一本のTCP接続の上に主チャネルとマルチプレックスするというのは、その目的にも適合しません。

社長:確かにUDPという話になると、そもそも単一ポートで複数のチャネルを最高速で受け取るのは難しいですね。

開発:サーバに複数のコマンドを先出しで送っておいて、あるいは並列に実行させて、結果が順不同で帰ってくることもありえます。

基盤:えーと、この件はとりあえず先延ばしで良さそうにも思います。

開発:基本、クライアントとサーバ間は自由にTCPコネクションが張れUDPが送れることを想定します。一番性能がでて一番簡単に作れる方式。運用上それが出来ない場合には、TCP上にマルチプレクスするなりで対処する。これは実装を変えずにトランスペアレントにラッピングなりできるはずです。VPN使うという手もあります。

社長:そうですね。ではそういう方向性で。

* * *

開発:ということで、第1版ができました。じゃん。

開発:遠隔コマンドを rex という名前にしました。rexec への懐旧です。rex -serv でサーバになります。-serv オプションがなければクライアントとしてサーバにコマンドを送ります。

社長:これは、SMTPプロトコル互換なんですかね…

開発:先人の知恵に従うということで。と言いますか、SMTPでもFTPでもNNTPでもPOPでも、古典的なインターネットのアプリケーション・プロトコルはみんなこれ流ですよね。HTTPはコネクションレスなのでちょっと違いますが踏襲しているわけです。系統が違うと言えばTelnetくらいですね。

基盤:マルチコアの同一ホスト上で1リクエスト750マイクロ秒もかかるんですか?

開発:最近はホストのiMacもそれなりに仕事てますしね。状況によりけりです。最短だと400マイクロ秒は切ります。

開発:そもそもログをコンソールにだしているので時間がかかりますから、これを時間に影響の無いのだけにしぼれば、250マイクロ秒くらいにはなります。

社長:コマンドはどのようなものがあるのですか?

開発:今の所HELOコマンドだけです。

社長:さて、それでは焦眉のフランクフルトを呼んでみましょう。

開発:750ミリ秒と出ました。

基盤:ローカルホストより2000倍遅いですね。

社長:きっちり250msの3回分ですか。

開発:サーバのオープニングご挨拶を待たずにコマンドを送れば500msにはなると思います。でもそれが限界ですかね。

基盤:SYNパケットにリクエストデータが載せられたらいいのにw

社長:でもTCPが開通した後は250msで帰ってくるのでは。少し待って応答をACKに乗せてくれませんかね。コマンドの応答として250msなら、許容できると思います。

開発:双方の現状をバックグランドで定期的に知らせ合っておけば、いざ人間がコマンドを送るという時に送る情報も少なくて済みますね。

社長:pwd とかは毎回応答を待つ必要も無いですね。

基盤:SMTPじゃなくてFTPにしませんか?

開発:構いませんよ。というか、太古にはFTPコネクションにSMTPコマンドを同居させてたりした時代もあるようです。

社長:putとgetコマンドを所望。NNTP的にPOSTでも。

開発:別チャネルコネクションの問題は置いておきたいので、ダミーでUPLOADとDOWNLOADみたいなコマンドでも作りましょうか。

社長:あと、やってみるまでも無いですがリモートでのファイルの超速コピーを実現するCOPYコマンドとか(^-^)

開発:では刹那的に、100MBのデータをサーバに向けて送信するコマンド、UPLDを作りました。まずはホスト内での転送。

基盤:8GB/s出ますね。

開発:そして問題のフランクフルト…

開発:2.4MB/sでした。

基盤:予想通りですね。

開発:だいぶこま切れになって届いています。受信一回平均7152バイト。

社長:ファイルのsyncコマンドを作りましょうよ。

開発:でもちょっと休みましょう。というか、そろそろロイヤルなリンクスの時間・・・

* * *

基盤:ところで、ポート番号ってなんなんですかね。アプリケーションプロトコルごとにポート番号を代えるより、一つのポートに入ってからどのプロトコルで話すかネゴっても良いと思うのですが。

開発:まあHTTPのUpgradeとかはそれですよね。というかそもそも初期のプロトコルにそういうプロトコルを切り替える変態コマンドを持ってるのがありましたよ。なんでしたっけ?

社長:TCPなら性能上問題ないでしょうけど、UDPだとネゴった結果の状態が保持が難しいし、一つのポートに到着したパケットを誰が使うか、応用層のプログラムで振り分けたら重いんじないですかね。

基盤:でも、iptables みたいなルータも、ただのソフトですよね。普通OSの中にあるでしょうけど。そもそもさっきの例みたいに、ソフトでやったって8GB/s程度の中継はできるみたいですから、UDPの振り分けだってユーザのアプリでやっても問題ないんじゃないでしょうか?とりあえずDeleGateのudprelayをSOCKSで使うとか。

開発:そうですねえ。実際に使えるネットのバンド幅が100MB/sでしかないとすると、ユーザ空間のプログラムで中継をやっても楽勝っぽいですね。まあ優先度が低くされて受信をこぼしちゃうことは増えるかもですが。

社長:そういえば、カーネルの中にあると思われるUDPパケットのバッファっの量って、ユーザが設定できないんでしょうかね。どうもちっちゃい臭い気がするんですが。受信脳力が他のアプリの動作に影響を受けるというか、緩衝能力が低い感じ。

開発:まあ1ギガビット、約100MB/sのネットなら、バッファがわずか100MBあれば、1秒間アプリでの処理が途絶えても持ちこたえる計算ですね。

基盤:それでポート番号ですが、ネットワークが作られ始めたころのもろもろの処理能力とかで、処理を軽くしたかったとか、機能ごとにきっちり縦割りしたい文化とか、硬直的な機能割当てとか、ユーザにはネットワークに手を触れさせない前提とか、とにかくなんか時代錯誤な感じもするんです。

開発:まあ、ポート番号ていらなくね?てすると、IPアドレスだってほとんどはいらなくね?て感じにはなると思いますけどね。アプリケーションレベルで識別する位置な名前だけあれば。末端で相手のアドレスをレゾルブする必要があるのか、とても疑問です。アプリケーションレベルで、プロキシサーバみたいのにやらせておけば良いと思うんです。

社長:番号での識別子には、アクセス制御の面の利点があると思います。IPアドレスやポート番号ならハードウェアでフィルタリングできる。このポートはこのサービスに使うって確定していれば、サービスへのアクセス制御が高速にできる。あるいは、ネットワークのモニタリングにも都合がよいですよね。

開発:やはりシンプルな処理は、専用ハードウェアでやれば、汎用CPUで実行するソフトウェアより2桁以上は高速にできます。私自身もソフトで300マイクロ秒かかる処理が、FPGAで3マイクロ秒でできちゃうとか、感動しました。

基盤:ただ、入り口は一つにして、そこで共通の認証なりアクセス制限をして、その先に進めたほうが、管理も実装も簡単になると思うんですよね。プロトコルごとにてんでにセッションの認証サブプロトコルを作るとか意味わからないです。

開発:そういえば、UnixのプロセスIDとかもう、まるでユニークIDじゃなくなってますよね。例えば1ミリ秒に1プロセス生成したら、1分で6万ですから、あっというまに一回りしてしまう。find -exec grep とか、実際に普通にやるわけですし。

開発:おや、いきなりボギーで発進ですか・・・

* * *

-- 2020-0820 SatoxITS

GShell 0.1.5 − そうだ、IMEを作ろう

開発:さて、適当に作って放置していた端末からのコマンド入力用のラインエディタですが。

社長:今はCの外部コマンドとして仮設ですね。カーソルでヒストリが選べないのが実用には問題です。

開発:それで、Goで書き直そうと思います。いや、書き直すというほどの量では無いのですが。

開発:それでせっかく書くなら、エスケープシーケンスだけでなくて、ひらがな入力なんかもできると良いかなと思うんです。自力で。大げさに言えばIME。

社長:オートマトンで作るんですか。

開発:いえ、べつに普通に関数の階層的な呼び出しで作れば良いと思うんです。ローマ字かな変換とか、今更カスタマイズなんて必要無いと思いますし、もし必要ならプログラムを書き換えれば良いだけです。書き換えたら再起動してプラグインとして取り込み直せば良い。所要3ミリ秒です。プログラムで書けば、正規表現的な状態遷移に束縛されることもありません。環境情報だって適宜取り入れられます。タイミングとかも。

社長:タイムアウトして確定なんていうのもあって良いですね。

社長:それはそうと、3指モールス符号IMEを是非作って下さい。JとKとLだけで入力できるやつ。

* * *

開発:さてそれでローマ字かな変換ですが。そもそもモードというものが必要かというところから。ふつう日本語を入力する状態になったら連続して何十文字も日本語を入力します。もし一文字ごととか単語ごとに、その文字列がカナとして解釈されるべきものだというのを指示するとしたら、面倒です。また、カナに変換することを前提にしてサポートできることもある。例えば子音字の連続による促音への変換みたいなやつですね。だからモードは必要と思います。

社長:CapsLockみたいなものですね。

開発:それと内部表現と外部表現。内部表現的には実際に入力されたキーの列を保持するのが簡単だし、完全にバックトラック可能という意味で面白いですが、外部に表示したり、描画されたデータを取り消すには面倒。ですがこの点は、コンピュータ的には大した処理では無いので、毎回計算し直せばよいかも知れません。

開発:それと実際の端末への表示制御。例えば CR と BS と再描画だけで全部やっちゃうという手もありえますね、行エディタなら。termcap とcursesかいらない。ANSIシーケンスとか使わない。とてもシンプル。いっそ入力も矢印キーでなく ^Pと^N、^F と ^B でやる。

社長:うーん、それはちょっと嫌かな。

開発:でも ^K とか ^I とか ^O は抵抗なく使うんですよね?

基盤:そのへんの文字端末コントロールはGoのパッケージにないんでしょうか?

社長:というか、入力モードを表現するのに、色やカーソル形状を有効利用すると良いと思うのですが。

開発:まあそれはいつでも付け足せることですので、ともかくミニマムな実装でやる、というのが出発点として面白いと思います。実はそれで実用のものができちゃったりする可能性もあります。なにせローカル端末なら、1秒に100万文字とか書けちゃうのかも知れませんから、途中でどう書いたかとかユーザには見えないと思います。私は組み込み系で115200ボー、つまりわずか14.4KBの出力でも、これ式の単純実装で実用してました。これは1秒書くのに0.07ミリ秒、100文字書くのに7ミリ秒ですから、ほぼ人間の目には認識できないです。

社長:まあ、ネタとしては面白いですね。プロトタイプだし。

開発:入力モードですが、たとえばカーソルの入力位置からひとつ開けて*印でも出しておく。

社長:昔 onew ではカーソル近くに[あr]を出すモードがあったような気がします。

基盤:カーソルの色か形状が変わるのがいいけどなぁ・・・

開発:ともかく、1文字毎に1行まるごと再描画、というのをまずやります。これの再描画は、プロンプトの左部分に時刻とか表示するのにも必要な機能だからです。どのみち ^L でやるべき処理ですし。

* * *

社長:そもそも GShell なんて無理やりカナに変換しようとするのが変ですよね。

開発:オートマトン的近視眼だからですかね。

社長;辞書の階層に行く前に字句レベルで気づくべきです。あ、どっちにしても字句レベル以下ですね。

開発:プログラミング言語の言語処理系だと、字句レベルスキャナ、構文レベルパーサ、意味レベルということになるわけですが、普通のIMEは基本的に字句レベル止まりですね。

社長:探索っていう意味では、大昔流行った Prolog とかを IME に持ち込めたら面白いんですけど。処理系は比較的簡単に作れるんですよね。私もちっちゃいのは作りました。

基盤:GoでProlog処理系が作られてますね。

社長:いずれ試してみたいです。

開発:きっと誰かLisp処理系も作ってるでしょうね…

社長:PrologてBNFみたいなもんで、LL1パーサとして使える気がするんです。!でカットしまる必要はあるかも知れませんが。

* * *

開発:ああそれで、今日のお題のIMEなんですが。入力のinsertモードとoverwrite モードの切り替えもしないといけないですね。

基盤:あれ、InsertキーがDelete や Back space キーの近くにあるもんで、しょっちゅう間違えて押してしまって本当に不便ですね。

社長:overwrite にしますよ、いいんですか?本当にいいですね?くらい聞いてきて欲しいですよね。

開発:邪魔なキーの典型として CapsLock と双璧ですよね。

基盤:間違ってoverwriteしてしまった部分を取り戻す undo 機能が必要だと思います。

開発:undo といえば ^Zだと思うんですが、これがコマンドインタプリタだとプロセスをストップさせるキーとかぶっちゃう。かと思うとmacOSのアプリだとコマンドキー+Zだったりするので、これもまた混乱します。

* * *

開発:それで、テキストの内部表現と端末へ出力する文字コードですが、やはりUTF-8かなと思います。

社長:昔OnewとかDeleGateの時代に自作したのはもう腐っちゃってるというか… JISコードとUnicodeの間のマッピングテーブルとか。失われた技術。あまり掘り返したくない気分でなくもありません。

開発:一時期自動判別に血道をあげてましたよね。

基盤:UTF-8関係のサポートパッケージはGoにありますが、中身の文字コードとかサポートされてますかね…

開発:単にエンコード・デコードだけだったりするかもですね。

社長:UTF-8 と聞くと敬愛するケン・トンプソンを思い出すんです。

開発:とりあえず[あr]を出したいんで… echo あ | od -c。そうですか。じゃあとりあえず fprintf(stderr,"[\343\201\202r]");

社長:おー、なんかそれっぽいですね。

基盤:「あr」って何ですか?

開発:あ,はローマ字からひらがなへの変換モードにあることを示します。「アr」はカタカナへの変換。他の事は記憶にありません。

社長:たぶん、Wnn起源の表示じゃないですかね。うーん… どうせやるなら Onew と互換のキーバインディングにしますかね…

開発:それはともかく、とりあえず「Hello, 世界!」を出しましょう。まずは辞書を作りました。

基盤:えらく刹那的な辞書ですねw

開発:とりあえず文字列を左からスキャンして最長マッチを表示して行くという戦略です。

社長:内部表現はあくまでも "konnnichiwa, sekai!" なわけですね。

開発:そもそも、そういう外部表現もあって良いかなという気もしまして。で、この辞書を読み込むと… ああ、なんだかGoで書きたくなってきましたが、とりあえずCで。

開発:ああもう、身体が半分 Go に侵されており、行末のセミコロン忘れまくりですw。で、できました。まずは、Hello, 世界!

基盤:要は表示モードを切り替えてるだけですね。

開発:そうです。オリジナルの入力列で記憶しているので、どちらでも表示可能。間違っていたら、ローマ字表示モードにしてローマ字表記として直せば良いというスグレモノw

社長:新しい世界が見えた気がした(^-^)/

開発:まじめな話、かな文字とか英数字とかASCIIにある記号とかは、一意に決まるものはこれ式で外部表現もできると良いと思うんです。Wikipediaの非欧米文字のページのURLとか悲惨ですよね。

社長:内部表現と外部記憶用表現は一致させて、表示する時に変換するという事ですね。

開発:日本語には正式なかなのローマ字表記があります。これは各国同じようなものではないかと思うんです。

社長:しかし表示の時点でかな漢字変換するとすると一意性が確保できなさそうですが。

開発:まあ漢字とかはUnicodeの文字番号でやるんでしょうね…

基盤:それなら、少なくともHTML中でなら表示できますね。

社長:なんとかローマ字表記の単語を漢字の単語に一意にマップする方法は無いでしょうかね・・・

開発:辞書を同梱すれば良いかもですね。同じ読みで複数の単語を使う場合には、辞書の中でのIDを添字的にするとか。

基盤:シンプルに、ローマ字表記と漢字混じり表記のUTF-8を併記して、表示する時に選択できれば良いようにも思いますが。HTMLのimageのALTテキスト的な。

開発:うーむ確かに。データのサイズとか気にする時代じゃないですしね。

* * *

開発:これでとりあえず一段落と。で gsh から使ってみる。

開発:あれ?

社長:ありがちな試練ですねw

開発:どこで壊れているのかなっと・・・

開発:ああ、自分で壊してるんですね。変数やらを置換する関数でマルチバイト対応に処理をしてない。とりあえずそこをスキップして。こんな感じ。

基盤:出ました。

開発:基盤にある処理系を信頼できるというのは楽で良いですね。昔は上から下まで自作だったので、問題が出ると全ての階層を疑ったものです(笑)

社長:ひととおりの見通しも付きましたので食事に行きましょう。今日はしらす丼かな。

* * *

社長:いやはや今日も外は容赦なく夏でした。

社長:でも写真をとりながら思ったんですが、空に雲がなかったらどんなにつまらないだろうって。

開発:よくぞ地球に生まれけりですね。

基盤:エアコンのおかげで室内は30度代に踏みとどまっています。

経理:おかげでdenkoメータが高値安定状態ですが。

社長:節電は生命を犠牲にしてまでする必要はありませんよ。

* * *

社長:それでJKモールス入力はどうなりましたかね。

開発:あー、ちょっと真面目な話にのめってまして… コマンドの実行結果をどう演算するかということなのですが。やっぱり我々はスタックが好きですね。名前を付けなくて良い変数。確実に開放される領域。シンプルな記法。キューもまた良いですね。コマンド間の通信にも使えちゃう。内蔵パイプです。OSを呼ばなくて良いし、Goルーチンさえ使わないので、内蔵コマンドのパイプライン処理を超高速にできそうです。

社長:問題はコマンドの実行結果をどう標準的に表現するかですね。

開発:基本的には可変長文字列の可変長配列を、可変長配列にすればいいんじゃないかと思うんです。要するにコマンドヒストリと同じ構造です。ただ、これもやはりヒストリと同じ構造ですが、各エントリには時刻とリソース使用量をデフォルトの属性として付ける。あと、それが実行されたワーキングディレクトリ。つまり、いつどこで何がされた結果であるかということです。これはファイルの属性と名前のセットを表すのにもちょうど良いです。

社長:サブジェクト側とオブジェクト側が同じ構造で表現されるというわけですね。

開発:で、少なくとも内蔵コマンドは全てこの標準結果出力的な構造のデータを残すことにします。そうすると、実行結果の間のいろんな演算が、共通の標準的なデータ処理コマンドで処理できるわけです。まあまずは集計とかですが。いずれ、実行結果を見て条件判断して実行するなんていうこともできるかなと思います。

社長:それでJKモールスの方は…

開発:まあ辞書を作ればよいだけですが… 我社のIMEは最長一致で変換表示するという、ある意味無敵のIMEなんで、辞書さえあれば… えーと、日本語モールス符号表… みなさんイロハ順でつらい。Googleさんのを見て手入力です。あー、あ、い、う、え、… やれやれ、入力終了です。

開発:でツーをj、トンをk、文字の終端をl ということにして、、降順ソート。ああ、終端まで完全一致を見てるからソートも必要ないですけどね。

基盤:これは… 「せかい 世界」って定義できないと、辞書を作るのが辛いですね。

開発:それはまあやればできることです。はい、できました。

社長:どれどれ、Hello, kjjjklkjkklkjl!

社長:できた!ううう、長年の夢が叶いました。。。あとはビープ音とか、読み上げが出るとうれしいな。

開発:それはまたおいおい。ところでこれ、終端まで見ずに現在の最長一致を表示させると、それはそれで面白いですね。

社長:とうたすかかかのモールス版みたいですね。

基盤:これは、3指の固定位置でブラインドタッチ入力できるのがメリットということですね。スマホのアプリを作る土地勘が無いのですが、最近買った7インチのディスプレイはタッチパネルなんです。あれで試してはどうかと。

開発:まあそれもおいおいですね。そもそも、モールス符号で定義されてない文字をどうするのかという、技術的では無い難しい問題があると思います。そこで結局ブラインドタッチできなくなっちゃうと、有用性ががっくり落ちてしまいますね。

社長:3指ブラインドを想定した新しい符号体系を考えると良いかもです。4回のタッチを考えれば、3の4乗で81文字を4タッチでいけますからね。

開発:モードを導入すれば、アルファベットも数字も記号も3タッチでイケますよ。かなが難関ですけど。

基盤:複数のキーを同時に押せばいいんじゃないですかね。7×7で49文字を2タッチでイケます。

開発:それは考えた事ありますが、多分タッチが難しい。連続入力には苦しいと思います。まあ、ファンクションキー的には良いかもですが。全押しで1文字消去とか、長押ししたら全消去とか。

社長:21世紀のモールスをめざしちゃおうかな(笑)

-- 2020-0819 SatoxITS

GShell 0.1.4 − scanf

社長:Google IMEはどうもこのGShellを「GSへっl」って変換してしましますね。

開発:ローマ字かな変換中に大文字が来たらローマ字単語モードに移行するんだと思うんですが、子音字が続くとひらがな促音の認識になってしまうんでしょうかね。

社長:ところでわたしは最近MDNで、Mozillaが Miz://a という洒落た表記をしている事に気が付きました。

基盤:ひょっとしてその駄洒落から名前がついたとか?

社長:で、うちも GSell みたいな洒落た表記をしたいなと。ですが、WordPress のブログのタイトル中では、イタリック表記みたいのをしてくれないみたいなんです。シクシク。

基盤:いずれWordPressも卒業する日が来るんじゃないでしょうか。

* * *

開発:さて今日は、これも文字列処理の核心中の核心、scanf // printf に取り組みたいと思います。

社長:私は3度のメシよりscanfが好きでしてね。

開発:で、fmt パッケージの Scanf がそのまま使えるかと思ったのですが、そうはいかないようです。

開発:何がやりたいかというと、指定した構文、できれば正規表現に従って値を取り込み、加工して、指定した構文に従って値を出力する、という操作です。 7/31のブログにはこう残っています。

開発:fmt.Scanf をこれに使うことができません。fmt.Scanf はスキャンした結果をそれぞれの値の型の変数に取り込もうとします。ですので、受け取る値の型も数も実行時まで決まらない場合には、使うことができません。

社長:型付きの値のベクターに取り込めれば良いのにね。

開発:そもそも実際に値に変換したいのではなくて、単にトークンとして認識して分解したいだけ、文字列のままで良いことも多いわけです。それに %d のようなフォーマット指定子は簡便な値のフォーマットの記述として便利ですが、内部的には正規表現の簡略表現として扱うべきだと思います。

社長:我々が欲しいのは単に正規表現に従ったスキャナーなんですね。値の型に従った演算のようなものは、やりたかったらその時に変換すれば良い。

開発:必ず値の演算が必要な、高速な処理が必要な場合には、実際 fmt.Scanf で値に変換してしまうのが適していると思いますが、そのようなケースでは、そもそもその処理はGoで直接書くとか、コマンドからGoプログラムを生成して動的ライブラリにビルドして動的リンクして予備出せばよいのだと思います。

社長:Goのビルドが遅いのがちょっと気になりますが、まあ数分以上かかるような処理に対して、ビルドに何百ミリ秒かかかっても許されますよね。

開発:そもそもが、ディスクが100MB/sでしか読み書き出来ない状況では、そのへんのCPUの処理はネックにならないだろうとも思うんです。まあ、SSD上のデータが対象の場合ですね。

社長:当面は放置で良いと。

開発:それに一般的なスキャナー機能は別途 fmt にもあるようですし、別のパッケージにもあるようですから、それを使うのが良いかなと思います。ただ、自分たちの好きなように使い倒すには、自作したほうが良い可能性はあります。

開発:ということで、何というコマンド名にするかがひとつ問題なのですが、printf というコマンドは一般的にあって、わりと使われている。なのでこれは尊重して避けたい。print というコマンドもあるけれど、これっと echo でいいんじゃね?っていう気はするわけです。なので、scan // print とうペアで行きたいと思います。

社長:まあ、コマンド名は非常に重要ですが、試作段階なので仮置きで良いと思います。

開発:もう一点。scan した結果をどこに取り込んで、どう print やら他の処理に渡すかが問題です。これ、取り込んだ各フィールドに名前を付けたいこともあれば、何番目で良いことも多いわけです。

基盤:囚人番号3みたいな。

社長:まあ、shell では一般的に $1 とか書けますよね。

開発:あれは参照はできますが、代入というか加工は shift くらいしか出来ないように思いますけどね。

社長:mainのargvに代入するだけで pstitile が変えられるとブラボーなんですけどね(^-^)

開発:実際、生で触らせて貰えればほとんどのOSでそうなるんで、Goが提供してないなら、Cで書いて動的リンクして突っ込めば良いかなとは思います。

基盤:man scan すると tcl のscanコマンドが出てきますね。

開発:正統派の仕様ですね… まあいずれ参考にさせてもらいます。私がフォローしたいのはawkで、決められた区切り記号で入力行をフィールドに分割して、フィールド番号で参照するというやつです。

社長:-F オプションですね。

開発:最近はフィールドの区切りを正規表現で書けるみたいなんですが、オリジナルはそんなこと出来ませんでしたよね?

社長:たぶん。

開発:あー、というか awk のマニュアルを読んでると、この仕様を実装すればいいんじゃないかという気もして来ました。

基盤:入力をパターマッチして処理するスクリプト言語は沢山あると思いますが。

開発:何せ私らは80年代にawkで育ちましたからね。その後のことは知らね。

社長:最終的な仕様はおいおい考えれば良いのでは。

開発:では、思いつきでやれるとこまでやります。まずは、デフォルトの値の置き場として、名前の無い文字列の配列を使うことにします。で、ただ単に scan して print する。

社長:汎用で無いだけにシンプルですねw

開発:試しに sort コマンドを追加。

基盤:普通に便利そうな気がして来ました。

開発:ついでに shift コマンドを追加。

社長:シフトローテートもあると良いかも。

開発:では -r オプションとか付けて。

基盤:ローテートするなら右シフトもあると良いのでは。

開発:うーん、この辺は単にGoのスライスのソートなんで、そもそもGo風の記法で書いても良いのかなとは思いますね。

社長:配列のn番目の要素というのは、やはり [n] って書くんですかね。

開発:そこがちょっと謎な感じなんですが、普通のshellではコマンド引数配列の要素を $1 $2 とか書けるわけです。ユーザが定義できる配列型の変数ってあるんでしたっけ?

社長:少くとも自分で使ったことは無いですね(^-^;

開発:予約名を _ で始めるというしきたりに従って、_1 _2 とか書けても良いかもですね。こんな感じ。

社長:おおー、なんだか printf ぽい (^-^)/

基盤:というかさっきから、実行時間の表示がうるさいですね…

開発:ああ、それはオプションにしましょう。

社長:あと、print にフォーマット記述子を書きたいですが、printfはフォーマットと値との対応関係が面倒くさいんですよね。値とくっつけちゃえば?例えば %d(0x100) とか %x(256) みたいな。

基盤:文字列表現のキャストみたいですね。Cでは(type)value でしたけど、Goでは type(value)なのがカッコいいと思います。

開発:そうしましょう。値をどういうフォーマットで表示するかをひとまとまりに記述する。scan側もそうしたいですね。読み込んだ値にどういう明示的な名前を付けるのか。%d(i1) %s(s1) みたいな。

社長:ところで、scan は文字列のスキャンという意味で良いと思うのですが、print という名前はいかがなものかと、以前から思うのですが。

開発:表現の変換という意味では conv とかですかね?内部表現敵な値を外部表現である文字列に変換する。

社長:print という名前は、変換した結果をどこか外部に表示するとこまで暗示してるんですよね。まあ、内部で終わる sprintf というのもありますけど。

* * *

開発:ちょっと値の文字列をスキャンするのに手間取りましたが、適当にでっちあげてこんなふうになりました。

社長:予はほぼ満足じゃ。あとは、カッコの中に式を書いてevalえると良いですね。

開発:それは Go で Eval のが良いと思うのですが、文字列の中のどの部分を Go に Eval させるのか、あまりウザくない感じで指示できると良いと思います。

社長:そこのところ、scan の最大のヤマな感じがしますね。

基盤:まるカッコ記号はあまりに多く使われているので避けたほうが良いかなとは思います。ただshellのコマンドの中でだけ考えるなら、構わないかもしれませんが…

開発:開き直って、カッコ使いまくる。もうコマンドの中にGoの関数呼び出しとか書けると良いのかなという気もします。fmt.Printf("Hello, 世界!¥n") ってコマンドで入れられるとか。

社長:tiny Go インタプリタみたいな感じですね。 代入とかもできちゃう。for 文なんかもかけちゃう。

開発:変数名の表現方法と、インタプリタとバイナリとの値の受け渡しが課題になるでしょうね。

* * *

開発:ああそれで、さっきの conv というコマンドですが。scan だとどうしても、入力を完全に終わるまで出力しないイメージが強いんですが、たとえば %s が1ギガバイトとかいう巨大な入力データかもしれない。で、実は処理というのはそれを出力に吐くだけだったりする場合。入力したら即吐き出すという選択もできると良いと思うのです。で、そういう機能に scan という名前を付けるのはいかがな感じもするわけです。conv かなと。

社長:うーん、それは trans とかのほうが適切な気もしますけどね。それに、それは正規表現というより、文脈自由文法で入力を書くのが良いと思うんです。コマンドで与えられた入力データの構文定義、まあ正規表現+BNFでしょうけど、に従ってその場でパーサというかコンパイラを生成して実行する。私は学生の時にそういうのを作ってました。LL1でインタプリタでしたけど。DeleGateも初期にはそれを使ってたんです。今のコンピュータの処理能力なら、ミリ秒でネイティブコードのコンパイラが生成できると思います。

基盤:Goにはパーサーっていうパッケージもあるようですから、あれが使えるんじゃないでしょうか。

開発:構文定義を与えたらパーサができて、パーサに文字列を与えたら構文木が帰ってくる。というのが良いですね。あるいは、パースの途中のノードの出入りでハンドラ的な処理を起動できる。

社長:もし Go言語のパーサが提供されているんでしたら、マジでGo言語をコマンドとかインタープリティブなスクリプト言語として使うのも良いかもですね。めっちゃ省略記法があるので、自分で文法を書くのが面倒なんじゃないかという気がしますし。

開発:スクリプト言語としてはどうですかね… アドホックな処理には良いかも知れませんが。繰り返し使うスクリプトだとすると。小さいパッケージならGoのコンパイルも一瞬なので、その場でコンパイルしてプラグインして使うっていうのが良いのかなとは思います。

* * *

社長:ゥエルシァでたばこ買ってきました。

基盤:それはレジ袋大ですね。

社長:最近はもうめんどくさいので、大で、って答えるようになりました。

開発:だばこ一箱だけ買って特大でって言ったら店員さんはどんな反応しますかねw

基盤:私は人間ではありませんみたいなバイトさんも多いから動揺しないのでは。

社長:それでちょっとうれしかったのが、会計が5000円ちょうどだったことです。人生で初めてみたいな。

開発:よい事が起こりそうな気がしますね。

社長:幸せの黄色いハンカチの人、亡くなっちゃいましたね。

基盤:高倉健は2014年に亡くなってるようですが。

開発:最近なくなったのは松竹梅の人ですね。

社長:人間120年、下天の内をくらぶれば…

基盤:下天の内って何ですかね?検索。ああ、天界のことのようですね。なんで天界の時間と比べればって平易に言わないんですかね。

開発:そもそも天界とか何ですかね?この宇宙という意味なら120億年超えてますからね。

基盤:人間の世界の天界という意味なら、120百年程度じゃないですか?

社長:でも人間だって、天文学的と言える下天の時間の1億分の1も生きる可能性があるってすごいことですよね。わずか10の8乗の違い。1秒に対して10ナノ秒。1000秒に対して10マイクロ秒。100000秒に対して1ミリ秒。一日に1ミリ秒ってかなりの存在感です。時間てそういう意味でも、空間軸とか質量に比べて平等な感じがします。

基盤:えーと、120億光年はだいたい 120 x 10^8 x 30 * 10000 * 1000 mだから。。

開発:人間のサイズに比べると下天のサイズは桁違いですね。

社長:まあ宇宙が光の速度で膨張してるわけでは無いですよね。質量があれば10桁とか20桁とか速度が違うから、宇宙のサイズも大したこと無いのかも。

開発:天体間の相対速度とかせいぜい100キロ/秒だから、ジャイアントインパクトとか動画的にはずぶずぶずぶって感じですよね。

社長:ひ弱なユーチューバーとかもう死滅してるでしょうけど。

基盤:あれ?でも宇宙の直径は780億光年以上ってWikipediaには。

社長:まあ時間とか距離とか、昔とは変わってるんじゃないですかね。

開発:ちょっと前までは尺貫法でしたし、もっと前は1日の中の時間も相対的でしたしね。

基盤:草木も眠る丑三つ時ってやつですね。

社長:明日は平賀さんの土用のうなぎキャンペーンに乗せられてうなぎでも食べに行きますか。

基盤:2020年の土用の丑の日は7/21と8/2だったそうです。残念。

社長:そうですか… それはそうと昨日カスミで野菜とか買ってきたんですが、異常に高いのに驚きました。私的な感覚からすると、値段が2〜3倍もするんです。スイカなんかも、ちょっとした大きさで3000円超とか。メロンと大して変わらないですね。

基盤:でもこの180円のとうもろこし、やっぱ最高ですね。もぐもぐ。

開発:90度で6分。今のところこれがベストの調理時間です。チンしてる途中から漂ってくる得も言われぬ甘い香りがまた。もぐもぐ。

社長:大好きなサニーレタスもちょっとしたのが250円もしてですね。ふつうのレタスとか冗談のようなこぶし大のスカスカのが並んでてしかも高い。手頃だったサラダ菜を買ってきたのですが、やはりこれ、イタリアンドレッシングでいただくと最高ですね。はぐはぐ。

開発:レタス系の野菜って根っこの部分が最高ですよね。ガシュ。ごちそうさまでしでした。

社長:しかしセロリまるごとはちょっとイタリアンドレッシングでは負けますね。次はマヨネーズかサウザン系のドレッシングも買ってきます。

-- 2020-0818 SatoxITS

// /* GShell-0.1.4 by SatoxITS

GShell // a General purpose Shell built on the top of Golang

*/ /*
Overview
To be written
*/ /*
Index

Implementation
	Structures
		import
		struct
	Main functions
		str-expansion	// macro processor
		finder		// builtin find + du
		grep		// builtin grep + wc + cksum + ...
		plugin		// plugin commands
		system		// external commands
		builtin		// builtin commands
		network		// socket handler
		redirect	// StdIn/Out redireciton
		history		// command history
		rusage		// resouce usage
		encode		// encode / decode
		getline		// line editor
		scanf		// string decomposer
		interpreter	// command interpreter
		main
*/ //
Source Code //
// gsh - Go lang based Shell
// (c) 2020 ITS more Co., Ltd.
// 2020-0807 created by SatoxITS (sato@its-more.jp)

package main // gsh main
// Imported packages // Packages
import (
	"fmt"		// fmt
	"strings"	// strings
	"strconv"	// strconv
	"sort"		// sort
	"time"		// time
	"bufio"		// bufio
	"io/ioutil"	// ioutil
	"os"		// os
	"syscall"	// syscall
	"plugin"	// plugin
	"net"		// net
	"net/http"	// http
	//"html"	// html
	"path/filepath"	// filepath
	"go/types"	// types
	"go/token"	// token
	"encoding/base64"	// base64
	//"gshdata"	// gshell's logo and source code
)

var NAME = "gsh"
var VERSION = "0.1.4"
var DATE = "2020-0818a"
var LINESIZE = (8*1024)
var PATHSEP = ":"	// should be ";" in Windows
var DIRSEP = "/"	// canbe \ in Windows
var GSH_HOME = ".gsh"	// under home directory
var PROMPT = "> "

// -xX logging control
// --A-- all
// --I-- info.
// --D-- debug
// --T-- time and resouce usage
// --W-- warning
// --E-- error
// --F-- fatal error

// Structures
type GCommandHistory struct {
	StartAt		time.Time // command line execution started at
	EndAt		time.Time // command line execution ended at
	ResCode		int       // exit code of (external command)
	CmdError	error     // error string
	OutData		*os.File  // output of the command
	FoundFile	[]string  // output - result of ufind
	Rusagev		[2]syscall.Rusage // Resource consumption, CPU time or so
	CmdId		int       // maybe with identified with arguments or impact
			          // redireciton commands should not be the CmdId
	WorkDir		string    // working directory at start
	CmdLine		string    // command line
}
type GChdirHistory struct {
	Dir		string
	MovedAt		time.Time
}
type CmdMode struct {
	BackGround	bool
}
type PluginInfo struct {
	Spec		*plugin.Plugin
	Addr		plugin.Symbol
	Name		string // maybe relative
	Path		string // this is in Plugin but hidden
}
type GshContext struct {
	StartDir	string	// the current directory at the start
	GetLine		string	// gsh-getline command as a input line editor
	ChdirHistory	[]GChdirHistory // the 1st entry is wd at the start
	gshPA		syscall.ProcAttr
	CommandHistory	[]GCommandHistory
	CmdCurrent	GCommandHistory
	BackGround	bool
	BackGroundJobs	[]int
	LastRusage	syscall.Rusage
	GshHomeDir	string
	TerminalId	int
	CmdTrace	bool // should be [map]
	CmdTime		bool // should be [map]
	PluginFuncs	[]PluginInfo
	iValues		[]string
	iDelimiter	string // field sepearater of print out
	iFormat		string // default print format (of integer)
}

func strBegins(str, pat string)(bool){
	if len(pat) <= len(str){
		yes := str[0:len(pat)] == pat
		//fmt.Printf("--D-- strBegins(%v,%v)=%v\n",str,pat,yes)
		return yes
	}
	//fmt.Printf("--D-- strBegins(%v,%v)=%v\n",str,pat,false)
	return false
}
func isin(what string, list []string) bool {
	for _, v := range list  {
		if v == what {
			return true
		}
	}
	return false
}
func isinX(what string,list[]string)(int){
	for i,v := range list {
		if v == what {
			return i
		}
	}
	return -1
}

func env(opts []string) {
	env := os.Environ()
	if isin("-s", opts){
		sort.Slice(env, func(i,j int) bool {
			return env[i] < env[j]
		})
	}
	for _, v := range env {
		fmt.Printf("%v\n",v)
	}
}

// - rewriting should be context dependent
// - should postpone until the real point of evaluation
// - should rewrite only known notation of symobl
func scanInt(str string)(val int,leng int){
	leng = -1
	for i,ch := range str {
		if '0' <= ch && ch <= '9' {
			leng = i+1
		}else{
			break
		}
	}
	if 0 < leng {
		ival,_ := strconv.Atoi(str[0:leng])
		return ival,leng
	}else{
		return 0,0
	}
}
func substHistory(gshCtx *GshContext,str string,i int,rstr string)(leng int,rst string){
	if len(str[i+1:]) == 0 {
		return 0,rstr
	}
	hi := 0
	histlen := len(gshCtx.CommandHistory)
	if str[i+1] == '!' {
		hi = histlen - 1
		leng = 1
	}else{
		hi,leng = scanInt(str[i+1:])
		if leng == 0 {
			return 0,rstr
		}
		if hi < 0 {
			hi = histlen + hi
		}
	}
	if 0 <= hi && hi < histlen {
		//fmt.Printf("--D-- %v(%c)\n",str[i+leng:],str[i+leng])
		if 1 < len(str[i+leng:]) && str[i+leng:][1] == 'f' {
			leng += 1
			xlist := []string{}
			list := gshCtx.CommandHistory[hi].FoundFile
			for _,v := range list {
				//list[i] = escapeWhiteSP(v)
				xlist = append(xlist,escapeWhiteSP(v))
			}
			//rstr += strings.Join(list," ")
			rstr += strings.Join(xlist," ")
		}else{
			rstr += gshCtx.CommandHistory[hi].CmdLine
		}
	}else{
		leng = 0
	}
	return leng,rstr
}
func escapeWhiteSP(str string)(string){
	if len(str) == 0 {
		return "\\z" // empty, to be ignored
	}
	rstr := ""
	for _,ch := range str {
		switch ch {
			case '\\': rstr += "\\\\"
			case ' ': rstr += "\\s"
			case '\t': rstr += "\\t"
			case '\r': rstr += "\\r"
			case '\n': rstr += "\\n"
			default: rstr += string(ch)
		}
	}
	return rstr
}
func unescapeWhiteSP(str string)(string){ // strip original escapes
	rstr := ""
	for i := 0; i < len(str); i++ {
		ch := str[i]
		if ch == '\\' {
			if i+1 < len(str) {
				switch str[i+1] {
					case 'z':
						continue;
				}
			}
		}
		rstr += string(ch)
	}
	return rstr
}
func unescapeWhiteSPV(strv []string)([]string){ // strip original escapes
	ustrv := []string{}
	for _,v := range strv {
		ustrv = append(ustrv,unescapeWhiteSP(v))
	}
	return ustrv
}

// str-expansion
// - this should be a macro processor
func strsubst(gshCtx *GshContext,str string,histonly bool) string {
	rstr := ""
	inEsc := 0 // escape characer mode 
	for i := 0; i < len(str); i++ {
		//fmt.Printf("--D--Subst %v:%v\n",i,str[i:])
		ch := str[i]
		if inEsc == 0 {
			if ch == '!' {
				leng,xrstr := substHistory(gshCtx,str,i,rstr)
				if 0 < leng {
					i += leng
					rstr = xrstr
					continue
				}
			}
			switch ch {
				case '\\': inEsc = '\\'; continue
				//case '%':  inEsc = '%';  continue
				case '$':
			}
		}
		switch inEsc {
		case '\\':
			switch ch {
				case '\\': ch = '\\'
				case 's': ch = ' '
				case 't': ch = '\t'
				case 'r': ch = '\r'
				case 'n': ch = '\n'
				case 'z': inEsc = 0; continue // empty, to be ignored
			}
			inEsc = 0 
		case '%':
			switch {
				case ch == '%': ch = '%'
				case ch == 'T':
					rstr = rstr + time.Now().Format(time.Stamp)
					continue;
				default:
					// postpone the interpretation
					rstr = rstr + "%" + string(ch)
					continue;
			}
			inEsc = 0
		}
		rstr = rstr + string(ch)
	}
	return rstr
}
func showFileInfo(path string, opts []string) {
	if isin("-l",opts) || isin("-ls",opts) {
		fi, _ := os.Stat(path)
		mod := fi.ModTime()
		date := mod.Format(time.Stamp)
		fmt.Printf("%v %8v %s ",fi.Mode(),fi.Size(),date)
	}
	fmt.Printf("%s",path)
	if isin("-sp",opts) {
		fmt.Printf(" ")
	}else
	if ! isin("-n",opts) {
		fmt.Printf("\n")
	}
}
func userHomeDir()(string,bool){
	/*
	homedir,_ = os.UserHomeDir() // not implemented in older Golang
	*/
	homedir,found := os.LookupEnv("HOME")
	//fmt.Printf("--I-- HOME=%v(%v)\n",homedir,found)
	if !found {
		return "/tmp",found
	}
	return homedir,found
}

func toFullpath(path string) (fullpath string) {
	if path[0] == '/' {
		return path
	}
	pathv := strings.Split(path,DIRSEP)
	switch {
	case pathv[0] == ".":
		pathv[0], _ = os.Getwd()
	case pathv[0] == "..": // all ones should be interpreted
		cwd, _ := os.Getwd()
		ppathv := strings.Split(cwd,DIRSEP)
		pathv[0] = strings.Join(ppathv,DIRSEP)
	case pathv[0] == "~":
		pathv[0],_ = userHomeDir()
	default:
		cwd, _ := os.Getwd()
		pathv[0] = cwd + DIRSEP + pathv[0]
	}
	return strings.Join(pathv,DIRSEP)
}

func IsRegFile(path string)(bool){
	fi, err := os.Stat(path)
	if err == nil {
		fm := fi.Mode()
		return fm.IsRegular();
	}
	return false
}

// Encode / Decode
// Encoder
func Enc(gshCtx *GshContext,argv[]string)(*GshContext){
	file := os.Stdin
	buff := make([]byte,LINESIZE)
	li := 0
	encoder := base64.NewEncoder(base64.StdEncoding,os.Stdout)	
	for li = 0; ; li++ {
		count, err := file.Read(buff)
		if count <= 0 {
			break
		}
		if err != nil {
			break
		}
		encoder.Write(buff[0:count])
	}
	encoder.Close()
	return gshCtx
}
func Dec(gshCtx *GshContext,argv[]string)(*GshContext){
	decoder := base64.NewDecoder(base64.StdEncoding,os.Stdin)	
	li := 0
	buff := make([]byte,LINESIZE)
	for li = 0; ; li++ {
		count, err := decoder.Read(buff)
		if count <= 0 {
			break
		}
		if err != nil {
			break
		}
		os.Stdout.Write(buff[0:count])
	}
	return gshCtx
}
// lnsp [N] [-crlf][-C \\]
func SplitLine(gshCtx *GshContext,argv[]string)(*GshContext){
	reader := bufio.NewReaderSize(os.Stdin,64*1024)
	ni := 0
	toi := 0
	for ni = 0; ; ni++ {
		line, err := reader.ReadString('\n')
		if len(line) <= 0 {
			if err != nil {
			fmt.Fprintf(os.Stderr,"--I-- lnsp %d to %d (%v)\n",ni,toi,err)
			break
			}
		}
		off := 0
		ilen := len(line)
		remlen := len(line)
		for oi := 0; 0 < remlen; oi++ {
			olen := remlen
			addnl := false
			if 72 < olen {
				olen = 72
				addnl = true
			}
			fmt.Fprintf(os.Stderr,"--D-- write %d [%d.%d] %d %d/%d/%d\n",
				toi,ni,oi,off,olen,remlen,ilen)
			toi += 1
			os.Stdout.Write([]byte(line[0:olen]))
			if addnl {
				//os.Stdout.Write([]byte("\r\n"))
				os.Stdout.Write([]byte("\n"))
			}
			line = line[olen:]
			off += olen
			remlen -= olen
		}
	}
	fmt.Fprintf(os.Stderr,"--I-- lnsp %d to %d\n",ni,toi)
	return gshCtx
}

// grep
// "lines", "lin" or "lnp" for "(text) line processor" or "scanner"
// a*,!ab,c, ... sequentioal combination of patterns
// what "LINE" is should be definable
// generic line-by-line processing
// grep [-v]
// cat -n -v
// uniq [-c]
// tail -f
// sed s/x/y/ or awk
// grep with line count like wc
// rewrite contents if specified
func xGrep(gshCtx GshContext,path string,rexpv[]string)(int){
	file, err := os.OpenFile(path,os.O_RDONLY,0)
	if err != nil {
		fmt.Printf("--E-- grep %v (%v)\n",path,err)
		return -1
	}
	defer file.Close()
	if gshCtx.CmdTrace { fmt.Printf("--I-- grep %v %v\n",path,rexpv) }
	//reader := bufio.NewReaderSize(file,LINESIZE)
	reader := bufio.NewReaderSize(file,80)
	li := 0
	found := 0
	for li = 0; ; li++ {
		line, err := reader.ReadString('\n')
		if len(line) <= 0 {
			break
		}
		if 150 < len(line) {
			// maybe binary
			break;
		}
		if err != nil {
			break
		}
		if 0 <= strings.Index(string(line),rexpv[0]) {
			found += 1
			fmt.Printf("%s:%d: %s",path,li,line)
		}
	}
		//fmt.Printf("total %d lines %s\n",li,path)
	//if( 0 < found ){ fmt.Printf("((found %d lines %s))\n",found,path); }
	return found
}

// Finder
// finding files with it name and contents
// file names are ORed
// show the content with %x fmt list
// ls -R
// tar command by adding output
type fileSum struct {
	Err	int64	// access error or so
	Size	int64	// content size
	DupSize	int64	// content size from hard links
	Blocks	int64	// number of blocks (of 512 bytes)
	DupBlocks int64	// Blocks pointed from hard links
	HLinks	int64	// hard links
	Words	int64
	Lines	int64
	Files	int64
	Dirs	int64	// the num. of directories
	SymLink	int64
	Flats	int64	// the num. of flat files
	MaxDepth	int64
	MaxNamlen	int64	// max. name length
	nextRepo	time.Time
}
func showFusage(dir string,fusage *fileSum){
	bsume := float64(((fusage.Blocks-fusage.DupBlocks)/2)*1024)/1000000.0
	//bsumdup := float64((fusage.Blocks/2)*1024)/1000000.0

	fmt.Printf("%v: %v files (%vd %vs %vh) %.6f MB (%.2f MBK)\n",
		dir,
		fusage.Files,
		fusage.Dirs,
		fusage.SymLink,
		fusage.HLinks,
		float64(fusage.Size)/1000000.0,bsume);
}
const (
	S_IFMT    = 0170000
	S_IFCHR   = 0020000
	S_IFDIR   = 0040000
	S_IFREG   = 0100000
	S_IFLNK   = 0120000
	S_IFSOCK  = 0140000
)
func cumFinfo(fsum *fileSum, path string, staterr error, fstat syscall.Stat_t, argv[]string,verb bool)(*fileSum){
	now := time.Now()
	if time.Second <= now.Sub(fsum.nextRepo) {
		if !fsum.nextRepo.IsZero(){
			tstmp := now.Format(time.Stamp)
			showFusage(tstmp,fsum)
		}
		fsum.nextRepo = now.Add(time.Second)
	}
	if staterr != nil {
		fsum.Err += 1
		return fsum
	}
	fsum.Files += 1
	if 1 < fstat.Nlink {
		// must count only once...
		// at least ignore ones in the same directory
		//if finfo.Mode().IsRegular() {
		if (fstat.Mode & S_IFMT) == S_IFREG {
			fsum.HLinks += 1
			fsum.DupBlocks += int64(fstat.Blocks)
			//fmt.Printf("---Dup HardLink %v %s\n",fstat.Nlink,path)
		}
	}
	//fsum.Size += finfo.Size()
	fsum.Size += fstat.Size
	fsum.Blocks += int64(fstat.Blocks) 
	//if verb { fmt.Printf("(%8dBlk) %s",fstat.Blocks/2,path) }
	if isin("-ls",argv){
		//if verb { fmt.Printf("%4d %8d ",fstat.Blksize,fstat.Blocks) }
//		fmt.Printf("%d\t",fstat.Blocks/2)
	}
	//if finfo.IsDir()
	if (fstat.Mode & S_IFMT) == S_IFDIR {
		fsum.Dirs += 1
	}
	//if (finfo.Mode() & os.ModeSymlink) != 0 
	if (fstat.Mode & S_IFMT) == S_IFLNK {
		//if verb { fmt.Printf("symlink(%v,%s)\n",fstat.Mode,finfo.Name()) }
		//{ fmt.Printf("symlink(%o,%s)\n",fstat.Mode,finfo.Name()) }
		fsum.SymLink += 1
	}
	return fsum
}
func xxFindEntv(gshCtx GshContext,depth int,total *fileSum,dir string, dstat syscall.Stat_t, ei int, entv []string,npatv[]string,argv[]string)(GshContext,*fileSum){
	nols := isin("-grep",argv)
	// sort entv
	/*
	if isin("-t",argv){
		sort.Slice(filev, func(i,j int) bool {
			return 0 < filev[i].ModTime().Sub(filev[j].ModTime())
		})
	}
	*/
		/*
		if isin("-u",argv){
			sort.Slice(filev, func(i,j int) bool {
				return 0 < filev[i].AccTime().Sub(filev[j].AccTime())
			})
		}
		if isin("-U",argv){
			sort.Slice(filev, func(i,j int) bool {
				return 0 < filev[i].CreatTime().Sub(filev[j].CreatTime())
			})
		}
		*/
	/*
	if isin("-S",argv){
		sort.Slice(filev, func(i,j int) bool {
			return filev[j].Size() < filev[i].Size()
		})
	}
	*/
	for _,filename := range entv {
		for _,npat := range npatv {
			match := true
			if npat == "*" {
				match = true
			}else{
				match, _ = filepath.Match(npat,filename)
			}
			path := dir + DIRSEP + filename
			if !match {
				continue
			}
			var fstat syscall.Stat_t
			staterr := syscall.Lstat(path,&fstat)
			if staterr != nil {
				if !isin("-w",argv){fmt.Printf("ufind: %v\n",staterr) }
				continue;
			}
			if isin("-du",argv) && (fstat.Mode & S_IFMT) == S_IFDIR {
				// should not show size of directory in "-du" mode ...
			}else
			if !nols && !isin("-s",argv) && (!isin("-du",argv) || isin("-a",argv)) {
				if isin("-du",argv) {
					fmt.Printf("%d\t",fstat.Blocks/2)
				}
				showFileInfo(path,argv)
			}
			if true { // && isin("-du",argv)
				total = cumFinfo(total,path,staterr,fstat,argv,false)
			}
			/*
			if isin("-wc",argv) {
			}
			*/
			x := isinX("-grep",argv); // -grep will be convenient like -ls
			if 0 <= x && x+1 <= len(argv) { // -grep will be convenient like -ls
				if IsRegFile(path){
					found := xGrep(gshCtx,path,argv[x+1:])
					if 0 < found {
						foundv := gshCtx.CmdCurrent.FoundFile
						if len(foundv) < 10 {
							gshCtx.CmdCurrent.FoundFile =
							append(gshCtx.CmdCurrent.FoundFile,path)
						}
					}
				}
			}
			if !isin("-r0",argv) { // -d 0 in du, -depth n in find
				//total.Depth += 1
				if (fstat.Mode & S_IFMT) == S_IFLNK {
					continue
				}
				if dstat.Rdev != fstat.Rdev {
					fmt.Printf("--I-- don't follow differnet device %v(%v) %v(%v)\n",
						dir,dstat.Rdev,path,fstat.Rdev)
				}
				if (fstat.Mode & S_IFMT) == S_IFDIR {
					gshCtx,total = xxFind(gshCtx,depth+1,total,path,npatv,argv)
				}
			}
		}
	}
	return gshCtx,total
}
func xxFind(gshCtx GshContext,depth int,total *fileSum,dir string,npatv[]string,argv[]string)(GshContext,*fileSum){
	nols := isin("-grep",argv)
	dirfile,oerr := os.OpenFile(dir,os.O_RDONLY,0)
	if oerr == nil {
		//fmt.Printf("--I-- %v(%v)[%d]\n",dir,dirfile,dirfile.Fd())
		defer dirfile.Close()
	}else{
	}

	prev := *total
	var dstat syscall.Stat_t
	staterr := syscall.Lstat(dir,&dstat) // should be flstat

	if staterr != nil {
		if !isin("-w",argv){ fmt.Printf("ufind: %v\n",staterr) }
		return gshCtx,total
	}
		//filev,err := ioutil.ReadDir(dir)
		//_,err := ioutil.ReadDir(dir) // ReadDir() heavy and bad for huge directory
		/*
		if err != nil {
			if !isin("-w",argv){ fmt.Printf("ufind: %v\n",err) }
			return total
		}
		*/
	if depth == 0 {
		total = cumFinfo(total,dir,staterr,dstat,argv,true)
		if !nols && !isin("-s",argv) && (!isin("-du",argv) || isin("-a",argv)) {
			showFileInfo(dir,argv)
		}
	}
	// it it is not a directory, just scan it and finish

	for ei := 0; ; ei++ {
		entv,rderr := dirfile.Readdirnames(8*1024)
		if len(entv) == 0 || rderr != nil {
			//if rderr != nil { fmt.Printf("[%d] len=%d (%v)\n",ei,len(entv),rderr) }
			break
		}
		if 0 < ei {
			fmt.Printf("--I-- xxFind[%d] %d large-dir: %s\n",ei,len(entv),dir)
		}
		gshCtx,total = xxFindEntv(gshCtx,depth,total,dir,dstat,ei,entv,npatv,argv)
	}
	if isin("-du",argv) {
		// if in "du" mode
		fmt.Printf("%d\t%s\n",(total.Blocks-prev.Blocks)/2,dir)
	}
	return gshCtx,total
}

// {ufind|fu|ls} [Files] [// Names] [-- Expressions]
//  Files is "." by default
//  Names is "*" by default
//  Expressions is "-print" by default for "ufind", or -du for "fu" command
func xFind(gshCtx GshContext,argv[]string)(GshContext){
	if 0 < len(argv) && strBegins(argv[0],"?"){
		showFound(gshCtx,argv)
		return gshCtx
	}
	var total = fileSum{}
	npats := []string{}
	for _,v := range argv {
		if 0 < len(v) && v[0] != '-' {
			npats = append(npats,v)
		}
		if v == "//" { break }
		if v == "--" { break }
		if v == "-grep" { break }
		if v == "-ls" { break }
	}
	if len(npats) == 0 {
		npats = []string{"*"}
	}
	cwd := "."
	// if to be fullpath ::: cwd, _ := os.Getwd()
	if len(npats) == 0 { npats = []string{"*"} }
	gshCtx,fusage := xxFind(gshCtx,0,&total,cwd,npats,argv)
	if !isin("-grep",argv) {
		showFusage("total",fusage)
	}
	return gshCtx
}

func showFiles(files[]string){
	sp := ""
	for i,file := range files {
		if 0 < i { sp = " " } else { sp = "" }
		fmt.Printf(sp+"%s",escapeWhiteSP(file))
	}
}
func showFound(gshCtx GshContext, argv[]string){
	for i,v := range gshCtx.CommandHistory {
		if 0 < len(v.FoundFile) {
			fmt.Printf("!%d (%d) ",i,len(v.FoundFile))
			if isin("-ls",argv){
				fmt.Printf("\n")
				for _,file := range v.FoundFile {
					fmt.Printf("") //sub number?
					showFileInfo(file,argv)
				}
			}else{
				showFiles(v.FoundFile)
				fmt.Printf("\n")
			}
		}
	}
}

func showMatchFile(filev []os.FileInfo, npat,dir string, argv[]string)(string,bool){
	fname := ""
	found := false
	for _,v := range filev {
		match, _ := filepath.Match(npat,(v.Name()))
		if match {
			fname = v.Name()
			found = true
			//fmt.Printf("[%d] %s\n",i,v.Name())
			showIfExecutable(fname,dir,argv)
		}
	}
	return fname,found
}
func showIfExecutable(name,dir string,argv[]string)(ffullpath string,ffound bool){
	var fullpath string
	if strBegins(name,DIRSEP){
		fullpath = name
	}else{
		fullpath = dir + DIRSEP + name
	}
	fi, err := os.Stat(fullpath)
	if err != nil {
		fullpath = dir + DIRSEP + name + ".go"
		fi, err = os.Stat(fullpath)
	}
	if err == nil {
		fm := fi.Mode()
		if fm.IsRegular() {
		  // R_OK=4, W_OK=2, X_OK=1, F_OK=0
		  if syscall.Access(fullpath,5) == nil {
			ffullpath = fullpath
			ffound = true
			if ! isin("-s", argv) {
				showFileInfo(fullpath,argv)
			}
		  }
		}
	}
	return ffullpath, ffound
}
func which(list string, argv []string) (fullpathv []string, itis bool){
	if len(argv) <= 1 {
		fmt.Printf("Usage: which comand [-s] [-a] [-ls]\n")
		return []string{""}, false
	}
	path := argv[1]
	if strBegins(path,"/") {
		// should check if excecutable?
		_,exOK := showIfExecutable(path,"/",argv)
		fmt.Printf("--D-- %v exOK=%v\n",path,exOK)
		return []string{path},exOK
	}
	pathenv, efound := os.LookupEnv(list)
	if ! efound {
		fmt.Printf("--E-- which: no \"%s\" environment\n",list)
		return []string{""}, false
	}
	showall := isin("-a",argv) || 0 <= strings.Index(path,"*")
	dirv := strings.Split(pathenv,PATHSEP)
	ffound := false
	ffullpath := path
	for _, dir := range dirv {
		if 0 <= strings.Index(path,"*") { // by wild-card
			list,_ := ioutil.ReadDir(dir)
			ffullpath, ffound = showMatchFile(list,path,dir,argv)
		}else{
			ffullpath, ffound = showIfExecutable(path,dir,argv)
		}
		//if ffound && !isin("-a", argv) {
		if ffound && !showall {
			break;
		}
	}
	return []string{ffullpath}, ffound
}

func stripLeadingWSParg(argv[]string)([]string){
	for ; 0 < len(argv); {
		if len(argv[0]) == 0 {
			argv = argv[1:]
		}else{
			break
		}
	}
	return argv
}
func xEval(argv []string, nlend bool){
	argv = stripLeadingWSParg(argv)
	if len(argv) == 0 {
		fmt.Printf("eval [%%format] [Go-expression]\n")
		return
	}
	pfmt := "%v"
	if argv[0][0] == '%' {
		pfmt = argv[0]
		argv = argv[1:]
	}
	if len(argv) == 0 {
		return
	}
	gocode := strings.Join(argv," ");
	//fmt.Printf("eval [%v] [%v]\n",pfmt,gocode)
	fset := token.NewFileSet()
	rval, _ := types.Eval(fset,nil,token.NoPos,gocode)
	fmt.Printf(pfmt,rval.Value)
	if nlend { fmt.Printf("\n") }
}

func getval(name string) (found bool, val int) {
	/* should expand the name here */
	if name == "gsh.pid" {
		return true, os.Getpid()
	}else
	if name == "gsh.ppid" {
		return true, os.Getppid()
	}
	return false, 0
}

func echo(argv []string, nlend bool){
	for ai := 1; ai < len(argv); ai++ {
		if 1 < ai {
			fmt.Printf(" ");
		}
		arg := argv[ai]
		found, val := getval(arg)
		if found {
			fmt.Printf("%d",val)
		}else{
			fmt.Printf("%s",arg)
		}
	}
	if nlend {
		fmt.Printf("\n");
	}
}

func resfile() string {
	return "gsh.tmp"
}
//var resF *File
func resmap() {
	//_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, os.ModeAppend)
	// https://developpaper.com/solution-to-golang-bad-file-descriptor-problem/
	_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		fmt.Printf("refF could not open: %s\n",err)
	}else{
		fmt.Printf("refF opened\n")
	}
}

// External commands
func excommand(gshCtx GshContext, exec bool, argv []string) (GshContext, bool) {
	if gshCtx.CmdTrace { fmt.Printf("--I-- excommand[%v](%v)\n",exec,argv) }

	gshPA := gshCtx.gshPA
	fullpathv, itis := which("PATH",[]string{"which",argv[0],"-s"})
	if itis == false {
		return gshCtx, true
	}
	fullpath := fullpathv[0]
	argv = unescapeWhiteSPV(argv)
	if 0 < strings.Index(fullpath,".go") {
		nargv := argv // []string{}
		gofullpathv, itis := which("PATH",[]string{"which","go","-s"})
		if itis == false {
			fmt.Printf("--F-- Go not found\n")
			return gshCtx, true
		}
		gofullpath := gofullpathv[0]
		nargv = []string{ gofullpath, "run", fullpath }
		fmt.Printf("--I-- %s {%s %s %s}\n",gofullpath,
			nargv[0],nargv[1],nargv[2])
		if exec {
			syscall.Exec(gofullpath,nargv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(gofullpath,nargv,&gshPA)
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage)
				gshCtx.LastRusage = rusage
				gshCtx.CmdCurrent.Rusagev[1] = rusage
			}
		}
	}else{
		if exec {
			syscall.Exec(fullpath,argv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(fullpath,argv,&gshPA)
			//fmt.Printf("[%d]\n",pid); // '&' to be background
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage);
				gshCtx.LastRusage = rusage
				gshCtx.CmdCurrent.Rusagev[1] = rusage
			}
		}
	}
	return gshCtx, false
}

// Builtin Commands
func sleep(gshCtx GshContext, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Sleep 100ms, 100us, 100ns, ...\n")
		return
	}
	duration := argv[1];
	d, err := time.ParseDuration(duration)
	if err != nil {
		d, err = time.ParseDuration(duration+"s")
		if err != nil {
			fmt.Printf("duration ? %s (%s)\n",duration,err)
			return
		}
	}
	//fmt.Printf("Sleep %v\n",duration)
	time.Sleep(d)
	if 0 < len(argv[2:]) {
		gshellv(gshCtx, argv[2:])
	}
}
func repeat(gshCtx GshContext, argv []string) {
	if len(argv) < 2 {
		return
	}
	start0 := time.Now()
	for ri,_ := strconv.Atoi(argv[1]); 0 < ri; ri-- {
		if 0 < len(argv[2:]) {
			//start := time.Now()
			gshellv(gshCtx, argv[2:])
			end := time.Now()
			elps := end.Sub(start0);
			if( 1000000000 < elps ){
				fmt.Printf("(repeat#%d %v)\n",ri,elps);
			}
		}
	}
}

func gen(gshCtx GshContext, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: %s N\n",argv[0])
		return
	}
	// should br repeated by "repeat" command
	count, _ := strconv.Atoi(argv[1])
	fd := gshPA.Files[1] // Stdout
	file := os.NewFile(fd,"internalStdOut")
	fmt.Printf("--I-- Gen. Count=%d to [%d]\n",count,file.Fd())
	//buf := []byte{}
	outdata := "0123 5678 0123 5678 0123 5678 0123 5678\r"
	for gi := 0; gi < count; gi++ {
		file.WriteString(outdata)
	}
	//file.WriteString("\n")
	fmt.Printf("\n(%d B)\n",count*len(outdata));
	//file.Close()
}

// network
// -s, -si, -so // bi-directional, source, sync (maybe socket)
func sconnect(gshCtx GshContext, inTCP bool, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: -s [host]:[port[.udp]]\n")
		return
	}
	remote := argv[1]
	if remote == ":" { remote = "0.0.0.0:9999" }

	if inTCP { // TCP
		dport, err := net.ResolveTCPAddr("tcp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		conn, err := net.DialTCP("tcp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()
		fmt.Printf("Socket: connected to %s, socket[%d]\n",remote,fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}else{
		//dport, err := net.ResolveUDPAddr("udp4",remote);
		dport, err := net.ResolveUDPAddr("udp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		//conn, err := net.DialUDP("udp4",nil,dport)
		conn, err := net.DialUDP("udp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()

		ar := conn.RemoteAddr()
		//al := conn.LocalAddr()
		fmt.Printf("Socket: connected to %s [%s], socket[%d]\n",
			remote,ar.String(),fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}
}
func saccept(gshCtx GshContext, inTCP bool, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: -ac [host]:[port[.udp]]\n")
		return
	}
	local := argv[1]
	if local == ":" { local = "0.0.0.0:9999" }
	if inTCP { // TCP
		port, err := net.ResolveTCPAddr("tcp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Listen at %s...\n",local);
		sconn, err := net.ListenTCP("tcp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Accepting at %s...\n",local);
		aconn, err := sconn.AcceptTCP()
		if err != nil {
			fmt.Printf("Accept error: %s (%s)\n",local,err)
			return
		}
		file, _ := aconn.File()
		fd := file.Fd()
		fmt.Printf("Accepted TCP at %s [%d]\n",local,fd)

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[0] = savfd

		sconn.Close();
		aconn.Close();
		file.Close();
	}else{
		//port, err := net.ResolveUDPAddr("udp4",local);
		port, err := net.ResolveUDPAddr("udp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		fmt.Printf("Listen UDP at %s...\n",local);
		//uconn, err := net.ListenUDP("udp4", port)
		uconn, err := net.ListenUDP("udp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		file, _ := uconn.File()
		fd := file.Fd()
		ar := uconn.RemoteAddr()
		remote := ""
		if ar != nil { remote = ar.String() }
		if remote == "" { remote = "?" }

		// not yet received
		//fmt.Printf("Accepted at %s [%d] <- %s\n",local,fd,"")

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		savenv := gshPA.Env
		gshPA.Env = append(savenv, "REMOTE_HOST="+remote)
		gshellv(gshCtx, argv[2:])
		gshPA.Env = savenv
		gshPA.Files[0] = savfd

		uconn.Close();
		file.Close();
	}
}

// empty line command
func xPwd(gshCtx GshContext, argv[]string){
	// execute context command, pwd + date
	// context notation, representation scheme, to be resumed at re-login
	cwd, _ := os.Getwd()
	switch {
	case isin("-a",argv):
		xChdirHistory(gshCtx,argv)
	case isin("-ls",argv):
		showFileInfo(cwd,argv)
	default:
		fmt.Printf("%s\n",cwd)
	case isin("-v",argv): // obsolete emtpy command
		t := time.Now()
		date := t.Format(time.UnixDate)
		exe, _ := os.Executable()
		host, _ := os.Hostname()
		fmt.Printf("{PWD=\"%s\"",cwd)
		fmt.Printf(" HOST=\"%s\"",host)
		fmt.Printf(" DATE=\"%s\"",date)
		fmt.Printf(" TIME=\"%s\"",t.String())
		fmt.Printf(" PID=\"%d\"",os.Getpid())
		fmt.Printf(" EXE=\"%s\"",exe)
		fmt.Printf("}\n")
	}
}

// History
// these should be browsed and edited by HTTP browser
// show the time of command with -t and direcotry with -ls
// openfile-history, sort by -a -m -c
// sort by elapsed time by -t -s
// search by "more" like interface
// edit history
// sort history, and wc or uniq
// CPU and other resource consumptions
// limit showing range (by time or so)
// export / import history
func xHistory(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	for i, v := range gshCtx.CommandHistory {
		// exclude commands not to be listed by default
		// internal commands may be suppressed by default
		if v.CmdLine == "" && !isin("-a",argv) {
			continue;
		}
		if !isin("-n",argv){ // like "fc"
			fmt.Printf("!%-3d ",i)
		}
		if isin("-v",argv){
			fmt.Println(v) // should be with it date
		}else{
			if isin("-l",argv) || isin("-l0",argv) {
				elps := v.EndAt.Sub(v.StartAt);
				start := v.StartAt.Format(time.Stamp)
				fmt.Printf("%s %11v/t ",start,elps)
			}
			if isin("-l",argv) && !isin("-l0",argv){
				fmt.Printf("%v",Rusagef("%t %u %s",argv,v.Rusagev))
			}
			if isin("-ls",argv){
				fmt.Printf("@%s ",v.WorkDir)
				// show the FileInfo of the output command??
			}
			fmt.Printf("%s",v.CmdLine)
			fmt.Printf("\n")
		}
	}
	return gshCtx
}
// !n - history index
func searchHistory(gshCtx GshContext, gline string) (string, bool, bool){
	if gline[0] == '!' {
		hix, err := strconv.Atoi(gline[1:])
		if err != nil {
			fmt.Printf("--E-- (%s : range)\n",hix)
			return "", false, true
		}
		if hix < 0 || len(gshCtx.CommandHistory) <= hix {
			fmt.Printf("--E-- (%d : out of range)\n",hix)
			return "", false, true
		}
		return gshCtx.CommandHistory[hix].CmdLine, false, false
	}
	// search
	//for i, v := range gshCtx.CommandHistory {
	//}
	return gline, false, false
}

// temporary adding to PATH environment
// cd name -lib for LD_LIBRARY_PATH
// chdir with directory history (date + full-path)
// -s for sort option (by visit date or so)
func xChdirHistory(gshCtx GshContext, argv []string){
	for i, v := range gshCtx.ChdirHistory {
		fmt.Printf("!%d ",i)
		fmt.Printf("%v ",v.MovedAt.Format(time.Stamp))
		showFileInfo(v.Dir,argv)
	}
}
func xChdir(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	cdhist := gshCtx.ChdirHistory
	if isin("?",argv ) || isin("-t",argv) {
		xChdirHistory(gshCtx,argv)
		return gshCtx
	}
	pwd, _ := os.Getwd()
	dir := ""
	if len(argv) <= 1 {
		dir = toFullpath("~")
	}else{
		dir = argv[1]
	}
	if strBegins(dir,"!") {
		if dir == "!0" {
			dir = gshCtx.StartDir
		}else
		if dir == "!!" {
			index := len(cdhist) - 1
			if 0 < index { index -= 1 }
			dir = cdhist[index].Dir
		}else{
			index, err := strconv.Atoi(dir[1:])
			if err != nil {
				fmt.Printf("--E-- xChdir(%v)\n",err)
				dir = "?"
			}else
			if len(gshCtx.ChdirHistory) <= index {
				fmt.Printf("--E-- xChdir(history range error)\n")
				dir = "?"
			}else{
				dir = cdhist[index].Dir
			}
		}
	}
	if dir != "?" {
		err := os.Chdir(dir)
		if err != nil {
			fmt.Printf("--E-- xChdir(%s)(%v)\n",argv[1],err)
		}else{
			cwd, _ := os.Getwd()
			if cwd != pwd {
				hist1 := GChdirHistory { }
				hist1.Dir = cwd
				hist1.MovedAt = time.Now()
				gshCtx.ChdirHistory = append(cdhist,hist1)
			}
		}
	}
	if isin("-ls",argv){
		cwd, _ := os.Getwd()
		showFileInfo(cwd,argv);
	}
	return gshCtx
}
func TimeValSub(tv1 *syscall.Timeval, tv2 *syscall.Timeval){
	*tv1 = syscall.NsecToTimeval(tv1.Nano() - tv2.Nano())  
}
func RusageSubv(ru1, ru2 [2]syscall.Rusage)([2]syscall.Rusage){
	TimeValSub(&ru1[0].Utime,&ru2[0].Utime)
	TimeValSub(&ru1[0].Stime,&ru2[0].Stime)
	TimeValSub(&ru1[1].Utime,&ru2[1].Utime)
	TimeValSub(&ru1[1].Stime,&ru2[1].Stime)
	return ru1
}
func TimeValAdd(tv1 syscall.Timeval, tv2 syscall.Timeval)(syscall.Timeval){
	tvs := syscall.NsecToTimeval(tv1.Nano() + tv2.Nano())  
	return tvs
}
/*
func RusageAddv(ru1, ru2 [2]syscall.Rusage)([2]syscall.Rusage){
	TimeValAdd(ru1[0].Utime,ru2[0].Utime)
	TimeValAdd(ru1[0].Stime,ru2[0].Stime)
	TimeValAdd(ru1[1].Utime,ru2[1].Utime)
	TimeValAdd(ru1[1].Stime,ru2[1].Stime)
	return ru1
}
*/

// Resource Usage
func Rusagef(fmtspec string, argv []string, ru [2]syscall.Rusage)(string){
	ut := TimeValAdd(ru[0].Utime,ru[1].Utime)
	st := TimeValAdd(ru[0].Stime,ru[1].Stime)
	fmt.Printf("%d.%06ds/u ",ut.Sec,ut.Usec) //ru[1].Utime.Sec,ru[1].Utime.Usec)
	fmt.Printf("%d.%06ds/s ",st.Sec,st.Usec) //ru[1].Stime.Sec,ru[1].Stime.Usec)
	return ""
}
func Getrusagev()([2]syscall.Rusage){
	var ruv = [2]syscall.Rusage{}
	syscall.Getrusage(syscall.RUSAGE_SELF,&ruv[0])
	syscall.Getrusage(syscall.RUSAGE_CHILDREN,&ruv[1])
	return ruv
}
func showRusage(what string,argv []string, ru *syscall.Rusage){
	fmt.Printf("%s: ",what);
	fmt.Printf("Usr=%d.%06ds",ru.Utime.Sec,ru.Utime.Usec)
	fmt.Printf(" Sys=%d.%06ds",ru.Stime.Sec,ru.Stime.Usec)
	fmt.Printf(" Rss=%vB",ru.Maxrss)
	if isin("-l",argv) {
		fmt.Printf(" MinFlt=%v",ru.Minflt)
		fmt.Printf(" MajFlt=%v",ru.Majflt)
		fmt.Printf(" IxRSS=%vB",ru.Ixrss)
		fmt.Printf(" IdRSS=%vB",ru.Idrss)
		fmt.Printf(" Nswap=%vB",ru.Nswap)
	fmt.Printf(" Read=%v",ru.Inblock)
	fmt.Printf(" Write=%v",ru.Oublock)
	}
	fmt.Printf(" Snd=%v",ru.Msgsnd)
	fmt.Printf(" Rcv=%v",ru.Msgrcv)
	//if isin("-l",argv) {
		fmt.Printf(" Sig=%v",ru.Nsignals)
	//}
	fmt.Printf("\n");
}
func xTime(gshCtx GshContext, argv[]string)(GshContext,bool){
	if 2 <= len(argv){
		gshCtx.LastRusage = syscall.Rusage{}
		rusagev1 := Getrusagev()
		xgshCtx, fin := gshellv(gshCtx,argv[1:])
		rusagev2 := Getrusagev()
		gshCtx = xgshCtx
		showRusage(argv[1],argv,&gshCtx.LastRusage)
		rusagev := RusageSubv(rusagev2,rusagev1)
		showRusage("self",argv,&rusagev[0])
		showRusage("chld",argv,&rusagev[1])
		return gshCtx, fin
	}else{
		rusage:= syscall.Rusage {}
		syscall.Getrusage(syscall.RUSAGE_SELF,&rusage)
		showRusage("self",argv, &rusage)
		syscall.Getrusage(syscall.RUSAGE_CHILDREN,&rusage)
		showRusage("chld",argv, &rusage)
		return gshCtx, false
	}
}
func xJobs(gshCtx GshContext, argv[]string){
	fmt.Printf("%d Jobs\n",len(gshCtx.BackGroundJobs))
	for ji, pid := range gshCtx.BackGroundJobs {
		//wstat := syscall.WaitStatus {0}
		rusage := syscall.Rusage {}
		//wpid, err := syscall.Wait4(pid,&wstat,syscall.WNOHANG,&rusage);
		wpid, err := syscall.Wait4(pid,nil,syscall.WNOHANG,&rusage);
		if err != nil {
			fmt.Printf("--E-- %%%d [%d] (%v)\n",ji,pid,err)
		}else{
			fmt.Printf("%%%d[%d](%d)\n",ji,pid,wpid)
			showRusage("chld",argv,&rusage)
		}
	}
}
func inBackground(gshCtx GshContext, argv[]string)(GshContext,bool){
	if gshCtx.CmdTrace { fmt.Printf("--I-- inBackground(%v)\n",argv) }
	gshCtx.BackGround = true // set background option
	xfin := false
	gshCtx, xfin = gshellv(gshCtx,argv)
	gshCtx.BackGround = false
	return gshCtx,xfin
}
// -o file without command means just opening it and refer by #N
// should be listed by "files" comnmand
func xOpen(gshCtx GshContext, argv[]string)(GshContext){
	var pv = []int{-1,-1}
	err := syscall.Pipe(pv)
	fmt.Printf("--I-- pipe()=[#%d,#%d](%v)\n",pv[0],pv[1],err)
	return gshCtx
}
func fromPipe(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}
func xClose(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}

// redirect
func redirect(gshCtx GshContext, argv[]string)(GshContext,bool){
	if len(argv) < 2 {
		return gshCtx, false
	}

	cmd := argv[0]
	fname := argv[1]
	var file *os.File = nil

	fdix := 0
	mode := os.O_RDONLY

	switch {
	case cmd == "-i" || cmd == "<":
		fdix = 0
		mode = os.O_RDONLY
	case cmd == "-o" || cmd == ">":
		fdix = 1
		mode = os.O_RDWR | os.O_CREATE
	case cmd == "-a" || cmd == ">>":
		fdix = 1
		mode = os.O_RDWR | os.O_CREATE | os.O_APPEND
	}
	if fname[0] == '#' {
		fd, err := strconv.Atoi(fname[1:])
		if err != nil {
			fmt.Printf("--E-- (%v)\n",err)
			return gshCtx, false
		}
		file = os.NewFile(uintptr(fd),"MaybePipe")
	}else{
		xfile, err := os.OpenFile(argv[1], mode, 0600)
		if err != nil {
			fmt.Printf("--E-- (%s)\n",err)
			return gshCtx, false
		}
		file = xfile
	}
	gshPA := gshCtx.gshPA
	savfd := gshPA.Files[fdix]
	gshPA.Files[fdix] = file.Fd()
	fmt.Printf("--I-- Opened [%d] %s\n",file.Fd(),argv[1])
	gshCtx, _ = gshellv(gshCtx, argv[2:])
	gshPA.Files[fdix] = savfd

	return gshCtx, false
}

//fmt.Fprintf(res, "GShell Status: %q", html.EscapeString(req.URL.Path))
func httpHandler(res http.ResponseWriter, req *http.Request){
	path := req.URL.Path
	fmt.Printf("--I-- Got HTTP Request(%s)\n",path)
	{
		gshCtx, _ :=  setupGshContext()
		fmt.Printf("--I-- %s\n",path[1:])
		gshCtx, _ = tgshelll(gshCtx,path[1:])
	}
	fmt.Fprintf(res, "Hello(^-^)/\n%s\n",path)
}
func httpServer(gshCtx GshContext, argv []string){
	http.HandleFunc("/", httpHandler)
	accport := "localhost:9999"
	fmt.Printf("--I-- HTTP Server Start at [%s]\n",accport)
	http.ListenAndServe(accport,nil)
}
func xGo(gshCtx GshContext, argv[]string){
	go gshellv(gshCtx,argv[1:]);
}
func xPs(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}

// Plugin
// plugin [-ls [names]] to list plugins
// Reference: plugin source code
func whichPlugin(gshCtx GshContext,name string,argv[]string)(pi *PluginInfo){
	pi = nil	
	for _,p := range gshCtx.PluginFuncs {
		if p.Name == name && pi == nil {
			pi = &p
		}
		if !isin("-s",argv){
			//fmt.Printf("%v %v ",i,p)
			if isin("-ls",argv){
				showFileInfo(p.Path,argv)
			}else{
				fmt.Printf("%s\n",p.Name)
			}
		}
	}
	return pi
}
func xPlugin(gshCtx GshContext, argv[]string)(GshContext,error){
	if len(argv) == 0 || argv[0] == "-ls" {
		whichPlugin(gshCtx,"",argv)
		return gshCtx, nil
	}
	name := argv[0]
	Pin := whichPlugin(gshCtx,name,[]string{"-s"})
	if Pin != nil {
		os.Args = argv // should be recovered?
		Pin.Addr.(func())()
		return gshCtx,nil
	}
	sofile := toFullpath(argv[0] + ".so") // or find it by which($PATH)

	p, err := plugin.Open(sofile)
	if err != nil {
		fmt.Printf("--E-- plugin.Open(%s)(%v)\n",sofile,err)
		return gshCtx, err
	}
	fname := "Main"
	f, err := p.Lookup(fname)
	if( err != nil ){
		fmt.Printf("--E-- plugin.Lookup(%s)(%v)\n",fname,err)
		return gshCtx, err
	}
	pin := PluginInfo {p,f,name,sofile} 
	gshCtx.PluginFuncs = append(gshCtx.PluginFuncs,pin)
	fmt.Printf("--I-- added (%d)\n",len(gshCtx.PluginFuncs))

	//fmt.Printf("--I-- first call(%s:%s)%v\n",sofile,fname,argv)
	os.Args = argv
	f.(func())()
	return gshCtx, err
}
func Args(gshCtx *GshContext, argv[]string){
	for i,v := range os.Args {
		fmt.Printf("[%v] %v\n",i,v)
	}
}
func Version(gshCtx *GshContext, argv[]string){
	if isin("-l",argv) {
		fmt.Printf("%v/%v (%v)",NAME,VERSION,DATE);
	}else{
		fmt.Printf("%v",VERSION);
	}
	if !isin("-n",argv) {
		fmt.Printf("\n")
	}
}

// Scanf // string decomposer
// scanf [format] [input]
func scanv(sstr string)(strv[]string){
	strv = strings.Split(sstr," ")
	return strv
}
func scanUntil(src,end string)(rstr string,leng int){
	idx := strings.Index(src,end)
	if 0 <= idx {
		rstr = src[0:idx]
		return rstr,idx+len(end)
	}
	return src,0
}
func (gsh*GshContext)printVal(fmts string, vstr string){
	//vint,err := strconv.Atoi(vstr)
	var ival int64 = 0
	n := 0
	err := error(nil)
	if strBegins(vstr,"_") {
		vx,_ := strconv.Atoi(vstr[1:])
		if vx < len(gsh.iValues) {
			vstr = gsh.iValues[vx]
		}else{
		}
	}
	// should use Eval()
	if strBegins(vstr,"0x") {
		n,err = fmt.Sscanf(vstr[2:],"%x",&ival)
	}else{
		n,err = fmt.Sscanf(vstr,"%d",&ival)
//fmt.Printf("--D-- n=%d err=(%v) {%s}=%v\n",n,err,vstr, ival)
	}
	if n == 1 && err == nil {
		//fmt.Printf("--D-- formatn(%v) ival(%v)\n",fmts,ival)
		fmt.Printf("%"+fmts,ival)
	}else{
		fmt.Printf("%"+fmts,vstr)
	}
}
func (gsh*GshContext)printfv(fmts,div string,list[]string){
	//fmt.Printf("{%d}",len(list))
	//curfmt := "v"
	outlen := 0
	curfmt := gsh.iFormat
	if 0 < len(fmts) {
		for xi := 0; xi < len(fmts); xi++ {
			fch := fmts[xi]
			if fch == '%' {
				if xi+1 < len(fmts) {
					curfmt = string(fmts[xi+1])
gsh.iFormat = curfmt
					xi += 1
	if xi+1 < len(fmts) && fmts[xi+1] == '(' {
		vals,leng := scanUntil(fmts[xi+2:],")")
		//fmt.Printf("--D-- show fmt(%v) val(%v) next(%v)\n",curfmt,vals,leng)
		gsh.printVal(curfmt,vals)
		xi += 2+leng-1
		outlen += 1
	}
					continue
				}
			}
			if fch == '_' {
				hi,leng := scanInt(fmts[xi+1:])
				if 0 < leng {
					if hi < len(gsh.iValues) {
						gsh.printVal(curfmt,gsh.iValues[hi])
					}else{
						fmt.Printf("((out-range))")
					}
					xi += leng
					continue;
				}
			}
			fmt.Printf("%c",fch)
			outlen += 1
		}
	}else{
		//fmt.Printf("--D-- print {%s}\n")
		for i,v := range list {
			if 0 < i {
				fmt.Printf(div)
			}
			gsh.printVal(curfmt,v)
			outlen += 1
			/*
			if v[0] == '_' {
				vx,_ := strconv.Atoi(v[1:])
				if vx < len(gsh.iValues) {
					fmt.Printf("%v",gsh.iValues[vx])
				}
			}else{
				fmt.Printf("%v",v)
			}
			*/
		}
	}
	if 0 < outlen {
		fmt.Printf("\n")
	}
}
func (gsh*GshContext)Scanv(argv[]string){
	//fmt.Printf("--D-- Scanv(%v)\n",argv)
	if len(argv) == 1 {
		return
	}
	argv = argv[1:]
	fmts := ""
	if strBegins(argv[0],"-F") {
		fmts = argv[0]
		gsh.iDelimiter = fmts
		argv = argv[1:]
	}
	input := strings.Join(argv," ")
	if fmts == "" { // simple decomposition
		v := scanv(input)
		gsh.iValues = v
		//fmt.Printf("%v\n",strings.Join(v,","))
	}else{
		v := make([]string,8)
		n,err := fmt.Sscanf(input,fmts,&v[0],&v[1],&v[2],&v[3])
		fmt.Printf("--D-- Scanf ->(%v) n=%d err=(%v)\n",v,n,err)
		gsh.iValues = v
	}
}
func (gsh*GshContext)Printv(argv[]string){
	//fmt.Printf("--D-- Printv(%v)\n",argv)
	//fmt.Printf("%v\n",strings.Join(gsh.iValues,","))
	div := gsh.iDelimiter
	fmts := ""
	argv = argv[1:]
	if 0 < len(argv) {
		if strBegins(argv[0],"-F") {
			div = argv[0][2:]
			argv = argv[1:]
		}
	}
	if 0 < len(argv) {
		fmts = strings.Join(argv," ")
	}
	gsh.printfv(fmts,div,gsh.iValues)
}
func (gsh*GshContext)Sortv(argv[]string){
	sv := gsh.iValues
	sort.Slice(sv , func(i,j int) bool {
		return sv[i] < sv[j]
	})
}
func (gsh*GshContext)Shiftv(argv[]string){
	vi := len(gsh.iValues)
	if 0 < vi {
		if isin("-r",argv) {
			top := gsh.iValues[0]
			gsh.iValues = append(gsh.iValues[1:],top)
		}else{
			gsh.iValues = gsh.iValues[1:]
		}
	}
}

// Command Interpreter
func gshellv(gshCtx GshContext, argv []string) (_ GshContext, fin bool) {
	fin = false

	if gshCtx.CmdTrace { fmt.Fprintf(os.Stderr,"--I-- gshellv((%d))\n",len(argv)) }
	if len(argv) <= 0 {
		return gshCtx, false
	}
	xargv := []string{}
	for ai := 0; ai < len(argv); ai++ {
		xargv = append(xargv,strsubst(&gshCtx,argv[ai],false))
	}
	argv = xargv
	if false {
		for ai := 0; ai < len(argv); ai++ {
			fmt.Printf("[%d] %s [%d]%T\n",
				ai,argv[ai],len(argv[ai]),argv[ai])
		}
	}
	cmd := argv[0]
	if gshCtx.CmdTrace { fmt.Fprintf(os.Stderr,"--I-- gshellv(%d)%v\n",len(argv),argv) }
	switch { // https://tour.golang.org/flowcontrol/11
	case cmd == "":
		xPwd(gshCtx,[]string{}); // emtpy command
	case cmd == "-x":
		gshCtx.CmdTrace = ! gshCtx.CmdTrace
	case cmd == "-xt":
		gshCtx.CmdTime = ! gshCtx.CmdTime
	case cmd == "-ot":
		sconnect(gshCtx, true, argv)
	case cmd == "-ou":
		sconnect(gshCtx, false, argv)
	case cmd == "-it":
		saccept(gshCtx, true , argv)
	case cmd == "-iu":
		saccept(gshCtx, false, argv)
	case cmd == "-i" || cmd == "<" || cmd == "-o" || cmd == ">" || cmd == "-a" || cmd == ">>" || cmd == "-s" || cmd == "><":
		redirect(gshCtx, argv)
	case cmd == "|":
		gshCtx = fromPipe(gshCtx, argv)
	case cmd == "args":
		Args(&gshCtx,argv)
	case cmd == "bg" || cmd == "-bg":
		rgshCtx, rfin := inBackground(gshCtx,argv[1:])
		return rgshCtx, rfin
	case cmd == "call":
		gshCtx, _ = excommand(gshCtx, false,argv[1:])
	case cmd == "cd" || cmd == "chdir":
		gshCtx = xChdir(gshCtx,argv);
	case cmd == "close":
		gshCtx = xClose(gshCtx,argv)
	case cmd == "dec" || cmd == "decode":
		Dec(&gshCtx,argv)
	case cmd == "#define":
	case cmd == "echo":
		echo(argv,true)
	case cmd == "enc" || cmd == "encode":
		Enc(&gshCtx,argv)
	case cmd == "env":
		env(argv)
	case cmd == "eval":
		xEval(argv[1:],true)
	case cmd == "exec":
		gshCtx, _ = excommand(gshCtx, true,argv[1:])
		// should not return here
	case cmd == "exit" || cmd == "quit":
		// write Result code EXIT to 3>
		return gshCtx, true
	case cmd == "fdls":
		// dump the attributes of fds (of other process)
	case cmd == "-find" || cmd == "fin" || cmd == "ufind" || cmd == "uf" || cmd == "fu":
		gshCtx = xFind(gshCtx,argv[1:])
	case cmd == "fork":
		// mainly for a server
	case cmd == "-gen":
		gen(gshCtx, argv)
	case cmd == "-go":
		xGo(gshCtx, argv)
	case cmd == "-grep":
		gshCtx = xFind(gshCtx,argv)
	case cmd == "history" || cmd == "hi": // hi should be alias
		gshCtx = xHistory(gshCtx, argv)
	case cmd == "jobs":
		xJobs(gshCtx,argv)
	case cmd == "lnsp":
		SplitLine(&gshCtx,argv)
	case cmd == "-ls":
		gshCtx = xFind(gshCtx,argv)
	case cmd == "nop":
		// do nothing
	case cmd == "pipe":
		gshCtx = xOpen(gshCtx,argv)
	case cmd == "plug" || cmd == "plugin" || cmd == "pin":
		gshCtx,_ = xPlugin(gshCtx,argv[1:])
	case cmd == "print":
		// output internal slice // also sprintf should be
		gshCtx.Printv(argv)
	case cmd == "ps":
		xPs(gshCtx,argv)
	case cmd == "pstitle":
		// to be gsh.title
	case cmd == "repeat" || cmd == "rep": // repeat cond command
		repeat(gshCtx,argv)
	case cmd == "scan":
		// scan input (or so in fscanf) to internal slice (like Files or map)
		gshCtx.Scanv(argv)
	case cmd == "set":
		// set name ...
	case cmd == "serv":
		httpServer(gshCtx,argv)
	case cmd == "shift":
		gshCtx.Shiftv(argv)
	case cmd == "sleep":
		sleep(gshCtx,argv)
	case cmd == "sort":
		gshCtx.Sortv(argv)
	case cmd == "time":
		gshCtx, fin = xTime(gshCtx,argv)
	case cmd == "pwd":
		xPwd(gshCtx,argv);
	case cmd == "ver" || cmd == "-ver" || cmd == "version":
		Version(&gshCtx,argv)
	case cmd == "where":
		// data file or so?
	case cmd == "which":
		which("PATH",argv);
	default:
		if whichPlugin(gshCtx,cmd,[]string{"-s"}) != nil {
			gshCtx, _ = xPlugin(gshCtx,argv)
		}else{
			gshCtx, _ = excommand(gshCtx,false,argv)
		}
	}
	return gshCtx, fin
}

func gshelll(gshCtx GshContext, gline string) (gx GshContext, rfin bool) {
	argv := strings.Split(string(gline)," ")
	gshCtx, fin := gshellv(gshCtx,argv)
	return gshCtx, fin
}
func tgshelll(gshCtx GshContext, gline string) (gx GshContext, xfin bool) {
	start := time.Now()
	gshCtx, fin := gshelll(gshCtx,gline)
	end := time.Now()
	elps := end.Sub(start);
	if gshCtx.CmdTime {
		fmt.Printf("--T-- " + time.Now().Format(time.Stamp) + "(%d.%09ds)\n",
			elps/1000000000,elps%1000000000)
	}
	return gshCtx, fin
}
func Ttyid() (int) {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return 0;
	}
	//fmt.Printf("Stdin: %v Dev=%d\n",
	//	fi.Mode(),fi.Mode()&os.ModeDevice)
	if (fi.Mode() & os.ModeDevice) != 0 {
		stat := syscall.Stat_t{};
		err := syscall.Fstat(0,&stat)
		if err != nil {
			//fmt.Printf("--I-- Stdin: (%v)\n",err)
		}else{
			//fmt.Printf("--I-- Stdin: rdev=%d %d\n",
			//	stat.Rdev&0xFF,stat.Rdev);
			//fmt.Printf("--I-- Stdin: tty%d\n",stat.Rdev&0xFF);
			return int(stat.Rdev & 0xFF)
		}
	}
	return 0
}
func ttyfile(gshCtx GshContext) string {
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	ttyfile := gshCtx.GshHomeDir + "/" + "gsh-tty" +
		 fmt.Sprintf("%02d",gshCtx.TerminalId)
		 //strconv.Itoa(gshCtx.TerminalId)
	//fmt.Printf("--I-- ttyfile=%s\n",ttyfile)
	return ttyfile
}
func ttyline(gshCtx GshContext) (*os.File){
	file, err := os.OpenFile(ttyfile(gshCtx),
		os.O_RDWR|os.O_CREATE|os.O_TRUNC,0600)
	if err != nil {
		fmt.Printf("--F-- cannot open %s (%s)\n",ttyfile(gshCtx),err)
		return file;
	}
	return file
}
// Command Line Editor
func getline(gshCtx GshContext, hix int, skipping, with_exgetline bool, gsh_getlinev[]string, prevline string) (string) {
	if( skipping ){
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}else
	if( with_exgetline && gshCtx.GetLine != "" ){
		//var xhix int64 = int64(hix); // cast
		newenv := os.Environ()
		newenv = append(newenv, "GSH_LINENO="+strconv.FormatInt(int64(hix),10) )

		tty := ttyline(gshCtx)
		tty.WriteString(prevline)
		Pa := os.ProcAttr {
			"", // start dir
			newenv, //os.Environ(),
			[]*os.File{os.Stdin,os.Stdout,os.Stderr,tty},
			nil,
		}
//fmt.Printf("--I-- getline=%s // %s\n",gsh_getlinev[0],gshCtx.GetLine)
proc, err := os.StartProcess(gsh_getlinev[0],[]string{"getline","getline"},&Pa)
		if err != nil {
			fmt.Printf("--F-- getline process error (%v)\n",err)
			// for ; ; { }
			return "exit (getline program failed)"
		}
		//stat, err := proc.Wait()
		proc.Wait()
		buff := make([]byte,LINESIZE)
		count, err := tty.Read(buff)
		//_, err = tty.Read(buff)
		//fmt.Printf("--D-- getline (%d)\n",count)
		if err != nil {
			if ! (count == 0) { // && err.String() == "EOF" ) {
				fmt.Printf("--E-- getline error (%s)\n",err)
			}
		}else{
			//fmt.Printf("--I-- getline OK \"%s\"\n",buff)
		}
		tty.Close()
		return string(buff[0:count])
	}else{
		// if isatty {
			fmt.Printf("!%d",hix)
			fmt.Print(PROMPT)
		// }
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}
}
//
// $USERHOME/.gsh/
//		gsh-rc.txt, or gsh-configure.txt
//              gsh-history.txt
//              gsh-aliases.txt // should be conditional?
//
func gshSetupHomedir(gshCtx GshContext) (GshContext, bool) {
	homedir,found := userHomeDir()
	if !found {
		fmt.Printf("--E-- You have no UserHomeDir\n")
		return gshCtx, true
	}
	gshhome := homedir + "/" + GSH_HOME
	_, err2 := os.Stat(gshhome)
	if err2 != nil {
		err3 := os.Mkdir(gshhome,0700)
		if err3 != nil {
			fmt.Printf("--E-- Could not Create %s (%s)\n",
				gshhome,err3)
			return gshCtx, true
		}
		fmt.Printf("--I-- Created %s\n",gshhome)
	}
	gshCtx.GshHomeDir = gshhome
	return gshCtx, false
}
func setupGshContext()(GshContext,bool){
	gshPA := syscall.ProcAttr {
		"", // the staring directory
		os.Environ(), // environ[]
		[]uintptr{os.Stdin.Fd(),os.Stdout.Fd(),os.Stderr.Fd()},
		nil, // OS specific
	}
	cwd, _ := os.Getwd()
	gshCtx := GshContext {
		cwd, // StartDir
		"", // GetLine
		[]GChdirHistory { {cwd,time.Now()} }, // ChdirHistory
		gshPA,
		[]GCommandHistory{}, //something for invokation?
		GCommandHistory{}, // CmdCurrent
		false,
		[]int{},
		syscall.Rusage{},
		"", // GshHomeDir
		Ttyid(),
		false,
		false,
		[]PluginInfo{},
		[]string{},
		" ",
		"v",
	}
	err := false
	gshCtx, err = gshSetupHomedir(gshCtx)
	return gshCtx, err
}
// Main loop
func script(gshCtxGiven *GshContext) (_ GshContext) {
	gshCtx,err0 := setupGshContext()
	if err0 {
		return gshCtx;
	}
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	//resmap()
	gsh_getlinev, with_exgetline :=
		 which("PATH",[]string{"which","gsh-getline","-s"})
	if with_exgetline {
		gsh_getlinev[0] = toFullpath(gsh_getlinev[0])
		gshCtx.GetLine = toFullpath(gsh_getlinev[0])
	}else{
	fmt.Printf("--W-- No gsh-getline found. Using internal getline.\n");
	}

	ghist0 := gshCtx.CmdCurrent // something special, or gshrc script, or permanent history
	gshCtx.CommandHistory = append(gshCtx.CommandHistory,ghist0)

	prevline := ""
	skipping := false
	for hix := len(gshCtx.CommandHistory); ; {
		gline := getline(gshCtx,hix,skipping,with_exgetline,gsh_getlinev,prevline)
		if skipping {
			if strings.Index(gline,"fi") == 0 {
				fmt.Printf("fi\n");
				skipping = false;
			}else{
				//fmt.Printf("%s\n",gline);
			}
			continue
		}
		if strings.Index(gline,"if") == 0 {
			//fmt.Printf("--D-- if start: %s\n",gline);
			skipping = true;
			continue
		}
		gline = strsubst(&gshCtx,gline,true)
		/*
		// should be cared in substitution ?
		if 0 < len(gline) && gline[0] == '!' {
			xgline, set, err := searchHistory(gshCtx,gline)
			if err {
				continue
			}
			if set {
				// set the line in command line editor
			}
			gline = xgline
		}
		*/
		ghist := gshCtx.CmdCurrent
		ghist.WorkDir,_ = os.Getwd()
		ghist.StartAt = time.Now()
		rusagev1 := Getrusagev()
		gshCtx.CmdCurrent.FoundFile = []string{}
		xgshCtx, fin := tgshelll(gshCtx,gline)
		rusagev2 := Getrusagev()
		ghist.Rusagev = RusageSubv(rusagev2,rusagev1)
		gshCtx = xgshCtx
		ghist.EndAt = time.Now()
		ghist.CmdLine = gline
		ghist.FoundFile = gshCtx.CmdCurrent.FoundFile

		/* record it but not show in list by default
		if len(gline) == 0 {
			continue
		}
		if gline == "hi" || gline == "history" { // don't record it
			continue
		}
		*/
		gshCtx.CommandHistory = append(gshCtx.CommandHistory, ghist)
		if fin {
			break;
		}
		prevline = gline;
		hix++;
	}
	return gshCtx
}
func main() {
	argv := os.Args
	if 1 < len(argv) {
		if isin("version",argv){
			Version(nil,argv)
			return
		}
		comx := isinX("-c",argv)
		if 0 < comx {
			gshCtx,err := setupGshContext()
			if !err {
				gshellv(gshCtx,argv[comx+1:])
			}
			return
		}
	}
	script(nil)
	//gshCtx := script(nil)
	//gshelll(gshCtx,"time")
}
//
//
Consideration
// - inter gsh communication, possibly running in remote hosts -- to be remote shell
// - merged histories of multiple parallel gsh sessions
// - alias as a function
// - instant alias end environ export to the permanent > ~/.gsh/gsh-alias and gsh-environ
// - retrieval PATH of files by its type
// - gsh as an IME
// - gsh a scheduler in precise time of within a millisecond
// - all commands have its subucomand after "---" symbol
// - filename expansion by "-find" command
// - history of ext code and output of each commoand
// - "script" output for each command by pty-tee or telnet-tee
// - $BUILTIN command in PATH to show the priority
// - "?" symbol in the command (not as in arguments) shows help request 
// - searching command with wild card like: which ssh-*
// - longformat prompt after long idle time (should dismiss by BS)
// - customizing by building plugin and dynamically linking it
// - generating syntactic element like "if" by macro expansion (like CPP) >> alias
// - "!" symbol should be used for negation, don't wast it just for job control
// - don't put too long output to tty, record it into GSH_HOME/session-id/comand-id.log
// - making canonical form of command at the start adding quatation or white spaces
// - name(a,b,c) ... use "(" and ")" to show both delimiter and realm
// - name? or name! might be useful
// - htar format - packing directory contents into a single html file using data scheme
//---END--- (^-^)/ITS more
/*
References

The Go Programming Language MDN web docs HTML CSS: Selectors repeat HTTP JavaScript: ...

--> */ //

GShell − ネーミングライツ

社長:このshellは今後我社の基軸になることは確実です。で、名前について考えています。

基盤:GShellとかいかにも沢山ありそうですね。

開発:玉寿司みたいな。

社長:北千住の玉寿司閉じちゃって残念です。青春の想い出。

経理:でもあそこでやってたような散財は今の生活水準ではもう不可能ですね。

社長:夢のまた夢まぼろしの如くなり…

基盤:gshell で検索。ぞろぞろ出てきますね。どの検索サイトでもトップに出てくるのは IBM の。

社長:敵として不足は無いですねw

基盤:当社 GShell は bingでは2ページめ、Yahoo!では10ページ目(pdf)、Googleでは2ページ目(delegate.org)に出てきます。できたての上にただのブログにしては健闘かと。

開発:Googleはhttp://delegate.orgが好きなんですよね。同じ内容なのにhttps://its-more.jpではほとんど載せてくれない。一方Bingは新しい情報を優先する傾向を感じます。

基盤:httpsじゃないと載せてくれないなんてことは無いですね。

社長:登録されてる商標だと面倒かなとは思います。

基盤:IBMのは、TMとか®とかは主張していない感じではあります。

開発:特許庁の商標検索では、gshellはヒット無し、大文字GSHが2件ありますね。

社長:昔UNIXが食器メーカーの名前かなんかとかぶって笑い話になりました。

基盤:gshell wikipedia で検索。載ってないですね。gshell golang で検索。当社 GShell がBingでトップ占拠してます(笑)ああ、golang で shell を作って gshell ってしている人いますね。githubに。

開発:それはマジでかぶりますね。

基盤:すごいコンパクトですね。ワーキングディレクトリに名前を付けてcdで飛べるようにしているのが、ひとつ特徴ですかね。ヒストリによると、2019年6月27日開始?同8月20日が最後のコミット。

社長:2ヶ月近くは活動されていたと。

開発:USのトレードマーク検索でもgshellは無いですね。powershell とかはきっちり登録されてますがw

社長:いや、私はgshellという名前にそれほどこだわりがあるわけでもなく、問題があるなら違う名前にすれば良いとは思うのです。

社長:というか、そもそもソフトの名前の命名権を売れないだろうか?という事を考えたわけですよ。

営業:それは新しいですね。

基盤:北海道日本ハムShellとかセーフコShellっていうやつですか。

開発:AIG Shellとか、ロレックスShellとか。

社長:ああ、全英は今週ですね。絶望的な状況みたいですけど。

開発:怖いもの見たさみたいな気分ですかね。

社長:ところで、gsh も gshellも、netとcomが既に取られているのは以前調べてわかっていたのですが、gshell.org が空いていたので取ることにしました。

経理:whoisでですか?

社長:いや、なんとなく愛着のあるxsoでw

経理:あそこは.orgの投げ売りはしてないですが。

社長:まあいいじゃないですか。

開発:ネームサーバを設定しないと。ここのドメイン管理サイト、相変わらずクソ遅いですね。xso!

社長:きっとひどいDDOS攻撃でも受け続けてるんじゃないですかね。

ドア:ぴんぽーん

社長:どなたですか?あ、大家さん。

社長:パタン。はい、はい、あーそうなんですか。あはは。はい。わかりました。ええ、全然心配してませんよ。よろしくお願いします。あ、これはどうも。パタン。

社長:大家さんが、管理会社を代えるそうです。ていうかこの建物、担当の不動産屋さんが数社あったのを、一社に一元化するんだそうです。大家さんもうちみたいに法人化して、効率化するみたいな。なんか、除菌スプレーをくれましたw

経理:大家さんの場合には実際、節税効果とかあるんじゃないでしょうかね。

社長:うちは心理的効果絶大でした。

開発:今の不動産屋さんとは前住んでたところからもう20年以上のお付き合いでしたね。

社長:あそこ、社長の奥さんが担当してくれてた時は色々融通も利いてよかったんですが、若い人に担当が変わっちゃってからはどうもねぇ。

開発:今度の不動産屋さんは前より大手だから、もっと望み薄ですね。

基盤:エアコンがポンコツでほぼ効かない件と台所の換気扇とともにクソうるさい件を伝えてほしかったです。

-- 2020-0817 SatoxITS

HTMLソース印刷用ブラウザ選定

社長:開発したGo言語埋め込みHTMLテキストの印刷用ブラウザを選定したいと思います。

基盤:ページソースの表示もそうですが印刷となると各ブラウザ個性がありますね。

社長:想定しているフローはこうです。

  1. G1.goをGo言語プログラムとして実行して動作を確認する
  2. G1.goをG1.htmlとしてブラウザでHTMLとして表示して動作確認
  3. G1.htmlのソースをブラウザで表示してPDFファイルG1.pdfに印刷
  4. G1.pdfに電子署名してアップロードする
  5. G1.pdfをダウンロードして電子署名を検証する
  6. G1.pdfからテキスト情報をG2.htmlに取り出す
  7. G2.htmlをブラウザで表示して動作を確認する
  8. G2.htmlをG2.goとしてGo言語プログラムとして実行して確認する

開発:理想的には全部のステップを1つのツール、まあ第一候補はブラウザですが、一つのツールで全部できると良いですね。現実には、編集は vi (vim)、HTML表示はブラウザ、署名はAcrobat、アップロードには Finderからドラッグを使っているわけです。

社長:3,4と5,6 を自動化してワンクリックにしたいんですけどね。GShellでできるといいな。

開発:問題は、G1.go == G2.go、G1.html == G2.html が維持されるかですね。インデント情報が壊れたり、変数名の途中で改行されちゃったりとかよくあります。

基盤:Acrobat とエクスポートだなんだとか格闘したく無いんですよね。Acrobat での電子署名にこだわらなければ、物事はすごくシンプルだと思うんですが。ただ G1.html.gz くらいをアップロード・ダウンロードすれば済むと思います。

開発:電子署名にこだわるなら、 PGPとかS/MIMEという選択肢もあります。

社長:まあそれは… まず第一に、PDF型式は電子署名のインフラとして独走状態にあるのが現状です。クリアテキストに電子署名というのは、プログラムを印刷物として眺めるのに都合が良いわけです。そのプログラムは誰が何時作成したものか、印刷という表示型式で著者が確認した見た目の同一性も含めて、電子署名で確実に検証・保証できる。今もよくある、ファイルと別に同一性確認用のハッシュ値を提供するとか、もうそういう時代じゃないと思うんです。

開発:まあ、印刷された型式に著者として納得ができるかどうかはまた別ですけどね。これについては、ブラウザごとに非常に個性があることがわかっています。

社長:第二に、当社はすでにAcrobatを1年間約2万円でサブスクリプション契約をしており、その元はまだ取れていない。まだブログ関係で署名し直しも含めて約500回、その他の用途も含めて1000回も署名してないと思います。

経理:Acrobat は 4/26 日に 20,856円で購入しています。現在まで4ヶ月の使用料を7000円分相当と考えると、一回7円。妥当な線かとは思います so far。

開発:現時点まで署名1回あたり20円と考えても、結構元が取れたように思われますが。署名だけに使用しているわけでも無く。時々OCR機能とか変換機能とか使いますよね。

基盤:Acrobat をコマンドモードで使うなり、マウスとキーボードの操作を自動生成するなりして、署名をバンバンさせれば良いと思いますけどね。クソ遅いから10秒くらいかかるかもしれませんが。それでも1日で8000署名できますよ。

開発:それやったら、無償で使わせてもらっている公証タイムスタンプ局から怒られるかもね。

社長:それで、一応上のフローで考えるとすると、HTMLのソースを表示してPDFにする、というところが第一関門です。

開発:それで各ブラウザでやってみたのですが、やはりとてもそれぞれ個性が強い。まずダメ組トリオ。

開発:上から Chrome、Opera、Edge ですが、ソースを表示するのに固定ピッチフォントで無い時点で終わっています。インデント情報も適当に生成している。使えません。

開発:次に見込みのあるトリオ。

開発:上から Firefox、Vivaldi,Safariです。

社長:いつも思うんですが、Firefox の色使いは只者でないというか、常人とは色彩モデルが違うものを感じますね。

基盤:開発者がすごく特殊な発色をするディスプレイを使っているのかも。開発費が無くて90年代のとか。

経理:電気代がかさむんじゃないですかね。

社長:でもFirefoxには世界中の色んな人が開発に参加してるんですよね。Mozilla MDNのウェブページにもただならぬ色彩が漂ってるので、何かそういうカッコとした色彩ポリシーのようなものがあるようにも思います。

開発:さて、この3者のうち、Safari は HTML の中にある Go の特殊記号省略記法に着いて来れないようなので脱落。

開発:で、あとは印刷対決なんですが、なーんと、Vivaldi は人間非可読なPDFをしれっと生成して来ます。

社長:さすが、独創の雄ですね。

開発:ということで、Firefox 一択となりました。と、そこでふと Chromium のことを思い出したので特別参加。でしたが、表示は良いのですが、印刷がペケで脱落です。

社長:印刷もよさそうに見えますが。

開発:確かにそう言われてみれば… そうか、自分でPDFを表示する時にはいいんですね。これを Acrobat で表示するとこうなってしまう。

開発:我社の基準ではソースコードの表示は Courier New でなくてはならないのです。

社長:まあ、固定ピッチならとりあえずいいんじゃないですかね…

開発:Chromium のHTMLソースPDF化では、常識的なフォントサイズで出力するので100%表示で読める。ただ、そのためにGShell コードの横幅が足りないことがある。100文字くらいになることはあるわけです。そうするとおり返して印刷するけれど、ここは一つの行でしたという事は行番号でわかります。

基盤:これはふつうに Courier になってますね。

開発:あれ… そうか、Acrobat が勝手にフォントを選択してるみたいですね。ぷんぷん。

開発:一方FirefixでのPDF化では、フォントサイズをすごく小さく印刷するので、まあ折り返される事は無い。あれで折り返されるとしたら、コードの書き方が変。もし折り返されるとしてもChromiumと同じ行番号処理をするでしょう。他のブラウザでもそうなってますし。で、同じ部分を表示するとこうなります。ただし200%表示です。

開発:ソースコードを紙に印刷する事はまあ無いと思いますし、やたらPDFの改ページで切られるとコードが読みにくいですから、私はFirefox の方針が妥当だと思います。

社長:でもそのへんって、印刷時のプロパティで設定するとか、もし出来ないようならソースコードがあるんで自由にいじれますよね。CSSの@print とか… ああ、ソースコードの印刷までは制御できないかな。

開発:デフォルトをどう設定しているかで、そのツールの姿勢というのがわかるわけで、そこは尊重したいと思います。

社長:了解。では、HTMLのソースの印刷はFirefoxということで。

基盤:ちょっとちゃぶ台返しですが、見た目の問題に関しては、そもそもソースとしての印刷用のHTMLを生成してやればよいのではないかと。適切な場所に改ページを入れたり。そうすれば、ブラウザ依存性も回避できると思いますが。

開発:・・・ うーむ、gsh.go.html に「ソースを印刷する型式にする」的なボタンを付けると・・・

基盤:必要なら行番号もつけちゃったりして。ついでに電子署名も付けちゃったりする。

開発:まあ、電子署名はちょっとわかりませんが、印刷用のHTMLは JavaScript で生成できそうですね。電子署名は・・・空白情報の保存が保証されないから、空白を正規化した形にして署名するんですかね・・・公開側の鍵情報というか証明書も内蔵すれば、署名を検証するのも簡単・・・

社長:自分を印刷する型式とか電子署名するアルゴリズムを内蔵するソースコードって、なんだか素敵ですね。いずれそうしましょう。でも当面は Firefox でも良いです。

開発:えーと。第7の選択肢としてXcodeでの印刷もなかなか良いと言う件を書こうと思ったのですが、自分で自分を印刷して署名する方法の話が面白そうなので、どうでも良くなりました(笑)

社長:HTMLに関しては、署名・検証アルゴリズム内蔵方式で、脱PDFできるかもですね。

開発:といいますか、data URIがかなり使えそうなので、ディレクトリ階層をごっそりHTMLにしてしまう「htar」というアーカイブ型式を考えているんです。これは形式としてはとてもシンプルです。ウェブページで言えば、引用しているCSSとかJavaScriptとか、インライン画像もdata URIで埋め込めば、そのままウェブ魚拓にもなるわけです。CSSにも自己署名させるようにしたら、部品単位でされた署名も自動的に引き継げる。まとめるのも展開するのもGShellのコマンド列でやるとすれば、GShell Archive つまり gshar 型式というようなものになります。

基盤:グシャー型式ですねw

社長:となると何でも署名付きHTMLになるから、バイバイPDF署名って事が可能になるかもですね。

社長:それはそうと、GShell を始めたのが 8/7なので、もう11日目になりました。先週は没頭していて曜日を忘れて月曜のボウリングの試合に出られませんでした。今日は8時に出かけられるように、計画的に続きの仕事を進めたいと思います。

-- 2020-0817 SatoxITS

GShell 0.1.2 − 動くソースコード

開発:ソースコードだって能動的に主張するべきだということで、動くソースコードを作ってみました。

基盤:てか、コメント内のJavaScriptがブラウザで実行されてるだけですよねw

開発:いやいや、たとえばJavaScriptでGoの処理系を書けば話は変わってきますよ。完全な処理系で無くてもいいんで。あるはGoでブラウザを作る。既存の資産はそのまま使って、Goと動的リンクするでも良いと思います。

社長:変更履歴とかコメント付けとか、プログラミング言語自身てサポートしてないでんですよね。たとえばWordでプログラムを書くと良いかも。

基盤:マークアップを想定した構文があると良いのでしょうかね。

開発:頭から尻尾まで自分の言語で書いてないと怒るというのが心が狭いといいいますか。ざーっとした文章の中でこの部分は自分の理解できる言語だなと思ったらそこを処理する。それまでの情報は文脈として利用するとかしたら良いと思いますね。昔、埋込み型のSQLだったか、そんな感じになってたような。

社長:そもそも自分自身がどう書かれているかも普通の言語のプログラムは知らないですしね。

開発:それはそうと、これをやってて幾つか面白いことがわかりました。この流れていくGShellのロゴはdata URIで埋め込んであるのですが、base64エンコーディングして8KBを超えています。すると、ロゴの底の部分が切れる。おそらく8KBまでで切るという規約か、共通の実装のせいかと思います。ですが、途中で画像データが切れているのに、エラーにはしないで表示している。。。と思ったのですが、私がbase64にする時に末尾をちょん切ってました。そのような制限は無いようです (^-^; 治りました。

社長:しかし、base64エンコーディングがライブラリにあるなんて良い時代ですね。MIMEが出来た当時は自作したものです。

開発:でも何故か、base64にするときに一行の長さを制約して折り返すというオプションが無いようなんです。不思議。MIMEを想定してないんですかね。そのせいで無駄な憶測をしてしまいました。

社長:言語によって、文字列定数の長さ制限が厳しかったりしますよね。昔はCで結構苦労しました。2KBとか?

開発:JavaScriptは全然楽勝ですね。まあ、長大なHTMLを生成しちゃう言語ですからねえ。ただ、JavaScriptっていうと自動生成されたか何かで改行もなくごちゃっと詰め込まれているのをよく見るので、改行の概念が無いようなイメージを持っていたんですが、ちゃんと文字列定数の途中を改行で分けるときには行末にバックスラッシュを入れれば良いということが分かりました。ごく普通の言語なんですね。// で行末までコメントというのも、大変助かります。

開発:最初はCSSのurl()の中に長大なdata uri を入れるのに、改行を入れたのですが、やはりそのせいでダメ。HTMLは行末にバックスラッシュが無くてもクォーテーションが終わるまで読んでくれるので、そのへんの流儀が違うのが、そもそも生まれ育ち志向が違うんだろうなという感じはします。

社長:ゼロから同時に、HTML的+CSS的+JavaScript的一体言語を作り直したら、どういうものになるでしょうね。

基盤:HTMLとCSSとJavaScriptとの間の属性名の関係とか面倒くさいですよね。一つの言語なら良いのに。

開発:実際3者の処理系は長年かけてとてもリッチに整備されて来ているわけで、それらをもっと簡単にまとめて書ける言語の皮をかぶせることはできそうに思いますね。

開発:ああそれで、5+1大ブラウザで試したわけですが、なぜか左下のSafariが表示してくれない。

開発:Safariのdata URIの処理に問題があるのだろうかと、この時は思ったんですが、どうやらちょん切れたPNGファイルを表示するのを拒否したようです。さっき直したのを食わせたらちゃんと動きました。

社長:ストイックなんですかね。狭量なんですかね。

開発:あともう一点は、favicon の扱いです。これはlink relで書いているのですが、なぜか上の集合写真にあるように Firefox しか表示してくれない。

開発:なんでかと思ったら、どうもこの Goのコードに無理やりHTMLのフリをさせるために入れているコメント記号が邪魔をしているようなんです。この // やら /* をはずしてやれば、他のブラウザでもちゃんと見てくれます。

社長:ノイズに強く作るかどうか、その是非はあるでしょうね。もとのHTMLはめっちゃノイズに強かった。

開発:あとは、この下のコードのようにWordPressに張り込むとJavaScrptが動かないという謎。インラインのスクリプトが動かなくなるような仕掛けでもあるんでしょうかね?

社長:まあお仕着せのブログサーバですからねぇ…

-- 2020-0817 SatoxITS

// /* GShell-0.1.3 by SatoxITS

GShell // a General purpose Shell built on the top of Golang

*/ /*
Overview
To be written
*/ /*
Index

Implementation
	Structures
		import
		struct
	Main functions
		str-expansion	// macro processor
		finder		// builtin find + du
		grep		// builtin grep + wc + cksum + ...
		plugin		// plugin commands
		system		// external commands
		builtin		// builtin commands
		network		// socket handler
		redirect	// StdIn/Out redireciton
		history		// command history
		rusage		// resouce usage
		encode		// encode / decode
		getline		// line editor
		interpreter	// command interpreter
		main
*/ //
Source Code //
// gsh - Go lang based Shell
// (c) 2020 ITS more Co., Ltd.
// 2020-0807 created by SatoxITS (sato@its-more.jp)

package main // gsh main
// Imported packages // Packages
import (
	"fmt"		// fmt
	"strings"	// strings
	"strconv"	// strconv
	"sort"		// sort
	"time"		// time
	"bufio"		// bufio
	"io/ioutil"	// ioutil
	"os"		// os
	"syscall"	// syscall
	"plugin"	// plugin
	"net"		// net
	"net/http"	// http
	//"html"	// html
	"path/filepath"	// filepath
	"go/types"	// types
	"go/token"	// token
	"encoding/base64"	// base64
	//"gshdata"	// gshell's logo and source code
)

var NAME = "gsh"
var VERSION = "0.1.3"
var DATE = "2020-0817b"
var LINESIZE = (8*1024)
var PATHSEP = ":"	// should be ";" in Windows
var DIRSEP = "/"	// canbe \ in Windows
var GSH_HOME = ".gsh"	// under home directory
var PROMPT = "> "

// -xX logging control
// --A-- all
// --I-- info.
// --D-- debug
// --W-- warning
// --E-- error
// --F-- fatal error

// Structures
type GCommandHistory struct {
	StartAt		time.Time // command line execution started at
	EndAt		time.Time // command line execution ended at
	ResCode		int       // exit code of (external command)
	CmdError	error     // error string
	OutData		*os.File  // output of the command
	FoundFile	[]string  // output - result of ufind
	Rusagev		[2]syscall.Rusage // Resource consumption, CPU time or so
	CmdId		int       // maybe with identified with arguments or impact
			          // redireciton commands should not be the CmdId
	WorkDir		string    // working directory at start
	CmdLine		string    // command line
}
type GChdirHistory struct {
	Dir		string
	MovedAt		time.Time
}
type CmdMode struct {
	BackGround	bool
}
type PluginInfo struct {
	Spec		*plugin.Plugin
	Addr		plugin.Symbol
	Name		string // maybe relative
	Path		string // this is in Plugin but hidden
}
type GshContext struct {
	StartDir	string	// the current directory at the start
	GetLine		string	// gsh-getline command as a input line editor
	ChdirHistory	[]GChdirHistory // the 1st entry is wd at the start
	gshPA		syscall.ProcAttr
	CommandHistory	[]GCommandHistory
	CmdCurrent	GCommandHistory
	BackGround	bool
	BackGroundJobs	[]int
	LastRusage	syscall.Rusage
	GshHomeDir	string
	TerminalId	int
	CmdTrace	bool
	PluginFuncs	[]PluginInfo
}

func strBegins(str, pat string)(bool){
	if 0 < len(str){
		yes := str[0:len(pat)] == pat
		//fmt.Printf("--D-- strBegins(%v,%v)=%v\n",str,pat,yes)
		return yes
	}
	//fmt.Printf("--D-- strBegins(%v,%v)=%v\n",str,pat,false)
	return false
}
func isin(what string, list []string) bool {
	for _, v := range list  {
		if v == what {
			return true
		}
	}
	return false
}
func isinX(what string,list[]string)(int){
	for i,v := range list {
		if v == what {
			return i
		}
	}
	return -1
}

func env(opts []string) {
	env := os.Environ()
	if isin("-s", opts){
		sort.Slice(env, func(i,j int) bool {
			return env[i] < env[j]
		})
	}
	for _, v := range env {
		fmt.Printf("%v\n",v)
	}
}

// - rewriting should be context dependent
// - should postpone until the real point of evaluation
// - should rewrite only known notation of symobl
func scanInt(str string)(val int,leng int){
	leng = -1
	for i,ch := range str {
		if '0' <= ch && ch <= '9' {
			leng = i+1
		}else{
			break
		}
	}
	if 0 < leng {
		ival,_ := strconv.Atoi(str[0:leng])
		return ival,leng
	}else{
		return 0,0
	}
}
func substHistory(gshCtx *GshContext,str string,i int,rstr string)(leng int,rst string){
	if len(str[i+1:]) == 0 {
		return 0,rstr
	}
	hi := 0
	histlen := len(gshCtx.CommandHistory)
	if str[i+1] == '!' {
		hi = histlen - 1
		leng = 1
	}else{
		hi,leng = scanInt(str[i+1:])
		if leng == 0 {
			return 0,rstr
		}
		if hi < 0 {
			hi = histlen + hi
		}
	}
	if 0 <= hi && hi < histlen {
		//fmt.Printf("--D-- %v(%c)\n",str[i+leng:],str[i+leng])
		if 1 < len(str[i+leng:]) && str[i+leng:][1] == 'f' {
			leng += 1
			xlist := []string{}
			list := gshCtx.CommandHistory[hi].FoundFile
			for _,v := range list {
				//list[i] = escapeWhiteSP(v)
				xlist = append(xlist,escapeWhiteSP(v))
			}
			//rstr += strings.Join(list," ")
			rstr += strings.Join(xlist," ")
		}else{
			rstr += gshCtx.CommandHistory[hi].CmdLine
		}
	}else{
		leng = 0
	}
	return leng,rstr
}
func escapeWhiteSP(str string)(string){
	if len(str) == 0 {
		return "\\z" // empty, to be ignored
	}
	rstr := ""
	for _,ch := range str {
		switch ch {
			case '\\': rstr += "\\\\"
			case ' ': rstr += "\\s"
			case '\t': rstr += "\\t"
			case '\r': rstr += "\\r"
			case '\n': rstr += "\\n"
			default: rstr += string(ch)
		}
	}
	return rstr
}
func unescapeWhiteSP(str string)(string){ // strip original escapes
	rstr := ""
	for i := 0; i < len(str); i++ {
		ch := str[i]
		if ch == '\\' {
			if i+1 < len(str) {
				switch str[i+1] {
					case 'z':
						continue;
				}
			}
		}
		rstr += string(ch)
	}
	return rstr
}
func unescapeWhiteSPV(strv []string)([]string){ // strip original escapes
	ustrv := []string{}
	for _,v := range strv {
		ustrv = append(ustrv,unescapeWhiteSP(v))
	}
	return ustrv
}

// str-expansion
// - this should be a macro processor
func strsubst(gshCtx *GshContext,str string,histonly bool) string {
	rstr := ""
	inEsc := 0 // escape characer mode 
	for i := 0; i < len(str); i++ {
		//fmt.Printf("--D--Subst %v:%v\n",i,str[i:])
		ch := str[i]
		if inEsc == 0 {
			if ch == '!' {
				leng,xrstr := substHistory(gshCtx,str,i,rstr)
				if 0 < leng {
					i += leng
					rstr = xrstr
					continue
				}
			}
			switch ch {
				case '\\': inEsc = '\\'; continue
				case '%':  inEsc = '%';  continue
				case '$':
			}
		}
		switch inEsc {
		case '\\':
			switch ch {
				case '\\': ch = '\\'
				case 's': ch = ' '
				case 't': ch = '\t'
				case 'r': ch = '\r'
				case 'n': ch = '\n'
				case 'z': inEsc = 0; continue // empty, to be ignored
			}
			inEsc = 0 
		case '%':
			switch {
				case ch == '%': ch = '%'
				case ch == 'T':
					rstr = rstr + time.Now().Format(time.Stamp)
					continue;
				default:
					// postpone the interpretation
					rstr = rstr + "%" + string(ch)
					continue;
			}
			inEsc = 0
		}
		rstr = rstr + string(ch)
	}
	return rstr
}
func showFileInfo(path string, opts []string) {
	if isin("-l",opts) || isin("-ls",opts) {
		fi, _ := os.Stat(path)
		mod := fi.ModTime()
		date := mod.Format(time.Stamp)
		fmt.Printf("%v %8v %s ",fi.Mode(),fi.Size(),date)
	}
	fmt.Printf("%s",path)
	if isin("-sp",opts) {
		fmt.Printf(" ")
	}else
	if ! isin("-n",opts) {
		fmt.Printf("\n")
	}
}
func userHomeDir()(string,bool){
	/*
	homedir,_ = os.UserHomeDir() // not implemented in older Golang
	*/
	homedir,found := os.LookupEnv("HOME")
	//fmt.Printf("--I-- HOME=%v(%v)\n",homedir,found)
	if !found {
		return "/tmp",found
	}
	return homedir,found
}

func toFullpath(path string) (fullpath string) {
	if path[0] == '/' {
		return path
	}
	pathv := strings.Split(path,DIRSEP)
	switch {
	case pathv[0] == ".":
		pathv[0], _ = os.Getwd()
	case pathv[0] == "..": // all ones should be interpreted
		cwd, _ := os.Getwd()
		ppathv := strings.Split(cwd,DIRSEP)
		pathv[0] = strings.Join(ppathv,DIRSEP)
	case pathv[0] == "~":
		pathv[0],_ = userHomeDir()
	default:
		cwd, _ := os.Getwd()
		pathv[0] = cwd + DIRSEP + pathv[0]
	}
	return strings.Join(pathv,DIRSEP)
}

func IsRegFile(path string)(bool){
	fi, err := os.Stat(path)
	if err == nil {
		fm := fi.Mode()
		return fm.IsRegular();
	}
	return false
}

// Encode / Decode
// Encoder
func Enc(gshCtx *GshContext,argv[]string)(*GshContext){
	file := os.Stdin
	buff := make([]byte,LINESIZE)
	li := 0
	encoder := base64.NewEncoder(base64.StdEncoding,os.Stdout)	
	for li = 0; ; li++ {
		count, err := file.Read(buff)
		if count <= 0 {
			break
		}
		if err != nil {
			break
		}
		encoder.Write(buff[0:count])
	}
	encoder.Close()
	return gshCtx
}
func Dec(gshCtx *GshContext,argv[]string)(*GshContext){
	decoder := base64.NewDecoder(base64.StdEncoding,os.Stdin)	
	li := 0
	buff := make([]byte,LINESIZE)
	for li = 0; ; li++ {
		count, err := decoder.Read(buff)
		if count <= 0 {
			break
		}
		if err != nil {
			break
		}
		os.Stdout.Write(buff[0:count])
	}
	return gshCtx
}
// lnsp [N] [-crlf][-C \\]
func SplitLine(gshCtx *GshContext,argv[]string)(*GshContext){
	reader := bufio.NewReaderSize(os.Stdin,64*1024)
	ni := 0
	toi := 0
	for ni = 0; ; ni++ {
		line, err := reader.ReadString('\n')
		if len(line) <= 0 {
			if err != nil {
			fmt.Fprintf(os.Stderr,"--I-- lnsp %d to %d (%v)\n",ni,toi,err)
			break
			}
		}
		off := 0
		ilen := len(line)
		remlen := len(line)
		for oi := 0; 0 < remlen; oi++ {
			olen := remlen
			addnl := false
			if 72 < olen {
				olen = 72
				addnl = true
			}
			fmt.Fprintf(os.Stderr,"--D-- write %d [%d.%d] %d %d/%d/%d\n",
				toi,ni,oi,off,olen,remlen,ilen)
			toi += 1
			os.Stdout.Write([]byte(line[0:olen]))
			if addnl {
				//os.Stdout.Write([]byte("\r\n"))
				os.Stdout.Write([]byte("\n"))
			}
			line = line[olen:]
			off += olen
			remlen -= olen
		}
	}
	fmt.Fprintf(os.Stderr,"--I-- lnsp %d to %d\n",ni,toi)
	return gshCtx
}

// grep
// "lines", "lin" or "lnp" for "(text) line processor" or "scanner"
// a*,!ab,c, ... sequentioal combination of patterns
// what "LINE" is should be definable
// generic line-by-line processing
// grep [-v]
// cat -n -v
// uniq [-c]
// tail -f
// sed s/x/y/ or awk
// grep with line count like wc
// rewrite contents if specified
func xGrep(gshCtx GshContext,path string,rexpv[]string)(int){
	file, err := os.OpenFile(path,os.O_RDONLY,0)
	if err != nil {
		fmt.Printf("--E-- grep %v (%v)\n",path,err)
		return -1
	}
	defer file.Close()
	if gshCtx.CmdTrace { fmt.Printf("--I-- grep %v %v\n",path,rexpv) }
	//reader := bufio.NewReaderSize(file,LINESIZE)
	reader := bufio.NewReaderSize(file,80)
	li := 0
	found := 0
	for li = 0; ; li++ {
		line, err := reader.ReadString('\n')
		if len(line) <= 0 {
			break
		}
		if 150 < len(line) {
			// maybe binary
			break;
		}
		if err != nil {
			break
		}
		if 0 <= strings.Index(string(line),rexpv[0]) {
			found += 1
			fmt.Printf("%s:%d: %s",path,li,line)
		}
	}
		//fmt.Printf("total %d lines %s\n",li,path)
	//if( 0 < found ){ fmt.Printf("((found %d lines %s))\n",found,path); }
	return found
}

// Finder
// finding files with it name and contents
// file names are ORed
// show the content with %x fmt list
// ls -R
// tar command by adding output
type fileSum struct {
	Err	int64	// access error or so
	Size	int64	// content size
	DupSize	int64	// content size from hard links
	Blocks	int64	// number of blocks (of 512 bytes)
	DupBlocks int64	// Blocks pointed from hard links
	HLinks	int64	// hard links
	Words	int64
	Lines	int64
	Files	int64
	Dirs	int64	// the num. of directories
	SymLink	int64
	Flats	int64	// the num. of flat files
	MaxDepth	int64
	MaxNamlen	int64	// max. name length
	nextRepo	time.Time
}
func showFusage(dir string,fusage *fileSum){
	bsume := float64(((fusage.Blocks-fusage.DupBlocks)/2)*1024)/1000000.0
	//bsumdup := float64((fusage.Blocks/2)*1024)/1000000.0

	fmt.Printf("%v: %v files (%vd %vs %vh) %.6f MB (%.2f MBK)\n",
		dir,
		fusage.Files,
		fusage.Dirs,
		fusage.SymLink,
		fusage.HLinks,
		float64(fusage.Size)/1000000.0,bsume);
}
const (
	S_IFMT    = 0170000
	S_IFCHR   = 0020000
	S_IFDIR   = 0040000
	S_IFREG   = 0100000
	S_IFLNK   = 0120000
	S_IFSOCK  = 0140000
)
func cumFinfo(fsum *fileSum, path string, staterr error, fstat syscall.Stat_t, argv[]string,verb bool)(*fileSum){
	now := time.Now()
	if time.Second <= now.Sub(fsum.nextRepo) {
		if !fsum.nextRepo.IsZero(){
			tstmp := now.Format(time.Stamp)
			showFusage(tstmp,fsum)
		}
		fsum.nextRepo = now.Add(time.Second)
	}
	if staterr != nil {
		fsum.Err += 1
		return fsum
	}
	fsum.Files += 1
	if 1 < fstat.Nlink {
		// must count only once...
		// at least ignore ones in the same directory
		//if finfo.Mode().IsRegular() {
		if (fstat.Mode & S_IFMT) == S_IFREG {
			fsum.HLinks += 1
			fsum.DupBlocks += int64(fstat.Blocks)
			//fmt.Printf("---Dup HardLink %v %s\n",fstat.Nlink,path)
		}
	}
	//fsum.Size += finfo.Size()
	fsum.Size += fstat.Size
	fsum.Blocks += int64(fstat.Blocks) 
	//if verb { fmt.Printf("(%8dBlk) %s",fstat.Blocks/2,path) }
	if isin("-ls",argv){
		//if verb { fmt.Printf("%4d %8d ",fstat.Blksize,fstat.Blocks) }
//		fmt.Printf("%d\t",fstat.Blocks/2)
	}
	//if finfo.IsDir()
	if (fstat.Mode & S_IFMT) == S_IFDIR {
		fsum.Dirs += 1
	}
	//if (finfo.Mode() & os.ModeSymlink) != 0 
	if (fstat.Mode & S_IFMT) == S_IFLNK {
		//if verb { fmt.Printf("symlink(%v,%s)\n",fstat.Mode,finfo.Name()) }
		//{ fmt.Printf("symlink(%o,%s)\n",fstat.Mode,finfo.Name()) }
		fsum.SymLink += 1
	}
	return fsum
}
func xxFindEntv(gshCtx GshContext,depth int,total *fileSum,dir string, dstat syscall.Stat_t, ei int, entv []string,npatv[]string,argv[]string)(GshContext,*fileSum){
	nols := isin("-grep",argv)
	// sort entv
	/*
	if isin("-t",argv){
		sort.Slice(filev, func(i,j int) bool {
			return 0 < filev[i].ModTime().Sub(filev[j].ModTime())
		})
	}
	*/
		/*
		if isin("-u",argv){
			sort.Slice(filev, func(i,j int) bool {
				return 0 < filev[i].AccTime().Sub(filev[j].AccTime())
			})
		}
		if isin("-U",argv){
			sort.Slice(filev, func(i,j int) bool {
				return 0 < filev[i].CreatTime().Sub(filev[j].CreatTime())
			})
		}
		*/
	/*
	if isin("-S",argv){
		sort.Slice(filev, func(i,j int) bool {
			return filev[j].Size() < filev[i].Size()
		})
	}
	*/
	for _,filename := range entv {
		for _,npat := range npatv {
			match := true
			if npat == "*" {
				match = true
			}else{
				match, _ = filepath.Match(npat,filename)
			}
			path := dir + DIRSEP + filename
			if !match {
				continue
			}
			var fstat syscall.Stat_t
			staterr := syscall.Lstat(path,&fstat)
			if staterr != nil {
				if !isin("-w",argv){fmt.Printf("ufind: %v\n",staterr) }
				continue;
			}
			if isin("-du",argv) && (fstat.Mode & S_IFMT) == S_IFDIR {
				// should not show size of directory in "-du" mode ...
			}else
			if !nols && !isin("-s",argv) && (!isin("-du",argv) || isin("-a",argv)) {
				if isin("-du",argv) {
					fmt.Printf("%d\t",fstat.Blocks/2)
				}
				showFileInfo(path,argv)
			}
			if true { // && isin("-du",argv)
				total = cumFinfo(total,path,staterr,fstat,argv,false)
			}
			/*
			if isin("-wc",argv) {
			}
			*/
			x := isinX("-grep",argv); // -grep will be convenient like -ls
			if 0 <= x && x+1 <= len(argv) { // -grep will be convenient like -ls
				if IsRegFile(path){
					found := xGrep(gshCtx,path,argv[x+1:])
					if 0 < found {
						foundv := gshCtx.CmdCurrent.FoundFile
						if len(foundv) < 10 {
							gshCtx.CmdCurrent.FoundFile =
							append(gshCtx.CmdCurrent.FoundFile,path)
						}
					}
				}
			}
			if !isin("-r0",argv) { // -d 0 in du, -depth n in find
				//total.Depth += 1
				if (fstat.Mode & S_IFMT) == S_IFLNK {
					continue
				}
				if dstat.Rdev != fstat.Rdev {
					fmt.Printf("--I-- don't follow differnet device %v(%v) %v(%v)\n",
						dir,dstat.Rdev,path,fstat.Rdev)
				}
				if (fstat.Mode & S_IFMT) == S_IFDIR {
					gshCtx,total = xxFind(gshCtx,depth+1,total,path,npatv,argv)
				}
			}
		}
	}
	return gshCtx,total
}
func xxFind(gshCtx GshContext,depth int,total *fileSum,dir string,npatv[]string,argv[]string)(GshContext,*fileSum){
	nols := isin("-grep",argv)
	dirfile,oerr := os.OpenFile(dir,os.O_RDONLY,0)
	if oerr == nil {
		//fmt.Printf("--I-- %v(%v)[%d]\n",dir,dirfile,dirfile.Fd())
		defer dirfile.Close()
	}else{
	}

	prev := *total
	var dstat syscall.Stat_t
	staterr := syscall.Lstat(dir,&dstat) // should be flstat

	if staterr != nil {
		if !isin("-w",argv){ fmt.Printf("ufind: %v\n",staterr) }
		return gshCtx,total
	}
		//filev,err := ioutil.ReadDir(dir)
		//_,err := ioutil.ReadDir(dir) // ReadDir() heavy and bad for huge directory
		/*
		if err != nil {
			if !isin("-w",argv){ fmt.Printf("ufind: %v\n",err) }
			return total
		}
		*/
	if depth == 0 {
		total = cumFinfo(total,dir,staterr,dstat,argv,true)
		if !nols && !isin("-s",argv) && (!isin("-du",argv) || isin("-a",argv)) {
			showFileInfo(dir,argv)
		}
	}
	// it it is not a directory, just scan it and finish

	for ei := 0; ; ei++ {
		entv,rderr := dirfile.Readdirnames(8*1024)
		if len(entv) == 0 || rderr != nil {
			//if rderr != nil { fmt.Printf("[%d] len=%d (%v)\n",ei,len(entv),rderr) }
			break
		}
		if 0 < ei {
			fmt.Printf("--I-- xxFind[%d] %d large-dir: %s\n",ei,len(entv),dir)
		}
		gshCtx,total = xxFindEntv(gshCtx,depth,total,dir,dstat,ei,entv,npatv,argv)
	}
	if isin("-du",argv) {
		// if in "du" mode
		fmt.Printf("%d\t%s\n",(total.Blocks-prev.Blocks)/2,dir)
	}
	return gshCtx,total
}

// {ufind|fu|ls} [Files] [// Names] [-- Expressions]
//  Files is "." by default
//  Names is "*" by default
//  Expressions is "-print" by default for "ufind", or -du for "fu" command
func xFind(gshCtx GshContext,argv[]string)(GshContext){
	if 0 < len(argv) && strBegins(argv[0],"?"){
		showFound(gshCtx,argv)
		return gshCtx
	}
	var total = fileSum{}
	npats := []string{}
	for _,v := range argv {
		if 0 < len(v) && v[0] != '-' {
			npats = append(npats,v)
		}
		if v == "//" { break }
		if v == "--" { break }
		if v == "-grep" { break }
		if v == "-ls" { break }
	}
	if len(npats) == 0 {
		npats = []string{"*"}
	}
	cwd := "."
	// if to be fullpath ::: cwd, _ := os.Getwd()
	if len(npats) == 0 { npats = []string{"*"} }
	gshCtx,fusage := xxFind(gshCtx,0,&total,cwd,npats,argv)
	if !isin("-grep",argv) {
		showFusage("total",fusage)
	}
	return gshCtx
}

func showFiles(files[]string){
	sp := ""
	for i,file := range files {
		if 0 < i { sp = " " } else { sp = "" }
		fmt.Printf(sp+"%s",escapeWhiteSP(file))
	}
}
func showFound(gshCtx GshContext, argv[]string){
	for i,v := range gshCtx.CommandHistory {
		if 0 < len(v.FoundFile) {
			fmt.Printf("!%d (%d) ",i,len(v.FoundFile))
			if isin("-ls",argv){
				fmt.Printf("\n")
				for _,file := range v.FoundFile {
					fmt.Printf("") //sub number?
					showFileInfo(file,argv)
				}
			}else{
				showFiles(v.FoundFile)
				fmt.Printf("\n")
			}
		}
	}
}

func showMatchFile(filev []os.FileInfo, npat,dir string, argv[]string)(string,bool){
	fname := ""
	found := false
	for _,v := range filev {
		match, _ := filepath.Match(npat,(v.Name()))
		if match {
			fname = v.Name()
			found = true
			//fmt.Printf("[%d] %s\n",i,v.Name())
			showIfExecutable(fname,dir,argv)
		}
	}
	return fname,found
}
func showIfExecutable(name,dir string,argv[]string)(ffullpath string,ffound bool){
	var fullpath string
	if strBegins(name,DIRSEP){
		fullpath = name
	}else{
		fullpath = dir + DIRSEP + name
	}
	fi, err := os.Stat(fullpath)
	if err != nil {
		fullpath = dir + DIRSEP + name + ".go"
		fi, err = os.Stat(fullpath)
	}
	if err == nil {
		fm := fi.Mode()
		if fm.IsRegular() {
		  // R_OK=4, W_OK=2, X_OK=1, F_OK=0
		  if syscall.Access(fullpath,5) == nil {
			ffullpath = fullpath
			ffound = true
			if ! isin("-s", argv) {
				showFileInfo(fullpath,argv)
			}
		  }
		}
	}
	return ffullpath, ffound
}
func which(list string, argv []string) (fullpathv []string, itis bool){
	if len(argv) <= 1 {
		fmt.Printf("Usage: which comand [-s] [-a] [-ls]\n")
		return []string{""}, false
	}
	path := argv[1]
	if strBegins(path,"/") {
		// should check if excecutable?
		_,exOK := showIfExecutable(path,"/",argv)
		fmt.Printf("--D-- %v exOK=%v\n",path,exOK)
		return []string{path},exOK
	}
	pathenv, efound := os.LookupEnv(list)
	if ! efound {
		fmt.Printf("--E-- which: no \"%s\" environment\n",list)
		return []string{""}, false
	}
	showall := isin("-a",argv) || 0 <= strings.Index(path,"*")
	dirv := strings.Split(pathenv,PATHSEP)
	ffound := false
	ffullpath := path
	for _, dir := range dirv {
		if 0 <= strings.Index(path,"*") { // by wild-card
			list,_ := ioutil.ReadDir(dir)
			ffullpath, ffound = showMatchFile(list,path,dir,argv)
		}else{
			ffullpath, ffound = showIfExecutable(path,dir,argv)
		}
		//if ffound && !isin("-a", argv) {
		if ffound && !showall {
			break;
		}
	}
	return []string{ffullpath}, ffound
}

func stripLeadingWSParg(argv[]string)([]string){
	for ; 0 < len(argv); {
		if len(argv[0]) == 0 {
			argv = argv[1:]
		}else{
			break
		}
	}
	return argv
}
func xEval(argv []string, nlend bool){
	argv = stripLeadingWSParg(argv)
	if len(argv) == 0 {
		fmt.Printf("eval [%%format] [Go-expression]\n")
		return
	}
	pfmt := "%v"
	if argv[0][0] == '%' {
		pfmt = argv[0]
		argv = argv[1:]
	}
	if len(argv) == 0 {
		return
	}
	gocode := strings.Join(argv," ");
	//fmt.Printf("eval [%v] [%v]\n",pfmt,gocode)
	fset := token.NewFileSet()
	rval, _ := types.Eval(fset,nil,token.NoPos,gocode)
	fmt.Printf(pfmt,rval.Value)
	if nlend { fmt.Printf("\n") }
}

func getval(name string) (found bool, val int) {
	/* should expand the name here */
	if name == "gsh.pid" {
		return true, os.Getpid()
	}else
	if name == "gsh.ppid" {
		return true, os.Getppid()
	}
	return false, 0
}

func echo(argv []string, nlend bool){
	for ai := 1; ai < len(argv); ai++ {
		if 1 < ai {
			fmt.Printf(" ");
		}
		arg := argv[ai]
		found, val := getval(arg)
		if found {
			fmt.Printf("%d",val)
		}else{
			fmt.Printf("%s",arg)
		}
	}
	if nlend {
		fmt.Printf("\n");
	}
}

func resfile() string {
	return "gsh.tmp"
}
//var resF *File
func resmap() {
	//_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, os.ModeAppend)
	// https://developpaper.com/solution-to-golang-bad-file-descriptor-problem/
	_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		fmt.Printf("refF could not open: %s\n",err)
	}else{
		fmt.Printf("refF opened\n")
	}
}

// External commands
func excommand(gshCtx GshContext, exec bool, argv []string) (GshContext, bool) {
	if gshCtx.CmdTrace { fmt.Printf("--I-- excommand[%v](%v)\n",exec,argv) }

	gshPA := gshCtx.gshPA
	fullpathv, itis := which("PATH",[]string{"which",argv[0],"-s"})
	if itis == false {
		return gshCtx, true
	}
	fullpath := fullpathv[0]
	argv = unescapeWhiteSPV(argv)
	if 0 < strings.Index(fullpath,".go") {
		nargv := argv // []string{}
		gofullpathv, itis := which("PATH",[]string{"which","go","-s"})
		if itis == false {
			fmt.Printf("--F-- Go not found\n")
			return gshCtx, true
		}
		gofullpath := gofullpathv[0]
		nargv = []string{ gofullpath, "run", fullpath }
		fmt.Printf("--I-- %s {%s %s %s}\n",gofullpath,
			nargv[0],nargv[1],nargv[2])
		if exec {
			syscall.Exec(gofullpath,nargv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(gofullpath,nargv,&gshPA)
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage)
				gshCtx.LastRusage = rusage
				gshCtx.CmdCurrent.Rusagev[1] = rusage
			}
		}
	}else{
		if exec {
			syscall.Exec(fullpath,argv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(fullpath,argv,&gshPA)
			//fmt.Printf("[%d]\n",pid); // '&' to be background
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage);
				gshCtx.LastRusage = rusage
				gshCtx.CmdCurrent.Rusagev[1] = rusage
			}
		}
	}
	return gshCtx, false
}

// Builtin Commands
func sleep(gshCtx GshContext, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Sleep 100ms, 100us, 100ns, ...\n")
		return
	}
	duration := argv[1];
	d, err := time.ParseDuration(duration)
	if err != nil {
		d, err = time.ParseDuration(duration+"s")
		if err != nil {
			fmt.Printf("duration ? %s (%s)\n",duration,err)
			return
		}
	}
	//fmt.Printf("Sleep %v\n",duration)
	time.Sleep(d)
	if 0 < len(argv[2:]) {
		gshellv(gshCtx, argv[2:])
	}
}
func repeat(gshCtx GshContext, argv []string) {
	if len(argv) < 2 {
		return
	}
	start0 := time.Now()
	for ri,_ := strconv.Atoi(argv[1]); 0 < ri; ri-- {
		if 0 < len(argv[2:]) {
			//start := time.Now()
			gshellv(gshCtx, argv[2:])
			end := time.Now()
			elps := end.Sub(start0);
			if( 1000000000 < elps ){
				fmt.Printf("(repeat#%d %v)\n",ri,elps);
			}
		}
	}
}

func gen(gshCtx GshContext, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: %s N\n",argv[0])
		return
	}
	// should br repeated by "repeat" command
	count, _ := strconv.Atoi(argv[1])
	fd := gshPA.Files[1] // Stdout
	file := os.NewFile(fd,"internalStdOut")
	fmt.Printf("--I-- Gen. Count=%d to [%d]\n",count,file.Fd())
	//buf := []byte{}
	outdata := "0123 5678 0123 5678 0123 5678 0123 5678\r"
	for gi := 0; gi < count; gi++ {
		file.WriteString(outdata)
	}
	//file.WriteString("\n")
	fmt.Printf("\n(%d B)\n",count*len(outdata));
	//file.Close()
}

// network
// -s, -si, -so // bi-directional, source, sync (maybe socket)
func sconnect(gshCtx GshContext, inTCP bool, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: -s [host]:[port[.udp]]\n")
		return
	}
	remote := argv[1]
	if remote == ":" { remote = "0.0.0.0:9999" }

	if inTCP { // TCP
		dport, err := net.ResolveTCPAddr("tcp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		conn, err := net.DialTCP("tcp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()
		fmt.Printf("Socket: connected to %s, socket[%d]\n",remote,fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}else{
		//dport, err := net.ResolveUDPAddr("udp4",remote);
		dport, err := net.ResolveUDPAddr("udp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		//conn, err := net.DialUDP("udp4",nil,dport)
		conn, err := net.DialUDP("udp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()

		ar := conn.RemoteAddr()
		//al := conn.LocalAddr()
		fmt.Printf("Socket: connected to %s [%s], socket[%d]\n",
			remote,ar.String(),fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}
}
func saccept(gshCtx GshContext, inTCP bool, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: -ac [host]:[port[.udp]]\n")
		return
	}
	local := argv[1]
	if local == ":" { local = "0.0.0.0:9999" }
	if inTCP { // TCP
		port, err := net.ResolveTCPAddr("tcp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Listen at %s...\n",local);
		sconn, err := net.ListenTCP("tcp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Accepting at %s...\n",local);
		aconn, err := sconn.AcceptTCP()
		if err != nil {
			fmt.Printf("Accept error: %s (%s)\n",local,err)
			return
		}
		file, _ := aconn.File()
		fd := file.Fd()
		fmt.Printf("Accepted TCP at %s [%d]\n",local,fd)

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[0] = savfd

		sconn.Close();
		aconn.Close();
		file.Close();
	}else{
		//port, err := net.ResolveUDPAddr("udp4",local);
		port, err := net.ResolveUDPAddr("udp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		fmt.Printf("Listen UDP at %s...\n",local);
		//uconn, err := net.ListenUDP("udp4", port)
		uconn, err := net.ListenUDP("udp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		file, _ := uconn.File()
		fd := file.Fd()
		ar := uconn.RemoteAddr()
		remote := ""
		if ar != nil { remote = ar.String() }
		if remote == "" { remote = "?" }

		// not yet received
		//fmt.Printf("Accepted at %s [%d] <- %s\n",local,fd,"")

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		savenv := gshPA.Env
		gshPA.Env = append(savenv, "REMOTE_HOST="+remote)
		gshellv(gshCtx, argv[2:])
		gshPA.Env = savenv
		gshPA.Files[0] = savfd

		uconn.Close();
		file.Close();
	}
}

// empty line command
func xPwd(gshCtx GshContext, argv[]string){
	// execute context command, pwd + date
	// context notation, representation scheme, to be resumed at re-login
	cwd, _ := os.Getwd()
	switch {
	case isin("-a",argv):
		xChdirHistory(gshCtx,argv)
	case isin("-ls",argv):
		showFileInfo(cwd,argv)
	default:
		fmt.Printf("%s\n",cwd)
	case isin("-v",argv): // obsolete emtpy command
		t := time.Now()
		date := t.Format(time.UnixDate)
		exe, _ := os.Executable()
		host, _ := os.Hostname()
		fmt.Printf("{PWD=\"%s\"",cwd)
		fmt.Printf(" HOST=\"%s\"",host)
		fmt.Printf(" DATE=\"%s\"",date)
		fmt.Printf(" TIME=\"%s\"",t.String())
		fmt.Printf(" PID=\"%d\"",os.Getpid())
		fmt.Printf(" EXE=\"%s\"",exe)
		fmt.Printf("}\n")
	}
}

// History
// these should be browsed and edited by HTTP browser
// show the time of command with -t and direcotry with -ls
// openfile-history, sort by -a -m -c
// sort by elapsed time by -t -s
// search by "more" like interface
// edit history
// sort history, and wc or uniq
// CPU and other resource consumptions
// limit showing range (by time or so)
// export / import history
func xHistory(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	for i, v := range gshCtx.CommandHistory {
		// exclude commands not to be listed by default
		// internal commands may be suppressed by default
		if v.CmdLine == "" && !isin("-a",argv) {
			continue;
		}
		if !isin("-n",argv){ // like "fc"
			fmt.Printf("!%-3d ",i)
		}
		if isin("-v",argv){
			fmt.Println(v) // should be with it date
		}else{
			if isin("-l",argv) || isin("-l0",argv) {
				elps := v.EndAt.Sub(v.StartAt);
				start := v.StartAt.Format(time.Stamp)
				fmt.Printf("%s %11v/t ",start,elps)
			}
			if isin("-l",argv) && !isin("-l0",argv){
				fmt.Printf("%v",Rusagef("%t %u %s",argv,v.Rusagev))
			}
			if isin("-ls",argv){
				fmt.Printf("@%s ",v.WorkDir)
				// show the FileInfo of the output command??
			}
			fmt.Printf("%s",v.CmdLine)
			fmt.Printf("\n")
		}
	}
	return gshCtx
}
// !n - history index
func searchHistory(gshCtx GshContext, gline string) (string, bool, bool){
	if gline[0] == '!' {
		hix, err := strconv.Atoi(gline[1:])
		if err != nil {
			fmt.Printf("--E-- (%s : range)\n",hix)
			return "", false, true
		}
		if hix < 0 || len(gshCtx.CommandHistory) <= hix {
			fmt.Printf("--E-- (%d : out of range)\n",hix)
			return "", false, true
		}
		return gshCtx.CommandHistory[hix].CmdLine, false, false
	}
	// search
	//for i, v := range gshCtx.CommandHistory {
	//}
	return gline, false, false
}

// temporary adding to PATH environment
// cd name -lib for LD_LIBRARY_PATH
// chdir with directory history (date + full-path)
// -s for sort option (by visit date or so)
func xChdirHistory(gshCtx GshContext, argv []string){
	for i, v := range gshCtx.ChdirHistory {
		fmt.Printf("!%d ",i)
		fmt.Printf("%v ",v.MovedAt.Format(time.Stamp))
		showFileInfo(v.Dir,argv)
	}
}
func xChdir(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	cdhist := gshCtx.ChdirHistory
	if isin("?",argv ) || isin("-t",argv) {
		xChdirHistory(gshCtx,argv)
		return gshCtx
	}
	pwd, _ := os.Getwd()
	dir := ""
	if len(argv) <= 1 {
		dir = toFullpath("~")
	}else{
		dir = argv[1]
	}
	if strBegins(dir,"!") {
		if dir == "!0" {
			dir = gshCtx.StartDir
		}else
		if dir == "!!" {
			index := len(cdhist) - 1
			if 0 < index { index -= 1 }
			dir = cdhist[index].Dir
		}else{
			index, err := strconv.Atoi(dir[1:])
			if err != nil {
				fmt.Printf("--E-- xChdir(%v)\n",err)
				dir = "?"
			}else
			if len(gshCtx.ChdirHistory) <= index {
				fmt.Printf("--E-- xChdir(history range error)\n")
				dir = "?"
			}else{
				dir = cdhist[index].Dir
			}
		}
	}
	if dir != "?" {
		err := os.Chdir(dir)
		if err != nil {
			fmt.Printf("--E-- xChdir(%s)(%v)\n",argv[1],err)
		}else{
			cwd, _ := os.Getwd()
			if cwd != pwd {
				hist1 := GChdirHistory { }
				hist1.Dir = cwd
				hist1.MovedAt = time.Now()
				gshCtx.ChdirHistory = append(cdhist,hist1)
			}
		}
	}
	if isin("-ls",argv){
		cwd, _ := os.Getwd()
		showFileInfo(cwd,argv);
	}
	return gshCtx
}
func TimeValSub(tv1 *syscall.Timeval, tv2 *syscall.Timeval){
	*tv1 = syscall.NsecToTimeval(tv1.Nano() - tv2.Nano())  
}
func RusageSubv(ru1, ru2 [2]syscall.Rusage)([2]syscall.Rusage){
	TimeValSub(&ru1[0].Utime,&ru2[0].Utime)
	TimeValSub(&ru1[0].Stime,&ru2[0].Stime)
	TimeValSub(&ru1[1].Utime,&ru2[1].Utime)
	TimeValSub(&ru1[1].Stime,&ru2[1].Stime)
	return ru1
}
func TimeValAdd(tv1 syscall.Timeval, tv2 syscall.Timeval)(syscall.Timeval){
	tvs := syscall.NsecToTimeval(tv1.Nano() + tv2.Nano())  
	return tvs
}
/*
func RusageAddv(ru1, ru2 [2]syscall.Rusage)([2]syscall.Rusage){
	TimeValAdd(ru1[0].Utime,ru2[0].Utime)
	TimeValAdd(ru1[0].Stime,ru2[0].Stime)
	TimeValAdd(ru1[1].Utime,ru2[1].Utime)
	TimeValAdd(ru1[1].Stime,ru2[1].Stime)
	return ru1
}
*/

// Resource Usage
func Rusagef(fmtspec string, argv []string, ru [2]syscall.Rusage)(string){
	ut := TimeValAdd(ru[0].Utime,ru[1].Utime)
	st := TimeValAdd(ru[0].Stime,ru[1].Stime)
	fmt.Printf("%d.%06ds/u ",ut.Sec,ut.Usec) //ru[1].Utime.Sec,ru[1].Utime.Usec)
	fmt.Printf("%d.%06ds/s ",st.Sec,st.Usec) //ru[1].Stime.Sec,ru[1].Stime.Usec)
	return ""
}
func Getrusagev()([2]syscall.Rusage){
	var ruv = [2]syscall.Rusage{}
	syscall.Getrusage(syscall.RUSAGE_SELF,&ruv[0])
	syscall.Getrusage(syscall.RUSAGE_CHILDREN,&ruv[1])
	return ruv
}
func showRusage(what string,argv []string, ru *syscall.Rusage){
	fmt.Printf("%s: ",what);
	fmt.Printf("Usr=%d.%06ds",ru.Utime.Sec,ru.Utime.Usec)
	fmt.Printf(" Sys=%d.%06ds",ru.Stime.Sec,ru.Stime.Usec)
	fmt.Printf(" Rss=%vB",ru.Maxrss)
	if isin("-l",argv) {
		fmt.Printf(" MinFlt=%v",ru.Minflt)
		fmt.Printf(" MajFlt=%v",ru.Majflt)
		fmt.Printf(" IxRSS=%vB",ru.Ixrss)
		fmt.Printf(" IdRSS=%vB",ru.Idrss)
		fmt.Printf(" Nswap=%vB",ru.Nswap)
	fmt.Printf(" Read=%v",ru.Inblock)
	fmt.Printf(" Write=%v",ru.Oublock)
	}
	fmt.Printf(" Snd=%v",ru.Msgsnd)
	fmt.Printf(" Rcv=%v",ru.Msgrcv)
	//if isin("-l",argv) {
		fmt.Printf(" Sig=%v",ru.Nsignals)
	//}
	fmt.Printf("\n");
}
func xTime(gshCtx GshContext, argv[]string)(GshContext,bool){
	if 2 <= len(argv){
		gshCtx.LastRusage = syscall.Rusage{}
		rusagev1 := Getrusagev()
		xgshCtx, fin := gshellv(gshCtx,argv[1:])
		rusagev2 := Getrusagev()
		gshCtx = xgshCtx
		showRusage(argv[1],argv,&gshCtx.LastRusage)
		rusagev := RusageSubv(rusagev2,rusagev1)
		showRusage("self",argv,&rusagev[0])
		showRusage("chld",argv,&rusagev[1])
		return gshCtx, fin
	}else{
		rusage:= syscall.Rusage {}
		syscall.Getrusage(syscall.RUSAGE_SELF,&rusage)
		showRusage("self",argv, &rusage)
		syscall.Getrusage(syscall.RUSAGE_CHILDREN,&rusage)
		showRusage("chld",argv, &rusage)
		return gshCtx, false
	}
}
func xJobs(gshCtx GshContext, argv[]string){
	fmt.Printf("%d Jobs\n",len(gshCtx.BackGroundJobs))
	for ji, pid := range gshCtx.BackGroundJobs {
		//wstat := syscall.WaitStatus {0}
		rusage := syscall.Rusage {}
		//wpid, err := syscall.Wait4(pid,&wstat,syscall.WNOHANG,&rusage);
		wpid, err := syscall.Wait4(pid,nil,syscall.WNOHANG,&rusage);
		if err != nil {
			fmt.Printf("--E-- %%%d [%d] (%v)\n",ji,pid,err)
		}else{
			fmt.Printf("%%%d[%d](%d)\n",ji,pid,wpid)
			showRusage("chld",argv,&rusage)
		}
	}
}
func inBackground(gshCtx GshContext, argv[]string)(GshContext,bool){
	if gshCtx.CmdTrace { fmt.Printf("--I-- inBackground(%v)\n",argv) }
	gshCtx.BackGround = true // set background option
	xfin := false
	gshCtx, xfin = gshellv(gshCtx,argv)
	gshCtx.BackGround = false
	return gshCtx,xfin
}
// -o file without command means just opening it and refer by #N
// should be listed by "files" comnmand
func xOpen(gshCtx GshContext, argv[]string)(GshContext){
	var pv = []int{-1,-1}
	err := syscall.Pipe(pv)
	fmt.Printf("--I-- pipe()=[#%d,#%d](%v)\n",pv[0],pv[1],err)
	return gshCtx
}
func fromPipe(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}
func xClose(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}

// redirect
func redirect(gshCtx GshContext, argv[]string)(GshContext,bool){
	if len(argv) < 2 {
		return gshCtx, false
	}

	cmd := argv[0]
	fname := argv[1]
	var file *os.File = nil

	fdix := 0
	mode := os.O_RDONLY

	switch {
	case cmd == "-i" || cmd == "<":
		fdix = 0
		mode = os.O_RDONLY
	case cmd == "-o" || cmd == ">":
		fdix = 1
		mode = os.O_RDWR | os.O_CREATE
	case cmd == "-a" || cmd == ">>":
		fdix = 1
		mode = os.O_RDWR | os.O_CREATE | os.O_APPEND
	}
	if fname[0] == '#' {
		fd, err := strconv.Atoi(fname[1:])
		if err != nil {
			fmt.Printf("--E-- (%v)\n",err)
			return gshCtx, false
		}
		file = os.NewFile(uintptr(fd),"MaybePipe")
	}else{
		xfile, err := os.OpenFile(argv[1], mode, 0600)
		if err != nil {
			fmt.Printf("--E-- (%s)\n",err)
			return gshCtx, false
		}
		file = xfile
	}
	gshPA := gshCtx.gshPA
	savfd := gshPA.Files[fdix]
	gshPA.Files[fdix] = file.Fd()
	fmt.Printf("--I-- Opened [%d] %s\n",file.Fd(),argv[1])
	gshCtx, _ = gshellv(gshCtx, argv[2:])
	gshPA.Files[fdix] = savfd

	return gshCtx, false
}

//fmt.Fprintf(res, "GShell Status: %q", html.EscapeString(req.URL.Path))
func httpHandler(res http.ResponseWriter, req *http.Request){
	path := req.URL.Path
	fmt.Printf("--I-- Got HTTP Request(%s)\n",path)
	{
		gshCtx, _ :=  setupGshContext()
		fmt.Printf("--I-- %s\n",path[1:])
		gshCtx, _ = tgshelll(gshCtx,path[1:])
	}
	fmt.Fprintf(res, "Hello(^-^)/\n%s\n",path)
}
func httpServer(gshCtx GshContext, argv []string){
	http.HandleFunc("/", httpHandler)
	accport := "localhost:9999"
	fmt.Printf("--I-- HTTP Server Start at [%s]\n",accport)
	http.ListenAndServe(accport,nil)
}
func xGo(gshCtx GshContext, argv[]string){
	go gshellv(gshCtx,argv[1:]);
}
func xPs(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}

// Plugin
// plugin [-ls [names]] to list plugins
// Reference: plugin source code
func whichPlugin(gshCtx GshContext,name string,argv[]string)(pi *PluginInfo){
	pi = nil	
	for _,p := range gshCtx.PluginFuncs {
		if p.Name == name && pi == nil {
			pi = &p
		}
		if !isin("-s",argv){
			//fmt.Printf("%v %v ",i,p)
			if isin("-ls",argv){
				showFileInfo(p.Path,argv)
			}else{
				fmt.Printf("%s\n",p.Name)
			}
		}
	}
	return pi
}
func xPlugin(gshCtx GshContext, argv[]string)(GshContext,error){
	if len(argv) == 0 || argv[0] == "-ls" {
		whichPlugin(gshCtx,"",argv)
		return gshCtx, nil
	}
	name := argv[0]
	Pin := whichPlugin(gshCtx,name,[]string{"-s"})
	if Pin != nil {
		os.Args = argv // should be recovered?
		Pin.Addr.(func())()
		return gshCtx,nil
	}
	sofile := toFullpath(argv[0] + ".so") // or find it by which($PATH)

	p, err := plugin.Open(sofile)
	if err != nil {
		fmt.Printf("--E-- plugin.Open(%s)(%v)\n",sofile,err)
		return gshCtx, err
	}
	fname := "Main"
	f, err := p.Lookup(fname)
	if( err != nil ){
		fmt.Printf("--E-- plugin.Lookup(%s)(%v)\n",fname,err)
		return gshCtx, err
	}
	pin := PluginInfo {p,f,name,sofile} 
	gshCtx.PluginFuncs = append(gshCtx.PluginFuncs,pin)
	fmt.Printf("--I-- added (%d)\n",len(gshCtx.PluginFuncs))

	//fmt.Printf("--I-- first call(%s:%s)%v\n",sofile,fname,argv)
	os.Args = argv
	f.(func())()
	return gshCtx, err
}
func Args(gshCtx *GshContext, argv[]string){
	for i,v := range os.Args {
		fmt.Printf("[%v] %v\n",i,v)
	}
}
func Version(gshCtx *GshContext, argv[]string){
	if isin("-l",argv) {
		fmt.Printf("%v/%v (%v)",NAME,VERSION,DATE);
	}else{
		fmt.Printf("%v",VERSION);
	}
	if !isin("-n",argv) {
		fmt.Printf("\n")
	}
}

// Command Interpreter
func gshellv(gshCtx GshContext, argv []string) (_ GshContext, fin bool) {
	fin = false

	if gshCtx.CmdTrace { fmt.Fprintf(os.Stderr,"--I-- gshellv((%d))\n",len(argv)) }
	if len(argv) <= 0 {
		return gshCtx, false
	}
	xargv := []string{}
	for ai := 0; ai < len(argv); ai++ {
		xargv = append(xargv,strsubst(&gshCtx,argv[ai],false))
	}
	argv = xargv
	if false {
		for ai := 0; ai < len(argv); ai++ {
			fmt.Printf("[%d] %s [%d]%T\n",
				ai,argv[ai],len(argv[ai]),argv[ai])
		}
	}
	cmd := argv[0]
	if gshCtx.CmdTrace { fmt.Fprintf(os.Stderr,"--I-- gshellv(%d)%v\n",len(argv),argv) }
	switch { // https://tour.golang.org/flowcontrol/11
	case cmd == "":
		xPwd(gshCtx,[]string{}); // emtpy command
	case cmd == "-x":
		gshCtx.CmdTrace = ! gshCtx.CmdTrace
	case cmd == "-ot":
		sconnect(gshCtx, true, argv)
	case cmd == "-ou":
		sconnect(gshCtx, false, argv)
	case cmd == "-it":
		saccept(gshCtx, true , argv)
	case cmd == "-iu":
		saccept(gshCtx, false, argv)
	case cmd == "-i" || cmd == "<" || cmd == "-o" || cmd == ">" || cmd == "-a" || cmd == ">>" || cmd == "-s" || cmd == "><":
		redirect(gshCtx, argv)
	case cmd == "|":
		gshCtx = fromPipe(gshCtx, argv)
	case cmd == "args":
		Args(&gshCtx,argv)
	case cmd == "bg" || cmd == "-bg":
		rgshCtx, rfin := inBackground(gshCtx,argv[1:])
		return rgshCtx, rfin
	case cmd == "call":
		gshCtx, _ = excommand(gshCtx, false,argv[1:])
	case cmd == "cd" || cmd == "chdir":
		gshCtx = xChdir(gshCtx,argv);
	case cmd == "close":
		gshCtx = xClose(gshCtx,argv)
	case cmd == "dec" || cmd == "decode":
		Dec(&gshCtx,argv)
	case cmd == "#define":
	case cmd == "echo":
		echo(argv,true)
	case cmd == "enc" || cmd == "encode":
		Enc(&gshCtx,argv)
	case cmd == "env":
		env(argv)
	case cmd == "eval":
		xEval(argv[1:],true)
	case cmd == "exec":
		gshCtx, _ = excommand(gshCtx, true,argv[1:])
		// should not return here
	case cmd == "exit" || cmd == "quit":
		// write Result code EXIT to 3>
		return gshCtx, true
	case cmd == "-find" || cmd == "fin" || cmd == "ufind" || cmd == "uf" || cmd == "fu":
		gshCtx = xFind(gshCtx,argv[1:])
	case cmd == "fork":
		// mainly for a server
	case cmd == "-gen":
		gen(gshCtx, argv)
	case cmd == "-go":
		xGo(gshCtx, argv)
	case cmd == "-grep":
		gshCtx = xFind(gshCtx,argv)
	case cmd == "history" || cmd == "hi": // hi should be alias
		gshCtx = xHistory(gshCtx, argv)
	case cmd == "jobs":
		xJobs(gshCtx,argv)
	case cmd == "-ls":
		gshCtx = xFind(gshCtx,argv)
	case cmd == "nop":
	case cmd == "pipe":
		gshCtx = xOpen(gshCtx,argv)
	case cmd == "plug" || cmd == "plugin" || cmd == "pin":
		gshCtx,_ = xPlugin(gshCtx,argv[1:])
	case cmd == "ps":
		xPs(gshCtx,argv)
	case cmd == "pstitle": // to be gsh.title
	case cmd == "repeat" || cmd == "rep": // repeat cond command
		repeat(gshCtx,argv)
	case cmd == "set":
		// set name ...
	case cmd == "serv":
		httpServer(gshCtx,argv)
	case cmd == "sleep":
		sleep(gshCtx,argv)
	case cmd == "lnsp":
		SplitLine(&gshCtx,argv)
	case cmd == "time":
		gshCtx, fin = xTime(gshCtx,argv)
	case cmd == "pwd":
		xPwd(gshCtx,argv);
	case cmd == "ver" || cmd == "-ver" || cmd == "version":
		Version(&gshCtx,argv)
	case cmd == "where":
		// data file or so?
	case cmd == "which":
		which("PATH",argv);
	default:
		if whichPlugin(gshCtx,cmd,[]string{"-s"}) != nil {
			gshCtx, _ = xPlugin(gshCtx,argv)
		}else{
			gshCtx, _ = excommand(gshCtx,false,argv)
		}
	}
	return gshCtx, fin
}

func gshelll(gshCtx GshContext, gline string) (gx GshContext, rfin bool) {
	argv := strings.Split(string(gline)," ")
	gshCtx, fin := gshellv(gshCtx,argv)
	return gshCtx, fin
}
func tgshelll(gshCtx GshContext, gline string) (gx GshContext, xfin bool) {
	start := time.Now()
	gshCtx, fin := gshelll(gshCtx,gline)
	end := time.Now()
	elps := end.Sub(start);
	fmt.Printf("--I-- " + time.Now().Format(time.Stamp) + "(%d.%09ds)\n",
		elps/1000000000,elps%1000000000)
	return gshCtx, fin
}
func Ttyid() (int) {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return 0;
	}
	//fmt.Printf("Stdin: %v Dev=%d\n",
	//	fi.Mode(),fi.Mode()&os.ModeDevice)
	if (fi.Mode() & os.ModeDevice) != 0 {
		stat := syscall.Stat_t{};
		err := syscall.Fstat(0,&stat)
		if err != nil {
			//fmt.Printf("--I-- Stdin: (%v)\n",err)
		}else{
			//fmt.Printf("--I-- Stdin: rdev=%d %d\n",
			//	stat.Rdev&0xFF,stat.Rdev);
			//fmt.Printf("--I-- Stdin: tty%d\n",stat.Rdev&0xFF);
			return int(stat.Rdev & 0xFF)
		}
	}
	return 0
}
func ttyfile(gshCtx GshContext) string {
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	ttyfile := gshCtx.GshHomeDir + "/" + "gsh-tty" +
		 fmt.Sprintf("%02d",gshCtx.TerminalId)
		 //strconv.Itoa(gshCtx.TerminalId)
	//fmt.Printf("--I-- ttyfile=%s\n",ttyfile)
	return ttyfile
}
func ttyline(gshCtx GshContext) (*os.File){
	file, err := os.OpenFile(ttyfile(gshCtx),
		os.O_RDWR|os.O_CREATE|os.O_TRUNC,0600)
	if err != nil {
		fmt.Printf("--F-- cannot open %s (%s)\n",ttyfile(gshCtx),err)
		return file;
	}
	return file
}
// Command Line Editor
func getline(gshCtx GshContext, hix int, skipping, with_exgetline bool, gsh_getlinev[]string, prevline string) (string) {
	if( skipping ){
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}else
	if( with_exgetline && gshCtx.GetLine != "" ){
		//var xhix int64 = int64(hix); // cast
		newenv := os.Environ()
		newenv = append(newenv, "GSH_LINENO="+strconv.FormatInt(int64(hix),10) )

		tty := ttyline(gshCtx)
		tty.WriteString(prevline)
		Pa := os.ProcAttr {
			"", // start dir
			newenv, //os.Environ(),
			[]*os.File{os.Stdin,os.Stdout,os.Stderr,tty},
			nil,
		}
//fmt.Printf("--I-- getline=%s // %s\n",gsh_getlinev[0],gshCtx.GetLine)
proc, err := os.StartProcess(gsh_getlinev[0],[]string{"getline","getline"},&Pa)
		if err != nil {
			fmt.Printf("--F-- getline process error (%v)\n",err)
			// for ; ; { }
			return "exit (getline program failed)"
		}
		//stat, err := proc.Wait()
		proc.Wait()
		buff := make([]byte,LINESIZE)
		count, err := tty.Read(buff)
		//_, err = tty.Read(buff)
		//fmt.Printf("--D-- getline (%d)\n",count)
		if err != nil {
			if ! (count == 0) { // && err.String() == "EOF" ) {
				fmt.Printf("--E-- getline error (%s)\n",err)
			}
		}else{
			//fmt.Printf("--I-- getline OK \"%s\"\n",buff)
		}
		tty.Close()
		return string(buff[0:count])
	}else{
		// if isatty {
			fmt.Printf("!%d",hix)
			fmt.Print(PROMPT)
		// }
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}
}
//
// $USERHOME/.gsh/
//		gsh-rc.txt, or gsh-configure.txt
//              gsh-history.txt
//              gsh-aliases.txt // should be conditional?
//
func gshSetupHomedir(gshCtx GshContext) (GshContext, bool) {
	homedir,found := userHomeDir()
	if !found {
		fmt.Printf("--E-- You have no UserHomeDir\n")
		return gshCtx, true
	}
	gshhome := homedir + "/" + GSH_HOME
	_, err2 := os.Stat(gshhome)
	if err2 != nil {
		err3 := os.Mkdir(gshhome,0700)
		if err3 != nil {
			fmt.Printf("--E-- Could not Create %s (%s)\n",
				gshhome,err3)
			return gshCtx, true
		}
		fmt.Printf("--I-- Created %s\n",gshhome)
	}
	gshCtx.GshHomeDir = gshhome
	return gshCtx, false
}
func setupGshContext()(GshContext,bool){
	gshPA := syscall.ProcAttr {
		"", // the staring directory
		os.Environ(), // environ[]
		[]uintptr{os.Stdin.Fd(),os.Stdout.Fd(),os.Stderr.Fd()},
		nil, // OS specific
	}
	cwd, _ := os.Getwd()
	gshCtx := GshContext {
		cwd, // StartDir
		"", // GetLine
		[]GChdirHistory { {cwd,time.Now()} }, // ChdirHistory
		gshPA,
		[]GCommandHistory{}, //something for invokation?
		GCommandHistory{}, // CmdCurrent
		false,
		[]int{},
		syscall.Rusage{},
		"", // GshHomeDir
		Ttyid(),
		false,
		[]PluginInfo{},
	}
	err := false
	gshCtx, err = gshSetupHomedir(gshCtx)
	return gshCtx, err
}
// Main loop
func script(gshCtxGiven *GshContext) (_ GshContext) {
	gshCtx,err0 := setupGshContext()
	if err0 {
		return gshCtx;
	}
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	//resmap()
	gsh_getlinev, with_exgetline :=
		 which("PATH",[]string{"which","gsh-getline","-s"})
	if with_exgetline {
		gsh_getlinev[0] = toFullpath(gsh_getlinev[0])
		gshCtx.GetLine = toFullpath(gsh_getlinev[0])
	}else{
	fmt.Printf("--W-- No gsh-getline found. Using internal getline.\n");
	}

	ghist0 := gshCtx.CmdCurrent // something special, or gshrc script, or permanent history
	gshCtx.CommandHistory = append(gshCtx.CommandHistory,ghist0)

	prevline := ""
	skipping := false
	for hix := len(gshCtx.CommandHistory); ; {
		gline := getline(gshCtx,hix,skipping,with_exgetline,gsh_getlinev,prevline)
		if skipping {
			if strings.Index(gline,"fi") == 0 {
				fmt.Printf("fi\n");
				skipping = false;
			}else{
				//fmt.Printf("%s\n",gline);
			}
			continue
		}
		if strings.Index(gline,"if") == 0 {
			//fmt.Printf("--D-- if start: %s\n",gline);
			skipping = true;
			continue
		}
		gline = strsubst(&gshCtx,gline,true)
		/*
		// should be cared in substitution ?
		if 0 < len(gline) && gline[0] == '!' {
			xgline, set, err := searchHistory(gshCtx,gline)
			if err {
				continue
			}
			if set {
				// set the line in command line editor
			}
			gline = xgline
		}
		*/
		ghist := gshCtx.CmdCurrent
		ghist.WorkDir,_ = os.Getwd()
		ghist.StartAt = time.Now()
		rusagev1 := Getrusagev()
		gshCtx.CmdCurrent.FoundFile = []string{}
		xgshCtx, fin := tgshelll(gshCtx,gline)
		rusagev2 := Getrusagev()
		ghist.Rusagev = RusageSubv(rusagev2,rusagev1)
		gshCtx = xgshCtx
		ghist.EndAt = time.Now()
		ghist.CmdLine = gline
		ghist.FoundFile = gshCtx.CmdCurrent.FoundFile

		/* record it but not show in list by default
		if len(gline) == 0 {
			continue
		}
		if gline == "hi" || gline == "history" { // don't record it
			continue
		}
		*/
		gshCtx.CommandHistory = append(gshCtx.CommandHistory, ghist)
		if fin {
			break;
		}
		prevline = gline;
		hix++;
	}
	return gshCtx
}
func main() {
	argv := os.Args
	if 1 < len(argv) {
		if isin("version",argv){
			Version(nil,argv)
			return
		}
		comx := isinX("-c",argv)
		if 0 < comx {
			gshCtx,err := setupGshContext()
			if !err {
				gshellv(gshCtx,argv[comx+1:])
			}
			return
		}
	}
	script(nil)
	//gshCtx := script(nil)
	//gshelll(gshCtx,"time")
}
//
//
Consideration
// - inter gsh communication, possibly running in remote hosts -- to be remote shell
// - merged histories of multiple parallel gsh sessions
// - alias as a function
// - instant alias end environ export to the permanent > ~/.gsh/gsh-alias and gsh-environ
// - retrieval PATH of files by its type
// - gsh as an IME
// - gsh a scheduler in precise time of within a millisecond
// - all commands have its subucomand after "---" symbol
// - filename expansion by "-find" command
// - history of ext code and output of each commoand
// - "script" output for each command by pty-tee or telnet-tee
// - $BUILTIN command in PATH to show the priority
// - "?" symbol in the command (not as in arguments) shows help request 
// - searching command with wild card like: which ssh-*
// - longformat prompt after long idle time (should dismiss by BS)
// - customizing by building plugin and dynamically linking it
// - generating syntactic element like "if" by macro expansion (like CPP) >> alias
// - "!" symbol should be used for negation, don't wast it just for job control
// - don't put too long output to tty, record it into GSH_HOME/session-id/comand-id.log
// - making canonical form of command at the start adding quatation or white spaces
// - name(a,b,c) ... use "(" and ")" to show both delimiter and realm
// - name? or name! might be useful
// - htar format - packing directory contents into a single html file using data scheme
//---END--- (^-^)/ITS more
/*
References

The Go Programming Language MDN web docs HTML CSS: Selectors repeat HTTP JavaScript: ...

--> */ //

GShell 0.1.1 − ロゴ案とCSS整備

広報:それでご依頼のGShellのロゴですが、第ゼロ次案としてこのようなものを考えてみました。

社長:いい感じですが、Sがもうちょとふくよかな感じが良いです。

広報:それがなかなか、そのようなフォントがないんですよね… macOS版のパワポではうんざりするほどロゴがあって、その中で30くらいは探したのですが…

社長:やはりCourier は良いですが、エルがちょっとうざいというか、間延び感がどうもですね。

開発:わたしはエルエルの ll が斜体で // みたいに見えるのが好きですね。実は、// というシンボルは、ゆるい順序関係もありパイプライン的な連結関係を表すのに良い記号だと思っていまして、何かに使いたいと思っているのです。

基盤:ところで名前はいっそGOshell にするとか?略してgosh。

開発:まあ、最終的には必ずしもGoにべったりを目指しているわけではありませんので… Gは general みたいなつもりでいたいと思っています。

社長:そういう意味では Unified とか Universal の U も好きですね。

開発:ush。母音字がから始まるところがどうかなとは思いますね。

基盤:では bush とか push とか zush とか:-)

社長:まあ当面は gsh で行きますか。

広報:グレースケールでグラデーションという案も考えてみました。

社長:悪くないと思いますが… これでもPNGで5KBあるんですね。

広報:ちょっとおフランス系とか。

開発:多色だとインパクトはありますね。

基盤:でも、Gまでがパクリだとすると、Gの部分は Goブルーのほうが良いのでは。

広報:ではGは青で、後はおとなしめに案。

広報:あるいはもう、いっそミキハウス案。

社長:うーん、とりあえずおとなしめのLogo005.pngで行きましょう。すみれ色が好きだし。

* * *

開発:ロゴをソースコードに張り込んでみました。

基盤:ソースコードに?

開発:gsh.go は、ソースコード兼、ホームページ兼、ドキュメント兼、…です。Go言語に従っていますが、HTMLでもあります。拡張子を変えると見え方が変わります。たとえばスマホで見るとこう。

開発:デスクトップで見るとこうです。

社長:ロゴは単色の第ゼロ次案なんですね。

開発:現在のページ構成の中では収まりが良いかと。とりあえずです。

開発:インデックスでソースコードの各部にジャンプする仕掛けですが、これは自分でもコードをブラウズするのにも便利しています。 参照しているパッケージにも即飛べます。ソースプロラムも能動的にハイパーテキストであるべきだと思います。アクティブソースコードとでも言いましょうか。

社長:Wikiを編集するようにしてプログラムが書けると良いのかも知れませねん。わたし的にはPDFにして署名するのが楽で良いです(^-^)

開発:WordPressにも拡張HTMLとして貼り付けられると良いのですが… ああそうか、HTMLのソースを表示して全選択してカスタムHTMLに貼り付ければよいのですね。ただ… CSSにスコープが無いから本文のスタイルまで変わっちゃうと言うw

社長:iframe に閉じ込められれば良いと思うのですが。インラインのテキストをiframeのコンテンツにする方法ってあるんですかね。textarea 的に。

開発:迫害されて消されてしまったframeでは出来たと思うんですが…

社長:data URI にするほど小さくも無いですしねw

開発:要は「id を付けた要素をここに展開する」という機能があれば良いのですが。iframe でなくてもなんでも、というか iframe 以外のほうが便利ですが。

開発:ともかくこの、CSSのスコープというかセレクタというのがどうもわからない。まとめて各方法というか {} が入れ子にできないのかわからなので、ベタに書いてみました。これで何とか。

社長:うんうん。よろしいんじゃないでしょうか。ではこれを印刷して終了したいと思います。

-- 2020-0816 SatoxITS

// /* GShell-0.1.1 by SatoxITS
GShell version 0.1.1 // 2020-08-16 // SatoxITS

GShell // a General purpose Shell built on the top of Golang

*/ /*
Overview
To be written
*/ /*
Index

Implementation
	Structures
		import
		struct
	Main functions
		str-expansion	// macro processor
		finder		// builtin find + du
		grep		// builtin grep + wc + cksum + ...
		plugin		// plugin commands
		system		// external commands
		builtin		// builtin commands
		network		// socket handler
		redirect	// StdIn/Out redireciton
		history		// command history
		rusage		// resouce usage
		getline		// line editor
		interpreter	// command interpreter
		main
*/ //
Source Code //
// gsh - Go lang based Shell
// (c) 2020 ITS more Co., Ltd.
// 2020-0807 created by SatoxITS (sato@its-more.jp)

package main // gsh main
// Documents: Packages
// Impoted packages
import (
	"fmt"		// fmt
	"strings"	// strings
	"strconv"	// strconv
	"sort"		// sort
	"time"		// time
	"bufio"		// bufio
	"io/ioutil"	// ioutil
	"os"		// os
	"syscall"	// syscall
	"plugin"	// plugin
	"net"		// net
	"net/http"	// http
	//"html"	// html
	"path/filepath"	// filepath
	"go/types"	// types
	"go/token"	// token
)

var VERSION = "gsh/0.1.1 (2020-0816a)"
var LINESIZE = (8*1024)
var PATHSEP = ":" // should be ";" in Windows
var DIRSEP = "/" // canbe \ in Windows
var PROMPT = "> "
var GSH_HOME = ".gsh" // under home directory

// -xX logging control
// --A-- all
// --I-- info.
// --D-- debug
// --W-- warning
// --E-- error
// --F-- fatal error

// Structures
type GCommandHistory struct {
	StartAt		time.Time // command line execution started at
	EndAt		time.Time // command line execution ended at
	ResCode		int       // exit code of (external command)
	CmdError	error     // error string
	OutData		*os.File  // output of the command
	FoundFile	[]string  // output - result of ufind
	Rusagev		[2]syscall.Rusage // Resource consumption, CPU time or so
	CmdId		int       // maybe with identified with arguments or impact
			          // redireciton commands should not be the CmdId
	WorkDir		string    // working directory at start
	CmdLine		string    // command line
}
type GChdirHistory struct {
	Dir		string
	MovedAt		time.Time
}
type CmdMode struct {
	BackGround	bool
}
type PluginInfo struct {
	Spec		*plugin.Plugin
	Addr		plugin.Symbol
	Name		string // maybe relative
	Path		string // this is in Plugin but hidden
}
type GshContext struct {
	StartDir	string	// the current directory at the start
	GetLine		string	// gsh-getline command as a input line editor
	ChdirHistory	[]GChdirHistory // the 1st entry is wd at the start
	gshPA		syscall.ProcAttr
	CommandHistory	[]GCommandHistory
	CmdCurrent	GCommandHistory
	BackGround	bool
	BackGroundJobs	[]int
	LastRusage	syscall.Rusage
	GshHomeDir	string
	TerminalId	int
	CmdTrace	bool
	PluginFuncs	[]PluginInfo
}

func strBegins(str, pat string)(bool){
	if 0 < len(str){
		yes := str[0:len(pat)] == pat
		//fmt.Printf("--D-- strBegins(%v,%v)=%v\n",str,pat,yes)
		return yes
	}
	//fmt.Printf("--D-- strBegins(%v,%v)=%v\n",str,pat,false)
	return false
}
func isin(what string, list []string) bool {
	for _, v := range list  {
		if v == what {
			return true
		}
	}
	return false
}
func isinX(what string,list[]string)(int){
	for i,v := range list {
		if v == what {
			return i
		}
	}
	return -1
}

func env(opts []string) {
	env := os.Environ()
	if isin("-s", opts){
		sort.Slice(env, func(i,j int) bool {
			return env[i] < env[j]
		})
	}
	for _, v := range env {
		fmt.Printf("%v\n",v)
	}
}

// - rewriting should be context dependent
// - should postpone until the real point of evaluation
// - should rewrite only known notation of symobl
func scanInt(str string)(val int,leng int){
	leng = -1
	for i,ch := range str {
		if '0' <= ch && ch <= '9' {
			leng = i+1
		}else{
			break
		}
	}
	if 0 < leng {
		ival,_ := strconv.Atoi(str[0:leng])
		return ival,leng
	}else{
		return 0,0
	}
}
func substHistory(gshCtx *GshContext,str string,i int,rstr string)(leng int,rst string){
	if len(str[i+1:]) == 0 {
		return 0,rstr
	}
	hi := 0
	histlen := len(gshCtx.CommandHistory)
	if str[i+1] == '!' {
		hi = histlen - 1
		leng = 1
	}else{
		hi,leng = scanInt(str[i+1:])
		if leng == 0 {
			return 0,rstr
		}
		if hi < 0 {
			hi = histlen + hi
		}
	}
	if 0 <= hi && hi < histlen {
		//fmt.Printf("--D-- %v(%c)\n",str[i+leng:],str[i+leng])
		if 1 < len(str[i+leng:]) && str[i+leng:][1] == 'f' {
			leng += 1
			xlist := []string{}
			list := gshCtx.CommandHistory[hi].FoundFile
			for _,v := range list {
				//list[i] = escapeWhiteSP(v)
				xlist = append(xlist,escapeWhiteSP(v))
			}
			//rstr += strings.Join(list," ")
			rstr += strings.Join(xlist," ")
		}else{
			rstr += gshCtx.CommandHistory[hi].CmdLine
		}
	}else{
		leng = 0
	}
	return leng,rstr
}
func escapeWhiteSP(str string)(string){
	if len(str) == 0 {
		return "\\z" // empty, to be ignored
	}
	rstr := ""
	for _,ch := range str {
		switch ch {
			case '\\': rstr += "\\\\"
			case ' ': rstr += "\\s"
			case '\t': rstr += "\\t"
			case '\r': rstr += "\\r"
			case '\n': rstr += "\\n"
			default: rstr += string(ch)
		}
	}
	return rstr
}
func unescapeWhiteSP(str string)(string){ // strip original escapes
	rstr := ""
	for i := 0; i < len(str); i++ {
		ch := str[i]
		if ch == '\\' {
			if i+1 < len(str) {
				switch str[i+1] {
					case 'z':
						continue;
				}
			}
		}
		rstr += string(ch)
	}
	return rstr
}
func unescapeWhiteSPV(strv []string)([]string){ // strip original escapes
	ustrv := []string{}
	for _,v := range strv {
		ustrv = append(ustrv,unescapeWhiteSP(v))
	}
	return ustrv
}

// str-expansion
// - this should be a macro processor
func strsubst(gshCtx *GshContext,str string,histonly bool) string {
	rstr := ""
	inEsc := 0 // escape characer mode 
	for i := 0; i < len(str); i++ {
		//fmt.Printf("--D--Subst %v:%v\n",i,str[i:])
		ch := str[i]
		if inEsc == 0 {
			if ch == '!' {
				leng,xrstr := substHistory(gshCtx,str,i,rstr)
				if 0 < leng {
					i += leng
					rstr = xrstr
					continue
				}
			}
			switch ch {
				case '\\': inEsc = '\\'; continue
				case '%':  inEsc = '%';  continue
				case '$':
			}
		}
		switch inEsc {
		case '\\':
			switch ch {
				case '\\': ch = '\\'
				case 's': ch = ' '
				case 't': ch = '\t'
				case 'r': ch = '\r'
				case 'n': ch = '\n'
				case 'z': inEsc = 0; continue // empty, to be ignored
			}
			inEsc = 0 
		case '%':
			switch {
				case ch == '%': ch = '%'
				case ch == 'T':
					rstr = rstr + time.Now().Format(time.Stamp)
					continue;
				default:
					// postpone the interpretation
					rstr = rstr + "%" + string(ch)
					continue;
			}
			inEsc = 0
		}
		rstr = rstr + string(ch)
	}
	return rstr
}
func showFileInfo(path string, opts []string) {
	if isin("-l",opts) || isin("-ls",opts) {
		fi, _ := os.Stat(path)
		mod := fi.ModTime()
		date := mod.Format(time.Stamp)
		fmt.Printf("%v %8v %s ",fi.Mode(),fi.Size(),date)
	}
	fmt.Printf("%s",path)
	if isin("-sp",opts) {
		fmt.Printf(" ")
	}else
	if ! isin("-n",opts) {
		fmt.Printf("\n")
	}
}
func userHomeDir()(string,bool){
	/*
	homedir,_ = os.UserHomeDir() // not implemented in older Golang
	*/
	homedir,found := os.LookupEnv("HOME")
	//fmt.Printf("--I-- HOME=%v(%v)\n",homedir,found)
	if !found {
		return "/tmp",found
	}
	return homedir,found
}

func toFullpath(path string) (fullpath string) {
	if path[0] == '/' {
		return path
	}
	pathv := strings.Split(path,DIRSEP)
	switch {
	case pathv[0] == ".":
		pathv[0], _ = os.Getwd()
	case pathv[0] == "..": // all ones should be interpreted
		cwd, _ := os.Getwd()
		ppathv := strings.Split(cwd,DIRSEP)
		pathv[0] = strings.Join(ppathv,DIRSEP)
	case pathv[0] == "~":
		pathv[0],_ = userHomeDir()
	default:
		cwd, _ := os.Getwd()
		pathv[0] = cwd + DIRSEP + pathv[0]
	}
	return strings.Join(pathv,DIRSEP)
}

func IsRegFile(path string)(bool){
	fi, err := os.Stat(path)
	if err == nil {
		fm := fi.Mode()
		return fm.IsRegular();
	}
	return false
}

// grep
// "lines", "lin" or "lnp" for "(text) line processor" or "scanner"
// a*,!ab,c, ... sequentioal combination of patterns
// what "LINE" is should be definable
// generic line-by-line processing
// grep [-v]
// cat -n -v
// uniq [-c]
// tail -f
// sed s/x/y/ or awk
// grep with line count like wc
// rewrite contents if specified
func xGrep(gshCtx GshContext,path string,rexpv[]string)(int){
	file, err := os.OpenFile(path,os.O_RDONLY,0)
	if err != nil {
		fmt.Printf("--E-- grep %v (%v)\n",path,err)
		return -1
	}
	defer file.Close()
	if gshCtx.CmdTrace { fmt.Printf("--I-- grep %v %v\n",path,rexpv) }
	//reader := bufio.NewReaderSize(file,LINESIZE)
	reader := bufio.NewReaderSize(file,80)
	li := 0
	found := 0
	for li = 0; ; li++ {
		line, err := reader.ReadString('\n')
		if len(line) <= 0 {
			break
		}
		if 150 < len(line) {
			// maybe binary
			break;
		}
		if err != nil {
			break
		}
		if 0 <= strings.Index(string(line),rexpv[0]) {
			found += 1
			fmt.Printf("%s:%d: %s",path,li,line)
		}
	}
		//fmt.Printf("total %d lines %s\n",li,path)
	//if( 0 < found ){ fmt.Printf("((found %d lines %s))\n",found,path); }
	return found
}

// Finder
// finding files with it name and contents
// file names are ORed
// show the content with %x fmt list
// ls -R
// tar command by adding output
type fileSum struct {
	Err	int64	// access error or so
	Size	int64	// content size
	DupSize	int64	// content size from hard links
	Blocks	int64	// number of blocks (of 512 bytes)
	DupBlocks int64	// Blocks pointed from hard links
	HLinks	int64	// hard links
	Words	int64
	Lines	int64
	Files	int64
	Dirs	int64	// the num. of directories
	SymLink	int64
	Flats	int64	// the num. of flat files
	MaxDepth	int64
	MaxNamlen	int64	// max. name length
	nextRepo	time.Time
}
func showFusage(dir string,fusage *fileSum){
	bsume := float64(((fusage.Blocks-fusage.DupBlocks)/2)*1024)/1000000.0
	//bsumdup := float64((fusage.Blocks/2)*1024)/1000000.0

	fmt.Printf("%v: %v files (%vd %vs %vh) %.6f MB (%.2f MBK)\n",
		dir,
		fusage.Files,
		fusage.Dirs,
		fusage.SymLink,
		fusage.HLinks,
		float64(fusage.Size)/1000000.0,bsume);
}
const (
	S_IFMT    = 0170000
	S_IFCHR   = 0020000
	S_IFDIR   = 0040000
	S_IFREG   = 0100000
	S_IFLNK   = 0120000
	S_IFSOCK  = 0140000
)
func cumFinfo(fsum *fileSum, path string, staterr error, fstat syscall.Stat_t, argv[]string,verb bool)(*fileSum){
	now := time.Now()
	if time.Second <= now.Sub(fsum.nextRepo) {
		if !fsum.nextRepo.IsZero(){
			tstmp := now.Format(time.Stamp)
			showFusage(tstmp,fsum)
		}
		fsum.nextRepo = now.Add(time.Second)
	}
	if staterr != nil {
		fsum.Err += 1
		return fsum
	}
	fsum.Files += 1
	if 1 < fstat.Nlink {
		// must count only once...
		// at least ignore ones in the same directory
		//if finfo.Mode().IsRegular() {
		if (fstat.Mode & S_IFMT) == S_IFREG {
			fsum.HLinks += 1
			fsum.DupBlocks += int64(fstat.Blocks)
			//fmt.Printf("---Dup HardLink %v %s\n",fstat.Nlink,path)
		}
	}
	//fsum.Size += finfo.Size()
	fsum.Size += fstat.Size
	fsum.Blocks += int64(fstat.Blocks) 
	//if verb { fmt.Printf("(%8dBlk) %s",fstat.Blocks/2,path) }
	if isin("-ls",argv){
		//if verb { fmt.Printf("%4d %8d ",fstat.Blksize,fstat.Blocks) }
//		fmt.Printf("%d\t",fstat.Blocks/2)
	}
	//if finfo.IsDir()
	if (fstat.Mode & S_IFMT) == S_IFDIR {
		fsum.Dirs += 1
	}
	//if (finfo.Mode() & os.ModeSymlink) != 0 
	if (fstat.Mode & S_IFMT) == S_IFLNK {
		//if verb { fmt.Printf("symlink(%v,%s)\n",fstat.Mode,finfo.Name()) }
		//{ fmt.Printf("symlink(%o,%s)\n",fstat.Mode,finfo.Name()) }
		fsum.SymLink += 1
	}
	return fsum
}
func xxFindEntv(gshCtx GshContext,depth int,total *fileSum,dir string, dstat syscall.Stat_t, ei int, entv []string,npatv[]string,argv[]string)(GshContext,*fileSum){
	nols := isin("-grep",argv)
	// sort entv
	/*
	if isin("-t",argv){
		sort.Slice(filev, func(i,j int) bool {
			return 0 < filev[i].ModTime().Sub(filev[j].ModTime())
		})
	}
	*/
		/*
		if isin("-u",argv){
			sort.Slice(filev, func(i,j int) bool {
				return 0 < filev[i].AccTime().Sub(filev[j].AccTime())
			})
		}
		if isin("-U",argv){
			sort.Slice(filev, func(i,j int) bool {
				return 0 < filev[i].CreatTime().Sub(filev[j].CreatTime())
			})
		}
		*/
	/*
	if isin("-S",argv){
		sort.Slice(filev, func(i,j int) bool {
			return filev[j].Size() < filev[i].Size()
		})
	}
	*/
	for _,filename := range entv {
		for _,npat := range npatv {
			match := true
			if npat == "*" {
				match = true
			}else{
				match, _ = filepath.Match(npat,filename)
			}
			path := dir + DIRSEP + filename
			if !match {
				continue
			}
			var fstat syscall.Stat_t
			staterr := syscall.Lstat(path,&fstat)
			if staterr != nil {
				if !isin("-w",argv){fmt.Printf("ufind: %v\n",staterr) }
				continue;
			}
			if isin("-du",argv) && (fstat.Mode & S_IFMT) == S_IFDIR {
				// should not show size of directory in "-du" mode ...
			}else
			if !nols && !isin("-s",argv) && (!isin("-du",argv) || isin("-a",argv)) {
				if isin("-du",argv) {
					fmt.Printf("%d\t",fstat.Blocks/2)
				}
				showFileInfo(path,argv)
			}
			if true { // && isin("-du",argv)
				total = cumFinfo(total,path,staterr,fstat,argv,false)
			}
			/*
			if isin("-wc",argv) {
			}
			*/
			x := isinX("-grep",argv); // -grep will be convenient like -ls
			if 0 <= x && x+1 <= len(argv) { // -grep will be convenient like -ls
				if IsRegFile(path){
					found := xGrep(gshCtx,path,argv[x+1:])
					if 0 < found {
						foundv := gshCtx.CmdCurrent.FoundFile
						if len(foundv) < 10 {
							gshCtx.CmdCurrent.FoundFile =
							append(gshCtx.CmdCurrent.FoundFile,path)
						}
					}
				}
			}
			if !isin("-r0",argv) { // -d 0 in du, -depth n in find
				//total.Depth += 1
				if (fstat.Mode & S_IFMT) == S_IFLNK {
					continue
				}
				if dstat.Rdev != fstat.Rdev {
					fmt.Printf("--I-- don't follow differnet device %v(%v) %v(%v)\n",
						dir,dstat.Rdev,path,fstat.Rdev)
				}
				if (fstat.Mode & S_IFMT) == S_IFDIR {
					gshCtx,total = xxFind(gshCtx,depth+1,total,path,npatv,argv)
				}
			}
		}
	}
	return gshCtx,total
}
func xxFind(gshCtx GshContext,depth int,total *fileSum,dir string,npatv[]string,argv[]string)(GshContext,*fileSum){
	nols := isin("-grep",argv)
	dirfile,oerr := os.OpenFile(dir,os.O_RDONLY,0)
	if oerr == nil {
		//fmt.Printf("--I-- %v(%v)[%d]\n",dir,dirfile,dirfile.Fd())
		defer dirfile.Close()
	}else{
	}

	prev := *total
	var dstat syscall.Stat_t
	staterr := syscall.Lstat(dir,&dstat) // should be flstat

	if staterr != nil {
		if !isin("-w",argv){ fmt.Printf("ufind: %v\n",staterr) }
		return gshCtx,total
	}
		//filev,err := ioutil.ReadDir(dir)
		//_,err := ioutil.ReadDir(dir) // ReadDir() heavy and bad for huge directory
		/*
		if err != nil {
			if !isin("-w",argv){ fmt.Printf("ufind: %v\n",err) }
			return total
		}
		*/
	if depth == 0 {
		total = cumFinfo(total,dir,staterr,dstat,argv,true)
		if !nols && !isin("-s",argv) && (!isin("-du",argv) || isin("-a",argv)) {
			showFileInfo(dir,argv)
		}
	}
	// it it is not a directory, just scan it and finish

	for ei := 0; ; ei++ {
		entv,rderr := dirfile.Readdirnames(8*1024)
		if len(entv) == 0 || rderr != nil {
			//if rderr != nil { fmt.Printf("[%d] len=%d (%v)\n",ei,len(entv),rderr) }
			break
		}
		if 0 < ei {
			fmt.Printf("--I-- xxFind[%d] %d large-dir: %s\n",ei,len(entv),dir)
		}
		gshCtx,total = xxFindEntv(gshCtx,depth,total,dir,dstat,ei,entv,npatv,argv)
	}
	if isin("-du",argv) {
		// if in "du" mode
		fmt.Printf("%d\t%s\n",(total.Blocks-prev.Blocks)/2,dir)
	}
	return gshCtx,total
}

// {ufind|fu|ls} [Files] [// Names] [-- Expressions]
//  Files is "." by default
//  Names is "*" by default
//  Expressions is "-print" by default for "ufind", or -du for "fu" command
func xFind(gshCtx GshContext,argv[]string)(GshContext){
	if 0 < len(argv) && strBegins(argv[0],"?"){
		showFound(gshCtx,argv)
		return gshCtx
	}
	var total = fileSum{}
	npats := []string{}
	for _,v := range argv {
		if 0 < len(v) && v[0] != '-' {
			npats = append(npats,v)
		}
		if v == "//" { break }
		if v == "--" { break }
		if v == "-grep" { break }
		if v == "-ls" { break }
	}
	if len(npats) == 0 {
		npats = []string{"*"}
	}
	cwd := "."
	// if to be fullpath ::: cwd, _ := os.Getwd()
	if len(npats) == 0 { npats = []string{"*"} }
	gshCtx,fusage := xxFind(gshCtx,0,&total,cwd,npats,argv)
	if !isin("-grep",argv) {
		showFusage("total",fusage)
	}
	return gshCtx
}

func showFiles(files[]string){
	sp := ""
	for i,file := range files {
		if 0 < i { sp = " " } else { sp = "" }
		fmt.Printf(sp+"%s",escapeWhiteSP(file))
	}
}
func showFound(gshCtx GshContext, argv[]string){
	for i,v := range gshCtx.CommandHistory {
		if 0 < len(v.FoundFile) {
			fmt.Printf("!%d (%d) ",i,len(v.FoundFile))
			if isin("-ls",argv){
				fmt.Printf("\n")
				for _,file := range v.FoundFile {
					fmt.Printf("") //sub number?
					showFileInfo(file,argv)
				}
			}else{
				showFiles(v.FoundFile)
				fmt.Printf("\n")
			}
		}
	}
}

func showMatchFile(filev []os.FileInfo, npat,dir string, argv[]string)(string,bool){
	fname := ""
	found := false
	for _,v := range filev {
		match, _ := filepath.Match(npat,(v.Name()))
		if match {
			fname = v.Name()
			found = true
			//fmt.Printf("[%d] %s\n",i,v.Name())
			showIfExecutable(fname,dir,argv)
		}
	}
	return fname,found
}
func showIfExecutable(name,dir string,argv[]string)(ffullpath string,ffound bool){
	var fullpath string
	if strBegins(name,DIRSEP){
		fullpath = name
	}else{
		fullpath = dir + DIRSEP + name
	}
	fi, err := os.Stat(fullpath)
	if err != nil {
		fullpath = dir + DIRSEP + name + ".go"
		fi, err = os.Stat(fullpath)
	}
	if err == nil {
		fm := fi.Mode()
		if fm.IsRegular() {
		  // R_OK=4, W_OK=2, X_OK=1, F_OK=0
		  if syscall.Access(fullpath,5) == nil {
			ffullpath = fullpath
			ffound = true
			if ! isin("-s", argv) {
				showFileInfo(fullpath,argv)
			}
		  }
		}
	}
	return ffullpath, ffound
}
func which(list string, argv []string) (fullpathv []string, itis bool){
	if len(argv) <= 1 {
		fmt.Printf("Usage: which comand [-s] [-a] [-ls]\n")
		return []string{""}, false
	}
	path := argv[1]
	if strBegins(path,"/") {
		// should check if excecutable?
		_,exOK := showIfExecutable(path,"/",argv)
		fmt.Printf("--D-- %v exOK=%v\n",path,exOK)
		return []string{path},exOK
	}
	pathenv, efound := os.LookupEnv(list)
	if ! efound {
		fmt.Printf("--E-- which: no \"%s\" environment\n",list)
		return []string{""}, false
	}
	showall := isin("-a",argv) || 0 <= strings.Index(path,"*")
	dirv := strings.Split(pathenv,PATHSEP)
	ffound := false
	ffullpath := path
	for _, dir := range dirv {
		if 0 <= strings.Index(path,"*") { // by wild-card
			list,_ := ioutil.ReadDir(dir)
			ffullpath, ffound = showMatchFile(list,path,dir,argv)
		}else{
			ffullpath, ffound = showIfExecutable(path,dir,argv)
		}
		//if ffound && !isin("-a", argv) {
		if ffound && !showall {
			break;
		}
	}
	return []string{ffullpath}, ffound
}

func stripLeadingWSParg(argv[]string)([]string){
	for ; 0 < len(argv); {
		if len(argv[0]) == 0 {
			argv = argv[1:]
		}else{
			break
		}
	}
	return argv
}
func xEval(argv []string, nlend bool){
	argv = stripLeadingWSParg(argv)
	if len(argv) == 0 {
		fmt.Printf("eval [%%format] [Go-expression]\n")
		return
	}
	pfmt := "%v"
	if argv[0][0] == '%' {
		pfmt = argv[0]
		argv = argv[1:]
	}
	if len(argv) == 0 {
		return
	}
	gocode := strings.Join(argv," ");
	//fmt.Printf("eval [%v] [%v]\n",pfmt,gocode)
	fset := token.NewFileSet()
	rval, _ := types.Eval(fset,nil,token.NoPos,gocode)
	fmt.Printf(pfmt,rval.Value)
	if nlend { fmt.Printf("\n") }
}

func getval(name string) (found bool, val int) {
	/* should expand the name here */
	if name == "gsh.pid" {
		return true, os.Getpid()
	}else
	if name == "gsh.ppid" {
		return true, os.Getppid()
	}
	return false, 0
}

func echo(argv []string, nlend bool){
	for ai := 1; ai < len(argv); ai++ {
		if 1 < ai {
			fmt.Printf(" ");
		}
		arg := argv[ai]
		found, val := getval(arg)
		if found {
			fmt.Printf("%d",val)
		}else{
			fmt.Printf("%s",arg)
		}
	}
	if nlend {
		fmt.Printf("\n");
	}
}

func resfile() string {
	return "gsh.tmp"
}
//var resF *File
func resmap() {
	//_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, os.ModeAppend)
	// https://developpaper.com/solution-to-golang-bad-file-descriptor-problem/
	_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		fmt.Printf("refF could not open: %s\n",err)
	}else{
		fmt.Printf("refF opened\n")
	}
}

// External commands
func excommand(gshCtx GshContext, exec bool, argv []string) (GshContext, bool) {
	if gshCtx.CmdTrace { fmt.Printf("--I-- excommand[%v](%v)\n",exec,argv) }

	gshPA := gshCtx.gshPA
	fullpathv, itis := which("PATH",[]string{"which",argv[0],"-s"})
	if itis == false {
		return gshCtx, true
	}
	fullpath := fullpathv[0]
	argv = unescapeWhiteSPV(argv)
	if 0 < strings.Index(fullpath,".go") {
		nargv := argv // []string{}
		gofullpathv, itis := which("PATH",[]string{"which","go","-s"})
		if itis == false {
			fmt.Printf("--F-- Go not found\n")
			return gshCtx, true
		}
		gofullpath := gofullpathv[0]
		nargv = []string{ gofullpath, "run", fullpath }
		fmt.Printf("--I-- %s {%s %s %s}\n",gofullpath,
			nargv[0],nargv[1],nargv[2])
		if exec {
			syscall.Exec(gofullpath,nargv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(gofullpath,nargv,&gshPA)
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage)
				gshCtx.LastRusage = rusage
				gshCtx.CmdCurrent.Rusagev[1] = rusage
			}
		}
	}else{
		if exec {
			syscall.Exec(fullpath,argv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(fullpath,argv,&gshPA)
			//fmt.Printf("[%d]\n",pid); // '&' to be background
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage);
				gshCtx.LastRusage = rusage
				gshCtx.CmdCurrent.Rusagev[1] = rusage
			}
		}
	}
	return gshCtx, false
}

// Builtin Commands
func sleep(gshCtx GshContext, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Sleep 100ms, 100us, 100ns, ...\n")
		return
	}
	duration := argv[1];
	d, err := time.ParseDuration(duration)
	if err != nil {
		d, err = time.ParseDuration(duration+"s")
		if err != nil {
			fmt.Printf("duration ? %s (%s)\n",duration,err)
			return
		}
	}
	//fmt.Printf("Sleep %v\n",duration)
	time.Sleep(d)
	if 0 < len(argv[2:]) {
		gshellv(gshCtx, argv[2:])
	}
}
func repeat(gshCtx GshContext, argv []string) {
	if len(argv) < 2 {
		return
	}
	start0 := time.Now()
	for ri,_ := strconv.Atoi(argv[1]); 0 < ri; ri-- {
		if 0 < len(argv[2:]) {
			//start := time.Now()
			gshellv(gshCtx, argv[2:])
			end := time.Now()
			elps := end.Sub(start0);
			if( 1000000000 < elps ){
				fmt.Printf("(repeat#%d %v)\n",ri,elps);
			}
		}
	}
}

func gen(gshCtx GshContext, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: %s N\n",argv[0])
		return
	}
	// should br repeated by "repeat" command
	count, _ := strconv.Atoi(argv[1])
	fd := gshPA.Files[1] // Stdout
	file := os.NewFile(fd,"internalStdOut")
	fmt.Printf("--I-- Gen. Count=%d to [%d]\n",count,file.Fd())
	//buf := []byte{}
	outdata := "0123 5678 0123 5678 0123 5678 0123 5678\r"
	for gi := 0; gi < count; gi++ {
		file.WriteString(outdata)
	}
	//file.WriteString("\n")
	fmt.Printf("\n(%d B)\n",count*len(outdata));
	//file.Close()
}

// network
// -s, -si, -so // bi-directional, source, sync (maybe socket)
func sconnect(gshCtx GshContext, inTCP bool, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: -s [host]:[port[.udp]]\n")
		return
	}
	remote := argv[1]
	if remote == ":" { remote = "0.0.0.0:9999" }

	if inTCP { // TCP
		dport, err := net.ResolveTCPAddr("tcp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		conn, err := net.DialTCP("tcp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()
		fmt.Printf("Socket: connected to %s, socket[%d]\n",remote,fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}else{
		//dport, err := net.ResolveUDPAddr("udp4",remote);
		dport, err := net.ResolveUDPAddr("udp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		//conn, err := net.DialUDP("udp4",nil,dport)
		conn, err := net.DialUDP("udp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()

		ar := conn.RemoteAddr()
		//al := conn.LocalAddr()
		fmt.Printf("Socket: connected to %s [%s], socket[%d]\n",
			remote,ar.String(),fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}
}
func saccept(gshCtx GshContext, inTCP bool, argv []string) {
	gshPA := gshCtx.gshPA
	if len(argv) < 2 {
		fmt.Printf("Usage: -ac [host]:[port[.udp]]\n")
		return
	}
	local := argv[1]
	if local == ":" { local = "0.0.0.0:9999" }
	if inTCP { // TCP
		port, err := net.ResolveTCPAddr("tcp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Listen at %s...\n",local);
		sconn, err := net.ListenTCP("tcp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Accepting at %s...\n",local);
		aconn, err := sconn.AcceptTCP()
		if err != nil {
			fmt.Printf("Accept error: %s (%s)\n",local,err)
			return
		}
		file, _ := aconn.File()
		fd := file.Fd()
		fmt.Printf("Accepted TCP at %s [%d]\n",local,fd)

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		gshellv(gshCtx, argv[2:])
		gshPA.Files[0] = savfd

		sconn.Close();
		aconn.Close();
		file.Close();
	}else{
		//port, err := net.ResolveUDPAddr("udp4",local);
		port, err := net.ResolveUDPAddr("udp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		fmt.Printf("Listen UDP at %s...\n",local);
		//uconn, err := net.ListenUDP("udp4", port)
		uconn, err := net.ListenUDP("udp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		file, _ := uconn.File()
		fd := file.Fd()
		ar := uconn.RemoteAddr()
		remote := ""
		if ar != nil { remote = ar.String() }
		if remote == "" { remote = "?" }

		// not yet received
		//fmt.Printf("Accepted at %s [%d] <- %s\n",local,fd,"")

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		savenv := gshPA.Env
		gshPA.Env = append(savenv, "REMOTE_HOST="+remote)
		gshellv(gshCtx, argv[2:])
		gshPA.Env = savenv
		gshPA.Files[0] = savfd

		uconn.Close();
		file.Close();
	}
}

// empty line command
func xPwd(gshCtx GshContext, argv[]string){
	// execute context command, pwd + date
	// context notation, representation scheme, to be resumed at re-login
	cwd, _ := os.Getwd()
	switch {
	case isin("-a",argv):
		xChdirHistory(gshCtx,argv)
	case isin("-ls",argv):
		showFileInfo(cwd,argv)
	default:
		fmt.Printf("%s\n",cwd)
	case isin("-v",argv): // obsolete emtpy command
		t := time.Now()
		date := t.Format(time.UnixDate)
		exe, _ := os.Executable()
		host, _ := os.Hostname()
		fmt.Printf("{PWD=\"%s\"",cwd)
		fmt.Printf(" HOST=\"%s\"",host)
		fmt.Printf(" DATE=\"%s\"",date)
		fmt.Printf(" TIME=\"%s\"",t.String())
		fmt.Printf(" PID=\"%d\"",os.Getpid())
		fmt.Printf(" EXE=\"%s\"",exe)
		fmt.Printf("}\n")
	}
}

// History
// these should be browsed and edited by HTTP browser
// show the time of command with -t and direcotry with -ls
// openfile-history, sort by -a -m -c
// sort by elapsed time by -t -s
// search by "more" like interface
// edit history
// sort history, and wc or uniq
// CPU and other resource consumptions
// limit showing range (by time or so)
// export / import history
func xHistory(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	for i, v := range gshCtx.CommandHistory {
		// exclude commands not to be listed by default
		// internal commands may be suppressed by default
		if v.CmdLine == "" && !isin("-a",argv) {
			continue;
		}
		if !isin("-n",argv){ // like "fc"
			fmt.Printf("!%-3d ",i)
		}
		if isin("-v",argv){
			fmt.Println(v) // should be with it date
		}else{
			if isin("-l",argv) || isin("-l0",argv) {
				elps := v.EndAt.Sub(v.StartAt);
				start := v.StartAt.Format(time.Stamp)
				fmt.Printf("%s %11v/t ",start,elps)
			}
			if isin("-l",argv) && !isin("-l0",argv){
				fmt.Printf("%v",Rusagef("%t %u %s",argv,v.Rusagev))
			}
			if isin("-ls",argv){
				fmt.Printf("@%s ",v.WorkDir)
				// show the FileInfo of the output command??
			}
			fmt.Printf("%s",v.CmdLine)
			fmt.Printf("\n")
		}
	}
	return gshCtx
}
// !n - history index
func searchHistory(gshCtx GshContext, gline string) (string, bool, bool){
	if gline[0] == '!' {
		hix, err := strconv.Atoi(gline[1:])
		if err != nil {
			fmt.Printf("--E-- (%s : range)\n",hix)
			return "", false, true
		}
		if hix < 0 || len(gshCtx.CommandHistory) <= hix {
			fmt.Printf("--E-- (%d : out of range)\n",hix)
			return "", false, true
		}
		return gshCtx.CommandHistory[hix].CmdLine, false, false
	}
	// search
	//for i, v := range gshCtx.CommandHistory {
	//}
	return gline, false, false
}

// temporary adding to PATH environment
// cd name -lib for LD_LIBRARY_PATH
// chdir with directory history (date + full-path)
// -s for sort option (by visit date or so)
func xChdirHistory(gshCtx GshContext, argv []string){
	for i, v := range gshCtx.ChdirHistory {
		fmt.Printf("!%d ",i)
		fmt.Printf("%v ",v.MovedAt.Format(time.Stamp))
		showFileInfo(v.Dir,argv)
	}
}
func xChdir(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	cdhist := gshCtx.ChdirHistory
	if isin("?",argv ) || isin("-t",argv) {
		xChdirHistory(gshCtx,argv)
		return gshCtx
	}
	pwd, _ := os.Getwd()
	dir := ""
	if len(argv) <= 1 {
		dir = toFullpath("~")
	}else{
		dir = argv[1]
	}
	if strBegins(dir,"!") {
		if dir == "!0" {
			dir = gshCtx.StartDir
		}else
		if dir == "!!" {
			index := len(cdhist) - 1
			if 0 < index { index -= 1 }
			dir = cdhist[index].Dir
		}else{
			index, err := strconv.Atoi(dir[1:])
			if err != nil {
				fmt.Printf("--E-- xChdir(%v)\n",err)
				dir = "?"
			}else
			if len(gshCtx.ChdirHistory) <= index {
				fmt.Printf("--E-- xChdir(history range error)\n")
				dir = "?"
			}else{
				dir = cdhist[index].Dir
			}
		}
	}
	if dir != "?" {
		err := os.Chdir(dir)
		if err != nil {
			fmt.Printf("--E-- xChdir(%s)(%v)\n",argv[1],err)
		}else{
			cwd, _ := os.Getwd()
			if cwd != pwd {
				hist1 := GChdirHistory { }
				hist1.Dir = cwd
				hist1.MovedAt = time.Now()
				gshCtx.ChdirHistory = append(cdhist,hist1)
			}
		}
	}
	if isin("-ls",argv){
		cwd, _ := os.Getwd()
		showFileInfo(cwd,argv);
	}
	return gshCtx
}
func TimeValSub(tv1 *syscall.Timeval, tv2 *syscall.Timeval){
	*tv1 = syscall.NsecToTimeval(tv1.Nano() - tv2.Nano())  
}
func RusageSubv(ru1, ru2 [2]syscall.Rusage)([2]syscall.Rusage){
	TimeValSub(&ru1[0].Utime,&ru2[0].Utime)
	TimeValSub(&ru1[0].Stime,&ru2[0].Stime)
	TimeValSub(&ru1[1].Utime,&ru2[1].Utime)
	TimeValSub(&ru1[1].Stime,&ru2[1].Stime)
	return ru1
}
func TimeValAdd(tv1 syscall.Timeval, tv2 syscall.Timeval)(syscall.Timeval){
	tvs := syscall.NsecToTimeval(tv1.Nano() + tv2.Nano())  
	return tvs
}
/*
func RusageAddv(ru1, ru2 [2]syscall.Rusage)([2]syscall.Rusage){
	TimeValAdd(ru1[0].Utime,ru2[0].Utime)
	TimeValAdd(ru1[0].Stime,ru2[0].Stime)
	TimeValAdd(ru1[1].Utime,ru2[1].Utime)
	TimeValAdd(ru1[1].Stime,ru2[1].Stime)
	return ru1
}
*/

// Resource Usage
func Rusagef(fmtspec string, argv []string, ru [2]syscall.Rusage)(string){
	ut := TimeValAdd(ru[0].Utime,ru[1].Utime)
	st := TimeValAdd(ru[0].Stime,ru[1].Stime)
	fmt.Printf("%d.%06ds/u ",ut.Sec,ut.Usec) //ru[1].Utime.Sec,ru[1].Utime.Usec)
	fmt.Printf("%d.%06ds/s ",st.Sec,st.Usec) //ru[1].Stime.Sec,ru[1].Stime.Usec)
	return ""
}
func Getrusagev()([2]syscall.Rusage){
	var ruv = [2]syscall.Rusage{}
	syscall.Getrusage(syscall.RUSAGE_SELF,&ruv[0])
	syscall.Getrusage(syscall.RUSAGE_CHILDREN,&ruv[1])
	return ruv
}
func showRusage(what string,argv []string, ru *syscall.Rusage){
	fmt.Printf("%s: ",what);
	fmt.Printf("Usr=%d.%06ds",ru.Utime.Sec,ru.Utime.Usec)
	fmt.Printf(" Sys=%d.%06ds",ru.Stime.Sec,ru.Stime.Usec)
	fmt.Printf(" Rss=%vB",ru.Maxrss)
	if isin("-l",argv) {
		fmt.Printf(" MinFlt=%v",ru.Minflt)
		fmt.Printf(" MajFlt=%v",ru.Majflt)
		fmt.Printf(" IxRSS=%vB",ru.Ixrss)
		fmt.Printf(" IdRSS=%vB",ru.Idrss)
		fmt.Printf(" Nswap=%vB",ru.Nswap)
	fmt.Printf(" Read=%v",ru.Inblock)
	fmt.Printf(" Write=%v",ru.Oublock)
	}
	fmt.Printf(" Snd=%v",ru.Msgsnd)
	fmt.Printf(" Rcv=%v",ru.Msgrcv)
	//if isin("-l",argv) {
		fmt.Printf(" Sig=%v",ru.Nsignals)
	//}
	fmt.Printf("\n");
}
func xTime(gshCtx GshContext, argv[]string)(GshContext,bool){
	if 2 <= len(argv){
		gshCtx.LastRusage = syscall.Rusage{}
		rusagev1 := Getrusagev()
		xgshCtx, fin := gshellv(gshCtx,argv[1:])
		rusagev2 := Getrusagev()
		gshCtx = xgshCtx
		showRusage(argv[1],argv,&gshCtx.LastRusage)
		rusagev := RusageSubv(rusagev2,rusagev1)
		showRusage("self",argv,&rusagev[0])
		showRusage("chld",argv,&rusagev[1])
		return gshCtx, fin
	}else{
		rusage:= syscall.Rusage {}
		syscall.Getrusage(syscall.RUSAGE_SELF,&rusage)
		showRusage("self",argv, &rusage)
		syscall.Getrusage(syscall.RUSAGE_CHILDREN,&rusage)
		showRusage("chld",argv, &rusage)
		return gshCtx, false
	}
}
func xJobs(gshCtx GshContext, argv[]string){
	fmt.Printf("%d Jobs\n",len(gshCtx.BackGroundJobs))
	for ji, pid := range gshCtx.BackGroundJobs {
		//wstat := syscall.WaitStatus {0}
		rusage := syscall.Rusage {}
		//wpid, err := syscall.Wait4(pid,&wstat,syscall.WNOHANG,&rusage);
		wpid, err := syscall.Wait4(pid,nil,syscall.WNOHANG,&rusage);
		if err != nil {
			fmt.Printf("--E-- %%%d [%d] (%v)\n",ji,pid,err)
		}else{
			fmt.Printf("%%%d[%d](%d)\n",ji,pid,wpid)
			showRusage("chld",argv,&rusage)
		}
	}
}
func inBackground(gshCtx GshContext, argv[]string)(GshContext,bool){
	if gshCtx.CmdTrace { fmt.Printf("--I-- inBackground(%v)\n",argv) }
	gshCtx.BackGround = true // set background option
	xfin := false
	gshCtx, xfin = gshellv(gshCtx,argv)
	gshCtx.BackGround = false
	return gshCtx,xfin
}
// -o file without command means just opening it and refer by #N
// should be listed by "files" comnmand
func xOpen(gshCtx GshContext, argv[]string)(GshContext){
	var pv = []int{-1,-1}
	err := syscall.Pipe(pv)
	fmt.Printf("--I-- pipe()=[#%d,#%d](%v)\n",pv[0],pv[1],err)
	return gshCtx
}
func fromPipe(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}
func xClose(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}

// redirect
func redirect(gshCtx GshContext, argv[]string)(GshContext,bool){
	if len(argv) < 2 {
		return gshCtx, false
	}

	cmd := argv[0]
	fname := argv[1]
	var file *os.File = nil

	fdix := 0
	mode := os.O_RDONLY

	switch {
	case cmd == "-i" || cmd == "<":
		fdix = 0
		mode = os.O_RDONLY
	case cmd == "-o" || cmd == ">":
		fdix = 1
		mode = os.O_RDWR | os.O_CREATE
	case cmd == "-a" || cmd == ">>":
		fdix = 1
		mode = os.O_RDWR | os.O_CREATE | os.O_APPEND
	}
	if fname[0] == '#' {
		fd, err := strconv.Atoi(fname[1:])
		if err != nil {
			fmt.Printf("--E-- (%v)\n",err)
			return gshCtx, false
		}
		file = os.NewFile(uintptr(fd),"MaybePipe")
	}else{
		xfile, err := os.OpenFile(argv[1], mode, 0600)
		if err != nil {
			fmt.Printf("--E-- (%s)\n",err)
			return gshCtx, false
		}
		file = xfile
	}
	gshPA := gshCtx.gshPA
	savfd := gshPA.Files[fdix]
	gshPA.Files[fdix] = file.Fd()
	fmt.Printf("--I-- Opened [%d] %s\n",file.Fd(),argv[1])
	gshCtx, _ = gshellv(gshCtx, argv[2:])
	gshPA.Files[fdix] = savfd

	return gshCtx, false
}

//fmt.Fprintf(res, "GShell Status: %q", html.EscapeString(req.URL.Path))
func httpHandler(res http.ResponseWriter, req *http.Request){
	path := req.URL.Path
	fmt.Printf("--I-- Got HTTP Request(%s)\n",path)
	{
		gshCtx, _ :=  setupGshContext()
		fmt.Printf("--I-- %s\n",path[1:])
		gshCtx, _ = tgshelll(gshCtx,path[1:])
	}
	fmt.Fprintf(res, "Hello(^-^)/\n%s\n",path)
}
func httpServer(gshCtx GshContext, argv []string){
	http.HandleFunc("/", httpHandler)
	accport := "localhost:9999"
	fmt.Printf("--I-- HTTP Server Start at [%s]\n",accport)
	http.ListenAndServe(accport,nil)
}
func xGo(gshCtx GshContext, argv[]string){
	go gshellv(gshCtx,argv[1:]);
}
func xPs(gshCtx GshContext, argv[]string)(GshContext){
	return gshCtx
}

// Plugin
// plugin [-ls [names]] to list plugins
// Reference: plugin source code
func whichPlugin(gshCtx GshContext,name string,argv[]string)(pi *PluginInfo){
	pi = nil	
	for _,p := range gshCtx.PluginFuncs {
		if p.Name == name && pi == nil {
			pi = &p
		}
		if !isin("-s",argv){
			//fmt.Printf("%v %v ",i,p)
			if isin("-ls",argv){
				showFileInfo(p.Path,argv)
			}else{
				fmt.Printf("%s\n",p.Name)
			}
		}
	}
	return pi
}
func xPlugin(gshCtx GshContext, argv[]string)(GshContext,error){
	if len(argv) == 0 || argv[0] == "-ls" {
		whichPlugin(gshCtx,"",argv)
		return gshCtx, nil
	}
	name := argv[0]
	Pin := whichPlugin(gshCtx,name,[]string{"-s"})
	if Pin != nil {
		os.Args = argv // should be recovered?
		Pin.Addr.(func())()
		return gshCtx,nil
	}
	sofile := toFullpath(argv[0] + ".so") // or find it by which($PATH)

	p, err := plugin.Open(sofile)
	if err != nil {
		fmt.Printf("--E-- plugin.Open(%s)(%v)\n",sofile,err)
		return gshCtx, err
	}
	fname := "Main"
	f, err := p.Lookup(fname)
	if( err != nil ){
		fmt.Printf("--E-- plugin.Lookup(%s)(%v)\n",fname,err)
		return gshCtx, err
	}
	pin := PluginInfo {p,f,name,sofile} 
	gshCtx.PluginFuncs = append(gshCtx.PluginFuncs,pin)
	fmt.Printf("--I-- added (%d)\n",len(gshCtx.PluginFuncs))

	//fmt.Printf("--I-- first call(%s:%s)%v\n",sofile,fname,argv)
	os.Args = argv
	f.(func())()
	return gshCtx, err
}

// Command Interpreter
func gshellv(gshCtx GshContext, argv []string) (_ GshContext, fin bool) {
	fin = false

	if gshCtx.CmdTrace { fmt.Fprintf(os.Stderr,"--I-- gshellv((%d))\n",len(argv)) }
	if len(argv) <= 0 {
		return gshCtx, false
	}
	xargv := []string{}
	for ai := 0; ai < len(argv); ai++ {
		xargv = append(xargv,strsubst(&gshCtx,argv[ai],false))
	}
	argv = xargv
	if false {
		for ai := 0; ai < len(argv); ai++ {
			fmt.Printf("[%d] %s [%d]%T\n",
				ai,argv[ai],len(argv[ai]),argv[ai])
		}
	}
	cmd := argv[0]
	if gshCtx.CmdTrace { fmt.Fprintf(os.Stderr,"--I-- gshellv(%d)%v\n",len(argv),argv) }
	switch { // https://tour.golang.org/flowcontrol/11
	case cmd == "":
		xPwd(gshCtx,[]string{}); // emtpy command
	case cmd == "-x":
		gshCtx.CmdTrace = ! gshCtx.CmdTrace
	case cmd == "-ot":
		sconnect(gshCtx, true, argv)
	case cmd == "-ou":
		sconnect(gshCtx, false, argv)
	case cmd == "-it":
		saccept(gshCtx, true , argv)
	case cmd == "-iu":
		saccept(gshCtx, false, argv)
	case cmd == "-i" || cmd == "<" || cmd == "-o" || cmd == ">" || cmd == "-a" || cmd == ">>" || cmd == "-s" || cmd == "><":
		redirect(gshCtx, argv)
	case cmd == "|":
		gshCtx = fromPipe(gshCtx, argv)
	case cmd == "bg" || cmd == "-bg":
		rgshCtx, rfin := inBackground(gshCtx,argv[1:])
		return rgshCtx, rfin
	case cmd == "call":
		gshCtx, _ = excommand(gshCtx, false,argv[1:])
	case cmd == "cd" || cmd == "chdir":
		gshCtx = xChdir(gshCtx,argv);
	case cmd == "close":
		gshCtx = xClose(gshCtx,argv)
	case cmd == "#define":
	case cmd == "echo":
		echo(argv,true)
	case cmd == "env":
		env(argv)
	case cmd == "eval":
		xEval(argv[1:],true)
	case cmd == "exec":
		gshCtx, _ = excommand(gshCtx, true,argv[1:])
		// should not return here
	case cmd == "exit" || cmd == "quit":
		// write Result code EXIT to 3>
		return gshCtx, true
	case cmd == "-find" || cmd == "fin" || cmd == "ufind" || cmd == "uf" || cmd == "fu":
		gshCtx = xFind(gshCtx,argv[1:])
	case cmd == "fork":
		// mainly for a server
	case cmd == "-gen":
		gen(gshCtx, argv)
	case cmd == "-go":
		xGo(gshCtx, argv)
	case cmd == "-grep":
		gshCtx = xFind(gshCtx,argv)
	case cmd == "history" || cmd == "hi": // hi should be alias
		gshCtx = xHistory(gshCtx, argv)
	case cmd == "jobs":
		xJobs(gshCtx,argv)
	case cmd == "-ls":
		gshCtx = xFind(gshCtx,argv)
	case cmd == "nop":
	case cmd == "pipe":
		gshCtx = xOpen(gshCtx,argv)
	case cmd == "plug" || cmd == "plugin" || cmd == "pin":
		gshCtx,_ = xPlugin(gshCtx,argv[1:])
	case cmd == "ps":
		xPs(gshCtx,argv)
	case cmd == "pstitle": // to be gsh.title
	case cmd == "repeat" || cmd == "rep": // repeat cond command
		repeat(gshCtx,argv)
	case cmd == "set":
		// set name ...
	case cmd == "serv":
		httpServer(gshCtx,argv)
	case cmd == "sleep":
		sleep(gshCtx,argv)
	case cmd == "time":
		gshCtx, fin = xTime(gshCtx,argv)
	case cmd == "pwd":
		xPwd(gshCtx,argv);
	case cmd == "ver" || cmd == "-ver":
		fmt.Printf("%s\n",VERSION);
	case cmd == "where":
		// data file or so?
	case cmd == "which":
		which("PATH",argv);
	default:
		if whichPlugin(gshCtx,cmd,[]string{"-s"}) != nil {
			gshCtx, _ = xPlugin(gshCtx,argv)
		}else{
			gshCtx, _ = excommand(gshCtx,false,argv)
		}
	}
	return gshCtx, fin
}

func gshelll(gshCtx GshContext, gline string) (gx GshContext, rfin bool) {
	argv := strings.Split(string(gline)," ")
	gshCtx, fin := gshellv(gshCtx,argv)
	return gshCtx, fin
}
func tgshelll(gshCtx GshContext, gline string) (gx GshContext, xfin bool) {
	start := time.Now()
	gshCtx, fin := gshelll(gshCtx,gline)
	end := time.Now()
	elps := end.Sub(start);
	fmt.Printf("--I-- " + time.Now().Format(time.Stamp) + "(%d.%09ds)\n",
		elps/1000000000,elps%1000000000)
	return gshCtx, fin
}
func Ttyid() (int) {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return 0;
	}
	//fmt.Printf("Stdin: %v Dev=%d\n",
	//	fi.Mode(),fi.Mode()&os.ModeDevice)
	if (fi.Mode() & os.ModeDevice) != 0 {
		stat := syscall.Stat_t{};
		err := syscall.Fstat(0,&stat)
		if err != nil {
			//fmt.Printf("--I-- Stdin: (%v)\n",err)
		}else{
			//fmt.Printf("--I-- Stdin: rdev=%d %d\n",
			//	stat.Rdev&0xFF,stat.Rdev);
			//fmt.Printf("--I-- Stdin: tty%d\n",stat.Rdev&0xFF);
			return int(stat.Rdev & 0xFF)
		}
	}
	return 0
}
func ttyfile(gshCtx GshContext) string {
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	ttyfile := gshCtx.GshHomeDir + "/" + "gsh-tty" +
		 fmt.Sprintf("%02d",gshCtx.TerminalId)
		 //strconv.Itoa(gshCtx.TerminalId)
	//fmt.Printf("--I-- ttyfile=%s\n",ttyfile)
	return ttyfile
}
func ttyline(gshCtx GshContext) (*os.File){
	file, err := os.OpenFile(ttyfile(gshCtx),
		os.O_RDWR|os.O_CREATE|os.O_TRUNC,0600)
	if err != nil {
		fmt.Printf("--F-- cannot open %s (%s)\n",ttyfile(gshCtx),err)
		return file;
	}
	return file
}
// Command Line Editor
func getline(gshCtx GshContext, hix int, skipping, with_exgetline bool, gsh_getlinev[]string, prevline string) (string) {
	if( skipping ){
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}else
	if( with_exgetline && gshCtx.GetLine != "" ){
		//var xhix int64 = int64(hix); // cast
		newenv := os.Environ()
		newenv = append(newenv, "GSH_LINENO="+strconv.FormatInt(int64(hix),10) )

		tty := ttyline(gshCtx)
		tty.WriteString(prevline)
		Pa := os.ProcAttr {
			"", // start dir
			newenv, //os.Environ(),
			[]*os.File{os.Stdin,os.Stdout,os.Stderr,tty},
			nil,
		}
//fmt.Printf("--I-- getline=%s // %s\n",gsh_getlinev[0],gshCtx.GetLine)
proc, err := os.StartProcess(gsh_getlinev[0],[]string{"getline","getline"},&Pa)
		if err != nil {
			fmt.Printf("--F-- getline process error (%v)\n",err)
			// for ; ; { }
			return "exit (getline program failed)"
		}
		//stat, err := proc.Wait()
		proc.Wait()
		buff := make([]byte,LINESIZE)
		count, err := tty.Read(buff)
		//_, err = tty.Read(buff)
		//fmt.Printf("--D-- getline (%d)\n",count)
		if err != nil {
			if ! (count == 0) { // && err.String() == "EOF" ) {
				fmt.Printf("--E-- getline error (%s)\n",err)
			}
		}else{
			//fmt.Printf("--I-- getline OK \"%s\"\n",buff)
		}
		tty.Close()
		return string(buff[0:count])
	}else{
		// if isatty {
			fmt.Printf("!%d",hix)
			fmt.Print(PROMPT)
		// }
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}
}
//
// $USERHOME/.gsh/
//		gsh-rc.txt, or gsh-configure.txt
//              gsh-history.txt
//              gsh-aliases.txt // should be conditional?
//
func gshSetupHomedir(gshCtx GshContext) (GshContext, bool) {
	homedir,found := userHomeDir()
	if !found {
		fmt.Printf("--E-- You have no UserHomeDir\n")
		return gshCtx, true
	}
	gshhome := homedir + "/" + GSH_HOME
	_, err2 := os.Stat(gshhome)
	if err2 != nil {
		err3 := os.Mkdir(gshhome,0700)
		if err3 != nil {
			fmt.Printf("--E-- Could not Create %s (%s)\n",
				gshhome,err3)
			return gshCtx, true
		}
		fmt.Printf("--I-- Created %s\n",gshhome)
	}
	gshCtx.GshHomeDir = gshhome
	return gshCtx, false
}
func setupGshContext()(GshContext,bool){
	gshPA := syscall.ProcAttr {
		"", // the staring directory
		os.Environ(), // environ[]
		[]uintptr{os.Stdin.Fd(),os.Stdout.Fd(),os.Stderr.Fd()},
		nil, // OS specific
	}
	cwd, _ := os.Getwd()
	gshCtx := GshContext {
		cwd, // StartDir
		"", // GetLine
		[]GChdirHistory { {cwd,time.Now()} }, // ChdirHistory
		gshPA,
		[]GCommandHistory{}, //something for invokation?
		GCommandHistory{}, // CmdCurrent
		false,
		[]int{},
		syscall.Rusage{},
		"", // GshHomeDir
		Ttyid(),
		false,
		[]PluginInfo{},
	}
	err := false
	gshCtx, err = gshSetupHomedir(gshCtx)
	return gshCtx, err
}
// Main loop
func script(gshCtxGiven *GshContext) (_ GshContext) {
	gshCtx,err0 := setupGshContext()
	if err0 {
		return gshCtx;
	}
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	//resmap()
	gsh_getlinev, with_exgetline :=
		 which("PATH",[]string{"which","gsh-getline","-s"})
	if with_exgetline {
		gsh_getlinev[0] = toFullpath(gsh_getlinev[0])
		gshCtx.GetLine = toFullpath(gsh_getlinev[0])
	}else{
	fmt.Printf("--W-- No gsh-getline found. Using internal getline.\n");
	}

	ghist0 := gshCtx.CmdCurrent // something special, or gshrc script, or permanent history
	gshCtx.CommandHistory = append(gshCtx.CommandHistory,ghist0)

	prevline := ""
	skipping := false
	for hix := len(gshCtx.CommandHistory); ; {
		gline := getline(gshCtx,hix,skipping,with_exgetline,gsh_getlinev,prevline)
		if skipping {
			if strings.Index(gline,"fi") == 0 {
				fmt.Printf("fi\n");
				skipping = false;
			}else{
				//fmt.Printf("%s\n",gline);
			}
			continue
		}
		if strings.Index(gline,"if") == 0 {
			//fmt.Printf("--D-- if start: %s\n",gline);
			skipping = true;
			continue
		}
		gline = strsubst(&gshCtx,gline,true)
		/*
		// should be cared in substitution ?
		if 0 < len(gline) && gline[0] == '!' {
			xgline, set, err := searchHistory(gshCtx,gline)
			if err {
				continue
			}
			if set {
				// set the line in command line editor
			}
			gline = xgline
		}
		*/
		ghist := gshCtx.CmdCurrent
		ghist.WorkDir,_ = os.Getwd()
		ghist.StartAt = time.Now()
		rusagev1 := Getrusagev()
		gshCtx.CmdCurrent.FoundFile = []string{}
		xgshCtx, fin := tgshelll(gshCtx,gline)
		rusagev2 := Getrusagev()
		ghist.Rusagev = RusageSubv(rusagev2,rusagev1)
		gshCtx = xgshCtx
		ghist.EndAt = time.Now()
		ghist.CmdLine = gline
		ghist.FoundFile = gshCtx.CmdCurrent.FoundFile

		/* record it but not show in list by default
		if len(gline) == 0 {
			continue
		}
		if gline == "hi" || gline == "history" { // don't record it
			continue
		}
		*/
		gshCtx.CommandHistory = append(gshCtx.CommandHistory, ghist)
		if fin {
			break;
		}
		prevline = gline;
		hix++;
	}
	return gshCtx
}
func main() {
	script(nil)
	//gshCtx := script(nil)
	//gshelll(gshCtx,"time")
}
//
//
Consideration
// - inter gsh communication, possibly running in remote hosts -- to be remote shell
// - merged histories of multiple parallel gsh sessions
// - alias as a function
// - instant alias end environ export to the permanent > ~/.gsh/gsh-alias and gsh-environ
// - retrieval PATH of files by its type
// - gsh as an IME
// - gsh a scheduler in precise time of within a millisecond
// - all commands have its subucomand after "---" symbol
// - filename expansion by "-find" command
// - history of ext code and output of each commoand
// - "script" output for each command by pty-tee or telnet-tee
// - $BUILTIN command in PATH to show the priority
// - "?" symbol in the command (not as in arguments) shows help request 
// - searching command with wild card like: which ssh-*
// - longformat prompt after long idle time (should dismiss by BS)
// - customizing by building plugin and dynamically linking it
// - generating syntactic element like "if" by macro expansion (like CPP) >> alias
// - "!" symbol should be used for negation, don't wast it just for job control
// - don't put too long output to tty, record it into GSH_HOME/session-id/comand-id.log
// - making canonical form of command at the start adding quatation or white spaces
// - name(a,b,c) ... use "(" and ")" to show both delimiter and realm
// - name? or name! might be useful
//---END--- (^-^)/ITS more
/*
References

The Go Programming Language MDN web docs HTML, CSS, HTTP, JavaScript...

*/ //

GShell 0.1.0 − Go1.15と内蔵find

基盤:8月11日にGo 1.15 がリリースされてたので更新しました。

基盤:以下がハイライト。

開発:リンカーが高速化って動的リンカーのことですかね。まずはビルド。これまでの1.14.3では…

開発:新作1.5では…

社長:コードが1MBくらい小さくなりましたね。

開発:この1.14.3はMacMini 3.6GHz i3x4で、1.15はiMac 3.7GHz i5x6で動かしているという違いがあります。gshのnopコマンドで比較すると、それなりの違いは見られます。

基盤:最速で比べると 26.8us : 23.9us だから、10%程度iMacのほうが速いですね。

開発:まだiMacは暇人ですしね。そのわりには値がぶれますが… まあ1コアだけ使う分にはほぼ同等と考えて良いようです。で、hello.goはというと。

開発:go run は239msが194msに19%高速化します。この理由はホストの10%の性能差には収まらないような。比較のためにCのバイナリは2.6msから2.1msへ。あーこれは20%ですね。CPU以外の足回りも含めると、 MacMini より 20%くらいiMac27のほうが速いみたいな。

基盤:大金をはたいた甲斐があったようですね。

開発:ところが、ビルドされたバイナリの起動は3ms台から6ms台に、2倍くらい遅くなります。なんなんでしょう?プラグインの動的リンクは速くなったように見えますが、これは再現性が無い。

社長:なんにしても、GShellにとってはSubstantial というような改善には見えないですね… まあ、利用法によるんでしょうけど。

開発:かといって特に新たな問題もないようですから、これからは Goは1.15で行こうと思います。

* * *

開発:さて、当面取り組みたいのは2つ、ひとつは内蔵find + 内蔵grep の検索結果で得たファイル名を vi などの外部コマンドに渡す機能。これはたとえば検索結果を環境変数に入れるとか、which や history の拡張のように!とかでファイルを呼び出せるようにする。

基盤:その機能はめっちゃ欲しいですね。

開発:もうひとつはfindの高速化。どうもGoのディレクトリスキャンはCで実現した場合より2倍遅いんです。これは os パッケージなりでディレクトリエントリを返す時に、エントリにあるファイルをGoが勝手に開いてソートして返しているせいではないかと昨日は格闘して疲れました。勝手に開くくせに、返してくるデータの中にはGoが stat システムコールで得ているはずのファイルのブロックサイズとかデバイス情報とかが含まれていないんです。これでは find の -ls とか du 互換の機能を作るのに使い物になりません。内蔵tarも作れません。で syscall パッケージでトライしたのですが、それでもまだダメっぽい。なので、これはこの際、ディレクトリのスキャンを並列化しようかと思います。

社長:ある意味面白い方向へ後押しされてますね。

開発:それにしても彼らがそういう「抽象化」とか「OS独立性」を実現したい気持ちはわかるのですが、ディレクトリのスキャンとかファイルの属性とかは、とうの昔にOSを超えて共通のものになっています。なのに今この時代にそういうところで引っかかってのってなんだろうって思うんです。

社長:10年前には片付いてた問題のように思いますけどね。そういうOSの太古の脳の部分にあれからまた新しい非互換性が生じたとは思えないんですが。

基盤:ひょっとしてAndroidに無いものは削りたいとか…

社長:Goでshellを書くとかシステム管理コマンドを作るとかは想定してないのかもですね。

社長:なんだかしらす丼が食べたくなったので食事に行ってきます。

* * *

社長:帰りました。そとは今日も夏でした。

社長:ところで私は、うちの玄関を出た時に見えるこの景色が結構好きです。

開発:電線の目障りがなければもっと良いのですが。

基盤:肉眼で感じる高さ感が消失しているのが面白いですね。

社長:それで、なんか夏らしさを写真に取ろうかと思ったのがこれ。

社長:アスファルトと夏草と短く暗い影。

基盤:残念ながらまったく伝わってこないですね。

社長:夏の撮影者。

基盤:これは少し感じるものがあります。

社長:電線と民家がなければなーと思いますね。

開発:北のベランダからポールを伸ばしてその先にカメラをつけるという計画です。

* * *

開発:さてさて、find 結果のファイルのリストを後続のコマンドで利用する機能にですが。

社長:できましたか。

開発:機能そのものはすぐに出来たのですが、ユーザにそれをどう引用してもらうかが問題でした。結果にユーザが名前を付けるのも良いのですが、ちょっとかったるい。で考えたのは、N番目のコマンドの実行の結果得られたファイルのリストを !Nf というふうに参照する、というものです。こんな感じです。

開発:行頭から -grep xxx で始まるのは、find . -exec grep xxx "{}" ";" の短縮形です。

社長:ファイルの名前による探索と中身による探索が一体化しているわけですね。

基盤:これは普通に便利。

社長:思うに従来のshellって、コマンドを起動するところまでは世話をするけど、コマンドの実行結果をどう他のコマンドに引き継ぐのかという所がほとんど無い用に思いますね。

開発:shellが直接関与している結果としてはコマンドの終了コードくらいですかね。あとは、コマンドの出力ストリームを他のコマンドにパイプで渡してやるくらいな。

社長:ファイルの中身の構造に関与しないところがUnix的な良さではあったと思うのですが、それがコマンドの実装に負担をかけていたことはあると思います。コマンドの実行の結果ファイルのリストが得られるなら、それを標準メタ情報出力的なファイルに出してもらって中継するとかが良いですかね。

開発:名前の付いたファイルの状態を保存する、ファイルの名前で他のコマンドにそれを伝える、というのは自然だとは思います。ただ、メタ情報については、名前と値の組とか、あるいはもっと構造的データ型式を定めてあげるのが良いかなとは思いますね。

基盤:今的にはJSONとかXMLでしょうか。

開発:それは嫌だなー… まあ環境変数的に、名前と値の組。値の内部構造としては… まあJSONでもいいのかな。

社長:上意下達が環境変数なので、下からの結果の戻しも環境変数と同じ構造になってれば良いですね。

開発:まあ、プロセス間でメモリマップしてやりとりすれば、ファイルであることを意識しなくても済みますしね。ファイルだと、第三者や時間を超えて情報を共有できるのが魅力です。

社長:Go言語間であれば、データのバイナリ表現であっても良いですよね。テキスト型式である必要は無い。

開発:いわゆる .conf tか .rc とかの初期設定ファイルにしても、そのための特殊な文法を導入する必要はなくて、Goで書いてコンパイルして動的リンクして組み込めば良いと思うんですよね。ユーザはGoだけ知ってればよいし、設定記述の構文的な間違いはコンパイラが見つけてくれるし、起動も速い。

社長:プラグインはGoの核心ですね。

開発:並列findの件は明日取り組みたいと思います。

-- 2020-0815 SatoxITS