生涯未熟

生涯未熟

プログラミングをちょこちょこと。

echoのmiddlewareで一度読んだRequestBodyをもう一度読む

Gopherの皆さんはecho使ってますでしょうか?僕は薄くて好きなのでよく使ってます。

さて、echoにはmiddlewareという機能がございます。
Middleware | Echo - High performance, minimalist Go web framework

ハンドラの実行前に処理差し込めるやつですが、このmiddlewareとして以下のような処理を差し込むとします。

func CheckJSON(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        jsonBlob, err := ioutil.ReadAll(c.Request().Body)
        if err != nil {
            return err
        }
        s := string(jsonBlob)
        if !validator.IsJSON(s) {
            return c.NoContent(http.StatusBadRequest)
        }

        return next(c)
    }
}

飛んできたリクエストがJSONかどうか?というのをチェックするmiddlewareになります。
さて、このmiddlewareを実行した後にハンドラでこのような処理を実行するとしましょう。

func Hoge(c echo.Context) error {
    var jsonBody map[string]interface{}
    jsonBlob, err := ioutil.ReadAll(c.Request().Body)
    if err != nil {
        return err
    }
    err := json.Unmarshal(jsonBlob, &jsonBody)
    if err != nil {
        return errors.New("failed unmarshal json")
    }

    return c.NoContent(200)
}

またもやリクエストから読み込んでる処理ですね。
さて、これを実行すると failed unmarshal json のエラーになります。
これは ioutil.ReadAll がc.Request().Bodyを全て読み込んでいるため、次回以降読み込もうとするとnilが返ってくるためです。

で、ここからどうやって2回目の読み込みを実現するかですが、答えは簡単で「Bodyの内容を上書く」という解決方法があります。

func CheckJSON(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        jsonBlob, err := ioutil.ReadAll(c.Request().Body)
        if err != nil {
            return err
        }
        s := string(jsonBlob)
        if !validator.IsJSON(s) {
            return c.NoContent(http.StatusBadRequest)
        }

        c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(jsonBlob))

        return next(c)
    }
}

こうすることで2回目にキチンと ioutil.ReadAll を実行しても内容を読み込んでくれます。

刺し身たんぽぽについて

刺し身たんぽぽについて調べてみました。

f:id:syossan:20190106180834p:plain

刺し身たんぽぽとは一体何なのか?

刺身の盛り合わせに添えられている食菊(食用菊)の通称。花の大きさからタンポポと誤認されやすい。菊には殺菌作用があり、かつ見た目の彩りや香りも添える。さらに刺身のツマとして食べられる。近年では形骸化しつつある。 - weblio辞書より引用 -

なるほど、実際にはたんぽぽでは無いのですね。勉強になります。
あと、食べれるものとは知りませんでした。むしろ、あんな花花しいものを食べる人はいるのだろうか?

いつから刺し身たんぽぽは誕生したのか?

奈良時代に、日本で現在でも食用菊として栽培されている「延命楽(もってのほか・カキノモト)」が中国から伝来した[2]。 - wikipediaより引用 -

そんな1000年以上前から刺し身たんぽぽはあったのですね・・・しゅごい。

食用としては、江戸時代から民間で食されるようになったとされており[4]、1695年に記された『本朝食鑑』に「甘菊」の記述が見られる[5]。 - wikipediaより引用 -

実際に食されるようになったのは結構最近なのですね。
刺し身のツマとして用いられるようになった経緯についても深掘りしたかったのですが、検索しても見つからず。インターネットの敗北です。

刺し身たんぽぽの効用とは?

刺し身にたんぽぽが入っている理由としては「殺菌作用・彩り」の2点のようですね。
このたんぽぽ自体には発ガン効果の抑制・コレステロールの低下・中性脂肪を低下させる効果があるそうです。めちゃくちゃ有能やんけ。

刺し身たんぽぽの危険性

ただ、こんな刺し身たんぽぽですがキク科のアレルギーを持つ方にとっては食すのに危険性が伴います。

togetter.com

食べる時には自分がアレルギーを持っていないか気を付けましょう。
また、刺し身たんぽぽという単語を真に受けて、その辺に生えてるたんぽぽを生で食べないようにしましょう。普通に汚いですからね。

刺し身たんぽぽについて完全に理解した

年始から刺し身たんぽぽの話題で溢れていたので、完全に理解するために色々と調べてみましたがどうだったでしょうか?
こういう単語はしっかり調べた上で、きちんと使える大人になりましょうね!

treeコマンド 〜Rオプションの謎〜

treeコマンド、皆さん知ってますか?
そうです、あのファイルやらディレクトリやらを良い感じに木構造で出力してくれるニクい奴です。

f:id:syossan:20190103044420p:plain

こんな感じ

さて、そんなtreeコマンドには多くのオプションがあります。
よく使われるのは -L だったり、 -a とかですかね?

そんな中、 -R オプションというものが存在するのをご存知でしょうか?

Rオプションとは何なのか?

まずは、 --help オプションに書かれている内容を見てみましょう。

-R            Rerun tree when max dir level reached.

直訳すると 最大ディレクトリレベルに達したらツリーを再実行します。 ってとこですかね。
あー、再帰的にツリーを辿るのねと試しに以下のようなコマンドを実行してみました。

$ tree -L 1 -R -o result

これで1階層ごとにtreeの結果が出力されるじゃね?と思ってみました。(あまりやっても意味ないですが・・・
で、やってみた結果、ワーキングディレクトリの下にしかresultが吐かれませんでした・・・

さてはて、予想が外れたのですが冷静に次はmanコマンドを参照してみます。

-R     Recursively cross down the tree each level directories (see -L option), and at each of them execute tree again adding `-o 00Tree.html' as a new option.

これを直訳すると

再帰的に各レベルのディレクトリにツリーをたどります(-Lを参照)
そして、それらのそれぞれで、新しいオプションとして '-o 00Tree.html'を追加して、それぞれtreeを実行します。

オッ、 --help の時と違って新たな -o 00Tree.html という概念が出てきましたね。
ただ、先の実行結果を見渡してみても 00Tree.html とやらが吐き出されている様子は無い模様。
ますます頭にはてなマークが生えてきました。

treeコマンドのソースコードを読む

このままでは埒が明かないので、treeコマンドのソースコードを読んじゃいましょう。
C言語で書かれているので、そこまで難しくはないはずです。

ソースコードは以下のリンク先からよしなに取得してきてください。
The Tree Command for Linux Homepage

さて、それでは読んでいきたいと思います。

まず、何はともあれ -R オプションを追っかけましょう。

case 'R':
  Rflag = TRUE;
  break;

-R オプションはこのようにコード内では Rflag という変数に置き換えられます。
これを追いかけようと、tree.cの中を見てみたのですが、まさかの以下のコードしか参照しているところがありませんでした。

if (Rflag && (Level == -1))
  Rflag = FALSE;

んん?意味自体は分かりますが、これが再帰処理に繋がる気配は無さそうですね・・・
一応他のコードも読んでみたのですが、これまたまさかの html.c というファイル内に以下の意味ありげなコードが書かれていました。

/* This is really hackish and should be done over. */
if (Rflag && (lev == Level) && (*dir)->isdir) {
  if (nolinks) fprintf(outfile,"%s",(*dir)->name);
  else {
    fprintf(outfile,"<a href=\"%s",host);
    url_encode(outfile,d+1);
    putc('/',outfile);
    url_encode(outfile,(*dir)->name);
    fprintf(outfile,"/00Tree.html\">");
    html_encode(outfile,(*dir)->name);
    fprintf(outfile,"</a><br>\n");
  }

  hdir = gnu_getcwd();
  if (sizeof(char) * (strlen(hdir)+strlen(d)+strlen((*dir)->name)+2) > pathsize)
    path = xrealloc(path, pathsize = sizeof(char) * (strlen(hdir)+strlen(d)+strlen((*dir)->name) + 1024));

  sprintf(path,"%s%s/%s",hdir,d+1,(*dir)->name);
  fprintf(stderr,"Entering directory %s\n",path);

  hcmd = xmalloc(sizeof(char) * (49 + strlen(host) + strlen(d) + strlen((*dir)->name)) + 10 + (2*strlen(path)));
  sprintf(hcmd,"tree -n -H \"%s%s/%s\" -L %d -R -o \"%s/00Tree.html\" \"%s\"\n", host,d+1,(*dir)->name,Level+1,path,path);
  system(hcmd);
  free(hdir);
  free(hcmd);
}

うーーーーーん、This is really hackish and should be done over.というコメントから察するに、なんか難易度高そうっすね・・・
一応この処理は -H オプションを付けて実行した際のHTMLを構築する処理の一部になります。

HTML構築のためにディレクトリを -L で指定した深さまで辿っている途中に、この処理が出てくるのですが、ざっくり読み解いてみると、どうやらRオプションがあり、かつLオプションで指定した深さのディレクトリの場合にHTMLを出力しつつ、再帰的に tree コマンドを実行していますね。
manの説明にあった、 00Tree.html の存在も伺えます。どうやら再帰的に実行する際に、各ディレクトリに 00Tree.html を生成しているようです。

というわけで、なんとなく分かったこととして -R オプションは -H オプションと共に使わないと意味が無さそうってことですね。
では実験してみましょう。

実験

それでは -H オプションを付加してやってみましょう。

$ tree -L 1 -HR .

<!DOCTYPE html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <meta name="Author" content="Made by 'tree'">
 <meta name="GENERATOR" content="$Version: $ tree v1.8.0 (c) 1996 - 2018 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro $">
 <title>Directory Tree</title>
 <style type="text/css">
  <!--
  BODY { font-family : ariel, monospace, sans-serif; }
  P { font-weight: normal; font-family : ariel, monospace, sans-serif; color: black; background-color: transparent;}
  B { font-weight: normal; color: black; background-color: transparent;}
  A:visited { font-weight : normal; text-decoration : none; background-color : transparent; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:link    { font-weight : normal; text-decoration : none; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:hover   { color : #000000; font-weight : normal; text-decoration : underline; background-color : yellow; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:active  { color : #000000; font-weight: normal; background-color : transparent; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  .VERSION { font-size: small; font-family : arial, sans-serif; }
  .NORM  { color: black;  background-color: transparent;}
  .FIFO  { color: purple; background-color: transparent;}
  .CHAR  { color: yellow; background-color: transparent;}
  .DIR   { color: blue;   background-color: transparent;}
  .BLOCK { color: yellow; background-color: transparent;}
  .LINK  { color: aqua;   background-color: transparent;}
  .SOCK  { color: fuchsia;background-color: transparent;}
  .EXEC  { color: green;  background-color: transparent;}
  -->
 </style>
</head>
<body>
    <h1>Directory Tree</h1><p>
    <a href=".">.</a><br>
    ├── <a href="./LICENSE">LICENSE</a><br>
    ├── <a href="./Makefile">Makefile</a><br>
    ├── <a href="./README.md">README.md</a><br>
    ├── <a href="./cmd/00Tree.html">cmd</a><br>
Entering directory /Users/syossan27/go/src/go-tree/cmd
Entering directory /Users/syossan27/go/src/go-tree/cmd/go-tree
<br>
    ├── <a href="./dist/00Tree.html">dist</a><br>
Entering directory /Users/syossan27/go/src/go-tree/dist
<br>
    ├── <a href="./go.mod">go.mod</a><br>
    ├── <a href="./go.sum">go.sum</a><br>
    ├── <a href="./tree.go">tree.go</a><br>
    ├── <a href="./tree.html">tree.html</a><br>
    ├── <a href="./tree_windows.go">tree_windows.go</a><br>
    └── <a href="./validate.go">validate.go</a><br>
    <br><br>
    </p>
    <p>

2 directories, 9 files
    <br><br>
    </p>
    <hr>
    <p class="VERSION">
         tree v1.8.0 © 1996 - 2018 by Steve Baker and Thomas Moore <br>
         HTML output hacked and copyleft © 1998 by Francesc Rocher <br>
         JSON output hacked and copyleft © 2014 by Florian Sesser <br>
         Charsets / OS/2 support © 2001 by Kyosuke Tokoro
    </p>
</body>
</html>

おー、HTMLは無事出力されましたね。
この状態で、 00Tree.html とやらは各ディレクトリに出力されているのか確認します。

$ tree
.
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── 00Tree.html
│   └── go-tree
│       ├── 00Tree.html
│       └── main.go
├── dist
│   ├── 00Tree.html
│   ├── go_tree_darwin_386
│   ├── go_tree_darwin_amd64
│   ├── go_tree_freebsd_386
│   ├── go_tree_freebsd_amd64
│   ├── go_tree_freebsd_arm
│   ├── go_tree_linux_386
│   ├── go_tree_linux_amd64
│   ├── go_tree_linux_arm
│   ├── go_tree_netbsd_386
│   ├── go_tree_netbsd_amd64
│   ├── go_tree_netbsd_arm
│   ├── go_tree_openbsd_386
│   ├── go_tree_openbsd_amd64
│   ├── go_tree_windows_386.exe
│   └── go_tree_windows_amd64.exe
├── go.mod
├── go.sum
├── tree.go
├── tree.html
├── tree_windows.go
└── validate.go

オッ、たしかに出てますね。

で、出力されたHTMLを見てみると以下のような感じになります。

f:id:syossan:20190103053127p:plain

ほほー?一応cmdディレクトリのリンクを踏んでみましょうか。

f:id:syossan:20190103053158p:plain

なるほど、リンク先もtree構造のHTMLになってますね。
これが -R オプション無しだと、リンク先を踏んだ場合にはこんな感じになります。

f:id:syossan:20190103053251p:plain

普通にブラウザでローカルディレクトリを覗いた時の画面ですね。
という形の結果になりました。

まとめ

結局のところ -R オプションってなんぞ?というところなのですが、これはtreeコマンドの開発者が単に内部的に00Tree.htmlを吐く機構が欲しかったから作ったのでは・・・?という感想があります。
CHANGELOGとかも遡って見たり、インターネット上を彷徨ったりしたのですが特に有力な情報は見つからず・・・

あと、ちょっと気になるところとして 00Tree.html の親ディレクトリへのリンク先を押しても戻ることが出来ず、リンク先の出力ロジックがなんか間違えてるんじゃないかと。
こういった時のバグの報告とかどうやればいいのかちょっと謎なので、treeコマンドに詳しい方、情報お待ちしております。

2018年の棚卸し

2018年もそろそろ終わりですね。

恒例の本年の棚卸しをやっていきたいと思います。

1月

syossan.hateblo.jp

今年の抱負は「つよく いきる」でした。
つよく いきました、よかったですね。

syossan.hateblo.jp

homebrewに自作ツール登録しようとして失敗したりしました。
来年は登録基準を満たすレベルのリポジトリを生成したいですね。

2月

syossan.hateblo.jp

TebataというLinuxシグナルをハンドリングするGoライブラリ作ったりしました。
ちょこちょこと使って頂いてるみたいで、

github.com

そこそこ大きいライブラリでも使って頂いたり、

github.com

何故かワームプログラムに使われたりしてました。
悪いことはやめようね!

3月

syossan.hateblo.jp

なんかバズりました。
けど結局Twitterをまだやってます。付き合い方は変わったと思うので、この時の経験は活きたのかな?と思います。
一旦距離を置くというのは大事ですね。

4月

syossan.hateblo.jp

Goの勉強会を開催し始めました。
僕の中でgolang.tokyoや、GoConでの登壇って皆様発表内容が高レベルでハードル高いなーと思っていたので、なんかゆるっと登壇できる勉強会を作りたかったのです。
来年も細々と続けていきたいです。

5月

syossan.hateblo.jp

クソ雑魚ナメクジエンジニアはこんなくらいのスカウトが来るよって事例を書いてみたりしました。
直近の転職ドラフト結果もまた書きたいですね。

syossan.hateblo.jp

主催の勉強会でGoの並行処理についての発表しました。
英語苦手なりに原書を頑張って読んだら、この5か月後に和訳版が発売されました。泣きたい。
というのは冗談で、めちゃくちゃ良い本なのでGopherは是非読みましょう。

6月

syossan.hateblo.jp

Cassandra周りをガリガリやってた時に、あまり日本語情報が無い現象にぶち当たったりしました。

syossan.hateblo.jp

Kafkaもガリガリとやってたので、ライブラリの挙動にハマったりしましたねぇ。
Kafkaはもっと極めていきたい。

7月

syossan.hateblo.jp

ランサーズさんの開発ランチにお邪魔したりもしました。

syossan.hateblo.jp

Go猿本読んだりもしました。
来年もGoの良本出ないかな〜。

gounconference.connpass.com

勉強会3回目をやったり。

8月

syossan.hateblo.jp

書籍のレビューをやらせて頂いたりもしました。
貴重な経験ができました。

syossan.hateblo.jp

あと、Sparkが分からなすぎてキレたりしてました。

9月

syossan.hateblo.jp

この頃からElmの教祖に熱心な勧誘を受け、Elm教に入信致しました。
今ではフロントはElm以外で書く気がしません。

syossan.hateblo.jp

buildersconに初参加もしました。
jollyjoester・・・一体何者だったんだ・・・

10月

gounconference.connpass.com

4回目やりました。

そして30になりました。
30代が不安過ぎてつらい。

11月

syossan.hateblo.jp

これ、今年めちゃくちゃ嬉しかったです。
ありがとうJetBrains、来年もいっぱいお布施します。

12月

アドカレ5本仕込みました。
あまりウケがよろしくなかったので、来年はもっとウケるようなネタを書きたいですね。

まとめ

というわけで、30になってしまいました。
こんなクソ雑魚ナメクジエンジニアなのになんの成果もなく30になってしまい、将来が不安です。
とりあえず来年もがむしゃらにエンジニアリングをがんばっていきたいと思います。

GoでSSH Managerを作成した際の知見

この記事は Go Advent Calendar 2018 の23日目の記事です。

今回は過去に作成したSSH Managerのお話をさせて頂きます。

github.com

どんなツールなのか?

ざっくり言うとSSH接続情報を管理するツールになります。
今の所パスワード認証のみ対応しておりますが、ゆくゆくは公開鍵認証にも対応していく予定です。

簡単に使い方ですが、まずはSSH接続先設定を追加し、

f:id:syossan:20181222025351p:plain

以下のように設定したSSH設定の名前を指定して、実行することでSSH接続が可能になります。

f:id:syossan:20181222024657p:plain

何故作ったか?

所属している会社で運用しているサーバが、パスワード認証で管理されており、毎回user/passwordを入力するのが面倒だなと思ったのがキッカケでした。
パスワード認証の場合、 .ssh/config での接続管理も出来ないので、だったらサクッとGoでSSH Managerを作ってしまおうと思い立ったのです。

類似のツールがあれば良かったのですが、パスワード認証に対応したツールは見つからず・・・

実装

まずは、実装したいものを大きく3つのステップに分けてみました。

  • 接続名、接続先、ユーザー、パスワードの追加・更新・削除、及びAES暗号化
  • パスワード認証によるSSH接続
  • bash-completionの対応

それではひとつずつ実装を追って行きたいと思います。

接続名、接続先、ユーザー、パスワードの追加・更新・削除、及びAES暗号化

実装のイメージとして、下図のようなフローを考えつつ実装を始めました。

f:id:syossan:20181222115732p:plain

順に流れを追って解説致します。

接続情報管理ファイルの操作

接続情報を保持しておくために、接続情報管理ファイルを置き、そこから接続情報の読み込み等を行うようにしました。
そのため、まずは接続情報管理ファイルの存在を確認し、存在しない場合は生成する処理を実装します。

次に、ファイルの存在を確認できたら読み込み処理に入ります。

読み込み処理は少し複雑で、暗号化された接続情報管理ファイルの復号処理を行っております。
今回はAES暗号を採用しており、ハッシュ化にはSHA-256を用いています。

主に復号は次のステップで行っています。

  1. 暗号・復号に用いる鍵をSHA-256で変換
  2. 暗号化された接続情報管理ファイルの内容をbase64デコードする
  3. 変換された鍵を基にブロック・サイファーでのブロックを生成
  4. CTRモードでの鍵ストリーム生成のために初期化ベクトル、また初期化ベクトルを除いた暗号データを取得
  5. CTRモードで鍵ストリームを生成し、XORすることで平文を取得

あまり暗号には詳しくないのですが、多分この流れでキチンと暗号化されている・・・はずです。(※有識者の方、ご指摘あればお願いします)
この暗号・復号に際して、deeeetさんのこちらの記事がとても参考になりました。この場をお借りして感謝🙇

Go言語と暗号技術(AESからTLS) | SOTA

さて、無事に復号出来たところで、このデータを用いて接続情報の追加・更新・削除を行っていきます。

まずは追加処理から。

こちらも復号と同じプロセスで接続情報を暗号化し、接続情報管理ファイルに書き込みを行っております。
接続情報入力に際してはサクッと実装したかったのでprompterを使っています。便利。

github.com

更新処理はあまり追加処理と変わらないですが、このようになります。

どんどんいきましょう、次は削除処理。

これも指定した接続情報を除外した接続情報配列をファイルに保存し直すという簡単な処理になっています。

この他に、接続情報の一覧表示もあるのですが、説明を省略させていただきます。

パスワード認証によるSSH接続

さて、ここからは保存した接続情報を用いた、パスワード認証によるSSH接続の実装に入ります。
何はともあれ、まずはコードを見ていきましょう。

ガガガっと書かれていますが、こちらも大きく分けて2つの要素で構築されています。

  • SSHセッションの確立と設定
  • SSH擬似端末の設定

SSHセッションの確立と設定

connect 関数でセッションの確立を行っています。
パスワード認証の場合、SSHクライアント設定にてパスワード認証を渡す必要があります。

セッション確立の中でアレ?となるのは HostKeyCallback: ssh.InsecureIgnoreHostKey() のところでしょうかね。
これはSSHセッション確立の際に、finger print verificationをskipしている処理になります。
コメントを読むと It should not be used for production code. と書いてあるんですが、正直使ってるの自分くらいなので怒られたら直します。(あかん

セッションの設定については、入出力の設定とSSH擬似端末の関連付けているところくらいですかね。
あまり特筆すべきことはないですが、入出力設定とかはマストだと思うので書くのを忘れないようにしましょう。

SSH擬似端末の設定

次にSSH擬似端末の設定ですが、ここでいちばん大事なのは擬似端末のモード設定になります。
擬似端末にはRAWモードとCOOKEDモードの2種類があり、何が違うかというと「入力文字データをすぐに送信するか、バッファに溜めてから送信するか」の違いになります。
通常、今回のようなSSHに関するツールを作る際にはRAWモードが使われると思いますが、うっかりCOOKEDモードで動かしてしまうとこんな感じになってしまいます。

f:id:syossan:20181222182630p:plain

気を付けましょう。以下のリンク先を読むとより理解できるかと思います。

d.hatena.ne.jp

また、コード内にコメントとして詳しく書いてありますが、RFC4254を読むことでどのようなパラメータを設定すればよいか?などが分かるので、困った時は読むのをオススメします。

bash-completionの対応

最後にbash-completionの対応に入ります。 bash-completionとはよくあるコマンドの入力補完ですね。今回はサブコマンドや接続名の補完に使っていきたいと思います。
このSSH Managerでは urfave/cli を使っており、それを前提としたお話になりますのであしからず。

github.com

さて、bash-completionの実現には次の2つのステップを踏む必要があります。

  • bash-completion機能の有効化
  • 接続名補完の実装

bash-completion機能の有効化

urfave/cli のREADMEにも丁寧にbash-completion機能についての説明がありますが、やることとしては

になります。

EnableBashCompletionをtrueに設定するのはとても簡単で、以下のように行います。

次に、bash_autocompleteファイルの設置ですが、READMEに詳しい方法が書かれています。

https://github.com/urfave/cli#distribution

要するに、「/etc/bash_completion.d/にbash_autocompleteファイルを置く」という手順になります。
簡単は簡単なのですが、ユーザーにとっては一回限りの行動であったとしてもハードルがあるのは否めません。
そこで、この手順をサブコマンドの一つとして組み込むことにしました。

やっていることは単純で、sudo権限でコマンドを実行してもらい、手順通りに/etc/bash_completion.dディレクトにファイルを設置している形になります。
中でも、sudoのチェックの方法が「uidが0か否か」というのを知らなかったので、実装しながらへぇ〜って唸ってました。

さて、ここからは接続名補完の実装のお話です。
幸いなことに、 urfave/cli ではbash-completion機能を有効化にするだけで、サブコマンド等の補完は実現できます。
ただし、接続名などCLIツールとしての範疇外のところは自分で実装する必要があります。
該当コードとしては以下の部分です。

見て分かるように、やっていることは単純に接続名を出力しているだけですね。
このように補完したい項目をfmt.Printlnで出力してあげるだけで実装できます。よかったですね。

まとめ

このように、様々なハマりどころがあったりしますが、比較的簡単にSSH Managerを作成することが出来ました。
実際に、業務の中で試したりしていますが、今の所なんとか使えるかなーというレベルにはなっています。
ただし、動的な擬似端末サイズの変更などまだまだ実装しきれていないところがありますので、その辺もゆくゆくは実装していきたいと考えています。

以上、長文でしたがお読み頂きありがとうございました。