Jak wczytać plik do zmiennej w powłoce?

489

Chcę odczytać plik i zapisać go w zmiennej, ale muszę zachować zmienną, a nie tylko wydrukować plik. W jaki sposób mogę to zrobić? Napisałem ten skrypt, ale nie jest to dokładnie to, czego potrzebowałem:

#!/bin/sh
while read LINE  
do  
  echo $LINE  
done <$1  
echo 11111-----------  
echo $LINE  

W moim skrypcie mogę podać nazwę pliku jako parametr, więc jeśli plik zawiera na przykład „aaaa”, wydrukuje to:

aaaa
11111-----

Ale to po prostu drukuje plik na ekranie i chcę zapisać go w zmiennej! Czy jest na to łatwy sposób?

kaka
źródło
1
To wydaje się być zwykłym tekstem. Jeśli byłby to plik binarny, byłbyś potrzebny , ponieważ wynik catlub $(<someFile)spowoduje niekompletne wyjście (rozmiar jest mniejszy niż rzeczywisty plik).
Aquarius Power,

Odpowiedzi:

1052

W wieloplatformowym, najniższym wspólnym mianowniku shużywasz:

#!/bin/sh
value=`cat config.txt`
echo "$value"

W bashlub zsh, aby odczytać cały plik do zmiennej bez wywoływania cat:

#!/bin/bash
value=$(<config.txt)
echo "$value"

Powołując catsię bashlub zshslurp plik zostanie uznany za Useless Korzystanie z Kat .

Zauważ, że nie jest konieczne cytowanie podstawienia polecenia, aby zachować nowe wiersze.

Patrz: Wiki Bash Hackera - Zastępowanie poleceń - Specjalizacje .

Alan Gutierrez
źródło
4
Ok, ale to bash, nie sh; może nie pasować do wszystkich przypadków.
moala
14
Czy nie byłoby value="`cat config.txt`"i value="$(<config.txt)"byłoby bezpieczniejsze, gdyby config.txt zawiera spacje?
Martin von Wittich,
13
Pamiętaj, że używanie catjak wyżej nie zawsze jest uważane za bezużyteczne użycie cat. Na przykład < invalid-file 2>/dev/nullspowoduje wyświetlenie komunikatu o błędzie, do którego nie można skierować /dev/null, a do którego cat invalid-file 2>/dev/nullzostanie poprawnie skierowany /dev/null.
Dejay Clayton,
16
W przypadku nowych skryptów powłoki, takich jak ja, zauważ, że wersja cat używa tylnych tyknięć, a nie pojedynczych cudzysłowów! Mam nadzieję, że pozwoli to zaoszczędzić komuś pół godziny, zanim to zrozumiałem.
ericksonla,
7
Dla nowych basherów takich jak ja: Zauważ, że value=$(<config.txt)to dobrze, ale value = $(<config.txt)źle. Uważaj na te miejsca.
ArtHare
88

Jeśli chcesz odczytać cały plik do zmiennej:

#!/bin/bash
value=`cat sources.xml`
echo $value

Jeśli chcesz to przeczytać wiersz po wierszu:

while read line; do    
    echo $line    
done < file.txt
mózg
źródło
2
@brain: Co jeśli plik to Config.cpp i zawiera ukośniki odwrotne; podwójne cytaty i cytaty?
user2284570,
2
Powinieneś podwójnie zacytować zmienną w echo "$value". W przeciwnym razie powłoka wykona tokenizację białych znaków i rozwinięcie wartości wieloznacznej na wartości.
tripleee
3
@ user2284570 Używaj read -rzamiast po prostu read- zawsze, chyba że wyraźnie potrzebujesz dziwnego, starszego zachowania, o którym wspominasz.
tripleee
74

Dwie ważne pułapki

które zostały dotychczas zignorowane przez inne odpowiedzi:

  1. Końcowe usuwanie nowego wiersza z rozszerzenia poleceń
  2. Usuwanie znaków NUL

Końcowe usuwanie nowego wiersza z rozszerzenia poleceń

Jest to problem dla:

value="$(cat config.txt)"

rozwiązania typu, ale nie dla readrozwiązań opartych.

Rozszerzenie poleceń usuwa końcowe znaki nowej linii:

S="$(printf "a\n")"
printf "$S" | od -tx1

Wyjścia:

0000000 61
0000001

To łamie naiwną metodę czytania z plików:

FILE="$(mktemp)"
printf "a\n\n" > "$FILE"
S="$(<"$FILE")"
printf "$S" | od -tx1
rm "$FILE"

Obejście POSIX: dodaj dodatkowy znak do rozszerzenia polecenia i usuń go później:

S="$(cat $FILE; printf a)"
S="${S%a}"
printf "$S" | od -tx1

Wyjścia:

0000000 61 0a 0a
0000003

Obejście prawie POSIX: kodowanie ASCII. Patrz poniżej.

Usuwanie znaków NUL

Nie ma rozsądnego sposobu na przechowywanie znaków NUL w zmiennych .

Wpływa to zarówno na rozwój, jak i na readrozwiązania, i nie znam na to żadnego dobrego obejścia.

Przykład:

printf "a\0b" | od -tx1
S="$(printf "a\0b")"
printf "$S" | od -tx1

Wyjścia:

0000000 61 00 62
0000003

0000000 61 62
0000002

Ha, nasz NUL zniknął!

Obejścia:

  • Kodowanie ASCII. Patrz poniżej.

  • użyj $""literałów rozszerzenia bash :

    S=$"a\0b"
    printf "$S" | od -tx1
    

    Działa tylko z literałami, więc nie jest przydatny do czytania z plików.

Obejście problemów

Przechowuj wersję pliku zakodowaną w formacie uuencode base64 w zmiennej i dekoduj przed każdym użyciem:

FILE="$(mktemp)"
printf "a\0\n" > "$FILE"
S="$(uuencode -m "$FILE" /dev/stdout)"
uudecode -o /dev/stdout <(printf "$S") | od -tx1
rm "$FILE"

Wynik:

0000000 61 00 0a
0000003

uuencode i udecode są POSIX 7, ale domyślnie nie są dostępne w Ubuntu 12.04 ( sharutilspakiet) ... Nie widzę alternatywy POSIX 7 dla <()rozszerzenia podstawiania procesu bash oprócz zapisu do innego pliku ...

Oczywiście jest to powolne i niewygodne, więc sądzę, że prawdziwa odpowiedź brzmi: nie używaj Bash, jeśli plik wejściowy może zawierać znaki NUL.

Ciro Santilli
źródło
2
Dzięki tylko ten działał dla mnie, ponieważ potrzebowałem nowych linii.
Jason Livesay
1
@CiroSantilli: A co jeśli FILE to Config.cpp i zawiera ukośniki odwrotne; podwójne cytaty i cytaty?
user2284570,
@ user2284570 ja nie wiem, ale jest to łatwe, aby dowiedzieć się: S="$(printf "\\\'\"")"; echo $S. Wyjście: \'". Więc to działa =)
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功
@CiroSantilli: w liniach 5511? Czy na pewno nie ma zautomatyzowanego sposobu?
user2284570,
@ user2284570 Nie rozumiem, gdzie jest 5511 linii? Pułapki wynikają z $()rozszerzenia, mój przykład pokazuje, że $()rozszerzenie działa \'".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3

to działa dla mnie: v=$(cat <file_path>) echo $v

angelo.mastro
źródło
2

Jak zauważa Ciro Santilli, stosowanie podstawień poleceń spowoduje usunięcie końcowych znaków nowej linii. Ich obejście polegające na dodawaniu końcowych znaków jest świetne, ale po dłuższym używaniu zdecydowałem, że potrzebuję rozwiązania, które w ogóle nie używałoby podstawiania poleceń.

Moje podejście wykorzystuje teraz readwraz z printfwbudowaną -vflagą do odczytu zawartości standardowego wejścia bezpośrednio do zmiennej.

# Reads stdin into a variable, accounting for trailing newlines. Avoids needing a subshell or
# command substitution.
read_input() {
  # Use unusual variable names to avoid colliding with a variable name
  # the user might pass in (notably "contents")
  : "${1:?Must provide a variable to read into}"
  if [[ "$1" == '_line' || "$1" == '_contents' ]]; then
    echo "Cannot store contents to $1, use a different name." >&2
    return 1
  fi

  local _line _contents
   while read -r _line; do
     _contents="${_contents}${_line}"$'\n'
   done
   _contents="${_contents}${_line}" # capture any content after the last newline
   printf -v "$1" '%s' "$_contents"
}

Obsługuje dane wejściowe z końcowymi znakami nowej linii lub bez.

Przykładowe użycie:

$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file
dimo414
źródło
Świetny! Zastanawiam się, dlaczego nie użyć czegoś takiego, _contents="${_contents}${_line}\n "aby zachować nowe linie?
Eenoku
1
Czy pytasz o $'\n'? To konieczne, w przeciwnym razie dodajesz dosłowność \ i nznaki. Twój blok kodu ma również dodatkową spację na końcu, nie jestem pewien, czy to celowe, ale wciskałby każdą kolejną linię z dodatkową spacją.
dimo414
Dziękuję za wyjaśnienie!
Eenoku
-3

Możesz uzyskać dostęp do 1 linii jednocześnie przez pętlę for

#!/bin/bash -eu

#This script prints contents of /etc/passwd line by line

FILENAME='/etc/passwd'
I=0
for LN in $(cat $FILENAME)
do
    echo "Line number $((I++)) -->  $LN"
done

Skopiuj całą zawartość do pliku (powiedzmy line.sh); Wykonać

chmod +x line.sh
./line.sh
Prakash D.
źródło
Twoja forpętla nie zapętla się nad liniami, zapętla się nad słowami. W przypadku /etc/passwd, zdarza się, że każdy wiersz zawiera tylko jedno słowo. Jednak inne pliki mogą zawierać wiele słów w wierszu.
mpb