shell: tetap mengikuti baris baru ('\ n') di substitusi perintah

14

Saya ingin dapat menangkap output yang tepat dari substitusi perintah, termasuk karakter baris baru yang tertinggal .

Saya menyadari bahwa mereka dilucuti secara default, jadi beberapa manipulasi mungkin diperlukan untuk menyimpannya, dan saya ingin menyimpan kode keluar yang asli .

Misalnya, diberi perintah dengan sejumlah variabel baris baru dan kode keluar:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

Saya ingin menjalankan sesuatu seperti:

exact_output f

Dan hasilnya adalah:

Output: $'\n\n'
Exit: 5

Saya tertarik pada keduanya bashdan POSIX sh.

Tom Hale
sumber
1
Newline adalah bagian dari $IFS, sehingga tidak akan ditangkap sebagai argumen.
Deathgrip
4
@Deathgrip Ini tidak ada hubungannya dengan IFS(coba ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). Hanya baris baru dilepaskan. \tDan `` tidak, dan IFStidak mempengaruhinya.
PSkocik
Lihat juga: tcsh menjaga baris baru dalam subtitusi perintah `...` untuktcsh
Stéphane Chazelas

Jawaban:

17

Kerang POSIX

Biasa ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Trik ) untuk mendapatkan stdout lengkap dari sebuah perintah adalah dengan melakukan:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

Idenya adalah menambah dan ekstra .\n. Substitusi perintah hanya akan menghapus itu \n . Dan Anda menghapusnya. dengan ${output%.}.

Perhatikan bahwa dalam cangkang selain zsh , itu masih tidak akan berfungsi jika output memiliki byte NUL. Dengan yash, itu tidak akan berfungsi jika outputnya bukan teks.

Perhatikan juga bahwa di beberapa lokal, penting karakter apa yang Anda gunakan untuk memasukkan di akhir. .umumnya harus baik-baik saja, tetapi yang lain mungkin tidak. Misalnya x(seperti yang digunakan dalam beberapa jawaban lain) atau @tidak akan berfungsi di lokal menggunakan rangkaian karakter BIG5, GB18030 atau BIG5HKSCS. Dalam rangkaian karakter tersebut, penyandian sejumlah karakter berakhir dengan byte yang sama dengan penyandian xatau @(0x78, 0x40)

Misalnya, ūdalam BIG5HKSCS adalah 0x88 0x78 (dan x0x78 seperti di ASCII, semua rangkaian karakter pada sistem harus memiliki penyandian yang sama untuk semua karakter set karakter portabel yang mencakup huruf bahasa Inggris, @dan .). Jadi jika cmditu printf '\x88'dan kita masukkan xsetelah itu, ${output%x}akan gagal untuk menghapusnya xsebagai$output sebenarnya mengandung ū.

Penggunaan .sebaliknya dapat menyebabkan masalah yang sama secara teori jika ada karakter yang pengkodeannya berakhir dengan pengkodean yang sama dengan. , tetapi karena telah memeriksa beberapa waktu yang lalu, saya dapat mengatakan bahwa tidak ada rangkaian karakter yang mungkin tersedia untuk digunakan di lokal di sistem Debian, FreeBSD, atau Solaris memiliki karakter seperti itu yang cukup baik untuk saya (dan mengapa saya memilih .yang juga merupakan simbol untuk menandai akhir kalimat dalam bahasa Inggris sehingga tampaknya sesuai).

Pendekatan yang lebih tepat seperti yang dibahas oleh @Arrow adalah mengubah lokal menjadi C hanya untuk pengupasan karakter terakhir (${output%.} ) yang akan memastikan hanya satu byte dilucuti, tetapi itu akan menyulitkan kode secara signifikan dan berpotensi menimbulkan masalah kompatibilitas dari itu sendiri.

alternatif bash / zsh

Dengan bashdan zsh, dengan asumsi output tidak memiliki NUL, Anda juga dapat melakukan:

IFS= read -rd '' output < <(cmd)

Untuk mendapatkan status keluar dari cmd, Anda dapat melakukan wait "$!"; ret=$?di bashtetapi tidak dalam zsh.

rc / es / akanaga

Untuk kelengkapan, perhatikan bahwa rc/ es/ akangaada operator untuk itu. Di dalamnya, substitusi perintah, dinyatakan sebagai `cmd(atau `{cmd}untuk perintah yang lebih kompleks) mengembalikan daftar (dengan memisahkan $ifs, spasi-tab-baris baru secara default). Dalam cangkang tersebut (berbeda dengan cangkang mirip Bourne), pengupasan baris baru hanya dilakukan sebagai bagian dari $ifspemisahan itu. Jadi Anda bisa mengosongkan $ifsatau menggunakan ``(seps){cmd}formulir tempat Anda menentukan pemisah:

ifs = ''; output = `cmd

atau:

output = ``()cmd

Bagaimanapun, status keluar dari perintah hilang. Anda harus menanamkannya di output dan mengekstraknya setelah itu yang akan menjadi jelek.

ikan

Dalam ikan, substitusi perintah adalah dengan (cmd)dan tidak melibatkan subkulit.

set var (cmd)

Menciptakan $vararray dengan semua baris dalam output cmdif $IFStidak kosong, atau dengan output cmddilucuti hingga satu (sebagai lawan dari semua di kebanyakan shell) karakter baris baru jika$IFS kosong.

Jadi masih ada masalah dalam hal itu (printf 'a\nb')dan (printf 'a\nb\n')berkembang ke hal yang sama bahkan dengan yang kosong$IFS .

Untuk mengatasinya, yang terbaik yang bisa saya pikirkan adalah:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

Alternatifnya adalah dengan melakukan:

read -z output < (begin; cmd; set ret $status; end | psub)

Shell Bourne

Shell Bourne tidak mendukung $(...)bentuk atau ${var%pattern}operator, sehingga sangat sulit untuk mencapai di sana. Salah satu pendekatan adalah menggunakan eval dan mengutip:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

Di sini, kami menghasilkan

output='output of cmd
with the single quotes escaped as '\''
';ret=X

untuk diteruskan ke eval. Adapun pendekatan POSIX, jika 'salah satu karakter yang pengkodeannya dapat ditemukan di akhir karakter lain, kita akan memiliki masalah (yang jauh lebih buruk karena akan menjadi kerentanan injeksi perintah), tapi untungnya, seperti ., itu bukan salah satu dari itu, dan teknik mengutip umumnya yang digunakan oleh apa pun yang mengutip kode shell (catatan yang \memiliki masalah, jadi tidak boleh digunakan (juga tidak termasuk "..."di dalamnya Anda perlu menggunakan garis miring terbalik untuk beberapa karakter) Di sini, kami hanya menggunakannya setelah 'yang OK).

tcsh

Lihat tcsh mempertahankan baris baru dalam substitusi perintah `...`

(tidak menjaga status keluar, yang dapat Anda atasi dengan menyimpannya dalam file sementara ( echo $status > $tempfile:qsetelah perintah))

Stéphane Chazelas
sumber
Terima kasih - dan terutama untuk petunjuk tentang rangkaian karakter yang berbeda. Jika zshbisa menyimpan NULdalam variabel, mengapa tidak IFS= read -rd '' output < <(cmd)berhasil? Itu harus dapat menyimpan panjang string ... apakah itu dikodekan ''sebagai string 1-byte \0daripada string 0-byte?
Tom Hale
1
@ TomHale, ya, read -d ''diperlakukan sebagai read -d $'\0'( bashmeskipun juga $'\0'sama di ''mana - mana).
Stéphane Chazelas
Anda menggabungkan karakter dan byte. Harap mengerti bahwa jika kami menghapus apa yang ditambahkan, entitas asli tidak boleh berubah. Ini tidak sulit untuk menghapus satu byte yang disebut xjika itu yang ditambahkan. Silakan lihat jawaban saya yang diedit.
Panah
@Arrow, ya var=value command evaltriknya sudah dibahas di sini ( juga ) dan di milis austin-grup sebelumnya. Anda akan menemukan itu tidak portabel (dan sangat jelas ketika Anda mencoba hal-hal seperti a=1 command eval 'unset a; a=2'atau lebih buruk bahwa itu tidak dimaksudkan untuk digunakan seperti itu). Sama untuk savedVAR=$VAR;...;VAR=$savedVARyang tidak melakukan apa yang Anda inginkan ketika $VARawalnya tidak disetel. Jika itu hanya untuk mengatasi masalah teoretis saja (bug yang tidak dapat dipukul dalam praktiknya), IMO, itu tidak layak untuk diganggu. Tetap saja, saya akan mendukung Anda untuk mencoba.
Stéphane Chazelas
Apakah Anda memiliki tautan ke tempat Anda membuang diskus dan akhirnya membuang penggunaan LANG=Cuntuk menghapus byte dari sebuah string? Anda mengemukakan kekhawatiran di sekitar titik nyata, semua mudah dipecahkan. (1) tidak ada yang tidak disetel yang digunakan (2) Uji variabel sebelum mengubahnya. @ StéphaneChazelas
Arrow
3

Untuk pertanyaan baru, skrip ini berfungsi:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

Pada eksekusi:

Output:$'\n\n\n'
Exit :25
Done

Deskripsi yang lebih panjang

Kebijaksanaan biasa untuk kerang POSIX untuk menangani penghapusan \n adalah:

tambahkan sebuah x

s=$(printf "%s" "${1}x"); s=${s%?}

Itu diperlukan karena baris baru terakhir ( S ) dihapus oleh perintah ekspansi per spesifikasi POSIX :

menghapus urutan satu atau lebih karakter di akhir substitusi.


Tentang trailing x.

Telah dikatakan dalam pertanyaan ini bahwa sebuah x dapat dikacaukan dengan byte trailing dari beberapa karakter dalam beberapa pengkodean. Tetapi bagaimana kita akan menebak karakter apa atau yang lebih baik dalam suatu bahasa dalam beberapa penyandian yang mungkin, itu adalah proposisi yang sulit, untuk sedikitnya.

Namun; Itu tidak benar .

Satu-satunya aturan yang perlu kita ikuti adalah menambahkan dengan tepat apa yang kita hapus.

Seharusnya mudah dipahami bahwa jika kita menambahkan sesuatu ke string yang sudah ada (atau urutan byte) dan kemudian kita menghapus sesuatu yang persis sama, string asli (atau urutan byte) harus sama.

Di mana kita salah? Ketika kita mencampur karakter dan byte .

Jika kita menambahkan byte, kita harus menghapus byte, jika kita menambahkan karakter, kita harus menghapus karakter yang sama persis .

Opsi kedua, menambahkan karakter (dan kemudian menghapus karakter yang sama persis) dapat menjadi berbelit-belit dan kompleks, dan, ya, halaman kode dan penyandian mungkin menghalangi.

Namun, opsi pertama sangat mungkin, dan, setelah menjelaskannya, itu akan menjadi sederhana.

Mari kita tambahkan byte, byte ASCII (<127), dan untuk menjaga hal-hal sesederhana mungkin, katakanlah karakter ASCII dalam kisaran az. Atau seperti yang seharusnya kita katakan, byte dalam kisaran hex 0x61- 0x7a. Mari kita pilih salah satunya, mungkin x (benar-benar nilai byte 0x78). Kita dapat menambahkan byte tersebut dengan menggabungkan x ke sebuah string (mari kita asumsikan sebuah é):

$ a
$ b=${a}x

Jika kita melihat string sebagai urutan byte, kita melihat:

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

Urutan string yang berakhiran x.

Jika kita menghapus x itu (nilai byte 0x78), kita mendapatkan:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

Ini bekerja tanpa masalah.

Contoh yang sedikit lebih sulit.

Katakanlah bahwa string yang kita minati diakhiri dengan byte 0xc3:

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

Dan mari kita tambahkan satu byte nilai 0xa9

$ b=$a$'\xa9'

String telah menjadi ini sekarang:

$ echo "$b"
a test string é

Tepat seperti yang saya inginkan, dua byte terakhir adalah satu karakter di utf8 (sehingga siapa pun dapat mereproduksi hasil ini di konsol utf8 mereka).

Jika kita menghapus karakter, string asli akan berubah. Tapi bukan itu yang kami tambahkan, kami menambahkan nilai byte, yang kebetulan ditulis sebagai x, tetapi byte tetap.

Yang perlu kita hindari salah mengartikan byte sebagai karakter. Yang kami butuhkan adalah tindakan yang menghapus byte yang kami gunakan 0xa9. Bahkan, abu, bash, lksh, dan mksh semuanya tampaknya melakukan hal itu:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

Tapi bukan ksh atau zsh.

Namun, itu sangat mudah dipecahkan, mari beri tahu semua shell untuk melakukan penghapusan byte:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

itu saja, semua kerang yang diuji bekerja (kecuali yash) (untuk bagian terakhir dari string):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

Sederhananya, beri tahu shell untuk menghapus karakter LC_ALL = C, yang persis satu byte untuk semua nilai byte dari 0x00ke 0xff.

Solusi untuk komentar:

Sebagai contoh yang dibahas dalam komentar, satu solusi yang mungkin (yang gagal dalam zsh) adalah:

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

Itu akan menghapus masalah pengkodean.

Panah
sumber
Senang mengetahui bahwa lebih dari satu baris tambahan dapat dihapus.
Tom Hale
Saya setuju bahwa memperbaiki lokal ke C untuk memastikan ${var%?}selalu menghapus satu byte lebih benar secara teori, tetapi: 1- LC_ALLdanLC_CTYPE menimpa $LANG, jadi Anda harus mengatur LC_ALL=C2- Anda tidak dapat melakukan var=${var%?}dalam subkulit seperti perubahan akan hilang, jadi Anda harus menyimpan dan mengembalikan nilai dan status LC_ALL(atau menggunakan localfitur lingkup non-POSIX ) 3- mengubah lokal di tengah-tengah skrip tidak sepenuhnya didukung di beberapa shell seperti yash. Di sisi lain, dalam praktiknya .tidak pernah menjadi masalah di rangkaian karakter kehidupan nyata, jadi menggunakannya tidak akan bergaul dengan LC_ALL.
Stéphane Chazelas
2

Anda dapat menampilkan karakter setelah output normal dan kemudian menghapusnya:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

Ini adalah solusi yang sesuai dengan POSIX.

PSkocik
sumber
Berdasarkan tanggapan, saya melihat pertanyaan saya tidak jelas. Saya baru saja memperbaruinya.
Tom Hale