O usuário chama meu script com um caminho de arquivo que será criado ou substituído em algum momento do script, como foo.sh file.txt
ou foo.sh dir/file.txt
.
O comportamento de criação ou substituição é muito parecido com os requisitos para colocar o arquivo no lado direito do >
operador de redirecionamento de saída ou transmiti-lo como argumento para tee
(na verdade, transmiti-lo como argumento para tee
é exatamente o que estou fazendo )
Antes de entrar no âmago do script, quero fazer uma verificação razoável se o arquivo pode ser criado / substituído, mas na verdade não o cria. Essa verificação não precisa ser perfeita, e sim, eu percebo que a situação pode mudar entre a verificação e o ponto em que o arquivo está realmente gravado - mas aqui eu estou bem com a melhor solução de tipo de esforço para que eu possa resgatar cedo no caso em que o caminho do arquivo é inválido.
Exemplos de razões pelas quais o arquivo não pôde ser criado:
- o arquivo contém um componente de diretório, como
dir/file.txt
mas o diretóriodir
não existe - o usuário não tem permissões de gravação no diretório especificado (ou o CWD se nenhum diretório foi especificado
Sim, eu percebo que verificar as permissões "de antemão" não é The UNIX Way ™ ; devo tentar a operação e pedir perdão mais tarde. No meu script específico, no entanto, isso leva a uma experiência ruim do usuário e não posso alterar o componente responsável.
fonte
/foo/bar/file.txt
. Basicamente, passo o caminho paratee
gostar detee $OUT_FILE
ondeOUT_FILE
é passado na linha de comando. Isso deve "apenas funcionar" com caminhos absolutos e relativos, certo?tee -- "$OUT_FILE"
pelo menos. E se o arquivo já existir ou existir, mas não for um arquivo regular (diretório, link simbólico, fifo)?tee "${OUT_FILE}.tmp"
. Se o arquivo já existir,tee
substitua, qual é o comportamento desejado nesse caso. Se for um diretório,tee
falhará (eu acho). symlink não tenho 100% de certeza?Respostas:
O teste óbvio seria:
Mas ele realmente cria o arquivo se ele ainda não estiver lá. Podemos limpar depois de nós mesmos:
Mas isso removeria um arquivo que já existia, o que você provavelmente não deseja.
No entanto, temos uma maneira de contornar isso:
Você não pode ter um diretório com o mesmo nome que outro objeto nesse diretório. Não consigo pensar em uma situação em que você seria capaz de criar um diretório, mas não criar um arquivo. Após esse teste, seu script estará livre para criar um convencional
/path/to/file
e fazer o que bem entender.fonte
if mkdir /var/run/lockdir
como um teste de bloqueio. Isso é atômico, emboraif ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfile
tenha uma pequena fatia de tempo entre o teste etouch
onde outra instância do script em questão pode ser iniciada.Pelo que estou coletando, você deseja verificar se, ao usar
(observe que
--
ou não funcionaria para nomes de arquivos que começam com -),tee
seria possível abrir o arquivo para gravação.Isso é isso:
Por enquanto, ignoraremos sistemas de arquivos como vfat, ntfs ou hfsplus que têm limitações sobre quais valores de bytes os nomes de arquivos podem conter, cota de disco, limite de processo, selinux, apparmor ou outro mecanismo de segurança, como sistema de arquivos completo, sem inode restante, dispositivo arquivos que não podem ser abertos dessa maneira por um motivo ou outro, arquivos executáveis atualmente mapeados em algum espaço de endereço do processo, os quais também podem afetar a capacidade de abrir ou criar o arquivo.
Com
zsh
:Em
bash
ou qualquer shell semelhante a Bourne, basta substituir ocom:
Os
zsh
recursos específicos aqui são$ERRNO
(que expõem o código de erro da última chamada do sistema) e a$errnos[]
matriz associativa para converter nos nomes de macro C padrão correspondentes. E o$var:h
(do csh) e$var:P
(precisa do zsh 5.3 ou superior).O bash ainda não possui recursos equivalentes.
$file:h
pode ser substituído pordir=$(dirname -- "$file"; echo .); dir=${dir%??}
, ou com GNUdirname
:IFS= read -rd '' dir < <(dirname -z -- "$file")
.Para
$errnos[ERRNO] == ENOENT
, uma abordagem poderia ser executadals -Ld
no arquivo e verificar se a mensagem de erro corresponde ao erro ENOENT. Fazer isso de maneira confiável e portável é complicado.Uma abordagem poderia ser:
(supondo que a mensagem de erro termine com a
syserror()
tradução deENOENT
e que essa tradução não inclua a:
) e, em vez de[ -e "$file" ]
:E verifique se há um erro ENOENT com
A
$file:P
parte é a mais difícil de conseguirbash
, especialmente no FreeBSD.O FreeBSD possui um
realpath
comando e umreadlink
comando que aceita uma-f
opção, mas eles não podem ser usados nos casos em que o arquivo é um link simbólico que não resolve. Isso é o mesmo comperl
'sCwd::realpath()
.python
'sos.path.realpath()
parece funcionar de forma semelhante azsh
$file:P
, por isso, assumindo que pelo menos uma versão dopython
está instalado e que há umpython
comando que refere-se a um deles, você poderia fazer (que não é um dado no FreeBSD é):Mas então, é melhor você fazer a coisa toda
python
.Ou você pode decidir não lidar com todos esses casos de canto.
fonte
tee
isso, o requisito é realmente que o arquivo possa ser criado se não existir, ou truncado para zero e substituído se o fizer (ou, no entanto,tee
lida com isso).perl
,awk
,sed
oupython
(ou mesmosh
,bash
...). Script de shell é sobre como usar o melhor comando para a tarefa. Aqui aindaperl
não tem um equivalente azsh
's$var:P
(Cwd::realpath
se comporta como o BSDrealpath
oureadlink -f
enquanto você deseja que se comporte como o GNUreadlink -f
oupython
' sos.path.realpath()
) #Uma opção que você pode considerar é criar o arquivo desde o início, mas apenas preenchê-lo posteriormente no seu script. Você pode usar o
exec
comando para abrir o arquivo em um descritor de arquivo (como 3, 4 etc.) e, posteriormente, usar um redirecionamento para um descritor de arquivo (>&3
etc.) para gravar o conteúdo desse arquivo.Algo como:
Você também pode usar a
trap
para limpar o arquivo com erro, isso é uma prática comum.Dessa forma, você recebe uma verificação real das permissões que poderá criar o arquivo e, ao mesmo tempo, pode executá-lo com antecedência suficiente para que, se isso falhar, você não tenha perdido tempo esperando as verificações longas.
ATUALIZADO: Para evitar atropelar o arquivo no caso de as verificações falharem, use o
fd<>file
redirecionamento do bash que não trunca o arquivo imediatamente. (Não nos preocupamos com a leitura do arquivo, essa é apenas uma solução alternativa, portanto não a truncamos. Anexar com>>
provavelmente funcionaria também, mas eu costumo achar esse um pouco mais elegante, mantendo o sinalizador O_APPEND fora da imagem.)Quando estivermos prontos para substituir o conteúdo, precisamos truncar o arquivo primeiro (caso contrário, se estivermos escrevendo menos bytes do que o existente no arquivo antes, os bytes à direita permanecerão lá.) Podemos usar o truncado (1) comando do coreutils para esse fim, e podemos passar o descritor de arquivo aberto que temos (usando o
/dev/fd/3
pseudo-arquivo) para que ele não precise reabrir o arquivo. (Novamente, tecnicamente algo mais simples: >dir/file.txt
provavelmente funcionaria, mas não ter que reabrir o arquivo é uma solução mais elegante.)fonte
echo -n >>file
outrue >> file
. Obviamente, o ext4 possui arquivos somente anexados, mas você pode conviver com esse falso positivo.my-file.txt.backup
) antes de criar o novo. Outra solução é gravar o novo arquivo em um arquivo temporário na mesma pasta e copiá-lo sobre o arquivo antigo depois que o restante do script for bem-sucedido - se a última operação falhar, o usuário poderá corrigir o problema manualmente sem perder seu progresso.Eu acho que a solução do DopeGhoti é melhor, mas isso também deve funcionar:
A primeira construção if verifica se o argumento é um caminho completo (começa com
/
) e define adir
variável para o caminho do diretório até o último/
. Caso contrário, se o argumento não começar com a,/
mas contiver um/
(especificando um subdiretório), ele será definidodir
como o diretório de trabalho atual + o caminho do subdiretório. Caso contrário, ele assume o diretório de trabalho atual. Em seguida, verifica se esse diretório é gravável.fonte
Que tal usar o
test
comando normal como descrito abaixo?fonte
man test
, e isso[ ]
é equivalente a teste (não é mais um conhecimento comum). Aqui é um one-liner eu estava prestes a uso na minha resposta inferior, para cobrir o caso exato, para a posteridade:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Você mencionou que a experiência do usuário estava direcionando sua pergunta. Vou responder de um ângulo UX, já que você tem boas respostas no lado técnico.
Em vez de executar a verificação antecipadamente, que tal gravar os resultados em um arquivo temporário e, no final, colocar os resultados no arquivo desejado do usuário? Gostar:
O bom dessa abordagem: ela produz a operação desejada no cenário normal de caminho feliz, evita o problema de atomicidade de testar e definir, preserva as permissões do arquivo de destino enquanto cria, se necessário, e é simples de implementar.
Nota: se tivéssemos usado
mv
, estaríamos mantendo as permissões do arquivo temporário - não queremos isso, penso: queremos manter as permissões definidas no arquivo de destino.Agora, o ruim: ele requer o dobro do espaço (
cat .. >
construção), força o usuário a fazer algum trabalho manual se o arquivo de destino não tiver sido gravável no momento em que precisava, e deixa o arquivo temporário por aí (o que pode ter segurança problemas de manutenção).fonte
TL; DR:
Diferentemente de
<> "${userfile}"
outouch "${userfile}"
, isso não fará modificações espúrias nos registros de data e hora do arquivo e também funcionará com arquivos somente para gravação.Do OP:
E do seu comentário à minha resposta da perspectiva do UX :
O único teste confiável é
open(2)
o arquivo, porque somente isso resolve todas as questões sobre a capacidade de gravação: caminho, propriedade, sistema de arquivos, rede, contexto de segurança etc. Qualquer outro teste abordará parte da capacidade de gravação, mas não outras. Se você quiser um subconjunto de testes, terá que escolher o que é importante para você.Mas aqui está outro pensamento. Pelo que entendi:
Você deseja fazer essa pré-verificação por causa do nº 1 e não deseja mexer em um arquivo existente por causa do nº 2. Então, por que você não pede ao shell para abrir o arquivo para anexar, mas na verdade não acrescenta nada?
Sob o capô, isso resolve a questão da gravabilidade exatamente como desejado:
mas sem modificar o conteúdo ou os metadados do arquivo. Agora, sim, esta abordagem:
lsattr
e reagir de acordo.rm
.Embora eu afirme (em minha outra resposta) que a abordagem mais amigável é criar um arquivo temporário que o usuário precisa mover, acho que essa é a abordagem menos hostil ao usuário para avaliar completamente sua entrada.
fonte