В Linux при работе в пользовательском пространстве используется стек пользовательского пространства. Если процесс выполняется в пространстве ядра, он использует стек ядра, «принадлежащий» процессу. Прерывания обрабатываются внутри ядра.
Вероятно, лучшее место, чтобы узнать такие подробности о частных частях вашего любимого ядра, - это покопаться в сайтах, обслуживающих людей, программирующих его, например, kernelnewbies или выполните поиск на «страницах ядра» LWN для Linux. Вы должны найти похожие места для BSD, Solaris и даже MacOS. Получить информацию о Windows может быть труднее ...
Такая информация не обсуждается в типичных текстах по операционной системе, вам придется искать описания для разработчиков.
Очевидным тестом будет:
if touch /path/to/file; then
: it can be created
fi
Но на самом деле файл создается, если его еще нет. Мы могли бы убрать за собой:
if touch /path/to/file; then
rm /path/to/file
fi
Но это приведет к удалению уже существующего файла, который вам, вероятно, не нужен.
Однако у нас есть способ обойти это:
if mkdir /path/to/file; then
rmdir /path/to/file
fi
У вас не может быть каталога с тем же именем, что и у другого объекта в этом каталоге. Я не могу представить ситуацию, в которой вы могли бы создать каталог, но не создать файл. После этого теста ваш скрипт сможет создать обычный /path/to/file
и делать с ним все, что захочет.
Я думаю, что решение DopeGhoti лучше, но оно тоже должно работать:
file=$1
if [[ "${file:0:1}" == '/' ]]; then
dir=${file%/*}
elif [[ "$file" =~.*/.* ]]; then
dir="$(PWD)/${file%/*}"
else
dir=$(PWD)
fi
if [[ -w "$dir" ]]; then
echo "writable"
#do stuff with writable file
else
echo "not writable"
#do stuff without writable file
fi
Первая конструкция if проверяет, является ли аргумент полным путем (, начинается с/
)и устанавливает переменную dir
в путь к каталогу до последнего /
. В противном случае, если аргумент не начинается с /
, но содержит /
(, указывающий подкаталог ), он установит dir
в текущий рабочий каталог + путь к подкаталогу. В противном случае предполагается текущий рабочий каталог. Затем он проверяет, доступен ли этот каталог для записи.
Один из вариантов, который вы, возможно, захотите рассмотреть, это создание файла на ранней стадии, но его заполнение в сценарии только позже. Вы можете использовать команду exec
, чтобы открыть файл в файловом дескрипторе (, таком как 3, 4 и т. д. ), а затем использовать перенаправление на файловый дескриптор(>&3
и т. д. )для записать содержимое в этот файл.
Что-то вроде:
#!/bin/bash
# Open the file for read/write, so it doesn't get
# truncated just yet (to preserve the contents in
# case the initial checks fail.)
exec 3<>dir/file.txt || {
echo "Error creating dir/file.txt" >&2
exit 1
}
# Long checks here...
check_ok || {
echo "Failed checks" >&2
# cleanup file before bailing out
rm -f dir/file.txt
exit 1
}
# We're ready to write, first truncate the file.
# Use "truncate(1)" from coreutils, and pass it
# /dev/fd/3 so the file doesn't need to be reopened.
truncate -s 0 /dev/fd/3
# Now populate the file, use a redirection to write
# to the previously opened file descriptor.
populate_contents >&3
Вы также можете использовать trap
для очистки файла при ошибке, это обычная практика.
Таким образом, вы получаете реальную проверку разрешений, позволяющих создать файл, и в то же время возможность выполнить его достаточно рано, чтобы в случае неудачи вы не потратил время на ожидание долгих чеков.
ОБНОВЛЕНО:Чтобы избежать затирания файла в случае неудачной проверки, используйте перенаправление bashfd<>file
, которое не обрезает файл сразу. (Мы не заботимся о чтении из файла, это всего лишь обходной путь, поэтому мы не усекаем его. Добавление с помощью >>
, вероятно, тоже сработает, но я склонен находить это немного более элегантным, поскольку флаг O _APPEND не используется.)
Когда мы будем готовы заменить содержимое, нам нужно сначала обрезать файл (, иначе, если мы записываем меньше байтов, чем было в файле раньше, конечные байты останутся там.)Для этой цели мы можем использовать команду truncate (1)из coreutils, и мы можем передать ей имеющийся у нас дескриптор открытого файла (, используя /dev/fd/3
псевдо--файл ), поэтому повторно открывать файл не нужно. (Опять же, технически что-то более простое, например : >dir/file.txt
, вероятно, сработает, но отсутствие необходимости повторного открытия файла является более элегантным решением.)
Из того, что я знаю, вы хотите проверить это при использовании
tee -- "$OUT_FILE"
(обратите внимание на --
, иначе это не сработает для имен файлов, начинающихся с -), tee
удастся открыть файл для записи.
Вот что:
На данный момент мы будем игнорировать файловые системы, такие как vfat, ntfs или hfsplus, которые имеют ограничения на то, какие байтовые значения могут содержать имена файлов, дисковую квоту, ограничение процесса, selinux, apparmor или другой механизм безопасности, полную файловую систему, без inode. слева, файлы устройств, которые не могут быть открыты таким образом по той или иной причине, файлы, которые являются исполняемыми файлами, в настоящее время сопоставленными в некотором адресном пространстве процесса, все из которых также могут повлиять на возможность открытия или создания файла.
Сzsh
:
zmodload zsh/system
tee_would_likely_succeed() {
local file=$1 ERRNO=0 LC_ALL=C
if [ -d "$file" ]; then
return 1 # directory
elif [ -w "$file" ]; then
return 0 # writable non-directory
elif [ -e "$file" ]; then
return 1 # exists, non-writable
elif [ "$errnos[ERRNO]" != ENOENT ]; then
return 1 # only ENOENT error can be recovered
else
local dir=$file:P:h base=$file:t
[ -d "$dir" ] && # directory
[ -w "$dir" ] && # writable
[ -x "$dir" ] && # and searchable
(($#base <= $(getconf -- NAME_MAX "$dir")))
return
fi
}
В bash
или любой другой оболочке, подобной Bourne -, просто замените
zmodload zsh/system
tee_would_likely_succeed() {
<zsh-code>
}
с:
tee_would_likely_succeed() {
zsh -s -- "$@" << 'EOF'
zmodload zsh/system
<zsh-code>
EOF
}
Специфическими функциямиzsh
-здесь являются $ERRNO
(, которые предоставляют код ошибки последнего системного вызова )и ассоциативный массив $errnos[]
для преобразования в соответствующие стандартные имена макросов C. А для$var:h
(из csh )и$var:P
(требуется zsh 5.3 или выше ).
bash еще не имеет эквивалентных функций.
$file:h
можно заменить на dir=$(dirname -- "$file"; echo.); dir=${dir%??}
или на GNU dirname
:IFS= read -rd '' dir < <(dirname -z -- "$file")
.
Для $errnos[ERRNO] == ENOENT
,можно запустить ls -Ld
для файла и проверить, соответствует ли сообщение об ошибке ошибке ENOENT. Однако сделать это надежно и портативно сложно.
Одним из подходов может быть:
msg_for_ENOENT=$(LC_ALL=C ls -d -- '/no such file' 2>&1)
msg_for_ENOENT=${msg_for_ENOENT##*:}
(предполагая, что сообщение об ошибке заканчивается переводом syserror()
ENOENT
и что этот перевод не включает :
), а затем вместо [ -e "$file" ]
выполните:
err=$(ls -Ld -- "$file" 2>&1)
И проверьте наличие ошибки ENOENT с помощью
case $err in
(*:"$msg_for_ENOENT")...
esac
Часть $file:P
сложнее всего реализовать в bash
, особенно на FreeBSD.
FreeBSD имеет команду realpath
и команду readlink
, которая принимает параметр -f
, но их нельзя использовать в случаях, когда файл является символической ссылкой, которая не разрешается. То же самое с perl
's Cwd::realpath()
.
python
os.path.realpath()
, по-видимому, работает аналогично zsh
$file:P
, поэтому предполагается, что установлена по крайней мере одна версия python
и что существует команда python
, ссылающаяся на одну из них (, которая не является данным во FreeBSD ), вы можете сделать:
dir=$(python -c '
import os, sys
print(os.path.realpath(sys.argv[1]) + ".")' "$dir") || return
dir=${dir%.}
Но тогда вы могли бы также сделать все это в python
.
Или вы можете решить не обрабатывать все эти крайние случаи.
Как насчет использования обычной команды test
, описанной ниже?
FILE=$1
DIR=$(dirname $FILE) # $DIR now contains '.' for file names only, 'foo' for 'foo/bar'
if [ -d $DIR ] ; then
echo "base directory $DIR for file exists"
if [ -e $FILE ] ; then
if [ -w $FILE ] ; then
echo "file exists, is writeable"
else
echo "file exists, NOT writeable"
fi
elif [ -w $DIR ] ; then
echo "directory is writeable"
else
echo "directory is NOT writeable"
fi
else
echo "can NOT create file in non-existent directory $DIR "
fi
Вы упомянули, что на ваш вопрос повлиял опыт пользователей. Я отвечу с точки зрения UX, так как у вас есть хорошие ответы на техническую сторону.
Вместо выполнения проверки -в начале, как насчет того, чтобы записать результаты во временный файл , а затем в самом конце поместить результаты в нужный пользователю файл? Нравится:
userfile=${1:?Where would you like the file written?}
tmpfile=$(mktemp)
#... all the complicated stuff, writing into "${tmpfile}"
# fill user's file, keeping existing permissions or creating anew
# while respecting umask
cat "${tmpfile}" > "${userfile}"
if [ 0 -eq $? ]; then
rm "${tmpfile}"
else
echo "Couldn't write results into ${userfile}." >&2
echo "Results available in ${tmpfile}." >&2
exit 1
fi
Хорошо с этим подходом :он производит желаемую операцию в нормальном сценарии счастливого пути, побочные -шаги теста -и -устанавливают проблему атомарности, сохраняют разрешения целевого файла при создании, если необходим, и очень прост в реализации.
Примечание :если бы мы использовали mv
, мы бы сохранили разрешения временного файла --нам это не нужно, я думаю :мы хотим сохранить разрешения, установленные на целевой файл.
Плохая :конструкция требует в два раза больше места,(cat.. >
), вынуждает пользователя выполнять некоторую ручную работу, если целевой файл не был доступен для записи в нужное время, и оставляет временный файл, лежащий вокруг (, который может иметь проблемы с безопасностью или обслуживанием ).
TL;DR:
: >> "${userfile}"
В отличие от <> "${userfile}"
или touch "${userfile}"
, это не приведет к каким-либо ложным изменениям меток времени файла, а также будет работать с файлами -только для записи.
Из ОП:
I want to make a reasonable check if the file can be created/overwritten, but not actually create it.
И из вашего комментария к моему ответу с точки зрения UX:
The overwhelming majority of the time this fails is because the path provided is not valid. I can't reasonably prevent some rogue user some concurrently modifying the FS to break the job in the middle of the operation, but I certainly can check the #1 failure cause of an invalid or not writable path.
Единственным надежным тестом являетсяopen(2)
файл, потому что только он разрешает все вопросы о пути :возможности записи, владельце, файловой системе, сети, контексте безопасности и т. д. Любой другой тест будет касаться некоторой части возможности записи, но не другие. Если вы хотите подмножество тестов, вам в конечном итоге придется выбрать то, что для вас важно.
Но вот еще одна мысль. Насколько я понимаю:
Вы хотите выполнить эту предварительную -проверку из-за #1 и не хотите возиться с существующим файлом из-за #2.Так почему бы вам просто не попросить оболочку открыть файл для добавления, но на самом деле ничего не добавлять?
$ tree -ps
.
├── [dr-x------ 4096] dir_r
├── [drwx------ 4096] dir_w
├── [-r-------- 0] file_r
└── [-rw------- 0] file_w
$ for p in file_r dir_r/foo file_w dir_w/foo; do : >> $p; done
-bash: file_r: Permission denied
-bash: dir_r/foo: Permission denied
$ tree -ps
.
├── [dr-x------ 4096] dir_r
├── [drwx------ 4096] dir_w
│ └── [-rw-rw-r-- 0] foo
├── [-r-------- 0] file_r
└── [-rw------- 0] file_w
Под капотом это решает вопрос возможности записи именно так, как хотелось:
open("dir_w/foo", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
, но без изменения содержимого или метаданных файла. Теперь да, этот подход:
lsattr
и отреагировать соответствующим образом. rm
. Хотя я утверждаю (в своем другом ответе ), что наиболее удобный для пользователя -подход заключается в создании временного файла, который пользователь должен переместить, я думаю, что это ] наименее пользовательский -враждебный подход к полной проверке их ввода.