というツイートを見かけました。
以前、勉強がてらに
を作ったことがありましたが、もうあれから3年も経つんだなとコードを見返しながら感慨に浸っていました。
そんなとき、ふと which コマンドを実行したところ、コマンドラインオプションがあることに気付きました!
$ which
usage: which [-as] program ...
$ man which
WHICH(1) BSD General Commands Manual WHICH(1)
NAME
which -- locate a program file in the user's path
SYNOPSIS
which [-as] program ...
DESCRIPTION
The which utility takes a list of command names and searches the path for each
executable file that would be run had these commands actually been invoked.
The following options are available:
-a List all instances of executables found (instead of just the first one of each).
-s No output, just return 0 if any of the executables are found, or 1 if none are found.
Some shells may provide a builtin which command which is similar or identical
to this utility. Consult the builtin(1) manual page.
これは、、、
。。。
過去の私の実装に足りないものを発見してしまいました、もう再実装するしかない! (> <)
ということで、コマンドラインオプションを追加するだけなのですが、結構難しくて悩んでしまいました。
import glob
import os
import sys
import argparse
from itertools import chain
from os.path import join as pathjoin
from operator import itemgetter
def search(cmd, paths, is_all=False):
for path in paths:
for match in glob.glob(pathjoin(path, cmd)):
if os.access(match, os.X_OK):
yield match
if not is_all:
raise StopIteration
def parse_argument(args=None):
parser = argparse.ArgumentParser()
parser.set_defaults(is_all=False, is_silent=False, commands=[])
parser.add_argument("-a", dest="is_all", action="store_true",
help="List all instances of executables found "
"(instead of just the first one of each).")
parser.add_argument("-s", dest="is_silent", action="store_true",
help="No output, just return 0 if any of the executables are found, "
"or 1 if none are found.")
parser.add_argument("commands", nargs="*")
args = parser.parse_args(args or sys.argv[1:])
return args
def main(cmd_args=None):
args = parse_argument(cmd_args)
env_paths = os.environ['PATH'].split(':')
result = []
for cmd in args.commands:
founds = list(search(cmd, env_paths, args.is_all))
result.append((0, founds) if founds else (1, [cmd]))
status_code = max(map(itemgetter(0), result))
if not args.is_silent:
cmd_paths = [paths for ret_val, paths in result if ret_val == 0]
for cmd_path in chain.from_iterable(cmd_paths):
print cmd_path
return status_code
if __name__ == '__main__':
sys.exit(main())
実行結果はこんな感じです。
$ python which.py -a ls vi unknown
/bin/ls
/opt/local/bin/vi
/usr/bin/vi
$ python which.py -s ls vi unknown; echo $?
1
ついでに本当に which コマンドと動作が一致しているか、簡単なテストを書いてみました。
import sys
from subprocess import Popen, PIPE
import pytest
import which
FILESYSTEM_ENCODING = sys.getfilesystemencoding()
def get_system_which_result(args):
cmds = ["which"]
cmds.extend(args)
p = Popen(" ".join(cmds), stdout=PIPE, stderr=PIPE, shell=True)
out, err = p.communicate()
return p.returncode, out, err
@pytest.mark.parametrize("args", [
["ls"],
["cd", "pwd"],
["non_existence"],
["-a", "vi"],
["-a", "ls", "vi"],
["-s", "non_existence"],
["-a", "-s", "ls", "vi"],
["-a", "-s", "ls", "vi", "non_existence"],
])
def test_which_command(args, capsys):
my_ret = which.main(args)
my_out, my_err = capsys.readouterr()
sys_ret, sys_out, sys_err = get_system_which_result(args)
assert sys_ret == my_ret
assert sys_out == my_out.encode(FILESYSTEM_ENCODING)
assert sys_err == my_err.encode(FILESYSTEM_ENCODING)
ソースは以下にあります。
こういったシンプルなツールを作るのはおもしろいですね。