Substitusi perintah: pemisahan pada baris baru tetapi tidak pada ruang

30

Saya tahu saya bisa menyelesaikan masalah ini dengan beberapa cara, tapi saya bertanya-tanya apakah ada cara untuk melakukannya hanya dengan menggunakan bash built-in, dan jika tidak, apa cara paling efisien untuk melakukannya.

Saya punya file dengan konten seperti

AAA
B C DDD
FOO BAR

maksud saya hanya memiliki beberapa baris dan setiap baris mungkin atau mungkin tidak memiliki spasi. Saya ingin menjalankan perintah seperti

cmd AAA "B C DDD" "FOO BAR"

Jika saya menggunakan cmd $(< file)saya mengerti

cmd AAA B C DDD FOO BAR

dan jika saya menggunakan cmd "$(< file)"saya dapatkan

cmd "AAA B C DDD FOO BAR"

Bagaimana cara agar setiap baris diperlakukan tepat satu parameter?

Pro tua
sumber

Jawaban:

26

Mudah dibawa:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

Atau menggunakan subkulit untuk membuat IFSdan opsi mengubah lokal:

( set -f; IFS='
'; exec cmd $(cat <file) )

Shell melakukan pemisahan bidang dan pembuatan nama file pada hasil dari variabel atau perintah substitusi yang tidak dalam tanda kutip ganda. Jadi, Anda perlu mematikan pembuatan nama file set -f, dan mengonfigurasi pemisahan bidang dengan IFShanya membuat baris baru bidang terpisah.

Tidak banyak yang bisa diperoleh dengan konstruksi bash atau ksh. Anda dapat membuat IFSlokal ke suatu fungsi, tetapi tidak set -f.

Di bash atau ksh93, Anda bisa menyimpan bidang dalam array, jika Anda harus meneruskannya ke beberapa perintah. Anda perlu mengontrol ekspansi pada saat Anda membangun array. Kemudian "${a[@]}"mengembang ke elemen array, satu per kata.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"
Gilles 'SANGAT berhenti menjadi jahat'
sumber
10

Anda bisa melakukan ini dengan array sementara.

Mendirikan:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Isi array:

$ IFS=$'\n'; set -f; foo=($(<input))

Gunakan array:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

Tidak dapat menemukan cara untuk melakukan itu tanpa variabel sementara - kecuali jika IFSperubahan itu tidak penting cmd, dalam hal ini:

$ IFS=$'\n'; set -f; cmd $(<input) 

harus melakukannya.

Tikar
sumber
IFSselalu membuatku bingung. IFS=$'\n' cmd $(<input)tidak bekerja IFS=$'\n'; cmd $(<input); unset IFStidak bekerja. Mengapa? Saya kira saya akan menggunakan(IFS=$'\n'; cmd $(<input))
Old Pro
6
@ OldPro IFS=$'\n' cmd $(<input)tidak berfungsi karena hanya diset IFSdi lingkungan cmd. $(<input)diperluas untuk membentuk perintah, sebelum penugasan ke IFSdilakukan.
Gilles 'SANGAT berhenti menjadi jahat'
8

Sepertinya cara kanonik untuk melakukan ini bashadalah sesuatu seperti

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

atau, jika versi bash Anda memiliki mapfile:

mapfile -t args < filename
cmd "${args[@]}"

Satu-satunya perbedaan yang dapat saya temukan antara mapfile dan loop sambil membaca versus satu-liner

(set -f; IFS=$'\n'; cmd $(<file))

adalah bahwa yang pertama akan mengonversi baris kosong ke argumen kosong, sedangkan satu-baris akan mengabaikan baris kosong. Dalam hal ini perilaku one-liner adalah apa yang saya inginkan, jadi bonus dua kali lipat menjadi kompak.

Saya akan menggunakan IFS=$'\n' cmd $(<file)tetapi tidak berhasil, karena $(<file)ditafsirkan untuk membentuk baris perintah sebelum IFS=$'\n'mulai berlaku.

Meskipun tidak bekerja dalam kasus saya, saya sekarang sudah belajar bahwa banyak alat mendukung mengakhiri baris dengan null (\000)bukannya newline (\n)yang tidak membuat banyak ini lebih mudah ketika berhadapan dengan, katakanlah, nama file, yang merupakan sumber umum dari situasi ini :

find / -name '*.config' -print0 | xargs -0 md5

feed daftar nama file yang sepenuhnya memenuhi syarat sebagai argumen untuk MD5 tanpa menggumpal atau interpolasi atau apa pun. Itu mengarah pada solusi non-built-in

tr "\n" "\000" <file | xargs -0 cmd

Meskipun ini, juga, mengabaikan garis kosong, meskipun tidak menangkap garis yang hanya memiliki spasi putih.

Pro tua
sumber
Menggunakan cmd $(<file)nilai tanpa mengutip (menggunakan kemampuan bash untuk membagi kata) selalu merupakan taruhan yang berisiko. Jika ada garis *itu akan diperluas oleh shell ke daftar file.
3

Anda bisa menggunakan bash built-in mapfileuntuk membaca file menjadi array

mapfile -t foo < filename
cmd "${foo[@]}"

atau, yang belum diuji, xargsmungkin melakukannya

xargs cmd < filename
glenn jackman
sumber
Dari dokumentasi mapfile: "mapfile bukan fitur shell yang umum atau portabel". Dan memang itu tidak didukung pada sistem saya. xargsjuga tidak membantu.
Old Pro
Anda akan membutuhkan xargs -datauxargs -L
James Youngman
@ James, tidak, saya tidak punya -dpilihan dan xargs -L 1menjalankan perintah sekali per baris tetapi masih membagi argumen di spasi putih.
Old Pro
1
@ OldPro, yah Anda memang meminta "cara untuk melakukannya hanya dengan menggunakan bash built-in" alih-alih "fitur shell umum atau portabel". Jika versi bash Anda terlalu lama, dapatkah Anda memperbaruinya?
glenn jackman
mapfilesangat berguna bagi saya, karena mengambil baris kosong sebagai item array, yang IFSmetode ini tidak lakukan. IFSmemperlakukan baris baru yang berdekatan sebagai pembatas tunggal ... Terima kasih telah menyajikannya, karena saya tidak mengetahui perintah (meskipun, berdasarkan pada data input OP dan baris perintah yang diharapkan, tampaknya ia benar-benar ingin mengabaikan baris kosong).
Peter.O
0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

Cara terbaik yang bisa saya temukan. Hanya bekerja.

Rahul Bali
sumber
Dan mengapa itu bekerja jika Anda mengatur IFSruang, tetapi pertanyaannya adalah untuk tidak membagi ruang?
RalfFriedl
0

Mengajukan

Loop paling dasar (portabel) untuk membagi file pada baris baru adalah:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Yang akan dicetak:

$ ./script
<AAA><A B C><DE F>

Ya, dengan IFS = standar spacetabnewline.

Mengapa ini berhasil?

  • IFS akan digunakan oleh shell untuk membagi input menjadi beberapa variabel. Karena hanya ada satu variabel, tidak ada pemisahan yang dilakukan oleh shell. Jadi, tidak ada perubahan yang IFSdibutuhkan.
  • Ya, spasi / tab depan dan belakang dihapus, tetapi sepertinya tidak menjadi masalah dalam kasus ini.
  • Tidak, tidak ada globbing dilakukan karena tidak ada ekspansi yang dikutip . Jadi, tidak set -fperlu.
  • Satu-satunya array yang digunakan (atau diperlukan) adalah parameter posisi seperti array.
  • Opsi -r(mentah) adalah untuk menghindari penghapusan sebagian backslash.

Itu tidak akan berhasil jika membelah dan / atau menggumpal diperlukan. Dalam kasus seperti itu dibutuhkan struktur yang lebih kompleks.

Jika Anda perlu (masih portabel) untuk:

  • Hindari menghilangkan spasi / tab depan dan belakang, gunakan: IFS= read -r line
  • Garis split untuk vars pada beberapa karakter, gunakan: IFS=':' read -r a b c.

Membagi file pada beberapa karakter lain (tidak portabel, berfungsi dengan ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

Ekspansi

Tentu saja, judul pertanyaan Anda adalah tentang memisahkan eksekusi perintah pada baris baru untuk menghindari pemisahan spasi.

Satu-satunya cara untuk mendapatkan pemisahan dari shell adalah meninggalkan ekspansi tanpa tanda kutip:

echo $(< file)

Itu dikontrol oleh nilai IFS, dan, pada ekspansi yang tidak dikutip, globbing juga diterapkan. Untuk membuat itu berhasil, Anda perlu:

  • Atur IFS ke baris baru saja , untuk mendapatkan pemisahan pada baris baru saja.
  • Hapus pilihan shell globbing set +f:

    set + f IFS = '' cmd $ (<file)

Tentu saja, itu mengubah nilai IFS dan globbing untuk sisa skrip.

Ishak
sumber