考虑一个在整个主目录中搜索具有错误权限的文件或目录的命令:
$ find $HOME -perm 777
这只是一个例子;该命令可能会列出损坏的符号链接:
$ find $HOME -xtype l
或列出冗长的符号链接:
$ symlinks -s -r $HOME
或任何数量的其他昂贵的命令,将换行符分隔的路径发送到stdout
.
现在,我可以在这样的寻呼机中收集结果:
$ find $HOME -perm 777 | less
然后cd
到不同虚拟终端中的相关目录。但我宁愿有一个脚本为每一行输出打开一个新的交互式 shell,如下所示:
$ find $HOME -perm 777 | visit-paths.sh
这样我可以检查每个文件或目录,检查时间戳,决定是否需要更改权限或删除文件等。
使用从文件或 stdin 读取路径的 bash 脚本是可行的,如下所示:
#! /usr/bin/env bash
set -e
declare -A ALREADY_SEEN
while IFS='' read -u 10 -r line || test -n "$line"
do
if test -d "$line"
then
VISIT_DIR="$line"
elif test -f "$line"
then
VISIT_DIR="$(dirname "$line")"
else
printf "Warning: path does not exist: '%s'\n" "$line" >&2
continue
fi
if test "${ALREADY_SEEN[$VISIT_DIR]}" != '1'
then
( cd "$VISIT_DIR" && $SHELL -i </dev/tty )
ALREADY_SEEN[${VISIT_DIR}]=1
continue
else
# Same as last time, skip it.
continue
fi
done 10< "${*:-/dev/stdin}"
这有一些好处,例如:
一旦出现新的输出行,脚本就会打开一个新的 shell
stdin
。这意味着在我开始做事之前,我不必等待慢速命令完全完成。当我在新生成的 shell 中执行操作时,slow 命令一直在后台运行,因此下一条路径可能在我完成时准备好访问。
如有必要,我可以提前退出循环,例如使用
false; exit
Ctrl-C Ctrl-D。该脚本处理文件名和目录。
该脚本避免连续两次导航到同一目录。(感谢@MichaelHomer 解释了如何使用关联数组来做到这一点。)
但是,这个脚本有一个问题:
- 如果最后一个命令具有非零状态,则整个管道退出,这对于提前退出很有用,但通常需要
$?
每次检查以防止意外提前退出。
为了尝试解决这个问题,我编写了一个 Python 脚本:
#! /usr/bin/env python3
import argparse
import logging
import os
import subprocess
import sys
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Visit files from file or stdin.'
)
parser.add_argument(
'-v',
'--verbose',
help='More verbose logging',
dest="loglevel",
default=logging.WARNING,
action="store_const",
const=logging.INFO,
)
parser.add_argument(
'-d',
'--debug',
help='Enable debugging logs',
action="store_const",
dest="loglevel",
const=logging.DEBUG,
)
parser.add_argument(
'infile',
nargs='?',
type=argparse.FileType('r'),
default=sys.stdin,
help='Input file (or stdin)',
)
args = parser.parse_args()
logging.basicConfig(level=args.loglevel)
shell_bin = os.environ['SHELL']
logging.debug("SHELL = '{}'".format(shell_bin))
already_visited = set()
n_visits = 0
n_skipped = 0
for i, line in enumerate(args.infile):
visit_dir = None
candidate = line.rstrip()
logging.debug("candidate = '{}'".format(candidate))
if os.path.isdir(candidate):
visit_dir = candidate
elif os.path.isfile(candidate):
visit_dir = os.path.dirname(candidate)
else:
logging.warning("does not exist: '{}'".format(candidate))
n_skipped +=1
continue
if visit_dir is not None:
real_dir = os.path.realpath(visit_dir)
else:
# Should not happen.
logging.warning("could not determine directory for path: '{}'".format(candidate))
n_skipped +=1
continue
if visit_dir in already_visited:
logging.info("already visited: '{}'".format(visit_dir))
n_skipped +=1
continue
elif real_dir in already_visited:
logging.info("already visited: '{}' -> '{}'".format(visit_dir, real_dir))
n_skipped +=1
continue
if i != 0:
try :
response = input("#{}. Continue? (y/n) ".format(n_visits + 1))
except EOFError:
sys.stdout.write('\n')
break
if response in ["n", "no"]:
break
logging.info("spawning '{}' in '{}'".format(shell_bin, visit_dir))
run_args = [shell_bin, "-i"]
subprocess.call(run_args, cwd=visit_dir, stdin=open('/dev/tty'))
already_visited.add(visit_dir)
already_visited.add(real_dir)
n_visits +=1
logging.info("# paths received: {}".format(i + 1))
logging.info("distinct directories visited: {}".format(n_visits))
logging.info("paths skipped: {}".format(n_skipped))
但是,我在将Continue? (y/n)
提示的回复传递给生成的 shell 时遇到了一些问题,导致类似y: command not found
. 我怀疑问题出在这条线上:
subprocess.call(run_args, cwd=visit_dir, stdin=open('/dev/tty'))
stdin
使用时我需要做一些不同的事情subprocess.call
吗?
或者,是否有一个广泛可用的工具可以使两个脚本变得多余,而我只是没有听说过?
您的 Bash 脚本似乎正在按预期执行所有操作,它只需要
|| break
在生成交互式 shell 的子 shell 之后出现:这样,当您从该交互式 shell 退出并引发错误时,例如Ctrl+C紧跟 aCtrl+D或exit 1
命令,您退出从整个管道早期。当然,正如您所指出的,当您从交互式 shell 使用的最后一个命令以(不需要的)错误退出时,它也会退出,但是您可以通过在任何正常 exit
:
之前发出简单的 as last 命令来轻松规避这种情况,或者也许(作为一个可能更好的解决方案)通过测试作为退出整个管道的唯一可接受的方式,即在产生交互式外壳的子外壳之后使用(而不是仅仅)。Ctrl+C|| { [ $? -eq 130 ] && break; }
|| break
作为一种根本不需要关联数组的更简单的方法,您可能只是-
uniq
输出find
如下所示:当然,这需要一个产生连续重复(如果有的话)的名称源,就像这样
find
做一样。或者您可以使用sort -u
而不是重新排序它们uniq
,但是您必须等待sort
完成,然后才能看到第一个交互式外壳生成,这是您似乎不希望的壮举。然后让我们看看 Python 脚本方法。
你没有说你是如何调用它的,但是如果你通过管道使用它,如下所示:
那么您将 stdin 用于两个相互冲突的目的:输入名称和输入 Python
input()
函数。然后,您可能想要调用您的 Python 脚本,例如:
请注意上面示例中所做的重定向:我们首先将刚刚创建的管道(在管道的那一部分中将是标准输入)重定向到任意文件描述符 3,然后将标准输入重新打开到 tty 以便 Python 脚本可以使用它的
input()
功能。然后通过 Python 脚本的参数将文件描述符 3 用作名称的来源。您还可以考虑以下概念验证:
上面的示例使用了相同的重定向技巧。因此,您可以将它用于您自己的 Bash 脚本,该脚本将看到的路径缓存在关联数组中,并在每个新看到的路径上生成一个交互式 shell。
作为后续,python脚本可以这样修复:
有关的:
https://stackoverflow.com/questions/5986544/cannot-launch-interactive-program-while-piping-to-script-in-python
https://stackoverflow.com/questions/7141331/pipe-input-to-python-program-and-later-get-input-from-user
https://stackoverflow.com/questions/8034595/python-raw-input-following-sys-stdin-read-throws-eoferror
https://stackoverflow.com/questions/40270252/eoferror-when-using-input-after-using-sys-stdin-buffer-read
https://bugs.python.org/issue512981
https://bugs.python.org/issue29396