Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Goのコードを書きながらAcmeエディタの基本を覚えるチュートリアル

AcmeエディタはPlan 9のためにRob Pikeによってデザインされたエディタで、現在はUnixにも移植されています。慣れると非常に快適なんですが、既存のエディタとは操作感が全然違うので、初見では何をすればいいのか分からないとよく聞きます。なので、この記事ではGoのコードを実際に書きながら、Acmeをどうやって使っていくのかを紹介していきます。ただし、Acmeは色々と便利な使い方があり、ひとつの記事で書き切るのは大変なので、複数の記事に分けて公開していこうと思っています。今のところ以下の内容を予定していますが、順番や内容は変更するかもしれません。

  1. Acmeの基本的な使い方(この記事)
  2. コードリーディングするときの使い方
  3. winで生活する方法、拡張機能の紹介など

ところでYouTubeでは、Russ CoxがAcmeを使っている様子を動画にしているので、見てみると学びがあるかもしれません。

www.youtube.com

次もYouTubeの動画で、RussがAcmeでプレゼンしている様子です。Acmeの拡張*1でコマンドをフックすることにより実現しています。

www.youtube.com

一時期、9fansで話題になっていましたが、この動画でタイトル文字のところに使っているフォントの書体が違うのはフォントをハックしているからで、Acmeには一部の文字だけを装飾する機能はありません。

基本的な使い方

まずはacmeを実行してみましょう。Acmeの実行にはplan9portが必要ですが、インストール自体はPlan 9 from User Space(plan9port)を使うに書いたのでここでは省略します。

Acmeはターミナルで

% acme

とするだけで起動します。ここでエラーになった場合はPLAN9環境変数が設定されていないことが原因かもしれないので、変数の値を確認してみてください。

Acmeの起動自体はこれだけなのですが、素の状態ではフォントが汚いとか、必要な9Pサービスが実行されていなくて使いづらい面があるので、macOSの人は以下のアプリケーション*2から使ってもらうと便利かもしれません。

github.com

Acmeを実行すると、カレントディレクトリにあるファイルが右のウィンドウで表示されていて、右は空白の画面が表示されます。

黄色いウィンドウごとに青い背景のヘッダが付いています。また、列ごとにも同様のヘッダがあり、いちばん外側にも同じものがあります。Acmeではコマンドを実行してファイルの保存などを行いますが、一部のコマンドは実行できる場所が決まっています。たとえばファイルを保存するPutは、ファイルを編集するとウィンドウごとのヘッダに出現するし、列を削除するDelcolは列ごとに用意されているヘッダにはじめから用意されています。この部分はタグライン(tag line)と呼ぶらしいけれど、分かりやすさのためこの記事ではヘッダと表記します。

最初から多くのコマンドがヘッダに並んでいますが、以下のチュートリアルで必要になったら説明するので、今はまだ気にしなくてもかまいません。

Goのコードを書く

このチュートリアルではGoのコードを書いていくので、ディレクトリを作りましょう。右のウィンドウ一番下に

mkdir src/acmetut

と入力して、mkdir src/acmetut 全体を選択した状態でマウスの中ボタン(ホイールマウスの場合はホイールクリック)を押すと、テキストで選択したものをコマンドとして実行します。中ボタンは一般的に「実行(Exec)」を意味します。トラックパッドしかない場合は、キーボードと組み合わせることで代用可能です。

  • 中クリック: Altキーを押しながらクリック
  • 右クリック: Commandキーを押しながらクリック

ただ、トラックパッドだけではボタンの同時押しができないなど非常に使いづらいので、マウスを買ったほうがいいと思います。

次に、上記で作ったディレクトリを開きます。mkdir src/acmetutと書いたテキストのうちsrc/acmetutのテキストをどこでもいいので右クリックすると、Acmeは新しいウィンドウでそれを開きます。右ボタンは一般的に「検索(Look)」を意味します。ひとつ上の例ではmkdir src/acmetutを事前に選択してから実行しましたが、スペースや記号を含まない文字列なら自動的に境界までを対象とするので、ここでは事前にsrc/acmetutを選択しておく必要はありません。

開いた直後の画面はこのようになるはず。

これ以降、ホームディレクトリは使わないので閉じましょう。ホームディレクトリが表示されたウィンドウのヘッダにDelと書かれたテキストがあるので、これにポインタをあわせて中クリックで閉じます。

(ヒント)コマンドを選択するのが面倒

上の説明でも少し言及していますが、スペースや特殊な記号を含まないテキストの場合は、選択せずにクリックするとAcmeが前後の区切りまでをコマンドの対象として扱います。src/acmetut/Delなどは単にテキスト上のどこかを中クリックするだけで実行できます。

または、テキストを入力してEscキーを押すと、直前の入力内容だけを選択した状態にするので、スペースや記号などを含む場合はEscキーで選択してから実行すると便利です。1行すべてがコマンドの場合は、行頭または行末でダブルクリックすると1行まるごと選択状態になるので、そのまま実行する方法もあります。

(ヒント)中ボタンでコマンドを実行するシェルを変更する

AcmeはもともとPlan 9のエディタなので、コマンドを実行するときのシェルはrcシェルが使われます。rcはとても書きやすいシェルなので慣れてしまえば困らないと思いますが、POSIXシェルの記法とは全く異なるので最初は混乱するかもしれません。

ここで使われるシェルを変更したい場合、acmeshell環境変数にシェルをセットしておくと、それが使われます。

% export acmeedit=bash
% acme

余談ですが、Plan 9環境変数は基本的に小文字なので、上のコマンドは間違いではありません。homepathなども全部小文字です。

コードを書いてテストする

ではまず簡単なコマンドを書いてみましょう。ホームディレクトリは閉じたので、src/acmetut/ディレクトリを表現したウィンドウだけ残っているはずです。src/acmetut/のウィンドウに

go mod init example.com/acmetut

と入力して、全てを選択した状態で中ボタンを押すと、新しいウィンドウでコマンドの実行結果が表示されます。このときウィンドウのタイトルは/path/to/dir+Errorsのように+Errorsが付いていますが、コマンドが正常に終了した場合でも+Errorsなので気にする必要はありません。src/acmetut+ErrorsのウィンドウはもういらないのでDelで閉じておきましょう。

ところで、go mod initが正常に終了したのでgo.modファイルが作られているはずですが、src/acmetut/ディレクトリはまだ何も表示されていません。Acmeは標準では自動リロードの機能を持たないので、src/acmetut/ディレクトリのヘッダにあるGetコマンドでリロードします(コマンドがなければ自分で入力します)。そうすると、現在のディレクトリにあるファイルリストでウィンドウが更新されてgo.modだけ表示されるはずです。ファイルの内容を見る場合は、ファイル名をポイントして右クリックです。以下のようになっているはず。

module example.com/acmetut

go 1.18

では次に、main.goを作成して、以下の内容で保存してみましょう。

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

もう分かると思いますが、ファイルの作成は

touch main.go

と書いて、すべて選択してから中クリックです*3main.goを開いて編集するとヘッダにPutというコマンドが増えます。main.goのコードを書き終わったら、Putを実行してファイルに保存しましょう。

(ヒント)コピペしたい

macOS版では、意外とcmd+ccmd+vに対応していた記憶がありますが、Acme本来の方法はマウスを使ったものになります。左ボタンを押しながら範囲を選択し、左ボタンを押したまま同時に中ボタンを押すと選択していたテキストをカットしてSnarfバッファ*4に格納します。同様に、左ボタンを押したまま選択して同時に右ボタンを押すと、Snarfバッファに入っているテキストをペーストします。

ではコピーはどうするのかというと、左ボタンを押したまま、同時に中→右ボタンと順番に押せばコピーです。途中で一瞬だけテキストが消えますが、このときの動作はCutUndoのようになります。

(ヒント)Acmeの設定

Acmeにはユーザーによる設定ファイルはありません*5。フォントや一部の挙動はコマンドラインオプションで指定します。

$ acme -a -f /mnt/font/GoRegular/14a/font -F /mnt/font/GoMono/14a/font

上の例で挙げたオプションは、それぞれ以下の意味を持ちます。

起動直後のAcmeプロポーショナルフォントで文字を描画しています。ウィンドウのヘッダにFontと入力して実行すると、プロポーショナルフォント等幅フォントを切り替えます。上のコマンドラインでは/mnt/fontというUnixでは見慣れないディレクトリが使われていますが、Acmeはフォントとして/mnt/fontで始まるファイルが渡されるとOSが管理するフォントを探しに行くので、このディレクトリを事前に作っておく必要はありません。

fontsrvコマンドで、具体的にどのようなフォントが利用可能か確認できます。

# フォントの名前を確認する
% fontsrv -p . | grep Go
Go-Bold/
Go-BoldItalic/
Go-Italic/
GoMedium/
GoMedium-Italic/
GoMono/
GoMono-Bold/
GoMono-BoldItalic/
GoMono-Italic/
...

# サイズを確認する(aがあるものはアンチエイリアス)
% fontsrv -p GoMono
4/
4a/
5/
5a/
6/
6a/
...

ウィンドウの移動

せっかくなのでここからのスクリーンショットはGoフォントにしました

縦に並んでいると狭いので、main.goのウィンドウを左に移動させましょう。ファイル名の左にある四角をドラッグするとウィンドウを移動できます。そうすると、右の下が空くので、

go run main.go

と書いて中クリックで実行してみましょう。正しく進めていれば以下の画面になるはず。

テストを書く

次にテストを書いてみます。main_test.goファイルを作成して、以下のコードを書いてください。

package main

import (
    "testing"
)

func TestAdd(t *testing.T) {
    n := Add(1, 3)
    if n != 4 {
        t.Errorf("Add(1, 3) = %d; want 4", n)
    }
}

今度はmain_test.goを開いているウィンドウのヘッダに

go test

と入力して実行します。Add関数はまだ実装していないので、当然ですが以下の画面になるはず。

このとき、エラーメッセージにファイル名と行番号が書かれたテキストがあるので、マウスでmain_test.go:8:7を右クリックしてみましょう。そうするとエラーが発生したソースコードの位置にジャンプして行が選択状態になるので、エラーになった行の前後をみて、原因を調べられますね。移動先のファイルが閉じている場合は、新しいウィンドウで開かれます。

今回のエラーはAddが見つからないことが原因なので、main.goAddを実装しましょう。Acmeの動作をみるため、ここでは意図的に、常に4を返すようにします。

func Add(i, j int) int {
    return 4
}

実装した後にPutで保存したら、main_test.goのヘッダに書いていたgo testがまだ残っていると思いますので、もういちどクリックするとテストが通るはずです。

テーブルテスト

ところで、上記のAddは常に4を返すバグがあります。テストを以下のように変更して

func TestAdd(t *testing.T) {
    tests := []struct{
        i, j int
        n int
    }{
        {1, 3, 4},
        {-1, -3, -4},
        {0, 0, 0},
    }
    for _, tt := range tests {
        n := Add(tt.i, tt.j)
        if n != tt.n {
            t.Errorf("Add(%d, %d) = %d; want %d", tt.i, tt.j, n, tt.n)
        }
    }
}

これを保存してからgo testすると以下のような出力になるはずです。

これまでと同じように、main_test.go:19を右クリックしてエラーの箇所(この場合はErrorfの行)にジャンプします。Goでは基本的に<filename>:<line>[:<offset>]のルールで行を出力するので、そのままAcmeでジャンプできるようになっています。この程度の行数だとあまり便利さを感じませんが、もっとテストケースがあるファイルの場合は嬉しいはず。

テストが失敗する理由は見れば分かりますね。常に4を返すところが問題なので、return i + jに変更してテストをすれば通ります。

(ヒント)スクロールの方法

Acmeはキーボードの上下キー、またはスクロールバーの上でマウスの左右ボタンをクリックするとページ単位のスクロールを行います。また、スクロールバーの上でマウスの中ボタンをクリックすると、クリックした位置まで移動しますし、中ボタンを押しながらマウスを上下に移動させると、マウスの移動にあわせて画面がスクロールします。個人的には、ファイルを頭から読むときはキーボードの上下で移動して、それ以外はマウスの中ボタンを使うことが多いですね。

キーボードなどでカーソルを上下に移動する方法は(たぶん)ありません。最初は常にキーボードから手を離さないようにしたかったですが、慣れてしまえば全然困りませんし、広い範囲を選択するときはマウスの方が早いと思います。

gofmt

ところでAcme自体は汎用のテキストエディタなので、保存時にgofmtで整形するなどは行いません。なので少し面倒ですが手動でgofmtしておきましょう。gofmtする方法は複数あります。最も簡単なのは、どこか適当な場所に

go fmt

と書いて実行した後に、差分のあったファイルをGetで再読み込みする方法です。それでもいいのですが、より応用がきく方法として、テキスト全体を選択した状態でヘッダ部分に

| gofmt

と書いて実行すると(先頭の|が重要です)、選択した部分にgofmtを適用します。この方法なら、一部の範囲をソートしたければ選択してから| sortとすればいいし、ほかにも色々と便利に使えます。

ファイル全体を選択するのが面倒な場合は、

Edit , | gofmt

として実行するだけでもいいです。EditAcme独自のコマンドで、Edit 1,$とすると1行目から最終行までを選択状態にしますが、1$は省略できるので,だけでファイル全体を選択する動作になります。

(ヒント)別のターミナルからファイルを渡したい

plumberが動作していればBコマンドで開けます。

最初のほうで紹介したacmeeditリポジトリのパッケージから起動すると、plumberが動作していなければ自動で実行するようになっているのでお手軽です。Bコマンドはファイル名だけではなく行番号なども一緒に渡せるので、以下のような開き方ができます。

% B main.go

% B main.go:20

% B 'main.go:/^func main/'

エディタの終了

では最後にエディタの終了方法です。ここまで進めたならもう分かると思いますが、Acmeのいちばん外側にあるヘッダからExitを実行すれば終了します。ただし、変更されたあと保存していないファイルがが残っている場合はExitが中断するので、Putで保存するかDeleteで保存せずに閉じてからExitしましょう。

おすすめする使いかた

Acmeでは拡張を書かなくても面白い使い方ができますが、特におすすめするのは、独自のスクリプト<filename>:<line>[:<offset>]形式のテキストを出力することです。簡単なものだと

grep -n '<pattern>' file /dev/null (git grepでも同じ)

とすると<pattern>にマッチした行ごとに<filename>:<line>形式の行番号も出力するので、修正する場所をあらかじめ絞り込むときに使うと便利です。grepの結果が出力された+Errors ウィンドウもただのテキストなので、不要な行は削除しておくと、より見通しがよくなります。Plan 9にはgという、カレントディレクトリのソースコードgrepする便利なラッパーコマンドがありますし、Rob Pike氏が紹介していたfcfといったコマンドも、やっていることは同じですね。

blog.lufia.org

ほかにも、grepした結果をブックマークのように残しておくと便利な場合があります。例えばfunc Addgrepすると

/home/lufia/src/acmetut/+Errors

というタイトルになるのですが、このウィンドウを開いたままfunc maingrepすると、結果はfunc Addのウィンドウに追記されます。これはこれで便利なときもあるものの、量が多くなると見通しが悪くなるので、区切りのいいところで+の前に任意の文字列を入れておきましょう。たとえばヘッダを編集して

/home/lufia/src/acmetut/Add+Errors

としておくと、次回以降にgrepしたときは新しく/home/lufia/src/acmetut/+Errorsウィンドウが作られるので、Addのウィンドウをそのまま残すことができます。これは比較的規模の大きいコードを読み進めるときに便利ではないかなと思います。

Acme上でターミナルを開く

Acme上でそのままbashなどを実行しても、すぐに終了してしまって使えません。winという特別なコマンドが用意されていて、winを実行すると新しいウィンドウでシェルのプロンプトが表示されて、そのままシェルとして利用できます。

シェルを実行しているウィンドウもただのテキストなので、コマンドの出力結果などを自由に編集できますが、Unixのターミナルと異なりヒストリや色などには対応していません。ヒストリについては " (quote1) と "" (quote2) が用意されていて、winの履歴からプロンプトのような行をgrepするコマンドがあるので、これを使うことになるでしょう。

色や装飾については、コマンドごとに無効化していく必要があってやや面倒です。次回以降のどこかで紹介するかもしれません。

Acmeのターミナルでgit commitしたい

Russ Cox氏によりeditinacmeコマンドが公開されているので、これをEDITOR環境変数またはGIT_EDITOR環境変数にセットします。

% go install 9fans.net/go/acme/editinacme@latest
% export GIT_EDITOR=editinacme

次の予定

基本的な操作のチュートリアルはこれで終わりです。次回はAcmeでコードリーディングすると便利だという話を書こうと思っていますが、先にGitを使えたほうが便利かもしれないので順番は前後するかもしれません。

過去にもAcmeの使い方を書いた記事があるので、こちらはチュートリアルではありませんが、興味があれば読んでみてください。

blog.lufia.org

*1:実態はAcmeが提供するファイルサーバを読み書きするプログラム

*2:実態はacmeを起動するまでに準備するだけのスクリプト

*3:他の方法もあるけどtouchがいちばん簡単だと思う

*4:クリップボードのようなものです

*5:起動時に読み込みするacme.dumpファイルはありますが、これはウィンドウの状態を保存するためのもので設定ではない