🌊

Nushellのカスタムエラー・イベント処理など

2022/12/09に公開

カスタムエラー処理

Nushellではエラーメッセージ情報が豊富で、誤りがどこにあるかわかりやすく表示されます。
例えば、ストリームデータの型チェックを行っており、下記のようにどこのコマンドでどのようなエラーが発生したかわかりやすく表示されます。

❯ ls | decode utf8
Error: nu::shell::unsupported_input (link)

  × Unsupported input
   ╭─[entry #23:1:1]
 1 │ ls | decode utf8
   ·      ───┬──
   ·         ╰── non-binary input
   ╰────

これをカスタムコマンドで、エラー処理を実行するにはerror makeコマンドでカスタムエラーを作成できます。
下記はカスタムコマンドで引数チェックでカスタムエラーを表示したものです。ここでは引数のエラー箇所が表示されています。これを実現するため、metadataコマンドを使いストリームデータ内の情報を取得します。

❯ s1 my-command 101
Error: 
  × なんてこった
   ╭─[entry #22:1:1]
 1 │ s1 my-command 101
   ·               ─┬─
   ·                ╰── この引数が100を超えたぞ
   ╰────

具体的には、下記コードのようにまず引数からmetadataコマンドでエラーメッセージで表示されるエラー箇所に表示すべき範囲を取得します。

# s1.nu
export def my-command [x:int] {
	if $x > 100 {
		let span = (metadata $x).span;
		error make {
			msg: "なんてこった",
			label: {
				text: "この引数が100を超えたぞ",
				start: $span.start,
				end: $span.end
			}
		}
	}
 }

範囲取得は下記部分で行い、error makeコマンドへ渡しています。
let span = (metadata $x).span;

カスタムイベント

カスタムイベントを定義すると、補完時などで独自処理を行うことができます。下記はCtr-tでカレントディレクトリ以下のディレクトリ一覧をfzfに渡して、その選択結果をcdの引数にするようにしたものです。

{
      name: change_dir_with_fzf
      modifier: CONTROL
      keycode: Char_t
      mode: emacs
      event:[
          { edit: Clear }
          { edit: InsertString,
            value: "cd (ls | where type == dir | each { |it| $it.name} | str join (char nl) | fzf | decode utf-8 | str trim)"

          }
          { send: Enter }
        ]
    }

上記のkeywordを下記で説明します。

  • name: $config.keybindings で簡単に参照できる、キーバインディングの一意の名前
  • modifier: キーバインドのキー修飾子。オプションは次のとおりです。
    none
    control
    alt
    shift
    control | alt
    control | alt | shift
  • keycode: これは、押されるキーを表します
コントロールキー使用のイベント一覧

すでにキーアサインのほとんどが使用済みですね。

❯ keybindings default | where modifier == CONTROL && code =~ 'Char\(\'\w\'\)' | reject mode
╭────┬──────────┬───────────┬──────────────────────────────────────────────────────────╮
│  # │ modifier │   code    │                          event                           │
├────┼──────────┼───────────┼──────────────────────────────────────────────────────────┤
│  1 │ CONTROL  │ Char('a') │ Edit([MoveToLineStart])                                  │
│  2 │ CONTROL  │ Char('b') │ UntilFound([MenuLeft, Left])                             │
│  3 │ CONTROL  │ Char('c') │ CtrlC                                                    │
│  4 │ CONTROL  │ Char('d') │ CtrlD                                                    │
│  5 │ CONTROL  │ Char('e') │ UntilFound([HistoryHintComplete, Edit([MoveToLineEnd])]) │
│  6 │ CONTROL  │ Char('f') │ UntilFound([HistoryHintComplete, MenuRight, Right])      │
│  7 │ CONTROL  │ Char('g') │ Edit([Redo])                                             │
│  8 │ CONTROL  │ Char('h') │ Edit([Backspace])                                        │
│  9 │ CONTROL  │ Char('k') │ Edit([CutToEnd])                                         │
│ 10 │ CONTROL  │ Char('l') │ ClearScreen                                              │
│ 11 │ CONTROL  │ Char('n') │ UntilFound([MenuDown, Down])                             │
│ 12 │ CONTROL  │ Char('o') │ OpenEditor                                               │
│ 13 │ CONTROL  │ Char('p') │ UntilFound([MenuUp, Up])                                 │
│ 14 │ CONTROL  │ Char('r') │ SearchHistory                                            │
│ 15 │ CONTROL  │ Char('t') │ Edit([SwapGraphemes])                                    │
│ 16 │ CONTROL  │ Char('u') │ Edit([CutFromStart])                                     │
│ 17 │ CONTROL  │ Char('w') │ Edit([CutWordLeft])                                      │
│ 18 │ CONTROL  │ Char('y') │ Edit([PasteCutBufferBefore])                             │
│ 19 │ CONTROL  │ Char('z') │ Edit([Undo])                                             │
│ 20 │ CONTROL  │ Char('a') │ Edit([MoveToLineStart])                                  │
│ 21 │ CONTROL  │ Char('c') │ CtrlC                                                    │
│ 22 │ CONTROL  │ Char('d') │ CtrlD                                                    │
│ 23 │ CONTROL  │ Char('e') │ UntilFound([HistoryHintComplete, Edit([MoveToLineEnd])]) │
│ 24 │ CONTROL  │ Char('l') │ ClearScreen                                              │
│ 25 │ CONTROL  │ Char('n') │ UntilFound([MenuDown, Down])                             │
│ 26 │ CONTROL  │ Char('o') │ OpenEditor                                               │
│ 27 │ CONTROL  │ Char('p') │ UntilFound([MenuUp, Up])                                 │
│ 28 │ CONTROL  │ Char('r') │ SearchHistory                                            │
│ 29 │ CONTROL  │ Char('a') │ Edit([MoveToLineStart])                                  │
│ 30 │ CONTROL  │ Char('c') │ CtrlC                                                    │
│ 31 │ CONTROL  │ Char('d') │ CtrlD                                                    │
│ 32 │ CONTROL  │ Char('e') │ UntilFound([HistoryHintComplete, Edit([MoveToLineEnd])]) │
│ 33 │ CONTROL  │ Char('h') │ Edit([Backspace])                                        │
│ 34 │ CONTROL  │ Char('l') │ ClearScreen                                              │
│ 35 │ CONTROL  │ Char('n') │ UntilFound([MenuDown, Down])                             │
│ 36 │ CONTROL  │ Char('o') │ OpenEditor                                               │
│ 37 │ CONTROL  │ Char('p') │ UntilFound([MenuUp, Up])                                 │
│ 38 │ CONTROL  │ Char('r') │ SearchHistory                                            │
│ 39 │ CONTROL  │ Char('w') │ Edit([BackspaceWord])                                    │
├────┼──────────┼───────────┼──────────────────────────────────────────────────────────┤
│  # │ modifier │   code    │                          event                           │
╰────┴──────────┴───────────┴──────────────────────────────────────────────────────────╯
  • mode: emacs、vi_insert、vi_normal (単一の文字列またはリスト。例: [vi_insert vi_normal])
  • event: キーバインドによって送信されるイベントのタイプ。オプションは次のとおりです。
    send
    edit
    until

次のキーバインドは、エンジンに送信される一連のイベントの例です。最初にプロンプ​​トをクリアし、文字列を挿入してからその値を入力します

    {
      name: change_dir_with_fzf
      modifier: CONTROL
      keycode: Char_t
      mode: emacs
      event:[
          { edit: Clear }
          { edit: InsertString,
            value: "cd (ls | where type == dir | each { |it| $it.name} | str join (char nl) | fzf | decode utf-8 | str trim)"

          }
          { send: Enter }
        ]
    }

以前のキーバインドの欠点の 1 つは、挿入されたテキストがバリデーターによって処理され、履歴に保存されるため、キーバインドが少し遅くなり、コマンド履歴に同じコマンドが入力されることです。そのため、executehostcommand タイプのイベントがあります。次の例は前の例と同じことをより簡単な方法で行い、単一のイベントをエンジンに送信します。

    {
      name: change_dir_with_fzf
      modifier: CONTROL
      keycode: Char_y
      mode: emacs
      event: {
        send: executehostcommand,
        cmd: "cd (ls | where type == dir | each { |it| $it.name} | str join (char nl) | fzf | decode utf-8 | str trim)"
      }
    }

par-each

eachのブロックをを並列実行するものです、複数スレッドで並行動作させるため高速化が望めます。
実例をいかに示します。CPU性能やディレクトリ数など環境の影響はあるものの、高速に実行できることがわかります。

❯ benchmark {ls | where type == dir | each { |it|
∙     { name: $it.name, len: (ls $it.name | length) }
∙ }
∙ }
90ms 656µs 150ns
❯ benchmark {ls | where type == dir | par-each { |it|
∙     { name: $it.name, len: (ls $it.name | length) }
∙ }
∙ }
11ms 593µs 827ns

Shells in shells

1 つのディレクトリで作業するのが一般的ですが、同時に複数の場所で作業すると便利な場合があります。このために、Nushell はシェルの概念を提供します。名前が示すように、複数のシェルで、作業ディレクトリなどの間をすばやく移動できます。pushd, popdのようなものを実現します。

開始するには、enterコマンドにディレクトリを入力しましょう。

 /home/jonathant/Source/nushell(main)> enter ../book
 /home/jonathant/Source/book(main)> ls
 ────┬────────────────────┬──────┬────────┬─────────────
  #  │ name               │ type │ size   │ modified
 ────┼────────────────────┼──────┼────────┼─────────────
   0 │ 404.html           │ File │  429 B │ 2 hours ago
   1 │ CONTRIBUTING.md    │ File │  955 B │ 2 hours ago
   2 │ Gemfile            │ File │ 1.1 KB │ 2 hours ago
   3 │ Gemfile.lock       │ File │ 6.9 KB │ 2 hours ago 

コマンド実行後の状態は、ディレクトリの変更に似ています。ディレクトリを変更する代わりに、ディレクトリにジャンプしてその中で作業することができます。 shellsコマンドを使用して、有効なシェルの現在のディレクトリを一覧表示できます。

 /home/jonathan/Source/book(main)> shells
 ───┬────────┬────────────┬─────────────────────────
  # │ active │    name    │          path
 ───┼────────┼────────────┼─────────────────────────
  0 │ false  │ filesystem │ /home/jt/Source/nushell
  1 │ true   │ filesystem │ /home/jt/Source/book
  2 │ false  │ filesystem │ /home/jt/Source/music
 ───┴────────┴────────────┴─────────────────────────

shells コマンドは、現在アクティブな 3 つのシェルがあることを示しています。元の「nushell」ソース ディレクトリと、この新しい「book」ディレクトリです。

npg のショートカット ("next"、"previous"、"goto" の略) コマンドを使用して、これらのシェル間(ディレクトリ)をジャンプできます。

 /home/jonathant/Source/book(main)> n
 /home/jonathant/Source/nushell(main)> p
 /home/jonathant/Source/book(main)> g 2
 /home/jonathant/Source/music(main)>

ディレクトリが変更されていることがわかりますが、作業していた以前のディレクトリにいつでも戻ることができます。これにより、同じセッションで複数のディレクトリで作業できます。

exitコマンドを使用して、入力したシェルを終了できます。これが最後に開いたシェルである場合、Nu は終了します。

--now フラグを終了コマンドに渡すことで、複数のシェルがアクティブであっても、いつでも Nuを終了できます。

Discussion