Użytkownik wywołuje mój skrypt ze ścieżką do pliku, która zostanie utworzona lub nadpisana w pewnym momencie skryptu, np . foo.sh file.txt
Lub foo.sh dir/file.txt
.
Zachowanie tworzenia lub zastępowania przypomina wymagania dotyczące umieszczenia pliku po prawej stronie >
wyjściowego operatora przekierowania lub przekazania go jako argumentu tee
(w rzeczywistości przekazanie go jako argumentu dotee
jest dokładnie tym, co robię ).
Zanim przejdę do sedna skryptu, chcę w rozsądny sposób sprawdzić, czy plik można utworzyć / zastąpić, ale tak naprawdę go nie tworzyć. Ta kontrola nie musi być idealna i tak, zdaję sobie sprawę, że sytuacja może się zmienić między sprawdzeniem a momentem, w którym plik jest rzeczywiście zapisywany - ale tutaj wszystko jest w porządku z najlepszym wysiłkiem rozwiązaniem typu , dzięki czemu mogę wcześnie wyskoczyć w przypadku, gdy ścieżka do pliku jest nieprawidłowa.
Przykłady powodów, dla których nie udało się utworzyć pliku:
- plik zawiera element katalogu, na przykład,
dir/file.txt
ale katalogdir
nie istnieje - użytkownik nie ma uprawnień do zapisu w określonym katalogu (lub CWD, jeśli nie określono katalogu
Tak, zdaję sobie sprawę, że sprawdzanie uprawnień „z góry” to nie jest UNIX Way ™ , raczej powinienem po prostu spróbować wykonać operację i poprosić o wybaczenie później. W moim konkretnym skrypcie prowadzi to jednak do złego komfortu użytkowania i nie mogę zmienić odpowiedzialnego komponentu.
źródło
/foo/bar/file.txt
. Zasadniczo mijam ścieżkętee
podobatee $OUT_FILE
gdzieOUT_FILE
jest przekazywane w linii poleceń. To powinno „po prostu działać” na ścieżkach absolutnych i względnych, prawda?tee -- "$OUT_FILE"
przynajmniej. Co jeśli plik już istnieje lub istnieje, ale nie jest zwykłym plikiem (katalog, dowiązanie symboliczne, fifo)?tee "${OUT_FILE}.tmp"
. Jeśli plik już istnieje,tee
zastępuje, co w tym przypadku jest pożądanym zachowaniem. Jeśli to katalog,tee
zawiedzie (tak myślę). symlink Nie jestem w 100% pewien?Odpowiedzi:
Oczywistym testem byłoby:
Ale tak naprawdę tworzy plik, jeśli jeszcze go nie ma. Możemy po sobie posprzątać:
Spowoduje to jednak usunięcie istniejącego pliku, którego prawdopodobnie nie chcesz.
Mamy jednak możliwość obejścia tego:
Nie możesz mieć katalogu o takiej samej nazwie jak inny obiekt w tym katalogu. Nie mogę wymyślić sytuacji, w której można utworzyć katalog, ale nie utworzyć pliku. Po tym teście skrypt będzie mógł swobodnie tworzyć konwencjonalne
/path/to/file
i robić z nim, co tylko zechce.źródło
if mkdir /var/run/lockdir
jako testu blokującego. Jest to atomowe, choćif ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfile
ma krótki przedział czasu między testem a momentem rozpoczęciatouch
innej instancji danego skryptu.Z tego, co zbieram, chcesz to sprawdzić podczas używania
(zwróć uwagę,
--
że inaczej nie zadziała w przypadku nazw plików rozpoczynających się od -),tee
udałoby się otworzyć plik do zapisu.To jest to:
Na razie zignorujemy systemy plików takie jak vfat, ntfs lub hfsplus, które mają ograniczenia co do tego, jakie nazwy plików mogą zawierać wartości bajtów, przydział dysku, limit procesów, selinux, apparmor lub inny mechanizm bezpieczeństwa w sposób, pełny system plików, brak i-węzła, urządzenie pliki, których nie można otworzyć w ten sposób z tego czy innego powodu, pliki wykonywalne aktualnie zamapowane w jakiejś przestrzeni adresowej procesu, z których wszystkie mogą również wpłynąć na możliwość otwarcia lub utworzenia pliku.
Z
zsh
:W
bash
dowolnej powłoce podobnej do Bourne'a po prostu wymieńz:
Dostępne
zsh
są tutaj specyficzne funkcje$ERRNO
(które ujawniają kod błędu ostatniego wywołania systemowego) i$errnos[]
tablica asocjacyjna do przetłumaczenia na odpowiednie standardowe nazwy makr C. I$var:h
(z csh) i$var:P
(potrzebuje Zsh 5.3 lub nowszej wersji).bash nie ma jeszcze równoważnych funkcji.
$file:h
można zastąpićdir=$(dirname -- "$file"; echo .); dir=${dir%??}
lub GNUdirname
:IFS= read -rd '' dir < <(dirname -z -- "$file")
.Na
$errnos[ERRNO] == ENOENT
, podejście może być prowadzonyls -Ld
pliku i sprawdzenia, czy komunikat o błędzie odpowiada błędowi ENOENT. Robienie tego niezawodnie i przenośnie jest jednak trudne.Jednym podejściem może być:
(zakładając, że komunikat o błędzie kończy się
syserror()
tłumaczeniemENOENT
i że to tłumaczenie nie zawiera a:
), a następnie zamiast[ -e "$file" ]
:I sprawdź ENOENT błąd z
Ta
$file:P
część jest najtrudniejsza do osiągnięciabash
, szczególnie na FreeBSD.FreeBSD ma
realpath
polecenie ireadlink
polecenie, które akceptuje-f
opcję, ale nie można ich użyć w przypadkach, gdy plik jest dowiązaniem symbolicznym, który nie jest rozpoznawany. To samo zperl
„s”Cwd::realpath()
.python
„sos.path.realpath()
wydaje się działać podobnie dozsh
$file:P
, więc przy założeniu, że co najmniej jedna wersjapython
jest zainstalowana i że jestpython
polecenie, które odnosi się do jednego z nich, można zrobić (co nie jest podane na FreeBSD):Ale równie dobrze możesz zrobić wszystko
python
.Lub możesz zdecydować, aby nie zajmować się tymi wszystkimi przypadkami narożnymi.
źródło
tee
więc wymaganie jest takie, aby plik mógł zostać utworzony, jeśli nie istnieje, lub może zostać skrócony do zera i zastąpiony jeśli tak (lub jakkolwiek totee
obsługuje).perl
,awk
,sed
lubpython
(lub nawetsh
,bash
...). Skrypty powłoki polegają na użyciu najlepszej komendy dla tego zadania. Tutaj nawetperl
nie ma odpowiednikazsh
's$var:P
(Cwd::realpath
zachowuje się jak BSDrealpath
lubreadlink -f
gdy chcesz, aby zachowywał się jak GNUreadlink -f
lubpython
' sos.path.realpath()
)Jedną z opcji, którą warto rozważyć, jest wcześniejsze utworzenie pliku, ale wypełnienie go później w skrypcie. Możesz użyć
exec
polecenia, aby otworzyć plik w deskryptorze pliku (takim jak 3, 4 itd.), A następnie użyć przekierowania do deskryptora pliku (>&3
itp.), Aby zapisać zawartość do tego pliku.Coś jak:
Możesz także użyć
trap
do wyczyszczenia pliku po błędzie, co jest powszechną praktyką.W ten sposób otrzymujesz prawdziwe sprawdzenie uprawnień, które będziesz w stanie utworzyć plik, a jednocześnie będziesz w stanie wykonać go wystarczająco wcześnie, aby w razie niepowodzenia nie spędzać czasu na czekaniu.
ZAKTUALIZOWANO: Aby uniknąć zablokowania pliku w przypadku niepowodzenia sprawdzeń, użyj
fd<>file
przekierowania bash, które nie powoduje natychmiastowego obcięcia pliku . (Nie zależy nam na czytaniu z pliku, jest to tylko obejście, więc go nie obcinamy.>>
prawdopodobnie też po prostu zadziałałoby, ale wydaje mi się, że ten jest nieco bardziej elegancki, utrzymując flagę O_APPEND poza zdjęcia.)Kiedy jesteśmy gotowi do zastąpienia zawartości, musimy najpierw obciąć plik (w przeciwnym razie, jeśli piszemy mniej bajtów niż w pliku wcześniej, końcowe bajty pozostaną tam.) Możemy użyć skrótu (1) w tym celu polecenie z coreutils, i możemy przekazać mu otwarty deskryptor pliku, który mamy (używając
/dev/fd/3
pseudopliku), więc nie trzeba go ponownie otwierać. (Znowu technicznie coś prostszego: >dir/file.txt
prawdopodobnie by działało, ale nie trzeba ponownie otwierać pliku to bardziej eleganckie rozwiązanie).źródło
echo -n >>file
lubtrue >> file
. Oczywiście ext4 ma pliki tylko do dołączania, ale możesz żyć z tym fałszywie dodatnim.my-file.txt.backup
) Przed utworzeniem nowego. Innym rozwiązaniem jest zapisanie nowego pliku do pliku tymczasowego w tym samym folderze, a następnie skopiowanie go do starego pliku po pomyślnym zakończeniu reszty skryptu --- jeśli ostatnia operacja się nie powiedzie, użytkownik może ręcznie naprawić problem bez utraty jego postęp.Myślę, że rozwiązanie DopeGhoti jest lepsze, ale powinno również działać:
Pierwsza konstrukcja if sprawdza, czy argument jest pełną ścieżką (zaczyna się od
/
) i ustawiadir
zmienną na ścieżkę katalogu do ostatniej/
. W przeciwnym razie, jeśli argument nie zaczyna się od,/
ale zawiera/
(określając podkatalog), ustawidir
na bieżący katalog roboczy + ścieżkę do podkatalogu. W przeciwnym razie zakłada bieżący katalog roboczy. Następnie sprawdza, czy ten katalog jest zapisywalny.źródło
Co powiesz na użycie normalnego
test
polecenia, jak opisano poniżej?źródło
man test
, i to[ ]
jest równoważne testowi (już nie jest to powszechna wiedza). Oto jedno-linijka, której miałem użyć w mojej gorszej odpowiedzi, aby objąć dokładny przypadek, dla potomności:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Wspomniałeś, że wrażenia użytkownika kierują Twoim pytaniem. Odpowiem pod kątem UX, ponieważ masz dobre odpowiedzi od strony technicznej.
Zamiast przeprowadzania kontroli z góry, co powiesz na zapisanie wyników do pliku tymczasowego, a następnie na samym końcu, umieszczenie wyników w żądanym pliku użytkownika? Lubić:
Zaleta tego podejścia: zapewnia pożądaną operację w normalnym scenariuszu szczęśliwej ścieżki, omija kwestię testowania i ustawiania atomowości, zachowuje uprawnienia do pliku docelowego podczas tworzenia, jeśli to konieczne, i jest bardzo prosta do wdrożenia.
Uwaga: gdybyśmy to wykorzystali
mv
, zachowalibyśmy uprawnienia do pliku tymczasowego - myślę, że tego nie chcemy: chcemy zachować uprawnienia ustawione w pliku docelowym.Teraz źle: wymaga podwójnej przestrzeni (
cat .. >
konstrukcji), zmusza użytkownika do wykonania pracy ręcznej, jeśli plik docelowy nie był zapisywalny w tym momencie, w którym powinien być, i pozostawia plik tymczasowy leżący (co może mieć zabezpieczenia lub problemy konserwacyjne).źródło
TL; DR:
W przeciwieństwie do
<> "${userfile}"
lubtouch "${userfile}"
, nie spowoduje to żadnych fałszywych modyfikacji znaczników czasu pliku, a także będzie działać z plikami tylko do zapisu.Z PO:
I od twojego komentarza do mojej odpowiedzi z perspektywy UX :
Jedynym wiarygodnym testem jest
open(2)
plik, ponieważ tylko to rozwiązuje każde pytanie dotyczące możliwości zapisu: ścieżki, własności, systemu plików, sieci, kontekstu bezpieczeństwa itp. Każdy inny test dotyczy pewnej części możliwości zapisu, ale nie innych. Jeśli chcesz mieć podzbiór testów, ostatecznie musisz wybrać to, co jest dla Ciebie ważne.Ale oto kolejna myśl. Z tego co rozumiem:
Chcesz zrobić to sprawdzenie wstępne z powodu # 1 i nie chcesz majstrować przy istniejącym pliku z powodu # 2. Dlaczego więc nie poprosisz powłoki o otwarcie pliku do dołączenia, ale tak naprawdę niczego nie dołączasz?
Pod maską rozwiązuje to kwestię zapisu dokładnie tak, jak chciał:
ale bez modyfikowania zawartości pliku lub metadanych. Teraz tak, to podejście:
lsattr
i odpowiednio zareagować.rm
.Chociaż twierdzę (w mojej innej odpowiedzi), że najbardziej przyjazne dla użytkownika podejście jest utworzenie pliku tymczasowego, który użytkownik musi przenieść, myślę, że jest to podejście najmniej przyjazne dla użytkownika, aby w pełni zweryfikować ich dane wejściowe.
źródło