El usuario llama a mi script con una ruta de archivo que se creará o se sobrescribirá en algún momento del script, como foo.sh file.txt
o foo.sh dir/file.txt
.
El comportamiento de crear o sobrescribir es muy similar a los requisitos para colocar el archivo en el lado derecho del >
operador de redireccionamiento de salida, o pasarlo como argumento a tee
(de hecho, pasarlo como argumento tee
es exactamente lo que estoy haciendo) )
Antes de entrar en las entrañas de la secuencia de comandos, quiero hacer una verificación razonable si el archivo se puede crear / sobrescribir, pero no crearlo realmente. Esta verificación no tiene que ser perfecta, y sí, me doy cuenta de que la situación puede cambiar entre la verificación y el punto en el que el archivo está realmente escrito, pero aquí estoy de acuerdo con una solución de tipo de mejor esfuerzo para poder rescatarme temprano en el caso de que la ruta del archivo no sea válida.
Ejemplos de razones por las que el archivo no se pudo crear:
- el archivo contiene un componente de directorio,
dir/file.txt
pero el directoriodir
no existe - el usuario no tiene permisos de escritura en el directorio especificado (o el CWD si no se especificó ningún directorio
Sí, me doy cuenta de que verificar los permisos "por adelantado" no es The UNIX Way ™ , sino que debería probar la operación y pedir perdón más tarde. Sin embargo, en mi script en particular, esto conduce a una mala experiencia de usuario y no puedo cambiar el componente responsable.
fuente
/foo/bar/file.txt
. Básicamente paso la ruta a Metee
gustatee $OUT_FILE
dondeOUT_FILE
se pasa en la línea de comando. Eso debería "funcionar" con rutas absolutas y relativas, ¿verdad?tee -- "$OUT_FILE"
al menos necesitarías . ¿Qué pasa si el archivo ya existe o existe pero no es un archivo normal (directorio, enlace simbólico, fifo)?tee "${OUT_FILE}.tmp"
. Si el archivo ya existe, setee
sobrescribe, que es el comportamiento deseado en este caso. Si es un directorio,tee
fallará (creo). enlace simbólico No estoy 100% seguro?Respuestas:
La prueba obvia sería:
Pero en realidad crea el archivo si aún no está allí. Podríamos limpiar después de nosotros mismos:
Pero esto eliminaría un archivo que ya existía, que probablemente no desee.
Sin embargo, tenemos una forma de evitar esto:
No puede tener un directorio con el mismo nombre que otro objeto en ese directorio. No puedo pensar en una situación en la que pueda crear un directorio pero no crear un archivo. Después de esta prueba, su script tendrá libertad para crear un convencional
/path/to/file
y hacer lo que le plazca.fuente
if mkdir /var/run/lockdir
como prueba de bloqueo. Esto es atómico, mientras queif ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfile
tiene una pequeña porción de tiempo entre la prueba ytouch
donde puede comenzar otra instancia del script en cuestión.De lo que estoy reuniendo, quieres verificar eso cuando uses
(tenga en cuenta que
--
o no funcionaría para los nombres de archivo que comienzan con -),tee
podría abrir el archivo para escribir.Eso es eso:
Ignoraremos por ahora los sistemas de archivos como vfat, ntfs o hfsplus que tienen limitaciones sobre los valores de byte que pueden contener los nombres de archivos, la cuota de disco, el límite de proceso, selinux, apparmor u otro mecanismo de seguridad en el camino, sistema de archivos completo, no queda ningún inodo, dispositivo archivos que no se pueden abrir de esa manera por una razón u otra, archivos que son ejecutables actualmente asignados en algún espacio de direcciones de proceso, todo lo cual también podría afectar la capacidad de abrir o crear el archivo.
Con
zsh
:En
bash
o cualquier shell similar a Bourne, simplemente reemplace elcon:
Las
zsh
características específicas aquí son$ERRNO
(que expone el código de error de la última llamada al sistema) y una$errnos[]
matriz asociativa para traducir a los nombres de macro C estándar correspondientes. Y el$var:h
(de csh) y$var:P
(necesita zsh 5.3 o superior).bash aún no tiene características equivalentes.
$file:h
puede ser reemplazado condir=$(dirname -- "$file"; echo .); dir=${dir%??}
, o con GNUdirname
:IFS= read -rd '' dir < <(dirname -z -- "$file")
.Para
$errnos[ERRNO] == ENOENT
, un enfoque podría ser ejecutarls -Ld
en el archivo y verificar si el mensaje de error corresponde al error ENOENT. Sin embargo, hacer eso de manera confiable y portátil es complicado.Un enfoque podría ser:
(suponiendo que el mensaje de error termina con la
syserror()
traducción deENOENT
y que esa traducción no incluye a:
) y luego, en lugar de[ -e "$file" ]
, hacer:Y compruebe si hay un error ENOENT con
La
$file:P
parte es la más difícil de lograrbash
, especialmente en FreeBSD.FreeBSD tiene un
realpath
comando y unreadlink
comando que acepta una-f
opción, pero no se pueden usar en los casos en que el archivo es un enlace simbólico que no se resuelve. Eso es lo mismo conperl
'sCwd::realpath()
.python
'Sos.path.realpath()
no parece funcionar de manera similar azsh
$file:P
, por lo que suponiendo que al menos una versión depython
está instalado y que hay unpython
comando que se refiere a uno de ellos, se puede hacer (que no es un hecho en FreeBSD es):Pero entonces, también podrías hacer todo el asunto
python
.O podría decidir no manejar todos esos casos de esquina.
fuente
tee
lo que el requisito es realmente que el archivo se pueda crear si no existe, o se puede truncar a cero y sobrescribir si lo hace (o de lo contrariotee
maneja eso)perl
,awk
,sed
opython
(o inclusosh
,bash
...). El scripting de Shell se trata de usar el mejor comando para la tarea. Aquí inclusoperl
no tiene un equivalente dezsh
's$var:P
(seCwd::realpath
comporta como BSDrealpath
oreadlink -f
mientras desea que se comporte como GNUreadlink -f
opython
' sos.path.realpath()
)Una opción que quizás desee considerar es crear el archivo desde el principio, pero solo completarlo más adelante en su secuencia de comandos. Puede usar el
exec
comando para abrir el archivo en un descriptor de archivo (como 3, 4, etc.) y luego usar una redirección a un descriptor de archivo (>&3
, etc.) para escribir contenidos en ese archivo.Algo como:
También puede usar a
trap
para limpiar el archivo por error, es una práctica común.De esta manera, obtienes una verificación real de los permisos para poder crear el archivo, al mismo tiempo que puedes realizarlo lo suficientemente temprano como para que si eso falla, no hayas pasado tiempo esperando las largas verificaciones.
ACTUALIZADO: para evitar tropezar con el archivo en caso de que las comprobaciones fallen, utilice la
fd<>file
redirección de bash que no trunca el archivo de inmediato. (No nos importa leer el archivo, esto es solo una solución alternativa, por lo que no lo truncamos. Anexar con>>
probablemente probablemente también funcione, pero tiendo a encontrar este un poco más elegante, manteniendo el indicador O_APPEND fuera de la foto.)Cuando estemos listos para reemplazar el contenido, primero debemos truncar el archivo (de lo contrario, si estamos escribiendo menos bytes de los que había antes en el archivo, los bytes finales permanecerían allí). Podemos usar el truncado (1) comando de coreutils para ese propósito, y podemos pasarle el descriptor de archivo abierto que tenemos (usando el
/dev/fd/3
pseudo-archivo) para que no necesite volver a abrir el archivo. (Nuevamente, técnicamente algo más simple como: >dir/file.txt
probablemente funcionaría, pero no tener que volver a abrir el archivo es una solución más elegante).fuente
echo -n >>file
otrue >> file
. Por supuesto, ext4 tiene archivos de solo agregado, pero podría vivir con ese falso positivo.my-file.txt.backup
) antes de crear el nuevo. Otra solución es escribir el nuevo archivo en un archivo temporal en la misma carpeta, y luego copiarlo sobre el archivo anterior después de que el resto del script tenga éxito --- si esa última operación falla, el usuario puede solucionar el problema manualmente sin perder su progresoCreo que la solución de DopeGhoti es mejor, pero esto también debería funcionar:
La primera construcción if comprueba si el argumento es una ruta completa (comienza con
/
) y establece ladir
variable en la ruta del directorio hasta la última/
. De lo contrario, si el argumento no comienza con a/
pero contiene un/
(especificando un subdirectorio) se establecerádir
en el directorio de trabajo actual + la ruta del subdirectorio. De lo contrario, asume el directorio de trabajo actual. Luego verifica si ese directorio es grabable.fuente
¿Qué pasa con el uso del
test
comando normal como se describe a continuación?fuente
man test
, y eso[ ]
es equivalente a la prueba (ya no es de conocimiento común). Aquí hay una frase que estaba a punto de usar en mi respuesta inferior, para cubrir el caso exacto, para la posteridad:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Usted mencionó que la experiencia del usuario estaba impulsando su pregunta. Contestaré desde un ángulo UX, ya que tienes buenas respuestas en el aspecto técnico.
En lugar de realizar la verificación por adelantado, ¿qué hay de escribir los resultados en un archivo temporal y luego, al final, colocar los resultados en el archivo deseado por el usuario? Me gusta:
Lo bueno de este enfoque: produce la operación deseada en el escenario normal de ruta feliz, evita el problema de atomicidad de prueba y configuración, conserva los permisos del archivo de destino mientras se crea si es necesario, y es muy simple de implementar.
Nota: si lo hubiéramos usado
mv
, estaríamos manteniendo los permisos del archivo temporal; creo que no queremos eso: queremos mantener los permisos tal como están establecidos en el archivo de destino.Ahora, lo malo: requiere el doble de espacio (
cat .. >
construcción), obliga al usuario a hacer un trabajo manual si el archivo de destino no se puede escribir en el momento en que lo necesitaba, y deja el archivo temporal por ahí (lo que podría tener seguridad o problemas de mantenimiento).fuente
TL; DR:
A diferencia de
<> "${userfile}"
otouch "${userfile}"
, esto no hará modificaciones espurias a las marcas de tiempo del archivo y también funcionará con archivos de solo escritura.Desde el OP:
Y de tu comentario a mi respuesta desde una perspectiva UX :
La única prueba confiable es
open(2)
el archivo, porque solo eso resuelve todas las preguntas sobre la capacidad de escritura: ruta, propiedad, sistema de archivos, red, contexto de seguridad, etc. Cualquier otra prueba abordará una parte de la capacidad de escritura, pero no otras. Si desea un subconjunto de pruebas, finalmente tendrá que elegir lo que es importante para usted.Pero aquí hay otro pensamiento. Por lo que entiendo:
Desea hacer esta verificación previa debido al n. ° 1, y no quiere jugar con un archivo existente debido al n. ° 2. Entonces, ¿por qué no le pide al shell que abra el archivo para agregarlo, pero en realidad no agrega nada?
Debajo del capó, esto resuelve la pregunta de escritura exactamente como se quería:
pero sin modificar los contenidos o metadatos del archivo. Ahora, sí, este enfoque:
lsattr
y reaccionar en consecuencia.rm
.Si bien afirmo (en mi otra respuesta) que el enfoque más fácil de usar es crear un archivo temporal que el usuario tenga que mover, creo que este es el enfoque menos hostil para el usuario para examinar completamente su entrada.
fuente