Bagaimana cara menyimpan kesalahan standar dalam suatu variabel

182

Katakanlah saya memiliki skrip seperti berikut:

tidak berguna

echo "This Is Error" 1>&2
echo "This Is Output" 

Dan saya punya skrip shell lain:

alsoUseless.sh

./useless.sh | sed 's/Output/Useless/'

Saya ingin menangkap "This Is Error", atau stderr lainnya dari useless.sh, menjadi variabel. Sebut saja ERROR.

Perhatikan bahwa saya menggunakan stdout untuk sesuatu. Saya ingin terus menggunakan stdout, jadi mengarahkan stderr ke stdout tidak membantu, dalam hal ini.

Jadi, pada dasarnya, saya ingin melakukannya

./useless.sh 2> $ERROR | ...

tapi itu jelas tidak berhasil.

Saya juga tahu bahwa saya bisa melakukannya

./useless.sh 2> /tmp/Error
ERROR=`cat /tmp/Error`

tapi itu jelek dan tidak perlu.

Sayangnya, jika tidak ada jawaban yang muncul di sini, itulah yang harus saya lakukan.

Saya berharap ada cara lain.

Adakah yang punya ide yang lebih baik?

psycotica0
sumber
4
Untuk apa tepatnya ingin menggunakan stdout? Apakah Anda hanya ingin melihatnya di konsol? Atau apakah Anda menangkap / mengarahkan output itu? Jika hanya untuk konsol Anda mengarahkan stdout ke konsol dan stderr ke stdout untuk menangkapnya:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Jawaban:

91

Akan lebih rapi untuk menangkap file kesalahan sebagai berikut:

ERROR=$(</tmp/Error)

Shell mengenali ini dan tidak harus menjalankan ' cat' untuk mendapatkan data.

Pertanyaan yang lebih besar sulit. Saya tidak berpikir ada cara mudah untuk melakukannya. Anda harus membangun seluruh pipa ke dalam sub-shell, akhirnya mengirim output standar akhir ke file, sehingga Anda dapat mengarahkan kesalahan ke output standar.

ERROR=$( { ./useless.sh | sed s/Output/Useless/ > outfile; } 2>&1 )

Perhatikan bahwa semi-colon diperlukan (dalam shell klasik - Bourne, Korn - pasti; mungkin di Bash juga). ' {}' Apakah saya / O pengalihan atas perintah terlampir. Seperti yang ditulis, itu akan menangkap kesalahan sedjuga.

PERINGATAN: Kode yang tidak diuji secara formal - gunakan dengan risiko sendiri.

Jonathan Leffler
sumber
1
Aku berharap akan ada trik yang benar-benar gila yang tidak kuketahui, tapi kelihatannya begini. Terima kasih.
psycotica0
9
Jika Anda tidak memerlukan output standar, Anda dapat mengarahkannya ke /dev/nullalih-alih outfile(Jika Anda seperti saya, Anda menemukan pertanyaan ini melalui Google, dan tidak memiliki persyaratan yang sama dengan OP)
Mark Eirich
2
Untuk jawaban tanpa file sementara, lihat di sini .
Tom Hale
1
Berikut adalah cara untuk melakukannya tanpa mengarahkannya ke file; itu bermain dengan menukar stdoutdan stderrmaju dan mundur. Tetapi berhati-hatilah , seperti yang dikatakan di sini : Dalam bash, akan lebih baik untuk tidak berasumsi bahwa file deskriptor 3 tidak digunakan " .
Golar Ramblar
69

alsoUseless.sh

Ini akan memungkinkan Anda untuk mem-pipe output useless.shskrip Anda melalui perintah seperti seddan menyimpan stderrdalam variabel bernama error. Hasil dari pipa dikirim ke stdoutuntuk ditampilkan atau disalurkan ke perintah lain.

Ini mengatur beberapa deskriptor file tambahan untuk mengelola pengalihan yang diperlukan untuk melakukan ini.

#!/bin/bash

exec 3>&1 4>&2 #set up extra file descriptors

error=$( { ./useless.sh | sed 's/Output/Useless/' 2>&4 1>&3; } 2>&1 )

echo "The message is \"${error}.\""

exec 3>&- 4>&- # release the extra file descriptors
Dijeda sampai pemberitahuan lebih lanjut.
sumber
4
Ini adalah teknik yang baik untuk menggunakan 'exec' untuk mengatur dan menutup deskriptor file. Penutupan sebenarnya tidak diperlukan jika skrip keluar segera setelahnya.
Jonathan Leffler
3
Bagaimana saya menangkap keduanya stderrdan stdoutdalam variabel?
Gingi
Luar biasa. Ini membantu saya mengimplementasikan dry_runfungsi yang andal dapat memilih antara menggemakan argumennya dan menjalankannya, terlepas dari apakah perintah yang sedang dijalankan sedang disalurkan ke file lain.
Mihai Danila
1
@ t00bs: readtidak menerima input dari pipa. Anda dapat menggunakan teknik lain untuk mencapai apa yang Anda coba peragakan.
Dijeda sampai pemberitahuan lebih lanjut.
2
Bisa lebih sederhana, dengan: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn
64

Pengarah stderr ke stdout, stdout ke / dev / null, dan kemudian gunakan backticks atau $()untuk menangkap stderr yang diarahkan:

ERROR=$(./useless.sh 2>&1 >/dev/null)
Chas. Owens
sumber
8
Inilah alasan saya memasukkan pipa ke dalam contoh saya. Saya masih menginginkan output standar, dan saya ingin melakukan hal lain, pergi ke tempat lain.
psycotica0
Untuk perintah yang mengirim output hanya ke stderr, cara sederhana untuk menangkapnya adalah, misalnyaPY_VERSION="$(python --version 2>&1)"
John Mark
9

Ada banyak duplikat untuk pertanyaan ini, banyak di antaranya memiliki skenario penggunaan yang sedikit lebih sederhana di mana Anda tidak ingin menangkap stderr dan stdout dan kode keluar semuanya secara bersamaan.

if result=$(useless.sh 2>&1); then
    stdout=$result
else
    rc=$?
    stderr=$result
fi

berfungsi untuk skenario umum di mana Anda mengharapkan output yang tepat jika berhasil, atau pesan diagnostik pada stderr jika gagal.

Perhatikan bahwa pernyataan kontrol shell sudah memeriksa di $?bawah tenda; jadi apa pun yang terlihat seperti

cmd
if [ $? -eq 0 ], then ...

hanyalah cara kikuk, tanpa kata-kata

if cmd; then ...
tripleee
sumber
Ini bekerja untuk saya: my_service_status = $ (layanan my_service status 2> & 1) Terima kasih !!
JRichardsz
6
# command receives its input from stdin.
# command sends its output to stdout.
exec 3>&1
stderr="$(command </dev/stdin 2>&1 1>&3)"
exitcode="${?}"
echo "STDERR: $stderr"
exit ${exitcode}
manusia9
sumber
1
commandadalah pilihan yang buruk di sini, karena sebenarnya ada builtin dengan nama itu. Mungkin membuatnya yourCommandatau semacamnya, agar lebih eksplisit.
Charles Duffy
4

Untuk kepentingan pembaca, resep ini ada di sini

  • dapat digunakan kembali sebagai oneliner untuk menangkap stderr menjadi variabel
  • masih memberikan akses ke kode kembali perintah
  • Mengorbankan deskriptor file sementara 3 (yang tentu saja dapat diubah oleh Anda)
  • Dan tidak mengekspos deskriptor file sementara ini ke perintah dalam

Jika Anda ingin menangkap stderrbeberapa commandke dalam varAnda dapat melakukannya

{ var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;

Setelah itu Anda memiliki semuanya:

echo "command gives $? and stderr '$var'";

Jika commandsederhana (bukan sesuatu seperti a | b) Anda dapat meninggalkan batin {}:

{ var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;

Dibungkus menjadi bashfungsi -mudah yang dapat digunakan kembali (mungkin membutuhkan versi 3 dan di atas untuk local -n):

: catch-stderr var cmd [args..]
catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }

Dijelaskan:

  • local -nalias "$ 1" (yang merupakan variabel untuk catch-stderr)
  • 3>&1 menggunakan file descriptor 3 untuk menyimpan poin stdout di sana
  • { command; } (atau "$ @") kemudian mengeksekusi perintah dalam output capturing $(..)
  • Harap perhatikan bahwa urutan pastinya penting di sini (melakukannya dengan cara yang salah mengacak deskriptor file dengan salah):
    • 2>&1pengalihan stderrke penangkapan output$(..)
    • 1>&3redirect stdoutdari output menangkap $(..)kembali ke "luar" stdoutyang disimpan dalam file descriptor 3. Perhatikan bahwa stderrmasih merujuk ke tempat FD 1 menunjuk sebelumnya: Ke output menangkap$(..)
    • 3>&-kemudian menutup file deskriptor 3 karena tidak diperlukan lagi, sehingga commandtidak tiba-tiba muncul beberapa deskriptor file terbuka yang tidak dikenal. Perhatikan bahwa cangkang luar masih memiliki FD 3 terbuka, tetapi commandtidak akan melihatnya.
    • Yang terakhir ini penting, karena beberapa program suka lvmmengeluh tentang deskriptor file yang tidak terduga. Dan lvmmengeluh stderr- hanya apa yang akan kita tangkap!

Anda dapat menangkap deskriptor file lain dengan resep ini, jika Anda menyesuaikannya. Kecuali deskriptor file 1 tentu saja (di sini logika redirection akan salah, tetapi untuk deskriptor file 1 Anda dapat menggunakan var=$(command)seperti biasa).

Perhatikan bahwa ini deskriptor file pengorbanan 3. Jika Anda membutuhkan deskriptor file itu, jangan ragu untuk mengubah nomornya. Namun perlu diperhatikan, bahwa beberapa kerang (dari tahun 1980-an) dapat dipahami 99>&1sebagai argumen yang 9diikuti 9>&1(ini bukan masalah bagi bash).

Perhatikan juga bahwa tidak mudah untuk membuat FD 3 ini dapat dikonfigurasi melalui variabel. Ini membuat banyak hal menjadi tidak terbaca:

: catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
catch-var-from-fd-by-fd()
{
local -n v="$1";
local fd1="$2" fd2="$3";
shift 3 || return;

eval exec "$fd2>&1";
v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}

Catatan keamanan: 3 argumen pertama yang catch-var-from-fd-by-fdtidak boleh diambil dari pihak ke-3. Selalu berikan mereka secara eksplisit dalam mode "statis".

Jadi tidak-tidak-tidak catch-var-from-fd-by-fd $var $fda $fdb $command, jangan pernah lakukan ini!

Jika Anda memasukkan nama variabel variabel, setidaknya lakukan sebagai berikut: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

Ini masih tidak akan melindungi Anda dari setiap eksploitasi, tetapi setidaknya membantu untuk mendeteksi dan menghindari kesalahan penulisan skrip yang umum.

Catatan:

  • catch-var-from-fd-by-fd var 2 3 cmd.. sama dengan catch-stderr var cmd..
  • shift || returnhanyalah beberapa cara untuk mencegah kesalahan jelek jika Anda lupa untuk memberikan jumlah argumen yang benar. Mungkin menghentikan shell akan menjadi cara lain (tetapi ini membuatnya sulit untuk menguji dari commandline).
  • Rutinitas ditulis sedemikian rupa, sehingga lebih mudah dipahami. Seseorang dapat menulis ulang fungsi sedemikian rupa sehingga tidak perlu exec, tetapi kemudian menjadi sangat jelek.
  • Rutin ini dapat ditulis ulang untuk non bash-juga sehingga tidak perlu local -n. Namun kemudian Anda tidak dapat menggunakan variabel lokal dan itu menjadi sangat jelek!
  • Perhatikan juga bahwa evals digunakan dengan cara yang aman. Biasanya evaldianggap berbahaya. Namun dalam hal ini tidak lebih jahat daripada menggunakan "$@"(untuk menjalankan perintah sewenang-wenang). Namun tolong pastikan untuk menggunakan kutipan yang tepat dan benar seperti yang ditunjukkan di sini (kalau tidak menjadi sangat berbahaya ).
Tino
sumber
3

Begini cara saya melakukannya:

#
# $1 - name of the (global) variable where the contents of stderr will be stored
# $2 - command to be executed
#
captureStderr()
{
    local tmpFile=$(mktemp)

    $2 2> $tmpFile

    eval "$1=$(< $tmpFile)"

    rm $tmpFile
}

Contoh penggunaan:

captureStderr err "./useless.sh"

echo -$err-

Ini tidak menggunakan file sementara. Tapi setidaknya barang jelek dibungkus dalam suatu fungsi.

tfga
sumber
@ShadowWizard Sedikit keraguan di pihak saya. Di Prancis, usus besar biasanya didahului oleh spasi. Saya keliru menerapkan aturan yang sama dengan jawaban bahasa Inggris . Setelah memeriksa ini , saya tahu saya tidak akan membuat kesalahan ini lagi.
Stephan
@Stephan bersorak, ini juga sudah dibahas di sini . :)
Shadow Wizard adalah Ear For You
1
Ada cara yang lebih aman untuk melakukan ini daripada menggunakan eval. Misalnya, printf -v "$1" '%s' "$(<tmpFile)"jangan mengambil risiko menjalankan kode arbitrer jika TMPDIRvariabel Anda telah disetel ke nilai berbahaya (atau nama variabel tujuan Anda berisi nilai seperti itu).
Charles Duffy
1
Demikian pula, rm -- "$tmpFile"lebih kuat daripada rm $tmpFile.
Charles Duffy
2

Ini adalah masalah menarik yang saya harap ada solusi yang elegan. Sayangnya, saya berakhir dengan solusi yang mirip dengan Mr. Leffler, tetapi saya akan menambahkan bahwa Anda dapat memanggil tidak berguna dari dalam fungsi Bash untuk meningkatkan keterbacaan:

#! / bin / bash

berfungsi tidak berguna {
    /tmp/useless.sh | sed 's / Output / Berguna /'
}

GALAT = $ (tidak berguna)
echo $ ERROR

Semua jenis pengalihan output lainnya harus didukung oleh file sementara.


sumber
2

POSIX

STDERR dapat ditangkap dengan beberapa sihir redirection:

$ { error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&3 ; } 2>&1); } 3>&1
lrwxrwxrwx 1 rZZt rZZt 7 Aug 22 15:44 /bin -> usr/bin/

$ echo $error
ls: cannot access '/XXXX': No such file or directory

Perhatikan bahwa pemipaan STDOUT dari perintah (di sini ls) dilakukan di bagian terdalam { }. Jika Anda menjalankan perintah sederhana (mis., Bukan pipa), Anda bisa menghapus kawat gigi bagian dalam ini.

Anda tidak bisa mem-pipe di luar perintah karena pemipaan membuat subshell di bashdan zsh, dan penugasan ke variabel dalam subshell tidak akan tersedia untuk shell saat ini.

pesta

Di bash, akan lebih baik untuk tidak menganggap bahwa file deskriptor 3 tidak digunakan:

{ error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1; 
exec {tmp}>&-  # With this syntax the FD stays open

Perhatikan bahwa ini tidak berhasil zsh.


Berkat jawaban ini untuk ide umum.

Tom Hale
sumber
dapatkah Anda menjelaskan baris ini dengan detail? tidak mengerti 1> & $ tmp; {error = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Thiago Conrado
1

Posting ini membantu saya menghasilkan solusi serupa untuk tujuan saya sendiri:

MESSAGE=`{ echo $ERROR_MESSAGE | format_logs.py --level=ERROR; } 2>&1`

Kemudian selama PESAN kami bukan string kosong, kami meneruskannya ke hal-hal lain. Ini akan memberi tahu kami jika format_logs.py kami gagal dengan semacam pengecualian python.

palmbardier
sumber
1

Ambil dan cetak stderr

ERROR=$( ./useless.sh 3>&1 1>&2 2>&3 | tee /dev/fd/2 )

Kerusakan

Anda dapat menggunakan $()untuk menangkap stdout, tetapi Anda ingin menangkap stderr. Jadi, Anda menukar stdout dan stderr. Menggunakan fd 3 sebagai penyimpanan sementara dalam algoritma swap standar.

Jika Anda ingin menangkap DAN mencetak gunakan teeuntuk membuat duplikat. Dalam hal ini output teeakan ditangkap $()daripada pergi ke konsol, tetapi stderr (dari tee) masih akan pergi ke konsol sehingga kami menggunakannya sebagai output kedua untuk teemelalui file khusus /dev/fd/2karena teemengharapkan path file daripada fd jumlah.

CATATAN: Itu adalah banyak sekali pengalihan dalam satu baris dan urutan penting. $()adalah meraih stdout teepada akhir pipa dan pipa itu sendiri mengarahkan stdout ./useless.shke stdin teeSETELAH kami bertukar stdin dan stdout untuk ./useless.sh.

Menggunakan stdout dari ./useless.sh

OP mengatakan dia masih ingin menggunakan (bukan hanya mencetak) stdout, seperti ./useless.sh | sed 's/Output/Useless/'.

Tidak masalah lakukan saja SEBELUM menukar stdout dan stderr. Saya sarankan untuk memindahkannya ke fungsi atau file (juga-useless.sh) dan memanggilnya sebagai ganti ./useless.sh pada baris di atas.

Namun, jika Anda ingin MENGAMBIL stdout DAN stderr, maka saya pikir Anda harus kembali pada file sementara karena $()hanya akan melakukan satu per satu dan itu membuat subkulit dari mana Anda tidak dapat mengembalikan variabel.

SensorSmith
sumber
1

Mengulang sedikit pada jawaban Tom Hale, saya merasa mungkin untuk membungkus yoga redirection ke dalam suatu fungsi agar lebih mudah digunakan kembali. Sebagai contoh:

#!/bin/sh

capture () {
    { captured=$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
}

# Example usage; capturing dialog's output without resorting to temp files
# was what motivated me to search for this particular SO question
capture dialog --menu "Pick one!" 0 0 0 \
        "FOO" "Foo" \
        "BAR" "Bar" \
        "BAZ" "Baz"
choice=$captured

clear; echo $choice

Hampir bisa dipastikan untuk menyederhanakan ini lebih lanjut. Belum diuji secara menyeluruh-menyeluruh, tetapi tampaknya berhasil dengan bash dan ksh.

YellowApple
sumber
0

Jika Anda ingin memotong penggunaan file sementara Anda mungkin dapat menggunakan substitusi proses. Saya belum membuatnya bekerja. Ini adalah upaya pertama saya:

$ .useless.sh 2> >( ERROR=$(<) )
-bash: command substitution: line 42: syntax error near unexpected token `)'
-bash: command substitution: line 42: `<)'

Lalu saya mencoba

$ ./useless.sh 2> >( ERROR=$( cat <() )  )
This Is Output
$ echo $ERROR   # $ERROR is empty

Namun

$ ./useless.sh 2> >( cat <() > asdf.txt )
This Is Output
$ cat asdf.txt
This Is Error

Jadi proses substitusi secara umum melakukan hal yang benar ... sayangnya, setiap kali saya membungkus STDIN >( )dengan sesuatu dalam $()upaya untuk menangkap itu ke suatu variabel, saya kehilangan konten $(). Saya pikir ini karena $()meluncurkan sub proses yang tidak lagi memiliki akses ke file descriptor di / dev / fd yang dimiliki oleh proses induk.

Substitusi proses telah memberi saya kemampuan untuk bekerja dengan aliran data yang tidak lagi di STDERR, sayangnya saya sepertinya tidak dapat memanipulasinya seperti yang saya inginkan.

Barton Chittenden
sumber
1
Jika Anda melakukannya ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" )maka Anda akan melihat output dari ERROR. Masalahnya adalah bahwa proses substitusi dijalankan dalam sub-shell, sehingga nilai yang ditetapkan dalam sub-shell tidak mempengaruhi shell induk.
Jonathan Leffler
0
$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr
Mario Wolff
sumber
3
Ini sepertinya ide yang bagus, tetapi pada Mac OSX 10.8.5, ia mencetaka=> b=>stderr
Heath Borders
3
Saya setuju dengan @HeathBorders; ini tidak menghasilkan output yang ditunjukkan. Masalahnya di sini adalah bahwa adievaluasi dan ditetapkan dalam sub-shell, dan penugasan dalam sub-shell tidak mempengaruhi shell induk. (Diuji pada Ubuntu 14.04 LTS dan juga Mac OS X 10.10.1.)
Jonathan Leffler
Sama di Windows GitBash. Jadi, itu tidak berhasil. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby
Tidak bekerja dengan SLE 11.4baik dan menghasilkan efek yang dijelaskan oleh @JonathanLeffler
smarber
Sementara kode ini dapat menjawab pertanyaan, memberikan konteks tambahan tentang mengapa dan / atau bagaimana kode ini menjawab pertanyaan meningkatkan nilai jangka panjangnya.
β.εηοιτ.βε
0

Dalam zsh:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )
Ray Andrews
sumber
0

Untuk kesalahan membuktikan perintah Anda:

execute [INVOKING-FUNCTION] [COMMAND]

execute () {
    function="${1}"
    command="${2}"
    error=$(eval "${command}" 2>&1 >"/dev/null")

    if [ ${?} -ne 0 ]; then
        echo "${function}: ${error}"
        exit 1
    fi
}

Terinspirasi dalam Lean manufacturing:

Alberto Salvia Novella
sumber
Solusi idiomatis adalah toutut tugas di dalam if. Biarkan saya memposting solusi terpisah.
tripleee
0

Solusi sederhana

{ ERROR=$(./useless.sh 2>&1 1>&$out); } {out}>&1
echo "-"
echo $ERROR

Akan menghasilkan:

This Is Output
-
This Is Error
K - Toksisitas dalam SO meningkat.
sumber
0

Memperbaiki jawaban YellowApple :

Ini adalah fungsi Bash untuk menangkap stderr ke dalam variabel apa pun

stderr_capture_example.sh:

#!/usr/bin/env bash

# Capture stderr from a command to a variable while maintaining stdout
# @Args:
# $1: The variable name to store the stderr output
# $2: Vararg command and arguments
# @Return:
# The Command's Returnn-Code or 2 if missing arguments
function capture_stderr {
  [ $# -lt 2 ] && return 2
  local stderr="$1"
  shift
  {
    printf -v "$stderr" '%s' "$({ "$@" 1>&3; } 2>&1)"
  } 3>&1
}

# Testing with a call to erroring ls
LANG=C capture_stderr my_stderr ls "$0" ''

printf '\nmy_stderr contains:\n%s' "$my_stderr"

Pengujian:

bash stderr_capture_example.sh

Keluaran:

 stderr_capture_example.sh

my_stderr contains:
ls: cannot access '': No such file or directory

Fungsi ini dapat digunakan untuk menangkap pilihan dialogperintah yang dikembalikan .

Léa Gris
sumber