membangun perintah dengan menyatukan string dalam bash

13

Saya memiliki skrip bash yang membangun baris perintah dalam string berdasarkan pada beberapa parameter sebelum menjalankannya dalam sekali jalan. Bagian-bagian yang digabungkan ke string perintah seharusnya dipisahkan oleh pipa untuk memfasilitasi "streaming" data melalui masing-masing komponen.

Contoh yang sangat sederhana:

#!/bin/bash
part1=gzip -c
part2=some_other_command
cmd="cat infile"

if [ ! "$part1" = "" ]
then
    cmd+=" | $part1"
fi


if [ ! "$part2" = "" ]
then
    cmd+=" | $part2"
fi


cmd+="> outfile"
#show command. It looks ok
echo $cmd
#run the command. fails with pipes
$cmd

Untuk beberapa alasan, pipa-pipa itu sepertinya tidak berfungsi. Ketika saya menjalankan skrip ini saya mendapatkan pesan kesalahan yang berbeda yang biasanya berkaitan dengan bagian pertama dari perintah (sebelum pipa pertama).

Jadi pertanyaan saya adalah apakah mungkin untuk membangun perintah dengan cara ini, dan apa cara terbaik untuk melakukannya?

Lennart Rolland
sumber
Apa pesan kesalahannya?
CameronNemo
Dalam skrip saya (yang agak lebih kompleks daripada penyederhanaan ini) saya mendapatkan "file tidak ditemukan"
Lennart Rolland
Apakah aman untuk menganggap infileada di direktori saat ini?
saiarcot895
Iya. dalam kode saya itu adalah wget -O - bukan sebuah file. Sebenarnya, jika saya hanya menyalin string bersambung dan pasete di terminal itu berfungsi dengan baik
Lennart Rolland

Jawaban:

17

Itu semua tergantung pada saat hal-hal dievaluasi. Saat Anda mengetik $cmd, seluruh baris lainnya dilewatkan sebagai argumen untuk kata pertama di $cmd.

walt@spong:~(0)$ a="cat /etc/passwd"
walt@spong:~(0)$ b="| wc -l"
walt@spong:~(0)$ c="$a $b"
walt@spong:~(0)$ echo $c
cat /etc/passwd | wc -l
walt@spong:~(0)$ $c
cat: invalid option -- 'l'
Try 'cat --help' for more information.
walt@spong:~(1)$ eval $c
62
walt@spong:~(0)$ a="echo /etc/passwd"
walt@spong:~(0)$ c="$a $b"
walt@spong:~(0)$ echo $c
echo /etc/passwd | wc -l
walt@spong:~(0)$ $c
/etc/passwd | wc -l
walt@spong:~(0)$ $c |od -bc
0000000 057 145 164 143 057 160 141 163 163 167 144 040 174 040 167 143  
          /   e   t   c   /   p   a   s   s   w   d       |       w   c  
0000020 040 055 154 012  
              -   l  \n  
0000024
walt@spong:~(0)$ eval $c
1  

Ini menunjukkan bahwa argumen yang diteruskan ke echoperintah adalah: " /etc/passwd", " |" (karakter bilah vertikal), " wc" dan " -l".

Dari man bash:

eval [arg ...]  
    The  args  are read and concatenated together into   
    a single command.  This command is then read and  
    executed by the shell, and its exit status is returned  
    as the value of eval.  If there are no args, or only null  
    arguments, eval returns 0.
waltinator
sumber
8

Salah satu solusi untuk ini, untuk referensi di masa mendatang, adalah menggunakan "eval". Ini memastikan bahwa apa pun cara string ditafsirkan oleh bash dilupakan dan semuanya dibaca seolah-olah itu diketik langsung di shell (yang persis seperti yang kita inginkan).

Jadi pada contoh di atas, gantikan

$cmd

dengan

eval $cmd

memecahkannya.

Lennart Rolland
sumber
Hati-hati dengan parameter yang dikutip. eval foo "a b"akan sama dengan eval foo "a" "b".
udondan
2

@waltinator sudah menjelaskan mengapa ini tidak berfungsi seperti yang Anda harapkan. Cara lain untuk menggunakannya adalah bash -cdengan menjalankan perintah Anda:

$ comm="cat /etc/passwd"
$ comm+="| wc -l"
$ $comm
cat: invalid option -- 'l'
Try 'cat --help' for more information.
$ bash -c "$comm"
51
terdon
sumber
1
Parsimony memberitahu saya untuk tidak memulai proses lain bash -c, tetapi gunakan evaluntuk melakukan perintah dalam proses saat ini.
waltinator
@waltinator yakin, saya mungkin akan menggunakan eval untuk ini juga (itulah sebabnya saya terbalik Anda dan Lennart). Saya hanya memberikan alternatif.
terdon
0

Mungkin cara yang lebih baik untuk melakukan ini adalah untuk menghindari menggunakan evaldan hanya menggunakan array Bash dan itu perluasan inline untuk membangun semua argumen dan kemudian jalankan melawan perintah.

runcmd=() # This is slightly messier than declare -a but works
for cmd in $part1 $part2 $part3; do runcmd+="| $cmd "; done
cat infile ${runcmd[@]} # You might be able to do $basecmd ${runcmd[@]}
# but that sometimes requires an `eval` which isn't great
dragon788
sumber