Як я можу перевірити, чи можна створити файл або скоротити / перезаписати файл у файлі bash?


15

Користувач викликає мій сценарій шляхом до файлу, який буде або створений, або перезаписаний в якийсь момент сценарію, наприклад, foo.sh file.txtабо foo.sh dir/file.txt.

Поведінка створення або перезапису схожа на вимоги щодо розміщення файлу праворуч від >оператора переадресації виводу або передачі його як аргументу tee(насправді передача його як аргументу tee- саме те, що я роблю ).

Перш ніж я потрапляю до кишок сценарію, я хочу зробити розумну перевірку, чи можна створити / перезаписати файл , а не створити його. Ця перевірка не повинна бути ідеальною, і так, я розумію, що ситуація може змінюватися між чеком і точкою, де файл насправді записаний - але тут я все в порядку з найкращим рішенням зусиль, щоб я міг достроково відмовитись у випадку, якщо шлях до файлу недійсний.

Приклади причин, через які файл не міг створити:

  • файл містить компонент каталогів, наприклад, dir/file.txtале каталог dirне існує
  • користувач не має дозволу на запис у вказаному каталозі (або CWD, якщо не вказано жодного каталогу)

Так, я розумію, що перевірка дозволів "на передній план" - це не шлях UNIX Way ™ , швидше я просто спробую операцію і пізніше попросити пробачення. Однак у моєму конкретному сценарії це призводить до поганого досвіду користувача, і я не можу змінити відповідальний компонент.


Чи завжди аргумент базуватиметься на поточному каталозі чи може користувач вказати повний шлях?
jesse_b

@Jesse_b - Я вважаю, що користувач міг би вказати абсолютний шлях на зразок /foo/bar/file.txt. В основному я проходжу шлях, щоб teeподобатися тому, tee $OUT_FILEде OUT_FILEпередано в командному рядку. Це повинно "просто працювати" як з абсолютними, так і з відносними шляхами, правда?
BeeOnRope

@BeeOnRope, ні, принаймні, тобі не знадобиться tee -- "$OUT_FILE". Що робити, якщо файл вже існує або існує, але це не звичайний файл (каталог, symlink, fifo)?
Стефан Хазелас

@ StéphaneChazelas - добре я використовую tee "${OUT_FILE}.tmp". Якщо файл вже існує, teeперезаписується, що є бажаною поведінкою в цьому випадку. Якщо це каталог, teeвийде з ладу (я думаю). symlink Я не впевнений на 100%?
BeeOnRope

Відповіді:


11

Очевидним тестом було б:

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і зробити з ним все, що заманеться.


2
Це дуже акуратно вирішує стільки питань! Коментар до коду, що пояснює та обґрунтовує, що незвичайне рішення може значно перевищити тривалість вашої відповіді. :-)
jpaugh

Я також використовував if mkdir /var/run/lockdirяк тест блокування. Це атомно, хоча if ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfileміж тестом є невеликий проміжок часу, і touchтам, де може початися інший екземпляр відповідного сценарію.
DopeGhoti

8

З того, що я збираю, ви хочете перевірити це під час використання

tee -- "$OUT_FILE"

(зауважте, --або це не працює для імен файлів, які починаються з -), teeвдалося б відкрити файл для запису.

Це те, що:

  • довжина шляху файлу не перевищує межу PATH_MAX
  • файл існує (після роздільної здатності символьного посилання) і не є типом каталогу, і ви маєте на нього дозвіл на запис.
  • якщо файл не існує, ім’я файлу існує (після роздільної здатності символьної посилання) як каталог, і ви маєте на нього дозвіл на запис та пошук, а довжина імені файлу не перевищує межу NAME_MAX файлової системи, в якій знаходиться каталог.
  • або файл є символьним посиланням, яке вказує на файл, який не існує і не є циклом символьної посилання, але відповідає критеріям трохи вище

Зараз ми будемо ігнорувати такі файлові системи, як vfat, ntfs або hfsplus, які мають обмеження щодо того, які імена файлів значень байтів можуть містити, квоту диска, ліміт процесу, selinux, apparmor чи інший механізм захисту, таким чином, повна файлова система, не залишається вхід, пристрій файли, які з тієї чи іншої причини неможливо відкрити, файли, які виконуються в даний час, відображені в деякому адресному просторі процесу, і все це також може вплинути на можливість відкриття або створення файлу.

З 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або будь-якій оболонці Борна просто замініть

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.

Або ви могли вирішити не займатися усіма цими кутовими справами.


Так, ви правильно зрозуміли мій намір. Насправді первісне питання було незрозумілим: я спочатку говорив про створення файлу, але насправді я його використовую, teeтому вимога справді полягає в тому, що файл можна створити, якщо його не існує, або його можна обрізати до нуля та перезаписати якщо він це робить (або все-таки це teeвпорається).
BeeOnRope

Схоже, ваша остання редакція витерла формат
D. Ben Knoble

Дякую, @ bishop, @ D.BenKnoble, див. Редагування.
Стефан Шазелас

1
@DennisWilliamson, так, оболонка - це інструмент для виклику програм, і кожна програма, яку ви викликаєте у своєму скрипті, повинна бути встановлена ​​для того, щоб ваш сценарій працював, це само собою зрозуміло. macOS поставляється з bash та zsh, встановленими за замовчуванням. FreeBSD не має жодного. У OP може бути поважна причина, що хочуть зробити це в баші, можливо, це те, що вони хочуть додати до вже написаного баш-сценарію.
Стефан Хазелас

1
@pipe, знову ж таки, оболонка - це інтерпретатор команд. Ви побачите , що багато коду оболонки уривки , написані тут Invoke інтерпретатори інших мов , таких як perl, awk, sedабо python(або навіть sh, bash...). Сценарій оболонки - це використання найкращої команди для виконання завдання. Тут навіть perlнемає еквівалента zsh's $var:P(він Cwd::realpathведе себе як BSD realpathабо readlink -fтоді, коли ви хочете, щоб він поводився як GNU readlink -fабо python' s os.path.realpath())
Stéphane Chazelas

6

Один з варіантів, який ви можете розглянути, - це створити файл на початку, але лише заповнити його пізніше у вашому сценарії. Ви можете скористатися 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

Ви також можете використовувати a, trapщоб очистити файл помилково, це звичайна практика.

Таким чином, ви отримуєте реальну перевірку дозволів на те, що зможете створити файл, в той же час зможете виконати його досить рано, що, якщо це не вдасться, ви не витратили час на очікування довгих перевірок.


ОНОВЛЕНО: Щоб уникнути випадання файлу у випадку невдачі перевірок, використовуйте fd<>fileперенаправлення bash, яке не скоротить файл відразу. (Нам не байдуже читати з файлу, це просто вирішення, тому ми не обрізаємо його. Додавання >>, ймовірно, теж спрацювало б, але я схильний вважати цей трохи елегантнішим, не зберігаючи прапор O_APPEND малюнка.)

Коли ми будемо готові замінити вміст, нам слід спершу обрізати файл (інакше, якщо ми запишемо менше байтів, ніж було у файлі раніше, то остаточні байти залишаться там.) Ми можемо використати усікання (1) команда з coreutils для цієї мети, і ми можемо передати його дескриптору відкритого файлу, який ми маємо (використовуючи /dev/fd/3псевдофайл), тому йому не потрібно повторно відкривати файл. (Знову ж таки, технічно щось, начебто : >dir/file.txt, спрацювало б, але не потрібно знову відкривати файл - більш елегантне рішення.)


Я це враховував, але проблема полягає в тому, що якщо ще одна невинна помилка виникає десь між тим, коли я створюю файл, і точкою через деякий час, де я б його писав, користувач, ймовірно, засмутиться, дізнавшись, що вказаний файл був перезаписаний .
BeeOnRope

1
@BeeOnRope Ще одним варіантом, який не скасовує файл, є спробувати до нього нічого не додати : echo -n >>fileабо true >> file. Звичайно, у ext4 є файли, що додаються, але ви можете жити з цим хибним позитивом.
mosvy

1
@BeeOnRope Якщо вам потрібно зберегти або (a) існуючий файл, або (b) (повний) новий файл, це питання інше, ніж ви задавали. Хороша відповідь на нього - перемістити існуючий файл до нового імені (наприклад my-file.txt.backup) перед створенням нового. Іншим рішенням є написати новий файл у тимчасовий файл у ту ж папку, а потім скопіювати його на старий файл після того, як решта сценарію буде успішною --- якщо ця остання операція не вдалася, користувач міг вручну виправити проблему, не втрачаючи його або її прогрес.
jpaugh

1
@BeeOnRope Якщо ви рухаєтесь по маршруту "атомна заміна" (який хороший!), Зазвичай вам потрібно буде записати тимчасовий файл у той самий каталог, що і кінцевий файл (вони повинні бути в тій же файловій системі для перейменування (2) щоб досягти успіху) і використовувати унікальне ім’я (тому, якщо є два екземпляри запущеного сценарію, вони не будуть зациклюватися на тимчасових файлах один одного.) Відкриття тимчасового файлу для запису в одному каталозі, як правило, є хорошим свідченням Ви зможете перейменувати її пізніше (ну, крім випадків, коли кінцева ціль існує і є каталогом), тож певною мірою вона також стосується вашого питання.
filbranden

1
@jpaugh - Я дуже неохоче став це робити. Це неминуче призведе до відповідей, намагаючись вирішити мою проблему вищого рівня, яка може визнати рішення, які не мають нічого спільного з питанням "Як я можу перевірити ...". Незалежно від того, яка моя проблема на високому рівні, я думаю, що це питання є окремим, як щось, що ви, можливо, хочете зробити. Було б несправедливо стосовно людей, які відповідають на це конкретне запитання, якби я змінив його на «вирішити цю конкретну проблему, яку я маю зі своїм сценарієм». Я включив ці деталі сюди, тому що мене попросили, але я не хочу змінювати основне питання.
BeeOnRope

4

Я думаю, що рішення 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

Перший, якщо конструкт перевіряє, чи аргумент є повним шляхом (починається з /), і встановлює dirзмінну шляху до каталогу до останнього /. В іншому випадку, якщо аргумент не починається з а, /але містить /(вказує підкаталог), він буде встановлений dirу поточну робочу директорію + шлях підкаталогу. В іншому випадку він передбачає поточний робочий каталог. Потім він перевіряє, чи є цей каталог доступним для запису.


4

Що з використанням звичайної 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

Я майже дублював цю відповідь! Приємне вступ, але ви можете згадати man test, і [ ]це рівнозначно тестуванню (вже не загальновідоме). Ось однолінійний я, який я збирався використати у своїй нижчій відповіді, щоб висвітлити точний випадок для нащадків:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Iron Gremlin

4

Ви згадали, що користувальницький досвід викликав ваше запитання. Я відповім під кутом 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 .. >конструкції), змушує користувача виконати ручну роботу, якщо цільовий файл не був доступний для запису в той час, який він потребував, і залишає тимчасовий файл, що лежить навколо (який може мати безпеку або питання технічного обслуговування).


Насправді це більш-менш те, що я зараз роблю. Я записую більшість результатів у тимчасовий файл, а потім наприкінці роблю остаточний етап обробки та записую результати у кінцевий файл. Проблема полягає в тому, що я хочу отримати заставу рано (на початку сценарію), якщо цей останній крок, ймовірно, не вдасться. Сценарій може працювати без нагляду протягом декількох хвилин або годин, тож вам дуже хочеться знати наперед, що він приречений на провал!
BeeOnRope

Звичайно, але існує так багато способів, що це може вийти з ладу: диск міг заповнитись, каталог видалення можна було видалити, дозволи можуть змінитися, цільовий файл може бути використаний для деяких інших важливих речей, і користувач забув, що він призначив цей самий файл знищений цим операція. Якщо ми говоримо про це з чистої точки зору UX, то, можливо, правильним є те, що це трактуватиметься як подання на роботу: наприкінці, коли ви знаєте, що вона працювала правильно до завершення, просто скажіть користувачеві, де отриманий контент знаходиться та пропонуйте запропонував команда для них , щоб перемістити його самі.
єпископ

Теоретично, так, існують нескінченні способи цього не змогти. На практиці переважна більшість часу цього не вдається, тому що наданий шлях не є дійсним. Я не можу обґрунтовано перешкодити якомусь шахрайському користувачеві одночасно змінювати FS, щоб порушити завдання в середині операції, але я, безумовно, можу перевірити причину відмови №1 щодо недійсного або не записаного шляху.
BeeOnRope

2

TL; DR:

: >> "${userfile}"

На відміну від <> "${userfile}"або touch "${userfile}", це не вносить помилкових модифікацій у часові позначки файлу, а також працюватиме з файлами лише для запису.


З ОП:

Я хочу зробити розумну перевірку, чи можна створити / перезаписати файл, а не створити його.

І від вашого коментаря до моєї відповіді з точки зору UX :

Переважна більшість часу цього не вдається, тому що наданий шлях не є дійсним. Я не можу обґрунтовано запобігти якомусь негідному користувачеві, якийсь одночасно модифікує FS, щоб зламати завдання в середині операції, але я, безумовно, можу перевірити причину відмови №1 щодо недійсного або не записаного шляху.

Єдиний надійний тест - open(2)це файл, тому що лише він вирішує кожне питання щодо записуваності: шлях, право власності, файлова система, мережа, контекст безпеки тощо. Будь-який інший тест стосується деякої частини записуваності, але не іншої. Якщо ви хочете підмножину тестів, вам, зрештою, доведеться вибрати те, що для вас важливо.

Але ось інша думка. З того, що я розумію:

  1. процес створення вмісту тривалий, і
  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.

Хоча я заперечую (в своїй іншій відповіді), що найбільш зручним для користувача підходом є створення тимчасового файлу, який користувач повинен перенести, я думаю, що це найменш ворожий підхід для повного ветеринального введення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.