R/O

CLI での作業メモをディレクトリ単位で管理する

Posted on 2021-01-16

作業メモの管理どうする問題

コンピュータ上で何かしら作業をしながらメモを取る場合、人によって色々な流儀があると思う。私はターミナル上でテキストエディタ (Vim) を起動してプレーンテキストで書くことが多い。

そうしているとテキストファイルの管理に困りがちで、

というようなことがあり、折衷案として作業ディレクトリごとにメモ置き場を用意する方法を考えた。 案というほど大したものではなくて、単に専用のディレクトリ (.notes) を作って全部そこに入れるだけ。

$ mkdir .notes
$ vim .notes/TODO

こうしておくと作業ディレクトリの直下がメモで散らかることはないし、バージョン管理システムに無視させる場合もディレクトリ一つの方が扱いやすい。Git の場合は ~/.config/git/ignore にグローバルな ignore 設定を書ける1 ので、そこに .notes を追加するだけで済む。

作業ディレクトリからたどって参照したいけれど、ファイルの実体は作業ディレクトリの外に置いておきたい、という場合もある。例えば定期的にメモのバックアップを取ることを考えると、作業ディレクトリごとに散らばっているよりは一か所にまとまっているほうが扱いやすい。これはシンボリックリンクを使えば簡単に実現できる。

$ mkdir -p ~/notes/some-project
$ ln -s ~/notes/some-project/ .notes
$ vim .notes/TODO

これですべて解決……とはならなくて、

という問題がある。このあたりの手間を減らすものがあるとうれしい……ということで、sidenote という小さい CLI ツールを作って使っている。

sidenote のかんたんな紹介

インストール方法は README を参照。 シングルバイナリで動くコマンドで、Bash 用のコマンドライン補完もある。

機能はサブコマンドに分かれていて、v0.1.6 時点では以下の通り。

$ sidenote -h
Usage: sidenote [-d path] [-version] <command> [command-arguments]

options:
  -d string
        Specify the directory for notes (env: SIDENOTE_DIR)
  -version
        Print the version and exit

commands:
  cat       Print contents of notes
  edit      Open notes with the editor ($VISUAL or $EDITOR)
  import    Import a note from the existing file or the standard input
  init      Initialize the directory for notes
  ls        List notes
  path      Print the path of notes
  rm        Remove notes
  serve     Serve notes over HTTP
  show      Open notes with $PAGER

Run sidenote <command> -h for usage of each command.

何かを書きたくなったら、まず init コマンドでカレントディレクトリ直下にディレクトリ (デフォルトでは .notes、名前は変更可能) を作る。

$ sidenote init
initialized .notes
$ ls -dl .notes
drwxr-xr-x 2 ryot4 ryot4 4096 Feb  9 19:03 .notes

カレントディレクトリの外にファイルを置きたい場合は init -l にパスを与えてその場所へのシンボリックリンクを作る。 指定したパスにディレクトリが存在しない場合は自動的に作成される。

$ sidenote init -l ~/Documents/notes
initialized .notes (-> /home/ryot4/Documents/notes)
$ ls -l .notes
lrwxrwxrwx 1 ryot4 ryot4 31 Feb  9 19:03 .notes -> /home/ryot4/Documents/notes

準備は以上で、メモの作成・編集は edit コマンドで行う。例えば、

$ sidenote edit TODO

とすると .notes/TODO をデフォルトのエディタ ($VISUAL または $EDITOR に設定されているもの) で開く。 .notes/ の部分は裏側で自動的に補われるので手で指定する必要はない。

カレントディレクトリに .notes が存在しない場合、sidenote はディレクトリ階層を上にたどって探す。2 例えば

$ tree -aF project
project/
├── .notes/
│   └── TODO
└── src/
    └── module/

    3 directories, 1 file

のようなディレクトリ構造がある場合、module ディレクトリの中で sidenote edit TODO を実行すると

  1. module の直下には .notes が無いので一階層上の src を見る
  2. src の直下にも .notes が無いので一階層上の project を見る
  3. project の直下に .notes があるので、その中の TODO をエディタで開く

という動きになる。この挙動によってサブディレクトリの中でも相対パスを意識せずにファイルを扱える。

基本はこれだけで、他にファイルの一覧 (ls) や内容の表示 (cat / show)、削除 (rm) などを行うコマンドがある (より詳しい使い方は README を参照)。

既存のコマンドを使って楽をする

sidenote コマンドはなるべく単純なものにしたいと思っていて、例えば検索を行うサブコマンドは存在しない。 代わりに .notes ディレクトリの場所を出力する path コマンドがあって、これと既存の検索コマンドの組み合わせで検索を行えるようになっている。

path コマンドは今いるディレクトリに対応する .notes ディレクトリのパスを出力するもので、上の module ディレクトリの例だと以下のようになる。

module$ sidenote path
../../.notes

これを使うと、例えば全てのメモの中から XXX という文字列を grep したい場合は以下のようにできる。

$ grep -R XXX "$(sidenote path)"

例えば sidenote grep XXX のような組み込みの検索コマンドを用意する方が直感的でコマンド入力も楽になるけれど、path と外部コマンドを組み合わせる方法は検索に grep 以外のものを使いたくなった場合でも対応できるし、検索に限らず「ファイルパスを受け取って何かする」類の処理全般に応用できるので柔軟性がある。 何より、実装量を減らせるので楽で良い。

同様に、cd "$(sidenote path)" でメモの在りかに移動すれば通常のファイル操作を行えるので、cpmv のような複雑なファイル操作 3 もサブコマンドとしては用意していない。

実装

sidenote は Go で書いた。この手のツールは簡単に使い始められることが重要だと思うので、シングルバイナリで動作する点は大きい。 また、依存関係のメンテナンスを考えずに済むように Go の標準ライブラリだけを使用して書いている。 大したことはしていないので実際のところシェルスクリプトで書いても良かったかもしれないけれど、それはそれで実装が辛いことになっていたかもしれない。

あと、Bash のコマンドライン補完をまともに実装したのはこれが初めてだった。 通常と異なるファイルパスの解釈をするツールなのでファイルパスの補完を行う _filedir がそのまま使えなかったりしてやや苦労したものの、やはり補完の有り無しでコマンドの使い勝手に天と地の差があるので用意してよかったと思う。

  1. 正確には $XDG_CONFIG_HOME/git/ignore 

  2. これは git コマンドが .git を探す際の挙動とだいたい同じ 

  3. 単純そうに見えるが、ちゃんと実装しようとすると考えるべきことが多くて大変