Jak odczytać cały skrypt powłoki przed jego uruchomieniem?


35

Zwykle po edycji skryptu wszystkie uruchomione skrypty są podatne na błędy.

O ile rozumiem, bash (także inne powłoki?) Stopniowo odczytuje skrypt, więc jeśli zmodyfikowałeś plik skryptu zewnętrznie, zaczyna on odczytywać niewłaściwe rzeczy. Czy jest jakiś sposób, aby temu zapobiec?

Przykład:

sleep 20

echo test

Jeśli wykonasz ten skrypt, bash przeczyta pierwszy wiersz (powiedzmy 10 bajtów) i przejdzie w tryb uśpienia. Po wznowieniu skrypt może mieć inną zawartość, zaczynając od 10-tego bajtu. Mogę być w środku linii w nowym skrypcie. W ten sposób działający skrypt zostanie uszkodzony.


Co rozumiesz przez „zewnętrzną modyfikację skryptu”?
maulinglawns

1
Być może istnieje sposób na zawinięcie całej zawartości w funkcję lub coś, więc powłoka najpierw przeczyta cały skrypt? Ale co z ostatnim wierszem, w którym wywołujesz funkcję, czy będzie ona czytana do EOF? Może pominięcie ostatniego \nmoże załatwić sprawę? Może ()zrobi to podpowłoka ? Nie mam z tym dużego doświadczenia, proszę o pomoc!
VasyaNovikov

@maulinglawns, jeśli skrypt zawiera takie treści, sleep 20 ;\n echo test ;\n sleep 20a ja zacznę go edytować, może źle się zachowywać. Na przykład bash może odczytać pierwsze 10 bajtów skryptu, zrozumieć sleeppolecenie i przejść do trybu uśpienia. Po wznowieniu w pliku będzie inna zawartość, zaczynając od 10 bajtów.
VasyaNovikov

1
Mówisz więc, że edytujesz skrypt, który jest wykonywany? Najpierw zatrzymaj skrypt, dokonaj edycji, a następnie uruchom go ponownie.
maulinglawns,

@maulinglawns tak, to po prostu wszystko. Problem polega na tym, że nie jest dla mnie wygodne zatrzymywanie skryptów i ciężko jest zawsze pamiętać, aby to zrobić. Być może istnieje sposób, aby zmusić basha do przeczytania całego skryptu jako pierwszego?
VasyaNovikov

Odpowiedzi:


43

Tak, powłoki, a bashzwłaszcza starają się czytać plik po jednym wierszu na raz, więc działa tak samo, jak podczas korzystania z niego interaktywnie.

Zauważysz, że gdy plik nie jest widoczny (jak potok), bashodczytuje nawet jeden bajt na raz, aby mieć pewność, że nie przeczyta on \nznaku. Gdy plik jest widoczny, optymalizuje się, odczytując jednocześnie pełne bloki, ale należy szukać później \n.

Oznacza to, że możesz robić takie rzeczy jak:

bash << \EOF
read var
var's content
echo "$var"
EOF

Lub pisz skrypty, które same się aktualizują. Nie byłbyś w stanie tego zrobić, gdyby nie dał ci takiej gwarancji.

Teraz rzadko zdarza się, że chcesz robić takie rzeczy i, jak się okazało, funkcja ta przeszkadza częściej niż jest przydatna.

Aby tego uniknąć, można spróbować i upewnij się, że nie należy zmodyfikować plik w miejscu (na przykład zmodyfikować kopię, i przenieść kopię w miejscu (jak sed -ilub perl -pia niektórzy redaktorzy zrobić na przykład)).

Lub możesz napisać skrypt:

{
  sleep 20
  echo test
}; exit

(zwróć uwagę, że ważne jest, aby exitznajdować się w tym samym wierszu co }; chociaż możesz również umieścić go w nawiasach klamrowych tuż przed zamknięciem).

lub:

main() {
  sleep 20
  echo test
}
main "$@"; exit

Powłoka będzie musiała przeczytać skrypt do momentu, aż exitzacznie cokolwiek robić. Dzięki temu powłoka nie będzie ponownie czytać ze skryptu.

Oznacza to jednak, że cały skrypt będzie przechowywany w pamięci.

Może to również wpłynąć na parsowanie skryptu.

Na przykład w bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Wyprowadziłby kod U + 00E9 zakodowany w UTF-8. Jeśli jednak zmienisz go na:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

\ue9Zostanie poszerzona w charset, który był w rzeczywistości w tym czasie, że rozkaz został przeanalizowany, która w tym przypadku jest przedexport komenda jest wykonywana.

Zauważ też, że jeśli użyjesz polecenia sourceaka ., z niektórymi powłokami, będziesz miał ten sam problem z plikami źródłowymi.

Nie dzieje się tak w przypadku, bashgdy sourcekomenda odczytuje plik w całości przed jego interpretacją. Jeśli piszesz bashspecjalnie, możesz z tego skorzystać, dodając na początku skryptu:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Nie polegałbym jednak na tym, ponieważ można sobie wyobrazić, że przyszłe wersje bashmogłyby zmienić to zachowanie, które może być obecnie postrzegane jako ograniczenie (bash i AT&T ksh są jedynymi powłokami podobnymi do POSIX, które zachowują się tak daleko, o ile są w stanie powiedzieć) a already_sourcedtrik jest nieco kruchy, ponieważ zakłada, że ​​zmienna nie znajduje się w środowisku, nie wspominając już o tym, że wpływa na zawartość zmiennej BASH_SOURCE)


@VasyaNovikov, wydaje się, że w tej chwili coś jest nie tak z SE (a przynajmniej dla mnie). Kiedy dodałem moje, było tylko kilka odpowiedzi i wydaje się, że twój komentarz pojawił się dopiero teraz, chociaż mówi, że został opublikowany 16 minut temu (a może po prostu tracę marmurki). W każdym razie zwróć uwagę na dodatkowe „wyjście”, które jest tutaj potrzebne, aby uniknąć problemów, gdy rozmiar pliku wzrośnie (jak zauważono w komentarzu, który dodałem do twojej odpowiedzi).
Stéphane Chazelas,

Stéphane, myślę, że znalazłem inne rozwiązanie. To jest do użycia }; exec true. W ten sposób nie ma wymagań dotyczących nowych linii na końcu pliku, co jest przyjazne dla niektórych edytorów (takich jak emacs). Wszystkie testy, o których mogłem myśleć, działają poprawnie}; exec true
VasyaNovikov

@VasyaNovikov, nie jestem pewien, co masz na myśli. Jak to jest lepsze niż }; exit? Tracisz także status wyjścia.
Stéphane Chazelas

Jak wspomniano w innym pytaniu: często parsuje się cały plik, a następnie wykonuje instrukcję złożoną w przypadku . scriptużycia polecenia kropki ( ).
schily

@schily, tak, wspominam o tym w tej odpowiedzi jako ograniczenie AT&T ksh i bash. Inne powłoki typu POSIX nie mają tego ograniczenia.
Stéphane Chazelas

12

Musisz po prostu usunąć plik (tzn. Skopiować go, usunąć, zmienić nazwę kopii z powrotem na pierwotną nazwę). W rzeczywistości można skonfigurować wiele edytorów, aby zrobili to za Ciebie. Kiedy edytujesz plik i zapisujesz w nim zmieniony bufor, zamiast nadpisania pliku zmieni on nazwę starego pliku, utworzy nowy i umieści nową zawartość w nowym pliku. Dlatego każdy uruchomiony skrypt powinien kontynuować bez problemów.

Korzystając z prostego systemu kontroli wersji, takiego jak RCS, który jest łatwo dostępny dla vimów i emacsa, zyskujesz podwójną zaletę posiadania historii zmian, a system kontroli powinien domyślnie usunąć bieżący plik i odtworzyć go w poprawnych trybach. (Oczywiście uważaj na twarde linkowanie takich plików).


„usuń” nie jest tak naprawdę częścią tego procesu. Jeśli chcesz, aby był poprawnie atomowy, zmieniasz nazwę pliku docelowego - jeśli masz krok usuwania, istnieje ryzyko, że proces umrze po usunięciu, ale przed zmianą nazwy, nie pozostawiając żadnego pliku na swoim miejscu ( lub czytelnik próbuje uzyskać dostęp do pliku w tym oknie i nie może znaleźć ani starych, ani nowych wersji).
Charles Duffy,

11

Najprostsze rozwiązanie:

{
  ... your code ...

  exit
}

W ten sposób bash przeczyta cały {}blok przed jego wykonaniem orazexit dyrektywa upewni się, że nic nie zostanie odczytane poza blokiem kodu.

Jeśli nie chcesz „uruchamiać” skryptu, ale „źródła”, potrzebujesz innego rozwiązania. Powinno to wtedy działać:

{
  ... your code ...

  return 2>/dev/null || exit
}

Lub jeśli chcesz bezpośredniej kontroli nad kodem wyjścia:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voilà! Ten skrypt można bezpiecznie edytować, pozyskiwać i uruchamiać. Nadal musisz się upewnić, że nie zmodyfikujesz go w tych milisekundach, gdy jest on początkowo czytany.


1
Odkryłem, że nie widzi EOF i przestaje czytać plik, ale zostaje zaplątany w przetwarzaniu „buforowanego strumienia” i kończy wyszukiwanie na końcu pliku, dlatego wygląda dobrze, jeśli rozmiar plik powiększa się niewiele, ale wygląda źle, gdy plik jest ponad dwa razy większy niż wcześniej. Niedługo zgłoś błąd do opiekunów bash.
Stéphane Chazelas,


Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
terdon

5

Dowód koncepcji. Oto skrypt, który sam się modyfikuje:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

widzimy zmienioną wersję wydruku

Wynika to z faktu, że ładowanie bash utrzymuje uchwyt pliku do otwarcia na skrypt, więc zmiany w pliku będą widoczne natychmiast.

Jeśli nie chcesz aktualizować kopii w pamięci, odłącz oryginalny plik i zastąp go.

Jednym ze sposobów jest użycie sed -i.

sed -i '' filename

dowód koncepcji

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Jeśli używasz edytora do zmiany skryptu, włączenie funkcji „zachowaj kopię zapasową” może być wszystkim, czego potrzeba, aby edytor zapisał zmienioną wersję do nowego pliku zamiast zastąpić istniejący.


2
Nie, bashnie otwiera pliku za pomocą mmap(). Wystarczy uważnie czytać jedną linię w razie potrzeby, tak jak wtedy, gdy otrzymuje polecenia z urządzenia końcowego, gdy jest interaktywne.
Stéphane Chazelas,

2

Zawijanie skryptu w bloku {}jest prawdopodobnie najlepszą opcją, ale wymaga zmiany skryptów.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

byłaby drugą najlepszą opcją (zakładając, że tmpfs ) wadą jest to, że psuje 0 $, jeśli skrypty tego używają.

użycie czegoś podobnego F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashjest mniej idealne, ponieważ musi przechowywać cały plik w pamięci i psuje 0 USD.

należy unikać dotykania oryginalnego pliku, aby ostatnia modyfikacja nie blokowała odczytu blokad odczytu i twardych łączy. w ten sposób możesz pozostawić otwarty edytor podczas uruchamiania pliku, a rsync nie będzie niepotrzebnie sprawdzać sumy pliku dla kopii zapasowych i funkcji dowiązań twardych zgodnie z oczekiwaniami.

zastąpienie pliku podczas edycji działałoby, ale jest mniej niezawodne, ponieważ nie jest możliwe do wyegzekwowania przez inne skrypty / użytkowników / lub ktoś może zapomnieć. I znowu zerwałoby twarde linki.


wszystko, co tworzy kopię, działałoby. tac test.sh | tac | bash
Jasen
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.