Bagaimana cara membaca file menjadi variabel di shell?

489

Saya ingin membaca file dan menyimpannya dalam variabel, tetapi saya perlu menyimpan variabel dan tidak hanya mencetak file. Bagaimana saya bisa melakukan ini? Saya telah menulis skrip ini tetapi itu tidak cukup yang saya butuhkan:

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

Dalam skrip saya, saya dapat memberikan nama file sebagai parameter, jadi, jika file tersebut berisi "aaaa", misalnya, itu akan mencetak ini:

aaaa
11111-----

Tapi ini hanya mencetak file ke layar, dan saya ingin menyimpannya ke dalam variabel! Apakah ada cara mudah untuk melakukan ini?

kaka
sumber
1
Tampaknya menjadi teks biasa. Jika itu adalah file biner, Anda akan memerlukan ini , sebagai hasil dari catatau $(<someFile)akan menghasilkan output yang tidak lengkap (ukurannya lebih kecil dari file sebenarnya).
Aquarius Power

Jawaban:

1052

Di lintas platform, penyebut terendah yang shAnda gunakan:

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

Di bashatau zsh, untuk membaca seluruh file menjadi variabel tanpa memohon cat:

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

Meminjam catdi bashatau zshuntuk mencucup file akan dianggap sebagai Useless Penggunaan Cat .

Perhatikan bahwa tidak perlu mengutip substitusi perintah untuk mempertahankan baris baru.

Lihat: Wiki Bash Hacker - substitusi perintah - Spesialisasi .

Alan Gutierrez
sumber
4
Ok tapi itu bash, bukan sh; itu mungkin tidak cocok untuk semua kasus.
moala
14
Bukankah value="`cat config.txt`"dan value="$(<config.txt)"lebih aman jika config.txt berisi spasi?
Martin von Wittich
13
Perhatikan bahwa menggunakan catseperti di atas tidak selalu dianggap sebagai penggunaan yang tidak berguna cat. Misalnya, < invalid-file 2>/dev/nullakan menghasilkan pesan kesalahan yang tidak dapat dialihkan ke /dev/null, sedangkan cat invalid-file 2>/dev/nullmendapatkan dialihkan dengan benar /dev/null.
Dejay Clayton
16
Untuk skrip shell baru seperti saya, perhatikan versi cat menggunakan tick kembali, bukan kutipan tunggal! Semoga ini akan menyelamatkan seseorang setengah jam butuh saya untuk mengetahuinya.
ericksonla
7
Untuk bashers baru seperti saya: Perhatikan itu value=$(<config.txt)bagus, tetapi value = $(<config.txt)buruk. Hati-hati dengan ruang itu.
ArtHare
88

Jika Anda ingin membaca seluruh file menjadi variabel:

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

Jika Anda ingin membacanya baris demi baris:

while read line; do    
    echo $line    
done < file.txt
otak
sumber
2
@brain: Bagaimana jika file tersebut Config.cpp dan mengandung backslash; kutip ganda dan kutipan?
user2284570
2
Anda harus memberi tanda kutip ganda variabel echo "$value". Jika tidak, shell akan melakukan tokenization dan ekspansi wildcard pada nilai.
tripleee
3
@ user2284570 Gunakan read -rbukan hanya read- selalu, kecuali jika Anda secara khusus memerlukan perilaku warisan aneh yang Anda singgung.
tripleee
74

Dua perangkap penting

yang diabaikan oleh jawaban lain sejauh ini:

  1. Mengikuti penghapusan baris baru dari ekspansi perintah
  2. Penghapusan karakter NUL

Mengikuti penghapusan baris baru dari ekspansi perintah

Ini adalah masalah untuk:

value="$(cat config.txt)"

ketik solusi, tetapi tidak untuk readsolusi berbasis.

Ekspansi perintah menghapus trailing newlines:

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

Output:

0000000 61
0000001

Ini memecah metode naif membaca dari file:

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

Solusi POSIX: tambahkan char tambahan ke perintah ekspansi dan hapus nanti:

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

Output:

0000000 61 0a 0a
0000003

Solusi Hampir POSIX: ASCII encode. Lihat di bawah.

Penghapusan karakter NUL

Tidak ada cara Bash yang waras untuk menyimpan karakter NUL dalam variabel .

Ini memengaruhi ekspansi dan readsolusi, dan saya tidak tahu solusi yang bagus untuk itu.

Contoh:

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

Output:

0000000 61 00 62
0000003

0000000 61 62
0000002

Ha, NUL kita hilang!

Penanganan masalah:

  • Encode ASCII. Lihat di bawah.

  • gunakan bash extension $""literals:

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

    Hanya berfungsi untuk literal, jadi tidak berguna untuk membaca dari file.

Solusi untuk perangkap

Menyimpan versi yang disandikan uuencode base64 dari file dalam variabel, dan mendekode sebelum setiap penggunaan:

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

Keluaran:

0000000 61 00 0a
0000003

uuencode dan udecode adalah POSIX 7 tetapi tidak di Ubuntu 12.04 secara default ( sharutilspaket) ... Saya tidak melihat alternatif POSIX 7 untuk proses bash<() ekstensi pengganti kecuali menulis ke file lain ...

Tentu saja, ini lambat dan tidak nyaman, jadi saya kira jawaban sebenarnya adalah: jangan gunakan Bash jika file input mungkin berisi karakter NUL.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
2
Terima kasih hanya ini yang bekerja untuk saya karena saya perlu baris baru.
Jason Livesay
1
@CiroSantilli: Bagaimana jika FILE adalah Config.cpp dan mengandung backslash; kutip ganda dan kutipan?
user2284570
@ user2284570 Aku tidak tahu, tapi mudah untuk mencari tahu: S="$(printf "\\\'\"")"; echo $S. Output: \'". Jadi itu berfungsi =)
Ciro Santilli 郝海东 冠状 病 六四 事件 事件
@CiroSantilli: Pada 5511 baris? Apakah Anda yakin tidak ada cara otomatis?
user2284570
@ user2284570 Saya tidak mengerti, di mana ada 5511 baris? Perangkap datang dari $()ekspansi, contoh saya menunjukkan bahwa $()ekspansi bekerja dengan baik \'".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3

ini bekerja untuk saya: v=$(cat <file_path>) echo $v

angelo.mastro
sumber
2

Seperti yang dicatat Ciro Santilli menggunakan pergantian perintah akan meninggalkan baris baru. Solusi mereka menambahkan karakter tambahan sangat bagus, tetapi setelah menggunakannya cukup lama saya memutuskan saya membutuhkan solusi yang tidak menggunakan substitusi perintah sama sekali.

Pendekatan saya sekarang menggunakan readbersama dengan printfbuiltin ini -vbendera untuk membaca isi stdin langsung ke variabel.

# 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"
}

Ini mendukung input dengan atau tanpa mengikuti baris baru.

Contoh penggunaan:

$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file
dimo414
sumber
Bagus! Saya hanya ingin tahu, mengapa tidak menggunakan sesuatu seperti _contents="${_contents}${_line}\n "mempertahankan baris baru?
Eenoku
1
Apakah Anda bertanya tentang $'\n'? Itu perlu, kalau tidak Anda menambahkan literal \ dan nkarakter. Blok kode Anda juga memiliki ruang ekstra di bagian akhir, tidak yakin apakah itu disengaja, tapi itu akan membuat setiap baris berikutnya dengan spasi kosong.
dimo414
Terima kasih atas penjelasannya!
Eenoku
-3

Anda dapat mengakses 1 baris sekaligus untuk loop

#!/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

Salin seluruh konten ke File (say line.sh); Menjalankan

chmod +x line.sh
./line.sh
Prakash D
sumber
Anda forlingkaran tidak loop melalui saluran, itu loop atas kata-kata. Dalam kasus /etc/passwd, setiap baris hanya berisi satu kata. Namun, file lain mungkin berisi banyak kata per baris.
mpb