اول که برنامهنویسی رو یاد میگیریم معمولا با ورودی و خروجی استاندارد سر و کار داریم، یعنی چی؟ یعنی "یه برنامه مینویسیم که یه عدد از کاربر بگیرد" و یه سری محاسبات انجام بده و "خروجی را چاپ کند".
توی این مطلب میخواهیم یه مقدار بیشتر به این مفاهیم بپردازیم.
برنامههایی که نوشته میشوند چند دسته کلی دارند که اگرچه شاید ما برای همشون برنامهنویسی نکرده باشیم یا هیچ وقت نکنیم، ولی باهاشون کار کردیم.
۱- برنامههای دسکتاپ این برنامهها روی کامپیوتر شما (مثلا ویندوزی یا لینوکسی یا هرچی) نصب میشوند و به صورت گرافیکی میتونید باهاشون کار کنید. مثلا همین مرورگری که الان بازه (اگر روی کامپیوتر هستید) یا برنامههای آفیس مثل Microsoft office یا ادیت تصویر مثل فتوشاپ و gimp و krita. این برنامهها عموما با یه زبانبرنامه نویسی و یه کتابخونه گرافیکی توسعه داده میشوند مثلا سیپلاسپلاس با QT یا پایتون با pyqt یا جاوا با javafx.
۲- برنامههای موبایل این برنامهها روی گوشی شما نصب میشوند و کار میکنند مثل هر برنامههایی که روزانه توی موبایلتون ازش استفاده میکنید از تلگرام تا توییتر و تاسکی و اسپاتیفای. بسته به اینکه گوشی اندروید باشه یا IOS، زبانهای متفاوتی استفاده میشوند مثلا از جاوا یا کاتلین برای اندروید یا swift برای IOS. همچنین می تونید از کتابخانههایی استفاده کنید که برنامه رو هم برای اندروید و هم برای IOS (و حتی وب) خروجی میدهند.
۳- برنامههای تحت وب این برنامهها در واقع سایتهایی هستند که ازشون بازدید میکنید مثل سایت gmail.com یا همین ویرگول دوست داشتنی.
۴- برنامههای خط فرمان این برنامهها رو معمولا کاربران عادی کمتر استفاده میکنند ولی اگر تا حالا cmd سیستم رو باز کردید و ping گرفتید یا ترمینال رو باز کردید و دستور زدید، از برنامههای خط فرمان استفاده کردید.
این برنامهها برخلاف باقی برنامهها، ظاهر گرافیکی جذابی ندارند (اگرچه میتونند خروجی رنگی داشته باشند ولی قطعا به زیبایی باقی برنامهها نیستند). اینکه چه کارهایی از این مدل برنامهها بر میاد تقریبا نامحدوده، از نقاشی کدن تا کانورت ویدیو و باز کردن صفحات وب و ارسال پیام تلگرام و ایمیل و تقویم و هرچیزی که فکرش رو بکنید.حتی نصب بسیاری از سیستمعامل ها (مثل arch linux) از طریق همین خط فرمان صورت میگیره.
اما این برنامهها چطوری کار میکنند؟ ۲ روش کلی کار دارند، یا در واقع ۲ شیوه تعامل با این رابط متنی.
اولی حالت سادهی cli هست که در این مطلب در مورد همین رابط میخواهیم بیشتر بخونیم. برنامه پینگ که بالا دیدیم از همین نوعه، اجرا میشه (و میتونه ورودی بگیره) و خروجی رو خط به خط چاپ میکنه. یا مثلا package managerها مثل chocolaty و apt و pacman از همین رابط استفاده میکنند
حالت دوم raw mode هست که رابط خط فرمان از حالت مرسوم خوندن ورودی و نوشتن خروجی خارج میشه و قابلیتهای بیشتری پیدا میکنه و میتونه مشابه یه برنامه گرافیکی المان های دکمه و قسمت تایپ نوشته داشته باشه. تنها تفاوتش با برنامههای گرافیکی نظیر اینه که توی خط فرمان اجرا میشه به جای باز شدن پنجره جدید. مثلا برنامه nmtui یه رابط خط فرمان با استفاده از raw mode برای مدیریت کانکشنهای شبکه هست. ادیتورهای متنی مثل vim و nano و micro هم از این دسته هستد.
نوشتن برنامههایی که مثل nmtui رابط tui داشته باشند، با استفاده از کتابخونههایی مثل ncurses برای سیپلاسپلاس یا lanterna برای جاوا امکان پذیره.
این رابط سادهترین رابط برای شروع کردن برنامهنویسی هست. تقریبا تمام زبانها قابلیت کار با این رابط رو به صورت پیشفرض دارند، مثلا در پایتون به راحتی نوشتن input() و print() میتونید با کاربر تعامل کنید و ورودی بگیرید و جواب چاپ کنید. مثلا برنامهی سادهی زیر که با پایتون نوشته شده:
print("please enter your name") name = input() print("hello " + name)
یک خط متن چاپ میکند و نام کاربر را ورودی میگیرد و خوشامدگویی را چاپ میکند.
پس از اجرا چنین چیزی روی صفحهی ترمینال (یا کامندلاین) میبینیم:
please enter your name roozbeh hello roozbeh
مثال نوشته شده در زبان پایتون بود، در زبانهای دیگر همین کارکرد با نامهای گوناگون وجود دارد مثلا scanf و printf یا cin و cout.
آیا ورودی و خروجی تفاوت دارند؟ البته! ۳ خط نوشتهی بالا که حاصل برنامهی پایتون بود عینا از ترمینال کپی شده، خط اول و سوم در واقع خروجی برنامه هستند (print) و خط وسط ورودی برنامه است برنامه دخالتی در تولیدش نداشته.
برای درک راحت میتونیم اینطوری نگاه کنیم که این یه برنامه چت هست و input ها رو ما وارد کردیم و printها رو طرف مقابل وارد کرده و همش رو در یک صفحه مشاهده می کنیم. اما این صفحه چت قابلیت این رو نداره (یا حداقل فعلا مکانیسمی بلد نیستیم) که پیام کاربر و پیام برنامه رو از هم جدا یا متفاوت نشون بده و این تصور رو در ما به وجود میاره که همش پیامهای برنامه هست، ولی ما که یادمون هست اون نوشتهی roozbeh رو ما وارد کردیم و پیام ماست و بقیه متن رو برنامه وارد کرده و پیامهای اونه.
برای درک بهتر اومدم و یکم ادیت گرافیکی(!) انجام دادم و حالا مشخصه که آبیها چیزیه که برنامه پرینت کرده و قرمزه چیزیه که کاربر (من) وارد کردم.
حالا که تفاوت ورودی و خروجی رو فهمیدیم، بد نیست یه مقدار مفهوم دقیقترش رو هم یاد بگیریم. همونطور که گفته شد، ورودی و خروجی اگرچه یک جا نوشته میشوند ولی به لحاظ منطقی کاملا متفاوت هستند و یه سریش پیامهای ماست و یه سریش پیام های برنامه. اسم درست پیامهای ما stdin هست و اسم درست پیامهای سیستم stdout. اون std به معنای standard هست یعنی ورودی استاندارد سیستم و خروجی استاندارد سیستم.
اینکه گفتیم فایل معنیش چیه؟ خیلی فهمیدن مفهومش اهمیتی نداره ولی چون توی لینوکس همهچیز فایله، این ورودی و خروجی استاندارد هم به شکل فایل هستند که وقتی ورودی جدیدی میاد اول فایل نوشته میشود و وقتی قرار است چیزی خوانده شود از آخر فایل خارج میشود. (مثل صف)
چرا لازمه فایل باشند؟ چرا نمیشه هرچی وارد میکنیم وارد بشه و حتما بای توی یه فایل به شکل صف ذخیره بشه؟
برنامه زیر رو در نظر بگیرید:
from time import sleep print("wait a second") sleep(1) print("now enter something") a = input() print("you entered " + a)
ابتدا به کاربر میگوید که یک ثانیه صبر کن و یک ثانیه منتظر میشود. سپس پیام میدهد که یک ورودی وارد کن و ورودی را میگیرد و در اخر یک خروجی چاپ میکند.
زمانی که برنامه در خط سوم، مشغول استراحت (sleep) هست، در واقع آماده ورودی گرفتن نیست، ولی آیا کاربر نمیتواند ورودی وارد کند؟ کیبرد کار نمیکند؟ چرا میتواند و کیبرد هم کار میکند. وقتی ورودی وارد شود در انتهای stdin نوشته میشود در حالی که هنوز برنامه نمی خواهد ورودی بگیرد ولی مشکلی نیست. این فایل مثل صف ورودیها را نگه میدارد تا هر زمان که برنامه درخواست کرد، ورودی را در اختیارش قرار میدهد.
حال در اجرای برنامه ۲ صورت ممکن است پیش بیاید. من به عنوان کاربر واقعا صبر کنم که این ۱ ثانیه sleep انجام شود و بعد ورودی را وارد کنم یا اینکه نه، صبر نکنم و قبل اینکه برنامه از من بخواهد چیزی وارد کنم، تایپ کنم.
زمانی که صبر نکردم، ورودی من در فایلِ stdin نوشته میشود تا پس از اتمام یک ثانیه برنامه درخواست کند و از فایل stdin بخواند.
در حالت اول اما اتفاق جالبتری میافتد. اینبار اول برنامه درخواست ورودی میکند ولی چون فایل stdin خالی است منتظر میشود تا کاربر یک خط ورودی وارد کند (و اینتر بزند) بعد ورودی جدید را به برنامه میدهد. در این صورت اصطلاحا برنامه block شده تا کاربر ورودی وارد کند.
همان طور که در تصورها میبینید که ۲ حالت متفاوت برای حالت نهایی ترمینال داریم ولی الان ما تفاوت stdin و stdout را میدانیم و میدانیم که به صورت منطقی ۲ فایل جدا هستند و در هر ۲ حالت محتویاتشان این هاست:
#stdin a #stdout wait for a second now enter something you entered a
جمله کلیدی تا اینجا این است که اگرچه ورودی و خروجی را یکجا در ترمینال میبینیم ولی در واقع ۲ فایل جدای stdin و stdout هستند.
در ویکیپیدا بیشتر بخوانید
بسیاری از ما برای برنامهنویسی در مسابقات آنلاین شرکت میکنیم و یا تکالیفمان را در یک judge مثلا quera.ir بارگذاری میکنیم. این جاجها دقیقا ورودی و خروجی برنامه ما را به شکل stdin میدهند و خروجی را از stdout میگیرند و چک میکنند. پس اینکه اول همه خروجیها را دریافت کنید و بعد همه خروجیها را چاپ کنید یا اینکه یک خط ورودی بگیرید و یک خط خروجی دهید یا هر ترکیب دیگری هیچ تفاوتی برایشان ندارد. در واقع اصلا در میان برنامه شما با ورودی و خروجی کاری ندارند، فقط اول برنامه همه ورودی را میدهند و پس از اتمام برنامه همهی خروجی را میخوانند و چک میکنند.
اما جاجها چطوری برنامههای ما را داوری میکنند؟ آیا یک نفر نشسته و به سرعت ورودیها را تایپ میکند و خروجی را بررسی میکند؟ البته که نه. رابط خط فرمان، قابلیتهای خیلی خوبی برای اینکار میدهد. شما میتوانید یک برنامه را اجرا کنید و تنظیم کنید که ورودی اش به جای اینکه مستقیم از کاربر (فایل stdin اصلی) خوانده شود، از یک فایل دیگر خوانده شود، یعنی عملا یک فایل دیگر (مثلا input1.txt را به ورودی برنامه redirect کردهاید) در این حالت دیگر ورودی به صورت blocking نیست چون هر ورودیای قرار باشد برنامه دریافت کند در همین فایل هست.
اگر به انتهای فایل برسد و هنوز هم درخواست ورودی کند؟ اتفاق خوبی نمیافتد مثلا در جاوا، اسکنری که ورودی میگیرد استثنای NoSuchElementException پرتاپ میکند و پایتون ارور EOFError: EOF when reading a line میدهد. در سیپلاسپلاس اتفاق جالبی نمیافتد و متغیر مربوطه مقدار جدید نمیگیرد و مقدار قبلیاش را حفظ میکند.
با دستورات echo و pipe کردن یا عملگر > میتوانید در ترمینال ورودی را از یک فایل بخوانید یا یک متن از اول بدهید.
برای سیو کردن خروجی در یک فایل هم میتوانید از عملگر < استفاده کنید.
اطلاعات بیشتر در این مطلب
تا اینجا با مفهوم stdout و stdin و مثال پیام و چت آشنا شدیم. حالا بیایید فرض کنیم که کامپیوتر در واقع ۲ تا اکانت دارد! یک اکانت برای پیامهای رسمی و درست، یه اکانت برای پیامهای خودمانی. باز هم این اکانت در چت کاملا مشابه stdin و stderr ظاهر میشود ولی تفاوتش این است که در موقع فرستادن output به یک فایل (مثلا با همین عملگر <) فقط stdout در فایل ریخته میشود و stderr همچنان مستقیم چاپ میشود.
این چه کمکی به ما میکند؟ اگر مثل من برنامهنویس نامرتبی باشید و از print برای دیباگ کردن استفاده کنید، حتما پیش میاد که فایلی رو برای جاج ارسال کردید که شامل print های اضافی بوده، در این صورت برنامه رانگ میخورد چون پرینتهای اضافی هم به عنوان خروجی در نظر گرفته میشوند. برای اینکار میتوانیم از این یار کمکی (اکانت دوم!) استفاده کنیم و پرینتهایی که به دیباگ مربوط هستند و جزو خروجی اصلی برنامه نیستند را در stderr چاپ کنید. جاجها اگرچه هنوز قادرند که stderr را هم (با یه مکانیسم متفاوت) در فایل بریزند و ارزیابی کنند ولی هیچگاه این کار را انجام نمیدهند و فقط stdout را بررسی میکنند.
برنامههای cli واقعی (که با هدف جاج نوشته نشدهاند) هم بنا به نیاز، خروجیهای خودمانی و ارورها را در این فایل چاپ میکنند.
زبانهای مختلف راههای متفاوتی اغلب شبیه به print کردن دارند. مثلا در سیپلاسپلاس در مقابل cout که در stdout چاپ می کند، cerr را داریم که با سینتکس مشابه در stderr چاپ میکند. (حالا فکر کنم معنی اسم cin و cout رو هم فهمیدید)
در زبان سی از fprintf استفاده می کنید که وظیفهاش نوشتن در فایلهاست. اما چه فایلی؟ stderr
fprintf( stderr, "my %s has %d chars\n", "string format", 30);
در پایتون چندین راه وجود دارد که میتوانید اینجا مطالعه کنید ولی من به شخصه از این راه استفاده میکنم:
import sys print("fatal error", file=sys.stderr)
برای جاوا نیز از
System.err.println("fatal error");
میتوان استفاده کرد.
اگر همه متن رو خوندید خسته نباشید! مثل همیشه نظر/انتقاد/پیشنهاد آزاده.