Apakah mungkin bagi suatu program untuk mendapatkan jumlah spasi di antara argumen baris perintah di POSIX?

23

Katakanlah jika saya menulis sebuah program dengan baris berikut:

int main(int argc, char** argv)

Sekarang ia tahu argumen baris perintah apa yang diteruskan dengan memeriksa konten argv.

Bisakah program mendeteksi berapa banyak spasi di antara argumen? Seperti saat saya mengetik ini di bash:

ibug@linux:~ $ ./myprog aaa bbb
ibug@linux:~ $ ./myprog       aaa      bbb

Lingkungan adalah Linux modern (seperti Ubuntu 16.04), tetapi saya kira jawabannya harus berlaku untuk sistem yang mendukung POSIX.

iBug
sumber
22
Hanya untuk rasa ingin tahu, mengapa program Anda perlu mengetahuinya?
nxnev
2
@ nxnev Saya dulu menulis beberapa program Windows dan saya tahu itu mungkin di sana, jadi saya ingin tahu apakah ada sesuatu yang serupa di Linux (atau Unix).
iBug
9
Saya samar-samar ingat di CP / M bahwa program harus mengurai baris perintah mereka sendiri - ini berarti bahwa setiap runtime C harus mengimplementasikan pengurai shell. Dan mereka semua melakukannya dengan sedikit berbeda.
Toby Speight
3
@ iBug Ada, tetapi Anda perlu mengutip argumen saat menjalankan perintah. Begitulah cara ini dilakukan pada shell POSIX (dan sejenisnya).
Konrad Rudolph
3
@ iBug, ... Windows memiliki desain yang sama dengan yang disebutkan Toby dari CP / M di atas. UNIX tidak melakukan itu - dari perspektif yang disebut proses ini, ada adalah tidak ada baris perintah yang terlibat dalam menjalankannya.
Charles Duffy

Jawaban:

39

Tidak ada artinya berbicara tentang "ruang antar argumen"; itu konsep shell.

Tugas shell adalah mengambil seluruh baris input dan membentuknya menjadi array argumen untuk memulai perintah. Ini mungkin melibatkan parsing string yang dikutip, memperluas variabel, file wildcard dan ekspresi tilde, dan banyak lagi. Perintah dimulai dengan execpanggilan sistem standar , yang menerima vektor string.

Ada cara lain untuk membuat vektor string. Banyak program bercabang dan mengeksekusi sub-proses mereka sendiri dengan permintaan perintah yang telah ditentukan - dalam hal ini, tidak pernah ada yang namanya "baris perintah". Demikian pula, shell grafis (desktop) mungkin memulai proses ketika pengguna menyeret ikon file dan menjatuhkannya pada widget perintah - sekali lagi, tidak ada baris teks untuk memiliki karakter "antara" argumen.

Sejauh menyangkut perintah yang dipanggil, apa yang terjadi di shell atau proses orangtua / prekursor lainnya bersifat pribadi dan tersembunyi - kita hanya melihat array string yang ditentukan oleh standar C yang main()dapat diterima.

Toby Speight
sumber
Jawaban yang baik - penting untuk menunjukkan ini untuk pemula Unix, yang sering berasumsi bahwa, jika mereka menjalankan tar cf texts.tar *.txtmaka program tar mendapat dua argumen dan harus memperluas yang kedua ( *.txt) itu sendiri. Banyak orang tidak menyadari cara kerjanya hingga mereka mulai menulis skrip / program mereka sendiri yang menangani argumen.
Laurence Renshaw
58

Secara umum, tidak. Penguraian baris perintah dilakukan oleh shell yang tidak membuat garis yang tidak diuraikan tersedia untuk program yang dipanggil. Bahkan, program Anda dapat dieksekusi dari program lain yang menciptakan argumen bukan dengan menguraikan string tetapi dengan membangun array argumen secara terprogram.

Hans-Martin Mosner
sumber
9
Anda mungkin ingin menyebutkan execve(2).
iBug
3
Anda benar, sebagai alasan lemah saya dapat mengatakan bahwa saya saat ini menggunakan telepon dan mencari halaman manual agak membosankan :-)
Hans-Martin Mosner
1
Ini adalah bagian yang relevan dari POSIX.
Stephen Kitt
1
@ Hans-MartinMosner: Termux ...? ;-)
DevSolar
9
"secara umum" dimaksudkan sebagai perlindungan terhadap mengutip kasus berbelit-belit khusus di mana dimungkinkan - misalnya, proses root suid mungkin dapat memeriksa memori shell panggilan dan menemukan string baris perintah yang tidak diuraikan.
Hans-Martin Mosner
16

Tidak, ini tidak mungkin, kecuali spasi merupakan bagian dari argumen.

Perintah mengakses argumen individu dari array (dalam satu bentuk atau lainnya tergantung pada bahasa pemrograman) dan baris perintah yang sebenarnya dapat disimpan ke file histori (jika diketik pada prompt interaktif di shell yang memiliki file histori), tetapi tidak pernah meneruskan perintah dalam bentuk apa pun.

Semua perintah pada Unix pada akhirnya dijalankan oleh salah satu exec()keluarga fungsi. Ini mengambil nama perintah dan daftar atau array argumen. Tak satu pun dari mereka mengambil baris perintah seperti yang diketik di prompt shell. The system()Fungsi tidak, tapi argumen string yang kemudian dieksekusi oleh execve(), yang, sekali lagi, mengambil array argumen bukan string baris perintah.

Kusalananda
sumber
2
@LightnessRacesinOrbit saya taruh di sana kalau-kalau ada beberapa kebingungan tentang "spasi antar argumen". Menempatkan spasi dalam tanda kutip di antara hellodan secara harfiahworld adalah spasi di antara dua argumen.
Kusalananda
5
@Kusalananda - Nah, tidak ada ... Puting spasi dalam tanda kutip antara hellodan worldadalah harfiah memasok kedua dari tiga argumen.
Jeremy
@ Jeremy Seperti yang saya katakan, kalau-kalau ada kebingungan tentang apa yang dimaksud dengan "antara argumen". Ya, sebagai argumen kedua di antara dua lainnya jika Anda mau.
Kusalananda
Teladan Anda baik-baik saja, dan instruktif.
Jeremy
1
Nah, teman-teman, contoh-contoh itu jelas merupakan sumber kebingungan dan kesalahpahaman. Saya telah menghapusnya karena tidak menambah nilai jawaban.
Kusalananda
9

Secara umum, itu tidak mungkin, seperti beberapa jawaban lain yang dijelaskan.

Namun, shell Unix adalah program biasa (dan mereka menginterpretasikan baris perintah dan menggumpalkannya , yaitu memperluas perintah sebelum melakukan fork& execveuntuk itu). Lihat penjelasanbash ini tentang operasi shell . Anda dapat menulis shell Anda sendiri (atau Anda dapat menambal beberapa shell perangkat lunak gratis yang ada , misalnya GNU bash ) dan menggunakannya sebagai shell Anda (atau bahkan shell login Anda, lihat passwd (5) & shells (5) ).

Sebagai contoh, Anda mungkin memiliki program shell Anda sendiri meletakkan baris perintah penuh dalam beberapa variabel lingkungan (bayangkan MY_COMMAND_LINEmisalnya) -atau menggunakan jenis lain komunikasi antar-proses untuk mengirimkan baris perintah dari shell ke proses anak-.

Saya tidak mengerti mengapa Anda ingin melakukan itu, tetapi Anda mungkin membuat kode shell berperilaku sedemikian rupa (tapi saya sarankan tidak melakukannya).

BTW, suatu program dapat dimulai oleh beberapa program yang bukan shell (tetapi yang melakukan fork (2) kemudian mengeksekusi (2) , atau hanya execvememulai program dalam proses saat ini). Dalam hal ini tidak ada baris perintah sama sekali, dan program Anda dapat dimulai tanpa perintah ...

Perhatikan bahwa Anda mungkin memiliki beberapa sistem Linux (khusus) tanpa shell yang terpasang. Ini aneh dan tidak biasa, tetapi mungkin. Anda kemudian harus menulis program init khusus memulai program lain sesuai kebutuhan - tanpa menggunakan shell apa pun tetapi dengan melakukan fork& execvepanggilan sistem.

Baca juga Sistem Operasi: Tiga potong mudah dan jangan lupa bahwa execvepraktis selalu merupakan panggilan sistem (di Linux, mereka terdaftar di syscalls (2) , lihat juga intro (2) ) yang menginisialisasi ulang ruang alamat virtual (dan beberapa lainnya) hal) dari proses melakukannya.

Basile Starynkevitch
sumber
Ini jawaban terbaik. Saya berasumsi (tanpa melihat ke atas) bahwa argv[0] untuk nama program dan elemen yang tersisa untuk argumen adalah spesifikasi POSIX dan tidak dapat diubah. Lingkungan runtime dapat menentukan argv[-1]untuk baris perintah, saya berasumsi ...
Peter - Reinstate Monica
Tidak, itu tidak bisa. Baca execvedokumentasi dengan lebih cermat . Anda tidak dapat menggunakan argv[-1], itu adalah perilaku yang tidak ditentukan untuk menggunakannya.
Basile Starynkevitch
Ya, poin bagus (juga petunjuk bahwa kita memiliki syscall) - idenya agak dibuat-buat. Ketiga komponen runtime (shell, stdlib dan OS) perlu berkolaborasi. Shell perlu memanggil execvepluscmdfungsi non-POSIX khusus dengan parameter tambahan (atau konvensi argv), syscall membangun vektor argumen untuk utama yang berisi pointer ke baris perintah sebelum pointer ke nama program, dan kemudian meneruskan alamat dari penunjuk ke nama program seperti argvketika memanggil program main...
Peter - Reinstate Monica
Tidak perlu menulis ulang shell, cukup gunakan tanda kutip. Fitur ini tersedia dari shell bourn sh. Jadi bukan hal baru.
ctrl-alt-delor
Menggunakan tanda kutip perlu mengubah baris perintah. Dan OP tidak menginginkan itu
Basile Starynkevitch
3

Anda selalu dapat memberi tahu shell Anda untuk memberi tahu aplikasi apa yang menyebabkan kode shell dijalankan. Misalnya, dengan zsh, dengan meneruskan informasi itu dalam $SHELL_CODEvariabel lingkungan menggunakan preexec()hook ( printenvdigunakan sebagai contoh, Anda akan menggunakannya getenv("SHELL_CODE")dalam program Anda):

$ preexec() export SHELL_CODE=$1
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv  SHELL_CODE
printenv  CODE
$ $(echo printenv SHELL_CODE)
$(echo printenv SHELL_CODE)
$ for i in SHELL_CODE; do printenv "$i"; done
for i in SHELL_CODE; do printenv "$i"; done
$ printenv SHELL_CODE; : other command
printenv SHELL_CODE; : other command
$ f() printenv SHELL_CODE
$ f
f

Semua itu akan dieksekusi printenvsebagai:

execve("/usr/bin/printenv", ["printenv", "SHELL_CODE"], 
       ["PATH=...", ..., "SHELL_CODE=..."]);

Mengizinkan printenvuntuk mengambil kode zsh yang mengarah ke eksekusi printenvdengan argumen tersebut. Apa yang ingin Anda lakukan dengan informasi itu tidak jelas bagi saya.

Dengan bash, fitur yang paling dekat dengan zsh's preexec()akan menggunakan nya $BASH_COMMANDdalam DEBUGperangkap, tetapi catatan bahwa bashmelakukan beberapa tingkat menulis ulang dalam (dan di refactors khususnya beberapa spasi digunakan sebagai pembatas) dan bahwa ini diterapkan pada setiap (baik, beberapa) perintah jalankan, bukan seluruh baris perintah seperti yang dimasukkan pada prompt (lihat juga functraceopsi).

$ trap 'export SHELL_CODE="$BASH_COMMAND"' DEBUG
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv $(echo 'SHELL_CODE')
printenv $(echo 'SHELL_CODE')
$ for i in SHELL_CODE; do printenv "$i"; done; : other command
printenv "$i"
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printf '%s\n' "$(printenv "SHELL_CODE")"
$ set -o functrace
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printenv "SHELL_CODE"
$ print${-+env  }    $(echo     'SHELL_CODE')
print${-+env  } $(echo     'SHELL_CODE')

Lihat bagaimana beberapa spasi yang merupakan pembatas dalam sintaksis bahasa shell telah diperas menjadi 1 dan bagaimana tidak, baris perintah penuh tidak selalu diteruskan ke perintah. Jadi mungkin tidak berguna dalam kasus Anda.

Perhatikan bahwa saya tidak akan menyarankan melakukan hal semacam ini, karena Anda berpotensi membocorkan informasi sensitif ke setiap perintah seperti pada:

echo very_secret | wc -c | untrustedcmd

akan membocorkan rahasia itu untuk keduanya wcdan untrustedcmd.

Tentu saja, Anda bisa melakukan hal semacam itu untuk bahasa lain selain shell. Misalnya, dalam C, Anda bisa menggunakan beberapa makro yang mengekspor kode C yang mengeksekusi perintah ke lingkungan:

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define WRAP(x) (setenv("C_CODE", #x, 1), x)

int main(int argc, char *argv[])
{
  if (!fork()) WRAP(execlp("printenv", "printenv", "C_CODE", NULL));
  wait(NULL);
  if (!fork()) WRAP(0 + execlp("printenv",   "printenv", "C_CODE", NULL));
  wait(NULL);
  if (argc > 1 && !fork()) WRAP(execvp(argv[1], &argv[1]));
  wait(NULL);
  return 0;
}

Contoh:

$ ./a.out printenv C_CODE
execlp("printenv", "printenv", "C_CODE", NULL)
0 + execlp("printenv", "printenv", "C_CODE", NULL)
execvp(argv[1], &argv[1])

Lihat bagaimana beberapa ruang dikondensasi oleh prosesor pra-C seperti dalam kasus bash. Dalam sebagian besar, jika tidak semua bahasa, jumlah ruang yang digunakan dalam pembatas tidak membuat perbedaan, jadi tidak mengherankan bahwa kompiler / penerjemah mengambil kebebasan di sini.

Stéphane Chazelas
sumber
Ketika saya menguji ini, BASH_COMMANDtidak mengandung argumen pemisahan spasi putih asli, jadi ini tidak dapat digunakan untuk permintaan literal OP. Apakah jawaban ini mencakup demonstrasi apa pun untuk kasus penggunaan tertentu?
Charles Duffy
@CharlesDuffy, saya hanya ingin menunjukkan padanan terdekat dari zsh's preexec () di bash (karena itu shell yang dimaksud OP) dan menunjukkan bahwa itu tidak dapat digunakan untuk kasus penggunaan tertentu, tapi saya setuju itu tidak sangat jelas. Lihat edit. Jawaban ini dimaksudkan untuk menjadi lebih umum tentang cara melewatkan kode sumber (di sini di zsh / bash / C) yang menyebabkan eksekusi ke perintah dieksekusi (bukan sesuatu yang berguna, tapi saya berharap saat melakukannya, dan terutama dengan contoh-contohnya, saya menunjukkan bahwa itu tidak terlalu berguna)
Stéphane Chazelas
0

Saya hanya akan menambahkan apa yang hilang di jawaban lain.

Tidak

Lihat jawaban lain

Mungkin semacam

Tidak ada yang bisa dilakukan dalam program, tetapi ada sesuatu yang bisa dilakukan di shell ketika Anda menjalankan program.

Anda perlu menggunakan kutipan. Jadi, bukannya

./myprog      aaa      bbb

Anda perlu melakukan salah satunya

./myprog "     aaa      bbb"
./myprog '     aaa      bbb'

Ini akan memberikan argumen tunggal ke program, dengan semua spasi. Ada perbedaan antara keduanya, yang kedua adalah literal, persis string yang muncul (kecuali yang 'harus diketikkan sebagai \'). Yang pertama akan menginterpretasikan beberapa karakter, tetapi dipecah menjadi beberapa argumen. Lihat kutipan shell untuk informasi lebih lanjut. Jadi tidak perlu menulis ulang shell, desainer shell sudah memikirkan itu. Namun karena sekarang menjadi satu argumen, Anda harus melakukan lebih banyak passing dalam program.

pilihan 2

Lulus data melalui stdin. Ini adalah cara normal untuk mendapatkan sejumlah besar data ke dalam suatu perintah. misalnya

./myprog << EOF
    aaa      bbb
EOF

atau

./myprog
Tell me what you want to tell me:
aaaa bbb
ctrl-d

(Miring adalah hasil dari program)

ctrl-alt-delor
sumber
Secara teknis, kode shell: ./myprog␣"␣␣␣␣␣aaa␣␣␣␣␣␣bbb"mengeksekusi (umumnya dalam proses anak) file yang disimpan dalam ./myprogdan meneruskannya dua argumen: ./myprogdan ␣␣␣␣␣aaa␣␣␣␣␣␣bbb( argv[0]dan argc[1], argcmenjadi 2) dan seperti dalam OP, ruang yang memisahkan kedua argumen tersebut tidak diteruskan dengan cara apa pun untuk myprog.
Stéphane Chazelas
Tetapi Anda mengubah perintah, dan OP tidak ingin mengubahnya
Basile Starynkevitch
@ BasileStarynkevitch Mengikuti komentar Anda, saya membaca pertanyaan itu lagi. Anda membuat asumsi. OP mana pun mengatakan bahwa mereka tidak ingin mengubah cara program dijalankan. Mungkin ini benar, tetapi mereka tidak mengatakan apa-apa tentang itu. Karenanya jawaban ini mungkin yang mereka butuhkan.
ctrl-alt-delor
OP bertanya secara eksplisit tentang spasi antar argumen, bukan tentang satu argumen tunggal yang mengandung spasi
Basile Starynkevitch