Linux基礎知識:bash環境でのパッケージのサジェスト機能

command-not-foundの実装の確認

公開日: 2022-02-06
更新日: 2023-01-26

  Table of Contents

コマンドがないときのパッケージのサジェスト

bashでは、ユーザーが入力したコマンドが存在しない場合, エラーを表示するだけでなく、必要となりそうなパッケージを推測, 提案してくれる機能があります.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ sl

Command 'sl' not found, but can be installed with:

sudo apt install sl

$ chinko
chinko: command not found

$ unko

Command 'unko' not found, did you mean:

  command 'nuko' from snap nuko (0.2.1)

See 'snap info <snapname>' for additional versions.

なお、zshでも同様の機能をもちいることができますが、.zshrcでちょっとした設定(後述)をしない限り以下のような出力になります(こちらのほうがシンプルで好き):

1
2
3
4
5
6
% sl
zsh: command not found: sl
% chinko
zsh: command not found: chinko
% unko
zsh: command not found: unko

パッケージのサジェストの仕組み

Bashには検索パスにコマンドが見つからない場合、command_not_found_handle関数を呼び出すという仕組みが存在します. 関数が存在する場合、元コマンドを引数にcommand-not-foundという関数を回して、関連パッケージの検索とサジェストを実行します. 関数が定義されていない場合は、エラーメッセージの出力と終了ステータス127を返す仕組みとなっています.

Ubuntuのbashパッケージが提供する/etc/bash.bashrcには最初から,command-not-foundがインストール済みならcommand_not_found_handleを定義するスクリプトが組み込まれています. 実際に確認してみると

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# if the command-not-found package is installed, use it
if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found/command-not-found ]; then
	function command_not_found_handle {
	        # check because c-n-f could've been removed in the meantime
                if [ -x /usr/lib/command-not-found ]; then
		   /usr/lib/command-not-found -- "$1"
                   return $?
                elif [ -x /usr/share/command-not-found/command-not-found ]; then
		   /usr/share/command-not-found/command-not-found -- "$1"
                   return $?
		else
		   printf "%s: command not found\n" "$1" >&2
		   return 127
		fi
	}
fi

/usr/lib/command-not-foundがパッケージサジェストの根幹をなしていることがわかります.

if条件の中に表れる-x, -oの意味はなにか?

-xはファイルテスト演算子、-oはブール演算子のことを指しています. 代表的なものは以下:

if [ -f path ]; then パスがファイルならばtrue
if [ -d path ]; then パスがディレクトリならばtrue
if [ -w file ]; then ファイルがwritableならばtrue
if [ -r file ]; then ファイルがreadableならばtrue
if [ -x file ]; then ファイルが実行可能ならばtrue
if test [ ... ] -a [ ... ]; then 2つの条件をANDでつなぐ
if test [ ... ] -o [ ... ]; then 2つの条件をORでつなぐ

これを踏まえると、

1
2
if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found/command-not-found]; then
    ...

/usr/lib/command-not-foundまたは/usr/share/command-not-found/command-not-foundのいずれかが実行可能ならば、という意味になります.

printf "%s: command not found\n" "$1" >&2は何をやっているのか?

まず「標準入出力」の概念を確認します. プロセスが起動すると、データストリームの出入り口が3つ与えられます. これをそれぞれ「標準入力」「標準出力」「標準出力エラー」といいます. これら出入り口には番号が与えられており、

0 標準入力
1 標準出力
2 標準出力エラー

printf "%s: command not found\n" "$1" >&2printf "%s: command not found\n" "$1"の出力結果をリダイレクトで2(つまり、標準出力エラー)に出力することを意味してます. なお、&>&でワンセットで、標準出入力へのリダイレクトを意味します. なので>&2で初めて意味をなし、>2のセットで標準出力を標準エラーにコピーするという意味となります.

なお、>2だけだと2という変数に出力結果をリダイレクトするという意味になってしまいます(=カレントディレクトリに2というファイルが生成されてしまいます).

zsh環境でのパッケージのサジェスト機能設定

zshにおけるcommand_not_found_handle

zshではコマンドが無い場合command_not_found_handlerが呼び出される仕組みになっています. コマンドが存在しない場合の出力を変更したい場合は、.zshrccommand_not_found_handlerを定義します.

1
2
3
4
function command_not_found_handler() {
  echo "$1; not found";
  apt moo;
}

.zshrcに書き込んでslという存在しないコマンドを打ち込むと

1
2
3
4
5
6
7
8
9
% sl
sl; not found
                 (__) 
                 (oo) 
           /------\/ 
          / |    ||   
         *  /\---/\ 
            ~~   ~~   
..."Have you mooed today?"...

.zshrcでの設定

以下の関数を.zshrcに書き込めばzsh環境でもパッケージサジェスト機能を利用することが出来ます.

1
2
3
4
5
6
7
8
9
10
11
12
13
function command_not_found_handler() {
  # check because c-n-f could've been removed in the meantime
  if [ -x /usr/lib/command-not-found ]; then
	   /usr/lib/command-not-found -- "$1"
     return $?
  elif [ -x /usr/share/command-not-found/command-not-found ]; then
	   /usr/share/command-not-found/command-not-found -- "$1"
     return $?
	else
		 printf "%s: command not found\n" "$1" >&2
		 return 127
	fi
}

Appendix: /usr/lib/command-not-foundの正体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/python3
# (c) Zygmunt Krynicki 2005, 2006, 2007, 2008
# Licensed under GPL, see COPYING for the whole text

from __future__ import absolute_import, print_function


__version__ = "0.3"
BUG_REPORT_URL = "https://bugs.launchpad.net/command-not-found/+filebug"

try:
    import sys
    if sys.path and sys.path[0] == '/usr/lib':
        # Avoid ImportError noise due to odd installation location.
        sys.path.pop(0)
    if sys.version < '3':
        # We might end up being executed with Python 2 due to an old
        # /etc/bash.bashrc.
        import os
        if "COMMAND_NOT_FOUND_FORCE_PYTHON2" not in os.environ:
            os.execvp("/usr/bin/python3", [sys.argv[0]] + sys.argv)

    import gettext
    import locale
    from optparse import OptionParser

    from CommandNotFound.util import crash_guard
    from CommandNotFound import CommandNotFound
except KeyboardInterrupt:
    import sys
    sys.exit(127)


def enable_i18n():
    cnf = gettext.translation("command-not-found", fallback=True)
    kwargs = {}
    if sys.version < '3':
        kwargs["unicode"] = True
    cnf.install(**kwargs)
    try:
        locale.setlocale(locale.LC_ALL, '')
    except locale.Error:
        locale.setlocale(locale.LC_ALL, 'C')


def fix_sys_argv(encoding=None):
    """
    Fix sys.argv to have only unicode strings, not binary strings.
    This is required by various places where such argument might be
    automatically coerced to unicode string for formatting
    """
    if encoding is None:
        encoding = locale.getpreferredencoding()
    sys.argv = [arg.decode(encoding) for arg in sys.argv]


class LocaleOptionParser(OptionParser):
    """
    OptionParser is broken as its implementation of _get_encoding() uses
    sys.getdefaultencoding() which is ascii, what it should be using is
    locale.getpreferredencoding() which returns value based on LC_CTYPE (most
    likely) and allows for UTF-8 encoding to be used.
    """
    def _get_encoding(self, file):
        encoding = getattr(file, "encoding", None)
        if not encoding:
            encoding = locale.getpreferredencoding()
        return encoding


def main():
    enable_i18n()
    if sys.version < '3':
        fix_sys_argv()
    parser = LocaleOptionParser(
        version=__version__,
        usage=_("%prog [options] <command-name>"))
    parser.add_option('-d', '--data-dir', action='store',
                      default="/usr/share/command-not-found",
                      help=_("use this path to locate data fields"))
    parser.add_option('--ignore-installed', '--ignore-installed',
                      action='store_true',  default=False,
                      help=_("ignore local binaries and display the available packages"))
    parser.add_option('--no-failure-msg',
                      action='store_true', default=False,
                      help=_("don't print '<command-name>: command not found'"))
    (options, args) = parser.parse_args()
    if len(args) == 1:
        try:
            cnf = CommandNotFound.CommandNotFound(options.data_dir)
        except FileNotFoundError:
            print(_("Could not find command-not-found database. Run 'sudo apt update' to populate it."), file=sys.stderr)
            print(_("%s: command not found") % args[0], file=sys.stderr)
            return
        if not cnf.advise(args[0], options.ignore_installed) and not options.no_failure_msg:
            print(_("%s: command not found") % args[0], file=sys.stderr)

if __name__ == "__main__":
    crash_guard(main, BUG_REPORT_URL, __version__)

Appendix: bashの終了ステータス

コマンド終了時には「終了ステータス(exit-status)」と呼ばれるコマンドの成否を表す数値が特殊変数 $? に自動で設定されます. bashの終了ステータスの範囲は 0 から 255 で、0 は正常終了、それ以外は異常終了です. 予約済みの終了ステータス一覧は以下、

終了ステータス 意味
1 一般的なエラー
2 ビルトインコマンドの誤用
126 コマンドを実行できなかった(実行権限がなかった)
127 コマンドが見つからなかった
128 exit に不正な値を渡した(例えば浮動小数点数)
128+n シグナル n で終了
255 範囲外の終了ステータス

References

関連ポスト

オンラインマテリアル



Share Buttons
Share on:

Feature Tags
Leave a Comment
(注意:GitHub Accountが必要となります)