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" >&2
はprintf "%s: command not found\n" "$1"
の出力結果をリダイレクトで2
(つまり、標準出力エラー)に出力することを意味してます. なお、&
は>&
でワンセットで、標準出入力へのリダイレクトを意味します. なので>&2
で初めて意味をなし、>
と2
のセットで標準出力を標準エラーにコピーするという意味となります.
なお、>2
だけだと2
という変数に出力結果をリダイレクトするという意味になってしまいます(=カレントディレクトリに2というファイルが生成されてしまいます).
zsh環境でのパッケージのサジェスト機能設定
zshにおけるcommand_not_found_handle
zshではコマンドが無い場合command_not_found_handler
が呼び出される仕組みになっています.
コマンドが存在しない場合の出力を変更したい場合は、.zshrc
でcommand_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
の正体
- プログラムはPythonスクリプト
- CommandNotFoundパッケージはzyga/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
関連ポスト
オンラインマテリアル
(注意:GitHub Accountが必要となります)