Considere um comando que pesquisa em todo o diretório inicial um arquivo ou diretório com as permissões erradas:
$ find $HOME -perm 777
Este é apenas um exemplo; o comando pode estar listando links simbólicos quebrados:
$ find $HOME -xtype l
ou listando links simbólicos longos:
$ symlinks -s -r $HOME
ou qualquer número de outros comandos caros que enviam caminhos delimitados por nova linha para stdout
.
Agora, eu poderia reunir os resultados em um pager como este:
$ find $HOME -perm 777 | less
e, em seguida, cd
para os diretórios relevantes em um terminal virtual diferente. Mas eu prefiro ter um script que abra um novo shell interativo para cada linha de saída, assim:
$ find $HOME -perm 777 | visit-paths.sh
Dessa forma, posso, por exemplo, inspecionar cada arquivo ou diretório, verificar o carimbo de data/hora, decidir se preciso alterar as permissões ou excluir arquivos, etc.
É possível com um script bash que lê caminhos de um arquivo ou de stdin , assim:
#! /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}"
Isso tem alguns pontos positivos, como:
O script abre um novo shell assim que uma nova linha de saída aparece no
stdin
. Isso significa que não preciso esperar que o comando slow termine completamente antes de começar a fazer as coisas.O comando slow continua sendo executado em segundo plano enquanto estou fazendo coisas no shell recém-gerado, então o próximo caminho está potencialmente pronto para ser visitado quando eu terminar.
Eu posso sair do loop mais cedo, se necessário, com, por exemplo
false; exit
, ou apenas Ctrl-C Ctrl-D.O script lida com nomes de arquivos e diretórios.
O script evita navegar para o mesmo diretório duas vezes seguidas. (Obrigado a @MichaelHomer por explicar como fazer isso com arrays associativos.)
No entanto, há um problema com este script:
- Todo o pipeline é encerrado se o último comando tiver um status diferente de zero, o que é útil para sair antecipadamente, mas em geral requer verificação a
$?
cada vez para evitar uma saída antecipada acidental.
Para tentar resolver esse problema, escrevi um script 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))
No entanto, estou tendo alguns problemas com as respostas ao Continue? (y/n)
prompt sendo passado para o shell que é gerado, causando erros como y: command not found
. Suspeito que o problema esteja nesta linha:
subprocess.call(run_args, cwd=visit_dir, stdin=open('/dev/tty'))
Preciso fazer algo diferente com o stdin
ao usar subprocess.call
?
Como alternativa, existe uma ferramenta amplamente disponível que torna os dois scripts redundantes da qual eu não ouvi falar?
Seu script Bash parece estar fazendo tudo como pretendido, ele só precisa de um
|| break
após o subshell que gera o shell interativo: dessa forma, quando você sai desse shell interativo com um erro induzido como Ctrl+Cimediatamente seguido por um Ctrl+Dou umexit 1
comando, você sai cedo de todo o pipeline.Isso, é claro, como você observou, fará com que ele saia também quando o último comando que você usou do shell interativo sair com um erro (indesejado), mas você pode facilmente contornar isso emitindo um simples
:
como último comando antes de qualquer saída normal , ou talvez (como uma solução possivelmente melhor) testando Ctrl+Ccomo a única maneira aceita de encerrar todo o pipeline, ou seja, usando|| { [ $? -eq 130 ] && break; }
(em vez de apenas|| break
) após o subshell que gera o shell interativo.Como uma abordagem muito mais simples que não requer matrizes associativas, você pode apenas
uniq
-ing a saídafind
como em:Claro que isso requer uma fonte de nomes que produza duplicatas consecutivas (quando houver), como
find
faz. Ou você pode reordená-los usandosort -u
em vez deuniq
, mas então você teria que esperar osort
término, antes de ver o primeiro spawn de shell interativo, o que é um feito que você parece não desejar.Vamos então ver a abordagem do script Python.
Você não diz como está invocando, mas se estiver usando através de um pipe como em:
então você está usando stdin para dois propósitos conflitantes: entrada para nomes e entrada para a
input()
função do seu Python.Você pode então querer invocar seu script Python como em:
Observe os redirecionamentos feitos no exemplo acima: primeiro redirecionamos o pipe recém-criado (que será stdin nessa parte do pipeline) para o descritor de arquivo arbitrário 3 e, em seguida, reabrimos stdin no tty para que o script Python possa usar isso por sua
input()
função. O descritor de arquivo 3 é então usado como fonte de nomes por meio do argumento do seu script Python.Você também pode considerar a seguinte prova de conceito:
O exemplo acima usa o mesmo truque de redirecionamento. Você pode, portanto, usá-lo para seu próprio script Bash, aquele que armazena em cache os caminhos vistos em matrizes associativas e gera um shell interativo em cada caminho recém-visto.
Apenas como acompanhamento, o script python pode ser corrigido assim:
Relacionado:
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