Saya baru saja menetapkan variabel, tetapi echo $ variabel menunjukkan sesuatu yang lain

104

Berikut adalah rangkaian kasus yang echo $vardapat menunjukkan nilai yang berbeda dari yang baru saja ditetapkan. Ini terjadi terlepas dari apakah nilai yang ditetapkan adalah "dikutip ganda", 'dikutip tunggal', atau tidak dikutip.

Bagaimana cara mendapatkan shell untuk mengatur variabel saya dengan benar?

Tanda bintang

Output yang diharapkan adalah /* Foobar is free software */, tetapi saya mendapatkan daftar nama file:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Tanda kurung siku

Nilai yang diharapkan adalah [a-z], tetapi terkadang saya mendapatkan satu huruf saja!

$ var=[a-z]
$ echo $var
c

Feed baris (baris baru)

Nilai yang diharapkan adalah daftar baris terpisah, tetapi sebaliknya semua nilai berada dalam satu baris!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Banyak spasi

Saya mengharapkan tajuk tabel yang disejajarkan dengan hati-hati, tetapi sebaliknya beberapa spasi hilang atau diciutkan menjadi satu!

$ var="       title     |    count"
$ echo $var
title | count

Tab

Saya mengharapkan dua nilai yang dipisahkan tab, tetapi saya mendapatkan dua nilai yang dipisahkan spasi!

$ var=$'key\tvalue'
$ echo $var
key value
orang lain itu
sumber
2
Terima kasih telah melakukan ini. Saya sering menemukan satu baris yang memberi makan. Jadi var=$(cat file)baik-baik saja, tetapi echo "$var"dibutuhkan.
snd
3
BTW, ini juga BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy
Juga, lihat juga stackoverflow.com/questions/2414150/…
tripleee

Jawaban:

139

Dalam semua kasus di atas, variabel disetel dengan benar, tetapi tidak terbaca dengan benar! Cara yang benar adalah dengan menggunakan tanda kutip ganda saat merujuk :

echo "$var"

Ini memberikan nilai yang diharapkan dalam semua contoh yang diberikan. Selalu mengutip referensi variabel!


Mengapa?

Ketika sebuah variabel tidak dikutip , itu akan:

  1. Lakukan pemisahan bidang di mana nilainya dibagi menjadi beberapa kata di spasi (secara default):

    Sebelum: /* Foobar is free software */

    Setelah: /*, Foobar, is, free, software,*/

  2. Masing-masing kata ini akan mengalami perluasan nama jalur , di mana polanya diperluas menjadi file yang cocok:

    Sebelum: /*

    Setelah: /bin, /boot, /dev, /etc, /home, ...

  3. Akhirnya, semua argumen diteruskan ke echo, yang menuliskannya dipisahkan oleh spasi tunggal , memberi

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    alih-alih nilai variabel.

Ketika variabel dikutip itu akan:

  1. Menjadi diganti nilainya.
  2. Tidak ada langkah 2.

Inilah mengapa Anda harus selalu mengutip semua referensi variabel , kecuali Anda secara khusus memerlukan pemisahan kata dan perluasan nama jalur. Alat seperti shellcheck ada untuk membantu, dan akan memperingatkan tentang tanda kutip yang hilang dalam semua kasus di atas.

orang lain itu
sumber
itu tidak selalu berhasil. Saya dapat memberikan contoh: paste.ubuntu.com/p/8RjR6CS668
recolic
1
Yup, $(..)strip trailing linefeed. Anda dapat menggunakannya var=$(cat file; printf x); var="${var%x}"untuk menyiasatinya.
pria lain itu
17

Anda mungkin ingin tahu mengapa ini terjadi. Bersama dengan penjelasan hebat oleh orang lain itu , temukan referensi Mengapa skrip shell saya tersedak spasi atau karakter khusus lainnya? ditulis oleh Gilles di Unix & Linux :

Mengapa saya perlu menulis "$foo"? Apa yang terjadi tanpa tanda kutip?

$footidak berarti "ambil nilai variabel foo". Artinya sesuatu yang jauh lebih kompleks:

  • Pertama, ambil nilai variabelnya.
  • Pemisahan bidang: perlakukan nilai itu sebagai daftar bidang yang dipisahkan spasi, dan buat daftar yang dihasilkan. Misalnya, jika variabel berisi foo * bar ​maka hasil dari langkah ini adalah daftar 3-elemen foo, *, bar.
  • Pembuatan nama file: perlakukan setiap bidang sebagai bola, yaitu sebagai pola karakter pengganti, dan gantilah dengan daftar nama file yang cocok dengan pola ini. Jika polanya tidak cocok dengan file mana pun, itu dibiarkan tanpa modifikasi. Dalam contoh kami, ini menghasilkan daftar yang berisi foo, diikuti oleh daftar file di direktori saat ini, dan akhirnya bar. Jika direktori saat kosong, hasilnya adalah foo, *, bar.

Perhatikan bahwa hasilnya adalah daftar string. Ada dua konteks dalam sintaks shell: konteks daftar dan konteks string. Pemisahan bidang dan pembuatan nama file hanya terjadi dalam konteks daftar, tetapi itu sebagian besar waktu. Tanda kutip ganda membatasi konteks string: seluruh string yang dikutip ganda adalah string tunggal, tidak untuk dipisahkan. (Pengecualian: "$@"untuk memperluas ke daftar parameter posisi, misalnya "$@"setara dengan "$1" "$2" "$3"jika ada tiga parameter posisi. Lihat Apa perbedaan antara $ * dan $ @? )

Hal yang sama terjadi pada penggantian perintah dengan $(foo)atau dengan `foo`. Di samping catatan, jangan gunakan `foo`: aturan kutipannya aneh dan tidak portabel, dan semua dukungan shell modern $(foo)yang benar-benar setara kecuali memiliki aturan kutipan yang intuitif.

Output dari substitusi aritmatika juga mengalami ekspansi yang sama, tetapi itu biasanya tidak menjadi perhatian karena hanya berisi karakter yang tidak dapat diperluas (dengan asumsi IFStidak berisi angka atau -).

Lihat Kapan kutipan ganda diperlukan? untuk detail lebih lanjut tentang kasus-kasus ketika Anda dapat mengabaikan tanda kutip.

Kecuali yang Anda maksudkan agar semua omong kosong ini terjadi, ingatlah untuk selalu menggunakan tanda kutip ganda di sekitar penggantian variabel dan perintah. Berhati-hatilah: mengabaikan tanda kutip tidak hanya menyebabkan kesalahan tetapi juga lubang keamanan .

fedorqui 'JADI berhenti merugikan'
sumber
7

Selain masalah lain yang disebabkan oleh gagal mengutip, -ndan -edapat dikonsumsi echosebagai argumen. (Hanya yang pertama sah menurut spesifikasi POSIX echo, tetapi beberapa implementasi umum melanggar spesifikasi dan konsumsi -ejuga).

Untuk menghindari hal ini, gunakan printfbukan echoketika rincian materi.

Jadi:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

Namun, kutipan yang benar tidak selalu menyelamatkan Anda saat menggunakan echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... sedangkan itu akan menyelamatkan Anda dengan printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n
Charles Duffy
sumber
Yay, kami membutuhkan dedup yang bagus untuk ini! Saya setuju ini cocok dengan judul pertanyaan, tetapi saya tidak berpikir itu akan mendapatkan visibilitas yang layak di sini. Bagaimana dengan pertanyaan baru à la "Mengapa -e/ -n/ garis miring terbalik saya tidak muncul?" Kami dapat menambahkan tautan dari sini yang sesuai.
pria lain itu
Apakah yang Anda maksud konsumsi -njuga ?
PesaThe
1
@PesaThe, tidak, maksudku -e. Standar untuk echotidak menentukan keluaran ketika argumen pertamanya adalah -n, membuat setiap / semua kemungkinan keluaran legal dalam kasus itu; tidak ada ketentuan untuk itu -e.
Charles Duffy
Oh ... Saya tidak bisa membaca. Mari kita salahkan bahasa Inggris saya untuk itu. Terima kasih untuk penjelasannya.
PesaThe
6

pengguna kutip ganda untuk mendapatkan nilai yang tepat. seperti ini:

echo "${var}"

dan itu akan membaca nilai Anda dengan benar.

vanishedzhou
sumber
Bekerja .. Terima kasih
Tshilidzi Mudau
2

echo $varkeluaran sangat tergantung pada nilai IFSvariabel. Secara default berisi spasi, tab, dan karakter baris baru:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Ini berarti bahwa ketika shell melakukan pemisahan bidang (atau pemisahan kata), shell menggunakan semua karakter ini sebagai pemisah kata. Inilah yang terjadi saat mereferensikan variabel tanpa tanda kutip ganda ke echo it ( $var) dan dengan demikian keluaran yang diharapkan diubah.

Salah satu cara untuk mencegah pemisahan kata (selain menggunakan tanda kutip ganda) adalah dengan menyetel IFSke null. Lihat http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Jika nilai IFS adalah null, tidak ada pemisahan bidang yang harus dilakukan.

Menetapkan ke nol berarti menyetel ke nilai kosong:

IFS=

Uji:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 
ks1322
sumber
2
Anda juga harus set -fmencegah globbing
pria lain itu
@thatotherguy, apakah benar-benar perlu untuk contoh pertama Anda dengan perluasan jalur? Dengan IFSdisetel ke nol, echo $varakan diperluas ke echo '/* Foobar is free software */'dan perluasan jalur tidak dilakukan di dalam string bertanda kutip tunggal.
ks1322
1
Iya. Jika Anda mkdir "/this thing called Foobar is free software etc/"melihatnya masih mengembang. Ini jelas lebih praktis sebagai [a-z]contoh.
Orang lain itu
Begitu, ini masuk akal [a-z]misalnya.
ks1322
2

The jawaban dari ks1322 membantu saya untuk mengidentifikasi masalah saat menggunakan docker-compose exec:

Jika Anda menghilangkan -Tbendera, docker-compose exectambahkan karakter khusus yang memutus keluaran, kita akan melihat balih-alih 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

Dengan -Tbendera, docker-compose execberfungsi seperti yang diharapkan:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b
AL
sumber
-2

Selain meletakkan variabel dalam kutipan, seseorang juga bisa menerjemahkan output dari variabel menggunakan trdan mengonversi spasi ke baris baru.

$ echo $var | tr " " "\n"
foo
bar
baz

Meskipun ini sedikit lebih berbelit-belit, ini menambahkan lebih banyak keragaman dengan output karena Anda dapat mengganti karakter apa pun sebagai pemisah antara variabel array.

Alek
sumber
2
Tapi ini mengganti semua spasi menjadi baris baru. Mengutip mempertahankan baris dan spasi baru yang ada.
user000001
Benar iya. Saya kira itu tergantung pada apa yang ada di dalam variabel. Saya sebenarnya menggunakan trcara lain untuk membuat array dari file teks.
Alek
3
Membuat masalah dengan tidak mengutip variabel dengan benar dan kemudian mengatasinya dengan proses ekstra yang sulit bukanlah pemrograman yang baik.
tripleee
@ Alek, ... err, apa? Tidak trperlu membuat array dengan benar / benar dari file teks - Anda dapat menentukan pemisah apa pun yang Anda inginkan dengan mengatur IFS. Misalnya: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')berfungsi sepenuhnya melalui bash 3.2 (versi terlama yang beredar luas), dan dengan benar menyetel status keluar ke false jika Anda catgagal. Dan jika Anda menginginkan, katakanlah, tab sebagai ganti baris baru, Anda cukup mengganti $'\n'dengan $'\t'.
Charles Duffy
1
@Alek, ... jika Anda melakukan sesuatu seperti arrayname=( $( cat file | tr '\n' ' ' ) ), maka itu rusak pada beberapa lapisan: Ini menggumpal hasil Anda (jadi *berubah menjadi daftar file di direktori saat ini), dan itu akan bekerja dengan baik tanpatr ( atau cat, dalam hal ini; seseorang bisa saja menggunakan arrayname=$( $(<file) )dan itu akan rusak dengan cara yang sama, tetapi kurang efisien).
Charles Duffy